Usb composite device что это
Составное устройство USB на STM32. Часть 4: Два-в-одном
В заключительной части публикации о составном устройстве USB я расскажу о том, как заставил заработать составное устройство USB, а также поделюсь некоторыми неочевидными нюансами этого процесса.
Работа составных частей устройства была описана во второй и третьей частях публикации.
Ответы на вопрос, зачем это всё было затеяно, даются в начале первой части и в конце четвёртой.
Ссылки на предыдущие части публикации:
Составное устройство USB на STM32. Часть 1: Предпосылки
Составное устройство USB на STM32. Часть 2: USB Audio Speaker
Составное устройство USB на STM32. Часть 3: Звуковое устройство отдельно, виртуальный СОМ-порт отдельно
Исходные коды публикуемой реализации составного устройства USB, состоящего из виртуального COM-порта и дуплексной звуковой карты находятся здесь: http://github.com/dmitrii-rudnev/selenite-habr
Создаём Composite Device Class
Файлы драйвера составного устройства usbd_comp.c и usbd_comp.h расположены в папках Core/Scr и Core/Inc соответственно.
Структура класса составного устройства аналогична структуре класса звукового устройства и содержит подобный набор функций-обработчиков событий.
Основная функция драйвера составного устройства заключается в том, чтобы определить, драйвер какого устройства нужно подключить для обработки события. При обработке запросов (Requests) это определяется по номеру интерфейса в случае Standard Requests или атрибутам запроса в случае Class-Specific Requests. При обработке пакетов данных переключение производится, как правило, по номеру конечной точки (EP).
Подробно Standard Requests описаны на стр.248 – 260 документа:
[5] Universal Serial Bus Specification, Revision 2.0, April 27, 2000
Запросы Communication Device Class-Specific Requests подробно описаны на стр.18 – 30 документа [4], а Audio Device Class-Specific Requests, соответственно, на стр.74 – 85 документа [3].
Читаем дескриптор
Дескриптор описанного в публикации составного устройства USB состоит из девяти байтов раздела Configuration Descriptor, восьми байтов раздела Interface Association Descriptor (IAD) для двух интерфейсов виртуального COM-порта, 58 байтов дескриптора виртуального COM-порта, восьми байтов раздела IAD для трёх интерфейсов звукового устройства и 183 байтов дескриптора звукового устройства USB.
Виртуальный COM-порт использует интерфейсы 0 и 1, а также конечные точки 1 и 2. Дуплексное звуковое устройство использует интерфейсы 2, 3 и 4, а также конечную точку 3.
Разбираем работу устройства
Рассмотрим доработанный файл usb_device.c, расположенный в папке USB_DEVICE/App:
Сначала создаётся переменная hUsbDeviceFS. Тип USBD_HandleTypeDef объявлен в usbd_def.h.
Функция MX_USB_DEVICE_Init вызывается из main.c.
Вызовом функции USBD_Init задаются начальные значения переменной hUsbDeviceFS.
Затем вызовом функций HAL_PCDEx_SetTxFiFo производится настройка буфера USB для каждой конечной точки составного устройства.
Неочевидный нюанс 1: по умолчанию настройка буфера USB производится при исполнении функции USBD_LL_Init, размещённой в файле usbd_conf.c. В теле этой функции области, помеченной как USER CODE, нет. Т.е. при каждой генерации кода STM32CubeMX будет удалять настройки буфера для конечных точек 2 и 3. Именно поэтому окончательная настройка буфера USB производится уже после того, как функция USBD_LL_Init отработала.
Вызовом функции USBD_RegisterClass в hUsbDeviceFS.pClass размещается указатель на созданную в usbd_comp.c переменную USBD_COMP, содержащую указатели на обработчики событий, относящихся к классу устройства. Тип USBD_ClassTypeDef объявлен в usbd_def.h.
Вызовом функции USBD_RegisterInterface в hUsbDeviceFS.pUserData размещается указатель на созданную в usbd_comp.h пустую переменную USBD_COMP_fops_FS.
В дальнейшем обработчики событий составного устройства USB будут вызывать обработчики событий нужного устройства, входящего в составное, а также подключать нужный интерфейс связи с оконечными устройствами.
Вызовом функции USBD_Start производится запуск устройства USB.
Неочевидный нюанс 2: составное устройство будет упорно определяться как виртуальный COM-порт, если не поменять значения трёх байтов в стандартном дескрипторе устройства USB (USB standard device descriptor), размещённом в файле usbd_desc.c, причём при каждой генерации кода STM32CubeMX эти изменения будет удалять:
Неочевидный нюанс 3: виртуальный COM-порт в данном решении работает корректно только в случае, когда номер используемой им конечной точки меньше, чем номер конечной точки звукового устройства.
Неочевидный нюанс 4: виртуальный COM-порт в данном решении работает корректно только в случае, когда при инициализации в его буфер прописываются параметры порта (см. USBD_COMP_Init). Без этой записи программы терминалов к COM-порту могут и не подключиться.
Проверка работоспособности драйвера составного устройства USB
Неочевидный нюанс 5: при проверке работоспособности «эхо» через COM-порт возвращается, когда составное устройство уже «переключено на COM-порт». В реальном применении устройства передача может начаться, когда подключено звуковое устройство. Чтобы избежать подобной ситуации, перед началом передачи производится вызов функции COMP_CDC_Transmit_FS для подключения драйвера виртуального COM-порта:
Выводы
Автору удалось реализовать составное устройство USB, состоящее из виртуального COM-порта и дуплексной звуковой карты, на ресурсах платы NUCLEO-F446ZE.
Решение оформлено в виде проекта в среде разработки STM32CubeIDE. После генерации кода STM32CubeMX для восстановления работоспособности решения необходимо вручную изменить значения трёх байтов в стандартном дескрипторе устройства USB (USB standard device descriptor), размещённом в файле usbd_desc.c.
От автора
Данный цикл публикаций подводит черту, фиксирует результат проекта, которой мне удалось достичь в одиночку.
Хочу поблагодарить своих читателей за доброжелательность и тёплый приём. Я никогда не был и никогда уже не буду профессиональным разработчиком ПО для микроконтроллеров. И это моя первая публикация про разработку программного обеспечения.
Благодарю Георгия (RX9CIM) за моральную поддержку при запуске проекта.
Отдельная благодарность romanetz_omsk, без которого я бы забросил проект ещё два года назад.
По логике дальнейшего развития MVP нужно приступать к написанию DSP, а это уже достаточно сложная для меня математика. Как это осилить в одиночку, ума не приложу…
Два в одном: USB хост и составное USB устройство
Не так давно, была опубликована статья «Пастильда — открытый аппаратный менеджер паролей». Так как данный проект является открытым, то мы решили, что будет интересно, если мы будем писать небольшие заметки о процессе проектирования, о задачах, которые перед нами стоят и о трудностях, с которыми мы сталкиваемся.
Реализация USB хоста
Итак, во-первых мне нужно было реализовать на устройстве USB хост, чтобы оно могло распознавать и общаться с подключенной к нему клавиатурой. Так как в работе я использую связку Eclipse + GNU ARM Eclipse + libopencm3, то очень хотелось найти уже что-то готовое и желательно написанное с использованием библиотеки libopencm3. Желание мое было очень жирным, до последнего момента не верила, что мои поиски увенчаются успехом. Однако под конец рабочего дня, проскролив интернет до самого дна, я вдруг наткнулась вот на это. libusbhost? Серьезно? И это был не просто написанный на основе libopencm3 usb хост, он еще и был написан под STM32F4, под тот самый, который мы решили использовать в проекте. В общем, звезды сошлись и радости моей не было предела. Кстати, оказалось, что этот проект создавался как часть libopencm3, однако его так и не добавили в библиотеку.
Как библиотеку, libusbhost я не собирала, просто взяла необходимые мне исходники, написала драйвер для клавиатуры и, в общем-то все, погнали! Но обо всем по-порядку.
По аналогии с usbh_driver_hid_mouse.[ch], я написала драйвер для клавиатуры (usbh_driver_hid_kbd.[ch]).
Далее был реализован простенький класс, для работы с хостом:
Реализация составного USB устройства
Далее мне нужно было сделать так, чтобы наше устройство отображалось в диспетчере устройств и как клавиатура, и как дисковый накопитель. Тут вся магия в дескрипторах=) В этом документе, в главе 9, подробно описан USB Device Framework. Эту главу нужно очень внимательно прочитать и в соответствии с ней описать дескрипторы устройства. В моем случае получилось следующее:
Для работы с составным устройством был написан класс USB_composite, представленный ниже.
Как правило, функции control_request и set_config должны быть явно описаны для каждого устройства. Однако из этого правила есть исключение: Mass Storage Device. Итак, разберемся с конструктором класса USB_Composite.
Во-первых, мы инициализируем ноги USB OTG FS:
Во-вторых, нам нужно проинициализировать наше составное устройство, зарегистрировать USB_set_config_callback, о котором шла речь выше, и разрешить прерывание:
Так вот. Теперь, когда конструктор класса USB_Composite дописан, можно собрать проект, прошить устройство и увидеть, что «Запоминающее устройство для USB» больше не помечено предупреждением, а во вкладке «Дисковые устройства» можно обнаружить «ThirdPin Pastilda USB Device». И, казалось бы, все хорошо. Но нет=) Проблем стало больше:
1. Зайти на диск невозможно. При попытке сделать это все виснет, умирает, компьютеру очень плохо.
2. Распознавание устройства как дискового занимает более 2-х минут.
Об этих проблемах и о том, как их решить без вреда для здоровья написано здесь: USB mass storage device и libopencm3.
И, о, чудо! Никаких пятен=) Теперь все работает. У нас есть USB хост и составное USB устройство. Осталось только объединить их работу.
Объединение хоста и составного устройства
, Пастильда должна перехватить управление и отправить сообщение в ПК как клавиатура, после чего мы возвращаемся в режим трансляции и снова ожидаем комбинацию.
Код, реализующий все это, простой как палка:
, мы будем попадать в однострочное меню, а во флеше будет храниться наша зашифрованная база данных паролей.
Буду рада любым комментариям и пожеланиям.
И, конечно же, ссылка на github.
Устройство USB Composite является устаревшим устройством USB и может не работать с USB 3.0
USB 3.0 стал крупным прорывом на рынке передачи данных. Отмеченный голубыми USB-портами, USB 3.0 предоставил пользователям скорость до 10 раз больше USB 2.0 (5 Гбит/с), благодаря чему большие медиа-файлы и программы можно было передавать за считанные минуты.
USB 3.0 поддерживает обратную совместимость, что означает, что все устройства, работающие на USB 2.0, должны хорошо работать с USB 3.0. Однако многие пользователи сообщают о следующей ошибке при попытке подключить устройства к слотам USB 3.0 – USB Composite Device является устаревшим устройством USB и может не работать в USB 3.0.
USB Composite Device – старое USB-устройство
Эта проблема более заметна с принтерами. Причиной обычно являются несовместимые драйверы. Попробуйте изменить порт USB, и если это не сработает, воспользуйтесь следующими решениями для решения проблемы:
1] Запуск оборудования и устранения неполадок USB
Средство устранения неполадок оборудования и устройств проверяет параметры, связанные с оборудованием, и, если возможно, корректирует их.
Чтобы использовать это средство устранения неполадок, нажмите кнопку «Пуск» и выберите «Настройки»> «Обновления и безопасность» >> «Устранение неполадок».
Выберите средство устранения неполадок оборудования и устройств и запустите его. Перезагрузите систему, как только вы закончите.
2] Обновите или переустановите драйверы USB
Чтобы обновить драйвер устройства. нажмите Win + R, чтобы открыть окно «Выполнить» и введите команду devmgmt.msc. Нажмите Enter, чтобы открыть диспетчер устройств.
Найдите драйверы Универсальный контроллер последовательной шины и раскройте список. Щелкните правой кнопкой мыши и выберите «Обновить драйвер» для каждого драйвера USB.
3] Удалите и переустановите драйверы принтера.
Поскольку вы не можете подключить принтер к компьютеру, новые драйверы необходимо будет загрузить с веб-сайта производителя. Установите их в систему и проверьте, нормально ли работает принтер после этого.
Это решение остается тем же для других аппаратных устройств, которые показывают ту же ошибку при попытке подключения к компьютеру.
USB Composite Device — старое USB-устройство
Если вы продолжаете получать приведенное выше сообщение об ошибке при попытке подключить USB-устройство к вашему компьютеру, вот реальные решения вашей проблемы. Эта статья поможет вам быстро и легко решить проблему!
Попробуйте эти исправления
Вот 3 исправления, которые помогли многим другим пользователям решить проблему с USB Composite Device. Вам не нужно пробовать их все; просто пройдите вниз по списку, пока не найдете тот, который подходит вам.
Исправление 1: Обновите драйвер USB Composite Device
Наиболее вероятной причиной ошибки USB Composite Device является проблема с драйвером устройства.
К счастью, это одна из самых простых проблем.
Есть два способа обновить драйвер USB Composite Device: вручную а также автоматически.
Обновите драйвер USB Composite Device вручную — Вы можете обновить драйвер вручную, перейдя на веб-сайт производителя оборудования и выполнив поиск последней версии драйвера для вашего USB Composite Device. Если вы выберете такой подход, обязательно выберите драйвер, совместимый с точным номером модели вашего оборудования и вашей версией Windows.
Обновите драйвер USB Composite Device автоматически — Если у вас нет времени, терпения или навыков работы с компьютером, чтобы обновить драйвер вручную, вместо этого вы можете сделать это автоматически с Водитель Легко. Вам не нужно точно знать, на какой системе работает ваш компьютер, вам не нужно беспокоиться из-за неправильного драйвера, который вы загружаете, и вам не нужно беспокоиться об ошибке при установке. Driver Easy справится со всем этим.
Исправление 2: переустановите драйверы контроллера USB
Вы также можете попробовать удалить и переустановить драйверы USB через диспетчер устройств. Это действие позволит Windows автоматически сканировать аппаратные изменения и переустанавливать необходимые драйверы. Вот что вам нужно сделать:
ключ
нажмите одновременно, чтобы открыть окно «Выполнить».
Исправление 3: использование средства устранения неполадок Windows USB
Если описанные выше действия не помогли вам, попробуйте Windows Устранение неполадок USB.
Чтобы использовать его, во-первых, вам нужно скачать инструмент с эта ссылка.
Затем вы можете открыть инструмент и следовать инструкциям на экране для устранения проблемы.
Если у вас есть какие-либо вопросы или предложения, пожалуйста, не стесняйтесь оставлять комментарии ниже.
CDC+MSC USB Composite Device на STM32 HAL
Мне хотелось бы верить, что хотя бы половина читателей может расшифровать хотя бы половину названия статьи 🙂 Кто не в курсе — поясню. Мое устройство должно реализовывать сразу две USB функции:
Я буду описывать создание композитного USB устройства на базе микроконтроллера STM32, но сам подход будет также применим и для других микроконтроллеров. В статье я детально разберу каждый из классов по отдельности, так и принцип построения композитных устройств. Но обо все по порядку.
Немного теории
Интерфейс USB очень сложный, многоуровневый и многогранный. С наскоку его не осилить. В одной из статей (забыл, правда, в какой) видел фразу в стиле “прочитайте эту статью 2 раза, а потом на утро еще раз”. Да, он такой, с первого раза точно не осилишь. Лично у меня интерфейс более-менее разложился по полочкам только через пару месяцев активного копания и чтения спецификаций.
Я по прежнему не являюсь экспертом в USB, а потому рекомендовал бы обратиться к статьям, которые бы детальнее рассказали суть происходящего. Я лишь укажу на самые важные места и вкратце поясню как оно работает — по большей части во что вляпался сам. В первую очередь я бы рекомендовал Usb in a nutshell (перевод), а также USB Made Simple (сам не читал, но многие рекомендуют). Также нам понадобятся спецификации для конкретных классов USB устройств.
Наверное, самой главной штукой в интерфейсе USB является дескриптор. Точнее даже пакет дескрипторов. Когда устройство подключается к шине хост запрашивает дескрипторы устройства, которые описывают возможности устройства, скорости обмена, частоту опроса, какие интерфейсы реализовывает устройство и много чего другого. Дескриптор штука важная и весьма нежная — даже ошибка в одном байте приведет к тому, что устройство работать не будет.
Устройство описывает себя с помощью нескольких дескрипторов разного типа:
Еще нужно понимать, что USB это хост ориентированный протокол. Настройка устройства, прием, передача — все в USB управляется со стороны хоста. Для нас это означает, что со стороны микроконтроллера нет никакого потока управления — вся работа с USB построена на прерываниях и обратных вызовах (callback). А это, в свою очередь, означает что нам не желательно запускать долгоиграющие операции и нужно быть очень аккуратными при взаимодействии с другими прерываниями (учитывать приоритет, и все такое прочее). Впрочем, попробуем не опускаться на такой низкий уровень.
Также хост-ориентированность проявляется еще и в названии функций. В терминологии USB направление от хоста к устройству называется OUT, хотя для контроллера это прием. И наоборот, направление от устройства к хосту называется IN, хотя для нас это означает отправку данных. Так что в микроконтроллере функция DataOut() на самом деле принимает данные, а DataIn() — отправляет. Но это так, к слову — мы будем пользоваться уже готовым кодом.
CDC — виртуальный COM порт
Наверное взять и сразу наваять композитное устройство целиком не выйдет — слишком много нюансов и подводных камней. Я думаю лучше будет сначала отладить каждый интерфейс в отдельности, а потом переходить к композитному устройству. Начну с CDC, т.к. он не требует никаких зависимостей.
Я недавно переехал на STM32 Cube — пакет низкоуровневых драйверов для STM32. В нем есть код по управлению USB с реализацией отдельных классов USB устройств. Возьмем шаблонные реализации USB Core и CDC и начнем пилить под себя. Заготовки лежат в директории \Middlewares\ST\STM32_USB_Device_Library. Я использую Cube для контроллеров серии STM32F1, версия Cube — 1.6 (Апрель 2017), версия библиотеки USB из комплекта — 2.4.2 (декабрь 2015)
Шаблонная реализация библиотеки подразумевает написание собственного кода в файлах с названием template. Без понимания всей библиотеки и принципов работы USB это сделать достаточно сложно. Но мы пойдем проще — сгенерируем эти файлы с помощью графического конфигуратора CubeMX.
Реализация предоставленная CubeMX готова к работе прямо из коробки. Аж даже немного обидно, что не пришлось писать никакого кода. Придется изучать CDC на примере полностью готовой реализации. Давайте взглянем на самые интересные места в сгенерированном коде.
Для начала заглянем в дескрипторы, которые находятся в файлах usbd_desc.c (дескриптор устройства) и usbd_cdc.c (дескрипторы конфигурации, интерфейсов, конечных точек). В статье usb in a nutshell (на русском) есть очень детальное описание всех дескрипторов. Не буду описывать каждое поле в отдельности, остановлюсь лишь на самых важных и интересных полях.
Тут нас интересуют такие поля:
Тут нам интересно следующее:
В этом интерфейсе живет только одна конечная точка (bNumEndpoints). Но прежде идет серия функциональных дескрипторов — настроек специфичных для данного класса устройств.
Тут сказано, что наше устройство не знает о понятии “звонок” (в смысле звонок по телефону), но при этом понимает команды параметров линии (скорость, стоп биты, DTR/CTS биты). Последний дескриптор описывает какой из двух интерфейсов CDC является управляющим, а где бегают данные. В общем, тут нам ничего не интересно и менять мы ничего не будем.
Тут сказано, что эта конечная точка используется для прерываний. Хост будет опрашивать устройство раз в 0x10 (16) мс с вопросом а не требует ли устройство внимания. Также через эту конечную точку будут ходить управляющие команды.
Описание второго интерфейса (там где данные бегают) будет попроще
В интерфейсе живут 2 конечные точки типа bulk — одна на прием, вторая на передачу. На самом деле в терминологии USB это одна конечная точка, просто двухсторонняя.
Как это все работает объяснять не буду, хотя бы потому что сам до конца не понимаю (например как хост узнает сколько данных нужно забирать со стороны устройства). Самое главное, что библиотека все реализует за нас. Давайте лучше посмотрим на архитектуру.
Библиотека USB от ST весьма слоиста. Я бы выделил такие архитектурные уровни
Эта функция инициализирует USB периферию микроконтроллера. Интереснее всего тут серия вызовов функции HAL_PCDEx_PMAConfig(). Дело в том, что на борту микроконтроллера находится цельных 512 байт памяти отведенных специально под буферы USB (эта память называется PMA — Packet Memory Area). Но поскольку заранее устройству неизвестно сколько будет конечных точек и какие будут их параметры, то эта память не распределена. Поэтому перед работой с USB память нужно распределить согласно выбранным параметрам.
Но вот что странно, объявляли только 2 конечные точки, а вызовов 5. Откуда взялись лишние? На самом деле лишних тут нет. Дело в том, что у каждого USB устройства обязательно должна быть одна двусторонняя конечная точка, через которую устройство инициализируется, а потом управляется. Эта конечная точка всегда имеет номер 0. Этой функции инициализируются не конечные точки, а буфера. Для нулевой конечной точки создаются 2 буфера — 0x00 на прием и 0x80 на передачу (старший бит указывает направление передачи, младшие — номер конечной точки). Оставшиеся 3 вызова описывают буфера для конечной точки 1 (прием и передача данных) и конечной точки 2 (прием команд и отсылка статуса — это происходит синхронно, поэтому буфер один)
Последний параметр в каждом вызове указывает смещение буфера конечной точки в общем буфере. На форумах видел вопросы «а что это за магическая константа 0x18 (начальный адрес первого буфера)?». Я детально рассмотрю этот вопрос позже. Сейчас лишь скажу, что первые 0x18 байт PMA памяти занимает таблица распределения буферов.
Но это все кишки и другие внутренности. А что снаружи?
Пользовательский код оперирует функциями приема и передачи, которые находятся в файле usbd_cdc_if.c. Чтобы устройство могло отправлять данные в виртуальный COM порт в сторону хоста нам предоставили функцию CDC_Transmit_FS()
С приемом чуть сложнее: ядро USB будет дергать функцию CDC_Receive_FS() по мере приема данных. В эту функцию нужно дописать свой код, который будет обрабатывать принятые данные. Или вызывать коллбек, который будет заниматься обработкой, например так:
Обращаю внимание, что эти функции работают с массивами байт без какой либо структуры. В моем случае мне нужно было отправлять строки. Чтобы это было делать удобно я написал аналог функции printf, которая форматировала строку и отправляла ее в порт. Чтобы повысить скорость я также озадачился двойной буферизацией. Подробнее тут в разделах “USB с двойной буферизацией” и “printf”.
Еще в этом же файле находятся функции инициализации/деинициализации виртуального COM порта, а также функция изменения параметров порта (скорость, четность, стоп биты и прочее). Реализация по умолчанию не ограничивает себя в скорости и это меня устраивает. Инициализация так же хороша. Оставим все как есть.
Финальный штрих — код, который это все запускает
Тут по очереди инициализируются разные уровни драйвера. Последняя команда включает USB прерывания. Важно понимать, что вся работа с USB происходит по запросу от хоста. В этом случае внутри драйвера вызывается прерывание, которое в свою очередь либо само обрабатывает запрос, либо делегирует это другому коду через коллбек.
Чтобы это все заработало нужен драйвер со стороны операционной системы. Как правило это стандартный драйвер и система может подхватить устройство без особой процедуры инсталляции. Насколько я понимаю у меня в системе уже был установлен Virtual COM Port драйвер от STM (поставился с ST Flash Utility) и мое устройство подхватилось самостоятельно. На линуксе также все завелось с полпинка.
MSC — запоминающее устройство
С драйвером CDC было все просто — устройство, как правило, само является конечным потребителем данных (например получает от хоста команды) или же генератором (например отправляет хосту показания датчиков).
С Mass Storage Class будет чуток сложнее. Драйвер MSC является всего лишь прослойкой между хостом и шиной USB с одной стороны, и запоминающим устройством с другой. Это может быть SD карта подключенная по SDIO, SPI Flash, может быть RAM Drive, дисковый накопитель, а может быть даже сетевой диск. В общем, в большинстве случаев запоминающее устройство будет представлено неким драйвером (как правило нетривиальным), который нам нужно будет состыковать с реализацией MSC.
В моем устройстве используется SD карта, подключенная через SPI. Для доступа к файлом на этой карте я использую библиотеку SdFat. Она также разделена на несколько уровней абстракции:
Реализация MSC требует от хранилища определенного интерфейса — уметь читать и писать, отдавать свой размер и статус. Примерно такие же возможности предоставляет интерфейс драйвера SD карты библиотеки SdFat. Остается лишь написать адаптер, который приведет один интерфейс к другому.
С направлением движения определились. Займемся реализацией. Я опять воспользовался конфигуратором CubeMX и сгенерировал нужные файлы для компонента USB. Изучение начнем, конечно же, с дескрипторов.
Дескриптор устройства практически не изменился. Разница только в полях, определяющих класс устройства — теперь класс устройства в целом не задан (нули в bDeviceClass), а будет задаваться на уровне интерфейса (это требование спецификации ).
Очень похоже на аналогичный дескриптор из CDC — определяется количество интерфейсов (1) и параметры питания от шины (до 100 мА)
Дескриптор интерфейса объявляет 2 конечных точки (по одной в каждую сторону передачи). Также дескриптор определяет какой именно это подкласс Mass Storage — Bulk Only Transport. Я не нашел толкового описания что же именно это за подкласс такой. Предполагаю, что это устройство, которое общается только посредством двусторонней передачи данных через 2 конечные точки (тогда как другие модели могут использовать еще и прерывания). Протоколом в этом общении являются SCSI команды.
Тут определяются 2 конечные точки типа Bulk — интерфейс USB не гарантирует скорость по таким конечным точкам, зато гарантирует доставку данных. Размер пакета устанавливается в 64 байта.
Раз уж мы говорим про конечные точки, то стоит заглянуть в файл usbd_conf.c где определяются соответствующие PMA буфера
Теперь посмотрим на MSC с другой стороны. Этот USB класс принимает от хоста команды на чтение/запись и транслирует их специализированный интерфейс — USBD_StorageTypeDef. Нам остается только подставить свою реализацию.
Поскольку это C, а не C++, то каждая их этих записей — указатель на соответствующую функцию. Как я уже говорил, нам нужно написать адаптер, который будет приводить интерфейс MSC к интерфейсу SD карты.
Начнем реализовывать интерфейс. Первой идет функция инициализации
Так SD карту можно было бы инициализировать прямо отсюда, если бы это была быстрая операция. Но в случае SD карты это может быть не всегда так. К тому же не стоит забывать, что эти все функции являются коллбеками и вызываются из прерывания USB, а прерывания надолго блокировать не стОит. Поэтому я вызвают функцию initSD() прямо из main() перед инициализацией USB, а SD_MSC_Init() у меня ничего не делает
Может показаться, что слишком много разных драйверов, но позвольте я напомню архитектуру. Класс SdSpiCard из библиотеки SdFat знает как общаться с SD картой через SPI, когда и какую команду послать и какой ждать ответ. Но он не знает как работать с самим SPI. Для этих целей я написал класс SdFatSPIDriver, который реализует общение с картой по SPI и передачу данных через DMA.
Реализация SD_MSC_GetCapacity() тривиальна — SdSpiCard умеет возвращать размер карты сразу в блоках
Чтение и запись также реализована вполне просто.
Карта у нас всегда готова (хотя в будущем я буду пристальнее смотреть на статус) и не защищена от записи.
LUN — Logic Unit Number. Теоретически наше запоминающее устройство может состоять из нескольких носителей (например жесткие диски в рейде). Все функции SCSI протокола указывают с каким носителем оно хочет работать. Функция GetMaxLun возвращает номер последнего устройства (количество устройств минус 1). Флешка у нас одна потому возвращаем 0.
Если честно, я особо не разобрался зачем оно нужно. Заглянув в спецификацию SCSI я увидел очень много полей смысла, которых я не понял. Из того, что я осилил – тут описывается стандартное устройство с прямым (не секвентальным) доступом, причем которое может быть извлечено (removable). Благо во всех примерах, которые я видел этот массив совпадает, так что пускай будет. Отлажено ведь.
Теперь все это нужно правильно проинициализировать
Подключаем, проверяем. Все работает, правда очень медленно — подключенный диск открывается секунд 50. Отчасти это из-за того, что линейная скорость чтения флешки через такой интерфейс получается около 200кб/с. Когда USB Mass Storage устройство подключается к компьютеру, операционная система вычитывает таблицу FAT. Я использую флешку на 8 гиг, а там FAT аж 7.5 мегабайт. Плюс чтение MBR, бут сектора, таблицы файлов — вот и получается почти 50 сек.
Также мне пришлось отключить DMA при работе с SD картой – там не все так просто с его включением. Дело в том, что моя реализация драйвера (как оказалось) не может работать из прерывания, а в USB все только через прерывания и работает. Не работает даже банальный HAL_Delay() т.к. он тоже завязан на прерывания, не говоря уже о синхронизации с использованием FreeRTOS. Это нужно будет переделать, но это отдельная история и к USB composite device она не относится. Как переделаю — обязательно напишу об этом статью и оставлю тут линку.
UPDATE: как и обещал вот линка. Удалось прокачать скорость до 650кб/с
CDC + MSC Composite Device
А теперь со всей этой фигней мы попробуем взлететь (С) анекдот
Итак, мы уже знаем как строить USB устройства, которые могут реализовывать либо CDC либо MSC. Попробуем сделать композитное устройство, которое реализует оба интерфейса одновременно. Я посмотрел несколько других проектов, которые реализовывали композитное USB устройство и, как мне кажется, их подход имеет смысл. А именно: реализовать собственный драйвер класса, который будет реализовывать и ту и ту функциональность.
Структура USB устройства будет такая:
Для удобства использования объявим в коде номера конечных точек и интерфейсов.
Нумерация конечных точек повторяет нумерацию интерфейсов. Будем использовать №1 для MSC, №2 для управления CDC, №3 для передачи данных через CDC. Есть еще нулевая конечная точка для общего управления устройством, но она обрабатывается в недрах ядра USB и объявлять эти номера не обязательно.
Интерфейс USB библиотеки от ST оставляет желать лучшего. В некоторых случаях номера конечных точек используются с флагом направления передачи — установленный старший бит означает направление IN — в сторону хоста (я для этого завел константу IN_EP_DIR). При этом другие функции используют просто номер конечной точки. В отличии от оригинального дизайна я предпочел разделить эти все номера и использовать правильные константы в нужных местах. Там где используются константы с суффиксом EP_IDX флаг направления передачи не используется.
ВАЖНО! Хоть по спецификации USB номера конечных точек могут быть какими угодно, все же лучше расположить их последовательно и в том же порядке, в котором они объявляются в дескрипторах. Мне это знание далось неделей жесткого дебага, когда виндовый USB драйвер упорно ломился не в ту конечную точку и ничего не работало.
Начнем как обычно с дескрипторов. Большая часть дескрипторов будут жить в нашей реализации класса (usbd_msc_cdc.c), но дескриптор устройства и кое какие глобальные штуки определены в ядре USB в файле usbd_desc.c
В целом тут все тоже самое, отличаются только поля, которые определяют класс устройства (bDeviceClass). Теперь эти поля указывают, что это композитное устройство. Хосту нужно будет потрудится, разобраться во всех остальных дескрипторах и подгрузить правильные драйвера для каждого из компонентов. Поле bDeviceProtocol означает, что части композитного устройства будут описываться специальным дескриптором – дескриптором ассоциации интерфейсов (Interface Association Descriptor). О нем чуть ниже.
Дескриптор конфигурации примерно такой же как и раньше, разница только в количестве интерфейсов. Теперь у нас их 3
Далее идет объявление интерфейса и конечных точек для MSC. Не знаю почему именно в таком порядке (сначала MSC потом CDC). Так было в одном из примеров, которые я нашел, оттуда и скопировал. По идее порядок интерфейсов не имеет значения. Главное, чтобы они возили все свои дополнительные дескрипторы рядом. Ну и приколы с нумерацией конечных точек также имеют значение.
Дескрипторы MSC ничем не отличаются от тех, что были в предыдущем разделе.
А вот дальше идет новый тип дескриптора — IAD (Interface Association Descriptor) – дескриптор ассоциации интерфейсов. Ассоциация тут не в смысле организации, а в смысле какой интерфейс с какой функцией ассоциировать.
Этот хитрый дескриптор говорит хосту что описание предыдущей функции USB устройства (MSC) закончилось и сейчас будет совсем другая функция. Причем тут же указано какая именно — CDC. Также указано количество связанных с ней интерфейсов и индекс первого из них.
IAD дескриптор не нужен для MSC, т.к. там всего один интерфейс. Но IAD нужен для CDC чтобы сгруппировать 2 интерфейса в одну функцию. Об этом сказано в спецификации этого дескриптора
Наконец дескрипторы CDC. Они полностью соответствуют дескрипторам для одиночной CDC функции с точностью до номеров интерфейсов и конечных точек
Когда все дескрипторы готовы можно посчитать суммарный размер конфигурации.
Перейдем к написанию кода. Ядро USB общается с драйверами классов используя вот такой интерфейс
В зависимости от состояния или события на шине USB ядро вызывает соответствующую функцию.
Любую архитектурную проблему можно решить введением дополнительного абстрактного слоя… (С) еще один анекдот
Разумеется мы не будем реализовывать весь функционал целиком — за реализацию классов CDC и MSC будет отвечать существующий код. Мы лишь напишем прослойку, которая будет перенаправлять вызовы либо в одну, либо в другую реализацию.
Тут все просто: инициализируем (деинициализируем) оба класса. Вызываемые функции сами займутся созданием/удалением своих конечных точек.
Пожалуй самой сложной функцией будет Setup.
Это коллбек на один из стандартных запросов по шине USB, но этот запрос очень многогранный. Это может быть как получение данных (get), так и установка (Set). Это может быть запрос к устройству в целом, к одному из его интерфейсов или конечных точек. Также тут может приплыть как стандартный запрос, определенный базовой спецификацией USB, так и специфичный для определенного устройства или класса. Подробнее тут (Раздел “Пакет Setup”).
Из-за обилия разных случаев структура обработчика пакета Setup весьма сложна. Тут не получается написать один if или switch. В коде ядра USB обработка размазана по 3-4 большим функциям и в определенных случаях передается отдельному специализированному обработчику (коих там еще с десяток). Радует только то, что на уровень драйвера класса передается только незначительная часть запросов.
Я подсмотрел какие пакеты ходят через эту функцию и, похоже, можно ориентироваться по получателю. Если получатель пакета интерфейс — в поле wIndex будет номер интерфейса, если конечная точка, то в wIndex будет номер конечной точки. Исходя из этого перенаправляем запросы в соответствующий обработчик.
Кстати, чтобы это работало нужно не забыть поменять дефайн, определяющий количество интерфейсов, а то запрос просто не дойдет и срежется внутри ядра USB
Коллбеками DataIn и DataOut все проще. Там есть номер конечной точки — по ней и определим куда запрос перенаправлять
Обратите внимание, что флаг направления передачи в номере конечной точки не используется. Т.е. даже если некоторые функции используют MSC_IN_EP (0x81), то в этой функции нужно использовать MSC_EP_IDX (0x01).
Иногда данные приходят в нулевую конечную точку и для этого есть специальный коллбек. Я не знаю что бы я делал, если бы оба класса (и CDC и MSC) имели обработчики на этот случай – в таком запросе не указан интерфейс или номер конечной точки. Было бы невозможно понять кому адресован запрос. Благо такой запрос умеет обрабатывать только класс CDC – вот ему и отправим
Больше у нас не будет нетривиальных обработчиков. Есть еще парочка геттеров для дескрипторов, но их код стандартный и не представляет интереса. Заполним «таблицу виртуальных функций»
Теперь код инициализации
Инициализируем USB ядро, устанавливаем ему наш драйвер класса и настраиваем вторичные интерфейсы. Все? Нет не все. В таком виде оно не запустится.
Дело вот в чем. Каждый класс имеет некоторое количество приватных данных – состояние драйвера, какие то переменные, которые должны быть доступны в разных функциях драйвера. Причем это не могут быть просто глобальные переменные – они привязаны к конкретному USB устройству (иначе невозможно было бы оперировать сразу с несколькими устройствами, если такое необходимо). Поэтому в хендле USB завели сразу несколько полей для такого случая
Проблема в том, что каждый класс считает эти поля своей собственностью и цепляет туда свою структуру.
Решать это можно несколькими способами. Товарищи отсюда вообще затолкали в свою реализацию класса весь код из обоих драйверов (CDC и MSC) чтобы на ходу разбираться что к чему. Другой подход в том, что в эти поля класть структуры, в которых есть место для данных обоих классов. Тут частично использован этот подход, вдобавок еще часть данных перенесена в глобальные переменные (что ок, если у нас только один USB порт)
Мы, пожалуй, пойдем путем попроще. Если драйверы классов хотят эксклюзивных полей – дадим им эти поля
Во-первых, я дал каждому классу свои поля – пусть терзают их как хотят. Во-вторых, я назвал эти поля согласно тому что в них реально лежит – никакая там не UserData, а указатель на интерфейс.
Конечно же на плюсах это было бы красивее и элегантнее (при том же расходе памяти и проца). но и на C можно сделать по человечески. Раз уж я запустил свои ручонки в структуру хендла, то и поменял непонятные void * на человеческие типы (кстати, поле void * pData теперь оно по человечески называется pPCDHandle с соответствующим типом). И const тоже расставил где надо. Пришлось, правда, повозиться с forward declarations.
Про организацию проекта. В некоторых IDE проект может быть построен следующим образом. Библиотека USB и исходники драйверов классов поставляются вместе с STM32 Cube, но часть файлов предлагается написать пользователю. Может так случится, что библиотека лежит где нибудь в общей локации и используется несколькими проектами. Стоит понимать, что я сейчас мы меняем код библиотеки USB и потому лучше иметь собственную копию, чтобы никому не мешать.
Конечно же переименования полей должны отразится в коде драйвера. Но тут как раз все просто – контекстная замена решает проблему.
Тут главное не переборщить. Я вот менял руками, просматривая каждое использование. Там я нашел «баг» в коде, зачинил его а потом 3 дня дебажился в попытке понять почему оно не работает.
Вот тут было все правильно – проверяем pClassData, а обращаемся к pClass. Если «починить» (проверять pClass), то работать не будет. Т.е. pClassData является своеобразным маркером того, что класс проинициализирован.
Возвращаясь к нашему драйверу. Поскольку Init() инициализирует обе переменные pClassDataXXX, то в этом коде можно проверять любую.
UPDATE: Важный нюанс, подмеченный пользователем fronders.
В оригинальных реализациях классов (CDC, HID, MSC, и почти всех остальных) в функциях инициализации (например USBD_CDC_Init() ) буфер для поля pClassDataXXX выделяется с помощью USBD_malloc(), который в шаблонной реализации является дефайном на malloc(). Вроде как ничего особенного — выделили кусок и используем его адрес.
Но, в некоторых проектах (в т.ч. в примерах от самой STMicroelectronics) решили сэкономить на памяти и написали свою реализацию аллокатора
В принципе такой подход работать будет, но только пока у нас только один класс устройства. Как только несколько классов попробуют «выделить» память через такой аллокатор — все сломается, т.к. несколько классов будут терзать один и тот же буфер.
На самом деле именно выделять память нужно будет только в случае если вы строите устройство с несколькими одинаковыми функциями — например устройство которое реализует два и более CDC. Ну может быть это еще понадобится в некоторых экзотических случаях, когда интерфейсы создаются и удаляются на лету. Во всех остальных случаях (коих подавляющее большинство) я бы не заморачивался с выделением памяти и распределил буфер статически. У себя в проекте я сделал так (заодно и типы данных побелил-покрасил):
Финальный штрих – распределение PMA буферов
Для наших конечных точек потребуется 7 буферов — 2 на нулевую конечную точку (точку управления), 2 на MSC и 3 на CDC. Но самое интересное тут — начальные адреса (последний параметр). По непонятной причине этот нюанс тщательно обходится всеми туториалами. В даташите написано про распределение буферов в PMA и как это выглядит на на уровне регистров, но вот как пользоваться соответствующими функциями из HAL информации нет. Восполним этот пробел.
Итак. У контроллера есть специальная память — PMA (Packet Memory Area). Это такая память куда программа может записать данные, а USB периферия их прочитать (и наоборот). Память эта заранее не распределена, т.к. разные конечные точки могут быть настроены на разный размер пакета. Поэтому существует таблица BTABLE в которой указано где какой буфер размещается. Причем сама эта таблица также размещается в PMA. Таблицу можно двигать и размещать в любом месте PMA, но HAL умеет ее размещать только в самом начале.
Картинка из Reference Manual микроконтроллеров серии STM32F103
Итак, как же высчитать смещения буферов? Размер таблицы напрямую зависит от количества используемых конечных точек. Каждая конечная точка в таблице представлена записью из 4 16-битных значений (по 2 на прием и 2 на передачу, даже если одно из направлений не используется). У нас используется 4 конечных точки — нулевая, MSC и две для CDC (не путайте с количеством буферов — у нас их 7 — по два на конечную точку, но одна точка однонаправленная, поэтому у нее только один буфер). Значит размер таблицы будет 4 точки * 4 записи * 2 байта = 32 байта.
Как я уже сказал HAL умеет располагать только вначале PMA области. Значит первый буфер мы можем расположить только по смещению 0x20 (32 байта — размер таблицы). Буферы для конечных точек можно размещать где угодно в PMA памяти, лишь бы они не налазили друг на друга. Каждая конечная точка определяет максимальный размер пакета, который она готова обрабатывать, буфер должен быть равен или больше этого размера.
Я расположил буфера с шагом 64 байта (максимальный рекомендуемый размер буфера для устройств USB Full Speed), но для некоторых конечных точек можно было бы и меньше. Так по управляющей CDC конечной точке много данных не бегает (CDC_CMD_PACKET_SIZE равно 8 байт), поэтому и буфер можно делать всего на 8 байт. Впрочем, мне было не жалко и 32 байт — просто чтобы круглые цифры получались.
Пора компилировать и запускать. Моя винда сразу определила само устройство, увидела также и 2 составляющие. Это хорошая новость. Но есть и плохая. Если Mass Storage устройство определилось сразу, то CDC — нет.
Не беда — нужно просто подсунуть винде правильный драйвер. Вообще-то устройство стандартное и специальный драйвер не нужен. Достаточно просто связать это устройство со стандартным драйвером (в нашем случае это будет usbser.sys)
На самом деле я в этой кухне не очень разбираюсь. По идее нужно скачать STMicroelectronics Virtual COM Port драйвер с сайта ST. Драйвер устанавливается в C:\Program Files (x86)\STMicroelectronics\Software\Virtual comport driver, а внутри есть файлик stmcdc.inf — вот он то нам и нужен. В этом файле в двух секциях есть строка вида
Вот она то и связывает наш VID/PID c драйвером устройства. Только этого мало — нужно еще указать номер интерфейса, который управляет CDC. В моем случае это первый интерфейс (нулевой отвечает за MSC). Для этого строка должна выглядеть так
На самом деле оригинальную строку можно не менять, а просто
добавлять строки в соответствующие секции.
После всех приготовлений находим нерабочее устройство в списке устройств, просим обновить драйвер, указываем директорию где лежит inf файл и вуаля — драйвер установлен. Винда сама присвоит этому устройство имя COMxx — можно брать любимую терминалку и открывать этот COM порт.
С линуксом все проще — там все заводится без танцев с бубном драйверами.
UPDATE от fronders: в Windows10 также все заводится самостоятельно. Более того, сами ST не рекомендуют для 10ки свой vcp драйвер а предлагают использовать стандартный.
Заключение
На некоторых форумах видел сообщения вроде “как все в этом USB сложно, какие-то драйверы… Я щас лучше на регистрах нафигачу”. Ребят, не все так просто. Уровень регистров это, наверное, самая простая часть. Но помимо нее есть огромный пласт логики, которую должно реализовывать устройство. И вот тут уже без знаний протоколов и многих сотен страниц спецификаций никак.
Но не все так плохо. Люди уже позаботились и написали всю логику. В большинстве случаев остается только подставить нужные значения и подправить некоторые параметры. Да, библиотека от ST — тот еще монстр. Но после вдумчивого прочтения USB In A Nutshell, парочки спецификаций конкретного класса устройств и работы со сниффером многие вещи становятся на свои места. Библиотека начинает выглядеть более-менее стройно. Можно даже сравнительно небольшими усилиями сделать кастомный драйвер класса, что мы с успехом и сделали.
Я делал реализацию композитного CDC+MSC устройства, но примерно такой же подход можно применить и для других комбинаций — CDC+HID, MSC+Audio, CDC+MSC+HID и других. Моя реализация предназначена для работы на микроконтроллерах серии STM32F103, но сам принцип может быть адаптирован и для других микроконтроллеров (в т.ч. и не STM32).
В этой статье я не ставил себе задачу рассказать как работает USB во всех деталях — во-первых есть статьи и книги, которые рассказывают это лучше (я затронул лишь малую часть), а во-вторых очень много вещей лучше черпать из первоисточников (спецификаций).
Вместо пересказа спецификаций я попробовал описать как работает реализация USB стека от ST. Также я постарался обратить внимание на особые моменты и рассказать почему делается именно так.
Я долго сомневался ставить ли галочку “Tutorial”. С одной стороны я даю рекомендации и пошаговые инструкции, обращаю внимание на особые моменты и даю ссылки на первоисточники. С другой стороны я не могу предоставить готовую библиотеку для скачивания и встраивания в свои проекты.
Дело в том, что в процессе работы над своим проектом я хорошенько поработал напильником, лобзиком и другими инструментами над этой библиотекой. Я выкинул много кода, который в моем устройстве не нужен, часть поменял, починил некоторые вещи, которые мне не нравились. Теперь библиотека USB весьма серьезно отличается от той, что выложена на сайте ST. Некоторые из изменений специфичны для моего проекта и могут не подойти для других ситуаций. Впрочем, добро пожаловать в мой репозиторий — изучайте, копируйте к себе, задавайте вопросы, предлагайте улучшения.
Напоследок хочу высказать благодарность всем тем, кто мне так или иначе помогал с моей реализацией. Спасибо, ребята!