在第 1 篇里,我们讲了整个过程:OpenTitan Earlgrey,在 SkyWater 130 上大约 113 万个标准单元,用开源工具从 RTL 做到签核时序,由 G2 组织,Claude Code 完成大部分动手的工作。这一篇讲这个故事的第一个大转折:我们先试了扁平的芯片,行不通,于是改成芯片团队会用的做法,由下而上。
按数据决定扁平芯片的大小
全部 40 个模块都用 Yosys 和 yosys-slang 干净地完成综合。布局之前,我们先按网表决定 die 的大小:113 万个单元加 23 块存储器大约需要 41 mm²,所以扁平芯片用了 8.1 mm 的 die。第一次运行之前要改一个设定:光是默认的单元间距就让布局面积翻倍,所以我们把它调小。
在 G2 里,扁平芯片是版本 v1,一次 OpenROAD 签出,每个阶段一个目录(init、floorplan、place、CTS、route 等等),每个目录都有自己的日志、报告、QoR 摘要和 README。
2:19在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
24.7 小时,几乎全花在一个步骤上
初始化花了 7 分钟,floorplan 花了 23 分钟。布局花了 24 小时 43 分钟。
其中大约 22.5 小时是同一个步骤:repair_design,它用调整单元大小和插入 buffer 的方式修正 slew、电容和扇出。它要走过芯片里的每一个驱动端,而且只用单线程。到最后,它的进度表要将近一小时才前进一行。总共插入了大约 76,000 个 buffer。
结果没办法用:
- 到处都拥挤。 估计的绕线拥挤度图在整片标准单元区域都是红的,不只是几个热点。
- 慢速 corner 的 reg2reg 路径有 −164 ns 的 setup slack,最差路径在大数加速器 otbn 里。
- 详细布局把单元移动了最多 2.8 mm 才找到合法的位置。
- 日志里有一个错误: 电源网络检查发现 VDD 和一个存储器 macro 的两个 pin 短路。
症状不同,原因一样:太多逻辑挤在同一个地方。在同一个扁平 floorplan 上再多花力气,也改变不了这一点。


跑一整天学到的规则
跑这么久的运行,每失败一次就是一天,它也让我们学到几条规则,在项目的其余部分一直遵守:
- 长时间的 EDA 运行不设 timeout。 更早的一次尝试被我们自己设的四小时 timeout 杀掉,什么输出都没有,也什么都没学到。
- 全局布局之后存一个 checkpoint, 后面失败时就不用从零开始。
- 要求工具输出进度, 才分得出一个步骤是慢还是卡住了。
- 在每个工具里明确设定线程数。
改成层次化
我们选了九个模块单独硬化:otbn、Ibex CPU 核心、kmac、aes、entropy_src、flash 和 RRAM 控制器、RRAM 阵列,以及芯片用了三次的 i2c。加起来是 11 个实例。将近 700,000 个单元离开顶层,大约 440,000 个留下。
扁平芯片留作 v1,记录发生过的事。没有任何东西被覆盖。层次化的芯片是 v2。
在 G2 里硬化一个模块
每个模块都走同样的步骤:一个新版本、一次 OpenROAD 签出,然后一个阶段接一个阶段,每个阶段都在自己的目录里。布局之后,全局绕线检查几分钟就能告诉我们这个模块到底绕不绕得通,再决定要不要花好几个小时做时钟树综合和详细绕线。
2:32在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
otbn 是最难的模块,一共走了五个版本:
- v2: 即使在 10 MHz,它很宽的数据通路还是差了大约 40 ns 的时序,所以在新的约束版本(SDC v03)里加了两周期路径。
- v3,2.1 mm core: 全局绕线需要 104% 的可用走线。我们把它停掉。
- v4,2.6 mm: 83.5%,还有大约 100,000 条溢出的走线。
- v5,3.0 mm: 50.6%,溢出为零。可以绕通。



即使是 v5,也试了五次才绕通。每一次失败都写在这次运行的 README 里,附上显示问题的日志和修正:一个永远跑不完的修复步骤、一个泄漏到后续重绕的参数,以及详细绕线器连不上的 guide。
真正的解法只是一个数字。全局绕线器原本只用 58% 的走线来规划,所以报告了其实不存在的溢出,做出详细绕线器用不了的 guide。把层的降额设成 0.2 之后,全局绕线的溢出变成零,详细绕线也从第一轮之后的 591,884 个违例,降到十一轮之后的 1,988 个。这次绕线花了六个小时。

另外八个模块,以及一次时钟变更
另外八个模块比较容易:九个模块的布局布线加起来大约 20 小时。下图汇总了九个模块的结果。负的 slack 是模块的输入输出预算和很小的 hold 差额,不是核心逻辑的失败。例如 flash_ctrl 的 −38.1 ns 是一条输入到输出的路径;它的 reg2reg slack 是 +26.8 ns。

其中七个模块,是在我们决定芯片时钟用 10 MHz 之前就绕好的。在 G2 里,时钟是发布的约束版本的一部分,所以由一个 G2 阶段用新的时钟重新分析这些绕好的模块的时序。没有任何东西需要重新布局或绕线。
把绕好的模块变成 IP
绕好的模块要变成可复用的 IP,靠三个视图,全部由 finish 阶段写出:
- 抽象 LEF: 外框、pin 和被阻挡的区域。
- 时序模型,两个 corner(ss 1.60 V 和 ff 1.95 V,都是 100 °C)。
- black box 的 Verilog stub,只有端口。
有一个坑。OpenROAD 的抽象 LEF 写出程序把模块里每一个电源图形都当作 pin 几何导出,却没有连接它们的 via。到了上一层,这些 pin 会是浮空的。我们只保留最上层的电源条作为电源 pin:模块使用 metal 1 到 7,顶层在它上方绕线。
2:04在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
在 G2 里,这些视图放进库管理,成为硬 IP:每个模块一个版本,每个 corner 一个 liberty 文件,就像供应商提供的 IP 一样。一共十一个条目:九个模块,加上 OpenTitan 的两个模拟 stub(ast 和 USB 差分接收器)。

在顶层,每个模块一行,决定它要怎么处理。综合的那一行让模块保持成逻辑。hardmacro 那一行带进 black box,OpenROAD 再从库里读它的版图和时序。主电源域有九行硬 macro;i2c 是一行、三个实例。
每份报告都说「没问题」的 bug
有一次,十一个 macro 全部消失了,而每一份报告看起来都还很干净。一个写扁平网表的辅助程序把 black box 属性弄丢了,所以综合把这些 macro 全部展平掉,报告的却是未映射单元为零,而这通常代表成功。
我们是靠数网表里的 macro 才发现的。现在只要某一层应该有硬 macro 却一个都没有,流程就会失败。教训是:要检查你期望的层次结构,而不只是检查有没有错误。

结果
新的顶层有 529,205 个实例,而不是 113 万个,布局只要几个小时。运行时间的问题解决了。绕线的问题还没有:在下一篇里,顶层第一次绕线检查显示有 122 万条溢出的走线,我们得找出它们从哪里来。