硬體描述語言:為什麼 Verilog 比 C 難寫

modern%20workspace%20with%20laptop%2C%20smartphone...
發表時間:2026 年 10 月 25 日 | 更新日期:2026 年 10 月 25 日 | 編輯:雅寶社區編輯團隊
硬體描述語言:為什麼 Verilog 比 C 難寫 - 雅寶社區 · 頂客論壇

Verilog:告訴工具「電路長什麼樣」

Verilog 則是一種宣告式(declarative)語言。你寫下的不是「步驟」,而是「結構」。當你寫下一行 assign y = a & b;,你並不是命令某個東西去執行 AND 運算,而是宣告「這裡有一個 AND 閘,它的兩個輸入是 a 與 b,輸出是 y」。

這些描述在合成(synthesis)之後,會變成一張由邏輯閘、正反器、連線組成的電路網表(netlist)。電路一旦成型,所有閘門就是同時、持續地在工作,沒有「先做哪一個」的問題。

平行性是硬體的預設值,卻是軟體的最難題

這是整篇文章最關鍵的一句話:在 Verilog 裡,平行(concurrency)是預設值;在 C 裡,平行是需要特別處理的例外。

在 C 語言中,如果你要兩件事同時發生,你得動用執行緒(thread)、多行程(process)、中斷(interrupt),還要處理鎖、競爭條件、記憶體同步等問題。但在 Verilog 中,你只要寫下兩個 always 區塊,它們天生就是同時運作的,不需要任何額外機制。

聽起來很美好,對吧?問題在於:多數人的大腦是「序列式」的,我們習慣一件事做完再做下一件。要一次思考十幾條電路同時運作、彼此還可能互相影響,這種「空間式」思考需要刻意訓練。這也是為什麼很多 C 寫得很強的人,寫 Verilog 卻頻頻卡關——他們的大腦被訓練成序列處理器,而不是並行處理器。

二、思維模型的分野:時間軸 vs. 空間圖

要真正理解 Verilog 的難,得先理解它背後的「時間觀」與 C 語言完全不同。以下用三個角度切入。

軟體的時間是「序列」,硬體的時間是「時鐘邊緣」

在 C 語言裡,「時間」是一個抽象的概念,代表程式碼執行的先後順序。你寫 a = 1; b = a + 1;,順序是明確的:先做 a = 1,再做 b = a + 1。

在 Verilog 裡,「時間」則是一個物理量。電路中真正決定資料何時被更新的,是時鐘訊號(clock)的上升緣或下降緣。你寫的 always @(posedge clk),意思是「每當時鐘從低電位轉為高電位時,這段邏輯就會被觸發」。

換句話說,C 的程式是「一路執行到底」,Verilog 的電路則是「每個時鐘邊緣更新一次狀態」。你不再問「程式跑到哪一行了」,而是問「這個時鐘週期結束時,暫存器裡的值會變成多少」。

變數不是記憶體,而是「導線」

在 C 裡,變數是記憶體中的一塊空間,可以隨時讀寫。但在 Verilog 裡,不同型別的「變數」有不同的物理對應:

wire:對應實際的導線,不能儲存值,只能被驅動。

  • reg:在 always 區塊中可被賦值,綜合後可能是正反器,也可能是組合邏輯的暫存值。
  • logic(SystemVerilog):統一型別,實際對應什麼要看使用情境。
  • 這代表你不能像 C 那樣「隨便宣告一個變數然後亂用」。你宣告的每一個東西,都會在電路圖上佔據某個位置:一根導線、一組正反器、一塊記憶體。這也解釋了為什麼 Verilog 的型別系統雖然寬鬆,卻比 C 更要求你「知道自己在寫什麼」。

    一個加法器,兩種世界觀

    我們用同一個功能來看兩種語言的差異。C 語言的加法:

    int sum = a + b;

    Verilog 的加法,如果寫成組合邏輯:

    assign sum = a + b;

    如果寫成有時鐘的循序邏輯:

    always @(posedge clk) begin

    if (rst)

    sum <= 0;

    else

    sum <= a + b;

    end

    第一種寫法會合成出一顆加法器,輸入一變輸出就跟著變;第二種寫法會合成出加法器加上一組正反器,只有在時鐘上升緣時,加法結果才會被鎖進暫存器。

    同樣是「相加」,你必須明確決定:這個結果需不需要被記住?需不需要等待時鐘? 這個問題在 C 裡不存在,在 Verilog 裡卻是每一天都要面對的設計決策。

    三、語法相似帶來的陷阱:看起來像 C,其實不是

    Verilog 的語法刻意設計得「很像 C」,本意是降低工程師的學習門檻,結果卻常常造成反效果。以下幾個陷阱幾乎是所有初學者必踩的。

    阻塞與非阻塞賦值:一個等號的生死之差

    Verilog 有兩種賦值方式:=(阻塞賦值,blocking)與 <=(非阻塞賦值,non-blocking)。這大概是 Verilog 最惡名昭彰的設計,因為兩者只差一個符號,行為卻天差地遠。

    先看阻塞賦值:

    always @(posedge clk) begin

    a = b;

    c = a;

    end

    因為是阻塞式,程式會「等」a = b 執行完,才執行 c = a,所以 c 拿到的會是更新後的 a,也就是 b 的值。

    再看非阻塞賦值:

    always @(posedge clk) begin

    a <= b;

    c <= a;

    end

    非阻塞賦值的特性是:所有右邊的運算式會先被計算完,等到時間步結束才統一更新左邊。所以 c 拿到的是更新前的 a,也就是上一個時脈的值。

    這個差異在設計移位暫存器時尤其致命。如果你用 = 寫一個三級移位暫存器,結果全部的值會在一個時脈內「穿過」所有級,變成直接連通,功能完全錯誤。

    業界慣例是:組合邏輯用 =,時序邏輯用 <=。這不是硬性規定,而是為了避免競爭條件(race condition)與模擬/合成不一致的保命規則。C 語言完全沒有這種概念,所以初學者很難「感覺」到差別,只能靠紀律。

    for 迴圈不是迴圈,是「複製貼上」

    在 C 裡,for 迴圈是執行時期的控制結構,電腦會真的「繞」很多圈。但在 Verilog 裡,for 迴圈在合成時會被完全展開(unroll),也就是說,迴圈跑幾次,電路就複製幾份。

    always @(*) begin

    parity = 1'b0;

    for (i = 0; i < 8; i = i + 1)

    parity = parity ^ data[i];

    end

    這段程式不是「執行八次 XOR」,而是「合成出八顆 XOR 閘串接」。迴圈變數 i 在硬體中根本不存在,它只是撰寫時的輔助工具。

    這也是為什麼 Verilog 的 for 迴圈邊界必須是常數:如果你寫 for (i = 0; i < n; i = i + 1) 而 n 是執行期才決定的變數,合成工具會直接告訴你「無法合成」。因為電路是靜態的,不可能在「執行時期」決定要長幾顆閘。

    if-else 不是流程控制,是「多工器」

    C 語言的 if-else 是「選擇要執行哪一段程式碼」。Verilog 的 if-else,在組合邏輯中其實是「選擇要輸出哪一個訊號」,也就是多工器(multiplexer)。

    always @(*) begin

    if (sel)

    y = a;

    else

    y = b;

    end

    這段會合成出一個 2 對 1 的多工器,sel 是選擇線。如果這段寫在 always @(posedge clk) 裡,則會合成出帶有致能(enable)的正反器,且如果沒有寫 else 分支,還可能推斷出鎖存器(latch)。

    這裡有個經典陷阱:不完整的 if 或 case 會推斷出鎖存器。因為電路必須「記住」上一個狀態,才能在下一次條件不成立時維持原值。多數初學者在組合邏輯中漏寫 else,結果合成報告出現一堆 latch,功能還時好時壞。

    位元寬度與溢位:硬體世界的強型別現實

    在 C 裡,int 通常是 32 位元,加法的溢位行為雖然是未定義或依實作而定,但至少「變數型別」是明確的。在 Verilog 裡,每一個訊號的位元寬度都由你自己決定,而寬度不匹配會導致無聲的錯誤。

    reg [7:0] a;

    reg [7:0] b;

    wire [8:0] sum = a + b; // 正確:多留一位進位

    wire [7:0] bad = a + b; // 危險:進位被截掉

    更麻煩的是有號數與無號數的混用。Verilog 預設是無號數,一旦你混用 signed 與 unsigned,運算結果可能跟你想的完全不同,而且不會有任何警告。

    這種「位元級」的思考,是軟體工程師最不習慣的地方。在 C 裡你很少需要關心「這個變數佔幾個 bit」,在 Verilog 裡,每一個 bit 都是真金白銀的硬體資源。

    四、C 語言沒有教你的硬體現實

    寫 C 程式的時候,你不太需要煩惱「這段程式碼要佔多少面積」、「跑起來多快」、「耗多少電」,因為這些都由 CPU 架構與編譯器幫你處理掉了。但寫 Verilog 的時候,這些問題全部回到你身上。

    時鐘、時序與「時間」其實是物理量

    在 FPGA 或 ASIC 設計中,時鐘頻率決定了電路的效能上限。而影響最高頻率的關鍵,是資料在邏輯閘之間傳遞所需的時間,也就是組合邏輯延遲。

    假設你在一個時鐘週期內串接了太多層邏輯閘(例如連續做了十次加法),資料還沒穩定下來,下一個時鐘邊緣就來了,暫存器就會鎖進錯誤的值。這就是所謂的建立時間違反(setup time violation)。

    為了解決這個問題,工程師必須做「管線化(pipelining)」:把長的組合邏輯切開,中間插入暫存器。這種「為了時序而重寫架構」的工作,在 C 語言裡幾乎不存在對應概念,卻是硬體設計的日常。

    除了建立時間,還有保持時間(hold time)、時鐘偏斜(clock skew)、跨時脈域(CDC)等問題。這些名詞對軟體工程師來說完全陌生,卻是硬體工程師判斷電路能不能動的基本功。

    面積、功耗、時脈頻率的三角取捨

    硬體設計永遠在三個目標之間拉扯:面積(Area)、功耗(Power)、效能(Performance),業界常稱為 APP 三角。

    面積:電路佔用多少邏輯閘或 LUT,直接影響晶片成本。

  • 功耗:動態功耗與切換頻率、電容、電壓平方成正比,靜態功耗則與製程有關。
  • 效能:也就是能跑多高的時脈頻率。

    舉例來說,你想讓加法更快,可以改用進位超前加法器(carry-lookahead adder),但它會用掉更多面積與功耗。你為了省面積把邏輯共用,可能導致關鍵路徑變長、頻率上不去。這種「沒有標準答案」的取捨,是硬體設計最燒腦的部分,也是 C 語言開發者從未面對的決策壓力。

    合成工具不是編譯器

    很多人會把「合成(synthesis)」想成「編譯(compile)」,這是另一個常見誤解。編譯器的工作是把高階語言轉成組合語言,只要語意正確,產出什麼指令隨便它。合成工具的任務則是推斷你的程式碼代表什麼電路,然後用目標製程的元件庫把它兜出來。

    差別在於:編譯器有明確的語言規範,合成工具卻充滿「推斷」與「最佳化」。同一段 Verilog,不同工具可能合成出不同結構;你以為寫的是 A 電路,工具可能推斷成 B 電路。更慘的是,模擬結果正確,合成後卻不能動,這就是所謂的「模擬與合成不匹配(simulation-synthesis mismatch)」。

    所以寫 Verilog 的時候,你不能只想著「語意對不對」,還要想着「工具會怎麼解讀這段程式碼」。這種「跟工具對話」的經驗,只能靠實作慢慢累積。

    五、除錯方式的徹底改變

    寫 C 的時候,你的好朋友是 printf、gdb、還有各種 profiler。遇到 bug,印幾個變數、設個中斷點,通常半小時內能找到問題。但到了 Verilog,這套方法幾乎全部失效。

    printf 不見了:波形圖才是硬體的眼睛

    Verilog 有 $display 與 $monitor,可以在模擬時印出訊號值,但它們不可合成,只用於測試平台(testbench)。而且硬體的問題往往不是「某個值算錯」,而是「時序對不上」、「訊號在某個時刻被多驅動了一次」、或是「某個暫存器根本沒被重置」。

    這些問題用文字輸出很難觀察,所以硬體工程師的主要除錯工具是波形圖(waveform)。透過模擬器(如 ModelSim、VCS、Verilator)產生的波形檔,你可以看到每一個訊號在每一個時間點的值,以及時鐘邊緣時暫存器如何變化。

    波形圖的思考方式跟 printf 完全不同:printf 是「點」的觀察,波形圖是「面和時間軸」的觀察。你必須學會「讀時序」,也就是在時間軸上判斷某個訊號的變化是否符合預期。這種能力,是軟體工程師轉硬體時最需要重新鍛鍊的。

    模擬通過不代表電路會動:Simulation-Synthesis Mismatch

    另一個殘酷的現實是:模擬通過的設計,不等於能合成、能燒錄、能實際運作。

    常見的情況包括:

  • 使用了不可合成的語法(如 #10 延遲、while 迴圈、initial 區塊的某些寫法),模擬器跑得很開心,合成工具直接報錯。
  • 組合邏輯中出現毛刺,模擬時因為事件排序恰好沒事,實際電路卻會誤觸發。

  • 多重驅動(multiple drivers):兩個 always 區塊同時驅動同一個訊號,模擬時可能出現 X,合成後可能短路。
  • 非同步復位處理不當,導致上電時暫存器處於未知狀態。

    這些問題之所以難抓,是因為它們跨越了「程式邏輯」與「物理現實」兩個世界。C 語言開發者習慣「邏輯正確就等於程式正確」,但硬體世界裡,邏輯正確只是最基本的要求。

    六、抽象層次的斷崖與「不可合成」的誘惑

    Verilog 另一個讓人困惑的地方,是它同時支援多個抽象層次,而這些層次的界線並不清楚。

    行為級、RTL、閘級:你在寫哪一層?

    Verilog 大致可以分為三個抽象層次:

  • 行為級(Behavioral):用高階語法描述功能,類似 C 語言,常用於 testbench 或演算法驗證。
  • 暫存器傳輸級(RTL):描述每個時脈週期資料如何在暫存器之間流動,這是可合成設計的主要層次。
  • 閘級(Gate-level):直接描述邏輯閘與連線,通常由合成工具產出,人工較少直接撰寫。
  • 初學者最容易犯的錯,就是把行為級寫法當成 RTL 來寫,結果發現「模擬會動,但不能合成」。例如用 for 迴圈搭配變數邊界、用 while 等待某個條件、用 fork-join 平行執行——這些在 testbench 裡很好用,在可合成電路裡卻行不通。

    可合成與不可合成的界線

    以下整理常見的可合成與不可合成語法,供初學者參考:

    語法/結構

    可合成?

    說明

    assign 連續賦值

    對應組合邏輯

    always @(posedge clk)

    對應時序邏輯

    always @(*)

    對應組合邏輯

    if / case

    推斷多工器或致能邏輯

    for(常數邊界)

    合成時展開

    for(變數邊界)

    無法決定電路規模

    while

    通常否

    無法靜態決定迭代次數

    #10 延遲

    僅用於模擬

    $display 等系統任務

    僅用於模擬與除錯

    initial 區塊

    視情況

    FPGA 可做初值設定,ASIC 一般不支援

    指標、動態記憶體

    硬體中沒有對應概念

    看懂這張表只是第一步,真正的難點在於:很多時候你寫的程式碼「看起來」是 RTL,實際上卻混入了行為級語法。這需要經驗才能判斷,也需要養成「寫之前先想電路」的習慣。

    七、為什麼還是值得學:難度背後的價值

    講了這麼多難處,或許你會問:既然如此,為什麼還是有這麼多人投入 FPGA 與 IC 設計?答案很簡單:因為這個「難」是有回報的,而且它訓練的是完全不同層次的能力。

    難,是因為你同時在學三件事

    學 Verilog 的過程,其實是同時在學三件不同的事:

    硬體思維:理解平行、時序、資源限制,這是一種全新的世界觀。

  • 數位邏輯設計:從邏輯閘、正反器、狀態機到匯流排協定,這是電路的基礎知識。
  • 工具鏈操作:模擬、合成、佈局佈線、時序分析,每一套工具都有各自的眉角。
  • 任何一個人要在短時間內同時吸收三大領域,本來就會覺得吃力。這不是你不夠聰明,而是學習曲線本來就長這樣。相較之下,C 語言的學習曲線雖然也陡,但至少「程式思維」與「工具操作」是分開的,比較容易分段突破。

    從「寫程式」到「設計系統」

    當你真正跨過 Verilog 的門檻之後,你會發現自己的思考方式改變了。你開始會問:「這個運算能不能用更少的邏輯閘完成?」「這個狀態機有沒有死鎖的可能?」「這條路徑的延遲會不會成為時序瓶頸?」

    這些問題的答案,不再只是「程式跑得對不對」,而是「這個系統在真實世界裡能不能穩定運作」。這種從「寫程式」升級到「設計系統」的視角轉換,是 Verilog 帶給學習者最有價值的禮物。

    八、給初學者的實務建議

    如果你已經決定要跨入 Verilog 的世界,以下提供幾個實務上的建議,幫助你少走一點冤枉路。

    學習順序與心法

  • 先補數位邏輯:不要急著寫程式,先把邏輯閘、卡諾圖、正反器、狀態機搞清楚。沒有這些基礎,寫 Verilog 就像在背咒語。
  • 從組合邏輯開始:先練習 assign 與 always @(*),熟悉多工器、解碼器、加法器的寫法。
  • 再進入時序邏輯:練習正反器、計數器、移位暫存器,並嚴格遵守「時序邏輯用非阻塞賦值」的紀律。
  • 一定要寫 testbench:不要只看波形,要學會用 testbench 自動驗證。這是專業與業餘的分水嶺。
  • 用 FPGA 實作:買一塊開發板,把設計燒進去,親眼看到 LED 閃爍,會比模擬一百次更有感。
  • 常見錯誤檢查清單

    每次完成一個模組,可以拿以下清單自我檢查:

  • 組合邏輯的 if / case 是否都有完整的 else 或 default,避免推斷出 latch?
  • 時序邏輯是否全部使用 <=?

    是否有同一個訊號被多個 always 區塊驅動?

    位元寬度是否足夠容納運算結果,尤其是加法與乘法?

    是否混用了 signed 與 unsigned?

    復位邏輯是否完整,且同步或非同步的選擇是否一致?

    是否有使用不可合成的語法?

    是否跑過 lint 工具或合成工具的警告?

    把這份清單養成習慣,可以避開八成以上的初學者錯誤。

    結語:難,但難得有道理

    回到最初的問題:為什麼 Verilog 比 C 難寫?

    答案不是「因為語法比較複雜」,也不是「因為工具比較難用」,而是因為 Verilog 要求你用完全不同的方式思考。它要求你放棄序列式的思維,改用平行式的思維;要求你不只想「程式怎麼跑」,還要想「電路怎麼長」;要求你不只考慮邏輯正確,還要考慮時序、面積、功耗與可合成性。

    這是一道貨真價實的門檻,但跨過去之後,你看待數位世界的方式會徹底改變。你會理解為什麼一顆晶片裡的幾十億個電晶體可以同時運作,也會理解為什麼一個看似簡單的功能,在硬體上卻需要如此謹慎的設計。

    如果你正在這條路上掙扎,請記住:你不是不會寫程式,你只是在學一種新的語言——一種用來描述電路的語言。給自己一點時間,從基礎邏輯開始,一步一步來。當你第一次看到自己設計的電路在 FPGA 上正確運作時,你會明白這一切的難,都是值得的。

    也希望這篇文章能幫助雅寶社區裡正在觀望或卡關的朋友,釐清 Verilog 的真實面貌。如果你有任何心得或問題,歡迎在論壇上一起討論,畢竟硬體設計這條路,有人一起走會輕鬆很多。

    ```

    🏠 返回首頁