简介:面向STM32F103系列嵌入式开发者,这份CAN bootloader固件升级方案利用CAN总线实现固件在线刷写,无需连接JTAG/SWD调试接口,适合车载电子、工业自动化等设备的现场维护与批量升级。压缩包共359个文件,以145个头文件、133个C源文件和72个汇编启动文件为主,配合Keil工程文件、说明文档与可执行工具,整体仅1.27MB,目录结构紧凑。资源完整覆盖boot程序启动流程、CAN报文收发与解析、固件完整性校验、Flash地址擦写、程序跳转以及传输加密等关键环节,能够帮助开发者理清从CAN总线接收数据到最终运行新固件的完整链路。对于正在研究STM32 CAN通信、引导加载程序或远程固件更新机制的工程师,这份工程源码可作为二次移植与学习参考。目前已有423人学习下载,适合具备STM32基础、希望提升设备维护效率的开发者深入研究。 手上的项目有一个很常见的“土办法”:通过调试器把固件直接烧进去,设备装到机箱里之后再想升级,就得拆壳子。这个CANbootloader_stm32f103_HD.rar对应的就是一套基于 CAN 总线的在线升级方案,目标芯片是 STM32F103 的高密度(HD)型号,典型容量 512KB Flash、64KB RAM。简单说,就是让设备在出厂前烧好一段 Bootloader,之后通过 CAN 总线收发固件包,远程把应用程序刷进 Flash,省去拆机、插调试器的麻烦。这套东西在汽车电子、工业控制器、分布式节点升级场景里非常常见,想从零手写 CAN Bootloader,或者正在被“跳转 App 不运行”“固件校验失败”这类问题折磨的朋友,这篇内容应该能帮上忙。
1. 项目整体设计与需求拆解
1.1 为什么选 CAN 总线做固件升级
很多第一次接触 CAN Bootloader 的人会问:用串口(UART)做升级不香吗?香,串口协议简单、调试方便、工具链也成熟。但一到了现场,问题就来了:设备分布在长距离总线上,节点之间隔着几十米甚至上百米,UART 点对点传输根本拉不动;工业现场强电磁干扰环境下,RS485 虽然能拉距离,但抗干扰能力和错误检测机制远不如 CAN。CAN 总线是差分信号,自带 CRC 校验、位仲裁、错误恢复机制,物理层抗干扰能力天然就比 UART 强一个档次。
再往实际应用看,很多做车载电子、BMS、电机控制器、PLC 扩展模块的团队,系统里本来就已经有一张 CAN 网络,节点平时就跑着传感器数据和控制报文。Bootloader 只是在这张现成的网络上加一种“特殊用途”的报文类型,不需要额外布线、不需要拆机,维护人员用一根 USB-CAN 工具接上总线就能远程刷固件。这就是 CAN Bootloader 的核心价值:复用现有总线,低成本远程升级。
1.2 STM32F103 HD 型号的特点与选型原因
标题里的 HD 容易被人忽略,但它决定了整个 Flash 分区方案。STM32F103 家族里有 LD(Low-Density,16~32KB)、MD(Medium-Density,64~128KB)、HD(High-Density,256~512KB)之分。HD 型号的 Flash 从 0x08000000 开始,最大到 0x0807FFFF(512KB),页大小是 2KB,这是我做分区设计的重要输入。
选 HD 型号做 CAN Bootloader,最直白的原因是用户应用程序体积大。中低密度型号装个裸机程序还好说,一旦上了 RTOS、图形界面、协议栈,Flash 空间就捉襟见肘。而 HD 型号的 512KB 空间,可以很从容地把 Bootloader 和 App 拆开,甚至还能再塞一个备份区进去。另外 HD 型号的 RAM 也比较充裕(64KB),CAN 接收缓冲、分包缓存、CRC 校验计算都可以放心用内存换速度。
还有一个会被忽略的点:HD 型号的 Flash 是 2KB 一页。计算一下就会发现,擦除一个 App 区(假设 256KB)需要 128 次页擦除,如果协议设计得不好,每擦一页就让上位机等一次,整个刷写过程会拖到天荒地老。所以后续协议设计里,我会优先考虑“连续擦除,再连续写入”的方式,而不是边收边擦。
1.3 Bootloader 方案的总体架构
这套 CAN Bootloader 我拆成三部分:下位机 Bootloader 程序、上位机刷写工具、CAN 通信协议。下位机 Bootloader 在芯片上电时先跑,判断是否需要升级:如果需要,就进入 CAN 接收模式,配合上位机完成擦除、写入、校验;如果不需要,直接跳转到 App。上位机负责读取待烧录的 bin 文件、按协议分包、发送、等待应答、校验结果。而协议是两者之间的契约,必须明确定义帧格式、帧类型、时序和容错机制。
从整体流程来看,一次完整的 CAN 升级大概是这样的:上位机发送握手帧 → Bootloader 回复版本和分区信息 → 上位机发送擦除指令 → Bootloader 擦除 App 区 → 上位机逐包发送数据帧 → Bootloader 写入 Flash 并回 ACK → 上位机发送校验指令 → Bootloader 回传 CRC 或逐包校验结果 → 上位机发送跳转指令 → Bootloader 跳转 App。
这里有个容易踩的坑:很多人会跳过握手和版本协商,直接发数据帧。早期我用这种方法做原型,一旦波特率或帧格式不匹配,表现就是“数据发了,板子没反应”,然后开始怀疑接线、怀疑 CAN 收发器,实际上就是协议缺了握手这一步。后来我把握手帧设计成固定 ID + 固定数据内容,上位机和下位机互相确认“我们能通信”,再开始刷写,排错难度直接降了一个量级。
2. 核心细节解析与关键设计决策
2.1 CAN 报文格式与 ID 分配
CAN 2.0 标准帧有 11 位 ID,扩展帧有 29 位 ID。在这个 Bootloader 项目里,我建议用标准帧就够了,一个节点升级场景最多十几个帧类型,11 位 ID 完全装得下。用扩展帧反而增加了调试复杂度,部分 CAN 分析工具对扩展帧的过滤设置也更繁琐。
帧类型我分成了五类,每类分配一个固定 ID:
| 帧类型 | ID(示例) | 方向 | 数据内容 |
|---|---|---|---|
| 握手帧 | 0x100 | 上位机→Bootloader | 魔数 + 固件版本 + App 起始地址 |
| 版本应答帧 | 0x101 | Bootloader→上位机 | Bootloader 版本 + 分区大小 + 页大小 |
| 擦除帧 | 0x102 | 上位机→Bootloader | 起始扇区 + 扇区数量 |
| 数据帧 | 0x103 | 上位机→Bootloader | 序列号(2字节) + 偏移(2字节) + 数据(4字节) |
| 控制帧 | 0x104 | 上位机→Bootloader | 命令字(校验/跳转/复位) |
| 应答帧 | 0x105 | Bootloader→上位机 | 状态码 + 当前序列号 |
每个 CAN 数据帧最多 8 字节,数据帧里实际能装有效数据的只有 4 字节,传输效率不算高,但是换来的是协议简单、每包独立校验。工业现场升级,200KB 固件按每包 4 字节计算需要 51200 包,按 500Kbps 波特率跑,加上帧间隔和应答时间,实测大概 2~3 分钟,这个时间在可接受范围内。如果想要更快的速度,可以把数据帧改成“多发一帧少回一帧 ACK”的滑动窗口模式,但协议复杂度会成倍上升,初期不建议。
2.2 CRC 校验策略:分包校验 + 整体校验
CAN 物理层自带 CRC15 校验,但那是校验“这一帧传输过程中有没有出错”,不能证明“收到的数据就是上位机想发的正确数据”。所以应用层必须再做一次校验。我用的是双层校验:每包数据用一个字节的简单和校验(对 8 字节数据求和取低 8 位),上位机收到 ACK 才发下一包;全部发完后,Bootloader 对整个 App 区做一次 CRC32 计算,回传给上位机比对。
这里有个实际教训:早期版本只做了逐包和校验,觉得物理层 CRC15 已经够用了。后来有一次现场升级,刷写完成、校验也通过,但 App 启动后行为异常,最后定位到是上位机读取 bin 文件时读错了一个字节,这个字节恰好没被单包校验发现(和校验冲突)。从那以后,我坚持在收完所有数据后再做一次整体 CRC32,上位机也在本地先算好 CRC32,两边比对一致才允许跳转。
2.3 Flash 分区规划与地址映射
分区规划是整个 Bootloader 最不能出错的部分。我用的方案是把 512KB Flash 切成三块:
| 分区 | 起始地址 | 大小 | 说明 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB (0x10000) | 2KB 页 × 32 页,实际上 20KB 就够,留余量 |
| App | 0x08010000 | 384KB (0x60000) | 应用程序区,上电默认跳到这里 |
| 备份区 | 0x08070000 | 64KB (0x10000) | 可选,存回滚固件或升级缓存 |
Bootloader 本身会占用中断向量表(放在 0x08000000),这部分不允许被擦除。App 的起始地址是 0x08010000,注意这里必须至少按扇区对齐,否则擦除时会误伤 Bootloader 的数据。我自己一般习惯留到足够大,Bootloader 给 32KB 都绰绰有余,但分 64KB 也没毛病,反正是自己的地盘自己说了算。
有一个细节:HD 型号的页大小是 2KB,但每两页组成一个 4KB 的扇区。不同的系列(如 F105/F107)扇区结构还不一样,写代码前务必翻参考手册的 Flash 编程章节,别拿 F1 的扇区结构套用其他型号。
2.4 Bootloader 跳转 App 的核心逻辑
跳转是整个 Bootloader 成败的关键点。很多人写完 Bootloader,发现能收数据、能写 Flash,但“跳转后 App 不运行”,十有八九是跳转细节没处理好。上电后 Bootloader 的流程是这样:
- 初始化时钟、CAN、串口(串口用于调试)。
- 检查是否需要进入升级模式:通过一个标志位判断。这个标志位可以放在备份寄存器(RTC Backup Register)里,也可以放在 Flash 末尾的一个固定地址。
- 不需要升级时,检查 App 区的启动标志(我习惯在 App 起始地址写一个固定魔数,比如 0xDEADBEEF),魔数有效才跳转。
- 跳转前必须关闭全局中断、关闭已经打开的外设时钟(重点是 CAN)、把 SysTick 停掉,然后读取 App 区首字作为 MSP,读取第二个字作为 PC,设置向量表偏移(SCB->VTOR = 0x08010000),最后用函数指针跳转。
下面这个是跳转的核心代码,我用标准外设库写的,虽然现在很多人转 HAL 了,但这个逻辑是通用的:
#define APP_ADDR 0x08010000U #define APP_MAGIC 0xDEADBEEFU typedef void (*pFunction)(void); void jump_to_app(void) { uint32_t app_stack = *(volatile uint32_t *)APP_ADDR; uint32_t app_pc = *(volatile uint32_t *)(APP_ADDR + 4); pFunction jump; // 检查 App 是否已经烧录 if (*(volatile uint32_t *)(APP_ADDR) != APP_MAGIC) { return; // 没有有效 App,继续留在 Bootloader } // 关闭全局中断,确保跳转前没有中断打扰 __disable_irq(); // 如果有使用 RTOS,这里还要挂起所有任务;裸机则简单一些 // 关闭已打开的外设时钟,尤其是 CAN,避免跳转后外设状态残留 RCC_APB1PeriphResetCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB1PeriphResetCmd(RCC_APB1Periph_CAN1, DISABLE); RCC_APB2PeriphResetCmd(RCC_APB2Periph_AFIO, ENABLE); RCC_APB2PeriphResetCmd(RCC_APB2Periph_AFIO, DISABLE); // 设置向量表 SCB->VTOR = APP_ADDR; // 设置主栈指针,跳转到 App Reset_Handler __set_MSP(app_stack); jump = (pFunction)app_pc; jump(); }这段代码里最容易忽略的是SCB->VTOR的设置顺序:必须先设置向量表,再设置 MSP。如果搞反了,中断一旦到来,CPU 会去老的向量表找中断入口,跑飞是必然的。另一个坑是:__disable_irq()只能关掉 Cortex-M3 内核的全局中断,如果 App 用到了某些外设中断,但这些外设在跳转前没有复位,App 初始化这些外设时可能会读到残留状态,所以我跳转前会把用过的外设统一复位一遍,图个干净。
3. 实操过程与核心环节实现
3.1 Bootloader 端代码结构
Bootloader 工程我个人喜欢用标准外设库写,原因很简单:代码直接操作寄存器,逻辑清晰,占用的 Flash 也小。用 HAL 库写 Bootloader 不是不行,但 HAL 库初始化的代码量大,一个简单的 CAN 初始化就能吃掉好几 KB Flash,对 Bootloader 这种追求精简的场景不太划算。
整个 Bootloader 工程的文件结构大致如下:
CAN_Bootloader/ ├── Core/ │ ├── main.c // 主流程:初始化、判断升级、进入升级循环 │ ├── can_boot.c // 协议处理核心:状态机、帧解析、Flash 写入 │ └── can_boot.h ├── Hardware/ │ ├── bsp_can.c // CAN 外设初始化、过滤器配置 │ └── bsp_flash.c // Flash 擦除、写入、读取封装 └── MDK-ARM/ // 工程文件主流程的状态机是协议的精髓,我这样定义:
typedef enum { BOOT_IDLE = 0, // 空闲,等待握手 BOOT_HANDSHAKE, // 已握手,等待擦除或数据 BOOT_ERASING, // 正在擦除 BOOT_WRITING, // 正在接收数据并写入 BOOT_VERIFY, // 校验阶段 BOOT_JUMP // 跳转 App } boot_state_t;状态机的好处是逻辑清晰,遇到非法状态可以直接复位回 BOOT_IDLE,不会出现“收了一堆数据帧,但 Bootloader 卡在不知道哪里”的情况。用状态机还有一个附加收益:可以加超时看门狗。如果上位机在某个状态停了超过 5 秒没发后续帧,Bootloader 自动复位回 Bootloader 模式,避免设备“半升级”挂在现场——这个设计救过我一次,有次上位机软件崩溃,板子停在擦除状态,如果没有超时,现场就得到一台只能拆机救活的设备。
3.2 CAN 初始化与波特率配置
CAN 波特率看起来是常规操作,但细算一下还挺容易出问题。我要求 500Kbps 的波特率,STM32F103 的 CAN 外设挂载在 APB1 总线上,APB1 时钟默认是 36MHz(如果系统时钟是 72MHz)。CAN 的波特率由分频寄存器 BRP 和时间段(BS1、BS2)共同决定:
波特率 = APB1 时钟 / ((1 + BS1 + BS2) × BRP)
我常用的配置是:BRP = 4,BS1 = 7,BS2 = 6,采样点 = (1 + 7) / (1 + 7 + 6) = 50%。但实际 500Kbps 总线,采样点推荐 75% 左右更稳。所以调整为 BRP = 4,BS1 = 9,BS2 = 4,采样点 = (1 + 9) / (1 + 9 + 4) ≈ 71.4%。这个采样点兼容性比较好,实测在较长总线下依然稳定。
写配置的时候有个前提——APB1 时钟到底是不是 36MHz,取决于系统时钟配置。如果用的外部 8MHz 晶振,PLL 倍频到 72MHz,APB1 预分频是 2,APB1 就是 36MHz。但如果你用的是内部 RC 或者其他晶振,这个数字会变。所以我在初始化代码里直接调用RCC_GetClocksFreq()动态读取 APB1 时钟,再反算 BRP 和 BS 段,而不是写死:
RCC_ClocksTypeDef clocks; RCC_GetClocksFreq(&clocks); uint32_t apb1 = clocks.PCLK1_Frequency; uint32_t brp = 0; uint8_t bs1 = 0, bs2 = 0; // 目标波特率 500Kbps // 先选 BS1=9, BS2=4,总时间段 = 1+9+4 = 14 // BRP = APB1 / (500000 * 14) brp = apb1 / (500000U * 14U) - 1U;这个动态计算方法比直接写死寄存器强,至少换晶振、换板子的时候不用重新查参考手册算半天。
3.3 Flash 擦除与写入的细节
Flash 操作在 STM32F103 上有几个铁律:擦写前必须解锁,写完必须加锁;擦除和编程过程中不能有中断发生(所以要在擦除和编程前关闭中断,结束后再打开);擦除操作必须按页或按扇区对齐。
我的擦除函数这样处理:
void flash_erase_pages(uint32_t start_addr, uint32_t num_pages) { FLASH_Unlock(); __disable_irq(); for (uint32_t i = 0; i < num_pages; i++) { if (FLASH_ErasePage(start_addr + i * FLASH_PAGE_SIZE) != FLASH_COMPLETE) { // 擦除失败,建议记录错误并进入错误状态 break; } } __enable_irq(); FLASH_Lock(); }写入函数同理,每写一个字(32 位)检查一次状态位,遇到 FLASH_BUSY 需要等待,如果连续超时就要报错。一个让我印象深刻的坑:擦除过程中如果收到了 CAN 中断,由于我正在关中断,CAN 外设的 RX FIFO 会溢出,等擦除结束再开中断,已经丢了若干帧数据。这就是为什么协议上要“先擦除,再开始接收数据”,而不是边收边擦。
3.4 上位机的实现思路
上位机这块,我常用脚本语言写原型,工程上一般用 C#、Python 或者 LabVIEW。热词里提到了 LabVIEW 实现 Bootloader 上位机,确实 LabVIEW 做这类工具可视化程度高,但跨平台和版本管理比较痛苦。我的个人偏好是:快速验证用 Python + python-can 库,面向现场交付用 C# WinForms/WPF。
上位机的核心逻辑其实就是协议的对端:
- 读取 bin 文件,按 4 字节一组分包,不足补 0xFF。
- 本地计算整个 bin 的 CRC32。
- 发送握手帧,等待 Bootloader 的版本应答。
- 发送擦除帧,等待擦除完成 ACK。
- 逐包发送数据帧,每包等待 ACK,超时重发(最多重发 3 次)。
- 发送校验帧,等待 CRC32 回传,与本地比对。
- 比对通过,发送跳转帧。
这里有一个非常实用的技巧:上位机发送数据包时,不是每发一包就死等 ACK,那太慢了。我会开启一个接收线程,专门收 ACK,主线程按固定周期(比如 5ms)持续发包,如果某个序列号的 ACK 在超时时间内没收到,就重发这个序列号的包。这样一个简单的“流水线”就能让刷写速度快不少,而且代码不算太复杂。
3.5 实测刷写流程
我用一个 128KB 的 App bin 实测了一下,500Kbps CAN 波特率,整体刷写流程耗时约 2 分 30 秒。整个过程分成四个阶段:
| 阶段 | 耗时 | 说明 |
|---|---|---|
| 握手协商 | 约 10ms | 基本都是瞬时完成 |
| 擦除 | 约 0.5s | 128KB / 2KB 每页 = 64 页擦除 |
| 数据写入 | 约 2 分钟 | 128KB / 4 字节每包 = 32768 包,每包间隔 3~4ms |
| 整体校验 | 约 10ms | Bootloader 本地 CRC32 |
刷完跳转后,App 第一次启动会花一点点时间重新初始化外设,这期间波形上能看到 CAN 总线停顿一会儿,属于正常现象。如果 App 启动时还要和外部设备通信,建议在 App 初始化时对外发一个“启动完成”的心跳帧,方便现场确认升级成功。
4. 常见问题与排查技巧实录
4.1 跳转后 App 不运行
这是出现频率最高的问题,每次有人来问我“Bootloader 跳不过去”,我基本按这个顺序排查:
先确认 App 工程的启动地址有没有改。很多人 Bootloader 写好了,App 工程忘了在 MDK 或 IAR 里把 Linker 的起始地址改成 0x08010000,结果 App 还是编译到 0x08000000,和 Bootloader 冲突。跳转过去自然跑飞。这个错误在 HEX 文件里一眼就能看出来——打开 App 的 hex,第一条记录地址如果还是 0x08000000,那就没改对。
再看SCB->VTOR是否设置在跳转前完成。HAL 库工程里,SystemInit 函数会在 main 之前调用,它会重置 VTOR 到默认值 0x08000000,所以如果你在 Bootloader 跳转前设置了 VTOR,但 App 的 SystemInit 又把它重置了,App 的中断向量就会错位。解决办法是 App 在 main 开头重新设置SCB->VTOR = 0x08010000,或者在 SystemInit 里修改。
最后检查中断向量表是否真的被映射到 App 区。可以在 App 的 main 最前面设置一个 GPIO 翻转,然后用示波器看有没有波形,有波形说明跳转成功,后续死机就是 App 自身初始化的问题了。
4.2 擦除失败或写 Flash 超时
Flash 写入失败的常见嫌疑:写入了非对齐地址、Flash 没有解锁、Flash 处于上锁状态、擦除期间总线上还在传数据导致的中断干扰。我的排查方式是先确认地址对齐——页擦除起始地址必须是 2KB 的整数倍,半字编程地址必须是 2 字节对齐。其次是检查时钟树,如果 Flash 等待周期配置有误(72MHz 主频下要配 2 个等待周期),Flash 操作也会异常。
还有一种比较隐蔽的情况:HSE 晶振启动失败导致系统跑到 HSI 上,APB1 时钟不是 36MHz,Flash 时序也跟着不对。当然这是整体系统问题,但一旦发生,表现就是“擦除偶尔失败,时好时坏”。
4.3 CAN 总线无应答
上位机发握手帧,Bootloader 没回包。先别查 Bootloader 代码,先用 CAN 分析仪挂在总线上,看看报文有没有到总线、Bootloader 有没有发任何数据。如果总线上什么都没有,检查 CAN 收发器(比如 TJA1050)的供电和 STBY 引脚,STBY 接高电平了芯片直接进入待机,收发器根本不在总线状态——这个低级错误我犯过不止一次。
如果总线能看到上位机的帧,但 Bootloader 没回包,多半是过滤器配置问题。F103 的 CAN 过滤器是 32 位屏蔽/列表模式,配置不对会把所有帧都滤掉。我的建议是调试阶段把过滤器设成“接收所有帧,然后在中断里按 ID 软件过滤”,等协议稳定了再改硬件过滤,排查问题会轻松很多。
常见问题速查表:
| 现象 | 优先级排查点 | 具体动作 |
|---|---|---|
| 握手无响应 | CAN 收发器状态 | 检查供电和 STBY 引脚 |
| 握手无响应 | 过滤器配置 | 先配置为接收所有帧 |
| 握手无响应 | 波特率不匹配 | 用示波器/分析仪核对位宽 |
| 擦除失败 | Flash 等待周期 | 确保 FLASH_ACR 配置正确 |
| 擦除失败 | 地址对齐 | 确认 2KB 页对齐 |
| 写入不完整 | 数据帧分包 | 检查偏移量是否正确递增 |
| 跳转后死机 | App 链接地址 | 检查 App 工程 Linker 是否改为 0x08010000 |
| 跳转后死机 | 外设残留 | 跳转前复位 CAN 和 AFIO 时钟 |
| 校验不一致 | 上位机补位值 | 最后不足 4 字节时统一补 0xFF |
4.4 实测踩坑记录
有一个坑我之前始终没想明白:一套代码在开发板上跑得好好的,拿到现场就出现“刷一半卡死”。后来用 CAN 分析仪抓包发现,现场总线上有其他设备的报文,ID 恰好和我的数据帧 I D冲突了。CAN 总线是仲裁机制,ID 越小优先级越高,我的数据帧 ID 是 0x103,现场那台设备的某个报文 ID 是 0x100,它一直在发周期报文,导致我的数据帧经常抢不到总线。解决办法很简单:给 Bootloader 报文分配一个比其他业务报文更低优先级(ID 数值更大)的 ID 段,或者让上位机在刷写前通知其他节点暂停发送。
另一个坑是 STM32F103 的 CAN 接收 FIFO 溢出。F103 的 CAN1 只有 3 个邮箱接收 FIFO,如果上位机发得飞快,Bootloader 中断里又在做 Flash 写入(写入期间中断被关了),FIFO 就会溢出。早期我用边收边写的方式,结果溢出丢包率高达 5%。后来改成“FIFO 满了先回发送端暂停”的流控,问题才缓解。前面提到的流水线重发机制也是针对这个问题的兜底方案。
5. 关于升级安全和扩展方向
5.1 让 Bootloader 更稳的几个改进
基础版跑通之后,往下走可以考虑这几点:一是双分区(A/B 分区)备份升级,Bootloader 总是先刷进备份区,校验无误再切换激活分区,A/B 分区方案最怕“升级到一半断电”的场景,备份分区能保证至少有一个可用的固件。二是加密传输,CAN 总线上抓包就能拿到你的固件,如果对知识产权有要求,可以在上位机做 AES-128 加密,Bootloader 端解密再写入。三是增加版本回滚能力,在 Flash 里存一个“上一次可用的固件版本号”,新版固件启动后一定时间内上报健康状态,超时未上报就回滚旧版——这个机制在远程升级大量车载节点时非常有用。
5.2 在现有 Bootloader 上扩展其他总线支持
CAN Bootloader 做出来后,接其他传输通道就只是换一层“传输接口”的事。我把协议层和物理层做了分离:底层是一组send_frame()和receive_frame()接口,上层跑同一个状态机。后来项目里需要支持 RS485 升级,我把 CAN 底层的收发函数换成 UART 的收发,协议层几乎没动逻辑就复用了。这个设计让我在后来的 LIN 总线升级、以太网升级项目中省了至少两周的开发时间,建议你在动手写 Bootloader 时就留好这个抽象层,别把协议逻辑和 CAN 寄存器操作混在一堆。
5.3 一些个人体会
做了好几版 Bootloader,我的体会是:Bootloader 是典型的“看起来简单、实际上全是细节”的活。协议设计阶段多想一步,后面能少排好几个通宵的错。尤其是握手、状态机、超时机制这三样,一定不能图省事跳过去。很多项目出问题,不是因为代码复杂,而是因为没做握手就开始发数据,没有状态就乱收帧,没有超时就死等。
另外,如果你准备把这个方案用到正式产品里,务必给 Bootloader 的版本号留个位置,每次修改 Bootloader 后更新版本号,并把它通过 CAN 报文上报给上位机。别小看这个版本号,当现场有几百台设备,其中十几台 Bootloader 版本不一致时,这个字段能救你命。我在一个实际项目中就遇到过“同一批设备,两种 Bootloader 版本”,上位机协议不兼容,一台一台拆机刷 Bootloader 升级,那种痛苦经历过一次就不想再有第二次了。
CAN Bootloader 说难不难,说简单也不至于。把握手、分包、CRC、跳转这几件事做扎实,一台原本需要拆机烧录的设备,就能变成通过 CAN 总线远程升级的“云设备”。希望这篇内容能帮你少走点弯路,把更多时间花在业务功能的迭代上,而不是和 Flash 扇区、中断向量表死磕。
本文还有配套的精品资源,点击获取