Verilog always 敘述完整教學:基礎語法、用法差異與 SystemVerilog 擴充

目次

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

這樣寫的話,當 abc 中任一個訊號發生變化時,always 區塊就會被執行,並重新計算輸出 y

使用 @(*) 的好處

  • 會自動將所有輸入訊號包含在敏感度列表中
  • 避免因敏感度列表遺漏而導致邏輯模擬與合成結果不一致

always @(posedge clk) 的用法(時序邏輯電路)

在時序邏輯電路中,狀態會依照時脈訊號同步改變。這時候,敏感度列表需要指定 posedge clk

always @(posedge clk) begin
  q <= d;
end

此例中,當時脈的上升沿(posedge)到來時,d 的值會被鎖存到 q。此處使用 <= 非阻塞賦值,這是在時序邏輯中最常用的方式。

posedgenegedge

  • 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。也就是說,變數的賦值順序會直接影響邏輯結果,因此必須小心。

主要用途

  • 組合邏輯電路中的控制結構(ifcase
  • 不需要保留狀態的處理

非阻塞賦值(<=)是什麼?

非阻塞賦值表示所有敘述同時被評估,並在時脈邊緣後一起更新,這能表現硬體的並行特性。

always @(posedge clk) begin
  a <= b;
  c <= a;
end

在這裡,a <= bc <= 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 生成

條件分支過於複雜

如果 ifcase 條件過於複雜,且未涵蓋所有可能輸入,可能導致未定義的行為或邏輯漏洞

常見錯誤:缺少 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部分條件未進行賦值使用 elsedefault 確保完整覆蓋
未定義行為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 clknegedge 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_ffalways_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. 當條件分支(ifcase)中沒有涵蓋所有輸入情況時,合成工具會推斷需要保存先前值,因而生成 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_combalways_ffalways_latch 語法與其優點
  • 實務開發中常見問題的 FAQ 整理與解答

程式描述的精確度決定設計品質

硬體描述語言有一個特點:「電路會依程式描述直接生成」。因此,任何小錯誤都可能直接導致電路不良。特別是 always 區塊,因為負責描述主要邏輯行為,必須特別注意:

  • 語法是否精確
  • 賦值方式是否正確
  • 條件分支是否完整涵蓋

下一步:邁向更高階的設計

當能正確使用 always 後,接下來可以挑戰以下進階主題:

  • 有限狀態機(FSM:Finite State Machine) 的設計
  • 管線化(Pipeline)與串流處理 的實作
  • IP Core 的開發 以及FPGA 實現

同時,若能進一步學習 SystemVerilog 或 VHDL 等語言,就能在更多專案與設計環境中發揮專業能力。

最後的提醒:設計者的心態

硬體設計並非「能動就好」,而是要正確運作、並能適應未來擴充與修改
希望透過本文,讀者不僅能掌握 always 敘述的語法,也能培養穩健、安全的設計思維