3. Verilog基础语法(上):模块结构、端口定义、数据类型(wire/reg)、参数化设计
各位同学,咱们今天聊聊Verilog最基础的东西。说实话,很多老工程师写了十年代码,回过头来看这些基础,反而能发现不少门道。我当年刚入行时,总觉得语法嘛,记住就行。后来踩了坑才明白——基础不牢,地动山摇。
3.1 模块结构——芯片设计的“积木块”
Verilog里,模块(module)就是最基本的单元。你可以把它想象成一块乐高积木。每个模块完成一个功能,多个模块拼起来,就是一个完整的芯片。
一个标准的模块长这样:
module counter #(
parameter WIDTH = 8
)(
input wire clk,
input wire rst_n,
input wire en,
output reg [WIDTH-1:0] count
);
// 功能描述
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 0;
else if (en)
count <= count + 1;
end
endmodule
结构上分三块:
- 模块名与参数声明——告诉别人你叫啥,有哪些可调参数
- 端口列表——输入输出,跟外界打交道的接口
- 内部逻辑——真正干活的地方
我的习惯:模块名用下划线分隔,比如
uart_tx、fifo_ctrl。别用驼峰,也别全大写。团队统一风格,比什么都重要。
3.2 端口定义——模块的“嘴巴和耳朵”
端口就是模块跟外界通信的通道。三种方向:
| 方向 | 关键字 | 说明 |
|---|---|---|
| 输入 | input |
数据流进模块,内部只能读不能写 |
| 输出 | output |
数据流出模块,内部可以赋值 |
| 双向 | inout |
既能读又能写,常用于总线接口 |
写端口时,我建议把数据类型直接写在端口声明里。别分开写,容易漏:
// 推荐写法——清晰明了
module example (
input wire a, // 输入线网
output reg b, // 输出寄存器
inout wire c // 双向线网
);
// 不推荐——分开写容易出错
module example (
input a,
output b,
inout c
);
wire a;
reg b;
// 万一漏了 wire/reg 声明,仿真器会报一堆莫名其妙的错
我曾经踩过的坑:有一次写一个I2C控制器,把
inout 端口的数据类型写成了 reg。综合时报了一堆三态冲突错误,查了整整半天。记住:inout 端口必须是 wire 类型,不能是 reg。
3.3 数据类型——wire 和 reg 到底怎么选?
这是新手最容易懵的地方。我直接说结论:
- wire——线网类型,表示物理连线。用
assign赋值,或者连接模块端口。 - reg——寄存器类型,表示存储单元。在
always块里赋值。
但注意!reg 不一定会被综合成寄存器。你想想看,如果在 always @(*) 组合逻辑块里用 reg,它综合出来就是一堆组合逻辑,没有存储功能。
// 这个 reg 综合出来是组合逻辑
reg [3:0] sum;
always @(*) begin
sum = a + b;
end
// 这个 reg 综合出来是触发器
reg [3:0] count;
always @(posedge clk) begin
count <= count + 1;
end
记住这个原则:
- 在
assign语句左边 → 用wire - 在
always块里赋值 → 用reg - 模块的输入端口 → 用
wire - 模块的输出端口 → 看情况,组合逻辑输出用
wire,时序逻辑输出用reg
3.4 参数化设计——让代码“活”起来
说白了,参数化就是给模块留个“旋钮”。同一个模块,拧一下参数,就能适应不同场景。我做过一个项目,里面用了同一个 FIFO 模块,只是深度和位宽不同,靠参数就搞定了。
Verilog 里用 parameter 定义参数:
module shift_reg #(
parameter WIDTH = 8, // 数据位宽
parameter DEPTH = 4 // 移位深度
)(
input wire clk,
input wire rst_n,
input wire [WIDTH-1:0] din,
output wire [WIDTH-1:0] dout
);
reg [WIDTH-1:0] shift_reg [DEPTH-1:0];
integer i;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (i = 0; i < DEPTH; i = i + 1)
shift_reg[i] <= 0;
end else begin
shift_reg[0] <= din;
for (i = 1; i < DEPTH; i = i + 1)
shift_reg[i] <= shift_reg[i-1];
end
end
assign dout = shift_reg[DEPTH-1];
endmodule
实例化时,可以覆盖默认参数:
// 用默认参数(8位宽,4深度)
shift_reg u_default (...);
// 覆盖参数(16位宽,8深度)
shift_reg #(
.WIDTH(16),
.DEPTH(8)
) u_custom (...);
我的建议:参数名用大写,比如
WIDTH、DEPTH、ADDR_WIDTH。这样一眼就能看出哪些是可调参数,哪些是内部信号。团队里统一这个约定,代码可读性会好很多。
3.5 知识体系总览
下面这张图,把咱们今天讲的内容串起来了。你仔细看看,模块结构是骨架,端口是接口,数据类型是血肉,参数化设计是灵魂。四者缺一不可。
嗯,今天的内容就到这儿。模块结构、端口定义、数据类型、参数化设计——这四个东西,你写每一行代码都会用到。别觉得简单,真正吃透了,后面学时序、学状态机才会顺手。
可可精品资料库