4. Verilog基础回顾:模块化设计、always块、assign语句、阻塞与非阻塞赋值

好,咱们进入第四讲。说实话,很多同学觉得Verilog语法简单,翻翻书就会了。但我在实际带项目时发现,真正能把代码写「稳」的人,往往是对这几个基础概念理解最透的人。今天咱们就掰开揉碎了聊聊:模块化设计、always块、assign语句,还有那个让无数人翻车的阻塞与非阻塞赋值。

4.1 模块化设计——把大问题拆成小积木

我刚开始做FPGA时,喜欢把所有逻辑写在一个模块里。结果呢?调试一次要翻几百行代码,改一个功能怕影响全局。后来带我的老工程师说了一句话,我记到现在:「一个模块只做一件事,做好它。」

模块化设计的核心思想,说白了就是「分而治之」。你想想看,一个编码器解码系统,如果拆成:

  • 边沿检测模块:负责捕捉A、B信号的上升沿和下降沿
  • 方向判断模块:根据边沿顺序判断正反转
  • 计数模块:累加或累减位置值
  • 滤波模块:去除毛刺信号

每个模块单独写、单独仿真、单独验证。最后像搭积木一样拼起来。哪个环节出问题,直接定位到那个模块就行。

我的个人习惯:每个模块的代码行数尽量控制在200行以内。超过这个数,我就会问自己——是不是该拆了?

模块的端口定义也有讲究。我建议把所有输入输出信号都列清楚,用注释标明位宽和功能。别嫌麻烦,三个月后你自己回头看代码,会感谢现在的自己。

// 一个典型的编码器接口模块
module encoder_if (
    input  wire        clk,          // 系统时钟
    input  wire        rst_n,        // 异步复位,低有效
    input  wire        ch_a,         // 编码器A相
    input  wire        ch_b,         // 编码器B相
    output reg  [31:0] position,     // 当前位置值
    output reg         dir           // 方向:1正转,0反转
);

// 内部逻辑...

endmodule

4.2 always块——时序逻辑的「心脏」

always块是Verilog里最常用的过程块。但很多人用着用着就出问题。我见过最典型的错误,是在一个always块里同时处理太多事情,结果时序跑不过。

always块有两种常见写法:

类型 敏感列表 典型用途
组合逻辑 所有输入信号 译码器、多路选择器
时序逻辑 时钟沿 + 复位 计数器、状态机、寄存器

嗯,这里要注意:组合逻辑的always块里,敏感列表必须列全所有输入信号。漏掉一个,综合出来的电路就可能跟你想要的不一样。我曾经因为这个原因,在仿真时功能全对,上板子就乱跳,查了两天才发现是敏感列表少写了一个信号。

// 时序逻辑示例:边沿检测
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        ch_a_dly <= 1'b0;
        ch_a_rise <= 1'b0;
        ch_a_fall <= 1'b0;
    end else begin
        ch_a_dly <= ch_a;
        ch_a_rise <= ch_a & ~ch_a_dly;   // 上升沿检测
        ch_a_fall <= ~ch_a & ch_a_dly;   // 下降沿检测
    end
end

4.3 assign语句——组合逻辑的「快车道」

assign语句用来描述组合逻辑,它和always块最大的区别是:assign是连续赋值,只要右边信号变化,左边立刻更新。而always块是过程赋值,需要触发条件。

我个人习惯用assign来处理简单的组合逻辑,比如信号取反、位拼接、简单的算术运算。一旦逻辑复杂了,比如需要if-else判断,我就会改用always块。为什么?因为assign写复杂条件判断,可读性太差了。

// assign的典型用法
assign dir = (ch_a & ~ch_b) | (~ch_a & ch_b);  // 简单的异或逻辑
assign pos_update = ch_a_rise | ch_a_fall;      // 位置更新标志

小技巧:assign语句的左边必须是wire类型,不能是reg。这个初学者容易搞混。记住一句话:assign是「连线」,always是「存数」。

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

好,重点来了。阻塞赋值(=)和非阻塞赋值(<=)的区别,是Verilog里最基础也最容易出错的地方。我面试过不少工程师,能把这个讲清楚的人,基本功一般都不差。

简单说:

  • 阻塞赋值(=):顺序执行,写完一句再写下一句。用于组合逻辑。
  • 非阻塞赋值(<=):并行执行,所有赋值同时发生。用于时序逻辑。

为什么会这样?因为硬件电路本身就是并行的。你想想看,一个D触发器在时钟沿来临时,所有寄存器的更新是同时发生的,没有先后顺序。非阻塞赋值正好模拟了这个行为。

我曾经犯过一个经典错误:在同一个always块里,既用阻塞赋值又用非阻塞赋值,而且赋值之间有依赖关系。结果仿真和综合结果完全不一样。从那以后,我给自己定了个规矩:

铁律

  • 写时序逻辑(always @(posedge clk))时,一律用非阻塞赋值 <=
  • 写组合逻辑(always @(*) 或 assign)时,一律用阻塞赋值 =
  • 绝对不要在同一个always块里混用两种赋值方式

看个对比例子:

// 错误示范:时序逻辑里用了阻塞赋值
always @(posedge clk) begin
    a = b;      // 阻塞赋值
    c = a;      // 此时a已经更新,c拿到的是新a的值
end
// 综合出来可能只有一级寄存器,而不是两级
// 正确示范:时序逻辑用非阻塞赋值
always @(posedge clk) begin
    a <= b;     // 非阻塞赋值
    c <= a;     // 此时a还是旧值,c拿到的是旧a的值
end
// 综合出两级寄存器,符合预期

这个区别在编码器解码里特别重要。比如你要做两级同步来消除亚稳态,如果用了阻塞赋值,两级同步就变成了一级,亚稳态问题根本解决不了。

4.5 本章知识体系一览

说了这么多,咱们用一张图把核心逻辑串起来:

Verilog基础回顾:核心知识体系 Verilog 基础 模块化设计 分而治之,每个模块 只做一件事 always 块 时序逻辑的心脏 敏感列表要完整 assign 语句 组合逻辑快车道 左边必须是wire 阻塞与非阻塞赋值 最容易翻车的坑 时序用<=,组合用= 核心原则: 结构清晰 + 赋值正确 = 可综合、可调试的好代码 编码器解码中的应用:边沿检测、方向判断、计数、滤波 每个模块独立设计,最后通过顶层模块连接

这张图把咱们今天讲的内容串起来了。你想想看,模块化设计是骨架,always块和assign语句是血肉,阻塞与非阻塞赋值则是神经——用对了,整个系统才能协调工作。

我的建议:初学者可以先从简单的模块开始练手,比如写一个4位计数器,分别用阻塞和非阻塞赋值试试,看看仿真波形有什么不同。亲手试过,印象才深。

好了,这一讲就到这里。记住我今天说的这些,后面讲编码器解码实战时,咱们会反复用到这些基础概念。