第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,就是连接这两层的桥梁。
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:分配释放requestusb_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回调在驱动注册时被调用。你在这里需要:
- 检查gadget的可用端点资源
- 调用
usb_ep_autoconfig为你的功能分配端点 - 调用
usb_ep_enable使能端点 - 分配一些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,把这个头文件里的结构体和函数声明通读一遍。我当年就是这么干的,虽然一开始有些眼花,但读完之后对整个框架的边界和可能性就有了清晰的认知。