Linux USB设备驱动设计
本文是Linux内核&内核驱动开发的第一篇专栏文章。
0、前言
目前无论是在PC、手持终端,还是在嵌入式领域,USB设备都被广泛应用。本文暂时抛开大量USB协议、概念等细节,从最基础的框架入手介绍如何设计设备驱动。在丰富细节功能的同时,逐步介绍涉及到的概念。
本文适合对Linux驱动、USB设备有所了解的开发人员,也适合USB产品设计者。
表0-1 术语、概念、缩略语
![717355f8b37714124fde2279838e95da.png](https://img-blog.csdnimg.cn/img_convert/717355f8b37714124fde2279838e95da.png)
1、准备工作
要开始USB设备驱动设计,首先需要有个USB设备。对于刚开始进行设备驱动开发学习的同学而言,不太容易找到现成的物理USB设备来做为练习使用;即是手头上有现成的优盘、音视频小设备,也会因为不清楚厂商的设计细节,无法拿来练手。出于这种目的,我专门针对模拟器设计了一个USB Sample的设备,将它实现一个通用输入输出功能(General IO)设备。基本功能比较简单:可进行基本设置与信息获取(速率,环回),可以将数据送出,并获取其接收到的数据。
如何在模拟器上模拟出一个USB设备,不是本文的主要内容(或许在下一篇专栏文章中介绍)。需要强调的是,使用软件方式模拟设备的做法在很多场景下非常有用,能够大大加速产品设计、开发进度;能够在真正投产(流片)之前,便完成功能设计、验证等工作。
设备模拟完成之后,启动GUEST操作系统,可以通过lsusb查看系统中所有的USB设备,如下表1-1所示。
表1-1模拟USB设备
/ # lsusb
Bus 001 Device 004: ID efb8:f201
Bus 001 Device 001: ID 1d6b:0001
Bus 001 Device 002: ID efb8:f102
Bus 001 Device 003: ID 0409:55aa
在USB规范以及Linux内核,USB设备对象相关概念关系可以用下图来表示,辅助理解。如下图1-1所示。
![f17502ad65256a99c8426b1b7aaab63c.png](https://img-blog.csdnimg.cn/img_convert/f17502ad65256a99c8426b1b7aaab63c.png)
图1-1设备-配置-接口-端点
某些设备,会有多种配置、多种接口;多数设备则往往只有一种配置,一个接口,若干个端点。这里,配置可以理解为设备的工作模式,接口专指功能,端点表征传输通道。举例说明这种功能,某一个USB视音频卡,只存在一个配置,接口0负责视频处理传输,接口1负责音频处理传输;两个接口分别有三个端点:控制端点、输入端点、输出端点。所以在Linux内核驱动中,probe函数接受的是usb_interface对象而不是usb_device对象:接口才表征了一个独立的功能。
USB Sample设备(以下简称usample设备)设计功能如下:设备只包含一个配置(Configuration),该配置中只包含一个接口(Interface),该接口中除控制端点之外,还包含两个端点(Endopint),端点1的属性为入向(in)批传输类型(bulk transfer),端点2的属性为出向(out)批传输类型;传输的最大包长均为64字节。
2、设备探测
系统启动时,HCD(Host Controller Driver)驱动会对挂载在USB总线上的设备进行枚举(热插拔同样会触发这个过程),对发现的设备将会创建一个usb_device设备对象(device,interface,endpoint三者之间的关系见第一章描述)并记录下来。通过lsusb命令,我们可以看到设备信息:本例usample是处于第1号总线的第004号设备,其Vendor厂商ID为efb8,Product产品ID为f201(每个厂商都有向IEEE组织申请的唯一ID,这两个组合可以用于确定设备类型)。此处,efb8以及f201为我们在模拟设备时指定的ID,取的数值偏大用来避开真实产品ID。
代码2-1 usample设备ID定义
#define PCI_VENDOR_ID_LTRIANGLE 0xefb8
#define PCI_DEVICE_ID_LTRIANGLE_USAMPLE 0xf201
USB驱动中,往往先要定义一个设备ID表,用于内核匹配使用。Usample驱动文件中定义的设备ID表如下所示:
代码2-2驱动支持设备列表
static const struct usb_device_id id_table[] = {
{ .idVendor = PCI_VENDOR_ID_LTRIANGLE,
.idProduct = PCI_DEVICE_ID_LTRIANGLE_USAMPLE,
.match_flags = USB_DEVICE_ID_MATCH_VENDOR|USB_DEVICE_ID_MATCH_PRODUCT,
},
};
MODULE_DEVICE_TABLE(usb, id_table);
当内核驱动被加载或者新USB设备被发现时,内核将对USB设备信息与设备驱动ID Table中的信息进行比对,如果匹配则使用驱动中的probe探测函数对设备进行探测。Match_flags表明需要匹配厂商ID以及产品ID。
当内核驱动被加载或者新的设备被发现时,内核将会做探测动作,行为可以用如下伪代码来描述:
新驱动模块插入与新设备被发现时,内核动作如下述伪代码描述。for_each_*表示遍历内核对象列表。
代码2-3驱动与设备注册流程
register_usb_driver(usb_driver) {
for_each_device(usb_device) {
if(match(usb_device<idVendor, idProduct>, usb_driver.id_table))
usb_driver->probe(usb_device);
}
}
register_usb_device(usb_device) {
for_each_driver(usb_driver) {
if(match(usb_device<idVendor, idProduct>, usb_driver.id_table))
usb_driver->probe(usb_device);
}
}
新注册驱动或者设备时,内核遍历设备或者驱动,进行ID匹配、探测处理。因此,我们并不用担心驱动注册或者设备发现两个事件发生的先后时序关系。
Linux下的USB设备驱动已经被抽象为一个名为usb_driver的结构体,如下所示。提供usb_driver结构体中的函数接口实现,即可为内核usbcore所用,实现特定USB设备的管理。
代码2-4 USB Sample驱动结构定义
static struct usb_driver usample_driver = {
.name = "usample",
.probe = usample_probe,
.disconnect = usample_disconnect,
.suspend = usample_suspend,
.resume = usample_resume,
.id_table = id_table,
.supports_autosuspend = 1,
};
module_usb_driver(usample_driver);
Name为驱动名称,在内核中应当唯一,不应有同名驱动存在。Probe函数接口,用于给设备驱动提供决策,是否愿意管理该USB设备下的一个接口(interface)。这里我们可以看出,usb_driver面对的对象粒度是接口,而不是设备(device)。Disconnect:当设备被删除(拔出)或者驱动被卸载时,会调用该接口以解除设备对象与内核之间的联系。后面的例子中,我们会看到该接口实现中应当做哪些事情。Suspend/resume:系统电源管理接口;当系统进入休眠/退出休眠状态时,分别调用该接口通知到设备驱动程序。前面已经说明,id_table用来给内核USB核心驱动决定探测到的设备(接口)是否可以提交当前驱动来处理。
当驱动被加载时,内核匹配到了设备的驱动,探测函数被调用。内核USB核心在设备枚举阶段,已经获取到了USB设备有关的基础信息,并用各种结构体将设备对象封装描述起来。探测函数需要获得设备/接口/端点的信息