前言:历史的记忆与异步的先兆

在经历了解析引擎与总线物流的漫长筑基后,我们的 4-bit 地牢神机在纸面上已经具备了完整的运算与传输能力。但在踏入那片由异步电平统治的“中断荒原”之前,一个更深层的体系结构缺陷将我们拦下:这台机器缺乏记住“微观历史”的能力。

在此之前,程序计数器(PC)的折跃(JMP/JZ/JNZ)是无情且不记来路的。CPU 一旦通过跳转律令跃迁到远方的代码段,就会在物理层面彻底丢失“我从哪里来”的线索。这种短视导致我们无法编写任何可复用的子程序(Subroutine/函数),所有的游戏业务逻辑只能以扁平的形式在 ROM 里一路平铺到底,极易引发控制流的地狱坍塌与空间炸裂。

更致命的是,我们即将引入的硬件中断(Interrupt)是一个完全不可预测的异步随机幽灵。当时钟滴答或玩家按键在主循环执行的任意刹那砸下时,CPU 将面临硬件级的“强行夺舍”。如果处理器没有一种机制能在灾难降临前封印住当前的肉体快照(寄存器与标志位状态),那么中断服务程序(ISR)执行完毕后的返回,将直接演变为一场由于现场被污染而导致的死机惨案。

因此,本期我们将开启一场双维度的整机系统进化:上半场,我们用物理堆栈(Hardware Stack)为控制流的历史建立法律;下半场,我们依托堆栈提供的现场救赎,正式拉开异步中断控制器的序曲。

堆栈立宪——满递减栈的时序与比特肢解

为了让 12 位的程序计数器拥有记忆,我们必须在 4KB 的全局内存地图中,从静态 RAM 区(0x700 - 0x7FF)的最末端倒扣出一块领土,建立满递减(Full-Descending)硬件堆栈。

  • 满(Full):隐藏在 CPU 内部的专用 12 位堆栈指针寄存器(SP),当前所指向的物理格位,永远存放着上一次压入的有效数据。上电复位时,SP 默认顶在 0x7FF
  • 递减(Descending):堆栈的水位线是倒悬的。随着数据的压入,SP 向着小地址方向(内存地牢的更深处)不断坍塌。

1. 核心状态机中的“比特肢解”与缝合(C++ 映射)

在 decodeAndExecute 的源码实现中,我们必须捍卫 4-bit 数据总线的纯正血统。外部 RAM 每一个物理格位只能吃下 4 个 bit,这意味着一个 16 位的 PC(实际有效寻址为 12 位),在执行 CALL 压栈时,必须被硬生生切成 4 个 4-bit 的 Nibble 碎片,分 4 次在总线上跑腿敲入堆栈:

case 0x1B: { // CALL addr (双字节/三字节特权指令)
    uint16_t targetAddr = bus->cpuRead(this->pc++); // 获取跳转目标

    // 将 16 位程序计数器拆解为 4 个 4-bit 物理碎片
    uint8_t pc_3 = (this->pc >> 12) & 0x0F; // 最高 4 位
    uint8_t pc_2 = (this->pc >> 8)  & 0x0F; // 次高 4 位
    uint8_t pc_1 = (this->pc >> 4)  & 0x0F; // 次低 4 位
    uint8_t pc_0 = (this->pc)       & 0x0F; // 最低 4 位

    // 4-bit 总线行为:连续 4 次向下坍塌压栈(先存后减)
    bus->cpuWrite(sp--, pc_3); 
    bus->cpuWrite(sp--, pc_2);
    bus->cpuWrite(sp--, pc_1);
    bus->cpuWrite(sp--, pc_0); 

    this->pc = targetAddr; // 灵魂折跃
    break;
}

而在次级译码扩展区(0x00 套娃区)执行 RET(操作码 0x1C)返回时,由于指针在逆向自增(先加后读),CPU 必须像剥洋葱一样,最先遇到最后压入的最低位(pc_0),并在寄存器内部无缝缝合回原貌:

else if (opcode == 0x1C) { // RET (单字节函数返回)
    // 逆序回弹水位线
    uint16_t pc_0 = bus->cpuRead(++sp) & 0x0F;
    uint16_t pc_1 = bus->cpuRead(++sp) & 0x0F;
    uint16_t pc_2 = bus->cpuRead(++sp) & 0x0F;
    uint16_t pc_3 = bus->cpuRead(++sp) & 0x0F;

    // CPU 内部重塑 16 位控制中枢
    this->pc = (pc_3 << 12) | (pc_2 << 8) | (pc_1 << 4) | pc_0;
    break;
}

中断序曲——多路异步优先级仲裁的硬件功能复现

当我们用 PUSH/POP 和 CALL/RET 彻底稳固了堆栈的水位线后,我们终于有底气去直面那片不可预测的异步荒原。

为了斩断上一期无休止的软件空转延时,我们正式挂载中断控制器(Interrupt Controller)。它在物理上是一个独立于 CPU 核心之外的电平监控芯片,通过位掩码(Bitmask)机制管理多路异步事件,并引入全局中断使能开关(GIE)。

以下是我们在硬件层面实现的优先级译码逻辑:

#include "InterruptController.h"

uint8_t InterruptController::getNextVector() {
    // 检查全局使能开关与待处理中断标志
    if (!globalEnable || pendingInterrupts == 0) {
        return 0; // 无有效异步请求,安全返回
    }

    // 静态优先级仲裁:从低位至高位进行物理扫描
    if (pendingInterrupts & (1 << 0)) {
        // Bit0: 硬件时钟中断(游戏重力的绝对滴答),最高优先级,映射至向量 0x004
        return 0x04; 
    }
    if (pendingInterrupts & (1 << 1)) {
        // Bit1: 外部按键中断(玩家的主动干预),次高优先级,映射至向量 0x008
        return 0x08; 
    }

    return 0; 
}

完美的时序合围:堆栈如何救赎中断

现在,我们可以清晰地看到这台机器进化的终极面貌。当 CPU 在取指周期检测到 hasPending() == true 时,底层的总线控制器将协同发力,上演全场最惊心动魄的“时序合围”:

  1. 强行剥夺控制权:CPU 挂起当前的正常指令流。
  2. 调用堆栈律令:利用我们刚刚在上半场立宪的物理堆栈,将当前的 PC 强行切碎压入 sp--,随后利用 PUSH A 和 PUSH FLAGS 将现场的肉体封印进深渊。
  3. 向量折跃:调用 getNextVector() 拿到优先级最高的电平信号(如 0x04),将 PC 强行指向时钟中断服务程序。
  4. 灵魂复活:中断处理完毕后,通过逆序 POP 恢复现场,最后执行 RETI(或扩展的 RET),让主循环在完全没有觉察到被“篡改”过的情况下,继续丝滑运行。

🏁 工程总结与下期预告

在本期系统篇的连击中,我们不仅在没有堆栈的荒原上起吊起了“满递减栈”的水位线,打破了扁平状态机的短视;更以此为掩体,挂载了具备多路静态仲裁能力的中断控制器。至此,这台 4-bit 机器正式完成了从“微型状态机”向“通用图灵机”的决定性跃升。

  • 下期预告:“双轨异步引擎的点火仪式!下一站【系统篇·下】:我们将正式在 C++ 主程序中模拟晶振引脚的电平跳变,让 InterruptController::request(0) 与主循环轮询疯狂肉搏,看硬件定时器如何真正斩断空转,用异步双轨驱动《俄罗斯方块》的重力奇点!”
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐