ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32F107 CAN Bootloader实现指南:基于CAN总线的远程固件升级方案

STM32F107 CAN Bootloader实现指南:基于CAN总线的远程固件升级方案 简介针对STM32F107芯片的CAN引导加载程序工程包面向嵌入式开发者以及工业自动化、汽车电子领域工程师用于在没有外部烧录器的情况下通过CAN总线远程更新应用固件从而降低设备维护成本。压缩包共359个文件以145个头文件、133个C源文件为主另有72个汇编文件、Keil工程配置、说明文档和可执行工具整体仅1.27MB结构紧凑便于快速导入和二次开发。内容涵盖启动引导、程序跳转、CAN底层收发、传输数据加密、错误重传、CRC完整性校验、Flash分区擦写与容错恢复等核心机制代码层次清晰注释较完整适合作为STM32F107 Bootloader项目的参考模板也便于移植到类似产品中。同时包内附有编译脚本和二进制固件可先在Keil中编译烧录验证再结合实际CAN总线设备联调从而快速验证升级流程。已有524人学习下载对正在设计在线升级方案的工程师具有直接的借鉴意义。 做嵌入式开发这么多年现场升级固件这事儿一直是绕不开的坎。尤其是设备已经装到机柜里、现场没有调试器、外壳也封死了的时候想更新一个bug修复版本总不能把设备拆下来寄回工厂。所以这次整理的这个CANbootloader针对STM32F107的方案就是专门解决这类远程升级痛点的。它基于CAN总线实现IAP功能可以让你通过车辆或工业现场的CAN网络直接把应用程序固件刷进芯片里整个过程不需要拆卸设备也不需要额外的硬件调试工具。这个方案我实测下来的核心价值有两个一是CAN总线在工业控制和车载环境里太普及了利用现成的总线网络做升级几乎零成本二是STM32F107这颗芯片本身自带两个CAN控制器做双CAN冗余或者CAN转以太网网关都非常合适。如果你正在做车载OBD诊断设备、工业控制器的远程维护或者物联网边缘网关这类产品这套CANbootloader方案可以直接作为参考模板使用。1. 为什么选择STM32F107做CANbootloaderSTM32F107属于意法半导体的互联型产品线和常见的F103相比最大的区别在于它内置了以太网MAC控制器和两个CAN 2.0B控制器。在做CANbootloader这个项目的时候这颗芯片的双CAN特性简直是量身定做的一个CAN接口可以用于正常通信和程序升级另一个CAN接口可以保留给其他功能两条总线互不干扰。从bootloader的架构角度看F107的Flash容量分为小容量64KB、中容量256KB和大容量512KB几个档位无论哪个型号Flash操作都需要遵循标准的解锁、擦除、编程流程。它的Flash不是按字节直接写入的而是以页为单位擦除大容量型号每页2KB编程时每次写入16位半字。这个特性直接决定了bootloader的设计——上位机下发的固件数据必须按照Flash页大小进行分包和排列否则就会出现擦写错位的问题。我选择F107做CANbootloader还有个现实原因这颗芯片在CANopen、J1939协议栈上的参考资料非常多很多现成的协议库可以直接复用。如果你手头刚好有基于F107的产品在跑CAN通信加上bootloader功能不需要改动太多硬件软件层面增加一个“升级模式”的入口就行。提示如果你使用的是F105或F107注意它们的USB OTG控制器和CAN控制器共用部分引脚在做引脚分配时要避免冲突。2. bootloader的整体架构与Flash分区设计2.1 内存分区规划在做CANbootloader之前第一个要解决的问题就是Flash分区。以STM32F107VCT6为例它拥有256KB Flash起始地址是0x08000000。我通常会把Flash分成三个区域区域地址范围大小用途Bootloader区0x08000000-0x08003FFF16KB存放bootloader本身App区0x08004000-0x0801FFFF112KB存放应用程序固件暂存区可选使用片外存储或按需划分视情况而定存放接收中的固件备份Bootloader放在起始地址因为芯片上电后默认从0x08000000取指执行。16KB的空间对于CANbootloader来说完全够用——CAN驱动、Flash驱动、简单的命令解析这些加起来不会超过8KB剩余空间还可以放一些版本信息和升级日志。App区是真正跑业务逻辑的地方。为什么App起始地址要偏移到0x08004000而不是紧挨着Bootloader这里有个细节考虑到Bootloader后续可能要扩展功能比如增加加密认证预留一些空间会方便很多。如果当初把App紧贴着Bootloader放后期Bootloader哪怕多塞一个功能整个App分区都要跟着挪应用程序里的中断向量表偏移、链接脚本、固件打包工具全都要改一遍这种牵一发动全身的事还是尽量避免。2.2 中断向量表重映射App区确定后必须要处理中断向量表的问题。默认情况下STM32的中断向量表固定在Flash起始地址0x08000000也就是Bootloader所在的位置。如果App直接运行而不做任何处理一旦产生中断芯片会跳到Bootloader的向量表去取中断服务函数地址结果就是程序跑飞。F107的解决方案是使用向量表偏移寄存器。在Cortex-M3内核中可以通过设置SCB-VTOR寄存器来改变向量表的起始位置。在App的启动代码中需要在main函数最开始执行类似这样的操作#define APP_BASE_ADDR 0x08004000 void SystemInit(void) { // 其他初始化代码... SCB-VTOR APP_BASE_ADDR; }当然不同库函数版本的写法有差异有些老的固件库可能没有在SystemInit里处理VTOR你就需要在main函数开头手动加上这句。还要注意一点向量表重映射之后App里的所有中断配置UART、CAN、定时器等才能正常工作否则会出现中断完全无效或者死机的现象。2.3 链接脚本LD文件的调整向量表偏移只是软件层面的第一步真正决定App能不能跑在正确位置的是链接脚本。如果你用的是Keil MDK需要在Options for Target里把IROM1的起始地址改为0x08004000大小改为0x1C000112KB。如果用STM32CubeIDE或GCC工具链则要修改链接脚本中的FLASH起始地址。MEMORY { FLASH (rx) : ORIGIN 0x08004000, LENGTH 112K RAM (xrw) : ORIGIN 0x20000000, LENGTH 64K }这个操作容易出错的地方在于修改了Flash起始地址后编译生成的bin文件就是从0x08004000开始的纯App代码里面不含Bootloader的部分。而上位机要发送给bootloader的恰恰是这种纯App的bin文件。如果你直接烧录了整个hex文件包含地址信息可能会导致bootloader覆盖所以在固件打包时建议使用bin格式并且由上位置软件按地址偏移进行解析。3. CAN通信协议设计与实现细节3.1 帧格式与ID分配CANbootloader和上位机之间的通信需要一套完整的协议不能简单地把固件数据往CAN总线上丢。我设计的协议基于CAN 2.0A标准帧11位ID因为标准帧在工业现场的兼容性更好而且对于升级这种小数据包场景足够用。帧ID分配策略如下帧方向CAN ID说明上位机→设备0x100命令帧上位机→设备0x101数据帧设备→上位机0x200应答帧设备→上位机0x201状态帧命令字定义在数据域的第一个字节比如0x01表示握手、0x02表示擦除、0x03表示写入、0x04表示校验、0x05表示跳转执行。数据域最多8字节对于一次Flash写入来说CAN标准帧的8字节数据刚好对应Flash编程时每次写入4个半字8字节的操作非常契合。3.2 通信流程与状态机一个完整的升级流程是这样的上位机发送握手命令0x01bootloader收到后回复设备版本号、Bootloader版本号、Flash大小等信息上位机发送擦除命令0x02bootloader执行Flash擦除上位机分包发送固件数据0x03每包包含地址偏移和数据内容上位机发送校验命令0x04bootloader计算已写入数据的CRC32并返回上位机发送跳转命令0x05bootloader执行App启动整个通信过程用状态机管理bootloader的逻辑是这样的typedef enum { STATE_IDLE, STATE_HANDSHAKE, STATE_ERASE, STATE_WRITE, STATE_VERIFY, STATE_JUMP } BootState; BootState currentState STATE_IDLE; void CAN_RX_Handler(uint32_t id, uint8_t *data, uint8_t len) { switch (currentState) { case STATE_IDLE: if (id CMD_FRAME_ID data[0] CMD_HANDSHAKE) { // 回复设备信息 currentState STATE_HANDSHAKE; } break; case STATE_HANDSHAKE: if (id CMD_FRAME_ID data[0] CMD_ERASE) { // 擦除App区 Flash_EraseAppArea(); currentState STATE_ERASE; } break; case STATE_ERASE: if (id DATA_FRAME_ID) { // 解析地址和数据写入Flash Flash_WriteData(address, data); } else if (id CMD_FRAME_ID data[0] CMD_VERIFY) { // 校验 currentState STATE_VERIFY; } break; // 其他状态处理... } }这里有几个容易踩的坑。第一个是超时处理如果上位机发了一帧数据后停住了bootloader不能一直傻等否则设备就死在那了。我在实际项目中加了看门狗和超时计数超过500ms没有收到下一帧数据就自动复位回到IDLE状态。第二个是连续帧的编号问题固件分包发送时每帧数据都要带一个包序号bootloader收到后检查序号是否连续。CAN总线不像USB那样有底层的流控机制丢帧是正常现象靠包序号做容错是非常必要的。3.3 Flash驱动编写要点STM32F107的Flash编程有几个必须注意的地方。Flash擦除操作在擦除期间CPU不能从Flash取指令执行所以擦除函数必须放在RAM中运行或者使用已经在RAM中的代码。如果你直接用Keil默认配置擦除Flash时会死机因为在擦除Flash的同时CPU还在从Flash读取擦除函数的指令。解决办法是在RAM中执行擦除操作__RAM_FUNC void Flash_ErasePage(uint32_t pageAddr) { // 等待Flash空闲 while (FLASH-SR FLASH_SR_BSY); // 解锁Flash FLASH-KEYR 0x45670123; FLASH-KEYR 0xCDEF89AB; // 执行页擦除 FLASH-CR | FLASH_CR_PER; FLASH-AR pageAddr; FLASH-CR | FLASH_CR_STRT; // 等待操作完成 while (FLASH-SR FLASH_SR_BSY); // 锁定Flash FLASH-CR ~FLASH_CR_PER; FLASH-CR | FLASH_CR_LOCK; }写Flash操作同样要注意写入前必须确保目标地址是擦除状态0xFFFFFFFF否则写入会失败。我遇到过的情况是上位机发的固件分包没有按页对齐导致写入时跨越了页边界结果后半个页的数据写不进去。解决方案是上位机在分包前先做对齐处理或者bootloader内部做跨页拼包。4. 上位机与联调过程中的关键经验4.1 上位机工具的选择与协议对接CANbootloader的下位机只是整个系统的一半上位机软件同样重要。如果你使用CAN分析仪比如周立功USBCAN、CANable等通常厂商都会提供二次开发SDK你可以用Python或C#写一个简单的刷写工具。我用Python写过一套基于python-can库的上位机工具核心逻辑很简单import can bus can.interface.Bus(bustypepcan, channelPCAN_USBBUS1, bitrate500000) def send_frame(can_id, data): msg can.Message(arbitration_idcan_id, datadata, is_extended_idFalse) bus.send(msg) def write_firmware(file_path): with open(file_path, rb) as f: firmware f.read() # 发送握手命令 send_frame(0x100, [0x01]) # 等待应答... # 发送擦除命令 send_frame(0x100, [0x02]) # 分包发送固件 chunk_size 8 for i in range(0, len(firmware), chunk_size): chunk firmware[i:ichunk_size] # 补充包序号和地址信息 data [0x03, seq, addr_hi, addr_lo] list(chunk) send_frame(0x101, data) seq 1 # 发送校验和跳转命令 send_frame(0x100, [0x04]) send_frame(0x100, [0x05])这里有一个需要特别注意的地方CAN的波特率必须和设备端的实际配置一致。Bootloader阶段的CAN波特率和App阶段的CAN波特率很可能是不同的——很多产品在正常工作时用的波特率是250KbpsCANopen标准而bootloader为了兼容不同波特率的总线环境可能默认配置成500Kbps或者自动识别。我在一个项目里就踩过这个坑上位机用500K发送握手命令设备端bootloader工作在250K两边互相收不到数据排查了好半天才发现是波特率不匹配。注意建议在bootloader中实现波特率自适应功能。通过监听总线上的数据流尝试不同的波特率配置直到收到合法帧为止。这个功能在总线上有多个设备、波特率不确定的场景下非常实用。4.2 坏块处理和掉电保护策略实际做产品时不可以假设升级过程永远顺利。CAN总线受干扰、上位机崩溃、现场突然断电这些情况都有概率发生。如果不做任何保护一旦在擦除Flash之后、写入完成之前断电设备就变砖了。我的做法是在Flash中保留一个升级标志区在做任何破坏性操作之前先在标志区写入“正在升级”的标记。Bootloader启动时检查这个标记如果标记显示“升级未完成”说明上次升级失败了此时bootloader不会跳转到App而是停留在bootloader模式等待重新升级如果标记显示“升级成功”bootloader正常跳转到App执行这个策略实现起来很简单可靠性却极高。我实际测试过在写入过程中直接断电重新上电后设备会停在bootloader模式用上位机重新刷一次完整固件就能恢复正常。这里再补一句经验不要用单个字节做标志用两个字节互为取反比如0xA5和0x5A可以避免Flash写入不完整导致的标志误判。4.3 提高CAN总线传输效率的技巧CAN总线波特率500Kbps时理论带宽是50000字节/秒但实际有效数据速率远低于这个值。因为每一帧还有帧头、仲裁、应答等开销标准帧的实际数据效率大概在60%左右。如果固件有100KB传输时间至少需要3秒以上实际可能要5到10秒。为了提高效率我做了两件事使用DLC8充分利用数据域。有些工程师的习惯是每帧只发有效数据长度结果是一包只发几个字节白白浪费了CAN帧的负载能力。bootloader固件在传输时一定要凑满8字节不足的部分用0xFF填充。支持多帧连续发送。上位机在发送数据帧时不要等每帧都收到应答后再发下一帧应该采用窗口机制一次连发16帧或32帧设备端通过包序号确认如果发现某帧丢了就连续发NACK上位机从这个序号开始重传。这个机制和TCP的滑动窗口非常像可以大幅提升传输速度。4.4 与LAN8720以太网功能的联动场景关于最新的网络热词LAN8720其实这和STM32F107的CANbootloader是有直接关联的。STM32F107内置以太网MAC配合LAN8720这颗PHY芯片可以构建以太网接口。有些产品既需要CAN通信又需要以太网上行连接通常情况下以太网接口负责和上位机通信CAN接口负责和底层设备通信。在这种架构下CANbootloader依然有它的用武之地——当设备通过以太网正常工作时如果收到云端或上位机的升级指令设备可以先把固件下载到片外Flash或SD卡中然后主动切换到bootloader模式再从本地Flash把固件通过CAN或内部Flash接口写入App区。这样虽然传输路径变了但bootloader的核心逻辑完全复用只是数据来源从CAN变成了以太网下载的本地缓存。这也是我在项目中实际验证过的做法升级成功率比直接走以太网在线刷写高很多因为本地缓存避免了网络中断导致的半截刷写问题。5. 常见问题排查与调试实战5.1 设备上电后无法进入bootloader这个问题的常见原因有两个一是boot引脚配置不正确STM32F107没有独立的BOOT1引脚需要通过Boot0和Boot1的组合来设置启动模式如果你用的是系统存储器启动模式CANbootloader可能根本没有执行二是跳转条件不满足我在bootloader中通常会在main函数开头检查一个升级标志位如果没有升级请求就直接跳转到App如果你发现无论如何都进不了bootloader先检查这个标志位的读取逻辑。int main(void) { // 初始化CAN、时钟等 // 检查是否需要进入升级模式 if (CheckUpgradeFlag() UPGRADE_REQUEST) { // 进入bootloader主循环 Bootloader_MainLoop(); } else { // 跳转到App JumpToApp(); } }很多人调试时容易忽略这一点以为只要烧录了bootloader就一定能进入bootloader模式实际上没有升级标志的话bootloader跑一圈就直接跳App了从外部看起来就像不存在一样。5.2 跳转到App后程序跑飞跳转后程序跑飞我在调试中最容易遇到的情况就是中断向量表没有正确设置。解决思路是在App工程中确认VTOR已设置用调试器单步执行看跳转后第一条指令是否从0x08004000取的是栈顶地址。这里贴一个标准的跳转代码里面包含了关键的处理步骤typedef void (*pFunction)(void); void JumpToApp(void) { uint32_t appStackAddr *(volatile uint32_t *)APP_BASE_ADDR; pFunction appEntry (pFunction)*(volatile uint32_t *)(APP_BASE_ADDR 4); // 检查栈顶地址是否合法RAM范围 if ((appStackAddr 0x2FFE0000) ! 0x20000000) { return; // 栈顶地址异常说明没有有效的App } // 关闭全局中断 __disable_irq(); // 设置主栈指针 __set_MSP(appStackAddr); // 跳转到App的复位向量 appEntry(); }注意末尾的appEntry()是一个不会返回的函数调用跳转后bootloader的代码就彻底放弃执行了。另外一定要在跳转前关闭所有外设中断否则中断挂在旧的中断处理函数上App启动时一旦触发就会跑飞。5.3 CAN通信正常但数据写不进Flash这种情况比较隐蔽表现为上位机发送数据后bootloader有应答但重新读Flash发现数据不对。我遇到过的原因有两个Flash没有解锁。STM32F107的Flash默认是锁定的直接写操作会被忽略必须按照前面提到的解锁序列写入两个密钥值。写入地址越界。如果上位机发的地址不小心超出了App区的范围比如把Bootloader区的地址也传给了bootloader写入操作会看到硬件错误。务必在bootloader里加地址范围检查一旦发现目标地址落在Bootloader区直接拒绝写入。还有一个我长期保养的习惯每次写完一页Flash后立刻把这页读回来做一次回读校验和原始数据比对。这一动作在调试阶段能帮你快速定位问题不至于等到最后校验失败时再大海捞针。5.4 波特率自动识别失败波特率自适应功能我实现过一版核心思路是配置CAN外设的硬件过滤器让它接收所有帧然后在每次接收中断里判断当前帧是否包含合法的命令字。如果收到非法数据就切换波特率重新尝试。实测发现在总线上有持续通信时波特率识别很快一两秒钟内就能锁定但如果总线空闲上位机一直不发数据自适应功能就等于摆设。所以我在实际项目中改成了半自动方式Bootloader默认尝试一个常用波特率列表500K、250K、125K、1M每个波特率等待0.5秒如果收到上位机的握手请求就锁定当前波特率如果列表轮询完毕还没握手成功则保持最后一个波特率进入循环监听。这种方式在工程中稳定得多也不会出现无限期的扫描等待。6. 针对当前方案的实际优化建议6.1 增加固件加密与签名认证当你的设备部署在用户现场通过CAN总线升级固件时任何人都能接入总线发送伪造的升级包。一旦有人逆向出你的协议格式就能往设备里灌入恶意固件后果不堪设想。我的建议是在bootloader中增加CRC32校验的基础上再叠加AES-128或AES-256加密。具体做法是上位机在发送固件前先用AES密钥对固件做加密bootloader收到后先解密再写入Flash。密钥可以预烧录在芯片的选项字节区或者使用芯片唯一的UID参与运算确保每个设备的密钥不同。这个方案虽然不能做到绝对安全但能挡住90%以上的恶意攻击场景。6.2 增加App版本回退机制升级过程中由于意外断电或者其他原因导致App固件不完整时bootloader通过前面提到的升级标志判断停在bootloader模式等待重新升级。但如果新固件本身就存在bug比如逻辑错误即使升级成功了设备也可能运行异常。更稳妥的做法是保留两个App区一个存放当前正在运行的稳定版本另一个存放新升级的版本。Bootloader默认执行稳定版本升级时先把新固件写入待升级区重启后尝试运行新固件如果新固件在启动自检时上报异常bootloader自动回退到稳定版本。这个方案需要Flash容量足够建议512KB以上型号但带来的维护便利性非常值得。6.3 日志记录与远程诊断最后补充一个我在现场维护过程中觉得很有用的功能在bootloader中增加一个简单的日志记录机制。把每次升级操作的时间、帧数、失败原因等关键信息写入Flash的独立扇区。当设备出现问题时可以通过CAN总线把这些日志读出来快速定位是上位机问题、总线问题还是设备Flash问题。这个日志功能占用资源很少一个扇区循环写入就够但排查问题时比任何调试工具都直观。我有一次在现场排查设备离线问题就是靠bootloader日志发现是上位机发送的帧序号跳变导致固件写入错乱而不是总线硬件故障。CANbootloader这个方案本质上解决的是“如何安全、可靠、低成本的更新嵌入式设备固件”这个普适性问题。我在多个项目中体会到bootloader设计的好坏直接决定产品后期的维护成本。如果你正打算给基于STM32F107的产品加上CAN升级功能我的建议是从最小可用版本开始先实现握手、擦除、写入、跳转这几步跑通之后再逐步加加密、双备份、日志这些进阶功能。视频或教程不可能告诉你每个细节真正深入理解这套机制还是需要自己动手把板子跑起来哪怕只是简单的点灯和CAN回环测试也会让你少走很多弯路。本文还有配套的精品资源点击获取
返回列表