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核配置)