设计并实现指令解码器(Instruction Decoder),将8位操作码转换为CPU内部的控制信号。解码器是CPU的"翻译官",决定了每条指令如何控制ALU、寄存器文件、内存等部件的协同工作。
当CPU从内存取出一条指令后,这只是一个8位或16位的二进制数。指令解码器的任务是将这个二进制数"翻译"成一组控制信号,告诉CPU的其他部件该做什么。没有解码器,指令只是一串无意义的0和1。
我们的指令采用变长编码,每条指令1-3字节:
// ========================================
// 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 |
为以下指令手动写出解码器输出:
0x14 (ADD 立即数模式)0x47 (JMP 绝对模式)0x70 (PUSH 隐含模式)添加以下指令到解码器:
- MUL(乘法)操作码0x30
- DIV(除法)操作码0x31
- MOD(取模)操作码0x32
注意:乘法结果需要16位,如何处理?
画出 LDA $1234 指令的微操作时序图,标注每个时钟周期解码器输出的控制信号状态。
达成条件:
奖励:你掌握了CPU的"语言"。解码器将二进制数翻译为动作,理解它意味着你理解了软件如何控制硬件——这是计算机最本质的抽象。