正点原子 发表于 2021-1-28 11:00:33

【正点原子FPGA连载】第十八章IP核之FIFO实验

本帖最后由 正点原子 于 2021-1-28 11:00 编辑

1)实验平台:正点原子超越者FPGA开发板
2)章节摘自【正点原子】超越者之FPGA开发指南
3)购买链接:https://item.taobao.com/item.htm?&id=631660290421
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz-chaoyuezhe.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子FPGA技术交流群:905624739





第十八章IP核之FIFO实验

FIFO的英文全称是First In First Out,即先进先出。FPGA使用的FIFO一般指的是对数据的存储具有先进先出特性的一种缓存器,常被用于数据的缓存,或者高速异步数据的交互(即跨时钟域信号传递)。FIFO与FPGA内部的RAM和ROM的区别是FIFO没有外部读写地址线,采取顺序写入数据,顺序读出数据的方式,使用起来简单方便,由此带来的缺点就是不能像RAM和ROM那样可以由地址线决定读取或写入某个指定的地址。本章将通过对ISE软件自带的FIFO IP核进行读写测试,来向大家介绍Xilinx FIFO IP核的使用方法。
本章包括以下几个部分:
1818.1简介
18.2实验任务
18.3硬件设计
18.4程序设计
18.5下载验证


18.1简介
根据FIFO工作的时钟域,可以将FIFO分为同步FIFO和异步FIFO。同步FIFO是指读时钟和写时钟为同一个时钟,在时钟沿来临时同时发生读写操作。异步FIFO是指读写时钟不一致,读写时钟是互相独立的。Xilinx的FIFO IP核可以被配置为同步FIFO或异步FIFO,其信号框图如下图所示。从图中可以了解到,当被配置为同步FIFO时,只使用WR_CLK,所有的输入输出信号都同步于WR_CLK信号。而当被配置为异步FIFO时,写端口和读端口分别有独立的时钟,所有与写相关的信号都是同步于写时钟WR_CLK,所有与读相关的信号都是同步于读时钟RD_CLK。

图 18.1.1 FIFO IP核示意图
对于FIFO需要了解一些常见参数。
FIFO的宽度:FIFO一次读写操作的数据位宽N。
FIFO的深度:FIFO可以存储多少个宽度为N位的数据。
空标志:empty。FIFO已空时由FIFO的状态电路送出的一个信号,用以阻止FIFO的读操作继续从FIFO中读出数据从而造成无效数据的读出。
将空标志:almost_empty。FIFO即将被读空。
满标志:full。FIFO已满时由FIFO的状态电路送出的一个信号,用以阻止FIFO的写操作继续向FIFO中写数据从而造成溢出。
将满标志:almost_full。FIFO即将被写满。
读时钟:读FIFO时所遵循的时钟,读操作在时钟的上升沿触发。
请注意,“almost_empty”和“almost_full”这两个信号分别被看作“empty”和“full”的警告信号,它们距离真正的空(empty)和满(full)都会提前一个时钟周期拉高。本实验将使用这两个信号。
写时钟:写FIFO时所遵循的时钟,写操作在每个时钟的上升沿触发。
对于FIFO的基本知识先了解这些就足够了,可能有人会好奇为什么会有同步FIFO和异步FIFO,它们各自的用途是什么。之所以有同步FIFO和异步FIFO是因为各自的作用不同,同步FIFO常用于同步时钟的数据缓存,异步FIFO常用于跨时钟域的数据信号的传递。例如时钟域A下的数据data1传递给异步时钟域B,当data1为连续变化信号时,如果直接传递给时钟域B则可能会导致所收非所送的情况(即在采集过程中会出现包括亚稳态问题在内的一系列问题),使用异步FIFO能够将不同时钟域中的数据同步到所需的时钟域中。
18.2实验任务
本节的实验任务是使用ISE生成FIFO IP核,并实现以下功能:当FIFO为空时,向FIFO中写入数据,写入的数据量和FIFO深度一致,即FIFO被写满;然后从FIFO中读出数据,直到FIFO被读空为止,以此向大家详细介绍一下FIFO IP核的使用方法。
18.3硬件设计
本章实验只用到了输入的时钟信号和按键复位信号,没有用到其它硬件外设。本实验中各端口信号的管脚分配如下表所示:
表18.3.1 引脚分配表

对应的UCF约束语句如下所示:
NET sys_clk                     TNM_NET = sys_clk_pin;
TIMESPEC TS_sys_clk_pin = PERIOD sys_clk_pin 50000 kHz;

NET sys_clk                     LOC = N8| IOSTANDARD = "LVCMOS33";
NET sys_rst_n                     LOC = G16 | IOSTANDARD = "LVCMOS33";
18.4程序设计
根据实验任务要求和模块化的设计思想,我们需要4个模块:fifo IP核、写fifo模块、读fifo模块以及顶层例化模块,其中顶层例化模块主要用来实现前三个模块的信号交互。由于FIFO多用于跨时钟域信号的处理,所以本实验使用异步FIFO来向大家详细介绍双时钟FIFO IP核的创建和使用。为了方便大家理解,这里将读/写时钟都用系统时钟来驱动。系统的功能框图如下图所示:

图 18.4.1系统框图
首先创建一个名为ip_fifo的工程,接下来创建fifo IP核。在ISE软件的左侧“Design”栏中右击工程名按钮,打开添加新文件选项栏,我们选择New Source,如下图所示:

图 18.4.2 “New Source”按钮
打开New Source之后选择IP(CORE Generator&Architecture Wizard),然后将FIFO IP核命名为fifo(也可以命名成别的名字,但是必须是英文命名),路径放在工程文件夹下,ISE会自动创建一个ipcore_dir文件夹用来存放IP核文件,如下图所示:

图 18.4.3 创建FIFO IP核
如上图所示步骤设置完成后点击Next,进入IP核选择界面。在Search IP Catalog搜索栏中搜索fifo,然后选择FIFO Generator,如下图所示:

图 18.4.4创建FIFO IP核
按上图所示步骤操作之后点击Next进入Summary界面,如下图所示:

图 18.4.5 Summary界面
在Summary界面直接点击Finish,接下来就是配置IP核参数的过程,如下图所示:

图 18.4.6 参数配置
在上图中:
Component Name:FIFO IP核的命名,这里会直接显示之前创建IP时的命名。
Interface Type:Native本机接口FIFO,可以利用区块RAM、分布式RAM或内置的FIFO资源;AX14除了支持Native的应用功能外,还可以用于AX14系统总线和点对点高速应用程序,但是AX14 FIFO不支持内置的FIFO与移位寄存器的FIFO设置。
选择Native类型然后点击Next,进入下一页参数设置,如下图所示:

图 18.4.7参数配置
上图中:
Block RAM:FPGA内部硬件存在的块RAM资源。
Distribute RAM:分布式RAM资源,使用FPGA内部的寄存器和查找表搭建起来的RAM,当深度要求小于32的时候可以使用。
Shift Register:使用FIFO产生一个移位寄存器。
Common clock:同步时钟。
Independent clock:异步时钟。
选择Independent clocks选项并使用Block RAM资源,然后点击Next进入下一页参数设置,如下图所示:

图 18.4.8参数配置
上图中:
Read Mode:选择Standard FIFO,标准FIFO模型。
Write Width:写数据位宽。
Write Depth:写数据深度,可以理解为FIFO的数据容量。
Read Width:读数据位宽。
这里按照上图所示设置好后,点击Next进入下一个参数页面设置,如下图所示:

图 18.4.9参数配置
上图中:
Almost Full Flag:将满标志。
Almost Empty Flag:将空标志。
Write Acknowledge Flag:写应答标志,可以在其下方设置高电平有效(Active High)还是低电平有效(Active Low)。
Overflow Flag:当写指针追到了读指针,然后还继续写就会给出Overflow Flag标志,可以在其下方设置高电平有效(Active High)或低电平有效(Active Low)用来表示写错误。
Vaild Flag:读有效,可以在其下方设置高电平有效(Active High)还是低电平有效(Active Low)。
Underflow Flag:当读指针追到了写指针,然后还继续读就会给出underflow Flag标志,可以在其下方设置高电平有效(Active High)或低电平有效(Active Low)用来表示读错误。
按照上图所示设置后,点击Next进入下一参数设置页面,如下图所示:

图 18.4.10参数配置
上图所示的这一页主要是复位的设置,这里我们不用作修改,保持默认,直接点击Next进入下一参数设置页面,如下图所示:

图 18.4.11参数配置
上图所示把Write Data Count和Read Data Count读写计数器选上,主要用来观察写进去了多少个数据,还剩多少个数据可读。注意读写计数器的位宽要足够大,保证计数器的最大值大于fifo的深度,否则计数器会溢出。之后点击Next进入下一参数设置页面,如下图所示:

图 18.4.12参数配置
上图所示的是要创建的FIFO IP核的Summary界面,检查一下设置,如果有问题就点击Back返回重新设置参数,如果没问题就点击Generate生成FIFO IP核。
回到Design工具栏下可以看到创建的FIFO IP核已经出现在工程栏下了,如下图所示:

图 18.4.13 FIFO创建完成
现在就可以添加设计源文件了。本次是对FIFO IP核进行读写,所以需要一个读模块、一个写模块还要一个顶层例化模块。下面先来看顶层例化模块:
1module fifo_top(
2   input    sys_clk   , // 时钟信号
3   input    sys_rst_n   // 复位信号   
4
5   );
6
7wire fifo_din                  ;
8wire fifo_wr_en                      ;
9wire fifo_rd_en                      ;
10 wire fifo_dout               ;
11 wire fifo_full                     ;
12 wire almost_full                     ;
13 wire wr_ack                        ;
14 wire overflow                        ;
15 wire fifo_empty                      ;
16 wire almost_empty                  ;
17 wire valid                           ;
18 wire underflow                     ;
19 wire fifo_wr_data_count      ;
20 wire fifo_rd_data_count      ;
21
22 fifo u_fifo      (
23   .wr_clk            (sys_clk            ), // input wr_clk
24   .rd_clk            (sys_clk            ), // input rd_clk
25   .din               (fifo_din         ), // input din
26   .wr_en             (fifo_wr_en         ), // input wr_en
27   .rd_en             (fifo_rd_en         ), // input rd_en
28 .dout            (fifo_dout          ), // output dout
29   .full            (fifo_full          ), // output full
30   .almost_full       (almost_full      ), // output almost_full
31   .wr_ack            (wr_ack             ), // output wr_ack
32   .overflow          (overflow         ), // output overflow
33   .empty             (fifo_empty         ), // output empty
34   .almost_empty      (almost_empty       ), // output almost_empty
35   .valid             (valid            ), // output valid
36   .underflow         (underflow          ), // output underflow
37   .rd_data_count   (fifo_wr_data_count ), // output rd_data_count
38   .wr_data_count   (fifo_rd_data_count )// output wr_data_count
39 );
40
41 fifo_wru_fifo_wr (
42    .clk            ( sys_clk         ), // 写时钟
43    .rst_n          ( sys_rst_n         ), // 复位信号
44      
45    .fifo_wr_en   ( fifo_wr_en      ), // fifo写请求
46    .fifo_wr_data   ( fifo_din          ), // 写入FIFO的数据
47    .almost_empty   ( almost_empty      ), // fifo空信号
48    .almost_full    ( almost_full       )// fifo满信号
49 );   
50
51 fifo_rdu_fifo_rd (
52    .clk            ( sys_clk         ), // 读时钟
53    .rst_n          ( sys_rst_n         ), // 复位信号
54
55    .fifo_rd_en   ( fifo_rd_en      ), // fifo读请求
56    .fifo_dout      ( fifo_dout         ), // 从FIFO输出的数据
57    .almost_empty   ( almost_empty      ), // fifo空信号
58    .almost_full    ( almost_full       )// fifo满信号
59 );
60
61 endmodule
顶层模块主要是对FIFO IP核、写FIFO模块和读FIFO模块进行例化。需要注意的是从整个顶层来看,工程对外没有任何输出,所有的数据读写都是在FPGA内部进行的,编译器会认为我们的代码端口对外界不起作用,所以ISE会编译报错,不过没关系,我们可以将需要观察的信号设置成output型,这样既可以防止在Chipscope中信号被综合优化,又可以使工程编译通过。当然你也可以不设置为输出,创建一个Chipscope的在线调试文件(可以参考软件使用篇),将待观察信号接到在线调试IP核上,这样编译也可以编译通过,然后通过Chipscope观察信号。(特别注意:刚拿到我们的例程是没有对端口作处理的,直接编译会报错)。
顶层模块完成后,接着看看例化的子模块代码。fifo ip核模块就不用看了,因为fifoip核是我们配置参数后软件自动生成的,主要来看看读写模块的代码。下面是写FIFO模块(fifo_wr.v)源文件的代码:
1module fifo_wr(
2      input                  clk    ,          // 时钟信号
3      input                  rst_n,          // 复位信号
4      
5      input                  almost_empty,   // FIFO将空信号
6      input                  almost_full ,   // FIFO将满信号
7      output    reg          fifo_wr_en ,      // FIFO写使能
8      output    reg   fifo_wr_data      // 写入FIFO的数据
9);
10
11 //reg define
12 regstate            ;//动作状态
13 reg         almost_empty_d0;//almost_empty 延迟一拍
14 reg         almost_empty_syn ;//almost_empty 延迟两拍
15 regdly_cnt          ;//延迟计数器
16 //*****************************************************
17 //**                  main code
18 //*****************************************************
19
20 //因为 almost_empty 信号是属于FIFO读时钟域的
21 //所以要将其同步到写时钟域中
22 always@( posedge clk ) begin
23if( !rst_n ) begin
24      almost_empty_d0<= 1'b0 ;
25      almost_empty_syn <= 1'b0 ;
26end
27else begin
28      almost_empty_d0<= almost_empty ;
29      almost_empty_syn <= almost_empty_d0 ;
30end
31 end
32
33 //向FIFO中写入数据
34 always @(posedge clk ) begin
35   if(!rst_n) begin
36         fifo_wr_en   <= 1'b0;
37         fifo_wr_data <= 8'd0;
38         state      <= 2'd0;
39      dly_cnt      <= 4'd0;
40   end
41   else begin
42         case(state)
43             2'd0: begin
44               if(almost_empty_syn) begin//如果检测到FIFO将被读空(下一拍就会空)
45                     state <= 2'd1;          //就进入延时状态
46               end
47               else
48                     state <= state;
49             end
50          2'd1: begin
51               if(dly_cnt == 4'd10) begin//延时10拍
52                                             //原因是FIFO IP核内部状态信号的更新存在延时
53                                             //延迟10拍以等待状态信号更新完毕                  
54                  dly_cnt    <= 4'd0;   
55                  state      <= 2'd2;      //开始写操作
56                  fifo_wr_en <= 1'b1;      //打开写使能
57            end
58            else
59                  dly_cnt <= dly_cnt + 4'd1;
60             end            
61          2'd2: begin
62               if(almost_full) begin      //等待FIFO将被写满(下一拍就会满)
63                     fifo_wr_en   <= 1'b0;//关闭写使能
64                     fifo_wr_data <= 8'd0;
65                     state      <= 2'd0;//回到第一个状态
66               end
67               else begin               //如果FIFO没有被写满
68                     fifo_wr_en   <= 1'b1;//则持续打开写使能
69                     fifo_wr_data <= fifo_wr_data + 1'd1;//且写数据值持续累加
70               end
71             end
72          default : state <= 2'd0;
73         endcase
74   end
75 end
76
77 endmodule
fifo_wr模块的核心部分是一个不断进行状态循环的状态机,如果检测到FIFO为空,则先延时10拍,这里注意,由于FIFO的内部信号的更新比实际的数据读/写操作有所延时,所以延时10拍的目的是等待FIFO的空/满状态信号、数据计数信号等信号更新完毕之后再进行FIFO写操作。如果写满,则回到状态0,即等待FIFO被读空,进行下一轮的写操作。
读FIFO模块fifo_rd.v源文件的代码如下:
1module fifo_rd(
2      input               clk          ,   // 时钟信号
3      input               rst_n      ,   // 复位信号
4
5      input      fifo_dout    ,   // 从FIFO读出的数据
6      input               almost_full,   // FIFO将满信号
7      input               almost_empty ,   // FIFO将空信号
8      outputreg         fifo_rd_en       // FIFO读使能
9);
10
11 //reg define
12 regstate         ;//动作状态
13 reg         almost_full_d0;//almost_full延迟一拍
14 reg         almost_full_syn ;//almost_full延迟两拍
15 regdly_cnt         ;//延迟计数器
16
17 //*****************************************************
18 //**                  main code
19 //*****************************************************
20
21 //因为 fifo_full 信号是属于FIFO写时钟域的
22 //所以要将其同步到读时钟域中
23 always@( posedge clk ) begin
24if( !rst_n ) begin
25      almost_full_d0<= 1'b0 ;
26      almost_full_syn <= 1'b0 ;
27end
28else begin
29      almost_full_d0<= almost_full ;
30      almost_full_syn <= almost_full_d0 ;
31end
32 end
33
34 //读出FIFO的数据
35 always @(posedge clk ) begin
36   if(!rst_n) begin
37         fifo_rd_en <= 1'b0;
38         state      <= 2'd0;
39         dly_cnt    <= 4'd0;
40   end
41   else begin
42         case(state)
43             2'd0: begin                     
44               if(almost_full_syn)      //如果检测到FIFO被写满
45                     state <= 2'd1;         //就进入延时状态
46               else
47                     state <= state;
48             end
49          2'd1: begin
50               if(dly_cnt == 4'd10) begin //延时10拍
51                                          //原因是FIFO IP核内部状态信号的更新存在延时
52                                          //延迟10拍以等待状态信号更新完毕
53                     dly_cnt <= 4'd0;
54                  state   <= 2'd2;          //开始读操作
55            end
56            else
57                  dly_cnt <= dly_cnt + 4'd1;
58             end
59          2'd2: begin
60               if(almost_empty) begin   //等待FIFO将被读空(下一拍就会空)
61                     fifo_rd_en <= 1'b0;    //关闭读使能
62                     state      <= 2'd0;    //回到第一个状态
63               end
64               else                     //如果FIFO没有被读空
65                     fifo_rd_en <= 1'b1;    //则持续打开读使能
66             end
67          default : state <= 2'd0;
68         endcase
69   end
70 end
71
72 endmodule
读模块的代码结构与写模块几乎一样,也是使用一个不断进行状态循环的状态机来控制操作过程,代码比较简单,这里就不再赘述。
下面我们来对写好的工程进行仿真验证一下功能,在仿真前我们先对顶层文件(fifo_top)做一下处理如下图所示:

图 18.4.14 对顶层模块做处理
将需要观察的信号全部重新定义一遍,并加上后缀为“_tb”,然后使用assign语句把原信号赋值给重新定义的待观察信号,最后把所有带后缀“_tb”的待观察信号当作输出端口输出,这样无论是写Tsetbench仿真文件还是使用Chipscope在线调试都比较方便,编译不会报错,抓取信号也不会被综合优化。
将顶层文件处理好后就可以生成Testbench文件了(不会生成Testbench文件的可以参考软件使用篇),模板生成完后写激励如下所示:
1 `timescale 1ns / 1ps
2
3 module fifo_top_tb;
4   // Inputs
5   reg sys_clk;
6   reg sys_rst_n;
7   
8   wire fifo_din_tb                   ;//仿真在线调试专用
9   wire fifo_wr_en_tb                     ;
10    wire fifo_rd_en_tb                     ;
11    wire fifo_dout_tb                  ;
12    wire fifo_full_tb                        ;   
13    wire almost_full_tb                      ;
14    wire wr_ack_tb                           ;
15    wire overflow_tb                         ;   
16    wire fifo_empty_tb                     ;
17    wire almost_empty_tb                     ;   
18    wire valid_tb                            ;   
19    wire underflow_tb                        ;   
20    wire fifo_wr_data_count_tb         ;   
21    wire fifo_rd_data_count_tb         ;      
22
23    // Instantiate the Unit Under Test (UUT)
24    fifo_top uut (
25      .sys_clk                (sys_clk                ),
26      .sys_rst_n            (sys_rst_n            ),
27      . fifo_din_tb         (fifo_din_tb            ),
28      . fifo_wr_en_tb         (fifo_wr_en_tb          ),
29      . fifo_rd_en_tb         (fifo_rd_en_tb          ),
30      . fifo_dout_tb          (fifo_dout_tb         ),
31      . fifo_full_tb          (fifo_full_tb         ),
32      . almost_full_tb      (almost_full_tb         ),      
33      . wr_ack_tb             (wr_ack_tb            ),
34      . overflow_tb         (overflow_tb            ),
35      . fifo_empty_tb         (fifo_empty_tb          ),
36      . almost_empty_tb       (almost_empty_tb      ),
37      . valid_tb            (valid_tb               ),   
38      . underflow_tb          (underflow_tb         ),
39      . fifo_wr_data_count_tb (fifo_wr_data_count_tb),
40      . fifo_rd_data_count_tb (fifo_rd_data_count_tb)      
41      
42    );
43
44    initial begin
45      // Initialize Inputs
46      sys_clk = 0;
47      sys_rst_n = 0;
48
49      // Wait 100 ns for global reset to finish
50      #100;
51      sys_rst_n=1;
52      // Add stimulus here
53
54    end
55    always #10 sys_clk=~sys_clk;
56endmodule
Testbench文件完成后点击保存,然后点击Modelsim仿真按钮,调用Modelsim进行仿真(不熟悉的可以参考Modelsim章节)。
仿真波形如下图所示:

图 18.4.15 仿真波形图
上图是整体波形,从图中可以看到读写是交替进行的,处于写状态时写使能打开(fifo_wr_en_tb)、写数据跳变(fifo_din_tb)、写有效标志拉高(wr_ack_tb)、写数据计数器计数(fifo_wr_data_count_tb)、没有出现写错误,写错误信号保持低电平(overflow_tb),当数据写结束时先提前一个时钟拉高将满信号(almost_full_tb),再拉高写满信号(fifo_full_tb);读也是一样的,处于读状态时读使能打开(fifo_rd_en_tb)、读数据读出(fifo_dout_tb)、读有效标志拉高(valid_tb)、读数据计数器计数(fifo_rd_data_count_tb)、没有出现读错误,读错误信号保持低电平(underflow_tb)、当数据读结束时先提前一个时钟拉高将空信号(almost_empty_tb),再拉高读空信号(fifo_empty_tb);
我们放大波形图来看一下细节如下图所示:

图 18.4.16仿真波形细节图
上图是处于写状态时的细节,可以看到一共写了255个数(0~254),在快写满时确实是先拉高将满信号,再拉高写满信号;看完了写状态的细节我们再看看读状态的细节,如下图所示:

图 18.4.17仿真波形细节图
读状态也可以看到它读出的数据跟写入的一模一样,也是先拉高将空信号,再拉高读空信号。这就说明我们的FIFO IP核工程目前仿真功能是完全正常的,接下来就可以添加UCF文件进行管脚分配和时序约束了(不熟悉的可以参考软件使用篇),最后我们使用Chipscope工具进行下板子实际验证一下功能是否正确。
18.5下载验证
首先将下载器与超越者开发板上的JTAG接口连接,下载器另外一端与电脑连接,然后连接开发板的电源,并打开电源开关。
打开Chipscope工具,将刚刚生成好的逻辑分析仪IP核下载到板子上去,然后运行如下图所示:

图 18.5.1在线调试波形图
上图中可以看到波形与仿真的波形是一样的,为了进一步确认功能,可以将波形放大看一下细节如下图所示:

图 18.5.2在线调试波形细节图
上图是写数据时的细节,跟我们仿真图是一样的,说明写数据没有问题,我们再看看读数据时的波形细节,如下图所示:

图 18.5.3在线调试波形细节图
可以看到读状态波形的细节也是符合预期,到此就可以说明本次FIFO IP核的功能是正常的,本次例程实验成功。
页: [1]
查看完整版本: 【正点原子FPGA连载】第十八章IP核之FIFO实验