前言

这是一种计算机硬件语言,是可以模拟计算机的一种仿真语言,语法上与 C 语言相似。本文主要是笔者为了学习《计算机组成原理》而去了解的一种语言

介绍

Verilog 是一种硬件描述语言,用于数字电路的系统设计。可对算法级、门级、开关级等多种抽象设计层次进行建模

继承了C语言的多种操作符和结构,与另一种硬件描述语言 VHDL 相比,语法不是很严格,代码更加简洁,更容易上手

Verilog 不仅定义了语法,还对语法结构都定义了清晰的仿真语义。因此, Verilog 编写的数字模型就能够使用 Verilog 仿真器进行验证

Verilog 环境搭配

在终端使用指令来安装

1
sudo apt-get install iverilog

配置 vscode

安装插件

  • waveTrace 插件
  • verilog-HDL 插件
  • Verilog Format 插件

配置

  1. 在 linux 上安装 ctags
  2. 找到 ctags 的安装路径,将其添加到 vscode 的设置 Verilog>ctags:Path 中
  3. 将 vscode 中设置的 Verilog>linting:Liner 设置为 iverilog
  4. 在 vscode 的设置 verilog>linting>iverilog:Arguments 中填入 -i ,然后就配置完成了

编译运行

对于 verilog 语言,文件的后缀是 .v ,并且需要专有的编译器来进行编译

1
iverilog xxx.v

编译结束之后,会生成 a.out 文件,可以使用指令来指定输出的文件名称的。然后在终端输入

1
./a.out

来运行

设计方法

Verilog 采取从上到下的设计方法。需要先定义顶层模块功能,进而分析要构成顶层模块的必要子模块,然后进一步对各个模块进行分解设计,直到到达无法进一步分解的底层功能块。可以把一个较大的系统,细化为多个小系统

vlg-design-method-1.png

预编译指令

与 C 语言不同的是, verilog 使用 `(反引号)来代替了 C 语言中的 #

  • `define 定义

  • `undef 取消定义

  • `ifdef 判断是否定义

  • `ifndef 判断是否未定义

  • `include 包含某个模块/文件

  • `timescale 将时间单位与实际时间相关联。该指令用于定义时延、仿真的单位和精度,格式如下

    1
    `timescale time_unit / time_precision

    其中

    • time_unit 表示时间的单位,由数字以及单位组成(s, ms, us, ns, ps, fs)。但是时间精度大小不能超过时间单位大小
    • time_precision 表示时间的精度
  • ``default_nettype` 该指令用于为隐式的线网变量指定为线网类型,即将没有被声明的连线定义为线网类型,用法如下

    1
    `default_nettype wand 
  • resetall` 该编译器指令将所有的编译指令重新设置为缺省值,可以使得缺省连线类型为线网类型。加到模块最后时,可以将当前的 ****timescale取消防止进一步传递,只保证当前的 ``timescale 在局部有效,避免 ``timescale` 的错误继承。

  • ``celldefine` 将模块标记为单元模块,他们包含模块的定义

  • ``endcelldefine` 结束单元模块

  • ``unconnected_drive, ****nounconnected_drive 出现在这两个编译指令间的任何未连接的输入端口,为正偏电路状态或者为反偏电路状态

数据类型

数据类型总的上分两类

  • 线网类 net 类
    • wire 线型——最常用
    • wand 线与
    • wor 线或
    • tri 三态
    • triand 三态与
    • trior 三态或
    • tri0 下拉电阻
    • tri1 上拉电阻
    • trireg 电容性电网
    • supply0 地
    • supply1 电源
  • 变量类 variable 类
    • reg 寄存器类型——最常用
    • integer 整型
    • real 实型
    • time 时间型
    • realtime 实时时间型

因为连续赋值语句和过程赋值语句的激活特点不同,故而赋值目标特点也不同。前者不需要保存,后者需要保存,因此规定两种数据类型。 net 类用于连续赋值目标或者门原语的输出,且仿真时不需分配内存空间, variable 类用于过程赋值语句,且仿真时需要分配内存空间

将一个信号定义为 net 类型还是 varibke 类型,由以下两个方面决定

1713198541998.png

  1. 对于端口信号来说, input 信号和 inout 信号必须定义为 net 类型的,而 output 信号可以是 net 类型也可以是 varible 类型,取决于如何赋值
  2. 使用何种赋值语句对该信号进行赋值。如果是连续赋值或者门原语句赋值或例化语句赋值,则定义为 net 型,如果是过程赋值则定义成 varible 型

线网wire

表示硬件单元之间的物理连线,由其连接的器件输出端连续驱动,如果没有驱动元件连接到线网类型,缺省一般为 z

线网型还有其他数据类型,包括 wand , wor , wri , triand , trior , trireg 等。这些数据类型用的频率不是很高

1
wire interput;

寄存器reg

用来表示存储单元,会保持数据原有的值,直到被改写

1
ref rax;

在 always 块中,寄存器可能被综合成边沿触发器,在组合逻辑中可能被综合成 wire 型变量。寄存器不需要驱动源,也不一定需要时钟信号。在仿真时,寄存器的值可在任意时刻通过赋值操作进行改写。

1
2
3
4
5
6
reg rstn ;
initial begin
rstn = 1'b0;
#100;
rstn = 1'b1;
end

向量

当位款大于 1 时, wire 或者 reg 可以声明为向量的形式,声明的形式为 type[end:begin] 其中是包含 end 和 begin 位的

1
reg[7:0] num;    //声明 8 位宽的寄存器 num

对于上面的向量,我们可以指定某一位或若干相邻位,作为其他逻辑使用,使用的形式为 data[begin:end] 其中是包含 end 和 begin 位的

1
2
wire [9:0] data_low = data[0:9] ;
addr_temp[3:2] = addr[8:7] + 1'b1 ;

verilog 支持指定位之后固定位宽的向量域选择访问

  • [bit+:width] 从起始 bit 位开始递增,位宽为 width
  • [bit-:width] 从起始 bit 开始递减,位宽为 width
1
2
a = data1[31-: 8] // == data1[31, 24]
a = data1[24+: 8] // == data1[31, 24]

信号可以重新组合成新的向量,需要使用大括号

1
2
3
wire[31: 0] data1, data2;
assign data1 = {byte1[0][7: 0], data1[31: 8]}; // 数据拼接
assign data2 = {32{1'b0}}; // 赋值

整数

使用 integer 表示。声明时不用指明位宽,位宽和编译器有关,一般为 32bit。 与 reg 的区别在于: reg 型变量为无符号数,而 integer 型变量为有符号数

integer 可用来作为辅助信号

1
2
3
4
5
integer j;
...
for (j = 0; j <= 3; j = j + 1) begin
...
end

整数数值格式

合法的数值格式有 4 种,包括十进制(d或D),十六进制(h或H),二进制(b或B),八进制(o或O),默认为十进制数据。数值可指明位宽,也可不指明

  • 无符号数表示格式为

    <位宽>’<进制><数字>

  • 有符号数表示格式为,是用补码格式来表示的

    <位宽>’s<进制><数字>

在数字的进制格式之前添加 n' 来指明是 n 位的数据,其中可以在每八位中间添加下划线,以此来增强代码可读性

如果不指明位宽,一般会根据编译器自动分频位宽,常见的为 32bit ,通常在数值前加符号表示负号

可以使用实数表示,也就是用小数表示,可以使用科学计数法。

实数

使用 real 来声明,可用十进制或者科学计数法表示,实数声明不能带有范围,默认位 0 ,如果实数数据赋值给整数,会将数据截断

1
2
3
4
5
6
real data1;
integer data2;
...
data1 = 1e3;
data1 = 12.13;
data2 = data1; // 12

时间

verilog 使用特殊的时间寄存器 time 型变量,对仿真时间进行保存。宽度一般为 64 位,通过条用系统函数 $time 获取当前仿真时间

1
2
3
time current;
...
current = $time;

数组

在 Verilog 中允许声明 reg wire integer time real 及其向量类型的数组,数组中的每个元素都可以作为一个标量或者向量,以用样的方式来使用

声明的形式为 data[end:begin] 其中是包含 end 和 begin 的

1
2
integer num[7:0]
num[0] = 1;

存储器

存储器变量就是一种寄存器数组,可用来描述 RAM 或 ROM 的行为,使用 reg 变量来声明

1
2
3
reg membit[0: 255];
reg[7: 0] mem[0: 1023]
mem[511] = 8'b0;

参数

参数用来表示常量,用关键字 parameter 声明,只能赋值一次

1
parameter data = 32‘8bbb_bbbb;

当参数只在某个模块内调用,需要使用 localparameter 来声明

字符串

字符串保存在 reg 类型中,每个字符占一个字节,所以寄存器变量的宽度应该足够大以不至于溢出

字符串是使用双引号包起来的字符队列,字符串不能多行书写,也不能包含回车符。实际上是把字符串作为字符的队列

数值

有四种基本的值来表示硬件电路中的电平逻辑

  • 0:逻辑 0 或者假
  • 1:逻辑 1 或者真
  • x 或 X:未知,意味着信号数值不确定,即在实际电路里,信号可能为 1,也可能为 0
  • z 或 Z:高阻,意味着信号处于高阻状态,常见于信号(input, reg)没有驱动时的逻辑结果。例如一个 pad 的 input 呈现高阻状态时,其逻辑值和上下拉的状态有关系。上拉则逻辑值为 1,下拉则为 0

逻辑值

  • 1 逻辑 1,高电平,数字 1
  • 0 逻辑 0,低电平,数字 0
  • x 不确定
  • z 高阻态

基础语法

注释

使用 // 进行单行注释,使用 /**/ 进行多行注释

标识符与关键字

标识符(identifier)可以是任意一组字母,数字, $ 符号和 _ (下划线)符号的组合,但标识符的第一个字符必须是字母或者下划线,不能以数字或者 $ 符开始。标识符是大小写敏感的。

关键字是 verilog 中预留的一些特殊标识符,关键字全部为小写

if语句

  • if(condition)...
  • if(condition)...else...
  • if(condition1)...else if(condition2)...
  • if(condition1)...else if(condition2)...else...

需要注意的是,在使用 if 语句设计组合电路时,如果条件不完整,会综合出寄存器,有两种使条件完整的方法

  1. 加 else
  2. 设定初始值

计算表达式 condition 可以是任意形式的表达式,条件表达式的结果只有 0 或 1,如果计算表达式的结果为 0,则条件表达式的值为 0,否则为 1

case语句

格式为

1
2
3
4
5
case (state)
1: ...;
2: ...;
default:...
endcase

与 C 语言类似,同时也可以没有 default 语句

描述方式

  • 结构化描述,全部用门原语和底层模块调用
  • 数据流级描述,全部使用 assign 语句
  • 行为级描述,全部使用 always 语句配合 if 和 case 语句
  • RTL 级别描述方式,数据流级+行为级,可综合

而实际的描述是三种混合的

打印输出

  • $display 使用方法和 C 语言中的 printf 函数非常类似,可以直接打印字符串,也可以在字符串中指定变量的格式对相关变量进行打印。如果没有指定变量的显示格式,变量值会根据在字符串的位置显示出来,相当于参与了字符串连接。

    如果没有指定格式, $display 默认显示是十进制。 $displayb , $displayo , $displayh 显示格式分别为二进制、八进制、十六进制。同理也有 $writeb , $writeo , $writeh, $strobeb 等。

    下表是常用的格式说明

  • $write

  • $strobe

  • $monitor

1
2
3
$display("This is a test.");   //直接打印字符串
$display("This is a test number: %b.", num); //打印变量 num 为二进制格式
$display("This is a test number: ", num, "!!!");

时延

连续赋值延时语句中的延时,用于控制任意操作数发生变化到语句左端赋予新值之间的时间延时,时延一般是不可综合的,寄存器的时延也是可以控制的,连续赋值时延分为三种

  • 普通赋值时延

    1
    2
    wire Z, A, B;
    assign #10 Z = A & B;
  • 隐式时延

    1
    2
    wire A, B;
    wire #10 Z = A & B;
  • 声明时延

    1
    2
    3
    wire A, B;
    wire #10 Z;
    assign Z = A & B;

惯性时延

对于上述的代码,当在延时的过程中, A 和 B 的值又发生了一次变化,那么计算的新值 Z 会取最新的 A 和 B 进行计算

表达式

表达式由操作数和操作符构成,目的是根据操作符的意义得到一个计算结果,表达式可以在出现数值的任何地方使用,由于很多语法是与 C 语言几乎一致的,所以这里就只介绍一些没见过的

需要注意的是, verilog 中的逻辑值有三个状态 0 和 1 , x 。其中 x 就是不确定值

等价运算符

  • =
  • !=
  • ===
  • !==

对于后两个,它在对操作数进行比较时对某些位的不定值 x 和高阻值 z 也进行比较,两个操作数必需完全一致,其结果才是1,否则为0

算符常用于 case 表达式的判别,所以又称为"case等式运算符"

这四个等式运算符的优先级别是相同的

移位运算

  • >> 左移
  • << 右移
  • >>> 算术左移
  • <<< 算术右移

注意

  • 移位运算的操作数是一位或者多位二进制数
  • 向左或者向右移动 n 位
  • 只有对有符号数的算数右移自动补符号位,其它均补 0

按位运算符

  • ~ 按位取反
  • & 按位与
  • | 或
  • ^ 异或
  • ^~ 或者 ~^ 同或

注意

  1. 按位运算的操作数是 1 位或者多位二进制数
  2. 如果操作数的位宽不同,位宽小的会自动在左端补 0
  3. 结果与操作数的位宽相同

缩减运算符

  • & 与
  • ~& 与非
  • | 或
  • ~| 或非
  • ^ 异或
  • ^~ 或者 ~^ 同或

注意

  1. 缩减运算符的操作数是 1 位或者多位二进制数
  2. 缩减运算符的操作数只有一个,将该数的各位自左至右进行逻辑运算,结果只有一位

拼接运算符

使用 {} 进行运算符的拼接,可以将两个信号接在一起

拼接复制运算符

使用 {n{}} 运算符实现

可以将第二个括号内的操作数复制 n 次然后拼接

与上述拼接运算符可以组合使用,例如

1
A = {2{B, C, D}, E, F};

模块

声明

1
2
3
4
5
module module_name(端口列表);
[端口信号声明];
[参数声明];
内部信号声明\底层模块或门源语调用\assign语句\always语句
endmodule;

其中

  • 端口列表是指电路的输入,输出信号的名称列表,信号名由用户指定,中间用逗号隔开
  • 端口信号声明是要说明端口信号的输入输出属性,信号的数据类型,以及信号的位宽。输入输出属性有 input , output , inout 三种,信号的数据类型通常用 wire 和 reg 两种,信号的位宽用 [begin:end] 表示。同一类信号之间用逗号隔开
  • 参数声明需要说明参数的名称和初值
  • 参数声明中,如果位宽不做说明,默认是 1 位
  • 数据类型不做声明,默认是 wire 类型

assign语句

assign 语句称作连续赋值语句,基本格式为

1
assign dst = src;

之所是是连续赋值语句,就是因为其总是处于激活状态,一旦 src 表达式中的操作数有更改,就会立即进行计算和赋值

赋值目标 dst 必须是 wire 类型的,表示电路之间的连线

always语句块

always 称为过程块,基本格式为

1
2
always @(敏感信号条件表)
各类顺序语句

特点

  • always 语句本身不是单一的有意义的一条语句,而是和下面的语句一起构成一个语句块,所以叫做过程块。过程块中的赋值语句就叫做过程赋值语句
  • 该语句块不是总处于激活的状态的,当满足激活条件才会执行,否则被挂起,挂起时该语句块不执行
  • 赋值目标必须是 reg 类型的
  • 激活条件由敏感信号条件表决定的,当敏感条件满足时,过程块激活。其中敏感条件有两种
    • 边沿敏感:
      • posedge signalName 信号上升沿到来
      • negedge signalName 信号下降沿到来
    • 电平敏感
      • (signalNameList) 信号列表中任意一个信号有电平变化就会执行代码块,其中信号名之间用逗号或者 or 来隔开
  • 在 always 中还可以使用 if , else , for 循环等语句

assign与always的区别

  • 连续赋值语句总是处于激活状态,只要有操作数变化就马上进行计算和赋值
  • 过程赋值语句只有当激活该过程时,才会进行计算和赋值,如果该过程不被激活,即使操作数发生变化也不会计算和赋值
  • verilog 规定 assign 中的赋值目标必须是 wire 类型,而且 always 语句中的赋值目标必须是 reg 类型的
  • always 中还可以使用 if , else , for 循环等语句,但是 assign 不能使用
  • always 中如果有多条语句必须使用 begin 和 end 包括起来,但是 assign 中没有

initial语句

从 0 时刻开始执行,只执行一次,多个 initial 块之间是相互独立的,如果有多条语句,就需要使用 begin 和 end 包括起来。

理论上讲是不可综合的,多用于初始化和信号检测等, initial 中的语句是顺序执行的

底层模块的调用

调用底层模块需要使用 ``include “xx.v”` 语句来调用,需要将对应的模块实例化,并且调用时实例化名称不能省略

1
xx a(ports);

调用底层模块时,需要把对应的端口的输入输出填写,所以就需要端口映射,有两种方法

  • 端口名关联法(命名法)
    • (.底层端口名1(外接信号名1), .底层端口名2(外接信号名2), ...)
    • 不需要按照底层模块信号列表顺序书写
  • 位置关联法(顺序法)
    • (外接口信号1, 外接口信号2)
    • 必须严格按照底层模块端口信号列表顺序书写

门原语调用

verilog 中提供了已经设计好的门,称为门原语,一共有 12 个。

  1. 这些门可以直接调用
  2. 与底层模块不一样的地方在于,调用时,实例名可以省略
  3. 端口连接只能采取顺序法,输出在前,输入在后。

普通门

  • and 与
  • or 或
  • xor 异或
  • nand 与非
  • nor 或非
  • xnor 同或

使用方式为

1
and(out, in1, in2);

第一个是输出,其余是输入,系统输入数量不限

非门和缓冲门

  • not 非门
  • buf 缓冲门

其中使用方式为

1
not(out1, in);

前面是输出,最后一个是输入,其中输出的数量不限制

三态门

  • bufif1 控制端 1 有效缓冲器
  • bufif0 控制端 0 有效缓冲器
  • notif1 控制端 1 有效非门
  • notif1 控制端 0 有效非门

使用方式都为

1
bufif b1(out, in, ctrl);

端口列表中,前面是输出,中间是输入,最后是使能端口,输出个数不限

阻塞赋值和非阻塞赋值

阻塞赋值

使用 = 来表示阻塞赋值运算符,如下的例子

1
2
3
data1 = a;
data2 = a & data1;
data3 = data1 | data2;

其中的运行顺序就是

  1. 先计算 a 直接赋值给 data1
  2. 计算 a & data1 直接赋值给 data2
  3. 计算 data1 | data2 直接赋值给 data3

非阻塞赋值

使用 <= 来表示非阻塞运算符,对于上述的例子

1
2
3
data1 <= a;
data2 <= a & data1;
data3 <= data1 | data2;

其中运行顺序是

  1. 先计算 a 不赋值
  2. 计算 a & data1 不赋值
  3. 计算 data1 | data2 不赋值

过程结束,然后进行赋值操作

  1. 赋值给 data1
  2. 赋值给 data2
  3. 赋值给 data3

应用

  • 设计组合电路时,常使用阻塞赋值
  • 设计时序电路时,常使用非阻塞赋值
  • 但是并不绝对
  • 不建议在 always 中混合使用阻塞赋值和非阻塞赋值