Lazy loaded image
USB专题09 - 理解Gadget框架
Words 8878Read Time 23 min
2025-4-29
 
参考资料:
  • 调试软件:USB view 、bushound 、及一些硬件USB信号分析仪
  • 可以用wireshark+usbmon捕捉usb协议数据包。

1. USB Gadget 框架概述

USB Gadget框架是Linux内核中支持设备端USB功能的软件架构,它使Linux设备能够作为USB外设(如U盘、网卡、串口等)被主机识别和使用。与USB主机模式不同,Gadget模式下Linux设备充当USB从设备角色,连接到USB主机(如PC)上。
USB Gadget框架的发展经历了多个版本的演进:
  • 早期的Gadget驱动是单一功能的,每个驱动只能实现一种USB设备功能
  • 随后引入了复合设备支持,允许一个物理设备呈现为多个逻辑USB设备
  • 在Linux 2.6.30引入ConfigFS接口后,Gadget框架实现了更灵活的运行时配置能力
  • 现代Linux内核中的USB Gadget已支持高速(2.0)和超高速(3.0/3.1)USB协议
Gadget框架广泛应用于嵌入式系统、移动设备和开发板,如安卓手机的ADB模式、各种开发板的虚拟网卡/串口功能等。

设计出发点

USB Gadget框架的核心设计理念是提供一个灵活、模块化的架构,解决以下关键问题:
  1. 硬件抽象:屏蔽不同USB控制器硬件的差异,提供统一的软件接口
  1. 功能重用:允许功能模块(如存储、网络、串口等)被不同的Gadget配置复用
  1. 动态配置:支持运行时配置USB设备的各种属性和功能
  1. 协议兼容:确保生成的USB描述符和通信过程符合USB标准规范
Gadget框架采用了分层设计模式,将USB控制器驱动、协议栈和功能实现分离,使开发者能专注于特定层次的开发而无需理解整个系统。

1.1 文件功能和作用

文件名
功能描述
composite.c
实现复合设备功能,允许多个功能组合成一个 USB 设备
config.c
处理 USB 配置相关功能
configfs.c
提供基于 ConfigFS 的 USB Gadget 配置接口
epautoconf.c
自动配置端点(Endpoint)功能
functions.c
管理 USB 功能(Function)的注册和注销
usbstring.c
处理 USB 字符串描述符
u_f.c/u_f.h
提供 USB 功能(Function)的通用工具函数

1.2 功能(Function)目录

function/ 目录包含各种 USB 功能的实现:
文件名
功能描述
f_acm.c
实现 USB CDC ACM(串口通信)功能
f_ecm.c
实现 USB CDC ECM(以太网控制模型)功能
f_eem.c
实现 USB CDC EEM(以太网仿真模型)功能
f_fs.c
实现 USB 功能文件系统,允许用户空间配置 USB 功能
f_mass_storage.c
实现 USB 大容量存储设备功能
f_rndis.c
实现 Microsoft RNDIS(远程网络驱动接口规范)功能
f_uac1.c/f_uac2.c
实现 USB 音频类功能(1.0/2.0 版本)
f_uvc.c
实现 USB 视频类功能

2. 软件架构分析

2.1 核心组件

  1. UDC (USB Device Controller): 硬件抽象层,与具体的 USB 控制器硬件交互
  1. Composite Framework: 管理多个功能的组合
  1. Functions: 实现具体的 USB 功能(如存储、网络、串口等)
  1. ConfigFS 接口: 提供用户空间配置 USB 设备的接口

2.2 USB Gadget 驱动框架

USB Gadget 驱动框架是 Linux 内核中用于实现 USB 设备功能的子系统。在这个框架中,主要包含三层结构:

功能层(Functions)

功能层实现具体的USB设备功能,如:
  • mass_storage.c - USB存储设备功能
  • f_acm.c - USB串口功能
  • f_ecm.c - USB以太网功能
典型功能驱动结构:
Linux内核中常见的UDC驱动包括:
  • dwc3_gadget.c - Synopsys DesignWare USB3控制器驱动
  • musb_gadget.c - Mentor USB控制器驱动
  • pxa27x_udc.c - PXA处理器系列UDC驱动

Gadget核心层

Gadget核心层(drivers/usb/gadget/udc/udc-core.c)负责:
  • 管理UDC驱动的注册与注销
  • 提供标准USB请求的处理框架
  • 管理USB设备状态(如已连接、已配置等)
核心代码结构:

UDC (USB Device Controller) 驱动层

UDC驱动负责抽象底层USB控制器硬件,主要包括:
 
下面从四个不同视角,看看它如何在软硬件层次、描述符构造、描述符获取和数据传输方面提供了完整的解决方案。
这些视角相互关联,共同构成了一个完整的工作流程:
  1. 硬件软件层次构建了基础架构
  1. 描述符构造实现了设备身份定义
  1. 描述符获取处理了与主机的初始交互
  1. 数据传输实现了实际功能

3. 从硬件软件角度理解Gadget框架

USB传输的核心是 endpoint,使用 endpoint 可以收发数据。在 endpoint 之上,就可以模拟USB串口、USB触碰屏、USB摄像头。基于这个角度,Gadget框架可以分为两层:
  • 底层endpoint操作
  • 上层模拟各类USB设备

3.1 底层硬件操作_UDC驱动

对于底层endpoint的代码,需要从UDC驱动开始分析:
下面以IMX6ULL的代码:Linux-4.9.88\drivers\usb\chipidea\ci_hdrc_imx.c为例,ChipIdea (CI) 控制器是一种常见的 USB 控制器实现,在多种 SoC 中被广泛使用,包括 Freescale/NXP 的 i.MX 系列处理器。

3.1.1 主要数据结构

struct ci_hdrc - 主控制器结构体
这是ChipIdea控制器的核心数据结构,包含了控制器的所有状态信息、寄存器映射、端点信息等。从源码中可以看出,该结构体包含以下重要成员:
struct ci_hw_ep - 硬件端点结构体
表示控制器中的物理端点,包含端点状态、DMA缓冲区、请求队列等信息:
struct ci_hw_req - 硬件请求结构体
表示一个USB传输请求,包含请求状态、数据缓冲区等信息:
ChipIdea控制器驱动通过以下方式与USB Gadget框架集成:
  1. 结构体嵌入
      • struct ci_hdrc中嵌入了struct usb_gadget gadget
      • struct ci_hw_ep中嵌入了struct usb_ep ep
      • struct ci_hw_req中嵌入了struct usb_request req
  1. 接口实现
      • 控制器驱动实现了usb_gadget_ops接口,提供了如get_framewakeup等功能
      • 端点实现了usb_ep_ops接口,提供了如queuedequeueset_halt等功能
  1. 数据流
      • Gadget驱动通过usb_ep_queue提交请求
      • 控制器驱动将请求转换为硬件特定的格式并提交给硬件
      • 当传输完成时,控制器驱动通过中断处理程序处理完成事件并通知Gadget驱动
  1. 状态管理
      • 控制器驱动负责管理USB设备状态(如配置、接口等)
      • 当收到USB主机的请求时,控制器驱动会调用Gadget驱动的回调函数

3.1.2 数据结构关系

 
  1. udclist:全局 UDC 列表,维护系统中所有的 USB 设备控制器
  1. udc:USB 设备控制器,代表物理硬件控制器
      • 包含 gadget 设备抽象
      • 负责与硬件交互
  1. gadget:USB 设备抽象
      • 包含端点列表 (ep_list)
      • 实现 usb_gadget 接口
      • 通过 gadget_ops 提供操作函数
  1. ep_list:端点列表
      • 包含多个 endpoint 结构
      • 管理设备的所有端点
  1. endpoint:USB 端点
      • 通过 ep_ops 提供端点操作函数
      • 处理数据传输请求

3.1.3 初始化流程

函数调用栈说明
  1. ci_hdrc_imx_probe
      • IMX平台特定的探测函数,负责初始化IMX平台上的ChipIdea控制器
      • 分配和初始化平台资源(中断、时钟、GPIO等)
  1. ci_hdrc_add_device
      • 创建并注册一个新的平台设备
      • 使用 platform_device_alloc("ci_hdrc", id) 分配平台设备
  1. platform_device_alloc("ci_hdrc", id)
      • 分配一个新的平台设备结构体,名称为"ci_hdrc"
      • 这将触发 ci_hdrc_driver 的probe函数
  1. ci_hdrc_probe
      • 控制器的主要初始化函数
      • 分配和初始化 ci_hdrc 结构体
      • 映射寄存器空间
      • 初始化硬件
      • 调用 hw_device_init 初始化设备硬件
      • 调用 ci_hdrc_gadget_init 初始化gadget驱动
  1. ci_hdrc_gadget_init
      • 初始化gadget相关的数据结构
      • 设置端点操作函数
      • 注册gadget设备到USB子系统
      • 最终调用 udc_start 启动UDC
  1. udc_start
      • 启动USB设备控制器
      • 连接到USB总线,准备好处理USB请求
notion image

3.2 上层软件操作

模拟各类USB设备时,软件怎么分层?以访问设备、获取描述符为例:
  • Host要分配地址、把地址发送给设备:不管要模拟什么设备,Gadget都必须接收地址,这部分由usb_gadget(硬件相关的驱动程序)实现
  • Host要读取各类描述符,这些描述符是由上层的驱动程序提供的
  • 怎么把上层的描述符通过底层硬件usb_gadget传回给Host?因此在USB Function Driver 还需要一个协议处理层 (usb_gadget_driver)。
所以,从获取描述符的角度看看,上层软件至少分为2层:
  • usb_gadget_driver:实现标准USB协议的通用操作,处理标准请求,如描述符获取,作为上层应用和底层硬件之间的桥梁
  • 在这上面提供各类描述符,实际上,描述符的提供还可以分为两层:
    • 提供设备描述符和配置描述符:由程序员决定,由复合设备层 (usb_composite_driver)提供
    • 接口描述符、endpoint描述符:由内核事先实现的、通用的存储、网络、串口等功能由驱动层 (function driver)提供
软件层次可以进一步细化,如下图:
notion image
这涉及2个结构体:
  • usb_composite_dev:它里面汇集有各类描述符、有一个usb_funciton链表(实现数据传输)
    • usb_udc:UDC的本意是"usb device controller",usb_udc结构体里面有usb_gadget(表示UDC本身)、usb_gadget_driver(表示UDC driver)

      4. 从构造描述符的角度理解Gadget框架

      假设你要模拟一个USB设备,USB设备需要通过描述符向主机表明自己的身份和功能
      • 这个USB设备含有厂家信息:它记录在设备描述符里,所以设备描述符应该由开发者提供
      • 这个芯片可能有多种配置,这也是由你决定,所以配置描述符应该由开发者提供
      • 某个配置下多个接口,接口就是功能,Linux内核里事先提供了很多功能的驱动程序,所以:接口描述符是内核提供的
      • 某个接口下需要什么端点,也是内核里各类功能的驱动程序提供的

      4.1 描述符层次结构

      4.2 Gadget框架中的描述符构造

      Gadget框架提供了一套完整的描述符构造机制,在Gadget框架中,描述符构造遵循从底向上的组装过程:
      1. 功能层定义接口和端点:每个功能模块定义自己需要的接口和端点
        1. Composite框架组装配置描述符:将多个功能的接口和端点组合成配置描述符
          1. Gadget核心提供设备描述符:设备级描述符由Gadget核心根据注册信息生成

            4.3 ConfigFS接口下的描述符构造

            在现代Linux系统中,更常用的是通过ConfigFS接口动态构造描述符:
            1. 用户空间挂载ConfigFS并创建Gadget配置
              1. 内核将用户空间配置转换为内部描述符结构

                4.4 zero.c 文件分析

                以zero.c为例:
                • 配置1:loopback,Host写数据给它,就可以读出原样的数据
                • 配置2:sourcesink,Host写数据给它(它只是记录下数据),Host还可以读数据(读到的都是0)
                从下到上涉及这些文件:
                 
                函数调用过程中主要的函数如下,重点关注"xxx_bind"函数,可以认为bind就是初始化的意思:
                • usb_composite_probe
                • composite_bind
                • zero_bind
                • sourcesink_bind/loopback_bind
                 
                notion image
                深入解读描述符的构造过程,可以得到下面的图:
                • 构造出一个usb_composite_dev结构体
                • 它把各层串联起来,里面构造有设备描述符、配置描述符、接口描述符、端点描述符
                notion image

                5. 从获取描述符的角度理解Gadget框架

                安装好gadget驱动程序后(比如modprobe g_zero),它只是构造好了各类描述符。在设备的枚举过程会读取描述符,枚举过程要做的事情可以参考
                使用OTG线连接电脑和开发板时,电脑软件会执行如下操作:
                • 使用控制传输,读取设备信息(设备描述符):第一次读取时,它只需要得到8字节数据,因为第8个数据表示端点0能传输的最大数据长度。
                • Host分配地址给设备,然后把新地址发给设备
                • 使用新地址,重新读取设备描述符,设备描述符长度是18
                • 读取配置描述符:它传入的长度是255,想一次性把当前配置描述符、它下面的接口描述符、端点描述符全部读出来
                • 读取字符描述符
                上述过程里,设备方都是接收到Host发给endpoint 0的数据,然后做出回应。不同的Gadget设备,在返回描述符给主机时,这些操作都是一样的,只是回应的数据不同而已。源码分析的起点都是某个中断函数:
                • IMX6ULL:ci_irq(drivers/usb/chipidea/core.c)
                • STM32MP157: dwc2_hsotg_irq(drivers/usb/dwc2/gadget.c)

                5.1 IMX6ULL的核心函数

                IMX6ULL芯片中USB控制器型号是chipidea,在Linux-4.9.88\drivers\usb\chipidea\core.c中注册了中断函数:
                发生中断后,对于endpoint 0的数据处理流程如下:
                函数isr_setup_packet_handler就是处理endpoint 0接收到的控制传输的关键。

                5.2 STM32MP157的核心函数

                STM32MP157芯片中USB控制器型号是dwc2,在Linux-5.4\drivers\usb\dwc2\gadget.c中注册了中断函数:
                发生中断后,函数dwc2_hsotg_irq被调用,它处理endpoint中断有两种方法:
                • 使用DMA时:调用dwc2_hsotg_epint来处理
                • 不使用DMA时:调用dwc2_hsotg_handle_rx来处理
                dwc2_hsotg_epint为例进行分析,对于endpoint 0的数据处理流程如下:
                函数dwc2_hsotg_epint中,对于endpoint 0的处理如下:
                函数dwc2_hsotg_enqueue_setup被调用时,Gadget设备已经收到了SETUP令牌包,但是还没收到DATA0令牌包。dwc2_hsotg_enqueue_setup的作用是设置、启动一个request,核心在于设置了requestcomplete函数(当SETTUP事务完成后这个函数被调用):
                notion image
                当控制传输的"setup事务"完成时,函数dwc2_hsotg_complete_setup被调用。

                5.3 如何处理控制传输

                无论是IMX6ULL的函数isr_setup_packet_handler,还是STM32M157的函数dwc2_hsotg_complete_setup,它们都是在Gadget设备收到"SETUP事务"后才被调用。接收完"SETUP事务"后,就可以从里面知道这个控制传输想做什么(req.bRequest是什么),然后就可以处理它了。
                怎么处理呢?可以分为3层:
                notion image
                • UDC驱动程序:类似"设置地址"的控制传输,在底层的UDC驱动程序里就可以处理,
                  • 这类请求有:
                    • 驱动程序位置
                    • gadget driver:涉及描述符的操作
                      • 这类请求有:
                        • 驱动程序位置
                        • usb_configuration或usb_function的处理:这是二选一的。大部分设备使用控制传输实现标准的USB请求,但是也可以用控制传输来进行实现相关的请求,对于这些非标准的请求,就需要上层驱动来处理。

                        6. 从数据传输的角度理解Gadget框架

                        USB设备枚举完成后,主要功能是进行数据传输。Gadget框架如何处理各种传输类型?

                        6.1 使用流程

                        在USB协议中,永远是Host主动发起传输。作为一个Gadget驱动程序,它永远都是这样:
                        • 想接收数据:
                          • 先构造好usb_request:分配buffer、设置回调函数
                          • 把usb_request放入队列
                          • UDC和Host完成USB传输,在usb_request中填充数据,并触发中断调用usb_request的回调函数
                        • 想发送数据:
                          • 先构造好usb_request:分配buffer、在buffer里填充数据、设置回调函数
                          • 把usb_request放入队列
                          • UDC和Host完成USB传输,把usb_request的数据发给Host,并触发中断调用usb_request的回调函数

                        6.2 USB Endpoint 传输流程

                        基于Linux内核源码中的f_acm.c文件,我将详细分析USB传输中endpoint的使用流程,并提供完整的函数调用栈及其含义说明。
                        USB传输的对象是endpoint,使用流程主要包括以下四个步骤:
                        1. 功能驱动通过endpoint描述符表明需要的endpoint类型
                        1. 功能驱动的bind函数根据endpoint描述符向底层申请分配endpoint
                        1. 功能驱动使能endpoint
                        1. 功能驱动给endpoint分配buffer、设置usb_request、提交usb_request

                        6.2.1 通过endpoint描述符表明需要的endpoint类型

                        函数调用栈:
                        含义说明:
                        • 功能驱动首先定义endpoint描述符,指定endpoint的属性
                        • bEndpointAddress字段指定端点地址和方向(IN/OUT)
                        • bmAttributes字段指定传输类型(批量、中断、等时等)
                        • wMaxPacketSize字段指定最大包大小
                        • bInterval字段指定轮询间隔(对中断和等时传输)
                        • 这些描述符会在USB枚举过程中发送给主机,告诉主机该设备需要什么样的endpoint

                        6.2.2 bind函数根据endpoint描述符向底层申请分配endpoint

                        函数调用栈:
                        关键代码:
                        含义说明:
                        • acm_bind()函数在设备连接到主机时被调用
                        • usb_interface_id()分配接口ID,用于区分不同的接口
                        • usb_ep_autoconfig()根据描述符要求自动配置物理endpoint
                          • 该函数会查找符合要求(方向、类型等)的物理endpoint
                          • 配置成功后,endpoint地址会被填入描述符中
                        • gs_alloc_req()为通知端点分配请求结构
                          • 通过usb_ep_alloc_request()分配USB请求结构
                          • 设置请求完成回调函数

                        6.2.3 功能驱动使能endpoint

                        函数调用栈:
                        关键代码:
                        含义说明:
                        • acm_set_alt()在主机选择接口的备用设置时被调用
                        • config_ep_by_speed()根据当前速度(全速/高速/超速)配置端点
                          • 选择合适的端点描述符(全速/高速/超速)
                          • 将描述符与端点关联
                        • usb_ep_enable()使能物理endpoint,准备数据传输
                          • 调用底层控制器的enable操作
                          • 初始化endpoint的硬件资源
                        • gserial_connect()连接串行端口,准备数据传输

                        6.2.4 给endpoint分配buffer、设置usb_request、提交usb_request

                        函数调用栈:
                        关键代码:
                        含义说明:
                        • acm_cdc_notify()用于发送CDC通知(如串行状态变化)
                          • 使用预先分配的请求结构
                          • 设置请求的数据缓冲区和长度
                          • 通过usb_ep_queue()提交请求
                        • gs_start_io()启动I/O操作
                          • gs_alloc_req()分配请求结构
                          • gs_alloc_buf()分配数据缓冲区
                          • 设置请求的完成回调函数
                          • 通过usb_ep_queue()提交请求
                        • 完成回调函数(gs_recv_completegs_write_complete
                          • 处理传输完成的数据
                          • 重新提交请求,实现连续传输

                        6.2.5 完整流程示例:ACM通知端点的使用

                        1. 描述符定义
                          1. 端点分配(在acm_bind中):
                            1. 端点使能(在acm_set_alt中):
                              1. 请求提交(在acm_cdc_notify中):

                                6.3 回调函数

                                功能驱动里构造的usb_request,可以是接收Host发来的数据,也可以是向Host发送数据。当传输完成,usb_request的回调函数被调用。在回调函数里,可以再次提交usb_request。
                                怎么调用到回调函数?源头是UDC的中断函数。参考IMX6ULL 和 STM32MP157函数调用栈进行分析。

                                6.3.1 IMX6ULL

                                调用关系如下:

                                6.3.2 STM32MP157

                                调用关系如下:

                                6.4 loopback分析

                                6.4.1 Gadget接收数据

                                loopback功能的核心是将从USB主机接收到的数据原样发送回主机。从源码分析,数据接收流程如下:
                                1. 初始化阶段
                                    • loopback_bind函数中,通过usb_ep_autoconfig配置输入和输出端点
                                    • alloc_requests函数中,为每个端点分配请求缓冲区
                                1. 数据接收过程
                                    • 当USB主机发送数据到设备时,数据到达OUT端点
                                    • loopback_complete回调函数处理接收到的数据
                                    • 关键代码片段:

                                6.4.2 Gadget回环数据

                                loopback功能的回环机制实现如下:
                                1. 请求关联
                                    • alloc_requests函数中,IN和OUT请求被关联起来:
                                1. 数据回环过程
                                    • 当OUT端点接收到数据后,loopback_complete回调将数据长度设置到IN请求中
                                    • 然后将IN请求提交到IN端点,将数据发送回主机
                                    • 当IN请求完成后,再次提交OUT请求等待新的数据
                                    notion image
                                1. 请求队列管理
                                    • 系统维护一个请求队列,通过usb_ep_queue将请求提交到端点
                                    • 通过qlen参数控制队列中的请求数量,提高吞吐量

                                6.5 f_sourcesink分析

                                前面的f_loopback也实现了两个方向的数据传输:Host到Gadget、Gadget到Host,但是它们之间是有依赖关系的,Host必须先发送数据再读数据。
                                f_sourcesink.c也实现了两个方向的数据传输:Host到Gadget、Gadget到Host,它们是独立的。
                                • Host读Gadget:驱动程序里构造好数据,Host可以读到,Gadget作为源(就是source)
                                • Host写Gadget:驱动程序里得到Host发来的数据,Gadget作为目的(就是sink)
                                source_sink功能比loopback更复杂,它不仅可以回环数据,还可以生成特定模式的数据。

                                6.5.1 Host与Gadget通信

                                1. 端点配置
                                    • source_sink支持多种速度(全速、高速、超速)的端点配置
                                    • 支持两种接口配置(alt0和alt1),alt1增加了同步传输端点
                                1. 数据传输方向
                                    • source(IN端点):从设备向主机发送数据
                                    • sink(OUT端点):从主机接收数据到设备
                                1. 接口描述符

                                  6.5.2 Host读Gadget

                                  1. 数据源生成
                                      • source_sink可以根据配置生成不同模式的数据发送给主机
                                      • 支持多种数据模式,如零填充、随机数据等
                                  1. 请求处理
                                      • 当主机请求读取数据时,设备通过IN端点发送数据
                                      • 数据可以是预先生成的模式数据,也可以是从OUT端点接收到的数据
                                  1. 完成回调
                                      • 当数据传输完成后,通过回调函数处理后续操作
                                      • 可以继续提交新的请求或执行其他操作

                                  6.6 不同传输类型的处理

                                  USB支持四种传输类型,Gadget框架对它们的支持方式如下:
                                  1. 控制传输:主要由Gadget核心的ep0处理逻辑实现
                                    1. 批量传输:最常用于高速数据传输,如存储设备
                                      1. 中断传输:用于低延迟但非周期性的小数据量传输,如HID设备
                                        1. 等时传输:用于有固定带宽要求的实时数据,如音频/视频

                                          7. 总结

                                          Linux USB Gadget 驱动框架采用模块化设计,通过 Composite 框架支持多功能组合,通过 ConfigFS 提供灵活的用户空间配置接口。核心组件包括 UDC 驱动、Gadget 核心、功能驱动和配置接口。该框架支持多种 USB 功能,如大容量存储、网络、串口、音频和视频等,能够满足各种嵌入式设备的 USB 设备端需求。
                                          上一篇
                                          模板设计模式:让你的代码结构更清晰
                                          下一篇
                                          Guide to Linux System

                                          Comments
                                          Loading...