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 协议层核心知识结构 DSI 包结构 短包 (4 字节) 长包 (可变长) DI (VC + DT) WC (固定 0x00) ECC (纠错码) 包头 (DI + WC + ECC) 数据载荷 (N 字节) CRC (16 位校验) 虚拟通道 VC 数据类型 DT ECC / CRC 短包传控制,长包传数据;VC 区分通道,DT 定义类型,ECC/CRC 保完整 三者配合,构成 DSI 协议层的完整通信模型

小结

DSI 协议层的内容就这些。短包和长包是基础,DT 告诉你怎么解析,VC 让你能复用总线,ECC 和 CRC 保证不出错。这四个概念搞清楚了,DSI 协议层就算入门了。

我个人觉得,协议层是 DSI 里最容易出 bug 的地方——不是因为它难,而是因为细节多。一个字节配错,整屏数据就乱了。所以写代码时,建议把 DT 表打印出来贴在工位上,随时对照。