Verilog 14: 阻塞和非阻塞赋值的异同

2023-12-25 17:40:28

verilog 的层次化事件队列

仿真器在解析和处理 Verilog 模块时其执行流程如下:

  1. 动态事件队列 (下列事件执行顺序可任意安排)
    • 阻塞赋值
    • 计算非阻塞赋值语句右边的表达式
    • 连续赋值
    • 执行 $display 命令
    • 计算原语的输入和输出的变化
  2. 停止运行的事件队列:#0延时阻塞赋值
  3. 非阻塞事件队列:更新非阻塞赋值语句LHS(左边变量)的值
  4. 监控事件队列
    1. 执行$monitor命令;
    2. 执行$strobe命令。
  5. 其它自定义 pli 命令

阻塞和非阻塞赋值的异同

定义

RHS: 右值, 赋值等号右边的表达式或变量
LHS: 左值, 赋值等号左边的表达式或变量

阻塞赋值 =

所谓的阻塞概念是指在同一个 always 块中, 在赋值时先计算 RHS 表达式的值,这时赋值语句不允许任何别的 Verilog 语句干扰,直到赋值完成它才允许别的赋值语句执行。

对于可综合的阻塞赋值语句,因为不允许延时,它与后面的赋值语句实际是一个先后关系,即前面表达式的电路将输出给后面表达式的电路。

仿真时,阻塞赋值会在计算出 RHS 表达式后立即赋值给 LHS,然后执行下一条语句。当并行块中存在变量的相互引用和赋值时,这种立刻发生改变的赋值方式将导致竞争冒险。

例如下面的模块是竞争的,当复位信号由 1 变 0 后

  • 若第一个 always 时钟沿先触发, 则 y1 = y2 = 1, 而后 y2 = y1 = 1
  • 若第二个 always 时钟沿先触发, 则 y2 = y1 = 0, 而后 y1 = y2 = 0
module fboscl(
    input clk, rst,
    output y1, y2
);

    reg y1, y2;

    always @(posedge clk or posedge rst)
        if(rst) y1 = 0;
        else    y1 = y2;

    always @(posedge clk or posedge rst)
        if(rst) y2 = 1;
        else    y2 = y1;

endmodule
非阻塞赋值 <=

非阻塞赋值也是先计算 RHS 表达式的值,但赋值操作结束时刻才更新 LHS
非阻塞赋值只能对寄存器类型变量进行赋值,因此只能在过程块中,不允许连续赋值

仿真时,阻塞赋值会在计算出 RHS 表达式将其保存起来,并往下执行,当离开 always 块后才赋值给 LSH。

例如对前一个例子改为非阻塞赋值后,当复位由 1 变为 0 后,不论哪个 always 块的时钟沿先到达,两个 always 块的非阻塞赋值都在赋值开始时刻计算 RHS 表达式,并保存到中间变量。而在离开 always 后才更新 LHS。因此 RHS 计算的结果是确定的。之后赋值给 LHS 的值也是确定的。

module fboscl(
    input clk, rst,
    output y1, y2
);

    reg y1, y2;

    always @(posedge clk or posedge rst)
        if(rst) y1 <= 0;
        else    y1 <= y2;

    always @(posedge clk or posedge rst)
        if(rst) y2 <= 1;
        else    y2 <= y1;

endmodule

在这个例子中阻塞赋值和非阻塞赋值虽然综合出来的电路是一样的,但是仿真结果却不一样。使用非阻塞赋值使得前仿真和后仿真结果是一致的。

自触发时钟

如下的时钟振荡器使用了阻塞赋值, 在 initial 块,当 clk 由不确定态变为 0 使得 always 块的 @(clk) 条件触发。

always 块触发后先经过 10 个单位时间的延迟,然后计算 RHS 表达式 (~clk) 得到 1 并立即更新 LHS 的值,clk 立即被赋予 1。

当 clk 电平已经为 1,才跳出 always,这时 @(clk) 无法感知从 0 到 1 曾经发生过的变化,所以就阻塞在那,只有等到 clk 变为 0 才能进入下一句。因此这是一个不能自触发的振荡器。

module mod(
    output clk
);
    initial #10 clk = 0;
    always @(clk) #10 clk = ~clk;
endmodule

当采用非阻塞赋值时, @(clk) 的第一次触发之后,RHS 表达式 (~clk) 先被计算出来,但是还没发生实际的赋值操作,而是等离开 always 块后赋值事件才发生。

这时 always @(clk) 能够检测到 clk 的变化,从而能够进入下一次赋值操作,如此往复。

module mod(
    output clk
);
    initial #10 clk = 0;
    always @(clk) #10 clk <= ~clk;
endmodule

移位寄存器

1. 不正确地使用阻塞赋值实现移位寄存器

因为 q3 的最终输出依赖 q1 和 q1 以及 d 的值, 而这些值的计算在离开 always 块前就被赋值和确定,因而三个赋值语句最终等价于 q3 = d;

module pipebl(
    input clk,
    input [7:0] d,
    output [7:0] q3
);

    reg [7:0] q3, q2, q1;

    always @(posedge clk) begin
        q1 = d;
        q2 = q1;
        q3 = q2;
    end
endmodule
2. 正确用阻塞方式实现移位寄存器

需要考虑阻塞赋值的顺序, 仿真和综合结果一致,但是不推荐这种实现方法。

q3 = q2;
q2 = q1;
q1 = d;
3. 多个 always 阻塞赋值描述移位寄存器风格

阻塞赋值被放在了不同的 always 块中,仿真时这些块的先后顺序是随机的,而阻塞赋值离开 always 块前就已经发生赋值, 因此发生了竞争冒险。

但是综合的结果却是正确的移位寄存器,也就是说前仿真和后仿真结果可能不一致。

always @(posedge clk) q1 = d;
always @(posedge clk) q2 = q1;
always @(posedge clk) q3 = q2;

上述三个例子若都采用非阻塞赋值,则仿真和综合结果都是一致且正确的。

仿真

module tb;

    reg clk;
    reg [7:0] d;
    wire [7:0] q3;

    initial begin
        clk = 0;
        d = 0;
        forever #10 begin
            clk = ~clk;
            d = clk == 1 ? d + 1: d;
        end
    end

    pipebl u_pipebl(clk, d, q3);

endmodule

虽然在时序逻辑中使用阻塞赋值如果考虑周到也能建立正确的模型,通过仿真并综合成期望的逻辑。
但是这容易养成使用阻塞赋值的习惯,在较为复杂的有多个 always 块的设计项目中就可能会发生竞争冒险。
因此在时序逻辑中应当时刻使用非阻塞赋值比较好。

组合逻辑建模时应使用非阻塞赋值

1. 使用非阻塞赋值描述组合逻辑

由于非阻塞赋值会先计算 RHS 的值,在离开 always 块后才发生赋值,因此 tmp1 和 tmp2 被计算时仍是进入 always 块时的值, 而不是在 always 块中经过计算得到值。

从而在仿真时 y 的值并没能更新正确的值,对于第一次触发 always 条件时,y 的值将为不确定值。

虽然能够综合出正确的电路,但是前仿真和后仿真将不一致。

module ao4(
    input a, b, c, d,
    output y
);

    reg y, tmp1, tmp2;

    always @(a or b or c or d) begin
        tmp1 <= a & b;
        tmp2 <= c & d;
        y <= tmp1 | tmp2;
    end
endmodule
2. 将 tmp1 和 tmp1 加入到敏感事件表

通过将 tmp1 和 tmp2 中间变量加入到敏感事件表,使得当第一次进入 always 并离开时对 tmp1 和 tmp2 的更新能够再次触发 always,从而给 y 的值进行更新。

虽然这种方法是可行的,但是一次 always 有多次参数传递,降低了仿真器的性能。

always @(a or b or c or d or tmp1 or tmp2) begin
    tmp1 <= a & b;
    tmp2 <= c & d;
    y <= tmp1 | tmp2;
end
3. 使用阻塞赋值实现

通过对 tmp1 和 tmp2 的赋值使用阻塞赋值,使得 tmp1 和 tmp2 的值总是能够立即得到更新,这样在计算 y 时,RHS 表达式中的 tmp1 和 tmp2 的值总是最新的。

这时,不论 y 使用非阻塞赋值(离开 always 块后才进行赋值), 还是使用阻塞赋值(立即进行赋值), y 的值总是正确的了。

always @(a or b or c or d) begin
    tmp1 = a & b;
    tmp2 = c & d;
    y <= tmp1 | tmp2;
end

不过为了统一性,y 还是也使用阻塞赋值比较好,即 y = tmp1 | tmp2; 并避免混合使用非阻塞和阻塞赋值。

时序和组合的混合逻辑中使用非阻塞赋值

有时为了方便,将组合逻辑和时序逻辑写到一个 always 块中,这时应遵循时序逻辑建模的原则

module nbex2(
    input clk, rst_n;
    input a, b;
    output reg q;
);

    always @(posedge clk or negedge rst_n)
        if(!rst_n)
            q <= 0;         // 时序逻辑
        else q <= a^b;      // 异或,为组合逻辑
endmodule

等价于

reg y;
// 纯时序逻辑
always @(posedge clk or negedge rst_n)
    if(!rst_n)
        q <= 0; 
    else q <= y;

// 纯组合逻辑
always @(a or b)
    y = a ^ b;

非阻塞赋值的3个问题

1. 变量显示问题

display 立即显示该值, strobe 其他语句执行完毕之后,才执行显示任务,monitor 监视变量,只要发生变化就执行显示任务

module display_cmds:
    reg a;

    initial $monitor("\$monitor: a = %b", a);

    initial begin 
        $strobe("\$strobe: a = %b", a);
        a = 0;
        a <= 1;
        $display("\$display: a = %b", a);
        #1 $finish;
    end 
endmodule

将输出

$display: a = 0
$monitor: a = 1
$strobe: a = 1
#0 使用问题

#0 就像一个微小的延时, 用于模拟实际电路波形上升或下降的延迟区间。

#0 可将事件强行插入到停止运行的事件队列。

原本 display 事件和连续赋值事件 b = a 属于动态事件队列,其执行顺序是未定义的。 通过对 display 添加一个 #0 延时, 使得显示事件被插入到停止运行的事件队列中, 从而显示一定是在连续赋值之后运行,保证 b 值一定是 0.

`timescale 1ns/1ps
module test();
    reg a;
    wire b;

    initial begin
        #1;
        a = 1;
        #1;
        a = 0;
        #0 $display("b is %b", b);
    end
    assign b = a;
endmodule
3. 对于同一个变量进行多次非阻塞赋值

最后一个非阻塞赋值决定了变量的值

initial begin
    a <= 0;
    a <= 1;
end

文章来源:https://blog.csdn.net/qq_36525177/article/details/135203241
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。