FPGA开发流程

FPGA开发流程

前言

本文是以"流水灯设计"进行FPGA开发流程讲解。需求如下:

使开发板上的4个LED灯顺序点亮并熄灭,循环往复产生流水灯的效果,流水间隔时间为0.5s。

需求分析

开发板上LED灯硬件连接的示意图如下图所示,当FPGA输出低电平时,LED灯点亮;输出高电平时,LED灯熄灭。

硬件实物图如下图所示:

FPGA引脚分配关系如下表所示:

引脚名称 IO方向(相对于FPGA) 电平标准(IOSTANDARD) 引脚位置(PACKAGE_PIN) 备注
led[0] Output LVCOMS33 F19
led[1] Output LVCOMS33 E21
led[2] Output LVCOMS33 D20
led[3] Output LVCOMS33 C20

模块设计

该需求功能是4个 LED 灯实现流水灯的效果,因此给模块命名为 flow_led。

两个灯流水的间隔时间为 0.5s,间隔时间的控制需要通过计数器来实现,即通过计数器来实现计时的功能,因此本次实验需要用到系统时钟;

除此之外,系统复位在 FPGA 系统中也是必不可少的,当程序出现跑飞等异常情况时,可以使程序恢复至默认状态;

由以上分析可知,本次实验需要2个输入的端口,分别为系统时钟和系统复位,输出为 4位的 LED 端口,模块框图如下图所示:

模块端口与功能描述如下表所示:

信号名 位宽 方向 端口说明
sys_clk 1 输入 系统时钟,50MHz
sys_rstn 1 输入 系统复位按键,低电平有效
led 4 输出 LED,低电平有效

前面需求分析过硬件设计部分,当IO输出低电平0时,点亮LED灯;当IO输出高电平1时,LED灯熄灭,因此要想控制LED实现流水灯效果,第一次的时候给led端口赋值4'b1110;等待0.5s后,给led端口赋值4'b1101;等待0.5s后,给led端口赋值4'b1011;等待0.5s后,给led端口赋值4'b0111;后面依次类推,4个LED即可实现流水的效果。


LED 灯流水的间隔为 0.5s,因此接下来还需要做一个 0.5s 的延时,延时的功能可以通过计数器实现,每当计数器计时到 0.5s 后,切换一次 LED 灯的显示状态即可。当计时完成后,开始控制 LED 灯的状态。对于流水的功能非常适合移位的操作来实现,由此得到模块内部的框图如下图所示:

系统时钟 sys_clk 的时钟周期为 20ns(对应开发板板载的晶振频率为 50Mhz),计数器计时 0.5s 需要0.5s/20ns=500_000_000ns/20ns = 25_000_000 个时钟周期,由于计数器是从 0 开始计数,所以计数器最大计数到25000000-1,刚好是 0.5s。


由此绘制出 flow_led 模块的波形图如下图所示:

由上图可知, tcnt 循环从 0 计数到 24999999,表示计时 0.5s。每当计数器计数到最大值时,此时切换LED 灯的状态。


流水灯(flow_led.v)代码编写如下:

verilog 复制代码
// 流水灯模块, 流水间隔0.5s

module flow_led (
  // 模块时钟及复位
  input         sys_clk,
  input         sys_rstn,
  // LED输出
  output [3:0]  led
);

//------------------------------------
//             Local Signal
//------------------------------------
  reg [24:0] tcnt = 0; // 计数器
  reg [3:0]  led_ff = 4'b1110; // 移位寄存器

//------------------------------------
//             User Logic
//------------------------------------
// 计数器计时0.5s
  always @ (posedge sys_clk or negedge sys_rstn)
    if (!sys_rstn)
      tcnt <= 25'd0;
    else if (tcnt < 24'd24_999_999)
      tcnt <= tcnt + 25'd1;
    else
      tcnt <= 25'd0; 
// led输出移位寄存器
  always @ (posedge sys_clk or negedge sys_rstn)
    if (!sys_rstn)
      led_ff <= 4'b1110;
    else if (tcnt == 24'd24_999_999)
      led_ff <= {led_ff[2:0],led_ff[3]};
    else
      led_ff <= led_ff; 
//------------------------------------
//             Output Port
//------------------------------------
  assign led = led_ff;

endmodule

流水灯(flow_led.v)的 Testbench 仿真代码编写如下:

verilog 复制代码
`timescale 1ns/1ns

module tb_flow_led;

//*************************** Parameters ***************************
  parameter  PERIOD_CLK = 20;
 
//***************************   Signals  ***************************
    // 模块时钟及复位
    reg        sys_clk = 0;
    reg        sys_rstn = 0;
    // LED输出
    wire [3:0] led;

//*************************** Test Logic ***************************
  always # (PERIOD_CLK/2) sys_clk = ~sys_clk;

  initial
    begin
      #100;
      sys_rstn = 1;
      #5000;
      $stop;
    end

//***************************  Instance  ***************************
  flow_led u_flow_led(
    // 模块时钟及复位
    .sys_clk  (sys_clk),
    .sys_rstn (sys_rstn),
    // LED输出
    .led      (led)
  );

endmodule

由于流水灯模块定义的流水0.5s间隔较长,虽然 0.5s 从时间上来说比较短暂,但是对于仿真来说,仿真 0.5s 需要的时间较长。尤其是对于复杂的程序来说,有时候仿真几十毫秒,就需要好几个小时,因此为了降低仿真的等待时间和提升效率,一般需要对程序中延时较长的代码进行修改,流水灯模块修改后的代码如下(将流水时间间隔改为500ns):

verilog 复制代码
// 流水灯模块, 流水间隔0.5s

module flow_led (
  // 模块时钟及复位
  input         sys_clk,
  input         sys_rstn,
  // LED输出
  output [3:0]  led
);

//------------------------------------
//             Local Signal
//------------------------------------
  reg [24:0] tcnt = 0; // 计数器
  reg [3:0]  led_ff = 4'b1110; // 移位寄存器

//------------------------------------
//             User Logic
//------------------------------------
// 计数器计时0.5s
  always @ (posedge sys_clk or negedge sys_rstn)
    if (!sys_rstn)
      tcnt <= 25'd0;
    // else if (tcnt < 24'd24_999_999)
    else if (tcnt < 24'd24)
      tcnt <= tcnt + 25'd1;
    else
      tcnt <= 25'd0; 
// led输出移位寄存器
  always @ (posedge sys_clk or negedge sys_rstn)
    if (!sys_rstn)
      led_ff <= 4'b1110;
    // else if (tcnt == 24'd24_999_999)
    else if (tcnt == 24'd24)
      led_ff <= {led_ff[2:0],led_ff[3]};
    else
      led_ff <= led_ff; 
//------------------------------------
//             Output Port
//------------------------------------
  assign led = led_ff;

endmodule

工程搭建(Vivado)

  1. 双击打开 Vivado,点击"Create Project";

  2. 点击 "Next";

  3. 工程命名;

  4. 选择工程类型"RTL Project",点击"Next";

  5. 一直点击 "Next";

  6. 选择芯片型号;

  7. 添加代码和仿真文件;

功能仿真(Modelsim)

  1. 设置选择仿真工具为 Modelsim;

  2. 设置Modelsim安装路径和库路径;

  3. 点击开始仿真;

  4. 添加要观察的信号至波形窗口;

  5. 点击"Run all",查看仿真波形是否跟功能一致;

  6. 由上图可知, led=4'b1110的持续时间为 500ns, led=4'b1101 的持续时间同样也是 500ns,即 LED 灯的流水间隔为 500ns,波形和预期相符。

    需要注意的是,在仿真结束之后,将代码中定义的 LED 流水间隔时间改回 0.5s,否则 500ns 的时间间隔太短,人的肉眼会看不到流水的效果。

生成下载文件

  1. 编写约束文件;

    tcl 复制代码
    ############## NET - IOSTANDARD ##################
    set_property CFGBVS VCCO [current_design]
    set_property CONFIG_VOLTAGE 3.3 [current_design]
    #############SPI Configurate Setting##################
    set_property BITSTREAM.GENERAL.COMPRESS TRUE [current_design]
    set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design]
    set_property CONFIG_MODE SPIx4 [current_design]
    set_property BITSTREAM.CONFIG.CONFIGRATE 50 [current_design]
    ############# clock #########################
    set_property IOSTANDARD LVCMOS33 [get_ports sys_clk]
    set_property PACKAGE_PIN Y18 [get_ports sys_clk]
    create_clock -period 20.000 [get_ports sys_clk]
    ############# reset ##########################
    set_property IOSTANDARD LVCMOS33 [get_ports sys_rstn]
    set_property PACKAGE_PIN F20 [get_ports sys_rstn]
    ########################### LED Define ###########################
    set_property PACKAGE_PIN F19 [get_ports {led[0]}]
    set_property PACKAGE_PIN E21 [get_ports {led[1]}]
    set_property PACKAGE_PIN D20 [get_ports {led[2]}]
    set_property PACKAGE_PIN C20 [get_ports {led[3]}]
    
    set_property IOSTANDARD LVCMOS33 [get_ports {led[*]}]
  2. 添加约束文件;

  3. Bitstream 设置,同时生成固化文件;

  4. 点击生成 Bitstream;

  5. 生成文件路径如下:

上板验证

  1. 先连接硬件,把 JTAG 下载器和开发板连接,然后开发板上电,下图为开发板的硬件连接图:
  1. 点击 Open target 按钮->Auto Connect, 在 hardware 界面下会显示 xc7a35t_0( AX7035 开发板)或 xc7s50_0( AX7050 开发板) 的图标,说明 JTAG 连接已经建立 ;

  2. 右键选择 xc7a35t_0( AX7035 开发板)或 xc7s50_0( AX7050 开发板) ,在弹出的选项里选择Program Device 项 ;

  3. 在弹出的 Program Device 对话框中, 选择 flow_led 项目生成的 bit 文件,点击 Program 按钮烧写 FPGA。

  4. 烧写完成后 xc7a35t_0( AX7035 开发板)或 xc7s50_0( AX7050 开发板) 的状态会变成Programmed, 这时我们可以看到开发板上的四个 LED 灯已经在做流水灯动作了 。

Flash固化

直接下载 Bit 文件到 FPGA 后,开发板重新上电后配置程序已经丢失,还需要JTAG 下载,只有固化到 Flash,才能实现上电不丢失。具体步骤如下:

  1. 在如下图中右键选择 xc7a35t_0( AX7035 开发板)或 xc7s50_0( AX7050 开发板) 芯片,在弹出的列表中选择 Add Configruation Memory Device... ;

    注意:如果发现此项变为灰色不能选,是因为我们提供的工程中已经添加了 FLASH 配置,不能再添加 FLASH, 如下:

    当然自己如果想在已有 flash 的工程中再次添加一下进行实验话,可按如下图移除 flash,然后按上面添加 flash 的步骤进行即可:

  2. 在 Add configruation Memory Device 的配置界面里选择正确的 FLASH 型号,如下图所示:

  3. 提示是否对 SPI FLASH 进行编程, 点击 OK;

  4. 在弹出的 Program Configuration Memory Device 窗口中, Configration file 项选择 Vivado 生成的flow_led.bin 文件(此文件默认在 impl_1 目录下) 。 PRM File 项不用选。另外在这个窗口用户还可以配置 I/O 为上拉,下拉或者无上下拉。 配置操作选项保留默认就可以;

  5. 点击 OK 开始编程 FLASH;

  6. FLASH 编程完毕后, 会弹出如下成功的界面。

  7. 至此, SPI FLASH 烧写完毕, flow_led 程序已经固化到 SPI FLASH 中了。我们来验证一下,关电重新启动开发板, 等待一会儿你就可以看到开发板上的 LED 灯已经在做跑马运动了。