模块与端口:Verilog设计的基石

各位同学,今天我们来聊聊Verilog里最基础、也最重要的东西——模块与端口。说白了,模块就是你的设计单元,端口就是它和外界沟通的桥梁。

我记得刚学Verilog那会儿,总觉得模块定义就是个形式,随便写写就行。后来做项目吃了亏,才明白这里面的门道。嗯,咱们今天就把这块彻底讲透。

一、module/endmodule结构

每个Verilog设计,都是从module开始,到endmodule结束。这就像写作文要有开头和结尾一样,是语法规定死的。

module 模块名 (端口列表);
    // 端口声明
    // 内部信号
    // 功能描述
endmodule

我个人习惯把模块名写得有意义一些,比如counter_8bituart_tx,别用module1top这种名字。你想想看,项目大了以后,几百个模块,名字起得不好,找起来真要命。

核心要点:模块名就是你的设计身份证,要见名知意。

二、输入输出端口定义

端口定义有三种方向:inputoutputinout。我工作中用得最多的是前两种,inout一般只在总线接口里用到。

module led_controller (
    input  wire       clk,      // 系统时钟
    input  wire       rst_n,    // 复位信号,低有效
    input  wire [3:0] sw,       // 拨码开关输入
    output reg  [7:0] led       // LED输出
);

这里有个细节要注意:端口定义时,wirereg类型可以写在端口列表里,也可以单独声明。我个人喜欢写在一起,代码更紧凑,看着舒服。

小技巧:端口顺序建议按功能分组,时钟复位放前面,数据信号放后面。这样别人看你的代码,一眼就能找到关键信号。

三、端口类型:wire vs reg

这是新手最容易搞混的地方。我简单说下:

  • wire:连线类型,用于组合逻辑输出、模块间连接
  • reg:寄存器类型,用于时序逻辑、always块内赋值

但注意!reg不一定会综合成寄存器。在组合逻辑的always @(*)里,reg只是语法要求,实际综合出来还是组合电路。

我曾经在项目中遇到过一个坑:同事把组合逻辑的输出定义成reg,然后在assign语句里赋值,结果编译报错。嗯,assign只能赋值给wire,这是硬性规定。

端口类型 适用场景 赋值方式
wire 组合逻辑、模块连接 assign、端口连接
reg 时序逻辑、always块 always @(posedge clk)

避坑指南:我曾经在Lattice的工程里,因为把时钟信号定义成reg,导致综合工具报了一堆警告。时钟信号必须用wire,除非你在always块里处理它。

四、模块实例化基础

模块写好了,怎么用?实例化!说白了就是把模块当成一个零件,焊接到你的电路板上。

// 方式一:按端口顺序连接(不推荐)
led_controller u_led (
    clk,        // 连接到顶层clk
    rst_n,      // 连接到顶层rst_n
    sw,         // 连接到顶层sw
    led         // 连接到顶层led
);

// 方式二:按端口名称连接(推荐)
led_controller u_led (
    .clk    (sys_clk),    // 显式连接
    .rst_n  (sys_rst_n),
    .sw     (switch_in),
    .led    (led_out)
);

我个人强烈建议用第二种方式。为什么?你想想看,如果模块端口顺序变了,第一种方式就全乱了。第二种方式,端口名和信号名一一对应,改起来也方便。

经验之谈:我在Lattice的Diamond工具里,习惯把实例化模块的命名加上u_前缀,比如u_counteru_uart。这样在波形图里一眼就能看出是哪个模块的实例。

五、知识体系结构图

下面这张图,是我画的本章节知识体系。你可以看到,模块与端口是设计的入口,往下分支出定义、类型、实例化三个方向。

模块与端口 module/endmodule 端口定义与类型 模块实例化 模块名规范 端口列表 input/output wire/reg 按名称连接 按顺序连接 核心:模块是设计单元,端口是通信接口 实例化时推荐按名称连接,避免顺序错误

六、实战中的注意事项

最后,我分享几个实际项目中的经验:

  1. 端口数量控制:一个模块的端口别超过20个,太多了就拆分成子模块。我曾经接手过一个300多端口的模块,那代码看得我头皮发麻。
  2. 命名一致性:顶层模块的端口名,尽量和子模块的端口名保持一致。比如顶层叫clk,子模块也叫clk,这样实例化时一目了然。
  3. 注释要到位:每个端口后面加个注释,说明功能、有效电平、位宽等信息。你想想看,半年后你自己回来看代码,没有注释,还得重新分析一遍。

我的习惯:在Lattice工程里,我会在模块头部加一个版本注释,记录修改日期和内容。这样团队协作时,每个人都知道这个模块是谁改的、改了啥。

好了,模块与端口这部分就讲到这里。记住,基础打牢了,后面学时序、状态机、接口协议才会顺手。别急着写复杂逻辑,先把module结构写规范了。


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