1. 分析指令集
- 经过分类,指令共包含R型指令和I型指令
- 列出其对应的32位机器码,寻找共性差异
R型指令:
Type |
Op |
Rs |
Rt |
Rd |
Shamt |
Func |
解释 |
add |
000000 |
(5) |
(5) |
(5) |
00000 |
100000 |
相加(rs+rt->rd) |
sub |
000000 |
(5) |
(5) |
(5) |
00000 |
100010 |
相减(rs-rt->rd) |
I型指令:
Type |
Op |
Rs |
Rt |
Immediate |
解释 |
ori |
001101 |
(5) |
(5) |
(16) |
或运算(rs|immediate->rt) |
lui |
001111 |
00000 |
(5) |
(16) |
立即数加载至高16位({immediate||{16{1’b0}}}->rt) |
Type |
Op |
Rs |
Rt |
Offset |
解释 |
lw |
100011 |
(5) |
(5) |
(16) |
加载字(rs+offset在memory中data->rt) |
sw |
101011 |
(5) |
(5) |
(16) |
保存字(rt->rs+offset在memory中的data) |
beq |
000100 |
(5) |
(5) |
(16) |
rs与rt相等则PC偏移offset*4 |
J型指令
type |
Op |
instr_index |
解释 |
jal |
000011 |
(26) |
跳转至index并且PC+4 -> $ra |
type |
Op |
rs |
0 |
0 |
func |
解释 |
jr |
000000 |
(5) |
{10{1’b0}} |
{5{1’b0}} |
001000 |
跳转至$rs中的地址 |
2. PC设计
端口设计:
方向 |
name |
位宽 |
input |
PCop |
[3:0] |
input |
beq |
[31:0] |
input |
jal |
[31:0] |
input |
jr |
[31:0] |
input |
reset |
1 |
input |
clk |
1 |
output |
PC |
[31:0] |
转移设计:
PCop |
解释 |
0000 |
PC <= PC + 4 |
0001 |
PC <= PC + 4 + beq |
0010 |
PC <= jal |
0011 |
PC <= jr |
3. IM设计
端口设计:
方向 |
name |
位宽 |
input |
PC |
[31:0] |
output |
instruction |
[31:0] |
转移设计:
解释 |
instruction <= instruction_memory[PC] |
4. 设计splitter
端口设计:
方向 |
name |
位宽 |
input |
instruction |
[31:0] |
output |
Op |
[5:0] |
output |
Func |
[5:0] |
output |
rs |
[4:0] |
output |
rt |
[4:0] |
output |
rd |
[4:0] |
output |
immediate |
[15:0] |
output |
jal |
[25:0] |

5. 设计ALU
ALU端口设计:
type |
name |
位宽 |
input |
num1 |
[31:0] |
input |
num2 |
[31:0] |
input |
ALUop |
[7:0] |
output |
ans |
[31:0] |
ALUop解释:
ALUop |
解释 |
10001 |
加法 |
10010 |
减法 |
10011 |
与运算 |
10100 |
或运算 |
10101 |
异或运算 |
10110 |
相等 |
10111 |
大于 |
11000 |
小于 |
6. CU
端口设计:
type |
name |
位宽 |
解释 |
input |
Op |
[5:0] |
机器码高6位 |
input |
Func |
[5:0] |
机器码低6位 |
output |
RegDst |
[1:0] |
GRF被写入寄存器是rt还是rd。0是rt,1是rd,10是$ra |
output |
ALUSrc |
[1:0] |
ALU第二个运算值是rt还是立即数。0是rt,1是立即数 |
output |
memtoReg |
[1:0] |
GRF写入值是ALUans还是DMans。0是ALUans,1是DMans,10是PC + 4 |
output |
Regwrite |
1 |
是否写入GRF |
output |
Memwrite |
1 |
是否写入RAM |
output |
PCsel |
[3:0] |
当前指令是否是beq\jal\jr |
output |
Extop |
[7:0] |
扩展操作类型类型,详见Extender |
output |
ALUop |
[7:0] |
ALU运算类型,详见ALU |
output |
instr_type |
[7:0] |
指令类型 |
- 内部电路采用最小项表达式判断法,先判断Op,再判断Func
每种指令所需控制信号:
|
RegDst |
ALUSrc |
memtoReg |
Regwrite |
Memwrite |
PCsel |
Extop |
ALUop |
add |
01 |
00 |
00 |
1 |
0 |
0000 |
|
ADD |
sub |
01 |
00 |
00 |
1 |
0 |
0000 |
|
SUB |
ori |
00 |
01 |
00 |
1 |
0 |
0000 |
0000 |
OR |
lui |
00 |
01 |
00 |
1 |
0 |
0000 |
0010 |
ADD |
lw |
00 |
01 |
01 |
1 |
0 |
0000 |
0001 |
ADD |
sw |
|
01 |
|
0 |
1 |
0000 |
0001 |
ADD |
beq |
|
00 |
|
0 |
0 |
0001 |
0011 |
EQUAL |
jal |
10 |
|
10 |
1 |
0 |
0010 |
0100 |
|
jr |
|
00 |
|
0 |
0 |
0011 |
|
ADD |
7. Extender
端口类型:
type |
name |
位宽 |
input |
num |
[15:0] |
input |
jal |
[25:0] |
input |
Extop |
[3:0] |
input |
PC |
[31:0] |
output |
ans |
[31:0] |
Extop类型解释:
Extop |
解释 |
0000 |
zero扩展 |
0001 |
sign扩展 |
0010 |
低16位移至高16位,补0 |
0011 |
sign扩展,左移2位,补0 |
0100 |
PC高4位,jal补2位0 |
8.GRF

9. DM
端口设计:
方向 |
name |
位宽 |
input |
addr |
[31:0] |
input |
WD |
[31:0] |
input |
clk |
1 |
input |
reset |
1 |
input |
WE |
1 |
output |
data |
[31:0] |
10. 测试方案
- 通过机器码来检验正确性。根据输出来比对MARS中操作行为。
- 通过评测机来检验正确性。
11. 思考题
addr信号是ALU计算得到的,在lw或sw指令中,ALU将一个寄存器中的值和立即数经扩展移位之后的结果相加,得到的运算结果为address,作为addr端传入到DM中去访存数据。addr位数是[11:2]是因为addr的单位是字节,但是DM中memory数组的位宽是[31:0],也就是一字。一字等于四字节。取addr的[11:2]位相当于将addr右移两位,即整除4,其代表的含义是该addr所在的字地址。通过该字地址,我们可以访存DM中addr的数据。
第一种是每种指令所对应的控制信息,代码示例如下:
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
| reg [31:0] instr;
(identify instr)
(assume outputs are o1,o2,o3...)
always@(*) begin case(instr) instr1: begin o1 = 1; o2 = 1; o3 = 0; o4 = 1; ... on = 1; end instr2: begin o1 = 0; o2 = 0; o3 = 0; o4 = 1; ... on = 1; end instr3: begin o1 = 1; o2 = 1; o3 = 0; o4 = 0; ... on = 0; end (similar codes like above) endcase end
|
好处:条理清晰,每种指令所对应的输出端口一目了然,结构层级明显,不容易出错,debug难度较低,便于统计。
坏处:每一条指令均需要罗列全部的输出端口,重复性操作较多,添加指令较为麻烦。
第二种是每种控制信息所需要的指令,代码示例如下:
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
| reg [31:0] instr;
(identify instr)
(assume outputs are o1,o2,o3...)
always@(*) begin if(instr == `instr1 || instr == `instr2 || instr == `instr4) begin o1 = 1; end else begin o1 = 0; end
if(instr == `instr3) begin o2 = 1; end else begin o2 = 0; end
if(instr == `instr4 || instr == `instr5) begin o3 = 1; end else begin o3 = 0; end (similar codes like above) end
|
好处:操作简单,新增指令只需要新增对应输出端口的判断条件即可,可维护性较强。
缺点:可读性差,debug难度较高,无法直观显示指令与输出之间的关系。
同步复位是指,所有复位操作均在时钟上升沿进行。即reset在时钟周期中有效不会立即生效,得保持到clk上升沿才能复位。clk优先级高于reset。 异步复位是指,reset有效时立即复位,因此不用等到clk上升沿即可复位。reset优先级高于clk。
add或者addi首先检查两个操作数是否是合法的word,如果不是,会抛出UndefinedResult()异常。之后尝试相加并判断相加之后是否有符号溢出,如果符号溢出则会抛出SignalException()异常。如果不溢出则赋值给相应寄存器。addu或者addiu也会首先检查两个操作数是否是合法的word,如果不是则同样抛出UndefinedResult()异常。不一样的地方在于,相加之后取结果的[31:0]位直接复制给对应寄存器。这是因为对于无符号数,没有符号位,每一位的位权都是正的,因此直接相加即可,取低32位为运算结果。但有符号数的最高位是符号位,如果因为相加进位导致符号位的改变,则会改变运算结果的符号,因此需要判断是否有符号溢出。如果不考虑符号溢出,则无需关注相加是否导致符号位改变,只需要相加取低32位即可,默认程序员会维护加法运算。因此在不考虑溢出的情况下,有符号加法和无符号加法的操作方式都是一样的,可以认为是等价。
附一张Logisim的CPU框架方便理解
