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 |
這張表說明兩件事:
- 同步性只影響敏感度列表(是否加
or negedge/or posedge)。 - 極性只影響
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 型別的極性」。極性這個資訊存在於型別裡:
其中 \(T\) 是 reset 型別, \(\pi\) 是從型別到極性的投影函數:
其中 \(H\) 與 \(L\) 分別是高極性與低極性的重置型別集合:
問題是:這個 \(\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)是非同步的,隨時可能發生。
而這個「隨時可能發生」的解除,如果恰好落在一個時脈邊沿附近,就會造成亞穩態。
所以語言層面的推導鏈是:
flowchart TD
A[建置設定 reset_type = reset_async_low] --> B[生成 always_ff @(posedge clk or negedge rst)]
B --> C[重置解除不對齊時脈邊沿]
C --> D[觸發器進入亞穩態]
D --> E[不同晶片進入不同初始狀態]
E --> F[系統狀態分歧]
A --> G[語言提供 reset_sync_high 型別]
G --> H[生成 always_ff @(posedge clk)]
H --> I[重置解除必須對齊時脈]
I --> J[無亞穩態風險]解法:重置同步器#
重置同步器(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 流程中,重置同步器通常會被實例化成一個專用的標準單元(
SYNCcell),以確保 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;
}
可攜性的來源:
- 原始碼中沒有任何
!或==用在重置訊號上(用if_reset)。 - 原始碼中沒有任何
posedge/negedge(用clock型別)。 - 重置後的值是常數 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」的來源之一。
本章小結#
if_reset把重置條件從語法中抽離,讓同一份程式碼適應正負極性與同步/非同步。它是結構化的分支(不是條件運算子),else是強制的。always_ff使用if_reset時必須提供重置參數;不需要重置就寫always_ff (i_clk)。- 重置值常值
reset_active/reset_inactive依 reset 型別的極性取值,抽象掉「重置有效時訊號是 0 還是 1」。 - 它們的可用位置受限,原因是型別資訊流:極性存在於型別裡,而透過型別別名或泛型參數傳遞時,極性資訊會遺失或變得依賴實例化,這會讓常數的值變得不可預測。
default clock型別修飾符解決多時脈模組中省略時脈的歧義。它是純粹的語言層級標記,不產生任何額外硬體。- 重置的同步性引出 CDC 問題:非同步重置的解除不對齊時脈邊沿會造成亞穩態,因此需要重置同步器(async assert, sync deassert)。
missing_reset_statementlint 檢查提醒「這個狀態的初值可能不確定」,但有 false positive(每拍都覆寫的暫存器)與 false negative(重置值錯誤、重置太晚)。- 生成的 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 與複合型別」