3、USB子系统概览:USB协议基础、Linux USB驱动架构、USB设备枚举过程
好,咱们进入第三章。这一章我打算聊聊USB子系统。说实话,做车载4G/5G模组驱动,USB是你绕不过去的一道坎。为什么?因为绝大多数模组跟主控芯片通信,走的就是USB接口。你想想看,模组插上去,系统怎么认出它是个网卡?怎么知道它能发AT指令?这些底层逻辑,全在USB子系统里。
我个人习惯,在讲具体移植之前,先把USB这套东西捋一遍。不然你后面遇到问题,连日志都看不懂,那就尴尬了。
3.1 USB协议基础——别被那些电气特性吓到
USB协议,说白了就是一套主从通信的规矩。主机(Host)说了算,设备(Device)乖乖听话。咱们做模组驱动,设备端就是模组,主机端就是你的车机SoC。
这里有几个核心概念,我帮你拎出来:
- 端点(Endpoint):数据通信的最小单元。每个端点就是一个数据缓冲区,有方向(IN或OUT),有类型。模组通常会有好几个端点,比如一个发AT指令,一个传网络数据。
- 接口(Interface):一组端点的集合。一个设备可以有多个接口。比如4G模组,可能同时暴露一个ACM接口(用于AT指令)和一个ECM/RNDIS接口(用于网络数据)。
- 配置(Configuration):一个设备可以有多种工作模式,每种模式就是一个配置。切换配置,相当于切换模组的工作状态。
我记得有一次调试一个5G模组,插上后系统只识别出一个串口,网络功能死活出不来。后来一查,原来是模组默认配置只开了ACM接口,需要发一条特殊命令切换到另一个配置才能启用网络接口。嗯,这就是配置切换的典型场景。
USB的传输类型有四种,咱们做模组驱动主要关注两种:
| 传输类型 | 特点 | 模组驱动中的应用 |
|---|---|---|
| 控制传输 | 可靠,数据量小 | 枚举过程、发送AT指令(有时) |
| 批量传输 | 可靠,数据量大 | 网络数据包收发 |
| 中断传输 | 可靠,延迟低 | 状态通知、URC上报 |
| 同步传输 | 不可靠,实时性高 | 音频流(模组一般不用) |
小提示: 模组的网络数据走的是批量传输端点。你如果看到驱动里申请URB时用了 usb_bulk_msg(),那就是在干这个活。
3.2 Linux USB驱动架构——分层,全是分层
Linux的USB驱动架构,说白了就是三层:
- USB主机控制器驱动(HCD):最底层,直接跟硬件寄存器打交道。咱们做模组移植基本不动这层,除非你换主控芯片。
- USB核心层(USB Core):中间层,负责USB总线管理、设备枚举、URB调度。这层是内核帮你搞定的,你只需要调用API。
- USB设备驱动:最上层,也就是咱们要写的模组驱动。你注册一个
usb_driver结构体,告诉内核:这个模组的VID/PID是多少,我负责驱动它。
我刚开始做驱动时,总想搞清楚每一行代码。后来发现,其实你只需要关注最上层。核心层那些复杂的调度算法,你暂时不用管。你想想看,你开车不需要懂发动机怎么点火吧?一个道理。
咱们写模组驱动,核心就是填充这个结构体:
static struct usb_driver modemu_driver = {
.name = "modemu",
.probe = modemu_probe,
.disconnect = modemu_disconnect,
.id_table = modemu_id_table,
};
probe 函数是入口。模组插上,枚举成功,内核就会调用你的 probe。你在里面干什么?注册网络设备、创建tty设备、初始化端点。说白了,就是告诉系统:这个模组能干什么。
注意: 模组的VID/PID一定要写对。我曾经见过一个同事,把PID写反了,结果驱动死活不加载。查了两天才发现是0x1234写成了0x1243。这种低级错误,真的会让人崩溃。
3.3 USB设备枚举过程——从插上到能用,发生了什么?
模组插上USB口,到系统识别出/dev/ttyUSB0或者eth1,中间发生了什么?这个过程叫枚举。我把它拆成几步,你记一下:
- 电气连接:模组插入,主机检测到D+或D-上的电平变化。嗯,硬件层面的事。
- 复位设备:主机发送复位信号,模组地址重置为0。
- 获取设备描述符:主机发控制请求,拿到模组的VID、PID、bcdDevice等信息。这一步决定了你的
id_table能不能匹配上。 - 分配地址:主机给模组分配一个唯一地址(1-127)。以后通信就用这个地址了。
- 获取配置描述符:主机问模组:你有几个配置?每个配置里有什么接口?接口里有什么端点?
- 选择配置:主机选择一个配置,模组进入工作状态。
- 加载驱动:内核根据VID/PID匹配驱动,调用
probe函数。
整个过程,你可以在内核日志里看到。用 dmesg 命令,会输出类似这样的信息:
[ 1234.567890] usb 1-1: new high-speed USB device number 5 using xhci_hcd
[ 1234.568901] usb 1-1: New USB device found, idVendor=0x2c7c, idProduct=0x0125
[ 1234.569012] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 1234.570123] usb 1-1: Product: Quectel EM05-CE
[ 1234.571234] usb 1-1: Manufacturer: Quectel
[ 1234.572345] option 1-1:1.0: GSM modem (1-port) converter detected
看到没?从 "new high-speed USB device" 到 "GSM modem converter detected",就是完整的枚举过程。我调试模组时,第一件事就是插上模组看 dmesg。如果枚举都没过,后面就不用看了。
核心要点: 枚举失败,90%的原因是硬件问题(供电不足、信号线接反)或者VID/PID不匹配。别一上来就怀疑驱动代码,先看看枚举日志。
3.4 实战中你可能会踩的坑
讲完理论,我分享几个实际项目中遇到的坑:
- 模组枚举慢:有些模组上电后需要几秒才能完成初始化。如果主机在模组准备好之前就发起了枚举,会失败。解决办法是在驱动里加一个延时,或者让模组的PWRKEY引脚晚点拉高。
- 多个接口的匹配问题:一个模组可能有多个接口(ACM、ECM、MBIM)。每个接口都需要一个子驱动。你注册的
usb_driver可能只匹配其中一个接口,其他接口需要另外的驱动来处理。 - URB提交失败:批量传输时,如果URB提交失败,检查一下端点地址对不对。我曾经犯过一个错,把IN端点地址写成了OUT端点,结果数据死活发不出去。
嗯,这一章的内容差不多就这些。USB子系统是个大话题,但咱们做模组驱动,掌握这些就够了。下一章我会带你手写一个模组驱动的probe函数,到时候你就知道这些概念怎么落地了。