STM32的OTA(Over-The-Air)升级是指通过通信接口(如UART、以太网、Wi-Fi等)远程更新固件的技术。与ESP32等集成无线功能的芯片不同,STM32本身无内置Wi-Fi/以太网(部分高端型号除外),且无官方统一的OTA框架,需手动设计Bootloader(引导程序)、Flash分区、通信协议和固件验证等核心模块。以下是STM32 OTA升级的完整实现方案和关键技术细节。
一、STM32 OTA升级的核心原理
STM32的程序运行依赖Flash中的代码,OTA升级的本质是:
通过通信接口(如UART、Wi-Fi模块)接收新固件(.bin文件);
将新固件写入Flash的指定区域(与当前运行的固件分区隔离);
重启后,由Bootloader验证新固件合法性,并引导其运行。
核心组件包括:
Bootloader:负责固件校验、Flash擦写、启动引导(最先运行的程序);
App分区:存放当前运行的应用程序和待升级的新固件(需至少2个App分区);
参数区:存放OTA标志(是否需要升级)、固件版本号、校验值等信息;
通信模块:用于接收新固件(如外接ESP8266 Wi-Fi模块、LAN8720以太网模块)。
二、硬件与Flash分区规划
1. 硬件准备
STM32芯片(需满足Flash容量≥2×App大小,推荐F4/F7/H7系列,Flash容量≥512KB);
通信模块(如:ESP8266(Wi-Fi)、CH340(UART)、LAN8720(以太网));
外部Flash(可选,用于临时存储大固件,如W25Q系列SPI Flash)。
2. Flash分区设计(关键)
STM32的Flash需划分为4个核心区域(以512KB Flash为例),地址需按芯片手册的扇区大小对齐:
注意:
分区大小需根据实际固件调整(如App区需≥固件大小+10%冗余);
地址需与STM32 Flash扇区边界对齐(如F4系列扇区0-3为16KB,扇区4为64KB,需避免跨扇区分配);
参数区需选择擦写次数少的区域(或用EEPROM模拟,如STM32的Data Flash)。
三、Bootloader设计(核心模块)
Bootloader是OTA的“大脑”,在芯片复位后最先运行,负责判断是否需要升级、校验新固件、引导程序启动。其流程如下:
1. Bootloader的核心功能
初始化硬件:配置时钟(保证通信和Flash操作正常)、初始化通信接口(如UART、SPI)、配置GPIO;
检查OTA标志:读取参数区的OTA标志(如0xAA55),判断是否需要执行升级;
固件校验:若需要升级,验证App2区新固件的完整性(如CRC32、MD5)和合法性(如版本号是否高于当前);
固件搬运:若校验通过,将App2区的新固件复制到App1区(或直接设置启动地址为App2区);
引导启动:清除OTA标志,跳转到App区(App1或App2)运行应用程序。
2. Bootloader关键代码实现
以下是基于STM32F4的Bootloader核心代码(使用HAL库):
#include "stm32f4xx_hal.h"
#include "string.h"
// 分区地址定义(根据实际芯片调整)
#define BOOT_ADDR 0x08000000
#define APP1_ADDR 0x08008000
#define APP2_ADDR 0x08028000
#define PARAM_ADDR 0x08048000
#define APP_SIZE 0x20000 // 128KB(App1和App2大小)
// 参数区结构体(存放OTA状态)
typedef struct {
uint16_t ota_flag; // OTA标志:0xAA55表示需要升级
uint32_t app1_crc; // App1区固件CRC
uint32_t app2_crc; // App2区固件CRC
uint8_t app1_version[8];// App1版本号
uint8_t app2_version[8];// App2版本号
} ParamTypeDef;
ParamTypeDef param;
// 读取参数区数据
void param_read(ParamTypeDef *p) {
memcpy(p, (void*)PARAM_ADDR, sizeof(ParamTypeDef));
}
// 计算CRC32(用于固件校验)
uint32_t crc32_calc(uint32_t start_addr, uint32_t size) {
uint32_t crc = 0xFFFFFFFF;
uint8_t *data = (uint8_t*)start_addr;
for (uint32_t i = 0; i < size; i++) {
crc ^= data;
for (int j = 0; j < 8; j++) {
crc = (crc >> 1) ^ ((crc & 1) ? 0xEDB88320 : 0);
}
}
return ~crc;
}
// 擦除Flash扇区(需根据芯片扇区表调整)
void flash_erase(uint32_t addr, uint32_t size) {
HAL_FLASH_Unlock();
FLASH_EraseInitTypeDef erase;
erase.TypeErase = FLASH_TYPEERASE_SECTORS;
erase.Sector = get_sector(addr); // 自定义函数:根据地址获取扇区号
erase.NbSectors = get_sector_count(addr, size); // 计算需要擦除的扇区数
erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
uint32_t page_error;
HAL_FLASHEx_Erase(&erase, &page_error);
HAL_FLASH_Lock();
}
// 写入数据到Flash
void flash_write(uint32_t addr, uint8_t *data, uint32_t len) {
HAL_FLASH_Unlock();
for (uint32_t i = 0; i < len; i += 4) {
uint32_t word = *(uint32_t*)(data + i);
HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr + i, word);
}
HAL_FLASH_Lock();
}
// 跳转到App程序
void jump_to_app(uint32_t app_addr) {
// 检查App地址是否合法(栈顶地址需在RAM范围内)
uint32_t stack_top = *(volatile uint32_t*)app_addr;
if (stack_top < 0x20000000 || stack_top > 0x20000000 + RAM_SIZE) {
return;
}
// 关闭中断,避免干扰App
__disable_irq();
// 跳转到App的复位向量(app_addr + 4是复位函数地址)
void (*app_reset_handler)(void) = (void*)*(volatile uint32_t*)(app_addr + 4);
// 设置主栈指针(MSP)为App的栈顶
__set_MSP(stack_top);
// 跳转执行
app_reset_handler();
}
int main(void) {
HAL_Init();
SystemClock_Config(); // 配置系统时钟(如168MHz)
uart_init(115200); // 初始化UART(用于调试或接收固件)
// 1. 读取参数区
param_read(¤tparam);
// 2. 检查是否需要升级(ota_flag == 0xAA55)
if (param.ota_flag == 0xAA55) {
// 3. 校验App2区新固件的CRC
uint32_t calc_crc = crc32_calc(APP2_ADDR, APP_SIZE);
if (calc_crc == param.app2_crc) {
// 4. 擦除App1区,将App2区的新固件复制到App1区
flash_erase(APP1_ADDR, APP_SIZE);
flash_write(APP1_ADDR, (uint8_t*)APP2_ADDR, APP_SIZE);
// 5. 更新参数区(清除OTA标志,更新App1版本和CRC)
param.ota_flag = 0x0000;
memcpy(param.app1_version, param.app2_version, 8);
param.app1_crc = param.app2_crc;
flash_erase(PARAM_ADDR, sizeof(ParamTypeDef));
flash_write(PARAM_ADDR, (uint8_t*)¤tparam, sizeof(ParamTypeDef));
} else {
// 校验失败,保留旧固件
param.ota_flag = 0x0000;
flash_write(PARAM_ADDR, (uint8_t*)¤tparam, sizeof(ParamTypeDef));
}
}
// 6. 跳转到App1区运行
jump_to_app(APP1_ADDR);
// 若跳转失败,进入死循环
while (1) {}
}
四、应用程序(App)设计
应用程序需实现两个核心功能:
正常业务逻辑(如传感器采集、控制输出);
触发OTA升级(如接收远程指令),并与Bootloader配合完成固件接收和标志设置。
1. App触发OTA的流程
接收升级指令:通过通信接口(如UART接收Wi-Fi模块转发的指令);
接收新固件:将新固件写入App2区(需按Flash扇区擦写后写入);
更新参数区:计算新固件CRC,设置OTA标志(0xAA55),记录版本号;
重启系统:触发复位,让Bootloader执行升级。
2. App关键代码示例(接收固件并设置标志)
// App中接收新固件并写入App2区
void ota_receive_firmware() {
uint8_t firmware_buffer[1024]; // 接收缓冲区
uint32_t firmware_size = 0; // 已接收固件大小
uint32_t app2_addr = APP2_ADDR;
// 1. 擦除App2区
flash_erase(APP2_ADDR, APP_SIZE);
// 2. 循环接收固件数据(通过UART/ETH等)
while (firmware_size < APP_SIZE) {
uint16_t recv_len = uart_receive(firmware_buffer, 1024); // 实际项目需实现超时处理
if (recv_len == 0) break;
// 写入Flash
flash_write(app2_addr, firmware_buffer, recv_len);
app2_addr += recv_len;
firmware_size += recv_len;
}
// 3. 计算新固件CRC并更新参数区
ParamTypeDef param;
param_read(¤tparam);
param.app2_crc = crc32_calc(APP2_ADDR, firmware_size);
memcpy(param.app2_version, "V2.0.0", 8); // 新固件版本号
param.ota_flag = 0xAA55; // 设置OTA标志
flash_erase(PARAM_ADDR, sizeof(ParamTypeDef));
flash_write(PARAM_ADDR, (uint8_t*)¤tparam, sizeof(ParamTypeDef));
// 4. 重启系统,触发Bootloader升级
NVIC_SystemReset();
}
五、通信协议设计(固件传输可靠性)
STM32需通过外部模块接收固件,需设计简单可靠的通信协议(以“UART+ESP8266”为例),确保数据传输不丢包、不错位。
协议帧格式示例
| 帧头(2B) | 命令(1B) | 长度(2B) | 数据(nB) | CRC(2B) | 帧尾(2B) |
|----------|----------|----------|----------|---------|----------|
| 0xAA55 | 0x01(升级) | 数据长度 | 固件分片 | 数据CRC | 0x55AA |
帧头/帧尾:用于帧同步(避免数据混淆);
命令:区分“开始升级”“传输数据”“结束传输”等操作;
分片传输:将大固件分为1KB/片(匹配Flash擦写粒度),每片校验后再确认;
重传机制:接收方校验失败时,发送“重传请求”,确保数据完整。
六、关键问题与解决方案
1. 固件校验失败(升级后无法启动)
原因:传输过程中数据丢包、Flash写入错误、固件被篡改;
解决:
用CRC32或MD5校验整个固件(Bootloader阶段验证);
校验固件的复位向量(确保是合法的STM32程序)。
2. 升级中途断电(固件损坏)
原因:断电导致App2区固件不完整,Bootloader可能误升级;
解决:
在参数区记录“升级状态”(如0x01表示升级中,0x02表示升级完成);
Bootloader仅在“升级完成”状态下才执行固件搬运,否则保留旧固件。
3. Flash擦写次数限制(影响寿命)
原因:STM32 Flash擦写次数约1万次,频繁升级会缩短寿命;
解决:
仅在新固件版本高于当前时才触发升级;
用外部SPI Flash(如W25Q,擦写次数10万次)临时存储新固件,减少内部Flash擦写。
4. 跳转App失败(卡在Bootloader)
原因:App地址错误、栈顶指针不合法、中断未关闭;
解决:
跳转前检查App首地址的栈顶值(必须在RAM范围内);
跳转前关闭所有中断(__disable_irq()),避免干扰App初始化。
总结
STM32的OTA升级需手动设计Bootloader、分区表、通信协议三大核心模块,灵活性高但实现复杂度高于ESP32。关键是:
合理规划Flash分区,隔离Bootloader、App和参数区;
Bootloader需实现固件校验、安全跳转和异常处理;
设计可靠的通信协议,确保固件传输完整;
处理断电、校验失败等异常场景,避免设备“变砖”。
适用于工业设备、物联网终端等需要远程维护的场景,可结合Wi-Fi/以太网模块实现无线OTA。
————————————————
版权声明:本文为CSDN博主「Shylock_Mister」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Shylock_Mister/article/details/153527538
|
|