ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32+ESP8266远程固件升级实战:从Bootloader到OneNET的SOTA方案

2026/10/4 1:12:31 拓冰建站 浏览量
STM32+ESP8266远程固件升级实战:从Bootloader到OneNET的SOTA方案 最近在调一套远程升级方案场景很典型设备已经部署出去了不在手边固件出了问题或者要加功能总不能每次都派个人带ST-Link跑现场。我做的方案是STM32 ESP8266通过WiFi访问OneNET平台的HTTP接口实现SOTA远程程序升级——不需要拆机不需要外接下载器只要设备能联网就能把新固件远程写进单片机的Flash里。这套东西我从零开始跑通中间踩了不少坑比如ESP8266的电源纹波导致反复重启、AT指令的返回解析不完整、固件下载到一半看门狗复位等等。这篇就把完整流程拆开来讲从硬件接线、OneNET平台配置到Bootloader设计、HTTP下载固件的关键代码最后是一份实测踩坑记录。如果你也在做IoT设备远程升级这篇文章应该能帮你少走很多弯路。1. 为什么是ESP8266OneNETSOTA方案的选型逻辑很多第一次做OTA的人会问STM32直接联网不行吗其实STM32本身不带WiFi协议栈除非用带网络功能的型号比如某些系列的以太网或WiFi模组否则就需要外挂一个通信模组。ESP8266之所以是首选不是因为性能最强而是它刚好卡在一个非常舒服的位置成本低、AT指令驱动简单、社区资料极多。1.1 两种常见通信方案的对比有人会考虑用4G模组比如Air724UG或者NB-IoT模组但对我来说纯室内场景WiFi足够了。下面这张表是我做选型时整理的对比方案成本功耗联网难度升级包大小适合场景ESP8266 WiFi低较高WiFi本身功耗大低AT指令即可几十KB适合室内有WiFi覆盖的设备4G Cat.1模组较高高中TCP/HTTP AT指令可大户外移动、无WiFi场景NB-IoT模组中很低中但速率低限制大低功耗、小数据量场景对SOTA来说固件包往往有几十KB到一两百KB如果是带字库、带图库的升级包可能到几百KB。NB-IoT的下行速率拉这个太痛苦了而ESP8266在WiFi信号正常的情况下拉几十KB数据就是几秒钟的事。1.2 为什么网关选OneNET而不是自建服务器其实SOTA的核心逻辑很简单设备端发起HTTP请求查询服务器上有没有新固件有就下载下载完校验然后跳转Bootloader写入固件。服务器端用什么都能实现但我最终选了OneNET原因有三第一OneNET是中国移动的物联网平台稳定性不用怀疑而且对于个人开发者和中小团队是免费开放的。第二它提供了完整的设备接入体系虽然我们只用到它的HTTP接口和OTA固件管理功能但这部分足够健壮。第三它支持直接上传固件并获取下载链接省去了自己搭文件服务器、写鉴权逻辑的麻烦。当然如果你不想依赖第三方平台也可以在自己的服务器上放一个静态JSON文件描述固件信息再放一个BIN文件用于下载。但这样要自己处理域名备案、带宽、并发等问题对于项目初期来说有点重了。需要说明的是OneNET的HTTP接口地址和鉴权方式在不同版本的平台上有差异具体以你注册后的开发者文档为准。但请求模型是一样的携带APIKey查询固件信息然后下载固件数据。2. 硬件接线与ESP8266联网准备先交代一下我用的硬件主控是STM32F103ZET6512KB FlashWiFi模组是ESP8266-01S。之所以用ZET6而不是最常见的C8T6是因为C8T6只有64KB Flash跑一个Bootloader再加上APP固件剩余空间确实紧张而ZET6有512KB可以很从容地划分Bootloader区、APP区和固件暂存区。2.1 供电问题ESP8266比想象中娇气先说最容易翻车的供电。ESP8266射频工作时的峰值电流能到300mA以上如果用STM32开发板上的3.3V引脚直接给ESP8266供电WiFi一发射电压瞬间跌落常见的表现就是模块反复重启、AT指令无响应或者连上WiFi后几秒钟就掉线。正确的做法是给ESP8266单独一路稳压。我用的是一颗AMS1117-3.3输入接5V输出接ESP8266的VCC和CH_PDEN引脚输出端并联两个电容一个100uF电解电容稳住低频一个0.1uF陶瓷电容滤高频。注意地线要单点汇聚别让大电流路径经过STM32的模拟地。CH_PD引脚是使能脚必须拉高否则模组不工作。可以直接串一个10k电阻接到3.3V。2.2 UART连接与电平匹配STM32和ESP8266之间的通信用串口我选择的是USART2避开了USART1用于调试信息打印。连接方式如下STM32 PA2USART2_TX → ESP8266 RXDSTM32 PA3USART2_RX → ESP8266 TXD双方共地这个必须接否则串口通信会不稳定ESP8266的IO电平是3.3V跟STM32的IO电平一致不需要电平转换。如果你用的是5V单片机比如旧版Arduino那就必须加电平转换芯片或者电阻分压不然大概率烧模块。串口参数建议用115200-8-N-1。ESP8266出厂默认波特率通常是115200如果你用了别的波特率记得先AT指令改回来。2.3 第一步联网测试确认模组活着硬件接好之后先别写任何代码用USB转TTL模块把ESP8266单独接到电脑上打开串口助手逐个发AT指令确认状态AT // 返回OK说明模组正常 ATCWMODE1 // 设置Station模式 ATCWJAP你的WiFi名,你的WiFi密码 // 连接WiFi ATCIFSR // 查看获取到的IP地址正常情况下ATCWJAP会返回WIFI CONNECTED然后返回WIFI GOT IP。如果卡在WIFI CONNECTED但拿不到IP检查路由器是否开了MAC地址过滤或者换个2.4G频段试试——ESP8266不支持5G WiFi这点很容易忽略很多人的路由器默认开了双频合一手机连的是5G8266连不上排查了半天才发现是这个原因。3. OneNET平台侧产品、设备、APIKey与固件上传平台侧的配置看起来简单就是点点鼠标但有几个细节如果没搞对后面设备端请求就会一直返回401或404。3.1 创建产品与设备登录OneNET平台创建一个产品产品类型根据实际业务选通信方式选择HTTP或MQTT都可以但因为我们用的是HTTP请求方式下载固件所以设备接入协议这一项要选HTTP或者选直连-HTTP之类的选项不同版本命名不同。创建完产品后在设备列表里添加一个设备记下两个东西设备IDDeviceIDAPIKey这是产品的APIKey不是设备的生成APIKey的地方在产品详情页或者设备详情页里有的版本叫数据流权限记得把读写权限都勾上。后面设备端发起HTTP请求时这个APIKey会放在请求头里面用于鉴权。3.2 上传固件到OneNET找到平台里的固件升级或OTA功能入口新建固件。需要填写的主要信息包括固件版本号建议用数字点比如V2.0.1固件适用设备固件文件.bin文件固件描述可选平台上传后会生成一个下载URL这个URL就是设备端要访问的地址。我在调试时发现固件文件上传后平台会做格式校验有些平台的校验比较严格如果你的bin文件里包含非标准的数据段比如某些加密固件可能传不上去。如果你只是做SOTA验证直接用KEIL或IAR编译生成的bin文件就行不要开启加密选项。还需要注意一点不同平台版本中OTA固件管理的入口名称可能叫固件升级、OTA升级或者远程升级找不到就翻一下文档中心。我这里描述的是通用流程具体菜单位置以你注册的平台为准。3.3 设计固件信息接口虽然OneNET平台自带固件管理但设备端怎么知道有没有新固件呢我这里是用一个HTTP请求去查询固件版本信息。一个可行的做法是在固件信息中把版本号、文件大小、校验值比如CRC32或MD5按约定格式存成JSON或键值对。设备端GET请求这个信息接口解析返回内容。对比本机当前运行版本如果远端版本号不同就继续发起固件下载请求。在OneNET平台上你可以直接用它的API能力来实现这个信息查询。具体接口路径在平台文档里有不同版本不一样我给出一个通用示例伪代码描述请求GET /api/device/{device_id}/firmware/latest Host: 你的OneNET接口域名 api-key: 你的APIKey返回示例{ version: 2.0.1, size: 52124, url: http://xxx/固件下载地址, crc32: A1B2C3D4 }如果你不想依赖平台自带的信息接口也可以把版本信息放到产品的数据流里面用GET /devices/{device_id}/datapoints去查自己约定好数据流名称和值。不管用哪种核心是让设备端能拿到固件的下载URL和校验参数。4. Bootloader架构与APP侧改造如果说ESP8266联网是SOTA的高速公路那Bootloader就是升级动作的搬运工。很多第一次接触OTA的朋友会忽略Bootloader的重要性以为只要把新固件下载下来写进Flash就完事了。实际上没有一套可靠的引导机制升级很容易变砖。4.1 Flash分区规划我把STM32F103ZET6的512KB Flash做了一个严格的分区地址范围大小用途0x08000000 - 0x08004FFF20KBBootloader区0x08005000 - 0x0801FFFF108KBAPP区当前运行程序0x08020000 - 0x0802FFFF64KB固件暂存区下载的新固件先存这里0x08030000 - 0x0807FFFF320KB预留/文件系统等这里有一点想强调不要把下载的固件直接覆盖写到APP区。因为如果下载了一半断电或者数据不完整APP区就已经被破坏了设备直接变砖。正确做法是先下载到暂存区全部下载完成并校验通过后再一次性擦除APP区并写入新固件。极端情况下下载失败旧的APP还在设备只是停留在Bootloader里等待重试不会变砖。4.2 Bootloader的基本流程Bootloader单独烧写在0x08000000它的工作流程很简单上电初始化时钟、串口、看门狗初始化检查一个升级标志位我存在Flash的0x08004FF0地址即Bootloader区末尾这个地址不会被程序代码占用如果升级标志有效进入升级模式连接WiFi、查询版本、下载固件、校验、写入APP区校验通过后清除升级标志跳转到APP区起始地址执行如果升级标志无效直接跳转APP。这样保证了正常启动时几乎零延迟也避免每次开机都去连WiFi导致启动变慢。4.3 APP区的中断向量表偏移这是Bootloader就绪后最容易被坑的一个点。APP如果还按照默认的中断向量表位置0x08000000去取中断向量那它跑起来后一旦发生中断程序跳转的地址还是Bootloader区的向量表而Bootloader区没有对应的中断处理函数程序不知道飞到哪里去了。所以APP工程里面必须设置向量表偏移。在STM32标准库或HAL库中这一行代码放在main函数最开始SCB-VTOR 0x08005000; // 设置为APP区的起始地址在KEIL中还需要修改IROM的起始地址和大小IROM1: Start0x08005000, Size0x1B000这样编译出的APP中断向量表才会位于0x08005000Bootloader跳转过去之后中断才能正常工作。4.4 跳转函数实现Bootloader跳转到APP的函数核心逻辑是取出APP起始地址的前两个32位数据——第一个是初始栈顶指针MSP第二个是复位中断向量Reset_Handler设置MSP后跳转。typedef void (*pFunction)(void); void JumpToApp(uint32_t app_addr) { uint32_t app_stack *(volatile uint32_t *)app_addr; pFunction app_code (pFunction)*(volatile uint32_t *)(app_addr 4); __disable_irq(); // 跳转前务必关闭全局中断 SysTick-CTRL 0; // 关闭系统滴答定时器 HAL_RCC_DeInit(); // 复位RCC时钟配置 __set_MSP(app_stack); // 设置APP的栈指针 app_code(); // 跳转 }注意几个细节跳转前要把Bootloader自身开过的时钟、外设全部恢复默认否则APP初始化时会因为外设状态残留出现问题。__disable_irq()一定要在清零SysTick和DeInit RCC之前还是之后我的经验是先disable再DeInit最后设置MSP并跳转顺序别搞反。跳转后APP的main函数里第一件事就是设置向量表偏移。5. 基于AT指令的HTTP请求从连接服务器到拉取固件数据ESP8266跟OneNET服务器之间的通信我全程用的是AT指令没用SDK二次开发。原因很简单这样STM32端的代码完全不依赖ESP8266的SDK逻辑更清晰调试更方便而且AT指令的HTTP请求足够满足SOTA的需求。5.1 HTTP请求的拼接用ESP8266的AT指令发起HTTP GET有两种路径一种是通过ATHTTPCLIENT这类HTTP专用指令部分固件支持另一种是走TCP透传自己裸写HTTP请求。我推荐后者因为兼容性更好而且能看到完整的响应。TCP方案的步骤是ATCIPSTARTTCP,你的OneNET服务器域名或IP,端口号连接成功后发送数据前需要先发ATCIPSEND数据长度随后等待模块返回提示符立即输入HTTP请求内容。一个完整的HTTP GET请求长这样GET /api/device/123456/firmware/latest HTTP/1.1 Host: api.heclouds.com api-key: 你的APIKey Connection: close注意结尾的空行\r\n\r\n不能少这是HTTP协议区分请求头和请求体的标志。如果用ATCIPSEND发送完请求后需要再发一个16进制数表示结束符0x1A或者根据固件不同用ATCIPSEND发送后不加内容直接发送结束。这里我用的CIPSEND长度要算得刚刚好。一个常见的错误就是长度多算了导致模块一直等在提示符那里超时或者数据粘包。5.2 响应数据的接收与保存服务器返回的HTTP响应前面是响应头和空行后面才是固件的二进制数据。但要注意如果服务器返回的是压缩数据比如gzip那必须让服务器知道设备端不支持压缩所以在请求头里不能加Accept-Encoding: gzip最好显式加一行Accept-Encoding: identity。固件数据的接收方式我用的是ESP8266 TCP透传模式下的IPD机制。每当模块收到串口数据会输出IPD,数据长度:数据STM32串口中断里要做的事情就是解析IPD后面的长度字段把跟随的数据按顺序写入固件暂存区Flash记录累计接收字节数一个很关键的细节进TCP透传模式后模块输出的是裸数据流不加IPD前缀。但我不建议一开始就进透传模式因为需要处理服务器可能先返回的响应头。我的做法是保持AT模式利用IPD分段接收同时解析每段的序号拼出完整报文。虽然效率稍低但每一步都能看到数据调试体验好很多尤其是第一次调这个功能的时候。5.3 下载超时与重启策略固件下载不是瞬间完成的几十KB的数据在WiFi环境下可能要几秒到几十秒。这个过程中要防止两类问题第一是看门狗复位。如果固件下载全程都在一个阻塞循环里看门狗早就把系统复位了。我的做法是设置一个较长的看门狗超时比如10秒在接收数据的串口中断里刷新喂狗。这样只要数据还在流动系统就不会复位。第二是WiFi断开。如果ESP8266中途掉线串口会输出CLOSED或WIFI DISCONNECT。这时必须暂停下载、重新连接WiFi和TCP连接然后重发请求。我建议加上断点续传逻辑每次写入Flash前记录当前数据偏移恢复连接后从偏移处继续请求HTTP的Range头支持这个。不过这样代码复杂度会上升很多如果固件包不大小于100KB直接重新下载也不慢我最终选择了后者省心。6. 固件校验、Flash写入与升级执行固件数据到暂存区之后还不能直接跳过去用必须经过一道严格的校验关。我用的校验方式是CRC32。OneNET平台在固件上传时一般会给出固件的校验值或者你自己算好写进固件描述里设备端下载完成后对暂存区的数据进行一次CRC32计算比对一致才允许写入。6.1 擦除与写入策略STM32的内部Flash是按页擦除的。F103ZET6每页是2KBAPP区108KB共54页。写入的时候是一页一页擦然后以半字16位为单位写入。写入前一定先擦除整个APP区。这里有个细节擦除Flash时CPU是从Flash里面取指执行的所以擦写Flash的代码不能放在被擦除的区域里。我的做法是把擦写函数放到SRAM中执行或者确保这些代码位于Bootloader区Bootloader区不在擦除范围内所以没问题。如果代码在APP区擦写自己所在的Flash会直接触发HardFault。6.2 写入过程中的掉电保护即使做了暂存区方案擦除后写入APP区的过程如果突然断电依然会变砖。为了把变砖概率降到最低我加了一个升级进行中标志擦除APP区之前写入一个值比如0xA5A5A5A5到升级状态标志位写入完成后确认校验通过再把标志位清除Bootloader上电时如果发现升级标志处于正在升级状态说明上次升级中途断电了自动重新下载固件这个标志位的理念跟数据库日志有点像用预写日志保证操作的原子性值得在正式产品里认真实现。6.3 触发升级的两种方式SOTA升级的触发时机我实现了两种上电自动检测Bootloader启动时直接连WiFi查版本有更新就升级。这种方式简单可靠但每次开机都要等几秒网络查询如果网络差就更久。APP主动请求APP运行期间收到用户指令或者定时器触发通过一个特殊的方式我用的标志位软复位让程序重启进入Bootloader升级。这种方式体验好但需要APP和Bootloader配合好。方案2的实现是在APP里定义一段固定的RAM地址比如0x20001000往这里写入一个魔数比如0x4F544131ASCII的OTA1然后软复位。Bootloader启动时检查这个RAM地址的内容如果魔数匹配就进入升级流程。RAM地址不关心掉电复位后RAM内容保留刚好用来跨复位传递信号。7. 实测过程中踩过的坑和最后的建议最后这部分是我个人最有价值的部分——踩坑记录。我按照时间顺序把几个让人头疼的问题列出来每个都写了现象、根因和解决方案希望对你有帮助。7.1 现象一固件下载到一半ESP8266重启这是我最先遇到的问题。下载几百字节后模块就重启串口输出一堆乱码。排查过程是这样的先用万用表测ESP8266的VCC电压下载瞬间电压从3.3V掉到2.7V。问题就在这3.3V跌落超过10%模块自身都保不住了。根源是供电不足。前面提到的AMS1117-3.3能提供800mA电流理论上够用但我的输入是从USB转TTL模块取的5V线材太长、压降大插线接触也松。解决换线加粗电源线并且在ESP8266供电引脚旁边再加一个470uF的电解电容。实测下载过程电压稳定在3.28V以上。7.2 现象二HTTP请求返回401请求能发出去但服务器返回401 Unauthorized。这个问题的排查就一条路请求头里有没有带对的APIKey以及APIKey对应的权限对不对。我犯过的错误是把设备ID和设备APIKey搞混了。OneNET平台上有产品APIKey和多个设备APIKey要用产品APIKey去访问固件下载接口。而且APIKey的值在请求头里的键名是api-key不是Authorization键名写错也会401。7.3 现象三固件下载完写入Flash后无法运行这是最隐蔽的一个坑。新固件写进APP区了Bootloader跳转后完全没反应。排查发现APP区起始地址的4个字节是0xFFFFFFFF——也就是说APP区的写入根本没有成功。问题是Flash写入之前忘了擦除。STM32内部Flash写入只能把1写成0不能把0写成1。如果在没有擦除的Flash上写入数据位完全不对读出来就是0xFFFFFFFF。这个错误很蠢但一旦出现没有经验的人真的会排查很久。我的解决方式很朴素写完之后不要急着跳转先把APP区起始地址的内容读出来对比一下固件开头的栈顶指针值连读4个字节都不对就直接报错省得跳过去了才后悔。7.4 最后的实用建议如果你准备把这个方案搬到自己的项目里我有几条建议Bootloader的串口调试信息一定要留。我在Bootloader里用USART1打印调试日志这样设备每次上电电脑上就能看到它是在正常跳转还是进入了升级流程整个状态一目了然。下载过程能让APP参与就尽量让APP参与。我现在这套方案的最终形态是APP负责下载、校验、写入Bootloader只负责一个跳转动作。这样Bootloader代码量很小出问题的概率也低。先跑通WiFi查询版本再跑固件下载两个步骤不要混在一起调。混在一起调的时候出错了你根本分不清到底是网络问题、HTTP协议问题还是写Flash的问题。做一次断电测试。下载过程中直接拔电源然后再上电看看设备能不能自动恢复。这一步通过你的OTA方案才算真正合格。说到底SOTA升级这件事技术栈并不复杂——一个WiFi模组一套HTTP请求逻辑一个Bootloader一个Flash分区方案。真正考验人的是各种异常情况下的处理能力断网、断电、校验失败、WiFi信号弱。把这些边界条件都处理到位远程升级才不会成为远程变砖。