简介:这份 C-PHY 规范文档面向移动端高速接口开发、图像传感器与显示链路设计人员,以及需要理解 MIPI 物理层协议的硬件与驱动工程师,可作为协议查阅、方案评估和实现对照的官方参考资料。压缩包共 1 个 PDF 文件,约 3.5MB,内容为 MIPI C-PHY Specification v2.1,便于直接检索三级编码、低功耗设计、同步定时、电气特性及错误处理等章节。已有 4397 人学习下载,说明其在相关工程与学习场景中具有一定参考价值。读者可据此梳理 C-PHY 与 DSI/CSI 的兼容关系,理解带宽、功耗与信号完整性之间的取舍,并为接口调试、链路验证和规范落地提供依据。
1. 三根线跑出 2.28 bit:C-PHY 绕过差分信令天花板的思路
第一次翻 C-PHY 规范的人,多半会在名字上先栽一跤:看到 C,下意识以为它代表 Compressed,然后顺理成章地以为这是一套“把 D-PHY 的差分数据压一压”的编码方案。翻到规范第 4 章 Overview 就会发现完全不是这回事——C-PHY 里根本没有差分对,它用三根线组成一个 trio,任意时刻三根线的电平两两不同:一根高、一根居中、一根低。名字里的 C 与其说是压缩,不如说是在强调这三线一组的信令结构。
真正反直觉的地方在带宽账本上。一个 symbol 只传 16/7 ≈ 2.28 bit,听起来比一位多不了多少,可三根线平摊下来,每根线每 UI 能跑 0.76 bit,比 D-PHY 差分对的 0.5 bit 高出一半还多。这个数字不是厂商调出来的玄学,是 5⁷ = 78125 > 2¹⁶ = 65536 这条不等式卡出来的。把这条不等式的来龙去脉搞清楚,后面关于符号编码、嵌入时钟、校准、Turnaround 的所有设计,都能顺着推出来。
2. 状态空间与符号编码:16 bit 是怎么被塞进 5⁷ 种组合里的
2.1 六个 wire state,和一个“不许停在原地”的硬约束
三根线记作 A、B、C,每根线可以处在高(H)、中(M)、低(L)三个电平之一。但 C-PHY 不允许任意组合:合法状态必须满足“三根线电平两两不同”,也就是恰好一根 H、一根 M、一根 L。三根线里挑一根做 H 有 3 种选法,剩下两根里挑一根做 L 有 2 种选法,一共 6 个合法状态:
| 状态 | A | B | C |
|---|---|---|---|
| S0 | H | M | L |
| S1 | M | H | L |
| S2 | L | H | M |
| S3 | L | M | H |
| S4 | M | L | H |
| S5 | H | L | M |
这 6 个状态首尾相接围成一个环,任意两个相邻状态之间只交换两根线的角色。这个环的结构很关键,后面讲 rotate 操作时会用到。
接下来是整篇规范里最容易被忽略、又最要命的一条约束:连续两个 symbol 不能停在同一个状态。原因很直接——如果三根线一根都不动,接收端就抓不到任何边沿,嵌入时钟立刻失锁。所以从任一状态出发,下一个状态只能是剩下 5 个之一,5 这个数字就是这么来的。5⁷ 里的 5,本质上不是“状态数”,而是“可选跳变数”。很多人算带宽时直接用 6 去开方,结果 PLL 频率配错、链路起不来,根子就在这儿。
提示:状态数 6 和跳变数 5 是两个不同层面的量。前者决定一帧里能出现多少种 wire state,后者才决定每个 symbol 能携带多少信息。混淆这两个数,是 C-PHY 入门阶段最常见的理解偏差。
2.2 为什么是 7 个 symbol:5⁷ > 2¹⁶ 卡出来的最小整数
一个 16 bit 的数据单元有 2¹⁶ = 65536 种取值。要让 k 个 symbol 装下它,必须满足 5ᵏ ≥ 65536。算一下:5⁵ = 3125,不够;5⁶ = 15625,还是不够;5⁷ = 78125,刚好越过 65536;5⁸ = 390625,冗余太多,白白浪费带宽。
所以 7 不是谁拍脑袋定的,是被不等式卡出来的最小整数解。78125 减掉 65536 剩下 12589 个码字,这些是规范里的“无效码字”,接收端一旦解出它们,就说明链路上出了错——这本身就是一层天然的检错能力,不需要额外加校验位。规范第 6.1.3 节把这段映射关系叫 encoding / decoding / mapping / de-mapping,名字很绕,本质就是“16 bit ↔ 7 个 symbol”的双向翻译。
顺带说一个容易被搞混的数字:每个 symbol 的理论信息上限是 log₂5 ≈ 2.32 bit,但有效载荷只有 16/7 ≈ 2.28 bit。差的这 0.04 bit 就是那 12589 个无效码字占掉的冗余。做链路预算时用哪个数,取决于你是在算“物理层能承载多少”还是“协议层能交付多少”,两者不能混着用。
2.3 rotate 与 flip:符号映射和 DC 平衡的两把工具
规范第 6.1.3 节给出了编码流程,核心是两个操作:rotate 和 flip。
rotate 解决的是“怎么把 0 到 4 这五个数字翻译成具体的下一个 wire state”。因为从当前状态出发有 5 个候选,需要一个确定的排序规则把它们和 0..4 对上。rotate 的做法是以当前状态为基准,沿状态环按固定方向数过去,第 k 个候选就对应数字 k。这样编码器只需要记住“当前在环上的哪个位置”和“要往前走几步”,不需要存一张 6×5 的大表。
flip 解决的是 DC 平衡。如果连续很多个 symbol 都让同一根线待在 H,那根线的平均电流就会偏,长期下来共模漂移、EMI 变差。规范要求按 7 个 symbol 一组做统计,看三根线被驱动到 H 的次数是否均衡,如果不均衡就在下一组做极性翻转(H 与 L 互换),把平均值拉回来。flip 是“整组生效”的,接收端必须知道当前组有没有被翻过,否则解出来全是反的——这也是为什么 C-PHY 的同步序列比 D-PHY 更讲究。
下面这段示意代码把 rotate 的位置关系固定下来,方便对照规范里的状态表看:
# 示意实现:16 bit -> 7 个 wire state 的编码骨架 # 不是规范原文,只用来把 rotate 的索引关系和 base-5 展开讲清楚 # 6 个合法 wire state,用 (A, B, C) 表示,2=H 1=M 0=L WIRE_STATES = [ (2, 1, 0), # S0 (1, 2, 0), # S1 (0, 2, 1), # S2 (0, 1, 2), # S3 (1, 0, 2), # S4 (2, 0, 1), # S5 ] def rotate(state_idx, digit): """digit 取 0..4,映射到从 state_idx 出发的 5 个候选状态。 候选里必须排除 state_idx 本身,否则就没有跳变、时钟失锁。""" candidates = [(state_idx + k) % 6 for k in range(1, 6)] return candidates[digit] def encode_symbols(data16, prev_state): """把 16 bit 拆成 7 个 0..4 的数字,再逐级 rotate 成 wire state。 这里用最直白的 base-5 展开,硬件实现可能换成查表或组合逻辑。""" digits = [] v = data16 for _ in range(7): digits.append(v % 5) v //= 5 digits.reverse() # 高位先发 states = [] cur = prev_state for d in digits: cur = rotate(cur, d) states.append(WIRE_STATES[cur]) return states # 例:0xABCD -> 7 个 wire state print(encode_symbols(0xABCD, 0))参数说明:data16是待发送的 16 bit 数据,取值范围 0..65535;prev_state是上一个 7-symbol 组结束时停留的状态索引,用于保证组与组之间也有跳变,不能随手填 0;digit是 base-5 展开后每一位的值,范围 0..4,正好对上 5 个候选。代码里刻意省掉了 flip,因为 flip 是每 7 个 symbol 做一次整组翻转,属于组级操作,塞进逐符号循环里反而看不清层次。
这段代码只描述了“怎么发”,接收端要做的是完全相反的过程:先按状态环算出相邻两个 wire state 之间的跳变数,还原出 0..4,再按 base-5 拼回 16 bit。如果拼出来的值落在 65536..78124 这个区间,说明中途有 symbol 判错了,这一组数据直接丢弃——这就是前面说的“天然检错”。
2.4 带宽账本:0.76 bit/UI 和 0.5 bit/UI 的差距从哪来
把数字摆在一起看最清楚:
| 项目 | D-PHY(一个差分对) | C-PHY(一个 trio) |
|---|---|---|
| 信号线数 | 2 | 3 |
| 每 UI 传输 | 1 bit | 16/7 ≈ 2.29 bit |
| 每根线每 UI | 0.5 bit | ≈ 0.76 bit |
| 时钟 | 独立 DDR 时钟 lane | 嵌入在跳变里,无专用线 |
| 信令电平 | 2 档(差分) | 3 档(三线两两不同) |
假设两条链路跑同样的 UI 频率 2.5 GS/s:一个 D-PHY 差分对是 2.5 Gbps,一个 C-PHY trio 是 2.5 × 2.2857 ≈ 5.71 Gbps。换算到“每根走线能拉多少流量”,C-PHY 是 1.90 Gbps/线,D-PHY 是 1.25 Gbps/线,比例正好是 1.52 倍。
再往上一层看整个接口。四组 D-PHY 差分对加一对专用时钟线,10 根走线跑 10 Gbps;对应的三个 C-PHY trio,9 根走线跑 17.1 Gbps。少一根线、多七成带宽,这就是为什么在高像素传感器和髙刷屏这类“走线紧张、带宽吃紧”的场景里,C-PHY 会被优先考虑。当然代价也在后面几章:三电平判决比差分判决难做,校准和调试的门槛都上去了。
3. 嵌入时钟与三电平信令:没有时钟线,接收端靠什么锁上
3.1 每一次跳变都是一次时钟边沿
C-PHY 没有专用时钟 lane,接收端的时钟完全来自 symbol 转换本身。因为 2.1 节那条“不许停在原地”的约束,每个 UI 内至少有一根线发生变化。接收端对三根线做两两差分——A−B、B−C、C−A——只要任意一对的相对关系翻转,就产生一个可检测的边沿。
举个具体例子。从 S0 = (H, M, L) 跳到 S1 = (M, H, L):A 从 H 掉到 M,B 从 M 升到 H,C 不动。此时 A−B 这一对从正翻负,B−C 和 C−A 都不翻。再看 S0 跳到 S2 = (L, H, M):三根线全动,A−B 翻、C−A 翻,B−C 不翻。也就是说,每次跳变至少有一对差分翻转,最多两对——这个“至少一对”的性质,就是时钟恢复能成立的物理基础。
采样点怎么定?三根线到达新电平的时间并不严格对齐,走线长度差、驱动强度差都会带来 skew。接收端通常用一个 PLL 或 DLL 把恢复出来的边沿对齐到 UI 中心附近,再做判决。这跟 D-PHY 的源同步思路完全不同:D-PHY 的时钟是发送端跟着数据一起送过来的,接收端只需要做 lane 间 deskew;C-PHY 的时钟是接收端自己从数据里“猜”出来的,对抖动、对 skew 都更敏感,所以规范里对时序参数(第 6.9 节 Global Operation Timing Parameters)抠得很细。
3.2 三电平的共模与摆幅:省电省在哪里
三电平信令的功耗优势,经常被一句“摆幅小所以省电”带过去,其实里面有两层。
第一层是电压摆幅。HS 模式下三根线的电平都压在一个较小的共模附近,从 H 到 L 的总摆幅远小于 LP 模式的 1.2 V 量级。每根线每次跳变要搬运的电荷少,动态功耗自然低。
第二层是 di/dt。D-PHY 的差分对是“一根往上拉、一根往下拉”,两根线的电流方向相反但幅度都不小;C-PHY 的三个电平是“往中间靠”,任意一次跳变里,三根线的电流变化互相部分抵消,电源网络上看到的尖峰更平缓。这一层收益在高速、多 lane 同时翻转时尤其明显,也是 C-PHY 在多摄模组里受青睐的原因之一。
代价同样明确:中间电平 M 距离 H 和 L 都只有半个摆幅,判决门限必须非常准。共模一漂、或者三根线的驱动强度不一致,M 就会被误判成 H 或 L,一个 symbol 错,整组 16 bit 全废。这就是校准(规范第 6.12 节)存在的理由——它不是可选项,是链路能跑起来的前提。
3.3 ALP 模式:夹在 LP 和 HS 之间的第三条路
规范目录里能看到三套工作模式:LP(Low Power)、HS(High Speed),以及 v2.1 里的 ALP(Alternate Low Power)。前两个是 D-PHY 时代就有的老面孔:LP 用大摆幅单端信令传控制命令,速率低到只够发命令字;HS 用三线小摆幅跑全速,功耗高。中间这段空白一直很尴尬——传感器待机、低帧率预览这类场景,数据量不大不小,用 LP 传不动,用 HS 又太费电。
ALP 填的就是这段空白。它沿用三线信令的结构,但用更低的速率和更低的摆幅传输,规范第 6.4.5 节单独给了 ALP 的传输 burst 格式,第 6.11.2 节给了 ALP 的初始化流程。跟 LP 相比它能传真正的数据而不是只有命令,跟 HS 相比它不需要那么高的时钟频率和那么严的时序余量。
从设计动机上看,ALP 是 v2.1 相对早期版本最有分量的增量之一。它让一条链路可以在“全速”“中速低功耗”“待机命令”三档之间切换,而不是只能在“开”和“关”之间二选一。实际系统里这意味着传感器可以在低帧率预览时留在 ALP,不用反复经历 HS 的启动和退出开销——进出 HS 一次要跑完整的 SoT/EoT 序列,那段时间的功耗并不低。
3.4 和 D-PHY 摆在一起看:源同步时钟 vs 嵌入时钟
把两种时钟方案对照着看,取舍就很清楚:
| 维度 | D-PHY 源同步 | C-PHY 嵌入时钟 |
|---|---|---|
| 时钟来源 | 专用 DDR 时钟 lane | 从 symbol 跳变中恢复 |
| 走线开销 | 多一对差分线 | 无额外走线 |
| 抖动敏感度 | 主要看 lane 间 skew | 还要看 CDR 环路和符号间干扰 |
| 接收端复杂度 | 相对低 | 需要 CDR,复杂度高 |
| 低频时的表现 | 时钟一直存在,静态功耗固定 | 没有跳变就没有时钟,需要专门的起搏机制 |
最后一行是关键。嵌入时钟有个天然弱点:如果数据里长时间没有跳变,接收端就抓不到边沿。C-PHY 用“不许停在原地”这条约束强制每个 UI 至少跳一次,把这个问题压到了最小,但代价是接收端必须一直跟着 symbol 节奏走,不能像 D-PHY 那样在空闲期把 CDR 关掉省电。所以你会看到规范里在 HS 之外还留了 LP 和 ALP——不是历史包袱,是嵌入时钟方案必须配的低功耗退路。
4. Lane Module 与 Turnaround:一个 PHY 的分层和双向切换
4.1 一个 trio 里装了什么
规范第 5.1 节把最小的可复用单元叫 Lane Module。电气上它就是一个 trio(三根线),逻辑上它包含一串东西:LP 收发器(大摆幅单端,用于控制模式)、HS 收发器(三电平,用于高速模式)、编码器和解码器(第 2 章讲的 16 bit ↔ 7 symbol)、时钟恢复电路、以及一个管着模式切换的状态机。
把这几个部件拆开看,会发现它们的时序并不共享。LP 收发器工作在毫秒到微秒量级,HS 收发器工作在百皮秒量级,状态机要在两者之间来回切。所以 Lane Module 内部实际上有不止一个时钟域,模式切换时要做跨域同步——这也是为什么规范第 6.3 节要把 Control、High-Speed、Escape 三种操作模式的状态迁移画得那么细。少写一个握手步骤,链路就可能在切换的瞬间丢一个 symbol。
4.2 Primary、Secondary 与 PPI 的分工
第 5.2 节讲 Primary 和 Secondary 的区分。一个 PHY 里只有一个 Primary Lane Module,它可以有若干个 Secondary。Primary 的额外职责是管高频时钟生成(第 5.3 节)和初始化流程,Secondary 基本是跟着 Primary 的节奏走。
第 5.4 节讲 PHY-Protocol Interface,也就是常说的 PPI。这是物理层和上层协议之间的边界:上面是 CSI-2 或 DSI 这类协议层,下面是 Lane Module 的收发逻辑。PPI 定义的是信号语义——什么时候算一个有效的 burst、什么时候可以进 LP、Turnaround 请求怎么传下去。把 PPI 和 Lane Module 分开看,会发现一个设计上的取舍:协议层不应该知道物理层用的是三电平还是差分,物理层也不应该知道上面传的是图像还是命令。这条边界划得越干净,同一个 PHY 换协议越容易。
4.3 单向、双向半双工、混合配置:三种拓扑的取舍
规范第 5.6 节定义了两种 Lane 类型:单向(5.6.1)和双向(5.6.2)。第 5.7 节在此基础上给出三种配置:单向(5.7.1)、双向半双工(5.7.2)、混合(5.7.3)。
单向配置最单纯:数据只往一个方向走,Lane Module 只需要发送或只需要接收,省掉一半的收发器和 Turnaround 逻辑。图像传感器往主控送数据这种场景,单向就够了。
双向半双工配置多了一套 Turnaround 机制:同一组 trio 在时间上分时复用,一会儿往东传、一会儿往西传,但绝不同时。这种配置适合需要回传少量控制信息、又不想多拉一组线的场景。
混合配置是把单向 lane 和双向 lane 放在同一个 PHY 里。听起来别扭,实际很常见:主数据通道用单向跑满速,单独留一条双向 lane 做控制和状态回传,各取所需。
4.4 Turnaround:两个方向不能同时开口说话
Turnaround 是整个规范里最容易翻车的地方,因为它涉及“谁在什么时候有权驱动线路”这个所有权问题。规范给了两套机制:控制模式 Turnaround(6.5.1)和 Fast Lane Turnaround(6.5.2)。
控制模式 Turnaround 走的是 LP 路径:当前驱动方先停止 HS 传输,退回 LP,发一串约定的 Turnaround 序列,然后释放线路;接收方看到序列后接管驱动,反向建立链路。整个过程慢,但稳,适合不着急的场景。
Fast Lane Turnaround 想在 HS 附近就完成切换,省掉退回 LP 再重新进 HS 的那一大段开销。难点在于两个方向切换的瞬间,线路的所有权必须交接得干干净净——早一步,两边同时驱动,电平冲突;晚一步,线路悬空,接收端抓到的是噪声。规范里对这一段的时序参数抠得极细,原因就在这儿。
我在第 7 章会讲到,规范专门为这种冲突准备了一个 Fault Detection 机制叫 Contention Detection(7.1 节)。它检测的就是“三根线的电平不再两两不同”这种非法状态——正常工作时不可能出现,一旦出现就说明有两个驱动源在抢同一根线。
5. 避坑:C-PHY 链路调试里那些看起来能跑、实际会翻车的细节
5.1 带宽算错:把 6 种状态当成 log₂6
现象:链路能起来,眼图也还行,但一跑高帧率就丢包,误码率随速率线性上升。查 PLL 配置,发现 symbol clock 频率比预期低了约 10%。
原因:算带宽时用了 6 个状态,按 log₂6 ≈ 2.58 bit/symbol 估算,然后反推 symbol rate。实际有效载荷是 16/7 ≈ 2.29 bit/symbol,中间差了 12%。用错误的 bit/symbol 反推,PLL 频率自然对不上。
解决:把 5⁷ = 78125 和 2¹⁶ = 65536 这两个数写在调试笔记第一页。任何带宽估算都从“7 个 symbol 传 16 bit”出发,不要从“6 个状态”出发。前者是规范定义的传输单元,后者只是状态空间的规模,两者不是一回事。
5.2 波形看错:单端探头下的“假数据”
现象:用单端探头分别抓三根线,波形看着乱七八糟,跟规范里的状态表对不上,怀疑是编码器写错了。
原因:C-PHY 的信息编码在“三根线的相对关系”里,不在任何单根线的绝对电平里。单端探头看到的是每根线各自的电压轨迹,跳变时刻还带着 skew,人眼几乎不可能直接把它翻译成 wire state。
解决:抓三根线的两两差分(A−B、B−C、C−A),或者用支持三线联合解码的示波器软件。差分波形里,每一次符号跳变会表现为某一对或两对信号的极性翻转,跟第 3.1 节讲的时钟恢复逻辑一一对应。看差分波形还有个附带好处:M
本文还有配套的精品资源,点击获取