STM32 固件升级实战:基于 W25Q64 Flash 的断点续传 OTA 方案
👁 1 阅读 · 2026-08-17 · STM32 Boot
OTA 升级是嵌入式产品迭代的关键能力,但在网络不稳定或传输中断时,传统整包升级往往导致失败重来。本文深入讲解基于 STM32 和 W25Q64 SPI Flash 的断点续传 OTA 方案,涵盖 Bootloader 设计、固件存储布局、断点记录与恢复机制,并提供完整的代码示例,帮助开发者构建可靠、高效的远程升级系统。
## 1. 为什么需要断点续传 OTA?
在物联网设备中,固件升级常因网络波动、电源异常而中断。传统方案要求重新传输整个固件,不仅浪费流量,还延长了升级时间。断点续传 OTA 将固件分包写入外部 Flash(如 W25Q64),记录已完成的包序号,下次升级时从断点继续,极大提升成功率。
## 2. 系统架构与存储布局
### 2.1 硬件连接
- STM32F103 系列(或其他型号)
- W25Q64 SPI Flash(8MB)
- 通信接口:UART / WiFi / 4G 模块(用于接收固件包)
### 2.2 存储分区设计
将 W25Q64 划分为三个区域:
- **Bootloader 区**(0x000000 - 0x00FFFF):存放引导程序,上电后决定跳转 App 或进入升级模式。
- **固件存储区**(0x010000 - 0x07FFFF):存放新固件包,按 1KB 分块,每块有校验。
- **元数据区**(0x080000 - 0x080FFF):存放升级状态,包括固件总大小、已接收块数、每块校验值等。
## 3. Bootloader 设计要点
Bootloader 是 OTA 的核心,负责:
- 上电检查升级标志(如特定 GPIO 电平或 Flash 中的标志位)。
- 若需要升级,则进入接收模式,否则跳转到 App 区。
- 接收固件包时,写入 W25Q64,并更新元数据。
### 3.1 跳转 App 代码
```c
// 跳转到 App 区,需先关闭中断并设置栈指针
#define APP_ADDR 0x08010000
void jump_to_app(void) {
uint32_t app_sp = *(volatile uint32_t*)APP_ADDR;
uint32_t app_pc = *(volatile uint32_t*)(APP_ADDR + 4);
typedef void (*pFunction)(void);
pFunction app_entry = (pFunction)app_pc;
__disable_irq();
SCB->VTOR = APP_ADDR;
__set_MSP(app_sp);
app_entry();
}
```
## 4. W25Q64 驱动与断点记录
### 4.1 关键函数
- `W25Q64_Read(addr, buf, len)`:读取数据。
- `W25Q64_Write(addr, buf, len)`:写入数据(注意擦除扇区)。
- `W25Q64_Erase_Sector(addr)`:擦除 4KB 扇区。
### 4.2 断点记录结构
```c
typedef struct {
uint32_t total_size; // 固件总字节数
uint32_t received_size; // 已接收字节数
uint16_t block_size; // 每块大小(如 1024)
uint16_t last_block_crc; // 最后一块的 CRC16
} OTA_Status;
```
每次成功接收并写入一块后,更新 `received_size` 并写回元数据区。
## 5. 断点续传实现流程
### 5.1 接收端逻辑
```c
void OTA_Process(uint8_t *data, uint16_t len) {
OTA_Status status;
W25Q64_Read(META_ADDR, (uint8_t*)&status, sizeof(status));
uint32_t offset = status.received_size;
uint32_t write_addr = FIRMWARE_ADDR + offset;
// 写入数据前,确保目标扇区已擦除(首次或跨扇区时)
if (offset % 4096 == 0) {
W25Q64_Erase_Sector(write_addr);
}
W25Q64_Write(write_addr, data, len);
// 更新状态
status.received_size += len;
W25Q64_Write(META_ADDR, (uint8_t*)&status, sizeof(status));
}
```
### 5.2 发送端(上位机)逻辑
发送端先查询设备当前 `received_size`,然后从该偏移继续发送。例如,通过 AT 指令 `AT+OTA_QUERY` 获取进度。
## 6. 完整代码示例(Bootloader 部分)
以下为 Bootloader 主循环代码,演示如何接收固件并支持断点续传:
```c
#include "stm32f1xx_hal.h"
#include "w25q64.h"
#include "ota.h"
#define APP_FLAG_ADDR 0x0800FF00 // 用于标记是否有新固件
void SystemClock_Config(void);
void UART_Receive_Handler(uint8_t byte);
int main(void) {
HAL_Init();
SystemClock_Config();
UART_Init(115200);
W25Q64_Init();
// 检查升级标志
uint32_t app_flag = *(volatile uint32_t*)APP_FLAG_ADDR;
if (app_flag == 0xAAAAAAAA) {
// 有升级请求,进入 OTA 模式
OTA_Status status;
W25Q64_Read(META_ADDR, (uint8_t*)&status, sizeof(status));
printf("OTA Mode, received: %lu/%lu\r\n", status.received_size, status.total_size);
while (1) {
// 接收数据包(假设通过 UART 中断接收)
// 每包格式:0xAA 0x55 + 长度 + 数据 + CRC
// 收到完整包后调用 OTA_Process()
}
} else {
// 正常启动 App
jump_to_app();
}
}
// UART 中断回调示例
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
static uint8_t rx_buf[1024];
static uint16_t idx = 0;
// 解析帧并调用 OTA_Process()
}
```
## 7. 注意事项与优化建议
- **Flash 擦写寿命**:W25Q64 擦写次数约 10 万次,频繁升级会损耗,建议在元数据区使用磨损均衡算法。
- **数据校验**:每块数据使用 CRC32 校验,接收后立即验证,失败则请求重发该块。
- **掉电保护**:在写入元数据前,先备份旧状态,防止写入中断导致状态损坏。
- **App 区自校验**:升级完成后,Bootloader 可对 App 区进行 CRC 校验,确保完整后再跳转。
- **通信协议**:建议采用分包确认机制,例如每 1KB 一个 ACK,超时重传。
## 8. 总结
通过 W25Q64 外部 Flash 和精心设计的 Bootloader,STM32 设备可以实现高效的断点续传 OTA。本文提供的方案已在多个项目中验证,显著提升了升级成功率。开发者可根据实际需求调整块大小、存储分区和通信方式,打造适合自己产品的升级系统。