Драйверы устройств в Linux
Часть 11: Драйверы USB в Linux
Оригинал: «Device Drivers, Part 11: USB Drivers in Linux»
Автор: Anil Kumar Pugalia
Дата публикации: October 1, 2011
Перевод: Н.Ромоданов
Дата перевода: июнь 2012 г.
Эта статья, которая является частью серии статей о драйверах устройств в Linux, поможет вам начать писать ваш первый драйвер USB в системе Linux.
Флеш устройство Пагса было именно тем устройством, которым Светлана воспользовалась, когда они сели вдвоем за изучение мира драйверов USB в Linux. Самым быстрым способом с ним разобраться был обычный способ Пагса — выбрать устройство USB и написать для него драйвер для того, чтобы с ним поэкспериментировать. Поэтому они выбрали флэш устройство (т.е. USB флешку), которое было под рукой — JetFlash от Transcend, с ID поставщика 0x058f и ID продукта 0x6387 .
Обнаружение устройства USB в Linux
Независимо от того, есть ли драйвер для устройств USB в Linux системе или его нет, допустимое устройство USB всегда будет обнаруживаться в системе Linux в пространстве аппаратных средств и в пространстве ядра, поскольку система создана (и выполняет обнаружение) в соответствии со спецификациями протокола USB. Обнаружение в аппаратном пространстве осуществляется хост контроллером USB — как правило, соответствующем шинным устройством, аналогичным устройству PCI в системах x86. Соответствующий драйвер хост-контроллера обнаруживает устройство и транслирует информацию низкоуровнего физического слоя в конкретную информацию более высокого уровня протокола USB. Затем информация протокола USB, касающаяся устройства и, имеющая специальный формат, заносится в общий слой ядра USB (драйвер usbcore) в пространстве ядра, что позволяет обнаруживать устройства USB в пространстве ядра даже в том случае, когда отсутствует драйвер конкретного устройства.
Дальше — дело различных драйверов, интерфейсов и приложений (которые различны в различных дистрибутивах Linux) отображать обнаруженные устройства в пользовательском пространстве. На рис.1 показана иерархия подсистемы USB в Linux.
Рис.1: Подсистема USB в Linux
Краткий список всех обнаруженных устройств USB можно получить с помощью команды lsusb , которую следует запустить в роли пользователя root. На рис.2 приведен такой список как для случая с флэш устройством, так и без него. Параметр -v в команде lsusb позволяет получить более подробную информацию.
Рис.2: Информация, выдаваемая командой lsusb
Во многих дистрибутивах Linux, таких как Mandriva, Fedora, . , драйвер usbfs сконфигурирован так, что он загружается по умолчанию. В результате можно с помощью команды cat /proc/bus/usb/devices из директория /proc извлечь конкретную информации об обнаруженном USB-устройстве, представленную в удобном виде. На рис.3 показан типичный пример такой информации, которая находится в специальной секции, описывающей флэш-устройство. В списке обычно присутствует по одному такому разделу для каждого допустимого устройства USB, обнаруженного в системе.
Рис.3: Фрагмент информации из proc, касающейся USB
Разбираемся в секции, описывающей устройство USB
Чтобы дальше разбираться с этими секциями, нужно в первую очередь понять, что такое допустимое устройство USB. Для всех допустимых устройств USB есть одна или несколько конфигураций. Конфигурация устройства USB похожа на профиль, причем в качестве конфигурации, используемой по умолчанию, обычно используется первая конфигурация. Таким образом, в Linux для каждого устройства по умолчанию поддерживается только одна конфигурация. Для каждой конфигурации в устройстве может быть один или несколько интерфейсов. Интерфейс соответствует функции, предоставляемой устройством.
Интерфейсов может быть столько, сколько есть функций, предоставляемых устройством. Так, скажем, устройство МФУ USB-принтер (многофункциональное устройство) может выполнять печать, сканирование и отправку факсов, и, скорее всего, для него будет, по крайней мере, три интерфейса, по одному для каждой из функций. Таким образом, в отличие от других драйверов устройств, драйвер USB устройства, как правило, связывается / пишется отдельно для каждого интерфейса, а не для устройства в целом — это значит, что для устройства USB может быть несколько драйверов устройств, причем для интерфейсов различных устройств может использоваться один и тот же драйвер, — хотя, конечно, для одного интерфейса не может быть более одного драйвера.
Вполне нормальной и достаточно обычной является ситуация, когда для всех интерфейсов устройства USB используется один и тот же драйвер USB. В записи Driver=. для директория proc (рис. 3) показано, что в драйвер отсутствует отображение интерфейса ( none ).
Для каждого интерфейса есть один или несколько источников / приемников данных. Источник / приемник данных (endpoint) похож на конвейер (pipe), используемый для передачи информации в зависимости от функции либо в интерфейс, либо из интерфейса устройства. В зависимости от типа информации, источники / приемники данных могут быть четырех типов: Control, Interrupt, Bulk и Isochronous.
Прим.пер.: Подробное описание указанных четырех типов источников / приемников данных будет приведено в следующей статье данной серии статей.
Согласно спецификациям протокола USB во всех допустимых устройствах USB должен быть неявно используемый источник / приемник данных с номером 0 (end-point zero) — единственный двунаправленный источник / приемник данных. На рис.4 приведена полная наглядная схема допустимого устройства USB, соответствующее приведенному выше объяснению.
Рис.4: Общий взгляд на устройство USB
Вернемся обратно к секциям устройств USB (рис. 3) — первая буква в каждой строке соответствует различным частям спецификации устройства USB. Например, D — устройству, C — конфигурации, I — интерфейсу, E — источнику / приемнику данных (endpoint) и т.д. Подробнее об этом и о многом другом смотрите в исходном коде ядра в файле Documentation/usb/proc_usb_info.txt .
Регистрация драйвера USB для флеш устройства
«Похоже, для того, чтобы можно было самостоятельно написать первый драйвер USB, потребуется узнать много всего о протоколе USB, — конфигурацию устройства, интерфейсы, конвейеры передачи данных, четыре типа передачи данных, а также многие другие обозначения, например, T, B, S, …, которые есть в спецификации устройств USB» — вздохнула Светлана.
«Да, но ты не беспокойся — со всем этим можно будет разобраться подробнее позже. Давай со всем этим разбираться последовательно — возьмем интерфейс флеш устройства, связанного с драйвером нашего USB-устройства ( pen_register.ko )» — утешил Пагс.
Как и в любом другом Linux-драйвере, здесь также требуется конструктор и деструктор — используется тот же самый шаблон драйвера, который использовался для всех драйверов. Но содержимое будет другим, поскольку это драйвер слоя аппаратного протокола, т.е. горизонтальный драйвер в отличие от символьного драйвера, который был одним из вертикальных драйверов, рассмотренных ранее. Разница лишь в том, что вместо регистрации и отмены регистрации в VFS, здесь это должно выполняться на уровне соответствующего протокола — в данном случае — в ядре USB; вместо того, чтобы предоставлять интерфейс пользовательского пространства, например, файл устройства, он должен подключиться к реальному устройству в пространстве аппаратных средств.
Интерфейсы API для ядра USB выглядят следующим образом (прототип в
):
В структуре usb_driver в соответствующих полях должны быть указаны имя устройства, идентификационная таблица, используемая для автообнаружения конкретного устройства, и две функции обратного вызова, которые вызываются ядром USB при горячем подключении и отключении устройства, соответственно.
Собираем все вместе в файл pen_register.c , который будет выглядеть следующим образом:
Затем можно повторить обычные шаги, выполняемые для любого Linux драйвера:
- Собираем драйвер (файл .ko ) с помощью запуска команды make .
- Загружаем драйвер с помощью команды insmod .
- Выдаем список загруженных модулей с помощью команды lsmod .
- Выгружаем драйвер с помощью команды rmmod .
Но, что удивительно, результат не будет таким, как ожидалось. Используйте команду dmesg и загляните в директорий proc для просмотра различных журналов и прочих подробностей. Это связано не с тем, что драйвер USB отличается от символьного драйвера, — здесь есть одна проблема. На рис.3 показано, что у флэш-устройства есть один интерфейс (с номером 0), который уже связан с обычным драйвером usb-storage.
Теперь, для того, чтобы связать наш драйвер с этим интерфейсом, нам нужно выгрузить драйвер usb-storage (т. е. выполнить команду rmmod usb-storage ) и переподключить флэш-накопитель. Как только это будет сделано, результаты станут такими, как ожидалось. На рис.5 показан фрагмент информации из журналов и из директория proc . Снова подключите и отключите (в горячем режиме) флеш устройство и пронаблюдайте, как действуют вызовы probe и disconnect.
Рис.5: Флеш устройство в действии
Подведем итог
«Наконец-то! Что-то действует!» — облегченно сказала Светлана. «Но мне кажется, что для того, чтобы собрать полный драйвер устройства USB, здесь есть еще много того, с чем следует разбираться (например, с идентификационной таблицей, обратными вызовами probe и disconnect и т. д.)».
«Да, ты права. Давай разбираться со всем по порядку и с перерывами » — ответил Пагс, прервав самого себя.
Источник
Пишем драйвер для самодельного 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;
Кроме объявления процедур чтения и записи, нужно не забыть их реализовать. На данном этапе я просто поставил заглушки, которые выводят переданные данные в 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. Тем более, что сейчас эту документацию стало довольно приятно читать и они изобилуют примерами.
Полный архив с исходниками можно скачать по ссылке.
В архиве находятся папки проименованные по пунктам, каждая содержит конечный результат, который мы достигли в соответствующем пункте.
Надеюсь статья получилась непохожей на руководство «как нарисовать сову», и кому-нибудь окажется полезной.
Источник