第4章 长包(Long Packet)结构总览:包头部、数据段、包尾部
长包是 MIPI CSI-2 传输数据的主力。我之前刚接手 CSI-2 项目时,觉得短包简单,长包无非是多传点数据。后来发现,长包才是调试的深水区。它的结构非常规整,分为三块:包头部(PH)、数据段(Data)、包尾部(PF)。每一块都有自己的职责,缺一不可。
4.1 长包的整体框架
一个完整的长包,用一句话说就是:“先告诉接收方我要发什么类型的数据、发多少字节,然后发数据,最后给个校验值确认数据没被篡改。”我习惯把 PH 看成“快递面单”,数据段是“包裹里的货”,PF 是“签收单”。面单写错了,包裹就送错地方;货少了或多了,接收方就懵了;签收单不对,说明路上出了事。
核心结构示意:
- 包头部(PH):4 字节,固定长度。包含数据标识符(DI,1 字节)、字计数(WC,2 字节)、ECC(1 字节)。
- 数据段(Data):长度不固定,由 WC 决定。字节数必须是 2 的倍数。
- 包尾部(PF):2 字节,固定长度。就是 CRC 校验和。
注意,长包之间必须用短包(帧开始/帧结束/行开始/行结束)隔开。我见过一个新人把长包一个个连在一起发,结果接收端根本找不到边界。嗯,这不是长包的问题,是协议层没吃透。
4.2 包头部(PH)深度拆解
PH 就是长包的“门牌号”。我从高位到低位给你拆开看。
4.2.1 数据标识符(DI)
DI 占 1 字节,实际上由 虚拟通道 ID(VC,高 2 位) 和 数据类型(DT,低 6 位) 组成。VC 范围 0~3,用来区分同一物理链路上的不同数据流。DT 告诉接收方这一包是 RAW8、RAW10、YUV422 还是 JPEG 等。
我记得有一次调试,图像全是花的,排查了半天,发现是发送端把 DT 写成了 RAW12,但接收端配成了 RAW8。数据长度对不上,像素错位。所以 DI 一定要跟 sensor 的输出格式严格匹配。
| 位域 | 宽度 | 含义 |
|---|---|---|
| Bit[7:6] | 2 | 虚拟通道 ID(VC) |
| Bit[5:0] | 6 | 数据类型(DT) |
4.2.2 字计数(WC)
WC 占 2 字节,小端序。表示数据段包含的字节数。取值范围跟数据类型有关:RAW8 一个像素 1 字节,RAW10 一个像素 1.25 字节但必须凑整成 2 字节倍数。WC 最小是 0(空包,但极少用),最大是 2^16 - 1,也就是 65535 字节。
常见陷阱:
- WC 计的是数据段的字节数,不包括 PH 和 PF。我见过有人把整个包长度写到 WC 里,导致接收端多读 6 个字节,CRC 永远算不对。
- 数据段字节数必须是 2 的倍数。如果 sensor 输出奇数个像素(比如一行 639 个 RAW8 像素),必须填充 1 个字节到 640。我曾经处理过一款老 sensor,它不填充,我只好在 FPGA 里硬补。
4.2.3 ECC(纠错码)
ECC 占 1 字节,覆盖 PH 的前 3 个字节(DI + WC 高字节 + WC 低字节)。它能纠正 1 比特错误,检测 2 比特错误。调试时我经常用逻辑分析仪抓 PH,如果 ECC 不对,直接丢掉整包。有些 sensor 出厂时序不稳定,偶尔丢 1 个 bit,ECC 就起作用了。
ECC 的生成算法是汉明码的一种变体,我建议直接用标准查表法实现,别自己手撸多项式——我试过,跑出来跟参考设计差一个位,查了两天才找到 bug。
4.3 数据段(Data)
数据段就是实打实的图像数据。按 WC 规定的字节数排列,遵循 小端序 和 LSB first 传输原则。比如一个 16 位像素,先传低字节再传高字节。
对于 RAW 格式,数据段内部没有额外包头,直接是像素灰度值。对于 YUV 或 RGB,也是连续排布。JPEG 压缩图像则把整帧打包在一个长包里(称为 JPEG 长包),WC 就是 JPEG 文件的总字节数。
调试技巧:
- 用逻辑分析仪抓一段长包,先手动解析 PH 的 WC,再检查数据段长度是否匹配。我经常拿一小段已知图像(比如黑帧或灰阶图)来验证数据段内容。
- 数据段最后两个字节有时会被误读为 PF,所以务必根据 WC 精确切割。
4.4 包尾部(PF)
PF 只有 2 字节,是数据段的 CRC 校验和。多项式是 0x1021(x^16 + x^12 + x^5 + 1),初始值 0x0000,不需要取反。计算范围从数据段的第一个字节到最后一个字节。
我踩过最深的一个坑:sensor 输出的 CRC 是正确计算的,但接收端我用了 SPI 模式的 CRC 引擎(初始值 0xFFFF),怎么都对不上。后来发现 CSI-2 协议要求初始值 0x0000,而且不做取反。改过来就通了。
// 简化的 CRC-16/CCITT-FALSE 实现(多项式 0x1021,初始 0x0000)
uint16_t csi2_crc16(uint8_t *data, uint32_t len) {
uint16_t crc = 0x0000;
for (uint32_t i = 0; i < len; i++) {
crc ^= (uint16_t)data[i] << 8;
for (int j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
}
return crc;
}
4.5 长包结构总览图
下面这张图把 PH、Data、PF 的组成和排列关系一口气画清楚了。你可以保存下来,调试时对照着看。
4.6 总结与经验备忘
长包的三个部分各有各的命门。我习惯用一张表记牢它们的属性和常见坑。
| 组成部分 | 长度 | 关键字段 | 常见错误 |
|---|---|---|---|
| 包头部 | 4 字节 | DI (VC+DT), WC, ECC | WC 不包含自身、ECC 计算错误 |
| 数据段 | WC 字节 | 像素数据 | 长度非 2 倍数、字节序搞反 |
| 包尾部 | 2 字节 | CRC-16 (0x1021, init 0x0000) | 初始值用错、漏算数据段范围 |
我的三板斧:
- 逻辑分析仪抓 PH,确认 DI 字段的 VC 和 DT 与 sensor 配置一致。
- 手动计算 ECC,看它能否纠正 PH 里 1 个 bit 的翻转——通常在干扰环境里会触发。
- 写一段小程序,把抓到的数据段跑一遍 CRC,跟收到的 PF 比对。差一个字节都不行。
长包是 CSI-2 传送图像的主体,结构并不复杂,但每个字段都藏着细节。别嫌我啰嗦,调通一次长包,你就算真正入门了。接下来,我建议你拿一块现成的 sensor 板,抓一包 RAW8 数据,按上面讲的字段一个一个拆开看。相信我,拆完这包,所有疑问都会烟消云散。