描述方式
Verilog有3种最基本的描述方式:
- 数据流描述,采用assign连续赋值语句;
- 行为描述,使用always或者initial语句块中的过程赋值语句;
- 结构化描述,实例化已有的功能模块或原语。
数据流描述
数据流
在数字电路中,信号经过组合逻辑时类似于数据的流动:信号从输入流向输出,而信号不会在其中存储。当输入辩护时,总会在一定时间后体现在输出端。为了模拟数字电路的这一特性,对其建模,这种建模方式称为数据流建模。数据流描述最基本的语句是assign连续赋值语句。
连续赋值语句
异或门描述:
assign #1 A_xor_wire = eq0 ^ eq1;
- 连续赋值语句特点:
- 连续驱动,任何时刻输入的任何变化都将导致该语句重新计算;
- 只有线网类型能在assign中赋值,线网类型的变量可以被多重驱动,即可以在多个连续赋值语句中驱动同一线网;寄存器变量就不行,不能被不同的行为进程驱动;
- 使用assign对组合逻辑建模,因为assign语句中加延时可以非常精确的模拟组合逻辑的惯性延时;
- 并行性,assign语句和行为语句块(always和initial)、其他连续赋值语句、门级模型之间是并行的,一个来连续赋值语句是一个独立进程,进程之间是并发的,同时也是交织的。
示例:
module HalfAdd (X, Y, SUM, C_out);//半加器模块
input X;
input Y;
output SUM;
output C_out;
assign SUM = X ^ Y ;
assign C_out = X & Y ;
endmodule
module FullAdd (X, Y, C_in, SUM, C_out);//全加器模块
input X;
input Y;
input C_in;
output SUM;
output C_out;
wire HalfAdd_A_SUM;
wire HalfAdd_A_COUT;
wire HalfAdd_B_COUT;
assign C_out = HalfAdd_A_COUT | HalfAdd_B_COUT ;
HalfAdd u_HalfAdd_A ( //半加器实例A
.X (X),
.Y (Y),
.SUM (HalfAdd_A_SUM),
.C_out (HalfAdd_A_COUT) );
HalfAdd u_HalfAdd_B ( //半加器实例B
.X (C_in),
.Y (HalfAdd_A_SUM),
.SUM (SUM),
.C_out (HalfAdd_B_COUT) );
endmodule
延时
在连续赋值语句中,可以对电路延时进行建模,当然也可以没有。下面的连续赋值语句表示:该异或门的延时为1ns。
assign #1 A_xor_wire = eq0 ^ eq1; //`timescale 1ns / 100ps
实际上,电路对不同的信号跳变表现出的延时往往并不一致,这些延时模型包括:上升沿延时(输出变为1)、下降沿延时(输出变为0)、关闭延时(输出变为Z高阻态)、输出变为x的延时。
assign #(1,2) A_xor_wire = eq0 ^ eq1;
assign #(1,2,3) A_xor_wire = eq0 ^ eq1;
- 第一句话表示,上升延时1ns,下降延时2ns,关闭延时和传递到X的延时为两者中的最小值,即1ns;
- 第一句话表示,上升延时1ns,下降延时2ns,关闭延时为3ns,传递到X的延时为三者中的最小值,即1ns;
在一些电路模型中,延时分为最大、典型和最小三种情况。连续赋值语句中也可以采用min:type:max的格式来表示:
assign #(4:5:6,3:4:5) A_xor_wire = eq0 ^ eq1;
上升延时的min:type:max为4:5:6;下降延时的min:type:max为3:4:5。
- 在连续赋值语句中的延时具有硬件电路的惯性延时特性,任何小于其延时的信号变化脉冲将被滤除掉,不会体现在输出端口上。
多线网驱动源
1.多重驱动wire,错误
module WS(A,B,C,D,WireShort);
input A,B,C,D;
output WireShort;
wire WireShort;
assign WireShort = A^B;
assign WireShort = C&D;
endmodule;
由于WireShort为wire类型,同时它有多重驱动源,因此仿真时WireShort的值是X(不定态)。但对于综合工具,这个语法是错误的。
2.线与、线或功能
- 可以使用wor线网类型将不同的输出“线或”在一起。
module WO(A,B,C,D,WireOr);
input A,B,C,D;
output WireOr;
wor WireOr;
assign WireOr = A^B;
assign WireOr = C&D;
endmodule;
- 可以使用wand线网类型将不同的输出“线与”在一起。
module WA(A,B,C,D,WireAnd);
input A,B,C,D;
output WireAnd;
wor WireAnd;
assign WireAnd = A^B;
assign WireAnd = C&D;
endmodule;
3.三态总线功能
如果要实现多个三态总线相连,可以使用tri型线网
module WT(A,B,C,D,WireTri,En1_n,En2_n);
input A,B,C,D,En1_n,En2_n;
output WireTri;
tri WireTri;
assign WireTri = (En1_n) ? 1'bZ : (A^B);
assign WireTri = (En2_n) ? 1'bZ : (C&D);
endmodule;
行为描述
行为描述的语句格式
initial和always后面一般跟语句或者语句组,语句可以是:非阻塞过程赋值、阻塞过程赋值、连续过程赋值、高级编程语言。
1. initial 或者 always 过程块(procedual block)
initial语句在0仿真时间执行,而且只执行一次;always语句同样在0仿真时刻开始执行,但将一直执行下去。时钟发生器模型:
`timescale 1ns/1ns
modual ClockGen(Clock);
output Clock;
reg Clock;
initial
Clock = 0;
always
#5 Clock = ~Clock;
endmodule
2. 过程块中的语句类型
在initial和always过程块中可以直接跟语句或者语句组:
- 直接跟的语句可以是非阻塞过程赋值、阻塞过程赋值、连续过程赋值、高级编程语言;
- 语句组可以是:begin…end和fork…join两种。
语句组中可以有其他几种语句类型,而高级编程语句中也可以有语句组,可以互相嵌套,完成非常复杂的逻辑功能描述。 - 在always过程块中直接跟阻塞赋值语句:
always
#5 Clock = ~Clock;
下面的代码描述了语句组和高级编程语句的相互嵌套:
always @(posedge Clock or negedge Rst_n)
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)
在行为描述中,有几种方式对设计模型进行时序控制:
- 事件语句 @
- 延时语句 #
- 等待语句
在执行initial或者always语句块时遇到一个事件语句(@)、延时语句(#)或者表达式值为假(false)的等待语句时,语句块(或称之为进程)的执行将被挂起(suspended)。直到发生该事件,或者已经过了指定延迟的时间单位数,或者等待语句表达式变为真(ture)时,才重新执行initial或者always块。这个过程就是时序控制。
事件语句(@)的用法:
基本D触发器
module TYP_DFF(Clock, D, Q)
input Clock,D;
output Q;
reg Q;
always @(posedge Clock)
begin
Q <= D;
end
endmodlue
在0仿真时刻,always语句块开始执行,当遇到@(posedge Clock)语句时,进程被挂起,等待Clock上升沿到来,才重新激活该进程。由于always语句的特点,always语句开始重新执行,当遇到@(posedge Clock)语句时进程再次被挂起,等待下一次Clock上升沿。同样的,采用下面的代码也能得到一个基本D触发器。
module TYP_DFF(Clock, D, Q)
input Clock,D;
output Q;
reg Q;
always
begin
@(posedge Clock)
Q <= D;
end
endmodlue
当有多个条件语句时,一般将它们用"or"分隔开。异步复位D触发器代码:
module TYP_DFF(Clock, D, Q, Rst)
input Clock,D;
output Q;
reg Q;
always @(posedge Clock or posedge Rst) //Rst高电平有效
begin
if(Rst)
D <= 0;
else
Q <= D;
end
endmodlue
延时语句(#)
always //每个5ns将Clock翻转一次 #5 Clock = ~Clock;
always语句开始执行,马上遇到#5,always语句块被挂起,直到5ns以后才恢复执行,这时将Clock取反。这段语句可以模拟一个周期10ns的时钟。这种写法一般用于仿真激励的产生,仅仅用于仿真。
- 利用延时语句产生一个复位信号
initialbegin Rst_n = 1; #5 Rst_n = 0; #100 Rst_n = 1;end
等待语句(wait)
module MY_LATCH(Strobe,D,Q)
input Strobe,D;
output Q;
reg Q;
always
begin
wait(Strobe == 1)
Q = D;
end
endmodule
当always语句开始执行后,遇到wait()语句,如果括号内的变量不为之真,则进程被挂起,直到(Strobe == 1)为真,always才继续往下执行,将D的值赋值给Q,这样就模拟了一个电平敏感的锁存器。大多数综合工具还不支持wait语句,因此这个锁存器的功能只能在仿真时用,不能实现为具体的电路。
过程赋值语句
所谓的过程赋值语句就是在initial和always语句块中的赋值语句。赋值对象只能是寄存器变量类型。
1. 阻塞赋值 =
阻塞赋值有两层含义:
- 右边表达式的计算和对左边寄存器变量的赋值是一个统一的一个操作中的两个动作,这两个动作之间不能插入任何其他的动作;
- 如果多个阻塞赋值语句顺序揣着你在begin…end语句中,前面的语句在执行时,将完全阻塞后面的语句,直到前面的语句赋值完成后,才会执行下一句的右边表达式的计算;如"begin m=n;m=n;end"语句中,当m被完全赋值后,再开始执行"m=n",将m的新值赋给n,这样操作的结果就是,n的值保持不变,m与n相等。
由于阻塞赋值的这一特点,通常对组合逻辑进行建模时使用阻塞赋值,能够根据赋值的先后顺序对组合逻辑电路进行分级,确定门电路的先后顺序
wire A_in,B_in,C_in;
reg Temp,D_out;
...
always @(A_in or B_in or C_in)
begin
Temp = A_in & B_in;
D_out = Temp | C_in;
end
2. 非阻塞赋值 <=
- 非阻塞赋值的特点是:在执行该语句时,首先计算右边的表达式,然后并不立刻对左边的变量赋值。由于这个赋值操作在当前仿真时间事件队列的优先级比较低,因此将赋值推迟到当前仿真时刻后期运行。
- 与阻塞赋值不同,如果多个非阻塞赋值语句顺序出现在begin…end语句中,前面语句的执行,并不会阻塞后面语句的执行。前面语句计算完成,还没有赋值时,就会执行下一句的右边表达式计算;如"begin m<=n;m<=n;end"语句中,最后结果就是m和n值互换了。
3. 过程连续赋值
过程连续赋值主要有两种:
- assign与deassign:在过程语句块中对寄存器变量强制赋值和放开;
- force与release:在过程语句块对寄存器和线网进行强制赋值和放开。
一个带异步清零端的D触发器:
module DFF(D, Clr, Clk, Q)
input D, Clr, Clk;
output Q;
reg Q;
always @(Clr)
begin
if(!Clr)
assign Q = 0; //D的值对Q无效,将Q强制为0
else
deassign Q; //将强制的Q值放开
end
always @(posedge Clk)
Q = D;
endmodule
语句组
1. 顺序语句组begin…end
其中的语句是一条一条顺序执行的。
initial
begin
DataBin = 0;
#6 DataBin = 0;
#4 DataBin = 1;
#2 DataBin = 0;
end
2. 并行语句组fork…join
其中语句是并行执行的。
initial
fork
DataBin = 0;
#6 DataBin = 0;
#4 DataBin = 1;
#2 DataBin = 0;
join
3. 语句组的标识符
- 语句组可以有标识符,也可以没有。当一个语句组有标识符时,在语句组内部可以定义局部变量,而不会传递到语句组外部。然而,在仿真语义上,这个变量是静态变量,它的值在整个仿真运行周期中是不变的,但不会与其他语句组中同一个名称的变量发生冲突。
integer i;
always @(...)
begin: SORT
...
integer i;
for(i = 0; i <= 7; i = i+1)
...
end
在always以外的变量和always里面定义的i变量属于两个不同的变量,并不冲突。它们在仿真时将占用两个不同的内存。
高级编程语句
高级编程语句分为3大类:
- if…else语句;
- case语句;
- 循环语句:forever、repeat、while、for。
if…esle
always @(sel_a ro sel_b or a or b or c)
begin
if(sel_a)
q = a;
else if(sel_b)
q = b;
else
q = c;
end
- 从上而下逐条检查的,if…else是有优先级顺序的。
- 在使用if…else语句时,尤其是在组合逻辑中,注意不要引入Latch电路(锁存器)。
不完整分支和不完整输出赋值(组合逻辑电路中): - 为了防止always块中意外的存储器,所有输出信号在任何时候都应该赋恰当的值,否则会保持之前的值,从而综合出锁存器。
在组合逻辑中,锁存器危害: - 锁存器是电平触发的存储器,触发器是边沿触发的存储器。所以锁存器对毛刺敏感,不能异步复位,所以上电以后处于不确定的状态;
- 锁存器容易引起竞争冒险;
- Latch会使静态时序分析变得非常复杂,不利于时序路径的分析;
- 在PLD芯片中,基本的单元是由查找表和触发器组成的,若生成锁存器反而需要更多的资源。
解决方法:
- 加上else/default分支并明确所有输出变量值;
- 在always块起始部分,给每个变量赋默认值,以包含所有未指定的分支和未赋值的变量。
如果加一个时钟变为时序电路,即便语句不完整,产生了锁存器(其实在时序电路中,即便语句不完整,也不会产生锁存器,只是具备锁存功能),那么也不会对毛刺敏感。
case
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
default包含了sel为2’b11、2’bzz、2’bxx等情况。
...
casez(encoder) //casez将分支中所有的z看作“不关心”的值
4'b1??? : q = 3; //z可以改写为?
4'b01?? : q = 2;
4'b001? : q = 1;
4'b0001 : q = 0;
default : q = 0;
endcase
...
...
casex(encoder) //casex将分支中所有的x和z看作“不关心”的值
4'b1xxx : q = 3; //z可以改写为?
4'b01xx : q = 2;
4'b001x : q = 1;
4'b0001 : q = 0;
default : q = 0;
endcase
...
循环语句
forever循环:永久执行
initial
begin
clk = 0;
forever #25 clk = ~clk; //产生一个50个时间单位的时钟
end
repeat循环:执行固定次数的
if(rotate == 1)
repeat(8)
begin
tmp = data[15];
data ={data << 1, tmp}; //rotate=1时,对data做8次循环左移
end
while循环:当表达式为真时执行
initial
begin
count = 0;
while(count < 101)
begin
$display("Count = %d",count);
count = count + 1;
end
end
for循环:从初值开始,如果表达式真就执行
integer i;
always @(inp or cnt)
begin
result[7:4] = 0;
result[3:0] = inp;
if(cnt == 1)
begin
for(i = 4; i <= 7; i = i+1) //从i=4执行到大于7
begin
result[i] = result[i-4]; //4位左移器
end
result[3:0] = 0;
end
end
结构化描述
结构化描述的3种实例类型:
- 实例化其他模块
- 实例化门
- 实例化UDP(用户自定义原语)
module HalfAdd (X, Y, SUM, C_out);//半加器模块
input X;
input Y;
output SUM;
output C_out;
//assign SUM = X ^ Y ;
xor u_xor (SUM, X, Y); //门级原语实例
//assign C_out = X & Y ;
and u_and (C_out, X, Y); //门级原语实例
endmodule
module FullAdd (X, Y, C_in, SUM, C_out);//全加器模块
input X;
input Y;
input C_in;
output SUM;
output C_out;
wire HalfAdd_A_SUM;
wire HalfAdd_A_COUT;
wire HalfAdd_B_COUT;
//assign C_out = HalfAdd_A_COUT | HalfAdd_B_COUT ;
or u_or (C_out, HalfAdd_A_COUT, HalfAdd_B_COUT);// 门级原语实例
HalfAdd u_HalfAdd_A ( //半加器实例A
.X (X),
.Y (Y),
.SUM (HalfAdd_A_SUM),
.C_out (HalfAdd_A_COUT) );
HalfAdd u_HalfAdd_B ( //半加器实例B
.X (C_in),
.Y (HalfAdd_A_SUM),
.SUM (SUM),
.C_out (HalfAdd_B_COUT) );
endmodule
实例化模块的方法
在结构化描述中,需要将模块实例与外部信号相连接。先看一个模块内部输入/输出/双向端口的内部属性:
-
input :在模块内部默认是一个线网类型;
-
output :在模块内部是一个寄存器(在过程赋值语句中被赋值)或者线网类型;
-
inout :在模块内部默认是一个线网类型,是双向信号,一般定义为tri。
当模块被例化时,与之相连的信号类型如下: -
与模块input端口相连:可以是一个线网或者寄存器;
-
与模块output端口相连:一定是驱动一个线网;
-
与模块inout端口相连:输入时从一个线网驱动来,输出驱动到一个线网。只有线网类型可以驱动inout端口。
模块实例化对应两种方式有两种:
-
名称对应:端口对应顺序可以任意,在没有对应外部信号的时候,可以将端口后面的括号留空。
HalfAdd u_HalfAdd_A ( //半加器实例A
.X (X),
.Y (Y),
.SUM (HalfAdd_A_SUM),
.C_out (HalfAdd_A_COUT) );
HalfAdd u_HalfAdd_B ( //半加器实例B
.X (C_in),
.Y (HalfAdd_A_SUM),
.SUM (SUM),
.C_out () );
- 位置对应:在模块实例化时外部信号需要按照该模块端口声明的顺序一一对应,在没有对应外部信号的时候,可以将端口位置留空。
HalfAdd u_HalfAdd_A (X, Y, HafAdd_A_SUM, HalfAdd_A_COUT);
HalfAdd u_HalfAdd_B (C_in, HalfAdd_A_SUM, SUM, );
参数化模块
参数定义
module中的参数一般是定义其中常量的工具,如:
module half_adder(a,b,co,sum);
input a,b;
output co,sum;
parameter and_delay = 2;
parameter xor_delay = 4;
and #and_delay u1(co,a,b);
xor #xor_delay u2(sum,a,b);
endmodule
在Verilog中,当实例化模块时用户可以修改模块中的参数,用来实现不同的特性。这个定制过程是通过“新参数直接带入”或“参数重定义”完成的。这一特性非常有用,用户可以定义一个通用的模块,其具有缺省的参数值,然后通过改变参数来做成不同的实例模块。例如,可以设计一个通用RAM模块,其位宽和地址深度定义为参数,在具体使用时,如果需要用到不同的位宽和深度,可以通过改变模块中的参数实现。
参数的定制
参数的用户定制有两种方法:
- 通过defparam关键字对模块参数重新定义(ALtera);
- 参数直接在实例化模块时代入(Xilinx)。
设计层次
系统级和行为级
- 系统架构师:通常用高级语言,如SystemC,来描述一个系统的规格,仿真整个系统的功能和性能等。往往不涉及到具体的实现细节,这种设计层次称为系统级或者算法级;
- 逻辑设计工程师:利用Verilog和各种描述手段,设计RTL的代码,精确到时钟周期。代码通过工具综合,可以转换为Verilog的门级网表,其中所有的功能块都由基本的门电路组成;