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里,主机必须轮询从机状态。我常用的做法是:
- 主机发一个命令字节,从机回复状态
- 如果是「请求发送数据」,主机就发N个字节的数据
- 从机每收完一包,回复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的命根子。
下一章,咱们讲传输层协议设计,包括分包、重组、流控这些内容。到时候我会拿一个实际项目来拆解,保证你们听完就能用。