3. Verilog模块结构:模块定义、端口声明、模块实例化、`timescale与注释

各位同学,今天我们来聊聊Verilog设计中最基础、也最核心的东西——模块结构。说白了,一个Verilog模块就像一块芯片的“设计图纸”。你画好这张图,工具才能帮你把它变成真正的硬件。

我刚开始学Verilog那会儿,总觉得模块定义就是个形式,随便写写就行。后来在项目中吃过亏,才发现这里面的门道真不少。咱们一个一个来看。

3.1 模块定义——你的设计“外壳”

每个Verilog模块,都以 module 开头,以 endmodule 结尾。这是硬性规定,少一个都不行。

module 模块名 (端口列表);
    // 内部逻辑
endmodule

模块名我建议用有意义的英文单词组合,比如 counter_8bituart_tx。别用 module1test 这种名字,三个月后你自己都看不懂。

我的习惯:模块名首字母小写,后续单词首字母大写。比如 pwmGenerator。当然这不是强制规定,但团队里统一风格很重要。

3.2 端口声明——模块的“手脚”

端口就是模块跟外界打交道的通道。Verilog支持三种端口方向:

端口类型 关键字 说明
输入 input 数据流入模块,不能赋值
输出 output 数据流出模块,可以赋值
双向 inout 数据可进可出,常用于总线

端口声明有两种写法。我个人偏爱ANSI风格,简洁明了:

module counter (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [7:0] count
);
    // 逻辑代码
endmodule

另一种是传统风格,端口列表只写名字,方向在内部声明。嗯,这种写法现在用得少了,但老代码里还能见到。

注意:我曾经在项目中遇到过一个bug,就是因为把 input 写成了 inout。仿真没问题,但综合后多了一堆三态缓冲器,资源翻了一倍。所以端口方向一定要写对。

3.3 模块实例化——把图纸变成实物

模块定义好了,怎么用?实例化。就像你画好了一个芯片的版图,然后把它“贴”到更大的电路板上。

实例化有两种方式:

方式一:按端口顺序连接

counter u_counter (
    clk,      // 连接到顶层clk
    rst_n,    // 连接到顶层rst_n
    en,       // 连接到顶层en
    count     // 连接到顶层count
);

这种方式代码短,但容易出错。端口顺序一变,所有实例化都得改。我不推荐。

方式二:按端口名称连接(推荐)

counter u_counter (
    .clk   (clk),
    .rst_n (rst_n),
    .en    (en),
    .count (count)
);

这种方式清晰、可读性强,而且端口顺序随便调。你想想看,如果项目里有几十个模块实例化,用这种方式维护起来多省心。

核心要点:实例化时,.后面是模块定义的端口名,括号里是当前模块的信号名。别搞反了。

3.4 `timescale——仿真时间单位

`timescale 是编译器指令,用来定义仿真中的时间单位和精度。它长这样:

`timescale 1ns / 1ps

斜杠前面是时间单位,后面是时间精度。上面这行表示:时间单位是1ns,精度是1ps。

为什么要有这个?因为仿真器需要知道 #10 到底代表多长时间。没有 `timescale,仿真器就懵了。

我的建议:每个设计文件的第一行都写上 `timescale。我曾经接手过一个项目,好几个文件没写,仿真结果对不上,查了两天才发现是时间单位不一致。这种坑,踩一次就够了。

常用的时间单位组合:

  • `timescale 1ns / 1ps —— 最常用,精度够用
  • `timescale 10ns / 1ns —— 低频设计可以用
  • `timescale 1ps / 1fs —— 高速设计,但仿真会慢

3.5 注释——写给未来的自己

注释分两种:

  • // 单行注释:用于简短说明
  • /* ... */ 多行注释:用于大段描述

我见过太多没有注释的代码了。说实话,三个月后你自己都看不懂当时写的逻辑。所以我的习惯是:

  • 每个模块开头写一段功能描述
  • 每个always块前面写清楚触发条件
  • 关键信号旁边加注释
// 8位同步计数器,带使能和异步复位
// 计数范围:0 ~ 255
module counter (
    input  wire       clk,    // 系统时钟
    input  wire       rst_n,  // 异步复位,低有效
    input  wire       en,     // 计数使能
    output reg  [7:0] count   // 计数值
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 8'd0;
        else if (en)
            count <= count + 1'b1;
    end
endmodule
避坑指南:我曾经见过有人把注释写成“这里加1”、“这里清零”这种废话。注释要写“为什么”,而不是“是什么”。比如“这里加1是因为需要补偿流水线延迟”,这才是有价值的注释。

3.6 知识体系总览

下面这张图,把本章的核心内容串起来了。你可以把它当作一个快速参考。

Verilog模块结构知识体系 模块定义 module ... endmodule 端口声明 input / output / inout 模块实例化 按名称 / 按顺序 模块名规范 有意义的英文组合 首字母小写驼峰式 避免 module1 等 端口方向 input: 数据流入 output: 数据流出 inout: 双向总线 实例化方式 按名称连接(推荐) 按顺序连接(不推荐) .端口名(信号名) `timescale 时间单位 / 时间精度 注释 // 和 /* */ 核心:模块是Verilog设计的基本单元,端口是模块的接口

好了,以上就是Verilog模块结构的全部核心内容。模块定义、端口声明、实例化、`timescale、注释——这五个点,是每个Verilog设计者必须烂熟于心的基本功。别觉得简单就跳过,我见过太多老手在这些基础问题上翻车了。

一句话总结:模块是Verilog的“细胞”,端口是细胞的“膜”,实例化是细胞的“复制”,`timescale是仿真的“时钟”,注释是留给未来的“说明书”。

公众号:蓝海资料掘金营,微信deep3321