第18章:流水灯实验

本章对应原书第 376~397 页,属于「第五篇 实战篇」。

本章在讲什么?(先看这个)

流水灯的地位堪比编程界的"Hello, World!"——对很多电子工程师来说,流水灯就是他们在硬件上看到的第一个实验现象。这个实验把前面学的知识全部串起来:计数器(第15章)做 0.5 秒延时,位拼接(第12章)实现 LED 移位,再补上一个新技能——时序约束。从本章开始进入实战篇,实验流程也更完整:硬件设计 → 程序设计 → 仿真 → 引脚约束与 RTL 视图 → 时序约束 → 上板验证。

核心概念拆解

LED 灯简介

LED,又名发光二极管。LED 灯工作电流很小(有的仅零点几毫安即可发光),抗冲击和抗震性能好,可靠性高、使用寿命长,因此被广泛用于仪器仪表指示灯、液晶屏背光源等领域。

不同材料的发光二极管可以发出红、橙、黄、绿、青、蓝、紫、白八种颜色的光。

图18.1.1 直插发光二极管实物图

图18.1.2 贴片发光二极管实物图

直插型二极管长的一端是阳极,短的一端是阴极;贴片二极管正面有颜色标记,有标记的那端是阴极

LED 与普通二极管一样具有单向导电性,不同颜色的正向导通电压不一样:红色和黄色约 2V,蓝色、绿色和白色约 3V(具体由生产厂家决定)。阳极加上正向电压后,通过 5mA 左右的电流就可以发光;电流越大亮度越强,但一般将电流限定在 3~20mA 之间,否则电流过大会烧坏二极管。

硬件设计

开发板底板板载 4 颗 LED,位于开发板右下侧:左边两个 LED 连接至 ZYNQ 的 PL 引脚,右边两个连接至 PS 引脚。

图18.2.1 LED灯实物图

图18.2.2 LED灯硬件原理图

图中 PL_LED0 和 PL_LED1 连接到 ZYNQ 的 PL 端,PS_LED0 和 PS_LED1 连接到 PS 端(本书只使用 PL 端外设)。每个 LED 都串联了电阻,起限流作用,防止电流过大烧坏 LED。

关键结论:PL_LED0 和 PL_LED1 的阴极通过 470 欧姆电阻连到地(GND),阳极由 ZYNQ PL 的 IO 管脚控制——所以:

  • IO 输出高电平 1 → 点亮 LED;
  • IO 输出低电平 0 → 熄灭 LED。

实验任务

使用底板上的两个 PL LED 灯顺序点亮并熄灭,循环往复产生流水灯效果,流水间隔时间为 0.5s

程序设计——模块设计

模块命名为 flow_led。0.5s 间隔需要计数器计时,所以需要系统时钟;复位也是必不可少的(程序跑飞时可以恢复默认状态)。

图18.4.1 模块框图与端口

信号名 方向 端口说明
sys_clk 输入 系统时钟,50MHz
sys_rst_n 输入 系统复位按键,低电平有效
led 输出 PL_LED(底板)

绘制波形图

流水灯是指同一时刻只有一个灯点亮,其余熄灭,每隔一段时间 LED 依次点亮(向右或向左流水)。底板只有两个 PL 端 LED,所以效果是两颗 LED 循环交替闪烁

图18.4.2 LED灯交替闪烁效果

因为 IO 输出 1 点亮 LED,所以控制方法为:第一次给 led 赋值 2'b01(LED0 亮);等 0.5s 后赋值 2'b10(LED1 亮);再等 0.5s 后又回到 2'b01……循环往复。

延时怎么实现? 用计数器。系统时钟周期为 20ns(50MHz 晶振),计 0.5s 需要:

0.5s / 20ns = 500000000ns / 20ns = 25000000 个时钟周期

计数器从 0 开始计数,所以最大计到 25000000-1,刚好 0.5s。模块内部框图:

图18.4.3 模块内部框图

flow_led 模块的波形图:

图18.4.4 flow_led模块波形图

cnt 循环从 0 计到 24999999 表示计时 0.5s;每当计到最大值时,切换一次 LED 状态。

编写代码

module flow_led(
    input               sys_clk  ,  //系统时钟
    input               sys_rst_n,  //系统复位,低电平有效

    output  reg  [1:0]  led         //LED灯
);

//reg define
reg  [24:0]  cnt ;                  //计数器

//*****************************************************
//**                    main code
//*****************************************************
//计数器计时0.5s
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        cnt <= 25'd0;
    else if(cnt < (25'd2500_0000 - 25'd1))
        cnt <= cnt + 25'd1;
    else
        cnt <= 25'd0;
end

//对LED灯进行移位控制,以输出2位LED的状态
always @(posedge sys_clk or negedge sys_rst_n) begin
    if(!sys_rst_n)
        led <= 2'b01;
    else if(cnt == (25'd2500_0000 - 25'd1))
        led <= {led[0],led[1]};
    else
        led <= led;
end

endmodule

逐段解读

  • 计数器位宽的选择:cnt 最大计到 24999999,对应二进制 1 0111 1101 0111 1000 0011 1111,共需 25 位。信号需要多大位宽,由它能计数到的最大值决定:

图18.4.5 数值位宽查看

  • led 初始值:移位前必须给 led 赋初值 2'b01(或 2'b10)。如果赋 2'b00,无论怎么移位,led 始终全为 0;
  • 移位的技巧:Verilog 移位运算符 <<(左移)和 >>(右移)会在低位(左移)或高位(右移)补零,单纯移位最终 led 会全变 0。所以这里用位拼接 {} 实现循环移位的效果:
led <= {led[0],led[1]};

即 led[0] 移到最高位,但最低位不补零,而是接上原最高位 led[1] 的值——2'b01 变 2'b10,再变回 2'b01,循环不止。

仿真验证——编写 TB 文件

TestBench 验证三步走:加激励 → 例化 → 判断输出。sys_clk 对应 50MHz 晶振(周期 20ns),让信号每隔 10ns 翻转一次;sys_rst_n 初始为 0(复位),延时一段时间后拉高结束复位——正常运行前先复位一段时间,是为了给模块内的相关信号赋初值。

`timescale 1ns / 1ns        //仿真单位/仿真精度

module tb_flow_led();

//parameter define
parameter  CLK_PERIOD = 20; //时钟周期 20ns

//reg define
reg           sys_clk;
reg           sys_rst_n;

//wire define
wire  [1:0]   led;

//信号初始化
initial begin
    sys_clk <= 1'b0;
    sys_rst_n <= 1'b0;
    #200
    sys_rst_n <= 1'b1;
end

//产生时钟
always #(CLK_PERIOD/2) sys_clk = ~sys_clk;

//例化待测设计
flow_led  u_flow_led(
    .sys_clk      (sys_clk),
    .sys_rst_n    (sys_rst_n),
    .led          (led)
    );

endmodule

代码要点:第 6 行把时钟周期定义成参数形式,方便后续修改;第 24 行 sys_clk 每隔 10ns(CLK_PERIOD/2)翻转一次,一个完整周期含一个高电平和一个低电平,共 20ns,对应 50MHz。

仿真提速技巧:流水间隔是 0.5s,直接仿真要等很久(复杂程序仿真几十毫秒可能就要几个小时)。为了提升效率,仿真时可以临时修改延时较长的代码——把流水间隔改为 500ns(25×20ns):

//    else if(cnt < (25'd2500_0000 - 25'd1))
    else if(cnt < (25'd25 - 25'd1))  //改为计时500ns
    //else if(cnt == (25'd2500_0000 - 25'd1))
    else if(cnt == (25'd25 - 25'd1))

运行仿真 10us 后:

图18.4.6 flow_led模块仿真波形

led=2'b01 持续 500ns,led=2'b10 同样持续 500ns,流水间隔与预期相符。

注意:仿真结束后要把流水间隔改回 0.5s——500ns 太短,人的肉眼看不到流水效果。

引脚约束与 RTL 视图

引脚约束:系统时钟、复位按键和两个 LED 的管脚分配如下(复位按键和两个 LED 在底板上,时钟在核心板上):

信号名 方向 管脚 端口说明 电平标准
sys_clk input U18 系统时钟,50MHz LVCMOS33
sys_rst_n input N16 系统复位按键,低电平有效 LVCMOS33
led[0] output H15 PL_LED0(底板) LVCMOS33
led[1] output L15 PL_LED1(底板) LVCMOS33
create_clock -period 20.000 -name sys_clk -waveform {0.000 10.000} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN U18 IOSTANDARD LVCMOS33} [get_ports sys_clk]
set_property -dict {PACKAGE_PIN N16 IOSTANDARD LVCMOS33} [get_ports sys_rst_n]
set_property -dict {PACKAGE_PIN H15 IOSTANDARD LVCMOS33} [get_ports {led[0]}]
set_property -dict {PACKAGE_PIN L15 IOSTANDARD LVCMOS33} [get_ports {led[1]}]

图18.5.1 IO Planning引脚约束界面

RTL 视图:在 Flow Navigator → RTL ANALYSIS → Open Elaborated Design → Schematic 打开。

图18.5.3 RTL原理图

代码与电路的对应关系:RTL_ADD 是加法器,实现计数器累加;RTL_LT(Less Than)是比较器,判断 cnt 是否小于 25'h17D783F(即十进制 24999999);RTL_MUX 是选择器,根据比较结果选择输出 0 或 cnt 累加结果;RTL_REG_ASYNC 是异步复位寄存器;RTL_ROM 将 cnt 与 24999999 对比并输出结果。RTL 分析原理图只反映 RTL 层面,综合后的原理图(SYNTHESIS → Open Synthesized Design → Schematic)才能看到与 FPGA 内部资源的对应关系。

时序约束与上板验证

什么是时序约束:时序约束(Timing Constraints)是在数字电路设计中,为保证电路的正确性和稳定性,对电路中各时序参数(时钟频率、时钟占空比、时钟延迟与信号传输延迟等)进行限制和约束的方法。

举个直观的例子:没有加时序约束时,图片显示模糊和错乱:

图18.6.1 画面显示异常

添加时序约束后,画面显示正常:

图18.6.2 画面显示正常

对时钟约束最简单的理解:告诉 EDA 工具设计中使用的时钟频率是多少,工具才能按要求优化布局布线,使设计在该频率下正常工作。本次实验 sys_clk 为 50MHz(周期 20ns),约束时可以等于或略低于该值,不建议周期设置太小,否则布局布线很难满足要求。

对于简单设计,不加时序约束可能也不影响功能;但设计变复杂或时钟频率较高时,不加约束就可能出现意外情况。推荐无论简单还是复杂的实验都做时序约束。本文档只对输入时钟做周期约束,在 XDC 文件中添加:

create_clock -period 20.000 -name sys_clk [get_ports sys_clk]

create_clock 是命令名,用于创建一个时钟;-name sys_clk 指定时钟名称、-period 20 指定周期为 20ns、[get_ports sys_clk] 指定时钟源端口,一般只对输入的时钟做周期约束

用 Vivado 图形界面添加时钟约束(避免手写命令出错):

  1. 先综合:点击 Flow Navigator 下的 Run Synthesis(约束前必须先综合):

图18.6.3 点击"Run Synthesis"按钮

  1. 综合完成后,点击 Flow Navigator 下的 Edit Timing Constraints

图18.6.6 点击"Edit Timing Constraints"按钮

  1. 在 Timing Constraints 界面点击"+"号添加时钟约束,弹出 Create Clock 界面:

图18.6.8 "Create Clock"界面

各参数含义: - Clock name:时钟约束的名称,通常与被约束的时钟信号同名,这里命名为 sys_clk; - Source objects:指定被约束的时钟对象; - Waveform:设置时钟周期(Period)、上升沿(Rise at)、下降沿(Fall at)。50MHz 时钟(周期 20ns、占空比 50%)对应 Period=20、Rise at=0、Fall at=10。"Add this clock to the existing clock"只在同一约束源存在多个时钟频率且都需约束时才勾选,本例无需勾选; - Command:根据上述配置自动生成的约束命令,可直接复制到 XDC 文件使用。

  1. 指定时钟源对象:点击 Source objects 框后的"…",在弹出的 Specify Clock Source Objects 界面中,"Find names of type"选择 I/O Port(系统时钟来自 IO 口),点击 Find:

图18.6.11 点击"Find"按钮后找到的信号

  1. 选中 sys_clk,点击"→"添加到右侧窗口,点击 Set,再点击 OK 完成添加:

图18.6.13 配置完成的"Create Clock"界面

  1. 保存约束:此时约束内容还没保存到文件中,点击左上角保存图标(或 Ctrl+S)。会弹出提示"保存当前约束文件会导致之前的综合分析过期",直接点击 OK;在 Save Constraints 界面默认保存到现有 XDC 文件,点击 OK 即可。

上板验证

编译工程生成比特流 .bit 文件后,点击 Flow Navigator 最下面的 Open Hardware Manager:

图18.6.18 Hardware Manager界面

将 Xilinx 下载器一端连接电脑,另一端连接开发板上的 JTAG 下载口,再连接电源线:

图18.6.19 开发板连接实物图

注意!一定要先把下载器连接到电脑和 JTAG 接口之后,再给开发板上电!带电热插拔 JTAG 接口有一定概率会损坏接口。

打开电源开关,点击 Hardware 窗口中的 Auto Connect,出现连接成功界面后点击 Program Device,Bitstream File 会自动识别比特流文件,点击 Program 下载:

图18.6.22 下载比特流界面

下载完成后,核心板上的 PL 配置完成灯(DONE 灯,绿色)点亮,底板上的两个 LED 呈现流水效果——不断交替闪烁:

图18.6.23 流水灯效果

本章总结

本章需要掌握三个要点: 1. LED 点亮原理:ZYNQ IO 输出高电平 1 点亮 LED,低电平 0 熄灭 LED; 2. 用计数器延时:0.5s = 25000000 个 20ns 时钟周期,计数器从 0 计到 24999999; 3. 用位拼接 {} 实现移位led <= {led[0],led[1]} 实现循环移位,避免普通移位补零导致全灭。

另外记住一个实用技巧:程序中延时较长时(尤其程序复杂),仿真会非常久,可以在不影响观察有效波形的前提下临时降低延时时间,提高仿真效率——仿真完记得改回来。

图片对照

本章原文图片较多(30+ 张),已挑选最有教学价值的部分分散插入各小节:LED 硬件图、模块框图与波形图、位宽查看、仿真波形、引脚约束、RTL 原理图、时序约束操作步骤和上板效果图。

初学者容易踩的坑

  • led 初值赋成 2'b00:全零怎么移位都是零,LED 永远不亮,必须赋 2'b01 或 2'b10 这样的非零初值。
  • 直接用 << 移位:左移会在低位补零,2'b01 左移一次变 2'b10,再移一次就变 2'b00 全灭——循环移位要用位拼接。
  • 计数器位宽不够:25000000 需要 25 位,位宽不够会导致计数溢出错误,延时不准。
  • 仿真完忘记改回延时:把 500ns 的仿真参数下载到板子上,流水快到肉眼无法分辨,看起来像两灯常亮。
  • 不做时序约束:简单实验可能侥幸没问题,但复杂设计或高频时钟下会出现显示错乱等问题,从现在开始养成每个工程都约束时钟的习惯。
  • JTAG 带电插拔:先接好下载器再上电,避免损坏 JTAG 接口。

小结 & 下一步

本章完成了 FPGA 世界的"Hello World":掌握了 LED 点亮原理、计数器延时、位拼接移位三个核心技能,还学会了时序约束的两种添加方式(XDC 语句和 Vivado 图形界面)。建议动手拓展:把现有例程稍作修改,改成两个 LED 灯同闪同灭的效果,间隔时间 0.25s——想想同闪同灭时 led 应该赋什么值(2'b11 和 2'b00 交替),计数器又要计到多少。