简介:面向嵌入式开发者,围绕STM32平台基于HTTP协议的固件远程升级实现,解决设备需要通过局域网或公网获取固件更新、提升维护效率的问题。资源包含完整工程,共202个文件,压缩包大小约5.23MB,以h/c源码文件为主,涵盖lwIP等网络协议栈适配、HTTP_GETPkt请求构建、固件接收与MD5/SHA校验、Flash写入等关键环节,另有启动文件、配置文件、编译中间文件及hex/axf可执行镜像,便于直接对照分析整个升级流程。目前已有5074人学习下载。开发者可借助其中的HttpDemo主程序、ringbuffer环形缓冲、cjson解析和GPIO控制等模块,快速理解STM32如何通过GET方法获取服务器固件,并参考完成自定义升级方案,适合具备一定嵌入式网络基础、希望实现远程固件更新功能的工程人员。 做嵌入式开发的朋友,大概率经历过这种尴尬:产品部署后想升级固件,结果只能带着烧录器去现场拆壳连线。STM32的HTTP远程升级,就是让设备通过网络自己完成固件更新的能力——单片机通过HTTP协议从服务器拉取新的bin文件,写入内部Flash,重启后由Bootloader引导进新程序。这篇文章是我在实际项目里跑通这套方案后的完整复盘,从方案选型、Flash分区、Bootloader设计到HTTP下载流程和常见坑,一次讲清楚。适合正在给STM32产品做OTA能力、或者想了解远程升级完整技术路径的开发者参考。
先说结论:STM32做HTTP远程升级,本质上就是IAP(In-Application Programming)的工程化应用,核心在于“一个常驻的Bootloader + 一个允许自改写的App区 + 一条可靠的下载通道”。把这三件事做对,升级功能就能稳定跑起来。
1. 远程升级方案选型:为什么多数项目盯上HTTP
1.1 IAP升级的几种通道对比
STM32的IAP升级通道其实很多,串口、USB DFU、CAN、以太网、Wi-Fi、4G模组都能做,但实际项目中大家越来越倾向HTTP,不是没有原因的。
| 升级通道 | 典型场景 | 优点 | 痛点 |
|---|---|---|---|
| 串口ISP/UART IAP | 产线烧录、本地维护 | 实现简单、稳定 | 必须物理接触设备,没法远程 |
| USB DFU | 消费类、带USB口设备 | 免驱烧录 | 仍需用户插线操作,不适合无人值守 |
| CAN IAP | 车载、工控总线设备 | 布线方便、实时性可控 | 得搭专用上位机,服务器端复用难 |
| HTTP/Wi-Fi/4G | 联网设备远程维护 | 无需接触设备、可复用现网架构 | 链路可靠性、下载中断恢复要重点设计 |
HTTP方案能成为主流,核心原因是它够“通用”。设备端只需要一个HTTP客户端,服务器端用Nginx、Apache、对象存储甚至一个Python脚本就能撑起来,不用为每类设备定制私有传输协议。工厂里已有的局域网、产品里的Wi-Fi/4G模组,都能直接当成传输通道。
1.2 HTTP方案的整体架构
一个典型的STM32 HTTP远程升级系统,分四层:
- 设备端:STM32 + 网络模组(ESP8266/ESP32/4G Cat.1等),App固件内置HTTP升级逻辑
- 传输层:TCP/IP栈,Wi-Fi或蜂窝网络
- 服务端:HTTP文件服务器,放置版本信息JSON和编译好的bin文件
- 触发方式:设备上电主动查询、定时轮询、或通过消息推送触发升级
这里有个容易搞混的点:HTTP只是“拿文件”的手段,真正决定升级成败的是拿到文件之后怎么处理。很多人第一次做远程升级,以为把串口IAP里的接收函数换成HTTP接收就够了,结果踩了Flash擦写、中断向量表、跳转时钟配置一堆坑。所以下面重点讲架构。
2. Flash分区怎么划:Bootloader、App、下载缓存区
2.1 分区规划与中断向量表重映射
先以STM32F103ZET6(512KB Flash)为例,我常用的分区方式:
| 区域 | 起始地址 | 大小 | 作用 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 上电引导、升级搬运、校验 |
| App区 | 0x08010000 | 384KB | 业务固件 |
| 下载缓存区 | 0x08070000 | 60KB | App运行时下载新固件临时存放 |
| 参数区 | 0x0807F000 | 4KB | 升级标志、版本号、CRC等信息 |
分区有两个硬性要求:一是每个分区起始地址要按Flash扇区对齐(F103是1KB/扇区,F407是16KB/扇区),不然擦除时会殃及邻居;二是App区起始地址偏移必须在编译时写进链接脚本,并同步修改中断向量表。
中断向量表重映射最常用的是SCB->VTOR。F103上可以直接这样操作:
void set_vector_table(uint32_t app_addr) { SCB->VTOR = app_addr; }但如果用的是标准库,部分老型号也可以调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, app_addr)。切记偏移必须按64字节对齐,F0系列芯片还要把VTOR的最低位置1,否则中断跳不进去,程序一开就死。
2.2 Bootloader的核心逻辑与跳转代码
Bootloader是远程升级的“安全气囊”,它的任务不是把工作做完,而是保证“最坏情况下设备还能活”。所以Bootloader里不要放复杂业务逻辑,只做三件事:检查升级标志、搬运固件、跳转App。
最基本的跳转代码如下:
typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_sp = *(volatile uint32_t *)app_addr; pFunction app_entry = (pFunction)*(volatile uint32_t *)(app_addr + 4); // 跳转前务必关闭全局中断,顺手关掉用到的外设 __disable_irq(); for (int i = 0; i < 8; i++) { NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; } SysTick->CTRL = 0; set_vector_table(app_addr); __set_MSP(app_sp); app_entry(); }这里有个不少新手栽过的坑:函数跳转前如果没关闭SysTick中断、串口中断,进入App后由于向量表切换时序问题,极容易触发中断风暴。我在Bootloader里直接清空所有NVIC中断使能和挂起标志,再关SysTick,顺序不能反。
2.3 缓存区方案 vs A/B双备份
分区方案我见过两类:一类是上面这种“下载缓存区”,新固件先临时存放在缓存区,校验通过后再由Bootloader搬到App区;另一类是A/B双备份,App区存两个版本,这次从A启动,下次就升级B,启动失败自动回退另一个。
双备份可靠性最高,但Flash占用几乎翻倍,适合512KB以上Flash的芯片。小容量如STM32F103C8T6只有64KB空间,我更推荐“缓存区+搬运”方案,新固件下载校验通过后再覆盖旧App,只要保证搬运期间不掉电,一样很稳。
3. HTTP升级协议设计:请求什么样的文件、解析什么样的响应
3.1 版本查询与固件下载流程
设备端和服务器端的交互,我习惯定义成两步:
- 第一步:GET version.json,拿版本号和固件下载地址
- 第二步:GET bin文件,下载固件
version.json内容大概是这样:
{ "name": "smart-lamp", "version": "1.1.0", "url": "/firmware/smart_lamp_1.1.0.bin", "md5": "38a1b9c2d4e5f6a7..." }设备上电后先向服务器要这个JSON,解析出version字段,和本地存储的版本号比对。如果服务器版本大于本地版本,再发起下载请求。这一步能避免每次上电都盲目下载大文件,省流量也省时间。
3.2 极简HTTP客户端:不要上重型库
总有人问STM32上该用哪个HTTP库。说句实话,裸机场景下我压根不推荐引入libcurl、cJSON这类重型库,MCU的RAM动不动几十KB,跑不起来。
直接用LwIP或者AT指令模组中的TCP收发接口,手写一个极简HTTP GET就够用。关键在于响应头与正文的切分——HTTP响应里\r\n\r\n之前是响应头,之后是正文数据。对固件下载来说,正文就是bin文件内容,拿到多少写多少Flash即可。
// 伪代码,示意极简HTTP下载过程 http_connect(server_ip, port); http_send_request( "GET /firmware/smart_lamp_1.1.0.bin HTTP/1.1\r\n" "Host: 192.168.1.100:8080\r\n" "Connection: close\r\n" "\r\n" ); http_skip_response_header(); // 读到 \r\n\r\n 为止 while ((len = http_recv(buf, BLOCK_SIZE)) > 0) { flash_program(app_cache_addr, buf, len); }用Connection: close是一个实用技巧,这样服务器发完文件就会主动断开TCP,设备端收到EOF就表示下载完成,省了Content-Length解析的麻烦。不过生产环境最好还是解析一下Content-Length,用于判断下载是否完整。
可能有人会问HTTP和HTTPS的区别。简单说,HTTPS把明文改成了TLS加密通道,但代价是握手耗时、RAM占用、还需要预置根证书。很多STM32项目和Wi-Fi模组只支持HTTP,或者用私有协议做加密。我的建议是:产品公开发布且涉及敏感数据的,至少上HTTPS或对固件做AES加密+签名;内网环境或数据不敏感的,HTTP配合MD5/CRC校验实用性已经很好了。
4. STM32 + ESP8266实操:从AT指令到Flash写入
4.1 硬件与开发环境准备
我实际验证用的硬件是STM32F103ZET6最小系统板 + ESP8266-01S模块,串口2和模组通信,串口1留给日志输出。开发环境用的Keil MDK标准库,后来也试过VSCode + EIDE + ARM GCC的组合,两者编译出来的bin文件在Bootloader面前没有区别,环境看个人习惯。
一个小建议:用VSCode写代码、Keil下载调试,两边共用工程文件夹会方便很多。Keil里记得在Options for Target的Linker选项卡里勾选Use Memory Layout,然后通过修改.sct或者直接在图里的IROM1起始地址填0x08010000、大小填0x60000,让App编译时知道自己将被放在偏移处。
4.2 AT指令流程与关键代码
ESP8266使用AT固件时连接TCP并发送HTTP请求的流程是固定的:
AT+CWMODE=1 // 设置为Station模式 AT+CWJAP="SSID","PASSWD" // 连接Wi-Fi AT+CIPSTART="TCP","192.168.1.100",8080 AT+CIPSEND=120 // 准备发送120字节数据发送完HTTP报文后,模组会先回SEND OK,然后被动接收服务器返回的数据。接收数据时有个关键经验:不要按“收到一帧完整的HTTP响应”去解析,因为ESP8266会把响应切成一包一包地发过来,你应该维护一个状态机,先找\r\n\r\n跳过响应头,后面收到的每一包数据都视为固件内容,直接按地址写入Flash缓存区。
写Flash的代码要注意标准库和HAL库的差异。标准库下是FLASH_Unlock/FLASH_ProgramWord,HAL库是HAL_FLASH_Program,中间还要处理Cache清理。以标准库为例:
FLASH_Unlock(); FLASH_ErasePage(page_addr); for (i = 0; i < len; i += 4) { FLASH_ProgramWord(addr + i, *(uint32_t *)(buf + i)); } FLASH_Lock();写入前务必确认flash地址是4字节对齐,写入的数据长度也按4字节对齐,不然会进HardFault。每次擦除扇区前先算好当前地址落在哪个扇区,别把已写入数据的扇区误擦了。
4.3 下载完成后的处理
下载完成后不要立刻跳车。先在缓存区做MD5或CRC32校验,对比服务器返回的MD5值,通过后写“升级请求标志”到参数区,再执行复位进入Bootloader。Bootloader里检测到标志后,把缓存区数据整体搬到App区,搬运完成清标志,最后跳转。
有人问为什么不在App里直接写App区,非要搬一次。因为App运行时代码正在Flash执行,把自己的运行区覆盖掉,小概率烧录现场就把自己写崩了;先缓存后搬运是工程上最稳的做法。实测下来,64KB固件在F103上搬运耗时约2秒,全程关闭中断,非常安全。
5. 升级失败的典型问题与排查实录
5.1 调试器连不上目标——no stm32 target found
这个提示常见于ST-LINK或J-LINK连不上芯片,报错类似“error: no stm32 target found”。排查顺序建议是:
- 检查SWDIO/SWCLK接线,是否有虚焊、杜邦线接触不良
- 确认目标板供电,VDD和GND必须是完整的
- 看复位电路,部分板子复位电容太大导致上电后SWD握手超时
- 留意芯片是否开启了Debug Authentication或SWD引脚复用,这两种情况会让调试器连不上,但程序运行正常
我的习惯是:每次板子上电后在串口打印一行日志,能看见日志就说明程序活着,再判断是调试器问题还是芯片状态问题,别上来就焊线。
5.2 下载到一半串口卡死、延时函数不跑
“STM32延时函数delay卡死”这个坑,十有八九是延时函数依赖SysTick中断,但Bootloader跳转前已经把SysTick关了。解决办法是:跳转前不关SysTick的中断响应,但把SysTick的计数器清零、重装值清零。或者干脆App启动后重新初始化SysTick,不依赖Bootloader遗留下来的状态。
5.3 固件能下载但跳转后白屏/跑飞
跳转后跑飞,90%是以下几个原因:
- App工程没改IROM偏移,编译出来的中断向量表跑到了0x08000000
- 跳转前外设中断没关干净,进入App后卡死
- App启动时重新初始化时钟,但Bootloader里配置了不同的PLL参数,切换时钟中间出现短暂不稳定
解决方案:App工程里必须把IROM1起始地址改为App区地址;跳转函数里清空NVIC;App启动代码开头先等时钟稳定再初始化外设。
5.4 网络模组经常收不到数据、升级卡住
ESP8266这类AT模组调试时,先用串口助手直接和模组对话,确认AT、CWJAP、CIPSTART每一步都返回OK,再让单片机接管。不要直接跳到最后一步,否则你分不清是Wi-Fi没连上还是TCP连接失败。
另一个容易踩的是无回显模式。部分AT固件默认开回显,收到“AT”会回复“AT\r\nOK”,如果你的单片机在解析“OK”时没处理回显,解析会乱。稳妥做法是先发ATE0关回显,再走流程。
5.5 常见问题速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 连不上调试器 | SWD接线/供电/复位电容 | 查硬件,用串口日志辅助判断 |
| 编译后下载地址错误 | IROM1未改偏移 | 修改Linker起始地址 |
| 跳转后死机 | 中断向量表未重映射/中断未清 | 配置SCB->VTOR、清NVIC |
| Flash写入HardFault | 地址未对齐/未解锁 | 按4字节对齐、FLASH_Unlock |
| 下载卡一半 | 模组回显干扰/超时太短 | ATE0关回显、加长等待时间 |
| 升级完复位回老版本 | 区分升级标志未写成功 | 检查参数区读写和擦除时序 |
6. 稳健性细节:校验、看门狗与回滚兜底
6.1 固件完整性校验
远程升级最怕下载一个残缺文件,然后又把它写进App区,设备从此变砖。所以校验必须做两道:
- 传输校验:下载过程中,对每个数据包计算累加和或CRC16
- 整体校验:下载完成后,对缓存区整个bin文件计算MD5或CRC32,和服务器端返回的校验值对比
有人只做单包校验,觉得每包都对了整体就对了。实际上HTTP协议本身有TCP做传输可靠性保障,单包校验基本用不上,整体校验才是关键。可以不用MD5那么重,一个CRC32计算在F103上跑64KB固件约几十毫秒,完全可接受。
6.2 固件加密:防抄板与防篡改
项目对安全性有要求的话,可以给固件做AES加密。典型做法是:编译好的bin文件在服务器端加密,Bootloader里内置解密密钥,搬运阶段解密后写入App区。
需要注意的是,AES加密只能防“拿到固件后直接分析”,密钥一旦被读出来就形同虚设。实际产品里我还会配合芯片的UID或者OTP区做密钥绑定,每个芯片的密钥不同,破解成本大幅上升。不过密钥管理是另一个大话题,这里只是提醒:远程升级不等于裸奔,至少要加一层校验。
6.3 看门狗与回滚机制
升级过程中如果程序卡死,没有看门狗就只能等用户断电。所以Bootloader和App里都应该开独立看门狗(IWDG),升级搬运过程中周期性喂狗。
但要小心:擦除Flash是个长耗时操作,F103擦一个1KB扇区大约几十毫秒,如果一擦就超了看门狗喂狗周期,板子直接复位。解决方法是把擦除和喂狗交错进行,每擦一个扇区喂一次狗。
回滚机制我用的是“升级前保存旧版本号”。Bootloader搬运完成后,先对比App区运行起来的版本号与服务器期望的版本号,若不一致且连续重启达到N次,则保持Bootloader不跳转,等待外部干预或自动恢复到升级前的备份版本(如果留了双A区)。
写在实操之后的几点体会
这套STM32 HTTP远程升级方案,我在两个项目上跑过量产,一个是智能灯具控制器,一个是小型网关设备。回头总结,最值钱的经验不是代码本身,而是设计时就把“失败路径”想清楚:下载失败怎么办、校验失败怎么办、跳转失败怎么办。把这些兜底逻辑想透了,升级功能才能真正交给客户去用。
最后分享一个能省很多事的小技巧:新项目开发时,即使还没做远程升级,我也建议把Bootloader和App的分区结构预留好,地址偏移、链接配置、跳转函数提前写好,哪怕先用串口IAP顶着,后面切到HTTP升级只是换一层传输逻辑。不要等产品上线了再回头补,那时候每个改动都是隔着钢丝跳舞。
远程升级做起来不难,做稳了才有价值,这套架构你可以在自己的板子上从Bootloader开始逐步验证,亲自跑通一次,比看十篇文章都有用。
本文还有配套的精品资源,点击获取