news 2026/4/30 2:50:46

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于 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 通过 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 的地址找中断服务函数,瞬间崩盘死机。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/30 2:47:25

贝塔智能挪车系统:构建汽车服务生态闭环的数字化解决方案

在技术领域&#xff0c;我们常常被那些闪耀的、可见的成果所吸引。今天&#xff0c;这个焦点无疑是大语言模型技术。它们的流畅对话、惊人的创造力&#xff0c;让我们得以一窥未来的轮廓。然而&#xff0c;作为在企业一线构建、部署和维护复杂系统的实践者&#xff0c;我们深知…

作者头像 李华
网站建设 2026/4/30 2:43:53

基于Tauri与React构建跨平台桌面工具箱:Clawset的设计与实现

1. 项目概述&#xff1a;一个面向Web开发者的桌面端工具箱最近在社区里看到不少朋友在讨论一个叫webdeb/clawset.app的项目&#xff0c;乍一看这个标题&#xff0c;可能有点摸不着头脑。webdeb像是一个开发者或组织的名字&#xff0c;clawset.app则是一个应用名&#xff0c;组合…

作者头像 李华
网站建设 2026/4/30 2:41:27

InferLLM:轻量级大模型推理引擎,打通端侧AI部署最后一公里

1. 项目概述&#xff1a;从推理框架到端侧AI的“最后一公里”最近在折腾端侧AI模型部署的朋友&#xff0c;估计都绕不开一个核心痛点&#xff1a;如何把一个动辄几GB、甚至几十GB的大模型&#xff0c;塞进我们手边那些算力有限、内存捉襟见肘的设备里&#xff0c;比如手机、嵌入…

作者头像 李华
网站建设 2026/4/30 2:40:28

AI Agent构建秘籍:Sub-Agents vs Agent Teams,打造更高效智能体系统!

本文深入探讨了AI Agent构建中的Sub-Agents和Agent Teams的区别与应用。Sub-Agents是主智能体的独立助手&#xff0c;可完成专项任务&#xff1b;而Agent Teams则是通过通信协作&#xff0c;共同解决复杂问题。文章强调应根据任务需求选择合适的协作方式&#xff0c;避免按角色…

作者头像 李华