跳轉到

4.3 if_reset、重置值常值與 default clock#

一個抽象必然帶來的後果#

上一節我們把時脈極性與重置極性從語法中抽離,放進了建置期設定。這是一個漂亮的抽象,但它立刻製造了三個必須解決的問題:

問題一:原始碼裡不能寫極性判斷了。

always_ff (i_clk, i_rst) {
    if !i_rst {        // 這行把「低電位有效」寫死進原始碼
        r_q = 0;
    } else {
        r_q = r_d;
    }
}

這行 if !i_rst 直接抵銷了整個抽象。當建置設定改成 reset_sync_high 時,這個判斷就變成錯的——而且是靜默地錯的:原始碼能編譯、能通過大部分測試,只有在重置真的被觸發時才表現出異常。

問題二:重置後的值是什麼?

if_reset {
    r_q = 0;          // 這個 0 是「重置值」
}

重置值本身通常不受極性影響(重置值是 0 還是 1 是設計決策),但有一類情況會受影響:重置值必須與重置訊號的極性配合。例如某些設計用 reset 訊號本身作為資料的重置值(if_reset { r_q = i_rst; }),這時就必須知道極性。

問題三:省略時脈的規則有歧義。

always_ff { ... } 省略時脈參數時,語言要決定用哪一個時脈。當模組只有一個時脈時沒有問題;當模組有多個時脈而只有一個用於所有 always_ff 時,需要一個明確的機制。

本章要介紹的三個機制——if_reset、reset_active / reset_inactive、default clock——分別對應這三個問題。


if_reset 語法#

語法#

always_ff (i_clk, i_rst) {
    if_reset {
        r_q = 0;
    } else {
        r_q = r_d;
    }
}

if_reset 是一個結構化的分支,它的意思是:

「當重處於有效狀態時,執行這個分支。」

注意它不是一個普通的 if。它沒有條件式,條件由語言從 always_ff 的重置參數推出。

為什麼它必須是結構化的分支,而不是一個運算子#

我們本可以這樣設計:

always_ff (i_clk, i_rst) {
    r_q = if_reset_active ? 0 : r_d;
}

但這有兩個問題:

(一)它無法表達「重置分支優先」的語意。 在硬體上,重置是一個優先級最高的強制條件:一旦重置有效,資料輸入就被完全忽略。這用一個 if 的優先級鏈表達得笨拙但可行;用條件運算子表達則暗示「資料輸入有時仍會被使用」。

(二)它無法防止「忘了寫 else」。 在一個時序區塊中,if 沒有 else 分支是合法的(意思是保持原值);但在 if_reset 裡,如果沒有 else,那麼「非重置情況下要做什麼」就必須靠外部賦值表達,那會讓重置分支與一般賦值混在同一個「先讀後寫」的語意下,產生細微的隱憂。

if_reset / else 的結構強制了每個時序邏輯區塊恰好有兩條路徑:重置路徑與正常路徑。這讓閱讀與驗證都變得容易。

if_reset 與 else 的必要性#

always_ff (i_clk, i_rst) {
    if_reset {
        r_q = 0;
    } else {
        r_q = r_d;
    }
}

else 分支是強制的。 這與 5.3 節的 case 表達式要求 default 是同一個設計哲學:表達式與結構必須窮盡所有路徑。

在 SystemVerilog 裡,always_ff 中的 if (!rst_n) q <= 0; 沒有 else 是完全合法的,意思是「重置時設 0,否則保持」。但在 Veryl 裡,若你寫了 if_reset 卻沒有 else,編譯器會報錯。

理由是:「保持」這個行為在硬體上是隱含的,但在語法上是隱藏的。 而在有重置的 FF 裡,「保持」其實意味著重置值被持續寫入——這是一個值得明寫的語意。

約束:always_ff 必須提供重置變數#

always_ff (i_clk) {
    if_reset {     // 錯誤:這個 always_ff 沒有重置參數
        r_q = 0;
    }
}

編譯期錯誤。 if_reset 的語意完全依賴 always_ff 的重置參數,沒有重置參數就沒有「重置狀態」的概念。

正確的寫法是兩種之一:

// (一)本來就不需要重置:寫 always_ff (i_clk)
always_ff (i_clk) {
    r_q = r_d;
}

// (二)需要重置:寫 always_ff (i_clk, i_rst)
always_ff (i_clk, i_rst) {
    if_reset {
        r_q = 0;
    } else {
        r_q = r_d;
    }
}

if_reset 的一致性保證#

if_reset 最大的價值,是讓 4.1 節講過的「事件列表與條件極性必須一致」這條規則從人肉紀律變成語言保證。

讓我們把三種語言並排看。

SystemVerilog:兩處必須手動一致#

always_ff @(posedge clk or negedge rst_n) begin   // 事件列表:negedge
    if (!rst_n) begin                             // 條件:低電位有效
        q <= 0;
    end
    else begin
        q <= d;
    end
end

這裡有兩個地方表達「低電位有效」:negedge rst_n 與 !rst_n。它們必須一致。若不一致:

不一致的寫法 症狀
@(posedge clk or negedge rst_n) + if (rst_n) 重置幾乎不發生,或發生在錯誤的時刻
@(posedge clk) + if (!rst_n) 變成同步重置,但觸發型別可能不同(時序分析錯誤)

第二種尤其陰險:模擬上「看起來有重置」,但硬體上是同步重置,在 reset 的解除邊沿會有 timing violation。

Verilog:完全沒有機制#

Verilog 的 always 連意圖都沒有表達:

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) q <= 0;
    else        q <= d;
end

Veryl:只有一個地方表達極性#

always_ff (i_clk, i_rst) {
    if_reset {
        r_q = 0;
    } else {
        r_q = r_d;
    }
}

極性只出現在 i_rst 的型別宣告裡:

i_rst : reset,   // 極性由建置設定決定

原始碼中沒有任何一處需要「知道」極性。if_reset 會被翻譯成正確的條件,重置參數會被翻譯成正確的敏感度列表。兩者由同一個來源推導,不可能不一致。

生成的 SystemVerilog 對照#

建置設定 生成的敏感度列表 生成的 if_reset
reset_async_low @(posedge i_clk or negedge i_rst) if (!i_rst) begin ... end
reset_async_high @(posedge i_clk or posedge i_rst) if (i_rst) begin ... end
reset_sync_low @(posedge i_clk) if (!i_rst) begin ... end
reset_sync_high @(posedge i_clk) if (i_rst) begin ... end

這張表說明兩件事:

  1. 同步性只影響敏感度列表(是否加 or negedge / or posedge)。
  2. 極性只影響 if_reset 的條件。

兩者正交,正如 4.2 節所述。


重置值常值:reset_active 與 reset_inactive#

兩個常值的語意#

常值 意義 在 reset_async_high / reset_sync_high 中 在 reset_async_low / reset_sync_low 中
reset_active 重處於有效狀態的值 1'b1 1'b0
reset_inactive 重處於無效狀態的值 1'b0 1'b1

對於泛用的 reset 型別,兩者的值由建置期設定決定。

為什麼需要這兩個常值#

考慮一個真實的設計:把重置訊號本身當作資料的重置值。

這不是罕見的做法。在某些介面(例如某些非同步握手的初始化、或「所有狀態歸零」的協定),設計者希望狀態被設成「重置訊號的值」,而不是常數 0 或 1。

always_ff (i_clk, i_rst) {
    if_reset {
        r_q = reset_active;     // 重置時,q 設成「重置有效的值」
    } else {
        r_q = r_d;
    }
}

如果重置是高電位有效,重置有效時 i_rst = 1,所以 r_q 應該被設成 1。 如果重置是低電位有效,重置有效時 i_rst = 0,所以 r_q 應該被設成 0。

用 reset_active 抽象掉這個差別,原始碼就不需要知道極性。

同理,reset_inactive 表達「重置未生效時的值」:

always_comb {
    o_state = i_rst == reset_inactive ? r_state : reset_active;
}

可用位置的限制#

reset_active / reset_inactive 不是通用的 reset 型別值。它們的使用位置受到嚴格限制。可用的位置是:

位置 例子 可用
實例的輸入埠連接 i_rst : reset_active 是
賦值右側 r_q = reset_active; 是
明確型別的 let 初始值 let x : reset = reset_active; 是
== / != 的比較對手 i_rst == reset_active 是
一般算術運算子的操作數 i_rst + 1 否
任意邏輯運算子的操作數 i_rst & x 否

比較特別的是「明確型別的 let 初始值」:

let r_rst : reset = reset_active;    // 可以
let r_rst = reset_active;             // 不行:型別無法推導

第二行之所以不行,是因為 reset_active 本身不是一個值,而是一個「取決於 reset 型別極性」的符號。如果沒有了明確的 reset 型別,就沒有辦法決定它是 0 還是 1。

限制的原因:型別資訊流#

這個限制的深層原因,值得完整說明,因為它觸及一個語言設計的核心議題:型別資訊如何流動。

我們把問題形式化。假設語言要提供一個符號 reset_active,它的值依賴於「某個 reset 型別的極性」。極性這個資訊存在於型別裡:

\[ \text{value}(\texttt{reset\_active}) = \pi(T) \]

其中 \(T\) 是 reset 型別, \(\pi\) 是從型別到極性的投影函數:

\[ \pi(T) = \begin{cases} 1 & T \in H \\ 0 & T \in L \\ \pi_{\text{build}} & T = \texttt{reset} \end{cases} \]

其中 \(H\) 與 \(L\) 分別是高極性與低極性的重置型別集合:

\[ H = \{\texttt{reset\_async\_high},\ \texttt{reset\_sync\_high}\}, \qquad L = \{\texttt{reset\_async\_low},\ \texttt{reset\_sync\_low}\} \]

問題是:這個 \(\pi\) 依賴哪一個 \(T\) ?

我們有三種可能的資訊來源:

來源 說明 可行性
(一)建置期設定 reset_active 永遠等於建置設定決定的那個值 需要語言保證「專案內所有 reset 型別的極性一致」
(二)型別引數 reset_active 的值由使用位置的型別脈絡決定 需要型別檢查器做上下文推導
(三)型別參數傳遞 由「宣告時的 reset 型別」決定 在型別被包裝時會遺失資訊

Veryl 的實作走的是(一)+(三)的組合,並且拒絕(三)中的一個變體。理由如下。

關於(三)的問題:型別資訊在包裝時會遺失。

考慮這樣的寫法:

// 使用者定義型別
type rst_t = reset_async_low;

fn f(x: rst_t) -> logic { ... }

如果 reset_active 的值由 rst_t 決定,那麼 f 裡的 reset_active 會是 0。這看起來可行。

但接著:

fn g(x: reset) -> logic {
    // 這裡的 reset_active 是多少?
}

reset 是泛用型別,其極性來自建置期設定。如果語言允許 reset_active 出現在這裡,那麼:

  • 在 rst_t 的脈絡裡它是 0;
  • 在泛用 reset 的脈絡裡它是 1(假設建置設定是高電位有效)。

同一個常數,在同一個函式裡,因為參數型別不同而有不同的值。 這會讓泛型函式的行為依賴於實例化時的型別——這正是泛型最危險的性質。

因此 Veryl 的規則是:reset_active / reset_inactive 的值,必須能從該位置的型別脈絡唯一決定,而且這個脈絡必須是直接的 reset 型別——也就是「宣告時直接寫 reset_async_low 這種關鍵字」,而不是「透過型別定義或型別參數傳遞而來」。

這就是「明確型別的 let 初始值」被允許、而「透過型別別名」不被允許的原因:

// 允許:直接宣告 reset 型別
let r : reset_async_low = reset_active;     // 值是 0

// 不允許:型別來自別名
type rst_t = reset_async_low;
let r : rst_t = reset_active;                // 型別資訊來自別名,無法判定

// 不允許:型別來自泛型參數
fn f::<T: reset_async_low>(x: T) -> logic {
    let r : T = reset_active;                // 型別來自參數,無法判定
}

這個限制是有代價的(它排除了某些合法的程式設計),但它是保守而安全的選擇:寧可拒絕一些程式,也不讓 reset_active 的值變得不可預測。這與第 0 章「語意的價值來自可預測性」的命題完全一致。

完整對照表#

reset 型別 reset_active reset_inactive 生成的 SV
reset_async_high 1'b1 1'b0 1'b1 / 1'b0
reset_async_low 1'b0 1'b1 1'b0 / 1'b1
reset_sync_high 1'b1 1'b0 1'b1 / 1'b0
reset_sync_low 1'b0 1'b1 1'b0 / 1'b1
reset(泛用) 由建置設定決定 由建置設定決定 具體的 1'b0 / 1'b1

注意:泛用 reset 在產生 SV 時會被折疊成具體的常數。這是因為 SV 裡沒有 reset_active 這種東西——SV 的重置就是普通的 logic,條件必須寫成 if (!rst) 或 if (rst)。Veryl 的抽象在跨越語言邊界時必須被還原成具體的語意,這是「語義等價」目標的必然結果。


default 型別修飾符#

問題#

always_ff 省略時脈參數的規則:

always_ff {
    r_q = r_d;
}

這個省略是合法的,但只在「這個模組的時脈是可以唯一確定的」情況下。具體地說:

  • 模組只有一個 clock 型別的時脈 → 可以省略。
  • 模組有多個 clock 型別的時脈 → 不能省略,因為語言無從選擇。

但這裡有一個中間地帶,非常常見:

模組有兩個時脈,但其中一個只用在某個介面的連接上,而所有 always_ff 都用同一個時脈。

在這種情況下,語言規則說「不能省略」(因為有多個時脈),但工程師的直覺是「省略比較清楚」。於是就需要一個明確的機制。

語法#

let clk: '_ default clock = i_clk;

這是型別修飾符(type modifier)的語法:把原本的 clock 型別修飾成「default clock」。'a 的 _ 是一個特殊的記號,表示「在這個位置做型別修飾」。

它的意思是:

宣告一個型別為 clock、但被標記為「預設時脈」的訊號。所有省略時脈參數的 always_ff 都使用它。

用法#

module DataPath (
    i_clk    : clock,
    i_clk_slow : clock,
    i_rst    : reset,
    i_d      : input  logic<32>,
    o_q      : output logic<32>,
) {
    let clk: '_ default clock = i_clk;

    var r_q : logic<32>

    always_ff {
        r_q = i_d;
    }
}

生成的 SV:

module DataPath (
    input  logic         i_clk,
    input  logic         i_clk_slow,
    input  logic         i_rst,
    input  logic [31:0]  i_d,
    output logic [31:0]  o_q
);
    logic [31:0] r_q;

    always_ff @(posedge i_clk) begin
        r_q <= i_d;
    end
endmodule

為什麼是 let 而不是 var#

let 宣告的是單一賦值的綁定,而且在模組層級它是組合的(見 4.1 節的分類)。這裡的 clk 綁定不是一條新的硬體線,而是「宣告 i_clk 這個既有的時脈是預設時脈」的型別層級標記。

生成的 SV 裡,i_clk 仍然是一個普通的 input logic,沒有產生任何多餘的邏輯。這是「宣告式」的優雅之處:它改變的是語言層面的綁定,不是硬體。

與 always_ff (i_clk) 的比較#

寫法 生成的 SV 差異
always_ff (i_clk) { ... } always_ff @(posedge i_clk) 明確指定
always_ff { ... } + let clk: '_ default clock = i_clk; always_ff @(posedge i_clk) 省略 + 預設指定

兩者生成的硬體完全相同。 差別在於閱讀體驗:

  • 在有預設時脈宣告的模組裡,always_ff { ... } 讀起來像「用預設時脈」,簡潔。
  • 但如果讀者不知道模組有 default clock 宣告,always_ff { ... } 就是資訊不足的。

我們的建議:default clock 的宣告應該放在模組的顯著位置(通常是埠清單之後的第一個宣告),並且在專案的 coding style 中要求「有 default clock 的模組,其 always_ff 允許省略時脈」。

與多時脈模組的關係#

module MultiClock (
    i_clk_a : clock,
    i_clk_b : clock,
    i_rst   : reset,
) {
    let clk: '_ default clock = i_clk_a;

    var r_a : logic
    var r_b : logic

    always_ff {
        r_a = 1;
    }

    always_ff (i_clk_b) {
        r_b = 1;
    }
}

這個模組合法:always_ff { } 用預設時脈 i_clk_a,always_ff (i_clk_b) 明確指定另一個時鐘。

但這裡有一個 CDC 警告:r_a 與 r_b 分屬不同時鐘域,如果 r_b 的邏輯讀取 r_a,就是跨時鐘域操作。第 6 章會詳細討論。


重置與 CDC 的關係#

現在我們要從語言層面出發,走進一個真實的硬體問題。這是本章最重要的「語言 → 硬體」推導鏈。

問題的物理根源#

考慮兩顆相同的晶片,同時上電,同一個電源開關同時打開。它們的重置訊號幾乎同時解除。

但「幾乎同時」不等於「同時」。兩顆晶片的重置訊號走過不同的走線長度、不同的邏輯閘延遲、不同的負載,因此解除時刻會有數十到數百毫秒的偏差(視設計而定)。

關鍵問題:如果重置訊號的解除時刻不對齊任何時脈邊沿,會發生什麼?

答案:在解除的那一瞬間,重置訊號的電壓可能正好處於邏輯閘的不確定區間( \(V_{IL}\) 與 \(V_{IH}\) 之間)。此時觸發器內部兩個分支的傳導延遲不相等,輸出可能暫時停在 \(V_{DD}/2\) 附近的某個電壓——這個狀態稱為 metastability(亞穩態)。

亞穩態會在幾十到幾百個皮秒內自行衰減到 0 或 1,但衰減到哪一邊是隨機的。因此兩顆晶片可能在解除重置後的同一個時脈邊沿,進入不同的狀態。

這為什麼是問題#

如果兩顆晶片原本應該進入完全相同的初始狀態(例如兩片構成一個分布式系統的兩個節點),那麼解除重置後它們的狀態分歧,就會讓整個系統進入一個沒有任何節點預期到的狀態。

這就是為什麼非同步重置的解除必須被同步化。

語言如何引出這個問題#

我們回到語言層面。在 4.2 節的建置設定裡,reset_type 可以設成 reset_async_low。這意味著生成的 SV 是:

always_ff @(posedge i_clk or negedge i_rst) begin
    if (!i_rst) begin
        r_q <= 8'(0);
    end
    ...
end

注意事件列表裡有 or negedge i_rst:重置的解除(從 0 變 1)是非同步的,隨時可能發生。

而這個「隨時可能發生」的解除,如果恰好落在一個時脈邊沿附近,就會造成亞穩態。

所以語言層面的推導鏈是:

解法:重置同步器#

重置同步器(reset synchronizer) 是一個小的模組,它的任務是:把非同步的重置解除,轉成同步於時脈的解除。

module ResetSynchronizer (
    i_clk : clock,
    i_rst : reset,          // 非同步重置輸入
    o_rst : output logic,   // 同步重置輸出
) {
    var r_sync0 : logic
    var r_sync1 : logic

    always_ff (i_clk, i_rst) {
        if_reset {
            r_sync0 = 0;
            r_sync1 = 0;
        } else {
            r_sync0 = 1;
            r_sync1 = r_sync0;
        }
    }

    assign o_rst = r_sync1;
}

原理:重置解除時,兩個 FF 都會在某個時脈邊沿看到非同步的重置值。但第一級 FF 仍可能亞穩態,而第二級 FF 只會看到第一級 FF 在下一個時脈邊沿的值——那時第一級已經衰減到確定值。

因此輸出的重置解除一定對齊某個時脈邊沿,亞穩態被隔離了。

生成的 SV:

module ResetSynchronizer (
    input  logic i_clk,
    input  logic i_rst,
    output logic o_rst
);
    logic r_sync0;
    logic r_sync1;

    always_ff @(posedge i_clk or negedge i_rst) begin
        if (!i_rst) begin
            r_sync0 <= 1'b0;
            r_sync1 <= 1'b0;
        end
        else begin
            r_sync0 <= 1'b1;
            r_sync1 <= r_sync0;
        end
    end

    assign o_rst = r_sync1;
endmodule

使用方式:

module Chip (
    i_clk      : clock,
    i_rst      : reset,
    o_ready    : output logic,
) {
    var r_sync : logic

    inst u_rst_sync : ResetSynchronizer (
        i_clk : i_clk,
        i_rst : i_rst,
        o_rst : r_sync,
    );

    // 內部邏輯全部使用同步重置
    ...
}

這裡有一個型別上的不一致要誠實指出:ResetSynchronizer 的 i_rst 宣告為 reset(泛用),輸出 o_rst 卻是 logic。嚴格來說,輸出應該是 reset_sync_high(或對應的同步型別),但它的極性取決於建置設定,所以語言用 logic 讓呼叫端自己去接。

這是 4.2 節抽象的一個真實漏洞,我們在「設計討論」中會再談。

async assert / sync deassert 模式#

另一個常見模式是「非同步進入、同步離開」:重置的進入是立即的(非同步),離開是同步的。

module AsyncAssertSyncDeassert (
    i_clk : clock,
    i_rst : reset,
    o_rst : output logic,
) {
    var r_ext : logic
    var r_int : logic

    always_ff (i_clk, i_rst) {
        if_reset {
            r_ext = 0;
            r_int = 0;
        } else {
            r_ext = 1;
            r_int = r_ext;
        }
    }

    assign o_rst = r_int;
}

這個模組與 ResetSynchronizer 的差別:輸出的重置是「已經解除」的訊號,用於讓外部邏輯知道「本晶片已經離開重置狀態」。典型用途是作為「晶片就緒(ready)」訊號。

兩者的實體結構其實相似,差別在語意標籤。

不帶重置的設計#

always_ff (i_clk) {
    r_q = r_d;
}

什麼時候不需要重置?

場景 理由
資料路徑上的流水級 流水級的初值不影響功能;每次重啟系統時資料流會被覆寫
掃描鏈的暫存器 掃描模式下由掃描控制器決定值
已經被上游重置過的暫存器 重置是分層的,一次重置會清掉所有相關狀態
記憶體 / RAM 大部分 RAM 的初值由初始化檔案決定

在 Veryl 裡,「不需要重置」是一個明確的宣告:always_ff (i_clk) 沒有重置參數,就是「我不要重置」。

但這裡有一個易被忽略的風險:ASIC 上 FF 的上電初值是不確定的(由製造時的隨機雜訊決定)。如果一段邏輯的正確性依賴某個暫存器初值為 0,那麼「不需要重置」就是一個隱藏的 bug。

Veryl 的 lint 有 missing_reset_statement 檢查來處理這類情況(下一節討論)。


lint 檢查 missing_reset_statement#

檢查規則#

這個檢查的存在理由是:

如果一段時序邏輯的狀態在重置後有明確的值,那它就不該依賴上電初值;如果它沒有被重置,那它的初值就是不確定的。

檢查的邏輯大致是:

  • 找出所有在 always_ff 中被賦值的模組層級 var;
  • 對每一個,確認它是否在每一條路徑上都被重置分支賦值;
  • 若不是,報警告。

誤判的可能#

這個檢查有幾種可能的誤判(false positive):

(一)設計上確實不依賴初值。

var r_valid : logic
always_ff (i_clk) {
    r_valid = 1;      // 每拍都賦值,不依賴初值
}

這裡 r_valid 從來不需要重置,因為它每拍都被覆寫。檢查卻會報警告。

解法:用屬性抑制。

#[allow(missing_reset_statement)]
module Top {
    ...
}

(二)重置在別的模組裡。

一個模組的所有狀態都由上層的「全域重置」清掉,這是分層重置的常見做法。模組本身不需要重置參數,但檢查不知道這件事。

(三)初值由初始化檔案提供。

記憶體的初值由 $readmemh 提供,不需要重置。

誤判的可能(false negative)#

反過來,也有漏報的情況:

(一)重置值不對。 檢查只驗證「有沒有被重置」,不驗證「重置值是否正確」。

(二)重置得太晚。 檢查不驗證重置訊號是否真的會在功能邏輯啟動前解除。

(三)跨模組的重置相依性。 檢查是單一模組層級的。

我們的態度:把這類檢查視為「值得看一眼的提醒」而非「絕對的保證」。它的價值在於低成本地發現「我忘了重置這顆 FF」這個高頻錯誤,而不是取代設計審查。


生成的 SystemVerilog 對照#

對照一:if_reset 與 else#

Veryl:

module Reg (
    i_clk : clock,
    i_rst : reset,
    i_d   : input  logic<8>,
    o_q   : output logic<8>,
) {
    var r_q : logic<8>

    always_ff (i_clk, i_rst) {
        if_reset {
            r_q = 0;
        } else {
            r_q = i_d;
        }
    }

    assign o_q = r_q;
}

生成的 SystemVerilog(reset_async_low):

module Reg (
    input  logic       i_clk,
    input  logic       i_rst,
    input  logic [7:0] i_d,
    output logic [7:0] o_q
);
    logic [7:0] r_q;

    always_ff @(posedge i_clk or negedge i_rst) begin
        if (!i_rst) begin
            r_q <= 8'(0);
        end
        else begin
            r_q <= i_d;
        end
    end

    assign o_q = r_q;
endmodule

生成的 SystemVerilog(reset_sync_high):

    always_ff @(posedge i_clk) begin
        if (i_rst) begin
            r_q <= 8'(0);
        end
        else begin
            r_q <= i_d;
        end
    end

對照二:reset_active#

Veryl:

always_ff (i_clk, i_rst) {
    if_reset {
        r_q = reset_active;
    } else {
        r_q = i_d;
    }
}

生成的 SystemVerilog(reset_async_low):

    always_ff @(posedge i_clk or negedge i_rst) begin
        if (!i_rst) begin
            r_q <= 1'b0;
        end
        else begin
            r_q <= i_d;
        end
    end

注意 reset_active 被折疊成具體的 1'b0。 這是抽象必須被還原的時刻——SV 沒有 reset_active 這個概念。

對照三:default clock#

Veryl:

module DataPath (
    i_clk : clock,
    i_d   : input  logic<32>,
    o_q   : output logic<32>,
) {
    let clk: '_ default clock = i_clk;

    var r_q : logic<32>

    always_ff {
        r_q = i_d;
    }

    assign o_q = r_q;
}

生成的 SystemVerilog:

module DataPath (
    input  logic        i_clk,
    input  logic [31:0] i_d,
    output logic [31:0] o_q
);
    logic [31:0] r_q;

    always_ff @(posedge i_clk) begin
        r_q <= i_d;
    end

    assign o_q = r_q;
endmodule

let clk: '_ default clock = i_clk; 這一行在 SV 中完全消失。 它是純粹的語言層級標記。


實作練習#

練習一:重置同步器#

題目: 實作 ResetSynchronizer 模組,輸入為非同步重置,輸出為同步重置。

解答:

module ResetSynchronizer (
    i_clk : clock,
    i_rst : reset,
    o_rst : output logic,
) {
    var r_sync0 : logic
    var r_sync1 : logic

    always_ff (i_clk, i_rst) {
        if_reset {
            r_sync0 = 0;
            r_sync1 = 0;
        } else {
            r_sync0 = 1;
            r_sync1 = r_sync0;
        }
    }

    assign o_rst = r_sync1;
}

驗證方法(依第 8 章的測試框架):

#[test]
async fn reset_sync_test() {
    // 1. 施加重置
    // 2. 在時脈邊沿之間解除重置
    // 3. 觀察 o_rst 只在時脈邊沿後才變成 1
}

關鍵的驗證點:o_rst 的變化時刻必須對齊時脈邊沿。在波形圖上,重置解除的時機應該是隨機的(模擬器不會讓你手動控制解除的時機),但 o_rst 的上升沿總是落在時脈的上升沿之後一個週期。

延伸思考:

  • 兩級 FF 是標準做法(第一級吸收亞穩態,第二級提供乾淨的輸出)。
  • 如果輸出還要驅動非同步邏輯,兩級不夠——需要三級(因為第二級本身也可能亞穩態)。
  • 在 ASIC 流程中,重置同步器通常會被實例化成一個專用的標準單元(SYNC cell),以確保 reset net 的 fanout 被適當處理。

練習二:三種重置風格的同一個設計#

題目: 寫一個帶致能訊號的計數器,並分別以 reset、reset_sync_high、reset_async_low 三種型別宣告 i_rst,比較生成的 SV。

解答:

module Counter #(
    parameter WIDTH : u32 = 8,
) (
    i_clk : clock,
    i_rst : reset,
    i_en  : input  logic,
    o_cnt : output logic<WIDTH>,
) {
    var r_cnt : logic<WIDTH>

    always_ff (i_clk, i_rst) {
        if_reset {
            r_cnt = 0;
        } else if i_en {
            r_cnt = r_cnt + 1;
        }
    }

    assign o_cnt = r_cnt;
}

把 i_rst : reset 改成 i_rst : reset_sync_high,觀察生成的 SV:

    always_ff @(posedge i_clk) begin
        if (i_rst) begin
            r_cnt <= 8'(0);
        end
        else if (i_en) begin
            r_cnt <= r_cnt + 8'(1);
        end
    end

改成 reset_async_low:

    always_ff @(posedge i_clk or negedge i_rst) begin
        if (!i_rst) begin
            r_cnt <= 8'(0);
        end
        else if (i_en) begin
            r_cnt <= r_cnt + 8'(1);
        end
    end

思考: 注意 reset_sync_high 與 reset_async_high 的差別在敏感度列表。這個差別在硬體上是真實的:前者用一般 DFF,後者用有非同步 reset 端的 DFF。在 ASIC 流程中,後者需要 reset net 的特殊處理(高 fanout 的 reset net 通常需要 buffer tree 甚至是時鐘樹式的平衡)。

練習三:可攜性測試#

題目: 寫一個模組,其行為不依賴重置的極性與同步性,並說明它是如何做到這點的。

解答:

module PortableCounter (
    i_clk : clock,
    i_rst : reset,
    i_en  : input  logic,
    o_cnt : output logic<8>,
) {
    var r_cnt : logic<8>

    always_ff (i_clk, i_rst) {
        if_reset {
            r_cnt = 0;
        } else if i_en {
            r_cnt = r_cnt + 1;
        }
    }

    assign o_cnt = r_cnt;
}

可攜性的來源:

  1. 原始碼中沒有任何 ! 或 == 用在重置訊號上(用 if_reset)。
  2. 原始碼中沒有任何 posedge / negedge(用 clock 型別)。
  3. 重置後的值是常數 0,與極性無關。

因此同一份原始碼,在四種重置設定下產生的 SV 都具有相同的外部行為。 這就是本章全部機制的目的。


設計討論:三個機制的取捨#

讓我們把本章的三個機制放在一張表上,看清楚它們各自解決什麼、犧牲什麼。

機制 解決的問題 犧牲
if_reset 極性判斷不能寫在原始碼裡 不能寫「只在重置時做 X,其他時保持」的簡潔形式(必須寫完整的 else)
reset_active / reset_inactive 重置值可能與極性相關 使用位置受限;型別別名與泛型參數無法使用
default clock 多時脈模組中省略時脈的歧義 需要額外的宣告;省略時脈降低可讀性

if_reset 的一個真實限制#

if_reset 必須有 else,這在某些設計裡不夠靈活。考慮:

always_ff (i_clk, i_rst) {
    if_reset {
        r_count = 0;
    } else {
        // 這裡只更新部分旗標,其他旗標保持
        if i_done {
            r_done = 1;
        }
    }
}

注意 r_done 在 else 分支中可能不被賦值,這在 always_ff 中是合法的(未賦值 = 保持)。這是 always_ff 語意的自然結果,也與 SV 的 always_ff 一致。

但這裡有一個陷阱:在 always_ff 中,「未賦值 = 保持」意味著這顆 FF 沒有重置值(在重置分支中它沒被賦值)。如果 r_done 在重置時應該是 0,必須在 if_reset 分支中明確賦值:

always_ff (i_clk, i_rst) {
    if_reset {
        r_count = 0;
        r_done  = 0;      // 必須明確賦值
    } else {
        if i_done {
            r_done = 1;
        }
    }
}

這正是 missing_reset_statement lint 檢查存在的理由。

default clock 的一個真實風險#

省略時脈參數的 always_ff { ... } 有一個危險:如果未來有人在模組裡加了第二個時脈,已有的 always_ff { } 會不會突然綁到錯的時脈?

在 Veryl 中,答案是不會:一旦宣告了 let clk: '_ default clock = i_clk;,always_ff { } 就永遠綁到 i_clk,與新增的時脈無關。而且若模組沒有 default clock 宣告卻有多個時脈,編譯會直接報錯。

這是「宣告式設計」的一次漂亮展現:語言規則讓「新增一個時脈」這個修改不會靜默地改變既有邏輯的硬體結構。在 SystemVerilog 裡,這類修改正是競態與「神秘 bug」的來源之一。


本章小結#

  1. if_reset 把重置條件從語法中抽離,讓同一份程式碼適應正負極性與同步/非同步。它是結構化的分支(不是條件運算子),else 是強制的。
  2. always_ff 使用 if_reset 時必須提供重置參數;不需要重置就寫 always_ff (i_clk)。
  3. 重置值常值 reset_active / reset_inactive 依 reset 型別的極性取值,抽象掉「重置有效時訊號是 0 還是 1」。
  4. 它們的可用位置受限,原因是型別資訊流:極性存在於型別裡,而透過型別別名或泛型參數傳遞時,極性資訊會遺失或變得依賴實例化,這會讓常數的值變得不可預測。
  5. default clock 型別修飾符解決多時脈模組中省略時脈的歧義。它是純粹的語言層級標記,不產生任何額外硬體。
  6. 重置的同步性引出 CDC 問題:非同步重置的解除不對齊時脈邊沿會造成亞穩態,因此需要重置同步器(async assert, sync deassert)。
  7. missing_reset_statement lint 檢查提醒「這個狀態的初值可能不確定」,但有 false positive(每拍都覆寫的暫存器)與 false negative(重置值錯誤、重置太晚)。
  8. 生成的 SV 把抽象還原成具體語意:if_reset → if (!rst) / if (rst),reset_active → 1'b0 / 1'b1,default clock 宣告則完全消失。

下一章(5.1)我們轉向另一個同樣充滿混亂的領域:資料型別系統。我們會看到 SystemVerilog 為什麼需要十幾種內建整數型別、名字與寬度為什麼不直觀,以及 Veryl 如何用 u32 / p32 這類「把寬度與性質編進名字」的型別,把硬體設計中最常見的一類錯誤(位寬與常數的錯誤)搬進型別系統。


下一節:第 5.1 節「資料型別系統:logic、bit、u32、p32 與複合型別」