当前位置: 首页 > news >正文

基于 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 的地址找中断服务函数,瞬间崩盘死机。

http://www.jsqmd.com/news/722618/

相关文章:

  • CentOS 7生产环境离线升级GCC全记录:从4.8.5到12.2.0的踩坑与避坑指南
  • 从运维视角看致远OA:如何快速自查并修复这三个高危文件上传漏洞(附修复脚本)
  • 3分钟掌握7-Zip:开源压缩工具实战指南与性能优化
  • 2026年小程序商城哪个平台最好?
  • 《中文AI圈炸了!860个智能体涌入「机乎」,人类竟被“请出”群聊?》
  • Synaptics SYN4382三模无线SoC技术解析与应用
  • 免费QQ空间备份神器:GetQzonehistory完整指南,永久保存青春记忆
  • Vue3移动端项目实战:用vue-virtual-scroller优雅集成Vant的PullRefresh和List组件
  • 拒绝“人海战术”:如何用 AI 翻译+自动化链路,重塑 LinkedIn 跨境开发流?
  • Qwen3模型网络故障诊断辅助:图解常见错误与解决方案
  • 2026乐山临江鳝丝店TOP5实测排行 附官方联系方式 - 优质品牌商家
  • 如何彻底解决Windows DLL缺失问题:VisualCppRedist AIO的技术实现与应用指南
  • BigCodeBench:真实世界代码生成模型的基准测试实战指南
  • 【数据分析】用于分析分数槽集中绕组永磁机的绕组布局附matlab代码
  • 别再纠结 GPT 和 Gemini 谁更强了,我把这俩塞进同一个入口后,效率直接翻倍
  • 构建高效测试反馈循环:从CI/CD到自动化测试的工程实践
  • “摄像头大王“养出一头仓储机器人巨兽:一年干出64亿
  • 脑矿奴隶起义:软件测试从业者的觉醒与革命
  • 从开源RocketMQ到金融级SOFAMQ:蚂蚁金服内部消息队列的选型与实战避坑指南
  • 题解:AtCoder AT_awc0005_a Reward of Multiples
  • C++实现简单计算器
  • 异或的密件 - Writeup by AI
  • 2026 AI存储行业迎来关键时刻:英伟达“补课”,华为存储“解题”
  • 畅百岁白酒哪家技术强
  • # Linux Shell 编程入门 Day02:条件测试、if 判断、循环与随机数
  • [Al+」数智升级,品牌种草营销新范式
  • 2026年3月评价高的钻攻机供应厂家推荐,多米钻攻一体机/圆管钻孔攻牙机/五轴钻床/数控钻攻一体机,钻攻机厂家哪家强 - 品牌推荐师
  • GKMLT通讯工具箱(WPF MVVM) - 02-Modbus RTU 与 TCP 报文格式、原理与CRC校验
  • 高并发场景下委托内存暴增?C# 13三大优化机制(静态委托缓存、目标弱引用、结构化闭包)全公开(仅限.NET 8.0+)
  • 大数据开发场景下,总结并翻译 Oracle 中常见的错误(补充其他错误码:适合初学者)