第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鼠标,驱动层会做这些事:
- Hub检测到端口状态变化
- 主机发送复位信号
- 读取设备描述符(第0个端点)
- 分配地址(比如地址3)
- 读取配置描述符、接口描述符、端点描述符
- 加载对应的类驱动(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枚举过程,看看设备插入后,主机到底在干什么。到时候我会分享一个我调试枚举失败的案例,保证让你少走弯路。
好,今天就到这里。记住,看协议栈代码前,先把这张架构图刻在脑子里。