介绍
verilog有两种建模方法:行为建模和结构建模。
结构建模作为一种比较“底层”的方法,是对电路具体结构的描述;而行为建模的描述非常像C语言,是对电路功能的描述,不涉及具体结构。
3.1 数据流行为建模
数据流模型是一种比较简单的行为模型,它只有一种形式,即使用关键词“assign”引导的一条连续赋值语句,其赋值目标只能是线网,而且这种赋值行为没有任何附加的判断条件。通常,描述组合逻辑电路的行为最好使用连续赋值语句建模。
3.1.1 连续赋值语句
连续赋值语句用于把值赋给线网,关键词是assign,格式如下:
assign LHS_target = RHS_expression;
其中LHS_target是目标线网,RHS_expression是赋值操作表达式。
wire[3:0]z, preset, clear;
assign z=preset & clear;
连续赋值语句的执行过程是这样的:只要等号右边的操作数上有事件发生时,右端表达式即被计算,如果结果有变化,新结果就赋给等号左端的线网。
连续赋值语句的赋值目标可以是:
·标量线网
·向量线网
·向量的常数型位选择;
·向量的常数型部分选择;
·上述类型的任意连接结果(使用连接操作符)
连续赋值语句的执行顺序与其书写顺序无关,只要等号右端的操作数的值有变化就执行。可以认为在连续赋值语句建模的过程中,等号右端操作数上的数据是始终处于被监控状态的,一旦这些数据发生变化就会引起赋值语句的执行。这也正是连续赋值语句建模被称为数据流行为建模的原因所在。
3.1.2 线网声明赋值
对线网的赋值并非必须要用到assign。可以在声明线网时就对其赋值。例如:
wire[3:0] sum = 4'b0;
这种赋值方式与assign的效果完全相同。
3.1.3 时延的概念
时延就是给出一个值,程序执行到此就会暂时停下来,等待这个值规定的若干个单位时间,然后再继续执行后面的语句。时延可以单独作为依据,也可以内嵌在连续赋值语句中,跟在assign之后。默认时延是0
连续赋值语句中的时延可以指定3类时延值:上升时延、下降时延和关闭时延(值变为x)。其语法格式如下: assign #(rise, fall, turn-off) LHS_target = RHS_expression;
3.1.4 线网时延
时延可以放在线网声明中。如果时延在线网声明赋值中出现,那么这个时延不是线网时延,而是赋值时延。通常,线网目标得到这所赋值需要经过这两个时延的和。
3.1.5 用数据流建模方式实现一位全加器
3.2 顺序行为建模
3.2.1 过程结构语句
在verilog顺序行为描述中,有两个必然出现的结构语句:initial语句和always语句。initial和always都不仅仅是单独的一条语句,它们引导一个“过程”或者说一个“结构”,跟在initial和always之后的都是一段程序,所有其他顺序行为语句都必须包含在这段程序中。可以说,initial和always的出现是顺序行为描述的开始。
作为顺序行为建模的两条基本语句,initial和always存在一些共同的特点:
·所有的initial和always语句都是在时刻0(仿真刚开始)开始执行;
·initial和always之后都跟随着一段程序,这段程序会被封装成一个“程序块”,可以用begin-end(顺序语句块)封装,也可以用fork-join(并行语句块)封装。
·一个模块中可以包含任意多个Initial或always语句,这些initial和always语句彼此之间都是并行执行的,即这些语句的执行顺序与其在模块中的书写顺序无关。
(1) initial语句
initial语句只执行一次,并且在仿真开始时(时刻0)就执行。如果initial之后要跟多条过程语句,就要把这些语句封装为“程序块”。顺序语句块(begin-end)是最常使用的封装结构,在begin和end之间的过程语句是按书写顺序依次执行的。下面是一个带有顺序语句块的initial语句程序片段。
parameter SIZE = 1024;
reg[7:0] RAM [0:SIZE-1];
reg RibReg;
initial
begin: SEQ_BLK_A
integer Index;
RibReg = 0;
for (Index = 0; Index < SIZE; Index = Index+1)
RAM[Index] = 0;
end
initial语句的作用是在执行时将存储器RAM的所有存储单元初始化为0。程序中begin之后的标识符“SEQ_BLK_A”是顺序语句块的名称,并不是所有的语句块都要求有名称。在这段过程语句中因为出现了局部变量Index,所以要求这个程序块必须有名称标记。
(2)always语句
always语句和initial语句一样,也从时刻0开始执行。不同的是,initial语句在整个仿真过程中只执行一次,而在always语句中,只要满足其规定的条件(时序控制中定义的条件),always语句就执行,如果在整个访问过程中多次满足这个条件,always就会被多次执行。其语法格式:
always
[timing_control] procedural_statement
其中,timing_control是时序控制,形式可以是时延控制(即等待一个确定的时间),或事件控制(即等待某一时间发生或某一条件为真);procedural_statement是过程语句。always中的时序控制可以是上例中的时延控制,也可以是事件控制。事件控制就是等待某一事件发生,该事件的发生将执行always语句。用符号“@”定义了事件控制的always语句。事件控制还有一种是“电平敏感事件”,使用关键词“wait”定义。
一个模块可以既包含initial语句也包含always语句,而且可以包含多条initial语句和always语句,所有initial语句和always语句都是在时刻0开始执行。
3.2.2 时序控制
时序控制有两种形式:时延控制和事件控制。
(1)时延控制
时延控制可以以三种不同的形式出现:
·语句前时延 ·单独时延 · 语句内时延
我们来看一下语句内时延,对于Done = #5 A&B;程序执行至此时,首先计算A&B的值,然后进入时延,经过5个单位时间后把计算出来的结果赋给Done。来看一个例子:
Done = #5 'b1;
与程序段:
begin
tmp = 'b1;
#5 Done = tmp;
end
效果是一样的。
如果时延表达式的值是0,则称之为显示零时延,它和不定义时延是完全不同的。后者默认时延是0,程序继续执行不受任何影响,而显式零时延将使整个程序的运行在当前仿真时间暂停,使其处于一种“等待”状态,等待所有其他正在被执行的事件全都执行完毕后,才将整个进程唤醒继续执行,处于等待状态时,仿真时间不前进。
(2)事件控制
事件控制就是把发生某个事件作为执行某个操作的条件。事件控制方式有两种:边沿触发事件和电平敏感事件。
·边沿触发事件
使用符号@定义,形式如下:
@ event procedural_statement
其中“@”是边沿事件使用的符号,event是边沿事件,procedural_statement是过程语句。
和时延控制类似,事件控制也可以独立成为一条语句。
@ event;
运行到这条语句时,整个程序在此被暂停,进入等待状态,一直等到这条语句中指定的事件发生后,才能继续执行后面的程序。
在事件控制的定义中,event可以是多个事件,这些事件之间只需要用关键字“or”分隔开即可。定义了多个事件后,只要其中的一个事件发生,就可以执行其后的程序。注意:在verilog中,上升沿不仅仅是从0到1这一种情况,下降沿也不仅仅是从1到0这种情况。
·电平敏感事件
电平敏感事件使用关键词wait,形式如下:
wait (condition)
procedural_statement
其中,condition就是电平敏感事件,prcedural_statement是过程语句。执行到wait时,检查condition定义的条件是否满足,满足就接着执行随后的过程语句;否则进入等待状态,知道条件满足为止。
3.2.3 语句块
(1)顺序语句块
(2)并行语句块
并行语句块是用fork-join封装的一段程序,和begin-end不同的是,并行语句块中的语句彼此之间都是并行执行的,和书写顺序无关。
3.2.4 过程性赋值
过程性赋值是最常见的赋值形式:等号左侧是赋值目标,右侧是表达式。它有以下几个特点:
· 过程性赋值只出现在initial语句和always语句内;
· 过程性赋值只能给寄存器变量赋值;
·赋值表达式的右端可以是任何表达式。
事实上,Verilog有两种过程性赋值:阻塞过程赋值和非阻塞过程赋值。之前程序中出现的都是阻塞过程赋值,其标志是使用赋值符号“=”,而非阻塞过程赋值使用赋值符号“<=”,二者在功能和特点上有很大区别。
(1)阻塞过程赋值
“阻塞”是从阻塞过程赋值的工作过程中得来的。它先计算右侧表达式的值,然后赋值给等号左端目标,而且在完成整个赋值之前不能被其它语句打断。也就是说在某一条阻塞过程赋值语句正在执行时,处于其后的其它赋值语句都不能执行。
(2)非阻塞过程赋值
非阻塞过程赋值操作符是“<=”,非阻塞过程赋值只能用于给寄存器赋值。之所以称为“非阻塞”,是因为通常所说的赋值过程都包括两个子过程:
·过程1,计算右侧表达式的值;
·过程2,给左侧目标赋值。
对阻塞过程赋值而言,这两个子过程可以视为连续完成的,而且在完成赋值之前不允许其后的其它语句执行。而对于非阻塞赋值,所谓“在某个时刻完成赋值”,其实是在这个时刻开始执行子过程1,在这个时刻结束时执行子过程2,这两个子过程之间有一个微小的事件间隔。在这个间隔期间,这条非阻塞赋值语句后面的其它的语句也可以执行。
(3)阻塞赋值与非阻塞赋值的混合使用
(4)过程性连续赋值
连续赋值适用于线网,过程赋值适用于寄存器。但是还有一类赋值方式,它既能对线网赋值也能对寄存器赋值(但不能是寄存器的位选择或部分选择),这种赋值方式被称为过程性连续赋值。它属于过程性赋值而非连续赋值,所以它能出现在always和initial语句中(连续赋值语句不能出现在always和initial内)。但是它又有连续赋值的一些特性,就是在过程性连续赋值语句中,右端表达式中操作数的任何变化都会引起赋值语句的重新执行。
过程性赋值语句有两种类型:assign-deassign(赋值-重新赋值),force-release(强制-释放虽然它也可以用于对寄存器赋值,但主要用于对线网赋值)。
·assign-deassign
assign用于对寄存器赋值,deassign用于取消之前由assign赋给某寄存器的值。也就是说,使用assign为寄存器赋值后,这个值将一直保存在寄存器上,直到遇到deassign为止。看下例:
module DEF(D, Clr, Clk, Q);
input D, Clr, Clk;
output Q;
reg Q;
always @(negedge Clk)
Q = D;
always @(Clr)
begin
if(!Clr)
assign Q = 0;
else
deassign Q;
end
endmodule
上面这段程序中,第一个always会在Clk的下降沿把D赋给Q,但是第二个always内的过程性连续赋值语句会使第一个always的赋值失效。在第二个always中,如果clr为0,assign赋值使Q被赋值为0,然后Q就始终为0,而不管Clk如何变化,即Clk和D对Q无效;等到Clr变为1时,deassign被执行,对Q的过程性连续赋值被取消。之后Clk才能对Q产生影响。换句话说,在Clr为0是,Q一直被assign所霸占,只有Clr为1时,才会把Q释放掉,从而才会受到D的影响。
·force-release
当force语句应用于寄存器时,寄存器的当前值被force语句的值覆盖;当release语句应用于寄存器时,寄存器中的当前值将保持不变,直到被赋予新值。来看两段代码:
reg[2:0] Colt;
Colt = 2;
force Colt = 1; //Colt被强制赋值为1
release Colt //对Colt的强制赋值被取消,从现在起Colt将保持值为1
assign Colt = 5;
force Colt = 3; //被强制赋值为3
release Colt; //对Colt的强制赋值被取消,assign Colt = 5重新生效,使得Colt的值变为5
用force语句对线网赋值时,将为线网替换所有驱动源,直到那个线网上执行release语句为止。
wire prt;
or #1 (prt, Std, Dzx); //定义一个或门,线网prt的驱动源由或门的结构给出
initial
begin
force prt = Dzx & Srd;
#5;
release prt;
end
上面的程序中,执行force语句将替换ptr原来的驱动源,ptr将一直保持这些驱动源,直到release把对prt的强制赋值取消之后,或门的prt驱动源才重新生效。
(5)连续赋值与过程赋值
3.2.5 if语句
对于if语句,如果某个分支内的过程语句多于一条,就应该把它们放在begin-end块内。这里的begin-end相当于C中的{}。
3.2.6 case语句
注意:在比较条件表达式和分支表达式的值时,如果值的某些位出现了x或z,那么这些x和z和0、1一样有意义,所以只要在两个值的相同位置出现x或z,就认为这两个值相同。
·casex和casez
在casez语句中,出现在条件表达式和任意分支项表达式中的值z被认为是无关值,出现z的那个位在比较时被忽略。在casex语句中,值x和z都被认为是无关位。所谓无关位即在比较时可以忽略的位,只要其它的位相匹配即可。
3.2.7 循环语句
(1)forever是个永远循环执行的语句,不需要声明任何变量,其语法格式如下:
forever
procedural_statement
其中,procedural_statement是过程语句,若过程语句多于一条,则应该放在begin-end块内。注意:forever语句中必须有某种形式的时序控制,否则forever会在0时延后永远重复执行过程语句。forever的最主要用途是产生周期性的波形作为仿真测试信号。
(2)repeat循环语句
repeat循环是个重复执行若干次数的语句,带有一个控制循环次数的常量或者变量,其语法形式如下:
repeat(loop_count)
procedural_statement
其中loop_count是控制循环次数的常数或变量;如果控制循环次数的常数或变量的值不确定(为x或z)时,那么循环次数按0处理。
Sum = repeat(Count) @ (posedge Clk) Sum+1; //首先计算出Sum+1的值,然后等待Clk上升沿,最后为左端赋值
repeat(4) @(negedge Clockz); //等待Clockz上出现4次下降沿之后,才能继续执行repeat之后的语句
(3)while循环语句
(4)for循环语句
一些代码例子
Verilog是一种硬件描述语言(HDL),用于模拟电子系统的行为、结构和数据流。在Verilog中,行为建模通常指的是使用过程性(procedural)语句来描述硬件的行为。这些过程性语句包括always
、initial
、assign
等块,以及控制流语句(如if
、else
、case
、for
、while
等)。
以下是一些Verilog行为建模的示例:
1. 使用always
块描述时钟驱动的行为
verilog复制代码 module clocked_behavior( input wire clk, input wire reset, output reg out ); always @(posedge clk or posedge reset) begin if (reset) begin // 如果reset为高,重置out为0 out <= 0; end else begin // 否则,每次时钟上升沿,out的值取反 out <= ~out; end end endmodule
2. 使用initial
块进行初始化或仿真控制
verilog复制代码 module initial_behavior; reg [7:0] counter; initial begin // 初始化counter为0 counter = 0; // 每隔10个时间单位,增加counter的值 #10 counter = counter + 1; #10 counter = counter + 1; // ... 可以继续添加更多步骤 // 停止仿真(这通常在测试平台中使用) $finish; end endmodule
3. 使用assign
语句进行连续赋值
verilog复制代码 module continuous_assignment( input wire a, input wire b, output reg y ); // 使用assign进行连续赋值(注意:这里通常使用wire而不是reg) assign y = a & b; // y是a和b的与操作结果 // 但为了示例,我们仍然使用reg,但通常这不是最佳实践 // 在实际硬件中,连续赋值应使用wire endmodule
4. 使用控制流语句(如if-else
)
verilog复制代码 module if_else_behavior( input wire [1:0] selector, input wire [7:0] a, b, c, output reg [7:0] out ); always @(*) begin // 使用@(*)表示这是一个组合逻辑块 case (selector) 2'b00: out = a; 2'b01: out = b; 2'b10: out = c; default: out = 8'hFF; // 默认为全1(即255) endcase end endmodule
以上示例展示了Verilog中行为建模的基本概念和用法。在实际的硬件设计和仿真中,您可能会根据具体需求使用这些概念的组合。