FPGA之道(51)数据的存储

前言

第一次见到这么来描述数据存储的书,感觉学了这两年的FPGA白学了,下面内容节选自《FPGA之道》,一起看看作者对于数据存储设计的智慧与经验。

数据的存储

为什么需要数据存储

数据存储功能是实现时序逻辑的基础,并且对于FPGA芯片来说,甚至连组合逻辑也是通过数据存储来实现的(因为FPGA中的组合逻辑是用查找表资源实现的,详见【数据存储的载体】章节),因此对于FPGA设计来说其重要性与必要性不言而喻,而对于FPGA开发者来说,能够灵活的掌握数据存储的使用方法也是一项必备技能。
数据存储功能虽然表面上看起来并没有什么高深之处,但是实际操作起来,难度还是非常大的,因为随着FPGA芯片的集成度越来越高、FPGA设计的复杂度越来越大、业界对FPGA处理速度的期望越来越高,对于FPGA开发者使用数据存储的能力要求也越来越高。因此,在这样一个大形势下,数据存储的要求早已经不是仅仅能够将数据存下来、读出去如此这般简单,而是要求数据存储系统的吞吐量更大、反应速度更快、正确性更高、消耗的资源更少等等。因此为了让数据存储不成为整个FPGA设计的短板,必须要重视对数据存储掌控与使用。

数据存储的载体

提到数据存储,数据到底存到哪里去,是首先应该搞清楚的问题。事实上,具有记忆功能的任何东西,都可以用来做数据存储之用。在本章节,将为大家介绍FPGA开发者可以利用的数据存储的载体。

FPGA芯片内部的载体

先来看看FPGA芯片内部,都有哪些资源可以用来做数据存储的载体。

触发器

触发器,即FPGA逻辑资源块中的Flip-flop,它一般只在时钟的有效边沿到来时更新自己的状态(也有电平敏感型的触发器),而其他时间则会记忆并保持这个状态,因此触发器具有记忆功能,是一种最常用、最被人们熟知的数据存储载体资源。
触发器载体的特点是:一个Flip-flop的记忆力仅为1个bit,而由于规模的不同,一片FPGA芯片中的Flip-flop数量也从几千、几万甚至到几百万不等。

查找表

查找表,即FPGA逻辑资源块中的LUT,它一般用来实现组合逻辑,但是,其实它也是具有记忆功能的。首先,当用来实现组合逻辑时,LUT将会在程序上电的最开始,记住组合逻辑的真值表关系,从而在后续的FPGA工作中能够给出正确的功能响应;其次,FPGA的配置是基于SRAM的,即上电时的配置会在掉电后全部丢掉,而LUT中的内容是可配置的,因此每个LUT其实也就相当于一个小SRAM。由此可见,LUT具有记忆功能,并且它既然能够在每次上电的时候被配置成为不同的内容,那么也应该可以在上电期间被重新配置并保持,因此,LUT的记忆性也并不是单次的,所以在FPGA中LUT也是数据存储的一大利器,只不过当我们在FPGA中实现组合逻辑时,可能并没有意识到自己也是在使用数据存储功能罢了。这也是为什么可以毫不夸张的说,整个FPGA设计实际上最终实现出来几乎(时钟处理单元、DSP等资源不属于数据存储的范畴)就是一个大的数据存储体系。
LUT载体的特点是:若一个LUT的输入端口数为n,那么其记忆力应该为bits,而FPGA芯片中的LUT通常为3~6输入的。一般来说,一片FPGA芯片中的LUT数量和Flip-flop数量是相当的,因此LUT也被称为分布式存储。但需要注意的是,虽然绝大多数的LUT都是可以用来作为数据存储的,但并不是所有LUT都具有被当做小SRAM来使用的特征的。当然,还有些更高级的LUT,它们甚至还可以实现移位寄存器的功能。

块存储

块存储,即FPGA中的BLOCK RAM资源,简称BRAM,它实际上就是在FPGA芯片中嵌入了一些小型的存储器,自然也就是实现数据存储功能的主力军之一。这些BRAM往往也都比较灵活,每一片BRAM,都可以被配置为多种位宽和深度的组合,例如一个4Kbits容量的BRAM,可以配置成为位宽为1bit、深度为4k,也可以配置成为位宽为4bits、深度为1k,等等。
BRAM载体的特点是:一块BRAM的记忆力通常在1kbits、甚至几十kbits以上,而由于规模的不同,一片FPGA芯片中的BRAM数量也从十几、几十甚至到几百不等。

FPGA芯片外部的资源

除了FPGA芯片内部的载体之外,还有很多成熟的、专门的存储芯片可供我们来选择,包括但不限于——SRAM、DRAM、SDRAM、OTPROM、EPROM、EEPROM、FLASH、FIFO甚至硬盘等等。

数据存储的形式、实现及应用场合

如果将数据存储的载体比作不同种类的木材,那么数据存储的形式就好比功能不同的实木家具,例如椅子、桌子、床等等(不好意思,最近正在买家具)。而从使用形式方面来看,可以将数据存储分为几个大类,接下来,就为大家一一介绍FPGA芯片内部各种存储形式的特征、实现载体选择以及一些合适的应用场合。

寄存器

特征简介

在【共同语言篇->数字逻辑电路基础知识->数字逻辑功能单元->时序逻辑基本单元->锁存器与寄存器】小节中,我们介绍了,寄存器其实就是边沿敏感型的触发器。由于其在每一个时钟有效边沿都能完成一次存储的更新,其内部存储的数据可以被任意读取,并且还具有异步置0、置1的功能,因此使用起来非常的灵活、方便。

实现载体

首先,FPGA芯片中的触发器,基本上都是边沿敏感型的触发器,因此实现寄存器的首选载体便是FPGA芯片中的触发器资源。
其次,LUT也可以作为寄存器的实现载体,这是因为:身为时序单元的触发器,其本质也就是在组合逻辑电路中引入恰当的反馈得到的,而LUT是用来实现组合逻辑的,因此只要按照【共同语言篇->数字逻辑电路基础知识->数字逻辑功能单元->时序逻辑基本单元】中各种触发器的电路图来配置和连接若干个LUT,便可实现寄存器所需的功能,例如在【知己知彼篇->FPGA内部资源介绍->逻辑资源块】小节中,就介绍了一种利用LUT实现主从D触发器的方法。注意,一个LUT的记忆力虽然是一个触发器的很多倍,但是要模拟一个触发器的功能,却需要多个LUT协作完成,因此除非出现触发器资源不够用了等极端情况,一般不会考虑使用LUT来作为寄存器的载体。不过有一点例外,那就是有些高级的LUT可以被配置成为移位寄存器的形式,因此当需要移位寄存器的时候,采用这些高级的LUT效果往往更好。

应用场合

寄存器的应用可以说是无处不在:
首先,几乎时序逻辑中的所有中间变量都是用寄存器来存储的。
其次,寄存器也可以用来做一些小规模的数据缓存。
第三,寄存器还具有延迟、抗干扰等特性,因此还可以用来调整数据流中不同信号间的相对位置以及去除信号毛刺等作用。
最后,多个寄存器可以形成多位寄存器以及寄存器阵列,这其中最有典型意义的便是移位寄存器,具体细节可以参考【共同语言篇->数字逻辑电路基础知识->数字逻辑功能单元->时序逻辑基本单元->锁存器与寄存器】小节。

RAM

特征简介

RAM,英文全称:Random Access Memory,翻译成中文即——随机访问存储器,即我们可以在任意时刻向任意一个RAM地址写入数据或者从任意一个RAM地址读取数据。在RAM浩瀚的存储空间中,一般同一时刻最多只能访问RAM中的一个存储单元,这点与寄存器阵列有着本质的不同。

实现载体

首先,FPGA中的BRAM资源就是一个个独立的RAM,因此它们当然是实现RAM的首选载体。
其次,如果能够将多个LUT联合起来,其记忆力也是很客观的,因此LUT也是实现RAM的载体之一。
第三,寄存器阵列的功能是大于RAM的功能,因为同一时刻我们可以访问到寄存器阵列中的所有存储单元,因此,它也可以用来作为RAM的载体,不过由于它提供的功能远远大于RAM的需求,因此其对触发器资源的消耗是非常大的,因此通常不建议用寄存器阵列来实现RAM。

应用场合

当需要一些大容量的数据缓存时,RAM通常是首选。并且RAM是所有涉及到大量数据存储的形式的本源,后续的所有存储形式都是在RAM的基础上发展起来的,并且,由于RAM的随机访问特性,我们可以基于此开发出适合自己的、自定义的、特殊的数据存取方式。
值得注意的是,RAM的功能不仅仅限于数据的存储,因为它实际上就是一个大的查找表,可以用来实现任意的、变化无穷的逻辑功能。

ROM

特征简介

ROM,英文全称:Read-Only Memory,翻译成中文即——只读存储器。它是一种预先设定好存储内容,然后只允许进行读操作的存储结构。

实现载体

从ROM的特征介绍,我们可以看出,ROM其实就是RAM功能的一个子集,因此,触发器、查找表、块存储也都可以是ROM的实现载体。

应用场合

当FPGA设计的某些算法中,需要用到篇幅较大,且规律不明显的常数表时,ROM是首选方案。

FIFO

特征简介

FIFO,英文全称:First Input First Output,翻译成中文即——先进先出队列。我们可以以在车站排队买票为例来理解FIFO的行为:初始时,售票窗口没人,对应FIFO为空;过了一阵售票窗口有人排队,大家将按照先来后到的顺序排好,对应FIFO中数据存储的顺序;开始售票时,最先来的人最先购票,并且之后离开窗口,不再参与排队,对应FIFO的先进先出特性;当人很多时,售票大厅站满了人,再也容不下更多的人排队时,对应FIFO为满。由于FIFO具有先进先出的特性,所以操作FIFO的时候,我们端口中并不需要专门的地址输入端口。

实现载体

现实世界中,不存在一个容量可随数据元素多少而改变的硬件结构,因此FPGA中的FIFO大小其实都是固定的。即使在C语言中,我们可以通过指针以及节点的动态申请和释放的形式来实现好似容量可变的FIFO,但是这也仅仅是高级语言对固定大小内存硬件空间的抽象划分罢了。所以,真正的FIFO,并不是通过动态调整自己的存储空间来实现先进先出特性的,而是在RAM的基础上,通过适当控制读、写地址的变化,来营造一个先进先出的氛围。因此,从FIFO中读出的数据仍然存储在FIFO的空间当中,之所以我们再也访问不到,只不过是因为在写操作对它进行覆盖前,读地址是不可能再指向它了而已。
由此可见,触发器、查找表、块存储也都是FIFO的实现载体,同时,为了实现先进先出的功能,还必须使用一些触发器和查找表来组成RAM的读写控制逻辑。
最后,请注意,FIFO与移位寄存器有着本质的不同,移位寄存器虽然也是先进先出的,但它并不是一个队列,而更像是一个隧道!队列的长短会随着排队人数的多少而变化,如果你前面的人多,你出队列需要等待的时间就长,如果你前面没有人,那么你无须等待就可以出队列;但隧道的长短却与等待通过的人数没有任何关系,无论你是第一个进入隧道的,还是最后一个进入隧道的,你出隧道所需要的时间都完全相同,因为隧道带给我们的是一个固定的延迟。除此以外,FIFO的写入和读出操作是分开的,只要条件允许,一段时间内可以只进行读或者只进行写操作;但移位寄存器的读、写操作是紧密联系的,如果写入一个数据,那么它必然也会同时吐出一个数据,反之亦然。而且移位寄存器中的数据一旦读出,就真的消失了(这里特指串行移位寄存器)。综上所述,移位寄存器并不能成为FIFO的实现载体。

应用场合

受到先进先出性质的约束,FIFO的应用范围远没有RAM广泛,但是在FPGA设计中,FIFO的人气指数却要远远高于RAM,这其中最主要的原因就在于绝大部分的数据传递都是遵循先进先出特性的,而且直接使用成熟的FIFO模块远比使用RAM再配合编写读、写控制逻辑要来得简便得多。

STACK

特征简介

STACK,即是堆栈的意思。与FIFO的先进先出特性恰恰相反,STACK遵循的是后进先出的原则。因此STACK的特征可以用汉诺塔的游戏原理来理解,如果你实在不知道汉诺塔是怎么回事,那么冰糖葫芦总见过吧,最后串进去的山楂球将会是第一个被你吃掉的。

实现载体

除了读取数据的位置相差甚远外,STACK和FIFO的其他性质都是一样的,因此它也是基于RAM的,所以触发器、查找表、块存储也都是STACK的实现载体,同时,为了实现后进先出的功能,还必须使用一些触发器和查找表来组成RAM的读写控制逻辑。

应用场合

STACK的思想多用于软件编程的子程序调用中,FPGA中存储数据时比较少碰到,但是当你的算法什么时候需要一个数据存储的后进先出特性时,别犹豫,非它莫属了!

寄存器的HDL描述与用法

HDL描述

下面的HDL代码所描述的就是一个端口较为完整的寄存器:

-- VHDL example	
signal Q : std_logic;
process(clk)
begin
	if(R = '1')then
		Q <= '0';
	elsif(S = '1')then
		Q <= '1';
	elsif(clk'event and clk = '1')then
		Q <= D;
	end if;
end process; 

// Verilog example
reg Q;
always@(posedge R, posedge S, posedge clk)
begin
	if(R == 1'b1)
	begin
		Q <= 1'b0;
	end
	else if(S == 1'b1)
	begin
		Q <= 1'b1;
	end
	else
	begin
		Q <= D;
	end
end

从上例代码可以看出,它描述的实际是一个敏感时钟上升沿的且具有高电平有效的异步置0、置1信号的边沿D触发器,需要注意的一点是,如果该段代码所使用的目标FPGA芯片上的触发器并不符合代码所描述的所有性质,那么编译器将会引入额外的资源来实现。

HDL用法

虽然我们可以将上例的代码封装成为模块,然后在HDL代码中通过实例化的方式来调用使用,但是通常来说,这样做会略有画蛇添足之嫌,因为寄存器是HDL语法中两个最为基本的描述元素之一(另一个是线网),所以,只要确保声明正确、用在敏感时钟边沿的程序块中且赋值方式选择恰当,编译器实现出来的就会是一个寄存器。因此,寄存器的HDL描述同样也是寄存器的HDL用法。不过在实际使用中,并不一定要用到寄存器的所有异步置位端口,并且还有可能需要添加同步复位端口,不过我们仅需要根据需求进行HDL代码描述即可,而不需要费心思考虑具体的实现问题,因为那是编译器的工作。

单口RAM的HDL描述与用法

RAM并不是HDL语法所支持的基本描述元素,因此若要使用之,必先创造之!创造一个具有RAM功能的模块有两种方法:一、使用现成的IP核生成工具来生成;二、自己使用HDL语法来描述。
通常来说,IP核的性能会较我们自己编写的模块更好一些,但是了解一下如何用HDL来描述RAM会更有助于我们理解RAM的功能行为,并且有些时候,使用自己描述的RAM模块也是有优势的,例如可增强代码的可移植性。那么本章节将重点讨论单口RAM的HDL描述与用法。

HDL描述

单口RAM,即仅有一套操作接口的RAM,这套接口包括:一个数据写入总线、一个数据读出总线、一个地址总线和一些相关的读写控制信号。由于仅有一套操作接口,所以单口RAM的读、写操作可能存在着冲突,那么根据单口RAM在发生冲突时所采用的仲裁机制,又可以将单口RAM分为Write first、Read first、No change三种;按照单口RAM的读取时序,又可将其分为异步读、伪同步读、真同步读三种;除此以外,是否使用使能信号也会造成RAM行为的不同。那么接下来,就为大家介绍一些具有相关特点的单口RAM的HDL描述。

No change

No change,即是一种读、写互不干扰的模式,它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clk)
begin
	if clk'event and clk = '1' then
		if en = '1' then
			if we = '1' then
				RAM(conv_integer(addr)) <= dIn;
			else
				dOut <= RAM(conv_integer(addr)) ;
			end if;
		end if;
	end if;
end process;

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clk)
begin	
	if (en)
	begin
		if (we)
		begin
			RAM[addr] <= dIn;
		end
		else
		begin
			dOut <= RAM[addr];
		end
	end
end

通过上例可以看出,no change模式下的RAM是需要操作使能信号en的。当en有效时,如果we等于1,表示此次进行的是写操作;如果we等于0,表示此次进行的是读操作。由此可见、该模式下读、写操作其实是分开的,不可能同时发生,因此写操作对数据输出端口dOut没有任何影响,故该类型的RAM的工作模式称为no change。

Read first

Read first,即冲突时读优先的操作模式,它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clk)
begin
	if clk'event and clk = '1' then
		if en = '1' then
			if we = '1' then
				RAM(conv_integer(addr)) <= dIn;
			end if;
			dOut <= RAM(conv_integer(addr)) ;
		end if;
	end if;
end process;

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clk)
begin	
	if (en)
	begin
		if (we)
		begin
			RAM[addr] <=  dIn;
		end
		dOut <= RAM[addr];
	end
end

通过上例可以看出,当en有效时,RAM的读操作便开启,但是如果此时we信号也有效,那么RAM会针对当前写入数据的地址同时进行读、写操作从而产生冲突,而针对此引入的仲裁机制是dOut输出该地址的旧数据,因此虽然读、写操作是同时的,但是输出结果与no change模式下先读再写是一样的,故该类型的RAM的工作模式称为read first。

Write first

Write first,即冲突时写优先的操作模式,它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clk)
begin
	if clk'event and clk = '1' then
		if en = '1' then
			if we = '1' then
				RAM(conv_integer(addr)) <= dIn;
				dOut <= dIn;
			else
				dOut <= RAM(conv_integer(addr)) ;
			end if;
		end if;
	end if;
end process;

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clk)
begin	
	if (en)
	begin
		if (we)
		begin
			RAM[addr] <= dIn;
			dOut <= dIn;
		end
		else
		begin
			dOut <= RAM[addr];
		end
	end
end

通过上例可以看出,当en有效时,无论we信号有效与否,RAM都会进行读操作,因此当we信号有效时,RAM会针对当前写入数据的地址同时进行读、写操作从而产生冲突,而针对此引入的仲裁机制是dOut输出待写入的数据dIn,也即该地址的新数据,因此虽然读、写操作是同时的,但是输出结果与no change模式下先写再读是一样的,故该类型的RAM的工作模式称为write first。

异步读

异步读,即采用异步的方式从RAM中读取数据的操作模式,它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clk)
begin
	if clk'event and clk = '1' then
		if we = '1' then
			RAM(conv_integer(addr)) <= dIn;
		end if;
	end if;
end process;
dOut <= RAM(conv_integer(addr)) ;

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clk)
begin	
	if (we)
	begin
		RAM[addr] <= dIn;
	end				
end
assign dOut = RAM[addr];

通过上例可以看出,异步读模式下的RAM是不需要操作使能信号en的,当we信号有效时,将进行RAM的写操作,而无论在任何时候,dOut都会根据addr来对RAM进行异步读取。因此,当发生读、写冲突时,dOut会先输出旧值,当写操作执行完后,便会输出新值,故由于读取是异步的,所以此时的RAM输出是与仲裁方案无关的。

伪同步读

伪同步读,即是采用实则为异步但表现上却好似为同步的方式从RAM中读取数据的操作模式,它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clk)
begin
	if clk'event and clk = '1' then
		if we = '1' then
			RAM(conv_integer(addr)) <= dIn;
		end if;
		dOut <= RAM(conv_integer(addr)) ;
	end if;
end process;

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clk)
begin	
	if (we)
	begin
		RAM[addr] <= dIn;
	end			
	dOut <= RAM[addr];			
end

通过上例可以看出,伪同步读模式下,dOut的变化虽然是与时钟有效沿同步地,但是RAM的地址信号却仍然是异步的,故称为伪同步模式。

真同步读

真同步读,即采用真正的同步方式从RAM中读取数据的操作模式,它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clk)
begin
	if clk'event and clk = '1' then
		if we = '1' then
			RAM(conv_integer(addr)) <= dIn;
		end if;
		addrLock <= addr;
	end if;
end process;	
dOut <= RAM(conv_integer(addrLock)) ;	

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clk)
begin	
	if (we)
	begin
		RAM[addr] <= dIn;
	end			
	addrLock <= addr;			
end
assign	dOut = RAM[addrLock];

通过上例可以看出,真同步读模式下,RAM的读取地址信号是真正经过同步的,故称为真同步模式。

单口RAM的用法

无论是IP核,还是自己编写的HDL代码,单口RAM都可以用实例化的形式来调用使用,当然了,如果是自己用HDL代码描述的RAM,也可以在此基础上直接展开功能代码编写。
由于单口RAM有很多种,因此请根据所选用单口RAM的特性,采用相应的访问方式。但是有一点请注意,由于单口RAM只有一个地址总线,除非读取和写入恰恰总是需要针对同一地址进行操作,否则它们不能同时进行。因此,在绝大多数情况下,单口RAM在写入的时候不能完成期望的读取,而在读取的时候不能完成期望的写入,所以有效的读取和写入往往是时分复用RAM的,这样将会造成RAM数据吞吐量的减半。而根据单口RAM的这个特性,可以将单口RAM的操作大致分为几种模式:

零存零取模式

该模式适用于对RAM的读、写操作情况均是串行的、随机的、离散的、不规律的。例如程序在PC机上运行时,由于其串行执行的思路,所以同一时刻仅能对其数据空间进行读、写操作中的一种,并且代码的赋值操作一般比较杂散,因此也是无规律、随机、离散的。

零存整取模式

该模式适用于RAM的写操作是离散的、不定长的,但是读操作要求是连续的、定长的,因此当RAM中缓存了一定数量的内容后,才可以开始读操作,并且要有机制能够确保读操作期间不会再出现写请求。

整存零取模式

该模式适用于RAM的读操作是离散的、不定长的,但是写操作要求是连续的、定长的,因此当RAM中连续的写入一批内容后,才可以开始离散的读操作,并且要有机制能够确保写操作期间不会出现读请求。

整存整取模式

该模式适用于RAM的读、写操作均要求是连续的、定长的,但前提是必须有机制来确保批量的读、写操作是串行发生且时间上没有交叉的。该模式也是吞吐量最大的一种单口RAM操作模式。

长期存储模式

该模式适用于RAM中的内容会被全部更新一次,然后就被长期的、多次的使用的情况。例如在某些通信算法中,用来存储每隔一段时间就更新的密钥解密参数表等。

双口RAM的HDL描述与用法

双口RAM是一个具有两套完全独立的时钟线、数据线、地址线和读写控制线,并允许这两套独立的操作端口同时对该RAM进行随机性的访问,因此双口RAM通常又被称为共享式RAM。
双口RAM与单口RAM一样,可以通过现成的IP核生成工具生成,也可以通过自己编写HDL代码来实现。通常来说,IP核的性能会较我们自己编写的模块更好一些,但是了解一下如何用HDL来描述会更有助于我们理解双口RAM的功能行为。

HDL描述

双口RAM的每一套端口都是独立的,因此单独针对某一套端口来说,它的操作方式和单口RAM完全相同,因此单口RAM所具有的各个特点双口RAM全都具有,因此如果细分起来,双端口RAM的种类将会非常之多,所以这里将选出两种比较典型的双口RAM为大家进行介绍,更多的情况请大家参考单端口RAM的各个特征去自行分析。一种无使能控制端口、异步读的双口RAM的HDL描述范例,参考如下:

全双口异步读RAM描述

全双口异步读RAM,即两套独立端口都采用异步读的方式操作RAM,并且每一套端口都可以对RAM进行读、写操作。它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clkA)
begin
	if clkA'event and clkA = '1' then
		if weA = '1' then
			RAM(conv_integer(addrA)) <= dInA;
		end if;
	end if;
end process;		
process (clkB)
begin
	if clkB'event and clkB = '1' then
		if weB = '1' then
			RAM(conv_integer(addrB)) <= dInB;
		end if;
	end if;
end process;
dOutA <= RAM(conv_integer(addrA)) ;
dOutB <= RAM(conv_integer(addrB)) ;

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clkA)
begin	
	if (weA)
	begin
		RAM[addrA] <= dInA;
	end				
end
always@(posedge clkB)
begin	
	if (weB)
	begin
		RAM[addrB] <= dInB;
	end				
end
assign dOutA = RAM[addrA];
assign dOutB = RAM[addrB];

关于上例需要注意的一点是,由于双口RAM具有两个写端口,因此上述HDL代码中的RAM变量从语法角度上来说存在着赋值冲突,这通常会导致编译报错。因此,上述代码编译成功的前提是必须要在编译器中开启特定的编译器的选项或者宏。还有一点需要注意的是,

A写B读双口RAM

A写B读双口RAM,即RAM具有两套独立的操作端口A和B,其中A端口只负责RAM的写操作,而B端口只负责RAM的读操作。它的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
process (clkA)
begin
	if clkA'event and clkA = '1' then
		if we = '1' then
			RAM(conv_integer(addrA)) <= dIn;
		end if;
	end if;
end process;		
process (clkB)
begin
	if clkB'event and clkB = '1' then
		dOut <= RAM(conv_integer(addrB)) ;
	end if;
end process;

// Verilog example
reg [7:0] RAM [255:0];
always@(posedge clkA)
begin	
	if (we)
	begin
		RAM[addrA] <= dIn;
	end				
end
always@(posedge clkB)
begin	
	dOut <= RAM[addrB];				
end

双口RAM的用法

双端口RAM这种具有两套独立的操作端口的特性,使得其可被同时进行随机的读取和写入,而使用双口RAM作为数据缓存,无论是从吞吐量,还是从灵活性上来说,都远比单口RAM要好很多,因此,双口RAM的应用比单口RAM要更加广泛。
但是,虽说双口RAM的两套端口是独立的,但是毕竟它们操作的存储空间是共享的,因此如果发生两套端口都对同一地址空间进行操作的时候,冲突便会发生,此时无论仲裁机制如何,总会有一方的操作失效,因此在使用中请尽量杜绝出现两套端口操作同一个地址的情况。
虽然双口RAM有两套独立的操作端口,但一般并不会全部都派上用场,而根据使用的情况,又可细分为A写A读、A写B读、2读1写、2写1读、2写2读五种。其中A写A读的情况即退化为单口RAM,因此双口RAM兼容单口RAM的所有操作模式。除此以外,上述情况中最最常见的就是A写B读,此时若两套端口接入同一个时钟信号时,这样的双端口RAM也叫作同步RAM,否则称之为异步RAM。而无论是同步RAM还是异步RAM,它们的使用模式都是一样的,具体使用细节请参阅【本篇->编程思路->时钟及时钟域->跨时钟域问题->异步RAM法】小节,不过这里需要补充一点,那就是RAM是可以存储一次然后就无限次读取的数据存储结构,但FIFO却是存储一次后仅能读取一次的数据存储结构。

ROM的HDL描述与用法

创造ROM与创造RAM类似,可以用IP核、也可以用HDL描述,所不同的是,用HDL来描述ROM,往往更受青睐。

HDL描述

ROM,只读存储器,顾名思义,对其只能进行读操作而不能进行写操作。在现实世界中,可以在制作ROM存储芯片的时候,就预先通过相关手段对其内部各个存储单元进行信息注入,但是在FPGA的世界中,如果不利用“写操作”,我们该怎么完成这预先的数据注入呢?方法有两种——编写ROM数据文件、编写HDL初始化代码。前一种方法跟FPGA厂商及其编译器相关性比较大,一般在使用IP核生成ROM时配合使用,因此,在这里,我们主要介绍如何通过HDL的描述来构建ROM。
ROM的HDL描述范例如下:

-- VHDL example
type rom_type is array (15 downto 0) of std_logic_vector (7 downto 0);
	signal ROM: rom_type :=
	(
	X"00", X"01", X"02", X"03", X"04", X"05", X"06", X"07",
	X"10", X"11", X"12", X"13", X"14", X"15", X"16", X"17"
	);
	process (clk)
	begin
		if clk'event and clk = '1' then
			dOut <= ROM(conv_integer(addr)) ;
		end if;
	end process;

	// Verilog example
	reg [7:0] ROM[15:0];
	initial 
	begin
		ROM[0] = 8'h00;	ROM[1] = 8'h01;	ROM[2] = 8'h02;	ROM[3] = 8'h03; 
		ROM[4] = 8'h04;	ROM[5] = 8'h05;	ROM[6] = 8'h06;	ROM[7] = 8'h07;
		ROM[8] = 8'h10;	ROM[9] = 8'h11;	ROM[10] = 8'h12; 	ROM[11] = 8'h13; 
		ROM[12] = 8'h14;	ROM[13] = 8'h15;	ROM[14] = 8'h16; 	ROM[15] = 8'h17;
	end
	always@(posedge clk)
	begin	
		dOut <= ROM[addr];				
	end

如上范例的形式也可以用在RAM的初始化行为上。不过无论是ROM还是RAM,切忌使用复位的方式来初始化其内部的存储单元,因为在同一时刻,我们只能访问1个(单口)或2个(双口)ROM(RAM)的存储单元,因此若采用复位的形式来实现上例中ROM的数据注入,那么综合出来的将不是真正的ROM,而是寄存器阵列。例如下例HDL代码所描述的ROM,如果编译器优化的不好的话,其载体很可能将会是寄存器阵列,而非查找表或者块存储,但若以下代码是用于初始化一个RAM的,那么这个RAM的载体必将是寄存器阵列。

type ROM_type is array (15 downto 0) of std_logic_vector (7 downto 0);
signal ROM: ROM_type;
process (clk)
begin
	if clk'event and clk = '1' then
		if rst = '1' then
			ROM[0] <= X"00"; ROM[1] <= X"01";  ROM[2] <= X"02";  ROM[3] <= X"03"; 
			ROM[4] <= X"04"; ROM[5] <= X"05";  ROM[6] <= X"06";  ROM[7] <= X"07";
			ROM[8] <= X"10"; ROM[9] <= X"11";  ROM[10] <= X"12"; ROM[11] <= X"13"; 
			ROM[12] <= X"14";ROM[13] <= X"15"; ROM[14] <= X"16"; ROM[15] <= X"17";
			dOut <= X"00";
		else
			dOut <= ROM(conv_integer(addr)) ;
		end if;
	end if;
end process;

// Verilog example
reg [7:0] ROM[15:0];
always@(posedge clk)
begin	
	if (rst)
	begin			
		ROM[0] <= 8'h00;  ROM[1] <= 8'h01;  ROM[2] <= 8'h02;  ROM[3] <= 8'h03; 
		ROM[4] <= 8'h04;  ROM[5] <= 8'h05;  ROM[6] <= 8'h06;  ROM[7] <= 8'h07;
		ROM[8] <= 8'h10;  ROM[9] <= 8'h11;  ROM[10] <= 8'h12; ROM[11] <= 8'h13; 
		ROM[12] <= 8'h14; ROM[13] <= 8'h15; ROM[14] <= 8'h16; ROM[15] <= 8'h17;
		dOut <= X"00";
	end
	else
	begin
		dOut <= ROM[addr];			
	end	
end

ROM的用法

ROM的功能其实是RAM的一个子集,因此,可以参考RAM的读操作方式来使用ROM。不过虽说ROM是RAM的子集,但它却不会淡出FPGA的舞台,事实上它还经常会被用到,因为在我们进行FPGA开发时,尤其是开发具有一定理论算法的项目时,ROM就会常常受到青睐。例如,有太多的算法中都需要一些常数参数表,而用ROM来实现这些参数表是最恰当不过的了。

同步FIFO的HDL描述与用法

创造FIFO与创造RAM、ROM类似,可以用IP核、也可以用HDL描述,不过通常来说,还是建议大家使用现成的IP核来生成FIFO,因为FIFO的描述相对于RAM要复杂许多,不过为了增强大家对FIFO的认识,还是有必要简要介绍一下FIFO的HDL描述方法的。

HDL描述

FIFO是基于双口RAM衍生出来的,至于同步还是异步,则主要看双口RAM的两套独立端口是否使用了同一个时钟。通过【双口RAM的HDL描述与用法】章节的介绍,我们很容易联想到,FIFO的实现就是在A写B读的双口RAM操作模式基础上,通过添加适当的逻辑来实现FIFO先进先出的特性罢了。那么本小节,就给出一个简易的同步FIFO的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
signal wrAddr, rdAddr : std_logic_vector(7 downto 0);

full <= '1' when (wrAddr + 1) = rdAddr else '0';
empty <= '1' when wrAddr = rdAddr else '0';
elements <= wrAddr - rdAddr;

process (clk, aRst)
begin
	if aRst = '0' then
		wrAddr <= (others => '0');
		rdAddr <= (others => '0');
		dOut <= (others => '0');
	else 
		if clk'event and clk = '1' then
			if wrEn = '1' and full /= '1' then
				RAM(conv_integer(wrAddr)) <= dIn;
				wrAddr <= wrAddr + 1;
			end if;		
			if rdEn = '1' and empty /= '1' then
				dOut <= RAM(conv_integer(rdAddr))
				rdAddr <= rdAddr + 1;
			end if;
		end if;
	end if;
end process;

// Verilog example
reg [255:0] RAM[7:0];
reg [7:0] wrAddr, rdAddr;

assign full = ((wrAddr + 1'b1) == rdAddr) ? 1'b1 : 1'b0;
assign empty = (wrAddr == rdAddr) ? 1'b1 : 1'b0;
assign elements = wrAddr - rdAddr;

always @(posedge clk, negedge aRst) 
begin
	if (aRst == 1'b0)
	begin
		wrAddr <= 8'b0;
		rdAddr <= 8'b0;
		dOut <= 8'b0;
	end
	else
	begin
		if (wrEn == 1'b1 && full != 1'b1)
		begin
			RAM[wrAddr] <= dIn;
			wrAddr <= wrAddr + 1'b1;
		end		
		if (rdEn == 1'b1 && empty != 1'b1)
		begin
			dOut <= RAM[rdAddr];
			rdAddr <= rdAddr + 1'b1;
		end
	end
end

上例中,wrAddr、rdAddr分别是双口RAM独立的读、写地址,full、empty为该同步FIFO的空、满状态,elements表示当前FIFO中存储的有效元素个数。注意,为了简化逻辑和代码,暂不引入状态机(接下来的章节将会讲到)的概念,所以上例中的FIFO深度实际为255,而不是256。另外,实际的FIFO往往具有更多的控制端口,但是万变不离其宗,其基于双口RAM实现的思路是不会改变的。

同步FIFO的用法

无论是IP核,还是自己编写的HDL代码,同步FIFO都可以用实例化的形式来调用使用。由于所有逻辑都工作在一个时钟域内,因此同步FIFO使用起来更加容易,也没有太多注意事项。概括来说,同步FIFO和异步FIFO除了在控制双口RAM时使用的时钟不同以外,再没有别的区别,因此它们的使用方法并没有什么区别,请一并参考【本篇->编程思路->时钟及时钟域->跨时钟域问题->异步FIFO法】小节中的介绍。

异步FIFO的HDL描述与用法

强烈建议大家使用现成的IP核来生成异步FIFO,但是为了增强理解,本小节也会给出异步FIFO的描述方法。

HDL描述

异步FIFO的原理和同步FIFO类似,都是基于双口RAM来实现的,但是由于异步FIFO的读、写操作处于不同的时钟域,并且读、写时钟域之间还有信息传递,因此还要考虑跨时钟域问题,因此描述起来比同步FIFO要复杂许多。那么本小节,就给出一个简易的异步FIFO的HDL描述范例如下:

-- VHDL example
type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
signal aRstTmpW, aRstTmpR, aRstW, aRstR : std_logic;
signal wrAddr, grayWrAddr, sGrayRdAddr, sRdAddr : std_logic_vector(7 downto 0);
signal grayWrAddr, sRdAddrTmp : std_logic_vector(7 downto 0);
signal rdAddr, grayRdAddr, sGrayWrAddr, sWrAddr : std_logic_vector(7 downto 0);
signal grayRdAddr, sWrAddrTmp: std_logic_vector(7 downto 0);

-- flags
full <= '1' when (wrAddr + 1) = sRdAddr else '0';
empty <= '1' when sWrAddr = rdAddr else '0';
elementsForWr <= wrAddr - sRdAddr;
elementsForRd <= sWrAddr - rdAddr;

-- gray coding and decoding
grayWrAddrTmp <= wrAddr(7) & (wrAddr(6 downto 0) xor wrAddr(7 downto 1));
sRdAddrTmp(7) <= sGrayRdAddr(7);
sRdAddrTmp(6) <= sRdAddrTmp(7) xor sGrayRdAddr(6);
sRdAddrTmp(5) <= sRdAddrTmp(6) xor sGrayRdAddr(5);
sRdAddrTmp(4) <= sRdAddrTmp(5) xor sGrayRdAddr(4);
sRdAddrTmp(3) <= sRdAddrTmp(4) xor sGrayRdAddr(3);
sRdAddrTmp(2) <= sRdAddrTmp(3) xor sGrayRdAddr(2);
sRdAddrTmp(1) <= sRdAddrTmp(2) xor sGrayRdAddr(1);
sRdAddrTmp(0) <= sRdAddrTmp(1) xor sGrayRdAddr(0);

grayRdAddrTmp <= rdAddr(7) & (rdAddr(6 downto 0) xor rdAddr(7 downto 1));
sWrAddrTmp(7) <= sGrayWrAddr(7);
sWrAddrTmp(6) <= sWrAddrTmp(7) xor sGrayWrAddr(6);
sWrAddrTmp(5) <= sWrAddrTmp(6) xor sGrayWrAddr(5);
sWrAddrTmp(4) <= sWrAddrTmp(5) xor sGrayWrAddr(4);
sWrAddrTmp(3) <= sWrAddrTmp(4) xor sGrayWrAddr(3);
sWrAddrTmp(2) <= sWrAddrTmp(3) xor sGrayWrAddr(2);
sWrAddrTmp(1) <= sWrAddrTmp(2) xor sGrayWrAddr(1);
sWrAddrTmp(0) <= sWrAddrTmp(1) xor sGrayWrAddr(0);
						
--write clock domain
process (clkW)
begin
	if clkW'event and clkW = '1' then
		aRstTmpW <= aRst;
		aRstW <= aRstTmpW;
	end if;
end process;
process (clkW, aRstW)
begin
	if aRstW = '0' then
		wrAddr <= (others => '0');
		grayWrAddr <= (others => '0');
		sGrayRdAddr <= (others => '0');
		sRdAddr <= (others => '0');
	else 
		if clkW'event and clkW = '1' then
			if wrEn = '1' and full /= '1' then
				RAM(conv_integer(wrAddr))  <= dIn;
				wrAddr <= wrAddr + 1;
			end if;					
			grayWrAddr <= grayWrAddrTmp;
			
			sGrayRdAddr <= grayRdAddr;
			sRdAddr <= sRdAddrTmp;
		end if;
	end if;
end process;

--read clock domain	
process (clkR)
begin
	if clkR'event and clkR = '1' then
		aRstTmpR <= aRst;
		aRstR <= aRstTmpR;
	end if;
end process;
process (clkR, aRstR)
begin
	if aRstR = '0' then
		rdAddr <= (others => '0');
		grayRdAddr <= (others => '0');
		sGrayWrAddr <= (others => '0');
		sWrAddr <= (others => '0');
	else 
		if clkR'event and clkR = '1' then
			if rdEn = '1' and empty /= '1' then
				dOut <= RAM(conv_integer(rdAddr));
				rdAddr <= rdAddr + 1'b1;
			end if;					
			grayRdAddr <= grayRdAddrTmp;
			
			sGrayWrAddr <= grayWrAddr;
			sWrAddr <= sWrAddrTmp;
		end if;
	end if;
end process;

// Verilog example
reg [255:0] RAM[7:0];
reg aRstTmpW, aRstTmpR, aRstW, aRstR;
reg [7:0] wrAddr, grayWrAddr, sGrayRdAddr, sRdAddr;
wire [7:0] grayWrAddr, sRdAddrTmp;
reg [7:0] rdAddr, grayRdAddr, sGrayWrAddr, sWrAddr;
wire [7:0] grayRdAddr, sWrAddrTmp;

//flags
assign full = ((wrAddr + 1'b1) == sRdAddr) ? 1'b1 : 1'b0;
assign empty = (sWrAddr == rdAddr) ? 1'b1 : 1'b0;
assign elementsForWr = wrAddr - sRdAddr;
assign elementsForRd = sWrAddr - rdAddr;

// gray coding and decoding
assign grayWrAddrTmp= wrAddr ^ {wrAddr[7], wrAddr[7:1]};
assign sRdAddrTmp = {sGrayRdAddr[7], 
						^sGrayRdAddr[7:6],
						^sGrayRdAddr[7:5],
						^sGrayRdAddr[7:4],
						^sGrayRdAddr[7:3],
						^sGrayRdAddr[7:2],
						^sGrayRdAddr[7:1],
						^sGrayRdAddr[7:0]};
assign grayRdAddrTmp = rdAddr ^ {rdAddr[7], rdAddr[7:1]};
assign sWrAddrTmp = {sGrayWrAddr[7], 
						^sGrayWrAddr[7:6],
						^sGrayWrAddr[7:5],
						^sGrayWrAddr[7:4],
						^sGrayWrAddr[7:3],
						^sGrayWrAddr[7:2],
						^sGrayWrAddr[7:1],
						^sGrayWrAddr[7:0]};
						
//write clock domain
always@(posedge clkW)
begin
	aRstTmpW <= aRst;
	aRstW <= aRstTmpW;
end
always @(posedge clkW, negedge aRstW) 
begin
	if (aRstW == 1'b0)
	begin
		wrAddr <= 8'b0;
		grayWrAddr <= 8'b0;
		sGrayRdAddr <= 8'b0;
		sRdAddr <= 8'b0;
	end
	else
	begin
		if (wrEn == 1'b1 && full != 1'b1)
		begin
			RAM[wrAddr] <= dIn;
			wrAddr <= wrAddr + 1'b1;
		end					
		grayWrAddr <= grayWrAddrTmp;

		sGrayRdAddr <= grayRdAddr;
		sRdAddr <= sRdAddrTmp;
	end
end

//read clock domain	
always@(posedge clkR)
begin
	aRstTmpR <= aRst;
	aRstR <= aRstTmpR;
end	
always @(posedge clkR, negedge aRstR) 
begin
	if (aRstR == 1'b0)
	begin
		rdAddr <= 8'b0;
		grayRdAddr <= 8'b0;
		sGrayWrAddr <= 8'b0;
		sWrAddr <= 8'b0;
	end
	else
	begin
		if (rdEn == 1'b1 && empty != 1'b1)
		begin
			dOut <= RAM[rdAddr];
			rdAddr <= rdAddr + 1'b1;
		end					
		grayRdAddr <= grayRdAddrTmp;

		sGrayWrAddr <= grayWrAddr;
		sWrAddr <= sWrAddrTmp;
	end
end

上例中,wrAddr、rdAddr分别是双口RAM独立的读、写地址,full为该异步FIFO的写端满状态,empty为该异步FIFO的读端空状态,elementsForWr表示写端当前FIFO中存储的有效元素个数,elementsForRd表示读端当前FIFO中存储的有效元素个数。注意,为了简化逻辑和代码,暂不引入状态机(接下来的章节将会讲到)的概念,所以上例中的FIFO深度实际为255,而不是256。另,实际的FIFO往往具有更多的控制端口,但是万变不离其宗,其基于双口RAM实现的思路是不会改变的。

格雷码编码法的深层应用探讨

通过上一小节中的示例,我们可以看出,异步FIFO成功的关键就是利用了【本篇->编程思路->数字电路中的隐患->寄存器输出的不稳定态->特定情况下去除不稳定态的方法】小节中,关于格雷码的编码知识。也恰恰是因为如此,所以在上一小节的示例中,我们强制要求异步FIFO所对应的实际存储空间为256(2的8次幂,即0~255),这样读、写地址计数器从最大值溢出到最小值时,也能保证格雷码的1bit跳变特性。可是,很多时候,我们希望随意的去定义异步FIFO的存储深度,例如存储深度为133、97、10001这样的值。此时,按照格雷码编码公式得到的地址的最大、最小值之间就很可能不满足1bit跳变的特性。
虽然在【本篇->编程思路->数字电路中的隐患->寄存器输出的不稳定态->特定情况下去除不稳定态的方法->从卡诺图看格雷码编码的非唯一性】小节的最后,我们简单地提到了如何利用卡诺图得到集合空间元素数为非2的整数次幂的格雷码集合,但是:
首先,当元素数为非2的整数次幂时,格雷码集合并不总是存在的(该小节中有过证明);
其次,即便对于某一个非2的整数次幂元素数,存在格雷码集合,但由于FIFO的深度通常都较大,所以要想利用卡诺图进行求解几乎不可能;
第三,即便通过某种渠道求出该格雷码集合,但是由于不是基于公式生成的,则需要建立编、解码查表表格,非常的消耗资源。
鉴于以上三点,本小节将为大家介绍两种任意深度异步FIFO的解决办法,也顺便为大家剖析一下格雷码编码法的深层次应用。

瞒天过海法

“瞒天过海”是三十六计中的第一计,含有表面一套背地一套的意思。而对于异步FIFO的任意存储深度实现来说,“瞒天过海”指的是表面上看起来你使用的是一个存储深度为n的异步FIFO,可实际上FIFO中缓存的容量仍是一个大于等于n但却是2的整数次幂的数。例如,你需要一个存储深度为190的FIFO,可事实上FIFO中的缓存容量为256,只不过当存储了190个数据后,通过逻辑控制来禁止继续写入新数据罢了,这样一来,读、写地址就可以取满8bits所能表示的256个元素(0~255),从而通过公式法得出的格雷码也满足地址最大、最小值相差1bit的要求。针对本章节的例子,只需要修改产生full信号的语句如下,即可得到一个使用深度为190的异步FIFO:

	-- VHDL example
full <= '1' when (wrAddr + 66) = sRdAddr else '0'; -- the same as wrAddr - sRdAddr = 190

	// Verilog example
	assign full = ((wrAddr + 8'd66) == sRdAddr) ? 1'b1 : 1'b0; 
	// the same as (wrAddr - sRdAddr) == 8'd190

这种做法常见于基于BLOCK RAM资源创建的异步FIFO,这是因为一个BRAM单元的存储容量往往是固定的,且无论在什么位宽配置模式下,其存储容量都是2的整数次幂。所以,只要给你一个(或者若干个聚合在一起,但此时一定也得是2的整数次幂个BRAM)容量够大的BRAM,就可以轻松利用“瞒天过海”法实现你所需要的异步FIFO存储深度。

双计数器法

瞒天过海法有一个缺点,那就是很可能会导致很大的资源浪费。例如,如果你之前用128个存储单元实现了存储深度为128的异步FIFO(注意,本章节中的例子为了逻辑判断方便,不引入状态机,所以存储深度为存储单元数减1),那么当你需要实现存储深度为129的异步FIFO时,至少需要将存储单元数扩充到256个,这样会造成至少127个存储单元的浪费。
那么,如果我们手头上的确有一个只含有特定个存储单元的双口RAM,那么由于读、写地址的取值空间并不能覆盖该地址位宽向量的所有可能取值,所以若直接沿用格雷码的编码方法,则很可能导致地址的最大、最小值发生多于1bit的跳变,从而造成异步FIFO的行为错误。
鉴于上述描述,这里再介绍一种使用双计数器的方法,来实现安全的跨时钟域读、写信息传递。
在本章节的异步FIFO描述示例中,我们可以看到,读、写异步FIFO时,我们各采用了一个计数器来产生递增的读地址和写地址。而所谓的双计数器法,就是在读、写异步FIFO时,各采用两个计数器:一个给本时钟域用,用来产生地址;另一个给其他时钟域用,用来反映读或写入元素的总数。由于另一个计数器仅反映读或写入的元素总数,因此就跟实际的存储单元个数无关,并且由于写总数肯定大于等于读总数、小于等于读总数和FIFO深度之和,所以该计数器的计数范围就可以设计成为2的整数次幂,并且位宽也只需和地址计数器一样即可。下面,仍以一个深度为190的异步FIFO为例,给出参考代码如下:

-- VHDL example
type ram_type is array (190 downto 0) of std_logic_vector (7 downto 0);
signal RAM: ram_type;
signal aRstTmpW, aRstTmpR, aRstW, aRstR : std_logic;
signal wrAddr, rdAddr : std_logic_vector(7 downto 0);

signal wrCnt, grayWrCnt, sGrayRdCnt, sRdCnt : std_logic_vector(7 downto 0);
signal grayWrCnt, sRdCntTmp : std_logic_vector(7 downto 0);
signal rdCnt, grayRdCnt, sGrayWrCnt, sWrCnt : std_logic_vector(7 downto 0);
signal grayRdCnt, sWrCntTmp: std_logic_vector(7 downto 0);

-- flags
full <= '1' when (wrCnt + 66) = sRdCnt else '0'; -- the same as wrCnt - sRdCnt = 190
empty <= '1' when sWrCnt = rdCnt else '0';
elementsForWr <= wrCnt - sRdCnt;
elementsForRd <= sWrCnt - rdCnt;

-- gray coding and decoding
grayWrCntTmp <= wrCnt(7) & (wrCnt(6 downto 0) xor wrCnt(7 downto 1));
sRdCntTmp(7) <= sGrayRdCnt(7);
sRdCntTmp(6) <= sRdCntTmp(7) xor sGrayRdCnt(6);
sRdCntTmp(5) <= sRdCntTmp(6) xor sGrayRdCnt(5);
sRdCntTmp(4) <= sRdCntTmp(5) xor sGrayRdCnt(4);
sRdCntTmp(3) <= sRdCntTmp(4) xor sGrayRdCnt(3);
sRdCntTmp(2) <= sRdCntTmp(3) xor sGrayRdCnt(2);
sRdCntTmp(1) <= sRdCntTmp(2) xor sGrayRdCnt(1);
sRdCntTmp(0) <= sRdCntTmp(1) xor sGrayRdCnt(0);

grayRdCntTmp <= rdCnt(7) & (rdCnt(6 downto 0) xor rdCnt(7 downto 1));
sWrCntTmp(7) <= sGrayWrCnt(7);
sWrCntTmp(6) <= sWrCntTmp(7) xor sGrayWrCnt(6);
sWrCntTmp(5) <= sWrCntTmp(6) xor sGrayWrCnt(5);
sWrCntTmp(4) <= sWrCntTmp(5) xor sGrayWrCnt(4);
sWrCntTmp(3) <= sWrCntTmp(4) xor sGrayWrCnt(3);
sWrCntTmp(2) <= sWrCntTmp(3) xor sGrayWrCnt(2);
sWrCntTmp(1) <= sWrCntTmp(2) xor sGrayWrCnt(1);
sWrCntTmp(0) <= sWrCntTmp(1) xor sGrayWrCnt(0);
						
--write clock domain
process (clkW)
begin
	if clkW'event and clkW = '1' then
		aRstTmpW <= aRst;
		aRstW <= aRstTmpW;
	end if;
end process;
process (clkW, aRstW)
begin
	if aRstW = '0' then
		wrAddr <= (others => '0');
		wrCnt <= (others => '0');
		grayWrCnt <= (others => '0');
		sGrayRdCnt <= (others => '0');
		sRdCnt <= (others => '0');
	else 
		if clkW'event and clkW = '1' then
			if wrEn = '1' and full /= '1' then
				RAM(conv_integer(wrAddr))  <= dIn;
				
				if(wrAddr == 190)then
					wrAddr <= (others => '0');
				else
					wrAddr <= wrAddr + 1;
				end if;
				
				wrCnt <= wrCnt + 1;
			end if;					
			grayWrCnt <= grayWrCntTmp;
			
			sGrayRdCnt <= grayRdCnt;
			sRdCnt <= sRdCntTmp;
		end if;
	end if;
end process;

--read clock domain	
process (clkR)
begin
	if clkR'event and clkR = '1' then
		aRstTmpR <= aRst;
		aRstR <= aRstTmpR;
	end if;
end process;
process (clkR, aRstR)
begin
	if aRstR = '0' then
		rdAddr <= (others => '0');
		rdCnt <= (others => '0');
		grayRdCnt <= (others => '0');
		sGrayWrCnt <= (others => '0');
		sWrCnt <= (others => '0');
	else 
		if clkR'event and clkR = '1' then
			if rdEn = '1' and empty /= '1' then
				dOut <= RAM(conv_integer(rdAddr));
				
				if(rdAddr == 190)then
					rdAddr <= (others => '0');
				else
					rdAddr <= rdAddr + 1;
				end if;
				
				rdCnt <= rdCnt + 1;
			end if;					
			grayRdCnt <= grayRdCntTmp;
			
			sGrayWrCnt <= grayWrCnt;
			sWrCnt <= sWrCntTmp;
		end if;
	end if;
end process;

// Verilog example
reg [190:0] RAM[7:0];
reg aRstTmpW, aRstTmpR, aRstW, aRstR;
reg [7:0] wrAddr, rdAddr;
reg [7:0] wrCnt, grayWrCnt, sGrayRdCnt, sRdCnt;
wire [7:0] grayWrCnt, sRdCntTmp;
reg [7:0] rdCnt, grayRdCnt, sGrayWrCnt, sWrCnt;
wire [7:0] grayRdCnt, sWrCntTmp;

//flags
assign full = ((wrCnt + 8'd66) == sRdCnt) ? 1'b1 : 1'b0; 
// the same as (wrCnt - sRdCnt) == 8'd190
assign empty = (sWrCnt == rdCnt) ? 1'b1 : 1'b0;
assign elementsForWr = wrCnt - sRdCnt;
assign elementsForRd = sWrCnt - rdCnt;

// gray coding and decoding
assign grayWrCntTmp= wrCnt ^ {wrCnt[7], wrCnt[7:1]};
assign sRdCntTmp = {sGrayRdCnt[7], 
						^sGrayRdCnt[7:6],
						^sGrayRdCnt[7:5],
						^sGrayRdCnt[7:4],
						^sGrayRdCnt[7:3],
						^sGrayRdCnt[7:2],
						^sGrayRdCnt[7:1],
						^sGrayRdCnt[7:0]};
assign grayRdCntTmp = rdCnt ^ {rdCnt[7], rdCnt[7:1]};
assign sWrCntTmp = {sGrayWrCnt[7], 
						^sGrayWrCnt[7:6],
						^sGrayWrCnt[7:5],
						^sGrayWrCnt[7:4],
						^sGrayWrCnt[7:3],
						^sGrayWrCnt[7:2],
						^sGrayWrCnt[7:1],
						^sGrayWrCnt[7:0]};
						
//write clock domain
always@(posedge clkW)
begin
	aRstTmpW <= aRst;
	aRstW <= aRstTmpW;
end
always @(posedge clkW, negedge aRstW) 
begin
	if (aRstW == 1'b0)
	begin
		wrAddr <= 8'd0;
		wrCnt <= 8'b0;
		grayWrCnt <= 8'b0;
		sGrayRdCnt <= 8'b0;
		sRdCnt <= 8'b0;
	end
	else
	begin
		if (wrEn == 1'b1 && full != 1'b1)
		begin
			RAM[wrAddr] <= dIn;
			
			if(wrAddr == 8'd190)
			begin
				wrAddr <= 8'd0;
			end
			else
			begin
				wrAddr <= wrAddr + 1'b1;
			end
			
			wrCnt <= wrCnt + 1'b1;
		end					
		grayWrCnt <= grayWrCntTmp;

		sGrayRdCnt <= grayRdCnt;
		sRdCnt <= sRdCntTmp;
	end
end

//read clock domain	
always@(posedge clkR)
begin
	aRstTmpR <= aRst;
	aRstR <= aRstTmpR;
end	
always @(posedge clkR, negedge aRstR) 
begin
	if (aRstR == 1'b0)
	begin
		rdAddr <= 8'd0;
		rdCnt <= 8'b0;
		grayRdCnt <= 8'b0;
		sGrayWrCnt <= 8'b0;
		sWrCnt <= 8'b0;
	end
	else
	begin
		if (rdEn == 1'b1 && empty != 1'b1)
		begin
			dOut <= RAM[rdAddr];
			
			if(rdAddr == 8'd190)
			begin
				rdAddr <= 8'd0;
			end
			else
			begin
				rdAddr <= rdAddr + 1'b1;
			end
			
			rdCnt <= rdCnt + 1'b1;
		end					
		grayRdCnt <= grayRdCntTmp;

		sGrayWrCnt <= grayWrCnt;
		sWrCnt <= sWrCntTmp;
	end
end

注意,由于在这里不想过早引入状态机的概念,所以为了判断方便,本例仍然采用之前的惯例,即分配给FIFO的存储空间比深度需求多1,虽然对于本例这并没有必要。
对照上例,可以看出,每成功写一次异步FIFO,wrAddr和wrCnt都在进行循环自增计数;而每成功读一次异步FIFO,rdAddr和rdCnt都在进行循环自增计数。所以,虽然经过191次计数后,wrAddr已经和wrCnt没有什么对应关系,rdAddr也已经和rdCnt没有什么对应关系,但是wrCnt与rdCnt的差却很好的反映出该异步FIFO中的实际元素数(由于这两个计数器都是满量程计数,所以不怕溢出情况),故只需要用格雷码的方法来处理这两个计数器即可。
这种双计数器的做法常见于基于寄存器、LUT等资源创建的异步FIFO,因为:首先,这两种资源比较宝贵,容不得你随便浪费;其次,每个寄存器的存储容量为1bit,每个LUT的存储容量在4bits到几十个bits不等,因此你可以根据FIFO深度需要来构造出几乎没有冗余的存储空间。

异步FIFO的用法

无论是IP核,还是自己编写的HDL代码,异步FIFO都可以用实例化的形式来调用使用。由于读、写逻辑工作在不同时钟域内,因此异步FIFO使用起来要特别注意,即当读操作时,一定要判断和读时钟同步的标志信号,虽然这些信号由于跨时钟域处理会稍有滞后,但是能够确保操作的成功性和正确性;反之也应如此。

FIFO使用小技巧之冗余法

FIFO与RAM的一个不同之处就在于:当你从FIFO中读出一个数据后,这个数据就从FIFO中“消失了”。当然了,这里的“消失了”并不是数据真正的不存在了,而是已读出的数据无法再次通过FIFO提供的接口被访问到而已,因为FIFO归根到底也是由RAM加上一部分控制逻辑实现的,只要对应存储单元不被新的数据覆盖,那么被读出的数据就依然存在在FIFO中。对于FIFO的这个特点,我姑且在这里称之为“FIFO的数据消失性”。
也许你对“FIFO的数据消失性”并不在意,因为你很清楚的知道,既然选择了FIFO来充当数据存储的载体,那么你所需要存储的数据内容肯定不会有需要被重复访问的时候;或者即便是有,那么大可以把需要重复访问的内容从FIFO中读出然后放到寄存器中,以备以后访问。否则的话,那么大可采用RAM的结构来实现数据的存储,何必采用FIFO呢?没错,如果数据包的内容不需要或者绝大部分都不需要被重复访问的时候,FIFO凭借其简单的接口操作赢得了众多设计者的青睐,而且“FIFO的数据消失性”看起来或者在实践中也并不会给我们的设计带来什么问题。并且事实上,需要被存储的数据大多是视频、图像、声音、文字等等信息的码流,这些码流是不需要、也不应该需要被重复访问的,即便是需要,那么这部分功能很可能也是不应该放在FPGA内部来完成的。因此,我们通常在使用FIFO时,“FIFO的数据消失性”通常不会给我们造成什么困扰。
但是,当设计的工作时钟频率越来越高的时候,或者你的BOSS对设计的性能要求越来越苛刻的时候,你会发现“FIFO的数据消失性”是那么的讨厌!例如,以前你的设计满负荷1秒钟只能处理10幅图片,现在要达到满负荷1秒钟处理20幅图片,那么原来工作在50MHz时钟频率下的设计,现在需要能够在100MHz下也能正常工作。速度向来是FPGA设计最大的杀手,因为它直接关系到FPGA时序逻辑的一个至关重要的时序指标——建立时间,所以对于FPGA来说,同样一个功能,在50MHz下和100MHz下实现起来难度是截然不同的。
这里我们不去展开来讨论,只把目光集中到如何连续的从FIFO中读取一个不定长的数据包的问题上去。
当处理速度提高导致FIFO时序不满足时,对FIFO接口的处理有一个简单而有效的做法就是对FIFO的所有接口信号进行缓存。例如,以前例化FIFO时,直接把内部信号连到FIFO的各个端口上,示例代码如下:

	-- VHDL example
	signal dataIn, dataOut : std_logic_vector(15 downto 0);
	signal wrEn, rdEn : std_logic;
	fifo0: myfifo 
	PORT MAP(
		clk => clk,
		din => dataIn, 
		rd_en => rdEn,
		srst => srst, 
		wr_en => wrEn,
		dout => dataOut);
	
	// Verilog example
	wire [15:0] dataIn, dataOut;
	wire wrEn, rdEn;
	myfifo  fifo0 (
		 	.clk(clk),
		 	.din(dataIn),
		 	.rd_en(rdEn),
		 	.srst(srst),
		 	.wr_en(wrEn),
		 	.dout(dataOut));

	加上缓存后,代码如下:
	-- VHDL example
	signal dataIn, dataOut : std_logic_vector(15 downto 0);
	signal wrEn, rdEn : std_logic;
	signal din, dout : std_logic_vector(15 downto 0);
	signal rd_en, wr_en : std_logic;
	--FIFO写接口缓存
	process (clk)
	begin
		if(clk'event and clk = '1')then
			wr_en <= wrEn;
			din <= dataIn;
		end if;
	end process;
	--FIFO读接口缓存
process (clk)
	begin
		if(clk'event and clk = '1')then
			rd_en <= rdEn;
			dout <= dataOut;
		end if;
	end process;
	fifo0: myfifo 
	PORT MAP(
		clk => clk,
		din => dataIn, 
		rd_en => rdEn,
		srst => srst, 
		wr_en => wrEn,
		dout => dataOut);
	
	// Verilog example
	wire [15:0] dataIn, dataOut;
	wire wrEn, rdEn;
	reg [15:0] din, dout;
	reg rd_en, wr_en;
	//FIFO写接口缓存
	always@(posedge clk)
	begin
		wr_en <= wrEn;
		din <= dataIn;
	end
	//FIFO读接口缓存
	always@(posedge clk)
	begin
		rd_en <= rdEn;
		dout <= dataOut;
	end
	myfifo  fifo0 (
		 	.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数据才会传递给寄存器dout。这也就是说每当发出一次读请求的时候,要经历3个时钟周期后才能得到想要的数据。这滞后的3个周期就是这种简单又有效的方法的一个重大隐患,接下来我们会分析由这个隐患所引起的所谓的“FIFO的数据消失性”给设计带来的难题。
如果FIFO内部存储的数据包是定长的,即每次只需要读取固定长度的数据出来的话,那么每次读FIFO只需要连续给出N个有效使能后然后滞后第一个使能3个时钟周期后收获数据即可。但是如果需要读出来的数据是不定长的该怎么办呢?
一般,不定长的数据包有两种:1、数据包的包头固定位置会存有数据包的长度;2、数据包会以某种固定字节模式结尾,例如0x1122。
当连续读取FIFO时,这两种数据包结构都会产生致命问题。对于第一种数据包,如果包长度信息位于第3个字节,那么当接收到第三个字节时,如果发现当前包长度是5以内,那么就会由于来不及撤销读使能而导致“FIFO的数据消失性”发生。对于第二种数据包结构,由于结尾字节模式何时到来无法预期,必然会导致“FIFO的数据消失性”发生。
一旦“FIFO的数据消失性”发生,那么在下次读取FIFO中的数据时,得到的必然是一个不完整的数据包,如此往复将导致每次读取都得到的是错误的数据包结构,从而使得设计失败。
也许有人会说,那就不要连续读取好了,每次发送一个有效的读使能,3个时钟周期后根据收到的数据判断是否需要发送下一个读使能,这样不就可以避免“FIFO的数据消失性”发生了么?没错,这种方法的确可以避免隐患的发生,但是,如果这样做的话就证明我们没有搞懂提高工作时钟频率的意义——提高工作时钟频率最大的好处就是提高了设计的性能,对于FIFO来说,就是要提高吞吐量。如果在50MHz频率下每个时钟周期可以从FIFO中读取一个数据,而在100MHz下每3个时钟周期才可以从FIFO中读取一个数据,那么我们把时钟频率从50MHz提高到100MHz的意义何在?因此,保证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中高速且连续的读取一个不定长的数据包的思路和黑客利用缓冲区溢出bug对我们的PC进行攻击时,通过写以一大段NOP开头的破坏程序来增加破坏程序执行命中率有着异曲同工之处,只不过“冗余法”采用的是一小段。

STACK的HDL描述与用法

STACK的结构在FPGA中并不常用,因此一般鲜有现成的IP核,所以本章节主要介绍一下STACK的HDL描述和使用方法。

HDL描述

STACK,后进后出,即读到得是当前缓存中最后一个写进去的数据,因此这种结构肯定不支持同时读、写,因为如果同时读、写的话,就无法保证读出来的是最后写进去的了。根据以上推断,应该可以看出STACK是在单口RAM的基础上添加一些控制逻辑来实现其后进先出的特性罢了。那么本小节,就给出一个简易的STACK的HDL描述范例如下:

-- VHDL example
	type ram_type is array (255 downto 0) of std_logic_vector (7 downto 0);
	signal RAM: ram_type;
	signal addr : std_logic_vector(7 downto 0);

	full <= '1' when addr = X"FF" else '0';
	empty <= '1' when addr = x"00" else '0';
	elements <= addr;

	process (clk, aRst)
	begin
		if aRst = '0' then
			addr <= (others => '0');
			dOut <= (others => '0');
		else 
			if clk'event and clk = '1' then
				if wrEn = '1' and full /= '1' then
					RAM(conv_integer(addr)) <= dIn;
					addr <= addr + 1;
				elsif rdEn = '1' and empty /= '1' then
					dOut <= RAM(conv_integer(addr)-1)
					addr <= addr - 1;
				end if;
			end if;
		end if;
	end process;

	// Verilog example
	reg [255:0] RAM[7:0];
	reg [7:0] addr;
	
	assign full = (addr == 8'd255) ? 1'b1 : 1'b0;
	assign empty = (addr == 8'd0) ? 1'b1 : 1'b0;
	assign elements = addr;

	always @(posedge clk, negedge aRst) 
	begin
		if (aRst == 1'b0)
		begin
			addr <= 8'b0;
			dOut <= 8'b0;
		end
		else
		begin
			if (wrEn == 1'b1 && full != 1'b1)
			begin
				RAM[addr] <= dIn;
				addr <= addr + 1'b1;
			end		
			else if (rdEn == 1'b1 && empty != 1'b1)
			begin
				dOut <= RAM[addr - 1'b1];
				addr <= addr - 1'b1;
			end
		end
	end

上例中,addr就是单口RAM独立的读、写地址,full、empty为该STACK的空、满状态,elements表示当前STACK中存储的有效元素个数。注意,为了简化逻辑和代码,暂不引入状态机(接下来的章节将会讲到)的概念,上例中的FIFO深度实际为255,而不是256。

STACK的用法

上述描述STACK的代码可以通过例化或者直接嵌入HDL代码中来使用,由于STACK是单口RAM的一个子集,因此其具体用法、模式可以参考单口RAM章节的说明。

外部存储芯片的HDL描述与用法

HDL描述

外部存储芯片是成品芯片,例如SRAM、SDRAM、FLASH等等,它们是存在于电路板上实实在在的存储载体,是不需要我们用HDL或者别的手段进行创造的。但是,为了能够顺利的和这些外部存储芯片打交道,FPGA的开发者们往往需要根据不同芯片的特性去编写其对应的控制器代码。事实上,对于某些外部芯片来说,其控制器的HDL描述还是十分复杂的,例如对于SDRAM,无论是读操作还是写操作,都要先对芯片进行命令发送、地址发送、预充电、数据发送(接收)、充电关闭等操作,并且在芯片上电后先要进行一连串初始化,并且需要在芯片工作中以适当间隔给以刷新操作等等。
因此,使用外部存储芯片往往比使用FPGA内部载体实现RAM或FIFO等存储结构要复杂得多,不过其优势就在于可以提供较大容量的数据缓存。鉴于可作为外部存储介质的载体品种繁多,操作方式大相迳庭,因此在这里就不做具体的HDL描述举例,大家在实际使用中,请务必严格遵守这些存储芯片的器件手册来完成控制器编写。

外部存储芯片的用法

外部存储芯片说到底,也是RAM或者FIFO等数据存储形式的载体,因此之前关于单、双口RAM等的用法介绍全部都适用于外部存储芯片。不过当涉及到大容量存储时,外部存储芯片就变得必不可少,并且通常来说,连续地读或者写操作要比单个的读或者写操作的效率高出很多。

数据存储的使用思路

数据存储是FPGA设计中的重头戏,以下给出了一种在FPGA设计中使用数据存储的建议思路,请大家参考。

step 1——确定存储需求

当刚刚开始接手一个FPGA项目时,需要根据情况确定是否需要使用存储。通常来说,只要不是纯组合逻辑设计,必然会用到寄存器,但是如果有一些比较大量的数据缓存需求,就决定着应该会采用RAM、FIFO等结构。但是有时候,即使没有这样的需求,RAM和FIFO的数据缓存也不一定就不需要,因为并不是只有涉及到数据的存储操作才会用到数据存储,例如,乘法器、波形发生器等等,也都可以通过查表的方式来实现,而表的内容就需要使用RAM等结构来存储,更概括的来说,任意的组合逻辑功能都可以用RAM来实现的,因为只需要用RAM来存储该组合逻辑的真值表即可。因此FPGA项目中是否会用到数据存储主要取决于两方面:一、是否确实有数据需要存储;二、是否想将某些逻辑功能用查表的方式来实现。

step 2——确定存储方案

一旦确定需要使用数据存储,那么接下来就要根据需求来考虑存储方案的问题了。以下是考虑数据存储方案时的几点建议:
1、根据数据访问的模式,决定使用寄存器阵列或者RAM、FIFO等,因为如果需要同时访问一个存储模块中的多个(两个以上)存储单元或者对数据流完成固定的周期延迟的话,寄存器阵列往往是最佳选择。
2、根据数据存、取的要求和速度,决定采用单一的存储模块,亦或是两个存储模块进行乒、乓操作,甚至是多个存储模块联合操作等。例如,如果仅仅是连续、顺序存储一些数据,然后再连续、顺序的读出,那么单一的RAM或者FIFO就可以满足;如果存储的数据紧接着就需要被处理,但是存储模块又是不能同时被读写的,那么这时候就需要使用到双缓冲机制来支持连续不断的数据流处理,而双缓冲机制的操作就遵循着乒、乓的形式,即写缓存1的时候,缓存2中的数据可以被读出和处理,而当写缓存2的时候,缓存1中的数据可以被读出和处理,只要掌握好恰当的节拍切换,就能从宏观上实现连续不断的数据流,因此双缓冲的乒、乓操作机制多用于对片外存储芯片的处理上;若存储模块中的内容需要有规律的非顺序写入或读取,或读、写数据宽度需求不同时,往往引入多缓冲机制能够更好的解决问题,多缓冲机制使用时往往更加灵活多变,因此也请根据实际情况考虑周全。
3、根据数据流的稳定性与存储操作的容错性,决定采用RAM模式还是FIFO模式。通常来说,RAM的容错性和抗干扰性要好于FIFO。例如,如果我们现在缓存的是数据包,每个数据包的大小均为100个数据,可是由于一些原因,某一个数据包多了或者少了1个数据,如果使用FIFO的话,如果仍按照100个来读取的话,那么这一个错误将会扩散到后续所有的数据包中,造成群体错误,可是如果使用RAM的话,固定从地址0、100、200、……开始数据包的存入和读出,那么错误将不会被扩散。再看一例,仍然是缓存数据包的,每个数据包长度仍为100,不过由于一些原因,该数据包的最后一个数据表明该包数据的有效性,若有效则缓存,无效则丢弃,针对这种情况,如果用RAM的话,可以很简单的通过写地址回调来完成数据包的丢弃功能,而如果用FIFO的话,必须先用一个小FIFO缓存当前数据包,若该包需要存储,则送至后续大FIFO中,否则从小FIFO中连续读取100个数据并丢弃。因此,RAM相对于FIFO来说具有更好的容错性和抗干扰性,但是由于FIFO的操作不需要显式的控制地址,因此对于通信质量有保证,且无特殊需求的数据存储来说,FIFO要比RAM的具有更好的易用性。

step 3——确定存储载体

在【数据存储的形式、实现及应用场合】章节中,我们介绍了每种数据存储的形式都是可以有多种实现载体的,那么当我们确定了存储的方案后,就应该确定具体使用哪些存储载体来支撑我们的存储方案了,因为不能被实现的存储方案都是没有意义的。而存储载体的选择标准,请参考如下建议:
首先,根据容量选择存储载体。对于极小量的数据存储需求,例如逻辑处理的中间变量,每次存储需求也就几个、十几个比特左右,选择触发器作为载体最佳;对于少量的数据缓存需求,例如几十到几百比特,使用触发器作为载体的话,消耗的数量较多,使用BRAM的话,又有大材小用的感觉,因此选择LUT最佳;对于较大量的数据存储,例如几千到几十万个比特,选择BRAM作为载体最佳;而对于海量的数据存储,例如几兆、几十兆甚至更多比特,此时穷尽FPGA芯片内部的所有存储载体也未必能够满足需求,所以选择片外存储芯片作为载体最佳。
其次,根据操作方式选择存储载体。即如果同时需要访问多个存储单元的内容的话,使用触发器作为载体的移位寄存器阵列最佳;如果希望能够同时进行高效的读、写操作的话,那么选择FPGA片内的载体最佳,因为外部存储芯片往往都是读、写总线公用的;
最后,根据FPGA设计的资源占用情况进行存储载体的调配,例如BRAM资源已经耗尽,但是LUT资源还有很多,此时使用LUT来实现较大量的数据存储也是可以的。

step 4——确定存储模型

存储载体一旦确定后,就表明我们的存储方案是切实可行的,但是我们还没有涉及到具体的读、写操作细节,那么接下来,就需要根据存取细节来确定数据存储的具体模型,例如RAM的读模式,读写仲裁,FIFO的复位输出等等。

step 5——创造存储模块

从存储方案到存储细节都完成了的话,下一步就是创造出可供使用的存储模块了。通常来说,方法有两种,即使用IP核和使用HDL语言描述,请根据不同的存储形式选择更易用的一种。例如,如果存储形式是寄存器或者移位寄存器的话,HDL描述来得往往比IP核更简单、快捷;如果是FIFO的话,IP核往往比HDL描述来得更加高效、可靠。

step 6——实现存储操作

创造出存储模块后,就到了该如何正确使用它的问题了,因此免不了要使用HDL来完成其复位、读、写等的控制,如果是和外界存储芯片打交道,恐怕还需要涉及到初始化、刷新甚至任务仲裁等逻辑。
关于存储的操作,需要特别注意一点,那就是对数据存储进行操作的时候一定要保证逻辑的严密性。例如,如果没有充分的论据推断出FIFO中已经存有数据,那么读之前请先判断一下其empty标志位;同样,如果没有充分的论据推断出FIFO中还有剩余的空间,那么写之前请先判断一下其full标志位。否则,如果空的时候读,或者满的时候写,通常来说操作都会被FIFO所忽略,但是你却不知道,导致仍然读走旧的输出或者丢掉该写入FIFO中的数据。

step 7——调试存储行为

以上的事情都做完后,必须要对实现后的存储方案进行调试,调试通过后,存储方案才算是真正的被实现了。

发布了806 篇原创文章 · 获赞 1541 · 访问量 151万+

猜你喜欢

转载自blog.csdn.net/Reborn_Lee/article/details/104367728
今日推荐