CPU设计

概述

本次我设计的logisim单周期CPU支持21条指令,采用多个模块进行设计。

所采用模块化设计为:

  • IFU:指令单元,接受指令地址PC来给出当前周期处理的指令
  • GRF:寄存器模块,通过该模块统一进行32个寄存器的读写
  • NPC:计算下一条指令的地址,接受jump,branch等指令来支持跳转操作
  • ALU:进行运算操作,该ALU操作码为4为
  • DM:内存组,通过内置的RAM实现
  • ControlUnit:指令控制单元,通过接受opfunc来发出指令,控制数据通路中的MUX

image-20231029131416770

支持指令

支持21条指令,为:

  • R指令:add,addu,sub,or,sll,sllv,slt,sltu
  • I指令:ori,lui,beq,sw,lw,sh,sb,lh,lb
  • J指令:j,jr,jal,jalr

数据通路模块定义

IFU:指令单元

该模块内部包含PC(程序计数器)和IM(指令存储器)。PC通过logism内置的寄存器实现, IM通过Logisim内置的ROM实现(容量为24*32bit)。可以根据PC的值从IM取出对应的指令。

image-20231029131530973

端口定义

信号名 方向 位宽 具体翻译
clk I 1 时钟信号
reset I 1 复位信号,为异步复位
PC_next I 32 下一条执行指令的地址
inStr O 32 当前正在执行的指令
PC O 32 当前正在执行指令的地址

功能定义

通过指令地址PC取得指令。

该组件支持异步复位,复位后输出的是地址为0x00003000处的指令。

image-20231029132906178

NPC:指令地址单元

image-20231029132339662

端口定义

信号名 方向 位宽 具体翻译
PC I 32 当前正在执行指令的地址
imm I 26 对于J型指令时跳转的偏移量
offest I 16 对于I型指令跳转时的偏移量
if_branch I 1 是否进行I型指令的分支跳转
if_jump I 1 是否进行J型指令的直接跳转
rs I 32 rs寄存器中的值
jumpSrc I 1 跳转的地址选择是imm还是rs
PC_next O 32 输出计算后得到的下一个PC地址
PC+4 O 32 输出当前输入PC值加4得到的PC值

功能定义

指令一共有三种情况:

  1. 不跳转,则PC=PC+4
  2. beq为代表的跳转,指令计算方式为PCoffest的计算,计算结果为PC_branch
  3. J型指令跳转,指令计算有两种,通过jumpSrc进行选择,计算结果为PC_jump
    1. j为代表的跳转,指令计算方式为立即数immPC的计算
    2. jr为代表的跳转,指令计算方式为寄存器中储存的地址,即输入的rs寄存器中储存的值

同时,以为jal,jalr等指令会存储返回的地址,需要输出一个PC+4的地址

image-20231029133111295

GRF:通用寄存器组

采用了P0课下实现的GRF。

image-20231029133738745

端口定义

信号名 方向 位宽 具体翻译
clk I 1 时钟信号
reset I 1 复位信号,为异步复位
WE I 1 写使能信号
A1 I 5 读取编号1
A2 I 5 读取编号2
A3 I 5 写地址
WD I 32 写数据
RD1 O 32 输出读取编号1对应寄存器的值
RD2 O 32 输出读取编号2对应寄存器的值

功能定义

实现了寄存器的读写功能,利用了MUX和DUX。

image-20231029134011200

ALU:计算单元

image-20231029134047608

端口定义

信号名 方向 位宽 具体翻译
inA I 32 输入数据1
inB I 32 输入数据2
op I 4 计算类型
shift I 5 偏移量
result I 32 计算结果
zero O 1 计算结果是否为0

功能定义

实现了如下操作加、建、与、或、异或、与或、左逻辑移位、右逻辑移位、右算数移位、大小判断操作。对应ALU操作码如下:

操作码 操作
0000 A+B
0001 A-B
0010 A and B
0011 A or B
0100 A xor B
0101 ~ ( A OR B )
0110 B << shift
0111 B >> shift
1000 B >>> shift
1001 B << 16
1010 A < B
image-20231029134308146

DM:内存

利用内置的RAM模拟了内存,可以对内存中的数据进行读写。支持对字、半字、字节的读写操作。

image-20231029134825713

端口定义

信号名 方向 位宽 具体翻译
address I 32 访问的内存地址
WD I 32 写入内存的数据
WE I 1 写使能
clk I 1 时钟信号
byte I 1 是否对字节进行读取
half I 1 是否对半字进行读写
RD O 32 读出内存的数据

功能定义

使用logisim内置的RAM模拟了内存,支持对字、半字、字节的读写操作。

对于读取:

  • RAM为按字索引,故将输入的地址右移两位,得到对应在RAM中的数据,进行读写
  • 按字读取,则此时if_halfif_byte均为低电平,直接输出结果
  • 按字节读取,此时if_byte为高电平,操作逻辑为:
    1. 取输入地址低两位,向左偏移3位得到对应的数据偏移量shift。即如果低两位为01,代表读写8-15为的数据,需要左移三位。
    2. 对于读,将从RAM中读出的数据右移shift位,再截取低8位,得到结果并输出
    3. 对于写,按照位运算的逻辑,将该地址在RAM中以存储的值dm和输入数据中对应位的数据进行操作,再存入RAM中。能够实现依赖于RAM的阻塞赋值特性
  • 按半字读取,此时if_half为高电平,实现逻辑与按字节读取类似
image-20231029135141813

Control Unit:控制单元

个人认为是CPU最关键的部分,依靠指令的opfunc部分生成控制逻辑,控制电路中的MUX单元,实现具体的运算。

image-20231029140159051

端口定义

信号名 方向 位宽 具体翻译
op I 6 指令中的op
func I 6 指令中的func
regWrite O 1 GRF写使能信号
regDst O 1 GRF的写入地址是rt还是rd
regSrc O 1 GRF的写入数据来源是ALU还是内存
regRa O 1 GRF的写入地址是regSrc还是31
aluSrc O 1 ALU的输入数据2来自rt还是指令中的offest字段
extendType O 1 对指令中offest字段extand的方式是sign还是zero
shiftSrc O 1 ALU的shift输入来自rs还是指令中的shamt字段
memWrite O 1 DM的写使能信号
branch O 1 NPC的if_branch信号
jump O 1 NPC的if_jump信号
jumpSrc O 1 NPC的jumpSrc输入来自指令中的imm字段还是rs
aluControl O 4 ALU的操作码

功能定义

根据指令的op字段和func字段,生成CPU的控制信号。

具体逻辑不表,给出对应的信号表:

0-1 enable rt-rd regDst-31 alu-mem regSrc-PC+4 rt-imm/off sign-zero rs-shamt enable enable enable imm-rs
op regWrite regDst regRa regSrc regPc aluSrc extendType shiftSrc memWrite branch jump jumpSrc aluControl alu aluOp func
add 000000 1 1 0 0 0 0 x 0 0 0 0 x + 0000 000 100000
addu 000000 1 1 0 0 0 0 x 0 0 0 0 x + 0000 000 100001
sub 000000 1 1 0 0 0 0 x 0 0 0 0 x - 0001 000 100010
or 000000 1 1 0 0 0 0 x 0 0 0 0 x or 0011 000 100101
sll 000000 1 1 0 0 0 0 x 1 0 0 0 x << 0110 000 000000
sllv 000000 1 1 0 0 0 0 x 0 0 0 0 x << 0110 000 000100
slt 000000 1 1 0 0 0 0 x 0 0 0 0 x < 1010 000 101010
ori 001101 1 0 0 0 0 1 1 0 0 0 0 x or 0011 011
lui 001111 1 0 0 0 0 1 0 0 0 0 0 x <-16 1001 100
sltu 000000 1 1 0 0 0 0 x 0 0 0 0 x < 1010 000 101011
beq 000100 0 x x x x 0 0 0 0 1 0 x - 0001 010
j 000010 0 x 0 x 0 x x x 0 0 1 0 x x x
jr 000000 0 x 0 x 0 x x x 0 0 1 1 x x x 001000
jal 000011 1 1 1 0 1 x x x 0 0 1 0 x x x
jalr 000000 1 1 0 x 1 x x x 0 0 1 1 x x x 001001
sw 101011 0 x x x 0 1 0 0 1 0 0 0 + 0000 001
sh 101001 0 x x x 0 1 0 0 1 0 0 0 + 0000 001
sb 101000 0 x x x 0 1 0 0 1 0 0 0 + 0000 001
lw 100011 1 0 0 1 0 1 0 0 0 0 0 0 + 0000 001
lh 100001 1 0 0 1 0 1 0 0 0 0 0 0 + 0000 001
lb 100000 1 0 0 1 0 1 0 0 0 0 0 0 + 0000 001

内部构成

使用了三个子模块:contorl1control2JumpControl。其中,各模块功能为:

  1. control1:负责根据op字段生成和func无关的信号

    control1control2之间通信的aluOp的含义:

    aluOp 含义
    000 依赖funct
    001 A + B
    010 A - B
    011 A or B
    100 B << 16
  2. control2:负责根据opfunc字段生成其他信号,包括aluControlshiftSrc

  3. JumpControl:负责生成和jump相关指令的控制信号,包括regRaregPcjumpSrcjump

image-20231029141448557

CPU测试

主要使用Mars生成测试码,之后手动导入logisim中进行测试,使用了助教提供的jar包测试工具。

自动测试工具

通过python程序将test.asm转化为16进制指令码1.txt,并通过正则表达式匹配得到ROM对应的代码区域,并形成新的电路文件cpu_test.circ

1
2
3
4
5
6
7
8
9
10
11
12
import os
import re
#generate ROM-File
command="java -jar Mars4_5.jar test.asm nc mc CompactTextAtZero a dump .text HexText 1.txt"
os.system(command)
content = open("1.txt").read()

#write ROM-File to Circle—File
cur = open("cpu.circ", encoding="utf-8").read()
cur = re.sub(r'addr/data: 5 32([\s\S]*)</a>',"addr/data: 5 32\n" + content + "</a>", cur)
with open("cpu_test.circ","w", encoding="utf-8") as file:
file.write(cur)

测试代码

忘记保存了,总的策略是对$1-$31为都测试一次,利用python进行批量生产数据。利用lui和ori的组合指令可以实现临界数据的生成。

思考题

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

    状态存储模块:IFU

    状态转移模块:NPC

  2. 现在我们的模块中 IM 使用 ROM, DM 使用 RAM, GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。

    不完全合理。

    • RAM较大的容量满足了DM对内存的模拟,可以满足读写要求;
    • GRF是通用寄存器堆,采用分开的寄存器实现可以满足其读写速度;
    • 但是ROM的实现有些奇怪,这样的设计使得CPU称为了“一次性CPU”,只能执行特定的命令,不能在程序执行过程中增删指令。作为学习过程中的初级CPU,这样做是可以的,因为无论编写的mips程序还是测评都是单个程序的测试,但是也只能适用于教学,不是符合真实情景的IM设计。
    • 至于改进方案,可以适用RAM实现,但在执行过程中对RAM进行指令读入,在logisim中难以实现,在verilog中可以实现。
  3. 在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。

    实现了,见上。

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

    由于我实现sll指令,那么nop指令相当于sll $0,$0,0,一方面,左移0位不对原有寄存器造成印象,另一方面,不允许对0寄存器的写操作,故没有影响。

    即使CPU没有设置sll指令,nop指令也不会对电路中任何元件进行操作,对电路没有任何影响。

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

    1. 测试数据过弱,没有很好地覆盖临界情况。
    2. 测试寄存器过少,没有测试绝大部分的寄存器,只测试了几个寄存器。
    3. 逻辑过于简单,没有实现beq嵌套等操作。