[Перевод] Пишем USB-драйверы для заброшенных устройств

Недавно на eBay мне попалась партия интересных USB-девайсов (Epiphan VGA2USB LR), которые принимают на вход VGA и отдают видео на USB как веб-камера. Меня настолько обрадовала идея, что больше никогда не придётся возиться с VGA-мониторами, и учитывая заявленную поддержку Linux, я рискнул и купил всю партию примерно за 20 фунтов (25 долларов США).
Получив посылку, я подключил устройство, но оно даже не подумало появиться в системе как
К сожалению, поддержка драйверов именно для этих устройств закончилась в Linux 4.9. Таким образом, его не увидит ни одна из моих систем (Debian 10 на Linux 4.19 или последняя версия LTS Ubuntu на Linux 5.0).
Но ведь это можно исправить, верно? Конечно, файлы ведь идут в пакете DKMS , который по требованию собирает драйвер из исходного кода, как и многие обычные драйверы…
Печально. Но здесь не так.
Внутри пакета оказался только предварительно скомпилированный бинарник vga2usb.o . Я начал его изучать, прикидывая сложность реверс-инжиниринга, и нашёл несколько интересных строк:
Чтобы изучить драйвер в его нормальной среде, я поднял виртуальную машину с Debian 9 (последний поддерживаемый релиз) и сделал
Я обнаружил, что при запуске на устройство передаётся большое количество мелких пакетов, прежде чем оно начинает захватывать картинку. Вероятно, оно действительно основано на платформе FPGA без хранилища данных. Каждый раз после подключения драйвер передавал на устройство прошивку в виде
ISL98002CRZ-170 — работает как ADC для сигналов VGA
XC6SLX16 — Xilinx Spartan 6 FPGA
CY7C68013A — USB-контроллер / фронтенд
Поскольку для «загрузки» устройства нужно отправить ему битстрим/прошивку, придётся поискать его в предварительно скомпилированных бинарниках. Я запустил binwalk -x и начал искать какие-нибудь сжатые объекты (zlib). Для этого я написал скрипт поиска hex-последовательностей — и указал три байта из перехваченного пакета.
После распаковки файла AA240.zlib оказалось, что там недостаточно данных для полного битстрима. Поэтому я решил захватить прошивку из пакетов USB.
Считывать USB-пакеты из файлов pcap может и
Оказалось, что из-за небольшой perf для общего представления о трассировке стека драйверов во время работы:

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

Чтобы лучше понять, что происходит внутри настоящего драйвера, я даже попробовал инструмент
Хотя Ghidra невероятна (когда я впервые использовал её вместо IDA Pro), но всё ещё недостаточно хороша, чтобы помочь мне понять драйвер. Для реверс-инжиниринга требовался другой путь.
Я решил поднять виртуальную машину Windows 7 и взглянуть на драйвер Windows, вдруг он подбросит идеи. И тогда заметил, что для устройств имеется SDK. Один из инструментов оказался особенно интересным:
Этот инструмент позволяет «выхватывать» единичные фреймы, причём изначально они не сжимаются, чтобы была возможность обработать фреймы позже на более быстрой машине. Это практически идеально, и я реплицировал последовательность пакетов USB, чтобы получить эти несжатые блобы. Количество байтов соответствовало примерно трём (RGB) на пиксель!
Первоначальная обработка этих изображений (просто принимая вывод и записывая его как пиксели RGB) дала нечто отдалённо напоминающее реальную картинку, которое устройство получало через VGA:

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

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

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

Тут мне пришло воспоминание о какой-то давней работе по 3D-рендерингу/шейдеру. Это было очень похоже на цветовую схему YUV .
В итоге я погрузился в чтение литературы по YUV и вспомнил, что во время реверс-инжиниринга официального драйвера ядра, если я ставил точку останова на функции под названием v2ucom_convertI420toBGR24 , то система зависала без возможности возобновления. Так что, может, на входе была кодировка I420 (от -pix_fmt yuv420p ), а выход RGB?
После применения встроенной в Go функции
Мы сделали это! Даже сырой драйвер выдавал 7 кадров в секунду. Честно говоря, мне этого достаточно, так как я использую VGA только в случае аварии как резервный дисплей.
Итак, теперь мы знаем это устройство достаточно хорошо, чтобы объяснить алгоритм его запуска с самого начала:
Предупреждая неизбежные претензии к качеству экспериментального кода, скажу сразу: я им не горжусь. Наверное, он в таком состоянии, какого мне достаточно для приемлемого использования.
Код и готовые сборки для Linux и OSX лежат на GitHub .
Даже если программу никто никогда не запустит, для меня это было чертовски увлекательное путешествие в дебрях протокола USB, отладки ядра, реверс-инжиниринга модуля и формата декодирования видео! Если вам нравятся такие вещи, можете посмотреть другие статьи в блоге .
Пишем драйвер для самодельного USB устройства
Целью этой статьи является пошаговая демонстрация процесса разработки всего набора программного обеспечения необходимого для организации связи самодельного устройства с компьютером посредством USB.
На данный момент, большинство радиолюбителей реализуют такой тип подключения используя чипы переходники USB в RS232 таким образом организуя связь со своим устройством посредством драйвера виртуального COM порта поставляемого с чипом переходником. Минусы такого подхода думаю понятны. Это как минимум лишний чип на плате и ограничения накладываемые этим чипом и его драйвером.
Мне же хочется осветить весь процесс организации такого взаимодействия так как оно и должно быть сделано, и как делается во всех серьезных устройствах.
В конце концов, сейчас 21-й век, модуль USB есть почти во всех микроконтроллерах. Именно о том, как наиболее быстро воспользоваться этим модулем и будет эта статья.
Так как для демонстрации процесса написания драйвера USB устройства нам необходимо собственно само устройство, то выберем одну из распространенных отладочных плат доступных в России. У меня это плата производства компании OLIMEX модель LPC-P2148. Основой платы является микроконтроллер LPC2148 архитектуры ARM7TDMI производства компании NXP. Всю информацию по плате можно получить на сайте производителя по следующей ссылке. Вот как она выглядит.

Выбор контроллера и отладочной платы абсолютно не принципиален т.к. процесс разработки взаимодействия между ОС на персональном компьютере и самой платой от этого не зависит. Среду разработки прошивки микроконтроллера будем использовать 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 модуль и всю необходимую процессору периферию.
Проект находится в папке ARM\Boards\Keil\MCB2140\RL\USB\. Пути здесь и далее я буду указывать относительно основной папки, куда установлена среда 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 по пути src\usb\osrusbfx2\. Самое интересное, что это не просто пример, а пошаговое обучение, как сделать драйвер к этой плате. Как раз то, что нам и нужно. Зайдем глубже в директорию kmdf\sys и видим, что там все шаги и лежат по папочкам. Подробнее о них можно почитать в описании примера, находящемся в файле osrusbfx2.htm.
Тут я сделаю небольшое отступление, чтобы немножко сделать более понятней следующие действия.
Дело в том, что с момента появления Windows NT кое что изменилось в процессе написания драйвера. В те времена нам приходилось напрямую использовать функции ядра ОС и часто, просто чтобы сделать пустышку способную правильно загружаться, выгружаться, отвечать на события PNP и т.п. базовые функции, приходилось много чего изучить и не один раз вылететь в BSOD. Потом Microsoft сделала модель, которую назвала Windows Driver Model и которая внесла некоторого рода стандарт что ли, как должен выглядеть драйвер. Особого облегчения, лично я от этого не почувствовал. А следующим шагом был сделан фреймворк, который называется Windows Driver Framework. И вот благодаря этому жить стало намного проще. Теперь фреймворк берет на себя реализацию всех базовых действий необходимых для обслуживания основных событий, а нам останется только правильным образом добавить нужных нам функций. Вот именно эту технологию мы и будем использовать.
Начинаем с первого шага. Запускаем «x86 Checked Build Environment» и при помощи команды “cd” перемещаемся в папку WinDDK\7600.16385.1\src\usb\osrusbfx2\kmdf\sys\step1\.
Выплняем команду 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)
Третье. Теперь необходимо создать интерфейс, через который драйвер станет доступный программам пользовательского режима. Раньше, программист должен был сам жестко прописывать имя, по которому мог быть открыт доступ к устройству через процедуру CreateFile. Теперь все стало проще. Нам нужно только создать интерфейс вызвав одну процедуру, а для его идентификации используется сгенерированный GUID. Далее в пользовательском режиме мы будем использовать этот же GUID, чтобы получить имя файла устройства. Итак, вот наш GUID и код связывающий его с интерфейсом:
DEFINE_GUID(GUID_DEVINTERFACE_OSRUSBFX2, // Generated using guidgen.exe
0x573e8c73, 0xcb4, 0x4471, 0xa1, 0xbf, 0xfa, 0xb2, 0x6c, 0x31, 0xd3, 0x84);
//
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 функции и создать ее, прицепив к объекту устройства. Поехали:
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 драйвера
Подход к написанию драйвера USB устройства аналогичен драйверу PCI: драйвер регистрирует свой объект драйвера с USB подсистемой и затем использует идентификаторы поставщика и устройства для сообщения, когда его оборудование было установлено.
Какие устройства поддерживает драйвер?
Структура struct usb_device_id содержит список различных типов USB устройств, которые поддерживает этот драйвер. Этот список используется ядром USB, чтобы решить, какой драйвер предоставить устройству, или скриптами горячего подключения, чтобы решить, какой драйвер автоматически загрузить, когда устройство подключается к системе.
Структура struct usb_device_id определена со следующими полями:
Определяет, какие из следующих полей в структуре устройства должны сопоставляться. Это битовое поле определяется разными значениями USB_DEVICE_ID_MATCH_* , указанными в файле include/linux/mod_devicetable.h . Это поле, как правило, никогда не устанавливается напрямую, а инициализируется с помощью макросов типа USB_DEVICE , описываемых ниже.
Идентификатор поставщика USB для устройства. Этот номер присваивается форумом USB для своих членов и не может быть присвоен кем-то еще.
Идентификатор продукта USB для устройства. Все поставщики, которые имеют выданный им идентификатор поставщика, могут управлять своими идентификаторами продукта, как они предпочитают.
Определяют нижнюю и верхнюю границу диапазона назначаемого поставщиком номера версии продукта. Значения bcdDevice_hi является включительным; его значение является значением наибольшего номера устройства. Обе эти величины представлены в двоично-десятичной (BCD) форме. Эти переменные в сочетании с idVendor и idProduct используются для определения данного варианта устройства.
Определяют класс, подкласс и протокол устройства, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB. Эти значения определяют поведение для всего устройства, в том числе все интерфейсы на этом устройстве.
Подобно зависимым от устройства вышеприведённым величинам, эти определяют класса, подкласс и протокол отдельного интерфейса, соответственно. Эти номера присваиваются форумом USB и определены в спецификации USB.
Это значение не используется для сравнения, но оно содержит информацию о том, что драйвер может использовать, чтобы отличать разные устройства друг от друга в функции обратного вызова probe драйвера USB.
Как и с PCI устройствами, существует ряд макросов, которые используются для инициализации этой структуры:
Создаёт 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 [ ] = <
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[ ] = <
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 = <
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)
/* регистрируем этот драйвер в подсистеме USB */
err("usb_register failed. Error number %d", result);
Когда драйвер USB будет выгружаться, необходимо разрегистрировать struct usb_driver в ядре. Это делается с помощью вызова usb_deregister . Когда происходит этот вызов, любые USB интерфейсы, которые в настоящее время связаны с этим драйвером, отключаются и для них вызывается функция disconnect .
static void __exit usb_skel_exit(void)
/* отменяем регистрацию этого драйвера в подсистеме USB */
probe и disconnect в деталях
В структуре struct usb_driver structure , описанной в предыдущем разделе, драйвер указывает две функции, которые в соответствующее время вызывает ядро USB. Функция probe вызывается, когда установлено устройство, которым, как думает ядро USB, должен управлять этот драйвер; функция probe должна выполнять проверки информации, переданной ей об устройстве, и решать, действительно ли этот драйвер подходит для этого устройства. Функция disconnect вызывается, когда по каким-то причинам драйвер не должен больше управлять устройством и может делать очистку.
Оба функции обратного вызова probe и disconnect вызываются в контексте потока USB узла ядра, так что засыпать в них допускается. Тем не менее, рекомендуется, чтобы большая часть работы выполнялась, когда устройство открыто пользователем, если это возможно, чтобы сократить время зондирования USB к минимуму. Такое требование появляется потому, что USB ядро обрабатывает добавление и удаление устройств USB в одном потоке, так что любой медленный драйвер устройства может привести к замедлению обнаружения USB устройства и это станет заметно для пользователя.
В функции обратного вызова probe , USB драйвер должен проинициализировать любые локальные структуры, которые он может использовать для управления USB устройством. Следует также сохранить в локальные структуры любую необходимую информацию об устройстве, так как это обычно легче сделать в данное время. Например, USB драйверы обычно хотят обнаружить адрес оконечной точки и размеры буферов для данного устройства, так как они необходимы для общения с устройством. Вот пример некоторого кода, который определяет две оконечные точки ВХОДА и ВЫХОДА поточного типа и сохраняет некоторую информацию о них в локальной структуре устройства:
Статья Создание драйвера под Windows. Часть 1: Введение
Для очередного проекта возникла необходимость написать простенький софтверный драйвер под Windows, но так как опыта в написании драйверов у меня примерно столько же, сколько и в балете, я начал исследовать данную тему. В таких делах я предпочитаю начинать с основ, ибо если кидаться сразу на сложные вещи, то можно упустить многие базовые понятия и приёмы, что в дальнейшем только усложнит жизнь.
После 20 минут поисков по сети я наткнулся на Github Павла Иосифовича (zodiacon — Overview). Личность легендарная в своих кругах, достаточно посмотреть на его репозиторий, публикации и выступления на именитых конференциях. Помимо этого, Павел является автором/соавтором нескольких книг: «Windows Internals» (книга, имеющаяся у меня на полке, которая принесла немало пользы), и «Windows Kernel Programming» 2019 года выпуска (бегло пролистав 11 Глав или 390 страниц, я понял – это то, что нужно!).
Кстати, книгу вы можете купить прямо на сайте Павла
Книгу я приобрёл в бумажной версии, чтобы хоть и немного, но поддержать автора. Безупречное качество, несмотря на то, что она издается в мягком переплете. Хорошие плотные листы формата А4 и качественная краска. (книга без проблем пережила вылитую на нее кружку горячего кофе).
Пока я сидел на балконе и читал четвёртую главу книги, в голову пришла мысль: а почему бы не сделать ряд статей на тему «Программирования драйвера под Windows», так сказать, совместить полезное, с еще более полезным.
И вот я здесь, пишу предысторию.
Как я вижу этот цикл статей и что от него ожидать:
Это будут статьи, которые будут базироваться на вышеупомянутой книге, своеобразный вольный и сокращенный перевод, с дополнениями и примечаниями.
Базовые понятия о внутреннем устройстве Windows (Windows Internals)
Для того, чтобы начать разрабатывать Драйвер под Windows, то есть работать на уровне с ядром ОС, необходимо базовое понимание того, как эта ОС утроена. Так как я хочу сосредоточиться на написании драйвера, а не на теории об операционных системах, подробно описывать понятия я не буду, чтобы не растягивать статью, вместо этого прикреплю ссылки для самостоятельного изучения.