从零开始搞懂FPGA:用Quartus设计一个会“记数”的电路
你有没有想过,电脑是怎么记住当前状态的?键盘敲下的每一个字符、程序运行的每一步跳转,背后都离不开一类特殊的数字电路——时序逻辑电路。它不像组合逻辑那样“健忘”,而是能记住过去的状态,并据此决定下一步动作。
在电子工程实验中,我们常通过 FPGA(现场可编程门阵列)来实现这类电路。而Intel Quartus Prime就是打开这扇大门的钥匙。今天,我们就以一个经典的“四位计数器”为例,带你一步步走过从写代码到点亮数码管的全过程,把抽象的时序逻辑变成看得见、摸得着的结果。
为什么需要“记忆”?时序电路的本质是什么?
先问一个问题:如果只给你一堆与门、或门、非门,你能做出一个自动递增的计数器吗?
答案是——不能。因为这些元件属于组合逻辑,输出完全由当前输入决定,没有“记忆力”。比如一个加法器,你给它3+4,它就出7;但如果你不告诉它上一次是多少,它永远不知道“下一个该是多少”。
要让电路具备记忆能力,就必须引入触发器(Flip-Flop)。最常见的就是D触发器:它会在每个时钟上升沿把输入 D 的值“锁”进内部,然后从 Q 输出。只要时钟不来,Q 就一直保持不变。
这就构成了时序逻辑的核心机制:
输出 = f(当前输入, 当前状态)
而“当前状态”是由触发器保存的,靠时钟统一驱动更新。
于是我们可以构建出:
- 计数器(每拍加一)
- 状态机(按流程切换状态)
- 寄存器堆(暂存数据)
- 甚至CPU控制单元
它们共同的特点是:有序、可控、可预测,而这正是现代数字系统稳定运行的基础。
开发工具怎么选?Quartus到底强在哪?
市面上做FPGA开发的工具有不少,Xilinx有Vivado,Lattice有Diamond……但我们这里用的是Quartus Prime,原因很简单:高校教学用得多、资料全、免费版够用。
更重要的是,它对初学者非常友好。哪怕你是第一次接触HDL语言,也能快速上手完成一次完整的设计闭环。
它能干什么?
| 功能 | 说明 |
|---|---|
| ✅ 写代码 | 支持 Verilog 和 VHDL |
| ✅ 画图 | 可以用原理图方式搭建模块(适合入门) |
| ✅ 综合 | 把你的代码翻译成实际的逻辑门和寄存器 |
| ✅ 引脚分配 | 拖拽式指定哪个信号连哪个物理引脚 |
| ✅ 仿真 | 看波形验证功能是否正确 |
| ✅ 下载 | 一键烧录到开发板 |
而且它自带和ModelSim的联动功能,仿真起来毫不费力。
最关键的是,它的Lite Edition(免费版)完全支持 Cyclone IV/V 系列芯片,正好匹配很多学校实验室用的 DE0-CV、DE10-Lite 这类开发板。
实战案例:做个会自己数数的电路
我们现在就动手做一个带清零和使能的四位同步计数器,让它接在七段数码管上,自动从0数到9再归零,循环往复。
系统结构长什么样?
整个系统的信号流如下:
[50MHz晶振] ↓ [分频器] → 产生1Hz时钟(太快三位看不清) ↓ [计数器] → 每秒+1,到9后归零 ↓ [BCD译码器] → 转换成七段码 ↓ [数码管显示]同时,我们还会用4个LED灯直接显示低四位二进制值,方便调试。
目标开发板是DE0-CV,主控为 Cyclone V FPGA(型号 EP4CE22E22C7)。
第一步:写代码——让电路“学会计数”
我们用 Verilog 来描述这个计数器的行为。别怕,不需要精通语法,关键是要理解逻辑意图。
// 文件名:counter_4bit.v module counter_4bit ( input clk, // 50MHz 主时钟 input rst_n, // 低电平有效复位 input en, // 计数使能 output reg [3:0] count // 4位计数输出 ); always @(posedge clk) begin if (!rst_n) count <= 4'b0000; // 异步清零 else if (en) begin if (count == 4'd9) count <= 4'b0000; // 数到9就归零 else count <= count + 1'b1; end end endmodule这段代码的关键点在于:
always @(posedge clk)表示“只有当时钟上升沿到来时才执行”!rst_n判断复位是否按下(低电平有效),优先级最高en是使能信号,相当于“启动开关”count == 4'd9实现模10计数,正好对应一位数码管- 使用非阻塞赋值
<=,这是时序逻辑的标准写法
⚠️ 注意:如果你在
else分支里漏写了en的情况,综合器可能会误判并生成锁存器(latch),带来功耗和时序问题!一定要确保所有路径都有明确赋值。
第二步:测试一下——别急着下载,先仿真!
代码写完不能直接下板子,万一逻辑错了呢?我们要先做功能仿真,看看它是不是真的会“数数”。
写个 Testbench(测试激励)
// 文件名:tb_counter.v `timescale 1ns / 1ps module tb_counter; reg clk, rst_n, en; wire [3:0] count; // 实例化被测模块 counter_4bit uut ( .clk(clk), .rst_n(rst_n), .en(en), .count(count) ); // 生成50MHz时钟(周期20ns) initial begin clk = 0; forever #10 clk = ~clk; // 半周期10ns end // 施加测试信号 initial begin rst_n = 0; // 上电复位 en = 0; #25; // 延迟25ns rst_n = 1; // 释放复位 #100; en = 1; // 启动计数 #400; // 观察四个周期 $stop; // 停止仿真 end endmodule这个 testbench 干了三件事:
1. 生成一个稳定的 50MHz 方波作为时钟;
2. 模拟真实上电过程:先拉低复位,再释放;
3. 在适当时间打开使能,观察计数行为。
然后我们在 Quartus 中点击Tools → Run Simulation Tool → RTL Simulation,自动调起 ModelSim,就能看到波形了。
波形结果说明什么?
你会看到这样的关键信号变化:
clk:规整的方波,周期20nsrst_n:开始为0,约25ns后变高count:复位期间为0;使能后开始递增,每次+1- 当
count=9后,下一拍回到0,完美循环
✅ 至此,功能正确性已验证。但这只是理想情况,还没考虑走线延迟、竞争冒险等问题。
第三步:编译 + 时序分析——现实世界没那么理想
接下来执行全编译(Processing → Start Compilation)。这个过程包括:
- 综合(Synthesis):将Verilog转为逻辑网表
- 布局布线(Fitter):把逻辑映射到FPGA的具体资源上
- 时序分析(Timing Analyzer):检查能否跑在目标频率下
编译完成后,打开Timing Analysis Report,重点关注两个指标:
| 项目 | 含义 |
|---|---|
| Minimum Clock Period | 最小时钟周期,本例约为8ns → 支持约125MHz |
| Setup Slack | 建立时间裕量,必须 >0 才安全 |
我们的设计工作在50MHz(周期20ns),远低于极限,所以肯定没问题。
但如果出现负的 slack(即时序违例),你就得优化设计了——比如拆分成更多级流水、减少组合逻辑层级,或者降低频率。
第四步:连接真实世界——引脚分配与下载
现在可以把它“焊”到开发板上了。当然不是真焊接,而是通过Pin Planner把信号绑定到具体引脚。
打开 Assignment → Pin Planner,填入以下典型配置:
| 信号 | FPGA引脚 | 对应硬件 |
|---|---|---|
| clk | PIN_Y2 | 板载50MHz晶振 |
| rst_n | PIN_R8 | KEY0 按键(低电平触发) |
| en | PIN_T8 | SW0 拨码开关 |
| count[0] | PIN_A13 | LEDR0 |
| count[1] | PIN_B13 | LEDR1 |
| count[2] | PIN_C13 | LEDR2 |
| count[3] | PIN_E13 | LEDR3 |
📌 提醒:务必核对开发板手册,避免误用配置引脚或电源引脚!
配置完成后重新编译,生成.sof文件。
插入 USB-Blaster 下载线,打开 Programmer 工具,选择 JTAG 模式,点击 “Start” —— 几秒钟后,你就会看到LED灯开始闪烁,数码管也开始自动递增!
🎉 成功了!你的第一个时序逻辑电路跑起来了。
高阶技巧与避坑指南
别高兴太早,实战中还有很多细节需要注意。下面是一些老工程师都不会轻易告诉你的经验。
1. 模块化设计才是王道
不要把所有逻辑写在一个文件里。建议拆分为:
-counter_4bit.v:核心计数
-seg7_decoder.v:BCD转七段码
-clock_divider.v:分频器(把50MHz降到1Hz)
这样便于复用和调试。
2. 参数化让你更灵活
别写死宽度和模值,改成参数:
module counter_param #( parameter WIDTH = 4, MODULUS = 10 )(...);以后想做个8位计数器?改个参数就行。
3. 时钟要用全局网络
高频时钟信号一定要走专用全局时钟线(Global Clock Buffer),否则偏移太大容易出错。Quartus 通常会自动推断,但最好显式使用 PLL 或create_clock约束。
4. 调试神器:SignalTap II
你想看内部某个中间信号?可以用SignalTap II Logic Analyzer,相当于在FPGA里嵌入一台示波器。
设置触发条件,抓取内部寄存器值,特别适合查状态机卡死、信号异常等问题。
5. 防止锁存器意外生成
再次强调:always 块中必须覆盖所有分支赋值!
错误示范:
if (sel) out = a; // else 没写 → 综合出锁存器!正确做法是补上else out = b;或默认赋值。
写在最后:从实验走向系统设计
通过这样一个简单的计数器实验,你已经走完了FPGA开发的完整流程:
写代码 → 仿真 → 编译 → 引脚约束 → 下载验证
这不是终点,而是起点。有了这套方法论,你可以继续挑战:
- 设计一个交通灯控制器(有限状态机)
- 实现UART串口收发(时序+状态管理)
- 构建流水线CPU(多模块协同)
每一次成功下载,都是你对数字世界的又一次掌控。
如果你也曾在仿真波形里找bug到深夜,也曾在引脚配错时怀疑人生——欢迎留言分享你的“踩坑史”。毕竟,每个优秀的FPGA工程师,都是从一次次
$stop中走出来的。