新闻详情

新闻详情

首页 / 资讯中心 / 详情

基于CH9121的STM32网络OTA升级与三区备份Bootloader设计

发布时间:2026/9/14 4:52:40来源:尧图网络
基于CH9121的STM32网络OTA升级与三区备份Bootloader设计
简介面向STM32设备远程升级场景QT上位机搭配以太网模块沁恒CH9121实现基于OTA的远程固件更新适合嵌入式开发人员、物联网设备维护者以及需要批量管理固件版本的项目团队借鉴。整套源码分为bootloader引导程序、测试APP程序和QT升级上位机三部分覆盖从设备端引导、固件下发校验、备份恢复到上位机通信控制的完整链路。MCU的FLASH划分为bootloader区、APP备份区和APP主区升级中途断电不会影响已有设备设备重启后可自动从备份区恢复数据到主区这种分区容错设计已用于量产稳定性有保障。资源包共539个文件以C/C源文件h/c/cpp、编译中间文件o/d/crf、KEIL工程配置uvprojx/sct和QT界面工程pro/ui/qrc为主同时包含hex/bin固件、PDF说明和docx笔记文档压缩包12.11MB目录结构清晰便于按模块对照学习和二次开发。目前已有1317人学习下载适合需要快速搭建安全可靠OTA升级方案的中高级开发者参考。1. 从串口烧录到CH9121网络OTA这套三区备份方案怎么选型传统STM32设备出厂后升级固件要么预留JTAG/SWD口让人跑现场要么拆壳接串口。设备数量一多维护成本立刻失控。用沁恒CH9121把MCU的串口转为以太网后配合一个能自恢复的bootloader和QT上位机工程师在办公室就能给远端设备做OTA升级。这套方案的关键并不是网络传输本身而是Flash三区规划bootloader区、APP备份区、APP主区。升级时先把新固件写入备份区重启后由bootloader负责拷贝到主区中途断电也不影响旧固件运行。本文从bootloader启动流程、自定义帧协议、QT客户端实现一直拆到量产掉电测试适合手里已有STM32F0单板又想加OTA功能的开发团队参考。2. bootloader三区划分与Flash拷贝策略2.1 Flash空间规划与升级标志位STM32F0系列Flash通常从0x08000000开始页大小为1KB具体容量取决于型号。这里以64KB容量为例将Flash划分为三个固定区和一个标志字地址分配如下表区域起始地址大小说明bootloader区0x080000008KB上电启动引导不参与升级APP备份区0x0800200028KB新固件先写入本区APP主区0x0800900028KB正式运行区由bootloader拷贝标志字区0x0800E0004字节存储升级完成标志紧跟在APP主区后选择0x0800E000作为标志字地址是因为它位于APP主区尾部且不会占用实际固件空间。实际固件大小必须小于APP区大小我通常还会在链接脚本里限制APP最大长度避免老手改出越界。2.2 bootloader启动流程与备份恢复逻辑bootloader的职责不是接收固件而是“校验跳转恢复”。每次上电依次做以下决策检查APP主区是否有有效固件若有则直接跳转。若主区无效但备份区有效则先将备份区整个拷贝到主区再跳转。若备份区存在升级标志则即使主区有效也要先执行从备份区到主区的拷贝然后清除标志再跳转。// bootloader/src/main.c 核心启动流程 #define BOOTLOADER_START 0x08000000UL #define APP_BAK_START 0x08002000UL #define APP_MAIN_START 0x08009000UL #define APP_MAX_SIZE 0x00007000UL // 28KB #define FLAG_ADDR 0x0800E000UL int main(void) { uint32_t *flag (uint32_t *)FLAG_ADDR; if (check_app_valid(APP_MAIN_START)) { if (*flag 0xAA55A55A) { // 有升级标志说明备份区已写完新固件 if (check_app_valid(APP_BAK_START)) { copy_flash(APP_BAK_START, APP_MAIN_START, APP_MAX_SIZE); *flag 0; // 清除标志 } } jump_to_app(APP_MAIN_START); } else { // 主区坏了从备份区拉一份回来 if (check_app_valid(APP_BAK_START)) { copy_flash(APP_BAK_START, APP_MAIN_START, APP_MAX_SIZE); jump_to_app(APP_MAIN_START); } else { // 备份区也无效停留在bootloader等待上位机重传 } } }check_app_valid通常验证两个点栈顶地址是否落在RAM范围内以及APP的复位向量是否合理。单纯检查首地址数据不够避免Flash里随机数据被误判为固件。jump_to_app需要设置中断向量表偏移并跳转这部分放在代码中一并说明。2.3 Flash擦写实现与拷贝函数的边界处理拷贝函数是整个恢复机制的核心。直接调用标准外设库的FLASH_ErasePage和FLASH_ProgramWord但必须注意每次解锁/上锁以及目标地址必须按页对齐。我一般把擦除和写入封装成带状态检查的函数void flash_erase_page(uint32_t addr) { FLASH_Unlock(); while (FLASH_ErasePage(addr) ! FLASH_COMPLETE) {} // 等待页擦完成 FLASH_Lock(); } void flash_write_word(uint32_t addr, uint32_t data) { FLASH_Unlock(); FLASH_ProgramWord(addr, data); while (FLASH-SR FLASH_FLAG_BSY) {} // 忙等待 FLASH_Lock(); } void copy_flash(uint32_t src, uint32_t dst, uint32_t len) { // 先擦除目标所有页 for (uint32_t off 0; off len; off 0x400) { flash_erase_page(dst off); } // 以4字节为单位从源地址读写入目标地址 for (uint32_t off 0; off len; off 4) { uint32_t word *(volatile uint32_t *)(src off); flash_write_word(dst off, word); } }参数说明src是备份区起始地址dst是主区起始地址len必须小于等于APP区大小。擦除时按页长度0x4001KB递增写入时按字长度0x4递增。若源区和目标区存在重叠比如后续想改成自己覆盖自己的原地升级这个拷贝函数就不能直接用了。另外在copy_flash过程中最好关闭全局中断防止串口中断打断Flash时序。3. 基于CH9121的升级协议与数据包设计3.1 CH9121工作模式选择与串口透传CH9121内部集成TCP/IP协议栈MCU只需要通过UART把数据交给它它就能透明传输到网络端。最常见的搭配是把CH9121配置成TCP Server模式设备端被动监听固定端口QT上位机作为TCP Client主动连接。这样一台设备对应一个IP/端口远程调试时路由器做端口映射即可。CH9121本身的配置工具可以设置工作模式、本地IP、端口和串口波特率。MCU的UART波特率建议设为115200这样单个数据包传输耗时更短也能降低以太网缓冲溢出的概率。需要留意的是CH9121的串口RX缓冲区通常只有几KB如果上位机连续灌数据的速度超过串口吞吐量模块会丢包。所以协议不能做成“发完不管”必须有一包一应答的手工流控。3.2 自定义升级帧格式与CRC16校验网络透传的下层是字节流接收端不知道一帧在哪结束。为此我自己定义了一套简短的升级协议帧头用0xA5 0x5A紧跟类型、长度、数据和CRC16。CRC采用CCITT初始值0xFFFF校验范围从类型字节到数据末字节能够捕获绝大多数由于网络抖动或串口噪声导致的位翻转。// 升级协议帧格式 #define FRAME_HEAD1 0xA5 #define FRAME_HEAD2 0x5A #define FRAME_TYPE_HANDSHAKE 0x01 // 握手帧上位机告知固件大小 #define FRAME_TYPE_DATA 0x02 // 数据帧携带固件内容 #define FRAME_TYPE_FINISH 0x03 // 结束帧校验整体校验和 #define FRAME_TYPE_ACK 0x80 // 应答帧接收方回复 #define FRAME_ACK_OK 0x00 #define FRAME_ACK_ERR_CRC 0x01 #define FRAME_ACK_ERR_ADDR 0x02 typedef struct { uint8_t head1; uint8_t head2; uint8_t type; uint16_t length; // 网络字节序这里直接小端处理 uint8_t data[256]; uint16_t crc; } __attribute__((packed)) upgrade_frame_t; uint16_t crc16_ccitt(uint8_t *data, uint32_t len) { uint16_t crc 0xFFFF; for (uint32_t i 0; i len; i) { crc ^ (data[i] 8); for (int j 0; j 8; j) { crc (crc 0x8000) ? (crc 1) ^ 0x1021 : (crc 1); } } return crc; }使用__attribute__((packed))是为了让结构体成员顺序与字节流完全一致避免编译器对齐插入填充字节。length表示data数组的有效字节数接收端必须校验length与缓冲区剩余字节数是否匹配。3.3 报文类型定义与应答重传机制握手帧用于bootloader告诉上位机自己的bootloader版本和接收能力。数据帧每一帧最多带256字节刚好对齐STM32F0 Flash页的4字节编程单位方便bootloader直接按帧地址写入备份区。结束帧携带整个固件的CRC32bootloader收到后对备份区做整体校验通过后置升级标志。方向类型载荷内容行为上位机 - 设备0x01 握手固件长度、固件CRC32设备回复0x80OK上位机 - 设备0x02 数据偏移地址 有效数据设备写入备份区后回复ACK上位机 - 设备0x03 结束固件CRC32设备校验后置标志并回复设备 - 上位机0x80 应答成功/失败原因上位机根据原因决定继续或终止应答机制很简单上位机发完一帧数据后启动500ms超时定时器收到对应ACK才发下一帧超时重发当前帧连续重发5次仍失败则中断升级任务。这里要注意TCP本身虽然有ACK但它只保证数据到达CH9121不保证MCU已经正确写入Flash所以应用层ACK不能省。4. QT上位机的升级控制与进度管理4.1 固件文件解析与包号计算QT端首先要把待升级的.bin文件读入QByteArray然后按256字节切包。每次发送前更新进度条。文件读取使用QFile一个容易踩的坑是QByteArray在32位系统上最大只能2GB但固件一般只有几十KB所以没有实际影响。bool UpgradeManager::loadFirmware(const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { return false; } m_firmwareData file.readAll(); file.close(); m_packetsCount (m_firmwareData.size() 255) / 256; // 向上取整 m_packetsSent 0; return true; }计算包号时使用(size 255) / 256而不是直接除以256是为了处理最后一包不足256字节的情况。如果固件大小正好是256的倍数这样计算出来的包数仍然正确。4.2 TCP连接管理与超时控制QTcpSocket连接CH9121的监听端口连接成功后先发送握手帧等待设备ACK。为了不让界面卡死所有网络事件都在槽函数里处理发送超时用QTimer控制。实际项目中我通常在UpgradeManager里维护一个简单的状态枚举enum class UpgradeState { Idle, Handshake, Sending, Finish, Failed }; bool UpgradeManager::sendFrame(uint8_t type, const QByteArray data) { if (!m_socket || m_socket-state() ! QAbstractSocket::ConnectedState) { return false; } QByteArray frame; frame.append((char)0xA5); frame.append((char)0x5A); frame.append((char)type); frame.append((char)((data.size() 8) 0xFF)); frame.append((char)(data.size() 0xFF)); frame.append(data); quint16 crc crc16(data); frame.append((char)(crc 0xFF)); frame.append((char)((crc 8) 0xFF)); return m_socket-write(frame) frame.size(); }sendFrame在发送前检查连接状态失败直接返回。这样调用方不需要到处判断socket是否为null。crc16函数使用Qt的qChecksum也可以但为了和单片机端算法完全一致我在PC端也实现了相同的crc16_ccitt。4.3 升级状态机与进度更新信号槽升级流程用状态机来控制连接到设备后进入Handshake状态握手成功进入Sending状态每收到一个ACK就继续发送下一包并发射progressChanged信号。UI上的进度条可以自定义外观比如显示当前速率和剩余时间这部分用QProgressBar重写paintEvent就能实现。void UpgradeManager::onReadyRead() { m_socketBuffer.append(m_socket-readAll()); while (m_socketBuffer.size() 6) { if (m_socketBuffer[0] ! 0xA5 || m_socketBuffer[1] ! 0x5A) { m_socketBuffer.remove(0, 1); // 搜索帧头 continue; } uint8_t type m_socketBuffer[2]; uint16_t len (m_socketBuffer[3] 8) | m_socketBuffer[4]; if (m_socketBuffer.size() len 7) return; // 未收完 QByteArray payload m_socketBuffer.mid(5, len); uint16_t crc (m_socketBuffer[5 len] 0xFF) | (m_socketBuffer[6 len] 8); m_socketBuffer.remove(0, len 7); // 校验crc后按类型处理 handleFrame(type, payload, crc); } }这段代码处理的是粘包和半包问题。每次从缓冲区头部找帧头找到后根据长度字段判断是否凑满一帧。校验通过后调用handleFrame分发到具体处理逻辑。注意len读取的是网络字节序和单片机端保持一致这里直接用大端解析防止跨平台不同架构产生乱码。5. 量产环境下的掉电恢复验证与传输优化技巧5.1 掉电恢复怎么实测量产前一定要做断电注入测试。常见做法是用一个可编程电源或继电器控制设备供电在升级进行到30%、60%、90%时随机断电然后重新上电观察设备是否回到旧固件并恢复运行。测试时我会在bootloader里加一个测试专用函数强制设置主区非法验证从备份区恢复的路径。注意恢复过程中备份区和主区都会有一段时间处于擦写状态如果此时再次断电恢复会被截断但下次上电还会继续恢复直到最终成功。5.2 提高多设备升级速率一套QT上位机同时只能对一个设备升级但通过多线程可以让一个进程管理多个设备。每个UpgradeManager实例放在独立线程中互不阻塞。更简单的做法是直接开多个上位机进程各连一个设备。限制每包256字节是为了配合Flash写入后台可以连续发送多个数据包来利用CH9121的空闲缓冲区但必须保证接收端每写完一包就回ACK否则高频发送只会增加丢包重传概率。实际调试中我发现把QTimer超时设为500ms比较合适太短容易误判太长会让重试等待显得卡顿。5.3 容易被忽略的细节CH9121在上电后需要几百毫秒完成网络初始化而STM32的bootloader可能在模块就绪之前就开始监听串口导致最初的握手帧丢失。我一般会在bootloader里等待串口收到数据超过500ms才认为上位机已连接或者让上位机在连接建立后先睡眠200ms再发握手帧。另一个细节是跳转APP前必须关闭所有用到的外设中断尤其是串口中断和定时器中断否则APP运行时会收到乱帧中断。处理方式是在jump_to_app里直接调用__disable_irq()跳转后由APP的启动代码重新初始化NVIC。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

PostHog Desktop 本地开发环境接入指南:OAuth 配置、区域机制与调试技巧 2026/9/14 5:40:51

PostHog Desktop 本地开发环境接入指南:OAuth 配置、区域机制与调试技巧

PostHog Desktop 本地开发环境接入指南:OAuth 配置、区域机制与调试技巧 【免费下载链接】posthog :hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags,…

阅读更多 →
TanStack Router CatchBoundary 详解:跨 React、Vue、Solid 的错误捕获、重置与默认错误组件 2026/9/14 5:40:51

TanStack Router CatchBoundary 详解:跨 React、Vue、Solid 的错误捕获、重置与默认错误组件

TanStack Router CatchBoundary 详解:跨 React、Vue、Solid 的错误捕获、重置与默认错误组件 【免费下载链接】router 🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more). 项目地址:…

阅读更多 →
Superpowers:开发者效率增强工具集解析 2026/9/14 5:40:51

Superpowers:开发者效率增强工具集解析

我无法基于当前输入生成符合要求的博文。原因在于:您提供的输入内容中,项目标题为 "superpowers",但后续未提供任何实质性的项目正文、关键词列表或摘要描述。整段输入仅包含空的代码块()和孤立的热词标签&a…

阅读更多 →
Univer @univerjs/design 设计系统解析:组件、样式令牌与本地化资源的实现内幕 2026/9/14 5:40:51

Univer @univerjs/design 设计系统解析:组件、样式令牌与本地化资源的实现内幕

Univer univerjs/design 设计系统解析:组件、样式令牌与本地化资源的实现内幕 【免费下载链接】univer Univer is a full-stack framework for creating and editing spreadsheets / word processor / presentation on both web and server. 项目地址: https://gi…

阅读更多 →
refine v3 Chakra UI 布局定制实战:从 LayoutProps 到 ThemedLayout 源码级解析 2026/9/14 5:40:51

refine v3 Chakra UI 布局定制实战:从 LayoutProps 到 ThemedLayout 源码级解析

refine v3 Chakra UI 布局定制实战:从 LayoutProps 到 ThemedLayout 源码级解析 【免费下载链接】refine A React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility. 项目地址: https://gitcode.com/…

阅读更多 →
OpenGL游戏开发框架与核心技术实践 2026/9/14 5:37:50

OpenGL游戏开发框架与核心技术实践

1. OpenGL游戏开发框架概述OpenGL作为跨平台的图形渲染API,在游戏开发领域占据着不可替代的地位。一个完整的OpenGL游戏框架需要包含以下几个核心模块:渲染管线管理:负责顶点着色器、片段着色器等着色器程序的编译链接资源管理系统&#xff1…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞