访存 MEM级 设计

访存访存,顾名思义,就是要去访问内存。这个内存是“数据内存”,因为我们写的CPU采用的是哈佛架构,指令内存和数据内存分离。

数据内存 DRAM.sv

首先登场的是数据内存。非常简单,实例化一堆RDFF即可。为什么是RDFF?为了便于测验交作业,我们暂时把DRAM写成 “同步写、异步读” 的。

几乎所有厂商都不支持“同步写、异步读”的DRAM,综合器也无法将其综合为DRAM。对于Xilinx的IP核,其BRAM是同步读写的,有的甚至需要两拍。

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
`timescale 1ns / 1ps

// DRAM 行为模型 - 用于仿真
// 替代 Xilinx IP 核
// [HACK] Vivado无法综合异步读的RAM
module DRAM (
input logic clk, // 时钟
input logic [15:0] a, // 地址输入 (16位 = 64K words)
output logic [31:0] spo, // 数据输出
input logic we, // 写使能
input logic [31:0] din // 数据输入
);

// 数据存储器 - 16K x 32bit
reg [31:0] ram_data[65536];

// 初始化
initial begin
integer i;
reg [256*8-1:0] ram_file; // 字符串缓冲区

for (i = 0; i < 65536; i = i + 1) begin
ram_data[i] = 32'h00000000;
end

if (1) begin
rom_file =
// "user/data/hex/sw_lw.hex"
"user/data/hex/addi.hex"
;
$readmemh(rom_file, rom_data, 0, 16383);
$display("IROM: Loaded instructions from %s", rom_file);
end

end

// 写操作 (同步)
always_ff @(posedge clk) begin
if (we) begin
ram_data[a] <= din;
end
end

// [HACK] 异步读
// assign spo = ram_data[a];
// 确保在写操作时读出未知值
assign spo = we ? 'x : ram_data[a];

endmodule

DRAM数据选择MUX

来自EX级的数据只有来自ID级的rD2。也不容易了,一路走到这里。

1
2
3
4
// 回写数据来源2选择MUX
// 选择是否为DRAM数据
// 对于同步DRAM 将MUX放到WB级
assign rf_wd_MEM = (wd_sel_MEM == `WD_SEL_FROM_DRAM) ? DRAM_output_data : rf_wd_MEM_PR2MUX;

MEM/WB级 流水线寄存器 PR_MEM_WB.sv

已经到最后一级了,信号也被分流的差不多了。

我们需要将L-Type需要的寄存器写入数据rf_wd传入WB级,准备写回;与之一起进入的还有寄存器堆写使能信号rf_we,和写入目标寄存器地址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
module PR_MEM_WB (
input logic clk,
input logic rst_n,
// 寄存器堆写使能
input logic rf_we_mem_i,
output logic rf_we_wb_o,
// 写回寄存器地址
input logic [ 4:0] wr_mem_i,
output logic [ 4:0] wr_wb_o,
// 写回数据
input logic [31:0] wd_mem_i,
output logic [31:0] wd_wb_o
);

always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rf_we_wb_o <= 1'b0;
wr_wb_o <= 5'b0;
wd_wb_o <= 32'b0;
end else begin
rf_we_wb_o <= rf_we_mem_i;
wr_wb_o <= wr_mem_i;
wd_wb_o <= wd_mem_i;
end
end

endmodule

写回 WB级 设计

WB级其实根本不用去设计——因为这一级根本没内容,只需要将要写回的数据送入寄存器堆即可。

顶层模块封装 CPU_TOP.sv

到此为止,我们已经完成了所有的流水线。接着,就是把它们拼在一起了。

我们封装后的CPU应当有IROM接口,输出地址、取回指令。此外,还要有时钟输入和低电平有效的复位信号。

1
2
3
4
5
6
7
8
9
module CPU_TOP (
input logic clk,
input logic rst_n,
// 来自指令存储器IROM的指令
input logic [31:0] instr,
// 输出给指令存储器IROM的地址
// 这里实际上是PC的高14位
output logic [13:0] pc
);

因为指令存储器以“字”(word)为单位寻址,且深度一般为214,对于普通RISCV指令来说占了4字节,且低两位恒为0(必须4字节对齐),所以取高14位即可。

我们从IF级拿的指令,直接取高14位:

1
assign pc = pc_IF[15:2];

之后对于各级模块,连线连起来即可。比较好的习惯是将信号所处的级做名称结尾。

仿真测试!

虽然我们写的是SystemVerilog,但是iverilog还是支持一些特性的——或者说,有warning有sorry但是工作正常。能跑就算语法支持,不过实际运行效果肯定是不如verilator的。

首先要写一个顶层模块,加入一些激励信号,并引出寄存器连线,便于观察:

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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
`timescale 1ns / 1ps

`include "../src/CPU_TOP.sv"
`define DEBUG

`define REG_FILE u_CPU_TOP.u_registerf

module tb_CPU_TOP;

// 时钟和复位信号
logic clk;
logic rst_n;

// IROM 信号
logic [13:0] irom_addr;
logic [31:0] irom_data;

// 实例化 IROM (指令存储器)
IROM u_IROM (
.a (irom_addr),
.spo(irom_data)
);

// 实例化 CPU_TOP
CPU_TOP u_CPU_TOP (
.clk (clk),
.rst_n(rst_n),
.instr(irom_data),
.pc (irom_addr)
);

// verilog_format: off
// 寄存器堆监控信号
logic [31:0] x0, x1, // ...
x31;

always_comb begin
x0 = `REG_FILE.rf_in[0];
x1 = `REG_FILE.rf_in[1];
// ...
x31 = `REG_FILE.rf_in[31];
end

// verilog_format: on

// 时钟生成 (100MHz, 周期 10ns)
initial begin
clk = 0;
forever #5 clk = ~clk;
end

// 复位和测试控制
initial begin
// 波形文件设置
`ifdef VCD_FILEPATH
$dumpfile(`VCD_FILEPATH);
`else
$dumpfile("wave.vcd");
`endif
$dumpvars;

// 初始化信号
rst_n = 0;

// 复位 CPU
#5; // 保持复位 25ns
rst_n = 1;
$display("========================================");
$display("CPU Reset Released at time %0t", $time);
$display("========================================");

// 运行一段时间让 CPU 执行指令
#2000;

$display("========================================");
$display("Simulation finished at time %0t", $time);
$display("========================================");

// 打印寄存器堆状态
print_register_file();

$finish;
end

// 监控关键信号
initial begin
$display("========================================");
$display("Time\t| PC\t| Instruction\t| Stage");
$display("========================================");

forever begin
@(posedge clk);
if (rst_n) begin
// IF 级
if (u_CPU_TOP.valid_IF)
$display("%0t\t| %h\t| %h\t| IF", $time, u_CPU_TOP.pc_IF, u_CPU_TOP.instr_IF);

// ID 级
if (u_CPU_TOP.valid_ID && u_CPU_TOP.instr_ID != 32'h00000013) // 跳过 NOP
$display("%0t\t| %h\t| %h\t| ID", $time, u_CPU_TOP.pc_ID, u_CPU_TOP.instr_ID);

// EX 级
if (u_CPU_TOP.valid_EX)
$display(
"%0t\t| %h\t| --------\t| EX \t (ALU=0x%h)",
$time,
u_CPU_TOP.pc_EX,
u_CPU_TOP.alu_result_EX
);

// MEM 级
if (u_CPU_TOP.dram_we_MEM) begin
$display("%0t\t| 0x%4h <| 0x%h\t|[MEM W]", $time,
u_CPU_TOP.alu_result_MEM[17:2], u_CPU_TOP.rf_rd2_MEM);
end else begin
if (u_CPU_TOP.wd_sel_MEM)
$display(
"%0t\t| 0x%4h |> 0x%h\t|[MEM R]",
$time,
u_CPU_TOP.alu_result_MEM[17:2],
u_CPU_TOP.DRAM_output_data
);
end
end
end
end

// 监控寄存器写回操作
initial begin
forever begin
@(posedge clk);
if (rst_n && u_CPU_TOP.rf_we_WB && u_CPU_TOP.wR_WB != 0) begin
$display("%0t\t| x%-2d <= 0x%h \t|[WB]", $time, u_CPU_TOP.wR_WB,
u_CPU_TOP.rf_wd_WB);
end
end
end

// 打印寄存器堆内容的任务
task automatic print_register_file();
integer i;
begin
$display("\n========================================");
$display("Register File Contents:");
$display("========================================");
for (i = 0; i < 32; i = i + 1) begin
if (u_CPU_TOP.u_registerf.rf_in[i] != 0) begin
$display("x%0d\t= 0x%h\t(%0d)", i, u_CPU_TOP.u_registerf.rf_in[i],
$signed(u_CPU_TOP.u_registerf.rf_in[i]));
end
end
$display("========================================\n");
end
endtask

// 超时保护
initial begin
#5000; // 50us 超时
$display("ERROR: Simulation timeout!");
$finish;
end

// 新建一个时钟 为clk的两倍周期 便于观察
logic slow_clk;
int unsigned count;
initial slow_clk = 0;

always_ff @(posedge clk) begin
slow_clk <= ~slow_clk;
count <= count + 1;
end


endmodule

之后用iverilog编译与仿真,并使用GTKWave看波形。这么多年过去了,还是没有一个现代化的波形查看器吗。

1
2
iverilog -g2012 -Wall -I ./user/src -I ./user/src/include -o tb_CPU_TOP.vvp
vvp tb_CPU_TOP.vvp

我们可以用官方的riscv工具链进行编译,当然也可以写一个Python文件手动将汇编文件转换成机器码。看个人喜好。

写一个简单的加法测试:

1
2
3
4
5
6
7
addi x1,x0,1
addi x2,x0,2
addi x3,x0,3
addi x4,x0,4
addi x5,x0,5
addi x6,x0,6
add x7,x1,x2

转换成二进制,应该是这样:

1
2
3
4
5
6
7
00100093
00200113
00300193
00400213
00500293
00600313
002083b3

objdump转换出的.verilog文件,是按照内存小端对齐的格式写的!而elf2hex则是将一个指令字当成一个整体输出,为大端对齐。实际写入逻辑还是按照大端,只是存储格式被认为是小端。

然后运行。

非常简单的加法

可以看到结果非常完美。我们成功了 ^_^