USB на регистрах: interrupt endpoint на примере HID
![](https://webcf.waybackmachine.org/web/20210410091532im_/https://habrastorage.org/webt/co/ym/_p/coym_parrmfppizoedl855tw8m4.jpeg)
Еще более низкий уровень (avr-vusb)
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: isochronous endpoint на примере Audio device
Продолжаем разбираться с USB на контроллерах STM32L151. Как и в предыдущей части, ничего платформо-зависимого здесь не будет, зато будет USB-зависимое. Если точнее, будем рассматривать третий тип конечной точки — interrupt. И делать мы это будем на примере составного устройства «клавиатура + планшет» (ссылка на исходники).
На всякий случай предупреждаю: данная статья (как и все остальные) — скорее конспект того, что я понял, разбираясь в этой теме. Многие вещи так и остались «магией» и я буду благодарен если найдется специалист, способный объяснить их.
Первым делом напомню, что протокол HID (Human Interface Device) не предназначен для обмена большими массивами данных. Весь обмен строится на двух понятиях: событие и состояние. Событие это разовая посылка, возникающая в ответ на внешнее или внутреннее воздействие. Например, пользователь кнопочку нажал или мышь передвинул. Или на одной клавиатуре отключил NumLock, после чего хост вынужден и второй послать соответствующую команду, чтобы она
Таким образом назначение у interrupt точки такое же как у прерывания в контроллере — быстро сообщить о редком событии. Вот только USB — штука хост-центричная, так что устройство не имеет права начинать передачу самостоятельно. Чтобы это обойти, разработчики USB придумали костыль: хост периодически посылает запросы на чтение всех interrupt точек. Периодичность запроса настраивается последним параметром в EndpointDescriptor'е (это часть ConfigurationDescriptor'а). В прошлых частях мы уже видели там поле bInterval, но его значение игнорировалось. Теперь ему наконец-то нашлось применение. Значение имеет размер 1 байт и задается в миллисекундах, так что опрашивать нас будут с интервалом от 1 мс до 2,55 секунд. Для низкоскоростных устройств минимальный интервал составляет 10 мс. Наличие костыля с опросом interrupt точек для нас означает, что даже в отсутствие обмена они будут впустую тратить полосу пропускания шины.
Логичный вывод: interrupt точки предназначены только для IN транзакций. В частности, они используются для передачи событий от клавиатуры или мыши, для оповещения об изменении служебных линий COM-порта, для синхронизации аудиопотока и тому подобных вещей. Но для всего этого придется добавлять другие типы точек. Поэтому, чтобы не усложнять пример, ограничимся реализацией HID-устройства. Вообще-то, такое устройство мы уже делали в первой части, но там дополнительные точки не использовались вовсе, да и структура HID-протокола рассмотрена не была.
ConfigurationDescriptor
static const uint8_t USB_ConfigDescriptor[] = {
ARRLEN34(
ARRLEN1(
bLENGTH, // bLength: Configuration Descriptor size
USB_DESCR_CONFIG, //bDescriptorType: Configuration
wTOTALLENGTH, //wTotalLength
1, // bNumInterfaces
1, // bConfigurationValue: Configuration value
0, // iConfiguration: Index of string descriptor describing the configuration
0x80, // bmAttributes: bus powered
0x32, // MaxPower 100 mA
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_INTERFACE, //bDescriptorType
0, //bInterfaceNumber
0, // bAlternateSetting
2, // bNumEndpoints
HIDCLASS_HID, // bInterfaceClass:
HIDSUBCLASS_BOOT, // bInterfaceSubClass:
HIDPROTOCOL_KEYBOARD, // bInterfaceProtocol:
0x00, // iInterface
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_HID, //bDescriptorType
USB_U16(0x0110), //bcdHID
0, //bCountryCode
1, //bNumDescriptors
USB_DESCR_HID_REPORT, //bDescriptorType
USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_ENDPOINT, //bDescriptorType
INTR_NUM, //bEdnpointAddress
USB_ENDP_INTR, //bmAttributes
USB_U16( INTR_SIZE ), //MaxPacketSize
10, //bInterval
)
ARRLEN1(
bLENGTH, //bLength
USB_DESCR_ENDPOINT, //bDescriptorType
INTR_NUM | 0x80, //bEdnpointAddress
USB_ENDP_INTR, //bmAttributes
USB_U16( INTR_SIZE ), //MaxPacketSize
10, //bInterval
)
)
};
Внимательный читатель тут же может обратить внимание на описания конечных точек. Со второй все в порядке — IN точка (раз произведено сложение с 0x80) типа interrupt, заданы размер и интервал. А вот первая вроде бы объявлена как OUT, но в то же время interrupt, что противоречит сказанному ранее. Да и здравому смыслу тоже: хост не нуждается в костылях чтобы передать в устройство что угодно и когда угодно. Но таким способом обходятся другие грабли: тип конечной точки в STM32 устанавливается не для одной точки, а только для пары IN/OUT, так что не получится задать 0x81-й точке тип interrupt, а 0x01-й control. Впрочем, для хоста это проблемой не является, он бы, наверное, и в bulk точку те же данные посылал… что, впрочем, я проверять не стану.
HID descriptor
Структура HID descriptor'а больше всего похожа на конфигурационных файл «имя=значение», но в отличие от него, «имя» представляет собой числовую константу из списка USB-специфичных, а «значение» — либо тоже константу, либо переменную размером от 0 до 3 байт.
Важно: для некоторых «имен» длина «значения» задается в 2 младших битах поля «имени». Например, возьмем LOGICAL_MINIMUM (минимальное значение, которое данная переменная может принимать в штатном режиме). Код этой константы равен 0x14. Соответственно, если «значения» нет (вроде бы такого не бывает, но утверждать не буду — зачем-то же этот случай ввели), то в дескрипторе будет единственное число 0x14. Если «значение» равно 1 (один байт) то записано будет 0x15, 0x01. Для двухбайтного значения 0x1234 будет записано 0x16, 0x34, 0x12 — значение записывается от младшего к старшему. Ну и до кучи число 0x123456 будет 0x17, 0x56, 0x34, 0x12.
Естественно, запоминать все эти числовые константы мне лень, поэтому воспользуемся макросами. К сожалению, я так и не нашел способа заставить их самостоятельно определять размер переданного значения и разворачиваться в 1, 2, 3 или 4 байта. Поэтому пришлось сделать костыль: макрос без суффикса отвечает за самые распространенные 8-битные значения, с суффиксом 16 за 16-битные, а с 24 — за 24-битные. Также были написаны макросы для «составных» значений вроде диапазона LOGICAL_MINMAX24(min, max), которые разворачиваются в 4, 6 или 8 байтов.
Как и в конфигурационных файлах, здесь присутствуют «секции», называемые страницами (usage_page), которые группируют устройства по назначению. Скажем, есть страница с базовой периферией вроде клавиатур, мышек и просто кнопок, есть джойстики и геймпады (искренне рекомендую посмотреть какие именно! Там и для танков, и для космических кораблей, и для подводных лодок и для всего чего угодно), даже дисплеи есть. Правда, где искать софт, умеющий со всем этим работать, я без понятия.
Внутри каждой страницы выбирается конкретное устройство. Например, для мышки это указатель и кнопки, а для планшета — стилус или палец юзера (что?!). Ими же обозначаются составные части устройства. Так, частью указателя являются его координаты по X и Y. Некоторые характеристики можно сгруппировать в «коллекцию», но для чего это делается я толком не понял. В документации к полям иногда ставится пометка из пары букв о назначении поля и способе работы с ним:
CA | Collection(application) | Служебная информация, никакой переменной не соответствующая |
CL | Collection(logical) | -/- |
CP | Collection(phisical) | -/- |
DV | Dynamic Value | входное или выходное значение (переменная) |
MC | Momentary Control | флаг состояния (1-флаг взведен, 0-сброшен) |
OSC | One Shot Control | однократное событие. Обрабатывается только переход 0->1 |
Есть, разумеется, и другие, но в моем примере они не используются. Если, например, поле X помечено как DV, то оно считается переменной ненулевой длины и будет включено в структуру репорта. Поля MC или OSC также включаются в репорт, но имеют размер 1 бит.
Один репорт (пакет данных, посылаемый или принимаемый устройством) содержит значения всех описанных в нем переменных. Описание кнопки говорит о всего одном занимаемом бите, но для относительных координат (насколько передвинулась мышка, например) требуется как минимум байт, а для абсолютных (как для тачскрина) уже нужно минимум 2 байта. Плюс к этому, многие элементы управления имеют еще свои физические ограничения. Например, АЦП того же тачскрина может иметь разрешение всего 10 бит, то есть выдавать значения от 0 до 1023, которое хосту придется масштабировать к полному разрешению экрана. Поэтому в дескрипторе помимо предназначения каждого поля задается еще диапазон его допустимых значений (LOGICAL_MINMAX), плюс иногда диапазон физических значений (в миллиматрах там, или в градусах) и обязательно представление в репорте. Представление задается двумя числами: размер одной переменной (а битах) и их количество. Например, координаты касания тачскрина в создаваемом нами устройстве задаются так:
USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
Здесь видно, что объявлены две переменные, изменяющиеся в диапазоне от 0 до 10000 и занимающие в репорте два участка по 16 бит.
Последнее поле говорит, что вышеописанные переменные будут хостом читаться (IN) и поясняется как именно. Описывать его флаги подробно я не буду, остановлюсь только на нескольких. Флаг HID_ABS показывает, что значение абсолютное, то есть никакая предыстория на него не влияет. Альтернативное ему значение HID_REL показывает что значение является смещением относительно предыдущего. Флаг HID_VAR говорит, что каждое поле отвечает за свою переменную. Альтернативное значение HID_ARR говорит, что передаваться будут не состояния всех кнопок из списка, а только номера активных. Этот флаг применим только к однобитным полям. Вместо того, чтобы передавать 101/102 состояния всех кнопок клавиатуры можно ограничиться несколькими байтами со списком нажатых клавиш. Тогда первый параметр REPORT_FMT будет отвечать за размер номера, а второй — за количество.
Поскольку размер всех переменных задается в битах, логично спросить: а что же с кнопками, ведь их количество может быть не кратно 8, а это приведет к трудностям выравнивания при чтении и записи. Можно было бы выделить каждой кнопке по байту, но тогда бы сильно вырос объем репорта, что для скоростных передач вроде interrupt, неприятно. Вместо этого кнопки стараются расположить поближе друг к другу, а оставшееся место заполняют битами с флагом HID_CONST.
Теперь мы можем если не написать дескриптор с нуля, то хотя бы попытаться его читать, то есть определить, каким битам соответствует то или иное поле. Достаточно посчитать INPUT_HID'ы и соответствующие им REPORT_FMT'ы. Только учтите, что именно такие макросы придумал я, больше их никто не использует. В чужих дескрипторах придется искать input, report_size, report_count, а то и вовсе числовые константы.
Вот теперь можно привести дескриптор целиком:
static const uint8_t USB_HIDDescriptor[] = {
//keyboard
USAGE_PAGE( USAGEPAGE_GENERIC ),//0x05, 0x01,
USAGE( USAGE_KEYBOARD ), // 0x09, 0x06,
COLLECTION( COLL_APPLICATION, // 0xA1, 0x01,
REPORT_ID( 1 ), // 0x85, 0x01,
USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
USAGE_MINMAX(224, 231), //0x19, 0xE0, 0x29, 0xE7,
LOGICAL_MINMAX(0, 1), //0x15, 0x00, 0x25, 0x01,
REPORT_FMT(1, 8), //0x75, 0x01, 0x95, 0x08
INPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x81, 0x02,
//reserved
REPORT_FMT(8, 1), // 0x75, 0x08, 0x95, 0x01,
INPUT_HID(HID_CONST), // 0x81, 0x01,
REPORT_FMT(1, 5), // 0x75, 0x01, 0x95, 0x05,
USAGE_PAGE( USAGEPAGE_LEDS ), // 0x05, 0x08,
USAGE_MINMAX(1, 5), //0x19, 0x01, 0x29, 0x05,
OUTPUT_HID( HID_DATA | HID_VAR | HID_ABS ), // 0x91, 0x02,
//выравнивание до 1 байта
REPORT_FMT(3, 1), // 0x75, 0x03, 0x95, 0x01,
OUTPUT_HID( HID_CONST ), // 0x91, 0x01,
REPORT_FMT(8, 6), // 0x75, 0x08, 0x95, 0x06,
LOGICAL_MINMAX(0, 101), // 0x15, 0x00, 0x25, 0x65,
USAGE_PAGE( USAGEPAGE_KEYBOARD ), // 0x05, 0x07,
USAGE_MINMAX(0, 101), // 0x19, 0x00, 0x29, 0x65,
INPUT_HID( HID_DATA | HID_ARR ), // 0x81, 0x00,
)
//touchscreen
USAGE_PAGE( USAGEPAGE_DIGITIZER ), // 0x05, 0x0D,
USAGE( USAGE_PEN ), // 0x09, 0x02,
COLLECTION( COLL_APPLICATION, // 0xA1, 0x0x01,
REPORT_ID( 2 ), //0x85, 0x02,
USAGE( USAGE_FINGER ), // 0x09, 0x22,
COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
USAGE( USAGE_TOUCH ), // 0x09, 0x42,
USAGE( USAGE_IN_RANGE ), // 0x09, 0x32,
LOGICAL_MINMAX( 0, 1), // 0x15, 0x00, 0x25, 0x01,
REPORT_FMT( 1, 2 ), // 0x75, 0x01, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_DATA | HID_ABS ), // 0x91, 0x02,
REPORT_FMT( 1, 6 ), // 0x75, 0x01, 0x95, 0x06,
INPUT_HID( HID_CONST ), // 0x81, 0x01,
USAGE_PAGE( USAGEPAGE_GENERIC ), //0x05, 0x01,
USAGE( USAGE_POINTER ), // 0x09, 0x01,
COLLECTION( COLL_PHISICAL, // 0xA1, 0x00,
USAGE( USAGE_X ), // 0x09, 0x30,
USAGE( USAGE_Y ), // 0x09, 0x31,
LOGICAL_MINMAX16( 0, 10000 ), //0x16, 0x00, 0x00, 0x26, 0x10, 0x27,
REPORT_FMT( 16, 2 ), // 0x75, 0x10, 0x95, 0x02,
INPUT_HID( HID_VAR | HID_ABS | HID_DATA), // 0x91, 0x02,
)
)
)
};
Помимо рассмотренных ранее полей тут есть и такое интересное поле, как REPORT_ID. Поскольку, как понятно из комментариев, устройство у нас составное, хосту надо как-то определить, чьи данные он принимает. Для этого данное поле и нужно.
И еще одно поле, на которое хотелось бы обратить внимание — OUTPUT_HID. Как видно из названия, оно отвечает не за прием репорта (IN), а за передачу (OUT). Расположено оно в разделе клавиатуры и описывает индикаторы CapsLock, NumLock, ScrollLock а также два экзотических — Compose (флаг ввода некоторых символов, для которых нет собственных кнопок вроде á, µ или ) и Kana (ввод иероглифов). Собственно, ради этого поля мы и заводили OUT точку. В ее обработчике будем проверять не надо ли зажечь индикаторы CapsLock и NumLock: на плате как раз два диодика и разведено.
Существует и третье поле, связанное с обменом данными — FEATURE_HID, мы его использовали в первом примере. Если INPUT и OUTPUT предназначены для передачи событий, то FEATURE — состояния, которое можно как читать, так и писать. Правда, делается это не через выделенные endpoint'ы, а через обычную ep0 путем соответствующих запросов.
Если внимательно рассмотреть дескриптор, можно восстановить структуру репорта. Точнее, двух репортов:
struct{
uint8_t report_id; //1
union{
uint8_t modifiers;
struct{
uint8_t lctrl:1; //left control
uint8_t lshift:1;//left shift
uint8_t lalt:1; //left alt
uint8_t lgui:1; //left gui. Он же hyper, он же winkey
uint8_t rctrl:1; //right control
uint8_t rshift:1;//right shift
uint8_t ralt:1; //right alt
uint8_t rgui:1; //right gui
};
};
uint8_t reserved; //я не знаю зачем в официальной документации это поле
uint8_t keys[6]; //список номеров нажатых клавиш
}__attribute__((packed)) report_kbd;
struct{
uint8_t report_id; //2
union{
uint8_t buttons;
struct{
uint8_t touch:1; //фактнажатия на тачскрин
uint8_t inrange:1; //нажатие в рабочей области
uint8_t reserved:6;//выравнивание до 1 байта
};
};
uint16_t x;
uint16_t y;
}__attribute__((packed)) report_tablet;
Отправлять их будем по нажатию кнопок на плате, причем. поскольку пишем мы всего лишь пример реализации, а не законченное устройство, делать это будем по-варварски — посылая два репорта, в первом из которых «нажимая» клавиши, а во втором — «отпуская». Причем с огромной «тупой» задержкой между посылками. Если не посылать репорт с «отпущенными» клавишами, система посчитает что клавиша осталась нажатой и будет ее повторять. Естественно, ни о какой эффективности тут не идет и речи, о безопасности тоже, но для теста сойдет. Ах да, куда ж без очередных граблей! Размер структуры должен совпадать с тем, что описано в дескрипторе, иначе винда сделает вид, что не понимает чего от нее хотят. Как обычно, линукс подобные ошибки игнорирует и работает как ни в чем не бывало.
В процессе тестирования наткнулся на забавный побочный эффект: в Windows7 при нажатии на «тачскрин» вылезает окошко рукописного ввода. Я об этой фиче не знал.
Если к вам попало готовое устройство
… и хочется посмотреть на него изнутри. Первым делом, естественно, смотрим, можно даже от обычного пользователя, ConfigurationDescriptor:
lsusb -v -d <VID:PID>
Для HID-дескриптора же я не нашел (да и не искал) способа лучше, чем от рута:
cat /sys/kernel/debug/hid/<address>/rdes
Для полноты картины сюда стоило бы добавить как смотреть подобные вещи в других ОС. Но у меня соответствующих знаний нет, может в комментариях подскажут. Желательно, конечно, без установки стороннего софта.
Заключение
Вот, собственно, и все, что я нарыл по HID. План-минимум — научиться читать готовые дескрипторы, эмулировать несколько устройств одновременно и реализовать планшетный ввод — выполнен. Ну и философию interrupt точек рассмотрели заодно.
Как и в плошлый раз, немножко документации оставил в репозитории на случай если дизайнеры USB-IF снова решат испортить сайт.
Комментарии 0
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.