3. USB PD物理层:BMC编码、前导码、SOF与CRC归纳总结

这一章我们聊聊USB PD的物理层。

说实话,很多做协议栈开发的朋友,一开始都容易忽略物理层。觉得那是硬件工程师的事。但我个人经验是,物理层出了问题,上层协议再对也没用。你想想看,底层的比特流都错了,CRC根本过不了,还谈什么协商?

USB PD物理层有几个核心要素:BMC编码、前导码、SOF(帧起始)和CRC(循环冗余校验)。我把它拆开来讲。

3.1 BMC编码:物理层的“摩尔斯电码”

BMC,全称是Bi-Phase Mark Coding,双相标记编码。

说白了,这就是USB PD在CC线上传输数据的方式。它跟我们熟悉的UART那种NRZ编码不太一样。

我记得第一次看BMC波形时,差点看懵了。它每个比特位都会有电平跳变。具体规则是这样的:

  • 每个比特位的开始处一定会发生电平翻转。
  • 如果当前比特是逻辑“1”,在比特位的中间再翻转一次。
  • 如果当前比特是逻辑“0”,比特位中间保持不变。

简单粗暴的记忆法:

  • “0”:只在开头跳一下(占一个完整周期)
  • “1”:开头跳一下,中间再跳一下(占两个半周期)

为什么要用这种别扭的编码?

好处很明显:自带时钟同步。接收端不需要单独的时钟线,直接从数据流的跳变沿就能提取出时钟信息。这在USB PD这种单线通信的场景下,非常实用。

我在调试一个PD快充项目时,曾经因为线路上的寄生电容太大,导致BMC编码的边沿变缓,接收端锁相环失锁。嗯,这个问题查了我整整两天。后来在CC线上加了个小电阻才解决。

设计建议:CC线上的RC时间常数应尽量小,确保BMC信号上升/下降时间不超过300ns。否则容易导致位采样错误。

3.2 前导码与SOF:数据包的“前戏”

一个完整的USB PD物理层数据包,不是上来就扔数据的。它有一套固定的“开场白”。

3.2.1 前导码(Preamble)

前导码是一串固定的比特序列:64位交替的“0”和“1”,即“01010101...”。

它的作用是什么?

  • 让接收端的PLL(锁相环)锁定到发送端的时钟频率。
  • 让接收端调整增益,适应信号幅度。

我建议你在示波器上看这个前导码波形。如果前导码部分出现抖动或者缺失,基本可以断定是CC线接触不良或者驱动能力不够。

3.2.2 SOF(Start of Frame,帧起始)

前导码发完之后,紧跟着的就是SOF。

SOF是一个固定的5位序列:"00110"(经过BMC编码后)。

它告诉接收端:“注意了,真正的数据包马上要来了”。接收端检测到SOF后,就能知道接下来的数据是正经的PD消息,而不是干扰信号。

避坑经验:

我曾经遇到过一个兼容性问题。某款充电器发送的SOF序列,因为上升沿过缓,被我们的设备误判为无效SOF。结果整个协商流程都走不下去。解决方法是调整了接收端的边沿检测阈值。所以,SOF的时序必须严格符合BMC比特率(300kbps)的±2.5%误差。

3.3 CRC:数据完整性的“守门员”

USB PD用的是CRC-32,多项式是标准化的0x04C11DB7。

你可能会问:“物理层为什么要有CRC?”

其实,USB PD的物理层数据包包含了SOF + 数据头 + 数据对象 + CRC。CRC就是为了保护数据头和数据对象这两个部分不被噪声干扰。

它的计算范围是:从数据头开始,一直到CRC字段之前的所有比特(不包括SOF和前导码)。

我个人习惯在固件里用查表法算CRC,速度比逐比特运算快很多。标准的多项式是0xEDB88320(这个是反射后的多项式)。

// CRC-32 查表法计算示例(USB PD标准多项式)
uint32_t crc32_table[256];

void crc32_init(void) {
    uint32_t crc;
    for (uint32_t i = 0; i < 256; i++) {
        crc = i;
        for (int j = 0; j < 8; j++) {
            if (crc & 1)
                crc = (crc >> 1) ^ 0xEDB88320;
            else
                crc >>= 1;
        }
        crc32_table[i] = crc;
    }
}

uint32_t crc32_calc(uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc = crc32_table[(crc ^ data[i]) & 0xFF] ^ (crc >> 8);
    }
    return ~crc;
}

注意:发送端发送CRC时,需要把高位在前的CRC值反序(按字节反转)发送。接收端收到后重新计算,如果结果等于0x2144DF1C(CRC魔术校验值),则说明数据完整。

3.4 物理层数据包结构总结

把上面几样东西串起来,一个完整的物理层数据包长这样:

字段 长度(字节) 内容 作用
前导码 8 0xAA(交替的0/1) 时钟同步
SOF 1(固定5位) 0x06("00110") 帧起始标识
数据头 2 消息类型、长度、方向 协议控制信息
数据对象 0~28 能力信息、请求数据等 具体协商内容
CRC-32 4 校验值 完整性校验
EOP 1 结束序列 帧结束标识

整个流程你可以理解成:先喊一声“喂”(前导码),再喊一声“我要说话了”(SOF),然后快速把正事说完(数据头+数据对象),最后补一句“我没说错吧?”(CRC)。

3.5 物理层汇总图

下面这张图梳理了从空闲状态到完成一个数据包发送的全过程。你可以对照上面的表格一起看。

USB PD 物理层数据包发送流程 空闲状态 检测到发送请求 前导码 64位交替0/1 SOF 5位 00110 数据头 + 数据对象 BMC编码传输 CRC-32 4字节校验 EOP 结束 关键节点说明: 1. 前导码 - 用于接收端PLL锁定时钟,不能省略或缩短,否则接收端无法正确采样。 2. SOF - 帧起始标志,接收端通过检测到SOF才认为一个有效包开始。 3. 数据包 - 包括数据头(固定2字节)和可选的数据对象,全部用BMC编码。 4. CRC - 覆盖数据头和数据对象,接收端重新计算CRC,若匹配0x2144DF1C则数据正确。 5. EOP - 帧结束标记,发送完EOP后线路进入空闲状态。

再次提醒:

物理层是USB PD协议的基石。很多协议上的疑难杂症,最终都追溯到物理层。

比如CRC不过,别急着怀疑算法,先看看BMC编码的时序对不对。SOF检测失败,先查CC线上有没有直流偏置干扰。

我曾经在一个量产项目中,发现某些线缆在4.75V下发送的前导码振幅不够,导致接收端无法同步。最后是通过调整发送端的驱动能力才解决的。

3.6 本章小结

总结一下今天讲的核心内容:

  • BMC编码是物理层的数据编码方式,自带时钟,适合单线通信。
  • 前导码用于时钟同步,是数据包发送的前奏。
  • SOF标志一个数据包的开始,接收端靠它来识别有效帧。
  • CRC-32保证数据完整性,算法固定,固件实现建议用查表法。

嗯,搞懂了这些,你就能真正看懂USB PD总线上的数据流了。下次用逻辑分析仪抓波形时,可以试着把前导码、SOF、CRC一一找出来,挺有成就感的。