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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void ps2_receive(void)
{
uint8_t status = inb(0x64);
if (!(status & 1))
return;

uint8_t byte = inb(0x60);
if (status & 0xc0) {
if (status & 0x20)
++mouse_dropped;
else
++keyboard_dropped;
} else if (status & 0x20) {
route_mouse(byte);
} else {
route_keyboard(byte);
}
}

这里的顺序不能颠倒:先保存状态,再读取一次数据,最后按保存的来源位路由。IRQ1 和 IRQ12 都调用这个入口,但不再拥有独立的端口读取逻辑。设备队列仍然分开,键盘扫描码解析器不会看到鼠标位移,鼠标组包器也不会把键盘释放码当成包尾。

专项运行同时注入鼠标动作和键盘字符串 kbd。最终六个键盘 make/break 字节与 147 个鼠标字节分别对账:

1
D27 SHARED keyboard=kbd kirq=6 krouted=6 kdropped=0 meoi=147/147

这种结构可以概括为“共享入口、按来源分流”。多个逻辑设备复用一个硬件读取窗口时,只允许一个位置消费不可重放的数据,再把副作用较小的解析工作交给各自队列。

初始化是一段有界事务

鼠标在上电后不会自动按预期发送标准三字节包。初始化先禁用键盘和辅助端口,排空旧输出,读取控制器配置字节,再依次读取、修改和写回,保留已有策略。键盘继续使用 set 1 扫描码,辅助端口则执行 reset 和 enable reporting:

1
2
3
4
controller: AD, A7, drain, 20, 60, AE, A8
keyboard: F5, F0, 01, F4
mouse: FF -> FA AA 00
mouse: F4 -> FA

发送鼠标命令前,控制器端口先收到 0xD4,表示下一字节转发给辅助设备。每次等待 input-buffer empty 或 output-buffer full 都有固定轮询上限。命令收到 0xFE RESEND 时最多重发三次,其他响应或超时都使初始化失败。

专项镜像把第一次 0xF4 的 ACK 有意解释为 RESEND,再发送第二次。运行记录同时证明 reset 的 ACK、BAT 和设备 ID:

1
INIT ack=2 bat=0xaa id=0 resend=1 selftest=2/3/1

id=0 把本篇协议限定为普通三字节鼠标。滚轮与五键扩展需要额外采样率序列和四字节解析,不在本次状态机里偷偷兼容。

配置只在所有设备命令成功后打开 IRQ。最终控制字保留键盘 IRQ1,启用鼠标 IRQ12,并关闭键盘翻译;PIC 同时放开主片 IRQ2 级联线和从片 IRQ12。初始化失败时不会发布一个只有部分设置生效的“可用鼠标”。

IRQ12 要完成两级确认

8259A 级联结构中,鼠标的 IRQ12 进入从片 IR4;从片再通过主片 IRQ2 把请求送到 CPU。PIC 已被重映射到 0x200x28,所以 IRQ12 对应 IDT vector 44。汇编 stub 表和 IDT 初始化范围都从 34 项扩展到 45 项,仍复用原来的 68 字节同特权级 trap frame。

GDB 在真实 mouse_irq 入口检查到:

1
IRQ frame vector=44 error=0 cs=8 saved_if=1 handler_if_df=0

处理结束要先确认从片,再确认主片:

1
2
outb(0xa0, 0x20);
outb(0x20, 0x20);

先从后主与中断经过控制器的方向相反。正常镜像中,每次 IRQ12 都产生一对 EOI,最终计数为 147/147,两个 PIC 的 ISR 均清零。

两个故障镜像分别省略一半操作。漏从片 EOI 后,从片 ISR 的 IRQ12 位保持为 1;漏主片 EOI 后,主片 ISR 的 IRQ2 位保持为 1:

1
2
3
4
5
STALLED irq=1 slave_eoi=0 master_eoi=1
pic1: irr=10 imr=ef isr=10

STALLED irq=1 slave_eoi=1 master_eoi=0
pic0: irr=05 imr=f9 isr=04

两种情况下再次移动鼠标都没有第二次 IRQ12。只观察“处理程序运行过一次”无法证明中断确认正确;ISR 位和第二次请求能否进入,才说明整条级联链已经恢复可用。

三字节包的边界不是天然可靠

标准包的第一个字节最低三位是左右中键,bit 3 固定为 1,bit 4/5 是 X/Y 符号,bit 6/7 是溢出标记。后两个字节保存 8 位位移。

解析器仅在等待首字节时要求 bit 3 为 1:

1
2
3
4
5
6
7
8
if (!parser->index) {
if (!(byte & 0x08)) {
++parser->sync_losses;
return 0;
}
parser->bytes[parser->index++] = byte;
return 0;
}

第二、第三字节也可能碰巧设置 bit 3,因此不能每看到 bit 3 就重开一包。凑齐三字节后还要拒绝溢出位,并检查首字节的符号位是否与位移字节最高位一致:

1
2
3
4
5
6
7
8
9
10
uint8_t flags = p->bytes[0];
if (flags & 0xc0)
return -1;
if (!!(flags & 0x10) != !!(p->bytes[1] & 0x80) ||
!!(flags & 0x20) != !!(p->bytes[2] & 0x80))
return -1;

event->dx = (int8_t)p->bytes[1];
event->dy = -(int8_t)p->bytes[2];
event->buttons = flags & 7;

屏幕坐标的 Y 轴向下为正,而 PS/2 位移向上为正,因此 dy 需要取反。符号一致性检查能拒绝部分错位组合,却不能从任意字节丢失中唯一找回边界。数据字节本身可能满足 bit 3,纯字节流没有足够信息消除全部歧义。

确定性自检依次送入合法包、非法首字节、丢失第二字节、插入的 0xFA、溢出包和恢复后的合法包,得到两个包、三次同步丢失和一次溢出拒绝。宿主侧解析器测试使用同一生产函数,避免用另一份“测试版协议”证明自己。

恢复策略因此放在队列边界。64 字节原始队列溢出后,消费者丢弃残余队列并清空半包状态,等待新的候选首字节。测试暂停消费者并连续注入移动,实际得到:

1
QUEUE dropped=12 irqs=78 routed=78

恢复后继续送入合法事件,最终 reset=1packets=23。这证明溢出确实发生,也证明解析器没有永久停在错误字节相位。

中断只收字节,任务上下文生成事件

IRQ 路径完成三件事:读取共享端口、把字节写入固定队列、发送 EOI。三字节组包、坐标运算、字符渲染和 64,000 字节全帧复制都不在中断处理程序里执行。

任务上下文取出一个完整事件后,用 64 位中间值累加位移,再裁剪到 [0,319] × [0,199]。指针使用 Day 26 已验证的有符号矩形裁剪函数画成十字,因此中心到达边缘时,伸出屏幕的线段会被裁掉,中心像素仍然可见。

测试依次把指针送到左、右、上、下四条边,按下并释放左、中、右键,同时走键盘分流路径。最终状态为:

1
STATE pos=319,199 edges=0xf buttons=0x7 changes=6 packets=23 reset=1 irq=147 route=147 eoi=147/147 keys=kbd

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
58b14d376ac1cd9a3c25b2128e6688738ec4c4e32b423d11255f01ca28db836f

解压后执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unzip os-day-27.zip
cd os-day-27
make build
make check-mouse
make check-graphics-text
make check-graphics
make check-pm
make check-pipe
make check-shell
make check-fd
make check-fat
make check-block
make check-process
make check-syscall
make check-elf
make check-memory
make check-runtime-limit
make check-keyboard

附件包含第 01–27 篇累计工程和打包前形成的 docs/day27-* 证据,不含 build/。冻结后在新的空目录重建并复跑上述检查;day27-zipcheck.txt 是对冻结附件的复验收据,不写回 ZIP 自身。

练习

  1. 删除主片 IRQ2 的 unmask,只保留从片 IRQ12,比较 ISR/IRR 与“漏主片 EOI”故障的差别。
  2. 给三字节流随机删除一个字节,统计只依赖 bit 3 恢复时的误组包率;再加入符号一致性检查比较结果。
  3. 把全帧重绘次数与鼠标包数同时记录,合并一批连续移动后再刷新,观察输入状态和显示频率怎样解耦。

上一篇:26 - 字体与后备缓冲

下一篇:28 - 窗口管理。

参考资料