3、Verilog基础(上):模块与端口、数据类型(wire/reg)、组合逻辑(assign/always)

各位同学,咱们今天聊聊Verilog最核心的几个概念。说实话,我见过太多新手一上来就啃语法书,结果越看越迷糊。其实Verilog没那么玄乎,你把它理解成「用代码画电路图」就行了。

我个人习惯把Verilog的学习分成三步:先搞清楚模块长什么样,再弄明白数据怎么存,最后学会描述逻辑行为。今天咱们就走完这三步。

3.1 模块与端口——芯片的「黑盒子」

模块是什么?说白了就是一个带接口的黑盒子。你给它输入信号,它给你输出结果。就像你家里的路由器,插上网线(输入),它就能吐出Wi-Fi信号(输出)。

来看一个最简单的模块定义:

module and_gate (
    input  wire a,      // 输入端口a
    input  wire b,      // 输入端口b
    output wire y       // 输出端口y
);

    assign y = a & b;   // 组合逻辑:y等于a与b

endmodule

这里有几个关键点:

  • module/endmodule:模块的起止标志,所有代码都包在里面
  • 端口方向:input(输入)、output(输出)、inout(双向)
  • 端口类型:wire(线网)或reg(寄存器),后面细说
我的小习惯:端口声明时,我习惯把input和output分开写,每个端口占一行。这样代码清晰,后期维护也方便。曾经有个项目,同事把所有端口挤在一行,结果改一个信号名找了半天...

模块可以嵌套。你可以在一个模块里调用另一个模块,这叫「模块实例化」。就像搭积木,小模块拼成大模块,大模块再拼成系统。

module top (
    input  wire [3:0] a,
    input  wire [3:0] b,
    output wire [3:0] sum
);

    // 实例化一个加法器模块
    adder u_adder (
        .a(a),
        .b(b),
        .sum(sum)
    );

endmodule

注意看实例化的写法:模块名 实例名 (端口连接)。这里的u_adder是我随便起的名字,你可以按自己的命名规范来。我个人喜欢用u_前缀,一眼就能看出是实例化的子模块。

3.2 数据类型——wire和reg的「爱恨情仇」

这是新手最容易搞混的地方。我当年也踩过坑,所以咱们好好捋一捋。

特性 wire reg
本质 物理连线 存储单元(不一定对应寄存器)
赋值方式 只能被assign连续赋值 只能在always块中被赋值
默认值 高阻态z 未知态x
综合结果 组合逻辑连线 可能是寄存器或组合逻辑

核心原则就一句话:用assign赋值的信号声明为wire,在always块里赋值的信号声明为reg。

注意:reg不一定会综合成寄存器!如果你在always块里写的是组合逻辑(比如用always @(*)),reg综合出来还是组合逻辑。我曾经见过有人以为reg就是寄存器,结果综合出来的面积比预期大了一倍...

举个例子你就明白了:

module example (
    input  wire clk,
    input  wire rst_n,
    input  wire [7:0] d,
    output reg  [7:0] q       // 这里用reg,因为要在always里赋值
);

    // 时序逻辑:reg综合成寄存器
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            q <= 8'd0;
        else
            q <= d;
    end

endmodule

再看一个组合逻辑的例子:

module comb_example (
    input  wire [3:0] a,
    input  wire [3:0] b,
    output reg  [3:0] y       // 虽然是reg,但综合出来是组合逻辑
);

    // 组合逻辑:reg综合成连线
    always @(*) begin
        y = a + b;
    end

endmodule

看到了吗?两个例子都用了reg,但综合结果完全不同。关键看always块的敏感列表和赋值方式。

3.3 组合逻辑——assign和always的「分工」

组合逻辑就是输出只取决于当前输入,跟历史状态无关。说白了就是「即时响应」,输入一变,输出立刻跟着变。

描述组合逻辑有两种方式:

3.3.1 assign语句——简单粗暴

适合简单的逻辑运算,一行搞定:

assign y = (a & b) | (c & d);

assign的左边必须是wire类型。它相当于在硬件上焊了一根线,信号变化实时传递。

3.3.2 always块——灵活强大

适合复杂的组合逻辑,比如多分支判断:

always @(*) begin
    case (sel)
        2'b00: y = a + b;
        2'b01: y = a - b;
        2'b10: y = a & b;
        2'b11: y = a | b;
        default: y = 8'd0;
    endcase
end

注意这里的@(*),它表示敏感列表包含所有输入信号。我建议你写组合逻辑时都用@(*),别手动列信号。为什么?因为曾经有个同事手动列敏感列表,漏了一个信号,仿真怎么跑都对,上板子就出错...查了三天才发现。

组合逻辑的黄金法则:
  • 用阻塞赋值 =(非阻塞赋值 <= 留给时序逻辑)
  • 所有输入信号都要出现在敏感列表里(用 @(*) 最保险)
  • 每个分支都要赋值,否则会综合出锁存器

说到锁存器,这是新手最容易踩的坑。看这个例子:

// 错误示例:会综合出锁存器!
always @(*) begin
    if (en)
        y = a;
    // 缺少else分支!
end

当en=0时,y要保持原值,但组合逻辑没有记忆功能,所以综合工具会给你插入一个锁存器。锁存器这东西,时序分析很难搞,我建议你尽量别用。

正确的写法是给每个分支都赋值:

always @(*) begin
    if (en)
        y = a;
    else
        y = 8'd0;   // 给个默认值
end

3.4 知识体系总览

说了这么多,咱们用一张图把今天的内容串起来:

Verilog基础(上)知识体系 模块与端口 数据类型 组合逻辑 端口方向 input / output / inout 模块实例化 模块名 实例名 (端口连接) wire(线网) assign赋值 / 物理连线 reg(寄存器) always赋值 / 可综合为组合逻辑 assign语句 简单逻辑 / 连续赋值 always块 复杂逻辑 / @(*)敏感列表 核心要点 ① 模块是基本单元,端口定义输入输出 ② wire用于assign,reg用于always ③ 组合逻辑用阻塞赋值,避免产生锁存器

这张图把今天的内容分成了三大块。你仔细看看,其实它们之间是有逻辑关系的:模块是容器,端口是接口,wire和reg是数据类型,assign和always是描述方式。搞清楚了这些,Verilog就算入门了。

我的建议:刚开始学的时候,别急着写复杂代码。先拿简单的与门、或门练手,确保每个模块都能正确综合。我当年就是太心急,一上来就写状态机,结果综合出一堆警告,改得头大。

好了,今天的内容就到这里。记住:模块是骨架,数据类型是血肉,组合逻辑是行为。这三样搞明白了,后面的时序逻辑、状态机都是水到渠成的事。

可可精品资料库