写在前面
在自己准备写一些简单的verilog教程之前,参考了许多资料----asic-world网站的这套verilog教程即是其一。这套教程写得极好,奈何没有中文,在下只好斗胆翻译过来(加了自己的理解)分享给大家。
这是网站原文:Verilog Tutorial
介绍
Verilog 是一种硬件描述语言 (HARDWARE DESCRIPTION LANGUAGE ,HDL)。硬件描述语言是一种用于描述数字系统(例如网络交换机、微处理器或存储器或简单的触发器)的语言。这意味着你可以通过使用 HDL来描述任何级别的任何(数字)硬件。
//D触发器
module d_ff ( d, clk, q, q_bar);
input d ,clk;
output q, q_bar;
wire d ,clk;
reg q, q_bar;
always @(posedge clk) begin
q <= d;
q_bar <= ! d;
end
endmodule
通过硬件描述语言,你可以描述一个如上图所示的简单D触发器,也可以描述一个超过100 万个逻辑门的复杂设计。Verilog 是业界可用于硬件设计的 HDL 语言之一。它允许我们在行为级(Behavior Level)、寄存器传输级 (Register Transfer Level,RTL)、门级(Gate level)和开关级(switch level)进行数字设计。Verilog 允许硬件设计人员使用行为结构来表达他们的设计,将实现细节推迟到最终设计的后期阶段。
很多想学这门语言的工程师,经常会问这样一个问题:"学Verilog需要多少时间?" 对此我的答案是“如果你碰巧知道至少一种编程语言的话(比如C、Java等),那可能只需要一周的时间。”
设计风格
Verilog 与任何其他硬件描述语言一样,允许采用自下而上(Bottom-up)或自上而下(Top-down)的方法进行设计。
自下而上的设计
传统的电子设计方法是自下而上的,每个设计都是使用标准门电路在门级实现的。随着新设计变得越来越复杂,这种方法几乎无法维护。新系统由 ASIC 或微处理器组成,复杂性达到了数千个晶体管级别。这种传统的自下而上的方法必须让位于新的结构化、层次化的设计方法。没有这些新方法,就无法解决新设计的复杂性。
自上而下的设计
所有设计师都期望的设计风格是自上而下的。真正的自上向下设计允许早期测试、不同技术的轻松更改、结构化系统设计并能提供许多其他优势。但是要实现纯粹的自上而下的设计是非常困难的,基于这个事实,大多数设计都是这两种方法(自下而上和自上而下)的混合,以结合两种方法的优点。
下图展示了自上而下的设计方法。
Verilog的抽象层级
Verilog 支持在不同的抽象层级上进行设计。其中三个非常重要:
- 行为级(Behavioral level)
- 寄存器转换级(Register-Transfer Level)
- 门级(Gate Level)
行为级
通过并行算法(行为)来描述系统。每个算法本身都是有顺序的,这意味着它由一组依次执行的指令组成。函数(Functions)、任务(Tasks)和 Always 块(Always blocks)是主要元素。行为级不考虑设计的结构实现。
寄存器转换级(RTL)
使用寄存器传输级别的设计通过操作和寄存器之间的数据传输来指定电路的特性。这需要使用显式的时钟信号。RTL 设计包含精确的时序边界:操作被安排在特定的时间发生。现代 RTL 代码定义是“任何可综合的代码都称为 RTL 代码”。
门级
在逻辑层内,系统的特性由逻辑链路及其时序属性描述。所有的信号都是离散信号,它们只能是明确的逻辑值('0'、'1'、'X'、'Z')。可用的操作是预定义的逻辑原语(AND、OR、NOT 等门)。对于任何级别的逻辑设计来说,使用门级建模可能都不是一个好主意。门级代码由综合工具等工具生成,此网表一般用于门级仿真和后端。
再介绍
每一个初学者的梦想都是能在一天之内掌握Verilog,至少也要能使用它。为了帮助初学者实现这个梦想,我接下来将讲解一些理论、示例和练习。
本教程不会教您如何编程(软件编程),因为它是为那些有一定编程(软件编程)经验的人设计的。Verilog 并发执行不同的代码块----尽管这与大多数软件编程语言的顺序执行所相反,但二者之间仍有许多相似之处。
此外,一些数字电路的设计背景也对学习Verilog很有帮助。
Verilog 出现之前的设计主要依赖原理图。每个设计,无论复杂程度如何,都是通过原理图设计的。它们难以验证且容易出错,这就很容易形成设计、验证、设计、验证……的漫长且乏味的开发迭代过程。
当 Verilog 出现后,我们突然有了一种新的思考方式来认识逻辑电路。Verilog 设计周期更像是一个传统的编程周期,本教程将引导您完成它。
使用Verilog进行设计的流程是这样的:
- 规格 (Specifications,specs)
- 高层级设计(High level design)
- 低层级设计(Low level (micro) design)
- RTL编码(RTL coding)
- 验证(Verification)
- 综合(Synthesis)
列表中的第一个是规格----我们将对设计施加何种限制和要求?我们要构建什么?
在本教程中,我们将构建一个2路仲裁器(2 agent arbiter):一种在2个源之间进行选择以争夺控制权(mastership)的设备。以下是我们可能会编写的一些规格。
- 2路仲裁器
- 高电平有效的异步复位
- 源 0 优先于源1的固定优先级
- 只要请求被断言,授权就会被断言(意思就是只要输入有效,输出就有效)
一旦我们有了规格,就可以绘制框图了,它基本上算是系统内数据流的一种抽象(什么进入黑盒子?或什么从黑盒子出来?)。我们举的例子很简单,对应的框图如下所示。现在我们还不用担心神奇的黑盒子里有什么。
仲裁器框图
如果现在我们不用Verilog来设计这个仲裁器,标准程序将要求我们绘制一个状态机,然后我们将为每个触发器制作一个包含状态转换的真值表,跟着我们会绘制卡诺图,根据卡诺图我们可以得到优化后的电路。此方法适用于小型设计,但对于大型设计,此流程将变得复杂且容易出错。这就是 Verilog 的用武之地,它向我们展示了另一种方式。
低层级设计
要了解 Verilog 是如何帮助我们设计仲裁器的话,让我们继续聚焦在状态机----现在我们进入低级设计并剥开上一个框图中黑盒子的封面,来看看输入是如何影响仲裁器的。
圆圈代表仲裁器可能处于的状态,每个状态都对应一个输出。状态之间的箭头是状态转换,由导致转换的事件标记。例如,最左边的橙色箭头表示如果仲裁器处于状态 GNT0(输出对应于 GNT0 的信号)并接收到输入 !req_0时,就会移动到状态 IDLE 并输出对应于该状态的信号。这个状态机描述了你所需要的系统的所有逻辑状态。下一步是将其全部放入 Verilog 中。
模块(Modules)
我们需要回溯一下才能做到这一点。如果你观察第一张图片中的仲裁器,就会发现它有一个名称(“仲裁器”)和输入/输出端口(req_0、req_1、gnt_0 和 gnt_1)。
由于Verilog是一种HDL(硬件描述语言,一种用于集成电路概念设计的语言),它也需要具备这些东西(名称和输入/输出端口)。在 Verilog 中,我们把“黑盒子”称为模块(Modules)。这是Verilog中的保留字,用于指代具有输入、输出和内部逻辑的事物。它们是其他编程语言中具有返回值的函数(Functions)的类似概念。
“仲裁器”模块的代码
如果你仔细观察仲裁块,就会看到有许多的箭头标记(进入模块的是输入,从模块出去的则是输出)。在Verilog中,我们声明了模块名和端口名之后,就可以定义每个端口的方向了。其RTL代码如下所示。
module arbiter (
clock , // clock
reset , // Active high, syn reset
req_0 , // Request 0
req_1 , // Request 1
gnt_0 , // Grant 0
gnt_1 // Grant 1
);
//-------------Input Ports-----------------------------
input clock ;
input reset ;
input req_0 ;
input req_1 ;
//-------------Output Ports----------------------------
output gnt_0 ;
output gnt_1 ;
双向端口示例
上面我们只使用了两种类型的端口,输入input和输出output,此外,还有一种双向端口也可以使用----inout。
inout read_enable; // 名为 read_enable 的端口是双向的
向量(vector )信号示例
应该如何定义向量信号(由多于一位的信号组成的序列)呢?Verilog 提供了一种简单的方法。
inout [7:0] address; //端口“address”是双向的
请注意 [7:0] 意味着我们使用的是little-endian(低字节序)约定----从最右边的 0 位开始依次向左递增。
如果我们使用的是 [0:7],意味着我们使用的是big-endian(高字节序)约定----从最左边的 0 位开始依次向右递增。
Endianness (字节序)是一种决定数据“读取”方式的排序方式,不同的系统之间确实存在差异,因此始终使用正确的 endianness 很重要。作为类比,想想一些从左到右(高字节序)书写的语言(英语)与从右到左(低字节序)书写的其他语言(阿拉伯语)。了解语言的书写方向对于能够阅读它至关重要,但方向本身是在几年前任意设置的。
概括
- 我们了解了如何在 Verilog 中定义块/模块(block/module)
- 我们学习了如何定义端口和端口方向(ports and port directions)
- 我们学习了如何声明向量/标量端口(vector/scalar)
数据类型
数据类型与硬件有什么关系?实际上,没什么关系。人们只是想再写一种包含数据类型的语言。但是等等……硬件确实有两种驱动(Drivers)。驱动?那是什么?
驱动是一种可以驱动负载的数据类型。在物理电路中,驱动器基本上可以是电子可以穿过/进入的任何东西。
- 可以存储数值的驱动(例如:触发器(flip-flop))
- 不能存储值,但能连接两点的驱动(例如:线(wire))。
第一种类型的驱动在 Verilog 中称为 reg(“register”的缩写)。第二种数据类型称为线(wire)。此外还有很多其他的数据类型,例如,寄存器可以是有符号的、无符号的、浮点数……作为新手,你现在暂时不要管这些。
示例:
wire and_gate_output; // "and_gate_output" 是只输出的线
reg d_flip_flop_output;// "d_flip_flop_output" 是一个寄存器;它存储并输出数值
reg [7:0] address_bus;// "address_bus" 是一个低字节序的 8 位寄存器
概括
- Wire 数据类型用于连接两点
- Reg 数据类型用于存储数值
运算符
值得庆幸的是,verilog中的运算符与其他编程语言中的相同。他们取两个值并比较(或其他的运算方式)它们以产生第三个结果----常见的例子是加法、等于、逻辑与……为了让我们的生活更轻松,几乎所有的运算符(至少下面列表中的那些)与它们在 C 语言中的对应部分完全相同。
类型 | 符号 | 功能 |
算术运算符 | * | 乘法 |
/ | 除法 | |
+ | 加法 | |
- | 减法 | |
% | 取余 | |
+ | 一元加法(Unary plus) | |
- | 一元减法(Unary minus) | |
逻辑运算符 | ! | 逻辑非 |
&& | 逻辑与 | |
|| | 逻辑或 | |
关系运算符 | > | 大于 |
< | 小于 | |
>= | 大于等于 | |
<= | 小于等于 | |
相等运算符 | == | 等于 |
!= | 不等于 | |
归约操作符 | ~ | 按位取反 |
~& | 与非 | |
| | 或 | |
~| | 活飞 | |
^ | 异或 | |
^~ | 同或 | |
~^ | 同或 | |
移位运算符 | >> | 向右移位 |
<< | 向左移位 | |
拼接运算符 | { } | 拼接 |
条件运算符 | ? | 条件 |
示例
a = b + c ; // 这很容易
a = 1 << 5; // 嗯,让我想想,好吧,将 '1' 左移 5 个位置。
a = !b ; // 它会反转 b 吗???
a = ~b ; // 你还想给'a'赋值多少次?这可能会导致多驱动(multiple-drivers)问题。
控制语句
等等,这是什么?if, else, repeat, while, for, case----Verilog 看起来和 C 语言一模一样(或者其他你会使用的编程语言)!尽管功能上verilog看起来与 C语言相同,但 Verilog 是一种 HDL,因此描述应被转化为硬件。这意味着您在使用控制语句时必须小心(否则您的设计可能无法在硬件中实现)。
If-else语句
If-else 语句通过检查条件来决定执行哪部分代码。如果满足条件,则执行该条件对应的代码。否则,它会运行代码的其他部分。
if (enable == 1'b1) begin
data = 10; //10进制赋值
address = 16'hDEAD; //16进制赋值
wr_enable = 1'b1; // 2进制赋值
end
else begin
data = 32'b0;
wr_enable = 1'b0;
address = address + 1;
end
你可以在条件检查中使用任何运算符,就像 C 语言的用法一样。如果需要,我们也可以嵌套 if-else 语句。没有 else 的语句也是合法的,但是它们可能会有其他问题----在实现组合逻辑时容易产生锁存器Latch。
Case语句
当我们有一个变量需要检查多个值时,可以使用 Case 语句。就像地址解码器一样,输入是一个地址,且需要检查它的所有可能值。我们没有使用多个嵌套的 if-else 语句(一个对应于我们要查找的单个值),而是使用单个 case 语句----这类似于 C/C++ 等语言中的 Switch 语句。
Case 语句以case开头,以endcase结束。在这两个分隔符之间列出你希望执行的所有语句(后跟冒号:)。
写default语句是一个好主意,就像有限状态机 (FSM) 一样,如果进入了非定义状态,状态机就被挂起(“死机”)。带有返回功能的default语句可以保证我们的安全。
case(address)
0 : $display ("It is 11:40PM");
1 : $display ("I am feeling sleepy");
2 : $display ("Let me skip this tutorial");
default : $display ("Need to complete");
endcase
注意: if-else 和 case 语句有一个共同点----如果您没有涵盖到所有可能的情况(If-else 中没有“else”或 Case 中没有“default”),并且你正在写一个组合逻辑语句的话,那么综合工具就会推断出锁存器Latch。
While语句
如果它检查的条件为真,那么 while 语句就会重复执行其中的代码。While循环通常不用于现实电路中的模型,一般都用于测试脚本testbench。与其他语句块一样,它们由begin和end分隔。
while (free_time) begin
$display ("Continue with webpage development");
end
只要 free_time 变量为真,就会重复执行 begin 和 end 内的代码----即打印“Continue with webpage development”。
让我们来看一个更奇怪的例子,它使用了大部分的 Verilog 结构。
module counter (clk,rst,enable,count);
input clk, rst, enable;
output [3:0] count;
reg [3:0] count;
always @ (posedge clk or posedge rst)
if (rst) begin
count <= 0;
end
else begin : COUNT
while (enable) begin
count <= count + 1;
disable COUNT;
end
end
endmodule
您会注意到一个名为 always 的新块----这说明了 Verilog 的一个关键特性。正如我们之前提到的,大多数软件语言都是按顺序执行的。相反,Verilog 程序通常有许多并行执行的语句。当满足其中列出的一个或多个条件时,所有标记为always 的块都将同时运行。
在上面的示例中,always 块将在 rst 或 clk 达到上升沿时运行----当它们的值从 0 上升到 1 时。您可以在程序中同时运行两个或多个always块(此处未显示,但常用)。
我们可以通过无效化保留字的方式来无效化代码块。在上面的示例中,每次计数器递增后,COUNT 代码块(此处未显示)都会被无效化。
For语句
Verilog 中的 for 循环几乎与 C/C++ 中的 for 循环完全相同。唯一的区别是 Verilog 不支持 ++ 和 -- 运算符,所以和在 C 中那样编写 i++ 不同,你需要写出完整的 i = i + 1。
for (i = 0; i < 16; i = i +1) begin
$display ("Current value of i is %d", i);
end
此代码将按顺序打印从 0 到 15 的数字。将 for 循环用于RTL时要小心,需要确保你的代码实际上可以在硬件中正常实现……并且循环不是无限的。
Repeat语句
repeat 语句类似于我们刚刚介绍的 for 循环。与我们声明 for 循环时显式地指定一个变量并递增它不同,repeat语句是告诉程序应该运行代码多少次,且没有变量递增(除非我们希望它们如此,就像在这个例子中)。
repeat (16) begin
$display ("Current value of i is %d", i);
i = i + 1;
end
输出与前面的 for 循环程序示例完全相同。在实际硬件实现中我们一般很少使用repeat语句(或 for 循环)。
概括
- while、if-else、case(switch)语句和C语言中的一样
- If-else 和 case 语句要求涵盖组合逻辑的所有情况
- For 循环与 C 中的相同,但没有 ++ 和 -- 运算符
- Repeat 与 for 循环相同,但没有递增变量
变量赋值
在数字电路中有两种类型的电路,组合逻辑电路和时序逻辑电路。我们当然知道这一点,但问题是“我们应该如何在 Verilog 中对此建模?”。Verilog 提供了两种对组合逻辑进行建模的方法和一种对时序逻辑进行建模的方法。
- 可以使用 assign 和 always 语句对组合逻辑电路建模
- 只能使用 always 语句对时序逻辑电路建模
- 还有第三个块,仅在testbench中(实际上也可以在RTL中进行初始化,但不常见)使用:称为Initial语句。
Initial块
顾名思义,Initial块(出师快)仅在仿真开始时执行一次。这在编写testbench时很有用。如果我们有多个Initial块,那么它们都会在仿真开始时执行。
示例
initial begin
clk = 0;
reset = 0;
req_0 = 0;
req_1 = 0;
end
在上面的示例中,在仿真开始时(即当仿真时间为0 时),begin-end内的所有变量都会被赋值为零。
Always块
顾名思义,always 块总是在执行,这与仅执行一次(在仿真开始时)的Initial块不同。第二个区别是 always 块应该有一个敏感列表或与之关联的延迟。
敏感列表告诉 always 块何时执行代码块,如下图所示。保留字alway后的@符号表示代码块将在符号@后括号中的条件“触发”。
关于 always 块的一个重要说明:它不能驱动 wire 数据类型,但可以驱动 reg 和 integer 数据类型。
always @ (a or b or sel)begin
y = 0;
if (sel == 0) begin
y = a;
end
else begin
y = b;
end
end
上面的例子是一个 2选1的多路选择器, a 和 b是输入,sel 是选择,y 是输出。
在任何组合逻辑电路中,只要输入发生变化,输出就会立即发生变化。当应用于 always 块时,该理论意味着只要输入变量(或输出控制变量)发生变化,就需要执行 always 块中的代码。这些变量是包含在敏感列表中的变量,即 a、b 和 sel。
有两种类型的敏感列表:电平敏感(用于组合电路)和边沿敏感(用于触发器)。下面的代码是相同的 2:1 Mux,但输出 y 现在是触发器输出。
always @ (posedge clk )begin
if (reset == 0) begin
y <= 0;
end
else if (sel == 0) begin
y <= a;
end
else begin
y <= b;
end
end
通常情况下我们不得不复位触发器,因此每次时钟从 0 转换到 1 (posedge) 时,都要检查复位是否有效(同步复位),然后继续执行正常逻辑。
如果我们仔细观察,就会发现在组合逻辑的情况下,我们使用了“=”进行赋值;而对于时序逻辑,我们则使用了“<=”运算符。“=”是阻塞赋值,“<=”是非阻塞赋值。“=”在begin-end内顺序执行代码,而非阻塞“<=”则并行执行代码。
我们可以写一个没有敏感列表的 always 块,在这种情况下需要有一个延迟,如下面的代码所示。
always begin
#5 clk = ~clk;
end
语句前面的 #5 将其延迟 5 个时间单位。
Assign块
assign 语句仅用于建模组合逻辑,并且会连续执行。所以赋值语句被称为“连续赋值语句(continuous assignment statement')”,因为其没有敏感列表。
assign out = (enable) ? data : 1'bz;
上面的例子是一个三态buffer。当使能为 1 时,数据被驱动到 out,否则 out 被拉到高阻抗。我们可以使用嵌套的条件运算符来构造多路选择器、解码器和编码器。
assign out = data;
这个例子是一个简单的buffer。
任务和函数
当一次又一次重复相同的旧事物时,Verilog 与任何其他编程语言一样,提供了解决重复使用代码的方法----任务和函数。
下面的代码可用于计算奇偶校验。
function parity;
input [31:0] data;
integer i;
begin
parity = 0;
for (i= 0; i < 32; i = i + 1) begin
parity = parity ^ data[i];
end
end
endfunction
函数和任务具有相同的语法,一个区别是任务可以有时间延迟,但函数不能。这意味着函数可用于建模组合逻辑。第二个区别是函数可以返回一个值,而任务不能。
Testbench(测试脚本)
好的,我们已经根据设计文档编写了代码,接着呢?
我们需要对其进行测试,看看它是否符合规格。大多数时候,这与我们在大学时代在数字实验室中所做的相同----驱动输入,将输出与预期值相匹配。
这是仲裁器的RTL代码:
module arbiter (
clock,
reset,
req_0,
req_1,
gnt_0,
gnt_1
);
input clock, reset, req_0, req_1;
output gnt_0, gnt_1;
reg gnt_0, gnt_1;
always @ (posedge clock or posedge reset)
if (reset) begin
gnt_0 <= 0;
gnt_1 <= 0;
end else if (req_0) begin
gnt_0 <= 1;
gnt_1 <= 0;
end else if (req_1) begin
gnt_0 <= 0;
gnt_1 <= 1;
end
endmodule
这是仲裁器的Testbench:
module arbiter_tb;
reg clock, reset, req0,req1;
wire gnt0,gnt1;
initial begin
$monitor ("req0=%b,req1=%b,gnt0=%b,gnt1=%b", req0,req1,gnt0,gnt1);
clock = 0;
reset = 0;
req0 = 0;
req1 = 0;
#5 reset = 1;
#15 reset = 0;
#10 req0 = 1;
#10 req0 = 0;
#10 req1 = 1;
#10 req1 = 0;
#10 {req0,req1} = 2'b11;
#10 {req0,req1} = 2'b00;
#10 $finish;
end
always begin
#5 clock = ! clock;
end
arbiter U0 (
.clock (clock),
.reset (reset),
.req_0 (req0),
.req_1 (req1),
.gnt_0 (gnt0),
.gnt_1 (gnt1)
);
endmodule
看起来我们已经将所有仲裁器的输入声明为 reg,将输出声明为 wire;我们这样做是因为测试平台需要驱动输入并需要监控输出。
在声明了所有需要的变量之后,我们将所有输入初始化为已知状态----这是在initial块中实现的。初始化后,我们按照要测试仲裁器的顺序断言/取消断言复位、req0、req1。时钟是用 always 块生成的。完成测试后,我们需要停止仿真----使用了 $finish 来终止仿真。$monitor 则用于监空信号列表的变化,并以我们想要的格式打印出来。
这是仿真运行后的结果:
req0=0,req1=0,gnt0=x,gnt1=x
req0=0,req1=0,gnt0=0,gnt1=0
req0=1,req1=0,gnt0=0,gnt1=0
req0=1,req1=0,gnt0=1,gnt1=0
req0=0,req1=0,gnt0=1,gnt1=0
req0=0,req1=1,gnt0=1,gnt1=0
req0=0,req1=1,gnt0=0,gnt1=1
req0=0,req1=0,gnt0=0,gnt1=1
req0=1,req1=1,gnt0=0,gnt1=1
req0=1,req1=1,gnt0=1,gnt1=0
req0=0,req1=0,gnt0=1,gnt1=0
- 📣您有任何问题,都可以在评论区和我交流📃!
- 📣本文由 孤独的单刀 原创,首发于CSDN平台🐵,博客主页:wuzhikai.blog.csdn.net
- 📣您的支持是我持续创作的最大动力!如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!