zoukankan      html  css  js  c++  java
  • 【黑金原创教程】【FPGA那些事儿驱动篇I 】实验十五:FIFO储存模块(同步)

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

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

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

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

    clip_image002

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

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

    clip_image004

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

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

    1. module fifo_savemod
    
    2. ( 
    
    3. input CLOCK, RESET,
    
    4. input [1:0]iEn,
    
    5. input [3:0]iData,
    
    6. ouptut [3:0]oData
    
    7. output [1:0]oTag
    
    8. );
    
    9. ......
    
    10. assign oTag[1] = ...; // Full
    
    11. assign oTag[0] = ...; // Empty
    
    12.
    
    13. endmodule

    代码15.1

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

    clip_image006

    图15.3 读空状态。

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

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

    Empty = (WP == RP);

    clip_image008

    图15.4 写入中①。

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

    clip_image010

    图15.5 写入中②。

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

    clip_image012

    图15.6写入中③

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

    clip_image014

    图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]);

    clip_image016

    图15.8 读出中①。

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

    clip_image018

    图15.9 读出中②。

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

    clip_image020

    图15.10 读出中③。

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

    clip_image022

    图15.11读空状态。

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

    Empty = (WP == RP);

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

    1. module fifo_savemod
    
    2. (
    
    3. input CLOCK, RESET, 
    
    4. input [1:0]iEn,
    
    5. input [3:0]iData,
    
    6. output [3:0]oData,
    
    7. output [1:0]oTag
    
    8. );
    
    9. reg [3:0] RAM [3:0]; 
    
    10. reg [3:0]D1;
    
    11. reg [2:0]C1,C2; // N+1
    
    12.
    
    13. always @ ( posedge CLOCK or negedge RESET )
    
    14. if( !RESET )
    
    15. begin
    
    16. C1 <= 3'd0;
    
    17. end
    
    18. else if( iEn[1] ) 
    
    19. begin
    
    20. RAM[ C1[1:0] ] <= iData; 
    
    21. C1 <= C1 + 1'b1; 
    
    22. end
    
    23.
    
    24. always @ ( posedge CLOCK or negedge RESET )
    
    25. if( !RESET )
    
    26. begin
    
    27. D1 <= 4'd0;
    
    28. C2 <= 3'd0;
    
    29. end
    
    30. else if( iEn[0] )
    
    31. begin 
    
    32. D1 <= RAM[ C2[1:0] ]; 
    
    33. C2 <= C2 + 1'b1; 
    
    34. end
    
    35.
    
    36. assign oData = D1;
    
    37. assign oTag[1] = ( C1[2]^C2[2] & C1[1:0] == C2[1:0] ); // Full
    
    38. assign oTag[0] = ( C1 == C2 ); // Empty
    
    39.
    
    40. 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行是写满状态与读空状态的逻辑关系。

    clip_image024

    图15.12 调用FIFO储存模块。

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

    1. case( i ) // Core
    
    2. 0:
    
    3. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hA; i <= i + 1’b1; end
    
    4. 1:
    
    5. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hB; i <= i + 1’b1; end
    
    6. 2:
    
    7. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hC; i <= i + 1’b1; end
    
    8. 3:
    
    9. if( iTag[1]! ) begin oEn[1] <= 1’b1; oData <= 4’hD; i <= i + 1’b1; end
    
    10. 4:
    
    11. begin oEn[1] <= 1’b0; i <= i + 1’b1; end
    
    12. 5:
    
    13. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end
    
    14. 6:
    
    15. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end
    
    16. 7:
    
    17. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end
    
    18. 8:
    
    19. if( iTag[0]! ) begin oEn[0] <= 1’b1; i <= i + 1’b1; end
    
    20. 9:
    
    21. begin oEn[0] <= 1’b0; i <= i + 1’b1; end
    
    22. endcase

    代码15.3

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

    clip_image026

    图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的写满状态或者读空状态都来不及反馈,因此发生调用上的混乱。

    clip_image028

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

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

    clip_image030

    图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

    clip_image032

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

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

    1. module fifo_savemod
    
    2. (
    
    3. input CLOCK, RESET, 
    
    4. input [1:0]iEn,
    
    5. input [7:0]iData,
    
    6. output [7:0]oData,
    
    7. output [1:0]oTag
    
    8. );
    
    以上内容是相关的出入端声明。
    
    9. reg [7:0] RAM [15:0]; 
    
    10. reg [7:0]D1;
    
    11. reg [4:0]C1,C2; // N+1
    
    12.
    
    以上内容是相关的内存与寄存器声明。第9行,RAM声明为8位宽还有24=16个深度。为此,第11行的写指针C1与读指针C2声明为5个位宽。
    
    13. always @ ( posedge CLOCK or negedge RESET )
    
    14. if( !RESET )
    
    15. begin
    
    16. C1 <= 5'd0;
    
    17. end
    
    18. else if( iEn[1] ) 
    
    19. begin
    
    20. RAM[ C1[3:0] ] <= iData; 
    
    21. C1 <= C1 + 1'b1; 
    
    22. end
    
    23.
    
    以上内容是fifo的写操作,第18行的 iEn[1] 每拉高一个时钟, 第20行的iData 便写入C1[3:0]指定的位置,随后第21行写指针也递增。
    
    24. always @ ( posedge CLOCK or negedge RESET )
    
    25. if( !RESET )
    
    26. begin
    
    27. D1 <= 8'd0;
    
    28. C2 <= 5'd0;
    
    29. end
    
    30. else if( iEn[0] )
    
    31. begin 
    
    32. D1 <= RAM[ C2[3:0] ]; 
    
    33. C2 <= C2 + 1'b1; 
    
    34. end
    
    35.
    
    以上内容是fifo的读操作,第30行的 iEn[0] 每拉高一个时钟, 第32行的D1便赋予C2[3:0]指定的数据,随后第33行的读指针也递增。
    
    36. assign oData = D1;
    
    37. assign oTag[1] = ( C1[4]^C2[4] & C1[3:0] == C2[3:0] ); // Full
    
    38. assign oTag[0] = ( C1 == C2 ); // Empty
    
    39.
    
    40. endmodule

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

    tx_rx_demo.v

    clip_image034

    图15.17 实验十五的建模图。

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

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

    1. module tx_rx_demo
    
    2. (
    
    3. input CLOCK, RESET, 
    
    4. input RXD,
    
    5. output TXD
    
    6. ); 
    
    以上内容是相关的出入端声明。
    
    7. wire DoneU1;
    
    8. wire [7:0]DataU1;
    
    9.
    
    10. rx_funcmod U1
    
    11. (
    
    12. .CLOCK( CLOCK ),
    
    13. .RESET( RESET ),
    
    14. .RXD( RXD ), // < top
    
    15. .iCall( isRX ), // < sub
    
    16. .oDone( DoneU1 ), // > U2
    
    17. .oData( DataU1 ) // > U2
    
    18. );
    
    19.
    
    以上内容是RX功能模块的实例化。第15行表示 isRX 充当使能。
    
    20. reg isRX;
    
    21.
    
    22. always @ ( posedge CLOCK or negedge RESET ) // sub
    
    23. if( !RESET ) isRX <= 1'b0;
    
    24. else if( DoneU1 ) isRX <= 1'b0; 
    
    25. else isRX <= 1'b1; 
    
    26.
    
    以上内容是周边操作,它主要重复调用RX功能模块。
    
    27. wire [1:0]TagU2;
    
    28. wire [7:0]DataU2;
    
    29.
    
    30. fifo_savemod U2
    
    31. (
    
    32. .CLOCK( CLOCK ),
    
    33. .RESET( RESET ),
    
    34. .iEn ( { DoneU1 , isRead } ), // < U1 & Core
    
    35. .iData ( DataU1 ), // < U1
    
    36. .oData ( DataU2 ), // > U3
    
    37. .oTag ( TagU2 ) // > core
    
    38. );
    
    39.
    
    以上内容是FIFO储存模块的实例化。第34行表示,DoneU1充当写入使能,isRead充当读出使能。
    
    40. wire DoneU3;
    
    41.
    
    42. tx_funcmod U3
    
    43. (
    
    44. .CLOCK( CLOCK ), 
    
    45. .RESET( RESET ),
    
    46. .TXD( TXD ), // > top
    
    47. .iCall( isTX ), // < core
    
    48. .oDone( DoneU3 ), // > core
    
    49. .iData( DataU2 ) // < U2
    
    50. );
    
    51.
    
    以上内容是TX功能模块的实例化。第47行表示 isTX充当使能。第49行表示,该模块的iData直接经由DataU2驱动。
    
    52. reg [3:0]i;
    
    53. reg isRead;
    
    54. reg isTX;
    
    55.
    
    56. always @ ( posedge CLOCK or negedge RESET ) // core
    
    57. if( !RESET )
    
    58. begin 
    
    59. i <= 4'd0;
    
    60. isRead <= 1'b0;
    
    61. isTX<= 1'b0;
    
    62. end
    
    以上内容是核心操作的相关寄存器声明与复位操作。
    
    63. else
    
    64. case( i )
    
    65.
    
    66. 0:
    
    67. if( !TagU2[0] ) begin isRead <= 1'b1; i <= i + 1'b1; end
    
    68.
    
    69. 1:
    
    70. begin isRead <= 1'b0; i <= i + 1'b1; end
    
    71.
    
    72. 2:
    
    73. if( DoneU3 ) begin isTX <= 1'b0; i <= 4'd0; end
    
    74. else isTX <= 1'b1;
    
    75.
    
    76. endcase
    
    77.
    
    78. endmodule

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

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

    细节一:完整的个体模块

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

  • 相关阅读:
    Oracle入门第六天(下)——高级子查询
    Oracle入门第六天(中)——SET运算符(交并差集)
    Oracle入门第六天(上)——用户控制
    数据库理论——数据库3范式
    Oracle入门第五天(下)——数据库其他对象
    Oracle入门第五天(上)——数据库对象之视图
    PHP PDO函数库详解
    python访问纯真IP数据库的代码
    有关linux磁盘分区优化
    Nginx日志深入详解
  • 原文地址:https://www.cnblogs.com/alinx/p/4223450.html
Copyright © 2011-2022 走看看