
wire 和用于在过程块内赋值的状态保持变量 reg。always 块中使用非阻塞赋值(<=),以正确建模同步、并行的硬件行为,例如数据流经移位寄存器。integer 可能造成资源浪费,混合使用 signed 和 unsigned 类型可能导致逻辑错误。$readmemh)之间存在一条关键界限。在数字设计的世界里,Verilog 是用来在硅片上构建整个宇宙的语言。要精通这门技艺,必须首先掌握其最基本的材料:数据类型。它们不仅仅是抽象的变量容器,更是所创硬件的本质,决定了其结构、行为和最终性能。对这些基础概念的误解会导致设计效率低下、出现细微的 bug,以及硬件行为完全出乎意料。本文旨在揭开这些核心组件的神秘面纱,弥合语法与综合之间的鸿沟。
我们将开启一段分为两部分的旅程。第一章“原理与机制”将通过探索主要的构建模块来奠定基础。您将学习到用于连接的 wire 和用于记忆的 reg 之间的关键区别,理解非阻塞赋值在描述时间和状态方面的威力,并避开数值表示中的常见陷阱。接下来,“应用与跨学科联系”将展示这些原理如何应用于构建从可重用 IP 核到复杂算法的一切事物,揭示其在信号处理和通信等领域的实际影响。读完本文,您不仅将了解 Verilog 数据类型的规则,还将理解如何运用它们来设计高效、稳健且复杂的数字系统。
想象一下,你是一位建筑师,但你不是用砖块和砂浆设计建筑,而是用逻辑门和触发器设计数字宇宙。你的蓝图不是图纸,而是一种语言:Verilog。正如建筑师必须了解钢材、混凝土和玻璃的属性一样,数字设计师也必须掌握这门语言的基本材料——其数据类型。它们不仅仅是变量的标签,更是你所创造硬件的本质。它们决定了结构、行为,甚至界定了真实与仿真幻象之间的边界。
每个数字设计的核心都存在一种基本的二元性,一种数据处理的阴阳两面。你将构建的一切都由两个基本概念组成:仅仅连接事物的东西和记忆事物的东西。在 Verilog 中,它们就是 wire 和 reg。
wire 是可以想象的最简单的实体。它是一个物理连接,一条电流通路。它没有记忆,也没有自己的意志。它忠实地将信号从源头传送到目的地。如果你停止向一个 wire 驱动信号,它会忘记自己承载的内容,并变得“松弛”(进入高阻态 z)。当你需要将事物连接在一起时,例如将一个子电路的输出连接到另一个子电路的输入时,就会使用 wire。在结构化设计中,如果你需要一个内部信号来桥接两个模块实例,wire 是你唯一明智的选择,因为它模拟了你在电路板或芯片上创建的物理连接。这是通过连续assign语句完成的,这是 Verilog 的一种表达方式,意为“使该 wire 永久等于此表达式”。这种连接是实时的、连续的和组合的。
然后是 reg。这个名字有点历史遗留的用词不当,因为它并不总是创建一个物理的“寄存器”或触发器。它的真正本质更为深刻:reg 是一个被设计用来保持其值的变量。与需要持续驱动的 wire 不同,reg 会记住从一次赋值到下一次赋值的状态。因为它保持一个值,所以它不能像 wire 那样被持续更新。那将是一个矛盾!相反,必须告诉它何时更新。这是 always 等过程块的工作。always 块定义了 reg 获得新值的条件——即事件。
这引出了 Verilog 数据类型的黄金法则:
wire 由连续的 assign 语句(或模块输出)驱动。它们用于建模组合逻辑。reg 由 always 或 initial 块内的过程赋值驱动。它们用于建模组合逻辑和时序逻辑,具体取决于 always 块的控制方式。语言严格执行这种分离。你不能用连续的 assign 语句驱动 reg。为什么?因为你将告诉一个记忆元件像一个无记忆的线网一样行事,这是一个概念上的冲突。reg 的目的是在特定时间点(如时钟边沿)被更新,这正是过程行为的定义。语言强制你将其赋值放在过程块内,以使你的意图清晰明确:你正在描述一个保持状态的组件。
有了用于连接的 wire 和用于存储的 reg,我们就可以构建静态结构。但数字电路是动态的;它们计算、反应,有脉搏。这个脉搏就是时钟,而描述在每个时钟节拍上发生什么的魔力由一种特殊的赋值处理:非阻塞赋值 (<=)。
想象一下,你想构建一个简单的两级移位寄存器。数据从一端(d)进入,移动到第一个暂存点(q1),然后在下一个时钟节拍,移动到第二个暂存点(q2)。Verilog 新手可能会按照他们脑海中顺序发生的逻辑来编写代码:首先,q2 获得 q1 的旧值,然后 q1 获得新的输入 d。
这里蕴含着语言所捕捉到的一个美妙的设计直觉。尽管我们一行接一行地写这两行代码,但非阻塞运算符 <= 告诉综合工具一些非凡的事情。它说:“在时钟跳变的那一刻,查看右侧所有事物的当前值。然后,同时更新左侧的所有事物。”
所以,在时钟的上升沿,硬件同时执行两个动作:
q1 的当前值加载到 q2 的寄存器中。d 的当前值加载到 q1 的寄存器中。结果是一个完美的两级移位寄存器:链中的两个触发器,都共享同一个时钟。“顺序”的代码完美地描述了并行的硬件。这是对数据如何流经寄存器流水线的正确建模方式。
理解这一点至关重要。如果你误解了 reg 和非阻塞赋值的性质,你可能会创造出你意想不到的硬件。假设你想要一个简单的组合反相器后跟一个组合异或门。初学者可能会在一个时钟块内这样写:
因为 inv_data 是一个用 <= 赋值的 reg,综合器看到的不是一根简单的线网。它看到的是一个构建存储元件——一个触发器——的命令!然后,因为 result 是根据这个新的寄存器 inv_data 计算出来的,所以最终的电路不是单层逻辑,而是一个两级流水线。仅仅因为选择了错误的建模风格,就引入了一个额外的、不希望有的时钟周期延迟。教训是明确的:当你打算跨时钟周期存储一个值时,使用 reg 和 <=;对于直接的组合连接,使用 wire 和 assign。
计算机只知道 0 和 1。像 11001000 这样的模式仅仅是一个模式。是数据类型赋予了它意义。它是无符号数 200,还是有符号数 -56?作为设计师,你必须主宰这种意义。
Verilog 允许你将一个数指定为 signed(有符号)。这是对编译器关于你打算如何解释最高有效位的承诺。但是当你混合使用这些隐喻时会发生什么?考虑一个 8 位有符号寄存器保存 -1(8'b11111111)和一个 8 位无符号寄存器保存 200(8'b11001000)。(200 > -1) 的结果是什么?
从逻辑上讲,200 当然大于 -1。但 Verilog 有其自己严格的规则。当一个关系运算符看到一个有符号操作数和一个无符号操作数时,它会做出一个决定性的选择:它将两者都视为无符号数。突然间,-1(8'b11111111)被解释为无符号数 255。比较变成了 (200 > 255),结果为假。一个看似简单的比较却得出了一个完全违反直觉的结果,这一切都源于一个隐式类型提升规则。这是粗心设计师的典型陷阱,也是在表达式中对类型一丝不苟的有力论据。
这种对精度的需求延伸到了像 integer 这样看似方便的类型。用 integer 来做状态机或计数器很诱人;感觉很自然。但在 Verilog 中,一个 integer 是一个综合一个 32 位有符号寄存器的承诺。如果你正在构建一个只有五个状态的状态机,你只需要 3 个比特来表示它们()。如果你将状态变量声明为 reg [2:0],综合器将精确地创建 3 个触发器。如果你将其声明为 integer,综合器会遵从标准,创建一个 32 位的寄存器——使用的硬件资源是前者的近十一倍,却没有任何好处!。方便可能代价高昂;精确才是效率。
伟大的设计不仅仅是有效的;它们是可重用的。你不想今天设计一个 8 位滤波器,明天又得为 16 位版本从头开始。这就是 parameter 关键字将你的设计从一个单一、具体的对象转变为一个灵活的蓝图的地方。
通过在模块头中声明一个参数,你就创建了一个可以在每次使用该模块时自定义的常量。
通过这个简单的添加,你创建的模块不再只是一个滤波器。它是一个完整的滤波器家族。你可以实例化一个 8 位、4 级版本,或者一个 32 位、12 级版本,所有这些都来自同一份源代码。parameter 不是最终硬件中的变量;它是综合工具的指南,告诉它在放置第一个门之前如何构建电路。它将你的思维从单个实例提升到整个架构。
最后,我们必须面对一个严酷的现实:你写的 Verilog 服务于两个主人。第一个是仿真器,一个执行你的代码以预测硬件行为的软件程序。第二个是综合器,实际构建你硬件的工具。它们并不生活在同一个世界。
仿真器是全能的。它运行在你的计算机上,可以访问其内存和文件系统。你可以编写一个带有 $readmemh 命令的 initial 块,从文本文件中将滤波器系数加载到内存模型中。这在仿真中工作得非常完美。
但当你尝试综合它时,综合器失败了。为什么?因为最终的 FPGA 芯片是独立运行的,它没有硬盘。它没有操作系统。它没有一个名为 "coeffs.hex" 的文件的概念。$readmemh 命令是一个仅限仿真的结构——是给仿真器的消息,而不是硬件的蓝图。同样,像用于浮点数的 real 数据类型通常也只用于仿真。虽然你可以构建浮点硬件,但它极其复杂,一个简单的 real 声明不会让综合器推断出它。使用 integer 的表达式 (25 / 8) 会得到 3,而不是 3.125,因为硬件执行的是整数算术,除非你明确构建了用于浮点数学的复杂机制。
仿真世界和物理世界之间的这种区别是设计师必须学习的最重要的界限。为了帮助强制执行这一纪律并捕捉细微的 bug,明智的设计师会使用一个编译器指令:`default_nettype none。默认情况下,如果你使用一个未声明的信号名,Verilog 会为你隐式地创建一个 1 位 wire。这个“乐于助人”的特性是因拼写错误引起 bug 的常见来源。通过将默认网络类型设置为 none,你可以禁用此行为。编译器现在会将任何未声明的信号标记为错误,迫使你明确声明每一个 wire 和 reg。这就像一个严格的导师,强迫你为设计中的每个组件命名并说明理由。
这种纪律迫使你思考。它迫使你在 wire 和 reg 之间、在 signed 和 unsigned 之间、在 3 位向量和 32 位整数之间做出选择。在做出这些选择的过程中,你超越了简单地编写代码,开始了数字设计的真正艺术:有意识地、刻意地在硅片上创造一个宇宙。
在遍历了 Verilog 数据类型的基本原理之后,你可能会有一种类似于学习国际象棋规则的感觉。你知道棋子如何移动——reg 可以保持一个值,wire 传输它——但你尚未见证大师精妙组合的绝美之处。这些概念的真正力量不在于它们的个别定义,而在于它们如何组合、互动,并给予我们构建整个数字宇宙的语言。
在本章中,我们将开始一场对这些构造的巡礼。我们将看到这些简单的原始元素——我们的数字粘土——如何被塑造成具有惊人复杂性和实用性的结构。我们将从定义一个组件的简单边界,到构建执行复杂算法的机器,将代码的抽象世界与计算、通信和信号处理的具体现实联系起来。
每个伟大的建设项目都始于一张蓝图。在数字设计中,这意味着定义一个组件的接口——它的输入和输出。这是我们第一个,也是最基本的应用。考虑一个为监控网络数据而设计的简单“数据包完整性检查器”。它在 Verilog 中的蓝图精确定义了它的连接点:一个时钟、一个复位,以及数据总线,所有这些都声明了它们的方向和位宽。这种使用基本 input 和 output 端口的声明行为,将一个抽象的想法转化为一个具有明确边界的具体实体,可以被连接到一个更大的系统中。
一旦边界设定,我们必须描述内部的逻辑。在这里,Verilog 的数据流建模以一种近乎诗意的优雅大放异彩。想象一下需要知道一个 8 位数据总线是否承载了一个非零值。人们可以写一个冗长的表达式来检查每一位,但 Verilog 提供了一种更深刻的方式:或归约运算符。一行代码 assign is_nonzero = |data_bus; 完成了七个或门的工作,将整个向量压缩为单个比特的信息。这是一个美妙的例子,展示了语言如何提供能够以硬件思维方式思考的工具,以惊人的简洁性表达一个常见的数字任务。
这种数据流范式自然地扩展到算术运算。例如,校验和计算器的一个阶段可以用一个单一的连续赋值来描述。使用三元运算符 (condition ? value_if_true : value_if_false),我们可以建模一种逻辑,其中 reset 信号将输出清零,否则它计算当前数据和前一和值的总和。加法 current_sum_in + data_in 如果超过 8 位限制会自然地回绕,这不是一个 bug,而是一个特性。它完美地反映了固定宽度硬件加法器的行为,展示了 Verilog 的数据类型如何与计算的物理特性内在联系。
然而,为每个任务设计定制组件是低效的。现代工程的真正革命是可重用性。在这里,parameter 关键字登场了。它不是一个承载信号的数据类型,而是一个定义硬件本身结构的元数据类型。通过创建一个通用的 2-to-1 多路复用器,其中数据宽度由 parameter N = 16 定义,我们不仅仅是在构建一个组件;我们正在创建一个灵活的蓝图,可以通过简单地更改一个参数来实例化为任何所需的宽度——8位、32位或64位。这是创建知识产权(IP)核的基础,这些可重用的乐高积木被用来构建庞大而复杂的系统。
到目前为止,我们构建的世界是纯组合的——一个没有记忆的世界,输出总是当前输入的直接函数。要构建任何真正有趣的东西,从一个简单的计数器到一台超级计算机,我们都需要引入时间和状态。我们需要记忆。这是 reg 数据类型的领域。
记忆的基本原子是触发器。一个正边沿触发的 D 触发器的行为模型揭示了其中深邃的魔力。一个 always @(posedge clk) 块告诉仿真器只在时钟信号从低电平转换到高电平的精确时刻“唤醒”。在那一刻,reg 输出 q 捕获数据输入 d 的值。在时钟周期的其余时间里,它坚定不移地保持那个值,创造了一个单一时间点的记忆。包含一个对负边沿(negedge clr_n)敏感的异步清除 clr_n 进一步展示了我们如何能将我们的意志强加于系统,迫使其进入一个已知状态,而不管时钟的滴答声。
我们能用这些记忆的原子做什么?我们可以将它们串联起来。考虑一个 3 级移位寄存器。在每个时钟节拍,数据沿线向下移动一个位置:din流入第一个寄存器,其先前的值流入第二个,第二个的值流入第三个。要描述这一点,需要 Verilog 的一个微妙而深刻的特性:非阻塞赋值(<=)。当我们写下:
我们不是在描述一个顺序的事件链。我们是在声明,在时钟边沿,所有这些传输都应该同时发生。q2 的值被更新为时钟边沿之前的 q1 的旧值,而不是它刚从 din 接收到的新值。这正确地模拟了硬件的并行性质,其中所有触发器在同一时间采样它们的输入并一起更新。使用简单的阻塞赋值(=)会打破这种幻觉,产生一个纹波效应而不是同步移位——这是一个区分正确硬件描述和简单软件程序的关键区别。
但这种记忆的能力伴随着责任。reg 类型,就其本质而言,想要保持它的值。如果你编写一个意图是纯组合的逻辑块,但你未能指定 reg 在所有可能情况下的行为,你就会创建一个“推断的锁存器”。一个其 always 块只对 sel 线敏感,而对数据输入不敏感的多路复用器,当数据改变时将无法更新其输出。相反,reg 将固执地保持其旧值,创建一个无意的、通常是灾难性的记忆元件。这不是语言的缺陷;这是一个教训。Verilog 迫使你精确地表达你的意图:这个逻辑是永恒和组合的,还是它有记忆?
拥有描述无状态逻辑和有状态机器的能力,我们现在可以搭建通往其他科学和工程学科的桥梁,将它们的算法直接在硅片上实现。
首先,我们必须弥合与共享电子设备物理世界的差距。多个设备,如 CPU 和 DMA 控制器,如何能同时写入同一个内存总线?如果两者都试图同时将一根线驱动到 1 和 0,会导致短路。解决方案是高阻态,1'bz。通过建模一个三态缓冲器,我们可以设计一个组件,当被禁用时,它在电气上将其输出与线网断开。它不驱动 0 或 1;它什么也不驱动。这允许另一个设备控制总线。z 值不是一个抽象概念;它是对物理晶体管“放开”线网的直接命令,对于构建构成每个现代计算机骨干的共享总线来说,它是绝对必要的。
我们的桥梁可以延伸到抽象数学和信息论的领域。通过噪声信道发送或从存储器读取的数据可能会被损坏。一个 (7,4) 汉明码_hamming_code|lang=zh-CN|style=Feynman)生成器是一个美妙的应用,我们使用简单的位逻辑来构建一个强大的纠错方案。通过对四个数据位的特定组合执行异或(^)操作,我们生成三个奇偶校验位。这七个位被交织并传输。接收器可以执行类似的检查,结果的模式可以唯一地识别在传输过程中是否有一个位被翻转,以及是哪一个——从而允许它被纠正。曾经是通信教科书中的一个概念,现在变成了一个有形的、高速的电路,这要归功于 Verilog 能够直接表达这些奇偶校验方程的能力。
当我们进入数字信号处理(DSP)的世界时,与数学的联系更加深入。在这里,我们不仅仅是移动比特;我们正在操纵代表真实世界信号(如声音或图像)的数字。建模一个带饱和的 8 位有符号乘法器显示了所需的复杂性。我们必须使用 signed 数据类型来正确处理负数。我们意识到两个 8 位数相乘可能会产生一个 16 位的结果,所以我们必须使用一个更宽的中间寄存器来执行计算而不会丢失信息。最后,我们实现饱和:如果结果超过了有效的 8 位范围(例如,大于 127),我们将其“钳位”到最大值,而不是让它回绕。对于音频信号,这意味着声音在最大音量处削波——一种刺耳但可预测的失真——而不是回绕到一个大的负值,那会听起来像一声可怕的爆音或咔嗒声。这个选择完全由应用驱动,而 Verilog 给了我们完美实现它的工具。
在这种复杂性的顶峰,是在硬件中实现整个算法。考虑一个多周期整数平方根计算器。这不是一个简单的门阵列;它是一个有限状态机,在许多时钟周期内一丝不苟地执行一个数字递推算法。它使用一套寄存器——有符号和无符号的、计数器和累加器——来管理部分余数并逐位构建结果。这是行为建模力量的证明,表明 Verilog 不仅用于描述电路,还用于描述计算本身。一个可能作为一系列软件指令运行的算法可以被重塑为一个专用的、高速的状态机,展示了计算机科学和数字硬件设计的深刻融合。
我们的旅程已经从定义简单的端口带我们到实现复杂的算法。我们已经看到,少数数据类型和规则可以用来构建一个数字世界。但随着系统增长到包含数十亿个晶体管——整个片上系统(SoC)——管理这种复杂性成为主要挑战。将一个处理器连接到十几个外设将涉及手动连接数百个信号。
这种设计理念的最终胜利是向更高层次的抽象迈进。现代 Verilog(SystemVerilog)提供了 interface。一个接口将复杂总线协议的所有信号——时钟、复位、地址、数据、控制信号——捆绑到一个单一的、命名的对象中。一个模块不再有一个包含五十个独立端口的列表;它只有一个端口:总线接口。接口内的 modport 从特定角度定义了信号的方向,例如从外设的角度。这种强大的抽象允许工程师用一行代码连接复杂的组件,隐藏了物理布线的繁琐细节。这是我们旅程的最后一步:从塑造数字粘土,到建造逻辑的城市,再到绘制连接它们的星际地图。
Verilog 数据类型系统,最终,远不止是一套编程语言的规则。它是一个精心设计的思想框架,一个我们借以观察、描述并最终创造数字时代复杂而精美机器的透镜。
always @(posedge clk) begin
q2 <= q1;
q1 <= d;
end
// 意图: result = (~data) ^ ctrl
// 实际写的代码:
always @(posedge clk) begin
inv_data <= ~data;
result <= inv_data ^ ctrl;
end
module fir_filter #(
parameter WIDTH = 8,
parameter STAGES = 4
) (
input [WIDTH-1:0] data_in,
...
);
q1 <= din;
q2 <= q1;
q3 <= q2;