条件语句,循环语句,结构说明语句,编译预处理语句,不同抽象级别的模型
整理复习一下常用的Verilog相关知识。
条件语句
条件表达式
信号 = 条件?表达式1 : 表达式2;
//当条件成立时,信号取表达式1的值,反之取表达式2的值。
if-else语句
- 其格式与C语言中if-else语句类似,使用方法有以下3种。系统对表达式的值进行判断,若为0,x,z,按“假”处理;若为“1”,按“真”处理,执行指定语句。 语句可是单句,也可是多句,多句时用“begin-end”语句括起来
if(表达式) 语句1;
//////////////////////////////
if(表达式) 语句1;
else 语句2;
/////////////////////////////
if(表达式1) 语句1;
else if(表达式2) 语句2;
……
else if(表达式n) 语句n;
else 语句n+1;
case语句
- 当敏感表达式的值为值1时,执行语句1;为值2时,执行语句2;依此类推,如果敏感表达式的值与上面列出的值都不相等的话,则执行default后面的语句。
- case语句中,敏感表达式与值1~值n间的比较是一种全等比较,必须保证两者的对应位全等。 在casez语句中,如果分支表达式某些位的值为高阻z,那么对这些位的比较就不予考虑,因此只须关注其他位的比较结果。而在casex语句中,则把这种处理方式进一步扩展到对x的处理,即如果比较的双方有一方的某些位是x或z,那么这些位的比较就都不予考虑。
- case中可以有重复的case item,首次匹配的将会被执行。
- case语句的使用格式如下:
case(敏感表达式)
值1:语句1; // case分支项
值2:语句2;
……
值n:语句n;
default:语句n+1; // default语句可以省略
endcase
锁存器与触发器
- 锁存器是一种对脉冲电平(也就是0或者1)敏感的存储单元电路,而触发器是一种对脉冲边沿(即上升沿或者下降沿)敏感的存储电路。
- 锁存器电路存在对毛刺敏感,不能异步复位,上电以后处于不确定的状态的问题;在PLD芯片中,基本的单元是由查找表和触发器组成的,若生成锁存器反而需要更多的资源;且锁存器的存在会导致静态时序分析变得非常复杂。由于综上所述的几个原因,我们并不希望在电路设计中,生成锁存器电路。
- 当我们在设计电路时,不能直接先写成代码然后期望它直接生成为合适的电路,如下典型错误所示:
If (cpu_overheated) then shut_off_computer = 1;
If (~arrived) then keep_driving = ~gas_tank_empty;
- 语法上正确的代码并不意味着设计成的电路也是合理的。我们来思考这么一个问题,如上图的错误示例,如果if条件不满足,输出如何变化呢?Verilog给出的解决方法是:保持输出不变。因为组合逻辑电路不能记录当前的状态,所以就会综合出锁存器。
使用条件语句注意事项
- 在使用条件语句时,应注意列出所有条件分支,否则,编译器认为条件不满足时,会引进一个触发器保持原值。这可用于设计时序电路,例如在计数器的设计中,条件满足则加1,否则保持不变;而在组合电路设计中,应避免这种隐含触发器的存在。
- 为避免偶然生成锁存器的错误。如果用到if语句,最好写上else项。如果用case语句,最好写上default项。
- 其实,从根源上,是要求对于任何一种输入组合,都需要有一确定的输出值,即可避免锁存器的生成。
循环语句
for语句
for(循环变量赋初值;循环结束条件; 循环变量增值) 执行语句;
- 先给控制循环次数的变量赋初值。再判定控制循环的表达式的值, 如为假则跳出循环语句,如为真则执行指定的语句后,执行一条赋值语句来修正控制循环变量次数的变量的值,然后再次判断。
repeat语句
repeat(循环次数表达式) 语句;
// or
repeat(循环次数表达式)begin……end;
- 连续执行一条语句或者一个语句块 n 次。
while语句
while(循环执行条件表达式) 语句;
// or
while(循环执行条件表达式) begin……end;
- 执行一条语句直到某个条件不满足。如果一开始条件即不满足(为假),则语句一次也不能被执行。
forever语句
forever 语句;
// or
forever begin……end;
- forever循环语句连续不断地执行后面的语句或语句块,常用来产生周期性的波形,作为仿真激励信号。它与always语句的不同之处在于它一般用在initial语句块中。若要用它进行模块描述,可用disable语句进行中断。
结构说明语句
- 在一个模块(module)中,使用initial和always语句的次数是不受限制的。initial说明语句一般用于仿真中的初始化,仅执行一次;always块内的语句则是不断重复执行的。task和function语句可以在程序模块中的一处或多处调用。
initial语句
- initial <语句>。initial块常用于测试文件和虚拟模块的编写,用来产生仿真测试信号和设置信号记录等仿真环境。如:
initial begin //在仿真开始前对各个变量进行初始化
areg=0; //初始化寄存器areg
for(index=0;index<size;index=index+1)
memory[index]=0; //初始化一个memory
end
always语句
- always <时序控制> <语句>。沿触发的always块常常描述时序逻辑, 如果符合可综合风格要求可用综合工具自动转换为表示时序逻辑的寄存器组和门级逻辑,而电平触发的always块常常用来描述组合逻辑和带锁存器的组合逻辑,如果符合可综合风格要求可转换为表示组合逻辑的门级逻辑或带锁存器的组合逻辑。如:
reg[7:0] counter;
reg tick;
always @(posedge areg) begin //每当areg信号的上升沿出现时
tick = ~tick; //把tick信号反相
counter = counter + 1; //并且把counter增加1
end
task语句
- task和function说明语句分别用来定义任务和函数。利用任务和函数可以把一个很大的程序模块分解成许多较小的任务和函数便于理解和调试。
- 任务调用变量和定义时说明的I/O变量是一一对应的。调用任务test时,端口的值按照顺序赋值给输入输出端口。变量data1和data2的值赋给in1和in2,而任务完成后输出通过out1和out2赋给了code1和code2。
task test; //任务名test
input in1,in2; //端口及数据类型声明语句
output out1,out2;
out1 = in1 & in2; //其他语句
out2 = in1 | in2;
endtask//任务定义结束
test(data1,data2,code1,code2); //任务的调用
- 需要注意:①任务的定义与调用须在一个module模块内。②定义任务时,没有端口名列表,但需要在后面进行端口和数据类型的说明。③当任务被调用时,任务被激活。任务的调用与模块调用一样通过任务名调用实现,调用时,需列出端口名列表,端口名的排序和类型必须与任务定义中的排序和类型一致。④一个任务可以调用别的任务和函数,可以调用的任务和函数个数不限。
function语句
- 函数的定义中蕴含了一个与函数同名的、函数内部的寄存器。在函数定义时,将函数返值所使用的寄存器设为与函数同名的内部变量,因此函数名被赋予的值就是函数的返回值。例子中,gefun最终赋予的值即为函数的返回值。
/*gefun函数循环核对输入的每一位,计算出0的个数,并返回一个适当的值*/
function [7:0] gefun; //<返回值位宽或类型说明> 函数名
input[7:0] x; //端口声明
reg [7:0] count = 0 //局部变量声明
integer i;
for (i = 0;i <=7;i = i + 1)//其他语句
if(x[i] == 1'b0) count = count + 1;
gefun = count;
endfunction//函数定义结束
assign out = is_legal ? gefun(in) : 1'b0; //函数调用<函数名>(<表达式>)
- 需要注意:1) 函数的定义不能包含有任何的时间控制语句,即任何用#、@、或wait来标识的语句。2) 函数不能启动任务。3) 定义函数时至少要有一个输入参量。4) 在函数的定义中必须有一条赋值语句给函数中的一个内部变量赋以函数的结果值,该内部变量具有和函数名相同的名字。
IP元件例化语句
- 当我们在调用别人已经写好的IP核,或者使用自己的IP核时,需要对IP核进行元件例化操作
- 例化的目的是在上一级模块中调用例化的模块完成代码功能,在 Verilog 里例化信号的格式如下:模块名必须和要例化的模块名一致,包括模块信号名也必须一致。连接信号为 TOP 程序跟模块之间传递的信号,模块与模块之间的连接信号不能相互冲突,否则会产生编译错误。
- 实际使用中,会有两种例化形式,具体格式如下:
//按信号位置例化(不推荐)
mudule_name new_module_name(new_signal_1,new_signal_2,new_signal_3);
//按信号名称例化(推荐)
mudule_name new_module_name
(
.signal_1 (new_signal_1),
.signal_2 (new_signal_2),
.signal_3 (new_signal_3)
);
- 在一个模块中如果有定义参数,而我们在调用该IP核时,想向其内部传递参数,可在在 module 后用#()进行传递,具体格式如下
- 被调用的功能模块
module rom
#(
parameter depth =15,
parameter width = 8
) (
input [depth-1:0] addr ,
input [width-1:0] data ,
output result
) ;
/////功能代码部分,此处省略不写////
endmodule
- 顶层调用模块模块
module top() ;
wire [31:0] addr ;
wire [15:0] data ;
wire result ;
rom
#(
.depth(32),
.width(16)
)
r1(
.addr(addr) ,
.data(data) ,
.result(result)
) ;
endmodule
生成语句
- Verilog中的生成语句主要使用
generate
语法关键字,按照形式主要分为循环生成与条件生成。
循环生成语句
- 循环生成的主要目的是简化我们的代码书写,利用循环生成语句我们可以将之前需要写很多条比较相似的语句才能实现的功能用很简短的循环生成语句来代替。
generate-for语句
- 基本语法如下:
generate
genvar <var1>;
for (<var1> = 0 ; <var1> < num ; <var1>=<var1>+1)
begin: <label_1>
<code>;
end
endgenerate
- 关于以上语法有四点注意:
- 循环生成中for语句使用的变量必须用genvar关键字定义,genvar关键字可以写在generate语句外面,也可以写在generate语句里面,只要先于for语句声明即可;
- 必须给循环段起一个名字。这是一个强制规定,并且也是利用循环生成语句生成多个实例的时候分配名字所必须的;
- for语句的内容必须加begin-end,即使只有一条语句也不能省略。这也是一个强制规定,而且给循环起名字也离不开begin关键字;
- 可以是实例化语句也可以是连续赋值语句。
- 具体举例如下:
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]));
条件生成语句
条件生成的目的是为了左右编译器的行为,类似于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
- 关于以上语法有三点注意:
- 必须是常量比较,例如一些参数,这样编译器才可以在编译前确定需要使用的代码;
- if语句的内容中,begin-end只有在 < code > 有多条语句时才是必须的;
- 每一个条件分支的名称是可选的,这点不像循环生成语句那么严格。
- 具体举例如下:
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连通。但是注意,一旦连通,那么要想更改必须修改参数后重新编译。
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类似:
- <constant_expression>必须是常量比较,例如一些参数,这样编译器才可以在编译前确定需要使用的代码;
- case语句的内容中,begin-end只有在< code >有多条语句时才是必须的;
- 每一个条件分支的名称是可选的,这点不像循环生成语句那么严格。
- 具体举例如下:
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小节的例子是一模一样的。
编译预处理语句
- 预处理命令以符号“`”开头,以区别于其他语句。在编译时,通常先对这些特殊语句进行“预处理”,然后再将预处理的结果和源程序一起进行编译。
`define语句
- `define语句用来将一个简单的名字或标志符(或称宏名)来代表一个复杂的名字或字符串,其一般形式为:"`define 标志符(宏名) 字符串"
`define IN ina + inb + inc + ind //用宏名IN来代替表达式ina + inb + inc + ind
`define WORDSIZE 8
reg [`WORDSIZE:1] data; // 相当于定义reg[8:0] data
- `define用于将一个简单的宏名来代替一个字符串或一个复杂的表达式。
- 宏定义语句行末不加分号,这一点尤其要注意。
- 在引用已定义的宏名时,不要忘了在宏名的前面加上符号“`”,以表示该名字是一个宏定义的名字。
- 采用宏定义,可以简化程序的书写,而且便于修改。若需要改变某个变量,只须改变`define定义行,一改全改。比如在上面的例子中,定义data是一个8位的寄存器变量,若要将其改为16位,只须将定义行改为“`defineWORDSIZE 16”即可。
`undef语句
\
undef` 用于取消之前的宏定义
`define DATA_DW 32
……
reg [DATA_DW-1:0] data_in ;
……
`undef DATA_DW
`include语句
- `include是文件包含语句,它可将一个文件全部包含到另一个文件中。其一般形式为:"`include 文件名"
`include "adder.v" //包含一个名为adder.v的文件
- 一个`include语句只能指定一个被包含的文件。
- `include语句可以出现在源程序的任何地方。被包含的文件若与包含文件不在同一个子目录下,必须指明其路径。
- 文件包含允许多重包含,比如文件1包含文件2,文件2又包含文件3等。
`timescale语句
- `timescale语句用于定义模块的时间单位和时间精度,其使用格式如下:"`timescale <时间单位> / <时间精度>"
`timescale 10ns/1ns //本模块的时间单位是10ns,时间精度为1ns
reg sel;
initial begin
#10 sel = 0; //在10ns*10时刻,sel变量被赋值为0
#10 sel = 1; //在10ns*20时刻,sel变量被赋值为1
end
- 其中用来表示时间度量的符号有:s、ms、us、ns、ps和fs,分别表示秒、毫秒( 秒)、微秒( 秒)、纳秒( 秒)、皮秒(秒)和飞秒(秒)。
`ifdef、`else、`endif
- 有时希望对其中的一部分内容只有
在满足条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。有时,希望当
满足条件时对一组语句进行编译,而当条件不满足是则编译另一部分。格式如下:
`ifdef 宏名 (标识符)
程序段1
`else
程序段2
`endif
参考文献:
《Verilog经典教程(第三版)》——夏闻宇
《FPGA之道》,狄超,刘萌著,西安交通大学出版社
FPGA之道(74)Verilog生成语句