ZYNQ从入门到秃头08 FPGA片内异步FIFO读写测试实验

Posted “逛丢一只鞋”

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ZYNQ从入门到秃头08 FPGA片内异步FIFO读写测试实验相关的知识,希望对你有一定的参考价值。


FIFO是 FPGA 应用当中非常重要的模块,广泛用于数据的缓存,跨时钟域数据处理等。学好 FIFO 是 FPGA 的关键,灵活运用好 FIFO 是一个 FPGA 工程师必备的技能。本章主要介绍利用XILINX 提供的 FIFO IP 进行读写测试。

实验原理

FIFO: First in, First out代表先进的数据先出 ,后进的数据后出

Xilinx 在 VIVADO 里为我们已经提供了 FIFO 的 IP 核 , 我们 只需 通过 IP 核例化一个 FIFO, 根据 FIFO 的读写时序来写入和读取FIFO 中存储的数据 。

其实FIFO 是也是在 RAM 的基础上增加了许多功能, FIFO 的典型结构如下,主要分为读和写两部分,另外就是状态信号,空和满信号,同时还有数据的数量状态信号,与 RAM 最大的不同是 FIFO 没有地址线,不能进行随机地址读取数据

什么是随机读取数据呢 ,也就是可以任意读取某个地址的数据。而 FIFO 则不同, 不能进行随机读取,这样的好处是不用频繁地控制地址线。


虽然用户看不到地址线,但是在FIFO 内部还是有地址的操作的,用来控制 RAM 的读写接口。

其地址在读写操作时如 下图所示 ,其中深度值也就是一个 FIFO 里最大可以存放多少个数据

初始状态下,读写地址都为 0 ,在向 FIFO 中写入一个数据后,写地址加 1 ,从 FIFO 中读出一个数据后,读地址加 1 。此时 FIFO 的状态即为空,因为写了一个数据,又读出了一个数据。


可以把 FIFO 想象成一个水池,写通道即为加水,读通道即为放水,假如不间断的加水和放水,如果加水速度比放水速度快,那么 FIFO 就会有满的时候,如果满了还继续加水就会溢出overflow 如果放水速度比加水速度 快 ,那么 FIFO 就会有空的时候, 所以把握好加水与放水的时机和速度,保证水池一直有水是一项很艰巨的任务。 也就是判断空与满的状态,择机写数据或读数据

根据读写时钟,可以分为同步FIFO (读写时钟相同)和异步 FIFO (读写时钟不同)

同步F IFO 控制比较简单,不再介绍,本节实验主要介绍异步 FIFO 的控制

其中读 时钟为 75 MHz 写时钟为 100MHz 。

实验中会通过 VIVADO 集成的 在想逻辑分析仪 ila ,我 们可以观察 FIFO 的读 写时序 和从 FIFO 中读取的数据。


对于FIFO需要了解一些常见参数:

FIFO的宽度: FIFO一次读写操作的数据位 N。

FIFO的深度: FIFO可以存储多少个宽度为 N位的数据。

将空标志:almost_empty。 FIFO即将被读空。

空标志:empty。 FIFO已空时由 FIFO的状态电路送出的一个信号,以阻止 FIFO的读操作继续从FIFO中读出数据而造成无 效数据的读出。

将满标志:almost_full。 FIFO即将被写满。

满标志:full。 FIFO已满或将要写满时由 FIFO的状态电路送出的一个信号,以阻止 FIFO的写操作继续向 FIFO中写数据而造成溢出。

读时钟:读FIFO时所遵循的时钟,在每个时钟的上升沿触发。

写时钟:写FIFO时所遵循的时钟,在每个时钟的上升沿触发。

这里请注意,,“almost_empty”和 “almost_full”这两个信号分别被看作 “empty”和 “full”的警告信号,他们相对于真正的空( empty)和满 full)都会提前 一个时钟周期拉高。

硬件设计

只用到 了输入的时钟信号和 按键 复位 信号 没有用到 其它 硬件 外设

对应的XDC约束语句如下所示:

set_property PACKAGE_PIN U18 [get_ports clk]
set_property PACKAGE_PIN N16 [get_ports rst_n]
set_property iosTANDARD LVCMOS33 [get_ports clk]
set_property IOSTANDARD LVCMOS33 [get_ports rst_n]

添加FIFO IP核

在添加FIFO IP 之前先新建一个 fifo_test 的工程 , 然后在工程中添加 FIFO IP ,方法如下

1) 点击下图中 IP Catalog ,在右侧弹出的界面中搜索 fifo ,找到 FIFO Generator, 双击打开。

2) 弹出的 配置 页面中, 这里可以选择读写时钟分开还是用同一个,一般来讲我们使用 FIFO 为了缓存数据,通常两边的时钟速度是不一样的。所以独立时钟是最常用的,我们这里 选择Independent Clocks Block RAM ,然后点击 “ 到下一个配置页面。

3) 切换到 Native Ports 栏目下 选择数据 位宽 16 FIFO 深 选择 512 ,实际使用大家根据需要自行设置就可以。

Read Mode 有两种方式,一个 Standard FIFO ,也就是平时常见的 FIFO数据滞后于读信号一个周期
还有一种方式为 First Word Fall Through ,数据预取模式 简称 FWFT 模式。

也就 是 FIFO 会预先取出一个数据,当读信号有效时,相应的数据也有效。我们首先做标准 FIFO 的实验。

4) 切换到 Data Counts 栏目下,使能 Write Data Count 已经 FIFO 写入多少数据)和 Read Data Count FIFO 中有多少数据可以读),这样我们可以通过这两个值来看 FIFO 内部的数据多少 。点击 OK, Generate 生成 FIFO IP 。

FIFO的端口定义与时序


FIFO的数据写入和读出都是按时钟的上升沿操作

当 wr_en 信号为高时写入 FIFO 数据

当 almost_full 信号有效时,表示 FIFO 只能再写入一个数据,一旦写入一个数据了, full 信号就会拉高

如果在 full 的情况下 wr_en 仍然有效,也就是继续向 FIFO 写数据,则 FIFO 的overflow 就会有效,表示溢出。

当rd_en 信号为高时读 FIFO 数据, 数据在下个周期有效。

valid 为数据有效信号, almost_empty表示还有一个数据读,当再读一个数据, empty 信号有效,如果继续读,则 underflow 有效,表示下溢,此时读出的数据无效。

而从 FWFT 模式读数据时序图可以看出, rd_en 信号有效时,有效数据 D0 已经在数据线上准备好有效了 ,不会再延后一个周期 。这就是与标准 FIFO 的不同之处。

添加PLL IP核

我们按照异步FIFO 进行设计,用 PLL 产生出两路时钟,分别是 100MHz 和 75MHz ,用于写时钟和读时钟,也就是写时钟频率高于读时钟频率。

添加ILA IP核


FIFO测试程序编写

Verilog

`timescale 1ns / 1ps
//
// Company: 
// Engineer: 
// 
// Create Date: 2021/12/23 10:41:01
// Design Name: 
// Module Name: fifo_alinx
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//


module fifo_alinx(
    input clk,
    input rst_n
    );

reg [15 : 0]    w_data;                 // FIFO写数据
wire            wr_en;                  // FIFO写使能
wire            rd_en;                  // FIFO读使能
wire [15 : 0]   r_data;                 // FIFO读数据
wire            full;                   // FIFO满信号
wire            empty;                  // FIFO空信号
wire [8 : 0]    rd_data_count;          // 可读数据数量
wire [8 : 0]    wr_data_count;          //已写入数据数量

wire            clk_100M;               // PLL产生100Mhz时钟
wire            clk_75M;                // PLL产生75Mhz时钟
wire            locked;                 // PLL lock信号,可作为系统复位 高电平有效
wire            fifo_rst_n;             // FIFO复位信号,低电平有效
wire            wr_clk;                 // 写FIFO时钟
wire            rd_clk;                 // 读FIFO时钟

reg [7 : 0]     wcnt;                   // 写FIFO复位后等待计数器
reg [7 : 0]     rcnt;                   // 读FIFO复位后等待计数器

// 例化PLL,产生100Mhz和75Mhz时钟
//----------- Begin Cut here for INSTANTIATION Template ---// INST_TAG
  clk_wiz_0 fifo_pll_inst
   (
    // Clock out ports
    .clk_out1_100m(clk_100M),           // output clk_out1_100m
    .clk_out2_75m(clk_75M),             // output clk_out2_75m
    // Status and control signals
    .reset(~rst_n),                     // input reset
    .locked(locked),                    // output locked
   // Clock in ports
    .clk_in1(clk));                     // input clk_in1

assign  fifo_rst_n      = locked;       // 将PLL的lock信号幅值给FIFO复位信号
assign  wr_clk          = clk_100M;     // 将100Mhz时钟赋值给写时钟
assign  rd_clk          = clk_75M;      // 将75Mhz时钟幅值给读时钟

// 写FIFO状态机
localparam     W_IDLE  = 1;
localparam     W_FIFO  = 2;

reg [2 : 0]  write_state;
reg [2 : 0]  next_write_state;   

always @( posedge wr_clk or negedge fifo_rst_n ) begin
    if( !fifo_rst_n )
        write_state <= W_IDLE;
    else
        write_state <= next_write_state;
end

always @( * ) begin
    case( write_state )
        W_IDLE : begin
            if( wcnt == 8'd79 )         // 复位后等待一定时间,safetycircuit模式下的最慢时钟60个周期
                next_write_state <= W_FIFO;
            else
                next_write_state <= W_IDLE;
        end
        W_FIFO : begin                  //一直在写FIFO状态
            next_write_state <= W_FIFO;
        end

        default : 
            next_write_state <= W_IDLE;
    endcase
end

// 在IDLE状态下,也就是复位之后,计数器计数
always @( posedge wr_clk or negedge fifo_rst_n ) begin
    if( !fifo_rst_n )
        wcnt <= 8'd0;
    else if ( write_state == W_IDLE )
        wcnt <= wcnt + 1'b1;
    else
        wcnt <= 8'd0;
end

// 在写FIFO状态下,如果不满就向FIFO中写数据
assign  wr_en = ( write_state == W_FIFO ) ? ~full : 1'b0;

// 在写使能有效情况下,写数据值加1
always @( posedge wr_clk or negedge fifo_rst_n) begin
    if( !fifo_rst_n )
        w_data <= 16'd1;
    else if( wr_en )
        w_data <= w_data + 1'b1;
end

// 读FIFO状态机
localparam  R_IDLE  = 1;
localparam  R_FIFO  = 2;
reg [2 : 0]  read_state;
reg [2 : 0]  next_read_state;

// 产生FIFO读的数据
always @( posedge rd_clk or negedge fifo_rst_n) begin
    if( !fifo_rst_n )
        read_state <= R_IDLE;
    else
        read_state <= next_read_state;
end

always @( * ) begin
    case( read_state )
        R_IDLE : begin
            if( rcnt == 8'd59 )                 // 复位后等待一定时间,safety circuit模式下的最慢时钟60个周期
                next_read_state <= R_FIFO;
            else
                next_read_state <= R_IDLE;
        end
        R_FIFO : begin
            next_read_state <= R_FIFO;         // 一直在读FIFO状态
        end
        default :
            next_read_state <= R_IDLE;
    endcase
end

// 在IDLE状态下,也就是复位之后,计数器计数
always @( posedge rd_clk or negedge fifo_rst_n) begin
    if( !fifo_rst_n )
        rcnt <= 8'd0;
    else if( write_state == W_IDLE )
        rcnt <= rcnt + 1'b1;
    else
        rcnt <= 8'd0;
end

// 在读FIFO状态下,如果不空就从FIFO中读数据
assign rd_en = ( read_state == R_FIFO ) ? ~empty : 1'b0;

//----------- Begin Cut here for INSTANTIATION Template ---// INST_TAG
fifo_generator_0 fifo_ip_inst (
  .rst( ~fifo_rst_n ),                          // input wire rst
  .wr_clk( wr_clk ),                            // input wire wr_clk
  .rd_clk( rd_clk ),                            // input wire rd_clk
  .din( w_data ),                               // input wire [15 : 0] din
  .wr_en( wr_en ),                              // input wire wr_en
  .rd_en( rd_en ),                              // input wire rd_en
  .dout( r_data ),                              // output wire [15 : 0] dout
  .full( full ),                                // output wire full
  .empty( empty ),                              // output wire empty
  .rd_data_count(rd_data_count),                // output wire [8 : 0] rd_data_count
  .wr_data_count(wr_data_count)                 // output wire [8 : 0] wr_data_count
);

ila_0 ila_w_fifo (
	.clk(wr_clk),                               // input wire clk

	.probe0(full),                              // input wire [0:0]  probe0  
	.probe1(w_data),                            // input wire [15:0]  probe1 
	.probe2(wr_en),                             // input wire [0:0]  probe2 
	.probe3(wr_data_count)                      // input wire [8:0]  probe3
);

ila_0 ila_r_fifo (
	.clk(rd_clk),                               // input wire clk

	.probe0(empty),                             // input wire [0:0]  probe0  
	.probe1(r_data),                            // input wire [15:0]  probe1 
	.probe2(rd_en),                             // input wire [0:0]  probe2 
	.probe3(rd_data_count)                      // input wire [8:0]  probe3
);

endmodule

testbeach

//~ `New testbench
`timescale  1ns / 1ps

module tb_fifo_alinx;

// fifo_alinx Parameters
parameter PERIOD  = 20;


// fifo_alinx Inputs
reg   clk                                  = 0 ;
reg   rst_n                                = 0 ;

// fifo_alinx Outputs



initial
begin
    forever #(PERIOD/2)  clk=~clk;          //20ns一个周期,产生50MHz时钟源
end

// initial
// begin
//     #(PERIOD*2) rst_n  =  1;
// end

fifo_alinx  u_fifo_alinx (
    .clk                     ( clk     ),
    .rst_n                   ( rst_n   )
);

initial
begin
    // Initialize Inputs
	clk = 0;
	rst_n = 0;

	// Wait 100 ns for global reset to finish
	#100;
    rst_n = 1;

//    $finish;
end

endmodule

结果分析

代码分析

在程序中采用 PLL 的 lock 信号作为 fifo 的复位,同时将 100MHz 时钟赋值给写时钟,75MHz 时钟赋值给读时钟

有一点需要注意的是, FIFO 设置默认为采用 safety circuit ,此功能是保证到达内部 RAM 的输入信号是同步的,在这种情况下**,如果异步复位后,则需要等待 60 个最慢时钟周期**,在本实验中也就是 75MHz 的 60 个周期,那么 100MHz 时钟大概需要 (100/75) x60=80 个周期。

因此在写状态机中,等待80 个周期进入写 FIFO 状态


在读状态机中,等待60 个周期进入读状态

如果FIFO 不满,就一直向 FIFO 写数据

如果FIFO 不空,就一直从 FIFO 读数据

例化两个逻辑分析仪,分别连接写通道和读通道的信号

仿真分析

以下为仿真结果,可以看到写使能wr_en 有效后开始写数据,初始值为 0001 ,从开始写到 empty 不空,是需要一定周期的,因为内部还要做同步处理。在不空后,开始读数据,读出的数据相对于 rd_en 滞后一个周期。

在后面可以看到如果FIFO 满了,根据程序的设计,满了就不向 FIFO 写数据了, wr_en 也就拉低了。

为什么会满呢,就是因为写时钟比读时钟快。如果将写时钟与读时钟调换,也就是读时钟快,就会出现读空的情况,大家可以试一下。

如果将FIFO 的 Re ad Mode 改成 First Word Fall Through

仿真结果如下,可以看到 rd_en 有效的时候数据也有效,没有相差一个周期

板上验证

生成好bit 文件,下载 bit 文件,会出现两个 ila ,先来看写通道的,可以看到 full 信号为高电平时, wr_en 为低电平,不再向里面写数据。

而读通道也与仿真一致

如果以 rd_en 上升沿作为触发条件,点击运行,然后按下复位,也就是我们绑定的 PL KEY1 ,会出现下面的结果,与仿真一致,标准 FIFO 模式下,数据滞后 rd _en 一个周期。

以上是关于ZYNQ从入门到秃头08 FPGA片内异步FIFO读写测试实验的主要内容,如果未能解决你的问题,请参考以下文章

ZYNQ从入门到秃头07 FPGA 片内 RAM && ROM 读写测试实验

ZYNQ从入门到秃头07 FPGA 片内 RAM && ROM 读写测试实验

ZYNQ从入门到秃头10 DAC FIFO实验(AXI-stream FIFO IP核配置)

ZYNQ从入门到秃头11 DAC FIFO实验(AXI-stream FIFO IP核配置)

ZYNQ从入门到秃头06 Vivado下的IP核MMC/PLL实验

ZYNQ从入门到秃头06 Vivado下的IP核MMC/PLL实验