CDC MSC USB Composite Device на STM32 HAL / Хабр

Cdc — виртуальный com порт

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

Я недавно переехал на STM32 Cube — пакет низкоуровневых драйверов для STM32. В нем есть код по управлению USB с реализацией отдельных классов USB устройств. Возьмем шаблонные реализации USB Core и CDC и начнем пилить под себя. Заготовки лежат в директории MiddlewaresSTSTM32_USB_Device_Library.

Шаблонная реализация библиотеки подразумевает написание собственного кода в файлах с названием template. Без понимания всей библиотеки и принципов работы USB это сделать достаточно сложно. Но мы пойдем проще — сгенерируем эти файлы с помощью графического конфигуратора CubeMX.

Реализация предоставленная CubeMX готова к работе прямо из коробки. Аж даже немного обидно, что не пришлось писать никакого кода. Придется изучать CDC на примере полностью готовой реализации. Давайте взглянем на самые интересные места в сгенерированном коде.

Для начала заглянем в дескрипторы, которые находятся в файлах usbd_desc.c (дескриптор устройства) и usbd_cdc.c (дескрипторы конфигурации, интерфейсов, конечных точек). В статье usb in a nutshell (на русском) есть очень детальное описание всех дескрипторов. Не буду описывать каждое поле в отдельности, остановлюсь лишь на самых важных и интересных полях.

Дескриптор устройства
#define USBD_VID     1155
#define USBD_LANGID_STRING     1033
#define USBD_MANUFACTURER_STRING     "STMicroelectronics"
#define USBD_PID_FS     22336
#define USBD_PRODUCT_STRING_FS     "STM32 Virtual ComPort"
#define USBD_SERIALNUMBER_STRING_FS     "00000000001A"
#define USBD_CONFIGURATION_STRING_FS     "CDC Config"
#define USBD_INTERFACE_STRING_FS     "CDC Interface"
 
#define USBD_MAX_NUM_CONFIGURATION     1
 
/* USB Standard Device Descriptor */
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
 {
   0x12,                       /*bLength */
   USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
   0x00,                       /* bcdUSB */  
   0x02,
   0x02,                        /*bDeviceClass*/
   0x02,                       /*bDeviceSubClass*/
   0x00,                       /*bDeviceProtocol*/
   USB_MAX_EP0_SIZE,          /*bMaxPacketSize*/
   LOBYTE(USBD_VID),           /*idVendor*/
   HIBYTE(USBD_VID),           /*idVendor*/
   LOBYTE(USBD_PID_FS),           /*idVendor*/
   HIBYTE(USBD_PID_FS),           /*idVendor*/
   0x00,                       /*bcdDevice rel. 2.00*/
   0x02,
   USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
   USBD_IDX_PRODUCT_STR,       /*Index of product string*/
   USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
   USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
 } ;
/* USB_DeviceDescriptor */


Тут нас интересуют такие поля:

Обратите внимание, что строковые константы (название устройства, серийный номер) не прописаны в самом дескрипторе. Строки описываются отдельными дескрипторами, а все остальные только указывают индекс строки. Строковый дескриптор в случае библиотеки от ST генерируется на лету (грррррр), поэтому приводить я его не буду.

Дескритор конфигурации
__ALIGN_BEGIN const uint8_t USBD_CDC_CfgHSDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END =
{
 /*Configuration Descriptor*/
 0x09,   /* bLength: Configuration Descriptor size */
 USB_DESC_TYPE_CONFIGURATION,      /* bDescriptorType: Configuration */
 USB_CDC_CONFIG_DESC_SIZ,                /* wTotalLength:no of returned bytes */
 0x00,
 0x02,   /* bNumInterfaces: 2 interface */
 0x01,   /* bConfigurationValue: Configuration value */
 0x00,   /* iConfiguration: Index of string descriptor describing the configuration */
 0xC0,   /* bmAttributes: self powered */
 0x32,   /* MaxPower 100 mA */

Тут нам интересно следующее:

Дальше идет дескриптор первого из интерфейсов CDC. Этот класс устройств может реализовывать несколько разных моделей общения (телефон, прямое соединение, многостороннее соединение), но в нашем случае это будет Abstract Control Model.

Дескриптор интерфейса управления CDC
/*Interface Descriptor */
 0x09,   /* bLength: Interface Descriptor size */
 USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
 /* Interface descriptor type */
 0x00,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x01,   /* bNumEndpoints: One endpoints used */
 0x02,   /* bInterfaceClass: Communication Interface Class */
 0x02,   /* bInterfaceSubClass: Abstract Control Model */
 0x01,   /* bInterfaceProtocol: Common AT commands */
 0x00,   /* iInterface: */


В этом интерфейсе живет только одна конечная точка (bNumEndpoints). Но прежде идет серия функциональных дескрипторов — настроек специфичных для данного класса устройств.

Функциональный дескрипторы
 /*Header Functional Descriptor*/
 0x05,   /* bLength: Endpoint Descriptor size */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x00,   /* bDescriptorSubtype: Header Func Desc */
 0x10,   /* bcdCDC: spec release number */
 0x01,
 
 /*Call Management Functional Descriptor*/
 0x05,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x01,   /* bDescriptorSubtype: Call Management Func Desc */
 0x00,   /* bmCapabilities: D0 D1 */
 0x01,   /* bDataInterface: 1 */
 
 /*ACM Functional Descriptor*/
 0x04,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x02,   /* bDescriptorSubtype: Abstract Control Management desc */
 0x02,   /* bmCapabilities */
 
 /*Union Functional Descriptor*/
 0x05,   /* bFunctionLength */
 0x24,   /* bDescriptorType: CS_INTERFACE */
 0x06,   /* bDescriptorSubtype: Union func desc */
 0x00,   /* bMasterInterface: Communication class interface */
 0x01,   /* bSlaveInterface0: Data Class Interface */

Тут сказано, что наше устройство не знает о понятии “звонок” (в смысле звонок по телефону), но при этом понимает команды параметров линии (скорость, стоп биты, DTR/CTS биты). Последний дескриптор описывает какой из двух интерфейсов CDC является управляющим, а где бегают данные. В общем, тут нам ничего не интересно и менять мы ничего не будем.

Тут сказано, что эта конечная точка используется для прерываний. Хост будет опрашивать устройство раз в 0x10 (16) мс с вопросом а не требует ли устройство внимания. Также через эту конечную точку будут ходить управляющие команды.

Описание второго интерфейса (там где данные бегают) будет попроще

Интерфейс данных CDC и его конечные точки
/*Data class interface descriptor*/
 0x09,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: */
 0x01,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x02,   /* bNumEndpoints: Two endpoints used */
 0x0A,   /* bInterfaceClass: CDC */
 0x00,   /* bInterfaceSubClass: */
 0x00,   /* bInterfaceProtocol: */
 0x00,   /* iInterface: */
 
 /*Endpoint OUT Descriptor*/
 0x07,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
 CDC_OUT_EP,                        /* bEndpointAddress */
 0x02,                              /* bmAttributes: Bulk */
 LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
 HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
 0x00,                              /* bInterval: ignore for Bulk transfer */
 
 /*Endpoint IN Descriptor*/
 0x07,   /* bLength: Endpoint Descriptor size */
 USB_DESC_TYPE_ENDPOINT,      /* bDescriptorType: Endpoint */
 CDC_IN_EP,                         /* bEndpointAddress */
 0x02,                              /* bmAttributes: Bulk */
 LOBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),  /* wMaxPacketSize: */
 HIBYTE(CDC_DATA_HS_MAX_PACKET_SIZE),
 0x00                               /* bInterval: ignore for Bulk transfer */


В интерфейсе живут 2 конечные точки типа bulk — одна на прием, вторая на передачу. На самом деле в терминологии USB это одна конечная точка, просто двухсторонняя.

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

Библиотека USB от ST весьма слоиста. Я бы выделил такие архитектурные уровни

  • Class Driver (в случае CDC это файлы usbd_cdc и usbd_cdc_if): реализуют логику конкретного класса устройств — CDC для виртуального COM порта, MSC для устройств хранения данных, HID для клавиатур/мышек и всяких специфических устройств с пользовательским интерфейсом.
  • USB Core (usbd_core.c, usbd_ctlreq.c, usbd_ioreq.c): реализует общую логику работы всех классов USB устройств, умеет отдавать хосту запрашиваемые дескрипторы, обрабатывает запросы от хоста и настраивает USB устройство в целом. Также перенаправляет потоки данных из уровня драйвера класса в нижележащие уровни и наоборот.
  • USB HW Driver (usbd_conf.c): Вышележащие слои платформенно независимые и работают одинаковым образом для нескольких серий микроконтроллеров. В коде нет низкоуровневых вызовов функций конкретного микроконтроллера. Файл usbd_conf.c реализует прослойку между USB Core и HAL — библиотеке низкоуровневых драйверов для выбранного микроконтроллера. В основном тут живут простые врапперы, которые перенаправляют вызовы сверху вниз и коллбеки снизу вверх.
  • HAL (stm32f1xx_hal_pcd.c, stm32f1xx_ll_usb.c): занимаются общением с железом микроконтроллера, оперирует регистрами и отвечает на прерывания.

На этом этапе нас будут интересовать только самый верхний слой и одна функция из usbd_conf.c. Начнем с последней:

Функция USBD_LL_Init()
/**
 * @brief  Initializes the Low Level portion of the Device driver.
 * @param  pdev: Device handle
 * @retval USBD Status
 */
USBD_StatusTypeDef  USBD_LL_Init (USBD_HandleTypeDef *pdev)
{
 /* Init USB_IP */
 /* Link The driver to the stack */
 hpcd_USB_FS.pData = pdev;
 pdev->pData = &hpcd_USB_FS;
 
 hpcd_USB_FS.Instance = USB;
 hpcd_USB_FS.Init.dev_endpoints = 8;
 hpcd_USB_FS.Init.speed = PCD_SPEED_FULL;
 hpcd_USB_FS.Init.ep0_mps = DEP0CTL_MPS_8;
 hpcd_USB_FS.Init.low_power_enable = DISABLE;
 hpcd_USB_FS.Init.lpm_enable = DISABLE;
 hpcd_USB_FS.Init.battery_charging_enable = DISABLE;
 if (HAL_PCD_Init(&hpcd_USB_FS) != HAL_OK)
 {
        	Error_Handler();
 }
 
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0xC0);  
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0x110);
 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x82 , PCD_SNG_BUF, 0x100);  
 return USBD_OK;
}

Эта функция инициализирует USB периферию микроконтроллера. Интереснее всего тут серия вызовов функции HAL_PCDEx_PMAConfig(). Дело в том, что на борту микроконтроллера находится цельных 512 байт памяти отведенных специально под буферы USB (эта память называется PMA — Packet Memory Area).

Но вот что странно, объявляли только 2 конечные точки, а вызовов 5. Откуда взялись лишние? На самом деле лишних тут нет. Дело в том, что у каждого USB устройства обязательно должна быть одна двусторонняя конечная точка, через которую устройство инициализируется, а потом управляется.

Эта конечная точка всегда имеет номер 0. Этой функции инициализируются не конечные точки, а буфера. Для нулевой конечной точки создаются 2 буфера — 0x00 на прием и 0x80 на передачу (старший бит указывает направление передачи, младшие — номер конечной точки).

Последний параметр в каждом вызове указывает смещение буфера конечной точки в общем буфере. На форумах видел вопросы «а что это за магическая константа 0x18 (начальный адрес первого буфера)?». Я детально рассмотрю этот вопрос позже. Сейчас лишь скажу, что первые 0x18 байт PMA памяти занимает таблица распределения буферов.

Но это все кишки и другие внутренности. А что снаружи?

Пользовательский код оперирует функциями приема и передачи, которые находятся в файле usbd_cdc_if.c. Чтобы устройство могло отправлять данные в виртуальный COM порт в сторону хоста нам предоставили функцию CDC_Transmit_FS()

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

в разделах “USB с двойной буферизацией” и “printf”.

Еще в этом же файле находятся функции инициализации/деинициализации виртуального COM порта, а также функция изменения параметров порта (скорость, четность, стоп биты и прочее). Реализация по умолчанию не ограничивает себя в скорости и это меня устраивает. Инициализация так же хороша. Оставим все как есть.

Финальный штрих — код, который это все запускает

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

Чтобы это все заработало нужен драйвер со стороны операционной системы. Как правило это стандартный драйвер и система может подхватить устройство без особой процедуры инсталляции. Насколько я понимаю у меня в системе уже был установлен Virtual COM Port драйвер от STM (поставился с ST Flash Utility) и мое устройство подхватилось самостоятельно. На линуксе также все завелось с полпинка.

Cdc msc composite device

А теперь со всей этой фигней мы попробуем взлететь (С) анекдот

Итак, мы уже знаем как строить USB устройства, которые могут реализовывать либо CDC либо MSC. Попробуем сделать композитное устройство, которое реализует оба интерфейса одновременно. Я посмотрел несколько других проектов, которые реализовывали композитное USB устройство и, как мне кажется, их подход имеет смысл. А именно: реализовать собственный драйвер класса, который будет реализовывать и ту и ту функциональность.

Заготовку для класса возьмем из пакета STM32 Cube (MiddlewaresSTSTM32_USB_Device_LibraryClassTemplate). Начинкой будет творчески преработаный код отсюда .

Структура USB устройства будет такая:

imageКрасивая картинка, которая описывает пример описания композитного устройства. Взято из спецификации IAD

Для удобства использования объявим в коде номера конечных точек и интерфейсов.

Нумерация конечных точек повторяет нумерацию интерфейсов. Будем использовать №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

Дескриптор устройства
__ALIGN_BEGIN const uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
{
       	0x12,                       /*bLength */
       	USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
       	0x00,                       /*bcdUSB */
       	0x02,
       	0xEF,                       /*bDeviceClass*/
       	0x02,                       /*bDeviceSubClass*/
       	0x01,                       /*bDeviceProtocol*/
       	USB_MAX_EP0_SIZE,      /*bMaxPacketSize*/
       	LOBYTE(USBD_VID),           /*idVendor*/
       	HIBYTE(USBD_VID),           /*idVendor*/
       	LOBYTE(USBD_PID),           /*idVendor*/
       	HIBYTE(USBD_PID),           /*idVendor*/
       	0x00,                       /*bcdDevice rel. 2.00*/
       	0x02,
       	USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
       	USBD_IDX_PRODUCT_STR,       /*Index of product string*/
       	USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
       	USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
};

В целом тут все тоже самое, отличаются только поля, которые определяют класс устройства (bDeviceClass). Теперь эти поля указывают, что это композитное устройство. Хосту нужно будет потрудится, разобраться во всех остальных дескрипторах и подгрузить правильные драйвера для каждого из компонентов.

Дескриптор конфигурации примерно такой же как и раньше, разница только в количестве интерфейсов. Теперь у нас их 3

Дескриптор конфигурации
#define USB_MSC_CDC_CONFIG_DESC_SIZ       98
 
/* USB MSC CDC device Configuration Descriptor */
static const uint8_t USBD_MSC_CDC_CfgDesc[USB_MSC_CDC_CONFIG_DESC_SIZ] =
{
	0x09,         /* bLength: Configuation Descriptor size */
	USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */
	USB_MSC_CDC_CONFIG_DESC_SIZ, /* wTotalLength: Bytes returned */
	0x00,
	0x03,         /*bNumInterfaces: 3 interface*/
	0x01,         /*bConfigurationValue: Configuration value*/
	0x02,         /*iConfiguration: Index of string descriptor describing the configuration*/
	0xC0,         /*bmAttributes: bus powered and Supports Remote Wakeup */
	0x32,         /*MaxPower 100 mA: this current is used for detecting Vbus*/
	/* 09 bytes */

Далее идет объявление интерфейса и конечных точек для MSC. Не знаю почему именно в таком порядке (сначала MSC потом CDC). Так было в одном из примеров, которые я нашел, оттуда и скопировал. По идее порядок интерфейсов не имеет значения. Главное, чтобы они возили все свои дополнительные дескрипторы рядом. Ну и приколы с нумерацией конечных точек также имеют значение.

Дескрипторы MSC
	/********************  Mass Storage interface ********************/
	0x09,   /* bLength: Interface Descriptor size */
	0x04,   /* bDescriptorType: */
	MSC_INTERFACE_IDX,   /* bInterfaceNumber: Number of Interface */
	0x00,   /* bAlternateSetting: Alternate setting */
	0x02,   /* bNumEndpoints*/
	0x08,   /* bInterfaceClass: MSC Class */
	0x06,   /* bInterfaceSubClass : SCSI transparent command set*/
	0x50,   /* nInterfaceProtocol */
	USBD_IDX_INTERFACE_STR,	/* iInterface: */
	/* 09 bytes */

	/********************  Mass Storage Endpoints ********************/
	0x07,   /*Endpoint descriptor length = 7*/
	0x05,   /*Endpoint descriptor type */
	MSC_IN_EP,   /*Endpoint address (IN, address 1) */
	0x02,   /*Bulk endpoint type */
	LOBYTE(USB_MAX_PACKET_SIZE),
	HIBYTE(USB_MAX_PACKET_SIZE),
	0x00,   /*Polling interval in milliseconds */
	/* 07 bytes */

	0x07,   /*Endpoint descriptor length = 7 */
	0x05,   /*Endpoint descriptor type */
	MSC_OUT_EP,   /*Endpoint address (OUT, address 1) */
	0x02,   /*Bulk endpoint type */
	LOBYTE(USB_MAX_PACKET_SIZE),
	HIBYTE(USB_MAX_PACKET_SIZE),
	0x00,     /*Polling interval in milliseconds*/
	/* 07 bytes */

Дескрипторы MSC ничем не отличаются от тех, что были в предыдущем разделе.

А вот дальше идет новый тип дескриптора — IAD (Interface Association Descriptor) – дескриптор ассоциации интерфейсов. Ассоциация тут не в смысле организации, а в смысле какой интерфейс с какой функцией ассоциировать.

Этот хитрый дескриптор говорит хосту что описание предыдущей функции USB устройства (MSC) закончилось и сейчас будет совсем другая функция. Причем тут же указано какая именно — CDC. Также указано количество связанных с ней интерфейсов и индекс первого из них.

IAD дескриптор не нужен для MSC, т.к. там всего один интерфейс. Но IAD нужен для CDC чтобы сгруппировать 2 интерфейса в одну функцию. Об этом сказано в спецификации этого дескриптора

Наконец дескрипторы CDC. Они полностью соответствуют дескрипторам для одиночной CDC функции с точностью до номеров интерфейсов и конечных точек

Дескрипторы CDC
	/********************  CDC interfaces ********************/

	/*Interface Descriptor */
	0x09,   /* bLength: Interface Descriptor size */
	USB_DESC_TYPE_INTERFACE,  /* bDescriptorType: Interface */
	/* Interface descriptor type */
	CDC_INTERFACE_IDX,   /* bInterfaceNumber: Number of Interface */
	0x00,   /* bAlternateSetting: Alternate setting */
	0x01,   /* bNumEndpoints: One endpoints used */
	0x02,   /* bInterfaceClass: Communication Interface Class */
	0x02,   /* bInterfaceSubClass: Abstract Control Model */
	0x01,   /* bInterfaceProtocol: Common AT commands */
	0x01,   /* iInterface: */
	/* 09 bytes */

	/*Header Functional Descriptor*/
	0x05,   /* bLength: Endpoint Descriptor size */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x00,   /* bDescriptorSubtype: Header Func Desc */
	0x10,   /* bcdCDC: spec release number */
	0x01,
	/* 05 bytes */

	/*Call Management Functional Descriptor*/
	0x05,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x01,   /* bDescriptorSubtype: Call Management Func Desc */
	0x00,   /* bmCapabilities: D0 D1 */
	CDC_INTERFACE_IDX   1,   /* bDataInterface: 2 */
	/* 05 bytes */

	/*ACM Functional Descriptor*/
	0x04,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x02,   /* bDescriptorSubtype: Abstract Control Management desc */
	0x02,   /* bmCapabilities */
	/* 04 bytes */

	/*Union Functional Descriptor*/
	0x05,   /* bFunctionLength */
	0x24,   /* bDescriptorType: CS_INTERFACE */
	0x06,   /* bDescriptorSubtype: Union func desc */
	CDC_INTERFACE_IDX,   /* bMasterInterface: Communication class interface */
	CDC_INTERFACE_IDX   1,   /* bSlaveInterface0: Data Class Interface */
	/* 05 bytes */

	/*Endpoint 2 Descriptor*/
	0x07,                          /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_CMD_EP,                    /* bEndpointAddress */
	0x03,                          /* bmAttributes: Interrupt */
	LOBYTE(CDC_CMD_PACKET_SIZE),   /* wMaxPacketSize: */
	HIBYTE(CDC_CMD_PACKET_SIZE),
	0x10,                          /* bInterval: */
	/* 07 bytes */

	/*Data class interface descriptor*/
	0x09,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_INTERFACE,       /* bDescriptorType: */
	CDC_INTERFACE_IDX   1,         /* bInterfaceNumber: Number of Interface */
	0x00,                          /* bAlternateSetting: Alternate setting */
	0x02,                          /* bNumEndpoints: Two endpoints used */
	0x0A,                          /* bInterfaceClass: CDC */
	0x00,                          /* bInterfaceSubClass: */
	0x00,                          /* bInterfaceProtocol: */
	0x00,                          /* iInterface: */
	/* 09 bytes */

	/*Endpoint OUT Descriptor*/
	0x07,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_OUT_EP,                    /* bEndpointAddress */
	0x02,                          /* bmAttributes: Bulk */
	LOBYTE(CDC_DATA_PACKET_SIZE),  /* wMaxPacketSize: */
	HIBYTE(CDC_DATA_PACKET_SIZE),
	0x00,                          /* bInterval: ignore for Bulk transfer */
	/* 07 bytes */

	/*Endpoint IN Descriptor*/
	0x07,   /* bLength: Endpoint Descriptor size */
	USB_DESC_TYPE_ENDPOINT,        /* bDescriptorType: Endpoint */
	CDC_IN_EP,                     /* bEndpointAddress */
	0x02,                          /* bmAttributes: Bulk */
	LOBYTE(CDC_DATA_PACKET_SIZE),  /* wMaxPacketSize: */
	HIBYTE(CDC_DATA_PACKET_SIZE),
	0x00,                          /* bInterval */
	/* 07 bytes */

Когда все дескрипторы готовы можно посчитать суммарный размер конфигурации.

#define USB_CDC_CONFIG_DESC_SIZ       98


Перейдем к написанию кода. Ядро USB общается с драйверами классов используя вот такой интерфейс

Интерфейс драйвера класса
typedef struct _Device_cb
{
uint8_t  (*Init)             (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
uint8_t  (*DeInit)           (struct _USBD_HandleTypeDef *pdev , uint8_t cfgidx);
/* Control Endpoints*/
uint8_t  (*Setup)            (struct _USBD_HandleTypeDef *pdev , USBD_SetupReqTypedef  *req);
uint8_t  (*EP0_TxSent)       (struct _USBD_HandleTypeDef *pdev );   
uint8_t  (*EP0_RxReady)      (struct _USBD_HandleTypeDef *pdev );
/* Class Specific Endpoints*/
uint8_t  (*DataIn)           (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);  
uint8_t  (*DataOut)          (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t  (*SOF)              (struct _USBD_HandleTypeDef *pdev);
uint8_t  (*IsoINIncomplete)  (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);
uint8_t  (*IsoOUTIncomplete) (struct _USBD_HandleTypeDef *pdev , uint8_t epnum);  
 
const uint8_t  *(*GetHSConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetFSConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetOtherSpeedConfigDescriptor)(uint16_t *length);
const uint8_t  *(*GetDeviceQualifierDescriptor)(uint16_t *length);
#if (USBD_SUPPORT_USER_STRING == 1)
uint8_t  *(*GetUsrStrDescriptor)(struct _USBD_HandleTypeDef *pdev ,uint8_t index,  uint16_t *length);  
#endif
} USBD_ClassTypeDef;

В зависимости от состояния или события на шине USB ядро вызывает соответствующую функцию.

Любую архитектурную проблему можно решить введением дополнительного абстрактного слоя… (С) еще один анекдот

Разумеется мы не будем реализовывать весь функционал целиком — за реализацию классов CDC и MSC будет отвечать существующий код. Мы лишь напишем прослойку, которая будет перенаправлять вызовы либо в одну, либо в другую реализацию.


Тут все просто: инициализируем (деинициализируем) оба класса. Вызываемые функции сами займутся созданием/удалением своих конечных точек.

Пожалуй самой сложной функцией будет Setup.

Это коллбек на один из стандартных запросов по шине USB, но этот запрос очень многогранный. Это может быть как получение данных (get), так и установка (Set). Это может быть запрос к устройству в целом, к одному из его интерфейсов или конечных точек. Также тут может приплыть как стандартный запрос, определенный базовой спецификацией USB, так и специфичный для определенного устройства или класса. Подробнее

(Раздел “Пакет Setup”).

Из-за обилия разных случаев структура обработчика пакета Setup весьма сложна. Тут не получается написать один if или switch. В коде ядра USB обработка размазана по 3-4 большим функциям и в определенных случаях передается отдельному специализированному обработчику (коих там еще с десяток). Радует только то, что на уровень драйвера класса передается только незначительная часть запросов.

Я подсмотрел какие пакеты ходят через эту функцию и, похоже, можно ориентироваться по получателю. Если получатель пакета интерфейс — в поле wIndex будет номер интерфейса, если конечная точка, то в wIndex будет номер конечной точки. Исходя из этого перенаправляем запросы в соответствующий обработчик.

Кстати, чтобы это работало нужно не забыть поменять дефайн, определяющий количество интерфейсов, а то запрос просто не дойдет и срежется внутри ядра USB

#define USBD_MAX_NUM_INTERFACES 	3


Коллбеками DataIn и DataOut все проще. Там есть номер конечной точки — по ней и определим куда запрос перенаправлять

Обратите внимание, что флаг направления передачи в номере конечной точки не используется. Т.е. даже если некоторые функции используют MSC_IN_EP (0x81), то в этой функции нужно использовать MSC_EP_IDX (0x01).

Иногда данные приходят в нулевую конечную точку и для этого есть специальный коллбек. Я не знаю что бы я делал, если бы оба класса (и CDC и MSC) имели обработчики на этот случай – в таком запросе не указан интерфейс или номер конечной точки. Было бы невозможно понять кому адресован запрос. Благо такой запрос умеет обрабатывать только класс CDC – вот ему и отправим

Больше у нас не будет нетривиальных обработчиков. Есть еще парочка геттеров для дескрипторов, но их код стандартный и не представляет интереса. Заполним «таблицу виртуальных функций»

Таблица указателей на функции
USBD_ClassTypeDef  USBD_MSC_CDC_ClassDriver =
{
    	USBD_MSC_CDC_Init,
    	USBD_MSC_CDC_DeInit,
    	USBD_MSC_CDC_Setup,
    	NULL, //USBD_MSC_CDC_EP0_TxReady,
    	USBD_MSC_CDC_EP0_RxReady,
    	USBD_MSC_CDC_DataIn,
    	USBD_MSC_CDC_DataOut,
    	NULL, //USBD_MSC_CDC_SOF,
    	NULL, //USBD_MSC_CDC_IsoINIncomplete,
    	NULL, //USBD_MSC_CDC_IsoOutIncomplete,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetCfgDesc,
    	USBD_MSC_CDC_GetDeviceQualifierDesc,
};


Теперь код инициализации

Инициализируем USB ядро, устанавливаем ему наш драйвер класса и настраиваем вторичные интерфейсы. Все? Нет не все. В таком виде оно не запустится.

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

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

Решать это можно несколькими способами. Товарищи отсюда вообще затолкали в свою реализацию класса весь код из обоих драйверов (CDC и MSC) чтобы на ходу разбираться что к чему. Другой подход в том, что в эти поля класть структуры, в которых есть место для данных обоих классов.

Мы, пожалуй, пойдем путем попроще. Если драйверы классов хотят эксклюзивных полей – дадим им эти поля

Msc — запоминающее устройство

С драйвером CDC было все просто — устройство, как правило, само является конечным потребителем данных (например получает от хоста команды) или же генератором (например отправляет хосту показания датчиков).

С Mass Storage Class будет чуток сложнее. Драйвер MSC является всего лишь прослойкой между хостом и шиной USB с одной стороны, и запоминающим устройством с другой. Это может быть SD карта подключенная по SDIO, SPI Flash, может быть RAM Drive, дисковый накопитель, а может быть даже сетевой диск.

В моем устройстве используется SD карта, подключенная через SPI. Для доступа к файлом на этой карте я использую библиотеку SdFat. Она также разделена на несколько уровней абстракции:

  • Пользователю предоставляется класс File, через который можно создавать/открывать файлы, читать и писать данные. Клиентский код не парится взаимодействием с носителем информации и тонкостями файловой системы.
  • Класс Volume занимается всей кухней по обслуживанию файловой системы, каталога, кластеров, FAT и такого прочего. Общение с носителем данных делегируется в нижележащие уровни.
  • Драйвер SD карты — этот компонент знает как общаться с картой, какие ей слать команды и какие слушать ответы. Библиотека предоставляет несколько видов драйверов для карт подключенных по SPI и SDIO. Теоретически можно подставить свой драйвер, например, для RAM диска.
  • Вышележащие слои кроссплатформенные, они ничего не знают о том как именно данные будут писаться на карту или читаться с нее. Это позволяет собирать библиотеку под разные платформу (как Ардуино, так и другие). Для конкретной платформы или микроконтроллера можно написать драйвер, который будет реализовывать передачу данных через необходимый интерфейс. По умолчанию библиотека предоставляет несколько драйверов, в т.ч. для ардуиновского SPI, но я заморочился и написал свой драйвер с преферансом и поэтессами передачей через DMA на основе HAL.
  • Наконец, HAL обеспечивает работу с регистрами конкретного микроконтроллера

В случае USB Mass Storage мы не будем работать с файлами на флешке — всю работу по интерпретации файловой системы будет делать хост. К устройству будут приходить запросы на чтение или запись конкретного блока данных. Так что нас будут интересовать уровни от драйвера карты и ниже.

Реализация MSC требует от хранилища определенного интерфейса — уметь читать и писать, отдавать свой размер и статус. Примерно такие же возможности предоставляет интерфейс драйвера SD карты библиотеки SdFat. Остается лишь написать адаптер, который приведет один интерфейс к другому.

С направлением движения определились. Займемся реализацией. Я опять воспользовался конфигуратором CubeMX и сгенерировал нужные файлы для компонента USB. Изучение начнем, конечно же, с дескрипторов.

Дескриптор устройства
* USB Standard Device Descriptor */
__ALIGN_BEGIN uint8_t USBD_FS_DeviceDesc[USB_LEN_DEV_DESC] __ALIGN_END =
 {
   0x12,                       /*bLength */
   USB_DESC_TYPE_DEVICE,       /*bDescriptorType*/
   0x00,                       /* bcdUSB */  
   0x02,
   0x00,                       /*bDeviceClass*/
   0x00,                       /*bDeviceSubClass*/
   0x00,                       /*bDeviceProtocol*/
   USB_MAX_EP0_SIZE,          /*bMaxPacketSize*/
   LOBYTE(USBD_VID),           /*idVendor*/
   HIBYTE(USBD_VID),           /*idVendor*/
   LOBYTE(USBD_PID_FS),           /*idVendor*/
   HIBYTE(USBD_PID_FS),           /*idVendor*/
   0x00,                       /*bcdDevice rel. 2.00*/
   0x02,
   USBD_IDX_MFC_STR,           /*Index of manufacturer  string*/
   USBD_IDX_PRODUCT_STR,       /*Index of product string*/
   USBD_IDX_SERIAL_STR,        /*Index of serial number string*/
   USBD_MAX_NUM_CONFIGURATION  /*bNumConfigurations*/
 } ;
/* USB_DeviceDescriptor */

Дескриптор устройства практически не изменился. Разница только в полях, определяющих класс устройства — теперь класс устройства в целом не задан (нули в bDeviceClass), а будет задаваться на уровне интерфейса (это требование

Дескриптор конфигурации
 0x09,   /* bLength: Configuation Descriptor size */
 USB_DESC_TYPE_CONFIGURATION,   /* bDescriptorType: Configuration */
 USB_MSC_CONFIG_DESC_SIZ,
 
 0x00,
 0x01,   /* bNumInterfaces: 1 interface */
 0x01,   /* bConfigurationValue: */
 0x04,   /* iConfiguration: */
 0xC0,   /* bmAttributes: */
 0x32,   /* MaxPower 100 mA */

Очень похоже на аналогичный дескриптор из CDC — определяется количество интерфейсов (1) и параметры питания от шины (до 100 мА)

Дескриптор интерфейса
 0x09,   /* bLength: Interface Descriptor size */
 0x04,   /* bDescriptorType: */
 0x00,   /* bInterfaceNumber: Number of Interface */
 0x00,   /* bAlternateSetting: Alternate setting */
 0x02,   /* bNumEndpoints*/
 0x08,   /* bInterfaceClass: MSC Class */
 0x06,   /* bInterfaceSubClass : SCSI transparent*/
 0x50,   /* nInterfaceProtocol */
 0x05,          /* iInterface: */

Дескриптор интерфейса объявляет 2 конечных точки (по одной в каждую сторону передачи). Также дескриптор определяет какой именно это подкласс Mass Storage — Bulk Only Transport. Я не нашел толкового описания что же именно это за подкласс такой. Предполагаю, что это устройство, которое общается только посредством двусторонней передачи данных через 2 конечные точки (тогда как другие модели могут использовать еще и прерывания). Протоколом в этом общении являются SCSI команды.

Дескрипторы конечных точек
 0x07,   /*Endpoint descriptor length = 7*/
 0x05,   /*Endpoint descriptor type */
 MSC_EPIN_ADDR,   /*Endpoint address (IN, address 1) */
 0x02,   /*Bulk endpoint type */
 LOBYTE(MSC_MAX_FS_PACKET),
 HIBYTE(MSC_MAX_FS_PACKET),
 0x00,   /*Polling interval in milliseconds */
 
 0x07,   /*Endpoint descriptor length = 7 */
 0x05,   /*Endpoint descriptor type */
 MSC_EPOUT_ADDR,   /*Endpoint address (OUT, address 1) */
 0x02,   /*Bulk endpoint type */
 LOBYTE(MSC_MAX_FS_PACKET),
 HIBYTE(MSC_MAX_FS_PACKET),
 0x00     /*Polling interval in milliseconds*/


Тут определяются 2 конечные точки типа Bulk — интерфейс USB не гарантирует скорость по таким конечным точкам, зато гарантирует доставку данных. Размер пакета устанавливается в 64 байта.

Раз уж мы говорим про конечные точки, то стоит заглянуть в файл usbd_conf.c где определяются соответствующие PMA буфера

Настройка PMA буферов
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x00 , PCD_SNG_BUF, 0x18);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x80 , PCD_SNG_BUF, 0x58);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x81 , PCD_SNG_BUF, 0x98);
HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData , 0x01 , PCD_SNG_BUF, 0xD8);

Теперь посмотрим на 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 умеет возвращать размер карты сразу в блоках

Функции чтения и записи
int8_t SD_MSC_Read (uint8_t lun,
                                                               	uint8_t *buf,
                                                               	uint32_t blk_addr,
                                                               	uint16_t blk_len)
{
       	(void)lun; // Not used
 
       	if(!card.readBlocks(blk_addr, buf, blk_len))
               	return USBD_FAIL;
 
       	return (USBD_OK);
}
 
int8_t SD_MSC_Write (uint8_t lun,
                                                               	uint8_t *buf,
                                                               	uint32_t blk_addr,
                                                               	uint16_t blk_len)
{
       	(void)lun; // Not used
 
       	if(!card.writeBlocks(blk_addr, buf, blk_len))
               	return USBD_FAIL;
 
       	return (USBD_OK);
}

Чтение и запись также реализована вполне просто.

Карта у нас всегда готова (хотя в будущем я буду пристальнее смотреть на статус) и не защищена от записи.

LUN — Logic Unit Number. Теоретически наше запоминающее устройство может состоять из нескольких носителей (например жесткие диски в рейде). Все функции SCSI протокола указывают с каким носителем оно хочет работать. Функция GetMaxLun возвращает номер последнего устройства (количество устройств минус 1). Флешка у нас одна потому возвращаем 0.

И последняя штука.

Описатель запоминающего устройства
const uint8_t SD_MSC_Inquirydata[] = {/* 36 */
/* LUN 0 */
0x00,
0x80,
0x02,
0x02,
(STANDARD_INQUIRY_DATA_LEN - 5),
0x00,
0x00,
0x00,
'S', 'T', 'M', ' ', ' ', ' ', ' ', ' ', /* Manufacturer : 8 bytes */
'P', 'r', 'o', 'd', 'u', 'c', 't', ' ', /* Product      : 16 Bytes */
' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
'0', '.', '0' ,'1'                     /* Version      : 4 Bytes */
};

Если честно, я особо не разобрался зачем оно нужно. Заглянув в спецификацию SCSI я увидел очень много полей смысла, которых я не понял. Из того, что я осилил – тут описывается стандартное устройство с прямым (не секвентальным) доступом, причем которое может быть извлечено (removable). Благо во всех примерах, которые я видел этот массив совпадает, так что пускай будет. Отлажено ведь.

Теперь все это нужно правильно проинициализировать

Подключаем, проверяем. Все работает, правда очень медленно — подключенный диск открывается секунд 50. Отчасти это из-за того, что линейная скорость чтения флешки через такой интерфейс получается около 200кб/с. Когда USB Mass Storage устройство подключается к компьютеру, операционная система вычитывает таблицу FAT.

Также мне пришлось отключить DMA при работе с SD картой – там не все так просто с его включением. Дело в том, что моя реализация драйвера (как оказалось) не может работать из прерывания, а в USB все только через прерывания и работает. Не работает даже банальный HAL_Delay() т.к. он тоже завязан на прерывания, не говоря уже о синхронизации с использованием FreeRTOS.

UPDATE: как и обещал вот линка. Удалось прокачать скорость до 650кб/с

Немного теории

Интерфейс USB очень сложный, многоуровневый и многогранный. С наскоку его не осилить. В одной из статей (забыл, правда, в какой) видел фразу в стиле “прочитайте эту статью 2 раза, а потом на утро еще раз”. Да, он такой, с первого раза точно не осилишь.

Я по прежнему не являюсь экспертом в USB, а потому рекомендовал бы обратиться к статьям, которые бы детальнее рассказали суть происходящего. Я лишь укажу на самые важные места и вкратце поясню как оно работает — по большей части во что вляпался сам. В первую очередь я бы рекомендовал Usb in a nutshell (перевод), а также USB Made Simple (сам не читал, но многие рекомендуют). Также нам понадобятся спецификации для конкретных классов USB устройств.

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

Устройство описывает себя с помощью нескольких дескрипторов разного типа:

  • Дескриптор устройства (Device Descriptor) — описывает устройство в целом, его название, производитель, серийный номер. Строковые данные описываются отдельными строковыми дескрипторами (String Descriptor)
  • Дескриптор конфигурации (Configuration Descriptor) — устройство может иметь одну или несколько конфигураций. Каждая конфигурация определяет скорость общения с устройством, набор интерфейсов и параметры питания. Так, например, ноутбук, который работает от батареи, может попросить устройство (выбрать конфигурацию) использовать более низкую скорость обмена и переключиться на собственный источник питания (вместо ноутбучной батареи). Разумеется это работает только если устройство предоставляет такую конфигурацию.
  • Дескриптор интерфейса (Interface descriptor) — описывает интерфейс общения с устройством. Интерфейсов может быть несколько. Например разные функции (MSC, CDC, HID) будут реализовывать свои интерфейсы. Некоторые функции (например CDC или DFU) реализуют сразу несколько интерфейсов для своей работы. В нашем случае композитного устройства нам потребуется реализовать сразу несколько интерфейсов от разных функций и заставить их ужиться друг с другом.
  • Дескриптор конечной точки (Endpoint descriptor) — описывает канал связи в рамках конкретного интерфейса, задает размер пакета, описывает параметры прерываний. Используя конечные точки мы будем получать и принимать данные.
  • Есть еще куча разных дескрипторов, которые описывают отдельные аспекты конкретных интерфейсов

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

Еще нужно понимать, что USB это хост ориентированный протокол. Настройка устройства, прием, передача — все в USB управляется со стороны хоста. Для нас это означает, что со стороны микроконтроллера нет никакого потока управления — вся работа с USB построена на прерываниях и обратных вызовах (callback).

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

Также хост-ориентированность проявляется еще и в названии функций. В терминологии USB направление от хоста к устройству называется OUT, хотя для контроллера это прием. И наоборот, направление от устройства к хосту называется IN, хотя для нас это означает отправку данных.

Заключение

На некоторых форумах видел сообщения вроде “как все в этом USB сложно, какие-то драйверы… Я щас лучше на регистрах нафигачу”. Ребят, не все так просто. Уровень регистров это, наверное, самая простая часть. Но помимо нее есть огромный пласт логики, которую должно реализовывать устройство. И вот тут уже без знаний протоколов и многих сотен страниц спецификаций никак.

Но не все так плохо. Люди уже позаботились и написали всю логику. В большинстве случаев остается только подставить нужные значения и подправить некоторые параметры. Да, библиотека от ST — тот еще монстр. Но после вдумчивого прочтения USB In A Nutshell, парочки спецификаций конкретного класса устройств и работы со сниффером многие вещи становятся на свои места.

Я делал реализацию композитного CDC MSC устройства, но примерно такой же подход можно применить и для других комбинаций — CDC HID, MSC Audio, CDC MSC HID и других. Моя реализация предназначена для работы на микроконтроллерах серии STM32F103, но сам принцип может быть адаптирован и для других микроконтроллеров (в т.ч. и не STM32).

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

Вместо пересказа спецификаций я попробовал описать как работает реализация USB стека от ST. Также я постарался обратить внимание на особые моменты и рассказать почему делается именно так.

Я долго сомневался ставить ли галочку “Tutorial”. С одной стороны я даю рекомендации и пошаговые инструкции, обращаю внимание на особые моменты и даю ссылки на первоисточники. С другой стороны я не могу предоставить готовую библиотеку для скачивания и встраивания в свои проекты.

Дело в том, что в процессе работы над своим проектом я хорошенько поработал напильником, лобзиком и другими инструментами над этой библиотекой. Я выкинул много кода, который в моем устройстве не нужен, часть поменял, починил некоторые вещи, которые мне не нравились.

Теперь библиотека USB весьма серьезно отличается от той, что выложена на сайте ST. Некоторые из изменений специфичны для моего проекта и могут не подойти для других ситуаций. Впрочем, добро пожаловать в мой репозиторий — изучайте, копируйте к себе, задавайте вопросы, предлагайте улучшения.

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

Если статья Вам понравилась, Вы можете поддержать меня купив чашечку кофе.
Buy Me A Coffee

Оцените статью
Huawei Devices
Добавить комментарий