在VIVADO上实现的非常简易的RISC-V CPU设计(来自《Verilog数字系统设计》夏宇闻著)

在VIVADO上实现的非常简易的RISC-V CPU设计

  • 一、实验要求重述:
    • 1.实验目的
    • 2.实验要求:
  • 二、学习准备:
    • 1.什么cpu?
    • 2.cpu需要具有哪些部件?
    • 3.什么是RISC_CPU?
    • 4.RISC CPU的结构:
  • 三、各模块设计:
    • 1.时钟发生器:
    • 2.指令寄存器:
    • 3.累加器:
    • 4.算数运算器:
    • 5.数据控制器:
    • 6.地址多路器:
    • 7.程序计数器:
    • 8.状态控制器:
    • 9.地址译码器:
    • 10.rom和ram:
    • 11.cpu例化主模块:
  • 四、PC测试:

以下来自本人的数字系统设计课程的实验设计报告,开发板采用的是ego1,平台采用VIVADO,VIVADO-modelsim联合仿真。其中代码来自北航夏宇闻老师编著的《Verilog数字系统设计》,具体可以参考他的教材,本文的代码仅是对内容进行复现。

一、实验要求重述:

1.实验目的

学习RISC_CPU的基本结构和原理,了解Verilog HDL仿真和综合工具的潜力,学习verilog设计方法对软/硬件联合设计和验证的意义。

2.实验要求:

学习精简指令集CPU的架构,完成了简化RISC_CPU设计,包含时钟发生器,指令寄存器,累加器,算术逻辑运算单元,数据控制器,状态控制器,程序计数器和地址多路器等模块。并且针对每个模块单独编写tb文件,完成各个模块的功能仿真。为上述系统添加ROM、RAM等模块,并完成对整个系统的功能仿真。
完成之后,对上述简化RISC_CPU进行综合,了解其资源占用情况,自行设计验证方法,下载到ego1开发板进行fpga验证。

二、学习准备:

1.什么cpu?

Cpu就是中央处理单元的英文缩写,是计算机的核心部件,cpu处理信息数据可以分为两步:第一步将数据和程序输入到cpu当中,第二步从第一条指令的地址开始执行,得到结果后结束,开始下一条指令。
在这里插入图片描述

那么一个cpu需要具有的基本功能就是:1)取指令,2)分析指令,或说是指令译码,3)执行指令。

2.cpu需要具有哪些部件?

Cpu需要能够完成的功能有:能对指令进行译码并且执行相应的动作、可以进行算数和逻辑运算、能与存储器、外设交换数据、提供整个系统需要的控制。针对上述的功能,我们可以得知:
一个cpu需要具有的部件至少包括:
1)算数逻辑运算部件(ALU)
2)累加器
3)程序计数器
4)指令寄存器
5)指令译码器
6)时序和控制部件

3.什么是RISC_CPU?

在这里插入图片描述

Risc_cpu时精简指令集计算机的缩写,是一种八十年代出现的改进过的cpu架构。Risc_cpu和一般cpu不同的地方在于:1)risc_cpu的指令系统更加精简,指令集设计更加合理,提高了运算速度。2)risc_cpu的时序控制信号形成部件是通过硬布线逻辑实现的而不是通过微程序控制的方式,这就意味着其省去了读取微控制指令的时间,进一步提高了速度。

4.RISC CPU的结构:

Risc_cpu虽然是一个复杂的逻辑电路,但是其基本部件并不复杂,可以分为这样八个部件:
1)时钟发生器
2)指令寄存器
3)累加器
4)RISC CPU算术逻辑单元
5)数据控制器
6)状态控制器
7)程序计数器
8)地址多路器
下面的实验内容将按这个顺序展开,对每个模块进行设计和单独仿真。

三、各模块设计:

1.时钟发生器:

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,输出的五个时钟都与预期功能相同,该模块功能正常。

2.指令寄存器:

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

4)PC仿真结果:
在这里插入图片描述

可以看出,刚开始仿真时,输出opc为x,当输入rst为1时,虽然ena为1,输出仍然被置位0;在rst为0时,ena为0,也没有进行读取。而当rst是0,ena又为1时,开始读取。
第一个时钟上升沿,opc的高八位被写入,第二个时钟上升沿,opc低八位被写入。

3.累加器:

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程序进行仿真。

4.算数运算器:

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传输到输出
测试结果符合预期,该模块设计正确。

5.数据控制器:

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的设计和仿真。

6.地址多路器:

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的设计和仿真。

7.程序计数器:

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的设计和仿真。

8.状态控制器:

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

9.地址译码器:

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

10.rom和ram:

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

11.cpu例化主模块:

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测试:

要进行PC端的测试,这里没有使用vivado的内置仿真器,而是使用的是modelsim进行仿真。
Tb文件如下:文章来源地址https://uudwc.com/A/mp6j

`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的指令执行与预期完全一致。

原文地址:https://blog.csdn.net/Jefferymeng/article/details/124282429

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请联系站长进行投诉反馈,一经查实,立即删除!

h
上一篇 2023年06月16日 20:48
运用selenium库写淘宝抢购详解【3】(文末附带源码)
下一篇 2023年06月16日 20:48