说明
- 为了有较好的可读性,报告仅仅粘贴关键代码。
- 该PDF带有大纲功能,点击大纲中的对应标题,可以快速跳转。
实验目标
- 掌握单周期CPU执行指令的流程和原理;
- 学习使用verilog HDL语言实现单周期CPU, 并通过功能仿真;
- 提高设计实现较复杂硬件系统的能力;
- 激发对硬件设计的兴趣。
在这一次实验中,需要实现可以执行以下指令的CPU
a) R型指令:addu,subu,add,and,or,slt
b) I型指令:addi,addiu,andi,ori,lui
c) 访存指令:lw,sw
d) 跳转指令:beq,j,jal,jr
实验步骤
按照实验指导完善CPU
目前是从零起步,要想对CPU进行功能测试以及仿真,必须先搭建一个框架,在这里首先按照教程,搭建一个可以实现addu的指令。
(一)实现addu指令
(1)对addu指令进行分析
addu指令所需要用到的器件基本上囊括了数据通路的器件。
以下是addu指令的器件:
- 寄存器堆:寄存器堆用来存储数据,是CPU内部的存储器,访问速度最快。在MIPS中,有32个寄存器。
寄存器的写入是需要写使能信号,并在时钟上升沿进行写入。
寄存器的读取是组合逻辑,在值进行变化之后,立即读出数字。 - 加法器:在该实验中,加法器仅仅用于加法,事实上,其名字为ALU,既可以执行算术运算,也可以执行逻辑运算。通过给ALU具体的操作码,可以让ALU做具体的运算。
- 指令存储器:为了避免后面的结构冒险,把存储器分为了IM和DM,其实在真正的计算机中,对应着指令cache和数据cache.
- PC寄存器:存储CPU当前执行的指令的地址,并在执行一条指令时自增,变为下一条指令的地址,以便下次使用。
所以要实现一个addu指令,就先要搭建好上述的器件。
(2)寄存器文件实现
寄存器文件的读端口是组合逻辑,写端口是时序逻辑。
特别的是寄存器文件的0号寄存器,其实可以被写入的,只不过是每一次读出来的值并不相同,所以采用在读寄存器的时候进行判断其是不是零号寄存器。
verilog
reg [31:0] gp_registers[31:0]; //32个寄存器
assign a = (rs==0)? 0 : gp_registers[rs];//若为0号寄存器,那么返回0
assign b = (rt==0)? 0 : gp_registers[rt];//若为0号寄存器,那么返回0
always @(posedge clock) begin//时钟上升沿的时候执行写入操作
if(reg_write) begin//只有写使能信号有效时才写
gp_registers[num_write] <= data_write;
end
end
(3)加法器设计
在这里为了先让CPU跑起来,象征性地设计一个加法器。
verilog
assign c = a + b;
(4)指令存储器设计
指令寄存器的空间大小有限,所以只是取低12位进行访存。
但是由于reg [31:0] ins_memory[1023:0]
,对于该数组的访问是一次性访问4字节,所以要把4字节作为一个整体进行访问,可以使用以下语句进行访问存储器
verilog
assign instruction = ins_memory[pc[11:0] >> 2];
(5)PC寄存器
同步低电平复位,所以复位信号的下降沿不作为敏感信号列表。
verilog
always @(posedge clock) begin
if(reset == 0) pc <= 32'h0000_3000;
else pc <= npc;
end
(6)顶层模块设计
在顶层模块中,采用了名称关联,这样可以不需要关注各个端口的顺序,可读性更强。
verilog
module s_cycle_cpu(clock,reset);
//输入
input clock;
input reset;
wire [31:0] npc,pc, instruction, a,b,c;
//下一条指令为当前指令+4
assign npc = pc + 4;
pc PC(.pc(pc),
.clock(clock),
.reset(reset),
.npc(npc));
im IM(.instruction(instruction),
.pc(pc));
gpr GPR(.a(a) , //寄存器1的值
.b(b) , //寄存器2的值
.clock(clock) ,
.reg_write(1'b1) , //写使能信号
.rs(instruction[25:21]) , //读寄存器1编号
.rt(instruction[20:16]) , //读寄存器2编号
.num_write(instruction[15:11]) , //写寄存器编号
.data_write(c) ); //写数据
alu ALU(.c(c),.a(a),.b(b));
endmodule
(7)波形仿真
汇编代码:
assembly
addu $2, $3, $4
addu $5, $6, $7
波形:
在图中可以看到寄存器组正确读出了位于3号以及4号寄存器中的值,然后把3与4相加,得到了正确的结果。
同时看到第3,4行的pc以及npc正确。
2号寄存器的值被写入3号以及4号的值,addu指令本地测试正确!
(二) 增加实现R型指令
下标为所需要增加的指令
根据这一张表,发现需要增加的指令同为R型,基本的步骤是完全相同的,唯一有区别的就是ALU单元所执行的运算操作不同。
需要增加的单元:控制单元(在其中通过给定的指令的op以及func字段,确定ALU的具体操作)
需要修改的单元:ALU单元(响应由控制单元所生成的控制信号alu_op,并执行不同的操作)
(1)增加控制模块
在控制模块中需要产生一些对ALU行为进行控制的信号,为了增强可读性,采取宏定义。
我在宏定义之前表示了alu_op_,表示这是关于ALU操作控制信号的宏定义,这样做可以避免之后的设计中出现命名冲突。
verilog
`define alu_op_add 4'b0000
`define alu_op_sub 4'b0001
`define alu_op_and 4'b0010
`define alu_op_or 4'b0011
`define alu_op_slt 4'b0100
对于不同的op字段,使用 if 语句进行判断,在op字段为000000的情况下,对其func字段进行译码。
大体框架如下(为了可读性,仅列举代码的框架,具体赋值通过表格形式展现):
verilog
always@(*) begin
if(op == 6'b000000)begin//当op字段为000000时,为R型指令,然后对func字段进行译码
case(funct)
6'b100001: begin
aluop = `alu_op_add;
reg_write = 1;
end
........//这里省略了其他情况的func字段下对应的赋值
default:........
endcase
end
else begin//防止出现多余的锁存器
reg_write = 0;
aluop = 0;
end
- 为了避免出现多余的锁存器,采用了else以及default语句。
- 由于在本CPU中不考虑异常,无需判断是否溢出,所以把有符号的加以及无符号的加全部当成无符号的加进行处理。
赋值情况如下表:
操作助记符 | op | funct | reg_write | aluop |
---|---|---|---|---|
addu | 6'b000000 | 6'b100001 | 1 | alu_op_add |
subu | 6'b000000 | 6'b100011 | 1 | alu_op_sub |
add | 6'b000000 | 6'b100000 | 1 | alu_op_add |
and | 6'b000000 | 6'b100100 | 1 | alu_op_and |
or | 6'b000000 | 6'b100101 | 1 | alu_op_or |
slt | 6'b000000 | 6'b101010 | 1 | alu_op_slt |
无 | 其他 | 其他 | 0 | 0 |
(2)完善ALU
在ALU中,仅仅需要根据控制器传过来的控制信号,直接进行相应的运算。
verilog
always @(*) begin
case(aluop)
`alu_op_add: c = a + b;
`alu_op_sub: c = a - b;
`alu_op_and: c = a & b;
`alu_op_or: c = a | b;
`alu_op_slt: begin
if($signed(a) < $signed(b)) c = 1;//符号比较
else c = 0;//防止多余锁存器
end
default c = 0;//防止多余锁存器
endcase
end
(3)完善顶层模块
verilog
wire [3:0] aluop;//新增了aluop,并把其接入ctrl以及alu模块中
alu ALU(.c(c),.a(a),.b(b), .aluop(aluop));
ctrl CTRL( .reg_write(reg_write) ,
.aluop(aluop),
.op(instruction[31:26]) ,
.funct(instruction[5:0]) );
(4)波形仿真
汇编代码:
assembly
addu $3, $1, $2
subu $4, $1, $2
add $5, $1, $2
and $6, $1, $2
or $7, $1, $2
slt $8, $1, $2
slt $9, $1, $zero
在执行过程中截取的寄存器中的数字
分析:对于0号寄存器,由于在testbench中未初始化,所以其值为x
对于1,2,3,4,5,6,7,8,9号寄存器,其初始值为1,2,3,4,5,6,7,8,9
在执行完成对应的汇编指令之后,有:
addu $3, $1, $2
: 将寄存器$1
的值加上寄存器$2
的值,并将结果存储在寄存器$3
中。根据分析结果和波形,当$1
= 1,$2
= 2 时,执行该指令后,$3
= 3。subu $4, $1, $2
: 将寄存器$1
的值减去寄存器$2
的值,并将结果存储在寄存器$4
中。根据分析结果和波形,当$1 = 1
,$2 = 2
时,执行该指令后,$4 = -1
,波形中为其补码,正确。add $5, $1, $2
: 将寄存器$1
的值与寄存器$2
的值相加,并将结果存储在寄存器$5
中。根据分析结果和波形,当$1 = 1
,$2 = 2
时,执行该指令后,$5 = 3
。and $6, $1, $2
: 将寄存器$1
的值与寄存器$2
的值进行按位与运算,并将结果存储在寄存器$6
中。根据分析结果和波形,当$1 = 1
,$2 = 2
时,执行该指令后,$6 = 0
。or $7, $1, $2
: 将寄存器$1
的值与寄存器$2
的值进行按位或运算,并将结果存储在寄存器$7
中。根据分析结果和波形,当$1 = 1
,$2 = 2
时,执行该指令后,$7 = 3
。slt $8, $1, $2
: 比较寄存器$1
的值是否小于寄存器$2
的值,如果成立,则将$8
设置为 1,否则为 0。根据分析结果和波形,当$1 = 1
,$2 = 2
时,执行该指令后,$8 = 1
。slt $9, $1, $zero
: 比较寄存器$1
的值是否小于零,如果成立,则将$9
设置为 1,否则为 0。根据分析结果和波形,当$1 = 1
时,执行该指令后,$9 = 0
。
对于波形分析之后,发现执行结果完全正确,本地测试通过!
(三)增加实现I型指令
对下面的指令进行分析
- 发现目的寄存器编号并不再是rd,而是rt,所以需要在寄存器堆的rd端口增加一个选择器。
- 在运算部分,ALU的第二个操作数为立即数,所以ALU的b端口需要一个多路选择器。
- ALU需要增加lui指令的功能。
- 在机器代码中,立即数是16位,但是ALU操作的数字的位宽是32位,所以需要进行符号拓展。
- 更改控制器,增加对于多路选择器的控制信号。
(1)增加多路选择器模块
为了进行层次化设计,专门实现一个mux2to1模块,而不是简单使用条件运算符进行运算。
在上面的需求分析中,多路选择器可能是5位的,也可能是32位的,为了适应不同的位宽,定义带有参数的多路选择器。
verilog
module mux2to1 (out, in0, in1, sel);
parameter WIDTH = 32;//可变参数
input [WIDTH - 1:0]in0; //第0个输入
input [WIDTH - 1:0]in1; //第1个输入
input sel;//选择信号
output [WIDTH - 1:0]out;//输出
assign out = sel ? in1 : in0;//选择信号为1,输出in1,选择信号为0,输出in0
endmodule
(2)修改ALU,增加lui指令
在case语句中加入针对lui的操作,同时更新一下宏定义。
verilog
case(aluop)
............
`alu_op_lui: c = {b[15:0], 16'h0};
default c = 0;
endcase
(3)增加符号拓展
符号拓展(extender)为组合逻辑,当signextend为1的时候对立即数进行符号拓展,反之进行零拓展。
verilog
module extender( input [15:0] in,//立即数
input signextend,//是否为符号拓展
output reg[31:0] out);//输出
always @(*) begin
if(signextend == 0)
out = {{16{1'b0}}, in};
else
out = { {16{in[15]}}, in};
end
endmodule
(4)改变控制模块
在控制模块中,新增二选一多路选择器的控制信号。
增加的方式是case语句的对应位置增加赋值语句。
代码框架(仅仅展示代码框架,为了可读性较好,把具体的赋值过程放在后面的表格中):
verilog
always@(*) begin
case(op)
6'b000000:begin
s_num_write = 1;
s_b = 0;
s_ext = 1;
case(funct)
6'b100001: begin
aluop = `alu_op_add;
reg_write = 1;
end
............//其他R型指令对应控制信号
default :begin
aluop = `alu_op_add;
reg_write = 1;
end
endcase
end
6'b001000:begin
s_num_write = 0;
s_b = 1;
s_ext = 1;
aluop = `alu_op_add;
reg_write = 1;
end
.............//其他op字段对应的控制信号
default: begin//防止出现多余的锁存器
s_num_write = 0;
s_b = 0;
s_ext = 0;
aluop = `alu_op_add;
reg_write = 0;
end
endcase
控制信号分配表:
助记符 | op | funct | s_num_write | s_b | s_ext | aluop | reg_write |
---|---|---|---|---|---|---|---|
addu | 6'b000000 | 6'b100001 | 1 | 0 | 1 | alu_op_add | 1 |
subu | 6'b000000 | 6'b100011 | 1 | 0 | 1 | alu_op_sub | 1 |
add | 6'b000000 | 6'b100000 | 1 | 0 | 1 | alu_op_add | 1 |
and | 6'b000000 | 6'b100100 | 1 | 0 | 1 | alu_op_and | 1 |
or | 6'b000000 | 6'b100101 | 1 | 0 | 1 | alu_op_or | 1 |
slt | 6'b000000 | 6'b101010 | 1 | 0 | 1 | alu_op_slt | 1 |
无 | 6'b000000 | 其他 | 1 | 0 | 1 | alu_op_add | 1 |
addi | 6'b001000 | 不关心 | 0 | 1 | 1 | alu_op_add | 1 |
addiu | 6'b001001 | 不关心 | 0 | 1 | 1 | alu_op_add | 1 |
andi | 6'b001100 | 不关心 | 0 | 1 | 0 | alu_op_and | 1 |
ori | 6'b001101 | 不关心 | 0 | 1 | 0 | alu_op_or | 1 |
lui | 6'b001111 | 不关心 | 0 | 1 | 0 | alu_op_lui | 1 |
无 | 其他 | 其他 | 0 | 0 | 0 | alu_op_add | 0 |
(5)顶层模块
其中,ALU,IM,PC的接线不需要改动,在这里没有列出来。
verilog
`include "header.v"
module s_cycle_cpu(clock,reset);
input clock;
input reset;
wire [31:0] npc, pc, instruction, a,b, c,mux2to1_out_to_alu_b,expanded_numbers;
wire [3:0] aluop;
wire reg_write;
wire [4:0] mux2to1_out_to_gpr_rd;
assign npc = pc + 4;
wire s_num_write, s_b, s_ext;
mux2to1 #(.WIDTH(5)) MUX2to1_GPR_RD(//表示连接在gpr的rd接口上的多选器
.in0(instruction[20:16]),//rt字段
.in1(instruction[15:11]),//rd字段
.sel(s_num_write),//与控制器连接
.out(mux2to1_out_to_gpr_rd) );//输出到gpr的rd口上
mux2to1 #(.WIDTH(32)) MUX2to1_ALU_B(
.in0(b), //rt中寄存器的值
.in1(expanded_numbers),//符号拓展之后的立即数
.sel(s_b), //与控制器相连接
.out(mux2to1_out_to_alu_b) );//输出到ALU的b端
extender EXTENDER(
.in(instruction[15:0]),//指令中的立即数字段
.signextend(s_ext),//是否进行符号拓展
.out(expanded_numbers)//拓展之后的数字
);
gpr GPR(.a(a) , //寄存器1的值
.b(b) , //寄存器2的值
.clock(clock) ,
.reg_write(reg_write) ,
.rs(instruction[25:21]) , //读寄存器1编号
.rt(instruction[20:16]) , //读寄存器2编号
.num_write(mux2to1_out_to_gpr_rd) , //写寄存器编号,与多选器连接
.data_write(c) ); //写数据 )
ctrl CTRL(.reg_write(reg_write) ,
.aluop(aluop),
.s_num_write(s_num_write),//与gpr的rd端口相连的mux的选择信号
.s_b(s_b),//与ALU------b端口连接的mux的选择信号
.s_ext(s_ext),//符号拓展与否
.op(instruction[31:26]) ,//两个输入
.funct(instruction[5:0]) );
endmodule
(6)波形仿真
汇编代码
assembly
addi $3, $1, -100
addiu $4, $1, 10
andi $5, $1, 3
ori $6, $1, 2
lui $7, 0x1234 #把$7设置为0x12345678
addi $7, $7, 0x5678
仿真波形
对于波形的分析,如表:
运算 | 寄存器 | 预期结果 | 实际结果 |
---|---|---|---|
$3=$1+(-100) |
$3 |
0xFFFFFF9D(-99) | FFFFFF9D |
$4=$1+10 |
$4 |
0xB | 0xB |
$5=$1&3 |
$5 |
0x1 | 0x1 |
`$6=$1 | 2` | $6 |
0x3 |
$7=0x12340000 |
$7 |
0x12340000 | 0x12340000 |
$7=$7+0x5678 |
$7 |
0x12345678 | 0x12345678 |
经过对波形的分析,结果正确,本地测试通过!
(四)增加实现MEM型指令
如图,推访存类指令进行分析,发现之前的绝大部分内容全部可以复用。
需要新加入的有数据存储器以及控制写入寄存器文件的数据的多选器。
(1)增加DM模块
DM模块的实现与指令存储器几乎一致,其写入是要在时钟上升沿的时候进行,读取为组合逻辑。
同时仅仅sw具有写DM的功能,所以DM还需要写使能信号控制。
由于在一个周期中,DM被读取或者是被写入,所以读与写可以共用地址线。
关键代码如下
verilog
reg [31:0] data_memory[1023:0]; //4K数据存储器
always @(posedge clock) begin//在时钟的上升沿
if(mem_write) //仅仅当写使能有效的时候才更新
data_memory[address[11:2]] <= data_in;
end
always @(*)begin//组合逻辑,读取地址线上的地址所对应的数据存储器中的内容
data_out = data_memory[address[11:2]];
end
(2)增加多路选择器
首先,在ctrl模块中增加连接到gpr写入数据端的多路选择器控制信号。
仅仅有lw指令需要把数据存储器的输出写入到寄存器中,所以可以设置仅仅当指令为lw的时候,mux2to1选择数据存储器的输出,否则选择ALU的输出。
然后,需要在控制模块中把dm接入
在顶层模块中,需要改动的有:
DM的调用模块,把DM接入
verilog
dm DM(
.data_out(dm_data_out),//连接到下面的多路选择器
.clock(clock),
.mem_write(mem_write),//由ctrl模块进行控制
.address(dm_address),//连接到ALU的输出
.data_in(dm_data_in) );//连接到gpr的第二个数据输出端口
多路选择器
verilog
mux2to1 #(.WIDTH(32)) MUX2to1_GPR_DATA_WRITE(
.in0(c), //ALU的输出
.in1(dm_data_out),//数据存储器读出的数据
.sel(s_data_write),//选择信号,由ctrl模块进行控制
.out(mux2to1_out_to_gpr_data_write) );//连接到gpr的数据写入端口
(3)修改ctrl模块
在ctrl模块中加入控制多路选择器以及mem_write的信号
对应控制信号与指令的关系如下表:
助记符 | op | funct | mem_write | s_data_write |
---|---|---|---|---|
addu | 6'b000000 | 6'b100001 | 0 | 0 |
subu | 6'b000000 | 6'b100011 | 0 | 0 |
add | 6'b000000 | 6'b100000 | 0 | 0 |
and | 6'b000000 | 6'b100100 | 0 | 0 |
or | 6'b000000 | 6'b100101 | 0 | 0 |
slt | 6'b000000 | 6'b101010 | 0 | 0 |
无 | 6'b000000 | 其他 | 0 | 0 |
addi | 6'b001000 | 其他 | 0 | 0 |
addiu | 6'b001001 | 其他 | 0 | 0 |
andi | 6'b001100 | 其他 | 0 | 0 |
ori | 6'b001101 | 其他 | 0 | 0 |
lui | 6'b001111 | 其他 | 0 | 0 |
sw | 6'b101011 | 其他 | 1 | 0 |
lw | 6'b100011 | 其他 | 0 | 1 |
无 | 其他 | 其他 | 0 | 0 |
(4)波形仿真
汇编代码
verilog
addi $t0, $zero, 0x3f
sw $t0, 4($zero)
lw $t1, 4($zero)
上图中的为dm的波形,从图中可以看到,在第二个周期结束的时候,成功把事先设定好的0x3f写入地址为4的存储器中。(由于一个数组是4个字节,所以数组下标为1的位置就对应着第4到7个字节,一次写入4个字节)
上图为寄存器文件的内容
在第一个周期中,1号寄存器被写入预定值,在第二个周期,没有对寄存器进行操作,在第三个周期末,成功把在第二个周期存入的数字放置在2号寄存器中。
经过验证,结果正确,通过本地的测试
(五)增加跳转型指令
在这个任务中,需要添加以下指令:j jal jr beq
对于这些指令的分析如下:
- 段内绝对跳转指令使用指令类型为"j"和"jal"。这些指令的操作码占据6位,剩余的26位用于存储目标地址。由于所有指令的长度为4字节,所以目标地址的最低2位不需要存储。因此,实际可供寻址的范围为228,即256MB,地址的高四位使用PC+4的高四位进行代替
- 寄存器跳转指令"jr"可以跳转到任意在寄存器中存放的32位目标地址。
- 相对跳转指令"beq"使用指令低16位的偏移量作为相对于当前PC的有符号偏移。由于偏移地址的最低两位不需要存储,因此可支持的跳转范围是相对于PC的偏移为-128KB~+128KB
如图,对于绿色箭头所指示的部分的多路选择器,对其进行分析。
- s_npc为0,相等则跳转,并采用相对跳转方式进行寻址,如果两个操作数相等,那么就跳转到
PC + 4 +(sign_extend(offset)<<2)
处,否则跳转到PC+4. - s_npc为1,绝对跳转指令(寄存器跳转指令)直接把PC寄存器的值改为寄存器中的地址。
- s_npc为2,段内绝对跳转,通过把26个立即数与PC+4的高四位进行拼接,得到地址。
- s_npc为3,指令的正常顺序执行,下一条指令的地址为PC+4
通过以上的分析,发现:增加跳转指令需要进行以下的操作:
- ALU增加zero输出信号
- 修改ctrl模块,增加对于四条跳转指令的控制信号以及s_npc的控制信号
- 增加两个求跳转地址的模块
- 更改原来的二选一多路选择器,把其变为四选一多路选择器。
- 更改顶层模块
(1)修改ALU
在声明zero端口
verilog
module alu(c, zero, a, b, aluop);
output zero;
assign zero = (c == 0);
(2)增加4选1选择器
同样为了解决数据宽度的问题,采用WIDTH可变参数。
verilog
module mux4to1 (out, in0, in1, in2, in3, sel);
parameter WIDTH = 32;
input [WIDTH - 1:0] in0; // 输入端口 in0,宽度为 WIDTH
input [WIDTH - 1:0] in1; // 输入端口 in1,宽度为 WIDTH
input [WIDTH - 1:0] in2; // 输入端口 in2,宽度为 WIDTH
input [WIDTH - 1:0] in3; // 输入端口 in3,宽度为 WIDTH
input [1:0] sel; // 选择信号 sel
output reg [WIDTH - 1:0] out; // 输出端口 out,宽度为 WIDTH
always @ (*) begin
case (sel)
0: out = in0; // 当 sel 等于 0 时,输出端口为 in0
1: out = in1; // 当 sel 等于 1 时,输出端口为 in1
2: out = in2; // 当 sel 等于 2 时,输出端口为 in2
default: out = in3; // 当 sel 等于 4 时,输出端口为 in3
endcase
end
endmodule
(3)修改ctrl模块
当根据op
和funct
字段的不同取值,与对应的MIPS汇编助记符建立表格,如下所示:
op | funct | MIPS助记符 | s_npc | s_data_write | mem_write | reg_write | s_num_write | s_b | s_ext | aluop |
---|---|---|---|---|---|---|---|---|---|---|
6'b000000 | 6'b100001 | add | 3 | 1 | 0 | 1 | 1 | 0 | 1 | alu_op_add |
6'b000000 | 6'b100011 | sub | 3 | 1 | 0 | 1 | 1 | 0 | 1 | alu_op_sub |
6'b000000 | 6'b100000 | add | 3 | 1 | 0 | 1 | 1 | 0 | 1 | alu_op_add |
6'b000000 | 6'b100100 | and | 3 | 1 | 0 | 1 | 1 | 0 | 1 | alu_op_and |
6'b000000 | 6'b100101 | or | 3 | 1 | 0 | 1 | 1 | 0 | 1 | alu_op_or |
6'b000000 | 6'b101010 | slt | 3 | 1 | 0 | 1 | 1 | 0 | 1 | alu_op_slt |
6'b000000 | 6'b001000 | jr | 1 | 不关心 | 0 | 0 | 不关心 | 不关心 | 不关心 | alu_op_add |
6'b000000 | default | alu_op_add | ||||||||
6'b001000 | addi | 3 | 1 | 0 | 1 | 0 | 1 | 1 | alu_op_add | |
6'b001001 | addiu | 3 | 1 | 0 | 1 | 0 | 1 | 1 | alu_op_add | |
6'b001100 | andi | 3 | 1 | 0 | 1 | 0 | 1 | 0 | alu_op_and | |
6'b001101 | ori | 3 | 1 | 0 | 1 | 0 | 1 | 0 | alu_op_or | |
6'b001111 | lui | 3 | 1 | 0 | 1 | 0 | 1 | 0 | alu_op_lui | |
6'b101011 | sw | 3 | 1 | 1 | 0 | 不关心 | 1 | 1 | alu_op_add | |
6'b100011 | lw | 3 | 2 | 0 | 1 | 0 | 1 | 1 | alu_op_add | |
6'b000010 | j | 2 | 不关心 | 0 | 0 | 不关心 | 不关心 | 不关心 | alu_op_add | |
6'b000011 | jal | 2 | 0 | 0 | 1 | 2 | 不关心 | 不关心 | alu_op_add | |
6'b000100 | beq | 0 | 不关心 | 0 | 0 | 不关心 | 0 | 不关心 | alu_op_sub | |
default | 3 | 不关心 | 0 | 0 | 不关心 | 不关心 | 不关心 | alu_op_add |
在表格中,设置了默认情况,用于防止出现多余的锁存器。
(4)增加下地址计算模块
首先增加段内绝对跳转(绿色框所标识的)
根据图示得知,该模块有两个输入(2'b00可以直接在模块内生成),一个输出
地址的拼接方法为:
addr = {PC[31..28 ], instr_index ,2'b00}
给这一个模块起名字为intra_segment_jump_addr_calc
verilog
module intra_segment_jump_addr_calc(addr, pc_add_4, instr_index);
input [31:0]pc_add_4;//经过前面的ADD模块生成的PC+4的值
input [25:0]instr_index;//指令中的低26位
output [31:0] addr;//计算得到的地址
assign addr = {pc_add_4[31:28], instr_index[25:0], 2'b00};//按照段内跳转的地址规范进行赋值
endmodule
通过这一个模块,可以对于指令j和jal的下一个地址进行计算。
然后增加beq地址的计算器
我设计了一个模块,包括了图中的绿色框中的内容。
如果要完全按照图中的三个小模块(MUX,ADD,左移二运算器)进行设计的话,那么电路设计就会显得非常繁琐。
所以在这里我把三者合并为一个模块,叫做relative_jump_addr_calc
我定义的这一个模块有以下输入输出信号:
zero
: 由ALU所产生,表示两个数字是否相等。若相等,那么执行跳转。pc_add_4
: 表示从图中紫色框输出出来的PC+4的值sign_extended_imm
: 32位经过符号扩展的立即数
输出信号为:
addr
: 表示计算得到的相对跳转的目标地址。
若条件满足,则为PC值+4+偏移量
若条件不满足,则目标地址为PC+4
最终得到这一个模块:
verilog
module relative_jump_addr_calc(addr, zero, pc_add_4, sign_extended_imm);
input zero;
input [31:0] pc_add_4, sign_extended_imm;
output [31:0]addr;
assign addr = zero ? (pc_add_4 + (sign_extended_imm << 2))://如果条件满足,那么跳转
pc_add_4;//条件不满足
endmodule
在代码中使用条件运算符表示多路选择器,采用运算符表示ADD,左移二运算器
(5)仿真测试
在这里采用仿真测试进行DEBUG,详细的操作见后面的"遇到的问题与解决办法",这里进列举部分。
现在编写如下代码测试je指令以及beq指令
发现跳转正常
解释:第二条指令跳转到第四条指令
第四条指令条件不满足,向下执行
第五条指令条件满足,跳转到第一条指令
遇到的问题与解决办法
问题一:
在设计存储器(IM)的时候,对于一个地址,在时钟上升沿写入的内容,在该周期内无法读出,不是组合逻辑。
错误代码局部:
verilog
always @ (pc) begin
instruction = ins_memory[pc[11:0] >> 2];
end
经过仔细检查代码,发现always语句中的敏感信号列表应该包含所有变化的值,而我的敏感列表仅仅有地址,没有实例化的数组,这样的化数组内容改变,敏感列表中并不包含,应该把always()中的内容写为*
问题二:
提交后错误如图
发现num_write赋值错误,检查我的代码,发现如下:
verilog
.num_write(instruction[15:10]) , //写寄存器编号
再次比对实验指导书,发现rd子段应该是instruction[15:11]
,修改后,问题解决。
问题三:
正确定义宏,但是在使用过程中报错误。
上网查询资料,发现应该在应用宏定义的时候,在宏名称前面加上 ` 符号
问题四:
随着指令ALU操作的指令定义数目增加,如果在每一个文件中拷贝相同的宏定义,那么容易遗漏,且可读性极差。
解决:学习C语言中的#include
,在verilog中,生成一个header.v
文件,把宏定义均放在这一个文件中,然后在需要使用宏定义的文件中开头输入:
verilog
`include "header.v"
问题五:
查阅对应的instruction,其表示slt。通过对比GPRa和GPR.b,发现我的程序实现的是无符号数字的比较,
解决办法:在网络中查阅资料得知,有三种方法可以进行有符号数比较。
-
在声明变量的时候采用
wire signed [31:0]
-
在进行比较之前,采用系统任务把无符号的数转化为有符号的数字
verilogif($signed(a) < $signed(b))
-
分析数字的表示进行比较
如果符号位相同,那么真实的大小与补码直接按照无符号数字比较的结果是相同的。
如果符号位不同,那么负数一定小于正数
verilogassign a[31] == b[31] ? a < b : b[31] == 0;
在代码中:
a[31] == b[31]
时,意味着a
和b
具有相同的符号,直接比较a,b的大小;否则(符号位不同),通过比较
b[31] == 0
可以判断b
是否为正数,如果b为正数,那么a一定小于b
问题六:
如下图
标红部分为测试文件第一行,在测试文件中,是通过系统任务直接写入值的,面对一个地址,我读出的值与系统实现存入的有冲突,推测问题处在dm的读数指令中。检查dm的读数指令,
verilog
data_memory[address[9:0]] <= data_in;
发现是取addr的时候取错了,由于数据存储器仅仅支持4个字节一起读取,一个data_memory就代表着四个直接,所以应该更改为:
verilog
data_memory[address[11:2]] <= data_in;
问题七:
在编译平台上遇到的gpr_write的值一直都是1
解决方法:首先检查控制模块中s_write_gpr的设置。虽然s_write_gpr的信号正确设置,但是gpr_write的值仍然为1.
然后检查顶层文件,发现在顶层文件中出现问题,在gpr的write_gpr的参数直接填写了1'b1,而不是s_write_gpr
启示:在之后的编码中,不应该随意使用常数,如果要让某一个值为1,可以设置assign,使得变量强制为1,而不是在调用子模块的端口处直接使用常数。
问题八:
写testbench的时候,发现使用$readmemh
之后,并在IM中写入应有的值。
问题代码:
verilog
$readmemh("D:\hcb\computer_composition\class2\inst", S_CYCLE_CPU.IM.ins_memory);
IM的数据并没有别写入,所以全部都是未知态
解决方法:仔细检查问题代码,根据C语言的知识进行类比,发现在绝对路径中,使用了反斜杠,而反斜杠会把后面的字符进行转义,和后面的字母一起构成一个特殊字符。
而在windows中,绝对地址一般采用反斜杠进行表示,所以我在代码中使用两个反斜杠代表反斜杠这一个字符,即:
'\\'表示字符\
verilog
$readmemh("D:\\hcb\\computer_composition\\class2\\inst", S_CYCLE_CPU.IM.ins_memory);
问题九
指令跳转错误
在提交我的作业文件后,显示运行超时,无法通过在线平台分析我的代码。
所以我采用自己写testbench的方式进行调试。
下图是我写的测试文件(对于第二条指令,由于IM仅仅关心地址的低位,所以高位无需理睬,可以在测试的环境中置位0)
进行仿真,得到下图
对于上图中,发现无论对于当前地址是什么,下一条指令地址始终是0x00
,怀疑是图中的多路选择器(蓝色方框)出现问题。
对于控制信号中的s_npc进行显示,发现其竟然不是向量
查看对应的源代码(ctrl模块):
verilog
output reg s_npc,
在ctrl的控制模块,声明出现错误。
更改s_npc指令,使为为位宽为2的向量,然后对于MUX的控制信号从一位变为两位的所有信号进行排查。
下图为更改之后的波形图
发现控制信号正常,但是实际跳转并不正常,然后对于连接到npc的4选1多路选择器进行查看。
如图:
在这一步调试的过程发现两个错误(使用白色方框框出来的)
首先:Sel信号位数错误,更正mux4to1多选器中的位数为两位向量
然后:发现In0信号接线错误,在顶层模块中的该MUX的实例接线处检查,in0接线为beq_addr,如下图:
其是relative_jump_addr_calc模块的输出。进而检查relative_jump_addr_calc模块的module代码,发现addr的位数声明错误。如下图:
经过修改,发现可以正常跳转,波形正常。
可以按照程序执行的顺序逻辑进行跳转。
但是与此同时,又发现了一个问题,即对于绝对跳转指令,s_npc设置错误
检查发现,加法指令的op字段与寄存器跳转指令的op字段均为6'b0,所以,这时候应该看指令的func字段。
所以说:jr指令的op字段和R型指令相同,应该在op字段为000000的内部对func字段进行判断。
问题十:
仿真过程中发现信号为红色的
检查对应信号,发现并不要紧,这正是我的优化措施,当s_npc不选择beq指令跳转时,我们就不需要关心alu的zero到底输出什么,所以符合要求。
即:红色的信号不被MUX所选择,所以其值无关紧要,这样可以使得组合逻辑中对应真值表成为无关项,进而使得编译器可以进行更好的优化,生成更加合理的电路。
实验收获
在这一个实验中我有下面的收获:
- 首先,我对于MIPS的一个子集的指令有了较为熟悉的了解,我在这一次实验中,采用归类的思想,在CPU中,一次性对于一类指令进行添加,这对我理解MIPS的R型,J型,I型指令的格式具有很大的帮助。
其中:
R型指令包括了操作码、源寄存器、目标寄存器以及移位位数,func字段,着重对寄存器中的数字进行操作。
J型指令包括了操作码和立即数,可以改变程序的执行逻辑。
I型指令包括操作码,源寄存器,目标寄存器,立即数,可以让在汇编指令中给定的立即数进行运算。 - 我还掌握了CPU的数据通路的总体架构。其是有PC寄存器,指令存储器,算术逻辑单元,存储器等等单元构成的,其仅仅负责计算,属于组合逻辑。从一个输入到输出是有延时的,所以需要有时钟以及控制信号来控制数据通路进行运作。
- 我还对于控制器的控制信号的产生有了更深刻的了解。在进行实验课之前,我对于控制器感到神秘莫测,通过这一次实验,我认识到,控制器的输入就是取出来的指令,但是仅仅有指令的高6位(op字段)以及低6位(func)字段参与译码。
通过在实验步骤中把ctrl模块的各种控制信号罗列出来,我了解了对于某一个确切的指令,应该沿着其数据通路,分析应该给予这条指令的控制信号。 - 我明白了层次化设计的必要性。在实验开始之前,看着复杂的数据通路以及控制通路,感觉无从下手。但是在第一次课中,我学习了如何设计指令存储器(IM)、程序计数器(PC)、只能进行加法运算的算术逻辑单元(ALU)和通用寄存器组(GPR)等等元件的设计。在设计好之后,仅需要按照电路,在顶层模块中接线即可,而无需关心每一个模块的具体的功能。通过一步一步的设计和一层一层的构建,我深刻地体会到模块化设计的好处。通过这种设计方式,可以使系统的整体结构更加清晰,更具有可读性。
- 我理解了存储程序的设计思想。在存储程序中,程序和数据都是以二进制方式存储在存储器中的,计算机可以区分二者。在程序执行的过程之中,计算机能够高速地从存储器中提取指令并加以分析和执行。
如果要是没有存储程序,那么ALU的运算就得使用按钮来进行实时地指挥,而由于存储程序思想,仅需要在开始存入程序,程序就可以高速执行,无需人为干预。
在本次实验之中,我仅需要把机器代码存入到指令存储器中,然后就可以对CPU进行分析,而不需要一直告诉CPU其所需要的操作。 - 我可以采用verilog进行较为复杂的工程设计。在数字逻辑中,我仅仅写了有限状态机,D触发器,计时器等等简单的内容,系统性的思维不够强。在这一次的实验中,我通过模块来对不同的器件进行管理,并且通过对各个模块的编写,加深了基本的组合逻辑,锁存器等模块的理解。
- 我学会了使用Mars编写汇编,生成汇编。Mars能够将汇编代码转换为机器码,并以仿真的方式执行程序。
通过Mars,我可以对一段代码进行仿真,得出其的正确运行情况,然后导出机器码到16进制Text,然后再利用verilog testbench加载机器码到指令存储器,即可进行仿真。 - 我可以数列使用波形仿真,添加相应的波形。使用波形仿真是写verilog必会的一项技能,通过这一次的实验,我学会了对于具有大量信号的module进行添加显示的波形,并设置其值的显示进制。这对我分析和解决程序中的错误具有很大的帮助。
- 我了解了testbench的高级用法
我在第一节课的预习报告中了解了testbench的高级用法。
主要是系统任务的使用。其中,$stop
可以在进行一段时间的仿真后自动停止仿真,而不需要一直手动终止波形仿真。$memreadh
可以把文件中的数据逐行读入到存储单元中,$display
任务在仿真过程中使用,以便于调试和观察变量的值。$monitor
相比于$display
,在仿真过程中可以实时监视信号的值,并在其发生变化的时候进行输出。