news 2026/9/8 11:29:24

STM32F103 AB双分区OTA升级:基于FreeModbus与标准库的可靠实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 AB双分区OTA升级:基于FreeModbus与标准库的可靠实现

STM32F103做OTA已经不算新鲜事,但要把OTA做成A/B双分区、带完整回滚机制、还能通过Modbus RTU串口稳定传输固件,这组合确实少见。我这次是把FreeModbus v1.6移植到标准库v3.5环境下,配合自定义Bootloader,实现了基于RS232串口的A/B分区在线升级,整个流程从零开始复现了一遍,踩了不少坑,也沉淀出一些能直接抄作业的经验。这篇就把完整思路、分区设计、关键代码和排障过程都摊开讲清楚,给想在STM32F103上做可靠OTA的小伙伴一条能走通的路。

1. 整体设计与思路拆解

1.1 AB双分区方案为什么比传统IAP更可靠

传统IAP方案基本都是单分区:APP区就一块,新固件直接覆盖写入,写坏了就只能回Bootloader干瞪眼,要是Bootloader里没留恢复逻辑,设备直接变砖。我最初也图省事用过这种方案,直到有一次升级过程中串口被误拔,固件写到一半断掉,整台设备彻底起不来,只能用烧录器重新刷,从那以后我就下定决心换A/B双分区。

A/B方案的核心逻辑很简单:Flash里同时存在两个APP分区,一个active(当前运行),一个update(待升级)。新固件只写入update分区,写入并校验通过后才切换启动标志,让Bootloader下次启动时跳转到新分区;如果新固件跑不起来,或者健康检查失败,Bootloader就自动回滚到旧的active分区。整个过程不破坏当前正在运行的程序,任何异常情况下设备都能恢复到可用状态,这在工业现场是刚需——设备一旦下线,损失的可不止是时间。

A/B双分区本质上是给固件升级买了一份“后悔药”,用Flash容量换可靠性。对STM32F103来说,Flash通常有256KB-512KB,如果产品固件本身就小,比如30KB以内,完全有条件做双分区。我在F103ZET6上验证过,512KB Flash,划分256KB给APP区,两个分区各128KB,Bootloader占8KB,剩余空间做参数存储,这套布局跑得很稳。

1.2 标准库v3.5与FreeModbus v1.6的搭配原因

固件工程师圈子里长期有“标准库还是HAL库”之争,但在这个项目里我坚定选标准库v3.5,原因就一个——资源占用更低、中断响应更可控。HAL库封装层次深,尤其在高频中断里调试Modbus超时计时,总是隔着一层,有时出了诡异问题你都不知道是HAL层触发的还是应用层触发的。标准库直接操作寄存器,代码量小,逻辑透明,特别适合Bootloader和通信协议栈这种对时序敏感的场景。

FreeModbus v1.6是经典中的经典,虽然官方已停止维护,但它的协议处理框架非常清晰,移植起来不费劲。它天然支持Modbus RTU和ASCII模式,RTU模式下一帧报文必须有3.5字符时间的静默间隔,这正好和串口升级的帧校验需求匹配——每帧升级数据用Modbus功能码封装,天然具备CRC16校验,不用自己再写一套传输协议。

我甚至建议你把这两个组合想成“轻量级工业通信协议栈”,它既能满足日常设备数据采集(读寄存器、写线圈),又能承担固件传输通道(自定义功能码),一套代码解决两个核心需求。

1.3 空间布局与存储映射的取舍

Flash划分是整个AB OTA方案的地基,一步算错,后面全乱。我的STM32F103ZET6是512KB Flash,起始地址0x08000000,扇区大小从主Flash后半段开始才是128KB大扇区,前4个扇区是16KB(其中第一个是16KB)。实际分配见下表:

区域起始地址大小用途
Bootloader0x0800000024KB引导程序、升级入口
参数区0x080060008KB分区状态、回滚计数
APP-A0x08008000128KB出厂固件/当前运行区
APP-B0x08028000128KB待升级固件区
备份区0x08048000剩余预留、字库等

这里有个取舍要点,BOOT区没卡在16KB边界上,而是留了24KB,因为Bootloader里除了跳转逻辑还要集成Modbus从站固件接收函数,这部分代码量比想象中大,留足余量能避免后期加功能没地方放的窘境。

APP_A和APP_B各128KB,对于大多数F103应用场景(电机控制、传感器采集、简单HMI)完全够用,你编译出来的hex/bin文件如果超过128KB,那基本上不是F103该干的活了,建议直接换芯片。

2. 环境准备与工具链选型

2.1 标准库v3.5工程搭建的细节坑

STM32F103标准库老项目很多,但如果你是从零搭,有几个文件必须手动改配置,不然轻则编译告警,重则链接失败:

  • stm32f10x.h里面需要根据芯片型号打开对应宏定义,比如STM32F10X_HD
  • system_stm32f10x.c里的SystemInit()函数会配置时钟,F103最高72MHz,外部晶振如果是8MHz,这段代码能自动倍频;
  • 启动文件startup_stm32f10x_hd.s必须对应选对,HD表示高密度Flash芯片,ZET6就是HD系列。

工程目录我推荐这样组织:USER(main、中断处理)、CORE(启动文件和内核头文件)、SYSTEM(delay、uart、gpio封装)、HARDWARE(外设驱动,比如Flash擦写、按键检测)、FREEMODBUS(协议栈源码和port层)、APP(业务逻辑和升级状态机)。

2.2 FreeModbus v1.6移植的核心动作

FreeModbus的移植重点不是把源码加进工程,而是把port.cport.h和串口中断处理对接好。核心就三件事:

第一,串口字节收发中断。RTU模式下每收到一个字节都要喂给eMBPortRxISR,每发送完一个字节触发eMBPortTxISR。F103的USART2我用了接收和发送两个中断源,接收中断里直接把字节塞给协议栈,不经过任何缓冲队列——FreeModbus内部自己有缓冲区,别画蛇添足。

第二,定时器。Modbus RTU的3.5字符时间间隔要用一个定时器来测量。我的做法是开TIM4作为协议栈时钟,配置成1ms中断,通过vMBPortSetTimerprvvTIMERExpiredISR回调实现。注意这里的1ms不是固定的,要按波特率推算,9600波特率下3.5字符时间约4ms,115200波特率下约0.3ms,我用1ms粒度在115200下实测没问题,但如果你追求极限性能,可以改成0.1ms粒度的定时器。

第三,错误处理。vMBPortEventPost里的事件类型有EV_READYEV_FRAME_RECEIVEDEV_FRAME_SENTEV_ERROR,排查通信问题时要重点看事件循环里拿到的到底是哪一类事件,很多莫名奇妙的超时故障其实都是EV_ERROR被吞了。

2.3 辅助工具:OTA提取器与文件服务器选型

热词里出现“ota提取器”和“nginx”,在ARM Linux设备上很常见,但F103这种纯MCU环境也可以借这个思路:PC端生成的新固件bin文件,最好用一个脚本工具做预处理——加头、算校验、拆分帧,我管这个叫“OTA提取器”的MCU版本。

我自己写的是个Python脚本:读入app.bin,在最前面加16字节自定义头(魔数+版本号+固件长度+CRC32校验+分区目标),然后按每帧256字节切片,每帧再套一层帧序号和CRC16,最后输出成可下载的二进制升级包。这个预处理动作相当于PC端做了“打包和加密”,让MCU端解析逻辑尽量简化。

nginx在MCU方案里不是必需品,但如果你开发的是带WiFi/以太网的接入设备,固件放nginx上、设备通过HTTP下载升级是常见玩法。F103本身不带以太网控制器,但通过SPI接口挂ENC28J60可以实现,这种情况下Bootloader里就得多集成一个轻量级TCP/IP协议栈(比如lwIP),复杂度上升一个量级。我这篇的重点是串口升级,所以nginx场景只在思路层面参考,实际验证用的是串口传输。

3. 核心细节解析与实操要点

3.1 分区状态设计与回滚机制

分区状态是整个AB方案的大脑,我设计了一个8字节的状态结构体,存放在固定偏移的参数区Flash:

typedef struct { uint32_t magic; // 0xA5A5A5A5 uint8_t active_slot; // 0 = A区, 1 = B区 uint8_t boot_count; // 启动计数 uint8_t max_boot_attempts; // 最大尝试次数,建议3 uint8_t reserved01; uint32_t upgrade_status; // 升级进行中/完成/失败 } ota_state_t;

这个结构体每次更新就写到参数区Flash的新地址(利用Flash擦写寿命和磨损均衡),核心逻辑是:Bootloader每次启动时boot_count++,正常APP运行后上报“运行正常”标志,将boot_count清零;如果启动后超过max_boot_attempts次都没有上报“运行正常”,则判定新固件异常,触发回滚。

回滚的流程我在第4部分详细讲,这里先记一个原则:A/B分区升级不是“切过去了就完事”,而是“确认新固件能活着才切过去”。你可以在APP里放一个心跳标志区,APP运行后5秒内写入健康标志,Bootloader下次启动时看到这个标志才认为切换成功,否则继续用旧分区。

3.2 上位机下发AB包的会话流程

升级过程不是一股脑把整个bin文件往串口灌,而是按“会话”方式分组进行,状态机如下:

  1. 空闲状态——设备正常执行业务逻辑,Modbus寄存器区里有升级控制寄存器(比如地址0x5000),上位机写入特定值启动升级会话;
  2. 升级请求——上位机下发固件头(16字节),包含魔数和校验和数据,设备校验通过后回复“OK”,进入“接收数据”状态;
  3. 数据帧传输——固件按512字节一帧切割,每帧包含帧序号(2字节)+ 数据长度(2字节)+ 数据(N字节)+ CRC16校验。设备写一帧回一帧确认,有确认才发下一帧,这样即便中途串口异常,也能及时重传;
  4. 结束确认——最后一帧传输完成后,设备对整个缓冲区再做一次全量CRC32校验,和固件头里记录的比对一致,才把upgrade_status置为“升级完成”;
  5. 重启切换——设备软复位,Bootloader检查upgrade_status后更新启动分区指针,跳转到新固件。

这套会话设计借鉴了Modbus RTU的主从确认思想,跟Modbus协议天然亲和。在FreeModbus里,你可以用自定义功能码0x66、0x67、0x68分别对应“升级请求”、“数据帧写入”、“升级结束确认”,不需要额外定义协议结构,直接用寄存器读写也能完成,但自定义功能码接收大块数据效率更高。

3.3 Flash擦写操作的关键禁区

STM32F103的Flash擦写有几个绕不开的坑,第一是“擦除期间不能跑Flash里的代码”。F103的Flash控制器在执行擦除操作时,总线会阻塞,如果你的擦除代码和中断向量表都在同一个Flash分区里,一旦擦除开始,中断响应会卡住,串口数据直接丢。所以我的升级数据接收区放在RAM里,攒够一页(1KB或2KB)再一次性写入Flash,避免频繁的小块擦写。

第二是“写Flash前必须先擦除,且按扇区擦除”。F103的最小擦除单位是扇区(16KB/64KB/128KB),不是按字节擦的。这意味着你没法原地改一个字节。升级过程中我选按128字节写(Flash编程一次最多可以写2字节,但批量写更高效),写入前必须先确保目标扇区已擦除。

第三是“中断保护”。擦写Flash前建议先__disable_irq(),擦写完再__enable_irq()。原因很简单:擦写过程中CPU被Flash控制器占用,中断一旦触发就可能会死等,而且如果中断服务函数恰好调用了Flash操作,会导致硬件错误(HardFault)。我的Bootloader代码里封装了一个flash_erase_region函数,内部统一做了关中断保护和错误标志检查。

3.4 加签验签:避免非法固件烧进去

热词里多次出现“加签验签”,这个在汽车嵌入式OTA里是强制要求,在工业设备上也越来越被重视。F103的资源做非对称加密(RSA/ECC)有点吃力,但做对称加密(AES-128-CBC)或者是HMAC-SHA256的校验完全可行。

我在上位机打包时用固定密钥对固件算了一个HMAC-SHA256摘要,放到固件头里;MCU接收完固件后,用同样的密钥对收到的数据算摘要,比对一致才认为固件合法。虽然对称加密的密钥在固件里逆向后会暴露,但对防误升级和防篡改已经够用了。如果你有更高的安全需求,可以把密钥放在MCU的Option Bytes区,并开启Flash读保护(RDP Level 1),这样即使固件被读出来也拿不到密钥。

注意,加签验签不是“升级流程的必需项”,但如果你做的是医疗、电力、安防类设备,没有验签环节基本没法过认证。验签算法务必放Bootloader里,不要放APP里,否则APP被替换了,验签就形同虚设。

3.5 编译环境与map文件定位

做OTA必须掌握从app.axf里提取bin文件的正确姿势,以及阅读map文件定位问题的能力。我在工程里用的编译工具链是Keil MDK,生成bin的命令是:

fromelf --bin --output ./output/app.bin ./build/app.axf

在Keil的User选项卡里,After Build/Rebuild处加上这行命令,每次编译都会自动生成bin文件。

map文件排查问题的思路:升级后程序跑飞,先看map文件里__initial_spReset_Handler的地址是否指向了正确的加载区;再做启动跳转前,对比编译器的分散加载文件(sct文件)和实际烧写地址是否一致。F103的向量表偏移通过SCB->VTOR设置,APP工程里的SystemInit会尝试重新映射向量表,Bootloader跳转前还需要手动关闭所有外设中断。

4. 实操过程与核心环节实现

4.1 从零搭建Bootloader工程

Bootloader部分我分四步搭建,每一步都有对应的验证方法:

**第一步,建立最小跳转框架。**工程只包含系统时钟初始化、LED闪烁、串口打印,然后实现一个跳转函数:

typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t jump_addr = *(volatile uint32_t *)(app_addr + 4); // SP pFunction jump = (pFunction)*(volatile uint32_t *)(app_addr); // Reset_Handler // 跳转前关闭所有中断,复位外设状态 __disable_irq(); for (int i = 0; i < 8; i++) { NVIC->ICER[i] = 0xFFFFFFFF; } __set_MSP(jump_addr); // 设置主栈指针 jump(); // 跳到APP入口 }

注意__disable_irq()之后进APP时要重新使能中断,所以APP的SystemInit通常都是在main函数开头才调用,这样能保证中断向量表重定向完成后才开中断。

**第二步,加入串口接收升级命令。**Bootloader里串口只做一件事——接收升级命令帧。我用的是USART2,波特率115200,8N1。中断配置好后,检测到一帧合法升级请求(魔数0xA5A5A5A5 + 目标分区标识),立刻置一个标志位,主循环进入升级模式。不加超时判断的话,设备会卡死在等待升级状态,所以升级模式下要加10秒无数据自动跳转到原APP的保护逻辑。

**第三步,实现Flash擦写函数。**封装擦除、编程、读取三个基本函数。擦除时注意F103的Flash扇区编号不是连续的:主存储区起始地址0x08000000,前四个扇区大小16KB,之后是64KB扇区,最后两个是128KB扇区。分区起始地址必须落在扇区边界上,否则会影响相邻分区数据。

**第四步,集成FreeModbus作为升级通道。**Bootloader里只需要保留Modbus RTU从站模式,地址固定为1(或通过拨码开关设置),注册一个“升级数据写入”功能码。整个Bootloader的思路就一句话:“能启动APP?启动APP。收到升级包?写升级包。”

4.2 APP工程的修改与中断向量重映射

APP工程不只是在原工程里加几行代码,至少需要改以下三处:

第一,分散加载文件(.sct)。默认情况下Keil给APP分配的起始地址是0x08000000,我们必须改成对应分区地址,比如APP-A从0x08008000开始:

LR_IROM1 0x08008000 0x00020000 { ER_IROM1 0x08008000 0x00020000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (+RW +ZI) } }

如果你用的是MDK图形化界面,在Options for Target -> Target页签里把IROM1的Start和Size改成对应分区即可,但要记得把STM32F10X_HD宏里的Flash大小改成256KB(如果总共是512KB但APP区只分到128KB,其实不用改宏,宏对应的是芯片实际Flash)。

第二,向量表重映射。在main函数一开始执行:

SCB->VTOR = FLASH_BASE | 0x8000; // APP-A分区偏移

如果不做这步,任何中断(包括SysTick、串口中断)触发时都会跳到Bootloader的向量表去执行,跑飞是必然的。F103支持VTOR寄存器偏移,但前提是系统时钟已经在SystemInit里初始化完成。

第三,固件自身版本号和健康上报。APP里定义一个只读的固件信息结构体,烧录地址固定放在分区首地址+固定偏移处。APP运行后延时5秒(给Bootloader留出判断时间),再往参数区写入“运行正常”标志。这个上报动作可以复用一个普通Modbus寄存器地址,也可以直接往Flash写。

4.3 FreeModbus移植到标准库的关键代码展示

FreeModbus的port.c里需要实现几个底层接口,我抽核心片段:

// 串口发送完成回调 void vMBPortTxISR(void) { // 清除发送完成标志,触发下一个字节发送 USART_ClearITPendingBit(USART2, USART_IT_TC); xMBPortEventPost(EV_FRAME_SENT); } // 串口接收中断 void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { uint8_t byte = USART_ReceiveData(USART2); eMBPortRxISR(byte); } if (USART_GetITStatus(USART2, USART_IT_TC) != RESET) { vMBPortTxISR(); } } // 定时器超时中断(判断3.5字符时间) void TIM4_IRQHandler(void) { if (TIM_GetITStatus(TIM4, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM4, TIM_IT_Update); prvvTIMERExpiredISR(); } }

FreeModbus协议栈初始化时要调用eMBInit(MB_RTU, 0x01, 0, 115200, MB_PAR_EVEN),其中0x01是从机地址,0是端口(串口编号从0开始),波特率115200,校验方式偶校验。Modbus RTU默认用偶校验,上位机配置时不能选错,否则一个字节都通不了。

4.4 上位机发送AB包与分帧传输实测

有了上位机工具,传输才有意义。我用Python写了一个简单的上位机脚本(也推荐你用开源串口调试助手配合“OTA提取器”自动分包),核心逻辑是:先发送“升级请求帧”,然后循环发送数据帧,最后发送“结束帧”。实测数据是100KB固件在115200波特率下传输,一次升级耗时约10秒左右。

重点说下分帧传输的容错机制。每帧大小我选256字节,包含2字节帧序号,上位机发完一帧后等待设备回复(一个字节的ACK/NAK),等待时间设500ms,超时重发。设备端收到帧后先查帧序号是否连续、CRC16是否正确,不合法就直接丢弃等重发。

这里有个细节:设备端对“重复帧”的处理一定要健壮。比如上位机超时重发了,但设备其实已经写进Flash了,这时设备如果再次写入就会重复,所以我用帧序号+当前写入偏移做去重,当收到偏移等于已写入位置时,直接返回ACK,不重复写Flash。

4.5 回滚机制模拟:从“升级失败”到“自动复原”

回滚机制不是纸上谈兵,实测验证一定要做。我模拟的场景是:把新固件里人为制造一个死循环,然后升级,观察设备是否能自动回到旧固件。

Bootloader判断回滚的完整代码逻辑:

void check_ota_state(void) { ota_state_t state = read_ota_state(); if (state.upgrade_status == UPGRADE_COMPLETED) { // 有一种情况:升级完成但还未来得及上报运行正常 // 这里给一次尝试机会 if (state.boot_count < state.max_boot_attempts) { state.boot_count++; write_ota_state(state); jump_to_app(update_slot); } else { // 回滚到旧分区 state.active_slot = old_slot; state.upgrade_status = UPGRADE_ROLLBACK; state.boot_count = 0; write_ota_state(state); jump_to_app(old_slot); } } else { jump_to_app(active_slot); } }

这个状态机有几个细节容易踩坑:一是boot_count清零的时机,不能放在APP刚启动时,必须放在业务逻辑确认无误后,我放在定时任务跑通后;二是回滚到旧分区后upgrade_status不能立即清空,需要保留“发生过回滚”的痕迹,方便上位机查询诊断;三是回滚动作要记录次数,连续多次回滚说明当前固件质量堪忧,必须强制进入Bootloader等待人工处理。

实测中我用一个LED颜色区分当前分区:绿色常亮在A区,蓝色常亮在B区,回滚时LED红绿交替闪烁,一眼就能看出系统处于什么状态。调试时这个可视反馈极大提升了排障效率。

5. 常见问题与排查技巧实录

5.1 跳转APP后跑飞

这是AB_OTA最常遇到的问题。排查路径我固定按三步走:

  • 第一步看栈指针。跳转前打印*(volatile uint32_t*)(app_addr)*(volatile uint32_t*)(app_addr + 4),确认不是0xFFFFFFFF。如果全是F,表示APP区根本没烧进去,或者烧录地址不对。
  • 第二步看VTOR。APP里没设置SCB->VTOR会导致中断向量表还在BOOT地址,一旦串口中断触发就会跑飞。建议在跳转前打一个延时,让APP先跑起来几秒,判断是“直接跑飞”还是“进中断后跑飞”。
  • 第三步看优化级别。如果Code optimization开到了-O3,跳转相关代码可能被编译器优化掉,尤其是jump()前的__set_MSP,建议关优化或设置__attribute__((optimize("O0")))

5.2 FreeModbus串口升级时丢包严重

在115200波特率下出现丢包,90%是因为Flash擦除期间串口数据没地方放。F103的USART接收寄存器只有1字节深,Flash擦除一个128KB扇区需要上百毫秒,这期间来的数据全被硬件丢弃。解决办法有两个:一是升级数据先全部收进RAM,攒够一整块(比如4KB)再统一擦写Flash;二是用DMA接收,在RAM里开环形缓冲区。我推荐DMA方式,代码量增加不多,但可靠性提升巨大。

DMA接收时要注意缓冲区溢出判断,FreeModbus的eMBPortRxISR调用时机是在DMA半满/全满中断和IDLE中断里,IDLE中断用来判断一帧Modbus报文结束,这是RTU模式最优雅的实现方式。

5.3 升级后回滚失败,系统反复重启

设备反复重启是最让人崩溃的现象。我的实测经验是,这类问题九成出在“健康上报”和“启动计数”的时序上。Bootloader判定“新固件启动失败”的依据是启动计数超限,但APP如果延迟上报健康标志,Bootloader在下一次重启时就会误判,导致反复重启。

排查方法是:

  • max_boot_attempts调大(从3调到10),观察是否还重启;
  • 把健康上报提前到APP初始化最前面(但注意不能放在中断向量重映射之前);
  • 在参数区加一个调试打印寄存器,记录最近一次复位的原因和启动计数值。

5.4 固件版本回退与版本校验

回滚机制不是简单地“跳回去”,它必须配合版本管理。我的做法是:Bootloader在升级前把当前运行固件版本号存到参数区,升级完成后APP上报新版本号,如果Bootloader发现新版本号低于旧版本号(或者异常的低),则直接忽略这次升级。这能防止“误把老版本固件当新版本刷进去”。

上位机打包工具里也要约束:新固件版本号必须大于等于设备当前版本号,且同一版本号不允许重复升级。因为AB切换是有代价的——每次切换,整个分区都要重新擦写、搬运,Flash寿命也是要钱的。

5.5 常见问题速查表

现象可能原因排查方法
跳转后白屏/无响应向量表未重映射检查SCB->VTOR设置
串口收不到ACKModbus应答超时排查3.5字符时间、波特率校验位
升级到一半停止Flash擦除导致接收中断卡死改用DMA接收或环形缓冲
新固件运行5秒后重启健康上报没写到Flash检查APP里上报标志写入位置
升级成功后无法切回旧版回滚计数未置零检查boot_count清零逻辑
升级包校验失败固件头信息解析错误检查魔数、CRC32比对字节序

6. 方案扩展与更多应用场景

6.1 从串口升级到HTTP+OTA的演进思路

如果你以后要做带WiFi/以太网的接入设备,AB_OTA这套框架可以直接平移。区别主要在传输层:从Modbus RTU换成HTTP,从串口中断换成lwIP协议栈。分区布局、状态管理、回滚机制都不用重新设计,只需把“数据帧接收”抽象成一个统一接口——数据从哪来不重要,重要的是数据怎么存、怎么验、怎么切换。

nginx作为固件服务器,核心配置就是开一个目录存放升级包,设备通过HTTP下载时能断点续传最好,可以配合Content-Range头实现。MCU端lwIP本身占用RAM较多,F103的内部SRAM是64KB,跑lwIP + mbedTLS(TLS加密)会非常紧张,建议评估带外部SRAM的型号或者直接换F4系列。

6.2 FreeModbus在车载、工业场景中的特殊要求

车载嵌入式OTA、电力保护设备在线升级等场景对安全要求更高,我建议在AB_OTA基础上至少增加三层防护:

第一层,升级包加密。AES-128或AES-256,密钥放进单独安全芯片或MCU Option Bytes区域,防止固件被提取后直接逆向分析; 第二层,通信通道安全。如果走网络,用TLS加密;如果走串口,至少增加白名单地址和升级时间窗口限制; 第三层,升级审计日志。每次升级的结果、发起方信息、失败原因都必须记录在Flash日志区,方便后期追责和定位问题。

6.3 不依赖Bootloader的AB实现可能性

还有一个思路值得尝试:不使用独立Bootloader,而是在APP内实现“自举升级”。即APP运行状态下,将新固件写入另一个分区,写入完成后设置启动标志并软复位,然后由APP开头的一段代码负责判断启动标志和跳转。这种方式省掉了Bootloader的维护,但风险是APP本身崩溃后就无法升级,只适合对可靠性要求不高的消费类产品。

如果你的芯片Flash足够大(比如512KB以上),甚至可以做三个分区——增加一个出厂恢复分区。这个分区放一个最小可运行固件,支持基础IO操作和串口升级入口,一旦两个APP分区都损坏,Bootloader还能拉起出厂固件救场。这就是汽车行业常说的“最小启动镜像”概念的简化版。

写在最后

这套STM32F103 AB_OTA方案我从立项到稳定跑通,前后用了近两个星期,大部分时间都花在调试“看似代码对但就是不工作”的问题上。回头总结,最有价值的经验就几条:一是分区规划和状态机设计一定要在写代码前定死,后端改动牵一发动全身;二是所有通信交互都要有超时和重试机制,嵌入式升级99%的故障都是时序问题;三是调试手段要可视化,LED、串口打印、参数区状态寄存器三管齐下,能帮你把定位时间从几小时缩短到几分钟。

最后再分享一个小技巧:把Bootloader和APP的串口打印信息加上不同前缀,比如BOOT:和APP:,这样抓串口日志时一眼就能看出当前执行到哪一部分。别看这个小改动不起眼,它帮我省下的调试时间相当可观。如果你也在折腾F103或者其他Cortex-M3芯片的OTA方案,这套思路可以照着搭,遇到具体问题再来交流,坑踩过了,路就平了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 11:29:10

PLC恒压供水系统5泵设计:PID控制与变频器切换全解析

1. 泵组规模背后的容量逻辑&#xff1a;为什么偏偏是5泵高层楼宇恒压供水这个领域&#xff0c;我接触过不少项目&#xff0c;坦白讲3泵、4泵很常见&#xff0c;5泵属于偏复杂的系统了。这个"No.928 西门子S7-200 PLC高层楼宇恒压供水系统变频供水5泵五泵"里的5泵方案…

作者头像 李华
网站建设 2026/9/8 11:28:44

ComfyUI节点式AI绘画:从零搭建到高效工作流实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:28:13

DeepSeek Harness实战:将DeepSeek接入Codex CLI与Claude Code的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 11:27:46

OpenCode实战:开源终端AI编程助手的安装、配置与效率秘籍

这段时间 AI 编程助手的圈子是真的热闹&#xff0c;codex 刚火完&#xff0c;claude code 又来了&#xff0c;然后 opencode 这个名字开始隔三差五出现在我时间线上。我本来没太当回事&#xff0c;直到身边好几个做后端和全栈的朋友同时推荐&#xff0c;才抱着试试看的态度装了…

作者头像 李华
网站建设 2026/9/8 11:27:05

基于鲁棒优化的风光并网备用容量配置Matlab实现

都说风光发电不好调度&#xff0c;难就难在“看天吃饭”这四个字上。你早上预测的出力曲线&#xff0c;可能中午就被一片云打乱&#xff0c;下午风一停&#xff0c;整个运行计划就得推翻重来。这篇要聊的项目&#xff0c;就是用鲁棒优化把这笔“看天吃饭”的账算清楚&#xff1…

作者头像 李华
网站建设 2026/9/8 11:26:35

嵌入式全栈安全体系:纵深防御、安全引导与应急响应实战

1. 为什么嵌入式安全不能靠“单点防御”做嵌入式开发这些年&#xff0c;我见过太多团队把安全当成最后一个环节来补。硬件设计完了、系统移植好了、驱动调通了、应用写完了&#xff0c;然后才想起来问一句&#xff1a;“我们这个产品需不需要做安全&#xff1f;”这时候再谈安全…

作者头像 李华