鼠标:PCI 与 USB — 先画出光标,再让它动起来¶
这一章讲什么: 操作系统要像操作系统,得能响应人的动作。本章分两段: 先把鼠标光标和桌面画出来(纯像素活儿),然后从总线上找到鼠标这个硬件—— 这要靠两件本章的主角:PCI(所有现代设备都挂在它上面)和 USB 主机控制器(xHC)。 找到之后用「轮询」让光标跟着真鼠标动。轮询是故意选的笨办法,就为让你亲手感到它笨。
1. 先看现象:五步走,光标开始跟着手走¶
主线例子,从开机到光标随鼠标移动:
① ScanAllBus():枚举所有 PCI 设备(读每个设备的配置空间,供应商ID != 0xffff 才算存在)
② 找 xHC:在设备表里挑类代码 = (0x0c, 0x03, 0x30) 的设备
——基类 0x0c=串行总线控制器,子类 0x03=USB,接口 0x30=xHCI
③ 读 BAR0:里面存着 xHC 寄存器组的内存地址(MMIO 基地址,低 4 位是标志,掩掉)
④ Controller.Initialize() → Run():xHC 复位、设置、起跑,自己开始识别 USB 设备
⑤ while(1) ProcessEvent():不停问 xHC「有事件没?」→ 有就处理 →
鼠标事件交给 MouseObserver → mouse_cursor->MoveRelative(dx, dy)
图说:①~④ 只做一次,⑤ 是永远转的主循环——这就是「轮询」。
书里还留了一个真实的「硬件考古」插曲:2012 年 Intel Panther Point 芯片组里, USB 2.0 端口默认归 EHCI(老控制器)管,得用 4 种特殊寄存器把端口「切换」给 xHCI—— 作者是在收到「某些机器上 MikanOS 的 USB 鼠标不能用」的报告后,去读 Linux 驱动源码 才挖出这个特殊规格的1。
2. 顶层全景¶
6.1 画:鼠标光标形状(字符点阵)→ FillRectangle/DrawRectangle → 桌面
工具:Vector2D 模板结构体 + operator+= 运算符重载
6.2 讲:USB 的世界观(总线/串行/驱动程序三层:主机控制器驱动 + 总线驱动 + 类驱动)
立场:本书直接用写好的 xHCI/HID 驱动,不教怎么写(源码在 kernel/usb 下)
6.3 找:PCI 配置空间(256 字节)→ CONFIG_ADDRESS/CONFIG_DATA 寄存器 →
MakeAddress 拼地址 → ScanBus/ScanAllBus 递归枚举 → 设备列表
6.4 动:xHC 筛选(Intel 优先)→ BAR0 → Initialize/Run → 端口扫描找鼠标 →
轮询 ProcessEvent → 光标移动(本章结束时它还会「瞬移」,第 9 章收拾)
图说:「画→找→动」三段;6.2 的理论地图是为了让你知道自己在用哪一层。
3. 核心原理¶
3.1 把桌面画出来:矩形与模板¶
光标形状就是一个字符点阵(比字体大些):'@' 画黑边、'.' 填白身,横宽故意 +1 是为了存 '\0'2。
画矩形这种事出现第二次就该成函数:FillRectangle(左上角, 大小, 颜色) 填充、
DrawRectangle 画框。参数里的 Vector2D 是个「模板结构体」——
template <typename T> 让一份定义服务各种坐标类型(int、unsigned 都行),
operator+= 是运算符重载,写 a+=b 就能把一个向量加到另一个上3。
淡蓝底 + 深色任务栏 + 30×30 小方框——桌面完工,「你不觉得它看起来更像操作系统吗」4。
3.2 USB 的世界观:控制器、总线、类¶
总线(bus)这个词借自公共汽车:一条线路上搭各种设备的信号,不是一对一专线。 USB 是串行总线——一条信号线一次发一个比特(省线、能跑高速);PCIe 也是串行5。 系统里真正和 USB 设备打交道的是主板上的 USB 控制器芯片,而驱动程序就是「主要为了操纵硬件」的那部分操作系统6。
USB 驱动分三层,自上而下:
| 层 | 干什么 | 本书怎么处理 |
|---|---|---|
| 类驱动 | 按设备类别服务(鼠标键盘属 HID 类) | 用现成的 usb::HIDMouseDriver |
| 总线驱动 | 给类驱动提供 USB 标准 API(如 GET_DESCRIPTOR) | 用现成的 |
| 主机控制器驱动 | 直接操纵 xHC 芯片 | 用现成的 usb::xhci::Controller |
| (PCI 总线驱动) | 找到 xHC 这个 PCI 设备 | 本章自己写 |
xHCI 是 USB 3.x 的主机控制器标准,符合它的控制器叫 xHC;现代机器都有7。 作者把话说得很明白:USB 驱动本身太复杂,本书只教「怎么用」不教「怎么写」—— 「本书的主要优势在于它能正确支持 USB 设备」这句自评,指的是接上就能用鼠标键盘这件事8。
3.3 PCI:找到每一个设备¶
PCI(Peripheral Component Interconnect)是连接组件和主板的标准—— NVMe 固态盘、网卡、GPU(显卡)、USB 控制器都挂在这棵树上;PCIe 与它软件兼容,会处理 PCI 就够9。
每个 PCI 设备都有 256 字节的配置空间,存着自己的身份证:供应商 ID、 类代码(class code,三位:基类/子类/接口) 等10。CPU 访问它的办法是经典桥段: 往 CONFIG_ADDRESS 寄存器写「一个神奇的 32 位数」(第 31 位是启用位, 23:16 总线号、15:11 设备号、10:8 功能号、7:0 寄存器偏移), 再读写 CONFIG_DATA 寄存器,数据就落到目标设备的配置空间里11。
枚举的递归结构:总线(0–255)→ 设备(0–31)→ 功能(0–7);
读某个功能的供应商 ID,是 0xffff(无效值)就说明这功能不存在,允许有孔洞12。
MakeAddress() 用 lambda 表达式(匿名函数)把这段位拼接写得干干净净13。
ScanAllBus() 跑完,全世界的 PCI 设备都进了 pci::devices 列表——
printk 把列表打印出来,你在自家 QEMU 上看到的清单会和作者的 MinnowBoard 不一样14。
3.4 枚举的完整走法(6.3 节补完)¶
扫描从 ScanAllBus() 开始,它的第一站在总线 0、设备 0、功能 0——这是每条 PCI 总线的
主桥,它管理着这条总线本身的配置22。逐设备扫描由 ScanBus(bus) 完成:对每个设备号
先读功能 0 的供应商 ID,是 0xffff 就跳过整个设备(设备不存在);存在则遍历它的 8 个功能
23。每发现一个有效功能,就把 {总线号, 设备号, 功能号, 供应商 ID, 类代码} 记进
pci::devices 列表——printk 把这个列表打印出来,你在自家 QEMU 上看到的清单会和作者的
MinnowBoard 不一样,这是正常的24。
读配置空间靠的是两个寄存器之外的第三件工具:MakeAddress() 已经把「总线/设备/功能/偏移」 打包成 CONFIG_ADDRESS 的 32 位格式,真正读数据时再写一个 ReadVendorId()/ReadClassCode() 这样的小函数,封装「写地址、读数据」两步25。类代码本身是三个字段拼出来的:寄存器偏移 0x09 处一个字节是子类+接口,0x0B 处是 基类,ReadClassCode() 把两段读出来拼成 {base, sub, interface} 三元组返回——后面找 xHC 的比对 (0x0c, 0x03, 0x30) 就是和这三个数对齐26。 这样上层代码只说「读这个设备的类代码」, 不用每次都手拼位段。
3.5 xHC 初始化与轮询鼠标¶
从设备列表挑 xHC 只看类代码 (0x0c, 0x03, 0x30);多个 xHC 时优先 Intel 的 (供应商 ID 0x8086,最可能是主控制器)15。
xHC 的寄存器不在 CPU 内部,而在内存地址空间里——这叫 MMIO(内存映射 IO): 可以像内存一样读写的寄存器。它的基地址记在 PCI 配置空间的 BAR0(基地址寄存器)里, 读出来、掩掉低 4 位的标志,就是 xHC 寄存器组的真正基地16。
之后是三步:Controller.Initialize()(复位+设置)、Run()(起跑,xHC 自己去识别设备)、
遍历端口 IsConnected() → ConfigurePort()(配置端口、生成类驱动;鼠标的数据会流进你注册的
MouseObserver)17。主循环 while(1) ProcessEvent() 处理 xHC 里累积的事件18。
轮询(polling)在这里被正式判了半死刑: 鼠标键盘的输入是零星的—— 问一万次动一次;可问稀了(比如 10 秒一次),最坏情况按键后 10 秒系统才有反应。 「对于零星发生的事件,中断方法比轮询更合适」19。这句话就是第 7 章的预告片。
另外两处顺手一提:MouseCursor 的移动策略是「擦旧画新」
(在原位置用背景色涂掉,在新位置重画光标)20;ConfigurePort() 不写命名空间(给名字分区加前缀,如 usb::xhci)前缀也能编译,
是 C++ 的 ADL(参数依赖查找)在帮忙21。
4. 作者的判断与证据¶
判断:现代 OS 教学必须直面 USB,不能靠 PS/2 仿真偷懒。 老书用 PS/2 是因为简单; 如今很多 UEFI BIOS 连 PS/2 仿真都没有,作者自己的测试机就没有——所以「本书讨论的是不带仿真功能的 USB 鼠标」22。这是本书区别于《30天自制操作系统》的时代性升级。
判断:驱动程序是拿来用的,不是拿来教的。 三层驱动直接用现成实现,正文只讲「你在用哪一层」。 这个取舍让本章篇幅可控,但也意味着读完本章你不会写 USB 驱动——想学,去读 kernel/usb 源码和 xHCI 规范。
证据: PCI 设备列表是真实枚举输出(不同机器不同结果,书里明说)23; Panther Point 的切换是「收到 bug 报告 → 读 Linux 驱动 → 挖出芯片组特殊规格」的实证案例24。
5. 边界与局限¶
- 光标移动是「瞬移」不是「滑动」:擦旧画新、一步到位;平滑移动和穿透窗口的遮挡关系,第 9 章图层系统才解决。
- 只选一个 xHC、只处理鼠标:多控制器、键盘(USB HID 键盘)都推迟到第 12 章。
- EHCI→xHCI 切换只对 Intel Panther Point:别的芯片组的怪癖,本书不覆盖——硬件兼容性是永远的清单,不是终点。
- 轮询在空转时 100% 烧 CPU:这是它「故意笨」的另一半代价,hlt 都没处放。
6. 可带走的¶
本章主线例子,一行写完: 枚举 PCI → 类代码 (0x0c,0x03,0x30) 挑 xHC → BAR0 得 MMIO 基址 → Initialize/Run → 端口找鼠标 → while 轮询 ProcessEvent → 光标 MoveRelative。
- 找设备的标准动作:枚举总线 → 读配置空间 → 按类代码筛选;0xffff 的供应商 ID = 不存在;
- CONFIG_ADDRESS 是一个「打包好的地址」:启用位 + 总线/设备/功能/偏移,写它再碰 CONFIG_DATA 就到设备;
- MMIO:寄存器住在内存地址空间里,BAR0 告诉你门牌号,低 4 位是标志要掩掉;
- USB 驱动三层(类/总线/主机控制器),本书只自己写「找到 xHC」这层,其余用现成的;
- 类代码是三维的:基类 0x0c 串行总线、子类 0x03 USB、接口 0x30 xHCI;
- 轮询适合高频事件;零星事件(键盘鼠标)适合中断——感受过轮询的笨,才懂中断的好;
- 硬件兼容性靠考古:Panther Point 的故事说明读别人的驱动源码是合法且必要的调研手段。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 光标形状与 FillRectangle | 6.1 鼠标光标(osbook_day06a) | text/46-ch06-01-6-1-osbook-day06a.txt:6(搜「光标的形状」) · text/46-ch06-01-6-1-osbook-day06a.txt:22(搜「FillRectangle」) |
| 模板 Vector2D 与运算符重载 | 6.1 鼠标光标(osbook_day06a) | text/46-ch06-01-6-1-osbook-day06a.txt:35(搜「模板」) · text/46-ch06-01-6-1-osbook-day06a.txt:41(搜「operator +=」) |
| 桌面绘制效果 | 6.1 鼠标光标(osbook_day06a) | text/47-ch06-02-6-2-usb-host.txt:1(搜「你不觉得它看起来更像」) |
| PS/2 仿真的退场与 USB 决心 | 6.2 USB Host驱动程序 | text/47-ch06-02-6-2-usb-host.txt:41(搜「エミュレーション機能がない」;中文 :42 搜「PS/2仿真」) |
| 总线、串行、驱动程序定义 | 6.2 USB Host驱动程序 | text/47-ch06-02-6-2-usb-host.txt:53(搜「Universal Serial Bus」) · text/47-ch06-02-6-2-usb-host.txt:63(搜「USB控制器」) · text/47-ch06-02-6-2-usb-host.txt:65(搜「驱动程序」) |
| 三层驱动结构与 xHCI | 6.2 USB Host驱动程序 | text/47-ch06-02-6-2-usb-host.txt:68(搜「三层」) · text/48-ch06-03-6-3-pci-osbook-day06b.txt:2(搜「xHCI是」) |
| 只教用不教写的立场 | 6.2 USB Host驱动程序 | text/48-ch06-03-6-3-pci-osbook-day06b.txt:19(搜「过于高级和复杂」) |
| PCI 配置空间与枚举流程 | 6.3 搜索PCI设备(osbook_day06b) | text/48-ch06-03-6-3-pci-osbook-day06b.txt:30(搜「枚举」) · text/48-ch06-03-6-3-pci-osbook-day06b.txt:61(搜「256字节的PCI配置空间」) |
| CONFIG_ADDRESS 位结构与 0xffff | 6.3 搜索PCI设备(osbook_day06b) | text/48-ch06-03-6-3-pci-osbook-day06b.txt:89(搜「CONFIG_ADDRESS」) · text/48-ch06-03-6-3-pci-osbook-day06b.txt:115(搜「0xffff」) |
| MakeAddress 与 lambda | 6.3 搜索PCI设备(osbook_day06b) | text/48-ch06-03-6-3-pci-osbook-day06b.txt:123(搜「MakeAddress」) · text/48-ch06-03-6-3-pci-osbook-day06b.txt:139(搜「lambda」) |
| 扫描所有总线 | 6.3 搜索PCI设备(osbook_day06b) | text/48-ch06-03-6-3-pci-osbook-day06b.txt:232(搜「ScanAllBus」) |
| 类代码找 xHC、Intel 优先 | 6.4 带轮询的鼠标输入(osbook_day06c) | text/49-ch06-04-6-4-osbook-day06c.txt:24(搜「类代码0x0c」) · text/49-ch06-04-6-4-osbook-day06c.txt:33(搜「优先选择Intel」) · text/49-ch06-04-6-4-osbook-day06c.txt:46(搜「0x8086」) |
| MMIO 与 BAR0 | 6.4 带轮询的鼠标输入(osbook_day06c) | text/49-ch06-04-6-4-osbook-day06c.txt:58(搜「MMIO」) · text/49-ch06-04-6-4-osbook-day06c.txt:63(搜「BAR0」) |
| Initialize/Run/ConfigurePort | 6.4 带轮询的鼠标输入(osbook_day06c) | text/49-ch06-04-6-4-osbook-day06c.txt:84(搜「Initialize」) · text/49-ch06-04-6-4-osbook-day06c.txt:118(搜「Run()」) · text/49-ch06-04-6-4-osbook-day06c.txt:122(搜「ConfigurePort」) |
| Panther Point 考古 | 6.4 带轮询的鼠标输入(osbook_day06c) | text/49-ch06-04-6-4-osbook-day06c.txt:92(搜「Panther Point」) · text/49-ch06-04-6-4-osbook-day06c.txt:109(搜「Linux设备驱动程序」) |
| 轮询的定义与判决 | 6.4 带轮询的鼠标输入(osbook_day06c) | text/49-ch06-04-6-4-osbook-day06c.txt:133(搜「累积」) · text/49-ch06-04-6-4-osbook-day06c.txt:142(搜「轮询」) · text/49-ch06-04-6-4-osbook-day06c.txt:150(搜「中断方法比轮询更合适」) |
-
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:109,搜「竭尽全力调查了这个bug」;:109,搜「Linux设备驱动程序」)。 ↩ -
出处:「6.1 鼠标光标」(
text/46-ch06-01-6-1-osbook-day06a.txt:10,搜「'@'是鼠标光标的边缘」;:11,搜「大1」)。 ↩ -
出处:「6.1 鼠标光标」(
text/46-ch06-01-6-1-osbook-day06a.txt:35,搜「模板」;:43,搜「运算符重载」)。 ↩ -
出处:「6.1 鼠标光标」(
text/47-ch06-02-6-2-usb-host.txt:1,搜「你不觉得它看起来更像」)。 ↩ -
出处:「6.2 USB Host驱动程序」(
text/47-ch06-02-6-2-usb-host.txt:53,搜「Universal Serial Bus」;:58,搜「串行」)。 ↩ -
出处:「6.2 USB Host驱动程序」(
text/47-ch06-02-6-2-usb-host.txt:63,搜「USB控制器」;:65,搜「驱动程序」)。 ↩ -
出处:「6.2 USB Host驱动程序」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:2,搜「xHCI是」;:2,搜「所有的现代计算机」)。 ↩ -
出处:「6.2 USB Host驱动程序」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:20,搜「只介绍如何使用」)。「主要优势」原话见同节前页脚注(text/47-ch06-02-6-2-usb-host.txt:85,搜「主要优势」——日文残页)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:54,搜「核心技术」;:57,搜「软件上是兼容的」)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:61,搜「256字节」;:63,搜「供应商ID」)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:89,搜「CONFIG_ADDRESS」;表6.1 位结构见:96)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:113,搜「32个PCI设备」;:115,搜「0xffff」)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:139,搜「lambda表达式」;:145,搜「匿名函数」)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/49-ch06-04-6-4-osbook-day06c.txt:2,搜「ScanAllBus」;:4,搜「可能会显示不同的结果」)。 ↩ -
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:24,搜「类代码0x0c」;:28,搜「优先」;:46,搜「0x8086」)。 ↩ -
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:58,搜「MMIO」;:63,搜「BAR0」;:74,搜「低4位」)。 ↩ -
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:118,搜「Run()」;:122,搜「ConfigurePort」;:125,搜「MouseObserver」)。 ↩ -
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:133,搜「累积在xHC中」)。 ↩ -
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:142,搜「轮询」;:148,搜「10s」;:150,搜「中断方法比轮询更合适」)。 ↩ -
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:153,搜「MoveRelative」))。 ↩ -
出处:「6.4 带轮询的鼠标输入」(
text/49-ch06-04-6-4-osbook-day06c.txt:129,搜「ADL」)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:241,搜「首先从总线0、设备0和功能0」)。 ↩↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:253,搜「0xffff」;:265,搜「if (ReadVendorId」)。 ↩↩ -
出处:「6.3 搜索PCI设备」(
text/49-ch06-04-6-4-osbook-day06c.txt:2,搜「ScanAllBus」;:4,搜「不同的结果」)。 ↩↩ -
出处:「6.3 搜索PCI设备」(
text/49-ch06-04-6-4-osbook-day06c.txt:50,搜「单参数的ReadVendorId」)。 ↩ -
出处:「6.3 搜索PCI设备」(
text/48-ch06-03-6-3-pci-osbook-day06b.txt:133,搜「reg_addr & 0xfcu」——寄存器偏移按 4 字节对齐的原因;:61,搜「类别代码」)。 ↩