- Подписка на печатную версию:
- Подписка на электронную версию:
- Подшивки старых номеров журнала (печатные версии)
LXF138:driver
Материал из Linuxformat.
- Большой проект Возьмем USB-устройство и напишем для него Linux-драйвер
Содержание |
USB: Драйвер своими руками
- Linux не поддерживает нужную вам периферию? Как и все в свободном ПО, это дефект можно исправить самостоятельно. Андрей Боровский рассмотрит процесс от и до.
Хотим мы того или нет, но многочисленные порты, унаследованные от IBM PC и PS/2, уходят в прошлое. Будущее принадлежит универсальным скоростным портам типа USB и Firewire. Об удобствах, которые USB предоставляет простым пользователям ПК, распространяться не приходится. Единый интерфейс для всех устройств, обладающий возможностями Plug’n’Play и продвинутого управления питанием – именно то, что нужно людям, для которых компьютер – часть бытовой техники. Другое дело – индивидуальные разработчики различных устройств и просто хакеры. Для этих категорий переход на USB представляет определенные сложности. Проблема заключается в том, что USB – «интеллектуальный» интерфейс. Любое устройство, предназначенное для подключения к компьютеру через USB, должно поддерживать хотя бы небольшую часть спецификации протокола USB: уметь «представиться» (сообщить информацию о себе и своих возможностях) и адекватно реагировать на стандартные сообщения USB, посылаемые компьютером. В результате даже устройство, все функции которого ограничиваются включением и выключением светодиода по сигналу с компьютера, при подключении через USB требует наличия микросхемы, которая умеет «разговаривать» с хостом.
Однако и для разработчиков собственных устройств переход на USB несет определенные преимущества. Прежде всего, упрощается процесс написания драйверов. Поскольку для общения с компьютером все USB-устройства используют один протокол, абстрагированный от таких аппаратно-зависимых вещей, как отображенные в память порты и прерывания, возникает возможность не писать для каждого устройства свой собственный драйвер уровня ядра. Вместо этого целые группы устройств могут использовать один и тот же драйвер уровня ядра, а специфичный код, учитывающий особенности конкретного устройства, может быть размещен в пространстве пользователя. При этом драйвер уровня ядра берет на себя такие функции, как управление питанием устройства (весьма нетривиальная задача, при условии, что сам компьютер может переключаться между несколькими энергосберегающими режимами), оставляя нам самое интересное – управление функциями устройства.
Перенос кода управления устройством в пространство пользователя не только упрощает отладку (при падении приложения, скорее всего, не придется перезагружать машину), но и позволит писать процедуры управления устройством на самых разных языках программирования, а не только на C. Более того, пользовательская часть драйвера, не взаимодействующая напрямую с механизмами ядра ОС, может быть сделана кросс-платформенной, что мы и имеем в случае таких инструментов как libusb и Jungo WinDriver. Благодаря последним, обходиться без собственных драйверов ядра могут даже многие устройства промышленного уровня. Что уж говорить о любительских?
Протокол USB
Протокол USB похож на стек TCP/IP (который отчасти и послужил его прототипом). Как и в случае с сетевыми протоколами, USB можно разделить на несколько уровней. На самом нижнем логическом уровне (спецификации физического уровня мы не рассматриваем) устройства обмениваются пакетами данных (со встроенными механизмами коррекции ошибок, подтверждения получения и т. д). Из пакетов формируются запросы, которые устройства посылают друг другу. Запросы составляют блоки запросов USB (USB Request Block, URB).
Программист, который пишет драйвер устройства USB, может не заботиться о деталях передачи отдельных пакетов – этими вещами управляют механизмы более низкого уровня. Хотя понимание работы протоколов USB на низком уровне может быть полезно, в нашей статье для его описания не хватит места. Рассмотрим только то, что необходимо разработчикам драйверов.
Протокол USB является «хост-центричным» – процесс передачи данных всегда инициируется хостом (то есть компьютером). Если у периферийного устройства появились данные для передачи хосту, оно должно ожидать запроса хоста на передачу данных. Существует четыре типа передач данных:
- Передача управляющих данных [control transfer] предназначена для определения параметров и настройки периферийных устройств, а также для передачи коротких команд. Полезная часть блока управляющих данных состоит из установочного пакета [setup packet] и (возможно) нескольких байтов данных. Установочный пакет содержит информацию о запросе, который хост направляет устройству, направлении передачи дополнительных данных (от хоста к устройству или наоборот), логическом адресате данных (устройство, интерфейс) и количестве байт дополнительных данных.
- Передача прерываний [interrupt transfer] – их не следует путать с прерываниями в компьютере – используется для коротких сообщений, в основном от устройства хосту. Поскольку инициатива в обмене данными всегда исходит от хоста, прерывания, посылаемые устройством, не могут прервать порядок работы хоста. Однако устройство ожидает, что хост будет опрашивать его на предмет прерываний с определенной частотой (она определяется в процессе настройки соединения). Таким образом, устройство может рассчитывать, что задержка при передаче прерываний не превысит определенного значения. При этом количество данных, передаваемое в одном прерывании, ограничено (8, 64 или 1024 байтами, в зависимости от скоростных параметров устройства). Следует учитывать, что «гарантированное максимальное время задержки» гарантировано только для доставки прерывания хосту. Фактическая обработка выполняется ПО и может быть отложена на неопределенное время.
- Изохронная передача данных [isochronous transfer] используется для мультимедиа, где важна гарантированная пропускная способность, но потеря отдельных пакетов из-за помех несущественна (ошибки выявляются, но повторная отправка сбойных пакетов не производится). Изохронная передача возможна в обоих направлениях. Обычно этим способом передаются живые мультимедиа-данные (например, видео, получаемое с цифровой камеры в режиме онлайн).
- Массовая передача данных [bulktransfer] – это передача больших объемов данных с гарантированной доставкой, но негарантированной максимальной задержкой и полосой пропускания. Примером такой передачи данных могут служить данные, передаваемые компьютером принтеру, или данные, которыми хост обменивается с устройством хранения. Массовая передача данных также возможна в обоих направлениях.
Продолжая аналогию между USB и сетевыми протоколами, мы можем вспомнить, что «пункт назначения» TCP/IP включает помимо адреса еще и порт. Его аналогом в USB является конечная точка [endpoint]. Каждое устройство USB поддерживает конечную точку с номером 0x00, предназначенную для передачи управляющих данных. Помимо этого, устройство может предоставлять еще несколько конечных точек, предназначенных для определенного типа передачи данных в определенном направлении (за исключением точки 0x00, каждая конкретная конечная точка может передавать данные только в одном направлении). Например, если устройству требуется принимать данные с помощью массовой передачи и передавать прерывания, оно предоставит две дополнительных конечных точки. Помимо направления передачи данных, в описании конечной точки указывается максимальный размер передаваемого пакета в байтах. Группы конечных точек устройства объединяются в интерфейсы. Дескриптор интерфейса содержит идентификатор класса устройства (HID, Mass Storage и т. д.), благодаря чему одно и то же физическое устройство может предоставлять интерфейсы различных классов. Интерфейсы объединяются в конфигурации, которые, помимо прочего, включают описания режимов питания устройства. Таким образом, одно физическое устройство может восприниматься системой как несколько разных устройств USB.
У большинства устройств присутствует интерфейс, состоящий из двух точек доступа: для передачи управляющих сообщений и для передачи прерываний от устройства хосту. Если, помимо передачи управляющей информации, устройству нужно передавать большие объемы данных в двух направлениях, оно может предоставить еще один интерфейс, объединяющий две точки доступа, предназначенные для массовой передачи данных, и так далее.
Само устройство идентифицируется двумя числами: идентификатором производителя VID и идентификатором продукта PID. С точки зрения системы устройство идентифицируется адресом на шине USB и этими двумя числами. Таким образом, настройка связи драйвера с устройством включает поиск устройства с заданными VID и PID на шине USB, после чего драйвер выбирает конфигурацию, интерфейс и группу конечных точек, если выбранный интерфейс поддерживает несколько групп. Все это выглядит сложно, но, к счастью, у нас под рукой есть утилиты, которые всегда подскажут нам, что именно включает настройка устройства.
Сведения о логической структуре устройства нам поможет получить утилита lsusb. Простой вызов lsusb перечислит адреса подключенных к шине USB устройств и их пары VID и PID. Вот как может выглядеть выдача команды lsusb, вызванной без параметров:
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub Bus 002 Device 002: ID 80ee:0021 Bus 002 Device 012: ID 1d34:0004
Числа, следующие за ID, представляют собой пары VID:PID для данного устройства. Если теперь мы хотим получить подробные сведения об устройстве 1d34:0004, командуем:
lsusb -v -d 1d34:0004
Фрагмент выдачи команды приводится ниже
Для целей обратного инжиниринга, Windows можно запустить в виртуальной машине. Тогда трафик, генерируемый «фирменной» программой, будет виден в Wireshark. Кроме того, некоторые ВМ, например, VMware, могут сами захватывать трафик USB и сохранять его в файле.
Device Descriptor: ... idVendor 0x1d34 idProduct 0x0004 bcdDevice 0.02 iManufacturer 1 Dream Link iProduct 2 DL100B Dream Cheeky Generic Controller bNumConfigurations 1 Configuration Descriptor: ... bNumInterfaces 1 Interface Descriptor: …. bNumEndpoints 1 bInterfaceClass 3 Human Interface Device bInterfaceSubClass 0 No Subclass bInterfaceProtocol 0 None ... Endpoint Descriptor: bLength 7 bDescriptorType 5 bEndpointAddress 0x81 EP 1 IN bmAttributes 3 Transfer Type Interrupt Synch Type None Usage Type Data wMaxPacketSize 0x0008 1x 8 bytes bInterval 10 Device Status: 0x0000 (Bus Powered)
Из этого фрагмента мы узнаем, что устройство поддерживает одну конфигурацию (поле bNumConfigurations) и один интерфейс с одной дополнительной конечной точкой (поле bNumEndpoints; точка 0x00 не учитывается, поскольку присутствует всегда). Эта конечная точка имеет адрес 0x81 и предназначена для передачи прерываний от устройства хосту.
Оборудование
Устройство, с которым мы познакомились таким необычным образом – это небольшая безделушка производства компании Dream Cheeky (http://www.dreamcheeky.com), известной своими USB-ракетницами, подогревателями кофе и другими столь же полезными изделиями. Рассматриваемое устройство (рис. 1) позиционируется компанией как индикатор поступления электронной почты. Оно представляет собой пластиковую коробочку с изображением конверта, которая подсвечивается изнутри с помощью комбинации трех светодиодов: красного, синего и зеленого (поскольку каждый светодиод обладает 256 градациями яркости, мы имеем возможность выбирать цвет из 24‑битной палитры). В комплекте с устройством идет Windows-программа, которая умеет опрашивать состояние указанных пользователем почтовых ящиков и выдавать определенные световые эффекты при поступлении почты.
С моей точки зрения, Dream Cheeky Webmail Notifier представляет собой яркий пример «железа», возможности которого искусственно ограничены сопутствующим ПО. Хотя подсвечивание пластика разноцветными светодиодами и нельзя назвать богатой функциональностью, у устройства может быть гораздо больше забавных и даже полезных применений, чем предлагает производитель (а его можно использовать, например, для индикации состояния компьютера, к которому не подключен монитор). Все, что для этого нужно – разобраться в работе устройства и написать для него свою программу управления.
Обратный инжиниринг
Важнейшим инструментом при написании драйвера для нового USB-устройства является USB-сниффер. Так же, как сетевые снифферы перехватывают сетевые пакеты и позволяют подсматривать их содержимое, USB-снифферы перехватывают пакеты USB. Снифферы бывают программные и аппаратные. Мы, естественно, сосредоточимся на первой категории.
Организовать «перлюстрацию» USB-пакетов в системе Linux очень легко. С незапамятных времен ядро включает модуль usbmon, который, собственно, этим и занимается. Для подключения модуля usbmon командуем:
modprobe usbmon
Теперь мы можем просматривать USB-трафик с помощью команды cat, например:
cat /dev/usbmon1
Однако содержимое специальных файлов, созданных usbmon, трудночитаемо. К счастью, у нас есть очень мощный инструмент – программа Wireshark (рис. 2). Традиционно она применяется для анализа сетевого трафика, однако с некоторых пор (версия 1.2 и выше) умеет читать и пакеты USB. Чтобы Wireshark смог читать трафик, генерируемый usbmon, следует подмонтировать специальные файловые системы
mount -t usbfs /dev/bus/usb /proc/bus/usb
В списке наблюдаемых интерфейсов Wireshark мы можем выбрать интерфейс USB X.Y, где X.Y – адрес интересующего нас устройства на шине USB (а узнать, какой адрес получило интересующее нас устройство, можно с помощью lsusb). Отмечу небольшой «глюк», с которым я столкнулся при работе с трафиком USB в Wireshark 1.2.1: фильтр, который программа по умолчанию применяет к интерфейсу USB, рассчитан на пакеты TCP/IP и вызывает ошибки. Чтобы это исправить, щелкните кнопку Capture Options... и в открывавшемся окне отредактируйте или вообще очистите строку Capture Filter.
Wireshark позволяет нам выбирать уровень детализации информации о пакетах, задавать специальные фильтры для отбора только интересующих нас данных, сохранять результаты в различных форматах, и что особенно ценно – корректно обрабатывать ситуации подключения и отключения устройств, во время которых с последними происходит много интересного.
И, тем не менее, на данном этапе средства мониторинга пакетов в ОС Linux нам не подходят. Ведь наша задача заключается в том, чтобы выяснить, как именно фирменная программа командует устройством. А программа эта предназначена для Windows. Если вы сторонник бесплатного ПО, можете воспользоваться пакетом USB Snoopy (сайт проекта закрылся, но программу еще можно скачать на таких ресурсах, как http://softpedia.com). Этот пакет состоит из фильтр-драйвера USB и утилиты для управления им. Для просмотра результатов используется программа DebugView, написанная известным исследователем внутренностей Windows Марком Руссиновичем [Mark Russinovich]. По удобству использования USB Snoopy уступает Wireshark (кстати, в документации Wireshark сказано, что Windows-версия этой программы тоже может отслеживать трафик USB, но процедура настройки Wireshark выглядит довольно сложной, и я ее не пробовал). В мире же платного ПО мне более других приглянулась программа Device Monitoring Studio (http://www.hhdsoftware.com). Распространяется она как shareware, стоит недорого, а первые две недели после установки ею можно пользоваться бесплатно (если только вам не надоедают напоминания об активации). В плане перехвата и анализа пакетов USB программа Device Monitoring Studio может делать все то, что может Wireshark+usbmon, и даже немного больше.
Базовых знаний протокола USB вполне достаточно, чтобы разобрать вывод программ Wireshark и Device Monitoring Studio. Я рекомендую вам последить за трафиком какого-нибудь устройства – например, мыши – чтобы лучше понять, как работает USB.
Вообще, хотя мы пользуемся самым «безопасным» способом написания драйверов, я рекомендую, во избежание потерь данных и времени на восстановление, выделить для экспери ментов отдельный компьютер. Можно воспользоваться и виртуальной машиной, но надо помнить, что эмуляция USB работает все же не совсем так, как «живой» интерфейс USB, а потому и результаты тестирования могут немного отличаться.
Смертельный пакет
Во времена моей компьютерной юности пользователи (а иногда и программисты) пугали друг друга историями о том, как коварные вирусы физически разрушают жесткие диски, многократно роняя считывающую головку на магнитную поверхность. Говорили еще, что такая кара может постичь нелицензионных пользователей Windows 95. На самом деле, подавляющее большинство периферийных устройств, предназначенных для массового использования, спроектировано так, что убить их случайной комбинацией байтов, переданных с компьютера, практически невозможно. Чаще всего устройство возвращается к жизни простым отключением и повторным включением. Иногда, правда, может потребоваться программатор... Но, в общем, экспериментируйте смело. Если вы все же найдете команду-убийцу для потребительского устройства – пишите письма на форумы и разработчикам «железки». Возможно, вас даже возьмут на работу.
Декодируем протокол
Рассмотрим фрагмент выдачи программы Device Monitoring Studio:
000006: Class-Specific Request (DOWN), 18.09.2010 11:40:17.171 +0.0 Destination: Interface, Index 0 Reserved Bits: 34 Request: 0x9 Value: 0x200 Send 0x8 bytes to the device 00 00 02 02 00 01 14 05
Мы перехватили специфичный для класса устройства запрос с передачей данных от хоста устройству (DOWN). Точные временные параметры позволяют отслеживать задержки между командами USB. Назначением запроса является интерфейс, индекс 0. Номер запроса – 0x9, значение – 0x200, вслед за установочным пакетом передается 8 байтов данных (они приведены в последней строке).
Исследования трафика USB нашего почтового индикатора выявили, что хост обращается к устройству посредством контрольных запросов, специфичных для класса устройства, с номером запроса 0x9 и значением 0x200. Вслед за установочным пакетом хост передает одну из 8‑байтных команд.
В начале работы устройству посылаются команды
0x1F 0x1E 0x92 0x7C 0xB8 0x01 0x14 0x03 0x00 0x1E 0x92 0x7C 0xB8 0x01 0x14 0x04
Это, судя по всему, «волшебные числа», которые необходимы для инициализации устройства. Возможно, изменения каких-то параметров этих команд влияют на параметры инициализации устройства – я не проверял. Управление светодиодами осуществляет команда
R G B ? ? 0x01 0x14 0x05
Первые три байта – значения яркости трех светодиодов. Что делают байты 4 и 5, я так и не выяснил. Наблюдения за трафиком показывают, что фирменная программа иногда записывает в них какие-то значения, но внешне это никак не влияет на работу устройства.
В ответ на команду с конечной точки 0x01 устройство посылает хосту прерывание – 8 байт, которые, судя по всему, имеют значения
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01
если команда выполнена, и
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
в противном случае.
Вообще, обратный инжиниринг – хороший тест на IQ. Помните задания, в которых предлагается угадать недостающие или пропущенные члены числовой последовательности? При анализе трафика USB нам часто приходится заниматься тем же самым.
Например, анализируя приведенные выше команды, можно предположить, что устройство поддерживает также команды
? ? ? ? ? 0x01 0x14 0x00 ? ? ? ? ? 0x01 0x14 0x01 ? ? ? ? ? 0x01 0x14 0x02
Я не проверял, существуют ли такие команды в действительности, и тем более, что они делают. Если почтовый индикатор попадет к вам в руки – можете попробовать сами.
Теперь мы знаем все, что нужно для написания собственного драйвера устройства. Осталось его реализовать.
С человеческим лицом
Помните вывод lsusb в начале статьи? В нем есть такая строка:
bInterfaceClass 3 Human Interface Device
Это значит, что наша светящаяся коробочка принадлежит к классу HID – Human Interface Devices, то есть устройств, предназначенных для непосредственного взаимодействия с человеком. Среди устройств USB класс HID является самым сложным и многообразным. Подробности вы можете узнать в официальной спецификации Device Class Definition for Human Interface Devices, которая доступна по адресу http://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 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.