
简介这是一套面向W5500网络芯片评估板与STM32F103微控制器的固件自动更新项目核心思路是在STM32F103上运行IAP程序通过W5500的以太网通信接收JSON格式的升级指令从而远程下载新固件并完成对W5500的更新。整个流程不依赖外部编程器适合嵌入式开发者研究网络芯片驱动、Bootloader与App分区设计以及远程升级方案。压缩包内共221个文件以92个C语言头文件、85个C源文件为主体另有16个启动汇编文件、3个批处理脚本、Keil工程文件、BIN/HEX固件以及一份DOCX技术文档整体仅1.19MB源码中涵盖了STM32F103底层驱动、W5500控制逻辑、JSON协议解析和IAP升级流程各模块且工程目录明确区分了s2e_boot与s2e_app两套构建目标便于对照学习。目前已有226人学习下载可作为实际产品中实现网络固件更新的参考范例。配套文档详细记录了设计背景、操作步骤与注意事项有助于快速迁移到自己的项目中。1. 设备在现场网线在人不在W5500 让 STM32F103 的 IAP 真正可用产线上用 ST-Link 刷固件再简单不过可设备装进机柜、接上 W5500EVB 之后再想修一个 bug 就得拆机开盖。W5500 把 TCP/IP 协议栈做进了硬件里STM32F103 这类 Cortex-M3 芯片只靠 SPI 读写 socket就能在几十 KB SRAM 的约束下完成一次完整的网络固件更新。这个方案里IAPIn-Application Programming负责 Flash 擦写和程序跳转JSON 负责把版本号、文件大小、校验值这些元数据从服务器送到设备两端彻底解耦后面想加 A/B 分区、灰度发布都不用动传输层。适合手上已经在用 W5500 做数据采集或 Modbus 转以太网的人把同一套硬件直接升级成支持远程固件更新的设备而不是换主控、重画板子。2. IAP 的 Flash 分区设计与 W5500 网络栈选型2.1 Flash 分区把 512 KB 分成 Boot、App 和暂存区IAP 的第一步不是写代码而是定地址。以 512 KB Flash 的 STM32F103典型型号如 ZET6为例Boot 不能太小因为它要放下 W5500 驱动、cJSON 解析器和升级主流程也不能太大否则 App 的空间被压缩。我一般这样切区域起始地址大小用途BootIAP0x0800000032 KBW5500 驱动、JSON 解析、升级流程App0x08004000其余 Flash业务代码中断向量表位于本区起始处升级标志页Flash 末尾最后一页以 0x0807F800 为例2 KB待升级 / 升级成功 / 回滚 状态记录为什么要单独留一个标志页因为擦写 Flash 的过程中一旦断电设备必须能在下次上电时知道自己处于什么状态。标志页不放代码只放一个自定义结构体用 Magic Number 加状态值做双重判断避免读出全 0xFF 时误判成「正常启动」。App 工程要改两个地方一是 IROM1 的起始地址改为 0x08004000长度相应缩短二是中断向量表偏移。后一个很多人会漏App 编出来了跳过去却直接进 HardFault原因就是向量表没重定位这在第四章和跳转代码一起讲。2.2 W5500 的 Socket 缓冲2 KB 的 RX 缓冲决定了接收写法W5500 内部有 16 KB 收发缓冲区8 个 socket 默认每个分 2 KB。做 IAP 时只开 1 个 socket我会把 RX 缓冲调到 4 KB 甚至 8 KB规则是接收窗口越大MCU 在擦 Flash 时 W5500 越不容易丢包。/* pbuf_size 的单位是 KB写入寄存器的是实际 KB 数 */ w5500_write_reg(sock, Sn_RXBUF_SIZE, 4); // RX 缓冲 4 KB w5500_write_reg(sock, Sn_TXBUF_SIZE, 2); // TX 缓冲 2 KB发请求足够 w5500_write_reg(sock, Sn_MR, Sn_MR_TCP); // TCP 模式W5500 的 SPI 帧是 16 位地址加 8 位控制字节再加数据控制字节里的高 5 位决定是读还是写、是寄存器还是 TX/RX buffer。F103 的 SPI1 我把分频压到 18 MHz 左右跑 HTTP 固件下载不是瓶颈真正的瓶颈在 MCU 擦写 Flash 期间暂停接收导致的窗口拥塞所以缓冲区大小和后续的「边收边写」节奏比 SPI 主频更值得调。2.3 JSON 元数据字段怎么设计才不会被 cJSON 坑到固件本体是二进制流但设备在下载之前需要先知道「这个固件叫几版、多大、校验值是多少」这就是 JSON 的活。常见的做法是设备先请求一个元数据接口拿到 JSON 后再决定是否进入下载流程{ device: w5500evb-stm32f103, version: 1.2.3, size: 132544, crc32: A1B2C3D4, force: 0 }设备端我一般用 cJSON 解析。需要清楚的是cJSON 是动态分配内存的F103 只有 64 KB SRAM所以解析完元数据要立刻删除对象树释放内存再开下载流程。size 字段是 JSON 数字cJSON 解析后类型是 double传给整型时要先判断范围老版本固件经常在这里踩坑size 182000 没问题但换了大固件超过 2^31 就会出现诡异负数。另一个容易被忽略的是 JSON 字符串的结尾从 TCP 收到的数据不保证以\0结尾必须手动补上再交给 cJSON_Parse否则会返回 NULL而报错信息在失败时还得靠 cJSON_GetErrorPtr 看位置。3. 在 STM32F103 上实现最小可用的 W5500 固件下载器3.1 连接固件服务器TCP 客户端的最小代码固件服务器可以是一个 PC 上跑的 Python 脚本也可以是板子所在局域网里的一台静态文件服务器。设备侧流程分两步先连一个端口取 JSON 元数据再连同一个端口拉二进制流。这样协议简单调试时用串口日志就能看清每一步走到哪了。/* 建立 TCP 连接阻塞轮询直到建立或超时 */ uint8_t sock 0; w5500_socket_open(sock, Sn_MR_TCP, 0, 0); w5500_socket_connect(sock, server_ip[4], 8080); uint32_t t0 get_tick_ms(); while (w5500_socket_status(sock) ! SOCK_ESTABLISHED) { if (get_tick_ms() - t0 3000) return ERR_CONNECT_TIMEOUT; /* 3 秒连接超时 */ }连接是异步的所以要用状态轮询加超时判断。三个参数值得注意目标端口固定为 8080避免和常用 Web 端口冲突超时 3 秒是一般局域网足够的值跨网段或 Wi-Fi 中继可放宽到 5 秒server_ip 用静态 IP 还是 DHCP由上位机下发不要在固件里写死服务器 IP否则换一台电脑就要重新烧 Boot。3.2 边收边写接收循环与 Flash 写入节奏固件数据到达 W5500 的 RX 缓冲后要尽快读走否则缓冲满了 TCP 对端会停发。读取和写 Flash 要交错进行不能让擦除操作长时间占据 CPU。uint32_t addr APP_TEMP_ADDR; /* 暂存区起始地址 */ uint32_t offset 0; int16_t len; while (offset fw_size) { len w5500_socket_recv(sock, buf, sizeof(buf)); /* 一次最多读 1 KB */ if (len 0) { flash_write(addr offset, buf, len); /* 按字写入 */ offset len; } else { /* 没有数据时做一次超时判断防止死等 */ if (get_tick_ms() - t0 5000) return ERR_RECV_TIMEOUT; } }这里有个前提暂存区在下载开始前已经整片擦除所以循环里不需要反复擦除。len 要声明成有符号数W5500 返回 0 表示无数据返回负值是 socket 错误用 uint16_t 接收会把错误值变成巨大正数导致 offset 越界。每收 1 KB 写一次 FlashF103 的页编程是几十毫秒量级这个间隔里 W5500 的 4 KB RX 缓冲能兜住对端连续发送的数据。3.3 三个必调参数超时、重传与分块大小参数推荐值调整方向连接超时3 秒跨网段调到 5-8 秒接收静默超时5 秒对端是硬盘服务器时可放宽单次读块大小1 KB调大要同步调大 socket RX 缓冲失败重传次数3 次不断电重试不是重启设备重传的思路是整个下载失败后不要立刻返回 Boot 菜单而是重新走「取 JSON → 对比版本 → 下载」的流程连续失败 3 次才彻底退出并打印错误码。现场没人盯着串口能自愈的故障尽量自愈这样回滚机制才有意义。分块大小受两个约束不大于 W5500 RX 缓冲的一半且能被 4 整除Flash 按字编程。4. 跳转、擦写与回滚IAP 的高危动作怎么处理4.1 向量表重定位与跳转函数固件下载完毕只是第一步真正让新固件跑起来的是跳转这一步也是翻车率最高的地方。void jump_to_app(uint32_t app_addr) { /* 关闭全局中断避免跳转瞬间被外设中断打断 */ __disable_irq(); /* 把向量表重定位到 App 起始地址 */ SCB-VTOR app_addr; /* 取出 App 向量表前两个值初始 MSP 和复位入口 */ uint32_t app_sp *(volatile uint32_t *)app_addr; uint32_t app_pc *(volatile uint32_t *)(app_addr 4); /* 设置主栈指针并跳转 */ __set_MSP(app_sp); ((void (*)(void))app_pc)(); }三个关键点第一跳转前要关闭所用外设的中断最好把外设都 DeInit 一遍否则中断服务函数地址还是旧的第二SCB-VTOR app_addr必须在取 App 向量表之前执行顺序反了会导致第一次进中断时取到错误的向量第三__set_MSP之后不要再定义任何局部变量并依赖它编译器可能把栈上的临时数据覆盖掉。4.2 擦写策略整片擦除还是按页擦除下载到暂存区的固件最终要覆盖 App 区。常见做法有两种一是下载前直接擦除 App 区二是先下载到暂存区校验通过再擦写覆盖。前者省 Flash 空间但下载过程中断电就变成砖后者多占一块暂存区却能保证任何时刻 Flash 里至少有一份完整固件。策略空间成本断电安全性适用场景直接覆盖低差中途断电变砖有专人驻场的调试阶段暂存区校验后覆盖高好旧 App 可回滚出货设备的 OTA我倾向于第二种暂存区下载完成后先算 CRC32 或 MD5和 JSON 元数据里的值比对一致才擦除 App 区并搬移。这一段虽然啰嗦但省掉它的代价是现场返修。4.3 回滚用一个 Flash 标志位实现「升级失败自动退回」回滚不需要复杂的 A/B 双分区Boot 里的逻辑可以很朴素升级前在标志页写入「待升级」App 写入完成并自检 OK 后由 Boot 在下次启动时改成「升级成功」如果上电发现标志是「待升级」且 App 版本没变说明升级中断了直接再拉一次固件。typedef struct { uint32_t magic; /* 固定 0xA5A5A5A5防止空 Flash 误判 */ uint32_t state; /* 0正常, 1待升级, 2升级成功 */ uint32_t attempts; /* 连续失败次数 */ } iap_flag_t;attempts 字段做自愈上限Boot 每次发现升级失败就让 attempts 加一超过 3 次就放弃自动重试并停留在 Boot 等待命令行避免设备陷入「下载失败-重启-再下载」的死循环。注意 Flash 有擦写寿命attempts 频繁写入会耗损标志页可以在写入前判断值是否已经相同相同则跳过编程。5. 先用 JSON 格式化工具校验元数据再用串口日志验证升级链路5.1 一次完整升级该看到什么元数据 JSON 建议先在 PC 上用 JSON 格式化工具校验一遍常见问题比如size: 132544后面漏逗号、字符串引号不配对都会让设备端 cJSON_Parse 返回 NULL。这个错误在串口日志里表现为json parse fail at offset: xxcJSON_GetErrorPtr 会给出出错位置对照格式化后的文本一眼就能找到。验证链路我按下面顺序来设备上电打印版本号 → 收到上位机升级命令 → 打印目标版本和大小 → 下载进度百分比 → 打印 CRC 校验结果 → 跳转后 App 打印自己的版本号。任何一步缺失都有对应排查方向日志现象大概率原因能取到 JSON但 version 是乱码忘记在 TCP 数据末尾补\0或长度越界读下载进度卡在 0%W5500 socket 未建立检查目标端口和防火墙进度跑到一半重启暂存区没擦干净或写地址越界核对 size 和地址映射跳转后无输出向量表没重定位或跳转前没关闭全局中断能跑但串口打印乱码波特率不对检查使用的是串口 1 还是串口 3 的时钟配置5.2 串口日志最容易误导人的一个点F103 的串口 1 挂在 APB2 上主频 72 MHz串口 2/3 挂在 APB1 上主频 36 MHz同样写USART_BRR 0x1D4C两个时钟域算出来的实际波特率完全不同。IAP 的调试日志和网络的调试信息不要同时挤在一个串口上输出我一般把升级流程日志放串口 1W5500 socket 状态丢给串口 3 或者直接通过 JSON 上报给上位机。否则升级卡住时你会分不清是 Flash 没擦干净还是 socket 断开两个故障的日志交织在一起极难定位。建议在 Boot 里固定用串口 1 打印一行带时间戳的启动日志App 也打印一行带时间戳的启动日志两个时间戳的间隔就是跳转耗时超过 500 毫秒就得检查向量表重定位是否生效。这个简单的观察点比加一堆断言都好用。本文还有配套的精品资源点击获取