前言

本文介绍了Verilog。

操作系统:Windows 11 家庭中文版

参考文档

  1. Verilog In One Day

介绍

每个新学习者的梦想都是在一天内理解Verilog,至少足够使用它。接下来的几页是我试图让这个梦想成为现实的尝试。会有一些理论和例子,后面跟着一些练习。本教程不会教你如何编程;它是为那些有一些编程经验的人设计的。即使Verilog同时执行不同的代码块,而不是大多数编程语言的顺序执行,仍然有很多相似之处。一些数字设计背景也很有帮助。

Verilog之前的生活是充满原理图的生活。每一个设计,无论复杂与否,都是通过原理图设计的。它们难以验证,容易出错,导致设计、验证……设计、验证……设计、验证……的开发周期漫长而乏味。

当Verilog到来时,我们突然对逻辑电路有了不同的思考方式。Verilog设计周期更像是传统的编程周期,这就是本教程将引导您完成的内容。事情是这样的:

  • 规格(规格)
  • 高级设计
  • 低级(微)设计
  • RTL编码
  • 验证
  • 合成。

清单上的第一个是规范——我们将在设计中设置哪些限制和要求?我们试图构建什么?在本教程中,我们将构建一个双代理仲裁器:一个在竞争主导权的两个代理中进行选择的设备。这里有一些我们可能会写的规范。

  • 两个代理仲裁者。
  • 主动高异步复位。
  • 固定优先级,代理0的优先级高于代理1
  • 只要请求被断言,授予就会被断言。

一旦我们有了规格,我们就可以绘制框图,这基本上是通过系统的数据流的抽象(什么进入或流出黑匣子?)。由于我们举的例子很简单,我们可以有一个如下所示的框图。我们还不担心魔法黑匣子里有什么。

仲裁器框图

现在,如果我们在没有Verilog的情况下设计这台机器,标准程序会要求我们绘制一个状态机。从那里,我们会为每个触发器制作一个包含状态转换的真值表。然后,我们会绘制卡诺图,从K图中我们可以得到优化的电路。这种方法适用于小设计,但对于大型设计,这种流程变得复杂且容易出错。这就是Verilog的用武之地,并向我们展示了另一种方法。

低级设计

要了解Verilog如何帮助我们设计仲裁器,让我们继续我们的状态机——现在我们进入低级设计,揭开上一张图的黑匣子,看看我们的输入如何影响机器。

每个圆圈代表机器可以处于的状态。每个状态对应一个输出。状态之间的箭头是状态转换,由导致转换的事件标记。例如,最左边的橙色箭头表示,如果机器处于状态GNT0(输出对应于GNT0的信号)并接收!req_0的输入,机器将移动到状态IDLE并输出对应于该状态的信号。此状态机描述了您需要的系统的所有逻辑。下一步是将其全部放入Verilog。

模块

我们需要回溯一点来做到这一点。如果您查看第一张图片中的仲裁块,我们可以看到它有一个名称(“arbiter”)和输入/输出端口(req_0、req_1、gnt_0和gnt_1)。

由于Verilog是一种HDL(硬件描述语言——一种用于集成电路概念设计的语言),它也需要具备这些东西。在Verilog中,我们称之为“黑匣子”模块。这是程序中的一个保留字,用于指代具有输入、输出和内部逻辑工作的东西;它们是其他编程语言中返回函数的大致等价物。

模块“arbiter”代码

如果您仔细查看arbiter块,我们会看到有箭头标记(输入的传入和输出的传出)。在Verilog中,在我们声明了模块名称和端口名称后,我们可以定义每个端口的方向。(版本说明:在Verilog 2001中,我们可以同时定义端口和端口方向)下面显示了代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module arbiter (
// Two slashes make a comment line.
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-----------------------------
// Note : all commands are semicolon-delimited
input clock ;
input reset ;
input req_0 ;
input req_1 ;
//-------------Output Ports----------------------------
output gnt_0 ;
output gnt_1 ;

这里我们只有两种类型的端口,输入和输出。在现实生活中,我们也可以有双向端口。Verilog允许我们将双向端口定义为“inout”。

双向端口示例:

1
inout read_enable; // port named read_enable is bi-directional

您如何定义向量信号(由超过一位的序列组成的信号)?Verilog也提供了一种简单的方法来定义这些。

矢量信号示例:

1
inout [7:0] address; //port "address" is bidirectional

请注意,[7:0]意味着我们使用的是小端约定——你从最右边的0开始开始向量,然后向左移动。如果我们使用[0:7],我们将使用大端约定,从左向右移动。字节序是一种纯粹任意的方式来决定你的数据将以哪种方式“读取”,但系统之间确实有所不同,因此一致地使用右字节序很重要。作为一个类比,想想一些语言(英语)是从左到右(大端)写的,而其他语言(阿拉伯语)是从右到左(小端)写的。知道语言的流动方式对于能够阅读它至关重要,但流动方向本身被任意设置了几年。

小结:

  • 我们了解了如何在Verilog中定义块/模块。
  • 我们学会了如何定义端口和端口方向。
  • 我们学习了如何声明向量/标量端口。

数据类型

数据类型和硬件有什么关系?实际上没什么。人们只是想再写一种包含数据类型的语言。这完全是没有必要的;没有意义。

但是等等……硬件确实有两种驱动程序。

(驱动程序?那些是什么?)

驱动器是一种可以驱动负载的数据类型。基本上,在物理电路中,驱动器是电子可以穿过/进入的任何东西。

  • 可以存储值的驱动程序(例如:触发器)。
  • 不能存储值但连接两点的驱动程序(例如:电线)。

第一种类型的驱动程序在Verilog中称为reg(“寄存器”的缩写)。第二种数据类型称为线(用于… well,“线”)。您可以参考花絮部分以更好地理解它。

还有很多其他数据类型——例如,寄存器可以是有符号的、无符号的、浮点的……作为新手,现在不要担心它们。

Examples:

1
2
3
4
5
wire and_gate_output; // "and_gate_output" is a wire that only outputs

reg d_flip_flop_output; // "d_flip_flop_output" is a register; it stores and outputs a value

reg [7:0] address_bus; // "address_bus" is a little-endian 8-bit register

小结:

  • 线数据类型用于连接两个点。
  • Reg数据类型用于存储值。
  • 愿上帝保佑其余的数据类型。总有一天你会看到他们的。

运算符

谢天谢地,这里的运算符和其他编程语言中的运算符是一样的。他们取两个值并比较(或以其他方式对它们进行操作)以产生第三个结果——常见的例子是加法、相等、逻辑和…为了让我们的生活更轻松,几乎所有运算符(至少下面列表中的运算符)都与C编程语言中的运算符完全相同。

Operator Type Operator Symbol Operation Performed
Arithmetic * Multiply
/ Division
+ Add
- Subtract
% Modulus
+ Unary plus
- Unary minus
Logical ! Logical negation
&& Logical and
|| Logical or
Relational > Greater than
< Less than
>= Greater than or equal
<= Less than or equal
Equality == Equality
!= inequality
Reduction ~ Bitwise negation
~& nand
| or
`~ `
^ xor
^~ xnor
~^ xnor
Shift >> Right shift
<< Left shift
Concatenation { } Concatenation
Conditional ? conditional

示例:

  • a = b + c ; //这很简单
  • a = 1 << 5 ; //嗯,让我想想,好的,把“1”向左移动5个位置。
  • a = !b ; //那么它是否反转b???
  • a = ~b ; //要分配给“a”多少次,可能会导致多个驱动程序。

小结:

让我们再次参加C语言培训,它们(几乎)就像C语言一样。

控制语句

等等,这是什么?if,else,repeat,while,for,case——它是Verilog,看起来和C语言一模一样(可能还有你用来编程的任何其他语言)!即使功能看起来和C语言一样,Verilog是一个HDL,所以描述应该翻译成硬件。这意味着你在使用控制语句时必须小心(否则你的设计可能无法在硬件中实现)。

If-else

if-else语句检查条件以决定是否执行一部分代码。如果满足条件,则执行代码。否则,它运行代码的另一部分。

1
2
3
4
5
6
7
8
9
10
// begin and end act like curly braces in C/C++.
if (enable == 1'b1) begin
data = 10; // Decimal assigned
address = 16'hDEAD; // Hexadecimal
wr_enable = 1'b1; // Binary
end else begin
data = 32'b0;
wr_enable = 1'b0;
address = address + 1;
end

可以在条件检查中使用任何运算符,就像在C语言中一样。如果需要,我们可以嵌套if else语句;没有else的语句也可以,但是在建模组合逻辑时,它们有自己的问题,以防它们导致闩锁(这并不总是正确的)。

Case

case语句用于我们有一个变量需要检查多个值的地方。就像地址解码器一样,输入是一个地址,需要检查它可以获取的所有值。我们不使用多个嵌套的if-else语句,每个值一个,而是使用单个case语句:这类似于C++等语言中的switch语句。

case语句以保留字case开头,以保留字endcase结尾(Verilog不使用括号来分隔代码块)。case、冒号和您希望执行的语句都列在这两个分隔符中。有一个默认case也是一个好主意。就像有限状态机(FSM)一样,如果Verilog机器进入一个非覆盖语句,机器就会挂起。将语句默认为返回空闲可以保护我们的安全。

1
2
3
4
5
6
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

看起来地址值是3,所以我仍在编写本教程。

注意:if-else和case语句的一个共同点是,如果您没有涵盖所有case(If-else中没有’else’或Case中没有’default’),并且您正在尝试编写组合语句,则合成工具将推断Latch。

While

如果分配给它检查的条件返回true,while语句会重复执行其中的代码。虽然循环通常不用于现实生活中的模型,但它们用于测试台。与其他语句块一样,它们由开始和结束分隔。

1
2
3
while (free_time) begin
$display ("Continue with webpage development");
end

只要设置free_time变量,开始和结束中的代码就会被执行。即打印“继续Web开发”。让我们看一个更奇怪的例子,它使用了大多数Verilog结构。嗯,你没听错。Verilog的保留字比VHDL少,在这几个中,我们在实际编码中使用的更少。Verilog真好……太对了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

上面的例子使用了Verilog的大部分结构。你会注意到一个叫做always的新块——这说明了Verilog的一个关键特性。正如我们之前提到的,大多数软件语言都是按顺序执行的——也就是说,一个语句接一个语句。另一方面,Verilog程序通常有许多并行执行的语句。当其中列出的一个或多个条件得到满足时,所有标有always的块都将同时运行。

在上面的例子中,always块将在rst或clk达到正边时运行——也就是说,当它们的值从0上升到1时。您可以在程序中同时运行两个或多个always块(此处未显示,但常用)。

我们可以通过使用保留字disable来禁用代码块。在上面的示例中,在每个计数器增量之后,COUNT代码块(此处未显示)被禁用。

For loop

Verilog中的for循环几乎与C或C++中的循环完全相同。唯一的区别是Verilog不支持++和–运算符。不是像在C中那样编写i++,而是需要写出它的完整操作等价物,i=i+1。

1
2
3
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循环时,我们不是显式指定一个变量并将其递增,而是告诉程序在代码中运行多少次,并且没有变量递增(除非我们希望它们是,就像在这个例子中一样)。

1
2
3
4
repeat (16) begin
$display ("Current value of i is %d", i);
i = i + 1;
end

输出与前面的for循环程序示例完全相同。在实际硬件实现中使用重复(或for循环)相对较少。

Summary

  • While,if-else,case(switch)语句与C语言中的相同。
  • If-else和case语句要求组合逻辑涵盖所有case。
  • for循环与C中相同,但没有++和–运算符。
  • Repeat与for循环相同,但没有递增变量。

Variable Assignment

在数字中,有两种类型的元素,组合的和顺序的。我们当然知道这一点。但问题是“我们如何在Verilog中建模?”Verilog提供了两种建模组合逻辑的方法,只有一种建模顺序逻辑的方法。

  • 可以使用assign和always语句对组合元素进行建模。
  • 序列元素可以仅使用always语句建模。
  • 还有第三个块,仅在测试台中使用:它被称为初始语句。

Initial Blocks

顾名思义,初始块在模拟开始时只执行一次。这在编写测试台时很有用。如果我们有多个初始块,那么它们都在模拟开始时执行。

1
2
3
4
5
6
initial begin
clk = 0;
reset = 0;
req_0 = 0;
req_1 = 0;
end

在上面的示例中,在模拟开始时(即当时间=0时),开始和结束块内的所有变量都被驱动为零。

转到下一页,讨论assign和always语句。

Always Blocks

顾名思义,always块总是执行,不像初始块只执行一次(在模拟开始时)。第二个区别是always块应该有一个敏感列表或与之相关的延迟。

敏感列表是告诉always块何时执行代码块的列表,如下figure所示。保留字’always’后的@符号表示该块将在符号@后括号中的条件“处”触发。

关于always块的一个重要注意事项:它不能驱动线数据类型,但可以驱动reg和integer数据类型。

1
2
3
4
5
6
7
8
9
always  @ (a or b or sel)
begin
y = 0;
if (sel == 0) begin
y = a;
end else begin
y = b;
end
end

上面的例子是一个2:1的mux,输入为a和b;sel是选择输入,y是mux输出。在任何组合逻辑中,只要输入发生变化,输出就会发生变化。这个理论应用于always块时意味着,只要输入变量(或输出控制变量)发生变化,就需要执行always块内的代码。这些变量是敏感列表中包含的变量,即a、b和sel。

敏感列表有两种类型:电平敏感(用于组合电路)和边缘敏感(用于触发器)。下面的代码是相同的2:1 Mux,但输出y现在是触发器输出。

1
2
3
4
5
6
7
8
always  @ (posedge clk )
if (reset == 0) begin
y <= 0;
end else if (sel == 0) begin
y <= a;
end else begin
y <= b;
end

我们通常必须重置触发器,因此每次时钟从0转换到1(posedge)时,我们检查重置是否被断言(同步重置),然后我们继续使用正常逻辑。如果我们仔细观察,我们会发现在组合逻辑的情况下,我们有“=”表示赋值,对于顺序块,我们有“<=”运算符。嗯,“=”是阻塞赋值,“<=”是非阻塞赋值。“=”在开始/结束内按顺序执行代码,而非阻塞“<=”并行执行。

我们可以有一个没有敏感列表的always块,在这种情况下,我们需要有一个延迟,如下面的代码所示。

1
2
3
always  begin
#5 clk = ~clk;
end

语句前面的#5将其执行延迟5个时间单位。

Assign Statement

赋值语句仅用于对组合逻辑进行建模,并且是连续执行的。因此赋值语句称为“连续赋值语句”,因为没有敏感列表。

1
assign out = (enable) ? data : 1'bz;

上面的例子是一个三态缓冲区。当使能为1时,数据被驱动到out,否则out被拉到高阻抗。我们可以有嵌套的条件运算符来构造复用器、解码器和编码器。

1
assign out = data;

这个例子是一个简单的缓冲区。

Task and Function

当一次又一次地重复同样的旧东西时,Verilog,像任何其他编程语言一样,提供了解决重复使用的代码的方法,这些代码被称为任务和函数。我希望我有类似的网页,只是叫它一次又一次地打印这种编程语言的东西。

下面的代码用于计算偶数奇偶校验。

1
2
3
4
5
6
7
8
9
10
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

函数和任务具有相同的语法;一个区别是任务可以有延迟,而函数不能有任何延迟。这意味着函数可用于对组合逻辑进行建模。

第二个区别是函数可以返回值,而任务不能。

Test Benches

好的,我们已经根据设计文档编写了代码,现在怎么办?

我们需要测试它,看看它是否符合规格。大多数时候,这和我们在大学时代在数字实验室里做的是一样的:驱动输入,将输出与期望值匹配。让我们看看仲裁器测试台。

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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 Code Goes here
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;嗯,这是真的。我们这样做是因为测试台需要驱动输入并需要监控输出。

在我们声明了所有需要的变量后,我们将所有输入初始化为已知状态:我们在初始块中这样做。初始化后,我们按照我们想要测试仲裁器的顺序断言/取消断言重置、req0、req1。时钟是用一个always块生成的。

在我们完成测试后,我们需要停止模拟器。好吧,我们使用完成来终止模拟。完成来终止模拟。Monitor用于监控信号列表中的变化,并以我们想要的格式打印它们。

1
2
3
4
5
6
7
8
9
10
11
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

我已经使用Icarus Verilog模拟器生成了上述输出。

结语

第一百五十八篇博文写完,开心!!!!

今天,也是充满希望的一天。