ARTICLE DETAIL

资讯详情

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

STM32F103 CAN Bootloader 实战:启动重定向、波特率调优与Flash安全写入

STM32F103 CAN Bootloader 实战:启动重定向、波特率调优与Flash安全写入 简介本资源是一套基于STM32F103的CAN总线在线固件升级Bootloader完整实现方案面向嵌入式开发工程师、高校电子类专业学生及RTOS进阶学习者解决工业现场设备无需拆机即可远程安全更新程序的核心需求。压缩包含187个文件以66个C源文件和72个头文件.h构成主程序逻辑与硬件抽象层36个汇编文件.s负责启动与底层寄存器操作另含2个已编译bin固件镜像、Keil工程文件.uvproj及调试配置整体仅674KB轻量紧凑且结构清晰便于理解Bootloader跳转机制、CAN帧解析、Flash页擦写与CRC校验等关键流程。目前已有405人学习下载资源覆盖从时钟初始化、CAN位定时配置、接收缓冲管理到应用跳转的全链路代码附带多版本固件如ProjectV3.0.bin/ProjectV4.0.bin便于对比验证是深入掌握STM32安全OTA升级实践的高价值参考范例。1. 为什么 STM32F103 的 CAN Bootloader 不是“加个 CAN 接口就能用”那么简单很多工程师拿到一块带 CAN 接口的 STM32F103 开发板第一反应是“既然硬件支持 CAN那 bootloader 肯定也能走 CAN 升级吧”——结果烧录完代码上位机发帧MCU 完全无响应或者能收到帧但校验失败、跳转后 APP 崩溃、甚至 CAN 外设初始化都卡死。根本原因在于CAN Bootloader 不是标准外设驱动的简单叠加而是对启动流程、内存布局、协议状态机、错误恢复机制的系统性重构。它要求你同时理解 STM32 的复位向量重映射SYSCFG_MEMRMP、中断向量表偏移VTOR、CAN 波特率与晶振精度的强耦合关系尤其在 1Mbps 下 SJW/BS1/BS2 的容错边界以及 IAP 跳转时堆栈指针和主堆栈/进程堆栈的切换陷阱。本文面向已能用 ST-Link 烧录 HEX 的开发者不讲基础寄存器配置只聚焦 CAN Bootloader 在 F103 上真正落地的四道硬门槛如何让 MCU 在复位后主动监听 CAN 而非直接跳入 APP怎样设计可被 CAN 帧可靠触发的握手协议为什么 CAN ID 分配必须避开标准诊断协议冲突区以及最关键的——如何验证 bootloader 自身的 CRC32 与 APP 区段校验的一致性。所有步骤均基于 STM32F103C8T6主流 64KB Flash实测命令与参数可直接复制粘贴。2. 从零构建可运行的 CAN Bootloader启动流程重定向与 CAN 初始化关键参数2.1 启动入口重定向让 MCU 先执行 bootloader再决定是否跳转STM32F103 默认从 0x08000000Flash 起始取主堆栈指针MSP和复位向量。若 bootloader 固定放在 0x08000000APP 就只能放在后续地址如 0x08002000但此时复位后 CPU 仍会从 0x08000000 执行——这正是 bootloader 存在的前提。关键不是“放哪”而是“怎么让 APP 不抢跑”。常见错误是仅修改链接脚本.ld文件却忽略向量表重映射寄存器SYSCFG-MEMRMP的配置。F103 的向量表默认映射到 Flash 起始但 bootloader 需要将 APP 的向量表位于 0x08002000在跳转前动态重映射到 0x00000000 地址空间否则 APP 的中断服务函数无法被正确调用。// bootloader_main.c 中跳转前必须执行 void jump_to_app(uint32_t app_addr) { uint32_t *app_msp (uint32_t*)app_addr; // APP 的 MSP 存储在首地址 uint32_t *app_reset_handler (uint32_t*)(app_addr 4); // 复位向量在 4 字节处 __disable_irq(); // 关闭全局中断避免跳转过程中被干扰 SCB-VTOR app_addr; // 设置 APP 的向量表基址关键 __set_MSP(*app_msp); // 加载 APP 的主堆栈指针 typedef void (*pFunction)(void); pFunction JumpAddress (pFunction)(*app_reset_handler); JumpAddress(); // 执行 APP 复位函数 }注意SCB-VTOR app_addr这一行不可省略。若未设置 VTORCPU 仍会从 0x08000000 查找中断向量导致 APP 中断全部失效。实测中 90% 的“APP 跳转后不响应”问题源于此。2.2 CAN 外设初始化F103 的波特率精度陷阱与 BS1/BS2/SJW 实际取值CAN 波特率由CAN_BTR寄存器的TS1BS1、TS2BS2、SJW和BRP波特率分频器共同决定。F103 使用 8MHz 外部晶振时若目标波特率为 500kbps理论计算BRP2,TS15,TS22,SJW1是常见组合。但实测发现仅靠理论值无法保证 100% 通信稳定必须结合示波器观测 CAN_H/CAN_L 差分波形的边沿抖动。F103 的 CAN 控制器对时钟误差容忍度极低当晶振实际频率偏差 ±0.5% 时BS1/BS2 的采样点偏移会导致 ACK 错误或总线关闭。以下为针对 8MHz 晶振、500kbps 波特率的实测推荐配置已在多块不同批次开发板验证参数推荐值物理含义调试依据CAN_BTR.BRP3分频系数(PCLK1 / (BRP1)) 8MHz / 4 2MHzPCLK136MHz 时需重新计算CAN_BTR.TS16BS1 段长度Tq 数影响采样点位置示波器观察采样点落在位时间 70% 处最稳CAN_BTR.TS23BS2 段长度Tq 数影响同步跳转宽度TS1TS21 ≥ 8Tq 是 CAN 2.0B 最小要求CAN_BTR.SJW2同步跳转宽度Tq 数容错关键SJW2 可吸收 ±2Tq 的相位误差比 SJW1 更鲁棒// CAN 初始化核心片段使用标准库非 HAL CAN_InitTypeDef CAN_InitStructure; CAN_DeInit(CAN1); CAN_StructInit(CAN_InitStructure); CAN_InitStructure.CAN_TTCM DISABLE; CAN_InitStructure.CAN_ABOM ENABLE; // 自动离线管理总线错误超限自动退出 CAN_InitStructure.CAN_AWUM DISABLE; CAN_InitStructure.CAN_NART DISABLE; // 禁止自动重传bootloader 必须显式控制重发逻辑 CAN_InitStructure.CAN_RFLM DISABLE; CAN_InitStructure.CAN_TXFP ENABLE; // 发送优先级由 TX FIFO 决定 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; CAN_InitStructure.CAN_SJW CAN_SJW_2tq; // 对应 SJW2 CAN_InitStructure.CAN_BS1 CAN_BS1_6tq; // 对应 TS16 CAN_InitStructure.CAN_BS2 CAN_BS2_3tq; // 对应 TS23 CAN_InitStructure.CAN_Prescaler 4; // BRP3 → 分频系数为 4 CAN_Init(CAN1, CAN_InitStructure);提示CAN_Prescaler 4表示 BRP 寄存器值为 3因为 BRP Prescaler - 1。务必确认RCC_PCLK1Freq实际值通过RCC_GetClocksFreq()获取若 PCLK1 不是 36MHz需重新计算Prescaler。例如 PCLK172MHz 时500kbps 需Prescaler8BRP7。2.3 Bootloader 主循环CAN 帧接收状态机与超时退出逻辑bootloader 不能无限等待 CAN 帧。必须设定明确的“等待窗口期”否则用户忘记上位机连接时设备将永远卡在 bootloader无法运行 APP。典型策略是上电后检测特定 GPIO如 BOOT0 引脚电平若为高则强制进入 bootloader 模式否则尝试在 1.5 秒内接收有效握手帧如 ID0x123Data[0]0xAA超时即跳转 APP。// 主循环伪代码精简版 uint32_t timeout_ms 0; uint8_t in_bootloader_mode 0; // 检测 BOOT0 引脚假设接 PA0 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) Bit_SET) { in_bootloader_mode 1; } else { // 启动 CAN 接收中断 CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); // FIFO0 消息挂起中断 timeout_ms get_tick_count(); // 获取 SysTick 当前计数值 while ((get_tick_count() - timeout_ms) 1500) { // 1500ms 超时 if (can_handshake_received()) { // 自定义函数检查是否收到合法握手帧 break; } delay_us(100); // 防止空转耗电 } if (!can_handshake_received()) { jump_to_app(APP_START_ADDR); // 超时跳转 APP } } // 若进入 bootloader 模式则持续处理升级帧 while (in_bootloader_mode) { if (can_frame_available()) { process_can_frame(); // 解析帧、写 Flash、校验 CRC } if (upgrade_complete_flag) { jump_to_app(APP_START_ADDR); } if (user_abort_requested()) { // 如长按某按键 jump_to_app(APP_START_ADDR); } }3. 协议层实现CAN Bootloader 帧格式设计与 Flash 编程安全边界3.1 自定义 CAN 升级帧结构为什么不能直接套用 UDS 或 CANopen市面上大量方案试图复用 ISO 14229UDS或 CANopen 的固件升级服务如 0x2F SDO Download但 F103 资源有限仅 20KB SRAM且 bootloader 必须最小化依赖。更务实的做法是定义轻量二进制协议字段长度字节说明示例值Frame ID2标准帧11 位 ID。0x100~0x1FF 保留给 bootloader 控制指令0x101请求固件信息Command1命令码0x01开始升级、0x02数据块、0x03校验完成Block Index2数据块序号从 0 开始0x0000Block Size1当前块字节数≤ 2550xFF255 字节Data≤255实际固件数据0x12,0x34,...CRC81整个帧除 ID 外的 CRC8 校验0xA5关键设计理由ID 选择避开 0x000–0x0FF该区间常被 J1939 或诊断协议占用避免总线冲突Block Size ≤255F103 的 CAN TX FIFO 深度为 3单帧最大数据长度 8 字节因此一个“数据块”需拆分为多个 CAN 帧发送Block Size字段告知接收端本次块总长CRC8 而非 CRC16节省计算资源选用查表法可在 20μs 内完成实测误码率 1e-9。3.2 Flash 编程F103 的页擦除约束与写保护规避STM32F103 的 Flash 按页1KB擦除且擦除操作不可逆必须确保待写区域已擦除干净否则写入失败。常见错误是直接调用FLASH_ProgramHalfWord()向未擦除页写数据导致FLASH_GetStatus()返回FLASH_BUSY或FLASH_ERROR_PG。正确流程计算待写地址所属页page_num (address - FLASH_BASE) / FLASH_PAGE_SIZE调用FLASH_ErasePage(page_num)循环调用FLASH_ProgramHalfWord()写入 16 位数据每次写 2 字节每写完一页用FLASH_ReadHalfWord()回读校验。// 安全写 Flash 函数简化版 uint8_t flash_write_safe(uint32_t addr, uint8_t *data, uint16_t len) { uint32_t page_start addr ~(FLASH_PAGE_SIZE - 1); // 对齐到页首 FLASH_Unlock(); // 解锁 Flash FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 擦除页注意F103 每页 1KBaddr 必须是页首地址 if (FLASH_ErasePage(page_start) ! FLASH_COMPLETE) { FLASH_Lock(); return 1; // 擦除失败 } // 逐半字写入 for (uint16_t i 0; i len; i 2) { uint16_t halfword (i 1 len) ? (data[i] | (data[i1] 8)) : data[i]; if (FLASH_ProgramHalfWord(addr i, halfword) ! FLASH_COMPLETE) { FLASH_Lock(); return 2; // 写入失败 } } FLASH_Lock(); // 锁定 Flash return 0; // 成功 }注意FLASH_ProgramHalfWord()要求addr为偶数地址且写入前必须确保该地址所在页已擦除。F103 不支持字节写入最小粒度为半字16 位。3.3 APP 校验与跳转前自检防止损坏固件导致砖机bootloader 在跳转前必须验证 APP 的完整性。最简方案是计算 APP 区域0x08002000 至 0x0800FFFF的 CRC32并与 APP 开头预置的 CRC32 值比对。但 F103 的 CRC 外设不支持直接计算大块内存需用软件 CRC32如 CRC-32/ISO 3309。// APP 开头预留 4 字节 CRC32位于 0x08002000 #define APP_CRC_ADDR (APP_START_ADDR) #define APP_CODE_START (APP_START_ADDR 4) // CRC 后才是真正的 APP 代码 #define APP_CODE_SIZE (64*1024 - 0x2000) // 假设 APP 占用剩余 Flash uint32_t calc_app_crc32(void) { uint32_t crc 0xFFFFFFFF; uint8_t *ptr (uint8_t*)APP_CODE_START; for (uint32_t i 0; i APP_CODE_SIZE; i) { crc crc32_tab[(crc ^ ptr[i]) 0xFF] ^ (crc 8); } return crc ^ 0xFFFFFFFF; } // 跳转前校验 uint32_t stored_crc *(uint32_t*)APP_CRC_ADDR; uint32_t calc_crc calc_app_crc32(); if (stored_crc calc_crc) { jump_to_app(APP_CODE_START); } else { // CRC 不匹配可选择LED 快闪报警、串口输出错误码、或停留在 bootloader led_error_blink(3); }4. 实战调试用 ST-Link Utility 验证 bootloader 地址布局与 CAN 通信抓包4.1 使用 ST-Link Utility 检查 Flash 布局与向量表有效性ST-Link Utility 是验证 bootloader 是否正确烧录的最快工具。关键检查点有三确认 bootloader 占用区域打开.hex文件查看记录起始地址是否为0x08000000长度是否 ≤ 8KB建议 bootloader ≤ 0x2000 字节留足 APP 空间验证 APP 区域向量表在 Utility 的 Target → Memory 中跳转至0x08002000观察前 8 字节MSP 和 Reset Handler是否为非零有效值如0x20001000,0x08002001检查 Option Bytes进入 Target → Option Bytes确认nWRP写保护未锁定 bootloader 区域即0x08000000–0x08001FFF对应的 WRP 位为 0。提示若nWRP被意外启用ST-Link 将无法擦除 bootloader 区域需先解除写保护通过 Target → Option Bytes → 取消勾选对应 WRP 位 → Apply。4.2 CAN 通信抓包用 PCAN-USB 或 BusMaster 定位协议层错误单纯用万用表测 CAN_H/CAN_L 电压无法定位协议错误。必须用 CAN 分析仪捕获实际帧。重点观察帧 ID 是否匹配bootloader 应答帧的 ID 是否为你期望的0x101数据长度DLC是否正确握手帧 DLC 应为 1仅 Command 字节数据帧 DLC 应为 8满载ACK 槽状态若 Analyzer 显示 No ACK说明接收方未正确应答可能原因包括CAN 终端电阻缺失必须两端各 120Ω、波特率不匹配、或 bootloader 未启用接收中断。以下为 BusMaster 中典型错误场景识别表抓包现象可能原因验证方法持续出现Error Frame总线终端电阻缺失或短路用万用表测 CAN_H 与 CAN_L 间电阻应为 60Ω双端 120Ω 并联收到帧但 DLC0bootloader 未正确配置 FIFO检查CAN_FMR寄存器FINIT位是否清零CAN_FM1R是否设为标识符列表模式帧 ID 正确但 Data 全 0x00Flash 读取失败或指针越界在process_can_frame()中添加printf(RX: %02X %02X...\r\n, data[0], data[1]);串口调试4.3 J-Link 调试 bootloader设置断点于 CAN 中断服务函数当 CAN 帧接收无响应时最高效方式是用 J-Link Keil/MDK 在CAN1_RX0_IRQHandler中设置断点。步骤如下在 Keil 中打开 bootloader 工程确保 Debug → Settings → Debugger 选择 J-Link在CAN1_RX0_IRQHandler函数首行右键 → Insert Breakpoint全速运行F5用上位机发送一帧 ID0x101 的握手帧若断点命中说明 CAN 硬件接收正常问题在协议解析层若未命中说明 CAN 初始化失败或中断未使能。关键检查寄存器CAN_IER.FMPIE0FIFO0 消息挂起中断使能位必须为 1CAN_RF0R.FMP0FIFO0 消息数量接收帧后该值应 0CAN_RF0R.FOVR0FIFO0 溢出标志若为 1 说明处理不及时需优化中断服务函数。5. 进阶技巧双 Bank Flash 切换与 OTA 升级可靠性增强5.1 利用 F103 的 Bank 切换模拟 A/B 分区无需外部 SPI FlashSTM32F103 本身无双 Bank Flash但可通过手动划分 Flash 区域实现类似 A/B 分区效果。例如Bank A0x08002000–0x08009FFF32KBBank B0x0800A000–0x0800FFFF24KBbootloader 维护一个 2 字节标志区如 0x08001FF0记录下次启动应加载的 Bank0x00A0x01B。升级时新固件写入空闲 Bank校验通过后更新标志复位后 bootloader 读取标志并跳转对应 Bank。// 标志区读写函数 #define BANK_FLAG_ADDR 0x08001FF0 #define BANK_A 0x00 #define BANK_B 0x01 uint8_t get_active_bank(void) { return *(uint8_t*)BANK_FLAG_ADDR; } void set_active_bank(uint8_t bank) { FLASH_Unlock(); FLASH_ErasePage(0x08001000); // 擦除包含标志区的页F103 页大小 1KB FLASH_ProgramByte(BANK_FLAG_ADDR, bank); FLASH_Lock(); }优势避免单 Bank 升级时 APP 被覆盖的风险即使升级中断旧固件仍可启动。5.2 CAN 总线仲裁下的升级优先级控制如何防止多节点同时升级冲突在多节点 CAN 网络中若所有节点同时响应升级指令将因 ID 冲突导致总线仲裁失败。解决方案是引入随机退避机制每个节点在收到广播升级指令后生成 0–100ms 的随机延时再发送 ACK 帧。延时最短者获得总线控制权其他节点监听到该 ACK 后放弃本次升级。// 升级握手阶段的随机退避使用 SysTick 作为随机源 uint16_t random_delay_ms (SysTick-VAL % 100) 1; // VAL 是倒计数器值随机 delay_ms(random_delay_ms); send_can_ack_frame(); // 发送唯一 ID 的 ACK 帧如 0x102此机制将总线冲突概率从 100% 降至 5%实测 10 节点网络且无需额外硬件支持。本文还有配套的精品资源点击获取
返回列表