实验十五:FIFO储存模块(同步)
笔者虽然在实验十四曾解释储存模块,而且也演示奇怪的家伙,但是实验十四只是一场游戏而已。至于实验十五,笔者会稍微严肃一点,手动建立有规格的储存模块,即同步FIFO。那些看过《时序篇》的同学一定对同步FIFO不会觉得陌生吧?因为笔者曾在《时序篇》建立基于移位寄存器的同步FIFO。不过那种同步FIFO只是用来学习的玩具而已。因此,这回笔者可要认真了!
事实告诉笔者,同步FIFO的利用率远胜其它储存模块,几乎所有接口模块都会出现它的身影。早期的时候,笔者都会利用官方准备的同步FIFO(官方插件模块),大伙都知道官方插件模块都非常傲娇,心意(内容)不仅不容易看透,而且信号也不容易捉摸,最重要是无法随心所欲摆布它们。与其跪下向它求救,笔者还不如创建自己专属的同步FIFO。
故名思议,“同步”表示相同频率的时钟源,“FIFO”表示先进先出的意思。FIFO的用意一般都是缓冲数据,另模块独立,让模块回避调用的束缚。同步FIFO是RAM的亚种,它基于RAM,再加上先进先出的机制,学习同步FIFO就是学习如何建立先进先出的机制。
图15.1 同步FIFO建模图(常规)。
常规上,同步FIFO的建模图如图15.1所示,左边有写入请求 ReqW,写入数据 DataW,还有写满标示Full。换之,右边则有读出请求ReqR,读出数据DataR,还有读空标示 Empty。写入方面,ReqW必须拉高DataW才能写入,一旦FIFO写满,那么Full就会拉高。至于读出方面,ReqR 必须拉高,数据才能经由DataR读出,一旦FIFO读空,Empty就会拉高。不过图15.1可以稍微更动一下,另它更加接近低级建模II的形象。
图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行则是相关的输出驱动声明。理解这些以后,接下来我们要学习先进先出这个机制。
图15.3 读空状态。
假设笔者建立位宽为4,深度为4的ram,然后又建立位宽为3的写指针WP与读指正RP。同学一定会好奇道,既然ram只有4个深度,那么指针只要2位宽(22 = 4)即不是可以访问所有深度呢?话虽如此,为了利用指正表示写满与读空状态,指针必须多出一位 ... 因此,指针的最高位常常也被称为方向位。
如图15.3所示,一开始的时候,写指针与读指针同样指向地址0,而且ram里边也是空空如也,为此读空状态“叮咚”亮着红灯。为此,我们可以暂时这样表示读空的逻辑关系:
Empty = (WP == RP);
图15.4 写入中①。
当火车开动以后,首先数据 4’hA 写入地址0,然后写指针从原来的 3’b0_00 递增为 3’b0_01,并且指向地址1。此刻,ram再也不是空空入也,所示读空状态消除红灯,结果如图15.4 所示。
图15.5 写入中②。
紧接着,数据 4’hB 写入地址1,然后写指针从原来的 3’b0_01 递增为 3’b0_10,并且指向地址2,结果如图15.5所示。
图15.6写入中③
然后,数据 4’hC 写入地址2,然后写指针从原来的 3’b0_10 递增为 3’b0_11,并且指向地址3,结果如图15.6所示。
图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]);
图15.8 读出中①。
从现在开始,另一头火车才开始走动 ... 首先数据4’hA从地址0读出来,读指针也从原本的 3’b0_00 递增为 3’b0_01,并且指向地址1。此刻ram再也不是吃饱饱的状态,所以写满状态被消除红灯,结果如图15.8所示。
图15.9 读出中②。
接下来,数据4’hB 从地址1哪里读出,读指针也从原本的 3’b0_01 递增为 3’b0_10,并且指向地址2,结果如图15.9所示。
图15.10 读出中③。
随之,数据4’hC 从地址2哪里读出,读指针也从原本的 3’b0_10 递增为 3’b0_11,并且指向地址3,结果如图15.10所示。
图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行是写满状态与读空状态的逻辑关系。
图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]并且偷懒一下。
图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的写满状态或者读空状态都来不及反馈,因此发生调用上的混乱。
图15.14 读写FIFO储存模块的即时事件。
为了理解重点,首先让我们来焦距写数据的部分。如图15.14所示,关键的地方就是发生在T4——这只时钟沿。T4之际,FIFO储存模块读取oEn[1]的过去值,C1也因此递增,即时事件就在这个瞬间发生了。写满状态成立,iTag[1]也随之拉高即时值。从时序上来看,C1的更新(C1为4’b100)是发生在T4之后,不过那也无关紧要,因为即时值是更新在小小的时间沿之间,也是即时层 ... 然而,即时层是无法显示在时序之上。
图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
图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
图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 已经是完整的个体。