资讯动态

STM32 HAL库驱动4G模块:AT指令、串口DMA与数据上报实战

发布时间:2026/10/15 20:40:28 来源:尧图企业网站定制
简介针对STM32与4G模块集成需求这份6.38MB的代码工程覆盖信号质量获取、ICCID/IMEI读取、登录包组装及DTU联网参数配置等关键环节适合有一定HAL基础的物联网开发者参考或直接移植。包体共159个文件以.h、.c源码为主配合.uvprojx工程文件、.ioc配置与.hex烧录文件还有.o、.axf等编译中间产物可帮助理解从源码到固件的完整构建链路。已有2489人学习下载印证了其在教学与项目中的实用价值。作者基于STM32 HAL库封装了与4G模块交互的AT指令逻辑并提供可运行工程框架读者能快速掌握4G模块初始化、网络注册及数据收发处理方式节省底层驱动调试时间。特别是DTU心跳包设置与身份信息整合的示例对实际物联网项目落地具有直接参考意义。1. 为什么说 4G 模块程序的关键不在模组而在你对 HAL 外设的掌控力做设备数据上报时很多人把 stm32-hal-4G模块程序 想复杂了以为难点在模组协议、在云平台对接。实际上把几十台设备跑完你会发现九成故障都出在串口收发不可靠、电源纹波把模组打死、AT 响应解析写得太脆这三件事上。Cat.1/Cat.4 模组本质上是一个通过串口收发 AT 指令的黑匣子你的全部工作就是用 STM32 HAL 把串口管稳、把指令时序管对、把异常恢复管住。这篇文章面向正在用 HAL 库做 4G 模组接入的嵌入式开发者从串口链路一路写到 TCP/HTTP 上报每一节都给出可以直接抄的参数表和代码照着搭就能把链路跑起来。2. 从 AT 指令到 HAL 串口先把通信链路的地基打牢2.1 4G 模组的通信本质串口 AT 指令先明确一个事实4G 模组内部是完整的协议栈TCP/IP、PPP、DNS 这些都在模组里跑完了MCU 看到的只有一根串口。你发ATCIPSTARTTCP,192.0.2.10,8080模组去建连接建好了回一个CONNECT OK之后收到的每一条数据都是模组替你解包后的净数据。所以程序的核心就两件事把 AT 指令可靠地发出去把模组的响应可靠地收回来并解析。这决定了你不需要懂蜂窝协议细节但必须把 UART 收发做成不出错的原语。AT 指令的格式有固定套路指令以\r\n结束响应以\r\n开头、以\r\nOK\r\n或\r\nERROR\r\n结束。关键坑在于模组会主动上报一些不带 OK 的异步消息比如信号变化时冒出来的CREG: 5这类叫 URC 数据随时可能插在你想收的响应中间。如果程序用“发完指令立刻死等响应”的写法URC 一打断就翻车。我的做法是先收进环形缓冲区再按行分割解析等目标关键词出现就算完成。常用参数先列在这里后面代码都以这张表为准参数项推荐值说明波特率115200多数模组出厂默认可通过 AT 指令改但没必要数据位/停止位/校验8N1HAL 默认配置兼容性最好流控关闭简单场景不开 RTS/CTS省两个引脚RX 模式DMA 循环 IDLE 中断收不定长响应最稳的方式TX 模式DMA 普通模式发完一次指令通知一次即可2.2 用 CubeMX 配好 UART 与 DMA三个必调的参数在 CubeMX 里新建工程选好芯片后把模组接的那组 USART 配成异步模式波特率 1152008N1。DMA 设置页里给该 USART 分别加 TX 和 RX 通道RX 必须选 CIRCULAR 循环模式TX 用 NORMAL。这里有三个参数我每次都会检查少一个后面调试都要加倍还债。第一个是 RX DMA 模式必须是 CIRCULAR。循环模式下 DMA 自动回卷你不需要在收到一帧后重新配置 DMA中断回调里把数据拷走就行非常适合处理不定长的 AT 响应。第二个是 NVIC 里必须同时打开 USART 全局中断和 DMA 中断只开其中一个会出现“收到数据但回调不触发”的玄学现象。第三个是波特率要和模组内部实际配置一致很多模组支持ATIPR115200改默认波特率如果模组被改过而程序没改握手阶段就会全乱码。初始化代码用 CubeMX 生成即可但有一个地方建议手动补开启接收。HAL 库不像老的 STD 库那样自动开始收数据必须在主程序里主动调用一次接收函数否则 DMA 永远不会把数据搬进缓冲区/* 开启 UART 的 ReceiveToIdle DMA 接收数据帧以空闲线为结束标志 */ HAL_UARTEx_ReceiveToIdle_DMA(huart2, rx_buf, RX_BUF_SIZE); /* 停止接收时用下面这句参数不同别混用 */ // __HAL_DMA_DISABLE(huart2.hdmarx);逻辑说明HAL_UARTEx_ReceiveToIdle_DMA是 HAL 库把 DMA 循环接收和空闲线检测封装在一起的接口总线上一帧数据发完、线路空闲超过一个字节时间就触发一次 RxEvent 回调正好契合 AT 响应以回车换行结尾的特征。参数里rx_buf是接收缓冲区RX_BUF_SIZE是缓冲区长度模组偶尔会把一长串 URC 连续吐出来缓冲区建议 1024 字节起步太小会丢帧。2.3 底层收发函数让“发指令、收响应”变成可靠原语底层要封装两个原语发送用阻塞式HAL_UART_Transmit加超时接收用 DMA 循环模式加回调。发送用阻塞就够了AT 指令长度最多一两百字节115200 波特率下发送时间只有十几毫秒不会卡死主循环。接收的关键在回调里把数据及时搬进自己的环形缓冲区搬得越早越不容易被覆盖。#define RX_BUF_SIZE 1024 static uint8_t uart_rx_buf[RX_BUF_SIZE]; /* DMA 直接写入 */ static uint8_t at_line_buf[512]; /* 按行解析用 */ static volatile uint16_t rx_write_idx 0; /* DMA 写入位置由硬件决定 */ void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart huart2) { /* Size 是本次空闲中断前新收到的字节数DMA 是循环模式 这里只记录末尾位置解析函数从上次位置读到这 */ rx_write_idx (rx_write_idx Size) % RX_BUF_SIZE; /* 收到数据后立即重新挂起接收循环模式下这一句是必需的 */ HAL_UARTEx_ReceiveToIdle_DMA(huart2, uart_rx_buf, RX_BUF_SIZE); } }逻辑说明DMA 在后台持续把串口数据写进uart_rx_buf空闲中断触发时Size给出本次新增字节数回调更新写入位置后立刻重新调用接收函数保证下一帧到达时 DMA 仍在工作。这里没有做复杂的临界区保护因为中断里只更新一个索引值主循环解析时即使被中断打断读到旧值最多晚解析一次不会出错。发送函数可以直接这样封装HAL_UART_Transmit(huart2, (uint8_t*)cmd, strlen(cmd), 1000)第二个参数是数据指针第三个是长度第四个是超时毫秒数。注意 HAL 的HAL_UART_Transmit在 DMA 模式下不能和HAL_UART_Transmit_DMA混用我习惯统一用阻塞发送简单可靠。底层有了这两个原语上层写 AT 逻辑时就不需要关心寄存器细节了。3. 让模组“活”过来AT 握手、网络注册与信号查询3.1 上电后的模组自检与 AT 握手时序模组上电到可以响应 AT中间有 1 到 3 秒的固件启动时间视模组型号和固件版本不同。常见做法是 MCU 先给模组的电源使能脚拉高延时 1.5 秒后再开始发第一条AT。有些模组有 PWRKEY 引脚需要脉冲触发开机这类时序要先看模组手册确认是高电平开机还是低电平开机接反了模组永远不启动。握手阶段我的固定检查序列如下每一条都带超时任何一条失败就重试三次然后报错发: AT\r\n 等: \r\nOK\r\n 超时 500ms 发: ATE0\r\n 关闭回显避免自己发的指令混进响应里 等: \r\nOK\r\n 超时 500ms 发: ATCSQ\r\n 查信号 等: \r\nCSQ: 15,0\r\n 或 OK\r\n逻辑说明第一条AT只验证串口通不通如果这条都回 ERROR后面全部免谈。ATE0关闭回显非常关键默认回显开着时模组会把收到的指令原文再发回来解析程序容易把回显内容当成响应的一部分导致关键词匹配失败。ATCSQ返回两个数字第一个是信号强度范围 0 到 31越大越好99 表示检测不到信号第二个是误码率0 表示正常99 表示未知。握手阶段最容易翻车的是时序延时太短模组还没就绪第一条 AT 会石沉大海。我吃过这个亏后来在代码里加了一个逻辑如果发送 AT 后 500ms 内没等到 OK就再补发一次 AT 而不是直接报错连续三次失败才判定模组异常。这个重试逻辑救了很多次现场调试值得保留。3.2 解析 CREG/CGREG 与 CSQ别只看有没有网ATCREG?查询的是电路域注册状态ATCGREG?查询的是分组域注册状态对 4G 数据业务来说真正有意义的是后者但前者也得查因为很多模组在电路域没注册时分组域也起不来。返回值第二个数字含义如下注册状态值含义处理建议0未注册正在搜索延时 3 秒重查1已注册本地网络可以继续下一步2未注册正在搜索延时 3 秒重查3注册被拒绝检查 SIM 卡和 APN 配置5已注册漫游可以继续留意流量费用我一般把“查注册”做成一个轮询函数每 3 秒发一次ATCGREG?解析逗号分隔的第二个字段连续 10 次都没进 1 或 5 就判定注册失败给上层返回错误码。这里有个容易踩的坑有些模组在ATCGREG?的响应里会连续返回两行一行是查询结果CGREG: 0,1一行是状态变化主动上报的CGREG: 1解析时如果只做字符串匹配很容易把两行混在一起。所以我解析时严格按\r\n分割行只处理以CGREG:开头且包含逗号的那一行。信号强度CSQ和注册状态要配合看。我调试时遇到过CGREG: 1但CSQ: 99的情况数据死活发不出去后来发现是天线没接好模组虽然完成了注册但射频链路异常。实际经验是CSQ 小于 10 时即使注册成功TCP 连接也会频繁断开这种现场问题不是程序能解决的要检查天线和信号环境。3.3 等待期望响应的调度结构状态机替代盲目延时新手写 AT 程序最喜欢HAL_Delay(500)加HAL_UART_Receive死等这种写法在响应准时的时候挺好用一旦模组因为信号问题慢了几百毫秒整个流程就卡死。我的做法是封装一个“发指令、等待期望关键词、超时返回”的函数上层所有流程都基于这个函数搭int at_send_expect(const char *cmd, const char *expect, uint32_t timeout_ms) { char line[512]; uint32_t start HAL_GetTick(); if (HAL_UART_Transmit(huart2, (uint8_t*)cmd, strlen(cmd), 500) ! HAL_OK) return AT_ERR_TX; while (HAL_GetTick() - start timeout_ms) { if (at_read_line(line, sizeof(line), 200) 0) { printf(AT_RX: %s\r\n, line); if (strstr(line, expect) ! NULL) return AT_OK; if (strstr(line, ERROR) ! NULL) return AT_ERR_RESP; } } return AT_ERR_TIMEOUT; }逻辑说明这个函数把“发送”和“等待”解耦发送后进入一个带总超时的循环每次最多等 200ms 去读取一行数据行内容用at_read_line从环形缓冲区里按\r\n分割取出。匹配用strstr而不是strcmp因为 AT 响应经常带参数前缀比如CREG: 1里要匹配的只是子串。超时参数timeout_ms按指令类型设不同值查询类设置 500 到 1000msTCP 连接类设置 15000ms因为建连要等模组和服务器完成握手。at_read_line的实现思路是记录上次解析到的缓冲区位置从那里开始找\n找到就把这一行拷出来并更新位置没找到就返回 0。这个函数要注意环形缓冲区的回绕位置指针越过RX_BUF_SIZE时要取模。底层解析稳定后上层代码就变成了一串at_send_expect调用整个流程清晰可读调试时把每行响应打印出来哪里卡住一目了然。4. 干正事基于 HAL 的 TCP/HTTP 数据上报实现4.1 建立 TCP 连接AT 指令序列与超时设计网络注册成功后建 TCP 连接的标准序列一般是配置 APN、激活 PDN、发起连接三步。APN 由运营商和 SIM 卡套餐决定国内物联网卡通常是cmnet移动或ctnet电信这类公用 APN具体以卡商给的信息为准。激活 PDN 的指令因模组厂商而不同常见的是ATCGACT1,1这条会让模组真正去附着网络耗时可能两三秒超时一定要给足。/* 第一步配置 APNCID 通常为 1 */ at_send_expect(ATCGDCONT1,\IP\,\cmnet\\r\n, OK, 1000); /* 第二步激活 PDN等待时间给长一些 */ at_send_expect(ATCGACT1,1\r\n, OK, 5000); /* 第三步发起 TCP 连接IP 和端口替换成实际服务器 */ at_send_expect(ATCIPSTART\TCP\,\192.0.2.10\,8080\r\n, CONNECT OK, 15000);参数说明CGDCONT 的三个参数分别是上下文编号、PDP 类型、APN 名PDP 类型写IP表示 IPv4走 IPv6 的场景写IPV6但大多数物联网平台还在用 IPv4不要贪新。CIPSTART 的第一个参数是协议类型支持TCP和UDP第二个是服务器地址可以是 IP 也可以是域名第三个是端口。如果服务器侧用域名模组要先配 DNS常见做法是在 CGDCONT 后追加ATCIFSR或者用ATQIDNSGP指定 DNS 服务器现场 Blood 经验是能用 IP 尽量用 IP域名解析是额外故障点。这条序列里最容易被忽略的是翻车点TCP 建立失败时模组可能回CONNECT FAIL而不是ERROR。我在at_send_expect里只匹配了 OK 和 ERROR 两个关键词遇到 CONNECT FAIL 会一直等到超时白白浪费 15 秒。后来我把期望关键词扩展成“匹配 CONNECT OK 即成功匹配 CONNECT FAIL 或 ERROR 即失败”建连流程才真正可靠。CIPSTART 失败后不要直接重发先发ATCIPSHUT关闭所有连接等 1 秒再重新走建连流程否则部分模组会报STATE NOT SUPPORT。4.2 拼 HTTP POST 帧JSON 转义与长度计算TCP 连上之后直接发ATCIPSEND模组会回一个提示符此时模组进入透传数据模式你发的所有字节都会原样交给 TCP 连接直到发送结束标记才退出。结束标记各模组不同常见是发送单字节0x1A也有用ATCIPSENDlength指定长度的方式后者更可控推荐优先用定长模式。拼 HTTP 报文时我用snprintf把请求头和 JSON 体合在一起最容翻车的是 JSON 里字符串值包含引号直接拼进snprintf会让格式串错乱。正确做法是先把 JSON 体单独构造好再整体拼进请求char payload[256]; char http_req[512]; int payload_len, req_len; memset(payload, 0, sizeof(payload)); memset(http_req, 0, sizeof(http_req)); /* 先用 %.*s 拼 JSON避免内部双引号破坏格式串 */ snprintf(payload, sizeof(payload), {\dev\:\%s\,\temp\:%.1f,\vbat\:%.2f,\ts\:%lu}, dev_id, temp_c, vbat, time(NULL)); req_len snprintf(http_req, sizeof(http_req), POST /api/report HTTP/1.1\r\n Host: 192.0.2.10:8080\r\n Content-Type: application/json\r\n Content-Length: %d\r\n\r\n %s, (int)strlen(payload), payload); at_send_expect(ATCIPSEND%d\r\n, , 3000); /* 这里实际要自己拼指令 */ at_send_data(http_req, req_len); at_send_data(\r\n, 2); /* 部分模组要求以\r\n结尾结束透传 */逻辑说明Content-Length必须用strlen(payload)算出来的实际字节数不能随手填一个写死的长度服务器按这个长度读体多了少了都会导致报文不完整。发送分两步先发ATCIPSEND长度让模组进入发送模式等提示符出现后再发真实数据。真实数据里的换行符、引号都是合法内容不用转义因为 HTTP 报文本身就是纯文本但要注意snprintf的缓冲区大小payload和http_req的长度要算清楚宁大勿小。4.3 断线重连与独立看门狗让设备能自己活下去TCP 在弱信号环境下说断就断程序必须做好“断了能自己爬回来”的机制。我的做法分三层第一层是发送失败重试ATCIPSEND后模组回ERROR或SEND FAIL时同一包数据重发三次第二层是长连接保活每 30 秒发一个自定义心跳包服务端 90 秒内没收到就认为设备离线第三层是彻底断开后重来连续三次建连失败就发ATCIPSHUT关闭全部连接重新走 CGDCONT 到 CIPSTART 的完整流程。独立看门狗IWDG是最后防线。我见过模组固件卡死导致串口完全无响应的案例程序层面怎么重试都没用只能靠看门狗复位 MCUMCU 重启后重新初始化和唤醒模组。IWDG 的喂狗位置不能放在主循环开头要放在“刚成功收发过 AT 数据”的地方逻辑是程序还在正常收发就喂狗卡死在某个 AT 等待里就不喂让看门狗把系统拉回来。/* 主循环里的喂狗逻辑 */ while (1) { if (last_at_activity 0 (HAL_GetTick() - last_at_activity) 5000) { HAL_IWDG_Refresh(hiwdg); /* 最近5秒有AT活动才喂 */ } /* 业务上报、心跳等逻辑 */ vTaskDelay(50); }逻辑说明last_at_activity在每次at_send_expect返回成功时更新变量冲突要小心最好用独占读写的 volatile 声明。看门狗超时设 10 秒比最长 AT 等待时间 15 秒短一点点这样设计有个隐藏问题如果模组建连真的需要 15 秒看门狗会在中途复位。我的处理是把建连流程拆成多个 5 秒以内的等待段每段之间检查一下系统状态既保证不喂狗也能在合理时间内完成整套流程。5. STM32 HAL 4G 模块程序的避坑指南现象、原因与解法5.1 DMA 收不到完整响应只收到一半现象程序发 AT 后回显能收到但解析函数一直匹配不到OK调试打印发现每次只收到响应的一半比如AT\r\nO后面的K丢了或者响应随机被截断成两段。原因RX DMA 配置成了普通模式而不是循环模式收到第一段数据后 DMA 停止搬运后续数据全部丢失或者启用了 DMA 但没启用空闲中断收到数据后没有事件通知解析函数读不到新数据。解决把 DMA 模式改成 CIRCULAR并在接收函数上统一用HAL_UARTEx_ReceiveToIdle_DMA这个接口内置了空闲检测一帧数据结束就会触发一次回调。另外检查 NVIC 里 UART 全局中断和 DMA 通道中断是否都以最高抢占优先级打开两个都开、优先级一致避免中断嵌套导致回调丢失。改完后用串口抓包工具看数据是否完整连续到达。5.2 模组上电后 AT 无响应查了一圈是电源问题现象代码逻辑完全正确串口发送正常但模组对 AT 指令毫无反应测量模组 VCC 引脚电压发现正常换一块模组也一样。原因快去翻一下模组手册的功耗参数4G 模组在发射瞬间电流会冲到 2AUSB 供电或小功率稳压芯片根本扛不住电压瞬间跌落导致模组复位或死机。症状就是“能上电但完全不工作”用万用表测静态电压看不出来必须用示波器抓动态波形。解决给模组单独配 4V/2A 以上的 DC-DC 电源输入端并联 1000uF 电解电容加 100nF 陶瓷电容靠近模组电源引脚放置。模组的电源地和 MCU 数字地单点连接避免地环路。我踩过这个坑后用示波器实测没加电容时发射瞬间电压从 3.9V 掉到 3.2V加了 1000uF 后稳住 3.8V问题直接消失。5.3 回显与乱码混在响应里解析被带偏现象AT 指令能收到响应但at_send_expect时而成功时而失败多试几次发现响应行里混着ATE0\r\n这样的指令原文或者出现半个乱码字符。原因回显没有关或者模组波特率与 MCU 实际波特率有微小偏差长期运行后累积出乱码。回显默认打开时模组把收到的指令内容原样返回这本身不影响strstr匹配但如果有模组在回显后紧跟一个\r\n按行解析时会把指令行当成响应行处理流水线解析就错位了。解决代码里第一条指令就发ATE0关闭回显并且at_send_expect内部不依赖“某一固定行号”而是逐行扫描整段缓冲区。乱码问题用逻辑分析仪抓实际波特率波形用示波器量一个字符位的宽度计算真实波特率和 MCU 配置对比。我给设备量产时加了一道上电引导逻辑先按 115200 发三条 AT没回应就自动尝试 9600 和 38400匹配上模组实际波特率后再正式握手。5.4 休眠后模组唤不醒低功耗设计要从模组侧入手现象程序加了低功耗逻辑MCU 休眠后定时唤醒去查模组状态发现模组完全无响应唤醒后第一条 AT 永远不回要断电重启才恢复。原因模组也进入了省电模式此时串口 RX 处于关闭状态MCU 发 AT 进不去或者 MCU 休眠时把 UART 时钟关了恢复后HAL_UART_Init状态错乱。模组的“睡眠”不是像 STM32 那样一条指令就睡死它有自己的停止模式和唤醒方式。解决不要用“MCU 苏醒后直接发 AT”的思路正确做法是模组侧先判断当前状态上电后固定发一条ATCFUN0让模组进入飞行模式需要上报时再发ATCFUN1恢复正常射频这个切换过程要留 3 到 5 秒等待因为模组重新搜网需要时间。MCU 休眠前把 UART 接收引脚配置成外部中断唤醒模组的 URC 消息比如网络恢复通知接到这个引脚上模组有事件时主动唤醒 MCU而不是 MCU 定时去吵模组。6. 调优技巧与验证方法让程序从“能跑”到“可信赖”程序能跑通和可靠之间差的是一套验证手段。我调 4G 链路时必做三件事第一用 USB 转串口工具把 MCU 和模组之间的两条数据线引出监听到电脑上双向抓包MCU 发出的指令、模组回的响应一清二楚能不能匹配、卡在哪一步直接看得见。我把这个抓包数据存成文件出问题回放比对比看代码找错快得多。第二做一个 AT 层的回放测试从抓包文件里取一段真实响应序列按行灌进解析函数断言每一行应该被分到哪个流程这个测试工程跑一次就能暴露解析逻辑里的错误。第三在 MCU 侧加一个调试串口把at_send_expect每次收发的行都带时间戳打出去只看日志就能判断是发慢、收慢还是解析慢不用总是接仿真器。调优要留意的几个数字模组 TCP 收发超时、心跳间隔、重连次数上限这些都要做成宏定义而不是散落在代码里。我习惯在工程根目录放一个net_config.h统一维护 APN、服务器地址、端口、心跳周期、重试次数、各个超时时间现场调试改参数只动这个文件不用翻代码。还有一点是模组和 MCU 之间的波特率不要追求高115200 就够4G 模组的上行带宽有限串口波特率不是瓶颈54600 以下反而对布线要求低更不容易受干扰。我的一个习惯是每次出货前跑 72 小时掉电重启老化测试定时 30 分钟给设备断电重启一次每次重启后必须完整走完 AT 握手、注册、TCP 建连、上报四步流程任何一次失败都会被记录下来。这个测试抓出来过三次真正的玄学问题都是典型的初始化时序和看门狗配合问题。习惯养成了设备到现场之后需要维护的情况就少得多。4G 模组程序没有太多神秘可言把底层收发做稳、把超时设计做对、把复位恢复做全这三点守住数据就基本不会丢。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑