【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

时间:2022-06-29 06:50:25

实验十五:FIFO储存模块(同步)

笔者虽然在实验十四曾解释储存模块,而且也演示奇怪的家伙,但是实验十四只是一场游戏而已。至于实验十五,笔者会稍微严肃一点,手动建立有规格的储存模块,即同步FIFO。那些看过《时序篇》的同学一定对同步FIFO不会觉得陌生吧?因为笔者曾在《时序篇》建立基于移位寄存器的同步FIFO。不过那种同步FIFO只是用来学习的玩具而已。因此,这回笔者可要认真了!

事实告诉笔者,同步FIFO的利用率远胜其它储存模块,几乎所有接口模块都会出现它的身影。早期的时候,笔者都会利用官方准备的同步FIFO(官方插件模块),大伙都知道官方插件模块都非常傲娇,心意(内容)不仅不容易看透,而且信号也不容易捉摸,最重要是无法随心所欲摆布它们。与其跪下向它求救,笔者还不如创建自己专属的同步FIFO。

故名思议,“同步”表示相同频率的时钟源,“FIFO”表示先进先出的意思。FIFO的用意一般都是缓冲数据,另模块独立,让模块回避调用的束缚。同步FIFO是RAM的亚种,它基于RAM,再加上先进先出的机制,学习同步FIFO就是学习如何建立先进先出的机制。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.1 同步FIFO建模图(常规)。

常规上,同步FIFO的建模图如图15.1所示,左边有写入请求 ReqW,写入数据 DataW,还有写满标示Full。换之,右边则有读出请求ReqR,读出数据DataR,还有读空标示 Empty。写入方面,ReqW必须拉高DataW才能写入,一旦FIFO写满,那么Full就会拉高。至于读出方面,ReqR 必须拉高,数据才能经由DataR读出,一旦FIFO读空,Empty就会拉高。不过图15.1可以稍微更动一下,另它更加接近低级建模II的形象。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.2 同步FIFO建模图(低级建模II)。

如图15.2所示,Req× 改为沟通信号 En,其中En[1] 表示写入使能, En[0]表示读出使能。Data× 改为数据信号Data,iData为写入数据,oData为读出数据。Full与Empty 则改为状态信号 Tag[1] 与 Tag[0] 。

. module fifo_savemod

. ( 

. input CLOCK, RESET,

. input [:]iEn,

. input [:]iData,

. ouptut [:]oData

. output [:]oTag

. );

. ......

. assign oTag[] = ...; // Full

. assign oTag[] = ...; // Empty

.

. endmodule

代码15.1

同步FIFO大致的外皮如代码15.1所示,第3~7行是相关的出入端声明,第10~11行则是相关的输出驱动声明。理解这些以后,接下来我们要学习先进先出这个机制。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.3 读空状态。

假设笔者建立位宽为4,深度为4的ram,然后又建立位宽为3的写指针WP与读指正RP。同学一定会好奇道,既然ram只有4个深度,那么指针只要2位宽(22 = 4)即不是可以访问所有深度呢?话虽如此,为了利用指正表示写满与读空状态,指针必须多出一位 ... 因此,指针的最高位常常也被称为方向位。

如图15.3所示,一开始的时候,写指针与读指针同样指向地址0,而且ram里边也是空空如也,为此读空状态“叮咚”亮着红灯。为此,我们可以暂时这样表示读空的逻辑关系:

Empty = (WP == RP);

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.4 写入中①。

当火车开动以后,首先数据 4’hA 写入地址0,然后写指针从原来的 3’b0_00 递增为 3’b0_01,并且指向地址1。此刻,ram再也不是空空入也,所示读空状态消除红灯,结果如图15.4 所示。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.5 写入中②。

紧接着,数据 4’hB 写入地址1,然后写指针从原来的 3’b0_01 递增为 3’b0_10,并且指向地址2,结果如图15.5所示。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.6写入中③

然后,数据 4’hC 写入地址2,然后写指针从原来的 3’b0_10 递增为 3’b0_11,并且指向地址3,结果如图15.6所示。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.7 写满状态。

接着,数据 4’hD 写入地址3,然后写指针从原来的 3’b0_11 递增为 3’b1_00,并且重新指向地址0。此刻写指针的最高位为1,这表示写指针已经绕弯ram一圈又回来原点,反之读指针从刚才开始一动也不动,结果最高为0 ... 所以我们可以说写指针与读指针目前处于不同的方向。在此ram已经写满,所以写满状态便“叮咚”亮红灯,结果如图15.6所示。写满状态的逻辑关系则可以这样表示:

FULL = (WP[2] ^ RP[2] && WP[1:0] == RP[1:0]);

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.8 读出中①。

从现在开始,另一头火车才开始走动 ... 首先数据4’hA从地址0读出来,读指针也从原本的 3’b0_00 递增为 3’b0_01,并且指向地址1。此刻ram再也不是吃饱饱的状态,所以写满状态被消除红灯,结果如图15.8所示。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.9 读出中②。

接下来,数据4’hB 从地址1哪里读出,读指针也从原本的 3’b0_01 递增为 3’b0_10,并且指向地址2,结果如图15.9所示。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.10 读出中③。

随之,数据4’hC 从地址2哪里读出,读指针也从原本的 3’b0_10 递增为 3’b0_11,并且指向地址3,结果如图15.10所示。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.11读空状态。

最后,数据4’hD 从地址3哪里读出,读指针也从原本的 3’b0_11 递增为 3’b1_00,并且重新指向地址0。当读指针绕弯一圈又回到原点的时候,读者的最高位也成为值1,换句话说 ... 此刻的读指针与写指针也处于同样的位置。同一个时候,ram也是空空如也,所以读空状态便“叮咚”亮起红灯,结果如图15.11所示。为此,读空状态的逻辑关系可以这样表示:

Empty = (WP == RP);

总结而言,当我们设置N位位宽的时候,读写指针的位宽便是 N + 1。此外,读空状态为写指针等价读指针。反之,写满状态是两个指针方向一致(异或状态),然后地址一致。理解先进先出的机制以后,接下来我们便可以填充一下FIFO储存模块的内容。

. module fifo_savemod

. (

. input CLOCK, RESET, 

. input [:]iEn,

. input [:]iData,

. output [:]oData,

. output [:]oTag

. );

. reg [:] RAM [:]; 

. reg [:]D1;

. reg [:]C1,C2; // N+1

.

. always @ ( posedge CLOCK or negedge RESET )

. if( !RESET )

. begin

. C1 <= 'd0;

. end

. else if( iEn[] ) 

. begin

. RAM[ C1[:] ] <= iData; 

. C1 <= C1 + 'b1; 

. end

.

. always @ ( posedge CLOCK or negedge RESET )

. if( !RESET )

. begin

. D1 <= 'd0;

. C2 <= 'd0;

. end

. else if( iEn[] )

. begin 

. D1 <= RAM[ C2[:] ]; 

. C2 <= C2 + 'b1; 

. end

.

. assign oData = D1;

. assign oTag[] = ( C1[]^C2[] & C1[:] == C2[:] ); // Full

. assign oTag[] = ( C1 == C2 ); // Empty

.

. endmodule

代码15.2

笔者在第9~11行创建相关的寄存器,C1取代WP,C2取代RP。第13~22行是写操作,内容非常单纯,即 iEn[1] 拉高便将 iData 写入 C1[1:0] 指定的地方,然后C1递增。

第24~34行是读操作,内容也是一样单纯,iEn[0] 拉高便将 C2[1:0] 指定的数据暂存至 D1,随后C2递增,最后由D驱动oData。第37~38行是写满状态与读空状态的逻辑关系。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.12 调用FIFO储存模块。

创建同步FIFO基本上没有什么难度,但是调用FIFO倒是一件难题。如图15.12所示,笔者建立一支核心操作尝试调用 FIFO储存模块,至于核心操作的内容如代码15.3所示:

. case( i ) // Core

. :

. if( iTag[]! ) begin oEn[] <= ’b1; oData <= ’hA; i <= i + ’b1; end

. :

. if( iTag[]! ) begin oEn[] <= ’b1; oData <= ’hB; i <= i + ’b1; end

. :

. if( iTag[]! ) begin oEn[] <= ’b1; oData <= ’hC; i <= i + ’b1; end

. :

. if( iTag[]! ) begin oEn[] <= ’b1; oData <= ’hD; i <= i + ’b1; end

. :

. begin oEn[] <= ’b0; i <= i + ’b1; end

. :

. if( iTag[]! ) begin oEn[] <= ’b1; i <= i + ’b1; end

. :

. if( iTag[]! ) begin oEn[] <= ’b1; i <= i + ’b1; end

. :

. if( iTag[]! ) begin oEn[] <= ’b1; i <= i + ’b1; end

. :

. if( iTag[]! ) begin oEn[] <= ’b1; i <= i + ’b1; end

. :

. begin oEn[] <= ’b0; i <= i + ’b1; end

. endcase

代码15.3

如代码15.3所示,步骤0~3用来一边检测 Tag[1] 是否为高,一边向储存模块写入数据 4’hA~4‘hD,步骤4则用来拉低 oEn[1] 并且歇息一下。步骤5~8用来一边检测 Tag[0] 是否为高,一边从储存模块哪里读出数据,步骤9则用来拉低 oEn[0]并且偷懒一下。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.13 读写FIFO储存模块的理想时序图。

图15.13是代码15.3所生产的理想时序图,同时也是核心操作作为视角的时序,至于C1~C2是FIFO储存模块作为视角的时序。各个视角的时序过程如下:

核心操作视角:

l T0,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hA(未来值)。

l T1,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hB(未来值)。

l T2,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hC(未来值)。

l T3,isTag[1]为低(即时值),拉高oEn[1](未来值),并且发送数据 4’hD(未来值)。

l T4,isTag[1]为高(即时值),拉低oEn[1](未来值)。

l T5,isTag[0]为低(即时值),拉高oEn[1](未来值)。

l T6,isTag[0]为低(即时值),拉高oEn[1](未来值),数据4’hA读出(过去值)。

l T7,isTag[0]为低(即时值),拉高oEn[1](未来值),数据4’hB读出(过去值)。

l T8,isTag[0]为低(即时值),拉高oEn[1](未来值),数据4’hC读出(过去值)。

l T9,isTag[0]为高(即时值),拉低oEn[1](未来值),数据4’hD读出(过去值)。

l T10,isTag[0]为高(即时值)。

FIFO储存模块视角:

l T0,oEn[1]为低(过去值)。C1等价C2为读空状态,iTag[0]拉高(即时值)。

l T1,oEn[1]为高(过去值),读取数据4’hA(过去值),递增C1。C1不等价C2,iTag[0]拉低(即时值)。

l T2,oEn[1]为高(过去值),读取数据4’hB(过去值),递增C1。

l T3,oEn[1]为高(过去值),读取数据4’hC(过去值),递增C1。

l T4,oEn[1]为高(过去值),读取数据4’hA(过去值),递增C1。C1等价C2为写满状态,iTag[1]拉高(即时值)。

l T5,oEn[1]为低(过去值)。

l T6,oEn[0]为高(过去值),读出数据4’hA(未来值),递增C2。C1不等价C2,isTag[1]拉低(即时值)。

l T7,oEn[0]为高(过去值),读出数据4’hB(未来值),递增C2。

l T8,oEn[0]为高(过去值),读出数据4’hC(未来值),递增C2。

l T9,oEn[0]为高(过去值),读出数据4’hD(未来值),递增C2。C1等价C2为读空状态,isTag[0]拉高(即时值)。

l T10,oEn[0]为低(过去值)。

读者是不是一边浏览一边捏蛋蛋呢?什么过去值,又什么未来值,又又什么即时值的 ... 没错,同步FIFO的设计原理虽然简单,但是时序解读却让人泪流满面。因为同步FIFO夹杂两种时序表现——时间点事件还有即时事件。如图15.13 所示,除了 iTag 信号是触发即时事件以外,所有信号都是触发时间点事件。读过《时序篇》或者《工具篇II》的朋友一定知晓,即时值不仅比过去值优先,而且即时值也会无视时钟。

好奇的同学可能困惑道:“为什么iTag不能设计成为时间点事件呢?”。笔者曾在《时序篇》建立基于移位寄存器的FIFO,其中iTag就是设计成为时间点事件,结果FIFO的写满状态或者读空状态都来不及反馈,因此发生调用上的混乱。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.14 读写FIFO储存模块的即时事件。

为了理解重点,首先让我们来焦距写数据的部分。如图15.14所示,关键的地方就是发生在T4——这只时钟沿。T4之际,FIFO储存模块读取oEn[1]的过去值,C1也因此递增,即时事件就在这个瞬间发生了。写满状态成立,iTag[1]也随之拉高即时值。从时序上来看,C1的更新(C1为4’b100)是发生在T4之后,不过那也无关紧要,因为即时值是更新在小小的时间沿之间,也是即时层 ... 然而,即时层是无法显示在时序之上。

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.15 迟到的写满状态。

假设,iTag[1]不是经由即时事件触发而是事件点事件,那么iTag就会反馈迟到的写满状态。如图15.15所示,T4之际 oEn[1] 为高,C1也因此递增为 3’b100。T5之际,C1与C2的过去值均为 3’b100 与 3’b000,然后拉高 iTag[1]。由于时间点事件的关系,所以iTag[1]迟一拍被拉高 ... 读者千万别小看这样慢来一拍,它是搞乱调用的罪魁祸首。

如果核心操作在T5继续写操作的话,此刻iTag[1]的过去值为0,它会认为FIFO未满,然后不管三七二十一执行写操作,结果FIFO发生错乱随之机能崩溃。从某种程度来看,即时事件的偷时钟能力,是建立同步FIFO的关键。

fifo_savemod.v

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.16 fifo储存模块的建模图。

图15.16基本上与图15.2没什么两样,不过FIFO储存模块的位宽还有深度发生改变而已。此外,图15.16的信号布局虽然有点违规低,不过这点小细节读者就不要太计较了。建模技巧毕竟不是暴力规范,用不着死守,反之随机应变才是本意。

. module fifo_savemod

. (

. input CLOCK, RESET, 

. input [:]iEn,

. input [:]iData,

. output [:]oData,

. output [:]oTag

. );

以上内容是相关的出入端声明。

. reg [:] RAM [:]; 

. reg [:]D1;

. reg [:]C1,C2; // N+1

.

以上内容是相关的内存与寄存器声明。第9行,RAM声明为8位宽还有24=16个深度。为此,第11行的写指针C1与读指针C2声明为5个位宽。

. always @ ( posedge CLOCK or negedge RESET )

. if( !RESET )

. begin

. C1 <= 'd0;

. end

. else if( iEn[] ) 

. begin

. RAM[ C1[:] ] <= iData; 

. C1 <= C1 + 'b1; 

. end

.

以上内容是fifo的写操作,第18行的 iEn[] 每拉高一个时钟, 第20行的iData 便写入C1[:]指定的位置,随后第21行写指针也递增。

. always @ ( posedge CLOCK or negedge RESET )

. if( !RESET )

. begin

. D1 <= 'd0;

. C2 <= 'd0;

. end

. else if( iEn[] )

. begin 

. D1 <= RAM[ C2[:] ]; 

. C2 <= C2 + 'b1; 

. end

.

以上内容是fifo的读操作,第30行的 iEn[] 每拉高一个时钟, 第32行的D1便赋予C2[:]指定的数据,随后第33行的读指针也递增。

. assign oData = D1;

. assign oTag[] = ( C1[]^C2[] & C1[:] == C2[:] ); // Full

. assign oTag[] = ( C1 == C2 ); // Empty

.

. endmodule

以上内容是相关输出驱动声明,其中第37行是写满状态,第38行是读空状态。在此,读者需要注意一下 ... 第36行相较第37~38行 ,前者由寄存器D1驱动,即oData信号为时间点事件。反之,后者由组合逻辑驱动,即 oTag[1:0] 信号为即时事件。为此,该储存模块的内部状态是以即时的方式反馈出去。

tx_rx_demo.v

【黑金原创教程】【FPGA那些事儿-驱动篇I 】实验十五:FIFO储存模块(同步)

图15.17 实验十五的建模图。

实验十五是实验十三的延续 ... 实验十三之际,RX功能模块接收并且失败发送一连串的数据,因为发送方不仅来不及,而且接收成功的数据也没有地方缓冲。如今实验十五多了一只FIFO储存模块作为缓冲空间。注意,图15.17虽然是实验十五的建模图,可是却与实际的连线部署有一点出入,不过大意上都是差不多的。

RX功能模块接收一连串的数据,然后经由周边操作协调,事后再将数据缓冲至FIFO储存模块。至于核心操作会不停从FIFO储存模块哪里读取数据,然后再调用TX功能模块将数据发送出去。

. module tx_rx_demo

. (

. input CLOCK, RESET, 

. input RXD,

. output TXD

. ); 

以上内容是相关的出入端声明。

. wire DoneU1;

. wire [:]DataU1;

.

. rx_funcmod U1

. (

. .CLOCK( CLOCK ),

. .RESET( RESET ),

. .RXD( RXD ), // < top

. .iCall( isRX ), // < sub

. .oDone( DoneU1 ), // > U2

. .oData( DataU1 ) // > U2

. );

.

以上内容是RX功能模块的实例化。第15行表示 isRX 充当使能。

. reg isRX;

.

. always @ ( posedge CLOCK or negedge RESET ) // sub

. if( !RESET ) isRX <= 'b0;

. else if( DoneU1 ) isRX <= 'b0; 

. else isRX <= 'b1; 

.

以上内容是周边操作,它主要重复调用RX功能模块。

. wire [:]TagU2;

. wire [:]DataU2;

.

. fifo_savemod U2

. (

. .CLOCK( CLOCK ),

. .RESET( RESET ),

. .iEn ( { DoneU1 , isRead } ), // < U1 & Core

. .iData ( DataU1 ), // < U1

. .oData ( DataU2 ), // > U3

. .oTag ( TagU2 ) // > core

. );

.

以上内容是FIFO储存模块的实例化。第34行表示,DoneU1充当写入使能,isRead充当读出使能。

. wire DoneU3;

.

. tx_funcmod U3

. (

. .CLOCK( CLOCK ), 

. .RESET( RESET ),

. .TXD( TXD ), // > top

. .iCall( isTX ), // < core

. .oDone( DoneU3 ), // > core

. .iData( DataU2 ) // < U2

. );

.

以上内容是TX功能模块的实例化。第47行表示 isTX充当使能。第49行表示,该模块的iData直接经由DataU2驱动。

. reg [:]i;

. reg isRead;

. reg isTX;

.

. always @ ( posedge CLOCK or negedge RESET ) // core

. if( !RESET )

. begin 

. i <= 'd0;

. isRead <= 'b0;

. isTX<= 'b0;

. end

以上内容是核心操作的相关寄存器声明与复位操作。

. else

. case( i )

.

. :

. if( !TagU2[] ) begin isRead <= 'b1; i <= i + 1'b1; end

.

. :

. begin isRead <= 'b0; i <= i + 1'b1; end

.

. :

. if( DoneU3 ) begin isTX <= 'b0; i <= 4'd0; end

. else isTX <= 'b1;

.

. endcase

.

. endmodule

以上内容是操作操作。步骤0用来判断FIFO是否读空,否则就拉高isRead。步骤1则拉低isRead,然后FIFO就会读出数据。步骤2则使能TX功能模块,并且将方才读出的数据发送出去。

编译完毕并下载程序。此刻,串口便可以支持一连串的数据发送与接收,为了避免部分数据凭空消失的怪事,数据流的容量必须配合FIFO的缓冲容量(深度)。此外,实验十五还有许多优化的空间,然而这些都是交由读者的功课。(注意,某些串口调试助手必须把检验位设置为标志位才能显示字符)

细节一:完整的个体模块

该实验的 fifo_savemod.v 已经是完整的个体。