FPGA流水灯的三层时序设计:PLL、time_en与状态机

发布时间:2026/11/2 21:52:54
FPGA流水灯的三层时序设计:PLL、time_en与状态机

简介:本资源是一份面向FPGA初学者与数字电路实践者的Verilog开发教程,聚焦流水灯这一经典时序控制实验,系统讲解从时钟管理、分频使能到状态机驱动LED的完整设计流程。资源以1个952KB的Word文档(.docx)形式提供,内容涵盖PLL锁相环配置(含Vivado中Clocking Wizard调用步骤、参数设置与仿真验证)、time_top时间使能模块设计(含7位计数器实现与parameter参数化定义)、water_led流水灯状态机逻辑,以及system_top顶层集成与全系统仿真波形分析。文档结构清晰,按五大章节组织,每部分均含模块框图、Verilog代码片段、测试平台编写要点及关键信号时序图解,便于边学边练、对照调试。目前已有3409人学习下载,适合高校数字逻辑课程实践、FPGA入门项目实训及Verilog语法巩固者使用。

1. 流水灯不是“跑马灯”:为什么 FPGA 上一个看似简单的 LED 循环,必须拆成 PLL + time_en + 状态机三层时序黑匣子?

你写完always @(posedge clk) led_out <= {led_out[2:0], led_out[3]};,烧进板子——灯不亮;改成<= {led_out[3:1], led_out[0]},灯乱闪;再加个if (cnt == 10000000) begin ... end,结果四颗 LED 同时猛闪一下就卡死。这不是代码写错了,是你正在用组合逻辑的思维,硬闯一个被严格分层的同步时序牢笼。FPGA 流水灯的本质,从来不是“让灯动起来”,而是在纳秒级确定性约束下,把时间切成三段:第一段由 PLL 切出精准节拍(100MHz),第二段用 time_en 在节拍里打点(每 1ms 打一次),第三段靠状态机在点上跳步(左→右→左→右)。这三层不是为了炫技,而是 FPGA 的物理现实逼出来的:晶体振荡器只有 50MHz,但你要让 LED 每 500ms 移动一次,而内部计数器每 10ns 就要采样一次——没有 PLL 倍频,time_en 的精度会漂移;没有 time_en 隔离,状态机直接被 100MHz 时钟冲垮;没有显式状态机,LED 序列会在复位释放瞬间崩出不可预测的中间态。这份 Verilog 实现之所以值得深挖,正因为它把 FPGA 最核心的“时钟域划分”“使能信号降频”“状态迁移守则”全摊开在流水灯这个最小可运行单元里。它不是入门玩具,是数字电路时序纪律的第一块试金石。

2. PLL 锁相环:为什么 50MHz 晶振必须先变成 100MHz,才能让流水灯不“拖影”?

2.1 时钟倍频不是为了快,而是为了给 time_en 提供足够分辨率的“滴答”

FPGA 板载晶振标称 50MHz,周期 20ns。若直接用它驱动 time_en 计数器实现 500ms 间隔,需计数500_000_000 / 20 = 25_000_000次。7 位计数器(最大 127)显然不够,得用 25 位——但问题不在位宽,而在抖动累积。实际晶振存在 ±50ppm 频率偏差,2500 万次计数后,误差可达 ±1.25ms。当 time_en 脉宽本身只有 10ns(如原文仿真所示),这种抖动会让脉冲位置漂移,导致 water_led 模块的状态机在错误时刻采样,出现 LED 闪烁、跳步或卡死。PLL 将 50MHz 倍频至 100MHz(周期 10ns),同样实现 500ms 间隔只需计数500_000_000 / 10 = 50_000_000次,但关键在于:100MHz 时钟由 PLL 内部 VCO 锁定,相位噪声极低,长期稳定性远超晶振直驱。Vivado Clocking Wizard 生成的 PLL IP,其 MMCM(Mixed-Mode Clock Manager)模块通过反馈环路持续校准 VCO 输出,使输出时钟的周期抖动(Jitter)控制在皮秒级。这才是 time_en 能稳定输出 10ns 宽度脉冲的物理基础——没有这个“稳压器”,后续所有逻辑都是沙上筑塔。

2.2 locked 信号是 PLL 的“心跳监护仪”,忽略它等于放弃时序安全

PLL 输出的locked信号(低电平有效,因共阳极设计)绝非可有可无的装饰。它由 MMCM 内部的相位检测器和锁定判断逻辑生成,当 VCO 输出频率与目标值偏差小于阈值(通常为 ±1%)、且相位差稳定在容限内时,locked才拉高(原文说“共阳故低电平有效”,实为locked_n信号,此处按原文逻辑统一称locked为低有效)。若在locked未置位前就让 time_top 开始计数,后果是灾难性的:

  • 时钟未稳,计数器行为不可预测:MMCM 输出时钟可能处于频率爬升或相位震荡阶段,边沿抖动极大,触发器可能采样到亚稳态,导致cnt值随机跳变;
  • 复位不同步,状态机起始态错乱:reset信号若未等待locked就释放,water_led 的状态寄存器可能在时钟不稳定时完成复位,进入非法状态(如state = 4'b1010);
  • 跨时钟域传输失效:若后续设计涉及多时钟域,locked是启动跨时钟域同步器(如两级触发器)的必要使能条件。
    原文中 TB_PLL 仿真显示 790ns 后clk_100m才稳定,这正是locked从低变高的时刻。任何依赖clk_100m的逻辑,都必须将locked作为复位释放的门控信号——这是 FPGA 设计的铁律,不是建议。

2.3 Vivado Clocking Wizard 配置中的三个致命陷阱

提示:以下配置项若选错,仿真波形看似正常,但上板必失败,且难以定位。

  1. Input Frequency 输入值必须与物理晶振完全一致
    原文设为50(单位 MHz),这要求板载晶振标称值确为 50.000MHz。若实际为 49.998MHz(常见温漂),而你在 Wizard 中仍填50,PLL 会强制将 VCO 锁定到错误频率,导致clk_100m实际为 99.996MHz。time_top 计数 100 次本应耗时 1000ns,实际变成 1000.04ns,累积误差在 1s 内达 40ms——流水灯节奏肉眼可见地“拖影”。正确做法:用示波器实测晶振频率,填入 Wizard 的Input frequency字段。

  2. Output Clock Phase Offset 必须设为 0
    Wizard 中Phase offset默认为0.000ns,但若误设为5.000ns,clk_100m相对于clk_50m产生固定相位偏移。这本身不影响单一时钟域,但当clk_50m用于外部接口(如按键消抖)而clk_100m用于 LED 控制时,两域间数据交互需跨时钟域同步。非零相位偏移会改变同步器的建立/保持时间裕量,极易引发亚稳态。原文未提此参数,但实践中必须确认其为0.000。

  3. Reset 引脚必须保留并连接
    原文步骤六“去除 reset 复位的勾”,这是严重错误。PLL IP 的reset输入用于异步复位 MMCM 内部逻辑,确保上电时 VCO 从已知状态启动。若去掉该端口,在 FPGA 配置完成后,MMCM 可能处于未定义状态,locked永远不置位。正确做法:在 system_top 中将全局复位rst_n连接到 PLL 的reset端口,并确保rst_n持续时间 > 100us(满足 MMCM 复位要求)。

3. time_top 模块:为什么不用#500_000_000延时,而要用 7 位计数器生成 time_en?

3.1#delay是仿真毒药,FPGA 综合器会直接忽略它

Verilog 中#500_000_000这类延迟语句(#后接时间值)仅在仿真(Simulation)中生效,用于描述信号传播延时。但 FPGA 综合器(Synthesis Tool)在将 RTL 转换为门级网表时,会完全删除所有#延迟语句,因为硬件中不存在“等待 500ms”的指令——所有操作必须由时钟边沿驱动。若你在 time_top 中写:

always @(posedge clk) begin #500_000_000 time_en <= 1'b1; // 错误!综合后消失 end

综合结果中time_en将永远为x(未知态),仿真波形看似正确,但烧录后 LED 完全不响应。time_top 的本质是用计数器将高频时钟“分频”为低频使能脉冲,这是唯一被综合器认可的时间控制方式。

3.2 7 位计数器的宽度不是拍脑袋,而是由时钟周期和目标时间精确推导

原文给出计算:“100MHz 时钟周期 T=10ns,设计跑 1000ns,计数 100 次,2^n ≥ 100,n=7”。这个推导隐含两个关键前提:

  • 计数器是递增型(up-counter):cnt <= cnt + 1'b1,初值为0,计满2^7 - 1 = 127后溢出回0;
  • time_en 脉宽由比较逻辑决定:assign time_en = (cnt == MAX_COUNT) ? 1'b1 : 1'b0,其中MAX_COUNT = 99(计数 100 次,从0到99)。

验证:cnt从0开始,每10ns加1,到99时耗时99 * 10ns = 990ns,第100个时钟沿(1000ns时刻)cnt更新为100,此时cnt == 99为假,time_en为0。因此,time_en高电平实际出现在cnt从98→99的边沿(990ns),持续一个时钟周期10ns,于1000ns结束。这与原文仿真图中 “1995ns 达到高电平,持续 10ns” 的描述一致(注意:原文时间轴单位应为 ns,1995ns 是示例值,原理相同)。若误用n=6(最大 63),则time_en周期仅为640ns,LED 移动过快;若用n=8(最大 255),周期2560ns,LED 移动过慢。位宽选择是精度与资源的硬约束,没有妥协空间。

3.3 复位释放时机决定 time_en 的首次触发时刻,必须与 locked 同步

原文仿真图显示:复位信号rst_n在1000ns由高变低(即释放),time_en在1995ns首次拉高。这意味着time_en的首次有效周期始于1000ns之后的第100个clk_100m周期(100 * 10ns = 1000ns),即2000ns时刻。但图中为1995ns,说明计数器在rst_n释放前已开始工作?不,真相是:rst_n释放时刻必须严格晚于locked置位时刻。原文 TB_PLL 显示locked在790ns稳定,rst_n在1000ns释放,留有210ns余量,确保clk_100m已完全稳定。若rst_n在800ns就释放,cnt可能在clk_100m边沿抖动时更新,导致初始计数值错误,time_en首次出现时间飘忽不定。这是硬件调试中最隐蔽的时序 bug 之一——波形上看复位释放了,但时钟没稳,逻辑已开始跑飞。

4. water_led 模块:为什么流水灯状态机必须用独热码(one-hot)而非二进制编码?

4.1 二进制编码状态机在复位释放瞬间会“跳步”,导致 LED 乱序点亮

假设用 2 位二进制编码 4 个状态:S0=2'b00,S1=2'b01,S2=2'b10,S3=2'b11。状态转移逻辑为:

always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= S0; else if (time_en) state <= {state[0], state[1]}; // 左移循环 end

问题出在rst_n释放瞬间:state寄存器从2'b00(复位值)切换到新状态时,两个比特位的翻转不可能绝对同时。由于布线延迟微小差异,state[0]可能比state[1]早10ps变为1,导致中间态2'b01(S1)短暂出现,随后才变为2'b10(S2)。若此时time_en恰好到来,状态机可能从S0直接跳到S1,而非预期的S0→S1。更糟的是,若复位释放时state因亚稳态采样到2'b11(S3),下一次time_en就会跳到2'b11(S3→S3),LED 卡死。这就是“毛刺跳步”,在高速时钟下概率极高。

4.2 独热码状态机用面积换可靠性:每个状态一位,杜绝非法中间态

独热码(One-Hot)为每个状态分配独立比特位:S0=4'b0001,S1=4'b0010,S2=4'b0100,S3=4'b1000。转移逻辑变为:

always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= 4'b0001; // S0 else if (time_en) begin case (state) 4'b0001: state <= 4'b0010; // S0->S1 4'b0010: state <= 4'b0100; // S1->S2 4'b0100: state <= 4'b1000; // S2->S3 4'b1000: state <= 4'b0001; // S3->S0 endcase end end

此时,每次状态转移只有一位从0变1(置位),其余位保持0。即使某位因延迟稍晚翻转,中间态也只会是4'b0000(全零)或4'b0011(两位为1),但只要综合工具将case语句映射为查找表(LUT),非法态会被默认分支捕获。更重要的是,rst_n释放时,所有state寄存器同步清零,然后4'b0001同时加载到四位,不存在比特位翻转时序差。虽然占用 4 倍触发器资源(4-bit vs 2-bit),但在 Xilinx 7 系列 FPGA 中,LUT6 可高效实现 4 输入逻辑,资源代价远低于调试跳步 bug 的时间成本。

4.3 led_out 输出必须做同步寄存,否则跨时钟域会“撕裂”LED 图像

water_led 模块的led_out [3:0]直接由state组合逻辑译码:

assign led_out = (state == 4'b0001) ? 4'b1110 : // S0: LED0亮(共阳,0有效) (state == 4'b0010) ? 4'b1101 : // S1: LED1亮 (state == 4'b0100) ? 4'b1011 : // S2: LED2亮 (state == 4'b1000) ? 4'b0111 : // S3: LED3亮 4'b1111; // 其他态全灭

这看似简洁,但埋下大坑:state是clk_100m域寄存器,led_out是组合逻辑输出。若led_out信号需被其他模块(如 UART 发送器)采样,而该模块时钟为clk_50m,则led_out从clk_100m域跨越到clk_50m域时,组合逻辑输出可能处于变化中途(如4'b1101→4'b1011,中间态4'b1111或4'b1001),被clk_50m采样到非法值,导致 LED 瞬间全亮或错位。解决方案是添加一级寄存器同步:

reg [3:0] led_out_sync; always @(posedge clk) led_out_sync <= led_out; // 在 clk_100m 域打一拍 assign led_out = led_out_sync; // 输出同步后的值

这一拍将led_out稳定在clk_100m边沿,消除组合逻辑毛刺,是跨时钟域传输的基石操作。

5. system_top 顶层集成:为什么模块间连线不能“就近原则”,而要遵循时钟域隔离法则?

5.1 时钟信号必须用专用全局缓冲器(BUFG),否则 skew 会导致 setup/hold violation

原文中clk_50m输入后,经 PLL 生成clk_100m,再分发给time_top和water_led。若在 system_top 中直接写:

wire clk_100m; PLL uut_pll (.clk_in1(clk_50m), .clk_out1(clk_100m), .locked(locked)); time_top uut_time (.clk(clk_100m), .rst_n(rst_n), .time_en(time_en)); water_led uut_water (.clk(clk_100m), .rst_n(rst_n), .time_en(time_en), .led_out(led_out));

clk_100m信号会经过普通布线资源(Routing)到达各模块,导致时钟偏斜(Clock Skew):time_top的clk_100m输入边沿比water_led早200ps。当time_en从time_top输出,在water_led采样时,若clk_100m边沿已过,time_en可能被漏采;若边沿未到,time_en可能被提前采样到旧值。Xilinx 7 系列 FPGA 要求clk_100m必须接入全局时钟缓冲器 BUFG(Buffer Global):

wire clk_100m_unbuf; PLL uut_pll (.clk_in1(clk_50m), .clk_out1(clk_100m_unbuf), .locked(locked)); BUFG uut_bufg (.I(clk_100m_unbuf), .O(clk_100m)); // 关键:插入 BUFG // 后续模块均使用 clk_100m

BUFG 是 FPGA 内部专用低偏斜布线网络,能将时钟信号同步送达所有目标寄存器,skew 控制在±50ps内,确保time_en在water_led的clk_100m边沿被可靠采样。

5.2 复位信号必须异步释放、同步撤除,避免亚稳态污染整个系统

rst_n是全局异步复位信号,但直接连到各模块会引发问题:rst_n释放时刻若接近clk_100m边沿,time_top和water_led的复位释放可能不同步,导致time_en和state不协调。正确做法是在每个时钟域内对rst_n做同步化处理:

// 在 clk_100m 域内同步 rst_n reg rst_n_sync1, rst_n_sync2; always @(posedge clk_100m or negedge rst_n) begin if (!rst_n) begin rst_n_sync1 <= 1'b0; rst_n_sync2 <= 1'b0; end else begin rst_n_sync1 <= rst_n; rst_n_sync2 <= rst_n_sync1; end end wire rst_n_100m = ~rst_n_sync2; // 同步后的 rst_n(低有效) // 将 rst_n_100m 连接到 time_top 和 water_led

两级触发器(Synchronizer)将异步rst_n转换为clk_100m域内干净的复位信号,消除亚稳态风险。这是 FPGA 设计中复位管理的黄金标准,原文未体现,却是上板稳定的前提。

5.3 顶层连线必须显式声明位宽,隐式截断是时序故障的温床

原文led_out定义为output [3:0] led_out,但在system_top实例化时若写:

wire [3:0] led_out; water_led uut_water (.led_out(led_out), ...);

看似无误。但若某天修改water_led输出为[7:0](扩展为 8 颗 LED),而system_top未同步修改wire声明,Verilog 编译器会静默截断高位,led_out[7:4]丢失,且不报错。更危险的是,若led_out在system_top中被赋值给一个reg [7:0]变量,低位led_out[3:0]会填充到reg[3:0],高位reg[7:4]保持x,导致仿真与综合结果不一致。必须强制显式:

wire [3:0] led_out; // 与 water_led 端口严格一致 // 或更安全:用 typedef 定义 // typedef logic [3:0] led_t; // led_t led_out;

位宽匹配是静态时序分析(STA)的基础,任何隐式转换都会让时序报告失效。

6. 避坑指南:FPGA 流水灯项目中五个血泪经验总结

6.1 现象:LED 完全不亮,仿真波形显示 time_en 为 x

原因:locked信号未拉高,PLL 未锁定。常见于Input frequency填错,或rst_n未连接到 PLL 的reset端口,导致 MMCM 无法启动。
解决:在 Vivado 中打开hw_ila(Integrated Logic Analyzer),抓取locked信号。若其始终为0,检查 PLL 配置;若为1但clk_100m无波形,用示波器实测板载晶振是否起振。

6.2 现象:LED 以极快速度乱闪(毫秒级),非预期的 500ms 间隔

原因:time_top中cnt位宽不足或MAX_COUNT设置错误。例如cnt为 7 位但MAX_COUNT设为127(应为99),导致time_en周期变为1280ns,但water_led状态机误判为1000ns,加速运行。
解决:在time_top测试文件中,将cnt和time_en同时加入波形观察。确认cnt从0计数到MAX_COUNT的时间是否等于目标间隔(如1000ns),公式:time = (MAX_COUNT + 1) * T_clk。

6.3 现象:LED 点亮顺序错乱,如 S0→S2→S1→S3,或某颗 LED 永远不亮

原因:water_led状态机未用独热码,或case语句缺少default分支。当state因亚稳态采样到非法值(如4'b0011),case无匹配项,state保持原值或变为x。
解决:在case末尾强制添加default: state <= 4'b0001;,并在仿真中注入非法state值测试恢复能力。综合后查看 Xilinx Report 中State Machine报告,确认是否为 one-hot 编码。

6.4 现象:上板后 LED 正常,但用逻辑分析仪抓time_en发现脉宽不稳,有时 10ns,有时 20ns

原因:time_en信号未经过寄存器同步输出。组合逻辑assign time_en = (cnt == MAX_COUNT);的传播延迟受工艺角(PVT)影响,在高低温下波动,导致脉宽抖动。
解决:在time_top中添加同步寄存器:

reg time_en_reg; always @(posedge clk) time_en_reg <= (cnt == MAX_COUNT); assign time_en = time_en_reg;

确保time_en为纯净的时钟周期脉冲。

6.5 现象:更换不同型号 FPGA 开发板(如从 xc7a100t 换为 xc7a35t),同一代码编译失败,报BUFG资源不足

原因:BUFG在小型 FPGA(如 xc7a35t)中数量有限(通常 32 个),而设计中未优化时钟树。若clk_100m被多个模块直接使用,综合器可能为每个实例分配独立BUFG,超出上限。
解决:在system_top中显式例化一个 BUFG,并将clk_100m从该 BUFG 单一输出扇出到所有模块,避免综合器自动复制。查阅目标芯片的Clocking Resources手册,确认BUFG数量。

7. 从那以后我每次写 FPGA 时序逻辑,都强制走一遍“三问检查法”

第一次在某高校实验室调试一块流水灯 Demo 板,花了三天没让 LED 动一下。最后发现是locked信号悬空,未连接到rst_n的门控逻辑——rst_n在 PLL 稳定前就释放了,time_top的cnt从0开始计数,但clk_100m还在震荡,cnt值随机跳变,time_en永远不出现。那次崩溃让我明白:FPGA 不是软件,它的每一行 Verilog 都对应着硅片上的物理开关,而时序就是这些开关的呼吸节奏。从此我养成了雷打不动的“三问检查法”,在综合前、上板前、调试前各执行一次:

  1. 问时钟:当前模块的输入时钟是否来自BUFG?locked信号是否参与复位门控?用 Vivado 的Clocking Wizard报告和Report Clock Networks交叉验证;
  2. 问使能:所有低频控制信号(如time_en)是否由计数器生成?是否在输出端加了寄存器同步?用Report DRC检查是否存在ASYNC_REG警告;
  3. 问状态:状态机是否为独热码?case是否有default?复位释放后第一个time_en是否在locked置位后至少 10 个时钟周期才出现?在仿真中用$display打印locked和time_en的时间戳。

这三问不是教条,而是把 PLL 的锁相环路、计数器的离散时间、状态机的有限状态,全部锚定在 FPGA 的物理现实里。当你看到led_out从左到右平稳移动,那不是代码的胜利,是locked信号在790ns稳稳落定、cnt在1000ns精准归零、state在1005ns干净跳转共同谱写的确定性乐章。希望帮到你。

本文还有配套的精品资源,点击获取