Series Overview: This is Part 13 of our 18-part STM32 Unleashed series. We implement a complete USB CDC Virtual COM Port — from USB enumeration and descriptor configuration through bulk data transfer, control request handling, and a production-quality UART-to-USB bridge.
1
Architecture & CubeMX Setup
STM32 family, clock tree, HAL vs LL, CubeMX workflow, first project
Completed
2
GPIO & Button Debounce
GPIO modes, pull-up/down, EXTI, software debounce, HAL_GPIO_ReadPin
Completed
3
UART Communication
Polling, interrupt, DMA modes, printf retargeting, ring buffers
Completed
4
Timers, PWM & Input Capture
TIM basics, PWM generation, input capture, encoder mode
Completed
5
ADC & DAC
Single/continuous conversion, DMA, injected channels, DAC waveforms
Completed
6
SPI Protocol
SPI master/slave, full-duplex, DMA transfers, sensor drivers
Completed
7
I2C Protocol
I2C master, 7/10-bit addressing, DMA, multi-master, error handling
Completed
8
DMA & Memory Efficiency
DMA streams, circular mode, memory-to-memory, zero-copy patterns
Completed
9
Interrupt Management & NVIC
Priority grouping, preemption, ISR design, HAL callbacks, latency
Completed
10
Low-Power Modes
Sleep, Stop, Standby modes, RTC wakeup, LP UART, power profiling
Completed
11
RTC & Calendar
RTC configuration, alarms, backup registers, calendar subseconds
Completed
12
CAN Bus
FDCAN/bxCAN, filters, message frames, error handling, automotive use
Completed
13
USB CDC Virtual COM Port
USB FS/HS, CDC class, virtual serial, control transfers, descriptors
You Are Here
14
FreeRTOS Integration
Tasks, queues, semaphores, mutexes, CMSIS-RTOS2 wrapper, stack sizing
15
Bootloader Development
Custom IAP bootloader, UART/USB DFU, flash programming, jump-to-app
16
External Storage: SD & QSPI Flash
FATFS on SD card, QSPI NOR flash, memory-mapped execution, wear levelling
17
Ethernet & TCP/IP Stack
LwIP integration, DHCP, TCP server, HTTP, MQTT, Ethernet DMA descriptors
18
Production Readiness
Watchdog, HardFault handler, flash option bytes, code signing, CI/CD
USB Fundamentals for STM32
USB (Universal Serial Bus) is a host-controlled, polled bus. Unlike CAN or UART where nodes can transmit at will, USB devices never initiate communication — they can only respond to host requests. Every data transfer on the bus is initiated by the host controller inside the PC, and the device answers within a strict time window. Understanding this host-centric model is essential before writing a single line of USB firmware.
STM32 devices expose USB in two forms. USB Full Speed (FS, 12 Mbit/s) is available on virtually all mid-range STM32 devices as either a standalone USB FS peripheral (STM32F0/G0/L0) or as part of the OTG_FS core (STM32F4/F7/H7). USB High Speed (HS, 480 Mbit/s) requires the OTG_HS core plus an external ULPI PHY or internal FS PHY (limited to 12 Mbit/s despite the HS core name). For CDC applications, FS at 12 Mbit/s provides more than enough bandwidth — the practical throughput ceiling is determined by CDC protocol overhead and PC driver behaviour, not the raw link speed.
USB Descriptor Hierarchy
USB descriptors form a hierarchical configuration tree that the host reads during enumeration. The STM32 USB middleware (located in Middlewares/ST/STM32_USB_Device_Library/) provides default CDC descriptors that you can customise in usbd_desc.c — primarily the VID/PID, manufacturer string, product string, and serial number.
- Device Descriptor — USB version, class codes, max packet size, VID/PID, number of configurations.
- Configuration Descriptor — total length, number of interfaces, power source, maximum current draw.
- Interface Descriptor — CDC uses two interfaces: Control Interface (class 0x02) and Data Interface (class 0x0A).
- Endpoint Descriptor — CDC Data Interface has Bulk IN (device to host) and Bulk OUT (host to device) at 64 bytes max packet size for FS.
- String Descriptors — language ID (0x0409 for English), manufacturer, product, serial. These appear in Device Manager and
/dev/ listings.
| USB Transfer Type |
Max Packet Size (FS) |
Guaranteed Timing |
Typical Use in CDC |
| Control |
8–64 bytes |
Yes (≤10 ms) |
Enumeration, SET_LINE_CODING, GET_LINE_CODING |
| Bulk IN |
64 bytes |
No (best effort) |
Device → PC data (CDC_Transmit_FS) |
| Bulk OUT |
64 bytes |
No (best effort) |
PC → device data (CDC_Receive_FS) |
| Interrupt |
64 bytes |
Yes (per frame) |
CDC notification endpoint (serial state) |
| Isochronous |
1023 bytes |
Yes (per frame) |
Not used in CDC |
The USB enumeration sequence — bus reset, get device descriptor, set address, get full configuration descriptor, set configuration — happens automatically when the device connects. The STM32 USB stack handles all of this internally. Your application code starts running as soon as USBD_CDC_SetTxBuffer and CDC_Receive_FS are available, which occurs after the host sends SET_CONFIGURATION.
/* USB CDC device initialisation — called from main() after clock setup
* File: usb_device.c (CubeMX-generated, add to USER CODE sections)
*/
#include "usb_device.h"
#include "usbd_core.h"
#include "usbd_desc.h"
#include "usbd_cdc.h"
#include "usbd_cdc_if.h"
USBD_HandleTypeDef hUsbDeviceFS;
void MX_USB_DEVICE_Init(void)
{
/* Initialise device library with Full Speed core */
if (USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS) != USBD_OK)
{
Error_Handler();
}
/* Register CDC class */
if (USBD_RegisterClass(&hUsbDeviceFS, &USBD_CDC) != USBD_OK)
{
Error_Handler();
}
/* Register CDC interface callbacks (usbd_cdc_if.c) */
if (USBD_CDC_RegisterInterface(&hUsbDeviceFS, &USBD_Interface_fops_FS)
!= USBD_OK)
{
Error_Handler();
}
/* Connect to host — pull D+ high via internal pull-up */
USBD_Start(&hUsbDeviceFS);
}
CDC Class Architecture
The CDC ACM (Communications Device Class — Abstract Control Model) is the USB class that makes an STM32 appear as a virtual COM port on the host. The OS loads a standard CDC driver (built into Windows 10+, Linux, and macOS) without requiring any custom driver installation — one of CDC's greatest practical advantages over custom HID or vendor-class implementations.
The CDC class uses two USB interfaces:
- Control Interface (Interface 0): carries management traffic over an Interrupt IN endpoint — serial state notifications, flow control signals, and modem emulation. Most applications never need to actively use this endpoint.
- Data Interface (Interface 1): the actual data path — Bulk IN (EP1 IN, STM32 to PC) and Bulk OUT (EP1 OUT, PC to STM32) endpoints, each 64 bytes maximum for FS.
In CubeMX-generated code, the file USB_DEVICE/App/usbd_cdc_if.c is where your application logic lives. It contains three stub functions you must implement:
CDC_Init_FS — called when USB is configured; set up RX buffers here.
CDC_DeInit_FS — called on disconnect; release resources.
CDC_Control_FS — handles control requests (SET_LINE_CODING, DTR/RTS signals).
CDC_Receive_FS — called from USB interrupt when an OUT packet arrives; copy data and call USBD_CDC_ReceivePacket to re-arm.
TX is handled from any context by calling CDC_Transmit_FS(buf, len), which internally calls USBD_CDC_SetTxBuffer + USBD_CDC_TransmitPacket. The critical constraint: you must not call CDC_Transmit_FS while a previous transmission is still in progress — the TX busy flag must be checked first.
/* usbd_cdc_if.c — core CDC callbacks
* Ring buffer for thread-safe delivery to main loop
*/
#include "usbd_cdc_if.h"
#include "ring_buffer.h"
#define APP_RX_RING_SIZE 512U
#define APP_TX_BUF_SIZE 256U
static uint8_t s_usbRxRingMem[APP_RX_RING_SIZE];
static RingBuffer_t s_usbRxRing;
static uint8_t s_usbRxPacketBuf[CDC_DATA_FS_MAX_PACKET_SIZE]; /* 64 bytes */
/* Called when USB is successfully configured */
static int8_t CDC_Init_FS(void)
{
RingBuffer_Init(&s_usbRxRing, s_usbRxRingMem, sizeof(s_usbRxRingMem));
/* Point OUT endpoint buffer at our packet buffer */
USBD_CDC_SetRxBuffer(&hUsbDeviceFS, s_usbRxPacketBuf);
return USBD_OK;
}
/* Called from USB interrupt when an OUT packet arrives */
static int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)
{
/* Copy received bytes into ring buffer (interrupt-safe) */
RingBuffer_WriteBlock(&s_usbRxRing, Buf, (uint16_t)*Len);
/* Re-arm OUT endpoint immediately so we do not NAK the next packet */
USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return USBD_OK;
}
/* Public helper — send up to 512 bytes over CDC with timeout */
HAL_StatusTypeDef CDC_Transmit_FS_Safe(const uint8_t *buf, uint16_t len)
{
USBD_CDC_HandleTypeDef *hcdc =
(USBD_CDC_HandleTypeDef *)hUsbDeviceFS.pClassData;
uint32_t tickStart = HAL_GetTick();
while (hcdc->TxState != 0)
{
if (HAL_GetTick() - tickStart > 10U) /* 10 ms timeout */
{
return HAL_TIMEOUT;
}
}
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, (uint8_t *)buf, len);
USBD_CDC_TransmitPacket(&hUsbDeviceFS);
return HAL_OK;
}
USB CDC Transmit
The STM32 USB stack handles fragmentation of data larger than 64 bytes automatically — you pass a pointer to your complete buffer and the stack schedules multiple USB transactions as needed. However, the transmit path is not re-entrant: while the stack is transmitting one buffer, calling USBD_CDC_TransmitPacket again will silently fail or corrupt state. Always check TxState first.
A zero-length packet (ZLP) is required when the transmitted length is an exact multiple of 64. Without it, the host may wait indefinitely for more data because it cannot distinguish between "64 bytes arrived, more coming" and "64 bytes arrived, transfer complete". The ST CDC middleware handles ZLPs automatically when the total transfer length is a multiple of max packet size — verify this in your BSP version.
For debug logging and human-readable output, a cdc_printf wrapper is far more convenient than manually marshalling buffers. The key is to format into a local stack buffer of adequate size, then pass it to the transmit function with the busy check integrated.
/* cdc_printf — printf-style helper for USB CDC output
* Safe to call from main loop context only (not from USB ISR).
*/
#include <stdarg.h>
#include <stdio.h>
#define CDC_PRINTF_BUF_SIZE 256U
int cdc_printf(const char *fmt, ...)
{
static uint8_t s_txBuf[CDC_PRINTF_BUF_SIZE];
va_list args;
va_start(args, fmt);
int n = vsnprintf((char *)s_txBuf, sizeof(s_txBuf), fmt, args);
va_end(args);
if (n <= 0) return n;
/* Clamp to buffer size */
uint16_t len = (n < (int)sizeof(s_txBuf)) ? (uint16_t)n
: (uint16_t)(sizeof(s_txBuf) - 1);
/* Check USB is actually connected and configured */
if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED)
{
return -1;
}
USBD_CDC_HandleTypeDef *hcdc =
(USBD_CDC_HandleTypeDef *)hUsbDeviceFS.pClassData;
uint32_t t0 = HAL_GetTick();
while (hcdc->TxState != 0)
{
if (HAL_GetTick() - t0 > 5U) return -1; /* timeout */
}
USBD_CDC_SetTxBuffer(&hUsbDeviceFS, s_txBuf, len);
USBD_CDC_TransmitPacket(&hUsbDeviceFS);
return (int)len;
}
/* Example usage in main loop */
void App_PrintStatus(uint32_t adcVal, float tempC)
{
cdc_printf("{\"tick\":%lu,\"adc\":%lu,\"temp\":%.2f}\r\n",
HAL_GetTick(), adcVal, (double)tempC);
}
USB CDC Receive
The CDC receive path has a subtle but critical ownership model. When the USB core delivers a packet, it calls CDC_Receive_FS from the USB interrupt handler with a pointer to the internal endpoint buffer and the byte count. Your callback has a very short window to copy the data before the buffer is reused — do not process commands inside the callback. Copy to a ring buffer, set a flag, or use a queue, then do all processing in the main loop.
After copying, you must call USBD_CDC_ReceivePacket(&hUsbDeviceFS) to re-arm the OUT endpoint. If you forget this call, the USB host will receive a NAK for every subsequent OUT transaction and eventually consider the device stalled. This is the most common CDC receive bug in production code.
The USB callback is called from an ISR with a priority level controlled by USB_OTG_FS_IRQn in your NVIC configuration. If your ring buffer uses a critical section or a mutex, ensure the priority level is compatible. The safest pattern is a lock-free ring buffer with a power-of-2 size and separate head/tail indices, where the ISR writes and the main loop reads.
/* Complete receive pipeline:
* USB ISR → ring buffer → main loop command processor
*/
/* --- main.c / app loop ------------------------------------------------ */
#define CMD_LINE_MAX 128U
static char s_cmdLine[CMD_LINE_MAX];
static uint16_t s_cmdLen = 0;
void App_ProcessUsbRx(void)
{
uint8_t byte;
/* Drain ring buffer one byte at a time in main loop */
while (RingBuffer_ReadByte(&s_usbRxRing, &byte) == RING_OK)
{
if (byte == '\r' || byte == '\n')
{
if (s_cmdLen > 0)
{
s_cmdLine[s_cmdLen] = '\0';
App_ExecuteCommand(s_cmdLine, s_cmdLen);
s_cmdLen = 0;
}
}
else if (s_cmdLen < CMD_LINE_MAX - 1)
{
s_cmdLine[s_cmdLen++] = (char)byte;
}
/* else: overflow — discard byte */
}
}
/* Simple command interpreter */
static void App_ExecuteCommand(const char *cmd, uint16_t len)
{
(void)len;
if (strcmp(cmd, "led on") == 0)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_SET);
cdc_printf("{\"status\":\"ok\",\"led\":\"on\"}\r\n");
}
else if (strcmp(cmd, "led off") == 0)
{
HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, GPIO_PIN_RESET);
cdc_printf("{\"status\":\"ok\",\"led\":\"off\"}\r\n");
}
else if (strcmp(cmd, "version") == 0)
{
cdc_printf("{\"fw\":\"1.0.0\",\"build\":\"" __DATE__ "\"}\r\n");
}
else
{
cdc_printf("{\"status\":\"err\",\"msg\":\"unknown command\"}\r\n");
}
}
Control Requests & Baud Rate Change
The CDC ACM control interface handles a small set of class-specific requests defined in the CDC specification. The most important for firmware developers are:
- SET_LINE_CODING (0x20): the host sends the virtual COM port settings — baud rate (4 bytes), stop bits (1 byte: 0=1 stop, 1=1.5 stop, 2=2 stop), parity (1 byte: 0=none, 1=odd, 2=even), and data bits (1 byte: typically 8). Your
CDC_Control_FS receives this and can reconfigure a physical UART to match if you are building a USB-UART bridge.
- GET_LINE_CODING (0x21): the host asks the device to report its current line coding. Reply with the same struct you stored from the last SET_LINE_CODING.
- SET_CONTROL_LINE_STATE (0x22): the host sets DTR (bit 0) and RTS (bit 1). DTR being set indicates the host application has opened the COM port. This is your connection-detection mechanism — when DTR goes high, the host is ready to receive data.
A critical practical point: many USB-CDC firmware designs send data immediately after enumeration and are surprised that frames are lost. The correct approach is to wait for DTR to be asserted before transmitting — this guarantees the host application is listening.
/* CDC_Control_FS — handle class-specific control requests
* Located in usbd_cdc_if.c, USER CODE section
*/
/* Line coding storage (persists between GET/SET pairs) */
static uint8_t s_lineCoding[7] = {
0x00, 0xC2, 0x01, 0x00, /* 115200 baud (little-endian) */
0x00, /* 1 stop bit */
0x00, /* no parity */
0x08 /* 8 data bits */
};
static uint8_t s_dtrActive = 0; /* Set when host opens the port */
static int8_t CDC_Control_FS(uint8_t cmd, uint8_t *pbuf, uint16_t length)
{
switch (cmd)
{
case CDC_SET_LINE_CODING:
/* Copy the 7-byte line coding structure from host */
memcpy(s_lineCoding, pbuf, sizeof(s_lineCoding));
{
/* Extract baud rate and reconfigure USART2 if bridging */
uint32_t baud = (uint32_t)pbuf[0]
| ((uint32_t)pbuf[1] << 8)
| ((uint32_t)pbuf[2] << 16)
| ((uint32_t)pbuf[3] << 24);
if (baud >= 1200U && baud <= 3000000U)
{
huart2.Init.BaudRate = baud;
HAL_UART_Init(&huart2);
}
}
break;
case CDC_GET_LINE_CODING:
/* Reply with current line coding */
memcpy(pbuf, s_lineCoding, sizeof(s_lineCoding));
break;
case CDC_SET_CONTROL_LINE_STATE:
/* Bit 0 = DTR, Bit 1 = RTS */
s_dtrActive = (pbuf[0] & 0x01) ? 1 : 0;
break;
default:
break;
}
return USBD_OK;
}
/* Check from application code before sending data */
uint8_t CDC_IsHostConnected(void)
{
return (hUsbDeviceFS.dev_state == USBD_STATE_CONFIGURED)
&& s_dtrActive;
}
USB CDC as UART Bridge
A common application for STM32 USB CDC is a transparent UART bridge — the STM32 forwards USB data to a UART peripheral and vice versa. This replaces dedicated USB-UART chips (CP2102, CH340) with a software implementation that also allows baud rate changes under software control, custom flow control, and protocol sniffing.
The critical design challenge is matching the two data rates. USB FS bulk transfer can burst at up to 12 Mbit/s but is frame-limited to 1 ms granularity — data arrives in 64-byte chunks every 1 ms at maximum. UART at 115200 baud produces ~11.5 kByte/s. You need ring buffers on both paths to absorb bursts and avoid dropped bytes.
The recommended approach: use UART DMA in circular mode for the UART RX path (always receiving, never dropping characters), and copy from the DMA buffer to a ring buffer using a DMA half-complete/complete interrupt. The USB TX path drains the ring buffer periodically (every 1 ms via TIM6 or SysTick hook). USB RX data goes directly to UART TX via HAL_UART_Transmit_DMA.
/* USB CDC <-> UART bridge implementation
* USB RX → UART TX (direct)
* UART RX (DMA circular) → ring buffer → USB TX (periodic flush)
*/
#define UART_DMA_BUF_SIZE 256U
#define UART_RING_SIZE 512U
#define USB_TX_FLUSH_MS 2U /* Flush every 2 ms */
static uint8_t s_uartDmaBuf[UART_DMA_BUF_SIZE];
static uint8_t s_uartRingMem[UART_RING_SIZE];
static RingBuffer_t s_uartRxRing;
static uint16_t s_lastDmaPos = 0;
void Bridge_Init(void)
{
RingBuffer_Init(&s_uartRxRing, s_uartRingMem, sizeof(s_uartRingMem));
/* Start UART RX DMA in circular mode */
HAL_UART_Receive_DMA(&huart2, s_uartDmaBuf, UART_DMA_BUF_SIZE);
}
/* Called from SysTick or TIM6 every 1 ms */
void Bridge_PollUartToUsb(void)
{
/* Calculate how many new bytes DMA has written */
uint16_t head = (uint16_t)(UART_DMA_BUF_SIZE
- __HAL_DMA_GET_COUNTER(huart2.hdmarx));
uint16_t newBytes;
if (head >= s_lastDmaPos)
newBytes = head - s_lastDmaPos;
else
newBytes = (uint16_t)(UART_DMA_BUF_SIZE - s_lastDmaPos + head);
if (newBytes == 0) return;
/* Copy new bytes into ring buffer with wrap-around handling */
for (uint16_t i = 0; i < newBytes; i++)
{
uint16_t idx = (s_lastDmaPos + i) % UART_DMA_BUF_SIZE;
RingBuffer_WriteByte(&s_uartRxRing, s_uartDmaBuf[idx]);
}
s_lastDmaPos = head;
/* Flush ring buffer to USB in chunks */
static uint8_t s_usbTxBuf[64];
uint16_t available = RingBuffer_BytesAvailable(&s_uartRxRing);
if (available == 0) return;
uint16_t toSend = (available > sizeof(s_usbTxBuf))
? (uint16_t)sizeof(s_usbTxBuf) : available;
RingBuffer_ReadBlock(&s_uartRxRing, s_usbTxBuf, toSend);
CDC_Transmit_FS_Safe(s_usbTxBuf, toSend);
}
/* USB RX → UART TX (called from CDC_Receive_FS) */
void Bridge_UsbToUart(const uint8_t *data, uint32_t len)
{
/* Non-blocking DMA transmit; for reliability add a TX queue */
HAL_UART_Transmit_DMA(&huart2, (uint8_t *)data, (uint16_t)len);
}
Exercises
Exercise 1
Beginner
USB CDC Echo Device
Configure USB CDC on any STM32 with USB OTG (F4 Nucleo, F7-Discovery, G0, L4 etc.) using CubeMX. Connect to a PC. Open a terminal (PuTTY on Windows, minicom on Linux). Send any character string — the device must echo it back with a prefix showing tick count: [1234] echo: hello. Verify Windows Device Manager shows "USB Serial Device (COMx)" or ls /dev/ttyACM* shows the device on Linux. Confirm that unplugging and reconnecting re-enumerates without a reset.
USB CDC
Enumeration
CubeMX
Exercise 2
Intermediate
JSON Command Interpreter over USB CDC
Build a USB CDC command interpreter that accepts text commands terminated by newline: led on, led off, adc read, version. Reply with JSON responses: {"status":"ok","led":"on"}, {"adc":2048,"voltage":1.65}. Write a Python test script using pyserial that opens the COM port, sends each command, parses the JSON reply, and validates the response structure. Achieve 100 round-trip transactions per second. Measure latency using Python timestamps and verify <5 ms average response time.
CDC Command Parser
pyserial
JSON Protocol
Exercise 3
Advanced
High-Speed ADC Streaming Benchmark
Implement continuous ADC sample streaming over USB CDC. Configure ADC1 in DMA circular mode at 100 kSps (12-bit). Pack samples in 64-byte blocks and transmit each block immediately via CDC_Transmit_FS_Safe. On the PC, write a Python script using pyserial with read(timeout=1) in a tight loop that counts received bytes per second. Measure achieved throughput in bytes/second and samples/second. Compare to theoretical maximum (12 Mbit/s = 1.5 MB/s for FS, but CDC overhead reduces this). Identify whether the bottleneck is STM32 ADC+DMA, STM32 USB scheduling, USB frame timing (1 ms), or Python receive rate. Document your findings.
ADC DMA
USB Throughput
Python Benchmark
Performance Profiling
USB CDC Config Tool
Use this tool to document your USB CDC peripheral configuration — peripheral selection, buffer sizes, descriptor values, and design notes. Download as Word, Excel, PDF, or PPTX for project documentation and team reviews.
Conclusion & Next Steps
In this article we implemented a complete USB CDC Virtual COM Port on STM32:
- USB fundamentals: host-controlled polled bus model, Full Speed vs High Speed, descriptor hierarchy, and the five transfer types — with CDC using Control, Bulk IN, Bulk OUT, and Interrupt endpoints.
- CDC class architecture: two USB interfaces (Control + Data), the four CDC callback functions in
usbd_cdc_if.c, and the TX busy state machine that prevents re-entrant transmissions.
- Reliable transmit:
cdc_printf wrapper with TxState busy check, connection detection via DTR, and zero-length packet (ZLP) semantics for multiples of 64 bytes.
- Safe receive: interrupt-safe ring buffer pattern, mandatory re-arming of the OUT endpoint via
USBD_CDC_ReceivePacket, and main-loop command processing.
- Control requests: implementing
SET_LINE_CODING and SET_CONTROL_LINE_STATE — the only two CDC control requests a typical application must handle.
- UART bridge: combining UART DMA circular mode with USB CDC bulk transfer to build a transparent, high-throughput USB-UART bridge without dedicated silicon.
Next in the Series
In Part 14: FreeRTOS Integration, we introduce real-time task scheduling to the STM32 — covering task creation, inter-task queues, binary and counting semaphores, mutexes, and the CMSIS-RTOS2 wrapper that bridges FreeRTOS with the rest of the STM32 middleware stack including USB.
Related Articles in This Series
Part 14: FreeRTOS Integration
Task creation, inter-task queues, semaphores, mutexes, CMSIS-RTOS2 wrapper, and integrating USB CDC with a real-time scheduler.
Read Article
Part 3: UART Communication
Polling, interrupt, and DMA UART modes, ring buffers, and baud rate calculation — the foundation for the USB CDC UART bridge pattern.
Read Article
Part 15: Bootloader Development
Custom IAP bootloader, USB DFU class implementation, flash programming, and application jump — building on the USB CDC foundation.
Read Article