4. Verilog基础语法(上):模块定义、端口声明、数据类型、参数化设计

好,咱们正式开始写Verilog了。说实话,很多初学者一上来就被各种语法细节吓住,其实没必要。你想想看,Verilog说白了就是描述硬件的一种语言,跟C语言那种软件思维完全不同。今天这一章,我带你先把最核心的四个概念捋清楚:模块怎么搭、端口怎么接、数据怎么存、参数怎么配。

4.1 模块定义——你的第一个硬件“黑盒子”

模块(module)是Verilog的基本单元。你可以把它想象成一个黑盒子:有输入、有输出、内部有逻辑。我刚开始学的时候,总觉得模块定义就是写个“外壳”,后来才发现,这个外壳的写法直接决定了代码的可读性和可维护性。

模块的基本结构:

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

举个例子,一个最简单的与门模块:

module and_gate (
    input  wire a,
    input  wire b,
    output wire y
);
    assign y = a & b;
endmodule

嗯,这里要注意:模块名最好用有意义的英文单词,别用a1、b2这种。我在项目中见过一个同事,模块名叫“test1”、“test2”,三个月后他自己都看不懂了。

4.2 端口声明——输入、输出、双向

端口就是模块跟外界通信的通道。Verilog里端口有三种方向:

方向 关键字 说明
输入 input 数据从外部流入模块
输出 output 数据从模块流出到外部
双向 inout 数据可以双向流动(慎用)

避坑指南:我曾经在项目里滥用inout端口,结果综合时出了一堆三态冲突。后来我给自己定了个规矩:除非是总线接口(比如I2C的SDA),否则尽量别用inout。内部模块之间用input/output就足够了。

端口声明的两种风格,我个人习惯用ANSI风格:

// ANSI风格(推荐)
module counter (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       en,
    output reg  [7:0] count
);

// 传统风格(不推荐)
module counter (clk, rst_n, en, count);
    input  wire       clk;
    input  wire       rst_n;
    input  wire       en;
    output reg  [7:0] count;

为什么推荐ANSI风格?因为端口列表里直接写了方向和类型,一眼就能看明白。传统风格你得往下翻好几行才能找到端口声明,代码一长就头疼。

4.3 数据类型——wire、reg、integer

这是新手最容易搞混的地方。我简单给你总结一下:

  • wire:线网类型,用于组合逻辑。说白了就是一根导线,值由驱动源决定。
  • reg:寄存器类型,用于时序逻辑。注意!它不一定是真正的寄存器,只是表示在always块中被赋值。
  • integer:整数类型,常用于循环计数,综合时会被映射成寄存器或线网。

我的经验:很多教材说“wire对应组合逻辑,reg对应时序逻辑”,这话不完全对。你可以在always块里用wire吗?不行。但你可以用reg描述组合逻辑,比如always @(*) 里的reg变量。关键看你怎么用,别被名字骗了。

看个例子就明白了:

module data_type_demo (
    input  wire       a, b,
    input  wire       clk,
    output reg        q,
    output wire       sum
);
    // wire用于组合逻辑
    assign sum = a ^ b;

    // reg用于时序逻辑
    always @(posedge clk) begin
        q <= a & b;
    end

    // integer用于循环
    integer i;
    always @(*) begin
        for (i = 0; i < 8; i = i + 1) begin
            // 做一些操作
        end
    end
endmodule

integer默认是32位有符号数。我建议你只在testbench或者循环里用integer,别在可综合代码里用它做算术运算,容易出问题。

4.4 参数化设计——让模块“活”起来

参数化设计是我最喜欢的特性之一。它让模块变得可配置,不用为每个不同位宽都写一个新模块。

用parameter关键字定义参数:

module param_counter #(
    parameter WIDTH = 8,
    parameter MAX   = 255
) (
    input  wire             clk,
    input  wire             rst_n,
    output reg  [WIDTH-1:0] count
);
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else if (count == MAX)
            count <= 0;
        else
            count <= count + 1;
    end
endmodule

实例化时传参:

// 用默认参数
param_counter u_default (...);

// 自定义参数
param_counter #(
    .WIDTH(16),
    .MAX(65535)
) u_custom (...);

为什么参数化很重要?我在一个项目中需要同时处理8位、16位、32位的计数器。如果没有参数化,我得写三个几乎一模一样的模块。有了参数化,一个模块搞定,代码量减少60%,而且改bug只需要改一处。

参数化还能做条件编译:

module flexible_adder #(
    parameter USE_PIPELINE = 0
) (
    input  wire [7:0] a, b,
    input  wire       clk,
    output reg  [8:0] sum
);
    generate
        if (USE_PIPELINE) begin
            // 流水线版本
            always @(posedge clk) begin
                sum <= a + b;
            end
        end else begin
            // 组合逻辑版本
            always @(*) begin
                sum = a + b;
            end
        end
    endgenerate
endmodule

你看,通过一个参数就能切换两种实现方式。这在做性能优化时特别有用。

4.5 本章知识体系

下面这张图帮你理清今天讲的内容:

Verilog基础语法(上) 模块定义 module ... endmodule 硬件黑盒子 端口声明 input / output / inout ANSI风格 vs 传统风格 数据类型 wire / reg / integer 组合 vs 时序 参数化设计 parameter / #() 传参 条件编译 generate 核心:结构清晰 + 可配置 + 可复用

今天这四个知识点,是Verilog的基石。模块定义搭框架,端口声明定接口,数据类型管存储,参数化设计提效率。你把这四个搞明白了,后面学always块、assign语句、状态机什么的,就会轻松很多。

一个小建议:刚开始写代码时,别急着追求花哨的语法。先把模块结构写规范,端口声明写清楚,数据类型用对。代码整洁了,bug自然就少了。我见过太多人因为reg和wire混用,debug花了一整天。

好了,今天就到这儿。下一章咱们接着聊always块和assign语句,那才是真正开始写逻辑的地方。


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