外设的常见问题

本篇博客主要对两个外设模块进行分析,分别是 SDIO WiFi 模块和 4G 通信模组。

一、SDIO WiFi 模块

SDIO 的驱动原理这里就不再详细展开了。如果想深入了解相关内容,可以参考博客Linux SDIO驱动学习_sdio wifi模块驱动移植-CSDN博客

SDIO 属于 Linux MMC 子系统。i.MX6ULL 的 USDHC 控制器驱动初始化时,会创建一个 struct mmc_host,用于抽象一个 MMC/SD/SDIO 主机控制器,并将 mmc_host->ops 设置为 SDHCI 提供的操作函数。MMC Core 下发命令或传输数据时,会通过 mmc_host->ops->request() 进入 sdhci_request(),最终操作 i.MX6ULL 的 USDHC 寄存器完成传输。

注册 mmc_host 后,MMC Core 开始扫描设备:

复制代码
USDHC/SDHCI 控制器初始化
└─ 创建 struct mmc_host                  // 抽象一个 MMC/SDIO 主机控制器
   ├─ host->ops->request                 // 下发命令和数据请求
   ├─ host->ops->set_ios                 // 设置时钟、总线宽度和电压
   └─ mmc_add_host()                     // 注册到 MMC Core
      └─ mmc_rescan()                    // 开始扫描 SD、SDIO 或 eMMC 设备
         ├─ CMD8                         // 辅助检测 SD 版本和接口电压,非判断 SDIO 的主要命令
         └─ mmc_attach_sdio()
            ├─ CMD5                      // 读取 SDIO OCR、支持电压和 function 数量
            ├─ 创建 struct mmc_card      // 表示整个 SDIO WiFi 芯片
            ├─ CMD3/CMD7                 // 分配 RCA,并选中该 SDIO 设备
            ├─ CMD52 读取 CCCR           // 公共能力:版本、总线宽度、高速和多块传输等
            ├─ CMD52 读取 CIS            // 厂商 ID、设备 ID、功能信息等元组
            ├─ CMD52 读取各 function FBR // function 类型、CIS 地址等基础寄存器
            ├─ 创建 struct sdio_func     // 每个 function 对应一个软件设备对象
            └─ sdio_add_func()           // 注册到 sdio_bus_type,匹配 WiFi 驱动

其中:

  • mmc_host:表示 i.MX6ULL 的一个 USDHC 主机控制器。
  • mmc_card:表示挂在该控制器上的整个 SDIO WiFi 芯片。
  • sdio_func:表示 WiFi 芯片内部的一个 SDIO function,并不是"指向寄存器"的指针。
  • CCCR:描述整个 SDIO 卡的公共能力,不只是速率。
  • FBR:描述某个 function 的类型、CIS 地址和块大小等信息。
  • CIS:保存厂商 ID、设备 ID及其他功能描述。
  • CMD52:主要用于少量寄存器读写。
  • CMD53:主要用于批量数据传输,是 WiFi 收发数据时经常使用的命令。

sdio_func 的厂商 ID、设备 ID或 class 与 SDIO WiFi 驱动匹配后,驱动的 probe() 被调用。随后 WiFi 驱动加载固件、初始化芯片并注册 wlan0。发送网络数据时,WiFi 驱动会调用 sdio_memcpy_toio() 等接口,经过 CMD53 将数据发送给 WiFi 芯片。

二、SDIO WiFi 的工作原理

1、先建立整体认识

SDIO WiFi 并不是一个单独的驱动就能工作,它同时涉及下面几个部分:

复制代码
用户空间
├─ iw / iwconfig                         // 扫描、查看无线状态
├─ wpa_supplicant                       // WPA/WPA2 认证和关联
└─ udhcpc                                // 关联成功后获取 IP
   ↓ nl80211,旧系统也可能使用 WEXT

Linux 无线管理层
└─ cfg80211                              // 无线配置、扫描、连接、密钥、国家码
   ↓ cfg80211_ops

WiFi 功能驱动
└─ BCMDHD                                // BCM4339 FullMAC 主机驱动
   ├─ cfg80211 适配                    // 把扫描/连接命令交给固件
   ├─ net_device                         // 注册 wlan0,承载业务数据
   ├─ BDC/CDC                            // 主机与 WiFi 固件间的协议
   └─ SDPCM                              // Broadcom SDIO 总线帧协议
   ↓ CMD52 / CMD53

MMC/SDIO 总线层
├─ SDIO Core / sdio_bus_type             // sdio_func 管理与驱动匹配
├─ MMC Core / mmc_host                   // 扫卡、命令和请求调度
├─ SDHCI                                  // 通用主机控制器层
└─ i.MX6ULL USDHC                         // 寄存器、DMA、时钟和中断
   ↓ CLK / CMD / DAT[3:0]

BCM4339 WiFi 芯片
├─ 固件运行在芯片内部
├─ 执行 802.11 MAC、扫描、关联、加密和重传
└─ 通过射频和无线 AP 通信

需要先更正一个名称:正确的是 cfg80211,不是 cfg8022


2、cfg80211 在其中负责什么

2.1. cfg80211 是无线配置和管理框架

cfg80211 主要管理:

  • 扫描热点;
  • 连接和断开 AP;
  • 设置 SSID、认证类型和密钥;
  • 设置国家码、频段和发射功率;
  • 管理 Station、AP、P2P 等接口类型;
  • 维护 wiphywireless_dev 和扫描到的 BSS 信息;
  • 将连接结果、断线原因等事件通知用户空间。

cfg80211 不负责把 TCP/IP 数据包封装成 CMD53,也不直接搬运网络数据。它主要负责"怎样扫描和连接",而 net_device 数据路径负责"连接后怎样收发数据"。

2.2. nl80211 和 cfg80211 的关系
复制代码
wpa_supplicant / iw
└─ nl80211 Netlink 命令
   └─ cfg80211
      └─ wiphy->cfg80211_ops
         ├─ .scan       = wl_cfg80211_scan
         ├─ .connect    = wl_cfg80211_connect
         ├─ .disconnect = wl_cfg80211_disconnect
         ├─ .add_key    = wl_cfg80211_add_key
         └─ .start_ap   = wl_cfg80211_start_ap

BCMDHD 在 wl_cfg80211.c 中定义 wl_cfg80211_ops,并通过 wiphy_new() 把这些回调函数交给 cfg80211。

扫描结束后,BCMDHD 会调用 cfg80211_scan_done();连接成功后调用 cfg80211_connect_result();断线后调用 cfg80211_disconnected() 将结果反向上报。

2.3. cfg80211 和 mac80211 的区别
复制代码
cfg80211
└─ 所有新式 Linux WiFi 驱动通用的配置框架

mac80211
└─ 主要服务于 SoftMAC 驱动,在 Linux 内核处理更多 802.11 MAC 逻辑

BCM4339 的 BCMDHD 属于 FullMAC 方案:

  • Linux 主机通过 cfg80211 发送扫描、连接和密钥命令;
  • WiFi 芯片内部固件执行大部分 802.11 MAC 工作;
  • Linux 数据路径通常看到的是以太网格式数据,而不是自己构建完整的 802.11 无线帧;
  • 本工程虽然编译了 CONFIG_MAC80211=y,但 BCMDHD 的主要控制和数据路径不经过 mac80211。

3、从上电到出现 wlan0

复制代码
设备树阶段
├─ USDHC 节点                         // 描述寄存器、IRQ、时钟和 bus-width
├─ pinctrl                            // 配置 CLK/CMD/DAT[3:0]
├─ WiFi 电源 / WL_REG_ON             // 给模组上电并退出复位
├─ non-removable 或 cd-gpios         // 描述板载设备或卡检测
└─ keep-power-in-suspend / wakeup       // 可选的休眠保持与唤醒

USDHC 控制器初始化
└─ sdhci_esdhc_imx_probe()              // i.MX6ULL USDHC 平台驱动 probe
   └─ sdhci_add_host()
      └─ mmc_add_host()                  // 向 MMC Core 注册 mmc_host

SDIO 枚举
└─ mmc_rescan()
   └─ mmc_attach_sdio()
      ├─ CMD5                            // 获取 OCR、ready 和 function 数量
      ├─ CMD3 / CMD7                     // 分配 RCA、选中设备
      ├─ CMD52                           // 读取 CCCR、FBR、CIS
      ├─ mmc_alloc_card()                // 创建 mmc_card
      ├─ sdio_init_func()                // 为 Function 1..N 创建 sdio_func
      └─ sdio_add_func()                 // 注册到 sdio_bus_type

BCMDHD 匹配
└─ sdio_register_driver()                // 注册 SDIO function driver
   └─ bcmsdh_sdmmc_probe()               // 匹配 sdio_func
      ├─ 使能 Function 1/2
      ├─ 配置 block size
      ├─ 识别 Broadcom 芯片
      ├─ 创建 DHD bus/protocol 对象
      └─ 注册网卡和 cfg80211 对象

固件启动
└─ dhd_bus_start()                       // 某些版本在 probe,某些在 ifconfig up 时执行
   ├─ 从 rootfs 读取 fw_bcmdhd.bin
   ├─ 从 rootfs 读取 NVRAM/校准文件
   ├─ 通过 CMD53 下载到 WiFi 芯片 RAM
   ├─ 启动芯片内部固件
   └─ Firmware up                        // 固件可以接受命令

网络接口就绪
├─ wiphy                                 // 物理无线设备能力
├─ wireless_dev                          // STA/AP/P2P 等无线接口属性
└─ net_device wlan0                      // IP 数据收发接口

wlan0 出现只说明网卡已经注册,并不代表已经连上 AP,也不代表已经获取 IP。


4、扫描和连接是怎样执行的

4.1. 扫描路径
复制代码
iw dev wlan0 scan / wpa_supplicant
└─ nl80211
   └─ cfg80211
      └─ wl_cfg80211_scan()              // BCMDHD 的 cfg80211 scan 回调
         └─ DHD iovar/ioctl              // 构建给固件的扫描命令
            └─ CDC control message
               └─ SDPCM control channel
                  └─ CMD53
                     └─ WiFi 固件扫描各信道

WiFi 固件返回扫描结果
└─ SDIO Function 中断
   └─ CMD53 读取 event/data
      └─ BCMDHD 解析扫描结果
         ├─ cfg80211_inform_bss()        // 向 cfg80211 上报 AP
         └─ cfg80211_scan_done()         // 通知用户空间扫描完成
4.2. WPA2 连接路径
复制代码
wpa_supplicant
├─ 选择 SSID
├─ 设置 WPA/WPA2 安全参数
└─ nl80211 connect
   └─ cfg80211
      └─ wl_cfg80211_connect()
         └─ 将 SSID、密钥、认证方式交给 WiFi 固件
            └─ 固件执行扫描和 802.11 认证/关联
               └─ cfg80211_connect_result()  // 向 wpa_supplicant 上报关联结果
                  └─ wpa_supplicant 处理 EAPOL 四次握手和密钥派生
                     └─ cfg80211 .add_key    // 将密钥安装到驱动/固件
                        └─ wpa_state=COMPLETED

上面是常见模式。部分固件支持 WPA 握手卸载,但不能因为它是 FullMAC 就笼统地认为四次握手一定全部在芯片内完成。

关联成功后才能运行 udhcpc -i wlan0 向 AP 后面的 DHCP 服务器获取 IP、默认网关和 DNS。

三、SDIO WIFI的常见的问题

关于 SDIO WiFi 的常见问题及排查方法,这里不再展开说明,可参考博客Linux SDIO驱动学习_sdio wifi模块驱动移植-CSDN博客

四、4G 模组

i.MX6ULL 的 USB 控制器首先由平台驱动读取设备树中的寄存器、时钟、PHY 和 dr_mode 等信息,并创建 ChipIdea 通用控制器对象 ci_hdrc。ChipIdea Core 根据 dr_mode 初始化 Host 或 Gadget 的角色操作函数,将其保存到 roles[],然后选择并启动对应角色。本项目配置为 Host,因此 host_start() 会创建通用主机控制器对象 usb_hcd,并通过 hc_driver 向 USB Core 提供 URB 提交、取消、中断处理和 Root Hub 控制等操作。usb_add_hcd() 注册 HCD、USB 总线以及逻辑 Root Hub,Hub 驱动随后监听端口变化;当 USB 设备接入时,完成端口复位、地址分配、描述符读取和配置选择,再为设备中的各个 Interface 创建 usb_interface 并匹配相应的功能驱动。设备驱动收发数据时使用 URB 描述一次 USB 传输请求,底层 EHCI 驱动再将 URB 转换成 QH、qTD 等硬件描述符,由控制器通过 DMA 和 USB PHY 完成真正的数据传输。

i.MX6ULL 的 ChipIdea USB 初始化过程,可以理解为从"控制器角色管理"逐步进入"USB Host 数据传输和设备枚举"。

复制代码
设备树
└─ usbotg2
   ├─ compatible = "fsl,imx6ul-usb"
   ├─ dr_mode = "host"
   ├─ clocks / PHY / usbmisc
   └─ status = "okay"

i.MX 平台适配层
└─ ci_hdrc_imx_probe()
   ├─ 读取设备树中的寄存器、IRQ、时钟和 PHY
   ├─ 创建 ci_hdrc_imx_data              // i.MX 平台私有运行状态
   ├─ 构造 ci_hdrc_platform_data          // 传递给通用 ChipIdea Core 的配置
   └─ ci_hdrc_add_device()
      └─ 创建通用 ChipIdea 子 platform_device

ChipIdea Core
└─ ci_hdrc_probe()
   ├─ 创建 struct ci_hdrc                // 一个 ChipIdea USB 控制器实例
   ├─ 取得 ci_hdrc_platform_data
   ├─ 映射寄存器并初始化 USB PHY
   ├─ 根据 dr_mode 初始化角色
   │
   ├─ ci_hdrc_host_init()
   │  └─ ci->roles[CI_ROLE_HOST]
   │     ├─ start = host_start
   │     ├─ stop = host_stop
   │     ├─ irq = host_irq
   │     ├─ suspend = ci_hdrc_host_suspend
   │     └─ resume = ci_hdrc_host_resume
   │
   └─ ci_role_start(ci, CI_ROLE_HOST)
      └─ host_start()

struct ci_hdrc 是 ChipIdea 通用核心的中心管理对象,里面保存控制器寄存器、当前角色、平台配置和 roles[]roles[] 中保存的不是已经运行的控制器,而是 Host/Gadget 两种角色的操作函数表。

创建 USB HCD

复制代码
host_start()
├─ __usb_create_hcd()
│  └─ 创建 struct usb_hcd               // USB主机控制器通用抽象
│
├─ hcd->driver = ci_ehci_hc_driver       // Host Controller操作函数表
│  ├─ reset
│  ├─ start
│  ├─ stop
│  ├─ urb_enqueue                       // 提交USB传输请求
│  ├─ urb_dequeue                       // 取消USB传输请求
│  ├─ hub_status_data                   // 获取根端口状态变化
│  └─ hub_control                       // 控制Root Hub端口
│
└─ usb_add_hcd()
   ├─ 注册 usb_hcd
   ├─ 注册 USB Bus
   ├─ 申请/注册控制器IRQ
   └─ register_root_hub()
      └─ 注册逻辑 Root Hub

struct usb_hcd 是 USB Core 对主机控制器的统一抽象,它通过:

复制代码
hcd->driver

指向 struct hc_driver 操作函数表。

USB Core 和 USB 设备驱动并不直接操作 ChipIdea/EHCI 寄存器,而是通过 hc_driver 中的 urb_enqueue()urb_dequeue()hub_control() 等函数进入底层控制器。

URB 数据传输

URB 的全称是 USB Request Block。它不是 USB 线上发送的数据包格式,而是描述"一次USB传输请求"的内核对象。URB 主要保存:

复制代码
struct urb
├─ dev                 // 目标usb_device
├─ pipe                // 端点号、方向和传输类型
├─ transfer_buffer     // 待发送或接收的数据缓冲区
├─ transfer_buffer_length
├─ actual_length       // 实际完成长度
├─ status              // 传输结果
├─ complete            // 完成回调函数
└─ context             // 驱动私有上下文

真正的数据传输流程是:

复制代码
USB功能驱动
└─ usb_submit_urb()
   └─ USB Core检查URB
      └─ usb_hcd_submit_urb()
         └─ hcd->driver->urb_enqueue()
            └─ ehci_urb_enqueue()
               ├─ 根据URB创建QH/qTD硬件描述符
               ├─ 把传输加入EHCI调度表
               └─ 启动DMA

ChipIdea/EHCI硬件
├─ 根据QH/qTD产生USB Token/Data/Handshake包
├─ 通过PHY发送到USB总线
└─ 传输完成后触发中断
   └─ EHCI中断处理
      └─ usb_hcd_giveback_urb()
         └─ urb->complete()

因此更准确地说:

URB 是软件层的一次传输请求描述,EHCI 驱动会把 URB 转换成 QH/qTD 等硬件描述符,USB 控制器再根据这些描述符产生真正的 USB 总线数据包。

五、4G 模组的工作原理

1、USB 4G模组的每个串口类Interface根据VID/PID和Interface信息与option驱动匹配后,USB Core首先进入usb_serial_probe()

2、USB-serial核心为当前Interface创建usb_serial,再为具体TTY端口创建usb_serial_port,将Bulk IN/Bulk OUT端点地址保存到端口中。

3、option_attach()为整个Interface创建usb_wwan_intf_privateusb_wwan_port_probe()为每个端口创建usb_wwan_port_private及专用IN/OUT URB和缓冲区。

4、allocate_minors()通过IDR建立"minor到usb_serial_port"的映射,tty_register_device()最终注册/dev/ttyUSBx

5、用户打开设备后,usb_wwan_open()预先提交Bulk IN URB等待接收;写入数据时usb_wwan_write()选择空闲OUT URB通过Bulk OUT发给4G模组,在这一过程中,数据的发送依赖 USB 框架中的 URB 机制完成封装与提交;模组返回数据后,IN URB完成回调将数据推入TTY缓冲区,供用户read()读取。

1、整体认识

USB 4G模组通常是一个USB复合设备,一个物理模组内部可以包含多个usb_interface,分别用于AT指令、Modem/PPP、GPS NMEA、诊断和网络等功能。

Linux USB Core会对每个Interface单独做驱动匹配。串口类Interface匹配option驱动后,经过USB-serial和TTY框架,最终表现为/dev/ttyUSBx

本文围绕下面五个步骤展开:

复制代码
1. USB Interface与option驱动匹配
   └─ usb_serial_probe()

2. 创建usb_serial和usb_serial_port
   └─ 扫描并保存Bulk IN/Bulk OUT端点信息

3. 创建Interface级和Port级私有数据
   ├─ usb_wwan_intf_private
   └─ usb_wwan_port_private + URB + buffer

4. 分配minor并注册ttyUSB设备
   └─ /dev/ttyUSBx

5. 打开设备并通过Bulk URB收发数据
   ├─ Bulk OUT发送
   └─ Bulk IN接收

2、第一步:Interface匹配后进入usb_serial_probe()

2.1. option驱动的两个重要表

option.c中定义了option_ids[]option_1port_device

复制代码
option_ids[]
└─ struct usb_device_id
   ├─ idVendor
   ├─ idProduct
   ├─ Interface class/subclass/protocol,可选
   └─ driver_info,可用于保留Interface信息

option_1port_device
└─ struct usb_serial_driver
   ├─ num_ports  = 1
   ├─ probe      = option_probe
   ├─ attach     = option_attach
   ├─ port_probe = usb_wwan_port_probe
   ├─ open       = usb_wwan_open
   ├─ write      = usb_wwan_write
   └─ close      = usb_wwan_close

option_ids[]负责描述"支持哪些USB Interface",option_1port_device则描述"匹配后怎样管理和操作这个USB串口"。

2.2 USB Core的匹配过程
复制代码
usb_probe_interface()
└─ usb_match_id(interface, option_ids)
   ├─ 比较VID/PID
   ├─ 根据ID表可选比较Interface信息
   └─ 匹配成功
      └─ usb_serial_probe(interface, id)

需要注意,USB Core首先调用的是USB-serial核心的usb_serial_probe(),不是直接调用option_probe()

usb_serial_probe()内部再执行:

复制代码
search_serial_device(interface)
└─ 遍历usb_serial_driver_list
   └─ 找到option_1port_device
      └─ option_probe(serial, id)
2.3 option_probe()还会再做一次过滤

option_probe()不只看VID/PID,还会检查当前Interface:

复制代码
option_probe()
├─ 跳过class = 0x08的虚拟CD-ROM/存储Interface
├─ 跳过driver_info中标记的reserved Interface
├─ 避免将网卡Interface误绑定为USB串口
└─ 保留需要的blacklist/sendsetup信息

因此,更准确的表述是:

USB 4G模组的每个Interface根据VID/PID和Interface描述信息与option驱动匹配。USB Core匹配成功后先进入usb_serial_probe(),再由USB-serial核心找到option_1port_device并调用option_probe()进一步过滤Interface。


3、第二步:创建usb_serial和usb_serial_port

3.1. 创建usb_serial

usb_serial_probe()找到option_1port_device后,先调用:

复制代码
serial = create_serial(dev, interface, type);

创建的usb_serial用来表示当前被USB串口驱动接管的USB Interface:

复制代码
struct usb_serial
├─ dev       ──► struct usb_device
│                 // 整个4G USB设备
├─ interface ──► struct usb_interface
│                 // 当前匹配的串口Interface
├─ type      ──► option_1port_device
│                 // option驱动的操作函数
├─ port[]    ──► usb_serial_port
│                 // 当前Interface包含的TTY端口
└─ private
                  // 后续保存Interface级私有数据

usb_serial不等于/dev/ttyUSBx,它是当前USB Interface的总管理对象。

3.2 扫描Bulk IN/Bulk OUT端点

usb_serial_probe()遍历当前Interface的端点描述符:

复制代码
interface->cur_altsetting
└─ endpoint[i].desc
   ├─ Bulk IN
   ├─ Bulk OUT
   ├─ Interrupt IN
   └─ Interrupt OUT

对典型4G串口而言:

复制代码
Bulk OUT
└─ i.MX6ULL → 4G模组
   └─ 用于发送AT命令、PPP数据等

Bulk IN
└─ 4G模组 → i.MX6ULL
   └─ 用于返回AT响应、PPP数据等
3.3. 创建usb_serial_port

option_1port_device.num_ports = 1,所以每个匹配的Interface通常创建一个usb_serial_port

复制代码
usb_serial_probe()
└─ kzalloc(sizeof(struct usb_serial_port))
   ├─ tty_port_init(&port->port)
   ├─ port->serial = serial
   ├─ serial->port[0] = port
   ├─ port->dev.bus = &usb_serial_bus_type
   └─ device_initialize(&port->dev)

usb_serial_port表示一个具体的TTY串口端口:

复制代码
struct usb_serial_port
├─ serial                       // 指回所属usb_serial
├─ struct tty_port port          // 接入TTY Core
├─ minor                         // 后续分配的ttyUSB次设备号
├─ port_number                   // 在usb_serial内的端口序号
├─ bulk_in_endpointAddress       // Bulk IN端点地址
├─ bulk_out_endpointAddress      // Bulk OUT端点地址
└─ struct device dev             // usb_serial_bus_type上的设备

usb_serialusb_serial_port建立双向关系:

复制代码
usb_serial
└─ serial->port[0] ─────► usb_serial_port
                                └─ port->serial ──► usb_serial
3.4. 端点如何与port关联

端点描述符不是被放入usb_serial_port的"发送队列",而是把端点地址、最大包长等关键信息保存到port:

复制代码
Bulk IN endpoint descriptor
├─ port->bulk_in_endpointAddress
└─ port->bulk_in_size

Bulk OUT endpoint descriptor
├─ port->bulk_out_endpointAddress
└─ port->bulk_out_size

后续创建URB时,使用这些端点地址构建USB pipe:

复制代码
usb_rcvbulkpipe(serial->dev, port->bulk_in_endpointAddress);
usb_sndbulkpipe(serial->dev, port->bulk_out_endpointAddress);

因此,更准确的表述是:

USB-serial核心为当前Interface创建usb_serial,再为具体TTY端口创建usb_serial_portusb_serial保存USB设备、Interface、option驱动和端口列表,usb_serial_port保存当前TTY端口的Bulk IN/Bulk OUT端点地址和TTY状态。


4、第三步:创建两级WWAN私有数据和URB

4.1 option_attach()创建Interface级私有数据

端点和usb_serial_port创建完成后,usb_serial_probe()调用:

复制代码
option_attach(serial)
└─ kzalloc(sizeof(struct usb_wwan_intf_private))
   ├─ spin_lock_init(&data->susp_lock)
   ├─ 设置use_send_setup
   └─ usb_set_serial_data(serial, data)

usb_wwan_intf_private保存整个USB Interface共享的状态:

复制代码
struct usb_wwan_intf_private
├─ susp_lock                  // suspend/resume同步锁
├─ suspended                 // Interface是否挂起
├─ use_send_setup            // 是否发送特定控制请求
├─ in_flight                 // 已提交、尚未完成的OUT URB数量
└─ open_ports                // 当前打开的TTY端口数

关联关系是:

复制代码
usb_serial.private
└─ usb_wwan_intf_private
4.2. usb_wwan_port_probe()创建Port级私有数据

后续device_add(&port->dev)会导致usb_serial_bus_type.probe被调用:

复制代码
usb_serial_device_probe(dev)
├─ port = to_usb_serial_port(dev)
├─ driver = port->serial->type
└─ driver->port_probe(port)
   └─ usb_wwan_port_probe(port)

usb_wwan_port_probe()为每个usb_serial_port创建:

复制代码
struct usb_wwan_port_private
├─ in_urbs[4]                   // 4个Bulk IN接收URB
├─ in_buffer[4]                 // 对应接收缓冲区
├─ out_urbs[4]                  // 4个Bulk OUT发送URB
├─ out_buffer[4]                // 对应发送缓冲区
├─ out_busy                     // 位图,记录哪些OUT URB正在使用
├─ delayed                      // Interface挂起时保存延迟发送URB
└─ DTR/RTS/CTS/DCD等串口信号状态

分配完成后通过:

复制代码
usb_set_serial_port_data(port, portdata);

建立关联:

复制代码
usb_serial_port.dev.driver_data
└─ usb_wwan_port_private
   ├─ in_urbs[4]
   ├─ out_urbs[4]
   └─ buffers/out_busy/delayed
4.3. 为什么要有两级私有数据
复制代码
usb_wwan_intf_private
└─ 管整个USB Interface共享的PM和请求状态

usb_wwan_port_private
└─ 管某个ttyUSB端口的URB、buffer和串口状态

对当前option_1port_device而言,一个Interface通常只有一个port,但USB-serial和usb_wwan仍然保留了Interface级与Port级的分层设计。

4.4. URB到底是什么

URB是USB Request Block,用来在内核中描述一次USB传输请求:

复制代码
struct urb
├─ dev                         // 目标usb_device
├─ pipe                        // 传输方向、端点号、传输类型
├─ transfer_buffer             // 待发送或接收的buffer
├─ transfer_buffer_length      // 请求长度
├─ actual_length               // 实际完成长度
├─ status                      // 传输结果
├─ complete                    // 完成回调
└─ context = port              // 完成后找回对应端口

需要修正"URB完成数据封装"这个说法:

URB不是额外加在AT命令或PPP数据前面的协议头,也不会被原样发送到USB线上。URB是内核对一次USB传输的软件描述。HCD/EHCI会把URB转换为QH/qTD等硬件描述符,USB控制器再生成真正的Token、Data和Handshake包。

因此,更准确的表述是:

option_attach()为整个Interface创建usb_wwan_intf_private,用于管理挂起、打开端口数和未完成请求数;usb_wwan_port_probe()为每个usb_serial_port创建usb_wwan_port_private,并分配该端口专用的IN/OUT URB与收发缓冲区。


5、第四步:minor与/dev/ttyUSBx如何关联

5.1. allocate_minors()建立minor到port的映射

USB-serial核心使用全局IDR保存minor和usb_serial_port的关系:

复制代码
static DEFINE_IDR(serial_minors);

allocate_minors()执行:

复制代码
minor = idr_alloc(&serial_minors, port, 0,
                  USB_SERIAL_TTY_MINORS, GFP_KERNEL);
port->minor = minor;
port->port_number = i;

建立的映射类似:

复制代码
serial_minors IDR
├─ minor 0 ──► usb_serial_port A
├─ minor 1 ──► usb_serial_port B
├─ minor 2 ──► usb_serial_port C
└─ minor 3 ──► usb_serial_port D

这个IDR映射是后续打开ttyUSBx时反查usb_serial_port的关键。

5.2. device_add()触发Port级probe

minor分配完成后:

复制代码
dev_set_name(&port->dev, "ttyUSB%d", port->minor)
└─ device_add(&port->dev)
   └─ usb_serial_device_probe()
      ├─ usb_wwan_port_probe(port)
      └─ tty_register_device(usb_serial_tty_driver, minor, dev)

usb_wwan_port_probe()先为端口准备专用URB和buffer,成功后才调用tty_register_device()注册TTY设备。

5.3. tty_register_device()注册TTY实例

USB-serial核心在初始化时已经注册了一个全局TTY驱动:

复制代码
usb_serial_tty_driver
├─ name = "ttyUSB"
├─ minor_start = 0
└─ operations = serial_ops
   ├─ install = serial_install
   ├─ open    = serial_open
   ├─ close   = serial_close
   └─ write   = serial_write

tty_register_device()将当前minor实例注册到这个TTY驱动下:

复制代码
tty_register_device(usb_serial_tty_driver, minor, &port->dev)
└─ TTY设备ttyUSB<minor>
   └─ devtmpfs/udev生成/dev/ttyUSB<minor>
5.4. 从ttyUSBx如何找回USB对象
复制代码
/dev/ttyUSB2
└─ tty->index = minor 2
   └─ serial_minors[2]
      └─ usb_serial_port
         ├─ port->serial ──► usb_serial
         │                      ├─ interface ──► usb_interface
         │                      ├─ dev ──► usb_device
         │                      └─ type ──► option_1port_device
         └─ port private ──► usb_wwan_port_private

因此,更准确的表述是:

allocate_minors()通过全局IDR建立"minor到usb_serial_port"的映射,tty_register_device()将该minor注册为TTY设备,最终由devtmpfs或udev生成/dev/ttyUSBx。通过minor找到usb_serial_port后,还可以继续找到所属的usb_serialusb_interfaceusb_deviceoption驱动。


6、第五步:打开、发送和接收数据

6.1. 打开/dev/ttyUSBx
复制代码
open("/dev/ttyUSB2", O_RDWR)
└─ TTY Core
   ├─ serial_install()
   │  ├─ idx = tty->index                         // minor 2
   │  ├─ usb_serial_port_get_by_minor(2)
   │  │  └─ idr_find(serial_minors, 2) ──► port
   │  ├─ serial = port->serial
   │  └─ tty->driver_data = port
   │
   └─ serial_open()
      └─ tty_port_open()
         └─ serial_port_activate()
            └─ port->serial->type->open(tty, port)
               └─ option_1port_device.open
                  └─ usb_wwan_open()

serial_install()通过minor找到usb_serial_port,并将它保存到:

复制代码
tty->driver_data = port;

这样后续的open、write和ioctl都可以从tty->driver_data取回当前USB串口端口。

6.2. usb_wwan_open()预先提交Bulk IN URB
复制代码
usb_wwan_open(tty, port)
├─ portdata = usb_get_serial_port_data(port)
├─ intfdata = usb_get_serial_data(serial)
├─ 如果存在interrupt_in_urb,则提交它
├─ 遍历portdata->in_urbs[0..3]
│  └─ usb_submit_urb(in_urb)
├─ intfdata->open_ports++
└─ 开始异步等待4G模组返回数据

这一点很重要:Bulk IN URB是open时提前提交的,而不是等用户每调用一次read()时才临时创建。

6.3. 写入数据:Bulk OUT路径

例如用户向AT口写入:

复制代码
AT+CSQ\r

函数路径是:

复制代码
write(fd, "AT+CSQ\r", 7)
└─ TTY Core / line discipline
   └─ serial_write(tty, buf, count)
      ├─ port = tty->driver_data
      └─ port->serial->type->write(tty, port, buf, count)
         └─ option_1port_device.write
            └─ usb_wwan_write()
               ├─ portdata = usb_get_serial_port_data(port)
               ├─ 在out_urbs[4]中选择空闲URB
               ├─ 通过out_busy位图标记已占用
               ├─ 将用户数据复制到urb->transfer_buffer
               ├─ 设置urb->transfer_buffer_length
               └─ usb_submit_urb(out_urb)
                  └─ USB Core
                     └─ HCD/EHCI
                        └─ Bulk OUT端点
                           └─ 4G模组

OUT URB完成后:

复制代码
usb_wwan_outdat_callback(urb)
├─ port = urb->context
├─ intfdata->in_flight--
├─ clear_bit(i, &portdata->out_busy)
└─ usb_serial_port_softint(port)
   └─ 通知TTY层可以继续写入
6.4. 接收数据:Bulk IN路径

4G模组返回AT响应时:

复制代码
4G模组返回"+CSQ: 20,99\r\nOK\r\n"
└─ Bulk IN端点
   └─ HCD/EHCI完成IN URB
      └─ usb_hcd_giveback_urb()
         └─ usb_wwan_indat_callback(urb)
            ├─ port = urb->context
            ├─ data = urb->transfer_buffer
            ├─ length = urb->actual_length
            ├─ tty_insert_flip_string(&port->port, data, length)
            ├─ tty_flip_buffer_push(&port->port)
            └─ usb_submit_urb(urb)            // 重新提交,继续等待数据

TTY flip buffer / line discipline
└─ read(fd, buffer, size)
   └─ 用户程序获取4G模组返回数据

IN URB的完成回调不会直接调用用户程序,而是将数据推入TTY flip buffer。用户程序再通过TTY层的read()读取这些字节。

ttyUSBx向用户层提供的是字节流,一次Bulk URB的边界不一定与用户的一次read()完全对应。

因此,更准确的表述是:

用户打开/dev/ttyUSBx后,usb_wwan_open()预先提交Bulk IN URB等待模组数据。用户写入数据时,usb_wwan_write()选择空闲OUT URB,将字节复制到URB buffer并通过Bulk OUT提交给4G模组。模组返回数据后,Bulk IN URB的完成回调将数据推入TTY flip buffer,再由用户程序通过read()读取。URB负责描述和提交USB传输,而不是一种附加在业务数据上的协议封装格式。

六、4G 模组常见问题与排查方法

1、总体分类

USB 4G模组从上电到最终上网,可以分成六个大类。排查时应从前往后找"第一个失败的类别":

复制代码
1. USB设备枚举
   └─ 供电、USB物理连接、描述符、地址和Configuration

2. option驱动匹配与ttyUSB注册
   └─ VID/PID、Interface过滤、usb_serial_port、minor和TTY设备

3. ttyUSB端口打开与USB数据收发
   └─ 端口选择、占用、Bulk IN/Bulk OUT URB和AT通道

4. 4G模组、SIM和运营商注网
   └─ SIM状态、天线、信号、频段和注网结果

5. PPP拨号和协议协商
   └─ APN/PDP、chat脚本、LCP、认证和IPCP

6. IP路由和DNS
   └─ ppp0、默认路由、metric和/etc/resolv.conf
快速判断
现象 归属类别
lsusb也看不到4G模组 第1类:USB枚举
lsusb有模组,但没有ttyUSB 第2类:驱动匹配/TTY注册
ttyUSB存在,但AT无返回 第3类:端口/URB收发
AT正常,但SIM或注网异常 第4类:模组/SIM/注网
AT和注网正常,但没有ppp0 第5类:PPP拨号
ppp0有IP,但不能上网 第6类:路由/DNS

2、第1类:USB 4G设备枚举错误

这一类发生在option驱动之前。如果USB设备本身还没有成功枚举,修改option_ids[]、TTY或PPP配置都没有意义。

1. 完全没有USB日志
现象
复制代码
dmesg中没有new USB device
lsusb中没有4G模组VID:PID
分析

Host没有检测到USB端口连接事件,故障点在USB描述符读取之前。

判断依据

USB Hub线程检测到端口连接后,最先打印new ... USB device,随后才读取描述符并在lsusb中显示设备。两处信息都没有,说明流程尚未进入USB设备枚举。

因此,应先查供电、上电时序、Host控制器和物理连接,而不是查option、TTY或PPP。

优先检查
  • 4G模组3.8 V主电源;
  • PWRKEY、RESET、W_DISABLE引脚时序;
  • USB VBUS检测电压;
  • D+/D-接线和焊接;
  • usbotg2是否以Host模式启动;
  • ChipIdea/EHCI、USB PHY和控制器IRQ。
2. 设备反复连接、断开
日志
复制代码
usb 1-1: new high-speed USB device number 3 using ci_hdrc
usb 1-1: USB disconnect, device number 3
usb 1-1: new high-speed USB device number 4 using ci_hdrc
分析

USB端口曾经检测到设备,但设备或物理连接反复消失。这通常不是AT或PPP问题。

判断依据

出现new high-speed USB device,证明Host至少检测到过USB连接。

紧接着出现USB disconnect,说明USB Core随后收到了端口断开或设备失效事件。此时上层ttyUSB即使短暂创建,也会被注销。

因此,根因更接近掉电、复位、接触不良或USB链路不稳定。

优先检查
  • 模组启动、搜网或发射时,3.8 V是否瞬间掉压;
  • 电源芯片容量、布线和去耦电容;
  • USB线缆、转接板和D+/D-信号质量;
  • PWRKEY/RESET是否受到干扰;
  • Runtime PM和autosuspend。
3. 描述符、分配地址或Configuration失败

这些错误可以合并为"USB Endpoint 0标准控制传输失败"。

日志
复制代码
device descriptor read/64, error -71
device descriptor read/64, error -110
device not accepting address 5, error -71
device not accepting address 5, error -110
can't set config #1, error -32
can't set config #1, error -71
unable to enumerate USB device
分析

这些日志都表示USB默认控制端点Endpoint 0上的标准请求失败,只是失败发生在枚举流程的不同步骤。

判断依据
复制代码
GET_DESCRIPTOR失败
└─ 还没稳定读到USB设备描述符

SET_ADDRESS失败
└─ 设备没有从默认地址0正常切换到新地址

SET_CONFIGURATION失败
└─ 已读取配置描述符,但设备没有启用选中的Configuration

只有SET_CONFIGURATION成功后,Linux才会注册usb_interface并进入usb_serial_probe()

因此,这些错误发生在option驱动匹配之前,应优先检查USB控制传输、供电和物理链路。

错误码合并理解
错误码 含义 主要方向
-71 EPROTO USB协议或响应错误 信号质量、供电、PHY、模组固件
-110 ETIMEDOUT 请求没有按时完成 模组未就绪、复位、无响应、HCD/IRQ
-32 EPIPE Endpoint 0返回STALL 模组拒绝请求、固件或配置状态异常
优先检查
  1. 测量3.8 V、VBUS和PWRKEY/RESET;
  2. 更换USB线缆或减少Hub、转接器;
  3. 将模组接到PC Linux,比较是否能稳定枚举;
  4. 用其他USB设备验证i.MX6ULL的usbotg2
  5. 完全断电后重新上电,排除模组USB固件状态异常。

正常的虚拟光驱模式通常也能完成SET_CONFIGURATION,只是后续绑定usb-storage,而不生成ttyUSB

3、第2类:option匹配和ttyUSB注册错误

这一类的共同前提是lsusb已经能稳定看到4G模组,说明USB设备级枚举已成功。

1. lsusb有设备,但没有ttyUSB
现象
复制代码
lsusb有VID:PID
没有GSM modem (1-port) converter detected
/dev下没有ttyUSBx
分析

USB枚举已经成功,但串口Interface没有成功匹配option驱动。

判断依据

lsusb能显示VID/PID,证明设备描述符和Configuration已经能够读取。

没有GSM modem (1-port) converter detected,说明执行链还没有走到usb_serial_probe()完成端点扫描的位置。

因此,应检查Interface类型、VID/PID匹配和驱动配置,而不是继续排查USB地址分配或PPP。

优先检查
  • CONFIG_USB_SERIAL_OPTIONCONFIG_USB_SERIAL_WWAN是否启用;

  • VID/PID是否存在于option_ids[]

  • 当前Interface是否是存储、QMI/NCM或reserved Interface;

  • option_probe()是否过滤了当前Interface;

  • Interface是否已经绑定到其他驱动;

  • 模组当前USB组合模式是否提供串口功能。

    lsusb
    lsusb -t
    lsusb -v
    dmesg | grep -Ei "option|usbserial|ttyUSB|GSM modem"
    readlink -f /sys/bus/usb/devices/:/driver

本项目的optionusb_wwanusbserial编译进内核,因此lsmod看不到它们不代表驱动缺失。

2. 只出现部分ttyUSB
现象

模组能够稳定枚举并生成ttyUSB,但端口数量少于预期。

分析

这不一定是故障。

不同模组、固件和USB组合模式提供的Interface数量不同。某些Interface也可能被正常分配给存储或网卡驱动。

判断依据

Linux通常按照"一个可用串口Interface对应一个ttyUSB端口"进行注册。

如果缺少的Interface本来就是QMI/NCM、存储或reserved接口,就不应该生成ttyUSB

只有串口Interface确实存在,却没有对应ttyUSB端口时,才可以判断为option过滤、端点识别或端口注册异常。

优先检查
  • lsusb -v中实际存在多少Interface;
  • 每个Interface的Class、端点和当前绑定驱动;
  • option.c对该PID设置的reserved Interface位图;
  • 模组手册中当前USB组合模式的端口数量。
3. converter detected,但没有now attached
日志
复制代码
GSM modem (1-port) converter detected
No more free serial minor numbers
Error registering port device, continuing
分析

option已经匹配,Bulk端点也已经被扫描。故障范围缩小到:

复制代码
usb_serial_port创建
→ usb_wwan_port_probe()
→ minor分配
→ device_add()
→ tty_register_device()
判断依据

converter detectedusb_serial_probe()扫描完端点后打印。

now attached to ttyUSBx则要等到device_add()触发端口Probe、usb_wwan_port_probe()tty_register_device()成功后才会打印。

前者存在而后者不存在,说明故障发生在两处日志之间,可以排除VID/PID未匹配和USB设备级枚举失败。

优先检查
  • No more free serial minor numbers:USB Serial没有可用minor,或者存在minor异常泄漏;
  • Error registering port device:Port设备注册失败,检查此前的内存、设备重名或Driver Core错误;
  • detected但没有attached:检查Port级URB、buffer分配和TTY注册。

4、第3类:ttyUSB端口和URB收发错误

这一类的前提是/dev/ttyUSBx已经出现。USB枚举、option匹配和TTY注册已经基本成功。

1. 打开ttyUSB失败
日志和分析
用户层报错 分析方向
No such file or directory 设备已掉线或重枚举、ttyUSB编号变化、devtmpfs异常
Device or resource busy 端口被pppd或其他AT程序占用
Permission denied 设备节点权限或用户组问题
Input/output error 设备已断开或usb_wwan_open()失败,需要结合dmesg查看
判断依据

open()发生在设备节点已经注册以后,因此这类问题不再属于初次枚举或VID/PID匹配。

  • ENOENT来自设备路径查找;
  • EBUSY表示资源已被占用;
  • EACCES表示权限检查失败;
  • EIO才需要继续结合usb_wwan_open()和URB日志判断底层收发是否异常。
优先检查
复制代码
ls -l /dev/ttyUSB*
fuser /dev/ttyUSB0
readlink -f /sys/class/tty/ttyUSB0/device
dmesg | tail -n 100
2. URB提交或完成失败
日志
复制代码
submit read urb 0 failed: -19
submit urb 0 failed: -108
nonzero status: -32 on endpoint 81
nonzero status: -71 on endpoint 81
resubmit read urb failed. (-108)
分析

先区分两种情况:

  • submit ... failed:URB没有正常进入HCD队列;
  • nonzero status:URB已经提交,但实际USB传输失败。
判断依据

usb_submit_urb()同步返回负数时,USB Core尚未接管这次请求,所以日志表现为submit ... failed

完成回调中的urb->status非0,说明URB此前已经成功提交,但是HCD在实际传输或取消过程中返回错误。

两者发生的时间不同,因此排查方向也不同。

错误码合并表
错误码 含义 优先方向
-19 ENODEV / -108 ESHUTDOWN 设备消失或Host/设备停止 往前查USB disconnect、供电和USB连接
-32 EPIPE 端点STALL 模组固件、错误端口或端点、端点状态
-71 EPROTO / -84 EILSEQ 协议、CRC、时序或无效响应 供电、D+/D-、线缆、PHY、固件
-110 ETIMEDOUT USB传输没有按时完成 模组无响应、PM、HCD/IRQ、供电或固件
-2 ENOENT / -104 ECONNRESET URB被unlink 如果只在close或拔出时出现,通常是正常收尾
3. ttyUSB存在,但AT无返回
分析

常见原因包括:

  • 选错ttyUSBx,当前端口是GPS、DIAG或PPP数据口;
  • 端口被其他程序占用;
  • AT命令缺少\r结尾;
  • TTY没有设置raw模式;
  • 模组还没有完成开机;
  • Bulk OUT发送或Bulk IN接收URB异常。
判断依据

ttyUSB存在,只能证明端口已经注册,并不能证明当前端口是AT Interface,也不能证明打开后提交的Bulk IN/OUT URB能够正常完成。

如果写入AT后完全没有返回,应同时验证端口用途、命令结束符、占用状态和URB日志。

如果换到另一个ttyUSBx后收到OK,就可以确认原先是端口选择错误。

优先检查
  1. 根据模组手册确认AT、GPS、DIAG和PPP端口对应关系;
  2. 使用fuser检查端口是否被占用;
  3. 使用raw模式发送带\r结尾的AT
  4. 查看是否存在Bulk IN/OUT URB错误。

如果AT返回:

复制代码
ERROR

这反而证明TTY、Bulk OUT、模组AT解析、Bulk IN和TTY读回基本都正常。

此时应检查AT命令拼写、参数、固件版本和模组当前状态,不要先修改USB驱动。

5、第4类:4G模组、SIM和注网错误

能够正常发送AT并收到OK,说明USB和TTY通道已经打通。后续问题应转向模组状态、SIM、RF和运营商网络。

1. SIM卡错误
日志和分析
复制代码
AT+CPIN?
返回 含义 排查方向
+CPIN: READY SIM正常 继续检查信号和注网
+CPIN: SIM PIN SIM需要PIN 输入正确PIN
+CPIN: SIM PUK SIM已被锁 使用运营商提供的PUK
SIM not inserted 未检测到SIM 卡座、插卡方向、SIM接口和SIM卡
SIM failure SIM通信失败 供电、ESD、走线、卡座和SIM兼容性
判断依据

AT+CPIN?必须先经过TTY和USB发送到模组,再由模组解析并返回结果。

能够收到结构完整的+CPIN或SIM错误文本,证明AT往返链路基本正常。

返回内容又明确来自模组的SIM检测状态,因此应把排查重点转到SIM卡、卡座和SIM接口,而不是USB驱动。

优先检查
  • 确认返回的是+CPIN: READY、PIN、PUK还是未插卡;
  • 检查插卡方向、卡座接触和SIM卡有效性;
  • 硬件异常时测量SIM_VDD、CLK、IO和RST。
2. 信号和注网错误
日志和分析

常用检查命令:

复制代码
AT+CSQ
AT+CREG?
AT+CGREG?
AT+CEREG?
AT+CGATT?
返回 含义 排查方向
+CSQ: 99,99 无有效信号数据 天线、频段、位置、RF是否开启
注网状态1 已注册本地网络 继续检查APN和PPP
注网状态5 已漫游注册 确认SIM套餐和漫游政策
注网状态2 正在搜索 长期不变时查天线、频段、SIM和覆盖
注网状态3 注册被拒绝 SIM状态、套餐、IMEI和运营商限制
+CGATT: 0 未附着分组域 注网、数据业务权限、APN/PDP
判断依据

CSQCREG/CGREG/CEREGCGATT都是模组协议栈返回的射频、注册和分组域状态。

能够收到这些返回,说明AT通道正常。状态值又把故障限定在无信号、正在搜索、被网络拒绝或未附着数据域。

因此,应检查天线、频段、SIM和运营商侧,而不是USB枚举。

优先检查
  1. 使用AT+CSQ确认是否有有效信号;
  2. 检查CREG/CGREG/CEREG是否已经注册;
  3. 检查CGATT和数据业务权限;
  4. 对照EC20或ME909S的AT手册解释具体状态码。

不同模组的CREG/CGREG/CEREG支持情况和返回格式可能不同,应以EC20或ME909S对应的AT手册为准。

6、第5类:PPP拨号和协议错误

这一类的前提是AT通道、SIM和注网基本正常,但从ttyUSB建立PPP网络接口的过程失败。

1. NO CARRIER / Connect script failed
日志
复制代码
NO CARRIER
Connect script failed
分析
  • NO CARRIER:模组收到拨号命令,但没有进入数据连接模式;
  • Connect script failedchat脚本没有得到预期响应,可能收到ERRORNO CARRIER或直接超时。
判断依据

chat位于PPP协议协商之前,负责通过TTY发送AT拨号命令并等待CONNECT

收到NO CARRIER,说明AT往返正常,但模组拒绝或无法建立数据承载。

脚本超时则说明预期响应没有出现。此时还没有进入LCP阶段,应先检查端口、APN、PDP和拨号脚本。

优先检查
  • 是否选择了正确的PPP/Modem端口;
  • APN是否正确;
  • PDP Context编号和ATD*99#等拨号字符串是否匹配;
  • SIM是否已经注网且CGATT=1
  • chat脚本的期望字符串、\r和超时配置是否正确;
  • SIM套餐是否开通数据业务。
2. LCP超时
日志
复制代码
Serial connection established.
Using interface ppp0
Connect: ppp0 <--> /dev/ttyUSB3
LCP: timeout sending Config-Requests
分析

TTY已经打开,PPP已经开始发送LCP帧,但是没有收到对端有效的PPP响应。

判断依据

日志已经出现:

复制代码
Serial connection established.
Using interface ppp0
Connect: ppp0 <--> /dev/ttyUSB3

这证明TTY打开和chat拨号阶段已经通过。

LCP Config-Requests属于PPP链路层协商的第一个阶段。请求持续超时,说明本端正在发送PPP帧,但对端没有用PPP帧响应。

因此,应重点检查是否真正进入数据模式,以及是否选对PPP数据端口。

优先检查
  • 选错了AT口,而不是PPP数据口;
  • chat没有真正等到CONNECT
  • APN/PDP上下文没有激活;
  • 模组返回的仍然是AT文本,而不是PPP帧;
  • PPP async或流控选项不匹配;
  • 是否同时存在Bulk IN/OUT URB错误。
3. PAP/CHAP/IPCP错误
日志和分析
日志 失败阶段 排查方向
PAP authentication failed PPP认证 用户名、密码、noauth和运营商要求
CHAP authentication failed PPP认证 CHAP凭据和PPP选项
IPCP: timeout sending Config-Requests IP参数协商 APN/PDP、IPCP选项和运营商对端
Could not determine remote IP address 未获取对端IP IPCP下发、noipdefault和PPP配置
判断依据

PPP通常按照以下顺序推进:

复制代码
LCP
→ PAP/CHAP认证
→ NCP/IPCP
→ 获取IP参数

出现PAP或CHAP日志,说明LCP已经基本完成。

出现IPCP日志,说明链路建立和需要执行的认证阶段已经越过。

日志中最晚出现的协议阶段,就是当前最接近根因的范围,因此不应该再回头优先修改USB枚举配置。

优先检查
  • PAP/CHAP失败:核对运营商是否需要认证、用户名、密码和pppd认证选项;
  • IPCP失败:核对APN/PDP、IPCP选项和运营商是否下发IP参数;
  • 保存完整的pppd debug nodetach日志,确认最后成功的阶段。
4. Modem hangup
日志
复制代码
Modem hangup
Connection terminated.
分析

首先检查是否同时出现:

复制代码
USB disconnect, device number N
  • USB disconnect:优先检查供电、USB信号、模组复位或Host PM;
  • 没有USB disconnectttyUSB仍存在:优先检查移动信号、PDP会话、运营商释放和PPP Keepalive。
判断依据

Modem hangup只表示pppd认为串行链路已经结束,它本身不能区分USB物理掉线和移动网络释放。

如果同时出现USB disconnect,内核已经给出了设备层断开的证据。

如果USB设备和ttyUSB仍然存在,说明USB物理链路还在,故障更可能发生在模组数据会话或PPP层。

优先检查
  1. 对齐pppddmesg的时间戳;
  2. 有USB断开时,检查供电、复位和USB链路;
  3. 无USB断开时,检查信号、PDP、LCP Echo和运营商释放原因。

7、第6类:IP路由和DNS错误

这一类的前提是ppp0已经出现并获取IP,说明USB、TTY、AT、注网和PPP主流程基本成功。

1. Network is unreachable
日志
复制代码
ping: sendto: Network is unreachable
分析

Linux内核没有找到到目标地址的有效路由。

判断依据

Network is unreachable通常由本机路由查找直接返回,此时数据包还没有通过ppp0发送给运营商。

它证明问题首先发生在本机接口或路由表,而不是远端服务器是否在线、DNS是否正常或公网链路是否丢包。

优先检查
复制代码
ifconfig ppp0
ip addr show ppp0
route -n
ip route

重点关注:

  • 是否存在指向ppp0的默认路由;
  • WiFi或以太网是否存在更低metric的错误默认路由;
  • ppp0是否处于UP/RUNNING状态;
  • PPP获得的本地IP和对端IP是否正常。
2. ping IP失败
现象和分析

默认路由已经存在,但是ping公网IP失败。

可能原因包括:

  • PPP数据会话已经失效;
  • 默认路由实际没有走ppp0
  • 运营商限制当前数据业务;
  • 对端禁止ICMP;
  • pppd同时出现LCP Echo失败或Modem hangup
判断依据

直接ping公网IP不需要DNS,路由又已经存在,因此可以先排除域名解析失败和完全没有路由两类问题。

剩余范围主要是路由选错出口、PPP数据通道失效、运营商限制,或者目标主机禁用ICMP。

因此,不能只凭ping失败判断公网已经断开,还要结合ip route get以及TCP/UDP测试。

优先检查
复制代码
ip route get 8.8.8.8
ping -I ppp0 8.8.8.8

随后结合TCP/UDP连接测试和pppd日志,确认PPP数据链路是否仍然正常。

3. ping IP成功,ping域名失败
现象和分析

公网IP能够ping通,但是域名无法解析。

复制代码
cat /etc/resolv.conf
判断依据

公网IP能够ping通,证明ppp0、默认路由和基本IP转发路径已经可用。

只有域名访问失败时,IP数据通道与域名解析形成了明确对照,因此故障范围可以收敛到DNS服务器地址、resolv.conf更新或DNS报文可达性。

优先检查
  • pppd是否使用usepeerdns
  • IPCP是否下发DNS地址;
  • /etc/ppp/ip-up或相关脚本是否更新/etc/resolv.conf

18:58

相关推荐
l1t2 小时前
在linux和windows中解决duckdb 1.6dev版本输出执行计划报错问题
linux·运维·数据库·windows·duckdb
柳鲲鹏2 小时前
LINUX高通平台交叉编译地图软件GDAL
linux
fei_sun2 小时前
路径MTU发现
linux·运维·网络
假如梵高是飞行员3 小时前
WSL2 从 img 镜像文件启动特定 Linux 发行版完整指南
linux·windows·wsl
瓶中怪4 小时前
ROS2 机器人软件系统
linux·c++·python·ubuntu·vmware·ros2·机器人软件开发
iangyu4 小时前
linux配置时间同步
linux·运维·服务器
天空'之城5 小时前
Linux 系统编程 04:进程基础
linux·开发语言·进程基础
从零开始的代码生活_5 小时前
NAT、代理服务与内网穿透详解
linux·服务器·网络·c++·http·智能路由器
灯厂码农5 小时前
C语言内存管理——内存对齐与共用体union
linux·服务器·c语言