STM32+EC20 4G通信实战:UART初始化、AT指令与TCP/MQTT接入详解
2026/9/13 13:11:28 网站建设 项目流程

简介:面向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. 为什么 STM32+EC20 是物联网项目里最稳的通信组合

调试过 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\n+CGPSINFO: 3110.1234,N,12133.5678,E\r\nOK\r\n,如果只开辟固定的uint8_t buffer[64],一旦网络信号不强导致响应分片,就很容易造成拆包错位。环形队列把缓冲界限从「一条消息」放大到「连续字节流」,后续解析函数可以直接按内容中的\r\n做切分,不用守着整包到达。

3. 用 AT 指令打通 4G 网络与 GPS 定位:从 AT+CGATT 到 NMEA 解析

3.1 AT 指令发送与响应判断的完整封装

EC20 的所有配置都走 AT 指令,发送格式是AT+<command>=<value>\r\n。但实际工程中不是发完就完事,必须等待模块返回OKERRORNetDevice_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的作用是在环形缓冲区里查找一个子串是否出现,这个函数直接按字节流扫描,不用等所有数据都到齐,所以网络慢的时候也不会错过响应。模块返回的响应可能是OKERROR+CME ERROR: 50这类格式,expect参数可以传"OK"或者"ERROR",调用者再根据返回值决定重试策略。

3.2 注册网络与开启 GPS:必须按顺序执行的指令序列

让 EC20 连上 4G 网络,不是只发一个AT+CGATT=1就完了。标准流程是先查 SIM 卡状态、再配置 APN、最后激活网络。以下是实际项目里常用的指令序列:

步骤AT 指令期望响应作用
1AT+CPIN?+CPIN: READY检查 SIM 卡是否就绪
2AT+CSQ+CSQ: 20,99查询信号强度,第一个数越大越好
3AT+CGDCONT=1,"IP","cmnet"OK设置 APN,移动卡常用 cmnet
4AT+CGACT=1,1OK激活 PDP 上下文
5AT+CGATT=1OK附着到 4G 网络
6AT+CGPS=1OK开启 GPS 功能

APN 一定要根据运营商卡片的参数填写,电信卡一般是ctnet,联通是3gnet,如果填错,后面建立 Socket 会一直报连接失败。AT+CGACT=1,1里第一个 1 表示 PDP 上下文 ID,第二个 1 表示激活。有些项目里会把AT+CGATT=1AT+CGACT=1,1顺序调换,实际测试下来先激活后再附着成功率更高,因为部分运营商会把附着流程挂在 PDP 激活之后。

3.3 解析 GPS NMEA 数据:GPGGA 与 GPRMC 的取舍

EC20 开启 GPS 后,会不断通过串口上报$GPGGA$GPRMC等 NMEA 语句。但注意,默认情况下 GPS 数据流是和 AT 响应走同一个串口的,所以必须用AT+CGPSINFO主动查询,或者配置AT+QGPS=1让模块只播报GPRMC。为了减少主控解析负担,推荐用AT+CGPSINFO按需查询,返回的数据格式类似:

+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/SE/W标识加上符号位。

4. 基于 EC20 的 TCP/UDP 数据通道:Socket 创建与数据重传处理

4.1 从 AT+QIOPEN 看 EC20 的 Socket API 与常见错误码

EC20 的 TCP/IP 协议栈是模块内部的,对外提供 AT 指令接口。核心指令是AT+QIOPEN=<contextID>,<connectID>,<serverIP>,<serverPort>,<socketType>,<accessMode>,其中socketType用 0 表示 TCP、1 表示 UDP,accessMode用 0 表示直接推送、1 表示缓冲读取。很多初学者直接发这条命令会收到ERROR,原因往往是没有先设置AT+QIMODE=1或者没有指定本地端口。更隐蔽的是,当模块已经处于数据通道状态时,不能再发普通 AT 指令,必须先发送+++退出数据模式,这个过程需要避开和 TCP 流中冲突。

创建 Socket 的代码封装通常长这样:

uint8_t NetDevice_CreateSocket(const char *host, uint16_t port, uint8_t is_tcp) { char cmd[128]; snprintf(cmd, sizeof(cmd), "AT+QIOPEN=1,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("AT+QIOPEN?", "CONNECT OK", 3000); }

这里用了AT+QIOPEN=1,0,...,第一个 1 是上下文 ID,必须和之前AT+CGDCONT里设置的一致;第二个 0 是连接 ID,一个模块最多支持多个连接,我们这里只用 0 号。NetDevice_SendCommand在收到 OK 后不意味着已经连接成功,需要继续查询AT+QIOPEN?获取状态,只有当状态变成CONNECT OK才能开始发数。如果失败,常见错误码如表所示:

错误码含义处理建议
+CME ERROR: 50PDP 未激活或 APN 错误检查AT+CGACT?的状态
+CME ERROR: 601DNS 解析失败确认网络可通或改用 IP 地址
+CME ERROR: 605服务器拒绝连接检查端口是否开放,防火墙规则

4.2 发送数据时的缓冲与重传机制

数据发送不能简单地把AT+QISEND=<length>和裸数据拼接在一起发。AT 指令模式是文本协议,数据一旦包含回车换行或十六进制不可见字符,就会和指令解析冲突。常见的做法是开启十六进制模式AT+QIMODE=1后,用AT+QISEND=0=<length>声明长度,再发送指定长度的原始字节,最后等待模块返回SEND OK

uint8_t NetDevice_TransmitData(uint8_t *data, uint16_t len) { char cmd[32]; // 切换到数据发送状态 snprintf(cmd, sizeof(cmd), "AT+QISEND=0,%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 上提供了AT+QMTCFGAT+QMTOPEN等 MQTT 指令集,使用起来比裸 TCP 更省心,因为协议栈里已经处理了 QoS 和心跳。

配置 MQTT 连接阿里云或 AWS 的常见指令步骤如下:

  1. AT+QMTCFG="aliauth",0,"productKey","deviceName","deviceSecret"—— 配置阿里云三元组,如果使用 HiveMQ 等通用 Broker,则配置"user","passwd"
  2. AT+QMTOPEN=0,"hostname",1883—— 打开一个 MQTT 连接,hostname 可以是 IP 或者域名。
  3. AT+QMTCONN=0,"clientId"—— 发起连接请求,clientId 必须全局唯一。
  4. AT+QMTSUB=0,1,"topic",0—— 订阅主题,1 是消息标识符,0 是 QoS。
  5. AT+QMTPUB=0,0,0,0,"topic",<len>后面跟上消息内容 —— 发布消息。

需要注意,MQTT 指令返回的+QMTCONN: 0,0,0中,第三个 0 表示连接成功,如果是 1 表示协议错误,2 表示网络错误。调试时优先检查前两条指令的返回,因为QMTOPENQMTSUB失败大多与网络不通或主题权限有关。

5. 实战中必须处理的六个边界问题

这部分内容是做完整机测试时最容易翻车的地方,每一项都能让设备在现场返修。

5.1 断线重连与看门狗配合

EC20 的 TCP 连接在信号弱时可能静默断开,底层不会主动通知应用。检测方法是在业务空闲时定期发送AT+QPING=1,"8.8.8.8"做网络探测,连续三次失败就重启整个数据链路。同时配合 STM32 的独立看门狗,主循环里只有成功收发过数据才喂狗,否则系统复位后重新初始化模块。这样能避免 LTE 协议栈死锁导致的无响应。

5.2 GPS 冷启动的等待时间

GPS 首次冷启动可能需要 30 到 120 秒,期间AT+CGPSINFO会返回空值。不要一开机就频繁查询,建议用一个状态机:开启 GPS 后每隔 10 秒查一次,直到返回有效 NMEA 时间戳。如果超过 120 秒仍无数据,检查天线是否放在窗外或者车辆前挡风玻璃处,陶瓷天线离金属平面太近会直接瘫痪。

5.3 串口缓冲区的溢出判断

环形队列虽然比固定数组强,但仍有溢出风险。当NetDevice_RxAvailable返回的值等于缓冲区容量时,意味着数据正在持续丢失。这时候要看是不是主循环解析太慢,比如在 GPS 解析里用了strstratof,这些都是耗时函数。更快的做法是把解析工作放到低优先级任务,或者增大缓冲区到 2048 字节。如果确认是 AT 响应风暴导致,可以用AT+QURCCFG关闭模块的主动上报,改成查询模式。

5.4 模块开机时序与电源跌落

EC20 在 LTE 发射时峰值电流可达 2A,如果 STM32 和模块共用同一个 3.3V LDO,瞬间电压跌落会让模块掉线甚至损坏。工程上要求模块供电独立,至少给 EC32 的 VBAT 接一个 1000μF 电解电容并联 100nF 陶瓷电容,电源输入走宽一点的铜皮。测试时将万用表探头放在模块供电引脚附近,观察「开机注册网络」和「发送大包」这两个时刻的电压波形,纹波超过 300mV 就要改电源。

5.5 时间同步与 GPS 时间戳的利用

EC20 的 RTC 在掉电后会失效,物联网设备如果没有 RTC 芯片,可以使用AT+QLTS获取基站时间。这个指令返回格式是+QLTS: "24/01/18,15:30:21+32",注意时区字段是 GMT+8 的偏移。用 GPS 时间做校准时,UTC 时间可以直接转为 Unix 时间戳,但要注意 GPS 周数翻转问题——ECE 2Q 2024 之后,某些老固件的 GPS 周数会回卷,最好在代码里判断年份大于 2038 时加 1024 周。

5.6 串口波特率自适应

最后一个小技巧:EC20 上电后默认波特率 115200,但有些出厂固件是 9600。为了避免生产环节麻烦,建议初始化时先以 115200 发送AT,如果没响应,自动切换到 9600、57600 再试。代码里可以预置一个波特率数组,用USART_Init重新初始化串口。这个做法在现场更换不同批次模块时非常实用,比打开 shell 手工改波特率省事得多。

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

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

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

立即咨询