Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 141 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,128 @@ jobs:
name: benchmark-results
path: benchmark_results.txt

# Pre-generate the MCU SoC timing IR (.jtir) on Linux so the macOS
# Metal runner — which doesn't have sky130 Liberty or OpenSTA — can
# consume it via `cosim --timing-ir`. Cosim does not accept raw SDF
# (see `docs/plans/post-phase-0-roadmap.md` — adding a subprocess
# wrapper is tracked there as a follow-up).
prepare-mcu-soc-jtir:
name: Prepare MCU SoC timing IR
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
with:
submodules: false

- name: Init vendor/opensta submodule
run: git submodule update --init vendor/opensta

- name: Resolve OpenSTA submodule SHA
id: opensta-sha
run: |
sha=$(git ls-tree HEAD vendor/opensta | awk '{print $3}')
echo "sha=$sha" >> "$GITHUB_OUTPUT"

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Install uv
uses: astral-sh/setup-uv@v4

- name: Install OpenSTA build deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake bison flex libfl-dev swig \
tcl-dev tcl-tclreadline libeigen3-dev libfmt-dev wget

- name: Cache CUDD
id: cache-cudd
uses: actions/cache@v4
with:
path: ~/cudd-3.0.0
key: cudd-3.0.0-v1

- name: Build CUDD from source
if: steps.cache-cudd.outputs.cache-hit != 'true'
run: |
cd "$HOME"
wget -q https://raw.githubusercontent.com/davidkebo/cudd/main/cudd_versions/cudd-3.0.0.tar.gz
tar -xf cudd-3.0.0.tar.gz
cd cudd-3.0.0
./configure
make -j"$(nproc)"

# Shares the cache key with the opensta-to-ir-tests job so whichever
# runs first warms it for the other.
- name: Cache OpenSTA build
id: cache-opensta
uses: actions/cache@v4
with:
path: vendor/opensta/build
key: opensta-${{ steps.opensta-sha.outputs.sha }}-cudd-3.0.0-v1

- name: Build OpenSTA
if: steps.cache-opensta.outputs.cache-hit != 'true'
env:
CUDD_DIR: /home/runner/cudd-3.0.0
run: scripts/build-opensta.sh

- name: Cache cargo (opensta-to-ir)
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
crates/opensta-to-ir/target/
key: ${{ runner.os }}-cargo-opensta-to-ir-${{ hashFiles('crates/opensta-to-ir/Cargo.toml', 'crates/timing-ir/Cargo.toml') }}
restore-keys: |
${{ runner.os }}-cargo-opensta-to-ir-

# sky130 Liberty isn't checked in. Volare (pinned in pyproject.toml
# under [tool.jacquard.pdks.sky130]) fetches the open_pdks
# distribution. Cache `~/.volare` keyed on the pinned hash so the
# ~500MB download happens once and warm runs reuse it.
- name: Cache volare PDK (sky130A)
id: cache-volare
uses: actions/cache@v4
with:
path: ~/.volare
key: volare-sky130A-c6d73a35f524070e85faff4a6a9eef49553ebc2b

- name: Install sky130A PDK via volare
if: steps.cache-volare.outputs.cache-hit != 'true'
run: |
uv sync --group dev
uv run volare enable c6d73a35f524070e85faff4a6a9eef49553ebc2b

- name: Build opensta-to-ir
working-directory: crates/opensta-to-ir
run: cargo build --release --bin opensta-to-ir

- name: Generate MCU SoC timing IR
# opensta-to-ir transparently extracts `module <--top>` from
# each input file before invoking OpenSTA (see ADR 0009), so
# the chipflow `openframe_project_wrapper` in 6_final.v —
# which uses RTL operators OpenSTA's parser rejects — is
# filtered out automatically.
run: |
LIB=$HOME/.volare/volare/sky130/versions/c6d73a35f524070e85faff4a6a9eef49553ebc2b/sky130A/libs.ref/sky130_fd_sc_hd/lib/sky130_fd_sc_hd__tt_025C_1v80.lib
crates/opensta-to-ir/target/release/opensta-to-ir \
--liberty "$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

- name: Upload MCU SoC timing IR artifact
uses: actions/upload-artifact@v4
with:
name: mcu-soc-jtir
path: tests/mcu_soc/data/6_final.jtir

# Run GPU simulation on macOS with pre-built post-P&R netlist
mcu-soc-metal:
name: MCU SoC Metal Simulation
Expand All @@ -560,6 +682,7 @@ jobs:
# (mcu-soc-cvc, mcu-soc-comparison) gate on this via `needs:`.
runs-on: macos-runner-1
timeout-minutes: 30
needs: [prepare-mcu-soc-jtir]
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -620,10 +743,11 @@ jobs:
- name: Run GPU co-simulation (500K ticks)
timeout-minutes: 30
run: |
set -o pipefail
cargo run --release --features metal --bin jacquard -- cosim \
tests/mcu_soc/data/6_final.v \
--config tests/mcu_soc/sim_config.json --top-module top \
--max-cycles 500000 \
--max-clock-edges 500000 \
2>&1 | tee cosim_output.txt

- name: Verify UART boot output
Expand All @@ -637,22 +761,21 @@ jobs:
exit 1
fi

- name: Strip SDF timing checks for GPU simulation
run: |
# Remove TIMINGCHECK directives from SDF (timing checks can cause parser errors)
uv run tests/mcu_soc/cvc/strip_sdf_checks.py \
tests/mcu_soc/data/6_final.sdf \
tests/mcu_soc/data/6_final_stripped.sdf
- name: Download MCU SoC timing IR
uses: actions/download-artifact@v4
with:
name: mcu-soc-jtir
path: tests/mcu_soc/data/

- name: Capture stimulus + timing VCD via cosim (10K ticks)
timeout-minutes: 15
run: |
set -o pipefail
cargo run --release --features metal --bin jacquard -- cosim \
tests/mcu_soc/data/6_final.v \
--config tests/mcu_soc/sim_config.json --top-module top \
--max-cycles 10000 \
--sdf tests/mcu_soc/data/6_final_stripped.sdf \
--sdf-corner typ \
--max-clock-edges 10000 \
--timing-ir tests/mcu_soc/data/6_final.jtir \
--stimulus-vcd tests/mcu_soc/stimulus.vcd \
--timing-vcd tests/mcu_soc/jacquard_timed_mcu.vcd \
2>&1 | tee cosim_stimulus.txt
Expand All @@ -667,7 +790,7 @@ jobs:
tests/mcu_soc/jacquard_replay_mcu.vcd \
1 \
--top-module top \
--max-cycles 10000 \
--max-clock-edges 10000 \
2>&1 | tee metal_mcu_replay.txt

- name: Report simulation results
Expand Down Expand Up @@ -973,11 +1096,14 @@ jobs:
sudo apt-get install -y --no-install-recommends build-essential zlib1g-dev
git clone --depth 1 https://github.com/cambridgehackers/open-src-cvc.git ~/cvc-src
cd ~/cvc-src/src
make -f makefile.cvc64 -j$(nproc) \
OPTFLGS="" \
CFLAGS="-Wall -I../pli_incs -D__RHEL6X__ -fno-pie -no-pie -O0"
# Upstream refactored the build (commit 1c5e043e, 2026-03-13):
# makefile.cvc64 → top-level Makefile; output now at
# ../build64/cvc64. Mirrors the working invocation in the
# cvc-reference job earlier in this workflow.
make -j$(nproc) \
CFLAGS="-pipe -Wall -Wno-incompatible-pointer-types -Wno-implicit-function-declaration -I../pli_incs -D__RHEL6X__ -fno-pie -no-pie -O0"
mkdir -p ~/cvc/bin
cp cvc64 ~/cvc/bin/
cp ../build64/cvc64 ~/cvc/bin/

- name: Add CVC to PATH
run: echo "$HOME/cvc/bin" >> "$GITHUB_PATH"
Expand Down
17 changes: 17 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,23 @@ uv run netlist-graph path tests/timing_test/minimal_build/6_final.v "gpio_in[40]

See `docs/timing-violations.md` for the full guide on enabling GPU-side setup/hold violation checks, interpreting violation reports, and tracing violations back to source signals using `netlist_graph`.

## Python tooling

Python tools (PDK fetchers, build scripts, harness utilities) belong in
the workspace's `uv` dev dependency group, not as ad-hoc system pip
installs. Add them under `[dependency-groups].dev` in the root
`pyproject.toml`; install with `uv sync --group dev`; invoke via
`uv run <tool>`. CI mirrors this — see the dev group declarations and
PDK install steps in `.github/workflows/ci.yml`.

PDK Liberty / tech files are fetched via **volare** (pinned in
`[tool.jacquard.pdks.*]` in the root `pyproject.toml`, hashes mirrored
into `crates/opensta-to-ir/tests/opensta_integration.rs`). Volare has
been renamed to **ciel** upstream
(<https://github.com/fossi-foundation/ciel>) — the existing `volare`
PyPI package still works at the pinned version, but a migration to the
`ciel` package is open follow-up.

## Releases

Cutting a release is a lightweight, manual procedure: roll the
Expand Down
1 change: 1 addition & 0 deletions crates/opensta-to-ir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
pub mod builder;
pub mod dump;
pub mod opensta;
pub mod verilog_filter;
41 changes: 40 additions & 1 deletion crates/opensta-to-ir/src/opensta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,19 @@ pub fn run(
})
.collect::<Vec<_>>()
.join("\n");
let verilog_arg = paths_to_lines(inv.verilog);

// Filter each verilog input through the named-module extractor.
// LibreLane + wafer.space final.v files have the structural top
// wrapped in an integration module (e.g. openframe_project_wrapper)
// that uses RTL operators OpenSTA's reader rejects. Extracting
// just the named --top from each file leaves any other modules
// — including offending wrappers — behind. Files that don't
// contain a `module <top>` declaration are passed through
// unchanged so hierarchical designs with sub-modules split across
// multiple files still link cleanly. See ADR 0009.
let filtered_verilog_paths =
filter_verilog_inputs(inv.verilog, inv.top, dir.path())?;
let verilog_arg = paths_to_lines(&filtered_verilog_paths);

let mut cmd = Command::new(binary);
cmd.arg("-no_init")
Expand Down Expand Up @@ -440,6 +452,33 @@ fn paths_to_lines(paths: &[PathBuf]) -> String {
.join("\n")
}

/// Pre-extract the `top` module from each input file so OpenSTA's
/// structural-only Verilog reader doesn't choke on integration
/// wrappers. Files that don't contain `module <top>` are passed
/// through unchanged. Filtered outputs are written into `tmp_dir`
/// with unique names; the returned paths replace the original
/// `--verilog` paths in the OpenSTA invocation. See ADR 0009.
fn filter_verilog_inputs(
inputs: &[PathBuf],
top: &str,
tmp_dir: &Path,
) -> Result<Vec<PathBuf>, InvokeError> {
let mut out = Vec::with_capacity(inputs.len());
for (idx, src) in inputs.iter().enumerate() {
let content = std::fs::read_to_string(src).map_err(InvokeError::Spawn)?;
match crate::verilog_filter::extract_named_module(&content, top) {
Some(extracted) => {
let stem = src.file_stem().and_then(|s| s.to_str()).unwrap_or("input");
let dst = tmp_dir.join(format!("filtered_{idx:02}_{stem}.v"));
std::fs::write(&dst, extracted).map_err(InvokeError::Spawn)?;
out.push(dst);
}
None => out.push(src.clone()),
}
}
Ok(out)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading