1. Verilog基础与数字设计入门

各位同学好,我是你们的讲师。今天咱们聊聊Verilog——这个在数字芯片设计领域摸爬滚打了几十年的硬件描述语言。说实话,我刚开始接触Verilog时也觉得它有点“怪”,跟C语言长得像,但思维方式完全不同。不过别担心,这一章我会把最核心的东西掰开揉碎了讲给你听。

1.1 Verilog的前世今生

Verilog诞生于1984年,由Gateway Design Automation公司开发。那时候数字电路设计还在用手工画门级电路图,效率低得吓人。Verilog的出现,说白了就是让设计师能用“写代码”的方式描述硬件。

我记得2005年刚入行时,老工程师还在用原理图设计,我试着用Verilog写了个简单的计数器,他们看了直摇头。结果后来流片验证,我的设计一次通过,他们才服气。嗯,这就是技术进步的力量。

Verilog在1995年成为IEEE标准(IEEE 1364),2005年又吸收了SystemVerilog的很多特性。现在,它和VHDL并列为两大主流硬件描述语言。我个人习惯用Verilog,因为它的语法更简洁,上手快。

1.2 模块结构——Verilog的骨架

Verilog设计的基本单元是模块(module)。一个模块就像一块芯片,有输入输出引脚,内部有逻辑功能。来看个最简单的例子:

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

    assign y = a & b;  // 与门逻辑

endmodule

这个模块描述了一个两输入与门。注意看:inputoutput定义了端口方向,wire表示连线类型。模块以endmodule结束,就像C语言的大括号。

我的小建议:写模块时,端口声明顺序最好统一——先输入、再输出、最后是双向端口(inout)。这样别人看你的代码时一目了然。我在项目中吃过亏,有一次端口顺序乱写,结果综合时工具报了一堆警告,排查了半天才发现是端口声明的问题。

1.3 数据类型——wire和reg

Verilog中最常用的两种数据类型是wirereg。很多新手搞不清它们的区别,我当年也迷糊过。

类型 含义 典型用法
wire 连线型,表示物理连线 assign语句、模块端口连接
reg 寄存器型,表示存储单元 always块中的赋值

简单来说:wire就像一根导线,只能被连续赋值驱动;reg就像一个小盒子,可以在时钟边沿保存数据。但注意,reg不一定会综合成寄存器——如果你在组合逻辑的always块里用reg,它综合出来还是连线。

避坑指南:我曾经在项目中把一个应该用wire的端口声明成了reg,结果仿真没问题,但综合时报了“multi-driver”错误。排查了两天才发现是类型用错了。所以记住:模块端口默认是wire类型,只有需要在always块里赋值时才用reg。

1.4 操作符——数字电路的“数学工具”

Verilog的操作符跟C语言很像,但有些特殊的地方。我整理了一张表:

类别 操作符 说明
算术 + - * / % 加减乘除取模
位运算 & | ~ ^ 与、或、非、异或
逻辑 && || ! 逻辑与、或、非
关系 > < >= <= == != 比较运算
移位 << >> 左移、右移
拼接 { } 将多个信号拼接成更宽的向量

这里有个容易混淆的点:位运算逻辑运算的区别。位运算是按位操作的,逻辑运算只返回0或1。举个例子:4'b1010 & 4'b1100结果是4'b1000;而4'b1010 && 4'b1100结果是1'b1(因为两个数都不为0)。

拼接操作符{}是我个人非常喜欢用的。比如你想把两个4位信号拼成一个8位信号:wire [7:0] result = {a, b};。简洁又直观。

1.5 组合逻辑建模——assign和always

组合逻辑就是输出只取决于当前输入,跟历史状态无关。Verilog里建模组合逻辑有两种方式:assignalways

1.5.1 使用assign连续赋值

assign是最直接的方式,它描述了一个持续不断的赋值关系。比如:

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

    assign sum  = a ^ b ^ cin;   // 和
    assign cout = (a & b) | (a & cin) | (b & cin);  // 进位

endmodule

这个全加器用了三个异或和几个与或门。注意assign左边必须是wire类型,右边可以是表达式。综合工具会自动生成对应的门级电路。

1.5.2 使用always块建模

always块更灵活,可以描述更复杂的逻辑。对于组合逻辑,要用敏感列表列出所有输入信号:

module mux_2to1 (
    input  wire [3:0] a, b,
    input  wire sel,
    output reg  [3:0] y
);

    always @(*) begin
        if (sel)
            y = b;
        else
            y = a;
    end

endmodule

这里always @(*)表示对所有输入信号敏感——只要a、b、sel中任何一个变化,就重新计算y。注意输出y声明为reg类型,因为它在always块里被赋值。

重要提醒:写组合逻辑的always块时,一定要确保所有分支都覆盖到。比如if语句要有else,case语句要有default。否则综合工具会推断出锁存器(latch),这通常不是你想要的结果。我曾经在项目中漏写了一个else,结果综合出来的电路多了几个latch,导致功能完全不对,查了两天才发现。

1.6 知识体系总览

为了让你更直观地理解本章的知识结构,我画了一张图:

Verilog基础与数字设计入门知识体系 Verilog历史(1984-至今) 模块结构(module/endmodule) 数据类型(wire / reg) 操作符(算术/位/逻辑/关系/移位/拼接) 组合逻辑建模(assign / always)

这张图展示了本章的五个核心知识点,从历史背景到具体建模方式,层层递进。你学完这一章,应该能自己写出简单的组合逻辑模块了。

1.7 本章小结

好了,咱们来捋一捋今天讲了什么:

  • Verilog历史:从1984年诞生到成为IEEE标准,它让数字设计从画图变成了写代码。
  • 模块结构:module是基本单元,端口声明要规范。
  • 数据类型:wire是连线,reg是寄存器(但组合逻辑里用reg不一定会综合出寄存器)。
  • 操作符:跟C语言类似,但要注意位运算和逻辑运算的区别。
  • 组合逻辑建模:assign适合简单逻辑,always适合复杂逻辑,但一定要避免生成latch。

说实话,这些基础内容看起来简单,但我在实际项目中见过太多人栽在数据类型和latch问题上。你想想看,如果连wire和reg都分不清,后面写状态机、写时序逻辑时肯定会出问题。所以这一章一定要吃透。

下一章咱们会深入时序逻辑,讲触发器、寄存器、计数器这些更“硬核”的东西。到时候你会发现,有了今天的基础,学起来会轻松很多。

课后练习:试着用assign和always两种方式分别实现一个4位比较器(比较两个4位数的大小,输出a>b、a==b、a<b三个信号)。写完后用仿真工具跑一下,看看结果对不对。

公众号:蓝海资料掘金营,微信deep3321