基于 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 的地址找中断服务函数,瞬间崩盘死机。
相关推荐
Decay__4 小时前
05_MySQL 常用 SQL 大全 —— 诊断、管理、优化辅助 SQL 与三大系统库速查
运维·数据库·sql·mysql·架构
代码山河6 小时前
Python就业方向全解析:后端、爬虫、数据分析、AI你选哪个?
python·学习·架构·教程·面向对象·项目
吠品7 小时前
智谱开源 ZCode 之后,真正的考试才开始
单片机·嵌入式硬件·1024程序员节
微订校园外卖平台8 小时前
校园外卖平台选模板还是定制:架构、数据流与验收清单
大数据·架构·外卖系统·本地生活·微订
Chery11408 小时前
高速ADC与精密DAC怎么选?DP9230D与DP8532D应用解析
嵌入式硬件·物联网·射频工程
ZhengEnCi9 小时前
用 AI 写代码为什么拼到最后还是拼人:算力瓶颈与领域专家提示词的本质差异
算法·架构
Decay__10 小时前
04_MySQL DBA 日常运维手册 —— 会话管理、锁与阻塞、事务监控、慢查询与日志巡检
运维·数据库·sql·mysql·架构
小李不想当小白11 小时前
SPI通信外设与W25Q64 (硬件SPI)(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
LCG米11 小时前
STM32驱动W5500以太网模块实测:TCP吞吐瓶颈定位、TX指针回绕陷阱与断线重连保活落地
stm32·单片机·嵌入式硬件·tcp/ip
华普微HOPERF13 小时前
Matter协议架构详解:数据模型与交互模型深度解析
microsoft·架构·交互