Еще более низкий уровень (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. Некоторые
характеристики можно сгруппировать в коллекцию, но для чего это
делается я толком не понял. В документации к полям иногда ставится
пометка из пары букв о назначении поля и способе работы с ним:
Есть, разумеется, и другие, но в моем примере они не используются.
Если, например, поле 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 снова решат испортить сайт.