告别AT指令:ESP32通过PPPoS驱动4G模块上网

前言

在之前的文章中,我们都是利用ESP32自带的WiFi进行网络连接。但在户外或者没有WiFi覆盖的角落,想要让设备联网,就得请出“4G模块”了。

air780eg_modem

通常大家驱动4G模块(比如SIM800, Air724, EC20等)最原始的方法是用UART发送AT指令。
比如 AT+HTTPINITAT+HTTPPARA… 这种方式不仅繁琐,而且解析返回字符串简直是噩梦,写出来的代码全是状态机,一旦模块吐点乱码,程序直接暴毙。手动解析AT指令简直是坏文明!

为了优雅地使用4G模块,我决定使用 PPPoS(Point-to-Point Protocol over Serial)。
简单来说,就是把串口“伪装”成一个网卡。这样底层的TCP/IP协议栈(LwIP)就能直接接管网络,我们写上层代码时,完全不用关心是在用WiFi还是4G,直接调标准的Socket接口就完事了。

真香

准备工作

1. 硬件连接

找一个支持PPP拨号的模块(市面上绝大多数Cat.1/Cat.4模块都支持)。
我用的是合宙的Air780EG, 这个模块属于4G+GNSS二合一模块, 在制作需要定位的设备很方便.
将模块的 TX/RX 接到 ESP32C3 的串口引脚上(记得共地)

PS: 模块进入PPPOS状态以后GPS就不能走主串口输出
好消息是我们可以使用ESP32C3的另外一个串口和GPS串口对接, 这样就可以同时使用GPS和4G PPPOS拨号了
比如说我用的是GPIO8和GPSTX连接, TXD0和RXD0接4G模块的主串口

核心代码实现

示例项目可以通过下面命令创建

1
idf.py create-project-from-example "espressif/esp_modem=1.0.3:pppos_client"

接下来会基于 ESP-IDF 的 pppos_client 示例解析解析并补充说明.

整个过程其实就分三步:定义事件、初始化DCE/DTE、切换到数据模式

1. 事件处理 (Event Handler)

PPPoS 的运行依赖于事件驱动。我们需要监听 IP 层面的事件,当拿到 IP 地址时,才算真正联网成功。

modem_event.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static EventGroupHandle_t event_group = NULL;
static const int CONNECT_BIT = BIT0;
static const int DISCONNECT_BIT = BIT1;

// 监听 IP 获取事件
static void on_ip_event(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
if (event_id == IP_EVENT_PPP_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data;

// 打印一下获取到的IP信息,看着就舒服
ESP_LOGI(TAG, "Modem Connect to PPP Server");
ESP_LOGI(TAG, "IP : " IPSTR, IP2STR(&event->ip_info.ip));
ESP_LOGI(TAG, "Netmask : " IPSTR, IP2STR(&event->ip_info.netmask));
ESP_LOGI(TAG, "Gateway : " IPSTR, IP2STR(&event->ip_info.gw));

// 通知主任务:我们连上网了!
xEventGroupSetBits(event_group, CONNECT_BIT);
} else if (event_id == IP_EVENT_PPP_LOST_IP) {
ESP_LOGW(TAG, "Modem Disconnect from PPP Server");
xEventGroupSetBits(event_group, DISCONNECT_BIT);
}
}

2. 初始化与拨号

这一步是重头戏。我们需要初始化 Netif(网络接口),配置 DTE(数据终端设备,即ESP32C3端的UART),然后初始化 DCE(数据通信设备,即4G模块)。

main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
void app_main(void)
{
// 1. 初始化网络接口和事件循环
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &on_ip_event, NULL));

event_group = xEventGroupCreate();

// 1.5 重启模块, 我的模块Reset脚接的GPIO7
gpio_config_t io_config = {.pin bit mask = BIT64(7),
.mode = GPIO MODE OUTPUT};
gpio_config(&io_config);
gpio_set_level(70);
TaskDelay(pdMS_TO_TICKS(500));
gpio_set_level(71);
// 2. 配置 PPP 网络接口
esp_modem_dce_config_t dce_config = ESP_MODEM_DCE_DEFAULT_CONFIG("cmnet"); // APN通常是cmnet
esp_netif_config_t netif_ppp_config = ESP_NETIF_DEFAULT_PPP();
esp_netif_t *esp_netif = esp_netif_new(&netif_ppp_config);

// 3. 配置串口 (DTE)
esp_modem_dte_config_t dte_config = ESP_MODEM_DTE_DEFAULT_CONFIG();
dte_config.uart_config.tx_io_num = 21; // TX引脚
dte_config.uart_config.rx_io_num = 20; // RX引脚
dte_config.uart_config.flow_control = ESP_MODEM_FLOW_CONTROL_NONE; // 没接流控线就选NONE

// 4. 创建 Modem 对象 (这里根据实际型号选择, 由于没有Air780eg的配置,所以使用的ESP_MODEM_DCE_CUSTOM)
ESP_LOGI(TAG, "Initializing esp_modem...");
esp_modem_dce_t *dce = esp_modem_new_dev(ESP_MODEM_DCE_CUSTOM, &dte_config, &dce_config, esp_netif);

// 5. 检查信号质量, 如果没有信号, 很大概率会有问题, 4G模块可能没有正常工作
int rssi, ber;
if (esp_modem_get_signal_quality(dce, &rssi, &ber) == ESP_OK) {
ESP_LOGI(TAG, "Signal quality: rssi=%d, ber=%d", rssi, ber);
}

// 6. 关键步骤:切换到 DATA 模式!
// 这一步之后,串口就变成了透明传输的网卡通道,不能再发AT指令了
ESP_LOGI(TAG, "Switching to Data Mode...");
esp_err_t err = esp_modem_set_mode(dce, ESP_MODEM_MODE_DATA);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to enter data mode!");
return;
}

// 7. 等待获取 IP
ESP_LOGI(TAG, "Waiting for IP address...");
xEventGroupWaitBits(event_group, CONNECT_BIT, pdFALSE, pdFALSE, portMAX_DELAY);

// 到这里,你已经连上网了!
// 可以尝试 Ping 一下
ESP_LOGI(TAG, "Pinging example.com...");
esp_console_run("ping example.com", NULL);
}

尤其要注意初始化之前进行模块的重启, 官方例子已经没有这部分代码了.所以我们手动补充在初始化之前

如果一切顺利就会看到下图ping成功的输出

running

踩坑小记

在调试过程中遇到过几个坑,顺便记录一下:

  1. 供电问题:4G模块瞬时电流很大,普通的3.3V LDO通常扛不住,导致模块会反复重启, 建议单独给模块供电。我用了一个DCDC提供3.9V电压给模块.
  2. 流控:如果你的线没接 RTS/CTS,一定要在配置里把 flow_control 设为 ESP_MODEM_FLOW_CONTROL_NONE,否则数据发不出去。
  3. APN:虽然现在很多卡都能自动识别APN,但最好还是显式指定一下(移动通常是 cmnet,联通 3gnet)。
  4. 4G模块初始化: RESET脚也得用ESP32 GPIO控制, 用于4G模块初始化重启

总结

使用 PPPoS 后,4G 模块的使用体验和 WiFi 几乎没有区别。
配合我在《ESP32异步网络请求》中介绍的 AsyncHTTP 或者 MQTT 库,就可以轻松地把设备部署到野外了。

那么,古尔丹,代价是什么呢?

答案是功耗爆炸.
由于4G模块一直处于数据模式, 如果需要发送一些功耗优化的AT指令, 比如飞行模式, 休眠. 那么会异常麻烦, 每次都需要切换回Command模式再发送指令.
如果和我一样是自带的GPS功能的4G模块, 那么功耗更是爆炸, 想要独立控制GPS电源必须通过AT指令, 来回切换失败风险很高.


参考资料

告别AT指令:ESP32通过PPPoS驱动4G模块上网

https://chaosgoo.com/2025/12/13/esp32-pppos-4g-modem/

作者

Chaos Goo

发布于

2025-12-13

更新于

2025-12-13

许可协议