ARTICLE DETAIL

资讯详情

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

STM32基于CAN总线的IAP固件升级:Bootloader与Flash分区设计

STM32基于CAN总线的IAP固件升级:Bootloader与Flash分区设计 简介面向STM32嵌入式开发者的IAP现场升级例程基于STM32F407平台重点演示在Flash指定地址写入程序版本标志、再由APP判断该标记决定是否触发升级的完整引导流程。压缩包内为IAP引导部分配套的APP部分基于FreeRTOS与双CAN通信已另行上传两者结合可构建一套实用的现场升级方案。资源包共910个文件、约50.67MB核心为126个h头文件与105个c源文件并包含调试配置、编译中间文件等整体呈现Keil MDK工程结构便于直接导入源码阅读。目前已有3745人学习下载适合正在调试BootLoader或需要实现现场升级功能的研发人员。通过源码可理清IAP跳转、Flash分区规划、版本标志写入/判断等关键细节工程内保留的编译配置也有助于快速复现实验环境、降低二次开发门槛工程文件组织完整可配合实际硬件验证升级流程对理解STM32在线升级机制具有直接参考价值。1. 现场拆机升级固件有多痛IAP 就是那个解药做过现场调试的都懂设备已经装进配电柜、封好上盖、线束扎带都剪断重绑之后突然要改一版协议或者修一个 bug这时候要么背着电脑和 J-Link 爬塔吊要么把整个设备拆下来返厂时间成本高到没法接受。IAPIn-Application Programming解决的就是这个问题利用 Bootloader 引导程序在设备运行状态下通过 CAN 总线接收新固件写入 Flash 后跳转执行。这个例程基于 STM32F407 实现思路是关键的一环——在 Flash 固定地址写一个程序版本 flagAPP 启动后判断是否需要升级用于标记是否有待更新的固件。整套代码包含 IAP引导程序和 APP 两个部分APP 基于 FreeRTOS 并使用 CAN1CAN2 双总线意味着你可以直接在真实项目中验证 IAP 的完整链路而不是拿一个空工程调半天。2. Flash 分区与版本判定 Flag先想清楚地址再谈升级IAP 的根基是 Flash 地址规划这一步做不好后面所有代码都是空中楼阁。STM32F407 的 Flash 容量通常是 1MB也有 512KB 版本扇区划分并不是均匀的前 4 个扇区每个 16KB第 5 个扇区 128KB从第 6 个扇区开始每个 128KB。规划 IAP 时要保证 IAP、APP、Flag 标志位三者在物理地址上不冲突且各自有足够的余量应对未来固件体积膨胀。2.1 地址分区设计从零开始划分 1MB Flash以常见的 1MB Flash 版本为例地址规划参考下面这张表区域起始地址大小存放内容IAP Bootloader0x0800000064KB扇区0~3引导程序、升级逻辑、CAN驱动APP 应用区0x08010000192KB扇区4~6FreeRTOS CAN1 CAN2 应用代码升级标志 Flag0x08040000 末尾 4 字节4B版本号、升级请求标记备份区可选0x08040000 4 偏移往后视需要旧固件备份用于回滚提示STM32F407 的扇区 0~3 是 16KB扇区 4 是 128KB。如果用扇区 0 单扇区存 IAP容量只有 16KB调试时可能够用但后续加协议栈就会紧张。建议把扇区 0~3 合并成 64KB IAP 区域编译时在链接脚本里把 IAP 的 Flash 起始地址设为 0x08000000 大小设为 0x10000。APP 起始地址 0x08010000 是从扇区 4 开始的这样 APP 本身占用扇区 4 和扇区 5 各 128KB共 256KB对于 FreeRTOS 双 CAN 的工程来说完全足够。Flag 放在 APP 区域之外、靠后的独立地址避免 APP 在擦除自身时把标记删掉。实际操作时把每个扇区的使用情况打印在串口上确认擦除范围没有误伤这一步在第一次跑通时尤其关键。2.2 Flag 机制设计不是简单的读写而是状态机这是一个典型的写标记—判断—执行升级流程。具体的机制设计如下FLAG_SYS_BASEFlag 所在的 Flash 地址如 0x08040000 后 4 字节FLAG_APP_NEW常数 0xA5A5A5A5代表有新版固件待升级FLAG_APP_CUR常数 0x5A5A5A5A代表当前 APP 为最新版本也代表当前 APP 里跑的恰好是当前固件需要跳转启动流程MCU 上电复位后IAP 先跑到FLAG_SYS_BASE读 4 字节如果判到FLAG_APP_NEW则进入接收固件并写 Flash的升级流程否则检查 APP 区栈顶地址是否合法合法则直接跳转 APP。APP 启动后如果一切正常主动把 Flag 改回FLAG_APP_CUR这就是确认当前版本可用的含义。以下是一段 IAP 侧读取和更新 Flag 的参考代码#define FLAG_SYS_BASE 0x08040000U #define FLAG_APP_NEW 0xA5A5A5A5U #define FLAG_APP_CUR 0x5A5A5A5AU uint32_t iap_get_flag(void) { return (*(volatile uint32_t *)FLAG_SYS_BASE); } void iap_set_flag(uint32_t flag) { uint32_t primask __get_PRIMASK(); __disable_irq(); FLASH_Unlock(); // 解锁 Flash FLASH_EraseSector(FLASH_Sector_11, VoltageRange_3); // 擦除 Flag 所在扇区 FLASH_ProgramWord(FLASH_SYS_BASE, flag); // 写入新标记 FLASH_Lock(); __set_PRIMASK(primask); }这段代码的逻辑是读 Flag 直接通过指针访问 Flash 映射地址不需要先擦后写但写入必须先擦除整个扇区。__get_PRIMASK/__disable_irq的作用是确保在写 Flag 过程中不被打断因为擦写 Flash 时 CPU 会暂停取指如果此时来了 CAN 中断会导致数据丢失。注意这里擦除的是扇区 11对应地址 0x08040000如果你的 Flag 地址不同务必按实际扇区号替换。另外擦除所有扇区时写程序时会默认把扇区里所有字节置为 0xFF写完一个值之后再写前必须再次擦除否则地址上的数据是上一个值的残留。第 2 章整体流程的关键路径设计是上电 → IAP 读 Flag → 如果是 App_New → 走升级流程 → 写入完整固件 → 擦除并写标志 → 跳转 APP → APP 启动后确认标志。这个环形流程跑通后你才算真正具备现场升级的基本能力。3. 跳转函数的正确打开方式别直接赋值函数指针就完事很多初学 IAP 的人以为跳转就是把 APP 的复位函数地址强转成函数指针然后调用实际上一跑就会 HardFault。问题出在细节上跳转前要确认 APP 栈顶地址是否为合法 RAM 区域要关闭全局中断要对系统时钟和 SysTick 做处理否则 APP 初始化时会各种异常。3.1 跳转核心代码栈检查与复位向量跳转APP 的起始 4 字节存放的是初始栈指针MSP 初始值紧随其后的 4 字节才是复位入口。跳转前必须验证初始栈指针是否落在 STM32F407 的 RAM 范围内避免冲进非法地址。以下代码适用于项目里常见的固件分区规划typedef void (*pFunction)(void); void iap_jump_to_app(uint32_t app_addr) { uint32_t app_msp *(volatile uint32_t *)app_addr; // 取 APP 初始栈指针 uint32_t app_pc *(volatile uint32_t *)(app_addr 4); // 取复位向量地址 // 检查栈顶是否在 STM32F407 RAM 范围0x20000000 - 0x2001FFFF if (app_msp 0x20000000U || app_msp 0x20020000U) { return; // 栈顶非法拒绝跳转 } __disable_irq(); // 关闭全局中断 SysTick-CTRL 0; // 关闭 SysTick避免跳转后进入 SysTick 异常 HAL_DeInit(); // 复位外设状态使用 HAL 库时 SCB-VTOR app_addr; // 将向量表偏移到 APP 区 __set_MSP(app_msp); // 重设主堆栈指针 pFunction jump (pFunction)app_pc; jump(); // 跳转到 APP 复位入口 }代码里做了三层保险一是栈顶地址合法性检查二是关闭全局中断和 SysTick三是手动重置 MSP。SCB-VTOR app_addr这是关键中的关键Cortex-M4 默认从 0x08000000 取向量表如果 APP 起始地址是 0x08010000不重新设置 VTORAPP 里任何中断触发都会跳到 IAP 的向量表去取地址轻则中断无效重则跑飞这个问题在添加 FreeRTOS 之后尤其容易暴露因为系统节拍中断全靠 SysTick 实现。3.2 APP 工程必须配合修改的偏移量设置IAP 代码写好了APP 侧不改一样跑不起来。APP 工程里要修改两处一是链接脚本里的 Flash 起始地址改为 0x08010000二是向量表偏移量打开。使用标准外设库时system_stm32f4xx.c中定义#define VECT_TAB_OFFSET 0x10000U // APP 起始地址偏移路径为system_stm32f4xx.c这个偏移量必须和 IAP 中的SCB-VTOR保持一致。使用正点原子等开发板例程时工程模板中会有一个SystemInit()函数它在 main 执行前完成系统时钟初始化和向量表偏移设置所以不能在SystemInit()里直接写VTOR否则会和 IAP 跳转设置冲突。正确做法是在跳转前由 IAP 设置VTORAPP 的SystemInit()中保持VECT_TAB_OFFSET与实际偏移一致即可因为SystemInit()的汇编代码里会根据这个宏重新配置 VTOR。这里有两种工程常见的坑坑一APP 工程里VECT_TAB_OFFSET设为 0但 IAP 跳转时SCB-VTOR设置正确那么 APP 初始化时会把向量表改回 0x08000000中断全部异常。坑二APP 工程里宏定义写对了但跳转前没设置SCB-VTORAPP 上电后一段时间内中断不响应直到第一个 SysTick 中断触发程序直接跑飞。所以在项目里测试时我一般会在 APP 的main()开头读取SCB-VTOR并串口打印若值与预期不符立刻报警而不是等 bug 随机复现。3.3 跳转前的外设清理这一点在带 RTOS 的工程里容易被忽视。跳转前CAN1 和 CAN2 可能正在接收报文UART 可能在中断中DMA 可能还挂着缓冲。如果 IAP 中直接裸跳外设状态会残留在内存里APP 初始化时去配置同样的外设寄存器可能处于非预期状态。常见做法是HAL_DeInit()把所有外设恢复默认再关闭所有中断源void iap_deinit_peripherals(void) { __disable_irq(); HAL_DeInit(); // 复位所有外设 for (int i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; // 全部中断恢复到复位状态 NVIC-ICPR[i] 0xFFFFFFFF; // 清除挂起中断 } SysTick-CTRL 0; SysTick-VAL 0; __enable_irq(); }关键在于HAL_DeInit()之后中断标志位和挂起位可能还残留在 NVIC 中这是非常隐蔽的问题。NVIC-ICER的数组循环会把所有中断源都清除干净。FreeRTOS 的 SysTick 和 PendSV 等异常如果不清APP 端启动调度器时会进入异常。清理完成后再次__disable_irq()并跳转整个流程就可以了。4. 基于 CAN 总线的固件传输帧格式与握手协议定好再谈接收效率现场升级的环境往往没有串口线也不方便拆设备接 USBCAN 总线才是大多数工业设备的标配通讯接口。这个例程使用 CAN1 作为升级通道同时 CAN1 和 CAN2 挂载应用数据升级数据走 CAN 就得考虑两个问题固件可能有几百 KBCAN 帧每次最多 8 字节如何分包传输传输过程如何保证不丢包、不乱序4.1 自定义应用层协议三个关键帧类型底层的 CAN 收发直接复用 HAL 库的HAL_CAN_Receive即可重点是数据链路层之上要有一套清晰、简洁的分包协议。本项目实际采用的协议帧格式参考下表帧类型帧 ID数据段长度数据段内容说明握手请求0x10180xAA 0x55 0x01 0x00 ...IAP 发送请求 APP 进入升级模式握手响应0x10280x55 0xAA 0x01 版本号低字节 版本号高字节APP 回复确认升级开始固件帧0x1038包序号(2B) 有效数据(4B) CRC16(2B)核心数据帧单帧传输 4 字节固件完成确认0x10480xCC 0x33 0x01 0x00 0x00 0x00 0x00 0x00固件发送完成请求校验并跳转每帧 8 字节只带 4 字节固件数据有效利用率 50%但换来的是安全性每帧都有 CRC 校验接收端发现不匹配可以立刻要求重传。如果追求效率可以把有效数据扩到 6 字节留 2 字节做 CRC16在 CANFD 环境下更是可以直接扩到 64 字节。但这里用标准 CAN2.0数据分段切得干净利落出错重传代价也低。4.2 CAN 接收与 Flash 写入的协作流程在 IAP 工程中CAN 接收采用中断方式Flash 写入放在主循环。核心思路是CAN 中断收到一帧完整校验通过后把数据拷贝到缓冲区并置位标志位主循环轮询到这个标志后执行FLASH_ProgramWord写入。 关键代码段如下void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx_header, rx_data); if (rx_header.StdId 0x103) // 固件数据帧 { uint16_t pkg_no (rx_data[0] 8) | rx_data[1]; // 包序号 uint32_t payload (rx_data[2] 24) | (rx_data[3] 16) | (rx_data[4] 8) | rx_data[5]; // 4字节固件数据 uint16_t crc (rx_data[6] 8) | rx_data[7]; // CRC16 if (crc16_check((uint8_t *)payload, 4, crc) 0) { if (pkg_no expected_pkg_no) // 防重传、防乱序 { flash_buffer[write_index] payload; expected_pkg_no; if (write_index 32) // 攒够 128 字节就写一次 { write_index 0; flash_write_block(write_addr, flash_buffer, 32); write_addr 128; } can_send_ack(pkg_no); // 回复 ACK发送端才发下一包 } } } }这段代码的逻辑是每帧 8 字节数据里前 2 字节是包序号后 4 字节是固件数据最后 2 字节是 CRC 校验。flash_buffer 是一个 32 字的 FIFO 缓冲攒够 32 个 uint32128 字节后统一调用 Flash 写函数显著减少擦写次数。为什么要攒批因为 STM32F407 的 Flash 编程虽然是字级操作但频繁调用FLASH_ProgramWord会让 CPU 长时间停顿而 CAN 中断是不等人的攒批写入能够保证接收和擦写异步运行不会互相堵塞。4.3 擦除扇区的边界条件Flash 写入前必须先擦除。STM32F407 的扇区擦除是整扇区操作不能只擦一部分。这里最容易犯的错误是APP 区跨越了多个扇区写入过程中擦除一个扇区会连带把下一个扇区的数据干掉。具体来说APP 从 0x08010000扇区 4开始固件大小若超过 128KB 就会顶到扇区 5因此固件更新时扇区 4 和 5 都必须擦除。擦除顺序是先擦全部目标扇区再逐块写入不能在写的过程中穿插擦除后续扇区否则已写入的数据会被二次擦除抹掉。典型代码如下void flash_erase_app_area(void) { FLASH_Unlock(); FLASH_EraseSector(FLASH_Sector_4, VoltageRange_3); // 0x08010000 - 0x0802FFFF FLASH_EraseSector(FLASH_Sector_5, VoltageRange_3); // 0x08030000 - 0x0804FFFF FLASH_EraseSector(FLASH_Sector_6, VoltageRange_3); // 0x08050000 - 0x0806FFFF FLASH_Lock(); }擦除扇区数量要与你 APP 的实际代码量匹配。一个带 FreeRTOS 双 CAN 的完整工程编译出来一般在 60KB 到 160KB 之间如果用了浮点打印和 libc 库会膨胀到 200KB 以上。所以每次编译后看.map输出记录 flash 占用再去确定要擦几个扇区是比较稳妥的做法。项目里可以在固件文件头部固定写入一个固件总长度字段IAP 收到这个字段后自动计算擦除扇区范围避免人工配置与固件大小脱节。4.4 失败重传与超时机制CAN 通信不可能永远不出错现场的电磁干扰、线缆松动、总线竞争都会导致丢帧或错帧。设计时要在两个层面防水ACK/重传机制发送端每发一包等待接收端 ACK超时 30ms 未收到就重发重发超过 5 次报错退出。接收端如果收到乱序包直接丢弃并回复 NACK发送端据此回退包序号。整体超时整个升级流程如果 10 秒内没有收到任何有效固件帧IAP 自动退出升级模式擦除 Flag恢复运行旧 APP。防止升级过程中有人拔线导致系统卡死在等待状态。#define ACK_TIMEOUT 30U // 毫秒 #define MAX_RETRY 5U uint8_t can_send_firmware_pkg(uint16_t no, uint32_t data) { uint8_t buf[8]; uint16_t crc crc16_calc((uint8_t *)data, 4); buf[0] no 8; buf[1] no 0xFF; buf[2] data 24; buf[3] data 16; buf[4] data 8; buf[5] data 0xFF; buf[6] crc 8; buf[7] crc 0xFF; for (int i 0; i MAX_RETRY; i) { can_send_frame(0x103, buf, 8); // 发送一帧 if (can_wait_ack(no, ACK_TIMEOUT)) // 等待对应包号的 ACK return 1; } return 0; // 重试耗尽返回失败 }这份代码背后的逻辑是把 ACK 等待做成阻塞式每个包序号都唯一对应一个 ACK收到的 ACK 序号不匹配时忽略防止上一个重传包残留的 ACK 干扰当前确认。这里需要留意的一点是整个升级过程中发送方要持续运行在 RTOS 的一个任务里配合vTaskDelay实现超时计时而不是使用裸机的HAL_Delay方便在等待中让出 CPU 给实时性要求更高的任务。5. 升级失败后的回滚策略与 FreeRTOS 下 IAP 的三个细节IAP 本身只是能升级但如果升级后 APP 起不来、控制逻辑跑飞、甚至机器直接停机那就不是升级是事故。所以成熟的 IAP 工程必然要有回滚机制尤其当你的 APP 运行在 FreeRTOS 里某些 RTOS 特性的处理稍不留神就会导致升级后系统行为异常。5.1 利用看门狗 Flag 实现回滚回滚的核心思路让控制器记住上次升级后我有没有正常跑起来。基于项目里的 Flag 机制可以再扩展一个升级计数字段IAP 写完 APP 固件后先把 Flag 设为升级完成但未确认然后跳转 APP。APP 启动后创建完 FreeRTOS 任务、CAN 初始化成功、跑完自检逻辑再把这位置为运行正常。如果 APP 起不来看门狗IWDG会在超时后复位 MCUIAP 上电发现 Flag 还是未确认就回退执行旧固件。// APP main 中启动完成后的确认代码 void app_boot_confirm(void) { // 只有首次升级后才需要确认 if (iap_get_flag() FLAG_APP_UPDATE_PENDING) { iap_set_flag(FLAG_APP_OK); // 标记当前固件可靠 // 可以把备份区的旧固件擦除释放空间可选 } }IWDG 超时时间要设长一点至少覆盖 APP 从复位到自检完成的整个时间窗口。一般设 3~5 秒如果 APP 初始化中卡在某个外设等待上看门狗会提前复位防止设备长期出于半死不活的状态。中间章涉及 FreeRTOS 时特别要注意 IAP 跳转前的临界区保护下面这三点是项目里踩过坑后总结的。5.2 FreeRTOS 与 IAP 共存的三个关键细节FreeRTOS 环境下做 IAP 比裸机麻烦的地方在于中断管理和调度器状态。以下三个细节是必须处理的细节一跳转前必须挂起调度器并关闭中断。FreeRTOS 的vTaskSuspendAll()只能阻止任务切换不能阻止中断嵌套调用 API。跳转前执行taskDISABLE_INTERRUPTS()或直接裸调__disable_irq()同时调用vTaskEndScheduler()让调度器退出。否则跳转后 SysTick 继续回调xTaskIncrementTick()而 TCB 内存已经被 APP 的初始化过程覆盖触发 HardFault。细节二CAN 接收中断在 IAP 升级阶段不要依赖 FreeRTOS 的队列。升级数据进了队列但主循环任务被抢占队列堆积会导致 ACK 超时。IAP 阶段的 CAN 接收放在中断回调里直接处理不走消息队列优先级和实时性都可控升级代码和 APP 业务代码通过编译宏隔离。细节三固件升级和运行任务分离。用 FreeRTOS 时IAP 代码不应该和 APP 代码编译到一个工程里存在同一 Flash 空间却由两个任务管理。保持 IAP 是独立、裸机的工程APP 才挂 FreeRTOS。这样升级过程中的 Flash 操作不会因为任务调度导致时序异常。5.3 如何在现场验证升级链路是否真的可靠所有代码写完之后拿出一个实测手段来确认整条链路。项目里惯用的做法是通过 CAN 升级一个版本号递增的测试固件连续升级 50 次不失败才算可靠。每次升级后检查 APP 版本号是否对应然后主动制造一次升级失败发送端中途断掉确认看门狗能复位并回滚到旧版本记录回滚耗时和复位次数。另外可以在 IAP 中留一份串口打印日志记录每步的状态码。以下状态码表方便定位状态码含义处理建议0xA1IAP 进入升级模式无需操作0xA2固件接收完成开始写 Flash检查 CAN 总线上是否有干扰0xA3APP 跳转成功Bootloader 正常0xA4升级帧校验失败重传检查 CAN 速率与终端电阻0xA5看门狗复位准备回滚检查 APP 初始化卡在哪一步0xA6回滚完成运行旧固件定位 APP 写入后的首个异常点每帧 CRC 计算时用多项式 0x8005可以快速用上位机模拟一个简单的 CAN 发送工具来触发升级把这套 IAP 移植到自己的硬件平台时只需要改 CAN 波特率和 Flash 扇区映射即可逻辑不用动。代码里留下的调试串口输出在整个联调周期里多打印一个状态字节发布前再关闭或者放到后台日志这个习惯能省掉很多现场排查时间。本文还有配套的精品资源点击获取
返回列表