自制工业控制系统:USB 2.0主机控制器实战——EHCI驱动、传输队列与设备枚举
Contents
起因
键盘、鼠标、U 盘都骑在 USB 上,这条"骑得上去"的路是 1996 年修的:Intel、微软、康柏那批公司受够了插个外设要开机箱拨跳线,联手定了 USB;2000 年 USB 2.0 把速度抬到 480Mbps,主机控制器规范 EHCI 由 Intel 主导,全速低速留给伴手的 UHCI/OHCI——这个分工就是下面那个矛盾的前传。这篇啃 USB 栈的地基——主机控制器驱动。先说清一个绕不开的事实:USB 2.0 的主机控制器规范是 EHCI,但 EHCI 只管高速(480Mbps)设备;键盘鼠标偏偏是低速(1.5Mbps)/全速(12Mbps)设备,这个矛盾得先解决。照例给到寄存器级。
一、分层与速度现实
USB 栈分三层:主机控制器驱动(EHCI,本篇)→ 设备枚举(SETUP/描述符,任何控制器通用)→ class 驱动(HID 键鼠、MSC U 盘,后续两篇)。分层的价值:底层换 xHCI 上层不动。
速度问题的两条工程出路:
- 伴随控制器:EHCI 规范设计了"端口路由"——全速/低速设备出现在根端口时,把端口所有权(PortOwner 位)交给伴随的 UHCI/OHCI 控制器处理。走这条路意味着还要实现一份 UHCI(好消息:UHCI 比 EHCI 简单,帧列表+TD 队列,没有异步调度引擎)
- TT Hub(推荐起步):机内加一个高速 Hub,它的 Hub 内置事务翻译器(TT),键鼠接 Hub 下行口,EHCI 只见高速流量。纯硬件解法,软件量最少
U 盘本身是高速设备,EHCI 直连没有任何问题。真遇上 xHCI-only 的新平台,换底层、上层照旧——分层留的后门就在这。
二、寄存器与所有权交接
EHCI 寄存器分两段:能力段(BAR0 起)和操作段(BAR0 + CAPLENGTH)。能力段先读:
| 偏移 | 字段 | 用途 |
|---|---|---|
| 0x00 | CAPLENGTH | 操作段偏移 |
| 0x04 | HCSPARAMS | bits0-3 端口数;bit4 PPC(端口供电受控);bits8-15 伴随控制器的数量与端口分配 |
| 0x08 | HCCPARAMS | bit0 是否支持 64 位寻址;bits8-15 EECP(扩展能力指针,BIOS 交接用) |
操作段(关键寄存器):
| 偏移 | 寄存器 | 关键位 |
|---|---|---|
| +0x00 | USBCMD | bit0 RUN;bit1 HCRESET;bit4 PSE 周期使能;bit5 ASE 异步使能;bit6 IAAD 门铃 |
| +0x04 | USBSTS | bit0-5 中断位(写 1 清零,bit5=异步推进);bit14/15 周期/异步调度运行态(只读);bit12 HCHalted |
| +0x08 | USBINTR | bit0 中断;bit1 错误;bit2 端口变化;bit5 异步推进 |
| +0x0C | FRINDEX | 帧号 |
| +0x14 | PERIODICLISTBASE | 周期帧列表基址(4KB 对齐) |
| +0x18 | ASYNCLISTADDR | 异步链表头(QH 地址) |
| +0x40 | CONFIGFLAG | bit0 = 端口全路由到 EHCI |
| +0x44+4i | PORTSC(i) | bit0 连接;bit1 连接变化(w1c);bit2 使能;bit8 复位;bit12 供电;bit13 PortOwner;bits4-5 线态(01b=低速) |
BIOS→内核交接:从 HCCPARAMS 的 EECP 沿 PCI 能力链找 legacy 支持能力(ID 0x01,USBLEGSUP),写 bit24 请求所有权、轮询 bit16 清零(BIOS 撤销 owned)。UEFI 退出启动服务后通常已释放,但代码必须处理"没释放"的分支——QEMU 的老 OVMF 就经常不释放。
初始化序列(顺序不能乱):
|
|
三、端口复位与设备出现
端口事件轮询或 USBINTR bit2 中断(CSC 置位):
|
|
坑:PORTSC 一律读-改-写。 复位结束清 bit8 时如果整字写回,会顺手把使能、供电、PortOwner 位一起抹掉——先 read 出来、只动目标位、再写回去。复位脉冲那 50ms 里设备在总线上跟主机交换 chirp 握手:握手成功端口以高速使能;不 chirp 的设备(全速/低速)复位后端口保持禁用、按规范交给伴随控制器——TT Hub 方案下这个分支发生在 Hub 内部,根端口永远只见高速。50ms 是规范下限,别为了启动快几毫秒去缩短它。
四、QH 与 qTD:传输的载体
EHCI 的传输描述全是内存结构,DMA 池分配(32 位可寻址、32 字节对齐,PCIe 篇的池子):
QH(队列头,48 字节):挂在一根端点的传输队列头。关键字段:next 指针(位 0 是 Terminate,位 1 VfH);端点特征字——设备地址(bits0-6)、端点号(bits8-11)、传输速度编码(bits12-13,高速=10b)、最大包长(bits16-26,高速控制/批量=64);后面 8 个 dword 是 qTD overlay(控制器直接在这里续链)。
qTD(传输描述符,32 字节):token 字(dword2)是灵魂——状态位 bits0-7(bit7 Active,出错位若干);PID bits8-9(0=OUT,1=IN,2=SETUP);C_Err bits10-11(重试次数);C_Page bits12-14;IOC bit15(完成中断);总字节数 bits16-30;DT bit31(数据切换 0/1);dword3-7 是五个 4KB 缓冲页指针(小传输用一页就够)。页指针有讲究:第一个带低 12 位偏移,后续页指针必须 4K 对齐;传输跨页时控制器靠 C_Page 推进——五个指针封顶单笔 20KB,再大就分段。
异步传输(控制/批量):ASYNCLISTADDR 指向的 QH 链成环,控制器自动飞行;软件把 qTD 挂到 QH 的 overlay,写 USBCMD.IAAD(bit6)敲门铃,USBSTS bit5 置位表示控制器推进过新元素。完成/出错看 qTD 的状态位,惯例重试 3 次。
控制传输(枚举用)= 一个 QH 上三条 qTD:SETUP(8 字节请求包,PID=2)→ 可选 DATA(IN/OUT)→ STATUS(反向 PID,长度 0)。8 字节请求包的格式(bmRequestType, bRequest, wValue, wIndex, wLength)是 USB 一切配置的钥匙,下一篇大量使用。
周期传输(键鼠中断端点):1024 项帧列表按间隔散布——8ms 间隔的端点,QH 挂到 list[i],i 步长 8(i%8 相同的槽位),两级间隔的 QH 用横向指针串。PSE 使能后控制器每帧自动轮询,数据到了写缓冲、置 IOC 触发中断。
五、从端口到设备
插入 → 复位 → 高速判定 → 控制传输走起来:SET_ADDRESS(地址从 1..127 里挑,0 保留给刚复位的新设备,所以同一时刻只能枚举一个端口)→ GET_DESCRIPTOR(设备 18 字节/配置/端点)→ SET_CONFIGURATION —— 到这里设备"活了",交给 class 驱动(HID/MSC 篇)接手。这段枚举逻辑与控制器无关,是 USB 层的公共财产。
GET_DESCRIPTOR 有个鸡生蛋的细节:读设备描述符之前得知道控制端点 0 的包长,而包长(bMaxPacketSize0)写在设备描述符第 7 字节。高速设备的控制端点固定 64 字节,没这问题;全速设备可能是 8/16/32/64——所以老老实实先只读前 8 字节,拿到 bMaxPacketSize0 再把 18 字节读完整。想一步到位读 18 字节,碰上包长 8 的全速设备直接传输出错。TT Hub 下行的键鼠正是全速设备,这个坑躲不开。
总结
实战清单:CAPLENGTH 分段读寄存器、EECP 走 legacy 交接、停机→复位→帧列表/异步环→RUN 的固定时序、PORTSC 复位流程与低速线态判定、QH/qTD 的 32 字节对齐纪律、token 位域、控制传输三段式、IAAD 门铃、周期列表按间隔散布。跑通标志:QEMU 用 -device usb-ehci 挂 -device usb-storage,U 盘端口能枚举出设备描述符,vendor/product 打到串口(QEMU 的 EHCI 模型不挑设备速度,真机上低速键鼠才需要 TT Hub 的分工)。下一篇:键鼠驱动(HID 实战)。
参考链接
- EHCI 规范(Intel 官方 PDF)——寄存器与 QH/qTD 的权威定义
- OSDev Wiki: USB / EHCI
- 本系列 PCIe 篇:PCIe总线枚举实战——ECAM配置空间、BAR探测与MSI配置(DMA 池与 BAR 的来源)
- 本系列第一篇:自制工业控制系统:从UEFI到内核
Author 软件开发大郭
LastMod 2026-09-11