自制CPU第二篇:Verilog HDL

Verilog HDL

要制作CPU,我没有选择使用分立元件构造,我选择使用了使用FPGA+代码的方式来实现。所以要学习Verilog HDL语言。这是一种HDL(硬件描述语言),可以进行抽象度很高的RTL电路设计。

设计电路的步骤

在这里插入图片描述

电路描述

Verilog HDL使用模块来设计一个功能单位的逻辑,模块是Verilog HDL中最基本的构成单位。

模块的声明

模块声明语法:

module<module name>{
		<输入信号定义>
    	<输入信号定义>
    	...
};
		<电路描述>
endmodule

比如使用模块来描述一个32位的加法器

module adder{
		input wire [31:0] in_0, //输入0
        input wire [31:0] in_1, //输入1
        output wire [31:0] out  //输出
};
    	assign out = in_0 + in_1;	//将in_0和in_1相加结果代入out中
endmodule

模块的实例化

模块的实例化语法:

<模块名> <实例名>(
    .<相连的端口名>(相连的信号名),
    .<相连的端口名>(相连的信号名),
    ...
);

adder adder01(
	.in_0	(adder01_in_0), //adder01_in_0信号连接到in_0端口
    .in_1	(adder01_in_1), //adder01_in_1信号连接到in_1端口
    .out	(adder01_out)
);

逻辑值和常数表达

逻辑值表达:
在这里插入图片描述
常数表达:
在这里插入图片描述

变量的声明与数据类型

变量声明

在这里插入图片描述

数据类型和变量名是必要项目,其他项可以省略。符号和位宽如果省略则根据数据类型设置为默认值,元素数省略默认声明元素数为1的变量。

数据类型

数据类型有寄存器型和网络型两种。寄存器类型是可以保存上次写入数据的数据类型,根据程序的不同可以生成锁存器、触发器等存储元件,也可能生成组合电路。

寄存器型变量

在这里插入图片描述
寄存器型变量会在always和initial语句中实现过程赋值。过程赋值分为阻塞式和非阻塞式赋值两种。

阻塞式赋值:按照代码顺序进行赋值的方式。在先赋值的代码赋值完成之前阻塞后续代码的赋值,因此得名阻塞式赋值。使用=

非阻塞赋值:所有代码不会相互阻塞,同时进行赋值。使用<=

在一个过程块中,两种方式只能使用其中一种
在这里插入图片描述

网络型变量

网络型是用来描述模块和寄存器间连接的数据类型。网络型只描述信号的传递不持有数据。

在这里插入图片描述
网络型变量可以在assign语句(分配语句)或声明语句中实现连续赋值,连续赋值就是进行连续的赋值。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

运算符

运算符和运算符优先级

在这里插入图片描述
在这里插入图片描述

运算符特殊使用方法

在这里插入图片描述

条件分支语句if和case

循环语句for和while

always过程块

always过程块是为了描述过程块而存在的语句。语法如下:
在这里插入图片描述
如果指定always语句中的事件表达式时,所指定的事件触发时执行其中的语句序列。事件可以是特定信号的变化、信号的上升沿、下降沿等。

如果always语句使用常数,则会在每次经过该常数时,执行一次always中的语句序列。

这个功能主要是在仿真时使用。always过程中可以使用寄存器变量赋值、if、case、for、while等语句。

always语句描述组合电路

使用always语句描述组合电路时,事件表达式描述方式如下:
在这里插入图片描述

组合电路描述中锁存器的推定和Don’t care

使用always语句描述组合电路时,如果信号未被赋值,有可能会引入锁存器。寄存器推定发生的原因是不完整的case语句或者没有else的if语句。为了规避这个问题,一定要将case语句或者if…else…写全。

也存在这种情况:确定不存在case和if语句设定之外的条件,或者设定条件之外随便输出什么都可以。这种情况称为don’t care.方法就是在default中为输出赋予不定值。

在这里插入图片描述
在这里插入图片描述

always语句描述时序电路

always描述时序电路时,事件表达式描述方法如图:

在这里插入图片描述

时序电路含有触发器等存储元件,基本上都是按照时钟同步执行。因此事件表达式中要指定时钟的信号边沿和时钟信号名。

时钟信号边沿:

  1. 确定在时钟信号上升或下降时触发电路动作。上升记为posedge,下降记为negedge,然后描述信号名。
  2. 事件表达式还可以使用or列举多个条件。
  3. 为存储元件设置异步复位(reset)信号时,除了时钟信号还要写上复位信号的边沿和信号名

预处理

预处理是在代码编译前对其进行预先处理的程序。Verilog HDL中的预处理可以实现宏定义和条件编译。预处理使用编译指示符可对编译器进行各种控制。

编译指示符以后引号(`)开头。

  1. `include语句:插入引用文件
  2. `define语句:进行宏定义(Verilog HDL中为了区分宏与变量名,宏的名字前面也加后引号(`),代码中使用宏,要像`BYTE_DATA_W一样描述。)
  3. `ifdef & `ifndef:实现条件编译。
    1. `ifdef:在指定的宏存在的条件下,执行`ifdef到`endif之间的代码
    2. `ifndef:在指定的宏不存在的条件下,执行`ifndef & `endif之间的代码
    3. 两者都可以使用`else指定不满足条件时执行的动作

在这里插入图片描述

Verilog HDL程序实例

设计目标

MISP CPU(32个32位的寄存器堆)

Q:什么是寄存器堆?

A:寄存器堆顾名思义,就是多个寄存器组合成的一个集合

为了方便访问寄存器堆中的寄存器,对他们进行统一编码,称为寄存器号或者寄存器地址。

该寄存器堆有:

  1. 2个读端口
  2. 1个写端口
  3. 异步读取,同步写入

在这里插入图片描述

程序代码

/*regfile.h*/
`ifndef __REGFILE_HEADER__
	`define __REGFILE_HEADER__
	
	/*信号电平*/
	`define HIGH		1'b1	//高电平
	`define LOW			1'b0	//低电平
	
	/*逻辑值*/
	`define ENABLE_		1'b0	//有效
	`define DISABLE_	1'b1	//无效
	
	/*数据*/
	`define DATA_W		32		//数据宽度
	`define DataBus		31:0	//数据总线
	`define DATA_D		32		//数据深度
	
	/*地址*/
	`define ADDR_W		5		//地址宽度
	`define AddrBus		4:0		//地址总线
`endif
/*regfile.v*/
/*引用头文件*/
`include "regfile.h"

/*模块声明*/
module regfile{
	/*时钟和复位*/
	input wire				clk,	//时钟
	input wire				reset_, //异步复位(低电平有效)
	/*访问接口*/
	input  wire [`AddrBus]	addr,	//地址总线输入 5b
	input  wire [`DataBus]	d_in,	//数据总线输入 32b
	input  wire 			we_,	//写入有效(低电平有效)
	output wire [`DataBus]	d_out	//数据总线输出 5b
};

	/*内部信号声明*/
	reg [`DataBus]			ff [`DATA_D-1:0];	//寄存器序列 32
	integer i;									//迭代器
	
	/*读取访问*/
	assign d_out = ff[addr];
	
	/*写入访问*/
	always @(posedge clk or negedge reset_) begin
		if (reset_ == `ENABLE_) begin			//异步复位
			for(i=0;i<`DATA_W;i=i+1) begin
				ff[i] <= #1 {`DATA_W{1'b0}};	//#:延迟(ns),延迟1ns将ff清零
			end
		end else begin
			if(we_ == `ENABLE_) begin			//同步写入
				ff[addr] <= #1 d_in
			end
		end
	end
endmodule

电路仿真

使用Verilog HDL不仅可以设计电路,还可以对所设计的电路进行仿真。仿真可以实现逻辑验证,从而测试设计好的电路能否正常工作。记述仿程序的文件称为Testbench。

Testbench的构造

Testbench是对制作的电路进行仿真、测试的模块。通常,输入端口为了将值代入,使用寄存器型变量,而输出信号接网络型变量对输出值进行观测。Testbench中,使用initial语句生成测试用例,然后观测模块的输出。

`timescale 1ns/1ps		//设定timescale

/*定义Testbench模块。无输入输出端口。
因为是一个抽象的测试模型,所以没有实际的输入输出。*/
module test_bench;

	reg adder01_in_0;	//(simple)输入0
	reg adder01_in_1;	//(simple)输入1
	wire adder01_out;	//(simple)输出
	
	adder adder01(		//实例化模块
		.in_0 (adder01_in_0),
		.in_1 (adder01_in_1),
		.out (adder01_out)
	);
	
	initial begin
		/*记述测试用例*/
	end
endmodule
timescale

`timescale用来设定仿真执行时间单位,使用数字和单位指定单位时间和时间精度。

单位时间:指定仿真的一个单位时间相当于多少秒

时间精度:表示仿真处理的时间精度,根据时间精度取数值的近似值(没有必要取过小的时间精度,这会延长仿真时间)

单位时间和时间精度的关系必须满足:单位时间>=时间精度

`timescale <单位时间>/<时间精度>
initial语句生成测试用例

initial语句是在仿真开始时只执行一次的语句,或是理解为仿真初始化设置。initial语句和延迟描述组合,可以用来生成测试用例。

【格式】
initial begin
    ... //过程的描述
end

【例】
initial begin
    #0 begin //时刻0 执行
        ...
    end
	
    #10 begin //时刻10 执行
		...
    end
    
    #10 begin //时刻20 执行
        ...
    end
end
延迟语句

# 字符用来记述延迟语句。延迟语句中的指定的数值意味着`timescale中设定单位时间的个数。

【格式】
# <常数表达式>

【例】
always @(*) begin
    a = #10 b; //b在10个单位时间延迟后赋值给a
end

initial begin   //initial 语句只在时刻0时执行一次
    #0  c = 1'b1 //c在时刻0时为1
    #10 c = 1'b0 //c在时刻10时为0
    #10 c = 1'b1 //c在时刻20时为1
    #10 c = 1'b0 //c在时刻20时为0
end

延迟语句只用在仿真程序中,用来在特定时间延迟后施加信号并生成测试用例。不会对逻辑综合的结果产生影响。

时钟的生成

如果被测模块要用到时钟,需要在Testbench中生成时钟信号。

always #10 begin
    clk <= ~clk //每经过10个单位时间clk的值翻转一次
end

initial begin
    #0 begin
        clk <= 1'b0 //clk在时刻0时被初始化
    end
    ...
end
系统任务

通过使用Verilog HDL预置的系统任务,可以达到控制仿真、输出字符串的目的。

  1. $display(“含有格式的字符串”,…)
  2. $write(“含有格式的字符串”,…)
  3. $time
  4. $finish
载入存储镜像

仿真时,有时候需要向存储器等读入预先准备好的数据。可以使用$readmemh系统任务从文件中读入数据并设置存储器。

在这里插入图片描述

波形的输出

仿真时的信号变化可以输出到波形文件中。波形文件有很多种,我们使用VCD格式波形文件的输出方法。

VCD文件的输出使用$dumpfile和$dumpvars两个系统任务来实现。

【格式】
$dumpfile(<文件名>)
$dumpvars(<开始时刻>,<输出波形的模块名或信号名>)

【例】
initial begin
    $dumpfile("test.vcd"); 	//将波形输出到test.vcd文件
    $dumpvars(0,test);		//从时刻0开始输出模块test的波形
end

在initial中调用$dumpfile和$dumpvars可以实现波形文件的输出

Testbench实例
`timescale 1ns/1ps

`include "regfile.h"

module regfile_test;
	/*定义内部信号*/
	reg 	clk;
	reg 	reset_;
	reg		[`AddrBus]	addr;
	reg		[`DataBus] 	d_in;
	reg 				we_
	wire	[`DataBus]	d_out;
	integer 			i;
	parameter			STEP = 100.0000; //100ns
	
	/*生成频率为10MHz的时钟*/
	always #(STEP/2) begin
		clk <= ~clk;
	end
	
	/*实例化测试模块*/
	regfile regfile(
		.clk 	(clk),
		.reset_ (reset_),
		.addr 	(addr),
		.d_in 	(d_in),
		.we_ 	(we_),
		.d_out 	(d_out)
	);
	
	/*测试用例*/
	initial begin
		/*信号线初始化*/
		#0 begin
			clk 	<= `HIGH;
			reset_ 	<= `ENABLE_;
			addr 	<= {`ADDR_W{1'b0}};
			d_in 	<= {`DATA_W{1'b0}};
			we_		<= `DISABLE_;
		end
		
		#(STEP*3/4)
		
		/*解除复位信号*/
		#STEP begin
			reset_ <= `DISABLE_;
		end
		
		/*对寄存器堆的读写进行验证*/
		#STEP begin
			for(i=0;i<`DATA_D;i=i+1) begin
				//地址为i的寄存器中写入i
				#STEP begin
					addr <= i;
					d_in <= i;
					we_	 <= `ENABLE_;
				end
				//地址为i的寄存器中读出i
				#STEP begin
					addr <= {`ADDR_W{1'b0}};
					d_in <= {`DATA_W{1'b0}};
					we_	 <= `DISABLE_;
					//使用if语句检测结果
					if(d_out == i) begin
						$display($time," ff[%d] Read/Write Check OK!",i);
					end else begin
						$display($time," ff[%d] Read/Write Check NG!",i);
					end
				end
			end
		end
		
		#STEP begin
			$finish;
		end
	end
	
	/*输出波形*/
	initial begin
		$dumpfile("regfile.vcd");
		$dumpvars(0,regfile);
	end
endmodule
使用Icarus Verilog进行仿真

使用Icarus Verilog进行仿真,首先需要用iverilog命令对原编码进行编译。为iverilog命令的参数指定正确的选项和源代码文件,执行后就会输出编译后的文件(.out文件)。iverilog命令的选项如表所示:

在这里插入图片描述

编译之后,使用vvp命令来执行仿真。vvp的参数中需要指定iverilog命令所输出的文件。vvp命令执行后,就会按照Testbench中记述的测试序列进行仿真。如果Testbench有波形输出,就会输出波形文件。

iverilog -s regfile_test -o regfile_test.out regfile_test.v regfile.v

生成regfile_test.out文件

vvp regfile_test.out

vvp进行仿真,参数设置为iverilog的输出文件。画面中出现Testbench中的输出信息。Testbench执行后的波形文件输出到了regfile.vcd中。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值