STM32 HTTP远程升级实战:告别串口烧录,实现OTA固件更新
发布时间:2026/10/19 16:56:37 锦皓数字建站

简介这份资源面向嵌入式开发者与STM32学习者聚焦通过HTTP协议实现设备固件的远程升级解决传统现场烧录维护效率低的问题。压缩包共202个文件约5.23MB以C源码与头文件为核心包含36个.c、38个.h另有编译产物.o、.d、.crf以及Keil工程文件.uvproj、.uvopt、启动文件.s、链接脚本.sct和可执行.hex、.axf等构成一套可直接编译调试的完整工程。资源围绕HTTP_GETPkt()函数展开演示STM32作为客户端发送GET请求、接收服务器分块下发的固件二进制流并涉及lwIP网络栈适配、请求头设置、MD5或SHA-1完整性校验、闪存写入与安全模式重启等关键环节同时提示局域网静态配置与公网NAT、动态域名解析的差异。已有5087人学习下载适合希望掌握TCP/IP协议栈与HTTP远程升级流程的中高级开发者可据此理解客户端与服务器接口的配合方式并借鉴其工程组织与排错思路。1. 串口烧录救不了现场设备HTTP 远程升级才是正解做过嵌入式现场维护的都懂那种无力感设备装在十几米高的塔吊上、埋在农田的监测站里、封在配电柜的角落中固件一旦有 bug唯一的办法就是扛着笔记本和 USB-TTL 跑一趟现场。更别提有些设备压根没留调试口外壳一装就只能拆机。这个基于 STM32 的 HTTP 远程升级项目解决的正是这个痛点——让设备自己通过 HTTP 协议从服务器拉取固件包校验后写入 Flash重启即完成升级。整套逻辑跑在 STM32 上配合以太网或 WiFi 模块不需要上位机、不需要串口线只要设备能联网升级就是一次 GET 请求的事。适合正在做物联网终端、工业网关、远程监测设备的开发者尤其是那些被“现场烧录”折磨过的团队。2. 整体架构拆解从 HTTP 请求到 Flash 写入的完整链路2.1 系统分层与数据流向这个项目的架构可以拆成四层从上到下依次是应用层升级触发逻辑、协议层HTTP 客户端 固件包解析、存储层Flash 驱动 分区管理、硬件层STM32 网络模块。数据流向很清晰设备上电后应用层判断是否需要升级可以定时轮询、也可以由服务器下发通知然后协议层发起 HTTP GET 请求从指定 URL 下载固件二进制文件边下载边写入 Flash 的备份区下载完成后校验完整性最后跳转到 Bootloader 完成搬运和重启。这里有个关键设计决策为什么用 HTTP GET 而不是 POST因为固件包是静态资源放在服务器上就是一个 .bin 文件GET 请求天然适合这种场景而且实现简单不需要处理表单编码。服务器端只需要一个静态文件服务Nginx、Apache 甚至 Python 的 http.server 都行把编译好的 bin 文件放上去就能用。整个链路里最容易被忽视的是内存管理。STM32 的 RAM 通常只有几十到几百 KB不可能把整个固件包缓存下来再写入。常见做法是分块下载每次从 HTTP 响应流里读 1KB 到 4KB 的数据立刻写入 Flash然后继续读下一块。这样 RAM 占用极小但要求 Flash 驱动支持按页写入并且要做好地址偏移的累加。2.2 目录结构一览拿到源码后先看目录结构心里有个地图再动手改stm32-http-ota/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── http_client.h # HTTP 客户端接口 │ │ ├── ota_manager.h # 升级流程管理 │ │ └── flash_if.h # Flash 操作接口 │ └── Src/ │ ├── main.c # 主循环与升级触发 │ ├── http_client.c # HTTP 请求组装与响应解析 │ ├── ota_manager.c # 升级状态机与校验逻辑 │ └── flash_if.c # Flash 擦写驱动 ├── Drivers/ │ ├── STM32F4xx_HAL_Driver/ # HAL 库 │ └── CMSIS/ # 内核头文件 ├── Middlewares/ │ └── Third_Party/ │ └── LwIP/ # 轻量级 TCP/IP 协议栈 ├── Bootloader/ │ └── Src/ │ └── boot_main.c # Bootloader 主逻辑 └── README.md这个结构是典型的 STM32 工程布局Core 放业务代码Drivers 放芯片支持包Middlewares 放第三方协议栈。Bootloader 单独一个目录因为它编译后要烧到 Flash 起始地址和应用程序分开。实际项目中Bootloader 和 App 是两个独立的工程通过链接脚本控制各自的 Flash 起始地址和大小。2.3 Flash 分区规划Flash 分区是整个升级方案的基石分错了后面全是坑。以 STM32F4071MB Flash为例常见做法是区域起始地址大小用途Bootloader0x0800000064KB启动引导负责搬运固件App 运行区0x08010000448KB当前运行的应用程序备份区0x08080000448KB下载的新固件暂存参数区0x080F000064KB存储升级标志、版本号、CRCBootloader 永远不动升级时新固件先写到备份区校验通过后设置升级标志重启后 Bootloader 把备份区的内容搬运到 App 运行区搬运完成清除标志跳转到 App。这样即使升级过程中断电App 运行区的旧固件依然完整设备不会变砖。注意分区大小要根据实际固件体积调整App 运行区和备份区必须一样大且不能有重叠。链接脚本里的 FLASH 长度要对应修改否则编译出来的 bin 文件可能超出分区范围。3. 核心模块实现HTTP 客户端与 Flash 写入的代码细节3.1 HTTP GET 请求的组装与发送先看 HTTP 客户端的核心代码。这里假设底层已经有一个可用的 TCP socket通过 LwIP 或 AT 指令模组实现我们只需要组装 HTTP 请求报文并解析响应。// http_client.c #include http_client.h #include string.h #include stdio.h #define HTTP_BUF_SIZE 1024 /** * brief 发起 HTTP GET 请求下载固件到 Flash * param host: 服务器域名或 IP * param port: 端口号HTTP 默认 80 * param path: 固件文件路径如 /firmware/app_v2.bin * param flash_addr: 固件写入 Flash 的起始地址 * return 0 成功-1 失败 */ int http_download_firmware(const char *host, int port, const char *path, uint32_t flash_addr) { char request[256]; char resp_buf[HTTP_BUF_SIZE]; int sockfd, ret; uint32_t write_addr flash_addr; uint32_t total_len 0; int header_parsed 0; // 1. 建立 TCP 连接 sockfd tcp_connect(host, port); if (sockfd 0) { printf([OTA] TCP connect failed\r\n); return -1; } // 2. 组装 HTTP GET 请求 snprintf(request, sizeof(request), GET %s HTTP/1.1\r\n Host: %s\r\n Connection: close\r\n User-Agent: STM32-OTA/1.0\r\n \r\n, path, host); ret tcp_send(sockfd, request, strlen(request)); if (ret 0) { printf([OTA] Send request failed\r\n); tcp_close(sockfd); return -1; } // 3. 擦除备份区 Flash按扇区擦除 flash_erase_sectors(flash_addr, APP_PARTITION_SIZE); // 4. 循环接收响应数据 while (1) { int len tcp_recv(sockfd, resp_buf, HTTP_BUF_SIZE); if (len 0) break; // 连接关闭或出错 if (!header_parsed) { // 跳过 HTTP 响应头找到 \r\n\r\n char *body strstr(resp_buf, \r\n\r\n); if (body) { body 4; // 跳过空行 int body_len len - (body - resp_buf); if (body_len 0) { flash_write(write_addr, (uint8_t *)body, body_len); write_addr body_len; total_len body_len; } header_parsed 1; } continue; } // 已解析头部后续数据直接写入 Flash flash_write(write_addr, (uint8_t *)resp_buf, len); write_addr len; total_len len; } tcp_close(sockfd); printf([OTA] Download done, total %lu bytes\r\n, total_len); return (total_len 0) ? 0 : -1; }这段代码有几个关键点。第一HTTP 请求头里Connection: close告诉服务器响应完就关闭连接这样我们不需要实现 chunked 编码解析简化了逻辑。第二响应头和数据可能在同一包或不同包里到达所以用header_parsed标志位处理跨包的情况。第三Flash 擦除在下载前完成因为 STM32 的 Flash 只能按扇区擦除不能按字节擦必须先擦后写。参数说明host可以是 IP 也可以是域名如果域名需要底层支持 DNS 解析flash_addr是备份区的起始地址要和分区表一致APP_PARTITION_SIZE是备份区大小擦除时按扇区对齐。3.2 Flash 写入与校验逻辑Flash 写入不是简单的 memcpySTM32 的 Flash 控制器有严格的时序要求。下面是基于 HAL 库的实现// flash_if.c #include flash_if.h #include stm32f4xx_hal.h /** * brief 向指定地址写入数据自动处理扇区边界 * param addr: 目标地址必须是 4 字节对齐 * param data: 数据指针 * param len: 数据长度 * return 0 成功-1 失败 */ int flash_write(uint32_t addr, uint8_t *data, uint32_t len) { HAL_FLASH_Unlock(); for (uint32_t i 0; i len; i 4) { uint32_t word 0; // 处理末尾不足 4 字节的情况 uint32_t remain len - i; if (remain 4) { memcpy(word, data i, 4); } else { memcpy(word, data i, remain); } if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i, word) ! HAL_OK) { HAL_FLASH_Lock(); return -1; } } HAL_FLASH_Lock(); return 0; } /** * brief 计算固件的 CRC32 校验值 * param addr: 固件起始地址 * param len: 固件长度 * return CRC32 值 */ uint32_t flash_crc32(uint32_t addr, uint32_t len) { uint32_t crc 0xFFFFFFFF; for (uint32_t i 0; i len; i 4) { uint32_t word *(volatile uint32_t *)(addr i); crc ^ word; for (int j 0; j 32; j) { if (crc 0x80000000) crc (crc 1) ^ 0x04C11DB7; else crc 1; } } return crc; }flash_write按 4 字节对齐写入因为 STM32F4 的 Flash 编程单位是字32 位。如果数据长度不是 4 的倍数末尾会补零写入这在固件场景下没问题因为 bin 文件本身通常会对齐。flash_crc32用于下载完成后校验固件完整性服务器端在生成 bin 文件时也要计算同样的 CRC 值通过 HTTP 响应头或单独的接口传给设备。注意Flash 写入前必须解锁写完立即上锁否则可能误擦除其他区域。另外写入地址必须在擦除过的扇区内否则写入会失败。3.3 升级状态机与 Bootloader 跳转升级流程用一个状态机管理状态包括空闲、下载中、校验中、待搬运、搬运完成。状态值存在参数区 Flash 里掉电不丢失。// ota_manager.c typedef enum { OTA_IDLE 0, OTA_DOWNLOADING, OTA_VERIFYING, OTA_PENDING, // 下载校验完成等待重启搬运 OTA_DONE } ota_state_t; // 参数区地址最后 64KB 的第一个扇区 #define PARAM_ADDR 0x080F0000 typedef struct { uint32_t magic; // 魔数标识参数有效 ota_state_t state; uint32_t fw_size; // 固件大小 uint32_t fw_crc; // 固件 CRC uint32_t version; // 版本号 } ota_param_t; void ota_set_state(ota_state_t state, uint32_t size, uint32_t crc) { ota_param_t param; param.magic 0x4F544131; // OTA1 param.state state; param.fw_size size; param.fw_crc crc; param.version get_current_version() 1; flash_erase_sectors(PARAM_ADDR, 0x10000); flash_write(PARAM_ADDR, (uint8_t *)param, sizeof(param)); } // Bootloader 中的搬运逻辑 void boot_copy_firmware(void) { ota_param_t *param (ota_param_t *)PARAM_ADDR; if (param-magic ! 0x4F544131 || param-state ! OTA_PENDING) return; // 从备份区搬运到 App 运行区 flash_erase_sectors(APP_ADDR, APP_PARTITION_SIZE); for (uint32_t i 0; i param-fw_size; i 4) { uint32_t word *(volatile uint32_t *)(BACKUP_ADDR i); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, APP_ADDR i, word); } // 校验搬运后的数据 uint32_t crc flash_crc32(APP_ADDR, param-fw_size); if (crc param-fw_crc) { param-state OTA_DONE; flash_erase_sectors(PARAM_ADDR, 0x10000); flash_write(PARAM_ADDR, (uint8_t *)param, sizeof(ota_param_t)); } }Bootloader 的搬运逻辑必须在跳转到 App 之前执行而且搬运过程中不能被打断。实际项目中Bootloader 会先检查参数区的状态如果是OTA_PENDING就执行搬运搬运完成后清除标志最后跳转到 App 的复位向量。跳转代码用汇编或直接操作 MSP 和 PC 指针实现。4. 避坑与排查远程升级最容易翻车的五个地方4.1 下载到一半断电设备变砖现象升级过程中断电重新上电后设备无法启动串口无输出。原因新固件直接覆盖了 App 运行区断电导致 App 区数据不完整Bootloader 又跳转到了损坏的 App。解决采用双区设计新固件先写备份区校验通过后再由 Bootloader 搬运。搬运过程如果断电下次上电重新搬运即可App 区始终是完整的旧固件。4.2 HTTP 响应头解析错误固件写入偏移现象下载完成后 CRC 校验失败对比发现固件头部多了几十个字节。原因HTTP 响应头没有正确跳过把HTTP/1.1 200 OK\r\n...这些文本也写进了 Flash。解决用strstr查找\r\n\r\n分隔符确保只写入分隔符之后的数据。如果响应头跨多个 TCP 包需要缓存头部直到找到分隔符。4.3 Flash 擦除不完整写入数据错乱现象固件写入后读取校验部分扇区数据是 0xFF 或旧数据。原因STM32 的 Flash 擦除是按扇区的如果只擦了部分扇区未擦除的区域写入会失败。解决擦除时按扇区对齐从起始地址到结束地址覆盖所有涉及的扇区。可以用flash_erase_sectors封装内部循环调用 HAL 的扇区擦除函数。4.4 网络不稳定导致下载中断现象下载到 80% 时 TCP 连接断开升级失败。解决实现断点续传。在 HTTP 请求头里加Range: bytesxxx-服务器返回 206 Partial Content设备从上次中断的地址继续写入。同时记录已下载的字节数到参数区重启后可以恢复。4.5 跳转到 App 后外设异常现象Bootloader 跳转到 App 后串口、定时器工作不正常。原因Bootloader 初始化了外设跳转前没有反初始化App 再次初始化时冲突。解决跳转前调用HAL_DeInit()关闭所有外设关闭全局中断设置 MSP 为 App 的栈顶地址然后跳转到 App 的复位向量。5. 二次开发改造从能跑到好用5.1 加入 HTTPS 支持HTTP 是明文传输固件包容易被篡改。改造方向是加入 TLS可以用 mbedTLS 库在 TCP 之上加一层 SSL。核心改动在tcp_connect之后加ssl_handshake发送和接收都走 SSL 接口。服务器端需要配置证书设备端内置 CA 证书或使用预共享密钥。这个改造工作量不小但安全性提升明显。5.2 差分升级减小流量完整固件包动辄几百 KB对于 NB-IoT 这类低带宽场景流量成本很高。差分升级的思路是只下载新旧固件的差异部分设备端用差分算法如 bsdiff合并生成新固件。服务器端需要保存旧版本固件生成差分包。这个方案能把升级包缩小到原来的 10% 到 20%但实现复杂度较高需要额外的差分合并代码。5.3 升级策略优化当前是设备主动轮询可以改成服务器推送。设备保持一个长连接MQTT 或 WebSocket服务器有新固件时下发通知设备再发起 HTTP 下载。这样升级更及时也减少了无效轮询。另外可以加入灰度发布逻辑根据设备 ID 或版本号决定是否升级避免一次性全量推送导致服务器带宽打满。5.4 验证方法改造完成后验证要覆盖几个场景正常升级、下载中断后恢复、固件 CRC 错误、断电重启后继续搬运、跳转后外设正常。我一般会写一个 Python 脚本模拟服务器控制响应速度和数据包大小测试设备在各种网络条件下的表现。脚本核心逻辑# ota_server_sim.py from http.server import HTTPServer, BaseHTTPRequestHandler import time class OTAServer(BaseHTTPRequestHandler): def do_GET(self): if self.path /firmware/app.bin: with open(app.bin, rb) as f: data f.read() self.send_response(200) self.send_header(Content-Type, application/octet-stream) self.send_header(Content-Length, str(len(data))) self.end_headers() # 模拟慢速网络每 1KB 延迟 10ms for i in range(0, len(data), 1024): self.wfile.write(data[i:i1024]) time.sleep(0.01) HTTPServer((0.0.0.0, 8080), OTAServer).serve_forever()这个脚本可以控制发送速率模拟弱网环境。改time.sleep的参数就能调整延迟改分块大小就能模拟不同的 MTU。从那以后我每次做远程升级都强制走一遍「断电测试」下载到一半直接拔电看设备能不能恢复。这个习惯帮我拦住了至少三次可能批量返工的 bug。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。