<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>CDC on Tech Snippets - 嵌入式技术笔记</title>
    <link>https://tech-snippets.xyz/tags/cdc/</link>
    <description>Recent content in CDC on Tech Snippets - 嵌入式技术笔记</description>
    <generator>Hugo</generator>
    <language>zh-cn</language>
    <lastBuildDate>Fri, 29 May 2026 19:00:00 +0800</lastBuildDate>
    <atom:link href="https://tech-snippets.xyz/tags/cdc/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>基于STM32的USB设备开发实战——从HID到CDC虚拟串口</title>
      <link>https://tech-snippets.xyz/posts/stm32-usb-device-development-guide/</link>
      <pubDate>Fri, 29 May 2026 19:00:00 +0800</pubDate>
      <guid>https://tech-snippets.xyz/posts/stm32-usb-device-development-guide/</guid>
      <description>前言 在嵌入式开发的世界里，USB 可能是最矛盾的存在：一方面它无处不在，从键盘鼠标到U盘摄像头，几乎所有外设都在用；另一方面它又以复杂著称，四层协议栈、十几种传输类型、上百页的规格书，让很多工程师望而却步。
我第一次接触 USB 开发是在 2018 年。当时项目需要做一个自定义的 USB 数据采集设备，我拿着 STM32 的参考手册看了三天，愣是没搞懂端点（Endpoint）和管道（Pipe）到底有什么区别。网上的教程要么是「打开 CubeMX 点几下就行了」，要么是直接扔给你一整个库的代码，中间的关键步骤一概省略。
那一周我熬了三个通宵，把 USB 协议栈的源代码一行一行地啃完，才终于明白：USB 其实没那么难，难的是没有人把它讲清楚。
这篇文章就是为了解决这个问题。我会从最基础的 USB 协议概念讲起，一步步带你完成 HID 自定义设备和 CDC 虚拟串口的完整开发。不需要你有任何 USB 开发经验，只要你会用 STM32 和 HAL 库，跟着这篇文章走，就能做出自己的 USB 设备。
一、USB 协议基础：五分钟搞懂核心概念 在写代码之前，我们必须先搞懂 USB 的几个核心概念。这部分是整个 USB 开发的基石，理解了这些，后面的代码就只是按部就班而已。
1.1 主机与设备的主从关系 USB 是一个严格的主从架构：
主机（Host）：只能是一个，通常是 PC 或手机，负责发起所有通信 设备（Device）：可以有多个，只能被动响应主机的请求 这一点非常重要。USB 设备永远不能主动给主机发数据，它只能在主机询问的时候才能回复。这是很多新手踩的第一个坑——他们在设备端写了一个发送数据的循环，然后奇怪为什么主机收不到任何东西。
1.2 端点：USB 通信的基本单元 如果把 USB 比作一条公路，那么**端点（Endpoint）**就是这条公路上的车道。每个端点都有：
编号：0-15，其中端点 0 是控制端点，所有设备必须实现 方向：IN（设备→主机）或 OUT（主机→设备） 类型：控制传输、批量传输、中断传输、等时传输 最大包长：全速设备最大 64 字节，高速设备最大 1024 字节 举个例子：一个 USB 鼠标通常有两个端点：</description>
      <content:encoded><![CDATA[<h2 id="前言">前言</h2>
<p>在嵌入式开发的世界里，<strong>USB</strong> 可能是最矛盾的存在：一方面它无处不在，从键盘鼠标到U盘摄像头，几乎所有外设都在用；另一方面它又以复杂著称，四层协议栈、十几种传输类型、上百页的规格书，让很多工程师望而却步。</p>
<p>我第一次接触 USB 开发是在 2018 年。当时项目需要做一个自定义的 USB 数据采集设备，我拿着 STM32 的参考手册看了三天，愣是没搞懂端点（Endpoint）和管道（Pipe）到底有什么区别。网上的教程要么是「打开 CubeMX 点几下就行了」，要么是直接扔给你一整个库的代码，中间的关键步骤一概省略。</p>
<p>那一周我熬了三个通宵，把 USB 协议栈的源代码一行一行地啃完，才终于明白：<strong>USB 其实没那么难，难的是没有人把它讲清楚。</strong></p>
<p>这篇文章就是为了解决这个问题。我会从最基础的 USB 协议概念讲起，一步步带你完成 HID 自定义设备和 CDC 虚拟串口的完整开发。不需要你有任何 USB 开发经验，只要你会用 STM32 和 HAL 库，跟着这篇文章走，就能做出自己的 USB 设备。</p>
<p><img alt="STM32 USB 设备开发架构" loading="lazy" src="/images/stm32-usb-architecture.svg"></p>
<hr>
<h2 id="一usb-协议基础五分钟搞懂核心概念">一、USB 协议基础：五分钟搞懂核心概念</h2>
<p>在写代码之前，我们必须先搞懂 USB 的几个核心概念。这部分是整个 USB 开发的基石，理解了这些，后面的代码就只是按部就班而已。</p>
<h3 id="11-主机与设备的主从关系">1.1 主机与设备的主从关系</h3>
<p>USB 是一个<strong>严格的主从架构</strong>：</p>
<ul>
<li><strong>主机（Host）</strong>：只能是一个，通常是 PC 或手机，负责发起所有通信</li>
<li><strong>设备（Device）</strong>：可以有多个，只能被动响应主机的请求</li>
</ul>
<p>这一点非常重要。USB 设备永远不能主动给主机发数据，它只能在主机询问的时候才能回复。这是很多新手踩的第一个坑——他们在设备端写了一个发送数据的循环，然后奇怪为什么主机收不到任何东西。</p>
<h3 id="12-端点usb-通信的基本单元">1.2 端点：USB 通信的基本单元</h3>
<p>如果把 USB 比作一条公路，那么**端点（Endpoint）**就是这条公路上的车道。每个端点都有：</p>
<ul>
<li><strong>编号</strong>：0-15，其中端点 0 是控制端点，所有设备必须实现</li>
<li><strong>方向</strong>：IN（设备→主机）或 OUT（主机→设备）</li>
<li><strong>类型</strong>：控制传输、批量传输、中断传输、等时传输</li>
<li><strong>最大包长</strong>：全速设备最大 64 字节，高速设备最大 1024 字节</li>
</ul>
<p>举个例子：一个 USB 鼠标通常有两个端点：</p>
<ul>
<li>端点 0（控制）：用于枚举和配置</li>
<li>端点 1 IN（中断）：用于上报鼠标位置和按键状态</li>
</ul>
<h3 id="13-四种传输类型什么时候用哪个">1.3 四种传输类型，什么时候用哪个？</h3>
<p>USB 定义了四种传输类型，分别应对不同的场景：</p>
<table>
<thead>
<tr>
<th>传输类型</th>
<th>特点</th>
<th>典型应用</th>
<th>可靠性</th>
<th>延迟</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>控制传输</strong></td>
<td>双向、可靠、低频率</td>
<td>设备枚举、配置命令</td>
<td>100%可靠</td>
<td>不确定</td>
</tr>
<tr>
<td><strong>中断传输</strong></td>
<td>单向、有保证的轮询间隔</td>
<td>鼠标、键盘、数据采集</td>
<td>100%可靠</td>
<td>低且确定</td>
</tr>
<tr>
<td><strong>批量传输</strong></td>
<td>单向、可靠、无延迟保证</td>
<td>U盘、打印机、固件更新</td>
<td>100%可靠</td>
<td>不确定</td>
</tr>
<tr>
<td><strong>等时传输</strong></td>
<td>单向、不可靠、固定带宽</td>
<td>音频、视频、摄像头</td>
<td>可能丢包</td>
<td>极低且固定</td>
</tr>
</tbody>
</table>
<p>对于我们做嵌入式设备来说，<strong>中断传输</strong>和<strong>批量传输</strong>是最常用的。中断传输适合小数据量的实时数据上传，批量传输适合大数据量的可靠传输。</p>
<h3 id="14-描述符告诉主机我是什么设备">1.4 描述符：告诉主机我是什么设备</h3>
<p>USB 设备的「自我介绍」是通过一系列**描述符（Descriptor）**完成的。当你把 USB 设备插到电脑上时，主机会依次请求这些描述符：</p>
<ol>
<li><strong>设备描述符</strong>：厂商 ID、产品 ID、设备类别、配置数量</li>
<li><strong>配置描述符</strong>：有多少个接口、每个接口有多少端点</li>
<li><strong>接口描述符</strong>：接口类别、子类、协议</li>
<li><strong>端点描述符</strong>：端点号、方向、类型、最大包长、轮询间隔</li>
<li><strong>类特殊描述符</strong>：如 HID 报告描述符、CDC 功能描述符</li>
</ol>
<p>这就是所谓的<strong>枚举过程</strong>。枚举完成后，主机就知道这是什么设备，该加载哪个驱动程序了。</p>
<blockquote>
<p><strong>新手提示</strong>：90% 的 USB 枚举失败问题，都是描述符写错了。如果设备插上后电脑提示「无法识别的设备」，先检查描述符，再检查硬件。</p>
</blockquote>
<h3 id="15-请求与状态机">1.5 请求与状态机</h3>
<p>主机与设备的所有控制通信，都是通过一种叫做**设置包（Setup Packet）**的结构发起的。每个设置包包含 8 个字节，定义了：</p>
<ul>
<li>请求类型（标准请求 / 类请求 / 厂商请求）</li>
<li>具体请求码</li>
<li>两个参数值</li>
<li>数据阶段的长度</li>
</ul>
<p>设备收到设置包后，根据请求类型进入相应的处理流程。这就是为什么 USB 设备的固件看起来像一个大的状态机——它永远在等待主机的下一个请求。</p>
<hr>
<h2 id="二stm32-的-usb-硬件otg-fs-与-hs">二、STM32 的 USB 硬件：OTG FS 与 HS</h2>
<p>STM32 系列芯片的 USB 外设经历了几代演进，目前主流的是<strong>OTG_FS</strong> 和 <strong>OTG_HS</strong> 两种。</p>
<h3 id="21-otg_fs最常用的全速-usb">2.1 OTG_FS：最常用的全速 USB</h3>
<p>几乎所有中高端 STM32 都集成了 OTG_FS 外设：</p>
<ul>
<li>支持 USB 2.0 全速（12Mbps）</li>
<li>内置 USB PHY，不需要外部芯片</li>
<li>最多 4 个双向端点（加上端点 0 共 5 个）</li>
<li>内置 DMA，支持数据自动传输</li>
</ul>
<p>代表型号：STM32F105/107、STM32F405/407、STM32F767、STM32H743、STM32L476</p>
<p>这是我们最常用的配置，一根 USB 线直接连到芯片的 PA11（DM）和 PA12（DP）引脚，不需要额外元件。</p>
<h3 id="22-otg_hs高速-usb-选项">2.2 OTG_HS：高速 USB 选项</h3>
<p>高端 STM32 还提供 OTG_HS 外设：</p>
<ul>
<li>支持 USB 2.0 高速（480Mbps）</li>
<li>可以使用内置 FS PHY 工作在全速模式</li>
<li>配合外部 ULPI PHY 芯片（如 USB3300）实现高速</li>
<li>最多 8 个双向端点</li>
<li>更强大的 DMA 能力</li>
</ul>
<p>代表型号：STM32F429、STM32F767、STM32H743</p>
<p>如果你需要做高速数据传输，比如视频流或者高速数据采集，就需要 OTG_HS + 外部 PHY 的组合。</p>
<h3 id="23-硬件连接的几个细节">2.3 硬件连接的几个细节</h3>
<p>做 USB 硬件时，有几个容易忽略的细节：</p>
<p><strong>1. D+ 上拉电阻</strong>：</p>
<ul>
<li>全速设备：在 D+（PA12）上接 1.5kΩ 上拉电阻到 3.3V</li>
<li>低速设备：在 D-（PA11）上接 1.5kΩ 上拉电阻</li>
<li>很多 STM32 内置了这个上拉电阻，可以通过软件控制</li>
</ul>
<p><strong>2. 电源处理</strong>：</p>
<ul>
<li>如果是总线供电设备，注意 USB 最大只能提供 500mA</li>
<li>如果是自供电设备，需要处理 Vbus 检测</li>
</ul>
<p><strong>3. ESD 保护</strong>：</p>
<ul>
<li>USB 接口是对外的，一定要加 ESD 保护二极管</li>
<li>推荐用专门的 USB ESD 芯片，比如 SRV05-4</li>
</ul>
<h3 id="24-st-的-usb-设备库">2.4 ST 的 USB 设备库</h3>
<p>ST 为我们提供了完整的 USB 设备库，叫做 <strong>STM32_USB_Device_Library</strong>。这个库的结构非常清晰：</p>
<pre tabindex="0"><code>STM32_USB_Device_Library/
├── Core/          # USB 核心层，处理控制传输和枚举
└── Class/         # 各类设备驱动
    ├── HID/       # 人机接口设备（键盘、鼠标、自定义）
    ├── CDC/       # 通信设备类（虚拟串口）
    ├── MSC/       # 大容量存储（U盘）
    ├── Audio/     # 音频设备
    └── Video/     # 视频设备
</code></pre><p>对我们来说，好消息是：<strong>我们几乎不需要修改核心层的代码</strong>，只需要实现类驱动层的几个回调函数就行。这正是 CubeMX 能帮我们自动生成的部分。</p>
<hr>
<h2 id="三usb-设备开发的一般流程">三、USB 设备开发的一般流程</h2>
<p>在进入具体的代码之前，我们先梳理一下 USB 设备开发的完整流程：</p>
<h3 id="第一步确定设备类型和需求">第一步：确定设备类型和需求</h3>
<p>首先想清楚几个问题：</p>
<ul>
<li>我的设备要实现什么功能？（数据采集？串口通信？）</li>
<li>需要传输的数据量有多大？（每秒几字节？还是几 KB？）</li>
<li>对延迟有要求吗？（可以等 10ms，还是必须 1ms 以内？）</li>
<li>主机端需要驱动吗？（HID 和 CDC 是标准类，Windows 自带驱动）</li>
</ul>
<p>举几个常见的选择：</p>
<ul>
<li>小型传感器数据采集 → HID 中断传输（免驱动）</li>
<li>调试串口、控制台 → CDC 虚拟串口（通用方便）</li>
<li>固件更新、大数据传输 → 批量传输（速度快）</li>
</ul>
<h3 id="第二步配置-cubemx-生成代码">第二步：配置 CubeMX 生成代码</h3>
<p>这一步我们要做的是：</p>
<ol>
<li>使能 USB_OTG_FS 或 USB_OTG_HS 外设</li>
<li>配置 USB 时钟（必须是 48MHz）</li>
<li>在 &ldquo;Middleware&rdquo; 中选择 USB Device</li>
<li>选择设备类（HID / CDC / MSC 等）</li>
<li>配置端点参数</li>
<li>生成代码</li>
</ol>
<h3 id="第三步实现用户回调函数">第三步：实现用户回调函数</h3>
<p>CubeMX 生成的代码只是一个「骨架」，真正的业务逻辑需要我们在回调函数中实现：</p>
<ul>
<li><code>USBD_*_Init()</code>：设备初始化</li>
<li><code>USBD_*_DeInit()</code>：设备反初始化</li>
<li><code>USBD_*_Setup()</code>：处理类特殊的控制请求</li>
<li><code>USBD_*_DataIn()</code>：数据发送完成回调</li>
<li><code>USBD_*_DataOut()</code>：数据接收完成回调</li>
</ul>
<p>这是整个开发过程中代码量最大的一步，也是我们后面要重点讲解的部分。</p>
<h3 id="第四步调试与验证">第四步：调试与验证</h3>
<p>USB 开发的调试相对麻烦，推荐几个工具：</p>
<ul>
<li><strong>USBlyzer / Wireshark</strong>：抓包分析 USB 总线数据</li>
<li><strong>STM32 ST-Link</strong>：单步调试固件代码</li>
<li><strong>串口调试</strong>：在关键位置加打印，观察状态机流转</li>
<li><strong>HID Test Tool / TeraTerm</strong>：主机端测试工具</li>
</ul>
<p>（第一部分完，约 2400 字）</p>
<hr>
<h2 id="四实战第一步cubemx-配置详解">四、实战第一步：CubeMX 配置详解</h2>
<p>理论讲了这么多，是时候动手写代码了。我们从最简单的 HID 自定义设备开始，这也是新手最好的入门项目。</p>
<h3 id="41-新建工程与基础配置">4.1 新建工程与基础配置</h3>
<p>打开 STM32CubeMX，新建一个工程，选择你的芯片型号（我用的是 STM32F407VGT6）：</p>
<p><strong>1. 配置系统时钟</strong></p>
<p>这是最关键的一步，USB 的时钟必须精确到 48MHz：</p>
<pre tabindex="0"><code>SYSCLK = 168MHz (F4 的标准配置)
USB OTG FS clock = 48MHz (必须精确!)
</code></pre><p>在 &ldquo;Clock Configuration&rdquo; 页面，确保 USB 时钟那一行显示的是 48MHz。如果不是，调整 PLL 分频系数。</p>
<blockquote>
<p><strong>致命坑提示</strong>：USB 时钟不对是枚举失败的头号原因。即使差 0.1%，设备也完全无法工作。</p>
</blockquote>
<p><strong>2. 使能 USB_OTG_FS 外设</strong></p>
<p>在 &ldquo;Pinout &amp; Configuration&rdquo; 页面，找到 &ldquo;Connectivity&rdquo; → &ldquo;USB_OTG_FS&rdquo;：</p>
<ul>
<li>Mode: 选择 &ldquo;Device_Only&rdquo;（我们只做设备，不做主机）</li>
<li>下面的 &ldquo;Activate_VBUS&rdquo; 可以不选（自供电设备不需要）</li>
</ul>
<p>确认 PA11 和 PA12 引脚已经被自动配置为 USB_OTG_FS_DM 和 USB_OTG_FS_DP。</p>
<p><strong>3. 配置中间件</strong></p>
<p>在 &ldquo;Middleware&rdquo; 选项卡中，找到 &ldquo;USB_DEVICE&rdquo;：</p>
<ul>
<li>勾选 &ldquo;USB_DEVICE&rdquo;</li>
<li>Class For FS IP: 选择 &ldquo;Human Interface Device Class (HID)&rdquo;</li>
</ul>
<p><strong>4. 调整 HID 参数</strong></p>
<p>点击 &ldquo;Parameter Settings&rdquo;，这里有几个重要参数：</p>
<ul>
<li><code>USBD_HID_IN_EP</code>: 默认 0x81（端点 1 IN），不用改</li>
<li><code>USBD_HID_OUT_EP</code>: 默认 0x01（端点 1 OUT），如果不需要双向可以删掉</li>
<li><code>USBD_HID_HS_BINTERVAL</code>: 高速模式轮询间隔，默认 5</li>
<li><code>USBD_HID_FS_BINTERVAL</code>: 全速模式轮询间隔，默认 10（单位 ms）</li>
</ul>
<p>轮询间隔是什么意思？就是主机每隔多少毫秒来问一次设备有没有数据要发。设为 10ms 意味着设备最快每 10ms 才能上报一次数据。如果你需要更快的响应，可以改成 1ms。</p>
<p><strong>5. 生成代码</strong></p>
<p>设置好工程名称和路径，Toolchain/IDE 选择 &ldquo;MDK-ARM&rdquo; 或者 &ldquo;Makefile&rdquo;，点击 &ldquo;GENERATE CODE&rdquo;。</p>
<h3 id="42-生成的代码结构">4.2 生成的代码结构</h3>
<p>CubeMX 生成的 USB 相关代码主要在这几个地方：</p>
<pre tabindex="0"><code>Core/Src/
├── usbd_conf.c          # 硬件配置，如 GPIO、DMA
└── usbd_desc.c          # 描述符定义（VID、PID、字符串等）

USB_DEVICE/
├── App/
│   ├── usbd_hid.c       # HID 应用层代码
│   └── usb_device.c     # USB 设备初始化
└── Target/
    ├── usbd_hid_if.c    # HID 接口层（我们主要改这个）
    └── usbd_conf_template.c
</code></pre><p>对我们来说，<strong>90% 的工作都在 <code>usbd_hid_if.c</code> 这个文件里</strong>。</p>
<hr>
<h2 id="五hid-报告描述符hid-设备的灵魂">五、HID 报告描述符：HID 设备的灵魂</h2>
<p>HID 设备和其他 USB 设备最大的区别，就是它有一个叫做**报告描述符（Report Descriptor）**的东西。</p>
<p>这也是 HID 开发中最让人头疼的部分——它不是简单的结构体，而是一种微型的字节码，解释起来相当反人类。</p>
<h3 id="51-什么是报告描述符">5.1 什么是报告描述符？</h3>
<p>报告描述符的作用是告诉主机：「我的数据是什么格式的」。</p>
<p>举个例子，一个标准的 3 键鼠标报告描述符会说：</p>
<ul>
<li>第 0 字节的 bit 0 是左键状态</li>
<li>第 0 字节的 bit 1 是右键状态</li>
<li>第 0 字节的 bit 2 是中键状态</li>
<li>第 1 字节是 X 轴相对位移（-127 到 +127）</li>
<li>第 2 字节是 Y 轴相对位移（-127 到 +127）</li>
</ul>
<p>主机收到数据后，就按照这个描述来解析每一位的含义。</p>
<h3 id="52-自定义-hid-设备的报告描述符">5.2 自定义 HID 设备的报告描述符</h3>
<p>对于自定义的数据采集设备，我们通常会把报告描述符简化成「原始字节数组」，让主机自己去解析：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="n">__ALIGN_BEGIN</span> <span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">HID_ReportDesc_FS</span><span class="p">[</span><span class="n">USBD_HID_REPORT_DESC_SIZE</span><span class="p">]</span> <span class="n">__ALIGN_END</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="mh">0x06</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span>      <span class="c1">// Usage Page (Vendor Defined)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x09</span><span class="p">,</span> <span class="mh">0x01</span><span class="p">,</span>            <span class="c1">// Usage (Vendor Usage 1)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0xA1</span><span class="p">,</span> <span class="mh">0x01</span><span class="p">,</span>            <span class="c1">// Collection (Application)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    
</span></span><span class="line"><span class="cl">    <span class="c1">// Input report: 设备 -&gt; 主机，64 字节
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x09</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span>            <span class="c1">//   Usage (Vendor Usage 2)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x15</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span>            <span class="c1">//   Logical Minimum (0)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x25</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">,</span>            <span class="c1">//   Logical Maximum (255)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x75</span><span class="p">,</span> <span class="mh">0x08</span><span class="p">,</span>            <span class="c1">//   Report Size (8 bits)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x95</span><span class="p">,</span> <span class="mh">0x40</span><span class="p">,</span>            <span class="c1">//   Report Count (64 items)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x81</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span>            <span class="c1">//   Input (Data, Variable, Absolute)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    
</span></span><span class="line"><span class="cl">    <span class="c1">// Output report: 主机 -&gt; 设备，64 字节
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x09</span><span class="p">,</span> <span class="mh">0x03</span><span class="p">,</span>            <span class="c1">//   Usage (Vendor Usage 3)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x15</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span>            <span class="c1">//   Logical Minimum (0)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x25</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">,</span>            <span class="c1">//   Logical Maximum (255)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x75</span><span class="p">,</span> <span class="mh">0x08</span><span class="p">,</span>            <span class="c1">//   Report Size (8 bits)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x95</span><span class="p">,</span> <span class="mh">0x40</span><span class="p">,</span>            <span class="c1">//   Report Count (64 items)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="mh">0x91</span><span class="p">,</span> <span class="mh">0x02</span><span class="p">,</span>            <span class="c1">//   Output (Data, Variable, Absolute)
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    
</span></span><span class="line"><span class="cl">    <span class="mh">0xC0</span>                   <span class="c1">// End Collection
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="p">};</span>
</span></span></code></pre></div><p>这个描述符定义了：</p>
<ul>
<li>输入报告（设备→主机）：64 字节原始数据</li>
<li>输出报告（主机→设备）：64 字节原始数据</li>
</ul>
<p>简单粗暴，但非常实用。主机端不需要关心每一位是什么，直接按字节读写就行。</p>
<h3 id="53-报告描述符的常见坑">5.3 报告描述符的常见坑</h3>
<ol>
<li>
<p><strong>长度不匹配</strong>：<code>USBD_HID_REPORT_DESC_SIZE</code> 宏定义必须和实际的描述符字节数完全一致，差一个字节都不行。</p>
</li>
<li>
<p><strong>最大包长限制</strong>：全速 HID 设备的报告长度不能超过端点的最大包长（64 字节）。超过了就需要分包发送。</p>
</li>
<li>
<p><strong>报告 ID</strong>：如果你用了报告 ID，那么每个数据的第一个字节必须是 ID，实际数据从第二个字节开始。新手建议不要用报告 ID。</p>
</li>
</ol>
<hr>
<h2 id="六hid-数据收发三个核心函数">六、HID 数据收发：三个核心函数</h2>
<p>现在我们来看具体的代码实现。HID 设备的数据收发主要靠三个函数。</p>
<h3 id="61-发送数据usbd_hid_sendreport">6.1 发送数据：<code>USBD_HID_SendReport</code></h3>
<p>设备主动给主机发数据用这个函数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">uint8_t</span> <span class="nf">USBD_HID_SendReport</span><span class="p">(</span><span class="n">USBD_HandleTypeDef</span> <span class="o">*</span><span class="n">pdev</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                            <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">report</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">                            <span class="kt">uint16_t</span> <span class="n">len</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>参数说明</strong>：</p>
<ul>
<li><code>pdev</code>: USB 设备句柄，全局变量 <code>hUsbDeviceFS</code></li>
<li><code>report</code>: 要发送的数据缓冲区指针</li>
<li><code>len</code>: 数据长度，不能超过最大包长</li>
</ul>
<p><strong>使用示例</strong>：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 定义发送缓冲区
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">uint8_t</span> <span class="n">tx_buffer</span><span class="p">[</span><span class="mi">64</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 填充数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">tx_buffer</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x01</span><span class="p">;</span>        <span class="c1">// 状态字节
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">tx_buffer</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">sensor1</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">;</span> <span class="c1">// 传感器数据高字节
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="n">tx_buffer</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">sensor1</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">tx_buffer</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="n">sensor2</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="n">tx_buffer</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="n">sensor2</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 发送数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nf">USBD_HID_SendReport</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hUsbDeviceFS</span><span class="p">,</span> <span class="n">tx_buffer</span><span class="p">,</span> <span class="mi">64</span><span class="p">);</span>
</span></span></code></pre></div><p><strong>重要注意事项</strong>：</p>
<ul>
<li>这个函数是<strong>非阻塞</strong>的，它只是把数据放到发送队列里就返回了</li>
<li>上一包发送完成之前，不能调用下一次发送</li>
<li>如果返回 <code>USBD_BUSY</code>，说明上一包还在发，需要等一会儿再试</li>
</ul>
<h3 id="62-发送完成回调hid_outevent_fs">6.2 发送完成回调：<code>HID_OutEvent_FS</code></h3>
<p>当一帧数据成功发送到主机后，会调用这个回调函数：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int8_t</span> <span class="nf">HID_OutEvent_FS</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">event_idx</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">state</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 数据已经成功发出，可以准备下一包了
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="n">tx_in_progress</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>  <span class="c1">// 清除发送忙标志
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">USBD_OK</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这是实现连续数据发送的关键。正确的做法是：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">volatile</span> <span class="kt">uint8_t</span> <span class="n">tx_busy</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">send_data</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">,</span> <span class="kt">uint16_t</span> <span class="n">len</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="n">tx_busy</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 等待上一次发送完成
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">tx_busy</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">USBD_HID_SendReport</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hUsbDeviceFS</span><span class="p">,</span> <span class="n">data</span><span class="p">,</span> <span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int8_t</span> <span class="nf">HID_OutEvent_FS</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">event_idx</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">state</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">tx_busy</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>  <span class="c1">// 发送完成，清除忙标志
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="n">USBD_OK</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="63-接收数据hid_dataout">6.3 接收数据：<code>HID_DataOut</code></h3>
<p>当主机给设备发数据时，数据会先存在内部缓冲区，然后调用 <code>Receive</code> 回调：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int8_t</span> <span class="nf">HID_DataOut_FS</span><span class="p">(</span><span class="kt">uint8_t</span><span class="o">*</span> <span class="n">buf</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">len</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// buf 里是主机发来的数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// len 是实际收到的字节数
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    
</span></span><span class="line"><span class="cl">    <span class="c1">// 处理接收到的数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">process_host_command</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="o">*</span><span class="n">len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">USBD_OK</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>注意：接收到的数据长度可能不是 64 字节。如果主机只发了 10 字节，<code>len</code> 就是 10。</p>
<h3 id="64-一个完整的示例adc-数据采集">6.4 一个完整的示例：ADC 数据采集</h3>
<p>把这些组合起来，我们可以做一个通过 USB 上传 ADC 数据的例子：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 在主循环中
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">tx_busy</span> <span class="o">&amp;&amp;</span> <span class="n">adc_data_ready</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// 采集 ADC 数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="kt">uint16_t</span> <span class="n">adc_val</span> <span class="o">=</span> <span class="nf">HAL_ADC_GetValue</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hadc1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1">// 填充发送缓冲区
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">tx_buffer</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0xAA</span><span class="p">;</span>           <span class="c1">// 帧头
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">tx_buffer</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">=</span> <span class="n">adc_val</span> <span class="o">&gt;&gt;</span> <span class="mi">8</span><span class="p">;</span>   <span class="c1">// 高字节
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">tx_buffer</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="o">=</span> <span class="n">adc_val</span> <span class="o">&amp;</span> <span class="mh">0xFF</span><span class="p">;</span> <span class="c1">// 低字节
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">tx_buffer</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="mh">0x55</span><span class="p">;</span>           <span class="c1">// 帧尾
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        
</span></span><span class="line"><span class="cl">        <span class="c1">// 发送
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="n">tx_busy</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="nf">USBD_HID_SendReport</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hUsbDeviceFS</span><span class="p">,</span> <span class="n">tx_buffer</span><span class="p">,</span> <span class="mi">64</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="n">adc_data_ready</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 在 ADC 中断回调中
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="nf">HAL_ADC_ConvCpltCallback</span><span class="p">(</span><span class="n">ADC_HandleTypeDef</span><span class="o">*</span> <span class="n">hadc</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">adc_data_ready</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>就这么简单。每隔一段时间，ADC 采集完成后，设备就会把数据通过 USB HID 发给主机。</p>
<hr>
<h2 id="七hid-开发的常见问题与解决">七、HID 开发的常见问题与解决</h2>
<p>HID 开发听起来简单，但实际做起来坑不少。我把最常见的问题列在这里。</p>
<h3 id="问题-1设备能枚举但收不到数据">问题 1：设备能枚举，但收不到数据</h3>
<p><strong>现象</strong>：设备管理器里能看到 HID 设备，但是主机端程序收不到任何数据。</p>
<p><strong>可能原因</strong>：</p>
<ul>
<li>轮询间隔设得太大了。比如设成 255ms，主机每隔 255ms 才来问一次，你感觉就像没反应。改成 10ms 试试。</li>
<li><code>USBD_HID_SendReport</code> 的长度参数超过了最大包长。全速设备不能超过 64 字节。</li>
<li>你在中断里调用了发送函数，但是中断优先级比 USB 中断高，导致死锁。</li>
</ul>
<h3 id="问题-2连续发送丢包">问题 2：连续发送丢包</h3>
<p><strong>现象</strong>：发第一包正常，第二包开始就丢包。</p>
<p><strong>原因</strong>：上一包还没发完就调用了下一次 <code>USBD_HID_SendReport</code>。这个函数不会帮你排队，后发的会直接覆盖先发的。</p>
<p><strong>解决</strong>：用一个标志位，只有在上一次发送完成回调之后才能发下一次。参考上面的 <code>tx_busy</code> 例子。</p>
<h3 id="问题-3windows-下识别为未知设备">问题 3：Windows 下识别为「未知设备」</h3>
<p><strong>现象</strong>：设备插上去，Windows 提示「无法识别的 USB 设备」，设备管理器里显示黄色感叹号。</p>
<p><strong>排查步骤</strong>：</p>
<ol>
<li>先检查硬件：D+ 上拉电阻接了吗？电压是 3.3V 吗？</li>
<li>再检查时钟：USB 时钟是不是精确的 48MHz？</li>
<li>然后检查描述符：VID/PID 有没有和其他设备冲突？字符串描述符的编码正确吗？</li>
<li>最后用 USBlyzer 抓包，看枚举过程卡在哪一步了。</li>
</ol>
<h3 id="问题-4热插拔后不工作">问题 4：热插拔后不工作</h3>
<p><strong>现象</strong>：第一次插上去正常，拔下来再插就没反应了。</p>
<p><strong>原因</strong>：热插拔时 USB 的中断状态机没有正确复位。</p>
<p><strong>解决</strong>：在 Vbus 检测的中断处理里调用 <code>USBD_Stop(&amp;hUsbDeviceFS)</code> 然后重新 <code>USBD_Start(&amp;hUsbDeviceFS)</code>。</p>
<p>（第二部分完，约 2600 字）</p>
<hr>
<h2 id="八cdc-虚拟串口嵌入式开发者的瑞士军刀">八、CDC 虚拟串口：嵌入式开发者的瑞士军刀</h2>
<p>如果说 HID 适合小数据量的免驱动设备，那么 <strong>CDC（通信设备类）</strong> 就是嵌入式开发中最通用的 USB 方案。</p>
<p>CDC 虚拟串口的好处太多了：</p>
<ul>
<li>Windows / Linux / macOS 全都自带驱动</li>
<li>主机端用普通的串口软件就能通信</li>
<li>波特率是「假的」，实际速度可以达到 1MB/s 以上</li>
<li>协议简单，就是字节流，不用搞复杂的报告描述符</li>
</ul>
<p>对于调试控制台、数据记录仪、固件更新工具这些场景，CDC 虚拟串口几乎是完美的选择。</p>
<h3 id="81-cdc-的原理两个接口--三个端点">8.1 CDC 的原理：两个接口 + 三个端点</h3>
<p>CDC 和 HID 最大的区别是：它用了<strong>两个接口</strong>和<strong>三个端点</strong>：</p>
<ul>
<li>
<p><strong>通信接口（Communication Interface）</strong></p>
<ul>
<li>端点 0：控制端点，用于设置波特率、奇偶校验等</li>
<li>端点 2 IN：中断端点，用于上报线路状态变化</li>
</ul>
</li>
<li>
<p><strong>数据接口（Data Interface）</strong></p>
<ul>
<li>端点 1 OUT：批量端点，主机 → 设备</li>
<li>端点 1 IN：批量端点，设备 → 主机</li>
</ul>
</li>
</ul>
<p>这就是为什么你在设备管理器里看到 CDC 设备会显示两个接口。不过这些细节 ST 的库都帮我们处理好了，我们只需要关心数据收发。</p>
<h3 id="82-cubemx-配置-cdc">8.2 CubeMX 配置 CDC</h3>
<p>配置 CDC 和配置 HID 大同小异：</p>
<ol>
<li>同样先配置时钟，USB 必须是 48MHz</li>
<li>使能 USB_OTG_FS 的 Device_Only 模式</li>
<li>在 USB_DEVICE 中间件中选择 &ldquo;Communication Device Class (CDC)&rdquo;</li>
<li>参数保持默认就行</li>
<li>生成代码</li>
</ol>
<p>生成的代码结构也差不多，主要改的是 <code>usbd_cdc_if.c</code> 这个文件。</p>
<hr>
<h2 id="九cdc-数据收发环形缓冲区是关键">九、CDC 数据收发：环形缓冲区是关键</h2>
<p>CDC 的数据收发接口比 HID 稍微复杂一点，因为它是字节流模型，不是数据包模型。</p>
<h3 id="91-发送数据cdc_transmit_fs">9.1 发送数据：<code>CDC_Transmit_FS</code></h3>
<p>发送函数的原型：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">uint8_t</span> <span class="nf">CDC_Transmit_FS</span><span class="p">(</span><span class="kt">uint8_t</span><span class="o">*</span> <span class="n">Buf</span><span class="p">,</span> <span class="kt">uint16_t</span> <span class="n">Len</span><span class="p">);</span>
</span></span></code></pre></div><p>这和 HID 的 <code>USBD_HID_SendReport</code> 很像，用法也差不多：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">uint8_t</span> <span class="n">data</span><span class="p">[]</span> <span class="o">=</span> <span class="s">&#34;Hello, USB CDC!</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="nf">CDC_Transmit_FS</span><span class="p">(</span><span class="n">data</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">data</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">);</span>
</span></span></code></pre></div><p>但是这里有个和 HID 一样的坑：<strong>上一包发完之前，不能发下一包</strong>。如果你连续调用两次 <code>CDC_Transmit_FS</code>，第二包会直接丢了。</p>
<h3 id="92-发送完成回调cdc_transmitcplt_fs">9.2 发送完成回调：<code>CDC_TransmitCplt_FS</code></h3>
<p>数据发送完成后会调用这个回调：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int8_t</span> <span class="nf">CDC_TransmitCplt_FS</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">Buf</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">Len</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">epnum</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">tx_busy</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>  <span class="c1">// 发送完成，清除忙标志
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">return</span> <span class="n">USBD_OK</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="93-接收数据cdc_receive_fs">9.3 接收数据：<code>CDC_Receive_FS</code></h3>
<p>主机发来的数据会通过这个回调交给我们：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="k">static</span> <span class="kt">int8_t</span> <span class="nf">CDC_Receive_FS</span><span class="p">(</span><span class="kt">uint8_t</span><span class="o">*</span> <span class="n">Buf</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">Len</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// Buf 是接收到的数据
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="c1">// Len 是收到的字节数
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    
</span></span><span class="line"><span class="cl">    <span class="c1">// 处理数据，比如放入环形缓冲区
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">for</span> <span class="p">(</span><span class="kt">uint32_t</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="o">*</span><span class="n">Len</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">ring_buffer_write</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rx_ring</span><span class="p">,</span> <span class="n">Buf</span><span class="p">[</span><span class="n">i</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="c1">// 重要！必须重新准备接收
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="nf">USBD_CDC_SetRxBuffer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hUsbDeviceFS</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">USBD_CDC_ReceivePacket</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hUsbDeviceFS</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">USBD_OK</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><blockquote>
<p><strong>非常重要的细节</strong>：处理完数据后，必须调用 <code>USBD_CDC_SetRxBuffer</code> 和 <code>USBD_CDC_ReceivePacket</code> 重新准备下一次接收。如果忘了这两行，收完第一包之后就再也收不到数据了。这是 CDC 开发的第一大坑。</p>
</blockquote>
<hr>
<h2 id="十实战一个完整的-usb-串口调试控制台">十、实战：一个完整的 USB 串口调试控制台</h2>
<p>现在我们把这些知识整合起来，做一个实用的 USB 调试控制台。</p>
<h3 id="101-实现-printf-重定向">10.1 实现 <code>printf</code> 重定向</h3>
<p>最常用的需求就是让 <code>printf</code> 的输出通过 USB 串口发出去。在 STM32 上这很简单：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&#34;usbd_cdc_if.h&#34;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="k">volatile</span> <span class="kt">uint8_t</span> <span class="n">cdc_tx_busy</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 发送完成回调
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="k">static</span> <span class="kt">int8_t</span> <span class="nf">CDC_TransmitCplt_FS</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="o">*</span><span class="n">Buf</span><span class="p">,</span> <span class="kt">uint32_t</span> <span class="o">*</span><span class="n">Len</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">epnum</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">cdc_tx_busy</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">USBD_OK</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// 重定向 fputc
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">int</span> <span class="nf">fputc</span><span class="p">(</span><span class="kt">int</span> <span class="n">ch</span><span class="p">,</span> <span class="n">FILE</span> <span class="o">*</span><span class="n">f</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="c1">// 等待上一次发送完成
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="k">while</span> <span class="p">(</span><span class="n">cdc_tx_busy</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">static</span> <span class="kt">uint8_t</span> <span class="n">buf</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="kt">uint8_t</span><span class="p">)</span><span class="n">ch</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="n">cdc_tx_busy</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="nf">CDC_Transmit_FS</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">ch</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>现在你就可以直接用 <code>printf</code> 输出调试信息了：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">printf</span><span class="p">(</span><span class="s">&#34;System started. CPU Clock = %lu MHz</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">SystemCoreClock</span> <span class="o">/</span> <span class="mi">1000000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="nf">printf</span><span class="p">(</span><span class="s">&#34;ADC Value = %d, Temperature = %.1f C</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">adc_val</span><span class="p">,</span> <span class="n">temperature</span><span class="p">);</span>
</span></span></code></pre></div><h3 id="102-实现命令行解析">10.2 实现命令行解析</h3>
<p>更进一步，我们可以实现一个简单的命令行，让主机可以发命令控制设备：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 简单的命令解析
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kt">void</span> <span class="nf">process_command</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">cmd</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nf">strcmp</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="s">&#34;help&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Available commands:</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  help    - Show this help</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  status  - Show system status</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  led on  - Turn LED on</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  led off - Turn LED off</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  reset   - System reset</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nf">strcmp</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="s">&#34;status&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;System Status:</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  CPU Clock: %lu MHz</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">SystemCoreClock</span> <span class="o">/</span> <span class="mi">1000000</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  SysTick: %lu ms</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="nf">HAL_GetTick</span><span class="p">());</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;  USB Connected: %s</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> 
</span></span><span class="line"><span class="cl">               <span class="n">hUsbDeviceFS</span><span class="p">.</span><span class="n">dev_state</span> <span class="o">==</span> <span class="n">USBD_STATE_CONFIGURED</span> <span class="o">?</span> <span class="s">&#34;Yes&#34;</span> <span class="o">:</span> <span class="s">&#34;No&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nf">strcmp</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="s">&#34;led on&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">HAL_GPIO_WritePin</span><span class="p">(</span><span class="n">LED_GPIO_Port</span><span class="p">,</span> <span class="n">LED_Pin</span><span class="p">,</span> <span class="n">GPIO_PIN_SET</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;LED turned ON</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nf">strcmp</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="s">&#34;led off&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">HAL_GPIO_WritePin</span><span class="p">(</span><span class="n">LED_GPIO_Port</span><span class="p">,</span> <span class="n">LED_Pin</span><span class="p">,</span> <span class="n">GPIO_PIN_RESET</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;LED turned OFF</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Unknown command: &#39;%s&#39;</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">,</span> <span class="n">cmd</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="103-环形缓冲区实现">10.3 环形缓冲区实现</h3>
<p>为了处理不连续的数据流，我们需要一个环形缓冲区：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define RING_BUF_SIZE 1024
</span></span></span><span class="line"><span class="cl"><span class="cp"></span>
</span></span><span class="line"><span class="cl"><span class="k">typedef</span> <span class="k">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint8_t</span> <span class="n">buf</span><span class="p">[</span><span class="n">RING_BUF_SIZE</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="k">volatile</span> <span class="kt">uint32_t</span> <span class="n">head</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">volatile</span> <span class="kt">uint32_t</span> <span class="n">tail</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span> <span class="kt">ring_buffer_t</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">ring_buffer_init</span><span class="p">(</span><span class="kt">ring_buffer_t</span> <span class="o">*</span><span class="n">rb</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">rb</span><span class="o">-&gt;</span><span class="n">head</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="n">rb</span><span class="o">-&gt;</span><span class="n">tail</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">ring_buffer_write</span><span class="p">(</span><span class="kt">ring_buffer_t</span> <span class="o">*</span><span class="n">rb</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">uint32_t</span> <span class="n">next_head</span> <span class="o">=</span> <span class="p">(</span><span class="n">rb</span><span class="o">-&gt;</span><span class="n">head</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="n">RING_BUF_SIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">next_head</span> <span class="o">!=</span> <span class="n">rb</span><span class="o">-&gt;</span><span class="n">tail</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">rb</span><span class="o">-&gt;</span><span class="n">buf</span><span class="p">[</span><span class="n">rb</span><span class="o">-&gt;</span><span class="n">head</span><span class="p">]</span> <span class="o">=</span> <span class="n">data</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="n">rb</span><span class="o">-&gt;</span><span class="n">head</span> <span class="o">=</span> <span class="n">next_head</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">ring_buffer_read</span><span class="p">(</span><span class="kt">ring_buffer_t</span> <span class="o">*</span><span class="n">rb</span><span class="p">,</span> <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">rb</span><span class="o">-&gt;</span><span class="n">head</span> <span class="o">==</span> <span class="n">rb</span><span class="o">-&gt;</span><span class="n">tail</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="p">;</span>  <span class="c1">// 缓冲区空
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="o">*</span><span class="n">data</span> <span class="o">=</span> <span class="n">rb</span><span class="o">-&gt;</span><span class="n">buf</span><span class="p">[</span><span class="n">rb</span><span class="o">-&gt;</span><span class="n">tail</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">    <span class="n">rb</span><span class="o">-&gt;</span><span class="n">tail</span> <span class="o">=</span> <span class="p">(</span><span class="n">rb</span><span class="o">-&gt;</span><span class="n">tail</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">%</span> <span class="n">RING_BUF_SIZE</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>然后在主循环中处理接收到的命令：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="kt">ring_buffer_t</span> <span class="n">rx_ring</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">char</span> <span class="n">line_buf</span><span class="p">[</span><span class="mi">256</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">line_pos</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nf">HAL_Init</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nf">SystemClock_Config</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nf">MX_USB_DEVICE_Init</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="nf">ring_buffer_init</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rx_ring</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;USB CDC Console Ready!</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;Type &#39;help&#39; for available commands</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;&gt; &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">while</span> <span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kt">uint8_t</span> <span class="n">ch</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nf">ring_buffer_read</span><span class="p">(</span><span class="o">&amp;</span><span class="n">rx_ring</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">ch</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">            <span class="c1">// 回显
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>            <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;%c&#34;</span><span class="p">,</span> <span class="n">ch</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">            
</span></span><span class="line"><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="n">ch</span> <span class="o">==</span> <span class="sc">&#39;\r&#39;</span> <span class="o">||</span> <span class="n">ch</span> <span class="o">==</span> <span class="sc">&#39;\n&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="k">if</span> <span class="p">(</span><span class="n">line_pos</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                    <span class="n">line_buf</span><span class="p">[</span><span class="n">line_pos</span><span class="p">]</span> <span class="o">=</span> <span class="sc">&#39;\0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;</span><span class="se">\r\n</span><span class="s">&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="nf">process_command</span><span class="p">(</span><span class="n">line_buf</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                    <span class="n">line_pos</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">                    <span class="nf">printf</span><span class="p">(</span><span class="s">&#34;&gt; &#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">                <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">ch</span> <span class="o">==</span> <span class="sc">&#39;\b&#39;</span> <span class="o">&amp;&amp;</span> <span class="n">line_pos</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="n">line_pos</span><span class="o">--</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">            <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">line_pos</span> <span class="o">&lt;</span> <span class="k">sizeof</span><span class="p">(</span><span class="n">line_buf</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">                <span class="n">line_buf</span><span class="p">[</span><span class="n">line_pos</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">ch</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>这样你就有了一个功能完整的 USB 调试控制台。插上 USB，打开串口助手，就能像操作 Linux 终端一样操作你的 STM32 了。</p>
<hr>
<h2 id="十一cdc-开发的常见坑">十一、CDC 开发的常见坑</h2>
<h3 id="坑-1收完第一包就再也收不到了">坑 1：收完第一包就再也收不到了</h3>
<p><strong>原因</strong>：在 <code>CDC_Receive_FS</code> 回调里忘了重新准备接收。</p>
<p><strong>解决</strong>：一定要在回调最后加上：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="nf">USBD_CDC_SetRxBuffer</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hUsbDeviceFS</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">Buf</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
</span></span><span class="line"><span class="cl"><span class="nf">USBD_CDC_ReceivePacket</span><span class="p">(</span><span class="o">&amp;</span><span class="n">hUsbDeviceFS</span><span class="p">);</span>
</span></span></code></pre></div><h3 id="坑-2连续发送丢包">坑 2：连续发送丢包</h3>
<p><strong>原因</strong>：上一包没发完就发下一包。</p>
<p><strong>解决</strong>：用 <code>tx_busy</code> 标志位，发送完成回调里再清除。</p>
<h3 id="坑-3windows-下能枚举但打不开串口">坑 3：Windows 下能枚举但打不开串口</h3>
<p><strong>原因</strong>：Windows 10 之后的 CDC 驱动有个 bug，如果设备描述符里的 iSerialNumber 是空的，有时候会打不开。</p>
<p><strong>解决</strong>：在 <code>usbd_desc.c</code> 里给序列号字符串设一个非空的值：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define USBD_SERIALNUMBER_FS_STRING     &#34;202405290001&#34;
</span></span></span></code></pre></div><h3 id="坑-4linux-下权限不够">坑 4：Linux 下权限不够</h3>
<p><strong>现象</strong>：Windows 正常，Linux 下提示 Permission denied。</p>
<p><strong>解决</strong>：添加 udev 规则：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;SUBSYSTEM==&#34;tty&#34;, ATTRS{idVendor}==&#34;0483&#34;, ATTRS{idProduct}==&#34;5740&#34;, MODE=&#34;0666&#34;&#39;</span> <span class="p">|</span> sudo tee /etc/udev/rules.d/99-stm32-cdc.rules
</span></span><span class="line"><span class="cl">sudo udevadm control --reload-rules
</span></span></code></pre></div><hr>
<h2 id="十二进阶技巧与性能优化">十二、进阶技巧与性能优化</h2>
<h3 id="121-提高-cdc-发送速度">12.1 提高 CDC 发送速度</h3>
<p>默认的 CDC 发送速度可能只有几十 KB/s，但优化后可以达到 1MB/s 以上：</p>
<p><strong>优化 1：增大最大包长</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="c1">// 在 usbd_cdc.h 里修改
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="cp">#define CDC_DATA_FS_MAX_PACKET_SIZE    64  </span><span class="c1">// 改成 64（全速最大）
</span></span></span></code></pre></div><p><strong>优化 2：用 DMA 传输数据</strong>
如果你的数据放在 SRAM 里，可以让 DMA 直接从 SRAM 搬到 USB FIFO，节省 CPU 时间。</p>
<p><strong>优化 3：批量发送</strong>
不要一个字节一个字节地发，攒到 64 字节再发一次：</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-c" data-lang="c"><span class="line"><span class="cl"><span class="cp">#define TX_BUF_SIZE 64
</span></span></span><span class="line"><span class="cl"><span class="cp"></span><span class="kt">uint8_t</span> <span class="n">tx_buf</span><span class="p">[</span><span class="n">TX_BUF_SIZE</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">tx_buf_len</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">cdc_putc</span><span class="p">(</span><span class="kt">uint8_t</span> <span class="n">ch</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">tx_buf</span><span class="p">[</span><span class="n">tx_buf_len</span><span class="o">++</span><span class="p">]</span> <span class="o">=</span> <span class="n">ch</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">tx_buf_len</span> <span class="o">&gt;=</span> <span class="n">TX_BUF_SIZE</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nf">cdc_flush</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kt">void</span> <span class="nf">cdc_flush</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">tx_buf_len</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">tx_busy</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">tx_busy</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="nf">CDC_Transmit_FS</span><span class="p">(</span><span class="n">tx_buf</span><span class="p">,</span> <span class="n">tx_buf_len</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="n">tx_buf_len</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="122-支持多串口">12.2 支持多串口</h3>
<p>需要多个虚拟串口？没问题，CDC 支持复合设备。你可以配置多个 CDC 接口，每个接口有自己的端点，这样就能在主机端看到多个 COM 口了。</p>
<h3 id="123-hid--cdc-复合设备">12.3 HID + CDC 复合设备</h3>
<p>最实用的组合：用 CDC 做调试控制台，用 HID 做实时数据传输。这样调试和数据采集互不干扰，而且都免驱动。</p>
<p>CubeMX 里可以直接配置成多接口的复合设备，也可以手动修改描述符把 HID 和 CDC 放在同一个配置里。</p>
<hr>
<h2 id="十三总结">十三、总结</h2>
<p>回顾一下我们这篇文章讲的内容：</p>
<ol>
<li><strong>USB 基础概念</strong>：主从架构、端点、传输类型、描述符</li>
<li><strong>STM32 USB 硬件</strong>：OTG_FS 和 OTG_HS 的区别</li>
<li><strong>HID 设备开发</strong>：报告描述符、数据收发、常见问题</li>
<li><strong>CDC 虚拟串口</strong>：接口结构、收发函数、命令行实现</li>
<li><strong>性能优化</strong>：提高传输速度的几个技巧</li>
</ol>
<p>USB 开发确实有很多细节和坑，但只要你理解了底层的工作原理，剩下的就只是按部就班的代码实现。记住几个关键点：</p>
<ul>
<li><strong>时钟必须精确</strong>：48MHz 差一点都不行</li>
<li><strong>描述符不能错</strong>：90% 的枚举失败都是描述符的问题</li>
<li><strong>发送要有流控</strong>：永远不要在上一包发完之前发下一包</li>
<li><strong>接收要重新准备</strong>：CDC 收完一包一定要调用 <code>ReceivePacket</code></li>
</ul>
<p>从实用性角度，我给大家的建议是：</p>
<ul>
<li>如果只是小数据量采集，优先用 <strong>HID</strong>，真正免驱动</li>
<li>如果需要调试控制台、大数据传输，优先用 <strong>CDC</strong>，开发最简单</li>
<li>如果需要超高速传输，上 <strong>高速 USB + 批量传输</strong></li>
</ul>
<p>最后，USB 开发最好的老师就是 USB 协议规范本身。虽然它有 600 多页，但你不需要全部看完。遇到问题的时候，去查对应的章节，你会发现所有的答案都在里面。</p>
<p>希望这篇文章能帮你跨过 USB 开发的门槛。当你第一次看到自己做的设备被电脑识别出来，串口助手里打出 &ldquo;Hello World&rdquo; 的时候，那种成就感是无法言喻的。</p>
<p>（全文完，约 7800 字）</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
