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 代码

// 跳转到 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 断点记录结构

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 接收端逻辑

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 主循环代码,演示如何接收固件并支持断点续传:

#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。本文提供的方案已在多个项目中验证,显著提升了升级成功率。开发者可根据实际需求调整块大小、存储分区和通信方式,打造适合自己产品的升级系统。