执行 EX级 设计

EX级以负责核心运算功能的ALU著称。可以说,它是CPU的灵魂。来一人一句ALU牛逼来。

算数逻辑单元 ALU.sv

ALU需要完成一系列R-Type、I-Type指令的运算。对于L-Type和S-Type,它们的处理逻辑其实差不多,都是相加——这样才能让ALU输出基地址和偏移量之和,算出目标地址。

同样地,使用字符串打印的方式来增强可读性,优化Debug体验,并更好的看到指令在流水线中的流动。同样使用DEBUG宏进行包装,在给综合器生成最终电路前取消宏定义即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
`include "include/defines.svh"

module ALU (
// 操作码
input logic [ 4:0] alu_op,
// 操作数
input logic [31:0] src1,
input logic [31:0] src2,
// 来自ID/EX级的立即数
input logic [31:0] imm,
// 第二操作数选择 0: src2 1: imm
input logic alu_src2_sel,
// 结果输出
output logic [31:0] alu_result,
// 标志位输出
output logic zero,
output logic sign,
output logic alu_unsigned
);
logic [31:0] src2_inner;

assign src2_inner = (alu_src2_sel) ? imm : src2;

// 运算操作
always_comb begin : alu_operation
case (alu_op)
// 基础运算
`ALU_ADD: alu_result = src1 + src2_inner;
`ALU_SUB: alu_result = src1 - src2_inner; // src1 + (~src2 + 1)
`ALU_OR: alu_result = src1 | src2_inner;
`ALU_AND: alu_result = src1 & src2_inner;
`ALU_XOR: alu_result = src1 ^ src2_inner;
`ALU_SLL: alu_result = src1 << src2_inner[4:0];
`ALU_SRL: alu_result = src1 >> src2_inner[4:0];
// 必须显式声明src1为有符号数
`ALU_SRA: alu_result = $signed(src1) >>> src2_inner[4:0];
`ALU_SLT: alu_result = ($signed(src1) < $signed(src2_inner)) ? 1 : 0;
`ALU_SLTU: alu_result = (src1 < src2_inner) ? 1 : 0;
`ALU_RIGHT: alu_result = src2_inner; // 用于AUIPC指令
// L-Type
`ALU_LW: alu_result = src1 + src2_inner; // 地址计算
// [TODO] 非对齐访存
//S-Type
`ALU_SW: alu_result = src1 + src2_inner; // 地址计算
default: alu_result = 0;
endcase
end

// Load/Store指令使用的地址计算不放在ALU内
// 拆分为独立模块 LoadStoreUnit

// 标志位输出
always_comb begin
zero = (alu_result == 32'b0);
sign = alu_result[31];
alu_unsigned = (src1 < src2_inner);
end

`ifdef DEBUG
// 方便调试的 ASCII 指令输出
logic [64-1:0] aluop_ascii;
always_comb begin : aluop_ascii_output
case (alu_op)
`ALU_ADD: aluop_ascii = "AADD";
`ALU_SUB: aluop_ascii = "ASUB";
`ALU_OR: aluop_ascii = "AOR";
`ALU_AND: aluop_ascii = "AAND";
`ALU_XOR: aluop_ascii = "AXOR";
`ALU_SLL: aluop_ascii = "ASLL";
`ALU_SRL: aluop_ascii = "ASRL";
`ALU_SRA: aluop_ascii = "ASRA";
`ALU_SLT: aluop_ascii = "ASLT";
`ALU_SLTU: aluop_ascii = "ASLTU";
`ALU_RIGHT: aluop_ascii = "ARGHT";
`ALU_LW: aluop_ascii = "ALW";
`ALU_SW: aluop_ascii = "ASW";
default: aluop_ascii = "ANOP";
endcase
end
`endif

endmodule

对于许多CPU,会将ALU运算的源操作数选择列为一个单独的二选一MUX。这里直接合并在ALU内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module ALU (
// 操作数
input logic [31:0] src1,
input logic [31:0] src2,
// 来自ID/EX级的立即数
input logic [31:0] imm,
// 第二操作数选择 0: src2 1: imm
input logic alu_src2_sel,
...
);

logic [31:0] src2_inner;
assign src2_inner = (alu_src2_sel) ? imm : src2;
...

endmodule

那为什么后面的MUX不合并呢?这是为了看起来更方便。

回写数据来源选择MUX

这一块实际上没有写成一个单独的模块,而是最终要集成在封装好的CPU模块内。想单独写一个也行,反正最终会被综合成一个多选一MUX。

我们需要选择哪些数据?看之前的 译码级设计 ,看Decoder.sv输出的选择信号wd_sel对应哪些指令:

  • R-Type和I-Type指令的写回数据来源于ALU;
  • B-Type指令不需要选择回写数据,但也得给个默认值;
  • jaljalr两个是跳转指令,需要选择pc+4作为数据写入寄存器;
  • luiauipc这两个很特殊,是立即数扩展,因此使用扩展后从ID级传入的数据;
  • L-Type需要从DRAM中读取数据,因此需要在MEM级选择;
  • 不应该有其余情况,这里写为unique case可以不写default。不过补一个最好。
1
2
3
4
5
6
7
8
9
10
11
12
13
// 回写数据来源选择MUX
// 在EX级完成选择以减少流水线寄存器宽度
// 注意 load 指令的数据在 MEM 级才可用 因此不可能选择 DRAM 作为回写数据来源
always_comb begin : wd_EX_MUX
case (wd_sel_EX)
`WD_SEL_FROM_ALU: rf_wd_EX = alu_result_EX;
// `WD_SEL_FROM_DRAM在MEM级处理
`WD_SEL_FROM_PC4: rf_wd_EX = pc4_EX;
// 如果回写的是立即数扩展值 则需要判断是否为AUIPC指令
`WD_SEL_FROM_IEXT: rf_wd_EX = (is_auipc_EX) ? branch_target_EX : imm_EX;
default: rf_wd_EX = 32'b0;
endcase
end

下一PC计算模块 NextPC_Generator.sv

我们的指令执行完怎么办?取下一个。那下一个从哪里取呢?

不是所有的指令地址都是+4+4变化。对于分支跳转指令等,我们在运算并比较后,要根据结果来进行跳转,而运算完成的最早时间便是EX级。此外,在EX作比较还能利用前向数据通路来加快运算,这是好的。

我们首先要知道,在EX级执行的指令是什么?是分支,还是跳转,或者根本不用去刻意变化pc?我们还需要获取跳转的目标地址,对于jalr还要进行相应的计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
`include "include/defines.svh"

module NextPC_Generator (
input logic is_branch_instr,
input logic [ 2:0] branch_type,
input logic [ 1:0] jump_type,
// ALU计算结果输入
input logic [31:0] alu_result,
// PC加立即数输入
input logic [31:0] branch_target_i,
// ALU标志位输入
input logic alu_zero,
input logic alu_sign,
input logic alu_unsigned,
// PC控制输出
output logic take_branch,
output logic [31:0] branch_target_NextPC

);

// verilog_format:off
always_comb begin
if (jump_type) take_branch = 1'b1;
else if (is_branch_instr) begin
unique case (branch_type)
`BRANCH_BEQ: take_branch = alu_zero;
`BRANCH_BNE: take_branch = ~alu_zero;
`BRANCH_BLT: take_branch = alu_sign;
`BRANCH_BGE: take_branch = ~alu_sign;
`BRANCH_BLTU: take_branch = alu_unsigned;
`BRANCH_BGEU: take_branch = ~alu_unsigned;
default: take_branch = 1'b0;
endcase
end else take_branch = 1'b0;

end

// JALR 需要将最低位置0
assign branch_target_NextPC = (jump_type == `JUMP_JALR) ?
{alu_result[31:1], 1'b0} : branch_target_i;

// verilog_format:on

endmodule

要注意:jalr指令要将最低位归零

因RISC‑V 要求所有指令地址至少要对齐到 16 位(半字,2 字节),而jalr是寄存器间接跳转,rs1 + imm的值可能是奇数,因此需要手动将低位置为0。

EX/MEM级 流水线寄存器 PR_EX_MEM.sv

对于EX向MEM级进发,我们需要传递来自MUX的回写数据wd,以及DRAM的写使能信号dram_we。MEM级的回写数据来源MUX控制信号wd_sel和EX级的是一个,直接复用即可。此外,ALU的计算结果也不能忽略,因为对于L-Type指令,需要作为地址传入MEM级。来自ID/EX级的rD2数据也需要传入,作为DRAM的写入数据。此外,对于S-Type指令,寄存器堆写使能信号wr也需要继续传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
module PR_EX_MEM (
input logic clk,
input logic rst_n,
// EX级输入
input logic [31:0] pc_ex_i,
// EX级输出 给MEM级输入
output logic [31:0] pc_mem_o,
// 判断指令是否有效
input logic instr_valid_ex_i,
output logic instr_valid_mem_o,
// 写使能
input logic dram_we_ex_i,
output logic dram_we_mem_o,
input logic rf_we_ex_i,
output logic rf_we_mem_o,
// 写回数据来源
input logic [ 1:0] wd_sel_ex_i,
output logic [ 1:0] wd_sel_mem_o,
// 写回寄存器地址
input logic [ 4:0] wr_ex_i,
output logic [ 4:0] wr_mem_o,
// ALU计算结果
input logic [31:0] alu_result_ex_i,
output logic [31:0] alu_result_mem_o,
// MUX数据 控制写回数据来源
input logic [31:0] wd_ex_i,
output logic [31:0] wd_mem_o,
// 回写数据 来自ID/EX级
input logic [31:0] rD2_ex_i,
output logic [31:0] rD2_mem_o
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
pc_mem_o <= 32'b0;
instr_valid_mem_o <= 1'b0;
dram_we_mem_o <= 1'b0;
rf_we_mem_o <= 1'b0;
wd_sel_mem_o <= 2'b0;
alu_result_mem_o <= 32'b0;
wd_mem_o <= 32'b0;
rD2_mem_o <= 32'b0;
end else begin
pc_mem_o <= pc_ex_i;
instr_valid_mem_o <= instr_valid_ex_i;
dram_we_mem_o <= dram_we_ex_i;
rf_we_mem_o <= rf_we_ex_i;
wd_sel_mem_o <= wd_sel_ex_i;
alu_result_mem_o <= alu_result_ex_i;
wd_mem_o <= wd_ex_i;
rD2_mem_o <= rD2_ex_i;
end
end

// 写回寄存器地址
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_mem_o <= 5'b0;
end else begin
wr_mem_o <= wr_ex_i;
end
end

endmodule

写到这里,已经完成30%了,可喜可贺!

为什么已经写了三级,才完成30%?

因为大的在后面呢。