從規則到數字:抵達時間與所需時間
想像一位快遞員,必須在收件人出發前往機場之前把包裹送到城市另一端。有兩個時刻很重要:包裹實際出現在門口的時間,以及它必須抵達的截止時間。若快遞員趕在截止前到達,皆大歡喜,還留有一點餘裕;若遲到了,收件人已經離開,這趟遞送就失敗了。數位時序講的正是這個故事,只是在一顆晶片上被重複了數百萬次。包裹是沿著時序路徑一路傳遞的邏輯值;門口是正反器的資料輸入端;而截止時間則由時脈邊緣加上該正反器的 setup 需求所決定。
我們給這兩個時刻正式的名字。[[ic-arrival-time|抵達時間]]是資料訊號實際到達某一點的時刻——也就是它從發射正反器出發後,一路上累積的所有延遲總和。[[ic-required-time|所需時間]]則是該訊號還能被安全擷取的最晚到達時刻。對一個一般的 setup 檢查而言,所需時間大致是 `(下一個時脈邊緣)−(setup 時間)−(時脈不確定量)`:資料必須在擷取邊緣之前一小段就已穩定安靜下來,而不是剛好踩在邊緣上。
Slack:決定一切的那個數字
一旦你有了抵達時間與所需時間,整個過或不過的問題就濃縮成一道減法。[[slack|時序裕度]]等於所需時間減去抵達時間。 這就是它的全部定義,值得牢牢記住,因為這個數字你往後整個職涯的每一次設計審查都會被問到。
slack = required_time - arrival_time slack > 0 -> PASS, with margin to spare (data arrived early) slack = 0 -> exactly on the edge (zero margin) slack < 0 -> VIOLATION (data arrived too late) Example (setup check at a capturing flop): required_time = 2.000 ns (edge 2.0 ns, setup already folded in) arrival_time = 1.870 ns ---------------------------------------------- slack = 2.000 - 1.870 = +0.130 ns -> PASS (+130 ps margin) Flip one delay so the data lands late: arrival_time = 2.090 ns slack = 2.000 - 2.090 = -0.090 ns -> FAIL (-90 ps, setup violation)
正的 slack 代表訊號早到了——這條路徑有餘裕,你可以拿去換更高的時脈頻率、更低的電壓,或更小更便宜的單元。零 slack 代表這條路徑剛好踩在臨界邊緣:任何額外延遲都會把它推向失敗。負的 slack 是一個違規——若是 setup 檢查就明確稱為 [[ic-setup-violation|setup 違規]]——其大小正好告訴你這條路徑需要變快多少。−90 ps 的 slack 表示你必須從這條路徑上至少削掉 90 皮秒(或把時脈放寬同樣的量),晶片才能跑在目標頻率上。
關鍵路徑不過就是最差的 slack
真實的一個區塊不是只有一條路徑,而是數百萬條,每條都有自己的 slack。[[critical-path|關鍵路徑]]就是整個設計中 slack *最小*(最負,或最不正)的那條。它就是瓶頸——最接近失敗的那一串邏輯,因此也是決定整顆晶片能跑多快的那條。把其他所有路徑都加強也沒用;唯獨關鍵路徑訂下了速度上限,就像單線道上最慢的那輛車,決定了後方每一輛車的步調。
正因如此,時序收斂是一場關於*排序輕重*的遊戲,而不是追求完美。工具會把每個終點依 slack 排序,先把最糟的幾條交給你。你修好關鍵路徑後,第二糟的路徑就成了新的關鍵路徑,如此反覆。它與頻率的關係非常直接:若你最差的 setup slack 是 +130 ps、時脈週期為 2.0 ns(500 MHz),你就有 130 ps 的餘裕,原則上可把週期壓到約 1.87 ns,slack 才會歸零。若最差 slack 是 −90 ps,你就*超支*了,這條路徑修好(或時脈放慢)之前,晶片無法達到 500 MHz。
逐行讀懂一份真實的 STA 報告
這一切都由[[static-timing-analysis|靜態時序分析]](STA)算出,這個工具不需模擬任何一組測試向量就能檢查每一條路徑——它純粹把延遲加總。它的主要輸出就是時序報告,一旦你能流暢地讀懂一份,你幾乎能除錯任何時序問題。下面是一份精簡但逼真的單條路徑 setup 報告。它分為兩半:資料抵達路徑(資料實際如何傳遞)與資料所需路徑(截止時間,由時脈構築)。從上讀到下即可。
Startpoint: u_ctrl/state_reg[2] (rising clk, launched by CLK) Endpoint: u_alu/result_reg[7] (rising clk, captured by CLK) Path Group: CLK Path Type: max (setup) Point Incr Path Type ------------------------------------------------------------- clock CLK (rise edge) 0.000 0.000 clock source latency 0.000 0.000 clock network delay (propagated) 0.182 0.182 <- clock path u_ctrl/state_reg[2]/CK (DFFX1) 0.000 0.182 r u_ctrl/state_reg[2]/Q (DFFX1) 0.094 0.276 r <- clk-to-Q net: state[2] (fanout=3) 0.041 0.317 r <- net delay u_and0/Y (AND2X2) 0.063 0.380 r <- cell delay net: n12 (fanout=1) 0.018 0.398 r u_add1/CO (FADDX1) 0.121 0.519 r net: carry (fanout=2) 0.029 0.548 r u_mux2/Y (MUX2X1) 0.072 0.620 r net: result_pre[7] (fanout=1) 0.022 0.642 r u_alu/result_reg[7]/D (DFFX1) 0.000 0.642 r data arrival time 0.642 <= ARRIVAL ------------------------------------------------------------- clock CLK (rise edge) 2.000 2.000 clock source latency 0.000 2.000 clock network delay (propagated) 0.205 2.205 <- capture clk clock uncertainty -0.050 2.155 u_alu/result_reg[7]/CK (DFFX1) 0.000 2.155 r library setup time (DFFX1) -0.061 2.094 <- setup data required time 2.094 <= REQUIRED ------------------------------------------------------------- data required time 2.094 data arrival time -0.642 ------------------------------------------------------------- slack (MET) 1.452 <= SLACK
把 Incr 欄讀成「這一個元件加了多少延遲」,把 Path 欄讀成「到此為止的累計總和」。資料路徑從發射時脈邊緣(時間 0)開始,先等時脈穿過網路(`clock network delay`,0.182 ns)抵達發射正反器,接著付出 `state_reg[2]` 的 clk-to-Q(0.094 ns)——也就是正反器輸出在其時脈邊緣後真正改變所需的時間。此後訊號在單元延遲(閘:那顆 AND、全加器的進位、那顆 MUX)與繞線延遲(連接它們的導線,在現代製程上佔比龐大且持續攀升)之間交替。當它抵達 `result_reg[7]/D` 時的累計總和,就是抵達時間:0.642 ns。
下半部構築截止時間。它從*下一個*時脈邊緣(2.000 ns——這是一條週期 2 ns 的單週期路徑)開始,先等時脈抵達*擷取*正反器(0.205 ns),再減去兩道安全餘裕:時脈不確定量(0.050 ns,涵蓋抖動與偏斜估計)以及正反器的程式庫 setup 時間(0.061 ns,資料必須在邊緣前保持安靜的窗口)。結果就是所需時間:2.094 ns。最後那道減法——2.094 − 0.642——得出 slack = +1.452 ns,標籤 `(MET)` 確認此檢查以充裕餘裕通過。
從報告到修正:收掉一條失敗的路徑
假設同一份報告回來時顯示的是 `slack (VIOLATED) -0.115` 而非通過。負 slack 並不神祕——它是你超支了 115 ps 的預算,而路徑報告本身就是那張逐項列出錢花到哪去的帳單。收掉它是一個有條理的循環,且永遠從最糟的路徑開始:
- 找出最糟的路徑。 把所有終點依 slack 排序,打開最負的那一條的報告。這就是你當前的關鍵路徑;其餘暫且不理。
- 讀懂帳單。 掃描資料路徑的 Incr 欄,找出最大的貢獻者。是某顆慢閘?一條高扇出的長導線?還是邏輯鏈太深(兩個正反器之間的閘級數過多)?
- 對症下藥。 慢閘 → 放大單元或改用更快的 Vt 種類。長導線 → 改善擺放或插入緩衝器。邏輯太深 → 重整邏輯或重新計時(移動暫存器),讓正反器之間的閘級數變少。
- 重跑 STA 並檢查新的 slack。 把 −0.115 變成 +0.010 的修正就收掉了這條路徑——但它可能拉長了某條鄰近路徑。新的最糟路徑就成了下一個目標。
- 反覆進行,直到所有路徑群組(以及所有製程角與模式)的最差 slack 都 ≥ 0 為止。這就是時序收斂。
還有一根槓桿完全位於資料路徑之外:[[timing-constraint|時序約束]]本身。報告中所需那一半的每個數字,都源自你寫下的約束——時脈週期、它的不確定量、輸入/輸出延遲。若某條路徑只差一根頭髮、而約束又過度悲觀(例如你重複計入了不確定量裡已含的餘裕),那麼收緊*約束*而非動*矽片*,會是所有解法中最便宜的。但絕不要為了讓報告變綠就放寬一個真實的約束:那只會藏起一個違規,而矽片會在全速運轉時樂於把它揭露出來。