1.关键字
2.运算符
按其功能可分为以下几类:
1) 算术运算符(+,-,×,/,%)
2) 赋值运算符(=,<=)
3) 关系运算符(>,<,>=,<=)
4) 逻辑运算符(&&,||,!)
5) 条件运算符( ? :)
6) 位运算符(,|,^,&,^)
7) 移位运算符(<<,>>)
8) 拼接运算符({ })
9) 其它
按其所带操作数的个数运算符可分为三种:
1) 单目运算符(unary operator):可以带一个操作数,操作数放在运算符的右边。
2) 二目运算符(binary operator):可以带二个操作数,操作数放在运算符的两边。
3) 三目运算符(ternary operator):可以带三个操作,这三个操作数用三目运算符分隔开。
见下例:
clock = ~clock; // ~是一个单目取反运算符, clock 是操作数。
c = a | b; // 是一个二目按位或运算符, a 和 b 是操作数。
r = s ? t : u; // ?: 是一个三目条件运算符, s,t,u 是操作数。
3.数据类型
Verilog 中共有 19 种数据类型。
基本的四种类型: reg 型、wire 型、integer 型、parameter 型。
其他类型:large 型、medium 型、small 型、scalared 型、time 型、tri 型、trio 型、tril 型、triand 型、trior 型、trireg 型、vectored 型、wand 型和 wor 型。
1) wire 型
wire 型数据常用来表示以 assign 关键字指定的组合逻辑信号。Verilog 程序模块中输入、输出信号类型默认为 wire 型。wire 型信号可以用做方程式的输入,也可以用做“assign”语句或者实例元件的输出。
wire 型信号的定义格式如下:
wire [n-1:0] 数据名 1,数据名 2,……数据名 N;
这里,总共定义了 N 条线,每条线的位宽为 n。下面给出几个例子:
wire [9:0] a, b, c; // a, b, c 都是位宽为 10 的 wire 型信号
wire d;
2) reg 型
reg 是寄存器数据类型的关键字。寄存器是数据存储单元的抽象,通过赋值语句可以改变寄存器存储的值,其作用相当于改变触发器存储器的值。reg 型数据常用来表示 always 模块内的指定信号,代表触发器。通常在设计中要由 always 模块通过使用行为描述语句来表达逻辑关系。在 always 块内被赋值的每一个信号都必须定义为 reg 型, 即赋值操作符的右端变量必须是 reg 型。
reg 型信号的定义格式如下:
reg [n-1:0] 数据名 1,数据名 2,……数据名 N;
这里,总共定义了 N 个寄存器变量,每条线的位宽为 n。下面给出几个例子:
reg [9:0] a, b, c; // a, b, c 都是位宽为 10 的寄存器
reg d;
reg 型数据的缺省值是未知的。reg 型数据可以为正值或负值。但当一个 reg 型数据是一个表达式中的操作数时,它的值被当作无符号值,即正值。如果一个 4 位的 reg 型数据被写入 -1,在表达式中运算时,其值被认为是+15。
reg 型和 wire 型的区别在于:reg 型保持最后一次的赋值,而 wire 型则需要持续的驱动。
3) integer 型
也是一种寄存器数据类型,integer 类型的变量为有符号数,而 reg 类型的变量则为无符号数,除非特别声明为有符号数。
还有就是 integer 的位宽为宿主机的字的位数,但最小为 32 位,用 integer 的变量都可以用 reg 定义,只是用于计数更方便而已。 reg, integer, real,time 都是寄存器数据类型,定义在 Verilog 中用来保存数值的变量,和实际的硬件电路中的寄存器有区别。
4) parameter 型
在 Verilog HDL 中用 parameter 来定义常量,即用 parameter 来定义一个标志符表示一个常数。采用该类型可以提高程序的可读性和可维护性。
parameter 型信号的定义格式如下:
parameter 参数名 1 = 数据名 1;
下面给出几个例子:
parameter s1 = 1;
parameter [3:0] S0=4'h0,
S1=4'h1,
S2=4'h2,
S3=4'h3,
S4=4'h4;
4. 缩位运算
缩减运算符是单目运算符,也有与或非运算。
其与或非运算规则类似于位运算符的与或非运算规则,但其运算过程不同。位运算是对操作数的相应位进行与或非运算,操作数是几位数则运算结果也是几位数。
而缩减运算则不同,缩减运算是对单个操作数进行或与非递推运算,最后的运算结果是一位的二进制数。
缩减运算的具体运算过程是这样的:
1) 第一步先将操作数的第一位与第二位进行或与非运算,
2) 第二步将运算结果与第三位进行或与非运算,
3) 依次类推,直至最后一位。
例如:reg [3:0] B;
reg C;
C = &B;
相当于:
C =( (B[0]&B[1]) & B[2] ) & B[3];
5. if-else
设计要点
1) 条件语句必须在过程块中使用。所谓过程块语句是指由 initial、always 引导的执行语句集合。除了这两个语句块引导的 begin end 块中可以编写条件语句外,模块中的其他地方都不能编写。
2) if 语句中的表达式一般为逻辑表达式或者关系表达式。系统对表达式的值进行判断;若为 0,z,X;按照假处理;若为 1 按照真处理,执行指定的语句;
3) if(a)等价于 if(a == 1);
4) if 语句可以·嵌套·使用
5) end 总是与离它最近的一份 else 配对。
如果 if 语句使用不当,没有 else,
可能会综合出来意想不到的锁存器
在 always 块里面,如果在给定的条件下变量没有被赋值,这个变量将会保持原来的值,也就是说会生成一个锁存器。
需要注意的是,这里说的是可能,
因此,不代表没有 else 就一定会出现锁存器,
同时,不代表有 else 就一定不会出现锁存器。
这个是根据具体设计来看的。
6. case
case 语句检查给定的表达式是否与列表中的其他表达式之一相匹配,并据此进行分支。它通常用于实现一个多路复用器。
如果要检查的条件很多,if-else 结构可能不合适,因为它会综合成一个优先编码器而不是多路复用器。
一个 Verilog case 语句以 case 关键字开始,以 endcase 关键字结束。在括弧内的表达式将被精确地评估一次,并按其编写顺序与备选方案列表进行比较,与给定表达式匹配的备选方案的语句将被执行。一块多条语句必须分组,并在 begin 和 end 范围内。
// Here 'expression' should match one of the items (item 1,2,3 or 4)
case (<expression>)
case_item1 : <single statement>
case_item2,
case_item3 : <single statement>
case_item4 : begin
<multiple statements>
end
default : <statement>
endcase
如果所有的 case 项都不符合给定的表达式,则执行缺省项内的语句,缺省语句是可选的,在case语句中只能有一条缺省语句。case 语句可以嵌套。
如果没有符合表达式的项目,也没有给出缺省语句,执行将不做任何事情就退出 case 块。避免锁存器同 if else,case 应当加上 default,以避免锁存器出现。
注意,如果 case 的情况是完备的,可以不加。(完备意为所有情况都设计了)
7. for
在 C 语言中,经常用到 for 循环语句,但在硬件描述语言中 for 语句的使用较 C 语言等软件描述语言有较大的区别。
for 循环会被综合器展开为所有变量情况的执行语句,每个变量独立占用寄存器资源。
简单的说就是:for 语句循环几次,就是将相同的电路复制几次,因此循环次数越多,占用面积越大,综合就越慢。
注意,i 的变化不跟时钟走:
在 Verilog 中使用 for 循环的功能就是,把同一块电路复制多份,完全起不到计数的作用,所以这个 i 的意思是复制多少份你这段代码实现的电路,和时钟没有任何关系。主要是为了提高编码效率。
8. generate
Verilog 中的 generate 语句常用于编写可配置的、可综合的 RTL 的设计结构。它可用于创建模块的多个实例化,或者有条件的实例化代码块。然而,有时候很困惑 generate 的使用方法,因此看下 generate 的几种常用用法。
我们常用 generate 语句做三件事情。一个是用来构造循环结构,用来多次实例化某个模块。一个是构造条件 generate 结构,用来在多个块之间最多选择一个代码块,条件 generate结构包含 if--generate 结构和 case--generate 形式。还有一个是用来断言。
在 Verilog 中,generate 在建模(elaboration)阶段实施,出现预处理之后,正式模拟仿真之前。因此。generate 结构中的所有表达式都必须是常量表达式,并在建模(elaboration)时确定。例如,generate 结构可能受参数值的影响,但不受动态变量的影响。
generate 循环的语法与 for 循环语句的语法很相似。但是在使用时必须先在 genvar 声明中声明循环中使用的索引变量名,然后才能使用它。genvar 声明的索引变量被用作整数用来判断 generate 循环。genvar 声明可以是 generate 结构的内部或外部区域,并且相同的循环索引变量可以在多个 generate 循环中,只要这些环不嵌套。genvar 只有在建模的时候才会出现,在仿真时就已经消失了。
在“展开”生成循环的每个实例中,将创建一个隐式 localparam,其名称和类型与循环索引变量相同。它的值是“展开”循环的特定实例的“索引”。可以从 RTL 引用此 localparam 以控制生成的代码,甚至可以由分层引用来引用。
Verilog 中 generate 循环中的 generate 块可以命名也可以不命名。如果已命名,则会创建一个 generate 块实例数组。如果未命名,则有些仿真工具会出现警告,因此,最好始终对它们进行命名。
9. 函数 function
function 函数的目的返回一个用于表达式的值。
verilog 中的 function 只能用于组合逻辑;
1 定义函数的语法
function <返回值的类型或范围> <函数名>
<端口说明语句>
<变量类型说明>
begin
<语句>
…
end
endfunction
说明:
1 function [7:0] getbyte ;
2 input [15:0] address ;
3 begin
4 <说明语句> //从地址字节提取低字节的程序
5 getbyte = result_expression ; //把结果赋给函数的返回字节
6 end
7 endfunction
① <返回值的类型或范围>这一项为可选项,如果缺失,则返回值为一位寄存器类型数据。② 从函数的返回值:函数的定义蕴含声明了与函数同名、位宽一致的内部寄存器。例子中,getbyte 被赋予的值就是调用函数的返回值。
③ 函数的调用:函数的调用是通过将函数作为表达式中的操作数来实现的。其调用格式:<函数名> (<表达式> ,…, <表达式>);
其中函数名作为确认符。下面的例子中,两次调用 getbyte,把两次调用的结果进行位拼接运算,以生成一个字。
word = control ? {getbyte(msbyte),getbyte(lsbyte)} : 8'd0 ;
④ 函数使用的规则
1 )函数定义不能包含有任何的时间控制语句,即任何用#、@、wait 来标识的语句。
2 )函数不能调用“task”。
3 )定义函数时至少要有一个输入参数。
4 )在函数的定义中必须有一条赋值语句给函数中与函数名同名、位宽相同的内部寄存器赋值。
5 )verilog 中的 function 只能用于组合逻辑;
2 具体实例
函数功能:实现两个 4bit 数的按位“与”运算。
实验现象:如果函数操作正确,则 led 灯闪烁;如果函数操作不正确,则 led 灯常灭。
module func_ex_01 (
input clk , //E1 25M
output led //G2 高电平 灯亮
);
///*counter_01*
reg [25:0] counter_01 = 26'd0 ;
always @ (posedge clk)
begin
counter_01 <= counter_01 + 1'b1 ;
end
/*& function*/
function [3:0] yu ;
input [3:0] a ;
input [3:0] b ;
begin
yu = a & b ;
end
endfunction
reg [3:0] reg_a = 4'b0101 ;
reg [3:0] reg_b = 4'b1010 ;
wire [3:0] result ;
assign result = yu(reg_a , reg_b) ;
//*verify and display*
assign led = (result == 4'd0) ? counter_01[25] : 1'b0 ;
endmodule
说明:verilog 中的 function 只能用于组合逻辑;
10. 任务 task
任务就是一段封装在“task-endtask”之间的程序。任务是通过调用来执行的,而且只有在调用时才执行,如果定义了任务,但是在整个过程中都没有调用它,那么这个任务是不会执行的。调用某个任务时可能需要它处理某些数据并返回操作结果,所以任务应当有接收数据的输入端和返回数据的输出端。另外,任务可以彼此调用,而且任务内还可以调用函数。
1.任务定义
任务定义的形式如下:
task task_id;
[declaration]
procedural_statement
endtask
其中,关键词 task 和 endtask 将它们之间的内容标志成一个任务定义,task 标志着一个任务定义结构的开始;task_id 是任务名;可选项 declaration 是端口声明语句和变量声明语句,任务接收输入值和返回输出值就是通过此处声明的端口进行的;procedural_statement是一段用来完成这个任务操作的过程语句,如果过程语句多于一条,应将其放在语句块内;endtask 为任务定义结构体结束标志。下面给出一个任务定义的实例。
定义一个任务。
task task_demo; //任务定义结构开头,命名为 task_demo
input [7:0] x,y; //输入端口说明
output [7:0] tmp; //输出端口说明
if(x>y) //给出任务定义的描述语句
tmp = x;
else
tmp = y;
endtask
上述代码定义了一个名为“task_demo”的任务,求取两个数的最大值。在定义任务时,
有下列六点需要注意:
(1)在第一行“task”语句中不能列出端口名称;
(2)任务的输入、输出端口和双向端口数量不受限制,甚至可以没有输入、输出以及双向端口。
(3)在任务定义的描述语句中,可以使用出现不可综合操作符合语句(使用最为频繁的就是延迟控制语句) ,但这样会造成该任务不可综合。
(4)在任务中可以调用其他的任务或函数,也可以调用自身。
(5)在任务定义结构内不能出现 initial 和 always 过程块。
(6)在任务定义中可以出现“disable 中止语句” ,将中断正在执行的任务,但其是不可综合的。当任务被中断后,程序流程将返回到调用任务的地方继续向下执行。
2.任务调用
虽然任务中不能出现 initial 语句和 always 语句语句,但任务调用语句可以在 initial 语句
和 always 语句中使用,其语法形式如下:
task_id[(端口 1, 端口 2, ........, 端口 N)];
其中 task_id 是要调用的任务名,端口 1、端口 2,…是参数列表。参数列表给出传入任务的数据(进入任务的输入端)和接收返回结果的变量(从任务的输出端接收返回结果)。任务调用语句中,参数列表的顺序必须与任务定义中的端口声明顺序相同。任务调用语句是过程性语句,所以任务调用中接收返回数据的变量必须是寄存器类型。下面给出一个任务调用实例。
例:通过 Verilog HDL 的任务调用实现一个 4 比特全加器。
module EXAMPLE (A, B, CIN, S, COUT);
input [3:0] A, B;
input CIN;
output [3:0] S;
output COUT;
reg [3:0] S;
reg COUT;
reg [1:0] S0, S1, S2, S3;
task ADD;
input A, B, CIN;
output [1:0] C;
reg [1:0] C;
reg S, COUT;
begin
S = A ^ B ^ CIN;
COUT = (A&B) | (A&CIN) | (B&CIN);
C = {COUT, S};
end
endtask
always @(A or B or CIN) begin
ADD (A[0], B[0], CIN, S0);
ADD (A[1], B[1], S0[1], S1);
ADD (A[2], B[2], S1[1], S2);
ADD (A[3], B[3], S2[1], S3);
S = {S3[0], S2[0], S1[0], S0[0]};
COUT = S3[1];
end
endmodule
在调用任务时,需要注意以下几点:
(1)任务调用语句只能出现在过程块内;
(2)任务调用语句和一条普通的行为描述语句的处理方法一致;
(3)当被调用输入、输出或双向端口时,任务调用语句必须包含端口名列表,且信号端口顺序和类型必须和任务定义结构中的顺序和类型一致。需要说明的是,任务的输出端口必须和寄存器类型的数据变量对应。 文章来源:https://www.toymoban.com/news/detail-459556.html
(4)可综合任务只能实现组合逻辑,也就是说调用可综合任务的时间为“0” 。而在面向仿真的任务中可以带有时序控制,如时延,因此面向仿真的任务的调用时间不为“0” 。 文章来源地址https://www.toymoban.com/news/detail-459556.html
到了这里,关于FPGA学习笔记-知识点3-Verilog语法1的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!