3、CAN Bootloader架构设计:上位机、下位机、通信协议栈三层架构

好,咱们今天聊点实在的。CAN Bootloader到底该怎么搭?

很多人一上来就写代码,结果写到一半发现上下位机对不上,协议栈乱成一锅粥。我早期也犯过这个错,后来才明白——架构先行

说白了,一个靠谱的CAN Bootloader,就三层:上位机、下位机、通信协议栈。这三层各司其职,缺一不可。我习惯把它们比作「发件人、收件人、快递规则」。你想想看,没有规则,发件人乱写地址,收件人看不懂包裹,那还怎么玩?

3.1 上位机:升级的发起者

上位机,就是你的PC工具,或者手机APP。它的任务很明确:把固件发下去

我个人习惯把上位机拆成三个模块:

  • 文件解析模块:读入.hex或.bin文件,提取出原始二进制数据。
  • 协议封装模块:把数据切成CAN报文能装下的小包,加上序号、校验、命令字。
  • 通信调度模块:负责发报文、收应答、超时重传、进度显示。

这里有个坑。我记得有一次,上位机把.hex文件解析错了,地址偏移搞反了,结果下位机把程序写到了错误的位置,板子直接变砖。嗯,从那以后,我每次都会在文件解析后打印一份地址映射表,人工核对一遍。

我的建议:上位机最好支持两种文件格式——.hex和.bin。.hex带地址信息,适合分段烧录;.bin简单粗暴,适合连续地址。我个人偏爱.bin,因为解析逻辑简单,不容易出错。

3.2 下位机:升级的执行者

下位机就是你的MCU,它要完成三件事:接收、校验、写入

下位机的架构,我一般分成这几层:

  • CAN驱动层:负责收发CAN报文,处理硬件中断。说白了就是让CAN口能正常工作。
  • 协议解析层:从CAN报文里提取命令、数据、序号,判断是哪个包,有没有丢包。
  • Flash驱动层:负责擦除、写入、校验Flash。这是最敏感的一层,写错了板子就废了。
  • 状态机管理层:管理整个升级流程——空闲、等待、接收、校验、完成、回滚。

为什么要用状态机?因为CAN通信是不可靠的,报文可能丢、可能乱序。状态机能帮你理清「现在该干什么」。我在项目中遇到过,有人用全局变量做流程控制,结果一个中断进来,变量被改了,流程直接跑飞。从那以后,我坚持用状态机,每个状态只做一件事,做完再切。

注意:下位机的Flash驱动,一定要加「写保护」和「读回校验」。我曾经因为没加写保护,升级过程中突然掉电,结果Flash里的数据一半新一半旧,板子彻底起不来了。后来我加了双备份机制——先写备份区,校验通过后再拷贝到主程序区。

3.3 通信协议栈:连接上下位的桥梁

通信协议栈,说白了就是「你们俩怎么说话」。CAN Bootloader的协议栈,我习惯定义成三层:

层级 功能 典型内容
物理层 CAN 2.0B标准,250kbps或500kbps CAN ID、DLC、数据场
传输层 分包、组包、序号管理、超时重传 包序号、总包数、数据长度
应用层 命令定义、状态反馈、校验机制 命令字、状态码、CRC32

这里我重点说一下应用层。命令字怎么定义?我一般用这种方式:

// 命令字定义
#define CMD_START      0x01   // 开始升级
#define CMD_DATA       0x02   // 传输数据
#define CMD_END        0x03   // 结束升级
#define CMD_ACK        0x04   // 应答
#define CMD_NACK       0x05   // 否定应答
#define CMD_STATUS     0x06   // 查询状态

每个命令后面跟着数据。比如CMD_DATA,数据场里包含:包序号(2字节)、数据长度(1字节)、有效数据(最多8字节)。

为什么会这样设计?因为CAN报文一帧最多8字节数据。你要传一个几百KB的固件,必须拆成很多小包。每个包都要有序号,不然下位机不知道谁先谁后。

核心要点:通信协议栈的设计,要兼顾「效率」和「可靠性」。效率方面,尽量让每个CAN报文都装满8字节;可靠性方面,每个包都要有CRC校验,并且支持超时重传。我一般用CRC32,虽然计算量大一点,但安全性高。

3.4 三层如何协同工作?

我举个例子,你就明白了。

假设你要升级一个固件,大小是100KB。上位机先读入.bin文件,切成12800个包(每个包8字节)。然后开始发:

  1. 上位机发CMD_START,告诉下位机「我要升级了,总共有12800个包」。
  2. 下位机收到后,擦除Flash,回复CMD_ACK。
  3. 上位机开始发CMD_DATA,每个包带序号和数据。
  4. 下位机收到一个包,校验CRC,写进Flash,回复ACK。
  5. 如果上位机没收到ACK,就重传这个包。最多重传3次,还不行就报错。
  6. 所有包发完后,上位机发CMD_END。
  7. 下位机收到后,对整个固件做一次CRC校验,如果正确,回复ACK,然后跳转到新程序。

你看,整个过程就是三层在配合。上位机负责「发什么」,下位机负责「怎么收」,协议栈负责「怎么传」。

避坑指南:我曾经遇到过一个问题——上位机发得太快,下位机来不及处理。CAN总线的缓冲区只有几个报文,一满就丢包。后来我在协议栈里加了「流控机制」:下位机每收到10个包,就发一个「窗口更新」报文,告诉上位机「我处理完了,继续发」。这样就不会丢包了。

3.5 架构设计的几个原则

最后,我总结几条经验:

  • 分层清晰:每一层只做自己的事,不要越界。CAN驱动层不要管什么命令字,协议解析层不要管Flash怎么写。
  • 接口标准化:上下位机之间的接口,一旦定下来就不要轻易改。我见过一个项目,协议改了三次,每次都要重新烧录所有设备,那叫一个痛苦。
  • 预留扩展:比如命令字里留几个保留位,以后加功能不用改协议。数据场里也留点冗余,万一以后要加时间戳呢?
  • 日志要全:上下位机都要有日志输出。出问题的时候,日志就是你的救命稻草。我习惯在每层都加一个调试打印开关,平时关掉,调试时打开。

嗯,三层架构说到这里,基本就清楚了。下一章咱们聊聊具体的协议帧格式设计,那才是真正动手的地方。