verilog语法实例学习(4)
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语句综合后的电路要好。
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
循环语句
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实现的程序代码:
示例一:
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中,对综合来说毫无意义。