编写一个简单的USB驱动程序

作者:Greg Kroah-Hartman

自从本专栏开始以来,它已经讨论了Linux驱动程序编写者如何创建各种类型的内核驱动程序,通过解释不同的内核驱动程序接口,包括TTY、串口、I2C和驱动程序核心。现在是时候继续前进,专注于为真正的硬件编写真正的驱动程序了。我们首先解释如何确定要使用哪种内核驱动程序接口,帮助弄清楚硬件实际工作原理的技巧以及许多其他现实世界的知识。

让我们从一个目标开始,使一个简单的USB灯设备在Linux下良好工作。编辑Don Marti指出了一款很棒的设备,即Delcom Engineering制造的USB视觉信号指示器,如图1所示。我与这家公司没有任何关系;我只是觉得他们生产的产品很棒。这款设备可以在Delcom网站www.delcom-eng.com上在线订购。Don向我提出了挑战,要求我在Linux上使该设备工作,本文解释了我如何做到这一点。

Writing a Simple USB Driver

图1. Delcom的USB视觉信号指示器是一个简单的第一个USB编程项目。

硬件协议

尝试为设备编写驱动程序的首要目标是确定如何控制设备。Delcom Engineering非常友好地随产品附带了其设备使用的完整USB协议规范,并且也可以在网上免费获得。该文档显示了USB控制器芯片接受哪些命令以及如何使用它们。他们还提供了一个Microsoft Windows DLL,以帮助其他操作系统的用户编写代码来控制该设备。

该设备的文档仅是灯中USB控制器的文档。它没有明确说明如何打开不同颜色的LED。为此,我们必须做一些研究。

没有文档?反向工程!

如果该设备的USB协议没有文档记录或对我可用,我将不得不从设备本身反向工程这些信息。一个用于此类工作的便捷工具是一个名为USB Snoopy的免费程序,www.wingmanteam.com/usbsnoopy;它的另一个版本是SnoopyPro,usbsnoop.sourceforge.net。这些程序都是Windows程序,允许用户捕获发送到Windows系统上任何USB设备以及从任何USB设备接收的USB数据。开发人员需要做的就是找到一台Windows机器,安装制造商为设备提供的Windows驱动程序,并运行snoop程序。数据被捕获到一个文件中,以便稍后分析。Perl脚本可以帮助过滤这些snoop程序的输出中的一些额外噪声,使其成为更容易理解的格式。

少数人用来反向工程设备USB协议的另一种方法是在Linux之上的VMware中使用Windows实例运行。VMware使Windows实例能够通过usbfs向Linux发送数据,从而与插入Linux机器的所有USB设备进行通信。对usbfs进行简单的修改会导致流经它的所有数据都记录到内核日志中。使用此方法,可以捕获完整的USB流量流,并在以后进行分析。

打开灯设备后,确保不要丢失拧开设备时容易弹出的弹簧,可以检查电路板(图2)。使用欧姆表或任何用于检测闭合电路的设备,确定三个不同的LED连接到主控制器芯片上端口1的前三个引脚。

在阅读文档时,控制端口1引脚电平的USB命令是主命令 10,次命令 2,长度 0。该命令将USB命令包的最低有效字节写入端口1,并且端口1在复位后默认为高电平。因此,这就是我们需要发送到设备的USB命令,以更改不同的LED。

Writing a Simple USB Driver

图2. 三个LED连接到控制器芯片的前三个引脚。

哪个LED是哪个?

现在我们知道了启用端口引脚的命令,我们需要确定哪个LED颜色连接到哪个引脚。这很容易通过一个简单的程序来完成,该程序遍历三个端口引脚的不同值的所有可能组合,然后将该值发送到设备。该程序使我能够创建一个值和LED颜色的表格(表1)。

表1. 端口值和产生的LED模式

十六进制端口值二进制端口值点亮的LED
0x00000红色、绿色、蓝色
0x01001红色、蓝色
0x02010绿色、蓝色
0x03011蓝色
0x04100红色、绿色
0x05101红色
0x06110绿色
0x07111没有LED点亮

因此,如果端口上的所有引脚都启用(十六进制值0x07),则没有LED点亮。这与数据表中的注释“端口1在复位后默认为高电平”相符。当设备首次插入时,不启用任何LED是有道理的。这意味着我们需要将端口引脚拉低(关闭)才能点亮该引脚的LED。使用该表,我们可以确定蓝色LED由引脚2控制,红色LED由引脚1控制,绿色LED由引脚0控制。

内核驱动程序

有了我们新发现的信息,我们开始快速编写一个内核驱动程序。它应该是一个USB驱动程序,但是我们应该使用哪种用户空间接口呢?块设备没有意义,因为此设备不需要存储文件系统数据,但是字符设备可以工作。但是,如果我们使用字符设备驱动程序,则需要为其保留主设备号和次设备号。并且此驱动程序需要多少个次设备号?如果有人想将100个不同的USB灯设备插入此系统怎么办?为了预料到这一点,我们将需要保留至少100个次设备号,如果任何人一次只使用一个设备,那将是完全浪费。如果我们制作一个字符驱动程序,我们还需要发明某种方法来告诉驱动程序分别打开和关闭不同的颜色。传统上,这可以通过在字符驱动程序上使用不同的ioctl命令来完成,但是我们比以往任何时候都更清楚,不要在内核中创建新的ioctl命令。

由于所有USB设备都显示在sysfs树中自己的目录中,那么为什么不使用sysfs并在USB设备目录中创建三个文件,blue、red和green呢?这将允许任何用户空间程序(无论是C程序还是shell脚本)更改LED设备上的颜色。这也将使我们不必编写字符驱动程序,也不必乞求为我们的设备分配一块次设备号。

要启动我们的USB驱动程序,我们需要向USB子系统提供五个内容

  • 指向此驱动程序的模块所有者的指针:这允许USB核心正确控制驱动程序的模块引用计数。

  • USB驱动程序的名称。

  • 此驱动程序应提供的USB ID列表:此表由USB核心用于确定应将哪个驱动程序与哪个设备匹配;热插拔用户空间脚本使用它在设备插入系统时自动加载该驱动程序。

  • 当找到与USB ID表匹配的设备时,USB核心调用的probe()函数。

  • 当设备从系统中移除时调用的disconnect()函数。

驱动程序使用以下代码检索此信息

static struct usb_driver led_driver = {
	.owner =	THIS_MODULE,
	.name =		"usbled",
	.probe =	led_probe,
	.disconnect =	led_disconnect,
	.id_table =	id_table,
};

id_table变量定义为

static struct usb_device_id id_table [] = {
	{ USB_DEVICE(VENDOR_ID, PRODUCT_ID) },
	{ },
};
MODULE_DEVICE_TABLE (usb, id_table);

led_probe()和led_disconnect()函数稍后描述。

加载驱动程序模块时,必须将此led_driver结构注册到USB核心。这可以通过对usb_register()函数的单次调用来完成

retval = usb_register(&led_driver);
if (retval)
        err("usb_register failed. "
            "Error number %d", retval);

同样,当驱动程序从系统中卸载时,它必须从USB核心注销自身

usb_deregister(&led_driver);

当USB核心找到我们的USB灯设备时,将调用led_probe()函数。它只需要初始化设备并在正确的位置创建三个sysfs文件。这是通过以下代码完成的

/* Initialize our local device structure */
dev = kmalloc(sizeof(struct usb_led), GFP_KERNEL);
memset (dev, 0x00, sizeof (*dev));

dev->udev = usb_get_dev(udev);
usb_set_intfdata (interface, dev);

/* Create our three sysfs files in the USB
* device directory */
device_create_file(&interface->dev, &dev_attr_blue);
device_create_file(&interface->dev, &dev_attr_red);
device_create_file(&interface->dev, &dev_attr_green);

dev_info(&interface->dev,
    "USB LED device now attached\n");
return 0;

led_disconnect()函数同样简单,因为我们只需要释放我们分配的内存并删除sysfs文件

dev = usb_get_intfdata (interface);
usb_set_intfdata (interface, NULL);

device_remove_file(&interface->dev, &dev_attr_blue);
device_remove_file(&interface->dev, &dev_attr_red);
device_remove_file(&interface->dev, &dev_attr_green);

usb_put_dev(dev->udev);
kfree(dev);

dev_info(&interface->dev,
         "USB LED now disconnected\n");

当从sysfs文件读取时,我们希望显示该LED的当前值;当写入时,我们希望设置该特定的LED。为此,以下宏为每种颜色LED创建两个函数,并声明一个sysfs设备属性文件

#define show_set(value)	                           \
static ssize_t                                     \
show_##value(struct device *dev, char *buf)        \
{                                                  \
   struct usb_interface *intf =                    \
      to_usb_interface(dev);                       \
   struct usb_led *led = usb_get_intfdata(intf);   \
                                                   \
   return sprintf(buf, "%d\n", led->value);        \
}                                                  \
                                                   \
static ssize_t                                     \
set_##value(struct device *dev, const char *buf,   \
            size_t count)                          \
{                                                  \
   struct usb_interface *intf =                    \
      to_usb_interface(dev);                       \
   struct usb_led *led = usb_get_intfdata(intf);   \
   int temp = simple_strtoul(buf, NULL, 10);       \
                                                   \
   led->value = temp;                              \
   change_color(led);                              \
   return count;                                   \
}                                                  \

static DEVICE_ATTR(value, S_IWUGO | S_IRUGO,
                   show_##value, set_##value);
show_set(blue);
show_set(red);
show_set(green);

这将创建六个函数:show_blue()、set_blue()、show_red()、set_red()、show_green()和set_green();以及三个属性结构:dev_attr_blue、dev_attr_red和dev_attr_green。由于sysfs文件回调的简单性质,以及我们需要为每个不同的值(蓝色、红色和绿色)执行相同的操作,因此使用了宏来减少键入。这对于sysfs文件函数来说是很常见的;内核源代码树中的一个示例是drivers/i2c/chips中的I2C芯片驱动程序。

因此,要启用红色LED,用户需要向sysfs中的red文件写入1,这将调用驱动程序中的set_red()函数,该函数又调用change_color()函数。change_color()函数看起来像

#define BLUE	0x04
#define RED	0x02
#define GREEN	0x01
   buffer = kmalloc(8, GFP_KERNEL);

   color = 0x07;
   if (led->blue)
      color &= ~(BLUE);
   if (led->red)
      color &= ~(RED);
   if (led->green)
      color &= ~(GREEN);
   retval =
      usb_control_msg(led->udev,
                      usb_sndctrlpipe(led->udev, 0),
                      0x12,
                      0xc8,
                      (0x02 * 0x100) + 0x0a,
                      (0x00 * 0x100) + color,
                      buffer,
                      8,
                      2 * HZ);
   kfree(buffer);

此函数首先将变量color中的所有位设置为1。然后,如果要启用任何LED,它仅关闭该特定位。然后,我们向设备发送USB控制消息,以将该颜色值写入设备。

首先看起来很奇怪,只有8字节长的微小缓冲区变量是通过调用kmalloc创建的。为什么不只是在堆栈上声明它,并跳过动态分配然后销毁它的开销呢?这样做是因为某些运行Linux的架构无法发送在内核堆栈上创建的USB数据,因此所有要发送到USB设备的数据都必须动态创建。

LED工作原理

创建、构建和加载此内核驱动程序后,当插入USB灯设备时,驱动程序将绑定到它。绑定到此驱动程序的所有USB设备都可以在驱动程序的sysfs目录中找到

$ tree /sys/bus/usb/drivers/usbled/
/sys/bus/usb/drivers/usbled/
`-- 4-1.4:1.0 ->
../../../../devices/pci0000:00/0000:00:0d.0/usb4/4-1/4-1.4/4-1.4:1.0

该目录中的文件是指向sysfs树中该USB设备实际位置的符号链接。如果我们查看该目录,我们可以看到驱动程序为LED创建的文件

$ tree /sys/bus/usb/drivers/usbled/4-1.4:1.0/
/sys/bus/usb/drivers/usbled/4-1.4:1.0/
|-- bAlternateSetting
|-- bInterfaceClass
|-- bInterfaceNumber
|-- bInterfaceProtocol
|-- bInterfaceSubClass
|-- bNumEndpoints
|-- blue
|-- detach_state
|-- green
|-- iInterface
|-- power
|   `-- state
`-- red

然后,通过向该目录中的blue、green和red文件写入0或1,LED的颜色会发生变化

$ cd /sys/bus/usb/drivers/usbled/4-1.4:1.0/
$ cat green red blue
0
0
0
$ echo 1 > red
[greg@duel 4-1.4:1.0]$ echo 1 > blue
[greg@duel 4-1.4:1.0]$ cat green red blue
0
1
1

这产生了图3中显示的颜色。

Writing a Simple USB Driver

图3. 红色和蓝色LED点亮的设备

有更好的方法吗?

既然我们已经为此设备创建了一个简单的内核驱动程序,可以在2.6内核树的drivers/usb/misc/usbled.c或Linux Journal FTP站点(ftp.linuxjournal.com/pub/lj/listings/issue120/7353.tgz)上看到,这真的是与设备通信的最佳方式吗?使用usbfs或libusb之类的东西从用户空间控制设备,而无需任何特殊的设备驱动程序怎么样?在我的下一专栏中,我将展示如何做到这一点,并提供一些shell脚本来轻松控制插入系统的USB灯设备。

如果您希望看到为任何其他类型的设备编写的内核驱动程序,在合理的范围内——我不会尝试从头开始编写NVIDIA显卡驱动程序——请告诉我。

感谢Don Marti催促我让这个设备在Linux上工作。没有他的推动,它永远不会完成。

Greg Kroah-Hartman目前是各种不同驱动程序子系统的Linux内核维护者。他在IBM工作,从事与Linux内核相关的工作,可以通过greg@kroah.com联系到他。

加载Disqus评论