Thursday, July 3, 2014

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


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

Хотим мы того или нет, но многообразные порты, унаследованные от компьютеров IBM PC и PS/2, уходят в прошлое. Будущее, да и настоящее, принадлежит универсальным скоростным портам типа USB и Firewire. Об удобствах, которые USB предоставляет простым пользователям ПК, распространяться не приходится. Единый интерфейс для всех устройств, обладающий возможностями Plug'n'Play и продвинутого управления питанием – именно то, что нужно пользователям, для которых компьютер – часть бытовой техники. Другое дело – индивидуальные разработчики различных устройств и просто хакеры. Для этих категорий переход на USB представляет определенные сложности. Проблема заключается в том, что USB – «интеллектуальный» интерфейс. Любое устройство, предназначенное для подключения к компьютеру через USB, должно поддерживать хотя бы небольшую часть спецификации протокола USB: уметь «представиться» (предоставить информацию о себе и своих возможностях) и адекватно реагировать на стандартные сообщения USB, посылаемые компьютером. В результате, даже устройство, все функции которого ограничиваются включением и выключением светодиода по сигналу с компьютера, при подключении через USB требует наличия микросхемы, которая умеет «разговаривать» с хостом. Однако и для разработчиков собственных устройств переход на USB несет определенные преимущества. Прежде всего, упрощается процесс написания драйверов. Поскольку для общения с компьютером все USB устройства используют единый протокол, причем протокол этот абстрагирован от таких аппаратно-зависимых вещей как отображенные в память порты и прерывания, возникает возможность не писать свой собственный драйвер уровня ядра для каждого устройства. Вместо этого целые группы устройств могут использовать один и тот же драйвер уровня ядра, а специфичный код, учитывающий особенности конкретного устройства, может быть размещен на пользовательском уровне. При этом драйвер уровня ядра берет на себя такие функции как управление питанием устройства (весьма нетривиальная задача с учетом того, что сам компьютер может переключаться между несколькими энергосберегающими режимами), оставляя нам самое интересное – управление функциями устройства.

Перенос кода управления устройством в область пользователя не только упрощает отладку (при падении приложения, скорее всего, не придется перезагружать машину), но и позволят писать процедуры управления устройством на самых разных языках программирования, а не только на C, как это делают те, кто пишет драйверы уровня ядра. Более того, пользовательская часть драйвера, не взаимодействующая напрямую с механизмами ядра операционной системы, может быть сделана кросс-платформенной (что мы и имеем в случае таких инструментов как libusb и Jungo WinDriver). Благодаря libusb и Jungo WinDriver даже многие устройства промышленного уровня могут обходиться без собственных драйверов на уровне ядра. Что уж говорить о любительских устройствах?

Протокол USB

Протокол USB похож на стек сетевых протоколов, основанных на TCP/IP (который, отчасти и послужил его прототипом). Так же как в случае с сетевыми протоколами, протокол USB можно разделить на несколько уровней. На самом нижнем логическом уровне (спецификации физического уровня мы не рассматриваем) устройства обмениваются пакетами данных (с встроенными механизмами коррекции ошибок, подтверждения получения и т.д). Из пакетов формируются запросы, которые устройства посылают друг другу. Запросы составляют блоки запросов USB (USB Request Block, URB).

Протокол USB является «хостоцентричным» - процесс передачи данных всегда инициируется хостом (то есть, компьютером). Если у периферийного устройства появились данные для передачи хосту, оно должно ожидать запроса хоста на передачу данных. Существует четыре типа передач данных:

• Передача управляющих данных (control transfer) предназначена для определения параметров и настройки периферийных устройств, а так же для передачи коротких команд. Полезная часть блока управляющих данных состоит из установочного пакета (setup packet) и (возможно) нескольких байтов данных. Установочный пакет содержит информацию о запросе, который хост направляет устройству, направлении передачи дополнительных данных (от хоста к устройству или наоборот), логическом адресате данных (устройство, интерфейс) и количестве байт дополнительных данных.
• Передача прерываний (interrupt transfer, их не следует путать с прерываниями в компьютере) используется для передачи коротких сообщений, в основном - от устройства хосту. Поскольку инициатива в обмене данными всегда исходит от хоста, прерывания, посылаемые устройством, не могут прервать порядок работы хоста. Однако устройство ожидает, что хост будет опрашивать его на предмет прерываний с определенной частотой (эта частота определяется в процессе настройки соединения). Таким образом, устройство может рассчитывать, что задержка при передаче прерываний не превысит определенного значения. При этом количество данных, передаваемое в одном прерывании, ограничено (8-мью, 64-мя или 1024-мя байтами, в зависимости от скоростных параметров устройства). Следует учитывать, что «гарантированное максимальное время задержки» гарантировано только для доставки прерывания хосту. Фактическая обработка выполняется ПО и может быть отложена на неопределенное время. 
• Изохронная передача данных (isochronous transfer) используется для передачи данных мультимедиа, для которых важна гарантированная пропускная способность, но потеря отдельных пакетов из-за помех несущественна (в этом смысле изохронная передача напоминает потоки данных UDP). Изохронная передача возможна в обоих направлениях. Обычно этим способом передаются живые мультимедиа-данные (например, видео, получаемое с цифровой камеры в режиме он-лайн). 
• Массовая передача данных (bulk trabsfer) – передача больших объемов данных с гарантированной доставкой, но негарантированной максимальной задержкой и полосой пропускания. Примером такой передачи данных могут служить данные, передаваемые хостом принтеру или данные, которыми хост обменивается с устройством хранения данных. Массовая передача данных так же возможна в обоих направлениях.

Продолжая аналогию между протоколами USB и сетевыми протоколами, мы можем вспомнить, что «пункт назначения» сетевого протокола включает в себя помимо адреса еще и порт. Аналогом сетевого порта в протоколе USB может служить конечная точка (endpoint). Каждое устройство USB поддерживает конечную точку с номером 0x00, предназначенную для передачи управляющих данных. Помимо этого устройство может предоставлять еще несколько конечных точек, каждая из которых предназначена для определенного типа передачи данных в определенном направлении. Например, если устройству требуется принимать данные с помощью массовой передачи и передавать прерывания, оно предоставит две дополнительные конечные точки. Помимо направления передачи данных в описании конечной точки указывается максимальный размер передаваемого пакета в байтах. Если конечная точка предназначена для передачи прерываний, ее описание содержит информацию о том, с какой частотой следует опрашивать эту точку. Группы конечных точек доступа устройства объединяются в интерфейсы. Интерфейсы объединяются в конфигурации, которые, помимо прочего, включают описания режимов питания устройства. У большинства устройств присутствует интерфейс, состоящий из двух точек доступа: для передачи управляющих сообщений и для передачи прерываний от устройства хосту. Если, помимо передачи управляющей информации устройству нужно передавать большие объемы данных в двух направлениях, оно может предоставить еще один интерфейс, объединяющий две точки доступа, предназначенные для массовой передачи данных, и т.д.

Само устройство идентифицируется двумя числами: идентификатором производителя (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

Команда выдаст более подробные севедения о выбранном устройствк. Фрагмент выдачи команды приводится ниже

Device Descriptor: bLength 18   bDescriptorType 1   bcdUSB 1.10   bDeviceClass 0 (Defined at Interface level)   bDeviceSubClass 0   bDeviceProtocol 0   bMaxPacketSize0 8   idVendor 0x1d34   idProduct 0x0004   bcdDevice 0.02   iManufacturer 1 Dream Link iProduct 2 DL100B Dream Cheeky Generic Controller   iSerial   0 bNumConfigurations   1 Configuration Descriptor:   bLength 9   bDescriptorType 2   wTotalLength 34   bNumInterfaces 1   bConfigurationValue 1   iConfiguration 0   bmAttributes 0x80 (Bus Powered)   MaxPower 500mA   Interface Descriptor:   bLength 9   bDescriptorType 4   bInterfaceNumber 0   bAlternateSetting 0   bNumEndpoints 1   bInterfaceClass 3   Human Interface Device   bInterfaceSubClass 0 No Subclass   bInterfaceProtocol 0 None   iInterface 0   HID Device Descriptor:   bLength 9   bDescriptorType 33   bcdHID 1.10   bCountryCode 0 Not supported   bNumDescriptors 1   bDescriptorType 34 Report   wDescriptorLength 37 Report   Descriptors: ** UNAVAILABLE **   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, известной своими USB-ракетницами, подогревателями кофе и другими столь же полезными изделиями. Рассматриваемое устройство (рис. 1) позиционируется компанией как индикатор поступления электронной почты. Сам девайс представляет собой пластиковую коробочку с изображением конверта, которая подсвечивается изнутри с помощью комбинации трех светодиодов, красного, синего и зеленого (поскольку каждый светодиод обладает 256 градациями яркости, мы имеем возможность выбирать цвет из 24-битной палитры). В комплекте с устройством идетWindows-программа, которая умеет опрашивать состояние указанных пользователем почтовых ящиков и выдавать определенные световые эффекты при поступлении почты.


Рисунок 1. Вам письмо!

С моей точки зрения Dream Cheeky Webmail Notifier представляет собой яркий пример «железа», возможности которого искусственно ограничены сопутствующим софтом. Хотя подсвечивание пластика разноцветными светодиодами и нельзя назвать богатой функциональностью, у девайса может быть гораздо больше забавных и даже полезных применений, чем предлагает производитель (устройство можно использовать, например, для индикации состояния компьютера, к которому не подключен монитор). Все что для этого нужно – разобраться в работе устройства и написать для него свою программу управления.

Обратный инжиниринг

Важнейшим инструментом при написании драйвера для нового USB-устройства является USB-сниффер. Так же как сетевые снифферы перехватывают сетевые пакеты и позволяют подсматривать их содержимое, USB-снифферы перехватывают пакеты USB. Снифферы бывают программные и аппаратные. Мы, естественно, сосредоточимся на первой категории.

Организовать «перлюстрацию» USB-пакетов а системе Linux очень легко. С незапамятных времен ядро системы включает модуль usbmon, который, собственно, этим и занимается. Для подключения модуля usbmon командуем:

modprobe usbmon

Теперь мы можем просматривать USB-трафик с помощью команды cat, например:

  cat /dev/usbmon1

Однако содержимое специальных файлов, созданных usbmon, трудночитаемо. К счастью у нас есть очень мощный инструмент – программа Wireshark (рис. 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 (версии, предшествующие 1.2, вообще не умеют читать USB пакеты): фильтр, который программа применяет по умолчанию к интерфейсу USB, рассчитан на пакеты TCP/IP и вызывает ошибки. Для того чтобы это исправить, щелкните кнопку Capture Options... и в открывавшемся окне отредактируйте или вообще очистите строку Capture Filter.


Рисунок 2. Wireshark: все пакеты USB как на ладони.

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

И, тем не менее, на данном этапе средства мониторинга пакетов в ОС Linux нам не подходят. Ведь наша задача заключается в том, чтобы выяснить, как именно фирменная программа командует устройством. А программа эта предназначена для Windows (можно, конечно, отлавливать в Linux пакеты USB, предназначенные для Windows, запустив винду в вирутальной машине из-под Linux, но мы пойдем более простым путем).

Если вы сторонник бесплатного ПО, можете воспользоваться пакетом USB Snoopy (www.wingmanteam.com/usbsnoopy/). Этот пакет состоит из фильтр-дравйвера USB и утилиты для управления им. Для просмотра результатов используется программа DebugView, написанная известным исследователем внутренностей Windows Марком Руссиновичем. По удобству использования USB Snoopy уступает Wireshark (кстати, в документации по Wireshark написано, что Windows-версия этой программы тоже может отслеживать трафик USB, но процедура настройки Wireshark выглядит довольно сложно и я ее не пробовал). В мире же платного ПО мне более других приглянулась программа Device Monitoring Studio (www.hhdsoftware.com). Программа эта распространяется как shareware, первые две недели после установки можно пользоваться бесплатно (если только вам не надоедают напоминания об активации). В плане перехвата и анализа пакетов USB программа Device Monitoring Studio может делать все то, что может Wireshark+usbmon и даже немного больше.


Рисунок 3. Wireshark: Device Monitoring Studio тоже следит за вами

Базовых знаний протокола 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 

Я не проверял, существуют ли такие команды в действительности, и, тем более, что они делают. Если почтовый индикатор попадет к вам в руки – можете попробовать сами.

Теперь мы знаем все, что нужно для написания собственного драйвера устройства. Осталось его реализовать.

Вторая часть - реализация драйвера

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


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


www.symmetrica.net

No comments:

Post a Comment