1. 程式人生 > >【黑金原創教程】【FPGA那些事兒-驅動篇I 】實驗十四:儲存模組

【黑金原創教程】【FPGA那些事兒-驅動篇I 】實驗十四:儲存模組

實驗十四比起動手筆者更加註重原理,因為實驗十四要討論的東西,不是其它而是低階建模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。

clip_image002

圖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視為功能類,至於主函式視為控制類。

clip_image004

圖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外掛怎樣呼叫?怎樣模擬?”,某人求救道。

類似問題每月至少出現數十次,而且還是快餐愛好者提問的。筆者也有類似的經驗,所以非常明白這種心境。年輕的筆者就是愛好快餐,凡事拿來主義,伸手比吃飯更多。漸漸地,筆者愈來愈懶,能不增反降,最終變成只會求救的肥仔而已。後悔以後,筆者才腳踏實地自力建模,慢慢減肥。

在此,筆者滔滔不絕只想告知讀者 ... 自由結構雖然麻煩,不過這是將想象力具體化的關鍵因素,儲存模組的潛能遠超保守的規範。規範有時候就像一粒絆腳石,讓人不經意跌倒一次又一次,阻礙人們前進,限制人們想象,最後讓人成為不動手即不動腦的懶人。最後,讓我們建立一隻不規格又畸形的儲存模組作為本實驗的句號。

clip_image006

圖14.3 實驗十四的建模圖。

圖14.3是實驗十四的建模圖,組合模組 savemod_demo 的內容包括一支核心操作,一隻數碼管基礎模組,還有一隻名字帥到掉渣的儲存模組。核心操作會拉高 oEn,並且將相關的 Addr 與 Data 寫入儲存模組,緊接著該儲存模組會經由 oData驅動數碼管基礎模組。事不宜遲,讓我們先來瞧瞧推擠位移儲存模組這位帥哥。

pushshift_savemod.v

clip_image008

圖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