简介:面向STM32F103单片机开发者,这份例程包演示了通过A7680C 4G模块实现固件远程升级(OTA)的完整方案,适用于物联网设备批量维护、现场程序更新等场景。代码基于KEIL标准库编写,当前适配STM32F103,其他同系列芯片可自行调整型号与Flash容量;工程中已定义单片机与模块的接线,并附有详细注释,方便二次开发。压缩包共456个文件,以C语言源码(h/c)、KEIL工程文件(uvprojx/uvoptx)为主,同时包含编译生成的hex/bin固件、map映射文件及清除编译残余的批处理脚本,整体大小12.35MB,结构清晰便于定位与学习。资源已有364人学习,适合正在做4G远程升级或想了解OTA流程的单片机开发者参考,包含从工程配置到固件生成的完整代码与说明。
1. 为什么 STM32F103 还要走 4G OTA:现场升级的最后一个痛点
一批部署在市政路灯杆上的控制器,板子还是好几年前买的 STM32F103C8T6 最小系统,突然要增加一个上报字段或者修复一个随机死机。派人到现场,每个点位爬杆、开柜、接 USB 转串口,一天最多更新四台;如果设备在信号较弱的车库里,还要抱着笔记本在冷风里等。A7680C 这类 4G Cat.1 模块把远程升级变成了常规能力:模块成本接近一块最小系统板,却能提供稳定的 TCP/HTTP 通道,STM32 只需要一个串口和一段够用的 Bootloader。接下来要拆解的是一条能直接落地的路径:用 STM32F103 通过 USART 连接 A7680C,在 4G 网络下把新固件包下载到内部 Flash,完成 OTA 远程升级。这里不搬 RTOS,也不依赖网络库,只靠串口中断和状态机,把 F103 变成一个可通过 4G 固件升级的节点。
2. STM32F103最小系统与A7680C的硬件连接:先把刷机通道打通
2.1 用标准库 V3.5 做 Bootloader,先定 Flash 分区
OTA 升级不是把程序烧进 Flash 就完事。要保证远程下载过程中任何一步失败,设备都不会变砖,第一步是在 STM32F103 上划分两块独立区域:Bootloader 区和 App 区。Bootloader 常驻 0x08000000,负责跟 A7680C 对话、接收固件、写入 Flash、校验,最后跳转;App 区只做业务,不关心升级协议。
对于最常见的 STM32F103C8T6,Flash 总容量 64KB,推荐分区如下表。注意 C8T6 属于中容量产品,Flash 页大小是 1KB;如果是 ZET6 大容量,页大小是 2KB,擦除处理略有差异。启动文件和标准库 V3.5 的工程模板里,Flash 大小定义也要改成对应型号,否则链接器会报地址溢出。
| 区域 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader | 0x08000000 | 16KB | IAP 程序,中断向量表在这里 |
| App | 0x08004000 | 44KB | 业务固件,中断向量表偏移到此处 |
| 参数区 | 0x0800F000 | 4KB | 升级标志、固件版本、CRC 值 |
参数区放在 Flash 尾部,记录当前固件版本号、升级状态、已接收字节数。升级开始时先写“升级中”,全部接收并校验成功再写“升级完成”。每次上电 Bootloader 检查这个标志,如果发现处于“升级中”状态,说明上次升级没结束,坚决不跳转 App,而是继续等待新固件。这样即使中间断电,重新上电后也能回到升级流程,而不是进入一个不完整的程序。
App 区起始地址要在工程里做向量表偏移。标准库工程中,可以在 App 的 main 函数开始时写SCB->VTOR = APP_ADDR;,这个操作在 F103 上有效。Bootloader 跳转前会关中断并复位外设,App 启动后第一件事就是重定位向量表,否则任何中断都会跑飞到 Bootloader 区,程序表现就是“似乎能跑但一进中断就复位”。
2.2 A7680C 的供电和串口电平匹配
A7680C 是 4G Cat.1 模块,工作电压接近 3.8V,瞬时发射电流很容易超过 1A。我见过有人直接从 STM32F103 的 3.3V LDO 给模块供电,结果一入网就欠压重启。正确接法是单路 DC-DC 从系统 5V 降压到 4V,电流能力不低于 2A,STM32F103 另行用 3.3V LDO 供电,两组电源共地。
串口电平方面,A7680C 的部分固件 UART 默认是 1.8V 逻辑,直接接 PA9/PA10 会把模块 IO 拉坏。最稳妥的是买带 3.3V-1.8V 电平转换的模块核心板;如果手里是裸模块,用两颗 MOSFET 组双向电平转换,或者按 A7680C 手册确认逻辑电平。USB 转串口调试时也要注意:USB 转 TTL 输出 3.3V,不确定时同样建议串接一颗 330Ω 电阻。
引脚接线如下。实际项目中 USART1 专用于 AT 通道,调试日志放到 USART2,避免调试信息和 A7680C 的响应混在一起。
| STM32F103 引脚 | 功能 | A7680C 引脚 |
|---|---|---|
| PA9 | USART1_TX | RXD |
| PA10 | USART1_RX | TXD |
| GND | 地 | GND |
| PA2 | USART2_TX | 串口调试器 RXD |
| PA3 | USART2_RX | 串口调试器 TXD |
天线位置也影响 OTA 稳定性。A7680C 的 LTE 天线尽量放在外壳顶部或侧面,远离 STM32F103 的晶振和 DC-DC 电感。天底下没有绝对标准,但把天线铺在金属柜里,信号强度会掉 10dB 以上,公共物联网卡在弱覆盖下基本无法完成固件下载。
2.3 最小系统上的 AT 命令调试函数
硬件接好,先不要写下载逻辑。用标准库 V3.5 写一个最简单的 AT 发送函数,确认 STM32F103 和 A7680C 能对讲。采用中断方式接收,收到换行符就把整条响应放到全局缓冲区。代码不依赖 RTOS,适合 Bootloader 环境。
#define AT_BUF_LEN 512 static char at_buf[AT_BUF_LEN]; static volatile uint16_t at_len = 0; static volatile uint8_t at_ready = 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { char c = (char)USART_ReceiveData(USART1); if (at_len < AT_BUF_LEN - 1) { at_buf[at_len++] = c; if (c == '\n') { at_buf[at_len] = '\0'; at_ready = 1; at_len = 0; } } } } uint8_t AT_Send(const char *cmd, uint32_t timeout_ms) { at_len = 0; at_ready = 0; while (*cmd) { while ((USART1->SR & USART_FLAG_TXE) == 0); USART1->DR = *cmd++; } uint32_t tick = 0; while (at_ready == 0 && tick < timeout_ms) { DelayMs(1); tick++; } return at_ready; }使用方式:AT_Send("AT\r\n", 2000)返回 1 表示模块回了数据,在at_buf里应能看到 OK。这个函数把缓冲区清零放在发送前,每个 AT 命令生成一次完整响应。对于AT+QIOPEN这类最终响应不以\n结尾,而是先回 CONNECT OK 再回 OK 的命令,要单独处理,不能依赖at_ready一个标志。调试时还有个小技巧:AT_Send失败不要只调大超时,先确认模块供电、串口助手能否收到 AT 回显,否则调大超时只能掩盖接线问题。
3. 通过 A7680C 建立 4G 网络连接并下载固件:AT 指令序列与代码
3.1 模块入网流程:从 APN 配置到 TCP 连接
A7680C 上电后先等它注册到网络。常见做法是循环查询AT+CREG?,返回+CREG: 0,1表示已经注册。运营商网络用中国移动时 APN 通常是cmnet,电信是ctnet,联通是3gnet。APN 配错会导致 PDP 激活成功但后续 TCP 连不上,表现为每次 connect 都超时。这一步的查询指令在不同固件上略有差异,以下典型指令序列基于常见 Cat.1 模块 AT 集,A7680C 实际命令以官方手册为准。
| 目标 | 典型指令 | 判断依据 |
|---|---|---|
| 检查模块响应 | AT | 返回OK |
| 查询网络注册状态 | AT+CREG? | +CREG: 0,1或,5 |
| 设置 APN | AT+CGDCONT=1,"IP","cmnet" | OK |
| 激活 PDP | AT+CGACT=1,1 | OK |
| 建立 TCP 连接 | AT+QIOPEN=1,0,"TCP","x.x.x.x",port | CONNECT OK |
如果服务器有域名,A7680C 可先用AT+CDNSGIP解析域名,再把解析到的 IP 填入 QIOPEN。不建议在 AT 里直接传域名,部分固件的 TCP connect 不支持域名。建立 TCP 后,模块进入数据透传模式,STM32F103 往同一个串口发什么,TCP 对端就收到什么;对端发来的数据也从这个串口出来。Bootloader 必须区分当前串口数据是 AT 响应还是网络数据,所以几乎所有生产级实现都直接用 TCP 数据模式,不用AT+QISEND一行行发。
3.2 用 TCP 自拼 HTTP GET,为断点续传留后路
模块自带 HTTP 客户端用起来最简单,但它的固件包要么一次性存到模块缓存,要么分块读出来,中途断开很难接上。所以生产环境我一般自己组 HTTP GET 请求,走前面建立好的 TCP 连接。请求头至少要包含:
#define HTTP_GET_REQ \ "GET /fw/app.bin HTTP/1.1\r\n" \ "Host: 120.76.xx.xx\r\n" \ "Connection: close\r\n" \ "\r\n"服务器返回的响应会包含 HTTP 状态行、若干 Header、空行,然后是二进制固件。Bootloader 解析 Header 时重点关注Content-Length,用它判断固件总大小;如果服务器响应带Transfer-Encoding: chunked,需要额外解析分块边界,OTA 服务器应固定返回Content-Length,让 Bootloader 只按长度收数据。状态行出现HTTP/1.1 200 OK说明请求命中;出现 302 说明固件地址已迁移,可以直接退到 Bootloader 重新解析服务器返回的 Location,也可以让运维人员避免在服务器上做重定向。
3.3 状态机处理 AT 响应和固件数据
OTA 主循环用一个枚举状态机推进,每个状态对应一个 AT 命令或一段数据处理:
typedef enum { ST_BOOT, // 等模块响应 ST_WAIT_REG, // 等待网络注册 ST_SET_APN, ST_ACT_PDP, ST_CONNECT_TCP, ST_SEND_HTTP, ST_PARSE_HDR, ST_RECV_DATA, ST_CRC_CHECK, ST_JUMP_APP, ST_ERROR } ota_state_t;在 RECV_DATA 状态下,每收到一个 TCP 数据包,就调用 Flash 写入函数:
void Write_FW_Block(uint8_t *buf, uint32_t len, uint32_t addr) { FLASH_Unlock(); if ((addr & 0x3FF) == 0) FLASH_ErasePage(addr); for (uint32_t i = 0; i < len; i += 2) { FLASH_ProgramHalfWord(addr + i, *(uint16_t *)(&buf[i])); } FLASH_Lock(); }写入前先校验len是偶数,并且addr + len不越界。擦除页时注意页对齐:F103 中容量 Flash 每页 1KB,大容量每页 2KB,上面的addr & 0x3FF只适合中容量 1KB 页。如果目标地址不是页起始地址,要先检查该页是否已经擦过,避免重复擦除导致 Flash 损耗。
所有网络数据必须先放进 512 字节的 RAM 缓冲,满一页再写 Flash。不要在串口中断里直接调用FLASH_ErasePage,擦除操作会阻塞几十毫秒,中断里做会产生丢包。正确做法是:串口中断只把数据搬到环形缓冲,主循环每次取缓冲里一段连续数据,写到 Flash 后立即喂狗。A7680C 的串口并不会因为 STM32 在擦 Flash 就暂停,环形缓冲深度建议至少 2KB,否则 460800 波特率下擦一页会丢半包数据。
4. 让 OTA 升级可落地的 4 个参数:超时、重传、校验和 Flash 写保护
4.1 固件包尾部附加 CRC32,校验通过才允许跳转
没有校验的 IAP 就是空中变砖。常见做法是在发布固件时,用脚本在 bin 文件末尾追加 8 个字节:前 4 字节是 CRC32,后 4 字节是固件长度。Bootloader 在接收数据时边写入边计算 CRC,收完后从包尾读出期望的 CRC,再和计算值比较。
CRC32 的查表法代码网上很多,注意 Bootloader 和打包脚本必须用同一个初始多项式、初值和异或输出。我常用初始值0xFFFFFFFF、最终异或0xFFFFFFFF,避免不同实现算出不同结果。CRC 只保证传输和写入没有错,不做固件签名。如果产品需要防伪造,再叠加 RSA2048 签名验证,否则 OTA 下载源一旦被劫持,设备会被刷入恶意程序。
4.2 超时和重传参数,按现场弱信号环境设置
A7680C 在弱信号下会频繁重传数据,AT 响应不会像手册那么准时。下表是经过现场验证的一组默认参数,可以直接作为起点:
| 环节 | 超时时间 | 重试次数 |
|---|---|---|
| 模块上电后 AT | 5s | 3 次,间隔 1s |
| 等待网络注册 | 30s | 无,一直等 |
| 等待 PDP 激活 | 15s | 2 次 |
| 建立 TCP | 20s | 2 次 |
| 等待 TCP 数据包 | 10s | 3 次数据重传 |
这里的“重传”指的是 STM32 没有收到有效数据,由 Bootloader 主动断开 TCP 再重连,而不是让 A7680C 做应用层重传。TCP 本身有重试,但在移动网络下链路可能长期半开,应用层必须有自己的超时。如果服务器支持断点续传,把当前文件偏移存到参数区,重新建立 TCP 后用 HTTP Range 头从断点请求。Range 请求头为Range: bytes=<start>-,服务器返回 206 状态码。这个偏移量要在写 Flash 前更新,并且写入参数区时要触发一次 Flash 擦除,频繁更新会损耗 Flash,建议每次下降 4KB 才记录一次。
4.3 波特率对吞吐量和丢包率的影响
A7680C 串口默认波特率通常 115200,单独跑数据没问题。115200 大约每秒 11KB,下载一个 44KB 固件要 4 秒以上,看起来不快但稳定。把波特率提到 460800 可以缩短时间,但要求 STM32F103 串口中断响应足够快,否则模块 TX 数据积压导致丢包。我的经验是:如果 Bootloader 里用了 DMA 环形缓冲,可以稳妥地跑 460800;如果只是普通中断逐字节搬运,就留在 115200。
另外,A7680C 的 UART 发送数据时不会因为 STM32 慢就暂停。模块 TCP 接收缓冲区从网络收到一包数据后,会一口气从串口吐出来,所以 Bootloader 里不要做“收到一个字节就处理一个字节”的阻塞逻辑。DMA 每收到 512 字节触发一次中断,主循环查看 DMA 剩余计数,算出本次收到的连续数据长度,再交给 Flash 写入函数。这样 CPU 不需要在中断里做重活,也能在 460800 波特率下不丢包。
4.4 Flash 写保护、看门狗和升级标志联动
STM32F103 的 Flash 可以用读保护(RDP)防止代码被读出来。如果产品开了 RDP,Bootloader 中直接FLASH_ProgramHalfWord会写失败,必须先解除 RDP,而解除 RDP 会触发全片擦除。所以带 OTA 的产品建议不开 RDP,或者由 Bootloader 在升级前执行FLASH_Unlock,并接受固件被读取的风险。另一个常见问题是固件升级时开了写保护(WRP),STM32F103 允许把指定页设为“不可写”,Bootloader 写入 App 区前必须先FLASH_Unlock,再调用FLASH_Unlock后还要检查FLASH_CR->WRPR状态。
看门狗选择:Bootloader 里保持 IWDG 开启,每收到一个有效 TCP 数据包就喂狗,同时把“升级中”标志写入参数区。如果升级中途复位,Bootloader 重启后发现标志,会继续等待新固件,不会跳转到一个不完整的 App。这个标志的写入时机很关键:不要在 TCP 连接刚建立时就写,而是等收到 HTTP 200 后再写,避免一次误触发导致 Bootloader 一直待在升级流程里。
if (ota_flag == 0x5AA5) { while (1) { if (try_download_firmware() == OTA_OK) break; DelayMs(1000); } }5. 用“最小成本”验证 OTA:串口日志、J-Link和升级失败回滚
5.1 先用一个 7 字节的伪固件验证整条链路
在把真实固件放上服务器之前,先在电脑上开 TCP Server,发 7 个字节,比如0x00 0x01 0x02 0x03 0x04 0x05。Bootloader 收到后打印收到的长度和 CRC,App 区多出来几个字节不可执行也没关系,只要能看到 Bootloader 完成接收流程。这一步能把 AT 指令、TCP 连接、数据解析和 Flash 写入拆开排查。真实固件测试放在最后,先做十几轮伪固件循环下载,再手动拔掉模块电源模拟中断,确认重启后 Bootloader 能回到升级等待状态。
5.2 日志输出和 DAP 下载失败的排查
STM32F103 的 USART2 一定预留为调试串口,在 Bootloader 每个状态切换点输出一行状态;App 启动后也输出版本号,这样串口助手就能看到BOOT_JUMP之后紧跟着APP_OK。如果 App 起不来,检查SCB->VTOR是否设置,以及 Bootloader 跳转前是否关闭了全局中断。跳转代码本身也容易踩坑:要用函数指针跳到 App 的 reset handler,并且把 MSP 初始化为 App 栈顶。
现场最常见的三个问题:
- Boot0 被拉高,导致芯片直接进入系统存储器而不是 Flash,J-Link 连接时报 DAP 相关错误。解决办法是用 10K 电阻把 Boot0 拉低,Boot1 也同样拉低。
- A7680C 和 STM32 的电平匹配错误,表现为 AT 指令时通时不通。直接测量模块 TX 引脚波形,确认逻辑高电平是 1.8V 还是 3.3V。
- 使用了 PA11/PA12 作为 A7680C 的复位或状态引脚,结果调试时 USB 功能干扰系统。建议模块状态监测脚选 PB 系列空闲引脚,避开 PA9-PA12 这一带高频外设。
5.3 双备份回滚,把升级失败的影响压到最低
如果内部 Flash 空间允许,升级前把当前固件备份到外部 SPI Flash,比如 W25Q32。Bootloader 先下载新版本到 W25Q32,CRC 校验通过后再整体写入内部 Flash;跳转前如果 App 校验失败,Bootloader 可以从 W25Q32 把旧固件写回。这样代价是多一个 SPI Flash 和约 2K 的 Bootloader 代码,但换来了“随便升级,不会砖”。如果产品内部 Flash 实在不够,也可以维持单备份,只是升级过程中掉电后需要重新下载,不能回滚。
验证回滚时,故意在服务器上放一个损坏的固件包:修改其中一个字节,然后触发升级。Bootloader 应能通过 CRC 检测出错误,保留旧 App 并打印OTA_CRC_FAIL,设备继续沿用老程序工作。这一测过了,OTA 才算真正上线。
本文还有配套的精品资源,点击获取