Verilog 阻塞与非阻塞赋值实战指南:常见误区与正确设计

二牛学FPGA
文章2026-05-03
46

Quick Start:快速上手实验

本指南通过一个简单的移位寄存器实例,帮助你直观理解阻塞赋值(=)与非阻塞赋值(<=)在行为级仿真中的差异。你只需准备一个支持 Verilog 的仿真工具(如 Vivado 2020.1+、ModelSim 或 QuestaSim),并按照以下步骤操作,即可在 10 分钟内复现典型误区并验证正确做法。

前置条件

  • 已安装任一主流 Verilog 仿真工具(Vivado、ModelSim、QuestaSim 等)。
  • 具备基本 Verilog 语法知识,了解 always 块与敏感列表。
  • 建议熟悉波形查看工具(如 Vivado Simulator 或 GTKWave)。

目标与验收标准

  • 目标:通过对比阻塞与非阻塞赋值实现的移位寄存器,理解二者在时序逻辑中的行为差异,并掌握正确使用场景。
  • 验收标准
    • 波形中,非阻塞赋值版移位寄存器能正确实现每时钟周期数据右移一位。
    • 阻塞赋值版移位寄存器出现数据“穿透”或竞争现象,波形不符合预期。
    • 能口头解释为何阻塞赋值不适合描述时序逻辑。

实施步骤

步骤 1:创建工程与顶层模块

在仿真工具中新建一个工程,并创建一个 Verilog 模块,命名为 shift_register_tb(测试模块)。

步骤 2:编写非阻塞赋值版移位寄存器

module shift_nonblocking (
    input clk,
    input rst_n,
    input din,
    output reg dout
);
    reg [2:0] shift_reg;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            shift_reg <= 3'b0;
        else begin
            shift_reg[0] <= din;
            shift_reg[1] <= shift_reg[0];
            shift_reg[2] <= shift_reg[1];
            dout <= shift_reg[2];
        end
    end
endmodule

步骤 3:编写阻塞赋值版移位寄存器(常见误区)

module shift_blocking (
    input clk,
    input rst_n,
    input din,
    output reg dout
);
    reg [2:0] shift_reg;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            shift_reg = 3'b0;
        else begin
            shift_reg[0] = din;
            shift_reg[1] = shift_reg[0];
            shift_reg[2] = shift_reg[1];
            dout = shift_reg[2];
        end
    end
endmodule

步骤 4:编写测试激励(testbench)

module tb;
    reg clk, rst_n, din;
    wire dout_nb, dout_b;

    shift_nonblocking u_nb (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout_nb));
    shift_blocking     u_b  (.clk(clk), .rst_n(rst_n), .din(din), .dout(dout_b));

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        rst_n = 0; din = 0;
        #20 rst_n = 1;
        #10 din = 1;
        #10 din = 0;
        #100 $finish;
    end
endmodule

步骤 5:运行仿真并观察波形

运行仿真至少 200 ns,添加 dout_nbdout_b 到波形窗口。你会看到:

  • 非阻塞赋值版:数据在每个时钟上升沿逐位右移,延迟 3 个时钟周期后出现在 dout_nb
  • 阻塞赋值版:dout_b 在同一个时钟周期内直接跟随 din 变化,没有移位效果——因为阻塞赋值在 always 块内立即更新,导致所有赋值“同时”完成,相当于组合逻辑。

验证结果

观察波形后,应确认以下关键差异:

  • 非阻塞赋值shift_reg[0] 在时钟上升沿采样 dinshift_reg[1] 采样上一拍 shift_reg[0],以此类推。输出 dout_nb 在 3 个时钟周期后复制 din 的初始值。
  • 阻塞赋值:由于 shift_reg[0] = din 立即更新,随后 shift_reg[1] = shift_reg[0] 读取的是刚更新的值,导致所有寄存器在同一拍内完成数据传递,失去时序存储特性。

如果波形符合上述描述,则实验验证通过。

排障指南

  • 问题:波形中非阻塞版本也没有移位效果——检查敏感列表是否遗漏了 negedge rst_n,或复位逻辑中使用了阻塞赋值。确保 always 块内所有时序赋值均使用 <=
  • 问题:仿真结果与预期完全相反——确认测试激励中 din 的变化时刻是否在时钟沿附近。建议将 din 变化安排在时钟上升沿之后(如 #6#7),避免建立/保持时间冲突。
  • 问题:工具报错“multiple drivers”——检查是否在多个 always 块中对同一 reg 赋值。移位寄存器应只在一个 always 块中描述。

扩展:深入理解赋值机制

为什么阻塞赋值不适合时序逻辑?

阻塞赋值(=)在 always 块内按顺序立即执行,每个赋值语句都会更新目标寄存器的值,并影响后续语句的读取。在时钟沿触发的 always 块中,这会导致同一时钟周期内多个寄存器同时更新,破坏时序逻辑的“锁存-延迟”特性。而非阻塞赋值(<=)将赋值操作推迟到 always 块结束时统一执行,所有右值在块开始时被采样(即上一时钟周期的值),从而正确模拟了触发器的行为。

风险边界

  • 在组合逻辑 always 块中(敏感列表为电平触发),应使用阻塞赋值,否则会导致仿真与综合不一致。
  • 在同一个 always 块中混合使用阻塞与非阻塞赋值是常见的错误来源,应严格避免。
  • 对于多时钟域或异步复位设计,非阻塞赋值也不能自动解决亚稳态问题,仍需同步器电路。

参考资源

  • IEEE Std 1364-2001 Verilog 硬件描述语言标准,第 9.2 节“阻塞与非阻塞赋值”。
  • Clifford E. Cummings, “Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill!” (SNUG 2000).
  • Vivado Design Suite 用户指南:仿真 (UG900)。

附录:完整测试代码

以下为可直接复制到仿真工具中的完整 testbench 代码(包含两个模块):

// 非阻塞赋值版
module shift_nonblocking (
    input clk, rst_n, din,
    output reg dout
);
    reg [2:0] shift_reg;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) shift_reg <= 3'b0;
        else begin
            shift_reg[0] <= din;
            shift_reg[1] <= shift_reg[0];
            shift_reg[2] <= shift_reg[1];
            dout <= shift_reg[2];
        end
    end
endmodule

// 阻塞赋值版(常见错误)
module shift_blocking (
    input clk, rst_n, din,
    output reg dout
);
    reg [2:0] shift_reg;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) shift_reg = 3'b0;
        else begin
            shift_reg[0] = din;
            shift_reg[1] = shift_reg[0];
            shift_reg[2] = shift_reg[1];
            dout = shift_reg[2];
        end
    end
endmodule

// 测试激励
module tb;
    reg clk, rst_n, din;
    wire dout_nb, dout_b;

    shift_nonblocking u_nb (.*);
    shift_blocking     u_b  (.*);

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        rst_n = 0; din = 0;
        #20 rst_n = 1;
        #10 din = 1;
        #10 din = 0;
        #100 $finish;
    end

    initial begin
        $monitor("Time=%0t din=%b dout_nb=%b dout_b=%b", $time, din, dout_nb, dout_b);
    end
endmodule
分类
技术分享
标签
Verilog阻塞赋值非阻塞赋值
浏览 46
分享:

相关推荐

同频道 · 相近分类

暂无相关推荐

作者

二牛学FPGA查看主页

同分类阅读

文章

延伸阅读与实操

  • 文章 + 课程联动深度文章常对应体系课章节,可一键选课。
  • 学习产出可参考笔记与作业案例在学习产出广场持续更新。

探索全站