
简介面向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 区只做业务不关心升级协议。对于最常见的 STM32F103C8T6Flash 总容量 64KB推荐分区如下表。注意 C8T6 属于中容量产品Flash 页大小是 1KB如果是 ZET6 大容量页大小是 2KB擦除处理略有差异。启动文件和标准库 V3.5 的工程模板里Flash 大小定义也要改成对应型号否则链接器会报地址溢出。区域起始地址大小内容Bootloader0x0800000016KBIAP 程序中断向量表在这里App0x0800400044KB业务固件中断向量表偏移到此处参数区0x0800F0004KB升级标志、固件版本、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电流能力不低于 2ASTM32F103 另行用 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 引脚PA9USART1_TXRXDPA10USART1_RXTXDGND地GNDPA2USART2_TX串口调试器 RXDPA3USART2_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 命令生成一次完整响应。对于ATQIOPEN这类最终响应不以\n结尾而是先回 CONNECT OK 再回 OK 的命令要单独处理不能依赖at_ready一个标志。调试时还有个小技巧AT_Send失败不要只调大超时先确认模块供电、串口助手能否收到 AT 回显否则调大超时只能掩盖接线问题。3. 通过 A7680C 建立 4G 网络连接并下载固件AT 指令序列与代码3.1 模块入网流程从 APN 配置到 TCP 连接A7680C 上电后先等它注册到网络。常见做法是循环查询ATCREG?返回CREG: 0,1表示已经注册。运营商网络用中国移动时 APN 通常是cmnet电信是ctnet联通是3gnet。APN 配错会导致 PDP 激活成功但后续 TCP 连不上表现为每次 connect 都超时。这一步的查询指令在不同固件上略有差异以下典型指令序列基于常见 Cat.1 模块 AT 集A7680C 实际命令以官方手册为准。目标典型指令判断依据检查模块响应AT返回OK查询网络注册状态ATCREG?CREG: 0,1或,5设置 APNATCGDCONT1,IP,cmnetOK激活 PDPATCGACT1,1OK建立 TCP 连接ATQIOPEN1,0,TCP,x.x.x.x,portCONNECT OK如果服务器有域名A7680C 可先用ATCDNSGIP解析域名再把解析到的 IP 填入 QIOPEN。不建议在 AT 里直接传域名部分固件的 TCP connect 不支持域名。建立 TCP 后模块进入数据透传模式STM32F103 往同一个串口发什么TCP 对端就收到什么对端发来的数据也从这个串口出来。Bootloader 必须区分当前串口数据是 AT 响应还是网络数据所以几乎所有生产级实现都直接用 TCP 数据模式不用ATQISEND一行行发。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 响应不会像手册那么准时。下表是经过现场验证的一组默认参数可以直接作为起点环节超时时间重试次数模块上电后 AT5s3 次间隔 1s等待网络注册30s无一直等等待 PDP 激活15s2 次建立 TCP20s2 次等待 TCP 数据包10s3 次数据重传这里的“重传”指的是 STM32 没有收到有效数据由 Bootloader 主动断开 TCP 再重连而不是让 A7680C 做应用层重传。TCP 本身有重试但在移动网络下链路可能长期半开应用层必须有自己的超时。如果服务器支持断点续传把当前文件偏移存到参数区重新建立 TCP 后用 HTTP Range 头从断点请求。Range 请求头为Range: bytesstart-服务器返回 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防止代码被读出来。如果产品开了 RDPBootloader 中直接FLASH_ProgramHalfWord会写失败必须先解除 RDP而解除 RDP 会触发全片擦除。所以带 OTA 的产品建议不开 RDP或者由 Bootloader 在升级前执行FLASH_Unlock并接受固件被读取的风险。另一个常见问题是固件升级时开了写保护WRPSTM32F103 允许把指定页设为“不可写”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 收到后打印收到的长度和 CRCApp 区多出来几个字节不可执行也没关系只要能看到 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 被拉高导致芯片直接进入系统存储器而不是 FlashJ-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 先下载新版本到 W25Q32CRC 校验通过后再整体写入内部 Flash跳转前如果 App 校验失败Bootloader 可以从 W25Q32 把旧固件写回。这样代价是多一个 SPI Flash 和约 2K 的 Bootloader 代码但换来了“随便升级不会砖”。如果产品内部 Flash 实在不够也可以维持单备份只是升级过程中掉电后需要重新下载不能回滚。验证回滚时故意在服务器上放一个损坏的固件包修改其中一个字节然后触发升级。Bootloader 应能通过 CRC 检测出错误保留旧 App 并打印OTA_CRC_FAIL设备继续沿用老程序工作。这一测过了OTA 才算真正上线。本文还有配套的精品资源点击获取