OTA (Over-The-Air) 升级技术文档
本文档描述了一个通用的双应用区(Dual Bank)OTA升级系统设计,适用于嵌入式MCU平台。该系统通过维护两个独立的应用程序区域(APP1和APP2),实现安全的固件升级,确保在任何情况下至少有一个可用的应用程序。OTA_STATE_IDLE = 0, // 空闲状态:无OTA操作OTA_STATE_DOWNLOADING, // 下载中:正在接收固件数据OTA_STATE_READY, //
OTA (Over-The-Air) 升级技术文档
目录
概述
本文档描述了一个通用的双应用区(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 关键组件
- Bootloader:负责系统启动、OTA升级、应用验证和跳转
- 应用程序:用户业务逻辑,通过通信接口接收OTA命令
- 通信协议:定义OTA数据传输格式和命令集
- 存储管理:管理内部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 多重验证
- 数据包级验证:CRC校验、帧格式验证
- 固件级验证:向量表验证、地址范围检查
- 写入验证:写入后立即读取验证
6.2 回退策略
启动流程:
1. 读取活动应用标志
2. 验证活动应用
├─ 有效 → 跳转到活动应用 ✅
└─ 无效 → 尝试另一个应用
├─ 有效 → 更新活动应用标志 → 跳转 ✅
└─ 无效 → 尝试恢复默认固件
├─ 恢复成功 → 更新活动应用标志 → 跳转 ✅
└─ 恢复失败 → 进入死循环(需要外部干预)⚠️
6.3 升级失败处理
如果升级过程中失败:
-
固件验证失败:
- 设置OTA状态为FAILED
- 不更新活动应用标志
- 跳转到原活动应用
-
写入失败:
- 设置OTA状态为FAILED
- 尝试恢复默认固件(如果存在)
- 跳转到原活动应用
-
外部存储损坏:
- 无法读取OTA状态时,默认跳转到APP_1
- 如果APP_1无效,尝试APP_2
6.4 中断恢复
如果升级过程中系统断电或重启:
-
下载阶段中断:
- 状态为DOWNLOADING
- 下次启动时检测到未完成状态
- 清除旧状态,重新开始下载
-
准备阶段中断:
- 状态为READY
- 下次启动时检测到READY状态
- 可以选择继续升级或清除状态
-
升级阶段中断:
- 状态为UPGRADING
- 目标应用可能不完整
- 验证目标应用,如果无效则回退到原应用
异常情况处理
7.1 数据包丢失
现象:接收到未来包(包ID > 期望包ID)
处理:
- 记录丢失的包ID范围
- 发送重传请求
- 继续接收后续包(支持乱序接收)
- 等待重传包到达后写入
7.2 数据包重复
现象:接收到已处理的包(包ID < 期望包ID)
处理:
- 检查该包是否已写入
- 如果已写入,跳过写入,直接返回确认
- 如果未写入(之前丢失),执行写入
7.3 数据包损坏
现象:CRC校验失败或帧格式错误
处理:
- 丢弃该包
- 更新统计信息
- 等待发送端重传
7.4 外部存储写入失败
现象:写入外部Flash失败
处理:
- 检查是否已擦除扇区
- 如果未擦除,先擦除再写入
- 如果跨扇区,确保所有扇区都已擦除
- 重试写入(可设置重试次数)
7.5 内部Flash写入失败
现象:写入内部Flash失败
处理:
- 检查地址对齐(MCU特定要求)
- 检查Flash是否已解锁
- 检查写入大小是否符合要求(如QUADWORD对齐)
- 验证写入的数据
- 如果失败,设置OTA状态为FAILED,回退
7.6 应用验证失败
现象:升级后应用验证不通过
处理:
- 不更新活动应用标志
- 设置OTA状态为FAILED
- 尝试恢复默认固件
- 跳转到原活动应用
7.7 两个应用都损坏
现象:APP_1和APP_2都验证失败
处理:
- 尝试从外部存储恢复默认固件
- 如果恢复成功,验证并跳转
- 如果恢复失败,进入死循环
- 提供外部恢复接口(如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, §or_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 关键适配点
-
Flash写入对齐要求:
- STM32U5:16字节(QUADWORD)
- STM32F4:8字节(双字)
- 其他MCU:查看数据手册
-
向量表偏移:
- 某些MCU需要设置VTOR寄存器
SCB->VTOR = app_addr;
-
缓存管理:
- 如果MCU没有缓存,可以忽略缓存相关操作
- 如果有缓存,必须正确管理
-
中断向量表:
- 某些MCU的中断向量表位置固定
- 某些MCU支持重定位向量表
9.3 测试清单
- 正常升级流程测试
- 数据包丢失处理测试
- 数据包重复处理测试
- 升级中断恢复测试
- 应用验证失败回退测试
- 两个应用都损坏的处理测试
- 外部存储损坏的处理测试
- 多次升级循环测试
- 边界条件测试(最大固件大小、最小固件大小)
总结
本文档描述了一个通用的双应用区OTA升级系统,具有以下特点:
- 安全性:多重验证机制,确保数据完整性
- 可靠性:支持断点续传、重传、回退
- 可移植性:清晰的接口定义,易于移植到不同平台
- 防变砖:多重回退策略,确保系统可恢复
在实际应用中,需要根据具体的MCU平台和通信协议进行适配,但核心的设计思路和流程是通用的。
附录
A. 参考实现
- STM32U5 Bootloader实现
- W25Q256外部Flash驱动
- Zigbee UART通信协议
B. 相关标准
- ARM Cortex-M向量表规范
- Flash存储器操作规范
- CRC校验算法标准
C. 故障排查
问题:升级后应用无法启动
- 检查向量表是否正确
- 检查栈指针是否在RAM范围内
- 检查Reset Handler地址是否正确
- 检查Flash写入是否完整
问题:升级过程中数据丢失
- 检查外部存储擦除是否完整
- 检查跨扇区写入是否正确处理
- 检查重传机制是否正常工作
问题:升级后系统变砖
- 检查回退机制是否正常
- 检查活动应用标志是否正确更新
- 检查应用验证逻辑是否正确
文档版本:1.0
最后更新:2025年
更多推荐


所有评论(0)