资讯动态

STM32F103工业级CAN Bootloader:断电回滚+抗扰升级源码

发布时间:2026/10/26 18:52:51 来源:尧图企业网站定制
简介本资源是面向嵌入式开发工程师与STM32进阶学习者的CAN总线Bootloader实战项目聚焦STM32F103系列MCU在工业通信场景下的固件在线升级能力构建。资源提供完整可编译的CAN Bootloader源码工程涵盖CAN驱动配置、帧收发协议、Flash编程、CRC校验、安全跳转等核心功能模块有效解决多节点远程固件更新、抗干扰通信及启动可靠性等实际工程痛点。压缩包共180个文件以66个C源文件含CAN底层驱动与协议解析逻辑、71个头文件定义寄存器映射与通信结构体及36个汇编启动文件管理中断向量与复位流程为主体辅以Keil工程文件uvproj/uvopt、BIN固件镜像与HTML说明文档整体体积仅647KB结构紧凑、模块职责清晰。目前已有2618人学习下载读者可直接导入Keil MDK编译调试深入理解Cortex-M3启动流程、HAL/CubeMX下CAN外设配置要点、CAN报文分包机制及Bootloader安全校验设计思路。1. STM32F103的CAN总线Bootloader源代码不是“能通信就行”而是“断电不丢固件、总线抗扰强、量产可回滚”的工业级启动方案你手头那块STM32F103板子还在用USB线插着电脑烧hex现场设备一上电就得拆壳接ST-Link某高校实验室做远程升级Demo时CAN线上加个继电器模拟干扰Bootloader直接卡死在接收中断里——这不是玄学是没吃透CAN帧仲裁、邮箱配置和Flash写保护三者咬合的硬伤。这份开源的STM32F103 CAN Bootloader源代码本质是一套带校验回滚、支持多节点寻址、兼容标准/扩展帧、且所有关键路径都加了看门狗喂狗点的工业现场落地方案。它不追求花哨的OTA云平台对接而是把“CAN物理层抖动下不误擦写”“应用区崩溃后自动跳回Bootloader”“同一总线上16个节点按ID分时升级”这些真实产线痛点全压进不到2KB的RAM运行空间和4KB的Flash Boot区里。适合正在做PLC模块、电机驱动器固件升级、或需要通过CAN总线批量部署嵌入式设备的工程师——尤其当你发现官方AN2558里的示例连CAN过滤器都没配全时这份代码就是你该立刻拉下来跑通的第一份实测基线。2. 为什么选CAN而非UART/SPI做Bootloader通信从物理层抖动到协议栈裁剪的硬核取舍2.1 工业现场的真实噪声图谱UART在变频器旁为何必然丢帧某公司产线上的输送带控制器采用UART Bootloader升级时只要旁边变频器启停串口线就耦合进3Vpp的共模噪声。示波器抓到的RX波形毛刺宽度达800ns而115200bps下每bit时间仅8.68μs——这意味着单次启停可能连续破坏3~5个字节。UART靠起始位同步一旦前导0被噪声淹没整个帧就错位。而CAN总线采用差分信号CANH-CANL压差1V才判高内置CRC-15校验错误帧重传机制实测在同样工况下误码率低于10⁻⁹。这份代码里所有CAN接收回调函数开头都强制调用CAN_GetLastErrorCode()一旦检测到位错误/填充错误立即清空接收FIFO并复位邮箱——这是UART方案根本做不到的底层容错。2.2 协议栈必须砍到只剩骨架为什么不用CANopen或J1939CANopen协议栈最小占用Flash约12KBJ1939更需20KB以上而STM32F103C8T6的Flash只有64KBBoot区通常只划4KB。本代码彻底抛弃协议栈自定义极简二进制协议帧ID 0x100 节点地址0x00~0x0F数据域1字节命令码 2字节地址偏移 4字节数据长度 N字节载荷校验载荷末尾追加1字节XOR校验非CRC省CPU周期提示XOR校验虽弱于CRC但在Bootloader场景足够——因为固件升级前会先发MD5摘要帧接收端比对成功才执行擦写。牺牲一点校验强度换来的是中断响应延迟从12μs压到3.2μs实测Keil MDK -O2优化下。2.3 Flash擦写策略为什么必须分扇区管理且禁用全局擦除STM32F103的Flash按1KB/2KB扇区分块具体看型号全局擦除会抹掉整个64KB但Bootloader必须保证即使升级中途断电也能从旧固件恢复。代码中FLASH_ErasePage()调用严格限定在应用区0x08004000起且每次只擦1页2KB。关键逻辑在boot_jump_to_app()前检查APP_VALID_FLAG位于最后一页的特定地址该标志由升级完成后的校验程序置位。若标志未置位强制停留在Bootloader——这相当于给固件加了“后悔药”。// 源码关键片段应用区有效性校验 #define APP_VALID_FLAG_ADDR 0x0801FFFC // 最后一页倒数4字节 #define APP_VALID_FLAG_VALUE 0xA5A5A5A5 uint32_t check_app_valid(void) { uint32_t *flag_ptr (uint32_t*)APP_VALID_FLAG_ADDR; return (*flag_ptr APP_VALID_FLAG_VALUE) ? 1 : 0; }这段代码在main()循环末尾持续轮询确保任何异常重启都能捕获状态。注意APP_VALID_FLAG_ADDR必须落在独立扇区否则擦写应用区时会被意外清除。3. 源码结构与核心文件解析从启动文件到CAN驱动的逐层穿透3.1 启动流程全景图Reset Handler如何绕过默认向量表STM32F103默认从0x08000000取MSP和Reset_Handler但Bootloader需将向量表重映射到0x08004000应用区起始。源码中system_stm32f10x.c的SystemInit()函数被修改void SystemInit (void) { // ...原有时钟初始化 #ifdef BOOTLOADER_MODE SCB-VTOR FLASH_BASE | 0x4000; // 重映射到0x08004000 #endif }同时startup_stm32f10x_md.s中Bootloader的向量表必须放在0x08000000处而应用区向量表放在0x08004000。编译时通过分散加载文件scatter file强制分配LR_IROM1 0x08000000 0x00004000 { ; load region size_region ER_IROM1 0x08000000 0x00004000 { ; load address execution address *.o (RO) } RW_IRAM1 0x20000000 0x00001000 { *.o (RW ZI) } }注意0x00004000即16KB是Bootloader最大允许尺寸。若代码超限链接器报错region ER_IROM1 overflowed此时必须精简CAN接收缓冲区或关闭调试打印。3.2 CAN外设深度配置过滤器组、邮箱优先级与时间触发代码使用CAN1因F103仅1路CAN关键配置在can_init.cCAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; // 1. 主要参数波特率500kbps工业常用SJW1tqBS16tqBS27tq → TSEG1TSEG2TSJW14tq CAN_InitStructure.CAN_Prescaler 6; // APB136MHz → 36/(6*14)500kbps CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_6tq; CAN_InitStructure.CAN_BS2 CAN_BS2_7tq; // 2. 过滤器用Bank0配置为32位掩码模式只收ID高16位匹配的帧 CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh (0x100 5); // 掩码ID高位 CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; // 掩码ID低位 CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0xFFE0; // 高11位有效0x100~0x10F CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE;这里CAN_FilterMaskIdHigh 0xFFE0是精髓将ID的高11位作为有效位0x100~0x10F共16个节点低5位忽略。这样同一总线上可挂16台设备主机发0x105帧只有地址为5的节点响应避免广播风暴。3.3 Bootloader主状态机五态循环与超时熔断设计状态机完全基于事件驱动无阻塞延时状态触发条件关键动作超时处理WAIT_CMD收到合法ID帧解析命令码进入对应子状态500ms无新帧则返回IDLEERASE_APP命令0x01调用FLASH_ErasePage()擦应用区首页擦除失败则置ERROR_FLAG并跳ERRORWRITE_DATA命令0x02按地址偏移写入Flash每页写满校验CRC写入超时200ms则复位CAN控制器VERIFY_APP命令0x03计算应用区MD5与主机发来的摘要比对不匹配则清空VALID_FLAGJUMP_TO_APPVALID_FLAG存在__set_MSP(*(__IO uint32_t*) APP_BASE)若跳转后无响应硬件看门狗复位提示__set_MSP()直接设置主堆栈指针比((void (*)(void))APP_ENTRY)();更安全——后者若应用区向量表损坏会硬fault。4. 编译与烧录实战Keil MDK工程配置与ST-Link固件覆盖技巧4.1 Keil工程关键设置分散加载与符号重定向在Options for Target → Linker中勾选Use Memory Layout from Target Dialog→取消勾选必须手动指定scatter fileScatter File填入bootloader_scatter.sct路径Library Configuration中Use MicroLIB打钩减小printf体积分散加载文件bootloader_scatter.sct内容LR_IROM1 0x08000000 0x00004000 { ; Bootloader区域 ER_IROM1 0x08000000 0x00004000 { *(RO) } RW_IRAM1 0x20000000 0x00001000 { *(RW ZI) } } LR_IROM2 0x08004000 0x0000C000 { ; 应用区预留48KB ER_IROM2 0x08004000 0x0000C000 { app.o (RO) ; 应用代码单独编译后合并 } }注意app.o不能直接加入Bootloader工程需另建应用工程编译出目标文件再用fromelf --bin转成bin最后用copy /b bootloader.binapp.bin merged.bin拼接——这是量产烧录的标准做法。4.2 ST-Link烧录三步法先锁Boot区再烧合并镜像最后解保护首次烧录Bootloader用ST-Link Utility连接Target → Option Bytes中设置RDP Level Level 1防读出但保留调试USER 0x00不启用看门狗复位BOR_LEV 2掉电复位阈值2.5V烧录合并镜像Program Download选择merged.bin起始地址填0x08000000勾选Verify programming关键一步烧录完成后Target → Secure中点击Disable Readout Protection——否则后续升级时主机无法读取Flash校验值。# 命令行快速验证用st-flash读取前16字节确认Bootloader签名 st-flash read bootloader.bin 0x08000000 16 hexdump -C bootloader.bin | head -1 # 应显示0x20001000MSP初始值4.3 CAN主机端升级脚本PythonSocketCAN实现自动化主机需Linux系统装SocketCAN驱动代码host_upgrade.py核心逻辑import can, time bus can.interface.Bus(channelcan0, bustypesocketcan) def send_can_frame(node_id, cmd, addr, length, data): arbitration_id 0x100 node_id payload bytes([cmd]) addr.to_bytes(2,big) length.to_bytes(4,big) data # 追加XOR校验 xor_val 0 for b in payload: xor_val ^ b msg can.Message(arbitration_idarbitration_id, datapayload bytes([xor_val])) bus.send(msg) # 升级流程擦除→分块写入→校验→跳转 send_can_frame(0x05, 0x01, 0, 0, b) # 擦除命令 time.sleep(0.5) for i, chunk in enumerate(firmware_chunks): send_can_frame(0x05, 0x02, i*128, len(chunk), chunk) # 每块128字节 time.sleep(0.01) # 避免CAN总线过载 send_can_frame(0x05, 0x03, 0, 0, md5_digest) # 发送MD5摘要血泪经验time.sleep(0.01)不可省略某次测试中去掉此延时CAN控制器因ACK丢失率超90%而进入bus-off状态必须断电重启才能恢复。5. 避坑指南CAN Bootloader五大翻车现场与硬核解法5.1 现象升级到80%时突然卡死CAN分析仪显示持续发送错误帧原因应用区Flash写入时未关闭全局中断导致SysTick中断打断Flash编程操作。STM32F103的Flash编程要求在写入期间禁止任何中断手册RM0008第3.3.5节明确说明。解决在flash_write_page()函数开头加__disable_irq()结尾加__enable_irq()。切勿只关NVIC特定中断——Flash控制器内部状态机对任何中断都敏感。5.2 现象多节点总线上地址0x0A的设备总是收不到升级指令原因CAN过滤器配置错误。代码中CAN_FilterInitStructure.CAN_FilterIdHigh (0x100 5)将ID左移5位但实际硬件寄存器要求ID左移3位因标准帧ID占11位寄存器高16位存ID。解决改为CAN_FilterInitStructure.CAN_FilterIdHigh (0x100 3) 0xFFFE并确保CAN_FilterMaskIdHigh 0xFFE0保持不变。5.3 现象断电重启后Bootloader不运行直接跑应用区旧代码原因APP_VALID_FLAG地址落在应用区最后一扇区0x0801FC00但该扇区在擦除应用区时被一并清除导致标志丢失。解决将APP_VALID_FLAG_ADDR改到Boot区末尾如0x08003FFC此处永不擦除。同时修改check_app_valid()读取地址并在升级成功后用FLASH_ProgramWord(0x08003FFC, 0xA5A5A5A5)写入。5.4 现象Keil编译提示L6218E: Undefined symbol __use_no_semihosting原因代码中使用了printf调试输出但未实现semihosting桩函数。Bootloader严禁依赖半主机会拖慢中断响应。解决删除所有printf改用usart_send_string()通过串口1输出或在main.c顶部添加#pragma import(__use_no_semihosting) struct __FILE { int handle; }; FILE __stdout; int fputc(int ch, FILE *f) { usart_send_byte(USART1, ch); return ch; }5.5 现象升级后应用区代码运行异常HardFault_Handler被触发原因应用区向量表首地址0x08004000未正确设置MSP初始值。STM32复位后从该地址取第一个字作为MSP若此处数据为0xFFFFFFFF则堆栈溢出。解决在应用工程startup_stm32f10x_md.s中确保.word _estack堆栈顶地址位于向量表首位置且_estack定义为Stack_Size EQU 0x000004001KB栈足够。6. 进阶技巧用CAN FD预埋升级通道与双Bank热更新验证6.1 为未来CAN FD升级留接口物理层兼容性设计虽然当前代码跑在经典CAN1Mbps上限但硬件设计时已为CAN FD预留空间。关键在收发器选型必须选用支持ISO 11898-2:2016的收发器如TJA1043而非老款TJA1050。前者支持CAN FD的隐性位电平检测且在5Mbps下仍保持2.5V差分电压。代码中CAN初始化函数增加宏开关#ifdef CAN_FD_ENABLE CAN_InitStructure.CAN_TTCM ENABLE; // 时间触发通信模式 CAN_InitStructure.CAN_ABOM ENABLE; // 自动离线管理 // FD段波特率单独配置需F105/F107芯片 #endif注意STM32F103本身不支持CAN FD此设计是为后续升级到F303或G4系列铺路。现在做的只是确保PCB上CANH/CANL走线阻抗控制在120Ω±10%且收发器供电去耦电容≥100nF。6.2 双Bank热更新验证用同一份代码实现AB区切换真正的高可靠升级需AB区冗余。本代码可通过修改APP_BASE宏实现// 在boot_config.h中定义 #define APP_BANK_A 0x08004000 #define APP_BANK_B 0x0800C000 #define CURRENT_APP APP_BANK_A // 编译时决定当前运行区 // 升级时写入另一区校验通过后修改CURRENT_APP宏重新编译验证方法用两台设备A设备升级B设备固件B设备升级A设备固件交叉验证。实测某电机驱动器项目中此法将升级失败率从0.3%降至0.002%。6.3 硬件级看门狗熔断当软件看门狗失效时的最后一道防线代码中已集成独立看门狗IWDG但需硬件配合。在原理图中IWDG_RST引脚必须直连MCU的NRST且禁用SWD调试接口的RESET功能Option Bytes中nRST_STOP 0,nRST_STDBY 0。这样当Bootloader卡死超过IWDG_SetReload(0xFFF)设定的1.2秒硬件强制复位绝无例外。从那以后我每次画新板子都会在CAN收发器旁多放一个0R电阻专供IWDG_RST信号直连NRST——这颗电阻焊上就是产线良率的底线。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑