0%

只需xx元,一周学会Verilog HDL,看到赚到 +qq 就能领取Verilog大礼包(二)

基本语法结构与约定

概述

在本文中,将对 Verilog HDL 语言的基本语法结构与约定进行整理与总结,这些结构与约定构成了 Verilog 语言的基本框架
总体上大致分为词法约定、数据类型和系统任务与编译指令三个板块,其内容基本来自参考书籍与实际教学中所使用的PPT,准确程度较高
书上写得简洁明了的地方就直接照抄了,有些合适的地方也会进行补充
Verilog 是一门类 C 语言,所以有 C / C++ 编程经验的老哥们上手会极快(对吧?不会有老哥学了 C 听不懂 Verilog 吧?不会吧?)

复习(预习)本文应达到的学习目标:

理解操作符、注释、空白符、数字、字符串和标识符的词法约定;
能够定义逻辑值集合和数据类型,包括线网、寄存器、向量、数字、仿真时间、数组、参数、存储器和字符串;
能够使用基本的系统任务与编译指令,系统任务包括显示和监视信息、暂停和结束仿真与值变转储,编译指令包括宏定义、文件包含与时间尺度。

词法约定

Verilog 中的基本词法约定与 C 语言类似。Verilog 描述包含一个 “单词” 流,这里的单词可以是注释、分隔符、数字、字符串、标识符和关键字。Verilog 是大小写相关的,其中的关键字全部为小写。

空白符

空白符由空格(\b)、制表符(\t)和换行符组成。除了字符串中的空白符,Verilog 中的空白符仅仅用于分隔标识符,在编译阶段被忽略。

注释

Verilog 的注释与 C 语言大概一致,注意多行注释不允许嵌套。

1
2
3
4
5
6
// 单行注释

/* 多行
注释 */
/* 这是 /* 不合法的 */ 注释 */
/*这是 // 合法的注释*/

操作符

操作符有三种类型:单目操作符、双目操作符和三目操作符,其中,单目操作符的优先级高于操作数。

分别举例如下:

1
2
3
a = ~ b;        // 单目操作符
a = b && c; // 双目操作符
a = b ? c : d; // 三目操作符

之后再详细介绍操作符。。。。。。

数字声明

表示形式

数字声明的基本表示形式为:<size>’<base format><number>

其中 <size> 用于指明数字的位宽度,用十进制数表示;
   <base format>指基数,用于指定数字的进制;
   <number>指数字本身,对于不同的基数有不同的范围;

合法的基数格式:十进制('d 或 'D)十六进制('h 或 'H)二进制('b 或 'B)八进制('o 或 'O);

合法的数字格式:0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f(允许字母大写);

没有 <size> 即为不指明位数的数字,其默认的位宽度与仿真器与使用的计算机有关(最小为32位);
不指定基数则默认为十进制数;

1
2
3
4
5
6
4'b1111 // 4 位的二进制数
12'habc // 12 位的十六进制数
16'd255 // 16 位的十进制数
23456 // 32 位的十进制数
'hc3 // 32 位的十六进制数
'o21 // 32 位的八进制数

X 与 Z 值

Verilog 中:x 表示不确定值,z 表示高阻值

1
2
3
12'h13x // 四个最低位不确定
6'hx // 所有位不确定
32'bz // 32 位的高阻值

注意:如果某数的最高位为 0,x 或 z,Verilog 语言约定将分别使用这三个值对这个数进行拓展。

负数

在表示位宽的数字前加负号表示负数

下划线符号与问号

下划线 “_” 可以出现在数字除第一个字符外的任何位置,用于提高可读性,不影响编译;
问号 “?” 是 z(高阻值)的另一种表示。

1
2
12'b1111_0000_1010
4'b10?? // 4'b10zz

字符串

字符串是由双引号括起来的一个字符队列,不能包含回车符
Verilog 将字符串当作一个单字节的 ACSII 字符队列

1
"Hello Verilog World" //一个经典字符串

标识符和关键字

标识符是程序代码中对象的名字,我们使用标识符访问对象(即在描述设计时,模块、端口、实例等Verilog对象的名字);

Verilog 中的标识符由字母数字字符、下划线(_)和美元符($)组成,区分大小写;

Verilog 标识符的第一个字符必须是字母数字字符或下划线,不能以数字或美元符开始,以美元符开始的标识符是为系统函数保留的(即标识符必须以字母a-z, A-Z或 _ 开头,后面可以是字母、数字、 $ 或 _ )。

关键字是语言中预留的用于定义语言结构的特殊标识符;
Verilog 中的关键字全部小写。

1
2
reg value;      // reg 是关键字;value 是标识符
input clk; // input 是关键字;clk 是标识符

Verilog 常用可综合关键字

module endmodule input output inout
wire reg parameter always assign
if else begin end case
endcase posedge negedge or default

注:所谓可综合,就是我们编写的Verilog代码能够被综合器转化为相应的电路结构。因此我们常用可综合语句来描述数字硬件电路。

转义标识符

以反斜线 “\” 开始,以空白符结束

数据类型

Verilog 提供了我们几种可供使用的数据类型
主要数据类型有三类:
net (线网):表示器件之间的物理连接;
register (寄存器) :表示抽象的存储元件;
parameters(参数):运行时的常数。

值的种类

Verilog 使用四值逻辑和八种信号强度来对实际的硬件电路建模。

四值电平逻辑

值 的 级 别 硬件电路中的条件
0 逻辑0,条件为假
1 逻辑1,条件为真
x 逻辑值不确定
z 高阻,浮动状态

除逻辑值外,Verilog 还使用强度值来解决数字电路中不同强度的驱动源之间的赋值冲突,这里不作说明。

线网

avatar

线网(net)表示硬件单元之间的连接。就像在真实的电路中一样,线网由其连接器件的输出端连续驱动,一般使用关键字 wire 进行声明,其默认值为 z ,线网的值由其驱动源确定,若无驱动源则为默认值。

如果没有显式地说明为向量,则默认线网的位宽为 1 。

1
2
3
wire a;         // 声明上面的电路中 a 是 wire(连线)类型
wire b, c; // 声明上面的电路中 b 和 c 也是 wire(连线)类型
wire d = 1'b0; // 连线 d 在声明时,d 被赋值为逻辑值 0

寄存器

avatar

寄存器用来表示存储元件,它保持原有的数值,直到被改写,一般使用关键字 reg 进行声明,其默认值为 x ;
这里的寄存器与实际电路中的由边沿触发的触发器构成的硬件寄存器不同,这里的寄存器 register 仅意味着声明一个保存数值的变量;
wire 需要驱动源,而 register 不需要;
在仿真过程中的任意时刻,寄存器的值都可以通过赋值来改变。

1
2
3
4
5
6
reg reset;              // 声明 register 类型的变量 reset
initial
begin
reset = 1'b1; // 初始化 reset 为 1
#100 reset = 1'b0; // 将 reset 置为 0
end

寄存器类型变量若需用于带符号的算术运算,则应声明为带符号(signed)类型的变量

1
reg signed [63:0] m;

向量

线网和寄存器类型的数据均可以声明为向量(即位宽大于 1 );
如果在声明中没有指定位宽,则默认为标量( 1 位);

向量通过[high#:low#]或[low#:high#]进行说明,方括号中左边的数总是代表最高有效位。

1
2
3
4
5
6
7
wire a;         // 标量线网变量,默认
wire [7:0] bus; // 8 位的总线

wire [31:0] busA, busB, busC; // 3 条 32 位宽的总线
reg clock; // 标量寄存器,默认
reg [0:40] virtual_addr; // 向量寄存器,41 位宽的虚拟地址
// 其最高有效位为第 0 位

向量域选择

可指定向量的某一位或是若干个相邻位;
高位写在范围说明的左侧。

1
2
3
busA[7]                 // 向量 busA 的第 7 位
bus[2:0] // 向量 bus 的最低三位
virtual_addr[0:1] // 向量 virtual_addr 的最高两位

可变的向量域选择

允许指定可变的向量域选择,如此可结合 for 循环来动态地选取向量的各个域;

[<starting_bit>+ : width]:从起始位开始递增,位宽为 width
[<starting_bit>- : width]:从起始位开始递减,位宽为 width

起始位可以是一个变量,但位宽须为常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
reg [255:0] data1;
reg [0:255] data2;
reg [7:0] byte;

byte = data1[31-:8]; // data1[31:24]
byte = data1[24+:8]; // data1[31:24]
byte = data2[31-:8]; // data2[24:31]
byte = data2[24+:8]; // data2[24:31]

for (j = 0; j <= 31; j=j+1)
byte = data1[(j*8)+:8]; // [7:0],[15:8]...[255:248]

data1[(byteNum*8)+:8] = 8'b0; // 若 byteNum = 1,共有八位被清零,[15:8]

整数、实数和时间寄存器数据类型

除 reg 类型之外,Verilog 还支持 integer,real,time 寄存器数据类型

avatar

整数

整数是一种通用的寄存器数据类型,用于对数量进行操作,使用关键字 integer 进行声明;
声明一个 integer 类型的变量(有符号数)来完成计数等功能比 reg 类型(无符号数)更为方便;
整数的默认位宽为宿主机的字的位数,最小为 32 位;

1
2
3
integer counter;
initial
counter = -1;

实数

实常量和实数寄存器数据类型使用关键字 real 来声明,可以用十进制或科学记数法来表示;
实数声明不能带有范围,其默认值为 0 ;
如将一实数赋给一整数,则实数被取为最接近的整数。

1
2
3
4
5
6
7
8
9
real delta;
initial
begin
delta = 4e10;
delta = 2.13;
end
integer i;
initial
i = delta; // i = 2

时间寄存器

保存仿真时间,通过关键字 time 进行声明,其单位为秒,表示为 s;
其宽度与具体实现有关,最小 64 位;
调用系统函数 $time 可以得到当前的仿真时间;

1
2
3
time save_sim_time;
initial
save_sim_time = $time; // 记录当前的仿真时间记录下来

数组

在 Verilog 中允许声明reg,integer,time,real,realtime 及其向量类型的数组,数组可以是任意维度的;
线网数组也可用于连接实例的端口,数组中的每个元素都可以作为一个标量或向量,以同样的方式来使用,形如 <数组名>[<下标>]。对于多维数组来讲,用户需要说明其每一维的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
integer count [0:7];        // 由 8 个计数变量组成的数组

reg bool [31:0]; // 由 32 个 1 位的布尔(Boolean)寄存器变量组成的数组

time chk_point[1:100]; // 由 100 个时间检查变量组成的数组

reg [4:0] port_id[0:7]; // 由 8 个端口标识变量组成的数组,端口变量的位宽为 5

integer matrix[4:0][0:255]; // 二维的整数型数组

reg [63:0] array_4d [15:0][7:0][7:0][255:0];
// 四维 64 位寄存器型数组
wire [7:0] w_array2 [5:0] // 声明 8 位向量的数组

wire w_array1[7:0][5:0]; // 声明 1 位线型变量的二维数组

注意,不要将数组和线网或寄存器向量混淆起来。向量是一个单独的元件,它的位宽为 n;数组由多个元件组成,其中的每个元件的位宽为 n 或 1。

对数组元素赋值的实例

1
2
3
4
5
6
7
8
9
10
count[5] = 0;
chk_point[100] = 0;
port_id[3] = 0;

matrix[1][0] = 33559;
array_4d[0][0][0][15:0] = 0;

/***** 非法 *****/
port_id = 0; // 非法,企图写整个数组
matrix [1] = 0; // 非法,企图写数组的整个第二行,即从 matrix[1][0] 直到 matrix[1][255]

存储器

在 Verilog 中,使用寄存器的一维数组来表示存储器(对存储器建模)

reg [MSB : LSB] <memory_name> [first_addr : last_addr];
  [MSB : LSB]定义存储器字的位数
  [first_addr : last_addr]定义存储器的深度

1
2
3
reg mem1bit[0:1023];            // 1 K 的 1 位存储器 mem1bit
reg [7:0] membyte[0:1023]; // 1 K 的字节(8 位)存储器 membyte
membyte[511] // 取出存储器 membyte 中地址 511 处所存的字节

参数

Verilog 允许使用关键字 parameter 在模块内定义常数;
parameter <list_of_assignment>
参数代表常数,不能像变量那样赋值,但是每个模块实例的参数值可以在编译阶段被重载,通过参数重载使得用户可以对模块实例进行定制;
可一次定义多个参数,用逗号隔开;
参数的定义是局部的,只在当前模块中有效。参数定义可使用以前定义的整数和实数参数;

1
2
3
parameter port_id = 5;
parameter cache_line_width = 256;
parameter signed [15:0] WIDTH;

Verilog 中的局部参数使用关键字 localparam 来定义,其作用等同于参数,区别在于它的值不能改变;
例如状态机的状态编码不能被修改,为避免被意外地更改,应当将其定义为局部参数。

1
2
3
4
localparam state1 = 4'b0001;
state2 = 4'b0010;
state3 = 4'b0100;
state4 = 4'b1000;

字符串

字符串保存在 reg 类型的变量中,每个字符占用 8 位(一个字节);
寄存器变量的宽度应足够大,保证能容纳全部字符;
如果寄存器变量的宽度大于字符串的大小(位),则 Verilog 使用 0 来填充左边的空余位;
如果寄存器变量的宽度小于字符串的大小(位),则 Verilog 截去字符串最左边的位;

1
2
3
reg [8*18:1] string_value; //其宽度为 18 个字节
initial
string_value = "Hello Verilog World";

加前缀转义字符的特殊字符

转义字符 显示的字符
\n 换行
\t tab(制表空格)
%% %
\\ \
\" "
\ooo 1 到 3 个八进制数字字符

系统任务与编译指令

Verilog 中的两个特殊概念:系统任务与编译指令

系统任务

Verilog 为某些常用操作提供了标准的系统任务(也称系统函数),这些操作包括屏幕显示、线网值动态监视、暂停和结束仿真等;
系统任务的形式:$<keyword>

显示信息

$display

用于显示变量、字符串或表达式的主要系统任务,其用法与 C 语言中的 printf 函数极其相似
$display 会自动在字符串的结尾处插入一个换行符;

如果参数列表为空,则 $display 的效果为显示光标移到下一行;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 显示小括号中的字符串
$display("Hello Verilog World"); //又见 Hello World!
-- Hello Verilog World

// 显示当前的仿真时间
$display($time);
-- 230

// 在时间为 200 的时刻,显示 41 位虚拟地址 1fe0000001c
reg [0:40] virtual_addr;
$display("At time %d virtual address is %h", $time, Virtual_addr);
-- At time 200 virtual address is 1fe0000001c

// 用二进制数显示 port_id 5
$display("ID of the port is %b", port_id);
-- ID of the port is 00101

// 显示 x 字符
// 用二进制数显示 4 位总线 bus 的信号值 10xx
reg [3:0] bus;
$display("Bus value is %b", bus);
-- Bus value is 10xx

// 在名为 top 的最高层模块中显示在该层被调用的实例 p1 的层次名
$display("This string is displayed from %m level of hierarchy");
-- This string is displayed from top.p1 level of hierarchy

字符串格式说明

       
 %d 或 %D   用十进制显示变量
 %b 或 %B   用二进制显示变量
 %s 或 %S   显示字符串
 %h 或 %H   用十六进制显示变量
 %c 或 %C   显示 ASCII 字符
 %m 或 %M   显示层次名
 %v 或 %V   显示强度
 %o 或 %O   用八进制显示变量
 %t 或 %T   显示当前时间格式
 %e 或 %E   用科学记数法格式显示实数
 %f 或 %F   用十进制浮点数格式显示实数
 %g 或 %G   用科学记数法或十进制格式显示实数,显示较短的格式

监视信息

$monitor

用于对信号值变化进行动态监视的手段,其格式与之前的 $display 基本一致;

系统函数 $monitor 对其参数列表中的变量值或信号值进行不间断的监视,当其中任何一个发生变化的时候,显示所有参数的数值;$monitor 只需调用一次即可在整个仿真过程中生效,这一点与 $display 不同;

由于 $monitor 在整个仿真过程中有效,因此在任意仿真时刻只有一个监视列表有效,即如果调用多个只有最后一次调用有效;

还可以使用 $monitoroff;$monitoron; 来控制监视的暂停与允许继续执行(仿真开始时的默认状态为允许监视);

1
2
3
4
5
6
7
8
9
10
11
12
// 监视时钟和复位信号的时间和值
// 时钟每 5 个时间单位翻转一次,复位信号 10 个时间单位后变低
initial

begin
$monitor($time, " Value of signals clock = %b reset = %b", clock, reset);
end

// 监视语句的部分输出:
-- 0 Value of signals clock = 0 reset = 1
-- 5 Value of signals clock = 1 reset = 1
-- 10 Value of signals clock = 0 reset = 0

暂停与结束仿真

$stop

用与暂停仿真,方便对设计进行调试,用法:$stop;

$finish

用于结束仿真,用法:$finish;

1
2
3
4
5
6
7
8
9
// 在仿真时刻为 100 单位时暂停仿真,检查运行结果
// 在仿真时刻为 1000 单位时结束仿真
initial
begin
clock = 0;
reset = 1;
#100 $stop;
#1000 $finish;
end

值变转储(VCD文件系统任务)

值变转储文件(VCD) 是一个 ASCII 文件,它包含仿真时间、范围与信号的定义以及仿真运行过程中信号值的变化等信息。设计中的所有信号或者选定的信号集合在仿真过程中都可以被写入 VCD 文件。

后处理工具 可以把 VCD 文件作为输入并把层次信息、信号值和信号波形显示出来。

Verilog 提供了系统任务来选择要转储的模块实例或者模块实例信号($dumpvars),选择 VCD 文件的名称($dumpfile),选择转储过程的起点和终点($dumpon$dumpoff),选择生成检测点($dumpall)等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 指定 VCD 文件名。若不指定 VCD 文件,则由仿真器指定一个默认文件名
initial
$dumpfile("myfile.dmp"); // 仿真信息转储到 myfile.dmp 文件

// 转储模块中的信号
initial
$dumpvars; // 没有指定变量范围,把设计中的全部信号都转储
initial
$dumpvars(1, top); // 转储模块实例 top 中的信号
// 数 1 表示层次的等级,只转储 top 下的第一层信号,即转储 top 模块
// 中的变量,而不转储 top 中的调用模块的变量

initial
$dumpvars(2, top.m1); // 转储 top.m1 模块以下两层的信号
initial
$dumpvars(0, top.m1); // 数 0 表示转储 top.m1 模块下面各个层的所有信号

// 启动和停止转储过程
initial
begin
$dumpon; // 启动转储过程
#100000 $dumpoff; // 1 000 000 个仿真时间单位后,停止转储过程
end

// 生成一个检查点,转储所有 VCD 变量的当前值
initial
$dumpall;

编译指令

Verilog 提供了一些编译指令供用户使用,其使用方式为:`<keyword>;

这里介绍几种常用的编译指令:

1
2
3
`define     // 宏定义
`include // 文件包含
`timescale // 时间尺度

关于条件编译,emmm… 现在也用不上

宏定义

编译指令 `define 用于定义 Verilog 中的文本宏;

`define <macro_name> <macro_text>

在编译阶段,当编译器遇到 `<宏名> 时,使用预定义的文本宏进行替换,它类似于 C 语言中的 #define 结构;

在使用预定义的常数或文本宏时,在宏名前加上前缀号 “ ` ” 。

1
2
3
4
5
6
7
8
9
10
// 规定字长的文本宏
// 在代码中用 `WORD_SIZE 表示
`define WOED_SIZE 32

// 定义别名,可以用 `S 来代替 $stop ;
`define S $stop;

// 定义经常使用的字符串
`define WORD_REG reg [31:0]
// 就可以用 `WORD_REG reg32 来定义一个 32 位的寄存器变量

文件包含

编译指令 `include 可在编译期间将一个 Verilog 源文件包含在另一个 Verilog 文件中,作用类似于 C 语言中的 #include 结构,其后应接双引号" ",双引号中可以是相对路径或绝对路径。

该指令通常用于将内含全局或公用定义的头文件包含在设计文件中。

1
2
3
4
5
6
7
8
// 包含 header.v 文件,在该文件中有主 Verilog 文件 design.v 需要的内容

`include "header.v"
...
...
<design.v 文件中的 Verilog 代码>
...
...

时间尺度

Verilog HDL 允许用 `timescale 编译指令为模块指定参考时间单位;

`timescale<reference_time_unit>/<time_precision>

<reference_time_unit>(参考时间单位)指定时间和延迟的测量单位;
<time_precision>(时间精度)指定仿真过程中延迟值进位取整的精度;

“时间精度”是仿真器的仿真时间步,不能大于“时间单位”;
但如果“时间单位”与“时间精度”差别很大可能会影响仿真的速度。如 `timescale 1s / 1ps,仿真器在 1 秒内要扫描其事件序列 1e12 次;

而 `timescale 1s/1ms 则只需扫描 1e3 次;

只有 1 ,10 ,100 才是合法的说明时间单位和时间精度的整数,单位可以是s, ms, us, ns, ps, fs;

`timescale 必须在模块之前出现;

如果没有timescale说明将使用仿真软件的缺省值;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 为模块 dummy1 定义时间尺度

// 参考时间单位为 100 ns,精度为 1 ns
`timescale 100 ns / 1 ns

module dummy;

reg toggle;

// 对 toggle 变量进行初始化
initial
toggle = 1'b0;

// 每 5 个时间单位把 toggle 寄存器翻转一次

// 本模块中 5 个时间单位 = 500 ns = 0.5 μs
always #5
begin
toggle = ~toggle;
$display("%d , In %m toggle = %b ", $time, toggle);
end

endmodule

// 为模块 dummy2 定义时间尺度
// 参考时间单位为 1 μs,精度为 10 ns
`timescale 1 us / 10 ns

module dummy2;

reg toggle;

// 对 toggle 变量进行初始化
initial
toggle = 1'b0;

// 每 5 个时间单位把 toggle 寄存器翻转一次

// 本模块中 5 个时间单位 = 5 μs = 5000 ns
always #5
begin
toggle = ~toggle;
$display("%d , In %m toggle = %b ", $time, toggle);
end

endmodule

部分仿真输出结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
5 , In dummy toggle = 1
10 , In dummy toggle = 0
15 , In dummy toggle = 1
20 , In dummy toggle = 0
25 , In dummy toggle = 1
30 , In dummy toggle = 0
35 , In dummy toggle = 1
40 , In dummy toggle = 0
45 , In dummy toggle = 1
5 , In dummy2 toggle = 1
50 , In dummy toggle = 0
55 , In dummy toggle = 1
60 , In dummy toggle = 0
65 , In dummy toggle = 1
70 , In dummy toggle = 0
75 , In dummy toggle = 1
80 , In dummy toggle = 0
85 , In dummy toggle = 1
90 , In dummy toggle = 0
95 , In dummy toggle = 1
10 , In dummy2 toggle = 0
100 , In dummy toggle = 0
105 , In dummy toggle = 1
......

参考文献

Verilog HDL数字设计与综合:第二版:本科教学版/(美)帕尔尼卡(Palnitkar,s.)著;夏宇闻等译 . 北京:电子工业出版社,2015.8

[Verilog的基础语法]https://blog.csdn.net/xinghuanmeiying/article/details/101022071

[Verilog基础语法总结]https://www.cnblogs.com/linkzijun/p/7603735.html