当前位置:   article > 正文

Verilog HDL

verilog hdl


在这里插入图片描述

1. 基本知识

硬件描述语言(Hardware Description Language, HDL) 是一种能够用于描述数字电路的语言,可通过分层的模块表示复杂的数字系统,并可通过电子设计自动化(Electrionic Design Automation, EDA) 工具进行仿真,以得到具体的物理门级电路组合,即电路网表

之后再将上述网表,在专用集成电路(Application Specific Integrated Circuit, ASIC) 或者 现场可编程门阵列(Field Programmable Gate Array, FPGA) 上将网表转化为具体的电路布局结构。

1.1. 什么是Verilog HDL

Verilog HDL 就是HDL的一种,用于数字电子系统设计,进行数字逻辑系统的仿真验证、时序分析、逻辑综合,是目前应用最广泛的一种硬件描述语言

Verilog的特点在于:

● 与工艺无关性:在使用verilog功能逻辑设计时无须过多考虑门级及工艺实现的具体细节,通过EDA工具的帮助可自动实现具体的网表。其实就是写代码实现电路,所以大大提高了verilog的可重用性。

知识产权核(Intellectual Property, IP):将一些在数字电路中常用、功能比较复杂、已经预先设计好的可修改参数的模块,即封装,类似于C++中的各种函数。IP核有三种形式:软核(HDL形式的IP核,代码)、固核(在FPGA上实现的正确的电路结构编码文件)和 硬核(在ASIC上实现的正确的电路结构版图掩膜,以经过完全的布局布线的网表形式提供)

1.2. Verilog HDL的功能

verilog可实现行为描述和结构描述,其中行为描述包括系统级模型(描述设计模块的外部特性)、算法级模型(实现算法运行)和寄存器级模型(Register Transfer Level, RTL)。

而结构描述就包括门级和开关级的模型,可描述逻辑门之间的连接。

比较重要而具体的功能:

● 顺序执行 or 并行执行:与电路网表一致,在verilog中既可以实现HDL的并行执行,也可实现与C相似的顺序执行结构。

● 时间延迟:可明确控制的启动时间延迟

● 事件触发:通过命名与设计事件设计激活or停止行为。

2. 语法

2.1. 模块

verilog建立的verilog模型就是模块,通过模块实现各种逻辑功能,并可通过EDA工具将verilog代码模块转化为门电路的连接,该过程为综合

如下所示,一个名为OR的模块

module OR(input a, input b, output c);			//分号千万别忘了
	assign c = a || b;
endmodule
  • 1
  • 2
  • 3

综合之后则为

在这里插入图片描述
注意模块module的结构写法,以及使用endmodule结尾

需要说明的是模块module实际上是创建了一个类型,例如与们、或门、非门、加法器等等,这些模块可以被实例化,即一个具体的有名字、输入信号确定的对象。

2.1.1. 端口

指的是该模块的输入/输出端口,即IO口。

既可以标注输入、输出以及输入输出口,注意信号位宽的写法一般是“高位:低位”,具体可见代码:

module OR(
	input [2:0] a, 
	input [3:1] b, 												//不写明类型就默认为wire类型 
	output reg [2:0] c,		
	inout [5:0] d
	);		
														
assign d = a | b;
always@(*) begin
	if(a)
		c = 3'b1;
	else
		c = 3'b0;
end
	
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注意module输入必须为wire型,输出建议也用wire

例如上面的代码,输出c虽然是reg型但在后期综合的时候还是会把它综合成导线而不是触发器。

但always块赋值右侧又必须是reg型,所以推荐的写法如下

module block(
	input a,
	input b,
	output c
);
reg c_r;						//由于要通过always赋值故声明一个reg型变量
always@(*) begin
	if(a)
		c_r = 1'b1;
	else
		c_r = 1'b0;
end
assign c = c_r;					//用一个assign为wire型输出c赋值
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
模块实例化

实例化引用就是,在某个模块内部引用其他模块,并将该模块的IO与外部连起来。

引用方式如下:

module trist2(input a, input b, output c);
	//......
	block u_block1(a_i,b_i,c_o);						//严格按照block模块定义端口的顺序			

	block u_block2(.c(c_o),.a(a_i),.b(b_i));			//指明谁连谁,注意写法是  ".模块端口(连接变量)"

endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如上代码所示有两种实例化方式。

但是这里有一个问题:实例化时模块端口连接的变量,是wire型还是reg型呢?答案是输入可wire可reg,输出必须是wire

有个小技巧,既然是例化连接,其实还是导线的连接,所以将例化时的变量连接看成assign语句。
那么输入就相当于 assign a = a_i;,那显然模块定义的时候input a必须是wire型,而a_i任意
输出相当于assign c_o = c;,那显然例化时连接的变量c_o必须是wire型,而模块定义时的output c任意(但从后期综合考虑建议为wire型)

2.1.2. 逻辑功能

指模块内部产生逻辑功能的语句,所有的逻辑一般均必须通过assign、always、initial以及实例化引用实现功能。

需要说明的是模块内的所有逻辑功能均是并行实现的,即同时执行。所以执行顺序与代码的写作顺序无关。

module exercise(input in1, input in2,...,output out1, output out2,... );

//块1
initial 
	...
//块2
assign ...;
//块3
assign...;
//块4
always@(...)
...
endmodule				//块1234同时执行
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

但是块内部则可能出现顺序执行的逻辑,这个后面会涉及到。

assign声明

直接assign后面接方程式即可,表示持续地满足该方程式的关系。

assign a = b & c;	//永远满足a为b与c的按位与的结果

assign #10 clk = ~clk;		//延迟10个单位时间之后,clk按位翻转,且永远如此变换
  • 1
  • 2
  • 3

注意上例子中的#10表示延迟10个单位时间再去执行。

always块

不断运行的语句,同时可以为always添加时序控制语句来判断语句执行的条件。会永远持续地判断条件,一旦条件满足,则执行后面的语句。

always块@里面是敏感列表,表示always块触发的条件:时序逻辑包括posedge表示上升沿触发、negedge表示下降沿触发

组合逻辑则使用(*)电平触发,即变量值发生变化时触发

同时可加入begin…end表示触发要执行的语句块,注意该语句块为顺序执行

具体的书写形式如下

always@(posedge clk or negedge rstn)			//时序逻辑
begin
	if(!rstn)
		q <= 1'b0;
	else if(flag)								//若某个posedge clk处flag为低,则q会保持原值。即 q <= q;
		q <= 1'b1;
end

always@(*) begin								//组合逻辑
	if(flag)
		q = 1'b0;
	else if(width)								//若flag为低、width也为低,则q会锁存,被综合成锁存器Latch
		q = 1'b1;
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

一定要注意verilog中的各always块均为并行执行,因此禁止多个always块对同一个变量赋值,可合并为一个always块解决此问题,例如:

always@(posedge clk)
begin
	if(A)
		q <= 1'b0;
	......
end

always@(posedge clk)
begin
	if(B)
		q <= 1'b1;
	......
end
//合并为一个always块/
always@(posedge clk)
begin
	if(A)
		q <= 1'b0;
	else if(B)
		q <= 1'b1;
	......
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

需要说明的是,always块中使用组合逻辑时,if语句的else、case语句的default等等要写全,否则会被综合成锁存器Latch。

而时序逻辑中写不全,触发器就不会成为Latch。

initial块

可在仿真开始时对各变量进行初始化,注意initial块只会执行一次,但initial内部的begin…end是顺序执行。

但是多个initial块则是并行运行的!!!见verilog中的initial语句

initial						//注意结尾没有分号
	begin
		inputs = 3'b000;
		#10 inputs = 3'b000;
		#20 inputs = 3'b001;
		#30 inputs = 3'b010;
	end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2.2. 模块的测试

在建立模块之后,可再建立一个专门用于测试的模块。在测试模块中引用之前建立的模块,并给定相应的输入,以观察所建立的模块的输出信号,以判断设计的结构合理与否。

所以测试模块不需要输入和输出,例子如下:

'include "mux.v"
module mux_test
	reg ain,bin,select,out;
	reg clock;

	//参数初始化	

	initial
		begin
			ain = 0;
			bin = 0;
			select = 0;
			clock = 0;
		end

	//设定时序
	
	assign #100 clock = ~clock;
	
	always@(posedge clock)
		begin
			#1 ain = {$random}%2;
			#3 bin = {$random}%5;
		end
	assign #1000 select = !select;
	
	//实例化mux
	
	mux m(.out(out),.a(ain),.b(bin),.sl(select));


endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

之后可以用相关仿真软件对信号的波形作仿真,以观察信号是否符合要求。

2.3. 常量

即程序运行中不能改变的量。

2.3.1. 数字常量

由于verilog为数字系统描述语言,对于数字系统来说最基本的是端口电平的高低,即基本为数字量,所以常用的是整数。

常见形式为

仅数字

默认为32bit的十进制的数

bit位宽 + ’ + 进制 + 数字

表示一定位宽下某个进制的值,例如3’b101、8’ha2等等。

注意位宽为二进制的bit,例如 8’ha2 表示 8bit的 十六进制a2。

其中进制表示法如下:

字母进制
b 或 B二 进制
d 或 D十 进制
h 或 H十六 进制

别忘记二进制 和 十六进制的相互转换
1位十六进制 = 4位二进制,例如 4’ha = 4’b1010, 4’hf = 4’b1111, 16’habc = 12’b1011_1100_1101,12’h1010 = 16’b0001_0000_0001_0000

所以位数÷4就是十六进制的总位数,例如16‘ha2的十六进制位数为16/4 = 4,因此完整写法为16’h00a2。32’h10的十六进制位数为32/4 = 8,所以完整写法为32’h00000010

对于数字而言,除了完整地按照位宽正常地写出各位的值以外,还有其他写法和注意事项:

● 值相同即可,bit数可以不相同,例如4’b0010 == 2’b10 == 2,这一点与数组相区别。

● 可通过下划线分割位数 以提高可读性,例如12’b1011_1100_1101

x 可用来表示不定值,z或?可用来定义高阻值,且均可以作为十六、八、二进制的一位,例如 4’hx == 4’bxxxx,4’b1x0z等等。

● bit数与数字的二进制位数不相等的,前补0,只有最左边的x或z才具有扩展性

这一点容易弄混,例如8’ha == 8’b0000_1010, 4’b1 == 4’b0001,16’h4x == 16’h004x, 8’bzx == 8’bzzzzzzzx, 8’hx == 8’hxx

● 十六进制数字部分不区分大小写, 不定值x 和 高阻值z 也不区分大小写

2.3.2. 参数常量 parameter 与 localparam

可以为某个参数定义某个常量数值,方便程序中使用,例如

parameter width = 1,
		  polarity = 2;
		  length = width * 10;
		  
localparam phy = 1.396;
localparam pi = 3.14159;

		begin
			a = 2 * pi * r;		//等价于2*3.14159*r
			...
		end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

无论是localparam还是parameter,一定要注意定义的是常量!是常量!值不可以更改的!上面的代码中出现pi = pi +1;是错误!想要更改的话要把类型定义成integer或是large等等的变量!

而parameter和localparam的区别是,parameter参数可通过其他模块重新配置,而localparam为私有参数常量不可被外部配置。

那么如何外部模块如何对该模块的parameter进行配置呢?一般在例化时指定

module block#(									//这里不可出现localparam!
		  parameter width = 1,
		  parameter polarity = 2;
		  parameter length = width * 10;					
		  )(
		  input a,
		  input b,
		  output c
		 );
//...
endmodule

module top;
	//...
	block u_block#(
			.width		(`WIDTH),
			.polarity	(`POLARITY),
			.length		(`LENGTH)
			)(
			.a	(a_i),
			.b	(b_i),
			.c	(c_o)
			);
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

parameter其实很像即将要讲到的宏定义’define,二者均可以进行参量代换,但是区别在于:parameter只能表示数字且可以在其他模块进行修改
'define可以表示任意量甚至是表达式的一部分,例如 'define DelayNand nand #5就可以用于表示DelayNand n1(a,b,c),等价于nand #5 n1(a,b,c);,但是’define一旦定义不可修改。
注意define使用结尾不加分号

2.4. 变量

即在程序运行过程中其值可以改变的量。

注意:禁止在initial, always块中进行常量、变量的定义

2.4.1. wire型

即导线,属于线网net型,用于描述模块内部各结构实体(例如门)之间的连接。因此wire型均为无符号数

常用assign 描述的组合逻辑信号,模块中的输入输出自动定义为wire,默认为高阻值z

wire型可以指多个位的变量,如下所示定义方式。

wire a;
wire [31:0] b;
wire [32:1] c,d;		//位不好数的时候可以将最低位下标设为1
  • 1
  • 2
  • 3

既然是导线的含义,通过如下例子解释wire的含义:

module compose(input a, input b, output c)	//可以写成input wire a, input wire b, output wire c
	wire mid;
	assign mid = a && b,c = ~mid;
endmodule
  • 1
  • 2
  • 3
  • 4

模块结构如下图所示,确实是一根导线。

在这里插入图片描述

2.4.2. reg型

来自于register寄存器,可用来表示数据存储器、寄存器、触发器等等。

常用于always模块中代表触发器,并且always模块中 每一个被赋值的信号 都必须是reg型,并且reg型默认为不定值x

见如下定义方式

reg [31:0] r1,r2,r3;	//定义了3个寄存器r1,r2,r3,每个寄存器有32bit
reg r4 [9:0];			//定义了10个寄存器,r4[0]...r4[9],每个寄存器有1bit
reg [31:0] r5 [9:0];	//定义了10个寄存器,r5[0]...r5[9],每个寄存器有32bit

always@(posedge clk)
	begin
		r5[0][0] <= 1;
		r5[0][1] < =0;
	end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

需要说明的是reg型可以被赋予负值,但是reg型数据实际上表示的是无符号数

无符号数可按照如下数字循环取到,以3bit为例:

在这里插入图片描述
对于有符号数而言,定义方法为:

reg signed [7:0] a;
  • 1
补码系统

有符号数中,

负数的表示方法为 符号位(1为负)+补码,例如-3在verilog中就表示为4’b1101,-5就表示为4’b1011。

正数的表示方法为 符号位(0为正)+原码

计算时直接相加即可,例如

reg signed [3:0] a = 4'b1011;	//-5
reg signed [3:0] b = 4'b0100;	//4
reg signed [3:0] sum;

assign sum = a + b;				// 4-5 = 4 + (-5) = 4'b1111,为-1
  • 1
  • 2
  • 3
  • 4
  • 5
reg signed [3:0] a = 4'b1101;	//-3
reg signed [3:0] b = 4'b0101;	//5
reg signed [3:0] sum;

assign sum = a + b;				// 5-3 = 5 + (-3) = 5'b10010,截掉最高位为1
  • 1
  • 2
  • 3
  • 4
  • 5

在SystemVerilog中为了避免纠结变量类型是wire还是reg,引入了新的变量类型logic,系统会自动推断是wire还是reg。
但是logic类型变量并不允许多于一次的持续赋值or输出端口给同一变量赋值
详情可参考systemverilog中logic变量的使用

由于经常出现模块类型定义和子模块变量传递类型的矛盾,此处对reg和wire的使用做如下总结:
always中 被赋值为reg,assign中 被赋值为wire
顶层模块:输入为wire,输出为 reg/wire
子模块实例:输出用wire连接,如下图所示:
在这里插入图片描述

2.4.3. integer型

其本质还是reg型,只不过是一种有符号的32位寄存器类型,其实直接看成整数变量即可。

例如常见的循环for(integer i = 0; i <= 10 ;i = i + 1)

注意verilog中变量没有小数的概念,可以有小数的运算,但是最终输入输出的值永远是整数。毕竟是数字电路只有高电平1和低电平0二者之分。

2.5. 运算符

有很多运算符,可分成单目、双目和三目三种。

2.5.1. 算术运算符(+,-,*,/,%,**)

比较简单,注意如果有个操作数存在不确定值x,则算术运算结果也为x

%表示求余运算符,但是余数符号只与第一个操作数一致

例如 -14%3 = -2,但 14%-3 = 2

实际设计中+、-可随意使用,但*、/、%、**不建议使用,因为无法判断最后会综合成什么电路。
那么怎么作这些计算呢?使用各种封装的IP模块,用多周期的流水设计实现运算结果,综合可控

2.5.2. 位运算符(~,&,|,^, ^ ~)

表示一个操作数或两个操作数相互按位一位一位地进行关系运算。

位数不同则自动高位补0

下面举例说明

wire [3:0] a;
wire [2:0] b;

assign a = 4'b1010;
assign b = 3'b011;

wire [3:0] c;

begin
	c = ~a;				// 按位取反,c为 4'b0101
	c = a & b;			// 按位与,c为 4'b0010
	c = a | b;			// 按位或,c为 4'b1011
	c = a ^ b;			// 按位异或,c为 4'b1001
	c = a ^~ b;			// 按位同或,c为 4'b0110
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

位运算符同样可以一个操作数的不同位一位一位的相互运算,即

wire d;
begin
	d = & a;			// 等价于a[0]&a[1]&a[2]&a[3],d为 1'b0
	d = | a;			// 等价于a[0]|a[1]|a[2]|a[3],d为 1'b1
end
  • 1
  • 2
  • 3
  • 4
  • 5

2.5.3. 逻辑运算符(!,&&,||)

注意逻辑运算符永远只能是单个bit进行运算,不能如~|&这种对应为相互运算。

wire a,b;
assign a = 1'b1;
assign b = 1'b0;

wire [3:0] c;
begin
	c = !(a && b);		//c为 4'b0001
	c = !(a || b);		//c为 4'b0000
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

但注意取反运算符~和!的区别,二者都可以对一个bit进行取反操作,但是多个bit只能用 ~。

常用!来进行低电平判断:

always@(negedge reset)
	begin
		if(!reset)
			...
	end
  • 1
  • 2
  • 3
  • 4
  • 5

2.5.4. 关系运算符(<,>,<=,>=, = = == ==,!=, = = = === ===

如果关系是模糊的,则返回不定值x

注意的变量值为x或z时a===b一律返回false,只有a与b均为0或均为1时才会返回true。例如a==b在a为x、b为1仍然为true,而a===b就为false

2.5.5. 移位运算符(<<,>>)

左移位后补0,右移位删除对应位。

注意变量位数是确定的,左移位可能会消掉某些高位,见下例

reg [4:0] a;
reg [6:0] b;
initial 
	begin
		a = 10;
		#10 a = (a<<2);		//a原为5'b01010,移位之后为5'b01000
		
		b = 10;
		#10 b = (b<<2);		//b原为7'b0001010,移位之后为7'b0101000
	end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2.5.6. 位拼接运算符({})

将某些信号的某些bit拼接在一起形成新的值,

注意一些写法,例如{3{w}}={w,w,w},还可以加入常量,例如

wire [3:0] a,b;

assign a = 4'b1011;
assign b = 4'b0101;

wire [16:1] c;

assign c = {a,b[2:0],a[1],a[2],1'b1,1'b0,2{a[1],b[2]}};		
//c为 16'b1011_101_1_0_1_0_11_11

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

2.6. 常用语句

很多语句都与C语言非常类似。

2.6.1. 块语句

将多条语句组合在一起,非常类似于C中的大括号。

顺序块 begin…end

begin…end内部的语句是顺序执行的,并且可以为该顺序块命名,执行完最后一条语句跳出。

begin:give			//命名不是必须
	a = b;
	c = d;
end
  • 1
  • 2
  • 3
  • 4

别忘了可以使用#10进行延迟执行。

'timescale 1ns			//定义时间单位
module seq
	reg [2:0] r;
	initial r = 4'b000;
	begin
		#5 r = 4'b001;
		#10 r = 4'b010;
		#15 r = 4'b011;
		#20 r = 4'b100;
	end
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

变量r产生的波形如下,其中delay表示持续的时间
在这里插入图片描述
也就是说 顺序块中的延迟时间为上一条语句执行结束之后等待的时间。

并行块 fork…join

即并行块中的语句为并行执行的,即所有语句同时开始执行,这个在C中没有定义。

所以如果要产生与seq模块相同的波形,使用并行块写法如下:

'timescale 1ns			//定义时间单位
module para
	reg [2:0] r;
	initial r = 4'b000;
	fork
		#5 r = 4'b001;
		#15 r = 4'b010;
		#30 r = 4'b011;
		#50 r = 4'b100;
	join
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

由于是并行的,所以一定要检查并行块是否存在矛盾的语句,即竞争冒险现象,例如重复赋值等等。

同时块语句相互之间可以嵌套,例如

begin
	x = 1'b0;
	fork
		#5 y = 1'b1;
		#10 z = {x,y};
	join
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
生成块 generate…endgenerate

可用于动态生成代码

例如

genvar i;
generate if(`DATA1 > `DATA2)					//只有满足if条件才会生成下面的代码
	for(i=0;i<`WIDTH;i=i+1)						//以for循环的方式生成多行相同代码
		always@(posedge clk) begin
			//...
		end
else
	//...
endgenerate
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
禁用关键字 disable

可用disable使任意块语句停止运行,也可用于跳出循环,类似于break

但注意必须提前给块语句命名才能disable,例如要终止块语句block1则有disable block1;

例如跳出循环

begin:block1
	while(i<16)
		begin:block2
			if( flag[i] )
				begin
					$display("True bit at %d", i);
					disable block1;		//跳出while,类似break
				end
			else
				begin
					$display("Wrong bit at %d",i);
					i = i + 1;
					disable block2;		//跳出本次循环,类似continue
				end
		end
end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

上面的语句意思是如果变量flag的第i位为1就disable block1,即跳出循环,表达出C里面break的效果。

如果flag的第i为为0,就先移动到下一位然后disable block2,之后根据while语句的规则,继续循环判断下一位是否为0,因此此处的disable表达出continue的效果。

所以如果将disable block1替换成i = i + 1; disable block2;循环的意思就变成了检查flag中所有的bit是否为1,此处就不再是break的效果而是continue

2.6.2. 条件语句(if…else)

非常熟悉的条件语句,常见写法如下:

if(!rst)
	begin
		instruction = 0;
		index = 0;
	end
else
	begin
		instruction = sa;
		index = index + 1;
	end
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

但是有一些需要注意的地方

● if后面的条件表达式结果,0,x,z均为假,只有1为真

在同一个块内,else只与最近的if 相对应

见下面的代码,请问下面的那个else是与哪个if相对应呢?

if(index>0)
	for(i = 0;i<index;i++)
		if(memory[i]>0)
			memory[i] = 0;
else
	$display("error");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

答案是第二个if,这是由于第二个if并没有在顺序块内,所以else可以与之匹配,但如果是下面这种情况就是else与第一个if相匹配了

if(index>0)
	begin
		for(i = 0;i<index;i++)
			if(memory[i]>0)
				memory[i] = 0;
	end
else
	$display("error");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

多个if嵌套需要用 begin…end包括

例如下面的代码并不等价于if(a>b && c>d) sig<= 1'b1;

if(a>b)
	if(c>d)
		sig <= 1'b1;
  • 1
  • 2
  • 3

上面代码中即使c>d满足也会执行sig <= 1'b1;这是因为上面的代码本质是

if(a>b)					//a>b满足什么都不做
if(c>d)					//c>d满足则赋值
	sig <= 1'b1;
  • 1
  • 2
  • 3

如果想等价于if(a>b && c>d) sig<= 1'b1;就要加入begin…end块

if(a>b) begin
	if(c>d)						//等价于if(a>b && c>d)
		sig <= 1'b1;
end
  • 1
  • 2
  • 3
  • 4

2.6.3. 分支语句(case)

多分枝选择语句,常用于指令译码、状态机等。

通过计算case后面括号内部的表达式,找到与表达式相等的分支项,注意比较时位宽必须相等,x与z也必须严格在对应的bit上彼此相等,则执行该分支项后面的语句,执行完后跳出case结构

如果没有与表达式相等的分支项,则执行default后面的语句,且default只有一项。

例子如下,注意写法,冒号等写法:

case(rega)
	2'b00: res = 4'b1110;
	2'b01: res = 4'b1101;
	2'b10: res = 4'b1011;
	2'b11: res = 4'b0111;
	2'bx,
	2'bz: res = 4'bx;
	default: begin 
				res = 4'bx;
				$display("Input is wrong.");
			end
endcase
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

注意HDL中的case与C++中的switch…case有着很大的区别,首先是写法:

switch(grade)
   {    
   	 case 'A' :   cout << "很棒!" << endl; break;  
      case 'B' :   
      case 'C' : cout << "做得好" << endl; break; 
      case 'D' : cout << "您通过了" << endl; break;  
      case 'F' : cout << "最好再试一下" << endl; break;  
      default :  cout << "无效的成绩" << endl;  
   }  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

然后HDL中case执行完对应的语句之后直接跳出,而C++的switch则是执行完某个case之后会继续执行下一个case的内容,直到遇到break才会跳出switch。
HDL中的case与状态机的原理是对应的:状态机就是在某个状态下经过某个条件转换到下一个状态,所以case语句在找到分支之后就直接跳出,而不会再执行下一个分支。

casez 与 casex

这两种语句是为了处理比较过程中不必考虑的情况

casez不考虑高阻值z的比较过程,即 控制表达式或分支表达式 的某个bit出现z,该bit就不再参与比较,但x还是要进行严格比较的
例如:

casez(ir)
	4'bzzz1: out = a;
	4'bzz1z: out = b;
	4'bz1zz: out = c;
	default: out = d;
endcase
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

但存在一个问题:如果存在多个相符的分支,该如何判定执行哪个?例如上述代码中ir = 4'bzzzz

注意case不会出现这种情况,因为case要求对应的bit严格相等才行,x必须与x对应,z也必须与z对应

答案是最上面的分支语句被执行,若ir = 4'bzzzz虽然与4'bzzz14'bzz1z4'bz1zz都匹配但out = a,也就是说系统的匹配检查是从第一个语句开始依次检查的。结果可参考Verilog语法有关casez和casex的分析

而casex则表示z和x都被视为不必关心的情况。

2.6.4. 循环语句(while,for)

这两个语句与C中的循环语句相同,各子循环按顺序执行,但注意使用块语句begin…end。

while(temp)
	begin
		if(temp[0])
			count = count + 1;
	end

for(integer i = 0;i <= 10;i = i + 1)
	temp[i] = 0;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

再次复习一下for循环各表达式的执行顺序:

若定义:
for(表达式1;表达式2;表达式3) 循环语句;

成立
不成立
表达式1
表达式2
循环语句
表达式3
跳出循环

2.6.5. 赋值语句

即将某个值赋给某个量,包括两种方式:阻塞赋值和非阻塞赋值。

阻塞赋值 =
例如a = b;表示在执行该语句的时候直接计算b的值,并 立刻改变 a的值。

这个与C语言是相同的,并且下面再用到a时,使用的是新值。

但是需要注意的是verilog的并行执行的特点有时会产生竞争与冒险

always@(posedge clk)
	y1 = y2;

always@(posedge clk)
	y2 = 0;
  • 1
  • 2
  • 3
  • 4
  • 5

在上述代码中当clk上升沿的时候,y1 = y2;y2 = 0;应是同时执行的,而系统内部实际的执行顺序是不确定的,不同的顺序会导致不同的结果。

注意阻塞赋值包括计算右侧值 + 赋予左侧变量 两个步骤,执行这两个步骤时绝不允许任何其他其他语句的干扰,阻塞的含义就在于此。

非阻塞赋值 <=

非阻塞赋值的符号与小于等于的符号是相同的。例如a<=b;表示先计算b的值,直到块结束之后才改变a的值。

所以在块内的下面的语句用的a值是旧值。

阻塞赋值 = 与 非阻塞赋值 <=

关键在于赋值操作是何时实现的,从下面这个例子可以看出区别:

module BlockAndUnblock;

integer a,b,c;
reg clk;
initial
	begin
		clk = 0;
		a = 1;
		b = 2;
		c = 3;
	end

assign #10 clk = ~clk;

always@(posedge clk)
	begin
		b <= a;		//阻塞赋值就为 b = a;
		c <= b;		//阻塞赋值就为 c = b;
	end
	
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

如果是非阻塞赋值,在一个clk上升沿到来时,b的值为a的值,c的值为b的旧值,综合的电路如下:

在这里插入图片描述

但如果是阻塞赋值(注释),在上升沿来了之后,b和c均为a的值,综合的电路如下:

在这里插入图片描述
也就是说在always模块中,如果是阻塞赋值,效果就可以概括为时钟到来时,赋值结果直接完成,表达的是组合逻辑(该时刻的输出仅仅与该时刻的输入有关)

如果在always模块中采用非阻塞赋值,效果就是时钟到来时,每个赋值操作含有旧值(例如c<=b;这一句),因此表达的是时序逻辑(该时刻的输出与原来电路的状态有关)

此处这么理解,还是上面模块BlockAndUnblock的例子,将a看作输入c看作输出,其余部分看作电路,b就为电路内部的量。
当clk上升沿到来之后,采取阻塞赋值c就是a的值(组合逻辑),而采取非阻塞赋值c的结果不是a的值,而是另一个电路内部的原来的值b(时序逻辑)。

因此得到结论:建立时序逻辑电路模型时,用非阻塞赋值;建立组合逻辑电路模型时,用阻塞赋值;时序和组合逻辑电路模型时,用非阻塞赋值。

2.7. 函数

系统函数

一些常见的系统函数如下

在这里插入图片描述

以及最重要的 $clog2(),注意这是个向上取整函数

2.8. 任务

2.9. 编译预处理

3. 流水线设计技术(Pipeline)

如果一件事情需要好几个阶段一步一步地完成,各个步骤之间没有反馈,而且这件事情需要重复做很多次,就可以使用pipeline。

pipeline的核心思想是若前一个事件的某个阶段已经完成,则立即进行下一个事件的该阶段工作

例如有一家咖啡吧有客人ABCDE依次排队买套餐。
一般为客人供给食物的策略是:员工甲放个盘子,员工乙装上薯条,员工丙放上豌豆,老板最后配上一杯饮料,完成对客人A的服务,然后再服务下一位客人B服务,顺序依次。
但是这样的策略有个问题:员工甲给了客人盘子之后,就一直闲着直到该客人套餐购买完全结束,才为下一个客人服务。其他员工同样存在相同的问题
而pipeline的策略是:在甲为客人A放完盘子之后,为客人B放盘子,再为客人C放盘子,直到客人E。其他员工也是这样的策略。

3.1. 组合逻辑模块中 使用Pipeline

与核心思想类似,可将一个组合逻辑电路分为好几个级,这些级只有前馈没有反馈。

一般情况下是对于某个输入,该电路的所有级全部处理完输出结果之后,再更换新的输入再运行。

而使用pipeline的方法是:在每一级之后插入寄存器组暂存数据,所有寄存器同步触发

下面分析这样做的好处,如下图所示是否采用pipeline结构示意图:

在这里插入图片描述

在采用了pipeline结构时,考虑每个 组合逻辑级传输延迟 T p d T_{pd} Tpd寄存器传输延迟 T c o T_{co} Tco时钟周期 T c l k T_{clk} Tclk,时序图为:

在这里插入图片描述

可以看出,组合逻辑中并不是整体处理完输入a之后才处理输入b,而是每一级处理完a在下一个脉冲到来时直接处理b。

从图中可以看出,在输出 K ( J ( F ( a ) ) ) K(J(F(a))) K(J(F(a)))时消耗了很长时间,但是在此之后每来一个时钟就会输出一个值。

注意 T p d + T c o < T c l k T_{pd}+T_{co}<T_{clk} Tpd+Tco<Tclk,这样才能保证每个时钟周期都能够正常的流动结果,即保证建立时间

就此提出以下概念:

首次延迟: 从开始输入第一个值到该值输出 所消耗的时间。

吞吐延迟: 每个输出之间的时间间隔。

并给出以下表格(忽略触发器的建立时间):

项目不使用pipeline使用pipeline
首次延迟 3 T p d 3 T_{pd} 3Tpd最大为 3 T c l k + T c o + T p d 3 T_{clk}+T_{co}+T_{pd} 3Tclk+Tco+Tpd
吞吐延迟 3 T p d 3 T_{pd} 3Tpd T c l k T_{clk} Tclk

所以如果组合逻辑非常冗长,即 3 T p d > > T c l k 3 T_{pd}>>T_{clk} 3Tpd>>Tclk,则数据的输入时间间隔or输出时间间隔就很长,吞吐量就很小。

但如果采用了pipeline,数据的输入时间间隔or输出时间间隔被控制在 T c l k T_{clk} Tclk以内,即短时间内输入or输出多个数据,因此提高了吞吐量

4. 有限状态机

对于时序电路而言,触发器是必不可少的元件,有了触发器才能够实现按照时钟的时序控制。

实际上,每一拍(时钟有效沿)的控制都可看作是电路状态的转换,类似于离散时间状态方程:

x ( k + 1 ) = A x ( k ) + B u ( k ) x(k+1)=Ax(k)+Bu(k) x(k+1)=Ax(k)+Bu(k)

而如果无论怎么控制,数字电路的状态都是有限的,且均是基于时钟沿进行转换的,这就构成了有限状态机。

4.1. 有限状态机的Verilog描述

此处给出标准的状态机Verilog模块写法,状态转移图如下所示:

在这里插入图片描述

方框表示状态名称,箭头表示状态转移,箭头上的标注表示:转移条件/输出结果。

建议的Verilog语言如下:

module FSM(
	input clk,
	input reset,
	input A,

	output reg K1,
	output reg K2
);

reg [3:0] state;
状态码编写///
parameter Idle = 4'b0001,
		  Start = 4'b0010,
		  Stop = 4'b0100,
		  Clear = 4'b1000;

仅状态转移逻辑///
always@(posedge clk)
begin
	if(reset)
		state <= Idle;
	else
		case(state)
			Idle:if(A)
					state <= Start;
				else
					state <= Idle;
			Start:if(!A)
					state <= Stop;
				else
					state <= Start;
			Stop:if(A)
					state <= Clear;
				else
					state <= Stop;
			Clear:if(!A)
					state <= Idle;
				else
					state <= Clear;
			default:state <= 4'bxxxx;
		endcase
end
仅输出逻辑///
always@(state or reset or A)
	if(!reset)
		K1 = 0;
	else if(state == Clear && !A)
			K1 = 1;
		else
			K1 = 0;

always@(state or reset or A)
	if(!reset)
		K2 = 0;
	else if(state == Stop && A)
			K2 = 1;
		else
			K2 = 0;
endmodule
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59

下面总结写状态机的几个注意点:

● 使用parameter确定状态码, 状态码尽量采用独热码,即只有一位为1的编码形式

这里说明以下格雷码和独热码。
格雷码:相邻的代码中只有一bit不同,例如十进制1到5就为000,001,011,010,110,111
其优势在于相邻数字的变化均只对应一个bit变化,相比于普通的二进制码,可靠性高错误率低。比如二进制码从0111到1000变化过程中,可能出现类似1100的中间状态,进而可能引起电路不稳定,而格雷码就没有这个问题。
独热码:只有一bit为高的编码,例如00000,00001,00010,00100,01000,10000
优势也是显而易见,只需比较一bit即可得到结果,但需要消耗更多触发器,然而会节省很多组合逻辑。

对于状态机来说,如果用独热码只需比较对应的bit来确定在哪个状态即可,例如上面的代码中只需根据state[0] == 1即可判断当前状态是否为Idle。但是格雷码or二进制码就得每一位都要比较,消耗很多逻辑。
FPGA内部有丰富的Slice,每个Slice有几个FF,所以FPGA不缺触发器的,故使用独热码更好。

● 大型状态机,可将状态转移always逻辑和输出always逻辑分开写

case语句必须加入default,其状态编码为’bx

有同步or异步复位端

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/688510
推荐阅读
相关标签
  

闽ICP备14008679号