以下来自本人的数字系统设计课程的实验设计报告,开发板采用的是ego1,平台采用VIVADO,VIVADO-modelsim联合仿真。其中代码来自北航夏宇闻老师编著的《Verilog数字系统设计》,具体可以参考他的教材,本文的代码仅是对内容进行复现。
学习RISC_CPU的基本结构和原理,了解Verilog HDL仿真和综合工具的潜力,学习verilog设计方法对软/硬件联合设计和验证的意义。
学习精简指令集CPU的架构,完成了简化RISC_CPU设计,包含时钟发生器,指令寄存器,累加器,算术逻辑运算单元,数据控制器,状态控制器,程序计数器和地址多路器等模块。并且针对每个模块单独编写tb文件,完成各个模块的功能仿真。为上述系统添加ROM、RAM等模块,并完成对整个系统的功能仿真。
完成之后,对上述简化RISC_CPU进行综合,了解其资源占用情况,自行设计验证方法,下载到ego1开发板进行fpga验证。
Cpu就是中央处理单元的英文缩写,是计算机的核心部件,cpu处理信息数据可以分为两步:第一步将数据和程序输入到cpu当中,第二步从第一条指令的地址开始执行,得到结果后结束,开始下一条指令。
那么一个cpu需要具有的基本功能就是:1)取指令,2)分析指令,或说是指令译码,3)执行指令。
Cpu需要能够完成的功能有:能对指令进行译码并且执行相应的动作、可以进行算数和逻辑运算、能与存储器、外设交换数据、提供整个系统需要的控制。针对上述的功能,我们可以得知:
一个cpu需要具有的部件至少包括:
1)算数逻辑运算部件(ALU)
2)累加器
3)程序计数器
4)指令寄存器
5)指令译码器
6)时序和控制部件
Risc_cpu时精简指令集计算机的缩写,是一种八十年代出现的改进过的cpu架构。Risc_cpu和一般cpu不同的地方在于:1)risc_cpu的指令系统更加精简,指令集设计更加合理,提高了运算速度。2)risc_cpu的时序控制信号形成部件是通过硬布线逻辑实现的而不是通过微程序控制的方式,这就意味着其省去了读取微控制指令的时间,进一步提高了速度。
Risc_cpu虽然是一个复杂的逻辑电路,但是其基本部件并不复杂,可以分为这样八个部件:
1)时钟发生器
2)指令寄存器
3)累加器
4)RISC CPU算术逻辑单元
5)数据控制器
6)状态控制器
7)程序计数器
8)地址多路器
下面的实验内容将按这个顺序展开,对每个模块进行设计和单独仿真。
1)时钟发生器是risc_cpu中所有时钟的来源,考虑到我们在开发板上只有一个系统时钟,在这里设计时钟发生器,输出CLK1,ALU_CLK,以及FETCH,其中,fetch是用来上升沿触发cpu控制器开始执行一条指令,同时也可以控制地址多路器输出指令地址和数据地址。Clk1是用来作为指令寄存器、累加器、状态控制器的时钟,alu_clk专用为算术逻辑运算单元的触发。
2).v程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 09:21:49
// Design Name: clkgenerator,时钟发生器
module clkgenerator(clk,rst,clk1,clk2,clk4,fetch,alu_clk);
input clk,rst;
output wire clk1;
output reg clk2,clk4,fetch,alu_clk;//三个不同时钟,fetch时钟,alu时钟,全部要是reg类型
reg [7:0] state;
parameter S1=8'b0000_0001,
S2=8'b0000_0010,
S3=8'b0000_0100,
S4=8'b0000_1000,
S5=8'b0001_0000,
S6=8'b0010_0000,
S7=8'b0100_0000,
S8=8'b1000_0000,
idle=8'b0000_0000;
//利用状态机来写,提高了代码的可综合性
assign clk1=~clk;
always @(negedge clk)
if(rst)
begin
clk2<=0;
clk4<=0;
fetch<=0;
alu_clk<=0;
state<=idle;
end
else
begin
case(state)
S1:
begin
clk2<=~clk2;//clk2每次都反向,其实是clk的二倍
alu_clk<=~alu_clk;
state<=S2;
end
S2:
begin
clk2<=~clk2;
clk4<=~clk4;
alu_clk<=~alu_clk;//alu在一个大周期(8个系统周期)内只有一次
state<=S3;
end
S3:
begin
clk2<=~clk2;
state<=S4;
end
S4:
begin
clk2<=~clk2;
clk4<=~clk4;//clk4每两个系统时钟变一次,就是四倍的系统周期
fetch<=~fetch;//fetch每个大周期内变一次,是八个系统周期
state<=S5;
end
S5:
begin
clk2<=~clk2;
state<=S6;
end
S6:
begin
clk2<=~clk2;
clk4<=~clk4;
state<=S7;
end
S7:
begin
clk2<=~clk2;
state<=S8;
end
S8:
begin
clk2<=~clk2;
clk4<=~clk4;
fetch<=~fetch;
state<=S1;
end
idle:
begin
state<=S1;//初始状态
end
default://为了避免电路级错误而写default
begin
state<=idle;
end
endcase
end
endmodule
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
//
// Create Date: 2022/03/24 09:37:56
module tb();
reg clk,rst;
wire clk1,clk2,clk4,fetch,alu_clk;
initial
begin
clk=0;rst=0;
#10 rst=1;
#10 rst=0;
end
clkgenerator my_clkgenerator(clk,rst,clk1,clk2,clk4,fetch,alu_clk);
always #2 clk=~clk;//产生一个系统时钟。
endmodule
4)PC仿真结果:
观察仿真结果发现:reset信号可以使输出全0,输出的五个时钟都与预期功能相同,该模块功能正常。
1)指令寄存器是用于寄存指令的模块,指令通过总线输入,存入高八位或低八位的寄存器中。触发时钟是clk1,但不是每个clk1的上升沿都会触发,而是受load_ir输入信号的控制。Load_ir指示了总线上是否正在传输指令。
每条指令为2个字节,16位,高三位是操作码,低12位是地址。在本实验中,我们设计的数据总线是八位的,那么每条指令需要取两次,先取高八位,再去低八位,当前取得是哪一位,是由state信号来指示的。
2).V程序:
`timescale 1ns / 1ps
//
// Company: 微电子92
// Engineer: jeff
// Create Date: 2022/03/24 09:55:26
// Design Name:
// Module Name: instr_reg
module instr_reg(opc_iraddr,data,ena,clk1,rst);
output [15:0] opc_iraddr;//这个就是输出的16进制opcode的地址
input [7:0] data; //这个是输入数据
input ena,clk1,rst; // load_ir输入进来,表示这时是否进行指令地址寄存
reg [15:0] opc_iraddr;
reg state;
always @(posedge clk1)
begin
if(rst)
begin
opc_iraddr<=16'b0000_0000_0000_0000;
state<=1'b0;
end
else
begin
if(ena)
begin
casex(state)
1'b0:
begin
opc_iraddr[15:8]<=data;//先存高八位
state<=1;
end
1'b1:
begin
opc_iraddr[7:0]<=data;//再存低八位
state<=0;
end
default:
begin
opc_iraddr[15:0]<=16'bxxxx_xxxx_xxxx_xxxx;
state<=1'bx;
end
endcase
end
else
state<=1'b0;//如果总线不是指令,则state一直是0
end
end
endmodule
3)tb程序:
`timescale 1ns / 1ps
//
// Company: 微电子92
// Engineer: jeff
// Create Date: 2022/03/24 10:06:40
module tb();
reg clk1,rst,ena;
reg [7:0] data;
wire [15:0] opc_iraddr;
instr_reg my_instr_reg(opc_iraddr,data,ena,clk1,rst);
initial
begin
clk1=1;rst=0;ena=0;
#10 rst=1; ena=1;
#10 rst=0; ena=0;
#10 rst=0; ena=1;
data=8'b1010_1010;
#4 data=8'b1111_1111;
end
always #2 clk1=~clk1;
endmodule
可以看出,刚开始仿真时,输出opc为x,当输入rst为1时,虽然ena为1,输出仍然被置位0;在rst为0时,ena为0,也没有进行读取。而当rst是0,ena又为1时,开始读取。
第一个时钟上升沿,opc的高八位被写入,第二个时钟上升沿,opc低八位被写入。
1)累加器是用于存放当前的结果,是双目运算(类似a+b)的数据来源。复位之后的值为0,在ena接收到cpu控制模块的load_cc时,在clk1的控制下,接受来自总线的数据。
2).v程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 10:19:48
// Design Name: 累加器模块
module accumulator(accum,data,ena,clk1,rst);
output [7:0] accum;
input [7:0] data;
input ena,clk1,rst;
reg [7:0] accum;
always @(posedge clk1 or negedge rst)
begin
if(rst)
accum<=8'b0000_0000;//进行reset
else
if(ena)
accum<=data;//ena时,信号正常输出
end
endmodule
由于这个模块比较简单,没有设计tb程序进行仿真。
1)算数运算逻辑单元是进行运算的核心,它可以根据输入的八种不同操作码实现相应的加、与、异或、跳转等八种基本操作运算。
运算受输入的alu专用clk控制,opcode即是操作码,accum和data是两个不用的操作数,输出不仅有一个计算结果,还有一个零标志位输出。
2).v程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 10:27:22
// Design Name: alu算数逻辑单元
module alu(alu_out,zero,data,accum,alu_clk,opcode);
output [7:0] alu_out;
output zero;
input [2:0] opcode;
input [7:0] data,accum;
input alu_clk;
reg [7:0] alu_out;
parameter HLT=3'b000, //暂停指令,将操作数accum传输到输出
SKZ=3'b001, //跳过指令,也是将操作数传输到输出
ADD=3'b010, //加法
ANDD=3'b011, //位与运算
XORR=3'b100, //位异或运算
LDA=3'b101, //传输指令,将data传输到输出
STO=3'b110, //存储指令,将accum传输到输出
JMP=3'b111; //跳转指令,将accum传输到输出
assign zero=!accum;//accum如果是全0,那么就输出zero标识位为1;
always @(posedge alu_clk)
begin
casex(opcode)
HLT: alu_out<=accum;
SKZ: alu_out<=accum;
ADD: alu_out<=data+accum;
ANDD: alu_out<=data&accum;
XORR: alu_out<=data^accum;
LDA: alu_out<=data;
STO: alu_out<=accum;
JMP: alu_out<=accum;
default: alu_out<=8'bxxxx_xxxx;
endcase
end
endmodule
3)tb程序:
1. `timescale 1ns / 1ps
2. //
3. // Company:
4. // Engineer: jeff
5. // Create Date: 2022/03/24 10:37:59
6. module tb();
7. reg alu_clk;
8. reg [7:0] data,accum;
9. reg [2:0] opcode;
10. wire zero;
11. wire [7:0] alu_out;
12. alu my_alu(alu_out,zero,data,accum,alu_clk,opcode);
13.
14. initial
15. begin
16. alu_clk=1;
17. opcode=3'd0;
18. data=8'd10;
19. accum=8'd5;
20. end
21. always #4 opcode=opcode+1;//遍历opcode的全部状态,测试alu的所有功能
22. always #2 alu_clk=~alu_clk;
23. endmodule
4)PC仿真结果:
由上面的波形图形可以看出,每个opcode对应的操作和下面的预期功能相同。
HLT=3’b000, //暂停指令,将操作数accum传输到输出
SKZ=3’b001, //跳过指令,也是将操作数accum传输到输出
ADD=3’b010, //加法
ANDD=3’b011, //位与运算
XORR=3’b100, //位异或运算
LDA=3’b101, //传输指令,将data传输到输出
STO=3’b110, //存储指令,将accum传输到输出
JMP=3’b111; //跳转指令,将accum传输到输出
测试结果符合预期,该模块设计正确。
1)数据控制器的作用是控制累加器进行数据输出,需要有ena来指示这时总线的传输状态,有时要传输指令,有时要传输ram区或接口的数据。
累加器的数据只有在往ram或端口写时才允许输出,否则就出于高阻态。
2).v程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 11:01:24
// Design Name: 数据控制器
module data_ctrl(data,in,data_ena);
output [7:0] data;
input [7:0] in;
input data_ena;
assign data=(data_ena)? in:8'bzzzz_zzzz;
//data_ena如果为0,输出高阻;
//data_ena如果为1,将输入进行输出;
//这相当于一个受控的buffer
endmodule
由于本模块的程序较为简单,不进行tb的设计和仿真。
1)地址多路器是用于选择输出的地址时PC(程序计数)的地址,还是数据或者端口的地址。每个指令周期的前四个周期用于从rom中读取指令,输出的应该时pc地址。
后四个周期用于对ram或端口的读写,该地址由指令给出。
这个8分频的控制时钟就是fetch时钟。
2).v程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 11:07:11
// Design Name: 地址多路器
module addr(addr,fetch,ir_addr,pc_addr);
output [12:0] addr;
input [12:0] ir_addr,pc_addr;
input fetch;
assign addr=fetch? pc_addr:ir_addr;//在fetch的高电平期间读pc地址,低电平期间读数据或端口的地址
endmodule
由于本模块的程序较为简单,不进行tb的设计和仿真。
1)程序计数器用于提供指令地址,以进行指令读取。
指令按地址的顺序存在存储器中。我们要读取这些地址的数据,就必须先形成地址,第一是可以按顺序执行,每次加一,第二是遇到一些特定的指令,比如JMP,就会形成一个新地址,跳转到新地址中。
这里的输入除了address信号之外,还有复位rst,在复位之后,指令指针变为0,即每次重启是从0开始读取。(每条执行需要2个时钟),这时pc_addr被增加2,只想下一条指令。
2).v程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 11:16:39
// Design Name: 程序计数器
module counter(pc_addr,ir_addr,load,clk,rst);
output [12:0] pc_addr;
input [12:0] ir_addr;
input load,clk,rst;
reg [12:0] pc_addr;
always @(posedge clk or posedge rst)
begin
if(rst)
pc_addr<=13'b0000_0000_0000;
else
if(load)//load是1时,直接将输入进行传输
pc_addr<=ir_addr;
else
pc_addr<=pc_addr+1;
end
endmodule
由于本模块的程序较为简单,不进行tb的设计和仿真。
1)状态控制器分为两部分,一个是状态机(MACHINE),一个是状态控制器(MACHINECTRL)。
*状态机控制器的功能就是接受复位信号时,当rst有效时通过信号ena使其为0输入到状态机中使其停止工作。
*在状态机模块:指令周期由八个时钟周期组成,每个时钟周期都要完成固定的操作。
第零个时钟:cpu状态控制器输出:rd和load_ir为高电平,其他为低电平,这时指令寄存 器寄存由rom送来的高8位指令代码。
第一个时钟:遇上一个相比,指示inc_pc由0变为1,因此PC会增加1,rom送来8位指 令代码,指令寄存器寄存该代码。
第二个时钟:空操作。
第三个时钟:pc增加一,指向下一条指令。
如果操作符为HLT,那么输出HLT为高,
否则,除了PC要加一之外,其他控制线输出为0。
第四个时钟:如果操作符为AND,ADD,XOR,LDA,那么九读相应的数据;
如果为LDA,那么就把地址送给程序计数器;如果为STO,输出累加器数据。
第五个时钟:如果操作符为ADD,ANDD,XORR,那么算数逻辑单元继续进行相应的运算,
如果是LDA,就把数据通过算术逻辑单眼送给累加器,
如果是SKZ,先判断累加器是否为0,
如果是0,PC+1,如果是1,PC不变,
如果是JUMP,所存目标地址,
如果是STO,则将数据写入地址。
第六个时钟:空操作;
第七个时钟:若操作符是SKZ,并且累加器为0,则PC值再增1,跳过一条指令,否则不变。
3)tb程序:
状态控制器:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 11:27:56
// Design Name: 状态机控制器
module state_ctrl(ena,fetch,rst);
output ena;
input fetch,rst;
reg ena;
always @(posedge fetch or posedge rst)
begin
if(rst)
ena<=0;
else
ena<=1;
//state_ctrl模块的作用就是:在fetch上升沿时刻,如果rst是高,那就enable为低,否则就正常工作
end
endmodule
状态机:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 11:27:21
// Design Name: 状态机
module state_machine(inc_pc,load_acc,load_pc,rd,wr,load_ir,
datactl_ena,halt,clk1,zero,ena,opcode);
output inc_pc,load_acc,load_pc;//指示信号
output rd,wr;//指示是进行读还是写状态
output load_ir; //指示是否进行装载(寄存地址)
output datactl_ena,halt;//输出的是data_ctrl的使能,以及halt信号
input clk1,zero,ena; //输入的零标志位,主控时钟是clk1(最快的那个),使能(由state_ctrl模块输出)
input [2:0] opcode;//输入的操作码
reg inc_pc,load_acc,load_pc,rd,wr,load_ir;
reg datactl_ena,halt;
parameter HLT = 3 'b000,
SKZ = 3 'b001,
ADD = 3 'b010,
ANDD = 3 'b011,
XORR = 3 'b100,
LDA = 3 'b101,
STO = 3 'b110,
JMP = 3 'b111;
reg [2:0] state;
always @(negedge clk1)
begin
if(!ena)
begin
state<=3'b000;
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;//在enable信号为低时,进行全部置零的操作
end
else
ctl_cycle;//在enable为1的情况下,每次clk1的上升沿都进行一次task
end
//———————ctl_cycle task————————
task ctl_cycle;
begin
casex(state)//是一个state数量为8的状态机,一个完整的操作周期
3'b000://进来后的第一个状态
begin
{inc_pc,load_acc,load_pc,rd}<=4'b0001;//第一次:rd和load_ir置高,寄存器读rom传过来的8位指令数据(高八位)
{wr,load_ir,datactl_ena,halt}<=4'b0100;
state<=3'b001;//按顺序进行下面的状态!
end
3'b001:
begin//第二次:inc_pc和rd,load_ir置高,pc会加一,并且继续读rom的八位指令数据(低八位)
{inc_pc,load_acc,load_pc,rd}<=4'b1001;
{wr,load_ir,datactl_ena,halt}<=4'b0100;
state<=3'b010;
end
3'b010:
begin//第三次:空操作
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
state<=3'b011;
end
3'b011:
begin//第四次:pc会加一,但前提是操作码不是HLT
if(opcode==HLT)
begin//如果操作码是hlt,说明要暂停,这时输出一个hlt标志位,并且是pc加一
{inc_pc,load_acc,load_pc,rd}<=4'b1000;
{wr,load_ir,datactl_ena,halt}<=4'b0001;
end
else
begin//如果不是hlt,pc正常加一
{inc_pc,load_acc,load_pc,rd}<=4'b1000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
state<=3'b100;
end
3'b100:
begin//第五次:对不同操作符进行分支赋值
if(opcode==JMP)
begin//如果是jump,跳过这一条,那么就直接load_pc,把目的地址送给程序计数器
{inc_pc,load_acc,load_pc,rd}<=4'b0010;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
else if(opcode==ADD || opcode==ANDD ||
opcode==XORR|| opcode==LDA)
begin//如果是ADD,ANDD,XORR,LDA,那就正常进行read,计算得到数据
{inc_pc,load_acc,load_pc,rd}<=4'b0001;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
else if(opcode==STO)
begin//如果是STO,那就将datactl_ena(数据控制模块使能)置高,输出累加器的数据
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0010;
end
else
begin//否则,就全部为0
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
state<=3'b101;
end
3'b101:
begin//第六次:
if(opcode==ADD || opcode==ANDD ||
opcode==XORR || opcode==LDA)
begin//如果是上面这些操作,那就要继续进行这些操作,与累加器的输出进行运算
{inc_pc,load_acc,load_pc,rd}<=4'b0101;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
else if(opcode==SKZ && zero==1)
begin//如果是SKz,先判断是否是零,如果是零就pc+1,否则就全零空操作
{inc_pc,load_acc,load_pc,rd}<=4'b1000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
else if(opcode==JMP)
begin//如果是JMP,那就pc+1,然后load_pc,锁定目的地址
{inc_pc,load_acc,load_pc,rd}<=4'b1010;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
else if(opcode==STO)
begin//如果是STO,就将数据写入地址处
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b1010;
end
else
begin
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
state<=3'b110;
end
3'b110:
begin//第七次,空操作
if(opcode==STO)
begin//如果是STO,那么需要使数据控制模块enable
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0010;
end
else if(opcode==ADD || opcode==ANDD ||
opcode==XORR || opcode==LDA)
begin//如果是这些操作码,那就进行read
{inc_pc,load_acc,load_pc,rd}<=4'b0001;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
else
begin
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
state<=3'b111;
end
3'b111:
begin//第八次
if(opcode==SKZ && zero==1)
begin//如果是SKZ,并且是零,那么就pc+1,否则空操作
{inc_pc,load_acc,load_pc,rd}<=4'b1000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
else
begin
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
end
state<=3'b000;
end
default:
begin
{inc_pc,load_acc,load_pc,rd}<=4'b0000;
{wr,load_ir,datactl_ena,halt}<=4'b0000;
state<=3'b000;
end
endcase
end
endtask
//————————end of task————————
endmodule
1)地址译码器用于产生选通信号,选通RAM或者ROM。
当地址码的前两位全1时,才选通ram,其他情况都是选通rom。
2).V程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 16:22:58
// Design Name: decoder
module decoder(addr,rom_sel,ram_sel);
output rom_sel,ram_sel;
input [12:0] addr;
reg rom_sel,ram_sel;
always @(addr)
begin
casex(addr)
13'b1_1xxx_xxxx_xxxx:{rom_sel,ram_sel}<=2'b01;
13'b0_xxxx_xxxx_xxxx:{rom_sel,ram_sel}<=2'b10;
13'b1_0xxx_xxxx_xxxx:{rom_sel,ram_sel}<=2'b10;
default:{rom_sel,ram_sel}<=2'b00;
endcase
//只有地址码的前两位全1,才选通ram,否则一直是rom
end
endmodule
1)rom是程序装在的存储,可读不可写,ram用于存放数据,可读可写。
2).v程序:
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 16:32:19
// Design Name: ram
module ram(data,addr,read,write,ena);
output [7:0] data;
input [9:0] addr;
input read,write,ena;
reg [7:0] ram [10'h3ff: 0];
assign data=(read && ena)?ram[addr]:8'hzz;
always @(posedge write)
begin
ram[addr]<=data;
end
endmodule
`timescale 1ns / 1ps
//
// Company:
// Engineer: jeff
// Create Date: 2022/03/24 16:32:19
// Design Name: rom
module rom(data,addr,read,ena);
output [7:0] data;
input [12:0] addr;
input read,ena;
reg [7:0] memory [13'h1fff : 0];
wire [7:0] data;
assign data=(read && ena)?memory[addr]:8'bzzzz_zzzz;
//定义了一个8位宽,深度为1_1111_1111的存储阵列
//当read模式、enable时,读addr的存储指令,否则就是高阻态
endmodule
1)例化了前面的9个模块。
2).v程序
`timescale 1ns / 1ps
//
// Company: Jeff
// Engineer: 孟祥健
//
// Create Date: 2022/03/24 16:46:32
// Design Name:
module risc_cpu(clk,reset,halt,rd,wr,addr,data);
input clk,reset;
output rd,wr,addr,halt;
inout data;
wire clk,reset,halt;
wire [7:0] data;
wire [12:0] addr;
wire rd,wr;
wire clk1,fetch,alu_clk;
wire [2:0] opcode;
wire [12:0] ir_addr,pc_addr;
wire [7:0] alu_out,accum;
wire zero,inc_pc,load_acc,load_pc,load_ir,data_ena,contr_ena;
//1时钟生成器
clkgenerator my_clkgenerator(.clk(clk),.clk1(clk1),.fetch(fetch),
.alu_clk(alu_clk),.rst(reset));
//2指令寄存器
instr_reg my_instr_reg(.opc_iraddr({opcode,ir_addr[12:0]}),.data(data),.ena(load_ir),
.clk1(clk1),.rst(reset));
//3累加器
accumulator my_accumulator(.accum(accum),.data(alu_out),.ena(load_ir),
.clk1(clk1),.rst(reset));
//4算数逻辑单元:
alu my_alu(.alu_out(alu_out),.zero(zero),.data(data),.accum(accum),
.alu_clk(alu_clk),.opcode(opcode));
//5状态机控制模块:
state_ctrl my_state_ctrl(.ena(contr_ena),.fetch(fetch),.rst(reset));
//6状态机模块:
state_machine my_machine (.inc_pc(inc_pc),.load_acc(load_acc),.load_pc(load_pc),
.rd(rd), .wr(wr), .load_ir(load_ir), .clk1(clk1),
.datactl_ena(data_ena), .halt(halt), .zero(zero),
.ena(contr_ena),.opcode(opcode));
//7数据控制器(累加器)
data_ctrl my_data_ctrl(.data(data),.in(alu_out),.data_ena(data_ena));
//8地址多路器
addr my_addr(.addr(addr),.fetch(fetch),.ir_addr(ir_addr),.pc_addr(pc_addr));
//9程序计数器
counter my_counter(.pc_addr(pc_addr),.ir_addr(ir_addr),
.load(load_pc),.clk(inc_pc),.rst(reset));
endmodule
要进行PC端的测试,这里没有使用vivado的内置仿真器,而是使用的是modelsim进行仿真。
Tb文件如下:
`timescale 1ns / 100ps
`define PERIOD 100 // matches clk_gen.v
//
// Company: 微电子92
// Engineer: 孟祥健
// Create Date: 2022/03/24 17:18:43
// Design Name: tb仿真文件
module tb();
reg reset_req,clock;
integer test;
reg [(3*8):0] mnemonic; //array that holds 3 8-bit ASCII characters
reg [12:0] PC_addr,IR_addr;
wire [7:0] data;
wire [12:0] addr;
wire rd,wr,halt,ram_sel,rom_sel;
//----------------------------------------------------------------------------
risc_cpu tb_risc_cpu(.clk(clock),.reset(reset_req),.halt(halt),.rd(rd),
.wr(wr),.addr(addr),.data(data));
ram tb_ram(.addr(addr[9:0]),.read(rd),.write(wr),.ena(ram_sel),.data(data));
rom tb_rom(.addr(addr),.read(rd),.ena(rom_sel),.data(data));
decoder tb_decoder(.addr(addr),.ram_sel(ram_sel),.rom_sel(rom_sel));
//------------------------------------------------------------------------------
initial
begin
clock=1;
//display time in nanoseconds
$timeformat ( -9, 1, " ns", 12);
display_debug_message;
sys_reset;
test1;
$stop;
test2;
$stop;
test3;
$stop;
end
task display_debug_message;
begin
$display("\n******************************************************");
$display("* THE FOLLOWING DEBUG TASK ARE AVAILABLE: *");
$display("* \"test1; \" to load the 1st diagnostic progran. *");
$display("* \"test2; \" to load the 2nd diagnostic program. *");
$display("* \"test3; \" to load the Fibonacci program. *");
$display("******************************************************\n");
end
endtask
task test1;
begin
test = 0;
//disable MONITOR;
$readmemb ("test1.pro", tb_rom.memory);
$display("rom loaded successfully!");
$readmemb("test1.dat",tb_ram.ram);
$display("ram loaded successfully!");
#1 test = 1;
#14800 ;
sys_reset;
end
endtask
task test2;
begin
test = 0;
disable MONITOR;
$readmemb("test2.pro",tb_rom.memory);
$display("rom loaded successfully!");
$readmemb("test2.dat",tb_ram.ram);
$display("ram loaded successfully!");
#1 test = 2;
#11600;
sys_reset;
end
endtask
task test3;
begin
test = 0;
disable MONITOR;
$readmemb("test3.pro",tb_rom.memory);
$display("rom loaded successfully!");
$readmemb("test3.dat",tb_ram.ram);
$display("ram loaded successfully!");
#1 test = 3;
#94000;
sys_reset;
end
endtask
task sys_reset;
begin
reset_req = 0;
#(`PERIOD*0.7) reset_req = 1;
#(1.5*`PERIOD) reset_req = 0;
end
endtask
always @(test)
begin: MONITOR
case (test)
1: begin //display results when running test 1
$display("\n*** RUNNING CPUtest1 - The Basic CPU Diagnostic Program ***");
$display("\n TIME PC INSTR ADDR DATA ");
$display(" ---------- ---- ----- ----- ----- ");
while (test == 1)
@(tb_risc_cpu.my_addr.pc_addr)//fixed
if ((tb_risc_cpu.my_addr.pc_addr%2==1)&& (tb_risc_cpu.my_addr.fetch == 1))
//if ((tb_risc_cpu.my_addr.pc_addr%2 == 1)&&(tb_risc_cpu.my_addr.fetch == 1))//fixed
begin
# 60 PC_addr<=tb_risc_cpu.my_addr.pc_addr-1;
IR_addr <=tb_risc_cpu.my_addr.ir_addr;
# 340 $strobe("%t %h %s %h %h",$time,PC_addr,mnemonic,IR_addr,data );
//HERE DATA HAS BEEN CHANGED T-CPU-M-REGISTER.DATA
end
end
2: begin
$display("\n*** RUNNING CPUtest2 - The Advanced CPU Diagnostic Program ***");
$display("\n TIME PC INSTR ADDR DATA ");
$display(" ---------- --- ----- ----- ---- ");
while (test == 2)
@(tb_risc_cpu.my_addr.pc_addr)
if ((tb_risc_cpu.my_addr.pc_addr%2 == 1)
&& (tb_risc_cpu.my_addr.fetch == 1))
begin
# 60 PC_addr = tb_risc_cpu.my_addr.pc_addr - 1 ;
IR_addr = tb_risc_cpu.my_addr.ir_addr;
# 340 $strobe("%t %h %s %h %h", $time, PC_addr,
mnemonic, IR_addr, data );
end
end
3: begin
$display("\n*** RUNNING CPUtest3 - An Executable Program ***");
$display("*** This program should calculate the fibonacci ***");
$display("\n TIME FIBONACCI NUMBER");
$display( " --------- -----------------");
while (test == 3)
begin
wait ( tb_risc_cpu.my_alu.opcode == 3'h1) // display Fib. No. at end of program loop
$strobe("%t %d", $time,tb_ram.ram[10'h2]);
wait ( tb_risc_cpu.my_alu.opcode != 3'h1);
end
end
endcase
end
//--------------------------------------------------------------------------------
always @(posedge halt) //STOP when HALT instruction decoded
begin
#500
$display("\n***********************************************************");
$display("* A HALT INSTRUCTION WAS PROCESSED !!! *");
$display("***********************************************************\n");
end
always #(`PERIOD/2) clock=~clock;
always @(tb_risc_cpu.my_alu.opcode)
//get an ASCII mnemonic for each opcode
case(tb_risc_cpu.my_alu.opcode)
3'b000 : mnemonic ="HLT";
3'h1 : mnemonic = "SKZ";
3'h2 : mnemonic = "ADD";
3'h3 : mnemonic = "AND";
3'h4 : mnemonic = "XOR";
3'h5 : mnemonic = "LDA";
3'h6 : mnemonic = "STO";
3'h7 : mnemonic = "JMP";
default : mnemonic = "???";
endcase
endmodule
将下面四个文件的内容放入文档中,存入仿真目录下,作为rom和ram的存入内容:
1. //------------------------------- test1.pro开始 ---------------------------------------------------------------------
2. @00 //address statement
3. 111_00000
4. 0011_1100
5. 000_00000
6. 0000_0000
7. 000_00000
8. 0000_0000
9. 101_11000
10. 0000_0000
11. 001_00000 // 08 SKZ
12. 0000_0000
13. 000_00000 // 0a HLT //SKZ or LDA did not work
14. 0000_0000
15. 101_11000 // 0c LDA DATA_2
16. 0000_0001
17. 001_00000 // 0e SKZ
18. 0000_0000
19. 111_00000 // 10 JMP SKZ_OK
20. 0001_0100
21. 000_00000 // 12 HLT //SKZ or LDA did not work
22. 0000_0000
23. 110_11000 // 14 SKZ_OK: STO TEMP //store non-zero value in TEMP
24. 0000_0010
25. 101_11000 // 16 LDA DATA_1
26. 0000_0000
27. 110_11000 // 18 STO TEMP //store zero value in TEMP
28. 0000_0010
29. 101_11000 // 1a LDA TEMP
30. 0000_0010
31. 001_00000 // 1c SKZ //check to see if STO worked
32. 0000_0000
33. 000_00000 // 1e HLT //STO did not work
34. 0000_0000
35. 100_11000 // 20 XOR DATA_2
36. 0000_0001
37. 001_00000 // 22 SKZ //check to see if XOR worked
38. 0000_0000
39. 111_00000 // 24 JMP XOR_OK
40. 0010_1000
41. 000_00000 // 26 HLT //XOR did not work at all
42. 0000_0000
43. 100_11000 // 28 XOR_OK: XOR DATA_2
44. 0000_0001
45. 001_00000 // 2a SKZ
46. 0000_0000
47. 000_00000 // 2c HLT //XOR did not switch all bits
48. 0000_0000
49. 000_00000 // 2e END: HLT //CONGRATULATIONS - TEST1 PASSED!
50. 0000_0000
51. 111_00000 // 30 JMP BEGIN //run test again
52. 0000_0000
53.
54. @3c
55. 111_00000 // 3c TST_JMP: JMP JMP_OK
56. 0000_0110
57. 000_00000 // 3e HLT //JMP is broken
58. //-----------------------------test1.pro的结束-------------------------------------------
1. -----------------test1.dat开始------------------------------------------------
2. @00 //address statement at RAM
3. 00000000 // 1800 DATA_1: //constant 00(hex)
4. 11111111 // 1801 DATA_2: //constant FF(hex)
5. 10101010 // 1802 TEMP: //variable - starts with AA(hex)
6. //------------------------------test1.dat的结束
1. ------------------test2.pro开始-----------------------
2. @00
3. 101_11000 // 00 BEGIN: LDA DATA_2
4. 0000_0001
5. 011_11000 // 02 AND DATA_3
6. 0000_0010
7. 100_11000 // 04 XOR DATA_2
8. 0000_0001
9. 001_00000 // 06 SKZ
10. 0000_0000
11. 000_00000 // 08 HLT //AND doesn't work
12. 0000_0000
13. 010_11000 // 0a ADD DATA_1
14. 0000_0000
15. 001_00000 // 0c SKZ
16. 0000_0000
17. 111_00000 // 0e JMP ADD_OK
18. 0001_0010
19. 000_00000 // 10 HLT //ADD doesn't work
20. 0000_0000
21. 100_11000 // 12 ADD_OK: XOR DATA_3
22. 0000_0010
23. 010_11000 // 14 ADD DATA_1 //FF plus 1 makes -1
24. 0000_0000
25. 110_11000 // 16 STO TEMP
26. 0000_0011
27. 101_11000 // 18 LDA DATA_1
28. 0000_0000
29. 010_11000 // 1a ADD TEMP //-1 plus 1 should make zero
30. 0000_0011
31. 001_00000 // 1c SKZ
32. 0000_0000
33. 000_00000 // 1e HLT //ADD Doesn't work
34. 0000_0000
35. 000_00000 // 20 END: HLT //CONGRATULATIONS - TEST2 PASSED!
36. 0000_0000
37. 111_00000 // 22 JMP BEGIN //run test again
38. 0000_0000
39. //-----------------------------test2.pro结束-----------------------------------------------------------------------------------------
1. -----------
2. //------------------------------test2.dat开始------------------------------------------------
3. @00
4. 00000001 // 1800 DATA_1: //constant 1(hex)
5. 10101010 // 1801 DATA_2: //constant AA(hex)
6. 11111111 // 1802 DATA_3: //constant FF(hex)
7. 00000000 // 1803 TEMP:
8. //------------------------------test2.dat结束 .--------------------------------------------------
1. ------------------test3.pro开始--------------------------------------------------------------------
2. @00
3. 101_11000 // 00 LOOP: LDA FN2 //load value in FN2 into accum
4. 0000_0001
5. 110_11000 // 02 STO TEMP //store accumulator in TEMP
6. 0000_0010
7. 010_11000 // 04 ADD FN1 //add value in FN1 to accumulator
8. 0000_0000
9. 110_11000 // 06 STO FN2 //store result in FN2
10. 0000_0001
11. 101_11000 // 08 LDA TEMP //load TEMP into the accumulator
12. 0000_0010
13. 110_11000 // 0a STO FN1 //store accumulator in FN1
14. 0000_0000
15. 100_11000 // 0c XOR LIMIT //compare accumulator to LIMIT
16. 0000_0011
17. 001_00000 // 0e SKZ //if accum = 0, skip to DONE
18. 0000_0000
19. 111_00000 // 10 JMP LOOP //jump to address of LOOP
20. 0000_0000
21. 000_00000 // 12 DONE: HLT //end of program
22. 0000_0000
23. //-----------------------------test3.pro结束--------------------------------------------
1. -----------------test3.dat开始------------------------------------------------
2. @00
3. 00000001 // 1800 FN1: //data storage for 1st Fib. No.
4. 00000000 // 1801 FN2: //data storage for 2nd Fib. No.
5. 00000000 // 1802 TEMP: //temproray data storage
6. 10010000 // 1803 LIMIT: //max value to calculate 144(dec)
7. //-----------------------------test3.pro结束--------------------------------------------
在modelsim上进行仿真,可以看到如下信息:
# ******************************************************
# * THE FOLLOWING DEBUG TASK ARE AVAILABLE: *
# * "test1; " to load the 1st diagnostic progran. *
# * "test2; " to load the 2nd diagnostic program. *
# * "test3; " to load the Fibonacci program. *
# ******************************************************
#
# rom loaded successfully!
# ram loaded successfully!
#
# *** RUNNING CPUtest1 - The Basic CPU Diagnostic Program ***
#
# TIME PC INSTR ADDR DATA
# ---------- ---- ----- ----- -----
# 1200.0 ns 0000 JMP 003c zz
# 2000.0 ns 003c JMP 0006 zz
# 2800.0 ns 0006 LDA 1800 00
# 3600.0 ns 0008 SKZ 0000 zz
# 4400.0 ns 000c LDA 1801 ff
# 5200.0 ns 000e SKZ 0000 zz
# 6000.0 ns 0010 JMP 0014 zz
# 6800.0 ns 0014 STO 1802 ff
# 7600.0 ns 0016 LDA 1800 00
# 8400.0 ns 0018 STO 1802 00
# 9200.0 ns 001a LDA 1802 00
# 10000.0 ns 001c SKZ 0000 zz
# 10800.0 ns 0020 XOR 1801 ff
# 11600.0 ns 0022 SKZ 0000 zz
# 12400.0 ns 0024 JMP 0028 zz
# 13200.0 ns 0028 XOR 1801 ff
# 14000.0 ns 002a SKZ 0000 zz
# 14800.0 ns 002e HLT 0000 zz
#
# ***********************************************************
# * A HALT INSTRUCTION WAS PROCESSED !!! *
# ***********************************************************
#
# Break in Module tb at ../../../../risc_cpu_top.srcs/sim_1/new/tb.v line 34
run -continue
# rom loaded successfully!
# ram loaded successfully!
#
# *** RUNNING CPUtest2 - The Advanced CPU Diagnostic Program ***
#
# TIME PC INSTR ADDR DATA
# ---------- --- ----- ----- ----
# 16200.0 ns 0000 LDA 1801 aa
# 17000.0 ns 0002 AND 1802 ff
# 17800.0 ns 0004 XOR 1801 aa
# 18600.0 ns 0006 SKZ 0000 zz
# 19400.0 ns 000a ADD 1800 01
# 20200.0 ns 000c SKZ 0000 zz
# 21000.0 ns 000e JMP 0012 zz
# 21800.0 ns 0012 XOR 1802 ff
# 22600.0 ns 0014 ADD 1800 01
# 23400.0 ns 0016 STO 1803 ff
# 24200.0 ns 0018 LDA 1800 01
# 25000.0 ns 001a ADD 1803 ff
# 25800.0 ns 001c SKZ 0000 zz
# 26600.0 ns 0020 HLT 0000 zz
#
# ***********************************************************
# * A HALT INSTRUCTION WAS PROCESSED !!! *
# ***********************************************************
#
# Break in Module tb at ../../../../risc_cpu_top.srcs/sim_1/new/tb.v line 36
run -continue
# rom loaded successfully!
# ram loaded successfully!
#
# *** RUNNING CPUtest3 - An Executable Program ***
# *** This program should calculate the fibonacci ***
#
# TIME FIBONACCI NUMBER
# --------- -----------------
# 33250.0 ns 0
# 40450.0 ns 1
# 47650.0 ns 1
# 54850.0 ns 2
# 62050.0 ns 3
# 69250.0 ns 5
# 76450.0 ns 8
# 83650.0 ns 13
# 90850.0 ns 21
# 98050.0 ns 34
# 105250.0 ns 55
# 112450.0 ns 89
# 119650.0 ns 144
#
# ***********************************************************
# * A HALT INSTRUCTION WAS PROCESSED !!! *
# ***********************************************************
#
# Break in Module tb at ../../../../risc_cpu_top.srcs/sim_1/new/tb.v line 38
可以看出,cpu的指令执行与预期完全一致。