目录
[第一战役: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 必须要有接盘的能力,否则一样会死机。这里有两处铁律:
-
肉体映射(ROM 配置) : 在 Keil 的魔术棒 -> Target 选项中,IROM1 的 Start 地址必须绝对改为
0x8001C00。- 原因 :这能让编译器在生成指令跳转时,把所有相对地址的计算基准都锚定在
0x8001C00,做到"身心合一"。
- 原因 :这能让编译器在生成指令跳转时,把所有相对地址的计算基准都锚定在
-
灵魂锚定(VTOR 防偷家):
int main(void) { // 必须放在所有初始化(尤其是 HAL_Init)的最前面! SCB->VTOR = 0x08001C00; HAL_Init(); __enable_irq(); // 必须重新开启全局中断 // ... }- 原因 :即使 Bootloader 临走前好心设置了
VTOR,App 底层自带的SystemInit()启动文件也会无脑把它重置回0x08000000。如果不强行纠正,一旦触发 SysTick(比如HAL_Delay),CPU 会跑去 Bootloader 的地址找中断服务函数,瞬间崩盘死机。
- 原因 :即使 Bootloader 临走前好心设置了