跳轉到

1.1 從Verilog到SystemVerilog:一門語言的三十年演進(1984-2005)#

一個語言的出身決定了它的性格#

要理解 Verilog,必須先理解它不是為了什麼而設計的。

1984 年,Gateway Design Automation 發表了 Verilog。它的原始身分不是「硬體設計語言」,而是類比電路模擬器 Verilog-XL 的描述語言層。Gateway 當時的核心產品是模擬器;Verilog 是讓工程師用文字描述「這個電路長什麼樣子」,好讓模擬器去跑。

Philip Skinner 等主要作者在後續的 Verilog 教材中把這段歷史寫得很清楚:Verilog 的第一版語言規格只有十幾頁,設計目標是「讓模擬器有一個好用的輸入格式」。它不是語言學家或語言設計師的產物,而是模擬器架構的自然延伸。

這個出身留下了極深的烙印,直到今天我們還能從 Verilog(以及它的後代 SystemVerilog、Veryl)看出:

  1. 語法像 C——因為 C 是當時寫模擬器介面程式最自然的語言。工程師能快速上手。
  2. 核心抽象是「事件」——模擬器的工作是「在某個事件的影響下,某些訊號的值改變了」。語言的每一條語法規則都是為了服務這個模型。
  3. 優化目標是模擬速度,不是設計正確性——Verilog 從來沒有把「這段程式碼會被合成成什麼」當成一級公民。
  4. 語意的模糊處一律交給使用者——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 的值域不是二值的,而是四值的:

\[ \mathcal{V} = \{ 0, 1, x, z \} \]
  • 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:

  1. Reset 檢查失效:設計者想確認「所有狀態都已初始化」,寫了 if (state == 0),但未初始化的狀態其實是 x,所以 state == 0 結果是 x,if 把它當成假,檢查形同虛設。正確寫法是 === 0(或改用 SystemVerilog 的 casez / 工具的初始化檢查)。
  2. 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):它從階層上的某個節點「伸手進去」修改另一個模組的參數。這個做法有幾個嚴重的問題:

  1. 它是隱式的。修改藏在檔案某處的一條陳述式裡,讀模組定義的人完全看不到參數被改掉了。
  2. 它與具名覆寫互斥。IEEE 1364-2001 明確禁止在同一個具名實例上同時使用 defparam 與具名參數覆寫。
  3. 它的可攜性差。因為是擴充,不同模擬器的行為細節曾經有差異。

這段歷史有兩層教訓。第一層是語言層的:「隱式的就地修改」是硬體語言設計中的反模式,因為硬體設計是階層式的,任何跨階層的隱式耦合都會破壞模組的可組合性。Veryl 因此從一開始就把參數覆寫限定為具名、顯式、單向的形式。第二層是流程層的:參數化不只需要模組參數,還需要「產生結構不同的模組」(例如產生 4 個不同寬度的 FIFO),這正是 generate / genvar 與後來的泛型要解決的問題。


Verilog-95 與 Verilog-2001:兩個關鍵躍遷#

Verilog-95(IEEE 1364-1995)#

這次標準化的核心變更有三項:

  1. 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)機制的先聲。

  1. 明確的阻塞 / 非阻擋賦值語意與 $time 等系統任務。
  2. 配置(configuration)機制的引入,讓同一組模組可以依宣告的方式組裝成不同組態。

Verilog-2001(IEEE 1364-2001)#

Verilog-2001 是 Verilog 最後一次大改,也是 Verilog 唯一一次「完美」的版本:

  1. 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]…)。這正是「硬體迴圈」的概念,也是之後所有硬體語言泛型機制的原點。

  1. 配置(configuration)的正式化與多模組支援。
  2. PLI 1.0(Programming Language Interface),把 Verilog 接到 C 語言,讓 Verilog-XL 時代的模擬器擴充機制正式化。
  3. 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 年代中期,這個甜蜜點開始不夠用了。理由是三個:

  1. 匯流排協定變複雜。AXI、PCIe、AHB——這些協定包含數十個訊號、多個通道、複雜的握手規則。用「一束訊號」來表達一個協定,需要在多個模組裡重複宣告相同的訊號清單。
  2. 模組數量爆炸。一個大型 SoC 有上萬個模組,每個模組的埠宣告都要重複寫,型別不一致的錯誤難以發現。
  3. 驗證環境變複雜。多個 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)」