Verilog HDL的程序结构
本文转自:IP与SOC设计
首先我们不开始讲Verilog HDL的语法,我们从Verilog HDL的程序结构出发。相信大家都看过芯片吧,它有个名字,有个外壳,外壳向外伸出有引脚(BGA封装的那种请不要乱搅和...),然后芯片它可以实现一定的功能。
Ok,知道这些之后,我们就来看看Verilog HDL的描述数字电路的程序结构吧。
在解释结构的时候,我拿芯片设计这个例子来打一个不恰当的比方。
VerilogHDL程序的大致结构就是这么一个形式,每一个模块的结构都是一致的只不过语句之间存在一些差别,每一部分的具体描述将在后面进行。
verilog程序结构的简单描述
OK,现在让我们现在开始了解一下一些语法和注意事项(注意,我这里不会把语法讲得很细很细,我主要是描述一些(我,或者初学者)易忘的,关键的语法和知识点),描述的顺序不一定按照上面的各个部分哟,我们先描述一些VerilogHDL程序必须的。
首先是模块说明:
module和endmodule ,这两个关键词成对出现,一般的内容都会囊括在这两个两个关键词之间。一个verilog(.v)文件可以有多个module ...endmodule,但是为了方便管理,建议只实现一个。
此外无论是能够综合成电路的verilog程序还是只是仿真的程序,都需要以模块的形式给出。
模块名:
①模块名的定义要符合标识符的定义,至于什么是标识符,以后会说的。此外也要注意书写的规范性。
端口说明:
①端口可以比方成芯片的输入输入管脚,它只有三种类型:输入,输出,双向;
②端口有一些附加属性,如数据类型、符号特性、位宽等;input端口只能是线网类型,output可以是寄存器类型也可以是线网类型,inout也只能是线网类型。至于线网类型和寄存器类型,在以后的数据类型中会介绍。
③当端口是总线类型时,可以简单地理解为端口有多位时,对应于同时描述芯片的多个管脚时,可以给端口加上位宽。即input wire [M:N] a;如果M>N,则为降序,a[M]为最高位,位宽为M-N+1位;如果M<N,则最高位位a[N]位,位宽位N-M+1位;
④端口的属性声明顺序可以是:
端口的输入输出 数据类型 有无符号 位宽 端口名称;
如:output reg signed [7:0] a;
注:数据类型没有写则默认为wire 型,有无符号没有写默认为无符号型;位宽没有写默认为1bit 。
⑤当书写完端口声明是,括号后面的‘;’千万不要忘记。
程序主体:
①前面我们提及到verilog的描述层次中有一个结构化描述,结构化的描述就是一点一点地例化(子模块、单元门)来实现系统/设计的功能。程序的主体可以是只例化一些门单元,但是verilog的这种纯结构化方式的显然是无法满足现代设计要求的,因此纯结构化方式已经被遗弃。
②程序的主体可以是行为描述,也就是通过一些verilog的行为语句来实现设计的要求/功能。行为描述主要有:控制流描述,过程描述,块语句,时序控制等。对于行为描述,有可综合的,有不可综合的,可综合的行为描述语句/语法就可以认为是RTL级描述时用到的,而不可综合的行为描述语句/语法可以认为是仿真/建模时用到的。
③下面大概说一下行为描述情况,也就是行为描述中无论是可综合还是不可综合都可以使用的情况,具体的可综合和不可综合的行为级描述语法在后面会描述。
控制流描述:以assign关键字开头的语句描述称为控制流描述,主要用来实现一些简单的组合逻辑。
块语句:包含在关键字begin...end 、fork ...join之间的语句称为块语句。
过程描述:由initial模块、always模块、function模块、task模块这四个模块实现的过程。
时序控制:延时控制、敏感信号控制语句等
④块语句可以有一个名字,写在begin/fork后面(如beign:adder_disc ),给它一个名字有什么好处呢?好处有两个,一个是可以在块内部定义寄存器变量(注意哦,只能寄存器变量,这个寄存器变量在块内部使用),另外一个就是可以用disable这个关键字来中断语句块的执行(具体怎么弄,请参看其他书籍,这里不详述)
⑤begin...end内的语句是串行的,这是从语法的结果上讲的,但是它是可以综合的,综合出来的实际电路是并行的,也就是实际电路中,各条语句之间并不全是串行的,这里需要建立一个概念,方便讲解以后的阻塞赋值和非阻塞赋值。而fork...join 内的语句是同时进行的,然而这是不可综合的。
⑥一个程序的主体中可以有多个initial模块、always模块、function模块、task模块。Function模块和task模块以后介绍。多个Initial模块、always模块之间是并行的,但是initial只执行一次,而always是反复执行。
initial一般是不可综合的,用在仿真当中;在进行仿真时,通常被用来描述测试模块的初始化、监视、波形生成等功能。
always用在可综合的描述当中,一般情况下由敏感列表触发(至于什么是敏感列表,敏感列表怎么用,请参考其他书籍,我后面也会做一些记载),也就是用在描述RTL级的描述当中,通常被用来对硬件的功能进行描述,可以实现组合逻辑和实现逻辑的功能。
上面没有给出initial格式和always格式只是提了一下简单应用。
⑦程序的主体大多可以由结构描述和行为描述构成
模块的例化:
①模块的例化主要是为实现总体的功能,比如说我要设计一块芯片,这个芯片的某个功能可以有某个芯片来完成,那我就可以调用这个芯片,例化就是调用这么一个概念。
②例化可以使用位置映射,也可使用名称映射,由于位置映射可读性太差,容易出错,所有建议使用名称映射。下面是名称映射的格式:
被调用(例化)的模块名字 自定义的例化模块名(
.被调用(例化)模块的端口名 (本模块中的线网变量名),
...
.被调用(例化)模块的端口名 (本模块中的线网变量名)
);
③悬空处理:
在模块例化时,如果被例化的模块输入管脚悬空,则该输入为高阻Z,相当于与外界隔绝,但是实际电路中...
如果将输出管脚悬空,则该输出管脚将被废弃掉。
④不同端口位宽处理
当模块例化端口和被例化的端口位宽不同时,端口通过无符号数的右对齐截断方式进行匹配。举个例子说:
参数定义与映射:
①参数定义就是parameter 那一部分了,也就是可以用一个标识符表示一个固定的参数,当然`define这种宏定义也可以实现这种功能,这样子提高程序的可维护性和可读性。
使用`define时语法为:`define A 2'b10 或者`define A 2.
一般把`define宏定义语句放在模块最前面,并且要注意,无论是在子模块还是顶层模块中,A 的值都代表2(调用时为 `A)(全局参数)。
使用parameter定义的语法为 parameter A = 2或者parameter A= 2’d2;
一般放在模块名字和模块端口列表之间:模块名 #(parameter A = XX,.....)(端口列表);使用parameter定义的参数只在当前模块中有用(局部参数)
localparam定义时,上层模块不能调用传递参数。
②参数传递:
参数传递就是在编译或者仿真的时候,进行维护时,对参数空闲重新复制而更改其值。传递的参数是子模块中定义的parameter,传递的方法有两种:
(1)使用’#’符号:在同一模块中使用’#’符号,参数赋值顺序必须与原始模块定义的顺序相同,并不是一定要给所以的参数赋值,但是不允许跳过任何一个参数,即使是保持不变的值也要写在相应的位置。说了这么一大坨,还是举个例子吧:
这样子,A_WITH就是x1,B_WITH不改变,但是要写在相应的位置。
(2)使用defparam关键字
直接上格式:
Verilog基本上熟悉了,继续整理一下Verilog的学习笔记吧。接下来就得了解一下Verilog的一些基本要素了,也就是Verilog是怎么一点一点写出来的。
Verilog HDL程序设计——基本要素
1、标识符与注释
前面已经说到,模块名的定义要符合标识符的定义,那么什么是标识符呢?它的语法是什么呢?
①标识符是赋给对象的唯一名称,通过标识符可以提及相应的对象,Verilog语法将对转义标识符中的字符逐个处理。
②标识符可以是字母、数字、下划线和美元符$的组合,并且标识符的第一个字母必须是字母或者是下划线。此外,在Verilog的标识符中,是区分大小写的。
③Verilog中有一些关键字,简单地了解就是,预定义好了的,用来说明语言节后的标识符,都是小写的。标识符不能和关键字重复。
④Verilog中还有一种叫做转义标识符的东西,定义为以\(反斜杠)符号开头,以空白结尾(如一个空格)的字符。如\initial就是一个转义字符。转义标识符和关键字是不一样的,比如\initial是非关键字,而initial是关键字。
语言中总需要一些注释的,Verilog中两种注释方法:
(1)以/*开始注释,*/结束注释,即/* 注释内容*/,可以多行注释。
(2)以//开头,这种注释只能注释一行
2、常量
对于一门语言,我们总可言考虑它的常量有哪些,变量有哪些,现在就让我们看看Verilog中的常量有哪些吧。
(1)数值逻辑
数字逻辑就是一种状态,可言说说一种常量了,有下面的知识点/注意点:
①Verilog中有四种罗家数值:逻辑0,逻辑1,x:未知态,Z高阻态;其中x、z是不区分大小写的;在verilog中,表达式和逻辑门输入的z通常解释为x,也就是不定态,不能确定这个逻辑值是1还是0。
②实际电路中只有0或者1,没有x和z,当你给电路中设置为x或z时,由编译软件或者综合软件等EDA软件决定电路最终是0或者1.
(2)数值
一个数也是一种常量。
①Verilog中主要可以这么对数值进行组合,整数和实数,有符号数和无符号数。在Verilog中,下划线’_’可以随意用在整数和实数中,没有实际意义,只是提高了可读性。
②对于Verilog中的整数,可以分为简单的十进制数和基数表示的整数。
One:简单的十进制格式的整数定义为带有“+”或者“-”操作符的数字序列,你如45表示十进制数45,-45表示十进制数-45。
注意,简单的十进制数格式的整数代表一个有符号的数,其中负数可使用两种补码形式表示。下面举例子进行说明:
对于简单的十进制32,它是一个有符号的数,由于二进制中才对有无符号进行区分,因此它在二进制中,用6位表示就是100000或者用7位表示就是0100000(最高位是符号位);对于简单的十进制数-15,在二进制中,用5位表示就是10001,用6位表示就是110001(最高位是符号扩展位)。
Two:基数格式的整数格式是:[位宽]’基数 数值,位宽是一个数值,表示数值位长,基数可以是二进制(b)、八进制(o)、十进制(d)、十六进制(h),字母不区分大小写,不然7’d100表示7位的十进制数100。
③实数有十进制计数法(如2.0)和科学计数法,但是根据Verilog的定义,实数都通过四舍五入隐式的转换为最相近的整数。
(3)字符串
字符串是双引号内的字符序列,不能分成多行写。此外,字符串是8位ASCII值的序列,比如“char”这个字符串,就要8x4=32bit寄存器存储它。一些综合器是不支持字符串的。
(4)参数
通过parameter 、localparam等定义的参数,也可以看成是常量,它们的格式记录在前面一篇的博文里面了。
3、变量--数据类型
在Verilog中,它的变量可以用另外的名称代替:数据类型。Verilog的数据类型是一种“变量”,用来表示数字电路硬件中的数据存储和传送元素。Verilog中主要有两大数据类型(变量):线网类型和寄存器类型。
(1)线网类型
①线网类型主要表示表示Verilog中的结构化元件之间的物理连线,其数值由驱动单元决定;如果没有驱动元件连接到网线上,则其默认值为高阻z。此外线网类型的变量只能用assign进行赋值驱动,不能在always中进行赋值。
(可综合的线网类型:)
②线网类型中的wire变量是最常用的,它可以最为任何表达式的输入,也可以用做assign语句和模块例化的输出。Wire的取值是0、1、x、z。此外虽然Verilog语法允许wire类型的数据变量允许多个驱动源,但是仅用于仿真当中,综合中任何变量连接多个驱动源都是错误的。
③tri线网类型,这个类型与wire类型功能几乎一样,但是当总线上需要描述高阻态的特性时,用它来描述以跟wire进行区分。
④supply1和supply0线网类型:supply1用来对电源建模,即高电平1,;supply0用来对低电平进行建模,即低电平0。
(不可综合,进用于仿真的线网类型:)
⑤wor(线或)、trior(三态或):专门用于单信号多驱动;
Wand(线与)、triand(三态与):专门用于多驱动源;
Trireg:具有电荷保持特性的连线;
Tri1:上拉电阻;tri0:下拉电阻。
(2)寄存器类型
①寄存器型变量,都有“寄存特性”,即在接受下一个赋值之前,将保持原值不变。寄存器变量没有强度之分,且所以的寄存器类型变量都必须明确给出类型说明(无默认状态)。
(可综合的寄存器类型:)
②最常用的是reg类型的寄存器变量。寄存器变量可以取任意长度,默认值未知,reg类型的数据可以是正值或者负值。但当一个reg类型的数据是一个表达式的操作数时,它的值被当做无符号数,即正值(比如,你定义了reg [3:0] a;...a = -2;那么由于-2的补码是1110,a中存储的值是1110,也就是值其实是14)。
③integer类型,这种类型是整数寄存器类型,可以作为32位的普通寄存器使用,但是不能直接取这个变量的某一位,而是通过把这个变量赋予给一个32位的reg变量,对reg变量进行操作。
④赋值注意:赋值总是从最右端的位向最左端的位进行;任何多余的位将被截断。此外由于整数的负数形式实质上是以补码向量表示的,因此需要注意赋值的位宽。
⑤reg的扩展类型——memory类型:reg [n-1:0] 存储器名 [m-1:0] ;(表示深度是m,字宽是n的存储器,可以存储mxn个bit)。此外不能直接对memory进行读写,而是先定义一个地址寄存器,通过这个地址寄存器进行索引,再取值。
(不可综合,仅用于仿真的寄存器变量有:)
⑥time类型:用于存储和处理时间,只存储无符号数;
Real类型:实数类型;realtime类型;
由于这种电路设计中不常用,所以不过多记载。
四、运算符
在Verilog中,所谓的运算符就是用来进行运算的,根据运算符所带的操作数的个数,可以分为单目、双目、三目。然后我们还是喜欢根据功能进行划分,大概有9种功能类型的运算符。
(1)赋值运算符
①赋值运算分为连续赋值和过程赋值。
②连续赋值语句,也成为数据流描述方式,用assign关键字表示,赋值符号是“=”,只能对线网赋值。
一个线网型变量一旦被连续赋值语句赋值之后,赋值语句右端赋值表达式的值将连续对被赋值变量产生连续驱动。只要右端表达式任一个操作数的值发生变化,就会立即出发对被赋值变量的更新操作。
③过程赋值,主要用于initial模块和always模块中的赋值语句。
在过程块中,只能使用过程赋值语句,同时过程赋值语句也只能用在过程赋值模块中。
过程赋值的赋值符号是“=”“<=”,分别表示阻塞赋值和非阻塞赋值。
(2)算术运算符
①算术运算符又称为二进制运算符,有+(加)、-(减)、*(乘)、/(除)、%(取余)。
②+、-、*是可以综合的,/和%只有在除数或者模值是2的整数次(2、4、8...)的时候才是可以综合。
③在进行乘除运算时,结果值会略去小数部分;在取余操作中,结果的符号位和取余运算第一位操作数的符号位保持一致(-12/(6/4),符号位与-12一致)。
④在进行基本算术运算时,如果某一操作数有不确定的值x,则运算结果也是不确定值x。
⑤算术表达式结果的位宽由位宽最大的操作数决定;在赋值赋值语句中(无论是连续还是过程赋值),算术操作的结果的位宽由操作符左端目标位宽决定;在较长的表达式中,中间结果的位宽应取最大操作数的位宽;此外由于位宽的关系,在运算是要进行位宽保留,也就是结果位宽取大一点,防止溢出。
⑥有无符号的算术运算讨论:在设计中,所有的算术运算都是按照无符号数进行的;如果要完成有符号数的计算,对于加减操作,通过补码处理即可用无符号加法完成;对于乘法操作,无符号数直接采用“*”操作,有符号数,通过定义输入输出和中间变量的符号类型为signed来处理。
(3)逻辑运算符
①逻辑运算符有:逻辑与&&、逻辑或||和逻辑非!;其中&&和||是双目运算符,运算结果是一位;!是单目运算符,结果是一位。
(4)关系运算符
①关系运算符有8种:大于(>)、大于等于(>=)、小于(<)、小于等于(<=)、逻辑相等(==)、逻辑不相等(!=)、全等(===)、全不等(!==)。
②关系运算符的结果是1位(包括1、0、x、z)。
③“===”和“!==”可以比较含有x和z的操作数,但是由于实际硬件中不存在x态和z态,在综合时将按照“==”和“!=”来进行;其实际的功能仅用在仿真中。
(5)条件运算符
①条件运算符,三目运算符:(条件表达式)?():();
(6)位运算符
①位运算符有:按位与(&)、按位或(|)、按位反(~)、按位异或(^)、按位同或(^~或者~^)。除~外,都是双目运算符。
②运算结果可能是多位(每一个操作数的对应位进行运算,得出的结果也是各个位运算“拼”起来的结果),不仅仅是1为,注意与逻辑运算符的区别!,此外如果两个操作数的长度不相等,将会对较短的数进行高位补0,然后进行相应的位运算,使输出结果的长度与位宽的操作数保持一致。
(7)拼接运算符
①拼接运算符可以将两个或者更多信号的某些位拼接起来进行运算操作,{a1,b1,c1...}这样子拼接,拼接运算也可以复制一个常量或者变量。
(8)移位运算符
①移位运算符有两个:左移(<<)、右移(>>),移位过程中都用0来填补移出的空位,左移会引起位数扩大,而右移则不会(如4’b1101<<2 = 6’b110100);因此要注意移位后变量的位数,以及存储移位后结果的存储器/线网位宽。
②左移相当于乘2,而右移相当于除2,在实际运算中,经常通过不同移位数的组合来计算简单的乘法和除法,比如result=data*19中,因为20=16 + 2 + 1=2^4+2^1+2^0,所以result = data<<4 + data<<1+data;(当然实际代码中不是这样的,而是在always块中进行不同的移位,中间寄存器进行存储移位结果,然后用assign语句进行加起来)
(9)一元简约/归约运算符
①一元归约运算符是单目运算符,操作数放在右边,操作符有归约与(&)、归约或(|)、归约与非(~&)、归约或非(~|)、归约异或(^)、归约同或(~^),运算形式是(?):首先将操作数的第一位和第二位进行相应的与、或等操作,然后再讲运算结果和第三位进行操作,依次类推直至最后一位。
本文转自:http://www.cnblogs.com/IClearner/
作者:IC_learner
*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧