
always 块中使用非阻塞赋值 (<=),以正确地为时序硬件的并行特性建模,并避免常见的仿真错误。在现代数字电子学中,创建如微处理器或通信集线器之类的复杂系统并非一项单体任务,而是一种基于模块化原则的架构行为。在 Verilog 硬件描述语言中,这种架构的基本单元是模块 (module)。这种自包含、可重用的逻辑块是管理复杂性和构建可扩展设计的关键。然而,许多 Verilog 的初学者难以超越简单的门级描述,无法掌握如何有效地定义、描述和连接这些强大的组件。本文旨在作为一份全面掌握 Verilog 模块的指南。在接下来的章节中,我们将首先探讨原理与机制,内容涵盖如何定义模块接口以及描述其内部逻辑的三种不同风格。随后,我们将考察应用与跨学科联系,展示如何将这些基础模块组装起来,以创建有限状态机、纠错电路和关键同步器等复杂系统。
想象一下,你想建造一个复杂的东西——比如一个精密的机器人。你不会从熔化一整块硅然后期待最好的结果开始。相反,你会设计独立的组件:一个电机控制器、一个传感器处理器、一个存储单元。每个组件都是一个自包含的“黑盒”,有特定的工作和明确定义的连接点。你会设计每个盒子的内部,然后将它们全部连接起来,形成最终的机器人。
使用 Verilog 进行数字电路设计的方式完全相同。其基本构建单元是模块 (module)。模块是硬件的蓝图。它是对一个电路的自包含描述,无论大小,其内部工作与外部世界之间有清晰的界限。要真正理解 Verilog,就是要理解如何定义这些模块,更重要的是,如何描述它们内部发生的事情。
在我们描述一个电路做什么之前,我们必须首先定义它的边界。什么信号输入?什么信号输出?这就是模块的接口,在 Verilog 中,我们使用端口 (ports) 来定义它。
以网络设备中的一个简单组件 PacketIntegrityChecker 为例。它的工作是检查传入的数据并标记错误。从外部看,我们不需要知道它如何检查错误,只需要知道它需要什么信息以及它产生什么结果。我们用一个端口列表来指定这一点:
input clk:一个用于协调其时序的时钟信号。input rst_n:一个用于将其置于已知状态的复位信号。input [3:0] data_in:一个 4 位的数据总线。[3:0] 的表示法声明了一个“向量”或一组四根线,索引从 3 到 0。output packet_ok:一根单线,如果数据包良好,则变为高电平。一个 Verilog 模块声明将所有这些整齐地包裹起来:
这个声明就是“盒子”。我们已经定义了引脚。现在,最引人入胜的部分开始了:我们要在里面放什么?Verilog 提供了三种优美而独特的风格来描述内部逻辑,每种都适用于不同的任务。
描述电路最直接的方式是写下定义其输出基于其输入的逻辑或数学公式。这被称为数据流建模 (dataflow modeling),它非常适合组合逻辑 (combinational logic)——即没有存储器的电路,其输出总是当前输入的直接函数。
想象一个简单的数据scrambler,它将一个输入的 8 位数据流与一个固定模式进行异或运算以对其进行随机化。这种关系是一个简单的公式:scrambled_out = data_in XOR 10101010。在 Verilog 中,我们使用 assign 关键字来表示这种连续的关系:
这一行不是一次性执行的命令;它是一个永久性真理的陈述。它声明 scrambled_out 这组线总是由这个异或运算的结果驱动。如果 data_in 改变,scrambled_out 就会立即改变,就好像它们被一个物理的异或门阵列连接起来一样。
这引出了 Verilog 中的一个关键区别:wire 与 reg。wire 就像一根物理电线——它没有存储能力。它只是将信号从驱动端传输到接收端。连续 assign 语句的目标必须是像 wire 这样的线网类型,因为该语句描述的是一个直接的、无状态的连接。你不能 assign 一个值给一个意在存储它的东西;这就像试图“命令”一根电线在你断开电池后记住一个电压。
有时,描述一个大型电路的最佳方式是用更小的、预先存在的部件来构建它。这就是结构化建模 (structural modeling)。这就像用标准的乐高积木搭建一个复杂的模型。你不用描述积木的塑料材质;你只需说:“在这里放一个红色的 2x4 积木,然后把它连接到一个蓝色的 1x2 积木上。”
假设我们需要构建一个 4 位的格雷码到二进制码的转换器,并且我们已经有一个名为 xor_gate 的 2 输入异或门模块的蓝图。转换逻辑是一系列的异或操作。与其写出公式,我们可以简单地创建 xor_gate 的实例并将它们连接起来:
在这里,u1、u2 和 u3 是我们 xor_gate 模块的唯一实例。这种风格使设计层次化且易于理解。当我们想要创建某个组件的许多副本时,Verilog 提供了一个强大的工具,称为 generate 块。它就像装配线上的机械臂,可以根据需要冲压出任意数量的模块实例,这对于从 N 个单位翻转触发器构建一个 N 位寄存器这样的宽结构非常完美。
数据流和结构化建模非常适用于输出仅取决于当前输入的电路。但对于那些需要记忆事物的电路呢?这是时序逻辑 (sequential logic) 的领域,为了描述它,我们需要一个新的工具:行为建模 (behavioral modeling)。
我们不再描述连接,而是描述随时间变化的行为。行为建模的核心是 always 块。always 块包含一组指令,这些指令在特定事件发生时执行,比如时钟信号的上升沿 (posedge clk)。
这就是 reg 数据类型变得至关重要的地方。任何在 always 块内被赋值的东西都必须声明为 reg,因为它需要保持其值直到下一个触发事件。它代表一个存储元件,比如一个触发器。
考虑一个带有异步复位的简单 D 型触发器。它的规则是:
rst_n 变为低电平,立即将输出 q 设为 0。clk 的上升沿,用输入 d 的值更新 q。在 Verilog 中,这可以优美地翻译为:
敏感列表 @(posedge clk or negedge rst_n) 告诉仿真器在时钟上升沿或复位下降沿“唤醒”并执行此块。代码接着描述了优先级:首先检查复位,否则执行时钟驱动的行为。我们可以轻松地扩展它来建模更复杂的寄存器,例如带有同步使能的寄存器,或者使用 reg 数组来建模整个存储器块。
请仔细看触发器示例中的赋值操作符:q <= d;。这与简单的等号 (=) 不同。这是一个非阻塞赋值 (non-blocking assignment),它可以说是正确建模时序逻辑最重要的概念。
为了理解原因,让我们考虑一个经典问题:在时钟沿交换两个寄存器 reg_A 和 reg_B 的值。
如果我们使用阻塞赋值 (blocking assignment) (=),代码会像计算机程序一样顺序执行:
想象一下 reg_A 的值是 10,reg_B 的值是 5。在第 1 步,reg_A 变为 5。原来的值 10 永远丢失了。在第 2 步,reg_B 被赋予 reg_A 的新值,也就是 5。两个寄存器最终都变成了 5。交换失败!
现在,让我们使用非阻塞赋值 (non-blocking assignment) (<=):
这是一个深刻的区别。非阻塞赋值的意思是:“在时钟沿,根据此刻存在的值,计算所有右侧表达式的值。然后,安排所有更新同时发生。”
所以,在时钟沿,仿真器看到 reg_A 是 10,reg_B 是 5。它确定 reg_A 应该变成 5,而 reg_B 应该变成 10。然后,仿佛魔法一般,所有更新同时发生。reg_A 变为 5,reg_B 变为 10。交换成功!这模拟了硬件的真实并行特性,即系统中所有的触发器在同一瞬间捕获它们的新值。
Verilog 的美妙之处在于它描述了一个物理的、并行的系统。但这也意味着我们必须小心,确保对系统的描述没有歧义。如果我们告诉系统的两个不同部分在同一时间将同一个信号驱动到两个不同的值,会发生什么?
考虑这个有问题的模块:
这段代码有两个 always 块,都由同一个时钟沿触发,都试图为同一个寄存器 q 赋值。这在硬件上是不可能的——就像把两个不同逻辑门的输出连接到同一根线上。在仿真世界中,这会产生一个竞争条件 (race condition)。
当时钟触发时,一个块安排用 a 的值更新 q,而另一个块则安排用 b 的值更新。哪一个会赢?Verilog 标准刻意没有规定并发 always 块的执行顺序。仿真器可能最后执行第一个块的更新,也可能最后执行第二个块的更新。因此,q 的最终值是不确定性的 (non-deterministic)——它可能是 a,也可能是 b。这不是仿真器的缺陷;这是设计描述中的一个根本性错误。它提醒我们,我们不是在编写一个顺序程序,而是在描述一个必须自洽的物理现实。就像在现实世界中一样,你不能让两个东西在同一时间占据同一个位置。
在熟悉了 Verilog module 的原理和机制——硬件描述的语法——之后,我们现在可以开始创作了。我们从语言的学生转变为作者和架构师。毕竟,一门语言真正的魔力不在于其语法,而在于它能讲述的故事和它能构建的世界。Verilog module 是我们的基本构建单元,是我们的概念性乐高积木。它是一个自包含的逻辑与结构的世界。令人惊奇的是,这些简单的、标准化的单元可以组合起来,创造出支撑我们现代世界的复杂而强大的数字系统。让我们踏上征途,看看我们能建造出什么。
任何数字系统的核心都是永恒的组合逻辑——这类电路的输出完全取决于它们当前的输入,没有过去或未来的概念。这些是我们数字物理学的基本定律。借助 Verilog,我们可以用优雅的简洁性来描述这些定律。例如,判断一个数是大于、小于还是等于另一个数是一个基本操作。使用数据流建模,我们几乎可以像在数学方程中一样表达这种比较,将逻辑关系直接映射到硬件结构上。这是最直接的硬件描述形式,类似于陈述一条自然法则,然后观察硬件如何聚合以遵守它。
但计算不仅仅是原始的比较;它关乎信息的编码和解释。考虑一个编码器,它是一个将信号位置转换为紧凑二进制码的电路。与其用一张逻辑门网络来描述它,我们可以使用行为建模来描述它的意图。我们可以使用 case 语句来说:“当这条输入线有效时,产生这个输出码。” 这种更高层次的抽象使我们摆脱了单个逻辑门的束缚,让我们能够从功能和行为的角度思考。
抽象的力量在参数化模块上实现了巨大的飞跃。为什么要设计一个特定的与门模块,然后再设计一个独立的或门模块,等等?一个更强大的想法是设计一个单一的、可配置的逻辑单元,它可以变成我们需要的任何东西。使用参数和 generate 语句,我们可以为逻辑片编写一个蓝图。然后,在创建的瞬间(阐述期),我们可以通过设置一个参数来命令它成为一个与门、一个或门,甚至是一个更复杂的函数。这就是现代可扩展设计的精髓:创建灵活、可重用的组件,这些组件可以为任何任务进行特化,就像一把可以切割以适应多种不同锁的主钥匙。
纯组合逻辑的世界是静态和无状态的。要构建任何真正有趣的东西——任何能够记忆、计数或控制的东西——我们必须引入时间。时钟信号是时序系统的心跳,随着每一次跳动,宇宙可以从一个状态演变到下一个状态。
最简单的状态行为是计数。但我们不限于标准二进制计数器的简单 0, 1, 2, 3...。不同的应用需要不同的序列。例如,格雷码是一种序列,其中连续的数字仅相差一个比特位。这个特性在机电系统中非常有用,因为它能防止传感器在不同位置之间移动时产生过渡错误。使用行为 Verilog,我们可以精确地定义一个遵循这种特殊序列的计数器,从而创建一个为解决特定物理世界问题而量身定制的组件。我们不仅仅是在计数;我们是在编排状态的流动。
从计数器,我们可以推广到时序设计中最强大的概念:有限状态机 (FSM)。FSM 是无数数字控制器背后的“大脑”。思考一下十字路口那个不起眼的交通灯。它的逻辑——等待车辆,循环切换绿、黄、红灯——可以完美地描述为一小组状态(MainGreen、MainYellow 等)以及它们之间的转换规则。我们可以在一个 FSM 中捕捉这种完整的行为,然后,使用结构化 Verilog,我们可以看到这个抽象机器是如何通过将状态寄存器(触发器)与一个根据当前状态和输入计算下一状态的组合逻辑块相结合来物理实现的。FSM 是连接抽象行为和具体硬件的一座美丽的桥梁。
除了控制宽泛的序列,时序逻辑还使我们能够检测和响应时间中的短暂瞬间。例如,一个“单稳态”电路被设计用来在检测到特定事件(如输入信号的上升沿)时,产生一个精确持续时间的、干净的单次脉冲。通过使用一个寄存器来存储前一个时钟周期的信号值,我们可以将过去与现在进行比较。signal_is_high_now AND signal_was_low_before 这个条件完美地定义了一个上升沿。这个简单而巧妙的技术是事件驱动设计的基石,使系统能够即时可靠地响应触发器。
拥有了丰富的组合和时序构建块库,我们现在可以将它们组装成更大、更复杂的系统,并将它们连接到其他科学和工程领域。这正是 module 作为管理复杂性工具的闪光之处。我们不需要每次需要时都从头开始重建一个触发器。在结构化建模中,我们只需实例化我们预先定义的模块并将它们连接起来。要构建一个将频率减半的分频器,我们可以简单地级联两个翻转触发器模块,将第一个的输出馈送到第二个的时钟输入。这种层次化的方法是构建我们今天所见的各种规模系统的唯一途径,从微处理器到整个片上系统。
这些数字构建块的应用远远超出了简单的计算。它们是其他科学领域的重要工具。
信息论与可靠性: 在有噪声的信道上传输或在不完美的存储器中存储的数据可能会被损坏。我们如何检测甚至纠正这些错误?答案在于纠错码,这是一个深奥的数学领域。例如,一个 (7,4) 汉明码_hamming_code|lang=zh-CN|style=Feynman)为一段 4 位数据添加了三个精心计算的奇偶校验位。这些由简单的异或运算生成的奇偶校验位,创建了一个具有非凡属性的 7 位码字:任何单个比特的错误都可以被检测和纠正。我们可以将汉明码_hamming_code|lang=zh-CN|style=Feynman)生成器实现为一个纯组合逻辑的 Verilog 模块,创造一个其唯一目的是赋予数据韧性和完整性的硬件。
数据通信: 接收器如何能将其时钟与传入的数据流完美同步?一个绝妙的解决方案是将时钟信号嵌入到数据本身之中。曼彻斯特编码正是这样做的。'1'被编码为比特周期中间的低到高转换,而'0'则为高到低转换。接收器可以从这些有保证的转换中提取时钟。这种自同步时钟方案可以用一个简单的 FSM 在 Verilog 模块中实现,为数据通信创建一个稳健的物理层。
最后,我们面临现代数字设计中最微妙和关键的挑战之一。大型系统很少是同步的;不同部分通常运行在不同、独立的时钟上。当一个信号必须跨越从一个时钟域到另一个时钟域的鸿沟时会发生什么?如果信号转换到达时间太接近目标时钟的边沿,接收触发器可能会进入一种被称为亚稳态 (metastability) 的奇异、不稳定状态,其输出在一段不可预测的时间内既不是一个干净的'0'也不是'1'。这是一种灾难性的失效模式。令人惊讶的是,解决方案简单而优雅:一个两级触发器同步器。异步信号首先被一个触发器捕获。它的输出可能会是亚稳态的,但在被第二个触发器采样之前,它有整整一个时钟周期来稳定下来。当信号从这第二级出来时,亚稳态的概率呈指数级降低。这个关键的安全电路可以通过结构化地串联两个 D 型触发器模块来构建——一个驯服了深刻而危险的物理现象的微小电路。
从逻辑门到纠错码,从交通控制器到时钟域边界的守护者,Verilog module 的旅程是一个不断提升能力和抽象层次的旅程。它是被设计的数字宇宙的基本粒子,证明了从简单、易于理解的规则和组件中,可以产生几乎无限复杂和实用的系统。
module PacketIntegrityChecker(
input clk,
input rst_n,
input [3:0] data_in,
input parity_in,
input sof,
output packet_ok,
output error_flag
);
// ... 内部的魔法在这里发生 ...
endmodule
assign scrambled_out = data_in ^ 8'b10101010;
module gray_to_binary(output [3:0] binary_out, input [3:0] gray_in);
// 最高有效位是直接连接
assign binary_out[3] = gray_in[3];
// 实例化三个异或“乐高积木”
xor_gate u1 (binary_out[2], binary_out[3], gray_in[2]);
xor_gate u2 (binary_out[1], binary_out[2], gray_in[1]);
xor_gate u3 (binary_out[0], binary_out[1], gray_in[0]);
endmodule
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
q <= 1'b0;
end else begin
q <= d;
end
end
// 错误的交换方式
always @(posedge clk) begin
reg_A = reg_B; // 第1步:reg_A 得到 reg_B 的值。
reg_B = reg_A; // 第2步:reg_B 得到 reg_A 的新值。
end
// 正确的交换方式
always @(posedge clk) begin
reg_A <= reg_B;
reg_B <= reg_A;
end
module RaceConditionModule( ... output reg [3:0] q ...);
always @(posedge clk) begin
q <= a; // 驱动源 1
end
always @(posedge clk) begin
q <= b; // 驱动源 2
end
endmodule