Как стать автором
Обновить

USB на регистрах: interrupt endpoint на примере HID

Время на прочтение10 мин
Количество просмотров9.9K


Еще более низкий уровень (avr-vusb)
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: isochronous endpoint на примере Audio device

Продолжаем разбираться с USB на контроллерах STM32L151. Как и в предыдущей части, ничего платформо-зависимого здесь не будет, зато будет USB-зависимое. Если точнее, будем рассматривать третий тип конечной точки — interrupt. И делать мы это будем на примере составного устройства «клавиатура + планшет» (ссылка на исходники).
На всякий случай предупреждаю: данная статья (как и все остальные) — скорее конспект того, что я понял, разбираясь в этой теме. Многие вещи так и остались «магией» и я буду благодарен если найдется специалист, способный объяснить их.

Первым делом напомню, что протокол HID (Human Interface Device) не предназначен для обмена большими массивами данных. Весь обмен строится на двух понятиях: событие и состояние. Событие это разовая посылка, возникающая в ответ на внешнее или внутреннее воздействие. Например, пользователь кнопочку нажал или мышь передвинул. Или на одной клавиатуре отключил NumLock, после чего хост вынужден и второй послать соответствующую команду, чтобы она это исправила, также послав сигнал нажатия NumLock и включила его обратно отобразила это на индикаторе. Для оповещения о событиях и используются interrupt точки. Состояние же это какая-то характеристика, которая не меняется просто так. Ну, скажем, температура. Или настройка уровня громкости. То есть что-то, посредством чего хост управляет поведением устройства. Необходимость в этом возникает редко, поэтому и взаимодействие самое примитивное — через ep0.

Таким образом назначение у 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>/rdesc

Для полноты картины сюда стоило бы добавить как смотреть подобные вещи в других ОС. Но у меня соответствующих знаний нет, может в комментариях подскажут. Желательно, конечно, без установки стороннего софта.

Заключение


Вот, собственно, и все, что я нарыл по HID. План-минимум — научиться читать готовые дескрипторы, эмулировать несколько устройств одновременно и реализовать планшетный ввод — выполнен. Ну и философию interrupt точек рассмотрели заодно.

Как и в плошлый раз, немножко документации оставил в репозитории на случай если дизайнеры USB-IF снова решат испортить сайт.
Теги:
Хабы:
Всего голосов 19: ↑17 и ↓2+23
Комментарии4

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань