2. Verilog模块结构:模块声明、端口定义、参数化设计(parameter/localparam)、模块实例化基础

各位同学,今天我们来聊聊Verilog模块的骨架——模块结构。这东西就像盖房子的图纸,图纸画不好,后面全是坑。我做了十几年FPGA,见过太多因为模块结构混乱导致的返工案例,今天就把我的经验掰开揉碎讲给你们听。

2.1 模块声明——你的设计身份证

每个Verilog模块,说白了就是一个黑盒子。模块声明就是这个盒子的标签,告诉别人你叫什么、长什么样、能干什么。

module counter #(
    parameter WIDTH = 8
)(
    input  wire        clk,
    input  wire        rst_n,
    input  wire        en,
    output reg  [WIDTH-1:0] count
);
// 内部逻辑
endmodule

嗯,这里要注意:moduleendmodule必须成对出现,就像括号一样。我刚开始学的时候,经常写完逻辑忘了写endmodule,编译报错半天找不到原因。

模块命名规范(我个人习惯):

  • 小写字母+下划线,比如 uart_rxspi_master
  • 不要用数字开头,比如 2bit_adder 这种写法会报错
  • 文件名和模块名保持一致,方便查找

2.2 端口定义——模块的嘴巴和耳朵

端口就是模块和外界通信的通道。我把它分成三类:输入、输出、双向。你想想看,一个模块如果没有端口,那它就是个自闭症患者,谁也跟它说不上话。

端口类型 关键字 默认方向 典型用途
输入 input wire 时钟、复位、数据输入
输出 output wire(可声明为reg) 数据输出、状态指示
双向 inout wire I2C数据线、DDR数据总线

避坑指南:我曾经在项目中把inout端口当output用,结果仿真没问题,上板后数据死活读不回来。后来发现是inout必须用三态门控制,不能直接赋值。记住:inout端口只能连wire类型,不能连reg。

2.3 参数化设计——让模块活起来

参数化设计是我最喜欢的功能。它让模块从「死板」变成「灵活」。说白了,就是给模块装了个旋钮,拧一下就能改变行为。

2.3.1 parameter vs localparam

这两个东西长得像,但性格完全不同:

  • parameter:对外开放的参数,实例化时可以修改。就像商品的公开售价,可以打折。
  • localparam:内部私有参数,外部不能改。就像商品的成本价,不对外公开。
module fifo #(
    parameter DATA_WIDTH = 8,
    parameter DEPTH      = 16
)(
    input  wire                    clk,
    input  wire                    wr_en,
    input  wire [DATA_WIDTH-1:0]  wr_data,
    // ... 其他端口
);

    // 内部计算,外部不可修改
    localparam ADDR_WIDTH = $clog2(DEPTH);
    localparam PTR_WIDTH  = ADDR_WIDTH + 1;

    reg [ADDR_WIDTH-1:0] wr_ptr;
    reg [ADDR_WIDTH-1:0] rd_ptr;
    // ... 逻辑实现
endmodule

我的经验:参数化设计一定要给默认值。我见过有人写 parameter WIDTH; 不给默认值,结果别人实例化时忘了传参,编译直接报错。给个默认值,比如 parameter WIDTH = 8,既安全又友好。

2.3.2 参数传递的两种方式

实例化时改参数,有两种写法:

// 方式一:按顺序传参(不推荐)
fifo #(16, 32) u_fifo (
    .clk(clk),
    // ...
);

// 方式二:按名称传参(推荐)
fifo #(
    .DATA_WIDTH(16),
    .DEPTH(32)
) u_fifo (
    .clk(clk),
    // ...
);

为什么推荐按名称传参?因为顺序传参太容易搞错。我记得有一次,我把DATA_WIDTH和DEPTH的顺序写反了,仿真跑了三天才发现数据截断了。从那以后,我再也不用顺序传参了。

2.4 模块实例化——把积木搭起来

模块写好了,怎么用?实例化就是把这个模块「复制」一份放到你的设计里。就像乐高积木,每个模块是一个零件,实例化就是把零件拼起来。

2.4.1 实例化语法

// 模块名 #(参数) 实例名 (端口连接);
counter #(
    .WIDTH(16)
) u_counter (
    .clk    (sys_clk),
    .rst_n  (sys_rst_n),
    .en     (counter_en),
    .count  (counter_val)
);

这里有几个关键点:

  • 实例名:每个实例必须有唯一的名字,就像每个人有身份证号
  • 端口连接:用 .端口名(连线名) 的方式,不要用顺序连接
  • 悬空端口:不用的输入要接固定电平,不用的输出可以悬空

警告:输入端口绝对不能悬空!我曾经在项目中把一个模块的使能信号忘了接,结果那个模块一直处于使能状态,功耗飙升。不用的输入,要么接0,要么接1,千万别留空。

2.4.2 数组实例化——批量生产

如果你需要多个相同的模块,可以用数组实例化:

// 生成8个相同的计数器
counter #(
    .WIDTH(8)
) u_counter [7:0] (
    .clk    (sys_clk),
    .rst_n  (sys_rst_n),
    .en     (en_array),
    .count  (count_array)
);

嗯,这里要注意:数组实例化时,端口连接的信号宽度必须匹配。比如 en_array 必须是8位宽,每一位对应一个实例的使能信号。

2.5 知识体系结构图

下面这张图,是我自己总结的模块结构知识体系,你们可以保存下来当参考:

Verilog模块结构知识体系 模块声明 端口定义 参数化设计 module/endmodule 成对出现 命名规范:小写+下划线 input / output / inout wire vs reg 类型选择 inout必须用三态门 parameter(外部可改) localparam(内部私有) 按名称传参(推荐) 模块实例化 实例名唯一 端口名(连线名)连接 输入不能悬空

2.6 避坑总结

最后,我把这些年踩过的坑总结一下,你们记好了:

  1. 端口方向别搞反:input和output写反了,仿真可能看不出来,综合会报错
  2. 参数默认值必须给:不给默认值的parameter,别人用起来很痛苦
  3. 实例名不能重复:同一个模块里,两个实例不能叫同一个名字
  4. inout端口要小心:必须用三态门,不能直接赋值
  5. 数组实例化注意位宽:端口信号宽度要和实例数量匹配

好了,模块结构这部分就讲到这里。记住一句话:好的模块结构,是成功设计的开始。你们写代码的时候,多想想这个模块以后会不会被复用,参数化设计能不能再优化一下。嗯,这些习惯养成了,后面做大型项目会轻松很多。