Verilog HDL 总结(1)

1. Verilog 语法的基础概念

Verilog HDL是一种硬件描述语言,其中HDL则是Hardware Description Language的缩写。因此,利用Verilog编写的程序最终会通过工具转换为具体的电路模块。此外,利用Verilog编写的模型可以是实际电路的不同级别的抽象,通常情况下,我们将这种抽象级别分为以下五类:

(1)系统级(system-level):用语言提供的高级结构能够实现待设计模块的外部性能的模型。
(2)算法级(algorithm-level):用语言提供的高级结构能够实现算法运行的模型。
(3)RTL级(register transfer level):描述数据在寄存器之间的流动和如何处理、控制这些数据流动的模型。
(4)门级(gate level):描述逻辑门以及逻辑门之间连接的模型。
(4)开关级(switch level):描述器件中三极管和存储结点以及它们之间连接的模型。

其中,前三种抽象级别为行为级抽象,它侧重于于描述电路的功能;门级和开关级属于结构级别抽象,侧重于模块内部结构实现的具体描述。关系图如下:

1.1 Verilog模块的基本概念

由于Verilog是一门硬件描述语言,因此在编写程序的时候往往是通过模块的形式来进行组织,这里的模块可以看做其他编程语言中的函数,它用于描述一个特定功能的模块。通过将多个模块联合起来最终可以形成我们的设计模型。

下面从几个简单模块的分析来形成对模块的基本认识:

例1:下面是一个二选一多路选择器的Verilog HDL程序:

module muxtwo(out, a, b, sl);
    input   a, b, sl;
    output  out;
    reg out;
        always @ (sl or a or b)
            if(!sl) out = a;
            eles out = b;
endmodule

其对应的电路图如下图所示:

从代码中可以看出,一个模块是一段以module-endmodule包含的代码段构成。紧跟在关键字module之后的是模块名muxtwo以及端口列表(out, a, b, sel),这里包含了四个端口,依次为输出端口out、信号端口a、信号端口b以及选择端口sel。

在模块内部,第2、3行是I/O说明,说明了端口列表中端口的输入输出方向以及端口的位数;第4行是内部信号声明,通常包括reg和wire两种;最后的always块是模块的功能定义,描述的是模块的核心功能。

值得注意的是,上例中的always语句实际上已经属于行为级抽象了,它只关心逻辑功能而不关心其电路结构。下面两个程序是对上面例子中的二选一多路选择器的门级描述方式:

module muxtwo(out, a, b, sel);
    input   a, b, sel;
    output  out;
    wire nsel, sela, selb;
        assign nsel = ~sel;
        assign sela = a & nsel;
        assign selb = b & sel;
        assign out = sela | selb;
endmodule

其对应的模块如下图所示:

注意:上面程序用到了&、|、~等逻辑运算符来实现门电路的功能,我们还可以利用Verilog中的逻辑元件来实现同样的功能。

module muxtwo(out, a, b, sel);
    input a, b, sel;
    output out;
        not         u1(nsel, sl);
        and     #1  u2(sela, a, nsel);
        and     #1  u3(selb, b, sel);
        or      #1  u4(out, sela, selb);
endmodule

注意:这里的not、and、or都是Verilog语言的保留字;u1、u2、u3、u4表示逻辑元件的实例名称;中间的#1代表该门输入到输出的延迟为1个单位时间。

1.2 Verilog用于模块的测试

Verilog可用于描述变化的测试信号。描述测试信号的变化和测试过程的模块也叫做测试平台,它可对上面介绍的电路模块进行动态的测试。通过观测被测试模块的输出信号是否符合要求。

下面看一个Verilog的测试模块,它对上面的二选一多路选择器进行测试:

`include "muxtwo.v"
    module t;
        reg ain, bin, select;
        reg clock;
        wire outw;

        initial
            begin
                ain = 0;
                bin = 1;
                select = 0;
                clock = 0;
            end
    
        always #50 clock = ~clock;

        always @ (posedge clock)
            begin
                #1 ain = {$ random} % 2;
                #3 bin = {$ random} % 2;
            end

        always #1000 select = !select;

        muxtwo m(.out(outw), .a(ain), .b(bin), .sel(select));
    end module 

这里定义了一个模块t,它没有端口列表和I/O说明,其主要包括三个部分:信号初始化、产生激励信号、模块测试。其中,模块测试部分只有一行代码,就是引用我们之前所设计的muxtwo模块,并将测试信号流传进去。我们通过观察输入输出的信号流的变化便可以看到模块的逻辑功能是否正确。

这种测试可以在功能(即行为)级上进行,也可以在逻辑网表(逻辑布尔表达式)和门级电路上进行。它的名称为(RTL)仿真、逻辑网表仿真和门级仿真。如果门级结构模块与具体的工艺技术对应起来,并加上布局布线引入的延迟模型,此时进行的仿真称为布线后仿真,这种仿真与实际电路情况非常接近。

2. Verilog的基本语法

Verilog的语言和C语言的语法很类似,但由于Verilog是硬件描述语言,因此在许多概念上和C语言是完全不同。

2.1 模块的结构

从上面的二选一多路选择器的代码可以看出,一个Verilog模块一致包含四个部分:端口定义、I/O说明、内部信号声明和功能定义。上面已经简单介绍了这一个部分的作用,下面对这几个部分进行详细的总结:

2.1.1 模块的端口定义

模块的端口定义和模块的C语言中函数的定义非常的相似,不过Verilog中模块的输入输出都定义在端口列表中,不像C语言那样由返回值。下面是Verilog中端口定义的标准形式:

module name(port1, port2, port3...);

其中,端口列表中包含了模块中所有的输入输出端口。

2.1.2 模块的I/O说明

I/O说明的作用是对端口列表中各个端口的输入输出方向以及端口的位宽进行说明,标准形式如下:

// 输入口
input[width-1:0] port1;
input[width-1:0] port2;
...
input[width-1:0] porti;
// 输出口
output[width-1:0] port1;
output[width-1:0] port2;
...
output[width-1:0] porj;
// 输入/输出口
inout[width-1:0] port1;
inout[width-1:0] port2;
...
inout[width-1:0] pork;

I/O说明也可以在端口定义列表中声明,格式如下:

module name(input port1, 
			input port2, 
			..., 
			input porti, 
			output port1, 
			output port2, 
			..., 
			output portj);

2.1.3 内部信号说明

内部信号说明指的是对模块内部需要用到的reg和wire类型变量进行说明,如:

reg[width-1:0] variable1, variable2, ...
wire[width-1:0] variable1, variable2, ...

2.1.4 功能定义

Verilog模块中最重要的部分就是模块的功能定义部分,主要由三种方法可以在模块中产生逻辑:
(1) 用assign声明语句,如:

assign a = b & c;

这种方法是最简单的一种方法,它通过"assign equation"的格式来生成逻辑。
(2) 用实例元件,如:

and #2 u1(q, a, b);

采用实例元件的方法像在电路图输入方式下调用库元件一样,键入元件的名字和相连的引脚即可。
(3) 用always块,如:

always @(posedge clk or posedge clr)
	begin
		if(clr) q <= 0;
		else if(en) q <= d;
	end

采用assign 语句是描述组合逻辑最常用的方法之一。而always块既可用于描述组合逻辑,也可用描述时序逻辑。always块可用很多种描述手段来表达逻辑,例如上例中就利用了if...else语句来描述逻辑关系。如果按照一定的风格来编写always块,可以通过综合工具把源代码自动综合成门级结构表示的组合或时序逻辑电路。

值得说明的是,Verilog模块中的所有过程块(如:initial块、always块)、连续赋值语句、实例引用都是并行的。而always块内部的语句是顺序执行的,例如上面always块中为一个if...else条件分支语句,它只有在顺序执行的情况下才是有意义的。此外,两个或多个always块之间是同时执行的。

2.2 Verilog数据类型

Verilog HDL 中总共有19种数据类型。它们用来表示数字电路硬件中的数据存储和传送元素的。其中最基本的四种数据类型为:reg型、wire型、integer型和parameter型,其他的数据大多数都与基本逻辑单元的建库有关,与系统设计没有很大的关系,因此我们可以先不用关心它们的具体用法。

对于我们一般的开发者来说,大多数情况下式利用Verilog进行行为级描述,因此不必过多地关心门级和开关级的Verilog HDL 语法现象。

2.2.1 常量

在程序运行过程中,其值不能被改变的量称为常量。下面先对Verilog HDL语言中使用的数字和表示方式进行总结。
(1) 数字
在Verilog HDL中,整型常量即整常数值有以下四种进制的表示形式:
a. 二进制整数(b或 B)
b. 十进制整数(d或D)
c. 八进制整数(o或O)
d. 十六进制整数(h或H)
举例如下:

8'b10101100

上面表示一个二进制整数,它的十进制值为172,其位宽为8位。该例子采用了一种很全面的表达数字的方式,即:
< 位 宽 > < 进 制 > < 数 字 > <位宽><进制><数字> <><><>
的表达方式,其中位宽和进制是可选项,当省略位宽项时,表示采用默认位宽(由具体的机器系统决定,至少32位);当省略进制项时,表达采用默认的十进制表示方式。

此外,在Verilog中我们可以采用x和z来表示不定值和高阻值,这经常用于判断语句和case语句中,以提高程序的可读性,如下例:

4'b10x0		//位宽为4的二进制数从低位数起第2位为不定值
4'b101z		//位宽为4的二进制数从低位数起第1位为高阻值
12'dz		//位宽为12的十进制数,其值为高阻值
12'd?		//同上
8'h4x		//位宽为8的十六进制数,其低4位值为不定值

值得注意的是,当采用不同进制表示时,x和z表示的位数也不同。

当我们需要表示负数时,只需要在表达式的最前面加上一个减号即可,如:

-8'd5

注意:写成8'-d58'd-5的形式都是错误的。

最后,可以用下划线来分隔开数的表达以提高程序的可读性,如:

16'1010_1011_1111_1010

(2) 参数(parameter)型
在Verilog HDL中用parameter来定义常量,即用parameter来定义一个标识符代表一个常量,称为符号常量,即标识符形式的常量,采用标识符代表的一个常量可以提高程序的可读性和可维护性。格式如下:

parameter	参数名1 = 表达式, 参数名2 = 表达式, 参数名3 = 表达式, ..., 参数名n = 表达式;

其中,parameter是参数性数据的确认符。
值得注意的是,表达式中只能包含之前定义的参数或者常量数字,如下例:

parameter	msb = 7;									//定义参数msb为常量7
parameter	e = 25, f = 29;								//定义两个常量参数
parameter 	r = 5.7;									//声明r为一个实型参数
parameter	hyte_size = 8, byte_msb = byte_size - 1;	//用常数表达式赋值
parameter	average_delay = (r + f) / 2;				//用常数表达式赋值

参数型常量经常用于定义延迟时间和变量宽度。在模块或实例引用时,可通过参数传递改变在别引用模块或实例中已定义的参数。下面的例子说明了在层次调用的电路中改变参数常用的一些用法:

module Decode(A, F);
    parameter   Width = 1, Polarity = 1;
    ...
endmodule
module Top;
    wire[3:0]   A4;
    wire[4:0]   A5;
    wire[15:0]  F16;
    wire[31:0]  F32;
    Decode  #(4, 0) D1(A4, F16);
    Decode  #(5)    D2(A5, F32);
endmodule

上述程序中,顶层模块为Top模块,在Top模块中两次引用了Decode模块,并通过#()的方式将参数的值传递到了D1和D2中。因此,实际上在Top模块中的D1是参数(Width, Polarity)=(4, 0)的Decode模块,D2是参数(Width, Polarity)=(5, 1)的Decode模块。

如果想要在一个模块中改变另一个模块中的parameter值,可以通过defparam命令实现,如下:

module Test;
    wire    W;
    Top     T();
endmodule

module Top;
    wire    W;
    Block   B1();
    Block   B2();
endmodule

module Block;
    parameter   P = 0;
endmodule

module Annotate;
    defparam
        Test.T.B1.P = 2,
        Test.T.B2.P = 3;
endmodule

其中,Annotate模块通过defparam命令改变了顶层模块Top中引用的两个Block模块中的参数P。

2.2.2 变量

变量是在程序运行过程中可以变化的量,在Verilog HDL中变量的类型由很多种,这里只介绍最常用的wire、reg和memory型。
(1) wire型
wire型数据常用来表示以assign关键字指定的组合逻辑信号。Verilog程序模块中输入、输出信号类型默认时自动定义为wire型。wire型信号可以用做任何方程式的输入,也可以用做"assign"语句或实例元件的输出。其定义格式如下:

wire				a;		//定义一个1位的wire型数据
wire[width-1:0]		b;		//定义一个width位的wire型数据

(2) reg型
寄存器是数据存储单元的抽象。寄存器数据类型的关键字为reg。通过赋值语句可以改变寄存器存储的值,其作用于改变触发器存储的值相当。

值得注意的是,reg型数据常用来表示always块中的指定信号,常代表触发器。且在always块中被赋值的每一个信号都必须定义成reg型!

reg型数据的定义方式和wire型数据定义方式相同,如下:

reg			rega;		//定义了一个1位的reg型数据
reg[width-1:0]	regb;		//定义了一个width位的reg型数据

reg型数据的默认初始值是不定值。reg型数据可以赋正值,也可以赋负值。但当一个reg型数据时一个表达式的操作数时,它的值被当做是无符号值,即正值。即当一个四位reg型数据被赋值为-1时,如果将其作为表达式的计算时,则其值被认为是+15。

此外,reg型数据指标是被定义的信号将用在"always"模块内这一点非常重要。并不是说reg型信号一定是寄存器或触发器的输出,虽然reg型信号常常是寄存器或触发器的输出,但并不一定总是这样。

(3) memory型
Verilog HDL中实际上是没有专门的memory类型数据的,它是通过对reg型变量建立数组来对寄存器进行建模,它类似于C语言中的二维数组。其格式如下:

reg[n-1:0]	mem1[m-1:0];
reg[n-1:0]	mem2[m:1];

具体的例子如下:

reg[7:0]	mema[255:0];

该例子定义了一个名为mema的存储器,该存储器有256个8位的寄存器,地址范围是0-255。

注意:在Verilog中对存储器进行地址索引的表达式必须为常数表达式,如下:

parameter	wordsize = 16,
			memsize = 256;
reg[wordsize]	mem[memsize-1:0];

该例子定义了一个名为mem的存储器,该存储器有256个16位的寄存器。

最后还有一点值得注意的是,我们不能对存储器整体进行赋值,如mem = 0语句是非法的,如果我们要改变存储器的值,应当指定该单元在存储器中的地址,如下:

mem[5] = 0;			//将mem中的第5个单元赋值为0

2.3 运算符及表达式

Verilog HDL 语言的运算符很丰富,按功能分可分为以下几类:

类别运算符
算数运算符+、-、×、/、%
赋值运算符=、<=
关系运算符<、>、<=、>=
逻辑运算符&&、||、!
条件运算符?:
位运算符~、|、^、&、^~
移位运算符<<、>>
拼接运算符{}
其他等式运算符、缩减运算符等

2.3.1 基本算数运算符

在Verilog HDL中,算数运算符又称为二进制运算符,其中加法运算符又称为正值运算符,减法运算符有称为负值运算符。在除法运算中,结果值要略去小数部分,只取整数部分。而进行取模运算时,结果值的符号位采用模运算式里的第一个操作数的符号位。此外,要求进行模运算的两个量必须都是整数。如下:

10/3			//结果为3
10%3			//余数为1
-10%3			//余数为-1
10%-3			//余数为1
12%3			//余数为0

2.3.2 位运算符

Veirlog HDL作为一种硬件描述语言,是针对硬件电路而言的。在硬件电路中有4中状态,即1,0,x,z。在电路中信号进行与、或、非时,在Verilog HDL中则是响应的操作数的位运算。VerilogHDL提供了以下5种位运算符。

位运算符作用
~取反
&按位与
|按位或
^按位异或
^~按位同或

其中,除了~是单目运算符外其余都是双目运算符。

下图列出了五种为运算符的运算规则:

在这里插入图片描述

值得提及的是,当两个长度不同的数据进行为运算时,系统会自动第将两者右端对齐,位数少的操作数会在相应的高位用0填满,以使两个操作数按位进行操作。

2.2.3 逻辑运算符

Verilog HDL中有三种逻辑运算符
(1)&&逻辑与
(2)|| 逻辑或
(3)!逻辑非
它们的用法和含义与C语言中的类似,因此在此不赘述了。

2.3.4 关系运算符

Verilog HDL中有共四种关系运算符
(1)a<b
(2)a>b
(3)a<=b
(4)a>=b
在进行关系运算时,如果声明的关系是假的,则返回值为0;如果声明的关系是真的,则返回值为1;如果某个操作数的值不定,返回的是不定值。

2.4.5 等式运算符

Verilog HDL中存在四种等式运算符
(1)==
(2)!=
(3)===
(4)!==
其中与C语言不同的点在于===!==,下面列出=====的真值表:
在这里插入图片描述

2.4.6 移位运算符

Verilog HDL中有两种移位运算符,分别是>><<,其使用方法如下:
a > > n 或 a < < n a>>n或a<<n a>>na<<n
其中n表示要移动的位数,Verilog中用0来填补移出的空位。

4'h1001 << 1			//5'h10010
4'b1010 << 2			//6'b101000
1<<b					//32'b1000000
4'b1001 >> 1			//4'b0100
4'b1001 >> 4			//4'b0000

2.4.7 位拼接运算符

位拼接运算符{}是Verilog HDL中特殊的运算符,利用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作。使用方法如下:
{ 信 号 1 的 某 几 位 , 信 号 2 的 某 几 位 , . . . , 信 号 n 的 某 几 位 } \{信号1的某几位,信号2的某几位,..., 信号n的某几位\} {12...,n}
例如:

{a, b[3:0], w, 3'b101}
或
{a, b[3], b[2], b[1], b[0], w, 1'b1, 1'b0, 1'b1}

位拼接还可以采用重复发来简化表达式,如

{4{w}}
{b, {3{a, b}}}

2.4.8 缩减运算符

缩减运算符是单目运算符,它的作用相当于对一个数的所有位进行递推位运算。如

reg[3:0]	B;
reg			C
	C = &B;

相当于:

C = ((B[0] & B[1]) & B[2]) & B[3]
  • 53
    点赞
  • 331
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值