简介:面向嵌入式软件工程师与单片机开发者的CAN总线引导加载工程实例,以STM32F103芯片为升级目标,可配合基于Qt开发的上位机程序,解决通过CAN总线完成固件远程升级与维护的问题,适用于汽车电子、工业自动化等需要在线更新设备的场景。压缩包为rar格式,共180个文件,以C语言与头文件源码为主,包含72个h文件、66个c文件,另有启动汇编文件、Keil工程配置和协议说明文档,整体大小仅650KB,目录紧凑便于快速定位。当前已有272人学习/下载。包内不仅提供基于STM32标准外设库的Bootloader完整实现,还包含通信协议定义与固件写入流程的说明,可帮助读者系统理解CAN帧格式、握手交互、数据校验、错误重传以及升级失败回滚等关键机制;既适合入门CAN总线升级原理,也可以直接导入工程进行二次开发,快速落地到实际的嵌入式产品维护方案中。 拆开 can_bootloader_f1031.rar 这个压缩包,我一开始以为又是一堆网上东拼西凑的裸代码,翻完之后才发现它把上位机、下位机、协议文档都收进来了,直接用来做 STM32F103 的 CAN 总线 Bootloader 完全可行。做嵌入式的人应该都有同感:给设备做 IAP 不难,难的是在没有调试口、没有串口线、设备已经装上车或者锁在机柜里的情况下,把新固件安全地刷进去。CAN Bootloader 就是在这种场景下最实用的一套方案。
这套工程选择 STM32F103 作为下位机,通过 CAN 总线接收主机发来的固件数据,写入 Flash 后完成升级。适合参考的人群包括做汽车电子、工程机械、BMS 电池管理、工业控制器、CANopen/J1939 应用,以及已经写过 UART IAP 想切换到 CAN IAP 的工程师。我尽可能按照实际做项目的思路,把硬件选型、Flash 分区、CAN 协议、升级流程、问题排查完整拆一遍,争取让你看完能少踩几个坑。
1. 这套 CAN Bootloader 到底解决什么问题:选型与分区
1.1 为什么偏偏是 STM32F103 + CAN
很多项目在选升级通道时会被硬件接口限制:如果板子上有 RS485,很多人会直接用 485 做 IAP,成本低、实现简单。但 485 是单主结构,一台主机轮询几十个从站,升级时得一台一台来;而且 485 的差分信号抗共模干扰能力一般,长距离大功率设备旁边容易出问题。CAN 就不一样,它是多主总线,天然支持消息仲裁和广播,非常适合一挂几十个节点的车载和工业现场。
STM32F103 被选中的原因也简单:出货量极大、资料多、价格低,而且内置了 bxCAN 控制器,支持标准帧和扩展帧、FIFO、过滤器,最高速率 1Mbps。同价位的 MCU 虽然也有带 CAN 的,但 F103 的软硬件生态最成熟,拿来做 Bootloader 不管是移植还是验证都很省事。需要提醒的是,F103 的 bxCAN 只支持经典 CAN,不支持 CAN FD,如果未来项目要跑 5Mbps 以上的大数据量升级,建议平台迁移到带 FDCAN 的型号,协议设计思路仍然可以沿用。
1.2 Flash 分区是 Bootloader 的第一个关键决定
拿到这套工程,先别看代码,先把 Flash 分区表看清楚。STM32F103C8 是 64KB Flash,1KB 一页。Bootloader 放在低地址区,App 放在后面,分区不合理,后面必然踩坑。给一个常用参考配置:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 16KB | IAP 程序固定区 |
| App | 0x08004000 | 47KB | 应用固件区 |
| 参数/标志区 | 0x0800FC00 | 1KB | 升级标志、版本号、参数保存 |
关键点:App 起始地址必须对齐到 1KB 页边界,因为擦除是按页来的。如果 App 起始地址没有落在页边界,擦除时会连带把 Bootloader 所在的页擦掉,代码直接变砖。计算办法很简单,Bootloader 占用 N 页,就把 App 放在 0x08000000 + N × 0x400,上面的 16KB 正好 16 页,App 起始 0x08004000 刚好对齐。
参数区建议独立存一个标志字,用于保存“待升级”状态和版本号。为什么要单独划一块?因为升级过程中如果断电,Flash 里部分数据可能处于半擦除状态,如果你把标志写在 App 区里,下次上电 Bootloader 一检查可能就懵了。独立参数区能让 Bootloader 每次都稳定判断到底该跳 App 还是留在升级模式。
2. CAN 通信层协议设计:报文 ID、数据帧格式、波特率与 SJW
2.1 报文 ID 怎么规划,数据段怎么编排
写协议前先想清楚一个问题:CAN 数据帧一次最多带 8 字节数据,升级一个 40KB 的 App 需要几千帧,协议设计不好,帧序号、校验、应答全堆在 8 字节里,很快就不够用。见过最简单实用的做法是:各类命令分别用独立的标准帧 ID,数据帧再用 ID 或数据段承载帧序号。
| 方向 | CAN ID | 含义 | 数据段内容 |
|---|---|---|---|
| 主机 → Bootloader | 0x100 | 请求升级 | 0xAA 0x55 + 协议版本 |
| Bootloader → 主机 | 0x101 | 升级模式确认 | Bootloader 版本号 |
| 主机 → Bootloader | 0x102 | 擦除请求 | App 起始地址、长度 |
| Bootloader → 主机 | 0x103 | 擦除完成 | 状态字 |
| 主机 → Bootloader | 0x104 | 固件数据帧 | 帧序号 + 7 字节固件数据 |
| Bootloader → 主机 | 0x105 | 接收确认 | 帧序号 |
| 主机 → Bootloader | 0x106 | 校验请求 | CRC32 |
| Bootloader → 主机 | 0x107 | 校验结果 | 通过/失败状态 |
数据帧内部再设 [帧序号][数据...],比如 0x104 的 8 字节里面,第 1 字节放帧序号,后 7 字节放固件数据,这样一帧有效数据 7 字节,32KB 固件大约 4700 帧就能传完。帧序号到 255 之后怎么办?可以在序号到达 255 时发送一帧“清序号控制帧”,上位机收到后归零继续发;或者直接采用两字节序号,牺牲 1 字节数据空间。多数场景下单字节序号加控制帧已经够用。
应答机制也很关键。Bootloader 每收一帧数据,写 Flash 成功以后回一帧 ACK,上位机如果超时 50ms 没收到 ACK,就重发当前帧。这里要注意:如果连续重复 3 次以上,说明总线质量或者波特率有问题,停止升级并提示,不要无限重试。
2.2 波特率公式与 SJW/采样点取值
STM32F103 的 CAN 挂在 APB1 总线上,时钟最高 36MHz。位时间由同步段(固定 1Tq)、传播/相位缓冲段 BS1 和 BS2 组成,总线波特率的公式是:
波特率 = PCLK1 / (Prescaler × (1 + BS1 + BS2))
不同速率下推荐配置如下:
| 波特率 | Prescaler | BS1 | BS2 | SJW | 采样点 |
|---|---|---|---|---|---|
| 125kbps | 18 | 11 | 4 | 1 | 75% |
| 250kbps | 9 | 11 | 4 | 1 | 75% |
| 500kbps | 4 | 13 | 4 | 1 | 77.8% |
| 1Mbps | 3 | 8 | 3 | 1 | 75% |
为什么要特别关注采样点?发送节点在上升沿同步,接收节点会在位时间约 75%~80% 的位置采样电平,如果采样点太靠后或太靠前,在总线过长、信号边沿延迟大时,很容易把临界电平误判成有效数据。多节点场景下,所有节点最好都用相同的采样点配置,不同采样点会造成兼容性问题,这也是 CAN 一致性测试里会反复检查的指标。
SJW 全称是同步跳跃宽度,它决定了节点在重同步时允许位时间伸缩的最大 Tq 数,取值 1~4。只要节点间晶振偏差不大,SJW=1 或 2 足够;如果用了内部 HSI 时钟或者现场温度变化大,可以调到 2~3。CAN 协议规定 SJW 必须大于等于 1,通常不建议超过 BS2,否则可能干扰正常位流。F103 在配置 CAN_BTR 时 SJW 位是 2bit,0~3 分别对应 1~4Tq。
3. 升级流程和 Flash 写入实战:从握手到跳转
3.1 完整升级流程:一帧一步应答
整套流程可以拆成下面几步,每一步都有明确应答,最大程度避免“不知道卡在哪”的情况:
- Bootloader 上电,先读参数区的升级标志。如果没有升级请求,则延时 100ms 等待主机握手命令,超时就跳转 App。
- 主机发送 0x100 请求升级,携带协议版本号。
- Bootloader 收到后回 0x101,进入升级模式,不再跳转 App。
- 主机发送 0x102 擦除请求,Bootloader 按页擦除 App 区。擦除期间建议先停 CAN 接收中断,擦完再打开。
- 擦除完成后回 0x103,主机开始分包发送固件。
- Bootloader 每收一帧 0x104,先校验帧序号是否连续,再写 Flash,写成功后回 0x105。帧序号不连续说明丢帧,直接丢弃当前帧,等上位机超时重发。
- 数据传完,主机发 0x106 校验请求,携带 CRC32。
- Bootloader 对 App 区重新计算 CRC32,比对一致则回 0x107,延时后跳转;不一致则回错误状态,留在 Bootloader 等待重新升级。
这个流程最大的好处是每一步都有应答,断点续传也比较容易扩展:主机可以从最后一帧序号继续发,不用重新擦整个 Flash。量产项目里建议:只要擦除过,重启后没有收到完整固件,就保持“待升级”状态,不要跳到一个半擦除的 App 里。这相当于给设备留了救砖的机会。
3.2 Flash 擦除和写入的实现细节
STM32F103 的 Flash 操作有三个硬性规矩:必须先解锁,擦除最小单位是整页,写入必须按半字(16 位)进行。下面给一个能在工程里直接用的擦除函数:
void flash_erase_app(uint32_t app_addr, uint32_t app_size) { uint32_t page = app_addr; FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); while (page < app_addr + app_size) { if (FLASH_ErasePage(page) != FLASH_COMPLETE) { // 记录错误,进入错误处理 break; } page += 1024; // F103C8 每页 1KB } FLASH_Lock(); }写入的时候也不能直接 memcpy 到 Flash 地址,必须将数据组装成半字,逐次调用 FLASH_ProgramHalfWord。比如从 CAN 数据帧中取出 7 字节,我要先在 RAM 里拼一个 2 字节对齐的缓冲,末尾不足 2 字节时用 0xFF 补齐,然后循环写入:
uint16_t temp; FLASH_Unlock(); for (int i = 0; i < len; i += 2) { temp = buf[i] | (buf[i + 1] << 8); FLASH_ProgramHalfWord(addr + i, temp); } FLASH_Lock();一个容易忽略的细节:CAN 接收中断里千万不要直接做 Flash 擦写。Flash 擦写期间 CPU 访问 Flash 会卡住,如果中断服务函数里还在做别的 Flash 操作,整个时间线就乱了。正确做法是 CAN 接收中断只把数据搬到 RAM 缓冲,擦写在主循环里完成。这也是很多初版 Bootloader 跑到第二十几帧就卡死的重灾区。
3.3 跳转 App 之前必须处理好的两个细节
跳转不能简单复位。第一要关闭所有中断,并把用到的外设复位,防止 Bootloader 的中断在 App 里被触发;第二是把栈指针设置为 App 中断向量表的第一个字,再把 PC 指向第二个字(复位向量),这才是标准跳转。
typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { pFunction jump; uint32_t stack_addr = *(volatile uint32_t *)app_addr; uint32_t reset_vector = *(volatile uint32_t *)(app_addr + 4); // 栈顶地址要落在 RAM 范围内,做一次预防性检查 if ((stack_addr & 0xFFF00000) != 0x20000000) { return; } __disable_irq(); SysTick->CTRL = 0; // 如果之前开了外设中断,这里统一做外设复位 __set_MSP(stack_addr); jump = (pFunction)reset_vector; jump(); }跳过去之后,App 工程里还必须设置中断向量表偏移:SCB->VTOR = app_addr。不做这一步,任何中断一来,CPU 会跑到 0x08000000 的 Bootloader 向量表,等于帮 Bootloader 做了个“清屏”,表现就是 App 偶尔跑飞。另一个高频需求是 App 里主动触发升级,做法是在参数区写一个“进入 Bootloader”标志字,然后调用 NVIC_SystemReset 复位,Bootloader 上电检查到这个标志就不再跳 App,原地等待握手命令。
4. 常见问题与波形排查:实测记录的避坑清单
4.1 升级失败常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 握手正常,传输几帧后停止 | 丢帧、ACK 超时、FIFO 溢出 | 检查帧序号逻辑,上位机加超时重发,中断里只收数据不要写 Flash |
| 总线上出现大量错误帧 | 波特率分频算错、采样点太后 | 用 CAN 分析仪测实际波特率,先开回环模式验证协议 |
| 跳转后 App 跑飞 | 没设置 VTOR、App 链接地址不对、外设中断没关 | 检查 App 工程的 FLASH ORIGIN,补上 VTOR 设置 |
| 全部传完但 CRC 校验失败 | 数据错位、末尾字节补齐补错 | 检查每帧有效字节数,末尾统一补 0xFF 再参与校验 |
| 板子偶发进不了升级模式 | 等待握手时间太短、标志位被意外擦除 | 加长等待窗口,参数区标志独立保存并做冗余校验 |
这里特别说一下 FIFO 溢出。bxCAN 的接收 FIFO 深度有限,如果上位机发得太快,CAN 中断还没来得及取走数据,新帧来了就会覆盖或者丢帧。可以先试试把单帧间隔拉长到 5~10ms,确认协议稳定后再一点点缩短,不要一上来就跑极限。多节点同时升级时也要考虑总线负载,建议把峰值占用率控制在 50% 以下。
4.2 从 CAN 波形判断通信好坏
很多新手遇到“偶尔升级失败”会先怀疑协议,其实多数问题在物理层。CAN 波形可以直接用示波器看:空闲状态时 CANH/CANL 都应稳定在 2.5V 附近,总线两端都要接 120 欧终端电阻,否则反射导致误位;帧起始 SOF 是显性电平,与后续电平切换正常;设置 500kbps 时一个 bit 时间是 2us,可以用示波器光标量一下显性位的宽度,如果不准,说明波特率分频配置或晶振偏差有问题。
重点观察隐性到显性的下降沿附近有没有明显回勾、振铃或毛刺,如果毛刺接近采样点位置,就会出现随机丢帧。对策是减少总线长度、降低波特率、调整 SJW 吸收抖动。在硬件防护上,建议用 TVS 加共模电感,严重场合再加隔离;气体放电管响应速度是微秒级,而 CAN 收发器在这种浪涌下早就被击穿了,不适合直接并联在总线收发端。
没有专业一致性测试工具时,可以做个土办法:把波特率表里采样点更靠近 80% 的配置替换当前配置,看升级失败率是否下降。如果明显下降,说明原来的采样点余量不足。实际项目里还可以先打开芯片的 LoopBack 回环模式,把 CAN 控制器自发自收测通,再接总线,能省掉一半的物理层干扰排查时间。
最后再分享一个量产项目里的体会。这套 Bootloader 刚做完时,主机、下位机、校验、跳转全通,一到现场就出幺蛾子,后来发现不是协议问题,而是我把 Flash 擦写放在了 CAN 接收中断里,跑几十帧就卡死。改成主循环擦写之后,再没出现过。做 Bootloader,稳定比速度重要,协议宁可简单一点,每帧都带应答和超时重发,现场能少跑好几趟。
本文还有配套的精品资源,点击获取