让我访问!

对于L-Type和S-Type,我们只支持了lw/sw,它们的兄弟姐妹lb/lbu/lh/lhu/sb/sh还没支持。

我们需要准备一个模块专门用来处理这些非字节存取。在前面ID级传出的sl_type信号终于能派上用场了。

这里有个小技巧:定义存取类型时,用高位来分辨是读还是写,剩下的读写类型一定程度上可以合并。

存取控制模块 LoadStoreUnit.sv

考虑到数据的存取都在MEM级完成,我们需要将模块放置在MEM级。我们要传入来自EX级的待存数据,对其进行适当移位,好将数据存放到合适位置,并根据dram_wesl_type来将1位写使能dram_we转换为4位的按位写使能dram_we_strbe

对于读出的数据,还要判断是有符号还是无符号读取。

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
module LoadStoreUnit (
input logic [ 3:0] sl_type,
input logic [31:0] addr,
input logic [31:0] load_data_i,
output logic [31:0] load_data_o,
input logic [31:0] store_data_i,
output logic [31:0] store_data_o,
input logic dram_we,
output logic [ 3:0] wstrb // 按位写使能
);
// [TODO] 不同的访存类型处理
logic is_load, is_load_unsigned;
always_comb begin
is_load = (sl_type[3] == 1'b0);
is_load_unsigned = (sl_type[2] == 1'b1);
end

always_comb begin
// 默认值,保证所有组合输出都有驱动,避免锁存
wstrb = 4'b0000;
load_data_o = 32'b0;
store_data_o = 32'b0;

if (is_load) begin
// 临时变量(也可以放在模块层)
logic [31:0] raw;
raw = 32'b0;
// 提取 raw,根据 sl_type 和 addr 偏移
case (sl_type[1:0])
2'b01: begin // byte
// 右移到最低位再掩码
raw = (load_data_i >> (addr[1:0] * 8)) & 32'h000000FF;
end
2'b10: begin // half
raw = (load_data_i >> (addr[1] * 16)) & 32'h0000FFFF;
end
2'b11: begin // word
raw = load_data_i;
end
default: raw = 32'b0;
endcase

// 扩展
if (is_load_unsigned) begin
load_data_o = raw; // 零扩展
end else begin
case (sl_type[1:0])
2'b01: load_data_o = {{24{raw[7]}}, raw[7:0]}; // LB
2'b10: load_data_o = {{16{raw[15]}}, raw[15:0]}; // LH
2'b11: load_data_o = raw; // LW
default: load_data_o = 32'b0;
endcase
end

end else if (dram_we) begin
logic [15:0] half_byte;
logic [ 4:0] shift; // 移位量 0, 8, 16, 24
shift = 4'b0;
unique case (sl_type[1:0])
2'b01: begin // SB
logic [7:0] byte_val;

half_byte = store_data_i[7:0]; // 只关心最低 8 位
shift = {addr[1:0], 3'b000}; // addr[1:0] * 8

// 写使能
unique case (addr[1:0])
2'b00: wstrb = 4'b0001;
2'b01: wstrb = 4'b0010;
2'b10: wstrb = 4'b0100;
2'b11: wstrb = 4'b1000;
default: wstrb = 4'b0000;
endcase

// 把 byte_val 放到对应 byte lane
store_data_o = ({24'b0, half_byte[7:0]} << shift);
end

2'b10: begin // SH
logic [15:0] half_val;

half_byte = store_data_i[15:0]; // 只关心最低 16 位
shift = {addr[1], 4'b0000}; // addr[1] ? 16 : 0

// 写使能
unique case (addr[1])
1'b0: wstrb = 4'b0011;
1'b1: wstrb = 4'b1100;
default: wstrb = 4'b0000;
endcase

// 把 half_val 放到低/高 halfword
store_data_o = ({16'b0, half_byte} << shift);
end

2'b11: begin // SW
wstrb = 4'b1111;
store_data_o = store_data_i; // 直接写整个 word
half_byte = 16'b0;
end

default: begin
wstrb = 4'b0000;
store_data_o = 32'b0;
half_byte = 16'b0;
end
endcase
end else begin
// 非 load/非 store:保持默认 wstrb=0, store_data_o 已初始化为 0
end
end

`ifdef DEBUG
logic [31:0] sl_type_ascii;
logic [ 1:0] select_bits;
assign select_bits = addr[1:0];
always_comb begin
case (sl_type)
`MEM_LB: sl_type_ascii = "LB ";
`MEM_LH: sl_type_ascii = "LH ";
`MEM_LW: sl_type_ascii = "LW ";
`MEM_LBU: sl_type_ascii = "LBU ";
`MEM_LHU: sl_type_ascii = "LHU ";
`MEM_SB: sl_type_ascii = "SB ";
`MEM_SH: sl_type_ascii = "SH ";
`MEM_SW: sl_type_ascii = "SW ";
default: sl_type_ascii = "UNKN";
endcase
end
`endif

endmodule

之后更新顶层模块,远方的MUX也不要放过:

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
// DRAM模块
logic [31:0] DRAM_output_data,DRAM_input_data;
logic [3:0] dram_we_MEM_strbe;
logic [3:0] wstrb;
DRAM u_DRAM (
.clk(clk),
.a (alu_result_MEM[17:2]), // 字节地址转换为字地址 (除以4)
.spo(DRAM_output_data),
.we (wstrb),
.din(DRAM_input_data)
);

// 对于同步DRAM,数据将在下一个时钟周期可用
// 将数据传递到WB级,在WB级进行选择
assign rf_wd_MEM = rf_wd_MEM_PR2MUX;

logic [31:0] load_data_o; // LSU输出的最终加载数据
LoadStoreUnit u_LoadStoreUnit(
.sl_type (sl_type_MEM),
.addr (alu_result_MEM),
.load_data_i (DRAM_output_data),
.load_data_o (load_data_o),
.store_data_i (rf_rd2_MEM),
.store_data_o (DRAM_input_data),
.dram_we (dram_we_MEM),
.wstrb (wstrb)
);

assign rf_wd_WB = (wd_sel_WB == `WD_SEL_FROM_DRAM) ? load_data_o : rf_wd_WB_from_PR;

我们的LoadStoreUnit组合逻辑模块,因此,不会打乱同步读写的DRAM时序。让我们测试一下看看:

1
2
3
4
5
6
7
8
9
li  x1,0x12345678

sw x1,0(x0)
lw x2,0(x0)
lw x3,0(x0)
lh x4,4(x0)
lhu x5,4(x0)
lb x6,0(x0)
lhu x7,2(x0)

lw指令读出错误

可以看到,本应该执行lw的指令却错误地读出了下一条的lh指令,而最后一条lhu直接消失了!这是为什么?

看上面的波形可以知道,用于控制存入数据的sl_type提早了一拍到达。组合逻辑不会影响时序,因此,应当根据时序来设计组合逻辑。我们是在MEM级读取出数据并进行处理的,但是写回阶段是在WB级——这导致我们写回时使用的仍然是MEM级的控制信号。DRAM_output_data在 WB 级可用时,应当使用和 DRAM 读取请求相对应的sl_type进行处理,但当前代码中LoadStoreUnit使用的是sl_type_MEM,而此时MEM级可能已经是下一条指令了。换句话说,“存”与“取”所在的流水级不同,导致“存”正常,“取”异常

我们的LoadStoreUnit模块也成了第二个跨流水级的模块。如何修改呢?最简单的方法,就是直接实例化两个一模一样的模块:

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
// [INFO] LoadStoreUnit模块 - MEM级只处理Store操作
logic [31:0] DRAM_input_data; // LSU处理后数据
logic [31:0] DRAM_output_data; // LSU从DRAM得到的数据
logic [3:0] dram_we_MEM_strbe;
// MEM级LSU仅用于Store处理,Load在WB级处理
LoadStoreUnit u_LoadStoreUnit_MEM(
.sl_type (sl_type_MEM),
.addr (alu_result_MEM),
.load_data_i (32'b0), // MEM级不使用load处理
.load_data_o (), // MEM级不使用load输出
.store_data_i (rf_rd2_MEM),
.store_data_o (DRAM_input_data),
.dram_we (dram_we_MEM),
.wstrb (dram_we_MEM_strbe)
);

// DRAM模块
DRAM #(
.ADDR_WIDTH(15)
) u_DRAM (
.clk(clk),
.a (alu_result_MEM[17:2]), // 字节地址转换为字地址
.spo(DRAM_output_data),
// .we ({4{dram_we_MEM}}),
.we (dram_we_MEM_strbe),
.din(DRAM_input_data)
);

// [INFO] WB级LoadStoreUnit - 专门处理Load操作
// 使用WB级的sl_type和地址来正确处理DRAM读取的数据
logic [31:0] load_data_WB;
LoadStoreUnit u_LoadStoreUnit_WB(
.sl_type (sl_type_WB),
.addr (alu_result_WB),
.load_data_i (DRAM_output_data), // DRAM的spo在WB级稳定可用
.load_data_o (load_data_WB),
.store_data_i (32'b0), // WB级不使用store处理
.store_data_o (),
.dram_we (1'b0), // WB级不写DRAM
.wstrb ()
);

我们再次进行测试:

修改后的LSU正常工作

全自动化测试:解放双手

到这里,我们的RV32I CPU 算是彻底完成了。但即使有前面的测试,我们仍不能确保我们的CPU万无一失。有什么办法可以证明它一定是符合RV32I标准的CPU呢?最好的办法,自然是使用形式化验证,但那个太烦了,而且我不会。使用官方的测试样例进行测试算是最容易的方法了。riscv-software-src设置了一个测试库,专门用于测试各种架构的RV32 CPU是否符合标准,在riscv-software-src/riscv-tests下。使用官方工具链编译时,必须带上_Zicsr扩展,这是因为其测试指令和初始化指令中用到了CSR寄存器,不过即使不支持也没关系。

在测试前,还需要修改一下DRAM。lw测试子集会直接从DRAM中读取值,而我们默认是将DRAM全部初始化为32'h00000000。考虑到之前已在IROM模块中指定了内存读取范围$readmemh(testcase, rom_data, 0, (1 << ADDR_WIDTH) - 1),这里在DRAM中可不指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
logic [31:0] ram_data[1 << ADDR_WIDTH];
// 初始化
initial begin
integer i;
string testcase;

// 初始化所有内存为0
for (i = 0; i < 1 << ADDR_WIDTH; i = i + 1) begin
ram_data[i] = 32'h00000000;
end

// 如果有testcase参数,从hex文件加载数据段
// hex文件是完整的内存镜像,按字地址索引
// 数据段从0x2000开始,即hex文件的第0x800行(2048)
if ($value$plusargs("TESTCASE=%s", testcase)) begin
// 读取整个hex文件到DRAM
// SystemVerilog的$readmemh会自动处理地址映射
// 但是最好还是手动指定范围以防万一
$readmemh(testcase, ram_data);
$display("DRAM: Loaded memory image from %s", testcase);
end
end

写个简单的Makefile进行测试,结果如下:

完美的测试结果!

后记

我们做完了吗?做完了99%。

后面,可能会首先完成Zicsr扩展与M扩展,再之后会往自己的项目上靠,研究一下最新的控制流加密扩展ZicfilpZicfissACF扩展暂不考虑。