实验内容
实验 2:ALU 与寄存器堆实验
算术逻辑部件 ALU 是处理器中完成算术、逻辑运算的模块,而寄存器堆 RF 是一组寄存器的集合,用于在内存与运算部件之间暂存数据。二者均是处理器中的核心部件,实现起来并不复杂。
在本实验中,大家将实现完整的 ALU 和寄存器堆模块,并通过一个控制状态机对二者进行控制,成为一个可以执行简单运算指令,并可以输入、输出数据的计算器。同时,本实验中,大家还将对两个模块进行仿真,体会仿真在硬件开发、调试过程中的重要性。
模板文件
本实验的顶层模块模板:thinpad_top.srcs/sources_1/new/lab3/lab3_top.sv
实验目的
- 了解 ALU 各类计算、寄存器堆逻辑的实现方式;
- 借助状态机代码模板,体会 SystemVerilog 中有限状态机的设计方法;
- 进一步熟悉模块化的硬件设计思路,学习仿真调试方法。
实验内容
设计需求
使用 32 位拨码开关和 push_btn
按键控制计算逻辑的工作。每次在拨码开关上输入一条 32 位的指令,按下 push_btn
,计算逻辑自动控制,完成指令中给定的操作,包括从寄存器中取出两个操作数进行计算、将立即数装载到寄存器,和从寄存器中读出操作数显示在 LED 上三种。具体需求见下文。
注意
你应该按照按下 push_btn
的次数来进行状态控制,即使一次按下 push_btn
多个周期 / 很长时间,你的设计仍然只应该执行 1 条指令。
设计框图
flowchart LR
controller[控制逻辑 Controller]:::seq;
regfile[寄存器堆 Register File]:::seq;
alu[算术逻辑部件 ALU]:::comb;
push_btn[微动按钮]:::periph -- push_btn --> btn1[按键检测 Button In]:::seq;
dip_sw[拨码开关]:::periph -- dip_sw ---> controller;
subgraph FPGA
direction BT
btn1 -- step --> controller;
regfile <==> controller;
alu <==> controller;
end
controller -- leds --> leds[LED]:::periph;
classDef comb fill:#e8f5e9,stroke:#43a047; %% green
classDef seq fill:#ffecb3,stroke:#ffa000; %% orange
classDef periph fill:#e3f2fd,stroke:#2196f3; %% blue
linkStyle default opacity:1.0
原理图
算术逻辑部件 ALU
算术逻辑部件 ALU 的主要功能是对二进制数据进行定点算术运算、逻辑运算和各种移位操作等。算术运算包括定点加减乘除运算;逻辑运算主要有逻辑与、逻辑或、逻辑异或和逻辑非等操作。
ALU 通常有两个数据输入端 A 和 B 输入操作数,一个数据输出端 Y 以及标志位输出结果,通过输入操作码 op 来确定所要进行的操作。在简单的处理器设计中,ALU 为一个 组合逻辑模块。
本实验中,我们将待设计的 ALU 数据位宽定为 16 位,即操作数 A、B,输出数据 Y 均为 16 位整数,其有无符号由具体的运算 op 决定。希望支持的 op 列表如下:
编号 | 操作码 | 功能 | 描述 |
---|---|---|---|
1 | ADD | Y = A + B |
加法 |
2 | SUB | Y = A - B |
减法 |
3 | AND | Y = A and B |
按位与 |
4 | OR | Y = A or B |
按位或 |
5 | XOR | Y = A xor B |
按位异或 |
6 | NOT | Y = not A |
按位取非 |
7 | SLL | Y = A sll B |
逻辑左移 B 位 |
8 | SRL | Y = A srl B |
逻辑右移 B 位 |
9 | SRA | Y = A sra B |
算术右移 B 位 |
10 | ROL | Y = A rol B |
循环左移 B 位 |
寄存器堆 RegFile
寄存器堆用于在 CPU 内部暂存一系列的运算数据,同学们在之前的实验中,已经了解过 RV32I 指令集规定的寄存器列表,并在汇编程序中使用过这些寄存器。本实验中,希望大家在硬件上实现这一寄存器堆的功能,为了降低复杂程度,我们将寄存器的位宽定为 16 位,而非 RV32I 中的 32 位。
由于需要在时钟周期之间保存当前的状态,寄存器堆是一个时序逻辑模块。通常而言,除时钟与复位信号外,一个基本的寄存器堆模块具有以下的输入/输出信号:
input [4:0] waddr
:写操作的寄存器编号;input [15:0] wdata
:写操作的数据;input we
:写操作的使能信号;input [4:0] raddr_a
:读端口 A 的寄存器编号;output [15:0] rdata_a
:读端口 A 的寄存器数据;input [4:0] raddr_b
:读端口 B 的寄存器编号;output [15:0] rdata_b
:读端口 B 的寄存器数据;
可以看到,上述的信号 1、2、3 组成了一个“写端口”,而 4、5 与 6、7 各自组成了两个“读端口”,因此,通常称为这一寄存器堆为 1 写 2 读。两个读端口的逻辑各自独立,因此可以在一个时钟周期内取出两个操作数。为了保证处理器性能,寄存器均为单周期读写操作,波形如下:
波形图中,第 1 个时钟上升沿,将数据 12 写入了 2 号寄存器,因此端口 A 在第 2 个周期处,读到的数据变成了 12。而读端口 B 在第 1 个周期的时候,读到的是未写入数据的寄存器,我们将其复位为 0。请注意,由于 SystemVerilog 对寄存器的赋值,将会在下个时钟周期时生效,所以上图中 2 号寄存器读出来的数据相对于写入的数据是延迟了一个周期的,4 号寄存器和 1a 号寄存器同理。
依照 RISC-V 的规范,本次实验中,也规定 0 号寄存器的数据恒为 0,而对其的写入操作将被忽略。
控制逻辑
本实验中,控制逻辑负责从拨码开关、按键处接受用户的输入,并根据用户输入的命令完成与 ALU 及寄存器堆的交互操作,并在有需要时将操作结果显示在 16 位 LED 上。
实际上,同学们也可以将这一控制逻辑看成最简单的“CPU”,它的本质是一个有限状态机,根据用户输入决定状态的转移。
命令格式
控制逻辑支持两种不同的命令格式,分别称为 I-Type 和 R-Type,命令的长度均为 32 位。
R-Type 指令用于在源寄存器 rs1
, rs2
和目标寄存器 rd
之间进行数学运算,其中,rs1
, rs2
和 rd
分别表示源寄存器和目标寄存器的编号,而 op 则表示 ALU 计算的类型,对应前面表格中的编号字段,即 rd = rs1 OP rs2
。rs1
对应 A 数值,rs2
对应 B 数值。
例如,对于指令 0x00820709 = 0b0000000_01000_00100_000_01110_0001_001
,其中 rs2 = 0b01000
,rs1 = 0b00100
,op = 0b0001 = ADD
,则表示对源寄存器 rs2 = regs[8]
与 rs1 = regs[4]
中的数据进行加法运算,并将结果写入目标寄存器 rd = regs[14]
中。
+------------+--------+--------+--------+-------+---------+-----+
| 31 25 | 24 20 | 19 15 | 14 12 | 11 7 | 6 3 | 2 0 |
+------------+--------+--------+--------+-------+---------+-----+
| 0000000 | rs2 | rs1 | 000 | rd | op[3:0] | 001 | R-Type
+------------+--------+--------+--------+-------+---------+-----+
I-Type 指令包含两条。POKE
指令将其中包含的 16 位立即数,写入寄存器 rd
;PEEK
指令读出寄存器 rd
中的数据,并显示在 LED 上,其中的立即数可以被忽略。
+--------------+--------+-------+------+-----+
| 31 16 | 15 12 | 11 7 | 6 3 | 2 0 |
+--------------+--------+-------+------+-----+
| imm[15:0] | 0000 | rd | 0001 | 010 | I-Type POKE
| imm[15:0] | 0000 | rd | 0010 | 010 | I-Type PEEK
+--------------+--------+-------+------+-----+
状态机设计
本实验中,控制逻辑为一个有限状态机,一个可能的设计如下(省略每个状态到自身的自环)。
flowchart LR
INIT -- step == 1 --> DECODE;
DECODE -- is_rtype == 1 --> CALC;
DECODE -- "(others)" --> INIT;
CALC --> WRITE_REG --> INIT;
DECODE -- is_poke == 1 --> WRITE_REG;
DECODE -- is_peek == 1 --> READ_REG --> INIT;
INIT
:复位后的初始状态,等待用户按键信号step
;DECODE
:解析用户输入的指令,根据上文中的对应关系,产生is_rtype
等信号;CALC
:针对 R-Type 指令,从寄存器堆读出数据,使用 ALU 计算结果;WRITE_REG
:针对 R-Type 指令,将结果写入rd
;针对POKE
指令,将imm
的数值写入寄存器rd
;READ_REG
:针对PEEK
指令,从寄存器堆读出rd
的数据,显示在 LED 上。
代码模板
仅供参考
下面给出的代码仅供参考,这只是有限状态机的一种编写方式,称为一段式状态机,即将状态转移、输出逻辑均放置在同一个 always_ff
时序逻辑块中。实际上,为了代码的清晰可读,我们更推荐使用三段式状态机的编写方式,同学们可以自行尝试。
module controller (
input wire clk,
input wire reset,
// 连接寄存器堆模块的信号
output reg [4:0] rf_raddr_a,
input wire [15:0] rf_rdata_a,
output reg [4:0] rf_raddr_b,
input wire [15:0] rf_rdata_b,
output reg [4:0] rf_waddr,
output reg [15:0] rf_wdata,
output reg rf_we,
// 连接 ALU 模块的信号
output reg [15:0] alu_a,
output reg [15:0] alu_b,
output reg [ 3:0] alu_op,
input wire [15:0] alu_y,
// 控制信号
input wire step, // 用户按键状态脉冲
input wire [31:0] dip_sw, // 32 位拨码开关状态
output reg [15:0] leds
);
logic [31:0] inst_reg; // 指令寄存器
// 组合逻辑,解析指令中的常用部分,依赖于有效的 inst_reg 值
logic is_rtype, is_itype, is_peek, is_poke;
logic [15:0] imm;
logic [4:0] rd, rs1, rs2;
logic [3:0] opcode;
always_comb begin
is_rtype = (inst_reg[2:0] == 3'b001);
is_itype = (inst_reg[2:0] == 3'b010);
is_peek = is_itype && (inst_reg[6:3] == 4'b0010);
is_poke = is_itype && (inst_reg[6:3] == 4'b0001);
imm = inst_reg[31:16];
rd = inst_reg[11:7];
rs1 = inst_reg[19:15];
rs2 = inst_reg[24:20];
opcode = inst_reg[6:3];
end
// 使用枚举定义状态列表,数据类型为 logic [3:0]
typedef enum logic [3:0] {
ST_INIT,
ST_DECODE,
ST_CALC,
ST_READ_REG,
ST_WRITE_REG
} state_t;
// 状态机当前状态寄存器
state_t state;
// 状态机逻辑
always_ff @(posedge clk) begin
if (reset) begin
// TODO: 复位各个输出信号
state <= ST_INIT;
end else begin
case (state)
ST_INIT: begin
if (step) begin
inst_reg <= dip_sw;
state <= ST_DECODE;
end
end
ST_DECODE: begin
if (is_rtype) begin
// 把寄存器地址交给寄存器堆,读取操作数
rf_raddr_a <= rs1;
rf_raddr_b <= rs2;
state <= ST_CALC;
end else if (...) begin
// TODO: 其他指令的处理
...
end else begin
// 未知指令,回到初始状态
state <= ST_INIT;
end
end
ST_CALC: begin
// TODO: 将数据交给 ALU,并从 ALU 获取结果
state <= ST_WRITE_REG;
end
ST_WRITE_REG: begin
// TODO: 将结果存入寄存器
state <= ST_INIT;
end
ST_READ_REG: begin
// TODO: 将数据从寄存器中读出,存入 leds
state <= ST_INIT;
end
default: begin
state <= ST_INIT;
end
endcase
end
end
endmodule
思考题
- 假设需要你的 ALU 支持乘法和除法运算,而这些运算均不能在一个周期内完成,应该如何改动你的状态机?给出新的状态转移图,并做简要说明。
实验报告要求
-
完成代码编写后,进行仿真,给出波形图,并简要介绍你的仿真输入波形的设计逻辑。(为什么要给出这个波形?是为了检测什么功能?)
-
在云平台上进行实验,验证 ALU 实现的正确性,并给出实验截图。注意不是 OJ 通过截图。
提示
不需要对全部指令进行验证,只需要验证 POKE + 某 1~2 条计算指令 + PEEK 的结果正确即可。
-
回答思考题。