跳轉到

4.2 clock 與 reset 型別:抽象掉極性與同步性#

一個在 ASIC 與 FPGA 之間卡住的問題#

考慮一個真實的工程情境。

你寫了一個 UART 接收器的 RTL。這個 IP 要交付給兩位客戶:一位做 ASIC(採用低功耗庫,重置是低電位非同步的,時脈是上升沿),另一位做 FPGA(採用高階庫,重置是高電位同步的,時脈是上升沿)。

在 SystemVerilog 裡,你的原始碼看起來像這樣:

module UartRx (
    input  logic       clk,
    input  logic       rst_n,
    input  logic       rx,
    output logic       rx_valid
);
    logic [15:0] shift;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            shift    <= 16'b0;
            rx_valid <= 1'b0;
        end
        else begin
            shift    <= {shift[14:0], rx};
            rx_valid <= 1'b0;
        end
    end
endmodule

要讓它同時服務兩種客戶,你必須做以下其中一種:

方案一:ifdef 條件編譯。

`ifdef ASIC
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
`elsif FPGA
    always_ff @(posedge clk) begin
        if (rst) begin
`endif
            ...

方案二:參數產生器。

parameter RESET_ACTIVE_LOW = 1;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin

參數方案看起來比較好,但仔細看就會發現問題:事件列表裡的 or negedge rst_n 與條件裡的 !rst_n 必須同時改變。 這兩處寫在不同的地方,卻必須保持一致——正是第 0 章與第 4.1 節說的「靠人肉紀律維持的一致性」。

而且這裡還有一個更深的問題:or negedge rst_n 這個寫法,在硬體上意味著非同步重置,這不只是語法上的開關。非同步重置的觸發器在實體上是不同的標準單元(例如 DFFR / DFFS),它有額外的非同步 reset 輸入腳、reset 網路的 fanout 限制、reset 的 deassertion 需要同步化等一整套物理要求。把它做成一個參數,等於允許工程師在不知不覺中改變實體結構。

本章的主題是:如何讓一份 RTL 在不同製程風格間重用,同時不讓「風格差異」滲入原始碼。 Veryl 的答案是把極性與同步性從語法中抽離,放進型別。


clock 型別族#

Veryl 的時脈型別有三個成員:

型別 意義
clock 極性由建置期選項決定
clock_posedge 明確的上升沿時脈
clock_negedge 明確的下降沿時脈

關鍵設計:極性不是型別的一部分#

這一點是本章的核心,也是最容易被誤解的地方。

在一般的語言設計直覺裡,「上升沿時脈」和「下降沿時脈」應該是兩個不同的型別,就像 i32 和 u32 是兩個不同的型別一樣。SystemVerilog 沒有型別,但 Verilog 的慣例是 clk / clk_n 的命名,工具(如 Spyglass)會靠命名來推斷時鐘域與極性。

Veryl 卻把 clock 的極性移出語言,放進建置期設定。也就是說:

clock 這個型別的成員,在原始碼裡是「一個上升沿時脈」還是「一個下降沿時脈」,取決於建置這個專案時的設定。

為什麼這樣是合理的#

我們回到工程問題本身。時脈極性是一個系統層級的選擇,不是一個模組層級的選擇。

想一想真實的晶片:整顆晶片通常只有一到數個時脈域,每個域的時脈極性是全域一致的(通常是上升沿)。如果一個 IP 的 RTL 裡寫死了 posedge,而這個 IP 要被用在下降沿時脈的系統裡,難道 IP 裡的每一顆 FF 都要改成下降沿嗎?

在硬體上,改變時脈極性不需要改變 IP 內部的邏輯——只需要改變敏感度列表。always_ff @(posedge clk) 與 always_ff @(negedge clk) 描述的是同一顆 FF,只是觸發邊沿不同。

所以把極性寫在 IP 的原始碼裡,本質上是在 IP 內部做了一個全域的決定。這在 IP 重用的時代是錯的。

Veryl 的選擇:IP 的原始碼只宣告「這是一個時脈」,至於上升沿還是下降沿,由整合這顆 IP 的專案在建置時決定。

module UartRx (
    i_clk : clock,        // 不管posedge 還是 negedge,由建置設定
    i_rst : reset,
    i_rx  : input  logic,
    o_rx_valid : output logic,
) {
    var r_shift : logic<16>

    always_ff (i_clk, i_rst) {
        if_reset {
            r_shift = 0;
        } else {
            r_shift = {r_shift[14:0], i_rx};
        }
    }
}

同一份原始碼,在 posedge 專案裡產生:

always_ff @(posedge i_clk or negedge i_rst) begin

在 negedge 專案裡產生:

always_ff @(negedge i_clk or negedge i_rst) begin

IP 的邏輯完全相同,硬體的觸發邊界符合系統需求。

clock_posedge 與 clock_negedge 的存在理由#

如果 clock 的極性可以隨建置改變,那為什麼還需要明確的 clock_posedge / clock_negedge?

因為有些設計確實需要混用兩種極性的時脈:

  • 下降沿時脈的典型用途:把資料在時脈的下降沿送出,好讓接收端在下一個上升沿有半個週期的時間來取樣(這是 DDR 介面的基礎)。
  • 多相時脈:某些低功耗設計會用正反兩相時脈交替工作。
  • 測試與掃描:某些掃描架構用反向時脈來避免與功能時脈競爭。

在這些情況下,「這個時脈的極性是系統層級設定」這個前提不成立,必須在原始碼裡明確寫出。

因此 Veryl 的規則是:預設用 clock;只有在同一個設計裡確實需要明確極性時,才用 clock_posedge / clock_negedge。這兩種明確型別不受建置期設定影響。


reset 型別族#

重置的型別族比時脈複雜,因為重置有兩個正交屬性:

  1. 極性(polarity):重置有效時是高電位還是低電位?
  2. 同步性(synchrony):重置的解除(deassertion)是同步於時脈的,還是非同步的?

Veryl 提供了五個重置型別:

型別 極性 同步性 解除行為
reset 建置期決定 建置期決定 建置期決定
reset_async_high 高電位有效 非同步 非同步
reset_async_low 低電位有效 非同步 非同步
reset_sync_high 高電位有效 同步 同步
reset_sync_low 低電位有效 同步 同步

為什麼需要五個而不是三個#

先看 SystemVerilog 裡「重置」有哪幾種實際形態:

形態 事件列表 條件 硬體
非同步高 or posedge rst if (rst) DFFR 類單元,reset 進 reset 網路
非同步低 or negedge rst_n if (!rst_n) DFFS 類單元
同步高 無 if (rst) 一般 DFF + 邏輯
同步低 無 if (!rst_n) 一般 DFF + 邏輯

注意:同步性影響事件列表,極性影響事件列表與條件。 兩者正交,所以有四種組合。加上「建置期決定」的泛用型別 reset,共五個。

reset 型別的語意#

reset 這個泛用型別的極性與同步性由建置期設定決定,設定的值會影響兩件事:

  1. 生成的敏感度列表:非同步重置會產生 or negedge rst / or posedge rst;同步重置不會。
  2. 生成的 if_reset 條件:有效的重置訊號會被翻譯成正確的極性測試。

這裡有一個非常重要的後果:因為極性不在原始碼裡,所以原始碼裡不能出現 if (!i_rst) 這種寫法——那會把極性硬編碼進去,抵銷整個抽象。

Veryl 的答案是 if_reset 敘述(4.3 節的主題):它把「重置條件」從語法中抽離,讓工程師不需要寫 ! 或 ==。


建置期配置#

極性與同步性在專案的 Veryl.toml 的 [build] 區段設定。

概念上,設定的形貌如下(實際的鍵名請以你的 Veryl 版本的文件為準,語言在演進中):

[build]
# 時脈極性:posedge 或 negedge
# 決定 `clock` 型別的時脈敏感度
clock_edge = "posedge"

# 重置型別:決定 `reset` 型別的極性與同步性
# 對應 reset_async_high / reset_async_low / reset_sync_high / reset_sync_low
reset_type = "reset_async_low"

設定的效果#

設定 影響的 Veryl 型別 生成的 SV
clock_edge = "posedge" clock @(posedge clk)
clock_edge = "negedge" clock @(negedge clk)
reset_type = "reset_async_low" reset @(posedge clk or negedge rst) + if (!rst)
reset_type = "reset_async_high" reset @(posedge clk or posedge rst) + if (rst)
reset_type = "reset_sync_high" reset @(posedge clk) + if (rst)
reset_type = "reset_sync_low" reset @(posedge clk) + if (!rst)

注意:clock_posedge、clock_negedge、reset_async_high 等明確型別不受這些設定影響。它們永遠是那樣。

一個實務上的重要限制#

建置期設定是專案層級的,這意味著:

一個專案裡的所有 clock 型別時脈,共享同一個極性;所有 reset 型別重置,共享同一組極性與同步性。

如果一個設計需要「主時脈用上升沿 + 某個衍生時脈用下降沿」,後者必須宣告成 clock_negedge(明確型別),而不是 clock。

同樣地,如果一個 SoC 裡有「全域非同步重置」與「某個子模組用同步重置」,後者必須宣告成 reset_sync_high 或 reset_sync_low。

這是這個抽象的代價,我們在「設計討論」一節會誠實地評估它。


型別檢查的價值#

現在來看這個抽象帶來的實際工程收益。以下是四個在 SystemVerilog 中無法被編譯器發現、但在 Veryl 中是編譯期錯誤的情況。

情況一:把時脈接到資料埠上#

module Bad (
    i_clk : clock,
    i_d   : input  logic<8>,
) {
    var r_q : logic<8>

    always_ff (i_clk, i_d) {   // 錯誤:i_d 是 logic,不是 clock
        r_q = i_d;
    }
}

編譯期錯誤:i_d 的型別是 logic<8>,與 clock 不符。

在 SystemVerilog 裡這是完全合法的程式碼,工具不會報錯。它會被綜合成「一顆由資料線觸發的正反器」——硬體上這是一顆永遠不會正常工作的 FF,而症狀會表現在隨機的、難以重現的時間點上。

根因:在 SV 中,「時脈」與「資料」的區別不在型別裡,只在命名慣例裡。always_ff @(posedge data_in) 之所以看起來不對,是因為人眼在看名字。Veryl 把這條資訊放進型別,於是機器可以檢查。

情況二:忘記接時脈到 always_ff#

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

    always_ff (i_rst, i_d) {   // 錯誤:i_rst 是 reset 不是 clock
        r_q = i_d;
    }
}

編譯期錯誤。

在 SV 中這會變成 always_ff @(posedge rst_n or negedge data)——用重置訊號當時脈。症狀是「設計偶爾會動」,因為重置訊號的上升沿偶爾會出現。

情況三:把 reset 接到資料埠#

module Bad (
    i_clk : clock,
    i_rst : reset,
    i_d   : input  logic<8>,
) {
    always_ff (i_clk, i_rst) {
        if_reset {
            r_q = 0;
        } else {
            r_q = i_d;
        }
    }
}

這裡 i_rst 沒接錯。真正的錯誤情境是把 reset 型別的訊號當成一般資料使用:

module Bad (
    i_clk : clock,
    i_rst : reset,
) {
    assign o_x = i_rst;   // 錯誤:reset 不能被當資料用
}

編譯期錯誤。

在 SV 中,rst_n 拿去當資料用是完全合法的。它之所以在實務上很少發生,只是因為大家都遵守命名慣例。

情況四:時脈寬度不對#

module Bad (
    i_clk : clock<2>,   // 錯誤:時脈必須是 1 bit
) {
}

編譯期錯誤。多 bit 的「時脈」在硬體上沒有意義。

這是「用型別系統做靜態接線檢查」的經典案例#

讓我們把這件事抽象化。

在硬體設計中,有一批規則是結構性的(structural),不是風格性的(stylistic):

規則 硬體意義
時脈必須是 1 bit 一條時脈線
時脈必須接到觸發器的時脈端 觸發的來源
重置必須接到觸發器的重置端 初始狀態的來源
資料線不能當時脈用 避免假觸發

在 Verilog 與 SystemVerilog 裡,這些規則只能靠命名慣例 + 外部 lint 規則來維持。lint 規則是可選的、依賴工具的、而且有 false negative 的(工具無法檢查跨模組的命名一致性)。

Veryl 的做法是:把結構性規則編進型別系統,讓它們變成無法繞過的語言保證。

這是「make invalid states unrepresentable」這個理念在硬體語言裡的實現:不是「禁止你寫錯」,而是「讓你寫不出來」。


與時鐘域標註的關係#

第 3.1 節介紹過時鐘域標註 'a:

module UartRx (
    i_clk : clock 'a,
    i_rst : reset 'a,
    i_rx  : input  logic 'a,
    o_rx_valid : output logic 'a,
) {
}

時鐘域標註與 clock 型別是同一套機制的兩個面向。

  • clock 型別告訴語言「這根線是時脈」——用於檢查時脈用法的合法性。
  • 'a 標註告訴語言「這根線屬於時鐘域 a」——用於檢查跨時鐘域的合法性。

兩者合起來,讓語言可以回答一個完整的問題:這條訊號從哪個時鐘域來?它被用在同域的邏輯裡嗎?

在 SystemVerilog 裡,時鐘域分析是 CDC 工具(Questa CDC、Verity CDC、Conformal CDC)的工作,它們靠命名與外部設定檔來建立時鐘域模型。在 Veryl 裡,時鐘域是宣告式的一等公民,語言本身知道。

第 6.2 節會完整討論時鐘域標註與跨時鐘域檢查。

但要注意一個重要的限制:時鐘域標註目前是手動宣告的。語言不會自動推斷「這個 FF 在時脈 a 下工作,所以它的輸出屬於域 a」。這是未來可以強化的地方,也是本章抽象的一個「未完成處」——我們在第 10 章的「未解問題」裡會再提到。


生成的 SystemVerilog 對照#

對照一:時脈與重置的還原#

Veryl:

module Counter (
    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;
}

生成的 SystemVerilog(clock_edge = "posedge", reset_type = "reset_async_low"):

module Counter (
    input  logic       i_clk,
    input  logic       i_rst,
    input  logic       i_en,
    output logic [7:0] o_cnt
);
    logic [7:0] r_cnt;

    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

    assign o_cnt = r_cnt;
endmodule

生成的 SystemVerilog(clock_edge = "negedge", reset_type = "reset_sync_high"):

    always_ff @(negedge 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

觀察:同一份 Veryl 原始碼,兩份不同的 SV,兩種不同的硬體。 這正是本章要達成的效果。

對照二:明確型別不受設定影響#

Veryl:

module DdrIface (
    i_clk   : clock_posedge,
    i_clk_n : clock_negedge,
    i_rst   : reset_async_high,
) {
}

生成的 SystemVerilog(無論建置設定為何):

module DdrIface (
    input  logic i_clk,
    input  logic i_clk_n,
    input  logic i_rst
);
    // 敏感度列表使用明確型別帶來的固定極性
endmodule

這裡 i_clk_n 與 i_clk 是兩個不同的型別,因此不能互相連接——如果工程師不小心把 i_clk 接到 i_clk_n 的埠上,會得到型別不符的編譯期錯誤。這正是明確型別存在的價值:它把「這個訊號有特定語意」這件事也變成可檢查的。

對照三:reset 型別在 if_reset 中的還原#

我們在 4.3 節會詳細討論 if_reset。這裡先看它如何還原:

reset 的建置設定 if_reset { ... } 還原成
reset_async_high / reset_sync_high if (rst) begin ... end
reset_async_low / reset_sync_low if (!rst) begin ... end

注意 if_reset 裡面不需要寫任何極性判斷——這是它最重要的性質。


設計討論:把極性從語言移到建置期的取捨#

我們已經把這個設計的收益講得很清楚了。現在來誠實地評估它的代價。

優點#

優點 說明
IP 可重用性 同一份 IP 原始碼可服務不同時脈極性與重置風格的專案
消除 ifdef 與參數產生器 原始碼裡不再有風格相關的分支,diff 乾淨
消除「事件列表與條件極性不一致」的錯誤 兩者由同一個設定推導,不可能不一致
消除「參數改變了實體結構」的隱憂 極性改變在語言層級是「建置選項」,明確地是一個全專案的決定
型別檢查的額外收益 clock / reset 讓時脈與重置的接線規則可被機器檢查

缺點#

缺點 說明
語意不在原始碼裡 讀者看到 i_clk : clock,無法從這一行知道極性。必須去看 Veryl.toml
團隊必須共享建置設定 這是組織層面的要求,不是技術層面的。設定散落在不同工程師的機器上,會造成「在我這裡是 ne 在你那裡是 pos」的困惑
細粒度控制受限 專案層級的設定意味著一個專案內只能有一種 clock 極性與一種 reset 風格。需要混用時必須退回明確型別
工具鏈複雜度 產生 SV 的步驟多了一個「查設定」的前置條件。產生的 SV 不再是原始碼的直接對應
與既有的 SystemVerilog 工具鏈對接 如果下游工具假設 posedge(例如某些 CDC 工具的設定、某些 lint 規則),產生的 SV 需要對應調整

第三種可能:兩種極端之間#

這個設計問題沒有唯一正解,它其實是三個方案的取捨:

方案 語意位置 優點 缺點
A. 全部寫死在原始碼 語言內 自我完整、可預測 無法重用、只能用 ifdef 補救
B. 全部移到建置期(Veryl) 建置期 可重用、無 ifdef 語意不在原始碼、細粒度受限
C. 語言提供兩種機制,由設計者選 兩者皆可 彈性 把判斷交回給人

Veryl 選 B。這是一個明確的立場:

對於「時脈極性」與「重置風格」這類全域性的、系統層級的決定,語言把它們集中在一個地方(建置設定),好讓這個決定一致、可稽核、可溝通,而不是散落在幾萬行的 posedge 與 negedge 裡。

這是第 2.2 節「三大支柱」中「互操作」與「生產力」的交會處:Veryl 產生的 SV 是可讀的,但可讀性建立在「你知道建置設定」的前提上。 對一個成熟、有文件、有 CI 的團隊,這個前提是成立的;對一個鬆散、文件不足的團隊,它可能成為新的困惑來源。

我們的建議:

  1. 把 Veryl.toml 的 [build] 區段版本控制,並在專案 README 中明確寫出。
  2. 在 CI 中產生 SV 之後,加一道檢查:把生成的敏感度列表印出來,讓 reviewer 看得見。這是把「隱藏的語意」重新拉回「可見的語意」的務實做法。
  3. 需要混用極性時,大膽使用明確型別(clock_posedge / clock_negedge / reset_sync_high),把它當作「這裡是例外」的標記,讓例外在程式碼裡可見。

實作練習#

練習一:同一個 Counter 在兩種重置設定下的 SV 輸出#

題目: 寫一個 Counter 模組,然後用兩種不同的 [build] 設定產生 SV,比較兩份輸出的敏感度列表與重置條件。

Veryl 原始碼(一份):

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;
}

設定 A(reset_type = "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

設定 B(reset_type = "reset_sync_high"):

    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

硬體差異:

項目 設定 A(非同步低) 設定 B(同步高)
觸發器型別 有非同步 reset 端(DFFS 類) 純 DFF
reset 網路 有,fanout 大 無(僅邏輯)
重置解除 非同步,需要同步化 同步,天然安全
時序分析 需要額外檢查 reset 的 recovery/removal 不需要
面積 較大 較小

這正是 4.3 節要討論的 CDC 問題的源頭:非同步重置的解除必須被同步化,否則不同晶片在同一個時間點解除重置,會造成 metastability。

練習延伸:把 i_rst 改成 reset_sync_low,觀察生成的條件變成 if (!i_rst) 但敏感度列表沒有 or negedge。這說明「同步性」與「極性」是兩個正交的屬性。

練習二:把型別檢查用起來#

題目: 故意寫三個錯誤的模組,確認編譯器會分別以「型別不符」拒絕它們。

// 錯誤一:資料當時脈
module Err1 (
    i_d : input logic<8>,
) {
    var r_q : logic<8>
    always_ff (i_d) {
        r_q = i_d;
    }
}

// 錯誤二:重置當時脈
module Err2 (
    i_clk : clock,
    i_rst : reset,
) {
    var r_q : logic<8>
    always_ff (i_rst) {
        r_q = 0;
    }
}

// 錯誤三:時脈當資料
module Err3 (
    i_clk : clock,
    o_x   : output logic,
) {
    assign o_x = i_clk;
}

觀察錯誤訊息:它們都應該是型別不符的診斷,而且訊息中會指出期望的型別(clock)與實際的型別(logic / reset)。

思考: 這三個錯誤在 SystemVerilog 中分別是什麼結果?——一個假觸發的 FF、一個由重置驅動的 FF、一個把時脈訊號接到資料線。


本章小結#

  1. clock 型別族:clock(極性由建置期決定)、clock_posedge、clock_negedge(明確,不受設定影響)。時脈極性是系統層級的決定,不該寫死在 IP 裡。
  2. reset 型別族:reset(極性與同步性由建置期決定)、reset_async_high、reset_async_low、reset_sync_high、reset_sync_low。
  3. 建置期配置在 Veryl.toml 的 [build] 區段;它決定 clock / reset 的還原方式,直接影響生成的 SV 與最終的實體結構。
  4. 型別檢查把硬體的結構性規則變成編譯期錯誤:時脈不能接資料埠、重置不能接資料埠、時脈必須 1 bit、資料不能當時脈用。這是「用型別系統做靜態接線檢查」的經典案例。
  5. 'a 時鐘域標註是同一套機制的另一個面向,回答「這條訊號屬於哪個時鐘域」,是第 6 章 CDC 的基礎。
  6. 取捨是誠實的:優點是可重用、消除 ifdef、消除極性不一致;缺點是語意不在原始碼裡、團隊必須共享設定、細粒度控制受限。Veryl 選了「把全域決定集中到一處」的立場。
  7. 生成的 SV 會把 clock / reset 還原成具體的 posedge / negedge / or negedge rst 敏感度列表——語義等價的還原。

下一節(4.3)我們要處理這個抽象的自然後果:既然極性不在原始碼裡,原始碼裡就不能寫 if (!i_rst)。Veryl 為此提供了 if_reset 敘述與 reset_active / reset_inactive 重置值常值——它們是本章抽象得以成立的必要配套。


下一節:第 4.3 節「if_reset、重置值常值與 default clock」