DeepDataFlow
Earlgrey in G2 · Part 2 of 6

Flat hits a wall

A 24.7-hour placement that still wouldn't route, and what it took to harden nine blocks and use them as IP at the top level.

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

In part 1 we gave the whole arc: OpenTitan Earlgrey, about 1.13 million standard cells on SkyWater 130, taken from RTL to signoff timing with open-source tools, organized by G2, with Claude Code doing much of the hands-on work. This post covers the first big turn in that story: we tried the chip flat, it didn't work, and we switched to the way a chip team would build it, bottom-up.

Sizing the flat chip from data

All 40 blocks synthesized cleanly with Yosys and yosys-slang. Before placing anything we sized the die from the netlist: 1.13 million cells plus 23 memories need about 41 mm², so the flat chip got an 8.1 mm die. One setting had to change before the first run: the default cell padding alone doubled the placed area, so we reduced it.

In G2 the flat chip is version v1, an OpenROAD checkout with one directory per stage (init, floorplan, place, CTS, route and so on), each with its log, reports, a QoR summary and a README.

2:19

Watch on YouTube ↗Watch on Bilibili ↗

24.7 hours, almost all of it in one step

Initialization took 7 minutes and the floorplan 23. Placement took 24 hours and 43 minutes.

About 22.5 hours of that was one step: repair_design, which fixes slew, capacitance and fanout by resizing cells and adding buffers. It walks every driver in the chip on a single thread. Near the end it took almost an hour per row of its progress table. In total it added about 76,000 buffers.

The result was not usable:

  • Congestion everywhere. The estimated routing congestion map was red across the whole standard-cell area, not just in a few hot spots.
  • −164 ns of setup slack on reg2reg paths at the slow corner, with the worst path in otbn, the big-number accelerator.
  • Cells moved up to 2.8 mm by detailed placement to find a legal spot.
  • One error in the log: the power-grid check found VDD shorted to two pins of a memory macro.

Different symptoms, one cause: too much logic in one place. More effort on the same flat floorplan wouldn't change that.

Flat chip v1 after 24.7 hours of placement. Left: estimated routing congestion (red is over capacity). Right: the placement itself.

Rules from a day-long run

A run this long costs a day every time it fails, and it taught us some rules we kept for the rest of the project:

  • No timeouts on long EDA runs. An earlier attempt was killed by a four-hour timeout that we had set ourselves. It produced no output and taught us nothing.
  • Save a checkpoint after global placement, so a later failure doesn't start from zero.
  • Ask the tools for progress output, so a slow step can be told apart from a hung one.
  • Set the thread count explicitly in every tool.

Going hierarchical

We chose nine blocks to harden on their own: otbn, the Ibex CPU core, kmac, aes, entropy_src, the flash and RRAM controllers, the RRAM array and i2c, which the chip uses three times. That makes 11 instances. Almost 700,000 cells leave the top level and about 440,000 stay.

The flat chip stayed as v1, the record of what happened. Nothing was overwritten. The hierarchical chip became v2.

Hardening a block in G2

Every block follows the same steps: a new version, an OpenROAD checkout, and the stages one by one, each in its own directory. After placement, a global-route check runs in minutes and tells us whether the block can route at all, before hours of clock tree synthesis and detailed routing.

2:29

Watch on YouTube ↗Watch on Bilibili ↗

otbn was the hardest block, and it went through five versions:

  • v2: even at 10 MHz its wide datapaths missed timing by about 40 ns, so it got two-cycle paths in a new constraint version (SDC v03).
  • v3, 2.1 mm core: the global route needed 104% of the available tracks. We stopped it.
  • v4, 2.6 mm: 83.5%, still about 100,000 overflowing tracks.
  • v5, 3.0 mm: 50.6%, zero overflow. Routable.
otbn's versions. The block was sized by routing demand, not by cell area.
otbn's versions. The block was sized by routing demand, not by cell area.
Same netlist, more room. otbn v4 (left) and v5 (right).

Even v5 needed five tries to route. Each failure is in the run's README with the log that showed it and the fix: a repair step that never finished, a flag that leaked into later reroutes, and guides the detailed router couldn't connect.

The real fix was one number. The global router was planning with only 58% of the tracks, so it reported overflow that wasn't there and produced guides the detailed router couldn't use. With the layer derate set to 0.2, global routing showed zero overflow, and the detailed route went from 591,884 violations after the first pass to 1,988 after eleven. That route took six hours.

The setting that made otbn route: the global router's layer derate.
The setting that made otbn route: the global router's layer derate.

The other eight, and one clock change

The other eight blocks were easier: about 20 hours of place and route for all nine together. The figure below summarizes all nine. The negative slacks are block input/output budgets and small hold misses, not core logic failures. For example, flash_ctrl's −38.1 ns is an input-to-output path; its reg2reg slack is +26.8 ns.

All nine blocks at 10 MHz.
All nine blocks at 10 MHz.

Seven of the blocks were routed before we settled on a 10 MHz chip clock. In G2 the clock is part of a published constraint version, so a G2 stage re-timed the routed blocks at the new clock. Nothing was placed or routed again.

Turning a routed block into IP

A routed block becomes reusable IP through three views, all written by the finish stage:

  • An abstract LEF: the outline, the pins and the blocked areas.
  • A timing model at two corners (ss 1.60 V and ff 1.95 V, both at 100 °C).
  • A black-box Verilog stub with just the ports.

There was one trap. OpenROAD's abstract writer exported every power shape inside the block as pin geometry, without the vias that connect them. One level up, those pins would float. We kept only the top straps as power pins: the block owns metal 1 to 7, and the top level routes above it.

2:05

Watch on YouTube ↗Watch on Bilibili ↗

In G2 the views go into the library manager as hard IP, one version per block with a liberty file per corner, as with IP from a vendor. Eleven entries: the nine blocks plus two analog stubs from OpenTitan (ast and the USB differential receiver).

The hardened blocks in G2's library manager, next to the analog stubs.
The hardened blocks in G2's library manager, next to the analog stubs.

At the top level one line per block decides what happens to it. A synthesis row keeps the block as logic. A hardmacro row brings in the black box, and OpenROAD reads its layout and timing from the library. The main power domain has nine hard-macro rows; i2c is one row and three instances.

The bug where every report said “fine”

Once, all eleven macros disappeared and every report still looked clean. A helper that writes the flattened netlists dropped the black-box attribute, so synthesis flattened the macros away and reported zero unmapped cells, which normally means success.

We caught it by counting the macros in the netlist. Now the flow fails if a level that should have hard macros has none. The lesson: check for the hierarchy you expect, not only for the absence of errors.

“Unmap 0” looked like success. The macros had been flattened away.
“Unmap 0” looked like success. The macros had been flattened away.

The result

24.7 h → 2.8 hchip placement, flat v1 vs hierarchical v5
1.13M → 529Ktop-level instances, 11 of them hard macros
9 blocks, 11 instanceshardened, re-timed at 10 MHz and registered as IP

The new top level had 529,205 instances instead of 1.13 million, and it placed in a few hours. That solved the runtime. It did not yet solve routing: in the next part, the first top-level route check showed 1.22 million overflowing tracks, and we had to find out where they came from.