基于 STM32 + ESP8266 + W25Q64 的双核 OTA 底层架构总结

目录

[第一战役:App 端固件下载与"三级缓存"防丢包机制 (App -> SPI Flash)](#第一战役:App 端固件下载与“三级缓存”防丢包机制 (App -> SPI Flash))

[1. 核心挑战:速度差与堵塞](#1. 核心挑战:速度差与堵塞)

[2. 解决方案 A:提前擦除(空间换时间)](#2. 解决方案 A:提前擦除(空间换时间))

[3. 解决方案 B:神级"三级缓存"架构](#3. 解决方案 B:神级“三级缓存”架构)

[4. 下载收尾动作](#4. 下载收尾动作)

[第二战役:Bootloader 固件搬运与内部 Flash 烧录 (SPI Flash -> Internal Flash)](#第二战役:Bootloader 固件搬运与内部 Flash 烧录 (SPI Flash -> Internal Flash))

[1. 验明正身(读标志位)](#1. 验明正身(读标志位))

[2. 内部 Flash 擦写机制(Bootloader 核心代码解读)](#2. 内部 Flash 擦写机制(Bootloader 核心代码解读))

第三战役:灵魂跃迁------现场清理与内核级跳转 (Bootloader -> App)

[1. 验证目标 APP 合法性](#1. 验证目标 APP 合法性)

[2. 扫除前朝余孽(环境隔离)](#2. 扫除前朝余孽(环境隔离))

[3. 指针飞跃(代码解读)](#3. 指针飞跃(代码解读))

[终极收尾:App 端的"防偷家"配置 (Keil/底层设置)](#终极收尾:App 端的“防偷家”配置 (Keil/底层设置))


整个 OTA 过程分为三大战役:App 端云端拉取Bootloader 端本地搬运内核级指针跳转

第一战役:App 端固件下载与"三级缓存"防丢包机制 (App -> SPI Flash)

1. 核心挑战:速度差与堵塞

  • Wi-Fi 端(快):ESP8266 通过 UART+DMA 疯狂往单片机吐数据,波特率极高,数据是持续不断的"流水"。

  • W25Q64 端(慢) :SPI Flash 有物理限制,写入只能按页(256字节)写,不能跨页;擦除只能按扇区(4096字节)擦 。擦除和写入时,芯片会处于 BUSY(忙碌)状态(大概需要几毫秒到几十毫秒)。

  • 矛盾点 :如果在接收数据时,同时去执行"擦除"或"等待 SPI 写入完成",CPU 就会被阻塞。而此时串口外设的接收不会停,导致 DMA 没法及时重启,直接引发严重丢包

2. 解决方案 A:提前擦除(空间换时间)

为了避免下载时现擦现写带来的阻塞,我们在发 AT+CIPSEND 请求数据之前,一次性把 W25Q64 的 OTA 存储区(0x1000 开始的 15 个扇区,约 60KB)全部擦除干净。下载时只管纯写,极大降低了延时。

3. 解决方案 B:神级"三级缓存"架构

为了彻底抹平串口接收和 SPI 写入的速度差,设计了极其精妙的三层 Buffer 机制:

  • 第一层(搬运工):UART_Rx_Buffer(DMA 专用)

    • 作用 :纯粹挂载在 HAL_UARTEx_ReceiveToIdle_DMA 上,用来无脑接收 ESP8266 吐出的网络包。

    • 机制:开启串口空闲中断(IDLE)。一包数据来到后,触发空闲中断。

  • 第二层(蓄水池):Process_Buffer(数据中转)

    • 作用 :在空闲中断触发的瞬间,光速使用 memcpy 把第一层的数据拷贝到这里

    • 关键动作 :拷贝完成后,立刻重启第一层 DMA。这就保证了在处理数据时,串口大门依然敞开,绝不漏掉接下来的任何一个字节。

  • 第三层(打包机):W25Q64_Buffer(256字节定长)

    • 作用:满足 W25Q64 "必须满一页写一次、不能跨页"的硬件物理限制。

    • 机制 :把第二层水池里的数据,一点点倒进这个 256 字节的量杯里。当第三层里的数据 < 256 时,只吃满剩余容量;一旦恰好凑满 256 字节,立刻触发 SPI 写入 W25Q64,然后清空量杯,继续接水,直到固件全部写完。

4. 下载收尾动作

  • 写入标志位 :当固件全部下载并写入 W25Q64 后,在 W25Q64 的绝对首地址(0x0000)写入标志位(如 0x55 0xAA)和紧随其后的目标固件真实大小(code_len)

  • 退出透传并重启 :向 ESP8266 发送 +++ 退出透传模式,彻底切断网络流。随后调用 NVIC_SystemReset() 触发单片机硬件级软复位,将控制权交给 Bootloader。


第二战役:Bootloader 固件搬运与内部 Flash 烧录 (SPI Flash -> Internal Flash)

系统复位后,永远最先执行编译在 0x08000000 的 Bootloader。

1. 验明正身(读标志位)

  • Bootloader 启动后,先读取 W25Q64 的 0x00 地址。

  • 逻辑 :如果是 0x55 0xAA,说明有新快递到了,进入 Upcode 模式;如果没有,说明是正常的开机,直接进入 jumpAPP 模式。

2. 内部 Flash 擦写机制(Bootloader 核心代码解读)

内部 Flash 的擦写同样有物理规则:必须先解锁、先擦除(按页擦,STM32F103通常一页1KB),再写入(必须按半字 16-bit 写入)。

复制代码
void bootloader_read_code(void) {
    HAL_FLASH_Unlock(); // 1. 解锁内部 Flash
    for (uint32_t i = 0; i < code_len; i += 16) {
        // 每次取 16 字节到 RAM 缓存中
        uint32_t current_len = (code_len - i) >= 16 ? 16 : (code_len - i);
        W25Q64_ReadData(Flash_Buff, CurrAddress_W25q64, current_len);

        // 2. 判断是否到了新的一页 (取余 1024 == 0)。如果是,触发内部 Flash 擦除!
        if ((CurrAddress_Flash % 1024) == 0) {
            FLASH_EraseInitTypeDef erase_init = {0};
            erase_init.TypeErase = FLASH_TYPEERASE_PAGES;
            erase_init.PageAddress = CurrAddress_Flash;
            erase_init.NbPages = 1;
            uint32_t page_error = 0;
            HAL_FLASHEx_Erase(&erase_init, &page_error); // 擦除当前 1KB 页
        }

        // 3. 按照半字 (16-bit) 强行拼接并写入内部 Flash
        for (uint8_t j = 0; j < current_len; j += 2) {
            uint16_t data16 = Flash_Buff[j] | (Flash_Buff[j + 1] << 8); // 组装成16位
            HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, CurrAddress_Flash + j, data16);
        }
        CurrAddress_Flash += current_len;
        CurrAddress_W25q64 += current_len;
    }
    HAL_FLASH_Lock(); // 4. 上锁保平安

    // 5. 【极其关键的一步】:擦除 W25Q64 第 0 扇区!
    // 把 0x55 0xAA 标志位毁尸灭迹,防止下一次开机又陷入升级死循环。
    W25Q64_EraseSector(0x00);
    b1 = jumpAPP; // 去往跳转函数
}

第三战役:灵魂跃迁------现场清理与内核级跳转 (Bootloader -> App)

这是最容易发生"跑飞、死机"的地方,必须做到滴水不漏。

1. 验证目标 APP 合法性

必须通过 MAP 文件规划好 App 的存放地址(如 0x08001C00)。 在跳转前,Bootloader 必须从 App 首地址取出两样最重要的东西:

  • 栈顶指针 (MSP) :存放在基地址的第 0~3 字节。指向 RAM (0x20000000 区间)。

  • 复位中断入口 (Reset Handler) :存放在基地址的第 4~7 字节。指向 Flash 代码区 (0x08000000 区间)。

2. 扫除前朝余孽(环境隔离)

Bootloader 在搬运数据时开启了 SPI、定时器等。如果不关掉就跳进 APP,APP 一旦开启全局中断,就会触发 Bootloader 残留的中断请求导致死机(进入 HardFault)。

  • 清理方法 :关闭 SysTick,调用 HAL_DeInit() 复位所有外设底层状态。

3. 指针飞跃(代码解读)

复制代码
void bootloader_Jump_To_App(void) {
    // 1. 获取新业主的身份信息
    uint32_t App_reset_hadler_address = *(volatile uint32_t *)(Flash_Address + 4);
    uint32_t App_stack_pointer = *(volatile uint32_t *)(Flash_Address);

    // 2. 安检:判断取出来的值是不是合法地址
    if ((App_reset_hadler_address & 0xffff0000) != 0x08000000) return;
    if ((App_stack_pointer & 0xffff0000) != 0x20000000) return;

    // 3. 彻底关停滴答定时器
    SysTick->VAL = 0; SysTick->CTRL = 0; SysTick->LOAD = 0;
    HAL_DeInit();

    // 4. 将单片机的主堆栈指针,切到 APP 的 RAM 空间
    __set_MSP(App_stack_pointer);

    // 5. 【防迷路】:告诉 CPU 中断向量表的新位置
    SCB->VTOR = Flash_Address;

    // 6. 函数指针强转,纵身一跃进入新世界!
    p Jump_to_app = (p)App_reset_hadler_address;
    Jump_to_app(); 
}

终极收尾:App 端的"防偷家"配置 (Keil/底层设置)

Bootloader 纵身一跃之后,App 必须要有接盘的能力,否则一样会死机。这里有两处铁律:

  1. 肉体映射(ROM 配置) : 在 Keil 的魔术棒 -> Target 选项中,IROM1 的 Start 地址必须绝对改为 0x8001C00

    • 原因 :这能让编译器在生成指令跳转时,把所有相对地址的计算基准都锚定在 0x8001C00,做到"身心合一"。
  2. 灵魂锚定(VTOR 防偷家)

    复制代码
    int main(void) {
        // 必须放在所有初始化(尤其是 HAL_Init)的最前面!
        SCB->VTOR = 0x08001C00; 
        HAL_Init(); 
        __enable_irq(); // 必须重新开启全局中断
        // ...
    }
    • 原因 :即使 Bootloader 临走前好心设置了 VTOR,App 底层自带的 SystemInit() 启动文件也会无脑把它重置回 0x08000000。如果不强行纠正,一旦触发 SysTick(比如 HAL_Delay),CPU 会跑去 Bootloader 的地址找中断服务函数,瞬间崩盘死机。
相关推荐
yongyoudayee3 小时前
CRM架构演进:从记录系统到执行引擎的技术解析
架构
xiangw@GZ3 小时前
WiFi 全世代(WiFi1~WiFi7)技术规范与核心参数总结
嵌入式硬件
振南的单片机世界3 小时前
CPU时钟:频率越高跑越快,但物理极限在“拖后腿”
stm32·单片机·嵌入式硬件
源码宝3 小时前
基于 SpringBoot + Vue 的医院随访系统:技术架构与功能实现
java·vue.js·spring boot·架构·源码·随访系统·随访管理
有马贵将4 小时前
【5】微前端知识点总结
前端·架构
普中科技4 小时前
【普中 51-Ai8051 开发攻略】-- 第 20 章 输入捕获实验
单片机·嵌入式硬件·输入捕获·pca·普中科技·ai8051u·aicube
d111111111d4 小时前
直流电机位置式 PID 控制 和 舵机的区别
笔记·stm32·单片机·嵌入式硬件·学习
ting94520005 小时前
深入解析 Social Fetch 机制:原理、架构、应用场景、实战落地与性能优化全攻略
人工智能·性能优化·架构
ZOOOOOOU5 小时前
云边端协同架构下,门禁权限引擎的离线决策与策略续存实现
大数据·人工智能·架构