第一章:数字逻辑基础与Verilog初识

各位同学好,我是你们的FPGA讲师。今天咱们从最底层开始,聊聊数字逻辑到底是怎么回事。说实话,很多初学者一上来就写Verilog代码,结果连最基本的“0”和“1”在硬件里是怎么来的都搞不清楚。我个人觉得,先把地基打牢,后面写RISC-V内核时才不会慌。

1.1 从晶体管到逻辑门

数字电路的基础是什么?是晶体管。说白了,晶体管就是一个开关。给它一个电压,它就导通;不给,它就断开。我们用这个开关来实现“0”和“1”。

举个例子,一个NMOS管,栅极接高电平时,源漏之间导通,输出低电平;栅极接低电平时,管子关断,输出高电平(通过上拉电阻)。这就是最简单的反相器——非门。

我在项目中遇到过一个问题:某次做低功耗设计,发现漏电流太大。查了半天,原来是晶体管阈值电压选低了。嗯,这里要注意,工艺库里的晶体管参数直接影响你的功耗和速度。

核心概念:逻辑门就是晶体管的组合。与门、或门、非门,这三个是最基本的。任何复杂的数字电路,包括RISC-V处理器,最终都是这些门的组合。

你想想看,一个32位的加法器,里面有多少个晶体管?几万个。但设计时我们不会一个个去画晶体管,而是用硬件描述语言(HDL)来抽象描述。

1.2 组合逻辑与时序逻辑基础

数字电路分两大类:组合逻辑和时序逻辑。这个区分很重要,我当年刚入行时经常搞混。

组合逻辑:输出只取决于当前输入。比如一个加法器,输入A和B,输出就是A+B。没有记忆功能。

时序逻辑:输出不仅取决于当前输入,还取决于之前的状态。比如一个计数器,它需要记住当前计数值。这就靠触发器(Flip-Flop)来实现。

为什么会这样?因为时序逻辑里有“时钟”这个家伙。时钟的上升沿或下降沿到来时,触发器才采样输入,更新输出。其他时间,输出保持不变。

我的经验:写Verilog时,组合逻辑用assign或always@(*)块,时序逻辑用always@(posedge clk)块。这个区分一定要刻在脑子里。我曾经见过一个同事把时序逻辑写成了组合逻辑,结果仿真没问题,上板子就跑飞了。

下面这张图展示了数字逻辑的基本分类和关系:

数字逻辑基础 组合逻辑 输出 = f(当前输入) 无记忆功能 例:加法器、多路选择器 时序逻辑 输出 = f(当前输入 + 历史状态) 有记忆功能(触发器) 例:计数器、状态机 关键区别 • 组合逻辑:没有时钟,输出随输入立即变化 • 时序逻辑:依赖时钟,输出在时钟边沿更新 • 实际设计中,两者通常混合使用

1.3 Verilog HDL语言概述与历史

Verilog诞生于1984年,由Gateway Design Automation公司开发。后来被Cadence收购,再后来成为IEEE标准(IEEE 1364)。

同期还有VHDL,但Verilog更简洁,更像C语言,所以做IC设计和FPGA的人用得更多。我个人习惯用Verilog,因为写起来快,调试也方便。

Verilog的核心能力是什么?就是用文本描述硬件。你可以描述一个门,也可以描述一个完整的RISC-V处理器。它支持不同抽象层次:

  • 行为级描述:用always块描述功能,不关心具体门电路
  • RTL级描述:寄存器传输级,描述数据如何在寄存器间流动
  • 门级描述:直接例化与门、或门等基本单元

注意:Verilog是硬件描述语言,不是编程语言。写Verilog时,脑子里要想的是电路,不是软件的执行流程。我曾经带过一个软件转硬件的工程师,他总喜欢用for循环写组合逻辑,结果综合出来的电路面积大得吓人。

1.4 第一个Verilog模块:半加器

好了,理论讲完了,咱们动手写第一个模块。半加器是最简单的加法电路,它把两个1位二进制数相加,输出和(Sum)与进位(Carry)。

真值表如下:

A B Sum Carry
0000
0110
1010
1101

从真值表可以看出:Sum = A XOR B,Carry = A AND B。就这么简单。

下面是用Verilog实现的半加器:

module half_adder (
    input  wire a,      // 输入位a
    input  wire b,      // 输入位b
    output wire sum,    // 和
    output wire carry   // 进位
);

    // 用连续赋值语句描述组合逻辑
    assign sum   = a ^ b;   // 异或运算
    assign carry = a & b;   // 与运算

endmodule

这段代码里,assign是连续赋值,表示sum和carry的值随a、b的变化立即更新。这就是组合逻辑的特点。

我的建议:初学者写模块时,先把端口定义写清楚。input、output、wire、reg这些关键字要搞明白。wire用于组合逻辑输出,reg用于时序逻辑(always块中赋值)。半加器是纯组合逻辑,所以用wire。

仿真测试一下:

module tb_half_adder;
    reg  a, b;
    wire sum, carry;

    half_adder uut (
        .a(a),
        .b(b),
        .sum(sum),
        .carry(carry)
    );

    initial begin
        a = 0; b = 0; #10;
        a = 0; b = 1; #10;
        a = 1; b = 0; #10;
        a = 1; b = 1; #10;
        $finish;
    end

    initial begin
        $monitor("a=%b b=%b sum=%b carry=%b", a, b, sum, carry);
    end
endmodule

运行结果:

a=0 b=0 sum=0 carry=0
a=0 b=1 sum=1 carry=0
a=1 b=0 sum=1 carry=0
a=1 b=1 sum=0 carry=1

嗯,和真值表完全一致。这个半加器虽然简单,但它包含了Verilog模块的基本结构:端口定义、内部逻辑、例化方式。后面我们写全加器、ALU、甚至RISC-V内核,都是在这个基础上扩展。

本章小结:我们从晶体管讲到逻辑门,从组合逻辑讲到时序逻辑,最后用半加器完成了第一个Verilog模块。这些是数字IC设计的基石。后面章节我们会逐步深入,从半加器扩展到全加器,再到算术逻辑单元,最终搭建一个完整的RISC-V处理器内核。


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