OTA (Over-The-Air) 升级技术文档

目录

  1. 概述
  2. 系统架构
  3. 状态机设计
  4. 升级流程详解
  5. 应用跳转机制
  6. 防变砖机制
  7. 异常情况处理
  8. 实现要点
  9. 移植指南

概述

本文档描述了一个通用的双应用区(Dual Bank)OTA升级系统设计,适用于嵌入式MCU平台。该系统通过维护两个独立的应用程序区域(APP1和APP2),实现安全的固件升级,确保在任何情况下至少有一个可用的应用程序。

核心特性

  • 双应用区设计:维护两个独立的应用程序区域,升级时写入非活动区域
  • 状态持久化:升级状态和活动应用标志存储在非易失性存储器中
  • 断点续传支持:支持升级过程中的中断恢复
  • 数据完整性校验:CRC校验、应用验证等多重保障
  • 防变砖机制:多重回退策略,确保系统可恢复性
  • 确认机制:基于确认的数据传输,支持重传

系统架构

2.1 内存布局

内部Flash布局
┌─────────────────────────────────────┐
│ Bootloader区域 (固定大小)            │
│ - 启动代码                           │
│ - OTA升级逻辑                        │
│ - 应用验证与跳转                     │
├─────────────────────────────────────┤
│ APP_1区域 (应用程序1)                │
│ - 完整的应用程序代码                 │
│ - 向量表                             │
├─────────────────────────────────────┤
│ APP_2区域 (应用程序2)                │
│ - 完整的应用程序代码                 │
│ - 向量表                             │
└─────────────────────────────────────┘

设计原则

  • Bootloader区域在系统生命周期内保持不变
  • APP_1和APP_2大小相同,便于管理
  • 两个应用区域互不重叠,确保升级安全
外部存储布局(可选)

如果使用外部Flash(如SPI Flash)存储OTA固件:

┌─────────────────────────────────────┐
│ OTA固件存储区                        │
│ - 待升级的固件数据                   │
│ - 大小:至少等于单个应用区域大小     │
├─────────────────────────────────────┤
│ OTA状态存储区                        │
│ - OTA状态标志                        │
│ - 活动应用标志                       │
│ - 固件大小信息                       │
│ - 其他元数据                         │
└─────────────────────────────────────┘

2.2 关键组件

  1. Bootloader:负责系统启动、OTA升级、应用验证和跳转
  2. 应用程序:用户业务逻辑,通过通信接口接收OTA命令
  3. 通信协议:定义OTA数据传输格式和命令集
  4. 存储管理:管理内部Flash和外部存储的读写操作

状态机设计

3.1 OTA状态定义

typedef enum {
    OTA_STATE_IDLE = 0,           // 空闲状态:无OTA操作
    OTA_STATE_DOWNLOADING,        // 下载中:正在接收固件数据
    OTA_STATE_READY,              // 准备升级:固件下载完成,等待升级
    OTA_STATE_UPGRADING,          // 升级中:正在将固件写入目标应用区
    OTA_STATE_SUCCESS,            // 升级成功:固件已成功写入并验证
    OTA_STATE_FAILED              // 升级失败:升级过程中出现错误
} ota_state_t;

3.2 状态转换图

    [IDLE]
      │
      │ 收到OTA开始命令
      ▼
[DOWNLOADING] ──┐
      │         │
      │ 数据包  │ 数据包丢失/错误
      │ 接收    │
      ▼         │
[DOWNLOADING] ──┘
      │
      │ 收到完成命令
      ▼
   [READY]
      │
      │ 系统重启/升级触发
      ▼
[UPGRADING]
      │
      ├─ 成功 ──► [SUCCESS] ──► [IDLE]
      │
      └─ 失败 ──► [FAILED] ──► [IDLE]

3.3 状态持久化

存储位置:非易失性存储器(内部Flash或外部Flash)

存储内容

  • OTA状态字节(1字节)
  • 活动应用标志(1字节):0x01=APP_1, 0x02=APP_2
  • 固件大小(4字节):实际固件大小
  • 其他元数据(可选)

更新时机

  • 状态转换时立即写入
  • 活动应用切换时立即写入
  • 固件大小确定时写入

升级流程详解

4.1 完整升级流程

阶段1:OTA启动
1. 应用程序收到OTA开始命令
2. 检查当前OTA状态
   ├─ 如果状态为DOWNLOADING或READY → 清除旧状态,重新开始
   └─ 如果状态为IDLE → 继续
3. 设置OTA状态为DOWNLOADING
4. 擦除外部存储的OTA固件区域
5. 初始化OTA相关变量
   - 接收偏移量 = 0
   - 期望包ID = 0
   - 接收统计清零
6. 发送确认,准备接收数据

关键点

  • 检测并清理未完成的OTA升级,避免状态不一致
  • 擦除外部存储确保没有旧数据干扰
阶段2:数据接收
循环处理:
1. 接收数据包
2. 解析帧结构
   - 验证帧头、帧尾
   - 校验CRC
   - 提取包ID和数据
3. 判断包类型
   ├─ 新包(包ID = 期望包ID)
   │  ├─ 计算写入地址 = 起始地址 + 接收偏移量
   │  ├─ 检查是否需要擦除扇区
   │  │  └─ 如果跨扇区,擦除所有涉及的扇区
   │  ├─ 写入外部存储
   │  ├─ 更新接收偏移量和期望包ID
   │  └─ 标记该包已接收
   │
   ├─ 重传包(包ID < 期望包ID)
   │  ├─ 检查是否已接收
   │  │  ├─ 已接收 → 跳过,返回确认
   │  │  └─ 未接收 → 写入外部存储
   │  └─ 更新期望包ID(如果补齐了缺失的包)
   │
   └─ 未来包(包ID > 期望包ID)
      └─ 请求重传缺失的包
4. 发送确认响应
5. 更新接收统计

关键点

  • 跨扇区擦除:如果数据包跨越多个扇区,必须擦除所有涉及的扇区
  • 重传处理:已接收的包不再重复写入,避免浪费和错误
  • 包ID管理:使用包ID跟踪接收进度,支持乱序接收
阶段3:接收完成
1. 收到OTA接收完成命令
2. 验证接收的数据量
   ├─ 与预期大小匹配 → 继续
   └─ 不匹配 → 报告错误,保持DOWNLOADING状态
3. 擦除OTA状态扇区(确保干净写入)
4. 写入固件大小到外部存储
5. 验证写入的固件大小
6. 设置OTA状态为READY
7. 发送确认响应

关键点

  • 擦除状态扇区后再写入,避免旧数据干扰
  • 验证写入的元数据,确保完整性
阶段4:固件升级(Bootloader中执行)
1. 系统重启,进入Bootloader
2. 读取OTA状态
   ├─ 如果状态为READY → 执行升级
   └─ 否则 → 跳转到应用程序
3. 确定目标应用
   - 当前活动应用 = APP_1 → 目标 = APP_2
   - 当前活动应用 = APP_2 → 目标 = APP_1
4. 设置OTA状态为UPGRADING
5. 从外部存储读取固件
6. 擦除目标应用区域
7. 写入固件到目标应用区域
   - 按扇区/页对齐写入
   - 使用MCU特定的写入方式(如QUADWORD)
   - 每写入一段后验证
8. 验证目标应用
   - 检查向量表
   - 检查栈指针范围
   - 检查Reset Handler地址
   - 检查Flash是否被擦除
9. 如果验证通过
   ├─ 设置目标应用为活动应用
   ├─ 更新活动应用标志(持久化)
   ├─ 设置OTA状态为SUCCESS
   └─ 跳转到新应用
10. 如果验证失败
    ├─ 设置OTA状态为FAILED
    ├─ 尝试恢复默认固件(如果存在)
    └─ 跳转到原活动应用

关键点

  • 升级过程在Bootloader中执行,确保即使升级失败也能恢复
  • 写入前必须擦除目标区域
  • 使用MCU特定的对齐要求(如STM32U5的16字节QUADWORD对齐)
  • 写入后立即验证,确保数据正确

4.2 数据包格式

帧结构
┌─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┬─────────┐
│ Header  │ Command │ Address │Packet ID│Data Len │  Data   │   CRC   │  Tail   │
│ 2 bytes │ 1 byte  │ 2 bytes │ 2 bytes │ 2 bytes │ N bytes │ 1 byte  │ 2 bytes │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┴─────────┘
  • Header:帧头标识,如 0xAA 0xBB
  • Command:命令类型
    • 0x01:OTA数据包
    • 0x02:OTA开始
    • 0x03:OTA接收完成
    • 0x04:OTA完成
  • Address:设备地址
  • Packet ID:包序号,用于重传和顺序控制
  • Data Len:数据长度
  • Data:实际数据
  • CRC:CRC8校验和
  • Tail:帧尾标识,如 0xCC 0xDD
CRC计算
  • 算法:CRC8(或其他合适的校验算法)
  • 计算范围:从Command到Data结束的所有字节
  • 目的:检测数据传输错误

应用跳转机制

5.1 应用验证

在跳转到应用程序之前,必须验证应用程序的有效性:

bool verify_app(uint32_t app_addr) {
    // 1. 检查地址范围
    if (app_addr < APP_1_START || app_addr > APP_2_END) {
        return false;
    }
    
    // 2. 检查地址对齐(通常要求4字节对齐)
    if (app_addr % 4 != 0) {
        return false;
    }
    
    // 3. 读取向量表
    uint32_t stack_pointer = *(volatile uint32_t*)app_addr;
    uint32_t reset_handler = *(volatile uint32_t*)(app_addr + 4);
    
    // 4. 验证栈指针
    // - 必须在RAM范围内
    // - 必须8字节对齐(Cortex-M要求)
    if (stack_pointer < RAM_START || stack_pointer > RAM_END) {
        return false;
    }
    if (stack_pointer % 8 != 0) {
        return false;
    }
    
    // 5. 验证Reset Handler地址
    // - 必须在对应应用区域内
    // - 最低位必须为1(Thumb模式)
    if ((reset_handler & 0x1) == 0) {
        return false;
    }
    if (app_addr == APP_1_START) {
        if (reset_handler < APP_1_START || reset_handler > APP_1_END) {
            return false;
        }
    } else {
        if (reset_handler < APP_2_START || reset_handler > APP_2_END) {
            return false;
        }
    }
    
    // 6. 检查Flash是否被擦除(全0xFF)
    uint32_t empty_count = 0;
    for (uint32_t i = 0; i < 256; i += 4) {
        if (*(volatile uint32_t*)(app_addr + i) == 0xFFFFFFFF) {
            empty_count++;
        }
    }
    if (empty_count >= 64) {  // 前256字节都是0xFF
        return false;
    }
    
    return true;
}

5.2 跳转流程

void jump_to_app(uint32_t app_addr) {
    // 1. 失效指令缓存(如果启用)
    // 确保读取到最新的Flash数据
    if (icache_enabled()) {
        invalidate_icache();
    }
    
    // 2. 清理并失效数据缓存(如果启用)
    if (dcache_enabled()) {
        clean_dcache();
        invalidate_dcache();
    }
    
    // 3. 读取向量表
    uint32_t stack_pointer = *(volatile uint32_t*)app_addr;
    uint32_t reset_handler = *(volatile uint32_t*)(app_addr + 4);
    
    // 4. 验证向量表
    if (stack_pointer == 0xFFFFFFFF || reset_handler == 0xFFFFFFFF) {
        // 错误处理
        while(1);
    }
    
    // 5. 关闭所有中断
    __disable_irq();
    
    // 6. 重置所有中断到默认状态
    // 清除所有挂起的中断
    for (int i = 0; i < NUM_IRQS; i++) {
        NVIC_DisableIRQ(i);
        NVIC_ClearPendingIRQ(i);
    }
    
    // 7. 设置向量表偏移(如果支持)
    // SCB->VTOR = app_addr;
    
    // 8. 设置栈指针
    __set_MSP(stack_pointer);
    
    // 9. 设置程序计数器
    typedef void (*app_entry_t)(void);
    app_entry_t app_entry = (app_entry_t)(reset_handler & ~0x1);
    
    // 10. 内存屏障,确保所有操作完成
    __DSB();
    __ISB();
    
    // 11. 跳转到应用程序
    app_entry();
    
    // 不会执行到这里
}

关键点

  • 必须在关闭中断前完成所有需要延时的操作(如缓存操作)
  • 必须正确设置栈指针和程序计数器
  • 使用内存屏障确保操作顺序
  • 跳转后不会返回

5.3 活动应用选择

app_select_t get_active_app(void) {
    // 从非易失性存储器读取活动应用标志
    uint8_t app_flag = read_from_storage(ACTIVE_APP_FLAG_ADDR);
    
    if (app_flag == 0x01) {
        return APP_SELECT_1;
    } else if (app_flag == 0x02) {
        return APP_SELECT_2;
    }
    
    // 默认返回APP_1
    return APP_SELECT_1;
}

void set_active_app(app_select_t app) {
    uint8_t app_flag = (app == APP_SELECT_1) ? 0x01 : 0x02;
    
    // 更新内存变量
    active_app = app;
    
    // 持久化到非易失性存储器
    // 注意:需要先擦除再写入,确保数据正确
    write_to_storage(ACTIVE_APP_FLAG_ADDR, app_flag);
    
    // 验证写入
    uint8_t verify_flag = read_from_storage(ACTIVE_APP_FLAG_ADDR);
    if (verify_flag != app_flag) {
        // 错误处理
    }
}

防变砖机制

6.1 多重验证

  1. 数据包级验证:CRC校验、帧格式验证
  2. 固件级验证:向量表验证、地址范围检查
  3. 写入验证:写入后立即读取验证

6.2 回退策略

启动流程:
1. 读取活动应用标志
2. 验证活动应用
   ├─ 有效 → 跳转到活动应用 ✅
   └─ 无效 → 尝试另一个应用
        ├─ 有效 → 更新活动应用标志 → 跳转 ✅
        └─ 无效 → 尝试恢复默认固件
             ├─ 恢复成功 → 更新活动应用标志 → 跳转 ✅
             └─ 恢复失败 → 进入死循环(需要外部干预)⚠️

6.3 升级失败处理

如果升级过程中失败:

  1. 固件验证失败

    • 设置OTA状态为FAILED
    • 不更新活动应用标志
    • 跳转到原活动应用
  2. 写入失败

    • 设置OTA状态为FAILED
    • 尝试恢复默认固件(如果存在)
    • 跳转到原活动应用
  3. 外部存储损坏

    • 无法读取OTA状态时,默认跳转到APP_1
    • 如果APP_1无效,尝试APP_2

6.4 中断恢复

如果升级过程中系统断电或重启:

  1. 下载阶段中断

    • 状态为DOWNLOADING
    • 下次启动时检测到未完成状态
    • 清除旧状态,重新开始下载
  2. 准备阶段中断

    • 状态为READY
    • 下次启动时检测到READY状态
    • 可以选择继续升级或清除状态
  3. 升级阶段中断

    • 状态为UPGRADING
    • 目标应用可能不完整
    • 验证目标应用,如果无效则回退到原应用

异常情况处理

7.1 数据包丢失

现象:接收到未来包(包ID > 期望包ID)

处理

  1. 记录丢失的包ID范围
  2. 发送重传请求
  3. 继续接收后续包(支持乱序接收)
  4. 等待重传包到达后写入

7.2 数据包重复

现象:接收到已处理的包(包ID < 期望包ID)

处理

  1. 检查该包是否已写入
  2. 如果已写入,跳过写入,直接返回确认
  3. 如果未写入(之前丢失),执行写入

7.3 数据包损坏

现象:CRC校验失败或帧格式错误

处理

  1. 丢弃该包
  2. 更新统计信息
  3. 等待发送端重传

7.4 外部存储写入失败

现象:写入外部Flash失败

处理

  1. 检查是否已擦除扇区
  2. 如果未擦除,先擦除再写入
  3. 如果跨扇区,确保所有扇区都已擦除
  4. 重试写入(可设置重试次数)

7.5 内部Flash写入失败

现象:写入内部Flash失败

处理

  1. 检查地址对齐(MCU特定要求)
  2. 检查Flash是否已解锁
  3. 检查写入大小是否符合要求(如QUADWORD对齐)
  4. 验证写入的数据
  5. 如果失败,设置OTA状态为FAILED,回退

7.6 应用验证失败

现象:升级后应用验证不通过

处理

  1. 不更新活动应用标志
  2. 设置OTA状态为FAILED
  3. 尝试恢复默认固件
  4. 跳转到原活动应用

7.7 两个应用都损坏

现象:APP_1和APP_2都验证失败

处理

  1. 尝试从外部存储恢复默认固件
  2. 如果恢复成功,验证并跳转
  3. 如果恢复失败,进入死循环
  4. 提供外部恢复接口(如UART命令)

实现要点

8.1 Flash操作

擦除操作
// 擦除前必须解锁Flash
HAL_FLASH_Unlock();

// 执行擦除(MCU特定)
FLASH_EraseInitTypeDef erase_init;
erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
erase_init.Banks = FLASH_BANK_1;
erase_init.Page = page_number;
erase_init.NbPages = num_pages;

uint32_t sector_error = 0;
HAL_FLASHEx_Erase(&erase_init, &sector_error);

// 擦除后锁定Flash
HAL_FLASH_Lock();
写入操作
// STM32U5示例:QUADWORD写入(16字节对齐)
uint32_t quadword_buffer[4] __attribute__((aligned(16)));
memcpy(quadword_buffer, data, 16);

HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD, address, (uint32_t)quadword_buffer);

// 内存屏障,确保写入完成
__DSB();
__ISB();

// 失效指令缓存(如果启用)
SCB_InvalidateICache_by_Addr((uint32_t*)address, 16);

关键点

  • 不同MCU有不同的对齐要求(如STM32U5要求16字节对齐)
  • 写入后必须验证
  • 注意缓存一致性(如果启用缓存)

8.2 状态管理

// 读取状态
ota_state_t read_ota_state(void) {
    uint8_t state_byte = 0;
    read_from_storage(OTA_STATE_ADDR, &state_byte, 1);
    return (ota_state_t)state_byte;
}

// 写入状态
int write_ota_state(ota_state_t state) {
    uint8_t state_byte = (uint8_t)state;
    
    // 1. 擦除状态扇区
    erase_sector(OTA_STATE_ADDR);
    
    // 2. 写入状态字节
    write_to_storage(OTA_STATE_ADDR, &state_byte, 1);
    
    // 3. 验证写入
    uint8_t verify_byte = 0;
    read_from_storage(OTA_STATE_ADDR, &verify_byte, 1);
    if (verify_byte != state_byte) {
        return -1;  // 验证失败
    }
    
    return 0;
}

8.3 包管理

// 包接收状态数组(用于跟踪已接收的包)
#define MAX_PACKET_ID 2048
static bool packet_received[MAX_PACKET_ID];
static uint32_t packet_offset[MAX_PACKET_ID];  // 每个包的写入偏移

// 处理数据包
int handle_ota_packet(uint16_t packet_id, uint8_t *data, uint16_t data_len) {
    bool is_retransmit = (packet_id < expected_packet_id);
    bool already_received = packet_received[packet_id];
    
    if (is_retransmit && already_received) {
        // 重传包且已接收,跳过写入
        return 0;
    }
    
    // 计算写入地址
    uint32_t write_addr = OTA_FIRMWARE_ADDR;
    if (is_retransmit) {
        write_addr += packet_offset[packet_id];
    } else {
        write_addr += current_write_offset;
    }
    
    // 检查并擦除扇区(如果跨扇区,擦除所有涉及的扇区)
    uint32_t sector_start = (write_addr / SECTOR_SIZE) * SECTOR_SIZE;
    uint32_t sector_end = ((write_addr + data_len - 1) / SECTOR_SIZE) * SECTOR_SIZE;
    
    for (uint32_t sector = sector_start; sector <= sector_end; sector += SECTOR_SIZE) {
        if (sector != last_erased_sector) {
            erase_sector(sector);
            last_erased_sector = sector;
        }
    }
    
    // 写入数据
    write_to_storage(write_addr, data, data_len);
    
    // 更新状态
    if (!is_retransmit) {
        packet_offset[packet_id] = current_write_offset;
        packet_received[packet_id] = true;
        current_write_offset += data_len;
        expected_packet_id++;
    } else {
        packet_received[packet_id] = true;
        // 如果补齐了缺失的包,更新期望包ID
        if (packet_id == expected_packet_id) {
            // 需要重新计算期望包ID(找到下一个未接收的包)
            while (packet_received[expected_packet_id]) {
                expected_packet_id++;
            }
        }
    }
    
    return 1;  // 新数据写入
}

8.4 缓存管理

如果MCU启用了指令缓存(ICache)或数据缓存(DCache),必须注意缓存一致性:

// 写入Flash后
__DSB();  // 数据同步屏障
__ISB();  // 指令同步屏障

// 失效指令缓存
SCB_InvalidateICache_by_Addr((uint32_t*)address, size);

// 读取Flash前(如果之前有写入)
SCB_InvalidateICache_by_Addr((uint32_t*)address, size);
__DSB();
__ISB();

移植指南

9.1 平台相关配置

Flash布局定义
// bootloader.h
#define BOOTLOADER_START_ADDRESS    0x08000000
#define BOOTLOADER_SIZE             0x00010000  // 64KB

#define APP_1_START_ADDRESS         0x08010000
#define APP_1_SIZE                  0x00038000  // 224KB

#define APP_2_START_ADDRESS         0x08048000
#define APP_2_SIZE                  0x00038000  // 224KB
RAM范围定义
#define RAM_START                   0x20000000
#define RAM_END                     0x2003FFFF  // 256KB
Flash操作接口
// 需要实现的接口
int flash_erase(uint32_t address, uint32_t size);
int flash_write(uint32_t address, const uint8_t *data, uint32_t size);
int flash_read(uint32_t address, uint8_t *data, uint32_t size);

// 外部存储接口(如果使用)
int external_storage_erase(uint32_t address, uint32_t size);
int external_storage_write(uint32_t address, const uint8_t *data, uint32_t size);
int external_storage_read(uint32_t address, uint8_t *data, uint32_t size);

9.2 关键适配点

  1. Flash写入对齐要求

    • STM32U5:16字节(QUADWORD)
    • STM32F4:8字节(双字)
    • 其他MCU:查看数据手册
  2. 向量表偏移

    • 某些MCU需要设置VTOR寄存器
    • SCB->VTOR = app_addr;
  3. 缓存管理

    • 如果MCU没有缓存,可以忽略缓存相关操作
    • 如果有缓存,必须正确管理
  4. 中断向量表

    • 某些MCU的中断向量表位置固定
    • 某些MCU支持重定位向量表

9.3 测试清单

  • 正常升级流程测试
  • 数据包丢失处理测试
  • 数据包重复处理测试
  • 升级中断恢复测试
  • 应用验证失败回退测试
  • 两个应用都损坏的处理测试
  • 外部存储损坏的处理测试
  • 多次升级循环测试
  • 边界条件测试(最大固件大小、最小固件大小)

总结

本文档描述了一个通用的双应用区OTA升级系统,具有以下特点:

  1. 安全性:多重验证机制,确保数据完整性
  2. 可靠性:支持断点续传、重传、回退
  3. 可移植性:清晰的接口定义,易于移植到不同平台
  4. 防变砖:多重回退策略,确保系统可恢复

在实际应用中,需要根据具体的MCU平台和通信协议进行适配,但核心的设计思路和流程是通用的。


附录

A. 参考实现

  • STM32U5 Bootloader实现
  • W25Q256外部Flash驱动
  • Zigbee UART通信协议

B. 相关标准

  • ARM Cortex-M向量表规范
  • Flash存储器操作规范
  • CRC校验算法标准

C. 故障排查

问题:升级后应用无法启动

  • 检查向量表是否正确
  • 检查栈指针是否在RAM范围内
  • 检查Reset Handler地址是否正确
  • 检查Flash写入是否完整

问题:升级过程中数据丢失

  • 检查外部存储擦除是否完整
  • 检查跨扇区写入是否正确处理
  • 检查重传机制是否正常工作

问题:升级后系统变砖

  • 检查回退机制是否正常
  • 检查活动应用标志是否正确更新
  • 检查应用验证逻辑是否正确

文档版本:1.0
最后更新:2025年

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐