STM32 USB CDC Tutorial: Create a Virtual COM Port on the Blue Pill (2026)

Quick answer: The STM32F103C8PriceAliExpress price Blue PillPriceAliExpress price can appear on Windows, Linux or macOS as a USB CDC-ACM virtual serial port without an external USB-to-UART converter. Its USB full-speed device signals are PA11 = USB D− and PA12 = USB D+. In STM32CubeMX, configure a valid 48 MHz USB clock, enable USB Device FS, add the USB_DEVICE → Communication Device Class (CDC) middleware, generate the project and flash it using ST-LINKPriceAliExpress price over SWD. The application can then send text through CDC_Transmit_FS() and receive bytes through the generated CDC_Receive_FS() callback. A working USB cable and a correctly implemented D+ pull-up are essential.

In this esp32.co.uk/ tutorial, we go beyond making a COM port appear. We separate hardware wiring, USB enumeration and application data transfer; build a complete greeting and echo application; explain transmit-busy handling and receive buffering; and troubleshoot everything from an absent COM port to a stalled USB endpoint. The instructions target the classic STM32F103C8T6 Blue Pill with STM32CubeMX, STM32CubeIDE 2.x, the STM32F1 HAL and ST’s classic USB device library. USB support and code-generation names may vary with the STM32Cube package version. The examples have been checked against documentation and API structure, not physically bench-tested.

STM32F103C8 Blue Pill with Micro-USB data cable, ST-LINK and laptop on warm maker workbench for USB CDC virtual COM port
A Blue Pill with USB CDC firmware can appear directly as a virtual COM port over native USB PA11/PA12.

What USB CDC actually does

A conventional serial port transmits asynchronous bits over TX and RX lines. A USB CDC-ACM virtual COM port instead presents a USB interface that the operating system treats like a serial terminal. The PC opens COMx, /dev/ttyACM* or /dev/cu.usbmodem*, while the STM32 firmware exchanges USB packets over D+ and D−. No UART start bit, stop bit or hardware baud clock is present on those USB wires.

That distinction matters because beginners often confuse three separate items: the Blue Pill’s built-in USB device peripheral, a USB-to-UART converter such as CP2102, and a factory USB bootloader. The F103C8 has the first, usually has neither of the other two, and does not acquire them just because the Micro-USB connector provides power. To use the USB peripheral as a COM port, it must run suitable application firmware. To install that firmware on a fresh board, use SWD or the factory USART1 ROM loader.

USB CDC is particularly useful for diagnostics, configuration menus, data logging to a host, command consoles, simple instrument links and communication with Python programs. In comparison with a UART adapter it saves hardware, but it also introduces USB clocks, host enumeration, middleware state and packet-buffer handling that a three-wire TTL UART does not require.

Feature Native STM32 USB CDC USART1 with USB-UART adapter
MCU signals PA11 D−, PA12 D+ PA9 TX, PA10 RX
External converter Not required Required at PC side
Host device type USB CDC ACM, virtual COM port USB serial bridge COM port
USB clock in MCU Precise 48 MHz Not required for USART alone
Firmware support USB device stack and CDC class HAL UART or equivalent
Terminal baud setting Usually advisory line-coding metadata Controls actual UART bit timing
Fresh F103 ROM firmware loading Not via factory USB DFU USART1 ROM loader available
Typical first-time debugging SWD, host USB device list and CDC messages USB adapter and crossed serial wires

An important limitation is that CDC virtual serial is not a promise of deterministic timing. The USB host schedules transactions, the device stack buffers data, and the operating system can pause or coalesce reads. Use timer input capture for precise edges and a hardware UART for a direct serial timing interface where appropriate.

Hardware: the exact Blue Pill USB signals

The standard STM32F103C8 peripheral includes a USB 2.0 full-speed device-only interface. On the typical Blue Pill, the Micro-USB connector feeds the MCU data lines and supplies the board regulator. This is a target USB device port, not the USB socket on an ST-LINK adapter.

USB or power net F103C8 MCU signal Practical check
USB data minus PA11, USB_DM Goes to Micro-USB D− through PCB routing
USB data plus PA12, USB_DP Goes to Micro-USB D+ and external full-speed attach pull-up
USB connector 5 V Board’s VBUS / 5 V rail Normally feeds onboard 3.3 V regulator, board-dependent
Device logic supply MCU VDD, typically 3.3 V Must remain within datasheet supply limits
USB and MCU ground GND Common reference across host and target
Debug data PA13 SWDIO, PA14 SWCLK External ST-LINK, separate from USB D+/D−

Do not connect USB 5 V directly to PA11, PA12 or the 3V3 rail. Use the actual board schematic before mixing external 5 V and USB power. Cheap Blue Pill revisions often lack protection against backfeeding between the USB 5 V connection and other power inputs. For a first test, power the board through one known USB source and connect SWD ground plus target-voltage reference without injecting a second supply.

A full-speed USB device signals attachment by pulling D+ up to 3.3 V through approximately 1.5 kΩ. On F103 USB designs this is an external pull-up, unlike some later STM32 families. On certain inexpensive Blue Pill variants, the part frequently identified as R10 is 10 kΩ or 4.7 kΩ instead. Those values may cause inconsistent or failed enumeration. The actual resistor designation is PCB-revision dependent: inspect the schematic or measure the D+ pull-up circuit before any modification. A wrong resistor is a board-assembly defect and is not proof that the microcontroller itself is counterfeit.

STM32F103C8 PA11 USB D minus PA12 USB D plus with 1.5 kilohm D plus resistor to 3.3 V, USB VBUS to board regulator and common ground
The STM32F103C8 uses PA11 for USB D−, PA12 for D+, and an external approximately 1.5 kΩ D+ pull-up to 3.3 V.

USB cables also deserve suspicion. A charge-only Micro-USB cable will happily light the Blue Pill power LED while delivering no data connection. Use a known-good data cable and connect directly to the PC before trying drivers, reprogramming or solder rework. A visibly powered device and a successfully enumerated USB device are different observations.

Equipment and software

For the reproducible demonstration use a genuine or documented compatible STM32F103C8 Blue Pill with an 8 MHz HSE crystal, a data-capable Micro-USB cable, an ST-LINK probe with verified SWD pinout, and a Windows, Linux or macOS computer. A logic analyser or oscilloscope is optional; the first successful test should need neither. The exact ST-LINK connector depends on the probe, particularly if it is a clone dongle.

Install STM32CubeMX, the matching STM32CubeF1 firmware package, STM32CubeIDE 2.x, and STM32CubeProgrammer where needed. As in our STM32CubeIDE setup guide, newer CubeIDE 2.x versions use a separate CubeMX configuration and generation workflow: create the project with CubeMX and import it into the IDE. Choose matching package releases instead of blindly mixing examples from very old tutorials with new generated files.

The host needs a serial terminal such as PuTTY, Tera Term, minicom, screen or a Python script using pyserial. A serial terminal is only useful after the board has enumerated as CDC. If Device Manager or the USB device list cannot see a proper USB device at all, terminal settings are not the first problem to solve.

The required 48 MHz USB clock

The F103 USB controller requires an accurately derived 48 MHz clock. Ordinary GPIO code may work with a misconfigured system clock, making USB clock errors look like a mysterious hardware problem. On the common Blue Pill with an 8 MHz HSE crystal, a straightforward 72 MHz core and 48 MHz USB configuration is:

Clock stage Example value Explanation
HSE crystal 8 MHz Confirm the actual component fitted to the board
PLL source HSE Use stable external clock reference
PLL multiplier ×9 8 × 9 = 72 MHz PLLCLK
AHB prescaler ÷1 72 MHz HCLK/SYSCLK
APB1 prescaler ÷2 36 MHz PCLK1, within F1 limit
APB2 prescaler ÷1 72 MHz PCLK2
USB prescaler ÷1.5 72 ÷ 1.5 = 48 MHz USBCLK

This is an example for verified 8 MHz HSE hardware, not a universal setting for every STM32 board. Check the crystal marking and STM32CubeMX clock view. The internal HSI oscillator is convenient for non-USB tests but is generally not a suitable accurate clock source for this particular F103 USB setup without additional provisions. Other STM32 families may have independent calibrated 48 MHz oscillators; do not carry their clock recipes across without checking the device reference manual.

An apparently correct 48 MHz number in the configurator is not enough if the physical crystal fails to start. If USB remains undetected while the application runs, verify oscillator startup status, power rails, the chosen clock source and the resulting RCC configuration before trusting the value shown in a tutorial screenshot.

STM32F103 USB clock configuration from 8 MHz HSE multiplied by nine to 72 MHz and divided by one and a half for 48 MHz USB
For a Blue Pill with verified 8 MHz HSE: PLL ×9 gives 72 MHz and USB prescaler ÷1.5 produces 48 MHz.

Create the project in STM32CubeMX

Open CubeMX, select the STM32F103C8Tx part (or the exact valid variant that matches the board), and create a project. For the primary USB test, keep the MCU configuration minimal: USB CDC, debug support and the right clock. Additional sensors, timers and CAN configurations only make initial fault isolation harder.

In System Core → SYS, select Serial Wire debug. This leaves PA13 and PA14 available for SWD so USB development can be recovered without relying on working USB firmware. In System Core → RCC, enable the HSE crystal oscillator and confirm the correct frequency in Clock Configuration. Set the 72/48 MHz plan above, or another datasheet-valid HSE-derived 48 MHz plan.

Under Connectivity → USB, enable the full-speed device interface. The F103 uses its USB device peripheral, not an OTG host configuration. Where CubeMX offers the USB_DEVICE middleware, choose Communication Device Class (CDC). CubeMX should assign PA11/PA12 to USB, generate a USB device stack and enable the appropriate USB low-priority interrupt integration. Confirm that the middleware actually generates a CDC interface file rather than only a low-level peripheral driver.

In Project Manager, set a stable project name such as F103_BluePill_USB_CDC, choose the STM32CubeIDE toolchain, install the required package if prompted, and generate the code. Import the project using CubeIDE’s existing-project import workflow. Preserve custom code inside designated USER CODE blocks as much as possible, and maintain a version-controlled copy of the .ioc configuration. Some generated function names or file paths can change with packages; the principles and HAL/USB API roles remain the same.

CubeMX section Target setting Why
MCU STM32F103C8Tx Right flash, memory and USB peripheral
SYS Debug: Serial Wire Retain PA13/PA14 for SWD recovery
RCC External crystal HSE Accurate 8 MHz reference on common Blue Pill
Clock tree SYSCLK 72 MHz / USB 48 MHz Required full-speed USB clock
Connectivity USB Device FS enabled Enables PA11/PA12 USB controller
Middleware USB_DEVICE, CDC class Produces CDC interface and descriptors
NVIC USB low-priority interrupt handling Host control and data transfers
Project manager STM32CubeIDE Generates importable project

If the USB_DEVICE class menu is missing, first verify that the STM32CubeF1 firmware package and device were selected correctly. Newer STM32 lines can have different USB middleware offerings, but this example is for the classic F1 family. Avoid manually copying arbitrary USB library files into a project until you understand the package/version mismatch.

Understand the generated USB files

CubeMX-generated F1 projects typically contain a USB_DEVICE directory with the class interface and initialization, along with the low-level glue connecting the STM32 USB peripheral control driver to the USB device middleware. File names commonly include:

Generated component Responsibility
usb_device.c / .h Initializes device stack and registers CDC interface
usbd_cdc_if.c / .h Application-facing CDC transmit, receive and control callbacks
usbd_desc.c / .h USB device strings and descriptors
usbd_conf.c Low-level PCD-to-USB middleware integration
usbd_cdc.c STM32 USB middleware CDC class logic
main.c Application initialization and foreground logic
stm32f1xx_it.c Generated IRQ entry points; leave USB handler wiring intact

The important division is that CDC_Transmit_FS() submits application data for USB IN transfer, while CDC_Receive_FS() is called when the USB host sends data to the device. FS means full-speed, not a hardware USART number. USB_DEVICE and the generated MX_USB_DEVICE_Init() call must execute after the MCU and relevant clock infrastructure have been initialized. Normally the code generator already places it in the correct startup sequence.

The host and device move USB packets, not newline-delimited text. A single PC write can arrive split into several USB packets or several writes may be coalesced. Application code must cope with arbitrary chunk sizes instead of assuming that one callback equals one command line.

First working example: periodic CDC greeting

Start with a minimal application that proves enumeration and data transmission before adding a receive queue. Place the following include inside /* USER CODE BEGIN Includes */ in Core/Src/main.c:

#include "usbd_cdc_if.h"

After MX_USB_DEVICE_Init() is called in the generated startup, use a stable string buffer and a periodic transmission in the foreground loop. The following belongs inside /* USER CODE BEGIN WHILE */ and the corresponding USER CODE section in the generated loop:

static uint8_t greeting[] = "STM32 USB CDC online\r\n";
uint32_t last_send = HAL_GetTick();

while (1)
{
    if ((uint32_t)(HAL_GetTick() - last_send) >= 1000U)
    {
        last_send = HAL_GetTick();
        /* BUSY before enumeration or during a pending IN transfer is normal. */
        (void)CDC_Transmit_FS(greeting, (uint16_t)(sizeof(greeting) - 1U));
    }

    /* USER CODE BEGIN 3 */
    /* Additional non-blocking application processing goes here. */
    /* USER CODE END 3 */
}

greeting has static storage duration. That matters because the USB middleware may retain the pointer until the asynchronous USB IN transfer completes. Do not pass a temporary stack buffer that is overwritten immediately after scheduling a transfer. The call returns USBD_OK, USBD_BUSY or another result as defined by the middleware; the demo tolerates an occasional busy return rather than blocking in a loop. For high-rate logging, we will use explicit buffering and completion management instead of silently dropping messages.

Important code-generation placement: Keep the skeleton generated by CubeMX. Do not duplicate while (1) or remove MX_USB_DEVICE_Init(); the example replaces the body of the generated while section only. The exact USER CODE labels vary by tool release. If a pre-existing while (1) already exists, insert only the timing logic and its greeting/last_send declarations in appropriate preserved sections.

Build and download the firmware with ST-LINK over SWD. Reset or resume the target, attach the Micro-USB data cable and observe the operating-system USB device list. The PC should enumerate a CDC device; open the corresponding new virtual COM port. After the firmware runs, the terminal should display one greeting approximately every second. This is an expected result to check on hardware, not a claimed measurement from our bench.

Find the CDC port on Windows, Linux and macOS

Windows 10 and 11: Open Device Manager and check Ports (COM & LPT) after connecting the device. A standards-compliant CDC-ACM interface is generally supported by the built-in usbser.sys driver. You may see “USB Serial Device (COM7)” or another port number. An invalid device descriptor is not repaired by manually assigning a COM-port driver, because the host has not successfully enumerated the USB device.

Linux: Run dmesg -w in one terminal, connect the board, and inspect the new device with lsusb. A USB CDC ACM device commonly appears as /dev/ttyACM0 or another ttyACM instance; a USB-UART bridge often creates /dev/ttyUSB0 instead. If the device is listed but permissions block access, check distribution-specific serial groups or another process that may have opened the port.

macOS: Check System Information → USB and inspect /dev/cu.usbmodem* entries. Use the cu device for an outgoing terminal connection where appropriate. The exact device path changes with the USB descriptor strings, connected devices and operating-system state.

For USB CDC, selecting 115200 8N1 in a terminal is a familiar convention but does not set the PA11/PA12 USB bus bit rate. The host sends CDC line-coding metadata that firmware could use to configure a separate UART bridge, but this direct CDC greeting application does not use any hardware USART. If a host terminal opens the port at 9600 or 115200, the USB packets remain full-speed USB transactions. Do not diagnose a USB enumeration failure by changing virtual COM baud values.

Echo incoming characters without blocking the USB callback

The next example shows a genuinely useful distinction: receiving data in a USB callback does not mean you should perform a long, blocking transmission inside that callback. The host can send bursts; a transmit endpoint can already be busy; and the USB stack may call your receive function again while the foreground still has data to process.

For a small educational application, use a single-producer/single-consumer 256-byte RX FIFO. The USB receive callback copies received bytes into the FIFO and promptly rearms the USB OUT endpoint. The main loop pulls the next chunk, submits it for echo, and keeps the TX buffer unchanged until the previous transfer has finished. This design is more reliable than calling CDC_Transmit_FS(Buf, *Len) unconditionally from the receive callback.

In USB_DEVICE/App/usbd_cdc_if.c, keep the generated includes and middleware callback table. Place the following near the other user declarations (and inside a preserved USER CODE section), with usbd_cdc.h available through the generated headers:

#define CDC_RX_FIFO_SIZE 256U
#define CDC_TX_CHUNK_SIZE 64U

static uint8_t cdc_rx_fifo[CDC_RX_FIFO_SIZE];
static volatile uint16_t cdc_rx_head = 0U; /* Written in USB RX context */
static volatile uint16_t cdc_rx_tail = 0U; /* Written in foreground */
static volatile uint32_t cdc_rx_dropped = 0U;

static uint8_t cdc_tx_chunk[CDC_TX_CHUNK_SIZE];
static uint8_t cdc_tx_inflight = 0U;

static void CDC_PushRxByte(uint8_t value)
{
    uint16_t next = (uint16_t)((cdc_rx_head + 1U) % CDC_RX_FIFO_SIZE);
    if (next == cdc_rx_tail)
    {
        cdc_rx_dropped++;
        return;
    }

    cdc_rx_fifo[cdc_rx_head] = value;
    cdc_rx_head = next;
}

Replace only the USER CODE region in the generated CDC_Receive_FS() with the following logic, keeping the generator’s function signature and its original return structure. A typical F1 USB device class uses hUsbDeviceFS:

/* Inside generated static int8_t CDC_Receive_FS(uint8_t *Buf,
 *                                               uint32_t *Len) */
for (uint32_t i = 0U; i < *Len; ++i)
{
    CDC_PushRxByte(Buf[i]);
}

USBD_CDC_SetRxBuffer(&hUsbDeviceFS, Buf);
(void)USBD_CDC_ReceivePacket(&hUsbDeviceFS);
return (USBD_OK);

This handler does only short, bounded work. USBD_CDC_ReceivePacket() rearms the OUT endpoint for additional host data. Do not leave the callback waiting for the PC terminal or repeatedly calling a busy transmitter. Because the ring buffer is finite, bursts faster than the foreground consumes them can still cause overflow; cdc_rx_dropped records that condition for debugging. If you need guaranteed delivery, add flow control at the application protocol level or a larger properly designed queue.

Now add a small foreground function to the same file, declared in usbd_cdc_if.h inside a preserved USER CODE section as void CDC_EchoTask(void);. The implementation below checks whether a previous asynchronous transmission has finished before touching its static TX buffer again:

void CDC_EchoTask(void)
{
    USBD_CDC_HandleTypeDef *hcdc =
        (USBD_CDC_HandleTypeDef *)hUsbDeviceFS.pClassData;
    if (hUsbDeviceFS.dev_state != USBD_STATE_CONFIGURED || hcdc == NULL)
    {
        return; /* PC not fully connected/enumerated */
    }

    if (cdc_tx_inflight != 0U)
    {
        if (hcdc->TxState != 0U)
        {
            return; /* Previous buffer still belongs to USB driver */
        }
        cdc_tx_inflight = 0U;
    }

    uint16_t cursor = cdc_rx_tail;
    uint16_t count = 0U;
    while (cursor != cdc_rx_head && count < CDC_TX_CHUNK_SIZE)
    {
        cdc_tx_chunk[count++] = cdc_rx_fifo[cursor];
        cursor = (uint16_t)((cursor + 1U) % CDC_RX_FIFO_SIZE);
    }

    if (count > 0U &&
        CDC_Transmit_FS(cdc_tx_chunk, count) == USBD_OK)
    {
        cdc_rx_tail = cursor;
        cdc_tx_inflight = 1U;
    }
}

Call CDC_EchoTask() repeatedly from the generated while (1) loop in main.c, after USB initialization. Keep the function call in a preserved USER CODE section. For this second demonstration, remove the periodic greeting loop so that both examples do not compete for the same CDC transmit endpoint. The echo project requires no deliberate HAL_Delay() because a long foreground delay can cause the finite RX queue to fill.

This is an educational, bare-metal, single-producer/single-consumer example. It assumes the generated F1 USB stack and class handler use the classic hUsbDeviceFS.pClassData and TxState fields. If your package or middleware revision changes those names, adapt to its documented transfer-completion API. A production logger should include explicit queue ownership, disconnect/reconnect handling, USB reset recovery and an application protocol that defines message boundaries.

STM32 USB CDC receive callback filling 256 byte ring FIFO and main loop sending 64 byte chunks only when USB transmit ready
USB OUT bytes go into an RX FIFO; the foreground echoes persistent chunks only when the previous asynchronous IN transfer completes.

Why USB CDC_Transmit_FS sometimes returns USBD_BUSY

USB data transfer is asynchronous. Calling CDC_Transmit_FS() normally asks the middleware to schedule an IN transfer; it does not mean the host has already received every byte. While a transfer is active, the USB CDC class may set TxState and reject a new transfer with USBD_BUSY. An example that writes a message inside a tight loop and ignores the return value may appear to work at low load but lose entire messages later.

The first rule is to keep TX data valid until completion. A static string or persistent buffer works; a short-lived stack array from a function that returns immediately is unsafe if the middleware still points to it. The second rule is to serialize transmit requests and decide what happens on busy: retry later, queue data, or explicitly drop low-priority log messages. The third is to distinguish CDC transmit acceptance from actual end-to-end delivery by the PC application.

The FIFO example illustrates a simple queue but does not impose flow control on the USB host. Under a sustained write stream, 256 bytes is tiny. For robust command communication, terminate commands with delimiters or length prefixes, parse frames in the foreground, acknowledge reception at application level where needed, and count overflows. If the host writes binary payloads, do not treat a zero byte as the end of a string. Use Len and explicit buffer lengths, never rely on strlen() for arbitrary received USB data.

The USB full-speed bulk endpoint maximum packet size is typically 64 bytes for CDC data on this stack. That does not restrict your application message to 64 bytes, nor does it guarantee exactly one host message per USB packet. The middleware fragments and reassembles transfers according to USB rules. The 64-byte TX staging in our example is a deliberate convenient choice, not a universal protocol frame size.

Control-line state, opening the terminal and connection timing

CDC ACM hosts may assert DTR or change line-coding parameters when a terminal opens the virtual COM port. A USB device can finish enumeration before a terminal is ready, which explains why a greeting sent only once during startup may never be seen. Repeating a short greeting every second makes a first demonstration easier to observe.

Applications that need reliable session startup can track CDC control requests such as CDC_SET_CONTROL_LINE_STATE, set a connected flag based on the host’s control-line state, and wait until the host application declares itself ready. Avoid making this a universal assumption: different terminals and operating systems may open CDC ports with different control-line behaviour. The example above intentionally does not rely on DTR for basic echo operation.

If the host unplugs and reconnects, the USB class and device state can be reset. Production firmware should explicitly clear or reinitialize its transport state, discard stale partial commands, and make sure pending TX buffers are not treated as successfully delivered after disconnection. A robust product also needs a well-defined way to distinguish USB CDC from a bootloader interface that may temporarily enumerate with another identity.

Verify the setup with a host-side Python test

A terminal is sufficient for manual typing and echo. For a repeatable host check, Python with pyserial can open the detected CDC port, write an ASCII payload and read the echoed bytes. The script below uses a port selected by the user, not an invented fixed COM number:

import serial

port = input("CDC port (e.g. COM7 or /dev/ttyACM0): ").strip()
message = b"esp32.co.uk/ USB CDC echo test\r\n"

with serial.Serial(port, baudrate=115200, timeout=2) as ser:
    ser.reset_input_buffer()
    ser.write(message)
    ser.flush()
    received = ser.read(len(message))

print("Sent:     ", repr(message))
print("Received: ", repr(received))
print("Match:    ", received == message)

For the echo firmware, expected output is Match: True if the host and USB queues deliver the complete payload within the timeout. pyserial must be installed separately with python -m pip install pyserial. This test is not suitable for the periodic-greeting-only firmware, because that example does not echo what the PC sends. If a few bytes are missing, inspect timing, driver state, buffer overflow and conflicting terminal processes before changing the USB crystal configuration.

If you want to test multiple messages, introduce a delimiter and a request/response scheme instead of assuming an arbitrary USB read contains exactly one message. The host can return fewer bytes than requested even with a valid device and correct firmware; robust host code loops until the expected length or an explicit timeout.

How CDC differs from factory DFU and ST-LINK programming

A conventional STM32F103C8 does not provide native USB DFU through its factory system-memory bootloader. Setting BOOT0 high and pressing reset does not turn this particular MCU into a USB DFU device. Use SWD via ST-LINK or the F103 ROM USART1 loader for initial firmware installation. A third-party USB bootloader can be installed into flash, but it occupies memory and has its own descriptors, toolchain and recovery behaviour.

Task Reliable F103C8 method Common misconception
First firmware programming ST-LINK SWD PA13/PA14 Micro-USB automatically uploads code
Factory ROM alternate upload USART1 PA9/PA10, correct boot pins BOOT0 automatically enables USB DFU
Application terminal output USB CDC firmware over PA11/PA12 ST-LINK COM port necessarily equals target native USB
Recover broken USB CDC build SWD independent of CDC Reinstall host driver to repair absent firmware
Reprogram over USB without SWD Optional installed custom USB bootloader Native CDC class itself is necessarily a bootloader

ST-LINK’s own USB connection carries debugger traffic and, on some newer probes or Nucleo boards, a separate virtual COM bridge to a hardware UART. That debugger bridge is not the Blue Pill’s native USB CDC interface. Our STM32CubeProgrammer flashing guide explains the different programming paths, while the Blue Pill USB troubleshooting guide concentrates on enumeration faults.

STM32 Blue Pill USB CDC problem-solving flowchart for data cable, USB enumeration, 48 MHz clock, D plus pullup and USB transmit busy
Troubleshoot in layers: USB data connection, enumeration, CDC driver, firmware callbacks and transmit-buffer state.

A structured troubleshooting table

When USB fails, find the layer that fails before editing files. A serial port visible on the host and a responsive echo task prove different things from an illuminated power LED.

Symptom Most likely layer Action to take
Board powers but host detects nothing Cable, D+ attach, USB firmware, clock Verify USB data cable, HSE/48 MHz, CDC initialization, pull-up
Unknown USB device / descriptor failed Electrical signaling or USB initialization Check PA11/PA12, pull-up, HSE startup, 3.3 V stability, descriptors
USB device visible, no COM port Incorrect USB class or host CDC binding Inspect descriptors/VID:PID and built-in CDC driver, not random drivers
COM port visible, no greeting Application startup or terminal choice Check firmware image, main loop, new port number, send timing
Greeting works, host writes are ignored Receive endpoint/callback Check CDC_Receive_FS, rearm call and host terminal local echo
Echo works only for short writes FIFO overflow or busy handling Check cdc_rx_dropped, TX buffer lifetime, throughput and queue size
CDC_Transmit_FS repeatedly BUSY Pending IN transfer / endpoint state Confirm configured state, completion handling and avoid conflicting writers
USB works, then drops on sensor activation Power, noise or blocking code Check regulator voltage, IRQ starvation, timing and attached hardware
DFU tool never sees target F103C8 ROM lacks native USB DFU Use SWD or factory USART1 ROM loader
MCU works through SWD but native USB never detects USB-specific hardware/firmware Inspect oscillator, PA11/PA12 routing, resistor and connector solder

The COM port never appears

First test with a known data-capable cable, another host port and the simplest firmware. Check the host’s USB device tree. If there is no attachment event, Windows driver replacement is not the sensible first action. Check board power, executable firmware, the D+ pull-up and USB clock. If the host reports an unrecognized device, it has detected at least some electrical activity, so USB signalling, descriptor responses, clock stability and firmware initialization become likely areas.

On some Blue Pill clones, the D+ pull-up is incorrectly fitted. Measure the actual circuit before modifying the board; the reference design should provide about 1.5 kΩ to 3.3 V. F103 clones and dev boards have varying silkscreen labels, so R10 is a clue, not a universal resistor reference. Don’t attempt live resistance measurements on an energized USB circuit. Disconnect power and follow safe measurement practices.

The COM port appears but nothing is transmitted

Make sure the greeting example actually calls CDC_Transmit_FS() after USB initialization, and that the MCU was resumed after a debugger halted it at main(). Confirm that the serial port being opened belongs to the STM32 CDC interface rather than ST-LINK VCP or an old USB-UART converter. If the program sends text only once before a terminal opens, the text may have been missed. The periodic example avoids that by sending repeatedly.

A virtual COM port number can change when descriptors, USB devices or bootloader states change. On Linux, check whether the correct device is /dev/ttyACM*, not a stale /dev/ttyUSB*; on macOS, recheck the relevant cu.usbmodem device. The terminal may also require DTR to be asserted before showing useful interaction, depending on its settings and your application.

Transmit reports BUSY and characters disappear

That normally means the program is submitting another IN transfer before the previous one completes, or USB is not in the configured device state. Use persistent TX buffers and submit again only when the class is ready. Avoid continuously writing to the same buffer while it is owned by the USB driver. Do not attempt blocking delays inside CDC_Receive_FS(); that can stall USB handling and create secondary faults.

USB becomes unreliable with other peripherals enabled

Check PA11/PA12 conflicts, particularly if another peripheral or GPIO function was configured on those pins. Some F103 designs use PA11/PA12 for default CAN routing; USB and CAN cannot simultaneously claim the same pins. If CAN is required too, check supported alternate remapping and the board schematic, and remember that CAN needs an external transceiver. On a small board, adding sensors or displays can also overload the 3.3 V rail. Return to the bare minimum USB CDC project before blaming Windows.

USB is stable on one computer and unstable on another

A marginal pull-up, data cable, connector joint or signal integrity issue may pass with one host but fail with another. Compare direct ports, check the USB cable and inspect the power and USB routing. USB full-speed requires appropriate signal routing and a reasonably stable clock; a firmware workaround cannot compensate reliably for every physical-board problem. ST’s AN4879 is the hardware reference for proper pull-up and layout rules.

Performance, packet size and memory budget

The F103C8 is a small MCU with 20 KB SRAM and 64 KB specified flash. USB middleware, class state, endpoint buffers, the HAL and your application share those resources. The 256-byte ring buffer, 64-byte TX staging buffer and generated USB FS packet RAM are adequate for a demonstration but not generous capacity for sustained data logging. Measure the actual map file and stack use rather than assuming that a 64-byte USB packet implies a 64-byte application queue.

The bus is USB full-speed (12 Mbit/s signaling), but the maximum theoretical bus bit rate is not the application throughput. Control transactions, framing, host scheduling, firmware overhead and buffering lower practical data delivery. For modest terminal output, prioritize correctness and responsiveness. For serious streaming, test sustained transfers with known host software, count dropped data, optimize processing and choose ring buffers sized for worst-case application latency.

When scaling to a data acquisition application, keep the ADC and timer sampling paths independent from slow USB transmit behavior. An ADC DMA buffer can be filled periodically, with complete blocks transferred to a separate USB queue in the foreground. Never block the ADC interrupt while waiting for USB TxState. The buffer ownership and latency calculations matter more than simply calling the CDC API frequently.

Production considerations before deploying USB CDC

The examples are suitable starting points, not a finished commercial communications stack. A robust device should define stable device descriptors, manufacturer/product strings and legitimate USB VID/PID usage, handle disconnects, recover from USB reset, protect its RX queue from overflow, parse command frames safely and avoid reliance on a specific COM number. Choose the actual CDC class/interface combination that matches the host operating systems you intend to support.

Do not ship unlicensed or borrowed USB vendor IDs. For a custom product, use an appropriately assigned VID/PID or a legitimate arrangement through the hardware/platform vendor. Descriptors copied from a random tutorial may create collisions or confusing host driver behavior. Also distinguish Windows’ standard CDC ACM driver support from other USB classes that may require different drivers.

If the device accepts privileged commands over CDC, decide what happens if the PC sends malformed or excessive input. Bound every received length, avoid unchecked sprintf or strcpy, keep calibration and persistent writes behind explicit validation, and consider authentication for commands that control actuators or security-sensitive state. USB CDC is a transport, not a security boundary.

For reliability, consider adding a watchdog only after your firmware handles resets and USB reconnection cleanly. A watchdog-reset loop can mask a power or clock fault and repeatedly force USB disconnects. Keep SWD available in the prototype for recovery; disabling debug pins too early turns routine firmware mistakes into avoidable restoration work.

Frequently asked questions

Does the Blue Pill need an external USB-to-serial adapter for USB CDC?

No. The STM32F103C8 has a native USB full-speed device peripheral on PA11 and PA12. With correctly configured firmware and hardware it can enumerate directly as CDC ACM through the Micro-USB connector. A USB-UART adapter is an alternative for USART communication, not a prerequisite for native USB CDC.

Can I flash the F103C8 through its Micro-USB port using STM32CubeProgrammer USB DFU?

Not on a standard blank F103C8 through its factory bootloader. Factory ROM support is different from the user application’s USB controller. Use SWD or USART1 to install firmware. Custom bootloaders can later provide a USB upload path if installed deliberately.

Why is USB 48 MHz necessary if the CPU only runs at 72 MHz?

The USB controller has a separate timing requirement: full-speed data signaling needs an accurate 48 MHz clock derived from a suitable oscillator/PLL arrangement. With a verified 8 MHz HSE crystal, multiplying by nine and dividing by 1.5 gives 48 MHz. The CPU’s ability to blink an LED does not prove the USB clock is correct.

Is CDC_Transmit_FS() a blocking UART write?

No. It submits USB IN data to the device middleware, which transfers it asynchronously. It can return BUSY if another transfer is still pending. Keep the buffer stable until completion and arrange application queuing for sustained data streams.

Why does opening the virtual COM port at a different baud rate still work?

Because a native CDC virtual serial port uses USB packets rather than a UART bitstream at a selected hardware baud. The baud setting is communicated as CDC line-coding metadata; firmware may apply it to a separate UART bridge, but a direct CDC application does not have to.

Why do I see a serial device but no echo?

Check whether you flashed the greeting-only demo or the echo demo, whether CDC_Receive_FS() rearms the OUT endpoint, and whether CDC_EchoTask() is called from the main loop. Also confirm the terminal’s local echo setting and make sure another program has not opened the same port.

Can the same Blue Pill use USB CDC and bxCAN simultaneously?

Potentially, with supported remapping and a board layout that exposes suitable alternative CAN signals, but not if both peripherals attempt to use the same default PA11/PA12 pins. The exact combination requires reference-manual AFIO verification and an external CAN transceiver. For an uncomplicated first USB project, leave CAN disabled.

Does USB CDC guarantee one receive callback for every command?

No. USB is packet-based; software writes and reads are not necessarily preserved as application message boundaries. Use delimiters or explicit frame lengths in your protocol and handle partial data correctly.

Practical checklist before publishing your firmware

  • Verify actual MCU marking and board revision, its 8 MHz HSE clock, and regulated 3.3 V supply.
  • Use a known-good Micro-USB data cable and avoid competing external power sources.
  • Configure USB DM PA11 and DP PA12 without other peripheral pin conflicts.
  • Make USBCLK 48 MHz from a valid stable oscillator and prescaler configuration.
  • Generate USB Device FS and the CDC class using a compatible STM32CubeF1 package.
  • Keep SWD PA13/PA14 enabled for recovery and flash the first build via ST-LINK.
  • Confirm host enumeration before troubleshooting terminal baud or application parsing.
  • Use static/persistent TX buffers, respect USBD_BUSY, and rearm the receive endpoint.
  • Bound RX queues and make message framing explicit for longer or binary messages.
  • Verify behavior on the specific host operating systems you plan to support.

Conclusion

A Blue Pill native virtual COM port is a useful building block once its layers are understood: electrical USB data on PA11/PA12, a genuine 48 MHz USB clock, the USB Device CDC middleware, and an application that correctly manages asynchronous receive and transmit buffers. Programming the F103C8 over ST-LINK first avoids dependence on a factory USB bootloader that this chip does not have. A visible COM port proves enumeration; stable two-way echo demonstrates that your application and endpoint state are actually working.

This esp32.co.uk/ guide is designed to be used alongside our STM32 GPIO, UART and USB troubleshooting articles, not as a substitute for the datasheet or board-specific schematic. Once the CDC echo works consistently, the same transport can support configuration commands, sensor streaming and PC logging, provided you add framing, flow control and error handling that match the application’s demands.

References and further reading

CONTINUE EXPLORING / ESP32 & ELECTRONICS

Read next

Practical guides related to this article.

Explore the topic →
HARDWARE DECISION GUIDEWhich ESP32 board should you use?

Compare practical options before you choose hardware.

Compare options →