FEATURED · 精选文章

基于RT-Thread与Ymodem协议实现STM32L4串口OTA固件升级

发布时间 / 2026/9/5 20:40:37
来源 / 创域科博编辑部
栏目 / 资讯中心
基于RT-Thread与Ymodem协议实现STM32L4串口OTA固件升级 简介本资源是一套基于RT-Thread操作系统的STM32L4系列单片机OTA固件升级完整工程面向嵌入式开发工程师及RTOS进阶学习者解决低功耗物联网设备在无调试器条件下通过串口安全远程更新固件的核心需求。工程以STM32L496为硬件平台深度集成Ymodem协议栈、串口驱动含DMA与中断优化、Flash安全写入逻辑及多任务调度机制覆盖从数据接收、校验解析到固件烧录的全链路实现。压缩包共2000个文件主体为2217个C源码与1887个头文件含HAL库驱动、RT-Thread组件及Ymodem协议实现辅以469份Markdown说明文档、64个Keil与IAR工程配置文件及大量编译中间产物总大小91.98MB结构清晰、模块解耦便于二次开发与移植。目前已有664人学习下载提供可直接编译运行的完整工程、关键流程注释详尽的源码、以及针对STM32L4闪存分区与启动跳转的实操级实现参考。1. 项目概述为什么要在STM32L4上折腾Ymodem OTA最近在做一个基于STM32L496的物联网终端项目设备部署后需要远程更新固件这几乎是所有嵌入式产品的刚需。一开始考虑过HTTP OTA或者厂商自己的私有协议但评估下来对于资源受限的MCU和追求稳定性的工业场景串口Ymodem协议配合RT-Thread的OTA组件是一个相当“香”的组合。这个方案不依赖复杂的网络栈利用设备本身就有的调试串口通过一条USB转TTL线就能完成固件升级可靠性极高。网上关于Ymodem和OTA的资料不少但真正把RT-Thread、STM32L4系列和完整的升级流程打通的详细分享却不多很多细节需要自己踩坑。今天我就把整个实现过程从原理到代码从工具链配置到避坑指南完整地梳理一遍。无论你是刚接触RT-Thread的新手还是正在为L4系列MCU寻找可靠升级方案的老鸟这篇长文都能给你提供一条清晰的路径。简单说这个项目就是在RT-Thread操作系统上为STM32L496同样适用于STM32L4全系列实现一个基于串口的Ymodem协议固件升级功能。你不需要额外的硬件就用你开发板上的那个打印日志的串口比如USART1通过电脑上的终端软件如SecureCRT、Xshell、甚至简单的串口助手发送新的固件文件设备就能自动完成下载、校验、写入Flash并重启运行新程序的全过程。这比用J-Link一个个烧录效率高多了特别适合批量生产后的现场维护和小批量迭代开发。2. 核心方案选型与设计思路2.1 为什么是Ymodem而不是Xmodem或Zmodem说到串口文件传输大家可能还听过Xmodem和Zmodem。Xmodem是最早的协议简单但效率低错误恢复能力弱128字节的包在如今动辄几百KB的固件面前显得力不从心。Zmodem功能强大支持断点续传但协议复杂在单片机上实现开销太大。Ymodem可以看作是Xmodem-1K的升级版它默认使用1024字节的数据包传输效率高并且最关键的是它在会话开始时可以传输文件名、文件大小等信息这对于OTA流程来说至关重要——设备可以提前知道要接收的固件大小从而校验Flash空间是否足够。在资源受限的嵌入式环境中Ymodem在复杂度与功能性之间取得了很好的平衡。RT-Thread的ymodem软件包已经提供了一个相当成熟的实现我们不必从零开始造轮子只需要做好“搭桥”工作把它和底层的Flash驱动、上层的OTA管理逻辑连接起来。2.2 为什么选择RT-Thread的OTA框架RT-Thread提供了一个名为rt_ota的组件它抽象了OTA的核心流程下载、校验、更新。它最大的好处是与文件系统、Flash设备驱动解耦。我们不需要关心固件具体下载到了哪里可以是文件系统也可以是Raw Flash也不需要关心怎么把新固件搬运到应用程序区rt_ota提供了标准的接口。我们只需要实现三个关键部分一个“下载器”对我们来说就是基于Ymodem协议从串口接收数据并写入存储介质。Flash设备操作告诉rt_ota我们的Flash布局哪个区域是Bootloader哪个是APP哪个用于存储下载的临时固件。启动逻辑通常是Bootloader来决定跳转到哪个APP运行。这个框架让我们的工作变得模块化未来如果你想将串口Ymodem升级改为HTTP或蓝牙升级只需要替换掉“下载器”部分核心的校验和更新逻辑无需改动。2.3 STM32L4系列的Flash特性与分区规划STM32L496拥有高达1MB的Flash这对于OTA来说非常充裕。L4系列的Flash通常以2KB为一个扇区Sector写入前必须擦除擦除操作可以按扇区进行。这是设计分区时必须牢记的。一个典型的支持OTA的Flash分区规划如下Bootloader区存放引导程序。它负责检查是否需要升级以及跳转到主应用程序。大小通常为32KB-64KB放在Flash起始地址0x0800 0000。主应用程序区APP存放我们正常运行的固件。我们称之为app分区。备份应用程序区存放新下载的固件。我们称之为download分区。当通过Ymodem接收完新固件并校验通过后Bootloader或OTA组件会将download分区的内容搬运到app分区。参数存储区存放一些系统参数例如当前运行的固件版本、升级标志位等。RT-Thread的EasyFlash组件非常适合用在这里它提供了磨损均衡和掉电保护可以用来可靠地存储“是否需要升级”这个标志。在我的设计中具体分区如下基于1MB Flash分区名起始地址大小用途bootloader0x0800 000032KB引导程序app0x0800 8000480KB主应用程序download0x0808 0000480KB下载固件存储区ef_env0x080F 800032KBEasyFlash环境变量存储区注意app和download分区的大小必须一致且必须是你实际固件大小编译后生成的.bin文件的整数倍同时要考虑到Flash擦除的扇区对齐。480KB是240个2KB扇区是一个对齐的、合理的大小。app分区起始地址从0x0800 800032KB后开始是为了给Bootloader留足空间。3. 工程搭建与关键组件配置3.1 创建RT-Thread工程与基础配置我使用RT-Thread Studio进行开发它针对STM32系列有很好的支持。新建一个基于STM32L496VG芯片的RT-Thread项目。工程创建好后首先通过RT-Thread Settings图形化配置工具打开以下关键配置和软件包使能rt_ota组件在“组件”中找到“OTA”并启用。这会自动引入rt_ota相关的头文件和源码。安装ymodem软件包在“软件包”中心搜索ymodem选择并安装。这个包实现了Ymodem协议的解析。安装EasyFlash软件包同样在“软件包”中心搜索easyflash并安装。我们将用它来保存升级状态。配置串口驱动确保你计划用于Ymodem通信的串口例如uart1的驱动在“硬件”中已正确启用并且DMA接收和发送功能也打开。Ymodem传输数据量大使用DMA可以极大减轻CPU负担避免丢包。3.2 Flash设备与分区表配置这是连接硬件和rt_ota框架的核心步骤。我们需要在项目中创建或修改fal_cfg.h文件FAL: Flash Abstraction Layer RT-Thread的Flash抽象层。首先定义Flash设备。STM32L4的片上Flash在FAL中被称为onchip_flash。// fal_cfg.h #include fal.h /* 定义 onchip_flash 设备 */ extern const struct fal_flash_dev stm32_onchip_flash; #define FAL_FLASH_DEV_TABLE \ { \ stm32_onchip_flash, \ }stm32_onchip_flash这个设备的结构体定义通常在drv_flash_l4.c中RT-Thread Studio为STM32L4生成的BSP包里应该已经包含。接着定义我们之前规划好的分区表// fal_cfg.h /* 分区表 */ #define FAL_PART_TABLE \ { \ {FAL_PART_MAGIC_WORD, bootloader, onchip_flash, 0x08000000, 32 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, app, onchip_flash, 0x08008000, 480 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, download, onchip_flash, 0x08080000, 480 * 1024, 0}, \ {FAL_PART_MAGIC_WORD, ef_env, onchip_flash, 0x080F8000, 32 * 1024, 0}, \ }这里FAL_PART_MAGIC_WORD是一个魔数0表示初始化时不进行任何操作。3.3 初始化顺序与依赖关系在main.c或专门的硬件初始化函数中必须按照严格的顺序进行初始化// 1. 初始化硬件特别是串口和Flash rt_hw_usart_init(); // 初始化串口配置好DMA // 2. 初始化FALFlash抽象层 fal_init(); // 3. 初始化EasyFlash依赖于FAL easyflash_init(); // 4. 初始化OTA框架依赖于FAL rt_ota_init(); // 5. 最后初始化Ymodem OTA业务逻辑 ymodem_ota_init();这个顺序不能乱。FAL是底层Flash操作的统一接口EasyFlash和rt_ota都依赖它。如果先初始化rt_ota而FAL未就绪会导致分区查找失败。4. Ymodem OTA下载器的具体实现4.1 串口Ymodem数据接收引擎ymodem软件包提供了一个回调函数机制。我们需要实现一个rt_ota_download_t类型的结构体它里面最重要的就是一个write函数。当Ymodem协议解析出一个有效的数据包时就会调用这个write函数。核心思路是在Ymodem会话开始时我们打开或创建download分区作为“文件”每收到一包数据就将其写入这个分区会话结束时关闭分区并设置升级标志。以下是下载器实现的关键代码片段// ymodem_ota.c #include rtthread.h #include rtdevice.h #include ymodem.h #include fal.h #include dfs_posix.h // 用于文件操作接口FAL分区可以挂载为文件系统 static struct rt_ota_download *ota_download RT_NULL; static int download_fd -1; // 分区操作句柄 static const struct fal_partition *download_part RT_NULL; /* Ymodem 数据接收回调 */ static int ymodem_ota_write(rt_uint8_t *buf, rt_size_t len) { if (download_fd 0 || download_part RT_NULL) { rt_kprintf(Ymodem OTA: download partition not ready!\n); return -RT_ERROR; } // 将数据写入download分区 ssize_t written fal_partition_write(download_part, current_offset, buf, len); if (written ! len) { rt_kprintf(Ymodem OTA: write failed at offset 0x%08x\n, current_offset); return -RT_ERROR; } current_offset len; return RT_EOK; } /* 启动Ymodem传输的线程或命令函数 */ static void ymodem_ota_entry(void *parameter) { rt_kprintf(\nPlease select the firmware file and start Ymodem upload...\n); // 1. 找到download分区 download_part fal_partition_find(download); if (download_part RT_NULL) { rt_kprintf(FATAL: download partition not found!\n); return; } // 2. 擦除整个download分区Ymodem传输前必须清空 if (fal_partition_erase(download_part, 0, download_part-len) 0) { rt_kprintf(FATAL: erase download partition failed!\n); return; } current_offset 0; // 3. 注册回调函数启动Ymodem接收 // 这里需要调用ymodem软件包提供的接收函数并将ymodem_ota_write注册进去 // 具体函数名可能因软件包版本而异例如 ymodem_recv_file // 假设函数原型为int ymodem_recv_file(int (*write_func)(rt_uint8_t*, rt_size_t)); int result ymodem_recv_file(ymodem_ota_write); if (result RT_EOK) { rt_kprintf(Ymodem OTA: Firmware received successfully. Size: %d bytes.\n, current_offset); // 4. 接收成功设置升级标志使用EasyFlash存储 ef_set_env(ota_update, 1); ef_save_env(); rt_kprintf(Update flag set. System will reboot in 3s...\n); rt_thread_delay(rt_tick_from_millisecond(3000)); rt_hw_cpu_reset(); // 重启系统 } else { rt_kprintf(Ymodem OTA: Transfer failed or canceled.\n); // 清理工作... } download_fd -1; download_part RT_NULL; }实操心得在调用fal_partition_erase擦除整个下载分区时一定要确认分区大小。对于大容量Flash全擦除可能需要几十到几百毫秒期间最好关闭中断或确保系统不会因此卡死。可以在擦除前给用户一个提示。4.2 与rt_ota框架的对接仅仅把固件收到download分区还不够我们需要让rt_ota框架知道有这么一个新的固件并触发其校验和更新流程。通常这个流程由Bootloader在启动时完成。但在RT-Thread的模型中我们也可以在应用程序中主动调用rt_ota的API。一种更清晰的做法是Ymodem下载器只负责接收和存储固件并设置一个标志。真正的固件校验、备份和切换交给一个独立的、高可靠性的OTA代理线程或者在下次重启后由Bootloader完成。在我们的实现中选择在应用程序初始化时检查这个标志// 在main线程或专门的初始化函数中 void check_ota_update(void) { char ota_flag[8] {0}; ef_get_env(ota_update, ota_flag, sizeof(ota_flag)); if (rt_strcmp(ota_flag, 1) 0) { rt_kprintf(Found OTA update flag. Starting verification and update...\n); // 1. 清除标志防止重复升级 ef_set_env(ota_update, 0); ef_save_env(); // 2. 调用rt_ota API进行升级 // 假设我们已经将download分区作为OTA的“固件源” struct rt_ota_partition *src_part rt_ota_partition_find(download); struct rt_ota_partition *dst_part rt_ota_partition_find(app); if (src_part dst_part) { rt_ota_partition_verify(src_part); // 校验固件如CRC32 SHA256 rt_ota_partition_copy(dst_part, src_part); // 将download复制到app rt_kprintf(OTA update successful. Rebooting...\n); rt_thread_delay(100); rt_hw_cpu_reset(); } else { rt_kprintf(OTA Error: Cannot find partitions!\n); } } }重要提示rt_ota_partition_copy操作涉及到对app分区即当前运行的程序区的擦除和写入。在运行中对自己所在的Flash区域进行写操作是极其危险的可能导致程序崩溃。因此更推荐的做法是将这个复制操作放到Bootloader中执行。上面的代码仅作原理演示生产环境慎用。5. Bootloader的设计与实现一个健壮的Bootloader是OTA系统可靠性的基石。它的核心职责是初始化基本硬件时钟、串口、Flash。检查升级标志从EasyFlash分区读取。如果需要升级则校验download分区的固件并将其复制到app分区。跳转到app分区的起始地址执行。5.1 Bootloader的工程配置Bootloader需要是一个独立的工程它应该非常精简只包含最必要的代码。在RT-Thread Studio中你可以新建一个“裸机”工程作为Bootloader。关键配置链接脚本.ld文件将Bootloader的起始地址设置为0x08000000大小限制在32KB内。关闭中断Bootloader中通常不开启复杂的中断系统复制Flash时最好关闭全局中断。包含FAL和EasyFlashBootloader也需要知道Flash分区表并且能读取EasyFlash环境变量。可以将fal_cfg.h和EasyFlash的移植文件单独拷贝到Bootloader工程中。5.2 Bootloader的核心跳转逻辑以下是Bootloader主函数的简化流程// bootloader_main.c int main(void) { // 1. 基础硬件初始化时钟 用于打印的串口 SystemClock_Config(); UART1_Init(); rt_kprintf(Bootloader v1.0\n); // 2. 初始化FAL和EasyFlash fal_init(); easyflash_init(); // 3. 检查升级标志 char ota_flag[8] {0}; ef_get_env(ota_update, ota_flag, sizeof(ota_flag)); if (rt_strcmp(ota_flag, 1) 0) { rt_kprintf(OTA update pending...\n); // 3.1 清除标志 ef_set_env(ota_update, 0); ef_save_env(); // 3.2 找到download和app分区 const struct fal_partition *src fal_partition_find(download); const struct fal_partition *dst fal_partition_find(app); if (src dst (src-len dst-len)) { // 3.3 这里可以添加固件校验例如CRC32 // uint32_t crc calculate_crc(src-flash_device, src-offset, src-len); // if (crc ! expected_crc) { ... error ... } // 3.4 复制固件 rt_kprintf(Copying firmware from download to app...\n); fal_partition_erase(dst, 0, dst-len); // 擦除目标分区 for (size_t offset 0; offset src-len; offset 256) { // 分块写入 uint8_t buffer[256]; size_t len_to_copy (src-len - offset) 256 ? 256 : (src-len - offset); fal_partition_read(src, offset, buffer, len_to_copy); fal_partition_write(dst, offset, buffer, len_to_copy); } rt_kprintf(Firmware update complete.\n); } else { rt_kprintf(OTA Error: Partition error.\n); } } else { rt_kprintf(No OTA update flag found.\n); } // 4. 跳转到主应用程序 rt_kprintf(Jumping to application at 0x%08x...\n\n, APP_START_ADDRESS); jump_to_app(APP_START_ADDRESS); // APP_START_ADDRESS 0x08008000 while(1); // 正常情况下不会执行到这里 }jump_to_app函数需要设置堆栈指针并跳转这是ARM Cortex-M的通用做法typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { pFunction jump_app; uint32_t jump_addr; /* 检查栈顶地址是否合法在RAM范围内 */ jump_addr *(__IO uint32_t*)(app_addr 4); // 复位向量地址 jump_app (pFunction)jump_addr; /* 关闭所有中断 */ __disable_irq(); /* 设置主堆栈指针 */ __set_MSP(*(__IO uint32_t*)app_addr); /* 跳转 */ jump_app(); }5.3 应用程序的向量表重映射为了让应用程序能在0x08008000地址正确运行必须在应用程序工程的system_stm32l4xx.c文件或类似的文件中修改向量表偏移量// 在SystemInit函数中或main函数最开始的地方添加 #define VECT_TAB_OFFSET 0x00008000U // 从Flash起始地址偏移32KB SCB-VTOR FLASH_BASE | VECT_TAB_OFFSET;同时在应用程序的链接脚本中起始地址也要设置为0x08008000。6. 完整操作流程与上位机配合6.1 固件打包与生成主应用程序编译后你需要生成一个.bin文件用于传输。在Keil MDK中可以通过User配置在编译后调用fromelf.exe工具生成。在RT-Thread Studio中编译后直接在Debug或Release文件夹下可以找到.bin文件。确保这个.bin文件的大小不超过你规划的app分区大小。6.2 使用终端软件进行Ymodem传输连接设备用USB转TTL线连接电脑和STM32开发板的对应串口如USART1。打开终端使用SecureCRT、MobaXterm或Xshell等支持Ymodem协议的终端软件打开对应的串口波特率设置为115200与你代码中配置的一致。触发升级在终端中按下你设备上设定的触发按键或者通过串口发送一个特定命令如ota设备会打印提示信息并进入等待接收状态。[I/main] OTA command received. [I/ymodem] Please select the firmware file and start Ymodem upload...发送文件在终端软件的菜单中找到“发送文件”Send File选项协议选择Ymodem注意不是Ymodem-G除非你的代码明确支持然后选择你编译好的.bin文件。等待传输传输过程中终端会显示进度和包号。传输完成后设备端会打印成功信息设置标志并重启。Ymodem OTA: Firmware received successfully. Size: 215040 bytes. Update flag set. System will reboot in 3s...观察升级过程设备重启后Bootloader会检测到标志执行固件复制然后跳转到新应用程序。你可以在串口日志中看到整个过程的输出。6.3 自动化测试脚本对于频繁的测试可以编写一个简单的Python脚本使用pyserial和xmodem/ymodem库来自动化传输过程避免每次手动点击。import serial import os from xmodem import XMODEM def send_ymodem(port, baudrate, filename): ser serial.Serial(port, baudrate, timeout5) def getc(size, timeout1): return ser.read(size) or None def putc(data, timeout1): return ser.write(data) modem XMODEM(getc, putc, modeymodem1k) # 注意使用ymodem1k模式 with open(filename, rb) as file: modem.send(file) ser.close() if __name__ __main__: send_ymodem(COM3, 115200, rtthread.bin)7. 常见问题排查与深度优化7.1 传输失败与数据校验问题Ymodem传输总是中途失败提示CRC错误或超时。排查降低波特率首先尝试将波特率从115200降到57600甚至9600。高波特率在长线或劣质USB转TTL模块上容易出错。检查流控确保串口助手和设备端都没有启用硬件流控RTS/CTS除非你明确配置了。启用DMA确认代码中串口接收和发送都配置了DMA。查询中断方式在高速、大数据量传输时极易因处理不及时而丢包。调整接收缓冲区增大Ymodem包处理任务的栈空间和串口DMA接收缓冲区。逻辑分析仪抓包如果条件允许用逻辑分析仪抓取TX/RX信号看波形是否干净时序是否符合波特率。7.2 升级后程序无法启动问题传输成功重启后设备“变砖”无任何输出。排查向量表地址这是最常见的原因。百分之百确认应用程序的SCB-VTOR已正确设置为0x08008000或你的app分区起始地址。检查system_stm32l4xx.c和链接脚本。中断处理Bootloader中可能打开了某些中断如SysTick在跳转前没有关闭导致应用程序的中断向量表错乱。确保在jump_to_app函数中调用__disable_irq()。堆栈指针Bootloader的跳转代码是否正确设置了主堆栈指针MSP__set_MSP(*(__IO uint32_t*)app_addr);这句必须执行。固件完整性在Bootloader中增加CRC32或SHA256校验。在复制download分区到app分区前先计算download分区内固件的校验和与文件头中预留的校验值可以在编译后通过脚本添加或预知的校验值对比不一致则不执行复制直接跳转旧APP。7.3 Flash操作导致的系统卡死问题在擦除或写入Flash时系统看门狗复位或直接死机。解决关闭中断在执行fal_partition_erase或fal_partition_write等耗时Flash操作时先关闭全局中断__disable_irq()操作完成后再开启__enable_irq()。Flash编程期间CPU会暂停可能导致中断无法及时响应。喂狗如果开启了独立看门狗IWDG在长时间的Flash操作循环中需要定期调用HAL_IWDG_Refresh(hiwdg)喂狗。分块操作不要一次性擦除整个480KB的分区。可以设计为循环擦除多个小扇区在每次擦除间隙处理一下系统任务或喂狗。fal_partition_erase本身支持指定偏移和大小。7.4 优化差分升级与压缩对于GPRS/NB-IoT等低带宽场景传输几百KB的完整固件耗时很长。可以考虑在服务器端生成差分升级包只包含新旧版本之间的差异设备端接收差分包后进行还原。这需要引入如bsdiff/bspatch或hdiffpatch等库。同时可以对固件进行压缩如LZ4、MiniLZO在设备端解压进一步减少传输数据量。这些优化会显著增加代码复杂度和对RAM的需求解压需要缓冲区需要根据项目资源情况权衡。7.5 安全加固签名与验签为了防止恶意固件被灌入设备必须加入签名验签机制。流程如下在发布固件时使用私钥对固件的哈希值如SHA256进行签名并将签名附加在固件文件末尾。在设备端强烈建议在Bootloader中固化一个公钥。Bootloader在复制固件前先计算接收到的固件哈希值再用公钥解密附带的签名对比两者是否一致。一致则证明固件来源可信且未被篡改。可以使用ECDSA椭圆曲线算法它相比RSA在相同安全强度下签名更短更适合嵌入式环境。mbed TLS或wolfSSL等库提供了相关实现。切记私钥必须妥善保管在安全的服务器上绝不能泄露。整个实现过程就像搭积木RT-Thread提供了ymodem、fal、rt_ota、easyflash这些现成且稳定的模块我们的工作就是用正确的“胶水代码”把它们粘合起来并处理好边界情况。最难的部分往往不是代码本身而是对Flash特性、中断、启动流程这些底层机制的理解。希望这篇超详细的梳理能帮你避开我踩过的那些坑顺利在STM32L4上实现稳定可靠的串口OTA。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻