STM32 HTTP远程升级实战:Flash分区、Bootloader跳转与CRC校验 简介这份资源面向嵌入式开发工程师与STM32学习者聚焦通过HTTP协议实现设备固件远程升级这一实用场景帮助读者理解如何在无物理接触的情况下完成固件更新提升设备可维护性。压缩包共202个文件约5.23MB以c与h源码文件为核心配合o、d、crf等编译中间文件以及uvproj、uvopt工程配置、sct分散加载、hex与axf可执行产物、map与lst映射清单另含少量htm、html说明页与txt文档构成一套可直接编译调试的完整工程。资源围绕HTTP_GETPkt()请求、URL参数配置、lwIP网络栈适配、固件分块接收与MD5/SHA-1校验、闪存写入及安全模式重启等环节展开读者可据此掌握TCP/IP协议栈与HTTP GET方法在STM32上的落地方式并借鉴服务器端固件接口设计思路。目前已有5087人学习下载适合具备一定网络编程基础、希望补齐远程升级能力的开发者参考。1. 需求先立住为什么 HTTP 远程升级值得单独做一套方案设备卖出去之后固件要改一个参数、修一个逻辑漏洞如果只能靠人上门插下载器那这套产品的维护成本会直接失控。基于 STM32 的 HTTP 远程升级要解决的正是这件事让设备自己通过 HTTP 协议从服务器拉取新固件校验后写入 Flash重启切换到新版本。它适合两类人——正在做嵌入式毕设或课设、需要一套完整可演示升级流程的学生以及手上有 STM32 联网产品、想补上 OTA 能力的工程师。选 HTTP 而不是别的传输方式是因为它复用现成 Web 服务器、能穿透大多数网络环境、调试时用浏览器就能验证资源是否可下载省掉一整套私有协议的服务端开发。这套方案的核心链路是 GET 请求拿固件、分块接收、写入、校验、跳转每一环都有翻车的可能下面按需求、设计、实现、验收完整推一遍。2. 总体方案设计Flash 分区、通信链路与升级状态机2.1 存储分区与地址规划远程升级第一件事不是写代码是把 Flash 地图画清楚。STM32 内部 Flash 通常按扇区擦除升级方案必须至少划分三个区域Bootloader 区、应用程序区App、新固件暂存区Download。Bootloader 负责判断该跳转到哪个 AppApp 负责业务逻辑和发起升级Download 区用来存放从 HTTP 服务器下载下来的新固件。以一款 512KB Flash 的 STM32F4 为例一种常见的划分如下区域起始地址大小用途Bootloader0x0800000032KB启动判断、固件搬运、跳转App0x08008000224KB业务程序含 HTTP 客户端Download0x08040000224KB新固件暂存参数区0x0807800032KB升级标志、版本号、CRC 记录这个划分不是随便定的。Bootloader 要足够小32KB 对只做跳转和搬运的代码绰绰有余App 和 Download 等大是为了保证新固件能完整放下参数区单独留一块是因为升级标志位必须写在 App 擦除时不会波及的地方。我一般会把参数区放在最后一个扇区擦 App 时按地址范围跳过它。提示不同型号 STM32 的扇区大小不一样F1 系列是 1KB/2KB 小扇区F4 是 16KB 到 128KB 的大扇区。分区前一定先查参考手册的 Flash 章节按扇区边界对齐否则擦除时会误伤相邻区域。2.2 HTTP 客户端与分块接收设计STM32 上跑 HTTP底层通常是 LwIP 协议栈加一个精简的 HTTP 客户端。完整实现 HTTP/1.1 太重远程升级场景只需要 GET 一个静态文件所以可以裁到最小建立 TCP 连接、发 GET 请求头、解析响应头拿到 Content-Length、然后循环 recv 把 body 写进 Download 区。关键设计点是分块接收。固件动辄上百 KB不可能一次性读进 RAM必须开一个缓冲区边收边写。缓冲区大小要权衡太小则 recv 调用频繁、升级慢太大则占 RAM。我一般用 1KB 到 2KB 的缓冲区配合 Flash 按字写入。// HTTP GET 固件并分块写入 Download 区伪代码基于 LwIP raw API #define RECV_BUF_SIZE 1024 #define DOWNLOAD_ADDR 0x08040000 uint8_t recv_buf[RECV_BUF_SIZE]; uint32_t write_addr DOWNLOAD_ADDR; uint32_t total_recv 0; int content_length -1; // 1. 建立 TCP 连接发送 GET 请求 send_get_request(server_ip, server_port, /firmware/app_v2.bin); // 2. 解析响应头提取 Content-Length content_length parse_http_header_for_length(); // 3. 循环接收 body 并写入 Flash while (total_recv content_length) { int len tcp_recv(recv_buf, RECV_BUF_SIZE); // 阻塞或回调方式 if (len 0) break; // 连接异常退出 flash_write(write_addr, recv_buf, len); // 按字对齐写入 write_addr len; total_recv len; } // 4. 收完后校验长度是否与 Content-Length 一致 if (total_recv content_length) { set_upgrade_flag(UPGRADE_READY); // 写升级标志到参数区 system_reset(); // 软复位进 Bootloader }这段代码里几个参数值得说清楚。RECV_BUF_SIZE取 1024 是经验值太小会让 TCP 频繁分包太大在 RAM 紧张的型号上放不下。flash_write内部要处理地址对齐STM32 写 Flash 一般要求半字或字对齐缓冲区长度不是对齐倍数时要做补齐。content_length必须校验因为网络中断会导致接收不完整如果直接跳转就会变砖。set_upgrade_flag写在参数区Bootloader 读这个标志决定是否搬运固件。2.3 升级状态机与 Bootloader 跳转逻辑整个升级流程是一个状态机空闲 → 检查更新 → 下载中 → 下载完成 → 校验 → 置标志 → 复位 → Bootloader 搬运 → 跳转新 App。每个状态都要有超时和失败回退否则卡在中间态设备就废了。Bootloader 的逻辑相对固定上电后先读参数区的升级标志。如果标志是UPGRADE_READY就把 Download 区的固件按块搬到 App 区搬运完做一次整体 CRC 校验校验通过则清除标志、跳转到 App校验失败则保留原 App清除标志跳回旧版本。如果标志不是升级态直接跳转 App。// Bootloader 主逻辑伪代码 #define APP_ADDR 0x08008000 #define DOWNLOAD_ADDR 0x08040000 #define APP_SIZE_MAX 0x38000 // 224KB void bootloader_main(void) { uint32_t flag read_upgrade_flag(); if (flag UPGRADE_READY) { // 擦除 App 区 flash_erase(APP_ADDR, APP_SIZE_MAX); // 从 Download 区搬运到 App 区 for (uint32_t i 0; i firmware_size; i 4) { uint32_t word *(volatile uint32_t *)(DOWNLOAD_ADDR i); flash_write_word(APP_ADDR i, word); } // 整体 CRC 校验 if (crc32_check(APP_ADDR, firmware_size) expected_crc) { clear_upgrade_flag(); jump_to_app(APP_ADDR); // 设置 MSP 和 PC 后跳转 } else { clear_upgrade_flag(); jump_to_app(APP_ADDR); // 校验失败回退旧 App } } else { jump_to_app(APP_ADDR); } }jump_to_app是容易踩坑的地方跳转前要关中断、设置主堆栈指针为 App 向量表第一个字、设置 PC 为第二个字然后调用。App 的链接脚本里起始地址必须改成0x08008000并且向量表偏移寄存器SCB-VTOR要设成 App 起始地址否则中断会跳到 Bootloader 的向量表里去。3. 关键模块实现HTTP 请求构造、Flash 写入与校验3.1 GET 请求构造与响应头解析HTTP GET 请求本身很简单但嵌入式端手写请求头有几个细节不能错。请求行必须是GET /path HTTP/1.1Host 头必须带Connection 建议用close避免长连接管理。下面是一个最小可用的请求构造// 构造 HTTP GET 请求头 void build_get_request(char *buf, const char *host, const char *path) { sprintf(buf, 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); }Connection: close是关键它让服务器在发完 body 后主动关闭连接嵌入式端就不用实现 chunked 或 keep-alive 逻辑收到连接关闭即认为接收结束。响应头解析只需要找Content-Length:这一行用字符串查找定位后atoi取值。如果服务器返回 chunked 编码那这套精简客户端处理不了所以服务端要配置成对固件文件返回固定长度。注意有些 Web 服务器默认对二进制文件启用 gzip 压缩Content-Length 会变成压缩后长度而 STM32 端不会解压写进去的就是压缩数据。服务端必须对固件目录关闭压缩或者请求头里带Accept-Encoding: identity。3.2 Flash 擦写与地址对齐处理STM32 的 Flash 写入有硬性约束擦除按扇区写入按半字或字且写入前目标区域必须是已擦除状态0xFF。远程升级里最容易翻车的就是擦写没对齐。擦除策略上App 区在 Bootloader 搬运前整体擦除Download 区在下载开始前整体擦除。擦除耗时较长F4 擦一个 128KB 扇区可能要一秒左右期间不能被打断所以擦除前要关掉不相关中断或者确保擦除函数在 RAM 里执行有些型号 Flash 擦写时取指会停顿。写入时如果接收缓冲区长度不是 4 的倍数最后几个字节要单独处理。我一般会在缓冲区末尾补 0xFF 到 4 字节对齐再写同时记录真实长度校验时按真实长度算。// 按字写入 Flash处理非对齐尾部 void flash_write_aligned(uint32_t addr, uint8_t *data, uint32_t len) { uint32_t i 0; // 按 4 字节对齐部分 for (; i 4 len; i 4) { uint32_t word data[i] | (data[i1]8) | (data[i2]16) | (data[i3]24); HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i, word); } // 尾部不足 4 字节补 0xFF if (i len) { uint32_t word 0xFFFFFFFF; for (uint32_t j 0; i j len; j) { ((uint8_t*)word)[j] data[i j]; } HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i, word); } }HAL_FLASH_Program的第二个参数是地址第三个是数据。写之前要调用HAL_FLASH_Unlock()写完HAL_FLASH_Lock()。如果写入返回错误通常是目标地址没擦干净或者地址越界到了别的分区。3.3 固件完整性校验长度 CRC32只校验长度不够网络传输中极少数情况下会出现内容损坏但长度正确。所以下载完成后要做一次 CRC32 校验。CRC 值有两个来源一是服务端在固件旁边放一个.crc文件或把 CRC 写进 HTTP 响应头如自定义头X-Firmware-CRC二是 App 下载完后自己算一遍和预期值比对。Bootloader 搬运完也要再算一次 CRC因为搬运过程本身也可能出错虽然概率低。两次校验都通过才跳转任何一次失败都回退旧版本。这套双校验看起来冗余但在实际项目里救过我好几次——有一次 Download 区某个扇区擦除不彻底写入后读出来是错的CRC 直接拦住了。// CRC32 校验多项式 0xEDB88320与常见工具一致 uint32_t crc32_calc(uint32_t addr, uint32_t len) { uint32_t crc 0xFFFFFFFF; for (uint32_t i 0; i len; i) { crc ^ *(uint8_t *)(addr i); for (int j 0; j 8; j) { crc (crc 1) ^ (0xEDB88320 -(crc 1)); } } return ~crc; }这个 CRC32 实现和 PC 上crc32命令、Pythonzlib.crc32结果一致方便服务端预计算。校验时把算出来的值和预期值比对一致才置升级标志。4. 避坑与常见问题排查升级失败、变砖与回退4.1 下载中途断网导致固件不完整现象是设备重启后卡在 Bootloader 或跳回旧版本串口打印显示接收长度小于 Content-Length。原因是 TCP 连接中断后tcp_recv返回错误循环退出但代码没有区分「正常收完」和「异常中断」直接置了升级标志。解决方式是在置标志前强制校验total_recv content_length不等就清除 Download 区标志、保留旧 App并记录一次失败计数。更稳妥的做法是下载前先在参数区写一个「下载中」标志下载完成才改成「就绪」Bootloader 看到「下载中」直接跳旧 App。4.2 跳转后中断不响应或 HardFault现象是 App 能跑主循环但一进中断就死或者直接 HardFault。原因是 App 的向量表偏移没设。Cortex-M 内核上电默认从 0x08000000 取向量表App 在 0x08008000如果不设SCB-VTOR 0x08008000中断发生时内核还是去 Bootloader 的向量表找入口自然跑飞。解决方式是在 App 的main函数开头、初始化任何中断之前先设置 VTOR同时确保 App 链接脚本的 FLASH 起始地址和分区一致。4.3 Flash 擦除误伤参数区现象是升级标志莫名其妙丢了或者版本号读出来是 0xFF。原因是擦除 App 区时地址范围算错把参数区所在的扇区也擦了。STM32 大扇区型号上App 区末尾和参数区如果在同一个扇区擦 App 就会连带擦掉参数。解决方式是分区时让参数区独占一个扇区擦除函数严格按分区表地址范围操作绝不越界。我一般会在擦除前加一句断言检查擦除范围是否落在目标分区内。4.4 HTTP 服务器返回 302 或 404现象是请求发出后收到的不是固件数据而是重定向页面或错误页Content-Length 很小写进去的是 HTML。原因是固件文件路径写错或者服务器配置了跳转。解决方式是在解析响应头时先检查状态码只有200才继续接收 body其他状态码直接报错退出。状态码在响应第一行HTTP/1.1 200 OK里解析时取第一个空格后的三位数字即可。4.5 升级后旧版本无法回退现象是新固件有 bug想退回旧版本却发现旧版本已经被覆盖。原因是搬运时直接擦除了 App 区没有保留旧固件。解决方式有两种一是双 App 分区交替A/B 分区新固件写到备用区启动时根据标志选择二是升级前把旧 App 备份到外部 Flash 或服务器。资源有限的型号上至少要在参数区保留旧版本号配合服务器端保留历史固件通过再次远程升级回退。5. 验收与答辩怎么证明这套升级方案真的可靠验收这套方案不能只看「升级成功了一次」。我一般会按下面这张表逐项过每项都有明确的通过标准验收项操作通过标准正常升级服务器放新固件设备发起升级下载完成、CRC 通过、重启后版本号更新断网中断下载到一半拔网线设备保留旧版本不跳转可再次发起升级固件损坏服务器放一个 CRC 错误的固件校验失败回退旧版本串口打印错误码重复升级连续升级同一版本每次都能正常完成不出现 Flash 写坏掉电恢复下载完成后、搬运前断电重新上电后 Bootloader 能继续搬运或安全回退版本回退服务器放旧版本固件能降级成功版本号正确答辩时老师或评审最常问的三个问题提前准备好答案一是「升级失败会不会变砖」答分区隔离加双校验加回退Download 区和 App 区独立任何一步失败都不动旧 App二是「怎么保证固件完整」答 Content-Length 加 CRC32 双重校验三是「Bootloader 和 App 怎么衔接」答参数区标志位加 VTOR 设置加链接脚本地址对齐。演示环节建议用串口打印把每个状态打出来连接服务器、收到响应头、Content-Length 值、已接收字节数、CRC 计算结果、置标志、复位、Bootloader 搬运进度、跳转。评审看到完整的状态流比只看一个「升级成功」的界面有说服力得多。从那以后我每次做远程升级方案都会强制走一遍「断网中断」和「固件损坏」这两个异常用例因为正常流程谁都能跑通异常路径才决定这套方案能不能交付。希望帮到你。本文还有配套的精品资源点击获取