起因

调度篇(中断驱动的硬实时层与协作式主循环)承诺"硬实时层的确定性来自中断优先级",这篇兑现它:屏蔽 PIC、启用 APIC、配置定时器、校准频率,最后加上工业系统天天要用的系统时间读写(RTC/CMOS 实操)。全部给到寄存器级。这条时间线上的每个部件都有来历:8259A 是 Intel 1976 年给 8080 配的中断控制器,PC/AT 上两片级联管 15 路中断一用四十年;APIC 体系是九十年代为多处理器机器生的,最早是主板上的外置 82489DX,Pentium 起把 Local APIC 塞进 CPU,多核时代每核一份;HPET 是 Intel 和微软 2000 年代联手定的规范,接 1981 年 IBM PC 那颗 8253/8254 PIT 的班——DOS 时代 18.2Hz 的系统节拍就是 PIT 传下来的祖制;RTC 则从 PC/AT(1984)起蹲在 CMOS 里,靠纽扣电池活到今天。

一、先屏蔽 PIC

8259A 是上世纪遗产,spurious IRQ7/15 会骚扰新框架。屏蔽(先从片后主片,防级联毛刺):

1
2
3
4
unsafe {
    x86_out(0xA1, 0xFF);   // 从片 IMR 全屏蔽
    x86_out(0x21, 0xFF);   // 主片 IMR 全屏蔽
}

Local APIC 存在性不用猜:从移交的 RSDP → XSDT → MADT(签名 “APIC”)解析,IO APIC 基址也在 MADT 的条目里。另外一句要提前说:SMI 是平台级黑洞(触发时 CPU 消失几十微秒,软件无感知),工控主板 BIOS 里关掉不必要的 SMI 源(legacy USB 支持之类)是硬件侧功课——它在中断延迟直方图上留下的毛刺,实时性验证篇会再遇到。

二、Local APIC:寄存器表与优先级布局

Local APIC 是一组 MMIO 寄存器,基址从 MSR 0x1B(IA32_APIC_BASE)读,bit 11 是全局使能,基址字段惯例 0xFEE00000向量号高 4 位就是优先级,布局直接落实两级调度模型:

向量区 优先级 挂载内容
0xF0–0xFF 最高 安全联锁(急停/断丝/限位)
0xE0–0xEF 次高 放电脉冲
0xD0–0xDF 插补节拍
0x20–0xCF 普通 设备中断(NVMe/USB 等)

MMIO 偏移表(基址 + 偏移,访问用 read_volatile/write_volatile):

偏移 名称 用途
0x080 TPR 任务优先级(屏蔽低优先级中断)
0x0B0 EOI 中断结束,写 0
0x0F0 SVR spurious 向量 + bit8 使能 APIC
0x320 LVT Timer 定时器 LVT:向量 + 模式
0x380 TIMER_ICR 初始计数(写入即启动)
0x390 TIMER_CCR 当前计数(递减)
0x3E0 TIMER_DCR 分频

初始化序列(对应代码):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const APIC: *mut u32 = 0xFEE0_0000 as *mut u32;

unsafe {
    // 1. 使能 APIC:SVR = spurious vector 0xFF | bit8
    write(APIC, 0xF0/4, 0x1FF);
    // 2. 定时器分频 16:DCR = 0x3
    write(APIC, 0x3E0/4, 0x3);
    // 3. LVT Timer:向量 0xD0(插补),periodic 模式 = bit17
    write(APIC, 0x320/4, 0xD0 | (1 << 17));
    // 4. 初始计数(校准后写入,见下节)
    write(APIC, 0x380/4, counts_for(period_us));
}

EOI 纪律:每个 ISR 尾部必须 write(APIC, 0xB0/4, 0),忘了就收不到下一个同优先级中断,且无任何报错——症状是"跑着跑着脉冲停了",极难定位,此处立碑。

三、频率校准:APIC 定时器没有标准频率

APIC 定时器频率随 CPU 在几 MHz 到几十 MHz 漂,必须开机校准。用 HPET 做基准:ACPI HPET 表(签名 “HPET”)给出 MMIO 基址,主计数器在偏移 0xF0(64 位自由运行),GCAP_ID(偏移 0x0)的 bits 32–63 是计数周期,单位飞秒(典型 10ns 级):

1
2
3
4
5
6
7
8
// 校准:窗口 T 微秒内数 APIC 递减次数
let c0 = read(APIC, 0x390/4);          // 先装一个大 ICR 让它跑起来
let t0 = hpet_read();                   // 64 位主计数器
wait_hpet(10_000);                      // 等 10ms(按 fs 周期换算)
let c1 = read(APIC, 0x390/4);
let ticks_per_us = (c0 - c1) / 10_000;  // 每微秒计数
// 目标周期 100µs 的插补中断:
write(APIC, 0x380/4, ticks_per_us * 100);

校准窗口取 10ms 是个权衡:窗口太短,计数量的量化误差占比大;太长,开机白等。另外 periodic 模式下 ICR 只写一次——计数减到 0 触发中断后硬件自动重装,ISR 里不用重填,这也是选 periodic 不选 one-shot 的理由:节拍固定,省一次写寄存器,也少一个抖动源。放电几十 kHz、插补几 kHz 是这类机床的典型量级,精确数值跟机械参数联动,属应用篇。TSC 单独说:CPUID 确认 invariant TSC(CPUID.80000007H:EDX[8])后只做时间戳(日志、事件审计),不做中断源——它快但不是事件驱动。

四、MSI:外设中断的正确姿势

MSI 的机制一句话:设备对 0xFEE00000 加向量信息发起一次普通内存写,中断即触发——无共享线、无电平维持,延迟确定。自研 FPGA 卡和 NVMe 都设计成 MSI(PCIe 篇细讲 MSI capability 配置)。IO APIC 重定向表保留给真正的外部引脚(目标板有才用)。

五、系统时间:RTC/CMOS 读写实操

工业系统对时间是刚需:加工记录时间戳、定时参数、断电恢复后对表。方案 = 开机从 RTC 读墙钟 + 运行期用校准过的单调时钟(HPET/TSC)推进,需要校时再写回 RTC。

RTC/CMOS 用两个端口:0x70 选寄存器(bit7 置 1 顺便屏蔽 NMI),0x71 读写数据。寄存器布局:

索引 内容 索引 内容
0x00 0x07
0x02 0x08
0x04 0x09 年(00–99)
0x0A 状态 A 0x0B 状态 B
0x32 世纪(ACPI 有才可靠)

读之前必须等"更新进行中"标志清零(否则读到撕裂数据):轮询状态 A 的 bit7(UIP),为 1 时 RTC 正在刷寄存器,等它变 0 再读。读出的格式由状态 B 决定,两种编码都要处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn read_rtc() -> DateTime {
    while cmos_read(0x0A) & 0x80 != 0 {}       // 等 UIP 清零
    let (s, m, h) = (cmos_read(0x00), cmos_read(0x02), cmos_read(0x04));
    let (d, mo, y) = (cmos_read(0x07), cmos_read(0x08), cmos_read(0x09));
    let b = cmos_read(0x0B);
    let century = cmos_read(0x32);
    if b & 0x04 == 0 {                          // BCD 模式:逐字节转换
        // (v & 0x0F) + (v >> 4) * 10
    }
    if b & 0x02 == 0 {                          // 12 小时制:PM 位换算
    }
    // y + century * 100 拼出完整年份
}

写时间(校时)用状态 B 的 bit7(SET 位)冻结更新再写:

1
2
3
4
5
6
unsafe {
    let b = cmos_read(0x0B);
    cmos_write(0x0B, b | 0x80);                 // 冻结更新
    cmos_write(0x00, to_bcd(sec));              // 写回时注意目标格式
    cmos_write(0x0B, b);                        // 解冻
}

运行期墙钟 = boot_time + 已流逝时间,流逝时间用 HPET 主计数器(或校准后的 TSC)算,跟 RTC 脱钩——RTC 精度月漂几分钟,只做开机基准和人工校时入口。RTC 还送一个隐藏福利:状态 A 的分频允许它产生 2Hz–8192Hz 的周期中断(IRQ8,状态 B bit6 使能),低速场景可以当备用慢时钟。

总结

实战清单:双端口屏蔽 PIC、MSR 0x1B 确认基址、SVR/DCR/LVT/ICR 四步配好周期定时器、HPET 窗口法校准 ticks_per_us、ISR 尾部写 EOI、RTC 等 UIP + BCD/12h 兼容读法 + SET 位冻结写法。跑通标志:插补向量的周期中断稳定触发(串口每秒打印计数不丢拍),read_rtc() 与宿主机时间一致。下一篇:显卡——从 GOP 帧缓冲到自己的显示输出。

参考链接