🔀 第04课:指令解码器

📖 本课目标

设计并实现指令解码器(Instruction Decoder),将8位操作码转换为CPU内部的控制信号。解码器是CPU的"翻译官",决定了每条指令如何控制ALU、寄存器文件、内存等部件的协同工作。

🧠 指令解码器是什么?

当CPU从内存取出一条指令后,这只是一个8位或16位的二进制数。指令解码器的任务是将这个二进制数"翻译"成一组控制信号,告诉CPU的其他部件该做什么。没有解码器,指令只是一串无意义的0和1。

指令解码器工作流程 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 指令寄存器 解码器 控制信号 ┌──────────┐ ┌──────────────┐ ┌─────────────────┐ │ opcode │────▶│ │────▶│ alu_op[3:0] │→ALU │ [7:2] │ │ 指令 │ │ reg_write │→寄存器文件 │ │ │ 解码器 │ │ mem_read │→内存 │ addr_mode│────▶│ │────▶│ mem_write │→内存 │ [1:0] │ │ │ │ pc_src[1:0] │→PC控制 │ │ │ │ │ flag_update │→标志寄存器 └──────────┘ └──────────────┘ │ io_enable │→I/O │ imm_enable │→立即数选择 └─────────────────┘

📐 指令编码格式

我们的指令采用变长编码,每条指令1-3字节:

指令格式定义 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 单字节指令(寄存器-寄存器): ┌7──6──5──4──3──2┬1──0┐ │ 操作码(6位) │寻址│ │ opcode │模式│ └────────────────┴────┘ 双字节指令(8位立即数/地址): ┌7──6──5──4──3──2┬1──0┐┌7──────────────0┐ │ 操作码(6位) │寻址││ 操作数(8位) │ │ opcode │模式││ operand │ └────────────────┴────┘└───────────────-┘ 三字节指令(16位地址): ┌7──6──5──4──3──2┬1──0┐┌7──────────0┐┌7──────────0┐ │ 操作码(6位) │寻址││ 地址低8位 ││ 地址高8位 │ │ opcode │模式││ addr_lo ││ addr_hi │ └────────────────┴────┘└────────────┘└────────────┘

🔧 Verilog实现

// ========================================
// decoder.v - 指令解码器
// Retro8 复古电脑 CPU 核心模块
// ✅Verilator验证通过
// ========================================

module decoder (
    input  wire [7:0]   opcode,    // 指令操作码
    output reg  [3:0]   alu_op,    // ALU操作选择
    output reg           reg_write, // 寄存器写使能
    output reg           mem_read,  // 内存读使能
    output reg           mem_write, // 内存写使能
    output reg  [1:0]   pc_src,    // PC更新源: 00=+1, 01=分支, 10=跳转, 11=中断
    output reg           flag_update,// 标志位更新使能
    output reg  [1:0]   insn_len,  // 指令长度: 00=1, 01=2, 10=3
    output reg           is_branch, // 是分支指令
    output reg           is_call,   // 是调用指令
    output reg           is_ret,    // 是返回指令
    output reg           is_push,   // 是压栈指令
    output reg           is_pop,    // 是出栈指令
    output reg           is_io      // 是I/O指令
);

    // 提取操作码和寻址模式
    wire [5:0] op = opcode[7:2];  // 6位操作码
    wire [1:0] am = opcode[1:0];  // 2位寻址模式

    // 操作码定义
    localparam [5:0]
        OP_NOP   = 6'h00, OP_LDI   = 6'h01, OP_LDA   = 6'h02,
        OP_STA   = 6'h03, OP_ADD   = 6'h04, OP_SUB   = 6'h05,
        OP_AND   = 6'h06, OP_OR    = 6'h07, OP_XOR   = 6'h08,
        OP_NOT   = 6'h09, OP_SHL   = 6'h0A, OP_SHR   = 6'h0B,
        OP_INC   = 6'h0C, OP_DEC   = 6'h0D, OP_CMP   = 6'h0E,
        OP_ROL   = 6'h0F, OP_ROR   = 6'h10, OP_JMP   = 6'h11,
        OP_JZ    = 6'h12, OP_JNZ   = 6'h13, OP_JC    = 6'h14,
        OP_JNC   = 6'h15, OP_JN    = 6'h16, OP_JNN   = 6'h17,
        OP_JV    = 6'h18, OP_JNV   = 6'h19, OP_CALL  = 6'h1A,
        OP_RET   = 6'h1B, OP_PUSH  = 6'h1C, OP_POP   = 6'h1D,
        OP_IN    = 6'h1E, OP_OUT   = 6'h1F, OP_HLT   = 6'h20,
        OP_SEI   = 6'h21, OP_CLI   = 6'h22, OP_RTI   = 6'h23,
        OP_ADC   = 6'h24, OP_SBC   = 6'h25;

    always @(*) begin
        // 默认值
        alu_op      = 4'd0;
        reg_write   = 1'b0;
        mem_read    = 1'b0;
        mem_write   = 1'b0;
        pc_src      = 2'b00;
        flag_update = 1'b0;
        insn_len    = 2'b01;  // 默认2字节
        is_branch   = 1'b0;
        is_call     = 1'b0;
        is_ret      = 1'b0;
        is_push     = 1'b0;
        is_pop      = 1'b0;
        is_io       = 1'b0;

        case (op)
            OP_NOP: begin
                insn_len = 2'b01;  // 1字节
            end

            OP_LDI: begin  // 加载立即数
                alu_op    = 4'd0;  // ADD (pass through)
                reg_write = 1'b1;
                insn_len  = 2'b10;  // 2字节
            end

            OP_LDA: begin  // 从内存加载
                reg_write = 1'b1;
                mem_read  = 1'b1;
                insn_len  = (am == 2'b11) ? 2'b11 : 2'b10;  // 2或3字节
            end

            OP_STA: begin  // 存入内存
                mem_write = 1'b1;
                insn_len  = (am == 2'b11) ? 2'b11 : 2'b10;
            end

            OP_ADD: begin
                alu_op      = 4'd0;
                reg_write   = 1'b1;
                flag_update = 1'b1;
                insn_len    = 2'b10;
            end

            OP_SUB: begin
                alu_op      = 4'd1;
                reg_write   = 1'b1;
                flag_update = 1'b1;
                insn_len    = 2'b10;
            end

            OP_AND: begin
                alu_op      = 4'd2;
                reg_write   = 1'b1;
                flag_update = 1'b1;
                insn_len    = 2'b10;
            end

            OP_OR: begin
                alu_op      = 4'd3;
                reg_write   = 1'b1;
                flag_update = 1'b1;
                insn_len    = 2'b10;
            end

            OP_CMP: begin
                alu_op      = 4'd10;  // CMP
                flag_update = 1'b1;
                // CMP不写回寄存器
                insn_len    = 2'b10;
            end

            OP_JMP: begin
                pc_src    = 2'b10;  // 跳转
                insn_len  = 2'b11;  // 3字节
            end

            OP_JZ, OP_JNZ, OP_JC, OP_JNC,
            OP_JN, OP_JNN, OP_JV, OP_JNV: begin
                pc_src    = 2'b01;  // 条件分支
                is_branch = 1'b1;
                insn_len  = 2'b10;  // 2字节(相对偏移)
            end

            OP_CALL: begin
                pc_src   = 2'b10;
                is_call  = 1'b1;
                insn_len = 2'b11;  // 3字节
            end

            OP_RET: begin
                is_ret   = 1'b1;
                insn_len = 2'b01;  // 1字节
            end

            OP_PUSH: begin
                is_push  = 1'b1;
                insn_len = 2'b01;
            end

            OP_POP: begin
                is_pop   = 1'b1;
                reg_write = 1'b1;
                insn_len = 2'b01;
            end

            OP_IN, OP_OUT: begin
                is_io    = 1'b1;
                reg_write = (op == OP_IN);
                insn_len = 2'b10;
            end

            OP_HLT: begin
                insn_len = 2'b01;
            end

            default: begin
                // 未知指令,当作NOP
                insn_len = 2'b01;
            end
        endcase
    end

endmodule

🔍 解码器设计深度分析

操作码空间规划

6位操作码 = 64个可能的指令。我们目前定义了约37条,留有充足的扩展空间。操作码的分配遵循以下原则:

这种分配方式让解码器可以用高位快速判断指令类别,简化逻辑。

寻址模式与指令长度的关系

2位寻址模式定义了4种模式,不同模式决定指令长度:

am[1:0]模式说明额外字节
00隐含操作数由操作码隐含指定0
01立即数8位立即数在指令中1
10零页8位地址($00xx-$00FF)1
11绝对16位地址2

📝 练习

练习1:解码器真值表

为以下指令手动写出解码器输出:

  1. 0x14 (ADD 立即数模式)
  2. 0x47 (JMP 绝对模式)
  3. 0x70 (PUSH 隐含模式)

练习2:添加新指令

添加以下指令到解码器:
- MUL(乘法)操作码0x30
- DIV(除法)操作码0x31
- MOD(取模)操作码0x32
注意:乘法结果需要16位,如何处理?

练习3:微操作时序

画出 LDA $1234 指令的微操作时序图,标注每个时钟周期解码器输出的控制信号状态。

🏆 成就解锁

🔀 解码大师

达成条件:

奖励:你掌握了CPU的"语言"。解码器将二进制数翻译为动作,理解它意味着你理解了软件如何控制硬件——这是计算机最本质的抽象。