DeepDataFlow
Earlgrey in G2 · Part 4 of 6

Signoff in a separate tool

Timing the finished chip from the files we would ship exposed two OpenROAD bugs that only appear in a hierarchical flow. We fixed both, sent them upstream, and then checked our own work.

Published 2026-10-22 · Part 4 of 6 · DeepDataFlow

At the end of part 3 chip v5 was routed and finished, with 15,056 routing violations written down as known leftovers. The next step was signoff timing. We did it the way industry does it, and that choice turned out to be what found the bugs.

Signoff reads what you ship

A place-and-route tool can report timing from its own database, and OpenROAD can do that. But what goes to a foundry, or to the next level of a hierarchy, is files: the netlist and the parasitics (SPEF), written out by the place-and-route tool. Signoff should read those files, in a separate tool. So the project rule was: signoff stays in a separate tool. OpenRCX extracts the parasitics, OpenROAD writes the netlist and the SPEF, and standalone OpenSTA reads them.

Timing inside the place-and-route tool reads its database. Signoff reads the written files.
Timing inside the place-and-route tool reads its database. Signoff reads the written files.

Extraction needs rules for every metal layer, and sky130 has five. On our 10-metal stand-in stack, OpenRCX crashed with the stock rules instead of reporting an error. So we generated rules where the upper layers copy the lower ones (metal 6–8 as metal 4, metal 9–10 as metal 5) and made four corners from them, with cworst and cbest at ±10%. These rules are fake, and we say so wherever the numbers appear.

In G2, extraction is a stage of its own. It reads the finished design, writes one SPEF per corner and checks the files into the design data manager next to the netlist. The OpenSTA scenarios pull both from there.

2:38

Watch on YouTube ↗Watch on Bilibili ↗

Bug 1: 98,251 drivers without parasitics

The first standalone OpenSTA run warned that 98,251 drivers had no parasitics, along with 48,926 "instance not found" and 431,493 "net not found" messages. About 77,000 of those drivers have no routed wire at all, which is expected. The other 21,000 were real wires, and mostly the whole clock tree.

Yet OpenROAD reading its own database with the same SPEF matched all but 490 names. The file was fine for the tool that wrote it, and broken for any other tool.

The wrong first theory

Claude Code's first answer was that the clock-tree tool named its buffers wrong. That was a plausible story, and it had traced one buffer to support it. The engineer asked a different question: the same file writes the synthesized nets of that module correctly, with their hierarchical path. What does the writer get right there that it gets wrong here?

That question pointed at the writer, not at CTS. Objects that a tool creates inside a sub-module are stored as a name plus the module they belong to. The netlist writer reads the module. write_spef didn't, so every buffer CTS created inside the hierarchy was written without its path.

The question that found the cause: compare with what the same writer gets right.
The question that found the cause: compare with what the same writer gets right.

We fixed write_spef in OpenROAD, added a regression test, and ran the existing hierarchy tests: all 60 were unchanged. On the chip, "instance not found" went from 48,926 to 0, and "net not found" from 431,493 to 164. The whole investigation is written up, including the wrong first theory, with the numbers before and after.

Why didn't this show up before? Inside OpenROAD the database is flat and complete, so names always resolve. Only reading the written files exposes the bug. That is the main reason to keep signoff separate. A second rule came out of it too: fix the data instead of timing around it.

Bug 2: dummy loads in the wrong module

One gap was left, in the clock tree's leaf nets. A dummy load is a small cell the clock-tree tool adds to balance a branch. In our chip, every clock buffer sat inside the top_earlgrey module, but CTS created its 14,348 dummy loads in the chip's top module, with no port between the two.

Inside OpenROAD the database is flat, so each one is still one net. But the netlist writer writes two nets with the same name: one with the driver and no wire, and one with the wire and no driver. 13,566 clock nets were split this way, and 206.7 pF of the 619 pF of clock wire capacitance (33%) landed on the wrong half.

1:57

Watch on YouTube ↗Watch on Bilibili ↗

A dummy load in the wrong module splits one clock net into two in the written netlist.
A dummy load in the wrong module splits one clock net into two in the written netlist.

The fix in CTS: create each dummy load in the module of the buffer it loads. The new test showed something else: one of OpenROAD's existing reference outputs (gated_clock4) already contained the same split, with 18 dummy loads in the top module. A golden file can hold a bug. The other 53 CTS tests were unchanged.

Re-running the chip from CTS would have taken more than a day. Instead, a five-minute script moved the 13,553 dummy loads in the finished v5 database into the right modules, and we re-ran extraction and timing.

24,531 → 13,531 → 8clock buffers without parasitics: before, after the SPEF fix, after the CTS fix
142,733 → 112"not connected" warnings in OpenSTA
98,251 → 73,824drivers without parasitics, most of them with no wire at all

Going upstream

Both bugs affect anyone who runs a hierarchical flow with OpenROAD, so we sent them upstream:

  • Each bug reproduced in a few cells, in OpenROAD's own test style, and checked on current upstream first.
  • Whole test suites compared against their reference outputs: rcx, cts and dbSta passed 142 of 142, and rsz 294 of 295, the same as before the change.
  • One branch per fix.
  • Nothing posted until the engineer said yes.

The result is two issues and two pull requests: issues #11839 and #11840, and pull requests #11841 and #11842. As of 2026-10-22 they are open and waiting for review.

Then we checked our own work

After an OpenROAD upgrade in the G2 tool bundle, smaller sandbox chips showed two problems: a ground gap at the pad ring and clock nets without parasitics. Did Earlgrey have them? The checks went into a test version, v5a: two stages and no new place and route, recorded in the version table like any other run.

1:51

Watch on YouTube ↗Watch on Bilibili ↗

Power grid. The check on the floorplan found VDD open: 416 power-grid shapes and over a thousand pad supply pins were not connected. Our power-grid script connects to the pads without naming the pad layers. That is our own mistake, hidden since chip v2, and the fix is known for the next version.

The check itself had a problem. The open check took about a minute; then OpenROAD's short check ran for 9 hours and 20 minutes on one core without any output. We stopped it, kept the result it had already written and filed it as a bug. VSS was not checked, and that is on the list too.

Parasitics. A probe ran on the same netlist and SPEF. 73,824 drivers without parasitics sounds bad, but sorted by cause almost all of them have no wire (71,633) or no load (1,939), or are ports. Only 120 actually drive something: the real gap, and small.

The v5a test version in G2: purpose, result and next steps in its README, and the result in the version table.
The v5a test version in G2: purpose, result and next steps in its README, and the result in the version table.

One finding was bad news and one was good, and both are written where the next person will look: a line of notes in the version table, the README, the bug list and the resume page.

The signoff numbers, with caveats

With both fixes, the slow-corner (ss, cworst) top-level reg2reg setup slack is −7.87 ns at 10 MHz, and no register clock pin is unclocked. Input/output paths still have no timing exceptions, so their numbers aren't meaningful yet, and there is no hold report. Those, and the VDD fix, are in the plan for chip v6.

Next: how the engineer and Claude Code split the work, including the night when the engineer was asleep.