单周期 CPU(简化版)
整体框架
上图展示了本次实验中 CPU 的数据通路。该数据通路可以兼容 RV32I 版本的 CPU 和 LA32R 版本的 CPU,二者仅在译码器 Decoder 的内部结构上不同。此外,通路中省略了控制信号,例如 alu_op、rf_we 等。
模块设计
PC 寄存器
寄存器是我们最先接触的时序元件。一般而言,我们可以用如下的 Verilog 程序描述寄存器的行为:
寄存器的 Verilog 实现 | |
---|---|
1 2 3 4 5 6 |
|
在 CPU 中,PC 寄存器时刻存储了正在执行的指令的地址。它的功能是将当前指令的地址传递给指令存储器,从而读出此时正确的指令内容。同时,它也需要能够接受下一条指令地址的输入,并在时钟上升沿到来时更新自己的值,从而实现了指令的连续运行。
PC 寄存器的模块示意如下所示:
其端口代码为:
PC | |
---|---|
1 2 3 4 5 6 7 8 |
|
为什么要有使能信号?
在课堂上我们提到过:单周期 CPU 每个时钟周期都会更新 PC 寄存器中的值。因此,寄存器的写入操作应当在每个时钟周期内进行。那么这里为什么依然保留了使能信号呢?
这是因为我们需要引入来自 PDU 的控制逻辑。试想:虽然单周期 CPU 每个时钟周期都会执行一条指令,但指令的数目是有限的。我们必然需要在合适的时候让 CPU 停下来,从而让我们观察 CPU 的执行情况。因此,PC 寄存器依然需要保留外界的使能信号。
值得一提的是,从代码上看,PC 寄存器的行为可以被一个 always 块完全实现。那么为什么我们还是将其封装成了一个模块呢?
- 一方面,高度模块化的设计可以使我们的电路结构更加清晰,减少杂乱的门电路的出现(看不见就是没有.jpg)。
- 另一方面,我们可以更为直观地看到各个模块之间数据的传输关系,便于和提供的单周期数据通路进行比对。
- 此外,为了代码的可读性,我们一般不会在一个模块中保留过多的代码。
综上所述,对于部分基础模块的封装是很有必要的。
正确设置 PC 的起始地址!
请根据你所使用的指令集合理设置 PC 寄存器的复位值。
在仿真和上板时,我们往往会先置 rst 信号为高电平,将所有的寄存器复位到正确的初始状态,而 LA32R 和 RV32I 指令集对用户程序的起始地址有着不同的规定。
- 对于 LA32R 指令集,程序段的起始地址位于 0x1C000000。这也对应着 Lars 中 .text 段的起始地址;
- 对于 RV32I 指令集,程序段的起始地址位于 0x00400000。这也对应着 Rars 中默认类型内存分布下 .text 段的起始地址。
寄存器堆
本次实验我们需要在 Lab2 的寄存器堆基础上,为其添加一组 debug 接口 debug_reg_ra、debug_reg_rd。这一对端口与数据读端口功能一致,只用于仿真与上板时的调试。CPU 在正常运行时并不会用到这两个端口。
数据选择器
ALU 的两个源操作数可能是寄存器堆的输出,也可能是立即数,还可能是当前 PC 的值。为此,我们需要为源操作数添加数据选择器,用于控制 ALU 的数据来源。一个简单的参数化选择器实现如下:
二选一选择器 | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
对于 LA32R 和 RV32I 指令集来说,ALU 的数据来源为:
- alu_src0:可能来自 PC,可能来自寄存器堆;
- alu_src1:可能来自立即数,可能来自寄存器堆。
译码器
译码器是 CPU 的心脏,它负责根据输入的指令生成相应的控制与数据信号。下面是本次实验中译码器的结构示意图。
其端口代码为:
译码器 | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
各个输出端口的作用是:
- alu_op:ALU 的运算模式码;
- imm:经过扩展的立即数;
- rf_ra0、rf_ra1:寄存器堆的读地址;
- rf_wa:寄存器堆的写地址;
- rf_we:寄存器堆的写使能;
- alu_src0_sel、alu_src1_sel:ALU 的源操作数选择信号。
译码器模块为一个巨大的组合逻辑单元,所有的信号都根据 inst 的数值,结合对应指令集的译码规则得出。需要指出的是:rf_ra0、rf_ra1 和 rf_wa 三条信号较为特殊,因为其在指令中的位置是固定的,可以直接通过截取得到;其他信号则需要判断指令的类型后,通过更为复杂的逻辑得到。
译码器
请根据你所使用的指令集实现译码器的功能。
由于指令集不同,因此每条指令的译码规则也不同。请根据 Lab1 中我们需要实现指令,查阅对应的指令集手册,以正确生成译码器模块的输出信号。
一条指令没有这个信号该怎么办?
这是一个很好的问题。以下面两条 LA32R 指令为例:
- 算数立即数右移:srai.w rd, rj, imm(RISC-V 中为 srai rd, rs1, shamt)
- 算数右移:sra.w rd, rj, rk(RISC-V 中为 sra rd, rs1, rs2)
srai.w 指令并没有 rk 字段,因此也就不需要译码器的 rf_ra1 端口。我们自然可以让此时 rf_ra1 的输出为 0,也可以按照相同的逻辑读取指令对应部分的值(尽管此时该字段为立即数),只需要保证后续送入 ALU 的源操作数是正确的即可。
同理,对于 sra.w 指令,译码器的 imm 端口可以是任意的数值,只需要保证后续送入 ALU 的源操作数是正确的即可。