Thursday, July 3, 2014

Собственный драйвер для устройства USB - это просто (часть 2)

http://symmetrica.net/usb/usb2.htm

Собственный драйвер для устройства USB - это просто (часть 2)

Первая часть

С человеческим лицом

Помните вывод lsusb в начале статьи? В нем есть такая строка:

bInterfaceClass 3 Human Interface Device

Это значит, что наша светящаяся коробочка принадлежит к классу HID – Human Interface Devices, то есть устройств, предназначенных для непосредственного взаимодействия с человеком. Среди устройств USB класс HID является самым сложным и многообразным. Подробности вы можете узнать в официальной спецификации Device Class Definition for Human Interface Devices, который доступен по адресу www.usb.org/developers/devclass_docs/HID1_11.pdf (текущая версия – 1.11). Из всего многообразия свойств HID для нас сейчас важнее всего две вещи: во-первых, поддержка устройств HID уже встроена в нашу операционную систему (будь то Linux или Windows). Ядро системы знает, как управлять устройством «в целом», а это значит, что для управления его специфическими функциями мы можем использовать интерфейсы прикладного уровня, оставив всю черную работу ядру ОС. Вторая важная особенность устройств HID связана с тем, как они обмениваются данными с компьютером. Предполагается, что устройства этого класса (мыши, клавиатуры, джойстики, текстовые терминалы) передают и получают не очень много данных. Традиционные устройства HID не используют массовую и изохронную передачу данных. Помимо стандартного канала передачи управляющих сообщений, устройство HID должно поддерживать канал передачи прерываний, направленных от устройства к хосту. Возможно, но не обязательно, наличие канала для передачи прерываний и в противоположном направлении. Наблюдение за трафиком Dream Cheeky WebMail Notifier под управлением ОС Windows свидетельствует о том, что устройство использует только управляющий канал и канал прерываний от устройства к хосту.

Знакомьтесь – libusb

Библиотека libusb представляет собой наиболее универсальный инструмент, который подойдет как для Linux, так и для Windows (а так же для FreeBSD и OS X). С помощью этой библиотеки прикладная программа может решать такие задачи, как поиск устройства на шине USB и обмен данными с ними.

Прежде чем приступать к работе, убедитесь, что в вашей системе установлена библиотека libusb версии не ниже 1.0 (этот совет относится к Linux и FreeBSD, если вы хотите использовать libusb под Windows, прочитайте врезку).

Libusb под Windows 
Библиотека libusb не входит в ваш дистрибутив Windows, так что придется установить ее самостоятельно. Домашняя страница проекта libusb for Windows находится по адресу: http://sourceforge.net/projects/libusb-win32/. Вопреки названию, начиная с версии 1.2, библиотека может работать и с 64-битной Windows. Я рекомендую вам воспользоваться самой последней версией библиотеки (на данный момент – 1.2.2.0). Ранние версии libusb for Win32 содержали ошибки, благодаря которым библиотека могла, например, отключить все драйверы USB разом (в том числе – мыши и клавиатуры). Не помогала даже перезагрузка Windows в безопасном режиме. Особенно весело все это выглядит на компьютере, у которого отсутствуют разъемы PS/2, так что мышь и клавиатуру можно подключить только через USB (кто-нибудь еще помнит мыши с подключением к последовательному порту?). Кроме того, ранние версии библиотеки не умели работать с устройствами класса HID, каковыми мы сейчас и занимаемся. Помимо самой библиотеки в дистрибутив libusb входят утилиты для установки драйверов и диагностическая программа, которая позволяет проверить, «видит» ли libusb ваше устройства, а заодно – собрать информацию об устройстве, аналогичную той, которую выдает утилита lsusb.

Да будет свет!

Теперь, когда мы знаем, как устройство взаимодействует с компьютером, мы можем написать аналог программы Webmail Notifier для Linux. Но мы поступим лучше. Сила и мощь Linux заключается в том, что многие полезные программы выполнены в виде консольных утилит, которыми легко управлять из командной строки и других программ. Для нашего устройства мы напишем программу dclight, с помощью которой мы сможем устанавливать произвольное значение яркости для каждого светодиода. Вызов программы выглядит как

dclight r g b

где r g b – значения яркости (от 0 до 255) для каждой цветовой составляющей. После вызова утилиты коробочка Dream Cheeky будет светить нам выбранным светом до тех пор, пока мы не изменим его значение. Для выключения устройства нужно вызвать

dclight 0 0 0

Как видим, программу dclight будет совсем нетрудно вызвать из других программ, в том числе, написанных на скриптовых языках. Благодаря кросс-платформенности libusb ее можно скомпилировать и под Windows (если вы пользуетесь MinGW, вносить изменений в исходные тексты вообще не придется). Полностью исходники доступны по ссыллке в конце страницы.

#define DEV_VID 0x1D34   #define DEV_PID 0x0004   #define DEV_CONFIG 1   #define DEV_INTF 0   #define EP_IN 0x81     unsigned char COMMAND_1[8] = {0x1F,0x1E,0x92,0x7C,0xB8,0x1,0x14,0x03};   unsigned char COMMAND_2[8] = {0x00,0x1E,0x92,0x7C,0xB8,0x1,0x14,0x04};   unsigned char COMMAND_ON[8] = {0x00,0x00,0x00,0x00,0x0,0x1,0x14,0x05};     int main(int argc, char * argv[])   {   ...     libusb_init(NULL);     libusb_set_debug(NULL, 3);     handle = libusb_open_device_with_vid_pid(NULL, DEV_VID, DEV_PID);  ...    if (libusb_kernel_driver_active(handle,DEV_INTF)) libusb_detach_kernel_driver(handle, DEV_INTF);     if ((ret = libusb_set_configuration(handle, DEV_CONFIG)) < 0) {       printf("Ошибка конфигурации\n");  ...     }     if (libusb_claim_interface(handle, DEV_INTF) < 0) {      printf("Ошибка интерфейса\n");   ...     }     ret = libusb_control_transfer(handle, LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE|LIBUSB_ENDPOINT_OUT, 0x9, 0x200, 0, COMMAND_1, 8, 100);     libusb_interrupt_transfer(handle, EP_IN, buf, 8, &ret, 100);     ret = libusb_control_transfer(handle, LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE|LIBUSB_ENDPOINT_OUT, 0x9, 0x200, 0, COMMAND_2, 8, 100);     libusb_interrupt_transfer(handle, EP_IN, buf, 8, &ret, 100);     COMMAND_ON[0] = r;     COMMAND_ON[1] = g;     COMMAND_ON[2] = b;     ret = libusb_control_transfer(handle, LIBUSB_REQUEST_TYPE_CLASS|LIBUSB_RECIPIENT_INTERFACE|LIBUSB_ENDPOINT_OUT, 0x9, 0x200, 0, COMMAND_ON, 8, 100);    buf[7] = 0;    libusb_interrupt_transfer(handle, EP_IN, buf, 8, &ret, 100);     if (buf[7] != 1) {       printf("Сбой в управлении устройством\n");   ...    }    libusb_attach_kernel_driver(handle, DEV_INTF);     libusb_close(handle);     libusb_exit(NULL);     return 0;   } 

В начале программы мы объявляем несколько полезных констант. Прежде всего, это значения VID и PID устройства, с которым нам предстоит работать, а так же номера интерфейса и конфигурации, которые оно поддерживает. Последние два значения мы могли бы узнать программно, и ниже мы расскажем, как это сделать, но сейчас мы упростим себе жизнь и жестко зашьем в программу данные, полученные с помощью утилиты lsusb. На практике это вполне допустимо, поскольку для любого конкретного устройства наборы интерфейсов и конфигураций – величина постоянная. Программная реализация поиска этих значений имеет смысл в приложениях, предназначенных для работы с большими группами различных устройств, и в этом случае вы должны очень хорошо ведать, что вы творите и зачем. Константа EP_IN определяет номер точки доступа для опроса прерываний. Массивы COMMAND_1, COMMAND_2 и COMMAND_ON содержат описанные ранее последовательности байтов, которые необходимо передать для инициализации устройства и для управления светодиодами.

Функция libusb_init() инициализирует библиотеку. Правила хорошего тона требует, чтобы в конце программы мы так же вызвали libusb_exit().

Первый аргумент libusb_init(), который мы оставляем равным NULL, определяет идентификатор сессии (контекст) работы с библиотекой. Использование в программе нескольких разных контекстов позволяет создавать независимые сессии при работе с библиотекой libusb (так, что вызов, например, libusb_exit() в одной сессии не повлияет на работу в другой). Структура данных, описывающая контекст, инициализируется функцией libusb_init(). Поскольку в нашей программе мы явно управляем вызовами всех функций libusb, мы можем обойтись без контекстов. Если же вы пишете разделяемую библиотеку, в которой задействована функциональность libusb, использовать контексты вам просто необходимо, так как пользователь вашей библиотеки может использовать и другие библиотеки, которые тоже работают с libusb.

Функция libusb_set_debug () определяет, насколько многословной будет программа в случае ошибки. Мы устанавливаем максимальный уровень информативности. Помимо этого, многие функции libusb возвращают численный код завершения операции, по которому можно выловить информацию об ошибке (соответствующие константы определены в файле libusb.h).

Функция libusb_open_device_with_vid_pid() ищет на шине устройство USB по заданным значениям Vendor ID и Product ID и открывает первое найденное для работы. Представители старшего компьютерного поколения помнят времена, когда при установке нового устройства требовалось указывать порты и прерывания (иногда на плате самого устройства, с помощью перемычек). В нашу эпоху безалкогольного пива пользователь ждет, что программа сама покажет ему список подходящих устройств в его системе (ниже мы расскажем о том, как это можно сделать в случае USB). Если подходящее устройство найдено, функция libusb_open_device_with_vid_pid() возвращает указатель на структуру libusb_device_handle, которую мы и используем далее для всех обращений к устройству. По окончании работы с устройством этот указатель передается функции libusb_close(). Стоит отметить, что на самом деле функции, открывающие и закрывающие устройства, на работу этих устройств не влияют никак. Выполняемые ими операции касаются только настройки структур данных внутри самой библиотеки libusb.

Диалог с коробочкой

Наша следующая задача – выбрать конфигурацию и интерфейс устройства, в результате чего нам откроются волшебные врата – точки доступа, через которые мы сможем обмениваться с устройством данными. Однако для начала стоит проверить, не захватила ли уже доступ жадная операционная система. Функция libusb_kernel_driver_active() позволяет определить, доступен ли заданный интерфейс, а функция libusb_detach_kernel_driver() отцепляет от него драйвер операционной системы. Следуя правилу «где что взял, положи обратно», в конце работы программы мы вызываем функцию libusb_attach_kernel_driver(). Теперь мы можем смело захватывать конфигурацию и интерфейс (функции libusb_set_configuration() и libusb_claim_interface() соответственно).

Для каждого из четырех типов передач данных протокола USB в библиотеке libusb определены специальные функции. Так, libusb_control_transfer() предназначена для передачи управляющих сообщений. Первый параметр функции – идентификатор открытого устройства. Далее следует комбинация флагов, определяющих параметры передачи. В нашем случае (передача специальных управляющих сообщений от хоста к устройству) используются следующие флаги: LIBUSB_REQUEST_TYPE_CLASS – означает, что сообщение специфично для класса устройства; LIBUSB_RECIPIENT_INTERFACE – указывает, что получателем дополнительных данных является интерфейс устройства; LIBUSB_ENDPOINT_OUT определяет направление передачи дополнительных данных (от хоста к устройству). В третьем, четвертом и пятом параметрах передаются номер запроса, значение запроса и значение индекса. Шестой параметр функции – указатель на массив дополнительных данных, далее следует длина массива в байтах. Последний параметр – время ожидания подтверждения сообщения в миллисекундах.

Функция libusb_interrupt_transfer() предназначена для передачи прерываний. Первый параметр – идентификатор устройства. Далее следует номер точки доступа. В отличие от управляющих сообщений, которые передаются по стандартной точке доступа, номер которой можно не указывать, для передачи прерываний этот номер необходимо указать явно. Зато все остальные параметры (направление передачи данных и т.п.) функция определяет сама, из описания указанной точки доступа. Третий параметр функции – адрес массива, который используется для передачи или приема данных. Длина массива передается в четвертом параметре. Пятый параметр – это указатель на переменную, в которой функция возвращает количество фактически переданных байт данных. Последний параметр – время ожидания в миллисекундах.

Функция libusb_bulk_transfer() предназначена для передачи больших массивов данных. Заголовок этой функции выглядит так же, как и у функции libusb_interrupt_transfer().

Теперь вы, конечно, захотите узнать, какая функция выполняет изохронную передачу данных. Я мог бы сказать вам, но не стану. Дело в том, что рассмотренный нами блок функций предназначен для работы с устройством в блокирующем режиме (запрос–ожидание ответа–ответ), который является самым простым. У библиотеки libusb есть и другой, неблокирующий (асинхронный) режим, в котором функции передачи данных не приостанавливают работу вызывающей программы. В этом режиме реализованы все четыре типа передачи данных, в том числе и изохронный. Для блокирующего же режима функции изохронной передачи данных просто нет, и, в общем, нетрудно понять, почему.

Программа компилируется строкой

gcc dclight.c -o dclight -lusb-1.0

Для получения доступа к устройству USB программа она должна обладать правами root (или же вы должны включить своего пользователя в группу, имеющего право записи в usbfs – как это сделать, ищите в документации к своему дистрибутиву). Для того чтобы утилиту dclight можно было вызывать из обычных программ, сделаем root ее владельцем и установим для нее «липкий бит»:

# chown root dclight   # chmod a+s dclight 

Теперь мы можем контролировать почтовый индикатор из Linux (и Windows) и заставить его делать то, что нужно нам!

Обратите внимание, что мы выполняем процедуру инициализации устройства при каждом вызове программы dclight, хотя это достаточно сделать один раз, при подключении устройства к компьютеру. Наша программа была бы заметно сложнее, если бы нам требовалось учитывать, было ли устройство уже инициализировано ранее, но ничего такого не требуется. После того как мы выполнили инициализацию устройства, оно попросту игнорирует последующие команды инициализации. Как уже отмечалось выше, устройство не выключается автоматически при завершении работы с libusb (да библиотека и не знает, как его выключить). Для нас это означает, что светодиоды будут гореть с заданной яркостью и после завершения работы программы. Такое поведение соответствует нашему замыслу, но, вообще говоря, оно выглядит несколько непривычно для прикладных программистов, которые привыкли, что, завершая работу, программа прибирает за собой… Добро пожаловать в мир работы с оборудованием напрямую! Здесь все в ваших руках.

Мы оставили в стороне один важный вопрос: временные задержки. При управлении устройством USB нам может понадобиться делать между командами определенные паузы. В случае WebMail Notifier этого не потребовалось, поскольку интерфейс устройства сам создает необходимые паузы (обмен сообщениями для установки определенного значения яркости светодиодов со всеми задержками подтверждающих сообщений со стороны устройства занимает около 0.01 секунды). В общем же случае нам могут понадобиться специальные таймеры.

Умный поиск

Функция libusb_open_device_with_vid_pid() которую мы использовали выше, реализует «быстрый и грязный» способ поиска и инициализации устройства по заданным значениям VID и PID. Она удобна в отладочных программах, но в приложениях для серьезного применения ее лучше не использовать. Недостатки libusb_open_device_with_vid_pid() очевидны: эта функция не позволяет инициализировать несколько устройств с одинаковыми VID и PID, и совершенно не годится для тех случаев, когда устройство выбирается не по паре VID и PID, а, например, по классу. Поиск устройств по всем параметрам можно выполнить с помощью функций libusb_get_device_list() и libusb_get_device_descriptor(). Функция libusb_get_device_list() позволяет получить список всех устройств USB, обнаруженных в системе. Она создает массив указателей на структуры libusb_device, каждая из которых соответствует одному экземпляру устройства USB. Функция libusb_get_device_descriptor() позволяет получить описание устройства, представленного структурой libusb_device, в виде структуры libusb_device_descriptor. Эта структура содержит VID и PID устройства, коды класса и подкласса, читабельные имена производителя и самого устройства, серийный номер и число конфигураций устройства. Этой информации обычно достаточно для того, чтобы выбрать устройство для подключения. Выбранное устройство открывается с помощью функции libusb_open(), которой передается указатель на структуру libusb_device. Ниже приводится фрагмент программы, в котором выбор устройств выполняется описанным способом. Устройство выбирается по значению VID из структуры libusb_device_descriptor, но его точно так же можно выбирать и по значения других полей структуры.

   libusb_device ** list;    libusb_device * found = NULL;    ssize_t count;    ssize_t i = 0;    int err = 0;    libusb_context * ctx;    libusb_init(&ctx);     if ((count = libusb_get_device_list(ctx, &list)) < 0) {       printf("Невозможная ошибка, но, тем не менее...\n");       return -1;     }     for (i = 0; i < count; i++) {       libusb_device * device = list[i];       struct libusb_device_descriptor desc;       libusb_get_device_descriptor(device, &desc);       if (desc.idVendor == DEV_VID) {         found = device; break;       }     }    if (found) {       err = libusb_open(found, &handle);       if (err) {         printf("Невозможно открыть устройство\n");         return -1;       }     } else {       printf("Устройство не найдено\n");       return -1;     }   ...     libusb_free_device_list(list, 1); 

Ложка дегтя

После перечисления всех достоинств libusb нельзя не упомянуть об одном недостатке. На данный момент библиотека игнорирует тот факт, что устройства USB могут подключаться и отключаться в процессе нормальной работы системы. Если устройство, с которым мы работаем, было отключено во время работы программы, следующее обращение к устройству вернет одно из возможных сообщений об ошибке, из которых можно сделать вывод, что устройство отключено. Если вы хотите, чтобы ваша программа реагировала на подключение новых устройств, вам следует периодически выполнять поиск устройств на шине USB, как это было описано выше. На данный момент это все, что можно сделать для обнаружения динамических подключений и отключений в рамках libusb, то есть, не прибегая к специальным средствам ОС. Разработчики libusb предлагают добавить в библиотеку функции обратного вызова, оповещающие программу об изменении списка доступных устройств, но на момент написания статьи эта функциональность не реализована.

Теперь вы и сами можете осчастливить Linux-сообщество, добавив в систему поддержку нового устройства USB (и я просто уверен, что вам не терпится это сделать). Мы же продолжим знакомство со средствами домашней автоматизации, управляемыми с помощью USB.

Исходные тексты

Понравилась статья? Нажми: 


Статья впервые опубликована в журнале Linux Format, © Андрей Боровский 2010


www.symmetrica.net

No comments:

Post a Comment