前言
在数据流级描述中已经将硬件建模从比较底层的门级结构提升到了数据流级。但数据流级描述除了个别语句外,主要的部分还是使用操作符来描述电路的逻辑操作或者计算公式,没有实现真正意义上的功能描述。行为级描述则可以实现从抽象层次更高的级别来描述功能电路。
initial与always语句
在Verilog 中有两种结构化过程语句:initial语句和always语句,它们是行为级建模的两种语句
。其他所有的行为语句只能出现在这两种结构化过程语句里。
Verilog中各个执行流程并发执行,而不是顺序执行的
,每个initial语句和 always语句代表一个独立的执行过程,每个执行过程从仿真时间0开始执行,并且这两种语句不能嵌套使用。
initial语句
所有在initial语句内的语句构成了一个initial 块。
initial块从仿真0时刻开始执行,在整个仿真过程中只执行一次
。
如果一个模块中包括若干个initial 块,则这些initial块从仿真0时刻开始并发执行,且每个块的执行是各自独立的。
如果在块内包含多条行为语句,那么需要将这些语句组成一组
一般是使用关键字
begin
和end
将它们组合为一个块语句;
如果块内只有一条语句,则不必使用begin和 end。
举例:
module stimulus;
reg x,y, a,b,m;
initial
m=1'b0;
initial
begin
#5 a=1'b1;
#25 y=1'b1;
end
initial
begin
#10 x=1'b0;
#25 y=1'b1;
end
initial
#50 $finish;
endmodule
三条initial语句在仿真0时刻开始并行执行。
如果在某一条语句前面存在延迟#
always 语句
always语句包括的所有行为语句构成了一个always语句块。
该always语句块从仿真0时刻开始顺序执行其中的行为语句
;
在最后一条执行完成之后,再次开始执行其中的第一条语句,如此循环往复,直至整个仿真结束。
因此 always语句通常用于对数字电路中一组反复执行的活动进行建模。
例子:用always语句为时钟发生器建立模型的一种方法
module clock_gen(output reg clock);
initial
clock = 1'b0;
always
#10 clock=~clock;
initial
#1000 $finish;
endmodule
在这个例子中,clock信号是在initial语句中进行初始化的,如果将初始化放在always块内,那么always语句的每次执行都会导致clock被初始化。如果没有\(stop或\)finish语句停止仿真,那么这个时钟发生器将一直工作下去。
控制方式
基于事件的控制在实际建模中使用最多,也是行为级建模的一个重要控制方式,其控制方式为“@”
引导的事件列表,也称为敏感列表
使用方法为:
always @(敏感事件列表);
敏感事件列表是由设计者来指定的。在模块中,任何信号的变化都可以称为事件,一旦这些事件发生了,always 结构中的语句就会被执行。
换言之,always结构时刻观察敏感事件列表中的信号,等待敏感事件出现,然后执行本结构中的语句。如果敏感时间有多个,可以使用or或“,”来隔开,这些事件是或的关系,只需要满足一个就会触发并执行 always语句结构。
基于事件控制的always语句
always @(a,b)
begin
e=c&d;
f=c|d;
end
这段代码中都是以信号的名称
作为敏感事件,表示的是对信号的电平敏感
,即信号只要发生了变化,就要执行always结构,这个变化指的是仿真器可以识别的任意变化,例如,从0变到1或从1变到0。
使用这种控制方式可以设计对电平信号敏感的电路,所有的组合逻辑电路
采用的都是这种控制方式。
敏感事件列表中的事件可以用“”代替,这个“”表示的是该always结构中所有的输入信号。
时序电路采用的敏感列表一般是边沿敏感的,信号的边沿用posedge(上升沿)和 negedge(下降沿)来表示,但边沿敏感不能使用“*”来省略。
顺序块和并行块
块语句包含两种类型,顺序块和并行块
顺序块
关键字begin和end
用于将多条语句组成顺序块。顺序块具有以下特点。
- (1)顺序块中的语句是一条接一条按顺序执行的;只有前面的语句执行完成之后才能执行后面的语句(带有内嵌延迟控制的非阻塞赋值语句除外)。
- (2)如果语句
包括延迟或事件控制
,那么延迟总是相对于
前面那条语句执行完成
的仿真时间的。
//eg1:
reg x,y;
reg [1:0] z,w;
initial
begin
x=1'b0;
y=1'b1;
z={x,y};
w={y,x};
end
//eg2:
reg x,y;
reg [1:0] z,w;
initial
begin
x=1'b0; //在仿真时刻0完成
#5 y=1'b1; //在仿真时刻5完成
#10 z={x,y}; //在仿真时刻15完成
#20 w={y,x}; //在仿真时刻35完成
end
在eg1中在仿真0时刻,x,y,z和 w的最终值分别为0,1,l和2。
在eg2中,这4个变量的最终值也是0,1,1和2。但是块语句完成时的仿真时刻为35。
并行块
并行块由关键字fork和 join
声明。并行块具有以下特性。
-
(1)并行块内的语句并发执行。
-
(2)语句执行的顺序是由各自语句中的延迟或事件控制决定的。
-
(3)语句中的延迟或事件控制是
相对于块语句开始执行的时刻
而言的。
注意,顺序块和并行块之间的根本区别
在于:
并行块中所有的语句同时开始执行,语句之间的先后顺序是无关紧要的。
将程序3.14中带有延迟的顺序块转换为一个并行块,
reg x,y;
reg [1:0] z,w;
initial
fork
x=1'b0; //在仿真时刻0完成
#5 y=1'b1; //在仿真时刻5完成
#10 z={x,y}; //在仿真时刻10完成
#20 w={y,x}; //在仿真时刻20完成
join
除了所有语句在仿真0时刻开始执行以外,仿真结果是完全相同的。
这个并行块执行结束的时间是仿真时刻20,而不是仿真时刻35。
并行块提供了并行执行语句的机制
可以将并行块的关键字fork看成是将一个执行流分成多个独立的执行流,而关键字join则是将多个独立的执行流合并为一个执行流。
每个独立的执行流之间是并发执行的。在使用并行块时需要注意,如果两条语句在同一时刻对同一个变量产生影响,那么将会引起隐含的竞争,这种情况是需要避免的。
块语句的特点
块语句具有三个特点:嵌套块、命名块和命名块的禁用
嵌套块
顺序块和并行块可以嵌套使用,使用时要注意每一个嵌套块开始的时间
举例:
initial
begin
x=1'b0;
fork
#5 y=1'b1;
#10 z={x,y};
join
#20 w={y,x};
end
命名块
块可以具有自己的名字,称为命名块。
命名块具有如下特点:
- 命名块中可以声明局部变量。
- 命名块是设计层次的一部分,命名块中声明的变量可以通过层次名引用进行访问。
- 命名块可以被禁用,例如停止其执行。
命名块和命名块的层次名引用:
module top;
initial
begin b1 //名字为b1的顺序命名块
integer i; //整型变量i是block1命名块的静态局部变量
…… //可以通过top.b1.i被其他模块所引用
end
initial
fork:b2 //名字为b2的并行命名块
reg i; //寄存器变量i是b2命名块的静态局部变量
…… //可以通过层次名,top.b2.i被其他模块引用
join
命名块的禁用:Verilog通过关键字disable
提供了一种终止命名块执行的方法。
disable可以用来从循环中退出,处理错误条件以及根据控制信号来控制某些代码段是否被执行。
对块语句的禁用导致紧接在块后面的那条语句被执行。
对于C程序员来说,这一点非常类似于使用break退出循环。
两者的区别在于 break 只能退出当前所在的循环,而使用disable则可以禁用设计中的任意一个命名块
举例:
//从标志寄存器的最低有效位开始查找第一个值为1的位
reg [15:0] flag;
integer i;
initial
begin
flag=16'b0010_0000_0000_0000;
i=0; //用于计数的整数
begin:b1
while(i<16) //while循环声明中的主模块是命名块b1
begin
if(flag[i])
begin
$display(Encountered a TRUE bit at element number % d",i);
disable b1; //在标志寄存器中找到了值为真(1)的位,禁用b1
end
i=i+1;
end
end
end
选择分支语句
if语句
条件语句用于根据某个条件来确定是否执行其后的语句,关键字if和 else
用于表示条件语句。
Verilog语言共有三种类型的条件语句:
//第一类条件语句:没有else 语句
if(!lock)buffer = data;
if(enable)out = in;
//第二类条件语句:有一个else语句
if(number_queued<MAX_Q_DEPTH)
begin
data_queue = data;
number_queued = number_queued + 1;
end
else
$display("Queue Full.Try again");
//第三类语句:嵌套的if-else-if语句
if(alu_control == 0)
y=x+z;
else if(alu_control == 1)
y=x-z;
else if(alu_control == 2)
y=x*z;
else
$display( " Invalid ALU control signal " );
if条件成立或不成立时,执行的语句可以是一条语句,也可以是一组语句。
如果是一组语句,则通常使用begin和 end关键字将它们组成一个块语句。
case语句
case语句使用关键字case、default和endcase来表示。
举例:
case(expression)
alternative1:statement1;
alternative2:statement2;
alternative3:statement3;
……
default:default_statement;
endcase
case语句中的每一条分支语句都可以是一条语句或一组语句,多条语句需要使用关键字begin和 end
组合为一个块语句。
在执行时,首先计算条件表达式的值,然后按顺序将它和各个候选项进行比较。如果等于第一个候选项,则执行对应的语句statementl;如果和全部候选项都不相等,则执行default_statement语句。
注意, default_statement 语句是可选的,而且在一条case语句中不允许有多条default_statement。
另外, case语句可以嵌套使用。
举例:
reg [1:0]alu_control;
……
case(alu_control)
2'd0:y=x+z;
2'd1:y=x-z;
2'd2=y=x*z;
default:$display( "Invalid ALU control signal");
endcase
case语句逐位比较表达式的值和候选项的值,每一位的值可能是0,l,x或z。
如果两者的位宽不相等,则使用0填补空缺位来使两者的位宽相等。
若选择信号中有不确定值x,则输出为x;若选择信号中没有不确定值x,但有高阻值z,则输出为z。
带x,z的case语句举例
除了上面讲述的case语句之外,case语句还有两个变形,分别使用关键字casex和casez来表示。
- casex语句将条件表达式或候选项表达式中的x作为无关值。
- casez语句将条件表达式或候选项表达式中的z作为无关值,所有值为z的位也可以用“?”来代表;
- casex和 casez的使用可以让我们在case表达式中只对非x或非z的位置进行比较。
reg [3:0] encoding;
integer state;
casex(encoding)
4'b1xxx:next_state = 3;
4'bx1xx:next_state = 2;
4'bxx1x:next_state = 1;
4'bxxx1:next_state = 0;
default:next_state = 0;
endcase
casez的使用与casex的使用类似
循环语句
Verilog语言中有4种类型的循环语句:while,for,repeat和forever。这些循环语句的语法与C语言中的循环语句类似。循环语句只能在always或initial 块中使用
,循环语句可以包含延迟表达式。
while语句
while循环使用关键字 while
来表示。
while循环执行的中止条件是while表达式的值为假。
如果遇到while语句时while表达式的值已经为假,那么循环语句一次也不执行。
如果循环中有多条语句,则必须将它们组合成为begin和end块。
举例:
for语句
for循环使用关键字for来表示,它由以下三个部分组成。
- 初始条件
- 检查终止条件是否为真
- 改变控制变量的过程赋值语句
用while循环语句描述的计数器也可以用for循环语句来描述
由于初始条件和完成自加操作的过程赋值语句都包括在for循环中,无须另外说明,因此for循环的写法较while循环更为紧凑。
但是要注意 while循环比 for循环更为通用,并不是在所有情况下都能使用for循环来代替while循环。
for循环一般用于具有固定开始和结束条件的循环。
如果只有一个执行循环的条件,最好还是使用while循环。
repeat语句
用关键字repeat
来表示。
repeat循环的功能是执行固定次数的循环
,它不能像while循环那样根据一个逻辑表达式来确定循环是否继续进行。
repeat循环的次数必须是一个常量、一个变量或者一个信号。
如果循环重复次数是变量或者信号,循环次数是循环开始执行时变量或者信号的值,而不是循环执行期间的值
。
上述程序给出了如何使用repeat循环对数据缓冲区建模,这个数据缓冲区的功能是在收到开始信号之后第8个时钟上升沿处锁存输入数据。
forever循环
关键字forever
用来表示永久循环。
在永久循环中不包含任何条件表达式,只执行无限的循环,直到遇到系统任务$finish为止。
forever循环等价于条件表达式永远为真的 while循环,例如 while(1)。
如果需要从forever循环中退出,可以使用disable语句
。
通常情况下, forever循环是和时序控制结构结合使用的
:如果没有时序控制结构,那么仿真器将无限次地执行这条语句,并且仿真时间不再向前推进,使得其余部分的代码无法执行。
//时钟发生器
reg clock;
initial
begin
clock=1'b0;
forever #10 clock = ~clock; //时钟周期为20个单位时间
end
//在每个时钟正跳变沿处使两个寄存器的值一值
reg clock;
reg x,y;
initial
forever @(posedge clock) x = y;
过程赋值语句
过程赋值语句的更新对象是寄存器、整数、实数或时间变量。
这些类型的变量在被赋值后,其值将保持不变,直到被其他过程赋值语句赋予新值。
这与连续赋值语句是不同的
连续赋值语句总是处于活动状态,任意一个操作数的变化都会导致表达式的重新计算以及重新赋值,
但过程赋值语句只有在执行到的时候才会起作用。
Verilog包括两种类型的过程赋值语句:阻塞赋值语句和非阻塞赋值语句。
阻塞赋值语句
顺序块语句
中的阻塞赋值语句按顺序执行,它不会阻塞其后并行块中语句的执行
。
阻塞赋值语句使用“=”
作为赋值符。由于阻塞赋值语句是按顺序执行的,因此如果在一个begin-end块中使用了阻塞赋值语句,那么这个块语句表现的是串行行为。
例子:
只有在语句x=0执行完成之后,才会执行y = 1,而语句count = count+ 1按顺序在最后执行。
begin-end块中各条语句执行的仿真时间如下。
- x=0 到 reg_b = reg_a 之间的语句在仿真0时刻执行;
- 语句reg_a[2] = 0在仿真时刻15进行
- 语句reg_b[15:13]={x,y,z}在仿真时刻25执行;
- 语句count=count+1在仿真时刻25执行。
注意,在对寄存器类型变量进行过程赋值时,如果赋值符两侧的位宽不相等,则采用以下原则。
- (1)如果右侧表达式的位宽较宽,则将保留从最低位开始的右侧值,把超过左侧位宽的高位丢弃;
- (2)如果左侧位宽大于右侧位宽,则不足的高位补0。
阻塞赋值(=)
always @ (<event-expression>)
begin
<LHS1 = RHS1 assignments > //阻塞赋值语句1
<LHS2 = RHS2 assignments > //阻塞赋值语句2
...
end
阻塞赋值语句在每个右端表达式计算完后,立即赋给左端变量,即赋值语句 LHS1=RHS1执行完后 LHS1 是立即更新的,同时只有 LHS1=RHS1 执行完后才可执行语句 LHS2=RHS2,依次类推。前一条语句的执行结果直接影响到后面语句的执行结果。
非阻塞赋值
非阻塞赋值语句允许赋值调度
,但它不会阻塞位于同一个顺序块中其后语句的执行
。
非阻塞赋值使用“≤=”
作为赋值符,它与“小于等于”关系操作符是同一个符号,但在表达式中它被解释为关系操作符,而在非阻塞赋值的环境下被解释成非阻塞赋值。
为了说明非阻塞赋值的意义以及与阻塞赋值的区别,看下面程序
在这个例子中,从x=0到reg_b = reg_a之间的语句是在仿真0时刻顺序执行的,之后的三条非阻塞赋值语句在reg_b = reg_a执行完成后**并发执行**
。
(1) reg_a[2]=0被调度到15个时间单位之后执行,即仿真时刻为15;
(2) reg_b[15:13]={x,y,z}被调度到10个时间单位之后执行,即仿真时刻为10;
(3) count = count+1被调度到无任何延迟执行,即仿真时刻为0。
从上面的分析中可以看到,仿真器将非阻塞赋值调度到相应的仿真时刻,然后继续执行后面的语句,而不是停下来等待赋值的完成。
一般情况下,非阻塞赋值是在当前仿真时刻的最后一个时间步,即阻塞赋值完成之后才执行的。
在上面的例子中,我们把阻塞和非阻塞赋值语句混合在一起使用,目的是想更清楚地比较和说明它们的行为。
需要提醒读者注意的是,不要在同一个always块中混合使用阻塞和非阻塞赋值语句。
非阻塞赋值可以被用来为常见的硬件电路行为建立模型,例如当某一事件发生后,多个数据并发传输的行为。
非阻塞赋值(<=)
always @ (<event-expression>)
begin
<LHS1 <= RHS1 assignments > //非阻塞赋值语句1
<LHS2 <= RHS2 assignments > //非阻塞赋值语句2
...
end
非阻塞赋值语句右端表达式计算完后并不立即赋给左端,而是同时启动下一条语句继续执行
,我们可以将其理解为所有的右端表达式 RHS1、RHS2 在进程开始时同时计算,计算完后在进程结束时,同时分别赋给左端变量 LHS1、LHS2。
举个栗子
假设已经有 m = 1,n = 2,i= 3
- 阻塞赋值
点击查看代码
//阻塞赋值
always @ (posedge clk)
begin
m = n;
n = i;
i = m;
end
//运行结果为 m = 2,n = 3,i = 2
- 非阻塞赋值
点击查看代码
//非阻塞赋值
always @ (posedge clk)
begin
m <= n;
n <= i;
i <= m;
end
//运行结果为 m = 2,n = 3,i = 1
- 选择赋值方式的原则
- 当用 always 块来描述组合逻辑时,既可以用阻塞赋值,也可以采用非阻塞赋值
- 设计时序逻辑电路,尽量使用非阻塞赋值方式
- 描迷锁存器(Latch),尽量使用非阻塞赋值
- 若在同一个 always 过程块中既为组合逻辑建模,又为时序逻辑建模,最好使用非阻塞赋值方式
- 在一个 always 过程中,最好不要混合使用阻塞赋值和非阻塞赋值,虽然同时使用这两种赋值方式在综合时并不一定会出错;对同一个变量,不能既进行阻塞赋值,又进行非阻塞赋值,这样在综合时会报错
- 不能在两个或两个以上的 always 过程中对同一个变量赋值,这样会引发冲突,在综合时会报错
任务与函数
在程序设计过程中,设计者经常需要在程序的许多不同地方实现相同的功能,此时可以把这些公共的部分提取出来
,写成子程序供重复使用
,在需要的位置直接调用子程序
。
Verilog HDI语法中也提供了类似的语法,就是任务和函数,设计者可以把所需的代码编写成任务和函数的形式,使代码更简洁。
任务
任务的弹性程度比函数大
,在任务中可以调用其他任务或函数,还可以包含延迟,时间控制等语法。
从一个模块的代码结构上来讲,任务应该和 initial ,always结构同处于一个层次,严格来说它属于行为级建模,所以只要行为级可以使用的语法在任务中都是支持的,这一点要和后面的函数区分。
任务的声明格式:
task 任务名称
input [宽度声明] 输入信号名;
output [宽度声明] 输出信号名;
inout [宽度声明] 双向信号名;
reg 任务所用变量声明;
begin
………… //任务包含语句
end
endtask
针对任务格式的语法要求,依次解释如下:
(1)任务声明以task
开始,以endtask
结束,中间部分是任务包含的语句。
(2)任务名称就是一个标识符
,满足标识符语法要求即可。
(3)任务可以有输入信号input 、输出信号output,双向信号inout和供本任务使用的变量
,变量不仅包括上面写出的reg型,行为级中支持的类型如 integer , time等都可以使用。
(4) 任务从整体形式上看和模块十分相似,task和 endtask类似于module和endmodule,但任务虽然有输入输出信号,却没有端口列表
。
(5)完成信号和变量的声明后,可以用begin和end封装task功能描述语句,也可以使用fork和 join并行块来封装,但要注意此语句块结构前没有initial和 always结构
。
(6)对于begin…end 所包含的任务语句部分,遵循行为级建模语法即可。
//4位全加器的任务
task add4;
input [3:0] x,y;
input cin;
output [3:0] s;
output cout;
begin
{cout,s}=x+y+cin;
end
endtask
任务调用时应采用如下格式:
任务名(信息对照表);
例如,对add4任务进行调用,就可以使用如下语句:add4(a, b, c, d, e);
任务的调用要注意以下几点:
(1) 任务调用时要写出任务调用的名称来进行调用,这一点与模块实例化过程相似,但是任务调用不需要使用实例化名称,像add4这个任务名可直接写出调用对应任务。
(2) 任务的功能描述虽然和 always、 initial处于同一层次,但是任务调用必须发生在initial , always , task中。
(3)任务中如果有输入,输出或双向信号,按照类似实例化语句中按名称连接的方式连接信号。
(4)任务的信号连接也要遵循基本的连接要求。
(5)任务调用后需要添加分号,作为行为级语句的一个语句处理。
(6)任务不能实时输出内部值,而是只能在整个任务结束时得到一个最终的结果,输出的值也是这个最终结果的值。
函数
函数与任务不同,任务其实没有太多的语法限制,可以把组合逻辑编写成任务,也可以使用时序控制等语法来完成任务。
但对函数来说,仅仅可以把组合逻辑编写成函数,因为函数中不能有任何的时序语句
,而且函数不能调用任务
,这是受函数自身语法要求限制的。
函数的声明格式:
function 返回值的类型和范围函数名;
input [端口范围] 端口声明;
reg 、 integer等变量声明;
begin
阻塞赋值语句块
end
endfunction
函数的基本要求和注意事项如下:
(1)函数以关键字
function
开头,以关键字endfunction
结尾。
(2)在关键字function
后和函数名称之间 ,要添加返回值的类型和范围,定义返回值类型时如果不指定类型,则会默认定义为reg类型,如果没有指定范围﹐默认为1位。
(3)函数至少需要一个输入信号,没有输出信号,所以 output之类的声明是无效的,函数的运算结果就是通过上一步定义的返回值进行传递的,也就是说函数只能得到一个运算结果,相当于只有一个输出。
(4)函数内部可以定义自身所需的变量。
(5)函数的功能语句也可以用begin…end进行封装。虽然使用fork…join在语法上是允许的,但出于可综合的角度考虑,一般还是使用顺序块。
(6)函数的 begin…end块内部有一些要求。首先不能有任何时间相关的语法,如@引导的事件、#引导的延迟等,而且用于时序电路描述的非阻塞语句也不能使用,但if语句、case语句或循环语句等与时序电路没有直接关系的语句仍然可以使用;其次必须要有语句明确规定函数中的返回值是如何得到赋值的。
具体实例:
//阶乘计算函数
function integer factorial;
input [3:0] a;
integer i;
begin
factorial = 1;
for(i=2;i<=a;i=i+1)
factorial = i*factorial;
end
endfunction
函数调用格式如下:
待赋值变量=函数名称(信号对照表);
函数的调用需要注意以下事项:
(1)函数的调用不像任务调用一样可以只出现任务名,函数调用之后必须把返回值赋给某个变量。任务有输出信号,直接通过输出信号的连接就可以把任务所得的结果进行输出。而函数没有直接定义的输出信号,是通过返回值,采用把函数的返回值赋值给某个变量的形式完成输出。
(2)信号对照列表部分需要按照函数内部声明的顺序出现。
(3)函数调用也作为行为级建模的一条语句,出现在initial ,always ,task , function结构中,即函数可以被任务调用,但任务不能被函数调用。
Verilog HDL除了可以允许设计者自己编写任务和函数外,还提供了可以直接使用的系统任务和系统函数。系统任务和函数都以\(作为开头,如Smonitor、\)finish、$time等,其调用方法和设计者自己编写的任务和函数完全相同。