前言:没有中断的荒原与悬空的钟摆

在现代软件开发中,我们对“时间”的挥霍已经习以为常。想要让方块每隔 0.5 秒下落一格,应用层程序员只需要轻飘飘地调用一句 sleep(500),或者挂载一个高精度的定时器回调函数。操作系统和底层的硬件中断控制器(APIC)会在幕后打点好一切,在时间切片耗尽的刹那,用一发精准的硬件中断信号唤醒你的线程。

然而,当我们按下这台 4-bit 地牢神机的复位键时,映入眼帘的是一片绝对没有中断的底层荒原。

请认清我们此刻面对的冷酷现实:

  • 没有操作系统:没有多线程调度,没有时间片概念,CPU 上电后唯一的宿命就是沿着程序计数器(PC)一往无前地啃食机器码,直到撞上 HALT 指令当场脑死亡。
  • 没有定时器中断:芯片上没有任何一个引脚能定期向 CPU 报警。CPU 根本不知道什么叫“微秒”,什么叫“帧率”,它眼中唯一的时间尺度,只有那颗 32.768 KHz 晶振每秒钟摇摆出的 32768 个机器周期。

如果让 CPU 以全速(Full Speed)去跑方块下落的代码,1KB 的逻辑可能在万分之一秒内就全部执行完毕,方块会在玩家甚至还没来得及眨眼的瞬间,直接贯穿整个屏幕砸向终点。

这就引发了控制流设计中最核心的工程矛盾:我们必须让全速狂飙的硅片电平,去屈尊对齐人类肉眼那极其迟钝的反应时间。

在没有硬件中断、算力极度贫血的地牢里,想拉住这匹脱缰的野马,我们无法依赖任何外部救援。我们唯一的武器,就是指令集里的分支跳转律令(Branching Instructions)。我们需要利用极其简陋的逻辑门,在 1KB 的领土内手搓出一套自我制衡的主游戏循环状态机(Main Game Loop State Machine)。

这是一场精密至极的“赛博木偶戏”。CPU 既是提线的木偶,又是幕后的牵线人。它必须一边疯狂地用空转循环去“谋杀”多余的时钟周期以实现软件延时,一边冷酷地通过标志位反馈去拦截玩家的按键输入。本篇我们将彻底扒开 JMP 与 JZ/JNZ 的底层电路,看看地牢神机是如何在没有中断的死寂世界里,起吊起精准的时间钟摆!

控制流律令档案

1. JMP 指令:空间维度的绝对折跃

  • 注记符:JMP [addr]
  • 硬编码:0x06(对应代码 case 0x06
  • 类型:无条件跳转(Unconditional Branch)
  • 指令周期:4(1周期取码 + 3周期取 12 位绝对地址并强刷给 PC)
  • 硬件解构:它的物理本质是总线对程序计数器(PC)的“粗暴夺舍”。一经译码,总线经理连续跑 3 趟,把 12 位的目标地址直接强行灌进 PC 寄存器的锁存器中。下一周期,CPU 的灵魂将瞬间在物理空间的另一端苏醒。
  • 实战场景(主游戏循环的死结):
GAME_LOOP:
  ; ... [读取按键 -> 方块消行 -> 渲染屏幕] ...
  JMP GAME_LOOP  ; 4周期折跃,构筑起永无止境的赛博游戏轮回

2. JNZ / JZ 指令:基于机器情绪的命运审判

  • 注记符:JNZ [addr] / JZ [addr]
  • 硬编码:0x07 / 0x08
  • 类型:条件跳转(Conditional Branch)
  • 指令周期:吃瓜周期为 2(条件不满足,直接跳过地址);折跃周期为 5(条件满足,吞下 12 位地址强刷 PC)
  • 硬件解构:它是状态寄存器(Flags)中 Zero (Z) 标志位 与 PC 控制线的直接肉搏。如果是 JNZ,硬件逻辑门会去窥探 Z 标志。如果 Z=0(意味着刚才的计算不为零),闸口轰然打开,允许 PC 吞下接下来的 3 个 Nibble 地址完成折跃;否则,CPU 将无情地把地址当成垃圾废料直接推下总线,顺延执行下一条。
  • 实战场景(用空转死磕时间):
; 4-bit 极其绝望的软件延时:用双重嵌套循环“杀死时间”
LDI B, 15        ; 外层循环 15 次
DELAY_OUTER:
  LDI A, 15      ; 内层循环 15 次
DELAY_INNER:
  DEC A          ; A 减 1 触发 ALU 灵魂审判
  JNZ DELAY_INNER; 如果 A 不为 0,给我在内层死等!
  DEC B
  JNZ DELAY_OUTER; 外层接力审判

💻 主状态机的 C++ 映射内幕

 在C++ 模拟器中,最为硬核的“时钟步进与条件拦截”代码。让我们看看在 C++ 里,我们是怎么模拟条件跳转满足与不满足时的“周期差异”:

// 核心解析引擎中的 JNZ 执行片段 (Opcode: 0x07)
case 0x07: { // JNZ [12-bit addr]
    if (!flags.zero) { 
        // 命运交响曲:条件满足!触发 5 周期大折跃
        uint16_t addr_h = fetch_next_nibble();
        uint16_t addr_m = fetch_next_nibble();
        uint16_t addr_l = fetch_next_nibble();
        PC = (addr_h << 8) | (addr_m << 4) | addr_l;
        cycles += 5; // 跑 3 趟总线的尊严税
    } else {
        // 条件不满足:冷酷地“踩碎”接下来的 3 个 Nibble 地址,直接无视
        PC += 3; // 强行跳过后面的地址数据段
        cycles += 2; // 只扣除基础取指和判定周期
    }
    break;
}

大家看这段代码的 else 分支,这就是底层架构最精妙的“无情”。当条件不满足时,留在 ROM 里的那 3 个 Nibble 地址根本不是代码,它们只是被 PC 粗暴碾过去的“数据废料”。条件满足与不满足之间产生的 3 个时钟周期差,就是我们后续计算游戏帧率时最需要抠搜的“时序幽灵”。


 🏁  下期预告——【实战篇:1260 字节的断头台】

手搓完了控制流,我们的机器不仅有了肉体(传输篇),也有了灵魂(控制篇)。接下来,我们要把所有的律令融会贯通,在 1260 字节的 ROM 断头台下,写出真正的《俄罗斯方块》游戏汇编!

Logo

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

更多推荐