FPGA入门实战:Verilog流水灯设计与仿真上板全解析

FPGA入门实战:Verilog流水灯设计与仿真上板全解析 简介面向FPGA入门学习者的流水灯Verilog设计包基于Xilinx ISE工具链开发完整呈现LED顺序亮灭的数字逻辑实现过程。包内共95个文件以.v源码、ISE工程配置.xise/.projectmgr、仿真波形.wdb、综合报告与可执行仿真工具为主整体仅1.57MB便于快速获取与反复练习。代码涵盖模块定义、输入输出信号声明、计数器和寄存器构建、有限状态机切换、assign组合逻辑以及时钟与复位处理等必备知识点并附带多个测试激励和仿真脚本帮助读者在ISim中观察波形并验证功能。工程文件结构完整既适合FPGA初学者对照学习也可直接作为数字电路实验或课程设计的参考模板。目前已有3363人浏览学习通过该资源可系统掌握Verilog语法、时序控制与ISE设计流程还能进一步扩展流水灯速度或花式切换。 聊到 FPGA 入门流水灯几乎是绕不开的第一个实战项目。不管是学校课程、培训班还是自己买开发板折腾大部分人第一次认认真真写完、仿真、下载到板子上跑起来的 Verilog 代码八成都是它。你别看流水灯实现起来似乎不复杂但这个项目背后涉及到的时序逻辑概念、计数器设计、分频思想、移位寄存器操作甚至后续的仿真调试流程和上板验证流程全都是 FPGA 开发里天天要用的基本功。把这一个项目吃透比你囫囵吞枣地抄十个例程都有用得多。这篇文章我就结合自己的开发经验把 FPGA 实现流水灯的完整思路、代码设计、仿真方法和上板实操从头到尾捋一遍。你跟着走一遍不仅能跑通流水灯还能把 Verilog 里最核心的时序逻辑写法搞明白为后面的串口通信、状态机、图像处理这些复杂设计打下一个比较扎实的基础。1. 项目概述与整体技术思路1.1 为什么流水灯是 FPGA 入门首选很多初学者上手 FPGA 时容易陷入一个误区一上来就想搞复杂的项目比如图像采集、PCIe 通信、以太网传输结果被各种协议和高深概念劝退。我见过不少同学买了两千多的开发板最后吃灰的比使用的多原因就是起步太难挫败感太强。流水灯这个项目恰好避开了这些坑。它需要的硬件资源极少随便一块入门级开发板都能跑甚至连 FPGA 最小系统板加几个 LED 都够用。从代码量看核心逻辑可能就几十行 Verilog就算是从零开始学语法一天之内也能写完并仿真通过。但麻雀虽小五脏俱全它完整覆盖了一个真实 FPGA 项目的全流程需求分析、模块划分、时钟分频、时序逻辑设计、仿真验证、引脚约束、上板调试。这套流程走一遍你后续做再复杂的项目框架也是这么搭的。另外流水灯的底层逻辑牵扯到计数器Counter和移位寄存器Shift Register这两个东西在数字电路设计里属于“出现频率最高”的基础模块。你现在把流水灯里的计数器写利索了后面做波特率发生器、PWM 输出、定时器中断全都是这套思路的变体。可以说流水灯不是终点而是你踏入时序逻辑世界的第一级台阶。1.2 整体方案设计分频、计数与移位三件套在写代码之前先把整体架构想清楚这是我后来养成的习惯。拿到一个需求先别急着打开编辑器脑子里得有框图——哪怕画在草稿纸上也行。流水灯的整体逻辑其实很简单让 LED 按顺序逐个点亮循环往复形成“流水”的视觉效果。要让人眼能看清流水效果LED 的变化频率不能太快也不能太慢一般控制在 0.5Hz 到 2Hz 比较合适也就是每 0.5 到 2 秒移动一位。但开发板上的时钟通常是 50MHz 或 100MHz这就面临一个矛盾时钟太快怎么降到人眼能感知的频率答案就是分频。分频的本质是用计数器对时钟脉冲进行计数。比如 50MHz 的时钟要用它产生一个 1Hz 的使能信号只需要计数 25000000 次50,000,000 / 2即可因为计数到一半信号翻转一次这样周期正好是 1 秒。这里有个细节很多人容易搞混如果你想要每 1 秒移一位那么计数器计满 50,000,000 个时钟周期即可不需要除以 2因为每计满一次就产生一个 tick 脉冲不是翻转电平用这个 tick 去驱动移位操作。有了 tick 之后流水逻辑就有两条路可走一条是用移位寄存器让一个 1 在寄存器组里循环移动另一条是直接用计数器值译码比如在 0 到 N 之间循环计数计到某个值就点亮对应的 LED。两种写法我在下文都会给出代码你先理解思路分频产生慢节奏的时钟节拍计数或移位实现流水效果顶层模块把这两部分组合起来。2. 开发环境与工具链准备2.1 硬件与软件选型建议做 FPGA 开发没有开发板那就是纸上谈兵。市面上常见的有 Xilinx 的 Artix-7 系列比如 EGO1 开发板、Intel原 Altera的 Cyclone IV 系列以及国产的紫光同创、安路等。EGO1 这个型号在高校里特别常见因为很多课程设计用的就是它板载资源也够用自带 LED、按键、数码管、VGA 接口做入门学习绰绰有余。软件方面Xilinx 的 Vivado 和 Intel 的 Quartus II 是两大主流 IDE。如果你用的是 EGO1 这类 Xilinx 芯片那就装 Vivado如果是 Cyclone 系列就用 Quartus。我这里以 Vivado 为例讲流程但 Quartus 的操作逻辑大差不差核心都是建工程、写代码、仿真、分配引脚、下载烧录。还有一个必须装的工具是仿真软件。Vivado 自带的仿真器xsim其实够用但我个人更习惯用 ModelSim 或者 QuestaSim因为它们的波形查看和调试体验更成熟尤其在工程复杂的时候QuestaSim 的性能优势很明显。不过对于流水灯这个项目用哪个都行你自己顺手就好。2.2 新建工程的三个关键配置项新建工程的时候有几个配置项我建议你留个心眼不然后期会返工。第一是顶层模块名称。Vivado 默认创建的顶层模块名是 project_name_wrapper 或者类似的名字你最好在 Create RTL Project 这一步就直接把它改成 led_flow 这种有意义的名称并且和你后面写的module名保持一致。第二是芯片型号的选择如果你不知道自己的板子具体是哪颗芯片去开发板说明书里翻一下或者直接问卖家选错了后面综合会报错。第三是语言版本Verilog 有 Verilog-2001 和 SystemVerilog 两种流水灯这种简单设计选 Verilog-2001 完全够用别一上来就选 SystemVerilog虽然它兼容 Verilog但在旧工程复用方面可能踩坑。3. Verilog 代码核心实现从语法到逻辑3.1 时钟分频与计数器参数计算是基本功咱先写第一个模块分频器。在 50MHz 的系统时钟下要产生一个 1Hz 的 tick 信号需要在计数器从 0 累加到 49,999,999 后输出一个单时钟周期的高电平脉冲。你可能会问为什么要计数 50,000,000 - 1因为计数器是从 0 开始的50MHz 时钟一秒内有 50,000,000 个上升沿从 0 数到 49,999,999 正好数了 50,000,000 次每次计满归零同时输出一个脉冲。module clock_divider #( parameter CLK_FREQ 50_000_000, // 输入时钟频率 50MHz parameter TICK_T 1 // 期望的节拍周期单位秒 )( input wire clk, input wire rst_n, output reg tick ); // 计算计数器最大值注意位宽要足够 localparam CNT_MAX CLK_FREQ * TICK_T - 1; localparam CNT_WIDTH $clog2(CNT_MAX 1); reg [CNT_WIDTH-1:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt d0; tick 1b0; end else if (cnt CNT_MAX) begin cnt d0; tick 1b1; end else begin cnt cnt 1b1; tick 1b0; end end endmodule这里有几个细节值得你注意。$clog2是 Verilog-2001 里非常好用的系统函数它能自动算出需要多少位二进制数才能表示最大值省得自己手算位宽。我之前见过不少新手直接写reg [25:0] cnt如果换了更高的时钟频率或者更大的分频系数位宽不够就会出现计数器溢出流水灯直接乱跳排查半天才发现是位宽问题。用$clog2一劳永逸。第二个细节是同步复位和异步复位的选择。上面的代码用的是异步复位也就是复位信号 rst_n 不需要经过时钟直接就能让寄存器归零。这种方式在复位瞬间响应快但对复位信号的毛刺比较敏感可能造成时序问题。在简单设计中用异步复位问题不大但在复杂高速设计中更推荐同步复位。流水灯这里两种写法都能跑但你要有这个意识。3.2 流水逻辑实现移位寄存器与译码器两种写法对比有了 tick 信号流水逻辑就有了“节拍”。接下来我给出两种主流写法。写法一移位寄存器方式module led_shift #( parameter LED_WIDTH 8 )( input wire clk, input wire rst_n, input wire tick, output reg [LED_WIDTH-1:0] led ); reg [LED_WIDTH-1:0] led_r; always (posedge clk or negedge rst_n) begin if (!rst_n) led_r {{LED_WIDTH-1{1b0}}, 1b1}; // 初始只有最右边 LED 亮 else if (tick) begin // 循环左移最高位移到最低位 led_r {led_r[LED_WIDTH-2:0], led_r[LED_WIDTH-1]}; end end assign led led_r; endmodule这种写法的核心思想是用一个寄存器组保存当前 LED 状态每次 tick 到来时把所有位向左移动一位同时把最高位补到最低位形成环形移位。你想象一下一个 1 在 8 位寄存器里左移超出最高位的 1 又从最低位钻回来循环往复就是流水灯的效果。代码里有一行可能让新手困惑{{LED_WIDTH-1{1b0}}, 1b1}。这是 Verilog 的拼接语法把 LED_WIDTH-1 个 0 和一个 1 拼在一起组成一个 LED_WIDTH 位宽的向量效果就是复位后让最低位置 1。不用怕复杂这行的意思就是“LED 状态初始化为最右边那个灯亮”。写法二计数器译码方式module led_counter #( parameter LED_WIDTH 8, parameter STEP_MAX 8 )( input wire clk, input wire rst_n, input wire tick, output reg [LED_WIDTH-1:0] led ); reg [$clog2(STEP_MAX)-1:0] cnt_step; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt_step d0; led {{LED_WIDTH-1{1b0}}, 1b1}; end else if (tick) begin if (cnt_step STEP_MAX - 1) cnt_step d0; else cnt_step cnt_step 1b1; end end always (*) begin led {{LED_WIDTH-1{1b0}}, 1b1} cnt_step; end endmodule第二种方式的思路是直接用计数器值做译码计数值是几就让第几位的 LED 亮。代码中的移位操作在这里其实是用组合逻辑实现的输出译码跟第一种用寄存器时序移位的方式在硬件结构上有区别第一种是纯时序的移位寄存器第二种是“计数器 组合逻辑译码”的结构。两者实现的视觉效果一样但如果你在后面要扩展其它功能比如让 LED 亮度渐变第一种可能会更灵活一些。3.3 顶层模块整合模块化设计的第一次真正实践有了分频器和流水逻辑最后一步是把它们像搭积木一样拼接到顶层模块里。module led_flow_top #( parameter CLK_FREQ 50_000_000, parameter TICK_T 1, parameter LED_WIDTH 8 )( input wire clk, input wire rst_n, output wire [LED_WIDTH-1:0] led ); wire tick; clock_divider #( .CLK_FREQ (CLK_FREQ), .TICK_T (TICK_T) ) u_clk_div ( .clk (clk), .rst_n (rst_n), .tick (tick) ); led_shift #( .LED_WIDTH (LED_WIDTH) ) u_led_shift ( .clk (clk), .rst_n (rst_n), .tick (tick), .led (led) ); endmodule顶层模块做的事情并不多就是例化两个子模块并用 wire 信号把它们连接起来。其中#(...)是参数传递的语法用来把顶层参数传给子模块。这种模块化设计的思想我现在做大型项目时依然在用。把大功能拆分成小模块每个模块职责单一再用信号把它们连起来调试的时候可以单独仿真每个子模块定位问题会快得多。在 Vivado 里写完这几个模块之后有一个小技巧综合前先用 Analysis 阶段的 RTL Elaboration 检查一下代码语法和模块例化关系它会画出一张 RTL 原理图你能清晰地看到clock_divider和led_shift两个模块就像两块积木一样连接在一起。我第一次看到这张图时对 FPGA 开发的抽象理解瞬间提升了一个台阶——原来我写的代码真的在“画电路”而不是在执行程序。4. 仿真验证与上板实操4.1 ModelSim 仿真别让 5000 万计数拖垮你的仿真速度写完代码别急着上板。硬件上板之前先仿真这是 FPGA 开发的不成文规矩。你想想板子上的器件就那几个一旦烧进去有问题查起来又费时又费劲。仿真可以在电脑上把功能验证好了再把代码烧到板子上大概率就不会出大问题。仿真第一步是写 testbenchTB它的作用是给被测模块DUT施加激励信号比如产生时钟、产生复位、观察输出波形。流水灯的测试比较特殊如果你仿真真实的分频参数 1 秒一个 tick那仿真 8 个 LED 流完一整圈需要 8 秒而在 ModelSim 里仿真 1 秒的数字电路耗时可能很长这会让你等到怀疑人生。最有效的解决办法是参数重载。testbench 里例化 DUT 时把分频参数改小比如把CLK_FREQ设为 10意思是每 10 个周期产生一个 tick这样整个流水过程几微秒就仿真完了你可以快速验证功能正确性。等仿真通过了再上板时沿用默认的 50MHz 参数板子上的效果就会是正常的秒级流水。timescale 1ns / 1ps module tb_led_flow; reg clk; reg rst_n; wire [7:0] led; // 产生 50MHz 时钟 initial clk 1b0; always #10 clk ~clk; // 复位激励 initial begin rst_n 1b0; #100; rst_n 1b1; #5000; $stop; end led_flow_top #( .CLK_FREQ (10), // 便于仿真实际可改为 50_000_000 .LED_WIDTH (8) ) uut ( .clk (clk), .rst_n (rst_n), .led (led) ); endmodule看到这里你可能会问CLK_FREQ 10意味着多久移一位其实这里的参数含义是“多少时钟周期产生一个 tick”不是具体的频率值。因为 testbench 里的时钟周期由#10决定10ns所以当CLK_FREQ 10时每 10 个时钟周期产生一个 tick对应 100ns。这个时间足够你在波形图里看清 LED 的移动过程了。4.2 上板调试引脚约束和下载的完整流程仿真通过之后就可以上板了。上板之前有一件必不可少的事分配管脚约束Pin Constraints。Vivado 默认不知道你的clk信号应该连接到板子上哪个物理引脚你需要告诉它。常见做法是直接编辑 XDC 约束文件或者用 Vivado 的 Pin Planning 图形界面。以 EGO1 开发板为例板载 50MHz 时钟源可能连接到 FPGA 的 E1 引脚具体以你的板卡原理图为准8 个 LED 可能连接在 P1~P8 之类的引脚上。新建 XDC 文件后格式大致如下set_property PACKAGE_PIN E1 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] set_property PACKAGE_PIN P1 [get_ports {led[0]}] set_property PACKAGE_PIN P2 [get_ports {led[1]}] ...写约束文件时最容易犯的错误是引脚号和数组对应错位比如 led[0] 和 led[7] 接反了后果是流水方向反着跑。不用担心这在板子上肉眼很明显改一下约束重新烧录就好属于小事一桩。但引脚号脱离开发板手册瞎填的话轻则功能异常重则可能因为 FPGA 引脚同时被多个外设占用甚至直接使用时序冲突所以我建议你第一步就翻开发板自带原理图一个引脚一个引脚地核对清楚。下载环节同样简单连接好 JTAG 下载器在 Vivado 里点击 Open Hardware Manager然后 Program Device选择生成的 bitstream 文件点下载看到进度条走完板子上的灯就开始跑了。如果没跑起来优先级最高的排查方向是复位按键和时钟引脚有没有接对这两个出问题的概率能占一半以上。5. 常见问题与排查技巧实录5.1 仿真与上板对不上大概率是复位或约束问题流水灯虽然简单但做实验时遇到的问题未必都简单。我自己带过不少同学做这个项目归纳下来最常见的问题集中在三类。第一类是仿真波形正常上板灯却不亮。这个问题优先级最高的怀疑对象是复位引脚。如果你的开发板复位按键按下时输出电平是 1上拉而代码里用的是低电平复位rst_n那板子一上电就等于一直在复位LED 永远不亮。解决办法是看一下板子的原理图确认复位的有效电平必要时把代码改成if (rst)或者 XDC 约束里取反。第二类是仿真一切正常板子上 LED 全亮或者全灭。通常问题出在 IOSTANDARD 配置上如果你的 FPGA 是 3.3V bank但 LED 通过推挽输出连接LVCMOS33 是最常用的标准如果出现了输出短路之类的问题就要检查引脚配置是否和其他模块冲突。还有一种可能是你把引脚约束到了一个没有连接到板子上的“悬空”引脚看起来自然不亮。第三类是流水速度太快或者太慢。速度太快说明分频的周期设置太短太慢说明计数溢出后 tick 丢失或时钟频率理解偏差。你只要验算一遍CNT_MAX的数值和实际分频后的频率期望 tick 频率 时钟频率 / (CNT_MAX 1)。代入数字一算便知。5.2 经验心得从流水灯开始建立调试验证思维我自己的体验是学 FPGA 和学软件有个非常大的不同软件崩了可以直接打印日志硬件出了问题只能看波形、量电平、猜原因。所以从流水灯开始你就应该有意识地建立一套“分层定位”的调试思路先看仿真波形对不对再看综合报告里的警告有没有异常接着看 RTL 视图的模块连线是否符合预期最后才上板用逻辑分析仪或者板载 LED去观察内部信号。另外我特别想提一点尽量别用“改一段代码、全量综合、重新上板”这种蛮力调试法。流水灯还好等你做大项目一次综合加布局布线动辄几十分钟你再循环往复一天的时间就没了。更好的做法是每一个小改动都先在 testbench 里快速验证仿真过了再上板。仿真跑得慢没关系用参数重载把仿真时间缩短仿真复杂也没关系把你的代码按功能拆成小块分别验证。这套方法论我从流水灯开始用到现在一直受益。6. 后续扩展流水灯之外还能玩出什么花样流水灯跑通了说明你已经掌握了 FPGA 开发的基本功。但这只是开始接下来有太多好玩的功能可以在这个基础项目上做扩展。我简单说几个方向你按兴趣挑一个往下走就行。第一个方向加入按键控制。比如按下按键流水暂停再按一下恢复或者按键切换流水方向长按调整流水速度。这个项目能让你学会按键消抖——按键的机械弹跳会导致一次按下被识别成很多次后续做任何交互功能都要面对这个问题。第二个方向把 LED 亮度做出来。用 PWM 技术让 LED 从暗到亮渐变再配合流水移动效果会比单纯亮灭炫酷得多。PWM 本质上还是计数器那一套你在流水灯里已经用过了。第三个方向换一种更快的通信接口。如果你手头有第二个 FPGA 开发板或者单片机可以尝试用 UART 串口让两个设备协作比如上位机发一个指令控制流水灯模式。这就涉及到跨时钟域、同步握手这些更深入的概念是 FPGA 工程化的核心内容之一。第四个方向增加显示输出。把流水灯的状态同时显示在板载数码管或 LCD 屏幕上。这一步能让你初步接触显示驱动的解码逻辑后面做 VGA 或 HDMI 也不至于两眼一抹黑。总的来说流水灯项目像是你走进 FPGA 世界的第一扇门。别嫌它简单把背后的计数器、分频、移位、仿真、约束这一整套逻辑都吃透你后面的路会顺畅非常多。我始终觉得数字电路设计拼的不是会多少个写法而是遇到问题时能不能快速定位到是逻辑错误、时序错误还是约束错误。这个能力恰恰就是从一个简简单单的流水灯开始培养起来的。本文还有配套的精品资源点击获取