STM32+EC20 4G通信实战:UART初始化、AT指令与TCP/MQTT接入详解

STM32+EC20 4G通信实战:UART初始化、AT指令与TCP/MQTT接入详解 简介面向STM32与EC20 4G模块联调场景的嵌入式源码包适合物联网开发者、嵌入式入门者以及需要为单片机增加4G联网与GPS定位能力的工程师使用。资源仅含两个源码文件1个头文件、1个C源文件压缩包体积约8KB体量精简可快速融入现有工程无需复杂依赖。C源文件重点实现了基于UART串口的EC20驱动逻辑涵盖串口初始化、AT指令发送与超时重试、应答状态解析、TCP/UDP套接字收发以及GPS NMEA报文如GPGGA、GPRMC的字段提取帮助开发者跨越“硬件连上但协议不通”的常见门槛。资源包目前已获得3355人次的学习下载对于正在设计4G DTU、车载定位终端、环境监测节点或远程控制设备的开发者有直接参考价值可在其基础上进一步适配运营商入网参数、叠加MQTT/HTTP应用层协议。1. 为什么 STM32EC20 是物联网项目里最稳的通信组合调试过 STM32 的工程师大多有过这种经历Wi-Fi 模块在实验室里跑得欢一到现场就丢包蓝牙方案传个几 KB 数据还要考虑手机兼容性NB-IoT 便宜但带宽又卡在 100kbps 上下。真正要同时满足「高带宽、广覆盖、可移动」这三个条件时4G 几乎是唯一不用重复造轮子的选择。EC20 是移远推出的一款 LCC 封装的 4G 模块支持 LTE-TDD/FDD、UMTS、GSM国内三大运营商都能直接上号而且自带 GPS/GLONASS 定位能力。它和 STM32 之间只靠一组 UART 就能完成数据面与控制面的交互底层网络协议全部封装在模块内部MCU 只要处理 AT 指令和业务数据即可。这套方案适合做车载终端、远程数据采集器和移动巡检设备也适合那些没有现成有线网络、但要求业务逻辑可裁剪的嵌入式项目。接下来我们从硬件初始化开始逐步拆解 NetDevice.c 和 NetDevice.h 里最值得复用的那部分代码。2. STM32 UART 与 EC20 的接线与初始化NetDevice_Init 背后的寄存器细节2.1 引脚分配与电平匹配先解决物理层问题EC20 的 UART 接口支持 3.3V TTL 电平和 STM32F103 这类主流 MCU 的 GPIO 电平正好兼容不需要额外加电平转换芯片。但要注意 EC20 的串口引脚是复用功能有些模块型号的 UART 默认位于主串口Pin 15/16有些是调试串口采购回来第一件事就是查硬件手册确认引脚序号否则初始化代码写得再对模块也不会回应一个字节。推荐的接线方式如下表STM32 引脚EC20 引脚说明PA9 (USART1_TX)UART_RX (Pin 16)STM32 发送EC20 接收PA10 (USART1_RX)UART_TX (Pin 15)EC20 发送STM32 接收GNDGND必须共地PB12可选PWRKEY (Pin 3)用于冷启动开机拉低 1 秒后释放需要特别注意 PWRKEY 的处理。EC20 上电后不会立刻进入可接收 AT 指令的状态必须把 PWRKEY 拉低至少 500ms 再释放模块才能正常开机。在 NetDevice_Init 里第一步不是配置串口而应该先把这个开机时序跑完。static void EC20_PowerOn(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); gpio.GPIO_Pin GPIO_Pin_12; gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); GPIO_ResetBits(GPIOB, GPIO_Pin_12); // 拉低 PWRKEY DelayMs(1000); // 保持 1 秒 GPIO_SetBits(GPIOB, GPIO_Pin_12); // 释放模块开始启动 DelayMs(500); // 等待模块内部初始化完成 }这里用 GPIOB_Pin_12 模拟 PWRKEY 时序。EC20 的 PWRKEY 内部有上拉外部用开漏或推挽输出都可以但要保证低电平持续时间足够长。很多新手在这里只延迟 100ms结果模块刚开机还没来得及正常响应后续 AT 指令全部超时。2.2 NetDevice_Init 的核心USART 参数与中断接收模型USART 的配置要跟着 EC20 的默认串口参数走常见组合是 115200 bps、8 位数据位、无校验、1 位停止位。下面这段代码是 NetDevice_Init 里最典型的初始化片段既配置了 TX/RX 脚也打开了接收中断方便后续异步接收数据。void NetDevice_Init(uint32_t baudrate) { GPIO_InitTypeDef gpio; USART_InitTypeDef usart; NVIC_InitTypeDef nvic; RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); usart.USART_BaudRate baudrate; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; usart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, usart); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); nvic.NVIC_IRQChannel USART1_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); USART_Cmd(USART1, ENABLE); EC20_PowerOn(); NetDevice_SendCommand(ATE0\r\n); // 关闭回声便于后续解析 DelayMs(200); NetDevice_SendCommand(AT\r\n); // 握手 }这段代码把 USART1 配置成最基本的异步收发模式关闭硬件流控。EC20 默认不带 RTS/CTS 也可以用但如果传输层有大数据量下发建议把流控打开否则 STM32 的内存可能被连续的数据包冲垮。USART_IT_RXNE中断使能后每收到一个字节都会跳到中断服务函数我们在这个函数里把数据塞进环形缓冲区而不是立刻解析这样能把串口接收和业务逻辑解耦。2.3 接收缓冲区的设计为什么环形队列比数组变量更可靠在 NetDevice.h 里通常会看到类似uint8_t rxBuffer[512]的声明但这只是存储空间真正的逻辑边界要靠读写索引来维护。以下是一个可用的环形队列实现配合中断使用。#define RX_BUF_SIZE 1024 static uint8_t rx_buf[RX_BUF_SIZE]; static volatile uint16_t head 0; static volatile uint16_t tail 0; void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); uint16_t next_head (head 1) % RX_BUF_SIZE; if (next_head ! tail) { // 检查是否满 rx_buf[head] data; head next_head; } } } uint16_t NetDevice_RxAvailable(void) { return (head tail) ? (head - tail) : (RX_BUF_SIZE - tail head); }这个环形队列用head表示写入位置tail表示读取位置。中断每收一个字节就写一次主循环定期调用NetDevice_RxAvailable检查是否有数据可读。为什么不用简单的数组加计数器因为 AT 指令的响应可能分多条到达比如\r\nCGPSINFO: 3110.1234,N,12133.5678,E\r\nOK\r\n如果只开辟固定的uint8_t buffer[64]一旦网络信号不强导致响应分片就很容易造成拆包错位。环形队列把缓冲界限从「一条消息」放大到「连续字节流」后续解析函数可以直接按内容中的\r\n做切分不用守着整包到达。3. 用 AT 指令打通 4G 网络与 GPS 定位从 ATCGATT 到 NMEA 解析3.1 AT 指令发送与响应判断的完整封装EC20 的所有配置都走 AT 指令发送格式是ATcommandvalue\r\n。但实际工程中不是发完就完事必须等待模块返回OK或ERROR。NetDevice_SendCommand通常的职责就是发送指令并阻塞等待指定响应这里要注意超时设置——4G 网络注册在网络差的时候可能需要几十秒不能用一个固定的短超时。uint8_t NetDevice_SendCommand(const char* cmd, const char* expect, uint16_t timeout_ms) { uint16_t elapsed 0; uint16_t len strlen(cmd); // 清空接收缓冲区避免上次残留数据干扰判断 NetDevice_RxFlush(); // 逐字节发送 for (uint16_t i 0; i len; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)cmd[i]); } // 等待期望的响应字符串 while (elapsed timeout_ms) { if (NetDevice_RxFind((uint8_t*)expect) 0) { return 1; } DelayMs(1); elapsed; } return 0; }这里在发送前先NetDevice_RxFlush()把上一个指令残留的\r\nOK\r\n清干净保证解析函数看到的第一个字符就是本次响应的起始。NetDevice_RxFind的作用是在环形缓冲区里查找一个子串是否出现这个函数直接按字节流扫描不用等所有数据都到齐所以网络慢的时候也不会错过响应。模块返回的响应可能是OK、ERROR、CME ERROR: 50这类格式expect参数可以传OK或者ERROR调用者再根据返回值决定重试策略。3.2 注册网络与开启 GPS必须按顺序执行的指令序列让 EC20 连上 4G 网络不是只发一个ATCGATT1就完了。标准流程是先查 SIM 卡状态、再配置 APN、最后激活网络。以下是实际项目里常用的指令序列步骤AT 指令期望响应作用1ATCPIN?CPIN: READY检查 SIM 卡是否就绪2ATCSQCSQ: 20,99查询信号强度第一个数越大越好3ATCGDCONT1,IP,cmnetOK设置 APN移动卡常用 cmnet4ATCGACT1,1OK激活 PDP 上下文5ATCGATT1OK附着到 4G 网络6ATCGPS1OK开启 GPS 功能APN 一定要根据运营商卡片的参数填写电信卡一般是ctnet联通是3gnet如果填错后面建立 Socket 会一直报连接失败。ATCGACT1,1里第一个 1 表示 PDP 上下文 ID第二个 1 表示激活。有些项目里会把ATCGATT1和ATCGACT1,1顺序调换实际测试下来先激活后再附着成功率更高因为部分运营商会把附着流程挂在 PDP 激活之后。3.3 解析 GPS NMEA 数据GPGGA 与 GPRMC 的取舍EC20 开启 GPS 后会不断通过串口上报$GPGGA、$GPRMC等 NMEA 语句。但注意默认情况下 GPS 数据流是和 AT 响应走同一个串口的所以必须用ATCGPSINFO主动查询或者配置ATQGPS1让模块只播报GPRMC。为了减少主控解析负担推荐用ATCGPSINFO按需查询返回的数据格式类似CGPSINFO: 3110.1234,N,12133.5678,E,101220,123456.0,50.0,0.0,0这条数据里分别包含纬度、北纬标识、经度、东经标识、日期、UTC 时间、地面速度、航向角等。解析时不能直接切逗号了事因为纬度是「度分」格式31 度 10.1234 分要转换成小数。下面是 NetDevice.c 里ParseGPSData的常见写法void ParseGPSData(const char *info, float *lat, float *lon) { char tmp[16]; const char *ptr info; // 跳过前缀 CGPSINFO: ptr strstr(info, : ); if (ptr ! NULL) { ptr 2; } else { ptr info; } // 第一个字段是 lat 原始值形如 3110.1234 for (int i 0; i 16 *ptr ! ,; i, ptr) { tmp[i] *ptr; if (*ptr , || *ptr \0) break; } tmp[15] \0; // 度分转换前两位是度其余是分 float raw atof(tmp); int deg (int)(raw / 100.0); float min raw - deg * 100.0; *lat deg min / 60.0; // 再跳过 N/S 和逗号读取经度 ptr strchr(ptr, ,) 1; // 跳过 N/S }这段代码先定位到数据部分然后按逗号移动到纬数字段再用atof转换。计算时把「度分」拆开3110.1234 表示 31 度 10.1234 分所以度是(int)(raw/100.0)剩余的小数部分是分除以 60 后加到度上。要注意经纬度都存在北纬/东经默认正值、南纬/西经为负值实际项目里要根据N/S和E/W标识加上符号位。4. 基于 EC20 的 TCP/UDP 数据通道Socket 创建与数据重传处理4.1 从 ATQIOPEN 看 EC20 的 Socket API 与常见错误码EC20 的 TCP/IP 协议栈是模块内部的对外提供 AT 指令接口。核心指令是ATQIOPENcontextID,connectID,serverIP,serverPort,socketType,accessMode其中socketType用 0 表示 TCP、1 表示 UDPaccessMode用 0 表示直接推送、1 表示缓冲读取。很多初学者直接发这条命令会收到ERROR原因往往是没有先设置ATQIMODE1或者没有指定本地端口。更隐蔽的是当模块已经处于数据通道状态时不能再发普通 AT 指令必须先发送退出数据模式这个过程需要避开和 TCP 流中冲突。创建 Socket 的代码封装通常长这样uint8_t NetDevice_CreateSocket(const char *host, uint16_t port, uint8_t is_tcp) { char cmd[128]; snprintf(cmd, sizeof(cmd), ATQIOPEN1,0,\%s\,%d,%s,0\r\n, host, port, is_tcp ? TCP : UDP); if (!NetDevice_SendCommand(cmd, OK, 5000)) { return 0; } // 等待 CONNECT OK部分固件返回 CONNECT OK return NetDevice_SendCommand(ATQIOPEN?, CONNECT OK, 3000); }这里用了ATQIOPEN1,0,...第一个 1 是上下文 ID必须和之前ATCGDCONT里设置的一致第二个 0 是连接 ID一个模块最多支持多个连接我们这里只用 0 号。NetDevice_SendCommand在收到 OK 后不意味着已经连接成功需要继续查询ATQIOPEN?获取状态只有当状态变成CONNECT OK才能开始发数。如果失败常见错误码如表所示错误码含义处理建议CME ERROR: 50PDP 未激活或 APN 错误检查ATCGACT?的状态CME ERROR: 601DNS 解析失败确认网络可通或改用 IP 地址CME ERROR: 605服务器拒绝连接检查端口是否开放防火墙规则4.2 发送数据时的缓冲与重传机制数据发送不能简单地把ATQISENDlength和裸数据拼接在一起发。AT 指令模式是文本协议数据一旦包含回车换行或十六进制不可见字符就会和指令解析冲突。常见的做法是开启十六进制模式ATQIMODE1后用ATQISEND0length声明长度再发送指定长度的原始字节最后等待模块返回SEND OK。uint8_t NetDevice_TransmitData(uint8_t *data, uint16_t len) { char cmd[32]; // 切换到数据发送状态 snprintf(cmd, sizeof(cmd), ATQISEND0,%d\r\n, len); if (!NetDevice_SendCommand(cmd, , 2000)) { return 0; } // 直接写入二进制数据 for (uint16_t i 0; i len; i) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, data[i]); } // 等待模块上报发送结果 return NetDevice_SendCommand(, SEND OK, 5000); }这里第一次SendCommand是为了等模块返回提示符表示模块已经准备好接收数据。发送完原始数据后模块会异步上报SEND OK或者SEND FAIL。如果收到SEND FAIL常见原因是对端窗口满了或者网络拥塞不能立即重发应当退避一段时间比如 1 秒再重新发送。很多项目直接无条件重发结果数据堆积导致模块缓冲区溢出反而加重拥塞。4.3 连接 MQTT 的实战配置从 TCP 到应用层协议的跨越虽然 EC20 原生支持 TCP/UDP但物联网平台大多走 MQTT 协议。移远在 EC20 上提供了ATQMTCFG和ATQMTOPEN等 MQTT 指令集使用起来比裸 TCP 更省心因为协议栈里已经处理了 QoS 和心跳。配置 MQTT 连接阿里云或 AWS 的常见指令步骤如下ATQMTCFGaliauth,0,productKey,deviceName,deviceSecret—— 配置阿里云三元组如果使用 HiveMQ 等通用 Broker则配置user,passwd。ATQMTOPEN0,hostname,1883—— 打开一个 MQTT 连接hostname 可以是 IP 或者域名。ATQMTCONN0,clientId—— 发起连接请求clientId 必须全局唯一。ATQMTSUB0,1,topic,0—— 订阅主题1 是消息标识符0 是 QoS。ATQMTPUB0,0,0,0,topic,len后面跟上消息内容 —— 发布消息。需要注意MQTT 指令返回的QMTCONN: 0,0,0中第三个 0 表示连接成功如果是 1 表示协议错误2 表示网络错误。调试时优先检查前两条指令的返回因为QMTOPEN和QMTSUB失败大多与网络不通或主题权限有关。5. 实战中必须处理的六个边界问题这部分内容是做完整机测试时最容易翻车的地方每一项都能让设备在现场返修。5.1 断线重连与看门狗配合EC20 的 TCP 连接在信号弱时可能静默断开底层不会主动通知应用。检测方法是在业务空闲时定期发送ATQPING1,8.8.8.8做网络探测连续三次失败就重启整个数据链路。同时配合 STM32 的独立看门狗主循环里只有成功收发过数据才喂狗否则系统复位后重新初始化模块。这样能避免 LTE 协议栈死锁导致的无响应。5.2 GPS 冷启动的等待时间GPS 首次冷启动可能需要 30 到 120 秒期间ATCGPSINFO会返回空值。不要一开机就频繁查询建议用一个状态机开启 GPS 后每隔 10 秒查一次直到返回有效 NMEA 时间戳。如果超过 120 秒仍无数据检查天线是否放在窗外或者车辆前挡风玻璃处陶瓷天线离金属平面太近会直接瘫痪。5.3 串口缓冲区的溢出判断环形队列虽然比固定数组强但仍有溢出风险。当NetDevice_RxAvailable返回的值等于缓冲区容量时意味着数据正在持续丢失。这时候要看是不是主循环解析太慢比如在 GPS 解析里用了strstr和atof这些都是耗时函数。更快的做法是把解析工作放到低优先级任务或者增大缓冲区到 2048 字节。如果确认是 AT 响应风暴导致可以用ATQURCCFG关闭模块的主动上报改成查询模式。5.4 模块开机时序与电源跌落EC20 在 LTE 发射时峰值电流可达 2A如果 STM32 和模块共用同一个 3.3V LDO瞬间电压跌落会让模块掉线甚至损坏。工程上要求模块供电独立至少给 EC32 的 VBAT 接一个 1000μF 电解电容并联 100nF 陶瓷电容电源输入走宽一点的铜皮。测试时将万用表探头放在模块供电引脚附近观察「开机注册网络」和「发送大包」这两个时刻的电压波形纹波超过 300mV 就要改电源。5.5 时间同步与 GPS 时间戳的利用EC20 的 RTC 在掉电后会失效物联网设备如果没有 RTC 芯片可以使用ATQLTS获取基站时间。这个指令返回格式是QLTS: 24/01/18,15:30:2132注意时区字段是 GMT8 的偏移。用 GPS 时间做校准时UTC 时间可以直接转为 Unix 时间戳但要注意 GPS 周数翻转问题——ECE 2Q 2024 之后某些老固件的 GPS 周数会回卷最好在代码里判断年份大于 2038 时加 1024 周。5.6 串口波特率自适应最后一个小技巧EC20 上电后默认波特率 115200但有些出厂固件是 9600。为了避免生产环节麻烦建议初始化时先以 115200 发送AT如果没响应自动切换到 9600、57600 再试。代码里可以预置一个波特率数组用USART_Init重新初始化串口。这个做法在现场更换不同批次模块时非常实用比打开 shell 手工改波特率省事得多。本文还有配套的精品资源点击获取