模块不是芯片。在上一个系列 RISC-V 实战:从 RTL 到签核 里,我们把一个 RISC-V 核心——lowRISC 的 Ibex——当作单一模块,从 RTL 做到签核。这个系列,我们围绕这个核心做出一颗芯片:加上存储器和外设、一个 PLL、真正的 IO pad,核心本身则作为硬核 macro 放进去。然后在 G2 里完成整颗芯片的综合、布局布线与签核。
这篇文章是 13 集视频的文字版。每一节包含该集视频、背后的决定,以及跑出来的数据。所有文件都在 G2_demo_sky130 项目里。
| 设计 | lowRISC Ibex Demo System:总线、RAM、GPIO、UART、SPI、PWM、定时器、JTAG 调试 |
| 工艺 | SkyWater 130 nm(sky130 HD 标准单元) |
| 工具 | Yosys、OpenROAD、OpenRCX、OpenSTA,全部在 G2 里运行 |
| 芯片 | 1.8 × 1.8 mm die、58 个 pad、6 个 macro(核心、4 个 RAM、PLL),加入 filler 后共 115,854 个实例 |
| 结果 | hold 满足;慢速 corner 的 setup 差 1.445 ns,即 24.1 MHz,目标是 25 MHz |
第一部分:芯片的组成
#1从模块到 SoC
2:07在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
这颗 SoC 是 lowRISC 的 Ibex Demo System:总线、RAM、GPIO、UART、SPI、PWM、定时器和 JTAG 调试模块,围绕一个 Ibex 核心。它原本是为 FPGA 写的。其中有两样会改:u_top 是核心,u_ram 是 128 KiB 的双端口存储器。
在 G2 里,SoC 是一个新模块 ibex_demo_system。名字用下划线,因为 G2 把模块名当作综合和布局布线的顶层模块名,而 Verilog 的名字不能有减号。一次点击就建好 spec 页面,以及这个模块在实现、脚本和设计数据管理器(DDM)里的目录。层级上,SoC 是第 0 层,系列一做好的核心是它里面的第 1 层。
第一个 spec 栏位是速度:25 MHz。核心在慢速 corner 签核到 25.56 MHz,SoC 不可能比它的核心更快。标准单元栏位会打开模块的 stdcell.cfg,流程读的就是这个文件,综合时会再用到这一点。
#2双端口 RAM macro
2:23在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
SoC 的 RAM 是 128 KiB、双端口:一个端口给数据,一个给取指。在 FPGA 上那是 block RAM;在芯片上它是一个存储器 macro,而我们的库里只有核心指令缓存用的单端口 RAM。
我们做了两个决定。第一,16 KiB 对这个 demo 的软件已经足够,128 KiB 在 sky130 上会非常大。第二,生成器的双端口 RAM 没有字节写掩码,所以 32 位存储器由四个 8 位宽的 macro 组成,一个字节通道一个。
FakeRAM2.0 为一个并不存在的存储器生成时序和版图视图,非常适合做流程。同样的 32 Kbit 可以有完全不同的形状:

一定要看生成的模型。这个模型先寄存地址再读阵列,数据晚两个周期才出来。liberty 和我们的 RAM wrapper 都说是一个周期,所以换成一个周期的模型。liberty 只有一个典型 corner,项目有四个 corner,所以我们写四份,表头放对应的电压和温度;每份的时序都一样,和系列一的 RAM 做法相同。
在 G2 的库管理器(LIM)里,操作顺序很重要:先 Find、再 Build all、最后 Fill。Fill 靠建好的页面把每个 liberty 对应到 corner,先跑 Fill 就什么都对不上。结果是两个端口,各有 12 位地址、8 位数据,时钟到数据 0.218 ns。
#3让 SoC 配合硬核
2:04在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
SoC 的 RTL 以 rtl v01 放进 DDM,从 demo 仓库复制,原仓库保持不动。问题来了:我们在系列一硬化的核心是 CHERIoT 版本的 Ibex,端口和这颗 SoC 对不上。它多了 SoC 完全不认识的 CHERIoT 和 lockstep 端口,而 SoC 还传了做好的 macro 无法接受的参数。
我们保留 macro,改 SoC:
- 关掉 CHERIoT,让核心跑成普通的 32 位 RISC-V。
- 多出来的输入接常数,多出来的输出悬空。
- 拿掉参数,SoC 得到的就是我们硬化时的配置。
- RAM 缩小到 16 KiB,每一处修改都加了标记,方便以后找。
RAM wrapper 把 32 位存储器对应到四个字节宽的 macro,每个字节的写使能就是它的掩码位。综合之前,先做一次快速的 elaboration 检查,核心和 RAM 都当黑盒。少了一个选项,slang 就会停下来:调试模块在声明之前就用了一个信号。仿真器放过它,slang 不放。加上选项后链接成功:一个核心、四个 RAM,端口名称和宽度都和真正的 macro 吻合。
#4真实芯片的时钟
2:19在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
在 FPGA 板上,时钟从一个引脚进来。真正的芯片自己产生时钟:一个 10 MHz 的参考时钟、一个把它乘十的 PLL、一个降到 25 MHz 给核心用的除频器,还有一个 bypass,PLL 不工作时芯片照样能跑。

sky130 没有我们能用的 PLL,所以像假 RAM 一样做一个假 PLL:有看起来真实的版图和时序视图,里面没有电路。其中一个选择是刻意的:参考时钟到输出没有时序弧。PLL 的输出是一个新的时钟,约束会说明这一点。
时钟产生器 clkgen 是 rtl v02 里需要小心的部分。一个两位计数器对 PLL 时钟除频,最高位就是 25 MHz 时钟。每个时钟有自己的时钟门控,每个门控的使能在自己的时钟域里同步,而且要等另一个关掉才会打开。复位之后先跑参考时钟,让 PLL 锁定。最后由复位同步器让复位干净地释放。
时钟变慢,SPI host 就出问题:25 MHz 除以 12.5 MHz 的 SPI 时钟,toggle 计数只剩一,计数器变成零位。我们保持原来的比例,系统时钟的四分之一。还有三个要说清楚的地方:bypass 时 UART 的波特率不对;bypass 引脚只能在复位时改变;这个 PLL 只是替身。
#5真实的 pad
2:41在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
芯片通过 pad 和电路板沟通。skywater-pdk 里 sky130 的 IO 库有版图和 Verilog,却没有时序,没有时序就不能综合,也不能签核。open_pdks 会建出完整的一套,包括每一颗 Caravel 芯片都在用的 Efabless 包装 pad,每个 corner 都有时序。LibreLane 的 pad 设定告诉我们,一个验证过的 sky130 pad ring 用哪些单元:通用 IO pad、带 ESD clamp 的核心电源与 IO 电源 pad、一个 corner 单元,以及填充 slice。
一个小安装脚本把五个单元拷进 G2 的 PDK,每个单元都有我们四个 corner 的 liberty。在 LIM 里每个单元一个条目,因为一个条目每个 corner 只能放一个 liberty。二十个 liberty 文件全部对上。(这里发现 LIM 的两个小 bug:Fill 会跳过符号链接,以及页面标题错误。)
rtl v03 的芯片顶层保留 SoC 的端口名称,现在都是 bond pad:50 个信号 pad、8 个电源 pad。pad wrapper 的高压使能来自 pad 自己的 tie-high 和 tie-low 输出,和 Caravel 的做法一样。顺便处理一件事:三个 128 字节、用 flip-flop 做的 FIFO 几乎占了整颗芯片三分之二的 flip-flop,每个都缩成 16 个 entry。

第二部分:把核心变成硬核 macro
#6让模块成为合格的 macro
2:50在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
硬核模块有一条规则:顶层金属归上一层。 macro 的 abstract 视图会挡住模块用到的每一层金属。系列一的核心把 metal 5 用在电源网格和绕线上,所以整个 macro 在 metal 5 上都被封住了。芯片在 metal 5 上的电源 strap 到它边缘就停了,我们练习时,芯片层级的电源检查就报告核心的电源引脚没有连接。核心会一直没有电。
解决办法是一个惯例:模块只用到 metal 4,芯片直接从 metal 5 在模块的 metal 4 电源引脚上打 via。我们把核心重跑成 build v03,系列一的 build 保持不动,脚本有三处修改:
- 信号和时钟的绕线都只到 metal 4。
- 每个 stage 都读这些设定,不只是 floorplan。
- 电源网格去掉 metal 5 的 strap。floorplan 脚本现在也会找设计自己的电源脚本,以及芯片会用到的 pad ring 脚本。
绕线花了十四分钟,结果是 120 个违规,比之前少一点,还有我们要的那一行:metal 5 上的线长是 0 µm。post-route 和 finish 之后是 130 个违规,这是少一层金属的代价,对 demo 来说没问题。
#7从模块到硬核 macro
2:49在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
上一层设计需要硬核模块的三种视图:寄生参数、每个 corner 的时序模型,以及 abstract 版图。
- 寄生参数:OpenRCX 在一分钟内完成新版图四个 corner 的提取。
- 时序模型:OpenSTA 的
write_timing_model把整个核心写成一个单元:输入上的 setup 和 hold 检查、包含它自己时钟树的时钟到输出延迟,还有时钟树延迟本身。原本的脚本让每个 corner 的模型同名,corner 之间会冲突,所以我们用 corner 来命名。要注意:模型里的面积是单元面积,不是 macro 的占地面积。 - abstract LEF:绕线器当作电源的 tie net 要改回信号,不然会变成空的电源引脚。abstract 挡住模块用到的每一层,电源引脚只保留 metal 4。原始的 abstract 列出几千个内部电源形状,却没有把它们连起来的 via,到了上一层看起来就是浮空的。
结果是一个 850 × 850 µm 的 macro,930 个引脚,电源在 metal 4,metal 5 上什么都没有。各视图也一致:LEF、网表和时序模型都是 928 个信号位。核心以硬核 IP 放进 LIM。两个 corner 对上了,就是我们签核过的那两个;另外两个没有时序模型,不会用到。
第三部分:约束与综合
#8为真实芯片写约束
2:14在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
综合约束 sdc_syn v01 由一个数字驱动:系统时钟 40 ns。总共有四个时钟。参考时钟在它的引脚上。PLL 输出是定义在 PLL 输出引脚上的新时钟,这就是假 PLL 没有时序弧的原因。系统时钟从它生成,除以四:
create_clock -name clk_ref -period $ref_period [get_ports clk_ref_i]
create_clock -name clk_pll -period $pll_period [get_pins u_core/u_clkgen/u_pll/clk_o]
create_generated_clock -name clk_sys -divide_by 4 \
-source [get_pins u_core/u_clkgen/u_pll/clk_o] \
[get_pins -of_objects [get_nets u_core/u_clkgen/clk_div4] -filter "direction == output"]
create_clock -name clk_tck -period $tck_period [get_ports tck_i]
综合会改 flop 的名字,从 RTL 复制来的 flop 名称在网表里不会存在。net 会保留名字,所以我们找驱动除频时钟 net 的那个引脚。JTAG 是自己的时钟;三组时钟彼此异步,因为 mux 的控制信号经过同步器。
功能模式下 PLL 是开的,而只对 bypass 引脚做 case analysis 是不够的。bypass 时钟门控的使能来自一个 flop,不是常数,所以参考时钟还是会漏到每个系统 flop 上:4,599 个引脚有两个时钟。关掉 bypass 门控的时钟弧之后,每个 flop 就只有一个时钟:
set_case_analysis 0 [get_ports pll_bypass_i]
set_disable_timing -from CLK -to GCLK [get_cells u_core/u_clkgen/u_icg_ref/icg]
现在每个端口都是 bond pad,是 inout,所以时序分析把每个 pad 同时看成输入和输出。pad 不用的方向给零延迟并排除,就不会意外留下没有约束的路径。输出 pad 驱动 5 pF 的板级负载。
#9综合整颗芯片
2:19在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
芯片有一个新的实现 build:rtl v03 加上第 8 集的约束。G2 从 spec 收集库,每个 corner 9 个:标准单元、RAM、核心的时序模型、PLL 和五个 pad 单元。它们来自 spec 的库文件,也就是编辑器写的那些,所以前面要用编辑器。原本的 Yosys 脚本在 sky130 上用错误的名字统计 flop;系列一已经修好了,我们沿用它的脚本,包括时序驱动的那个。
运行约 40 秒:没有错误、没有未映射的单元,11,692 个实例、2,126 个触发器,每个 macro 和 pad 都以黑盒出现在网表里。接着让约束对上网表。有 22 个没有约束的端点,每一个都在预期之中:输入 pad 不用的那一侧,以及电源 pad。每个时钟都到达它该到的 flop。
布局前的时序要如实解读。系统时钟的 setup 是 −10.4 ns、复位 recovery 是 −21.4 ns,因为复位同步器完全没有 buffer 就驱动了 1,316 个 flop,slew 是 40.9 ns。综合不会帮这条 net 加 buffer,placement 会。把这一条 net 排除,所有时钟都满足时序。
第四部分:芯片的布局布线
#10pad ring 与芯片电源
2:35在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
布局布线的约束和综合的一样,只改一处:板级信号用 transition 驱动输入 pad,不用标准单元,因为没有标准单元能驱动 pad。die 是 1.8 mm 见方,core 是 1.3 mm。58 个 pad 每边至少需要 1.6 mm,所以决定尺寸的是 pad,不是逻辑。
pad ring 脚本在 die 四周放 IO site,然后依序、等距放每一边的 pad,再加上 corner、填充 slice、abutment,以及每个端口的 bond pad 端子。有一个顺序很关键:先命名 pad 电源,再做 abutment。否则 abutment 会自己产生 ring net,电源 pad 永远接不到核心电源,之后也没有任何步骤能修正。
# 先连接 pad 电源(add_global_connection ...),然后:
global_connect
connect_by_abutment
place_io_terminals {*/PAD */VCCD_PAD */VSSD_PAD */VDDIO_PAD */VSSIO_PAD}
电源网格加上 pad 电源、接到电源 pad 的 core ring,以及延伸到 ring 的 stripe。少了 -extend_to_core_ring,stripe 会停在 ring 之前,整个核心网格都是浮空的。这两个问题都是我们练习时吃过亏才发现的。
add_pdn_stripe -grid {grid} -layer {met5} -width {1.600} -pitch {27.200} -offset {13.600} -extend_to_core_ring
add_pdn_ring -grid {grid} -layers {met4 met5} -widths {10.0 10.0} -spacings {2.0 2.0} \
-core_offsets {10.0 10.0} -connect_to_pads

这就是第 6 集的回报:32 条 metal 5 电源 strap 跨过核心,900 个 via 打到它的 metal 4 电源引脚上。
#11Placement、CTS 与 post-CTS
2:05在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
placement 之后电源检查通过:电源上的每个形状都连上了。练习时,直到网格接到 ring、核心空出 metal 5 之前,这个检查一直失败,所以前面要先做那些修正。
CTS 建了十棵时钟树。系统时钟从无毛刺 mux 出来,到达九个 macro 时钟引脚和 1,892 个 flop;JTAG 有自己的时钟树。
接着是层级设计的部分。核心的时序模型说,它自己的时钟树在 macro 里面又加了五纳秒以上:到引脚 3.8 ns,里面再加 5.6 ns。核心里的 flop 比 RAM 晚很多才看到时钟,结果在核心的输入端出现 −3.3 ns 的 hold 违规。post-CTS 修复插入 317 个 hold buffer,修好所有 hold 违规,包括核心的。
剩下一个 setup 违规,在除频器。除频器自己的输出现在是一条时钟树 net,它经过这棵树以 100 MHz 反馈回自己的计数器。真正的设计会从另一个 flop 反馈;我们记下来,继续往前。
#12绕线与完成
0:54在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
整颗芯片绕线大约十分钟,结果只有一个违规,线长 1.07 m、106,115 个 via。单独的核心有 120 个:芯片的 core 有空间,而且 macro 上方的 metal 5 对芯片是开放的。post-route 清理和 finish(加入 filler,写出最终网表和版图)之后剩 2 个违规,没有短路。finish 打印的时序还是理想时钟,真正的数字要到签核才有。

第五部分:签核
#13芯片签核
2:56在 YouTube 观看(英文) ↗在 Bilibili 观看(中文) ↗
签核从真实连线开始:OpenRCX 用 18 秒提取四个 corner。签核约束使用传播时钟,让真实的时钟树算进去,uncertainty 也更小,因为 skew 已经不用猜了。
有一节只有带真实 pad 的芯片才需要。pad 的时序取决于它的驱动模式。我们的 RTL 把那些引脚接成常数,但经过的是没有驱动的 net,时序分析看不到它们的值,就假设最慢的模式。没有 pad 模式时,输出 pad 要 120 ns,芯片差了 102 ns;加上之后只要 6.9 ns,还有 12.2 ns 的余量。
接着用 OpenSTA 分析整颗芯片:核心是它的时序模型,每个 pad、RAM 和 PLL 都来自 LIM。合并后的结果:

最差路径正是这个系列的一课。时钟在 7.8 ns 到达核心。核心的时序模型说,它的数据地址在 27.9 ns 之后才出来:它自己的时钟树加上它的逻辑。这样 SoC 只剩大约 12 ns 给总线和调试存储器,而它需要 13.5 ns。用模块搭芯片时,每个模块的输出时序都是芯片必须接受的预算。芯片跑在约 24.1 MHz,而不是 25 MHz。
我们做了什么,还剩下什么
从系列一的一个核心,到一颗芯片:双端口 RAM、带安全 bypass 的 PLL、58 个真实 pad、为芯片留出空间的硬核 macro、综合、布局布线与签核。全部在 G2 里完成,每一次交接都经过设计数据管理器和库管理器。
遗留问题,以及从哪里着手:
- 核心边界 setup −1.45 ns。这是核心 clock-to-output 的预算;下一步是更快的核心,或在总线边界加寄存器。
- 输出 pad 的 slew。pad 的 transition 表不区分模式,这是库建模的限制。
- PLL 支路上偏小的时钟门控(max capacitance)。
- 100 MHz 时钟域里除频器的反馈。
- 两个 DRC 违规,而且这个系列不做 DRC、LVS 或 GDS 签核。
观看与动手
- 视频:Bilibili 合集(中文)与 YouTube 播放列表(英文)。
- 本系列使用的核心:RISC-V 实战:从 RTL 到签核。
- 下一个系列:RISC-V 倒装芯片实战:从焊盘到凸点,拿掉 pad ring,把这颗 wirebond 芯片和倒装版本做比较。
- 项目:G2_demo_sky130。G2 本身有免费的社区版。
- 关于 G2 或自己跑流程的问题:support@deepdataflow.com。授权与合作:sales@deepdataflow.com。
