基本的控制单元和数据通路
模块实现
本实验需要完成大部分指令,自行完成数据通路和控制器的设计,从而得到自己设计实现的 SCPU。
在本节实验中,你需要实现以下指令:
- R-Type: add, sub, and, or, xor, slt, srl
- I-Type: addi, andi, ori, xori, slti, srli, lw
- S-Type: sw
- B-Type: beq
- J-Type: jal
其他指令将在 Lab4-3 指令拓展与 Lab4-4 中断处理两小节实现。如果不清楚以上指令的指令格式与含义,请查看 RISC-V 手册。
在分别完成 DataPath 和 CtrlUnit 两部分后,将 Lab4-0 中的 IP Core 替换,得到自行实现的 SCPU。
- CtrlUnit 的作用是根据指令的 OPcode 与 Funct3 以及 Funct7 产生相应的控制信号,用于控制数据通路中的各个模块。
- DataPath 可以自行设计,也可以参考附件 DataPath.pdf。注意这里的数据通路只支持部分指令,你需要在 Lab4-3 中拓展以支持所有指令。因此建议理解数据通路的数据流动后再进行实验,而不是单纯地参照附件连线。
- DataPath 中可能需要用到如下模块:ALU、RegFile、ImmGen、VGA、RAM、ROM、SCPU_ctrl。
- ALU、RegFile 在之前的实验里已经实现,可以直接使用。
- ImmGen 用来生成立即数,对于不同的指令类型要使用不同的生成方式。SCPU_ctrl 即我们上面提到的 CtrlUnit,用来产生控制信号。
- 请注意 RAM 中的数据存放方式为小端序,即数据的最低有效字节存放在最低的内存地址,最高有效字节存放在最高的内存地址。
- 我们鼓励你使用自己的方式完成设计。对于不知道如何设计的同学,我们在下面提供了 SCPU_ctrl、ImmGen 模块的接口定义,你可以参考这些定义来完成你的设计。
Tip
- 在设计本次实验之前,请检查你是否完成了之前实验要求的修改,这些修改将方便你在板上 debug 时查看寄存器值的变化。
- RegFile 有 32 个寄存器值读口,并在本次实验中接到 SCPU 的输出口。
- VGA 模块拓展了 32 个寄存器值的入口,并修改 VGA 内部使这些值可以显示在屏幕上。
- 修改 IMem.coe 或 DMem.coe 内容后,需要重新生成对应的存储器 IP 核。
SCPU_ctrl
参考模块接口如下:
module SCPU_ctrl(
input [4:0] OPcode,
input [2:0] Fun3,
input Fun7,
input MIO_ready,
output reg [1:0] ImmSel,
output reg ALUSrc_B,
output reg [1:0] MemtoReg,
output reg Jump,
output reg Branch,
output reg RegWrite,
output reg MemRW,
output reg [3:0] ALU_Control,
output reg CPU_MIO
);
endmodule
这里我们规定控制信号及其含义如下:(参考附件 Lab4_header.vh
)
控制信号介绍
ImmSel
用于选择生成立即数的方式,0 为 I-Type,1 为 S-Type,2 为 B-Type,3 为 J-Type。ALUSrc_B
用于选择 ALU 的 B 输入口,0 为寄存器值,1 为立即数。MemtoReg
用于选择写回寄存器的数据来源,0 为 ALU 输出,1 为存储器读出的值,2 为 PC+4。Jump
用于选择 PC 的下一个值,0 为 PC+4,1 为 J-Type 指令的目标地址。Branch
用于选择是否进行分支跳转,0 为不跳转,1 为跳转。RegWrite
用于选择是否写回寄存器,0 为不写回,1 为写回。MemRW
用于选择存储器的读写方式,0 为读,1 为写。ALU_Control
用于选择 ALU 的运算方式,接口定义与 Lab2 中 ALU 的实现相同。CPU_MIO
用于选择是否进行存储器的读写,0 为不进行,1 为进行。(实际上这个控制信号可有可无)
当然,你可以使用任何你认为合适的方式来实现这些控制信号,或者去掉某些你认为不重要的信号,增加你认为重要的信号,也可以改变控制信号的定义。(如果你的接口定义和上面不同,请在实验报告中说明)
立即数生成器
ImmGen 用来生成立即数,对于 I-Type、S-Type、B-Type、J-Type 指令,我们需要不同的生成方式。具体的指令格式可以查看 RISC-V 手册或速查表。
参考模块接口如下:
这里我们规定控制信号及其含义如下:(参考附件 Lab4_header.vh
)
- ImmSel 为 0 时,生成 I-Type 指令的立即数;为 1 时,生成 S-Type 指令的立即数;为 2 时,生成 B-Type 指令的立即数;为 3 时,生成 J-Type 指令的立即数。
你可以使用其他方式实现,或者改变 ImmSel 与不同类型立即数生成方法的对应,这里仅作参考。
仿真测试
ImmGen 的仿真测试
对于按照我们提供的接口定义实现的 ImmGen 模块,我们提供了一个仿真测试文件 ImmGen_tb.v。你可以自行进行验证,正确的参考波形可以查看标准波形文件。
如果你的模块接口或者接口定义与我们提供的不同,请自行编写仿真测试文件。你可以在 test.s 编写更多的仿真代码,通过 Venus 平台获得十六进制文件并更名为 console.out
,使用 ImmGen_tb_gen.py 获得要填写进仿真激励文件的代码。这一段 Python 代码非常简单,请自行查看文件命名与含义。当然,你也可以使用其他方式获得测试代码,这里仅作示例,以后不再提供类似文件。
SCPU_ctrl 的仿真测试
SCPU_ctrl
模块也将采取类似的仿真测试。对于按照我们提供的接口定义实现的 SCPU_ctrl
模块,我们提供了一个仿真测试文件 SCPU_ctrl_tb.v。
如果你的模块接口或者接口定义与我们提供的不同,请自行编写仿真测试文件。方式与上面类似,这里不再重复。
SCPU 的仿真测试
为了方便测试,我们需要搭建一个简单的仅包含 SCPU 以及 Mem(IMem & DMem) 的测试平台。你可以直接使用以下代码,如果你的端口名与之不同请自行修改。
搭建好测试平台后,请自行书写测试代码,生成 ROM 核,并进行仿真。我们要求 SCPU 的仿真测试覆盖各类指令。
module testbench(
input clk,
input rst
);
/* SCPU 中接出 */
wire [31:0] Addr_out;
wire [31:0] Data_out;
wire CPU_MIO;
wire MemRW;
wire [31:0] PC_out;
/* RAM 接出 */
wire [31:0] douta;
/* ROM 接出 */
wire [31:0] spo;
SCPU u0(
.clk(clk),
.rst(rst),
.Data_in(douta),
.MIO_ready(CPU_MIO),
.inst_in(spo),
.Addr_out(Addr_out),
.Data_out(Data_out),
.CPU_MIO(CPU_MIO),
.MemRW(MemRW),
.PC_out(PC_out)
);
RAM_B u1(
.clka(~clk),
.wea(MemRW),
.addra(Addr_out[11:2]),
.dina(Data_out),
.douta(douta)
);
ROM_B u2(
.a(PC_out[11:2]),
.spo(spo)
);
endmodule
下板验证
验收时,你需要使用以下验收代码。
验收代码
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 |
|
memory_initialization_radix=16;
memory_initialization_vector=
0240006F,
00000013,
00000013,
00000013,
00000013,
00000013,
00000013,
00000013,
FE5FF06F,
00000663,
00000F93,
FD9FF06F,
FFF00093,
0010C193,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003181B3,
003182B3,
005281B3,
00318233,
00420333,
006303B3,
00106413,
01F06E13,
01C3DEB3,
01D40663,
00100F93,
F41FF06F,
00000013,
407301B3,
40338233,
00102493,
0041A533,
00322533,
F2A482E3,
01E1DE93,
009E8663,
00200F93,
F15FF06F,
00000013,
0030A513,
0012A5B3,
0030A633,
0FF57513,
00B57533,
00C57533,
00100593,
00B50663,
00300F93,
EE9FF06F,
00000013,
0033E5B3,
00658663,
00400F93,
ED5FF06F,
00000013,
02000913,
00592023,
00492223,
00092E83,
005ECEB3,
00692023,
00092F03,
01EECEB3,
01D30663,
00500F93,
EA5FF06F,
66600F93,
E9DFF06F;
伪指令
在验收代码中,你会看到 li, j, bnez
等你并不熟悉的指令,这些指令都是“伪指令”。伪指令在标准指令集之外,不是真正的机器指令,而是汇编语言级别上提供的语法糖(或宏指令),用于简化编程并提高代码可读性。在实际的机器指令级别,它们会被汇编器翻译成等效的一条或多条标准指令来执行。下面简单介绍本次验收代码中给出的 RV32 伪指令,其他伪指令可以自行搜索:
li(load immediate)
,用于将一个立即数加载到寄存器中。少于 12 位的立即数加载,如li x1, 0x666
,可以翻译成addi x1, x0, 0x666
一条指令完成;大立即数,如li x1, 0x66666666
,可以翻译成lui x1, 0x66666; addi x1, x1, 0x666
两条指令来完成。j(jump)
,用于无条件跳转到一个标签处。如j label
会被翻译成jal x0, label
。
使用上述验收代码,如果没有非常严重的实现问题,不论是否通过测试,最后都会进入 dummy
的死循环,进入循环后,x31(t6)
寄存器会存放一个值,若为 0x666
即表示验收代码通过。
如果 x31
的值并不是 0x666
,表明你的实现存在问题,你可以在 check_code.s
中搜索 li x31,
快速定位产生问题的检测点;如果并没有进入 dummy
,你的实现有严重问题,请从头开始单步调试,直到发现并解决问题。
给出的验收代码是完成验收的最低标准,我们很欢迎对验收代码的补充完善,优秀的测试代码将获得本实验的加分。
实验要求
- 实现 CtrlUnit 和 DataPath,得到自行实现的 SCPU。
- 对实现的 CPU 进行仿真验证、下板验证、验收。
- 实验报告里需要包含的内容:
- CtrlUnit 和 DataPath 代码(包括 ImmGen 等子模块)并简要分析。
- 仿真代码与波形截图(注意缩放比例、数制选择合适),并分析仿真波形。
- 你需要完成 SCPU_ctrl、ImmGen 的仿真测试(使用我们给的仿真代码或者自行书写),以及 SCPU 的仿真测试(自行书写仿真代码)。
- 如果你完善了验收代码或自己设计了测试代码,请在报告中给出。