跳转至

实验内容

实验 3:ALU 与寄存器堆实验

算术逻辑部件 ALU 是处理器中完成算术、逻辑运算的模块,而寄存器堆 RF 是一组寄存器的集合,用于在内存与运算部件之间暂存数据。二者均是处理器中的核心部件,实现起来并不复杂。

在本实验中,大家将实现完整的 ALU 和寄存器堆模块,并通过一个控制状态机对二者进行控制,成为一个可以执行简单运算指令,并可以输入、输出数据的计算器。同时,本实验中,大家还将对两个模块进行仿真,体会仿真在硬件开发、调试过程中的重要性。

模板文件

本实验的顶层模块模板:thinpad_top.srcs/sources_1/new/lab3/lab3_top.sv

实验目的

  1. 了解 ALU 各类计算、寄存器堆逻辑的实现方式;
  2. 借助状态机代码模板,体会 SystemVerilog 中有限状态机的设计方法;
  3. 进一步熟悉模块化的硬件设计思路,学习仿真调试方法。

实验内容

设计需求

使用 32 位拨码开关和 push_btn 按键控制计算逻辑的工作。每次在拨码开关上输入一条 32 位的指令,按下 push_btn,计算逻辑自动控制,完成指令中给定的操作,包括从寄存器中取出两个操作数进行计算、将立即数装载到寄存器,和从寄存器中读出操作数显示在 LED 上三种。具体需求见下文。

设计框图

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 位。

由于需要在时钟周期之间保存当前的状态,寄存器堆是一个时序逻辑模块。通常而言,除时钟与复位信号外,一个基本的寄存器堆模块具有以下的输入/输出信号:

  1. input [4:0] waddr:写操作的寄存器编号;
  2. input [15:0] wdata:写操作的数据;
  3. input we:写操作的使能信号;
  4. input [4:0] raddr_a:读端口 A 的寄存器编号;
  5. output [15:0] rdata_a:读端口 A 的寄存器数据;
  6. input [4:0] raddr_b:读端口 B 的寄存器编号;
  7. output [15:0] rdata_b:读端口 B 的寄存器数据;

可以看到,上述的信号 1、2、3 组成了一个“写端口”,而 4、5 与 6、7 各自组成了两个“读端口”,因此,通常称为这一寄存器堆为 1 写 2 读。两个读端口的逻辑各自独立,因此可以在一个时钟周期内取出两个操作数。为了保证处理器性能,寄存器均为单周期读写操作,波形如下:

clkwaddr241a2wdata12345678weraddr_a240rdata_a12340raddr_b21ardata_b012056regs[0x02]01278regs[0x04]034regs[0x1a]056

波形图中,第 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, rs2rd 分别表示源寄存器和目标寄存器的编号,而 op 则表示 ALU 计算的类型,对应前面表格中的编号字段,即 rd = rs1 OP rs2rs1 对应 A 数值,rs2 对应 B 数值。

例如,对于指令 0x00820709 = 0b0000000_01000_00100_000_01110_0001_001,其中 rs2 = 0b01000rs1 = 0b00100op = 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 位立即数,写入寄存器 rdPEEK 指令读出寄存器 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

思考题

  1. 假设需要你的 ALU 支持乘法和除法运算,而这些运算均不能在一个周期内完成,应该如何改动你的状态机?给出新的状态转移图,并做简要说明。

实验报告要求

  1. 完成代码编写后,进行仿真,给出波形图,并简要介绍你的仿真输入波形的设计逻辑。(为什么要给出这个波形?是为了检测什么功能?)

  2. 在云平台上进行实验,验证 ALU 实现的正确性,并给出实验截图。注意不是 OJ 通过截图。

  3. 回答思考题。


最后更新: 2023年9月27日
作者:Jiajie Chen (11.3%), Youyou Lu (1.71%), gaoyichuan (82.19%), cuibst (4.45%), Kang Chen (0.34%)