Verilog模块
Verilog中代码描述的电路叫模块,模块具有以下的结构:
module module_name[ (portname {, portname})]; //端口列表
[parameter declarations] //参数定义
[input declarations] // I/O定义
[output declarations]
[inout declarations]
[wire or tri declarations] //内部信号定义
[reg or integer declartions]
[function or task declarations] //功能定义
[assign continuous assignments]
[initial block]
[always block]
[gate instantiations]
[module instantiations]
endmodule
模块通常以module开始,endmodule结束,并具有模块名,模块名可以是任何有效的标识符。名字后面跟随的是端口列表,端口都有一个相关联的类型。端口类型可以是input, ouput或者inout(双向),可以是标量,也可以是矢量。下面是全加器模块和4位加法模块的代码。
module fulladd(cin,x,y,s,cout);
input cin,x,y;
output s,cout;
assign {cout,s}=x+y+cin;
endmodule
module fulladd4(cin,x,y,s,cout);
input [3:0] x,y;
input cin;
output [3:0] s;
output cout;
assign {cout,s}=x+y+cin;
endmodule
端口在默认状态下是线网(wire,tri等)类型。信号可以分为端口信号和内部信号。出现在端口列表中的信号是端口信号,其它的信号为内部信号。对于端口信号,输入端口只能是线网类型。输出端口可以是线网类型,也可以是reg类型。若输出端口在过程块中赋值则为reg类型;若在过程块外赋值(包括实例化语句),则为线网类型。内部信号类型与输出端口相同,可以是线网或reg类型。判断方法也与输出端口相同。若在过程块中赋值,则为reg类型;若在过程块外赋值,则为线网类型。
内部信号定义时候我们可以定义一些在模块内部使用的信号。比如下面n位加法器的代码中:我们定义了integer 类型k以及内部信号C。
module addern(carryin,X,Y,S,carryout);
parameter n=32;
input carryin;
input [n-1:0] X, Y;
output reg [n-1:0] S;
output reg carryout;
reg [n:0] C;
integer k;
always @(X,Y, carryin)
begin
C[0]=carryin;
for(k=0;k<n;k=k+1)
begin
S[k]=X[k]^Y[k]^C[k];
C[k+1]=(X[k]&Y[k])|(X[k]&C[k])|(Y[k]&C[k]);
end
carryout=C[n];
end
endmodule
在功能定义中,我们通过always过程语句实现n位加法操作,当然我们也可以用assign连续赋值语句实现同样的功能,比如上面fulladd4的代码。
并行语句
在硬件描述语言中,并行语句是代表着电路的一部分,这些语句是并行执行的,它们的顺序并不重要。并行语句包括连续赋值语句和门实例化语句。
连续赋值语句
连续赋值语句用来描述组合逻辑电路,用来给线网赋值,赋值时用阻塞赋值。但不同连续赋值语句之间是并行执行的,因为它们都代表电路的一部分,这些电路在物理上可以并行执行。
连续赋值语句的格式为:assign net_assginment[,net assignment];
下面是一些连续赋值语句的例子:
assign cout = (x&y)|(y&cin)|(x&cin);
assign s = x^y^z;
门实例化语句
verilog中包括预定义的基本逻辑门。这些逻辑门允许通过门实例化语句来调用。门实例化语句结构为:
gate_name instance_name(output_port, input_port{,input_port});
实例名甚至可以省略,直接调用gate_name实现逻辑功能。
下面是门实例化实现全加器代码:
module fulladd(cin,x,y,s,cout);
input cin,x,y;
ouput s,cout;
wire z1,z2,z3,z4;
and and1(z1,x,y);
and and2(z2,x,cin);
and and3(z3,y,cin);
or or1(cout,z1,z2,z3);
xor xor1(z4,x,y);
xor xor2(s,z4,cin);
endmodule
module fulladd(cin,x,y,s,cout);
input cin,x,y;
ouput s,cout;
wire z1,z2,z3,z4;
and(z1,x,y);
and(z2,x,cin);
and(z3,y,cin);
or(cout,z1,z2,z3);
xor(z4,x,y);
xor(s,z4,cin);
endmodule
我们还可以在门电路中设置一个延时参数,例如:and #(20) and1(z,x1,x2,x3), 表示这个与门延时20时间单位。但这种参数只用在testbench中,电路中是不能综合的。如果电路中用到这些延时参数,综合工具通常会忽略它们。Verilog允许逻辑门有任意的输入,但是实际上受CAD系统限或工艺等等限制,逻辑门的扇入和扇出都是有限制的。扇入:逻辑门的输入数量。扇出:某个门驱动其它门的数量。
Verilog中支持的逻辑门主要由以下几种。
名称 |
说明 |
用法 |
and |
f = a&b&… |
and(f,a,b,…) |
nand |
f=~(a&b&…) |
nand(f,a,b,…) |
or |
f=a|b|… |
or(f,a,b,…) |
nor |
f=~(a|b|…) |
nor(f,a,b,…) |
xor |
f=a^b^… |
xor(f,a,b,…) |
xnor |
f=~(a^b^…) |
xnor(f,a,b,…) |
not |
f=~a |
not(f,a) |
buf |
f=a; |
buf(f,a) |
notif0 |
f=!e?~a:'bz 三态门 |
notif0(f,a,e) |
notif1 |
f=e?~a:'bz 三态门 |
notif1(f,a,e) |
bufif0 |
f=!e?a:'bz 三态门 |
bufif0(f,a,e) |
bufif1 |
f=e?a:'bz 三态门 |
bufif1(f,a,e) |
过程语句
除了并行语句,verilog中还提供了过程语句。并行语句是并行执行,而过程语句则是按照代码的顺序执行,verilog语法要求过程语句包含在一个always块内部:
always块
always块包含一个或多个过程语句的结构,它的形式如下:
always @(sensitivity_list) //敏感信号列表
[begin] //当一个always块中包含多条语句时,就必须用begin…end
[procedural assignment statement]
[if-else statement]
[case statement]
[while, repeat, and for loops]
[task and function calls]
[end]
相比于连续赋值语句和门实例化,上面的这些语句提供了更为强大的行为级电路描述方式。
敏感信号列表是一个直接影响always块信号输出的信号列表。敏感信号之间用逗号(,)或者 or分开。当敏感信号列表中任何一个信号发生改变时,always块中的过程语句即被顺序执行。我们可以用always @(*),表示所有的输入信号多包含在敏感信号列表中。下面是一个简单always块例子:
always @(x,y) begin s=x^y; c=x&y; end
敏感信息列表也可以在信号的边沿触发,posedge 信号的升沿触发,negedge信号的下降沿触发,例如下面的代码:
always @(posedge clk,negedge Rst_n)
begin
if(Rst_n==0)
Q<=0;
else
Q<=D;
end
一个verilog module可以包含多个always块,它们都代表电路的一部分,不同的always块之间是并行执行的。
过程赋值语句
always块中赋值的信号都是reg或integer等变量类型,不能是wire类型。给一个信号赋值用过程赋值语句。过程赋值语句有两种,阻塞赋值和非阻塞赋值。
阻塞赋值:
s = x + y; //先执行第一句
p = s[0]; //再执行第二句
非阻塞赋值:
s <= x + y; //两条语句同时执行
p<= s[0]; //p此时更新的s[0]仍是之前的值。
在一个always块中,一般不建议混用阻塞和非阻塞赋值语句。
前面提到连续赋值语句中,我们采用阻塞赋值,是不是组合电路都不能采用非阻塞赋值?其实在很多情况下是可以使用的,但是如果分支语句的赋值取决于之前的结果,非阻塞赋值可能产生无意义的电路。比如下面bit_count模块代码,用来统计四位数中1的数目,综合后是3个加法器。如果把for循环中的赋值改为非阻塞赋值,则循环过程为:
count <= count +x[0];
count <= count +x[1];
count <= count +x[2];
此时,count的初始值都为0,则for循环退化为
count<=x[0];
count<=x[1];
count<=x[2];
当always块当中存在多条给同一个变量赋值的语句时,相当于多源激励输入。
module bit_count(x,count);
parameter n=4;
parameter logn=2;
input [n-1:0] x;
output reg[logn:0] count;
integer k; always @(x)
begin
count=0;
for(k=0;k<n;k=k+1)
count = count + x[k];
end endmodule
module bit_count(x,count);
parameter n=4;
parameter logn=2;
input [n-1:0] x;
output reg[logn:0] count;
integer k; always @(x)
begin
count<=0; #10
for(k=0;k<n;k=k+1)
count <= count + x[k];
end endmodule
用阻塞赋值,用下面的testbench,得到结果:
# .main_pane.objects.interior.cs.body.tree
# run -all
# x=10101010,count= 4
# x=11111010,count= 6
`timescale 1ns/1ns
module bit_count_tb;
reg [7:0] x;
wire [3:0] count;
bit_count #(.n(8),.logn(3)) bit_count0(.x(x),.count(count)); initial
begin
x = 8'b10101010;
#20
$display("x=%b,count=%d",x,count);
x = 8'b11111010;
#20
$display("x=%b,count=%d",x,count); $stop;
end
endmodule
如果用非阻塞赋值的代码,则得到下面的结果:这是此时相当于多源激励输入,如果有的0,有的1,最终的值为x。
# .main_pane.objects.interior.cs.body.tree
# run -all
# x=10101010,count= x
# x=11111010,count= x
# ** Note: $stop : D:/fpga/veriloglearn/bitcount/testbench/bit_count_tb.v(17)
if else 语句
和c语言的if else语句语法一样,if else语句必须包含在always块中。if else语法结构为:
if( 表达式1)
begin
statement;
end
else if(表达式2)
begin
statement;
end
else
begin
statement;
end
下面的if else语句,定义了一个二选一电路。
module vif1(a,b,sel,r);
input a;
input b;
input sel;
output reg r; always @(*)
begin
if(sel==0)
r = a;
else
r = b;
end
endmodule
下面的代码综合后是两个比较器和两个级联的二路选择器。
module vif1(a,b,c,sel,r);
input a;
input b;
input c;
input [1:0] sel;
output reg r; always @(*)
begin
if(sel==2'b00)
r = a;
else if(sel==2'b01)
r = b;
else
r = c;
end
endmodule
下面的代码综合后是三个比较器和三个级联的二路选择器。也就是说elseif会被综合成级联的二路选择器,而不是多路选择器。
module vif1(a,b,c,d,sel,r);
input a;
input b;
input c;
input d;
input [1:0] sel;
output reg r; always @(*)
begin
if(sel==2'b00)
r = a;
else if(sel==2'b01)
r = b;
else if(sel==2'b10)
r = c;
else
r = d;
end
endmodule
case 语句
case语句是一种多分支选择语句,可以直接处理多分支语句。
1)case(表达式) <case分支项> endcase
2)casex(表达式) <case分支项> endcase
3)casez(表达式) <case分支项> endcase
case分支项的一般格式如下:
分支表达式: 语句;
……
默认项(default) 语句;
1)case后括号内的表达式称为控制表达式,分支项后的表达式称作分支表达式,又称作常量表达式。控制表达式通常表示为控制信号的某些位,分支表达式则用这些控制信号的具体状态值来表示。
2)当控制表达式和分支表达式的值相等时,就执行分支表达式后的语句。
3)default项可有可无,一个case语句里只准有一个default项。(为了防止程序自动生成锁存器,一般都要设置default项)
4)每一个case的表达是必须各不相同,执行完case分支项的语句后,跳出case块。
5)case语句的所有表达式的值的位宽必须相等。
case casex 和 casez 的真值 (行0,1,x,z是控制表达式,列0/1/x/z是分支表达式)
case |
0 1 x z |
casez |
0 1 x z |
casex |
0 1 x z |
||
0 |
1 0 0 0 |
0 |
1 0 0 1 |
0 |
1 0 1 1 |
||
1 |
0 1 0 0 |
1 |
0 1 0 1 |
1 |
0 1 1 1 |
||
x |
0 0 1 0 |
x |
0 0 1 1 |
x |
1 1 1 1 |
||
z |
0 0 0 1 |
z |
1 1 1 1 |
z |
1 1 1 1 |
casex和casez是case的特殊情况,用来处理过程中不必考虑的情况。casez用来处理不用考虑高阻值,casex表示不用考虑高阻值和不定值。
上述表格说明casez中,可以将z任意匹配,casex中可以将x任意匹配。在case的分支语句中,从上到下开始匹配,输出第一个匹配成功的值。
下面代码中我们用case语句实现上面ifelse中的电路功能。综合工具通常只考虑x=0,x=1的情况,所以在case中,我们不考虑x,z的情况。
从rtl viewer中,可以看到综合后是一个多路选择器,这个要比上面ifelse语句综合后的电路要好。
循环语句
Verilog包括四种循环语句,for, while, repeat和forever,综合工具通常仅支持for语句。其它几种语句主要用在testbench当中。
在C语言中,经常用到for循环语句,但在硬件描述语言中for语句的使用和C语言等软件描述语言有较大的区别。
在Testbench中for语句在生成激励信号等方面使用较普遍,但在RTL级编码中却很少使用for循环语句。主要原因就是for循环会被综合器展开为所有变量情况的执行语句,每个变量独立占用寄存器资源,每条执行语句并不能有效地复用硬件逻辑资源,造成巨大的资源浪费。简单的说就是:for语句循环几次,就是将相同的电路复制几次,因此循环次数越多,占用面积越大,综合就越慢。
在RTL硬件描述中,遇到类似的算法,推荐的方法是先搞清楚设计的时序要求,做一个reg型计数器。在每个时钟沿累加,并在每个时钟沿判断计数器情况,做相应的处理,能复用的处理模块尽量复用,即使所有的操作不能复用,也采用case语句展开处理。
对于下面的for循环语句:
for(i=0;i<16;i++)
DoSomething();
可以采用如下代码实现:
reg [3:0] counter;
always @(posedge clk)
if(syn_rst)
counter<=4'b0;
else
counter<=counter+1;
always @(posedge clk)
begin
case(counter)
4'b0000:
4'b0001:
......
default:
endcase
end
另外,有几个语法的细节需要注意一下。for(i=0;i<16;i=i+1)中的i既可以是reg型的变量也可以是integer类型的变量,但是当i是reg型的变量时,需要注意因为判断语句i<16的缘故,i应定义为reg[4:0] i而不是reg[3:0] i 。由于verilog中没有自增运算符,文中提到的for语句不能写成for(i=0;i<16; i++)的形式。
下面简单的列举几个用for实现的程序代码:
示例一:
module vfor1(clk,Rst_n,data, num); input clk; //时钟信号
input Rst_n; //复位信号
input [3:0] data; output reg [2:0] num;
integer i; always @(posedge clk)
begin
if(Rst_n==0)
num = 0;
else
begin
for(i=0;i < 4; i=i+1)
if(data[i]) num = num + 1; end
end endmodule
综合后,用rtl vieer,可以看到这儿有四个相同的电路,而且效率很高,但所消耗的逻辑资源较大。在对速度(时钟周期数)要求不是很高的情况下,可以多用几个时钟周期完成任务,而没有必要用for循环来做。
while, repeat, forever都用在testbench当中,下面是它们的语法:
while(condition)
begin
statement;
end
repeat(constanat_value)
begin
statement;
end
forever
begin
statement;
end
我们用下面的代码说明它们的用法:
`timescale 1ns/1ns
`define clock_period 20 module addern_tb;
reg [7:0] x,y; wire cout;
wire [7:0] s;
reg clk;
integer i,j; addern #(.n(8)) addern_0(
.x(x),
.y(y),
.s(s),
.cout(cout)
); initial clk = 0;
always #(`clock_period/2) clk = ~clk; initial begin
x = 0;
repeat(10)
#(`clock_period) x = $random;
i = 10;
while(i>0)
begin
#(`clock_period) x = $random;
i = i -1;
end
forever
#(`clock_period) x = $random;
end initial begin
y = 0;
repeat(10)
#(`clock_period) y = $random;
j = 10;
while(j>0)
begin
#(`clock_period) y = $random;
j = j -1;
end
forever
#(`clock_period) y = $random; end initial begin
#(`clock_period*200)
$stop;
end endmodule
initial语句
initial和always具有相同的结构,但initial块内的内容只在仿真开始的时候执行一次。initial语句用在testbench中,对综合来说毫无意义。