絡時序優(yōu)化:從XDC約束到RTL寄存器復制的完整實戰(zhàn))
做FPGA設計久了你會發(fā)現(xiàn)一個特別有意思的現(xiàn)象同一段RTL不同的人做的約束策略不一樣出來的時序結(jié)果可能天差地別。有一回我排查一個圖像縮放工程的關鍵路徑查來查去最后發(fā)現(xiàn)罪魁禍首就是一個扇出fanout超過500的使能信號。當時我就在Vivado里寫了一條set_property MAX_FANOUT的XDC約束但綜合、實現(xiàn)跑完時序沒改善多少問題根本沒解決。后來才搞清楚扇出約束這事兒遠不是寫一行約束那么簡單它牽扯到綜合策略、實現(xiàn)階段的物理優(yōu)化、代碼里寄存器的組織方式甚至復位信號的專門處理。這篇文章就把我從XDC約束寫到代碼優(yōu)化的完整排查經(jīng)驗梳理一遍把扇出約束這個專題講透。1. 扇出到底指什么一個信號驅(qū)動到底有多難1.1 先搞清楚工具里的Fanout是怎么統(tǒng)計的扇出的字面意思很直白就是一個寄存器輸出端信號所驅(qū)動的負載單元數(shù)量。但FPGA里負載不完全等于LUT端口它還包括DSP、BRAM、進位鏈、甚至IOB等資源。比如一個寄存器輸出同時接到了20個LUT的輸入、5個DSP的CE端口、還有8個觸發(fā)器復位端那這個網(wǎng)絡的扇出就是33。在Vivado里看扇出最直接的方式是打開綜合后的原理圖Schematic選中任意一根網(wǎng)絡屬性面板里就會顯示Fanout值或者用Tcl命令查詢get_property FANOUT [get_nets valid_global]不過要注意綜合階段的扇出統(tǒng)計和布局布線后的扇出統(tǒng)計不完全一致。綜合工具看到的邏輯連接關系是理想化的而布局布線后由于物理優(yōu)化phys_opt_design可能復制寄存器、重映射邏輯網(wǎng)絡的扇出會有變化。所以排查時序問題時應該以實現(xiàn)后的報告為準尤其是report_design_analysis里列出的高扇出網(wǎng)絡High Fanout Nets。1.2 高扇出為什么會讓時序爆炸扇出高為什么會影響時序我用一句話概括驅(qū)動一個負載和驅(qū)動五百個負載信號翻轉(zhuǎn)時需要充放電的電容差了兩個數(shù)量級延遲自然就上去了。放到FPGA內(nèi)部看這個過程會更具體。一個寄存器的輸出Q要送到下一級寄存器的D端口信號從Q端出來后會先走一段可控的布線資源經(jīng)過可編程開關矩陣Switch Matrix中轉(zhuǎn)再逐級扇出到各個LUT的輸入。扇出越高需要經(jīng)過的開關節(jié)點越多RC寄生效應越明顯網(wǎng)絡延遲Net Delay就會變大。這個Net Delay在時序報告里體現(xiàn)得很直觀——路徑延遲從幾百皮秒漲到幾納秒配合組合邏輯延遲直接吃掉整個時鐘周期的預算。另外高扇出還會帶來一個隱蔽問題叫偏移Skew。信號驅(qū)動多個負載時由于各負載的布線路徑長度不一致信號到達各自負載的時間會有差異。對數(shù)據(jù)信號來說這個Skew會讓后一級的建立時間裕量變差對復位信號來說Skew會讓不同寄存器的復位釋放時刻不一致嚴重時直接導致功能錯誤。我個人的經(jīng)驗閾值是這樣的普通數(shù)據(jù)信號扇出超過100就該留心使能類信號、復位信號、模式配置信號只要上200就建議主動干預。Vivado默認把扇出超過1000的網(wǎng)絡定義成High-Fanout Net但如果真想等項目跑到那一步再去優(yōu)化時序早就飛了。2. XDC扇出約束的正確寫法set_property MAX_FANOUT的語法與生效范圍2.1 約束語法和作用對象XDC扇出約束的核心命令就一條語法非常簡潔set_property MAX_FANOUT 50 [get_nets valid_global]這里MAX_FANOUT指定的數(shù)值是工具允許保留的最大扇出數(shù)等于告訴綜合器和實現(xiàn)工具這個網(wǎng)絡驅(qū)動超過50個負載時你就要想辦法復制源端寄存器把負載拆開。這個屬性的作用對象比較靈活可以是網(wǎng)絡net、單元引腳cell pin、甚至是頂層端口port。日常用得最多的是網(wǎng)絡和引腳。比如約束某個寄存器輸出引腳的扇出set_property MAX_FANOUT 20 [get_pins u_ctrl_sync/en_ff_reg/C]注意這里的引腳指的是驅(qū)動管的輸出引腳不是負載端的輸入引腳。如果手滑選成了負載端的輸入引腳這條約束基本不會生效因為工具復制寄存器時關心的是源端驅(qū)動能力。2.2 綜合階段和實現(xiàn)階段的約束行為差異很多人以為寫了XDC約束綜合和實現(xiàn)都會自動遵守。但實際Vivado處理MAX_FANOUT的方式有階段差異綜合階段Vivado綜合器Vivado Synthesis會把MAX_FANOUT當作邏輯復制的參考依據(jù)。綜合時遇到高扇出網(wǎng)絡如果設置了MAX_FANOUT工具會自動復制源端寄存器生成多份邏輯相同的寄存器來分擔負載。實現(xiàn)階段布局布線前Vivado會對整個設計做時序預估。這里的物理優(yōu)化phys_opt_design也會根據(jù)MAX_FANOUT做進一步的寄存器復制或合并。但物理優(yōu)化更關注實際布局的擁塞程度和時序瓶頸它可能會把綜合階段復制出來的寄存器重新合并——如果它判斷復制反而會導致布局更擁擠、路徑更長。布局布線后布線階段基本不再做寄存器復制。這個階段再大量調(diào)整邏輯結(jié)構(gòu)已經(jīng)不現(xiàn)實了所以扇出約束必須在布局前完成布局。這個階段差異直接影響了你排查問題的思路。比如綜合后打開原理圖扇出已經(jīng)變小了但實現(xiàn)完一查又變回去了那大概率是物理優(yōu)化重新合并了這些復制寄存器的結(jié)果。2.3 一個典型的約束配置案例假設工程里有一個時鐘使能信號ce_mult驅(qū)動了96個乘法器的CE端口時序報告顯示這條路徑是瓶頸。我的常見做法是分兩步走第一步先在XDC里添加MAX_FANOUT約束set_property MAX_FANOUT 32 [get_nets ce_mult]第二步在綜合設置里確認沒有強制關閉高扇出優(yōu)化。如果是用綜合策略Synthesis Strategy默認的Flow_PerfOptimized_high高扇出復制默認開啟如果用了某些追求面積的策略工具可能會優(yōu)先省寄存器而不主動復制。接著跑綜合看綜合網(wǎng)表里該網(wǎng)絡的扇出是否降到了32以內(nèi)。如果降下來了說明綜合階段約束生效如果沒降需要進入下一章的排查流程。3. 寫了約束卻沒效果的常見原因約束失效的完整排查鏈路3.1 排查鏈路第一步確認約束有沒有作用到目標對象上我自己踩過的一個坑是在XDC里寫了set_property MAX_FANOUT 50 [get_nets valid_global]但綜合后打開原理圖一看valid_global網(wǎng)絡還是三百多負載。排查第一步就是檢查get_nets到底選中了什么對象。綜合器在處理RTL時會給很多中間信號自動生成新名字原始RTL里的信號名可能被加了后綴比如valid_global_reg_0_0。這個時候你用get_nets valid_global可能選中的是一個中間層次的名字而驅(qū)動負載的實際網(wǎng)絡名已經(jīng)變了。解決辦法是用通配符set_property MAX_FANOUT 50 [get_nets -hierarchical *valid_global*]-hierarchical選項會遞歸尋找所有層級的網(wǎng)絡通配符*能匹配中間生成的名稱。加了這個選項之后大概率能選中真實的目標網(wǎng)絡。3.2 排查鏈路第二步區(qū)分綜合階段和實現(xiàn)階段的布局行為還有一種情況綜合階段約束確實生效了原理圖里網(wǎng)絡扇出變成了50以內(nèi)但實現(xiàn)完查看最終布線結(jié)果扇出又漲回去了。原因就是我在第2章提到的物理優(yōu)化重新合并了寄存器。這時候你需要查實現(xiàn)日志搜索關鍵詞replicating register或者register duplication。Vivado的phys_opt_design在判斷某個寄存器復制后對時序無益或讓布線擁塞惡化時會撤銷綜合階段的復制。常見觸發(fā)條件是復制出的寄存器分散太遠導致源端到各副本的路徑變長或者布局資源緊張副本沒有合適位置放置。面對這種情況我的建議是不要只依賴MAX_FANOUT約束改用下一章的代碼優(yōu)化手段把寄存器復制落實到RTL層面。3.3 排查鏈路第三步檢查OOC模塊綜合的XDC加載情況這條經(jīng)驗主要針對使用Out-Of-ContextOOC方式綜合子模塊的工程。OOC模式單獨綜合子模塊時主工程的XDC約束默認是不加載進來的。這意味著你在頂層XDC里寫的MAX_FANOUT約束對OOC子模塊內(nèi)部網(wǎng)絡根本不生效。如果子模塊內(nèi)部確實有高扇出網(wǎng)絡需要在子模塊的XDC文件里單獨設置約束或者在綜合設置中將該模塊的XDC文件加入。Vivado的OOC綜合有一個選項叫-include_optimization或者在子模塊的約束文件里直接加同一條MAX_FANOUT屬性。這一步非常容易漏我見過不少團隊因為OOC子模塊的扇出約束缺失導致綜合結(jié)果和全工程實現(xiàn)結(jié)果不一致。排查這條鏈路時一個直接的手段是在綜合后打開子模塊原理圖檢查目標網(wǎng)絡的扇出。如果約束沒加載就該去子模塊的XDC里補上。4. 代碼層面的扇出優(yōu)化手段從源頭拆解高扇出網(wǎng)絡4.1 手動復制寄存器最直接的拆分方式很多人把扇出優(yōu)化的希望全寄托在工具的自動復制上但工具畢竟是工具它不會理解設計的功能意圖復制的時機、位置也不一定理想。到項目后期遇到反復無法收斂的扇出問題我的做法往往是回到RTL手動復制寄存器??匆粋€例子原始代碼里有一個全局使能信號always (posedge clk) begin if (~rst_n) begin valid_ff 1b0; end else begin valid_ff valid_src; end end // valid_ff 輸出驅(qū)動了200多個運算單元的使能端口要把它拆成4份用generate循環(huán)可以少寫很多重復代碼(* KEEP TRUE *) reg [3:0] valid_ff; genvar i; generate for (i 0; i 4; i i 1) begin : valid_copy_gen always (posedge clk) begin if (~rst_n) begin valid_ff[i] 1b0; end else begin valid_ff[i] valid_src; end end end endgenerate復制出來后每個valid_ff[i]只驅(qū)動四分之一負載扇出直接從200降到50左右。需要注意兩點第一(* KEEP TRUE *)屬性告訴綜合器保留這組寄存器不要因為邏輯等價又把它合并回去。我見過不寫這個屬性綜合器自作主張把4份寄存器合并成1份扇出問題原樣復現(xiàn)。第二復制出的多份寄存器之間會存在時鐘Skew但對使能類控制信號來說幾百皮秒的偏差通常不影響功能。如果是對時序敏感的跨時鐘域信號不建議這樣簡單復制需要另行設計。手動復制寄存器在物理上還有額外好處你可以在RTL里就有意識地規(guī)劃寄存器擺放位置。比如最好把4份寄存器分散在運算陣列的不同象限這樣到各自負載的布線會比較短。工具復制的時候不一定能完美做到這一點。4.2 復位信號的扇出處理一套專門的打法復位信號是FPGA里最容易出現(xiàn)超高扇出的網(wǎng)絡沒有之一。芯片上電后一個全局復位要同時驅(qū)動幾萬個觸發(fā)器這種扇出用普通的MAX_FANOUT約束根本處理不了——你不可能讓工具復制幾十萬份復位寄存器那會讓資源爆炸。處理復位信號的頭號選擇是用全局時鐘資源。Vivado里全局復位信號通常會建議走BUFG網(wǎng)絡或者直接映射到專用復位引腳這樣可以借助全局布線網(wǎng)絡的強驅(qū)動能力大幅降低復位網(wǎng)絡的延遲。XDC里的寫法通常是set_property MAX_FANOUT 200000 [get_nets rst_n]注意這里是故意給一個非常大的值意思是不希望綜合器對這個復位網(wǎng)絡做復制讓它走全局資源。因為全局復位信號的負載本來就有幾萬、十幾萬如果約束值設得太小綜合器可能嘗試復制一堆寄存器結(jié)果布局資源被浪費時序反而更差。對于局部復位情況又不同。一個模塊內(nèi)部的模塊級復位信號扇出一般在上千左右。這種場景我建議做一個復位同步器把全局復位轉(zhuǎn)換為兩級同步后的局部復位再進模塊。復位同步器的標準結(jié)構(gòu)是reg [1:0] rst_sync; always (posedge clk) begin if (~rst_async_n) begin rst_sync 2b00; end else begin rst_sync {rst_sync[0], 1b1}; end end assign rst_sync_n rst_sync[1];這個同步器既解決了異步復位的亞穩(wěn)態(tài)問題又給內(nèi)部邏輯提供了一個干凈的同步復位信號。配合上模塊的局部使用扇出范圍還是可以控制的。關于復位信號與扇出還有個常見困惑既然復位不能高扇出那能不能干脆不做全局復位全靠上電初始值我的建議是功能模塊可以依賴初始值但控制通路里的狀態(tài)機、FIFO指針、握手信號這些地方還是老老實實上復位信號不然仿真和上電后行為會很難一致。4.3 邏輯重構(gòu)與使能鏈路的優(yōu)化思路除了寄存器復制和復位處理代碼層面的邏輯重構(gòu)也是降低扇出的有力手段。高扇出信號不一定非得被直接復制如果能在邏輯上把廣播變成局部共享效果會更好。最常見的一種重構(gòu)是拆分條件表達式。假設有一個模式配置信號mode_cfg它的每一位都被幾十個模塊共同用來控制運算模式。如果每個模塊都去讀取mode_cfg扇出必然很高。一種做法是在每個子模塊的入口處把mode_cfg打一拍存成局部寄存器always (posedge clk) begin if (mode_valid) begin mode_cfg_local mode_cfg; end end這樣一來頂層的mode_cfg只需要驅(qū)動每個子模塊入口的一兩個寄存器而不是每一路組合邏輯。子模塊內(nèi)部使用打拍后的mode_cfg_local扇出就被限制在局部范圍內(nèi)。這個手法的本質(zhì)是讓配置信息的傳播路徑結(jié)構(gòu)化而不是讓它像洪水一樣漫到整個設計里。另一種重構(gòu)思路針對多路選擇器。當一個選擇信號控制幾十個MUX時扇出同樣爆炸。優(yōu)化方向是把這種一刀切的選擇信號改成先局部寄存再逐級傳遞的形式。比如在通道化數(shù)據(jù)鏈路中可以把使能信號與數(shù)據(jù)流同步移位讓每一級處理單元不再共享同一個長距離使能信號而是使用靠近自己的延遲后的使能副本。雖然代價是多了一些移位寄存器但在時序收斂面前這點資源開銷完全值得。還有一個細節(jié)值得留意扇出優(yōu)化時不要腦子一熱把負載數(shù)壓得太低。有些工程師上來就寫MAX_FANOUT 10結(jié)果綜合器復制了上百份寄存器布局資源、布線資源全線告急時序不升反降。經(jīng)驗上把扇出控制在30到50之間是比較穩(wěn)妥的區(qū)間除非你的布局結(jié)構(gòu)天然支撐更低的扇出。5. 一個圖像處理工程的扇出修復實戰(zhàn)從時序違例到收斂5.1 問題現(xiàn)場一個300負載的使能信號去年做一個圖像縮放加速模塊時鐘跑到200MHz綜合和實現(xiàn)都能過但時序收斂不干凈。Vivado時序報告里WNS最差負裕量是-0.42ns關鍵是這個負裕量對應的路徑重復出現(xiàn)在好幾條路徑上顯示的都是同一個網(wǎng)絡valid_global。打開report_design_analysis找到高扇出網(wǎng)絡列表valid_global赫然掛著300多個負載。它的驅(qū)動源是圖像處理流水線里的數(shù)據(jù)有效信號這個信號同時控制整條流水線200多個乘加單元的CE端口外加若干狀態(tài)機的跳變條件。路徑上組合邏輯本身不長但Net Delay因為扇出太大被拉到了1.8ns加上源端寄存器的Clk-to-Q和目的端的建立時間一個周期下來剛好差了口氣。5.2 第一次嘗試XDC約束為什么沒有達到預期我先按照常規(guī)思路在頂層XDC里加了約束set_property MAX_FANOUT 50 [get_nets valid_global]跑完綜合打開網(wǎng)表原理圖檢查扇出確實降到了50左右綜合器復制出了6份valid信號寄存器。但繼續(xù)跑布局布線實現(xiàn)后的時序報告顯示W(wǎng)NS仍然是負的雖然不是-0.42ns那么差但也只到了-0.2ns左右。再次實現(xiàn)后檢查網(wǎng)絡扇出發(fā)現(xiàn)綜合階段復制出的6份寄存器的布局情況很不理想——其中3份被擺到了離負載很遠的地方布線的繞線增加了凈延遲的改善遠不如預期。我觀察到實現(xiàn)日志里還出現(xiàn)了merge register的記錄意味著物理優(yōu)化認為部分復制不劃算又把它們合并了。這其實暴露了純靠XDC約束的短板工具自動復制但它不會站在功能結(jié)構(gòu)的角度做最優(yōu)布局。它復制的邏輯可能覆蓋到了不合理的層級也可能因為資源擺放導致副本間的布線互相干擾。5.3 第二次嘗試RTL手動復制與結(jié)構(gòu)重構(gòu)被這次失敗刺激之后我決定回到RTL做手動復制徹底控制寄存器的數(shù)量和位置。具體動作分三步第一步把RTL里唯一的valid_ff拆成4份用generate循環(huán)生成并用(* KEEP TRUE *)屬性鎖住。4份正好對應流水線運算陣列的四個象限。第二步在RTL里用(* DONT_TOUCH TRUE *)或者(* KEEP_HIERARCHY TRUE *)把復制邏輯和負載邏輯包在同一個子模塊里防止綜合器做跨層優(yōu)化時又把它們合并。第三步寫了一條placement約束把這4份寄存器手工鎖定到四個象限的空白區(qū)域。Vivado允許用set_property LOC或Pblock來做位置約束我這里用Pblock把4份寄存器分別圈住create_pblock valid_copy_0 add_cells_to_pblock [get_pblocks valid_copy_0] [get_cells {u_scale_core/valid_copy_gen[0].valid_ff_reg[*]}] resize_pblock [get_pblocks valid_copy_0] -add {SLICE_X20Y40 SLICE_X30Y70}雖然寫Pblock需要一定后端經(jīng)驗但這一步是讓扇出優(yōu)化真正落實到物理層面的關鍵。改完代碼、加上位置約束后重新跑綜合和實現(xiàn)結(jié)果如下表優(yōu)化階段扇出數(shù)WNSNet Delay (關鍵路徑)初始狀態(tài)320-0.42ns1.82ns僅XDC約束50綜合后/ 128實現(xiàn)后-0.21ns1.46nsRTL復制位置約束320.08ns0.94ns扇出從320降到32關鍵路徑的Net Delay從1.82ns降到0.94nsWNS由負轉(zhuǎn)正。整體實現(xiàn)后的資源占用率只增加了約1.5%完全可接受。5.4 修復后的復盤與經(jīng)驗沉淀這個項目結(jié)束之后我總結(jié)出了幾條處理扇出問題的核心原則先看路徑構(gòu)成再動手。如果時序報告里Net Delay只占很小的比例組合邏輯Delay是主導那扇出不是瓶頸不必費勁去優(yōu)化。如果Net Delay占了一半以上且對應網(wǎng)絡負載數(shù)量巨大再考慮扇出優(yōu)化。工具自動復制是備選而不是首選。XDC的MAX_FANOUT可以快速試水但效果不穩(wěn)定因為它不理解設計的布局意圖。對關鍵的時序路徑手動RTL復制配合位置約束才是可控的手段。要防綜合器好心辦壞事。邏輯等價寄存器的大規(guī)模復制是綜合器的合法優(yōu)化但對我們來說往往是噪聲。關鍵信號靠KEEP、DONT_TOUCH鎖住能讓綜合器的自由度縮小到可控范圍歸檔報告也更容易解釋。復位信號另外對待。全局復位不要設小的MAX_FANOUT值走全局時鐘資源才是正解局部復位置務必同步不要圖省事從全局復位直接拉線。我現(xiàn)在做新項目時已經(jīng)把扇出檢查前置到了RTL Review階段。綜合跑完第一版第一件事就是用report_design_analysis批量看高扇出網(wǎng)絡列表超過200扇出的網(wǎng)絡全部過一遍確定是主動放任還是動手優(yōu)化。提前干預一小時比項目后期反復跑實現(xiàn)省一整天這個賬非常劃算。最后再分享一個小技巧在Vivado里選中一個高扇出網(wǎng)絡按F9可以高亮所有負載這時圖形界面會非常直觀地展示這些負載在芯片上的分散程度。如果負載密密麻麻擠成一片布局問題不大如果負載散布在芯片四角那無論怎么復制寄存器布線長度都很難壓下來這時候要優(yōu)先考慮邏輯重構(gòu)把信號傳播路徑拉近而不是純粹靠復制解決問題。