数据存储功能虽然表面上看起来并没有什么高深之处,但实际操作起来难度还是非常大的。随着FPGA芯片的集成度越来越高、FPGA设计的复杂度越来越大、业界对FPGA处理速度的期望越来越高,对于FPGA开发者使用数据存储的能力要求也越来越高。因此,在这样一个大形势下,数据存储的要求早已经不是仅仅能够将数据存下来、读出去那么简单,而是要求数据存储系统的吞吐量更大、反应速度更快、正确性更高、消耗的资源更少等等。因此,为了让数据存储不成为整个FPGA设计的短板,必须要重视对数据存储的掌控与使用。
触发器载体的特点是:
查找表,即FPGA逻辑资源块中的LUT,它一般用来实现组合逻辑,其实它也是具有记忆功能的。
LUT载体的特点是:
块存储,即FPGA中的BLOCK RAM资源,简称BRAM,它实际上就是在FPGA芯片中嵌入的一些小型的存储器,自然也就是实现数据存储功能的主力军之一。这些BRAM往往也都比较灵活,每一片BRAM,都可以被配置为多种位宽和深度的组合,例如一个4Kb容量的BRAM,可以配置成为位宽为1 bit、深度为4K,也可以配置成为位宽为4 bit、深度为1 k,等等。
BRAM载体的特点是:
除了FPGA芯片内部的载体之外,还有很多成熟的、专门的存储芯片可供选择,包括但不限于:
RAM,英文全称:Random Access Memory,翻译成中文为随机访问存储器,即我们可以在任意时刻向任意一个RAM地址写入数据或者从任意一个RAM地址读取数据。在RAM浩瀚的存储空间中,一般同一时刻最多只能访问RAM中的一个存储单元,这点与寄存器阵列有着本质的不同。
当需要一些大容量的数据缓存时,RAM通常是首选。并且RAM是所有涉及到大量数据存储的形式的本源,后续的所有存储形式都是在RAM的基础上发展起来的,并且,由于RAM的随机访问特性,我们可以基于此开发出适合自己的、自定义的、特殊的数据存取方式。
ROM,英文全称:Read-Only Memory,翻译成中文为只读存储器。它是一种预先设定好存储内容,然后只允许进行读操作的存储结构。
从ROM的特征介绍可以看出,ROM其实就是RAM功能的一个子集,因此,触发器、查找表、块存储也都可以是ROM的实现载体。
在FPGA设计的某些算法中,需要用到了篇幅较大、且规律不明显的常数表时,ROM是首选方案。
FIFO,英文全称:First In First Out,翻译成中文为先进先出队列。受到先进先出性质的约束,FIFO的应用范围远没有RAM广泛,但是在FPGA设计中,FIFO的人气指数却要远远高于RAM,这其中最主要的原因就在于绝大部分的数据传递都是遵循先进先出特性的,而且直接使用成熟的FIFO模块远比使用RAM再配合编写读、写控制逻辑要来得简便得多。
但是,当设计的工作时钟频率越来越高时,或者你的BOSS对设计的性能要求越来越苛刻时,你会发现“FIFO的数据消失性”是很令人头疼的问题。例如,以前你的设计满负荷1秒钟只能处理10幅图片,现在要达到满负荷1秒钟处理20幅图片,那么原来工作在50 MHz时钟频率下的设计,现在需要能够在100 MHz下也能正常工作。速度向来是FPGA设计最大的杀手,因为它直接关系到FPGA时序逻辑的一个至关重要的时序指标——建立时间,所以对于FPGA来说,同样一个功能,在50 MHz下和100 MHz下实现起来难度是截然不同的。
这里我们不去展开来讨论,只把目光集中到如何连续地从FIFO中读取一个不定长的数据包的问题上去。
always @(posedge clk)
begin
wr_en <= wrEn;
din <= datain1;
end
always @(posedge clk)
begin
rd_en <= rdEn;
dout <= dataOut;
end
myfifo fifo (
.clk(clk),
.din(din),
.rd_en(rd_en),
.srst(srst),
.wr_en(wr_en),
.dout(dataOut)
);
第二段代码由于对FIFO的接口增加了一级缓存,有效地截断了前、后级组合逻辑和布局、布线的延迟时间与FIFO接口时序之间的相互影响,因此最高工作时钟的频率肯定会比第一段不加缓存的代码高出不少。但是与此同时,这种简单而有效的方法带来一个重大隐患——注意观察一下第二段代码的FIFO读接口缓存部分,与写接口不同的是,读接口的使能rd_en
与数据dataOut
相对于FIFO是反向的,即对于FIFO来说,rd_en
是输入、dataOut
是输出,给它们同时加上了缓存,那就意味着后级模块在时刻0(令一个时钟周期为一个时刻)给出一个有效的使能rdEn
,在时刻1便能传递给FIFO,在时刻2时FIFO才能送出这次请求所需要的数据dataOut
,在时刻3数据才会传递给寄存器clout
。这也就是说每当发出一次读请求的时候,要经历3个时钟周期后才能得到想要的数据。这滞后的3个周期就是这种简单又有效的方法的一个重大隐患,接下来我们会分析由这个隐患所引起的所谓的“FIFO的数据消失性”给设计带来的难题。
如果FIFO内部存储的数据包是定长的,即每次只需要读取固定长度的数据出来,那么每次读FIFO只需要连续给出N个有效使能后然后滞后第一个使能3个时钟周期后收获数据即可。但是如果需要读出来的数据是不定长的该怎么办呢?
一般,不定长的数据包有两种:
0x11 22
。当连续读取FIFO时,这两种数据包结构都会产生致命问题。
一旦“FIFO的数据消失性”发生,那么在下次读取FIFO中的数据时,得到的必然是一个不完整的数据包,如此往复将导致每次读取都得到的是错误的数据包结构,从而使得设计失败。
也许有人会说,那就不要连续读取好了,每次发送一个有效的读使能,3个时钟周期后根据收到的数据判断是否需要发送下一个读使能,这样不就可以避免“FIFO的数据消失性”发生了吗?没错,这种方法的确可以避免隐患的发生,但是,如果这样做就证明我们没有搞懂提高工作时钟频率的意义——提高工作时钟频率最大的好处就是提高了设计的性能,对于FIFO来说,就是要提高吞吐量。如果在50 MHz频率下每个时钟周期可以从FIFO中读取一个数据,而在100 MHz下每3个时钟周期才可以从FIFO中读取一个数据,那么把时钟频率从50 MHz提高到100 MHz的意义何在?因此,保证FIFO中的数据能够连续地被读出是提高FIFO吞吐量的关键。
数据包的长度是未知的,又必须连续读取,又不能让“FIFO的数据消失性”影响了数据包的结构,该如何是好呢?
你可能尝试从第二段代码的dataOut
开始着手,但是dataOut
是受着FIFO输出延迟影响的信号,对它进行判断很可能会降低系统的工作频率。经过一番尝试之后,你甚至可能想干脆用RAM来代替FIFO算了,因为RAM不存在“FIFO的数据消失性”,不过这就意味着你要对自己的设计动手术了。先别急,我有一个方法,在大多数情况下都可以解决这个问题,简称为“冗余法”。
“冗余法”,顾名思义,就是通过一些“多余的”东西来解决问题,这些“多余的”东西看似多余,如果用得恰到好处,就一点也不多余。下面,就以固定字节模式结尾的数据包具体给出一种“冗余法”的应用例子。
假设原数据包的结构如下:
0x????, 0x????,…,0x1122;
如果在数据包的末尾追加3个无效的数据,(最好与固定结尾字节模式不同,这样有利于逻辑方面的处理),例如0x3344、0x3344、0x3344
,那么数据包变成如下结构:
0x????, 0x????,…,0x1122, 0x3344, 0x3344, 0x3344;
结构修改后的数据包,采用连续读取的方法,当发现dout
等于0x1122
时关掉FIFO读使能rdEn
,刚好将3个无效数据0x3344
从FIFO中剔除出来,等到下一次读取数据包时,正好能够从新数据包的包头开始完整接收信息了!
在具体的应用中,“冗余法”可以用在数据包的尾部,也可以用在数据包的头部,也可以无所谓头部还是尾部,只要给两个数据包中间添加足够多的无效字节即可,一切按照具体情况以及逻辑处理的方便来定即可。
利用冗余法从FIFO中高速且连续地读取一个不定长的数据包的思路和黑客利用缓冲区溢出漏洞对我们的PC进行攻击时,通过写以一大段NOP开头的破坏程序来增加破坏程序执行命中率有着异曲同工之处,只不过“冗余法”采用的是一小段。
通常来说,RAM的容错性和抗干扰性要好于FIFO。例如,如果我们现在缓存的是数据包,每个数据包的大小均为100个数据,可是由于一些原因,某一个数据包多了或者少了1个数据,如果使用FIFO,仍按照100个来读取,那么这一个错误将会扩散到后续所有的数据包中,造成群体错误,可是如果使用RAM,固定从地址0、100、200、…开始数据包的存入和读出,那么错误将不会被扩散。
再看一例,仍然是缓存数据包的,每个数据包长度仍为100,不过由于一些原因,该数据包的最后一个数据表明该包数据的有效性,若有效则缓存,无效则丢弃,针对这种情况下,如果用RAM,可以很简单地通过写地址回调来完成数据包的丢弃功能,而如果用FIFO,必须先用一个小FIFO缓存当前数据包,若该包需要存储,则送至后续大FIFO中,否则从小FIFO中连续读取100个数据并丢弃。因此,RAM相对于FIFO来说具有更好的容错性和抗干扰性,但是由于FIFO的操作不需要显式地控制地址,因此对于通信质量有保证,且无特殊需求的数据存储来说,FIFO要比RAM具有更好的易用性。
STACK,即是堆栈的意思。与FIFO的先进先出特性恰恰相反,STACK遵循的是后进先出的原则。因此STACK的特征可以用汉诺塔的游戏原理来理解。如果你实在不知道汉诺塔是怎么回事,那么冰糖葫芦总见过吧,最后串进去的山楂球将会是第一个被你吃掉的。
除了读取数据的位置相差甚远外,STACK和FIFO的其他性质都是一样的,因此它也是基于RAM的,所以触发器、查找表、块存储也都是STACK的实现载体,同时,为了实现后进先出的功能,还必须使用一些触发器和查找表来组成RAM的读写控制逻辑。
STACK的思想多用于软件编程的子程序调用中,FPGA中存储数据时比较少碰到,但是当算法需要一个数据存储的后进先出特性时,别犹豫,非它莫属了!
通过对FPGA内部和外部的数据存储资源进行分析,可以明确不同应用场景下应选择的存储类型(RAM、ROM、FIFO、STACK等)。RAM和FIFO各有优缺点,应根据数据流的稳定性与存储操作的容错性来选择合适的存储模式。