☰
STM32F103 CAN Bootloader完全攻略:Flash分区与协议实现
2026/10/23 15:59:22 网站建设 项目流程

简介:面向嵌入式软件工程师与单片机开发者的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 放在后面,分区不合理,后面必然踩坑。给一个常用参考配置:

区域起始地址大小用途
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、数据帧格式、波特率与 SJW

2.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校验请求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))

不同速率下推荐配置如下:

波特率PrescalerBS1BS2SJW采样点
125kbps18114175%
250kbps9114175%
500kbps4134177.8%
1Mbps383175%

为什么要特别关注采样点?发送节点在上升沿同步,接收节点会在位时间约 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 完整升级流程:一帧一步应答

整套流程可以拆成下面几步,每一步都有明确应答,最大程度避免“不知道卡在哪”的情况:

  1. Bootloader 上电,先读参数区的升级标志。如果没有升级请求,则延时 100ms 等待主机握手命令,超时就跳转 App。
  2. 主机发送 0x100 请求升级,携带协议版本号。
  3. Bootloader 收到后回 0x101,进入升级模式,不再跳转 App。
  4. 主机发送 0x102 擦除请求,Bootloader 按页擦除 App 区。擦除期间建议先停 CAN 接收中断,擦完再打开。
  5. 擦除完成后回 0x103,主机开始分包发送固件。
  6. Bootloader 每收一帧 0x104,先校验帧序号是否连续,再写 Flash,写成功后回 0x105。帧序号不连续说明丢帧,直接丢弃当前帧,等上位机超时重发。
  7. 数据传完,主机发 0x106 校验请求,携带 CRC32。
  8. 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,稳定比速度重要,协议宁可简单一点,每帧都带应答和超时重发,现场能少跑好几趟。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询