Menu

Verilog 连续赋值:assign 语句

assign 如何工作:它描述的始终成立的关系,它能驱动什么、不能驱动什么,以及与过程代码相比它最擅长的写法。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

assign 描述一个永远成立的事实

wire 声明创建一个信号。assign 描述由什么来驱动它。这个关系是持续的:右侧是什么,左侧就等于什么,在仿真时间的每一个时刻都是如此:

wire y;
assign y = a & b;

这两行隐含了两件事:

  • y 作为一根连线存在于电路中。
  • y 在任何时刻都是 a 和 b 的按位与。任一输入变化,y 都会跟着变。

没有时钟,也没有事件触发更新。仿真器看到 a 或 b 变化,就把 y 标记为需要更新,然后重新计算表达式。在硬件中,这对应几个与门:组合逻辑,无状态,即时响应。

隐式写法

你可以把声明和赋值合并成一行:

wire y = a & b;

这与上面两行完全相同。适合那些作用域之外没人关心的内联连线。很多编码规范其实更喜欢这种隐式写法,因为它让声明紧挨着等式。

assign 能驱动什么

assign 的目标必须是线网类型。在普通 Verilog 中就是 wire(或者 tri、wand、wor 这些较少见的同类)。它不能是 reg。如果你不小心把目标声明成了 reg:

reg y;
assign y = a & b;   // ERROR: cannot drive reg with assign

编译器会报错。要么把目标改成 wire,要么把逻辑移到 always @(*) 块中,在那里 reg 才是合法的目标。

右侧可以是任何能求出值的东西:字面量、信号、参数、运算符表达式、函数调用。它可以混合多种输入宽度,适用标准的扩宽规则。

什么时候用 assign,什么时候用 always

两者都能生成组合逻辑,选择主要取决于代码的可读性:

  • 关系是单个表达式时,assign 最合适。 加法器、用 ?: 写的简单多路选择器、掩码、奇偶校验位,以及任何能用一行写完的东西。
  • 需要过程语句时,always @(*) 最合适。 多路 case 语句、嵌套的 if/else if,以及任何能从带名字的中间 reg 中受益的逻辑。我们在 always 块中讲解这部分内容。

下面是用两种方式写出的同一个 4 选 1 多路选择器:

两个模块综合出的基本上是同一个多路选择器。assign 版本只有一行代码,always 版本有六行。四种情况时两者差不多;如果有十六种情况,case 块显然更容易阅读。

常见用法

普通组合逻辑

assign sum   = a + b;
assign carry = a[7] & b[7];
assign equal = (data == 8'hFF);

一个表达式,一根连线。这是 assign 最基本的用法。

2 选 1 多路选择器

assign out = sel ? a : b;

单个条件表达式,综合工具会把它变成一个 2 选 1 多路选择器。这是“在 a 和 b 之间选择”最简洁的写法。

位打包

assign status = {error, overflow, ready, busy, 4'b0};

在 assign 右侧使用拼接,就能把标志位打包成一个状态字节。结果会被持续地计算和驱动。

三态输出

assign data_pin = output_enable ? data_out : 1'bz;

output_enable 为高电平时驱动引脚,为低电平时释放为高阻态。这是芯片引脚处的标准写法,那里可能有多个驱动源共用一根线。

并发:多个 assign 不是顺序执行的

这一点永远值得提醒:同一个模块中的多个 assign 语句全部并行运行,它们不是一个执行序列:

assign y = a & b;     // exists at all times
assign z = a | b;     // also exists at all times, independently

它们在文件中的顺序无关紧要,两个等式同时成立。综合工具可能把与门放在或门左边,也可能放在右边,这都无所谓,两个门都在持续工作。

如果你想要看起来像顺序执行的行为,就需要 always 块(可能还需要时钟)。那是另一章的内容。

多个驱动源:总线模式

一个 wire 可以被多个 assign 赋值,但除了三态总线之外,你几乎从不需要这样做。两个驱动源争夺同一根线会产生未定义行为:

assign y = a;
assign y = b;   // BAD - two drivers, simulator picks one or x's it out

合理的写法是:每个驱动源在不工作时释放为 z,并且任何时刻最多只有一个在工作。

assign bus = device_a_active ? data_from_a : 1'bz;
assign bus = device_b_active ? data_from_b : 1'bz;

这样可行,因为在任何时刻,两个三元表达式中最多只有一个产生非 z 的值。线上的实际值就是那个没有释放的驱动源给出的值。

在内部逻辑中,也就是芯片引脚或片上共享总线以外的任何地方,每根线只能有一个驱动源。多驱动源的 bug 非常难调试。

assign 不能做的事

有些事情不适合用 assign:

  • 存储。 assign 描述的是组合关系,不能引入触发器。如果你需要一个值在多个时钟周期之间保持,那就要用 always @(posedge clk) 块。
  • 多步骤的过程逻辑。 你不能在 assign 中写 if/else 或 case。最接近的是链式 ?:,超过三个分支就会变得很难看。
  • 在过程块内驱动寄存器。 reg 目标需要过程赋值,而不是 assign。

了解这些限制,你就知道什么时候该换用 always。

接下来

现在你已经看过 Verilog 结构方面的全部内容:声明模块、实例化模块,以及用 assign 连接组合逻辑。下一章进入过程块,也就是 initial 和 always 结构,从这里开始,时间和顺序变得重要起来。

常见问题

Verilog 中的连续赋值是什么?

assign target = expression; 声明一个永久、持续的关系:target 始终等于 expression。表达式中任何信号变化时,仿真器都会重新计算右侧并更新 target。没有时钟,也没有事件,这个关系在每个时刻都成立。

在 Verilog 中 assign 可以给什么赋值?

assign 可以驱动 wire,但绝不能驱动 reg。目标必须是线网类型。如果你需要在 always 块中给某个信号赋值,就把它声明为 reg。如果 x 是 reg,编译器会拒绝 assign x = ...;如果 x 是 wire,编译器会拒绝 always 中的 x = ...。

什么时候用 assign,什么时候用 always 块?

简单的组合逻辑用 assign:一个表达式输入,一个信号输出,不需要 if/else。当逻辑需要过程语句(case、if/else if 链、for 循环)时,用 always @(*)。两者生成的都是组合逻辑硬件,选择哪个取决于可读性。

Verilog 中可以对同一个 wire 使用多个 assign 吗?

只有在建模三态总线时才可以,这时每个驱动源在不工作时会把 wire 释放为 z。两个 assign 同时试图把 wire 驱动成确定的值会产生冲突:仿真器可能选其中一个,也可能把信号变成 X,取决于工具。对于普通的组合逻辑,每个 wire 只能有一个驱动源。

Coddy 编程语言插图

和 Coddy 一起学编程

开始