文章目录

前言

本文节选自《FPGA之道》,来一起学习下作者对于并行与串行的讲解。

Verilog的并行语句

在Verilog的基本程序框架一节中,我们了解到,module模块实现中的语句部分里面的语句都是并行的。那么到底Verilog里面有哪些并行语句可以供我们使用呢?请看如下描述:
module <module_name>(<port_list>);
<Verilog连续赋值语句>;
<Verilog程序块语句>;
<Verilog实例化语句>;
<Verilog生成语句>;
<Verilog函数调用语句>;
<Verilog模块说明语句>;
endmodule;
以上这些并行语句,没有哪一类是不可或缺的,但是一个module中怎么着也得至少有一条,否则虽然从语法上来讲没什么问题,但是这个module就不具有任何功能了。一般来说,只要有模块实例化语句、程序段语句和连续赋值语句这三类语句就足够描述FPGA的功能了,这也正是我们在【Verilog的基本程序框架】一节中所介绍的。下面详细介绍一下这些并行语句。

Verilog连续赋值语句

Verilog中共有两种连续赋值语句,即普通连续赋值语句和条件连续赋值语句。它们都只能给线网类型的变量赋值,前面的章节已经对于这两种语句有过介绍,这里简要总结如下:

普通连续赋值语句

该语句的显式语法为:
assign <wire_name> = ;
也可以有隐式的写法,即在声明线网变量时同时赋值,例如:
wire a;
assign a = 1’b1;
可以简写成:
wire a = 1’b1;

条件连续赋值语句

该语句的显式语法为:
assign = <1-bit_select> ? <input_for_1> : <input_for_0>;
也可以有隐式的写法,即在声明线网变量时同时赋值,例如:
wire a, b, sel, c;
assign c = sel ? a : b;
可以简写成:
wire c = sel ? a : b;

Verilog程序块语句

Verilog中共包含两种程序块语句——initial与always,它们的本质区别是initial程序块仅在程序的最开始执行一次,而always程序块会不断地、循环地得到执行。因此,initial程序块主要负责模块的初始化功能,而always程序块才主要负责模块的功能描述。下面,我们将针对always程序块,进行详细讲解。always的语法结构如下:

always@(<sensitive_list>)
begin : <lable>
   	<statements>;
end

其中,是可选的,它是程序块的标号,主要起到提高代码可读性的作用。注意,如果always中只包含一条子语句,那么begin-end关键字可省略,不过必须紧跟在begin关键字的后面,因此有的程序块不能省略begin-end关键字,其它含有begin-end关键字语法的语句也是类似。
之前已经介绍过,按照<sensitive_list>的形式来分,always共有三个“纯种”的基本类型:纯组合always、纯同步时序逻辑always、具有异步复位的同步时序逻辑always。不过如果从always程序块的结构入手,我们的可以将always的语法划分可以更细一些,即:纯组合always、纯时序always、具有同步复位的always、具有异步复位的always以及具有混合复位的always。可见,基于结构的划分比基于<sensitive_list>的划分更细致一些、面更广一些。基于结构的划分其实就是基于时钟描述结构的划分,因此,首先来介绍一下在always中表示时钟事件的方法。

沿事件

时钟描述实际上就是基于时钟信号跳变沿的描述。如果在有一个变量a,那么表示a变化的时候会触发always的执行,但是如果要描述时序逻辑,我们显然需要更精确的描述方法,于是Verilog提供了沿事件。沿事件的描述有两种语法:
posedge ,
negedge ,
分别对应敏感变量的上升沿事件或者下降沿事件。我们可以利用这两句语法来方便的描述时钟等事件。
切记!在一个always的敏感量表中,只能出现一个时钟信号的一种边沿事件。这是由寄存器的结构决定的,因为一个寄存器只有一个时钟端口,并且只敏感这个端口的某一个边沿,因此凡是不尊重这个事实的代码都是不可综合的。不过,异步复位信号有时候也做边沿事件放入到敏感量表中,但是请注意,异步复位其实是电平敏感事件,之所以作为边沿事件放入到敏感量表中,很大程度上是为了方便仿真。

下面根据时钟描述结构的不同,分别介绍五种基本always代码结构如下:

纯组合always

纯组合always语法如下:
always@(<sensitive_list>)begin
;
end
参考例子如下:
// b must be register data types
always@(a)begin
b = not a;
end
上述例子描述了一个非门的结构,关于纯组合always程序块,有三点需要注意:
一、纯组合always程序块中的语句强烈推荐只使用阻塞赋值符号,而时序always程序块中推荐只使用非阻塞赋值符号,否则会带来非常多的隐患。
二、虽然从字面上理解,always是在变量a出现变化的情况下才触发执行,但是不可自作聪明将上例写成:
// It is wrong!!
always@(posedge a or negedge a)begin
b = not a;
end
注意,只有时序逻辑才能用posedge和negedge关键字,虽然从代码事件解释来看上述两例好像功能相似,但是若出现沿事件关键字,则编译器会将程序块综合为时序逻辑,而这个世界上目前还没有既能够敏感一个信号上升沿又能够敏感这个信号下降沿的触发器,所以综合会报错。
三、若<sensitive_list>中有多个变量,则可以用逗号“,”或者关键字or分隔开来。如果<sensitive_list>中的变量确实太多,Verilog给大家提供了一个偷懒的方法,那就是使用匹配符号“”,此时编译器将会完成<sensitive_list>中的元素推断。例如:
always@(
)

always@*

纯时序always

纯时序always的语法如下:
always@(<edge_type> clk) // only clk in the sensitive list
begin
;
end
参考例子如下:
// a must be register data types
always@(posedge clk)
begin
a <= b;
end

具有同步复位的always

具有同步复位的always的语法如下:
always@(<edge_type> clk)
begin
if(rst) //or if(!rst)
begin
;
end
else
begin
;
end
end
参考例子如下:
//a must be register data types
always@(negedge clk)
if(!rst)
a <= 1’b0;
else
a <= b & c;

具有异步复位的always

具有异步复位的always语法如下:
always@(<edge_type> clk, <edge_type> aRst)
begin
if(aRst) //or if(!aRst)
begin
;
end
else
begin
;
end
end
参考例子如下:
//n must be register data types
always @(posedge clk or negedge aRst)
if (!aRst)
n <= 8’b0;
else
n <= m;
注意,在Verilog中必须通过敏感量表中的一个沿事件再加上代码中的电平判断来实现一个电平敏感的异步复位。以下写法虽然道理上也说的通,但是却是Verilog不支持的:
always @(posedge clk or aRst) //will cause compile error
if (!aRst)
n <= 8’b0;
else
n <= m;
这是由于always敏感量表中一旦出现了沿事件,就不允许再出现描述组合逻辑的信号事件了。

具有混合复位的always

具有混合复位的always的语法如下:
always@(<edge_type> clk, <edge_type> aRst)
begin
if(aRst) //or if(!aRst)
begin
;
end
else
begin
if(rst) //or if(!rst)
begin
;
end
else
begin
;
end
end
end
也可以写成如下形式:
always@(<edge_type> clk, <edge_type> aRst)
begin
if(aRst) //or if(!aRst)
begin
;
end
else if(rst) //or else if(!rst)
begin
;
end
else
begin
;
end
end
参考例子如下:
//a must be register data types
always@(posedge clk, negedge aRst)
if(!aRst)
a <= 4’h0;
else
if(rst) //or if(!rst)
a <= 4’hF;
else
a <= b;

Verilog实例化语句

实例化语句是非常重要的一种语句,有了它,我们才可以化繁为简、聚简成繁!在之前的Verilog基本程序框架小节中我们对实例化语句进行了一些简单了解,而在这里我们将详细介绍一下Verilog中的实例化语句。Verilog语言中支持两种模块实例化方式——单独实例化与数组实例化,分别介绍如下:

单独实例化

单独实例化的语法如下:
<module_name> <instance_name> (.<port_name_0> (),
.<port_name_1> (),

.<port_name_N> ());
其中,<module_name>是一个已经完成模块的名字,<instance_name>是我们给它实例化对象起的一个名字,这两者之间的对应关系很像C++中类和对象之间的关系。<port_name_X>对应被实例化模块中的具体端口名称,其后的为与端口连接的父模块内部的变量。例如:
wire a, b, c;
myAnd insAnd (.in0 (a), .in1(b), .out©);
注意,实例化的时候,实例的输出端口只能连接线网类型的变量,而输入端口可以连接线网或者寄存器类型的变量。

数组实例化

有些情况下,我们可能需要同时实例化一个模块多次,这个时候如果使用单独实例化语句会使代码显得非常的臃肿,也不利于阅读和修改,于是Verilog提供了数组实例化语句,语法如下:
<module_name> <instance_name> <instance_array_range>
(.<port_name_0> (variable0),
.<port_name_1> (variable1),

.<port_name_N> (variableN));
可以看出,相比于单独实例化语句,它主要多了一个<instance_array_range>参数,利用这个参数,我们就可以控制实例的数量。例如:
wire [3:0] a, b, c;
myAnd insAnd[3:0] (.in0 (a), .in1(b), .out©);
上述数组实例化语句的功能相当于
myAnd insAnd3 (.in0 (a[3]), .in1(b[3]), .out(c[3]));
myAnd insAnd2 (.in0 (a[2]), .in1(b[2]), .out(c[2]));
myAnd insAnd1 (.in0 (a[1]), .in1(b[1]), .out(c[1]));
myAnd insAnd0 (.in0 (a[0]), .in1(b[0]), .out(c[0]));
有些时候,众多实例中的有些端口是需要共用信号的,例如使能信号,此时可以写成这样:
wire en;
wire [3:0] a, b, c;
myEnAnd insEnAnd[3:0] (.in0 (a), .in1(b), .inEn (en), .out©);
此时的数组实例化语句的功能相当于
myAnd insAnd3 (.in0 (a[3]), .in1(b[3]), .inEn (en), .out(c[3]));
myAnd insAnd2 (.in0 (a[2]), .in1(b[2]), .inEn (en), .out(c[2]));
myAnd insAnd1 (.in0 (a[1]), .in1(b[1]), .inEn (en), .out(c[1]));
myAnd insAnd0 (.in0 (a[0]), .in1(b[0]), .inEn (en), .out(c[0]));
注意,数组实例化时,对输入的变量位宽是有一定要求的:
一、等于所有实例对应端口的位宽之和。例如对于上例的变量a来说,它的位宽等于4个实例中in0端口的位宽和:1bit*4 = 4bits。这样变量的位宽将会被均分到各个实例的对应端口上;
二、等于模块对应端口的位宽。例如对于上例的变量en来说,它的位宽就等于模块只能够inEn端口的位宽,为1bit,此时该变量就会被连接至所有的实例对应的端口上。
对于其他情况的位宽输入Verilog的数组实例化语句都是不支持的,请不要在这个地方进行错误的发明创造。

实例参数重载

参数重载也是实例化语句的一个重要组成部分。在Verilog基本程序框架中,我们提到过,为了增强模块的重用性,Verilog会在模块中定义一些参数,从而通过再例化的时候对参数进行重载来适应不同的需求。按照例化时对参数的重新赋值方式,我们可以把参数重载分为内部重载与外部重载,分别介绍如下:

内部重载
内部重载使用”#(.<parameter_name>(new_value), …)”语法,例如:
cellAnd #(.WIDTH(4)) m (a,b,c);
这是我们在【Verilog基本程序模板】小节中给出的例子。

外部重载
相比于在模块实例化的时候来修改参数的值,外部重载允许在编译的时候再修改参数的值。外部重载需要用到defparam关键字,举例如下:
cellAnd m (a,b,c);
defparam m.WIDTH = 4;
不过在使用defparam的时候需谨慎,因为有些综合工具或者它们的早期版本并不支持该语法。

端口赋值形式

实例化时实例端口的赋值形式有多种,当然,最常用,最典型也是最推荐的就是映射赋值,不过除此以外,端口赋值还有多种形式,列举如下供大家了解:
一、映射赋值。例如:
wire a, b, c;
myAnd insAnd (.in0 (a), .in1(b), .out©);

二、位置赋值。例如:
wire a, b, c;
myAnd insAnd (a, b, c);

三、部分赋值。这是由于有些模块在使用时并不是所有端口都需要的,若上例中的端口b是可以不使用的,那么按照映射赋值可以写成:
myAnd insAnd (.in0 (a), .out©);
而按照位置赋值必须写成:
myAnd insAnd (a, , c);
注意其中的多余的那个逗号,是用来占位用的,有了它,后面的c变量来能正确对应到out端口。

四、常数赋值。例如:
wire a, c;
myAnd insAnd (.in0 (a), .in1(1’b1), .out©);
注意,常数只能用于实例的输入端口。

五、表达式赋值。例如:
wire a, b, c, d;
myAnd insAnd (.in0 (a&d), .in1(~b), .out©);
不过不建议这样做,因为不太符合规范。

Verilog生成语句

Verilog中的生成语句主要使用generate语法关键字,按照形式主要分为循环生成与条件生成,分别介绍如下:

循环生成

循环生成的主要目的是简化我们的代码书写,利用循环生成语句我们可以将之前需要写很多条比较相似的语句才能实现的功能用很简短的循环生成语句来代替。基本语法如下:
genvar ;
generate
for (=0; < ; =+1)
begin:

end
endgenerate
关于以上语法有四点注意:
1、循环生成中for语句使用的变量必须用genvar关键字定义,genvar关键字可以写在generate语句外面,也可以写在generate语句里面,只要先于for语句声明即可;
2、必须给循环段起一个名字。这是一个强制规定,并且也是利用循环生成语句生成多个实例的时候分配名字所必须的;
3、for语句的内容必须加begin-end,即使只有一条语句也不能省略。这也是一个强制规定,而且给循环起名字也离不开begin关键字;
4、可以是实例化语句也可以是连续赋值语句。
关于循环生成,举例如下:

input [3:0] a,b;
output [3:0] c,d;

generate
genvar i;
	for (i=0; i < 4; i=i+1) 
	begin : genExample
		myAnd insAnd (.a(a[i]), .b(b[i]), .c(c[i]));
		assign d[i] = a[i];
	end
endgenerate

注意,利用循环生成语句生成的实例名称不能像数组例化那样用方括号表示,否则会报错。那么,你可能会疑惑上例中实例的名字,其实,上述实例化展开来类似:

myAnd genExample(0).insAnd (.a(a[0]), .b(b[0]), .c(c[0]));
myAnd genExample(1).insAnd (.a(a[1]), .b(b[1]), .c(c[1]));
myAnd genExample(2).insAnd (.a(a[2]), .b(b[2]), .c(c[2]));
myAnd genExample(3).insAnd (.a(a[3]), .b(b[3]), .c(c[3]));

这也是为什么循环生成语句必须要有个名字。从上例我们还可以看出,当循环语句用作实例化时,所表述的功能跟数组实例化语句其实是类似的。
最后,循环生成语句是支持嵌套的,例如二重循环生成语法如下:

	genvar <var1>, <var2>;
generate
       for (<var1>=0; <var1> < <limit>; <var1>=<var1>+1) 
       begin: <label_1>
          for (<var2>=0; <var2> < <limit>; <var2>=<var2>+1) 
          begin: <label_2>
             <code>
          end
       end
endgenerate	

条件生成

条件生成的目的是为了左右编译器的行为,类似于C语言中的条件选择宏定义,根据一些初始参数来决定载入哪部分代码来进行编译。Verilog中共提供了两种条件生成语句,一种是generate-if语句,一种是generate-case语句,两者的功能几乎相同,只是书写形式不一样而已,分别介绍如下:

generate-if语句

该语句的语法如下:

generate
	if (<condition>) begin: <label_1>
		<code>;
	end else if (<condition>) begin: <label_2>
		<code>;
	end else begin: <label_3>
		<code>;
	end
endgenerate

关于该语法有三点注意:
1、必须是常量比较,例如一些参数,这样编译器才可以在编译前确定需要使用的代码;
2、if语句的内容中,begin-end只有在有多条语句时才是必须的;
3、每一个条件分支的名称是可选的,这点不像循环生成语句那么严格。
关于generate-if语句,举例如下:

wire c, d0, d1, d2;
parameter sel = 1;

generate
	if (sel == 0)
		assign c = d0;
	else if (sel == 1)
		assign c = d1;
	else
		assign c = d2;
endgenerate

该例子表示编译器会根据参数sel的值,来确定到底是让d0~d2中哪个变量和c连通。但是注意,一旦连通,那么要想更改必须修改参数后重新编译,如果需要动态选择,可以写成如下这样,但是资源上却都需要一个多路复用器来实现。
assign c = (sel == 0) ? d0 : (sel == 1) ? d1 : d2;

generate-case语句

该语句的语法如下:

	generate
		case (<constant_expression>)
			<value>: begin: <label_1>
				<code>
				end
			<value>: begin: <label_2>
<code>
				end
			……
			default: begin: <label_N>
<code>
end
      		endcase
   	endgenerate

关于该语法也有三点注意,和generate-if类似:
1、<constant_expression>必须是常量比较,例如一些参数,这样编译器才可以在编译前确定需要使用的代码;
2、case语句的内容中,begin-end只有在有多条语句时才是必须的;
3、每一个条件分支的名称是可选的,这点不像循环生成语句那么严格。
关于generate-case语句,举例如下:

wire c, d0, d1, d2;
parameter sel = 1;

generate
	case (sel)
		0 :
			assign c = d0;
		1: 
			assign c = d1;
		default: 
			assign c = d2;
	endcase
endgenerate

该例所描述的功能和generate-if小节的例子是一摸一样的。

Verilog函数调用语句

函数的定义可以放在模块实现部分中的声明部分,使用function语法定义如下:

function  [<lower>:<upper>] <output_name> ;
input <name>;
<other inputs>
<variable declarations>
begin
<statements>
end
endfunction

关于函数的定义有以下几点说明:
1、<output_name>既是输出的变量名也是函数调用名,它的位宽由function关键字后面的范围指定;
2、中,只能够声明寄存器类型的变量;
3、函数体中语句只能使用阻塞赋值符号;
4、函数调用的时候只能使用位置赋值,因此需要严格按照input的顺序罗列变量;
5、函数调用可以用在并行语句中也可以用在串行语句中;
6、函数中可以调用别的函数;
7、函数支持递归,不过此情况一般用于仿真;
8、函数不能调用任务,这主要是由于任务中可以有定时相关语句,而函数中不能够有。

举例如下:
示例一:几种常用函数调用方法;

module tft(input clk, a, b, c, output reg d,e); 
function  andFunc ;
input a;
input b;
begin
andFunc = a & b;
end
endfunction

always@(posedge clk)
begin
	e <= andFunc(a, b); // called by no-blocking assignment
end

always@*
begin
		d = andFunc(a, b); // called by blocking assignment
end

assign c = andFunc(a, b); // called by continuous assignment
endmodule

示例二:函数调用函数

module(input a,b, output c); 
function  bufFunc ;
		input a;
		begin
			bufFunc = a;
		end
endfunction
function  andFunc ;
		input a;
		input b;
		reg t;
		begin
			andFunc = bufFunc(a) & b;
		end
endfunction
assign c = andFunc(a,b);
endmodule

示例三:函数递归

function [3:0] addFunc ;
	input [3:0] a;
	reg [3:0] t;
	begin
		if(a == 4'b0)
			addFunc = 1'b0;
		else begin
			t = a - 1'b1;
			addFunc = a + addFunc(t);
		end
	end
endfunction

最后,需要提醒大家注意的是,函数的抽象级别比较高,它的编程思路更像是软件而不是硬件,因此一般多用于仿真时使用,具体设计FPGA时,如果需要重复使用某一个功能,完全可以通过模块实例化的方式来实现。

Verilog模块说明语句

模块说明语句的关键字是specify,它主要用来说明模块的一些时延信息。它的语法如下:
specify
<specparam_declarations> //一些参数定义
<timing_constraint_checks> //设置一些时序检查选项
<simple_pin-to-pin_path_delay> //设置模块中组合逻辑管脚到管脚的时间延迟
<edge-sensitive_pin-to-pin_path_delay> //设置模块中时序逻辑时钟相关的时间延迟
<state-dependent_pin-to-pin_path_delay> //条件延迟语句,类似条件生成语句
endspecify
一个简单的例子如下:
specify
  specparam d_to_q =9;
  specparam clk_to_q =11;
  (d=>q) = d_to_q;
  (clk=>q) = clk_to_q;
endspecify
一般来说,各个FPGA厂商一般会针对自己的根据硬件相关的一些原语编写specify,这样我们才能够对我们的设计进行时序仿真或者时序分析,因此基本上我们不需要在自己设计的模块中编写specify。所以本小节仅对模块说明语句进行一些简单介绍,让大家对specify有个概念,做个了解即可。

Verilog的串行语句

串行语句的执行思路是顺序执行的,一般高级编程语言中的语句执行方式都是顺序执行的,例如c语言,由此可见,顺序执行的语句更容易帮助我们来表达我们的设计思想,尤其是使描述时序逻辑变得容易。所以,虽然FPGA的设计思路都是并行的,module中仅支持并行语句的调用,但是为了方便设计者表达自己的思想,尤其是表达时序逻辑的思想,Verilog中的一些并行语句中的子语句体允许是顺序执行的,例如always。那么到底Verilog语言里面有哪些串行语句可以供我们使用呢?以always为例描述如下:

always@(...)
begin
		<Verilog阻塞赋值语句>;
		<Verilog非阻塞赋值语句>;
		<Verilog条件语句>;
		<Verilog循环语句>;
		<Verilog等待语句>;
		<Verilog函数调用语句>;
		<Verilog任务调用语句>;
end

Verilog阻塞赋值语句

使用阻塞赋值操作符对变量进行赋值的语句叫阻塞赋值语句。一般来说,如果你认为你描述的这个变量在FPGA硬件中对应连线,那么你就应该使用阻塞赋值语句。使用阻塞赋值符号的赋值语句,一定要等到赋值行为结束之后才会开始执行下一条程序,因此阻塞赋值语句的书写顺序改变会引起综合或者仿真的问题。举例如下:
always@(c, d) begin
b = c & d;
a = ~ b;
end
若赋值语句顺序颠倒会引起仿真的问题。

Verilog非阻塞赋值语句

使用非阻塞赋值操作符对变量进行赋值的语句叫非阻塞赋值语句。一般来说,如果你认为你描述的这个变量在FPGA硬件中对应寄存器等存储记忆单元,那么你就应该使用非阻塞赋值语句。使用非阻塞赋值符号的赋值语句,在赋值行为未完成之前就会开始执行下一条程序,也正是因为如此,所以非阻塞赋值语句的书写顺序是无所谓的。举例如下:
always@(posedge clk) begin
b <= c & d;
a <= ~ b;
end
赋值语句顺序颠倒无所谓。
如果无视变量对应的硬件结构而乱用赋值符号的话,会造成非常大的隐患。

Verilog条件语句

条件语句是一种典型的串行语句。Verilog中有两类条件语句——带优先级条件语句和无优先级条件语句。其中优先级条件语中的各个条件分支是具有优先级的,且分支优先级按照书写顺序从高至低,代表为if条件语句;而无优先级条件语句中,各个分支间的地位是等同的,代表为case条件语句。除了if和case语句外,Verilog还支持casex和casez两种衍生的无优先级条件语句,分别介绍如下:

if条件语句

if条件语句的完全语法如下:

if (<condition>) begin
<statement>;
end
else if (<condition>) begin
<statement>;
end
else begin
<statement>;
end

其中的 else if和else分支都不是必须的,可以根据具体情况来确定。以求A、B、C三者中的最大值为例描述如下:

if (A >= B and A >= C)
	max = A;
else if (B >= C)
	max<= B;
else
	max <= C;

为什么说if条件语句是具有优先级的条件语句呢?需要从两个方面来说:
第一,从语句的功能描述来分析。如果要描述上述求最大值的例子,我们可以这样翻译代码:首先,判断数A是不是大于等于B和C,如果成立,则最大值是A,结束判断;否则说明A不是最大值,那么这时候只需判断数B是不是大于等于C,如果成立,则最大值是B,判断结束;否则,由于之前已经得出A、B两数都不是最大值,那么最大值只能是C了。由此可见,每一个分支的判断都是建立在写在它之前的所有分支的基础上的。
第二,从硬件实现上来说。上述求最大值的例子,对应到门级电路上,肯定是从A到max之间的路径最短,即所经过的逻辑门最少,而从B到max之间的路径次之,从C到max之间的路径最长。关于门级实现可以参考如下示意图:
FPGA之道(35)Verilog中的并行与串行语句_赋值语句
由此可见,基于优先级条件语句的特点,如果我们知道A、B、C三个数中最大值的概率是B大于C大于A,那么我们应该把对B的判断放在第一个分支,然后C放在第二个分支,而A放在最后一个分支。这样,今后的仿真效率会更高,且对于具体的FPGA实现,也能保证最短路径得到最充分的利用,这样芯片即使工作在比较恶劣的环境下,也能保证故障率达到最低。

case条件语句

case条件语句的完全语法如下:

case (<expression>)
	<constant-value1> : 
begin
			<statements>;
		end
	<constant-value2> : 
begin
			<statements>;
		end
	<other branchs>	
	default :
begin
			<statements>;
		end
endcase
其中,<constant-value>的值必须互相不同,以四选一多路选择器为例描述如下:
case (sel)
2’b00 : data = channel0; 
2’b01 : data = channel1; 
2’b10 : data = channel2;
2’b11 : data = channel3;
default : data = channel0;
endcase

上述例子中的分支已经覆盖完全,但是还是有一个default分支,这虽然有些画蛇添足,但确是一个编程的好习惯,请大家注意!
为什么说case条件语句是无优先级的条件语句呢?也需要从两方面来说:
第一,从语句的功能描述来分析。如果要描述上述多路选择器的例子,我们可以这样翻译代码:如果sel等于2’b00,那么选择第一路输出;如果sel等于2’b01,那么选择第二路输出;如果sel等于2’b10,那么选择第三路输出;如果sel等于2’b11那么选择第四路输出。可见这四个分支的判断之间没有任何相互关系,也互不为前提。
第二,从硬件实现上来说。上述多路复用器的例子,对应到门级电路上,无论是channel0~3中的任何一个,到data的路径都是等长的。关于门级实现可以参考如下示意图:
FPGA之道(35)Verilog中的并行与串行语句_实例化_02
由此可见,在使用无优先级条件语句时,分支的顺序是无关的,不会影响电路的最终实现。

if与case的对比

为了进一步说明优先级条件语句与非优先级条件语句之间的区别,我们用if条件语句重写上节中四选一多路选择器的例子如下;

	if(sel == 2'b00)
		data = channel0;
	else if(sel == 2'b01)
		data = channel1;
else if (sel == 2'b10)
		data = channel2;
	else
		data = channel3;

关于其门级实现可参考如下电路图:
FPGA之道(35)Verilog中的并行与串行语句_实例化_03
可见,此时,channel0~3到data的路径长度就不一致了,最短的为一个两输入复用器延迟,最大的为3个两输入复用器延迟。当然,由于上图并不是最简形式,所以此处我们没必要深究它与【case条件语句】小节中的例子到底孰优孰劣,但是请注意,由于目前的编译器都会对我们的代码有一定优化作用,因此有时候if和case也可能会综合成为一样的电路。

case语句中的判断表达式有可能出现的情况比较多,但是分支却有可能没有那么多,因此下面介绍一些case的变形写法,能够更加方便我们去描述电路。

case语句的一些变形

首先,利用特殊的“或”符号——“,”来简化代码,例如,要构建一个三输入的多路复用器,可以描述如下(当然,这并不是最优描述形式):

case (sel)
2’b00 : data = channel0; 
2’b01 : data = channel1; 
2’b10 ,
2’b11 : data = channel2;
default : data = channel0;
endcase

其次,case的常量和表达式还可以互换位置,例如

reg sel;
case (2'b00)
sel : data = channel0; 
default : data = channel1;
endcase

case、casex与casez

在Verilog语法中,case的比较是十分高效的,但它的匹配成功要求所有位上的逻辑值必须精确相等。于是,Verilog又提供了casex与casez两种语法结构作为补充,它们和case的语法结构相同,只不过分别以casex和casez开头而已。这样,在比较的时候就可以引入不关心位,从而能够达到简化代码的效果。在【本篇->编程语法->Verilog基本语法->Verilog数据类型->Verilog四值逻辑系统】小节,我们介绍了Verilog中的四种逻辑形式:0、1、X、Z,那么,对于casex来说,它会将X、Z视为“不关心位”;而对于casez来说,它会将Z视为“不关心位”。
在Verilog中,我们可以用“?”来表示“不关心位”,讨论如下:

条件表达式中有“不关心位”

举例说明如下:
reg a;
case (a)
1’b0 : statement1;
1’b1 : statement2;
1’bx : statement3;
1’bz : statement4;
endcase
上例中,若a = 1’b0或1’b1,那么statement1或statement2将会执行;若我们令a = ?,那么statement4将会执行,因为语法认为“?”等于Z状态。

reg a;
casez (a)
1’b0 : statement1;
1’b1 : statement2;
1’bx : statement3;
1’bz : statement4;
endcase
上例中,若a = 1’b0、1’b1或1’bx,那么statement1、statement2或statement3将会执行;但是若a = ?或者1’bz,那么statement1会执行,因为此时casez将这两种值视为无关状态,会直接执行第一条语句,所以statement4永远得不到执行。

reg a;
casex (a)
1’b0 : statement1;
1’b1 : statement2;
1’bx : statement3;
1’bz : statement4;
endcase
上例中,若a = 1’b0或1’b1,那么statement1或statement2将会执行;但是若a = ?或者1’bx、1’bz,那么statement1会执行,因为此时casex将这三种值视为无关状态,会直接执行第一条语句,所以statement3、statement4永远得不到执行。

常数项中有“不关心位”

举例如下:
case (sel)
3’b1?? : data0 = channel0;
3’b01? : data0 = channel1;
default : data0 = channel2;
endcase
由于case要求精确的匹配,所以无论当sel是什么情况,都只能执行default语句,因此data0只能取到channel2的值。

casex (sel)
	3'b1z? : data1 = channel0; 
	3'b01x : data1 = channel1; 
	default : data1 = channel2;

endcase
由于casex将?、X、Z均视为“不关心位”,因此,sel从3’b1003’b111都能匹配3’b1z?,而sel从3’b0103’b011都能匹配3’b01x,而3’b000、3’b001什么都不匹配,因此data1可以取到channel0、channel1和channel2的所有值。

casez (sel)
	3'b1z? : data2 = channel0; 
	3'b01x : data2 = channel1; 
	default : data2 = channel2;

endcase
由于casez将?、Z均视为“不关心位”,因此,sel从3’b1003’b111都能匹配3’b1z?,而sel从3’b0103’b011却不能匹配3’b01x,再加上3’b000、3’b001也什么都不匹配,因此data2可以取到channel0和channel2的值,却没有办法通过匹配获得channel1的值。
上述几个例子如果写在一个module中,我们可以通过其综合后的电路来更加形象的理解:

要想用case来实现上例casex实现的优先级译码功能,最优的情况下可以写成这样:
case (sel)
3’b001 : data0 = channel2;
3’b010,
3’b011 : data0 = channel1;
default : data0 = channel0;
endcase

最后,需要说明的一点是,casex和casez中,可以通过使用不关心位来实现代码的简化或一些特殊的逻辑功能,例如优先级译码器。但是在其他情况下请避免使用,因为casex和casez的很多用法都只能停留在仿真阶段。

Verilog循环语句

Verilog中的循环语句有很多种,包括for循环、while循环、repeat循环以及forever循环等。这些循环语法中除了for循环有时候可以用来帮助我们简化一些代码的编写外,基本都是主要用于仿真激励的设计,因此本小节主要介绍一下Verilog中的for循环,剩下的将会在【功能仿真篇->仿真语法->Verilog Test Fixture】章节中介绍。
for循环的语法为:
integer ; //递减
for ( = <initial_value>; >= <final_value>; =-1) begin
;
end
或者
integer ; //递增
for ( = <initial_value>; <= <final_value>; =+1) begin
;
end
例如,如果我们要将一个矢量信号高低位颠倒的赋给另一个矢量信号,可以用for循环简便的表述如下:
integer i;
for (i = 7; i >= 0; i=i+1) begin
a[i] <= b[7-i];
end
注意,在描述设计时,for循环一般不应该进行功能描述,而应该只进行结构描述。否则,由于for循环抽象级别比较高,编译器不一定能够正确给对应的实现电路,而且有时候很可能就不存在能够对应该功能的电路。

Verilog等待语句

Verilog中有三种等待语句,分别介绍如下:

事件等待

事件等待的语法如下:
@( or or … )
每个always程序块中都必有一个事件等待语法,除此以外,事件等待语法还可以位于always程序块中,此时的always程序块主要是用于仿真。

直接时间等待

直接时间等待的语法如下:
#
直接时间等待只能用于仿真。

表达式等待语句

表达式等待语句的语法如下:
wait ();
当为真的时候程序才往下执行,它也主要用于仿真。

由于等待语句主要用于仿真结构中,所以详情请参阅【功能仿真篇->仿真语法->Verilog Test Fixture】小节。

Verilog函数调用语句

函数调用语句即可以用在并行语句中,也可以用在串行语句中。

Verilog任务调用语句

任务即是task,它的语法如下:
task <task_name>;
input <input_name>;
<more_inputs>
output <output_name>;
<more_outputs>

begin
;
end
endtask
关于任务调用有以下几点说明:
1、任务中有输入端口也有输出端口,所以它的调用是通过输入端口将数据传入任务中,然后从输出端口得到结果,所以任务可以同时有多个输出,这点与函数不同;
2、中,只能够声明寄存器类型的变量;
3、任务中可以使用阻塞赋值也可以使用非阻塞赋值,具体要看调用任务的always是在描述时序逻辑还是组合逻辑;
4、任务调用的时候只能使用位置赋值,因此需要严格按照端口的顺序罗列变量;
5、任务调用只能用在串行语句中;
6、任务中可以调用别的任务;
7、任务支持递归,不过此情况一般用于仿真;
8、任务中可以调用函数。

举例如下:
示例一:任务中的阻塞赋值与非阻塞赋值
task bufTask;
input a;
output b;
begin
b = a;
end
endtask
task regTask;
input a;
output reg b;
begin
b <= a;
end
endtask
always@*
bufTask(a,b);
always@(posedge clk)
regTask(a,b);
示例二:任务调用任务
task andTask;
input a,b;
output c;

	reg t;
	begin
		t = bufFunc(a);
		c = t & b;
	end
endtask
示例三:任务递归

task addTask ;
input [3:0] a;
output [3:0] b;
reg [3:0] t,h;
begin
if(a == 4’b0)
b = 4’b0;
else begin
t = a - 1’b1;
addTask(t, h);
b = h + 1’b1;
end
end
endtask
最后,需要提醒大家注意的是,任务和一般也多用于仿真时使用,虽然任务的描述跟模块有些类似,但是具体在设计FPGA时,如果需要重复使用某一个功能,完全可以通过模块实例化的方式来实现。