设计文档综述
数据通路设计
数据通路设计和参考设计几乎一致,如下图所示:
模块文件结构
上面数据通路涉及到的模块,PC
,RF
等等,在下面都有对应的文件实现。然后额外实现了 Controller
(就是一个套壳),以及配套的 MUX
,当然还有用于全局的 macro
模块叙述
PC.v
模块声明:
module PC (
input clk,
input reset,
input [31:0] npc,
output reg [31:0] pc
);
很简单的模块,只是一个用于存储 pc
的寄存器。
时钟上升沿到来时,如果 reset
在高电平,pc
置 0x3000
,否则 pc
置为 npc
。
PCjump.v
模块声明:
module PCjump (
input [31:0] pc,
input [31:0] jpc,
input enable,
output [31:0] pc4,
output reg [31:0] npc
);
很简单的模块,只是一个专用于 pc
的 mux
部件。
其中 pc4
始终等于 pc + 4
。enalbe
为高电平时 npc
的值为 jpc
(跳转值),否则为 pc4
。
IM.v
模块声明:
module IM (
input [31:0] programCounter,
output [31:0] instruction
);
很简单的模块,包含一个储存指令的 ROM
。
首先在 initial
中使用 $readmemh
指令读取指令机器码。然后 assign instruction = ROM[programCounter[13:2]-12'hc00];
。其中 0x0c00
就是 0x3000 >> 2
。
DM.v
模块声明:
module DM (
input clk,
input reset,
input memWrite,
input [31:0] addr,
input [31:0] dataWrite,
input [31:0] programCounter,
output [31:0] dataRead
);
基础模块。接口意义同 P3
。pc
接口只是用于课程要求的输出。
主要逻辑是,时钟上升沿时,若 reset
为高电平,用 for
循环置零所有数据;否则,检查写使能,考虑是否写入;并且始终读取数据(并非在写数据时读取浮空值)。
RF.v
模块声明:
module RF (
input clk,
input reset,
input writeEnable,
input [4:0] writeAddr,
input [4:0] readAddr1,
input [4:0] readAddr2,
input [31:0] writeData,
output [31:0] readData1,
output [31:0] readData2,
input [31:0] programCounter
);
基础模块。接口意义同 P3
。pc
接口只是用于课程要求的输出。
主要逻辑是,时钟上升沿时,若 reset
为高电平,用 for
循环置零所有数据;否则,检查写使能,考虑是否写入;并且始终读取数据。
ALU.v
模块定义:
module ALU (
input [31:0] rs, // %rs, may be $zero
input [31:0] rt, // %rt, may be extend(imm)
input [ 4:0] shamt, // shamt
input [ 3:0] op, // ALUop
output reg [31:0] rd // %rd, may be %rt if rt is imm
);
基础模块。相较于 P3
做了一些改动。新增 shamt
接口直接实现移位指令。
对于普通指令 rd = rs op rt
,对于移位指令 rd = rt op shamt
,对于单目指令 rd op rt
。
op
的意义在 macro.v
宏文件中定义。
Controller
模块定义:
module Controller (
input [31:0] pc,
input [31:0] pc4,
input [31:0] instr,
output reg [31:0] npc,
output reg pcJumpEnable,
output [31:0] memAddr,
output reg memWE,
input [31:0] memRead,
output [31:0] memWrite,
output reg grfWE,
output [4:0] grfRA1,
output [4:0] grfRA2,
output [4:0] grfWA,
input [31:0] grfRD1,
input [31:0] grfRD2,
output [31:0] grfWD,
output [31:0] aluRs,
output [31:0] aluRt,
input [31:0] aluRd,
output [4:0] shamt,
output reg [3:0] aluOp
);
几乎就是数据通路了,因此顶层模块实际上只是套壳。
模块内部代码如下:
wire [5:0] opcode, funct;
assign opcode = instr[31:26];
// one-hot code: RIJ
wire [2:0] instrType;
assign instrType = {
// R format
opcode == `MIPS_SPEC,
// I format
opcode == `MIPS_ORI ||
opcode == `MIPS_LW ||
opcode == `MIPS_SW ||
opcode == `MIPS_LUI ||
opcode == `MIPS_BEQ ,
// J format
opcode ==
`MIPS_JAL
};
// R format {opcode, rs, rt, rd, shamt, funct} = instr;
wire [4:0] rs, rt, rd;
assign rs = instr[25:21];
assign rt = instr[20:16];
assign rd = instr[15:11];
assign shamt = instr[10:6];
assign funct = instr[5:0];
// I format {opcode, rs, rt, imm} = instr;
wire [15:0] imm;
assign imm = instr[15:0];
// J format {opcode, jump} = instr;
wire [25:0] jump;
assign jump = instr[25:0];
// ALU controll
reg alu_rt_sel;
assign aluRs = grfRD1;
MUX_alu_rt mux_alu_rt (
.rt(grfRD2),
.imm(imm),
.sel(alu_rt_sel),
.aluRt(aluRt)
);
// GRF controll
reg [1:0] grf_wa_sel, grf_wd_sel;
assign grfRA1 = rs;
assign grfRA2 = rt;
MUX_grf_wa mux_grf_wa (
.rd(rd),
.rt(rt),
.sel(grf_wa_sel),
.grfWA(grfWA)
);
MUX_grf_wd mux_grf_wd (
.alu (aluRd),
.mem (memRead),
.pc4 (pc4),
.sel (grf_wd_sel),
.grfWD(grfWD)
);
// DM controll
assign memAddr = grfRD1 + {{16{imm[15]}}, imm};
assign memWrite = grfRD2;
// PC controll
always @(*) begin
case (instrType)
`MIPS_RFORMAT: begin
alu_rt_sel = 0;
grf_wa_sel = 0;
grf_wd_sel = 0;
memWE = 0;
npc = grfRD1;
case (funct)
`FUNCT_ADD: begin
aluOp = `ALU_ADD;
grfWE = 1;
pcJumpEnable = 0;
end
`FUNCT_SUB: begin
aluOp = `ALU_SUB;
grfWE = 1;
pcJumpEnable = 0;
end
`FUNCT_SLL: begin
aluOp = `ALU_SLL;
grfWE = 1;
pcJumpEnable = 0;
end
`FUNCT_JR: begin
aluOp = `ALU_NOP;
grfWE = 0;
pcJumpEnable = 1;
end
default: begin
aluOp = `ALU_NOP;
grfWE = 0;
pcJumpEnable = 0;
end
endcase
end
`MIPS_IFORMAT: begin
alu_rt_sel = 1;
grf_wa_sel = 1;
npc = pc4 + {{14{imm[15]}}, imm, 2'b00};
case (opcode)
`MIPS_ORI: begin
aluOp = `ALU_OR;
grfWE = 1;
grf_wd_sel = 0;
memWE = 0;
pcJumpEnable = 0;
end
`MIPS_BEQ: begin
aluOp = `ALU_NOP;
grfWE = 0;
grf_wd_sel = 0;
memWE = 0;
pcJumpEnable = grfRD1 == grfRD2;
end
`MIPS_LW: begin
aluOp = `ALU_NOP;
grfWE = 1;
grf_wd_sel = 1;
memWE = 0;
pcJumpEnable = 0;
end
`MIPS_SW: begin
aluOp = `ALU_NOP;
grfWE = 0;
grf_wd_sel = 0;
memWE = 1;
pcJumpEnable = 0;
end
`MIPS_LUI: begin
aluOp = `ALU_LUI;
grfWE = 1;
grf_wd_sel = 0;
memWE = 0;
pcJumpEnable = 0;
end
default: begin
aluOp = `ALU_NOP;
grfWE = 0;
grf_wd_sel = 0;
memWE = 0;
pcJumpEnable = 0;
end
endcase
end
`MIPS_JFORMAT: begin
alu_rt_sel = 1;
aluOp = `ALU_NOP;
grf_wa_sel = 2;
grf_wd_sel = 2;
memWE = 0;
npc = {pc[31:28], jump, 2'b00};
case (opcode)
`MIPS_JAL: begin
grfWE = 1;
pcJumpEnable = 1;
end
default: begin
grfWE = 0;
pcJumpEnable = 0;
end
endcase
end
default: /* default */;
endcase
end
mips.v
顶层模块代码:
module mips (
input clk,
input reset
);
wire [31:0] pc, npc, pc4, instr, jpc;
wire jumpEnable;
PC ppc (
.clk(clk),
.reset(reset),
.pc(pc),
.npc(npc)
);
PCjump pcj (
.pc(pc),
.npc(npc),
.jpc(jpc),
.enable(jumpEnable),
.pc4(pc4)
);
IM im (
.programCounter(pc),
.instruction(instr)
);
wire [31:0] regRD1, regRD2, regWD, memRD, memWD;
wire [4:0] regRA1, regRA2, regWA;
wire [31:0] memAddr;
wire regWE, memWE;
DM dm (
.clk(clk),
.reset(reset),
.programCounter(pc),
.memWrite(memWE),
.addr(memAddr),
.dataWrite(memWD),
.dataRead(memRD)
);
RF rf (
.clk(clk),
.reset(reset),
.programCounter(pc),
.writeEnable(regWE),
.writeAddr(regWA),
.readAddr1(regRA1),
.readAddr2(regRA2),
.writeData(regWD),
.readData1(regRD1),
.readData2(regRD2)
);
wire [31:0] aluA, aluB, aluC;
wire [4:0] shamt;
wire [3:0] aluOp;
ALU alu (
.rs(aluA),
.rt(aluB),
.rd(aluC),
.shamt(shamt),
.op(aluOp)
);
Controller controller (
.pc(pc),
.pc4(pc4),
.instr(instr),
.npc(jpc),
.pcJumpEnable(jumpEnable),
.memAddr(memAddr),
.memWrite(memWD),
.memRead(memRD),
.memWE(memWE),
.grfRA1(regRA1),
.grfRA2(regRA2),
.grfWA(regWA),
.grfRD1(regRD1),
.grfRD2(regRD2),
.grfWD(regWD),
.grfWE(regWE),
.aluRs(aluA),
.aluRt(aluB),
.aluRd(aluC),
.shamt(shamt),
.aluOp(aluOp)
);
endmodule
顶层模块只是做了一个连线工作,就结束了。只要把名字相同的连在一起就算完工。但是如果不小心连错了的话……
自动化测试
mips_tb.v
`timescale 1ns / 1ps
module mips_tb;
// Inputs
reg clk;
reg reset;
initial begin
$dumpfile("mips.vcd");
$dumpvars();
end
// Instantiate the Unit Under Test (UUT)
mips uut (
.clk (clk),
.reset(reset)
);
initial begin
// Initialize Inputs
clk = 0;
reset = 1;
// Wait 100 ns for global reset to finish
#100;
// Add stimulus here
reset = 0;
#100000;
$finish;
end
always #5 clk = ~clk;
endmodule
非常正常的 testbench
。因为自动化测试使用的 iverilog
,所以查看波形也用的 .vcd
。
randomMIPS.py
自动化测试数据生成脚本。强度可以说是很高了。详见中间那个字符串。
#!/usr/bin/python
import random
import os, sys, difflib
INSTRUCTIONSET = {'add', 'sub', 'ori', 'lw',
'sw', 'beq', 'lui', 'jal', 'jr', 'nop'}
def calculationInstr(instrLength: int, readableReg: list[str], writableReg: list[str]) -> list[str]:
instr: list[str] = []
myInstr = ['adduo', 'subuo', 'ori', 'lui']
for _ in range(instrLength):
randInstr = random.choice(myInstr)
rs = random.choice(readableReg)
rt = random.choice(readableReg)
rd = random.choice(writableReg)
imm = str(random.randint(0, 0xffff))
if randInstr == 'ori':
instr.append(' '.join([randInstr, rd, rt, imm]))
elif randInstr == 'lui':
instr.append(' '.join([randInstr, rd, imm]))
else:
instr.append(' '.join([randInstr, rd, rs, rt]))
assert len(instr) == instrLength
return instr
def memoryInstr(instrLength: int, writeRegs: list[str], tempRegs: list[str]) -> list[str]:
instr: list[str] = []
myInstr = ['lw', 'sw']
for _ in range(instrLength):
randInstr = random.choice(myInstr)
rd = random.choice(writeRegs)
base = random.choice(tempRegs)
imm1 = random.randint(-0xfff, 0xfff)
imm2 = random.randint(-imm1, 0x2fff - imm1)
imm2 -= (imm1 + imm2) % 4
if imm1 >= 0:
in1 = 'ori ' + base + ' $zero ' + str(imm1)
else:
in1 = 'lui ' + base + ' 0xffff\nori ' + base + ' ' + base + ' ' + str(imm1 + 0x10000)
in2 = ' '.join([randInstr, rd, str(imm2) + '(' + base + ')'])
instr.append('\n'.join([in1, in2]))
assert len(instr) == instrLength
return instr
'''
100 calculationInstr
jal A
200 calculationInstr
100 memoryInstr
ori $a0 $0 0
beq $a0 $0 B
A:
300 calculationInstr
300 memoryInstr
beq $s0 $s1 B
jr $ra
B:
300 calculationInstr
100 memoryInstr
jal C
beq $a0 $a1 END
jal END
C:
300 calculationInstr
200 memoryInstr
jr $ra
END:
300 calculationInstr
200 memoryInstr
'''
REGISTERS = ['$' + str(i) for i in range(2, 26)] + ['$zero']
OTHERS = ['$at', '$k0', '$k1']
def singleTest():
instrList = calculationInstr(100, REGISTERS, REGISTERS) + ['jal A']
temp = calculationInstr(200, REGISTERS, REGISTERS) + \
memoryInstr(100, REGISTERS, OTHERS)
random.shuffle(temp)
instrList += temp
instrList += ['ori $a0 $0 0', 'beq $a0 $0 B', 'A:']
temp = calculationInstr(300, REGISTERS, REGISTERS) + \
memoryInstr(300, REGISTERS, OTHERS)
random.shuffle(temp)
instrList += temp
instrList += ['beq $s0 $s1 B', 'jr $ra', 'B:']
temp = calculationInstr(300, REGISTERS, REGISTERS) + \
memoryInstr(100, REGISTERS, OTHERS)
random.shuffle(temp)
instrList += temp
instrList += ['jal C', 'beq $a0 $a1 END', 'jal END', 'C:']
temp = calculationInstr(300, REGISTERS, REGISTERS) + \
memoryInstr(200, REGISTERS, OTHERS)
random.shuffle(temp)
instrList += temp
instrList += ['jr $ra', 'END:']
temp = calculationInstr(300, REGISTERS, REGISTERS) + \
memoryInstr(200, REGISTERS, OTHERS)
random.shuffle(temp)
instrList += temp
with open('code.asm', 'w') as f:
f.write('\n'.join(instrList))
os.system(
'java -jar mars.jar code.asm np nc mc CompactLargeText cl adduo.class cl subuo.class coL1 > code.ans')
# os.system(
# 'java -jar mars.jar code.asm np mc CompactLargeText cl adduo.class cl subuo.class coL2 > code.log')
with open('code.asm', 'a') as f:
# call a never end loop to makesure my verilog CPU output correct
f.write('\nlabel_self: beq $0 $0 label_self\n')
os.system(
'java -jar mars.jar code.asm np nc mc CompactLargeText a dump .text HexText code.txt cl adduo.class cl subuo.class')
os.system('vvp mips > code.out')
def specialJudge():
ans = open('code.ans').readlines()
out = open('code.out').readlines()
out = out[2:-1]
ans = ans[:-1]
if ans == out:
return True
else:
print('Check failed!')
print(''.join(difflib.context_diff(out, ans)), end="")
return False
if __name__ == '__main__':
for Ti in range(30):
singleTest()
flag = specialJudge()
if flag == False:
print()
print(f'Failed in #{Ti}')
sys.exit(1)
print(f'Check #{Ti}: Good!')
print('Check over!')
print('No difference found!')
Mars.jar
这个就不展开讲了。发在讨论区和 github
了。
只不过目前 $gp,$sp
还没有改,会每次赋得初值(而非 0
),所以上面那个 python
脚本也没有生成这两个指令相关代码。
思考题
阅读下面给出的 DM 的输入示例中(示例 DM 容量为 4KB,即 32bit × 1024字),根据你的理解回答,这个 addr 信号又是从哪里来的?地址信号 addr 位数为什么是 [11:2] 而不是 [9:0] ?
因为我们的内存是以字为单位寻址的,所以最低两位是无意义的,可以直接舍去。
addr
端口来源是GRF[base] + imm
,而且由于MIPS
只用这种访问内存方式,所以只可能是这个值。
思考上述两种控制器设计的译码方式,给出代码示例,并尝试对比各方式的优劣。
指令对应的控制信号如何取值:从指令出发,是正向思维,列表简单高效,查指令集即可获取表格
控制信号每种取值所对应的指令:调试是可以看到波形图获取执行的指令是什么,方便快捷。麻烦是获取表格是逆向思维,比较繁琐。
在相应的部件中,复位信号的设计都是同步复位,这与 P3 中的设计要求不同。请对比同步复位与异步复位这两种方式的 reset 信号与 clk 信号优先级的关系。
同步复位:clk 信号具有高优先级。如果 reset 信号在 clk 的上升沿以外的时候为高电平,那么是起不到任何作用的。
异步复位:reset 信号具有高优先级。无论 clk 信号处于什么阶段,只要 reset 信号来临,就必须进行重置操作。
C 语言是一种弱类型程序设计语言。C 语言中不对计算结果溢出进行处理,这意味着 C 语言要求程序员必须很清楚计算结果是否会导致溢出。因此,如果仅仅支持 C 语言,MIPS 指令的所有计算指令均可以忽略溢出。 请说明为什么在忽略溢出的前提下,addi 与 addiu 是等价的,add 与 addu 是等价的。提示:阅读《MIPS32® Architecture For Programmers Volume II: The MIPS32® Instruction Set》中相关指令的 Operation 部分。
参考指令集如下:
显然除了检测溢出,
add
和addu
执行的操作时一模一样的,addi
和addiu
执行的操作也是一致的,对立即数都是符号扩展。