【黑金原創教程】【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

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結束操作。

clip_image010

圖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”,那么表示實驗成功。最后還是要強調一下,推擠位移目前是沒有意義的儲存模塊,可是實驗十四的目的也非常清楚,就是解釋儲存模塊,演示畸形的儲存模塊。


免責聲明!

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



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