第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 的组成和排列关系一口气画清楚了。你可以保存下来,调试时对照着看。

长包(Long Packet)结构总览 包头部 (PH, 4 Bytes) DI (1B) WC (2B) ECC (1B) 数据段 (Data, WC Bytes) 像素数据(RAW / YUV / JPEG ...) 包尾部 (PF, 2 Bytes) CRC-16 校验和 (2B) 长包总字节数 = 4 (PH) + WC (偶数) + 2 (PF)

4.6 总结与经验备忘

长包的三个部分各有各的命门。我习惯用一张表记牢它们的属性和常见坑。

组成部分长度关键字段常见错误
包头部4 字节DI (VC+DT), WC, ECCWC 不包含自身、ECC 计算错误
数据段WC 字节像素数据长度非 2 倍数、字节序搞反
包尾部2 字节CRC-16 (0x1021, init 0x0000)初始值用错、漏算数据段范围

我的三板斧:

  1. 逻辑分析仪抓 PH,确认 DI 字段的 VC 和 DT 与 sensor 配置一致。
  2. 手动计算 ECC,看它能否纠正 PH 里 1 个 bit 的翻转——通常在干扰环境里会触发。
  3. 写一段小程序,把抓到的数据段跑一遍 CRC,跟收到的 PF 比对。差一个字节都不行。

长包是 CSI-2 传送图像的主体,结构并不复杂,但每个字段都藏着细节。别嫌我啰嗦,调通一次长包,你就算真正入门了。接下来,我建议你拿一块现成的 sensor 板,抓一包 RAW8 数据,按上面讲的字段一个一个拆开看。相信我,拆完这包,所有疑问都会烟消云散。