🔀 第11课:分支与跳转

📖 本课目标

深入实现分支与跳转指令:JMP、JZ/JNZ、JC/JNC、JN/JNN、CALL、RET。这些指令控制程序的执行流——从简单的条件判断到子程序调用,是结构化编程的基础。

🧠 控制流指令分类

控制流指令体系 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 无条件跳转 条件分支 ┌──────────────┐ ┌──────────────┐ │ JMP addr │ │ JZ offset │ Z=1 │ PC ← addr │ │ JNZ offset │ Z=0 │ 3字节,绝对 │ │ JC offset │ C=1 └──────────────┘ │ JNC offset │ C=0 │ JN offset │ N=1 子程序调用 │ JNN offset │ N=0 ┌──────────────┐ └──────────────┘ │ CALL addr │ 2字节,相对偏移 │ PUSH PC │ -128 ~ +127 │ PC ← addr │ └──────────────┘ 中断返回 ┌──────────────┐ 子程序返回 │ RTI │ ┌──────────────┐ │ POP PC │ │ RET │ │ POP FLAGS │ │ PC ← POP │ └──────────────┘ └──────────────┘

🔧 条件分支的实现

条件分支的关键是正确判断标志位。CMP指令设置标志位,然后条件分支根据标志位决定是否跳转:

// CMP R1, R2 的标志位含义
//
// CMP做减法 R1 - R2,但不存结果
//
// R1 == R2  → Z=1  → JZ跳转
// R1 != R2  → Z=0  → JNZ跳转
// R1 > R2   → Z=0, C=0 (无符号) 或 Z=0, N==V (有符号)
// R1 < R2   → C=1 (无符号) 或 N!=V (有符号)

// 示例:if (R1 >= 10) goto label
LDI  R2, #10
CMP  R1, R2
JNN  label     // N=0 说明 R1 >= R2 (有符号)

// 示例:if (R1 < 100) goto label (无符号比较)
LDI  R2, #100
CMP  R1, R2
JC   label     // C=1 说明 R1 < R2 (无符号,借位)

🔍 子程序调用机制

CALL/RET与堆栈

CALL指令将返回地址(PC+3,因为CALL是3字节指令)压入堆栈,然后跳转到目标地址。RET从堆栈弹出返回地址到PC。这就是C语言函数调用的底层实现。

CALL $9000 执行过程(假设PC=$8005) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 执行前: 执行后: PC = $8005 PC = $9000 SP = $7E SP = $7C 堆栈: 堆栈: $7D: ?? $7D: $80 ← 返回地址高位 $7E: ?? $7E: $08 ← 返回地址低位 $7F: ?? $7F: ?? 步骤: 1. SP--, [SP] ← $08 (地址高位) 2. SP--, [SP] ← $08 (地址低位) 返回地址 = PC + 3 = $8005 + 3 = $8008 3. PC ← $9000

嵌套调用与递归

因为每次CALL都把返回地址压栈,RET都弹出,所以自然支持嵌套调用和递归:

// 嵌套调用示例
// main() → func_a() → func_b()

main:
    CALL func_a
    HLT

func_a:
    CALL func_b
    RET

func_b:
    // 做些事情
    RET

// 堆栈变化:
// CALL func_a:  PUSH main_ret, PC=func_a
// CALL func_b:  PUSH func_a_ret, PC=func_b
// RET (func_b): POP → PC=func_a_ret
// RET (func_a): POP → PC=main_ret

📝 练习

练习1:条件分支编码

写出以下高级代码的Retro8汇编:

if (x > 10) {
    y = 1;
} else {
    y = 0;
}

假设x在R1,y在R2。

练习2:实现循环

用条件分支实现 for(i=0; i<100; i++) { sum += i; }

假设sum在R1,i在R2。计算偏移量。

练习3:开关语句

用跳转表实现switch-case语句:根据R1的值(0-3)跳转到不同的处理代码。设计跳转表的数据结构和访问方式。

🏆 成就解锁

🔀 控制流大师

达成条件:

奖励:你的CPU现在能做判断和选择了!if/else、for/while、函数调用——所有程序结构的基础都已就绪。

🔬 高级控制流模式

除了基本的if/else和for/while,还有几种常见的控制流模式:

状态机实现

状态机是嵌入式编程的核心模式:

// 简单的状态机:LED闪烁控制器
// 状态0: LED灭,等待1秒
// 状态1: LED亮,等待0.5秒

STATE0:  // LED灭
    ANDI R1, #0xFE   // LED off (bit0=0)
    STA  R1, $FF07
    LDI  R2, #100    // 1秒(10ms×100)
WAIT0:
    LDI  R3, #10     // 10ms延时
    CALL delay_ms
    DEC  R2
    JNZ  WAIT0
    JMP  STATE1

STATE1:  // LED亮
    ORI  R1, #0x01   // LED on (bit0=1)
    STA  R1, $FF07
    LDI  R2, #50     // 0.5秒
WAIT1:
    LDI  R3, #10
    CALL delay_ms
    DEC  R2
    JNZ  WAIT1
    JMP  STATE0

协作式多任务

用状态机模拟多任务——每个"任务"是一个状态,每个循环执行一步:

// 3个协作任务的调度器
MAIN_LOOP:
    CALL task_led      // 任务1: LED闪烁
    CALL task_key      // 任务2: 键盘扫描
    CALL task_uart     // 任务3: 串口通信
    JMP  MAIN_LOOP     // 循环调度

// 每个任务内部用状态变量记录进度
// 每次被调用时执行一步,然后返回
task_led:
    LDA  R1, led_state
    CMP  R1, #0
    JZ   led_off_state
    // ... LED on state
    RET
led_off_state:
    // ... LED off state
    RET

📐 分支指令编码详解

相对分支偏移计算器

条件分支使用8位有符号偏移。偏移的计算公式:

偏移 = 目标地址 - (分支指令地址 + 指令长度)

例子:从$8010跳转到$8005
  偏移 = $8005 - ($8010 + 2) = $8005 - $8012 = -13
  -13的8位补码 = 0xF3

验证:PC = $8012 + 0xFFF3 = $8012 - 13 = $8005 ✅

例子:从$8020跳转到$8030
  偏移 = $8030 - ($8020 + 2) = $8030 - $8022 = +14
  14 = 0x0E

注意:偏移范围 = -128 ~ +127
  如果跳转距离超过这个范围,需要用JMP替代

📐 结构化编程的底层实现

if-else 的多种实现

同一个if-else可以有多种汇编实现,效率各不同:

// 方案1: 条件跳过then块(适合短then)
// if (R1 == 0) then_block
    JNZ  skip_then
    // then块
skip_then:

// 方案2: 条件跳到else(标准方式)
// if (R1 > 10) { then } else { else }
    LDI  R2, #10
    CMP  R1, R2
    JNN  else_block    // R1 <= 10 → else
    // then块 (R1 > 10)
    JMP  end_if
else_block:
    // else块
end_if:

// 方案3: 位测试(检查单个位)
// if (R1 的 bit3 为1)
    ANDI R1, #0x08
    JZ   bit3_is_0
    // bit3 = 1
    JMP  done
bit3_is_0:
    // bit3 = 0
done:

switch-case的跳转表实现

当case数量多且值连续时,跳转表比一连串CMP+JZ更高效:

// switch(R1) { case 0:... case 1:... case 2:... }
// 跳转表存放在000
// 每个表项是2字节地址

    // 边界检查
    CMP  R1, #3
    JNN  default_case
    
    // 计算跳转表偏移: R1 * 2
    MOV  R3, R1       // X = index
    SHL  R1           // R1 = index * 2
    MOV  R3, R1       // X = offset
    
    // 从跳转表读取目标地址
    LDA  R2, 000,X  // 地址低8位
    INC  R3
    LDA  R1, 000,X  // 地址高8位
    // JMP {R1,R2} (间接跳转)

🔬 循环优化技巧

循环展开

减少循环次数,每次迭代多做几次操作:

// 原始循环:每次处理1个元素
// for(i=0; i<100; i++) sum += array[i]
LDI R3, #0
LDI R4, #100
LOOP1:
    LDA  R2, 000,X
    ADD  R1, R2
    INC  R3
    CMP  R3, R4
    JNZ  LOOP1
// 每个元素:5条指令 + 分支开销

// 展开4倍:每次处理4个元素
// 100/4 = 25次迭代
LDI R3, #0
LDI R4, #25
LOOP4:
    LDA  R2, 000,X  // 元素0
    ADD  R1, R2
    INC  R3
    LDA  R2, 000,X  // 元素1
    ADD  R1, R2
    INC  R3
    LDA  R2, 000,X  // 元素2
    ADD  R1, R2
    INC  R3
    LDA  R2, 000,X  // 元素3
    ADD  R1, R2
    INC  R3
    DEC  R4
    JNZ  LOOP4
// 减少了75%的分支判断开销

倒计数循环

与0比较不需要CMP指令(可以用DEC+JNZ直接判断零标志),所以倒计数比正计数更高效:

// 正计数:需要CMP
    LDI  R2, #0
loop:
    // ... 循环体 ...
    INC  R2
    LDI  R3, #100
    CMP  R2, R3      // 需要CMP
    JNZ  loop

// 倒计数:DEC自动设置Z标志
    LDI  R2, #100
loop:
    // ... 循环体 ...
    DEC  R2           // DEC自动设置Z
    JNZ  loop         // 不需要CMP!省2字节

📊 控制流指令周期数

指令字节周期说明
JMP addr33无条件跳转
JZ offset22-3条件分支:taken=3, not taken=2
CALL addr35压2字节+跳转
RET14弹2字节+跳转
RTI16弹3字节+跳转

注意:条件分支taken比not taken多1个周期(需要计算偏移地址)。

📐 延时循环设计

在8位系统中,精确延时是常见需求——等待I/O设备、产生音频频率、控制动画速度:

// 精确延时R2×10ms
// 假设CPU频率10MHz,每条指令平均3周期
delay_ms:
    PUSH R3
    PUSH R4
outer:
    LDI  R3, #200     // 外层循环计数
inner:
    LDI  R4, #166     // 内层循环计数(调整得到精确延时)
wait:
    DEC  R4
    JNZ  wait         // 2周期×166 = 332周期
    DEC  R3
    JNZ  inner        // 332×200 = 66400周期 ≈ 6.64ms
    DEC  R2
    JNZ  outer
    POP  R4
    POP  R3
    RET
💡 精确计时:在真实硬件上,用定时器中断比忙等延时更可靠。但忙等延时在简单场景(如初始化等待)中仍然有用。Verilator仿真中时间概念不同,延时循环不会产生真实的等待时间。