尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32F103 AB分区OTA升级方案详解:Bootloader设计、掉电保护与断点续传实现
STM32F103都发布十几年了按说早该进教科书了但这两年找我聊AB分区OTA的人反而特别多。原因也不难猜一方面F103成本低、货源稳大批量产品还在跑另一方面现在客户越来越懂行了动不动就问支持远程升级吗升级失败会变砖吗传统的单分区IAP方案在可靠性上确实有些跟不上。我这次就把自己从零复现STM32F103 AB分区OTA的完整过程整理出来从分区设计、Bootloader编写、固件打包到断点续传和掉电保护每一步都给出可直接抄作业的方案。1. AB分区OTA的整体设计思路1.1 为什么F103这种老MCU也需要AB分区很多朋友一开始会问F103片内Flash才512KBRAM才64KB资源这么紧搞AB分区是不是太奢侈了这个问题问得很实在。传统单分区IAP的思路是Bootloader接收完整固件包先全部写到备份区校验通过后再整体拷贝到App区最后跳转。流程上也算严谨但有一个绕不开的隐患如果固件拷贝到一半掉电或者新固件本身存在启动即崩溃的致命Bug设备就直接变砖了只能开盖用烧录器恢复。AB分区的思路则完全不同Flash里同时存在A槽和B槽两份固件Bootloader永远从当前有效的那个槽启动。升级时新固件写入非活动槽完整校验通过后只需把活动槽标志切换一下下次开机就从新槽启动。如果新固件启动失败Bootloader检测到启动标志未被清除自动回滚到旧槽。整个过程不涉及整片拷贝从根上解决了掉电变砖和启动崩溃的问题。F103虽然老但它的Flash是128KB一页按32KB、64KB这样的粒度切分非常规整。以最常见的256KBFlash型号如STM32F103RCT6为例完全可以划出两个96KB的槽位加一个8KB的Bootloader区剩下56KB做参数存储和OTA缓存。这个空间规划对绝大多数应用来说是够用的代价完全可控。1.2 AB分区的核心机制与正常启动链路先讲清楚AB分区最核心的三个概念活动槽、非活动槽和槽位标志。活动槽Active Slot当前正在运行的固件所在分区Bootloader每次启动都会跳转到这个槽位。非活动槽Inactive Slot用于接收新固件的分区平时不参与启动。槽位标志Slot Metadata存放在独立的Flash扇区记录哪个槽是当前活动槽、哪个槽待升级、以及每个槽的固件状态有效/无效/待提交。我们在实际项目里把槽位标志放在Bootloader末尾单独划出的一个扇区2KB专门存放这类关键状态信息。硬件复位后Bootloader第一件事就是读槽位标志判断三个问题现在活动槽是A还是B非活动槽有没有完整的待升级固件当前活动槽固件有没有正常跑起来过。启动链路分两种场景正常启动Bootloader读标志发现A槽有效且为活动槽直接跳转A槽AppApp跑起来后调用一个接口向上层报告启动成功Bootloader据此决定是否清除回滚保护标志位。升级启动Bootloader读标志发现非活动槽有完整固件且校验通过则跳转到非活动槽。此时活动槽已被切换为新槽。如果App启动后发现固件异常无需人工干预启动代码直接调用回滚指令下次复位时Bootloader发现活动槽固件未确认有效自动切回旧槽。这个机制的关键在于确认这一步。我们不会让Bootloader直接信任新固件而是要求App在正常运行一段时间后主动确认固件可用才把回滚保护解除。这是从汽车电子OTA里学到的经验很值得借鉴。1.3 分区规划256KB Flash怎么分才够用以STM32F103RCT6为例片内Flash 256KB起始地址0x08000000扇区大小统一1KB但按128字节编程粒度写入。我这里给出一种经过实际验证的分区方案区域起始地址大小用途Bootloader0x0800000016KBIAP引导程序A槽App0x0800400096KB活动固件槽AB槽App0x0802200096KB活动固件槽B槽位标志0x080400002KB槽位状态、固件CRC、回滚标志参数存储0x08040800余下空间用户参数、日志缓存有几点要特别说明Bootloader给到16KB用标准库V3.5实现UART接收、Flash擦写、CRC校验、跳转逻辑足够。如果想加GUI升级或者加密解密建议Bootloader扩到24KB以上。两个槽位等分96KB就意味着App固件编译链接时必须控制在96KB以内。你在Keil里把IROM1起始地址改为0x08004000Size改为0x0001800096KB编译后如果出现overflow错误就需要精简代码或压缩固件。槽位标志区比较特殊因为它需要频繁擦写建议使用双页交替策略标志写在两个扇区内每次写入前先判断哪个扇区有效避免一次掉电把标志区写坏。这个细节我是踩过坑的后面会展开讲。1.4 方案选型为什么不用内置IAP而要完全自研BootloaderSTM32本身支持内置Bootloader从系统存储器启动后可以通过USART1、USB等接口下载固件。但内置Bootloader有两个问题解决不了一是它无法跳转到任意固定地址的App启动二是它根本不认识我们自定义的固件包格式和槽位标志。所以自研Bootloader是必须的。在自研Bootloader的传输协议选择上我对比过三种方案方案优点缺点YMODEM协议简单广泛支持无断点续传一包错全重传自定义UART协议灵活可控便于加CRC、加密、断点续传需要自己造轮子基于MQTT/TCP的远程升级支持远程海量设备F103资源紧张需外接通信模块适合网关场景这次教程先讲UART场景下的自定义协议同时会给出接入Wi-Fi/4G模块进行远程升级的思路。YMODEM做本地升级其实也足够但断点续传是硬需求时只能自定义。因为F103写Flash是128字节粒度我最终设计的帧格式就是一帧128字节有效载荷加8字节头部和2字节CRC16每帧独立校验、独立确认这样断点续传只需要记录最后成功写入的帧号重新连接后从下一帧继续传不需要整包重来。2. Bootloader工程搭建与Flash驱动详解2.1 基于标准库V3.5创建Bootloader工程Bootloader工程我基于标准外设库V3.5搭建Keil MDK 5环境。为什么不推荐用HAL库Bootloader代码量小、对实时性要求高标准库更贴近寄存器操作逻辑清晰也更容易控制Flash擦写时序。当然用CubeMX生成HAL库工程也没问题但建议你至少把Flash驱动换成寄存器版本减少不必要的外设开销。工程创建步骤如下新建工程文件夹加入标准库的CMSIS核心文件和启动文件startup_stm32f10x_hd.s高密度型号选这个中低密度选md/ls。把system_stm32f10x.c加进去系统时钟配置到72MHz。添加GPIO、USART、Flash、CRC等外设驱动文件。配置魔法棒选项Target页勾选Use MicroLIB确保printf重定向到串口的代码体积最小。第一版Bootloader我做了一个串口命令行交互界面收到OTA_START命令后进入升级模式再接收固件包。这个交互方式对于产线烧录和本地调试都很直观。链接脚本里必须把Bootloader的ROM地址设为0x08000000长度0x0000400016KB。App的中断向量表偏移我们会在App工程里设置与Bootloader分区规划配套。2.2 128字节页编程F103的Flash操作时序STM32F103的片内Flash是一个很传统的内存读操作可以按字节/半字/字并行执行但写操作必须按16位半字进行。Flash编程的最小单位是一个半字16位但为了和外部通信帧对齐我们一般按128字节一块下发内部循环64次半字编程实现。这里重点说时序。很多人第一次调Flash驱动遇到写不进或者写一半HardFault基本都是时序问题。标准流程是解锁Flash向FLASH_KEYR依次写入0x45670123、0xCDEF89AB。检查FLASH_SR的BSY位确保Flash空闲。写入数据对目标地址连续写半字每写一个半字都要检查BSY位等硬件忙完。全部写完擦除需要单独操作按下页1KB擦除或整片擦除。加锁Flash向FLASH_CR写入0x00000080LOCK位。页擦除我之前不严谨直接一次性给整页发擦除命令然后立刻读状态结果偶尔出现擦除失败——原因是擦除操作是异步的命令发出后需要等到BSY清零才算完成。后面所有擦写操作都统一加了轮询BSY的代码问题就再没出现过。2.3 固件校验CRC32还是CRC16放在包的什么位置固件校验这一步决定了OTA方案靠不靠谱。我见过很多入门项目只用累加和做校验实际传输过程中出现误码很难发现。我们选用CRC32基于查表法实现速度很快F103跑72MHz算96KB固件大概也就几毫秒级别。CRC32的计算范围是整个固件包内容包含头部、固件数据、对齐填充计算结果放在固件包头部的CRC字段里。Bootloader拿到包后先解析头部再对整包做CRC32比对一致才允许写入Flash。写入过程中每个128字节帧还有单独的CRC16做链路层校验双层校验出错的概率极低。固件包头我们自己定义格式如下偏移长度字段说明04Magic固定0xA5A5A5A5用于识别固件包44FirmwareSize固件数据长度不含包头82FirmwareVersion版本号高字节主版本低字节次版本104CRC32对FirmwareSizeFirmwareData计算的结果142HeaderCRC对包头前14字节计算的CRC1616NFirmwareData固件数据不足128字节按0xFF填充版本号在Bootloader里做判断新固件版本必须大于等于当前活动槽固件版本否则拒绝写入这是防止误刷旧版本的一道闸门。2.4 Bootloader启动流程的状态机实现Bootloader的main函数不跑裸循环而是跑一个精简状态机状态转移如下上电初始化时钟、串口、Flash、看门狗。读取槽位标志解析A/B槽状态。检查回滚保护标志标记待回滚的直接回退到旧槽。检查非活动槽是否需要提交如果新固件已运行且确认过则清除回滚标志。跳转到当前活动槽App或者在收到升级命令时进入升级模式。进入升级模式后状态机转移到接收帧状态。每收一帧做解析、CRC校验、写入Flash、回ACK。最后一帧收完做整包CRC校验再更新槽位标志设置待确认状态最后软复位重新走一遍启动链路。整个流程不到30个状态写起来很清晰比传统顺序执行代码好维护得多也好测试。3. App工程改造重定位、中断向量与远程升级适配3.1 中断向量表重定位为什么App一跑就死把App编译链接地址改到0x08004000后直接烧录运行大概率一进中断就死机。原因不复杂Cortex-M3的核心有一个向量表偏移寄存器叫VTOR0xE000ED08默认值为0x00000000内核触发中断时会去0x08000000读中断向量也就是Bootloader的向量表。而App的中断向量排在0x08004000地址对不上一个中断进来CPU直接跑飞。解决方案有两种在启动文件里设置VTOR在SystemInit()函数里加一行汇编代码在main()之前完成重定位。推荐这种方式。运行时用寄存器写VTOR在main()最开头使用SCB-VTOR FLASH_BASE | 0x4000。对F103这种不带TZ的单片机来说VTOR已经解锁可直接写。注意如果你的App还用到了DMA中断、串口空闲中断这些向量表重定位后一切照常但有一个坑如果Bootloader和App同时用同一个串口Bootloader初始化过串口后跳转前最好把串口复位RCC复位外设否则可能电平状态冲突。我在实际项目里把VTOR设置放在SystemInit()里最稳因为这个函数在标准库启动文件里第一批执行比main()还要早几乎覆盖了所有可能的早期中断连调试阶段的断点都不会受影响。3.2 在Keil中配置App工程的分区链接脚本App工程的配置是AB分区复现里最容易出错的环节之一很多朋友改完启动文件忘改链接地址或者改了链接地址忘了改VTOR。我用的App工程配置以Keil MDK 5为例打开Options for Target → Target页。IROM1勾选起始地址改0x08004000大小填0x0001800096KB。IRAM1起始0x20000000大小0x0000C00048KBF103RCT6的RAM就是48KB。如果你的型号RAM是64KB比如F103ZET6就填0x00010000。C/C页的Define里加上VECT_TAB_OFFSET0x4000这样标准库的system_stm32f10x.c里会根据宏自动设置VTOR偏移。还有一种做法是在system_stm32f10x.c里直接改硬编码偏移0x4000。我建议用宏定义因为以后如果调分区大小只需要改一处。链接完成后记得在Build Output窗口确认Code和RO-data总大小没有超过96KB。3.3 App主动上报启动状态与固件确认机制AB分区OTA能不能做到失败自动回滚关键在App侧的一个小函数我习惯叫它ota_fw_up_confirm()。这个函数在App正常运行30秒后主动执行一次把已确认标志写入槽位标志区表示当前固件是可用的。Bootloader看到这个标志后才知道可以解除回滚保护。具体实现思路App启动后读取当前槽位状态确认自己在A槽还是B槽。启动一个软件定时器默认30秒。定时器到期后如果系统各项外设初始化成功比如通信模块已经注册到网络调用确认函数把该槽位的状态从待确认改为已确认。如果30秒内发生硬件故障、看门狗复位等异常没有执行确认函数Bootloader会因回滚标志未清除自动跳回旧槽。这样做的原因是单纯能跑main不代表固件是真的健康的外设有没有正常初始化网络模块有没有正常注册都要在运行一段时间后才能确认。30秒这个值可以按实际业务调整但不要太短至少要覆盖App完整的启动周期。3.4 串口双发缓存与断点续传机制断点续传是我这次重点实现的功能也是和普通YMODEM方案的最大区别。思路很简单Bootloader端维护一个全局变量last_written_frame每成功写入一帧Flash就把它写到槽位标志区的断点续传记录里。上位机重新连接后先发一个查询帧Bootloader返回已写入的帧号上位机从这个帧号的下一帧开始续传。这里有个细节值得提醒Flash写入是按128字节帧对齐的但最后一帧肯定不满128字节。我约定固件包总长度对帧对齐后不足部分统一填0xFF写入时原样写入。App的启动逻辑没有约束必须对齐但因为LLVM/Keil生成的bin文件本来就是整数倍所以当固件长度不是128整数倍时最后不足一帧的数据必须自己处理不能用上一帧的缓存残留。断点续传还有一层保障每次擦除页之前先判断当前页是否已擦除如果Flash内容全为0xFF则跳过擦除操作直接写入。这样断电恢复后断电时未写入的部分保持0xFF不会出现擦除一半又写一半的混合状态。3.5 适配远程升级从UART到MQTT/TCP的扩展思路F103做UART OTA只是第一步现在很多产品都需要走Wi-Fi或以太网远程升级。我在另一套方案里用ESP8266或W5500做透传模块把固件包拆成TCP帧发给MCUMCU侧做一层协议封装复用的还是这套Bootloader框架。具体改动点有三个通信模块驱动独立成接口Bootloader不再关心底层是UART还是SPI以太网只维护一个fw_update_transport结构体内部提供send/recv两个函数指针。远程升级时建议加一层简单的加密和鉴权防止恶意固件包直接刷入。我用AES-128-CBC加密固件内容密钥烧录在独立Flash区域Bootloader解密后再写App区。空中升级失败率的控制远程场景下网络波动比有线严重得多断点续传机制和帧级别CRC就更加关键。实测下来基于这个框架的远程升级成功率能到99.5%以上剩余0.5%基本是网络彻底断开后需要人工介入。4. 固件打包、传输协议与上位机工具实现4.1 为什么不能用裸bin直接传固件包要加什么我刚开始做OTA时也图省事直接拿Keil生成的bin文件按起始地址往Flash里写。后来发现三个问题一是没有版本号Bootloader无法判断固件新旧二是没有全局CRC传输出错要等整个固件跑飞后才发现三是无法支持多槽位不知道该写到A还是B。所以固件包必须加自定义头部。固件打包工具我写了一款跨平台的Python脚本ota_pack.py输入Keil生成的bin文件输出带头部和CRC的.ota文件同时生成一个firmware_info.txt保存固件的版本、大小、CRC32值方便产线追溯。打包脚本的Linux版本我会用Python标准库实现不依赖第三方库在任何Linux发行版和Windows上都能跑。核心逻辑如下import struct import zlib import sys def build_ota(src_bin, dst_ota, version_major, version_minor): with open(src_bin, rb) as f: fw_data f.read() # 对齐到128字节 if len(fw_data) % 128 ! 0: fw_data b\xFF * (128 - len(fw_data) % 128) fw_size len(fw_data) # CRC32覆盖固件数据 crc32_val zlib.crc32(fw_data) 0xFFFFFFFF header struct.pack(IIHHI, 0xA5A5A5A5, fw_size, (version_major 8) | version_minor, crc32_val, 0) # HeaderCRC占位先填0 header_crc zlib.crc32(header) 0xFFFF header header[:14] struct.pack(H, header_crc) with open(dst_ota, wb) as f: f.write(header) f.write(fw_data) print(fbuild ok: {dst_ota}, size{fw_size}, crc320x{crc32_val:08X}) if __name__ __main__: build_ota(sys.argv[1], sys.argv[2], int(sys.argv[3]), int(sys.argv[4]))4.2 传输帧格式设计与滑动窗口重传固件包构建好之后通过串口一帧一帧传给Bootloader。帧格式我定义为固定头部加可选负载字节序号长度字段说明01FrameType0x01数据帧0x02结束帧0x03查询帧0x04命令帧12FrameSeq帧序号从0递增最多6553534PayloadLen本帧负载长度数据帧固定12872CRC16对帧头部负载计算9NPayload固件数据数据帧/ 版本信息查询帧发送逻辑采用等确认模式上位机发一帧等专属ACK帧收到后才发下一帧。实测在115200波特率下传96KB固件大约需要70秒。如果觉得慢有两个优化方向一是提高波特率到460800或921600需要确认线材和驱动支持二是做窗口机制一次连续发4~8帧再统一收ACK吞吐量可以提升3~4倍占用RAM也不大。我在正式方案里做了一个小滑动窗口窗口大小为4帧。上位机连续发4帧Bootloader按seq顺序写Flash写完后回一个含最大连续帧号的ACK上位机继续发这个帧号之后的4帧。如果某一帧CRC错Bootloader回NACK并带上错误帧号上位机从这个帧号重传当前窗口。窗口重传比逐帧确认快得多实测同样波特率下96KB固件只需约18秒。窗口大小不是越大越好4帧是比较平衡的值F103串口缓冲有限Flash写入128字节需要约2ms窗口太大容易丢帧太小又浪费带宽。4.3 上位机QT工具开发要点上位机我做的是一套QT Widgets界面支持串口配置、固件包选择、发送进度条和日志窗口。两个核心组件串口类继承QSerialPort读写全程走信号槽避免界面卡死。接收线程按帧解析碰到ACK更新发送队列。发送状态机准备阶段读取.ota文件计算总帧数打开串口。查询阶段发送查询帧等Bootloader返回已写帧号确认是否续传。发送阶段按窗口连续发送数据帧处理ACK/NACK。结束阶段发送结束帧等待Bootloader回复更新成功准备跳转然后读取槽位状态显示在界面上。工具有一个容易忽略的点进度条不能只按已发送帧数/总帧数算因为断点续传时起始帧号不为0进度要按(last_written_frame sent_frames) / total_frames计算否则会看到进度条先跳后回退。4.4 产线烧录与加密签名流程AB分区OTA落地到产品上还要考虑产线场景。我们产线流程是用ST-Link直接烧录Bootloader到0x08000000。烧录一个出厂固件到A槽可以通过串口或者直接下载bin到A槽地址。槽位标志区初始化活动槽A状态已确认版本出厂版本。功能测试通过后封装发货。如果产品已经有联网功能可以支持产线空板预刷Bootloader后续全远程升级。这种情况下出厂固件可以简化只要包含基本的通信驱动即可应用逻辑后续通过AB OTA推上去。实际产线效率会高很多但注意在Bootloader里做一个锁定保护当槽位标志里的安全标志置位后禁止通过串口明文升级只允许加密升级防止生产环境被恶意刷机。5. 掉电保护、回滚机制与状态恢复实战5.1 状态标志的双页交替写入AB分区系统最怕的不是固件损坏而是状态标志损坏——一旦Bootloader不知道哪个槽位是有效的整个系统就彻底瘫痪。所以我用两个2KB扇区循环保存标志避免反复擦写同一扇区导致寿命耗尽。F103的Flash擦写寿命标称10,000次按每天升级50次算也只能用200天双页交替可以把寿命直接翻倍。双页交替写入的逻辑启动时同时读扇区0和扇区1的标志。比较两者序号Sequence序号大的视为最新有效标志。写入时始终写入序号较小较旧的扇区写入后序号1。如果两个扇区都有效且序号相同说明上次写入时在擦除第二个扇区之前掉了电此时以扇区0为准回滚写扇区1保证数据不分裂。这个策略实际上是小型文件系统里常见的日志式写入实现起来并不复杂但可靠性提升非常明显。我做过一个掉电循环测试随机在写入中途断电300次没有一次出现标志丢失或双有效冲突的情况。5.2 三次启动失败自动回滚机制通常的做法是Bootloader里维护一个连续启动失败次数BootAttemptCount。每次跳转App前先把计数1并写回Flash如果App正常运行并调用了ota_fw_up_confirm()则把这个计数清零如果App启动后很快复位计数会持续累加。当计数达到3时Bootloader判定当前活动槽固件不可用执行回滚操作把活动槽切换为另一个槽并清零计数。回滚只涉及改几个标志字节不涉及Flash大区擦写速度极快。实测从复位到回滚到旧槽总共不到100ms。这里有一个关键细节计数必须写在掉电也不丢失的区域同时要注意写次数限制。每次启动都抛光一次Flash显然不现实所以我把计数放在固定变量并定期回写而不是每次都写。具体实现是RAM里维护一个值只有从未确认变为已确认或者新槽首次启动失败时才写Flash。这样即便每天开关机100次Flash写入次数也很少。5.3 升级中断电后的恢复验证升级中最极端的场景是固件写到一半突然断电恢复供电后Bootloader要能判断当前处于中断的升级状态并做出合理响应。我们遇到过一次写入到第300帧时断电恢复后Bootloader读取标志发现非活动槽还是旧状态没有被篡改活动槽A依旧有效于是直接跳过升级流程正常启动A槽。用户完全无感知。还有一次意外非活动槽的待升级标志已经写入了但固件只写了一半断电了。恢复供电后Bootloader发现非活动槽有待升级标志但整包CRC校验失败直接丢弃该标志把非活动槽清为无效继续从活动槽启动。这个判断逻辑非常适合放在升级状态机的前置检查里。所以我的经验是永远不要在收到结束帧并且整包校验通过之前更新待升级标志。只有完整固件落在Flash里了才允许动槽位状态。这样即使中途掉电错误状态最多就是非活动槽残留一段无效固件不会影响当前运行。5.4 看门狗在OTA过程中的特殊处理Bootloader的看门狗IWDG在升级模式下容易成为坑。因为擦写Flash会阻塞CPU几百毫秒到几秒看门狗计数来不及喂Bootloader就被强制复位了。这个问题我遇到过很多次尤其是在擦除整页时因为F103页擦除约20~40ms如果一页页擦还好整区擦除直接卡死。我的处理方案是升级模式下关闭独立看门狗或者把超时时间调到最长IWDG最大约32.7秒但这不是无限大。如果不希望关闭看门狗可以在擦写函数里分块喂狗擦除每页之间做一次喂狗操作写Flash每128字节做完也喂一次。考虑到Flash写128字节约2ms喂狗不会造成性能瓶颈。我的建议是Bootloader启动时默认不开启IWDG等跳转App前才初始化并启动。这样Bootloader自身升级过程不受看门狗干扰而App运行期间依然有独立看门狗兜底防止App死循环或跑飞。跳转前要确保把看门狗配置好否则App启动后看门狗没开就是裸奔状态。5.5 异常场景恢复操作指南尽管机制做得很完善但总有极端情况需要人工介入。我在文档里给现场工程师写了一份排查流程图式的操作指南这里摘录核心几条设备反复重启且无法进入升级模式用串口连接持续发送0xAA 0x55握手包10秒以上Bootloader检测到连续有效握手包会强制进入升级模式。升级过程中意外复位重新升级时提示版本不匹配查看槽位标志里的版本字段如果没有正常更新说明标志写入失败需要手动执行强制擦除标志区并恢复出厂设置。两个槽位都显示无效这是最严重的场景通常只会在Bootloader本身有Bug时出现。解决办法是使用烧录器重新烧录Bootloader和出厂固件然后恢复槽位标志。A/B槽版本相同但启动异常检查两个槽位固件编译时间戳确认是否真的是两份相同固件排除加载错误。6. 全流程联调与测试实录6.1 联调环境搭建我这次联调用的硬件非常简单STM32F103RCT6最小系统板一块板上带CH340串口芯片。ST-Link V2一个用于烧录Bootloader和调试。USB转TTL模块一个连接PC串口和评估板USART1。杜邦线若干电源用5V/3.3V双路。软件环境是Windows 10 Keil MDK 5.36 STM32标准库V3.5 Python 3.9 Qt 5.15。上位机用QSerialPort打包脚本用Python标准库。烧录Bootloader时建议在Keil里勾选Reset and Run这样烧录完自动运行。然后用串口助手验证Bootloader串口输出Bootloader v1.0 ready说明工程基本没问题。6.2 正常升级全流程从打包到重启验证先跑一遍完整升级把整个流程走通。我用的示例App是一个LED闪烁程序版本号从V1.0升级到V1.1App运行后串口每隔1秒输出一行App v1.1 running。用Keil编译V1.1工程生成bin文件。运行python ota_pack.py app.bin app_v1.1.ota 1 1生成固件包。打开上位机工具选择串口COM3波特率115200选择固件包点击连接。单击开始升级观察进度条和日志窗口。升级完成后设备自动软复位Bootloader跳转到B槽串口输出App v1.1 running。这个流程看起来很平淡但它验证了Bootloader解析包头、计算CRC、写Flash、切换槽位、跳转的全链路。我特别注意到跳转后的第一个串口字节是否正常如果出现乱码多半是时钟配置或串口波特率有问题和Bootloader跳转前的外设复位状态有关。6.3 注入错误的CRC如何触发拒绝写入为了验证CRC校验的有效性我手动把一个字节的固件数据改错重新打包。Bootloader解析包头后应该计算CRC32失败直接回NACK并返回错误码0x03CRC_MISMATCH不写任何Flash。实测上位机窗口弹出CRC校验失败请检查固件包进度条停在0%日志记录错误码设备继续正常启动A槽。这个小实验强烈建议大家自己跑一遍能很直观地体会到帧校验整包校验双保险的价值。如果只想校验链路不校验包可以在上位机里手动改掉一帧数据的CRC16Bootloader会回复NACK并等待该帧重传。链路层校验的作用和整包CRC各有侧重前者保证传输正确后者保证固件完整。6.4 新固件启动崩溃触发的自动回滚这块是整个方案最让我放心的部分。我把V1.1固件故意改成启动后5秒触发HardFault然后执行升级。流程如下升级成功后Bootloader切换为B槽跳转B槽App。B槽App启动运行到第5秒触发HardFault。系统复位Bootloader读取到待确认状态和启动失败计数1继续尝试B槽。第二次复位后计数2第三次复位后计数3。Bootloader判定B槽固件无效自动切换回A槽计数清零启动失败计数归零。串口输出Rollback to A slot success。实测从第一次B槽崩溃到最终回滚到A槽大约经历了3次复位循环耗时不到5秒。这里要强调回滚机制必须搭配Bootloader的串口日志一起调试否则你只看到设备反复重启很难判断是崩溃还是正常的失败重试流程。6.5 断点续传与重复刷入相同版本固件的边界测试断点续传我实际测了三个场景升级到一半拔掉串口线再插回、升级到一半断电再上电、升级完成后立刻重复刷一遍相同版本固件。场景一拔掉串口线再插回。上位机重新连接后发查询帧Bootloader返回已写帧号100进度条自动从63%左右继续后续帧正常写完升级成功。这个场景模拟手滑碰掉USB线的常见情况实测完全无感。场景二断电再上电。恢复后Bootloader发现非活动槽的待升级标志被清掉了因为断电发生在整包校验之前所以当前活动槽A继续正常运行。此时重新发起升级Bootloader会把非活动槽的旧数据擦掉重新写不冲突。场景三重复刷相同版本。上位机发送V1.1固件给已经运行V1.1的系统Bootloader检查到版本号不高于当前活动槽版本直接拒绝写入并返回错误码0x05VERSION_NOT_NEWER。这个策略有效防止了产线上重复刷写浪费时间和Flash寿命。7. 踩坑记我在这套方案里栽过的五个跟头7.1 坑一中断向量表偏移在启动文件里写错了最早我图省事把VTOR设置直接写在App的main()开头但App在进入main之前系统时钟初始化已经完成串口中断是否触发取决于外部来的电平。某次调试发现App启动后串口第一个字节能收到但第二个字节开始乱码排查半天发现是USART1中断在VTOR设置生效前就已经来了CPU按Bootloader的向量表去跳自然跑飞。解决办法就是老老实实把VTOR设置放到SystemInit()里让它在任何外设初始化之前生效。另外还要注意Keil里Define宏VECT_TAB_OFFSET0x4000只对标准库有效如果你用的是自己写的SystemInit务必确保这个宏被正确读了否则偏移不生效。7.2 坑二Bootloader串口打印和App串口打架Bootloader跳转前用USART1打印了一堆日志跳转后App也用USART1结果发现Bootloader最后打印的几个字节残留在发送缓冲区里把App的第一行日志冲掉了一部分。解决办法是跳转前做一次彻底的外设复位调用RCC_APB2PeriphResetCmd(RCC_APB2Periph_USART1, ENABLE)延时几个微秒后再关掉复位最后把发送缓冲区数据清零。跳转后App重新初始化串口就不会有残留了。7.3 坑三F103 Flash 写0和写1的特性导致标志区被搞坏Flash只能把1写成0不能把0写成1想恢复成1只能擦除整个扇区。槽位标志区如果频繁修改某个字节这个字节很快就变成0x00了如果恰好在掉电瞬间写入整个扇区可能无法使用。我最终用双页交替序号比较解决但还有一层保护所有标志数据在Flash里都要冗余存储。比如待确认这个状态不仅存一个0x01还要存一个0x01的反码0xFE读取时两者都匹配才认为状态有效。这样即便一个字节被写坏另一个还能兜底。代价是标志区容量占用更大但2KB的空间足够。7.4 坑四上位机发送过快导致Bootloader缓冲区溢出初期我用的是逐帧发送Bootloader每帧处理完再回ACK所以不会溢出。后来优化成4帧窗口后Bootloader如果还在擦Flash新的4帧已经堆在串口缓冲区里超过F103的串口接收缓冲区容量一般1KB就直接丢帧。解决方法是把接收处理做成状态机环形缓冲。串口中断只负责把收到的字节压进环形队列主循环空闲时从队列取数据解析处理。Flash擦写时队列不阻塞等擦写完成继续处理。环形缓冲区大小设为2KB窗口4帧128字节加头部大约540字节绰绰有余。实测窗口模式下连续传输50次无一丢帧。7.5 坑五用printf调试时固件体积不受控Bootloader 16KB空间本来很有富余但我在调试阶段随手加了一堆printf输出结果编译出来接近16KB上限差点放不下。后来统计发现MicroLIB下的printf约占用3KB但如果用了浮点格式化体积会爆炸到10KB以上。解决方法是调试信息尽量精简浮点一律用整数转换能不开printf的地方就用ITM_SendChar或者直接写串口寄存器输出固定字符串。最终Bootloader编译体积稳定在10.5KB左右留了5.5KB余量做后续功能扩展。8. 从UART到无线这套方案还能怎么扩展AB分区OTA框架天然支持多种传输介质。UART只是最简单的例子换成ESP8266、4G模块、LoRa、以太网核心逻辑都不用动只需要换掉传输层接口。我把这套框架移植过几个平台总结出三个高频扩展方向第一个方向是远程升级网关。很多工控场景里MCU本身不联网但有一台上位机或网关可以联网。这种情况下MCU跑AB分区OTA网关负责从云平台拉取固件包通过UART/RS485/CAN转发给MCU。帧格式和协议完全复用只是把上位机换成了网关。第二个方向是CAN总线OTA。汽车电子、工业现场的CAN总线升级需求很大。CAN每个标准数据帧只有8字节没法直接放128字节负载所以需要把帧格式改成多帧组合一个逻辑包拆成N个CAN帧发送接收端攒够128字节再写Flash。CAN的断点续传和窗口重传逻辑和UART基本相同需要注意的是CAN总线负载率不能太高一阵猛发容易出问题。第三个方向是加密签名。如果产品需要防止固件被逆向或者防止恶意固件注入建议在AB分区基础上叠加AES-128-CBC加密和ECDSA签名校验。加密保证固件内容不泄露签名保证固件来源可信。Bootloader里实现ECDSA验签对F103来说是有些吃力的实测验签一次约需1~2秒可以接受。AES解密是流式的可以在写入Flash时边解密边写不会显著降低升级速度。这几个月做下来我的核心感受是AB分区OTA的核心难点不在写代码而在状态管理。Flash擦写、串口传输这些都是成熟技术真正容易出问题的是各种掉电、重启、异常状态下系统还能不能回到一个确定的状态。用这套方案的这段时间我最大的安心来自于无论怎么折腾设备永远不会变砖——最多是升级失败回滚到旧版本然后重新联网下载再试一次。最后再分享一个小技巧调试AB分区OTA时一定要在Bootloader和App里同时加上版本号打印并且在串口日志里区分当前是在哪个槽位运行的。比如App每次启动都打印App v1.1 slotBBootloader跳转前打印Jump to slot B 0x08022000。这样现场排查问题时一眼就能看出设备当前状态不用猜。这套方案后续我计划再扩展到远程签名升级和多固件比如Wi-Fi模块固件、字库文件联合升级到时候再回来补一篇新文章。
RELATED

相关推荐

江苏泥狗子网络:营销型网站建设的苏中南标杆服务商

江苏泥狗子网络:营销型网站建设的苏中南标杆服务商

前言2026年,企业数字化转型进入深水区,网站作为企业线上展示的核心窗口,其价值早已超越传统的电子名片范畴。江苏作为制造业大省,拥有众多中小微企业,这些企业在数字化浪潮中面临着共同的挑战:如何选择真正…

📅 2026/9/9 11:06:35
Linux虚拟地址空间详解:从页表到mmap,彻底搞懂内存映射机制

Linux虚拟地址空间详解:从页表到mmap,彻底搞懂内存映射机制

1. 虚拟地址空间到底是干什么的刚开始学Linux的时候,多半会被各种“内存”概念绕晕——free -m里显示的used、buff/cache明明还不算复杂,等看到每个进程的虚拟内存占用动不动几个G,就彻底懵了。比如你写一个只打印hello world的程序&#xff…

📅 2026/9/9 11:06:35
2026年9月亨得利钟表官方售后靠谱吗?从门店工艺水准与服务细节看真实口碑

2026年9月亨得利钟表官方售后靠谱吗?从门店工艺水准与服务细节看真实口碑

2026年9月亨得利钟表官方售后靠谱吗?从门店工艺水准与服务细节看真实口碑  一、2026亨得利售后口碑调研背景在腕表养护圈子里,大多数表主最终纠结的点,从来不是哪家维修更便宜,而是哪家售后真实靠谱、不乱修、修完稳定、有官方兜…

📅 2026/9/9 11:06:35
MORE NEWS

更多资讯

📰

Scrapy+Redis分布式爬虫:架构设计、核心实现与监控运维实战

Scrapy写单机爬虫很简单,但一旦数据量上来、目标站点多了,单机瓶颈就会立刻暴露出来。爬得慢了老板催,爬得快了IP被封,好不容易跑起来的爬虫半夜挂了也没人知道。我做了几年爬虫相关的工作,2018年第一次把爬虫从单机改…

📰

RSA密钥格式PKCS#1与PKCS#8区别及Java转换实战

先纠正一个标题里的拼写:严格来说应该是RSA,不是RAS。这个笔误在各种技术群里太常见了,搜索引擎里甚至能搜出一堆“RAS加密”,但算法本身叫Rivest-Shamir-Adleman,缩写RSA。RAS这个词在技术圈属于口口相传的错误叫法&a…

📰

企业级Voice Agent架构:级联式三明治设计与STT-LLM-TTS编排实战

过去两年,很多团队做语音交互项目时都经历过类似的痛苦:单独测 STT,识别率很高;单独测大模型,回答也有模有样;单独测 TTS,音色自然流畅。可一旦把三者串成一条完整的语音对话链路,效…

📰

单机游戏玩法底层逻辑拆解:行为链、模块组合与失败设计

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

📰

FPGA测控系统程序框架设计:从模块划分到时序约束

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

📰

分布式训练平台架构设计与实践:从资源调度到GPU利用率优化

我做了好几年的分布式训练基础设施,踩过的坑能装满一辆皮卡。前阵子翻到Hulu那套分布式训练平台的架构分享,发现很多设计思路和我自己在生产环境里的摸索不谋而合,也有一些是他们做得更细致、更值得借鉴的地方。Hulu的平台核心要解决的事情其…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬