1.1 從Verilog到SystemVerilog:一門語言的三十年演進(1984-2005)#
一個語言的出身決定了它的性格#
要理解 Verilog,必須先理解它不是為了什麼而設計的。
1984 年,Gateway Design Automation 發表了 Verilog。它的原始身分不是「硬體設計語言」,而是類比電路模擬器 Verilog-XL 的描述語言層。Gateway 當時的核心產品是模擬器;Verilog 是讓工程師用文字描述「這個電路長什麼樣子」,好讓模擬器去跑。
Philip Skinner 等主要作者在後續的 Verilog 教材中把這段歷史寫得很清楚:Verilog 的第一版語言規格只有十幾頁,設計目標是「讓模擬器有一個好用的輸入格式」。它不是語言學家或語言設計師的產物,而是模擬器架構的自然延伸。
這個出身留下了極深的烙印,直到今天我們還能從 Verilog(以及它的後代 SystemVerilog、Veryl)看出:
- 語法像 C——因為 C 是當時寫模擬器介面程式最自然的語言。工程師能快速上手。
- 核心抽象是「事件」——模擬器的工作是「在某個事件的影響下,某些訊號的值改變了」。語言的每一條語法規則都是為了服務這個模型。
- 優化目標是模擬速度,不是設計正確性——Verilog 從來沒有把「這段程式碼會被合成成什麼」當成一級公民。
- 語意的模糊處一律交給使用者——Verilog 不阻止你寫出模擬器會接受、但硬體語意不成立的程式碼。
第三點尤其重要。很多今天看來荒謬的 Verilog 特性(隱含網路、x 傳播、競態容忍),在「這是一個模擬器輸入格式」的框架下都是合理的工程妥協:模擬器要能快速跑,設計者要能速寫,於是把判斷責任全部下放給使用者。
Verilog 的輝煌與它的罪惡,是同一個設計決定的兩面。
一次收購與一次事實標準#
1990 年,Cadence 收購 Gateway Design Automation。Cadence 沒有改變 Verilog 的語言設計,只是改變了它的分發方式。
接下來發生了一件在語言史上極不尋常的事:Verilog 在被 IEEE 收編成正式標準(IEEE 1364-1995)之前,就已經完全支配了產業。所有主要 EDA 工具都支援它,所有 IP 供應商都用它交付,所有 textbooks 都教它。而 IEEE 1364 這個正式標準,是在語言被用了整整十一年之後才出現的。
因此有句話流傳於業界:「Verilog 是唯一一個不需要標準就能統治硬體設計的語言。」
這句話背後的現實是三層的:
- 教學層:Verilog 是 FPGA 教學、數位電路課與新進工程師訓練的預設語言,教材、範例、解答程式碼在網路上形成了壓倒性的規模效應。
- IP 層:IP 供應商只交付 Verilog,因為客戶只會寫 Verilog。這形成了一個自我強化的循環。
- 工具層:EDA 工具的投資優先順序完全倒向 Verilog,Verilog 的模擬與合成支援永遠是最好、最快的那一條路。
這三層使得 Verilog 的語言缺陷「即使被知道也無法定案」——因為任何改動語言的嘗試都會面對「那我的 IP 怎麼辦」「那我的教材怎麼辦」的巨大遷移成本。
Verilog 的語意核心#
接下來我們把 Verilog 最核心的語意講透。這一節不是語法手冊,而是「這些規則為什麼會是這個樣子,以及它們如何繼續活在今天的工具鏈裡」。
四態邏輯與 x 的傳播#
Verilog 的值域不是二值的,而是四值的:
0:邏輯零。1:邏輯一。x:未知(unknown)。表示這個位元的值無法確定——可能是未初始化、可能是競態結果、可能是$readmemb讀到未指定的記憶體內容。z:高阻抗(high impedance)。用來表示三態匯流排上「沒有驅動者」。
z 的存在是硬體的必然:三態匯流排是 1980 年代匯流排架構的主流做法,z 讓語言能表達「這個匯流排目前沒有任何人驅動」。
x 的存在則是模擬器的需求:要偵測「這個值還沒被賦值」,最簡單的方式就是引入一個第三值。但代價是所有運算子都必須定義在四值域上。
Verilog 定義了兩套運算子:
- 邏輯運算(
&&、||、!)採用三值邏輯:把x視為「未知真值」。0 && x的結果是0(只要有一個明確為假,整體就是假);1 && x的結果是x;x && x的結果是x。 - 位元運算(
&、|、^、移位等)則是逐位元的四值運算,每個位元各自按表計算。
我們以 XOR 為例。純二值的 XOR 表是熟悉的:
| \(a\) | \(b\) | \(a \oplus b\) |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Verilog 的 ^ 是逐位元運算,其四值擴充表為:
| \(a\) | \(b\) | \(a \oplus b\) | \(a \leftrightarrow b\) |
|---|---|---|---|
| 0 | 0 | 0 | 1 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 0 | 1 |
| 0 | x | x | x |
| x | 0 | x | x |
| 1 | x | x | x |
| x | 1 | x | x |
| x | x | x | x |
其規則可以用一句話概括:只要任何一個運算元是 x,輸出就是 x。注意這是「安全」的傳播——x 不會憑空變成 0 或 1。這是 Verilog 語意設計中少數明確且一致的原則。
但四態邏輯真正的破壞力不在運算子,而在比較。
== 與 ===:Verilog 最著名的地雷#
Verilog 提供兩種相等性運算子,語意截然不同:
| 運算子 | 名稱 | 遇 x/z 的行為 |
|---|---|---|
== | 邏輯相等(logical equality) | 若任一運算元含 x/z,結果為 x |
!= | 邏輯不相等 | 同上,結果為 x |
=== | 案例相等(case equality) | 精確比對四值,x === x 為真,x === 0 為假 |
!== | 案例不相等 | 同上 |
於是:
if (a == b) begin
// 當 a 或 b 含 x 時,條件結果為 x
// 在 if 中,x 被視為 false,此區塊不會執行
end
if (a === b) begin
// 精確比對:x 只會等於 x
end
這個差異在實務上導致了兩類經典 bug:
- Reset 檢查失效:設計者想確認「所有狀態都已初始化」,寫了
if (state == 0),但未初始化的狀態其實是x,所以state == 0結果是x,if把它當成假,檢查形同虛設。正確寫法是=== 0(或改用 SystemVerilog 的casez/ 工具的初始化檢查)。 - Case 陳述式的隱含
x行為:case陳述式在 Verilog 中的匹配是精確匹配(類似===),所以未匹配的x會落到default。這也是為什麼 Verilog 的case對x的處理語意清楚,而casez(把z當 don't care)與casex(把x也當 don't care)在 SystemVerilog 中被譏為「過於危險」。
x 傳播還有一個惡名昭彰的應用場合——X-propagation 檢查:在 gate-level 模擬中把未定義值以 x 傳播,用來找出「這個訊號其實沒有被驅動」的問題。今天的這類工具仍然是 ASIC sign-off 的必要環節。
事件式模擬與求值順序#
Verilog 模擬器的核心是一個事件佇列(event queue)。當某個訊號的值改變,就產生一個事件,排入佇列;事件被取出後,觸發所有「敏感於此訊號」的行程(process)。這個模型的好處是:沒有變化的地方不花時間。一個千萬閘級的設計,如果只有幾千個節點在動,模擬器可以只處理那些節點。這是 Verilog 在效能上長期勝出的原因。
壞處是:「事件什麼時候排入佇列」這個順序,不是語言規範的一部分,而是模擬器實作的選擇。
Verilog 標準規定的是「阻塞賦值立即生效、非阻塞賦值延遲到目前時間槽末端」,但它沒有規定多個 always 區塊之間的執行先後。於是:
always @(posedge clk) q = d; // 區塊 A
always @(posedge clk) q = ~d; // 區塊 B
哪個最後執行,取決於模擬器把哪個 always 排在前面。商業模擬器(VCS、Questa、Xcelium)在實務上確實有一致的行為(通常依宣告順序),但這是「事實標準」而非「語言規範」,並且在 IEEE 1364 中明確地說明此行為是未定義的。
這就是競態(race)的根源,也是 Verilog 語意中最大的不確定性來源。
阻塞賦值與非阻擋賦值#
Verilog 有兩種賦值,它們的語意差異是整個硬體語言世界裡最重要的一條規則。
阻塞賦值(blocking assignment, =):讀取右側、立即更新左側,並且在同一個程序內立即生效。
always @(posedge clk) begin
a = b; // 立刻執行
c = a; // 讀到的是剛才寫進去的 b
end
語意是先寫後讀:賦值完成後,左側變數的值立刻是新值,後續的讀取看到新值。
非阻擋賦值(non-blocking assignment, <=):讀取右側、把結果排入更新佇列,在目前時間槽的最末端統一更新左側。
always @(posedge clk) begin
a <= b; // 排入佇列
c <= a; // 讀到的是「舊的 a」,不是即將寫入的新值
end
語意是先讀後寫:右側在賦值敘述執行的當下被求值,左側的更新延後到時間槽末端。
兩者在功能上等價的情況下,會合成出完全不同的硬體。這是 Verilog(以及 SystemVerilog)最令人意外、也最重要的一條語意規則。
一個最小的競態範例#
module RaceDemo (
input wire clk,
input wire d,
output reg q_b,
output reg q_nb
);
reg dbl;
// 阻塞版本
always @(posedge clk) begin
dbl = d;
q_b = dbl;
end
// 非阻塞版本
always @(posedge clk) begin
dbl <= d;
q_nb <= dbl;
end
endmodule
我們來看兩個時序槽的結果。假設 clk 在邊緣上升,d 在上升緣之前已經穩定為 0,而在上升緣之後切換為 1。
時槽 \(n\) (上升緣發生):
| 敘述 | 阻塞版本 | 非阻塞版本 |
|---|---|---|
讀 d | 得 0 | 得 0 |
寫 dbl | dbl 立即變成 0 | dbl 的更新排入佇列 |
讀 dbl | 得 0(新值) | 得舊值(上一槽的 1) |
寫 q | q_b 變成 0 | q_nb 的新值 0 排入佇列 |
時槽 \(n+1\) (下一個上升緣),此時 d 已為 1:
| 敘述 | 阻塞版本 | 非阻塞版本 |
|---|---|---|
讀 d | 得 1 | 得 1 |
寫 dbl | 立即變成 1 | 排入佇列 |
讀 dbl | 得 1(新值) | 得舊值 0 |
寫 q | q_b 變成 1 | q_nb 的新值 1 排入佇列 |
時間槽末統一更新後,兩者的 q 都是 1,看不出差異。差異出現在連續兩個時鐘邊沿之間的那一瞬間:非阻塞版本中,q_nb 更新後的「新資料」被 dbl 讀到的是舊值,因此 q_nb 在時槽 \(n\) 反映的是 \(n-1\) 的 d;而阻塞版本中,dbl 立刻生效。於是對一個「用 dbl 做兩級暫存」的路徑而言,兩者的有效延遲深度不同。
對時序電路來說這是關鍵:非阻塞賦值描述的是「所有觸發器同時取樣」,阻塞賦值描述的是「一串組合邏輯」。 在一個時脈觸發的 always 區塊裡,如果右側有跨時槽的相依,用錯賦值就等於把一級流水線拿掉。
更明顯的例子是計數器:
// 正確
always @(posedge clk) count <= count + 1;
// 錯誤(功能幾乎等價,但硬體拓撲不同)
always @(posedge clk) count = count + 1;
在這裡兩者恰好合成出相同的硬體,因為 count 只出現在自己右邊。但一旦有第二個暫存器讀 count,兩者的有效延遲立刻不同——這正是競態 bug 難以在模擬中被發現的原因。
語法決定硬體結構:映射表#
這是整個硬體語言領域最重要的一張表。
| 程式碼寫法 | 合成結果 | 硬體結構 |
|---|---|---|
always @(posedge clk) q <= d; | D 觸發器 | 一顆 FF + 時鐘樹 |
always @(*) q = d; | 組合邏輯 | 一層邏輯閘(無記憶) |
always @(*) q = d;(有回授) | Latch | 電容記憶 + 時鐘無效 |
always @(posedge clk) q = d; | 工具相關 | 可能是 FF,也可能被最佳化成組合邏輯加警告 |
assign q = d; | 連續賦值 | 一層邏輯閘(永遠組合) |
initial q = 0; | 取決於裝置 | ASIC 上無意義,FPGA 上是初始化值 |
for (i=0; i<N; i=i+1) ... | 通常是 N 份硬體 | 複製,不是迴圈 |
最後兩列特別值得強調。硬體語言裡的 for 不是迴圈,是複製。 這是初學者最常犯的誤解,也是「語法決定硬體結構」最戲劇性的一例。
另一個關鍵差異是 Latch 的隱形存在。在 always @(*) 中,如果某個分支沒有被賦值,工具必須推斷出「條件不成立時要保持原值」,也就是生成 latch。Latch 在 ASIC 上可接受(面積小),但在 FPGA 上通常被轉成 FF 加邏輯,導致時序分析結果與 RTL 直覺不符——這是 ASIC 與 FPGA 流程之間最著名的行為差異之一。
隱含網路:Verilog 最著名的「友善陷阱」#
Verilog 有一個至今仍存在於 SystemVerilog 的特性:如果一個識別字在運算式裡被使用但未被宣告,編譯器會自動宣告它為 1-bit 的 wire。
module ImplicitNet (
input wire clk,
input wire rst,
output wire [7:0] data
);
always @(posedge clk) begin
if (rst) begin
cnt <= 0; // 忘記宣告
end else begin
cnt <= cnt + 1; // cnt 隱含宣告為 1-bit wire
end
end
assign data = cnt; // 隱含 1-bit wire 接到 8-bit 輸出
endmodule
這段碼會編譯通過嗎?多數工具會報錯或警告(現代工具預設對隱含網路發出警告),但在 Verilog 的寬鬆時代,它會安靜地通過:cnt 成為 1-bit wire,cnt + 1 恆為 1(1-bit 溢位),整個計數器根本不存在。
為什麼保留這個特性? 因為它是「速寫利器」。早期 Verilog 的速寫風格是:
module Adder (
input [7:0] a,
input [7:0] b,
output [8:0] s
);
assign s = a + b; // 埠 a, b, s 都必須宣告,但內部暫存不需要
endmodule
配合「不需要宣告暫存」的心智模型,隱含網路讓工程師可以只寫意圖、不寫宣告,對教育與速寫極為友善。但它的代價是打字錯誤不會被語言擋下:
always @(posedge clk) begin
count <= count + 1; // 拼錯成 "conut"
end
conut 會被自動宣告成 1-bit wire,結果是 conut 永遠等於 1。這個 bug 在波形上看起來「有訊號在動」,因此極難被發現——這類錯誤正是 Verilog 時代 RTL 除錯的主要痛點之一。
SystemVerilog 保留了這條規則(為了相容性),Veryl 則完全廢除它:未宣告的識別字一律是編譯錯誤。這是 Veryl「消除隱含規則」原則最直接的例子。
參數化模組:從 parameter 到 defparam 的教訓#
Verilog-1995 的模組參數化只有 parameter:
module Fifo #(
parameter DEPTH = 16,
parameter WIDTH = 8
) (
input clk,
input rst,
input push,
input [WIDTH-1:0] din,
output full,
output empty,
output [WIDTH-1:0] dout
);
// ...
endmodule
這已經很好用。問題出在參數覆寫。
第一種方式:具名實例的參數覆寫(parameter override by instance),Verilog-2001 正式化:
Fifo #(.DEPTH(32), .WIDTH(16)) u_fifo (
.clk(clk), .rst(rst), .push(push), .din(din),
.full(full), .empty(empty), .dout(dout)
);
第二種方式:defparam,Verilog-1995 就存在,但實務上是模擬器廠商(而非 IEEE)推廣的擴充:
Fifo u_fifo ( ... );
defparam u_fifo.DEPTH = 32;
defparam u_fifo.WIDTH = 16;
defparam 的問題在於就地修改(hierarchical override):它從階層上的某個節點「伸手進去」修改另一個模組的參數。這個做法有幾個嚴重的問題:
- 它是隱式的。修改藏在檔案某處的一條陳述式裡,讀模組定義的人完全看不到參數被改掉了。
- 它與具名覆寫互斥。IEEE 1364-2001 明確禁止在同一個具名實例上同時使用
defparam與具名參數覆寫。 - 它的可攜性差。因為是擴充,不同模擬器的行為細節曾經有差異。
這段歷史有兩層教訓。第一層是語言層的:「隱式的就地修改」是硬體語言設計中的反模式,因為硬體設計是階層式的,任何跨階層的隱式耦合都會破壞模組的可組合性。Veryl 因此從一開始就把參數覆寫限定為具名、顯式、單向的形式。第二層是流程層的:參數化不只需要模組參數,還需要「產生結構不同的模組」(例如產生 4 個不同寬度的 FIFO),這正是 generate / genvar 與後來的泛型要解決的問題。
Verilog-95 與 Verilog-2001:兩個關鍵躍遷#
Verilog-95(IEEE 1364-1995)#
這次標準化的核心變更有三項:
- ANSI 風格埠宣告。Verilog-1995 要求把方向與位寬寫在
module關鍵字之後、埠名稱之前:
// Verilog-1995 風格
module Adder (a, b, sum, cout);
input [7:0] a;
input [7:0] b;
output [7:0] sum;
output cout;
// Verilog-95 ANSI 風格
module Adder (
input wire [7:0] a,
input wire [7:0] b,
output wire [7:0] sum,
output wire cout
);
ANSI 風格把埠的型別資訊集中在一處,這是模組介面可讀性的第一次結構化改善,也是後來介面(interface)機制的先聲。
- 明確的阻塞 / 非阻擋賦值語意與
$time等系統任務。 - 配置(configuration)機制的引入,讓同一組模組可以依宣告的方式組裝成不同組態。
Verilog-2001(IEEE 1364-2001)#
Verilog-2001 是 Verilog 最後一次大改,也是 Verilog 唯一一次「完美」的版本:
generate/genvar/generate if/generate case/generate for。這讓「參數化產生結構」變成一級語言機制。
module Bank #(parameter N = 4) (input clk, input [7:0] din, output [7:0] dout);
wire [7:0] tmp [0:N-1];
genvar i;
generate
for (i = 0; i < N; i = i + 1) begin : g_slice
assign tmp[i] = din ^ i[7:0];
end
endgenerate
assign dout = tmp[0];
endmodule
注意 generate for 的語意:迴圈主體被複製 \(N\) 份,每一份有自己命名空間(g_slice[0]、g_slice[1]…)。這正是「硬體迴圈」的概念,也是之後所有硬體語言泛型機制的原點。
- 配置(configuration)的正式化與多模組支援。
- PLI 1.0(Programming Language Interface),把 Verilog 接到 C 語言,讓 Verilog-XL 時代的模擬器擴充機制正式化。
signed關鍵字,補上整數符號的支援。
generate 的歷史意義#
generate 的出現值得單獨強調,因為它是 Verilog 從「速寫語言」走向「工程語言」的關鍵一步。
在 generate 之前,參數化模組只能改變埠的寬度與內部常數;要改變結構(例如產生 \(N\) 個並聯運算單元),唯一的辦法是:
- 在模組外部用
for迴圈產生 \(N\) 個實例(但迴圈在頂層模組裡,模組本身不知道); - 或者依賴
defparam這類隱式機制。
generate 把「依參數產生結構」變成模組內部可宣告、可重複使用、可組合的東西。從這一刻起,Verilog 才真正能支撐大型設計的可重用階層。
Verilog 的黃金年代(1990-2005)#
1990 到 2005 這十五年,是 Verilog 的黃金年代,也是硬體設計語言史上最樂觀的一段時期。
產業面:
- 幾乎所有 EDA 工具都把 Verilog 當作第一等公民。Synopsys 的 Design Compiler、DC 的輸出網表是 Verilog;Cadence 的 Virtuoso、Innovus 一樣;FPGA 廠商的 Quartus、Vivado、LibreSilicon 在早期都以 Verilog 為主。
- IP 產業成型。ARM、MIPS、Altera、Xilinx、以及後來的 numerous 專用 IP 供應商全部用 Verilog 交付。IP 以「原始碼 + 加密網表」的形式流通,Verilog 是通用載體。
- EDA 工具的商業模式成熟。邏輯合成、時序分析、模擬、形式驗證、DFT(設計-for-test)插值——每一個環節都是一門生意,每門生意的輸入格式都是 Verilog。
教育面:
- Verilog 成為數位設計課程的預設語言。
- 「用 Verilog 學計算機結構」這件事因此成為可能——可以先寫一個可模擬的處理器,再讓它上 FPGA。
- 網路上的教學、程式碼範例、stackoverflow 的累積效應,讓新進工程師的第一語言變成了 Verilog。
Verilog 之所以能持續勝出,除了「先發優勢」之外,還有一個常被忽略的技術理由:Verilog 的抽象層級恰好合適。
Verilog 太低階——你會被迫寫出 assign 與位元操作來描述一個簡單的暫存器陣列,太彆扭;Verilog 太高階——你無法描述一個帶參數的 FIFO 與它的介面。Verilog 剛好落在「RTL 抽象」這個甜蜜點上,而這個抽象在 1990 年代是夠用的。
但到了 2000 年代中期,這個甜蜜點開始不夠用了。理由是三個:
- 匯流排協定變複雜。AXI、PCIe、AHB——這些協定包含數十個訊號、多個通道、複雜的握手規則。用「一束訊號」來表達一個協定,需要在多個模組裡重複宣告相同的訊號清單。
- 模組數量爆炸。一個大型 SoC 有上萬個模組,每個模組的埠宣告都要重複寫,型別不一致的錯誤難以發現。
- 驗證環境變複雜。多個 DUT、多個協定、多個情境,Verilog 完全沒有能力表達「帶方法的物件」。
這三個壓力在 2005 年匯聚,SystemVerilog 因此誕生。
四個結構性缺陷#
我們把 Verilog 的問題收束成四點,因為這四點正好對應 SystemVerilog 的四個主要功能,也正好對應 Veryl 的四個主要設計原則。
缺陷一:語法糖掩蓋語意(Syntactic Sugar Hiding Semantics)#
Verilog 讓程式碼「看起來像 C」,但語意不是 C。具體表現在:
- 隱含網路讓未宣告的識別字變成硬體。
x傳播讓「未初始化」與「不確定」在算術中無法區分。- 阻塞與非阻擋的差異只由字元
=與<=決定,卻導致完全不同的硬體拓撲。 - 求值順序依賴程式碼位置。
系統性問題:工程師在寫碼時無法建立「這個程式碼會變成什麼電路」的心智模型,只能靠記憶大量隱藏規則與例外。
缺陷二:沒有嚴謹的型別系統#
Verilog 的型別只有 wire、reg、integer、real、time、signed 等少數幾種,而且語意分工不嚴謹:
reg這個名字極其誤導——在 Verilog-1995 中reg只是「可以出現在某些程序性賦值左側」的變數宣告,與「暫存器」或「register」毫無關係。一個純組合邏輯的變數也常常必須宣告為reg。- 位寬是宣告的一部分而非型別的一部分,導致隱式的位寬擴充與截斷。
- 沒有
enum、struct、union、二維陣列。 - 沒有
always_comb/always_ff/always_latch這類意圖標記。
系統性問題:型別錯誤無法在編譯期被全面捕捉,許多錯誤只會以「綜合結果不如預期」的形式出現。
缺陷三:沒有模組間契約#
Verilog 的模組介面就是「一束方向與寬度各异的訊號」。沒有介面(interface)、沒有埠方向約束(modport)、沒有參數化的協定描述。
兩個模組要對接一個 SPI 匯流排,兩邊都要各自完整地宣告 5 個訊號的名稱與寬度,並且必須手工保持一致。當協定有 40 個訊號(AXI4)時,這是不可維護的。
系統性問題:模組間契約沒有單一真相來源,錯誤只會在整合時以「訊號接錯」的形式浮現。
缺陷四:語意負擔全落在設計者身上#
Verilog 不強迫工程師宣告任何意圖:
- 不說明這個區塊是「時脈觸發的暫存器」還是「組合邏輯」——全靠
always的敏感度列表猜。 - 不說明「這裡是串接還是並行」——全靠賦值字元記。
- 不說明「這個連接是驅動還是接收」——全靠
wire與多驅動的隱含規則。
Verilog 是信任工程師的語言。這個信任在 1990 年代的小設計上成立,在 2000 年代的百萬閘級 SoC 上不成立。
Verilog 到 SystemVerilog:關鍵語法對照#
下一節將進入 SystemVerilog 時代。先把這份對照表放在這裡,作為本節的收束,也作為下一節的入口。
| 需求 | Verilog(2001) | SystemVerilog(2005) | Veryl |
|---|---|---|---|
| 宣告資料型別 | reg [7:0] x; | logic [7:0] x; | var x : logic [7:0] |
| 連續賦值 | assign y = a & b; | 同 | assign y = a & b; |
| 組合邏輯 | always @(*) y = a & b; | always_comb y = a & b; | always_comb { y = a & b; } |
| 時序邏輯 | always @(posedge clk) q <= d; | always_ff @(posedge clk) q <= d; | always_ff { q <= d; } |
| 鎖存器 | always @(*) if (e) q = d; | always_latch if (e) q = d; | 隱含禁止,須明確建模 |
| 埠(舊式) | module M(a,b); input a; output b; | 保留但棄用 | 不支援 |
| 埠(ANSI) | module M(input wire a, output reg b); | module M(input logic a, output logic b); | module M (i_a: input logic, o_b: output logic) |
| 結構體 | 無 | typedef struct packed { ... } t; | struct t { ... } |
| 列舉 | 無 | typedef enum logic [1:0] {A,B} e; | enum e: logic [1:0] { A, B } |
| 介面 | 無 | interface bus_if #(parameter W=8); ... endinterface | interface bus_if ( ... ) { ... } |
| 埠方向約束 | 無 | modport master (output ...); | modport(部分支援) |
| 類別 | 無 | class C; ... endclass | 無(刻意不提供) |
| 參數化類別 | 無 | class C #(parameter type T = int); ... endclass | gen 區塊 + 泛型模組 |
| 硬體迴圈 | generate for | generate for | for in always_comb / gen |
| 巨集 | `define | `define + `include | 幾乎不需要 |
| 斷言 | 無 | assert property (@(posedge clk) a \|-> ##1 b); | assert 陳述式 |
| 隨機化 | 無 | rand + constraint | 交由外部工具 |
從這張表可以看到一個趨勢:Verilog 的每一項功能在 SystemVerilog 都被「擴充」而非「重構」。擴充意味著舊的陷阱全部保留。隱含網路還在,x 還在,競態還在,阻塞與非阻擋的語意負擔還在。SystemVerilog 在這四十年裡從未真正「重新思考」Verilog 的語意核心——它只是在旁邊加了一層。
這正是下一節要說的故事:SystemVerilog 的黃金年代,以及它的帳單。
下一節:第 1.2 節「SystemVerilog 的黃金年代與 UVM 的代價(2005-2017)」