Написание простейшего USB-драйвера

Apr 01, 2004 By Greg Kroah-Hartman
in Software

Нашей целью будет заставить простую USB-люстру работать хорошо под Linux. Редактор Don Marti притащил это забавное устройство под названием USB Visual Signal Indicator производства Delcom Engineering, оно показано на картинке 1. Я никогда не имел дел с этой компанией, хотя как мне представляется, они должны выпускать приличные девайсы. Цветовой индикатор можно заказать на их сайте, www.delcom-eng.com. Don попросил меня заставить устройство работать под Linux и статья описывает, как этого добиться.


Figure 1. Delcom's USB Visual Signal Indicator is a simple first USB programming project.

Протокол аппаратной части
Разберемся, как контролировать устройство. Delcom Engineering достаточно хороша для того, чтобы выложить полные спецификации своих устройств в свободном доступе. Документация описывает, какие команды воспринимает USB-контроллер и как их использовать. Также предоставляется разделяемая библиотека для Microsoft Windows, это должно помочь пользователям, которые хотят написать код для других операционных систем.

Документация к данному устройству сводится к описанию контроллера лампы. Она не сообщает точно, как включать светодиоды различных цветов. Отсутствие ясных указаний должно подвигнуть нас на некоторые исследования.

Нет документации? Прибегай к реверс-разработке!
Если нет информации, будем пытаться получит ее от самого устройства. Полезным инструментом для этой работы будет свободная программка USB Snoopy, www.wingmanteam.com/usbsnoopy; альтернативная версия программы SnoopyPro, usbsnoop.sourceforge.net . Эти программы работают под Windows и предназначены для захвата данных, которые гуляют по USB-соединению. Устанавливаем устройство в Windows, устанавливаем драйвер, предоставляемый производителем и запускаем одну из программ перехвата. Данные обмена захватываются в файл, который можно проанализировать.

Перловый скрипт поможет отфильтровать некоторое количество шума, производимого программой захвата вместе с полезной информацией, таким образом конечный формат будет проще для анализа.

Другой способ - запустить виртуальную машину Windows в своём экземпляре Linux. VMware позволяет Windows подхватывать все устройства, подключенные в Linux, а мы можем читать обмен Windows с устройством через usbfs. Небольшая модификация ядра позволяет сбрасывать весь поток обмена usbfs в журнал ядра, поэтому он может быть захвачен и проанализирован.

Раскрутим устройство и проанализируем, где чего подключено. Используя омметр или любой другой индикатор замкнутых проводников, устанавливаем, что три различных светодиода подключены к первым трем выводам порта 1 основного чипа контроллера, картинка 2.

В документации сказано, что USB команда для контроля выводов порта 1 имеет следующую конструкцию: Major 10, Minor 2, Length 0. Команда записывает значащий байт командного пакета USB в порт 1 и порт 1 поднимается после ресета.


Figure 2. The three LEDs are connected to the first three pins of the controller chip.

Какой конкретно светодиод?
Теперь, когда мы знаем команду, понимаемую портом 1, мы должны определить, какой диод подключен к какому выводу. Этого мы достигнем с помощью простейшей программки, которая переберет все возможные комбинации значений для трех выводов, посылая их в устройство. Такой подход позволил мне составить таблицу 1.

Table 1. Значения, посылаемые в порт и их результат

Шестнадцатиричное значение, которое пишем в порт

Двоичное значение

Результат: какие светодиоды горят

0x00

000

Red, Green, Blue

0x01

001

Red, Blue

0x02

010

Green, Blue

0x03

011

Blue

0x04

100

Red, Green

0x05

101

Red

0x06

110

Green

0x07

111

Не горит ни один

Если все выводы порта включены в состояние "доступен" (значение 0x07 hex), ни один диод не горит. Это состояние в таблице состояний обозначается как "порт активен по умолчанию после ресета". Это разумно: позволяет устройству оставаться выключенным после подсоединения в разъем USB в первый раз. Это означает, что мы должны послать сигнал "обнулить" на тот вывод, на котором хотим чтобы светодиод загорелся.

По результирующей таблице видим, что вывод 1 управляет красным диодом, вывод 2 - синим, вывод 0 - зелёным.

Вооружённые этой новой для нас информацией, мы можем приступить к быстрой разработке драйвера устройства. Это должен быть USB-драйвер, но какого рода интерфейс для использования в пространстве пользователя мы можем задействовать? Блочное устройство не имеет смысла, кроме того данное устройство не хранит никакой информации на файловой системе, следовательно, символьное устройство будет работать. Однако, если мы задействуем символьное устройство, должны быть зарезервированы старший и младший номера. И, кроме того, сколько младших номеров нам нужно для девайса? Что если кто-то захочет подключить к компу 100 таких люстрочек? Чтобы это не стало неожиданностью, зарезервируем на всякий случай сто младших номеров. Если мы делаем драйвер символьного устройства, потребуется какой-то путь для сообщения драйверу информации о включении конкретных светодиодов. Традиционно это можно было бы сделать с помощью разных ioctl-комманд, но мы знаем, что есть пути гораздо лучше, чем городить в ядро еще какие-то ioctl.

Поскольку все устройства USB появляются в своей директории под деревом sysfs, почему бы этим не воспользоваться и не создать три файла в соответствующем каталоге, через которые мы будем общаться с устройством? Это позволит любым программам пространства пользователя моргать диодами, будь то С программа или шелл-скрипт. Это также позволит нам удержаться от написания символьного драйвера со всеми этими проблемами касаемо младших номеров устройств.

Для начала написания USB драйвера мы должны обеспечить подсистему USB со следующими опциями:

* Указатель на модуль, предоставляющий этот драйвер, это позволит ядру USB контролировать количество ссылок на модуль правильным образом.

* Имя USB-драйвера.

* Список USB идентификаторов, который данный драйвер должен предоставлять; эта таблица используется ядром USB для определения, какой драйвер должен быть сопоставлен соответствующему устройству; скрипт горячего подключения пространства пользователя будет использовать эту информацию для загрузки драйвера в момент, когда устройство подключается в систему.

* Функция probe(), вызываемая ядром USB, когда устройство найдено в таблице USB ID-ов.

* Функция 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);

Функция led_probe() вызывается, когда ядро USB обнаруживает наше устройство. Всё, в чём мы нуждаемся, это инициализация устройства и создание трёх файлов в правильном месте. Делаем это с помощью следующего кода:


/* 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");

Когда мы читаем из файлов, мы хотим получить текущее значение для данного светодиода, когда туда пишем - устанавливаем его. Для производства этих операций нужен следующий макрос. Он создаёт две функции для каждого диода и декларирует нужные файлы:


#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, образец можно найти в дереве исходников ядра для драйвера чипа I2C в drivers/i2c/chips.

Итак, для готовности красного светодиода пользователь пишет 1 в красный файл, это событие вызывает функцию set_red() в драйвере, что в свою очередь вызывает функцию 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);

Эта функция стартует путём установки всех значащих битов цвета в 1. Далее, если какой-нибудь LED должен быть готовым, выключается именно этот специфический бит. Далее мы посылаем управляющее сообщение USB устройству "записать значение в порт".

В первую очередь это означает выделение буфера в 8 байт путём вызова kmalloc. Почему я не рекомендую декларировать это в стеке, динамически задавая смещение, и затем удалять из стека? Это сделано как сделано, потому что некоторые процессорные архитектуры не могут отправить созданные данные USB в стек ядра. Поэтому все данные, которые должны быть отправлены в 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. Если мы посмотрим в это расположение, увидим файлы, созданные для светодиодов:


$ 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

И, далее, записывая 0 или 1 в blue, green и red, мы можем менять цвета люстрочки:


$ 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.


Figure 3. The Device with the Red and Blue LEDs On

Действительно ли указанный путь есть путь наилучший?
Теперь, когда мы написали простой драйвер USB-устройства (он может быть найден в дереве исходников ядра 2.6 в drivers/usb/misc/usbled.c или на сайте Linux Journal FTP: ftp.linuxjournal.com/pub/lj/listings/issue120/7353.tgz ), зададимся вопросом: действительно ли данный путь общения с устройством оптимальный? Как насчёт использования usbfs или libusb для контроля устройства из пространства пользователя без каких-либо специализированных драйверов?

В других своих колонках Грег рассказывает, как контролировать люстрочку с помощью шелл-скриптов (прим. перев.).

Greg Kroah-Hartman currently is the Linux kernel maintainer for a variety of different driver subsystems. He works for IBM, doing Linux kernel-related things, and can be reached at greg@kroah.com.

Назад