P3 Logisim实现单周期处理器

目录 :

[toc]

整体结构

  1. 所要设计的版块:IFUSplitterControllerGRFALUDMEXT
  2. 设计要求:支持的指令集:add sub ori lw sw beq lui nop 其中addsub按照无符号处理
  3. 顶层驱动信号:异步复位reset

指令分析:

  • R型:add sub nop # Opcode + Rs + Rt + Rd + shamt + Func
  • I型:ori lw sw lui beq # Opcode + Rs + Rt + immediate
1
2
3
4
5
6
7
8
9
add $t0, $t1, $t2                       # 机器码:000000_01001_01010_01000_00000_100000
sub $t0, $t1, $t2 # 机器码:000000_01001_01010_01000_00000_100010
nop # 机器码:000000_00000_00000_00000_00000_000000

ori $t0, $t1, 1 # 机器码:001101_01001_01000_0000000000000001
lw $t0, 1($t1) # 机器码:100011_01001_01000_0000000000000001
sw $t0, 1($t1) # 机器码:101011_01001_01000_0000000000000001
lui $t0, 1 # 机器码:001111_00000_01000_0000000000000001
beq $t0, $t1, 1 # 机器码:000100_01000_01001_0000000000000001

数据通路

不涉及j指令:

CO_P3_G1

j指令:

CO_P3_G2

指令类型分析

CO_P3_G3

CO_P3_G4

思路整理

在搭建完Logisim实现上述8条指令的单周期处理器后,我们尝试来回溯一下整个工作的思路。

首先,也是最重要的一步,是明确我们要实现的是哪些指令,以及这些指令到底要做一件什么事:

1
2
3
4
5
6
7
8
9
10
11
add $t0, $t1, $t2                       # Opcode:000000 Func:100000   rd = rs + rt
sub $t0, $t1, $t2 # Opcode:000000 Func:100010 rd = rs - rt
nop # Opcode:000000 Func:000000
# 由于nop指令的机器码是全0,我们可以认为nop = add $0, $0, $0

ori $t0, $t1, imm_16 # Opcode:001101 rt = rs | imm_unsign_ext32
lw $t0, imm_16($t1) # Opcode:100011 rt = getFromMem (rs + imm_sign_ext32)
sw $t0, imm_16($t1) # Opcode:101011 WriteMen (rs + imm_sign_ext32)
lui $t0, imm_16 # Opcode:001111 rs = 000000 rt = imm << 16
beq $t0, $t1, 1 # Opcode:000100 rs - rt shifted = (imm_sign_ext32) << 2
# 当 rs - rt == 0 时,PC = PC + 4 + shifted

接下来我们需要一张由instruction确定的控制信号表,这张表决定了各个部件在什么时候应该做什么操作:

  • 我们观察到ALU涉及的计算有加法,减法,或运算,因而我们需要2位ALUOp来指示ALU做哪种具体的运算
  • 我们观察到ALU的两个输入中的第一个一定是rs,而第二个可能是rt或者imm_ext,因而我们需要1位ALUSrc来指示第二位操作数来自哪里
  • 我们观察到并非所有的ALU计算结果都要回写到GRF,因而我们需要1位RegWrite来指示是否需要写入GRF
  • 我们观察到回写的目标寄存器可以是rt或者rd,因而我们需要1位RegDst来指示是否回写入rd
  • 我们观察到并非所有指令都要访存,因而我们需要1位MemRead来指示是否访存指令
  • 我们观察到并非所有时候都能写入DM,因而我们设计1位MemWrite来指示能否写入DM
  • 我们观察到并非所有时候DM的输出都要回写入GRF,因而我们设计1位MemtoReg来指示是否将DM输出写入GRF
  • 我们还需要1位Branch来指示是否分支指令,因为分支指令会影响NPC的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Controller 模块规格
module Controller (
input [5 : 0] Opcode,
input [5 : 0] Func,
output RegDst, // RegDst == 0 写入寄存器时,目标寄存器的编号来自rt
// RegDst == 1 写入寄存器时,目标寄存器的编号来自rd
output Branch, // 是否是分支指令beq
output MemRead, // 是否读取DM
output MemtoReg, // 是否要将DM中读取到的数据回写到GRF
output ALUOp, // ALUOp = 2'b00 add
// ALUOp = 2'b01 sub
// ALUOp = 2'b10 and
// ALUOp = 2'b11 or
output MemWrite, // 是否要写入内存
output ALUSrc, // ALUSrc == 0 ALU的第二个输入来自寄存器
// ALUSrc == 1 ALU的第二个输入来自16_extent_32
output RegWrite // 是否要回写入GRF
)

接下来就是具体的将我们要实现的指令对应到这张控制信号表中:

RegDst Branch MemRead MemtoReg ALUOp_1 ALUOp_0 MemWrite ALUSrc RegWrite
add 1 0 0 0 0 0 0 0 1
sub 1 0 0 0 0 1 0 0 1
nop 1 0 0 0 0 0 0 0 1
ori 0 0 0 0 1 1 0 1 1
lw 0 0 1 1 0 0 0 1 1
sw 0 0 1 0 0 0 1 1 0
lui 0 0 0 0 0 0 0 1 1
beq 0 1 0 0 0 1 0 0 0

因此,当我们站在顶层回看,我们发现利用Logisim构造单周期处理器事实上只需要做三件事:

  • 构造IFUGRFALUDM这四个功能模块,具体的模块规格如下,这是简单的。

  • 考虑我们所涉及的所有指令来设计控制信号,并利用控制信号将功能模块组织起来,这可能要求我们灵活地添加一些多路选择器等,这是抽象而复杂的。

    (考虑需要哪些控制信号 + 每个信号对应功能模块什么样的表现 + 怎么把信号与模块组织起来才能不冲突)

  • 根据指令生成控制信号,这只需要我们稍微分析一下每条指令的功能,因而也是简单的。

1
2
3
4
5
6
7
8
9
10
// IFU 模块规格
// IFU实现PC的更新,并输出instruction
module IFU (
input Branch,
input Zero,
input clk,
input reset,
input [31 : 0] Shift, // 分支指令的imm_sign_ext32
output [31 : 0] instruction
)
1
2
3
4
5
6
7
8
9
10
11
12
// GRF 模块规格
module GRF (
input RegWrite,
input reset,
input clk,
input [4 : 0] A1,
input [4 : 0] A2,
input [4 : 0] A3,
input [31 : 0] WD,
output [31 : 0] W1,
output [31 : 0] W2
)
1
2
3
4
5
6
7
8
9
10
11
12
//ALU 模块规格
`define add 2'b00
`define sub 2'b01
`define and 2'b10
`define or 2'b11
module ALU (
input [1 : 0] ALUOp,
input [31 : 0] A1,
input [31 : 0] A2,
output [31 : 0] WD,
output zero
)
1
2
3
4
5
6
7
8
9
10
11
// DM模块规格
module DM (
input MemRead,
input MemWrite,
input MemtoReg,
input [31 : 0] Address, // 访存地址
input [31 : 0] Data2Write, // 输入数据
output [31 : 0] WD // 输出数据
)
// 对于lw MemRead = 1 MemWrite = 0 MemtoReg = 1
// 对于sw MemRead = 1 MemWrite = 1 MemtoReg = 0

优化

在我们第一版的设计中,BranchZero信号直接接入了EXT这样做不符合我们“高内聚低耦合”的设计理念,我们不希望EXT还要承担判断拓展类型的功能,而是有一个EXTOp专门告诉EXT该做怎样的拓展。

在我们目前实现的8指令中:

  • lwswbeq需要将16位imm符号拓展为32位
  • ori需要将16位imm零拓展为32位
  • lui需要将16位imm右补16位0
  • addsubnop不需要拓展
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// EXTOp模块规格
module EXTOp(
input Branch,
input isLui,
input MemRead,
output [1:0] EXTOp
)
reg EXTOp_reg = 2'b0; // 默认将imm零拓展为32位
always@(*) begin
if (Branch == 1 || MemRead == 1) begin
EXTOp <= 2'b01; // 将imm符号拓展为32位
end
else if (isLui == 1) begin
EXTOo <= 2'b10; // 将imm右补16位0
end
end
assign EXTOp = EXTOp_reg;
endmodule

这样,EXT就只需要按照EXTOp告诉它的方式将16位的输入imm转化为32位的输出imm_ext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// EXT模块规格
module EXT (
input [1:0] EXTOp,
input [15:0] imm,
output [31:0] imm_ext
)
reg [31:0] imm_ext_reg;
always@(*) begin
if (EXTOp == 2'b00) begin
imm_ext_reg = {16{0}, imm};
end
else if (EXTOp == 2'b01) begin
imm_ext_reg = {16{imm[15]}, imm};
end
else if (EXTOp == 2'b10) begin
imm_ext_reg = {imm, 16{0}};
end
end
assign imm_ext = imm_ext_reg;
endmodule

第二个优化是关于WriteBack

WriteBack是将要写回寄存器的值,在8指令中有以下可能:

  • addsubori写回的数据来自ALU_ans
  • sw写回的数据来自DM的输出WD
  • lui写回的数据来自imm_ext
  • nopswbeq不需要写回

同样的思想,我们设计一个WBSource来说明WD的取值来自哪里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// WBSource模块规格
module WBSource (
input MemRead,
input isLui,
output [1:0] WBSource
)
reg [1:0] WBSource_reg = 2'b00; // 默认来自ALU_ans
always@(*) begin
if (MemRead == 1) begin
WBSource_reg = 2'b01; // 来自WD
end
else if (isLui == 1) begin
WBSource_reg = 2'b10; // 来自imm_ext
end
end
assign WBSource = WBSource_reg;
endmodule

思考题

  • Q:上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能?

    A:将IFU看作一个Moore_FSM,状态储存是:PC ,状态转移是:NPC

    ​ 将GRF+ALU+DM看作一个Mealy_FSM,状态储存是:GRF,状态转移时ALU+DM

  • Q:现在我们的模块中 IM 使用 ROMDM使用 RAMGRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。

    A:合理。IM 使用 ROM 是因为IM中要存储大量仅读的数据。DM 使用 RAM 是因为DM涉及大量数据的读写。GRF涉及读写但数据量不大,使用Register访问和更改更方便。

  • Q:在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。

​ A:实现了Splitter模块,将32位instruction按照R型指令的方式拆分。目的是便于确定具体指令。

  • Q:事实上,实现 nop 空指令,我们并不需要将它加入控制信号真值表,为什么?

    A:事实上,nop等价于sll $0, $0, $0,我们只需要在检测到Func字段为000000时把它也当作加法处理即可。

  • Q:阅读 Pre“MIPS 指令集及汇编语言” 一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。

  • A:

    1. 应当测试sub指令,因为sub指令涉及了特有的Opcode的处理

    2. 在测试add指令时未考虑操作0号寄存器

    3. lwsw指令未涉及负跳转

    4. beq跳转指令的未涉及负跳转

    5. 未涉及nop指令的测试