3. Verilog语法基础(上):模块声明、端口定义、数据类型、参数化设计
各位同学,欢迎来到Verilog实战课的第一章。今天咱们聊聊最基础的东西——模块怎么写、端口怎么定义、数据怎么存,还有参数化设计这个小技巧。
说实话,我刚开始学Verilog的时候,觉得这些语法太简单了,不就是声明几个变量嘛。结果后来在项目中吃过亏——端口方向搞反了,仿真半天查不出问题;wire和reg混用,综合出来一堆警告。嗯,基础不牢,地动山摇。咱们今天就把这些坑一个个填上。
3.1 模块声明——Verilog的“外壳”
Verilog里,每个设计单元都是一个模块(module)。你可以把它想象成一个黑盒子:外面有引脚,里面有逻辑。
模块声明的标准格式长这样:
module 模块名 (
端口列表
);
// 内部逻辑
endmodule
举个例子,一个最简单的与门:
module and_gate (
input a,
input b,
output y
);
assign y = a & b;
endmodule
这里要注意几点:
- 模块名最好见名知意,比如
uart_tx、spi_master。别用module1、test这种名字,回头自己都看不懂。 - 端口列表写在括号里,每个端口用逗号隔开。最后一个端口后面不要加逗号,有些编译器会报错。
- endmodule别忘了写。我见过有人写了一大段代码,最后少了个endmodule,编译报错半天找不到原因。
3.2 端口定义——input、output、inout
端口有三种方向:
| 关键字 | 方向 | 说明 |
|---|---|---|
| input | 输入 | 信号从外部进入模块 |
| output | 输出 | 信号从模块内部输出到外部 |
| inout | 双向 | 既可以输入也可以输出(比如I2C的SDA线) |
端口定义有两种写法。我个人更推荐ANSI风格,简洁明了:
// ANSI风格(推荐)
module counter (
input clk,
input rst_n,
output reg [3:0] count
);
另一种是非ANSI风格,老代码里常见:
// 非ANSI风格(不推荐)
module counter (clk, rst_n, count);
input clk;
input rst_n;
output [3:0] count;
reg [3:0] count;
3.3 数据类型——wire和reg
这是新手最容易搞混的地方。我简单总结一下:
- wire:线网类型,用于组合逻辑。可以理解为物理连线,没有存储功能。
- reg:寄存器类型,用于时序逻辑。可以存储数据,在时钟沿更新。
但注意,这里的“reg”并不一定综合成寄存器。如果你在always块里写组合逻辑,reg只是语法上的要求,综合出来还是组合电路。
看个例子:
// wire用法:assign语句
wire sum;
assign sum = a + b;
// reg用法:always块
reg [7:0] data;
always @(posedge clk) begin
if (rst_n)
data <= 8'd0;
else
data <= data_in;
end
什么时候用wire,什么时候用reg?记住一条原则:
在assign语句左边,用wire;在always块里被赋值的变量,用reg。
嗯,就这么简单。但有个例外——端口声明时,input默认是wire,output可以是wire或reg。比如:
module example (
input wire a, // 输入可以是wire
input b, // 不写默认也是wire
output reg y // 输出可以声明为reg
);
3.4 参数化设计——让模块更灵活
参数化设计,说白了就是给模块加个“旋钮”。同一个模块,调一下参数就能适应不同场景。
用 parameter 关键字定义参数:
// 参数化计数器
module counter #(
parameter WIDTH = 8 // 默认8位
) (
input clk,
input rst_n,
output reg [WIDTH-1:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else
count <= count + 1;
end
endmodule
实例化时,可以覆盖默认参数:
// 实例化一个16位计数器
counter #(
.WIDTH(16)
) u_counter (
.clk (clk),
.rst_n (rst_n),
.count (count_16)
);
参数化设计的好处太多了:
- 代码复用:写一次,到处用。改个参数就行。
- 可读性强:别人一看就知道这个模块是干什么的。
- 便于维护:要改位宽?改一个参数,不用到处改代码。
3.5 本章知识体系
下面这张图,帮你理清本章的核心逻辑:
这张图把本章的核心知识点串起来了。模块声明是外壳,端口定义是接口,数据类型是内部存储,参数化设计是灵活性。四者结合,就能写出高质量、可复用的Verilog代码。
3.6 避坑指南
最后,分享几个我踩过的坑:
- 端口方向搞反:我曾经把一个输出口写成input,仿真时信号死活传不出去。查了半天,才发现是方向写反了。
- reg和wire混用:在assign语句里给reg赋值,编译器报错。记住:assign左边只能是wire。
- 参数忘记覆盖:实例化时忘了写参数,结果模块用了默认值,功能完全不对。所以实例化时一定要检查参数。
- 模块名冲突:两个模块同名,综合时报错。建议加前缀或后缀区分,比如
uart_tx_v1、uart_tx_v2。
好了,这一章就到这里。基础语法虽然简单,但一定要动手写、动手仿真。下一章咱们聊运算符和always块,到时候见。