ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA入门:用Verilog实现流水灯及Quartus II工程流程

FPGA入门:用Verilog实现流水灯及Quartus II工程流程 简介这是一份基于 Quartus II 与 Verilog 的流水灯设计完整工程包适合初学 FPGA 或数字逻辑的电子类学生、工程师作为入门练手项目。资源围绕模块定义、有限状态机、时钟分频、移位寄存器等核心知识点展开从代码编写、综合仿真到硬件下载均有涉及可帮助读者理解 LED 流水灯从逻辑描述到实际落地的完整流程。压缩包共 132 个文件、约 1.27MB主体为 Quartus II 工程文件qpf、qsf、sof、qws与 ModelSim 仿真相关文件do、v、wlf另有大量 cdb、hdb、rpt 等综合与适配过程生成的过程文件便于对照工程目录了解各类型文件的用途。资源目前已有 568 人学习适合想要快速体验 Verilog 编程、状态机设计及 Quartus 开发流程的入门者动手实践。通过解压并打开工程可直接查看源码、仿真脚本与编译报告节省自行搭建环境与排查报错的时间是一份实用性较强的 FPGA 教学示例。1. 流水灯FPGA 入门的第一道门槛流水灯就是让一排 LED 依次点亮、熄灭形成像水流动一样的效果。在 Quartus II 里用 Verilog 写一个流水灯看起来是“点灯”实际上是把 FPGA 开发的完整链路走了一遍时钟处理、计数器设计、组合逻辑与时序逻辑的区分、引脚约束、编译下载、板级验证。很多工程师学 FPGA 的第一个工程就是这个 led.zip它几乎是所有开发板厂商默认附带的例程。这个标题适合两类人一是刚接触 Verilog 的初学者想找一份能直接跑通的代码理解计数器怎么控制 LED 亮灭节奏二是已经在用 Quartus II 做项目、但想回头把基础模块写得更规范的人。需要说明的是流水灯的实现方式不止一种——移位寄存器、计数器加译码、状态机——选哪种取决于你之后要做什么。如果你只是入门计数器加移位寄存器是最容易调试的写法如果你后面要做通信协议那从状态机角度去写流水灯反而更有练习价值。2. Verilog 流水灯的核心逻辑从分频到移位2.1 为什么不能直接让时钟驱动 LED开发板上通常是 50MHz 有源晶振也就是一秒钟翻转五千万次。直接把时钟信号给 LED人眼根本捕捉不到亮灭变化看到的只会是常亮或微暗。所以第一步一定是分频。分频的实质是计数。50MHz 时钟计数到 25_000_000刚好是 0.5 秒。每计满一次就翻转一个标志信号这个标志信号的频率就是 1Hz用来驱动 LED 变化节奏。常见做法是写一个参数化的计数器模块module clk_div #( parameter DIV_CNT 25_000_000 )( input wire clk, input wire rst_n, output reg clk_1hz ); reg [31:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_1hz 0; end else if (cnt DIV_CNT - 1) begin cnt 0; clk_1hz ~clk_1hz; end else begin cnt cnt 1; end end endmodule这个模块里DIV_CNT是参数为什么定义成 25_000_000 而不是 25_000_000 - 1因为计数器的比较条件是cnt DIV_CNT - 1。从 0 计到 24_999_999 一共是 25_000_000 个时钟周期占满 0.5 秒。如果写成cnt DIV_CNT那 0 到 25_000_000 是 25_000_001 个周期频率会差一点点肉眼虽然看不出来但作为习惯不值得养成。clk_1hz的翻转频率是 1Hz周期 1 秒占空比 50%这个信号再给到下一级做流水的节拍。这里有一个很容易理解偏差的点分频得到的时钟不推荐直接拿来驱动其他模块的时钟端——在 FPGA 里全局时钟网络是专门给 PLL 输出的除非用altpll或clk_wiz之类的 IP 做时钟管理否则直接用内部逻辑产生的信号当 clock 容易引起时序收敛问题。更稳的做法是把分频信号当成普通使能脉冲来用。2.2 用移位寄存器实现流水流水效果最常见、也最好理解的写法是移位寄存器。初始值让最低位为 1其余为 0每个节拍左移一位最高位移出后就回到最低位。用 Verilog 写就是module led_shift #( parameter WIDTH 8 )( input wire clk, input wire rst_n, input wire en, output reg [WIDTH-1:0] led ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin led {{(WIDTH-1){1b0}}, 1b1}; end else if (en) begin led led[WIDTH-2:0] | (led[WIDTH-1] ? {1b0, {(WIDTH-1){1b0}}} : 0); end end endmodule上面这写法有点绕因为把“最高位回绕”和“左移”挤在了一行里。工程上我更推荐写得更直白一些always (posedge clk or negedge rst_n) begin if (!rst_n) begin led {{(WIDTH-1){1b0}}, 1b1}; end else if (en 1b1) begin if (led[WIDTH-1] 1b1) begin led 1b1; end else begin led led 1; end end end逐行解释复位时 LED 只有最低位亮。en是节拍使能每来一个脉冲就移一次位。当最高位点亮时下一次直接回到最低位否则左移一位。这段代码逻辑上没有开头那段紧凑但可读性明显更好。流水灯这种简单模块可维护性比节省几行代码重要得多。2.3 另一种思路计数器直接译码移位寄存器写法适合 LED 数量不太多的情况。如果 LED 数量达到 16 或 32 个用led led 1这种写法仍然没问题。但有一种场景更推荐计数器加译码你需要任意控制某个灯亮比如做成呼吸灯、追逐灯、来回扫的效果。计数器加译码的思路是把流水位置定义成一个计数值然后只让对应位输出 1。比如 8 个 LED一个 3 位计数器从 0 数到 7然后用case或优先级逻辑把它转成独热码module led_decoder #( parameter WIDTH 8 )( input wire [3:0] pos, output reg [WIDTH-1:0] led ); always (*) begin case (pos) 4d0: led 8b0000_0001; 4d1: led 8b0000_0010; // 中间省略 4d7: led 8b1000_0000; default: led 8b0000_0000; endcase end endmodule这种写法的优点是位置和灯的关系完全可控缺点是需要多写一个译码模块。对于 8 个灯以内的流水灯两种方式差别不大。但如果你打算把代码改成“向两边扩散”或“先左后右再折返”那计数器版本优势就出来了——你只需要改译码逻辑不用动时序部分。3. Quartus II 下创建流水灯工程的完整流程3.1 新建工程和添加文件打开 Quartus II从File - New Project Wizard进入新建工程向导。有几个地方容易踩坑逐一说明。第一工程路径中不能出现中文和空格。很多人把工程直接放在桌面上的“新建文件夹”里编译时报路径错误其实就是这个原因。建议目录结构按项目名单独建一个文件夹比如D:/fpga_proj/led_flow。第二Device 选择要和你的开发板型号一致。选错芯片型号后续编译可能通过但引脚约束时找不到对应的引脚名。比如 Cyclone IV 系列EP4CE6F17C8 和 EP4CE10F17C8 引脚排列相同但资源量不同如果型号选小了综合面积大的设计时会报资源不足。第三在 Add Files 页面可以直接把之前写好的.v文件添加进来也可以先跳过等文件写好后再通过Project - Add/Remove Files in Project补充。建议先用文本编辑器写好 Verilog 文件再添加而不是在 Quartus 里新建文件——Quartus 自带的编辑器没有代码补全写起来效率低。工程建好后把上面两段代码分别保存为clk_div.v和led_shift.v添加进工程。顶层文件这样写module led_top ( input wire clk, input wire rst_n, output wire [7:0] led ); wire en_1hz; clk_div #( .DIV_CNT(25_000_000) ) u_clk_div ( .clk(clk), .rst_n(rst_n), .clk_1hz(en_1hz) ); led_shift #( .WIDTH(8) ) u_led_shift ( .clk(clk), .rst_n(rst_n), .en(en_1hz), .led(led) ); endmodule顶层模块把分频器的输出当作 led_shift 的en输入而不是当时钟用。这个信号每个周期只高电平一个 clk 周期相当于使能脉冲。这样做的好处是 led_shift 内部仍然用主时钟clk驱动时序上不会有内部生成时钟带来的额外不确定性。3.2 引脚分配前的准备编译前先做引脚分配。在 Quartus II 里打开Assignments - Pin Planner左侧列出的是你在顶层模块声明的所有端口右侧是芯片的物理引脚。不同厂商开发板的 LED 引脚差异很大必须对照原理图来填。这里列出引脚分配的一些基本原则先找到时钟引脚。绝大多数开发板把 50MHz 晶振接到 FPGA 的专用时钟引脚上如 Cyclone IV 系列常见的是 PIN_E1 或 PIN_M9具体看板子。复位按键有的接高电平有效有的接低电平有效要特别看一下你的模块里rst_n是低有效如果板子是按键按下为高那按键没按时是低电平你的模块会一直处于复位态。引脚分配完成后保存.qsf文件Quartus II 会自动写入引脚约束。如果你想手动编辑.qsf也可以在工程目录下直接改。下面是典型的约束内容set_location_assignment PIN_E1 -to clk set_location_assignment PIN_M1 -to rst_n set_location_assignment PIN_A1 -to led[0] set_location_assignment PIN_B1 -to led[1]这些语句的意思分别把clk、rst_n、led[0]和led[1]绑定到芯片的实际引脚上。手动编辑.qsf时要注意分配引脚之后必须重新编译否则新约束不会生效。3.3 编译与下载编译直接在Processing - Start Compilation点一下或者用快捷键CtrlL。编译过程分四个阶段Analysis Synthesis把 Verilog 代码映射成基本的逻辑单元LE / ALM检查语法错误和模块连接是否有未定义端口。Fitter布局布线把逻辑单元放到芯片的具体位置并连接布线资源。这一步如果时序约束不合理会报时序违规。Assembler生成最终配置文件.sof。EDA Netlist Writer生成给仿真工具使用的网表文件这一步只有你配置了第三方工具时才执行。下载程序用Tools - Programmer或者双击编译报告里Assembler - Device Programming File Setup下方的.sof文件打开 Programmer 后先点Hardware Setup选择 USB-Blaster然后Start。烧写完如果 LED 没有按预期流水先把复位键按几次确认复位极性是否正确。一个需要注意的点是.sof文件下载到芯片后断电就丢了。要生成掉电不丢失的.jic文件需要通过File - Convert Programming Files把.sof转换成 JTAG 间接配置文件再烧到配置芯片里。这部分对于只是拿流水灯练手的初学者来说可以先跳过但如果在做实际项目这一关早晚要过。4. 仿真验证与常见报错排查4.1 用 Modelsim 跑一遍流水灯波形Quartus II 通常自带 Modelsim-Altera 版本或者在 Tools 菜单里可以调起。要跑仿真先得保证工程里设置了仿真工具。在Assignments - Settings - EDA Tool Settings - Simulation里把仿真工具选为 Modelsim-Altera语言选 Verilog HDL然后写一个测试文件。测试文件是仿真中必须自己写的激励源。和真实硬件不同仿真里时钟和复位都需要你手动生成下面是一个可直接用的 testbenchtimescale 1ns / 1ps module led_top_tb; reg clk; reg rst_n; wire [7:0] led; // 时钟周期 20ns 对应 50MHz always #10 clk ~clk; initial begin clk 0; rst_n 0; // 仿真时间 2 微秒以后释放复位 #2000 rst_n 1; // 观察足够长时间 #500000 $finish; end led_top u_led_top ( .clk(clk), .rst_n(rst_n), .led(led) ); endmodule把这个文件也加入工程然后在 Quartus 里点击Tools - Run Simulation Tool - RTL SimulationModelsim 会自动启动并加载设计。如果一切正常你能在波形窗口看到led每隔一段固定时间变化一次。仿真是排错最快的途径。你在板子上看不到 LED 流水可能是硬件问题也可能是逻辑问题但通过仿真可以直接观察en_1hz有没有翻转、led有没有移位从而快速定位到哪个模块出了问题。我一般先仿真分频器的cnt是否按预期计数再看en_1hz波形是否整齐最后看led移位。这三层波形逐层确认基本没有查不出来的逻辑问题。4.2 仿真 license 报错的三种解法标题热词里出现过一段报错17.1 error: failure to obtain a verilog simulation license这是 Quartus II 17.1 版本在首次调用 Modelsim 时非常典型的错误。原因基本是环境变量没有指向 license 文件或者 Modelsim 版本不是一个可独立运行的完整版。遇到这个报错常见处理顺序是先检查系统环境变量LM_LICENSE_FILE是否设置到了合法的 license 路径如果是破解版或学校机房版本再确认 Modelsim 是从 Quartus 内部启动的而不是单独打开外部的 Modelsim最后看 Quartus 的 EDA 仿真工具设置里有没有选错版本位数32bit / 64bit 必须和你安装的 Modelsim 一致。如果你不依赖 Modelsim也可以去用 Vivado 系或开源工具 Icarus Verilog做仿真——它们的仿真流程和 Modelsim 大同小异都是编译源文件、跑 testbench、看波形。关键是逻辑设计方法没变化工具只是一个载体。4.3 波形出来了但流水方向不对检查位序仿真波形中如果发现 LED 是从高位往低位流动而板子上的灯是反向的不用改逻辑先检查引脚约束。看板子原理图里 LED 编号和 FPGA 引脚的关系比如 LED1 可能对应led[7]而不是led[0]。这种情况下改引脚映射比反过来写代码更符合硬件设计习惯——代码里保证位序有明确语义硬件上的对应关系交给约束文件处理。如果你的板子 LED 是低电平点亮也就是引脚输出 0 时灯亮那模块里的初始值和移位方向也需要反转否则会看到只有一个灯灭其他灯全亮。最直观的办法是在顶层模块里把输出取反assign led_out ~led;这种方式比修改底层led_shift模块好得多因为底层模块保持独立的逻辑语义输出 1 表示点亮硬件适配问题在上层解决。5. 三种流水效果的 Verilog 写法对比实现方式核心思路代码量扩展难度适用场景移位寄存器led led 1最少只能单向和回绕入门简单示范计数器译码计数值映射独热码中等任意自定义花样复杂花式流水状态机每个状态定义一组 LED 输出较多最容易扩展协议学习状态机同时控制多个外设如果你要做的流水灯只是“点缀”功能它在整个系统里占用的逻辑不会超过 1%。这时用最简单的方式即可。但如果你打算用流水灯做状态指示——比如系统启动、自检、运行中、报错——那状态机方式更合适。每种状态对应一组 LED 亮灭模式状态切换由外部信号触发这其实就是嵌入式开发里常见的“状态指示器”设计。5.1 用状态机写一个可扩展的流水控制状态机流水灯的核心是把不同段的亮灭用localparam定义抽象状态而不是直接操作比特位。下面是一个四状态的简化例子localparam IDLE 2d0; localparam RUN 2d1; localparam ERR 2d2; reg [1:0] state; reg [1:0] next_state; always (posedge clk or negedge rst_n) begin if (!rst_n) state IDLE; else state next_state; end always (*) begin case (state) IDLE: next_state start ? RUN : IDLE; RUN: next_state error ? ERR : RUN; ERR: next_state start ? RUN : ERR; default: next_state IDLE; endcase end always (*) begin case (state) IDLE: led 8b0000_0001; RUN: led shift_pattern; ERR: led 8b1010_1010; default: led 8b0000_0000; endcase end这里不做完整的状态机代码重点是设计思路组合逻辑里写状态跳转条件时序逻辑里寄存状态输出逻辑里状态对应的 LED 模式。拆成三个always是三段式状态机的标准写法对初学者来说可能比一段式复杂但对后续工程维护非常有帮助。状态机和移位寄存器在综合后消耗的资源差不了几个 LE真正拉开差距的是写代码的可扩展性。5.2 控制流水速度参数改哪里需要改变流水速度时很多人的第一反应是改DIV_CNT。这确实是最直接的方案但有个不便改参数必须重新编译整个工程而 FPGA 编译一次往往要几分钟到十几分钟。如果只是想快速试几种速度你有两个选择。选择一把DIV_CNT设成多个不同的值然后用板上的拨码开关或按键选择。这种方法适合在演示时需要现场切换速度的场景。选择二把分频模块改成可配置的——用输入信号在运行时动态改变计数值。但动态改计数值要注意一个细节计数到一半时改变量可能出现短脉冲或者计数跳过具体表现是流水节奏突然快一下或慢一下。如果项目对速度切换有硬性时序要求建议设计成“先停计数再改参数再恢复”的流程。对于流水灯这个级别我一般做法是直接在代码里用parameter要换速度就改一个数。这也是为什么我在第 2 章特意把DIV_CNT写成参数而不是硬编码——工程习惯就是从这些小地方养成的。本文还有配套的精品资源点击获取
返回列表