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 型別族#
重置的型別族比時脈複雜,因為重置有兩個正交屬性:
- 極性(polarity):重置有效時是高電位還是低電位?
- 同步性(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 這個泛用型別的極性與同步性由建置期設定決定,設定的值會影響兩件事:
- 生成的敏感度列表:非同步重置會產生
or negedge rst/or posedge rst;同步重置不會。 - 生成的
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 的團隊,這個前提是成立的;對一個鬆散、文件不足的團隊,它可能成為新的困惑來源。
我們的建議:
- 把
Veryl.toml的[build]區段版本控制,並在專案 README 中明確寫出。 - 在 CI 中產生 SV 之後,加一道檢查:把生成的敏感度列表印出來,讓 reviewer 看得見。這是把「隱藏的語意」重新拉回「可見的語意」的務實做法。
- 需要混用極性時,大膽使用明確型別(
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、一個把時脈訊號接到資料線。
本章小結#
clock型別族:clock(極性由建置期決定)、clock_posedge、clock_negedge(明確,不受設定影響)。時脈極性是系統層級的決定,不該寫死在 IP 裡。reset型別族:reset(極性與同步性由建置期決定)、reset_async_high、reset_async_low、reset_sync_high、reset_sync_low。- 建置期配置在
Veryl.toml的[build]區段;它決定clock/reset的還原方式,直接影響生成的 SV 與最終的實體結構。 - 型別檢查把硬體的結構性規則變成編譯期錯誤:時脈不能接資料埠、重置不能接資料埠、時脈必須 1 bit、資料不能當時脈用。這是「用型別系統做靜態接線檢查」的經典案例。
'a時鐘域標註是同一套機制的另一個面向,回答「這條訊號屬於哪個時鐘域」,是第 6 章 CDC 的基礎。- 取捨是誠實的:優點是可重用、消除
ifdef、消除極性不一致;缺點是語意不在原始碼裡、團隊必須共享設定、細粒度控制受限。Veryl 選了「把全域決定集中到一處」的立場。 - 生成的 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」