简介:MIPI CSI-2 是 MIPI Alliance 制定的摄像头串行接口规范,用于连接图像传感器与手机、无人机、车载电子等设备的主控处理器。这份文档为 2009 年 4 月 2 日发布的 Draft 版本 1.01.00 r0.04,面向从事摄像头驱动开发、图像采集链路调试与硬件接口设计的工程师,帮助理解该协议在正式定稿前的技术框架与设计取舍。压缩包共 1 个 PDF 文件,约 2.6MB,内容涵盖多 Lane 并行传输配置、差分信号编码、YUV 与 RGB 等像素格式及 8/10/12 位位深、包头与校验字段组成的包结构、时钟 Lane 对齐的同步机制、电源管理、CRC 错误检测与恢复,以及与 D-PHY、C-PHY 的兼容关系,并附有版权与免责声明条款。已有 1507 人学习下载,适合需要查阅协议原始条文、梳理数据链路分层与排查时序问题的中高级读者对照使用。
1. 一份 2009 年的草案,为什么还要翻
摄像头上电不出图,逻辑分析仪往排线上一挂,四对差分线跑得飞快,抓下来的字节流里全是 0x2B、0x1E 这类值,没人告诉你哪四个字节是包头、哪两位是长度、校验位怎么算。这时候手边要有一份 MIPI CSI-2 的底层协议文档,翻到长包格式那页,一小时能把整条链路对齐。这份 1.01.00 r0.04 草案(2009 年 4 月 2 日)讲的正是这些:CCI 寄存器读写、多 Lane 分配合并、低层协议包格式、Hamming 校验与 CRC、RAW/YUV/RGB 的位打包。它适合正在写摄像头驱动、做 FPGA 图像采集、验证 SoC 端 CSI 控制器的人,也适合刚接手一块 sensor 板、想从波形反推协议的新手。草案归草案,这套分层框架和包结构在后续版本里没大动过,当第一手对照手册仍然好用。
2. 先立骨架:CSI-2 的五层分工与这份文档的检索地图
拿到一份一百多页的规范,最怕从头读到尾。我的习惯是先画出层,再把每层对应到章节号,之后遇到问题直接跳页,而不是通读。这一章先把骨架搭起来,顺便把最容易和图像数据搞混的控制通道单独拎出来讲。
2.1 五层各自管什么,出问题时谁背锅
CSI-2 把一条摄像头链路拆成几层,每层职责很清晰,排查时按层往下压,比凭感觉换线换板子高效得多。
| 层 | 对应章节 | 管什么 | 出问题时的典型症状 |
|---|---|---|---|
| 应用层 | 第 10、11 章 | 色彩空间定义、像素格式与位打包规则 | 图像能出但颜色错、位深不对、右侧有条纹 |
| 协议层(LLR) | 第 9 章 | 包格式、DI/VC/DT、ECC、CRC、包间隔 | 花屏、整帧丢失、CRC 报错 |
| Lane 管理 | 第 8 章 | 多 Lane 字节分配与合并 | 图像斜切、半幅错位、左右半屏对不上 |
| PHY 层 | 第 7 章 | 只做引用,不含电气细节 | 完全无数据、误码率高、眼图差 |
| 控制通道 CCI | 第 6 章 | 主机读写 sensor 寄存器 | 配置写不进去、读回全 0xFF |
这里有个新手常犯的混淆:图像数据是单向的,从 sensor 流向主机;CCI 是双向的,主机用它配置 sensor 的曝光、增益、输出格式。两条链路在物理上完全独立,一根排线里既有高速数据对,也有两根低速控制线。调不通图的时候先分清是哪条链路的问题——CCI 都没通,谈图像没有意义。
2.2 把目录当检索表用:16 页到 114 页怎么跳
这份草案的目录本身就是一张索引表,我把常用的几处整理成下面这样,贴在工位边上比记在脑子里管用。
| 你要解决的问题 | 直接翻 | 看什么 |
|---|---|---|
| 整体框架不清楚 | 第 4、5 章 | CSI-2 概览与层定义,一页纸看懂数据怎么从像素变成字节 |
| sensor 寄存器写不进去 | 第 6 章 | CCI 消息类型、读写时序、多字节寄存器字节序 |
| 图像斜切、半幅错位 | 第 8 章 | 多 Lane 分配与合并规则、Lane 间互操作要求 |
| 包头解不出、CRC 报错 | 第 9.1~9.6 节 | 长包/短包格式、DI 定义、ECC 与校验和生成 |
| 帧率够但偶发丢帧 | 第 9.7、9.10 节 | 包间隔规则与包间隔示例 |
| 一路数据里混了多路图像 | 第 9.13 节 | 数据类型交织与虚拟通道交织 |
| RAW10 出条纹 | 第 11.4.4 节 | RAW10 的位打包与行尾对齐 |
| YUV422 颜色错 | 第 11.2.4 节 | YUV422 8-bit 的字节排列顺序 |
翻文档的顺序建议反过来:先看第 5 章的层定义建立框架,再跳到第 9 章把包格式吃透,最后回到第 8 章和第 11 章补多 Lane 和像素打包。第 10 章色彩空间那两页很短,但 RGB 和 YUV 的定义顺序(是 Y-U-Y-V 还是 Y-U-Y-V 交错)决定了你解出来的图是不是偏色,别跳过。
2.3 CCI:上电后最先要跑通的一段
CCI 是从 I2C 精简出来的两线接口,SCL/SDA,主机侧做 Master 读写 sensor 内部寄存器。规范第 6 章给了消息类型、读写操作、从机地址、多字节寄存器协议和 I/O 电气时序几块内容,其中真正天天用的是前三块。
消息类型按长度分几种,用途差别不小:
| 消息类型 | 组成 | 典型用途 |
|---|---|---|
| 16-bit | 8-bit 寄存器地址 + 8-bit 数据 | 读写 8 位寄存器,最常用 |
| 24-bit | 8-bit 地址 + 16-bit 数据 | 写 8 位地址的 16 位寄存器 |
| 32-bit | 16-bit 地址 + 16-bit 数据 | 16 位地址空间的主流 sensor |
| 64-bit | 16-bit 地址 + 32-bit 数据 | 少数需要一次写 32 位参数的场景 |
实际动手的顺序我一般是这样:先确认 sensor 的寄存器地址宽度是 8 位还是 16 位(这决定了你发几个地址字节),再用最简单的一次读操作读 chip ID,读回来的值和手册对上,才说明 CCI 时序、从机地址、上拉电阻、时钟频率这一串都没问题。这一步不通就往下走,后面全是玄学。
规范 6.4 节给了 I/O 级的电气与时序参数,包括建立保持时间和时钟频率上限。这些数值用来判断你的上拉电阻选得合不合适——上拉太大,上升沿变缓,高速下读回全 0xFF 或者随机值;上拉太小,灌电流超限。血泪经验是:CCI 跑不通时,八成不是协议理解错了,是上拉和走线的问题。
2.4 PHY 的边界:为什么这里找不到 HS/LP 时序
文档第 7 章只有一句话的分量——物理层引用另一份 D-PHY 规范,本文档不展开。这意味着你在 CSI-2 里找不到 HS 进入/退出的时序、LP 状态电平、差分摆幅这些参数。很多人拿着这份文档找"时钟 lane 怎么对齐",翻遍也找不到,因为它压根不在这里。
分清边界的好处是排查有方向:逻辑分析仪能解出包头、CRC 也对,说明 PHY 侧基本没问题,往协议和上层查;包头解出来全是乱码、CRC 大面积报错,先别怀疑协议,回去看差分对阻抗、lane 间等长、参考时钟。这个判断顺序能省掉大量无头苍蝇式的换板子。
3. 把字节流切开:长包、短包与 DI/VC/DT 三个字段
协议层是这份文档最值钱的部分,也是实际调试里翻得最多的一章。抓包工具给你一大段十六进制,能不能把它切成一个个完整的包,取决于你对包头四个字节的理解有多熟。
3.1 长包:4 字节包头 + 载荷 + 2 字节包尾
长包用来传图像数据,结构是固定的:4 字节包头(PH)、可变长的数据载荷、2 字节包尾(PF)。载荷长度由包头里的字计数决定,单位是字节,不是像素。
| 字段 | 长度 | 内容 | 传输顺序 |
|---|---|---|---|
| DI(Data Identifier) | 1 字节 | 虚拟通道号 + 数据类型 | 先传 |
| WC(Word Count) | 2 字节 | 载荷字节数 | 低字节先传 |
| ECC | 1 字节 | 对 DI + WC 的 6-bit 校验 | 最后传 |
| 数据载荷 | WC 字节 | 像素数据或嵌入信息 | 按字节顺序 |
| PF(Packet Footer) | 2 字节 | 对载荷的 16-bit 校验和 | 低字节先传 |
有两个点必须记住。第一,WC 是字节数而不是像素数,RAW10 一个像素只占 10 bit,所以 WC 和像素个数之间差着一个 4:5 的换算。第二,包尾的校验和只覆盖载荷,不覆盖包头——包头由 ECC 单独保护。这两套校验机制分开,是因为包头只有 4 字节且必须能纠错,载荷数据量大且只需要检错。
3.2 DI 字节:VC 和 DT 怎么塞进同一个字节
一个字节八位,高两位给虚拟通道,低六位给数据类型:
| 位 | 含义 |
|---|---|
| DI[7:6] | 虚拟通道标识(VC),最多 4 路 |
| DI[5:0] | 数据类型(DT),标识后面跟的是什么格式的数据 |
虚拟通道是给多摄场景准备的。一路 CSI 链路上可以交织传输多颗 sensor 的数据,靠 VC 区分,接收端按 VC 分发到不同的缓冲。数据类型则决定了载荷该怎么解:是 RAW10 还是 YUV422,是帧起始短包还是普通图像包,全靠这六位。
文档 9.4 节列出了 1.01.00 版本支持的数据类型码,常用的几个是:帧起始 0x00、帧结束 0x01、行起始 0x02、行结束 0x03、通用短包 0x08~0x0F、空包 0x10、消隐数据 0x11、嵌入信息 0x12、YUV422 8-bit 0x1E、RGB888 0x22、RGB565 0x24、RAW8 0x2A、RAW10 0x2B、RAW12 0x2C。这张表在不同版本之间偶有增减,落地时以你手上文档的那一版为准,别拿别的版本的码表硬套。
3.3 短包:4 个字节把帧和行的边界标出来
短包没有载荷也没有包尾,就是 4 个字节:DI、两个字节的数据域、ECC。它的用途是同步和事件通知,图像数据本身不会用短包传。
帧同步和行同步就是短包实现的。帧起始包的数据域里可以放帧号,行起始包里放行号。接收端靠这些短包知道一帧从哪开始、到哪结束、每一行从哪切。调试时如果发现"图能出但上下错位"或者"整帧少了几行",先看短包有没有被正确识别——很多接收端为了省事直接按固定行数切,短包全丢了也能出图,但一旦分辨率或消隐变了就崩。
通用短包(0x08~0x0F)留给厂商自定义事件,比如闪光灯同步、陀螺仪时间戳。这类包在标准里只规定了外壳,内容由实现方约定,接收端不认识就应该丢掉而不是报错。
3.4 包与包之间:间隔规则和它的实际影响
第 9.7 节讲包间隔,9.10 节给了一堆示例。核心意思是一句话:包与包之间可以有空隙,但空隙的大小受 PHY 状态切换时间约束,发送端不能想停多久停多久。
这个规则在实际调试里表现为两类问题。一类是发送端为了省电频繁进出低功耗状态,包间隔被拉得很长,接收端的超时机制误判为链路断开;另一类是反过来,包挨得太紧,接收端 FIFO 还没排空下一包就来了,溢出丢包。文档里的示例给的是合规的下限和上限区间,做 FPGA 接收端时,超时阈值应该按你自己链路的实际包间隔留足余量,别照抄别人的常数。
3.5 用一段脚本把抓到的字节流切成包
逻辑分析仪导出的是连续字节流,手工数太累。我一般写个小脚本先按包头切,把每个包的 DI、VC、DT、WC 打出来,肉眼扫一遍就知道有没有异常。
# 按 CSI-2 长包结构切分一段连续字节流 def parse_header(hdr: bytes) -> dict: assert len(hdr) == 4, "长包包头固定 4 字节" di, wc_l, wc_h, ecc = hdr return { "vc": (di >> 6) & 0x03, # DI[7:6] 虚拟通道 "dt": di & 0x3F, # DI[5:0] 数据类型 "wc": (wc_h << 8) | wc_l, # 低字节先传,别写反 "ecc": ecc, } def split_packets(buf: bytes): i = 0 while i + 6 <= len(buf): info = parse_header(buf[i:i + 4]) # 短包没有载荷和包尾,长度恒为 4 if info["dt"] <= 0x0F: yield info, buf[i + 4:i + 4] i += 4 continue total = 4 + info["wc"] + 2 if i + total > len(buf): break yield info, buf[i + 4:i + 4 + info["wc"]] i += total # 抓包数据示例:RAW10、WC=0x0A00(2560 字节) raw = bytes([0x2B, 0x00, 0x0A, 0x1E]) + bytes(2560) + bytes([0x00, 0x00]) for info, payload in split_packets(raw): print(info, len(payload))这段代码的逻辑很直白:每次从当前位置读 4 字节解析包头,用 DT 判断是不是短包,是就跳 4 字节;不是就按 4 + WC + 2 算出整包长度往后跳。参数上唯一容易翻车的是 WC 的字节序,低字节在前,写反了整段数据全错位,后面切出来的包头全是垃圾。实际用的时候把parse_header里的 ECC 拿去和第四字节比对,不一致说明要么字节流错位了,要么链路有误码,先别急着解图。
4. 校验这一层:6-bit Hamming ECC 与 CRC-16 的分工
校验是这份文档里最容易被跳过、又最容易在关键时刻救命的部分。抓包工具报"CRC 错"的时候,你得知道它算的是哪一段、用什么多项式、初值多少,否则只能干瞪眼。
4.1 为什么包头用纠错码,载荷用检错码
包头只有 4 个字节,一旦错了,整个包的长度和类型全判不出来,后面的数据没法切分,等于整条流废掉。所以包头用 Hamming 修正码:6 个校验位保护 24 位数据,能纠正单比特错误,同时检出双比特错误。多花 1 个字节换回"包头错了也能救回来",很划算。
载荷动辄几千字节,逐位纠错的开销撑不住,所以用 CRC-16 做检错。检出错误就整包丢弃,靠上层重传或者下一帧补上。两个机制分工明确,别指望 CRC 能纠错,也别指望 ECC 能覆盖载荷。
4.2 6-bit ECC 的覆盖关系:照着表抄成掩码最省事
规范 9.5.2 节的修正 Hamming 码,本质是 6 个校验位各自覆盖一组数据位,覆盖关系以表格形式给出。硬记没用,正确做法是把表格抄成 6 行 × 24 列的 0/1 矩阵,代码里按位与再数 1 的个数。
| 校验位 | 覆盖范围 | 说明 |
|---|---|---|
| P1~P5 | 各自覆盖一组 D 位 | 具体位号照抄文档表格,不要自己推 |
| P6 | 覆盖全部 24 位数据 | 总校验位,用于区分单比特与双比特错误 |
为什么要强调"不要自己推":标准 Hamming 码的覆盖是 2 的幂规律,但 CSI-2 的位编号顺序和教科书不一样,硬推出来的覆盖关系是错的。我见过有人按教科书推导写了一版,收到的包全是"可纠正单比特错误",实际上每个包都对不上。照抄表格,一行都别改。
4.3 CRC-16:多项式、初值、覆盖范围
校验和用的是 CRC-16,多项式 x^16 + x^12 + x^5 + 1,也就是常见的 0x1021,初值 0xFFFF,覆盖范围只有数据载荷,包头不参与计算。包尾两个字节低字节先传。
| 参数 | 取值 |
|---|---|
| 多项式 | x^16 + x^12 + x^5 + 1(0x1021) |
| 初值 | 0xFFFF |
| 覆盖范围 | 仅数据载荷,不含包头 |
| 传输顺序 | 低字节先发 |
有一类实现差异值得警惕:有些控制器算 CRC 时把包尾两个字节也喂进去了,或者初值用了 0x0000。这类差异在标准里没得商量,但不同厂商的 IP 核确实会写错。抓包时如果发现"其他都对、就是 CRC 对不上",把初值和覆盖范围换几种组合试一遍,通常能定位到是接收端的问题还是发送端的问题。
4.4 接收侧:单比特纠错、双比特报错
ECC 的接收侧逻辑分三种情况。算出来的 syndrome 为 0,包头无误,直接用;syndrome 对应某个单比特位置,翻转该位后使用,同时把可纠正错误计数加一;syndrome 指示双比特错误,整包丢弃,计数加一。
工程上最容易被忽略的是"可纠正错误"这个计数器。链路刚开始劣化时,表现不是丢包,而是单比特错误计数缓慢上涨。如果只在丢包时才看寄存器,等到真丢包的时候,眼图已经烂得不成样子了。我的习惯是在 bring-up 阶段把这个计数器读出来盯着,只要不是恒为 0,就得回去查等长和阻抗。
4.5 一段能直接跑的校验代码
ECC 的覆盖关系按文档表格填进掩码数组,CRC 用查表法或逐位法都行。下面这段是逐位实现,牺牲一点速度换可读性,用来核对抓包数据足够。
# CRC-16/CCITT:多项式 0x1021,初值 0xFFFF,覆盖载荷 def crc16(payload: bytes) -> int: crc = 0xFFFF for byte in payload: crc ^= byte << 8 for _ in range(8): if crc & 0x8000: crc = ((crc << 1) ^ 0x1021) & 0xFFFF else: crc = (crc << 1) & 0xFFFF return crc # ECC:COVER 是 6 个 24 位掩码,逐行照抄文档 9.5.2 的表格 def calc_ecc(di: int, wc: int, cover: list) -> int: data = (di << 16) | (wc & 0xFFFF) # DI 在高 8 位,WC 在低 16 位 ecc = 0 for i, mask in enumerate(cover): ecc |= (bin(data & mask).count("1") & 1) << i return ecc & 0x3Fcrc16的输入只放载荷,不要把包头塞进去;返回值和包尾两字节比对时注意低字节在前。calc_ecc里的cover必须按你手上那版文档的表格逐行填写,6 个掩码对应 P1 到 P6,第 6 位是总校验。写完拿文档 9.5.3 节给的发送侧示例值验一遍,示例对得上再用,这一步别省。
5. 避坑与排查:多 Lane、字节序、RAW 对齐的五条血泪记录
规范读得再熟,落到板子上照样翻车。这一章把我在摄像头链路上踩过的、以及帮别人排查时反复见到的几个坑整理出来,每条按现象、原因、解决的顺序写,对着症状查比通读规范快。
5.1 多 Lane 的分配与合并:最容易错位的一段
第 8 章讲的是发送端如何把一串字节分摊到多条 Lane 上,接收端再合并回来。规则本身不复杂:字节按顺序轮流分配到各 Lane,接收端按同样顺序拼回去。复杂的是它和 PHY 的 lane 顺序、PCB 走线顺序、控制器配置三者必须完全一致。
| 环节 | 常见错误 | 后果 |
|---|---|---|
| 发送端配置 | 4 Lane 输出,接收端按 2 Lane 配 | 数据全乱,包头都切不出 |
| PCB 走线 | Lane 顺序对调但没在寄存器里纠正 | 图像斜切、条带 |
| 接收端合并 | 起始 Lane 认错 | 整幅图左右半屏互换 |
| Lane 数切换 | 动态切 Lane 数时未重新同步 | 切一次错一次 |
调试多 Lane 我一般先降到单 Lane 跑通,确认包头、CRC、出图都正常,再往上加。单 Lane 通、双 Lane 不通,问题一定在分配合并或者走线顺序上,不用怀疑数据格式。
5.2 五条踩坑记录
第一条:图像斜切,像被斜着剪了一刀。现象是每一行都比上一行偏移几个像素,整体呈斜条带。原因是多 Lane 合并时起始 Lane 认错,或者接收端把 Lane 顺序搞反了。解决办法是先把发送端降到单 Lane 验证,再逐条 Lane 单独使能,看哪条加进来之后开始斜;同时检查控制器里 lane 映射寄存器的配置和 PCB 实际走线是否一致。有些平台默认映射和板子对不上,必须手动改。
第二条:帧率正常,偶发整帧花屏。现象是大部分帧正常,隔几十帧花一次,重上电就好一阵。原因是链路误码,多半出在等长、阻抗或者参考时钟抖动上,也可能是包间隔配置太紧导致接收 FIFO 溢出。解决时先读单比特错误计数器,如果它在缓慢上涨,就是信号完整性问题,回去量差分对;如果计数器不动但整帧丢,看接收端 FIFO 状态和超时阈值,把包间隔放宽一点试试。
第三条:RAW10 图像右侧有规律性条纹。现象是图像左边正常,右边每隔几个像素出现一条竖纹,位置固定。原因是 RAW10 的打包规则没吃透:四个像素打包成五个字节,一行结束时如果像素数不是 4 的倍数,需要按规定填充。很多接收端按"每像素 10 bit"直接解,忽略了字节对齐,越往右累积误差越大。解决办法是严格按第 11.4.4 节的位排布解包,行尾按 WC 算出的字节数处理,不要自己按像素数反推长度。
第四条:CCI 读回全 0xFF,寄存器写不进去。现象是上电后读 chip ID 一直是 0xFF,写配置也没反应。原因通常是三个:从机地址选错(7 位地址左移一位后发,很多人忘了移位)、寄存器地址宽度配错(8 位当 16 位发,或者反过来)、多字节寄存器字节序搞反。解决办法是先确认 sensor 手册里的地址宽度和从机地址,用最简单的单字节读操作验证,读通了再上多字节。上拉电阻和总线电容也顺手量一下,上升沿太缓同样读回 0xFF。
第五条:换了颗 sensor 后能收到包,但解不出图。现象是链路正常、包头也对,就是画面全黑或者颜色完全不对。原因是 DT 和实际格式不匹配——发送端按 RAW10 发,接收端按 RAW8 解,或者 VC 撞车导致两路数据混在一起。解决办法是把解析出来的 DI 打出来,逐个核对 DT 是否落在预期范围、VC 是否唯一。多摄系统里尤其要注意给每颗 sensor 分配不同的 VC,否则交织在一起没法分开。
提示:这五条里,前三条属于"配置与信号"层面,后两条属于"协议与格式"层面。排查时先分层再动手,比一上来就换排线有效得多。
6. 拿一份抓包数据把规范跑一遍:四步自检流程
规范读完之后,最好的巩固方式不是再读一遍,而是拿一份真实抓包数据从头到尾跑一遍,看每一层能不能对上。我一般按下面四步走,每一步都有明确的通过标准,卡在哪一步就回去翻对应的章节。
| 步骤 | 做什么 | 通过标准 | 对不上时翻哪 |
|---|---|---|---|
| 一、切包 | 按 4 + WC + 2 切分字节流 | 每个包的 DT 合法、长度自洽、能连续切到尾 | 第 9.1 节 |
| 二、校包头 | 用 ECC 覆盖表算校验,与第四字节比对 | 全部一致,或仅有可纠正的单比特错误 | 第 9.5 节 |
| 三、校载荷 | 用 CRC-16 算载荷,与包尾比对 | 全部一致 | 第 9.6 节 |
| 四、解像素 | 按 DT 对应的格式解包成像素 | 与参考图逐像素一致,或差异可解释 | 第 11 章 |
第三步经常被跳过,因为它最枯燥——几百个包挨个算 CRC。但恰恰是这一步能区分"链路误码"和"格式理解错":CRC 全过而图不对,说明数据没错,是解包规则错了;CRC 大面积不过,说明是链路或者包头切分的问题,别在格式上浪费时间。
第四步的对比我有个笨办法但很管用:让 sensor 输出一个已知图案(很多 sensor 支持测试图案模式),解出来的像素和预期图案逐点比。测试图案通常是渐变色块或者棋盘格,一旦解包规则错了,色块的边界会出现在错误的位置,一眼就能看出来是位打包错了还是字节序反了。
最后说个习惯。从那以后我每次接手新的摄像头链路,都会先把这四步做成一个脚本,抓一段数据跑一遍,四个步骤全绿了才开始调图像质量。链路本身没验证就去看画面,等于在黑匣子里猜,改一个参数换一个现象,一天下来也不知道改对了什么。希望这套流程能帮到你,少走几个我当年走过的弯路。
本文还有配套的精品资源,点击获取