STM32F103基于4G模块的OTA远程固件升级实战:BootLoader与Flash分区设计
2026/9/16 15:45:22 网站建设 项目流程

简介:面向STM32F103单片机的嵌入式开发者,这份资料演示了通过A7680C 4G模块实现远程固件升级(OTA)的完整工程。代码基于KEIL标准库编写,已在STM32F103上运行,同系列其他型号也可通过修改芯片型号和Flash容量适配。压缩包共456个文件,其中96个.h头文件与84个.c源文件构成主体逻辑,配合uvprojx、uvoptx等工程文件可直接打开编译;bin、hex固件便于烧录验证,sct、map、lst等则记录了链接与编译细节,适合深入分析。资源整体约12.35MB,已有364人学习。工程内包含Boot区和APP区程序,并标注了与A7680C的接线引脚,还附带一键清除KEIL编译残余的批处理脚本。对于需要快速搭建4G远程升级链路、避开常见移植问题的开发者,这份例程能有效缩短调试周期。

1. 为什么拿 STM32F103 和 A7680C 搭 4G OTA 升级

我第一次按下 A7680C 的 PWRKEY 时,想法和大多数人一样:一块几块钱的 STM32F103,加一块 4G 模块,能不能真的把固件远程刷上去?答案是肯定的,而且这套例程已经把链路跑通了。它要解决的场景很常见:设备装在配电箱、农田大棚、工地塔吊上,返厂刷程序根本不现实,而 STM32F103 又是存量最大的控制核心。与其把老设计推倒重来做 NB-IoT 或 WiFi,不如外挂 4G 模块,通过 AT 指令把新固件拉回来,走 BootLoader 跳转更新。适合正在做数据采集、远程控制、物联网网关的单片机开发者,也适合手里攥着标准库工程、不想迁移到 HAL 库的老手。

2. BootLoader 与 Flash 分区设计:把升级标志位藏进最后 2KB

OTA 升级的核心不是一个下载动作,而是系统结构上的重新分工。如果不加 BootLoader,下载下来的二进制根本不知道该往哪放,程序自己覆盖自己也是极其危险的。我的做法是把整个工程拆成 BootLoader 和 App 两个独立固件,App 负责业务,BootLoader 负责启动和远程更新,两者用 Flash 地址硬隔离。下面先定义分区,再解决启动判断,最后串起状态机。

2.1 为什么 OTA 一定要有 BootLoader

BootLoader 的本质是“升级执行器”,App 跳转和下载过程的可靠性都由它兜底。对于 STM32F103 这种内部 Flash 只有几十到上百 KB 的 MCU,外挂 4G 模块后,BootLoader 要做的事反而要精简:上电判断是否进入升级模式,若需要升级则初始化 A7680C 下载固件,写入 App 区,最后跳转。不需要把 TCP/IP 协议栈跑在单片机里,也不用跑复杂文件系统,这样才能把 BootLoader 压到 16KB 以下。

另一个容易被忽略的原因是:App 运行期间如果直接擦写自身 Flash 来升级,指令预取会有机会执行到擦了一半的代码,导致 HardFault。所以升级动作必须从 BootLoader 这个独立的地址空间发起,App 只能把升级请求写到固定标志区,然后软复位,把控制权交还给 BootLoader。这个设计是 OTA 能稳定工作的前提。

2.2 Flash 分区与升级标志位

分区设计我按 STM32F103C8T6(64KB Flash)来写,如果你用的是 RCT6 或别的型号,只需要按比例平移地址。下面是一张常用的分区表。

区域地址范围大小功能
BootLoader0x08000000 - 0x08003FFF16KB上电启动、升级逻辑、跳转判断
App 业务区0x08004000 - 0x0800F3FF44KB用户业务程序,编译时 IROM1 从 0x08004000 开始
OTA 标志区0x0800F400 - 0x0800F7FF1KB保存升级请求和固件校验信息
保留/日志区0x0800F800 - 0x0800FFFF2KB可存版本号、运行日志,也可留给 BootLoader 做标记

注意 STM32F103 的 Flash 页大小为 1KB,擦除以页为单位,所以我把标志区对齐到整页。升级标志用一个结构体放在一个页里,避免频繁擦写把 Flash 磨坏。

#define OTA_FLAG_ADDR 0x0800F400U #define OTA_MAGIC 0xA5A5A5A5U typedef struct { uint32_t magic; /* 固定魔数,用于判断标志区是否有有效内容 */ uint32_t app_len; /* 待升级 App 固件的长度 */ uint32_t app_crc; /* App 固件的 CRC32 校验值 */ uint32_t upgrade_state; /* 升级状态:0=无请求,1=请求升级,2=下载完成 */ } ota_flag_t;

这段结构体是整个升级流程的“通信协议”。App 端收到云平台指令后,先填充 app_len 和 app_crc,再把 upgrade_state 置为 1,最后写进 0x0800F400。BootLoader 每次启动只读这一个结构体,就能决定是进升级流程还是直接跳 App。之所以不用外部 EEPROM,是因为 F103 内部没有 EEPROM,额外挂 24C02 会增加成本和接线,直接用一个 Flash 页足够。

2.3 升级状态机与启动流程

整个升级流程用状态机描述,BootLoader 和 App 各占一半。App 运行时如果收到云平台下发的升级指令,先通过 4G 模块确认有新版本,把上述元数据写进 OTA 标志区,然后软复位进入 BootLoader。BootLoader 上电后在 main 函数最开始读标志区:magic 对应且 upgrade_state 为 1 时,进入升级流程;否则直接跳转 App。

下载过程中 App 区的旧固件可能已经被擦掉,如果中途断电,下次上电因为 App 区栈顶指针无效,BootLoader 的跳转函数检测到非法地址,同样会进入升级模式,防止设备变砖。判断栈顶的代码放在 BootLoader 里是一个关键点。

uint32_t app_msp = *(volatile uint32_t *)APP_BASE_ADDR; if ((app_msp & 0xFFFF0000) == 0x20000000) { jump_to_app(APP_BASE_ADDR); } else { /* App 不完整,进入 4G 升级模式 */ ota_enter_upgrade(); }

这里 APP_BASE_ADDR 对应 0x08004000,0x20000000 是 STM32F103 SRAM 的起始地址。判断栈顶地址落在 RAM 范围内,说明 App 的向量表头没有被擦掉或没有变成全 0xFF;如果栈顶非法,就说明 App 区不可信,必须进入升级模式。这个判断比重置向量更可靠,因为某些调试器会填零而不是填 FFFF。

3. 用 A7680C AT 指令把固件抓到 F103:建连、HTTP 下载、分包接收

A7680C 是 4G Cat.1 模块,内部自带 TCP/IP 协议栈,STM32F103 只需要通过串口发 AT 指令就能拉取固件。这一章把接线、网络建立、HTTP 取固件和分包接收拆开讲,每一步都对应例程里的实际代码。

3.1 A7680C 的启动与 UART 接线

A7680C 工作电压 3.4-4.2V,跟 STM32F103 不在一个电源轨上,而且模块的串口电平通常不兼容 3.3V 直接输入。例程代码里把 RX、TX 定义在某个 USART 上,接线定义都写在注释里,用之前先确认三件事:模块供电能拉到 2A 峰值,PWRKEY 拉低 600ms 以上启动,串口 TTL 电平需要做转换。很多开发板直接连 F103 也能跑,但功耗一高就容易重启。

STM32F103A7680C说明
PA2 (USART2_TX)UART_RX主控发送 AT 指令到模块
PA3 (USART2_RX)UART_TX模块返回应答和固件数据
GNDGND必须共地
3.3V → 电平转换UART_RX/ UART_TX建议用 1.8V 电平转换芯片

A7680C 的 UART 接口一般支持 1.8V 逻辑,和 F103 的 3.3V 引脚直连存在长期损伤风险。稳妥办法是加一块 TXS0108 或电平转换小板,至少用两个电阻分压,否则长时间运行会出现偶发乱码,升级文件一错就是一串地址错误。

3.2 先调通网络再谈下载:AT 命令序列

模块上电后首先要确认 AT 握手成功。A7680C 的 AT 指令与常见 4G 模组兼容度很高,用串口工具逐个发送下面的指令即可。网络建立序列是:

指令期望应答作用
AT\r\nOK检查串口和模块状态
ATE0\r\nOK关闭回显,减少干扰
AT+CGDCONT=1,"IP","cmnet"OK设置 APN,cmnet 是中国移动默认
AT+CGACT=1,1OK激活 PDP 上下文
AT+CSQ+CSQ: 20,99查询信号强度,第一值 0-31,20 以上比较稳
AT+COPS?+COPS: 0,0,"CHINA MOBILE",7确认已注册网络

如果你的卡是电信或联通,APN 要改成 ctlte 或 3gnet。注意 AT+CSQ 只返回信号强度,不代表网络已注册,所以还要用 AT+COPS? 确认注册上。模块内部自带的 APN 配置不一定能和后续 HTTP 链路配合,稳妥做法是在上下文里显式指定 APN。

串口发送函数我封装成带期望值判断的形式,避免每条指令都要写一遍超时循环。

static uint8_t at_cmd(const char *cmd, const char *expect, uint32_t timeout) { uart_put_str(cmd); ringbuf_reset(&g_rxbuf); uint32_t start = now_ms(); while (now_ms() - start < timeout) { if (strstr((const char *)g_rxbuf.data, expect) != NULL) { return 0; } } return 1; /* 超时返回失败 */ }

用法是at_cmd("AT+CGACT=1,1\r\n", "OK", 3000),返回 0 表示成功,返回 1 就打印错误码。这个函数背后依赖一个环形缓冲区,不要一边从串口中断里收数据一边在主循环里 strstr,那样会出现半个包匹配不到的问题。CSTX2023 例程里用的是中断加缓冲区的经典结构,调用前先 reset 一次,保证检测的是本次命令的应答。

3.3 用 HTTP 把固件 body 一节节拉下来

网络通了以后,用模块的 HTTP 功能下载 APP1.bin 或 APP3.bin。A7680C 支持 HTTP 客户端,流程是:AT+HTTPINIT 初始化,AT+HTTPPARA 设置 URL,AT+HTTPACTION=0 发起 GET,AT+HTTPREAD 读取响应 body。但模块内部 RAM 有限,固件动辄几十 KB,不能一次性全读回来,需要边读边写 Flash。

我采用的序列是:先 GET 拿到固件长度,再用 HTTPREAD 按 1024 字节分段读取,凑满一页就写入 App 区。这里有一个关键参数:AT+HTTPPARA 里的 READTIMEOUT,如果设太短,大文件下载容易被截断;设太长,升级失败又要在串口傻等。我一般设 15 秒。

uint32_t file_len = get_http_file_len("http://192.168.1.10/ota/app.bin"); for (uint32_t offset = 0; offset < file_len; offset += 1024) { memset(g_buf, 0xFF, sizeof(g_buf)); snprintf((char*)tmp, sizeof(tmp), "AT+HTTPREAD=%lu,%lu\r\n", offset, 1024); at_cmd((char*)tmp, "+HTTPREAD:", 5000); /* 从模块返回的数据帧中解析出 1024 字节 */ parse_httpread_bytes(g_buf, 1024); /* 写入 APP 区,地址 = APP_BASE_ADDR + offset */ ota_write_flash(APP_BASE_ADDR + offset, g_buf, 1024); }

AT+HTTPREAD 的偏移和长度要自己拼,有些模块支持AT+HTTPREAD=len,但它会从缓冲头开始读,不利于分包续传,所以我统一用带偏移的方式。每写完一页就打印一次进度,方便从串口确认卡在哪一包。如果模块返回 CONNECT FAIL,先不要反复发指令,用AT+CGACT=0,1再恢复激活一次,通常是 DNS 解析失败,重置 PDP 上下文比重启模块更快。

4. F103 标准库 Flash 写入与 App 跳转代码拆解

固件数据通过串口进来后,接下来的硬骨头是 Flash 写入和跳转。STM32F103 标准库 V3.5 的 Flash 操作接口不多,但用错地址和时序会把自己锁住。这一章把跳转、擦写、校验三个关键函数拆开讲。

4.1 跳转前的关键检查:栈顶和复位向量

从 BootLoader 跳转 App 前,必须检查两个 32 位内容:App 区地址处的栈顶指针(MSP)和偏移 0x04 的复位向量。很多人只检查第一个字,第二个字不检查,结果跳到一个空地址上死循环。我写的跳转函数会把两个条件都验证。

void jump_to_app(uint32_t app_base) { uint32_t msp = *(volatile uint32_t *)app_base; uint32_t reset = *(volatile uint32_t *)(app_base + 4); if ((msp & 0xFFFF0000) != 0x20000000) { return; /* 无效 App,停留在 BootLoader */ } if ((reset & 0xFFFF0000) != 0x08000000) { return; /* reset 向量不在 Flash 范围,放弃跳转 */ } __disable_irq(); NVIC_SetVectorTable(NVIC_VectTab_FLASH, app_base); /* 标准库写法 */ __set_MSP(msp); ((void (*)(void))reset)(); }

NVIC_SetVectorTable 是老标准库函数,作用是把 Cortex-M3 的向量表基地址重定位到 0x08004000,避免 App 中断进来时查到 BootLoader 的向量表。旧版本标准库里也有 SCB->VTOR 直接写的方法,但 F103 某些早期芯片对 VTOR 偏移有限制,用库函数更稳。跳转前关掉全局中断,是因为外设中断状态在跳转瞬间不可预测,App 启动代码会重新初始化 NVIC。

4.2 按页擦除、半字写入的细节

Flash 写入的坑比想的多。F103 擦除按页进行,C8T6 每页 1KB,而写入必须以半字(16 位)为单位。先擦后写,顺序不能反,否则数据写不进去。标准库的流程固定在五个函数之间。

void ota_write_flash(uint32_t addr, const uint8_t *data, uint32_t len) { FLASH_Unlock(); FLASH_ClearFlag(FLASH_FLAG_BSY | FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); /* 按 1KB 页对齐擦除 */ uint32_t page_start = addr & ~(FLASH_PAGE_SIZE - 1); for (uint32_t p = page_start; p < addr + len; p += FLASH_PAGE_SIZE) { FLASH_ErasePage(p); } /* 半字写入,注意一次只能写 16 bit */ for (uint32_t i = 0; i < len; i += 2) { uint16_t halfword = data[i]; if (i + 1 < len) { halfword |= (uint16_t)((uint16_t)data[i + 1] << 8); } FLASH_ProgramHalfWord(addr + i, halfword); } FLASH_Lock(); }

为什么必须半字写:F103 的 Flash 控制器不支持单字节编程,写 0xFF 是擦除后的状态,写其他字节要先把对应半字整体写入。如果 len 是奇数,最后一个字节要补 0xFF,否则会越界访问 data 数组。擦除整页很慢,一页大约 20ms,下 44KB 固件要擦 43 页,累计接近 1 秒,这个期间要关掉外部看门狗喂狗,否则会被打断。

注意 FLASH_ClearFlag 这一步不是形式上的,之前如果发生写保护或编程错误,标志位不清理,后续擦写会一直失败。特别是调试 Halt 在 Flash 操作中途时,很容易留下 PGERR 标志。

4.3 升级完成后的校验与软件复位

写完不校验就是耍流氓。我的做法是下载完成后,对 App 区整体读回算 CRC32,和 OTA 标志区里的 app_crc 比对。app_crc 在下载开始前由服务器侧算好,写进标志区;如果比对失败,说明这次升级内容有问题,保留升级请求并复位重试。

uint32_t calc_crc32(uint32_t addr, uint32_t len); if (calc_crc32(APP_BASE_ADDR, file_len) != flag->app_crc) { flag->upgrade_state = 1; /* 保留升级请求,下次重试 */ save_ota_flag(flag); NVIC_SystemReset(); } flag->upgrade_state = 2; save_ota_flag(flag); jump_to_app(APP_BASE_ADDR);

save_ota_flag 内部先擦标志页再写新数据。这里有个容易中招的细节:如果写入过程中断电,标志区会变成一半新、一半旧,下次读出来的结构体可能既不是升级请求也不是完成状态。解决方法是调整结构体成员的写入顺序,先写 magic 和 app_crc,最后写 state;BootLoader 判断时只认 state 的值,中间态一律视为未完成升级。

例程里给了 APP1.bin 和 APP3.bin 两个文件,实际使用中分别代表验证版本和正式版本。通过 HTTP 请求参数指定不同 bin,就能在同一个 BootLoader 上反复测试升级流程。

5. 远程升级避坑:Keil 编程器选择、启动文件匹配和防变砖技巧

这一章整理几个最容易让项目卡壳的实操点,全部来自我跑 CSTX2023 例程时踩过的坑,每一条都能直接套用。

5.1 Keil 里 JLINK、STLINK 选择决定你能否一次下载成功

拿到工程第一件事是看 Debug 配置。如果电脑上插的是 ST-Link,而工程里选的还是 J-LINK,一按下载就会报 RDDI-DAP Error。打开 Options for Target → Debug,右侧下拉框选成自己的调试器,再确认 Settings 里的 Flash Download 算法。算法容量选错也会报 Programming Failed at address 0x08000000。STM32F103C8T6 用 Medium-density 算法,RCT6 和 ZET6 用相应容量型号,不要盲目选 High-density。

5.2 启动文件与 Flash 容量匹配

标准库 V3.5 的工程里,startup_stm32f10x_hd.s 和 startup_stm32f10x_md.s 不能混用。MD 用于中等容量片内 Flash,HD 用于高密度。容量不匹配时,启动代码里的 SystemInit 调用没问题,但中断向量表地址在链接阶段就偏了,最常见的表现是程序能下载但跑飞。如果下载时提示 No target connected,先检查 BOOT0 是否被拉高,而不是怀疑芯片坏了,BOOT0=1 时芯片跳到系统存储器,调试口默认不响应。

另一个坑是 App 工程 IROM1 地址。App 的 Options for Target → Target 里,IROM1 Start 必须写成 0x08004000,Size 写 0xB000(对应 44KB),不能保持 0x08000000。如果忘了改,App 编出来的向量表在 BootLoader 区,跳转后执行的是 App 代码但中断向量还是 BootLoader 的,一开中断就跑飞。

5.3 防变砖三板斧

OTA 最怕升级一半断电。我的习惯是:第一,BootLoader 启动后 5 秒内检查 OTA 标志,没有升级请求就立即跳转 App,把升级窗口压到最短;第二,App 区栈顶非法时不要硬跳,停留在 BootLoader,同时串口打印错误码;第三,在 BootLoader 里启用独立看门狗,喂狗周期短于下载超时时间,程序卡在 HTTP 解析上时自动复位。

如果不幸把 App 区和标志区都刷坏了,还有最后一招:通过串口强制让 BootLoader 进入升级模式。CSTX2023 例程的 BootLoader 上电时会在串口打印一个短窗口,窗口期内收到任意字符就进入 AT 升级流程。此时即使 App1.bin 已经废了,也可以用 APP3.bin 恢复。工程里同时给出这两个 bin,目的就是让你验证多版本回退。

恢复新固件前先用 AT+CGATT=1 确认网络注册完成,再走 HTTP 下载。如果因为中途断电导致标志区数据混乱,先把 ota_flag_t 所在页整体擦除一次,让 BootLoader 判定 App 无效,然后主动进入升级模式等待新固件。只要 BootLoader 还在,设备就不会彻底变砖。

远程升级做完后,我习惯把 APP1.bin 对应的版本号写进保留区,每次上电通过标志区比对,这样下次升级前能确认固件是从旧版本升级上来,而不是重复刷同一个版本。用一个简单的 4 字节版本号就能实现。

本文还有配套的精品资源,点击获取

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

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

立即咨询