一、Verilog 模块——数字世界的“黑盒子”

各位同学,咱们今天聊聊 Verilog 里最基础的东西——模块与端口。

你想想看,数字电路设计说白了就是搭积木。每个积木块就是一个 module。我刚开始学的时候,总觉得这东西玄乎,后来做项目多了才发现,module 其实就是个“黑盒子”。

盒子里装的是逻辑,盒子外面露出来的是端口。别人用你的模块,根本不用管里面怎么实现的,只要知道怎么接信号就行。

1.1 module/endmodule——模块的骨架

每个 Verilog 文件,开头一定是 module 模块名,结尾一定是 endmodule。这是规矩,少一个都不行。

module my_first_module (
    // 端口声明写在这里
);

// 内部逻辑写在这里

endmodule

嗯,这里要注意:模块名最好见名知意。我见过有人用 module amodule b 这种名字,三个月后自己都看不懂。我个人习惯用 模块名_功能 的格式,比如 counter_8bituart_tx

1.2 端口定义——模块的“手脚”

端口分三种:inputoutputinout

端口类型 方向 典型用途
input 输入 时钟、复位、数据输入
output 输出 数据输出、状态指示
inout 双向 I2C数据线、三态总线

说白了,input 是别人给你的信号,output 是你给别人发的信号。inout 比较特殊,既能发也能收,但同一时刻只能干一件事。

我在项目中遇到过用 inout 端口踩坑的情况。当时做 I2C 控制器,忘了加三态控制,结果数据线冲突,仿真没问题,上板就死。后来加了个 assign sda = (sda_en) ? sda_out : 1'bz; 才搞定。

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

assign y = a & b;

endmodule

二、wire 与 reg——数据类型的“双胞胎”

很多新手搞不清 wire 和 reg 的区别。我当年也迷糊过一阵子。

其实很简单:

  • wire:线网类型,相当于物理连线。它不能存储值,只能传递值。
  • reg:寄存器类型,可以存储值。但注意,它不一定是真正的寄存器!

为什么会这样?因为 reg 在组合逻辑里只是“变量”的概念,综合出来可能只是一根线。只有在时序逻辑(always @(posedge clk))里,它才会变成真正的触发器。

重要原则:

  • assign 语句左边必须是 wire
  • always 块里赋值的变量必须是 reg
  • 模块端口默认是 wire 类型

我建议你刚开始做设计时,先记住这个规则:组合逻辑用 wire,时序逻辑用 reg。等做多了自然就理解了。

三、assign 连续赋值——组合逻辑的“直通车”

assign 语句是 Verilog 里最直接的组合逻辑描述方式。它的语法很简单:

assign 左边信号 = 右边表达式;

左边必须是 wire 类型,右边可以是任何表达式。只要右边信号一变,左边立刻更新。这就是“连续赋值”的含义——信号一直在“连续”地计算。

来看几个例子:

// 基本逻辑门
assign y = a & b;           // 与门
assign y = a | b;           // 或门
assign y = ~a;              // 非门
assign y = a ^ b;           // 异或门

// 多输入组合
assign y = (a & b) | (c & d);  // 与或组合

// 条件赋值(三目运算符)
assign y = sel ? a : b;     // 二选一多路器

我曾经犯过一个低级错误:在 assign 语句里写了 assign y = y + 1;。你想想看,这相当于把输出又连回输入,综合出来就是个组合逻辑环,仿真直接崩了。所以记住:assign 不能描述带反馈的电路

四、组合逻辑电路实战——用模块实现一个全加器

光说不练假把式。咱们用刚才学的知识,写一个 1 位全加器。

全加器有三个输入:a、b、cin(进位输入),两个输出:sum(和)、cout(进位输出)。

真值表如下:

a b cin sum cout
00000
00110
01010
01101
10010
10101
11001
11111

逻辑表达式:

  • sum = a ^ b ^ cin
  • cout = (a & b) | (a & cin) | (b & cin)

用 Verilog 实现:

module full_adder (
    input  wire a,
    input  wire b,
    input  wire cin,
    output wire sum,
    output wire cout
);

assign sum  = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);

endmodule

你看,用 assign 语句描述组合逻辑就是这么直接。把逻辑表达式写出来,assign 一赋值,完事。

小技巧:写组合逻辑时,尽量用 assign 而不是 always @(*)。assign 更直观,综合结果也更可控。我一般只在需要 if-else 或 case 语句时才用 always。

五、本章知识体系

下面这张图帮你理清本章的核心脉络:

Verilog 模块与端口知识体系 module/endmodule 端口定义 数据类型 input output inout wire(连线) reg(变量) assign 连续赋值 → 组合逻辑 核心原则:组合逻辑用 wire + assign,时序逻辑用 reg + always

避坑指南:

  • 千万不要在多个 always 块里对同一个 reg 赋值,综合会报多驱动错误
  • inout 端口一定要有三态控制,否则上板必死
  • assign 左边不能是 reg,always 块里不能给 wire 赋值

好了,这一章的内容就这些。模块、端口、wire 和 reg、assign 语句,这四个东西是 Verilog 的基石。你把这章吃透了,后面学时序逻辑、状态机什么的就轻松多了。

记住我的一句话:写 Verilog 不是写软件,你脑子里要时刻想着电路。每个 assign 都是一根线,每个 reg 都可能是一个触发器。想清楚了再下笔,比写完再改要快十倍。

专注资料整理