第2章:USB协议栈架构

大家好,欢迎来到《USB主机与设备栈移植实战》的第二讲。

这一章,我们来聊聊USB协议栈的整体架构。说实话,很多初学者一上来就扎进代码里,结果越看越迷糊。我当年也是这样,对着Linux内核的USB子系统代码看了三天,脑子里全是浆糊。后来我才明白——你得先看清地图,再上路。

2.1 什么是USB协议栈?

USB协议栈,说白了就是一套软件层,用来管理USB通信的。它把复杂的USB协议拆成了几个层次,每一层只管自己的事。

你想想看,USB协议从电气信号到应用数据,中间要经过多少环节?位填充、CRC校验、事务处理、管道管理……如果全揉在一起写,那代码就没法维护了。

所以,业界把USB协议栈分成了两大阵营:

  • 主机侧协议栈——负责管理USB总线,发起所有通信
  • 设备侧协议栈——负责响应主机的请求,提供功能

这两者虽然角色不同,但分层思路是相通的。我个人习惯把协议栈想象成一座桥,主机在桥头,设备在桥尾,中间每一层都是桥墩。

2.2 USB主机侧协议栈

主机侧,说白了就是“发号施令”的那一方。USB总线上的所有传输,都是由主机发起的。设备只能被动响应。

主机侧协议栈从上到下,大致分三层:

2.2.1 应用层(Client Driver Layer)

这一层离用户最近。比如你的U盘插入电脑后,系统会加载一个“大容量存储驱动”。这个驱动就属于应用层。

应用层的职责很简单:

  • 识别设备类型(比如是HID键盘,还是Mass Storage)
  • 提供用户接口(比如文件系统操作)
  • 调用下层API发送URB(USB Request Block)

我在项目中遇到过一个问题:一个客户自己写的HID驱动,死活收不到数据。查了两天,最后发现是应用层没有正确设置中断传输的间隔时间。嗯,这种坑,踩过一次就记住了。

2.2.2 驱动层(USB Core / Hub Driver)

这一层是USB协议栈的心脏。它负责:

  • 管理USB总线拓扑(Hub检测、设备枚举)
  • 分配地址、解析描述符
  • 管理管道(Pipe)和端点(Endpoint)
  • 调度传输事务(事务拆分、带宽管理)

我建议你把这一层看作“交通调度员”。它不关心你传的是什么数据,只关心怎么把数据按时、按顺序送到。

举个例子,当你在电脑上插入一个USB鼠标,驱动层会做这些事:

  1. Hub检测到端口状态变化
  2. 主机发送复位信号
  3. 读取设备描述符(第0个端点)
  4. 分配地址(比如地址3)
  5. 读取配置描述符、接口描述符、端点描述符
  6. 加载对应的类驱动(HID驱动)

整个过程,驱动层是总指挥。

2.2.3 控制器层(Host Controller Driver, HCD)

这一层直接和硬件打交道。它负责:

  • 初始化USB控制器寄存器
  • 管理传输描述符链表(TD链表)
  • 处理中断(如传输完成、错误)
  • 实现具体的传输协议(OHCI/UHCI/EHCI/xHCI)

说白了,控制器层就是把上层发来的URB,翻译成硬件能理解的寄存器操作。

我曾经在移植xHCI驱动时,被一个“门铃寄存器”搞到崩溃。你想想看,你写对了所有描述符,但忘了敲一下门铃,硬件就不干活。这种细节,文档里往往一笔带过,但实际调试时能让你怀疑人生。

主机侧分层总结:

层次职责典型代码
应用层设备类驱动、用户接口usb-storage, usbhid
驱动层总线管理、枚举、管道usbcore, hub driver
控制器层硬件操作、传输调度ehci-hcd, xhci-hcd

2.3 USB设备侧协议栈

设备侧,就是被主机“管”的那一方。它不能主动发起传输,只能等主机来问。

设备侧协议栈同样分三层,但角色完全反过来:

2.3.1 应用层(Device Class Layer)

这一层实现具体的设备功能。比如:

  • HID设备:报告描述符、按键上报
  • CDC设备:虚拟串口、数据收发
  • Mass Storage:SCSI命令处理、块读写

我做过一个项目,要在STM32上实现一个自定义HID设备。应用层代码其实很简单,就是填充报告描述符,然后等主机来读。但坑在于——报告描述符写错一个字节,主机就认不出你的设备。我调试了整整一个下午,最后发现是报告ID写成了0x00(保留值)。

2.3.2 驱动层(Device Core / Gadget Framework)

这一层是设备侧的“交通调度员”。它负责:

  • 管理端点(Endpoint)资源
  • 处理标准请求(如Get Descriptor, Set Configuration)
  • 维护设备状态(Default, Address, Configured)
  • 提供API给应用层(如usb_ep_queue)

在Linux中,这一层叫Gadget Framework。它把硬件控制器抽象成统一的接口,让上层驱动不用关心具体用的是哪个MCU。

我建议你重点理解“端点”这个概念。每个端点就像一个邮箱,有固定的地址和方向。主机发来的数据,会投递到对应的邮箱里。设备侧驱动层的工作,就是管理这些邮箱。

2.3.3 控制器层(Device Controller Driver, UDC)

这一层直接操作USB设备控制器硬件。它负责:

  • 初始化控制器寄存器
  • 管理端点寄存器(如FIFO、状态寄存器)
  • 处理硬件中断(如SETUP包、传输完成)
  • 实现具体的传输逻辑(如DMA传输)

说白了,控制器层就是“硬件翻译官”。上层发来一个“发送数据”的请求,它就去操作FIFO、触发DMA、等待中断。

我记得有一次移植UDC驱动到一款国产MCU上,发现它的端点中断标志位是写1清除,而标准做法是写0清除。就这么一个细节,让我多花了三天时间。所以,读芯片手册时,一定要一个字一个字地看。

小提示:设备侧协议栈在Linux中通常称为“USB Gadget”,在RTOS中则叫“USB Device Stack”。名字不同,但分层思想完全一样。

2.4 分层模型的价值

为什么要分层?我直接说结论:

  • 解耦:应用层不用关心底层是OHCI还是xHCI
  • 可移植:换一个MCU,只需要重写控制器层
  • 可复用:同一个HID驱动,可以跑在Linux、FreeRTOS、裸机上

你想想看,如果没有分层,你写一个USB键盘驱动,就得同时处理:

  • USB协议细节(事务、握手、重试)
  • 硬件寄存器操作(FIFO、DMA、中断)
  • 应用逻辑(按键映射、报告格式)

这代码写出来,估计只有你自己能看懂,而且换个平台就得重写。

注意:分层虽然好,但不要过度抽象。我在项目中见过有人把USB协议栈分了7层,结果一个简单的控制传输要经过5个回调函数。调试起来,那叫一个酸爽。记住,分层是为了解决问题,不是为了炫技。

2.5 一个简单的分层示例

我们来看一个实际例子。假设你要在STM32上实现一个USB虚拟串口(CDC ACM)。

分层后的代码结构大概是这样的:

// 应用层:CDC ACM类驱动
void cdc_acm_send_data(uint8_t *buf, uint32_t len) {
    // 调用驱动层API发送数据
    usb_ep_queue(ep_bulk_in, buf, len);
}

// 驱动层:Gadget Framework
void usb_ep_queue(usb_ep_t *ep, uint8_t *buf, uint32_t len) {
    // 检查端点状态
    // 调用控制器层API
    udc_ep_queue(ep->hw_ep, buf, len);
}

// 控制器层:UDC驱动
void udc_ep_queue(hw_ep_t *hw_ep, uint8_t *buf, uint32_t len) {
    // 写FIFO
    // 设置传输长度寄存器
    // 使能DMA
    // 等待传输完成中断
}

你看,每一层只做自己的事。应用层不知道数据是怎么发出去的,控制器层不知道数据是什么含义。这就是分层的好处。

2.6 本章小结

这一章我们讲了USB协议栈的架构。核心就三点:

  • 主机侧和设备侧,分层思路一致,但角色相反
  • 三层模型:应用层、驱动层、控制器层
  • 分层是为了解耦、可移植、可复用

下一章,我们会深入USB枚举过程,看看设备插入后,主机到底在干什么。到时候我会分享一个我调试枚举失败的案例,保证让你少走弯路。

好,今天就到这里。记住,看协议栈代码前,先把这张架构图刻在脑子里。