ARTICLE DETAIL

资讯详情

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

基于RT-Thread与W25Q128的STM32 OTA远程升级方案详解

基于RT-Thread与W25Q128的STM32 OTA远程升级方案详解 1. 为什么要把OTA放在片外Flash上做嵌入式开发的朋友应该都遇到过这种尴尬产品已经量产了结果现场反馈某个功能有Bug或者需要加一个新协议这时候只能派工程师带着ST-Link/J-Link出差去现场挨个设备拆壳、接线、烧录。说句实话有些设备安装在偏远站点光来回的路费和时间就够喝一壶的。OT**A升级就是解决这个痛点的标准方案它让设备能够通过网络、串口、SD卡等介质远程更新固件不用再开壳子动硬件。但OTA这事说起来简单真正做起来有几个绕不开的问题。第一个就是存储介质的选择。很多人习惯把固件放在MCU内部Flash里比如STM32F411CEU6有512KB Flash如果Bootloader占32KBApp占256KB再留两个分区做备份你会发现内部Flash根本不够用。尤其是升级过程中如果你想把当前运行的固件先备份一份再把新固件写入写坏了还能回滚那分区数量翻倍内部Flash直接爆掉。而且很多MCU的内部Flash在写的时候会影响中断响应如果在擦写过程中来了一个关键中断处理不好就会出现系统卡死的情况。所以把OTA的存储区域放到片外Flash比如W25Q128上就成了一个非常务实的方案。W25Q128是华邦Winbond推出的一款SPI NOR Flash容量128Mbit也就是16MB价格便宜、货源稳定、读写寿命通常在10万次擦写以上在工控、消费电子、通信模块里用得极其广泛。16MB是什么概念STM32F411的内部Flash才512KB16MB是它的32倍放Bootloader、App、备份分区、参数区、日志区随便怎么折腾都够用。片外Flash属于外设读写操作走SPI总线不占用MCU内部程序存储空间也不影响中断响应升级过程更安全。这次的项目使用的是RT-Thread操作系统配合RT-Thread Studio开发环境MCU是STM32F411系列通过YModem协议接收固件将固件存放到片外Flash W25Q128中再完成校验和跳转。文章会从头到尾把整个方案的技术选型、分区计算、协议流程、代码实现、烧录验证、坑点排查全部讲一遍适合用什么场景呢如果你的产品也有远程升级需求手头正好有片外Flash资源想搭建一套稳定可靠的BootloaderApp架构那这篇笔记可以直接拿来当参考。还要说一下这篇文章是系列笔记的第四篇前三篇分别记录了RT-Thread Studio的基础工程创建、串口外设配置、W25Q128驱动的移植过程。如果读者是第一次接触RT-Thread Studio建议先熟悉一下基本操作因为后面的内容会默认你已经具备了RT-Thread基础工程和板级外设配置的能力。但就算你是新手只要跟着这篇文章一步一步走也能搭出一套能用的OTA链路只是中间可能要多花一点时间排查环境问题。2. 系统总体架构与分区规划2.1 Bootloader、App与下载区的三角关系OTA升级和普通的程序烧录最大的区别在于系统里必须有至少两块独立的程序区域才能实现自更新。最经典的架构就是Bootloader引导程序App应用程序双区方案。设备上电后先运行BootloaderBootloader负责检查是否有新固件需要升级如果有就执行升级流程如果没有就直接跳转到App运行。那W25Q128放在这个架构里扮演什么角色呢有两种典型用法。第一种是把W25Q128作为下载缓冲区和备份区Bootloader通过YModem协议接收到新的固件后先写入W25Q128的下载区全部接收完成后做一次CRC校验校验通过再搬运到内部Flash的App分区然后跳转执行。这种方案的优点是App运行在内部Flash上执行速度快W25Q128只是作为中转站缺点是如果固件很大拷贝过程需要时间而且在搬运过程中掉电内部Flash里可能会出现半个固件的情况。第二种方案是把App直接放在W25Q128上执行也就是XIPExecute In Place方案但这需要MCU支持从SPI Flash映射执行代码很多MCU不具备这个能力而且执行速度远不如内部Flash。所以对STM32F411这类不支持XIP的MCU来说采用第一种方案更可靠。再来说分区规划。W25Q128有16MB空间浪费是可耻的而且合理的分区能避免很多后续麻烦。我给这次项目做的分区表如下分区名称起始地址容量作用固件备份区0x0000001MB保存当前正常运行中的App固件下载缓冲区0x1000001MB暂存通过YModem接收的新固件参数存储区0x200000128KB保存升级标志、版本号、校验结果日志存储区0x2200001MB记录升级日志和运行日志保留区域0x320000剩余空间后续扩展使用如字体库、音频资源分区规划的核心思路是备份区和下载区至少能存下App最大固件体积而且这两个区之间还要留足余量。为什么备份区要1MB因为如果你的App因为优化不足膨胀到600KB256KB的分区就直接废掉了。我见过不少项目在前期规划分区时拍脑袋结果后期固件增长超出分区容量被迫重新设计整个存储方案周期和成本都上去了。所以在芯片容量允许的前提下分区宁大勿小。2.2 片内Flash的Bootloader与App地址分配Bootloader和App在内部Flash中的地址分配也需要仔细计算。STM32F411的Flash起始地址是0x08000000但Bootloader和App不能共用同一个起始地址否则跳转后就乱套了。我这次使用的分配如下Bootloader区域0x08000000分配64KB。为什么不是32KB因为Bootloader里除了必要的跳转逻辑还要包含YModem协议解析、W25Q128驱动、CRC校验、Flash擦写驱动这些代码量加起来肯定超过32KB了万一Bootloader本身写爆了Flash升级链路就彻底瘫痪了。所以Bootloader的空间一定要留足64KB是一个比较稳妥的起步值。App区域0x08010000也就是从Bootloader结束位置开始大约448KB可用空间。对STM32F411CEU6来说这是内部Flash的剩余全部空间。这里有一个非常关键的配置点App工程的链接脚本起始地址必须改成0x08010000否则编译出来的固件会默认从0x08000000开始放置向量表跳转后MCU一上电取向量表地址直接拿错。向量表偏移也是一个不能漏的设置。Cortex-M4内核的MCU在启动时CPU会从向量表偏移地址读取初始栈指针和复位向量。默认情况下向量表放在Flash起始位置0x08000000如果你的App跑在0x08010000就必须设置VTOR寄存器指向0x08010000。在STM32的HAL库里这个操作通常通过SCB-VTOR APP_ADDRESS;语句实现要在App的main函数里尽早执行越早越好因为在设置VTOR之前任何中断响应使用的都是Bootloader的向量表如果此时来了中断中断处理函数就会跑错位置轻则功能异常重则死机。3. YModem协议与升级流程拆解3.1 YModem协议基础不会过时的串口升级老兵说到串口升级协议很多人第一反应是用XModem但XModem一次只能传一个文件而且没有文件信息头对于现代固件升级来说功能太弱了。YModem是在XModem基础之上扩展出来的协议它增加了文件名的传输能力支持一次会话传输多个文件每个文件都带独立的长度校验128字节或1024字节的扇区长度自适应错误重传机制也保留了。虽然现在各种网络OTA、USB DFU很流行但YModem在工业设备、通信模块、Bootloader引导场景里依然有一席之地原因就三个字简单可靠。具体来看YModem的帧格式。一次YModem会话可以分成几个阶段接收方设备端Bootloader发送字符C表示等待发送方PC端的SecureCRT或者YModem发送工具开始传输。发送方回应一个起始帧帧头是SOH0x01表示后面是128字节的数据块。这个起始帧的数据块前两个字节是文件名通常是固件文件名后面跟着文件大小ASCII字符串形式再后面填充0x00补齐128字节。CRC校验占据最后两个字节。然后发送方逐块发送数据帧。数据块序号从0x01开始每块可能是STX0x02开头的1024字节大块也可能是SOH开头的128字节小块。接收方每收到一个块就回一个ACK0x06如果CRC校验失败就回NAK0x15发送方收到NAK就重发当前块。这个过程就是YModem最基础的停止等待ARQ自动重传请求。因为没有滑动窗口所以传输效率不是极致但是在115200波特率下传一个200KB固件大约需要40秒左右作为本地升级手段完全够用。文件数据全部发送完毕后发送方发一个结束帧EOT0x04。接收方回ACK然后发送方再发一个空的YModem结束帧帧头SOH数据块全0x00接收方回ACK传输结束。用生活化的类比来解释YModem的工作方式它就像一个非常谨慎的快递签收员快递员每送一箱货物都要等收件人清点完数量、确认包装没破然后签收一张回执单快递员才会送下一箱。如果收件人发现这一箱有问题就不签收快递员只能原路回去再送一箱一模一样的过来。每一箱货物都有一个唯一的编号确保不会收重。3.2 完整的OTA升级状态机Bootloader里的OTA逻辑不能是一坨简单的“接收固件-写入Flash-跳转”的线性代码而是需要一个清晰的状态机来管理否则任何一步出问题都可能导致设备变砖。我设计的升级状态机如下IDLE - 检查是否有升级标志 CHECK_FLAG读取参数存储区的升级标志位 |-- 无升级标志 - JUMP_APP跳到App运行 |-- 有升级标志 - WAIT_DOWNLOAD进入待接收状态 WAIT_DOWNLOAD等待并接收YModem数据 |-- 接收超时 - JUMP_APP超时后不能卡死直接跑App |-- 接收完成 - VERIFY_FW进入固件校验状态 VERIFY_FWCRC校验下载缓冲区的固件完整性 |-- 校验失败 - JUMP_OLD_APP备份区固件还原 |-- 校验成功 - BUFFER_TO_APP写入内部Flash BUFFER_TO_APP擦除内部App区并写入新固件 |-- 写入失败 - RESTORE_OLD_APP回滚备份区固件 |-- 写入成功 - CLEAR_FLAG清升级标志- JUMP_APP这里面有几个细节值得单独拿出来讲。第一个细节是升级标志的存储位置。为什么不用一个简单的GPIO电平来判断是否进入升级模式因为GPIO可能在设备重启后被初始化成其他功能而且GPIO状态无法记录“升级到一半失败了”的中间状态。所以我选择把升级标志放在W25Q128的参数区用一个结构体来管理包含魔数Magic Number、升级标志位、目标固件版本号、CRC校验值、升级时间等字段。魔数的目的是防止参数区数据被意外破坏后系统误判。比如说如果参数区因为某些原因被写入了随机数据而恰好这个数据的第一个字节等于升级标志的值系统就会错误地进入升级流程。加上魔数校验后只有当整个结构体满足“魔数匹配标志位置位CRC匹配”三个条件时才认为是合法升级请求。第二个细节是接收超时处理。我用了一组时间变量在等待YModem数据帧时记录最近一次收到数据的时间如果超过10秒没有收到任何有效数据帧就放弃升级流程直接跳转到App。这个设计的理由是不能因为一次失败的升级尝试就让设备永久停留在Bootloader界面这在无人值守的场景下是致命的。第三个细节是App固件搬运。YModem把新的固件完整接收到了W25Q128下载缓冲区但App最终要放在内部Flash的0x08010000处所以需要把固件从SPI Flash读出来写入内部Flash。这个过程不是简单的memcpy因为内部Flash写入前必须擦除且擦除的最小单位是扇区STM32F411的Flash扇区大小不均匀前4个16KB扇区接着1个64KB扇区最后3个128KB扇区。擦除操作不能影响正在执行的代码所在扇区尤其注意如果你的擦写驱动代码恰好放在被擦除的扇区里代码执行直接崩溃。我习惯把Flash驱动和临时缓冲数据都放在Bootloader的固定区域这样不管擦除哪个App扇区驱动代码自身都不会消失。3.3 CRC32校验固件完整性的最后一道防线固件在传输过程中可能因为波特率偏差、电磁干扰、线缆质量等问题产生比特错误。YModem协议自身带CRC16校验这个校验只保证单个数据块在传输链路上的正确性但固件写入内部Flash的过程同样可能出问题。所以完整的校验机制应该是分层级的YModem协议自带CRC16保证串口链路上的数据正确性。固件整个文件在打包时计算一个CRC32值和固件头一起存放。Bootloader在接收完整个固件后重新对下载缓冲区的内容计算CRC32和固件头中记录的CRC32比对只有一致才允许搬运到内部Flash。CRC32的计算不难代码实现可以直接用查表法网上随便搜都能找到完整的查表代码。关键在于计算范围要统一打包时对哪一段数据计算CRCBootloader也必须对相同范围计算CRC。如果打包工具把CRC放在固件文件末尾然后Bootloader对包含CRC的文件整体再算一次CRC那校验永远不过。我这次采用的是自定义固件头的方式结构体定义如下typedef struct { uint32_t magic; /* 固定为0xAA55AA55用于识别合法固件头 */ uint32_t fw_version; /* 固件版本号高16位主版本低16位次版本 */ uint32_t fw_size; /* 固件有效数据长度 */ uint32_t fw_crc32; /* 固件有效数据的CRC32校验值 */ } firmware_header_t;固件头放在固件文件的最前面固定16字节。Bootloader先读取前16字节判断magic是否为0xAA55AA55如果不是就立刻判定固件无效。这样在整个固件传输完成之前Bootloader就能提前判断这次的固件是否合法省去无谓的等待。4. RT-Thread Studio工程配置与代码实现4.1 创建Bootloader工程并配置组件Bootloader工程和App工程在RT-Thread Studio里的创建方式基本一致但在配置上有一些差别。Bootloader工程更注重精简不需要太多系统组件一个内核加上必要的驱动就够了。在RT-Thread Studio中新建RT-Thread项目芯片型号选择STM32F411系列基于开发板创建或者基于芯片创建都可以。工程创建完成后进入RT-Thread Settings页面建议做以下裁剪关闭内核里的RT_USING_CONSOLE是可以的但在开发调试阶段建议保留Bootloader阶段通过控制台打印日志能极大方便调试。保留RT_USING_HEAPBootloader里用动态内存来管理接收缓冲会方便很多。添加SPI驱动框架因为W25Q128挂在SPI总线上。不需要打开DFS文件系统Bootloader阶段没必要挂文件系统直接操作Flash驱动更直接。需要特别提醒的是Bootloader工程的链接脚本和启动文件地址不需要修改因为Bootloader本身就是从0x08000000开始运行的。你只需要在App工程里修改起始地址。串口的配置在这里就显得很关键了。YModem走的是串口和PC端进行通信我使用的是USART1波特率1152008位数据位无校验位1位停止位。RT-Thread的串口驱动框架里需要把串口注册成设备并设置接收回调。YModem是接收方主动驱动的协议设备端必须持续监听串口数据。在RT-Thread Studio里串口设备的初始化通常是在board.h中配置引脚然后在应用代码中使用rt_device_find查找设备用rt_device_open打开设备并设置回调函数。有一个细节在打开串口设备时需要配置DMA接收模式还是中断接收模式。YModem接收需要保证每个字节都不丢中断接收模式在高速传输时可能因为频繁进入中断而增加CPU负担但115200波特率下问题不大DMA接收模式则可以大幅减少CPU占用但接收不定长数据时你要自己处理空闲中断或者定时器超时来判断一帧数据是否结束。这里我建议直接用中断接收模式实现简单代码可读性高115200波特率下CPU占用完全可以接受。4.2 W25Q128驱动对接与SPI Flash抽象W25Q128驱动在系列笔记第三篇中已经详细移植过了这里只强调在OTA工程中对接时需要特别注意的地方。W25Q128的标准SPI接口是四线制CS/CLK/MOSI/MISO最大支持104MHz时钟频率但实际工程中受布线、MCU SPI外设限制通常跑在10MHz~50MHz之间。在RT-Thread环境下W25Q128驱动需要对接的是RT-Thread的SPI设备框架。大体流程是通过rt_hw_spi_device_attach把W25Q128挂载到SPI总线上相当于给SPI总线上的某个片选引脚绑定一个从设备。配置SPI的波特率、数据位、时钟极性CPOL和相位CPHA。W25Q128支持SPI Mode 0和Mode 3即CPOL0/CPHA0或者CPOL1/CPHA1。我习惯用Mode 0也就是空闲时时钟为低电平数据在上升沿采样。调用SPI设备接口rt_spi_send_then_recv来发送命令和接收数据。W25Q128的读操作不用先擦除直接发0x03读命令加上24位地址就能读写操作需要先发0x06写使能命令再发0x02页编程命令每页最多256字节写超过256字节就得按页处理。擦除操作分为4KB扇区擦除0x20、32KB块擦除0x52、64KB块擦除0xD8以及整片擦除0xC7/0x60。对OTA来说4KB扇区擦除是最常用的灵活度高擦除时间一般在45ms~400ms之间。还有一个非常实用的接口是0x9F读ID命令可以用来在启动时验证W25Q128是否正常连接。如果读出的Manufacturer ID不等于0xEFWinbond或者Device ID不等于0x4018对应128Mbit就应该打印错误并停止升级流程避免把固件写到一块不存在的Flash上。这个小检查在量产阶段帮过我大忙——有批板子焊接时W25Q128的引脚虚焊导致SPI通信不稳定如果不去检查设备ID升级过程中会出现各种诡异的写失败错误排查半天还找不到原因。4.3 YModem接收模块的代码实现YModem接收模块是整个Bootloader的核心。我把它封装成独立的C文件对外提供三个接口ymodem_init初始化接收状态机、ymodem_poll在串口收到数据时调用驱动状态机、ymodem_get_result获取最终接收结果。状态机的核心数据结构如下typedef enum { YMODEM_STATE_WAIT_START, /* 等待起始帧 */ YMODEM_STATE_RECEIVE_DATA, /* 接收数据帧 */ YMODEM_STATE_WAIT_EOT, /* 等待结束帧 */ YMODEM_STATE_FINISH /* 传输完成 */ } ymodem_state_t; typedef struct { ymodem_state_t state; volatile uint32_t recv_count; uint32_t file_size; uint32_t recv_size; uint16_t seq; /* 当前包序号 */ uint8_t ack_flag; uint8_t buffer[1024 64]; /* 接收缓冲预留帧头 */ } ymodem_ctx_t;核心流程是这样的初始化时状态机先处于WAIT_START状态此时Bootloader通过串口发送字符C然后把接收到的第一个数据块当作起始帧解析。起始帧里包含文件名和文件大小这里我直接跳过了文件名只解析文件大小用来判断固件是否超出分区容量。接下来进入RECEIVE_DATA状态逐块接收数据。每收完一个数据块先做本地CRC校验校验通过就返回ACK并写入W25Q128下载缓冲区校验失败返回NAK请求重传。写入W25Q128时有一个性能优化点YModem一个数据块最大1024字节而W25Q128的页编程最大256字节如果把1024字节逐个按页写每次都要等写状态寄存器读状态寄存器0x05直到bit0为0串口中断可能会在等待期间堆积数据。更好的做法是启用RT-Thread的串口DMA接收模式或者在中断回调里直接把数据拷到一个大缓冲然后主循环里批量写入Flash。我在实际工程里采用的方式是在串口中断回调函数中只做内存拷贝然后设置一个标志位告诉主循环“有新数据帧了”主循环拿到标志后再执行YModem协议解析和Flash写入这样把耗时操作移出了中断上下文。在接收完最后一个数据帧后发送方会发EOT设备端回ACK后再收一个空帧最终结束传输。此时整个固件已经在W25Q128的下载缓冲区里可以执行CRC32整体校验了。这里要注意一个细节YModem接收到的数据长度可能和固件头声明的fw_size不一致如果收到的字节数不等于fw_size同样需要判定固件无效。防止极少数情况下发送方提前断开会话导致文件不完整。4.4 App工程需要改动的3个关键位置App工程的改动相对简单主要是三个地方第一个是链接脚本。打开App工程里的.lds文件RT-Thread Studio的链接脚本通常是link.lds或者stm32f411xe_flash.lds将FLASH的起始地址改为0x08010000长度改为0x70000448KB。如果你用的是Keil MDK工具链对应修改分散加载文件的LR_IROM1和ER_IROM1起始地址如果用的是IAR对应修改.icf文件里的ROM地址。链接脚本是App能在正确地址运行的基础这里错了后面全部白搭。第二个是向量表偏移。在App工程的main函数最开始处增加如下代码/* 将向量表重定位到App起始地址 */ SCB-VTOR 0x08010000;代码位置越靠前越好最好在进入RT-Thread系统初始化之前就设置完成。第三个是App内部如果也要用到W25Q128的存储功能比如存参数、存日志要注意App里对W25Q128的分区规划必须和Bootloader保持一致。如果Bootloader认为0x200000是参数区App却把它当成日志区去写那两边数据就会互相踩踏。我建议把分区表定义在一个公共头文件里Bootloader和App工程都引用同一个头文件从源头避免不一致。4.5 升级标志的写入与App端实现升级标志一般由App写入。比如App提供一个菜单、一个按键处理函数或者收到远端服务器指令后在重启前调用ota_enter_update()函数该函数先备份当前固件其实就是把内部Flash的App区内容读出来写入W25Q128的备份区然后在参数区写入升级标志最后调用系统软复位NVIC_SystemReset使MCU重启进入Bootloader。这一步里的固件备份要重点说。很多初做OTA的人会问为什么升级前还要把旧固件备份一份直接覆盖不行吗答案是不行。升级过程中任何一个环节出问题断电、通信中断、Flash写失败都可能导致新固件无法启动。如果没有旧固件备份设备就只能返厂烧录这在工业场景中是不可接受的。有了备份区Bootloader在发现新固件校验失败后试着把Flash里残存的App区擦掉再把备份区固件原样写回设备就能恢复到升级前的可用状态。这就是OTA设计中常说的“回滚机制”。App与Bootloader之间的通信也可以通过固件版本号来配合。Bootloader在升级完成后可以把新版本号记录到参数区App启动后读取参数区版本号和自身编译时写死的版本号比对如果不一致说明上次升级可能被异常中断了App可以主动触发一次回到Bootloader的操作重新尝试升级。这种App与Bootloader联动的机制虽然增加了部分代码量但显著提升了远程升级的可靠性。5. 实操从PC端发送固件完成一次完整升级5.1 准备升级文件的3个关键步骤升级文件不是直接把App编译出的bin文件扔给YModem发出去中间需要做一步打包处理。我这里的打包工具体积很小整体流程分三步第一步编译App工程在RT-Thread Studio的构建配置里启用生成bin文件最终得到rtthread.bin。第二步用打包工具给bin文件加上固件头。工具会读入rtthread.bin计算CRC32填充firmware_header_t结构体字段然后在bin文件前面插入16字节固件头输出app_v1.2.bin这样的固件文件。这里需要重点提醒固件头的CRC32是对原bin文件的数据部分计算的不包括固件头自身Bootloader在校验时要保持同样的计算区间。第三步检查固件文件大小是否小于分区预留的1MB空间。1MB1048576字节如果你的bin本身很大比如超过了1MB说明App工程需要做代码裁剪或者评估分区容量是否足够。在开发阶段这个检查可能显得多余但真正量产时一旦固件体积逼近分区上限每次升级都如同在悬崖边上走钢丝风险极大。5.2 SecureCRT与YModem交互全过程演示PC端发送YModem文件有很多工具我这次使用的是SecureCRT配置方法如下打开SecureCRT新建串口连接选择正确的COM口号波特率115200数据位8停止位1无校验。连接成功后按住MCU复位按键让设备进入Bootloader模式前提是升级标志位有效或者Bootloader在启动时检测到外部触发条件比如某个GPIO保持低电平。Bootloader首先在串口打印一行固件信息和“Waiting for YModem...”的提示然后持续发送字符C。在SecureCRT菜单栏选择“Transfer Manager”或直接按快捷键选择“YModem”在弹出的对话框中选择刚才打包好的app_v1.2.bin文件点击发送。这时SecureCRT就开始通过YModem协议逐块发送数据设备端会实时打印接收进度比如Recv block: 1024, total: 51200 bytes这种日志。整个交互过程中最容易出现问题的点Bootloader启动后必须等待数秒让PC端的SecureCRT有机会发送起始帧。如果两边一个急着一个等着会出现起始帧还没收到Bootloader已经超时跳去了App的情况。解决方法是Bootloader在进入YModem等待状态后给足接收窗口比如等60秒超时或者在打印提示后延时10秒再开始主动发送C给PC端操作留出时间。传输过程结束后Bootloader会打印校验结果和写入内部Flash的进度。日志样例大概长这样YModem: receive start... YModem: file size 163872 YModem: block received, total 71680 YModem: block received, total 126976 YModem: block received, total 163840 YModem: transfer complete, verify crc32... YModem: crc32 verify ok Flash: erase app area... Flash: write app area... Flash: write done, jump to app...最后一行打印完成后设备会自动跳转到App看到App的启动日志就说明整套OTA链路已经打通。5.3 升级流程中的版本管理与状态验证成功的OTA不仅仅是“能升级”还要能确认“这次升级是真的成功”。我在参数区的结构体里设计了升级状态的四个阶段UPGRADE_READY准备升级、UPGRADE_DOWNLOADING接收固件中、UPGRADE_VERIFYING校验中、UPGRADE_DONE升级完成。每次进入Bootloader先读取参数区状态根据当前所处阶段决定下一步动作。举个例子如果在接收固件到一半时设备断电重启后Bootloader发现状态还是UPGRADE_DOWNLOADING且下载缓冲区里只有部分数据、CRC校验不过就会直接判定为一次失败的升级自动从备份区恢复旧固件然后清除升级状态标志。App启动后也要主动告诉用户或运维系统当前运行的固件版本。可以把版本号做成设备信息的一部分通过AT指令或者MQTT上报到服务器。这样即使设备远在千里之外运维人员也能远程得知设备当前跑的是哪个版本有没有升级成功。6. 常见问题、排查技巧与工程经验6.1 YModem常见故障速查表调试OTA的过程中我整理了一份踩坑频率最高的故障清单按发生概率和影响程度排了个序故障现象根本原因排查方法Bootloader发出C后PC无响应COM口号选错、波特率不对检查设备管理器中的COM口SecureCRT连接属性里确认波特率是115200发送后一直重传NAKSPI Flash写入失败或CRC算法不一致用逻辑分析仪查看MOSI/MISO上是否有数据回传用调试串口打印CRC计算区间起始帧被跳过直接进入数据帧解析YModem状态机处理顺序错误在WAIT_START状态下打印收到的第一个字节的值确认0x01SOH是否到来升级完成后App无法跳转VTOR偏移未设置或链接脚本地址未改检查App工程的.lds文件起始地址确认SCB-VTOR赋值在main函数最前面升级中途设备卡死重启串口中断里做了耗时操作Flash写入把Flash写入移出中断回调采用标志位主循环轮询的方式固件校验经常失败固件打包工具和Bootloader的CRC计算范围不一致明确CRC计算范围固件头后的数据部分两端代码保持一致频繁升级后Flash挂掉忽略了扇区擦写寿命W25Q128号称10万次擦写但如果整片反复擦写寿命骤减升级逻辑不要做无意义的整片擦除6.2 中断上下文与Flash写操作的正面对抗前面提到要把Flash写入移出中断回调这一步值得再深入展开因为这个坑我在早期项目里踩了不止一次。一开始我在编码时图方便直接在USART的中断回调函数里调用了W25Q128的写函数结果发现传输过程中每隔一段时间板子就会死机。为什么因为W25Q128的页编程操作需要等待内部状态寄存器翻转这个等待过程消耗的时间虽然只有几毫秒但足够串口把后续的几十个字节数据挤到RDR寄存器里。如果RDR溢出数据直接丢弃YModem这一块的数据帧不完整接收方就会回NAK本来115200波特率下能顺畅传输的链路忽然变得支离破碎。正确做法是设计双缓冲机制。串口中断回调函数只负责把接收的数据拷贝到内存缓冲区然后针对于YModem的数据帧结构当检测到当前帧已经收满可以根据帧头长度字段判断就把整帧数据移交到待处理队列里。主循环定期检查待处理队列一旦发现待处理帧就把它从队列里取出来完成Flash写入和ACK响应。这样做之后串口中断的处理时间被压缩到微秒级就算Flash写入再慢也不会影响串口数据接收。还要注意缓冲区队列的容量至少要能装下一整块YModem数据帧。YModem最大数据块1024字节加上帧头帧尾缓冲建议设置在1.2KB以上如果用两个缓冲交替总内存占用不到3KB对STM32F411来说完全不用担心内存不够。6.3 从源头改进打包工具自动生成版本与CRC很多人做OTA时把打包工具做成一个独立的小程序写完固件还要手动调命令、填参数步骤繁琐且容易出错。我后来写了一个命令行打包工具集成在构建脚本里每次编译完自动调用。工具输入是App编译出的bin文件输出是带固件头的升级文件。它自动处理以下几件事自动从Git分支或构建环境变量读取版本号比如v1.2.3生成fw_version字段。自动计算CRC32。自动检查文件长度超过分区上限直接报错阻止错误固件被生产出来。这样从源头杜绝了“版本号忘改”、“CRC算错范围”这类人为失误。工具代码量很小用Python写几十行就能搞定推荐大家也都这样做别每次都手动敲命令行算CRC太容易出错了。升级包的安全问题简单提一句。如果产品部署在公共场所或者竞争对手容易接触到的环境固件可能被恶意提取和重打包。常见的做法是对固件做加密或签名Bootloader在验签通过后才执行更新。这不属于本文的核心范围但如果做商用量产产品建议在一开始设计的时候就把签名验签模块预留出来Cortex-M4软解RSA1024或者ECDSA P-256大概几百毫秒到一两秒配合OTA完全可接受。6.4 量产阶段容易忽视的几个隐患到这里基本实现了完整可用的BootloaderAppW25Q128的YModem OTA链路但量产和开发是两回事有几个隐患建议在项目交付前逐一排查第一个隐患是Bootloader固件本身的更新问题。Bootloader必须保证足够稳定因为它一旦坏了设备就永远无法升级了只能返厂。所以Bootloader代码要尽量少改动每次发布前做尽量长时间的稳定性测试尤其要验证串口连续传输大文件、断电恢复、擦写循环这些极端场景。第二个隐患是串口引脚被复用的问题。很多MCU的串口引脚同时还映射了其他外设功能如果App初始化时把这些引脚配置成了GPIO输出而用户在Bootloader阶段依赖串口升级那升级时引脚功能可能冲突。所以要确保App和Bootloader对串口引脚的初始化方式一致避免升级模式下的通信被引脚复用破坏。第三个隐患是Flash擦写次数监控。W25Q128的寿命虽然很长但日志反复写入同一地址还是会加速老化。更科学的做法是日志区采用环形写入策略把写负载均衡到整个日志分区而不是每次只盯着一块写。第四个隐患是固件包版本管理的规范化。OTA升级的核心价值在于它可以持续迭代但如果版本管理混乱今天升级到v1.2明天又回退到v1.1后台只会看到一片混乱的版本号。建议建立严格的发布流程每个固件包都带唯一版本号和构建时间设备的升级记录也要能回溯。7. 最后的一点个人体会整套方案做下来我的感受是OTA链路本身的技术难度并不高真正考验人的是那些容易被忽略的细节分区的规划是否留了余量、超时机制是否完备、固件校验是否可靠、异常中断后能否恢复。如果你正在规划自己的OTA方案建议不要一上来就直接写代码先把分区表和状态机画出来哪怕只是几张草稿纸也能帮你避免后面改架构的大返工。我在几次项目的历练中养成的习惯是任何涉及Flash分区的改动优先检查Bootloader和App两边的一致性任何涉及跳转的逻辑优先检查向量表和链接脚本。排查环境问题比写代码更花时间把这几个关键点做好了OTA这条路会顺利很多。
返回列表