DeepDataFlow
RISC-V 實戰 · 12 集

RISC-V 實戰:從 RTL 到簽核

把一個真實的 RISC-V 核心——帶指令快取與 4 個 SRAM macro 的 lowRISC Ibex——在 sky130 上從 RTL 做到簽核:Yosys 合成、OpenROAD 八個階段的佈局繞線、OpenRCX 提取寄生參數、OpenSTA 簽核,全部在 G2 裡完成。

發佈日期 2026-10-21 · 12 集 · DeepDataFlow
RISC-V 實戰:從 RTL 到簽核

同樣的 RTL、同樣的約束、兩套合成腳本:一個時序差了 16 ns,另一個滿足時序。這個系列就從這裡開始。我們把一個真實的 RISC-V 核心——開啟指令快取的 lowRISC Ibex——在 SkyWater 130 nm 上從 RTL 做到簽核,只用 G2 裡的開源工具:Yosys 做合成、OpenROAD 做佈局繞線、OpenRCX 提取寄生參數、OpenSTA 簽核。

這篇文章是 12 集影片的文字版。每一節包含該集影片、背後的決定,以及跑出來的數據。所有內容都來自 GitHub 上的 G2_demo_sky130 專案:把它 clone 到你的工作區,執行 make setup,專案就出現了。

設計ibex_top:開啟指令快取的 lowRISC Ibex,因此有 4 個 SRAM macro
製程SkyWater 130 nm(sky130 HD 標準單元)
工具Yosys、OpenROAD、OpenRCX、OpenSTA,全部在 G2 裡執行
模組850 × 850 µm、約 2,400 個正反器,加入 filler 後共 98,222 個實例(其中 70,818 個 filler)
結果hold 滿足(+0.17 ns);setup 只有暫存器到輸出沒過,8 個位址位元差 1.74 ns;107 個 DRC

第一部分:合成

#1Yosys 合成:從 RTL 到第一份網表

4:07

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

我們的模組是 ibex_top,也就是 lowRISC 的 Ibex RISC-V 處理器核心。它的 spec 綁定了 sky130 的標準元件庫和兩種記憶體 macro。開啟指令快取以後,Ibex 有兩路,每一路各有一個 tag RAM 和一個 data RAM,一共四個 SRAM macro,比只有標準單元的設計更接近真實的晶片。記憶體和其他元件庫一樣放在 G2 的元件庫管理器(LIM)裡,每個都有一個 LEF 和四個時序 corner,而且隨專案一起提供,用標準的 G2 安裝套件就能執行。

開啟指令快取的 `ibex_top`:2 路 ×(tag + data)= 4 個 SRAM macro。
開啟指令快取的 ibex_top:2 路 ×(tag + data)= 4 個 SRAM macro。

RTL 在 DDM 裡是 rtl v01。檔案清單的第一行 -GICache=1 設定了配置,上游的 RTL 本身沒有任何修改。剩下的由一個小小的 sky130 製程層完成:Ibex 需要一個通用的單埠 RAM,sky130 版本把每一種 RAM 尺寸映射到一個真實的 macro。時脈閘控使用元件庫裡真實的整合閘控時脈單元,而不是合成工具無法映射的鎖存器。所有 sky130 專用的檔案都放在一個資料夾裡,其他檔案和上游完全一致。

v01 用的是基礎腳本:先做通用合成,再以面積為目標映射到元件庫,完全沒有時序資訊。執行大約 20 秒:沒有未映射的單元,約 2,400 個暫存器,四個 macro。網表以 syn v01 簽入 DDM,之後任何一步都能按版本取用。

在做任何時序分析之前,先用 G2 的 netins 工具看看網表裡有什麼,結果報告了 1,800 個 macro。分類器不認識 sky130 的單元名稱,所以正反器、幾個四輸入門、記憶體和時脈閘控全被歸成了 macro。這不需要等 G2 出新版本。規則就放在一個純文字檔案 mapping.cfg 裡:一個模式、一個冒號、一個類別,第一個匹配的規則生效。

__a4 : Compound
__o4 : Compound
__df : Seq-FF
__sdlclkp : ICG
^fakeram : Memory

重新執行,網表就和預期一致了:2,395 個正反器、四個記憶體、一個時脈閘控。netins 還會根據它找到的埠寫出一份初始 SDC:一個時脈,以及每個埠上的預設延遲。這只是骨架,不是真正的約束。

#2時序驅動的合成,用數據做選擇

4:01

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

骨架變成真正的約束。時脈在慢速 corner 下是 40 ns。輸入和輸出各給週期的四分之一作為外部預算,驅動單元和負載讓邊界更真實,測試模式關閉,靜態配置引腳和重置不做時序分析。約束以 sdc_syn v01 放進 DDM;sdc_signoff_func v01 目前用同樣的約束,版圖完成後再出第二版。

問題來了:Yosys 不能針對 SDC 做最佳化。新版本能解析 SDC,但只是用來保留裡面引用到的物件。Yosys 有的是 ABC 的延遲目標 -D,以及一個只含驅動單元和負載的約束檔案。陷阱在這裡:我們只用 -D,得到的網表一個位元組都沒變。只有同時給 ABC -constr,它才會插 buffer、調整門的尺寸。

所以進階腳本直接從 SDC 讀數字:週期減掉 IO 預算、再減掉 uncertainty,給 ABC 的就是 29 ns。驅動單元和負載也來自同一個檔案,只有一個資料來源。它還打開了 Booth 乘法器。日誌確認 ABC 拿到了 29,000 ps 和 buf_2 驅動單元。

接著用 OpenSTA 評估兩份網表,同樣的 scenario、同樣的 SDC,只有網表不同。

各路徑組的最差 slack(理想時脈、沒有連線):基礎版 `v01` 除了輸入到輸出全部失敗;時序驅動的 `v02` 全部滿足。
各路徑組的最差 slack(理想時脈、沒有連線):基礎版 v01 除了輸入到輸出全部失敗;時序驅動的 v02 全部滿足。
v01 基礎v02 時序驅動
reg2reg−16.25 ns滿足(+6.65)
in2reg−12.17 ns滿足(+13.13)
reg2out−12.32 ns滿足(+6.67)
buffer 數01,494
標準單元16,63417,432(+4.8%)

基礎版為什麼失敗?它的最差路徑上,一個正反器透過最小尺寸的輸出驅動 259 個引腳:單一一級就花了將近 12 ns。這正是插 buffer 要解決的。多 5% 的單元換來 23 ns 的 slack:v02 交給 OpenROAD。這些是佈局前的數字,理想時脈、沒有連線,而且 v02 還有 slew 違規,佈局繞線會修好。

第二部分:OpenROAD,一集一個階段

#3把網表帶進 OpenROAD

3:58

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

佈局繞線需要自己的約束。第一輪的 sdc_apr_func v01 就是合成約束:40 ns,帶 uncertainty 的理想時脈,後面的階段再換成真實的時脈樹。在 G2 裡檢出 OpenROAD 會得到八個階段,從 init 到 finish,每個階段的 DDM 視圖都列出它的輸入。

init 腳本讀入製程和單元的 LEF(包括兩個 SRAM),從 MMMC 檔案按 corner 載入時序元件庫,讀入並連結網表,再讀入 SDC。這裡改了一處:我們的網表是層級式的,而 OpenROAD 的層級連結模式還標著開發中,所以我們用扁平方式連結。

link_design $blkname    ;# 原本是:link_design -hier $blkname

執行 20 秒,沒有錯誤,但要看警告:ORD-2011,兩個 SRAM 有 LEF 卻沒有 liberty。OpenROAD 能放置它們,卻不能對它們做時序分析。這個版本的生成器只列出標準元件庫(下個版本已修正),所以我們手動加上記憶體的元件庫,慢速和快速 corner 各一個,而且不要再按 Gen,否則會覆蓋這個檔案。之後每個階段都讀這個檔案,修一次就涵蓋整個流程。

修正之後,慢速 corner 下各路徑組的最差 slack 和第 2 集的 OpenSTA 完全一樣,reg2reg +6.652 ns:同樣的網表、同樣的 SDC、同樣的元件庫。四個 macro、17,400 個實例,以及合成加入的 1,800 個 buffer 和反相器。transition 和電容還有違規,placement 會修好。

#4Floorplan:die、SRAM macro 與電源網格

2:23

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

floorplan 腳本建立 die 和 core、鋪設繞線軌道、移除合成加入的 buffer(OpenROAD 會根據真實的佈局重新插入)、以 20 µm 的 halo 放置四個 SRAM、插入 tap cell,然後建立電源網格。

原本 die 和 core 一樣大,都是 850 µm,沒有留邊。這樣 SRAM 會緊貼 die 的邊緣,也就是 IO 引腳的位置:引腳壓在 macro 上,繞線時就會短路,電源環也會超出 die。我們留了 10 µm,core 是 830 µm。

floorplan:SRAM 放在左邊和右下角,離邊緣 10 µm;電源 strap 橫跨整個 core,每個 SRAM 上方另有一套網格。還沒有引腳。
floorplan:SRAM 放在左邊和右下角,離邊緣 10 µm;電源 strap 橫跨整個 core,每個 SRAM 上方另有一套網格。還沒有引腳。

時序是故意變紅的:移除合成的 1,494 個 buffer 之後 reg2reg 是 −14.7 ns,長線用很小的單元驅動大負載。這個階段加了 8,568 個 tap cell;buffer 和反相器從 1,800 個降到 334 個;利用率是 850 µm die 的 31%。

#5Placement 與設計修復

2:35

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

placement 先做一次忽略引腳的粗略全域佈局,把引腳放到它連接的邏輯旁邊,再考慮繞線擁塞重新佈局一次。接著估算連線,執行 repair_design,透過插 buffer 和調整尺寸修好所有 slew、電容和扇出違規,最後把每個單元合法化到行上。大約一分鐘。

repair_design 找到 710 個 slew 違規和 235 個電容違規,插入 1,258 個 buffer,調整了 673 個單元的尺寸。用真實的佈局連線,時序回來了:reg2reg +1.820 ns,reg2out 差 0.63 ns,post-CTS 會修好。transition 和電容都沒有違規了,利用率 34%。

單元密度:上方最熱的區塊是用正反器做的暫存器堆;SRAM 在邊緣。
單元密度:上方最熱的區塊是用正反器做的暫存器堆;SRAM 在邊緣。

擁塞估算在暫存器堆上方、以及左側 SRAM 旁邊的窄通道最熱。這個通道到繞線時還會再出現。

#6時脈樹合成:零錯誤,卻沒有時脈樹

2:34

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

CTS 把時脈反相器複製到負載旁邊,只用一種 buffer clkbuf_4 建樹,修正從時脈引腳到樹根的長線,再把新的 buffer 合法化。第一次執行報告沒有錯誤。但日誌說時脈連線的電阻和電容都是零,接著 CTS 以為時脈有 3,350 個 macro 負載,還跳過了幾百條複製出來的線,時脈閘控單元後面根本沒有建出時脈樹。

placement 階段設定過連線 RC,但這個設定不會存進資料庫,而 CTS 腳本又沒有重新設定。加三行、和 placement 用的一樣,就修好了:

source $fvar(layer_rc)
set_wire_rc -signal -layer $wire_rc_layer
set_wire_rc -clock -layer $wire_rc_layer_clk

現在 CTS 找到三個時脈網路:一個從時脈引腳驅動四個 SRAM 和時脈閘控單元;一個是沒有閘控的正反器;還有 core clock gate 後面的閘控時脈,幾乎包含了所有暫存器,放了 660 個 buffer。修正之前,這個時脈域根本沒有時脈樹,2,400 個正反器掛在一條沒有緩衝的線上,而這個階段照樣報告零錯誤。要看日誌,不能只看錯誤數。

#7Post-CTS 時序修復

2:17

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

CTS 之後,時序才是真的。時脈從理想改成傳播時脈,每個正反器看到的都是經過時脈樹的真實到達時間。接著 repair_timing 透過調整尺寸和插 buffer 修正 setup 與 hold,再對電源網格做一次靜態 IR drop 檢查。

用了真實時脈,有 17 個端點 setup 不過:時脈樹增加了大約 4 ns 的延遲,吃掉了輸出預算。repair_timing 用八個 buffer 全部修好,從 −5.2 ns 到 +0.109 ns,hold 也滿足。有一條 net 超出 slew 限值 160 ps;真正流片時,我們會在 CTS 之後再跑一次 repair_design。靜態 IR drop 最差 147 µV,約為 1.6 V 的 0.01%:對這個設計來說電源網格非常充裕。

#8全域與詳細繞線

2:01

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

繞線先把常數 tie net 標為特殊線讓繞線器跳過,為每個單元引腳找到合法的接入方式,在粗網格上規劃每條 net,最多再跑一百輪來消除擁塞,修復天線效應,然後用十輪詳細繞線畫出真正的金屬。它在背景執行大約 16 分鐘(執行時間卡片顯示一個半小時,那是八個執行緒加起來的時間)。

全域繞線結束時 overflow 為零,天線修復把 291 個違規降到零,詳細繞線從 8,985 個違規降到 127 個。

繞線使用率:暫存器堆上方最重,和 placement 的估算一致。沒有任何溢位。
繞線使用率:暫存器堆上方最重,和 placement 的估算一致。沒有任何溢位。

這個階段的時序表可以先跳過:route 的報告又回到理想時脈,不是真正的數字。簽核才是。

#9繞線後清理與 DRC

1:51

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

post-route 修復天線、重新計算引腳接入、再跑五輪詳細繞線和一個天線修復迴圈,最後用獨立的 DRC 檢查收尾。大約六分鐘,剩下 107 個違規,其中 62 個是短路。繞線器自己的計數忽高忽低,而且包含重複;DRC 卡片的數字來自最後那次獨立檢查。

幾乎全部——107 個裡有 105 個——都在同一個 SRAM,也就是指令快取的一個 data bank,以及緊鄰它的 net 上,位於 metal 2 和 3。這個 SRAM 的 140 個訊號引腳全在 metal 3 的同一條邊上,標準單元又緊緊貼著這條邊,繞線器根本沒有空間接進引腳。多跑幾輪繞線修不好,要改 floorplan。

#10Finish:完整的晶片,以及還剩下什麼

2:48

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

finish 用 filler 單元填滿行上的空隙,再檢查一次佈局合法性和 DRC,做 IR 分析,寫出 DEF、網表和資料庫。提取寄生參數的那幾行被註解掉了,所以這裡不產生 SPEF,交給 OpenRCX。結果是 98,222 個實例,其中 70,818 個是 filler;DRC 和 post-route 一樣是 107 個(finish 不會重新繞線);IR drop 最差約 0.11 mV。

完整的晶片,850 µm 見方:四個 SRAM 在邊緣,暫存器堆在上方,Ibex 核心繞線在中間。
完整的晶片,850 µm 見方:四個 SRAM 在邊緣,暫存器堆在上方,Ibex 核心繞線在中間。

從 init 到 finish,每個階段都在 G2 的同一個頁面上,各有日誌、報告、卡片和圖片,而且每個階段都簽入了 DDM。改了一個腳本,就從那個階段重跑,和這次的結果比較。

第三部分:簽核

#11用 OpenRCX 提取寄生參數

2:16

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

簽核時序需要真實的連線:每條已繞線 net 的電阻和電容。在 G2 的流程頁面裡,OpenRCX 就在 OpenROAD 旁邊。它的腳本讀入完成的資料庫(apr_finish v02),對專案要求的每個 corner——從低溫到高溫共四個——用 PDK spec 裡該 corner 的規則檔案提取並寫出一個 SPEF。

執行 14 秒,沒有錯誤:18,670 條 net、111,215 個電阻、206,593 個耦合電容。四個 SPEF 檔案大小相同,這很誠實:sky130 開放的規則檔案只有一個 RC 模型,所以每個 corner 得到的數字都一樣。量產用的 PDK 會為每個 corner 提供規則檔案,而流程本來就為每個 corner 產生一個 SPEF,所以只需要換檔案。SPEF 簽入和版圖相同的 DDM 版本,現在 apr_finish v02 帶著它的網表、DEF 和寄生參數。

#12用 OpenSTA 做繞線後簽核

5:03

在 YouTube 觀看(英文) ↗在 Bilibili 觀看(中文) ↗

第 2 集我們用 OpenSTA 評估網表時,是理想時脈、沒有連線。現在同樣的 OpenSTA 拿到真實的版圖。簽核 SDC 出第二版,有三處修改:傳播時脈,讓 OpenSTA 跟著真實的時脈樹走;更小的 uncertainty,setup 從 1 ns 降到 0.5、hold 從 0.2 降到 0.1,因為 skew 現在是測量出來的;去掉 clock transition,因為真實的 slew 來自時脈樹。

把 OpenSTA 指向 apr_finish v02 和它的 SPEF。執行之前還有一個修正:原本的腳本只檢查 setup,所以 hold 卡片永遠顯示零,不管通過與否。我們讓它跟著 scenario 走:max corner 查 setup,min corner 查 hold。然後用 Merge 彙總兩個 corner。

setup:reg2reg、in2reg 和 in2out 都通過。reg2out 在 8 個位址位元上差 1.74 ns。沿著時脈看:它從埠進來,經過兩個 buffer、時脈閘控,再經過四級時脈樹,在慢速 corner 下到達正反器時已經晚了 4.6 ns。可是輸出預算還是 10 ns,是從埠上的理想時脈算起的,這 4.6 ns 就直接從預算裡扣掉了。這要和負責 SoC 的人討論:用考慮時脈延遲的 output delay,或者把位址路徑做得更快。(915 個「SPEF 錯誤」是沒有負載的 net,五個缺少的元件庫單元是沒有時序的 filler 和 tap cell。)

hold 在快速 corner 全部通過,最緊的一對還有 0.17 ns 餘量。第 10 集繞線之後 hold 差了 0.83 ns,但那個數字來自估算的連線。用提取出來的連線,hold 是通過的。這就是為什麼簽核要用提取結果,而不是估算。還剩一個 slew 違規:2.2 ns,限值 1.5 ns。

我們做了什麼,還剩下什麼

從 RTL 到簽核,全部用開源工具、全部在 G2 裡。DDM 裡有整顆晶片:RTL、約束、兩個合成版本、每個佈局繞線階段、寄生參數,以及兩版簽核 SDC。每一步都從那裡取得輸入。

遺留問題,以及從哪裡著手。每一項都只要改一個腳本或一個 SDC,再從那一步重跑:

  • 輸出預算 −1.74 ns。要和 SoC 一起商定的約束:考慮時脈延遲的 output delay,或者更快的位址路徑。
  • 一個 slew 違規。在 CTS 之後再跑一次 repair_design。
  • 一個 SRAM 引腳邊上的 107 個 DRC。在 floorplan 裡給 macro 更寬的 halo,或者手動擺放。
  • route 階段的時序表用的是理想時脈。在讀入 SDC 之後設定傳播時脈。
  • 所有 corner 只有一個 RC 模型。量產 PDK 會解決。

觀看與繼續