4、通信链路层设计:UART通信协议设计、SPI通信协议设计、I2C通信协议设计、CAN通信协议设计、USB通信协议设计

各位同学,咱们接着往下聊。上一章我们把物理层那些电平标准、接口定义讲清楚了。这一章,咱们要深入通信链路层,也就是协议层面的事儿。

说白了,物理层只负责把「0」和「1」发出去。但怎么发、发多少、发错了怎么办,这些都得靠链路层协议来管。我做了这么多年Bootloader,发现很多新手栽就栽在协议设计上——物理层焊得再漂亮,协议没设计好,升级一样会翻车。

好,咱们一个一个来看。

4.1 UART通信协议设计

UART,串口,这玩意儿太常见了。几乎每个MCU都带,成本低,接线简单。但正因为简单,很多人就随便搞,结果升级到一半数据错位,设备变砖。

我个人习惯,在UART的Bootloader里,帧结构一定要设计严谨。来看一个我常用的格式:

帧头(1字节) + 长度(1字节) + 命令(1字节) + 数据(N字节) + CRC(2字节) + 帧尾(1字节)

帧头我一般用0xAA,帧尾用0x55。为什么?因为这两个字节在二进制里是01010101和10101010,交替的位模式,不容易被噪声模拟出来。

关键参数:

  • 波特率:115200或921600(我建议用后者,升级速度快很多)
  • 数据位:8位
  • 停止位:1位
  • 校验位:无(CRC已经做了校验,没必要再加奇偶校验)

避坑指南:

我曾经在一个项目里,波特率设成了230400,结果某批次的晶振精度不够,导致接收端采样点偏移,数据大面积出错。后来我学乖了——所有UART Bootloader都加上波特率自适应功能。上电后先发一串0x55,接收端根据脉冲宽度自动算出实际波特率。

还有一个细节:超时处理。UART是异步的,你不知道下一帧什么时候来。我一般设两个超时:字节间超时(5ms)和帧超时(100ms)。超过字节间超时,说明当前帧可能断了,直接丢弃。

4.2 SPI通信协议设计

SPI比UART快,全双工,硬件流控也简单。但SPI有个问题——没有标准的帧格式,全靠你自己定义。

嗯,这里要注意:SPI的从机不能主动发数据。所以Bootloader里,主机必须轮询从机状态。我常用的做法是:

  1. 主机发一个命令字节,从机回复状态
  2. 如果是「请求发送数据」,主机就发N个字节的数据
  3. 从机每收完一包,回复ACK

你看,这其实是个半双工的主从问答模式。为什么不用全双工?因为全双工虽然快,但协议复杂度翻倍,对于Bootloader这种对可靠性要求极高的场景,不值得。

我建议的SPI帧结构:

命令(1字节) + 地址(4字节) + 长度(2字节) + 数据(N字节) + CRC(2字节)

SPI的时钟极性(CPOL)和时钟相位(CPHA)一定要固定。我见过太多人在这上面踩坑——明明硬件连好了,就是不通,最后发现是模式0和模式3搞混了。

小技巧:

SPI的片选信号(CS)一定要在整帧传输过程中保持低电平。我曾经遇到一个bug:主机在发送过程中CS抖动了一下,从机以为新帧开始了,结果数据全乱。后来我在CS线上加了个RC滤波,问题解决。

4.3 I2C通信协议设计

I2C,两根线,SDA和SCL。优点是接线少,支持多从机。缺点是速度慢,而且没有硬件流控。

说实话,我个人不太喜欢在Bootloader里用I2C。为什么?因为I2C的时序太敏感了。你想想看,主从机之间靠的是开漏输出和上拉电阻,线一长,波形就变形。

但有些场景不得不用,比如某些传感器模块的固件升级。那怎么办?

我的经验:

  • 速率降到100kHz以下,别用400kHz的快速模式
  • 每帧数据不超过32字节,避免时钟延展导致超时
  • 加上重试机制,I2C总线冲突的概率比UART高得多

I2C的帧结构我一般这样设计:

从机地址(1字节) + 命令(1字节) + 数据(N字节) + CRC(1字节)

注意,I2C的地址是7位,加上读写位组成1字节。很多新手搞不清这个,写出来的驱动死活找不到从机。

曾经踩过的坑:

有一次,我在I2C Bootloader里没加总线恢复机制。结果升级过程中从机拉低了SCL,主机一直等,整个系统卡死。后来我加了个超时复位——如果SCL被拉低超过100ms,就强制发9个时钟脉冲,让从机释放总线。

4.4 CAN通信协议设计

CAN总线,工业界和汽车界的标配。它的物理层和链路层都有标准定义(ISO 11898),所以咱们主要关注应用层协议。

CAN的帧结构是固定的:11位或29位ID,加上最多8字节数据。对,你没看错,最多8字节。这意味着你没法像UART那样一包发几百字节。

那怎么办?拆包。

我常用的做法是:

  • 用CAN ID来区分命令类型(比如0x100表示开始升级,0x101表示数据包)
  • 数据包用多帧传输,每帧带序号
  • 接收端收到后重组,缺帧就请求重传

举个例子:

帧1: ID=0x101, Data=[0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]  // 序号0
帧2: ID=0x101, Data=[0x01, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E]  // 序号1
...

CAN的波特率一般用250kbps或500kbps。我建议用后者,因为Bootloader的数据量通常不大,500k足够快,而且兼容性好。

关键点:

CAN总线有仲裁机制,ID越小的帧优先级越高。所以,错误帧和紧急命令的ID一定要设小。我一般把「升级中止」命令的ID设为0x001,确保它能在总线冲突时优先通过。

4.5 USB通信协议设计

USB,这个就复杂了。它分低速(1.5Mbps)、全速(12Mbps)、高速(480Mbps)。Bootloader一般用全速就够了。

USB的协议栈很厚:设备描述符、配置描述符、接口描述符、端点描述符……光这些就能写一本书。但在Bootloader里,我们不需要实现完整的USB协议栈。

我的做法:用USB HID类或USB CDC类。

  • HID类:不需要驱动,即插即用。但传输速度慢,一次只能发64字节。
  • CDC类:虚拟串口,上位机用串口工具就能操作。速度快,但需要驱动(Windows自带)。

我个人更倾向CDC类。为什么?因为调试方便。你想想看,用串口助手就能发数据,不用写专门的USB上位机。

USB的帧结构我一般这样设计:

命令(1字节) + 长度(2字节) + 数据(N字节) + CRC(2字节)

注意,USB的传输单位是包(packet),每个包最大64字节(全速)。所以如果数据超过64字节,需要拆成多个包发送。

经验之谈:

USB的枚举过程很耗时,大概需要几百毫秒。所以Bootloader上电后,不要急着发数据,先等USB枚举完成。我一般加个1秒的延时,确保主机端识别到了设备。

小结

好了,五种通信协议都讲完了。咱们总结一下:

协议 速度 接线 可靠性 适用场景
UART 2线 通用,调试方便
SPI 4线 板内通信,大数据量
I2C 2线 多从机,小数据量
CAN 2线 工业/汽车,远距离
USB 4线 PC连接,大文件传输

选哪种协议,取决于你的硬件资源和应用场景。但不管选哪种,帧结构、超时处理、重传机制这三个东西一定要设计好。这是Bootloader的命根子。

下一章,咱们讲传输层协议设计,包括分包、重组、流控这些内容。到时候我会拿一个实际项目来拆解,保证你们听完就能用。