起因

键盘、鼠标、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 就经常不释放。

初始化序列(顺序不能乱):

1
2
3
4
5
6
// 1. 停机:USBCMD=0,轮询 USBSTS.HCHalted=1
// 2. 复位:USBCMD=HCRESET,轮询该位自清
// 3. 拿所有权(EECP 走 legacy 能力)
// 4. 建 1024 项周期帧列表(全填 Terminate),写 PERIODICLISTBASE
// 5. 建异步哨兵 QH(next 指向自己成环),写 ASYNCLISTADDR
// 6. CONFIGFLAG=1;USBCMD = RUN | ASE | PSE

三、端口复位与设备出现

端口事件轮询或 USBINTR bit2 中断(CSC 置位):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn handle_port(ehci: &Ehci, i: usize) {
    let sc = read_portsc(i);
    if sc & 0x2 != 0 {                       // 连接状态变化
        if sc & 0x1 != 0 {                   // 有设备插入
            if (sc >> 4) & 0x3 == 0b01 {     // 线态=低速:交给伴随控制器或忽略(TT 方案时不会发生)
                write_portsc(i, sc | (1 << 13));
                return;
            }
            // 供电(PPC 型控制器)→ 等 100ms 稳定
            // 复位:PORTSC |= bit8,等 50ms,清 bit8
            // 等 bit2(使能)置位 → 高速设备就绪,进入枚举
        }
        write_portsc(i, read_portsc(i) | 0x2);   // 写 1 清变化位
    }
}

坑: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 实战)。

参考链接