【正点原子FPGA连载】第七章Verilog HDL语法 -摘自【正点原子】新起点之FPGA开发指南_V2.1
1)實驗平臺:正點原子新起點V2開發板
2)平臺購買地址:https://detail.tmall.com/item.htm?id=609758951113
2)全套實驗源碼+手冊+視頻下載地址:http://www.openedv.com/thread-300792-1-1.html
3)對正點原子FPGA感興趣的同學可以加群討論:994244016
4)關注正點原子公眾號,獲取最新資料更新
第七章Verilog HDL語法
Verilog HDL(Hardware Description Language)是在用途最廣泛的C語言的基礎上發展起來的一種硬件描述語言,具有靈活性高、易學易用等特點。Verilog HDL可以在較短的時間內學習和掌握,目前已經在FPGA開發/IC設計領域占據絕對的領導地位。
本章包括以下幾個部分:
77.1Verilog概述
7.2Verilog基礎知識
7.3Verilog程序框架
7.4Verilog高級知識點
7.5Verilog編程規范
7.1Verilog概述
本節主要描述了Verilog HDL(以下簡稱Verilog)簡介、Verilog和VHDL以及和C語言的區別。
7.1.1Verilog簡介
Verilog是一種硬件描述語言,以文本形式來描述數字系統硬件的結構和行為的語言,用它可以表示邏輯電路圖、邏輯表達式,還可以表示數字邏輯系統所完成的邏輯功能。
數字電路設計者利用這種語言,可以從頂層到底層逐層描述自己的設計思想,用一系列分層次的模塊來表示極其復雜的數字系統。然后利用電子設計自動化(EDA)工具,逐層進行仿真驗證,再把其中需要變為實際電路的模塊組合,經過自動綜合工具轉換到門級電路網表。接下來,再用專用集成電路ASIC或FPGA自動布局布線工具,把網表轉換為要實現的具體電路結構。
Verilog語言最初是于1983年由Gateway Design Automation公司為其模擬器產品開發的硬件建模語言。由于他們的模擬、仿真器產品的廣泛使用,Verilog HDL作為一種便于使用且實用的語言逐漸為眾多設計者所接受。在一次努力增加語言普及性的活動中,Verilog HDL語言于1990年被推向公眾領域。Verilog語言于1995年成為IEEE標準,稱為IEEE Std1364-1995,也就是通常所說的Verilog-95。
設計人員在使用Verilog-95的過程中發現了一些可改進之處。為了解決用戶在使用此版本Verilog過程中反映的問題,Verilog進行了修正和擴展,這個擴展后的版本后來成為了電氣電子工程師學會Std1364-2001標準,即通常所說的Verilog-2001。Verilog-2001是對Verilog-95的一個重大改進版本,它具備一些新的實用功能,例如敏感列表、多維數組、生成語句塊、命名端口連接等。目前,Verilog-2001是Verilog的最主流版本,被大多數商業電子設計自動化軟件支持。
7.1.2為什么需要Verilog
在FPGA設計里面,我們有多種設計方式,如原理圖設計方式、編寫描述語言(代碼)等方式。一開始很多工程師對原理圖設計方式很鐘愛,這種輸入方式能夠很直觀的看到電路結構并快速理解,但是隨著電路設計規模的不斷增加,邏輯電路設計也越來越復雜,這種設計方式已經越來越不滿足實際的項目需求了。這個時候Verilog語言就取而代之了,目前Verilog已經在FPGA開發/IC設計領域占據絕對的領導地位。
7.1.3Verilog和VHDL區別
這兩種語言都是用于數字電路系統設計的硬件描述語言,而且都已經是IEEE的標準。 VHDL 1987年成為標準,而Verilog是1995年才成為標準的。這是因為VHDL是美國軍方組織開發的,而Verilog是由一個公司的私有財產轉化而來。為什么Verilog能成為IEEE標準呢?它一定有其獨特的優越性才行,所以說Verilog有更強的生命力。
這兩者有其共同的特點:
但是兩者也各有特點。Verilog推出已經有20年了,擁有廣泛的設計群體,成熟的資源,且Verilog容易掌握,只要有C語言的編程基礎,通過比較短的時間,經過一些實際的操作,可以在1個月左右掌握這種語言。而VHDL設計相對要難一點,這個是因為VHDL不是很直觀,一般認為至少要半年以上的專業培訓才能掌握。
近10年來,EDA界一直在對數字邏輯設計中究竟用哪一種硬件描述語言爭論不休,目前在美國,高層次數字系統設計領域中,應用Verilog和VHDL的比率是80%和20%;日本與中國臺灣和美國差不多;而在歐洲VHDL發展的比較好;在中國很多集成電路設計公司都采用Verilog。我們推薦大家學習Verilog,本教程全部的例程都是使用Verilog開發的。
7.1.4Verilog和C的區別
Verilog是硬件描述語言,在編譯下載到FPGA之后,FPGA會生成電路,所以Verilog全部是并行處理與運行的;C語言是軟件語言,編譯下載到單片機/CPU之后,還是軟件指令,而不會根據你的代碼生成相應的硬件電路,而單片機/CPU處理軟件指令需要取址、譯碼、執行,是串行執行的。
Verilog和C的區別也是FPGA和單片機/CPU的區別,由于FPGA全部并行處理,所以處理速度非常快,這個是FPGA的最大優勢,這一點是單片機/CPU替代不了的。
7.2Verilog基礎知識
本節主要講解了Verilog的基礎知識,包括5個小節,下面我們分別給大家介紹這5個小節的內容。
7.2.1Verilog的邏輯值
我們先看下邏輯電路中有四種值,即四種狀態:
邏輯 0:表示低電平,也就是對應我們電路的GND;
邏輯 1:表示高電平,也就是對應我們電路的VCC;
邏輯 X:表示未知,有可能是高電平,也有可能是低電平;
邏輯 Z:表示高阻態,外部沒有激勵信號是一個懸空狀態。
如下圖所示:
圖 7.2.1 Verilog邏輯值
7.2.2Verilog的標識符
1)定義
標識符(identifier)用于定義模塊名、端口名和信號名等。Verilog的標識符可以是任意一組字母、數字、和(下劃線)符號的組合,但標識符的第一個字符必須是字母或者下劃線。另外,標識符是區分大小寫的。以下是標識符的幾個例子:CountCOUNT//與Count不同。R5668FIVE和_(下劃線)符號的組合,但標識符的第一個字符必須是字母或者下劃線。另外,標識符是區分大小寫的。以下是標識符的幾個例子: Count COUNT //與Count不同。 R56_68 FIVE和(?下劃線)符號的組合,但標識符的第一個字符必須是字母或者下劃線。另外,標識符是區分大小寫的。以下是標識符的幾個例子:CountCOUNT//與Count不同。R566?8FIVE
雖然標識符寫法很多,但是要簡潔、清晰、易懂,推薦寫法如下:
count
fifo_wr
不建議大小寫混合使用,普通內部信號建議全部小寫,參數定義建議大寫,另外信號命名最好體現信號的含義。
2)規范建議
以下是一些書寫規范的要求:
1、用有意義的有效的名字如sum、cpu_addr等。
2、用下劃線區分詞語組合,如cpu_addr。
3、采用一些前綴或后綴,比如:時鐘采用clk前綴:clk_50m,clk_cpu;低電平采用_n后綴:enable_n;
4、統一縮寫,如全局復位信號rst。
5、同一信號在不同層次保持一致性,如同一時鐘信號必須在各模塊保持一致。
6、自定義的標識符不能與保留字(關鍵詞)同名。
7、參數統一采用大寫,如定義參數使用SIZE。
7.2.3Verilog的數字進制格式
Verilog數字進制格式包括二進制、八進制、十進制和十六進制,一般常用的為二進制、十進制和十六進制。
二進制表示如下:4’b0101表示4位二進制數字0101;
十進制表示如下:4’d2表示4位十進制數字2(二進制0010);
十六進制表示如下:4’ha表示4位十六進制數字a(二進制1010),十六進制的計數方式為0,1,2…9,a,b,c,d,e,f,最大計數為f(f:十進制表示為15)。
當代碼中沒有指定數字的位寬與進制時,默認為32位的十進制,比如100,實際上表示的值為32’d100。
7.2.4Verilog的數據類型
在Verilog語法中,主要有三大類數據類型,即寄存器類型、線網類型和參數類型。從名稱中,我們可以看出,真正在數字電路中起作用的數據類型應該是寄存器類型和線網類型。
1)寄存器類型
寄存器類型表示一個抽象的數據存儲單元,它只能在always語句和initial語句中被賦值,并且它的值從一個賦值到另一個賦值過程中被保存下來。如果該過程語句描述的是時序邏輯,即always語句帶有時鐘信號,則該寄存器變量對應為寄存器;如果該過程語句描述的是組合邏輯,即always語句不帶有時鐘信號,則該寄存器變量對應為硬件連線;寄存器類型的缺省值是x(未知狀態)。
寄存器數據類型有很多種,如reg、integer、real等,其中最常用的就是reg類型,它的使用方法如下:
2)線網類型
線網表示Verilog結構化元件間的物理連線。它的值由驅動元件的值決定,例如連續賦值或門的輸出。如果沒有驅動元件連接到線網,線網的缺省值為z(高阻態)。線網類型同寄存器類型一樣也是有很多種,如tri和wire等,其中最常用的就是wire類型,它的使用方法如下:
3)參數類型
我們再來看下參數類型,參數其實就是一個常量,常被用于定義狀態機的狀態、數據位寬和延遲大小等,由于它可以在編譯時修改參數的值,因此它又常被用于一些參數可調的模塊中,使用戶在實例化模塊時,可以根據需要配置參數。在定義參數時,我們可以一次定義多個參數,參數與參數之間需要用逗號隔開。這里我們需要注意的是參數的定義是局部的,只在當前模塊中有效。它的使用方法如下:
7.2.5Verilog的運算符
大家看完了Verilog的數據類型,我們再來介紹下Verilog的運算符。Verilog中的運算符按照功能可以分為下述類型:1、算術運算符、 2、關系運算符、3、邏輯運算符、 4、條件運算符、 5、位運算符、 6、移位運算符、 7、拼接運算符。下面我們分別對這些運算符進行介紹。
1)算術運算符
算術運算符,簡單來說,就是數學運算里面的加減乘除,數字邏輯處理有時候也需要進行數字運算,所以需要算術運算符。常用的算術運算符主要包括加減乘除和模除(模除運算也叫取余運算)如下表所示:
大家要注意下,Verilog實現乘除比較浪費組合邏輯資源,尤其是除法。一般2的指數次冪的乘除法使用移位運算來完成運算,詳情可以看移位運算符章節。非2的指數次冪的乘除法一般是調用現成的IP,Quartus/Vivado等工具軟件會有提供,不過這些工具軟件提供的IP也是由最底層的組合邏輯(與或非門等)搭建而成的。
2)關系運算符
關系運算符主要是用來做一些條件判斷用的,在進行關系運算符時,如果聲明的關系是假的,則返回值是0,如果聲明的關系是真的,則返回值是1;所有的關系運算符有著相同的優先級別,關系運算符的優先級別低于算術運算符的優先級別如下表所示。
3)邏輯運算符
邏輯運算符是連接多個關系表達式用的,可實現更加復雜的判斷,一般不單獨使用,都需要配合具體語句來實現完整的意思,如下表所示。
表 7.2.3 邏輯運算符
4)條件運算符
條件操作符一般來構建從兩個輸入中選擇一個作為輸出的條件選擇結構,功能等同于 always中的if-else語句,如下表所示。
表 7.2.4 條件運算符
5)位運算符
位運算符是一類最基本的運算符,可以認為它們直接對應數字邏輯中的與、或、非門等邏輯門。常用的位運算符如下表所示。
表 7.2.5 位運算符
位運算符的與、或、非與邏輯運算符邏輯與、邏輯或、邏輯非使用時候容易混淆,邏輯運算符一般用在條件判斷上,位運算符一般用在信號賦值上。
6)移位運算符
移位運算符包括左移位運算符和右移位運算符,這兩種移位運算符都用0來填補移出的空位。如下表所示。
表 7.2.6 移位運算符
符號 使用方法 說明
假設a有8bit數據位寬,那么a<<2,表示a左移2bit,a還是8bit數據位寬,a的最高2bit數據被移位丟棄了,最低2bit數據固定補0。如果a是3(二進制:00000011),那么3左移2bit,3<<2,就是12(二進制:00001100)。一般使用左移位運算代替乘法,右移位運算代替除法,但是這種也只能表示2的指數次冪的乘除法。
7)拼接運算符
Verilog中有一個特殊的運算符是C語言中沒有的,就是位拼接運算符。用這個運算符可以把兩個或多個信號的某些位拼接起來進行運算操作。如下表所示。
表 7.2.7 位拼接運算符
8)運算符的優先級
介紹完了這么多運算符,大家可能會想到究竟哪個運算符高,哪個運算符低。為了便于大家查看這些運算符的優先級,我們將它們制作成了表格,如下表所示。
表 7.2.8 運算符的優先級
? 最低
7.3Verilog程序框架
在介紹Verilog程序框架之前,我們先來看下Verilog一些基本語法,基礎語法主要包括注釋和關鍵字。
7.3.1注釋
Verilog HDL中有兩種注釋的方式,一種是以“/”符號開始,“/”結束,在兩個符號之間的語句都是注釋語句,因此可擴展到多行。如:
/* statement1 ,
statement2,
…
statementn */
以上n個語句都是注釋語句。
另一種是以//開頭的語句,它表示以//開始到本行結束都屬于注釋語句。如:
//statement1
我們建議的寫法:使用//作為注釋。
7.3.2關鍵字
Verilog和C語言類似,都因編寫需要定義了一系列保留字,叫做關鍵字(或關鍵詞)。這些保留字是識別語法的關鍵。我們給大家列出了Verilog中的關鍵字,如下表所示。
表 7.3.1 Verilog的所有關鍵字
雖然上表列了很多,但是實際經常使用的不是很多,實際經常使用的主要如下表所示。
表 7.3.2 Verilog常用的關鍵字
關鍵字 含義
注意只有小寫的關鍵字才是保留字。例如,標識符always(這是個關鍵詞)與標識符ALWAYS(非關鍵詞)是不同的。
7.3.3程序框架
我們以LED流水燈程序為例來給大家展示Verilog的程序框架,代碼如下所示(注意:代碼中前面的行號只是為了方便大家閱讀代碼與快速定位到行號的位置,在實際編寫代碼時不可以添加行號,否則編譯代碼時會報錯)。
首先//開頭的都是注釋,這個之前我們講解過了。下面我們來看下具體的解釋。
第1行為模塊定義,模塊定義以module開始,endmodule結束,如59行所示。
其次2到5行為端口定義,需要定義led模塊的輸入信號和輸出信號,此處輸入信號為系統時鐘和復位信號,輸出為led控制信號。
7到9行為參數parameter定義,語法如7到9行所示,定義parameter的好處是可以靈活改變參數數字就能控制一些計數器最大計數值或者信號位寬的最大位寬。
12到14行為reg信號定義,reg信號一般情況下代表寄存器,比如此處控制0.5秒使能信號的計數器counter。
16到17行為wire信號定義,wire信號就是硬件連線,比如此處的counter_en,代表計數到最大值時產生高電平使能,本質上是一個硬件連線,其實代表的是一些計數器/寄存器做邏輯判斷的結果。
19到21行為moudle開始的注釋,不添加工具綜合也不會報錯,但是我們推薦添加,作為一個良好的編程規范。
23到24行為assign語句的樣式,條件成立選擇1,否則選擇0。
26到34行是always語句的樣式,27行代表在時鐘上升沿或者復位的下降沿進行信號觸發。begin/end代表語句的開始和結束。28到33行為if/else語句,和C語言是比較類似的。29行的“<=”標記代表信號是非阻塞賦值,信號賦值有非阻塞賦值和阻塞賦值兩個方式,這個我們后面會詳細解釋。
36和42行也是一個always語句,和26到34行類似。
44和57行也是一個always語句,不過這個always語句中嵌入了一個case語句,case語句的語法如49到55行所示,需要一個case關鍵字開始,endcase關鍵字結束,default作為默認分支,和C語言也是類似的。當然case語句也可以用在不帶時鐘的always語句中,不過本例子的always都是帶有時鐘的。不帶時鐘的always和帶時鐘的always語句的差異這個我們后面也會詳細解釋。
59行是endmodule標記,代表模塊的結束。
在這里需要補充一點的是,一些初學者可能會有這樣一個疑問,在always語句中編寫if語句或else語句時,后面需要加begin和end嗎?其實這個主要看if條件后面跟著幾條賦值語句,如果只有一條賦值語句時,if后面可以加begin和end,也可以不加;如果超過一條賦值語句時,就必須加上begin和end。
if條件只有一條賦值語句時,下面兩種寫法都是可以的,這里更推薦第一種寫法,因為第二種寫法會占用更多的行號,代碼如下所示:
好了,程序框架就講解完了,大家是不是覺得也很簡單呢?這些都是基本的語法規范,希望大家能記住這些基礎的知識點。如果有些地方大家還是覺得比較抽象,一時很難理解,這個沒有關系,相信大家會在后面的學習中,會慢慢理解的。
7.4Verilog高級知識點
前幾節主要介紹了Verilog一些基礎的知識點和程序框架,本節給大家介紹一些高級的知識點。高級知識點包括阻塞賦值和非阻塞賦值、assign和always語句差異、什么是鎖存器、狀態機、模塊化設計等。
7.4.1阻塞賦值(Blocking)
阻塞賦值,顧名思義即在一個always塊中,后面的語句會受到前語句的影響,具體來說就是在同一個always中,一條阻塞賦值語句如果沒有執行結束,那么該語句后面的語句就不能被執行,即被“阻塞”。也就是說always塊內的語句是一種順序關系,這里和C語言很類似。符號“=”用于阻塞的賦值(如:b = a;),阻塞賦值“=”在begin和end之間的語句是順序執行,屬于串行語句。
在這里定義兩個縮寫:
RHS:賦值等號右邊的表達式或變量可以寫作RHS表達式或RHS變量;
LHS:賦值等號左邊的表達式或變量可以寫作LHS表達式或LHS變量;
阻塞賦值的執行可以認為是只有一個步驟的操作,即計算RHS的值并更新LHS,此時不允許任何其他語句的干擾,所謂的阻塞的概念就是值在同一個always塊中,其后面的賦值語句從概念上來講是在前面一條語句賦值完成后才執行的。
為了方便大家理解阻塞賦值的概念以及阻塞賦值和非阻塞賦值的區別,我們這里以在時序邏輯下使用阻塞賦值為例來實現這樣一個功能:在復位的時候,a=1,b=2,c=3;而在沒有復位的時候,a的值清零,同時將a的值賦值給b,b的值賦值給c,代碼以及信號波形圖如下圖所示:
圖 7.4.1 阻塞賦值代碼
圖 7.4.2 阻塞賦值的信號波形圖
代碼中使用的是阻塞賦值語句,從波形圖中可以看到,在復位的時候(rst_n=0),a=1,b=2,c=3;而結束復位之后(波形圖中的0時刻),當clk的上升沿到來時(波形圖中的2時刻),a=0,b=0,c=0。這是因為阻塞賦值是在當前語句執行完成之后,才會執行后面的賦值語句,因此首先執行的是a=0,賦值完成后將a的值賦值給b,由于此時a的值已經為0,所以b=a=0,最后執行的是將b的值賦值給c,而b的值已經賦值為0,所以c的值同樣等于0。
7.4.2非阻塞賦值(Non-Blocking)
符號“<=”用于非阻塞賦值(如:b <= a;),非阻塞賦值是由時鐘節拍決定,在時鐘上升到來時,執行賦值語句右邊,然后將begin-end之間的所有賦值語句同時賦值到賦值語句的左邊,注意:是begin—end之間的所有語句,一起執行,且一個時鐘只執行一次,屬于并行執行語句。這個是和C語言最大的一個差異點,大家要逐步理解并行執行的概念。
非阻塞賦值的操作過程可以看作兩個步驟:
(1)賦值開始的時候,計算RHS;
(2)賦值結束的時候,更新LHS。
所謂的非阻塞的概念是指,在計算非阻塞賦值的RHS以及LHS期間,允許其它的非阻塞賦值語句同時計算RHS和更新LHS。
我們下面使用非阻塞賦值同樣來實現這樣一個功能:在復位的時候,a=1,b=2,c=3;而在沒有復位的時候,a的值清零,同時將a的值賦值給b,b的值賦值給c,代碼以及信號波形圖如下圖所示:
圖 7.4.3 非阻塞賦值代碼
圖 7.4.4 非阻塞賦值的信號波形圖
代碼中使用的是非阻塞賦值語句,從波形圖中可以看到,在復位的時候(rst_n=0),a=1,b=2,c=3;而結束復位之后(波形圖中的0時刻),當clk的上升沿到來時(波形圖中的2時刻),a=0,b=1,c=2。這是因為非阻塞賦值在計算RHS和更新LHS期間,允許其它的非阻塞賦值語句同時計算RHS和更新LHS。在波形圖中的2時刻,RHS的表達是0、a、b,分別等于0、1、2,這三條語句是同時更新LHS,所以a、b、c的值分別等于0、1、2。
在了解了阻塞賦值和非阻塞賦值的區別之后,有些朋友可能還是對什么時候使用阻塞賦值,什么時候使用非阻塞賦值有些疑惑,在這里給大家總結如下。
在描述組合邏輯電路的時候,使用阻塞賦值,比如assign賦值語句和不帶時鐘的always賦值語句,這種電路結構只與輸入電平的變化有關系,代碼如下:
示例1:assign賦值語句
assign data = (data_en == 1’b1) ? 8’d255 : 8’d0;
在描述時序邏輯的時候,使用非阻塞賦值,綜合成時序邏輯的電路結構,比如帶時鐘的always語句;這種電路結構往往與觸發沿有關系,只有在觸發沿時才可能發生賦值的變化,代碼如下:
示例3: always @(posedge sys_clk or negedge sys_rst_n) begin if (!sys_rst_n) begina <= 1'b0;b <= 1'b0;endelse begina <= c;b <= d;end end7.4.3assign和always區別
assign語句和always語句是Verilog中的兩個基本語句,這兩個都是經常使用的語句。
assign語句使用時不能帶時鐘。
always語句可以帶時鐘,也可以不帶時鐘。在always不帶時鐘時,邏輯功能和assign完全一致,都是只產生組合邏輯。比較簡單的組合邏輯推薦使用assign語句,比較復雜的組合邏輯推薦使用always語句。示例如下:
7.4.4帶時鐘和不帶時鐘的always
always語句可以帶時鐘,也可以不帶時鐘。在always不帶時鐘時,邏輯功能和assign完全一致,雖然產生的信號定義還是reg類型,但是該語句產生的還是組合邏輯。
在always帶時鐘信號時,這個邏輯語句才能產生真正的寄存器,如下示例counter就是真正的寄存器。
26 //用于產生0.5秒使能信號的計數器 27 always @(posedge sys_clk or negedge sys_rst_n) begin 28 if (sys_rst_n == 1'b0) 29 counter <= 1'b0; 30 else if (counter_en) 31 counter <= 1'b0; 32 else 33 counter <= counter + 1'b1; 34 end7.4.5什么是latch
latch是指鎖存器,是一種對脈沖電平敏感的存儲單元電路。鎖存器和寄存器都是基本存儲單元,鎖存器是電平觸發的存儲器,寄存器是邊沿觸發的存儲器。兩者的基本功能是一樣的,都可以存儲數據。鎖存器是組合邏輯產生的,而寄存器是在時序電路中使用,由時鐘觸發產生的。
latch的主要危害是會產生毛刺(glitch),這種毛刺對下一級電路是很危險的。并且其隱蔽性很強,不易查出。因此,在設計中,應盡量避免latch的使用。
代碼里面出現latch的兩個原因是在組合邏輯中,if或者case語句不完整的描述,比如if缺少else分支,case缺少default分支,導致代碼在綜合過程中出現了latch。解決辦法就是if必須帶else分支,case必須帶default分支。
大家需要注意下,只有不帶時鐘的always語句if或者case語句不完整才會產生latch,帶時鐘的語句if或者case語句不完整描述不會產生latch。
下面為缺少else分支的帶時鐘的always語句和不帶時鐘的always語句,通過實際產生的電路圖可以看到第二個是有一個latch的,第一個仍然是普通的帶有時鐘的寄存器。
圖 7.4.5 缺少else的帶時鐘的always語句電路圖
圖 7.4.6 缺少else的不帶時鐘的always語句電路圖
7.4.6狀態機
Verilog是硬件描述語言,硬件電路是并行執行的,當需要按照流程或者步驟來完成某個功能時,代碼中通常會使用很多個if嵌套語句來實現,這樣就增加了代碼的復雜度,以及降低了代碼的可讀性,這個時候就可以使用狀態機來編寫代碼。狀態機相當于一個控制器,它將一項功能的完成分解為若干步,每一步對應于二進制的一個狀態,通過預先設計的順序在各狀態之間進行轉換,狀態轉換的過程就是實現邏輯功能的過程。
狀態機,全稱是有限狀態機(Finite State Machine,縮寫為FSM),是一種在有限個狀態之間按一定規律轉換的時序電路,可以認為是組合邏輯和時序邏輯的一種組合。狀態機通過控制各個狀態的跳轉來控制流程,使得整個代碼看上去更加清晰易懂,在控制復雜流程的時候,狀態機優勢明顯,因此基本上都會用到狀態機,如SDRAM控制器等。在本手冊提供的例程中,會有多個用到狀態機設計的例子,希望大家能夠慢慢體會和理解,并且能夠熟練掌握。
根據狀態機的輸出是否與輸入條件相關,可將狀態機分為兩大類,即摩爾(Moore)型狀態機和米勒(Mealy)型狀態機。
Mealy狀態機:組合邏輯的輸出不僅取決于當前狀態,還取決于輸入狀態。
Moore狀態機:組合邏輯的輸出只取決于當前狀態。
1)Mealy狀態機
米勒狀態機的模型如下圖所示,模型中第一個方框是指產生下一狀態的組合邏輯F,F是當前狀態和輸入信號的函數,狀態是否改變、如何改變,取決于組合邏輯F的輸出;第二框圖是指狀態寄存器,其由一組觸發器組成,用來記憶狀態機當前所處的狀態,狀態的改變只發生在時鐘的跳邊沿;第三個框圖是指產生輸出的組合邏輯G,狀態機的輸出是由輸出組合邏輯G提供的,G也是當前狀態和輸入信號的函數。
圖 7.4.7 Mealy狀態機模型
2)Moore狀態機
摩爾狀態機的模型如下圖所示,對比米勒狀態機的模型可以發現,其區別在于米勒狀態機的輸出由當前狀態和輸入條件決定的,而摩爾狀態機的輸出只取決于當前狀態。
圖 7.4.8 Moore狀態機模型
3)三段式狀態機
根據狀態機的實際寫法,狀態機還可以分為一段式、二段式和三段式狀態機。
一段式:整個狀態機寫到一個always模塊里面,在該模塊中既描述狀態轉移,又描述狀態的輸入和輸出。不推薦采用這種狀態機,因為從代碼風格方面來講,一般都會要求把組合邏輯和時序邏輯分開;從代碼維護和升級來說,組合邏輯和時序邏輯混合在一起不利于代碼維護和修改,也不利于約束。
二段式:用兩個always模塊來描述狀態機,其中一個always模塊采用同步時序描述狀態轉移;另一個模塊采用組合邏輯判斷狀態轉移條件,描述狀態轉移規律以及輸出。不同于一段式狀態機的是,它需要定義兩個狀態,現態和次態,然后通過現態和次態的轉換來實現時序邏輯。
三段式:在兩個always模塊描述方法基礎上,使用三個always模塊,一個always模塊采用同步時序描述狀態轉移,一個always采用組合邏輯判斷狀態轉移條件,描述狀態轉移規律,另一個always模塊描述狀態輸出(可以用組合電路輸出,也可以時序電路輸出)。
實際應用中三段式狀態機使用最多,因為三段式狀態機將組合邏輯和時序分開,有利于綜合器分析優化以及程序的維護;并且三段式狀態機將狀態轉移與狀態輸出分開,使代碼看上去更加清晰易懂,提高了代碼的可讀性,推薦大家使用三段式狀態機,本文也著重講解三段式。
三段式狀態機的基本格式是:
第一個always語句實現同步狀態跳轉;
第二個always語句采用組合邏輯判斷狀態轉移條件;
第三個always語句描述狀態輸出(可以用組合電路輸出,也可以時序電路輸出)。
在開始編寫狀態機代碼之前,一般先畫出狀態跳轉圖,這樣在編寫代碼時思路會比較清晰,下面以一個7分頻為例(對于分頻等較簡單的功能,可以不使用狀態機,這里只是演示狀態機編寫的方法),狀態跳轉圖如下圖所示:
圖 7.4.9 七分頻狀態跳轉圖
狀態跳轉圖畫完之后,接下來通過parameter來定義各個不同狀態的參數,如下代碼所示:
這里是使用獨熱碼的方式來定義狀態機,每個狀態只有一位為1,當然也可以直接定義成十進制的0,1,2……7。
因為我們定義成獨熱碼的方式,每一個狀態的位寬為7位,接下來還需要定義兩個7位的寄存器,一個用來表示當前狀態,另一個用來表示下一個狀態,如下所示:
接下來就可以使用三個always語句來開始編寫狀態機的代碼,第一個always采用同步時序描述狀態轉移,第二個always采用組合邏輯判斷狀態轉移條件,第三個always是描述狀態輸出,一個完整的三段式狀態機的例子如下代碼所示:
1 module divider7_fsm ( 2 //系統時鐘與復位 3 input sys_clk , 4 input sys_rst_n , 5 6 //輸出時鐘 7 output reg clk_divide_7 8 ); 9 10 //parameter define 11 parameter S0 = 7'b0000001; //獨熱碼定義方式 12 parameter S1 = 7'b0000010; 13 parameter S2 = 7'b0000100; 14 parameter S3 = 7'b0001000; 15 parameter S4 = 7'b0010000; 16 parameter S5 = 7'b0100000; 17 parameter S6 = 7'b1000000; 18 19 //reg define 20 reg [6:0] curr_st ; //當前狀態 21 reg [6:0] next_st ; //下一個狀態 22 23 //***************************************************** 24 //** main code 25 //***************************************************** 26 27 //狀態機的第一段采用同步時序描述狀態轉移 28 always @(posedge sys_clk or negedge sys_rst_n) begin 29 if (!sys_rst_n) 30 curr_st <= S0; 31 else 32 curr_st <= next_st; 33 end 34 35 //狀態機的第二段采用組合邏輯判斷狀態轉移條件 36 always @(*) begin 37 case (curr_st) 38 S0: next_st = S1; 39 S1: next_st = S2; 40 S2: next_st = S3; 41 S3: next_st = S4; 42 S4: next_st = S5; 43 S5: next_st = S6; 44 S6: next_st = S0; 45 default: next_st = S0; 46 endcase 47 end 48 49 //狀態機的第三段描述狀態輸出(這里采用時序電路輸出) 50 always @(posedge sys_clk or negedge sys_rst_n) begin 51 if (!sys_rst_n) 52 clk_divide_7 <= 1'b0; 53 else if ((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3)) 54 clk_divide_7 <= 1'b0; 55 else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6)) 56 clk_divide_7 <= 1'b1; 57 else 58 ; 59 end 60 61 endmodule在編寫狀態機代碼時首先要定義狀態變量(代碼中的參數S0~S6)與狀態寄存器(curr_st、next_st),如代碼中第10行至第21行所示;接下來使用三個always語句來實現三段狀態機,第一個always語句實現同步狀態跳轉(如代碼的第27至第33行所示),在復位的時候,當前狀態處在S0狀態,否則將下一個狀態賦值給當前狀態;第二個always采用組合邏輯判斷狀態轉移條件(如代碼的第35行至第47行代碼所示),這里每一個狀態只保持一個時鐘周期,也就是直接跳轉到下一個狀態,在實際應用中,一般根據輸入的條件來判斷是否跳轉到其它狀態或者停留在當前轉態,最后在case語句后面增加一個default語句,來防止狀態機處在異常的狀態;第三個always輸出分頻后的時鐘(如代碼的第49至第59行代碼所示),狀態機的第三段可以使用組合邏輯電路輸出,也可以使用時序邏輯電路輸出,一般推薦使用時序電路輸出,因為狀態機的設計和其它設計一樣,最好使用同步時序方式設計,以提高設計的穩定性,消除毛刺。
從代碼中可以看出,輸出的分頻時鐘clk_divide_7只與當前狀態(curr_st)有關,而與輸入狀態無關,所以屬于摩爾型狀態機。狀態機的第一段對應摩爾狀態機模型的狀態寄存器,用來記憶狀態機當前所處的狀態;狀態機的第二段對應摩爾狀態機模型產生下一狀態的組合邏輯F;狀態機的第三段對應摩爾狀態機產生輸出的組合邏輯G,因為采用時序電路輸出有很大的優勢,所以這里第三段狀態機是由時序電路輸出的。
狀態機采用時序邏輯輸出的狀態機模型如下圖所示:
圖 7.4.10 狀態機時序電路輸出模型
采用這種描述方法雖然代碼結構復雜了一些,但是這樣做的好處是可以有效地濾去組合邏輯輸出的毛刺,同時也可以更好的進行時序計算與約束,另外對于總線形式的輸出信號來說,容易使總線數據對齊,減小總線數據間的偏移,從而降低接收端數據采樣出錯的頻率。
7.4.7模塊化設計
模塊化設計是FPGA設計中一個很重要的技巧,它能夠使一個大型設計的分工協作、仿真測試更加容易,代碼維護或升級更加便利,當更改某個子模塊時,不會影響其它模塊的實現結果。進行模塊化、標準化設計的最終目的就是提高設計的通用性,減少不同項目中同一功能設計和驗證引入的工作量。劃分模塊的基本原則是子模塊功能相對獨立、模塊內部聯系盡量緊密、模塊間的連接盡量簡單。
在進行模塊化設計中,對于復雜的數字系統,我們一般采用自頂向下的設計方式。可以把系統劃分成幾個功能模塊,每個功能模塊再劃分成下一層的子模塊;每個模塊的設計對應一個module,一個module設計成一個Verilog程序文件。因此,對一個系統的頂層模塊,我們采用結構化的設計,即頂層模塊分別調用了各個功能模塊。
下圖是模塊化設計的功能框圖,一般整個設計的頂層模塊只做例化(調用其它模塊),不做邏輯。頂層下面會有模塊A、模塊B、模塊C等,模塊A/B/C又可以分多個子模塊實現。
圖 7.4.11 模塊化設計框圖
在這里我們補充一個概念,就是Verilog語法中的模塊例化。FPGA邏輯設計中通常是一個大的模塊中包含了一個或多個功能子模塊,Verilog通過模塊調用或稱為模塊實例化的方式來實現這些子模塊與高層模塊的連接,有利于簡化每一個模塊的代碼,易于維護和修改。
下面以一個實例(靜態數碼管顯示實驗)來說明模塊和模塊之間的例化方法。
在靜態數碼管顯示實驗中,我們根據功能將FPGA頂層例化了以下兩個模塊:計時模塊(time_count)和數碼管靜態顯示模塊(seg_led_static),如下圖所示:
圖 7.4.12 靜態數碼管顯示模塊框圖
計時模塊部分代碼如下所示:
頂層模塊代碼如下所示:
1 module seg_led_static_top ( 2 input sys_clk , // 系統時鐘 3 input sys_rst_n, // 系統復位信號(低有效) 4 5 output [5:0] sel , // 數碼管位選 6 output [7:0] seg_led // 數碼管段選 7 8 ); 9 10 //parameter define 11 parameter TIME_SHOW = 25'd25000_000; // 數碼管變化的時間間隔0.5s 12 13 //wire define 14 wire add_flag; // 數碼管變化的通知信號 15 16 //***************************************************** 17 //** main code 18 //***************************************************** 19 20 //例化計時模塊 21 time_count #( 22 .MAX_NUM (TIME_SHOW) 23 ) u_time_count( 24 .clk (sys_clk ), 25 .rst_n (sys_rst_n), 26 27 .flag (add_flag ) 28 ); 29 30 //例化數碼管靜態顯示模塊 31 seg_led_static u_seg_led_static ( 32 .clk (sys_clk ), 33 .rst_n (sys_rst_n), 34 35 .add_flag (add_flag ), 36 .sel (sel ), 37 .seg_led (seg_led ) 38 ); 39 40 endmodule我們上面貼出了頂層模塊的完整代碼,子模塊只貼出了模塊的端口和參數定義的代碼。這是因為頂層模塊對子模塊做例化時,只需要知道子模塊的端口信號名,而不用關心子模塊內部具體是如何實現的。如果子模塊內部使用parameter定義了一些參數,Verilog也支持對參數的例化(也叫參數的傳遞),即頂層模塊可以通過例化參數來修改子模塊內定義的參數。
我們先來看一下頂層模塊是如何例化子模塊的,例化方法如下圖所示:
圖 7.4.13 模塊的例化
上圖右側是例化的數碼管靜態顯示模塊,子模塊名是指被例化模塊的模塊名,而例化模塊名相當于標識,當例化多個相同模塊時,可以通過例化名來識別哪一個例化,我們一般命名為“u_”+“子模塊名”。信號列表中“.”之后的信號是數碼管靜態顯示模塊定義的端口信號,括號內的信號則是頂層模塊聲明的信號,這樣就將頂層模塊的信號與子模塊的信號一一對應起來,同時需要注意信號的位寬要保持一致。
接下來再來介紹參數的例化,參數的例化是在模塊例化的基礎上,增加了對參數的信號定義,如下圖所示:
圖 7.4.14 模塊參數的例化
在對參數進行例化時,在模塊名的后面加上“#”,表示后面跟著的是參數列表。計時模塊定義的MAX_NUM和頂層模塊的TIME_SHOW都是等于25000_000,當在頂層模塊定義TIME_SHOW=12500_000時,那么子模塊的MAX_NUM的值實際上也等于12500_000。當然即使子模塊包含參數,在做模塊的例化時也可以不添加對參數的例化,這樣的話,子模塊的參數值等于該模塊內部實際定義的值。
值得一提的是,Verilog語法中的localparam代表的意思同樣是參數定義,用法和parameter基本一致,區別在于parameter定義的參數可以做例化,而localparam定義的參數是指本地參數,上層模塊不可以對localparam定義的參數做例化。
7.5Verilog編程規范
本節主要給大家介紹下編程規范,良好的編程規范是一個FPGA工程師必備的素質。
7.5.1編程規范重要性
當前數字電路設計越來越復雜,一個項目需要的人越來越多,當幾十號設計同事完成同一個項目時候,大家需要互相檢視對方代碼,如果沒有一個統一的編程規范,那么是不可想象的。大家的風格都不一樣,如果不統一的話,后續維護、重用等會有很大的困難,即使是自己寫的代碼,幾個月后再看也會變的很陌生,也會看不懂(您可能不相信,不過筆者和同事交流發現大家都是這樣的,時間長不看就忘記了),所以編程規范的重要性顯而易見。
另外養成良好的編程規范,對于個人的工作習慣、思路等都有非常大的好處。可以讓新人盡快融入項目中,讓大家更容易看懂您的代碼。
7.5.2工程組織形式
工程的組織形式一般包括如下幾個部分,分別是doc、par、rtl和sim四個部分。
doc:一般存放工程相關的文檔,包括該項目用到的datasheet(數據手冊)、設計方案等。不過為了便于大家查看,我們開發板文檔是統一匯總存放在資料盤下的;
par:主要存放工程文件和使用到的一些IP文件;
rtl:主要存放工程的rtl代碼,這是工程的核心,文件名與module名稱應當一致,建議按照模塊的層次分開存放;
sim:主要存放工程的仿真代碼,復雜的工程里面,仿真也是不可或缺的部分,可以極大減少調試的工作量。
7.5.3文件頭聲明
我們建議一個.V只包括一個module,這樣模塊會比較清晰易懂。
7.5.4輸入輸出定義
端口的輸入輸出有Verilog 95和2001兩種格式,推薦大家采用Verilog 2001語法格式。下面是Verilog 2001語法的一個例子,包括module名字、輸入輸出、信號名字、輸出類型、注釋。
我們建議如下幾點:
1)一行只定義一個信號;
2)信號全部對齊;
3)同一組的信號放在一起。
7.5.5parameter定義
我們建議如下幾點:
7.5.6wire/reg定義
一個module中的wire/reg變量聲明需要集中放在一起,不建議隨處亂放。
因此,我們建議如下:
1)將reg與wire的定義放在緊跟著parameter之后;
2)建議具有相同功能的信號集中放在一起;
3)信號需要對齊,reg和位寬需要空2格,位寬和信號名字至少空四格;
4)位寬使用降序描述,[6:0];
5)時鐘使用前綴clk,復位使用后綴rst;
6)不能使用Verilog關鍵字作為信號名字;
7)一行只定義一個信號。
7.5.7信號命名
大家對信號命名可能都有不同的喜好,我們建議如下:
1)信號命名需要體現其意義,比如fifo_wr代表FIFO讀寫使能;
2)可以使用“_”隔開信號,比如sys_clk;
3)內部信號不要使用大寫,也不要使用大小寫混合,建議全部使用小寫;
4)模塊名字使用小寫;
5)低電平有效的信號,使用_n作為信號后綴;
6)異步信號,使用_a作為信號后綴;
7)純延遲打拍信號使用_dly作為后綴。
7.5.8always塊描述方式
always塊的編程規范,我們建議如下:
1)if需要空四格;
2)一個always需要配一個begin和end;
3)always前面需要有注釋;
4)beign建議和always放在同一行;
5)一個always和下一個always空一行即可,不要空多行;
6)時鐘復位觸發描述使用posedge sys_clk和negedge sys_rst_n
7)一個always塊只包含一個時鐘和復位;
8)時序邏輯使用非阻塞賦值。
7.5.10空格和TAB
由于不同的解釋器對于TAB翻譯不一致,所以建議不使用TAB,全部使用空格。
7.5.11注釋
添加注釋可以增加代碼的可讀性,易于維護。我們建議規范如下:
7.5.12模塊例化
模塊例化我們建議規范如下:
7.5.13其他注意事項
其他注意事項如下:
1)代碼寫的越簡單越好,方便他人閱讀和理解;
2)不使用repeat等循環語句;
3)RTL級別代碼里面不使用initial語句,仿真代碼除外;
4)避免產生Latch鎖存器,比如組合邏輯里面的if不帶else分支、case缺少default語句;
5)避免使用太復雜和少見的語法,可能造成語法綜合器優化力度較低。
6)良好的編程規范是大家走向專業FPGA工程師的必備素質,希望大家都能養成良好的編程規范。
總結
以上是生活随笔為你收集整理的【正点原子FPGA连载】第七章Verilog HDL语法 -摘自【正点原子】新起点之FPGA开发指南_V2.1的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: VS2010 旗舰版序列号
- 下一篇: 物联网系统中服务器的功,智慧消防预警物联