genesis usb mass storage device что это

USB mass storage device и libopencm3

genesis usb mass storage device что это

Моя работа связана с программированием микроконтроллеров, в частности STM32. Долгое время для работы с периферией я использовала STM32 Standard Peripheral Library, так как она предоставляется производителем и, соответственно, является наиболее полной. Однако работать с ней крайне неудобно: инициализирующие структуры зачастую избыточны, в функциях черт ногу сломит, в общем, очень скоро появляется непреодолимое желание слезть с этой библиотеки и перейти на что-нибудь более аккуратное, грамотно спроектированное и написанное «чистым кодом».

После долгих поисков была обнаружена open source библиотека libopencm3, которая отвечала всем требованиям. Отзывы о ней были положительные и работать с ней оказалось максимально приятно.

Одной из последних задач на работе было поднять USB MSD. Для решения задачи использовалась отладочная плата STM32F4-discovery и вот этот пример. Пример не завелся. Проблем было две:
1. Было невозможно зайти на диск и прочитать находящийся там файл.
2. Распознавание устройства как дискового занимало более 2-х минут.

Все это было связано с наличием нескольких багов в файле usb_msc.c. Таким образом, в данной статье я расскажу о том, как исправить эти ошибки и продолжать с удовольствием пользоваться библиотекой libopencm3.

Решение проблемы №1:
Суть ошибки в том, что когда устройство получает запрос на запись, оно правильно его обрабатывает, однако не посылает обратно статус обработки запроса CSW (Command Status Wrapper). Таким образом, usb хост (в нашем случае, это наш ПК) уходит в бесконечное ожидание ответа на запрос, все виснет, глючит, умирает до тех пор, пока не отсоединишь устройство=)

* Более подробно ознакомиться с Mass Storage Bulk-Only or CBI Transport Specification можно здесь.

Поэтому находим функцию msc_data_rx_cb в файле usb_msc.c и приводим ее к следующему виду:

Ура, теперь мы можем зайти на диск прочитать файл!

Решение проблемы №2:
Суть этой проблемы в том, что во все том же файле usb_msc.с не реализованы две SCSI команды. Это было выявлено с помощью очень полезной программы usblyser, которая позволяет в удобном виде просматривать обмен посылками между usb устройством и usb хостом.

Итак, во-первых, хост не получает ответа на команду READ_FORMAT_CAPACITIES. Следовательно, добавляем в файл usb_msc.с функцию scsi_read_format_capacities и приводим функцию scsi_command к следующему виду:

Во-вторых, хост не получает ответа на команду INQUIRY (SERIAL NUMBER). Для исправления данной ошибки необходимо создать массив _spc3_inquiry_sn_response и привести функцию scsi_inquiry к следующему виду:

* Более подробно ознакомиться сo SCSI командами можно опять-таки здесь.

После всех этих хирургических вмешательств библиотеку нужно пересобрать, предварительно выполнив команду make clean.

В скором времени я планирую сделать pull request в репозиторий с libopencm3, однако остается только предполагать, как скоро владельцы внесут эти изменения в библиотеку, а работать тем временем нам всем нужно здесь и сейчас.

Очень надеюсь, что хоть кого-нибудь эта статья избавит от лишней головной боли и будет полезной.

Источник

genesis usb mass storage device что этоalt_mob_reviews

alt.mobile.reviews

По поводу режима USB Mass Storage, неизвестно куда «исчезнувшего» из Android Ice Cream Sandwich, в интернете нынче бушует много страстей. Высказались все, и знающие, но редко высказывающися, и незнающие, зато всегда имеющие мнение. Страсти бушуют настолько нешуточные, что мне даже пришлось «вернуться» с блоггерской пенсии, зайти в ЖЖ и написать несколько строк.

Начнем издалека. С кофигурацией хард драйвов при сборке домашнего компьютера сталкивались все гики. Кто-то покупает один большой и разбивает его на несколько разделов, кто-то покупает небольшой SSD для установки на него системы и большой внутренний SATA для хранения файлов, кто-то докупает еще и внешний USB для хранения/переноса фильмов, фотографий и музыки. Надо прикидывать, какого размера покупать диски, какой скорости, с каким интерфейсом подключения, какой фирмой произведенные, и т.п. Знакомая ситуация? Так вот с аналогичной ситуацией сталкиваются и производители телефонов при проектировании и производстве очередной модели.

Что нужно хранить на телефоне? Да практически то же самое:
1. Системные файлы (ОС)
2. Установленные программы и их файлы
3. Файлы пользователя

Продолжая аналогию microSD с USB внешним хардом, скажем, что процесс подключения его к другому устройсву (компьютеру, медиа плееру, и т.п.) требовал его полного отключения от вашего смартфона. Нельзя же USB хард воткнуть сразу в два устройства. Вот и при включении USB Mass Storage Mode контроллер чтения/записи SD карты полностью переключался на USB. В результате телефон доступа к SD карте больше не имел, а контроллер ее выступал в роли USB ридера. Как и в случае с любым ридером, компьютер работал с SD картой напрямую. То есть если бы она была отформатирована под файловую систему, неизвестную ОС, стоящей на компьютере, ОС вам просто сказала бы, что целостность SD карты нарушена, и ее надо бы отформатировать. Так работает любой ридер, он не понимает файловой структуры на накопителе, он просто дает доступ к «блокам» (кластерам) на нем, остальное должна «понять» ОС.

В качестве замены нам предлагается протокол MTP (Media Transfer Protocol), при использовании которого ваш компьютер видит телефон как MP3 плеер. В Windows такой протокол понимает Windows Media Player (да и сам Windows понимает, начиная с Vista), позволяя переливать на телефон медиа контент из вашей медиатеки (из того же Media Player). Решение менее демократичное, чем USB Mass Storage, но более демократичное, чем тот же iTunes. Решение спорное и не всем понравится, но давайте учтем следующее:

— Galaxy Nexus, хоть и является флагманским устройством для Android Ice Cream Sandwich, создан для демонстрации не всех возможностей ОС, а лишь их части. И ни в коем случае возможности ОС не ограничены возможностями данного телефона.

— Если вы настоящий гик, вы всегда найдете способ перебросить любые (не только медиа) файлы на телефон. Подсказка: adb push file /sdcard/

P.S. Любителям конспирологических теорий: а как вам идея, что Гугл избрала такую конфигурацию, чтобы избежать использования VFAT (обязательный при использовании SD card), опасаясь судебного иска от Майкрософта на основе патента 17-летней давности на использование длинных имен файлов в файловой системе FAT?

Рекомендованные материалы к прочтению:
— Отличная заметка от genesis usb mass storage device что этоcd_riper на эту тему: http://cdriper.blogspot.com/2011/11/android-ics-usb-mass-storage.html
— Перевод комментариев Дэна Моррила к обсуждению на reddit: http://habrahabr.ru/blogs/android/133172/
— Само обсуждение на reddit: http://www.reddit.com/r/Android/comments/mg14z/whoa_whoa_ics_doesnt_support_usb_mass_storage/

Источник

CDC+MSC USB Composite Device на STM32 HAL

genesis usb mass storage device что это

Мне хотелось бы верить, что хотя бы половина читателей может расшифровать хотя бы половину названия статьи 🙂 Кто не в курсе — поясню. Мое устройство должно реализовывать сразу две 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 умеет ее размещать только в самом начале.

genesis usb mass storage device что это
Картинка из 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 — нет.

genesis usb mass storage device что это

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

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *