1. 前言
Verilog 中 always
敘述的角色是什麼?
在數位電路設計中被廣泛使用的硬體描述語言「Verilog HDL」中,always
敘述扮演著非常重要的角色。Verilog 並不是像軟體一樣描述演算法,而是以「在什麼條件下,訊號如何變化」的方式來表現電路。在這之中,always
敘述是用來描述特定條件發生時要執行的動作的基本語法。
為什麼需要 always
敘述?
在 Verilog 中,主要有兩種方式來描述電路的行為:
- 組合邏輯電路:輸入改變時,輸出立即跟著改變的電路
- 時序邏輯電路:依照時脈訊號等時間條件改變輸出的電路
僅靠 assign
敘述無法描述複雜的條件分支或狀態記憶。在這種情況下,就需要 always
敘述。
例如,要描述具有多重條件分支的邏輯,或是透過觸發器(flip-flop)進行的記憶功能,就必須在 always
中使用控制結構(例如 if
敘述或 case
敘述)。
常見的 always
使用模式
always
敘述有幾種常見用法,會依照欲設計的電路種類來區分:
always @(*)
→ 用於組合邏輯電路always @(posedge clk)
→ 用於時脈上升沿觸發的時序電路always @(posedge clk or negedge rst)
→ 用於帶有非同步重置信號的時序電路等較複雜的控制結構
因此,理解Verilog 的核心語法 always
,可以說是成為硬體設計工程師的第一步。
本文的目的
本篇文章將針對 Verilog 中的 always
敘述,從基礎語法、進階用法、常見陷阱到 SystemVerilog 的擴充功能,進行全面解說。
- 想了解
always
的正確寫法 - 無法判斷邏輯合成錯誤的原因
- 分不清楚
=
與<=
的使用時機 - 想避免初學者常犯的錯誤
這篇文章將成為解答這些疑問與困惑的實用指南,幫助讀者快速掌握 always
的設計技巧。
2. always
敘述的基本語法與種類
always
敘述的基本語法
Verilog 的 always
敘述是一種根據特定條件(敏感度列表)反覆執行處理的描述方式。基本語法如下:
always @(敏感度列表)
begin
// 要執行的處理
end
在這個語法中,最重要的部分是「敏感度列表」。它用來定義當哪些訊號發生變化時,該區塊需要被執行。
always @(*)
的用法(組合邏輯電路)
在組合邏輯電路中,輸入每次變化時,輸出都需要立即更新。在這種情況下,敏感度列表會使用 @(*)
。
always @(*) begin
if (a == 1'b1)
y = b;
else
y = c;
end
這樣寫的話,當 a
、b
、c
中任一個訊號發生變化時,always
區塊就會被執行,並重新計算輸出 y
。
使用 @(*)
的好處
- 會自動將所有輸入訊號包含在敏感度列表中
- 避免因敏感度列表遺漏而導致邏輯模擬與合成結果不一致
always @(posedge clk)
的用法(時序邏輯電路)
在時序邏輯電路中,狀態會依照時脈訊號同步改變。這時候,敏感度列表需要指定 posedge clk
。
always @(posedge clk) begin
q <= d;
end
此例中,當時脈的上升沿(posedge)到來時,d
的值會被鎖存到 q
。此處使用 <=
非阻塞賦值,這是在時序邏輯中最常用的方式。
posedge
與 negedge
posedge
:在上升沿觸發negedge
:在下降沿觸發
需要根據設計需求選擇正確的邊緣觸發方式。
always @(posedge clk or negedge rst)
(含非同步重置)
在複雜電路設計中,經常需要具備重置功能。以下是帶有非同步重置的寫法:
always @(posedge clk or negedge rst) begin
if (!rst)
q <= 1'b0;
else
q <= d;
end
這樣的描述方式,當重置信號為「0」時,q
會立即被清零;其餘情況下則會隨時脈同步鎖存 d
。
組合邏輯電路與時序邏輯電路的區別
電路類型 | 使用的 always | 特徵 |
---|---|---|
組合邏輯電路 | always @(*) | 輸入改變時輸出立即更新 |
時序邏輯電路 | always @(posedge clk) | 依時脈同步觸發動作 |
3. always
敘述中的賦值種類
Verilog 中有兩種賦值方式
在 Verilog 的 always
區塊內,可以使用兩種不同的賦值運算子:
=
:阻塞賦值(blocking assignment)<=
:非阻塞賦值(non-blocking assignment)
如果在不了解差異的情況下隨意使用,容易造成非預期的行為或模擬結果與合成結果不一致,因此這是非常重要的知識點。
阻塞賦值(=
)是什麼?
阻塞賦值表示一個敘述執行完後,才會執行下一個敘述,有點像軟體中的順序流程控制。
always @(*) begin
a = b;
c = a;
end
在這個例子中,a = b
會先執行,然後 c = a
使用的是更新後的 a
。也就是說,變數的賦值順序會直接影響邏輯結果,因此必須小心。
主要用途
- 組合邏輯電路中的控制結構(
if
、case
) - 不需要保留狀態的處理
非阻塞賦值(<=
)是什麼?
非阻塞賦值表示所有敘述同時被評估,並在時脈邊緣後一起更新,這能表現硬體的並行特性。
always @(posedge clk) begin
a <= b;
c <= a;
end
在這裡,a <= b
與 c <= a
是同時評估,並在時脈觸發後一併更新。因此,c
會接收到前一個時脈週期的 a
值。
主要用途
- 時序邏輯電路(暫存器、觸發器)
- 需要準確保存與傳遞多個狀態時
阻塞與非阻塞的差異比較
特徵 | 阻塞賦值 (= ) | 非阻塞賦值 (<= ) |
---|---|---|
執行順序 | 由上而下依序執行 | 同時評估,統一更新 |
主要使用場合 | 組合邏輯電路 | 時序邏輯電路 |
反映時機 | 立即更新 | 時脈邊緣後更新 |
常見錯誤 | 意外產生鎖存器(Latch) | 數值未更新或傳遞錯誤 |
混合使用會怎樣?
原則上,=
與 <=
不應在同一個區塊或同一個訊號上混合使用。例如:
always @(posedge clk) begin
a = b;
a <= c;
end
在這個例子中,a
被賦值兩次,而且使用了不同的方式,會導致最終數值不確定,模擬與實際電路行為可能不同。
使用準則
- 組合邏輯電路 → 使用
=
(阻塞賦值,寫在always @(*)
區塊內) - 時序邏輯電路(時脈同步) → 使用
<=
(非阻塞賦值,寫在always @(posedge clk)
區塊內)
只要遵守這個規則,就能避免大部分常見的錯誤。
4. 使用 always
敘述時的注意事項與常見錯誤
敏感度列表(sensitivity list)的描述錯誤
若未正確指定應感知的訊號,將成為錯誤的來源
在 Verilog 中,always
的敏感度列表(@(...)
)必須明確寫出哪些訊號變化時要觸發。以下是僅列出部分訊號的錯誤範例:
always @(a) begin
if (b)
y = 1'b1;
else
y = 1'b0;
end
在這段程式中,b
的變化不會觸發 always
區塊,因此即使 b
改變,輸出 y
也不會更新,造成錯誤。
解決方案:使用 @(*)
為了避免遺漏或忘記敏感度列表中的訊號,建議使用 @(*)
:
always @(*) begin
if (b)
y = 1'b1;
else
y = 1'b0;
end
@(*)
會自動將程式中所有被參考的訊號納入敏感度列表,提升程式的可維護性與安全性。
意外產生鎖存器(Latch)
if 或 case 未涵蓋所有情況,會導致 Latch
若在條件分支中並未對所有情況賦值,合成工具會認為「需要保存先前的數值」,進而生成鎖存器。例如:
always @(*) begin
if (enable)
y = d; // 當 enable=0 時,y 未被更新
end
這段程式看似正常,但當 enable=0
時,y
不會更新,導致自動插入 Latch 保存舊值。
解決方案:確保所有情況皆有賦值
always @(*) begin
if (enable)
y = d;
else
y = 1'b0; // 所有情況都有賦值
end
這樣就能避免不必要的 Latch 生成。
條件分支過於複雜
如果 if
或 case
條件過於複雜,且未涵蓋所有可能輸入,可能導致未定義的行為或邏輯漏洞。
常見錯誤:缺少 case 的 default
always @(*) begin
case(sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
// 2'b11 的情況未定義
endcase
end
因為沒有處理 2'b11
,所以可能產生未定義輸出。
解決方案:加上 default
always @(*) begin
case(sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = 1'b0; // 安全保護
endcase
end
透過 default
節,無論輸入為何,輸出都會有定義,提升設計可靠性。
同時控制多個訊號時的注意事項
當一個 always
區塊同時控制多個訊號時,賦值順序或遺漏可能會導致非預期的依賴關係。在複雜電路中,建議將控制拆分為多個 always
區塊,以提高可讀性與安全性。
常見陷阱整理
問題 | 原因 | 解決方案 |
---|---|---|
輸出未更新 | 敏感度列表缺少必要訊號 | 使用 @(*) 自動感知 |
意外產生 Latch | 部分條件未進行賦值 | 使用 else 或 default 確保完整覆蓋 |
未定義行為 | case 未涵蓋所有條件 | 務必加入 default |
控制過於複雜 | 同一區塊同時操作多個訊號 | 拆分多個 always 區塊 |
5. SystemVerilog 中 always
敘述的擴充
always_comb
:組合邏輯專用
概要
always_comb
與傳統的 always @(*)
幾乎相同,但它明確表示該區塊是組合邏輯。
always_comb begin
y = a & b;
end
主要優點
- 自動生成敏感度列表
- 當設計中不小心產生 Latch 時,工具會發出警告
- 避免與先前定義的同名變數互相干擾
使用範例(與 Verilog 比較)
// Verilog
always @(*) begin
y = a | b;
end
// SystemVerilog
always_comb begin
y = a | b;
end
always_ff
:時序邏輯專用(觸發器)
概要
always_ff
用來描述時脈驅動的時序邏輯,必須指定觸發條件,例如 posedge clk
或 negedge rst
。
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
q <= 1'b0;
else
q <= d;
end
主要優點
- 僅允許使用
<=
(非阻塞賦值),若使用=
會報錯 - 工具會檢查敏感度列表是否正確
- 一眼就能辨識該區塊為「時序邏輯」,可讀性與維護性更高
always_latch
:鎖存器專用
概要
always_latch
用於有意識地描述鎖存器(level-sensitive latch)。不過,為了避免誤用,建議僅在必要時使用。
always_latch begin
if (enable)
q = d;
end
注意事項
- 若條件分支允許跳過賦值,會明確生成 Latch
- 除非設計需求必須,否則應盡量避免使用
SystemVerilog 語法的使用區分
語法 | 用途 | 對應 Verilog | 特徵 |
---|---|---|---|
always_comb | 組合邏輯 | always @(*) | 自動生成敏感度列表,可檢測 Latch |
always_ff | 觸發器(時序邏輯) | always @(posedge clk) | 時脈同步,避免錯誤賦值 |
always_latch | 鎖存器 | always @(*) (條件分支不完整) | 僅用於有意圖的 Latch,工具可檢測誤用 |
未來設計趨勢:SystemVerilog 為主流
近年來,開發現場越來越多推薦使用 SystemVerilog 語法,因為其可讀性與安全性更佳。EDA 工具也不斷進化,透過 always_ff
與 always_comb
可以降低「看似正確卻無法運作」的錯誤。
特別是在團隊合作與大型專案中,語法能讓設計意圖更加清晰,提升程式審查與後續維護的效率。
6. FAQ:關於 always
敘述的常見問題與解答
本章將針對 Verilog 與 SystemVerilog 中 always
敘述的使用,解答設計實務上常遇到的疑問與搜尋熱門問題,幫助初學者到中階工程師快速釐清觀念。
Q1. 在 always
區塊中,該使用 if
還是 case
?
A. 基本原則是依條件數量與邏輯複雜度來選擇:
- 條件只有 2~3 個 → 使用
if
較簡潔 - 多種明確的狀態切換 → 使用
case
更清晰且可讀性高
此外,case
通常要求涵蓋所有情況,能降低設計遺漏的風險。
Q2. 如果省略敏感度列表會發生什麼?
A. 若敏感度列表未完整指定,部分訊號變化不會觸發 always
區塊,導致輸出無法更新。
這會造成模擬結果與實際電路行為不一致。最佳解法是使用 @(*)
或 SystemVerilog 的 always_comb
。
Q3. 為什麼 always
會產生非預期的 Latch?
A. 當條件分支(if
、case
)中沒有涵蓋所有輸入情況時,合成工具會推斷需要保存先前值,因而生成 Latch。
錯誤範例:
always @(*) begin
if (en)
y = d; // 當 en==0 時,y 保持不變
end
正確寫法:
always @(*) begin
if (en)
y = d;
else
y = 1'b0; // 確保所有情況皆有賦值
end
Q4. 可以在同一個區塊混用 =
與 <=
嗎?
A. 原則上不建議。常見規則如下:
- 組合邏輯 → 使用
=
(阻塞賦值) - 時序邏輯 → 使用
<=
(非阻塞賦值)
若同一個訊號同時被 =
與 <=
賦值,可能模擬正常但實際電路錯誤。
原則:
同一訊號應保持一致的賦值方式。
Q5. always_ff
與傳統 always @(posedge clk)
有什麼差別?
A. 功能上相近,但 always_ff
在安全性與可讀性上更佳。
比較項目 | always @(posedge clk) | always_ff |
---|---|---|
敏感度列表 | 需自行指定 | 工具會自動檢查 |
賦值方式 | 允許 = 或 <= (可能出錯) | 僅允許 <= ,錯誤立即檢出 |
可讀性 | 無法明確區分用途 | 一眼就能辨識為「時序邏輯」 |
Q6. 在 always
中同時控制多個訊號可以嗎?
A. 可以,但越多訊號會降低可維護性。建議在以下情況下拆分為多個 always
區塊:
- 不同輸出彼此獨立
- 同步控制與非同步控制混雜
Q7. 如果在組合邏輯中使用 <=
,會怎樣?
A. 雖然模擬可能正常,但在邏輯合成時可能產生預期外的電路。因此,組合邏輯應使用 =
(阻塞賦值)。
7. 總結
always
敘述是 Verilog 設計的基礎與核心
在 Verilog 的硬體設計中,always
敘述是一個能同時描述組合邏輯與時序邏輯的強大工具。它不僅能擴展設計的表達能力,還能清楚描述控制流程與時序,因此對從初學者到專業工程師而言,都是必須掌握的核心知識。
本文重點解說了以下內容:
本文重點回顧
always @(*)
與always @(posedge clk)
的差異與使用場景=
(阻塞賦值)與<=
(非阻塞賦值)的差異與正確用法- 敏感度列表的寫法、避免產生 Latch 等常見錯誤的解決方法
- SystemVerilog 新增的
always_comb
、always_ff
、always_latch
語法與其優點 - 實務開發中常見問題的 FAQ 整理與解答
程式描述的精確度決定設計品質
硬體描述語言有一個特點:「電路會依程式描述直接生成」。因此,任何小錯誤都可能直接導致電路不良。特別是 always
區塊,因為負責描述主要邏輯行為,必須特別注意:
- 語法是否精確
- 賦值方式是否正確
- 條件分支是否完整涵蓋
下一步:邁向更高階的設計
當能正確使用 always
後,接下來可以挑戰以下進階主題:
- 有限狀態機(FSM:Finite State Machine) 的設計
- 管線化(Pipeline)與串流處理 的實作
- IP Core 的開發 以及FPGA 實現
同時,若能進一步學習 SystemVerilog 或 VHDL 等語言,就能在更多專案與設計環境中發揮專業能力。
最後的提醒:設計者的心態
硬體設計並非「能動就好」,而是要正確運作、並能適應未來擴充與修改。
希望透過本文,讀者不僅能掌握 always
敘述的語法,也能培養穩健、安全的設計思維。