使用 logisim 设计实现单周期 CPU
相信大家看了历史上的计算机还有基本数字逻辑电路这两篇笔记之后,肯定也像我一样迫不及待地想要自己动手实现一个 CPU 了吧,所以这篇笔记记录设计实现一块简单的单周期 CPU (MIPS 模型机)的过程。
MIPS 简介
MIPS 是 Microprocessor without interlocked piped stages 的缩写,翻译为无内部互锁流水级的微处理器。在设计理念上MIPS 架构强调软硬件协同提高性能,同时简化硬件设计,这个架构极大地影响了后来的精简指令集架构。
MIPS 指令格式
MIPS 指令中各字段名称及含义如下:
op:指令的基本操作,通常称为操作码(opcode);
rs:第一个源操作数寄存器(register source);
rt:第二个源操作数寄存器(register target);
rd:用于存放操作结果的目的寄存器(register destination);
shamt:位移量(shift amount);
func:一般称为功能码(function code),用于指明 op 字段中操作的特定变式。
R型指令:
op | rs | rt | rd | shamt | func |
---|---|---|---|---|---|
6位(31~26) | 5位(25~21) | 5位(20~16) | 5位(15~11) | 5位(10~6) | 6位(5~0) |
I型指令:
I型指令用低 16 位表示一个 16 位的常量或者地址
op | rs | rt | constant or address |
---|---|---|---|
6位(31~26) | 5位(25~21) | 5位(20~16) | 16位(15~0) |
J型指令:
J型指令则用低 26 位表示地址参数
op | address |
---|---|
6位(31~26) | 5位(25~0) |
这样的设计保持了所有的指令长度相同,但不同类型的指令采用不同的指令格式。
寻址方式
R 型指令
寄存器寻址,操作数在寄存器中,指令地址字段给出寄存器的地址:
I 型指令
立即数寻址,操作数直接在指令代码中给出:
基址寻址,操作数在存储器中,指令地址字段给出一个基址寄存器和一形式地址,基址寄存器的值与形式地址之和是操作数的内存地址:
PC 相对寻址,由程序计数器 Program Counter 作为基址寄存器,指令中给出的形式地址作为位移量,两者之和是操作数的内存地址:
J 型指令:
伪直接寻址(pseudodirect),跳转地址由指令中的低 26 位地址左移两位与 PC 的高 4 位相连而成。
指令集
指令类型 | 指令 | 功能 | 说明 |
---|---|---|---|
R | add rd rs rt | R[rd] R[rs] + R[rt] | 加运算:将寄存器 rs 和寄存器 rt 的值相加,结果送寄存器 rd |
R | sub rd rs rt | R[rd] R[rs] - R[rt] | 减运算:将寄存器 rs 和寄存器 rt 的值相减,结果送寄存器 rd |
R | and rd rs rt | R[rd] R[rs] & R[rt] | 与运算:将寄存器 rs 和寄存器 rt 的值按位与,结果送寄存器 rd |
R | or rd rs rt | R[rd] R[rs] | R[rt] | 与运算:将寄存器 rs 和寄存器 rt 的值按位或,结果送寄存器 rd |
I | lw rt rs imm16 | Add = R[rs] + Signext(imm16);R[rt] M[Add] | 取字:寄存器 rs 和立即数 imm16(符号扩展至32位)相加得到内存地址,从内存该地址单元读取数据送寄存器 rt |
I | sw rt rs imm16 | Add = R[rs] + Signext(imm16);M[Add] R[rt] | 取字:寄存器 rs 和立即数 imm16(符号扩展至32位)相加得到内存地址,寄存器 rt 的数据写入内存该地址单元 |
I | beq rs rt imm16 | if (R[rs] - R[rt] == 0) then PC PC + Signext(imm16)<<2 | 分支:如果寄存器 rs 和寄存器 rt 相等,则将立即数 imm16(符号扩展至32位)乘 4 与 PC 相加转移到对应指令位置,否则顺序执行 |
J | j target | PC(31:2) PC(31:28) || target(25:0) | 跳转:当前 PC 的高 4 位与 target (26位) 拼接成 30 位目标地址送 PC (31:2) |
注意:基于 MIPS 的系统通常将用户可用的内存分割成三个部分,第一部分,接近地址空间的底部,开始于
,是代码段,保存的是程序的指令;第二部分,在代码段的上面,称为数据段,它被进一步分成两部分,静态数据段开始于
,包含目标代码,动态数据紧靠静态数据的上面;第三部分为程序堆栈段,存在于地址空间顶部,从
开始,当程序向堆栈段压入变量时,操作系统会自动向下(数据段)方向扩展。
这也就解释了为什么 j target 指令使用了 PC 的高 4 位,因为位于代码段的指令地址空间高 4 位永远是 0000 ,而之所以要将指令中 26 位的数据左移两位与 PC 高 4 位进行拼接,是因为 MIPS 是按字节编址的,一个字节是 8 位,一个字是 4 个字节 32 位,所以地址必须是 4 的倍数,左移两位即将 26 位数据乘 4 对齐地址。
R型指令 | OP | Rs | Rt | Rd | Shamt | Func |
---|---|---|---|---|---|---|
add rd, rs, rt | 000000 | rs | rt | rd | XXXXX | 100000 |
sub rd, rs, rt | 000000 | rs | rt | rd | XXXXX | 100010 |
and rd, rs, rt | 000000 | rs | rt | rd | XXXXX | 100100 |
or rd, rs, rt | 000000 | rs | rt | rd | XXXXX | 100101 |
I型指令 | OP | Rs | Rt | constant or address |
---|---|---|---|---|
lw rt , rs, imm16 | 100011 | rs | rt | imm16 |
sw rt, rs, imm16 | 101011 | rs | rt | imm16 |
beq rs, rt, imm16 | 000100 | rs | rt | imm16 |
J型指令 | OP | address |
---|---|---|
j target | 000010 | target |
寄存器
设有 32 个 32 位的通用寄存器(general purpose register , GPR),一个 PC 寄存器,不设有乘除法、浮点数和异常处理寄存器。
32 个通用寄存器的使用约定如下:
寄存器编号 | 寄存器名称 | 用途 |
---|---|---|
0 | $zero | 常数0 |
1 | $at | 保留给汇编器使用 |
2~3 | $v0 ~ $v1 | 结果值和表达式求值 |
4~7 | $a0 ~ $a3 | 参数 |
8~15 | $t0 ~ $t7 | 临时变量 |
16~23 | $s0 ~ $s7 | 数据寄存器 |
24~25 | $t8 ~ $t9 | 其他临时变量 |
26~27 | $k0 ~ $k1 | 保留给操作系统使用 |
28 | $gp | 全局指针 |
29 | $sp | 栈指针 |
30 | $fp | 帧指针 |
31 | $ra | 返回地址 |
数据通路设计
所有指令执行周期固定为单一时钟周期,即 CPI = 1(Cycles Per Instruction)。通路设计使用哈佛体系结构,即使用指令存储区(Instruction Memory,IM)数据存储区(Data Memory,DM)分别保存指令和数据。我们首先为每类指令设计独立的数据通路,然后再考虑数据通路合并。
指令执行的共性:
-
根据 PC 从指令存储器读取指令,取指令后,PC + 4;
-
模型机除 J 型指令之外的其他 7 条指令在读取寄存器后,都要使用 ALU:
- add/sub/and/or 指令用 ALU 完成算术逻辑运算
- lw/sw 指令用 ALU 计算数据地址
- beq 指令用 ALU 进行比较(减法运算)
-
指令执行一般会分为如下几个步骤:
- 取指令:根据PC访问指令存储器获得指令,然后 PC + 4;
- 读寄存器:根据指令格式读取相应寄存器操作数;
- ALU 运算:在 ALU 完成相应的算术逻辑运算;
- 数据存取:lw/sw 指令的数据存储器访问;
- 写寄存器:运算类指令和 lw 指令要把数据写入寄存器;
我们根据指令执行的共性和指令执行的一般步骤确定数据通路所需的部件和部件之间的连接关系。在这个过程中我们使用表格记录数据通路部件输入端的输入来源。
1. 取指和 PC 自增数据通路
功能描述:
- 取指:IM address PC;instruction == IM[PC]
- PC 自增:PC PC + 4
所需部件:PC,Adder(实现 PC 加 4),IM(指令存储器)
2. R 型指令数据通路
功能描述(以 add 指令为例):R[rd] R[rs] + R[rt]
所需部件:GRF(寄存器堆),ALU
3. 取数指令数据通路
功能描述:R[rt] DM[ R[s] + Signext(imm16) ]
所需部件:GRF、ALU、Signext(符号扩展单元)、DM(数据存储器)
4. 存数指令数据通路
功能描述:DM[ R[s] + Signext(imm16) ] R[rt]
所需部件:GRF、ALU、Signext、DM
5. R 型指令与访存指令数据通路合并
输入端数据源出现多个时需要增加 MUX(多路选择器)对数据源进行选择。
6. 分支指令数据通路
功能描述:
if (R[rs] - R[rt] == 0)
then PC (PC + 4) + Signext(imm16) << 2
else PC (PC + 4)
所需部件:GRF,ALU,Signext,Shift(移位器),Nadd(实现 (PC + 4) + Signext(imm16) << 2)
7. 数据通路再合并
合并后的数据通路支持 R 型指令、内存访存指令和 beq 指令。
根据上图,我们需要增加 4 个二选一 MUX:
- PC 输入端数据源选择 MUX,选择控制信号 PCSrc
- GRF 写入端地址选择 MUX,选择控制信号 RegDst
- GRF 写入端数据源选择 MUX,选择控制信号 MemtoReg
- ALU 输入端 B 数据源选择 MUX,选择控制信号 ALUSrc
8. 扩展实现跳转指令
功能描述:PC (PC + 4)[31:28] 连接 target(26)<<2
所需部件:Jumpadd
根据上图我们需要再增加 1 个二选一 MUX,跟在上一个 PC 输入端数据源选择 MUX 后面,选择控制信号为 Jump。
控制器设计
ALU 控制:
输入 | ALUCtrl | ALU 运算 |
---|---|---|
A和B | 0000 | A & B |
A和B | 0001 | A | B |
A和B | 0010 | A + B |
A和B | 0110 | A - B |
8 个控制信号:
控制信号 | 失效时作用(=0) | 有效时作用(=1) |
---|---|---|
RegDst | GRF 写入端地址选择 Rt | GRF 写入端地址选择 Rd |
RegWrite | 无 | 把数据写入 GRF 中对应的寄存器 |
ALUSrc | ALU 输入端 B 数据源选择 R[rt] | ALU 输入端 B 数据源选择 Signext |
PCSrc(Branch & Zero) | PC 输入端输入源选择 PC + 4 | PC 输入端输入源选择 beq 指定的目的地址 |
Jump | PCSrc 控制选择的地址 | J 指令指定的目的地址 |
MemRead | 无 | 数据存储器 DM 读数据(输出) |
MemWrite | 无 | 数据存储器 DM 写数据(输入) |
MemtoReg | GRF 写入端数据源来自 ALU 的输出 | GRF 写入端数据源来自 DM 的输出 |
控制器分成两个部分:主控单元与 ALU 控制单元
- 主控单元输入指令操作码字段 Op (指令 31:26 位),输出 8 个控制信号和 ALU 所需的 2 两位输入 ALUOp
- ALU 控制单元输入主控单元生成的 2 位 ALUOp 和功能码字段 Func (指令 5:0 位),输出 ALU 运算控制信号 ALUCtrl(3 位)
ALUOp 指明 ALU 的运算类型:
- 00:访存指令所需加法
- 01:beq 指令所需减法
- 10:R 型指令 Func 决定
指令 | func 字段 | ALUOp | ALU 运算类型 | ALUCtrl |
---|---|---|---|---|
lw | XXXXXX | 00 | 加 | 010 |
sw | XXXXXX | 00 | 加 | 010 |
beq | XXXXXX | 01 | 减 | 110 |
add | 100000 | 10 | 加 | 010 |
sub | 100010 | 10 | 减 | 110 |
and | 100100 | 10 | 与 | 000 |
or | 100101 | 10 | 或 | 001 |
模块设计
IM 模块
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
A[4:0] | i | 5 位地址 |
RD[31:0] | o | 32 位指令 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 取指令 | 根据 PC 从 IM 中取出指令 |
IFU 模块
IFU 模块使用了 IM 模块。
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
Clk | i | 时钟信号 |
Reset | i | 复位信号,1:复位;0:无效 |
Zero | i | ALU 计算结果是否为 0 的标志信号,1:计算结果结果为 0;0:计算结果非 0 |
Branch | i | 当前指令是否为 beq 指令,1:是;0:不是 |
Jump | i | 当前指令是否为 J 指令,1:是;0:不是 |
Imm32 | i | beq 指令的 singext 立即数 |
target28 | i | J 指令指定的目的地址 |
Instr | o | 32 位 MIPS 指令 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 复位 | 当复位信号有效时,PC 被设置为 0x00000000 |
2 | 取指令 | 根据 PC 从 IM 中取出指令 |
3 | 计算下一条指令 | 如果当前指令不是 beq 指令也不是 J 指令,则 PC + 4;如果当前指令是 beq 指令,且 Zero == 1,则 PC = PC + 4 + signext;如果当前指令是 J 指令,则 PC = (PC + 4)[31:28] 连接 target[25:0] |
GRF 模块
32 个通用寄存器组成通用寄存器堆(General Register File,GRF)。
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
WD[31:0] | i | 写入数据输入 |
WA[4:0] | i | 写寄存器地址 |
RA1[4:0] | i | 读寄存器地址1 |
RA2[4:0] | i | 读寄存器地址2 |
Clk | i | 时钟信号 |
Reset | i | 复位信号,0 无效,1 复位 |
RegWrite | i | 是否可以写入控制信号,0 不可以,1可以 |
RD1[31:0] | o | 寄存器数据输出1 |
RD2[31:0] | o | 寄存器数据输出2 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 复位 | 当复位信号有效时,所有寄存器被设置为 0x00000000 |
2 | 读寄存器 | 根据输入的寄存器地址输出数据 |
3 | 写寄存器 | 根据输入的地址,将输入的数据写进寄存器 |
ALU 模块
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
A[31:0] | i | 32 位输入数据 1 |
B[31:0] | i | 32 位输入数据 2 |
ALUCtrl[2:0] | i | 控制信号,000:与;001:或;010:加;110:减 |
Result[31:0] | o | 32 位结果数据输出 |
Zero | o | A和B是否相等的标志,1:相等;0:不相等; |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 与 | A&B |
2 | 或 | A |
3 | 加 | A+B |
4 | 减 | A-B |
5 | 判零 | A=?B |
DM 模块
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
Clk | i | 时钟信号 |
Reset | i | 复位信号,1:复位;0:无效 |
MemWrite | i | 读写控制信号,1:写操作;0:无效 |
MemRead | i | 读写控制信号,1:读操作;0:无效 |
A[4:0] | i | 操作寄存器地址 |
WD[31:0] | i | 写入内存的 32 位数据 |
RD[31:0] | o | 32 位数据输出 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 复位 | 当复位信号有效时,所有数据被设置为 0x00000000 |
2 | 读 | 根据输入的寄存器地址读出数据 |
3 | 写 | 根据输入的地址,将数据写入内存 |
EXT 模块
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
In[15:0] | i | 16 位数据输入 |
Out[31:0] | o | 32位数据输出 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 高位符号扩展 | 高 16 位补符号位 |
Controler(主控单元)模块
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
Op[5:0] | i | 6 位 opcode 字段 |
RegDst | o | 寄存器写地址控制 |
ALUSrc | o | ALU 第二操作数选择控制 |
RegWrite | o | GRF 写入控制 |
MemRead | o | DM 读信号 |
MemWrite | o | DM 写信号 |
MemToReg | o | GRF 写入数据的选择信号 |
Branch | o | 判断是否为 beq 指令的信号 |
ALUOp[2:0] | o | 传递给 ALUControler 与 func[5:0] 共同确定 ALUCtrl[3:0] 的信号 |
Jump | o | 判断是否是 J 指令的信号 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 产生控制信号 | 产生主控信号 |
ALUControler(ALU 控制单元)模块
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
ALUOp[2:0] | i | 由 opcode[5:0] 产生的控制信号 |
Func[5:0] | i | 6 位 func 字段 |
ALUCtrl[2:0] | o | 3 位 ALU 操作选择信号 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 产生 ALU 控制信号 | 产生 ALU 控制信号,控制 ALU 进行不同的运算 |
Shift 模块
模块接口:
信号名 | 方向(input or output) | 描述 |
---|---|---|
Signext | i | 16 位立即数经符号位扩展之后的 32 位数据 |
ExtNum | i | 向左移位的移动位数 |
Result | o | 移位后的数据输出 |
功能定义:
序号 | 功能名称 | 功能描述 |
---|---|---|
1 | 数据左移指定位数 | 输出数据左移指定位数的结果 |
模块连接及功能验证
为了方便验证 CPU 设计,我们再增加一条 ori 指令:
指令类型 | 指令 | 功能 | 说明 |
---|---|---|---|
I | ori rt rs imm16 | R[rt] R[rs] + Signext(imm16) | 或立即数:将寄存器 rs 和立即数 imm16(符号扩展至32位)的值相加,结果送寄存器 rt |
I型指令 | OP | Rs | Rt | constant |
---|---|---|---|---|
ori rt, rs, imm16 | 001101 | rs | rt | imm16 |
ori 指令数据通路:
Controler 更改:
ALUControler 更改:
指令 | func 字段 | ALUOp | ALU 运算类型 | ALUCtrl |
---|---|---|---|---|
ori | XXXXXX | 11 | 或 | 001 |
将所有设计好的模块按照数据通路进行连接:
现在让我们编写一些测试程序以验证 CPU 的功能:
ori $a0, $0, 1999 // 将 $0 寄存器中的内容与立即数 0x000007cf 进行或运算,结果储存在 $a0 寄存器中
ori $a1, $a0, 111 // 将 $a0 寄存器中的内容与立即数 0x000006f 进行或运算,结果储存在 $a1 寄存器中
add $t0, $a0, $a1 // 将 $a0 寄存器与 $a1 寄存器中的内容相加,结果储存在 $t0 寄存器中
add $t1, $a0, $t0 // 将 $a0 寄存器与 $t0 寄存器中的内容相加,结果储存在 $t1 寄存器中
sub $s0, $t1, $a1 // 将 $t1 寄存器的内容减去 $a1 寄存器中的内容,结果储存在 $s0 寄存器中
sub $s1, $t1, $s0 // 将 $t1 寄存器的内容减去 $s0 寄存器中的内容,结果储存在 $s1 寄存器中
and $s2, $s0, $s1 // 将 $s0 寄存器与 $s1 寄存器中的内容进行与运算,结果储存在 $s2 寄存器中
and $s3, $s1, $s2 // 将 $s1 寄存器与 $s2 寄存器中的内容进行与运算,结果储存在 $s3 寄存器中
or $t3, $s0, $s1 // 将 $s0 寄存器与 $s1 寄存器中的内容进行或运算,结果储存在 $t3 寄存器中
sw $t1, 4($0) // 将 $t1 寄存器的值存储到 $0 寄存器的值加上偏移量 4 所指向的 RAM 地址中
sw $s2, 8($0) // 将 $s2 寄存器的值存储到 $0 寄存器的值加上偏移量 8 所指向的 RAM 地址中
beq $s4, $t1, 0x0004 // 判断 $s4 寄存器的值与 $t1 寄存器的值是否相等,相等则跳转到 PC + 0x0004 << 2
lw $s4, 4($0) // 将 $0 寄存器的值加上偏移量 4 所指向的 RAM 地址存储的数据加载到 $s4 寄存器中
lw $s5, 8($0) // 将 $0 寄存器的值加上偏移量 8 所指向的 RAM 地址存储的数据加载到 $s5 寄存器中
or $s6, $s4, $s5 // 将 $s4 寄存器与 $s5 寄存器中的内容进行或运算,结果储存在 $s6 寄存器中
j 0x00002c // 将程序直接跳转到 PC[31:28] 连接 0x00002c[25:0]
beq $s4, $t1, 0xfffa // 判断 $s4 寄存器的值与 $t1 寄存器的值是否相等,相等则跳转到 PC + 0xfffa << 2
程序对应的十六进制编码:
340407cf
3485006f
00854020
00884820
01258022
01308822
02119024
02329824
02115825
ac090004
ac120008
12890004
8c140004
8c150008
0295b025
0800000b
1289fffa
经过程序验证,各项指令执行正常!
这种单周期的 CPU 设计所有指令周期为固定单一时钟周期,时钟周期由最长的指令执行时间决定,但其实某些指令可以在更短的时间内完成,因此使用多周期的解决方案即将指令执行分解为多个步骤可提升性能,对此感兴趣的小伙伴可以寻找资料继续学习!
logisim 源文件及数据通路 excel 源文件已上传至百度云(提取码:iv2c)。
参考链接:
1.计算机组成与设计:硬件/软件接口
2.北航计算机学院计算机组成原理课程设计
3.北航计算机学院计算机组成原理课程PPT