[应用相关] STM32 OTA升级全攻略:从原理到代码实现

[复制链接]
89|0
小海师 发表于 2025-11-6 08:36 | 显示全部楼层 |阅读模式
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为例),地址需按芯片手册的扇区大小对齐:

31085690bed80c98c2.png

注意:

分区大小需根据实际固件调整(如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

您需要登录后才可以回帖 登录 | 注册

本版积分规则

97

主题

300

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部