61.嵌入式OTA远程升级:原理、流程与实战代码
一、OTA的核心本质:不是芯片自带功能,是完整方案
很多人误以为OTA是某款MCU的内置功能,实际上它是一套从云端到设备端的完整升级流程,类似手机的系统更新:
- 云端:存储新固件、版本管理、下发升级指令;
- 设备端:接收固件、校验完整性、切换运行程序;
- 核心依赖:Bootloader引导程序、Flash分区管理、异常回滚机制。
下图清晰地展示了OTA远程升级的四步执行流程:
flowchart TD
A[云端下发新固件] --> B[设备无线接收]
B --> C[存储到Flash备用分区]
C --> D{固件完整性校验}
D -- 校验通过 --> E[写入升级标志]
E --> F[设备重启]
F --> G[Bootloader检测标志]
G --> H[切换运行分区]
H --> I[启动新固件]
D -- 校验失败 --> J[丢弃固件,重新下载]
J --> B
图:OTA四步执行流程图,展示了从云端下发到设备重启切换的完整过程
关于图中关键步骤的详细说明:
1. 固件完整性校验:这是确保OTA升级可靠性的关键环节。图中"固件完整性校验"节点对应实际实现中的CRC、MD5或SHA等校验算法。具体实现时需要注意:
- 校验算法选择:CRC32适合资源受限的嵌入式设备,计算速度快;MD5/SHA-1提供更强的防篡改能力,但计算开销较大。
- 校验值存储:云端应在固件打包时计算校验值并随固件一起下发,设备端接收完成后重新计算比对。
- 分块校验:对于大容量固件,可采用分块校验机制,每接收一定数据量就进行一次校验,避免传输中途出错导致全部重传。
- 错误处理:校验失败时应丢弃已接收的固件数据,并触发重传机制,如图中"校验失败→丢弃固件,重新下载"分支所示。
2. Bootloader检测标志:图中"Bootloader检测标志"节点是OTA升级的决策点,具体实现逻辑包括:
- 标志位设计:通常在Flash固定地址设置特定值(如0x12345678)作为升级标志,Bootloader启动时首先读取该标志。
- 多重验证:检测到升级标志后,Bootloader不应立即切换,而应再次校验新固件的完整性和有效性,防止标志被误写或固件损坏。
- 安全考量:标志位应存储在独立的Flash扇区,避免与程序存储区冲突;同时考虑写入保护机制,防止意外修改。
- 状态清理:升级成功后必须清除升级标志,否则设备每次重启都会尝试升级,影响正常启动速度。
- 异常处理:如果新固件校验失败,Bootloader应清除标志并跳转到旧固件,确保设备可用性。
这两个步骤共同构成了OTA升级的"安全门",确保只有完整、正确的固件才会被实际应用,从机制上避免了因传输错误或存储异常导致的设备变砖风险。
二、OTA的完整执行流程(四步走)
第一步:无线接收新固件
设备通过WiFi、蓝牙或4G连接云端,下载新固件到本地。此时设备仍在运行旧程序,不会影响正常业务。
第二步:存储到Flash备用分区
将新固件写入Flash的备用分区,旧固件保持在原分区不变,确保升级过程中设备仍能正常工作。
第三步:固件完整性校验
接收完成后,通过CRC、MD5等算法校验固件是否传输错误,校验通过后写入升级标志,通知Bootloader准备切换。
第四步:重启切换固件
设备重启进入Bootloader,查验升级标志无误后,将新固件映射到运行分区,完成底层切换。
三、异常处理:新固件“翻车”怎么办?
如果新固件存在Bug导致系统崩溃,设备会触发看门狗重启,Bootloader检测到启动异常后,自动执行反向映射,切回旧固件,确保设备不会变砖。
四、OTA的实战价值
- 节省人力成本:量产设备遍布各地时,无需上门拆机,远程一键即可批量升级。
- 支持产品快速迭代:通过OTA快速修复Bug、新增功能,提升用户体验。
五、STM32 OTA实战代码示例
1. Flash分区定义
/**
* STM32F407 Flash分区定义
* 说明:Flash地址空间需要根据具体芯片型号和链接脚本进行调整
* 典型分区方案:Bootloader + 双固件分区 + 标志存储区
*/
#define FLASH_BOOTLOADER_ADDR 0x08000000 // Bootloader起始地址(通常占用前64KB)
#define FLASH_OLD_FW_ADDR 0x08010000 // 旧固件起始地址(运行分区)
#define FLASH_NEW_FW_ADDR 0x08020000 // 新固件起始地址(备用分区)
#define FLASH_SIGN_ADDR 0x08030000 // 升级标志存储地址(独立扇区)
// 固件大小定义(根据实际固件大小调整)
#define FW_SIZE 0x00010000 // 每个固件分区大小:64KB
// CRC校验值存储地址(可选:存储在标志区之后)
#define FLASH_CRC_ADDR (FLASH_SIGN_ADDR + 4)
2. 固件接收与存储
#include "stm32f4xx_hal.h"
/**
接收并存储新固件到Flash备用分区
@param data 接收到的固件数据指针
@param len 数据长度(字节)
@param offset 写入Flash的偏移地址(相对于FLASH_NEW_FW_ADDR)
@return HAL_OK表示成功,其他值表示失败
注意事项:
Flash写入前必须先擦除对应扇区
建议分块接收和存储,避免一次性接收大文件导致内存不足
实际项目中应加入超时和重传机制
*/
HAL_StatusTypeDef OTA_Receive_Firmware(uint8_t *data, uint32_t len, uint32_t offset)
{
HAL_StatusTypeDef status;
uint32_t flash_addr = FLASH_NEW_FW_ADDR + offset; // 计算实际Flash地址
// 解锁Flash(必须先解锁才能进行编程操作)
HAL_FLASH_Unlock();
// 写入数据到Flash
for (uint32_t i = 0; i < len; i++) {
// 按字节编程(对于STM32F4,也可以使用字/半字编程提高效率)
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, flash_addr + i, data[i]);
if (status != HAL_OK) {
HAL_FLASH_Lock(); // 出错时锁定Flash
return status; // 返回错误状态
}
}
// 锁定Flash(编程完成后必须锁定以保护数据)
HAL_FLASH_Lock();
return HAL_OK; // 返回成功状态
}
3. CRC校验
方案一:使用STM32硬件CRC外设(推荐,效率高)
#include "stm32f4xx_hal.h"
#include "stm32f4xx_hal_crc.h"
CRC_HandleTypeDef hcrc; // CRC外设句柄
/**
使用STM32硬件CRC外设计算固件CRC32校验值
@param addr 固件起始地址
@param len 固件长度(字节数)
@return 计算得到的CRC32校验值
优点:
硬件加速,计算速度快,适合大容量固件
不占用CPU资源,计算过程中CPU可执行其他任务
标准CRC32多项式,与云端计算保持一致
注意事项:
需要先初始化CRC外设(在main函数中调用HAL_CRC_Init)
硬件CRC通常按32位字计算,需要处理非4字节对齐的情况
*/
uint32_t OTA_Hardware_CRC_Check(uint32_t addr, uint32_t len)
{
uint32_t crc_result = 0;
uint32_t data_ptr = (uint32_t)addr;
uint32_t word_count = len / 4; // 32位字数量
uint32_t byte_remain = len % 4; // 剩余字节数
// 计算完整32位字的CRC
if (word_count > 0) {
crc_result = HAL_CRC_Calculate(&hcrc, data_ptr, word_count);
}
// 处理剩余字节(如果不是4的倍数)
if (byte_remain > 0) {
uint32_t temp = 0;
uint8_t byte_ptr = (uint8_t)(addr + word_count * 4);
// 将剩余字节复制到临时变量
for (uint32_t i = 0; i < byte_remain; i++) {
*((uint8_t*)&temp + i) = byte_ptr[i];
}
// 计算剩余数据的CRC
crc_result = HAL_CRC_Accumulate(&hcrc, &temp, 1);
}
return crc_result;
}
/**
硬件CRC校验函数
@param addr 固件起始地址
@param len 固件长度
@param expected_crc 云端下发的预期CRC值
@return 1表示校验通过,0表示校验失败
*/
uint8_t OTA_CRC_Check_Hardware(uint32_t addr, uint32_t len, uint32_t expected_crc)
{
uint32_t calculated_crc = OTA_Hardware_CRC_Check(addr, len);
return (calculated_crc == expected_crc) ? 1 : 0;
}
方案二:软件CRC32实现(备选方案,兼容性好)
/**
* 软件CRC32计算(查表法,效率较高)
* @param addr 固件起始地址
* @param len 固件长度(字节数)
* @return 计算得到的CRC32校验值
*
* 优点:
* 1. 不依赖硬件CRC外设,兼容所有STM32型号
* 2. 代码可移植性好
*
* 缺点:
* 1. 计算速度较慢,特别是大容量固件
* 2. 占用CPU资源,计算期间无法执行其他任务
*
* CRC32多项式:0x04C11DB7(标准IEEE 802.3)
*/
uint32_t OTA_Software_CRC32(uint32_t addr, uint32_t len)
{
static const uint32_t crc32_table[256] = {
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F,
0xE963A535, 0x9E6495A3, 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
// ... 完整的CRC32表(实际代码中需要256个表项)
};
uint32_t crc = 0xFFFFFFFF; // CRC初始值
uint8_t *data = (uint8_t*)addr;
for (uint32_t i = 0; i < len; i++) {
uint8_t index = (crc ^ data[i]) & 0xFF;
crc = (crc >> 8) ^ crc32_table[index];
}
return crc ^ 0xFFFFFFFF; // 最终异或值
}
/**
软件CRC校验函数
@param addr 固件起始地址
@param len 固件长度
@param expected_crc 云端下发的预期CRC值
@return 1表示校验通过,0表示校验失败
/
uint8_t OTA_CRC_Check_Software(uint32_t addr, uint32_t len, uint32_t expected_crc)
{
uint32_t calculated_crc = OTA_Software_CRC32(addr, len);
return (calculated_crc == expected_crc) ? 1 : 0;
}
/*
统一的CRC校验接口(自动选择硬件或软件方案)
@param addr 固件起始地址
@param len 固件长度
@param expected_crc 预期CRC值
@param use_hardware 是否使用硬件CRC(1:使用硬件, 0:使用软件)
@return 1表示校验通过,0表示校验失败
*/
uint8_t OTA_CRC_Check(uint32_t addr, uint32_t len, uint32_t expected_crc, uint8_t use_hardware)
{
#ifdef HAL_CRC_MODULE_ENABLED
if (use_hardware) {
return OTA_CRC_Check_Hardware(addr, len, expected_crc);
}
#endif
// 默认使用软件CRC
return OTA_CRC_Check_Software(addr, len, expected_crc);
}
硬件CRC vs 软件CRC对比:
- 硬件CRC:速度快、不占CPU、功耗低,但需要芯片支持CRC外设
- 软件CRC:兼容性好、不依赖硬件,但速度慢、占用CPU资源
- 建议:STM32F4/F7/H7等系列推荐使用硬件CRC;Cortex-M0/M3等无CRC外设的芯片使用软件CRC
4. 升级标志设置
#include "stm32f4xx_hal.h"
/**
设置OTA升级标志
@return HAL_OK表示成功,其他值表示失败
功能说明:
在Flash固定地址写入特定值作为升级标志
Bootloader启动时会检查这个标志决定是否执行升级
升级完成后必须清除标志,否则每次重启都会尝试升级
注意事项:
标志地址应选择独立的Flash扇区,避免与程序区冲突
写入前需要确保该扇区已被擦除
实际项目中可考虑使用更复杂的标志结构(如包含版本号、时间戳等)
*/
HAL_StatusTypeDef OTA_Set_Sign(void)
{
HAL_StatusTypeDef status;
// 解锁Flash(必须步骤)
HAL_FLASH_Unlock();
// 在指定地址写入升级标志(0x12345678为示例值)
// FLASH_TYPEPROGRAM_WORD表示按32位字编程
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SIGN_ADDR, 0x12345678);
// 锁定Flash(保护数据)
HAL_FLASH_Lock();
return status; // 返回编程结果
}
/**
清除升级标志(升级完成后调用)
@return HAL_OK表示成功
*/
HAL_StatusTypeDef OTA_Clear_Sign(void)
{
HAL_StatusTypeDef status;
HAL_FLASH_Unlock();
// 将标志地址清零
status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SIGN_ADDR, 0x00000000);
HAL_FLASH_Lock();
return status;
}
/**
检查升级标志是否存在
@return 1表示有升级标志,0表示无升级标志
*/
uint8_t OTA_Check_Sign(void)
{
uint32_t sign_value = (__IO uint32_t)FLASH_SIGN_ADDR;
return (sign_value == 0x12345678) ? 1 : 0;
}
5. Bootloader固件切换
#include "stm32f4xx_hal.h"
/**
Bootloader中检测升级标志并执行固件切换
说明:此函数在Bootloader的main函数中调用,设备重启后首先执行
执行流程:
检查升级标志是否存在
如果存在,校验新固件的完整性和有效性
校验通过后,擦除旧固件分区
将新固件复制到旧固件分区
清除升级标志
跳转到新固件执行
*/
void Bootloader_Check_Upgrade(void)
{
// 读取升级标志
uint32_t sign = (__IO uint32_t)FLASH_SIGN_ADDR;
// 检查是否有升级请求(标志值为0x12345678)
if (sign == 0x12345678) {
// 第一步:校验新固件的完整性和有效性
// 使用硬件CRC校验(如果支持),否则使用软件CRC
if (OTA_CRC_Check(FLASH_NEW_FW_ADDR, FW_SIZE, EXPECTED_CRC, 1)) {
// 第二步:擦除旧固件分区(为新固件腾出空间)
FLASH_EraseInitTypeDef eraseInit;
uint32_t sectorError;
// 配置擦除参数
eraseInit.TypeErase = FLASH_TYPEERASE_SECTORS; // 扇区擦除模式
eraseInit.Sector = FLASH_SECTOR_4; // 擦除的起始扇区(根据实际分区调整)
eraseInit.NbSectors = 1; // 擦除的扇区数量
eraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3; // 电压范围
HAL_FLASH_Unlock(); // 解锁Flash以进行擦除操作
HAL_FLASHEx_Erase(&amp;eraseInit, &amp;sectorError); // 执行擦除
// 第三步:将新固件复制到旧固件分区(原地升级)
for (uint32_t i = 0; i &lt; FW_SIZE; i++) {
// 逐字节复制(实际项目中可按字/半字复制提高效率)
uint8_t data = *(__IO uint8_t*)(FLASH_NEW_FW_ADDR + i);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, FLASH_OLD_FW_ADDR + i, data);
}
// 第四步:清除升级标志(防止重复升级)
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SIGN_ADDR, 0x00000000);
// 锁定Flash,保护已写入的数据
HAL_FLASH_Lock();
// 可选:写入升级成功日志或更新版本信息
// ...
} else {
// CRC校验失败:清除错误标志,不执行升级
HAL_FLASH_Unlock();
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, FLASH_SIGN_ADDR, 0x00000000);
HAL_FLASH_Lock();
// 可选:记录校验失败日志
// ...
}
}
// 第五步:跳转到主程序(无论是否升级都执行)
// 获取主程序的栈顶指针(向量表第一个字)
uint32_t main_stack_pointer = (__IO uint32_t)FLASH_OLD_FW_ADDR;
// 获取主程序的复位向量地址(向量表第二个字)
void (jump_to_app)(void) = (void ()(void))((__IO uint32_t)(FLASH_OLD_FW_ADDR + 4));
// 设置主栈指针(MSP)
__set_MSP(main_stack_pointer);
// 跳转到主程序(应用程序)
jump_to_app();
// 注意:跳转后不会返回,Bootloader任务结束
}
/**
安全跳转函数(带基本检查)
@param address 应用程序起始地址
@return 无
*/
void Bootloader_Jump_To_App(uint32_t address)
{
// 检查地址是否有效(在Flash范围内)
if ((address < 0x08000000) || (address >= 0x08100000)) {
// 地址无效,执行错误处理(如进入死循环或重启)
while(1);
}
// 检查栈指针是否有效(通常应为RAM地址)
uint32_t stack_pointer = (__IO uint32_t)address;
if ((stack_pointer < 0x20000000) || (stack_pointer >= 0x20020000)) {
// 栈指针无效
while(1);
}
// 设置栈指针和跳转
__set_MSP(stack_pointer);
void (app_entry)(void) = (void ()(void))((__IO uint32_t)(address + 4));
app_entry();
}
六、总结
OTA技术让嵌入式产品的维护和迭代变得更加高效,核心在于合理的Flash分区、可靠的固件校验和完善的异常回滚机制。在实际项目中,还需要结合云端管理平台,实现版本控制、灰度发布等高级功能,进一步提升产品的竞争力。
更多推荐


所有评论(0)