Verilog HDL(Hardware Description Language)是在 C 语言的基础上发展起来的一种硬件描述语言(用它可以表示逻辑电路图、逻辑表达式、数字逻辑系统所完成的逻辑功能等)具有灵活性高、易学易用等特点。Verilog HDL 可以在较短的时间内学习和掌握,目前已经在 FPGA 开发/IC 设计领域占据绝对的领导地位。
Verilog HDL的基本语法
Verilog 的逻辑值
逻辑电路中有四种值,即四种状态:
- 逻辑 0:表示低电平
- 逻辑 1:表示高电平
- 逻辑 X:表示未知,有可能是高电平,也有可能是低电平
- 逻辑 Z:表示高阻态,外部没有激励信号是一个悬空状态
Verilog 的标识符
标识符(identifier)用于定义模块名、端口名、信号名等。Verilog 的标识符可以是任意一组字母、数字、$和_(下划线)符号的组合,但标识符的第一个字符必须是字母或者下划线,另外标识符是区分大小写。
常量
在程序运行过程中,其值不能被改变的量称为常量,Verilog 常见的常量有数字、参数。
数字
- 整数
在Verilog HDL中,整型常量有以下四种进制表示形式:
- 二进制整数(b或B)
- 十进制整数(d或D)
- 十六进制整数(h或H)
- 八进制整数(o或O)
表达方式有以下三种: - <位宽><进制><数字>这是一种全面的描述方式
- <进制><数字>在这种描述方式中,数字的位宽采用默认位宽(这由具体的机器系统决定,但至少32位)
- <数字>在这种描述方式中,数字进制采用默认的十进制,数字的位宽采用默认位宽(这由具体的机器系统决定,但至少32位)
8'b10101100 //位宽为8的数的二进制表示, 'b表示二进制
8'ha2 //位宽为8的数的十六进制,'h表示十六进制
- x和z值
在数字电路中,x代表不定值,z代表高阻值。在十六进制格式中x表示四位二进制,在八进制格式中x表示三位二进制,在二进制格式中x表示一位二进制。z的表示方式同x类似。
4'b10x0 //位宽为4的二进制数从低位数起第二位为不定值
4'b101z //位宽为4的二进制数从低位数起第一位为高阻值
12'dz //位宽为12的十进制数其值为高阻值
8'h4x //位宽为8的十六进制数其低四位值为不定值
- 负数
一个数字可以被定义为负数,只需在位宽前加一个减号。
-8'd5 //这个表达式代表-5的补码(用八位二进制数表示)
- 下划线
下划线可以用来对数进行分隔,以提高程序可读性
16'b1010_1011_1111_1010
parameter参数
在Verilog HDL中可以用parameter来定义常量,即用parameter来定义一个标识符代表一个常量,称为符号常量,即标识符形式的常量,采用标识符代表一个常量可提高程序的可读性和可维护性。其说明格式如下:
parameter 参数名1=表达式,参数名2=表达式, …, 参数名n=表达式; //语法格式
parameter msb=7; //定义参数msb为常量7
parameter 参数可以定义在模块参数列表中,通过在例化模块时传入不同的值,以达到配置模块功能的目的。
localparam参数
与parameter类似,localparam也可以用来定义符号常量,但是localparam不能用在模块参数列表中。
变量
Verilog中常用的变量类型有reg型、wire型
wire型
wire型即线型,可以把他理解为数字电路中的导线,只能在assign语句中对其赋值,其定义方法如下:
wire [n-1:0] 数据名1,数据名2,…数据名i; //共有i条总线,每条总线内有n位
wire [7:0] b; //定义了一个八位的wire型数据
wire [4:1] c, d; //定义了二个四位的wire型数据
reg型
wire型即寄存器类型,可以把他理解为数字电路中的寄存器(在综合时组合逻辑中的reg可能不会综合成寄存器),只能在always语句中对其赋值,其定义方法如下:
reg [n-1:0] 数据名1,数据名2,… 数据名i;
reg [3:0] regb; //定义了一个四位的名为regb的reg型数据
reg [4:1] regc, regd; //定义了两个四位的名为regc和regd的reg型数据
memory型
Verilog HDL通过对reg型变量建立数组来对存储器建模(可以将memory型理解为reg数组,在Verilog中只支持一维数组),数组中的每一个单元通过一个数组索引进行寻址,其定义方法如下:
reg [n-1:0] 存储器名[m-1:0]; //reg[n-1:0]定义了存储器中每一个存储单元的大小,[m-1:0]定义了该存储器中有多少个这样的寄存器([n-1:0]决定位宽,[m-1:0]决定个数)
reg [7:0] mema[255:0]; //定义了一个名为mema的存储器,该存储器有256个8位的寄存器,存储单元索引范围是0到255
mema[3]=0; //给memory中的第3个存储单元赋值为0。
signed关键字
在Verilog中wire和reg默认是无符号(unsigned)类型的,可以通过关键字signed定义有符号的wire和reg型变量(有符号变量负数采用补码表示),其定义方法如下:
reg signed [3:0] regb; //定义了一个四位的有符号reg型数据
wire signed [7:0] wireb; //定义了一个八位的有符号wire型数据
在使用有符号变量时需要注意以下几点:
- 一条运算究竟是按unsigned运算还是按signed运算,取决于其右值的操作数是否含有unsigned变量,只要右值存在unsigned变量,整个操作就会按unsigned处理(这里可能会出现将一个表示负数的补码当作一个很大的正数来处理)
- 如果对signed进行截位运算,截位后会变成unsigned变量,如wireb是有符号wire型数据,而wireb[5:0]就不是有符号wire型数据
- signed变量需要有符号位,所以单bit变量无法表示一个有符号数负数,若用其做有符号运算时会出错(必须为其扩展符号位才能进行有符号运算)
- 无符号变量可以通过$signed()转换为有符号变量,比如一个无符号变量和一个有符号变量进行运算时,希望按有符号处理则可以使用$signed()将无符号变量转换为有符号变量($signed()不会改变变量的值,只是告诉编译器,这个无符号变量要当作有符号变量处理)
- 无符号数和有符号数进行比较时按无符号数处理(即有符号的负数会被当做一个很大的正数)
运算符
算术运算符
在Verilog HDL语言中,算术运算共有下面几种:
- + (加法运算符,或正值运算符,如 rega+regb,+3)
- - (减法运算符,或负值运算符,如 rega-3,-3)
- × (乘法运算符,如rega*3)
- / (除法运算符,相除取整,如5/3的值为1)
- % (模运算符,相除取余。如7%3的值为1)
不建议在Verilog使用除法运算符,应使用除法IP核来进行除法处理,如果是除以2^n可以用移位操作替代
位运算符
Verilog HDL作为一种硬件描述语言,提供了以下五种位运算符:
- ~ //取反
- & //按位与
- | //按位或
- ^ //按位异或
- ^~ //按位同或
逻辑运算符
在Verilog HDL语言中提供了三种逻辑运算符:
- && 逻辑与
- || 逻辑或
- ! 逻辑非
关系运算符
Verilog HDL语言中关系运算符共有以下四种:
- a < b a小于b
- a > b a大于b
- a <= b a小于或等于b
- a >= b a大于或等于b
等式运算符
Verilog HDL语言中有四种等式运算符:
- == 等于,当于操作数中某些位为不定值x或高阻值z,结果可能为不定值x
- != 不等于,当于操作数中某些位为不定值x或高阻值z,结果可能为不定值x
- === 等于 ,不对定值x和高阻值z也进行比较
- !== 不等于 ,不对定值x和高阻值z也进行比较
移位运算符
在Verilog HDL中有两种移位运算符:
- << 左移位运算符
- >> 右移位运算符
拼接运算符
在Verilog HDL语言有一个特殊的运算符:拼接运算符{}。用这个运算符可以把两个或多个信号的某些位拼接起来进行运算操作,其使用方法如下:
{信号1的某几位,信号2的某几位,…,…,信号n的某几位}
运算符优先级别
对各种运算符的优先级别如下:
Verilog关键字
Verilog 和 C 语言类似,都因编写需要定义了一系列保留字,叫做关键字(或关键词),如下是Verilog的所有关键字
其中常用的关键字如下
关键字 | 含义 |
---|---|
module | 模块开始定义 |
input | 输入端口定义 |
output | 输出端口定义 |
inout | 双向端口定义 |
parameter | 参数常量定义 |
localparam | 参数常量定义 |
wire | 线型变量定义 |
reg | 寄存器变量定义 |
always | 产生reg型变量语句的关键字 |
assign | 产生wire型变量语句的关键字 |
begin | 顺序块起始标志,当出现多条语句时需要写在begin end之间 |
end | 顺序块结束标志 |
posedge | 表示上升沿敏感 |
negedge | 表示下降沿敏感 |
case | 多条件分支语句起始标记 |
default | 多条件分支语句的默认分支 |
endcase | 多条件分支语句结束标记 |
if | f/else语句标记 |
else | if/else语句标记 |
for | for语句标记 |
endmodule | 模块结束定义 |
模块开始和结束
Verilog模块以以module开始,endmodule结束,其格式如下:
//模块开始
module 模块名 #(
//模块参数,模块参数非必选项
parameter 参数1,
parameter 参数2,
......,
parameter 参数n
)
(
//输入参数
input 输入端口1,
input 输入端口2,
......,
input 输入端口n,
//双向参数
inout 双向端口1,
inout 双向端口2,
......,
inout 双向端口n,
//输出参数
output 输出端口1,
output 输出端口2,
......,
output 输出端口n
)
//内部参数变量定义
//内部功能定义
//模块结束
endmodule
条件语句
- if_else语句,if语句是用来判定所给定的条件是否满足,根据判定的结果(真或假)决定执行给出的两种操作之一,Verilog HDL语言提供了三种形式的if语句。
1、
if(表达式)
语句
2、
if(表达式)
语句1
else
语句2
3、
if(表达式1)
语句1;
else if(表达式2)
语句2;
else if(表达式3)
语句3;
........
else if(表达式m)
语句m;
else
语句n;
在if和else后面可以包含一条语句,也可以包含多条语句,当包含多条语句时需要用用begin和end将多条语句组合成一个复合语句。
if(a>b) begin
out1<=int1;
out2<=int2;
end
else begin
out1<=int2;
out2<=int1;
end
- case语句,case语句是一种多分支选择语句,它的一般形式如下:
case(控制表达式)
条件1 : 语句1;
条件2 : 语句2;
条件3 : 语句3;
......
条件n : 语句n;
default : 语句n+1;
endcase
当控制表达式的值与分支条件的值相等时(分支条件必须是常量,且不能与其他分支相同,位宽也必须与控制表达式的位宽一致),就执行分支条件后面的语句,如果所有分支都不匹配,就执行default后面的语句(如果没有default则此时case不执行任何分支的语句)
循环语句
在Verilog HDL中存在着四种类型的循环语句,用来控制执行语句的执行次数,不过Verilog在综合时只能综合有限次数的循环语句。
- repeat语句,repeat语句用于控制语句执行指定的次数,语法格式如下
1、
//表达式用于指定循环次数,一般为常量
repeat(表达式)
语句;
2、
repeat(表达式) begin
多条语句
end
- while语句,while循环语句通过循环条件来控制循环次数,语法格式如下
1、
while(表达式)
语句
2、
while(表达式) begin
多条语句
end
- for语句,for语句与while语句一样,都是通过循环条件控制循环次数,语法格式如下:
1、
for(表达式1; 表达式2; 表达式3)
语句
2、
for(表达式1; 表达式2; 表达式3) begin
语句
end
其执行流程相当于下面的while语句:
表达式1;
while(表达式2) begin
语句;
表达式3;
end
assign语句和always语句
assign语句和always语句是Verilog中的两个基本语句,使用频率很高。
assign语句使用时不能带时钟,只能生成组合逻辑电路,在assign语句中只能对wire型变量赋值。
always语句可以带时钟,也可以不带时钟,不带时钟时生成组合逻辑,带时钟时生成时序逻辑,在always语句中只能对reg型变量赋值
task和function语句
task和function说明语句分别用来定义任务和函数,利用任务和函数可以把一个很大的程序模块分解成许多较小的任务和函数便于理解和调试
- function语句
function语句的格式如下:
function [automatic] [返回值宽度] 函数名;
输入端口列表;
[变量声明语句];
begin
语句;
......
end
endfunction
function:表示函数开始。
automatic:声明函数未可重入,表示函数内定义的变量默认为动态的,在调用函数时单独分配,可以避免在多个地方同时调用时出现结果未知的情况,但是会消耗更多资源。
[返回值宽度]:它是是可选的,若未指定则默认未1bit。
函数名:函数名是函数的名字,调用时通过此名字来调用,同时还会在函数内部定义一个同名的变量用于返回函数的返回值。
输入端口列表:一个函数至少包含一个输入端口,不能有输出端口和双向端口。
[变量声明语句]:用于声明函数内部使用的变量。
endfunction:表示函数结束。
function的调用格式如下:
函数可以在always中被调用,也可以在assign被调用,函数调用时只能做右值,不能单独作为一条语句
变量 = 函数名(表达式1[, 表达式2, ......])
注意:
在函数中使能实现组合逻辑,不能出现延时语句(如#200),不能出现敏感事件控制语句(如wait),不能出现过程块语句(如always)。
- task语句
task语句的格式如下:
task [automatic] 任务名;
[输入端口列表];
[输出端口列表];
[双向端口列表];
[变量声明语句];
begin
语句;
......
end
endtask
task:表示任务开始
[automatic]:表示任务内定义的变量默认为动态的,在调用任务时单独分配
任务名:任务的名称,在always中可以通过任务名调用任务
[输入端口列表]:任务可以包含0个或多个输入端口
[输出端口列表]:任务可以包含0个或多个输出端口
[双向端口列表]:任务可以包含0个或多个双向端口
[变量声明语句]:声明任务内部所需要使用的变量
endtask:表示任务结束
任务调用格式如下:
任务只能在过程语句中被调用(如always),其调用格式如下:
任务名([表达式1], [表达式2], ......]);
注意:
虽然在任务中可以使用延时语句(如#200),敏感事件控制语句(如wait),但是使用了这些语句的任务无法综合,因为Verilog只能综合组合逻辑电路的任务。
Verilog中的赋值操作
在Verilog中有两种赋值方式:
- 阻塞式赋值(如b = a;)
在执行赋值语句时计算右值,计算完成后立即赋给变量,整个过程完成再执行后面的语句 - 非阻塞式赋值(如 b <= a;)
在执行赋值语句时计算右值,计算完成后不会立即赋给变量,然后继续执行后面的指令,待语句块结束后才将右值结果赋给变量
一般再组合逻辑中用阻塞式赋值,在时序逻辑中用非阻塞式赋值(若在时序电路中用阻塞赋值会出现时钟到来时一边在进行赋值操作,一边在进行读取操作,从而导致读取的结果不确定的问题,而组合逻辑电路没有时钟,输入直接取决于输出,故而用阻塞式赋值)
Verilog程序框架
下面以一个LED流水灯为例展示Verilog 的程序框架
`timescale 1ns / 1ns
//module表示模块开始
module flow_led #(
//参数列表
parameter LED_CHANNEL = 4, //LED数量
parameter COUNT_WIDTH = 25, //内部计数器宽度
parameter COUNT_PERIOD = 25_000_000 //计数器最大周期,决定LED多久变化依次
)
(
//输入参数列表,输入参数列表只能式wire
input sys_clk, //时钟
input sys_rst_n, //复位,低电平有效
//双向(inout)参数列表,双向参数只能是wire,这里暂未使用
//输出参数列表,输出参数可以是wire可以是reg,默认是wire
output reg [LED_CHANNEL-1:0] led //LED
);
//周期计数器
reg [COUNT_WIDTH-1:0] count;
//一个周期计数器,计数范围为0~COUNT_PERIOD-1,即周期为COUNT_PERIOD
//always用于产生一个对reg型变量进行赋值的语句块
//posedge表示关注信号上升沿
//@表示敏感变量表条件成立执行一次
//begin/end产生一个顺序块
//<=用于赋值时是非阻塞式赋值,执行时先计数右值,并不会立即赋给变量,需要等语句块结束后才赋给变量,因此需要在下一个时钟沿才能读取到这时钟沿赋的值
always @(posedge sys_clk) begin
if(!sys_rst_n)
count <= 0;
else if(count < (COUNT_PERIOD - 1))
count <= count + 1;
else
count <= 0;
end
//控制LED输出
always @(posedge sys_clk) begin
if(!sys_rst_n)
led <= 1;
else if(count == (COUNT_PERIOD - 1))
led <= {led[LED_CHANNEL-2:0],led[LED_CHANNEL-1]};
end
//endmodule是模块结束标记
endmodule