
В предыдущей части цикла мы создали виртуальный COM‑порт USB на базе микроконтроллера AT32F403ACGT7 производства компании Artery Technology.
В этой части мы проведём анализ сгенерированного кода с помощью официальной документации на класс устройств USB, предназначенных для передачи данных по последовательным интерфейсам, а затем дополним функциональность нашего виртуального COM‑порта.
Данный цикл публикаций состоит из четырёх частей:
Составное устройство USB на AT32. Часть 1: Подготовка
Составное устройство USB на AT32F403A. Часть 2: Виртуальный COM-порт и его доработки <— Вы тут
Составное устройство USB на AT32F403A. Часть 3: Звуковое устройство и его доработки
Составное устройство USB на AT32F403A. Часть 4: Составное устройство Audio + CDC
Исходный код проекта можно получить по ссылке: https://gitflic.ru/project/selenite_pro/at32f403acgt7_composite
По моему опыту изучение устройства USB проще всего начинать с чтения его дескрипторов, из которых нужно выделить:
-
дескриптор устройства (Device Descriptor),
-
дескриптор конфигурации (Configuration Descriptor),
-
дескрипторы интерфейсов устройства (Class‑Specific Interface Descriptors).
Здесь и далее нумерация байтов начинается с нуля. По байтам 4 и 5 дескриптора устройства определяем класс устройства USB и его подкласс. По дескриптору конфигурации определяем интерфейсы, входящие в эти конфигурации. По дескрипторам интерфейсов определяем тип интерфейсов и их функциональность.
Читаем дескрипторы
Сначала обратимся к дескриптору устройства: открываем файл cdc_desc.c и находим там массив g_usbd_descriptor[USB_DEVICE_DESC_LEN]. По значению байта 4 (bDeviceClass = 0×02) определяем, что это — устройство USB для передачи данных. Далее открываем раздел 5.1.1 на странице 15 документа [1] и сверяем по таблице 8 значения байтов 5 и 6 (bDeviceSubClass = 0×00, bDeviceProtocol = 0×00). Значения этих полей дескриптора соответствуют указанным в таблице значениям для класса устройств Communications Device Class.
cdc_desc.c ... USB_DEVICE_DESC_LEN, /* bLength */ USB_DESCIPTOR_TYPE_DEVICE, /* bDescriptorType */ 0x00, /* bcdUSB */0x02,0x02, /* bDeviceClass */0x00, /* bDeviceSubClass */0x00, /* bDeviceProtocol */USB_MAX_EP0_SIZE, /* bMaxPacketSize */LBYTE(USBD_CDC_VENDOR_ID), /* idVendor */HBYTE(USBD_CDC_VENDOR_ID), /* idVendor */LBYTE(USBD_CDC_PRODUCT_ID), /* idProduct */HBYTE(USBD_CDC_PRODUCT_ID), /* idProduct */0x00, /* bcdDevice rel. 2.00 */0x02,USB_MFC_STRING, /* Index of manufacturer string */USB_PRODUCT_STRING, /* Index of product string */USB_SERIAL_STRING, /* Index of serial number string */1 /* bNumConfigurations */
Переходим к дескриптору конфигурации: находим в файле cdc_desc.c массив g_usbd_configuration[USBD_CDC_CONFIG_DESC_SIZE] и определяем по значению байта 4 (bNumInterfaces = 0×02), что конфигурация содержит два интерфейса.
Далее обратимся к дескриптору первого интерфейса конфигурации. По значениям байтов 11 и 13 определяем логический номер этого интерфейса (байт 11, bInterfaceNumber = 0×00) и количество связанных с ним конечных точек (байт 13, bNumEndpoints = 0×01, одна конечная точка). По значениям байта 14 (bInterfaceClass = 0×02, CDC class code) и байта 15 (bInterfaceSubClass = 0×02, Abstract Control Model subclass code) дескриптора конфигурации определяем, что это интерфейс управления виртуальным COM‑портом, и что порт этот собран на базе устройства подкласса ACM класса CDC. Описание этого подкласса дано в разделе 3.2.2 на странице 7 документа [2].
cdc_desc.c...USB_DEVICE_IF_DESC_LEN, /* bLength: interface descriptor size */USB_DESCIPTOR_TYPE_INTERFACE, /* bDescriptorType: interface descriptor type */0x00, /* bInterfaceNumber: Communication class interface number */0x00, /* bAlternateSetting: alternate set */0x01, /* bNumEndpoints: number of endpoints */USB_CLASS_CODE_CDC, /* bInterfaceClass: CDC class code */0x02, /* bInterfaceSubClass: subclass code, Abstract Control Model*/0x01, /* bInterfaceProtocol: protocol code, AT Command */0x00, /* iInterface: index of string descriptor */
Значения вышеуказанных байтов дескриптора интерфейса управления (Communication Class Interface Descriptor) даны в разделе 5.1.3 на странице 15 документа [1]. Значение следующего за ними байта (bInterfaceProtocol = 0×01, AT Command) указывает на поддержку устройством набора команд по стандарту V.250 ITU‑T, то есть устройство USB, которое мы видим в Диспетчере устройств как COM‑порт, на самом деле описывается дескриптором Hayes‑совместимого телефонного модема.
Функциональность интерфейса управления описывается в следующих за дескриптором интерфейса девятнадцати байтах дескриптора конфигурации:
-
Header Functional Descriptor (5 байт).
-
Call Management Functional Descriptor (5 байт).
-
ACM Functional Descriptor (4 байта).
-
Union Functional Descriptor (5 байт).
Обратим особое внимание на байт 32 дескриптора конфигурации (ACM Functional Descriptor, bmCapabilities = 0×02). Согласно данным таблицы 4 на странице 15 документа [2] этот виртуальный COM‑порт поддерживает обработку запросов GET_LINE_CODING, SET_LINE_CODING, SET_CONTROL_LINE_STATE, а также отсылку уведомления SERIAL_STATE. К этой важной информации мы вернёмся позже.
Конфигурация устройства, помимо интерфейса управления, содержит ещё и один интерфейс передачи данных. Этот интерфейс подчинён интерфейсу управления, о чём и указано в части дескриптора, обозначенной как Union Functional Descriptor. Номер интерфейса передачи данных (CDC_BULK_INTERFACE_NUMBER) указан там в поле bSubordinateInterface0.
Дескриптор интерфейса передачи данных следует сразу за дескриптором конечной точки интерфейса управления. С интерфейсом передачи данных связаны две конечные точки: одна для приёма данных (USBD_CDC_BULK_OUT_EPT), другая — для передачи (USBD_CDC_BULK_IN_EPT).
Заметим, что интерфейсов в этом классе устройств может быть больше двух. Их количество (n) указывается в байте 4 дескриптора конфигурации, а для указания номеров дополнительных подчинённых интерфейсов в дескриптор вводятся поля bSubordinateInterface1 и так далее. Каждый дополнительный интерфейс, естественно, должен иметь свой дескриптор в составе дескриптора конфигурации.
Дорабатываем дескрипторы
Всякий раз, когда речь идёт об улучшении работающего решения, нужно решить, зачем это нужно, и стоит ли это делать.
Поскольку я программист ненастоящий, то не всегда понимаю логику разработчиков, а именно, исходя из чего в данном конкретном случае, номера интерфейсов виртуального COM‑порта в дескрипторе конфигурации были прописаны числами, а номера конечных точек — макросами.
Целью моей доработки дескриптора конфигурации виртуального COM‑порта было обеспечение переноса без правок любой его части в дескриптор конфигурации составного устройства USB.
Байты массива с явно указанными номерами интерфейсов были закомментированы, а вместо них в дескриптор вставлены заменяющие их макросы. Также, массив g_usbd_configuration[USBD_CDC_CONFIG_DESC_SIZE] был дополнен комментариями, содержащими названия составных частей дескриптора конфигурации, как они обозначены в [1] и [2], и длину этих частей в байтах.
cdc_class.h...#ifndef CDC_CTRL_INTERFACE_NUMBER#define CDC_CTRL_INTERFACE_NUMBER 0x00#endif#ifndef CDC_BULK_INTERFACE_NUMBER#define CDC_BULK_INTERFACE_NUMBER 0x01#endif cdc_desc.c.../*Union Functional Descriptor*/0x05, /* bFunctionLength: size of this descriptor in bytes */USBD_CDC_CS_INTERFACE, /* bDescriptorType: CDC interface descriptor type */USBD_CDC_SUBTYPE_UFD, /* bDescriptorSubtype: Union Function Descriptor subtype *///0x00,CDC_CTRL_INTERFACE_NUMBER, /* bControlInterface: Communication class interface number *///0x01,CDC_BULK_INTERFACE_NUMBER, /* bSubordinateInterface0: Data Class Interface number *//* 05 bytes */
Разбираем обработчики…
Теперь, когда мы в общих чертах разобрались по дескрипторам с функциональностью устройства, настало время перейти к обработчикам, код которых размещён в файле cdc_class.c.
С инициализацией (деинициализацией), в принципе, всё понятно: создаём логическую структуру устройства, открываем его конечные точки на приём и передачу. С обработчиками событий конечных точек немного сложнее.
Обработчик class_out_handler( ) вызывается по прерыванию при получении данных конечной точкой USBD_CDC_BULK_OUT_EPT. Обработчик class_in_handler( ) вызывается по прерыванию при отправке данных конечной точкой USBD_CDC_BULK_IN_EPT. А вот для событий, связанных с конечной точкой интерфейса управления USBD_CDC_INT_EPT, обработчика нет.
Зато есть достаточно сложный на вид обработчик class_setup_handler( ). Эта функция нужна для обработки запросов, адресованных на конечную точку EP0, которая по умолчанию используется для управления устройством USB. Часть кода обрабатывает «стандартные» запросы, типа GET_INTERFACE, SET_INTERFACE и тому подобные. Другая часть — «специфические для класса».
Переходим к функции usb_vcp_cmd_process ( ) и обнаруживаем обработку только двух «специфических» запросов SET_LINE_CODING и GET_LINE_CODING. Кода, ответственного за обработку SET_CONTROL_LINE_STATE и посылку уведомления SERIAL_STATE, обнаружено не было.
…и дополняем их функциональность
Попытаемся добавить в программу обработку запроса SET_CONTROL_LINE_STATE и посылку уведомления SERIAL_STATE.
Структура запроса SET_CONTROL_LINE_STATE приведена в разделе 6.3.12 на странице 25 документа [2]. Состояние линий RTS и DTR содержится в байте 2 (младший байт поля wValue) запроса, значение WLength = 0×00.
Структура уведомления SERIAL_STATE приведена в разделе 6.5.4 на странице 32 документа [2]. Состояние линий RI, CDC и DSR передаётся в байте 9 уведомления. Структура данных отображена в таблице 31 на следующей странице документа.
Сначала добавим в программу нужные переменные:
cdc_class.c...#define SET_CONTROL_LINE_STATE 0x22#ifdef SET_CONTROL_LINE_STATEtypedef struct { uint8_t bmRequestType; /* characteristics of request */ uint8_t bNotification; /* class specific notification */ uint16_t wValue; /* always = 0 */ uint16_t wIndex; /* cdc control interface number */ uint16_t wLength; /* number of bytes to transfer = 2 */ uint8_t bSerial_state; /* D3 = RI, D1 = DSR, D0 = DCD */ uint8_t reserved;}cdc_notification_struct_type;cdc_notification_struct_type serial_state_note = {0xA1, 0x20, 0, (uint16_t)CDC_CTRL_INTERFACE_NUMBER, 2, 0, 0};uint8_t rts, dtr;#endif
Затем дополним код функции class_setup_handler( ) обработкой запросов с нулевым значением поля wLength для обработки запроса SET_CONTROL_LINE_STATE:
cdc_class.c...static usb_sts_type class_setup_handler(void *udev, usb_setup_type *setup){ usb_sts_type status = USB_OK; usbd_core_type *pudev = (usbd_core_type *)udev; cdc_struct_type *pcdc = (cdc_struct_type *)pudev->class_handler->pdata; switch(setup->bmRequestType & USB_REQ_TYPE_RESERVED) { /* class request */ case USB_REQ_TYPE_CLASS: if(setup->wLength) { ... }#ifdef SET_CONTROL_LINE_STATE else {usb_vcp_cmd_process(udev, setup->bRequest, (uint8_t *)setup, setup->wLength);}#endif break; /* standard request */ case USB_REQ_TYPE_STANDARD: ... break; default: ... break; } return status;}
Далее «научим» функцию usb_vcp_cmd_process( ) извлекать из запроса SET_CONTROL_LINE_STATE состояние линий RTS и DTR, а затем передавать их назад своеобразным «эхом» сигналами CTS и CDC с помощью уведомления SERIAL_STATE через конечную точку интерфейса управления USBD_CDC_INT_EPT:
cdc_class.c...static void usb_vcp_cmd_process(void *udev, uint8_t cmd, uint8_t *buff, uint16_t len){ usbd_core_type *pudev = (usbd_core_type *)udev; cdc_struct_type *pcdc = (cdc_struct_type *)pudev->class_handler->pdata; switch(cmd) { case SET_LINE_CODING: ... break; case GET_LINE_CODING: ... break;#ifdef SET_CONTROL_LINE_STATE case SET_CONTROL_LINE_STATE: { /* control line state parsing */ if(buff[2] & 0x01) dtr = 1; else dtr = 0; if(buff[2] & 0x02) rts = 1; else rts = 0; /* control line state to serial state note loopback */ /* RTS -> DSR, DTR -> CDC */ serial_state_note.bSerial_state = buff[2] & 0x03; usbd_ept_send(pudev, USBD_CDC_INT_EPT, (uint8_t *)&serial_state_note, 10); } break;#endif default: break; }}
Проверим это всё программой терминала и убедимся, что организовать как приём состояния сигналов RTS и DTR, так и передачу сигналов CDC и DSR назад «эхом» через конечную точку USBD_CDC_INT_EPT удалось.
Заметим, что уровень сигнала RTS при передаче его в устройство изменяется только по фронту сигнала DTR. С чем это связано, не знаю, но такое же поведение этой линии наблюдалось и в составе виртуального COM‑порта на базе STM32.
Краткие выводы и дальнейшие действия
В данной части цикла нам удалось понять в общих чертах принципы организации виртуального COM‑порта на микроконтроллере AT32F403ACGT7 производства компании Artery Technology, добавить «абстракции» в дескрипторы этого устройства и ввести обработку состояний линий RTS, DTR, CTS, CDC и RI.
Далее мы разберём пример реализации звукового устройства USB из архива с пакетом драйверов и попытаемся приспособить его к нашей отладочной плате.
Список использованной литературы:
1. Universal Serial Bus Class Definitions for Communications Devices. Revision 1.2 (Errata 1), November 3, 2010
2. Universal Serial Bus Communications Class Subclass Specification for PSTN Devices. Revision 1.2, February 9, 2007
3. Universal Serial Bus Device Class Definition for Audio Devices. Release 1.0, March 18, 1998
4. USB Interface Association Descriptor Device Class Code and Use Model. Revision 1.0, July 23, 2003
© 2026 ООО «МТ ФИНАНС»
Источник: habr.com