Verilog描述方法与层次
Verilog语言有多种描述方法,这些方法也可以在多个层次上来描述硬件。
描述方式
在上一篇当中已经引入过数据流描述、行为描述、结构化描述这三种描述的方式的概念,本篇将继续深入说明这三种描述方式。
数据流描述
1.数据流 :组合逻辑电路的信号传输其实就类似于数据的流动,数据从中经过但是不会存储,一旦输入改变,输出随之在一定的延时(Tpd)之后发生改变。
连续赋值语句
连续赋值语句具有以下的特点:
1.连续驱动:输入的改变将导致该语句重新计算
2.只有线网型变量可以用assign赋值:仿真器中不会储存assign赋值的变量,寄存器有存储要求显然不能用assign赋值,只有线网型可以。另外,assign语句允许多驱动即一个变量可以多个输入(实际上就是实现线与和线或),建议复习一下数字电路中线与和线或的实现。
3.适用于组合逻辑建模:若要模拟延迟可以通过 # 来加延迟
4.并行性:assign语句同其他语句块是并发的。
以一个异或门为例:
module FullAdder(input x,input y,input c_in,output c_out,output sum);
assign sum = x^y^c_in;
assign c_out = (x&y) || (x&c_in) || (y&c_in)
//以上两条语句都是并发执行且连续赋值的。
endmodule
延时
任何一个元器件都必然存在延时,而在数据流描述方式中,我们也可以选择对其进行建模。
延时具体也可以分为:上升延时即输出变为1时的延时,下降延时即输出变为0的延时,关闭延时即输出变为高阻态的延时,以及输出变为X即中间态的延时(通常取前面三种中最小的)。
关于延时的建模也有多种方法,以下是一个例子:
assign #1 out = in1^in2;//`timescale 1ns/1ns输入到输出须1ns
assign #(1,2) out1 = in1^in2;//上升延时1ns,下降延时2ns,变x和关闭取最小为1ns
assign #(1,2,3) out2 = in1^in2;//上升1ns,下降2ns,关闭3ns,变X1ns
assign #(4:5:6,3:4:5)out3 = in1^in2;
//(min:typ:max)描述的时延时最小典型最大三种情况,同样第一个是上升,第二个是下降
注意:连续赋值中的延时是惯性延时,即出现小于延时的信号(毛刺信号)会被过虑掉。用户只能在逻辑综合中对时序加以控制,没办法直接操控器件本身的特性。
多驱动线网
多驱动的意思实际上就是可以将几条线连到一起,实际上这样的情况主要集中在:线与、线或以及三态总线等实际应用当中。
wor wire_or;
wand wire_and;
tri wire_tri;
assign wire_or = in1 & in2;
assign wire_or = in3 & in4;//以上两个右边结果的或
assign wire_and = in1 | in2;
assign wire_and = in3 | in4;//以上两个右边结果的与
assign wire_tri = (Write)? mem1 : 1'bz;
assign wire_tri = (read)? mem1 : 1'bz;//高阻态确保了能切断电气连接不会混乱
千万注意:一般的wire类型是绝对不允许多驱动的,数字电路中一旦这样接就会产生各种各样的竞争冒险,造成严重的输出混乱。
行为描述
行为描述使用语言描述电路的行为。行为描述的语句只有:always和initial。
行为描述的语句格式
1.initial与always语句块
主要把握两者的区别:initial只执行一次之后就会永久挂起,而always一旦符合敏感表的条件,就会反复执行。
2.过程块中的语句种类
主要有四类:阻塞语句与非阻塞语句,连续过程赋值(是可以做连续过程赋值的)和高级编程语句。
always@(posedge Clock or negedge Rst_n)//always语句块
begin
if(!Rst_n)//高级编程语句
begin
Reg_A <= 0;//非阻塞语句
Reg_b <= 0;
end
else
begin
Reg_A = Input_A;//阻塞语句
Reg_b = Input_B;
end
end
3.时序控制(Timing Control)
时序控制主要通过三种方式进行控制:事件语句“@ ”,延时语句“#”,等待语句
`timescale 1ns/1ns
module Dff(input D,input Clk,input Rst,output Q);
always@(posedge Clk or negedge Rst)//事件发生即变量发生变化时执行
begin
if(Rst)
Q <= 0;
else
Q <= D;
end
endmodule
initial
begin
#5 clk = 0;//表示5ns后clk赋值0
end
wait()语句许多综合工具仍不支持,也主要用在仿真时,所以不赘述了。
过程赋值语句
过程赋值语句即在initial块和always块中赋值的语句,主要分为:阻塞语句与非阻塞语句。
1.阻塞语句
常规形式 寄存器变量 = 表达式
所谓阻塞其实有两层含义:其一,计算表达式与赋值是统一的原子操作,不可再分,其中不能插入其他操作。其二,在begin...end代码块中必须把这个表达式赋值完才能做后续的操作,完成之前将完全阻塞后续操作,是顺序执行的。而阻塞往往也是用在组合逻辑电路当中的。
wire A;
wire B;
wire C;
reg temp;
reg D;
always@(A or B or C )
begin
temp = A&B;
D = temp | C;//这样写可以使得逻辑非常清晰
end
有意思的是,实际综合的过程中 D 与 temp 是不会被作为寄存器综合的,综合时仍然作为组合电路去处理。
2.非阻塞赋值
常规形式 寄存器变量 <= 表达式
对应于阻塞赋值的情况:非阻塞意味着左右两边的操作不是作为一个原子操作进行的,执行时首先计算右边表达式,但并不马上赋值。同时在begin...end语句中出现的非阻塞赋值语句会默认优先级较低,计算后会在靠后的时间才赋值。这带给我们以良好的并发特性,在时序逻辑电路中非常实用。
always@(posedge Clk)
begin
Q1 <= D;
Q2 <= Q1;
Q3 <= Q2;//这很好地描述了一个三级流水寄存器电路
end
3.过程连续赋值
在过程语句中是允许用assign对寄存器赋值的。但是需要注意这一赋值是强制性的,有需要时必须放开否则无法继续对该寄存器进行操作了。常用在测试中。这即称为连续过程赋值。
关键字:assign 与 deassign;force 与 release;
module(input D,input Clk,input Clr,output Q);
always@(Clr)
begin
if(!Clr)//实现一个异步清零的操作
assign Q = 0;
else
deassign;
end
always@(posedge Clk)
Q <= D;
endmodule
语句组
语句组包含两类:顺序语句组begin...end...;并行语句组fork...join;
1.顺序语句组
其中的语句是一条一条的顺序执行的。当然,正如前文所述,对于非阻塞性的语句,是不会阻塞后续执行的。
下面以一个典型的带延时的语句来说明这一点:
`timescale 1ns/1ns
initial
begin
#2 Data = 0;
#4 Data = 1;
#8 Data = 2;
end
//实际上如果观察波形图会发现:2ns时Data为0,6ns时Data为1,14ns时Data为2
//三个语句是顺序的
2,并行语句组
用同样的例子来阐述这一情况
`timescale 1ns/1ns
initial
fork
#2 Data = 0;
#4 Data = 1;
#8 Data = 2;
join
//实际上如果观察波形图会发现:2ns时Data为0,4ns时Data为1,8ns时Data为2
//三个语句是并发的
3.语句组的标识符
通过语句组的标识符可以实现类似C中局部变量的存在,即只在代码块内有效的变量。
高级编程语句
Verilog的特点就是设计层次较高,不仅在晶体管级和门级,主要在RTL级和行为级描述,以及编写测试激励。
为了提高抽象能力,Verilog从C中借鉴了一些语句,同时也创造了一些语句,统称为高级编程语句。
注意:高级编程语句只能出现在initial和always过程块中,也可以互相嵌套。
主要有三类:if...else语句,case语句,循环语句
1.if...else语句
正如C语言中那样,if...else表示的是条件判断。其中也允许继续嵌套语句组(begin..end或fork..join)。但是在硬件当中,则会体现一些新的特点。
首先,if...elif...else天然就具有优先级。
always@(a or b or c or sela or selb)
begin
if(sel_a)
out = a;
else if(sel_b)
out = b;
else
out = c;
end
//实际上实现了一个两级的多路选择器
//而选择sela的优先级最高,如果sela是关键路径则显然有利于电路实现
注意:应该避免使用if语句产生锁存器。尽量应该避免出现 a = a的情况导致锁存,因而有时需要遍历全部的条件,或者设置不关心值 x。
always@(sel or a or b or c)
begin
if(sel == 2'b00)
q = a;
else if(sel == 2'b01)
q = b;
else if(sel == 2'b10)
q = c;
//这里没有说明sel = 2‘b11时的情况,因而系统默认此时q值不变从而出现锁存器
//锁存器(latch)是应该避免的器件,因为容易产生竞争冒险
//应该加上以下语句:
//else q = 1'bx 此时不关心q取什么。
end
2.case语句
case语句的语法仍然同C语言中没有大的区别。但是我们要注意同为条件判断语句在硬件实现中它与if...else的区别与联系。
区别:case中所有判断是并发的,不存在优先级的概念。
联系:case中也容易产生锁存器,因而也需要格外注意default的设置。
always@(sel or a or b or c)
begin
case(sel)
2'b00 : q = a;
2'b01 : q = b;
2'b10 : q = c;
default : q = 1'bx;//如果不写就会出现锁存。
endcase
end
case还有两个派生的语句casex(表示对x不关心)与casez(表示对高阻态不关心)
casez(encoder)
4'b1zzz : out1 = 3;
4'bz1zz : out1 = 2;
4'bzz1z : out1 = 1;
4'bzzz1 : out1 = 0;
endcase
casex(encoder1)
4'b1xxx : out2 = 3;
4'bx1xx : out2 = 2;
4'bxx1x : out2 = 1;
4'bxxx1 : out2 = 0;
endcase
实际上,在单热线(one-hot)中为了节省译码时间经常这样编码加快运行速度。
3.循环语句
循环语句用于重复的操作。
在描述硬件的过程中,不是非常推荐写循环语句,因为在综合时,软件实际上时对循环从头到尾全部执行一遍。可能看起来接近软件程序的写法比较简洁,实际上对于硬件描述不是很好。
通常循环语句都是用于后期对于电路的验证激励。
循环语句主要有四种:forever, repeat, for(), while()
//===================单独的代码块
initial
begin
clk = 0;
count = 0;
while(count<101)
begin
$display("count = %d",count);
count = count + 1;
end
forever # clk = ~clk;//forever常常用于生成时钟
end
//===================单独的代码块
integer i;
always@(posedge clk)
begin
for(i = 0; i<100;i = i+1)//也是常常用在激励中做输入输出
q = i;
end
//===================单独的代码块
if(in == 1)
repeat(8)//表示重复以下操作八次
a = a + 1;
结构化描述
结构化描述实际上就是在描述中使用已有的或者已经写好的功能模块。包括:门源语,用户自定义原语,其他模块。
写好结构化描述对于代码的维护和扩展有很大的帮助。也容易理清各个模块之间的关系。
module half_add(
input a, input b, output c_out, output sum)
xor u_xor(sum,a,b);//assign sum = a^b;
and u_and(c_out,a,b);//assign c_out = a&b;
//使用了门原语
endmodule
实例化模块的方法
1.首先要再次强调输入输出的问题:
input 输入在模块内是默认一个线网类型
output 输出在模块内默认为一个寄存器或者线网类型
inout 在模块内默认是线网类型,双向信号应该是tri
2.实例的方式
名称对应:module(.x(a),.y(b),.z(c) ...)通过名称做对应;
位置对应:module(a,b,c ...)外部的信号与对应端口信号在括号的位置一一对应,类似C语言的函数调用;
参数化模块
1.参数定义
参数在硬件描述中有着重要的位置,例如上一篇提到过的,使用参数而不是编译指令去对状态机的状态进行描述等等。
实际上,当实例化模块是,用户是可以修改模块的参数来实现不同的特性。因此,用户可以定制一个模块具有缺省的参数值从而来改变参数做成不同的模块。
2.参数的定制
有两种方法:主流的Altera和Xlinx各使用了一种。这里介绍简单介绍一下:
通过defparam关键字对参数重新定义
在实例化模块中代入。类似于模块的位置对应。
系统层次
上一篇提到过,Verilog语言共有五个层次,自上到下:系统级,行为级,RTL级,门级,晶体管级。
我们设计中主要使用的都是RTL级。RTL级也是主流综合器最常用的级别,行为级虽然描述简单,但是实际综合中会出现很多错误。
而对于验证中则常常使用行为级。
严格的说,描述层次之间没有严格的界限,根据工作特点和关注要素不同要合理选择。
主要内容和示例来源:《轻松称为设计高手 Verilog HDL实用精解》;北京航空航天大学出版社;
推荐练习网站:https://hdlbits.01xz.net/wiki