3. Verilog基础(一):模块结构、端口定义、数据类型、参数化设计、always块与assign语句
各位同学,欢迎来到Verilog基础的第一讲。说实话,每次带新人时,我总会先问一个问题:“你觉得Verilog是硬件描述语言,还是硬件设计语言?” 很多人会愣住。其实,它既是描述,也是设计。你写下的每一行代码,最终都会变成实实在在的电路。所以,从一开始,我们就要带着“电路思维”去写代码。
今天这一章,我们先把地基打牢。我会带你过一遍模块结构、端口、数据类型这些最基础的东西。别觉得枯燥,这些细节决定了你未来能不能写出“可综合”的、不出bug的代码。
3.1 模块结构:一切设计的起点
在FPGA的世界里,模块(module) 就是最小的设计单元。你可以把它想象成一个黑盒子,有输入、有输出,内部封装了具体的逻辑功能。
一个完整的模块结构长这样:
module 模块名 (
端口列表
);
// 端口声明
// 内部信号声明
// 逻辑功能描述
endmodule
我个人习惯,module 和 endmodule 一定要对齐。代码的层次感,说白了就是可读性。你想想看,一个几千行的模块,如果缩进乱七八糟,调试起来得多痛苦。
uart_top、spi_master。别用驼峰命名,容易和系统函数混淆。
3.2 端口定义:模块的“嘴巴”和“耳朵”
端口就是模块和外界通信的通道。Verilog 定义了三种端口方向:
- input:输入端口,只能从外部读入数据。
- output:输出端口,只能从内部驱动出去。
- inout:双向端口,既能输入也能输出(比如I2C的SDA线)。
这里有个坑,我刚开始做设计时踩过:inout 端口内部必须用三态门驱动,否则综合会报错。我们来看个例子:
module my_module (
input wire clk, // 时钟
input wire rst_n, // 复位,低有效
input wire [7:0] data_in, // 8位输入数据
output reg [7:0] data_out, // 8位输出数据
inout wire sda // 双向数据线
);
// 内部逻辑
endmodule
注意看,data_out 我声明成了 output reg。为什么?因为我要在 always 块里给它赋值。如果只是用 assign 语句,那声明成 output wire 就行。嗯,这里要记住:端口类型要和驱动方式匹配。
3.3 数据类型:wire 与 reg 的“爱恨情仇”
这是新手最容易搞混的地方。我直接说结论:
| 数据类型 | 本质 | 驱动方式 | 典型用途 |
|---|---|---|---|
wire |
线网型,相当于物理连线 | 只能被 assign 或模块端口驱动 |
组合逻辑、模块间连线 |
reg |
寄存器型,代表存储单元 | 只能在 always 块中被赋值 |
时序逻辑、状态机 |
但是!注意这个但是——reg 不一定会综合成寄存器。如果你在 always 块里写的是组合逻辑(比如 always @(*)),那 reg 综合出来还是连线。我曾经见过有人把 reg 和“寄存器”划等号,结果综合出来的面积比预期大了一倍。
3.4 参数化设计:让模块“活”起来
写死代码是最容易的,但也是最蠢的。参数化设计,说白了就是给模块留个“旋钮”,让你能随时调整位宽、深度、计数阈值等。
Verilog 里用 parameter 关键字定义参数:
module counter #(
parameter WIDTH = 8, // 计数器位宽,默认8位
parameter MAX = 255 // 计数最大值
) (
input wire clk,
input wire rst_n,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (count == MAX)
count <= 0;
else
count <= count + 1;
end
endmodule
实例化时,你可以覆盖默认参数:
counter #(
.WIDTH(16),
.MAX (50000)
) u_counter (
.clk (clk),
.rst_n(rst_n),
.count(cnt_16bit)
);
我在项目中遇到过,一个 SPI 控制器因为位宽写死,导致换芯片后全部重写。从那以后,我所有模块都强制加参数化设计。你想想看,改一个参数和改几百行代码,哪个划算?
DATA_WIDTH、FIFO_DEPTH。
3.5 always 块与 assign 语句:两种驱动方式
这是 Verilog 里最核心的两个语法结构。简单来说:
- assign:连续赋值,用于组合逻辑。等号左边必须是 wire 类型。
- always:过程赋值,用于组合或时序逻辑。等号左边必须是 reg 类型。
先看 assign 的例子:
assign data_out = (sel) ? data_a : data_b;
这行代码综合出来就是一个多路选择器。assign 语句是并行执行的,和代码顺序无关。
再看 always 块:
// 时序逻辑:带同步复位的D触发器
always @(posedge clk) begin
if (rst_n)
q <= 0;
else
q <= d;
end
// 组合逻辑:译码器
always @(*) begin
case (sel)
2'b00: y = 4'b0001;
2'b01: y = 4'b0010;
2'b10: y = 4'b0100;
2'b11: y = 4'b1000;
default: y = 4'b0000;
endcase
end
注意两个细节:
- 时序逻辑用
posedge clk或negedge clk触发,组合逻辑用@(*)自动推导敏感列表。 - 时序逻辑用
<=(非阻塞赋值),组合逻辑用=(阻塞赋值)。混用会导致仿真和综合结果不一致。
3.6 本章知识体系
为了让你更直观地理解本章内容,我画了一张结构图:
这张图把本章的核心知识点串起来了。你可以看到,模块结构是骨架,端口是接口,数据类型是血液,参数化是灵活性,而 always 和 assign 是驱动逻辑的两种方式。它们共同构成了一个完整的 Verilog 设计单元。
好了,这一章的内容就到这里。记住,基础不牢,地动山摇。下一章我们会深入运算符和表达式,到时候你会发现,今天学的这些概念会反复用到。好好消化,我们下次见。