在这一次实验中,需要实现可以执行以下指令的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进行功能测试以及仿真,必须先搭建一个框架,在这里首先按照教程,搭建一个可以实现addu的指令。
addu指令所需要用到的器件基本上囊括了数据通路的器件。
以下是addu指令的器件:
所以要实现一个addu指令,就先要搭建好上述的器件。
寄存器文件的读端口是组合逻辑,写端口是时序逻辑。
特别的是寄存器文件的0号寄存器,其实可以被写入的,只不过是每一次读出来的值并不相同,所以采用在读寄存器的时候进行判断其是不是零号寄存器。
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
在这里为了先让CPU跑起来,象征性地设计一个加法器。
assign c = a + b;
指令寄存器的空间大小有限,所以只是取低12位进行访存。
但是由于reg [31:0] ins_memory[1023:0]
,对于该数组的访问是一次性访问4字节,所以要把4字节作为一个整体进行访问,可以使用以下语句进行访问存储器
assign instruction = ins_memory[pc[11:0] >> 2];
同步低电平复位,所以复位信号的下降沿不作为敏感信号列表。
always @(posedge clock) begin
if(reset == 0) pc <= 32'h0000_3000;
else pc <= npc;
end
在顶层模块中,采用了名称关联,这样可以不需要关注各个端口的顺序,可读性更强。
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
汇编代码:
addu $2, $3, $4
addu $5, $6, $7
波形:
在图中可以看到寄存器组正确读出了位于3号以及4号寄存器中的值,然后把3与4相加,得到了正确的结果。
同时看到第3,4行的pc以及npc正确。
2号寄存器的值被写入3号以及4号的值,addu指令本地测试正确!
下标为所需要增加的指令
根据这一张表,发现需要增加的指令同为R型,基本的步骤是完全相同的,唯一有区别的就是ALU单元所执行的运算操作不同。
需要增加的单元:控制单元(在其中通过给定的指令的op以及func字段,确定ALU的具体操作)
需要修改的单元:ALU单元(响应由控制单元所生成的控制信号alu_op,并执行不同的操作)
在控制模块中需要产生一些对ALU行为进行控制的信号,为了增强可读性,采取宏定义。
我在宏定义之前表示了alu_op_,表示这是关于ALU操作控制信号的宏定义,这样做可以避免之后的设计中出现命名冲突。
`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字段进行译码。
大体框架如下(为了可读性,仅列举代码的框架,具体赋值通过表格形式展现):
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
赋值情况如下表:
操作助记符 | 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 |
在ALU中,仅仅需要根据控制器传过来的控制信号,直接进行相应的运算。
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
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]) );
汇编代码:
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
。对于波形分析之后,发现执行结果完全正确,本地测试通过!
对下面的指令进行分析
为了进行层次化设计,专门实现一个mux2to1模块,而不是简单使用条件运算符进行运算。
在上面的需求分析中,多路选择器可能是5位的,也可能是32位的,为了适应不同的位宽,定义带有参数的多路选择器。
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
在case语句中加入针对lui的操作,同时更新一下宏定义。
case(aluop)
............
`alu_op_lui: c = {b[15:0], 16'h0};
default c = 0;
endcase
符号拓展(extender)为组合逻辑,当signextend为1的时候对立即数进行符号拓展,反之进行零拓展。
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
在控制模块中,新增二选一多路选择器的控制信号。
增加的方式是case语句的对应位置增加赋值语句。
代码框架(仅仅展示代码框架,为了可读性较好,把具体的赋值过程放在后面的表格中):
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 |
其中,ALU,IM,PC的接线不需要改动,在这里没有列出来。
`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
汇编代码
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 |
经过对波形的分析,结果正确,本地测试通过!
如图,推访存类指令进行分析,发现之前的绝大部分内容全部可以复用。
需要新加入的有数据存储器以及控制写入寄存器文件的数据的多选器。
DM模块的实现与指令存储器几乎一致,其写入是要在时钟上升沿的时候进行,读取为组合逻辑。
同时仅仅sw具有写DM的功能,所以DM还需要写使能信号控制。
由于在一个周期中,DM被读取或者是被写入,所以读与写可以共用地址线。
关键代码如下
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
首先,在ctrl模块中增加连接到gpr写入数据端的多路选择器控制信号。
仅仅有lw指令需要把数据存储器的输出写入到寄存器中,所以可以设置仅仅当指令为lw的时候,mux2to1选择数据存储器的输出,否则选择ALU的输出。
然后,需要在控制模块中把dm接入
在顶层模块中,需要改动的有:
DM的调用模块,把DM接入
dm DM(
.data_out(dm_data_out),//连接到下面的多路选择器
.clock(clock),
.mem_write(mem_write),//由ctrl模块进行控制
.address(dm_address),//连接到ALU的输出
.data_in(dm_data_in) );//连接到gpr的第二个数据输出端口
多路选择器
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的数据写入端口
在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 |
汇编代码
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
对于这些指令的分析如下:
如图,对于绿色箭头所指示的部分的多路选择器,对其进行分析。
PC + 4 +(sign_extend(offset)<<2)
处,否则跳转到PC+4.通过以上的分析,发现:增加跳转指令需要进行以下的操作:
在声明zero端口
module alu(c, zero, a, b, aluop);
output zero;
assign zero = (c == 0);
同样为了解决数据宽度的问题,采用WIDTH可变参数。
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
当根据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 |
在表格中,设置了默认情况,用于防止出现多余的锁存器。
首先增加段内绝对跳转(绿色框所标识的)
根据图示得知,该模块有两个输入(2‘b00可以直接在模块内生成),一个输出
地址的拼接方法为:
addr = {PC[31..28 ], instr_index ,2’b00}
给这一个模块起名字为intra_segment_jump_addr_calc
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
: 表示计算得到的相对跳转的目标地址。最终得到这一个模块:
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,左移二运算器
在这里采用仿真测试进行DEBUG,详细的操作见后面的“遇到的问题与解决办法”,这里进列举部分。
现在编写如下代码测试je指令以及beq指令
发现跳转正常
解释:第二条指令跳转到第四条指令
第四条指令条件不满足,向下执行
第五条指令条件满足,跳转到第一条指令
在设计存储器(IM)的时候,对于一个地址,在时钟上升沿写入的内容,在该周期内无法读出,不是组合逻辑。
错误代码局部:
always @ (pc) begin
instruction = ins_memory[pc[11:0] >> 2];
end
经过仔细检查代码,发现always语句中的敏感信号列表应该包含所有变化的值,而我的敏感列表仅仅有地址,没有实例化的数组,这样的化数组内容改变,敏感列表中并不包含,应该把always()中的内容写为*
提交后错误如图
发现num_write赋值错误,检查我的代码,发现如下:
.num_write(instruction[15:10]) , //写寄存器编号
再次比对实验指导书,发现rd子段应该是instruction[15:11]
,修改后,问题解决。
正确定义宏,但是在使用过程中报错误。
上网查询资料,发现应该在应用宏定义的时候,在宏名称前面加上 ` 符号
随着指令ALU操作的指令定义数目增加,如果在每一个文件中拷贝相同的宏定义,那么容易遗漏,且可读性极差。
解决:学习C语言中的#include
,在verilog中,生成一个header.v
文件,把宏定义均放在这一个文件中,然后在需要使用宏定义的文件中开头输入:
`include "header.v"
查阅对应的instruction,其表示slt。通过对比GPRa和GPR.b,发现我的程序实现的是无符号数字的比较,
解决办法:在网络中查阅资料得知,有三种方法可以进行有符号数比较。
在声明变量的时候采用wire signed [31:0]
在进行比较之前,采用系统任务把无符号的数转化为有符号的数字
if($signed(a) < $signed(b))
分析数字的表示进行比较
如果符号位相同,那么真实的大小与补码直接按照无符号数字比较的结果是相同的。
如果符号位不同,那么负数一定小于正数
assign 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的读数指令,
data_memory[address[9:0]] <= data_in;
发现是取addr的时候取错了,由于数据存储器仅仅支持4个字节一起读取,一个data_memory就代表着四个直接,所以应该更改为:
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中写入应有的值。
问题代码:
$readmemh("D:\hcb\computer_composition\class2\inst", S_CYCLE_CPU.IM.ins_memory);
IM的数据并没有别写入,所以全部都是未知态
解决方法:仔细检查问题代码,根据C语言的知识进行类比,发现在绝对路径中,使用了反斜杠,而反斜杠会把后面的字符进行转义,和后面的字母一起构成一个特殊字符。
而在windows中,绝对地址一般采用反斜杠进行表示,所以我在代码中使用两个反斜杠代表反斜杠这一个字符,即:
'\\'表示字符\
$readmemh("D:\\hcb\\computer_composition\\class2\\inst", S_CYCLE_CPU.IM.ins_memory);
指令跳转错误
在提交我的作业文件后,显示运行超时,无法通过在线平台分析我的代码。
所以我采用自己写testbench的方式进行调试。
下图是我写的测试文件(对于第二条指令,由于IM仅仅关心地址的低位,所以高位无需理睬,可以在测试的环境中置位0)
进行仿真,得到下图
对于上图中,发现无论对于当前地址是什么,下一条指令地址始终是0x00
,怀疑是图中的多路选择器(蓝色方框)出现问题。
对于控制信号中的s_npc进行显示,发现其竟然不是向量
查看对应的源代码(ctrl模块):
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所选择,所以其值无关紧要,这样可以使得组合逻辑中对应真值表成为无关项,进而使得编译器可以进行更好的优化,生成更加合理的电路。
在这一个实验中我有下面的收获:
$stop
可以在进行一段时间的仿真后自动停止仿真,而不需要一直手动终止波形仿真。$memreadh
可以把文件中的数据逐行读入到存储单元中,$display
任务在仿真过程中使用,以便于调试和观察变量的值。$monitor
相比于$display
,在仿真过程中可以实时监视信号的值,并在其发生变化的时候进行输出。