前面已经记录了一些组成Verilog的基本组成,可以用这些基本组成来构成表达式。这一节,就来记录一下把这些表达式构成一个文件的各种行为描述语句。 ①这里用Verilog基本要素进行的行为描述主要是针对综合来的,也就是可以设计出实际电路来的(行为描述语句有两大子集,一个是面向综合,一个是面向仿真)。②行为描述语句一般指放在always语句中。内容提纲如下所示:
·触发事件控制
·条件语句(if与case语句)
·循环语句
·任务和函数
·编译预处理
一、触发事件控制
①电平敏感事件是指 指定信号的电平发生变化时发生指定的行为。
②边沿触发事件(信号跳变沿)是指 指定信号的边沿信号跳变时发生指定的行为,分为信号的上升沿(x→1或者z→1或者0→1)和下降沿x→0或者z→0或者1→0)。
③信号跳变沿触发电路对信号的某一跳变沿敏感名字一个时钟周期内,只有一个上升沿和一个下降沿,因此计算结果在一个周期内保持不变,而电平触发电路则可能会引起数据在一个时钟周期内变化一次或多次。
其他敏感列表的事项请查看这篇博文:http://www.cnblogs.com/IClearner/p/7253002.html。
二、条件语句
Verilog的条件语句包括if语句和case语句。
(1)if语句
①if语句中的条件判断表达式(括号中的那个)一般为逻辑表达式或者关系表达式或者就一个变量。如果表达式的值是0、X或者Z,则全部按照“假”处理;若为1,则按照“真”处理。
②在应用中,else if 分支的语句数目由实际情况决定;else分支可以省略,但在描述组合逻辑中,会综合得到锁存器。
(2)case语句
①case语句,case语句是一个多路条件分支的形式,常用于多路译码、状态机以及微处理器的指令译码等场合,有case分支 、casez分支、casex分支这三种形式。
②case语句首先对条件表达式求值,然后同时并行对各分支项求值并进行比较;当case语句跳转到某一分支后,控制指针将转移到endcase。
③case分支,case分支语句在执行时,条件表达式和分支之间进行的比较是一种按位进行的全等比较,也就是说,只有在分支项表达式和条件表达式的每一位都彼此相等的情况下,才会认为二者是相等的;此外x、z这两种逻辑状态也作为合法的状态参与比较(1 ==1,,0==0,x==x,z==z)。
④当分支的结果以常数出现时,如果没有指定位宽,则Verilog编译器默认其具有与PC字长相等的位宽。
⑤在casez分支中,如果分支取值的某些位为高阻z,则这些位的比较就不予以考虑,只关注其他位的比较结果。
⑥在casex分支中,则x、z都不予以考虑。
(3)if与case的区别
①if语句指定了一个有优先级的编码逻辑,而case语句生成的逻辑是并行的,不具有优先级。
②通常if结构得出的电路速度较慢,但是占用面积较小,由于速度慢,因此不适合构建特别长的if语句。
③case结构较if结构的速度快,但是占用面积大。
三、循环语句
循环语句有四种:for循环、repeat循环、while循环、forever循环。但是forever循环不能进行综合,而其他三种在一定情况下可以进行综合,因此这里记录可以综合的这三种。一般在电路设计中,不是经常用到循环语句,因为循环语句不好进行优化,占用的资源多,即使用到也是用到for循环。
(1)for循环
①for循环中,在循环次数确定的情况下,也就是循环结束条件是个常量下,才可以进行综合。主体注意要用begin...end来进行包括。
②在Verilog中,不支持++,--这些运算,需要用完整的i=i+1来完成。
(2)repeat循环
①repeat语句执行指定的循环次数,如果循环次数是x或者z,那么循环次数将会按照0次处理。主体注意要用begin...end来进行包括。
②当循环次数在程序编译过程中保持不变时,可以进行综合。
(3)while循环
①只有在指定的循环条件为真(0、x、z都是假)时才会重复执行循环体,注意循环体要用begin...end来包括。
②在执行语句中,必须有改变循环执行条件表达式的值的语句,否则循环可能进入死循环。
循环语句使用注意事项:
①循环语句中出现的变量都采用阻塞赋值(时序中,即沿触发中),这是因为在always块中,使用非阻塞赋值时,只有在always结束后才会把右端的值赋给左边的寄存器,如果采用非阻塞赋值,则会造成循环语句只执行一次。
②在沿触发中,语句采用阻塞赋值,这是由于循环语句本质是由组合逻辑实现的,虽然整体是基于时序逻辑,但是循环部分确是组合逻辑。
③基于循环语句的Verilog显得相对精简,但是面向硬件设计的关注点是时序、面积、功耗等,在使用循环语句进行电路设计时要慎重。
四、任务和函数
(1)任务语句(task)
任务(task)就是一段封装在“task-endtask”之间的程序。任务是通过调用来执行的,而且只有在调用时才执行,如果定义了任务,但是在整个过程中都没有调用它,则该任务不会被执行的。调用某个任务时可能需要它处理某些数据并返回操作结果,所以Verilog 中的task 存在输入端和输出端。
任务定义语法格式如下所示:
1 task<任务名>; // <= task task_id; 2 <端口及数据类型声明语句> // <= [declaration] 3 <语句1> // <= procedural_statement 4 <语句2> // <= procedural_statement 5 ..... // <= 6 <语句n> // <= procedural_statement 7 endtask // <= endtask
上升的格式中,关键词 task 和 endtask 将它们之间的内容标志成一个任务定义,task 标志着一个任务定义结构的开始;task_id 是任务名;可选项 declaration 是端口声明语句和变量声明语句,任务接收输入值和返回输出值就是通过此处声明的端口进行的;procedural_statement 是一段用来完成这个任务操作的过程语句,如果过程语句多于一条,应将其放在语句块内;endtask 为任务定义结构体结束标志。一个任务的例子如下所示:
1 task find_max; //任务定义结构开头,命名为 find_max 2 input [15:0] x,y; //输入端口说明 3 output [15:0] tmp; //输出端口说明 4 if(x>y) //给出任务定义的描述语句 5 tmp = x; 6 else 7 tmp = y; 8 endtask
编写任务时,需要注意:
①调用某个任务时,可能需要它处理某些数据并返回操作结果,因此任务应当有接收数据的输入端和返回数据的输出端。
②任务的输入、输出和双向端口的数量不受限制,甚至可以没有输入、输出和双向端口。
③在任务定义的描述语句中,可以出现不可综合的语句,但是这样会引起任务不可综合。
④任务的定义结构定义不能在initial或者always语句中,在任务定义结构内不能出现 initial 和 always 过程块;但是任务的调用必须在这两个语句块中。
⑤任务定义中可以出现“disable”终止语句,但是这是不可综合的;在仿真中,如果出现的该终止语句,将中断正在执行的任务;任务被中断后,程序流程将返回到调用任务的地方继续向下执行。
⑥在第一行“task”语句中不能列出端口名称。
⑦可以使用出现不可综合操作符合语句(使用最为频繁的就是延迟控制语句,例如#10ns),但这样会造成该任务不可综合。
任务的使用(调用):
task_id (端口1,端口2,...,端口n);
task_id 是要调用的任务名,端口 1、端口 2,…是参数列表。参数列表给出传入任务的数据(进入任务的输入端)和接收返回结果的变量(从任务的输出端接收返回结果)。
任务调用的注意事项:
①任务调用中接收返回数据的变量必须是寄存器类型。
②任务调用语句和一条普通的行为描述语句的处理方法一致。
③可综合任务只能实现组合逻辑;也就是说调用可综合任务的时间为0。而在面向仿真的任务中可以带有时序控制,如多个时钟周期或时延,因此面向仿真的任务调用时间不为0。
④在任务中可以调用其他任务或者函数,也可以调用自身。
⑤当调用输入、输出和双向端口时,任务调用语句必须包含端口名列表,且信号端口顺序和类型必须和任务定义结构中的顺序和类型一致。任务的输出端口必须和寄存器类型的数据变量相对应。
(2)函数(function)
函数与任务类似,函数(function)是一段封装在“function-endfunction”之间的程序。函数的定义格式如下所示:
1 function<返回值的类型或范围>(函数名); 2 <端口说明语句>// input XXX; 3 <变量类型说明语句>// reg YYY; 4 begin 5 <语句> 6 ........ 7 函数名= ZZZ; //函数名就相当于输出变量; 8 end 9 endfunction
函数编写的注意事项如下:
①定义函数时至少要有一个输入参量;可以按照ANSI 和module 形式直接定义输入端口;例如:
function[63:0] alu (input[63:0] a, b,input[7:0] opcode );
此外,函数至少有一个输入端口,可以有多个输入端口;不允许输出声明,也不允许双向端口,因为函数名本身充当一个返回值。
②和任务一样,函数的定义只能在模块中完成,不能出现在过程块中。
③函数结果中,不能出现任何形式的时间控制语句(如#,wait),也不能使用disable。
④函数内部可以调用函数,但是不能调用任务。
⑤如果描述语句是可综合的,则必须所有分支均赋值,不允许存在不赋值情况,只能按照组合逻辑方式描述。
函数编写的一个例子如下所示,这是一个计算有符号数绝对值的函数:
1 function[width-1 : 0] DWF_absval; 2 input [width-1 : 0] A; 3 begin 4 DWF_absval = ((^(A ^ A) !== 1‘b0)) ? {width{1‘bx}} :(A[width-1] == 1‘b0) ? A : (-A); 5 end 6 endfunction
关于函数调用的注意事项:
①函数调用可以在过程块中完成,也可以在assign这样的连续赋值语句中出现。
②函数调用语句不能单独作为一条语句出现,只能作为赋值语句的右端操作数。
使用函数的一个完整实例:一个乘法电路——输入操作数a和b,输出他们的乘积;这里使用移位相加的算法来实现乘法。使用函数的实现的代码如下:
1 module MAC #(parameter N=8)( 2 input clk, 3 input reset, 4 input [N-1:0] opa, 5 input [N-1:0] opb, 6 output reg[2*N-1:0] out 7 ); 8 9 function[2*N-1:0] mult;//函数定义,mult 函数完成乘法操作 10 input[N-1:0] opa; //函数只能定义输入端 11 input[N-1:0] opb; //输出端口为函数名本身 12 reg[2*N-1:0] result; 13 integer i; 14 begin 15 result = opa[0]? opb : 0; 16 for(i= 1; i <= N-1; i = i+1) 17 begin 18 if(opa[i]==1) result=result+(opb<<i); 19 end 20 mult=result; 21 end 22 endfunction 23 24 wire[2*N-1:0] sum; 25 assign sum = mult(opa,opb) + out; 26 always@(posedge clk or negedge reset) 27 if(!reset) 28 out<=0; 29 else 30 out<=sum; 31 32 endmodule
(3)关于任务和函数的对比和总结
①task语句和function语句都是可以综合的,但是只能实现组合逻辑,具备组合逻辑的优点和缺点。
②task和function都必须在模块内部定义,除参数个数不同外,还可以定义内部变量,包括寄存器、时间变量、整型等,但是不能定义线网变量。二者只能出现在行为描述中。
③区别:
比较点 |
任务 |
函数 |
输入、输出 |
可以有任意多个种类类型的参数 |
至少有一个输入,不能有输入和双向端口 |
调用 |
只能在过程语句中调用 |
可以在过程语句中调用,也可以在作为赋值操作的表达式用在assign赋值语句中 |
触发事件控制 |
任务可以包含延迟控制语句等,但是只能面向仿真,不可综合。 |
不能有时间控制语句 |
调用其他函数和任务 |
可以调用其他函数和任务 |
只能调用函数 |
返回值 |
任务没有返回值 |
函数向调用它的表达式返回一个值 |
其他 |
任务调用语句可以作为一条完整的语句出现 |
函数语句只能作为赋值操作的表达式,不能作为一条独立的语句出现 |
五、编译预处理语句
①编译预处理是Verilog编译系统的一个组成部分,指编译系统会对一些特殊命令进行预处理,然后将预处理结果和源程序一起在进行通常的编译处理。编译预处理是可以综合的。
②在Verilog语言编译时,特定的编译器指令在整个编译过程中有效(编译过程可跨越多个文件),直到遇到其他的不同的编译程序指令。
③常用的编译预处理语句有:`define、`undef ; `ifdef、`else、`endif ; `include ; `timescale。
(1)`define,`undef
格式:`define 宏的名称 宏的正文
①宏定义的名称可以是大写,也可以是小写,但是主要不要和变量名重复,此外宏定义的使用要注意带上`。
②和所有的编译器伪指令一样,宏定义在超过单个文件边界时仍然有效(对工程中的其他源文件),除非被后面的`define或者`undef伪指令覆盖,否则`define不受范围的限制。
③当用变量定义宏时,变量可以在宏正文中使用,并且在使用宏时可以用实际的变量表达式代替。
④通过用反斜杠\转移中间的换行符,宏定义可以跨越几行,新的行是宏正文的一部分。
⑤宏定义的行末不需要添加分号或者逗号表示结束
⑥宏正文不能分离的语言记号包括注释、数字、字符串、保留的关键字、运算符。
⑦编译器伪指令不允许作为宏的名字,此外红的正文可以是一个表达式,并不仅英语变量名的替换。
⑧`define和parameter的区别:
区别点 |
`define |
parameter |
作用域 |
①`define从编译器读到这条指令开始到编译结束都有效(除非遇到上面的提到的情况),可以应用于整个工程。 ②`define可以写在代码的任何位置。 |
①parameter作用于声明的当前文件,如果要让它作用与整个项目,可以将这些声明单独列在一个文件中,然后用`include进行包含。 ②parameter必须放在应用之前,也就是你要用到某个parameter参数了,你必须先parameter它。 |
传递功能 |
`define不能实现参数传递,但是不局限与定义变量,还可以定义 |
Parameter可以用作模块例化时的参数传递,实现参数化调用,但是仅局限于定义变量,而不能定义表达式。 |
(2)条件编译命令`if语句
①条件编译指令包括`ifdef 、`else 、`endif,其中`ifdef 、`endif必不可少,`else可选,且条件编译语句可以放在程序的任何地方调用。
②举例:
1 `ifdef ABCD 2 Parameter X1 = 1; 3 `else 4 Parameter X2 = 1; 5 `endif
意思是:如果用`define 定义的ABCD,那么就会执行第一个模块(Parameter X1 = 1);否则执行第二个模块( Parameter X2 = 1)。
(3)文件包含`include语句
①当某个模块需要调用某一个文件时,但是这个文件不在当前目录下,那么就需要使用`include语句进行包含,这样调用才不会出现错误。如 `include “../../xxxx.v”
②一个`include指令只能指定一个被包含的文件。如果要完成N个文件的包含,需要N个`include指令。
③可以将多个`include指令写在同一行,在`include命令行只能出现空格和注释。
④如果文件A同时包含了文件B和C,那么文件C可以直接利用B的内容,而不需要在对B文件进行包括,同理,B也可以直接利用C的内容。
(4)时间尺度`timescale语句
①`timescale用于定义延时的单位和延时的精度,如`timescale 1ns/100ps那么时间单位就是1ns,精度就是100ps。
②时间单位,表示了仿真时测量的单位,比如延时1,1ns;精度则表示仿真器只识别的范围,比如精度是100ps,那么如果你1.3ns,编译器是识别,但是如果写1.32,那么由于精度达不到那么细,所以0.02被四舍五入掉。
③`timescale影响着全部模块,知道遇到另外的`timescale。