4. Verilog基础语法(上):模块定义、端口声明、数据类型、参数化设计
好,咱们正式开始写Verilog了。说实话,很多初学者一上来就被各种语法细节吓住,其实没必要。你想想看,Verilog说白了就是描述硬件的一种语言,跟C语言那种软件思维完全不同。今天这一章,我带你先把最核心的四个概念捋清楚:模块怎么搭、端口怎么接、数据怎么存、参数怎么配。
4.1 模块定义——你的第一个硬件“黑盒子”
模块(module)是Verilog的基本单元。你可以把它想象成一个黑盒子:有输入、有输出、内部有逻辑。我刚开始学的时候,总觉得模块定义就是写个“外壳”,后来才发现,这个外壳的写法直接决定了代码的可读性和可维护性。
模块的基本结构:
module 模块名 (
端口列表
);
// 内部逻辑
endmodule
举个例子,一个最简单的与门模块:
module and_gate (
input wire a,
input wire b,
output wire y
);
assign y = a & b;
endmodule
嗯,这里要注意:模块名最好用有意义的英文单词,别用a1、b2这种。我在项目中见过一个同事,模块名叫“test1”、“test2”,三个月后他自己都看不懂了。
4.2 端口声明——输入、输出、双向
端口就是模块跟外界通信的通道。Verilog里端口有三种方向:
| 方向 | 关键字 | 说明 |
|---|---|---|
| 输入 | input | 数据从外部流入模块 |
| 输出 | output | 数据从模块流出到外部 |
| 双向 | inout | 数据可以双向流动(慎用) |
避坑指南:我曾经在项目里滥用inout端口,结果综合时出了一堆三态冲突。后来我给自己定了个规矩:除非是总线接口(比如I2C的SDA),否则尽量别用inout。内部模块之间用input/output就足够了。
端口声明的两种风格,我个人习惯用ANSI风格:
// ANSI风格(推荐)
module counter (
input wire clk,
input wire rst_n,
input wire en,
output reg [7:0] count
);
// 传统风格(不推荐)
module counter (clk, rst_n, en, count);
input wire clk;
input wire rst_n;
input wire en;
output reg [7:0] count;
为什么推荐ANSI风格?因为端口列表里直接写了方向和类型,一眼就能看明白。传统风格你得往下翻好几行才能找到端口声明,代码一长就头疼。
4.3 数据类型——wire、reg、integer
这是新手最容易搞混的地方。我简单给你总结一下:
- wire:线网类型,用于组合逻辑。说白了就是一根导线,值由驱动源决定。
- reg:寄存器类型,用于时序逻辑。注意!它不一定是真正的寄存器,只是表示在always块中被赋值。
- integer:整数类型,常用于循环计数,综合时会被映射成寄存器或线网。
我的经验:很多教材说“wire对应组合逻辑,reg对应时序逻辑”,这话不完全对。你可以在always块里用wire吗?不行。但你可以用reg描述组合逻辑,比如always @(*) 里的reg变量。关键看你怎么用,别被名字骗了。
看个例子就明白了:
module data_type_demo (
input wire a, b,
input wire clk,
output reg q,
output wire sum
);
// wire用于组合逻辑
assign sum = a ^ b;
// reg用于时序逻辑
always @(posedge clk) begin
q <= a & b;
end
// integer用于循环
integer i;
always @(*) begin
for (i = 0; i < 8; i = i + 1) begin
// 做一些操作
end
end
endmodule
integer默认是32位有符号数。我建议你只在testbench或者循环里用integer,别在可综合代码里用它做算术运算,容易出问题。
4.4 参数化设计——让模块“活”起来
参数化设计是我最喜欢的特性之一。它让模块变得可配置,不用为每个不同位宽都写一个新模块。
用parameter关键字定义参数:
module param_counter #(
parameter WIDTH = 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
实例化时传参:
// 用默认参数
param_counter u_default (...);
// 自定义参数
param_counter #(
.WIDTH(16),
.MAX(65535)
) u_custom (...);
为什么参数化很重要?我在一个项目中需要同时处理8位、16位、32位的计数器。如果没有参数化,我得写三个几乎一模一样的模块。有了参数化,一个模块搞定,代码量减少60%,而且改bug只需要改一处。
参数化还能做条件编译:
module flexible_adder #(
parameter USE_PIPELINE = 0
) (
input wire [7:0] a, b,
input wire clk,
output reg [8:0] sum
);
generate
if (USE_PIPELINE) begin
// 流水线版本
always @(posedge clk) begin
sum <= a + b;
end
end else begin
// 组合逻辑版本
always @(*) begin
sum = a + b;
end
end
endgenerate
endmodule
你看,通过一个参数就能切换两种实现方式。这在做性能优化时特别有用。
4.5 本章知识体系
下面这张图帮你理清今天讲的内容:
今天这四个知识点,是Verilog的基石。模块定义搭框架,端口声明定接口,数据类型管存储,参数化设计提效率。你把这四个搞明白了,后面学always块、assign语句、状态机什么的,就会轻松很多。
一个小建议:刚开始写代码时,别急着追求花哨的语法。先把模块结构写规范,端口声明写清楚,数据类型用对。代码整洁了,bug自然就少了。我见过太多人因为reg和wire混用,debug花了一整天。
好了,今天就到这儿。下一章咱们接着聊always块和assign语句,那才是真正开始写逻辑的地方。
公众号:蓝海资料掘金营,微信deep3321