Plan — WS3 follow-up: re-add cosim --sdf via opensta-to-ir
Status: Deferred. Tracked here so future work can pick it up.
Predecessor: WS3 phase 3.4 (deletes hand-rolled src/sdf_parser.rs).
Background
Phase 3.4 deleted src/sdf_parser.rs. The jacquard sim subcommand kept
SDF input working (Phase 3.3 wired --sdf through
setup::load_sdf_via_opensta_to_ir, an internal subprocess wrapper that
calls the opensta-to-ir crate to convert SDF→IR). The jacquard cosim
subcommand chose Option B of the phase 3.4 handoff: drop --sdf
entirely rather than thread --liberty through. As a result, cosim now
only accepts pre-converted IR via --timing-ir.
What was removed in 3.4
CosimArgs::sdf,sdf_corner,sdf_debugCLI fields (src/bin/jacquard.rs).- The
config.timing.sdf_file/sdf_cornerfallback path insrc/sim/cosim_metal.rs::run_cosim. TimingSimConfig::sdf_fileandsdf_cornerJSON fields (src/testbench.rs).
User-facing migration (current state)
The tests/mcu_soc/ cosim flow that used to load SDF via the testbench
config now needs an explicit pre-conversion step.
Feed 6_final.v directly to opensta-to-ir
Retraction (2026-05-18). Earlier versions of this section
recommended feeding tests/mcu_soc/data/top_synth.v (post-synthesis,
pre-P&R) to opensta-to-ir to dodge a parse error on 6_final.v's
chipflow integration wrapper. That was wrong: top_synth.v is
missing the ~236K cells P&R inserts (clkbuf_regs_* CTS buffers,
ANTENNA_* diodes, delaybuf_*, fillers), so OpenSTA silently drops
every SDF entry referencing a P&R-inserted cell and the resulting IR
is missing the bulk of the design's timing. The "28162 matched /
2090 unmatched" verification log we celebrated at the time measured
jtir records against the cosim-loaded netlist, not SDF coverage
against the jtir — high surface match rate, materially incomplete
IR. See ADR 0009 (OpenSTA Verilog reader input constraints) for
the broader rule.
opensta-to-ir now transparently extracts module <--top> from each
input file before invoking OpenSTA (implementation in
crates/opensta-to-ir/src/verilog_filter.rs). For the chipflow
mcu_soc case this strips the openframe_project_wrapper module
automatically; the same handling kicks in for any LibreLane +
wafer.space user (hazard3 and future tapeouts) whose final netlist
carries an integration wrapper around the structural top.
# Convert SDF → IR once. Pass 6_final.v directly; the wrapper module
# is dropped automatically.
opensta-to-ir \
--liberty /path/to/sky130_fd_sc_hd__tt_025C_1v80.lib \
--verilog tests/mcu_soc/data/6_final.v \
--sdf tests/mcu_soc/data/6_final.sdf \
--top top \
--output tests/mcu_soc/data/6_final.jtir
# Run cosim with the pre-converted IR. Cosim loads 6_final.v (the
# wrapper) because that's what carries GPIO ports. The IR consumer's
# hierarchy-prefix detection strips the `top_inst/` prefix from the
# wrapper's cell paths so they match the IR's instance names.
cargo run -r --features metal --bin jacquard -- cosim \
tests/mcu_soc/data/6_final.v \
--config tests/mcu_soc/sim_config_sky130.json \
--top-module openframe_project_wrapper \
--timing-ir tests/mcu_soc/data/6_final.jtir
tests/mcu_soc/sim_config_sky130.json no longer carries sdf_file /
sdf_corner (the fields would be silently ignored if added back;
cosim does not consume them).
Events-reference comparison: nuances
tests/mcu_soc/events_reference.json was wired into the sky130 cosim
config as part of phase 3.4 verification. End-to-end pipeline result
on a 3M-tick run:
- 67 UART bytes captured; the reference's 155 UART events end at
timestamp 4,187,182. All 67 captured payloads match the reference's
leading bytes (decoded UART output:
....: nyaa~!\nSoC type: CA7F100F\nFlash ID: CA7CA7FF\nQuad mode). No payload divergence. - 15 non-UART entries in the reference (cxxrtl-emitted SPI
deselectevents withpayload: "") are filtered out at parse time by the tolerant deserializer incosim_metal.rs::run_cosim. Without that filter the comparison panicked on the first SPI entry.
chipflow's num_steps and timestamp are edge-counted
Retraction. Earlier drafts of this section claimed Jacquard's
--max-cycles counts half-cycles. That was a misdiagnosis based on
reading MultiClockScheduler::new (which does emit per-edge raw
entries) without noticing the pairing layer at
src/sim/cosim_metal.rs:2604-2675 that collapses them into one
paired buffer per cycle. Today, --max-cycles N correctly counts
N full clock cycles: each cosim tick does one fall-edge dispatch
plus one rise-edge dispatch and DFFs capture once per tick. Verified
via --stimulus-vcd trace (5 ticks → simulated time spans 0–200000
ps for a 40 ns period clock, exactly 5 cycles).
The actual unit difference vs chipflow's cxxrtl harness:
- chipflow's
num_stepsis the count oftick()calls; eachtick()bumps++timestamptwice (once after the negedge dispatch, once after the posedge), so theevents_reference.jsontimestampfield counts clock edges (a full cycle = posedge-to-posedge = 2 edges). The harness:
Seeauto tick = [&]() { {{interface}}.step(timestamp); top.clk.set(false); agent.step(); ++timestamp; // post-negedge (odd) top.clk.set(true); agent.step(); ++timestamp; // post-posedge (even) }; for (int i = 0; i < num_steps; i++) tick();chipflow-lib/chipflow/common/sim/main.cc.jinja:32-74. - The half-tick timestamp is an intentional design, not a bug: parity tags each event with the clock phase it fired on (useful for verification of async paths).
- chipflow's
num_stepstherefore doubles as an edge budget: 3 M num_steps = 3 M edges = 1.5 M full clock cycles.
To compare a Jacquard cosim run against today's events_reference.json,
divide reference timestamps by 2 to convert edges → cycles. Empirical
spot-check on mcu_soc/sky130: byte-0 in Jacquard at --max-cycles 200000 arrives at tick 28682; reference timestamp 58290 / 2 = 29145
cycles; ratio 0.984× (simulators agree on simulated time within 2%).
The earlier "67 of 155 events captured" gap is not a budget
issue — chipflow drives input stimulus via design/tests/input.json
and reference events 69+ require those driven inputs. The input-stimulus
dispatcher was added in commit 4a1a989, and the mcu_soc/sky130 cosim
now matches the cut-down chipflow reference 1:1 (90/90 events).
The earlier "Jacquard ~14% slower per byte than cxxrtl" claim relied on a phantom half-cycle correction; it is also retracted. There is no rate gap to explain at this level.
Done: --max-cycles renamed to --max-clock-edges (commit 46b5c28)
Cosim's internal granularity moved from full clock cycles to scheduler
edges, aligning Jacquard's CLI 1:1 with chipflow's num_steps and
unlocking per-edge event timestamping. Section retained for context on
the unit conventions captured above.
Option A — restore cosim --sdf ergonomics
When this becomes a priority, mirror the jacquard sim surface:
Changes
- Add
--libertytoCosimArgs(src/bin/jacquard.rs). Plumb it throughDesignArgs::liberty(currently hardcodedNoneincmd_cosim). Also passthrough--top-moduleif not already. - Add
--sdf,--sdf-corner,--sdf-debugback toCosimArgs. Make them mutually exclusive with--timing-ir(clapconflicts_with = "timing_ir"). - Re-add
TimingSimConfig::sdf_file/sdf_corner(optional) — plus a newliberty_filefield for the OpenSTA invocation. Updatetests/mcu_soc/sim_config_sky130.jsonto use the new shape. - Restore the cosim config-file fallback: in
src/sim/cosim_metal.rs::run_cosim, when timing is not yet enabled and the config provides SDF + Liberty paths, callsetup::load_sdf_via_opensta_to_ir. Match the priority order: CLI > config.timing.* > nothing. - Update
--output-vcderror message to mention--sdfagain.
Out of scope for Option A
- Rebuilding a hand-rolled SDF parser. (See ADR 0006 — the durable replacement is the native Rust SDF→IR converter, tracked separately as Phase 3 in the original phasing.)
- Adding cosim-specific corner-selection beyond what
jacquard simalready offers. The IR'smin/typ/maxtriple is selected viair_corner0_max(currently alwaysmax); changing that is a separate concern that affects both subcommands.
Verification
After Option A lands:
cargo build --features metal
cargo test --lib
# Manual smoke test of the previous mcu_soc workflow:
cargo run -r --features metal --bin jacquard -- cosim \
tests/mcu_soc/data/6_final.v \
--config tests/mcu_soc/sim_config_sky130.json \
--liberty <path>/sky130.lib \
--sdf tests/mcu_soc/data/6_final.sdf
Should produce equivalent results to the pre-3.4 hand-rolled-parser
path within the IR's representational bounds (single-value
interconnect delays, max corner selection).
Walk-back
If Option A is never picked up before first release, the existing IR-only
cosim surface is fine — contributors using SDF can pre-convert via
opensta-to-ir and pass the resulting .jtir. The follow-up exists as a
contributor-ergonomics improvement, not a correctness gap.