GD32F407 基于 ESP8266 的 OTA 无线升级实战(Bootloader + App + PC服务器全流程)
2026/8/30 16:13:27 网站建设 项目流程

本文基于立创梁山派天空星(GD32F407VET6,8MHz 晶振)开发板,用 ESP8266 实现了一套完整的 OTA 无线固件升级。全程从零搭建,踩了大量坑(串口编号、跳转汇编、ACK 错位……),方法、协议、代码都给你讲透。


一、为什么做 OTA

单片机固件通常用下载器(ST-Link/J-Link/DAP)离线烧录。产品一旦装机,要升级就得拆壳连线,非常麻烦。OTA(Over-The-Air)就是让设备通过 WiFi/网络自己下载新固件、自己写进 Flash、自己重启——远程升级、无人值守

本文方案:GD32F407 + ESP8266(AT 固件),PC 端用 Python 当固件服务器,板子通过 WiFi 拉取新固件在线升级。
文章底部附上了Git 仓库链接,有需要的朋友可以自行下载。


二、整体架构

+--------------------+ USART1 +-----------+ WiFi/TCP +------------------+ | GD32F407 Bootloader| <------------------> | ESP8266 | <----------> | PC (ota_server.py)| | 0x08000000 (32KB) | PA2-TX / PA3-RX | +IPD帧 | | 192.168.x.x:9000 | +--------------------+ 115200 +-----------+ +------------------+ | GD32F407 App | (引导/升级/跳转) (发送 app.bin) | 0x08008000 (480KB) | +--------------------+

三层的角色:

  1. GD32 板子:总导演。平时跑 App;上电先跑 Bootloader 决定"升级还是跳 App"。
  2. ESP8266:透明"无线串口线"。由板子用 AT 指令指挥它连 WiFi、连服务器;连上后只负责在 WiFi 和串口之间搬运字节,不"理解"任何内容。
  3. PC 服务器:仓库管理员。把固件app.bin按协议一块块发给板子,并核对确认。

物理链路:服务器 ←WiFi/TCP→ ESP8266 ←UART(115200)→ GD32


三、Flash 分区(512KB)

区域地址范围大小占用扇区内容
Bootloader0x08000000 ~ 0x08007FFF32KB扇区 0~1引导 + OTA 逻辑(约 15KB)
App0x08008000 ~ 0x0807FFFF480KB扇区 2~7业务固件
  • 分界线 0x08008000 正好是扇区 2 起始(16KB 对齐),擦除和链接都干净。
  • GD32F407 扇区:0~3 各 16KB,4 号 64KB,5~7 各 128KB。

升级标志:用 RTC 备份寄存器 BKP0(RTC_BASE+0x50),写魔数0x4F544131(“OTA1”)。软复位后保留(备份域不受 NRST 影响),断电才清——正好用于"App 请求升级 → 复位 → Bootloader 看到标志进入 OTA"。


四、Bootloader 工程(gd32f4_boot)

Bootloader 是"看门人",只管引导 + 升级,不做业务。主流程:

intmain(void){intupdate=0;systick_config();dbg_init();led_init();// 时钟/调试串口/LED// 入口1:升级标志(App 请求过)if(*(volatileuint32_t*)OTA_FLAG_ADDR==OTA_FLAG_MAGIC)update=1;// 入口2:上电 2 秒内调试串口收到 'u'if(!update){/* 轮询 2s */}if(update){if(ota_run()==0)打印成功;else打印失败;}if(ota_app_valid())ota_jump_to_app();// 有效则跳转while(1){/* 无有效App则等 'u' 重试,永不砖 */}}

核心文件:

  1. ota.c:升级主体——包头解析、擦除 App 扇区、逐块收写、CRC32 校验、App 有效性检查、跳转。
  2. esp8266.c:AT 驱动(连 WiFi、TCP、+IPD 帧解析、发送 ACK)。
  3. dbg.c:USART0 调试打印。
  4. ota.h:分区地址、升级标志、协议魔数、块大小等。

OTA 传输流程(ota_run)

1. esp8266_connect() 连 WiFi → TCP 连服务器 2. 发就绪握手 0x06 服务器收到后开始 3. 收 10 字节包头 A5 5A + 长度(u32 LE) + CRC32(u32 LE) 4. 擦除 App 区 扇区 2~7(擦除前无数据在途) 5. 发擦除完成握手 0x06 服务器放行 6. 逐块收(128B)+写Flash+回ACK 0x06 7. 全收完比对 CRC32 一致才跳转

五、App 工程改造(gd32f4)

App 是 OTA 架构里的"业务程序",需要两处改造:

  1. 链接到 0x08008000:KeilOptions → Target里 IROM1 起点 0x08008000、大小 0x78000。
  2. 向量表偏移system_gd32f4xx.cVECT_TAB_OFFSET改成0x8000,否则中断全跑飞。
  3. 升级触发:串口 USART0 收到up回车 → 写 BKP0 = “OTA1” →NVIC_SystemReset()复位进 Bootloader。
  4. 编译后自动生成 app.bin:在Options → User → After Build加一句:
    "D:\Program Files\Keil\ARM\ARMCLANG\Bin\fromelf.exe" --bin -o .\Objects\app.bin .\Objects\GD32F407_Template.axf

六、PC 服务器(ota_server.py)

一个纯 Python 的 TCP 服务器,约 80 行。核心循环:

# 就绪握手ready=conn.recv(1)# 等板子发 0x06# 发 10 字节包头conn.sendall(MAGIC+struct.pack("<I",len(data))+struct.pack("<I",crc))# 等擦除完成go=conn.recv(1)# 等板子发 0x06# 分块发送,每块等一个 ACK,收到后延时 50ms 再发下一块sent=0whilesent<len(data):chunk=data[sent:sent+CHUNK]conn.sendall(chunk)ack=conn.recv(1)# 等板子 ACKifack!=b"\x06":breaksent+=len(chunk)time.sleep(0.05)# 发完等 2 秒再关连接,给 ESP8266 时间把尾部数据吐给板子time.sleep(2)conn.close()

七、传输协议详解(通俗版)

整个通信像"打电话对暗号":

① 板子→服务器: 0x06 "我准备好了" ② 服务器→板子: 10字节包头 A5 5A + 长度 + CRC32 ③ 板子→服务器: 0x06 "我擦除完了,放行" ④ 服务器→板子: 数据块(128B) 一块一块发 板子→服务器: 0x06 "这块写好了" (重复直到最后一块) ⑤ 板子校验 CRC32 对上了才跳转

关键设计

  • 包头先行:长度(收多少)+ CRC32(收完怎么验收)。
  • 停等协议(发一块等一个 ACK):板子写 Flash 会暂停 CPU,一发一答保证数据不溢出、不丢。
  • 擦除先打招呼:擦除期间不能有数据在途。
  • CRC32 全程边收边算:最后一次性比对,一个错字节都逃不掉。

八、烧录与使用流程

  1. 先烧 Bootloader:打开GD32F407_Boot.uvprojx,F7 + F8。
  2. 再烧 App:打开GD32F407_Template.uvprojx,F7(自动生成 app.bin)+ F8。
  3. esp8266.h里的 WiFi 名称/密码/服务器 IP。
  4. PC 跑服务器:python ota_server.py Objects\app.bin
  5. 触发升级:串口助手发up回车(App 里),或上电 2 秒内发u(Bootloader)。
  6. 完整链路:连 WiFi → 连服务器 → 交互协议 → 写 Flash → 校验 → 跳转新 App。

九、踩坑记录(这才是本文的精华)

这套方案看似简单,实际从零调通翻了大车。下面每个坑都是真实踩出来的,供大家避坑。

坑 1:跳转 App 时汇编级崩溃(最难)

现象:Bootloader 打印"跳转成功",App 却完全没跑。

根因:__set_MSP(sp); ((void(*)(void))rv)();这两句被 armclang 优化后,编译器把跳转地址暂存在旧栈[sp,#4]切栈之后才从"新栈"读地址 → 读到垃圾值,直接跳飞。

反汇编实锤:

MSR MSP,r0 ; ← 已切到 App 的栈 LDR r0,[sp,#4] ; ← 从这里读跳转地址!读到 App 栈的残留数据 BLX r0 ; ← 跳飞

修复:换栈 + 跳转合并成一条内联汇编,地址全程在寄存器里,不经过栈:

__asmvolatile("MSR msp, %0\nBX %1"::"r"(sp),"r"(rv):"memory");

教训:凡"先切栈指针、再用局部变量"的代码,编译器可能在切栈后读旧栈 —— 用寄存器或内联汇编钉死。

坑 2:跳转后全局中断没开

现象:App 能进main,卡死在delay_1ms

根因:Bootloader 跳转前__disable_irq()跳转 ≠ 复位,PRIMASK 残留为 1,所有中断(含 SysTick)被屏蔽,延时计数器没人递减 → 死循环。

修复:App 的main里加__enable_irq()

坑 3:块/ACK 错位 + 幽灵 ACK(导致 OTA 反复失败)

现象:服务器"凭空收到"一个 ACK,全程错位,最后一块永远等不到、超时。

根因:板子在擦除前后发了三个握手0x06,服务器只读两个。多余的字节滞留 TCP 缓冲区,被服务器误读成"第一块的 ACK",从此服务器永远比板子快一块 → 提前发完关闭 → 尾块丢失。

修复:删除多余的"收到包头"字节,只保留就绪+擦除完成两个握手;服务器发完再等 2 秒关连接。

坑 4:ESP8266 透传模式不可靠

部分 AT 固件的透传模式(AT+CIPMODE=1)收发大数据不可靠——小数据能通、大数据一传就断。改用最经典的+IPD数据帧模式AT+CIPMUX=0+ 逐帧解析+IPD,<长度>:<数据>),兼容性最好。

坑 5:等 “SEND OK” 吃掉下一帧头

发完 ACK 等SEND OK应答,但服务器收到 ACK 后会立刻发下一块,+IPD帧头可能先到并被扫描函数吞掉 → 收帧超时。修复:不再等SEND OK,让帧解析自动跳过残留文本。

坑 6:Keil 下载后程序不运行

不是代码问题:Keil 的 “Reset and Run” 没勾选,下载后芯片停在暂停态。Options → Utilities → Settings → Flash Download勾选"Reset and Run"即可。


十、总结

这套 OTA 方案跑通的四步:

  1. 分区:Bootloader(32KB) + App(480KB),升级标志放备份寄存器。
  2. Bootloader:引导 + 联网 + 擦写 + 校验 + 跳转,所有"脏活"都它干。
  3. App:只负责"请求升级",收到命令置标志复位,不参与接收。
  4. 协议:包头(长度+CRC32) + 分块停等 ACK + 擦除握手,保证不丢不坏。

代码侧最难的两个点:boot跳转app、ACK 错位。把这几个悟透,任何单片机的 Bootloader + OTA 基本都能拿下。希望这篇文章帮你少走我走的弯路。


附:工程文件清单(供参考)

https://gitee.com/born-again-i-am-a-handsome-man/gd32f407-ota

文件作用
gd32f4_boot\GD32F407_Boot.uvprojxBootloader 工程(32KB,0x08000000)
gd32f4_boot\User\ota.c/.h升级协议、擦写、CRC、跳转
gd32f4_boot\User\esp8266.c/.hESP8266 AT 驱动(+IPD 帧模式)
gd32f4_boot\User\main.c引导流程
gd32f4\GD32F407_Template.uvprojxApp 工程(480KB,0x08008000)
gd32f4\ota_server.pyPC 固件服务器
gd32f4\OTA_README.md使用说明

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询