Как написать драйвер для usb устройства

Время на прочтение
13 мин

Количество просмотров 168K

Целью этой статьи является пошаговая демонстрация процесса разработки всего набора программного обеспечения необходимого для организации связи самодельного устройства с компьютером посредством USB.

На данный момент, большинство радиолюбителей реализуют такой тип подключения используя чипы переходники USB в RS232 таким образом организуя связь со своим устройством посредством драйвера виртуального COM порта поставляемого с чипом переходником. Минусы такого подхода думаю понятны. Это как минимум лишний чип на плате и ограничения накладываемые этим чипом и его драйвером.
Мне же хочется осветить весь процесс организации такого взаимодействия так как оно и должно быть сделано, и как делается во всех серьезных устройствах.
В конце концов, сейчас 21-й век, модуль USB есть почти во всех микроконтроллерах. Именно о том, как наиболее быстро воспользоваться этим модулем и будет эта статья.

Так как для демонстрации процесса написания драйвера USB устройства нам необходимо собственно само устройство, то выберем одну из распространенных отладочных плат доступных в России. У меня это плата производства компании OLIMEX модель LPC-P2148. Основой платы является микроконтроллер LPC2148 архитектуры ARM7TDMI производства компании NXP. Всю информацию по плате можно получить на сайте производителя по следующей ссылке. Вот как она выглядит.

image

Выбор контроллера и отладочной платы абсолютно не принципиален т.к. процесс разработки взаимодействия между ОС на персональном компьютере и самой платой от этого не зависит. Среду разработки прошивки микроконтроллера будем использовать KEIL версии 4.23, что так же не принципиально. В итоге, планируется реализовать только BULK тип передачи. Будем считывать массив данных из устройства в компьютер, а передавать на устройство будем состояние светодиодов, чтобы было видно, что плата реагирует на наши команды.

Для удобства понимания разделим дальнейшие действия на стадии и будем проходить их по-порядку.

1. Адаптация готового примера USB устройства под нашу плату с целью убедиться, что плата работает и USB канал так же работоспособен. Это будет как бы наша стартовая точка.
2. Изменение прошивки платы, чтобы она стала для Windows неизвестным устройством, требующее драйвер производителя.
3. Адаптация базового шаблона, пустого драйвера, чтобы Windows могла его корректно установить, для обслуживания нашего устройства.
4. Реализация взаимодействия драйвера с пользовательским приложением.
5. Написание консольного приложения Windows для работы с нашим драйвером, а следовательно и подключенным USB устройством.
6. Наполнение всей системы необходимыми функциями.

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

1. Адаптация примера RTX_Memory под плату OLIMEX LPC-P2148

За основу прошивки к нашему проекту мы возьмем пример RTX_Memory поставляемый вместе с KEIL. Данный пример, когда успешно заработает, позволит нашу плату подключать к компьютеру и она будет там видна как обычная USB флешка. Таким образом мы получим прошивку, которая заведомо корректно настраивает USB модуль и всю необходимую процессору периферию.

Проект находится в папке ARMBoardsKeilMCB2140RLUSB. Пути здесь и далее я буду указывать относительно основной папки, куда установлена среда KEIL.

Скопируем проект в отдельное место, загрузим его в KEIL и соберем. Собраться должен без ошибок. В итоге мы получили HEX файл, который можем прошить с помощью утилиты FlashMagic.
Правда можно пока его не прошивать так как очевидно, что он работать на нашей плате не будет.
Если сравнить схему нашей платы и платы для которой написан пример, а это модель MCB2140 производства KEIL, то видно различия в подключении подтяжки линии D+.
На плате MCB2140 она всегда подтянута к 3.3В, а на LPC-P2148 этой подтяжкой управляет микроконтроллер через транзистор.

Схемы обеих плат доступны на сайтах www.olimex.com и www.keil.com соответственно.

Для простоты, мы немного изменим код инициализации, чтобы наша плата всегда при включении включала подтяжку линии D+, о чем будет сообщать светодиод USB_LINK.
В процедуре USB_Init() отключим линию CONNECT от модуля USB и будем ею управлять сами. А так как на этом же транзисторе есть еще и светодиод USB_LINK то получится, когда мы его включим, автоматически вулючится и подтяжка линии D+.

Кроме того, на нашей плате меньше светодиодов чем у MCB2140. По-этому их назначение так же нужно переопределить. На данном этапе я их переназначил просто для индикации процессов чтения/записи.
Так как у нас нет индикаторов LED_CFG и LED_SUSP то закоментируем их использование везде по коду проекта.
Теперь можно собрать проект и прошить его в контроллер. Подключив плату к компьютеру, видно, что он ее распознает как внешний накопитель и в системе появляется еще один диск размером всего около 25КБайт и с файлом readme.txt.

На этом первый этап можно считать законченным.

2. Переход от USB накопителя к уникальному устройству.

На данный момент мы имеем устройство, которое на любом компьютере с любой ОС будет распознаваться, как внешний USB накопитель. Но нам требуется, чтобы Windows не знала, ким образом работать с нашим устройством и требовала драйвер. О том, что подключенное устройство относится ко классу накопителей, говорит параметр Interface class находящийся в дескрипторе интерфейса.

Если открыть файл usbdesc.c и найти там этот параметр то будет видно что он имеет значение USB_DEVICE_CLASS_STORAGE.

Заменим его на USB_DEVICE_CLASS_VENDOR_SPECIFIC, и следующие за ним два поля заменим на нули.
Теперь пересобрав проект и прошив плату мы увидим, что Windows больше не знает, что наше устройство является накопителем и требует предоставить подходящий драйвер.

Тут может возникнуть проблема. Дело в том, что Windows запомнив VID и PID нашего устройства в предыдущий раз, как относящиеся к устройству внешнего хранения, может продолжать ставить на него свой драйвер не обращая внимание на то, что класс устройства поменялся. Решение простое. Если плата по-прежнему определяется как накопитель, найдите ее в ветке USB диспетчера устройств и удалите драйвер вручную. После этого ОС должна начать просить драйвер.

3. Создаем базовый драйвер.

Итак, у нас есть рабочее USB устройство для которого требуется предоставить драйвер.
Для начала мы напишим самый простой драйвер, который не будет делать ничего полезного, кроме как загружаться в систему при появлении нашего устройства на шине USB. Драйвер будет иметь минимальный код, чтобы только корректно загрузиться и выгрузиться системой.

Писать драйвер мы будем самым минималистическим методом. Сам код будет редактироваться в блокноте, а собираться будет в командной строке.

Для начала, нужно скачать с сайта Microsoft набор для разработки драйвером. Называется он Windows Driver Kit. Я использую версию WDK 7600.16385.1.

После установки, мы получим много примеров, окружение для сборки и документацию. В меню пуск, нужно найти раздел WDK и там Build Environments. Это так называемые окружения для сборки. Фактически они предоставляют нам консоль, которая уже настроина так, чтобы собирать драйверы для нужной системы.

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

Я буду использовать далее окружение «x86 Checked Build Environment» от windows XP. Это даст мне универсальный драйвер корректно работающий на системах от Windows XP и новее.

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

Самым подходящим кандидатом оказался пример к некой плате OSR USB-FX2 learning kit. Что это за плата я абсолютно не имею понятия, но нужный нам пример находится в WDK по пути srcusbosrusbfx2. Самое интересное, что это не просто пример, а пошаговое обучение, как сделать драйвер к этой плате. Как раз то, что нам и нужно. Зайдем глубже в директорию kmdfsys и видим, что там все шаги и лежат по папочкам. Подробнее о них можно почитать в описании примера, находящемся в файле osrusbfx2.htm.

Тут я сделаю небольшое отступление, чтобы немножко сделать более понятней следующие действия.
Дело в том, что с момента появления Windows NT кое что изменилось в процессе написания драйвера. В те времена нам приходилось напрямую использовать функции ядра ОС и часто, просто чтобы сделать пустышку способную правильно загружаться, выгружаться, отвечать на события PNP и т.п. базовые функции, приходилось много чего изучить и не один раз вылететь в BSOD. Потом Microsoft сделала модель, которую назвала Windows Driver Model и которая внесла некоторого рода стандарт что ли, как должен выглядеть драйвер. Особого облегчения, лично я от этого не почувствовал. А следующим шагом был сделан фреймворк, который называется Windows Driver Framework. И вот благодаря этому жить стало намного проще. Теперь фреймворк берет на себя реализацию всех базовых действий необходимых для обслуживания основных событий, а нам останется только правильным образом добавить нужных нам функций. Вот именно эту технологию мы и будем использовать.

Начинаем с первого шага. Запускаем «x86 Checked Build Environment» и при помощи команды “cd” перемещаемся в папку WinDDK7600.16385.1srcusbosrusbfx2kmdfsysstep1.

Выплняем команду build -ceZ.

Происходит процесс сборки, и в результате создается папка objchk_wxp_x86( ее название зависит от выбранного окружения ), где мы и находим файл с расширением sys. Это и есть наш драйвер. Чтобы установить его, нам нужен INF файл. Найдем его в папке final этого же проекта. Она называется osrusbfx2.inf. Проблема только в том, что он рассчитан на плату из примера. Чтобы этот файл был способен установить драйвер для нашей платы, просто поменяем в нем везде значения VID и PID на те, которые прописаны в дескрипторе USB устройства в файле usbdesc.c. Просмотрев глазами INF файл, можно заметить, что для установки драйвера еще требуется файл WdfCoInstaller01009.dll. Он тоже находится в поставке WDK.

Итак, копируем в отдельную папку три файла: собранный SYS, INF, WdfCoInstaller01009.dll.

Подключаем нашу плату к компьютеру, и на вопрос Windows о пути к драйверу указываем эту папку.

Наблюдаем обычный процесс копирования файлов драйвера и в диспетчере устройств появляется наше устройство под классом Sample Device. Все, операционная система удовлетворена!

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

В режиме ядра, отладочную информацию выводит функция KdPrint(). Ее использование такое же, как всем известной printf(). Чтобы увидеть ее вывод, нужно установить программу DbgView. Она доступна на сайте Microsoft по ссылке http://technet.microsoft.com/en-us/sysinternals/bb896647. Просто держите ее запущенной и будете видеть вывод всей отладочной информации из режима ядра ОС. Я обычно настраиваю фильтр, чтобы отображались только сообщения нужного мне модуля. В моем варианте Step_1 я добавил вывод в процедуры DeviceEntry() и DeviceAdd() так, что он просто пишет какая функция вызвалась. Подключая и отключая плату, в окне DbgView хорошо видно в каком порядке это происходит.

4. Взаимодействие между режимами ядра и пользователя.

Как известно, драйверы устройств работают в режиме ядра( за некоторым исключением ), а наши приложения в режиме пользователя. Для взаимодействия используется тот же механизм, что и для работы с файлами. Иными словами, для каждого подключенного устройства в системе есть символическое имя, по которому его можно открыть, как обычный файл. Ну а потом использовать обычные процедуры для работы с файлами типа ReadFile() и WriteFile(). В этой части, мы добавим в наш драйвер функционал, позволяющий его открывать, закрывать, писать и читать из него данные.

Записанные данные будем сохранять, чтобы потом отдавать их при операции считывания.

Первое, что нужно сделать, это зарегистрировать свой callback функцию для события EvtDevicePrepareHardware, которую вызовет менеджер PnP после того, как устройство перейдет в неинициализированное состояние D0 и перед тем, как сделать его доступное драйверу. По сути это означает очень простую вещь, устройство мы воткнули, драйвер загрузился, но возможно ваше устройство требует некоторой настройки перед тем, как с ним станет возможно работать. Вот такого рода настройку мы и сделаем в этом событии. В применении к USB, как минимум нужно выбрать нужную конфигурацию. Итак, регистрируем нашу функцию. Для этого добавляем в DriverEntry следующий код:

WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnpPowerCallbacks);
pnpPowerCallbacks.EvtDevicePrepareHardware = EvtDevicePrepareHardware;
WdfDeviceInitSetPnpPowerEventCallbacks(DeviceInit, &pnpPowerCallbacks);

Второе. Если обратите внимание на вызов процедуры WdfDeviceCreate из кода драйвера предыдущего параграфа, то можно заметить, что второй параметр этой процедуры передается константа WDF_NO_OBJECT_ATTRIBUTES. Означает это, что объект устройства не имеет никаких атрибутов. Но в реальной жизни нам понадобится как минимум один атрибут. Это так называемый контекст устройства. Упрощенно говоря, это некоторого рода структура, которая относится к конкретному экземпляру устройства поддерживаемого драйвером, и будет далее доступна нам практически в любом месте драйвера. Например она может содержать какой-нибудь буфер. А привязывается она к объекту устройства, а не драйвера т.к. К компьютеру может быть подключено несколько одинаковых устройств, которые будет обслуживать один и тот же драйвер, но все они будут иметь свой собственный объект устройства.

Итак, создадим структуру контекста, и инициализируем ею, параметр атрибутов, передаваемый далее в WdfDeviceCreate:

typedef struct _DEVICE_CONTEXT {
WDFUSBDEVICE UsbDevice;
WDFUSBINTERFACE UsbInterface;
WDFUSBPIPE BulkReadPipe;
WDFUSBPIPE BulkWritePipe;
} DEVICE_CONTEXT, *PDEVICE_CONTEXT;
WDF_DECLARE_CONTEXT_TYPE_WITH_NAME(DEVICE_CONTEXT, GetDeviceContext)

WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&attributes, DEVICE_CONTEXT);

Третье. Теперь необходимо создать интерфейс, через который драйвер станет доступный программам пользовательского режима. Раньше, программист должен был сам жестко прописывать имя, по которому мог быть открыт доступ к устройству через процедуру CreateFile. Теперь все стало проще. Нам нужно только создать интерфейс вызвав одну процедуру, а для его идентификации используется сгенерированный GUID. Далее в пользовательском режиме мы будем использовать этот же GUID, чтобы получить имя файла устройства. Итак, вот наш GUID и код связывающий его с интерфейсом:

DEFINE_GUID(GUID_DEVINTERFACE_OSRUSBFX2, // Generated using guidgen.exe
0x573e8c73, 0xcb4, 0x4471, 0xa1, 0xbf, 0xfa, 0xb2, 0x6c, 0x31, 0xd3, 0x84);
// {573E8C73-0CB4-4471-A1BF-FAB26C31D384}

status = WdfDeviceCreateDeviceInterface(device,
(LPGUID) &GUID_DEVINTERFACE_OSRUSBFX2,
NULL);// Reference String

Последнее. В первом пункте мы зарегистрировали процедуру, обрабатывающую событие EvtDevicePrepareHardware. Теперь нужно ее написать. Не буду переписывать ее текст в статью, думаю проще будет глянуть в исходном коде. Скажу только, что в этой процедуре, мы подготавливаем все что нужно для последующей работы драйвера с подключенным устройством. А конкретно создаем объект USB устройства, выбираем нужную конфигурацию, и сохраняем в контексте устройства идентификаторы каналов, относящихся к BULK конечным точкам реализованного в устройстве интерфейса. Нам эти идентификаторы понадобятся позже, для реализации передачи данных. Для наглядности, я добавил вывод параметров каналов в DbgView. Можно заметить, что их параметры — это ни что иное, как те же самые значения, которые мы прописали в дескрипторах конечных точек в файле usbdesc.h прошивки.
Итак, теперь можно опять пересобрать драйвер, и обновить его в системе. На данный момент наш драйвер может уже не просто загрузиться. Он уже умеет настраивать подключенное устройство, и, что самое важное, стал доступен для программ из режима пользователя.

5. Работаем с драйвером из режима пользователя.

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

Работа с устройствами, сводится к открытию их, как обычного файла, и записи и чтения данных при помощи обычных процедур WriteFile и ReadFile. Есть еще очень полезная процедура DeviceIoControl, для организации взаимодействия с драйвером, которое выходит за формат работы с файлами, но мы ее использовать не будем. Открывается файл обычным вызовом CreateFile, вот только нам нужно имя файла. И тут нам пригодится GUID, который мы привязали к интерфейсу драйвера. Я не буду описывать всю процедуру получения имени через GUID, и честно признаюсь, что полностью взял ее из примеров WDK. Процедура GetDevicePath получает GUID и возвращает полный путь ему соответствующий.

Файл открыт. Добавим пару вызовов, которые запишут и считают из файла десяток байт.

Но вернемся к нашему драйверу. В пользовательской программе мы уже пишем в драйвер и читаем из него, но сам код драйвера про это ничего не знает. Исправим ситуацию.

Логика тут такая же, как и с EvtDevicePrepareHardware. Нам нужно зарегистрировать callback функции, которые вызовутся, когда произойдут процедуры чтения из драйвера или записи в него. Делается это в EvtDeviceAdd. Необходимо инициализировать очередь ввода/вывода, заполнить ее поля указателями на наши callback функции и создать ее, прицепив к объекту устройства. Поехали:

WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&ioQueueConfig,
WdfIoQueueDispatchParallel);

ioQueueConfig.EvtIoRead = EvtIoRead;
ioQueueConfig.EvtIoWrite = EvtIoWrite;

status = WdfIoQueueCreate(device,
&ioQueueConfig,
WDF_NO_OBJECT_ATTRIBUTES,
WDF_NO_HANDLE);

Кроме объявления процедур чтения и записи, нужно не забыть их реализовать. На данном этапе я просто поставил заглушки, которые выводят переданные данные в DbgView и отдают массив из 10 байт при чтении. Код их вы можете посмотреть в исходниках. Там ничего интересного, только советую обратить внимание на работу с памятью. Необходимо по определенным правилам получать буферы т.к. Данные у нас перемещаются между режимами ядра и пользователя. На скриншоте хорошо видно, как мы посылаем данные в драйвер и они появляются в окне DbgView. Потом мы читаем пакет из драйвера и получаем его в выводе консольного приложения.

6. Делаем драйвер полезным.

Вот и настало время, сделать наш драйвер полезным. На данный момент он производит коммуникацию с режимом пользователя но никак не работает с реальным устройством. А все что нам осталось сделать, это в процедуре записиси добавить код, передающий данные на устройство, а в процедуре чтения — код принимающий данные с устройства. В исходниках вы видите, как совсем незначительно изменились процедуры обслуживающие ввод/вывод в драйвере. Мы всего лишь передаем наши буферы далее подсистеме USB ядра, а она уже все сделает, как нужно.
Перед началой реальной передачи данных между PC и устройством, нам еще нужно изменить прошивку устройства, чтобы она как-то реагировала на наши данные.

Изменим немного код в обработке события приема данных таким образом, чтобы если первый принятый байт 0x01 то включим LED_1, а если он 0x02 то включим LED_2. А т.к. После записи в устройство мы из него сразу читаем 10 байт, то добавим этот код тоже. Обратите внимание, что мы отправляем пакет на передачу в событии обработки входящего пакета. Это такая особенность работы модуля USB. Нам нужно заранее отдать ему данные для передачи, чтобы он мог исполнить IN транзакцию. А для наглядности, будем передавать два разных массива. Меняем содержимое MSC_BulkOut() следующим образом:

void MSC_BulkOut (void) {

BulkLen = USB_ReadEP(MSC_EP_OUT, BulkBuf);

LED_Off( LED_RD | LED_WR );
if( BulkBuf[ 0 ] == 0x01 )
{
USB_WriteEP( MSC_EP_IN, (unsigned char*)aBuff_1, sizeof( aBuff_1 ) );
LED_On( LED_RD );
}
else
if( BulkBuf[ 0 ] == 0x02 )
{
USB_WriteEP( MSC_EP_IN, (unsigned char*)aBuff_2, sizeof( aBuff_1 ) );
LED_On( LED_WR );
}
}

А в процедуре MSC_BulkIn() закоментируем весь код, оставив ее полностью пустой.

Результат работы всей связки вы видете на скриншоте.
При этом сама плата моргает двумя светодиодами.

Вот собственно и все. Мы написали прошивку и полноценный драйвер для собственного устройства USB. Если запустить передачу блоками по 4кб, можно добиться скорости 800 Кбайт/сек.
Как видите текст драйвера довольно прост и содержит всего около 250-ти строк.

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

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

Надеюсь статья получилась непохожей на руководство «как нарисовать сову», и кому-нибудь окажется полезной.

Подход к написанию драйвера USB устройства аналогичен драйверу PCI: драйвер регистрирует свой объект драйвера с USB подсистемой  и затем использует идентификаторы поставщика и устройства для сообщения, когда его оборудование было установлено.

Какие устройства поддерживает драйвер?

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

Структура struct usb_device_id определена со следующими полями:

__u16 match_flags

Определяет, какие из следующих полей в структуре устройства должны сопоставляться. Это битовое поле определяется разными значениями USB_DEVICE_ID_MATCH_*, указанными в файле include/linux/mod_devicetable.h. Это поле, как правило, никогда не устанавливается напрямую, а инициализируется с помощью макросов типа USB_DEVICE, описываемых ниже.

__u16 idVendor

Идентификатор поставщика USB для устройства. Этот номер присваивается форумом USB для своих членов и не может быть присвоен кем-то еще.

__u16 idProduct

Идентификатор продукта USB для устройства. Все поставщики, которые имеют выданный им идентификатор поставщика, могут управлять своими идентификаторами продукта, как они предпочитают.

__u16 bcdDevice_lo

__u16 bcdDevice_hi

Определяют нижнюю и верхнюю границу диапазона назначаемого поставщиком номера версии продукта. Значения bcdDevice_hi является включительным; его значение является значением наибольшего номера устройства. Обе эти величины представлены в двоично-десятичной (BCD) форме. Эти переменные в сочетании с idVendor и idProduct используются для определения данного варианта устройства.

__u8 bDeviceClass

__u8 bDeviceSubClass

__u8 bDeviceProtocol

Определяют класс, подкласс и протокол устройства, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB. Эти значения определяют поведение для всего устройства, в том числе все интерфейсы на этом устройстве.

__u8 bInterfaceClass

__u8 bInterfaceSubClass

__u8 bInterfaceProtocol

Подобно зависимым от устройства вышеприведённым величинам, эти определяют класса, подкласс и протокол отдельного интерфейса, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB.

kernel_ulong_t driver_info

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

Как и с PCI устройствами, существует ряд макросов, которые используются для инициализации этой структуры:

USB_DEVICE(vendor, product)

Создаёт struct usb_device_id, которая может быть использована только для соответствия указанными значениям идентификаторов поставщика и продукта. Она очень часто используется для устройств USB, которым необходим специальный драйвер.

USB_DEVICE_VER(vendor, product, lo, hi)

Создаёт struct usb_device_id, которая может быть использована только для соответствия указанным значениям идентификаторов поставщика и продукта внутри диапазона версий.

USB_DEVICE_INFO(class, subclass, protocol)

Создаёт struct usb_device_id, которая может быть использованы для соответствия определённому классу USB устройств.

USB_INTERFACE_INFO(class, subclass, protocol)

Создаёт struct usb_device_id, которая может быть использована для соответствия определённому классу USB интерфейсов.

Итак, для простого драйвера USB устройства, который управляет только одним USB устройством от одного поставщика, таблица struct usb_device_id будет определяться как:

/* таблица устройств, которые работают с этим драйвером */

static struct usb_device_id skel_table [ ] = {

    { USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },

    { }        /* Завершающая запись */

};

MODULE_DEVICE_TABLE (usb, skel_table);

Как и с драйвером PCI, необходим макрос MODULE_DEVICE_TABLE, чтобы разрешить инструментам пространства пользователя выяснить, какими устройствами может управлять этот драйвер. Но для USB драйверов первым значением в этом макросе должна быть строка usb.

Регистрация USB драйвера

Основной структурой, которую должны создать все USB драйверы, является struct usb_driver. Эта структура должна быть заполнена драйвером USB и состоит из ряда функций обратного вызова и переменных, описывающих USB драйвер для кода USB ядра:

struct module *owner

Указатель на модуль владельца этого драйвера. Ядро USB использует его для правильного подсчёта ссылок на этот драйвер USB, чтобы он не выгружался в несвоевременные моменты. Переменной должен быть присвоен макрос THIS_MODULE.

const char *name

Указатель на имя драйвера. Он должен быть уникальным среди всех USB драйверов в ядре и, как правило, установлен на такое же имя, что и имя модуля драйвера. Оно проявляется в sysfs в /sys/bus/usb/drivers/, когда драйвер находится в ядре.

const struct usb_device_id *id_table

Указатель на таблицу struct usb_device_id, которая содержит список всех различных видов устройств USB, которые драйвер может распознать. Если эта переменная не установлена, функция обратного вызова probe в драйвере USB никогда не вызывается. Если вы хотите, чтобы ваш драйвер всегда вызывался для каждого USB устройства в системе, создайте запись, которая устанавливает только поле driver_info:

static struct usb_device_id usb_ids[ ] = {

    {.driver_info = 42},

    { }

};

int (*probe) (struct usb_interface *intf, const struct usb_device_id *id)

Указатель на зондирующую функцию в USB драйвере. Эта функция (описанная в разделе «probe и disconnect в деталях») вызывается USB ядром, когда оно думает, что оно имеет структуру usb_interface, которую этот драйвер может обработать. Указатель на struct usb_device_id, который использовало USB ядро, чтобы принять это решение также передается в эту функцию. Если USB драйвер признаёт переданную ему структуру usb_interface, он должен правильно проинициализировать устройство и вернуть 0. Если драйвер не хочет признавать устройство или произошла ошибка, он должен вернуть отрицательное значение ошибки.

void (*disconnect) (struct usb_interface *intf)

Указатель на функцию отключения в USB драйвере. Эта функция (описанная в разделе «probe и disconnect в деталях») вызывается USB ядром, когда структура usb_interface была удалена из системы, или когда драйвер выгружается из ядра USB.

Таким образом, чтобы создать значимую структуру struct usb_driver, должны быть проинициализированы только пять полей:

static struct usb_driver skel_driver = {

    .owner = THIS_MODULE,

    .name = «skeleton»,

    .id_table = skel_table,

    .probe = skel_probe,

    .disconnect = skel_disconnect,

};

struct usb_driver содержит несколько больше обратных вызовов, которые, как правило, очень часто не используются, и не требуются для правильной работы USB драйвера:

int (*ioctl) (struct usb_interface *intf, unsigned int code, void *buf)

Указатель на функцию ioctl в USB драйвере. Если он присутствует, то вызывается, когда программа пользовательского пространства делает вызов ioctl для записи файловой системы устройств usbfs, связанной с устройством USB, относящемуся к этому USB драйверу. На практике только драйвер USB концентратора использует этот ioctl, так как любому другому USB драйверу нет иной реальной необходимости его использовать.

int (*suspend) (struct usb_interface *intf, u32 state)

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

int (*resume) (struct usb_interface *intf)

Указатель на функцию возобновления в USB драйвере. Она вызывается, когда работа устройства возобновляется USB ядром.

Чтобы зарегистрировать struct usb_driver в USB ядре, выполняется вызов usb_register_driver с указателем на struct usb_driver. Для USB драйвера это традиционно делается  в коде инициализации модуле:

static int __init usb_skel_init(void)

{

    int result;

    /* регистрируем этот драйвер в подсистеме USB */

    result = usb_register(&skel_driver);

    if (result)

        err(«usb_register failed. Error number %d», result);

    return result;

}

Когда драйвер USB будет выгружаться, необходимо разрегистрировать struct usb_driver в ядре. Это делается с помощью вызова usb_deregister. Когда происходит этот вызов, любые USB интерфейсы, которые в настоящее время связаны с этим драйвером, отключаются и для них вызывается функция disconnect.

static void __exit usb_skel_exit(void)

{

    /* отменяем регистрацию этого драйвера в подсистеме USB */

    usb_deregister(&skel_driver);

}

probe и disconnect в деталях

В структуре struct usb_driver structure, описанной в предыдущем разделе, драйвер указывает две функции, которые в соответствующее время вызывает ядро USB. Функция probe вызывается, когда установлено устройство, которым, как думает ядро USB, должен управлять этот драйвер; функция probe должна выполнять проверки информации, переданной ей об устройстве, и решать, действительно ли этот драйвер подходит для этого устройства. Функция disconnect вызывается, когда по каким-то причинам драйвер не должен больше управлять устройством и может делать очистку.

Оба функции обратного вызова probe и disconnect вызываются в контексте потока USB узла ядра, так что засыпать в них допускается. Тем не менее, рекомендуется, чтобы большая часть работы выполнялась, когда устройство открыто пользователем, если это возможно, чтобы сократить время зондирования USB к минимуму. Такое требование появляется потому, что USB ядро обрабатывает добавление и удаление устройств USB в одном потоке, так что любой медленный драйвер устройства может привести к замедлению обнаружения USB устройства и это станет заметно для пользователя.

В функции обратного вызова probe, USB драйвер должен проинициализировать любые локальные структуры, которые он может использовать для управления USB устройством. Следует также сохранить в локальные структуры любую необходимую информацию об устройстве, так как это обычно легче сделать в данное время. Например, USB драйверы обычно хотят обнаружить адрес оконечной точки и размеры буферов для данного устройства, так как они необходимы для общения с устройством. Вот пример некоторого кода, который определяет две оконечные точки ВХОДА и ВЫХОДА поточного типа и сохраняет некоторую информацию о них в локальной структуре устройства:

/* установить информацию оконечной точки */

/* используем только первые поточные точки входа и выхода */

iface_desc = interface->cur_altsetting;

for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {

    endpoint = &iface_desc->endpoint[i].desc;

    if (!dev->bulk_in_endpointAddr &&

        (endpoint->bEndpointAddress & USB_DIR_IN) &&

        ((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)

        == USB_ENDPOINT_XFER_BULK)) {

        /* мы нашли оконечную точку входного потока */

        buffer_size = endpoint->wMaxPacketSize;

        dev->bulk_in_size = buffer_size;

        dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;

        dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);

        if (!dev->bulk_in_buffer) {

            err(«Could not allocate bulk_in_buffer»);

            goto error;

        }

    }

    if (!dev->bulk_out_endpointAddr &&

        !(endpoint->bEndpointAddress & USB_DIR_IN) &&

        ((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)

        == USB_ENDPOINT_XFER_BULK)) {

        /* мы нашли оконечную точку выходного потока */

        dev->bulk_out_endpointAddr = endpoint->bEndpointAddress;

    }

}

if (!(dev->bulk_in_endpointAddr && dev->bulk_out_endpointAddr)) {

    err(«Could not find both bulk-in and bulk-out endpoints»);

    goto error;

}

Этот блок кода сначала в цикле обходит каждую оконечную точку, которая присутствует в этом интерфейсе, и назначает локальный указатель для структуры оконечной точки для облегчения доступа впоследствии:

for (i = 0; i < iface_desc->desc.bNumEndpoints; ++i) {

    endpoint = &iface_desc->endpoint[i].desc;

Затем, после того, как мы получили оконечную точку, и если мы уже не нашли ВХОДНУЮ оконечную точку поточного типа, мы проверяем, является ли направление этой оконечной точки ВХОДНЫМ. Это может быть проверено просмотром, содержится ли битовая маска USB_DIR_IN в переменной bEndpointAddress оконечной точки. Если это так, мы определяем, имеет ли оконечная точки тип поточной или нет, сначала накладывая битовую маску USB_ENDPOINT_XFERTYPE_MASK на переменную bmAttributes, а затем проверяя, совпадает  ли она со значением USB_ENDPOINT_XFER_BULK:

if (!dev->bulk_in_endpointAddr &&

    (endpoint->bEndpointAddress & USB_DIR_IN) &&

    ((endpoint->bmAttributes & USB_ENDPOINT_XFERTYPE_MASK)

    == USB_ENDPOINT_XFER_BULK)) {

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

/* мы нашли оконечную точку входного потока */

buffer_size = endpoint->wMaxPacketSize;

dev->bulk_in_size = buffer_size;

dev->bulk_in_endpointAddr = endpoint->bEndpointAddress;

dev->bulk_in_buffer = kmalloc(buffer_size, GFP_KERNEL);

if (!dev->bulk_in_buffer) {

    err(«Could not allocate bulk_in_buffer»);

    goto error;

}

Поскольку драйверу USB позднее в жизненном цикле устройства необходимо получать локальные структуры данных, связанные с этой struct usb_interface, может быть вызвана функция usb_set_intfdata:

/* сохраняем наш указатель на данные в этом интерфейсе устройства */

usb_set_intfdata(interface, dev);

Эта функция принимает указатель на любой тип данных и сохраняет его в структуре struct usb_interface для последующего доступа. Для получения данных должна быть вызвана функция usb_get_intfdata:

struct usb_skel *dev;

struct usb_interface *interface;

int subminor;

int retval = 0;

subminor = iminor(inode);

interface = usb_find_interface(&skel_driver, subminor);

if (!interface) {

    err («%s — error, can’t find device for minor %d»,

            __FUNCTION__, subminor);

    retval = -ENODEV;

    goto exit;

}

dev = usb_get_intfdata(interface);

if (!dev) {

    retval = -ENODEV;

    goto exit;

}

usb_get_intfdata обычно вызывается в функции open USB драйвера и снова в функции disconnect. Благодаря этим двум функциям USB драйверам не требуется держать статический массив указателей, которые хранят отдельные структуры устройства для всех текущих устройств в системе. Косвенная ссылка на информацию об устройстве позволяет любому USB драйверу поддерживать неограниченное количество устройств.

Если USB драйвер не связан с другим типом подсистемы, которая обрабатывает взаимодействие пользователя с устройством (такой, как ввод, терминал, видео и так далее), драйвер может использовать старший номер USB, чтобы использовать традиционный интерфейс символьного драйвера с пользовательским пространством. Чтобы сделать это, драйвер USB должен вызвать функцию usb_register_dev в функции probe, когда он хочет зарегистрировать устройство в USB ядре. Убедитесь, что устройство и драйвер находятся в надлежащем состоянии, чтобы выполнить желание пользователя получить доступ к устройству, как только вызвана эта функция.

/* мы можем зарегистрировать это устройство сейчас, так как оно готово */

retval = usb_register_dev(interface, &skel_class);

if (retval) {

    /* что-то помешало зарегистрировать этот драйвер */

    err(«Not able to get a minor for this device.»);

    usb_set_intfdata(interface, NULL);

    goto error;

}

Функция usb_register_dev требует указатель на struct usb_interface и указатель на struct usb_class_driver. struct usb_class_driver используется для определения ряда различных параметров, о которых драйвер USB желает, чтобы их знало USB ядро при регистрации на младший номер. Эта структура состоит из следующих переменных:

char *name

Имя, которое использует sysfs для описания устройства. Головное имя пути, если присутствует, используется только в devfs и в этой книге не рассматривается. Если ряду устройств необходимо быть в этом имени, в строке имени должны быть символы %d. Например, чтобы создать в devfs имя usb/foo1 и в sysfs имя класса foo1, строка имени должна быть установлена как usb/foo%d.

struct file_operations *fops;

Указатель на struct file_operations, которую этот драйвер определил, чтобы использовать для регистрации в качестве символьного устройства. Смотрите Главу 3 для получения дополнительной информации об этой структуре.

mode_t mode;

Режим для файла devfs, который будет создан для этого драйвера; иначе неиспользуемый. Типичный установкой для этой переменной будет значение S_IRUSR в сочетании со значением S_IWUSR, которыми владелец файла устройства предоставит доступ только для чтения и записи.

int minor_base;

Это начало установленного младшего диапазона для этого драйвера. Все устройства, связанные с этим драйвером, создаются с уникальными, увеличивающимися младшими номерам, начиная с этого значения. Если в ядре была включена опция конфигурации CONFIG_USB_DYNAMIC_MINORS, в любой момент допускается только 16 устройств, связанных с этим драйвером. Если это так, эта  переменная игнорируется и все младшие номера для этого устройства распределяются по принципу «первый пришёл, первым обслужен». Рекомендуется, чтобы системы, которые имеют эту опцию разрешённой, использовали такие программы, как udev для управления узлами устройств в системе, так как статическое дерево /dev не будет работать должным образом.

После отключения USB устройства, все ресурсы, связанные с устройством должны быть очищены, если это возможно. В это время, если в функции probe для выделения младшего номера для этого USB устройства была вызвана usb_register_dev, должна быть вызвана функция usb_deregister_dev, чтобы вернуть USB ядру младший номер обратно.

В функции disconnect также важно извлечь из этого интерфейса все данные, которые была ранее установлены вызовом usb_set_intfdata. Затем установить указатель на данные в структуре struct usb_interface в NULL, чтобы предотвратить дальнейшие ошибки при доступе к данным ненадлежащим образом:

static void skel_disconnect(struct usb_interface *interface)

{

    struct usb_skel *dev;

    int minor = interface->minor;

    /* предохраняем skel_open( ) от гонки со skel_disconnect( ) */

    lock_kernel( );

    dev = usb_get_intfdata(interface);

    usb_set_intfdata(interface, NULL);

    /* возвращаем наш младший номер */

    usb_deregister_dev(interface, &skel_class);

    unlock_kernel( );

    /* уменьшаем наш счётчик использования */

    kref_put(&dev->kref, skel_delete);

    info(«USB Skeleton #%d now disconnected», minor);

}

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

Перед вызовом для устройства USB функции disconnect все urb-ы, которые в настоящее время находятся в процессе передачи для устройства, будут отменены ядром USB, поэтому драйвер не должен явно вызывать usb_kill_urb для этих urb-ов. Если драйвер пытается отправить urb в USB устройство после того, как оно было отключено вызовом usb_submit_urb, отправка завершится неудачно с ошибочным значением -EPIPE.

Отправка и управление Urb

Когда драйвер имеет данные для передачи в USB устройство (как обычно бывает в функции записи драйвера), для передачи данных на устройство должен быть создан urb:

urb = usb_alloc_urb(0, GFP_KERNEL);

if (!urb) {

    retval = -ENOMEM;

    goto error;

}

После успешного создания urb-а, для отправки данных в устройство наиболее эффективным образом также должен быть создан буфер DMA и данные, которые переданы в драйвер, должны быть скопированы в этот буфер:

buf = usb_buffer_alloc(dev->udev, count, GFP_KERNEL, &urb->transfer_dma);

if (!buf) {

    retval = -ENOMEM;

    goto error;

}

if (copy_from_user(buf, user_buffer, count)) {

    retval = -EFAULT;

    goto error;

}

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

/* проинициализируем urb надлежащим образом */

usb_fill_bulk_urb(urb, dev->udev,

            usb_sndbulkpipe(dev->udev, dev->bulk_out_endpointAddr),

            buf, count, skel_write_bulk_callback, dev);

urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;

Теперь, когда должным образом выделен urb, должным образом скопированы данные и urb проинициализирован соответствующим образом, он может быть отправлен в ядро USB для передачи в устройство:

/* отправляем данные из поточного порта */

retval = usb_submit_urb(urb, GFP_KERNEL);

if (retval) {

    err(«%s — failed submitting write urb, error %d», __FUNCTION__, retval);

    goto error;

}

После того, как urb успешно передан в USB устройство (или что-то произошло при передаче), USB ядром выполняется обратный вызов urb. В нашем примере мы проинициализировали urb для указания на функцию skel_write_bulk_callback и это та самая функция, которая вызывается:

static void skel_write_bulk_callback(struct urb *urb, struct pt_regs *regs)

{

    /* сообщения об синхронные/асинхронные разъединениях не являются ошибками */

    if (urb->status &&

        !(urb->status == -ENOENT ||

        urb->status == -ECONNRESET ||

        urb->status == -ESHUTDOWN)) {

        dbg(«%s — nonzero write bulk status received: %d»,

                __FUNCTION__, urb->status);

    }

    /* освобождаем наш выделенный буфер */

    usb_buffer_free(urb->dev, urb->transfer_buffer_length, urb->transfer_buffer, urb->transfer_dma);

}

Первое вещью, которую делает функция обратного вызова, является проверка состояния urb-а для определения, завершён ли этот urb успешно или нет. Ошибочные значения, -ENOENT, -ECONNRESET и -ESHUTDOWN являются не реальными ошибками передачи, а просто  сообщают об условиях, сопровождающих успешную передачу. (Смотрите список возможных ошибок для urb-ов, подробно изложенный в разделе «struct urb».) Затем обратный вызов освобождает выделенный буфер, который был выделен для передачи этого urb-а.

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

Недавно на eBay мне попалась партия интересных USB-девайсов (Epiphan VGA2USB LR), которые принимают на вход VGA и отдают видео на USB как веб-камера. Меня настолько обрадовала идея, что больше никогда не придётся возиться с VGA-мониторами, и учитывая заявленную поддержку Linux, я рискнул и купил всю партию примерно за 20 фунтов (25 долларов США).

Получив посылку, я подключил устройство, но оно даже не подумало появиться в системе как UVC. Что не так?

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

К сожалению, поддержка драйверов именно для этих устройств закончилась в Linux 4.9. Таким образом, его не увидит ни одна из моих систем (Debian 10 на Linux 4.19 или последняя версия LTS Ubuntu на Linux 5.0).

Но ведь это можно исправить, верно? Конечно, файлы ведь идут в пакете DKMS, который по требованию собирает драйвер из исходного кода, как и многие обычные драйверы…

Печально. Но здесь не так.

Внутри пакета оказался только предварительно скомпилированный бинарник vga2usb.o. Я начал его изучать, прикидывая сложность реверс-инжиниринга, и нашёл несколько интересных строк:

$ strings vga2usb.ko | grep 'v2uco' | sort | uniq
v2ucom_autofirmware
v2ucom_autofirmware_ezusb
v2ucom_autofirmware_fpga

Так это на самом деле FPGA-on-a-stick? Как же заставить работать нечто подобное?

Ещё одной забавной и слегка тревожной находкой стали строки с параметрами закрытого ключа DSA. Это заставило меня задуматься: что же он может защищать внутри драйвера?

$ strings vga2usb.ko | grep 'epiphan' | sort | uniq
epiphan_dsa_G
epiphan_dsa_P
epiphan_dsa_Q

Чтобы изучить драйвер в его нормальной среде, я поднял виртуальную машину с Debian 9 (последний поддерживаемый релиз) и сделал KVM USB Passthrough, чтобы дать прямой доступ к устройству. Затем установил драйвер и убедился, что он работает.

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

Для этого я загрузил на хост виртуальной машины модуль usbmon и запустил Wireshark для захвата USB-трафика на устройство и с него во время запуска и захвата видео.

Я обнаружил, что при запуске на устройство передаётся большое количество мелких пакетов, прежде чем оно начинает захватывать картинку. Вероятно, оно действительно основано на платформе FPGA без хранилища данных. Каждый раз после подключения драйвер передавал на устройство прошивку в виде битстрима FPGA.

Я убедился в этом, открыв одну из коробок:

Поскольку для «загрузки» устройства нужно отправить ему битстрим/прошивку, придётся поискать его в предварительно скомпилированных бинарниках. Я запустил binwalk -x и начал искать какие-нибудь сжатые объекты (zlib). Для этого я написал скрипт поиска hex-последовательностей — и указал три байта из перехваченного пакета.

$ bash scan.sh "03 3f 55"
trying 0.elf
trying 30020
trying 30020.zlib
trying 30020.zlib.decompressed
...
trying 84BB0
trying 84BB0.zlib
trying 84BB0.zlib.decompressed
trying AA240
trying AA240.zlib
trying AA240.zlib.decompressed
000288d0  07 2f 03 3f 55 50 7d 7c  00 00 00 00 00 00 00 00  |./.?UP}|........|
trying C6860
trying C6860.zlib

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

Считывать USB-пакеты из файлов pcap может и tshark, и tcpdump, но обе сохраняют их лишь частично. Поскольку у каждой утилиты были разные части головоломки, я написал небольшую программу, которая объединяет выходные данные обеих программ в структуры go, чтобы воспроизвести пакеты обратно на устройство.

В этот момент я заметил, что загрузка происходит в два этапа: сначала USB-контроллер, а затем FPGA.

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

В итоге я решил проблему, тщательно изучив pcap с учётом времени ответа на каждый пакет — и заметил большую разницу во времени одного конкретного пакета:

Оказалось, что из-за небольшой опечатки запись происходила в неправильную область устройства. Будет мне уроком, как вводить значения вручную…

Тем не менее, на устройстве наконец-то замигал светодиод! Огромное достижение!

Было относительно просто реплицировать те же пакеты, которые запускали передачу данных, так что я смог написать конечную точку USB Bulk и мгновенно сбросить данные на диск!

Вот тут и начались настоящие сложности. Потому что после анализа оказалось, что данные не были явно закодированы каким-либо образом.

Для начала я запустил perf для общего представления о трассировке стека драйверов во время работы:

Хотя я мог выловить функции с данными фреймов, но понять кодировку самих данных никак не удавалось.

Чтобы лучше понять, что происходит внутри настоящего драйвера, я даже попробовал инструмент Ghidra от АНБ:

Хотя Ghidra невероятна (когда я впервые использовал её вместо IDA Pro), но всё ещё недостаточно хороша, чтобы помочь мне понять драйвер. Для реверс-инжиниринга требовался другой путь.

Я решил поднять виртуальную машину Windows 7 и взглянуть на драйвер Windows, вдруг он подбросит идеи. И тогда заметил, что для устройств имеется SDK. Один из инструментов оказался особенно интересным:

PS> ls

    Directory: epiphan_sdk-3.30.3.0007epiphanbin

Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---        10/26/2019  10:57 AM     528384 frmgrab.dll
-a---        10/27/2019   5:41 PM    1449548 out.aw
-a---        10/26/2019  10:57 AM     245760 v2u.exe
-a---        10/26/2019  10:57 AM      94208 v2u_avi.exe
-a---        10/26/2019  10:57 AM     102400 v2u_dec.exe
-a---        10/26/2019  10:57 AM     106496 v2u_dshow.exe
-a---        10/26/2019  10:57 AM     176128 v2u_ds_decoder.ax
-a---        10/26/2019  10:57 AM      90112 v2u_edid.exe
-a---        10/26/2019  10:57 AM      73728 v2u_kvm.exe
-a---        10/26/2019  10:57 AM      77824 v2u_libdec.dll

PS> .v2u_dec.exe
Usage:
      v2u_dec  [format] [compression level] 
               - sets compression level [1..5],
               - captures and saves compressed frames to a file
      v2u_dec x [format] 
               - decompresses frames from the file to separate BMP files

Этот инструмент позволяет «выхватывать» единичные фреймы, причём изначально они не сжимаются, чтобы была возможность обработать фреймы позже на более быстрой машине. Это практически идеально, и я реплицировал последовательность пакетов USB, чтобы получить эти несжатые блобы. Количество байтов соответствовало примерно трём (RGB) на пиксель!

Первоначальная обработка этих изображений (просто принимая вывод и записывая его как пиксели RGB) дала нечто отдалённо напоминающее реальную картинку, которое устройство получало через VGA:

После некоторой отладки в hex-редакторе выяснилось, что каждые 1028 байт повторяется какой-то маркер. Немного стыдно, как много времени я потратил на написание фильтра. С другой стороны, в процессе можно было насладиться некоторыми образцами современного искусства.

Затем я понял, что наклон и искажение изображения вызваны пропуском и переносом пикселя на каждой строке (x=799 не равно x=800). И тогда, наконец, у меня получилось почти правильное изображение, если не считать цвета:

Сначала я думал, что проблема с калибровкой из-за выборки данных, когда вход VGA застрял на сплошном цвете. Для исправления я сделал новое тестовое изображение, чтобы выявить такие проблемы. Задним числом понимаю, что надо было использовать что-то вроде тестовой карты Philips PM5544.

Я загрузил изображение на ноутбук, и тот выдал такую картинку VGA:

Тут мне пришло воспоминание о какой-то давней работе по 3D-рендерингу/шейдеру. Это было очень похоже на цветовую схему YUV.

В итоге я погрузился в чтение литературы по YUV и вспомнил, что во время реверс-инжиниринга официального драйвера ядра, если я ставил точку останова на функции под названием v2ucom_convertI420toBGR24, то система зависала без возможности возобновления. Так что, может, на входе была кодировка I420 (от -pix_fmt yuv420p), а выход RGB?

После применения встроенной в Go функции YCbCrToRGB изображение внезапно стало намного ближе к оригиналу.

Мы сделали это! Даже сырой драйвер выдавал 7 кадров в секунду. Честно говоря, мне этого достаточно, так как я использую VGA только в случае аварии как резервный дисплей.

Итак, теперь мы знаем это устройство достаточно хорошо, чтобы объяснить алгоритм его запуска с самого начала:

  1. Нужно инициализировать USB-контроллер. Судя по объёму информации, на самом деле драйвер передаёт на него код для загрузки.
  2. Когда вы закончите загрузку USB, устройство отключится от шины USB и через мгновение вернётся с одной конечной точкой USB.
  3. Теперь можно отправлять битстрим FPGA, по одному 64-байтовому пакету USB за каждую контрольную передачу.
  4. По окончании передачи индикатор на устройстве начнёт мигать зелёным цветом. На этом этапе можно отправить то, что кажется последовательностью параметров (overscan и другие свойства).
  5. Затем запускаем контрольный пакет для получения фрейма, в пакете указано разрешение. Если отправить запрос фрейма 4:3 на широкоэкранный вход, то это обычно приведёт к повреждению фрейма.

Для максимальной простоты использования я внедрил в драйвер небольшой веб-сервер. Через браузерные MediaRecorder API он легко записывает поток с экрана в видеофайл.

Предупреждая неизбежные претензии к качеству экспериментального кода, скажу сразу: я им не горжусь. Наверное, он в таком состоянии, какого мне достаточно для приемлемого использования.

Код и готовые сборки для Linux и OSX лежат на GitHub.

Даже если программу никто никогда не запустит, для меня это было чертовски увлекательное путешествие в дебрях протокола USB, отладки ядра, реверс-инжиниринга модуля и формата декодирования видео! Если вам нравятся такие вещи, можете посмотреть другие статьи в блоге.

title description ms.date

How to write your first USB client driver (KMDF)

In this article you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio to write a simple kernel-mode driver framework (KMDF)-based client driver.

02/22/2023

How to write your first USB client driver (KMDF)

In this article you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio to write a simple kernel-mode driver framework (KMDF)-based client driver. After building and installing the client driver, you’ll view the client driver in Device Manager and view the driver output in a debugger.

For an explanation about the source code generated by the template, see Understanding the KMDF template code for a USB client driver.

Prerequisites

For developing, debugging, and installing a kernel-mode driver, you need two computers:

  • A host computer running Windows 7 or a later version of the Windows operating system. The host computer is your development environment, where you write and debug your driver.
  • A target computer running Windows Vista or a later version of Windows. The target computer has the kernel-mode driver that you want to debug.

Before you begin, make sure that you meet the following requirements:

Software requirements

  • Your host computer hosts your development environment and has Visual Studio.
  • Your host computer has the latest Windows Driver Kit (WDK). The kit include headers, libraries, tools, documentation, and the debugging tools required to develop, build, and debug a KMDF driver. To get the latest version of the WDK, see Download the Windows Driver Kit (WDK).
  • Your host computer has the latest version of debugging tools for Windows. You can get the latest version from the WDK or you can Download and Install Debugging Tools for Windows.
  • Your target computer is running Windows Vista or a later version of Windows.
  • Your host and target computers are configured for kernel debugging. For more information, see Setting Up a Network Connection in Visual Studio.

Hardware requirements

Get a USB device for which you will be writing the client driver. In most cases, you are provided with a USB device and its hardware specification. The specification describes device capabilities and the supported vendor commands. Use the specification to determine the functionality of the USB driver and the related design decisions.

If you are new to USB driver development, use the OSR USB FX2 learning kit to study USB samples included with the WDK. You can get the learning kit from OSR Online. It contains the USB FX2 device and all the required hardware specifications to implement a client driver.

You can also get a Microsoft USB Test Tool (MUTT) devices. MUTT hardware can be purchased from JJG Technologies. The device does not have installed firmware installed. To install firmware, download the MUTT software package from this Web site and run MUTTUtil.exe. For more information, see the documentation included with the package.

Recommended reading

  • Concepts for All Driver Developers
  • Device nodes and device stacks
  • Getting started with Windows drivers
  • Kernel-Mode Driver Framework
  • Developing Drivers with Windows Driver Foundation, written by Penny Orwick and Guy Smith. For more information, see Developing Drivers with WDF.

Step 1: Generate the KMDF driver code by using the Visual Studio USB driver template

For instructions about generating KMDF driver code, see the steps in Writing a KMDF driver based on a template.

For USB-specific code, select the following options in Visual Studio:

  1. In the New Project dialog box, in the search box at the top, type USB.
  2. In the middle pane, select Kernel Mode Driver, USB (KMDF).
  3. Select Next.
  4. Enter a project name, choose a save location, and select Create.

The following screen shots show the New Project dialog box for the USB Kernel-Mode Driver template.

visual studio new project options.

visual studio new project options second screen.

This article assumes that the name of the Visual Studio project is «MyUSBDriver_». It contains the following files:

Files Description
Public.h Provides common declarations shared by the client driver and user applications that communicate with the USB device.
<Project name>.inf Contains information required to install the client driver on the target computer.
Trace.h Declares tracing functions and macros.
Driver.h; Driver.c Declares and defines driver entry points and event callback routines.
Device.h; Device.c Declares and defines event callback routine for the prepare-hardware event.
Queue.h; Queue.c Declares and defines an event callback routine for the event raised by the framework’s queue object.

Step 2: Modify the INF file to add information about your device

Before you build the driver, you must modify the template INF file with information about your device, specifically the hardware ID string.

In Solution Explorer, under Driver Files, double-click the INF file.

In the INF file you can provide information such as the manufacturer and provider name, the device setup class, and so on. One piece of information that you must provide is the hardware identifier of your device.

To provide the hardware ID string:

  1. Attach your USB device to your host computer and let Windows enumerate the device.

  2. Open Device Manager and open properties for your device.

  3. On the Details tab, select Hardward Ids under Property.

    The hardware ID for the device is displayed in the list box. Select and hold (or right-click) and copy the hardware ID string.

  4. Replace USBVID_vvvv&PID_pppp in the following line with your hardware ID string.

    [Standard.NT$ARCH$] %MyUSBDriver_.DeviceDesc%=MyUSBDriver__Device, USBVID_vvvv&PID_pppp

Step 3: Build the USB client driver code

To build your driver:

  1. Open the driver project or solution in Visual Studio
  2. Select and hold (or right-click) the solution in the Solution Explorer and select Configuration Manager.
  3. From the Configuration Manager, select the Active Solution Configuration (for example, Debug or Release) and the Active Solution Platform (for example, Win32) that correspond to the type of build you’re interested in.
  4. From the Build menu, select Build Solution.

For more information, see Building a Driver.

Step 4: Configure a computer for testing and debugging

To test and debug a driver, you run the debugger on the host computer and the driver on the target computer. So far, you have used Visual Studio on the host computer to build a driver. Next you need to configure a target computer. To configure a target computer, follow the instructions in Provision a computer for driver deployment and testing.

Step 5: Enable tracing for kernel debugging

The template code contains several trace messages (TraceEvents) that can help you track function calls. All functions in the source code contain trace messages that mark the entry and exit of a routine. For errors, the trace message contains the error code and a meaningful string. Because WPP tracing is enabled for your driver project, the PDB symbol file created during the build process contains trace message formatting instructions. If you configure the host and target computers for WPP tracing, your driver can send trace messages to a file or the debugger.

To configure your host computer for WPP tracing:

  1. Create trace message format (TMF) files by extracting trace message formatting instructions from the PDB symbol file.

    You can use Tracepdb.exe to create TMF files. The tool is located in the <install folder>Windows Kits10.0bin<architecture> folder of the WDK. The following command creates TMF files for the driver project.

    tracepdb -f <PDBFiles> -p <TMFDirectory>

    The -f option specifies the location and the name of the PDB symbol file. The -p option specifies the location for the TMF files that are created by Tracepdb. For more information, see Tracepdb Commands.

    At the specified location you’ll see three files (one per .c file in the project). They are given GUID file names.

  2. In the debugger, type the following commands:

    1. .load Wmitrace

      Loads the Wmitrace.dll extension.

    2. .chain

      Verify that the debugger extension is loaded.

    3. !wmitrace.searchpath +<TMF file location>

      Add the location of the TMF files to the debugger extension’s search path.

      The output resembles this:

      Trace Format search path is: 'C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDE;c:driverstmf'

To configure your target computer for WPP tracing:

  1. Make sure you have the Tracelog tool on your target computer. The tool is located in the <install_folder>Windows Kits8.0Tools<arch> folder of the WDK. For more information, see Tracelog Command Syntax.

  2. Open a Command Window and run as administrator.

  3. Type the following command:

    tracelog -start MyTrace -guid #c918ee71-68c7-4140-8f7d-c907abbcb05d -flag 0xFFFF -level 7-rt -kd

    The command starts a trace session named MyTrace.

    The guid argument specifies the GUID of the trace provider, which is the client driver. You can get the GUID from Trace.h in the Visual Studio Professional 2019 project. As another option, you can type the following command and specify the GUID in a .guid file. The file contains the GUID in hyphen format:

    tracelog -start MyTrace -guid c:driversProvider.guid -flag 0xFFFF -level 7-rt -kd

    You can stop the trace session by typing the following command:

    tracelog -stop MyTrace

Step 6: Deploy the driver on the target computer

  1. In the Solution Explorer window, select and hold (or right-click) the *<project name>*Package , and choose Properties.
  2. In the left pane, navigate to Configuration Properties > Driver Install > Deployment.
  3. Check Enable deployment, and check Import into driver store.
  4. For Remote Computer Name, specify the name of the target computer.
  5. Select Install and Verify.
  6. Select Ok.
  7. On the Debug menu, choose Start Debugging, or press F5 on the keyboard.

[!NOTE]
Do not specify the hardware ID of your device under Hardware ID Driver Update. The hardware ID must be specified only in your driver’s information (INF) file.

For more information about deploying the driver to the target system in Visual Studio, see Deploying a Driver to a Test Computer.

You can also manually install the driver on the target computer by using Device Manager. If you want to install the driver from a command prompt, these utilities are available:

  • PnPUtil

    This tool comes with the Windows. It is in WindowsSystem31. You can use this utility to add the driver to the driver store.

    C:>pnputil /a m:MyDriver_.inf
    Microsoft PnP Utility
    
    Processing inf : MyDriver_.inf
    Driver package added successfully.
    Published name : oem22.inf

    For more information, see PnPUtil Examples.

  • DevCon Update

    This tool comes with the WDK. You can use it to install and update drivers.

    devcon update c:windowsinfMyDriver_.inf USBVID_0547&PID_10025&34B08D76&0&6

Step 7: View the driver in Device Manager

  1. Enter the following command to open Device Manager:

  2. Verify that Device Manager shows a node for the following node:

    Samples

    MyUSBDriver_Device

Step 8: View the output in the debugger

Visual Studio first displays progress in the Output window. Then it opens the Debugger Immediate Window. Verify that trace messages appear in the debugger on the host computer. The output should look like this, where «MyUSBDriver_» is the name of the driver module:

[3]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverContextCleanup Entry
[1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Entry
[1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Exit
[0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Entry
[0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Exit

Related topics

  • Understanding the KMDF template code for a USB client driver
  • Getting started with USB client driver development
title description ms.date

How to write your first USB client driver (KMDF)

In this article you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio to write a simple kernel-mode driver framework (KMDF)-based client driver.

02/22/2023

How to write your first USB client driver (KMDF)

In this article you’ll use the USB Kernel-Mode Driver template provided with Microsoft Visual Studio to write a simple kernel-mode driver framework (KMDF)-based client driver. After building and installing the client driver, you’ll view the client driver in Device Manager and view the driver output in a debugger.

For an explanation about the source code generated by the template, see Understanding the KMDF template code for a USB client driver.

Prerequisites

For developing, debugging, and installing a kernel-mode driver, you need two computers:

  • A host computer running Windows 7 or a later version of the Windows operating system. The host computer is your development environment, where you write and debug your driver.
  • A target computer running Windows Vista or a later version of Windows. The target computer has the kernel-mode driver that you want to debug.

Before you begin, make sure that you meet the following requirements:

Software requirements

  • Your host computer hosts your development environment and has Visual Studio.
  • Your host computer has the latest Windows Driver Kit (WDK). The kit include headers, libraries, tools, documentation, and the debugging tools required to develop, build, and debug a KMDF driver. To get the latest version of the WDK, see Download the Windows Driver Kit (WDK).
  • Your host computer has the latest version of debugging tools for Windows. You can get the latest version from the WDK or you can Download and Install Debugging Tools for Windows.
  • Your target computer is running Windows Vista or a later version of Windows.
  • Your host and target computers are configured for kernel debugging. For more information, see Setting Up a Network Connection in Visual Studio.

Hardware requirements

Get a USB device for which you will be writing the client driver. In most cases, you are provided with a USB device and its hardware specification. The specification describes device capabilities and the supported vendor commands. Use the specification to determine the functionality of the USB driver and the related design decisions.

If you are new to USB driver development, use the OSR USB FX2 learning kit to study USB samples included with the WDK. You can get the learning kit from OSR Online. It contains the USB FX2 device and all the required hardware specifications to implement a client driver.

You can also get a Microsoft USB Test Tool (MUTT) devices. MUTT hardware can be purchased from JJG Technologies. The device does not have installed firmware installed. To install firmware, download the MUTT software package from this Web site and run MUTTUtil.exe. For more information, see the documentation included with the package.

Recommended reading

  • Concepts for All Driver Developers
  • Device nodes and device stacks
  • Getting started with Windows drivers
  • Kernel-Mode Driver Framework
  • Developing Drivers with Windows Driver Foundation, written by Penny Orwick and Guy Smith. For more information, see Developing Drivers with WDF.

Step 1: Generate the KMDF driver code by using the Visual Studio USB driver template

For instructions about generating KMDF driver code, see the steps in Writing a KMDF driver based on a template.

For USB-specific code, select the following options in Visual Studio:

  1. In the New Project dialog box, in the search box at the top, type USB.
  2. In the middle pane, select Kernel Mode Driver, USB (KMDF).
  3. Select Next.
  4. Enter a project name, choose a save location, and select Create.

The following screen shots show the New Project dialog box for the USB Kernel-Mode Driver template.

visual studio new project options.

visual studio new project options second screen.

This article assumes that the name of the Visual Studio project is «MyUSBDriver_». It contains the following files:

Files Description
Public.h Provides common declarations shared by the client driver and user applications that communicate with the USB device.
<Project name>.inf Contains information required to install the client driver on the target computer.
Trace.h Declares tracing functions and macros.
Driver.h; Driver.c Declares and defines driver entry points and event callback routines.
Device.h; Device.c Declares and defines event callback routine for the prepare-hardware event.
Queue.h; Queue.c Declares and defines an event callback routine for the event raised by the framework’s queue object.

Step 2: Modify the INF file to add information about your device

Before you build the driver, you must modify the template INF file with information about your device, specifically the hardware ID string.

In Solution Explorer, under Driver Files, double-click the INF file.

In the INF file you can provide information such as the manufacturer and provider name, the device setup class, and so on. One piece of information that you must provide is the hardware identifier of your device.

To provide the hardware ID string:

  1. Attach your USB device to your host computer and let Windows enumerate the device.

  2. Open Device Manager and open properties for your device.

  3. On the Details tab, select Hardward Ids under Property.

    The hardware ID for the device is displayed in the list box. Select and hold (or right-click) and copy the hardware ID string.

  4. Replace USBVID_vvvv&PID_pppp in the following line with your hardware ID string.

    [Standard.NT$ARCH$] %MyUSBDriver_.DeviceDesc%=MyUSBDriver__Device, USBVID_vvvv&PID_pppp

Step 3: Build the USB client driver code

To build your driver:

  1. Open the driver project or solution in Visual Studio
  2. Select and hold (or right-click) the solution in the Solution Explorer and select Configuration Manager.
  3. From the Configuration Manager, select the Active Solution Configuration (for example, Debug or Release) and the Active Solution Platform (for example, Win32) that correspond to the type of build you’re interested in.
  4. From the Build menu, select Build Solution.

For more information, see Building a Driver.

Step 4: Configure a computer for testing and debugging

To test and debug a driver, you run the debugger on the host computer and the driver on the target computer. So far, you have used Visual Studio on the host computer to build a driver. Next you need to configure a target computer. To configure a target computer, follow the instructions in Provision a computer for driver deployment and testing.

Step 5: Enable tracing for kernel debugging

The template code contains several trace messages (TraceEvents) that can help you track function calls. All functions in the source code contain trace messages that mark the entry and exit of a routine. For errors, the trace message contains the error code and a meaningful string. Because WPP tracing is enabled for your driver project, the PDB symbol file created during the build process contains trace message formatting instructions. If you configure the host and target computers for WPP tracing, your driver can send trace messages to a file or the debugger.

To configure your host computer for WPP tracing:

  1. Create trace message format (TMF) files by extracting trace message formatting instructions from the PDB symbol file.

    You can use Tracepdb.exe to create TMF files. The tool is located in the <install folder>Windows Kits10.0bin<architecture> folder of the WDK. The following command creates TMF files for the driver project.

    tracepdb -f <PDBFiles> -p <TMFDirectory>

    The -f option specifies the location and the name of the PDB symbol file. The -p option specifies the location for the TMF files that are created by Tracepdb. For more information, see Tracepdb Commands.

    At the specified location you’ll see three files (one per .c file in the project). They are given GUID file names.

  2. In the debugger, type the following commands:

    1. .load Wmitrace

      Loads the Wmitrace.dll extension.

    2. .chain

      Verify that the debugger extension is loaded.

    3. !wmitrace.searchpath +<TMF file location>

      Add the location of the TMF files to the debugger extension’s search path.

      The output resembles this:

      Trace Format search path is: 'C:Program Files (x86)Microsoft Visual Studio 14.0Common7IDE;c:driverstmf'

To configure your target computer for WPP tracing:

  1. Make sure you have the Tracelog tool on your target computer. The tool is located in the <install_folder>Windows Kits8.0Tools<arch> folder of the WDK. For more information, see Tracelog Command Syntax.

  2. Open a Command Window and run as administrator.

  3. Type the following command:

    tracelog -start MyTrace -guid #c918ee71-68c7-4140-8f7d-c907abbcb05d -flag 0xFFFF -level 7-rt -kd

    The command starts a trace session named MyTrace.

    The guid argument specifies the GUID of the trace provider, which is the client driver. You can get the GUID from Trace.h in the Visual Studio Professional 2019 project. As another option, you can type the following command and specify the GUID in a .guid file. The file contains the GUID in hyphen format:

    tracelog -start MyTrace -guid c:driversProvider.guid -flag 0xFFFF -level 7-rt -kd

    You can stop the trace session by typing the following command:

    tracelog -stop MyTrace

Step 6: Deploy the driver on the target computer

  1. In the Solution Explorer window, select and hold (or right-click) the *<project name>*Package , and choose Properties.
  2. In the left pane, navigate to Configuration Properties > Driver Install > Deployment.
  3. Check Enable deployment, and check Import into driver store.
  4. For Remote Computer Name, specify the name of the target computer.
  5. Select Install and Verify.
  6. Select Ok.
  7. On the Debug menu, choose Start Debugging, or press F5 on the keyboard.

[!NOTE]
Do not specify the hardware ID of your device under Hardware ID Driver Update. The hardware ID must be specified only in your driver’s information (INF) file.

For more information about deploying the driver to the target system in Visual Studio, see Deploying a Driver to a Test Computer.

You can also manually install the driver on the target computer by using Device Manager. If you want to install the driver from a command prompt, these utilities are available:

  • PnPUtil

    This tool comes with the Windows. It is in WindowsSystem31. You can use this utility to add the driver to the driver store.

    C:>pnputil /a m:MyDriver_.inf
    Microsoft PnP Utility
    
    Processing inf : MyDriver_.inf
    Driver package added successfully.
    Published name : oem22.inf

    For more information, see PnPUtil Examples.

  • DevCon Update

    This tool comes with the WDK. You can use it to install and update drivers.

    devcon update c:windowsinfMyDriver_.inf USBVID_0547&PID_10025&34B08D76&0&6

Step 7: View the driver in Device Manager

  1. Enter the following command to open Device Manager:

  2. Verify that Device Manager shows a node for the following node:

    Samples

    MyUSBDriver_Device

Step 8: View the output in the debugger

Visual Studio first displays progress in the Output window. Then it opens the Debugger Immediate Window. Verify that trace messages appear in the debugger on the host computer. The output should look like this, where «MyUSBDriver_» is the name of the driver module:

[3]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverContextCleanup Entry
[1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Entry
[1]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]MyUSBDriver_EvtDriverDeviceAdd Exit
[0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Entry
[0]0004.0054::00/00/0000-00:00:00.000 [MyUSBDriver_]DriverEntry Exit

Related topics

  • Understanding the KMDF template code for a USB client driver
  • Getting started with USB client driver development

Урок 3. Сборка и запуск драйвера

Еще один, особый, вид программ – драйверы. В отличие от остальных – выполняются в особом режиме, где они имеют ряд возможностей, недоступных для обычных приложений Windows. Однако это осложняется тем, что в этом режиме практически отсутствуют средства для взаимодействия с пользователем. Для тестирования драйвера можно пользоваться функцией DbgPrint, выдающей отладочные сообщения отладчику. Но отладчика может не оказаться и этих сообщений никто в таком случае не увидит. Для обычного же взаимодействия с пользователем драйвер должен полагаться на прикладные приложения, обмениваясь с ними запросами ввода/вывода.

Сообщения, выдаваемые отладчику, проще всего увидеть при помощи программы DebugView, которую нужно запустить перед сеансом проверки.

Создание проекта будем вести следующим образом. Создаем папку DrvHello, где будем разрабатывать приложение-драйвер. Внутри нее создаем такие файлы:

• makefile

#
# DO NOT EDIT THIS FILE!!!  Edit .sources. if you want to add a new source
# file to this component.  This file merely indirects to the real make file
# that is shared by all the driver components of the Windows NT DDK
#

!INCLUDE $(NTMAKEENV)makefile.def

Над текстом этого файла не следует задумываться, он просто должен присутствовать в таком вот неизменном виде. Хотя в чужих проектах этого файла может и не быть, либо он будет работать другим образом. В общем, если он есть и содержит это содержимое – то все в порядке, если нет – необходимы дополнительные исследования.

• sources

TARGETNAME=drvhello
TARGETTYPE=DRIVER
TARGETPATH=obj
SOURCES=main.c

Задает параметры сборки. Здесь в параметре SOURCES необходимо перечислить через пробел все файлы с исходным кодом, которые нужно будет скомпилировать.

• main.c – это наш файл с исходным кодом. Может носить любое имя, их может быть много, а может быть очень много. Главное, не забыть их все перечислить в файле sources

#include "ntddk.h"

#pragma code_seg("INIT")
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegistryPath)
{
    DbgPrint("Hello, world!");			// Выдача отладочного сообщения
    return STATUS_DEVICE_CONFIGURATION_ERROR;	// Выдача ошибки заставит систему сразу же выгрузить драйвер
}
#pragma code_seg()

Основная функция у нас здесь носит название DriverEntry. Запускаться она будет при попытке загрузки драйвера. А чтобы сделать такую попытку – нужна программа-загрузчик, которую напишем чуть поздней.

Откомпилируем драйвер. Чтобы мы могли что-то увидеть в отладчике, драйвер должен быть собран в отладочном режиме. Напомню, что отладочный режим у программистов драйверов называется checked, а финальный — free (прикладные программисты так не заморачиваются и называют их debug и release соответственно). В этом режиме программный код не оптимизируется и исполняемый файл содержит много избыточной информации, совершенно не нужной для работы, но очень полезной при изучении того, что будет делать этот код.
Для сборки открываем меню Start, затем Programs/Windows Driver Kits/WDK <номер версии>/Build Environment/Windows Server 2003 (или Windows XP)/ x86 Checked Build Environment. Щелкаем и попадаем в консольное окно. Если FAR был добавлен в PATH, то можно набрать far и перейти в директорию проекта обычным способом, иначе туда придется добираться используя команду cd (и может быть смену диска). Когда добрались до папки DrvHello (там где лежат три файла, которые были созданы выше) – набираем команду nmake. Если сборка прошла без ошибок, то будет созданы директории objchk_wnet_x86/i386, а в ней уже можно обнаружить файл drvhello.sys. Это и есть наш собранный драйвер.

Теперь вернемся к загрузчику. Это у нас будет консольное приложение (см. главу 1). Имя я ему дал DrvHelloloader и разместил, как всегда, в c:Projects. Имя файла с исходным кодом такое же – main.c, содержимое:

#include <windows.h>
#include <shlwapi.h>
#pragma comment(lib, "shlwapi.lib") 

void main(int argc, char* argv[])
{
	char serviceName[] = "drvhello";
	char driverBinary[MAX_PATH] = "";
	SC_HANDLE hSc;
	SC_HANDLE hService;

	// Чтобы запустить драйвер, потребуется полный путь к нему
	// Предполагаем, что он лежит в той же папке, что и экзешник
	strcpy(driverBinary, argv[0]);			// argv[0] - здесь будет имя экзешника с полным путем
	PathRemoveFileSpec(driverBinary);		// Выкидываем имя экзешника, остается только путь к папке
	strcat(driverBinary, "\drvhello.sys");	// Добавляем имя драйвера.
					// Бэкслэш в строках Си надо удваивать, из-за его специального значения.

	hSc = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);	// Открываем SCM (Service Control Management)
								// Это такая штука, которая позволяет запускать драйверы
								// из пользовательского режима
	CreateService(hSc, serviceName, serviceName,
		SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL,
		driverBinary, NULL, NULL, NULL, NULL, NULL);				// Загрузка в 3 этапа - создаем службу
	hService = OpenService(hSc, serviceName, SERVICE_ALL_ACCESS);	// Открываем ее
	StartService(hService, 0, NULL);	// И запускаем. Вообще-то было бы неплохо
						// еще и закрыть ее... Как нибудь потом.
}

Выбираем в меню cтудии – Build/Rebuild Solution. Произойдет пересборка проекта без запуска. В принципе, можно и запустить. Находим экзешник в папке проекта, докидываем в ту же папку drvhello.sys. Далее запускаем DebugView, включаем галочку Capture/Capture Kernel, как показано на рисунке:

Изображение
Рис. 3.1 – Настройка DebugView

Теперь запускаем программу. Если все прошло успешно – видим следующую картину:

Изображение
Рис. 3.2 – Проверка драйвера

Вот такой простой код позволяет уже очень много – работать на уровне ядра. Но следует помнить, что если допустить ошибку в прикладной программе – упадет она одна; если в драйвере – вся система. К примеру, если в код драйвера перед DbgPrint добавить строку:

int a = *((int *)0);

которая делает попытку прочесть содержимое памяти по нулевому адресу – то картинка будет уже иной:

Изображение
Рис. 3.3 – Попытка чтения из нулевого адреса

Если бы это был не драйвер, а просто программа пользовательского режима, то при выполнении этого кода она бы просто закрылась, выдав ошибку:

Изображение
Рис. 3.4 – Та же ошибка в программе пользовательского режима

24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

1

19.12.2016, 19:22. Показов 21218. Ответов 128


Всем здрасте…
Хотелось бы услышать мнения профессионалов. В частности Убежденного видел много сообщений от него..
Итак. Есть устройство usb, в данный момент используется виндовый usbser, но говорят работает плохо.
Мне поставили задачу(я на первом своём испытательном сроке) написать драйвер для данного устройства. Драйвер должен эмулировать COM порт(основной приоритет), а также возможно как сетевое устройство или сырой обмен, можно все вместе.
Знания драйверов у меня на уровне хелловорлд. Английский очень плохо. Читаю Уолтер Они, Комиссарова, Агуров, USB in Nutshell.
Вопрос первый — реально ли с моими знаниями хорошо сделать это за 2 месяца?
Вопрос второй — направьте меня на путь истинный)) дельные советы дайте

__________________
Помощь в написании контрольных, курсовых и дипломных работ, диссертаций здесь



0



Ушел с форума

Эксперт С++

16456 / 7420 / 1186

Регистрация: 02.05.2013

Сообщений: 11,617

Записей в блоге: 1

19.12.2016, 21:12

2

Цитата
Сообщение от VD
Посмотреть сообщение

Хотелось бы услышать мнения профессионалов. В частности Убежденного

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

Цитата
Сообщение от VD
Посмотреть сообщение

Вопрос первый — реально ли с моими знаниями хорошо сделать это за 2 месяца?

Нет.

А как ты попал в разработку драйверов USB, — такую головоломную и сложную тему, —
со «знанием драйверов у меня на уровне хелловорлд» и английским, который «очень плохо»?

Цитата
Сообщение от VD
Посмотреть сообщение

Вопрос второй — направьте меня на путь истинный)) дельные советы дайте

Пиши код, исследуй, думай, задавай вопросы.
2 месяца — нет, не реально. Но за полгодика, думаю, вполне можно что-то наваять…



1



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

22.12.2016, 11:44

 [ТС]

3

А может стоит начать с WDF. Может быстрее получится решить задачу?



0



Ушел с форума

Эксперт С++

16456 / 7420 / 1186

Регистрация: 02.05.2013

Сообщений: 11,617

Записей в блоге: 1

22.12.2016, 11:54

4

WDF хорош тем, что позволяет сконцентрироваться на задаче, не отвлекаясь на реализацию.
Но с WDF или без него — тебе все равно придется осваивать науку программирования в ядре:
устройство памяти, уровни привилегий, контексты, IRQL, синхронизация, стандартные
функции, планировщик, системные API и т.д. От этого никуда не денешься.



0



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

22.12.2016, 12:04

 [ТС]

5

Да я бы и не спрашивал а начал c всего этого.. читал здесь комменты по этому поводу ваши.. просто время.. боюсь не успеть и не примут. А может набросать план действий для конкретной задачи? Сделать из USB устройства COM порт, а то сказали стандартный usbser глючит? План в смысле изучить…список глав например из Уолтера Они или еще что то. Может есть где пример какой. Может потренироваться с флешкой есть какие примеры несложные чтоб USB понять. А то в голове каша пока… т.к. в википедии написано: «WDM требует от разработчиков драйверов полного знакомства со множеством сложных технических деталей перед написанием даже простейшего драйвера.»



0



Ушел с форума

Эксперт С++

16456 / 7420 / 1186

Регистрация: 02.05.2013

Сообщений: 11,617

Записей в блоге: 1

22.12.2016, 12:36

6

Цитата
Сообщение от VD
Посмотреть сообщение

А то в голове каша пока… т.к. в википедии написано: «WDM требует от разработчиков драйверов полного знакомства со множеством сложных технических деталей перед написанием даже простейшего драйвера.»

Так оно и есть. И добавить к этому нечего.

«Приняли на работу хирургом, как скальпель держать правильно?
Может, есть какие-то видеоуроки, чтобы побыстрее начать оперировать?
А то в Википедии написано: хирургия требует от хирурга высокого мастерства и
знания множества технических деталей перед проведением даже самой
простой операции»…



0



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

22.12.2016, 12:38

 [ТС]

7

Да я же не прошу у вас книжку как за 11 дней выучить С++. Я просто хочу выжать максимум из этого времени и не тратить время на вещи которые можно потом освоить



0



Ушел с форума

Эксперт С++

16456 / 7420 / 1186

Регистрация: 02.05.2013

Сообщений: 11,617

Записей в блоге: 1

22.12.2016, 12:44

8

Начни с чтения литературы. В первую очередь Уолтер Они и MSDN, главы, связанные с
общими вопросами программирования драйверов и USB в частности.

Скачай Windows Driver Kit и изучай исходники примеров для USB. Они сейчас есть онлайн.
Вопросов, которые будут возникать по ходу изучения, хватит для полной и глубокой
загрузки на много лет

А два месяца — нет, это совершенно нереально.



0



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

26.12.2016, 19:31

 [ТС]

9

Взял пример stupid из Они, сделал инициализации в DriverEntry, в AddDevice создал device object, зарегистрировал в ней интерфейс COM порта, и посадил созданный девайс в стек. Взял валявшийся у меня usb rutoken, взял inf файл от его дров, там тоже прописал интерфейс для ком порта. В итоге когда я подключаю рутокен вызывается AddDevice. Я правильно двигаюсь? Теперь я так понимаю нужно сделать обработку IRP от PnP, Power и WMI т.к. все WDM драйверы должны это уметь? И для моих целей обработку Read Write? Получается есть наверно какая то спецификация по преобразованию запросов IRP для дальнейшей передачи их на драйвер шины? А можно в общих словах объяснить логику обработки PnP, Power и WMI например если устройство питается от USB и нет?



0



Ушел с форума

Эксперт С++

16456 / 7420 / 1186

Регистрация: 02.05.2013

Сообщений: 11,617

Записей в блоге: 1

26.12.2016, 19:49

10

Цитата
Сообщение от VD
Посмотреть сообщение

Взял пример stupid из Они, сделал инициализации в DriverEntry, в AddDevice создал device object, зарегистрировал в ней интерфейс COM порта, и посадил созданный девайс в стек. Взял валявшийся у меня usb rutoken, взял inf файл от его дров, там тоже прописал интерфейс для ком порта.

Ну что ж, поздравляю, это весьма глубокое продвижение за такой короткий срок!

Цитата
Сообщение от VD
Посмотреть сообщение

Я правильно двигаюсь?

Судя по успехам — да.

Цитата
Сообщение от VD
Посмотреть сообщение

Теперь я так понимаю нужно сделать обработку IRP от PnP, Power и WMI т.к. все WDM драйверы должны это уметь?

Да.
PnP и Power — обязательно. WMI — опционально.

Цитата
Сообщение от VD
Посмотреть сообщение

Получается есть наверно какая то спецификация по преобразованию запросов IRP для дальнейшей передачи их на драйвер шины?

Цитата
Сообщение от VD
Посмотреть сообщение

А можно в общих словах объяснить логику обработки PnP, Power и WMI например если устройство питается от USB и нет?

Логика обработки PnP и Power во всех драйверах одинакова на 90-95%.
5% — это специфика твоего драйвера и/или устройства, которым он управляет.
Если ты программируешь какое-то устройство, то у тебя должна быть спецификация для него.
Иначе это мало чем будет отличаться от реверс инжиниринга…



1



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

27.12.2016, 21:24

 [ТС]

11

Вроде как появился COM3 в диспетчере устройств. IoRegisterDeviceInterface и IoSetDeviceInterfaceState вызываются. Но putty не подключается к нему, как будто его нет. Я что то не так понял или не дочитал?



0



Ушел с форума

Эксперт С++

16456 / 7420 / 1186

Регистрация: 02.05.2013

Сообщений: 11,617

Записей в блоге: 1

28.12.2016, 08:46

12

«Я написал программу. Но она не работает. Я что-то сделал не так?»

Давай уже конкретику.



0



VD

24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

28.12.2016, 11:17

 [ТС]

13

Нуу я не написал, я просто взял пример toaster/func из ddk 2600, и изменил там пару строчек

Добавлено через 1 час 35 минут

C++
1
2
3
4
5
6
7
8
// COM {4d36e978-e325-11ce-bfc1-08002be10318}
DEFINE_GUID(GUID_DEVINTERFACE_COM, 
        0x4d36e978L, 0xe325, 0x11ce, 0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18);
...
    status = IoRegisterDeviceInterface(PhysicalDeviceObject, &GUID_DEVINTERFACE_COM, NULL, &fdoData->InterfaceName);
    DbgPrint("%wZ", &fdoData->InterfaceName);
...
status = IoSetDeviceInterfaceState(&FdoData->InterfaceName, TRUE);

После этого я могу обращаться к com порту например по com3 или еще что то нужно? Вроде почитал msdn не особо въехал. Написано что то типа пользовательские приложения могут использовать &fdoData->InterfaceName
&fdoData->InterfaceName

Добавлено через 37 минут
О вот на \.USB#Vid_………{4d36e978-e325-11ce-bfc1-08002be10318} реагирует, когда я ввожу это в pytty. Выдает код для STATUS_NOT_SUPPORTED. А как сделать так чтоб он на имя COM… реагировал?



0



Ушел с форума

Эксперт С++

16456 / 7420 / 1186

Регистрация: 02.05.2013

Сообщений: 11,617

Записей в блоге: 1

28.12.2016, 11:21

14

Создай символьную ссылку COM -> \.USB#Vid…
А вообще, здесь моя сфера знаний заканчивается. Я реальные устройства никогда не
программировал, занимался только безопасностью — перехват процессов, фильтрация на
разных уровнях, недокументированные возможности системы, сигнатурный поиск и т.п.
Так что на меня в вопросах программирования железа лучше не полагаться



0



VD

24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

29.12.2016, 10:52

 [ТС]

15

Да я в курсе читал уже в каком то посте про «безопасностью — перехват процессов, фильтрация на
разных уро….». Ладно спасибо..лихо там в примере все закручено буду разбираться

Добавлено через 23 часа 22 минуты
Создай символьную ссылку COM -> \.USB#Vid…
Что то не получается создать символьную ссылку на символьную ссылку

C++
1
2
3
4
5
6
7
8
status = IoRegisterDeviceInterface(PhysicalDeviceObject, &GUID_DEVINTERFACE_COM, NULL, &fdoData->InterfaceName);
status = IoCreateSymbolicLink(&fdoData->portName, &fdoData->InterfaceName);
if (!NT_SUCCESS (status)) 
{
    DbgPrint("  XXX   Filed Create Symbolic Link");
    DbgPrint("  XXX   %wZ", &fdoData->InterfaceName);
    DbgPrint("  XXX   %wZ", &fdoData->portName);
}

XXX Filed Create Symbolic Link
XXX ??USB#Vid_0a89&Pid_0020#5&18f54cb7&0&2#{4d36e978-e325-11ce-bfc1-08002be10318}
XXX COM3



0



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

02.02.2017, 15:54

 [ТС]

16

Кто нибудь разбирается в запросах к USB CDC устройству
Например как отправить SET_CONTROL_LINE_STATE?
Накопал UsbBuildVendorRequest в MSDN с запросом GET_LINE_CODING получилось. А с SET_CONTROL_LINE_STATE не очень



0



1366 / 511 / 70

Регистрация: 21.07.2015

Сообщений: 1,290

07.02.2017, 12:32

17

VD, найди документацию на свой чип, там должно быть описание usb-запросов. Я думаю, что там одна или две дополнительные конечные точки, одна из них скорее всего имеет тип передачи по прерыванию. По второй данные записываются в порт, а возможно что и вообще через 0ю (контрольную) запись и управление идет. В любом случае для инициализации устройства нужно установить ему конфигурацию одноименным стандартным запросом. По умолчанию у USB устройств обычна установлена конфигурация ожидания, в которой по факту только могут отвечать на запросы (типа режим спячки такой). Также тебе потребуются запросы установки параметров обмена (скорости обмена и прочего). В крайнем случае залезь в исходники линуксового драйвера.
ЗЫ: я много работал с USB железом, но только не в винде (через свой самописный драйвер хост-контроллера). Поэтому могу просто подсказать по железной части, про программную модель USB видны не знаю и знать не хочу.



1



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

08.02.2017, 08:12

 [ТС]

18

Спасибо за ответ. Да конфигурация уже была установлена и с этим разобрался в принципе, простой обмен прошел устройство отвечает. Оно соответствует спецификации CDC, узнал все из дескрипторов там 4 конечные точки. 2 для данных bulk, нулевая и прерывающая. Ни как не пойму как задействована прерывающая. Снифером смотрел как стандартный драйвер от майкрософт общается с устройством, но запросов к прерывающей точке не видел.

И как моему драйверу узнавать что есть данные для считывания. Прерываний для USB нет, вроде как хост опрашивает. В винде я так понимаю этим занимаются драйверы хоста и прочее их куча под моим. Может кто то из них оповещает мой драйвер? Может кто то знает кроме товарища shmkv, который не хочет об этом знать



0



1366 / 511 / 70

Регистрация: 21.07.2015

Сообщений: 1,290

08.02.2017, 13:46

19

Цитата
Сообщение от VD
Посмотреть сообщение

Снифером смотрел как стандартный драйвер от майкрософт общается с устройством, но запросов к прерывающей точке не видел.

А данные в этот момент принимаются?

Цитата
Сообщение от VD
Посмотреть сообщение

И как моему драйверу узнавать что есть данные для считывания.

Мне думалось, что для этого и предназначена interrupt-in КТ. Тогда всех проще посмотреть снифером какие команды и куда шлет виндовый драйвер и потом отыскать их в документации. Я думаю, что там какой-то запрос наличия данных в буфере постоянно высылается, а после него уже команда считывания данных. А непосредственно сами данные вчитываются из bulk-in. В винде должен быть механизм асинхронного уведомления о завершении обработки USB запроса.



0



24 / 13 / 3

Регистрация: 02.08.2012

Сообщений: 160

08.02.2017, 14:18

 [ТС]

20

Да данные в этот момент принимаются и нет запросов к прерывающей точке. Хост вроде как опрашивает все устройства на шине. Ну по идее он должен бы моему сообщить драйверу что есть данные для приема и я уже сигнализировать об этом приложению чтоб оно выдало запрос на чтение. Короче ни как пока не въеду



0



Понравилась статья? Поделить с друзьями:

Не пропустите и эти статьи:

  • Как написать докладную на подчиненного
  • Как написать картину на холсте акрилом
  • Как написать докладную на начальника вышестоящему руководству
  • Как написать картину маслом сирень
  • Как написать докладную на мастера цеха образец

  • 0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest

    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии