第3章 USB Gadget框架介绍:Linux内核中的gadget API

上一章我们从宏观上了解了USB Gadget能做什么。这一章,我们得深入内核看看,这个能力到底是怎么暴露给驱动开发者的。

说白了,Linux内核的USB Gadget API就是一套标准接口。它帮我们屏蔽了底层硬件的差异。你写一个gadget功能驱动,只要调这套API就行。硬件是Freescale的、TI的、还是Synopsys的?不用管。API帮你搞定。

我个人觉得,理解gadget API最好的方式,就是先把它当成一个分层框架来看。

核心概念: Linux USB Gadget 框架采用分层设计。上层是功能驱动(比如ECM、ACM、Mass Storage),下层是UDC(USB Device Controller)驱动。中间的gadget API,就是连接这两层的桥梁。

USB Gadget 框架分层结构图 Linux USB Gadget 框架分层 Gadget Function 层 ECM, ACM, Mass Storage, Audio, RNDIS ... Gadget API 层 usb_gadget, usb_ep, usb_request ... UDC 层 dwc2, dwc3, chipidea, renesas_usb3 ... USB 硬件控制器 分层职责 ◦ 上层:实现具体功能 ◦ 中层:抽象接口 ◦ 下层:硬件适配 ◦ 底层:物理传输 驱动开发关注点 ◦ 写 Function 驱动 ◦ 调用 Gadget API ◦ 配置端点 ◦ 处理请求 数据流向:来自主机 → 硬件 → UDC → API → 功能驱动

3.1 核心数据结构的门道

框架好不好用,关键看它怎么定义核心对象。Linux gadget API里,最重要的三个是usb_gadget、usb_ep和usb_request。

我给你列个表,咱们对比着看:

数据结构 代表什么 关键字段/方法 我常遇到的坑
usb_gadget 整个USB设备控制器实例 name, ops, ep0, speed 不要直接操作它。它由UDC驱动管理
usb_ep 一个USB端点 name, ops, maxpacket, caps 端点号分配不对,会导致枚举失败
usb_request 一次数据传输请求 buf, length, complete, actual buf 必须DMA对齐,否则会段错误

usb_gadget是整个框架的“根”。你想想看,它代表的就是一个真实的USB硬件控制器。它有个ops结构体指针,指向底层的硬件操作函数。我们平时写功能驱动,基本碰不到它。

usb_ep是端点的抽象。每个端点都是一条管道。端点0是控制端点,默认就存在。其他端点需要你在代码里主动申请。我在做复合设备的时候,就踩过这个坑——端点号冲突了,导致第二个功能死活枚举不上。

小经验:端点有方向(IN/OUT)和类型(BULK/INT/ISO/CTRL)。配置时一定要看芯片datasheet,有些硬件对端点类型有限制。比如,某些USB IP核只支持特定端点做ISO传输。

usb_request才是我们打交道最多的。说白了,它就是个异步请求包。你申请一个request,把要发的数据放在buf里,填好length,挂到某个端点上,然后就等回调。回调里检查actual字段,看实际传输了多少字节。

3.2 API层次:从下往上怎么看

我教学的时候,喜欢从最底层往上看。咱们也这么来。

第一层:UDC 驱动层

这一层直接和硬件寄存器打交道。它需要实现usb_gadget_ops结构体里的一堆回调函数。像udc_start、udc_stop、pullup这些。作为功能驱动开发者,这一层我们基本不用碰。但你要知道它存在。内核已经帮我们集成了DWC2、DWC3、ChipIdea这些常见控制器的驱动了。

第二层:Gadget API 层

这一层就是我们写功能驱动的主战场。核心API包括:

  • usb_ep_enable/usb_ep_disable:使能或禁用端点
  • usb_ep_alloc_request/usb_ep_free_request:分配释放request
  • usb_ep_queue:提交一个请求到端点队列
  • usb_ep_dequeue:取消一个未完成的请求
  • usb_gadget_activate/usb_gadget_deactivate:激活/休眠gadget

你想想看,这套API设计的挺巧妙的。它把USB协议里复杂的包交互、握手、重传都封装掉了。你只需要调queue,然后就等着complete回调就行了。

第三层:Function 驱动层

这里就比较自由了。内核有现成的function驱动,你直接拿来用就行。比如:

  • u_ether.c 提供以太网功能
  • u_serial.c 提供串口功能
  • f_mass_storage.c 提供U盘功能

如果你想自己写一个独特的USB设备,比如一个自定义的数据采集器,那你就要在这一层实现一个新的function驱动。

3.3 核心回调和事件处理

写gadget驱动,本质上就是写回调函数。内核gadget框架定义了几个关键的回调点:

/* 一个典型的gadget function驱动结构 */
static struct usb_gadget_driver my_driver = {
    .function =  "my_usb_gadget",   // 驱动名称
    .max_speed = USB_SPEED_HIGH,    // 支持高速
    .bind      = my_bind,           // 绑定回调,配置端点
    .unbind    = my_unbind,         // 解绑回调,释放资源
    .setup     = my_setup,          // 处理标准/类请求
    .suspend   = my_suspend,        // 挂起回调
    .resume    = my_resume,         // 唤醒回调
    .reset     = my_reset,          // 总线复位回调
};

这里面,bind和setup是最重要的两个。

bind回调在驱动注册时被调用。你在这里需要:

  1. 检查gadget的可用端点资源
  2. 调用usb_ep_autoconfig为你的功能分配端点
  3. 调用usb_ep_enable使能端点
  4. 分配一些request并做好准备

setup回调处理所有来自主机的控制请求(Setup包)。标准请求(比如获取描述符、设置地址)内核框架帮你处理了。你只需要处理类请求和厂商自定义请求。比如,一个CDC ACM设备,就要在setup里处理SEND_ENCAPSULATED_COMMAND这些类请求。

我曾经踩过的一个坑: 在setup回调里,如果处理完请求需要回复数据,一定要注意数据长度和端点0的maxpacket的关系。长度不对的话,主机那边会收错数据或者一直等待。处理完请求后,一定要调用usb_ep_queue把响应提交到端点0。很多初学者会忘记这一步。

3.4 事件处理与数据流动

整个gadget框架是事件驱动的。你看它的名字就知道了——request提交后,等complete回调。这个模型和我们写单片机裸机程序有点像,但更规范。

数据从主机发来时:主机发起OUT传输 → UDC硬件收到数据 → 中断通知 → gadget框架调度complete回调 → 你的代码在回调里处理数据。

数据发往主机时:你的代码调usb_ep_queue → 框架把request挂到硬件队列 → UDC硬件在合适时机发起IN传输 → 传输完成 → complete回调通知你。

整套流程非常流畅。我个人习惯是在complete回调里立即提交一个新的request,这样就能持续不断地收发数据,实现一个数据流的管道的效果。

3.5 小结:站在框架的肩膀上

嗯,这一章我们理清了Linux USB Gadget API的脉络。从数据结构到API层次,再到回调事件处理。有了这个框架认知,下一章我们就能动手写一个最简单的gadget驱动了。

记住,学习这套API最好的方式就是打开内核源码,找到include/linux/usb/gadget.h,把这个头文件里的结构体和函数声明通读一遍。我当年就是这么干的,虽然一开始有些眼花,但读完之后对整个框架的边界和可能性就有了清晰的认知。