外设的常见问题

本篇博客主要对两个外设模块进行分析,分别是 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

相关推荐
FoldWinCard5 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计7 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud1237 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质7 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX7 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.7 小时前
Linux系统管理
linux·运维
阿龙的工作记录8 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手8 小时前
Linux的基本指令
linux·运维·服务器
hkj88088 小时前
Ubuntu 切换java版本
java·linux·ubuntu
撩得Android一次心动10 小时前
Linux编程笔记3【个人用】
linux·笔记