
简介STM32F107 CAN升级程序完整工程包面向嵌入式开发者与汽车电子、工业控制等领域的维护人员用于解决通过CAN总线远程更新固件、避免设备拆机烧录的痛点适用于产品量产、现场维护和远程升级等场景。包内提供Bootloader与APP双工程Bootloader源码涵盖CAN初始化、消息帧解析、固件校验、Flash扇区擦除与写入、跳转APP等功能APP工程展示业务处理与CAN交互逻辑二者结合可形成完整升级闭环。另附CAN刷写步骤说明文档从环境准备、CAN配置、固件打包到升级验证逐步讲解支持配合CAN盒模拟节点发送升级指令进行调试并给出常见问题排查思路。压缩包共2000个文件以c/h工程源码、s汇编文件、icf链接脚本、txt说明等类型为主整体约75.36MB目录结构清晰便于按模块定位Bootloader与APP代码。已有2921人学习下载适合需要搭建CAN IAP方案或正在调试验证升级流程的中高级开发者参考。 第一次给现场设备做程序升级时我是带着笔记本加串口线去的。到了机柜前才发现要升级的STM32F107板子被埋在一堆端子排和电源模块后面想摸到调试口必须拆盖现场又不允许长时间断电。那次之后我就下了决心产品里所有能通过CAN总线互联的设备升级通道必须走CAN而且要把Bootloader、APP和说明文档当成一套完整交付物来维护。STM32F107本身带双CAN控制器做CAN升级属于典型的IAPIn-Application Programming方案结构上分为Bootloader和APP两层。这套东西单独看每个模块都不难难的是内存地址怎么切、CAN协议怎么设计、跳转怎么才干净、升级失败怎么兜底。这篇文章把我实际项目里的完整方案做一次梳理包含存储布局、协议帧格式、关键代码思路和当时踩过的坑。如果你手头正好是F107或者同系列其他STM32这套思路可以照样搬。1. 为什么现场升级固件我最终敲定了CAN 自研bootloader这套方案1.1 先想清楚的不是CAN而是Boot和APP的职责边界做升级程序前很多人第一反应是写接口、写Flash读写函数但最容易翻车的其实是“职责边界”。Bootloader的职责必须压缩到极小初始化基本时钟和CAN、等待升级指令、接收固件数据、写入APP区、跳转到APP。它不该承担业务层的任何逻辑也不该做复杂的显示、存储、故障记录。原因很简单Bootloader一旦复杂出bug的概率就高而Bootloader本身又是一段很难在线更新的代码。APP的职责则是正常运行业务同时预留一个“收到云端或上位机升级命令后设置标志并软复位”的入口。这个入口可以由CAN报文触发也可以由按键触发只是在APP里不要把整个升级流程都做掉APP的工作止步于通知Bootloader“我要升级了”。这套项目交付包里之所以包含独立的Boot工程、独立APP工程和说明文件就是为了让这两个程序在编译、烧录、维护上彻底分离。量产时用烧录器直接烧Bootloader之后所有APP更新都只走CAN不再开盖Bootloader也尽量不更新。1.2 CAN升级在工业现场的优势和方案选型我把常见几种升级方式摆在一起对比过很容易看出为什么CAN是工业现场的万金油升级通道优点缺点适合场景串口简单、便宜速度低需开盖或专用调试端子开发调试、小批量USB速度快F107做USB从机要占用引脚且很多设备没有USB口带USB接口的设备以太网速度快硬件成本高现场不一定会布网线有以太网口的网关设备CAN抗干扰强、距离远、可复用现场总线帧数据短8字节协议要自己设计工业控制、车载、分布式采集系统STM32F107上的优势更明显它内部有两个CAN控制器一个用于业务数据交互另一个完全可以独立作为升级通道或者至少保证一个CAN口既跑业务又跑升级。如果现场已经布了CAN总线连接线根本不用改只是上位机向总线发一段升级报文就能进入Bootloader。要注意CAN同时依赖物理层和数据链路层。很多例程里只写代码不做波形层验证可离了稳定的波形一切都白搭。调试时建议用示波器或CAN分析仪看CAN_H和CAN_L的差分波形确认显性位幅值达到2V左右、隐形位落到0V附近波形没有明显毛刺再谈后续协议。现场长线传输时波特率不要盲目上500k我用过一段时间后发现许多故障点来自线缆质量和终端电阻把终端电阻两侧120欧姆都接上才能保证稳定。2. 存储布局与工程地址改动Boot和APP是两个独立程序2.1 地址规划给Boot留出的“安全区”和APP装载区STM32F107有多个型号这里按F107VCT6这类256KB Flash的器件来设计容量不同的话思路一样数值等比缩放即可。整体地址规划如下区域起始地址大小内容Bootloader区0x0800000032KBCAN升级引导程序APP区0x08008000224KB业务主程序末尾用户参数区可选Flash最后1页1页保存程序版本、升级标志等参数把Boot区设为32KB是出于安全考虑。很多最小实现只给Boot分8KB跑一段简单的串口IAP确实够用但一旦升级协议里要处理复杂命令、Flash写保护、CRC校验、多帧缓存8KB很容易被塞满后期每次想加功能都要在Flash布局上动刀。多预留24KB能换来充足的维护余地对量产设备来说这笔开销值得。APP从0x08008000开始意味着APP工程里所有中断向量、常量、代码段都必须以这个地址为基址重新编译。如果你手头的老项目默认按0x08000000编译直接烧进去会和白屏没区别——CPU虽然跳到了APP的main但复位向量和中断向量全部指向错误位置。末尾用户参数区不是必选项但强烈建议预留一到两页用来存放当前APP版本号、CRC校验值或升级完成标志。因为Bootloader和APP都需要读这些参数把它固定放在独立区域比散落在代码里好维护得多。2.2 修改链接脚本和向量表重映射假设你用的开发环境是Keil MDKAPP工程的Target选项里要把IROM1起始地址改成0x08008000大小改成0x38000即224KB。ST官方标准库或HAL库工程的system_stm32f1xx.c里通常会有一句#define VECT_TAB_OFFSET 0x00000000APP工程必须改成#define VECT_TAB_OFFSET 0x00008000如果用的是HAL库它会在SystemInit里调用SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET。如果是老标准库且没有这行代码你需要在main函数最开头手动执行SCB-VTOR 0x08008000;放在main函数开头的原因是CubeMX等生成的初始化代码会在进入main后立刻开启中断如果不提前把向量表切到APP区任何中断触发都会让CPU去错误地址取向量。这里有个常见误区只改链接脚本不改VTORAPP能正常跑主循环但一来中断就飞只改VTOR不改链接脚本程序下载后运行位置和向量表地址对不上同样会出问题。两者必须同时改。2.3 Bootloader启动后如何决定“正常跑”还是“进升级”Bootloader每次上电会做一次极简初始化然后检查以下条件中任意一个成立就进入CAN升级模式否则立刻跳转APP升级请求标志位存在。这个标志由APP在软复位前写入备份寄存器或Flash参数区。硬件跳线或拨码开关处于升级状态。Bootloader上电后的极短窗口内收到有效的升级握手帧。直接用“上电后延时1秒等待CAN命令”的做法虽然简单但不推荐。很多设备要求上电后立刻进入工作状态每次启动都等1秒用户不能接受。更合适的方案是让APP通过备份寄存器设置标志APP运行中收到升级指令把自己正在使用的外设关闭或释放。向备份寄存器写入固定魔数例如0xA5A5。调用NVIC_SystemReset()软复位。Bootloader启动后检查备份寄存器读到魔数则清除该标志并进入升级等待状态。这个方案的启动延迟几乎为零只有APP主动触发软复位后系统才进入升级模式。调试初期为了测试方便可以把拨码开关判定逻辑也一起保留。3. CAN升级协议怎么设计帧ID、状态机、失败处理3.1 先规划一组专用的CAN ID和帧格式我设计的哪套CAN协议帧格式看起来不算花哨但稳健性足够。帧ID使用标准帧的11位ID占用ID范围特意避开了设备正常业务报文段避免总线互扰。约定如下帧方向CAN ID用途主机 - 目标板0x701命令帧握手、开始升级、结束升级、复位主机 - 目标板0x702数据帧固件内容目标板 - 主机0x601应答帧ACK / NAK数据场最多8字节第一字节固定放命令或帧序号后续字节放额外信息。命令定义如下0x01查询版本目标板应答当前Boot版本和APP版本。0x02开始升级后面4字节表示固件总长度后4字节可放CRC模式。0x03传输数据第2字节开始最多放7字节固件数据。0x04结束升级后面4字节是整包CRC32。0x05复位APP。0x06应答ACK紧随一字节状态值。0x07应答NAK紧随一字节错误码。错误码尽量明确比如0x01表示帧序号不连续0x02表示写入Flash失败0x03表示CRC校验失败0x04表示地址越界。现场升级出问题时这些错误码能让你一眼定位问题而不是靠猜。3.2 Bootloader端的升级状态机与超时重传Bootloader不能用一个超大while循环从头读到尾我最终把接收逻辑整理成了状态机IDLE状态等待0x01或0x02。如果收到的是查询命令则立即应答收到开始升级命令则校验固件长度是否符合APP区容量然后擦除APP区的Flash进入数据接收状态。DATA状态每收到一帧0x702先检查帧序号是否等于当前期望序号。序号正确就把后7字节写入Flash并向后移动序号不应答ACK不正确则回NAK并附上当前期望序号主机则根据这个序号重发。FINISH状态收到0x04结束帧后把收到的总长度和CRC32与帧内值比较。如果一致则应答ACK并复位跳转APP不一致则NAK并重返DATA状态等待重发。具体的CAN接收处理放在中断或高优先级任务里。写Flash不能放在CAN中断里长时间执行因为Flash擦写会阻塞总线访问的时间较长容易丢失后续连续帧。我采用一个简单但可靠的方式接收中断只把帧内容拷贝到全局接收缓冲置一个标志主循环发现标志后再去执行擦写和写Flash操作。波特率我一般选250kbps帧间隔留5到10毫秒。测试下来如果上位机每帧间隔1毫秒连续猛发Bootloader很容易因为Flash写入耗时和邮箱溢出丢帧而把间隔调到5毫秒以上几乎不丢帧。对工业固件升级来说一次升级慢一两秒根本不敏感丢帧重传才是真正的灾难。3.3 主机上位机的交互节奏上位机端我建议按三步走。第一步先发查询版本命令收到目标板的Boot版本号之后确认设备在线第二步选好固件bin文件读取文件长度和CRC32发开始升级命令随后以逐帧发送的方式传输数据第三步发结束命令根据回应判断是否成功。不要一上来就狂发数据帧。现场总线上除了升级设备还可能挂着其他CAN节点如果升级设备没有及时进Bootloader状态上位机最好先确认握手成功再继续否则一堆0x702数据帧会把总线冲得很乱。我在上位机里加了一个异常保护逻辑连续3秒没有收到ACK或NAK就中止升级流程并提示用户检查接线和目标地址避免脚本傻等。4. Flash擦写与跳转APP用“软复位”避开外设残留这个坑4.1 Flash擦写前的准备和中断处理标准外设库下Flash写入流程比较固定。Bootloader收到开始升级命令后先做擦除我用类似这样的逻辑FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPERR); for (uint32_t addr APP_BASE; addr APP_BASE app_size; addr FLASH_PAGE_SIZE) { if (FLASH_ErasePage(addr) ! FLASH_COMPLETE) { // 返回 NAK错误码 0x02 } }擦写期间最好关闭全局中断或者保证没有其他中断会触发Flash读写。写Flash时CPU自带的总线访问会被暂停这时候如果有一个定时器中断抢占执行中断函数里的变量访问倒是问题不大但一些外设操作就可能超时误判。我在设计时把接收到的数据先攒在一个RAM缓冲中等缓冲区里的数据量达到一次Flash写入周期能承受的程度再执行写操作这样既不会长期占用总线任务也不会因为频繁擦写导致性能过低。另外要注意的是APP里如果开了独立看门狗APP没运行自然不会喂狗但如果Bootloader自己开了看门狗擦写耗时较长时必须在每个数据包处理完后喂一次否则Flash写到一半系统就复位了升级自然失败。最稳的做法是Bootloader不初始化独立看门狗把这个外设完全留给APP。4.2 收到结束帧后别急着跳转先做整包回读校验很多草根版本的IAP在收到最后一个数据帧后直接跳转APP了。这其实是一个隐患传输过程中万一有某一帧虽然收到了ACK但实际Flash写入已经出错整包数据到结尾根本没有被发现。我的做法是收完所有数据帧后计算收到的固件CRC32和结束帧里的CRC32比对。如果一致再从Flash的APP起始地址回读固件长度的数据对回读内容再算一次CRC32。两次都通过才认为固件完全落盘。这个“双CRC”的做法会多花几百毫秒但能挡住绝大多数传输异常。第一次计算出错说明总线上数据有篡改或丢帧第二次计算出错说明Flash写入有问题便于区分故障方向。项目里也预留了一个最小化的“APP区域有效性判断”uint32_t app_sp *(volatile uint32_t *)APP_BASE; uint32_t app_pc *(volatile uint32_t *)(APP_BASE 4); if ((app_sp 0xFFFF0000) ! 0x20000000) // 栈顶不在RAM范围 return; // 不跳转继续等待升级 if ((app_pc 0xFFF80000) ! 0x08000000) // 复位向量不在Flash范围 return; // 不跳转继续等待升级这个判断能避免空Flash或非法程序时跳进一个未定义的地址。芯片上电后Bootloader如果发现APP区域无有效程序不会尝试跳转而是老老实实停在升级模式等待上位机。整个设备出厂时如果不先烧Bootloader直接烧APPBootloader就能靠这个判断避免自己把自己卡死。4.3 跳转要干净优先使用软复位很多IAP教程里给的跳转代码是函数指针式跳转void (*jump_to_app)(void); jump_to_app (void (*)(void))(*((volatile uint32_t *)(APP_BASE 4))); __set_MSP(*((volatile uint32_t *)APP_BASE)); jump_to_app();这段代码在简单场景下能用但直接用函数指针从Bootloader跳进APPBootloader里已经初始化的时钟配置、外设状态、中断使能会全部残留给APP。APP如果再用标准库重新初始化一遍外设有些外设会产生“二次初始化”的边界问题。比如你在Bootloader里开启了CAN接收中断并设置回调跳转到APP后APP里还没有执行初始化代码总线上一来报文就会触发一个指向地址错乱的中断。我后来在量产版本上改成了“软复位跳转”Bootloader向备份寄存器写入一个“APP已就绪”标志清掉升级请求标志然后调用NVIC_SystemReset()。整个芯片重新上电Bootloader检查到标志后直接以正常方式跳转APP。这样APP启动时所有外设都处于刚复位的最干净状态省去一堆外设反初始化代码也避免了跳转后中断错乱的问题。代价只是多了十几毫秒复位时间在工程上完全可以接受。5. 实测中踩过的坑和当时的排查过程5.1 现象一APP能跑但任何中断一进来就死机项目第一次联调时Bootloader跳转成功LED闪烁正常主程序功能都正常。结果我按下按键触发串口发送设备立刻进HardFault。一开始怀疑是中断优先级分组或串口初始化问题后来用调试器读寄存器才发现SCB-VTOR的值还是0x08000000APP工程忘了把向量表偏移改成0x08008000。中断发生后CPU去Bootloader区的中断向量表找处理函数而Bootloader里根本没有对应中断处理函数只能进错误处理。重新在APP里加上SCB-VTOR 0x08008000后串口中断正常。排查建议写APP工程后第一次联调先不接任何业务逻辑开一个定时器中断看能不能正常进中断再逐步加其他功能。这个步骤能帮你把“启动问题”和“业务问题”做一个快速隔离。5.2 现象二升级到一半总线丢帧Bootloader直接卡死另一个问题相对隐蔽。设备在升级过程中我使用的上位机软件每帧间隔设成了2毫秒升级到约30%位置时一定会停止响应。分析仪抓包发现设备返回了一帧NAK之后就不再有后续报文。原因是上位机持续发帧时没有做窗口确认连续发送导致CAN控制器接收FIFO溢出。Bootloader虽然有中断接收但中断里如果只把帧拷进缓冲并立即返回主循环还来不及消化缓冲第二波数据又来了。这个问题的修正方式是两种选其一要么把上位机帧间隔提高到10毫秒以上要么改成一次发16帧等一次ACK的窗口机制。我工程上最终采用“每16帧回一个窗口ACK”接收端用一个环形缓冲接收主循环批量消费。这样让升级速度提升了也保证每批数据都能确认上。CAN接收滤波区域也要特别留意。Bootloader里进升级模式后要把CAN过滤器重新配置成只接收0x701和0x702两个ID避免总线上正常业务报文干扰升级流程。否则旧的APP过滤器配置可能把升级帧都滤掉设备怎么等也等不到完整固件。5.3 现象三升级中途断电设备直接“变砖”这个坑是设计层面要提前防御的。设备在升级过程中擦除并写入APP区一旦中途断电Flash里可能只留下一段不完整的程序。再次上电时Bootloader检测APP头数据判断无效就会停留在Bootloader等待升级表现为非正常业务状态操作上的确像“变砖”。避免这种情况要从两个方向着手。一是Bootloader必须自带完整的CAN升级接收逻辑即使APP区已经损坏也应该能通过上位机重新刷写而不是只能靠烧录器。二是设计上可以保留一份“最后的可用版本”。如果Flash空间足够可以在升级前把当前APP区备份到Flash后部的备份区升级失败后Bootloader自动从备份区恢复。F107容量不够做完整双备份时至少把起始2KB的向量表和头部参数备份下来方便失败时判断原来版本信息。庆幸的是我设计这套项目时Bootloader一直没怎么改动因此没有出现“烧写Bootloader失败”的灾难场景。建议不要把Bootloader在线升级功能写进CAN协议里。Bootloader本身应该尽量用烧录器烧写并且永不在生产环境中通过CAN更新否则一旦Bootloader半途损坏整板就真的需要拆壳救砖了。6. 交付包里不能少一页说明文件以及它应该长什么样6.1 说明文件要覆盖的核心信息标题里写到的“说明文件”容易被当成边角料但我实际体会是一份写得到位的说明文件比项目代码本身更决定维护效率。说明文件至少要有以下内容内容项具体说明硬件信息主控型号、CAN收发器型号、终端电阻位置、升级接口引脚定义软件版本匹配表Bootloader版本、APP版本、对应固件bin文件编号CAN参数波特率、CAN ID分配表、升级命令定义升级步骤从连接总线到成功跳转的详细操作流程失败应急恢复升级失败后如何重新进入Bootloader、如何通过CAN强刷常见问题收不到握手帧、数据卡在某个百分比、跳转后死机等典型故障做过一段时间现场支持的人都懂现场维护人员的眼睛盯的是“每一步点哪里”和“失败后怎么办”。文档里如果只放一句“请参考源代码”等于没用。6.2 现场升级的一张标准操作清单我项目中说明文件最后固定附一张A4操作清单确认设备供电稳定禁止在升级中拔掉电源。使用USB-CAN分析仪连接设备所在CAN总线确认终端电阻正确。打开上位机选择设备ID、CAN波特率点击“查询版本”。如果能正常读到Bootloader版本选择新固件点击升级。等待进度条走完并显示CRC校验通过。观察设备是否自动跳到APP并进入正常运行状态。如果升级失败按文档中的“强制升级”步骤重新尝试不要断电重启后误以为设备报废。这份清单文字不复杂但每次都能帮现场少打十几分钟电话。以后不管任何人接手这个项目拿到“boot APP 说明文件”这三个东西就能独立完成一次固件升级这才是这套交付物真正的价值。我自己的习惯是写完Bootloader和APP后先把这份说明文档写好再做最后一项真机升级测试。如果在测试中改动了任何一帧协议或一个Flash地址立刻同步更新文档。没有一次例外。本文还有配套的精品资源点击获取