2. Verilog模块结构:模块定义、端口声明、always块基础、assign语句

好,咱们直接进入正题。Verilog 里最核心的东西是什么?模块。你可以把模块想象成一个黑盒子——有输入、有输出,里面装着具体的逻辑。整个 FPGA 设计,说白了就是搭积木,把一个个模块拼起来。

我个人习惯,写任何代码之前,先画个框。这个框就是模块的边界。今天咱们就把这个框怎么画、怎么接线、怎么填逻辑,一次性说清楚。

2.1 模块定义——你的第一个“黑盒子”

模块定义,语法上就三部分:模块名、端口列表、功能描述。看个最简单的例子:

module led_controller (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    output reg        led_out   // LED 输出
);

// 功能描述写在这里

endmodule

嗯,这里要注意:moduleendmodule 是成对出现的。少了任何一个,编译器都会报错。我在项目中遇到过新手同事,复制代码时漏了 endmodule,结果整个工程编译不过,找了半天才发现。

模块名我建议用下划线命名法,比如 uart_rxspi_master。别用驼峰,也别用拼音。你想想看,如果模块名叫 dengpao_kongzhi,别人看了得多崩溃?

2.2 端口声明——给黑盒子接线

端口声明,就是告诉外界:我这个模块有哪些引脚。方向有三种:

方向 关键字 说明
输入 input 数据流入模块
输出 output 数据流出模块
双向 inout 数据可进可出(慎用)

端口的数据类型,最常见的是 wirereg。简单记:

  • wire:连线型,用于组合逻辑。比如 assign 语句的输出。
  • reg:寄存器型,用于时序逻辑。比如 always 块里的赋值。

但注意,reg 不一定会综合成寄存器。如果你在 always 块里写组合逻辑,reg 综合出来还是连线。这一点我刚开始学的时候也迷糊过,后来才明白——reg 只是语法上的变量类型,跟硬件没有必然对应关系。

我的小技巧:端口声明时,我习惯把 inputoutput 分开写,每组按位宽对齐。这样代码看起来清爽,也方便后续维护。

2.3 assign 语句——组合逻辑的“直通车”

assign 是连续赋值语句。说白了,就是左边信号的值,始终等于右边表达式的计算结果。只要右边变了,左边立刻跟着变。

举个例子:

module and_gate (
    input  wire a,
    input  wire b,
    output wire y
);

assign y = a & b;

endmodule

这个模块实现了一个与门。只要 a 或 b 变化,y 会立即更新。没有时钟,没有延迟——这就是组合逻辑的本质。

我曾经在项目中用 assign 写过一个多路选择器,结果因为表达式太长,综合后时序不满足。后来拆成几个短 assign,问题就解决了。所以我的建议是:一个 assign 只做一件事。别把几十个信号塞进一个表达式里,那是在给自己挖坑。

避坑指南:assign 的左边必须是 wire 类型,不能是 reg。如果你写了 assign reg_a = ...,编译器会直接报错。我曾经因为这个低级错误,浪费了半小时查语法手册。

2.4 always 块基础——时序逻辑的“心脏”

如果说 assign 是组合逻辑的直通车,那 always 块就是时序逻辑的心脏。它有两种常见写法:

2.4.1 时序逻辑:带时钟和复位

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        led_out <= 1'b0;
    end else begin
        led_out <= ~led_out;
    end
end

这段代码实现了一个简单的分频器——每个时钟上升沿,LED 翻转一次。注意这里用的是 非阻塞赋值 <=。为什么?因为时序逻辑里,所有赋值应该同时发生。如果用 =,就会出现竞争,仿真结果会乱掉。

我个人习惯,写 always 块时先写复位逻辑,再写正常逻辑。这样结构清晰,也方便别人阅读。

2.4.2 组合逻辑:不带时钟

always @(*) begin
    if (sel) begin
        y = a;
    end else begin
        y = b;
    end
end

这里用的是 阻塞赋值 =。因为组合逻辑不需要时钟,赋值顺序就是执行顺序。注意敏感列表写 @(*),表示所有输入信号变化都会触发这个块。我建议你永远用 @(*),别手动列信号——漏一个就出大问题。

核心区别:
  • 时序逻辑 → 用 <=(非阻塞)
  • 组合逻辑 → 用 =(阻塞)
混用会怎样?仿真结果和实际硬件不一致。我吃过这个亏,真的。

2.5 知识体系图

下面这张图,帮你理清模块结构的关系:

Verilog 模块 模块定义 端口声明 功能描述 module/endmodule 命名规范 input/output/inout wire/reg 类型 assign 语句 always 块 模块 = 定义 + 端口 + 功能 功能描述:assign(组合) / always(时序+组合)

2.6 实战建议

写模块时,我建议你遵循这个顺序:

  1. 先写端口:想清楚输入输出,确定位宽和方向。
  2. 再写内部信号:用 wire 还是 reg,心里要有数。
  3. 最后写逻辑:组合逻辑用 assign,时序逻辑用 always。

嗯,今天就到这里。记住一句话:模块是骨架,端口是神经,always 和 assign 是血肉。把这三样搞明白,后面的路就好走了。

避坑总结:
  • always 块里,时序用 <=,组合用 =,别混。
  • 敏感列表用 @(*),别手动列。
  • assign 左边必须是 wire。
  • 模块名别用拼音,别用驼峰。

公众号:蓝海资料掘金营,微信deep3321