基于 STM32 + ESP8266 + W25Q64 的双核 OTA 底层架构总结
目录第一战役App 端固件下载与“三级缓存”防丢包机制 (App - SPI Flash)1. 核心挑战速度差与堵塞2. 解决方案 A提前擦除空间换时间3. 解决方案 B神级“三级缓存”架构4. 下载收尾动作第二战役Bootloader 固件搬运与内部 Flash 烧录 (SPI Flash - Internal Flash)1. 验明正身读标志位2. 内部 Flash 擦写机制Bootloader 核心代码解读第三战役灵魂跃迁——现场清理与内核级跳转 (Bootloader - App)1. 验证目标 APP 合法性2. 扫除前朝余孽环境隔离3. 指针飞跃代码解读终极收尾App 端的“防偷家”配置 (Keil/底层设置)整个 OTA 过程分为三大战役App 端云端拉取、Bootloader 端本地搬运、内核级指针跳转。第一战役App 端固件下载与“三级缓存”防丢包机制 (App - SPI Flash)1. 核心挑战速度差与堵塞Wi-Fi 端快ESP8266 通过 UARTDMA 疯狂往单片机吐数据波特率极高数据是持续不断的“流水”。W25Q64 端慢SPI Flash 有物理限制写入只能按页256字节写不能跨页擦除只能按扇区4096字节擦。擦除和写入时芯片会处于BUSY忙碌状态大概需要几毫秒到几十毫秒。矛盾点如果在接收数据时同时去执行“擦除”或“等待 SPI 写入完成”CPU 就会被阻塞。而此时串口外设的接收不会停导致 DMA 没法及时重启直接引发严重丢包。2. 解决方案 A提前擦除空间换时间为了避免下载时现擦现写带来的阻塞我们在发ATCIPSEND请求数据之前一次性把 W25Q64 的 OTA 存储区0x1000 开始的 15 个扇区约 60KB全部擦除干净。下载时只管纯写极大降低了延时。3. 解决方案 B神级“三级缓存”架构为了彻底抹平串口接收和 SPI 写入的速度差设计了极其精妙的三层 Buffer 机制第一层搬运工UART_Rx_BufferDMA 专用作用纯粹挂载在HAL_UARTEx_ReceiveToIdle_DMA上用来无脑接收 ESP8266 吐出的网络包。机制开启串口空闲中断IDLE。一包数据来到后触发空闲中断。第二层蓄水池Process_Buffer数据中转作用在空闲中断触发的瞬间光速使用memcpy把第一层的数据拷贝到这里。关键动作拷贝完成后立刻重启第一层 DMA。这就保证了在处理数据时串口大门依然敞开绝不漏掉接下来的任何一个字节。第三层打包机W25Q64_Buffer256字节定长作用满足 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、定时器等。如果不关掉就跳进 APPAPP 一旦开启全局中断就会触发 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 临走前好心设置了VTORApp 底层自带的SystemInit()启动文件也会无脑把它重置回0x08000000。如果不强行纠正一旦触发 SysTick比如HAL_DelayCPU 会跑去 Bootloader 的地址找中断服务函数瞬间崩盘死机。