简介:这份资源面向嵌入式与物联网开发者,聚焦STM32与ESP8266组合下的在线OTA升级方案,借助腾讯云物联网平台完成固件远程分发与更新。内容围绕设备上线、固件分发、下载校验、存储管理、烧录更新与状态反馈等关键环节展开,适合已具备单片机基础、希望掌握远程固件升级技能的工程师与学习者参考。压缩包共288个文件,约6.64MB,以54个c源文件与52个h头文件为核心,配合45个d依赖文件、45个o目标文件、42个crf交叉引用文件,以及uvprojx、uvoptx工程配置、hex与bin固件、sct分散加载文件、map映射文件等,完整呈现BootLoader与STM32F103C8_MD双工程的构建脉络。资源已有5980人学习下载,可帮助读者理解串口通信、MQTT连接、固件分区与烧录验证的实现思路,并对照工程结构排查编译与下载问题。
1. STM32+ESP8266在线OTA:为什么你的设备出厂后还能“换脑子”
设备卖出去之后发现固件有 bug,或者客户想要加个新功能,传统做法是派人到现场用 J-Link 重新烧录,成本高得离谱。STM32 搭配 ESP8266 走腾讯云物联网平台做在线 OTA 升级,解决的正是这个痛点:设备联网后自己从云端拉取新固件,校验、写入、重启,全程不用拆机。这套方案适合做物联网终端、共享设备、远程采集节点的嵌入式开发者,尤其是那些已经用 STM32 做主力 MCU、用 ESP8266 做联网模块的项目。核心思路是把 STM32 的 IAP 升级能力和 ESP8266 的 Wi-Fi 通信能力拼在一起,云端负责存固件、发通知,设备端负责下载和搬运。听起来简单,但固件怎么分片、写到哪个 Flash 地址、升级标志位怎么设计、断网了怎么办,每一步都有翻车的可能。下面按实际落地顺序拆开讲。
2. 先搞懂 IAP 和 OTA 的分工:STM32 管搬运,ESP8266 管通信
2.1 IAP 是地基,OTA 是上层建筑
STM32 的 IAP(In-Application Programming)指的是程序自己擦写自己的 Flash,把新固件写到指定区域,然后跳转执行。没有 IAP,OTA 就是空中楼阁。STM32 的 Flash 通常从 0x08000000 开始,我们一般把它切成三块:Bootloader 区、APP 运行区、下载暂存区。Bootloader 放在最前面,负责判断“要不要升级”和“跳到哪个区执行”;APP 区放当前运行的固件;暂存区用来接收新固件,等校验通过后再搬运到 APP 区。
这里有个关键选择:Bootloader 和 APP 的地址怎么分。以 STM32F103C8T6 为例,64KB Flash,常见分法是 Bootloader 占 0x08000000~0x08003FFF(16KB),APP 从 0x08004000 开始,暂存区放在 0x0800C000 往后。分的时候要算清楚 APP 编译出来的 bin 文件大小,别让 APP 区装不下。我一般会在 Bootloader 里留一个升级标志位,存在 Flash 最后一页或者备份寄存器里,上电先读这个标志,决定是跳 APP 还是进升级流程。
OTA 则是在 IAP 基础上加了“远程”两个字。ESP8266 通过 Wi-Fi 连到腾讯云物联网平台,订阅固件升级的 topic,收到通知后把固件分片下载下来,通过串口发给 STM32,STM32 再按 IAP 的逻辑写 Flash。所以整个链路是:云端 → ESP8266 → 串口 → STM32 → Flash。
2.2 腾讯云物联网平台的 OTA 能力怎么用
腾讯云物联网平台提供固件升级服务,你需要在控制台创建产品、设备,然后上传固件包。固件包一般是一个 bin 文件,平台会生成版本号和 MD5 校验值。设备端通过 MQTT 协议订阅$ota/update/${productId}/${deviceName}这个 topic,平台下发升级通知时会带上固件大小、版本号、MD5 等信息。ESP8266 收到通知后,通过 HTTP 或者 MQTT 分片请求固件数据。
常见做法是 ESP8266 用 AT 指令连腾讯云,或者直接烧写支持 MQTT 的固件。如果你用的是 ESP8266 NonOS 开发环境,可以自己写 MQTT 客户端;如果图省事,用 AT 固件也行,但 AT 固件对 OTA 分片下载的支持有限,需要自己拼 HTTP 请求。我一般会选 ESP8266 跑一个轻量 MQTT 客户端,直接和腾讯云交互,拿到固件数据后通过串口透传给 STM32。
提示:腾讯云物联网平台的 OTA 固件包有大小限制,分片下载时每片建议不超过 4KB,否则 ESP8266 的 RAM 扛不住。
2.3 串口协议设计:STM32 和 ESP8266 怎么对话
STM32 和 ESP8266 之间用串口通信,协议要简单可靠。我一般定义几种帧类型:升级通知帧、数据帧、结束帧、确认帧。升级通知帧告诉 STM32 固件大小和 MD5;数据帧携带固件分片,每片带偏移地址;结束帧表示传输完成;确认帧由 STM32 回复,告诉 ESP8266 这一片写成功了还是失败了。
帧格式可以这样定:帧头(2 字节,比如 0xAA 0x55)+ 帧类型(1 字节)+ 数据长度(2 字节)+ 数据(N 字节)+ CRC16(2 字节)。STM32 收到数据帧后,先校验 CRC,再擦写对应 Flash 地址,然后回确认帧。如果校验失败或者写失败,回 NACK,ESP8266 重发这一片。重发次数超过 3 次就终止升级,保留旧固件继续运行。
这个协议看起来简单,但实际调试时最容易出问题的是串口波特率和流控。ESP8266 在下载固件时数据量较大,如果波特率太高(比如 921600),STM32 的串口中断处理不过来会丢数据。我一般用 115200 或者 230400,并且开启硬件流控(RTS/CTS),或者用软件流控(XON/XOFF)。如果硬件不支持流控,就在协议里加窗口机制,STM32 每收到几帧就暂停一下,等处理完再让 ESP8266 继续发。
3. 从零搭建:Bootloader 分区、串口协议和云端固件上传的完整步骤
3.1 Bootloader 工程配置与 Flash 分区
先建一个 Bootloader 工程,用 STM32CubeMX 生成 HAL 库基础代码。关键在链接脚本(.ld 文件)里把 Bootloader 的 Flash 起始地址设为 0x08000000,长度设为 16KB。APP 工程的链接脚本则把起始地址改成 0x08004000,长度根据芯片剩余空间调整。下面是一个典型的 APP 工程 .ld 文件片段:
/* APP 工程链接脚本片段,STM32F103C8T6,APP 从 0x08004000 开始 */ MEMORY { RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K FLASH (rx) : ORIGIN = 0x08004000, LENGTH = 48K }逻辑说明:ORIGIN 改成 0x08004000 后,编译出来的中断向量表偏移也要改。在 APP 的 main 函数开头,调用SCB->VTOR = 0x08004000;把向量表重定位到 APP 区。参数说明:LENGTH 根据芯片实际 Flash 大小减去 Bootloader 和暂存区后得出,别写超了,否则编译会报错或者运行时跑飞。
Bootloader 里要实现几个函数:Flash 擦除、Flash 写入、跳转到 APP。跳转前先关中断,设置主堆栈指针为 APP 区的第一个字,然后跳转到 APP 区的第二个字(复位向量)。代码大概长这样:
/* Bootloader 跳转到 APP 的核心代码 */ typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t jump_addr = *(volatile uint32_t *)(app_addr + 4); pFunction app_entry = (pFunction)jump_addr; __disable_irq(); MSR_MSP(*(volatile uint32_t *)app_addr); app_entry(); }逻辑说明:app_addr 是 APP 区的起始地址,第一个字是栈顶地址,第二个字是复位向量。MSR_MSP 是一个宏,用来设置主堆栈指针。参数说明:调用前要确保 APP 区已经有有效固件,否则跳过去会 HardFault。
3.2 ESP8266 端 MQTT 连接与固件分片下载
ESP8266 这边,如果用 NonOS 开发环境,需要自己移植 MQTT 客户端。腾讯云物联网平台的 MQTT 接入信息包括设备三元组(ProductId、DeviceName、DeviceSecret),连接时用 HMAC-SHA1 算密码。连接成功后订阅 OTA topic,收到升级通知后解析 JSON,拿到固件大小和 MD5。
固件下载可以用 HTTP,也可以用 MQTT。腾讯云支持通过 MQTT 分片下发固件,每片最大 4KB。ESP8266 收到分片后,先存到自己的 Flash 或者直接通过串口转发给 STM32。如果 ESP8266 的 Flash 够大(比如 4MB),可以先存下来,校验完再统一发给 STM32;如果不够,就边收边转发。
下面是一个简化的 MQTT 消息处理伪代码:
/* ESP8266 收到 OTA 通知后的处理逻辑 */ void ota_notify_handler(char *json) { int firmware_size = parse_json_int(json, "size"); char *md5 = parse_json_string(json, "md5"); uart_send_ota_start(firmware_size, md5); // 通知 STM32 准备升级 int offset = 0; while (offset < firmware_size) { int chunk_size = (firmware_size - offset) > 4096 ? 4096 : (firmware_size - offset); char *chunk = mqtt_request_firmware_chunk(offset, chunk_size); uart_send_data_frame(offset, chunk, chunk_size); // 通过串口发给 STM32 if (!wait_for_ack()) { // 等 STM32 确认 retry_count++; if (retry_count > 3) break; continue; } offset += chunk_size; } uart_send_ota_end(); }逻辑说明:先通知 STM32 升级开始,然后循环请求固件分片,每片通过串口发出去并等待确认。参数说明:chunk_size 建议 2048 或 4096,太大 ESP8266 内存不够,太小传输效率低。retry_count 是重试次数,超过 3 次就放弃,避免死循环。
3.3 STM32 端 Flash 写入与升级标志位管理
STM32 收到数据帧后,先判断帧类型。如果是升级开始帧,擦除暂存区,记录固件大小和 MD5;如果是数据帧,把数据写到暂存区的对应偏移;如果是结束帧,计算暂存区数据的 MD5,和云端下发的比对,一致就把升级标志位置位,然后重启。
Flash 写入要注意对齐。STM32 的 Flash 按半字(16 位)或字(32 位)编程,写入前必须先擦除。擦除按页进行,STM32F103 每页 1KB。写的时候用 HAL_FLASH_Program 函数,参数选 FLASH_TYPEPROGRAM_HALFWORD 或 FLASH_TYPEPROGRAM_WORD。
升级标志位我一般存在 Flash 的最后一页,或者用备份寄存器(BKP_DRx)。存 Flash 的好处是不怕掉电,坏处是每次升级都要擦写一页。存备份寄存器更简单,但需要纽扣电池供电,否则掉电丢失。下面是一个写标志位的例子:
/* 在 Flash 最后一页写入升级标志 */ #define OTA_FLAG_ADDR 0x0800FC00 // 最后一页起始地址 #define OTA_FLAG_VALUE 0x5A5A5A5A // 自定义标志值 void set_ota_flag(void) { HAL_FLASH_Unlock(); FLASH_EraseInitTypeDef erase; erase.TypeErase = FLASH_TYPEERASE_PAGES; erase.PageAddress = OTA_FLAG_ADDR; erase.NbPages = 1; uint32_t page_error; HAL_FLASHEx_Erase(&erase, &page_error); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, OTA_FLAG_ADDR, OTA_FLAG_VALUE); HAL_FLASH_Lock(); }逻辑说明:先解锁 Flash,擦除最后一页,写入标志值,再上锁。参数说明:OTA_FLAG_ADDR 要根据芯片 Flash 大小调整,别写到 APP 区去了。OTA_FLAG_VALUE 可以自定义,只要 Bootloader 和 APP 约定好就行。
Bootloader 启动时先读这个标志,如果是 OTA_FLAG_VALUE,就把暂存区的固件搬运到 APP 区,搬运完清除标志,再跳转到 APP。搬运过程也要校验 MD5,防止暂存区数据损坏。
3.4 腾讯云物联网平台固件上传与版本管理
登录腾讯云物联网控制台,进入产品详情,找到“固件升级”模块。先创建固件版本,填写版本号(比如 1.0.1),上传编译好的 bin 文件。平台会自动计算 MD5 和文件大小。然后选择要升级的设备,下发升级任务。设备端收到通知后开始下载。
版本号管理有个坑:APP 固件里要有一个地方记录当前版本号,Bootloader 搬运完新固件后,APP 启动时上报版本号给云端,云端才能知道升级成功了。我一般把版本号存在 APP 区的固定偏移,或者用宏定义编译进去。上报版本号的 topic 是$ota/update/${productId}/${deviceName}/report,payload 里带版本号和升级结果。
注意:腾讯云物联网平台的固件升级任务下发后,设备如果不在线,任务会保留一段时间。设备上线后重新订阅 topic 就能收到。但保留时间有限,别指望离线太久还能收到。
4. 避坑指南:OTA 升级中最容易翻车的 5 个地方
4.1 串口丢数据导致固件损坏
现象:升级完成后设备重启,APP 跑不起来,或者 HardFault。原因:ESP8266 发送固件分片时,STM32 串口中断处理不及时,数据被覆盖,写入 Flash 的固件不完整。解决:降低波特率到 115200,开启硬件流控;或者在协议里加序号和重传机制,STM32 每收到一帧就回 ACK,ESP8266 收到 ACK 才发下一帧。如果还是丢,就在 STM32 端加一个环形缓冲区,中断里只存数据,主循环再处理。
4.2 Flash 擦除地址算错,把 Bootloader 擦了
现象:升级后设备彻底变砖,连 Bootloader 都跑不起来。原因:暂存区或者 APP 区的地址计算错误,擦除时覆盖了 Bootloader 区。解决:在 Bootloader 里加地址检查,擦除前确认目标地址在合法范围内。APP 工程的链接脚本和 Bootloader 里的分区定义要一致,最好用一个头文件统一定义。我一般会在 Bootloader 里硬编码一个分区表,擦除前断言地址在暂存区范围内。
4.3 升级标志位没清,设备反复进 Bootloader
现象:升级成功后设备重启,又进 Bootloader,反复循环。原因:Bootloader 搬运完固件后忘记清除升级标志,或者清除失败。解决:搬运完成后立即擦除标志页,或者在跳转 APP 前把标志改成非 OTA_FLAG_VALUE 的值。另外,APP 启动后也可以主动清一次标志,双保险。
4.4 MD5 校验不通过但没重试
现象:升级过程中网络抖动,固件分片下载不完整,MD5 校验失败,设备直接停在 Bootloader。原因:没有重试机制,或者重试次数太少。解决:ESP8266 端在 MD5 校验失败后,重新请求整个固件或者缺失的分片。STM32 端在暂存区 MD5 校验失败后,不要置升级标志,保留旧固件继续运行,同时通过 ESP8266 上报升级失败。腾讯云平台支持升级失败重试,可以在控制台配置重试策略。
4.5 电源不稳导致 Flash 写入失败
现象:升级过程中设备突然重启,固件损坏。原因:Flash 写入时电流较大,如果电源供电不足,电压跌落会导致写入失败甚至芯片复位。解决:升级前确保电源稳定,最好加一个大电容。另外,Bootloader 里可以在写入前检查电压,低于阈值就拒绝升级。如果设备用电池供电,升级时建议插上充电器。
5. 进阶技巧:差分升级、断点续传和升级成功率统计
5.1 差分升级:只传变化的部分
全量包升级每次都要传整个固件,流量和时间都浪费。差分升级只传新旧固件的差异部分,设备端用差分算法合并出完整固件。常见做法是用 bsdiff 或者 hdiffpatch 生成差分包,设备端集成对应的合并库。STM32 的 RAM 和 Flash 有限,合并时可能需要外部 Flash 做缓存。我一般会在 ESP8266 端做差分合并,因为 ESP8266 的 Flash 和 RAM 比 STM32 宽裕,合并完再通过串口发给 STM32。这样 STM32 端不用改,还是收全量固件。
5.2 断点续传:网络断了不用从头来
设备下载固件到一半断网了,重新连上后如果从头下载,浪费流量和时间。断点续传的思路是记录已下载的偏移,重新连接后从断点继续。ESP8266 端可以在 Flash 里记录当前下载进度,每次请求固件分片时带上偏移。腾讯云物联网平台的 OTA 接口支持按偏移请求,所以实现起来不难。STM32 端也要配合,暂存区里已经写入的数据不要重复擦除,只写缺失的部分。
5.3 升级成功率统计:用数据驱动优化
升级成功率是衡量 OTA 方案好坏的核心指标。我一般会在设备端记录每次升级的结果(成功、失败原因、耗时),通过 MQTT 上报到云端。腾讯云物联网平台有数据统计功能,可以看升级成功率曲线。如果发现某个版本失败率突然升高,就要排查是固件问题还是网络问题。下面是一个简单的上报格式:
{ "version": "1.0.1", "result": "success", "duration": 45, "retry_count": 0, "error_code": 0 }逻辑说明:version 是目标版本号,result 是升级结果,duration 是耗时(秒),retry_count 是重试次数,error_code 是错误码。参数说明:error_code 可以自定义,比如 1 表示串口超时,2 表示 MD5 校验失败,3 表示 Flash 写入失败。云端收到后可以按版本、按设备分组统计。
5.4 一个具体技巧:用双备份区实现无缝升级
如果 Flash 够大,可以搞双备份区:APP 区放当前运行固件,备份区放新固件。升级时新固件写到备份区,校验通过后修改启动地址,下次重启直接跑备份区。这样升级过程中设备不用停机,旧固件继续运行,新固件准备好后一键切换。STM32F103 的 Flash 通常不够,但 STM32F407 或者带外部 Flash 的型号可以这么玩。我做过一个项目用 STM32F407+W25Q128,双备份区加外部 Flash,升级成功率从 85% 提到了 99%。
最后说个血泪教训:OTA 升级的测试一定要覆盖异常场景,比如升级中断电、断网、串口拔线。我早期做的一个项目,实验室测试全过,现场升级时因为电源纹波导致 Flash 写入失败,设备变砖了几十台。后来加了电压检测和双备份区才解决。做 OTA,永远要留后悔药。希望帮到你。
本文还有配套的精品资源,点击获取