键盘与文本框 — 顺着 ACPI 表链校准时钟,然后敲下第一个字符
键盘与文本框 — 顺着 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 闪烁光标。
- ACPI 表链:RSDP → XSDT(地址数组)→ 按签名找表;每张表带头 = 签名 + 校验和;
- 校准 = 拿频率已知的尺子量未知的尺子;溢出绕回要单独处理;
- kTimerFreq 是权衡值:分辨率 ↑ = 中断开销 ↑;教学系统取 100(10ms);
- 键盘发键码不发字符:字符 = 键码 + 修改键状态,经映射表解释;一版布局一张表;
- 修改键状态随普通按键上报;差分逻辑的敌人是「位图恰好等于键码」;
- HID 启动接口 = 固定的 8 字节报告,免去报告描述符;
- 文本框三件套:顺序填入、退格涂白、上限截断;光标 = 二态标志 + 0.5s 定时器续杯;
- 操纵中断处理程序共享的数据(如 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(搜「一团糟」) |
Footnotes
-
出处:「12.1 查找FADT(osbook_day12a)」(
text/84-ch12-01-12-1-fadt-osbook-day12a.txt:5(搜「XSDT来知道FADT」))。 ↩ -
出处:「12.1 查找FADT(osbook_day12a)」(
text/84-ch12-01-12-1-fadt-osbook-day12a.txt:13(搜「描述头」))。 ↩ -
出处:「12.1 查找FADT(osbook_day12a)」(
text/84-ch12-01-12-1-fadt-osbook-day12a.txt:42(搜「FACP是FADT的签名」))。 ↩ -
出处:「12.1 查找FADT(osbook_day12a)」(
text/84-ch12-01-12-1-fadt-osbook-day12a.txt:51(搜「总共276」))。 ↩ -
出处:「12.2 使用ACPI PM定时器(osbook_day12b)」(
text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:4(搜「3.579545MHz」))。 ↩ -
出处:「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(搜「溢出」))。 ↩ -
出处:「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(搜「平衡点」))。 ↩ -
出处:「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(搜「键码是分配给每个键的数值」))。 ↩ -
出处:「12.3 USB键盘驱动程序(osbook_day12c)」(
text/86-ch12-03-12-3-usb-osbook-day12c.txt:50(搜「keycode_map」))。 ↩ -
出处:「12.3 USB键盘驱动程序(osbook_day12c)」(
text/86-ch12-03-12-3-usb-osbook-day12c.txt:61(搜「11」))。 ↩ -
出处:「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」))。 ↩ -
出处:「12.4 修改键(osbook_day12d)」(
text/87-ch12-04-12-4-osbook-day12d.txt:38(搜「启动接口」);text/87-ch12-04-12-4-osbook-day12d.txt:50(搜「字节位置」))。 ↩ -
出处:「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键」))。 ↩ -
出处:「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」))。 ↩ -
出处:「12.6 光标(osbook_day12f)」(
text/89-ch12-06-12-6-osbook-day12f.txt:65(搜「禁用或启用中断」);text/89-ch12-06-12-6-osbook-day12f.txt:72(搜「一团糟」))。 ↩ -
出处:「12.3 USB键盘驱动程序(osbook_day12c)」(
text/86-ch12-03-12-3-usb-osbook-day12c.txt:50(搜「keycode_map」))。 ↩ -
出处:「12.2 使用ACPI PM定时器(osbook_day12b)」(
text/85-ch12-02-12-2-acpi-pm-osbook-day12b.txt:4(搜「3.579545MHz」))。 ↩ -
出处:「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键」))。 ↩