Пример работы с USB HID из Windows DDK Печать
Добавил(а) microsin   

Эта статья содержит дословный перевод комментария к программе HClient - пример (hclient.htm) работы с устройствами USB HID, который можно найти в Windows DDK (см. папку c:\WINDDK2003\3790.1830\src\wdm\hid\hclient). HClient представляет из себя, кроме примера, полезную программу, дающую информацию обо всех подключенных в системе устройствах USB HID.

Этот документ и связанный с ним код примера показывает, как написать приложение-клиент пользовательского режима (user-mode client application), предназначенное для обмена данными с устройствами, которые отвечают спецификации HID device class. Эта статья полезна для программистов, кому нужно разработать приложение пользовательского режима, которое работает с HID-совместимыми устройствами, получает и декодирует информацию от них. Этот пример иллюстрирует метод детектирования устройств HID, открытие этих устройств HID для обмена, и распаковку данных из репортов или форматирование данных в репорты устройства HID.

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

Основной механизм обмена данными для устройств HID class - HID report (далее эти слова я иногда не буду переводить). Каждое устройство HID должно поддерживать report descriptor (дескриптор репорта, это тоже я не буду переводить, т. к. по английски звучит куда лучше) который описывает формат различных репортов, которые он создает для этих устройств. Драйверы HID class и HID.DLL предоставляют интерфейс для распаковки подходящих данных из этих репортов.

Несмотря на то, что пример HClient является приложением пользовательского режима, многие функции HID.DLL также доступны для HID клиентов режима ядра (kernel-mode HID clients). Функции, экспортируемые из HID.DLL, имеют префикс либо HidD_, либо HidP_. Все функции с префиксом HidP_ доступны для клиентов режима ядра. Однако механизм для открытия устройств HID и получения нужной информации в этом контексте отличается - как, например, для получения интерпретируемых данных.

СБОРКА ПРИМЕРА
Для того, чтобы скомпилировать HClient.exe, следуйте инструкциям далее.

1. Запустите «standard Windows NT®/Windows® 2000 DDK build environment (checked or free)» - это просто командная строка, у которой в Path прописаны пути до утилит компиляции DDK (это все ставится вместе с DDK).
2. Поменяйте текущую папку на .\src\wdm\hid\hclient
3. Выполните команду build
4. Сборка HClient.exe will появится в папке .\lib\\
Исходные файлы HClient зависимы от следующих системных включаемых файлов и библиотек.

HIDSDI.H  Определения и декларации только для пользовательского режима
HIDPI.H  Определения и декларации для HID-клиентов пользовательского режима и HID-клиентов kernel-mode
HIDUSAGE.H  макроопределения для предварительно заданной таблицы usage и величин usage, как задано в спецификации HID Spec 1.0 и HID Usage Table Spec 1.0
HID.LIB  Библиотечный файл, необходимый для разрешения экспортируемых функций HID.DLL

Клиенты kernel-mode могут также нуждаться в следующих файлах:
HIDPDDI.H  Определения и декларации для особенностей, доступных через интерфейс HID ioctl
HIDPARSE.LIB  Библиотечный файл, необходимый для разрешения экспортируемых функций HIDPARSE.SYS

ИНСТРУМЕНТЫ (TOOLS)
Инструменты нужны только для работы HClient с устройствами HID. В Windows NT/Windows 2000 дескрипторы файлов (file handles) не могут быть открыты для мышей и клавиатур. Однако все другие устройства HID devices будут доступны и распознаны HClient для тестирования.

РЕСУРСЫ
См. Universal Serial Bus Device Class Definition для Human Interface Devices (HID) Version 1.0 и Universal Serial Bus HID Usage Tables 1.0.

ОБЗОР КОДА (CODE TOUR)
Список файлов (файл, описание)

HCLIENT.HTM Документация по примеру (этот текст)
SOURCES  Обычный файл для сборки кода примера
BUFFERS.C Код для отображения буферов HID report в диалоговом окне «extended calls»  
BUFFERS.H Декларации функций и структур, видимых для других модулей
DEBUG.C  Содержит определения функции для отладки выделения памяти
DEBUG.H Содержит публичные макроопределения и декларации функций отладки подпрограмм для работы с assert-ами, traps-ами и выделениями памяти
ECDISP.C     Код для поддержки диалогового окна «extended calls»
ECDISP.H     Содержит публичные декларации диалогового окна «extended calls»
HCLIENT.C Код для поддержки для главного диалогового окна HClient
HCLIENT.H Содержит публичные декларации и определения для HCLIENT.C, видимые для других модулей
HCLIENT.RC Сгенерированный с помощью Visual C++ файл ресурсов для HClient
HID.H   Содержит декларации и определения для поддержки устройств и данных внутри HClient
LIST.H   Содержит публичные макроопределения для манипулирования двойными связанными списками (doubly-linked lists)
LOGPNP.C Код для нахождения, загрузки и построения логических структур устройства HID
LOGPNP.H     Содержит декларации публичных функций LOGPNP.C
MAKEFILE NT DDK build environment makefile
PNP.C   Содержит код для нахождения, добавления, удаления и идентификации устройств HID
REPORT.C    Содержит код для чтения/записи репортов HID и транслирования этих репортов HID в удобную информацию
RESOURCE.H Сгенерированный с помощью Visual C++ файл определения ресурсов
STRINGS.C Код для конвертации буферов данных и переменных целого типа в и из строк для отображения.
STRINGS.H Содержит декларации публичных функций STRINGS.C

Обзор программирования
Основная функциональность приложений HID клиента содержится в файлах REPORT.C и PNP.C. Код в этих файлах реализует базовые функции, которые могут понадобиться большинству клиентов. Остальные исходные файлы содержат код для поддержки интерфейса пользователя и вызова основных действий, которые описаны в этих двух вышеуказанных файлах.

Основные темы, раскрываемые в этой секции:
• Детектирование установленных устройств HID
• Открытие устройств HID
• Обмен данными с устройствами HID
• Построение/интерпретирование репортов HID

Детектирование установленных устройств HID
Компонент первой необходимости приложения HID клиента – детектирование установленных устройств HID. Функция FindKnownHidDevices() в PNP.C показывает, как это делать. Основные шаги для идентификации подсоединенных устройств HID:
HidD_GetHidGuid() – получение HID device class GUID
SetupDiGetClassDevs() – получить дескриптор (handle) набора устройств, которые реализуют HID интерфейс
SetupDiEnumDeviceInterfaces() – для каждого устройства в возвращенном наборе устройств получить информацию для всех представленных интерфейсов HID.
SetupDiGetDeviceInterfaceDetail() – для каждого интерфейса, полученного на предыдущем вызове, получить детализированный блок информации для этого интерфейса. Эта детальная информация включает строки, которые можно передавать в функцию CreateFile() для открытия дескриптора (handle) для устройства
SetupDiDestroyDeviceInfoList() – уничтожить набор информации об устройстве, которая была получена в вызове SetupDiGetClassDevs().

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

Открытие устройств HID
После детектирования устройства HID, HClient выполняет открытие устройства. Когда устройство открывается, HClient создает структуру HID_DEVICE для хранения любой информации об устройстве, которая может в дальнейшем понадобиться подпрограммам. HID_DEVICE задана в файле HID.H следующим образом:

typedef struct _HID_DEVICE
{   
    HANDLE               HidDevice; // дескриптор (handle) устройства HID
    PHIDP_PREPARSED_DATA Ppd;       // тупой парсер информации, описывающей это устройство
    HIDP_CAPS            Caps;      // возможности (Capabilities) этого HID устройства
    HIDD_ATTRIBUTES      Attributes;
  
    PCHAR                InputReportBuffer;
    PHID_DATA            InputData;       // массив структур hid data
    ULONG                InputDataLength; // количество элементов в массиве
    PHIDP_BUTTON_CAPS    InputButtonCaps;
    PHIDP_VALUE_CAPS     InputValueCaps;
  
    PCHAR                OutputReportBuffer;
    PHID_DATA            OutputData;
    ULONG                OutputDataLength;
    PHIDP_BUTTON_CAPS    OutputButtonCaps;
    PHIDP_VALUE_CAPS     OutputValueCaps;
  
    PCHAR                FeatureReportBuffer;
    PHID_DATA            FeatureData;
    ULONG                FeatureDataLength;
    PHIDP_BUTTON_CAPS    FeatureButtonCaps;
    PHIDP_VALUE_CAPS     FeatureValueCaps;
} HID_DEVICE, *PHID_DEVICE;

Помимо хранения основных HID структур, как это определено в HIDPI.H и HIDSDI.H, в структуре HID_DEVICE содержится также целый ряд HID_DATA структур для каждого типа репорта. Эти структуры содержат самые последние значения, используемые для каждого из органов управления (controls), определенных в списках HIDP_VALUE_CAPS и HIDP_BUTTON_CAPS для данного типа отчета. Поля в этой структуре используются подпрограммами, которые пакуют/распаковывают репорты данных (data reports). Как использовать эти поля рассмотрено в секции Building/Interpreting HID Reports (Построение/интерпретирование репортов HID).

Функция OpenHidDevice() в PNP.C выполняет необходимые шаги по заполнению структуры HID_DEVICE для устройства:
CreateFile() – открывает дескриптор (handle) указанного отдельного устройства HID. В этом случае мы запрашиваем доступ к устройству на чтение/запись, позволяем устройству быть общим (shared), и синхронизируем доступ к дескриптору устройства (нет флага OVERLAPPED).
HidD_GetPreparsedData() – Получение preparsed data для устройства. Термин preparsed data означает HID парсер – особенный блок данных, используемый для обработки репортов HID. Память для этой структуры выделяется функцией HID.DLL, и она должна быть освобождена, когда больше не нужна, с помощью HidD_FreePreparsedData().
HidD_GetAttributes() – получение атрибутов устройства. Структура атрибутов содержит идентификатор вендора и продукта (vendor ID и product ID), и номер версии указанного устройства HID
HidD_GetCaps() – получение возможностей (capabilities) устройства. Эти возможности включают страницу использования (usage page) и использование (usage) устройства, требуемую длину буфера для различных репортов устройства, число узлов соединения (link collection nodes) на устройстве и количество кнопок и величины для каждого отдельного типа репорта.
FillDeviceInfo() – заполнение остальной части структуры HID_DEVICE. Эта функция выделяет место для этого и получает узлы соединения (link collection nodes), величины возможностей (caps), и информацию о кнопках для устройства.

Разрабатываемое приложение HID-клиента может и не нуждаться во всех перечисленных выполняемых шагах. Например, если приложение-клиент работает только с конкретным HID device (определяемым по vendor ID/product ID), оно может отказаться от обработки других устройств, кроме того, которое интересно.

Приложение клиента может также работать только для требуемой комбинации usage page/usage. Например, приложение управления монитором может нуждаться только в открытии устройств, которые подходят к комбинации usage page и usage монитора.

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

Обмен данными с устройствами HID
Как уже упоминалось выше, основной метод общения с HID устройством происходит через репорты. Устройство HID device может содержать до 255 репортов каждого типа (Input, Output, и Feature). Для того чтобы правильно общаться с устройством, а клиент должен иметь возможность создавать репорты (при передаче данных) или извлекать данные из репортов (при приеме данных). См. секцию Построение и интерпретирование репортов HID (Building/Interpreting HID Reports) для получения информации о том, как манипулировать буферами, в которых содержатся репорты.

Устройство HID поставляет информацию для хоста (для приложения HID-клиента, в частности) через репорты Input Reports или Feature Reports. Обычно input reports содержат данные, которые образовались при работе пользователя с HID-устройством (нажатия кнопок и т. п.). С другой стороны, Feature reports содержат текущее состояние или установки для устройства. Методы получения этих двух различных репортов реализованы в функциях Read() и GetFeature(), находящиеся в файле REPORT.C.

В порядке приема данных, HClient должен использовать дескриптор файла (file handle), созданный в функции OpenHidDevice, которая имеет доступ на ЧТЕНИЕ (READ) и буфер, выделенный для нужного типа репорта. Для input reports функция Read() делает вызовы функции Win32 API ReadFile() и ожидает от устройства возврата репорта. Для feature reports функция GetFeature() устанавливает первый байт буфера репорта в значение требуемого report ID и вызывает функцию HidD_GetFeature() для получения feature report. После приема репорта обратно от нижележащих драйверов, эти подпрограммы вызывают UnpackReport() для заполнения соответствующих структур HID_DATA.

Вывод данных в устройство несколько сложнее. Тут также имеются два типа репортов, которые можно посылать в устройство - Output и Feature. Соответствующие функции для вывода данных - Write() и SetFeature() в файле REPORT.C.

Первый шаг, который выполняют обе из этих функций – создание нужного буфера репорта. Сначала находится структура HID_DATA с требуемым значением report ID. Далее, эти функции вызывают PackReport() для установки величин данных внутри выделенного буфера репорта для этого ID. Как только report buffer создан, вызываются либо WriteFile(), либо HidD_SetFeature() для отправки пакета репорта в устройство.

Важно понимать, что все обращения к файловому дескриптору устройства HID (file handle) синхронизированы. Таким образом, все потоки, которые используют этот дескриптор, будут блокированы, пока все предыдущие запросы к устройству не будут выполнены. Одна из возможных реализаций клиента должна иметь один поток, непрерывно читающий input reports, пока другой поток отправляет или принимает feature reports. Поскольку функции HidD_GetFeature() и HidD_SetFeature реализованы как вызовы DeviceIoControl, они также синхронизированы с ReadFile() и WriteFile(). В этом сценарии есть два решения. Один вариант использует перекрывающийся ввод/вывод (overlapped I/O) для асинхронного управления. Второй вариант – открытие двух дескрипторов (handles) для устройства, один для потока чтения (read thread), другой для потока feature (feature thread).

Построение/интерпретация репортов HID
Последняя важная идея, когда происходит работа с репортами, - распаковка/установка величин данных из/в буфер репорта. Эта секция раскрывает детали функций PackReport() и UnpackReport(), которые реализованы в файле REPORT.C.

Как упоминалось ранее, в процессе коммуникации с устройством HID приложение клиента должно быть способно либо создавать подходящий репорт для отправки в устройство, либо распаковывать нужную информацию из репорта, полученного от устройства. Когда HClient первоначально открыл устройство HID и создал структуру HID_DEVICE, он также создал массив структур HID_DATA для каждого типа репорта. Формат этой структуры HID_DATA следующий:

typedef struct _HID_DATA
{
   BOOLEAN     IsButtonData;
   UCHAR       Reserved;
   USAGE       UsagePage;   // usage page, которая нам нужна
   ULONG       Status;      // последний статус, возвращенный из функции accessor,
                            // когда это поде обновляется
   ULONG       ReportID;    // ReportID для предоставляемой структуры данных
   BOOLEAN     IsDataSet;   // Переменная для отслеживания – добавлена ли предоставляемая
                            //  структура данных уже в структуру репорта
   union
   {
      struct
      {
         ULONG       UsageMin;       // переменные для отслеживания usage min и max
         ULONG       UsageMax;       // Если равны, то только одиночное usage
         ULONG       MaxUsageLength; // Длина буфера usage
         PUSAGE      Usages;         // Список usage (кнопки "down" на устройстве)
      } ButtonData;
      struct
      {
         USAGE       Usage; // Использование описанием этого значения
         USHORT      Reserved;
         ULONG       Value;
         LONG        ScaledValue;
      } ValueData;
   };
} HID_DATA, *PHID_DATA;

Когда создаются репорты HID для отправки в устройство, используется функция PackReport(). PackReport получает в качестве входного параметра указатель на структуру HID_DATA и соответствующий буфер репорта. Указатель должен указывать на первую структуру HID_DATA в массиве, которая содержит report ID для создаваемого репорта. С этой информацией PackReport() выполняет следующие шаги:

• Сбрасывает содержимое текущего буфера репорта
• Ищет по массиву структур HID_DATA, просматривая все значения данных, которые совпадают с report ID
• Для каждой найденной структуры данных вызывается HidP_SetUsageValue для установки в репорт значения, сохраненного с настоящий момент в структуре
• Для каждой найденной структуры кнопки вызывается HidP_SetUsages с соответствующей величиной usage для установки состояния кнопки в репорте в значение "ВКЛ".

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

Аналогичным образом UnpackReport() распаковывает данные из данного репорта. Как и PackReport(), эта функция принимает буфер репорта, возвращенный от устройства и массив структур HID_DATA, который может быть заполнен. Эта подпрограмма выполняет следующие шаги:
• Распаковывает report ID из первого байта буфера репорта
• Просматривает массив структур HID_DATA с целью найти все структуры, которые соответствуют с report ID
• Для каждой найденной структуры вызывает HidP_GetUsageValue и HidP_GetScaledUsageValue для установки полей Value и ScaledValue
• Для каждой структуры кнопки UnpackReport() вызывает HidP_GetUsages() для получения всех кнопок в состоянии "ВКЛ" для этой структуры

По завершении массив структур HID_DATA будет содержать новые установки, основанные на информации, возвращенной в буфере репорта.

Исходный код и скомпилированный выполняемый файл можно скачать в архиве hclient.zip.

[Ссылки]

1. Исходный код и скомпилированный выполняемый файл проекта, описанного в статье.
2. Как интегрировать вместе проект из Windows DDK и Visual Studio?
3. Проект HIDTest (пример ПО хоста) и проект usb-device-hid-transfer-project-at91sam7x-ek (firmware для USB HID на ARM, которое работает вместе с HIDTest).