阻塞赋值--非阻塞赋值

核心思想:模拟硬件的两种不同行为

首先要牢记,HDL(硬件描述语言)的根本目的是描述硬件行为。阻塞和非阻塞赋值就是用来描述两种截然不同的硬件数据传输方式:

阻塞赋值 (`=`): 模拟组合逻辑的行为,或者说是一种“串行”的数据流。

非阻塞赋值 (`<=`): 模拟时序逻辑(寄存器)的行为,即一种“并行”的数据更新。 理解了这一点,后面的规则就都是顺理成章的。

第一部分:阻塞赋值 (Blocking Assignment, `=`)

1. 专业描述

阻塞赋值之所以叫“阻塞”,是因为在 `begin...end` 块内,一条阻塞赋值语句的执行会阻塞后面语句的执行。

只有当前语句的赋值操作完成后,下一条语句才能开始执行。这与C语言等传统编程语言的执行顺序完全相同。

执行模型立即计算,立即更新:当程序执行到 `a = b;` 时,会立即计算右侧(RHS, Right-Hand Side)`b` 的值,并立即用这个值更新左侧(LHS, Left-Hand Side)`a` 的变量。

顺序执行: 在一个 `always` 块中,代码从上到下顺序执行,如同执行一个脚本。

2. 硬件映射与适用场景

阻塞赋值描述的是一种级联的、有先后顺序的逻辑。这正是组合逻辑的特征。数据流经一片组合逻辑,就像水流过一串管道,是有先后顺序的。

适用场景:组合逻辑 (`always @(*)` 或 `always_comb`)

// 示例:描述一个简单的组合逻辑计算
// y = (a & b) | c;
always @(*) begin
    reg temp;
    temp = a & b;  // 1. 首先计算 a & b, 结果赋给 temp
    y    = temp | c; // 2. 然后,用 temp 的新值与 c 计算,结果赋给 y
end

在这个例子中,`temp` 的计算必须在 `y` 的计算之前完成。使用阻塞赋值能够完美地描述这种数据依赖关系。

3. 错误使用的后果

如果在时序逻辑中使用阻塞赋值,会产生严重问题,导致仿真行为与综合出的硬件行为完全不符。

第二部分:非阻塞赋值 (Non-Blocking Assignment, `<=`)

1. 专业描述

非阻塞赋值之所以叫“非阻塞”,是因为在一个 `begin...end` 块内,一条非阻塞赋值语句的执行不会阻塞后面语句的执行。

执行模型

并行调度,延迟更新:在一个 `always` 块中,当程序执行到 `a <= b;` 时,它会立即计算右侧 `b` 的值,但并不立即更新左侧 `a`。它只是把这个“更新计划”放入一个事件队列中。

同时发生: 块内所有的语句都会以这种“只计算,不更新”的方式执行完。

统一更新: 在整个 `always` 块执行完毕的那个时间点(Time Step)结束时,事件队列中所有的“更新计划”同时生效,所有左侧变量在这一瞬间同时被更新。

2. 硬件映射与适用场景

非阻塞赋值描述的是一种并行的、同步的更新。这正是时序逻辑(寄存器阵列)的特征。在一个时钟沿到来时,系统中所有的触发器(Flip-Flop)会同时采样各自输入端的数据,然后在时钟沿之后同时更新自己的输出。采样和更新这两个动作之间存在微妙的时间差,非阻塞赋值完美地模拟了这一硬件行为。

适用场景:时序逻辑 (`always @(posedge clk)`)

// 示例:一个三级移位寄存器
always @(posedge clk) begin
    q1 <= d_in;  // 计划1: q1 的新值是 d_in 的当前值
    q2 <= q1;    // 计划2: q2 的新值是 q1 的当前值 (旧值!)
    q3 <= q2;    // 计划3: q3 的新值是 q2 的当前值 (旧值!)
end

执行分析:

1. 时钟上升沿到来,`always` 块被触发。

2. 仿真器读取第一行 `q1 <= d_in;`。它计算出 `d_in` 的当前值,并计划在稍后将这个值赋给 `q1`。

3. 仿真器读取第二行 `q2 <= q1;`。它计算出 `q1` 的当前值(也就是上一个时钟周期的值),并计划在稍后将这个值赋给 `q2`。它不会使用本周期 `d_in` 的值。

4. 仿真器读取第三行 `q3 <= q2;`,同理,它使用的是 `q2` 的旧值。

5. 三行都执行完毕,`always` 块结束。

6. 此时,仿真器执行所有计划:`q1` 更新为 `d_in`,`q2` 更新为 `q1` 的旧值,`q3` 更新为 `q2` 的旧值。

这完美地模拟了三个D触发器级联构成的移位寄存器在硬件中的真实行为。

第三部分:黄金法则与实践指南 

为了在实际工程中正确使用,请牢记以下三条黄金法则:

法则一:描述时序逻辑时,总是使用非阻塞赋值 (`<=`)。

对象: `always @(posedge clk ...)` 块。

原因: 确保正确模拟寄存器在时钟沿同时采样的并行行为。

法则二:描述组合逻辑时,总是使用阻塞赋值 (`=`)。

对象: `always @(*)` 或 `always_comb` 块,以及 `assign` 语句。

原因: 确保正确模拟组合逻辑的数据流传递顺序,避免仿真中出现不必要的竞争冒险

法则三:不要在同一个 `always` 块中混用阻塞和非阻塞赋值。

原因: 混用会导致极其复杂的仿真调度行为,几乎一定会引发仿真与综合结果不匹配的问题。这是一个强烈的危险信号。

第四部分:危险的误用——仿真与综合的不匹配

让我们通过一个反例来深刻理解为什么必须遵守这些规则。错误示例:在时序逻辑块中使用阻塞赋值 (`=`) 来写移位寄存器

// !!! 这是一个错误的、危险的设计 !!!
always @(posedge clk) begin
    q1 = d_in;  // 阻塞赋值
    q2 = q1;    // 阻塞赋值
    q3 = q2;    // 阻塞赋值
end

1. 仿真器看到了什么?

由于是阻塞赋值,仿真器会顺序执行:

1. 时钟上升沿到来。

2. 执行 `q1 = d_in;`。`q1` 的值立即被更新为 `d_in` 的值。

3. 执行 `q2 = q1;`。此时 `q1` 已经是新值(等于 `d_in`),所以 `q2` 也立即被更新为 `d_in` 的值。

4. 执行 `q3 = q2;`。此时 `q2` 也已经是新值(等于 `d_in`),所以 `q3` 也立即被更新为 `d_in` 的值。 仿真结果: 在一个时钟周期内,`d_in` 的值直接传递给了 `q3`。仿真器模拟出的是一根导线,而不是一个三级移位寄存器!

2. 综合器看到了什么?

综合器(如Vivado)在解析这段代码时,它的目标是推断硬件结构。它看到 `always @(posedge clk)`,就知道要生成带时钟的硬件,即触发器。它看到 `q1`, `q2`, `q3` 三个变量在时钟沿被赋值,它就会为这三个变量分别推断出一个D触发器。它会根据赋值关系连接这些触发器:`d_in` 连接到 `q1` 的D输入端,`q1` 的输出连接到 `q2` 的D输入端,`q2` 的输出连接到 `q3` 的D输入端。

综合结果: 硬件上生成了一个标准的三级移位寄存器。数据需要三个时钟周期才能从 `d_in` 传递到 `q3`。


原文链接:,转发请注明来源!