
这阵子折腾了好几个晚上终于把一块吃灰很久的4G模块和ESP32凑到了一块儿实现了真正的蜂窝网络上网。整个过程并不复杂但中间踩的坑确实不少尤其是PPP拨号这一块的细节网上资料很零散。这篇学习笔记就把我自己的操作过程、关键原理和踩坑记录完整地整理出来给同样想玩“ESP32 4G模块”的朋友一个参考。先说清楚这玩意儿能干什么ESP32本身只带Wi-Fi和蓝牙离开路由器就没法上网。接上4G模块之后ESP32就能像手机一样直接通过SIM卡连接蜂窝网络实现真正的“无网线、无路由器”的联网能力。特别适合野外数据采集、移动车载设备、无人机地面站、临时布点的IoT传感器这些场景。如果你是刚接触ESP32的开发者想给设备加一个“永远在线”的联网通道或者你手头正好有一块闲置的4G模组想让它跑起来——这篇笔记都适合你。我会从方案选型讲到硬件接线再到ESP-IDF里的代码配置和实际拨号过程最后把最容易出问题的几个坑单独拿出来分析。整个过程中涉及到的工具、命令和代码片段都是我自己实际跑通过的可以直接参考。1. 这个项目解决什么问题为什么ESP32非要接4G模块1.1 场景拆解哪些地方Wi-Fi真的不可靠先说实际需求。我最初想做的是一个户外环境监测节点设备丢在农田里周围几百米都没有路由器也没人愿意给它专门拉一条宽带。如果只用ESP32自带的Wi-Fi这个设备就是一个废铁。接上4G模块后只要SIM卡有信号设备就能把温湿度、土壤数据传到云平台。还有一个非常典型的场景是车载设备。车在高速上跑Wi-Fi热点切换非常痛苦有时候压根没有信号覆盖。用4G模块拨号上网网络跟着SIM卡走信号跟着基站走完全不受周围Wi-Fi环境影响。还有一类场景是临时展会、户外直播、应急通信这类需要快速布网的场合。你可能今天在A现场明天在B现场拉网线、调试路由器费时费力。一块4G模块一张流量卡通电就能联网运维成本低很多。1.2 方案对比AT指令TCP/IP、串口透传、PPP拨号怎么选刚接触这个项目的时候我最先想到的是用AT指令让模块直接建TCP连接。也就是通过串口向4G模块发送类似ATQIOPEN这样的命令让模块自己去连接远程服务器ESP32只负责收发数据。这种方式实现起来最简单很多同学的第一版代码都是这么写的。但仔细一想问题就来了如果云平台用的是MQTT协议AT指令模式还能对付可一旦涉及HTTP/HTTPS、WebSocket或者需要跑自定义TCP协议AT指令会让你在“模块协议栈”和“MCU协议栈”之间来回切代码越写越丑。而且很多模块的AT指令缓冲区和并发连接能力有限不适合做复杂通信。另一种方案是串口透传模式模块固件把串口数据直接转换成网络数据。这个方案在部分4G模块上支持但它把网络栈完全封装在模块内部ESP32拿不到完整的网络状态和IP层信息没法做精细控制。对于想学习网络协议栈的同学来说这种方案也不太友好。最后我选了PPP拨号方案。PPP全称Point-to-Point Protocol是一种数据链路层协议专门用来在两个网络节点之间建立点对点连接。拨号时ESP32把4G模块当作一只“外置调制解调器”和早年电脑通过电话线上网是同一个思路通过AT指令发起拨号然后整个串口通道切换为PPP数据通道ESP32的lwIP协议栈直接跑在这个PPP链路上。这样ESP32拿到的就是一个正常的IP网络接口TCP/UDP/HTTP/MQTT全部照常使用。虽然前期配置比AT指令麻烦一点但一旦跑通后续开发体验会顺很多。1.3 为什么“随身WiFi”本质上也在走同一条路可能有人会好奇市面上那些插SIM卡的随身WiFi是怎么实现的说实话很多随身WiFi棒子内部的结构就是一颗通信芯片加一个路由芯片SIM卡的数据连接最终通过PPP或者类似的NDIS方式走网络协议栈再通过Wi-Fi分享出去。也就是说我这次做的“ESP32 4G模块”本质上就是精简版的随身WiFi——只不过我不需要分享Wi-Fi直接让芯片自己用网络。理解了这一层再去看PPP拨号就不会觉得它陌生了。它真的不是古董技术今天的蜂窝网络数据通道依然在大量使用类似机制。只是模组厂商把很多步骤封装在了内部开发者只看到“模块自己联网了”反而忽略了底层的拨号本质。顺着这个思路我决定不依赖模块的AT指令建连能力自己上手走一遍PPP把这层窗户纸捅破。2. 硬件准备与连线模块选型、引脚分配、供电细节2.1 4G模块选型与SIM卡准备市面上常见的4G模块品牌有移远、广和通、中兴微等我手头这块是移远的EC200U系列支持LTE Cat 1速率不算高但胜在功耗低、价格便宜非常适合物联网场景。你如果用的是SIM7600、SIM800等模块只要支持PPP拨号思路完全一样。选模块时有几个点要注意确认模块支持串口AT指令和PPP协议。绝大部分4G模块都支持但个别只支持USB的模组需要额外处理建议先看数据手册。尽量选择有完整开源资料和示例代码的型号调试时会省很多力气。模块的供电要求通常比较苛刻EC200U在发射瞬间电流可能到2A普通稳压芯片容易直接拉垮。这一点我下面专门讲。SIM卡方面我用的是一张物联网卡注意确认卡的APN接入点名称一般物联网卡都有专门的APN比如天翼物联的ctnb、移动的cmiot等。如果APN填错PPP可以拨通但拿不到正确的IP无法上网。这个细节很容易被忽略。2.2 ESP32与模块的硬件连接我用的是UART方案PPP over Serial的意思就是通过串口跑PPP协议所以把4G模块的串口和ESP32的串口连起来就行。我选了经典款ESP32 DevKit使用UART2作为数据通道引脚对应关系如下ESP32引脚4G模块引脚说明GPIO17TXD模块侧ESP32发送接模块接收GPIO16RXD模块侧ESP32接收接模块发送GNDGND必须共地GPIO4PWRKEY可选用于开机脉冲控制需要注意串口是交叉连接的ESP32的TXD接模块的RXDESP32的RXD接模块的TXD。很多新手第一次就栽在这里TX接TX浪费了半天。模块的PWRKEY引脚是一个开机电平触发脚EC200U需要在PWRKEY上拉一个几百毫秒的低电平脉冲才能开机。如果用PC调试可以直接用USB转串口工具配合串口助手发AT指令验证。但在ESP32项目里我更建议直接用ESP32的GPIO控制PWRKEY这样每次开机都能自动完成模块上电复位流程。2.3 供电与电平匹配最容易翻车的环节4G模块是“电流大户”这一点真的不要低估。EC200U在LTE状态下平均电流300mA左右峰值可以到2A。如果你想用ESP32开发板自带的3.3V稳压输出给模块供电几乎必挂——压降一出来模块要么反复重启要么拨号中途电流不够直接掉线。我踩坑之后换成了单独的DC-DC降压板输入5V也可以用锂电池或12V电源输出4V给4G模块供电实测稳定很多。模块数据手册的供电范围一般是3.4V到4.3V最佳工作电压在4V附近。供电电路上还建议并一个大容值的电解电容比如470uF或1000uF以及一个100nF高频去耦电容用来扛住瞬时大电流。电平匹配方面EC200U的逻辑电平是1.8V而ESP32的UART电平是3.3V。直接连上去会存在电平不匹配的问题。稳妥做法是用两路电平转换电路或者在模块数据手册确认是否可以直接兼容3.3V输入。部分模块的引脚标称可以容忍3.3V但为了保险起见最好不要直接接加一个TXB0104或者简单的分压电路更放心。我这一版直接在模块的TX脚串联了一个1K电阻到ESP32的RX脚实测也能工作但如果你手头有理电芯片还是按标准电路来做。2.4 别忘了串口电平选择如果你用的是模块的USB接口而不是串口那事情会更麻烦一点。ESP32经典款不带USB Host功能跑不了USB协议栈所以学习阶段强烈建议走UART通道。选择模块上有标注“UART”或者“MAIN UART”的引脚不要接在debug串口上调试口的AT指令功能有限。3. 软件侧准备ESP-IDF的PPP支持怎么打开3.1 为什么选择ESP-IDF而不是Arduino网上很多ESP32的教程都是基于Arduino框架的配置简单适合快速上手。但PPP拨号这种东西Arduino的支持并不完善——最关键的是它默认网络协议栈不一定会把PPP接口暴露出来很多情况下你需要自己折腾很底层的参数费力不讨好。所以我这次乖乖用了乐鑫官方的ESP-IDF开发框架版本是v5.x。ESP-IDF内置了完整的esp_netif网络接口抽象层PPP Support是官方直接提供的功能。你只需要在menuconfig里打开开关然后在代码里创建一个PPP网络接口整个连接逻辑就清晰了。3.2 menuconfig关键配置在项目目录下执行idf.py menuconfig需要检查以下几个配置项Component config → ESP-NETIF Adapter → Support PPPoS (Point to Point Protocol over Serial)。这个是总开关必须打开。Component config → LWIP → Enable IPV6。如果你的场景不需要IPv6可以关掉省一点内存。Component config → ESP-NETIF Adapter → Enable validation callback for PPP。建议打开它会在PPP状态变化时给你回调通知方便调试。串口相关的驱动配置默认就行波特率我设的是1152004G模块和ESP32之间的UART波特率不要拉到太高否则在长线干扰下容易误码。3.3 核心代码结构PPP网络接口初始化配置好之后代码逻辑其实非常清晰。主要步骤是创建一个esp_netif对象绑定PPP协议配置UART参数然后启动一个后台任务持续处理串口数据。我简化一下核心逻辑#include esp_netif.h #include esp_netif_ppp.h static esp_netif_t *ppp_netif NULL; static void ppp_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) { if (event_base IP_EVENT event_id IP_EVENT_PPP_GOT_IP) { esp_netif_dns_info_t dns; ip_event_got_ip_t *event (ip_event_got_ip_t *)event_data; ESP_LOGI(TAG, Got IP: IPSTR, IP2STR(event-ip_info.ip)); esp_netif_get_dns_info(ppp_netif, ESP_NETIF_DNS_MAIN, dns); ESP_LOGI(TAG, DNS: IPSTR, IP2STR(dns.ip)); xSemaphoreGive(net_connected_sem); } else if (event_base IP_EVENT event_id IP_EVENT_PPP_LOST_IP) { ESP_LOGW(TAG, PPP disconnected); } } void ppp_init(void) { esp_netif_config_t ppp_netif_config ESP_NETIF_DEFAULT_PPP(); ppp_netif esp_netif_new(ppp_netif_config); assert(ppp_netif); esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, ppp_event_handler, NULL); }实际的UART收发逻辑我放在另一个任务里用的是ESP-IDF的UART驱动。串口收到的所有数据直接uart_read_bytes读出来然后交给esp_netif_receive送给PPP协议栈反过来协议栈要发数据的时候通过注册的回调ppp_output交给UART驱动发送。这一层数据通路打通了剩下的就交给lwIP了。4. 实操过程从串口AT调试到顺利拨号联网4.1 第一步先用AT指令确认模块活着硬件连好之后我先把ESP32程序里UART初始化的部分单独跑起来写成一段只负责回显AT指令的调试代码。用电脑的串口助手直接观察模块是否正常启动避免一上来就跑复杂逻辑出问题都不知道是哪一环。典型的AT检查流程AT # 模块基本响应正常返回OK ATE0 # 关闭回显避免后续PPP数据受到干扰 ATCPIN? # 检查SIM卡是否识别返回READY表示正常 ATCSQ # 查询信号强度返回数字越大越好比如CSQ: 18 ATCREG? # 查询网络注册状态返回0,1或0,5表示已注册我第一次测试的时候在ATCPIN?这一步卡了很久模块一直返回CME ERROR: SIM not inserted。检查之后发现是SIM卡槽接触不良重新插拔一下才好。如果插卡正确但依然报错可以检查一下SIM卡座的焊点和卡的方向有些卡槽需要把卡往里推到位才能锁住。4.2 第二步设置APN和拨号前的最后准备确认模块已经注册上网络之后接下来要在AT指令模式下把APN设置好。APN就是网关接入点每个运营商都不一样具体可以问SIM卡供应商。我这张卡用的APN是ctnb对应的AT指令是ATCGDCONT1,IP,ctnb设置成功之后可以再用ATCGDCONT?查询一遍确认参数已经生效。这里有一个很容易忽略的点如果在AT模式下提前开启了额外的数据连接或者模块内缓存了旧的拨号参数PPP拨号可能会异常。我的习惯是在拨号前执行一遍ATCFUN1把射频模块完整打开然后再设置APN。下一步准备进入拨号流程。注意一旦执行拨号指令串口就不能再收发AT指令了串口完全被PPP协议占用。如果拨号失败想退回AT模式通常需要拉低复位引脚重新上电或者发送“”字符序列切换回命令模式。4.3 第三步编译烧录日志里看到Got IP代码写好后执行idf.py set-target esp32 idf.py build idf.py -p /dev/ttyUSB0 flash monitor这里要提醒一下开发板的USB转串口芯片在Linux下一般显示为/dev/ttyUSB0或/dev/ttyACM0。如果用Windows注意在设备管理器里确认端口号。在ESP32启动后我的程序流程是这样的先把模块通过GPIO拉PWRKEY开机。延时等待模块启动完成这里至少等3秒模块初始化需要时间。发送ATE0、ATCGDCONT1,IP,ctnb配置。发送ATD*99***1#发起PPP拨号。串口模式切换到PPP数据模式开始等待PPP协议协商。看到IP_EVENT_PPP_GOT_IP事件说明拿到了IP地址网络正式通了。第一次成功的时候日志里出现I (12450) ppp: Got IP: 10.xxx.xxx.xxx I (12452) ppp: DNS: 120.xxx.xxx.xxx那一刻还是很有成就感的。4.4 第四步验证网络连通性拿到IP不等于可以上网还要验证路由和DNS。我直接写了两个小测试一个用来连TCP服务器int sock socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_port htons(8888); inet_pton(AF_INET, 47.xxx.xxx.xxx, addr.sin_addr); connect(sock, (struct sockaddr *)addr, sizeof(addr));另一个是用lwIP自带的DNS解析测试域名解析是否正常struct hostent *he gethostbyname(www.baidu.com); if (he) { ESP_LOGI(TAG, DNS resolve OK: %s, inet_ntoa(*(struct in_addr *)he-h_addr)); }实测下来TCP连接和DNS解析都正常。如果发现DNS不行十有八九是运营商或模块没有把DNS服务器地址下发给PPP链路。解决办法有两个一是手动配置esp_netif_set_dns_info填入运营商的公共DNS比如114.114.114.114或者223.5.5.5二是检查拨号前是否漏了ATCGDCONT的参数配置因为有些运营商会通过这个PDP上下文下发DNS地址。4.5 拨号后的链路保活与自动重连PPP链路建立起来之后并不是完全放心的。我在实测中发现如果网络空闲时间比较长运营商侧的PDP上下文可能会被回收表现为ESP32这边链路还挂着但流量已经发不出去了。这个问题需要主动检查。最简单的保活方案是定期发送一个ICMP Ping包到公网地址比如每隔30秒ping一次223.5.5.5。如果连续几次没有回应就主动断开PPP链路并重新拨号。在ESP-IDF里可以用esp_ping组件实现配置一个定时器即可。还有一点是SIM卡的流量耗尽或者余额不足时模块可能不会立刻断链而是表现为所有包都发到黑洞里。这种时候不要过度依赖自动检测最好在应用层上报心跳云端发现设备失联后再远程触发重启。5. 常见问题排查拨号启动失败、断线重连、功耗偏高5.1 拨号一直超时连IP都拿不到这个坑我踩了不下五次最典型的症状是发完ATD*99***1#之后日志里面一直在重复PPP LCP配置请求但模块就是没有响应。排查路径我整理成了速查表现象可能原因解决办法发完AT指令无任何返回接线错误、模块没开机用USB转串口直接连接模块PC测试确认模块能独立工作返回ERROR但SIM卡已识别APN没设置或错误确认APN后执行ATCGDCONT1,IP,正确的APNLCP一直发送但无应答串口回显没关或波特率不稳定确认执行过ATE0试试降低波特率到9600或115200模块返回NO CARRIER信号差、SIM卡欠费用ATCSQ看信号值低于10基本不可用LCP协商成功但拿不到IPPDP上下文配置问题重新设置ATCGDCONT或换一张SIM卡交叉验证5.2 网络通了但DNS解析失败我实际遇到过一次这种问题IP层通了TCP连接也没问题但gethostbyname永远超时。后来发现PPP协商成功的事件里DNS地址字段是空的。这很可能是模块没有从网络侧获取到DNS信息。解决方式比较粗暴但有效直接用esp_netif_set_dns_info手动配置DNS地址。我在代码里写了一个逻辑如果IP_EVENT_PPP_GOT_IP回调中DNS是0.0.0.0就默认设置为223.5.5.5和114.114.114.114问题立刻解决。5.3 模块复位后ESP32侧ppp会话挂了如果你在运行中手动按下模块复位键或者模块因为供电波动自动重启ESP32侧的PPP接口不会自动恢复。这是很常见的一个问题。我在实践中采用了一个看门狗方案建立一个“PPP链路状态”标志位。每10秒检测一次这个标志位和最后一次收到TCP ACK的时间。如果超过30秒没有任何数据活动并且状态位显示PPP连接中断就调用esp_netif_action_disconnect和esp_netif_action_start重新初始化PPP接口。需要注意的是重新初始化PPP接口之前务必先确认4G模块已经完成了重启流程并且串口重新处于AT命令模式。否则你发出的ATD指令会被当成PPP数据导致协商永远失败。5.4 功耗偏高电池供电怎么优化这个项目的最终形态大概率是电池供电的所以功耗是一个躲不开的话题。指路给我实测数据静态待机时纯ESP32跑PPP协议栈大约几十毫安但4G模块只要注册上网电流就会稳定在几十毫安到几百毫安之间。如果跑业务数据和频繁心跳平均功耗会明显抬升。我的优化思路有这几个方向在不需要数据传输时用ATCFUN0把模块切到飞行模式电流可以降到很低。需要传输数据时再切回ATCFUN1重新注册网络。对于低频上报场景可以考虑“拨号-传数-断线”的循环每次上报事件发生时才发起PPP拨号上报完成后主动挂断。这样4G模块大部分时间处于休眠状态。在模块侧选择支持PSMPower Saving Mode或者eDRX的低功耗模式部分新版模块固件支持这些省电特性。这方面不同运营商支持程度不一样需要实测。ESP32本身在深度睡眠下可以做到微安级待机搭配一个RTC定时器唤醒操作流程整体待机时间能拉长很多。5.5 我最后的调试经验如果说这个项目最值得记住的一句话那就是先保证AT指令能稳定工作再想PPP拨号的事。ES32和4G模块之间的串口链路是最容易出错的一环但它也是最容易被忽略的一环。很多人包括我一上来就想直接跑PPP结果AT指令还回显乱七八糟拨号自然是水中花。我在调试过程中养成了检查日志的习惯。ESP-IDF的日志系统很强大把ESP_LOG_LEVEL调到ESP_LOG_VERBOSE之后可以看到lwIP和UART驱动的详细行为。很多问题看着像是“玄学”打开详细日志之后原因就一目了然了。比如UART丢字节、PPP帧校验失败、LCP协商超时这些都会在日志里有明确提示。另外养成“按模块节奏走”的习惯也很重要。4G模块开机之后至少要等2到3秒才能接受AT指令某些模块甚至需要5秒以上。如果ESP32启动速度比较快一上电就发AT模块还在初始化只会收到一串乱码。这个等待时间不能省。6. 后续还能怎么玩从拨号到物联网平台PPP拨号打通之后后面的路就宽了。ESP32已经是一个有独立公网IP能力的节点你完全可以在这个基础上继续接入云平台。我自己下一步计划是把MQTT协议跑在这个PPP链路上对接阿里云的物联网套件。AT指令模式下模块自带的TCP/IP协议栈不太容易和MQTT库配合但PPP模式下ESP32的lwIP已经是一个完整的协议栈移植标准MQTT客户端就是很常规的操作了。另外还有一个值得尝试的方向是把4G模块的USB接口用起来。ESP32-S3是支持USB Host的可以直连4G模块的USB口跑RNDIS或ECM协议数据传输带宽比串口PPP高不少。不过USB Host在资源占用和协议实现上要复杂一些串口PPP方案稳定后我准备把那部分也研究一遍。回到开头说的随身WiFi话题——有些便携WiFi盒子其实就是一颗4G通信芯片加一颗带Wi-Fi的SoC数据通道同样是走PPP或者NDIS只是外面加了一层壳子和电源管理。理解了ESP324G模块的PPP拨号机制你其实就已经理解了一大半随身WiFi的工作逻辑。差别只在于你的ESP32不需要把网络热点分享出去自己直接消费流量就行。最后分享一个小细节如果你用的是物联网卡而不是普通手机SIM卡请注意大多数物联网卡是默认关闭“语音服务”的只开放数据业务。这不影响PPP拨号但如果一开始用ATD*99***1#拨不通可以先换个普通手机卡试试排除掉SIM卡本身的限制。这个排查方式简单但有效我自己就是靠“换卡交叉验证”才确认模块没有问题最终定位到APN配置上。