合成到底做了什麼
你的 RTL 是一份*行為描述*:「當這些輸入一致時輸出為高」「這個值在時脈邊緣更新」。它完全沒說是哪些電晶體在幹活。邏輯合成就是把這份行為描述翻譯成一張具體的真實閘網表的步驟——一份零件清單,外加它們之間的連線。把 RTL 想成用大白話寫的食譜(「翻拌到混勻為止」),把網表想成某位特定廚師會用的那一連串確切手部動作。
關鍵在於,工具是對著一個目標庫來做這件事的——這是一份固定的閘目錄,裡面的閘已經有人為你的製程節點設計好、刻畫好了。合成不是在發明電晶體,而是在從目錄裡選購,再把買來的東西排佈好。兩樣東西進去(你的 RTL 和一個單元庫),一樣東西出來(一張閘級網表),還有第三樣東西——你的約束——告訴工具「好」是什麼意思。
標準單元庫
標準單元庫就是合成選購時翻的那本目錄。每個單元都是一個小巧的、預先設計好的邏輯閘——一個反相器、一個 NAND、一個及或閘、一個 D 正反器——晶圓廠把它一直佈局到電晶體層級,並做了特性刻畫:對每個單元,他們都量過它翻轉要花多久、佔多少面積、燒多少功耗。這些單元共用一個固定的高度,於是能整整齊齊地拼成一行行,就像一堆同樣凸點高度的樂高積木。
下面這點會讓新手意外:同一個邏輯功能存在很多變體。一個庫裡可能放著十幾種「驅動強度」的 NAND2——小的那個又慢但只啜一點點功耗和面積,大的那個翻轉飛快但兩樣都吃得凶。真值表一樣,肌肉不一樣。合成不只挑*哪個*閘,還挑*這個閘的哪種尺寸*,而面積/時序/功耗的權衡,大半就活在這一個選擇裡。
RTL → 最佳化後的閘級網表
我們來落到實處。這裡有一小段 RTL:一個暫存器,每個時脈邊緣都把 `d` 裝進去,外加一個組合輸出。它描述的是行為——*發生了什麼*——從頭到尾沒點名過任何一個閘。
module tiny (input clk, input a, input b, input d,
output q, output y);
reg q_r;
always @(posedge clk) q_r <= d; // a register: remembers d each clock edge
assign q = q_r;
assign y = ~(a & b); // combinational: a NAND of a and b
endmodule現在合成把它映射到真實的庫單元上。那個時脈驅動的 `reg` 變成一個 DFF 標準單元;`~(a & b)` 塌縮成單獨一個 NAND2——正是庫裡已經備好的那個閘,所以一個反相器都沒浪費。結果是結構化的:現在每一行都點名一個*實體零件*,並接上它的接腳。
module tiny (clk, a, b, d, q, y); input clk, a, b, d; output q, y; DFF u_q (.CLK(clk), .D(d), .Q(q)); // chosen standard cell NAND2 u_y (.A(a), .B(b), .Y(y)); // chosen standard cell endmodule
約束左右最終結果
沒有指引,工具根本不知道你想要這份設計*又小*還是*又快*——而它沒法白白兩樣都要。約束就是你告訴它的方式。最重要的那一條是時脈週期:你宣告「這個時脈比如說跑在 1 ns」,這一個數字就成了每個訊號在兩個暫存器邊緣之間必須塞進去的預算。
# A constraint (timing format, not Verilog): the clock budget create_clock -name clk -period 1.0 [get_ports clk] # 1.0 ns -> 1 GHz
把這個週期收緊,工具就會*作出回應*:它會去抓更大、更快的單元,複製邏輯來縮短最長的那條路徑,重組運算以省下一級閘——拿面積和功耗去換速度。把週期放寬,它就反著來,換上小巧、低功耗的單元,因為它現在時間有餘裕。約束不只是檢查結果,它還主動塑造哪些單元會被選中。工具忙完後一條路徑上剩下的那點喘息空間,就是它的時序裕量——為正表示塞得下,為負表示失敗了,而最緊的那條路徑就是關鍵路徑。
面積/時序/功耗的取捨
合成裡的每一個選擇都會同時扯動三根線:面積(矽片,也就是錢)、時序(時脈能跑多快)和功耗(發熱和電池)。想像一個三角形——使勁往一個角拉,你就從另兩個角滑開。想更快?那就買更大的單元:面積漲、功耗漲。想省功耗?那就接受更慢的單元和更鬆的時脈。沒有哪種設定能三樣全贏;工具的全部工作,就是找出三角形裡那個仍然滿足你約束的最佳點。
這正是約束如此要緊的原因:它們告訴工具*你想住在三角形的哪個位置*。手機晶片偏向功耗那個角;高階 CPU 偏向時序,代價是面積和瓦數。合成器會在你卡得最緊的那一項上下最狠的功夫——給它一個激進的時脈,它就樂呵呵地拿面積和功耗去把它達成,不管你是不是真有這個意思。
讀懂一張網表
打開一張合成好的網表,乍看像天書——一頁頁帶著古怪名字的單元實例。但它不過是那本目錄的字面落地。分三遍來讀,它立刻就敞開了。
- 先找暫存器。凡是單元名以 DFF(或 FF、SDFF 等等)開頭的實例,都是一塊狀態——一個正反器。數一數它們,就知道你的設計*記住*了多少東西,而它們正是每一條時序路徑的起點和終點。
- 把單元名讀成功能加驅動強度。NAND2_X4 的意思是「二輸入 NAND,驅動強度 4」。字母是邏輯,末尾的數字是肌肉。一條路徑上扎堆出現的大數字,是工具在喊「這條路徑當時很緊——我在這兒砸了大單元」。
- 跟著接腳之間的連線走,別跟著行的順序走。網表是硬體,所以它的行全都同時存在;告訴你什麼餵給什麼的是 `.A(...) .Y(...)` 這些連接,而不是從上到下的順序。從一個暫存器的輸出,順著穿過那些閘,追到下一個暫存器的輸入——這條鏈就是一條時序路徑。