发布时间:2022-12-21 08:00
在篇博客里引入FIFO IP核的概念,FIFO是FPGA中最常用的IP核,经常用在接口模块、串并转换、协议处理、数据缓存等很多场合,所以活学活用这个IP核对于后期项目开发很重要,并且灵活掌握FIFO,也是一名合格的FPGA工程师的一项基本功。
FIFO顾名思义就是First In First Out的简称,相信学过严蔚敏版数据结构的同学都有队列和堆栈的概念吧。而这里FPGA中FIFO IP核,其本质就和数据结构中的队列很相似,先进先出起到暂缓数据的作用。其实笔者起初学习FPGA的时候,也没太想明白为什么要用FIFO,什么时候用FIFO,用FIFO的时候又要注意些什么,所以在这篇博客里举例说明之前,笔者想先带着大家真正去搞明白上面三个问题,这将会对后期的理解学习起到很大帮助,毕竟磨刀不误砍柴工嘛。
首先为什么要用FIFO,可能绝大多数资料教程都会异口同声的说是为了缓冲数据,当然这样说肯定是没有问题的,但是对于很多新手朋友们来说,这个回答似乎没有任何意义,因为其中并没有包含有价值的信息。
大家可以类比下想一想,在STM32或者ARM开发中,我们用到队列的情况多不多,如经典的Freertos,用过的朋友都知道有消息队列的概念,因为任务的时间片轮询,消息队列的引入可以很好地规避进程切换带来的数据丢失,但在写一个STM32裸机程序的时候,就几乎很少用到队列了,因为本身C语言是顺序执行,比如定义一个数组或者指针,用到的时候拿来、不用的时候放着,不会有任何影响,不存在任何干扰。那么类似的FPGA本身是时序逻辑,其程序是每个独立的功能模块,各个模块又通过信号的例化,把彼此相关的信号关联在一起,这里显然就有可能出现有的模块处理慢,有的模块处理快,而每个模块的时钟又是在一直给定,那么处理快的就需要缓一缓去等待处理慢的,因为可能处理慢的模块产生输出数据正好是处理快的模块输入数据。
解决了第一个问题,大家再来思考下第二个问题即什么时候用FIFO,我们可以用上一节的“串口收发的八字节数据包文CRC校验”工程案例来说明这个问题,因为当时事先定义好包文长度是固定的八字节,所以在FPGA程序的设计当中,我们可以用一个位宽为64的rxd_package信号去保存收到和发送的八字节包文,但是随着串口之间数据量大,一个包文有很多内容比如100字节,显然那时候再去通过定义一个位宽为800的信号量作为收发缓存,是很不合适的,这将非常占用底层资源,并且人为的带来了程序设计上的复杂,大家不妨去设想一下,如果程序中要去对一个800位宽的数据进行组包、拆包以及做各种逻辑判断,那真的会让人很头疼,另外这样的程序上也很难去维护和修改。
所以通俗地说,FPGA程序中模块与模块之间存在数据交互的不同步,并且数据量较大的时候,FIFO几乎为FPGA工程师提供了最佳的解决途径。当然在实际项目工程中,这种不同步可能是源于跨时钟域的数据交互、也可能是由于各个模块数据处理速度的不平衡等很多因素所造成的。
最后笔者和大家聊一聊,FIFO IP核配置后各个信号接口的一些说明,这同样也是个很重要的内容,如图1是异步时钟FIFO IP核中各个信号接口的示意图,我们注意到FIFO IP核:1.分为读写独立的端口,即FIFO_READ和FIFO_WRITE;2. FIFO_WRITE写端口有一个full输出信号用于标记FIFO写满,FIFO_READ读端口有一个empty输出信号用于标记FIFO读空;3.读写两端都有各自的时钟和使能,即rd_clk和rd_en,wr_clk和wr_en,且相互独立不影响;4.写端口的输入数据din和读端口的输出数据dout,它们是相互独立的,其位宽的长度可以一样,也可以不一样;5.写端口有wr_data_count输出信号指示目前FIFO写端口有多少数据量,读端口有rd_data_count输出信号指示目前FIFO读端口有多少数据量,并且读写端口是相互隔离的。
所以总体上概括来说,FIFO IP核读写两端是彼此独立的,从时钟、使能、输入输出等各个方面;同时FIFO的写满full信号以及读空empty信号作为两个重要的指示信号,显然在FPGA设计中起到关键的作用,另外FIFO IP还有一些不常使用的可选信号,关于更多Xilinx的FIFO IP核细节,感兴趣的同学可以查看官方手册pg057-fifo-generator。
图1 异步时钟FIFO IP核中各个信号接口的示意图
搞清楚异步时钟FIFO IP核中各个信号接口的意义和作用以后,我们和大家进一步去探讨下,使用FIFO的时候具体又有哪些注意要点。在这里大家不妨先去思考下面三个在实践FIFO当中可能遇到的问题:1.如何避免写满读空;2.何时读取何时写入;3.读写深度怎么选择,这是三个很普遍且具有代表性的问题,可能大部分同学在刚接触到FIFO的时候,都会有抱有这样类似的疑问。
那么下面我们再逐一把上述三个问题搞清楚,首先为什么FIFO会出现写满或者读空的现象,大部分情况是因为在设计FPGA程序时,读写FIFO的机制本身存在一定的问题,比如不关心写端口处的full写满信号和wr_data_count写入多少数据的指示信号,也不关心读端口处的empty读空信号和rd_data_count可读多少数据的指示信号,一直向FIFO不断地写入数据,但是读出数据的速度又慢于写入数据的速度,在这里大家可以先直观地把FIFO理解成一个水池,写入FIFO好比向水池注水,读取FIFO好比向水池取水,所以如果不做任何限制,只要注水速度快于取水速度,那么FIFO一定会被写满,同样的道理只要注水速度慢于取水速度,那么FIFO一定会被读空。
接着我们也去想想看应该什么时候写入或者读取FIFO呢,其实从理论上来说,只要缓冲数据到了都是可以去写入FIFO,但是最好在程序设计中需要添加一些必要的逻辑判断,比如当上游数据达到多少数量时再触发写FIFO操作,也可以从写端口wr_data_count信号进行判断,当目前已经写入FIFO的数据超过多少数量时不再写入FIFO避免写满溢出情况。在读取FIFO数据的时候,请大家一定要去注意读端口处的empty信号,并且根据empty信号用组合逻辑产生读FIFO的rd_en信号,如果这里去使用时序逻辑根据empty信号产生读FIFO的rd_en信号,则逻辑上会存在一定的延时,如图2所示,根据读端口的empty信号,我们分别用组合逻辑和时序逻辑去产生rd_en信号,可以看到当empty信号为高即此时FIFO中已经没有数据了,但是使用时序逻辑去产生rd_en信号,将会保持一个时钟周期,这样就会导致FIFO读空的误操作,而使用组合逻辑去产生rd_en信号,当empty信号为高时,就马上拉低rd_en信号,可以有效地防止FIFO读空的操作。
图2 异步时钟FIFO IP核中读使能rd_en信号用组合和时序逻辑产生的示意图
最后,我们再去思考在实际项目工程中FIFO的读写深度怎么选择,这个问题在很多大厂的FPGA岗笔试题中也屡次出现,题干中会给出各种各样的设计背景,让面试者去做出判断等等,其实这里说到底FIFO的读写深度选取多少合适,主要取决于其最坏的情况,通俗地说就是选择的FIFO深度能够保证在最极端的情况下仍不会溢出,所以通常背景也都是写时钟频率大于读时钟频率、写数据的速度大于读数据的速度,一般情况程序设计中读写FIFO都是突发brust型的,即一次性从FIFO中读写具体数目的数据量,因为如果程序设计中读写都是连续不断的数据流,那么即使给出很大深度的FIFO也都无法保证数据不溢出。
笔者在这里举个例子,帮助大家更加方便地理解这个问题,也是一道大厂FPGA岗的笔试题,具体题干要求如下:一个8bit位宽的FIFO,输入时钟是100Mhz,输出时钟是20Mhz,设计一个读写包文的缓存是2Kbit,且两个包文之间的发送时间间隔足够大,问异步时钟FIFO应该选取多大的读写深度。
我们来仔细分析下这个题目要求,发送一包文brust的突发长度是2Kbit,因为题干中说明了FIFO的位宽是1Byte,所以2Kbit换算过来也就是250Byte,上游模块发送一包文brust突发时间即写入FIFO的时间为:T=250*10ns=2500ns,在T这段时间内,下游模块接收的数据即从FIFO中读取的数据量为:Data= T/20ns=2500ns/50ns=50Byte,所以理论上需要异步时钟FIFO的最小读写深度为:250Byte-50Byte=200Byte,才能满足设计需求。
通过前面的介绍,相信大家对于FIFO的使用已经有初步的了解,在本小节中我们会和大家动手去完成四个经典的FIFO设计从而进一步地加深理解,在此之前我们会先在Vivado环境下打开FIFO IP核如图3所示,去熟悉FIFO IP核的配置,其配置界面包括多个项目,我们需要逐一地去按顺序配置,通俗地说也就是初始化FIFO IP核。
图3 在Vivado环境下打开FIFO IP核
如图4所示是FIFO IP核的基本配置界面,从实际工程应用出发大家一般都选择Independent Clocks Block RAM,通常这种配置也是项目工程中应用最多的,其读和写是异步时钟控制的,使用起来也非常灵活,用户可以根据需求例化成相同的读写时钟也可以通过MMCM/PLL IP核分频倍频出不同的读写时钟。
如图5所示是FIFO IP核的读写位宽和初始化配置界面,在该界面的Read Mode项目中,大家可以看到有Standard FIFO和First Word Fall Through两个选项,刚刚接触到FIFO设计的时候,其实相信任何人看到这里也都是一头雾水,Xilinx对于两个选项官方的说明如下:The standard read mode provides the user data on the cycle after it was requested. The First Word Fall Through read mode provides the user data on the same cycle in which it is requested.
这里英语好的同学可以单纯从字面上理解这段说明,大概意思就是说选项Standard FIFO会在rd_en读使能信号后一个时钟周期给出用户数据,选项First Word Fall Through则会在rd_en读使能信号的当前周期给出用户数据,笔者在这里推荐大家去选择First Word Fall Through选项,即读数据和读使能是相互对齐的,这样可以更加方便地例化使用FIFO IP去设计FPGA程序,在Data Port Parameters项目中可以支持读写位宽不同步的情况,这里可以根据实际的工程需求来进行选择,并且配置相应的读写深度,最后在Initialization项目中一般不去勾选Enable Safety Circuit选项。
如图6所示是FIFO IP核配置读端口数据量和写端口数据量的输出信号,一般情况下会选择勾选上,方便FPGA的程序设计上有效避免写满或者读空。配置完毕后单击OK,如图7所示是FIFO IP核生成后选择综合方式,去选择Out of context per IP即可。
图4 FIFO IP核的基本配置界面
图5 FIFO IP核的读写位宽和初始化配置界面
图6 FIFO IP核配置读端口数据量和写端口数据量的输出信号
图7 FIFO IP核生成后选择综合方式
下面通过一个例子去具体说明FIFO的使用方法,设计一个模块包含读写位宽是8bit、读写深度是32的异步时钟FIFO,其中输入数据信号din和输入数据指示信号din_vld是属于clk_in时钟域的,上游模块会负责写数据到本模块的FIFO中,当本模块FIFO写端口已写入了30个数据,也就是FIFO快要写满溢出的时候,这时上游模块仍有数据要写入FIFO则直接丢弃该数据。下游模块负责从本模块的FIFO中读数据,当下游模块输入rdy信号时表示下游模块已准备好可以接收FIFO中的数据了,这时如果FIFO中有数据,那么就把FIFO中的数据赋值给输出数据信号dout送至下游模块并同时拉高dout_vld输出数据指示信号,其中输出数据信号dout和输出数据指示信号dout_vld则是属于clk_out时钟域的,上游模块din信号的写位宽以及下游模块dout信号的读位宽均是8bit,表1为练习的信号列表,这里为了方便仿真观察波形,把fifo_empty信号也例化出来。
信号列表 |
||
信号名 |
I/O |
位宽 |
clk_in |
I |
1 |
rst_n |
I |
1 |
rdy |
I |
1 |
din |
I |
8 |
din_vld |
I |
1 |
clk_out |
I |
1 |
dout |
O |
8 |
dout_vld |
O |
1 |
fifo_empty |
O |
1 |
表1 FIFO IP练习设计中的信号列表
我们来思考下练习功能模块的代码设计,首先我们用上面所介绍的方法,在Vivado下初始化一个读写位宽是8bit、读写深度是32的异步时钟FIFO,FIFO的写使能fifo_wren和写数据fifo_din建议大家都使用时序逻辑去产生,当然都使用组合逻辑产生也是没问题的,但是在这里fifo_wren和fifo_din需要统一使用同一种逻辑产生,使得两者同步避免延时,FIFO的读使能fifo_rden,根据题干要求,只需要FIFO的读空信号为低,即FIFO中仍有数据,同时下游模块输入rdy信号,即其已经做好了接收FIFO中数据的准备,这时读使能fifo_rden为高,根据前面的分析fifo_rden我们用组合逻辑去产生,从而去避免读FIFO延迟而造成读空FIFO的情况,并且本模块的输出数据dout和输出数据指示信号dout_vld都用时序逻辑根据读使能fifo_rden和读数据fifo_dout去产生。FIFO IP练习的详细代码设计如图8所示。
对于FIFO IP练习1的Testbench,笔者编写得也非常简单明了,先是给出30个时钟周期的上游模块输入信号din和din_vld,并且din用随机数进行了赋值操作,再给出下游模块准备好的rdy输入信号15个时钟周期,这是因为FIFO写端口的输入时钟是50Mhz,而读端口的输出时钟是100Mhz,如图9是FIFO IP练习1的输入信号激励设计。
大家打开Vivado环境,添加好功能文件和测试文件后,启动Modelsim进行仿真,如图10是FIFO IP练习1的仿真结果,可以清楚地观察到当fifo_empty为高时,即FIFO当前已经没有数据了,此时恰好dout输出FIFO中的最后一个数据,且dout_vld被拉高,完全符合我们的设计预期,而这里又因为dout和dout_vld在功能文件中使用了时序逻辑去产生,所以产生了一个时钟周期的延迟。
图8 FIFO IP练习的代码设计
图9 FIFO IP练习的输入信号激励设计
图10 FIFO IP练习的仿真结果
源工程代码下载链接:
链接:https://pan.baidu.com/s/11yK1THcbPXkQrSChISWKTA
提取码:1jh4