4. Verilog基础回顾(上):模块结构、端口定义、数据类型、连续赋值

各位同学,咱们今天聊聊Verilog最基础的东西。说实话,这部分内容看起来简单,但我在带新人时发现,很多bug都出在这些“常识”上。你想想看,如果连模块怎么搭、端口怎么接都没搞明白,后面写testbench肯定要翻车。

4.1 模块结构——Verilog的“骨架”

一个Verilog模块,说白了就是一个“黑盒子”。它定义了输入输出,以及内部怎么干活。我习惯把模块想象成一个芯片的封装——外面露出来的是引脚(端口),里面藏着的是逻辑(功能)。

最基本的模块长这样:

module counter (
    input  wire       clk,      // 时钟
    input  wire       rst_n,    // 复位,低有效
    output reg  [3:0] count     // 4位计数器输出
);

    // 内部逻辑写在这里

endmodule

嗯,这里要注意几个点:

  • module/endmodule:成对出现,就像括号一样,少一个编译器就报错。我曾经见过有人把endmodule拼成endmodle,找了半天bug……
  • 模块名:建议用有意义的英文,别用a、b、c。我在项目中吃过亏,三个月后回看代码,完全不知道“module top1”是干嘛的。
  • 端口列表:写在括号里,每个端口都要说明方向(input/output/inout)。
我的习惯:端口声明时,我会把时钟和复位放在最前面,数据信号放后面。这样别人看你的模块,第一眼就知道时序控制的关键信号在哪。

4.2 端口定义——给“引脚”起名字

端口定义有两种写法,我个人更推荐ANSI风格(也就是上面那种)。为什么?因为代码更紧凑,一眼就能看到端口的方向和类型。

另一种是传统风格,现在用得少了:

module counter (clk, rst_n, count);
    input       clk;
    input       rst_n;
    output [3:0] count;
    // 内部再声明 reg 或 wire
    reg    [3:0] count;
endmodule

你看,这种写法把端口声明和内部类型声明分开了,代码一长就容易乱。我建议新手直接学ANSI风格,少走弯路。

端口方向就三种:

方向 含义 举个栗子
input 输入,从外部进来 时钟、复位、数据输入
output 输出,送到外部去 计数结果、状态标志
inout 双向,既能进也能出 I2C的SDA线、三态总线
避坑指南:我曾经在项目中把一个inout端口误写成output,结果仿真时数据死活读不回来。查了两天才发现,原来是端口方向搞反了。记住:inout端口内部必须用三态门控制,不能直接赋值。

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

这个问题,我几乎每次面试新人都会问。很多人背了答案:“wire是线网,reg是寄存器”。但实际用起来,还是搞混。

说白了,记住一条黄金法则:

  • wire:用assign连续赋值,或者连接模块端口。它不存储值,只是“导线”。
  • reg:用在always块里,被赋值。它存储值,可以想象成一个小容器。

但注意!reg不一定是寄存器。在组合逻辑的always块里,reg综合出来只是导线。这个坑我当年也踩过——以为reg一定会生成触发器,结果综合出来一堆LUT。

看个例子就明白了:

wire       a, b;      // 导线
wire       sum;       // 导线
reg        c;         // 可以是寄存器,也可以是组合逻辑

// 连续赋值:用wire
assign sum = a + b;

// 时序逻辑:用reg,生成触发器
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        c <= 1'b0;
    else
        c <= a & b;
end
核心总结:在always块里被赋值的变量,必须声明为reg。在assign语句里被赋值的变量,必须声明为wire。记住这个,90%的情况不会错。

4.4 连续赋值(assign)——组合逻辑的“直通车”

assign语句,说白了就是“一有风吹草动,立刻更新”。它描述的是组合逻辑,没有时钟,没有边沿触发。

基本语法:

assign 目标信号 = 表达式;

举个例子:

module adder (
    input  wire [3:0] a, b,
    output wire [4:0] sum      // 多1位防止溢出
);
    assign sum = a + b;
endmodule

这里sum是wire类型,用assign赋值。只要a或b变了,sum立刻跟着变。这就是“连续”的含义——赋值是持续的,不是一次性的。

assign可以做的事情很多:

  • 算术运算:assign y = a + b - c;
  • 逻辑运算:assign flag = (a > b) & enable;
  • 位运算:assign data_out = {data_in[3:0], 4'b0};
  • 三态门:assign bus = enable ? data : 1'bz;
我的经验:写assign时,尽量让表达式简单明了。如果一行写不下,就拆成多行。我曾经见过有人把几十个信号塞进一个assign里,那代码简直没法调试。记住:代码是写给人看的,顺便给机器跑。

4.5 知识体系总览

为了让你更直观地理解这四块内容的关系,我画了张图:

Verilog 模块 模块结构 module/endmodule 端口列表 内部逻辑 端口定义 input / output / inout ANSI风格 传统风格 数据类型 wire:导线 reg:存储 选择规则 连续赋值 (assign) 组合逻辑 表达式驱动

这张图把四个知识点串起来了。你看,模块是外壳,端口是接口,wire/reg是材料,assign是连接方式。四者缺一不可。

最后说一句:Verilog基础就像盖房子的地基。地基没打好,后面盖再高的楼也悬。我建议你把这章的内容亲手敲一遍代码,别光看。仿真跑通了,才算真懂了。

可可精品资料库