第4章:Verilog基础语法:模块化设计、always块、assign语句、阻塞与非阻塞赋值

好,咱们进入正题。这一章讲的是Verilog最核心的几个语法点。说实话,很多新手一上来就被阻塞赋值和非阻塞赋值搞晕了。我当年也一样,第一次写计数器,波形仿真出来全是毛刺,查了半天才发现是赋值方式用错了。

咱们一个一个来。先聊聊模块化设计。

4.1 模块化设计——把大问题拆成小零件

什么叫模块化?说白了,就是搭积木。一个复杂的工业控制系统,你不可能把所有逻辑都塞到一个文件里。那样做,后期维护起来会让人崩溃的。

我在项目中遇到过这样一个案例:一个同事把整个电机控制逻辑写在一个模块里,足足两千行。后来要改一个PWM占空比的计算方式,他找了三天才找到对应的代码段。嗯,这就是没有模块化的后果。

模块化设计有几个好处:

  • 可复用:写好的模块,换个项目还能用
  • 可调试:哪个模块出问题,单独测它就行
  • 可维护:改一个功能,不用动整个工程

一个典型的模块长这样:

module counter (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [7:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;
        else if (en)
            count <= count + 1'b1;
    end
endmodule

你看,端口定义清晰,功能一目了然。这就是模块化的精髓——高内聚、低耦合

我的习惯:每个模块只做一件事。比如计数器模块只管计数,PWM模块只管生成波形。别把两个功能混在一起,否则后面调试起来你会想哭的。

4.2 always块——时序逻辑的发动机

always块是Verilog里最常用的过程块。它有两种触发方式:

  • 电平触发:用于组合逻辑
  • 边沿触发:用于时序逻辑

在工业控制中,我们绝大多数情况用的是边沿触发。你想想看,一个电机转速信号,如果用电平触发,那信号抖动一下,计数器就乱跳了。用边沿触发,只在时钟上升沿采样,稳定得多。

这里有个常见的坑:

// 错误写法:敏感列表不全
always @(a or b) begin
    c = a & b;
    d = c | e;  // e不在敏感列表里!
end

我曾经因为这个bug,整整排查了一个下午。仿真结果和实际硬件对不上,后来发现是敏感列表漏了一个信号。嗯,这里要注意:组合逻辑的敏感列表一定要写全。或者干脆用 always @(*),让工具自动推导。

4.3 assign语句——组合逻辑的快捷方式

assign语句,说白了就是连续赋值。它和always块的区别在于:

特性 assign always块
适用场景 组合逻辑 组合/时序逻辑
赋值方式 连续赋值 过程赋值
变量类型 wire reg
代码简洁度

我个人习惯是:简单的组合逻辑用assign,复杂的用always块。比如:

// 简单的数据选择器
assign data_out = (sel) ? data_a : data_b;

// 复杂的译码逻辑
always @(*) begin
    case (addr)
        3'b000: cs = 8'b0000_0001;
        3'b001: cs = 8'b0000_0010;
        // ... 更多分支
        default: cs = 8'b0000_0000;
    endcase
end
注意:assign语句的左边必须是wire类型。如果你试图给reg类型用assign赋值,综合工具会报错。我见过有人把reg和wire搞混,结果综合出来的电路和预期完全不一样。

4.4 阻塞与非阻塞赋值——最容易翻车的地方

好,重点来了。阻塞赋值(=)和非阻塞赋值(<=)的区别,是每个FPGA工程师必须搞清楚的。

简单说:

  • 阻塞赋值(=):顺序执行,后面的语句等前面的执行完
  • 非阻塞赋值(<=):并行执行,所有赋值同时发生

为什么会这样?因为硬件本身就是并行的。你想想看,一个D触发器,它的输出是在时钟沿来的时候同时更新的,不是先更新第一个再更新第二个。

看个例子:

// 阻塞赋值——容易出问题
always @(posedge clk) begin
    a = b;
    c = a;  // c得到的是a更新后的值
end

// 非阻塞赋值——正确做法
always @(posedge clk) begin
    a <= b;
    c <= a;  // c得到的是a更新前的值
end

在第一个例子中,c会得到b的值。因为a先被赋值,然后c再被赋值。但在硬件里,这两个操作应该是同时发生的。所以用阻塞赋值,你实际上是在描述一个和硬件行为不一致的逻辑。

黄金法则
- 时序逻辑用非阻塞赋值(<=)
- 组合逻辑用阻塞赋值(=)
- 同一个always块里,不要混用两种赋值方式

我曾经在一个项目里,把非阻塞赋值用在了组合逻辑里。结果仿真没问题,上板子就跑飞了。查了两天才发现,是因为非阻塞赋值在组合逻辑里会产生意想不到的延迟。嗯,从那以后,我再也不敢乱用了。

4.5 实战中的小技巧

最后分享几个我在工业控制项目里常用的技巧:

  1. 模块接口要规范:所有输入用wire,所有输出用reg。这样别人一看就知道哪个是驱动端,哪个是被驱动端。
  2. always块里尽量用非阻塞赋值:除非你明确知道自己在做组合逻辑。
  3. 敏感列表要完整:组合逻辑用 always @(*),时序逻辑用 always @(posedge clk or negedge rst_n)
  4. 一个always块只做一个功能:比如计数和状态机分开写,别混在一起。

好了,这一章的内容就这些。记住,Verilog语法不难,难的是理解硬件是怎么工作的。你写出来的代码,最终是要变成实实在在的电路。多想想「这个代码会综合成什么电路」,你的水平就会慢慢提升了。