声明
P3对alu、grf的设计非常不合理,直接将各个可能的操作数接入alu,而且默认grf要读的寄存器是rs、rt。但因为足够应付需求(懒得重构)就保留了这种错误方法,但在后面的设计中都注意规避了。
前置理解
内存
1. 哈佛结构(本CPU应用的) 内存 = 数据内存(DM) + 指令内存(IM)
- IM:
CPU只会从IM读取指令,永远不会写入指令
使用ROM - DM:
可读(lw)可写(sw)
使用RAM
2. 冯·诺伊曼结构 指令和数据存放在同一个物理内存RAM中
指令处理
- 用RTL描述每条指令
-
:数据传送方向; - R[a] : 寄存器 a;
- M[a] : 主存中地址为a的单元;
- PC :程序计数器
- f(data) : 表示对数据data进行f操作
-
- 加指令:
- R/I/J
- 看清指令的RTL描述,把数据流搞清楚
- 测试指令直接使用
$3这样的寄存器,更易读
Debug
Wrong instruction behavior: in MIPS code 0x3c09cba1, we got ‘@00003030: $9 <= 0xcba10000’ when we expected ‘@0000303c: $11 <= 0x00030000’. The wrong behavior was next to the correct execution of MIPS code 0x3c083bc2.
| 部分 | 含义 |
|---|---|
| MIPS code 0x3c09cba1 | 实际出错的机器码(十六进制) |
| ‘@00003030: $9 <= 0xcba10000’ | 当PC = 00003030时,0xcba10000被写入$9 |
| next to … | 出错前的最后一条正确执行的指令(帮助你定位上下文) |
-
机器码
指令:转化为32位机器码,翻译为指令,例如:opcode rs rt immediate 001101 00110 00110 0110011001101100 ori $6, $6, 0x666c -
PC:起始地址为00003000
简化操作的logisim部件
- odd/even partry:odd/even输出1
- Bit Finder:找到最高位/最低位1/0的位置
- negator:取负
- Bit Adder:不要傻傻地自己去写 (TT…
易错
- 输出的regWrite等信号是否为终值?
- 即:到底有没有写入?
模块设计
IM (指令存储器)
- 核心任务:根据PC中的地址,从ROM中读出指令输出
- 整体设计
- 地址转换逻辑:
- 地址偏移:pc - 0x00003000
- 将PC的字节地址转换为ROM的字地址
- 地址转换逻辑:
- 模块规格
信号名 方向 位宽 描述 PC I 32 指令的地址 Instruction O 32 指令机器码
GRF(通用寄存器组)
-
核心任务:为ALU提供快速的数据源,并接收计算结果
-
整体设计
- 读数据:指令最多需要同时读取
两个不同地址的数据 -两个读端口 - 写数据
-
0 号寄存器的值始终保持为 0。其他寄存器初始值(复位后)均为 0,无需专门设置
- 读数据:指令最多需要同时读取
-
模块规格:
信号名 方向 位宽 描述 reset I 1 异步复位信号 clk I 1 时钟信号 RA1 I 5 读地址1 RA2 I 5 读地址2 WA I 5 写地址 WD I 32 写数据 RegWrite I 1 写使能信号 RD1 O 32 读数据1 RD2 O 32 读数据2 注:这里的“地址”指的是GRF中的第n个reg
ALU
- 整体设计
- 能够实现多种运算功能,并根据op选择对应功能
op 运算 0000 无符号 rs + rt0001 无符号 rs - rt0010 rs & imm0011 rs | imm0100 16位立即数加载至高位 0101 rs + imm
- 能够实现多种运算功能,并根据op选择对应功能
- 模块规格
信号名 方向 位宽 描述 op I 4 选择运算功能 rs I 32 rs value rt I 32 rt value immEXT I 32 扩展后的立即数 result O 32 运算结果 equal O 1 判断是否相等
DM(数据存储器)
- 核心任务
根据
CPU提供的地址,进行读或写操作。它连接了 CPU 的核心与外部数据存储。 - 整体设计
-
使用
RAM实现,容量为3072 × 32bit,应具有异步复位功能,复位值为0x00000000。 -
起始地址:
0x00000000。地址范围:0x00000000 ~ 0x00002FFF。 -
RAM 应使用
双端口模式,即设置 RAM 的Data Interface属性为Separate load and store ports。
-
- 模块规格
信号名 方向 位宽 描述 Address I 32 读/写地址 WriteData I 32 要写入的数据 MemWrite I 1 写使能信号 MemRead I 1 读使能信号 clk I 1 时钟信号 reset I 1 异步复位信号 MemData O 32 读出的数据
EXT(扩展单元)
- 核心任务 把16位立即数扩展到32位
- 整体设计
能够对立即数进行
零扩展/符号扩展 - 模块规格
信号名 方向 位宽 描述 immIn I 16 待扩展的立即数 op I 1 扩展方式选择信号:
1 - 符号扩展
0 - 零扩展immOut O 32 扩展后的立即数
Controller
-
核心任务:根据当前正在执行的
指令输出对应控制信号 -
整体设计 解码逻辑 = 和逻辑 + 或逻辑
-
和逻辑:识别
- 将输入的机器码识别为相应指令
-
或逻辑:生成
- 根据输入指令生成对应的控制信号
- 控制信号表;
控制信号\指令 add sub ori lw sw beq lui addi j RegWrite 1 1 1 1 0 0 1 1 0 ALUop 0000 0001 0011 0101 0101 0000 0100 0101 0000 MemWrite 0 0 0 0 1 0 0 0 0 MemRead 0 0 0 1 0 0 0 0 0 EXTop 0 0 0 1 1 1 0 1 0 RegDst 00 00 01 01 00 00 01 01 00 WDsel 0 0 0 1 0 0 0 0 0 NPCop 00 00 00 00 00 01 00 00 10
-
-
模块规格
信号名 方向 位宽 描述 opcode I 6 指令[31:26]位 funct I 6 指令[5:0]位 RegWrite O 1 GRF写使能信号 ALUop O 4 ALU运算模式选择信号 MemWrite O 1 DM写使能信号 MemRead O 1 DM读使能信号 EXTop O 1 EXT扩展模式选择信号 RegDst O 2 GRF写回地址选择信号:
01:rt
00:rd
10:rsWDsel O 2 GRF写入数据选择信号:
0:ALU运算结果
1:DM读出的数据(lw)NPCop O 2 PC更新模式选择信号:
00:顺序执行
01:beq分支跳转
10:j跳转
NPC
-
核心任务 计算下一周期PC应更新成的值
-
整体设计
- 顺序执行:
NPC = PC + 4- 分支跳转:
NPC = (PC + 4) + (SignExtend(offset)) - j跳转:NPC = PC31…28||instr_index||00
- 分支跳转:
- 顺序执行:
-
模块规格
信号名 方向 位宽 描述 immExt I 32 拓展后的立即数(offset) instr_index I 26 instr_index equal I 1 判断reg rs、rt所存值是否相同 NPCop I 2 00:NPC = PC + 4
01:分支跳转
10:jPC I 32 当前指令地址 NPC O 32 下一条指令地址
测试
核心思路:让 CPU 在执行指令后,将关键的结果存储到DM的特定位置。测试结束后,比较 Logisim 和 Mars 的 DM 内容是否完全一致
测试方法
环境设置
- 选择Compact,Data at Address 0
编写MIPS汇编测试程序
- 初始化
- 初始化寄存器:初始化为不同的非零值
- 初始化指针:设置$t0作为指向DM的指针,初始值为0x0000
- 书写程序
- 执行一条指令,保存一个结果,移动一次指针
- 对于直接产生结果的指令(如add、ori)
- 直接将结果保存到内存
- 对于不直接产生结果的指令(如beq、j)
- 构造场景,使得
跳转成功与否导致存入内存的值不同
- 构造场景,使得
- 对于直接产生结果的指令(如add、ori)
- 例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# .globl main 告诉汇编器 main 是一个全局标签,程序的入口
.globl main
main:
# --- 1. 初始化阶段 ---
# 初始化寄存器为非零值
addi $s0, $zero, 10
addi $s1, $zero, 20
addi $s2, $zero, 10
# ... 初始化更多寄存器 ...
# 初始化内存指针 $t0
addiu $t0, $zero, 0x0000 # $t0 = 0x0000, 指向 DM 的开始
# --- 2. 测试指令并保存结果 ---
# --- 测试 add 指令 ---
add $t1, $s0, $s1 # $t1 = 10 + 20 = 30
sw $t1, 0($t0) # 将结果 30 保存到内存地址 0x0000
addi $t0, $t0, 4 # 指针移动到 0x0004
# --- 测试 beq (分支不发生) ---
beq $s0, $s1, BRANCH_TAKEN # 10 != 20, 不会跳转
# 如果没跳转,存入 0x0
addiu $t2, $zero, 0x0
sw $t2, 0($t0)
addi $t0, $t0, 4
j END_BRANCH_1
BRANCH_TAKEN:
# 如果跳转了,存入 0x1
addiu $t2, $zero, 0x1
sw $t2, 0($t0)
addi $t0, $t0, 4
END_BRANCH_1:
# --- 测试 beq (分支发生) ---
beq $s0, $s2, BRANCH_TAKEN_2 # 10 == 10, 应该跳转
# 如果没跳转,存入 0x0
addiu $t2, $zero, 0x0
sw $t2, 0($t0)
addi $t0, $t0, 4
j END_BRANCH_2
BRANCH_TAKEN_2:
# 如果跳转了,存入 0x1
addiu $t2, $zero, 0x1
sw $t2, 0($t0)
addi $t0, $t0, 4
END_BRANCH_2:
# --- 3. 结束程序 ---
# 可以用一个无限循环来停住 CPU
END_LOOP:
j END_LOOP
- 执行一条指令,保存一个结果,移动一次指针
- 在Mars中生成标答
- 执行程序
- 手动点击Pause暂停死循环
- 导出DM
File - Dump Memory
- 选择Data Segment
- 选择Hexadecimal Text格式
- Dump to file得到txt文件
- 在txt文件开头加上
v2.0 raw
在logisim中运行CPU
- 找到IM中的ROM,右键选择
Load Image - reset
- 看DM
- 运行时钟直到进入死循环
- 找到DM中的RAM,右键选择
Edit Content
- 实例化grf后直接观察grf
- 右键main中grf,选择
view grf ctrl + T开始观察即可
- 右键main中grf,选择
- 看DM
测试数据
计算类指令功能测试
- 寄存器数据:
- 0及附近的数:−2,−1,0,1,2
- 32 位数边界附近的数:−2147483648,−2147483647,2147483646,2147483647
- 32 位数范围内的一些随机数:−1000786109,1919156834,…
- 无符号立即数:
- 0 及附近的数:0,1,2,3
- 16 位无符号数边界附近的数:65533,65534,65535
- 16 位无符号数范围内的一些随机数:25779,42528,…
- 符号立即数:
- 0 及附近的数:−2,−1,0,1,2
- 16 位符号数边界附近的数:−32768,−32767,32766,32767
- 16 位符号数范围内的一些随机数:−5329,25299,…
存取类指令功能测试
- offset :+/-/0
- $base:+/-/0
跳转类指令功能测试
- 跳转,且目标在此跳转指令之前
- 跳转,且目标是此跳转指令
- 跳转,且目标在此跳转指令之后
- 不跳转,且目标在此跳转指令之前
- 不跳转,且目标是此跳转指令
- 不跳转,且目标在此跳转指令之后
可能添加的
lb/sb
dm本身不变
- lb:dm取字后再经MUX,将Addr[1:0]作为选择端,选出真正要读出的字节
- sb:先利用MUX选出要读出的字节
- 太麻烦暂时不考虑
- 多周期CPU中实现……
思考题
-
上面我们介绍了通过 FSM 理解单周期 CPU 的基本方法。请大家指出单周期 CPU 所用到的模块中,哪些发挥状态存储功能,哪些发挥状态转移功能。
状态存储:GRF、PC、DM; 状态转移:NPC、Controller、EXT、ALU
-
现在我们的模块中 IM 使用 ROM, DM 使用 RAM, GRF 使用 Register,这种做法合理吗? 请给出分析,若有改进意见也请一并给出。
合理。
- IM没有增加、修改或删除指令的需求,所以用ROM(Read-Only Memory)可以覆盖需求,而且可以避免指令被意外修改导致程序故障;
- DM需要实现数据的写入和读出,所以使用RAM即可;
- GRF使用register也可以完状态储存的需求。
改进:用同一个RAM储存Instructions和Data,可以支持自修改代码,内存使用也更灵活
-
在上述提示的模块之外,你是否在实际实现时设计了其他的模块?如果是的话,请给出介绍和设计的思路。
无
-
事实上,实现 nop 空指令,我们并不需要将它加入控制信号真值表,为什么?
因为nop指令本质上就是
sll $0, $0, 0的R型指令,机器码为0x00000000。把它当作R型指令处理,执行时任何存储单元的值都不会发生变化,所以不用把它再加入控制信号真值表。 -
阅读 Pre 的 “MIPS 指令集及汇编语言” 一节中给出的测试样例,评价其强度(可从各个指令的覆盖情况,单一指令各种行为的覆盖情况等方面分析),并指出具体的不足之处。
- 寄存器数据、立即数方面:没有测试边界附近的数;没有测试目标寄存器为0的情况
- 存取类指令:没有测试offset为负的情况
- 跳转类指令:没有测试跳转到前面和当前指令的情况