kai迪皮 发表于 2025-6-24 17:51

如何使用 APM32F402 USB CDC 做日志输出

本帖最后由 kai迪皮 于 2025-6-24 18:00 编辑

## 1 背景

为啥突然想用 USB CDC 做日志输出?很多开发者在做 MCU 项目时,往往会用 UART 串口来打印调试信息。可是一旦项目复杂了,UART 线缆多、资源占用大、带宽还有限,就会开始琢磨:“能不能利用 USB 的特性,让它既传输数据又打印日志,一根线搞定?” 正好,APM32F402 这颗芯片具备 USB OTG 功能,我们可以让设备模拟成一个“复合设备(Composite Device)”,同时跑 WinUSB(高速数据传输)和 CDC(虚拟串口)这两个接口。这样只要插上一条 USB 数据线,就能既供应电源、又上传下载数据、还输出调试日志,堪称“一线三用”。

举个具体例子:

1. 比如一款 Data Logger(数据记录器)要定期或实时采集某种传感器数据,比如压力、流量、震动幅度等;
2. 现场维护人员可以用笔记本通过 USB 与设备建立 WinUSB 通道,一次性下载大量的历史数据;
3. 另一方面,调试工程师能在终端软件里打开 CDC 虚拟串口,看到 MCU 的实时运行状态或告警信息,刷刷往外吐;
4. 整个过程只需要一根 USB 线,简化了线缆管理,也不耽误高带宽的数据传输。

   !(data/attachment/forum/202506/24/174654cqe6knnbsxnyk6qy.png "image2.png")

## 2 为什么选择 USB CDC?它比 UART 好在哪儿?

说到日志输出,传统串口 UART 一般满足日常需求,不过也有不可忽视的缺点:

* 带宽不足:常见波特率 115200bps,一旦数据量大了,容易拖慢速度;若是使用高波特率又容易受干扰,对线材的性能又要求。
* 需要额外转接:如果开发板只提供 TTL 接口,还得准备 USB 转 TTL 线;
* 接线繁琐:调试线、电源线、串口线挤在一起,非常容易打结。

而 USB CDC(虚拟串口)能直接呈现给操作系统一个 COM 口,速率理论上可比常规 UART 更高,加上只要一根 USB 数据线就能兼顾供电、调试和传输。尤其是在需要同时跑其他 USB 功能(如 WinUSB、U 盘模拟、HID 等)时,CDC 可以与它们共存,大大提升灵活度。想象一下,我们在 PC 端通过 USB 与设备高速交换数据的同时,也能在一个虚拟 COM 终端看到调试日志,何乐而不为?

## 3 什么是 USB 复合设备?

“USB 复合设备”指的是在同一个物理 USB 接口上,开启多个逻辑接口(Interface)。典型组合像下面这样:

* CDC 接口(虚拟串口)——输出调试日志;
* WinUSB 接口(自定义协议)——跟上位机程序进行高速数据交互;
* 其他 HID、MSC(U 盘)、Audio 之类也可以加进去,只要资源够用。

对于 APM32F402 而言,Geehy 官方提供了一个名为 OTGD\_Composite\_CDC\_WINUSB 的例程。它已经帮我们写好了复合设备的描述符和端点分配,我们只需要在对应的工程里稍微改动或者增加一点代码,就能把 printf 输出定向到 CDC 虚拟串口上。不影响 WinUSB 通道的数据传输,也不会阻塞其他 USB 功能,完美实现“一条 USB 线,双通道齐头并进”。

!(data/attachment/forum/202506/24/174709ejunjt6ou6h8guie.png "image.png")

## 4 搞定 printf 重定向

### 4.1 USB CDC 初始化

先确认当前工程里已经包含了 USB 库的相关文件,例如 usb\_core、usb\_init、usb\_cdc 等驱动。如果是基于官方 OTGD\_Composite\_CDC\_WINUSB 工程,那么在 `main()` 或者您自定义的初始化函数里,需要调用 `USB_DeviceInit()`,让系统启动时就注册并枚举 CDC 和 WinUSB 接口。

### 4.2 找到“重定向”大本营

在 MDK 或者其他编译器环境下,通常实现了针对 printf 的底层函数,比如 `fputc(int ch, FILE *f)` 或者 `_write()`。咱们只要在这些函数中,把原本“写到 UART 的”那部分改成“调用 USB CDC 的发送函数”。考虑到如果逐字发送,会频繁调用发送函数,会占用资源,我们可以在这里用一个静态缓冲区,凑够一定长度再将数据一次性打包发给 USB。

### 4.3 核心代码示例

下面给出一个简单的 `fputc()` 示例,带有缓冲区机制,供大家参考。请注意,此处示例函数名、宏定义等可能需要和您具体的工程环境保持一致。

```c
#define CDC_TX_BUF_SIZE(128)

/*!
*            Redirect C Library function printf to serial port.
*            After Redirection, you can use printf function.
*
* @param       ch:The characters that need to be send.
*
* @param       *f:pointer to a FILE that can recording all information
*            needed to control a stream
*
* @retval      The characters that need to be send.
*
* @note
*/
int fputc(int ch, FILE* f)
{
    /* send a byte of data to the serial port */
//    USART_TxData(DEBUG_USART, (uint8_t)ch);

//    /* wait for the data to be send*/
//    while (USART_ReadStatusFlag(DEBUG_USART, USART_FLAG_TXBE) == RESET);

    /* static 关键字确保在函数内部维持状态 */
    static uint8_t s_cdcTxBuf;
    static uint16_t s_cdcTxCount = 0;

    /* 如果在输出时需要将 '\n' 转化为 Windows 习惯的 "\r\n",可按需处理 */
    if (ch == '\n')
    {
      /* 注意下一步操作前先检查避免缓冲区越界 */
      if (s_cdcTxCount >= CDC_TX_BUF_SIZE)
      {
            USBD_FS_CDC_ItfSend(s_cdcTxBuf, s_cdcTxCount);
            s_cdcTxCount = 0;
      }
    }

    /* 将当前字符装入缓冲区 */
    s_cdcTxBuf = (uint8_t)ch;

    /* 如果缓冲区已满,或者本次输出是换行字符,就立刻发送 */
    if (s_cdcTxCount >= CDC_TX_BUF_SIZE || ch == '\n')
    {
      USBD_FS_CDC_ItfSend(s_cdcTxBuf, s_cdcTxCount);
      s_cdcTxCount = 0;
    }

    return (ch);
}
```

#### 相关说明

1. s\_cdcTxBuf\[ \]:用来累积数据,避免频繁调用发送函数;
2. s\_cdcTxCount:记录当前已经存进缓冲区的字节数;
3. 若遇到 `'\n'` 或者缓冲区满了,我们就调用 `USBD_FS_CDC_ItfSend(...)` 一次性输出;

### 4.4 编译 & 测试

写完重定向函数后,编译并下载到 APM32F402 板子上,插上 USB 线,看看电脑里面设备管理器是否出现了“虚拟 COM 端口”和一个 WinUSB 设备。如果都枚举成功,用任意串口调试软件打开该虚拟 COM 口,就能实时看见 printf 的日志输出了。

* 波特率填多少通常没太大影响,因为 USB CDC 其实是“假装”有一个波特率;
* 如果你要传输的数据量很大,就可以在 WinUSB 接口那边跑高速协议,而这边 CDC 不受影响,依旧可以当调试输出使用。
* !(data/attachment/forum/202506/24/174815rj5v2zteavcv9hjj.gif "PixPin_2025-06-24_14-56-50.gif")
*

## 5 总结

通过在 Geehy 官方 OTGD\_Composite\_CDC\_WINUSB 例程中做少量改动,我们就能成功把 APM32F402 的 printf 重定向到 USB CDC 虚拟串口上,实现以下优点:

1. 只用一根 USB 数据线,开发、供电、调试都搞定;
2. 节省硬件 UART 资源,留给需要硬件流控或者其他串口外设;
3. 还能让 WinUSB 通道继续高效传输大数据,CDC 专心搞日志,两条通路互不打架。

如果你打算让这个 Data Logger 或其他设备在工业、医疗或消费领域使用,那么通过 USB CDC + WinUSB 的融合方式,不仅能减少线缆成本,还能统一管理数据和日志。至于更高阶玩法,比如 DMA、RTOS 下的多线程发送队列,就可以在此基础上继续扩展。

希望这篇文章能帮到还在为“哪里再插一条串口线”而头痛的朋友,也欢迎留言一起探讨更多进阶技巧。

kai迪皮 发表于 2025-6-24 17:52

本帖最后由 kai迪皮 于 2025-6-24 17:59 编辑

#申请原创# @21小跑堂

永恒的一瞥 发表于 2025-6-25 16:56

极海提供的winUSB驱动还需要付费吗?

VelvetNight 发表于 2025-6-29 08:42

我一直想使用USB做为调试输出。
但对USB协议的理解一直较差,看了楼主的帖子,倒是觉得USB通讯的难度也不大啊

ZenithSeeker 发表于 2025-6-29 16:08

讲的很不错,原创帖不容易,向楼主学习

kai迪皮 发表于 2025-6-29 17:13

ZenithSeeker 发表于 2025-6-29 16:08
讲的很不错,原创帖不容易,向楼主学习

感谢支持,只是学习分享{:titter:}

kai迪皮 发表于 2025-6-29 17:14

VelvetNight 发表于 2025-6-29 08:42
我一直想使用USB做为调试输出。
但对USB协议的理解一直较差,看了楼主的帖子,倒是觉得USB通讯的难度也不大 ...

其实很多东西我们在极海的官方提供的SDK例程上改改就搞定了{:victory:}

VelvetNight 发表于 2025-7-3 10:29

我也有APM32F103的小板。回头我也试试USB去

wangwu1976@ 发表于 2025-9-25 09:42

支持原创
页: [1]
查看完整版本: 如何使用 APM32F402 USB CDC 做日志输出