☰
STM32+W5500远程升级实战:IAP原理与HTTP固件下载全解析
2026/10/14 1:43:00 网站建设 项目流程

简介:面向嵌入式开发者的STM32远程固件升级方案,基于W5500以太网控制器与HTTP协议,实现设备在线获取新固件并完成IAP更新,免拆机、免编程器,适合物联网产品维护与批量升级场景。资源包共213个文件、压缩后4.82MB,源码以C头文件与源文件为主,涵盖W5500驱动、固件下载、校验存储和IAP跳转逻辑,并含Keil工程配置、编译生成的hex/bin镜像、HTML使用说明与TXT注意事项,结构清晰,可直接导入工程学习或二次开发。方案完整走通从HTTP GET下载固件、校验存储到跳转执行IAP的升级链路,并给出POST触发升级等交互示例,能帮助开发者快速掌握远程升级的设计要点与状态机处理思路。已有3154人学习下载,适合具备一定STM32基础、希望实现网络远程升级的工程师参考。

1. 拆解一个 STM32 + W5500 的远程升级实战:为什么 IAP 不是你想象中那样复杂

工厂里有几十台设备,程序有 bug 要更新固件,以前得派人拿着烧录器一台一台开盖刷写,一来一回耽误生产。后来换成了 STM32 + W5500 的以太网远程升级方案,人在办公室就能把新固件推下去,省掉了大量出差成本。这套资源核心就是围绕这个场景:基于 STM32 内置 IAP 机制,搭配 W5500 以太网控制器,用 HTTP 协议把 bin 固件文件从服务器拉下来,写入片内 Flash 指定分区,实现远程升级。它不是一套完整的商业 OTA 平台,而是一个能跑通、能改、能上量产的工程级参考实现,适合做工业设备、电力监控、数据采集终端的开发者。

2. 先从 Flash 布局开始:Bootloader、App 与备份分区划分

任何 IAP 远程升级都绕不开一个前提:芯片里至少要放两个程序,一个是引导用的 Bootloader,一个是实际业务用的 App。Bootloader 负责启动时判断要不要升级、从网络拉取固件、写入 Flash,然后跳转到 App 执行。App 跑业务逻辑,同时能接收远程升级指令。这两个程序是独立编译、独立烧写的,所以 Flash 空间规划直接决定后面所有代码的写法。

2.1 片内 Flash 分区规划的通用做法

以常见的 STM32F103VET6(512KB Flash)为例,我一般会这样划分:

分区起始地址大小用途
Bootloader0x0800000032KBIAP 引导、HTTP 下载、Flash 擦写
App0x08008000400KB实际业务程序
备份区0x0807200064KB新固件临时存储区
参数区0x0807E0008KB升级标志、版本号、校验值

要注意 Bootloader 给 32KB 其实是偏保守的,如果只是实现 HTTP 下载和 Flash 写入,16KB 就够,但留大一点以后加加密验签不捉襟见肘。App 起始地址定了 0x08008000 后,编译时必须同步修改链接脚本,否则程序一启动就跑到错误地址去执行,直接 HardFault。备份区的意义在于:先把新固件完整下载到备份区,校验通过后再搬到 App 区,防止下载到一半断电导致 App 区损坏。参数区用来记录当前固件版本、下载状态、校验结果,这样 Bootloader 启动时能快速判断走正常启动还是走升级流程。

2.2 链接脚本修改是第一步,也是最容易翻车的一步

MDK 工程里用的是分散加载文件,比如 .sct 文件。App 工程的启动文件和链接脚本都得改:

; 修改前 LR_IROM1 0x08000000 0x00080000 { ER_IROM1 0x08000000 0x00080000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (+RW +ZI) } } ; 修改后 LR_IROM1 0x08008000 0x00064000 { ER_IROM1 0x08008000 0x00064000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (+RW +ZI) } }

我把 App 区的可执行空间改成了 0x64000 也就是 400KB,这与 Flash 分区表对应。这里容易被忽视的是向量表偏移:大多数固件默认从 0x08000000 取向量表,App 跑在 0x08008000,必须在 main 函数开头调用SCB->VTOR = 0x08008000;,否则任何中断来临时,CPU 都会按老的向量表去取地址,表现就是中断进不去、串口没输出、系统看起来像死机。

2.3 Bootloader 启动流程的代码骨架

Bootloader 主逻辑其实是一个状态判断,不会太复杂:

int main(void) { uint32_t app_entry_addr; uint32_t app_valid_flag; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_UART_Init(); W5500_Init(); // 检查升级标志和备份区固件完整性 app_valid_flag = *(volatile uint32_t *)APP_FLAG_ADDR; if (app_valid_flag == 0xA5A5A5A5) { // 参数区标记了“需要升级”,说明备份区已有新固件 if (Check_Backup_Firmware() == PASS) { Erase_App_Sector(); Copy_Backup_To_App(); Clear_Upgrade_Flag(); } } // 校验 App 区首字是否为有效栈顶地址 app_entry_addr = *(volatile uint32_t *)APP_ADDR; if ((app_entry_addr & 0x2FFE0000) == 0x20000000) { Jump_To_App(); } // App 不存在或损坏则进入升级模式 Enter_HTTP_Upgrade_Mode(); }

这段逻辑里有一个细节:判断 App 是否有效,不是靠标志位,而是直接读 App 区第一个字是否是合法的 RAM 地址。因为任何有效的 ARM 程序,第一个字必须是初始栈指针,其值一定落在片内 RAM 范围内。如果刷进去的是垃圾数据,大概率读出来不是 0x20000000 开头,这就直接判为无效。这种方式避免了对参数区标志位的过度依赖,即使标志位被意外篡改也能兜底。Jump_To_App 函数标准写法是先关中断、改向量表、再调用函数指针,顺序不能乱。

3. W5500 接入与 HTTP 协议解析:从 TCP 裸数据到 bin 文件落盘

W5500 是硬件 TCP/IP 协议栈芯片,内部集成了 TCP/IP 协议,32KB 的 Socket 缓冲区,MCU 通过 SPI 接口发命令就能操作。这里最大的好处是 MCU 不需要跑 lwIP 这种大型协议栈,RAM 占用和代码复杂度都低很多,对 103 这类芯片特别友好。

3.1 SPI 驱动初始化与 Socket 建立

W5500 的 SPI 速率我一般拉到 18MHz,硬件上能稳定跑。初始化的核心是配置网络参数和打开 Socket:

void W5500_Init(void) { uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}; uint8_t ip[4] = {192, 168, 1, 120}; uint8_t gw[4] = {192, 168, 1, 1}; uint8_t mask[4] = {255, 255, 255, 0}; SPI_Init(); Reset_W5500(); setSHAR(mac); // 设置 MAC 地址 setSIPR(ip); // 设置本机 IP setGAR(gw); // 网关 setSUBR(mask); // 子网掩码 setRTR(2000); // 重传超时 2 秒 setRCR(8); // 最大重传 8 次 }

几点说明:setRTR 和 setRCR 是硬件 TCP 重传参数,关系到弱网环境下连接稳定性。RTR 默认值是 2000(单位 100us,也就是 200ms),我给到 2000 其实有点大,但考虑到工控现场网络环境复杂,重传多等一下换取成功率是值得的。接下来打开 Socket 走 HTTP 就是这样一段逻辑:

3.2 用 HTTP GET 从服务器拉取固件

W5500 的 Socket 操作是软硬结合的:socket() 建套接字,connect() 发 SYN 握手,之后 send() 发请求、recv() 收数据。用 Socket 0 操作服务器时,流程如下:

int8_t HTTP_Download_Firmware(uint8_t *buf, uint32_t buf_size) { uint16_t read_len; uint32_t total_len = 0; uint8_t http_header[256]; // 解析服务器 IP 和端口 uint8_t server_ip[4] = {192, 168, 1, 50}; uint16_t server_port = 80; // 拼 HTTP 请求头 snprintf((char *)http_header, sizeof(http_header), "GET /fw/app_v2.bin HTTP/1.1\r\n" "Host: 192.168.1.50\r\n" "Connection: close\r\n" "\r\n"); if (socket(0, Sn_MR_TCP, 0, 0) != 0) { return -1; } if (connect(0, server_ip, server_port) != SOCK_OK) { close(0); return -2; } send(0, http_header, strlen((char *)http_header)); // 循环接收数据,跳过 HTTP 响应头 while (1) { read_len = recv(0, buf, buf_size); if (read_len > 0) { // 第一次收到的数据包含 HTTP 响应头,需要剥离 if (total_len == 0) { uint16_t header_len = Find_Header_End(buf, read_len); if (header_len < read_len) { total_len += read_len - header_len; Flash_Write_Buffer(buf + header_len, read_len - header_len); } } else { total_len += read_len; Flash_Write_Buffer(buf, read_len); } } else if (read_len == SOCK_RECV_TIMEOUT) { // 超时继续等,不立刻判失败 continue; } else { break; } } close(0); return total_len; }

这段代码的关键在于剥离 HTTP 响应头。如果直接把整个响应写进 Flash,前几百字节是 "HTTP/1.1 200 OK" 之类的 ASCII 文本,写入后固件头全毁,跳转必然失败。我第一次移植时就是没处理这层,bin 文件怎么都跑不起来,串口打印的全是乱码。Find_Header_End 的实现逻辑就是搜索\r\n\r\n这个序列,找到后返回其后的偏移量。还有一个容易忽略的坑是:TCP 是流式协议,一次 recv() 收到的数据可能跨越文件边界,也可能只收到半个文件,所以每次必须把读到的字节原样写入 Flash,不文件追加逻辑的什么偏移量都得算准。

3.3 Flash 写入与擦除的边界细节

STM32 的 Flash 有扇区对齐要求,F103 小容量是 1KB/扇区,大容量是 2KB/扇区。W5500 每次收包 2KB 左右,搬运时必须按扇区处理:

void Flash_Write_Buffer(uint8_t *buf, uint16_t len) { static uint32_t write_addr = APP_BACKUP_ADDR; // 先擦除扇区 if ((write_addr % SECTOR_SIZE) == 0) { FLASH_EraseSector(Get_Sector_Num(write_addr), VOLTAGE_RANGE_3); } // 按字(32位)写入 for (uint16_t i = 0; i < len; i += 4) { uint32_t word = *(uint32_t *)(buf + i); FLASH_ProgramWord(write_addr, word); write_addr += 4; } }

注意一个坑:当 len 不是 4 的倍数时,最后那几个字节不能直接按字读,否则越界访问 buf。常见做法是把尾部字节手动拼成一个 32 位数据再写。另外 Flash 写前必须先擦除,这是一条硬规矩。如果擦除扇区时程序还在运行、中断没关,极有可能擦到中断向量表所在扇区,直接死掉。所以这里要在擦除前__disable_irq(),写完再__enable_irq()。

4. App 端配合:升级指令、版本号与跳转复位机制

Bootloader 拉完固件只是升级的一半,另一半在 App 这边。App 要能接收远程指令、能主动复位进入 Bootloader,而且要保证升级失败后设备还能回到老固件跑,这不是简单复位能实现的,需要一套版本与标志位的配合。

4.1 App 接收升级指令并置标志位

常见做法是 App 监听一个 TCP 端口或串口指令,收到升级指令后先校验当前版本号,然后向参数区写入特殊标志,再软复位启动到 Bootloader:

#define APP_UPGRADE_FLAG_ADDR 0x0807E000 #define APP_UPGRADE_FLAG_VALUE 0xA5A5A5A5 void Handle_Upgrade_Command(void) { // 擦除参数区扇区 FLASH_ErasePage(APP_UPGRADE_FLAG_ADDR); // 写入升级标志 FLASH_ProgramWord(APP_UPGRADE_FLAG_ADDR, APP_UPGRADE_FLAG_VALUE); // 执行软复位 NVIC_SystemReset(); }

这是一个非常典型的软件复位升级流程。Bootloader 启动时读到 0xA5A5A5A5,就会知道备份区可能又一个待升级固件,于是去校验它的 CRC32 和长度,全通过才拷贝替换 App。如果用户只是想让设备重启而不是升级,那不要写标志位,直接复位就行,否则 Bootloader 会发现备份区没数据或者校验不过,干等半天又跳回 App,浪费时间。所以升级指令和复位指令必须分开,两者不是一个概念。

4.2 版本号对升级流程的影响

远程升级最怕的是把旧版本覆盖新版本。网络工程师改配置时经常这样翻车,嵌入式也一样。我在这个项目里给参数区增加了 4 字节的版本号字段,App 在收到升级指令时先比较本地版本和目标版本:

typedef struct { uint32_t magic; uint32_t version; uint32_t firmware_len; uint32_t crc32; } upgrade_info_t;

服务器下发升级前,先把版本号发过来,App 判断目标版本是否高于当前版本,如果低于等于就拒绝执行。这个逻辑看着适用但要结合现场:如果设备当前版本是 2.0,服务器误发了 1.8 的包,没有版本判断的代码会把设备刷回旧版本,之后再想远程刷回来就得多走一趟流程,非常啰嗦。有版本门禁后,把误刷风险挡在了入口处。此外也建议在升级前把 4 字节版本号、4 字节长度、4 字节 CRC 发给 Bootloader,Bootloader 在写入备份区前先核对长度和 CRC,不匹配直接放弃,等待服务器重传。

4.3 升级失败后的回滚策略

如果固件下载到一半断电或网络中断,备份区只有半截数据,App 区还是旧固件。下次上电 Bootloader 检查备份区长度和 CRC 被不对,会直接忽略备份区,进入 App 正常运行。这种结果还算好,最怕的是固件拷贝阶段出问题:App 区快写完时崩溃,App 和 Bootloader 都活着,但 App 已经不能用了。所以我习惯在 App 区起始位置放一个 8 字节的固件描述结构体:长度+CRC32。Bootloader 拷贝完成后会回头验证 App 区前 8 字节里的 CRC 是否匹配,如果失败,把备份区再拷回来回滚一次。整个过程需要将备份区保留到 App 校验成功后才清空,所以别一完成拷贝就急着擦备份区,先跳转,App 里主动执行一次完整性自检后再通知 Bootloader 清理,这样升坏了能后悔。

5. 避坑指南:远程升级最容易踩的五个坑

远程升级开发时遇到的坑非常有规律,我把最常见的五个现象、原因和解决办法整理出来,都是实操中反复踩过的。

5.1 现象:启动后串口无输出,程序不运行,进入 HardFault

原因:App 的编译链接脚本没有改成偏移后的地址,向量表还在 0x08000000,Bootloader 跳转到 App 后,第一个中断就把 CPU 搞死了。有时连 main 函数都没进去,因为启动文件的向量表定位全程错误。

解决:修改 .sct 文件把 ER_IROM1 起始地址改为 App 偏移地址,同时在 main 开头加上SCB->VTOR = APP_START_ADDR;。注意在 MDK 里同时勾选 "Use Memory Layout from Target Dialog" 容易和 .sct 文件冲突,建议二选一。我在实际项目里直接用 .sct 文件最稳当。

5.2 现象:HTTP 下载时偶尔少几十字节,固件 CRC 经常不对,但重试几次又能成功

原因:W5500 recv() 返回数据后,应用层没有把剩余数据读干净,导致下一个包调用 recv() 时上次残留数据混入。尤其在连续收包速度很快时,内部 32KB Socket 缓冲区溢出不报错,直接丢弃新数据。

解决:每次 recv() 必须循环读到返回 0,不能只读一次就退出。同时把 W5500 的 RX 缓冲区调大,setSNSR把 socket 0 的 RX 空间设置为 16KB 甚至更大。另外注意头部剥离只应在第一个包出现一次,后续包如果再去 Header 会把二进制数据截掉,这里的状态机要分清。

5.3 现象:升级过程中断电,重新上电后设备变砖,Bootloader 和 App 都起不来

原因:App 区擦除到一半断电,Bootloader 检查 App 区首字发现不是有效栈指针,进入 HTTP 升级模式,但服务器不在线,于是 Bootloader 没有可执行代码,设备就一直停在等待状态。

解决:断电死在升级模式其实不算真变砖,只要再接上服务器就能救回来。真正要防的是 App 区旧固件损坏,所以分区设计里一定要有独立的备份区。Bootloader 启动时若发现 App 区校验失败但备份区有完整固件,就自动从备份区恢复,等服务器发指令后再继续升级。这个习惯是血泪教训换来的,从那以后我做的每套远程升级方案都必须留一个备份分区。

5.4 现象:W5500 连接服务器经常超时,ping 能通但 TCP 连不上

原因:路由器或交换机端口限制、ARP 表老化后没有重新请求,或者 W5500 的 MAC 地址和其他设备冲突。W5500 的硬件 TCP 协议栈对物理层依赖度挺高,网线质量差也会导致大量重传,RTR 和 RCR 参数设得太小会直接抛超时。

解决:先在 PC 上连同一台服务器测试端口通不通。然后把 W5500 的 RTR 设为 2000,RCR 设为 8,这组参数在复杂电磁环境下表现还可以。每次连接失败后,执行一次软复位重新初始化 W5500,别带着脏状态重试,兜底效果比其他技巧都强。

5.5 现象:升级成功后 App 运行异常,串口打印乱码,偶尔死机

原因:升级后的 App 和升级前的版本不是同一套软件,Flash 里残留的旧变量或旧数据没清理干净。最典型的是日志存储在片内 Flash 里,版本 1.0 和 2.0 的日志格式不同,Bootloader 拷贝固件时不动这个区域,残留数据让 App 启动时解析出错。

解决:App 首版开发时就要定义好片内 Flash 各分区的数据格式,预留字段,版本升级时做兼容。日志、参数、配置这类数据区要单独规划,不能用 App 区尾部剩下的零散空间。我在资源里的 Flash 分区表已经给参数区单独留了 8KB,就是为了避免这个问题。

6. 用 HTTP Range 实现断点续传:那个能救命的单点技巧

整个 IAP 方案里,最值得单独拿出来讲的是 HTTP Range 断点续传。设备升级到一半网络断了,重连服务器后直接从头下载,浪费带宽也浪费时间,很多时候反复断续让设备一直停在升级状态,现场维护小哥就要被骂。W5500 是硬件协议栈,TCP 层面没有自动续传,但 HTTP 定标准里就带了一个 Range 头,我们可以直接利用,不用改协议、不用自己设计分段编号。

snprintf((char *)http_header, sizeof(http_header), "GET /fw/app_v3.bin HTTP/1.1\r\n" "Host: 192.168.1.50\r\n" "Range: bytes=%d-\r\n" "Connection: close\r\n" "\r\n", flash_written_len);

其中 flash_written_len 是已经安全写入 Flash 的字节数。服务器收到 Range 后会返回206 Partial Content,数据从偏移位置开始。App 掉线后重连时,向 Bootloader 查询 flash_written_len 就行,因为 W5500 是硬件处理 TCP,MCU 资源消耗极低,但写 Flash 的位置必须由固件维护。关键点来了:断点续传的偏移量必须是 Flash 扇区边界对齐的,比如 2KB 的倍数。为什么?因为如果上次写到偏移 2048 的中间,重新连接后从 2048 接着写,但这个扇区之前已经写过一部分数据,直接往已写过的 Flash 地址写数据不会生效。所以续传策略要和 Flash 写入策略统一:

  • 每次写完一个完整扇区,才把扇区号记录到参数区。
  • 重新下载时读参数区里的扇区号,乘上扇区大小就是起点。
  • 服务器返回 206 后,从起点地址开始写,起点必然是扇区对齐的。

这样还能顺便解决一个问题:如果上一次下载写了半个扇区就掉电,参数区记录的扇区号是上一次完整写完全部扇区的值,重新写只会覆盖一个完整的干净扇区,不会出现半擦除半写入的状态。W5500 的 HTTP 下载速度我实测大约在 120KB/s,一个 100KB 的固件不到一秒就能下完,加上断点续传后,弱网环境重试成本极低。

验证断点续传是否真的生效,可以用 Wireshark 抓包,看第二次请求的 HTTP 请求头里是否带 Range,以及服务器响应状态码是不是 206。如果服务器返回 200 而不是 206,说明服务器没启用 Range 支持比如某些开发板自带的简易 HTTP 服务器,这时固件别去按偏移写,否则数据前段会有重叠,固件损坏。所以实现时最好做两层判断:响应码是 206 时才续传,收到 200 就清掉偏移量重新下载,这也是最常见的坑。

从那以后,我每次设计远程升级方案都会强制把 Range 续传加上,哪怕项目只要求最简单的整包下载。因为现场的坑不在功能正常时,而在网络抖动的那十几秒。断点续传不复杂,却能省掉最疼的返修成本。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询