Skip to content
Draft
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
75 changes: 74 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,72 @@ cf gpio-config --project-root /path/to/project
> [!NOTE]
> Invalid modes cannot be saved. All GPIOs must have valid configurations.

### Configure an Openframe Project (GPIO pads and power domains)

On openframe projects (`project.type: openframe`), `cf gpio-config` opens a 44-pad grid instead of the
caravel editor. It edits the `project.openframe` block in `.cf/project.json` and regenerates:

- `verilog/rtl/openframe_gpio.v`: one `CF_gpio_config` instance per pad, with named user ports
(instantiated by `openframe_project_wrapper.v`; do not edit).
- `openlane/openframe_project_wrapper/power.json`: `VDD_NETS`, `GND_NETS` and `PDN_MACRO_CONNECTIONS`
for the wrapper PDN (LibreLane 3.x variable names).

Grid keys: arrows move, `Space` selects, `Enter` edits the selected pads (mode, signal, bit, overrides),
`u` cycles the mode for unlisted pads, `p` edits power domains and macros, `d` validates and saves,
`q` quits without saving. Selecting several pads and giving a signal name assigns consecutive bits.

```bash
cf gpio-config # interactive grid
cf gpio-config --view # print the current pinout
cf openframe validate # validate project.openframe
cf openframe generate # regenerate openframe_gpio.v and power.json
cf openframe generate --check # CI: exit 1 if generated files are stale
cf openframe export pinout.yaml # standalone YAML or JSON (by extension or --format)
cf openframe import pinout.yaml # validate, store in .cf/project.json and regenerate
cf openframe import pinout.yaml --force # replace a different existing spec
```

Spec format (`schema_version: 1`, JSON Schema in `chipfoundry_cli/openframe/openframe.schema.json`):

```yaml
schema_version: 1
gpio:
unused_mode: analog # optional: analog | input_pd | input_pu; if omitted, all 44 pads must be listed
pads:
- {pad: 0, mode: input, signal: clk}
- {pad: 1, mode: input_pu, signal: rst}
- {pad: 2, mode: output, signal: out, bit: 0}
# ...
- {pad: 12, mode: output, signal: out, bit: 10}
- {pad: 20, mode: bidir, signal: sda, overrides: {slow: 1, vtrip: 1}}
power:
domains: [vccd1] # from vccd1, vccd2, vdda1, vdda2; the first is the primary domain
macros:
- {instance: mprj, domain: vccd1, vdd_pin: vccd1, gnd_pin: vssd1}
```

- Modes map to `CF_gpio_config` `MODE`: `analog`=0, `input`=1, `input_pd`=2, `input_pu`=3, `output`=4, `bidir`=5.
- Pads with the same `signal` form a bus; `bit` indices must be unique and contiguous from 0, and all pads of a
signal must share a direction. A `bidir` signal `x` becomes ports `x_in`, `x_out` and `x_oeb`.
- `output`/`bidir` pads need a signal; `analog` pads cannot have one (use `analog_io[n]` on the wrapper).
- `overrides` (`slow`, `vtrip`, `ib_mode`, `holdover`, `analog_en`, `analog_sel`, `analog_pol`, each 0 or 1)
require `CF_gpio_config` v1.2.0 or later.
- Invalid specs are rejected with every problem listed; nothing is generated from an invalid spec.

`cf init` on an openframe project seeds `project.openframe` from the template's `.cf/openframe_default.json`
when the project has no spec yet.

**Harden and precheck integration:**
- `cf harden openframe_project_wrapper` runs `librelane … power.json config.json`: LibreLane has no config
include and the last file's `meta` wins, so the generated `power.json` (PDN nets) goes first and
`config.json` (flow, step reorder) last. This applies to any macro directory with a `power.json`.
- `cf harden openframe_project_wrapper` and `cf precheck` print a warning panel when the generated files do not
match `project.openframe`, with the reason per file: missing, generated from a different spec (the
`spec-sha256` header differs), edited by hand, or not generated by cf. They also warn when the spec is
missing or invalid. The command continues with the files on disk; run `cf openframe generate` to fix it.

See [docs/OPENFRAME.md](docs/OPENFRAME.md) for the full configure → generate → harden → precheck flow.

### Harden a Macro

```bash
Expand All @@ -398,6 +464,11 @@ cf harden [MACRO] [OPTIONS]
2. Runs the hardening flow (synthesis, placement, routing) for a specific macro
3. Generates GDS, LEF, and other output files
4. Supports both Nix and Docker execution environments
5. If `openlane/<macro>/power.json` exists (generated by `cf openframe generate` for
`openframe_project_wrapper`), passes it to LibreLane **before** `config.*`
(`librelane … power.json config.json`), on both Nix and Docker, like `openlane/Makefile`
6. For `openframe_project_wrapper`, warns when `project.openframe` and the generated files differ
(see [Openframe](#configure-an-openframe-project-gpio-pads-and-power-domains))

**Key Options:**
- `--list` or no argument: List all available macros
Expand Down Expand Up @@ -473,7 +544,8 @@ cf precheck [OPTIONS]

**Prerequisites:**
- **Must run `cf init` first** - Project initialization is required
- **Must run `cf gpio-config` first** - GPIO configuration is required
- **Must run `cf gpio-config` first** - GPIO configuration is required (caravel/caravan; on openframe it
configures `project.openframe` instead and is not gated)
- Precheck tools must be installed (via `cf setup`)
- PDK must be installed (via `cf setup`)
- Docker must be available
Expand All @@ -483,6 +555,7 @@ cf precheck [OPTIONS]
2. Runs DRC, LVS, and other design rule checks
3. Ensures your design will fit into the SoC reference design
4. Generates reports highlighting any violations
5. On openframe projects, warns first if `project.openframe` and the generated files differ


**Key Options:**
Expand Down
17 changes: 16 additions & 1 deletion chipfoundry_cli/librelane_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,21 @@ def find_config_file(macro_dir: Path) -> Optional[Path]:
return None


POWER_JSON_NAME = "power.json"


def librelane_config_files(config_file: Path) -> List[Path]:
"""Config files to pass to LibreLane for a macro, in order.

A ``power.json`` next to the config (generated by ``cf openframe generate`` for
openframe_project_wrapper) carries the PDN nets. LibreLane has no config include and
the last file's ``meta`` wins, so ``power.json`` must come before ``config.*``,
matching ``openlane/Makefile``.
"""
power = config_file.parent / POWER_JSON_NAME
return [power, config_file] if power.is_file() else [config_file]


def list_macro_configs(project_root: Path) -> List[Dict[str, str]]:
"""Discover LibreLane macros as ``{name, config_path, flow_root}``."""
found: List[Dict[str, str]] = []
Expand Down Expand Up @@ -220,7 +235,7 @@ def build_librelane_command(
cmd.extend(["--from", from_step])
if to_step:
cmd.extend(["--to", to_step])
cmd.append(str(config_file))
cmd.extend(str(p) for p in librelane_config_files(Path(config_file)))
return cmd, env


Expand Down
59 changes: 48 additions & 11 deletions chipfoundry_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
verify_remote_precheck_repo,
)
from chipfoundry_cli.version_check import maybe_warn_outdated
from chipfoundry_cli.librelane_run import librelane_config_files
from chipfoundry_cli.openframe.cli import (
OPENFRAME_WRAPPER,
register_openframe,
run_openframe_gpio_config,
seed_openframe_spec,
warn_openframe_out_of_sync,
)
from chipfoundry_cli.utils import (
collect_project_files, ensure_cf_directory, update_or_create_project_json,
sftp_connect, upload_with_progress, sftp_ensure_dirs, sftp_download_recursive,
Expand Down Expand Up @@ -46,6 +54,20 @@

console = Console()

# LibreLane 3 writes terminal control sequences (e.g. cursor restore) to stdout on exit,
# so the step-list probe tags its JSON line instead of owning the whole stream.
STEP_LIST_MARKER = "CF_STEP_LIST_JSON:"


def parse_step_list_output(stdout: str) -> List[str]:
"""Extract the step list printed by the `cf harden` LibreLane probe."""
for line in stdout.splitlines():
idx = line.find(STEP_LIST_MARKER)
if idx != -1:
return json.loads(line[idx + len(STEP_LIST_MARKER):])['steps']
raise ValueError(f"LibreLane step list not found in probe output: {stdout.strip()[:500]!r}")


class CategorizedCommand(click.Command):
"""Click command with categorized help sections for options."""

Expand Down Expand Up @@ -593,6 +615,8 @@ def _merged(key_local: str, key_platform: Optional[str] = None) -> Optional[str]
proj['github_repo_url'] = github_repo_url
else:
proj.pop('github_repo_url', None)
if project_type == 'openframe':
seed_openframe_spec(Path(project_root), proj)

if not api_key:
with open(project_json_path, 'w') as f:
Expand Down Expand Up @@ -728,7 +752,12 @@ def _merged(key_local: str, key_platform: Optional[str] = None) -> Optional[str]
@click.option('--project-root', required=False, type=click.Path(exists=True, file_okay=False), help='Path to the project directory (defaults to current directory).')
@click.option('--view', is_flag=True, help='Display current GPIO configuration summary without editing.')
def gpio_config(project_root, view):
"""Configure GPIO settings interactively and save to project config and user_defines.v."""
"""Configure GPIO settings interactively.

Caravel/caravan: saves project.gpio_config and updates user_defines.v.
Openframe: edits project.openframe (pad modes, signals, overrides, power) and
regenerates verilog/rtl/openframe_gpio.v and the wrapper power.json.
"""
if not project_root:
project_root = os.getcwd()

Expand Down Expand Up @@ -780,11 +809,10 @@ def gpio_config(project_root, view):
project_data = json.load(f)
project_type = project_data.get('project', {}).get('type', 'digital')

# For openframe, GPIO config is not needed
# Openframe uses the project.openframe spec and generated openframe_gpio.v, not user_defines.v
if project_type == 'openframe':
console.print("[red]GPIO configuration is not available for openframe projects.[/red]")
console.print("[yellow]Openframe projects do not use user_defines.v.[/yellow]")
raise click.Abort()
run_openframe_gpio_config(project_root, project_json_path, view)
return

user_defines_path = project_root / 'verilog' / 'rtl' / 'user_defines.v'

Expand Down Expand Up @@ -4018,6 +4046,13 @@ def harden(

project_root_path = Path(project_root)

# The wrapper is the only design built from the generated openframe files (openframe_gpio.v, power.json).
if (
macro == OPENFRAME_WRAPPER
and not (list_designs or list_from_steps or open_in_openroad or open_in_klayout)
):
warn_openframe_out_of_sync(project_root_path, 'cf harden')

if poll and not remote:
console.print("[red]✗[/red] --poll requires --remote.")
raise SystemExit(1)
Expand Down Expand Up @@ -4137,6 +4172,7 @@ def harden(
console.print(f"[red]✗[/red] No config file found for {macro}")
console.print(f"[yellow]Expected one of: config.json, config.yaml, config.tcl[/yellow]")
return
config_files = [str(p) for p in librelane_config_files(Path(config_file))]

# Check for LibreLane venv
librelane_venv = openlane_dir / '.venv'
Expand Down Expand Up @@ -4178,7 +4214,7 @@ def get_valid_from_steps(librelane_python, macro_config, working_dir):
" continue\n"
" seen.add(step_id)\n"
" steps.append(step_id)\n"
"print(json.dumps({'steps': steps}))\n"
f"print({STEP_LIST_MARKER!r} + json.dumps({{'steps': steps}}))\n"
)
result = subprocess.run(
[str(librelane_python), '-c', script, macro_config],
Expand All @@ -4190,8 +4226,7 @@ def get_valid_from_steps(librelane_python, macro_config, working_dir):
err = (result.stderr or result.stdout or 'unknown error').strip()
return None, err
try:
payload = json.loads(result.stdout.strip())
return payload.get('steps', []), None
return parse_step_list_output(result.stdout), None
except Exception as exc:
return None, str(exc)

Expand Down Expand Up @@ -4350,7 +4385,7 @@ def get_valid_from_steps(librelane_python, macro_config, working_dir):
# Display configuration
console.print("\n" + "="*60)
console.print(f"[bold cyan]Hardening: {macro}[/bold cyan]")
console.print(f"Config: [yellow]{Path(config_file).name}[/yellow]")
console.print(f"Config: [yellow]{' + '.join(Path(f).name for f in config_files)}[/yellow]")
console.print(f"Run tag: [yellow]{tag}[/yellow]")
if auto_selected_latest_tag:
if gui_mode_count:
Expand Down Expand Up @@ -4390,7 +4425,7 @@ def get_valid_from_steps(librelane_python, macro_config, working_dir):
cmd.append('--overwrite')
if from_step:
cmd.extend(['--from', from_step])
cmd.append(config_file)
cmd.extend(config_files)

env = os.environ.copy()
env.update({
Expand Down Expand Up @@ -4453,7 +4488,7 @@ def get_valid_from_steps(librelane_python, macro_config, working_dir):
cmd.append('--overwrite')
if from_step:
cmd.extend(['--from', from_step])
cmd.append(config_file)
cmd.extend(config_files)

# Run LibreLane

Expand Down Expand Up @@ -4678,6 +4713,7 @@ def precheck(project_root, skip_checks, magic_drc, checks, list_checks, dry_run,
return

project_json_path = project_root_path / '.cf' / 'project.json'
warn_openframe_out_of_sync(project_root_path, 'cf precheck')

if poll and not remote:
console.print("[red]✗[/red] --poll requires --remote.")
Expand Down Expand Up @@ -5790,6 +5826,7 @@ def whoami_cmd():
from chipfoundry_cli.preview import register_preview

register_preview(main)
register_openframe(main)


if __name__ == "__main__":
Expand Down
15 changes: 15 additions & 0 deletions chipfoundry_cli/openframe/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Openframe project configuration: GPIO pad spec, validation and code generation."""

from chipfoundry_cli.openframe.generate import render_outputs, stale_outputs, write_outputs # noqa: F401
from chipfoundry_cli.openframe.spec import ( # noqa: F401
MODES,
NUM_PADS,
OVERRIDES,
POWER_DOMAINS,
OpenframeSpec,
SpecError,
load_spec_from_project,
read_spec_file,
validate_spec,
write_spec_file,
)
Loading
Loading