可编程逻辑/Verilog 针对软件程序员
Verilog 是一种硬件描述语言 (HDL)。虽然编码风格看起来类似于 C++ 或 Java 等软件语言,包括 if 语句、循环、变量和表达式,但您不能像编写软件一样编写 Verilog。优秀的硬件设计师头脑中会清晰地呈现他们想要生成的硬件结构。Verilog 语言仅仅是一种方便的表达这些硬件结构的方式:多路复用器、寄存器、随机逻辑、状态机等等。
软件程序通常从调用 main() 例程开始。执行过程一直持续到代码退出。在 Verilog 中没有这样的起点:数字电路会一直运行,不断地处理其输入,观察其内部状态并生成输出。电路以由称为时钟的特殊信号决定的规律时间间隔进行处理。您以前听说过这种信号:2GHz CPU 是由 2 GHz 信号驱动的电路。这意味着该电路每秒将执行 20 亿步。
数字电路的典型起点由另一个称为复位的特殊信号标记。当复位处于活动状态时,电路不会尝试执行任何操作。但是一旦复位被停用,人们就期望数字电路开始执行有用的操作。
您可以将您的数字设计视为一系列步骤。每一步都在一个时钟周期内执行。如果您想执行一项复杂的任务,使用多个步骤,有时甚至是数千个步骤,是很正常的。
软件程序是一系列汇编指令。如果您更改代码并重新编译它,您可以生成新的指令并在同一台计算机上运行不同的程序:您不需要扔掉您的计算机并购买一台新的计算机来运行新的程序。
不幸的是,硬件不能像那样工作:您的设计所需的所有逻辑都必须定义,并且不能在运行时更改。如果您需要在一个步骤内添加三个数字,那么您需要提前提供该特殊的加法器。
然而,硬件与软件相比有一个很大的优势,这就是为什么它如此强大的原因:所有步骤都以并行方式执行,并且始终执行。对于复杂的设计,实际上会有数百万个代码片段,所有代码都同时执行。
并行执行是刚接触软件语言时 Verilog 如此奇怪的原因。让我们尝试了解一下,如果您编写的所有 C 代码都始终以并行方式执行,那么它将如何编写。考虑这段微小的 C 代码
main() {
int a = 5; int b = 7;
if(a < b) {
b = b - a;
}
}
假设您有一个神奇的变量 __LINE__,您必须控制下一行代码执行的顺序。但是所有行都永远执行,并且同时执行。下面的 C 代码将与原始代码具有相同的行为
main() {
__LINE__ = 1;
while(1) {
if(__LINE__ == 1) { int a = 5; int b = 7; __LINE__ = 2}
if(__LINE__ == 2) {if(a < b) { __LINE__ = 3; else __LINE__ = 4;}
if(__LINE__ == 3) { b = b - a; }
}
}
很可怕,不是吗?硬件就是这样工作的。硬件设计是一系列步骤。在每一步的开始,所有代码都会从头开始执行,因此您必须在某处保存您在上一步中的状态,以便您可以在您离开的地方继续执行。在上面的示例中,__LINE__ 是您的状态。
您如何在 Verilog 中实现经典的“Hello World”程序?在硬件中无法使用 printf。但我们可以编写类似的代码,以突出显示硬件与软件的不同之处。让我们尝试设计一个电路,该电路有一个输入:一个 8 位字符,和一个输出:HelloWorld 输出。该电路将一步一步地读取传入的字符,当它检测到“Hi”序列(字母 H 后跟字母 i”时,它会将输出提升到 1,以表示 HelloWorld,直到电路复位。如果您有一个开发板,您可以将该信号连接到 LED,当您输入正确的序列时,您将看到灯光亮起。
电路的声明如下
module HelloWorld(input [7:0] char, output HelloWorld, input clk, input reset);
电路的核心是一个状态机,它将首先查找字母“H”,然后查找字母“i”。
always @(posedge clk) begin
switch(state) begin
case GOT_NOTHING: begin
if(char=='H') state <= GOT_H;
else state <= GOT_NOTHING;
end
case GOT_H: begin
if(char=='i') state <= GOT_HI;
else state <= GOT_NOTHING;
end
end
然后,我们只需要在处于 GOT_HI 状态时,将 HelloWorld 输出信号驱动为真即可
assign HelloWorld = (state == GOT_HI);
而且我们不应该忘记声明我们用于编码状态的状态类型和常量
parameter GOT_NOTHING = 0;
parameter GOT_H = 1;
parameter GOT_HI = 2;
reg [1:0] state;
现在您已经了解了 Verilog 的每个代码块都在每一步都从头开始执行,我们就可以讨论更高级的语言结构,而不会将它们与它们的软件等效项混淆。是的,Verilog 支持循环和 if 语句。在合理的范围内。
首先,循环必须在同一步骤内完成。您不能在一步中开始循环并在以后完成。如果您想这样做,您必须在某处保存一个状态,告诉您您在循环中的位置,并在每一步都适当地恢复。
其次,循环不能具有取决于其他变量的可变迭代次数。您只能循环遍历一个常量。例如,您可以循环遍历向量的所有 16 位。但您不能循环直到两个变量相等。
正如我们之前所见,您不能在运行时创建新的硬件。这意味着 Verilog 中没有 malloc() 语句的等效项。您不能分配内存。内存要么存在,要么不存在。如果您考虑一下,这与您的个人电脑上的情况是一样的:您有 8 GB 的 RAM,此后您将耗尽内存。只是您的程序往往会使用少于可用总内存的内存(通常)。
由于我们无法分配内存,因此指针在 Verilog 中没有意义。但是有内存。它们是允许您读取和写入数据的较小块。它们的主要限制是,您只能在每一步执行一定数量的读写操作。实际上,大多数内存最多只允许您在每一步执行一个读或写操作。在 Verilog 中,内存看起来像一个数组。数组的维度指定了内存的深度和宽度。例如,这将是一个 256 个字的 32 位内存
reg [31:0] my_memory[255:0];
您可以在 always 块内访问内存的内容,但请注意:每次您引用内存信号时,都会算作一个读或写端口。除非您知道您的内存至少具有与循环大小相同的端口数量,否则您绝对不想在循环内访问内存信号。像从一个地址读取到另一个地址这样的看似简单的事情需要正确实现。下面的代码不好,因为它将消耗两个读端口,这太浪费了
always @(posedge clk) begin
if(select_addr1)
data <= my_memory[addr1];
else
data <= my_memory[addr2];
end
您想用这种方式编写内存读取代码,以确保只访问一次内存数组
always @(posedge clk) begin
if(select_addr1)
addr = addr1;
else
addr = addr2;
data <= my_memory[addr];
end
一个经典的软件面试问题是如何在不使用临时变量的情况下交换两个变量的内容。如果你还没听说过这个技巧,那么对于整数来说,解决方案是使用异或运算符。
function swap(int a, b) {
a = a xor b;
b = a xor b;
a = a xor b;
}
Verilog 更加强大:你可以在不使用临时变量和任何操作的情况下交换两个变量。这是怎么实现的呢?请记住,当你给一个变量赋值时,赋值会在下一步生效。因此,要交换两个变量,你真正需要说的是:“b 的下一个值是 a 的当前值,a 的下一个值是 b 的当前值。” 这在 Verilog 中可以使用非阻塞赋值 ("<=") 来表达。
always @(posedge clk) begin
a <= b;
b <= a;
end
Verilog 中的另一种赋值方式被称为阻塞赋值(你习惯看到的 "=")。这种赋值意味着对值的更改会立即生效。它在 always 块中计算中间值很有用,但是当你想分配一个从一步到下一步都存在的信号时,非阻塞赋值是首选。因此,下面的代码是错误的:在 always 块结束时,a 和 b 都有相同的值,而 a 的原始值丢失了。
always @(posedge clk) begin
a = b; // we just lost a's value
b = a;
end
步骤在 Verilog 中是一个非常重要的概念,几乎不可能在没有轻松访问所有变量的历史记录的情况下调试数字电路的行为。由于每个块在每一步都从头开始执行,你需要知道上一步生成的狀態,才能理解为什么当前步没有按你的预期执行。这就是硬件设计师使用波形的原因,波形是一种特殊的数值跟踪,显示了数值随时间的变化情况。由于时间是一系列离散的步骤,因此很容易在图表中排列起来。硬件设计师几乎从不单步执行 Verilog 代码,因为并行执行使得不可能有一个正在执行的“当前行”的概念。
你编写的 C 程序并不是真正运行在你的 PC 上的程序。它会被编译成汇编指令,然后由你 PC 内部的处理器执行。Verilog 的汇编语言等效是什么?
像下面这样简单的 C 代码会被翻译成这些汇编指令
main() {
int a = 2;
int b = a + 1;
}
0000000000400448 <main>:
400448: 55 push %rbp
400449: 48 89 e5 mov %rsp,%rbp
40044c: c7 45 f8 02 00 00 00 movl $0x2,0xfffffffffffffff8(%rbp)
400453: 8b 45 f8 mov 0xfffffffffffffff8(%rbp),%eax
400456: 83 c0 01 add $0x1,%eax
400459: 89 45 fc mov %eax,0xfffffffffffffffc(%rbp)
40045c: c9 leaveq
40045d: c3 retq
阅读完 Verilog 的所有限制后,你可能会想知道为什么要费这个劲。软件不是更强大吗?并不总是这样。如果你需要执行非常复杂的函数,这些函数本质上始终相同(例如视频压缩、3D 图形等),那么你可以将它们硬编码到 Verilog 中。在软件中,执行大型计算需要许多汇编指令:处理器在一个周期内可以处理的基本上只是一些对 64 位值的简单操作。
想象一下,你需要同时执行 100 个 32 位数字的加法。在软件中,你需要 100 条指令。在硬件中,只要你负担得起空间,你就可以用你想要的任意步数来编码它,包括只有一步。
reg [31:0] sum [99:0];
always @(posedge clk) begin
for(i=0;i<100;i++) begin
sum[i] <= a[i] + b[i];
end
end
该代码的软件版本看起来会一模一样,只是执行需要 100 个周期,而不是 1 个。硬件运行速度比软件快 100 倍。