
1. Background
Why the sudden interest in using USB CDC for log output? Many developers use a UART serial port to print debugging information for their MCU projects. However, as projects become more complex, the limitations of UART—multiple cables, significant resource occupation, and limited bandwidth—lead them to wonder: “Can I leverage USB’s features to handle both data transfer and log printing with a single cable?”
Fortunately, the APM32F402 chip is equipped with USB OTG functionality. This allows us to configure the device as a “Composite Device, ” running both WinUSB (for high-speed data transfer) and CDC (as a virtual serial port) interfaces simultaneously. By simply plugging in one USB data cable, we can supply power, upload/download data, and output debug logs—a true “three-in-one” solution.
Consider a specific example:
- A Data Logger needs to periodically or in real-time collect sensor data, such as pressure, flow rate, or vibration amplitude.
- A field maintenance technician can use a laptop to establish a WinUSB channel with the device via USB to download large amounts of historical data at once.
- Meanwhile, a debugging engineer can open the CDC virtual serial port in a terminal software to view the MCU’s real-time operational status or alert messages as they are generated.
- This entire process requires only one USB cable, simplifying cable management without compromising high-bandwidth data transmission.

2. Why Choose USB CDC? What are its Advantages Over UART?
When it comes to log output, the traditional UART serial port generally meets basic needs, but it has notable disadvantages:
- Insufficient Bandwidth: The common baud rate of 115200bps can become a bottleneck when data volume is large. Using higher baud rates can introduce interference and requires higher-quality cables.
- Requires Additional Adapters: If the development board only provides a TTL interface, a USB-to-TTL cable is necessary.
- Cumbersome Wiring: Debugging lines, power cords, and serial cables get tangled together easily.
In contrast, USB CDC (Virtual COM Port) presents a COM port directly to the operating system. Its speed is theoretically much higher than a standard UART. Furthermore, a single USB cable can handle power, debugging, and data transfer. This is especially advantageous when other USB functions (like WinUSB, U-Disk simulation, HID, etc.) need to run concurrently. CDC can coexist with them, greatly enhancing flexibility. Imagine simultaneously exchanging data with a PC at high speed via USB while also viewing debug logs in a virtual COM terminal—what’s not to like?
3. What is a USB Composite Device?
A “USB Composite Device” is a device that exposes multiple logical interfaces on a single physical USB port. A typical combination looks like this:
- CDC Interface (Virtual COM Port) – For outputting debug logs.
- WinUSB Interface (Custom Protocol) – For high-speed data exchange with a host application.
- Other interfaces like HID, MSC (U-Disk), or Audio can also be added, provided there are sufficient resources.
For the APM32F402, Geehy provides an official example project named OTGD_Composite_CDC_WINUSB. It has already prepared the composite device descriptors and endpoint allocations for us. We only need to make minor modifications or add a little code to our project to redirect printf output to the CDC virtual serial port. This does not affect the WinUSB data channel or block other USB functions, perfectly achieving “one USB cable, two channels running in parallel.”

4. Getting printf Redirection to Work
4.1 USB CDC Initialization
First, confirm that your project includes the necessary USB library files, such as usb_core, usb_init, usb_cdc, etc. If you are basing your work on the official OTGD_Composite_CDC_WINUSB project, you will need to call USB_DeviceInit() in main() or your custom initialization function. This ensures that the CDC and WinUSB interfaces are registered and enumerated when the system starts.
4.2 Finding the “Redirection” Headquarters
In an IDE environment like MDK or others, there are typically low-level functions for printf, such as fputc(int ch, FILE *f) or _write(). All we need to do is modify these functions, changing the part that “writes to UART” to “calls the USB CDC send function.” To avoid resource-intensive, frequent calls to the send function for every single character, we can use a static buffer here to accumulate data and send it in a single packet once it reaches a certain length.
4.3 Core Code Example
Below is a simple fputc() example that includes a buffering mechanism for your reference. Please note that the function names, macro definitions, etc., may need to be adjusted to match your specific project environment.
#define CDC_TX_BUF_SIZE (128)
/*!
* @brief Redirect C Library function printf to serial port.
* After Redirection, you can use printf function.
*
* @param ch: The character to be sent.
*
* @param *f: A pointer to a FILE structure that is not used in this implementation.
*
* @retval The character that was sent.
*
* @note
*/
int fputc(int ch, FILE* f)
{
/* The original UART redirection code can be commented out or removed. */
// USART_TxData(DEBUG_USART, (uint8_t)ch);
// while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);
/* The 'static' keyword ensures state is maintained across function calls. */
static uint8_t s_cdcTxBuf[CDC_TX_BUF_SIZE];
static uint16_t s_cdcTxCount = 0;
/* If you need to convert '\n' to "\r\n" for Windows terminals, handle it here. */
if (ch == '\n')
{
/* Check for buffer overflow before the next operation. */
if (s_cdcTxCount >= CDC_TX_BUF_SIZE)
{
USBD_FS_CDC_ItfSend(s_cdcTxBuf, s_cdcTxCount);
s_cdcTxCount = 0;
}
/* You could add s_cdcTxBuf[s_cdcTxCount++] = '\r'; here if needed. */
}
/* Add the current character to the buffer. */
s_cdcTxBuf[s_cdcTxCount++] = (uint8_t)ch;
/* If the buffer is full or the character is a newline, send the data immediately. */
if (s_cdcTxCount >= CDC_TX_BUF_SIZE || ch == '\n')
{
USBD_FS_CDC_ItfSend(s_cdcTxBuf, s_cdcTxCount);
s_cdcTxCount = 0;
}
return (ch);
}
Notes on the Code:
s_cdcTxBuf[]: This buffer accumulates data to avoid frequent calls to the send function.
s_cdcTxCount: This tracks the number of bytes currently stored in the buffer.
When a newline character \n is encountered or the buffer is full, we call USBD_FS_CDC_ItfSend(...) to transmit the data in one go.
4.4 Compilation & Testing
After writing the redirection function, compile the code and download it to your APM32F402 board. Plug in the USB cable and check the Device Manager on your computer to see if a “Virtual COM Port” and a WinUSB device have appeared. If both enumerate successfully, open the virtual COM port with any serial terminal software to see the real-time printf log output.
- The baud rate setting in the terminal software usually has no effect, as USB CDC only “pretends” to have a baud rate.
- If you need to transfer a large amount of data, you can run a high-speed protocol on the WinUSB interface, while the CDC interface remains unaffected and continues to serve as a debug output.

5. Summary
By making minor changes to the official Geehy OTGD_Composite_CDC_WINUSB example project, we can successfully redirect the printf output of the APM32F402 to a USB CDC virtual serial port, achieving the following advantages:
- A single USB cable handles development, power, and debugging.
- Hardware UART resources are saved and can be used for other peripherals that require hardware flow control or serial communication.
- The WinUSB channel can continue to transfer large amounts of data efficiently while the CDC channel is dedicated to logging, with both pathways operating without interfering with each other.
If you plan to use this Data Logger or another device in an industrial, medical, or consumer application, this combined USB CDC + WinUSB approach can not only reduce cable costs but also unify data and log management. For more advanced implementations, you can build upon this foundation by adding features like DMA or multi-threaded send queues in an RTOS environment.
We hope this article helps fellow engineers who are struggling with “where to plug in another serial cable.” Feel free to leave a comment to discuss more advanced techniques.