2. Verilog模块结构:模块定义、端口声明、always块基础、assign语句
好,咱们直接进入正题。Verilog 里最核心的东西是什么?模块。你可以把模块想象成一个黑盒子——有输入、有输出,里面装着具体的逻辑。整个 FPGA 设计,说白了就是搭积木,把一个个模块拼起来。
我个人习惯,写任何代码之前,先画个框。这个框就是模块的边界。今天咱们就把这个框怎么画、怎么接线、怎么填逻辑,一次性说清楚。
2.1 模块定义——你的第一个“黑盒子”
模块定义,语法上就三部分:模块名、端口列表、功能描述。看个最简单的例子:
module led_controller (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
output reg led_out // LED 输出
);
// 功能描述写在这里
endmodule
嗯,这里要注意:module 和 endmodule 是成对出现的。少了任何一个,编译器都会报错。我在项目中遇到过新手同事,复制代码时漏了 endmodule,结果整个工程编译不过,找了半天才发现。
模块名我建议用下划线命名法,比如 uart_rx、spi_master。别用驼峰,也别用拼音。你想想看,如果模块名叫 dengpao_kongzhi,别人看了得多崩溃?
2.2 端口声明——给黑盒子接线
端口声明,就是告诉外界:我这个模块有哪些引脚。方向有三种:
| 方向 | 关键字 | 说明 |
|---|---|---|
| 输入 | input |
数据流入模块 |
| 输出 | output |
数据流出模块 |
| 双向 | inout |
数据可进可出(慎用) |
端口的数据类型,最常见的是 wire 和 reg。简单记:
- wire:连线型,用于组合逻辑。比如 assign 语句的输出。
- reg:寄存器型,用于时序逻辑。比如 always 块里的赋值。
但注意,reg 不一定会综合成寄存器。如果你在 always 块里写组合逻辑,reg 综合出来还是连线。这一点我刚开始学的时候也迷糊过,后来才明白——reg 只是语法上的变量类型,跟硬件没有必然对应关系。
input 和 output 分开写,每组按位宽对齐。这样代码看起来清爽,也方便后续维护。
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 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 知识体系图
下面这张图,帮你理清模块结构的关系:
2.6 实战建议
写模块时,我建议你遵循这个顺序:
- 先写端口:想清楚输入输出,确定位宽和方向。
- 再写内部信号:用 wire 还是 reg,心里要有数。
- 最后写逻辑:组合逻辑用 assign,时序逻辑用 always。
嗯,今天就到这里。记住一句话:模块是骨架,端口是神经,always 和 assign 是血肉。把这三样搞明白,后面的路就好走了。
- always 块里,时序用
<=,组合用=,别混。 - 敏感列表用
@(*),别手动列。 - assign 左边必须是 wire。
- 模块名别用拼音,别用驼峰。
公众号:蓝海资料掘金营,微信deep3321