3. Verilog基础语法(上):模块结构、端口定义、数据类型(wire/reg)、参数化设计

各位同学,咱们今天聊聊Verilog最基础的东西。说实话,很多老工程师写了十年代码,回过头来看这些基础,反而能发现不少门道。我当年刚入行时,总觉得语法嘛,记住就行。后来踩了坑才明白——基础不牢,地动山摇。

3.1 模块结构——芯片设计的“积木块”

Verilog里,模块(module)就是最基本的单元。你可以把它想象成一块乐高积木。每个模块完成一个功能,多个模块拼起来,就是一个完整的芯片。

一个标准的模块长这样:

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

    // 功能描述
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            count <= 0;
        else if (en)
            count <= count + 1;
    end

endmodule

结构上分三块:

  • 模块名与参数声明——告诉别人你叫啥,有哪些可调参数
  • 端口列表——输入输出,跟外界打交道的接口
  • 内部逻辑——真正干活的地方
我的习惯:模块名用下划线分隔,比如 uart_tx、fifo_ctrl。别用驼峰,也别全大写。团队统一风格,比什么都重要。

3.2 端口定义——模块的“嘴巴和耳朵”

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

方向 关键字 说明
输入 input 数据流进模块,内部只能读不能写
输出 output 数据流出模块,内部可以赋值
双向 inout 既能读又能写,常用于总线接口

写端口时,我建议把数据类型直接写在端口声明里。别分开写,容易漏:

// 推荐写法——清晰明了
module example (
    input  wire       a,   // 输入线网
    output reg        b,   // 输出寄存器
    inout  wire       c    // 双向线网
);

// 不推荐——分开写容易出错
module example (
    input       a,
    output      b,
    inout       c
);
wire a;
reg b;
// 万一漏了 wire/reg 声明,仿真器会报一堆莫名其妙的错
我曾经踩过的坑:有一次写一个I2C控制器,把 inout 端口的数据类型写成了 reg。综合时报了一堆三态冲突错误,查了整整半天。记住:inout 端口必须是 wire 类型,不能是 reg。

3.3 数据类型——wire 和 reg 到底怎么选?

这是新手最容易懵的地方。我直接说结论:

  • wire——线网类型,表示物理连线。用 assign 赋值,或者连接模块端口。
  • reg——寄存器类型,表示存储单元。在 always 块里赋值。

但注意!reg 不一定会被综合成寄存器。你想想看,如果在 always @(*) 组合逻辑块里用 reg,它综合出来就是一堆组合逻辑,没有存储功能。

// 这个 reg 综合出来是组合逻辑
reg [3:0] sum;
always @(*) begin
    sum = a + b;
end

// 这个 reg 综合出来是触发器
reg [3:0] count;
always @(posedge clk) begin
    count <= count + 1;
end
记住这个原则:
  • 在 assign 语句左边 → 用 wire
  • 在 always 块里赋值 → 用 reg
  • 模块的输入端口 → 用 wire
  • 模块的输出端口 → 看情况,组合逻辑输出用 wire,时序逻辑输出用 reg

3.4 参数化设计——让代码“活”起来

说白了,参数化就是给模块留个“旋钮”。同一个模块,拧一下参数,就能适应不同场景。我做过一个项目,里面用了同一个 FIFO 模块,只是深度和位宽不同,靠参数就搞定了。

Verilog 里用 parameter 定义参数:

module shift_reg #(
    parameter WIDTH = 8,      // 数据位宽
    parameter DEPTH = 4       // 移位深度
)(
    input  wire             clk,
    input  wire             rst_n,
    input  wire [WIDTH-1:0] din,
    output wire [WIDTH-1:0] dout
);

    reg [WIDTH-1:0] shift_reg [DEPTH-1:0];
    integer i;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            for (i = 0; i < DEPTH; i = i + 1)
                shift_reg[i] <= 0;
        end else begin
            shift_reg[0] <= din;
            for (i = 1; i < DEPTH; i = i + 1)
                shift_reg[i] <= shift_reg[i-1];
        end
    end

    assign dout = shift_reg[DEPTH-1];

endmodule

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

// 用默认参数(8位宽,4深度)
shift_reg u_default (...);

// 覆盖参数(16位宽,8深度)
shift_reg #(
    .WIDTH(16),
    .DEPTH(8)
) u_custom (...);
我的建议:参数名用大写,比如 WIDTH、DEPTH、ADDR_WIDTH。这样一眼就能看出哪些是可调参数,哪些是内部信号。团队里统一这个约定,代码可读性会好很多。

3.5 知识体系总览

下面这张图,把咱们今天讲的内容串起来了。你仔细看看,模块结构是骨架,端口是接口,数据类型是血肉,参数化设计是灵魂。四者缺一不可。

Verilog 模块 module / endmodule 模块结构 module 声明 内部逻辑 / 实例化 端口定义 input / output / inout 方向 + 数据类型 数据类型 wire / reg assign / always 参数化设计 parameter / #() 可配置、可复用 骨架 + 接口 + 血肉 + 灵魂 = 高质量 RTL 设计

嗯,今天的内容就到这儿。模块结构、端口定义、数据类型、参数化设计——这四个东西,你写每一行代码都会用到。别觉得简单,真正吃透了,后面学时序、学状态机才会顺手。

可可精品资料库