Verilog HDL是一种硬件描述语言,用于数字电路的系统设计。以文本形式来描述数字系统硬件的结构和行为,它可以用来表示逻辑电路图、逻辑表达式、还可以表示数字逻辑系统所完成的逻辑功能。
1. Verilog基础语法
格式
Verilog区分大小写,可以在一行内编写,也可以跨多行编写。
注释
两种注释方式: 单行注释 // 多行注释 /* */
标识符与关键字
标识符可以是任意一组字母、数字、¥符号和_下划线符号的集合,但第一个符号必须是字母或者下划线。
Verilog中关键字全部为小写,标识符区分大小写。
reg [3:0] counter ; //reg为关键字,counter为标识符
2. 数值表示
电平逻辑的数值表示
四种基本值: 0:逻辑0或"假"
1:逻辑1或"真"
x或X:未知,信号可能为0可能为1
z或Z:高阻,常见于没有驱动时的逻辑结果,和上下拉的状态有关系
整数数值表示
合法的基数格式有4种:十进制('d或’D),十六进制('h或’H),二进制(‘b或B’),八进制('o或’O)。
数值可指名位宽,也可不指明位宽。直接写数字式,默认为十进制表示
eg:
4'b1101 //4bit数值
32'h2145_1cde //32bit数值
num = 100; //十进制100,一般根据编译器自动分频位宽,常见32bit
负数表示
通常在表示位宽的数字前面加一个减号表示负数
eg:
-6'd12
-13
实数表示
实数表示方法主要两种方式:
十进制:
12.211
5.0
0.001
科学记数法:
1.2e4 //12000
1e-3 //0.001
1_0001e4 //100010000
字符串表示
字符串是由双引号包起来的字符队列,不能多行书写。Verilog将字符串当作一系列的单字符ASCII字符队列。存储"hello"需要5*8bit的存储单元。
eg:
reg [0: 5*8-1] str ;
initial begin
str = "hello";
end
3. 数据类型
按物理数据类型分
最常用的两种数据类型就是线网(wire)与寄存器(reg),其余类型可以理解为这两张数据类型的扩展和辅助
线网 wire
wire类型表示硬件单元之间的物理连线,由其连接到的器件输出端连续驱动。如果没有驱动元件连接到wire型变量,缺省值一般为"Z"。
wire interrupt;
wire flag1,flag2;
wire gnd = 1'b0 ;
寄存器 reg
寄存器reg用来表示存储单元,它会保持数据原有的值,直到被改写。
reg clk_temp;
reg flag1,flag2;
寄存器型数据和线型数据的区别在于:寄存器型数据保持最后一次的赋值,而线型数据需要持续的驱动
按抽象数据类型分
integer整型
算数运算中整型被视为二进制补码形式的有符号数,寄存器型数据当作无符号数来处理,除此以外整型数据与32位寄存器型数据在实际意义上相同。
time时间型
时间型变量与整型类似,只是它是64位的无符号数。
real实型
在机器码表示法中是浮点型数值。
event事件型
特殊的变量类型,不具有任何值,作用是使模块不同部分的事件在时间上同步。
parameter参数型
参数型数据是被命名的常量,在仿真开始前对其赋值,整个仿真过程中保持其值不变,数据的具体类型是由所赋的值来决定的。可以用它来定义变量的位宽以及延迟时间等。
4. 运算符和表达式
Verilog语言参考了C语言中大多数运算符的语义和句法,一个例外,Verilog中没有增1++和减1–运算符。
1. 算术运算符
Verilog中又称为二进制运算符,包括乘(*)、除(/)、加(+)、减(-)、求幂(**)、取模(%)
2. 关系运算符
关系操作符有大于(>),小于(<),大于等于(>=),小于等于(<=)。
关系操作符的正常结果有 2 种,真(1)或假(0)。
如果操作数中有一位为 x 或 z,则关系表达式的结果为 x。
A = 4 ;
B = 3 ;
X = 3'b1xx ;
A > B //为真
A <= B //为假
A >= Z //为X,不确定
3.逻辑运算符
逻辑操作符主要有 3 个:&&(逻辑与), ||(逻辑或),!(逻辑非)。
逻辑操作符的计算结果是一个 1bit 的值,0 表示假,1 表示真,x 表示不确定。
如果一个操作数不为 0,它等价于逻辑 1;如果一个操作数等于 0,它等价于逻辑 0。如果它任意一位为 x 或 z,它等价于 x。
如果任意一个操作数包含 x,逻辑操作符运算结果不一定为 x。
A = 3;
B = 0;
C = 2'b1x ;
A && B // 为假
A || B // 为真
! A // 为假
! B // 为真
A && C // 为X,不确定
A || C // 为真,因为A为真
(A==2) && (! B) //为真,此时第一个操作数为表达式
4.位逻辑运算符
按位操作符包括:取反(),与(&),或(|),异或(^),同或(^)。
按位操作符对 2 个操作数的每 1bit 数据进行按位操作。
如果 2 个操作数位宽不相等,则用 0 向左扩展补充较短的操作数。
取反操作符只有一个操作数,它对操作数的每 1bit 数据进行取反操作。
5. 一元约简运算符
约简运算符是单目运算符,也有与、或、非运算。
约简操作符只有一个操作数,它对这个向量操作数逐位进行操作,最终产生一个 1bit 结果。
逻辑操作符、按位操作符和约简操作符都使用相同的符号表示,因此有时候容易混淆。区分这些操作符的关键是分清操作数的数目,和计算结果的规则。
A = 4'b1010 ;
&A ; //结果为 1 & 0 & 1 & 0 = 1'b0,可用来判断变量A是否全1
~|A ; //结果为 ~(1 | 0 | 1 | 0) = 1'b0, 可用来判断变量A是否为全0
^A ; //结果为 1 ^ 0 ^ 1 ^ 0 = 1'b0
6. 移位操作符
移位操作符包括左移(<<),右移(>>),算术左移(<<<),算术右移(>>>)。
移位操作符是双目操作符,两个操作数分别表示要进行移位的向量信号(操作符左侧)与移动的位数(操作符右侧)。
算术左移和逻辑左移时,右边低位会补 0。
逻辑右移时,左边高位会补 0;而算术右移时,左边高位会补充符号位,以保证数据缩小后值的正确性。
A = 4'b1100 ;
B = 4'b0010 ;
A = A >> 2 ; //结果为 4'b0011
A = A << 1; //结果为 4'b1000
A = A <<< 1 ; //结果为 4'b1000
C = B + (A>>>2); //结果为 2 + (-4/4) = 1, 4'b0001
7. 拼接操作符
拼接操作符用大括号 {,} 来表示,用于将多个操作数(向量)拼接成新的操作数(向量),信号间用逗号隔开。
拼接符操作数必须指定位宽,常数的话也需要指定位宽。例如:
A = 4'b1010 ;
B = 1'b1 ;
Y1 = {B, A[3:2], A[0], 4'h3 }; //结果为Y1='b1100_0011
Y2 = {4{B}, 3'd4}; //结果为 Y2=7'b111_1100
Y3 = {32{1'b0}}; //结果为 Y3=32h0,常用作寄存器初始化时匹配位宽的赋初值
8. 条件操作符
条件表达式有 3 个操作符,结构描述如下:
condition_expression ? true_expression : false_expression
计算时,如果 condition_expression 为真(逻辑值为 1),则运算结果为 true_expression;如果 condition_expression 为假(逻辑值为 0),则计算结果为 false_expression。
assign hsel = (addr[9:8] == 2'b0) ? hsel_p1 : hsel_p2 ;
//当信号 addr 高 2bit 为 0 时,hsel 赋值为 hsel_p1; 否则,将 hsel_p2 赋值给 hsel。
其实,条件表达式类似于 2 路(或多路)选择器,其描述方式完全可以用 if-else 语句代替。
当然条件操作符也能进行嵌套,完成一个多次选择的逻辑。例如:
assign hsel = (addr[9:8] == 2'b00) ? hsel_p1 :
(addr[9:8] == 2'b01) ? hsel_p2 :
(addr[9:8] == 2'b10) ? hsel_p3 :
(addr[9:8] == 2'b11) ? hsel_p4 ;
5. 编译指令
以反引号 ` 开始的某些标识符是 Verilog 系统编译指令。
编译指令为 Verilog 代码的撰写、编译、调试等提供了极大的便利。
`define
编译阶段,`define用于文本替换,类似C语言中的#define。
``define DATA 32`
`undef
`undef用来取消之前的宏定义
``undef DATA`
`ifndef条件编译
`ifndef WINDOW
parameter DATA_DW = 32 ;
`else
parameter DATA_DW = 64 ;
`endif
`include
使用 `include 可以在编译时将一个 Verilog 文件内嵌到另一个 Verilog 文件中,作用类似于 C 语言中的 #include 结构。该指令通常用于将全局或公用的头文件包含在设计文件里。
文件路径既可以使用相对路径,也可以使用绝对路径。
`include "../../param.v"
`include "header.v"
`timescale
在 Verilog 模型中,时延有具体的单位时间表述,并用 `timescale 编译指令将时间单位与实际时间相关联。
该指令用于定义时延、仿真的单位和精度,格式为:
`timescale time_unit / time_precision
time_unit 表示时间单位,time_precision 表示时间精度,它们均是由数字以及单位 s(秒),ms(毫秒),us(微妙),ns(纳秒),ps(皮秒)和 fs(飞秒)组成。时间精度可以和时间单位一样,但是时间精度大小不能超过时间单位大小,例如下面例子中,输出端 Z 会延迟 5.21ns 输出 A&B 的结果。
`timescale 1ns/100ps //时间单位为1ns,精度为100ps,合法
//`timescale 100ps/1ns //不合法
module AndFunc(Z, A, B);
output Z;
input A, B ;
assign #5.207 Z = A & B
endmodule
在编译过程中,该指令会影响后面所有模块中的时延值,直至遇到另一个 ˋtimescale 指令或 ˋresetall 指令。
由于在 Verilog 中没有默认的`timescale,如果没有指定ˋtimescale ,Verilog 模块就有会继承前面编译模块的 ˋtimescale参数。有可能导致设计出错。
如果一个设计中的多个模块都带有 ˋtimescale 时,模拟器总是定位在所有模块的最小时延精度上,并且所有时延都相应地换算为最小时延精度,时延单位并不受影响。
6. 连续赋值
assign
连续赋值语句是 Verilog 数据流建模的基本语句,用于对 wire 型变量进行赋值。格式如下:
assign left_target = right_expression ;
left_target必须是标量或者wire变量
right_expression可以是标量或线型或寄存器型向量,也可以是函数调用
assign 为关键词,任何已经声明 wire 变量的连续赋值语句都是以 assign 开头.
wire Cout, A, B ;
assign Cout = A & B ; //实现计算A与B的功能
7.过程结构
过程结构语句有 2 种,initial 与 always 语句。它们是行为级建模的 2 种基本语句。
一个模块中可以包含多个 initial 和 always 语句,但 2 种语句不能嵌套使用。
这些语句在模块间并行执行,与其在模块的前后顺序没有关系。
但是 initial 语句或 always 语句内部可以理解为是顺序执行的(非阻塞赋值除外)。
每个 initial 语句或 always 语句都会产生一个独立的控制流,执行时间都是从 0 时刻开始。
initial语句
initial 语句从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的。
如果 initial 块内包含多个语句,需要使用关键字 begin 和 end 组成一个块语句。
如果 initial 块内只要一条语句,关键字 begin 和 end 可使用也可不使用。
initial 理论上来讲是不可综合的,多用于初始化、信号检测等。
always 语句
always 语句是重复执行的。always 语句块从 0 时刻开始执行;当执行完最后一条语句后,便再次执行语句块中的第一条语句,如此循环反复。多用于仿真时钟的产生,信号行为的检测等。
下面用 always 产生一个 100MHz 时钟源,并在 1010ns 时停止仿真代码。
`timescale 1ns/1ns
module test ;
parameter CLK_FREQ = 100 ; //100MHz
parameter CLK_CYCLE = 1e9 / (CLK_FREQ * 1e6) ; //switch to ns
reg clk ;
initial clk = 1'b0 ; //clk is initialized to "0"
always # (CLK_CYCLE/2) clk = ~clk ; //generating a real clock by reversing
always begin
#10;
if ($time >= 1000) begin
$finish ;
end
end
endmodule
8. 阻塞和无阻塞过程赋值
简单的阻塞过程赋值语句有如下三种形式:
lhs_expression = expression; //立即赋值
lhs_expression = #delay expression; //等待delay时间之后赋值
lhs_expression = @event expression; //等待event事件发生再赋值
无阻塞过程赋值
lhs_expression <= expression; //立即赋值
lhs_expression <= #delay expression; //等待delay时间之后赋值
lhs_expression <= @event expression; //等待event事件发生再赋值
二者的差别在于无阻塞赋值语句右端计算好后并不立即赋给左端,在要赋值的同时控制下一条语句的继续执行。
9. 任务(task)和函数(function)结构
Verilog语言中最有效的仿真方法就是将一段代码封闭起来形成任务或函数结构。
任务和函数之间有以下差异:
任务块可以含有时间控制结构,而函数块没有,也就是函数块从零仿真时刻开始运行,结束后立即返回。而任务块在继续下面的运行过程之前其初始化代码必须保持到任务全部执行结束或者失效
一个任务块可以有输入和输出,而一个函数块必须至少有一个输入,没有任何输出,函数结构通过自身名字返回结果
任务块的引发是通过一条语句,而函数块只有当它被引用在一个表达式中时才会生效
10. 时序控制
Verilog 提供了 2 种显式时序控制方法:延迟控制和事件控制。事件控制主要分为边沿触发事件控制与电平敏感事件控制,主要通过事件表达式来完成的,只有当某一事件发生才允许语句继续向下执行。
延时控制
它是将程序的执行过程中断一定时间,时间长度有expression的值来确定
//延时控制格式
# expression //等待时间之后赋值
事件
一个事件可以通过运行表达式:->enent 变量来被激发,事件控制用符号**@**表示。
关键字posedge是指信号发生边沿正向跳变、negedge是指信号发生负向边沿跳变,未指名跳变方向时,则2种情况的边沿变化都会触发相关事件。
//信号clk只要发生变化,就执行q<=d,双边沿D触发器模型
always @(clk) q <= d ;
//在信号clk上升沿时刻,执行q<=d,正边沿D触发器模型
always @(posedge clk) q <= d ;
//在信号clk下降沿时刻,执行q<=d,负边沿D触发器模型
always @(negedge clk) q <= d ;
//立刻计算d的值,并在clk上升沿时刻赋值给q,不推荐这种写法
q = @(posedge clk) d ;
命名事件控制
用户可以声明 event(事件)类型的变量,并触发该变量来识别该事件是否发生。命名事件用关键字 event 来声明,触发信号用 -> 表示。
event start_receiving ;
always @( posedge clk_samp) begin
-> start_receiving ; //采样时钟上升沿作为时间触发时刻
end
always @(start_receiving) begin
data_buf = {data_if[0], data_if[1]} ; //触发时刻,对多维数据整合
end
11. 语句块
Verilog 语句块提供了多条语句组成语法结构上相当于一条一句的机制。主要包括两种类型:顺序块和并行块。
顺序块
顺序块用关键字 begin 和 end 来表示。
顺序块中的语句是一条条执行的。当然,非阻塞赋值除外。
顺序块中每条语句的时延总是与其前面语句执行的时间相关。
在本节之前的仿真中,initial 块中的阻塞赋值,都是顺序块的实例。
并行块
并行块有关键字 fork 和 join 来表示。
并行块中的语句是并行执行的,即便是阻塞形式的赋值。
并行块中每条语句的时延都是与块语句开始执行的时间相关。
`timescale 1ns/1ns
module test ;
reg [3:0] ai_sequen, bi_sequen ;
reg [3:0] ai_paral, bi_paral ;
reg [3:0] ai_nonblk, bi_nonblk ;
//============================================================//
//(1)Sequence block
initial begin
#5 ai_sequen = 4'd5 ; //at 5ns
#5 bi_sequen = 4'd8 ; //at 10ns
end
//(2)fork block
initial fork
#5 ai_paral = 4'd5 ; //at 5ns
#5 bi_paral = 4'd8 ; //at 5ns
join
//(3)non-block block
initial fork
#5 ai_nonblk <= 4'd5 ; //at 5ns
#5 bi_nonblk <= 4'd8 ; //at 5ns
join
endmodule
12. 选择结构
if语句
Verilog HDL语句与C语言的十分相似,使用起来也很简单。
eg:
if ( a < 0 )
begin
b=1;
end;
else
begin
b=0;
end
case语句
与C语言的case语句不同,Verilog语言中,选择第一个与<表达式>的值匹配的<数值>,并执行相关的语句,然后控制指针将移到endcase语句之后,它不需要break语句。形式如下:
case (<表达式>)
<数值>:(语句)
<数值>:(语句)
default:(语句)
endcase
13.重复结构
重复结构包含for循环结构、while循环结构、repeat重复结构和forever循环结构
for循环结构
for循环语句与C语言的for循环语句非常相似,只是Verilog语句中没有增1++和减1–运算符,因此要使用i=i+1的形式。
eg:
module for_loop;
integer i;
initial
for (i=0;i<4;i=i+1) begin
$display ("i=%0d(%b binary)",i,i);
end
endmodule
while循环结构
eg:
module while_loop;
integer i;
initial begin
i=0;
while (i<4) begin
$display ("i=%0d(%b binary)",i,i);
i=i+1;
end
end
endmodule
repeat(重复)循环结构
/*等待5个时钟周期然后停止仿真*/
module repeat_loop (clock);
input clock;
initial begin
repeat (5)
@(posedge clock);
$stop
end
endmodule
forever循环结构
forever循环用来监控一些条件,当条件发生时显示一条信息。
module forever_statement(a,b,c);
input a,b,c;
initial forever begin;
@(a or b or c)]
if(a+b == c) begin
$display ("a(%d)+b(%d)=c(%d)",a,b,c);
$stop;
end
end
endmodule
14. 模块与端口
module(模块)例化语句
模块格式定义
module module_name
#(parameter_list)
(port_list) ;
Declarations_and_Statements ;
endmodule
模块定义必须以关键字 module 开始,以关键字 endmodule 结束。
模块名,端口信号,端口声明和可选的参数声明等,出现在设计使用的 Verilog 语句( Declarations_and_Statements)之前。
模块内部有可选的 5 部分组成,分别是变量声明,数据流语句,行为级语句,低层模块例化及任务和函数。这 5 部分出现顺序、出现位置都是任意的。但是,各种变量都应在使用之前声明。变量具体声明的位置不要求,但必须保证在使用之前的位置。
端口
端口是模块与外界交互的接口。对于外部环境来说,模块内部是不可见的,对模块的调用只能通过端口连接进行。
端口列表
模块的定义中包含一个可选的端口列表,一般将不带类型、不带位宽的信号变量罗列在模块声明里。下面是一个 PAD 模型的端口列表:
module pad(
DIN, OEN, PULL,DOUT, PAD);
一个模块如果和外部环境没有交互,则可以不用声明端口列表。
端口声明
(1) 端口信号在端口列表中罗列出来以后,就可以在模块实体中进行声明了。
根据端口的方向,端口类型有 3 种: 输入(input),输出(output)和双向端口(inout)。
input、inout 类型不能声明为 reg 数据类型,因为 reg 类型是用于保存数值的,而输入端口只能反映与其相连的外部信号的变化,不能保存这些信号的值。
output 可以声明为 wire 或 reg 数据类型。
//端口类型声明
input DIN, OEN ;
input [1:0] PULL ; //(00,01-dispull, 11-pullup, 10-pulldown)
inout PAD ; //pad value
output DOUT ; //pad load when pad configured as input
//端口数据类型声明
wire DIN, OEN ;
wire [1:0] PULL ;
wire PAD ;
reg DOUT ;