跳转至

键盘与文本框 — 顺着 ACPI 表链校准时钟,然后敲下第一个字符

这一章讲什么: 两件事。 第一件:给时钟对上秒表。 上一章拿到 RSDP 后,本章沿 ACPI 的表链(XSDT → FADT) 找到频率已知的官方时钟 ACPI PM 定时器,让它给 Local APIC 定时器标定—— 从此「tick」在任何机器上都等于实打实的 10 毫秒。 第二件:让键盘敲出字符。 USB 键盘和鼠标同属 HID 类,驱动现成; 要做的是键码 → 字符的映射、Shift 等修改键、文本框窗口,以及一个 0.5 秒一闪的光标—— 定时器的第一次正式上岗。

1. 先看现象:一次标定 + 一次输入

主线例子 A,标定 Local APIC 定时器:

  RSDP(上一章从 UEFI 配置表按 GUID 领来)
    → rsdp.xsdt_address 处是 XSDT:一个「地址数组」,逐项找签名 "FACP" 的那张表(FADT)
    → FADT 里读出 pm_tmr_blk = ACPI PM 定时器的 IO 端口
    → WaitMilliseconds(100):
        记 start = 读 PM 定时器;等它走 0.1 秒(3.579545MHz × 0.1 ≈ 357955 次);
        同一段时间前后各读一次 Local APIC Current Count,差值 × 10 = Local APIC 频率
    → 按频率设 initial_count → 从此约每 10ms 一次中断,QEMU 和真机一致

  主线例子 B,敲一个 "A":
    按 Shift + a → USB 键盘发来 8 字节:字节0 = 修改键(Shift 位=1),
    字节2~7 = 正按着的键码(0x04 = a)
    → 驱动解析:modifier 有 Shift → 查 keycode_map_shifted[4] = 'A'
    → 消息 {kKeyPush, {keycode, ascii='A'}} 进 main_queue
    → 主循环画进文本框;光标右移一格;0.5s 周期定时器让它一闪一闪

2. 顶层全景

  12.1 表链第二段:XSDT(描述头 + 地址数组)→ 签名 FACP → FADT(276 字节,挖两个字段)
  12.2 标定:PM 定时器 3.579545MHz、24/32 位宽、溢出处理;100ms 测量;
       kTimerFreq=100 → tick = 10ms
  12.3 键盘:InitializeKeyboard() 注册按键 lambda;keycode_map;消息加 kKeyPush
  12.4 修改键:modifier 按位携带 8 键状态;USB 数据结构(字节0 修改键,字节2-7 键码);
       顺手修了一个差分比较的 bug(Alt+a 失灵)
  12.5 文本框:带框窗口 + InputTextWindow()(退格 = 涂白最后一个字符)
  12.6 光标:DrawTextCursor() + 0.5s 周期定时器反转可见位 → 闪烁

  图说:校准给了时间以意义;键盘给了事件以内容;光标是两者第一次合流。

3. 核心原理

3.1 XSDT 与 FADT:ACPI 的表链

ACPI 世界里每张表都带一个公共描述头(签名 + 长度 + 校验和); XSDT 的结构就是「描述头 + 一串 64 位地址」——每张后续表的地址1。 遍历它,找签名 "FACP" 的表:那就是 FADT(名字与签名不同是历史原因,不是笔误)2。 FADT 有 276 字节,但控制 PM 定时器只需要两个成员:pm_tmr_blk(IO 端口号)和 flags 的第 8 位 (计数器是 32 位还是 24 位)——结构体里用 reserved 数组跳过无关字段,这是直接映射 硬件数据结构的常规手法3。

3.2 标定:拿频率已知的尺子量未知的尺子

ACPI PM 定时器恒定 3.579545 MHz,24 或 32 位宽(24 位约 4.7 秒绕回一圈)4。 WaitMilliseconds() 的骨架:读起始值,算出「过 N 毫秒后的目标值」,死等计数器到达; 要小心溢出:目标值绕回比起始值小时,先等计数器归零再继续5。 标定程序等 100 ms(原本想测 1 秒,「启动慢 0.1 秒尚可接受」), 数出 Local APIC 计数器这段时间的减量 ×10 = 每秒计数 = 频率6。

有了频率,设定 initial_count 让每 10 ms 一次中断(kTimerFreq = 100)。 作者特意提示这个值的权衡:调大更精确,但超时处理更频繁、整体性能下降, 「找到合适的平衡点非常重要」7。图 12.3 的验收:原本「100 次计数」的周期定时器, 现在恰好 1 秒超时一次,而且 QEMU 与真机一致——可移植的时钟,成了8。

3.3 USB 键盘与键码映射

键盘和鼠标同属 USB HID 类(人机接口设备),而且驱动走的是 HID 启动接口—— 标准规定的固定 8 字节数据结构,免去解读「报告描述符」的麻烦(那是给认真做驱动的人准备的)9。 键盘发来的 8 字节:字节 0 是修改键位图,字节 2–7 是当前所有正按着的键码 (数组里没有的 = 没按)10。

键码(keycode)是每个键的编号(HID 标准定义,键码 4 = a),不是字符。 要显示字符得查映射表 keycode_map:按下时事件带键码,内核用表翻成 ASCII (键码 4 → 'a');Shift 按下时用另一张 keycode_map_shifted(键码 4 → 'A')11。 为什么键盘不直接发 ASCII?因为同一个键在不同修改键下字符不同, 键盘只负责报「哪个键」,字符是操作系统的解释12。

事件经 Message{kKeyPush, arg.keyboard={keycode, ascii}} 进主队列, 主循环里画出字符——「!!」 打成 「11」 的场景宣告 Shift 支持的到来13。

3.4 修改键与一个真实的 bug

修改键(Shift、Ctrl、Alt……)单独按不产生字符事件,所以它们的状态随下一次普通按键 一起上报:modifier 一个字节按位携带 8 个修改键的按下状态(如左 Ctrl + 右 Shift = 0x41)14。

这里还抓到一个值得记住的 bug:驱动用「上次 8 字节和这次的差分」找新按下的键, 但比较范围忘了跳过字节 0 和 1——某些修改键组合的位图恰好等于某键的键码, 于是那个键再也输不进去(按住左 Alt 时按 A 失灵)15。 修复只是 begin() 改 begin()+2,但发现它的过程说明:差分/增量逻辑的天敌是「恰好相等」。

3.5 文本框与闪烁光标

文本框 = 白底 + 框线的新窗口 + InputTextWindow():普通字符顺序填入, '\b' 退格 = 把最后一个字符涂白;字符数超上限(18 个)就拒收16。 光标是一条跟着 text_window_index 的竖条,显示/擦除二态17。

闪烁靠第 11 章的定时器:注册一个 0.5 秒超时的定时器,超时处理里反转可见标志、 重画光标、再注册下一个——周期定时器就是这么「手动续杯」实现的。 AddTimer 前后照例 cli/sti:timers_ 被中断处理程序读,改一半被打断是未定义行为18。

4. 作者的判断与证据

判断:标定优先于一切花哨功能。 本章把键盘这个大主角晾在 12.3 节,先用两节把时钟校准做完—— 因为「光标每 0.5 秒一闪」这类体验,完全建立在可信的时间上。

判断:用启动接口,不解读报告描述符。 又一次「够用就好」的取舍,理由写明: 报告描述符解释器复杂,启动接口是标准给引导器/UEFI 的退路,「至少能用键盘和鼠标」19。

证据: 表链每步都有校验(IsValid:签名 + 校验和);标定的验收是「100 tick 的周期定时器 恰好 1 秒超时、QEMU 真机一致」20;键盘的验收是输入截图——包括 Shift 打出的大写和符号。

5. 边界与局限

  • 只支持 US 布局的键码表:日文键盘的假名键、其他语种的布局不在映射表里; 第 28 章的字符编码问题会再次提醒这件事。
  • Ctrl/Alt 组合只解析出 Shift;Ctrl+C 这类「快捷键语义」要等终端命令(第 16 章)。
  • PM 定时器靠经验「几乎所有机型都有」:没有它的机器,标定程序跑不了——硬件世界的常态。
  • 0.1 秒标定会引入误差:作者明说「会有一点误差,但可以接受」;更认真的做法是多段测量求平均。

6. 可带走的

本章主线例子,一行写完: XSDT 找 FACP → FADT 取端口 → PM 定时器(3.579545MHz)量 100ms → Local APIC 频率到手 → 10ms tick;Shift+a → 8 字节报告 → keycode_map_shifted → 文本框 + 0.5s 闪烁光标。

  1. ACPI 表链:RSDP → XSDT(地址数组)→ 按签名找表;每张表带头 = 签名 + 校验和;
  2. 校准 = 拿频率已知的尺子量未知的尺子;溢出绕回要单独处理;
  3. kTimerFreq 是权衡值:分辨率 ↑ = 中断开销 ↑;教学系统取 100(10ms);
  4. 键盘发键码不发字符:字符 = 键码 + 修改键状态,经映射表解释;一版布局一张表;
  5. 修改键状态随普通按键上报;差分逻辑的敌人是「位图恰好等于键码」;
  6. HID 启动接口 = 固定的 8 字节报告,免去报告描述符;
  7. 文本框三件套:顺序填入、退格涂白、上限截断;光标 = 二态标志 + 0.5s 定时器续杯;
  8. 操纵中断处理程序共享的数据(如 AddTimer)要 cli/sti 夹住——这是第 7 章规矩的复用。

7. 原文地图

主题 原书章 原文位置
RSDP→XSDT→FADT 关系图 12.1 查找FADT(osbook_day12a) text/84-ch12-01-12-1-fadt-osbook-day12a.txt:5(搜「XSDT来知道FADT」)
描述头与 XSDT 结构 12.1 查找FADT(osbook_day12a) text/84-ch12-01-12-1-fadt-osbook-day12a.txt:13(搜「描述头」)
搜 FACP 签名 12.1 查找FADT(osbook_day12a) text/84-ch12-01-12-1-fadt-osbook-day12a.txt:42(搜「FACP是FADT的签名」)
FADT 挖两个字段 12.1 查找FADT(osbook_day12a) text/84-ch12-01-12-1-fadt-osbook-day12a.txt:51(搜「总共276」)
PM 定时器频率与位宽 12.2 使用ACPI PM定时器(osbook_day12b) text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:4(搜「3.579545MHz」)
WaitMilliseconds 与溢出 12.2 使用ACPI PM定时器(osbook_day12b) text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:9(搜「WaitMilliseconds」) · text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:32(搜「溢出」)
频率与 kTimerFreq 权衡 12.2 使用ACPI PM定时器(osbook_day12b) text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:41(搜「lapic_timer_freq」) · text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:49(搜「平衡点」)
100ms 测量与 1s 超时验收 12.2 使用ACPI PM定时器(osbook_day12b) text/86-ch12-03-12-3-usb-osbook-day12c.txt:5(搜「缩短为0.1s」) · text/86-ch12-03-12-3-usb-osbook-day12c.txt:9(搜「每10ms」)
键盘注册与键码定义 12.3 USB键盘驱动程序(osbook_day12c) text/86-ch12-03-12-3-usb-osbook-day12c.txt:39(搜「InitializeKeyboard」) · text/86-ch12-03-12-3-usb-osbook-day12c.txt:46(搜「键码是分配给每个键的数值」)
keycode_map 映射 12.3 USB键盘驱动程序(osbook_day12c) text/86-ch12-03-12-3-usb-osbook-day12c.txt:50(搜「keycode_map」)
「11」变「!!」的插曲 12.3 USB键盘驱动程序(osbook_day12c) text/86-ch12-03-12-3-usb-osbook-day12c.txt:61(搜「11」)
modifier 按位状态 12.4 修改键(osbook_day12d) text/87-ch12-04-12-4-osbook-day12d.txt:3(搜「修改键的状态」) · text/87-ch12-04-12-4-osbook-day12d.txt:22(搜「0x41」)
HID 启动接口与 8 字节结构 12.4 修改键(osbook_day12d) text/87-ch12-04-12-4-osbook-day12d.txt:38(搜「启动接口」) · text/87-ch12-04-12-4-osbook-day12d.txt:50(搜「字节位置」)
begin()+2 的 bug 修复 12.4 修改键(osbook_day12d) text/87-ch12-04-12-4-osbook-day12d.txt:92(搜「prev_buf.begin」) · text/87-ch12-04-12-4-osbook-day12d.txt:101(搜「无法输入A键」)
文本框与 InputTextWindow 12.5 文本框(osbook_day12e) text/88-ch12-05-12-5-osbook-day12e.txt:1(搜「文本框」) · text/88-ch12-05-12-5-osbook-day12e.txt:38(搜「InputTextWindow」) · text/89-ch12-06-12-6-osbook-day12f.txt:13(搜「18」)
光标绘制与闪烁定时器 12.6 光标(osbook_day12f) text/89-ch12-06-12-6-osbook-day12f.txt:37(搜「DrawTextCursor」) · text/89-ch12-06-12-6-osbook-day12f.txt:56(搜「0.5s」)
AddTimer 的 cli 理由 12.6 光标(osbook_day12f) text/89-ch12-06-12-6-osbook-day12f.txt:65(搜「禁用或启用中断」) · text/89-ch12-06-12-6-osbook-day12f.txt:72(搜「一团糟」)

  1. 出处:「12.1 查找FADT(osbook_day12a)」(text/84-ch12-01-12-1-fadt-osbook-day12a.txt:5(搜「XSDT来知道FADT」))。 ↩

  2. 出处:「12.1 查找FADT(osbook_day12a)」(text/84-ch12-01-12-1-fadt-osbook-day12a.txt:13(搜「描述头」))。 ↩

  3. 出处:「12.1 查找FADT(osbook_day12a)」(text/84-ch12-01-12-1-fadt-osbook-day12a.txt:42(搜「FACP是FADT的签名」))。 ↩

  4. 出处:「12.1 查找FADT(osbook_day12a)」(text/84-ch12-01-12-1-fadt-osbook-day12a.txt:51(搜「总共276」))。 ↩

  5. 出处:「12.2 使用ACPI PM定时器(osbook_day12b)」(text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:4(搜「3.579545MHz」))。 ↩

  6. 出处:「12.2 使用ACPI PM定时器(osbook_day12b)」(text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:9(搜「WaitMilliseconds」); text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:32(搜「溢出」))。 ↩

  7. 出处:「12.2 使用ACPI PM定时器(osbook_day12b)」(text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:41(搜「lapic_timer_freq」); text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:49(搜「平衡点」))。 ↩

  8. 出处:「12.2 使用ACPI PM定时器(osbook_day12b)」(text/86-ch12-03-12-3-usb-osbook-day12c.txt:5(搜「缩短为0.1s」); text/86-ch12-03-12-3-usb-osbook-day12c.txt:9(搜「每10ms」))。 ↩

  9. 出处:「12.3 USB键盘驱动程序(osbook_day12c)」(text/86-ch12-03-12-3-usb-osbook-day12c.txt:39(搜「InitializeKeyboard」); text/86-ch12-03-12-3-usb-osbook-day12c.txt:46(搜「键码是分配给每个键的数值」))。 ↩

  10. 出处:「12.3 USB键盘驱动程序(osbook_day12c)」(text/86-ch12-03-12-3-usb-osbook-day12c.txt:50(搜「keycode_map」))。 ↩

  11. 出处:「12.3 USB键盘驱动程序(osbook_day12c)」(text/86-ch12-03-12-3-usb-osbook-day12c.txt:61(搜「11」))。 ↩

  12. 出处:「12.4 修改键(osbook_day12d)」(text/87-ch12-04-12-4-osbook-day12d.txt:3(搜「修改键的状态」); text/87-ch12-04-12-4-osbook-day12d.txt:22(搜「0x41」))。 ↩

  13. 出处:「12.4 修改键(osbook_day12d)」(text/87-ch12-04-12-4-osbook-day12d.txt:38(搜「启动接口」); text/87-ch12-04-12-4-osbook-day12d.txt:50(搜「字节位置」))。 ↩

  14. 出处:「12.4 修改键(osbook_day12d)」(text/87-ch12-04-12-4-osbook-day12d.txt:92(搜「prev_buf.begin」); text/87-ch12-04-12-4-osbook-day12d.txt:101(搜「无法输入A键」))。 ↩

  15. 出处:「12.5 文本框(osbook_day12e)」(text/88-ch12-05-12-5-osbook-day12e.txt:1(搜「文本框」); text/88-ch12-05-12-5-osbook-day12e.txt:38(搜「InputTextWindow」); text/89-ch12-06-12-6-osbook-day12f.txt:13(搜「18」))。 ↩

  16. 出处:「12.6 光标(osbook_day12f)」(text/89-ch12-06-12-6-osbook-day12f.txt:37(搜「DrawTextCursor」); text/89-ch12-06-12-6-osbook-day12f.txt:56(搜「0.5s」))。 ↩

  17. 出处:「12.6 光标(osbook_day12f)」(text/89-ch12-06-12-6-osbook-day12f.txt:65(搜「禁用或启用中断」); text/89-ch12-06-12-6-osbook-day12f.txt:72(搜「一团糟」))。 ↩

  18. 出处:「12.3 USB键盘驱动程序(osbook_day12c)」(text/86-ch12-03-12-3-usb-osbook-day12c.txt:50(搜「keycode_map」))。 ↩

  19. 出处:「12.2 使用ACPI PM定时器(osbook_day12b)」(text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:4(搜「3.579545MHz」))。 ↩

  20. 出处:「12.4 修改键(osbook_day12d)」(text/87-ch12-04-12-4-osbook-day12d.txt:92(搜「prev_buf.begin」); text/87-ch12-04-12-4-osbook-day12d.txt:101(搜「无法输入A键」))。 ↩