跳转至

多周期 CPU 的设计与实现

多周期 CPU 的控制单元实际上就是一个大的状态机——每个周期处于不同的状态,给各个功能部件提供不同的控制信号。

下面我们以 addi 为例,重新回顾一下上课讲过的多周期 CPU 数据通路和控制信号通路的设计方法。

指令功能分析

addi rd, rs1, imm 的功能是 R[rd] = R[rs1] + sign_extend(imm)。其中 R[rs1] 表示源寄存器的值,sign_extend(imm) 表示对立即数 imm 做符号扩展,即用立即数的最高位补齐高位,使负数扩展后仍表示同一个值。

例如,addi t1, t0, 1 会把 t0 的当前值加 1,再写回 t1。硬件完成这件事需要:

  1. 从寄存器中读出 rs1 对应的数值
  2. 将指令的一部分翻译成立即数,并做符号扩展
  3. 将上述两个结果相加
  4. 将结果存入 rd 对应的寄存器中

控制信号设计

首先,根据上面的功能说明,将指令的执行分为以下几个阶段(状态机状态)

  1. STATE_IF : 从 SRAM 中读取指令,更新 pc,寄存当前 pc。
  2. STATE_ID : 从寄存器中读取对应寄存器的值,同时生成立即数。
  3. STATE_EXE : 做运算。
  4. STATE_WB : 将结果写回寄存器。

接下来将一步步分析这个状态机的转移和每个状态要输出的控制信号。

STATE_IF

这个状态需要从 SRAM 中读取指令,让 ALU 计算 pc + 4,同时寄存当前的 pc。

读取指令需要发送 Wishbone 请求,方法与实验 4 的 Wishbone Master相同。用 PC 作为字节地址,设置 CYC=1、STB=1、WE=0、SEL=4'b1111,读取一条 32 位指令。

发起请求后,保持地址和控制信号不变。在时钟上升沿采样到 CYC && STB && ACK 时,说明读取完毕,将返回的数据存入指令寄存器,保存当前指令地址,再更新 PC 并转到 ID。

部分代码大致如下:

reg [31:0] pc_reg;
reg [31:0] pc_now_reg;
reg [31:0] inst_reg;
always_comb begin
    ...
    case(state)
        STATE_IF: begin
            wb_addr_o = pc_reg;
            wb_cyc_o = 1'b1;
            wb_stb_o = 1'b1;
            wb_we_o = 1'b0;
            wb_sel_o = 4'b1111;
            alu_operand1_o = pc_reg;
            alu_operand2_o = 32'h00000004;
            alu_op_o = ALU_ADD;
            ...
    endcase
end
always_ff @ (posedge clk) begin
    ...
        case(state)
            STATE_IF: begin
                inst_reg <= wb_data_i;
                pc_now_reg <= pc_reg;
                ...
                if (wb_ack_i) begin
                    pc_reg <= alu_result_i; // 注意更新的位置,wishbone 请求时,addr 地址不能变
                    state <= STATE_ID;
                end
            ...
        endcase
    ...
end                    

STATE_ID

这个状态需要从寄存器中读取数据。

由于读寄存器没有副作用,因此读寄存器不需要使能端。

因此,直接将指令的 rs1 部分和 rs2 部分传递给寄存器,读出结果即可。

虽然 addi 只有 rs1 一个源寄存器,但是为了让读端口连线统一,可以把 rs1 和 rs2 分别接到两个读地址端,但 addi 指令中的 rs2 部分实际上属于立即数,而不是真正的 rs2,因此第二个端口读出的结果不参与运算。

注意要同时在这个状态生成立即数。因此要给 imm_gen 信号和指令,让其生成 I 型立即数。

这时需要记录下需要使用的两个操作数以便后续使用。因此在控制器需要再建立两个寄存器 operand1 和 operand2,寄存器 rs1 读到的结果存到 operand1 内,生成的立即数存到 operand2 内。寄存器 rs2 读出的结果丢弃。

部分代码大致如下:

reg [31:0] operand1_reg;
reg [31:0] operand2_reg;
always_comb begin
    ...
    case(state)
        STATE_ID: begin
            rf_raddr_a_o = <instruction rs1 segment>;
            rf_raddr_b_o = <instruction rs2 segment>;
            imm_gen_inst_o = <instrction segment to generate immediate>;
            if(<instruction is type I>) begin
                imm_gen_type_o = TYPE_I;
            end 
            ...
    endcase
end
always_ff @ (posedge clk) begin
    ...
        case(state)
            STATE_ID: begin
                if(<instruction is ADDI>) begin
                    operand1_reg <= rf_rdata_a_i;
                    operand2_reg <= imm_gen_imm_i;
                end
                ...
                state <= STATE_EXE;
            ...
        endcase
    ...
end                    

STATE_EXE

在这个状态,我们需要根据 operand1 和 operand2 进行计算。

同时将计算结果存下来供下一个状态进行写入。因此再设计一个 rf_writeback_reg 用于存储这个结果。

部分代码大致如下:

reg [31:0] rf_writeback_reg;
always_comb begin
    ...
    case(state)
        STATE_EXE: begin
            alu_operand1_o = operand1;
            alu_operand2_o = operand2;
            if(<instruction is ADDI>) begin
                alu_op_o = ALU_ADD;
            end 
            ...
    endcase
end
always_ff @ (posedge clk) begin
    ...
        case(state)
            STATE_EXE: begin
                if(<instruction is ADDI>) begin
                    rf_writeback_reg <= alu_result_i;
                    state <= STATE_WB;
                end
                ...
            ...
        endcase
    ...
end                    

STATE_WB

在这个状态需要将结果写回寄存器。 部分代码大致如下:

reg [31:0] rf_writeback_reg;
always_comb begin
    ...
    case(state)
        STATE_WB: begin
            rf_wen_o = 1'b1;
            rf_wdata_o = rf_writeback_reg;
            rf_waddr_o = <rd segment of instruction>
            ...
    endcase
end
always_ff @ (posedge clk) begin
    ...
        case(state)
            STATE_WB: begin
                state <= STATE_IF;
                ...
            ...
        endcase
    ...
end                    

这样我们就完成了对 addi 指令执行的状态设计。同学们可以参照给出的状态设计,继续设计其他指令的数据通路,从而完成整个实验。

总结

addi 指令执行的信号表如下:

状态 wb_addr wb_cyc rf_raddr_a rf_raddr_b imm_gen_type alu_operand1 alu_operand2 alu_op rf_wen rf_wdata rf_waddr
STATE_IF pc 1 x x x pc 32'h4 ADD 0 x x
STATE_ID x 0 rs1 rs2 I x x x 0 x x
STATE_EXE x 0 x x x operand1 operand2 ADD 0 x x
STATE_WB x 0 x x x x x x 1 result rd

状态转移和时序如下:

原状态 新状态 条件 时序操作
STATE_IF STATE_IF ack == 0
STATE_IF STATE_ID ack == 1 inst <= wb_data_i, pc_now <= pc, pc <= alu_result
STATE_ID STATE_EXE TRUE operand1 <= rf_rdata_a, operand2 <= imm_gen_imm
STATE_EXE STATE_WB TRUE rf_writeback <= alu_result
STATE_WB STATE_IF TRUE

状态转移图如下:

flowchart LR
  IF[STATE_IF] -- 尚未收到 ACK --> IF;
  IF -- 取指完成 --> ID[STATE_ID];
  ID --> EXE[STATE_EXE];
  EXE --> WB[STATE_WB];
  WB --> IF;

同学们在实现的时候也可以画出以上图表,以便于自己编写代码以及进行 debug。

以上仅是一种对于 addi 指令可行的状态设计,同学们在实现时可以按照需要,自己设计对应的状态,信号和转移。

学完这节内容我需要做什么?

参照 addi 的分析方法,为其余指令确定执行路径、控制信号、跨状态保存的信息,再据此实现各功能部件,完成多周期 CPU 设计。

作者:cuibst (88.05%), 于越洋 (11.06%), Jiajie Chen (0.88%)