尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

基于CAN总线的STM32 IAP固件升级:Bootloader与Flash分区设计 简介一套基于CAN通信完成STM32固件在线升级的完整工程资料面向嵌入式开发者、毕业生和竞赛选手适合用于IAP远程升级方案学习、课程设计与项目复刻。资源包共365个文件压缩后约7.71MB包含115个H头文件和109个C源码文件这些构成工程主体同时配有Keil工程配置、链接脚本、bin/axf可执行固件、Python辅助脚本以及说明文档覆盖从源码编辑、编译链接、固件生成到烧录验证的完整链路。工程经严格测试可正常运行能够直接复现CAN总线远程烧录流程即使不会设计PCB也可按引脚定义用面包板、杜邦线和外设模块搭出验证环境快速完成移植。目前已有357人学习下载作者专注嵌入式领域多年使用中如遇到协议解析、Flash分区或驱动调试等问题欢迎私信交流会及时提供帮助若有嵌入式开发工具、学习资料方面的需求也可进一步沟通。1. CAN通信烧录设备封壳之后固件升级还有一条路现场设备灌胶密封、只引出两根CAN线拆壳接SWD不现实产线上一批设备不带下载口逐台插下载器也拖慢节奏。用CAN总线做IAPIn-Application Programming解决的就是这个问题工程里先固化一段Bootloader之后的固件更新全部走CAN帧完成。它不依赖USB转串口也不依赖JTAG/SWD传输距离和抗干扰能力比串口更适合工业现场。首次烧录Bootloader仍然需要SWD但之后整机生命周期内都不需要再开壳。这篇文章以一套完整的Keil工程为主线拆解Bootloader分区设计、CAN波特率参数计算、Flash分块写入策略、上位机协议帧以及最后怎么用示波器验证波形适合正在做STM32系统集成、毕业设计或产线固件升级方案的人。2. Bootloader与Flash分区IAP的第一步是重新规划存储2.1 上电启动链路与IAP的核心思路STM32上电后CPU从0x08000000处读出栈顶指针随后从0x08000004处读出复位向量跳转到启动代码执行。如果整个Flash只放一份用户程序程序跑飞或固件损坏只能借助下载器恢复。IAP的做法是在Flash起始位置放Bootloader用户程序App放在后面偏移地址。Bootloader上电后先检查是否需要升级需要则通过CAN接收固件并写入App分区不需要则直接跳转App整个过程对用户透明。这套工程采用的划分方式如下以512KB Flash容量的F103ZE为例区域起始地址大小内容Bootloader0x0800000032KBIAP固件App0x08008000440KB用户程序系统参数区0x0807E0008KB版本号、升级标志Bootloader放在0x08000000编译下载时直接烧写到这里。App烧写到0x08008000。系统参数区记录当前固件版本和上次升级结果升级请求也可以通过参数区标志位触发上位机先写标志设备下次上电时Bootloader读取标志决定是否进入升级流程。这样即使升级中途断电重新上电后Bootloader仍然能再次进入升级流程设备不会被锁死。分区方案的关键在于App起始地址必须与Flash页边界对齐F103高密度芯片每页2KB0x08008000正好是页边界。2.2 Keil工程地址配置与分散加载Bootloader工程保持默认IROM1起始地址0x08000000大小按实际使用量配置这里设为0x8000即32KB。App工程必须把IROM1起始地址改为0x08008000大小对应0x00078000。IRAM1保持芯片实际SRAM容量。这一步如果漏改App编译出来的中断向量表还在0x08000000跳转后中断全部指向Bootloader的向量App一进中断就跑飞。改完起始地址后MDK在Build时根据Target窗口配置生成分散加载文件.sct。工程里出现的IAP_sct.Bak是分散加载文件备份可以打开查看链接器实际使用的地址范围。IAP.axf是ARM ELF格式编译产物调试器下载和fromelf工具转换都依赖它。在App工程After Build里加一条命令把axf转成bin用于CAN传输fromelf --bin --output.\Output\App.bin .\Output\App.axf这条命令把App.axf转换为纯二进制格式的App.bin。对比axfbin没有调试信息和段信息正是Bootloader通过CAN逐帧接收时需要的数据格式。如果App工程把空余Flash初始化为0x00bin会比实际代码大很多接收时间明显变长所以通常用默认0xFF填充未用区。转换后的bin大小就是后面CAN传输的数据量上位机解析bin文件时可以直接用文件长度作为固件总长度。提示App和Bootloader的IROM1起始地址、大小要写进项目文档。换了电脑或换了芯片型号第一个要检查的就是这两个值。2.3 工程文件里除了源码还有什么这套IAP工程里除了源码还保留了MDK界面配置文件IAP.uvguix.Administrator记录窗口布局和断点位置属于用户级配置不影响编译。keilkilll.bat是清理脚本作用是删除Objects、Listings目录下编译中间文件打包上传时控制体积。拿到工程后建议先执行一次keilkilll.bat再用MDK重新编译中间文件会按本机工具链重新生成避免路径不匹配问题。Bootloader和App两份工程要严格分开存放。常见错误是把两份工程放在同一目录MDK会根据.uvguix里的用户信息互相覆盖配置文件导致编译输出混乱。3. CAN初始化与数据帧接收250kbps下的通信可靠性3.1 引脚、时钟和初始化代码F103的CAN1挂在APB1总线上引脚默认是PA11RX和PA12TX。如果板上这两个引脚被占用可以用重映射功能把CAN1移到PB8/PB9但要注意重映射后复用功能推挽配置随之变化。RX用上拉输入而不是浮空输入避免无信号时电平不确定导致误触发。初始化代码如下void CAN_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; /* CAN_RX */ GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; /* CAN_TX */ GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); CAN_InitStructure.CAN_TTCM DISABLE; CAN_InitStructure.CAN_ABOM ENABLE; /* 总线离线自动恢复 */ CAN_InitStructure.CAN_AWUM ENABLE; CAN_InitStructure.CAN_NART DISABLE; CAN_InitStructure.CAN_RFLM DISABLE; CAN_InitStructure.CAN_TXFP DISABLE; 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_2tq; CAN_InitStructure.CAN_Prescaler 16; CAN_Init(CAN1, CAN_InitStructure); CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN_FilterInitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); }这段代码里最容易忽略的是CAN_ABOM ENABLE。总线发生严重错误时CAN控制器自动进入离线状态不打开ABOM控制器一直停在离线状态不再参与通信必须复位才能恢复。现场其他节点干扰、端子松脱都可能导致离线打开ABOM后控制器能在总线恢复空闲时自动重新上线。CAN_BS1和CAN_BS2决定采样点位置1tq、6tq、2tq的组合让采样点落在位时间的77.8%处这是CAN总线比较标准的采样点位置兼顾了总线传播延迟和同步误差容忍度。3.2 波特率计算与参数调整CAN波特率计算方式APB1时钟 / CAN_Prescaler / (1 BS1 BS2)。以F103默认APB1时钟36MHz为例BS1为6tq、BS2为2tq加上同步段1tq一个位时间9tq。Prescaler取16时波特率等于36MHz / 16 / 9 250kbps。常用参数组合如下目标波特率PrescalerBS1BS2实际波特率一个位时长1Mbps4621Mbps1μs500kbps862500kbps2μs250kbps1662250kbps4μs这套工程选250kbps有实际考量固件传输耗时主要由Flash擦写时间决定波特率从250k提升到500k省下的时间有限250kbps在常见双绞线下的稳定传输距离能到百来米量级1Mbps下距离明显缩短。更换外部晶振导致APB1时钟变化时按公式重新计算Prescaler。接收端如果出现大量位错误用示波器量CAN TX引脚一个显性位宽度250kbps下恰好4μs偏差超过5%就要检查时钟配置或分频值。3.3 滤波策略与接收中断设计IAP下载过程中总线上可能混有设备自身业务报文。工程里采用掩码全零的配置让所有帧进入FIFO在中断服务函数里再按StdId判断是否属于本轮升级报文。这样做的优点是不会漏帧调试阶段方便观察总线上的每一条报文。生产环境可以收紧掩码只接收ID在0x500到0x5FF范围内的帧避免无关中断频繁打断Flash写入流程。接收中断处理逻辑简化为StdId等于0x500走命令解析等于0x501走数据分发。注意CAN接收FIFO只有3个邮箱中断里要尽快把帧搬出来不要在中断函数里做Flash擦写擦写耗时在毫秒级期间邮箱溢出直接丢帧。数据搬进RAM缓冲区后Flash写入放到主循环或状态机里处理这是CAN IAP代码设计里一个容易踩的坑。4. Flash擦写与程序跳转真正动Flash的代码4.1 页擦、半字写入与缓冲区设计STM32F103的Flash编程粒度是半字16位擦除粒度是页。中容量芯片一页1KB高容量芯片一页2KB。每收一帧CAN数据就擦一次页效率很低Flash寿命损耗也大。常见做法是在RAM里开一个页大小的缓冲区收满一页再统一擦写。这套工程用高密度芯片页大小2048字节缓冲区按2048字节分配。接收数据帧时先把有效数据拷贝到pageBufferpageOffset累加帧长度。pageOffset达到2048字节或固件全部接收完毕触发一次Flash写入。写入前检查目标地址是否落在App区域防止异常数据包把Bootloader自身擦掉。具体写入函数如下uint8_t Flash_WritePage(uint32_t addr, uint8_t *buf, uint16_t len) { uint16_t i; if (addr len APP_END_ADDR) { return 0x01; /* 地址越界拒绝写入 */ } FLASH_Unlock(); if (FLASH_ErasePage(addr) ! FLASH_COMPLETE) { FLASH_Lock(); return 0x02; } for (i 0; i len; i 2) { uint16_t halfWord buf[i]; if (i 1 len) { halfWord | (uint16_t)buf[i 1] 8; } else { halfWord | 0xFF00; /* 奇数长度补0xFF */ } if (FLASH_ProgramHalfWord(addr i, halfWord) ! FLASH_COMPLETE) { FLASH_Lock(); return 0x03; } } FLASH_Lock(); return 0; }先检查地址范围再解锁Flash执行页擦除最后按半字写入。奇数长度的最后一字节补0xFF因为Flash擦除后状态本来就是0xFF补齐后不会引入多余数据又能避免ProgramHalfWord写入非半字对齐地址触发HardFault。FLASH_Unlock和FLASH_Lock必须成对出现中途掉电没关系再次上电Flash仍然处于锁定状态。4.2 分块接收与剩余长度控制上位机发送固件前先通过命令帧告知固件总长度下位机存入totalLen变量每收一帧数据就减去实际写入字节数。最后一帧长度通常不足6字节如果机械地等pageOffset攒到2048才写Flash最后一帧会一直被卡在缓冲区里。所以数据帧处理逻辑的判断标准改成两个条件pageOffset达到页大小或者remainLen减到0任一成立就写入Flash并清空缓冲区void Firmware_RxProcess(CanRxMsg *rxMsg) { uint8_t len rxMsg-DLC; uint8_t i; if (len remainLen) { len remainLen; /* 防止最后一帧超出实际长度 */ } for (i 0; i len; i) { pageBuffer[pageOffset] rxMsg-Data[i]; } remainLen - len; if (pageOffset PAGE_SIZE || remainLen 0) { if (Flash_WritePage(currentAddr, pageBuffer, pageOffset) ! 0) { SendResponse(0x01); /* 写入失败回错误码 */ return; } currentAddr PAGE_SIZE; /* 指向下一页 */ pageOffset 0; } SendResponse(0x00); /* ACK */ }这里的SendResponse是回给上位机的ACK。上位机收到ACK才发下一帧数据帧丢失后上位机超时重发当前帧下位机因为还没收到该帧缓冲区数据不完整在Flash_WritePage的地址越界检查处会直接拒绝。这是保证固件完整性的第一道防线。掌握这个节奏后还可以改成批量ACK策略连续发32帧下位机攒够32帧回一次ACK带最高包序号上位机确认序号连续即可继续吞吐量比逐帧确认高三四倍。4.3 跳转App与中断向量重映射固件写完后Bootloader跳转App。跳转不是简单把PC指到App地址还要处理栈指针、中断向量表和外设状态。F103的中断向量表默认在0x08000000App运行在0x08008000不重映射向量表App里任何一个中断触发都会跳回Bootloader的向量位置程序立刻跑飞。跳转函数标准写法typedef void (*pFunction)(void); void Jump_To_App(uint32_t appAddr) { uint32_t stackAddr; pFunction jumpFunc; stackAddr *(volatile uint32_t *)appAddr; jumpFunc (pFunction)(*(volatile uint32_t *)(appAddr 4)); if ((stackAddr 0x20000000) || (stackAddr 0x20010000)) { return; /* 栈顶不在SRAM范围拒绝跳转 */ } CAN_DeInit(CAN1); GPIO_DeInit(GPIOA); RCC_DeInit(); __disable_irq(); SCB-VTOR appAddr; /* 重映射中断向量表 */ __set_MSP(stackAddr); jumpFunc(); }第一行读App起始地址处存放的栈顶指针第二行读复位向量。校验栈顶指针落在SRAM范围内防止App区域全是0xFF时跳进非法地址。跳转前反初始化CAN外设和GPIO否则外设中断挂起标志带到App里App初始化时可能卡死在等待标志位。SCB-VTOR将中断向量表重定向到App区最后用__set_MSP切换主栈指针再调用复位向量完成跳转。提示跳转前读取App区起始两个字的校验值如果栈顶指针和复位向量都不在合理范围说明App区域没有有效程序此时停在Bootloader等待重新升级比直接跑飞更合理。5. 上位机协议与烧录流程让CAN报文替你做版本管理5.1 协议帧定义IAP通信双方是上位机USB-CAN适配器加PC工具和下位机Bootloader。使用标准帧11位ID波特率按第3章的250kbps配置。帧ID分配如下帧ID方向用途0x500上位机到下位机命令帧0x501上位机到下位机固件数据帧0x502下位机到上位机应答帧命令帧Data[0]存命令码Data[1]到Data[4]放参数。命令码定义0x01握手0x02擦除并告知固件长度0x03结束传输0x04跳转运行。数据帧每帧最多携带6字节有效数据剩余2字节留作包序号和校验字段。应答帧Data[0]是状态码0x00成功0x01写入失败0x02序号不连续。上位机根据状态码决定继续发送还是重传。5.2 握手、擦除、传输、跳转四个阶段完整烧录流程分四步。第一步握手上位机发送ID0x500、数据0x01的握手帧下位机应答版本号。这一步确认CAN物理链路正常同时确认下位机处于Bootloader状态。如果设备上电后默认跳转App握手收不到应答需要先发跳转Bootloader的命令或用硬件引脚让Bootloader跳过App启动。第二步擦除上位机发送0x02命令帧参数带固件总长度。下位机收到后清空计数器和缓冲区计算需要擦除的页数。注意擦除不是升级开始前一次性做完而是按第4.2节逐页擦写。预先告知总长度的作用是让下位机知道最后一帧什么时候到来同时可以直接拒绝超过App分区容量的固件。第三步传输上位机把App.bin按每帧6字节打包逐帧发送到ID0x501下位机每收一帧写缓冲区并回ACK。传输结束后发送0x03结束帧下位机把缓冲区剩余数据写入Flash。第四步跳转确认数据全部写完上位机发送0x04命令下位机执行Jump_To_App。5.3 Python上位机最小实现调试阶段不用写完整上位机界面用Python配合python-can库就能把协议跑通。以下是一段可运行的核心逻辑import can bus can.interface.Bus(channel0, interfacepcan, bitrate250000) def send_cmd(cmd, param0): data bytearray(8) data[0] cmd data[1:5] param.to_bytes(4, big) bus.send(can.Message(arbitration_id0x500, datadata)) def wait_ack(timeout1.0): msg bus.recv(timeouttimeout) return msg is not None and msg.arbitration_id 0x502 firmware open(App.bin, rb).read() send_cmd(0x01) # 握手 wait_ack() send_cmd(0x02, len(firmware)) # 擦除并告知固件长度 wait_ack() for i in range(0, len(firmware), 6): chunk firmware[i:i6] if len(chunk) 6: chunk chunk b\xFF * (6 - len(chunk)) data bytearray(8) data[0] (i // 6) 8 # 包序号高字节 data[1] (i // 6) 0xFF # 包序号低字节 data[2:8] chunk bus.send(can.Message(arbitration_id0x501, datadata)) wait_ack() send_cmd(0x03) # 结束传输 wait_ack() send_cmd(0x04) # 跳转运行interfacepcan对应PCAN硬件换用周立功或其他USB-CAN卡时改成对应的python-can接口名。每个关键阶段都等待ACK数据帧逐帧确认牺牲吞吐保证可靠性。包序号放在数据域前两个字节最后一个分片用0xFF填充后再发送和下位机的剩余长度判断逻辑配套。bin文件小于6字节时firmware[i:i6]会截断到实际长度填充逻辑自动补齐。6. 回环自测与CAN波形排查技巧6.1 回环测试是最快的物理层自检调通Bootloader之前先在板子上烧一个只做CAN回环的最小程序或直接在Bootloader代码里加测试分支收到ID0x600的帧原样用ID0x601发回。上位机发一帧测试数据能收到回发帧说明CAN控制器、收发器、总线链路、终端电阻、波特率五个环节全部正常。回发帧收不到优先检查终端电阻和CAN收发器的TXD/RXD引脚。F103的TXD是PA12用示波器看PA12和CAN收发器TXD输入脚之间是否虚焊或走线断裂。6.2 用示波器看CAN波形判断通信质量CAN总线的显隐性电平直接反映物理层状态。示波器探头接在CAN_H和CAN_L之间用差分测量方式观察。隐性位时两根线都在2.5V附近差分电压接近0V显性位时CAN_H拉到约3.5VCAN_L降到约1.5V差分约2V。能看到清晰的显隐性交替波形但上位机收不到数据多半是波特率不匹配或采样点设置不对。用示波器光标测量一个显性位的时长250kbps对应4μs偏差超过5%按第3.2节公式重新计算预分频。总线只有单节点时显性位后的反向回跳过冲明显说明缺少终端电阻或阻值不对。6.3 几个容易踩的坑CAN IAP最容易出问题的点集中在三方面。一是Bootloader和App工程的IROM1地址不一致跳转后中断向量错乱现象是App能运行但一按按键或一开定时器就复位。二是数据帧发送过快导致FIFO溢出现象是升级过程中偶尔卡住重试又恢复解决办法是按批次确认而不是逐帧确认。三是Flash写入函数漏了地址越界检查生产环境下上位机固件发错版本把App写入Bootloader区域覆盖IAP程序设备直接变砖。调试下载速度时先统计擦写一页的耗时。STM32F103擦除加写入2KB页的时间在几十毫秒量级上位机发完一帧马上等ACK即可不需要额外加延时。加了延时整体下载时间会明显变长量产场景下这是容易被忽略的效率问题。最直接的验证方式把App.bin用标准烧录器下载到0x08008000再用CAN IAP重新刷一遍同一份固件对比两次运行行为一致说明CAN IAP链路工作正常。本文还有配套的精品资源点击获取
返回列表