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



    Еще более низкий уровень (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>/rdes

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

    Заключение


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

    Как и в плошлый раз, немножко документации оставил в репозитории на случай если дизайнеры USB-IF снова решат испортить сайт.

    Комментарии 2

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

      Интересно, какие проблемы могут возникнуть при попытке более или менее интенсивного обмена посредством HID? Ведь USB HID нынче применяется для всего подряд, в том числе для управления всевозможными процессами, например, большинство лабораторного и медицинского оборудования, подключаемого к хосту посредством USB, являются именно HID устройствами. Например, у меня пара автоматических биохимических анализаторов, являющихся HID устройствами, при том, что исходно они работали по RS-232 и при переходе на USB внутрь поставили кастомный COM-USB HID адаптер. Оборудование весьма глючное, производитель во избежание возникновения проблем не рекомендует подключать к хосту иные USB устройства, кроме мыши и клавиатуры.
        0
        Думаю, именно то, что ни ep0, ни interrupt точки не предназначены для интенсивного обмена. По стандарту хост не будет выделять им слишком большую полосу. Через fearure (которыми обычно реализуется custom hid) еще не получится передавать пакеты переменной длины или поток. Только заранее предопределенные.
        Впрочем, у USB_HID для медицинского применения есть специальная страница Medical Instrument Page (0x40). Но где найти софт, умеющий с этим работать без понятия.
        А так, я тоже когда-то делал переходник с хитрого RS232 (научный вольтметр) на тоже custom-hid. Но там скорость обмена не требовалась.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое