實驗十五: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] 。
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行則是相關的輸出驅動聲明。理解這些以后,接下來我們要學習先進先出這個機制。
圖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儲存模塊的內容。
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行是寫滿狀態與讀空狀態的邏輯關系。
圖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]並且偷懶一下。
圖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的信號布局雖然有點違規低,不過這點小細節讀者就不要太計較了。建模技巧畢竟不是暴力規范,用不着死守,反之隨機應變才是本意。
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
圖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 已經是完整的個體。

















