前言
本文介绍了Verilog。
操作系统:Windows 11 家庭中文版
参考文档
介绍
每个新学习者的梦想都是在一天内理解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中,我们可以同时定义端口和端口方向)下面显示了代码。
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”。
双向端口示例:
inout read_enable; // port named read_enable is bi-directional
您如何定义向量信号(由超过一位的序列组成的信号)?Verilog也提供了一种简单的方法来定义这些。
矢量信号示例:
inout [7:0] address; //port "address" is bidirectional
请注意,[7:0]意味着我们使用的是小端约定——你从最右边的0开始开始向量,然后向左移动。如果我们使用[0:7],我们将使用大端约定,从左向右移动。字节序是一种纯粹任意的方式来决定你的数据将以哪种方式“读取”,但系统之间确实有所不同,因此一致地使用右字节序很重要。作为一个类比,想想一些语言(英语)是从左到右(大端)写的,而其他语言(阿拉伯语)是从右到左(小端)写的。知道语言的流动方式对于能够阅读它至关重要,但流动方向本身被任意设置了几年。
小结:
- 我们了解了如何在Verilog中定义块/模块。
- 我们学会了如何定义端口和端口方向。
- 我们学习了如何声明向量/标量端口。
数据类型
数据类型和硬件有什么关系?实际上没什么。人们只是想再写一种包含数据类型的语言。这完全是没有必要的;没有意义。
但是等等……硬件确实有两种驱动程序。
(驱动程序?那些是什么?)
驱动器是一种可以驱动负载的数据类型。基本上,在物理电路中,驱动器是电子可以穿过/进入的任何东西。
- 可以存储值的驱动程序(例如:触发器)。
- 不能存储值但连接两点的驱动程序(例如:电线)。
第一种类型的驱动程序在Verilog中称为reg(“寄存器”的缩写)。第二种数据类型称为线(用于… well,“线”)。您可以参考花絮部分以更好地理解它。
还有很多其他数据类型——例如,寄存器可以是有符号的、无符号的、浮点的……作为新手,现在不要担心它们。
Examples:
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语句检查条件以决定是否执行一部分代码。如果满足条件,则执行代码。否则,它运行代码的另一部分。
// 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机器进入一个非覆盖语句,机器就会挂起。将语句默认为返回空闲可以保护我们的安全。
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语句会重复执行其中的代码。虽然循环通常不用于现实生活中的模型,但它们用于测试台。与其他语句块一样,它们由开始和结束分隔。
while (free_time) begin
$display ("Continue with webpage development");
end
只要设置free_time变量,开始和结束中的代码就会被执行。即打印“继续Web开发”。让我们看一个更奇怪的例子,它使用了大多数Verilog结构。嗯,你没听错。Verilog的保留字比VHDL少,在这几个中,我们在实际编码中使用的更少。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
上面的例子使用了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。
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 (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
顾名思义,初始块在模拟开始时只执行一次。这在编写测试台时很有用。如果我们有多个初始块,那么它们都在模拟开始时执行。
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数据类型。
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现在是触发器输出。
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块,在这种情况下,我们需要有一个延迟,如下面的代码所示。
always begin
#5 clk = ~clk;
end
语句前面的#5将其执行延迟5个时间单位。
Assign Statement
赋值语句仅用于对组合逻辑进行建模,并且是连续执行的。因此赋值语句称为“连续赋值语句”,因为没有敏感列表。
assign out = (enable) ? data : 1'bz;
上面的例子是一个三态缓冲区。当使能为1时,数据被驱动到out,否则out被拉到高阻抗。我们可以有嵌套的条件运算符来构造复用器、解码器和编码器。
assign out = data;
这个例子是一个简单的缓冲区。
Task and Function
当一次又一次地重复同样的旧东西时,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
函数和任务具有相同的语法;一个区别是任务可以有延迟,而函数不能有任何延迟。这意味着函数可用于对组合逻辑进行建模。
第二个区别是函数可以返回值,而任务不能。
Test Benches
好的,我们已经根据设计文档编写了代码,现在怎么办?
我们需要测试它,看看它是否符合规格。大多数时候,这和我们在大学时代在数字实验室里做的是一样的:驱动输入,将输出与期望值匹配。让我们看看仲裁器测试台。
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用于监控信号列表中的变化,并以我们想要的格式打印它们。
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模拟器生成了上述输出。
结语
第一百五十八篇博文写完,开心!!!!
今天,也是充满希望的一天。