多周期 CPU 的设计与实现
多周期 CPU 的控制单元实际上就是一个大的状态机——每个周期处于不同的状态,给各个功能部件提供不同的控制信号。
下面我们以 addi 为例,重新回顾一下上课讲过的多周期 CPU 数据通路和控制信号通路的设计方法。
指令功能分析
addi 指令的功能描述如下:
- 从寄存器中读出 rs1 对应的数值
- 将指令的一部分翻译成立即数,并做符号扩展
- 将上述两个结果相加
- 将结果存入 rd 对应的寄存器中
控制信号设计
首先,根据上面的功能说明,将指令的执行分为以下几个阶段(状态机状态)
- STATE_IF : 从 SRAM 中读取指令,更新 pc,寄存当前 pc。
- STATE_ID : 从寄存器中读取对应寄存器的值,同时生成立即数。
- STATE_EXE : 做运算。
- STATE_WB : 将结果写回寄存器。
接下来将一步步分析这个状态机的转移和每个状态要输出的控制信号。
STATE_IF
这个状态需要从 SRAM 中读取指令,让 ALU 计算 pc + 4
,同时寄存当前的 pc。
读取指令需要发送 wishbone 请求,因此需要类似于实验 5 中 wishbone master 操作 wishbone 总线的相关信号,包括 addr, cyc, stb 等等。这里请参考实验 5 输出对应信号读取位于 pc 的数据,并等待 ack 和结果。
当 ack 为 1 时,说明读取完毕,此时需要转到下一个状态 ID,同时将读出的 data 存到指令寄存器中。
部分代码大致如下:
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;
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 只需要读 1 个寄存器,但是因为读寄存器没有副作用,为了通用性考虑,我们就直接把 rs1, 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 |
同学们在实现的时候也可以写出这两张表,以便于自己编写代码以及进行 debug。
以上仅是一种对于 addi 指令可行的状态设计,同学们在实现时可以按照需要,自己设计对应的状态,信号和转移。