资讯详情

资讯详情

FPGA入门:流水灯Verilog代码中的分频、移位与时序设计

简介面向FPGA入门学习者的流水灯设计工程包基于Verilog HDL在Xilinx ISE环境中实现多路LED的顺序点亮与循环流动适合数字电路实验、课程设计以及FPGA开发流程练习。工程核心包含完整的模块定义、输入输出/时钟复位信号、计数器与有限状态机、assign组合逻辑等关键代码段源码文件包括water_lights.v、clk.v、light_test.v并配有ISE工程文件xise/prj、wdb仿真波形、exe仿真程序、log日志与综合报告便于直接打开工程查看代码、运行仿真理解LED状态切换的时序控制。压缩包共95个文件、约1.57MB除v源文件外还含obj、c、exe、log、didat等编译/仿真中间产物可对照ISE工程目录结构梳理设计与验证流程。已有3369人学习下载除基础闪烁与移位效果外读者可参考工程中的模块划分和测试代码进一步扩展速度控制、方向切换或灯效模式是入门数字系统设计与FPGA工具链的实用资料。1. 流水灯是FPGA入门最好的“第一块跳板”它到底在练什么初次接触FPGA实现流水灯Verilog代码的人往往以为这就是“让LED轮流亮”的小儿科。真上手才发现这个看起来简单的工程同时压着三个基本功怎么把时钟变成可控的慢信号分频、怎么用寄存器产生“移动”的效果移位、怎么让逻辑在时序上不打架时序约束与复位。可以说流水灯不是“点灯”而是你第一次用硬件思维去思考问题——软件里写个for循环就完事硬件里每一拍都在真实地搬运比特。这篇文章就是按我自己带人时的路径来的先讲清楚流水灯在FPGA里到底是怎么“动”起来的再给一份可复现的Verilog代码然后讲仿真怎么验、上板怎么查最后把最容易翻车的几个点逐个拆开。适合刚入手某款入门开发板、想把点灯玩明白的初学者也适合想回顾时序基础、准备自己写IP核的熟手——流水灯代码虽小但“时钟怎么处理、复位怎么给、移位怎么写”这些决定会影响你后面写UART、写SPI、写状态机的一整套习惯。2. 流水灯背后的硬件逻辑分频、计数器与移位的关系2.1 为什么不能直接拿50MHz时钟去驱动LED多数入门开发板上焊着的晶振是50MHz也就是一秒钟电平翻转五千万次。LED肉眼看得出“亮灭变化”需要大约10Hz以上的切换频率也就是每100毫秒左右变化一次而要看到“流水”的拖影效果通常需要每200毫秒到500毫秒移动一位。如果直接拿50MHz时钟去驱动LED那LED只会以肉眼无法感知的速度狂闪看起来就是常亮。所以第一步必须是“分频”。但要澄清一个常见误解FPGA里的分频不一定非要例化PLL锁相环或MMCM对于流水灯这种对频率精度毫无要求的场景最朴素的方式是用计数器对时钟进行计数然后翻转一个标志信号。这个做法虽然会产生一定的时钟偏斜但它足够简单、可读、可移植也最适合初学者理解“时间是怎么在硬件里度量的”。计数器的本质是一个寄存器在每个时钟上升沿做加一操作当计数值达到设定上限时归零并同时输出一个脉冲或翻转一个使能信号。这个使能信号就是“慢时钟”的替代品——我们不会真的去生成一个慢时钟那样会引入跨时钟域问题而是用快时钟采样一个“使能脉冲”在脉冲有效的那一拍去做移位操作。2.2 移位寄存器LED“流动”的本质是数据在移动流水灯效果不是靠“让某个LED熄灭、另一个点亮”这种组合逻辑实现的——那会写成一大堆case分支既不优雅也难扩展。最常见的做法是用一个移位寄存器初始化值为8b0000_0001每个移位使能脉冲到来时左移一位最低位补零。当最高位的1被移出去之后整个寄存器变为0此时需要检测到这个状态并重新加载初始值形成循环。这个“循环检测”有两种常见写法。一种是组合逻辑判断当移位寄存器等于0时下一个周期载入初始值另一种是计数器配合预先算好移位次数比如8位LED移8次后回到初始状态。前一种依赖译码逻辑后一种依赖计数状态。我一般推荐第二种因为它的时序更直观而且在后续扩展“双向流水”“间隔点亮”时只需要改计数条件不用动移位逻辑本身。这里还有一个新人容易懵的点移位寄存器用的是非阻塞赋值还是阻塞赋值在时序逻辑里必须用非阻塞赋值。原因在于阻塞赋值会在同一时间片内立即更新值导致移位链中途出现“穿透”现象——第一个触发器刚更新第二个触发器在同一时刻读到的已经是更新后的值流水效果就变成了瞬间全亮全灭。非阻塞赋值则是所有触发器在时钟沿采样旧值、在时钟沿之后才一起更新这样才是真正的“每个周期移一位”。2.3 按键复位与初始值异步复位为什么比同步复位省心流水灯代码里一般会有一个复位信号。常见写法是异步复位复位信号低电平时所有寄存器立即清零或载入初始值。异步复位的好处是不需要等待时钟沿上电后只要复位引脚被拉低系统立刻进入已知状态坏处是复位释放时如果刚好撞上时钟沿可能出现亚稳态。对初学者而言我建议直接使用异步复位、同步释放的标准写法——即复位信号先经过两级触发器打拍再用打拍后的信号去复位逻辑。这样既保留了异步复位的“即时性”又规避了释放时的时序风险。对于流水灯这种低速设计不这么做大概率也能跑但这个习惯到了写复杂状态机时就会救你一命。如果把初始值的加载也放到复位里做那复位之后LED的状态就是确定的第一个LED亮其余灭。这对接下来的波形仿真非常有利因为你不需要猜测上电后的初值。3. 手写一份可复现的流水灯Verilog代码分频、移位与按键控制3.1 模块划分与端口设计我一般会把流水灯拆成两个模块一个是led_flow顶层负责例化分频和移位一个是clk_div分频模块。也有同事喜欢把分频写进顶层里代码更短但我不推荐——分开写的好处是后面如果想把流水速度调成“按按键改变”只需要替换或修改分频模块的接口不需要动移位逻辑。端口定义如下端口名方向位宽功能说明clkinput150MHz系统时钟rst_ninput1异步复位低有效ledoutput[7:0]8位LED输出高电平点亮key_ctrlinput1可选按键切换流水方向key_ctrl这个端口不是必需的我建议先不用按键跑通基础流水再把这个端口加上作为进阶练习。下面先给基础版完整代码。3.2 基础版计数器分频 循环移位module led_flow ( input wire clk, // 50MHz 系统时钟 input wire rst_n, // 异步复位低有效 output reg [7:0] led // 8位LED输出 ); // 分频参数50MHz / 500_000 100Hz parameter CNT_MAX 500_000 - 1; reg [18:0] cnt; reg shift_en; // 计数器每到CNT_MAX产生一个shift_en脉冲 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; shift_en 1b0; end else if (cnt CNT_MAX) begin cnt 0; shift_en 1b1; end else begin cnt cnt 1b1; shift_en 1b0; end end // 移位寄存器shift_en有效时左移一位 always (posedge clk or negedge rst_n) begin if (!rst_n) begin led 8b0000_0001; end else if (shift_en) begin if (led 8b0000_0000) begin led 8b0000_0001; end else begin led {led[6:0], 1b0}; end end end endmodule这段代码对应的硬件结构是一个19位计数器2^19524288刚好容得下500000、一个1位使能寄存器、一个8位移位寄存器。CNT_MAX 500_000 - 1里的减一是因为计数器从0开始计数计到499999时正好是500000个周期对应500000/50MHz 10毫秒。如果你想要200毫秒换一位就把这个参数改成10_000_000 - 1。判断led 8b0000_0000是在“1被移出去之后”的下一拍重新载入初始值。注意这里的判断用的是组合逻辑条件但它出现在时序逻辑的always块里不会产生额外的组合逻辑路径延迟问题——因为条件判断的结果只在时钟沿被采样不参与同一时刻的移位链传播。上板时如果LED是低电平点亮只需把led 的赋值取反或者在顶层模块里用assign led_out ~led;转换一下。3.3 进阶版按键切换流水方向给基础版加上方向控制只需要新增一个按键输入和一个判断条件module led_flow_dir ( input wire clk, input wire rst_n, input wire key_ctrl, // 按键默认左移按下右移 output reg [7:0] led ); parameter CNT_MAX 500_000 - 1; reg [18:0] cnt; reg shift_en; // 按键异步打拍消除抖动影响 reg [1:0] key_r; always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_r 2b11; // 按键按下为低电平默认高 end else begin key_r {key_r[0], key_ctrl}; end end wire key_press ~key_r[1]; // 检测到按下低电平有效 always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; shift_en 1b0; end else if (cnt CNT_MAX) begin cnt 0; shift_en 1b1; end else begin cnt cnt 1b1; shift_en 1b0; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin led 8b0000_0001; end else if (shift_en) begin if (led 8b1000_0000 !key_press) begin led 8b0000_0001; // 左移到最后一位后重新开始 end else if (led 8b0000_0001 key_press) begin led 8b1000_0000; // 右移到最后一位后重新开始 end else if (!key_press) begin led {led[6:0], 1b0}; // 左移 end else begin led {1b0, led[7:1]}; // 右移 end end end endmodule这里key_r是一个简单的两级同步器用来消除按键的异步抖动和亚稳态风险。严格来说真实的机械按键抖动时间在几十毫秒量级需要用计数器来做消抖但作为方向切换演示两级同步再加“只在shift_en时采样按键”已经足够。参数CNT_MAX 500_000意味着每10毫秒采样一次按键方向。即使按键在某个采样点处于抖动中间态顶多导致一次无效的方向切换不会造成系统崩溃。如果你希望更稳可以把采样周期加大到50毫秒或者加一个状态机记录按键的“按下前状态”只在状态跳变时切换方向。3.4 参数怎么调从流水速度到LED位宽两个最常调整的参数是流水速度和LED数量。速度由CNT_MAX控制它和最终流水周期每一位点亮的时间的关系是流水周期 CNT_MAX / 系统时钟频率。比如50MHz下CNT_MAX5_000_000时周期100毫秒CNT_MAX25_000_000时周期500毫秒肉眼能看到明显的“一步一顿”效果。LED位宽的扩展方法有两种。一种是直接改端口位宽和初始值比如16位LED就改成led 16b0000_0000_0000_0001判断条件改成led 16b0000_0000_0000_0000移位拼接改成{led[14:0], 1b0}。第二种是用参数化写法把位宽和初始值都改成parameter这样例化时只需改参数就能复用。参数化写法是更专业的方式我强烈建议从第一次写流水灯就养成这个习惯module led_flow_param #( parameter LED_WIDTH 8, parameter CNT_MAX 500_000 - 1 )( input wire clk, input wire rst_n, output reg [LED_WIDTH-1:0] led ); reg [$clog2(CNT_MAX)-1:0] cnt; reg shift_en; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; shift_en 1b0; end else if (cnt CNT_MAX) begin cnt 0; shift_en 1b1; end else begin cnt cnt 1b1; shift_en 1b0; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin led {{(LED_WIDTH-1){1b0}}, 1b1}; end else if (shift_en) begin if (led {LED_WIDTH{1b0}}) begin led {{(LED_WIDTH-1){1b0}}, 1b1}; end else begin led {led[LED_WIDTH-2:0], 1b0}; end end end endmodule$clog2是Verilog 2001标准里的系统函数用于计算某个值以2为底的对数并向上取整。这行代码保证了计数器位宽会随着CNT_MAX的大小自动调整不需要手动改。{(LED_WIDTH-1){1b0}}, 1b1这种拼接语法是Verilog里生成“最高位为1、其余为0”的标准写法注意复制的花括号里必须先写复制次数再写被复制的值。4. 用仿真把时序“看”清楚testbench编写与关键波形检查4.1 写一个能自动检查的testbench仿真不是为了“跑一遍看着对”而是为了验证两个问题一是分频使能脉冲是否按预期周期产生二是移位寄存器是否在每个使能脉冲后恰好移动一位。下面给一个基础testbenchtimescale 1ns / 1ps module tb_led_flow; reg clk; reg rst_n; wire [7:0] led; // 实例化被测试模块 led_flow uut ( .clk (clk), .rst_n (rst_n), .led (led) ); // 生成50MHz时钟 initial clk 0; always #10 clk ~clk; // 复位与测试流程 initial begin rst_n 0; #100; rst_n 1; // 等待约55毫秒观察多次移位 #55_000_000; $display(Test completed.); $finish; end // 自动监测每次shift_en有效后检查led是否左移一位 reg [7:0] prev_led; always (posedge clk) begin if (rst_n led ! prev_led) begin if (led {prev_led[6:0], 1b0} || (prev_led 8b1000_0000 led 8b0000_0001)) begin $display(Shift OK: %b - %b, prev_led, led); end else begin $error(Shift ERROR: %b - %b, prev_led, led); end end prev_led led; end endmodule#10表示每10个时间单位翻转一次时钟在timescale 1ns/1ps下就是20ns周期正好50MHz。#55_000_000是55毫秒的仿真时间这个长度足够看到5次以上的移位。自动监测always块里led ! prev_led用来捕捉LED的任何变化。注意这里有个边界情况最左移出一位后load回初始值也就是1000_0000 - 0000_0001所以判断条件里单独允许了这种跳变。如果漏掉这个特判仿真会误报“移位错误”。4.2 三类必须盯住的波形打开仿真波形图重点看三处。第一处是复位释放后的第一个使能脉冲是否离复位足够远——如果复位释放和shift_en上升沿几乎重叠说明复位释放时机没选好虽然这个设计里不一定出错但你要知道这是一种时序隐患。第二处是shift_en的脉冲宽度。理想情况下它是一个周期宽20ns如果它持续了两个周期甚至更长说明计数器清零逻辑写错了会导致LED一次移动两位。这种问题在波形图上极其明显LED状态跳过了中间值。第三处是移位方向。连续观察四到五次移位确认是0000_0001 - 0000_0010 - 0000_0100这样依次左移而不是0000_0001 - 0000_0011这种多bit同时变化。后者说明你在时序逻辑里用了阻塞赋值造成了移位链穿透。很多初学者仿真时只看LED波形变化不看使能信号的时序关系。我的习惯是把clk、shift_en、led三个信号放在同一窗口里观察以时钟沿为参考线数一数从使能脉冲到LED状态变化之间隔了几拍。正常情况是1拍或2拍取决于LED的赋值逻辑在使能脉冲的同一个always块还是下一个always块如果隔了3拍以上说明你的代码里引入了多余的延迟环节。4.3 仿真通过不代表上板没问题仿真验证的是“逻辑功能正确”但上板问题往往出在仿真覆盖不到的地方。最常见的就是时钟引脚约束如果你用的开发板时钟不是默认引脚需要在约束文件里把clk端口映射到正确的物理引脚上并声明正确的I/O标准比如LVCMOS33。另一个是LED的输出引脚是否被其他外设占用很多开发板上同一组GPIO会同时连LED和按键需要在约束文件里检查是否有冲突。还有一个仿真和上板行为不一致的来源是初始值。仿真器里reg类型的初始值是X未知态如果你的代码里没有复位逻辑LED会一直显示X波形乱七八糟但上板时FPGA配置完成后寄存器初值是从配置比特流里加载的通常为0所以“没有复位也能跑”在实物上可能碰巧工作。这种碰巧恰恰是最危险的——你以为复位可以不要实际上只是这次的初始状态恰好没出问题。5. 避坑流水灯常见问题排查与三条“翻车”记录5.1 现象LED全灭怎么按复位都没反应原因排查时先做三件事用LED闪烁程序替换流水灯工程确认开发板LED硬件正常检查约束文件里led端口引脚绑定是否正确有没有被注释掉用在线逻辑分析仪或板载LED查看系统时钟是否真的到达FPGA。我见过最典型的原因是时钟引脚没绑对。有个项目把系统时钟接到了普通IO口而该IO口默认被IP核占用导致时钟根本没有进入FPGA内部路由。解决方法是查原理图或用户手册确认晶振引脚编号然后在约束文件里按create_clock -period 20.000 [get_ports clk]补一条时钟约束。这样做不仅能让时序分析工具正确计算路径延迟还能在引脚绑错时直接报错提示。5.2 现象LED从两边向中间亮而不是单向流水如果LED的流水方向不是从LED0到LED7逐步点亮而是出现“两边同时亮”“乱序亮”的现象说明你没有按位序连接LED。常见做法是把led[0]连到原理图上的LED_L最左边led[7]连到LED_R最右边。如果原理图上LED的丝印编号是反的或者约束文件里位序和板上丝印不一致就会出现这类现象。解决办法只有一种逐个确认。把LED赋值改成常量比如led 8b0000_0001上板看是哪个位置的灯亮记下来再改成led 8b0000_0010再记。两次就能判断出位序映射关系。不要靠猜不要靠“看着差不多就行”。5.3 现象仿真X态LED波形全是“红色”仿真器打开后led波形全是X这是初学者最常见的仿真问题。原因几乎都是没有复位或复位时间太短。前面说过仿真器里寄存器初值是X如果不复位就直接跑永远看不到有效值。解决方法是把testbench里的复位拉低时间加长到至少100ns并且在rst_n 1之前确保时钟已经跑了很多个周期。还有一种情况是复位信号本身是X。如果你在testbench里只写了rst_n 0没有给它初值rst_n 1它在0时刻可能会被当成X。正确写法是initial rst_n 0;然后在后面对它赋值。很多教程里写reg rst_n; initial begin rst_n 1; #100 rst_n 0; #100 rst_n 1; end这种复位在仿真的初期是高电平这样是不对的——复位必须从0开始拉低然后再释放。5.4 现象LED流水速度是“理论值的一半”如果LED换位的速度比你按CNT_MAX计算出的周期慢一倍最可能的原因是计数器从0计数到CNT_MAX然后又从0开始但shift_en的判断条件写成了cnt CNT_MAX - 1导致实际计数周期比预期多了一个时钟。这类“差一”问题在计数器里极其常见。还有一种可能是你把分频使能信号接到了时钟端口上比如写了always (posedge shift_en)。这样每一个使能脉冲每10毫秒才一个会被当成一个时钟沿LED状态不会像预期那样每个脉冲移动一位而是每个脉冲触发一次always块看起来“有时动有时不动”速度不稳定。检查方法是看综合报告里的Registers数量和时钟树结构。如果你发现某个寄存器的时钟端口不是全局时钟网络而是来自组合逻辑那一定是这种错误。5.5 现象按下按键后方向切换不灵敏方向切换不灵敏有两种情况一是按键按下去以后要等好一会儿才反应二是按一次会跳两下方向。第一种的常见原因是采样周期太长比如CNT_MAX设成了25_000_000500毫秒那你最快也要等半秒才能采样到按键状态第二种是按键没有消抖抖动导致每次按下产生了多次有效沿。机械按键某入门开发板上最常见的那种按下时抖动时间约5到20毫秒如果采样周期小于抖动时间一次按下可能会被采样到二到三次。解决思路是加一个按键消抖计数器只有按键状态保持稳定超过20毫秒才认为是一次有效按下。注意这个消抖计数器的时钟是系统时钟不是shift_en否则消抖逻辑本身的采样周期就变长了。6. 进阶把流水灯改造成“可调速度 呼吸效果”的状态机当你把基础流水灯跑通、仿真和上板都验证无误后下一个值得投入的方向是让同一个设计支持多种效果。我强烈建议不要用一大堆if-else嵌套去实现模式切换而是引入一个简单的状态机把“左流水”“右流水”“交替流水”“呼吸灯”各作为一个状态。这不只是为了好看而是让你在动手写UART或SPI之前先适应“硬件里的状态流转”这种思维。以状态机为核心的进阶设计控制逻辑是这样的主状态机每个一段时间比如1秒检查一次按键根据按键次数切换效果模式每种模式内部用自己的分频参数。状态切换时注意所有相关寄存器都要被复位到该模式的初始值否则会出现“从呼吸灯切成流水灯时LED却接着上一个状态乱跳”的尴尬。实现呼吸灯效果的核心是PWM占空比渐变。PWM周期可以固定为1毫秒占空比从0逐步增加到100%再从100%逐步减到0。这里有两个计数器一个频率较高的PWM计数器决定周期一个频率较低的占空比计数器决定渐变速度。两个计数器之间是“比较”关系而不是“分频链”关系——每次PWM计数器归零占空比计数器加一LED在PWM计数器小于当前占空比值时点亮。这里有一个容易被忽略的坑占空比计数器在达到上限后需要反向递减而反向标志位的翻转时机要在计数器等于上限或等于0时做判断不要用“大于/小于”做方向控制否则会出现边界处两个周期重复或跳变。实际调试时我习惯用在线逻辑分析仪捕捉占空比计数器和PWM计数器两个信号的波形对比看有没有“占空比从100%直接跳到0%”这种异常正常情况应该是从100%到99%再到98%这种平滑过渡。最后说一个我自己的习惯写完流水灯后我会故意把移位方向改成右移、故意删掉复位信号、故意把非阻塞赋值换成阻塞赋值然后分别跑仿真看波形差异。这个“故意破坏”的过程比跑十次正常仿真更能帮你建立时序直觉——你会亲眼看到同一个设计在不同写法下完全不同的行为这种对比经验是看任何教程都学不来的。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →