嵌入式开发笔记:USB通信软件配置完全指南——从驱动安装到应用开发

1. 前言:硬件连上了,软件怎么配?

在嵌入式开发中,USB硬件电路设计完成、PCB打样回来、焊接好接口之后,真正的挑战才刚刚开始——软件层面的配置

你可能会遇到这样的场景:把设备插上电脑,设备管理器里出现了“未知设备”;或者设备能被识别,但无法通信;又或者Windows上跑得好好的,换到Linux下就完全不认了。

USB通信的软件配置是一个系统工程,涉及设备枚举、驱动安装、固件描述符配置、操作系统驱动栈匹配等多个环节。任何一个环节出了问题,都会导致通信失败。

本文将从设备端到主机端、从固件到驱动、从Windows到Linux,系统讲解USB通信的软件配置全流程。全程不涉及硬件电路设计,纯软件配置实战。

在这里插入图片描述

2. USB通信的软件架构

2.1 主机端的驱动栈

在主机端(PC),USB通信依赖于一套层次分明的驱动栈

以Windows为例,USB驱动栈从下到上大致为:

层级 组件 功能
硬件层 USB主机控制器(EHCI/xHCI) 物理层数据收发
总线驱动层 usbport.sys、usbhub.sys 端口管理、设备枚举、电源管理
类驱动层 usbstor.sys、hidclass.sys等 实现特定设备类的通用功能
客户端驱动层 用户自定义驱动 / WinUSB 与具体设备通信的应用层驱动

当USB设备插入时,PnP(即插即用)管理器会枚举设备、读取描述符,然后根据设备的VID/PID(厂商ID/产品ID)设备类代码,在系统中搜索匹配的驱动程序。

2.2 设备端的固件架构

在设备端(嵌入式MCU),USB通信由固件程序驱动。固件需要实现:

  1. USB协议栈:处理标准请求(GET_DESCRIPTOR、SET_ADDRESS等)
  2. 端点(Endpoint)管理:配置各端点的方向和传输类型
  3. 设备类请求处理:实现特定设备类(CDC、HID、MSC等)的功能

2.3 USB通信的标准流程

USB通信的软件流程可以概括为三个阶段:

阶段一:设备枚举。主机识别设备、获取描述符、分配地址。

阶段二:驱动加载。主机根据设备信息加载对应的驱动程序。

阶段三:数据传输。应用程序通过驱动与设备进行数据交换。

3. 设备枚举:USB通信的“第一步”

3.1 什么是枚举

枚举是USB设备上电后与主机建立通信的第一步。你可以把枚举理解为一个“入职面试”——主机(老板)对新插入的设备(新员工)进行一系列的“提问”,了解它的身份和能力。

主机通过发送标准请求(如GET_DESCRIPTOR、SET_ADDRESS)获取设备信息并分配地址。整个流程由主机主动发起,设备被动响应。

3.2 枚举的完整流程

以下是USB枚举的标准步骤:

步骤1:设备插入与供电
设备接上USB线后上电,处于供电状态。

步骤2:主机检测设备插入
主机通过D+/D-的电平变化检测到设备插入。

步骤3:复位设备
主机对设备进行复位操作。

步骤4:获取设备描述符(8字节)
主机发送GET_DESCRIPTOR请求,获取设备描述符的前8字节。这8字节包含了设备的最大包长度信息,主机据此知道后续应该如何传输数据。

步骤5:设置设备地址
主机发送SET_ADDRESS请求,为设备分配一个1-127之间的唯一地址。

步骤6:获取完整设备描述符(18字节)
主机再次发送GET_DESCRIPTOR请求,获取完整的18字节设备描述符。

步骤7:获取配置描述符
主机获取配置描述符、接口描述符和端点描述符。这一步可能会获取多次。

步骤8:获取字符串描述符
主机获取设备相关的字符串描述符(如厂商名、产品名、序列号)。

步骤9:获取设备限定描述符
用于获取设备在其他速度模式下的信息。如果设备不支持,必须回复STALL。

步骤10:设置配置
主机发送SET_CONFIGURATION请求,对设备的端点进行配置。

步骤11:加载类驱动
主机根据接口描述符加载对应的类驱动程序(如CDC_ACM、HID、MSC等)。

步骤12:类特定请求
驱动加载完成后,执行该类相关的特定请求。

步骤13:数据传输
进入正常的数据流传输阶段。

在这里插入图片描述

3.3 描述符:USB设备的“身份证”

枚举过程中,主机获取的所有信息都以描述符(Descriptor) 的形式存在。描述符是USB通信的基石——主机通过它们决定加载哪个驱动程序、分配多少带宽和电源、以及理解数据的组织格式。

描述符类型 包含信息 作用
设备描述符 VID/PID、设备类、最大包长度 标识设备身份
配置描述符 接口数量、供电方式、最大功耗 描述设备的一种工作配置
接口描述符 接口号、设备类/子类/协议 描述一个功能(Function)
端点描述符 端点地址、传输类型、最大包大小 描述一个数据通道
字符串描述符 厂商名、产品名、序列号 提供人类可读的信息

💡 关键理解:一个USB设备可以有多个配置(Configuration),一个配置下可以有多个接口(Interface),一个接口下可以有多个端点(Endpoint)。这种层次结构让USB设备能够支持复杂的功能组合。

4. 设备端固件配置

4.1 USB协议栈的选择

在嵌入式MCU上实现USB通信,首先需要选择一个USB协议栈

方案 特点 适用场景
厂商SDK自带协议栈 与芯片深度绑定、稳定性高、文档齐全 STM32(STM32CubeUSB)、NXP、Infineon等官方开发
开源协议栈(CherryUSB) 轻量级、支持多厂商MCU、活跃的社区支持 需要跨平台、轻量化的项目
RTOS自带协议栈 与操作系统深度集成 RT-Thread、Zephyr等RTOS环境

4.2 描述符的配置(以CDC虚拟串口为例)

对于最常见的CDC(通信设备类)虚拟串口设备,需要在固件中配置以下描述符:

(1)设备描述符:定义VID/PID、设备类(0x02表示CDC)、最大包长度等。

(2)配置描述符:定义接口数量(CDC通常需要2个接口:数据接口+控制接口)、供电方式等。

(3)接口描述符:定义每个接口的功能。CDC虚拟串口通常包含:

  • 接口0(控制接口) :用于管理串口参数(波特率、数据位等)
  • 接口1(数据接口) :用于实际的数据收发

(4)端点描述符:定义数据通道。CDC虚拟串口通常需要:

  • 端点1(IN) :设备→主机的数据通道
  • 端点2(OUT) :主机→设备的数据通道

(5)字符串描述符:定义厂商名、产品名和序列号。

4.3 端点0(控制端点)的处理

端点0是所有USB设备都必须实现的默认控制端点。在设备完成配置之前,只有端点0可用,主机通过端点0来发送命令、获取设备信息、配置设备。

固件必须正确处理端点0上的所有标准请求,包括:

  • GET_DESCRIPTOR:返回各种描述符
  • SET_ADDRESS:设置设备地址
  • SET_CONFIGURATION:选择配置
  • GET_CONFIGURATION:获取当前配置
  • CLEAR_FEATURE / SET_FEATURE:控制端点特性

💡 工程要点:端点0的最大包大小在设备描述符中定义。对于全速设备,通常为8/16/32/64字节;对于高速设备,通常为64字节。

4.4 CDC虚拟串口的特殊配置

CDC虚拟串口除了标准USB描述符外,还需要配置:

  1. CDC类特定请求:处理SET_LINE_CODING(设置波特率)、GET_LINE_CODING(获取波特率)、SET_CONTROL_LINE_STATE(设置DTR/RTS)等请求

  2. 数据缓冲管理:实现IN端点和OUT端点的数据收发回调函数

  3. 串口参数映射:将USB CDC的波特率、数据位、停止位、奇偶校验等参数映射到实际的UART外设配置

4.5 STM32 CubeMX配置示例

以STM32F407实现USB CDC虚拟串口为例:

  1. 在CubeMX中启用USB_OTG_FS外设,模式选择Device_Only
  2. 在Middleware中选择USB_DEVICE,Class选择Communication Device Class (Virtual Port COM)
  3. 配置USB中断优先级
  4. 生成代码后,实现CDC接收回调函数CDC_Receive_FS和发送函数CDC_Transmit_FS
  5. 编译烧录后,插入电脑即可识别为虚拟串口

5. Windows平台驱动配置

5.1 通用驱动方案对比

Windows下访问USB设备,主要有三种驱动方案:

方案 特点 适用场景
系统内置类驱动 Windows自带,免安装 CDC虚拟串口、HID设备、大容量存储等标准设备类
WinUSB 通用USB驱动,无需编写INF 自定义USB设备、批量/中断/同步传输
自定义驱动(KMDF/UMDF) 完全控制、功能最强大 需要特殊功能、性能要求极高的场景

5.2 CDC虚拟串口驱动安装

对于CDC虚拟串口设备,Windows通常会自动识别并安装usbser.sys驱动。

如果系统没有自动识别:

  1. 打开设备管理器,找到带黄色感叹号的“未知设备”
  2. 右键选择 “更新驱动程序”
  3. 选择 “浏览计算机以查找驱动程序软件”
  4. 选择 “从计算机的设备驱动程序列表中选择”
  5. 选择 “通用串行总线设备”“USB串行设备”

如果仍然无法识别,可能需要安装芯片厂商提供的专用驱动(如CH340的CH341SER.EXE)。

5.3 CH340驱动安装

CH340是最常见的USB转串口芯片。驱动安装方法:

方法一(自动安装)

  1. 将CH340设备插入电脑USB口
  2. Windows会自动尝试安装驱动

方法二(手动安装)

  1. 访问南京沁恒微电子网站,下载CH340驱动
  2. 解压驱动包,找到CH341SER.EXE
  3. 右键 “以管理员身份运行”
  4. 点击 “安装” ,等待完成

验证:打开设备管理器 → 端口(COM和LPT),应能看到“USB-SERIAL CH340 (COMx)”。

⚠️ 常见问题:如果安装失败,可能是系统兼容性问题、驱动冲突或权限不足。尝试卸载旧驱动后重新安装,或更换USB线缆。

5.4 WinUSB驱动安装

对于自定义USB设备,WinUSB是最便捷的驱动方案。

方法一(使用Zadig工具)

  1. 下载并运行Zadig
  2. 选择目标USB设备
  3. 选择驱动为 WinUSB
  4. 点击 “Install Driver”

方法二(手动安装)

  1. 在设备管理器中右键点击设备 → 更新驱动程序
  2. 选择“浏览计算机以查找驱动程序软件”
  3. 选择“从计算机的设备驱动程序列表中选择”
  4. 选择 “WinUSB Device”

💡 原理说明:WinUSB使用Winusb.sys作为功能驱动,应用程序通过WinUSB API与设备通信。无需编写自定义INF文件即可完成安装。

5.5 自定义驱动开发(WDK)

如果需要开发自定义USB客户端驱动:

开发环境准备

  1. 安装Visual Studio(勾选“C++桌面开发”)
  2. 安装Windows Driver Kit (WDK)
  3. 安装Windows SDK(版本10.0.19041.0+)

开发流程

  1. 使用Visual Studio的USB驱动模板生成KMDF或UMDF驱动框架代码
  2. 实现驱动入口函数、设备添加回调、PNP和电源管理回调
  3. 实现读写、IO控制等分发函数
  4. 编写INF安装文件
  5. 编译、签名、安装

6. Linux平台驱动配置

6.1 Linux USB驱动框架

Linux USB子系统分为三层:

层级 组件 说明
主机控制器驱动(HCD) dwc2、ehci-platform等 直接操作硬件,芯片原厂通常已做好
USB核心层(USBCore) 内核核心部分 实现USB协议、管理总线,无需修改
设备驱动层 usb-storage、usbhid等 针对特定设备类,内核已集成大量通用驱动

对于嵌入式开发者,工作流程通常是:在设备树中正确描述USB控制器,然后在内核配置中启用对应的设备驱动。

6.2 内核USB支持配置

Linux内核需要启用USB支持:

# 在menuconfig中启用以下选项
Device Drivers → USB Support → <*> Support for Host-side USB
Device Drivers → USB Support → <*> EHCI HCD (USB 2.0) support
Device Drivers → USB Support → <*> xHCI HCD (USB 3.0) support

对于设备模式(Gadget),还需要启用:

Device Drivers → USB Support → USB Gadget Support → <*> USB Gadget Support

6.3 设备枚举与识别

设备插入后,可以通过以下命令查看枚举结果:

# 查看USB设备树
lsusb

# 查看详细设备信息
lsusb -v -d <vid>:<pid>

# 查看内核日志(观察枚举过程)
dmesg | tail -20

# 查看USB设备节点
ls /dev/bus/usb/

6.4 CDC虚拟串口驱动

CDC虚拟串口设备在Linux下使用cdc_acm驱动。

自动识别:大多数现代Linux发行版会自动加载cdc_acm驱动,设备节点为/dev/ttyACMx

手动加载(如果未自动加载):

sudo modprobe cdc_acm

权限配置:默认情况下,普通用户无法访问/dev/ttyACMx。可以通过添加用户到dialout组解决:

sudo usermod -a -G dialout $USER

6.5 udev规则配置

udev规则用于在设备插入时自动执行特定操作,如设置权限、创建符号链接。

创建规则文件

sudo vim /etc/udev/rules.d/99-usb-device.rules

规则示例(为特定VID/PID的设备设置权限):

SUBSYSTEM=="usb", ATTR{idVendor}=="1234", ATTR{idProduct}=="5678", MODE="0666"

重新加载规则

sudo udevadm control --reload-rules
sudo udevadm trigger

对于FTDI等特定芯片,也可以创建规则允许普通用户访问:

SUBSYSTEM=="usb", ATTR{idVendor}=="0403", MODE="0666"

6.6 USB Gadget配置(设备模式)

当嵌入式Linux作为USB设备(Gadget)时,可以使用Configfs进行配置。

配置CDC虚拟串口(ACM)的示例

# 挂载configfs
mount -t configfs none /sys/kernel/config

# 创建gadget
mkdir /sys/kernel/config/usb_gadget/g1
cd /sys/kernel/config/usb_gadget/g1

# 设置VID/PID
echo 0x1234 > idVendor
echo 0x5678 > idProduct

# 创建配置
mkdir configs/c.1
mkdir configs/c.1/strings/0x409
echo "CDC Serial" > configs/c.1/strings/0x409/configuration

# 创建功能(ACM)
mkdir functions/acm.usb0

# 关联功能到配置
ln -s functions/acm.usb0 configs/c.1

# 启用gadget
echo <udc_name> > UDC

7. 应用层USB通信库

7.1 libusb:跨平台的瑞士军刀

libusb是一个跨平台的、用户态的USB设备访问库。它绕过了操作系统自带的标准USB驱动,让开发者可以直接用代码和USB设备“聊天”,无需为每个设备编写内核驱动。

Linux下安装

# Debian/Ubuntu
sudo apt install libusb-1.0-0-dev

# Fedora/RHEL
sudo dnf install libusbx-devel

Windows下安装

  • 使用vcpkg:vcpkg install libusb
  • 下载预编译二进制,将libusb-1.0.dll放入C:\Windows\System32
  • 使用Zadig为设备安装WinUSB驱动

基本使用流程

#include <libusb-1.0/libusb.h>

// 1. 初始化libusb
libusb_init(NULL);

// 2. 获取设备列表
libusb_device **devs;
ssize_t count = libusb_get_device_list(NULL, &devs);

// 3. 遍历设备,查找目标VID/PID
for (ssize_t i = 0; i < count; i++) {
    struct libusb_device_descriptor desc;
    libusb_get_device_descriptor(devs[i], &desc);
    if (desc.idVendor == 0x1234 && desc.idProduct == 0x5678) {
        // 找到目标设备
    }
}

// 4. 打开设备
libusb_device_handle *handle;
libusb_open(devs[i], &handle);

// 5. 进行数据传输...
// 6. 关闭设备、释放资源
libusb_close(handle);
libusb_free_device_list(devs, 1);
libusb_exit(NULL);

⚠️ 权限问题:在Linux下,普通用户直接访问USB设备会遇到“Permission denied”错误。解决方法:使用sudo运行,或配置udev规则。

7.2 libftdi1:FTDI芯片专用库

libftdi1是专门用于与FTDI芯片通信的开源库。

安装(Debian系):

sudo apt install libftdi1-dev

设备枚举示例

#include <ftdi.h>

struct ftdi_context *ftdi = ftdi_new();
struct ftdi_device_list *devlist;

// 查找所有FTDI设备
int count = ftdi_usb_find_all(ftdi, &devlist, 0, 0);
printf("Found %d FTDI devices\n", count);

7.3 WinUSB API(Windows原生)

对于Windows平台,可以直接使用WinUSB API与设备通信。

基本流程

  1. 使用SetupDiGetClassDevs获取设备信息集
  2. 使用SetupDiEnumDeviceInfo枚举设备
  3. 使用SetupDiGetDeviceInterfaceDetail获取设备路径
  4. 使用CreateFile打开设备
  5. 使用WinUsb_Initialize初始化WinUSB
  6. 使用WinUsb_ReadPipe/WinUsb_WritePipe进行数据传输

8. 调试与排查工具

8.1 主机端调试工具

工具 平台 功能
设备管理器 Windows 查看设备状态、驱动信息、COM口号
lsusb Linux 查看USB设备树和描述符信息
dmesg Linux 查看内核日志,观察枚举过程
USB View Windows 查看详细的USB描述符信息
Wireshark + usbmon Linux USB数据包抓取与分析
Zadig Windows 驱动安装与替换
Bus Hound Windows USB总线监控

8.2 常见问题排查

问题 可能原因 解决方案
设备管理器显示“未知设备” 驱动未安装或描述符错误 安装对应驱动;检查固件描述符配置
CDC设备不显示COM口 cdc_acm驱动未加载或INF缺失 sudo modprobe cdc_acm;手动安装INF
libusb打开设备返回“Permission denied” 权限不足 使用sudo;配置udev规则
设备枚举到一半失败 描述符格式错误或端点0处理异常 检查固件中的描述符数据;用USB分析仪抓包
数据传输不稳定 端点配置错误或缓冲区溢出 检查端点最大包大小;增加缓冲区
CH340驱动安装失败 驱动冲突或权限问题 卸载旧驱动;以管理员身份运行

9. 总结

USB通信的软件配置是一项从设备端固件主机端驱动、从底层协议栈应用层API的系统工程。

配置环节 核心要点
设备端固件 正确配置描述符(设备/配置/接口/端点);正确处理端点0的标准请求;实现设备类特定功能
Windows驱动 CDC设备用系统内置驱动;自定义设备用WinUSB(Zadig安装);特殊需求开发WDK驱动
Linux驱动 内核启用USB支持;cdc_acm自动识别;udev配置权限;Configfs配置Gadget模式
应用层通信 libusb跨平台通用;libftdi1专用于FTDI芯片;WinUSB API用于Windows原生开发

核心原则

  1. 枚举是第一步:设备必须正确响应主机的标准请求,才能被识别
  2. 描述符是身份证:描述符的任何错误都会导致枚举失败
  3. 驱动匹配靠VID/PID:确保固件中的VID/PID与驱动INF中的一致
  4. 权限是Linux的坑:配置udev规则解决普通用户访问问题
  5. 调试工具是救星:lsusb、dmesg、设备管理器是排查问题的第一道防线

掌握USB通信的软件配置,你就能从“硬件连上但系统不认”进阶到“插上就能用、想怎么通信就怎么通信”


📚 参考资料

  • USB 2.0规范(USB-IF)
  • Windows驱动程序工具包(WDK)文档
  • Linux内核USB子系统文档
  • libusb官方文档
  • STM32CubeMX USB设备配置指南
  • CherryUSB开源协议栈文档
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐