4. Verilog语法基础(下):assign连续赋值、always过程块、阻塞与非阻塞赋值、initial块

好,咱们接着往下聊。上一节我们把Verilog的基本数据类型和运算符过了一遍,今天要讲的这几个概念,可以说是Verilog里最核心、也最容易让人栽跟头的地方。

我个人觉得,搞懂今天这节内容,你写出来的代码才算真正有了“硬件思维”。说白了,Verilog不是C语言,你不能指望它从上到下顺序执行。咱们今天要聊的assign、always、阻塞与非阻塞,就是帮你建立这种思维的关键。

4.1 assign连续赋值——组合逻辑的“直通车”

先说说assign。这个关键字,我习惯叫它“连续赋值”。为什么叫连续?因为它只要右边的信号一变,左边立刻跟着变,没有延迟,没有时钟控制。

核心理解:assign描述的是组合逻辑,相当于硬件里直接用导线连起来。

举个例子,你想实现一个半加器:

module half_adder(
    input  a, b,
    output sum, cout
);
    assign sum  = a ^ b;   // 异或,得到和
    assign cout = a & b;   // 与,得到进位
endmodule

你看,只要a或b变了,sum和cout立刻重新计算。这就是连续赋值的特点——它没有“记忆”,纯粹是输入到输出的映射。

我的经验:我在项目中遇到过有人用assign写了一个很长的表达式,结果综合出来一大堆LUT,时序跑不过。后来拆成几个简单的assign,中间加了一级寄存器,问题就解决了。所以,assign虽然方便,但别滥用,太复杂的组合逻辑该拆就拆。

assign还有一些使用规则:

  • 左边必须是wire类型,不能是reg
  • 一个wire只能被一个assign驱动
  • 可以在assign里用条件运算符(? :),实现简单的多路选择
// 用assign实现2选1多路器
assign out = sel ? b : a;

4.2 always过程块——时序逻辑的“心脏”

如果说assign是组合逻辑的直通车,那always就是时序逻辑的发动机。它不像assign那样“时刻盯着”,而是由敏感列表里的信号变化来触发。

always块的语法长这样:

always @(敏感列表) begin
    // 过程赋值语句
end

敏感列表里写什么,决定了这个always块什么时候执行。我刚开始学的时候,总觉得这东西像C语言里的while(1),后来才明白完全不是一回事。

4.3 阻塞赋值与非阻塞赋值——最容易翻车的坑

嗯,这里要注意。阻塞赋值(=)和非阻塞赋值(<=)的区别,是Verilog新手最容易搞混的地方。我当年也在这上面吃过亏,仿真结果和实际硬件对不上,查了半天才发现是赋值方式用错了。

咱们直接上对比:

特性 阻塞赋值 = 非阻塞赋值 <=
执行顺序 顺序执行,阻塞后面的语句 并行执行,不阻塞
使用场景 组合逻辑(always @(*) 或 assign) 时序逻辑(always @(posedge clk))
硬件行为 像导线,立即传递 像寄存器,时钟沿更新
典型错误 在时序逻辑里用阻塞赋值,导致仿真与硬件不符 在组合逻辑里用非阻塞赋值,产生意外锁存器

为什么会有这种区别?你想想看,硬件里的寄存器是在时钟沿统一更新的,不是谁先写谁就先变。非阻塞赋值正好模拟了这种行为——所有赋值在时钟沿到来时同时生效。

避坑指南:我曾经在一个项目里,用always @(posedge clk)写了一个移位寄存器,结果用了阻塞赋值。仿真看起来没问题,但上板子后数据全乱了。后来发现,阻塞赋值导致同一个时钟沿内,前一个寄存器的值立刻被后一个读到,相当于一个时钟沿移了多位。从那以后,我养成了一个习惯:时序逻辑里只用非阻塞赋值,组合逻辑里只用阻塞赋值。

来看一个正确的例子:

// 正确的移位寄存器写法
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        reg1 <= 1'b0;
        reg2 <= 1'b0;
        reg3 <= 1'b0;
    end else begin
        reg1 <= data_in;
        reg2 <= reg1;   // 这三个赋值同时生效
        reg3 <= reg2;   // 不会出现“穿透”现象
    end
end

4.4 initial块——仿真的“启动器”

initial块,说白了就是仿真专用的。它只在仿真开始时执行一次,然后就不动了。硬件综合工具会忽略它,所以它只能用在testbench里。

它的典型用法:

initial begin
    clk = 0;
    rst_n = 0;
    #100 rst_n = 1;   // 100个时间单位后释放复位
    #500 $finish;     // 500个时间单位后结束仿真
end

// 生成时钟
always #10 clk = ~clk;  // 每10个时间单位翻转一次

我个人习惯在initial块里做三件事:初始化信号、产生复位、控制仿真时长。别用它来写复杂的逻辑,那会让仿真变得很难调试。

4.5 知识体系总览

说了这么多,咱们用一张图把今天的内容串起来:

Verilog 赋值与过程块知识体系 连续赋值 (assign) 过程块 (always / initial) 组合逻辑,左边必须是 wire 输入变化立即输出,无记忆 适合简单组合逻辑、数据通路 由敏感列表触发执行 时序逻辑用 <=,组合逻辑用 = initial 仅仿真用,综合忽略 核心原则: 时序逻辑用非阻塞,组合逻辑用阻塞

这张图把今天的内容分成了两大块:左边是assign连续赋值,右边是always和initial过程块。最下面那句话,是我最想让你记住的——时序逻辑用非阻塞,组合逻辑用阻塞。记住了这个,能避开80%的坑。

一个小技巧:如果你不确定该用哪种赋值方式,先问自己一个问题:“这个always块是不是由时钟沿触发的?”如果是,就用<=;如果不是(比如always @(*)),就用=。这个方法我用了好多年,从来没出过错。

好了,今天的内容就到这儿。assign、always、阻塞与非阻塞,这三个概念你搞清楚了,Verilog就算入门一半了。剩下的就是多写、多仿真、多踩坑——嗯,踩坑也是学习的一部分嘛。

可可精品资料库