
В STM32 через USB интерфейс можно настроить последовательный COM порт. В этом тексте я расскажу как это сделать.
В чем проблема?
Как устроена жизнь сейчас? Существует целая куча учебно-треннировочных электронных плат с STM32 и торчащим наружу USB интерфейсом. Вот взять хоть PCB JZ-F407VET6. При этом при отладке прошивок нужна UART CLI. Это требует отдельного переходника с USB на UART, удобный PLD разъём и проводные перемычки. Понятное дело, что всех этих примочек не всегда оказывается под рукой . Из-за этого затруднена и осложнена отладка прошивки.
Постановка задачи:
Реализовать USB Virtual Com Port . Наладить двусторонний обмен данными через TeraTerm между PC и STM32 по USB. Надо сделать так, чтобы при соединении электронной платы с STM32 и PC по USB lapTop-PC увидел на своей стороне в диспетчере устройств виртуальный последовательный порт.
Теоретический минимум
Прерывание (Interrupt, Trap) — событие в микропроцессоре, которое провоцирует смену контекста. Т. е. замену значений регистров ядра процессора (старые значения запоминаются в стек) и вызов отдельной функции ISR. После исполнения функции ISR управление возвращается обратно в прерванную функцию main() со старыми значениями регистров процессора, которые временно хранятся в стековой RAM памяти. Как правило, прерывания используются для работы с внешними периферийными для процессора устройствами : аппаратные таймеры, трансиверы UART, USB, CAN, подсистемы DMA и пр.
FIFO - абстрактная структура данных, которая позволяет добавлять и извлекать элементы по принципу первый пришел первый ушел.
Суперцикл — тот код, который бесконечно снова и снова исполняется внутри бесконечного цикла while(1) внутри функции main()
Race Condition (состояние гонки ) - ошибка проектирования программы с прерываниями , при которой работа приложения зависит от того, в каком порядке выполняются части кода. Это когда прерывание и основной поток берут и пишут одну и ту же переменную в RAM памяти. Результат - непредсказуемое значение.
Критическая секция — участок кода, выполнение которого не должно быть прервано прерываниями процессора. Перед критической секцией прерывания просто отключаются. После критической секции - снова включаются.
Реализация
--Настроить тактирование PLL от кварцевого резонатора

--Настроить PLL так чтобы на USB поступало 48 MHz.

Существует не так уж и много комбинаций настроек PLL при которых на USB шине образуются нужные для работы 48 MHz. Вот они все перед Вами.
pll_calc_stm32_all 25000000 1000000 I,[PLL],OSC_FREQUENCY:25000000 Hz I,[PLL],sys_freq_hz_step:1000000 Hz HSE:25000000 Hz,SYSCLK:24000000 Hz:M:25,N:144,P:6,Q: 3,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:30000000 Hz:M:20,N:192,P:8,Q: 5,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:32000000 Hz:M:25,N:192,P:6,Q: 4,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:36000000 Hz:M:25,N:144,P:4,Q: 3,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:40000000 Hz:M:20,N:192,P:6,Q: 5,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:42000000 Hz:M:25,N:336,P:8,Q: 7,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:48000000 Hz:M:25,N:192,P:4,Q: 4,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:54000000 Hz:M:25,N:432,P:8,Q: 9,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:56000000 Hz:M:25,N:336,P:6,Q: 7,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:60000000 Hz:M:20,N:192,P:4,Q: 5,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:64000000 Hz:M:25,N:384,P:6,Q: 8,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:72000000 Hz:M:25,N:144,P:2,Q: 3,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:84000000 Hz:M:25,N:336,P:4,Q: 7,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:96000000 Hz:M:25,N:192,P:2,Q: 4,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:108000000 Hz:M:25,N:432,P:4,Q: 9,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:120000000 Hz:M:20,N:192,P:2,Q: 5,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:144000000 Hz:M:25,N:288,P:2,Q: 6,Error: 0 Hz, HSE:25000000 Hz,SYSCLK:168000000 Hz:M:25,N:336,P:2,Q: 7,Error: 0 Hz,
--Подать тактирование на GPIO ассоциированные с USB. Для порта USB-Full Speed это PortA.
--Настроить GPIO пины на USB на альтернативную функцию с номером PinMux=10
GPIO | Function | PinMux | dir | Pull |
PA11 | OTG_FS_DM | 10 | io | Air |
PA12 | OTG_FS_DP | 10 | io | Up |
--Подать тактирование на подсистему USB OTG_FS
--Проинициализировать USB-FS порт.

--В выборе типа USB устройства выбрать Communication Device Class (Virtual Port Com)

--Настройки дескриптора оставить по умолчанию.

--Определить и написать обработчик прерываний по USB.
void OTG_FS_IRQHandler(void) { UsbHandle_t* Node = UsbGetNodeBySpeed(USB__SPEED_FS); if(Node) { HAL_PCD_IRQHandler(&Node->PcdHandle); } }
--Активировать прерывания для USB

--Определить приоритет прерываний по USB
Весь необходимый системный код из SDK в общем-то генерирует STM CubeMX. Далее можно лишь прокомментировать некоторые прикладные моменты.
Драйвер в Windows 10
В случае успешной сборки прошивки Win10 увидит драйвер как "Устройство и последовательным интерфейсом USB (COM3) "

Корректная прошивка отображается в диспетчере задач с VID=05E3; PID=0610

При подключении через виртуальный COM порт настройки битовой скорости не имеют никакого влияния. В TeraTerm можно выбирать любую битовую скорость и данные всё равно будут поступать корректно.
Прием байтов
Когда TeraTerm отправляет текст, то в прошивке вызывается функция
int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len). Вы можете ее заполнить, как вам угодно.
/* Data received over USB OUT endpoint are sent over CDC interface through this function. This function will issue a NAK packet on any OUT packet received on USB endpoint until exiting this function. If you exit this function before transfer is complete on CDC interface (ie. using DMA controller) it will result in receiving more data while previous ones are still not sent. Buf: Buffer of data to be received Len: Number of data received (in bytes) retVal Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ const InterfaceType_t interface_if = {.num = 1,.interface_name = INTERFACE_NAME_USB, }; static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t* Len) { writer_interface_set(interface_if); int8_t status = USBD_FAIL; bool res = false; (void) res; LOG_PARN(USB_SERIAL, "RxData:%c,Len:%u", Buf, Len); UsbHandle_t *Node = UsbGetNode(USB_DEVICE_NUM); if (Node) { Node->rx_len = *Len; if( (*Len) <= sizeof(Node->RxData) ) { memcpy(Node->RxData, Buf, *Len); } #ifdef HAS_STRING_READER uint32_t i = 0; for(i=0; i<Node->rx_len; i++) { res = string_reader_rx_byte(interface_if, Buf[i]); } #endif USBD_CDC_SetRxBuffer(&Node->hUsbDevice, &Buf[0]); USBD_CDC_ReceivePacket(&Node->hUsbDevice); status = USBD_OK; } return status; }
в аргументах и будут прописаны принятые с улицы байты. Вызывается эта функция CDC_Receive_FS из обработчика прерываний по USB.

Отправка байтов
Отправка производится функцией CDC_Transmit_FS
/* Data to send over USB IN endpoint are sent over CDC interface through this function. tx_data: Buffer of data to be sent size: Number of data to be sent (in bytes) */ bool usb_serial_send(uint8_t num , const uint8_t* const tx_data, const uint16_t size) { bool res = false ; UsbHandle_t *Node = UsbGetNode(num); if (Node) { USBD_StatusTypeDef status=USBD_FAIL; status = CDC_Transmit_FS(tx_data, size); res = usb_device_status_to_res(status); if(res) { Node->tx_in_progress = true; Node->tx_start_ms = time_get_ms32(); Node->tx_done = false; LOG_PARN(USB_SERIAL, "%u,TxData:%s",num,ArrayToStr(tx_data,size) ); }else{ LOG_ERROR(USB_SERIAL, "%u,Status:%u=%s",num,status,UsbDeviceErrToStr(status) ); } } return res; }
После успешной отправки происходит вызов callBack функции CDC_TransmitCplt_FS
/* CDC_TransmitCplt_FS Data transmitted callback This function is IN transfer complete callback used to inform user that the submitted Data is successfully sent over USB. Buf: Buffer of data to be received Len: Number of data received (in bytes) retVal Result of the operation: USBD_OK if all operations are OK else USBD_FAIL */ static int8_t CDC_TransmitCplt_FS(uint8_t* Buf, uint32_t* Len, uint8_t epnum) { uint8_t result = USBD_OK; UNUSED(Buf); UNUSED(Len); UNUSED(epnum); UsbHandle_t *Node = UsbGetNode(USB_DEVICE_NUM); if (Node) { Node->tx_in_progress = false ; Node->tx_done = true; Node->tx_cnt++; bool res = false; uint32_t out_len = 0; res = fifo_pull_array(&Node->TxFifo, Node->TxBuff, sizeof(Node->TxBuff), &out_len); if (res) { if(out_len){ res = usb_serial_send(1, Node->TxBuff, out_len); result = usb_device_res_to_ret(res); } } } return result; }
Функция CDC_TransmitCplt_FS тоже вызывается из прерывания по USB

Алгоритмическая часть
Вот мы настроили USB serial трансивер. Но есть проблема. Как писать логи до инициализации USB serial трансивера? Как писать логи в инициализации тактирования, GPIO? Выход есть. Я завожу очередь на отправку байтов TxFIFO. Каждый раз, когда надо что-то отправить в USB, я буду просто складировать байты в очередь TxFIFO. Как выстрелит функция CDC_TransmitCplt_FS, программа извлечет из TxFIFO еще порцию из N байт и тут же в прерывании по USB инициирует очередную отправку. При вызове usb_send байты складировать в очередь TxFIFO. Отправлять байт в прерывании по окончанию отправки байта. По сути отправка происходит в прерывании USB. Но это даже и не отправка, а отмашка на отправку. Пока байт отправляется процессор преспокойно себе возвращается в main() и делает какую-то по-настоящему нужную работу.
При старте суперцикла, драйвер USB посмотрит на размер TxFiFo. Если трансивер не отправляет и в очереди что-то есть то тут же иницировать начальную отправку.
При этом TxFIFO придется инициировать до запуска функции main заполнив структуру прямо при инициализации.
В результате Вы можете пользоваться логированием до инициализации USB serial трансивера.
На время заполнения usb tx fifo следует отключить прерывания глобально. Создать критическую секцию. Иначе одновременная запись очереди где-то в main и чтение этой же очереди из прерывания по окончанию отправки повредят переменные и данные очереди. На выходе вы увидите испорченный лог. Оно Вам надо?
Если же очередь отправки TxFIFO была некоторое время пуста, как мы поймём, что в ней появились новые данные? Очень просто. Для этого есть периодическая проверка usb_serial_proc_one в суперцикле.
/*can be called form ISR TODO rename to usb_serial_tx_next */ bool usb_serial_tx_next(const uint8_t num) { bool res = false; UsbHandle_t *Node = UsbGetNode(num); if (Node) { if (Node->init) { uint32_t outLen = 0; res = fifo_pull_array(&Node->TxFifo, Node->TxBuff, sizeof(Node->TxBuff), &outLen); if (res) { res = usb_serial_send(num, Node->TxBuff, outLen); } } } return res; } bool usb_serial_proc_one(const uint8_t num) { bool res = false; LOG_PARN(USB_SERIAL, "Proc:%u", num); UsbHandle_t *Node = UsbGetNode(num); if (Node) { if(false==Node->tx_in_progress) { uint32_t cnt = fifo_get_count(&Node->TxFifo); if(cnt) { res = usb_serial_tx_next(num); } } } return res; }
Вот и получилось запустить Виртуальную консоль поверх USB-FS. Теперь можно выбросить в сторону переходник с USB на UART и отлаживать код прямо по USB.
Итог
Удалось научиться отправлять и принимать байты по виртуальному COM порту, который организован поверх USB - Full Speed шины. Это открывает возможность запускать CLI на PCB из которых торчит только USB интерфейс.
Акронимы
Сокращение | Расшифровка |
CDC | Communication Device Class |
VCP | virtual COM port |
USB FS | Universal Serial Bus Full Speed |
MCU | Micro Controller Unit |
USB | Universal Serial Bus |
STM32 | STM 32Bit |
Ссылки
Название | URL |
Виртуальный COM-порт на STM32 или как управлять контроллером через USB @Hikarinaka | |
STM32 USB_Device(CDC_Standalone) | https://en.stamssolution.com/stm32-usb_devicecdc_standalone/ |
STM32 и USB. Реализация USB Virtual COM Port. |