首页 / 资讯中心 / 文章详情

STM32F4串口/485 OTA升级:Bootloader设计方案解析

STM32F4串口/485 OTA升级:Bootloader设计方案解析 ★ FEATURED ARTICLE
简介一套面向STM32F4的OTA升级参考实现针对需要自行维护Bootloader、通过串口或RS485总线完成固件远程更新的嵌入式开发场景适合有C语言和STM32基础、希望快速上手Bootloader设计的工程师。包内包含Bootloader与App两个完整工程Bootloader负责接收固件并请求跳转App工程可作为待升级用户程序模板采用自制方案读者可以从中学到分区划分、固件校验、传输协议及跳转执行的实现思路再迁移至真实产品。压缩包共277个文件以90个头文件、88个C源文件及Keil工程文件为主体同时包含axf/hex/bin固件镜像、map映射表、分散加载描述文件和清理脚本整体约2.86MB文件组织清晰。已有2540人学习下载对于希望降低串口/485升级改造门槛的开发者这套资料提供了可直接编译验证的工程结构与可运行的参考代码能节省从零搭建的时间。1. 串口和 485 上的 STM32F4 OTA为什么现场升级离不开这块 Bootloader现场设备固件更新最烦人的其实不是写代码而是跑现场。机柜里几十台设备每台都要开盖插 JTAG一天下来手都抖。串口和 485 做 OTA 的意义就在这里不拆机、不用调试器靠现场已有的两根线就能把固件刷进去还能顺便兼顾配置读取和设备诊断。这次拆的方案标题里写的「方式1」指的是自己定义帧结构、按包分块传输的 IAP 玩法和走现成 YMODEM 协议是两条不同的路线前者对指令集和流程的把控更细。它适合正在给 STM32F4 写 Bootloader 的开发者也适合想把 485 总线复用成升级通道的工控项目。读完你能搞明白分区怎么划、跳转怎么写、协议怎么定以及最容易被忽视的 485 方向切换和升级断电回滚问题这些都是量产固件里绕不开的硬骨头。2. Bootloader 设计的底座Flash 分区与启动跳转2.1 为什么 Boot 和 App 必须分家分区表怎么定IAP 的基本思想是芯片上电先执行 Bootloader由它决定是继续驻留等待升级还是跳转到 App 跑业务。要做到这一点Flash 里必须给 Bootloader 和 App 划出互不侵犯的区域否则 App 一写就把升级逻辑冲掉下次上电直接变砖。以 1MB Flash 的 STM32F407 为例它的扇区大小不全是均等的前 4 个扇区每个 16KB第 5 个扇区 64KB后面是 3 个 128KB 的大扇区。分区时最好按扇区边界切省得擦除时牵连邻居。我一般这样划区域起始地址大小扇区用途Bootloader0x0800000064KB0~3串口/485 升级逻辑App0x08010000896KB4~11业务固件标志区0x080FF8002KB12 尾部Boot 标志、升级状态、固件 CRC这里有个容易被忽略的细节App 的起始地址必须是 0x08010000 这类扇区起始位置不能随意定在 0x08008000。因为 App 开头 64KB 是中断向量表和启动代码如果地址落在扇区中间擦除操作会波及到其他扇区里还活着的代码。标志区放在最后一个扇区尾部平时不擦不写只有收到升级指令时才动它这样能最大程度降低标志区被意外写坏的概率。另一个要点是编译链接。Bootloader 工程和 App 工程的链接脚本都要改Bootloader 的 FLASH 起始地址保持 0x08000000 不变App 工程要把 FLASH 起始地址改成 0x08010000同时把中断向量表的偏移设置成一致的值。这一步忘了改后面跳转写得再漂亮中断也全跑飞。链接脚本里还要把 RAM 区域一并检查有些工程把栈顶指到 RAM 顶部两边的栈顶地址如果重叠跳过去会互相踩内存。我见过一个蛋疼案例App 里全局数组超大栈顶顶到了 Bootloader 的堆区Bootloader 跑升级时内存被 App 残留数据污染表现是升级到一半无缘无故复位。2.2 跳转函数MSP、VTOR、中断关闭缺一不可跳转的原理其实很朴素芯片复位后从 0x08000000 读栈顶指针 MSP 和复位向量 PC然后从 PC 开始执行。我们要做的就是同样从 App 起始地址读出这两个值自己扮演一次复位过程。代码不长但顺序错了就是灾难现场#define APP_BASE_ADDR 0x08010000 typedef void (*pFunction)(void); void JumpToApp(void) { // App 首 4 字节是栈顶指针紧接着 4 字节是复位向量 uint32_t sp_value *(volatile uint32_t *)APP_BASE_ADDR; uint32_t pc_value *(volatile uint32_t *)(APP_BASE_ADDR 4); __disable_irq(); // 第一步关全局中断 SysTick-CTRL 0; // 停掉系统节拍防止跳转途中进 Tick 中断 HAL_RCC_DeInit(); // 复位时钟和外设把状态清干净 SCB-VTOR APP_BASE_ADDR; // 重定向向量表到 App 区域 __set_MSP(sp_value); // 再设栈指针 pFunction app_entry (pFunction)pc_value; app_entry(); }逻辑说明先关中断是因为如果中断开着跳转过程中一个随机中断进来向量表此时还没切到 App 区域执行流会跑到 Bootloader 的中断处理函数里复位向量根本没机会被执行。接着停 SysTick它一秒几万次地触发是最容易捣乱的中断源。HAL_RCC_DeInit 把时钟和外设复位避免把 Bootloader 里使能过的外设状态带进 App——比如 DMA 如果还在跑进了 App 可能持续往内存写数据。最后设置 VTOR 再设 MSP顺序不能倒因为 MSP 是当前栈指针改了之后如果系统还要压栈保存局部变量就全乱套了。参数说明APP_BASE_ADDR 必须与 App 工程链接脚本里的 FLASH 起始地址严格一致。如果 App 实际链接在 0x08020000Bootloader 却按 0x08010000 去读读出来的是旧固件的栈顶和复位向量跳过去即 HardFault。这类问题黑匣子一样难查因为 Bootloader 看起来跳了App 却一点动静没有。排查方法是在跳转前把读到的 sp_value 和 pc_value 通过串口打印出来和 App 工程的 map 文件里 __initial_sp 以及 Reset_Handler 地址比对一眼就能看出是不是地址不匹配。2.3 Boot 标志决定上电后走哪条路Bootloader 每次上电都要回答一个问题跳 App 还是留在原地等升级我的做法是在标志区放一个 32 位魔数。正常状态写一个跳转魔数Bootloader 看到它就直接跳要升级时App 或者上位机把这个魔数改写掉复位后 Bootloader 就留在原地接收固件。关键代码如下#define BOOT_FLAG_ADDR 0x080FF800 #define BOOT_MAGIC_RUN 0xA5A54321 // 正常跳 App #define BOOT_MAGIC_UPGRD 0x5A5A5678 // 等待升级 void WriteBootFlag(uint32_t magic) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase {0}; erase.TypeErase FLASH_TYPEERASE_SECTORS; erase.Sector FLASH_SECTOR_12; // 标志区所在扇区 erase.NbSectors 1; erase.VoltageRange FLASH_VOLTAGE_RANGE_3; uint32_t err 0; HAL_FLASHEx_Erase(erase, err); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, BOOT_FLAG_ADDR, magic); HAL_FLASH_Lock(); } uint32_t ReadBootFlag(void) { return *(volatile uint32_t *)BOOT_FLAG_ADDR; }逻辑说明标志区先整体擦除再写入新魔数因为 Flash 只能把 1 写成 0不能把 0 写成 1不擦直接写的话新旧值会按位与在一起得到谁都不是的脏数据。Bootloader 主函数里流程很简单读标志是 BOOT_MAGIC_RUN 就跳 App否则进入升级等待循环。擦除和写入前记得调 HAL_FLASH_UnlockF4 系列 Flash 默认是锁住的不解锁直接调用写函数会返回错误码而且这个错误码容易被 HAL 层的宏吞掉表现为写入静默失败。这里有个设计取舍为什么不把标志放在备份寄存器或者 RTC 后备区备份寄存器掉电会丢放 Flash 掉电不丢适合App 请求升级后复位Bootloader 依旧知道要升级的场景。代价是 Flash 有擦写寿命频繁升级的设备要注意别把标志区磨穿所以我把标志区单独划在一个不常写的扇区尾部平时不参与任何操作。如果产品一天要升十几次建议把标志做成先写备份魔数再双区交替但这属于后话本文单区方案够用。3. 传输协议设计帧结构、CRC 与接收状态机3.1 帧结构让分包传输有章可循串口升级最忌讳的就是把固件当裸流直接往外塞。没有帧边界接收端不知道一条指令在哪结束、下一条从哪开始错一个字节后面全乱。所以我的习惯是先定帧格式再写收发逻辑协议文档先行代码后补。这个方案的帧结构长这样字段字节数说明帧头2固定 0xAA 0x55用于同步命令10x01 握手 / 0x02 擦除 / 0x03 写入 / 0x04 校验 / 0x05 跳转长度2小端序数据域字节数数据N负载写入时为固件分块CRC162从命令字开始到数据域末尾的 CRC16-CCITT命令字拆开看握手用于上位机探测设备、获取固件大小和版本信息避免拿到错误的固件直接开刷擦除可以是按扇区逐个擦也可以是整片擦整片擦省事但中途断电风险更大我的做法是先整片擦反正传输前断电的概率远小于传输中写入是升级的主体一包建议 256 或 1024 字节取决于下位机 RAM 缓冲大小我用 1024校验是升级完成前最后一道保险跳转是 App 通过自身业务逻辑触发升级时用的比如设备收到一条特定串口指令就自己进入升级流程不用上位机干预。CRC16 多项式用 0x1021 的 CCITT 版本初值 0xFFFF。为什么不用 CRC8 或 CRC32CRC8 只有 8 位传输中两位翻转可能查不出来CRC32 强但计算开销大在 115200 波特率下纯软件算 1MB 固件要好几秒体验很差。CRC16 对固件传输这种场景已经够用查表实现也简单。帧头不参与 CRC 计算这样状态机即使在乱流中也能靠帧头重新同步如果帧头也参与 CRC同步逻辑会绕一大圈。响应帧也值得提前约定每收到一帧合法指令下位机回一个单字节 ACK0x06或 NAK0x15必要时带上一字节的状态码。ACK/NAK 机制虽然原始但调试起来直观上位机侧打印出来就能看出是哪一类错误比一堆自定义状态码好使。3.2 接收解析用状态机代替傻等接收数据如果写在中断里做整包解析一个字节一个字节地 switch一旦 Flash 写入这种耗时操作混进来中断就被堵住后面的字节全部丢。我的做法是串口中断只把字节塞进环形队列主循环里用状态机消费队列逐字节推进解析。状态机长这样typedef enum { ST_IDLE, ST_HEADER1, ST_HEADER2, ST_CMD, ST_LEN_L, ST_LEN_H, ST_DATA, ST_CRC_L, ST_CRC_H } FrameState; FrameState state ST_IDLE; uint8_t rxbuf[1028]; uint16_t rxlen 0, rxidx 0; void FrameParser(uint8_t b) { switch (state) { case ST_IDLE: if (b 0xAA) state ST_HEADER1; break; case ST_HEADER1: state (b 0x55) ? ST_CMD : ST_IDLE; break; case ST_CMD: rxbuf[0] b; state ST_LEN_L; break; case ST_LEN_L: rxbuf[1] b; // 长度低字节 state ST_LEN_H; break; case ST_LEN_H: rxbuf[2] b; // 长度高字节 rxlen rxbuf[2] 8 | rxbuf[1]; rxidx 0; state (rxlen 0) ? ST_DATA : ST_CRC_L; break; case ST_DATA: rxbuf[3 rxidx] b; if (rxidx rxlen) state ST_CRC_L; break; case ST_CRC_L: rxbuf[3 rxlen] b; state ST_CRC_H; break; case ST_CRC_H: rxbuf[4 rxlen] b; // 一帧收齐交给协议处理函数 ProcessFrame(rxbuf, rxlen); state ST_IDLE; break; } }逻辑说明状态机每一帧只推进一步任何一步收到不符合预期的字节就回退到 ST_IDLE 重新等帧头这样不会死锁。帧头 0xAA 0x55 要两个字节都匹配才算同步只查第一个字节的话数据域里出现的 0xAA 就会导致误同步后续所有解析全部错位。长度字段在小端序下低字节在前收到低字节先存收高字节再拼出 rxlen这样解析逻辑和上位机构包逻辑一致省得两边各错一回。参数说明rxlen 在收到长度高字节后立刻计算如果上位机协议版本不匹配导致长度字段异常比如声明 0xFFFF缓冲区直接就溢出。量产固件里必须在 ST_DATA 阶段加一个边界判断rxidx 超过缓冲区上限就强制回 ST_IDLE宁可丢一帧等重传也不能让内存被越界写爆。缓冲区大小 1028 是 1024 数据3 头部2 CRC 的余量换数据域大小时这部分要同步改我曾经为省 RAM 把缓冲区缩到 516结果上位机一发 1024 的包解析直接崩。3.3 485 半双工方向切换是最大的坑485 和纯串口的本质区别是半双工同一根总线发的时候不能收收的时候不能发。控制收发方向的是收发器芯片上的 DE/RE 引脚用 SP3485 这类 3.3V 收发器时DE 拉高进入发送模式拉低回到接收模式。代码上就这点事但时序上一不留神就翻车#define RS485_TX_MODE 1 #define RS485_RX_MODE 0 void RS485_SetMode(uint8_t mode) { HAL_GPIO_WritePin(DE_GPIO_Port, DE_Pin, mode); if (mode RS485_TX_MODE) { // 收发器使能后需要短暂稳定时间否则第一字节会被截掉 for (volatile uint32_t i 0; i 200; i); } }逻辑说明拉高 DE 后芯片内部驱动器开始接管总线但如果立刻发数据前半段的电平可能还没稳定接收端会把这几个 bit 当成噪声丢掉。所以在 TX 模式加一个极短延时通常几十微秒就够具体值跟收发器的上电时间和 MCU 主频有关我这边 168MHz 主频下空转 200 次循环约 20 微秒实测稳定。另一个坑是发送完最后一字节后如果立刻拉低 DE 切回接收模式总线上最后一个停止位可能被截断对端会报帧错误所以发送函数末尾也要延时约一个字节时间再切回接收模式。一个字节在 115200 波特率下约 87 微秒延时取 100 微秒以上比较稳妥。485 还牵扯到终端电阻和偏置电阻的问题。总线空闲时如果 A/B 之间压差不够收发器输出的是不定态接收端会不停收到 0xFF 或者触发串口错误中断。常见做法A 线上拉、B 线下拉总线两端各放一个 120 欧终端电阻。我踩过一次总线空闲收一堆乱码的坑排查半天最后发现是漏了偏置电阻补上就安静了。现场没有示波器时可以先查串口错误标志有没有 ORFE帧错误或 NE噪声错误有就优先怀疑总线电平别一上来就怀疑解析代码。4. 升级流程落地擦除、写入、校验与状态机4.1 Flash 写操作先擦后写扇区边界对齐STM32F4 的 Flash 规则很简单写之前必须先擦擦除的最小单位是扇区写入的最小单位是字。也就是说想改一个字节也得先把整个扇区擦掉再重新写全扇区的内容。固件升级这种整包操作擦除用扇区粒度写入用包粒度。直接上代码uint8_t EraseAppSectors(void) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase {0}; erase.TypeErase FLASH_TYPEERASE_SECTORS; erase.Sector FLASH_SECTOR_4; // App 起始扇区 erase.NbSectors 8; // 擦到扇区 11 erase.VoltageRange FLASH_VOLTAGE_RANGE_3; uint32_t fail_sector 0; if (HAL_FLASHEx_Erase(erase, fail_sector) ! HAL_OK) { HAL_FLASH_Lock(); return 0; // 返回 0 表示擦除失败 } HAL_FLASH_Lock(); return 1; } uint8_t WriteAppData(uint32_t addr, uint8_t *buf, uint16_t len) { HAL_FLASH_Unlock(); uint32_t aligned addr ~0x03u; // 4 字节对齐 for (uint16_t i 0; i 4 len; i 4) { uint32_t word buf[i] | (buf[i1] 8) | (buf[i2] 16) | (buf[i3] 24); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, aligned i, word) ! HAL_OK) { HAL_FLASH_Lock(); return 0; } } HAL_FLASH_Lock(); return 1; }逻辑说明EraseAppSectors 把 App 区从 FLASH_SECTOR_4 开始连续擦 8 个扇区覆盖 0x08010000 到 0x080FFFFF。擦除是耗时操作128KB 的大扇区要几百毫秒这个阶段一定不能让看门狗超时否则擦到一半复位App 区只剩半截。WriteAppData 把缓冲区里的字节拼成 32 位字写入HAL_FLASH_Program 内部处理命令序列和 BUSY 等待但为了统一控制外层还是自己加锁解锁。拼字的顺序是小端序和 ARM Cortex-M 的内存模型一致别写成高位在前否则写进去的固件字节序是反的。参数说明len 必须是 4 的倍数否则最后一包不足 4 字节时写入函数会把对齐后多出来的字节也写进去。我的约定是上位机下发固件前先计算总长度补零成 4 的倍数再分包下位机收到的每包都是完整对齐的。addr 用 ~0x03u 强制对齐一次防止协议解析时地址参数出错造成非对齐写非对齐写在 F4 上直接返回参数错误。擦除接口里的 VoltageRange 也要注意芯片供电在 2.7V 以下时要选 FLASH_VOLTAGE_RANGE_2选成 RANGE_3 在低压场景下擦大扇区会失败这个问题在电池供电设备上特别容易出现。4.2 写回校验固件不是写进去就完事了Flash 写入失败不是罕见事供电波动、温度、VDD 掉电都可能让写入出错。所以升级流程里我强制留一道写回校验每包写入后读回来比对或者全片写完后读回算 CRC 对比。前者的好处是能立即定位错误包让上位机重发后者的好处是快但只能发现错误、不能定位。我的方案是两者结合uint16_t CalcCRC16(uint8_t *buf, uint32_t len) { uint16_t crc 0xFFFF; for (uint32_t i 0; i len; i) { crc ^ buf[i] 8; for (uint8_t j 0; j 8; j) { crc (crc 0x8000) ? (crc 1 ^ 0x1021) : (crc 1); } } return crc; } uint8_t VerifyAppImage(uint32_t app_addr, uint32_t image_len, uint16_t expect_crc) { uint16_t crc 0xFFFF; uint8_t tmp[64]; for (uint32_t offset 0; offset image_len; offset 64) { uint32_t rd_len (offset 64 image_len) ? 64 : (image_len - offset); for (uint32_t i 0; i rd_len; i) { tmp[i] *(volatile uint8_t *)(app_addr offset i); } crc UpdateCRC16(tmp, rd_len, crc); } return (crc expect_crc); }逻辑说明校验阶段上位机下发完整的固件 CRC 值下位机从 App 起始地址逐块读回 Flash 内容边读边算 CRC算完比对。这个函数在设计上是幂等的重复调用结果一致所以升级中断后重新连接可以直接从上一次结束的地址继续写这就是可断点续传的依据。读 Flash 用 volatile 指针强制每次从实际地址取数避免编译器把读操作优化成缓存值。参数说明image_len 是固件实际长度不是补零后的长度因为 CRC 只对有效字节计算。分块大小 64 字节是经验值太大会让主循环卡住太小则函数调用开销太大64 在 115200 波特率下刚好保持流畅。UpdateCRC16 是逐字节推进的增量版本和 CalcCRC16 配合使用时初值保持一致很关键一个用 0xFFFF 一个用 0x0000算出来的值对不上排查时最容易晕。4.3 完整升级时序从握手到跳转的六步把上面这些串起来一次升级就是六个状态步骤动作说明1握手上位机发固件大小和 CRC下位机返回版本确认2擦除按扇区擦除 App 区域3写入循环收包、写 Flash、回 ACK4回读校验下位机读回全片算 CRC 对比5清标志置为跳转态写 BOOT_MAGIC_RUN6复位跳转NVIC_SystemResetBootloader 读标志跳 App写 ACK 的设计值得单独说每包写入后回一个单字节 ACK上位机收到 ACK 才发下一包。这样天然构成流控下位机 Flash 写入期间不会积压接收缓冲。超时重传放上位机侧发出后 500ms 等不到 ACK 就重发连续三次失败则报错终止。我一般还会在上位机界面里把已发送包数和总包数实时显示出来现场排查进度一目了然。握手响应里带上固件大小上位机发现大小不对立刻中止避免擦完才发现固件尺寸超出 App 区。App 侧发起升级的代码也很简单核心就是写标志然后复位void AppTriggerUpgrade(void) { WriteBootFlag(BOOT_MAGIC_UPGRD); // 让 Bootloader 停在升级态 delay_ms(10); // 等标志写稳定 NVIC_SystemReset(); }逻辑说明把升级标志写成等待态而不是直接跳是为了让 Bootloader 在上电后统一接管升级流程。App 里自己跳转到 Bootloader 区域的代码是非法的行为F4 的 Flash 保护机制会把这种跨区域执行视为违规正确路径永远是写标志加复位。NVIC_SystemReset 是标准复位入口复位后 Bootloader 从 0x08000000 重新开始读标志发现不是跳转魔数就留在原地等上位机发握手帧。参数说明delay_ms(10) 是习惯性保险保证 Flash 标志位写入完成、缓存刷干净再复位。有些场景里 App 触发升级前需要保存当前业务参数到备份区那就把参数保存逻辑放在 WriteBootFlag 之前复位后 Bootloader 只处理固件传输参数部分完全不动。App 里别在中断服务函数里调这个升级函数复位前的中断残留会让跳转后的时序乱掉最好在主循环里处理。5. 避坑指南串口 OTA 最常见的五个翻车现场5.1 跳转后卡死死在启动文件里出不来现象Bootloader 串口打印跳转成功之后 App 没有任何反应串口无输出看门狗也不复位。原因最常见的是跳转前没关中断。Bootloader 里如果开着某个外设中断跳转瞬间一个中断进来PC 已经指向 App 代码但向量表还没切过去执行流跳到旧地址直接 HardFault。第二个常见原因是 HAL_RCC_DeInit 被注释掉DMA 和串口外设的状态全部带进 AppApp 初始化时和这些残留状态打架。解决跳转函数严格按 关中断 → 停 SysTick → RCC 复位 → 设 VTOR → 设 MSP → 跳转 的顺序执行其中设 VTOR最容易被漏漏了之后 App 里任何一个中断触发都会跑回 Bootloader 的向量表表现千奇百怪。如果已经卡死在 App 启动文件开头加一个 LED 翻转做指示能快速区分是没跳到 App 还是在 App 初始化途中挂的。5.2 485 发完数据总线一直占用现象一台上位机挂几十台 485 设备升级时下位机回 ACK 后总线上其他设备全部收不到轮询指令。原因发送完之后 DE 引脚没有及时拉低收发器一直处于发送模式持续驱动总线。或者拉低 DE 后立刻切接收最后一个停止位被截断对端认为总线忙。解决发送函数末尾先拉低 DE再延时约一个字节时间确保停止位完整发出再切回接收模式。严谨的写法是发送完把串口也切回接收中断使能双保险。用示波器量 A/B 线看发送后的空闲电平正常应该是 A 高 B 低如果 A/B 之间持续有信号跳变基本就是 DE 没释放。没有示波器时看总线上其他设备的表现也能判断其他设备通信正常唯独刚发完数据的那台占着总线多半是方向切换时序问题。5.3 Flash 写入失败读回全是 0xFF现象升级过程中 ACK 正常但校验阶段 CRC 对不上读回 Flash 全是 0xFF。原因八成是没擦除直接写了。F4 的 Flash 写入只能把 1 变 0擦除动作把扇区全部置 1写入再把需要的位变 0。如果扇区本来就是新的里面全是 0xFF写入前后看不出毛病但如果上次升级留下的旧数据还在里面写入就报 Program Error。解决每次升级都先执行完整擦除再进入写入循环。擦除失败时 HAL_FLASHEx_Erase 会返回错误码fail_sector 参数会指出具体卡在哪个扇区排查时先读这个返回值别只看函数返回 OK 就继续。另一个隐蔽原因擦除电压范围参数配错F4 工作电压低于 2.7V 时要选 FLASH_VOLTAGE_RANGE_2配成 RANGE_3 写大扇区会失败这个坑在电池供电的便携设备上概率很高。5.4 分包错乱升级到一半 CRC 频繁失败现象前 100 包正常第 101 包开始 CRC 连续报错重发也报错。原因中断里做了 Flash 写入或者耗时的 CRC 计算。有人把整包处理和 Flash 写入都塞进串口中断一个扇区擦除耗时几百毫秒这段时间串口来的字节全部进不了缓冲数据流就断了一截。重发的包和断点之间对不上CRC 自然一直错。解决把中断函数里只保留字节进环形队列这唯一动作其余全部搬进主循环。升级期间关掉其他无关中断尤其是定时器中断里做延时判断的逻辑避免主循环被其他任务抢占导致串口丢字节。接收缓冲用环形队列或者双缓冲生产环境建议把环形队列缓冲区开大一点比如 2048 字节给主循环留出处理余量我见过缓冲区只有 128 字节的工程在 115200 波特率下主循环稍慢就丢包。5.5 升级断电设备变砖现象升级到一半现场断电重新上电后设备没有任何反应Bootloader 也不打印跟砖一样。原因擦除动作已经把旧固件抹掉新固件才写了一半断电后 Flash 里既没有完整旧固件也没有完整新固件。Bootloader 如果默认跳 App跳到一个残缺的镜像就死了如果默认等升级设备就一直停在升级等待态看起来也像砖。解决第一道防线是 Bootloader 启动时先检测 App 区第一个扇区的有效性比如读复位向量是否为 0xFFFFFFFF 或者是否在合法 Flash 范围内是就自动进入升级等待而不是跳转。第二道防线是 App 区划出双备份新固件先写备份区校验通过再整体拷贝到运行区拷贝是逐扇区操作断电最多丢当前扇区重新上电可以从备份区恢复。如果只做单区升级至少要在升级前把正在升级标志写进 FlashBootloader 上电看到该标志就强制等待升级而不是尝试跳转这样能保住 Bootloader 侧的最后底线。6. 升级后的强校验一次断电事故教会我的验证流程升级稳定跑通只是第一步真正让我改掉写完就跑习惯的是一次升级断电带来的教训。那次在客户现场升级一切正常校验通过跳转成功设备跑起来了。结果第二天客户说设备没响应过去一看固件运行期间某个外设初始化出错直接卡死在初始化流程里而 Bootloader 因为校验通过已经标记了跳转态上电就跳进这个有问题的 App连重新升级的机会都没有。从那以后我给所有 OTA 方案都加了App 自检回跳机制。核心逻辑是Bootloader 跳转前不立即清跳转标志而是把标志区的值保留为待确认状态App 启动后跑一段自检确认关键外设和业务模块正常后主动把标志改写成跳转确认态。如果 App 在 3 秒内没有确认或者自检失败它就主动复位回 BootloaderBootloader 看到标志还不是确认态就停留在升级等待模式等上位机重新下发固件。运行期间的完整性由独立看门狗 IWDG 兜底App 主循环正常时定期喂狗异常时复位回 Bootloader形成一个闭环// App 侧自检失败则请求回跳 void AppSelfCheck(void) { if (CheckPeripherals() ! 0) { WriteBootFlag(BOOT_MAGIC_UPGRD); // 明确让 Bootloader 等待升级 NVIC_SystemReset(); } WriteBootFlag(BOOT_MAGIC_RUN); // 自检通过确认跳转态 }验证方法是升级完成后强制做一次断电测试写入 App 后不立即复位直接断电再上电确认 Bootloader 能正确识别 App 区的完整性能进就进进不了就停在升级态。这个测试我每次升级流程改动后都强制走一遍从那以后先断电再验收就成了我 OTA 方案里雷打不动的一条验收项。思路归思路真正要落到自己的工程里还得把分区表、协议帧和跳转时序按实际硬件调一遍知道这些坑在哪你手里的 STM32F4 串口 OTA 和 485 OTA 项目至少能少走我当年一半的弯路希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站