Русский
Русский
English
Статистика
Реклама

Stm32l151

USB на регистрах STM32L1 STM32F1

21.03.2021 16:08:22 | Автор: admin

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

С программным USB на примере AVR мы уже познакомились, пришла пора взяться за более тяжелые камни stm32. Подопытными у нас будут классический STM32F103C8T6 а также представитель малопотребляющей серии STM32L151RCT6. Как и раньше, пользоваться покупными отладочными платами и HAL'ом не будем, отдав предпочтение велосипеду.

Раз уж в заглавии указано два контроллера, стоит рассказать об основных отличиях. В первую очередь это резистор подтяжки, говорящий usb-хосту, что в него что-то воткнули. В L151 он встроен и управляется битом SYSCFG_PMC_USB_PU, а в F103 нет, придется впаивать на плату снаружи и соединять либо с VCC, либо с ножкой контроллера. В моем случае под руку попалась ножка PA10. На которой висит UART1 А другой вывод UART1 конфликтует с кнопкой замечательную плату я развел, не находите? Второе отличие это объем флеш-памяти: в F103 ее 64 кБ, а в L151 целых 256 кБ, чем мы когда-нибудь и воспользуемся при изучении конечных точек типа Bulk. Также у них немного отличаются настройки тактирования, да и лампочками с кнопочками могут на разных ногах висеть, но это уже совсем мелочи. Пример для F103 доступен в репозитории, так что адаптировать под него остальные эксперименты с L151 будет несложно. Исходные коды доступны тут: github.com/COKPOWEHEU/usb

Общий принцип работы с USB


Работа с USB в данном контроллере предполагается с использованием аппаратного модуля. То есть мы ему говорим что делать, он делает и в конце дергает прерывание я готовое!. Соответственно, из основного main'а нам вызывать почти ничего не надо (хотя функцию usb_class_poll я на всякий случай предусмотрел). Обычный цикл работы ограничен единственным событием обмен данными. Остальные сброс, сон и прочие события исключительные, разовые.

В низкоуровневые подробности обмена я на этот раз углубляться не буду. Кому интересно, может почитать про vusb. Но напомню, что обмен обычными данными идет не по одному байту, а по пакету, причем направление передачи задает хост. И названия этих направлений диктует тоже он: IN передача означает что хост принимает данные (а устройство передает), а OUT что хост передает данные (а мы принимаем). Более того, каждый пакет имеет свой адрес номер конечной точки, с которой хост хочет общаться. Пока что у нас будет единственная конечная точка 0, отвечающая за устройство в целом (для краткости я еще буду называть ее ep0). Для чего нужны остальные я расскажу в других статьях. Согласно стандарту, размер ep0 составляет строго 8 байт для низкоскоростных устройств (к каковым относится все тот же vusb) и на выбор 8, 16, 32, 64 байта для полноскоростных вроде нашего.

А если данных слишком мало и они не заполняют буфер полностью? Тут все просто: помимо данных в пакете передается и их размер (это может быть поле wLength либо низкоуровневая комбинация сигналов SE0, обозначающая конец передачи), так что даже если нам надо через ep0 размером 64 байта передать три байта, то переданы будут именно три байта. В результате расходовать пропускную способность, гоняя ненужные нули, мы не будем. Так что не стоит мельчить: если можем себе позволить потратить 64 байта, тратим не раздумывая. Помимо прочего это несколько уменьшит загруженность шины, ведь передать за раз кусок в 64 байта (плюс все заголовки и хвосты) проще, чем 8 раз по 8 байт (к каждому из которых опять-таки заголовки и хвосты).

А если данных напротив слишком много? Тут сложнее. Данные приходится разбивать по размеру конечной точки и передавать порциями. Скажем, размер ep0 у нас 8 байт, а передать хост пытается 20 байт. При первом прерывании к нам придут байты 0-7, во втором 8-15, в третьем 16-20. То есть чтобы собрать посылку целиком нужно получить целых три прерывания. Для этого в том же HAL придуман хитрый буфер, с которым я попытался разобраться, но после четвертого уровня пересылки одного и того же между функциями, плюнул. Как результат, в моей реализации буферизация ложится на плечи программиста.

Но хост хотя бы всегда говорит сколько данных пытается передать. Когда же данные передаем мы, надо как-то хитро дернуть низкоуровневые состояния ножек чтобы дать понять что данные закончились. Точнее, дать понять модулю usb, что данные закончились и что надо дернуть ножки. Делается это вполне очевидным способом записью только части буфера. Скажем, если буфер у нас 8 байт, а мы записали 4, то очевидно, данных у нас всего 4 байта, после которых модуль пошлет волшебную комбинацию SE0 и все будут довольны. А если мы записали 8 байт, это значит что у нас всего 8 байт или что это только часть данных, которая влезла в буфер? Модуль usb считает что часть. Поэтому если мы хотим остановить передачу, то после записи 8-байтного буфера должны записать следом 0-байтный. Это называется ZLP, Zero Length Packet. Про то, как это выглядит в коде, я расскажу чуть позже.

Организация памяти


Согласно стандарту, размер конечной точки 0 может достигать 64 байт. Размер любой другой аж 1024 байт. Количество точек также может отличаться от устройства к устройству. Те же STM32L1 поддерживают до 7 точек на вход и 7 на выход (не считая ep0), то есть до 14 кБ одних только буферов. Которые в таком объеме скорее всего никому никогда не понадобятся. Непозволительный расход памяти! Вместо этого модуль usb отгрызает себе кусок общей памяти ядра и пользуется ей. Эта область называется PMA (packet memory area) и начинается с USB_PMAADDR. А чтобы указать где внутри нее располагаются буферы каждой конечной точки, в начале выделен массив на 8 элементов каждый со следующей структурой, и только потом собственно область для данных:
typedef struct{    volatile uint32_t usb_tx_addr;    volatile uint32_t usb_tx_count;    volatile uint32_t usb_rx_addr;    volatile union{      uint32_t usb_rx_count;      struct{        uint32_t rx_count:10;        uint32_t rx_num_blocks:5;        uint32_t rx_blocksize:1;      };    };}usb_epdata_t;


Здесь задаются начало буфера передачи, его размер, потом начало буфера приема и его размер. Обратите внимание во-первых, что usb_tx_count задает не собственно размер буфера, а количество данных для передачи. То есть наш код должен записать по адресу usb_tx_addr данные, потом записать в usb_tx_count их размер и только потом дернуть регистр модуля usb что данные мол записаны, передавай. Еще большее внимание обратите внимание на странный формат размера буфера приема: он представляет собой структуру, в которой 10 бит rx_count отвечают за реальное количество прочитанных данных, а вот остальные уже действительно за размер буфера. Надо же железке знать докуда писать можно, а где начинаются чужие данные. Формат этой настройки тоже довольно интересный: флаг rx_block_size говорит в каких единицах задается размер. Если он сброшен в 0, то в 2-байтных словах, тогда размер буфера равен 2*rx_num_blocks, то есть от 0 до 62. А если выставлен в 1, то в 32-байтных блоках, соответственно размер буфера тогда оказывается 32*rx_num_blocks и лежит в диапазоне от 32 до 512 (да, не до 1024, такое вот ограничение контроллера).

Для размещения буферов в этой области будем использовать полудинамический подход. То есть выделять память по запросу, но не освобождать ее (еще не хватало malloc / free изобретать!). На начало неразмеченного пространства будет указывать переменная lastaddr, изначально указывающая на начало области PMA за вычетом таблицы структур, рассмотренной выше. Ну а при каждом вызове функции настройки очередной конечной точки usb_ep_init() она будет сдвигаться на указанный там размер буфера. И в соответствующую ячейку таблицы будет вносится нужное значение, естественно. Значение этой переменной сбрасывается по событию ресета, после чего тут же следует вызов usb_class_init(), в котором точки настраиваются заново в соответствии с юзерской задачей.

Работа с регистрами приема-передачи


Как было только что сказано, на прием мы читаем сколько же реально данных пришло (поле usb_rx_count), потом читаем сами данные, потом дергаем модуль usb что буфер свободен, можно принимать следующий пакет. На передачу наоборот: записываем данные в буфер, потом выставляем в usb_tx_count сколько же записано и наконец дергаем модуль что буфер заполнен, можно передавать.

Первые грабли начинаются при собственно работе с буфером: он организован не по 32 бита, как весь остальной контроллер, и не по 8 бит, как можно было ожидать. А по 16 бит! В результате запись и чтение в него осуществляются по 2 байта, выровненные по 4 байта. Спасибо, ST, что сделали такое извращение! Как бы скучно без этого жилось! Теперь обычным memcpy не обойтись, придется городить специальные функции. Кстати, если кто любит DMA, то оно такое преобразование делать вроде умеет самостоятельно, хотя я это не проверял.

И тут же вторые грабли с записью в регистры модуля. Дело в том, что за настройку каждой конечной точки за ее тип (control, bulk и т.д.) и состояние отвечает один регистр USB_EPnR, то есть просто так в нем бит не поменяешь, надо следить чтобы не попортить остальные. А во-вторых, в этом регистре присутствуют биты аж четырех типов! Одни доступны только на чтение (это замечательно), другие на чтение и запись (тоже нормально), третьи игнорируют запись 0, но при записи 1 меняют состояние на противоположное (начинается веселье), а четвертые напротив игнорируют запись 1, но запись 0 сбрасывает их в 0. Скажите мне, какой наркоман додумался в одном регистре сделать биты, игнорирующие 0 и игнорирующие 1?! Нет, я готов предположить что это сделано ради сохранения целостности регистра, когда к нему обращаются и из кода, и из железа. Но вам что, лень было поставить инвертор чтобы биты сбрасывались записью 1? Или в другом месте инвертор чтобы другие биты инвертировались записью 0? В результате выставление двух битов регистра выглядит так (еще раз спасибо ST за такое извращение):
#define ENDP_STAT_RX(num, stat) do{USB_EPx(num) = ((USB_EPx(num) & ~(USB_EP_DTOG_RX | USB_EP_DTOG_TX | USB_EPTX_STAT)) | USB_EP_CTR_RX | USB_EP_CTR_TX) ^ stat; }while(0)


Ах да, чуть не забыл: доступа к регистру по номеру у них тоже нет. То есть макросы USB_EP0R, USB_EP1R и т.д. у них есть, но вот если номер пришел в переменной, то увы. Пришлось изобретать свой USB_EPx() а что поделать.

Ну и для соблюдения формальностей укажу, что флаг готовности к чтению (то есть что предыдущие данные мы уже прочитали) задается битовой маской USB_EP_RX_VALID, а к записи (то есть что данные мы записали полностью и их можно передавать) маской USB_EP_TX_VALID.

Обработка IN и OUT запросов


Возникновение прерывания USB может сигнализировать о разных вещах, но сейчас мы сосредоточимся на запросах обмена данными. Флагом такого события будет бит USB_ISTR_CTR. Если увидели его, можем разбираться с какой точкой хочет общаться хост. Номер точки скрывается под битовой маской USB_ISTR_EP_ID, а направление IN или OUT под битами USB_EP_CTR_TX и USB_EP_CTR_RX соответственно.

Поскольку точек у нас может быть много, и каждая со своим алгоритмом обработки, заведем им всем callback-функции, которые бы вызывались по соответствующим событиям. Например, послал хост данные в endpoint3, мы прочитали USB->ISTR, вытащили оттуда что запрос у нас OUT и что номер точки равен 3. Вот и вызываем epfunc_out[3](3). Номер точки в скобках передается если вдруг юзерский код захочет повесить один обработчик на несколько точек. Ах да, еще в стандарте USB принято входные точки IN помечать взведенным 7-м битом. То есть endpoint3 на выход будет иметь номер 0x03, а на вход 0x83. Причем это разные точки, их можно использовать одновременно, друг другу они не мешают. Ну почти: в stm32 у них настройка типа (bulk, interrupt, ...) общая и на прием, и на передачу. Так что та же 0x83-я точка IN будет соответствовать callback'у epfunc_in[3](3 | 0x80).

Тот же принцип используется и для ep0. Разница только в том, что ее обработка происходит внутри библиотеки, а не внутри юзерского кода. Но что делать если нужно обрабатывать специфичные запросы вроде какого-нибудь HID не лезть же ковырять код библиотеки? Для этого предусмотрены специальные callback'и usb_class_ep0_out и usb_class_ep0_in, которые вызываются в специальных местах и имеют специальный формат, рассказывать про который я буду ближе к концу.

Стоит упомянуть еще про один не очень очевидный момент, связанный с возникновением прерываний обработки пакетов. С OUT запросами все просто: данные пришли, вот они. А вот IN прерывание генерируется не тогда, когда хост послал IN запрос, а когда в буфере передачи пусто. То есть по принципу действия это прерывание аналогично прерыванию по опустошению буфера UART. Следовательно, когда мы хотим что-то передать хосту, мы это просто записываем данные в буфер передачи, ждем прерывания IN и дописываем что не поместилось (не забываем про ZLP). И ладно еще с обычными endpoint'ами, ими программист управляет, можно пока не обращать внимание. Но вот через ep0 обмен идет всегда. Поэтому и работа с ней должна быть встроена в библиотеку.

Как следствие, начало передачи осуществляется функцией ep0_send, которая записывает в глобальную переменную адрес начала буфера и объем данных для передачи, после чего, обратите внимание, сама в первый раз дергает обработчик события IN. В дальнейшем этот обработчик будет вызываться по аппаратным событиям, но толчок дать все же надо.

Ну а сам обработчик устроен довольно просто: записывает очередной кусок данных в буфер передачи, сдвигает адрес начала буфера и уменьшает количество оставшихся для передачи байтов. Отдельный костыль связан с тем самым ZLP и необходимостью на некоторые запросы отвечать пустым пакетом. В данном случае конец передачи обозначается тем, что адрес данных стал NULL. А пустой пакет что он равен константе ZLPP. И то и другое происходит при равенстве размера нулю, так что реальной записи не происходит.

Похожий алгоритм придется реализовывать и при работе с другими конечными точками. Но это уже забота пользователя. Да и логика их работы зачастую отличается от работы с ep0, так что в ряде случаев такой вариант окажется удобнее буферизации на уровне библиотеки.

Логика общения по USB


Сам факт подключения хост определяет по наличию подтягивающего резистора между какой-нибудь линией данных и питанием. Он производит сброс устройства, назначает ему адрес на шине и пытается определить что же именно в него воткнули. Для этого он читает дескрипторы устройства и конфигурации (а если надо, то и специфичные). Также он может прочитать строковые дескрипторы чтобы понять как устройство само себя называет (хотя если пара VID:PID ему знакома, предпочтет вытянуть строки из своей базы данных). После этого хост может подгрузить соответствующий драйвер и работать с устройством уже на понятном ему языке. В понятный ему язык входят специфичные запросы и обращения к конкретным интерфейсам и конечным точкам. До этого тоже дойдем, но сначала надо чтобы устройство хотя бы отобразилось в системе.

Обработка SETUP запросов: DeviceDescriptor


Человек, хоть немного ковырявший USB, уже давно должен был насторожиться: COKPOWEHEU, ты говоришь про запросы IN и OUT, но ведь в стандарте прописан еще и SETUP. Да, так и есть, но это скорее разновидность OUT запроса, специально структурированная и предназначенная исключительно для конечной точки 0. Об ее структуре и особенностях работы и поговорим. Сама структура выглядит следующим образом:
typedef struct{  uint8_t bmRequestType;  uint8_t bRequest;  uint16_t wValue;  uint16_t wIndex;  uint16_t wLength;}config_pack_t;


Поля этой структуры рассмотрены во множестве источников, но все же напомню.
bmRequestType битовая маска, биты в которой означают следующее:
7: направление передачи. 0 от хоста к устройству, 1 от устройства к хосту. Фактически, это тип следующей передачи, OUT или IN.
6-5: класс запроса
0x00 (USB_REQ_STANDARD) стандартный (обрабатывать пока будем только их)
0x20 (USB_REQ_CLASS) специфичные для класса (до них дойдем в следующих статьях)
0x40 (USB_REQ_VENDOR) специфичные для производителя (надеюсь, не придется их трогать)
4-0: собеседник
0x00 (USB_REQ_DEVICE) устройство в целом
0x01 (USB_REQ_INTERFACE) отдельный интерфейс
0x02 (USB_REQ_ENDPOINT) конечная точка

bRequest собственно запрос
wValue небольшое 16-битное поле данных. На случай простых запросов, чтобы не гонять полноценные пересылки.
wIndex номер получателя. Например, интерфейса, с которым хост хочет пообщаться.
wLength размер дополнительных данных, если 16 бит wValue недостаточно.

Первым делом при подключении устройства хост пытается узнать что же именно в него воткнули. Для этого он посылает запрос со следующими данными:
bmRequestType = 0x80 (запрос на чтение) + USB_REQ_STANDARD (стандартный) + USB_REQ_DEVICE (к устройству в целом)
bRequest = 0x06 (GET_DESCRIPTOR) запрос дескриптора
wValue = 0x0100 (DEVICE_DESCRIPTOR) дескриптор устройства в целом
wIndex = 0 не используется
wLength = 0 дополнительных данных нет
После чего шлет запрос IN, куда устройство должно положить ответ. Как мы помним, запрос IN от хоста и прерывание контроллера слабо связаны, так что записывать ответ будем сразу в буфер передатчика ep0. Теоретически, данные из этого, да и всех прочих, дескрипторов привязаны к конкретному устройству, поэтому помещать их в ядро библиотеки бессмысленно. Соответствующие запросы передаются функции usb_class_get_std_descr, которая возвращает ядру указатель на начало данных и их размер. Дело в том, что некоторые дескрипторы могут быть переменного размера. Но DEVICE_DESCRIPTOR к ним не относится. Его размер и структура стандартизованы и выглядят так:
uint8_t bLength; //размер дескриптораuint8_t bDescriptorType; //тип дескриптора. В данном случае USB_DESCR_DEVICE (0x01)uint16_t bcdUSB; //число 0x0110 для usb-1.1, либо 0x0200 для 2.0. Других значений я не встречалuint8_t bDeviceClass; //класс устройстваuint8_t bDeviceSubClass; //подклассuint8_t bDeviceProtocol; //протоколuint8_t bMaxPacketSize0; //размер ep0uint16_t idVendor; // VIDuint16_t idProduct; // PIDuint16_t bcdDevice_Ver; //версия в BCD-форматеuint8_t iManufacturer; //номер строки названия производителяuint8_t iProduct; //номер строки названия продуктаuint8_t iSerialNumber; //номер строки версииuint8_t bNumConfigurations; //количество конфигураций (почти всегда равно 1)

В первую очередь обратите внимание на первые два поля размер дескриптора и его тип. Они характерны почти для всех дескрипторов USB (кроме HID, пожалуй). Причем если bDescriptorType это константа, то bLength приходится чуть ли не считать вручную для каждого дескриптора. В какой-то момент мне это надоело и был написан макрос
#define ARRLEN1(ign, x...) (1+sizeof((uint8_t[]){x})), x

Он считает размер переданных ему аргументов и подставляет вместо первого. Дело в том, что иногда дескрипторы бывают вложенными, так что один, скажем, требует размер в первом байте, другой в 3 и 4 (16-битное число), а третий в 6 и 7 (снова 16-битное число). Точные значения аргументов макросам безразличны, но хотя бы количество совпадать должно. Собственно, макросы для подстановки в 1, в 3 и 4, а также в 6 и 7 байты там тоже есть, но их применение я покажу на более характерном примере.
Пока же рассмотрим 16-битные поля вроде VID и PID. Понятное дело что в одном массиве смешать 8-битные и 16-битные константы не выйдет, да плюс endiannes в общем, на выручку снова приходят макросы: USB_U16( x ).

В плане выбора VID:PID вопрос сложный. Если планируется выпускать продукцию серийно, все же стоит купить персональную пару. Для личного же пользования можно подобрать чужую от похожего устройства. Скажем, у меня в примерах будут пары от AVR LUFA и STM. Все равно хост определяет по этой паре скорее специфичные баги реализации, чем назначение. Потому что назначение устройства подробно расписывается в специальном дескрипторе.

Внимание, грабли! Как оказалось, Windows привязывает к этой паре драйвера, то есть вы, например, собрали устройство HID, показали системе и установили драйвера. А потом перепрошили устройство под MSD (флешку), не меняя VID:PID, то драйвера останутся старые и, естественно, работать устройство не будет. Придется лезть в управление оборудованием, удалять драйвера и заставлять систему найти новые. Я думаю, ни для кого не станет неожиданностью, что в Linux такой проблемы нет: устройства просто подключаются и работают.

StringDescriptor


Еще одной интересной особенностью дескрипторов USB является любовь к строкам. В шаблоне дескриптора они обозначаются префиксом i, как например iSerialNumber или iPhone. Эти строки входят во многие дескрипторы и, честно говоря, я не знаю, зачем их так много. Тем более что при подключении устройства видны будут только iManufacturer, iProduct и iSerialNumber. Как бы то ни было, строки представляют собой те же дескрипторы (то есть поля bLength и bDescriptorType в наличии), но вместо дальнейшей структуры идет поток 16-битных символов, похожих на юникод. Смысл данного извращения мне опять непонятен, ведь подобные названия даются все равно обычно на английском, где и 8-битного ASCII хватило бы. Ну хорошо, хотите расширенный набор символов, так UTF-8 бы взяли. Странные люди Для удобного формирования строк удобно применять угадайте что правильно, макросы. Но на этот раз не моей разработки, а подсмотренные у EddyEm. Поскольку строки являются дескрипторами, то и запрашивать их хост будет как обычные дескрипторы, только в поле wValue подставит 0x0300 (STRING_DESCRIPTOR). А вместо младшего байта будет собственно индекс строки. Скажем, запрос 0x0300 это строка с индексом 0 (она зарезервирована под язык устройства и почти всегда равна u"\x0409"), а запрос 0x0302 строка с индексом 2.

Внимание, грабли! Сколь бы ни был велик соблазн засунуть в iSerialNumber просто строку, даже строку с честной версией вида u''1.2.3'' не делайте этого! Некоторые операционные системы считают, что там должны быть только шестнадцатеричные цифры, то есть '0'-'9', 'A'-'Z' и все. Даже точек нельзя. Наверное, они как-то считают от этого числа хэш чтобы идентифицировать при повторном подключении, не знаю. Но проблему такую заметил при тестировании на виртуальной машине с Windows 7, она считала устройство бракованным. Что интересно, Windows XP и 10 проблему не заметили.

ConfigurationDescriptor


С точки зрения хоста устройство представляет набор отдельных интерфейсов, каждый из которых предназначен для решения какой-то задачи. В дескрипторе интерфейса описывается его устройство и привязанные конечные точки. Да, конечные точки описываются не сами по себе, а только как часть интерфейса. Обычно интерфейсы со сложной архитектурой управляются SETUP запросами (то есть через ep0), в которых поле wIndex номеру интерфейса и соответствует. Максимум позволяется прикарманить конечную точку для прерываний. А от интерфейсов данных хосту нужны только описания конечных точек и обмен будет идти через них.

Интерфейсов в одном устройстве может быть много, причем очень разных. Поэтому чтобы не путаться где заканчивается один интерфейс и начинается другой, в дескрипторе указывается не только размер заголовка, но и отдельно (обычно 3-4 байтами) полный размер интерфейса. Таким образом интерфейс складывается подобно матрешке: внутри общего контейнера (который хранит размер заголовка, bDescriptorType и полный размер содержимого, включая заголовок) может находиться еще парочка контейнеров поменьше, но устроенных точно так же. А внутри еще и еще. Приведу пример дескриптора примитивного HID-устройства:
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    0, // bNumEndpoints    HIDCLASS_HID, // bInterfaceClass:     HIDSUBCLASS_NONE, // bInterfaceSubClass:     HIDPROTOCOL_NONE, // bInterfaceProtocol:     0x00, // iInterface  )  ARRLEN1(    bLENGTH, //bLength    USB_DESCR_HID, //bDescriptorType    USB_U16(0x0101), //bcdHID    0, //bCountryCode    1, //bNumDescriptors    USB_DESCR_HID_REPORT, //bDescriptorType    USB_U16( sizeof(USB_HIDDescriptor) ), //wDescriptorLength  )  )};


Здесь уровень вложенности небольшой, плюс ни одной конечной точки не описано ну так я старался выбрать устройство попроще. Некоторое недоумение здесь могут вызвать константы bLENGTH и wTOTALLENGTH, равные восьми- и шестнадцатибитному нулям. Поскольку в данном случае для расчета размера используются макросы, было бы странно дублировать их работу и считать байты руками. Как и странно писать нули. А константы штука заметная, наглядности кода способствующая.

Как можно видеть, данный дескриптор состоит из заголовка USB_DESCR_CONFIG (хранящего полный размер содержимого включая себя!), интерфейса USB_DESCR_INTERFACE (описывающего подробности устройства) и USB_DESCR_HID, в общих чертах говорящего что же именно за HID мы изображаем. Причем именно что в общих чертах: конкретная структура HID описывается в специальном дескрипторе HID_REPORT_DESCRIPTOR, рассматривать который я здесь не буду, просто потому что слишком плохо его знаю. Так что ограничимся копипастом из какого-нибудь примера.

Вернемся к интерфейсам. Учитывая, что у них есть номера, логично предположить, что в одном устройстве интерфейсов может быть много. Причем они могут отвечать как за одну общую задачу (скажем, интерфейс управления USB-CDC и интерфейс данных), так и за принципиально несвязанные. Скажем, ничто не мешает нам (кроме отсутствия знаний пока) на одном контроллере реализовать два переходника USB-CDC плюс флешку плюс, скажем, клавиатуру. Очевидно, что интерфейс флешки знать не знает про COM-порт. Впрочем, тут есть свои подводные камни, которые, надеюсь, когда-нибудь рассмотрим. Еще стоит отметить, что один интерфейс может иметь несколько альтернативных конфигураций (bAlternateSetting), отличающихся, скажем, количеством конечных точек или частотой их опроса. Собственно, для того и сделано: если хост считает, что лучше пропускную способность поберечь, он может переключить интерфейс в какой-нибудь альтернативный режим, который ему больше понравился.

Обмен данными с HID


Вообще говоря, HID-устройства имитируют объекты реального мира, у которых есть не столько данные, сколько набор неких параметров, которые можно измерять или задавать (запросы SET_REPORT / GET_REPORT) и которые могут уведомлять хост о внезапном внешнем событии (INTERRUPT). Таким образом, собственно для обмена данными данные устройства не предназначены но кого это когда останавливало!

Прерываний мы пока касаться не будем, поскольку им нужна специальная конечная точка. А вот чтение и настройку параметров рассмотрим. В данном случае параметр всего один, представляющий собой структуру из двух байтов, по замыслу отвечающих за два светодиода, либо за кнопку и счетчик.

Начнем с более простого чтения по запросу HIDREQ_GET_REPORT. По сути это такой же запрос, как и всякие DEVICE_DESCRIPTOR, только специфичный именно для HID. Плюс этот запрос адресован не устройству в целом, а интерфейсу. То есть если мы реализовали в одном устройстве несколько независимых HID-устройств, их можно различить по полю wIndex запроса. Правда, именно для HID это не лучший подход: проще сам дескриптор сделать составным. В любом случае до таких извращений нам далеко, так что даже не будем анализировать что и куда хост пытался послать: на любой запрос к интерфейсу и с полем bRequest равным HIDREQ_GET_REPORT будем возвращать собственно данные. По идее, такой подход предназначен чтобы возвращать дескрипторы (со всеми bLength и bDescriptorType), но в случае HID разработчики решили все упростить и обмениваться только данными. Вот и возвращаем указатель на нашу структуру и ее размер. Ну и небольшая дополнительная логика вроде обработки кнопок и счетчика запросов.

Более сложный случай запрос на запись. Это первый раз, когда мы сталкиваемся с наличием дополнительных данных в SETUP запросе. То есть ядро нашей библиотеки должно сначала прочитать сам запрос, и только потом данные. И передать их юзерской функции. А буфера у нас, напоминаю, нет. В результате некоторой низкоуровневой магии был разработан следующий алгоритм. Callback вызывать будем всегда, но укажем ему с какого по счету байта данные сейчас лежат в буфере приема конечной точки (offset) а также размер этих данных (size). То есть при приеме самого запроса значения offset и size равны нулю (данных-то нет). При приеме первого пакета offset все еще равен нулю, а size размеру принятых данных. Для второго offset будет равен размеру ep0 (потому что если данные пришлось разбивать, делают это по размеру конечной точки), а size размеру принятых данных. И так далее. Важно! Если данные приняты, их надо считать. Это может сделать либо обработчик вызовом usb_ep_read() и возвратом 1 (мол я там сам считал, не утруждайся), либо просто вернув 0 (мне эти данные не нужны) без чтения тогда очисткой займется ядро библиотеки. По этому принципу и построена функция: она проверяет в наличии ли данные и если да, то читает их и зажигает светодиоды.

Софт для обмена данными


Тут я велосипед изобретать не стал, а взял готовую программу из предыдущей статьи.

Заключение


Вот, собственно, и все. Основы работы с USB при помощи аппаратного модуля в STM32 я рассказал, некоторый грабли тоже пощупал. Учитывая значительно меньший объем кода, чем тот ужас, что генерирует STMCube, разобраться в нем будет проще. Собственно говоря, в Cube'ической лапше я так и не разобрался, уж больно много там вызовов одного и того же в разных комбинациях. Гораздо лучше для понимания вариант от EddyEm, от которого я отталкивался. Конечно, и там не без косяков, но хотя бы пригодно для понимания. Также похвастаюсь, что размер моего варианта едва ли не в 5 раз меньше ST'шного (~2.7 кБ против 14) при том, что оптимизацией я не занимался и наверняка можно еще ужать.

Отдельно хочу отметить разницу поведения различных операционных систем при подключении сомнительного оборудования. Linux просто работает, даже если в дескрипторах ошибки. Windows XP, 7, 10 при малейших ошибках ругаются что устройство поломанное, я с ним работать отказываюсь. Причем XP иногда даже в BSOD падала от негодования. Ах да, еще они постоянно выводят устройство может работать быстрее, что с этим делать я не знаю. В общем, как бы хорош Linux не был для разработки, он прощает слишком многое, тестировать надо и на менее юзер-френдли системах.

Дальнейшие планы: рассмотреть остальные типы конечных точек (пока что был пример только с Control); рассмотреть другие контроллеры (скажем, у меня еще at90usb162 (AVR) и gd32vf103 (RISC_V) валяются), но это совсем далекие планы. Также хорошо бы поподробнее разобраться с отдельными USB-устройствами вроде тех же HID, но тоже не приоритетная задача.
Подробнее..

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

10.04.2021 12:12:31 | Автор: admin

Еще более низкий уровень (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 снова решат испортить сайт.
Подробнее..

USB на регистрах isochronous endpoint на примере Audio device

23.05.2021 14:09:40 | Автор: admin
image<картинка с платой и наушниками>
Еще более низкий уровень (avr-vusb): habr.com/ru/post/460815
USB на регистрах: STM32L1 / STM32F1
USB на регистрах: bulk endpoint на примере Mass Storage
USB на регистрах: interrupt endpoint на примере HID

Сегодня рассмотрим последний тип конечных точек, изохронный. Он предназначен для передачи данных, критичных к времени доставки, однако не гарантирует ее успешность. Самый классический пример аудиоустройства: колонки, микрофоны.

Как ни странно, этот тип конечной точки оказался самым мозговыносящим (и это после всего, что я успел повидать с stm'ками!). Тем не менее, сегодня мы сделаем аудиоустройство и заодно чуть-чуть допилим ядро библиотеки USB. Как обычно, исходные коды доступны:
github.com/COKPOWEHEU/usb/tree/main/4.Audio_L1
github.com/COKPOWEHEU/usb/tree/main/4.Audio_F1

Доработка ядра


Допиливать ядро нужно потому, что у STM изохронные точки могут быть только с двойной буферизацией, то есть, грубо говоря, нельзя сделать 0x01 изохронной, а 0x81 управляющей. То есть в дескрипторе USB это прописать, конечно, можно, но внутренности контроллера это не изменит, и реальный адрес точки просто будет отличаться от видимого снаружи. Что, естественно, повысит риск ошибок, поэтому в эту сторону извращаться не будем.

Стоит отметить, что двойная буферизация бывает не только у изохронных точек, но и у bulk, причем включается и работает она иначе. Если изохронные точки включают буферизацию автоматически, просто потому что не умеют по-другому, то для соответствующей настройки bulk приходится использовать специальный бит USB_EP_KIND, который нужно установить вместе с собственно настройкой типа точки.

Сама по себе буферизация означает, что если раньше одной точке соответствовал один буфер на передачу и один на прием, то теперь оба буфера будут работать либо на передачу, либо на прием, причем работать они будут только вместе. Как результат, настройка буферизованной точки сильно отличается от обычной, ведь нужно настроить не один буфер, а два. Поэтому не будем лепить лишние условия в обычную инициализацию, а создадим на ее основе отдельную функцию usb_ep_init_double().

Прием и передача пакетов отличается не так сильно, хотя и отняла гораздо больше времени сначала на попытки понять как же она должна работать по логике ST, потом на подгонку заклинания из интернета чтобы все-таки заработало. Как говорилось раньше, если для обычной точки два буфера независимы и отличаются направлением обмена, то для буферизованной они одинаковы и отличаются только смещением. Так что немножко изменим функции usb_ep_write и usb_ep_read чтобы они принимали не номер точки, а номер смещения. То есть если раньше эти функции предполагали существование восьми сдвоенных точек, то теперь 16 одинарных. Соответственно, номер новой полуточки на запись равен всего лишь номеру обычной, умноженному на два, а для usb_ep_read надо еще добавить единицу (см. распределение буферов в PMA). Собственно, это и делается инлайн-функциями usb_ep_write и usb_ep_read для обычных точек. А вот логику буферизованных рассмотрим чуть подробнее.

Согласно документации, один буфер такой точки доступен для железа, второй для софта. Потом они переключаются и снова не мешают друг другу. Для OUT точки флагом со стороны железа является бит USB_EP_DTOG_RX, который нужно прочитать чтобы понять в какой из буферов только что закончилась запись и откуда соответственно софт может читать. Когда он прочитал свой буфер, нужно дернуть бит USB_EP_DTOG_TX, что собственно переключит буферы. Не уверен, что подразумевалось именно это, но оно, по крайней мере, работает.

Симметричная ситуация должны была быть с IN точками. Но на практике оказалось, что и проверять, и дергать надо USB_EP_DTOG_RX. Почему не TX я так и не понял Спасибо пользователю kuzulis за ссылку на github.com/dmitrystu/libusb_stm32/edit/master/src/usbd_stm32f103_devfs.c

За счет инлайна функций особого оверхеда не добавилось, не считая инициализации. Но ее можно при желании выкинуть флагами линковщика. А можно и не выкидывать: не так много места занимает, да и вызывается только при инициализации. Это вам не HAL, где функции мало того что тяжелые, но и вызывают друг друга постоянно.

В результате конечные точки научились работать и в буферизованном режиме если не дышать на них слишком сильно.

Для пользователя разница невелика: вместо usb_ep_init использовать usb_ep_init_double, а вместо usb_ep_write и usb_ep_read соответственно usb_ep_write_double и usb_ep_read_double.

Устройство AudioDevice


А вот теперь, когда с техническими граблями разобрались, перейдем к самому интересному настройке аудиоустройства.

Согласно стандарту USB аудиоустройство представляет собой набор сущностей (entity), соединенных друг с другом в некую топологию, по которой и проходит аудиосигнал. Каждая сущность имеет свой уникальный номер (bTerminalID, он же UnitID), по которому к ней могут подключаться другие сущности или конечные точки, по нему же обращается хост, если хочет изменить какие-то параметры. И он же считается единственным выходом данной сущности. А вот входов может вообще не быть (если это входной терминал), а может быть и больше одного (bSourceID). Собственно записью в массив bSourceID номеров сущностей, от которых текущая получает аудиосигнал, мы и описываем всю топологию, которая в результате может получиться весьма резвесистой. Для примера приведу топологию покупной USB-звуковой карты (цифрами показаны bTerminalID / UnitID):

lsusb и его расшифровка
Bus 001 Device 014: ID 0d8c:013c C-Media Electronics, Inc. CM108 Audio Controller#Тут пока ничего интересногоDevice Descriptor:  bLength                18  bDescriptorType         1  bcdUSB               1.10  bDeviceClass            0   bDeviceSubClass         0   bDeviceProtocol         0   bMaxPacketSize0         8  idVendor           0x0d8c C-Media Electronics, Inc.  idProduct          0x013c CM108 Audio Controller  bcdDevice            1.00  iManufacturer           1   iProduct                2   iSerial                 0   bNumConfigurations      1  #интересное начинается тут  Configuration Descriptor:    bLength                 9    bDescriptorType         2    wTotalLength       0x00fd    bNumInterfaces          4  # общее количество интерфейсов    bConfigurationValue     1    iConfiguration          0     bmAttributes         0x80      (Bus Powered)    MaxPower              100mA    #интерфейс 0 - описание топологии    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        0      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      1 Control Device      bInterfaceProtocol      0       iInterface              0       AudioControl Interface Descriptor:        bLength                10        bDescriptorType        36        bDescriptorSubtype      1 (HEADER)        bcdADC               1.00        wTotalLength       0x0064        bInCollection           2  # ВАЖНО! количество интерфейсов данных (2)        baInterfaceNr(0)        1  #номер перовго из них        baInterfaceNr(1)        2  #номер второго ##### Топологоия ###### 1 InputTerminal (USB, на динамик)       AudioControl Interface Descriptor:        bLength                12        bDescriptorType        36        bDescriptorSubtype      2 (INPUT_TERMINAL)        bTerminalID             1  # Вот номер данной сущности        wTerminalType      0x0101 USB Streaming        bAssocTerminal          0        bNrChannels             2  # Здесь задается количество каналов        wChannelConfig     0x0003  # А здесь - их расположение в пространстве          Left Front (L)          Right Front (R)        iChannelNames           0         iTerminal               0         # 2 InputTerminal (микрофон)      AudioControl Interface Descriptor:        bLength                12        bDescriptorType        36        bDescriptorSubtype      2 (INPUT_TERMINAL)        bTerminalID             2        wTerminalType      0x0201 Microphone        bAssocTerminal          0        bNrChannels             1        wChannelConfig     0x0001          Left Front (L)        iChannelNames           0         iTerminal               0         # 6 OutputTerminal (динамик), вход соединен с сущностью 9      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      3 (OUTPUT_TERMINAL)        bTerminalID             6        wTerminalType      0x0301 Speaker        bAssocTerminal          0        bSourceID               9  # Номера входов указываются здесь        iTerminal               0         # 7 OutputTerminal (USB), вход соединен с сущностью 8      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      3 (OUTPUT_TERMINAL)        bTerminalID             7        wTerminalType      0x0101 USB Streaming        bAssocTerminal          0        bSourceID               8        iTerminal               0         # 8 Selector, входы соединены только с сущностью 10      AudioControl Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      5 (SELECTOR_UNIT)        bUnitID                 8        bNrInPins               1  # У сущностей с несколькими входами указывается их количество        baSourceID(0)          10  # а потом номера        iSelector               0         # 9 Feature, вход соединен с сущностью 15      AudioControl Interface Descriptor:        bLength                10        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                 9        bSourceID              15        bControlSize            1        bmaControls(0)       0x01          Mute Control        bmaControls(1)       0x02          Volume Control        bmaControls(2)       0x02          Volume Control        iFeature                0         # 10 Feature, вход соединен с сущностью 2      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                10        bSourceID               2        bControlSize            1        bmaControls(0)       0x43          Mute Control          Volume Control          Automatic Gain Control        bmaControls(1)       0x00        iFeature                0         # 13 Feature, вход соединен с сущностью 2      AudioControl Interface Descriptor:        bLength                 9        bDescriptorType        36        bDescriptorSubtype      6 (FEATURE_UNIT)        bUnitID                13        bSourceID               2        bControlSize            1        bmaControls(0)       0x03          Mute Control          Volume Control        bmaControls(1)       0x00        iFeature                0         # 15 Mixer, входы соединены с сущностями 1 и 13      AudioControl Interface Descriptor:        bLength                13        bDescriptorType        36        bDescriptorSubtype      4 (MIXER_UNIT)        bUnitID                15        bNrInPins               2  # Снова массив входов        baSourceID(0)           1  # и их номера        baSourceID(1)          13        bNrChannels             2        wChannelConfig     0x0003          Left Front (L)          Right Front (R)        iChannelNames           0         bmControls(0)        0x00        iMixer                  0 ##### конец топологии ###### Интерфейс 1 (основной) - заглушка без конечных точек    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        1      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       # Интерфейс 1 (альтернативный) - рабочий с одной конечной точкой    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        1      bAlternateSetting       1      bNumEndpoints           1      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       AudioStreaming Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      1 (AS_GENERAL)        bTerminalLink           1        bDelay                  1 frames        wFormatTag         0x0001 PCM      AudioStreaming Interface Descriptor:        bLength                14        bDescriptorType        36        bDescriptorSubtype      2 (FORMAT_TYPE)        bFormatType             1 (FORMAT_TYPE_I)        bNrChannels             2        bSubframeSize           2        bBitResolution         16        bSamFreqType            2 Discrete        tSamFreq[ 0]        48000        tSamFreq[ 1]        44100      Endpoint Descriptor:        bLength                 9        bDescriptorType         5        bEndpointAddress     0x01  EP 1 OUT        bmAttributes            9          Transfer Type            Isochronous          Synch Type               Adaptive          Usage Type               Data        wMaxPacketSize     0x00c8  1x 200 bytes        bInterval               1        bRefresh                0        bSynchAddress           0        AudioStreaming Endpoint Descriptor:          bLength                 7          bDescriptorType        37          bDescriptorSubtype      1 (EP_GENERAL)          bmAttributes         0x01            Sampling Frequency          bLockDelayUnits         1 Milliseconds          wLockDelay         0x0001          # Интерфейс 2 (основной) - заглушка    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        2      bAlternateSetting       0      bNumEndpoints           0      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       # Интерфейс 2 (альтернативный)    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        2      bAlternateSetting       1      bNumEndpoints           1      bInterfaceClass         1 Audio      bInterfaceSubClass      2 Streaming      bInterfaceProtocol      0       iInterface              0       AudioStreaming Interface Descriptor:        bLength                 7        bDescriptorType        36        bDescriptorSubtype      1 (AS_GENERAL)        bTerminalLink           7        bDelay                  1 frames        wFormatTag         0x0001 PCM      AudioStreaming Interface Descriptor:        bLength                14        bDescriptorType        36        bDescriptorSubtype      2 (FORMAT_TYPE)        bFormatType             1 (FORMAT_TYPE_I)        bNrChannels             1        bSubframeSize           2        bBitResolution         16        bSamFreqType            2 Discrete        tSamFreq[ 0]        48000        tSamFreq[ 1]        44100      Endpoint Descriptor:        bLength                 9        bDescriptorType         5        bEndpointAddress     0x82  EP 2 IN        bmAttributes            9          Transfer Type            Isochronous          Synch Type               Adaptive          Usage Type               Data        wMaxPacketSize     0x0064  1x 100 bytes        bInterval               1        bRefresh                0        bSynchAddress           0        AudioStreaming Endpoint Descriptor:          bLength                 7          bDescriptorType        37          bDescriptorSubtype      1 (EP_GENERAL)          bmAttributes         0x01            Sampling Frequency          bLockDelayUnits         0 Undefined          wLockDelay         0x0000##### Конец описания аудиоинтерфейсов ###### Интерфейс 3 "Клавиши громкости и всего остального" (не интересно)    Interface Descriptor:      bLength                 9      bDescriptorType         4      bInterfaceNumber        3      bAlternateSetting       0      bNumEndpoints           1      bInterfaceClass         3 Human Interface Device      bInterfaceSubClass      0       bInterfaceProtocol      0       iInterface              0         HID Device Descriptor:          bLength                 9          bDescriptorType        33          bcdHID               1.00          bCountryCode            0 Not supported          bNumDescriptors         1          bDescriptorType        34 Report          wDescriptorLength      60         Report Descriptors:            ** UNAVAILABLE **      Endpoint Descriptor:        bLength                 7        bDescriptorType         5        bEndpointAddress     0x87  EP 7 IN        bmAttributes            3          Transfer Type            Interrupt          Synch Type               None          Usage Type               Data        wMaxPacketSize     0x0004  1x 4 bytes        bInterval               2



image

Мы же будем делать нечто более простое (заготовку брал отсюда):

image

Здесь видно две независимых ветки распространения сигнала: либо от USB через фичу к динамику, либо из микрофона через другую фичу к USB. Микрофон и динамик не просто так взяты в кавычки: на моей отладочной плате их нет, поэтому вместо собственно звука будем пользоваться кнопками и светодиодами. Впрочем, ничего нового. Фичи в моем случае ничего не делают и добавлены скорее для красоты.

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

Глубоко в различия сортов фич и прочих сущностей я не копал, но пересказать кусок документации не побрезгую.

1. Входной терминал (Input Terminal)
Как следует из названия, именно через него в аудиоустройство попадает звуковой сигнал. Это может быть USB, может быть микрофон обыкновенный, микрофон гарнитурный, даже микрофонный массив.

2. Выходной терминал (Output Terminal)
Тоже вполне очевидно то, через что звук покидает наше устройство. Это может быть все тот же USB, может быть динамик, гарнитура, динамик в мониторе, динамики различных частот и куча других устройств.

3. Микшер (Mixer Unit)
Берет несколько входных сигналов, усиливает каждый на заданную величину и складывает то, что получилось, в выходной канал. При желании можно задать усиление в ноль раз, что сведет его к следующей сущности.

4. Селектор (Selector Unit)
Берет несколько входных сигналов и перенаправляет один из них на выход.

5. Фильтр (Feature Unit)
Берет единственный входной сигнал, меняет параметры звука (громкость, тембр и т.п.) и выдает на выход. Естественно, все эти параметры одинаковым способом прикладываются ко всему сигналу, без взаимодействия логических каналов внутри него

6. Processing Unit
А вот эта штука уже позволяет проводить манипуляции над отдельными логическими каналами внутри каждого входного. Более того, позволяет сделать количество логических каналов в выходном не равным количеству во входных.

7. Extension Unit
Весь набор нестандартных сущностей, чтобы больной фантазии производителей оборудования было раздолье. Соответственно, и поведение, и настройки будут зависеть от этой самой фантазии.

Некоторые сущности обладают параметрами вроде коэффициента усиления или номера канала, на которые хост может повлиять при помощи setFeature / getFeature запросов по номеру сущности. Но тут, если честно, я не слишком понимаю как это вообще проверить. Наверное, нужен какой-то спецсофт, которого у меня нет. Ну да и ладно, все равно я в это полез только чтобы все типы точек проверить на свою голову

Грабли в дескрипторе


В отличие от предыдущих USB-устройств, здесь дескриптор сложный, многоуровневый и склонный пугать виндоусы до BSOD'а. Как мы видели выше, топология у аутиоустройства может быть весьма сложной и развесистой. Под ее описание выделяется целый интерфейс. Очевидно, endpoint'ов он содержать не будет, зато будет содержать список дескрипторов сущностей и описаний к чему подключены их входы. Тут особо описывать смысла не вижу, проще посмотреть в коде и документации. Отмечу только главные грабли: здесь описывается какие интерфейсы с соответствующими конечными точками относятся именно к данному устройству. Скажем, если вы захотите изменить мою конфигурацию и убрать оттуда динамик, придется не просто удалить половину сущностей (слава макросам, хотя бы с подсчетом длины дескриптора проблемы не будет), но и уменьшить поле bInCollection до 1, после чего из следующего за ним массива bInterfaceNr убрать номер лишнего интерфейса.

Дальше находятся интерфейсы, отвечающие за обмен данными. В моем случае 1-й интерфейс отвечает за микрофон, а 2-й за динамик. Здесь стоит обратить внимание в первую очередь на два варианта каждого из этих интерфейсов. Один с bAlternateSetting равным 0, второй с 1. Они отличаются наличием конечной точки. То есть если наше устройство в данный момент не используется, хост просто переключается на тот альтернативный интерфейс, который конечной точкой не оборудован, и уже не тратит на нее пропускную способность шины.

Вторая особенность интерфейсов данных это формат аудиосигнала. В соответствующем дескрипторе задается тип кодирования, количество каналов, разрешение и частота дискретизации (которая задается 24-битным числом). Вариантов кодирования предусмотрено довольно много, но мы будем использовать самый простой PCM. По сути это просто последовательность значений мгновенной величины сигнала без какого-либо кодирования, причем величина считается целым числом со знаком. Разрешение сигнала задается в двух местах (зачем непонятно): в поле bSubFrameSize указывается количество байтов, а в bBitResolution количество битов. Вероятно, можно указать, что диапазон нашей звуковой карты не доходит до полного диапазона типа данных, скажем, int16_t и составляет всего 10 бит.

Ну и наконец дескриптор собственно конечной точки. Он тоже немного отличается от обычных, поскольку предусматривает во-первых несколько вариантов синхронизации, а во-вторых, номер сущности, с которой эта точка ассоциирована (bTerminalLink). Варианты синхронизации прописываются старшими битами прямо в тип конечной точки (именно поэтому изохронная точка вынесена в ветку default в функции инициализации), но их подробностями я не занимался, поэтому ничего интересного рассказать не смогу. Вместо синхронизации мы воспользуемся обычным таймером контроллера, который будет генерировать прерывания с примерно нужной частотой.

Ах да, чуть не забыл упомянуть очередную пачку BSOD'ов при тестировании неправильных дескрипторов. Еще раз напоминаю: количество интерфейсов данных должно соответствовать числу bInCollection, а их номера следующему за ним массиву!
Скрытый текст
Как представлю отладку подобного кода под виндами, с этими постоянными вылетами, да еще без нормальной консоли. бр-р-р.


Логика работы устройства


Как я уже говорил, для тестов не имеет смысла городить на отладочную плату навесные компоненты, поэтому все тестирование будет осуществляться тем, что уже установлено кнопки да светодиоды. Впрочем, в данном случае это проблемы не составляет: микрофон может просто генерировать синусоиду частотой, скажем, 1 кГц, а динамик включать светодиод при превышении порогового значения звука (скажем, выше числа 10000: при указанных 16 битах разрешения, что соответствует диапазону -32768 +32767, это примерно треть).

А вот с тестированием возникла небольшая проблема: я не нашел простого способа перенаправить сигнал с микрофона на stdin какой-нибудь программы. Вроде бы раньше это делалось просто чтением /dev/dsp, но сейчас что-то поломалось. Впрочем, ничего критичного, ведь есть всякие библиотеки взаимодействия с мультимедией SDL, SFLM и другие. Собственно на SFML я и написал простенькую утилиту для чтения с микрофона и, если надо, визуализации сигнала.

Особое внимание уделю ограничениям нашего аудиоустройства: насколько я понял, изохронный запрос IN отправляется один раз в миллисекунду (а вот OUT'ов может быть много), что ограничивает частоту дискретизации. Допустим, размер конечной точки у нас 64 байта (учитывая буферизацию, в памяти она занимает 128 байт, но хост об этом не знает), разрешение 16 бит, то есть за раз можно отправить 32 отсчета. Учитывая интервал в 1 мс получаем теоретический предел 32 кГц для одного канала. Самый простой способ это обойти увеличить размер конечной точки. Но тут надо помнить, что размер общего буфера PMA у нас всего 512 байт. Минус таблица распределения точек, минус ep0, получаем максимум 440 байт, то есть 220 байт на единственную точку с учетом буферизации. И это теоретический предел.

Но то, что хост может за один фрейм послать несколько OUT запросов наводит на мысли, что и устройство тоже так может. Осталось понять как. Возможно, это решается грамотной настройкой синхронизации. Но для меня этот вопрос интереса уже не представляет: изохронные точки работают, буферизованные точки работают, аудиоустройство работает задача выполнена.

Заключение (общее для цикла)


Ну вот мы и познакомились с устройством USB в контроллерах STM32F103 и STM32L151 (и других с аналогичной реализацией), поудивлялись логике некоторых архитектурных решений (особенно меня впечатлил регистр USB_EPnR, впрочем двойная буферизация тоже не отстает), рассмотрели все типы конечных точек и проверили их, построив соответствующие устройства. Так что можно сказать, что данный цикл статей подошел к логическому заключению. Хотя это, конечно, не значит, что я заброшу контроллеры или USB: в отдаленных планах еще разобраться с составными устройствами (пока что выглядит несложно, но ведь и изохронные точки тоже проблем не предвещали) и USB на контроллерах других семейств.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru