news 2026/10/19 9:01:31

STM32 HTTP远程升级方案:从Bootloader到IAP的完整实现与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32 HTTP远程升级方案:从Bootloader到IAP的完整实现与避坑指南

简介:这份资源面向嵌入式开发者与STM32学习者,聚焦通过HTTP协议实现设备固件远程升级这一实用场景,帮助读者理解如何在无物理接触的情况下完成固件更新,提升设备可维护性。压缩包共202个文件,约5.23MB,以c与h源码、o与d编译产物、crf交叉引用文件为主,另含uvproj工程、sct分散加载、hex与axf固件、map映射及lst列表等,构成一套可编译验证的完整工程。内容围绕HTTP GET请求获取服务器固件展开,涉及HTTP_GETPkt()函数调用、URL与IP端口配置、lwIP等网络库适配、请求头设置、固件分块接收与闪存写入,以及MD5或SHA-1完整性校验和升级安全模式等关键环节,并兼顾公网动态域名与NAT环境下的通信处理。目前已有5087人学习下载,适合希望掌握STM32网络编程与远程升级流程的开发者参考,可据此梳理协议栈调用、服务器接口设计与固件校验的排错思路。

1. 从一次现场翻车说起:为什么我要拆这套 HTTP 远程升级方案

去年冬天,某工业现场部署了三十多台基于 STM32 的数据采集终端,装在离市区两小时车程的厂房里。客户临时要求改一段采样逻辑,按老办法得派人带 J-Link 一台台去刷,光路上时间就够呛。当时我拍胸脯说“搞个远程升级不就完了”,结果第一版方案用 TFTP 传固件,现场网络一抖,设备直接卡在 Bootloader 里变砖,最后只能挨个拆机救回来。那次血泪经验让我彻底转向 HTTP 方案——用 GET 请求从普通 Web 服务器拉固件,走标准 80 端口,现场只要有个能上网的路由就能跑。

这套基于 STM32 的 HTTP 远程升级方案,核心就是让 MCU 自己当客户端,通过 HTTP GET 把新固件下载到外部 Flash,校验通过后跳转 IAP 完成自更新。它适合谁?做工业物联网终端、共享设备、分布式传感器的嵌入式开发者,尤其是那些设备装出去就收不回来、又必须支持功能迭代的场景。下面我把这套方案的目录结构、核心代码和改造点一层层拆开,让你拿到就能跑、跑完就能改。

2. 方案整体架构:从 HTTP 请求到 IAP 跳转的完整链路

2.1 硬件与软件的分层设计

这套方案在硬件上通常需要一颗 STM32 主控(F1/F4/H7 系列都行,Flash 至少 128KB),外加一颗 SPI Flash(比如 W25Q 系列)用来暂存下载的固件。为什么不用内部 Flash 直接存?因为 STM32 内部 Flash 擦写时必须停 CPU,而且容量有限,一旦下载中途断电,内部 Flash 里连旧固件都可能保不住。外部 Flash 就从容多了——先完整写入、校验 CRC,确认无误再擦内部 App 区,这是工业场景里最稳妥的做法。

软件上分三层:最底层是硬件驱动层,包括 SPI Flash 读写、以太网或 WiFi 模块驱动;中间是协议栈层,跑 LwIP 或者 AT 指令封装的 HTTP 客户端;最上层是升级管理逻辑,负责版本比对、分片下载、CRC 校验和跳转决策。这三层之间用清晰的接口隔开,换网络模块时只动协议栈层,换 Flash 型号时只动驱动层,升级流程本身不用改。

常见做法是 Bootloader 和 App 各占一块内部 Flash 区域。Bootloader 放在 0x08000000 起始处,大小 32KB 到 64KB,负责上电判断、固件搬运和跳转;App 从 0x08008000 或 0x08010000 开始,正常跑业务逻辑。外部 Flash 则划分成几个扇区:头部存固件元信息(版本号、大小、CRC32),后面连续存放固件二进制。

2.2 目录结构长什么样

拿到源码包后,根目录一般是这样组织的:

stm32_http_ota/ ├── bootloader/ # Bootloader 工程 │ ├── Core/ │ │ ├── Src/ │ │ │ ├── main.c # 上电判断、跳转逻辑 │ │ │ ├── flash_if.c # 内部 Flash 擦写接口 │ │ │ └── spi_flash.c # 外部 Flash 驱动 │ │ └── Inc/ │ └── MDK-ARM/ # Keil 工程文件 ├── application/ # App 工程 │ ├── Core/ │ │ ├── Src/ │ │ │ ├── main.c │ │ │ ├── http_client.c # HTTP GET 实现 │ │ │ ├── ota_manager.c # 升级流程控制 │ │ │ └── crc32.c # 校验算法 │ │ └── Inc/ │ ├── LwIP/ # 网络协议栈 │ └── MDK-ARM/ ├── tools/ # 上位机辅助脚本 │ ├── pack_firmware.py # 固件打包加头部 │ └── gen_version.py # 版本文件生成 └── docs/ # 协议说明 └── ota_protocol.md

这个结构把 Bootloader 和 App 彻底分开编译,各自有独立的启动文件和链接脚本。tools 目录里的 Python 脚本负责把编译出来的 bin 文件加上自定义头部,再算好 CRC 塞进去,最后放到 Web 服务器上供设备下载。

2.3 一次完整升级的时序

设备上电后,Bootloader 先跑。它读外部 Flash 的头部区域,看有没有标记为“待更新”的固件。如果有,就校验 CRC,通过则把固件从外部 Flash 搬运到内部 App 区,搬完清掉标记,跳转到 App。如果没有待更新固件,直接跳转现有 App。

App 跑起来后,业务逻辑里会定期(比如每天凌晨)去请求服务器上的 version.json,对比本地版本号。发现新版本就触发升级流程:先擦除外部 Flash 的固件区,然后发 HTTP GET 请求下载固件,边收边写外部 Flash,全部收完后算 CRC 并和头部里的值比对。校验通过就在头部打上“待更新”标记,然后软复位。复位后 Bootloader 接管,完成搬运和跳转。

这个时序的关键在于:下载和搬运是分开的两个阶段,中间隔着一次复位。这样即使下载过程中断电,外部 Flash 里的数据不完整,Bootloader 校验 CRC 不过就会忽略它,继续跑旧 App,设备不会变砖。

3. 核心模块拆解:HTTP GET 下载与 Flash 写入的代码实现

3.1 HTTP 客户端怎么发 GET 请求

这套方案里 HTTP 客户端不用完整协议栈,手写一个精简版就够了。核心是拼请求报文、解析响应头、按 Content-Length 收数据。下面这段代码基于 LwIP 的 raw API 实现:

// http_client.c // 发起 HTTP GET 请求,下载固件到外部 Flash int http_download_firmware(const char *host, uint16_t port, const char *path) { struct tcp_pcb *pcb; err_t err; char request[256]; int len; // 1. 创建 TCP 控制块 pcb = tcp_new(); if (pcb == NULL) return -1; // 2. 连接服务器 ip_addr_t server_ip; // 这里假设已经通过 DNS 或硬编码拿到 IP IP4_ADDR(&server_ip, 192, 168, 1, 100); err = tcp_connect(pcb, &server_ip, port, http_connected_cb); if (err != ERR_OK) { tcp_close(pcb); return -2; } // 3. 拼 GET 请求报文 len = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "User-Agent: STM32-OTA/1.0\r\n" "Connection: close\r\n" "\r\n", path, host); // 4. 发送请求(在 connected 回调里实际发送) // 这里把请求内容存到全局缓冲区,等连接建立后发 memcpy(g_http_request, request, len); g_http_request_len = len; return 0; }

这段代码做了四件事:创建 TCP 控制块、连接服务器、拼装 HTTP 请求报文、把请求暂存起来等连接建立后发送。注意Connection: close这个头,它告诉服务器发完数据就关连接,省得我们还要处理 keep-alive 的超时和分块传输。参数host是服务器域名或 IP 字符串,port一般是 80,path是固件在服务器上的路径,比如/firmware/app_v1.2.bin。

连接建立后的回调里,把暂存的请求发出去,然后进入数据接收状态。接收回调里要做的事更细:先解析响应头,找到Content-Length和HTTP/1.1 200 OK,确认服务器返回的是固件而不是 404 页面,再把后续的 body 数据一包包写进外部 Flash。

3.2 响应解析与分片写入 Flash

接收回调是整套代码里最容易翻车的地方,因为 TCP 分包是随机的,响应头和 body 可能混在同一个包里,也可能 body 被切成好几段。下面这段处理逻辑:

// http_recv_cb: TCP 数据到达回调 static err_t http_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p == NULL) { // 服务器关闭连接,下载结束 ota_finish_download(); return ERR_OK; } // 1. 如果还没解析完响应头,先找 \r\n\r\n if (!g_header_parsed) { char *header_end = find_header_end(p->payload, p->len); if (header_end != NULL) { // 解析 Content-Length g_content_length = parse_content_length(p->payload); g_header_parsed = 1; // body 从 header_end + 4 开始 uint32_t body_offset = header_end + 4 - (char *)p->payload; if (body_offset < p->len) { spi_flash_write(g_flash_addr, (uint8_t *)p->payload + body_offset, p->len - body_offset); g_flash_addr += p->len - body_offset; g_received += p->len - body_offset; } } } else { // 2. 纯 body 数据,直接写 Flash spi_flash_write(g_flash_addr, p->payload, p->len); g_flash_addr += p->len; g_received += p->len; } // 3. 检查是否收完 if (g_received >= g_content_length) { ota_finish_download(); } tcp_recved(tpcb, p->len); pbuf_free(p); return ERR_OK; }

这段代码的关键点有三个。第一,g_header_parsed标志位区分“还在解析头”和“纯收 body”两个阶段,避免把 HTTP 响应头当成固件写进 Flash。第二,find_header_end找\r\n\r\n这个分隔符,它之后就是 body 起始位置。第三,每次写完 Flash 都要更新g_flash_addr和g_received,前者是外部 Flash 的写入地址,后者是已接收字节数,用来判断是否达到Content-Length。

参数说明:g_flash_addr初始值应该是外部 Flash 固件区的起始地址,比如 0x00010000;g_content_length从响应头里解析出来,单位是字节;spi_flash_write是底层驱动,内部要处理页对齐和忙等待。

3.3 CRC 校验与跳转决策

固件收完后不能直接跳,必须先校验。常见做法是在固件头部预留 16 字节元信息:4 字节魔数、4 字节版本号、4 字节固件长度、4 字节 CRC32。下载完成后,从外部 Flash 读回固件,算一遍 CRC32,和头部里的值比对。

// ota_manager.c // 校验固件完整性 int ota_verify_firmware(void) { uint32_t magic, version, length, crc_expect, crc_calc; uint8_t buf[512]; uint32_t offset = 0; // 1. 读头部 spi_flash_read(FLASH_FW_ADDR, (uint8_t *)&magic, 4); spi_flash_read(FLASH_FW_ADDR + 4, (uint8_t *)&version, 4); spi_flash_read(FLASH_FW_ADDR + 8, (uint8_t *)&length, 4); spi_flash_read(FLASH_FW_ADDR + 12, (uint8_t *)&crc_expect, 4); if (magic != 0x4F544131) { // "OTA1" return -1; // 魔数不对,不是有效固件 } // 2. 分段算 CRC,避免一次性申请大内存 crc32_init(); while (offset < length) { uint32_t chunk = (length - offset > sizeof(buf)) ? sizeof(buf) : (length - offset); spi_flash_read(FLASH_FW_ADDR + 16 + offset, buf, chunk); crc32_update(buf, chunk); offset += chunk; } crc_calc = crc32_final(); // 3. 比对 if (crc_calc != crc_expect) { return -2; // CRC 不匹配,固件损坏 } return 0; // 校验通过 }

校验通过后,在外部 Flash 的头部区域写一个“待更新”标志,然后调用NVIC_SystemReset()软复位。Bootloader 启动后读到这个标志,就把固件从外部 Flash 搬运到内部 App 区。搬运过程就是读一页写一页,注意内部 Flash 要先擦后写,擦除粒度通常是 1KB 或 2KB。

搬运完成后,清掉“待更新”标志,设置栈指针和跳转地址,执行__set_MSP()和函数指针跳转。这一步的代码在 Bootloader 的main.c里,网上模板很多,核心就三行:

// bootloader/main.c // 跳转到 App typedef void (*pFunction)(void); pFunction jump_to_app; uint32_t app_addr = 0x08008000; // 检查栈顶地址是否合法(在 RAM 范围内) if (((*(__IO uint32_t *)app_addr) & 0x2FFE0000) == 0x20000000) { jump_to_app = (pFunction)(*(__IO uint32_t *)(app_addr + 4)); __set_MSP(*(__IO uint32_t *)app_addr); jump_to_app(); }

参数app_addr要和 App 工程的链接脚本里设置的起始地址一致,否则跳过去就跑飞。栈顶地址检查是防止 App 区是空的或者被擦了一半,跳过去直接 HardFault。

4. 避坑与排查:那些让我熬夜的升级故障

4.1 下载到一半卡死,看门狗复位后反复重来

现象是设备下载固件时进度条走到一半不动了,看门狗超时复位,复位后重新开始下载,又卡在同一位置。原因通常是 HTTP 响应头里的Content-Length和实际 body 长度不一致,或者服务器开了 gzip 压缩,body 是压缩后的数据,长度对不上。解决方法是请求头里加Accept-Encoding: identity明确告诉服务器不要压缩,同时在接收回调里加超时计数,超过 30 秒没收到新数据就主动断开重连。

4.2 外部 Flash 写入偶发失败,读回来全是 0xFF

现象是 CRC 校验总是不通过,读回固件发现某些扇区是 0xFF。原因是 SPI Flash 的页写入有 256 字节对齐限制,跨页写入时如果没处理好边界,数据会写到错误的地址。解决方法是封装一个spi_flash_write_unaligned函数,内部按页拆分,每页写之前先检查目标地址是否在页边界内,跨页就分两次写。另外写完要轮询状态寄存器,等 BUSY 位清零再发下一条命令。

4.3 跳转 App 后进 HardFault,但单独烧 App 能跑

现象是 Bootloader 跳转后 App 跑不起来,但用调试器直接烧 App 却正常。原因通常是中断向量表偏移没设置。App 工程里要在system_stm32fxxx.c里把VECT_TAB_OFFSET改成相对于 Bootloader 的偏移量,比如 0x8000。另外 Bootloader 跳转前要关掉所有外设中断,__disable_irq()之后再跳,否则残留的中断会打到 App 的向量表外面。

4.4 版本号比对逻辑写反,设备无限升级

现象是设备每次上电都提示有新版本,反复下载同一个固件。原因是版本号比对时把“大于”写成了“不等于”,或者服务器返回的版本号格式和本地不一致(一个带 v 前缀一个不带)。解决方法是统一版本号格式为纯数字,比如 1002003 表示 1.2.3,比对时只比大小。另外在外部 Flash 头部里记录“已下载版本号”,下载前先比一次,避免重复下载。

4.5 断电后设备变砖,Bootloader 也起不来

现象是升级过程中断电,再上电设备完全没反应。原因是 Bootloader 在搬运固件时擦除了内部 App 区,但搬运到一半断电,App 区不完整,而 Bootloader 又没有校验机制直接跳转。解决方法是 Bootloader 搬运前先校验外部 Flash 里的固件 CRC,搬运后再校验内部 App 区的 CRC,两次都通过才跳转。如果搬运中途断电,下次上电重新搬运即可,因为外部 Flash 里的固件是完整的。

5. 二次开发改造:从能跑到好用的几个关键点

5.1 加断点续传,别让弱网环境反复重来

原方案是每次下载都从头开始,现场网络一抖就前功尽弃。改造思路是在外部 Flash 头部加一个“已下载字节数”字段,每次下载前先读这个值,然后在 HTTP 请求头里加Range: bytes=xxx-,告诉服务器从断点处继续发。服务器返回 206 Partial Content 就接着写,返回 200 就从头来。这个改动需要服务器支持 Range 请求,Nginx 和 Apache 默认都支持。

// 断点续传请求头拼装 if (g_downloaded > 0) { snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\n" "Host: %s\r\n" "Range: bytes=%lu-\r\n" "Accept-Encoding: identity\r\n" "Connection: close\r\n" "\r\n", path, host, g_downloaded); } else { // 普通请求 }

参数g_downloaded从外部 Flash 头部读出,每次写完 Flash 后更新这个值。注意断点续传时 CRC 校验要覆盖完整固件,不能只算续传部分。

5.2 双分区备份,升级失败自动回滚

更稳妥的做法是在外部 Flash 里划两个固件区:A 区和 B 区。当前运行的固件在 A 区,新固件下载到 B 区。下载校验通过后,在头部标记“下次从 B 区启动”,然后复位。Bootloader 根据标记决定搬运哪个区。如果 B 区固件跑不起来(比如 App 里加个心跳检测,启动后 10 秒内没喂狗就复位),Bootloader 检测到连续复位次数超限,自动切回 A 区。这个机制在工业设备里几乎是标配,代价是外部 Flash 容量要翻倍。

5.3 用 HTTPS 还是 HTTP,看场景别硬上

HTTP 明文传输在公网环境有被篡改的风险,但上 HTTPS 对 STM32 来说开销不小。如果设备走内网或专网,HTTP 加 CRC 校验够用了。如果必须走公网,常见做法是固件包做 RSA 签名,设备端用公钥验签,这样即使传输被劫持,攻击者也伪造不出合法签名。签名验证的代码可以放在 Bootloader 里,搬运前先验签,不过 STM32 跑 RSA 验签大概需要几百毫秒,对启动时间敏感的场合要权衡。

5.4 一个我踩过的坑:别在中断里写 Flash

最后说个血泪教训。我第一版代码把 SPI Flash 写入放在 TCP 接收回调里,而接收回调是在中断上下文跑的。结果 Flash 写入的忙等待把中断响应拖慢,网络包丢了不说,偶尔还触发硬件错误。后来改成接收回调只把数据塞进环形缓冲区,主循环里再取出来写 Flash,问题就消失了。从那以后我每次做 OTA,都强制把 Flash 操作和网络接收解耦,中间加一层缓冲区,哪怕多占几 KB RAM 也值得。

这套方案的核心代码和工程结构就拆到这里。拿到源码后建议先跑通 Bootloader 跳转,再调 HTTP 下载,最后联调升级流程,一步一步来,别一上来就整包烧录。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 9:01:13

VS2015配置OpenCV3.4.16实战:ABI兼容性与运行时库匹配详解

1. 项目概述&#xff1a;为什么VS2015配OpenCV这件事&#xff0c;至今还有人反复踩坑&#xff1f;“【OpenCV基础】VS2015配置OpenCV运行环境”——这个标题看起来平平无奇&#xff0c;甚至有点过时。毕竟现在连VS2022都成了主流开发环境&#xff0c;OpenCV也迭代到了4.10版本&…

作者头像 李华
网站建设 2026/10/19 9:00:58

Tr0ll靶机渗透实战:从信息噪音中提取有效线索的完整方法论

1. 从一台“折磨人”的靶机说起&#xff1a;Tr0ll 到底想教会我们什么如果你在渗透测试的学习路上已经刷过几台常规靶机&#xff0c;习惯了“扫端口—找漏洞—拿shell—提权”的线性流程&#xff0c;那么第一次接触 Tr0ll 的时候&#xff0c;大概率会被它“戏耍”一番。这台靶机…

作者头像 李华
网站建设 2026/10/19 9:00:31

open-code-review:开源可本地部署的智能代码审查新范式

1. 什么是 open-code-review&#xff1a;一个被严重低估的开发者协作新范式“open-code-review”这个词最近在 GitHub Trending 和 CLI 工具圈里频繁出现&#xff0c;但它不是某个具体软件的名字&#xff0c;也不是某家公司的商业产品&#xff0c;而是一类新型代码审查实践的统…

作者头像 李华
网站建设 2026/10/19 9:00:00

Linux系统安全加固指南:五大关键策略与实战操作

1. 先把话说在前面&#xff1a;为什么安全加固比你想的更迫切做了这么多年服务器运维&#xff0c;我有个特别深的感受&#xff1a;很多人对Linux的印象是“开源、稳定、安全”&#xff0c;觉得病毒少、黑客少&#xff0c;装上就能跑&#xff0c;跑起来就不用管。这个想法在十年…

作者头像 李华
网站建设 2026/10/19 8:59:11

AI视频生成API渠道价省一半?接入指南与成本避坑实操

上个月有个做短视频矩阵的朋友跟我算了笔账&#xff1a;他们团队靠某AI视频生成平台的API批量产视频&#xff0c;官网明码标价&#xff0c;一个月烧了快十万。后来渠道商务给了一份报价&#xff0c;同样的文生视频接口&#xff0c;同样分辨率、同样时长&#xff0c;价格直接砍了…

作者头像 李华
网站建设 2026/10/19 8:59:07

Linux 上 CUDA 与 cuDNN 安装及多版本一键切换指南

Linux 上装 CUDA 与 cuDNN 本身不复杂&#xff0c;真正容易卡人的是思路没理顺。默认路径、指定路径、多版本共存——这些词看着普通&#xff0c;实际决定了后续是“环境在为你服务”还是“你天天为环境返工”。尤其一台机器既要跑老框架又要跑新模型时&#xff0c;装法和切换方…

作者头像 李华