GB/CPU/LR35902 中断

中断(Interrupt)是计算机科学中的一个术语, 指 CPU 接收到来自硬件或软件的事件信号. 来自硬件的中断称为硬件中断, 来自软件的中断称为软件中断.

在接收到来自外围硬件的异步信号, 或来自软件的同步信号之后, CPU 将会进行相应的硬件软件处理. 发出中断信号称为进行中断请求(Interrupt request, IRQ). 硬件中断将导致 CPU 通过一个运行时上下文切换来保存当前执行状态(以程序计数器和程序状态字等寄存器信息为主). 软件中断则通常作为 CPU 指令集中的一个指令, 以可编程的方式直接指示这种运行信息切换, 并将处理导向一段中断处理代码. 中断在计算机多任务处理, 尤其是即时系统中尤为有用. 这样的系统, 包括运行于其上的操作系统, 也被称为"中断驱动的"(interrupt-driven).

中断的几个要点:

  • 中断请求来自中断源, 中断源通常是外围硬件. 中断不是"异常", 而是 CPU 正常处理过程一部分.
  • CPU 接受中断请求后将执行该类型中断请求对应的中断服务程序(ISR).
  • CPU 接受中断请求后将进行保留现场操作, 在执行完中断请求后执行恢复现场操作.
  • 中断存在中断优先级的概念, 即当多个中断同时向处理器发出请求时, 处理器将对请求做优先级的排序.
  • 中断可以存在/不存在嵌套, 即当 CPU 正在处理一个请求时接受到更高优先级的中断, CPU 可以选择"中断"当前的中断请求转而去执行更高优先级的中断请求.

LR35902 用于负责处理中断的组件包含 IME 标志, IE 寄存器和 IF 寄存器等, 现介绍如下.

IME 标志, IE 与 IF 寄存器

在前面介绍标准指令集的小节中, 已经了解到有 3 个指令 EI(0xfb), DI(0xf3) 与 RETI(0xd9) 可以分别启用和禁用中断. CPU 通过 IME(interrupt master enable) 标志用来保存中断是否被禁用的信息: EI 指令使 IME 置位, DI 指令使 IME 置零.

CPU 内有两个寄存器参与中断的逻辑控制, 它们分别是 IE 与 IF, 其具体说明如下.

IE

IE(Interrupt Enable)寄存器映射至地址 0xffff, 它同样控制中断的启用或禁用, 但不同的是它只负责某一具体类型的中断, 与 IME 之间的区别类似总控开关与普通开关的区别. Game Boy 总共会产生 5 种不同类型的中断, 它们分别是:

  • V-Blank
  • LCD STAT
  • Timer
  • Serial
  • Joypad

这些中断请求分别来自于不同的外围硬件: GPU, 内部定时器, 串行接口与手柄等. IE 寄存器的低 5 位控制这些中断请求的启用或禁用.

名称说明
0V-BlankInterrupt Enable (INT 40h) (1=Enable)
1LCD STATInterrupt Enable (INT 48h) (1=Enable)
2TimerInterrupt Enable (INT 50h) (1=Enable)
3SerialInterrupt Enable (INT 58h) (1=Enable)
4JoypadInterrupt Enable (INT 60h) (1=Enable)

IF

IF(Interrupt Flag)寄存器映射至地址 0xff0f, 负责保存当前已经产生的中断请求.

名称说明
0V-BlankInterrupt Request (INT 40h) (1=Request)
1LCD STATInterrupt Request (INT 48h) (1=Request)
2TimerInterrupt Request (INT 50h) (1=Request)
3SerialInterrupt Request (INT 58h) (1=Request)
4JoypadInterrupt Request (INT 60h) (1=Request)

当外围硬件的中断信号从低电平变为高电平时, IF 寄存器中的相应位置位. 例如, 当 LCD 控制器进入 V-Blank 周期时, 第 0 位置位.

中断请求与执行

当中断请求发生时, IF 寄存器中的相应位置位. 只有当 IME 标志和 IE 寄存器中的相应位都置位时, 才会发生实际的中断执行, 否则中断请求将"等待"直到 IME 和 IE 都允许其执行.

当中断执行时, IF 寄存器中的相应位将由 CPU 自动复位, 并且 IME 标志变为清零状态(在程序重新启用中断之前, 通常使用 RETI 指令取消任何进一步的中断). CPU 将调用中断请求对应的中断向量(即 0x0040-0x0060 范围内的地址, 如上面的 IE 和 IF 寄存器描述所示). 在以下三种情况下, 可能会发生 IF 寄存器中多个位置位的情况, 即同时请求多个中断:

  • 多个中断信号同时从低电平变为高电平
  • 在一段事件内请求了多个中断, 但 IME/IE 寄存器不允许其立即执行
  • 用户手动向 IF 寄存器写入值

如果 IME 和 IE 允许执行多个请求的中断, 则首先执行具有最高优先级的中断. 优先级按 IE 和 IF 寄存器中的位排序, 具有最高优先级的是位 0(V-Blank), 具有最低优先级的位 4(Joypad).

代码实现

定义一个枚举类型表示 5 种不同类型的中断:

pub enum Flag {
    VBlank  = 0,
    LCDStat = 1,
    Timer   = 2,
    Serial  = 3,
    Joypad  = 4,
}

定义 IF 寄存器如下. 当 IF 寄存器收到不同的中断时, 其特定位可以被置位.

pub struct Intf {
    pub data: u8,
}

impl Intf {
    pub fn power_up() -> Self {
        Self { data: 0x00 }
    }

    pub fn hi(&mut self, flag: Flag) {
        self.data |= 1 << flag as u8;
    }
}

为 CPU 实现中断处理逻辑. 中断处理函数返回 CPU 机器周期, 因为 LR35902 中断处理固定消费 4 个机器周期, 所以中断处理函数的返回值为 0 或 4 之一.

impl Cpu {
    fn hi(&mut self) -> u32 {
        if !self.halted && !self.ei {
            return 0;
        }
        let intf = self.mem.borrow().get(0xff0f);
        let inte = self.mem.borrow().get(0xffff);
        let ii = intf & inte;
        if ii == 0x00 {
            return 0;
        }
        self.halted = false;
        if !self.ei {
            return 0;
        }
        self.ei = false;

        // Consumer an interrupter, the rest is written back to the register
        let n = ii.trailing_zeros();
        let intf = intf & !(1 << n);
        self.mem.borrow_mut().set(0xff0f, intf);

        self.stack_add(self.reg.pc);
        // Set the PC to correspond interrupt process program:
        // V-Blank: 0x40
        // LCD: 0x48
        // TIMER: 0x50
        // JOYPAD: 0x60
        // Serial: 0x58
        self.reg.pc = 0x0040 | ((n as u16) << 3);
        4
    }
}

最后, 每当 CPU 执行指令之前都需要做一次中断判断, 因此在 next 函数内写入中断处理函数如下:

impl Cpu {
    pub fn next(&mut self) -> u32 {
        let mac = {
            let c = self.hi();
            if c != 0 {
                c
            } else if self.halted {
                OP_CYCLES[0]
            } else {
                self.ex()
            }
        };
        mac * 4
    }
}