简介面向嵌入式软件工程师与单片机开发者的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 Flash1KB 一页。Bootloader 放在低地址区App 放在后面分区不合理后面必然踩坑。给一个常用参考配置区域起始地址大小用途Bootloader0x0800000016KBIAP 程序固定区App0x0800400047KB应用固件区参数/标志区0x0800FC001KB升级标志、版本号、参数保存关键点App 起始地址必须对齐到 1KB 页边界因为擦除是按页来的。如果 App 起始地址没有落在页边界擦除时会连带把 Bootloader 所在的页擦掉代码直接变砖。计算办法很简单Bootloader 占用 N 页就把 App 放在 0x08000000 N × 0x400上面的 16KB 正好 16 页App 起始 0x08004000 刚好对齐。参数区建议独立存一个标志字用于保存“待升级”状态和版本号。为什么要单独划一块因为升级过程中如果断电Flash 里部分数据可能处于半擦除状态如果你把标志写在 App 区里下次上电 Bootloader 一检查可能就懵了。独立参数区能让 Bootloader 每次都稳定判断到底该跳 App 还是留在升级模式。2. CAN 通信层协议设计报文 ID、数据帧格式、波特率与 SJW2.1 报文 ID 怎么规划数据段怎么编排写协议前先想清楚一个问题CAN 数据帧一次最多带 8 字节数据升级一个 40KB 的 App 需要几千帧协议设计不好帧序号、校验、应答全堆在 8 字节里很快就不够用。见过最简单实用的做法是各类命令分别用独立的标准帧 ID数据帧再用 ID 或数据段承载帧序号。方向CAN ID含义数据段内容主机 → Bootloader0x100请求升级0xAA 0x55 协议版本Bootloader → 主机0x101升级模式确认Bootloader 版本号主机 → Bootloader0x102擦除请求App 起始地址、长度Bootloader → 主机0x103擦除完成状态字主机 → Bootloader0x104固件数据帧帧序号 7 字节固件数据Bootloader → 主机0x105接收确认帧序号主机 → Bootloader0x106校验请求CRC32Bootloader → 主机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))不同速率下推荐配置如下波特率PrescalerBS1BS2SJW采样点125kbps18114175%250kbps9114175%500kbps4134177.8%1Mbps383175%为什么要特别关注采样点发送节点在上升沿同步接收节点会在位时间约 75%~80% 的位置采样电平如果采样点太靠后或太靠前在总线过长、信号边沿延迟大时很容易把临界电平误判成有效数据。多节点场景下所有节点最好都用相同的采样点配置不同采样点会造成兼容性问题这也是 CAN 一致性测试里会反复检查的指标。SJW 全称是同步跳跃宽度它决定了节点在重同步时允许位时间伸缩的最大 Tq 数取值 1~4。只要节点间晶振偏差不大SJW1 或 2 足够如果用了内部 HSI 时钟或者现场温度变化大可以调到 2~3。CAN 协议规定 SJW 必须大于等于 1通常不建议超过 BS2否则可能干扰正常位流。F103 在配置 CAN_BTR 时 SJW 位是 2bit0~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稳定比速度重要协议宁可简单一点每帧都带应答和超时重发现场能少跑好几趟。本文还有配套的精品资源点击获取