ARTICLE DETAIL

资讯详情

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

STM32F411外挂W25Q128实现YModem OTA升级的完整实践

STM32F411外挂W25Q128实现YModem OTA升级的完整实践 前一阵项目上要把升级方案从“必须进DFU模式用烧录器”改成“现场用串口就能刷固件”手里正好是一颗STM32F411板载内部Flash 512KB单独放Bootloader和App倒是没问题坏就坏在升级时没有足够的RAM暂存一整包固件。后来把W25Q128拉进来当片外下载缓存配合RT-Thread Studio里的YModem软件包总算把这条链路跑通了。这套方案改完之后体验很顺所以这篇笔记就专门写带片外Flash的OTAYModem怎么搭、怎么调、以及那些光看文档绝对想不到的坑。这篇笔记适合两类人一类是正在用RT-Thread Studio做Bootloader和OTA但内部Flash空间不够、想引入片外Flash的开发者另一类是手里有STM32F411之类Cortex-M4板子想搞清楚YModem协议、分区规划、跳转逻辑这些事到底怎么落地的朋友。内容以实际工程为主线原理、代码、避坑都会讲到。1. 为什么要给OTA引入片外Flash先想清楚固件数据去哪里的问题很多人第一次做OTA第一反应是“Bootloader从串口收完固件直接覆盖写进App区不就行了”。这个想法在小固件、大Flash的片子上确实能跑但一旦固件体积超过内部Flash余量或者你想在升级失败时保留旧版本继续运行就必须额外找一块“中间存放区”。片内ROM不够用RAM更不够用唯一的出路就是片外Flash。1.1 三种常见OTA分区方案的对比我把做OTA时最常见的三种分区方式整理成了一张表方便你对着自己的硬件资源选型方案存储结构优点缺点方案ABootloader AppBootloader收包后直接写App区逻辑最简单Flash占用最少升级中途断电或写坏会直接变砖无回退能力方案BBootloader App 片内Download区固件先缓存到片内Download区校验后再搬进App区双区结构升级失败可回退需要内部Flash预留Download区小Flash芯片不够用方案CBootloader App 片外Download区固件通过YModem先写入W25Q128校验后搬进App区内部Flash只放Bootloader和App片外空间基本不受限多了一颗SPI Flash的操作逻辑Bootloader代码量稍大我这次选的是方案C。原因很直接STM32F411的512KB Flash需要同时放Bootloader和App如果App本身比较胖比如把GUI资源、协议栈都编进去再抠一块Download区出来就会把App的可用空间压得很紧张。W25Q128片外Flash有16MB拿最前面的几MB当下载缓存绰绰有余剩下的还可以存字库、参数、日志一举多得。1.2 片外Flash在OTA链路中的真实角色很多新手会有一个误解以为片外Download区就是“存放固件备份”的地方。实际上在方案C里它的角色是传输中间缓存YModem一包包收数据先落到W25Q128全部收完后做CRC或MD5校验校验通过后再由Bootloader把整份固件从片外Flash搬进内部App区最后跳转。为什么要绕这么一圈因为YModem是流式的一个包一个包地传如果你收一包就写一包到内部App区一旦中途断电或串口断开App区可能已经被写坏旧版本也丢了板子直接变砖。而有了片外Flash做缓存只要在搬运前确认数据完整就基本不会出现“升级升一半设备废了”的尴尬。数据每一步去哪里、什么时候校验、什么时候搬运都必须清楚这是整个OTA设计里最重要的一张图。2. 分区规划与链接脚本先算账再动手在做任何代码之前先把地址算明白。片上资源、链接脚本、Bootloader的跳转地址、App的中断向量表偏移这几样必须完全对得上否则后面全是玄学问题。2.1 我用的分区表以手里的STM32F411CEU6为例它内部Flash 512KB起始地址0x08000000RAM 128KB起始地址0x20000000。W25Q128挂在SPI1上。分区划分如下分区存储介质起始地址大小用途Bootloader片内Flash0x080000000x20000128KBYModem接收、校验、跳转App片内Flash0x080200000x60000384KB用户业务固件Download区W25Q1280x000000008MBYModem接收固件的临时缓存参数/配置区W25Q1280x00800000剩余存版本号、升级标志、日志等Bootloader留128KB对YModem SFUD这个组合来说是够用的实际编译出来大概在70KB到90KB看你怎么裁剪RT-Thread组件。App区384KB一般业务固件很难超如果超了就要重新平衡Bootloader和App的大小。这里有个硬性规则App的起始地址必须和编译App时的链接脚本一致同时必须和Bootloader跳转的目标地址一致三点少一个都会出问题。2.2 GCC链接脚本如何修改RT-Thread Studio默认用的是GCC工具链链接脚本文件一般在工程的linker_scripts/link.ld不同版本位置略有差异。以App工程为例关键的改动是FLASH的ORIGIN和LENGTHMEMORY { FLASH (rx) : ORIGIN 0x08020000, LENGTH 0x60000 RAM (xrw) : ORIGIN 0x20000000, LENGTH 0x20000 }如果你用的是Keil或者IAR对应的是sct文件或icf文件原理一样。修改完链接脚本后还要处理中断向量表偏移。在STM32F4系列上App需要把向量表搬到新的位置否则中断一进来就跳到错误的地方。在RT-Thread里这个通常在board.c或者系统初始化相关文件里设置#define APP_ADDRESS_BASE 0x08020000 SCB-VTOR APP_ADDRESS_BASE;如果是RT-Thread标准工程你可以在系统初始化函数的开头加上上面的设置确保App一启动就把向量表指过去。注意STM32F4的向量表要求对齐到Flash扇区边界0x08020000正好是合法的扇区边界没问题。2.3 分区的“边界”是最容易出错的地方我建议在做分区表时把Bootloader和App之间留一点余量不要紧挨着。比如Bootloader实际只用了60KB但你给它分64KB或128KB是因为后面OC升级YModem功能时Bootloader大概率还会膨胀。同时Bootloader的结束地址和App的起始地址之间如果有空隙跳转时也不会误伤什么。分完区后我强烈建议你在代码里写一个编译期检查或者至少用注释把分区表放在工程最显眼的地方。这个做法能避免三个月后你自己翻代码时对着地址挠头/* 分区定义 - 必须与链接脚本一致 */ #define BOOTLOADER_ADDR 0x08000000 #define APP_ADDR 0x08020000 #define APP_MAX_SIZE 0x60000 #define DOWNLOAD_FLASH_ADDR 0x000000003. RT-Thread Studio里的工程搭建与组件配置这节说实际配置步骤。RT-Thread Studio的好处是大部分组件可以在图形界面里直接勾选省掉了手工移植的麻烦但勾完之后有些细节还是要自己改。3.1 创建Bootloader工程并裁剪组件第一步用RT-Thread Studio新建一个基于芯片的裸工程芯片选STM32F411CEUx。Bootloader工程要尽量精简因为它只做三件事初始化时钟和串口、初始化W25Q128、接收YModem并跳转。用不到的设备框架组件能关就关。在RT-Thread Settings里我建议的组件配置是勾选SPI驱动并启用SPI1对应W25Q128挂载的SPI总线启用SFUD组件Serial Flash Universal Driver这样W25Q128能被自动识别成块设备在Online Packages里搜索YModem勾选并启用关闭不必要的文件系统、网络协议栈、电源管理组件减少Flash占用和启动时间这里为什么要用SFUD而不是直接写W25Q128的驱动因为SFUD把Flash厂商、型号识别、擦写接口都统一了你配置好SPI之后SFUD能自动识别出W25Q128然后通过统一接口读写。Bootloader里省去一堆Flash驱动适配工作代码量也小很多。3.2 W25Q128在Bootloader里的初始化在RT-Thread里SPI Flash的初始化一般通过rt_sfud_flash_probe完成。你需要先确保SPI设备已经注册到设备框架里然后调用探测函数把W25Q128挂到SFUD上#include sfud.h #include spi_flash_sfud.h #define W25Q128_SPI_DEV_NAME spi10 static void spi_flash_init(void) { /* 将SPI1下的W25Q128注册为名称为W25Q128的块设备 */ if (rt_sfud_flash_probe(W25Q128, W25Q128_SPI_DEV_NAME) RT_NULL) { rt_kprintf(W25Q128 probe failed!\n); return; } rt_kprintf(W25Q128 probe success.\n); }注意spi10这个名字要与RT-Thread的SPI总线注册名一致。在STM32F411的BSP里SPI1总线通常注册为spi1挂在spi1下的0号片选设备对应spi10。如果名字写错SFUD探测会直接失败所以第一次跑的时候可以打印一下SPI总线的枚举结果确认。3.3 YModem软件包的核心调用逻辑RT-Thread的YModem软件包把协议栈封装得很干净你不需要关心包序号、CRC、EOT这些底层细节只需要实现几个回调并启动接收流程。核心流程是这样的调用YModem接收初始化函数注册一个“创建下载上下文”的控制块协议收到起始包时回调里会包含文件名和文件大小此时你在W25Q128上创建一块下载区域每收到一包数据协议调用数据写入回调你把数据写到W25Q128对应偏移传输完成后协议调用结束回调你在里面做校验和状态标记我这边Bootloader里核心调用大概是这个形态简化版static int download_to_external_flash(rt_uint8_t *data, rt_size_t len, rt_uint32_t offset) { return w25q128_write(DOWNLOAD_FLASH_ADDR offset, data, len); } static int download_context_init(const char *file_name, rt_uint32_t file_size) { /* 根据file_size计算需要擦除的扇区数然后擦除下载区 */ return w25q128_erase_download_region(file_size); } static int download_finish_cb(void) { /* 标记下载完成供跳转前校验使用 */ return 0; }YModem协议层的工作就是按包解析、校验、然后按顺序调用这些回调。你全局记录一下当前写入偏移每来一包写一包偏移累加即可。我建议把这三个回调封装在一个结构体里方便后续换HTTP OTA、4G OTA时复用同一套写入和校验逻辑。4. 下载流程与跳转逻辑数据怎么一步步从串口落到Flash再进内存很多教程讲到“跳转”就只给一行函数指针调用好像跳过去就万事大吉。实际上Bootloader在跳转前必须做完整的状态判断和外设清理否则App一启动就会被各种残留状态坑死。4.1 Bootloader主流程的状态机我的Bootloader主流程可以抽象成下面的状态机int main(void) { /* 1. 基础初始化时钟、串口、SPI、W25Q128 */ board_init(); spi_flash_init(); /* 2. 判断是否需要进入YModem升级模式 */ if (need_enter_upgrade_mode()) { /* 3. YModem接收固件写入W25Q128下载区 */ ymodem_receive_to_external_flash(); } /* 4. 校验下载区固件是否完整有效 */ if (download_region_valid()) { /* 5. 从片外Flash搬运到片内App区 */ copy_download_to_app(); set_boot_success_flag(0); } /* 6. 跳转到App */ jump_to_app(); return 0; }进入升级模式的触发条件我一般做两层一是升级标志位比如参数区里某个Magic Word被App写入说明App主动请求升级Bootloader启动后先等YModem二是App区有效性检查如果App区头部标识无效说明当前没有可运行固件也直接进YModem接收这相当于一个“出厂烧录”模式。4.2 从片外Flash搬运到片内App区的细节下载完成后固件还躺在W25Q128里必须搬进内部Flash。搬运的本质是从片外地址读一页擦除内部Flash对应扇区写入一小段重复直到固件全部搬完。这个搬运过程有两个关键点需要特别小心第一内部Flash擦除扇区大小与固件对齐。STM32F411的Flash扇区大小不均匀有16KB的、64KB的、128KB的你搬运前最好按App区的扇区边界做擦除。否则擦多了、擦少了都会导致后面写入错位。第二边写边校验和先整体校验的区别。我的做法是先在W25Q128整包接收完成后做一次CRC32整体校验确保片外缓存区里的文件是完整的然后搬运时再对每页做一次“读回比对”搬运结束后再整体校验一次。多一道读回校验就能在量产时尽早暴露Flash物理坏块或者SPI线序问题。4.3 跳转前的“清场”工作跳转函数本身很简单但简单不代表可以不做准备。我的跳转代码是这样的typedef void (*app_entry_t)(void); void jump_to_app(void) { uint32_t app_entry_addr *(volatile uint32_t *)(APP_ADDR 4); app_entry_t app_entry (app_entry_t)app_entry_addr; /* 1. 关闭全局中断 */ __disable_irq(); /* 2. 反初始化Bootloader用到的外设 */ rt_hw_usart_detach(); rt_hw_spi_detach(); /* 其他外设按需处理 */ /* 3. 设置MSP为主堆栈指针 */ __set_MSP(*(volatile uint32_t *)APP_ADDR); /* 4. 跳转 */ app_entry(); while (1); }这里比较容易被忽略的是第三步。App链接时已经把初始栈顶地址放在它的开头4字节里跳转前必须把主栈指针切到App的栈顶否则App的局部变量和函数调用会乱。还有一个细节跳转前__disable_irq()之后SysTick、UART、SPI的中断都不会再进来但这不能保证外设硬件层面不产生pending请求所以最好在关闭中断前把用到的DMA、UART、SPI全部反初始化干净。关于跳转我遇到过一个很隐蔽的现象App里没开任何中断但是跳过去后偶尔卡死后来发现是Bootloader没关串口DMA接收DMA还在往某个地址写数据把App的变量区给覆盖了。反初始化外设这件事别省。4.4 App侧需要配合什么不是Bootloader改动完了就结束App侧也有两个事情要做第一App的链接脚本必须指向0x08020000否则Bootloader跳过去就是跑飞第二App启动后要主动把向量表设置到0x08020000F4系列需要SCB-VTOR因为默认向量表在0x08000000。有些App里还做了软件复位升级、通过命令触发Bootloader的标志位写入。这些都属于App和Bootloader之间的通信约定建议把标志位放在外部RTC备份寄存器或者片外Flash参数区避免和固件存储区混在一起减少误擦风险。5. 实际踩过的坑三个“很隐蔽但毁所有”的问题每个OTA方案做下来总有几个坑是文档里不写、但是现场调试时能让人折腾到半夜的。我这次就踩了三个按排查链路讲一下你们以后遇到类似的可以直接对号入座。5.1 坑一Bootloader里SFUD探测失败卡死不动现象是Bootloader启动后串口打印了半天然后直接卡在rt_sfud_flash_probe返回NULL的地方。我当时第一反应是SPI引脚配置错了但检查了好几遍CubeMX配置都没问题。真正的排查链路是在probe之前加打印确认SPI总线有没有起来再用逻辑分析仪抓CS、CLK、MOSI、MISO四根线。结果发现CLK一直没有波形这才想起来在初始化SPI总线前得先确保对应GPIO的复用功能被正确配置并且SPI外设的时钟要提前使能。RT-Thread BSP里SPI初始化一般已经写好但不同板子的片选引脚定义不同。如果W25Q128的CS引脚没有手动拉高Flash可能会处于不确定状态probe就会失败。解决方法是确认板级初始化里CS引脚的正确配置或者在spi_flash_init前先把CS拉高一次。5.2 坑二YModem收完固件校验通过跳转后App跑飞这个坑最典型也更难排查。现象是YModem进度走满、CRC校验过了但跳转过去App一运行发现中断不进或者主循环乱跳。一开始我怀疑是链接地址没对齐重新查了链接脚本和SCB-VTOR没问题。后来我把App文件大小打出来再对比YModem实际接收到的字节数发现差了1024。排查链路是YModem协议在发完文件内容后会发一个结束包协议栈接收时把所有包都算进偏移里了但当最后不足一包的数据补零填充时接收字节数会被算成按包长对齐的整数倍而文件真实大小在起始包里。解决方式也很简单在download_context_init里从起始包的file_size字段拿到真实文件大小接收完成回调结束时用这个真实大小去搬运和校验不要把“已收包数乘以包长”当成实际固件长度。5.3 坑三跳转后App启动即死机但用调试器单步又能跑这个现象最邪门。跳转后直接死机复位一次就好了而且只要用调试器加载调试就一切正常。查到最后发现是看门狗问题Bootloader里开启了IWDG跳转到App后需要几十毫秒才能跑到喂狗代码。如果Bootloader里喂狗周期设置得太短等App启动时刚好超时就直接被复位了。问题根因不在App也不在跳转而是Bootloader退出前没有处理看门狗。解决方式是在跳转前把IWDG刷新周期拉长或者直接关闭IWDG等App启动后再重新初始化。更保险的做法是Bootloader里如果开了IWDG在跳转前先刷新一次给App留够启动时间。这三个坑其实都指向同一个经验OTA不是“Bootloader写对就行”而是Bootloader、App、外设状态、协议细节四者咬合的过程。每一个边界条件都值得单独打日志验证。6. 实测结果与整包升级验证从下载到跳转的完整过程调试期间我做了很多次完整的升级测试这里给出一组有参考价值的实测数据都是基于STM32F411CEU6 W25Q128 串口115200波特率的环境测试项数据App固件大小约236KBYModem包大小1024字节YModem-1K波特率115200固件上传耗时约7分20秒CRC32校验耗时约0.3秒片外Flash搬运到片内耗时约12秒整体升级成功率连续15次升级通过115200波特率下236KB固件传7分钟看起来很久但这是串口YModem的物理上限。如果产线上追求效率可以尝试把波特率提到460800甚至921600Bootloader和上位机工具同时修改串口配置即可但要注意线材和共地问题长距离传输时高波特率反而容易丢包。升级验证的完整操作步骤我按实际操作顺序放这里第一步修改App版本号比如从v1.0改成v1.1重新编译App工程生成bin文件。App固件里建议在启动时打印版本号方便验证升级是否成功。第二步把App重新生成的支持YModem发送的工具我用的串口工具带的YModem发送功能准备好。Bootloader上电后会先检查升级标志正常情况下直接跳App想主动升级就在App里触发一次“写升级标志并复位”或者按住某个按键让Bootloader进入强制升级模式。第三步Bootloader进入升级模式后用工具发送App固件bin文件。这时能看到YModem进度条走动Bootloader侧打印包序号和写Flash偏移。第四步发送完成后观察Bootloader日志看到CRC校验通过、搬运完成、跳转执行然后App串口打印新的版本号就说明升级链路全部闭环了。整个流程里我还会故意制造一次“中途断电”做可靠性测试在YModem传输到50%时直接断电重新上电确认Bootloader能识别出片外下载区固件未完成不会把它当成有效固件搬运而是重新进入YModem接收模式。这种非正常路径的测试比正常升级成功更能说明方案可靠。7. 一个小建议再做一层抽象后续换升级通道就轻松了这次做完串口YModem OTA后我最大的体会是YModem本身只是升级通道的一种YModem能做的HTTP、4G、USB也能做。真正值钱的不是YModem协议而是Bootloader里那层“接收固件 - 暂存片外Flash - 校验 - 搬运 - 跳转”的抽象流程。所以我在Bootloader里把接收通道和Flash操作拆成了两层。后面如果客户要换成网络升级只需要把YModem回调部分替换成从网络接口收到数据后调用同样的flash_download_write、flash_download_check、flash_download_commit上面的链路完全不用动。W25Q128这片16MB的空间将来除了缓存OTA固件还能放字库、放配置、放日志这些都可以在分区表里一并规划好。如果你也正在做类似的带片外Flash OTA方案建议先把串口YModem这条链路彻底跑通、验证稳定再考虑加网络通道。串口永远是最容易定位问题的通道它能帮你在最简单的模式下把Flash擦写、分区规划、跳转逻辑这些基础模块都打磨扎实。后面换任何传输协议踩坑的概率都会小很多。
返回列表