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

各位同学,欢迎来到Verilog实战课的第一章。今天咱们聊聊最基础的东西——模块怎么写、端口怎么定义、数据怎么存,还有参数化设计这个小技巧。

说实话,我刚开始学Verilog的时候,觉得这些语法太简单了,不就是声明几个变量嘛。结果后来在项目中吃过亏——端口方向搞反了,仿真半天查不出问题;wire和reg混用,综合出来一堆警告。嗯,基础不牢,地动山摇。咱们今天就把这些坑一个个填上。

3.1 模块声明——Verilog的“外壳”

Verilog里,每个设计单元都是一个模块(module)。你可以把它想象成一个黑盒子:外面有引脚,里面有逻辑。

模块声明的标准格式长这样:

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

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

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

这里要注意几点:

  • 模块名最好见名知意,比如 uart_tx、spi_master。别用 module1、test 这种名字,回头自己都看不懂。
  • 端口列表写在括号里,每个端口用逗号隔开。最后一个端口后面不要加逗号,有些编译器会报错。
  • endmodule别忘了写。我见过有人写了一大段代码,最后少了个endmodule,编译报错半天找不到原因。
我的习惯:端口列表里,我通常把输入写在前面,输出写在后面,inout放中间。这样结构清晰,别人看你的代码也舒服。

3.2 端口定义——input、output、inout

端口有三种方向:

关键字 方向 说明
input 输入 信号从外部进入模块
output 输出 信号从模块内部输出到外部
inout 双向 既可以输入也可以输出(比如I2C的SDA线)

端口定义有两种写法。我个人更推荐ANSI风格,简洁明了:

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

另一种是非ANSI风格,老代码里常见:

// 非ANSI风格(不推荐)
module counter (clk, rst_n, count);
    input        clk;
    input        rst_n;
    output [3:0] count;
    reg   [3:0] count;
注意:非ANSI风格里,output和reg要分开写,很容易漏掉。我曾经在维护一个老项目时,就因为少写了一个reg声明,仿真结果一直不对,查了两天才发现。所以,能用ANSI就用ANSI吧。

3.3 数据类型——wire和reg

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

  • wire:线网类型,用于组合逻辑。可以理解为物理连线,没有存储功能。
  • reg:寄存器类型,用于时序逻辑。可以存储数据,在时钟沿更新。

但注意,这里的“reg”并不一定综合成寄存器。如果你在always块里写组合逻辑,reg只是语法上的要求,综合出来还是组合电路。

看个例子:

// wire用法:assign语句
wire sum;
assign sum = a + b;

// reg用法:always块
reg [7:0] data;
always @(posedge clk) begin
    if (rst_n)
        data <= 8'd0;
    else
        data <= data_in;
end

什么时候用wire,什么时候用reg?记住一条原则:

在assign语句左边,用wire;在always块里被赋值的变量,用reg。

嗯,就这么简单。但有个例外——端口声明时,input默认是wire,output可以是wire或reg。比如:

module example (
    input  wire a,      // 输入可以是wire
    input       b,      // 不写默认也是wire
    output reg  y       // 输出可以声明为reg
);

3.4 参数化设计——让模块更灵活

参数化设计,说白了就是给模块加个“旋钮”。同一个模块,调一下参数就能适应不同场景。

用 parameter 关键字定义参数:

// 参数化计数器
module counter #(
    parameter WIDTH = 8   // 默认8位
) (
    input                clk,
    input                rst_n,
    output reg [WIDTH-1:0] count
);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else
            count <= count + 1;
    end
endmodule

实例化时,可以覆盖默认参数:

// 实例化一个16位计数器
counter #(
    .WIDTH(16)
) u_counter (
    .clk   (clk),
    .rst_n (rst_n),
    .count (count_16)
);

参数化设计的好处太多了:

  • 代码复用:写一次,到处用。改个参数就行。
  • 可读性强:别人一看就知道这个模块是干什么的。
  • 便于维护:要改位宽?改一个参数,不用到处改代码。
我的经验:写模块时,尽量把可能变化的量都参数化。比如数据位宽、FIFO深度、分频系数等。这样以后项目升级,改参数比改代码省事多了。

3.5 本章知识体系

下面这张图,帮你理清本章的核心逻辑:

Verilog模块基础 模块声明 • module ... endmodule • 端口列表写在括号内 • 模块名见名知意 端口定义 • input / output / inout • ANSI风格(推荐) • 非ANSI风格(不推荐) 数据类型 • wire:组合逻辑连线 • reg:时序逻辑存储 • 注意综合结果 参数化设计 • parameter定义参数 • 实例化时覆盖参数 • 提高代码复用性 模块实例化 • 模块名 #(参数) 实例名 • .端口名(连接信号) • 支持参数覆盖

这张图把本章的核心知识点串起来了。模块声明是外壳,端口定义是接口,数据类型是内部存储,参数化设计是灵活性。四者结合,就能写出高质量、可复用的Verilog代码。

3.6 避坑指南

最后,分享几个我踩过的坑:

  • 端口方向搞反:我曾经把一个输出口写成input,仿真时信号死活传不出去。查了半天,才发现是方向写反了。
  • reg和wire混用:在assign语句里给reg赋值,编译器报错。记住:assign左边只能是wire。
  • 参数忘记覆盖:实例化时忘了写参数,结果模块用了默认值,功能完全不对。所以实例化时一定要检查参数。
  • 模块名冲突:两个模块同名,综合时报错。建议加前缀或后缀区分,比如 uart_tx_v1、uart_tx_v2。
我曾经犯过的错:有一次写一个FIFO模块,参数化深度时忘了把地址位宽也参数化。结果深度改了,地址位宽没变,仿真时数据读写错位。从那以后,我每次写参数化模块,都会把所有相关参数列出来,逐一检查。

好了,这一章就到这里。基础语法虽然简单,但一定要动手写、动手仿真。下一章咱们聊运算符和always块,到时候见。

可可精品资料库