Формируем дескрипторы отчётов HID по-человечески
kayo — Пнд, 01/05/2017 - 01:19
Разрабатывая USB или Bluetooth периферию, довольно часто требуется реализовать интерфейс HID. В этом посте мы будем делать это по-человечески, а не так, как все…
Вкратце: что такое HID и с чем его едят
Как следует из названия, рассматриваемый класс интерфейсов предназначен для реализации устройств взаимодействия с пользователем. Но в силу простоты и удобства зачастую через этот интерфейс работают и с другими типами периферии. Со стороны устройства реализация HID очень проста: не нужно ломать голову над низкоуровневыми протоколами обмена данными, описал отчёты и знай себе обрабатывай запросы. А со стороны хоста с HID устройствами можно работать в пространстве пользователя через стандартный драйвер операционной системы. Конечно, CDC ACM тоже позволяет обмениваться данными с устройством аналогичным образом, но этот класс сложнее в реализации на устройстве, он удобен для организации непрерывного поточного взаимодействия, и менее подходит для работы в режиме запрос-ответ.
Подробнее: откуда ноги растут и как оно, собственно, работает
HID — штука универсальная: обмен данными осуществляется путём отправки и получения так называемых отчётов (HID Reports), каждый из которых мы обязаны описать специальным образом, чтобы драйвер операционной системы, отвечающий за работу с HID, мог правильно понимать внутреннюю структуру наших данных. Если мы реализуем стандартное устройство ввода, типа клавиатуры, мыши или джойстика, то проанализировав содержимое дескрипторов отчётов нашего устройства, драйвер поймёт, как получить интересующие его данные, будь то нажатые кнопки или информация о перемещении указателя. Даже если мы используем HID для каких то своих целей, и будем обмениваться отчётами из собственной программы, мы всё равно обязаны описать отчёты надлежащим образом в соответствии со спецификацией.
Дескрипторы отчётов
Так вот, формирование этих самых дескрипторов отчётов и является самой сложной частью в реализации HID периферии. Чего только ни делают разработчики и всё получается как-то мимо кассы. Одни используют специальные программы (вроде официального HID Descriptor Tool), которые позволяют формировать дескрипторы в графическом интерфейсе, и на выходе выдают готовый набор байтов. Другие использует конвертеры, преобразующие текстовое описание в код и обратно (например, hidrd). Всё бы хорошо, но дальнейшая поддержка такого сформированного сторонними инструментами кода будет значительно осложнена: при каждом исправлении необходимо будет снова воспользоваться соответствующей программой. Третьи тупо берут готовый дескриптор из примеров, правят размер и количество данных под свою задачу и обменивается пакетами в своём формате, не соответствующем фактическому содержимому дескриптора. Но этот подход тоже чреват многими нехорошими моментами, не всегда очевидными, как в общем-то и любое другое отклонение в сторону от буквы спецификации.
Мы же пойдём прямым путём: будем формировать дескриптор понятным читаемым образом, с использованием макросов препроцессора Си, так, как нам, сишникам, привычно и удобно. Этот подход не отменяет необходимость ознакомления со спецификацией HID 1.11 в целях понимания базовых принципов формирования отчётных дескрипторов, однако освобождает от необходимости знать подробности бинарного формата. Другими словами, мы будем описывать дескрипторы примерно так, как они описаны в примерах означенной выше спецификации.
Базовые принципы
Кому лень прямо сейчас лезть в спеку и вникать в суть, тот читает мой вольный текст ниже.
Описание отчёта состоит из так называемых элементов или пунктов, каждый из которых состоит из заголовка и, опционально, блока данных. Вообще, элементы бывают короткие и длинные, но здесь мы опишем только короткие, потому как работать будем только с ними. Заголовок элемента включает поле размера данных (2 бита), поле типа (2 бита) и тег (4 бита) — итого 8 бит. Поле размера задаёт длину данных в байтах: 0, 1, 2 или 4 байта. Поле типа задаёт тип: главный (main), глобальный (global), локальный (local). Поле тега задаёт собственно элемент. Элементов много разных, и описывать здесь их все мы не будем, остановимся лишь на некоторых основных по ходу описания.
Пример дескриптора
Предположим, что мы разрабатываем стандартное устройство типа мыши. Вот пример дескриптора отчёта для него из спецификации:
Usage Page (Generic Desktop), Usage (Mouse), Collection (Application), Usage (Pointer), Collection (Physical), Usage Page (Buttons), Usage Minimum (01), Usage Maximun (03), Logical Minimum (0), Logical Maximum (1), Report Count (3), Report Size (1), Input (Data, Variable, Absolute), Report Count (1), Report Size (5), Input (Constant), Usage Page (Generic Desktop), Usage (X), Usage (Y), Logical Minimum (-127), Logical Maximum (127), Report Size (8), Report Count (2), Input (Data, Variable, Relative), End Collection, End Collection
Что мы тут видим? А видим мы как раз таки наши элементы по одному элементу на строку. Каждый элемент в этом примере занимает 2 либо 1 байт. Первым идёт глобальный элемент Usage Page, который описывает предназначение нашего устройства (Generic Desktop). Далее идёт локальный элемент Usage, который определяет собственно тип устройства (Mouse), давая тем самым подсказку драйверу ОС. Затем при помощи главного элемента Collection начинается описание коллекции типа приложение (Application), и снова идёт элемент Usage Page, но на этот раз он определяет назначение коллекции как указатель (Pointer) тем самым все последующие элементы до элемента конца коллекции будут относиться к указателю.
Далее начинается коллекция типа Physical и уже внутри этой коллекции описывается элемент отчёта Buttons. Локальные элементы Usage Minimum и Usage Maximum связаны с конкретным вариантом использования, в данном случае они идентифицируют первую и последнюю кнопки мыши. Далее глобальные элементы Logical Minimum и Logical Maximum задают минимальное и максимальное значения состояния этих кнопок. Следующие глобальные элементы Report Count и Report Size задают количество значений в отчёте и размер каждого значения в битах. Главный элемент Input заканчивает описание кнопок и определяет описанные поля как часть дескриптора входного отчёта. В скобках приводятся флаги: Data - означает, что описанные поля следует трактовать как данные, а не как константы, Variable - описана переменная а не массив, Absolute - значения следует трактовать как абсолютные.
Часто бывает удобно выравнивать данные по байта или словам, однако в описании отчетов размеры задаются в битах, поэтому для выравнивания добавляют так называемые отступы. Следующие три строки как раз декларируют такой отступ после битов состояний трёх кнопок в отчёте количеством 1 и размером 5 бит, а чтобы драйвер проигнорировал эти биты, в элементе Input вместо Data используется флаг Constant.
Далее похожим образом описываются поля координат курсора. Поскольку мышь в отличии от дигитайзера обычно генерирует относительные координаты, то вместо Absolute в элементе Input указано Relative. А поскольку эти относительные координаты могут быть как положительными так и отрицательными, то указываются соответствующие предельные значения в Logical Minimum и Logical Maximum от -127 до 127. На каждое значение отводится до 8 бит (Report Size) и всего значений 2 (Report Count). Подсказка драйверу о порядке полей со значениями координат здесь даётся с использованием элементов Usage.
Описание дескриптора на Си
Итак, наша задача представить то же самое описание средствами языка Си, в чём нам поможет Си препроцессор. Вот как будет выглядеть теперь тот же самый пример:
#include <stdint.h> /* Определения макросов нашего HID Report Descriptor DSL */ #include "hid_def.h" static const uint8_t hid_report_descriptor[] = { HID_USAGE_PAGE (GENERIC_DESKTOP), HID_USAGE (MOUSE), HID_COLLECTION (APPLICATION), HID_USAGE (POINTER), HID_COLLECTION (PHYSICAL), HID_USAGE_PAGE (BUTTONS), HID_USAGE_MINIMUM (1, 1), HID_USAGE_MAXIMUM (1, 3), HID_LOGICAL_MINIMUM (1, 0), HID_LOGICAL_MAXIMUM (1, 1), HID_REPORT_COUNT (3), HID_REPORT_SIZE (1), HID_INPUT (DATA, VARIABLE, ABSOLUTE), HID_REPORT_COUNT (1), HID_REPORT_SIZE (5), HID_INPUT (CONSTANT), HID_USAGE_PAGE (GENERIC_DESKTOP), HID_USAGE (X), HID_USAGE (Y), HID_LOGICAL_MINIMUM (1, -127), HID_LOGICAL_MAXIMUM (1, 127), HID_REPORT_SIZE (8), HID_REPORT_COUNT (2), HID_INPUT (DATA, VARIABLE, RELATIVE), HID_END_COLLECTION (PHYSICAL), HID_END_COLLECTION (APPLICATION), };
Как видим, описание максимально приближено к примеру из спецификации, но есть несколько отличий, связанных с реализацией макросов препроцессора, описывающих элементы. Поскольку размер данных некоторых элементов не фиксированный, то нам понадобилось как-то указать этот размер. Поэтому первым параметром у элементов, задающие границы minimum/maximum, идёт размер значения в байтах, а вторым само значение. Макросы, задающие главные элементы Input, Output, Feature, устроены так, что могут принимать на вход любое количество флагов, в том числе и ни одного, поэтому флаги Data и Absolute на самом деле не обязательны, поскольку установлены по-умолчанию.
Вы наверняка заметили что наш макрос HID_END_COLLECTION принимает тип коллекции в качестве аргумента. Так вот, с точки зрения дескриптора это не имеет особого смысла, так сделано просто для удобства, чтобы было сразу видно, какая коллекция завершается, а технически можно указать какие угодно аргументы или вообще обойтись без оных.
Присоединяйтесь к вечеринке
Не буду скрывать, макро-определения для нашего предметно-ориентированного языка в файле hid_def.h содержат изрядную дозу препроцессорной магии, которая определена в другом заголовочном файле macro.h.
Этот проект я оформил в репозиторий github:katyo/hid_def, так что теперь самое время начать писать дескрипторы отчётов HID по-человечески.

Спасибо за хорошую статью.
Паша (не проверено) — Чт, 31/05/2018 - 14:29Большое спасибо.Очень
Владимир Хлуденьков (не проверено) — Пт, 13/07/2018 - 13:13Большое спасибо.
Очень интересная статья.
А как насчёт кастомного HID устройства?
Спасибо.А как нсчёт
Владимир Хлуденьков (не проверено) — Пт, 13/07/2018 - 13:15Спасибо.
А как нсчёт кастомного хид-устройства?
А в чём проблема таким
kayo — Втр, 14/08/2018 - 10:53Хорошая статья,
Расул (не проверено) — Ср, 10/10/2018 - 11:45Хорошая статья, спасибо!
Будет ли статья про препроцессорную магию? не совсем понятно, как там что вызывает.
Отправить комментарий