从零编写操作系统 27 - 鼠标输入:共享控制器、三字节包与级联中断
Day 26 已经能在 Mode 13h 中重建完整画面,但画面仍然没有指针。接入鼠标也不只是再读一个端口:PS/2 键盘和鼠标共享控制器输出缓冲区,鼠标包可能在任意字节处损坏,IRQ12 还要经过从片和主片两级 8259A。
本篇为固定的 QEMU PC 增加标准三字节 PS/2 鼠标。中断入口只读一次 0x60,依据控制器状态把字节分到键盘或鼠标队列;任务上下文负责组包、符号扩展、Y 轴转换、按键状态和坐标裁剪。实验会填满 64 字节鼠标队列,注入坏包和溢出位,并分别漏掉从片、主片 EOI,观察 IRQ12 为什么停在第一轮。
0x60 只有一个消费者
8042 兼容控制器的状态端口是 0x64,数据端口是 0x60。状态 bit 0 表示输出缓冲区有数据,bit 5 表示该字节来自辅助设备。键盘和鼠标并没有两份独立数据寄存器。
如果 IRQ1 入口和 IRQ12 入口各自无条件执行一次 inb(0x60),先运行的处理程序可能拿走另一设备的字节,后运行的一方只能读到下一字节或空缓冲。来源信息也会随着读取消失。读取动作必须收敛到同一个函数:
1 | |
这里的顺序不能颠倒:先保存状态,再读取一次数据,最后按保存的来源位路由。IRQ1 和 IRQ12 都调用这个入口,但不再拥有独立的端口读取逻辑。设备队列仍然分开,键盘扫描码解析器不会看到鼠标位移,鼠标组包器也不会把键盘释放码当成包尾。
专项运行同时注入鼠标动作和键盘字符串 kbd。最终六个键盘 make/break 字节与 147 个鼠标字节分别对账:
1 | |
这种结构可以概括为“共享入口、按来源分流”。多个逻辑设备复用一个硬件读取窗口时,只允许一个位置消费不可重放的数据,再把副作用较小的解析工作交给各自队列。
初始化是一段有界事务
鼠标在上电后不会自动按预期发送标准三字节包。初始化先禁用键盘和辅助端口,排空旧输出,读取控制器配置字节,再依次读取、修改和写回,保留已有策略。键盘继续使用 set 1 扫描码,辅助端口则执行 reset 和 enable reporting:
1 | |
发送鼠标命令前,控制器端口先收到 0xD4,表示下一字节转发给辅助设备。每次等待 input-buffer empty 或 output-buffer full 都有固定轮询上限。命令收到 0xFE RESEND 时最多重发三次,其他响应或超时都使初始化失败。
专项镜像把第一次 0xF4 的 ACK 有意解释为 RESEND,再发送第二次。运行记录同时证明 reset 的 ACK、BAT 和设备 ID:
1 | |
id=0 把本篇协议限定为普通三字节鼠标。滚轮与五键扩展需要额外采样率序列和四字节解析,不在本次状态机里偷偷兼容。
配置只在所有设备命令成功后打开 IRQ。最终控制字保留键盘 IRQ1,启用鼠标 IRQ12,并关闭键盘翻译;PIC 同时放开主片 IRQ2 级联线和从片 IRQ12。初始化失败时不会发布一个只有部分设置生效的“可用鼠标”。
IRQ12 要完成两级确认
8259A 级联结构中,鼠标的 IRQ12 进入从片 IR4;从片再通过主片 IRQ2 把请求送到 CPU。PIC 已被重映射到 0x20 和 0x28,所以 IRQ12 对应 IDT vector 44。汇编 stub 表和 IDT 初始化范围都从 34 项扩展到 45 项,仍复用原来的 68 字节同特权级 trap frame。
GDB 在真实 mouse_irq 入口检查到:
1 | |
处理结束要先确认从片,再确认主片:
1 | |
先从后主与中断经过控制器的方向相反。正常镜像中,每次 IRQ12 都产生一对 EOI,最终计数为 147/147,两个 PIC 的 ISR 均清零。
两个故障镜像分别省略一半操作。漏从片 EOI 后,从片 ISR 的 IRQ12 位保持为 1;漏主片 EOI 后,主片 ISR 的 IRQ2 位保持为 1:
1 | |
两种情况下再次移动鼠标都没有第二次 IRQ12。只观察“处理程序运行过一次”无法证明中断确认正确;ISR 位和第二次请求能否进入,才说明整条级联链已经恢复可用。
三字节包的边界不是天然可靠
标准包的第一个字节最低三位是左右中键,bit 3 固定为 1,bit 4/5 是 X/Y 符号,bit 6/7 是溢出标记。后两个字节保存 8 位位移。
解析器仅在等待首字节时要求 bit 3 为 1:
1 | |
第二、第三字节也可能碰巧设置 bit 3,因此不能每看到 bit 3 就重开一包。凑齐三字节后还要拒绝溢出位,并检查首字节的符号位是否与位移字节最高位一致:
1 | |
屏幕坐标的 Y 轴向下为正,而 PS/2 位移向上为正,因此 dy 需要取反。符号一致性检查能拒绝部分错位组合,却不能从任意字节丢失中唯一找回边界。数据字节本身可能满足 bit 3,纯字节流没有足够信息消除全部歧义。
确定性自检依次送入合法包、非法首字节、丢失第二字节、插入的 0xFA、溢出包和恢复后的合法包,得到两个包、三次同步丢失和一次溢出拒绝。宿主侧解析器测试使用同一生产函数,避免用另一份“测试版协议”证明自己。
恢复策略因此放在队列边界。64 字节原始队列溢出后,消费者丢弃残余队列并清空半包状态,等待新的候选首字节。测试暂停消费者并连续注入移动,实际得到:
1 | |
恢复后继续送入合法事件,最终 reset=1、packets=23。这证明溢出确实发生,也证明解析器没有永久停在错误字节相位。
中断只收字节,任务上下文生成事件
IRQ 路径完成三件事:读取共享端口、把字节写入固定队列、发送 EOI。三字节组包、坐标运算、字符渲染和 64,000 字节全帧复制都不在中断处理程序里执行。
任务上下文取出一个完整事件后,用 64 位中间值累加位移,再裁剪到 [0,319] × [0,199]。指针使用 Day 26 已验证的有符号矩形裁剪函数画成十字,因此中心到达边缘时,伸出屏幕的线段会被裁掉,中心像素仍然可见。
测试依次把指针送到左、右、上、下四条边,按下并释放左、中、右键,同时走键盘分流路径。最终状态为:
1 | |
edges=0xf 保存四边都曾到达的事实;buttons=0x7 和六次变化覆盖三键按下、释放。最终坐标停在右下角,截图中的黄色十字中心仍可见:
当前 UI 循环没有单独创建图形任务,但它仍遵守“中断生产原始事实,任务消费并解释”的边界。Day 28 会把同一批事件交给窗口命中测试、焦点和拖动状态机。
实验记录与累计回归
专项检查结果、初始化与输入串口记录、IRQ12 栈帧、队列溢出现场、最终状态、漏从片 EOI、漏主片 EOI和完整证据说明保存了本篇运行数据。
专项检查之后,Day 26 图形文字、Day 25 图形、保护模式、管道、Shell、文件描述符、FAT16、ATA、进程、系统调用、ELF、物理页、加载容量和真实 IRQ1 键盘检查全部退出 0,合并记录见累计回归日志。冻结 ZIP 又从不含 build/ 的空目录完成同一轮专项和累计复验,收据见独立附件复验。
一次较早的探索运行触发了既有 Day 22 时序敏感断言。没有修改源码绕过它;Day 27 专项和最终完整累计回归随后均干净通过。这个记录只能说明最终固定运行通过,不能把时序敏感检查包装成形式证明。
当前边界
实现只面向固定 QEMU 8.2.2、SeaBIOS 1.16.3、pc-i440fx-7.2、TCG、qemu32、1 CPU 和 64 MiB 环境。设备只接受 ID 0 标准三字节包,不支持滚轮、第四和第五按键、绝对坐标、触摸板、USB HID 或热插拔。
IRQ 中没有完成鼠标协议解析,但当前 UI 消费循环仍采用轮询队列后执行 sti; hlt; cli 的简单结构,没有通用输入事件对象、优先级或跨进程图形 ABI。每次变化仍重绘并复制完整 64,000 字节,不同步垂直回扫,也没有测量输入到显示的延迟。
QEMU 的普通相对移动路径会把大位移拆成有限增量,不会为本次 HMP 事件产生经典溢出位。溢出分支与插入 ACK 由确定性测试输入覆盖,不能声称来自真实宿主鼠标轨迹。
机制速查
| 遇到的问题 | 可迁移做法 | 本篇落点 |
|---|---|---|
| 多个设备共享不可重放的数据端口 | 单点读取,再按来源路由 | ps2_receive() |
| 初始化包含多次命令和响应 | 有界等待、受控重试、全部成功后发布 | reset/BAT/ID/F4 |
| 中断经过级联控制器 | 沿接收链反向逐级确认 | 从片 EOI,再主片 EOI |
| 变长状态可能因丢字节失步 | 候选边界、交叉字段校验、溢出时整段复位 | 三字节解析器与 64 字节队列 |
| 设备坐标系和显示坐标系相反 | 在事件归一化层转换一次 | screen_y -= packet_dy |
| 中断处理需要保持短小 | ISR 记录原始事实,任务上下文完成解释与渲染 | 原始队列到鼠标事件 |
下载与复现
下载第 27 篇完整源码。附件 SHA-256:
1 | |
解压后执行:
1 | |
附件包含第 01–27 篇累计工程和打包前形成的 docs/day27-* 证据,不含 build/。冻结后在新的空目录重建并复跑上述检查;day27-zipcheck.txt 是对冻结附件的复验收据,不写回 ZIP 自身。
练习
- 删除主片 IRQ2 的 unmask,只保留从片 IRQ12,比较 ISR/IRR 与“漏主片 EOI”故障的差别。
- 给三字节流随机删除一个字节,统计只依赖 bit 3 恢复时的误组包率;再加入符号一致性检查比较结果。
- 把全帧重绘次数与鼠标包数同时记录,合并一批连续移动后再刷新,观察输入状态和显示频率怎样解耦。
上一篇:26 - 字体与后备缓冲。
下一篇:28 - 窗口管理。
参考资料
- QEMU 7.2
hw/input/pckbd.c:8042 命令、状态位、共享输出缓冲和 IRQ1/IRQ12 路由。 - QEMU 7.2
hw/input/ps2.c:固定模拟鼠标的 reset、enable reporting、三字节包和位移拆分。 - Intel 8259A Programmable Interrupt Controller:级联结构与 End of Interrupt 语义。
- IBM Keyboard and Auxiliary Device Controller Hardware Interface Technical Reference:键盘与辅助设备控制器原始资料的公开镜像;正文中的固定行为以 QEMU 源码和实测为边界。

