返回

ESP32 连接 4G 模块:ESP-IDF PPPoS 拨号与 Air780EG 实战

发布于:  at  08:09 上午
更新于:  at  03:40 上午
⏱️ 约 5867 字 • 预计阅读 15 分钟

Read in English

用 ESP32-C3 与 Air780EG 实现 PPPoS 拨号,让 4G 连接交给 lwIP 和 Socket。记录 esp_modem 配置、UART 接线、IP 事件及供电、流控、APN 排查。

环境与范围: 原项目使用 ESP32-C3、Air780EG;示例入口固定为 esp_modem 1.0.3。ESP-IDF 具体版本未记录,代码片段不是完整工程,迁移时需核对组件 API 和模块固件。

前言

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

air780eg_modem

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

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

So Fragrant

准备工作

1. 硬件连接

先确认模块的具体型号与固件支持 UART PPP 拨号,不能仅凭 Cat.1/Cat.4 分类判断。 我用的是合宙的Air780EG, 这个模块属于4G+GNSS二合一模块, 在制作需要定位的设备很方便. 将模块的 TX/RX 接到 ESP32-C3 的串口引脚上,记得共地,并按模块资料核对 UART 电平;电平不同需要转换,不能直接连接。

ESP32-C3Air780EG / 模块端用途
GPIO21(示例 TX)UART RX发送到模块
GPIO20(示例 RX)UART TX接收模块数据
GNDGND共地
GPIO7(原项目)RESET 控制电路电平、驱动方式与脉宽按硬件资料确认
GPIO8(原项目 RX)GPS TX可选的独立定位输出

模块电源不接 ESP32 的 GPIO。供电电压、峰值电流和去耦按模块或开发板资料设计;文末的 3.9V 是我的项目配置,不是所有 4G 模块的通用参数。

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

核心代码实现

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

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

接下来基于 ESP-IDF 的 pppos_client 示例补充说明。请先构建原示例,再按自己的模块修改;下方只展示关键流程,包含头文件、Kconfig 和模块适配的完整工程应以组件示例为起点。

PPPoS 并不是完全不用 AT 指令。 注册网络、配置 APN 和拨号通常仍由 esp_modem 通过 AT 指令完成;进入数据模式后,上层 HTTP、MQTT 或 Socket 流量交给 lwIP,不再逐个解析 AT 网络请求。

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

1. 事件处理 (Event Handler)

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

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模块)。

void app_main(void)
{
// 1. 初始化网络接口和事件循环
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
    event_group = xEventGroupCreate();
    if (event_group == NULL) {
        ESP_LOGE(TAG, "Failed to create event group");
        return;
    }
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &on_ip_event, NULL));

    // 1.5 重启模块, 我的模块Reset脚接的GPIO7
    // 仅对应原项目的低有效 RESET 控制电路;不要直接套用到其他模块。
    gpio_config_t io_config = {.pin_bit_mask = BIT64(7),
                              .mode = GPIO_MODE_OUTPUT};
    ESP_ERROR_CHECK(gpio_config(&io_config));
    ESP_ERROR_CHECK(gpio_set_level(7, 0));
    vTaskDelay(pdMS_TO_TICKS(500));
    ESP_ERROR_CHECK(gpio_set_level(7, 1));
    // 此后应按模块手册等待启动完成,再发送 AT 指令。
    // 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);
    if (esp_netif == NULL) {
        ESP_LOGE(TAG, "Failed to create PPP netif");
        return;
    }

    // 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...");
    // CUSTOM 需要提供该组件版本要求的自定义 DCE 实现;
    // 仅设置此枚举不会自动获得 Air780EG 支持,请参考原示例的模块适配。
    esp_modem_dce_t *dce = esp_modem_new_dev(ESP_MODEM_DCE_CUSTOM, &dte_config, &dce_config, esp_netif);
    if (dce == NULL) {
        ESP_LOGE(TAG, "Failed to create modem; check the DCE implementation");
        return;
    }

    // 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...");
    EventBits_t bits = xEventGroupWaitBits(event_group, CONNECT_BIT, pdFALSE,
                                          pdFALSE, pdMS_TO_TICKS(60000));
    if ((bits & CONNECT_BIT) == 0) {
        ESP_LOGE(TAG, "PPP timeout; check power, registration, APN and UART");
        return;
    }

    // 获得 IP 后再验证 DNS 和实际网络请求;有 IP 不等于公网一定可达。
    // 原示例若已注册 ping 控制台命令,可从控制台执行 ping example.com。
    ESP_LOGI(TAG, "PPP got IP; ready to test DNS and connectivity");

}

原项目在初始化前增加了模块复位。是否需要这一步、RESET 的驱动方式与启动等待时间都取决于模块和固件,不能把上述 500ms 脉冲当成通用要求。

如果一切顺利就会看到下图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模块初始化:原项目用 GPIO 控制 RESET。先确认模块启动完成、AT 可响应,再进行注册与拨号。

按现象排查

现象优先检查
AT 无响应供电、启动时间、TX/RX 交叉连接、波特率、电平转换
模块反复重启发射时电源压降、线材、去耦与电源峰值能力
能发 AT,但拨号失败SIM 状态、注册状态、套餐与 APN、模块 PPP 支持
已注册网络,但没有 IP_EVENT_PPP_GOT_IPDCE 适配、拨号日志、UART 流控、PPP 配置
已有 IP,但域名请求失败DNS;先区分地址可达性和域名解析问题
大量数据传输时断线供电、UART 缓冲与波特率、硬件流控配置

本文没有记录完整的 ESP-IDF 版本与可独立构建工程,因此这里修正的是展示代码与排查指引,不代表已完成新一轮硬件或编译验证。

总结

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

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

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


参考资料


在以下平台分享此文章:

评论


上一篇
屏幕不够,算法来凑(一):Ditherpunk 抖动算法原理与 JS 实时演示
下一篇
CH592F利用SPI+DMA驱动WS2812灯珠