Verilog文法
Verilog簡介
Verilog是一種硬體描述語言,以文本形式來描述數字系統硬體的結構和行為的語言,用它可以表示邏輯電路圖、邏輯表達式,還可以表示數字邏輯系統所完成的邏輯功能。
Verilog 和 C 的差別:
- Verilog是硬體描述語言,編譯下載下傳到在編譯下載下傳到FPGA之後,會生成電路,是以Verilog全部是并行處理與運作的
- C語言是軟體語言,編譯下載下傳到單片機/CPU之後,還是軟體指令,而不會根據代碼生成相應的硬體電路,而單片機/CPU處理軟體指令需要取址、譯碼、執行,是串行執行的。
Verilog邏輯值
邏輯電路中有四種值,即四種狀态
- 邏輯0:表示低電平,對應電路的GND
- 邏輯1:表示高電平,對應電路的VCC
- 邏輯X:表示未知,有可能是高電平,也有可能是低電平
- 邏輯Z:表示高阻态,外部沒有激勵信号是一個懸空狀态
Verilog辨別符
用于定義子產品名、端口名和信号名等
Verilog的辨別符可以是任意一組字母、數字、$和_(下劃線) 符号的組合,但辨別符的第一個字元必須是字母或者下劃線。區分大小寫
不建議大小寫混合使用,普通内部信号建議全部小寫,參數定義建議大寫
建議:
- 用有意義的有效的名字如sum、cpu_addr等
- 用下劃線區分詞語組合,如cpu_addr
-
采用一些字首或字尾
比如:時鐘采用clk字首:clk_50m,clk_cpu;低電平采用_n字尾:enable_n
- 統一縮寫,如全局複位信号rst
- 同一信号在不同層次保持一緻性,如同一時鐘信号必須在各子產品保持一緻。
- 自定義的辨別符不能與保留字(關鍵詞)同名
- 參數統一采用大寫,如定義參數使用SIZE
Verilog數字進制
Verilog數字進制格式包括二進制、八進制、十進制和十六進制,一般常用的為二進制、十進制和十六進制。
- 二進制表示如下:4’b0101表示4位二進制數字0101
- 十進制表示如下:4’d2表示4位十進制數字2
- 十六進制表示如下:4’ha表示4位十六進制數字a
當沒有指定數字的位寬與進制時,預設為32位的十進制,比如100,實際上表示的值為32’d100。
資料類型
主要有三大類資料類型,即寄存器類型、線網類型和參數類型。真正在數字電路中起作用的資料類型是寄存器類型和線網類型。
寄存器類型
寄存器類型表示一個抽象的資料存儲單元,它隻能在always語句和initial語句中被指派,并且它的值從一個指派到另一個指派過程中被儲存下來。
如果語句描述的是時序邏輯,即always語句帶有時鐘信号,則該寄存器變量對應為寄存器;如果該過程語句描述的是組合邏輯,即always語句不帶有時鐘信号則該寄存器變量對應為硬體連線
寄存器類型的預設值是x(未知狀态)。
寄存器資料類型有很多種,如reg、integer、real等,其中最常用的就是reg類型
reg [31:0] delay_cnt; // 延時計數器
reg key_flag; // 按鍵标志
線網類型
線網表示Verilog結構化元件間的實體連線。
值由驅動元件的值決定,例如連續指派或門的輸出。
如果沒有驅動元件連接配接到線網,線網的預設值為z(高阻态)。
線網類型,如tri和wire等,其中最常用的就是wire類型,它的使用方法如下:
wire data_en; // 資料使能信号
wire [7:0] data; // 資料
參數類型
參數其實就是一個常量,常被用于定義狀态機的狀态、資料位寬和延遲大小等
可以在編譯時修改參數的值,是以又常被用于一些參數可調的子產品中,使使用者在執行個體化子產品時,可以根據需要配置參數。
在定義參數時,可以一次定義多個參數,參數與參數之間需要用逗号隔開。
要注意的是參數的定義是局部的,隻在目前子產品中有效。
parameter DATA_WIDTH = 8; // 資料位寬為8
Verilog運算符
類型:
- 算術運算符
+ - * / %
Verilog實作乘除比較浪費組合邏輯資源,尤其是除法。一般2的指數次幂的乘除法使用移位運算來完成運算.
非2的指數次幂的乘除法一般是調用現成的IP,QUARTUS/ISE等工具軟體會有提供,不過這些工具軟體提供的IP也是由最底層的組合邏輯(與或非門等)搭建而成的。
- 關系運算符
> < >= <= == !=
用來進行條件判斷,在進行關系運算符時,如果聲明的關系是假的,則傳回值是0,如果聲明的關系是真的,則傳回值是1;
所有的關系運算符有着相同的優先級别,關系運算符的優先級别低于算術運算符的優先級别
- 邏輯運算符
! && ||
連接配接多個關系表達式,可實作更加複雜的判斷,一般不單獨使用,都需要配合具體語句來實作完整的意思
- 條件運算符
? :
從兩個輸入中選擇一個作為輸出的條件選擇結構,功能等同于always中的if-else語句
- 位運算符
~ & | ^
直接對應數字邏輯中的與、或、非門等邏輯門
位運算符一般用在信号指派上
- 移位運算符
<< >>
移位運算符包括左移位運算符和右移位運算符,這兩種移位運算符都用0來填補移出的空位。 一般使用左移位運算代替乘法,右移位運算代替除法,但是隻能表示2的指數次幂的乘除法。
- 拼接運算符
{a,b}
可以把兩個或多個信号的某些位拼接起來進行運算操作
注釋
/* */
//
建議使用 //
關鍵字
常用關鍵字
- module 子產品開始定義
- input 輸入端口定義
- output 輸出端口定義
- inout 雙向端口定義
- parameter 信号的參數定義
- wire wire信号定義
- reg reg信号定義
- always 産生reg信号語句的關鍵字
- assign 産生wire信号語句的關鍵字
- begin 語句的開始标志
- end 語句的結束标志
- poseedge / negedge 時序電路的标志
- case case語句的起始标志
- default case語句的預設分支标記
- if else
- for
- endmodule 子產品結束定義
區分大小寫
程式架構
LED流水燈程式
module led(
input sys_clk, // 系統時鐘,50M
input sys_rst_n, // 系統複位,低電平有效
output reg[3:0] led // 4位led燈
);
// parameter define
parameter WIDTH = 25;
parameter COUNT_MAX = 25_000_000; // 分頻
// reg define
reg [WIDTH-1:0] counter;
reg [1:0] led_ctrl_cnt;
wire counter_en;
// 計數到最大值時産生高電平使能信号 --> 不做分頻而是産生一個使能信号
assign counter_en = (counter == (COUNT_MAX - 1'b1)) ? 1'b1:1'b0;
// 産生0.5秒使能信号的計數器
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
counter <= 1'b0;
else if(counter_en)
counter <= 1'b0;
else
counter <= counter + 1'b1;
end
// led流水控制計數器
always @(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
led_ctrl_cnt <= 2'b0;
else if(counter_en)
led_ctrl_cnt <= led_ctrl_cnt + 2'b1;
end
// 通過控制IO口的高低電平實作發光二極管的亮滅
always @ (posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
led <= 4'b0;
else begin
case(led_ctrl_cnt)
2'd0: led <= 4'b0001;
2'd1: led <= 4'b0010;
2'd2: led <= 4'b0100;
2'd3: led <= 4'b1000;
default: ;
endcase
end
end
endmodule
阻塞指派(Blocking)
在一個always塊中,後面的語句會受到前語句的影響,具體來說,在同一個always中,一條阻塞指派語句如果沒有執行結束,那麼該語句後面的語句就不能被執行,即被“阻塞”。
也就是說always塊内的語句是一種順序關系
符号“=”用于阻塞的指派(如:b = a;),阻塞指派“=”在begin和end之間的語句是順序執行,屬于串行語句。 其後面的指派語句從概念上來講是在前面一條語句指派完成後才執行的。
非阻塞指派( Non-Blocking )
符号“<=”用于非阻塞指派(如:b <= a;),非阻塞指派是由時鐘節拍決定,在時鐘上升到來時,執行指派語句右邊,然後将begin-end之間的所有指派語句同時指派到指派語句的左邊,
begin—end之間的所有語句,一起執行,且一個時鐘隻執行一次,屬于并行執行語句。
非阻塞指派的操作過程可以看作兩個步驟:
- 指派開始的時候,計算RHS( 等号右邊的表達式 );
- 指派結束的時候,更新LHS( 等号左邊的表達式 )。
非阻塞的概念是指,在計算非阻塞指派的RHS以及LHS期間,允許其它的非阻塞指派語句同時計算RHS和更新LHS。
阻塞與非阻塞指派
在描述組合邏輯電路的時候,使用阻塞指派,比如assign指派語句和不帶時鐘的always指派語句,這種電路結構隻與輸入電平的變化有關系
assign data = (data_en == 1'b1)?8'd255:8'd0;
always@(*)begin
if(en) begin
a = a0;
b = b0;
end
else begin
a = a1;
b = b1;
end
end
在描述時序邏輯的時候,使用非阻塞指派,綜合成時序邏輯的電路結構,比如帶時鐘的always語句;這種電路結構往往與觸發沿有關系,隻有在觸發沿時才可能發生指派的變化
always @(poseedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
a<= 1'b0;
b<= 1'b0;
end
else begin
a <= c;
b <= d;
end
end
assign 和 always的差別
assign 語句使用時不能帶時鐘。
always 語句可以帶時鐘,也可以不帶時鐘。
在always不帶時鐘是,邏輯功能和assign 完全一緻,都是隻産生組合邏輯。
比較簡單的組合邏輯推薦使用assign語句,比較複雜的組合邏輯推薦使用always語句。
帶時鐘 和 不帶時鐘的always:
always 語句可以帶時鐘,也可以不帶時鐘。
- 在always不帶時鐘時,邏輯功能和assign完全一緻,雖然産生的信号定義為reg類型,但是該語句産生的還是組合邏輯
- 在always帶時鐘信号時,這個邏輯語句才能産生真正的寄存器
latch
latch指鎖存器,是一種對脈沖電平敏感的存儲單元電路
鎖存器和寄存器都是基本存儲單元,鎖存器是電平觸發的存儲器,寄存器是邊沿觸發的存儲器。
兩者的基本功能是一樣的,都可以存儲資料。
鎖存器是組合邏輯産生的,而寄存器是在時序電路中使用,由時鐘觸發産生的。
latch的主要危害是會産生毛刺(glitch),這種毛刺對下一級電路是很危險的。并且其隐蔽性很強,不易查出。
在設計中,應盡量避免latch的使用。
代碼裡出現latch的兩個原因是在組合邏輯中, if或者case語句不完整的描述,比如if缺少else分支,case缺少default分支,導緻代碼在綜合過程中出現了latch。解決辦法就是if必須帶else分支,case必須帶default分支。
隻有不帶時鐘的always語句if或者case語句不完整才會産生latch,帶時鐘的語句if或者case語句不完整描述不會産生latch。
狀态機
Verilog是硬體描述語言,硬體電路是并行執行的,當需要按照流程或者步驟來完成某個功能時,代碼中通常會使用很多個if嵌套語句來實作,這樣就增加了代碼的複雜度,以及降低了代碼的可讀性,這個時候就可以使用狀态機來編寫代碼。
狀态機相當于一個控制器,它将一項功能的完成分解為若幹步,每一步對應于二進制的一個狀态,通過預先設計的順序在各狀态之間進行轉換,狀态轉換的過程就是實作邏輯功能的過程。
狀态機,全稱是有限狀态機(Finite State Machine,縮寫為FSM),是一種在有限個狀态之間按一定規律轉換的時序電路,可以認為是組合邏輯和時序邏輯的一種組合。狀态機通過控制各個狀态的跳轉來控制流程,使得整個代碼看上去更加清晰易懂,在控制複雜流程的時候,狀态機優勢明顯,是以基本上都會用到狀态機,如SDRAM控制器等。
根據狀态機的輸出是否與輸入條件相關,可将狀态機分為兩大類,即摩爾(Moore)型狀态機和米勒(Mealy)型狀态機
- Mealy狀态機:組合邏輯的輸出不僅取決于目前狀态,還取決于輸入狀态
- Moore狀态機:組合邏輯的輸出隻取決于目前狀态。
三段式狀态機
根據狀态機的實際寫法,狀态機還可以分為一段式、二段式和三段式狀态機。
- 一段式:整個狀态機寫到一個always子產品裡面,在該子產品中既描述狀态轉移,又描述狀态的輸入和輸出。
不推薦,一般都會要求把組合邏輯和時序邏輯分開,組合邏輯和時序邏輯混合在一起不利于代碼維護和修改
- 二段式:用兩個always子產品來描述狀态機,其中一個always子產品采用同步時序描述狀态轉移;另一個子產品采用組合邏輯判斷狀态轉移條件,描述狀态轉移規律以及輸出,需要定義兩個狀态,現态和次态,然後通過現态和次态的轉換來實作時序邏輯
-
三段式:在兩個always子產品描述方法基礎上,使用三個always子產品,一個always子產品采用同步時序描述狀态轉移,一個always采用組合邏輯判斷狀态轉移條件,描述狀态轉移規律,另一個always子產品描述狀态輸出(可以用組合電路輸出,也可以時序電路輸出)。
三段式狀态機的基本格式是:第一個always語句實作同步狀态跳轉;第二個always語句采用組合邏輯判斷狀态轉移條件;第三個always語句描述狀态輸出(可以用組合電路輸出,也可以時序電路輸出)。
demo
7分頻
狀态跳轉
module divider7_fsm (
//系統時鐘與複位
input sys_clk ,
input sys_rst_n ,
//輸出時鐘
output reg clk_divide_7
);
// 狀态參數
parameter S0 = 7'b0000001; //獨熱碼定義方式
parameter S1 = 7'b0000010;
parameter S2 = 7'b0000100;
parameter S3 = 7'b0001000;
parameter S4 = 7'b0010000;
parameter S5 = 7'b0100000;
parameter S6 = 7'b1000000;
// 定義兩個7位的寄存器,一個用來表示目前狀态,一個用來表示下一狀态
reg [6:0] curr_st ; //目前狀态
reg [6:0] next_st ; //下一個狀态
// 三個always語句
// 第一個always采用同步時序描述狀态轉移
// 第二個always采用組合邏輯判斷狀态轉移條件
// 第三個always描述狀态輸出
//***************************
//** main code
//***************************
// 描述狀态轉移
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
curr_st <= S0;
else
curr_st <= next_st;
end
// 判斷狀态轉移條件
always @(*)begin
case(curr_st)
S0: next_st = S1;
S1: next_st = S2;
S2: next_st = S3;
S3: next_st = S4;
S4: next_st = S5;
S5: next_st = S6;
S6: next_st = S0;
default: next_st = S0; // 防止latch
endcase
end
// 狀态輸出
always @(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
clk_divide_7 <= 1'b0;
else if((curr_st == S0) | (curr_st == S1) | (curr_st == S2) | (curr_st == S3))
clk_divide_7 <= 1'b1;
else if ((curr_st == S4) | (curr_st == S5) | (curr_st == S6))
clk_divide_7 <= 1'b0;
else ;
end
endmodule
狀态機的第三段可以使用組合邏輯輸出,也可以使用時序邏輯輸出一般推薦使用時序電路輸出,使用同步時序方式設計,可以提高設計的穩定性,消除毛刺。
子產品化設計
劃分子產品的基本原則是子子產品功能相對獨立、子產品内部聯系盡量緊密、子產品間的連接配接盡量簡單。
在進行子產品化設計中,對于複雜的數字系統,我們一般采用自頂向下的設計方式。可以把系統劃分成幾個功能子產品,每個功能子產品再劃分成下一層的子子產品;每個子產品的設計對應一個module,一個module設計成一個Verilog程式檔案。是以,對一個系統的頂層子產品,我們采用結構化的設計,即頂層子產品分别調用了各個功能子產品。
FPGA邏輯設計中通常是一個大的子產品中包含了一個或多個功能子子產品,Verilog通過子產品調用或稱為子產品執行個體化的方式來實作這些子子產品與高層子產品的連接配接,有利于簡化每一個子產品的代碼,易于維護和修改。
如果子子產品内部使用parameter定義了一些參數,Verilog也支援對參數的例化(也叫參數的傳遞),即頂層子產品可以通過例化參數來修改子子產品内定義的參數。
子子產品名是指被例化子產品的子產品名,而例化子產品名相當于辨別,當例化多個相同子產品時,可以通過例化名來識别哪一個例化,一般命名為“u_”+“子子產品名”
參數的例化,參數的例化是在子產品例化的基礎上,增加了對參數的信号定義
// 例子
time_count #(
.MAX_NUM (TIME_SHOW) // 參數例化
)u_time_count(
.clk (sys_clk),
.rst_n (sys_rst_n), // 信号例化
.flag (add_flag)
);
localparam代表的意思同樣是參數定義,用法和parameter基本一緻,差別在于parameter定義的參數可以做例化,而localparam定義的參數是指本地參數,上層子產品不可以對localparam定義的參數做例化。
Verilog程式設計規範
工程的組織形式一般包括如下幾個部分,分别是doc、par、rtl和sim四個部分
- doc:一般存放工程相關的文檔,包括該項目用到的datasheet(資料手冊)、設計方案等。
- par:主要存放工程檔案和使用到的一些IP檔案
- rtl:主要存放工程的rtl代碼,是工程的核心,檔案名與module名稱應當一緻,建議按照子產品的層次分開存放
- sim:主要存放工程的仿真代碼,複雜的工程裡面,仿真也是不可或缺的部分,可以極大減少調試的工作量。
檔案頭聲明
每一個Verilog檔案的開頭,都必須有一段聲明的文字。
包含檔案的版權、作者、建立日期,以及内容簡介等等
//*************************************Copyright(c)*******************//
// FileName:
// Last modified Date:
// Last Version:
// Descriptions:
//*******************************************************************//
輸入輸出定義
module led(
input sys_clk, // 系統時鐘
input sys_rst_n , // 系統複位
output reg [3:0] led // 4位LED燈
);
- 一行隻定義一個信号
- 信号全部對齊
- 同一組的信号放在一起
parameter定義
- module中的parameter聲明,不建議随處亂放,将parameter定義放在緊跟着module的輸入輸出定義之後
- parameter等常量命名全部使用大寫
wire/reg定義
- 将reg與wire的定義放在緊跟着parameter之後
- 建議具有相同功能的信号集中放在一起
- 信号需要對齊,reg和位寬需要空2格,位寬和信号名字至少空四格
- 位寬使用降序描述,[6:0]
- 時鐘使用字首clk,複位使用字尾rst
- 一行隻定義一個信号
信号命名
- 内部信号不要使用大寫,也不要使用大小寫混合,建議全部使用小寫
- 子產品名字使用小寫
- 異步信号,使用_a作為信号字尾
always塊
- 一個always需要配一個begin和end
- always前面需要有注釋
- 一個always和下一個always空一行即可,不要空多行
- 時序邏輯使用非阻塞指派
assign塊
- assign的邏輯不能太複雜,否則易讀性不好
- assign前面需要有注釋
- 組合邏輯使用阻塞指派
空格 和 TAB