為什麼要用文字描述硬體?
想像一下,要畫出一座城市,得親手擺好每一塊磚。當一個設計超過幾千個閘之後,逐個閘去畫的電路圖就是這種感覺——而真正的晶片有數十億個閘。畫到某個程度,畫圖這件事就再也撐不下去了。你需要一種辦法,把*你想要什麼*說清楚,再讓機器去算每一塊磚。
這正是 HDL(硬體描述語言)的用武之地。你寫下一段精煉的文字描述——「這個區塊把兩個 8 位元數相加」「那個區塊在兩路輸入之間切換」——再由合成工具把你的話變成一張真正的閘級網路。文字才是真理之源;電路圖成了工具*替*你畫出來的東西,而不是你*親手*去畫的東西。
HDL 不是程式語言
這就是那個讓每個新手都栽跟頭的觀念。在軟體裡,程式碼是一行接一行執行的。而在 Verilog 這樣的 HDL 裡,每一行描述的是同時存在的硬體——更像一支管弦樂團裡各個聲部,而不像食譜裡的一步步操作。寫下 assign y = a & b; 並不是做一次「及」運算;它鋪下的是一根永久的導線,時時刻刻都在算這個結果。
module and_gate(input a, b, output y); assign y = a & b; // a permanent wire: y is ALWAYS (a AND b) endmodule
這一點一旦想通,你的 assign 敘述的先後順序基本就不再重要了。兩行 assign 描述的是兩根導線;先寫哪一行都沒差別,因為兩根線是同時通電的。這正是「描述硬體」與「寫程式」之間最深的區別——也是值得從第一天就養成的習慣。
你的第一個模組:一個閘
一個 Verilog 設計是由一個個模組搭起來的。模組就是一個貼了標籤的硬體盒子:它有一個名字、一組連接埠(也就是它的接腳,標著 input 或 output),還有一段說明接腳之間關係的主體。你把模組拼接起來的方式,就像把晶片插到電路板上——小盒子裝進大盒子裡。
module inverter(input a, // one pin in
output y); // one pin out
assign y = ~a; // y is always NOT a
endmodule把它讀成對一個零件的描述,而不是一連串動作:「這裡有個叫 inverter 的盒子;有一個進來的接腳 a 和一個出去的接腳 y;y 永遠是 a 的反面。」一通電,它就保持這個關係——沒有哪一行會「執行」。你將來會用到的每一個閘(及、或、非、互斥或)都只差一個運算子和一句 assign。
導線、訊號與匯流排
在模組內部、模組之間,數值都在導線上傳輸。一根導線只承載一個位元——一個 0 或一個 1。但你很少只搬一個位元;一個 8 位元數、一種顏色、一個位址,都需要好幾根導線綁在一起。這一捆就叫匯流排,在 Verilog 裡你用方括號寫出它的位元寬度。
wire ready; // 1 bit wire [7:0] data; // an 8-bit bus: data[7] down to data[0] wire [7:0] sum; // an 8-bit bus carrying the result of a + b assign sum = a + b; // (a and b are 8-bit wires declared elsewhere)
把匯流排想像成一條排線:許多股線芯並排黏在一起,作為一個有名字的整體一起移動。data[0] 是最低位那一股,data[7] 是最高位那一股。位元寬度不是裝飾——它是實打實的物理量。一個存放 data[7:0] 的暫存器就是八個正反器那麼寬,而更寬的匯流排,字面意義上就是晶片上更多的金屬線。
行為級與結構級
描述同一塊硬體有兩種寫法,好的設計者能在兩者之間自如切換。結構級是由下而上的:你點出各個零件,再親手把它們連起來,像一張裝配圖。行為級是由上而下的:你說出*你想要的行為*,讓合成去想出該用哪些閘。同一顆晶片,兩種高度的描述。
拿一個 2 選 1 多工器來說——它是一個開關,根據一根選擇線 sel 來挑選輸入 a 還是輸入 b。用行為級的寫法,你只要說出這個選擇,再讓工具去把它搭出來。
module mux2(input a, b, sel, output y); assign y = sel ? b : a; // sel=1 -> y=b, sel=0 -> y=a endmodule
*同一個*多工器的結構級版本,會把那些閘一個個寫出來——一個反相器、兩個「及」閘、一個「或」閘——再一股一股地連起來。兩種描述合成出來的是同一套硬體。這給我們的啟示是:從行為級起步,先描述意圖,只有在你需要對具體用哪些閘做精確控制時,才下沉到結構級。先站在高處;只在划得來的地方才放大細看。