Verilog 学习笔记
这篇文章仅记录了一些本人在修读课程 「数字逻辑与计算机组成实验」时自学 Verilog 留下的一些笔记和备忘,不是一则教程,也不能作为教程使用,因此不可避免的含有大量缺乏例子,难以理解甚至出错的段落,若读到请仅供参考。
另外,作者作为一名水平一般的计算机相关专业本科生,修读本课程使用 Verilog 的场景仅限于在 FPGA 上实现一个 riscv32 架构的流水线 CPU,因此涉及的设计思路和语言特性相当有限。
电路描述
数据类型
Verilog 的数据类型分为 reg
和 wire
,其后还可以增加一个范围描述符,表示其位宽,如 wire [7:0] ch
。
wire
定义一个线网,它没有记忆,随时被其连接信号所驱动,当然驱动其的信号也只能是唯一的。reg
定义一个有所谓 「含蓄记忆」的信号,这里所谓记忆的意义是行为级的(原意是令仿真器暂存此值)。
数据值
Verilog 的数据值基本上限于整数,其中整数用 [size]['radix]value
指定,其中 radix
可以用 o
,h
,b
,d
来指定,默认用十进制,高位补 0 。
特别地,Verilog 中分别用 x
或 z
来表示数据中那些为不定值或高阻态的数据位,同时当数据最高位是这两种状态之一时,高位默认补对应的状态位。
Verilog 也可以用字符串为多位 reg
赋值,用大端方式存储。
操作符
Verilog 的操作符和 C 相差无几,需要特别注意的是带运算符的赋值符和自增自减操作并不受支持 (supported in SystemVerilog) 。
特别地,Verilog 支持一些缩简的运算符,诸如 |A
,&A
,~^A
等,它们的意义是对 A
的所有位之间执行逻辑运算并返回一位的值,对于四态值,其还支持全等(===
)和不全等(!==
)运算。
Verilog 还支持一种特殊的操作符:连接,形如 {expr, expr}
或 {value expr}
。它既可以被用于连接右值,也可以被用于连接左值,这样可以实现很多很方便的赋值逻辑(尤其是连接左值)。
数组和其他数据类型
在数据类型方面,Verilog 和 SystemVerilog 有很大的不同之处,下面分别讨论。
Verilog 的数组
对于 Verilog,定义在信号名称前的 range 只能有一维,它是这个数据类型特征的一部分。
因此,Verilog 的数组一般用如下方法定义:
reg [31:0] mem[31:0][31:0];
这实际上可以被理解为 reg [31:0]
的一个二维数组,而且它们的空间分布不是连续的,也不能被一起赋值。
SystemVerilog 的数组
SystemVerilog 提供打包数组 (packed array) ,也就是允许在信号名称前提供多维的位宽来形成一维或多维数组,这意味着它们的内存是连续的,如:
reg [31:0][31:0] mem[31:0][31:0];
这允许了对于打包维的连续赋值,同时名称后的维数和 Verilog 原义相同,这在 SystemVerilog 被称作非打包的 (unpacked array),而且也有对应的赋值语法值列表 '{}
,然而这种语法在 Verilog 中不受支持。
自定义数据类型
Verilog 不支持自定义数据类型,但 SystemVerilog 引入了 tydedef
支持了自定义数据类型,而且,后者还引入了 C 式的 union
和 struct
。
局部电路
门级电路描述
Verilog 提供了一些逻辑门模块来进行逻辑门级别的电路描述,在此不多讨论。
硬连线
assign
描述了一种硬连线,它可以为一个 wire
指定一个唯一的驱动信号,这个信号可以是其他多个信号和操作符构成的表达式。
过程语句
always
描述了过程性的电路行为,它拥有一个关键事件列表,用 @(events)
标识,若 events = *
则表示其涉及的任意非左值改变时触发语句块。
过程语句有如下几种:
- 赋值,包含阻塞赋值(
=
)与并行赋值(<=
)操作符,前者之间是顺序执行的,而后者不关心顺序,并行执行,且后者都先于前者执行。
(换句话说,并行赋值得到的右值都是事件触发前时那些右值的值,阻塞赋值得到的右值是上一条阻塞语句执行后右值的值) if-else
分支,和 C 类似,一般综合为二选一多路选择/分配器,这里不展开。case
分支,和switch
类似,值得一提的是还提供casez
和casex
语句,能够将z
或x
视为通配符?
。for
和repeat
,用于实现循环的逻辑,实际实现是被展开的,是否阻塞由内部语句而定。
描述过程性语句的还有 initial
,但除了仿真和给一些存储单元赋初值外不建议使用。
用 Verilog 描述的电路一般都是可综合 (Synthesizable) 的,但有些可以仿真的电路不可综合,即使可以综合,也可能会产生不合理的连线。比如:
always @(*)
begin
if (rst)
a <= 0;
if (en)
a <= 1;
end
这实际上指定了一种同时驱动 a
而且互相矛盾的连线,可以预见的是综合出的电路是不适合被实现 (Implementation) 的。由于 always
块之间其实是并行的,因此相似的错误也不允许发生。还有一种不可综合的情况发生在使用 initial
和 join/fork
块时,这些块在很多时候,在很多特定的硬件上不可综合。
任务、函数
task
和 function
定义了一种组合逻辑的「子任务」或者说「子过程」,它们可以在 always
块内被使用。但是不能包含其它的 always
——因为它们是「子过程」。
时序逻辑
时序逻辑电路由时钟激励和次态逻辑两部分所基本决定,在 Verilog 中,可以使用 posedge
和 negedge
来利用时钟的上升下降沿来界定时钟激励的瞬间:
always @(posedge clock)
//some next state logic here
模块布局
连接模块
模块用 module
定义,其包含一些 input
和 output
接口,在被上层模块实例化时,input
以 wire
的形式与上级相连,output
也被上级以 wire
相连的方式接收。
为了在电路设计中提高接口的复用性(比如很多模块都有 en
,clk
,rst
等接口),SystemVerilog 提供了 interface
来封装一些通用的接口集合。
批量生成
generate
定义了一种用过程语句展开块的行为。具体来说,generate
的迭代是定界的,它以特定的命名域来将实例化语句、过程语句、硬连线语句重复若干确定次数。它适合用于需要包含和管理大量相对独立的相同子模块的模块。
作为电路描述语言的 Verilog
如果有着软件开发背景,新学习 Verilog 时可能会有着一种熟悉的陌生感,这种陌生感可能首先来自于对于 reg
和 wire
的区别的疑惑,但是其背后更深的原因大概是 Verilog 中过程语句(或称行为级描述语句)和连线描述语句(或称并行语句)的不同:前者旨在令你直接指定电路的连线,后者旨在让综合器替你设计电路,两者分野不同,利用的是两套系统,这也是 assign
和 always
的区别的根源。
同时,由于根植于硬件设计的特性,Verilog 描述的电路会出现不可综合,不可部署的情况。这本质上是因为,和高级语言向机器语言的编译不同,Verilog 代码的综合更受到实际可用可编程器件的限制,其中很多特性没有也不可能被抽象到 Verilog 的语言标准之内。
正是基于这样的理解,以及对于由于翻译问题产生的许多对于 Verilog 语言特性有歧义和各家不同的表述的困惑,本人才决定写这么一个小笔记来记录一下学习 Verilog 过程中对于 Verilog 语言特性和设计思路的一些想法。然而,Verilog 不仅可以用于电路描述,也可以用于电路仿真,在用于仿真时,其设计思路又与在用于描述时大有不同,下个部分将对其进行一些简要介绍。
电路仿真
尽管电路器件是可编程的,但实际电路千变万化,编程到器件上进行调试不仅可能耗时耗力,而且可能编程后器件根本不工作,更罔论调试了,因此利用软件的方法模拟仿真是必要的。
因此,Verilog 和各种 EDA 也提供了对于仿真的支持,可以料想的是,仿真的设计流程以过程语句为主,其设计思路更像高级语言,尤其是对于专门针对电路验证进行了拓展的 SystemVerilog 而言。
时间粒度
仿真器模拟一个实际的电路应用场景,但受硬件资源的限制,其必须规定一个相对于现实世界较大的时间粒度,这用 `timescale
来规定,其语法为:
`timescale <time_unit>/<time_precision>
它规定了仿真的时间粒度和使用的时间单位。
提供激励
仿真器需要知道其为模拟设备提供特定激励信号的时机,一般我们仍然使用由 initial
和 always
块包裹的过程语句来提供激励。
延时和 initial
initial
指定了从仿真开始时刻开始的一系列过程语句,#(time)
则指定了一种「延时」的过程,仿真器待延时超时后再执行后续的语句。
其它的过程语句格式和电路描述时使用的非常相似。
reg a, b, c, d;
wire e;
testee testee1(a, b, c, d, e);
initial
begin
integer i;
for (i = 0; i <= 4b'1111; i = i + 1)
#5 {a, b, c, d} = i;
end
周期性激励
这一般由 always
块实现,例如,提供一个理想的时钟:
always #5
clk = ~clk;
数据类型
由于并不直接面向硬件,出于便利,Verilog 提供了主要用于仿真的数据类型,若其被滥用于电路描述,则可能是不可综合的。
integer
指定一个 32 位整数。real
指定一个浮点数。time
和realtime
指定一个整数或浮点的时间类型,前者是 64 位的。
功能语句
功能语句用于控制仿真流程或输出调试信息,以下列出一些语句。
流程控制
$finish
$stop
终端输出
$write
$display
$strobe
$monitor
波形输出
$dumpfile
指定输出的波形文件名。$dumpvars
指定需要输出波形的型号和输出等级,用例:$dumpvars([level], [module_name])
。
开发环境
为便于学习(乱搞),可以安装 iverilog + gtkwave 套件进行仿真。
使用示例
为指定仿真模块,在仿真顶层模块内加入:
initial
begin
$dumpfile("wave.vcd");
$dumpvars;
end
将仿真源文件和设计源文件放在一起编译运行:
$ iverilog -o test test.v test_tb.v
$ ./test
$ gtkwave wave.vcd