
简介一份基于STM32与W5500的IAP远程升级完整工程源码面向嵌入式固件开发者重点解决设备在不拆卸外壳、无需外部编程器的情况下通过HTTP协议在线更新固件的实际需求。项目整合STM32F10x标准外设库、W5500硬连线TCP/IP协议栈及IAP引导流程覆盖固件下载、校验、闪存写入与跳转执行等关键环节适合具备一定STM32基础并希望拓展网络升级能力的开发者参考学习。压缩包共213个文件约4.82MB以C语言源码43个.c、头文件48个.h为核心同时包含Keil MDK工程文件.uvprojx、链接脚本.sct、编译生成的.hex/.bin固件、汇编启动文件.s以及htm/html说明文档等便于对照工程实际搭建与调试。相关资料已有3154人学习可用作产品远程维护方案选型或自行实现OTA升级的蓝本。通过源码可快速理解HTTP GET/POST请求发起、固件分包接收与Flash分区规划等实现细节显著降低IAP功能开发门槛。1. 为什么远程升级会成为STM32设备绕不开的坎当你手里有一批已经部署到现场的控制器客户要求改一个判断分支或者修一个只在特定数据组合下才触发的Bug时派人去现场烧录的成本往往比重新做一块板子还高。STM32 W5500 IAP 这条路线就是让设备具备通过网络更新固件的能力Bootloader 跑在 Flash 最前面平时把控制权交给 App需要升级时固件通过 W5500 这块 SPI 接口的以太网芯片传进设备写入 App 区再跳转执行。这篇文章会把分区设计、W5500 移植、传输协议、Flash 写入和跳转的完整链路讲清楚也会把最容易让人翻车的几个地方提前摆出来。适合手里有以太网接入的 STM32 设备、正在评估远程升级方案的团队或个人开发者照着做至少能避开 80% 的常见坑。2. IAP启动流程与Flash分区设计先让Bootloader和App各就各位远程升级不是把新固件丢进 Flash 就完事。设备上电后先执行谁、App 坏了怎么办、升级标志放在哪里这些问题都要在动手写代码之前定下来。这一章先把 IAP 的运行机制和分区布局讲透后面的网络和写入代码才有地方落脚。2.1 IAP的工作机制上电先跑Bootloader再决定进App还是等升级IAP 的思路是把 Flash 拆成两个逻辑上独立的程序区。Bootloader 区存放的是一个自包含的小程序它有自己的外设驱动、网络栈和 Flash 擦写代码App 区才是真正的业务固件。设备每次上电CPU 从 0x08000000 开始执行 BootloaderBootloader 先检查升级标志和 App 区的完整性再决定是直接跳转 App还是留在原地等待网络传来的新固件。这里有一个关键认知升级过程中的网络收发、Flash 擦写全部由 Bootloader 自己完成不能依赖 App 里的任何函数。原因是 Bootloader 无法假设 App 里的驱动还处于可用状态——新固件可能已经把外设初始化逻辑改掉了甚至 App 本身已经损坏。所以 Bootloader 必须自包含一套精简的 W5500 驱动和协议解析代码这套代码在整个设备的生命周期里基本不变变化越少越稳。启动流程按顺序拆开是这样的上电后先关中断初始化时钟、串口和 W5500然后读取升级控制区的内容。如果控制区里的标志显示“有新固件待完成”就直接进入升级模式如果标志显示“App 有效”就校验 App 区头部信息通过后跳转。跳转之前要完成中断关闭、外设复位、栈指针重定向这一整套动作缺一个都会在 App 启动瞬间出问题。整条链路里Bootloader 越短越容易验证我一般会把 Bootloader 的功能限制在“最小网络传输 Flash 擦写 跳转”这三件事上不做任何业务逻辑。2.2 分区布局Bootloader区、App区、标志区怎么切才不越界分区是 IAP 设计里最不能含糊的部分。以常见的中大容量 STM32F1 系列 256KB Flash 型号为例一个稳妥的布局是把前 32KB 划给 Bootloader中间约 222KB 划给 App最后一页留给升级控制区。Bootloader 的 32KB 里要塞下 W5500 驱动精简版、TCP/IP socket 处理、自定义升级协议、Flash 擦写函数和跳转函数实际编译出来通常在 15KB 到 25KB 之间留足余量。App 区则是业务代码的完整镜像从中断向量表开始包含全部中断处理函数和主逻辑。区域地址范围存放内容Bootloader0x08000000 ~ 0x08007FFF32KB网络栈精简版、升级协议、Flash擦写、跳转代码App 区0x08008000 ~ 0x0803F7FF约222KB业务固件完整镜像控制区0x0803F800 ~ 0x0803FFFF1页升级标志、固件长度、CRC32、当前有效区控制区单独占一整页是因为 Flash 擦除的最小单位就是页标志信息不能和 App 数据混在同一页里否则每次写标志都要连带擦掉一大片 App 数据。控制区里一般放一个结构体包含魔数、固件总长度、固件 CRC32、升级完成标志和“当前应执行 App 区”的指针。读写这个结构体要小心Flash 只能从 1 写成 0所以状态迁移只能用赋值 0 的方式翻转不能把已经写成 0 的位再写回 1。注意STM32F1 系列不同容量的 Flash 页大小不一样中容量多为 1KB、高容量多为 2KB做分区表之前必须翻目标芯片的参考手册核实。地址边界必须落在页边界上App 的起始地址尤其不能卡在页中间否则擦写时会牵连到 Bootloader 区。3. W5500网络接入与固件传输协议先把升级通道打通远程升级的通道核心是 W5500。它是一块带硬件 TCP/IP 协议栈的以太网控制芯片MCU 只通过 SPI 读写它的寄存器TCP 的分包、重传、校验都由芯片内部完成。对 IAP 场景来说最大的好处是 Bootloader 里不需要移植一整套 TCP/IP 协议栈省掉大量代码和调试时间。3.1 基于HAL库的W5500 SPI初始化与Socket配置W5500 与 STM32 之间是标准的 SPI 主从连接片选 CS 和复位 RSTn 各占一个 GPIO。初始化顺序有讲究先配置 GPIO 和 SPI 外设再给 W5500 做一次硬件复位最后把 SPI 读写回调注册给 W5500 驱动库让它通过回调函数完成寄存器访问。下面的代码是基于 HAL 库的初始化骨架SPI1 的 SCK、MOSI、MISO 分别接到 PA5、PA7、PA6CS 用 PA4RSTn 用 PA3。// w5500_hal.c —— W5500 在 STM32 上的 HAL 初始化 #include w5500_hal.h #include wizchip.h // 常见的 W5500 驱动库头文件 SPI_HandleTypeDef hspi1; static void W5500_GPIO_Init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); // PA5SCK, PA7MOSI 配置为复用推挽输出 gpio.Pin GPIO_PIN_5 | GPIO_PIN_7; gpio.Mode GPIO_MODE_AF_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); // PA6MISO 配置为浮空输入 gpio.Pin GPIO_PIN_6; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, gpio); // PA4CS, PA3RSTn 配置为推挽输出初始置高 gpio.Pin GPIO_PIN_4 | GPIO_PIN_3; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); } void W5500_HAL_Init(void) { W5500_GPIO_Init(); __HAL_RCC_SPI1_CLK_ENABLE(); hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // W5500 要求 CPOL0 hspi1.Init.CLKPhase SPI_PHASE_1EDGE; // CPHA1采样沿要对上 hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_8; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi1); // 对 W5500 做硬件复位复位后必须等待芯片稳定 HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_3, GPIO_PIN_SET); HAL_Delay(50); // 注册片选与 SPI 字节读写回调再执行驱动库初始化 reg_wizchip_cs_cbfunc(W5500_CS_Set, W5500_CS_Clr); reg_wizchip_spi_cbfunc(W5500_SPI_ReadByte, W5500_SPI_WriteByte); WIZCHIP_INIT(W5500_Config); }这段代码里有两个参数值得单独说明。SPI 时钟极性 CPOL 和相位 CPHA 必须按 W5500 数据手册配成 CPOL0、CPHA1也就是 SPI 模式 3配错之后读到的寄存器全是乱码而且这种故障用示波器都不一定能一眼看出来。波特率预分频先给到 8对应主频 72MHz 时 SPI 时钟 9MHz等传输稳定性验证过再往上提W5500 的 SPI 极限速度虽然更高但实际博文线长和插座接触都会限制速率远程升级场景优先稳而不是快。硬件复位之后要至少等 50msW5500 内部 PHY 上电链路建立需要时间等不够会出现能读寄存器但 TCP 连不上的怪问题。网络参数和 socket 的配置是紧接着的一步。MAC 地址、本机 IP、网关、子网掩码通过驱动库的 set 系列函数写入然后打开一个 TCP socket 并进入监听状态。// upgrade_net.c —— 网络参数配置与升级 socket 建立 #include wizchip.h void Upgrade_NetConfig(uint8_t *mac, uint8_t *ip, uint8_t *gw, uint8_t *mask) { uint8_t tmp[6]; memcpy(tmp, mac, 6); setSHAR(tmp); // 写入 MAC 地址 setSIPR(ip); // 写入本机 IP setGAR(gw); // 写入网关地址 setSUBR(mask); // 写入子网掩码 } uint8_t Upgrade_SocketListen(uint16_t port) { uint8_t sn 0; // 驱动库 socket() 成功返回 socket 编号失败返回 0 if (socket(sn, Sn_MR_TCP, port, 0x00) sn) { listen(sn); return 1; } return 0; }MAC 地址在批量生产时建议从芯片唯一 ID 派生避免多台设备同网段 MAC 冲突IP 则按现场网络规划分配。socket 的收发缓冲大小由驱动默认分配每个 socket 默认 2KB 发送加 2KB 接收远程升级一包数据通常不超过 1KB默认配置够用。如果要并发处理多条连接socket 数量和缓冲配比需要在 W5500 的 socket 寄存器里重新分配那属于进阶场景本文最后会提一句。3.2 自定义升级协议帧结构、握手与固件分包策略TCP 是字节流协议没有天然的消息边界所以必须在应用层自定义帧结构。我把升级协议设计成 5 种帧握手、开始升级、固件数据、升级结束、复位执行。所有帧共用同一套头尾格式统一走 CRC32 校验。上位机先建立 TCP 连接发握手帧确认设备在升级模式随后发开始升级帧携带固件总长度和整体 CRC32接着发固件数据帧一包一包地把固件内容传完最后发结束帧设备收到后先整体校验再决定是否置位跳转标志。帧类型命令字载荷内容说明握手请求0x10无设备收到后返回设备ID和Bootloader版本开始升级0x21固件总长度(4B) 固件CRC32(4B)设备检查长度是否超出App区容量固件数据0x22偏移地址(4B) 固件数据(最多1024B)偏移从0x08008000基址开始计算升级结束0x23无设备对已写入区做整体CRC校验复位执行0x24无设备置跳转标志并软复位固件数据帧的载荷大小选 1024 字节是一个平衡值。W5500 的 socket 接收缓冲默认 2KB一包 1024B 加上帧头帧尾单帧不会把缓冲撑爆Flash 按 32 位写入1024 也是 4 的整数倍省掉尾部补位逻辑再往上提高到 1400B 以上会碰到以太网 MTU 的分片边界容易出现传输层重传导致的乱序。整个传输过程采用停等协议设备每收到一包写完 Flash 后回一个 ACK上位机收到 ACK 才发下一包。这样的吞吐不是最高的但能把 Flash 擦写阻塞和网络接收天然错开不会出现一边擦 Flash 一边 W5500 缓冲溢出丢包的窘境。协议的解析骨架可以抽象成下面这段代码它把 TCP 流里的一段字节解析成一个结构体供上层分发处理// upgrade_proto.c —— 升级帧解析骨架 #define FRAME_SOF 0xA5 #define CMD_HANDSHAKE 0x10 #define CMD_START 0x21 #define CMD_DATA 0x22 #define CMD_FINISH 0x23 #define CMD_RESET 0x24 typedef struct { uint8_t cmd; uint16_t payload_len; uint8_t *payload; // 指向帧内载荷起点 } Frame_t; // 帧格式SOF(1B) 命令字(1B) 长度(2B) 载荷(NB) CRC32(4B) int Upgrade_ParseFrame(uint8_t *buf, uint16_t len, Frame_t *out) { if (len 8 || buf[0] ! FRAME_SOF) return -1; // 帧头不对直接丢弃 uint32_t crc_recv 0; memcpy(crc_recv, buf len - 4, 4); if (crc32(buf, len - 4) ! crc_recv) return -2; // CRC 不匹配要求重传 out-cmd buf[1]; out-payload_len (uint16_t)((buf[2] 8) | buf[3]); out-payload buf[4]; return 0; }crc32 函数可以用常见的查表法实现这里不再展开。要注意上位机和设备端必须用同一张多项式表CRC 的初值和结果异或值也要一致否则会出现“本地电脑校验没问题、设备端全程报错”的怪象。帧解析里把 CRC 放在帧尾而不是帧头目的是让设备端可以一边接收一边把整帧数据存进一个临时缓冲收满后再统一校验逻辑更简单。4. 固件写入与跳转实现把收到的固件安全落进Flash网络通道打通之后真正决定升级成败的是 Flash 擦写和跳转这两段代码。Flash 的擦写有硬件约束代码写得再花哨也绕不过页对齐、字对齐和擦写期间的时序隔离跳转则是从 Bootloader 世界切到 App 世界的“交接仪式”漏一个细节就是复位循环。4.1 Flash擦除与分包写入对齐、页边界与擦写时序写 App 区之前必须先整片擦除Flash 擦除后的状态是全 0xFF写操作只能把 1 写成 0。擦除是嵌入式开发里最容易被玄学支配的操作擦除超时、擦到一半来中断、写入地址不对齐任何一个都会让 MCU 直接掉进 HardFault。用 HAL 库做擦除时需要构造一个 FLASH_EraseInitTypeDef 结构体把起始页地址和页数填进去然后调用 HAL_FLASHEx_Erase。// flash_if.c —— App 区整片擦除与数据写入 #include flash_if.h #include stm32f1xx_hal.h #define APP_BASE_ADDR 0x08008000u // Bootloader 占前 32KB #define APP_MAX_SIZE 0x00020000u // App 区最大容量此处按 128KB 预留给业务固件 #define FLASH_PAGE_SIZE 0x00000800u // 按目标芯片实际页大小填写中容量为 0x400 static uint32_t GetAppPageCount(void) { return (APP_MAX_SIZE FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE; } uint8_t Flash_EraseApp(void) { FLASH_EraseInitTypeDef erase; uint32_t page_error 0; erase.TypeErase FLASH_TYPEERASE_PAGES; erase.PageAddress APP_BASE_ADDR; erase.NbPages GetAppPageCount(); HAL_FLASH_Unlock(); if (HAL_FLASHEx_Erase(erase, page_error) ! HAL_OK) { HAL_FLASH_Lock(); return 1; // 擦除失败page_error 指向出错的页号 } HAL_FLASH_Lock(); return 0; } uint8_t Flash_WritePacket(uint32_t addr, uint8_t *data, uint16_t len) { uint32_t word 0xFFFFFFFF; uint16_t i 0; // 越界保护写地址超出 App 区范围直接拒绝 if ((addr APP_BASE_ADDR) || (addr len APP_BASE_ADDR APP_MAX_SIZE)) return 1; HAL_FLASH_Unlock(); // 按 32 位字写入Flash 写入的最小宽度是半字字写入效率更高 for (i 0; i 4 len; i 4) { memcpy(word, data i, 4); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i, word) ! HAL_OK) { HAL_FLASH_Lock(); return 2; } } // 尾部不足 4 字节时补 0xFF 凑成一个字再写 if (i len) { word 0xFFFFFFFF; memcpy(word, data i, len - i); if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr i, word) ! HAL_OK) { HAL_FLASH_Lock(); return 2; } } HAL_FLASH_Lock(); return 0; }代码里的 FLASH_PAGE_SIZE 宏必须按实际型号核对不同容量的 STM32F1 页大小不同写错会出现擦除范围错位最常见的结果是 Bootloader 区被擦掉一大块。写入函数里的越界保护不是可有可无的防御而是升级协议里最后一道保险——如果上位机传来的偏移地址被算错写穿 App 区直接覆盖到控制区甚至 Bootloader设备就没救了。尾部补 0xFF 的逻辑保证任何长度的数据包都能按字对齐写入避免 HAL_FLASH_Program 收到未对齐地址时报参数错误。擦写期间的中断处理是一个容易被忽略的细节。Flash 擦除和写入期间CPU 访问 Flash 会被硬件暂停如果此时来了一个中断中断服务程序里恰好又读了 Flash 里的常量或函数指针会造成总线访问冲突。升级主循环在调用擦除函数之前我会把可能抢占的中断源关掉擦写完成再打开串口和 W5500 的中断尤其要处理干净否则会偶发 HardFault。4.2 跳转到App的完整动作与中断向量表重映射固件写完并且整体校验通过之后Bootloader 的工作就剩下把 CPU 交给 App。跳转不是简单地用函数指针调用 App 的 reset handler 就行必须按顺序做四件事校验 App 区开头的栈顶地址和复位向量是否合法、关闭全局中断、把主栈指针 MSP 切到 App 的栈顶、最后才跳转。下面这段跳转函数是这套流程的标准实现。// jump_app.c —— 从 Bootloader 跳转到 App #define APP_ADDR 0x08008000u typedef void (*pFunction)(void); void JumpToApp(void) { uint32_t app_sp *(volatile uint32_t *)APP_ADDR; uint32_t app_pc *(volatile uint32_t *)(APP_ADDR 4); pFunction app_entry; // 栈顶地址必须落在 SRAM 区域否则说明镜像不完整 if ((app_sp 0x2FFF0000u) ! 0x20000000u) return; // 复位向量是 Thumb-2 编码最低位必须为 1 if ((app_pc 0x00000001u) 0) return; __disable_irq(); // 关闭全局中断防止跳转过程中断乱入 SysTick-CTRL 0; // 停掉 SysTick避免跳转后立刻进中断 __set_MSP(app_sp); // 重新设置主栈指针为 App 的栈顶 app_entry (pFunction)app_pc; app_entry(); // 跳转后不会返回Bootloader 使命结束 }跳转前关闭中断这步很多初版 Bootloader 都会漏掉。跳转瞬间 App 的栈指针和中断向量表还没生效此时任何一个外设中断触发CPU 会按旧向量表去取中断函数地址轻则跑飞重则直接 HardFault。SysTick 也要停掉否则跳转后 App 尚未重配中断向量表SysTick 到期中断会第一时间打进来。跳转前最好还把已经打开的 W5500、SPI、串口外设时钟一并关掉或复位让 App 从干净的外设状态开始初始化。App 侧要做的对应动作是把中断向量表重映射到自身。STM32F1 系列虽然不像 F4 那样有完整的向量表重定位寄存器但 SCB-VTOR 在绝大多数 F1 型号上可用App 启动后在 main 函数最前面执行一行即可。// app_main.c —— App 侧启动时重映射中断向量表 #define APP_BASE_ADDR 0x08008000u int main(void) { SCB-VTOR APP_BASE_ADDR; // 中断向量表偏移到 App 区起始处 // 其余的时钟、外设初始化... }注意如果目标型号的 SCB-VTOR 不可用或者编译器启动文件在进入 main 前就依赖向量表需要在链接脚本里把 App 的中断向量段直接放到 0x08008000 地址并在启动汇编里保证向量表对齐。两种做法选一种即可但绝不能什么都不做。5. 远程升级最容易翻车的5个地方现象、原因与排查远程升级的坑集中在网络传输、Flash 操作和跳转交接这三段。下面五条是我在调试过程中反复遇到的真实问题每条按现象、原因、解决三个层次拆开排查时对着看能省很多时间。5.1 升级后直接复位循环中断向量表没有重映射现象固件写入成功跳转也执行了但设备既不正常运行也不停在 Bootloader而是不断复位。串口打印停在 App 启动早期有时能跑几行初始化代码就死掉看门狗喂不上整机循环重启。原因App 里没有重映射中断向量表。跳转到 App 后第一个中断一来CPU 仍然按照 0x08000000 处的 Bootloader 向量表取中断函数地址取到的地址往往是 Bootloader 里的函数或者无效地址执行后立刻 HardFaultHardFault 处理函数又没写好最后走到复位。有些情况下 SCB-VTOR 写了但写晚了在启动文件执行到 main 之前已经有中断发生同样复现这个现象。解决把 SCB-VTOR 的设置放到 App 启动文件里紧跟系统时钟初始化之后、任何外设中断使能之前。如果确认型号的 VTOR 不可用就改用链接脚本分段方案把向量表整体放到 App 区起始处。排查时可以临时在跳转前禁止所有中断并在 App 第一行代码里点亮一个 LED观察灯亮不亮来判断是根本没跳过去还是跳过去之后中断崩了。5.2 擦写Flash时进HardFault对齐与中断抢占现象升级过程中偶发死机位置不固定有时擦除到一半就挂有时写了几百包才挂。重新上电后 App 区是半擦半写状态控制区标志混乱设备既不进升级也不进 App。原因第一类是地址对齐问题写入地址不是字对齐或者页擦除的起始地址不在页边界HAL 库直接报错触发断言。第二类更隐蔽擦写 Flash 期间有中断抢占而中断服务函数里访问了 Flash 区域的数据比如查表、取字符串常量硬件上 Flash 正在被擦写CPU 同时读 Flash 造成总线互斥最终 HardFault。W5500 的接收中断在升级期间尤其活跃是最常见的肇事者。解决升级流程进入 Flash 擦写前把 W5500 中断、定时器中断、串口中断全部关掉擦写完成再恢复。升级主循环改为“收包 → 关中断 → 写 Flash → 开中断 → 回 ACK”的串行结构保证 Flash 操作期间没有任何中断源竞争。对齐问题则通过代码里的越界和地址检查提前拦截擦除函数的起始地址必须由分区表的页边界计算而来不要手写地址常量。5.3 W5500传输丢包、校验失败SPI速率与缓冲管理现象传输几百包后出现一帧 CRC 错误设备要求重传重传后可能连续错几包甚至 TCP 连接直接断开。查 W5500 寄存器发现接收缓冲剩余字节数比预期少有时读到的数据和上位机发的数据不一致。原因最常见的是 SPI 时钟跑太快线缆稍长或接触电阻大时信号边沿劣化W5500 读回来的数据偶发错位。另一个原因是 W5500 的接收缓冲读取时机不对驱动库的 recv() 返回的是实际读取字节数如果代码里用固定长度去读会把缓冲区的读指针推进错位后续所有数据全部错位。还有一部分是 socket 缓冲区溢出了上位机发送速度太快设备还没读完新数据已经把旧数据覆盖。解决先把 SPI 预分频降到 8 甚至 16确认连续传输 1000 包无误再逐步提速。读取 W5500 数据时严格按 recv() 的返回值处理不要假设每次都能读到期望长度。协议层加上每包 ACK 的停等机制上位机必须等设备写 Flash 完毕回 ACK 后才发下一包。排查时在设备端打印每一帧的序号和 CRC能快速定位是链路丢包还是设备端处理不及时。5.4 升级过程中掉电设备从此变砖缺少完整性校验与回滚现象升级进行到一半现场断电重新上电后设备既不能进 App 也不能耐着性子等升级Bootloader 里没有进入升级模式的人口设备直接瘫在那里只能开壳用烧录器救砖。原因Bootloader 只看了控制区里的“升级中”标志但 App 区只写了一半跳转过去必然崩溃同时 Bootloader 又没做“App 无效则自动进入升级模式”的逻辑既跳不了 App 也不主动收固件整个设备没有自救路径。根本原因是把“收到完整固件”和“具备可执行 App”这两件事混为一谈了。解决升级控制区里必须存一个完整固件的校验信息包括固件长度和整体 CRC32。Bootloader 每次上电先读控制区如果标志是“App 有效”再校验 App 区的 CRCCRC 通过才跳转任何一项不满足都进入升级模式等待新固件。开始升级时先把标志写成“升级中”全部数据收完并整体校验通过后才把标志改成“App 有效”。这样升级中掉电上电后 CRC 不过设备自动回到升级等待状态重新发一次固件就能救回。5.5 看门狗把升级过程打断喂狗策略与断点续传现象升级过程中设备毫无征兆地复位复位后又进升级模式从头再传传一会儿又复位。有时能传完纯看运气。观察复位原因寄存器发现是独立看门狗 IWDG 复位。原因Bootloader 里开了 IWDG升级主流程是“收包 → 擦写 → 回 ACK”整片擦除 App 区可能耗时几百毫秒到一两秒超出喂狗周期狗先叫了。升级过程的耗时和固件大小、Flash 页数正相关小固件侥幸能跑完大固件必然超时。解决在升级专用的主循环里喂狗喂狗点放在每包数据处理完成后、开始下一包等待之前。如果擦写一页的时间超过喂狗周期就把整片擦除拆成逐页擦除每擦完一页喂一次狗。更彻底的做法是在握手后关掉 IWDG升级完成跳转前由 App 重新初始化看门狗。断点续传则需要在控制区记录上次已写入的偏移Bootloader 收到开始升级帧时对比偏移决定是从头写还是从断电点继续。断点续传能省大量重传时间但控制区的状态机要设计得更严谨建议先做通整包重传再做续传。6. 让升级更稳的双Bank思路与验证习惯单分区方案在断电故障下有“回升级模式重传”兜底已经能应付大多数现场。如果设备在升级期间不允许长时间离线或者固件体积接近 App 区上限可以升级成软件双 Bank 方案把 App 可用空间拆成 A、B 两个区新固件总是写入当前没有运行的那个区写入并校验通过后改控制区里的“当前有效区”指针下次复位从新区启动。这样升级过程中掉电旧区还是完整的重启后 Bootloader 发现新区无效会自动回到旧区运行设备不会停摆。对比项单 App 区双 Bank A/B断电事故影响需重传整个固件自动回到旧区运行App 可用容量约占 Flash 70%约占 Flash 35%升级流程复杂度较低多一套区切换逻辑适合场景成本敏感、可容忍短暂离线高可用现场、难以上门维护双 Bank 的实现难点在控制区的状态机要从“当前 A 区有效”平滑切换到“当前 B 区有效”中间必须经历“新固件写入未完成”和“新固件校验通过待切换”两个中间态任何一步掉电都不能让两个区都失效。代码上只是把跳转地址从固定值改成一个变量纯粹是设计层面对细节的要求。我自己的习惯是每次发布升级方案改动后先跑三轮验证第一轮用一个 CRC 错误的假固件走全流程确认设备拒绝升级且保持当前区不变第二轮做随机断电测试在擦写循环里任意时刻断电上电后必须回到升级等待或旧区运行第三轮用一段超过 App 区容量一半的随机数据压测传输跑通后再测真实固件。这套流程看着笨但救过我很多次——远程升级这个功能一旦发布出去就没有在实验室里重来的机会了。希望你手里的设备也能从这个方案里少踩几个坑。本文还有配套的精品资源点击获取