1. 从零开始:搭建你的第一个Quartus项目

如果你刚接触FPGA,看到Quartus这个软件可能会有点发怵,感觉界面复杂,选项繁多。别担心,我刚开始用的时候也是这种感觉,但跟着我一步步来,你会发现从画一个最简单的半加器开始,到做出一个能实际运行的四位全加器,整个过程其实非常清晰,也很有成就感。这篇文章,我就带你完整走一遍这个流程,把每一步的细节和容易踩的坑都讲清楚。

我们这次的目标很明确:用Intel(以前叫Altera)的Quartus Prime软件,在一块常见的Cyclone IV E系列开发板(具体型号是EP4CE115F29C7)上,实现一个四位全加器。为什么从半加器开始?因为这是理解数字电路加法最基础的“积木”。半加器只管两个一位二进制数相加,不考虑来自低位的进位。而全加器则更“聪明”一点,它能把来自低位的进位也考虑进去。把四个全加器串起来,就构成了我们的四位全加器。我们会用两种方式来实现它:一种是更直观的原理图绘制,就像搭积木一样;另一种是更灵活高效的Verilog代码编写。最后,我们还会把它烧录到真实的FPGA芯片上,看看小灯是怎么亮起来的。

首先,你得把软件准备好。我用的版本是Quartus Prime 18.1 Lite Edition,这个版本对学习来说完全免费,功能也足够强大。安装过程就是一路“Next”,记得安装路径不要有中文和空格,这是很多软件的通病。安装时,一定要把ModelSim-Altera这个仿真工具也勾选上,后面我们验证电路对不对全靠它了。装好软件,插上你的开发板,给电脑安装好USB-Blaster的驱动(一般开发板卖家会提供),硬件准备就齐活了。

打开Quartus,第一件事就是新建项目。点击菜单栏的 File -> New Project Wizard。第一个界面是项目目录和名称,我建议你专门建一个文件夹,比如叫“My_FPGA_Projects”,然后把这次的项目命名为“Full_Adder_Tutorial”。名字起得清楚点,以后项目多了好找。接下来的几步,如果你没有现成的设计文件,都直接点“Next”跳过。关键的一步来了:选择你的FPGA芯片。在“Device”页面,在“Family”里选择“Cyclone IV E”,然后在“Available devices”列表里找到“EP4CE115F29C7”并选中它。这一步千万不能选错,否则后面编译和烧录都会出问题。之后的EDA工具设置页面,在“Simulation”那一行,工具名选择“ModelSim-Altera”,格式选择“Verilog HDL”。这样,项目的基本框架就搭好了。你可能觉得步骤有点多,但大部分都是固定流程,做熟了一两分钟就能搞定。

2. 积木搭建法:用原理图设计加法器

对于硬件设计新手来说,直接看代码可能有点抽象,而原理图则非常直观,它能让你清晰地看到信号是怎么从输入流到输出的,每个逻辑门是如何连接的。我们就先从画图开始,感受一下“硬件连接”的思维。

2.1 绘制最基础的积木:半加器原理图

在Quartus里,原理图文件的后缀是 .bdf (Block Diagram File)。我们新建一个:File -> New,选择“Block Diagram/Schematic File”。首先,我们要把这个空白文件保存到我们的项目目录下,名字就叫 Half_adder.bdf。

现在,你的面前是一张“数字画布”。怎么往上面放元件呢?在空白处双击,或者点击左侧工具栏的符号工具(一个“与门”图标),会弹出“Symbol”对话框。这里就是Quartus的元件库。对于半加器,我们只需要两个基本逻辑门:一个异或门(XOR)和一个与门(AND)。在库里的路径通常是 primitives/logic 下面,找到 xor 和 and2(表示2输入与门)。把它们拖到画布上。

接下来是连线。点击工具栏的“正交节点工具”(像一根折线的图标),就可以开始画线了。把异或门的两个输入端分别引出来,命名为 a 和 b,它的输出端就是半加器的“和”(sum)。把与门的两个输入端也分别连接到 a 和 b,它的输出端就是“进位”(cout)。这里有个小技巧:放置输入输出端口。同样在“Symbol”对话框里,找到 primitives/pin 下面的 input 和 output。拖两个 input 进来,分别连接到 a 和 b 的连线,并把引脚名改为 a 和 b。拖两个 output 进来,一个连接到异或门输出,命名为 sum;一个连接到与门输出,命名为 cout。最后,别忘了用“选择工具”把图形排列整齐。你的半加器原理图应该看起来非常简洁:两个输入进来,经过两个门,产生两个输出。画好后,按 Ctrl+S 保存。

2.2 创建可复用的元件模块

画好半加器,我们当然可以直接用它。但更好的做法是把它封装成一个独立的“黑盒子”模块,这样在画更复杂的电路时,就可以像调用一个芯片一样直接调用它,而不用每次都把内部的异或门、与门再画一遍。这个封装后的文件后缀是 .bsf (Block Symbol File)。

操作很简单:在打开 Half_adder.bdf 文件的情况下,点击菜单 File -> Create/Update -> Create Symbol Files for Current File。软件会提示你成功创建了 Half_adder.bsf。现在,你可以在别的原理图中,像调用库里的门电路一样调用这个“Half_adder”模块了。这其实就是硬件设计中的“模块化”思想,大大提高了设计效率和可读性。

2.3 组装一位全加器

有了半加器这个积木,我们就可以搭建一位全加器了。新建一个原理图文件,保存为 Full_adder1.bdf。一位全加器有三个输入:加数 a、加数 b、来自低位的进位 cin;有两个输出:和 sum、向高位的进位 cout。它的经典结构是用两个半加器加一个或门构成的。

在空白处双击,这次在“Symbol”对话框的“Libraries”下面,你应该能看到“Project”目录,里面赫然躺着我们刚刚创建的 Half_adder!把它拖两个到画布上。然后再从元件库 primitives/logic 里拖一个 or2(2输入或门)出来。现在开始连线:第一个半加器(HA1)的输入接 a 和 b,它的 sum 输出接到第二个半加器(HA2)的 a 输入;HA2的另一个输入 b 接全加器的 cin。HA2输出的 sum 就是全加器的最终 sum。那么进位呢?HA1产生的进位(cout)和HA2产生的进位(cout)进行“或”运算,得到的结果就是全加器最终的 cout。最后,别忘了放置并命名输入输出引脚。当你把线连好,一个结构清晰的一位全加器原理图就诞生了。你可以清晰地看到,低位来的进位 cin 是如何参与到运算中,并与本位的进位进行合并的。同样,将这个全加器也创建为符号文件(.bsf),以备后用。

2.4 扩展为四位全加器

有了封装好的一位全加器模块,设计四位全加器就变成了简单的重复和连接工作。新建 Full_adder4.bdf。我们需要四个 Full_adder1 模块。把它们依次排开,分别代表从最低位(bit0)到最高位(bit3)的四个全加器。

关键是如何连接进位链:最低位全加器(FA0)的 cin 接整个四位加法器的进位输入(通常可以接地表示0);FA0的 cout 连接到FA1的 cin;FA1的 cout 连接到FA2的 cin,以此类推。最高位FA3的 cout 就是整个四位加法器的进位输出。输入方面,我们需要两个4位宽的输入 a[3..0] 和 b[3..0]。在Quartus原理图中,可以用“总线”来表示多位信号。放置输入引脚时,将其命名为 a[3..0],软件会自动识别为4位总线。然后用“正交节点工具”中的“总线模式”画出总线,再分别用“导线模式”从总线上引出单根线连接到每个全加器的对应输入位上。输出 sum[3..0] 和 cout 也同理。画完后的原理图会显得很有规模感,你能直观地看到进位信号像流水一样从最低位传递到最高位,这就是行波进位加法器(Ripple Carry Adder)的由来。它的优点是结构简单,缺点是高位要等到低位的进位算出来后才能开始计算,速度相对较慢。不过对于初学者理解原理,这再好不过了。

3. 代码描述法:用Verilog编写加法器

原理图直观,但当设计变得复杂时,画图会非常繁琐。这时,硬件描述语言(HDL)的优势就体现出来了。Verilog就像给硬件写“剧本”,用代码描述电路的功能和行为,然后由软件(综合器)自动转换成实际的电路网表。这种方式灵活、高效,是现代FPGA设计的主流。我们来用Verilog重新实现一遍加法器。

3.1 编写半加器与一位全加器模块

在Quartus中新建一个Verilog HDL文件(File -> New -> Verilog HDL File),保存为 half_adder.v。一个Verilog模块的基本结构是 module 和 endmodule 包裹。

// 半加器模块
module half_adder (
    input a,      // 输入a
    input b,      // 输入b
    output sum,   // 和输出
    output cout   // 进位输出
);
    // 数据流描述方式:用assign持续赋值语句描述逻辑关系
    assign sum = a ^ b;  // 异或:相同为0,不同为1,这正是“和”的定义
    assign cout = a & b; // 与:只有两个输入都为1时,进位才为1
endmodule

你看,代码非常简洁明了。^ 是Verilog中的按位异或运算符,& 是按位与运算符。assign 语句描述的是信号之间持续的赋值关系,一旦右边表达式的值改变,左边信号立刻更新,这正符合硬件电路“并行工作”的特性。

接下来是一位全加器 full_adder1.v。我们可以用结构化的方式,调用刚才写的半加器模块,就像在原理图里做的一样:

// 一位全加器(结构化描述)
module full_adder1 (
    input a, b, cin,
    output sum, cout
);
    wire s1, c1, c2; // 定义内部连接线

    // 实例化第一个半加器,实例化名称为ha1
    half_adder ha1 (
        .a(a),       // 端口连接:.模块端口名(连接线名)
        .b(b),
        .sum(s1),    // s1是内部信号,连接第一个半加器的和
        .cout(c1)
    );

    // 实例化第二个半加器
    half_adder ha2 (
        .a(s1),
        .b(cin),
        .sum(sum),   // 直接连接到模块的输出sum
        .cout(c2)
    );

    // 用或门产生最终进位
    assign cout = c1 | c2; // | 是按位或运算符
endmodule

这种写法完全对应了原理图的结构。但Verilog更强大的地方在于行为级描述。对于全加器这种简单的组合逻辑,我们完全可以用一个赋值语句搞定:

// 一位全加器(行为级描述,更简洁)
module full_adder1_behavioral (
    input a, b, cin,
    output sum, cout
);
    // {cout, sum} 是一个2位的拼接信号,cout是高位,sum是低位
    // a+b+cin 的结果是一个整数,Verilog会自动将其转换为二进制并赋值给拼接信号
    assign {cout, sum} = a + b + cin;
endmodule

这里用到了位拼接运算符 {} 和直接的算术运算符 +。综合器看到 +,会自动为我们生成一个加法器电路。这种方式写起来最快,可读性也高,但有时会牺牲对电路结构的精确控制。对于初学者,两种写法都值得掌握。

3.2 实现四位全加器模块

有了行为级描述的一位全加器,四位全加器写起来几乎一样简单。新建 full_adder4.v。

// 四位全加器
module full_adder4 (
    input [3:0] a, // [3:0] 表示一个4位宽的向量,a[3]是最高位,a[0]是最低位
    input [3:0] b,
    input cin,      // 进位输入
    output [3:0] sum, // 4位和输出
    output cout       // 进位输出
);
    // 核心就这一句:将5位宽的结果(1位进位+4位和)赋值给 {cout, sum}
    assign {cout, sum} = a + b + cin;
endmodule

看,是不是简单得不可思议?a + b + cin 这个表达式,综合器会为我们生成一个4位的加法器电路,并自动处理所有的进位逻辑。这就是硬件描述语言的魅力——你描述功能,工具负责实现细节。当然,我们也可以用结构化的方式,实例化四个 full_adder1 模块并把它们串起来,就像原理图里做的那样。这能让你更深刻地理解行波进位的过程。我建议你都尝试一下,然后对比综合出来的电路,看看有什么区别。

// 四位全加器(结构化描述,行波进位)
module full_adder4_structural (
    input [3:0] a,
    input [3:0] b,
    input cin,
    output [3:0] sum,
    output cout
);
    wire [3:0] c; // 内部进位信号,c[3]是最终进位,c[0],c[1],c[2]是中间进位

    // 实例化第0位(最低位)全加器
    full_adder1 fa0 (.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(c[0]));
    // 实例化第1位全加器,进位来自前一位
    full_adder1 fa1 (.a(a[1]), .b(b[1]), .cin(c[0]), .sum(sum[1]), .cout(c[1]));
    // 实例化第2位全加器
    full_adder1 fa2 (.a(a[2]), .b(b[2]), .cin(c[1]), .sum(sum[2]), .cout(c[2]));
    // 实例化第3位(最高位)全加器
    full_adder1 fa3 (.a(a[3]), .b(b[3]), .cin(c[2]), .sum(sum[3]), .cout(cout)); // 最终进位输出
endmodule

4. 验证逻辑:编译与功能仿真

设计完电路,不管是原理图还是代码,我们都不能直接烧到板子上。必须先用仿真工具验证一下逻辑功能是否正确。仿真就像在计算机里给电路搭建一个虚拟的测试环境,我们给它各种输入刺激,观察输出是否符合预期。这是FPGA开发中至关重要的一步,能避免很多低级错误。

4.1 编译设计并检查警告

在Quartus中,编译(Compilation)是一个将你的高级描述(原理图或Verilog)转换成FPGA芯片可以理解的低级网表文件的过程。点击工具栏上那个紫色的三角形“Start Compilation”按钮。编译过程包括分析综合、布局布线等多个阶段。

编译完成后,一定要仔细查看“Messages”窗口。如果有“Error”(错误),必须解决,否则无法进行下一步。常见的错误有语法错误、模块找不到、端口连接不匹配等。对于“Warning”(警告),虽然不影响生成文件,但也不能完全忽视。比如“引脚未分配”的警告在仿真阶段是正常的,但最终烧录前必须解决。再比如“时序约束未设置”的警告,对于这个简单的组合逻辑电路可以暂时不管,但对于复杂设计就必须重视。养成检查警告信息的习惯,是成为合格工程师的必修课。

4.2 创建并运行仿真测试文件

编译通过后,我们开始仿真。Quartus配合ModelSim进行仿真,需要先创建一个测试平台文件(Testbench)。Testbench本身也是一个Verilog模块,但它不对应任何实际的硬件。它的作用是实例化待测试的设计(DUT, Design Under Test),并产生测试激励。

以测试一位全加器为例,新建一个Verilog文件,保存为 tb_full_adder1.v。注意,Testbench模块没有输入输出端口。

`timescale 1ns / 1ns // 定义仿真时间单位/精度
module tb_full_adder1(); // 测试模块,无端口

    // 1. 声明与DUT输入端口对应的“寄存器”型变量,用于产生激励
    reg a, b, cin;
    // 2. 声明与DUT输出端口对应的“线网”型变量,用于观察响应
    wire sum, cout;

    // 3. 实例化待测试的设计(DUT)
    full_adder1 uut ( // uut (Unit Under Test) 是实例化名
        .a(a),
        .b(b),
        .cin(cin),
        .sum(sum),
        .cout(cout)
    );

    // 4. 生成测试激励
    initial begin
        // 初始化所有输入为0
        a = 0; b = 0; cin = 0;
        #10; // 等待10个时间单位(10ns)

        // 开始遍历所有8种可能的输入组合 (2^3=8)
        a=0; b=0; cin=0; #10; // 组合1
        a=0; b=0; cin=1; #10; // 组合2
        a=0; b=1; cin=0; #10; // 组合3
        a=0; b=1; cin=1; #10; // 组合4
        a=1; b=0; cin=0; #10; // 组合5
        a=1; b=0; cin=1; #10; // 组合6
        a=1; b=1; cin=0; #10; // 组合7
        a=1; b=1; cin=1; #10; // 组合8

        $stop; // 仿真停止
    end
endmodule

写好Testbench后,需要在Quartus中设置仿真工具和指定Testbench文件。点击 Assignments -> Settings,在左边Category中选择 EDA Tool Settings -> Simulation。确保“Tool name”是“ModelSim-Altera”,并且“Compile test bench”选项被选中。点击后面的“Test Benches...”按钮,新建一个,将“Test bench name”和“Top level module in test bench”都填写为 tb_full_adder1,然后在“File name”中添加你刚写的 tb_full_adder1.v 文件。

设置好后,重新编译整个项目。然后点击 Tools -> Run Simulation Tool -> RTL Simulation。ModelSim会自动启动并运行仿真。在ModelSim的波形窗口(Wave)中,你需要手动将DUT的端口信号拖进去。然后就可以看到随着时间变化,输入信号按照我们设定的序列变化,输出信号 sum 和 cout 也随之改变。你可以对照全加器的真值表,逐一检查这8个时间段的输出是否正确。比如,当 a=1, b=1, cin=1 时,二进制 1+1+1=11,所以 sum 应该为1,cout 应该为1。如果所有组合都通过了,恭喜你,你的全加器逻辑功能完全正确!

对于四位全加器的仿真,Testbench的编写思路类似,只是输入变成了4位宽,穷举所有组合(2^9=512种)不太现实。我们可以采用更灵活的激励方式,比如让输入在一定周期内自动递增,或者随机化测试。这里给出一个让输入自动变化的例子:

module tb_full_adder4();
    reg [3:0] a, b;
    reg cin;
    wire [3:0] sum;
    wire cout;
    full_adder4 uut (.a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));

    initial begin
        a = 4'b0000; b = 4'b0000; cin = 0;
        #200; // 运行一段时间

        // 测试几个边界和典型值
        a = 4'b1111; b = 4'b0001; cin = 0; #20; // 15+1=16, 应产生进位
        a = 4'b1001; b = 4'b0110; cin = 1; #20; // 9+6+1=16
        a = 4'b0101; b = 4'b1010; cin = 0; #20; // 5+10=15

        $stop;
    end
endmodule

5. 从虚拟到现实:引脚分配与硬件烧录

仿真通过,意味着我们的设计在逻辑上已经完美无缺。最后一步,就是把它放到真实的FPGA芯片上运行,用物理的输入(比如开关)和输出(比如LED灯)来验证。

5.1 引脚分配与约束

FPGA芯片有几十甚至几百个引脚,我们的设计信号(如 a[3..0], sum[3..0])需要映射到具体的物理引脚上,这些引脚连接着开发板上的按键、开关、LED等外设。这个过程叫做引脚分配(Pin Assignment)。

首先,你需要找到开发板的原理图或用户手册,弄清楚哪些引脚连接了什么外设。比如,你的开发板可能有4个拨码开关连接着FPGA的 PIN_AB10, PIN_AC10, PIN_AD10, PIN_AE10 这四个引脚,这4个引脚就可以作为我们的4位输入 a[3..0]。同样,找到4个LED对应的引脚作为 sum[3..0] 的输出,再找一个LED作为 cout 的输出,找一个按键作为 cin 的输入。

在Quartus中,有几种方式进行引脚分配。最直观的是使用“Pin Planner”图形化工具。点击 Assignments -> Pin Planner,会打开一个芯片引脚视图的表格。在“Node Name”列,你可以输入或从下拉列表中选择你的设计中的所有输入输出信号。然后在对应的“Location”列,输入或选择目标引脚的编号(如 PIN_AB10)。分配完后关闭即可,Quartus会自动保存这些约束到一个 .qsf 文件中。

另一种更专业、可维护性更高的方式是自己编写约束文件(.qsf 或 .sdc),但入门阶段用Pin Planner足够了。分配完引脚后,必须重新进行完整编译,因为布局布线需要根据新的引脚位置来优化。

5.2 程序烧录与上板测试

编译成功后,就可以连接开发板进行烧录了。确保开发板已上电,并通过USB-Blaster下载器连接到电脑。点击 Tools -> Programmer 打开编程器窗口。如果第一次使用,可能需要点击“Hardware Setup...”选择“USB-Blaster”作为硬件。然后点击“Auto Detect”,软件可能会识别出你的FPGA芯片型号(如EP4CE115)。

在编程器窗口中,你需要添加编译生成的 .sof 文件(SRAM Object File)。这个文件包含了FPGA的配置信息,但断电后会丢失。点击“Add File”,在项目输出目录(一般是 output_files 文件夹)下找到 .sof 文件并添加。确保“Program/Configure”选项框被勾选。

激动人心的时刻到了:点击“Start”按钮。进度条会开始走动,当达到100%时,烧录完成。此时,你的设计已经运行在FPGA芯片内部了!现在,你可以拨动开发板上的开关来改变输入 a 和 b 的值,按下或松开按键来改变 cin,同时观察LED灯的亮灭是否与加法结果一致。例如,设置开关为 a=0101 (5),b=0011 (3),cin=0,那么LED显示的 sum 应该是 1000 (8),并且进位LED应该不亮。如果 cin=1,则结果应为 1001 (9)。通过手动测试多组数据,你可以确信你的四位全加器在真实硬件上工作正常。

这个过程从抽象的图纸和代码,到虚拟的仿真验证,最终变成手中一块实实在在能运行的硬件,这种创造和实现的乐趣,正是数字电路和FPGA设计的魅力所在。我刚开始做第一个流水灯实验时,看到小灯按我的意愿亮起,那种兴奋感至今难忘。加法器是数字世界的基石,走通了这条路,后面学习更复杂的计数器、状态机、处理器,你都有了坚实的基础和信心。

更多推荐