【黑金原創教程】【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 已經是完整的個體。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM