本篇博客主要对两个外设模块进行分析,分别是 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 等接口类型;
- 维护
wiphy、wireless_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_private,usb_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_serial和usb_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_port。usb_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_serial、usb_interface、usb_device和option驱动。
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 | 模组拒绝请求、固件或配置状态异常 |
优先检查
- 测量3.8 V、VBUS和PWRKEY/RESET;
- 更换USB线缆或减少Hub、转接器;
- 将模组接到PC Linux,比较是否能稳定枚举;
- 用其他USB设备验证i.MX6ULL的
usbotg2; - 完全断电后重新上电,排除模组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_OPTION和CONFIG_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
本项目的option、usb_wwan和usbserial编译进内核,因此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 detected在usb_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,就可以确认原先是端口选择错误。
优先检查
- 根据模组手册确认AT、GPS、DIAG和PPP端口对应关系;
- 使用
fuser检查端口是否被占用; - 使用raw模式发送带
\r结尾的AT; - 查看是否存在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 |
判断依据
CSQ、CREG/CGREG/CEREG和CGATT都是模组协议栈返回的射频、注册和分组域状态。
能够收到这些返回,说明AT通道正常。状态值又把故障限定在无信号、正在搜索、被网络拒绝或未附着数据域。
因此,应检查天线、频段、SIM和运营商侧,而不是USB枚举。
优先检查
- 使用
AT+CSQ确认是否有有效信号; - 检查
CREG/CGREG/CEREG是否已经注册; - 检查
CGATT和数据业务权限; - 对照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 failed:chat脚本没有得到预期响应,可能收到ERROR、NO 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 disconnect且ttyUSB仍存在:优先检查移动信号、PDP会话、运营商释放和PPP Keepalive。
判断依据
Modem hangup只表示pppd认为串行链路已经结束,它本身不能区分USB物理掉线和移动网络释放。
如果同时出现USB disconnect,内核已经给出了设备层断开的证据。
如果USB设备和ttyUSB仍然存在,说明USB物理链路还在,故障更可能发生在模组数据会话或PPP层。
优先检查
- 对齐
pppd和dmesg的时间戳; - 有USB断开时,检查供电、复位和USB链路;
- 无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