简介:面向电子信息、自动化等专业学生及EDA初学者的Quartus II实验报告文档,以FPGA驱动LED为主题,帮助读者熟悉开发流程与VHDL基础。内容围绕SmartSOPC实验箱展开,涵盖工程建立、器件选择EP3C5E144C8、VHDL文件添加、编译下载,以及点亮LED1、LED3、LED5、LED7的验证;并解释FPGA的I/O可配置为输入、输出、双向等模式,输出时最大可吸收24mA电流直接驱动发光二极管。包内仅1个PDF文件,约2.84MB,图文并茂,含实验目的、实验内容、原理分析、步骤说明与结果观察记录,便于课程实验参考与报告撰写借鉴,对实验报告格式与数据记录也有一定参考价值。目前已有161人学习,可作为Quartus II操作、引脚分配与下载配置等环节的对照材料。
1. 从一份 EDA 实验报告说起:Quartus 驱动 LED 到底在做什么
很多人第一次打开 Quartus,面对的是空白的工程向导和几十页的器件手册,实验指导书却只写了一句"点亮 LED"。真正卡人的从来不是那行assign,而是它前面的链路:用 Verilog 把逻辑写出来,综合器把它映射成查找表和触发器,布局布线器再把信号绑到具体的物理引脚上,最后通过下载器把配置写进 FPGA,灯才亮。任何一环错位,板子给你的反馈都是一片漆黑。
这份实验报告的价值,把 Quartus 的工程流、引脚约束、功能仿真和在线调试串成一条可复现的路径,比结果本身重要得多。它适合刚接触 FPGA 的物理、电子类专业学生,也适合从单片机转过来、习惯GPIO_Init()却对"并行执行"和"引脚定义在前端完成"这两件事没感觉的工程师。后面我按实验台上真正会走的顺序来讲:先建工程,再写驱动,然后仿真烧录,最后落到亮度可调这种进阶需求。
2. Quartus 工程搭建与 LED 驱动的引脚约束
2.1 器件选型与新建工程的关键参数
Quartus 现在主流是两个分支:老的 Quartus II(13.1 是很多教学板驱动兼容性最好的版本)和新的 Quartus Prime(Lite 版免费,20.1、25.1 都在持续更新)。教学实验里 Cyclone IV E 系列最常见,型号如 EP4CE6E22C8、EP4CE10F17C8,容量从 6K 到 10K 逻辑单元;如果是新板子,可能是 MAX 10 或 Cyclone V。选型时只看一件事:板子上那颗芯片的完整型号,必须和工程里选的完全一致,否则后面引脚约束会报"pin location 不存在"。
新建工程走File > New Project Wizard,三个地方要盯住:工程目录和工作名、顶层实体名(Top-Level Entity,默认与工程名相同,但可以改)、器件家族与具体型号。EDA 工具设置那一页可以留空,也可以挂 ModelSim 做仿真。
提示:顶层实体名一旦确定,和你
module后的名字必须一字不差,大小写敏感。这是新手第一个高频报错来源。
2.2 Verilog 顶层模块:LED 驱动的第一步
LED 驱动在 FPGA 上本质是一个分频加输出。下面是最小可跑版本,50MHz 时钟,4 位 LED,2Hz 翻转:
module led_drive ( input wire clk, // 50MHz 板载时钟 input wire rst_n, // 低电平复位 output reg [3:0] led // 4 位 LED,低电平点亮 ); reg [24:0] cnt; // 25 位计数器,2^25 ≈ 33.5M // 分频计数:每 25M 个时钟翻转一次,得到 2Hz always @(posedge clk or negedge rst_n) begin if (!rst_n) cnt <= 25'd0; else if (cnt == 25'd24_999_999) cnt <= 25'd0; else cnt <= cnt + 1'b1; end // LED 状态翻转 always @(posedge clk or negedge rst_n) begin if (!rst_n) led <= 4'b1111; // 复位时全灭(低电平点亮) else if (cnt == 25'd24_999_999) led <= ~led; // 每 0.5s 取反一次 else led <= led; end endmodule逻辑说明:计数器cnt负责把 50MHz 降到人眼可分辨的频率,cnt位宽 25 位是因为 2^25 = 33 554 432 > 25 000 000,够用又不浪费触发器和布线资源。第二个always块里保留led <= led这一句是显式给出所有分支的赋值,避免综合器推断出锁存器(latch)。参数说明:24_999_999决定翻转周期,想要 1Hz 就改成49_999_999并把位宽提到 26 位;想要 10Hz 就改成4_999_999;复位值4'b1111和板子的点亮电平有关,共阳板子低电平点亮,共阴则改成4'b0000。
2.3 Pin Planner 引脚分配与 I/O 标准
代码编译通过不代表灯会亮,因为信号还没落到物理引脚。引脚编号的唯一来源是开发板原理图,常见教学板的对照如下(以某 Cyclone IV EP4CE6 板为例,具体以你手里的原理图为准):
| 端口名 | 板载资源 | 示例引脚 | I/O 标准 |
|---|---|---|---|
| clk | 50MHz 晶振 | PIN_23 | 3.3-V LVTTL |
| rst_n | 复位按键 | PIN_25 | 3.3-V LVTTL |
| led[0] | LED0 | PIN_87 | 3.3-V LVTTL |
| led[1] | LED1 | PIN_88 | 3.3-V LVTTL |
| led[2] | LED2 | PIN_89 | 3.3-V LVTTL |
| led[3] | LED3 | PIN_90 | 3.3-V LVTTL |
分配方式有两种:图形界面用Assignments > Pin Planner,双击 Location 列填引脚号;命令行或批量时直接写 QSF:
# led_drive.qsf 片段 set_location_assignment PIN_23 -to clk set_location_assignment PIN_25 -to rst_n set_location_assignment PIN_87 -to led[0] set_location_assignment PIN_88 -to led[1] set_location_assignment PIN_89 -to led[2] set_location_assignment PIN_90 -to led[3] set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to led[*]参数说明:IO_STANDARD一定要和板子供电电压匹配,3.3V 器件写成 2.5-V 会导致电平判定错误,灯表现成常亮或半亮。led[*]是通配写法,一次覆盖整个总线。
2.4 编译流程与常见报错定位
编译用Processing > Start Compilation或 Ctrl+L。编译完成后看 Compilation Report,重点三块:Analysis & Synthesis 有无 latch 警告、Fitter 报告的引脚分配成功率、Timing Analyzer 的 Fmax。
高频报错和对应处理:
Error (12007): Top-level design entity "led" is undefined → 顶层实体名与 module 名不一致,检查大小写 Error (176310): Can't place multiple pins assigned to pin location PIN_87 → 同一引脚被分配两次,检查 QSF 是否重复 Critical Warning (332012): No clocks defined in design → 时钟端口没被识别,确认 clk 引到寄存器且已分配引脚定位方法:Error 里带 "(数字)" 的是 Quartus 内部错误码,Messges 窗口双击可直接跳到源文件行号;Critical Warning 不一定阻断编译,但必须在烧录前处理,否则行为不可预期。
3. LED 驱动逻辑设计:从静态点亮到流水灯与 PWM 调光
3.1 分频计数器的参数计算
分频是 LED 驱动的地基。公式很简单:想要输出频率 f_out,时钟 f_clk,计数终值 N = f_clk / (2 × f_out)。50MHz 出 1Hz,N = 25 000 000,位宽取 ⌈log2(N)⌉ = 25 位。有人图省事写 32 位,综合后多消耗几十个触发器,在资源紧的 EP4CE6 上可能就是布不下某些逻辑的临界点。
注意:计数终值用十进制下划线分隔写清楚,
25000000比25_000_000更容易看错位数,一个 0 的差别就是 10 倍频率。
3.2 流水灯的状态机实现
流水灯用移位寄存器最省资源,不需要状态机:
// 在 led_drive 基础上增加移位逻辑 reg [24:0] div_cnt; wire tick = (div_cnt == 25'd4_999_999); // 10Hz 节拍 always @(posedge clk or negedge rst_n) begin if (!rst_n) div_cnt <= 25'd0; else if (tick) div_cnt <= 25'd0; else div_cnt <= div_cnt + 1'b1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) led <= 4'b0001; // 初始仅 LED0 亮 else if (tick) led <= {led[2:0], led[3]}; // 循环左移 end逻辑说明:tick是一个单周期脉冲,把 50MHz 降成 10Hz 的节拍,移位操作只在节拍到来时发生。{led[2:0], led[3]}是拼接运算,把低 3 位和高 1 位重新组合,实现环形左移。想改成右移,写成{led[0], led[3:1]};想 8 路就用[7:0]并把拼接改为{led[6:0], led[7]}。4_999_999决定流水速度,改小变快。
3.3 PWM 呼吸灯的占空比控制
LED 亮度可调靠 PWM,核心是一个比较器:计数器小于占空比时输出点亮电平,否则熄灭。占空比缓慢变化就成了呼吸效果。
// PWM 呼吸灯核心,14 位计数器 reg [9:0] pwm_cnt; // 0 ~ 1023,PWM 周期 1024 个时钟 reg [9:0] duty; // 当前占空比 reg dir; // 0 递增,1 递减 wire pwm_clk; // 由前面降频得到,约 1kHz always @(posedge pwm_clk or negedge rst_n) begin if (!rst_n) begin pwm_cnt <= 10'd0; duty <= 10'd0; dir <= 1'b0; end else begin // 计数器循环 0~1023 pwm_cnt <= (pwm_cnt == 10'd1023) ? 10'd0 : pwm_cnt + 1'b1; // 每个 PWM 周期末更新占空比 if (pwm_cnt == 10'd1023) begin if (dir == 1'b0) begin if (duty == 10'd1023) dir <= 1'b1; else duty <= duty + 1'b1; end else begin if (duty == 10'd0) dir <= 1'b0; else duty <= duty - 1'b1; end end end end assign led = (pwm_cnt < duty) ? 1'b0 : 1'b1; // 低电平点亮逻辑说明:PWM 周期由pwm_cnt从 0 数到 1023 决定,占空比duty决定高电平时间。dir控制占空比在 0 到满之间往返,形成呼吸。参数说明:PWM 频率要高于 200Hz 才不会被人眼察觉闪烁,pwm_clk取 1kHz 时 1024 个周期对应约 1Hz 的呼吸节奏;想要呼吸更快就把pwm_clk提到 2kHz 或把duty步进改成+2/-2。若同时驱动多路 LED 且亮度要独立,每路各带一个duty寄存器和一个比较器即可。
3.4 驱动模式与资源配置对照
| 模式 | 触发器 | 关键参数 | 频率要点 |
|---|---|---|---|
| 静态点亮 | 4 | 无 | 无 |
| 2Hz 闪烁 | 25 + 4 | 计数终值 | 人眼可分辨,≤5Hz |
| 流水灯 | 25 + 4 | 节拍终值 | 5~20Hz 视觉效果最好 |
| PWM 呼吸 | 10 + 10 + 1 | PWM 周期、步进 | PWM ≥ 200Hz,呼吸 0.5~2Hz |
资源不是越省越好,而是要"刚好够"。PWM 的 10 位分辨率对应 1/1024 的亮度级差,人眼已经基本看不出台阶;如果想要更平滑,升到 12 位代价是计数器多 2 位触发器,几乎无感。
4. 仿真、烧录与在线调试
4.1 功能仿真:Testbench 与 ModelSim
烧板子前先仿真,能省掉反复插拔下载器的功夫。写一个最小 Testbench:
`timescale 1ns/1ps module tb_led_drive; reg clk; reg rst_n; wire [3:0] led; led_drive uut (.clk(clk), .rst_n(rst_n), .led(led)); // 产生 50MHz 时钟:周期 20ns initial begin clk = 1'b0; forever #10 clk = ~clk; end initial begin rst_n = 1'b0; #100 rst_n = 1'b1; // 100ns 后释放复位 #2000 $stop; // 仿真 2us 后停止 end endmodule逻辑说明:#10 clk = ~clk产生半周期 10ns 的时钟,即 50MHz。复位在 100ns 后拉高,观察led是否按 0.5s 周期翻转。因为仿真时间通常跑不到 0.5s,实际验证时把计数终值临时改小(比如改成 49)再跑波形,否则仿真器要跑很久。工具上挂 ModelSim-Altera,在 Quartus 里Tools > Run Simulation Tool > RTL Simulation直接调用;用 Vivado 的小伙伴换成 xsim,思路一样。
4.2 下载器驱动与 JTAG 烧录
USB-Blaster 下载器插上电脑后,如果 Quartus Programmer 里 Hardware 下拉框是空的,九成是驱动没装。驱动位置在安装目录下:
<Quartus安装目录>\quartus\drivers\usb-blaster设备管理器里找到带感叹号的未知设备,右键"更新驱动程序",手动指到上面这个文件夹。装完后 Quartus 里Tools > Programmer能自动识别。
烧录流程:Auto Detect 确认检测到芯片型号 → Add File 添加.sof文件 → 勾选 Program/Configure → Start。.sof是 SRAM Object File,掉电即失;要断电保存,需要在Convert Programming Files里生成.jic(JTAG Indirect Configuration),再烧到配置芯片里。
注意:烧录前务必回到 Programmer 确认 Mode 是 JTAG、下载器型号与实物一致。选错下载器,Start 会一直卡在进度条不动。
4.3 SignalTap 抓波形定位 LED 不亮
灯不亮时别急着改代码,先用 SignalTap 看内部信号。Tools > SignalTap Logic Analyzer打开,新建实例,采样时钟选clk,把cnt、led、tick拖进 Setup 列表,Sampled 设为 1024,编译后用 JTAG 下载并运行。
典型结论分三种:
cnt完全不动:时钟没进 FPGA 或引脚分配错误,回查 QSF 的clk和原理图晶振脚。cnt正常但led不变:看两处,复位信号是否一直有效(按键极性反了),以及翻转条件里的终值是否写错位数。led在跳但板子不亮:99% 是极性或引脚问题,把led输出取反测试,或换一个已知好的引脚。
5. LED 亮度可调:多路驱动与资源时序优化技巧
要做多路、多级亮度,先把"驱动方式"想清楚。FPGA 直接驱动几个 LED 是最简单的,但如果要驱动 LED 模组或点阵,常见的做法是用扫描:行选通 + 列数据,配合刷新率不低于 100Hz。若亮度还要逐路独立可调,把第 3 章的 PWM 复制一份,每路给一个独立的duty寄存器和比较器,共享同一个pwm_cnt,这样计数器只占一份资源。
// 多路独立亮度:4 路共享 PWM 计数器 wire [3:0] pwm_out; genvar i; generate for (i = 0; i < 4; i = i + 1) begin : gen_pwm assign pwm_out[i] = (pwm_cnt < duty[i]) ? 1'b0 : 1'b1; end endgenerate assign led = pwm_out;逻辑说明:generate在综合时展开成 4 份并行的比较逻辑,pwm_cnt只有一份,duty是 4×10 位的寄存器组。参数上,若发现时序报告里pwm_out的路径 slack 为负,说明一级比较逻辑后接了太重的负载(比如直接驱动大电流灯),此时应把驱动电流交给外部的 LED 闪灯驱动芯片或三极管,FPGA 只出逻辑信号。
三件事值得在做实验报告时记下来。第一,计数器位宽按需给,EP4CE6 这类小容量器件上,随手写 32 位最终会体现在资源占用的百分比里。第二,加一条时钟约束让时序分析有依据:
# led_drive.sdc create_clock -name clk -period 20.000 [get_ports {clk}]-period 20.000对应 50MHz,Fmax 报告会告诉你当前逻辑最快能跑多少,低于 50MHz 说明组合逻辑链太长,考虑插一级寄存器。第三,验证亮度和频率这类"看不太准"的参数,不要靠眼睛,SignalTap 抓一段时间窗内的波形计数,或者干脆写个计数器记录led翻转次数,再和理论值对。
本文还有配套的精品资源,点击获取