操作系统学习14 IDT + PIC + 键盘中断

在这里插入图片描述

一、本章说明

1. 前置知识

请先阅读:

本章在 lesson13 基础上,把键盘输入从 CPU 空转轮询 升级为 IRQ1 硬件中断驱动

2. 本章目标

完成本章后,我们将能够:

  • 构造 IDT(中断描述符表),用 LIDT 告诉 CPU 中断向量对应的处理程序
  • 初始化 8259A PIC(ICW1~4 重映射、IMR 屏蔽),把 IRQ 映射到向量 32~47
  • 编写 IRQ1 键盘中断处理程序:读 0x60 扫描码、写入环形缓冲、向 PIC 发 EOI
  • init_keyboard 末尾 STI 开中断read_charHLT 休眠 等待按键,不再占用 CPU
  • 理解 8253 PIT 三通道、端口 0x40~0x43 及与 PIC IRQ0 的关系(本章屏蔽 IRQ0,下一章启用)

3. 目录结构

lesson14/
├── Makefile
├── boot/
│   ├── loader.s        # 与 lesson13 相同:读盘 + 进保护模式
│   └── pm.inc          # 段/门描述符宏
├── kernel/
│   ├── kernel.c        # 欢迎语 + 提示符 + 键盘回显(同 lesson13)
│   ├── utilities.s     # IDT + PIC + IRQ1 + 环形缓冲
│   └── utilities.h
└── README.md

4. 与 lesson13 的差异

项目 lesson13 lesson14
读键方式 循环 IN 0x64 轮询 IRQ1 中断 + 环形缓冲
IDT 256 项 IDT,注册向量 33
PIC 8259 重映射 + 仅开放 IRQ1
CPU 等待 空转轮询 .wait 缓冲优先;空则读 8042(Bochs 兜底)
8042 0xAE + 0xF4 额外 置命令字节 bit0 允许键盘中断
kernel.c 相同 相同

lesson14 中断

按键

8042 OBF=1

PIC IRQ1

CPU 查 IDT 向量33

irq1_handler 读0x60入缓冲

EOI → STI 后 HLT 唤醒

read_char 从缓冲取键

lesson13 轮询

IN 0x64

否 空转

read_char

OBF=1?

IN 0x60


二、IDT:中断来了跳去哪

1. 为什么需要 IDT

在实模式下,中断发生时 CPU 会去内存前 1KB 的 IVT(中断向量表) 寻找 4 字节的 CS:IP。但进入保护模式后,IVT 宣告失效,取而代之的是 IDT(Interrupt Descriptor Table)。

中断门描述符(Interrupt Gate)的硬件细节

IDT 数组中存放的每一项都是一个 8 字节(64位) 的门描述符。其硬件内存布局如下:

31                16 15  14 13 12  11  8 7    5 4        0
+---------------------+---+-----+---+-----+---+---+--------+
|   Offset 31:16      | P | DPL | 0 | Type| 0 | 0 |Reserved|32 位
+---------------------+---+-----+---+-----+---+---+--------+
|   Segment Selector  |           Offset 15:0              |32 位
+---------------------+------------------------------------+
  • Offset (31:0):中断处理程序(ISR)在内存中的 32 位绝对物理地址(分为高 16 位和低 16 位拼凑)。
  • Segment Selector (15:0):目标代码段的选择子。在平坦模型内核中,这里恒为内核代码段选择子 0x08(GDT 的第 1 项)。
  • Type (11:8):对于 32 位中断门,此字段固定为 0xE(即二进制 1110)。
  • DPL (14:13):特权级。内核中断设为 00。当发生硬件中断时,即使当前 CPU 处于用户态(DPL=3),也能无缝切入内核态执行。
  • P (15):有效位。置 1 表示该中断门有效。

门描述符里保存:

字段 含义
目标偏移 处理程序入口 EIP
段选择子 处理程序所在代码段(平坦模式下为 8
类型 0x8E = 32 位中断门(进入 ISR 时自动清 IF)

LIDT 加载 IDTR(基址 + 界限),与 LGDT 加载 GDT 类似。

2. 向量号布局

范围 用途
0 ~ 31 CPU 保留异常(#DE、#GP、#PF …)
32 ~ 47 常见 OS 把 8259 PIC 的 IRQ0~15 映射到此
48 ~ 255 软件中断、其他设备

IRQ 与向量不是同一个概念: 键盘占 IRQ1 这条硬件线;经 PIC 重映射后,对应 中断向量 330x20 + 1)。8259A 的端口、ICW/OCW、IMR、EOI 等详见 第三节

3. 代码:注册 IRQ1 门描述符

utilities.s 中预留 256×8 字节的 idt,只填写向量 33 一项:

SELECTOR_CODE equ 8
IRQ1_VECTOR   equ 33          ; PIC 重映射后 IRQ1 → 0x21

idt:      times 256 * 8 db 0
idt_ptr:
    dw idt_end - idt - 1
    dd idt

init_idt:
    MOV  EAX, irq1_handler
    MOV  WORD [idt + IRQ1_VECTOR * 8 + 0], AX      ; 偏移低 16 位
    MOV  WORD [idt + IRQ1_VECTOR * 8 + 2], SELECTOR_CODE
    MOV  BYTE [idt + IRQ1_VECTOR * 8 + 5], 0x8E  ; 32 位中断门
    SHR  EAX, 16
    MOV  WORD [idt + IRQ1_VECTOR * 8 + 6], AX      ; 偏移高 16 位
    LIDT [idt_ptr]
    RET

pm.inc 中的 Gate 可静态生成门描述符;本章仅注册一项,直接在内存里写字段更直观。


三、8259A 可编程中断控制器(PIC)

1. 8259A 是什么

8259A(Programmable Interrupt Controller,可编程中断控制器)是 IBM PC/AT 时代的经典芯片,用来管理 外部设备中断

CPU 只有一条 INTR 中断请求输入线,外设却有键盘、时钟、磁盘、串口等多路信号。8259A 的作用就是:

  1. 汇聚 多条 IRQ 线;
  2. 优先级 决定先响应哪一路;
  3. 向 CPU 拉 INTR;
  4. CPU 响应后,通过 INTA 周期告知 IRQ 编号
  5. OS 把 IRQ 编号换算成 中断向量号,再查 IDT 跳转。

IRQ1

IRQ0

IRQ6

INTR

INTA

查 IDT

键盘 8042

8259A PIC

时钟 8253

软盘

x86 CPU

中断处理程序

历史说明: 现代多核 PC 已普遍改用 APIC / IOAPIC,8259A 在物理机上多由芯片组模拟或仅用于兼容模式。但在 Bochs、QEMU 教学环境以及《30天自制操作系统》等入门路线中,8259A 仍是理解 「外设 IRQ → 向量号 → IDT」 的最佳起点。


2. 主从级联:最多 15 路 IRQ

一片 8259A 有 8 条 IRQ 输入(IRQ0~IRQ7)。PC/AT 通过 级联 再挂第二片,形成 主片 + 从片

芯片 I/O 端口 管理 IRQ
主片(Master) 命令 0x20 / 数据 0x21 IRQ0~7
从片(Slave) 命令 0xA0 / 数据 0xA1 IRQ8~15

从片的 INTR 输出接到 主片的 IRQ2,因此主片 IRQ2 被占用为「级联线」,实际可用 15 路 外设中断(IRQ01、37、8~15)。

INTR 接主片 IRQ2

从片 8259A 端口 A0h/A1h

IRQ8 RTC

IRQ9 ...

IRQ15 ...

主片 8259A 端口 20h/21h

IRQ0 时钟

IRQ1 键盘

IRQ2 级联从片

IRQ3 串口2

IRQ4 串口1

IRQ5 并口2

IRQ6 软盘

IRQ7 并口1

CPU INTR

本章键盘走主片 IRQ1,EOI 只需写主片命令端口 0x20


3. IRQ 线 vs 中断向量号

初学者容易混淆的两个概念:

概念 含义 键盘例子
IRQ 线 硬件请求线编号(8259A 输入脚) IRQ1
中断向量号 CPU 查 IDT 用的下标(0~255) 由 PIC 配置决定

8259A 只决定「哪条 IRQ 线对应哪个向量号」;CPU 拿到向量号后再查 IDT。因此 PIC 映射IDT 注册 必须一致。


3.1 为什么必须「重映射」(本章最核心)

这是本章最容易让初学者遭遇 Triple Fault(三重故障 → 重启) 的知识点。

致命隐患:向量号冲突

IBM PC 在设计之初留下了一个历史包袱:实模式下,BIOS 默认把 8259A 主片 IRQ0~7 映射到中断向量 8~15

然而 Intel 在设计 80386 保护模式时规定:向量 0~31 强制保留给 CPU 内部异常(如 0 为除零、13 为 #GP、14 为 #PF)。

硬件引脚 BIOS 默认向量 保护模式下 CPU 的理解 灾难后果
IRQ0(时钟) 0x08 #DF(Double Fault 双重故障) 时钟一触发,CPU 误以为发生双重故障,直接崩溃重启
IRQ1(键盘) 0x09 协处理器段超越(历史保留异常) 敲击键盘也会撞上异常区;IDT 空门 → #GP → #DF → Triple Fault
IRQ2(级联) 0x0A #TS(Invalid TSS) 从片相关中断同样踩雷
11~15 #NP / #SS / #GP / #PF / 保留 任意未屏蔽 IRQ 都可能引爆

BIOS 默认映射一览:

IRQ 默认向量号
IRQ0 8
IRQ1 9
IRQ2 10
IRQ7 15
IRQ8 16
IRQ15 23

8253 PIT 以约 18.2 Hz 周期拉高 IRQ0。若不重映射就 STI,时钟中断会以向量 8 进入 IDT——而这正是 #DF 的槽位。空门描述符 → #GP → 再 #DF → Triple Fault 重启。键盘 IRQ1→9 同理。

解药:ICW 四步握手,整体后移到 32~47

为避开 0~31 的雷区,必须向主片端口(0x20/0x21)和从片端口(0xA0/0xA1)按固定顺序发送 ICW(Initialization Command Words),把外部中断整体映射到 向量 32~47

步骤 命令字 本章典型值 作用
1 ICW1 0x110x20/0xA0 启动初始化,级联模式,需要 ICW4
2 ICW2 主片 0x20、从片 0x28 向量基址:IRQ0→32,IRQ8→40
3 ICW3 主片 0x04、从片 0x02 主片 IRQ2 接从片,从片编号 2
4 ICW4 0x01 8086 模式(x86 必备)
之后 IMR 主片 0xFD、从片 0xFF 仅开放 IRQ1,其余全屏蔽

重映射后的布局(本章做法,基址 0x20):

IRQ 新向量号 典型设备
IRQ0 32 8253 PIT Channel 0(系统 tick,见 第四节;本章 IMR 屏蔽)
IRQ1 33 8042 键盘(本章开放,IDT[33] 已注册)
IRQ2 34 级联从片
IRQ15 47 各类外设
向量范围 用途
0 ~ 31 CPU 保留异常(#DE、#DF、#GP、#PF 等)——外设 IRQ 绝不能落在这里
32 ~ 47 8259A 重映射后的 IRQ0~15
48 ~ 255 软件中断 / 其他设备

结论: 任何进入保护模式的 OS,都必须在 STI 之前 对 8259A 执行完整的 ICW1~4 重映射。本章 PIC_REMAP equ 1,详细位域与代码见下文 §5§6;踩坑记录见 第十节


4. 端口与寄存器组

8259A 通过 4 个 I/O 端口 访问,每个端口读写含义由 命令字 决定:

端口
0x20 主片 命令(OCW/ICW 首字节) 主片 ISR/IRR(需 OCW3)
0x21 主片 数据(ICW2~4、IMR) 主片 IMR
0xA0 从片 命令 从片 ISR/IRR
0xA1 从片 数据 从片 IMR

4.1 两类控制字

在微机原理与接口技术(特别是可编程中断控制器 8259A)中,在系统启动或复位后,要对 8259A 芯片的工作模式进行一次性配置,建立其基础的工作框架,即ICW,全称是 Initialization Command Words,中文称为初始化命令字。

ICW 的核心功能与分类

8259A 共有 4 个初始化命令字(ICW1 ~ ICW4),它们需要严格按照顺序写入,分别负责不同的基础配置:

  • ICW1(基本功能设置):决定中断请求的触发方式(边沿触发或电平触发)、设置是单片工作还是多片级联模式,并指示是否需要写入 ICW4。
  • ICW2(中断向量设置):用于设置中断向量基地址(中断类型码的起始地址)。CPU 收到中断后,会根据这个基地址和中断引脚的编号找到对应的中断服务程序。
  • ICW3(级联控制字):仅在多片 8259A 级联时使用。主片用它来标识哪些 IRQ 引脚连接了从片;从片用它来标识自己在级联系统中的编号。
  • ICW4(高级方式控制):用于配置更高级的系统行为,例如选择 CPU 系统模式(如 8086 模式)、设置中断结束方式(如自动 EOI)、缓冲模式以及全嵌套方式等。
ICW 与 OCW 的区别

与 ICW 对应的是 OCW(Operation Command Words,操作命令字)。两者的主要区别在于:

  • ICW(初始化命令字):属于一次性配置。在系统上电或复位时按严格顺序写入,决定了芯片的底层基础架构,初始化完成后一般不再改动。
  • OCW(操作命令字):属于运行时控制。在芯片初始化完成后的正常工作期间,可以随时写入,用于动态控制中断行为,例如动态屏蔽/开放某个中断(OCW1)、发送中断结束命令 EOI(OCW2)或读取内部状态等。
    结合你上一轮提供的汇编代码,init_pic 函数中向 0x20 和 0x21 等端口连续写入的 0x11、0x20、0x28 等字节,正是严格按照顺序写入的 ICW1 到 ICW4,用于重新映射(Remap)主从 8259A 的中断向量基地址。
类型 名称 何时使用
ICW Initialization Command Word 初始化 时按 ICW1→ICW4 顺序写入
OCW Operation Command Word 运行中 屏蔽 IRQ、读状态、发 EOI 等

初始化完成后,再写 0x11 等 ICW 值会破坏芯片状态,须重新初始化。

4.2 内部关键寄存器(不必直接读,理解即可)

寄存器 全称 作用
IRR Interrupt Request Register 外设拉高了 IRQ 线,对应 bit 置 1
ISR In-Service Register CPU 已开始处理的中断,对应 bit 置 1
IMR Interrupt Mask Register 1 = 屏蔽 该 IRQ,0 = 允许

优先级(默认): IRQ0 最高,IRQ7 最低;从片 IRQ8~15 整体优先级介于主片 IRQ1 与 IRQ2 之间(具体与 ICW4 配置有关)。


5. 初始化命令字 ICW1 ~ ICW4

8259A 上电后必须按固定顺序初始化。本章 init_pic主从各写一遍

ICW1 — 启动初始化(写命令端口)

0x20 / 0xA00x11

含义
D0 1 需要 ICW4
D1 1 级联模式(非单片)
D4 1 固定特征位,必须是 1

写 ICW1 后,芯片进入初始化状态,自动屏蔽所有 IRQ,并准备接收后续 ICW。

ICW2 — 中断向量基址(写数据端口)

芯片 写入端口 本章值 含义
主片 0x21 0x20 IRQ0→32,IRQ1→33,…
从片 0xA1 0x28 IRQ8→40,IRQ9→41,…

写入的是 IRQ0 / IRQ8 对应的向量号,后续 IRQ 依次 +1。

ICW3 — 级联关系(写数据端口)

芯片 本章值 含义
主片 0x04 bit2=1 → IRQ2 上接从片
从片 0x02 从片在中片级联链上的编号为 2

ICW4 — 附加模式(写数据端口)

0x21 / 0xA10x01

含义
D0 1 8086/8088 模式(必须,AT 架构)

未设置此项时,EOI、向量格式可能与 x86 保护模式不兼容。

初始化后立刻设置 IMR

ICW 序列结束后,通过 IMR 决定哪些 IRQ 能进来。写 数据端口 0x21 / 0xA1

    ; 主片:仅开放 IRQ1(键盘)
    MOV  AL, 0xFD          ; 二进制 11111101,bit1=0 → IRQ1 开放
    OUT  0x21, AL
    ; 从片:全部屏蔽
    MOV  AL, 0xFF
    OUT  0xA1, AL

IMR 位含义: bit N = 1 表示 屏蔽 IRQN0 表示 允许

bit1 IRQ1 键盘
0xFF 1 屏蔽
0xFD 0 开放

为何要屏蔽 IRQ0 等: 本章 IDT 只注册了向量 33。若 IRQ0(时钟)未屏蔽且触发,CPU 会跳到空的 IDT 项 → triple fault


6. 完整 init_pic 与 ICW 对照

init_pic:
    ; ICW1
    MOV  AL, 0x11
    OUT  0x20, AL          ; 主片
    OUT  0xA0, AL          ; 从片

    ; ICW2:向量基址
    MOV  AL, 0x20
    OUT  0x21, AL          ; 主片 IRQ0 → 32
    MOV  AL, 0x28
    OUT  0xA1, AL          ; 从片 IRQ8 → 40

    ; ICW3:级联
    MOV  AL, 0x04
    OUT  0x21, AL          ; 主片 IRQ2 接从片
    MOV  AL, 0x02
    OUT  0xA1, AL          ; 从片编号 2

    ; ICW4:8086 模式
    MOV  AL, 0x01
    OUT  0x21, AL
    OUT  0xA1, AL

    ; IMR:仅开放 IRQ1
    MOV  AL, 0xFD
    OUT  0x21, AL
    MOV  AL, 0xFF
    OUT  0xA1, AL
    RET

7. 一次键盘中断的完整流程

irq1_handler IDT CPU 8259A 主片 8042 按键 irq1_handler IDT CPU 8259A 主片 8042 按键 IF=1 且 STI 已执行 扫描码 OBF=1, 拉 IRQ1 IRQ1 有效 IRR bit1=1 INTR=1 INTA 周期 向量号 33 查门描述符 跳转 irq1_handler IN 0x60 读扫描码 OUT 0x20, 0x20 EOI IRET
  1. 8042 收到扫描码,置 OBF,若命令字节 bit0=1 则向 PIC IRQ1 发请求。
  2. PIC 置 IRR 相应位,若未被 IMR 屏蔽且优先级足够,向 CPU 拉 INTR
  3. CPU 完成当前指令且 IF=1,响应中断,经 INTA 从 PIC 取得 向量 33
  4. CPU 查 IDT[33],压栈 CS/EIP/EFLAGS,跳转 irq1_handler
  5. ISR 读 0x60(必须读,否则 8042 不会发下一次中断),写环形缓冲,发 EOI
  6. IRET 恢复现场;若主线程在 HLT,此时被唤醒。

8. EOI(End Of Interrupt)

中断处理程序结束前,必须通知 PIC:当前这一级 IRQ 已服务完毕,否则 ISR 位一直置位,同级或更低优先级中断可能被阻塞。

非特定 EOI(本章使用)

    MOV  AL, 0x20
    OUT  0x20, AL          ; 主片:结束优先级最高的那一条 in-service 中断

键盘 IRQ1 接 主片,写 0x20 命令端口 即可。

特定 EOI(了解)

若需明确结束某一条 IRQ,可写 0x20 | IRQ号 到主/从片命令端口,例如结束 IRQ1:OUT 0x20, 0x21。多 IRQ 并发、嵌套中断时才常用;本章单 IRQ 场景用 0x20 足够。

从片 IRQ 的 EOI

若中断来自 从片(IRQ8~15),须 向从片发 EOI, 向主片发 EOI(因为 IRQ2 级联)。键盘 IRQ1 在主片,无需0xA0


9. 运行期 OCW 操作(本章未用,供扩展)

操作 做法
屏蔽某 IRQ 读 IMR → 置对应 bit → 写回 0x21/0xA1
开放某 IRQ 读 IMR → 清对应 bit → 写回
读 IRR/ISR 先写 OCW3 到命令端口,再从命令端口读

例如开放 IRQ0 定时器(须先注册 IDT[32] 并初始化 PIT,见 第四节 8253):

    IN   AL, 0x21
    AND  AL, 0xFE          ; 清 bit0,开放 IRQ0
    OUT  0x21, AL

10. BIOS 默认 vs 本章重映射

场景 IRQ1 向量 IDT 应注册 是否写 ICW
BIOS 实模式 / 未 remap 9 IDT[9]
本章保护模式 OS 33 IDT[33]

常见错误: 执行了 init_pic 重映射,IDT 却仍注册向量 9;或 重映射,IDT 注册 33。按键后 CPU 跳到 未注册的门 → triple fault 或「无反应」(视环境而定)。

调试时可对照 tools/bochsout.txt:若 IF=1 且 EIP 停在 HLT,多为 8042 未开中断位IMR 仍屏蔽 IRQ1;若按键立刻重启,优先查 向量号与 IDT 是否一致


四、8253 可编程间隔定时器(PIT)

1. 8253 是什么

8253(Programmable Interval Timer,可编程间隔定时器)是 PC 上负责 硬件计时 的经典芯片;后续 8254 与其 pin 兼容、功能相近,BIOS/OS 文档里常统称 PIT

它内部有 3 个独立的 16 位计数器(通道 Channel 0~2),对固定 输入时钟 分频,在计数到设定值时改变输出电平,或向 8259A PIC 的 IRQ0 发中断请求。

Channel 0

Channel 1

Channel 2

输入时钟 1.193182 MHz

8253 PIT

PIC IRQ0

DRAM 刷新 历史用途

PC 扬声器 Gate

8259A

CPU 定时中断

与 8259A 的关系: 8253 不产生 CPU 向量,只拉 IRQ0 线;向量号仍由 PIC 决定(BIOS 默认 8,重映射后 32)。下一章做 定时器中断 / sleep / 调度 时,通常同时配置 PIT + IDT[32] + 开放 PIC IRQ0

本章为何屏蔽 IRQ0: init_pic 写 IMR 0xFD 屏蔽 IRQ0,因为尚未注册 向量 32 的处理程序;若开放,约 18.2 次/秒 的 IRQ0 会触发 triple fault。


2. 输入时钟与 3 个通道

2.1 输入频率

PC 上 PIT 输入时钟固定为:

f in = 1   193   182    Hz ≈ 1193182    Hz f_{\text{in}} = 1\,193\,182 \;\text{Hz} \approx 1193182 \;\text{Hz} fin=1193182Hz1193182Hz

(即 ISA 总线时钟 14.31818 MHz12 分频。)

输出频率:

f out = 1193182 分频值  N f_{\text{out}} = \frac{1193182}{\text{分频值 }N} fout=分频值 N1193182

分频值 N 为 16 位,范围 1 ~ 65535。例如 N = 65535 时:

f ≈ 1193182 65535 ≈ 18.2    Hz f \approx \frac{1193182}{65535} \approx 18.2 \;\text{Hz} f65535119318218.2Hz

约每 55ms 中断一次——BIOS 默认 Channel 0 常用此量级,也是 DOS 时代 INT 08h 系统时钟 tick 的来源。

2.2 三个通道在 PC 上的用途

通道 I/O 数据端口 PC/AT 典型用途 本章
0 0x40 系统定时器 → PIC IRQ0 被 IMR 屏蔽
1 0x41 历史:DRAM 刷新(现代几乎不用) 未使用
2 0x42 PC 扬声器(与 0x61 bit0 Gate 配合) 未使用

8253 三通道

OUT

OUT

OUT

Channel 0 系统时钟

Channel 1 刷新

Channel 2 扬声器

8259A IRQ0

内存刷新电路

0x61 bit0 Gate

扬声器


3. I/O 端口

端口 方向 含义
0x40 读/写 Channel 0 计数寄存器
0x41 读/写 Channel 1 计数寄存器
0x42 读/写 Channel 2 计数寄存器
0x43 控制字(选择通道、模式、读/写方式)

注意: 三个数据端口 共用 一套「先写控制字、再写分频值」的协议;必须先向 0x43 写入 控制字,再向 0x40/0x41/0x42 写入 16 位分频值(低字节、高字节顺序由控制字决定)。


4. 控制字(Control Word)格式

0x43 写入 1 字节 控制字,格式如下:

  7   6   5   4   3   2   1   0
┌───┬───┬───────┬───────────────┐
│ SC│ RW│  MODE │     BCD       │
└───┴───┴───────┴───────────────┘
字段 含义
SC 7~6 选择通道:00=Ch0,01=Ch1,10=Ch2,11=读回命令
RW 5~4 读/写方式:00=锁存,01=仅低字节,10=仅高字节,11=先低后高
MODE 3~1 工作模式 0~5
BCD 0 0=二进制计数,1=BCD(PC 上恒用 0

示例: 配置 Channel 0、先写低字节再写高字节、Mode 3、二进制:

SC=00 ,  RW=11 ,  MODE=011 ,  BCD=0 \text{SC=00},\ \text{RW=11},\ \text{MODE=011},\ \text{BCD=0} SC=00, RW=11, MODE=011, BCD=0
⇒ 0 x 0011011 2 = 0 x 36 \Rightarrow 0x0011011_2 = \mathbf{0x36} 0x00110112=0x36


5. 六种工作模式(MODE 0~5)

模式 名称 典型用途
0 中断结束计数 一次性定时,到时输出高电平
1 硬件可重触发单稳 脉冲宽度由计数值决定
2 速率发生器 周期性分频,计数到 1 时输出一个周期低脉冲
3 方波发生器 Channel 0 默认:对称方波,占空比约 50%
4 软件触发选通 写入计数值后启动
5 硬件触发选通 外部 Gate 触发

Channel 0 系统时钟 几乎总是 Mode 3(方波):输出在 0/1 间周期性翻转,每个周期可在 OUT 下降沿 触发 IRQ0(具体边沿与模式有关;BIOS 默认配置下约 18.2 Hz 进 IRQ0)。

Mode 2(速率发生器)也常见于 OS 自行初始化 PIT 时(如 Linux 早期 tick)。


6. 初始化 Channel 0 示例(下一章参考)

下列代码 本章未使用;演示如何把 PIT 设为 约 100 Hz,并配合 PIC/IDT 开放 IRQ0:

; 1193182 / 100 ≈ 11932
PIT_HZ      equ 100
PIT_DIV     equ 1193182 / PIT_HZ

init_pit_ch0:
    ; 控制字:Ch0, 先低后高, Mode 3, 二进制 → 0x36
    MOV  AL, 0x36
    OUT  0x43, AL

    MOV  AX, PIT_DIV
    OUT  0x40, AL          ; 低 8 位
    MOV  AL, AH
    OUT  0x40, AL          ; 高 8 位
    RET

配套步骤(下一章):

  1. IDT 注册 向量 32IRQ0_VECTOR equ 32
  2. init_pit_ch0 设置分频
  3. PIC IMR 清 bit0,开放 IRQ0:IN 0x21AND AL, 0xFEOUT 0x21, AL
  4. 定时器 ISR 内 tick++,发 EOI OUT 0x20, 0x20

7. 常用分频对照表

分频值 N 频率(约) 周期 典型场景
65535 18.2 Hz ~55 ms BIOS 默认 tick
11932 100 Hz 10 ms 简易内核 heartbeat
1193 1000 Hz 1 ms 较细粒度计时(开销大)
1193182 1 Hz 1 s 慢速演示

公式:N = 1193182 / 目标 Hz(整数除,会有微小误差)。


8. Channel 2 与 PC 扬声器(了解)

Channel 2 输出接 8255 PPI 端口 0x61 bit0(Gate),Gate=1 时方波驱动扬声器:

    ; 设置 Ch2 频率(控制字 0xB6 = Ch2, 先低后高, Mode 3)
    MOV  AL, 0xB6
    OUT  0x43, AL
    ; 写入分频值到 0x42 ...
    ; 打开 Gate
    IN   AL, 0x61
    OR   AL, 3              ; bit0 Gate, bit1 数据(简化)
    OUT  0x61, AL

本章节未使用,与键盘中断无直接关系。


9. 一次 IRQ0 定时中断的流程

timer_handler CPU 8259A IRQ0 8253 Ch0 1.193182 MHz timer_handler CPU 8259A IRQ0 8253 Ch0 1.193182 MHz 每个时钟递减计数 计数到 0,OUT 翻转 IRQ0 有效 INTR(若 IMR bit0=0) INTA 向量 32 IDT[32] tick++, 任务调度等 OUT 0x20, 0x20 EOI IRET

IRQ1 键盘 对比:

对比项 IRQ0(8253) IRQ1(8042 键盘)
设备 PIT Channel 0 8042 键盘接口
数据端口 0x40~0x43 0x60/0x64
触发频率 周期性(如 18.2 Hz) 事件驱动(按键)
重映射后向量 32 33
本章 IMR 屏蔽 开放并处理
ISR 典型工作 tick++、调度 读扫描码入缓冲

10. 本章与 8253 的关系

项目 本章做法 原因
PIT 初始化 不改(沿用 BIOS 默认 Ch0) 只演示键盘中断
PIC IRQ0 屏蔽(IMR bit0=1) 无 IDT[32],避免 triple fault
PIT 端口 不访问 避免破坏 BIOS 已设频率

BIOS 启动后 Channel 0 通常已在运行并 不断请求 IRQ0;本章用 0xFD 屏蔽 IRQ0 是刻意为之。下一章实现 定时器中断 时,再按 第 6 节 初始化 PIT、注册 IDT[32]、开放 IMR bit0 即可。


五、8042 与键盘中断使能

与 lesson13 相同:0xAE 启用键盘 + 0xF4 开启扫描。不在 Bochs 里读写 8042 命令字节(易卡死或破坏键盘)。

注意: 不要使用 0xAD/0xA7/0xD2 那套「完整 OSDev 流程」——在 Bochs 里可能 卡死在 wait_8042

init_8042:
    ; 0xAE → drain → 0xF4(kbd_cmd)→ drain
    RET

初始化顺序: CLIinit_pic(仅屏蔽 IRQ0)→ init_idt(向量 9 与 33)→ init_8042STI


六、IRQ1 处理程序与环形缓冲

1. 为何用缓冲

中断处理应尽量 :读硬件、存数据、EOI,复杂逻辑放到 read_char(主线程上下文)。扫描码先写入 环形队列,避免中断里调用 print_char 等可能重入的代码。

KBD_RING_SIZE equ 64
kbd_ring: times KBD_RING_SIZE db 0
kbd_rpos: dd 0
kbd_wpos: dd 0

2. irq1_handler

irq1_handler:
    PUSH EAX

    IN   AL, 0x60
    CALL kbd_ring_push

    MOV  AL, 0x20
    OUT  0x20, AL              ; EOI

    POP  EAX
    IRET

注意: ISR 必须用 IRET 返回;此处只保存/恢复 EAX 即可。

3. read_char:缓冲优先,端口兜底

kbd_pop_scancode:
    MOV  EAX, [kbd_rpos]
    CMP  EAX, [kbd_wpos]
    JNE  .from_ring
    CALL read_kbd_scancode     ; 缓冲空:与 lesson13 相同,读 8042
    RET
.from_ring:
    ; 从 kbd_ring 取一字节
    RET

设计说明:

  • IRQ 正常时irq1_handler 先入队,kbd_pop 只消费缓冲,不读端口。
  • Bochs 实测:IDT/PIC/IF 均正确时 IRQ1 仍可能进不了 handler,但 8042 OBF 有数据;缓冲空时 回退到 read_kbd_scancode,与 lesson13 一致,保证能打字。
  • 仍是 lesson14 架构(IDT + PIC + ISR + 环形缓冲),不是退回纯 lesson13。

read_char 仍采用:等按下 → 等释放 → 查 scancode_map


七、kernel.c

与 lesson13 相同,无需感知中断细节:

void main(void)
{
    clear_screen();
    init_keyboard();    /* 内部:PIC + IDT + 8042 + STI */
    print_str("NotOneOS 32-bit Protected Mode.\r\nWelcome..\r\n", ...);
    print_str("> ", 2);

    char a;
    fin:
        a = read_char();
        if (a != 0) {
            print_char(a);
        }
        goto fin;
}

八、编译与运行

cd lesson14
make
make install
make run      # Bochs
make debug    # 调试

验证清单:

  • 欢迎语与 > 提示符正常
  • 软件光标 _ 可见
  • 点击 Bochs 窗口 获得焦点后,按键可回显
  • 调试器中 IF=1read_char 停在 HLT 而非 IN 0x64 轮询
  • 无 triple fault

调试建议(详见下方「按键无反应排查」):

make debug
b 0xC200
c

地址以 ../target/kernel.map 为准(make 后自动生成)。


按键无反应排查

0. 你的日志说明什么

tools/bochsout.txt 末尾类似:

IF=1
EIP=0000cc5f
>> call ... (0x0000cc33)

表示:欢迎语已打印,STI 已执行,CPU 在 read_charkbd_popHLT 里等键盘中断,不是 lesson13 那种 IN 0x64 轮询。按键无反应 = IRQ1 没进来或 8042 没把扫描码送给 PIC

1. 先排除低级问题

  • 重新 make install 再运行。
  • 鼠标点击 Bochs 显示窗口(黑屏区域),不是下方调试命令行。
  • 确认能看到 Welcome..> 提示符。

2. 用断点定位「中断有没有触发」

重要: CPU 停在断点上时按键不会进中断。须 bc 清断点或只保留 irq1_handler,再 c 继续运行后按键。

make 后打开 ../target/kernel.map(当前典型地址):

符号 典型地址 作用
irq1_handler 0xCC2F 按键应最先进这里
read_char 0xCC5D 主循环读键
init_keyboard 0xCBF6 末尾应已 STI
IDT[9] 0xC342 默认 PIC_REMAP=0 时查此项

推荐命令:

bc
b 0xCC2F
c

点击 Display 窗口,按 a(调试器命令行不要抢焦点):

  • 能断在 irq1_handler → 中断通,查 scancode_map / 环形缓冲。
  • 断不到 → 继续第 4、5 步。

验证 IDT(PIC_REMAP=0 时查向量 9):

xp /8bx 0xC342
info cpu

期望:06 2f cc 08 00 8e 00 00 一类(指向 0xCC2F),且 eflags 含 IF

3. 对照 lesson13(最快二分)

用 lesson13 编译运行:若 lesson13 能回显、lesson14 不能 → 8042 有数据,问题在 IDT/PIC/8042 命令字节 bit0。若 两个都不行 → 先查 Bochs 焦点和键盘配置。

4. 查 PIC 与向量号(最常见根因)

代码里 utilities.s 顶部:

PIC_REMAP equ 0    ; Bochs 默认:不重映射,IRQ1 → 向量 9
                   ; 改为 1 则 ICW 重映射,IRQ1 → 向量 33
PIC_REMAP IRQ1 向量 IDT 应注册 主片 IMR
0(Bochs 推荐) 9 IDT[9] @ 0xC342 读改写:屏蔽 IRQ0、开放 IRQ1
1(真 OS 常用) 33 IDT[33] @ 0xC402 0xFD

你之前的症状(IDT[33] 正确、IF=1、按 a 不进 handler):多半是 Bochs 仍按 BIOS 发向量 9,或 8042 命令字节 bit0 未真正置 1。当前默认已改为 PIC_REMAP=0 + 读命令字节前 drain_kbd

  • 按键后 立刻重启 → IDT 向量与 PIC 映射不一致(例如 remap=1 却只注册了 9)。
  • 不重启也不进 handler → 8042 未开 IRQ(查命令字节 bit0)或未点 Display 焦点。

5. 查 8042 是否有数据(中断以外的证据)

程序停在 HLT 时,若能在调试器里看到 0x64 的 bit0(OBF)随按键变 1,说明扫描码已到 8042,只是 没产生 IRQ → 重点查 init_8042 里命令字节 bit0 是否为 1

6. 现象对照

现象 方向
IF=1,EIP 在 kbd_pop/HLT 等中断;查 8042 bit0、PIC IMR、窗口焦点
按键即重启 IDT 向量号与 PIC 映射不一致
进了 irq1_handler 无字符 查表 / make-break 逻辑
lesson13 行、lesson14 不行 中断路径问题

九、常见问题

现象 可能原因
triple fault 启动即崩溃 IDT 未注册却 STI;或 IRQ0(8253) 未屏蔽且无 IDT[32]
有欢迎语但按键无反应 未点击 Bochs 窗口;8042 未开扫描/未开中断位
中断进错向量 PIC 未重映射却用向量 9;或重映射了却用向量 9
只响应一次键 忘记 EOI
乱码 scancode_map 偏移错误(见 lesson13 说明)

向量号对照:

配置 IRQ1 对应向量
BIOS 默认(不重映射 PIC) 9
本章 PIC 重映射到 0x20 33

本章采用 重映射 + 向量 33,与主流教程(《30天自制操作系统》等)一致。


十、PIC_REMAP 必须为 1:踩坑与修复

理论背景(向量冲突、Triple Fault、ICW 四步握手)已写入 第三节 §3.1。本节只保留实践结论。

1. 问题现象

PIC_REMAP equ 0(沿用 BIOS 默认 IRQ0~7 → 向量 8~15),在 Bochs 中常见:按键无反应,且很快自动重启(triple fault)

2. 根因(一句话)

BIOS 映射与 CPU 异常区 0~31 重叠;PIT 的 IRQ0 以向量 8(#DF)进来,空 IDT 门 → #GP → #DF → Triple Fault。即使勉强屏蔽 IRQ0,不写完整 ICW 也无法可靠开放 IRQ1、配置从片与 8086 模式。

3. 修复

utilities.s 中改为:

PIC_REMAP     equ 1    ; 必须为 1:保护模式下必须重映射 8259A

init_pic 走完整 ICW1~4(主片基址 0x20、从片 0x28)+ IMR 0xFD/0xFF,IRQ1 → 向量 33,与 IDT[33] 一致。对照表见 §3.1§6

4. 教训总结

要点 说明
保护模式必须重映射 PIC BIOS 默认 8~15 与异常重叠,IRQ0 一触发即 triple fault
IMR 必须显式设置 0xFD 精确只开放 IRQ1,不能赌 BIOS 遗留值
IDT 向量号必须与 PIC 映射一致 重映射后 IRQ1→33;不一致则按键重启或无反应
ICW4 的 8086 模式位不可省略 不写 ICW4 则 EOI / 向量格式可能与 x86 不兼容

十一、本章小结

  • IDT 定义「向量号 → 处理函数」;8259A PIC 定义「IRQ 线 → 向量号」,并通过 IMR 屏蔽 / EOI 结束 管理中断生命周期
  • 8253 PIT1.193182 MHz 分频驱动 IRQ0;BIOS 默认约 18.2 Hz;本章 屏蔽 IRQ0,下一章再初始化 Channel 0 并注册 IDT[32]
  • 8259A 主从级联共 15 路 IRQ;键盘占 主片 IRQ1,重映射后对应 向量 33
  • 键盘中断链路:按键 → 8042 → PIC IRQ1 → CPU → IDT[33] → irq1_handler → EOI
  • 环形缓冲 + HLT 把「硬件响应」与「字符处理」解耦,CPU 不再空转
  • lesson13 的显存输出、软件光标、扫描码表可原样复用

后面章节可在此基础上增加:8253 定时器 IRQ0(tick / sleep)、Shift 大写、或 分页(CR3 + 页表)

源码:
https://gitee.com/xundh/learn-os/tree/master/lesson14

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐