DSI 协议层:短包与长包结构、数据类型定义、虚拟通道机制、ECC与CRC校验
DSI 协议层,说白了就是 MIPI DSI 接口的“语言规范”。你硬件上把差分信号线连好了,但数据怎么组织、怎么区分命令还是图像、怎么保证不出错——这些都得靠协议层来定。我刚开始接触 DSI 时,总觉得物理层搞定就万事大吉,结果第一次调屏,图像花得一塌糊涂,查了半天才发现是包结构没对齐。嗯,从那以后我再也不敢小看协议层了。
4.1 短包与长包:DSI 的两种“信封”
DSI 协议里,所有数据都装在包里。包分两种:短包和长包。短包固定 4 字节,长包则可变长,最大能到 65541 字节。
核心区别一句话:短包传控制信息(命令、状态),长包传批量数据(图像、大块配置)。
短包结构
短包只有 4 个字节,结构极其紧凑:
| 字节偏移 | 内容 | 说明 |
|---|---|---|
| Byte 0 | DI (Data Identifier) | VC[1:0] + DT[5:0] |
| Byte 1 | WC (Word Count) 低字节 | 短包中固定为 0x00 |
| Byte 2 | ECC (Error Correction Code) | 6 位校验 + 2 位奇偶 |
| Byte 3 | ECC 续 / 保留 | 实际 ECC 覆盖前 2 字节 |
你可能会问:短包的数据载荷呢?其实短包不传数据,它的“数据”就是命令本身。比如关机命令、进入睡眠模式、读寄存器——这些都用短包搞定。
长包结构
长包就复杂多了。我习惯把它拆成三段:包头、数据、包尾。
+--------+--------+--------+--------+--------+--------+--------+--------+
| DI | WC_L | WC_H | ECC | Data[0..N-1] | CRC_L | CRC_H |
+--------+--------+--------+--------+--------+--------+--------+--------+
1 Byte 1 Byte 1 Byte 1 Byte N Bytes 1 Byte 1 Byte
- DI (Data Identifier):1 字节,高 2 位是虚拟通道号,低 6 位是数据类型。
- WC (Word Count):2 字节,小端序,表示数据载荷的字节数。范围 0~65535。
- ECC:1 字节,保护包头(DI + WC)的完整性。
- Data:N 字节,真正的数据载荷。
- CRC:2 字节,校验整个数据载荷的完整性。
我的经验:长包的 WC 如果填 0,表示这是一个空数据包。有些屏在初始化序列里会发空包做同步,别漏了处理。
4.2 数据类型 (DT) 定义:包的身份标识
DI 字节的低 6 位就是 DT(Data Type)。MIPI 联盟定义了一整套 DT 值,我挑几个最常用的列出来:
| DT 值 (Hex) | 名称 | 包类型 | 用途 |
|---|---|---|---|
| 0x01 | DT_SHUTDOWN | 短包 | 关闭显示 |
| 0x05 | DT_TURN_ON | 短包 | 开启显示 |
| 0x0C | DT_DCS_LONG_WRITE | 长包 | 写 DCS 命令 + 参数 |
| 0x29 | DT_PIXEL_RGB888 | 长包 | 24 位真彩色像素数据 |
| 0x2E | DT_PIXEL_RGB565 | 长包 | 16 位高彩色像素数据 |
| 0x37 | DT_GEN_SHORT_WRITE_1P | 短包 | 通用短写,带 1 字节参数 |
DT 值决定了接收端怎么解析这个包。比如收到 0x29,就知道后面跟着的是 RGB888 像素,直接往帧缓冲里填就行。收到 0x05,就知道是开屏命令,不需要数据载荷。
我曾经踩过的坑:有个项目里,我把 DT_PIXEL_RGB888 (0x29) 误写成了 DT_PIXEL_RGB565 (0x2E)。结果屏虽然亮了,但颜色完全不对,红色变成紫色。查了两天才发现是 DT 配错了。所以 DT 值一定要跟屏厂规格书对清楚。
4.3 虚拟通道 (VC) 机制:一条总线,多路数据
虚拟通道是 DSI 协议里一个很巧妙的设计。它允许在同一条物理链路上,同时传输最多 4 路独立的数据流。每个包头的 DI 字节高 2 位就是 VC 号,范围 0~3。
为什么需要 VC?我举个实际场景:
- VC0:主显示数据流(RGB 像素)
- VC1:触摸屏数据(通过 DSI 回传)
- VC2:摄像头预览数据(画中画)
- VC3:调试日志输出
接收端(比如显示驱动芯片)会根据 VC 号,把数据分发到不同的处理模块。这样一条 DSI 总线就能干四件事,省了额外的接口引脚。
关键点:VC 是逻辑上的通道,不是物理上的。所有 VC 的数据还是走同一对差分线,靠包头里的 VC 字段来区分。
我在一个多屏拼接项目里用过 VC 机制。主屏用 VC0,副屏用 VC1,两个屏共用一条 DSI 总线。只要在驱动芯片里配好 VC 过滤规则,数据就不会串。嗯,这个方案省了不少 PCB 走线空间。
4.4 ECC 与 CRC 校验:数据完整性的两道防线
高速传输中,信号受干扰是难免的。DSI 协议用两种校验来保证数据不出错:ECC 保护包头,CRC 保护数据载荷。
ECC(纠错码)
ECC 覆盖包头的前 2 个字节(DI + WC)。它采用汉明码,能纠正 1 位错误,检测 2 位错误。ECC 字节的生成算法如下:
// ECC 生成伪代码(简化版)
// 输入:data[0] = DI, data[1] = WC_L, data[2] = WC_H
// 输出:ecc_byte
bit0 = data[0] ^ data[1] ^ data[2];
bit1 = data[0] ^ data[1];
bit2 = data[0] ^ data[2];
// ... 实际有 6 个校验位 + 2 个奇偶位
ecc_byte = (parity << 6) | (check_bits << 0);
接收端收到包头后,重新计算 ECC,跟包头里的 ECC 对比。如果一致,说明包头没坏。如果有 1 位错误,ECC 能自动纠正。如果有 2 位错误,ECC 会报错,接收端可以请求重传。
我的建议:FPGA 实现 ECC 时,直接用查找表比实时计算快得多。把 256 种可能的 DI+WC 组合的 ECC 值预存好,查表就行。我在一个 4K@60Hz 的项目里这么干过,时序轻松过。
CRC(循环冗余校验)
CRC 保护长包的数据载荷部分。DSI 协议用的是 CRC-16,多项式是 x^16 + x^12 + x^5 + 1。CRC 放在长包的末尾,占 2 个字节。
CRC 的计算范围是从数据载荷的第一个字节到最后一个字节。包头和 ECC 不参与 CRC 计算。
// CRC-16 生成伪代码
// 输入:data[] 数据载荷数组,len 数据长度
// 输出:crc_value (16 位)
crc = 0xFFFF;
for (i = 0; i < len; i++) {
crc ^= data[i];
for (j = 0; j < 8; j++) {
if (crc & 0x0001) {
crc = (crc >> 1) ^ 0x8408; // 多项式反转
} else {
crc = crc >> 1;
}
}
}
crc_value = ~crc;
注意:CRC 只校验数据载荷,不校验包头。所以包头坏了靠 ECC,数据坏了靠 CRC。两者分工明确,缺一不可。
知识结构总览
下面这张图把 DSI 协议层的核心要素串起来了。我画图时习惯把包结构放在中间,因为它是协议层的基础。
小结
DSI 协议层的内容就这些。短包和长包是基础,DT 告诉你怎么解析,VC 让你能复用总线,ECC 和 CRC 保证不出错。这四个概念搞清楚了,DSI 协议层就算入门了。
我个人觉得,协议层是 DSI 里最容易出 bug 的地方——不是因为它难,而是因为细节多。一个字节配错,整屏数据就乱了。所以写代码时,建议把 DT 表打印出来贴在工位上,随时对照。