實驗十四比起動手筆者更加注重原理,因為實驗十四要討論的東西,不是其它而是低級建模II之一的模塊類,即儲存模塊。接觸順序語言之際,“儲存”不禁讓人聯想到變量或者數組,結果它們好比數據的暫存空間。
1. int main() 2. { 3. int VarA; 4. char VarB; 5. VarA = 20; 6. VarB = 5; 7. }
代碼14.1
如代碼14.1所示,主函數內一共聲明兩個變量VarA與VarB(第3~4行)。VarA是兩個字節的整型變量,VarB是一個字節的字符變量,然后VarA賦值20(第5行),VarB則賦值5(第6行)。,其中 int 與 char 等字眼用來表示字節,即暫存空間的位寬,然后儲存的內容僅局限於二進制,非0即1。
1. int main() 2. { 3. int VarC[20]; 4. VarC[0] = 30; 5. for( int i = 0; i < 20; i++ ) VarC[i] = i; 6. VarC[0] = VarC[1]; 7. }
代碼14.2
除了變量以外,順序語言也有數組這個玩意,亦即一連串的變量。如代碼14.2所示,主函數內聲明數組VarC,數組的成員位寬是兩個字節的int,數組的成員長度則是20(第3行)。然而數組常見的賦值方法除了成員直接賦值以外(第4行),也有使用for循環為逐個成員賦值的方法(第5行)。此外,還有某個數組成員為某個數組成員直接賦值的方法(第6行)。目前為止,順序語言還有儲存之間的故事就圓滿結束。
人是一種自虐的生物,事情越是順利,越是容易萌起疑心 ... 然后暗道:“儲存是不是太容易理解呢?容易到讓人覺得惡心!“。沒錯,事實的確如此。“儲存”一旦投入描述語言之中,話題便會嚴肅起來。順序語言是一件懶人多得的語言,它有許多底層工作都交由編譯器處理,相較之下描述語言是一件多勞多得的語言,許多底層工作都必須交由我們自己聲明與定義。
1. reg [3:0]D1 = 4’d1; 2. reg [3:0]D2 ; 3. reg [3:0]D3; 4. 5. initial begin D2 = 4’d2; end 6. 7. always @ ( posedge CLOCK or negedge RESET ) 8. if( !RESET ) 9. D3 <= 4’d3; 10. ......
代碼14.3
首先讓我們來理解一下初始化與復位化之間的區別。我們知道順序語言的變量只有初始化,沒有復位化這一回事 ... 反之,描述語言卻不同。如代碼14.3所示,筆者在第1~3行聲明D1~D3三個寄存器,其中D1聲明不久便立即賦予初值 4’d1。換之,D2則在第5行賦予初值 4’d2,最后D3則在第8~9行賦予復位值4’d3。
所謂初值就是初始化所給予的起始內容,反之復位值就是復位觸發所給予的內容。初始化一般都是編譯器的賦值活動,第1行的D1還有第5行的D2都是經由編譯器的活動給予初值。反觀之下,復位化不是編譯器活動而是硬件活動,也是俗稱的RESET,即電平變化所引起的復位觸發。
如代碼行第7~9所示,敏感區種含有 negedge RESET的字眼表示,如果RESET的電平由高變低並且產生下降沿觸發,結果就執行一下 always 的內容。其中的內容便是復位操作,最終 D3 賦予復位值 4’d3。
圖14.1 初始化與復位化的時序圖。
如果用時序來表示的話 ... 如圖14.1所示,灰色區域表示初始化狀態又或者未上電狀態
,當中D1與D2都賦予初值4’d1與4’d2,同樣D3也給予初值 4’d0。雖然D3在代碼14.3之間並沒有任何初始化的痕跡,不過默認下編譯器都會一視同仁,將所有暫存聲明都給予初值0,除非有特別聲明,例如第1行的D1與第5行的D2。上電以后,RESET電平又高變低便產生下降沿,結果復位發生了,然后D3被賦予復位值4’d3。
我們知道容器有大有小,所以儲存空間也有大小之分,然而決定空間大小就是位寬這種東西。位寬一般是指數據的長度,順序語言會用 int 或者 char 等關鍵字表示位寬,反之描述語言會直接聲明位寬的大小,如 reg[3:0]D1。在此,順序語言的位寬區別都是一個字節一個字節比較,反之描述語言比較隨意。
1. reg [3:0]D1; // Verilog 2. reg var [3:0]D2; // Sytem Verilog 3. logic var [3:0]D3; // System Verilog
代碼14.4
除了位寬以外,我們還要理解什么是儲存內容。描述語言相較順序語言,儲存內容的花樣較多一些 ... 順序語言由於比較比較偏向軟件,所以儲存內容僅有兩態,即1與0而已。反之描述語言是介於硬件與軟件之間,所以儲存內容除了0與1兩態之外,也有高阻態z與切開x。
如代碼14.4所示,當我們聲明D1的時候,除了需要強調位寬以外,我們還要強調儲存內容 ... 以Verilog為例的話,關鍵字 reg 的用意並非強調儲存資源是基於寄存器,而是表示儲存內容有0有1,還有z與x等四個狀態。相反的,SystemVerilog在這方面卻做得很好,如代碼行2~3所示,var 關鍵字表示對象是儲存空間,reg 關鍵字表示對象的儲存內容有4態,logic關鍵字則表示對象的儲存內容有2態。
1. char VarA; // 變量(內存) 2. char VarB[4] // 數組(內存) 3. reg [7:0] D1; // 寄存器 4. reg [31:0] D2; // 寄存器
代碼14.5
我們知道順序語言有所謂的變量與數組,儲存資源一般都是基於內存,例如第1行的VarA與第2行的VarB。反之,描述語言不僅可以用寄存器資源建立變量,寄存器資源也能建立數組,例如與第3行的D1與第4行的D2。(雖然順序語言偶爾也會用到寄存器型的儲存資源,不過該儲存資源對處理器來說太珍貴了,如果不是特殊條件,一般都不會隨意使用)
1. reg [3:0] RAM [15:0]; // 片上內存
代碼14.6
此外,描述語言還有另外一種叫做片上內存的儲存資源,聲明方法如代碼14.6所示。FPGA的片上內存與單片機的內存雖然都是內存,但是兩者之間卻是不同性質的內存。簡單而言,單片機的內存是經過燒烤的熟肉,隨時可以享用 ... 反之,FPGA的片上內存則是未經過燒烤的生肉,享用之前必須做好事先准備。為此,FPGA的片上內存無法像級單片機內存那樣,隨便賦值,隨意調用。
1. int main() 2. { 3. char VarC[4]; 4. for( int i = 0; i < 3; i++ ) VarC[i] = VarC[i+1] 5. }
代碼14.7
如代碼14.7所示,筆者先建立一個 char 類型的數組 VarC長度並且為4,緊接着利用for循環為數組的整組成員賦值,其中VarC[i] 的賦予內容是 VarC[i+1] 的結果。代碼
14.7算是順序語言常見的例子,期間初始化也好,還是利用for循環為數組賦值也好,許多底層的工作都交由編譯器去作,我們只要翹腳把代碼照顧好就行。
1. reg [7:0] RAM [3:0] 2. reg [3:0]i; 3. 4. always @ ( posedge CLOCK ) // 錯誤 5. for( i = 0; i < 3; i = i + 1 ) RAM[i] = RAM[i+1];
代碼14.8
換做描述語言,如代碼14.8所示 ... 筆者在第1~2行當中先聲明位寬為8長度為4的RAM,隨之又聲明i。假設RAM要實現代碼14.7同樣的賦值操作,首先最常見的錯誤就是第4~5行的例子 ... 許多人會直接將關鍵字for套用在 always 塊里,這種賦值操作有兩種問題:
其一,編譯器並不清楚我們到底要利用空間實現for,還是利用時鍾實現for。默認下,編譯器會選擇前者,后果就是吃光邏輯資源。
其二,RAM[i] = RAM[i+1] 這種賦值操作會搞砸綜合,結果片上內存的布線狀況會變得非常復雜,從而導致綜合失敗。
代碼14.8算是新手最容易犯下的問題之一,代碼14.8雖然沒有語法上的錯誤,而且仿真也會通過,但是綜合卻萬萬不可。為此,代碼14.8需要更動一下。
1. reg [7:0] RAM [3:0] 2. reg [3:0]i; 3. 4. always @ ( posedge CLOCK ) // 錯誤 5. case( i ) 6. 0,1,2: 7. begin RAM[i] <= RAM[i+1]; i <= i + 1’b1; end 8. endcase
代碼14.9
如代碼14.9所示,筆者舍棄關鍵字 for,取而代之卻利用仿順序操作充當循環,這是一種利用時鍾實現for的方法 (偽循環)。不過代碼14.9依然不被綜合器接受,結果報錯 ... 因為片上內存並不支持類似 RAM[i] <= RAM[i+1] 的賦值方式,因為綜合期間會導致布線復雜化,並且進一步搞砸綜合。為此,代碼14.9需要繼續更動。
1. reg [7:0] RAM [3:0] 2. reg [3:0]i; 3. reg [7:0]D1; 4. 5. always @ (posedge CLOCK) // 正確 6. case( i ) 7. 0,2,4: 8. begin D1 <= RAM[i<<1]; i <= i + 1’b1; end 9. 1,3,5: 10. begin RAM[ (i<<1)+1 ] <= D1; i <= i + 1’b1; end 11. endcase
代碼14.10
如代碼14.10所示,筆者多建立一個作為暫存作用的寄存器D1,然后利用兩組步驟移動RAM之間的數據。步驟0,2與4將RAM[i] 的內容暫存至D1,步驟1,3與5則將D1的內容賦予 RAM[i+1]。如此一來,片上內存成員與成員之間的數據移動便大功告成。事實上代碼14.7也干同樣的事情,不過事實卻被編譯器隱藏了 ... 如果讀者讀者打開代碼14.7的編譯結果,讀者會看見類似的匯編語言,結果如代碼14.11所示:
T0 Load RAM[1] => R0; T1 Load R0 => RAM[0]; T2 Load RAM[2] => R0; T3 Load R0 => RAM[1]; T4 Load RAM[3] => R0; T5 Load R0 => RAM[2];
代碼14.11
如代碼14.11所示,匯編內容會重復使用 Load 指令將某個RAM的內容先暫存至通用寄存器R0,然后又從R0移至另某個RAM當中。至於代碼14.11的正確性,筆者不能確保什么,畢竟距離上一次接觸匯編語言已經是N年前的事情。不過感覺上差不多就是那樣 ... 這就是被編譯器所隱藏的底層工作之一,代碼14.10不過是將其模仿而已。
講到這里,我們開始接觸重點了。上述的例子告訴我們,編譯器不會幫忙描述語言處理底層操作。所以,變量與數組之間的儲存操作不及順序語言那么便捷,而且模仿起來也非常麻煩 ... 不過,我們也不用那么灰心,良駒有良駒的跑法,歪駒有歪駒的走法,我們只要換個角度去面對情況,不視問題,問題自然迎刃而解。
根據筆者的妄想,儲存有“儲存資源“ 還有“儲存方式”之分。描述語言可用的儲存資源有寄存器還有片上內存,然而變量與數組也是最簡單也是最基礎的“儲存方式”。基於這些 ... 事實上,描述語言可以描述各種各樣的“儲存方式”。
1. module rom( input [1:0]iAddr, output [7:0]oData ); 2. reg [7:0]D1; 3. always @ (*) 4. if( iAddr == 2’b00 ) D1 = 8’hA; 5. else if( iAddr == 2’b01 ) D1 = 8’hB; 6. else if( iAddr == 2’b10 ) D1 = 8’hC; 7. else if( iAddr == 2’b11 ) D1 = 8’hD; 8. else D1 = 8’dx; 9. 10. assign oData = D1; 11. 12. endmodule
代碼14.12
例如一個簡單的靜態ROM模塊,它可以基於寄存器或者片上內存,結果如代碼14.12與14.13所示。代碼14.12是基於寄存器的靜態ROM,它有2位iAddr與8位的oData
,其中第3~8行是ROM的內容定義,第10行則是輸出驅動,為此oData會根據iAddr的輸入產生不同的輸出。
1. module rom( input [1:0]iAddr, output [7:0]oData ); 2. reg [7:0] RAM [3:0]; 3. initial begin 4. RAM[0] = 8’hA; 5. RAM[1] = 8’hB; 6. RAM[2] = 8’hC; 7. RAM[3] = 8’hD; 8. end 9. 10. assign oData = RAM[ iAddr ]; 11. 12. endmodule
代碼14.13
反之,代碼14.13是基於片上內存的靜態ROM,它也有2位iAddr與8位oData,第3~7行是內容的定義也是初始化片上內存,第10行則是輸出驅動,oData會根據iAddr的輸出產生不同的輸出。
代碼14.12與代碼14.13雖然都是靜態ROM,不過卻有根本性的不同,因為兩者源於不同的儲存資源,其中最大的證據就是第10行的輸出驅動,前者由寄存器驅動,后者則由片上內存驅動。不同的儲存資源也有不同的性質,例如寄存器操作簡單,而且布線有余,不過不支持大容量的儲存行為。換之,片上內存雖然操作麻煩,布線也緊湊,可是卻支持大容量的儲存行為。
儲存方式相較儲存資源理解起來稍微抽象一點,而且想象范圍也非常廣大 ... 如果儲存資源是“容器的種類”,那么儲存方式就是“容器的用法”。舉例而言,一個簡單靜態ROM,根據需要它還可以演變成為其它亞種,例如常見的單口ROM或者雙口ROM或等。
1. module rom( input CLOCK,input [1:0]iAddr, output [7:0]oData ); 2. reg [7:0] RAM [3:0]; 3. initial begin 4. RAM[0] = 8’hA; 5. RAM[1] = 8’hB; 6. RAM[2] = 8’hC; 7. RAM[3] = 8’hD; 8. end 9. 10. reg [1:0] D1; 11. always @ ( posedge CLOCK) 12. D1 <= iAddr; 13. 14. assign oData = RAM[ D1 ]; 15. 16. endmodule
代碼14.14
如代碼14.14所示,那是單口ROM的典型例子,然而單口ROM與靜態ROM之間的差別就在於前者有時鍾信號,后者沒有時鍾信號。期間,代碼14.14用D1暫存iAddr,然后再由D1充當RAM的尋址工具。
1. module rom( input CLOCK,input [1:0]iAddr1, iAddr2,output [7:0]oData1,oData2 ); 2. reg [7:0] RAM [3:0]; 3. initial begin 4. RAM[0] = 8’hA; 5. RAM[1] = 8’hB; 6. RAM[2] = 8’hC; 7. RAM[3] = 8’hD; 8. end 9. 10. reg [1:0] D1; 11. always @ ( posedge CLOCK) 12. D1 <= iAddr1; 13. 14. assign oData1 = RAM[ D1 ]; 15. 16. reg [1:0] D2; 17. always @ ( posedge CLOCK) 18. D2 <= iAddr2; 19. 20. assign oData2 = RAM[ D2 ]; 21. 22. endmodule
代碼14.15
如代碼14.15所示,那是雙口ROM的典型例子,如果將其比較單口ROM,它則多了一組 iAddr與oData而已,即iAddr1與oData1,iAddr2與oData2。第10~14行是第一組(第一口),第16~20行則是第二組(第二口),不過兩組 iAddr 與 oData 都從同樣的RAM資源哪里讀取結果。
事實上,ROM還會根據更多不同要求產生更多亞種,而且亞種的種類也絕非局限在於專業規范,因為亞種的儲存模塊會依照設計者的欲望——有多畸形就多畸形,死守傳統只會固步自封而已。無論模塊對象是靜態ROM,單口ROM還是雙口ROM等 ... 筆者眼中,它們都是任意的“儲存方式”而已。
根據筆者的妄想,儲存方式的覆蓋范圍非常之廣。簡單而言,凡是模塊涉及數據的儲存操作,低級建模II都視為儲存類。舉例而言,ROM模塊儲存自讀不寫的數據; RAM模塊儲存又讀又寫的數據;FIFO模塊儲存先寫先讀的數據。
為此,我們可以這樣命名它們:
rom_savemod.v // rom儲存模塊
ram_savemod.v // ram儲存模塊
fifo_savemod.v // fifo儲存模塊
好奇的朋友一定會覺得疑惑,筆者究竟是為了定義儲存類呢?事情說來話長,筆者也是經過多番考慮以后才狠下心腸去決定的。首先,讓我們繼續從順序語言的角度去理解吧:
1. unsigned char Variable; 2. void FunctionA( unsinged char A ) { Variable = A; } 3. unsinged char FunctionB( void ) { return Variable; } 4. int main() 5. { 6. unsigned char D1; 7. FunctionA( 0x0A ); 8. D1 =FunctionB(); 9. ...... 10. }
代碼14.16
假設有N個函數想共享數據,一般而言我們都會建立全局變量(數組)。如代碼14.16所示,筆者先建立全局變量Variable,然后又聲明函數A為Variable 賦值,反之函數B則返回Variable的內容。完后,再編輯主函數的操作 ... 期間,主函數先聲明變量D,然后調用函數A,並且傳遞參數 0x0A,完后便調用函數B,並且將返回的內容賦予D。
函數之間之所以可以共享數據,那是因為編譯器在后面大力幫忙,並且處理底層操作才得以實現。換之,描述語言雖然沒有類似的好處,但是描述語言可以模仿。
1. reg [7:0]Variable; 2. reg [7:0]T,D1; 3. reg [3:0]i,Go; 4. always @ ( posedge CLOCk ) // 核心操作 5. case(i) 6. 0: // 主操作 7. begin T <= 8’h0A; i <= 4’d8; Go <= i + 1’b1; end 8. 1: 9. begin i <= 4’d9; Go <= i + 1’b1; end 10. 2: 11. begin D1 <= T; i <= i + 1’b1; end 12. ...... 13. 8:// Fake Function A 偽函數A 14. begin Variable = T; i <= Go; end 15. 9: // Fake Function B 偽函數B 16. begin T = Variable; i <= Go; end 17. endcase
代碼14.17
如代碼14.17所示,筆者先建立Variable,然后又建立T與D,還有i與Go。Variable模仿全局變量,T則是偽函數的暫存空間(數據傳遞),i指向步驟,Go則是指向返回步驟。步驟0~2,我們可以視為主函數,步驟8~9則是偽函數A與偽函數B。
步驟0,i將指向偽函數A的入口,T賦予 8’h0A,Go則指向下一個步驟。
步驟8,Variable 賦予 T 的內容,然后返回步驟。
步驟1,i將指向偽函數B的入口,Go則指向下一個步驟。
步驟9,T賦予Varibale 的內容,然后返回步驟。
步驟2,D1賦予Varibale的內容,然后操作結束。
如果我們將代碼14.16與代碼14.17互相比較的話,它們存在幾處區別甚微的地方。
其一,代碼14.17的代碼量比代碼14.16還要多;
其二,代碼14.16的Variable是真正意義上的全局變量,反之代碼14.17則是山寨。
除此之外,代碼14.17還是一只核心操作組成,或者代碼14.17是有一只函數而已。
如果主函數,函數A還有函數B之間只有簡單操作,而且數據的傳遞量也不多的話,那么僅有一只核心操作也沒有什么問題。相反的,如果函數之間不僅有復雜的操作,而且數據的傳遞量也很多的話,獨秀的核心操作就要舉白旗投降了。為此,我們必須借助多模塊的力量來解決復雜的操作,但是多模塊之間又如何共享數據呢?首先,讓我們換個思路思考問題。
1. unsigned char Variable; // 儲存類 2. void FunctionA( unsinged char A ) { Variable = A; } // 功能類 3. unsinged char FunctionB( void ) { return Variable; } // 功能類 4. int main() { ...... } // 控制類
代碼14.18
如代碼14.18所示,全局變量視為儲存類,函數A與函數B視為功能類,至於主函數視為控制類。
圖14.2 代碼14.18的建模圖。
代碼14.18經過分類以后,大致的建模布局如圖14.2所示。一只名為main的控制模塊充當中介,次序調度,協調者等角色。其中,A功能模塊與B功能模塊負責最基本的操作,variable儲存模塊則負責儲存操作。余下,所有模塊都經由問答信號聯系起來,至於Verilog則可以這樣表示:
1. module ( ... ); 2. 3. wire [2:0]CallU1; 4. main_ctrlmod U1 5. ( 6. .oCall( CallU1 ), 7. .iDone( { DoneU1, DoneU2, DoneU3 } ), 8. ... 9. ); 10. 11. wire DoneU2; 12. a_funcmod U2 13. ( 14. .iCall( CallU1[0] ), 15. .oDone( DoneU2 ), 16. ... 17. ); 18. 19. wire DoneU3; 20. b_funcmod U3 21. ( 22. .iCall( CallU1[1] ), 23. .oDone( DoneU3 ), 24. ... 25. ); 26. 27. wire DoneU4; 28. varibale_savemod U1 29. ( 30. .iCall( CallU1[2] ), 31. .oDone( DoneU4 ), 32. ... 33. ); 34. 35. endmodule
代碼14.18
如代碼14.18所示,組合模塊的內容包含,main控制模塊為實例U1,a功能模塊與b功能模塊為實例U2~U3,variable儲存模塊為實例 U4。最后,各個模塊經由問答信號 Call/Done 聯系起來。
前面的例子告訴我們,描述語言在變量上的運用,遠遠不及順序語言那么便捷,畢竟描述語言沒有底層補助,而且模仿它人也超麻煩。話雖如此,這是描述語言的缺點也是優點 ... 優點?筆者有沒有搞錯?那么麻煩還稱為優點,筆者是不是腦子進水了?這位同學別猴急,筆者會慢慢解釋的。
1. unsigned char LUT[4] = { 10, 20, 30, 40 }; 2. int main() 3. { 4. int D1; 5. D1 = LUT[1] + LUT[2]; 6. ... 7. }
代碼14.19
如代碼14.19所示,第1行聲明位寬為8,長度為4的LUT查表,第2~7行則是查表的運用。表面上,順序語言雖有驚人的便捷性,不過底子里卻是一片死殘,尤其是時鍾的利用率更是慘不忍睹。那些寫過算法的同學一定知道,查表常常用來優化算法的運算速度 ... 簡單來說,查表就是順序語言“空間換速度”的優化手段。
查表既是ROM也是一種儲存方式。如果把話說難聽一點,所謂查表也不過是順序語言在利用數組模仿ROM而已,它除了便捷性好以外,無論是資源的消耗,還是時鍾的消耗等效率都遠遠不及描述語言的ROM。順序語言偶爾雖然也有山寨的FIFO,Shift等儲存方式,不過性能卻是差強人意。
順序語言之所以那么遜色,那是因為被鋼鐵一般堅固的順序結構綁得死死。述語言是自由的語言,結構也是自由。雖然自由結構為人們帶來許多麻煩,但是“儲存方式”可以描述的范疇,絕對超乎人們的估量。歸根究底,究竟是順序語言好,還是描述語言模比較厲害呢?除了見仁見智以外,答案也只有天知曉。
隨着時代不斷變遷,“儲存方式”的需求也逐漸成長,例如50年代需要rom,60年代需要ram,70年代需要 fifo。二十一世紀的今天,保守的規范再也無法壓抑“儲存方式”的放肆衍生,例如rom衍生出來靜態rom,單口rom,雙口rom等許多亞種;此外,fifo也衍生出同步fifo或者異步fifo等亞種。至於ram的亞種,比前兩者更加恐怖!不管怎么樣,大伙都是筆者的好孩子,亦即 ××_savemod。
雖然偉大的官方早已准備數之不盡的儲存模塊,但是筆者還是強調手動建模比較好,因為官方的東西有太多限制了。此刻,可能有人跳出來反駁道:“為什么不用官方插件模塊,它們既完整又便捷,那個白痴才不吃天上掉下來的餡餅!筆者是呆子!蠢貨!“。話說這位同學也別那么激動,如果讀者一路索取它人的東西,學習只會本末倒置而已。
除此之外,官方插件模塊是商業的產物,不僅自定義有限內容也是隱性,而且還是不擇不扣的快餐。快餐即美味也方便,偶爾吃下還不錯,但是長期食用就會危害健康,危害學習。
“fifo插件的數據位寬能不能設為11位?”,某人求救道。
“ram插件怎樣調用?怎樣仿真?”,某人求救道。
類似問題每月至少出現數十次,而且還是快餐愛好者提問的。筆者也有類似的經驗,所以非常明白這種心境。年輕的筆者就是愛好快餐,凡事拿來主義,伸手比吃飯更多。漸漸地,筆者愈來愈懶,能不增反降,最終變成只會求救的肥仔而已。后悔以后,筆者才腳踏實地自力建模,慢慢減肥。
在此,筆者滔滔不絕只想告知讀者 ... 自由結構雖然麻煩,不過這是將想象力具體化的關鍵因素,儲存模塊的潛能遠超保守的規范。規范有時候就像一粒絆腳石,讓人不經意跌倒一次又一次,阻礙人們前進,限制人們想象,最后讓人成為不動手即不動腦的懶人。最后,讓我們建立一只不規格又畸形的儲存模塊作為本實驗的句號。
圖14.3 實驗十四的建模圖。
圖14.3是實驗十四的建模圖,組合模塊 savemod_demo 的內容包括一支核心操作,一只數碼管基礎模塊,還有一只名字帥到掉渣的儲存模塊。核心操作會拉高 oEn,並且將相關的 Addr 與 Data 寫入儲存模塊,緊接着該儲存模塊會經由 oData驅動數碼管基礎模塊。事不宜遲,讓我們先來瞧瞧推擠位移儲存模塊這位帥哥。
pushshift_savemod.v
圖14.4 推擠位移儲存模塊的建模圖。
顧名思義,該模塊是推擠功能再加上位移功能的儲存模塊,左邊是儲存模塊常見的iEn,iAddr與iData,右邊則是超乎常規的oData。
1. module pushshift_savemod 2. ( 3. input CLOCK,RESET, 4. input iEn, 5. input [3:0]iAddr, 6. input [3:0]iData, 7. output [23:0]oData 8. ); 第3~7行是相關的出入端聲明。 9. reg [3:0] RAM [15:0]; 10. reg [23:0] D1; 11. 12. always @ ( posedge CLOCK or negedge RESET ) 13. if( !RESET ) 14. begin 15. D1 <= 24'd0; 16. end 第9行是片上內存RAM的聲明,第10行則是寄存器D1的聲明。第15行則是D1的復位操作。 17. else if( iEn ) 18. begin 19. RAM[ iAddr ] <= iData; 20. D1[3:0] <= RAM[ iAddr ]; 21. D1[7:4] <= D1[3:0]; 22. D1[11:8] <= D1[7:4]; 23. D1[15:12] <= D1[11:8]; 24. D1[19:16] <= D1[15:12]; 25. D1[23:20] <= D1[19:16]; 26. end 27. 28. assign oData = D1; 29. 30. endmodule
第17行表示 iEn不拉高該模塊就不工作。第18~26行是該模塊的核心操作,第19行表示RAM將iData儲存至 iAddr指定的位置;第20行表示,RAM將iAddr指定的內容賦予D1[3:0]。如此一來,第19行與第20行的結合就成為推擠功能。至於第21~25行則是6個深度的位移功能(即4位寬為一個深度), iEn每拉高一個時鍾,D1的內容就向左移動一個深度。
savemod_demo.v
該組合模塊的連線部署根據圖14.3,具體內容我們還是來看代碼吧。
1. module savemod_demo 2. ( 3. input CLOCK,RESET, 4. output [7:0]DIG, 5. output [5:0]SEL 6. ); 以上內容是相關的出入端聲明。 7. reg [3:0]i; 8. reg [3:0]D1,D2; // D1 for Address, D2 for Data 9. reg isEn; 10. 11. always @ ( posedge CLOCK or negedge RESET ) // Core 12. if( !RESET ) 13. begin 14. i <= 4'd0; 15. { D1,D2 } <= 8'd0; 16. isEn <= 1'b0; 17. end 18. else
以上內容是相關的寄存器聲明以及復位操作。其中D1用來暫存地址數據,D2用來暫存讀寫數據。第12~17行是這些寄存器的復位操作。
19. case( i ) 20. 21. 0: 22. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hA; i <= i + 1'b1; end 23. 24. 1: 25. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hB; i <= i + 1'b1; end 26. 27. 2: 28. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hC; i <= i + 1'b1; end 29. 30. 3: 31. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hD; i <= i + 1'b1; end 32. 33. 4: 34. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hE; i <= i + 1'b1; end 35. 36. 5: 37. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'hF; i <= i + 1'b1; end 38. 39. 6: 40. begin isEn <= 1'b1; D1 <= 4'd0; D2 <= 4'h0; i <= i + 1'b1; end 41. 42. 7: 43. begin isEn <= 1'b0; i <= i; end 44. 45. endcase 46.
以上內容為核心操作,操作過程如下:
步驟0為地址0寫入數據 4’hA;,將原本的數據擠出來,並且發生位移。
步驟1為地址0寫入數據 4’hB;,將4’hA擠出來,並且發生位移。
步驟2為地址0寫入數據 4’hC;,將4’hB擠出來,並且發生位移。
步驟3為地址0寫入數據 4’hD;,將4’hC擠出來,並且發生位移。
步驟4為地址0寫入數據 4’hE;,將4’hD擠出來,並且發生位移。
步驟5為地址0寫入數據 4’hF,將4’hE擠出來,並且發生位移。
步驟6為地址0寫入數據 4’d0,將4’hF擠出來,並且發生位移。
步驟7結束操作。
圖14.5 savemod_demo 部分時序圖。
圖14.5是 savemod_demo 部分重要的理想時序圖,其中isEn,D1與D2 是核心操作所發送的數據,至於RAM[0]與oData是推擠位移儲存模塊的內部狀況與輸出結果。時序過程如下:
T0,核心操作拉高isEn,發送4’d0地址數據與4’hA讀寫數據。
T1,核心操作拉高isEn,發送4’d0地址數據與4’hB讀寫數據。儲存模塊將4’hA載入地址0。
T2,核心操作拉高isEn,發送4’d0地址數據與4’hC讀寫數據。儲存模塊將4’hB載入地址0,並且將數據 4’hA擠出,oData的結果為 24’h00000A。
T3,核心操作拉高isEn,發送4’d0地址數據與4’hD讀寫數據。儲存模塊將4’hC載入地址0,並且將數據 4’hB擠出,同時發生位移,oData的結果為 24’h0000AB。
T4,核心操作拉高isEn,發送4’d0地址數據與4’hE讀寫數據。儲存模塊將4’hD載入地址0,並且將數據 4’hC擠出,同時發生位移,oData的結果為 24’h000ABC。
T5,核心操作拉高isEn,發送4’d0地址數據與4’hF讀寫數據。儲存模塊將4’hE載入地址0,並且將數據 4’hD擠出,同時發生位移,oData的結果為 24’h00ABCD。
T6,核心操作拉高isEn,發送4’d0地址數據與4’d0讀寫數據。儲存模塊將4’hF載入地址0,並且將數據 4’hE擠出,同時發生位移,oData的結果為 24’h0ABCDE。
T7,儲存模塊將4’d0載入地址0,並且將數據 4’hF擠出,同時發生位移,oData的結果為 24’hABCDEF。
47. wire [23:0]DataU1; 48. 49. pushshift_savemod U1 50. ( 51. .CLOCK( CLOCK ), 52. .RESET( RESET ), 53. .iEn( isEn ), // < Core 54. .iAddr( D1 ), // < Core 55. .iData( D2 ), // < Core 56. .oData( DataU1 ) // > U2 57. ); 58.
第47~58行是該儲存模塊的實例化。
59. smg_basemod U2 60. ( 61. .CLOCK( CLOCK ), 62. .RESET( RESET ), 63. .DIG( DIG ), // top 64. .SEL( SEL ), // top 65. .iData( DataU1 ) // < U1 66. ); 67. 68. endmodule
第59~66行是數碼管基礎模塊的實例化。編譯完畢便下載程序,如果數碼管從左至右顯示“ABCDEF”,那么表示實驗成功。最后還是要強調一下,推擠位移目前是沒有意義的儲存模塊,可是實驗十四的目的也非常清楚,就是解釋儲存模塊,演示畸形的儲存模塊。





