From 64d76a5724c02f0668abaaf84b945e50781700c3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 09:48:36 +0000 Subject: [PATCH 1/6] Add openframe GPIO/power configurator (cf openframe, cf gpio-config on openframe) - project.openframe spec in .cf/project.json (schema_version 1) with a shipped JSON Schema plus semantic validation: duplicate pads, bus bit gaps/duplicates, mixed directions, missing signals, reserved/keyword names, port clashes, macro domains not enabled. All errors are reported; no silent defaults. - Generators for verilog/rtl/openframe_gpio.v (CF_gpio_config per pad, named user ports, override parameters) and openlane/openframe_project_wrapper/power.json (VDD_NETS, GND_NETS, PDN_MACRO_CONNECTIONS). - cf openframe validate/generate [--check]/export/import (YAML or JSON). - cf gpio-config opens a Textual 44-pad grid on openframe projects. - cf init seeds project.openframe from .cf/openframe_default.json when present. - Adds pyyaml and jsonschema dependencies; bumps version to 2.7.0. --- chipfoundry_cli/main.py | 18 +- chipfoundry_cli/openframe/__init__.py | 15 + chipfoundry_cli/openframe/cli.py | 247 ++++ chipfoundry_cli/openframe/generate.py | 44 + chipfoundry_cli/openframe/gpio_gen.py | 131 ++ .../openframe/openframe.schema.json | 119 ++ chipfoundry_cli/openframe/power_gen.py | 29 + chipfoundry_cli/openframe/spec.py | 482 ++++++++ chipfoundry_cli/openframe/tui.py | 399 ++++++ chipfoundry_cli/utils.py | 29 + pyproject.toml | 4 +- tests/data/CF_gpio_config.v | 144 +++ tests/data/openframe_gpio_timer.v | 1101 +++++++++++++++++ tests/data/openframe_timer.json | 97 ++ tests/test_openframe.py | 575 +++++++++ 15 files changed, 3428 insertions(+), 6 deletions(-) create mode 100644 chipfoundry_cli/openframe/__init__.py create mode 100644 chipfoundry_cli/openframe/cli.py create mode 100644 chipfoundry_cli/openframe/generate.py create mode 100644 chipfoundry_cli/openframe/gpio_gen.py create mode 100644 chipfoundry_cli/openframe/openframe.schema.json create mode 100644 chipfoundry_cli/openframe/power_gen.py create mode 100644 chipfoundry_cli/openframe/spec.py create mode 100644 chipfoundry_cli/openframe/tui.py create mode 100644 tests/data/CF_gpio_config.v create mode 100644 tests/data/openframe_gpio_timer.v create mode 100644 tests/data/openframe_timer.json create mode 100644 tests/test_openframe.py diff --git a/chipfoundry_cli/main.py b/chipfoundry_cli/main.py index deacfd5..89a9da4 100644 --- a/chipfoundry_cli/main.py +++ b/chipfoundry_cli/main.py @@ -9,6 +9,7 @@ verify_remote_precheck_repo, ) from chipfoundry_cli.version_check import maybe_warn_outdated +from chipfoundry_cli.openframe.cli import register_openframe, run_openframe_gpio_config, seed_openframe_spec 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, @@ -593,6 +594,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: @@ -728,7 +731,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() @@ -780,11 +788,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' @@ -5790,6 +5797,7 @@ def whoami_cmd(): from chipfoundry_cli.preview import register_preview register_preview(main) +register_openframe(main) if __name__ == "__main__": diff --git a/chipfoundry_cli/openframe/__init__.py b/chipfoundry_cli/openframe/__init__.py new file mode 100644 index 0000000..dd01db6 --- /dev/null +++ b/chipfoundry_cli/openframe/__init__.py @@ -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, +) diff --git a/chipfoundry_cli/openframe/cli.py b/chipfoundry_cli/openframe/cli.py new file mode 100644 index 0000000..95a6e81 --- /dev/null +++ b/chipfoundry_cli/openframe/cli.py @@ -0,0 +1,247 @@ +"""`cf openframe ...` commands and the openframe branch of `cf gpio-config`.""" + +from pathlib import Path +from typing import Any, Dict, Optional + +import click +from rich.console import Console +from rich.table import Table + +from chipfoundry_cli.openframe.generate import stale_outputs, write_outputs +from chipfoundry_cli.openframe.spec import ( + OpenframeSpec, + SpecError, + _format_ranges, + load_spec_from_project, + normalize_spec, + read_spec_file, + validate_spec, + write_spec_file, +) +from chipfoundry_cli.utils import ( + CF_PROJECT_JSON_REL, + get_openframe_spec_from_project_json, + load_project_json, + save_openframe_spec_to_project_json, +) + +console = Console() + +# Seed file shipped by the openframe_user_project template; `cf init` imports it once. +OPENFRAME_SEED_REL = ".cf/openframe_default.json" + + +def _print_spec_error(err: SpecError) -> None: + console.print("[red]✗ Invalid openframe spec:[/red]") + for msg in err.errors: + console.print(f" [red]-[/red] {msg}") + + +def _project_root(project_root: Optional[str]) -> Path: + return Path(project_root).resolve() if project_root else Path.cwd() + + +def _require_openframe_project(root: Path) -> Path: + project_json = root / CF_PROJECT_JSON_REL + if not project_json.exists(): + console.print(f"[red]✗ {project_json} not found. Run 'cf init' first.[/red]") + raise click.Abort() + project_type = load_project_json(str(project_json)).get("project", {}).get("type") + if project_type != "openframe": + console.print( + f"[red]✗ This command is for openframe projects; project.type in {project_json} is '{project_type}'.[/red]" + ) + raise click.Abort() + return project_json + + +def _load_or_abort(root: Path) -> OpenframeSpec: + try: + return load_spec_from_project(root) + except SpecError as e: + _print_spec_error(e) + raise click.Abort() + + +def print_spec_summary(spec: OpenframeSpec) -> None: + table = Table(show_header=True, header_style="bold", box=None, padding=(0, 2)) + table.add_column("Signal", style="cyan") + table.add_column("Direction") + table.add_column("Pads") + table.add_column("Modes") + for sig in spec.signals: + name = f"{sig.name}[{sig.width - 1}:0]" if sig.width is not None else sig.name + modes = sorted({spec.pads[p].mode for p in sig.pads}) + table.add_row(name, sig.direction, _format_ranges(list(sig.pads)), ", ".join(modes)) + console.print(table) + + by_mode: Dict[str, list] = {} + for pad in spec.pads: + if pad.signal is None: + label = f"{pad.mode} (unused)" if pad.unused else f"{pad.mode} (no signal)" + by_mode.setdefault(label, []).append(pad.pad) + for label, pads in by_mode.items(): + console.print(f" [dim]{label}:[/dim] {_format_ranges(pads)}") + overridden = [p for p in spec.pads if p.overrides] + for p in overridden: + ov = ", ".join(f"{k}={v}" for k, v in p.overrides.items()) + console.print(f" [dim]pad {p.pad} overrides:[/dim] {ov}") + macros = ", ".join(f"{m.instance}->{m.domain}" for m in spec.macros) or "none" + console.print(f" [dim]power domains:[/dim] {', '.join(spec.domains)} [dim]macros:[/dim] {macros}") + + +def _generate(root: Path, spec: OpenframeSpec) -> None: + try: + changed = write_outputs(root, spec) + except FileNotFoundError as e: + console.print(f"[red]✗ {e}[/red]") + raise click.Abort() + if changed: + for rel in changed: + console.print(f"[green]✓ Wrote {rel}[/green]") + else: + console.print("[green]✓ Generated files are up to date[/green]") + + +def seed_openframe_spec(project_root: Path, proj: Dict[str, Any]) -> None: + """Import the template's seed spec into ``proj`` when the project has none. Used by `cf init`.""" + if "openframe" in proj: + return + seed = Path(project_root) / OPENFRAME_SEED_REL + if not seed.exists(): + console.print( + "[dim]No openframe spec yet. Run [bold]cf gpio-config[/bold] or " + "[bold]cf openframe import [/bold] to create one.[/dim]" + ) + return + try: + spec = validate_spec(read_spec_file(seed, "json")) + except SpecError as e: + console.print(f"[red]✗ {seed} is not a valid openframe spec.[/red]") + _print_spec_error(e) + raise click.Abort() + proj["openframe"] = spec.raw + console.print(f"[green]✓ Seeded project.openframe from {OPENFRAME_SEED_REL}[/green]") + + +def run_openframe_gpio_config(project_root: Path, project_json_path: Path, view: bool) -> None: + """Openframe branch of `cf gpio-config`: edit project.openframe in a 44-pad grid, then regenerate.""" + block = get_openframe_spec_from_project_json(str(project_json_path)) + if view: + if block is None: + console.print("[yellow]No openframe spec found.[/yellow]") + console.print("[dim]Run 'cf gpio-config' to configure GPIOs.[/dim]") + return + try: + spec = validate_spec(block) + except SpecError as e: + _print_spec_error(e) + raise click.Abort() + console.print("\n[bold cyan]Openframe GPIO configuration[/bold cyan]") + print_spec_summary(spec) + return + + from chipfoundry_cli.openframe.tui import OpenframeGridApp + + project_name = load_project_json(str(project_json_path)).get("project", {}).get("name", "") + result = OpenframeGridApp(block, project_name).run() + if result is None: + console.print("[yellow]Quit without saving; .cf/project.json unchanged.[/yellow]") + return + spec = validate_spec(result) + save_openframe_spec_to_project_json(str(project_json_path), spec.raw) + console.print(f"[green]✓ Saved project.openframe to {project_json_path}[/green]") + print_spec_summary(spec) + _generate(project_root, spec) + + +def register_openframe(main_group) -> None: + @main_group.group("openframe") + def openframe(): + """Openframe GPIO/power spec: validate, generate RTL and PDN settings, import/export.""" + + project_root_opt = click.option( + "--project-root", + type=click.Path(exists=True, file_okay=False), + help="Path to the project directory (defaults to current directory).", + ) + + @openframe.command("validate") + @project_root_opt + def validate_cmd(project_root): + """Validate project.openframe in .cf/project.json.""" + root = _project_root(project_root) + _require_openframe_project(root) + spec = _load_or_abort(root) + console.print("[green]✓ project.openframe is valid[/green]") + print_spec_summary(spec) + + @openframe.command("generate") + @project_root_opt + @click.option("--check", is_flag=True, help="Do not write; exit non-zero if generated files are missing or stale.") + def generate_cmd(project_root, check): + """Generate verilog/rtl/openframe_gpio.v and openlane/openframe_project_wrapper/power.json.""" + root = _project_root(project_root) + _require_openframe_project(root) + spec = _load_or_abort(root) + if check: + stale = stale_outputs(root, spec) + if stale: + for rel in stale: + console.print(f"[red]✗ {rel} is missing or out of date[/red]") + console.print("[yellow]Run 'cf openframe generate' to update.[/yellow]") + raise SystemExit(1) + console.print("[green]✓ Generated files match project.openframe[/green]") + return + _generate(root, spec) + + @openframe.command("export") + @click.argument("output", type=click.Path(dir_okay=False)) + @project_root_opt + @click.option("--format", "fmt", type=click.Choice(["json", "yaml"]), help="Defaults to the file extension.") + def export_cmd(output, project_root, fmt): + """Write project.openframe to a standalone YAML or JSON file.""" + root = _project_root(project_root) + _require_openframe_project(root) + spec = _load_or_abort(root) + try: + write_spec_file(output, spec.raw, fmt) + except SpecError as e: + _print_spec_error(e) + raise click.Abort() + console.print(f"[green]✓ Exported project.openframe to {output}[/green]") + + @openframe.command("import") + @click.argument("input_file", metavar="INPUT", type=click.Path(exists=True, dir_okay=False)) + @project_root_opt + @click.option("--format", "fmt", type=click.Choice(["json", "yaml"]), help="Defaults to the file extension.") + @click.option("--force", is_flag=True, help="Replace an existing, different project.openframe block.") + @click.option("--generate/--no-generate", "do_generate", default=True, show_default=True, + help="Regenerate RTL and power.json after importing.") + def import_cmd(input_file, project_root, fmt, force, do_generate): + """Validate a YAML or JSON spec file and store it as project.openframe.""" + root = _project_root(project_root) + project_json = _require_openframe_project(root) + try: + spec = validate_spec(read_spec_file(input_file, fmt)) + except SpecError as e: + _print_spec_error(e) + raise click.Abort() + existing = get_openframe_spec_from_project_json(str(project_json)) + if existing is not None and not force: + same = False + try: + same = normalize_spec(existing) == spec.raw + except (KeyError, TypeError): + same = False + if not same: + console.print( + "[red]✗ .cf/project.json already has a different project.openframe block. " + "Re-run with --force to replace it (export it first to keep a copy).[/red]" + ) + raise click.Abort() + save_openframe_spec_to_project_json(str(project_json), spec.raw) + console.print(f"[green]✓ Imported {input_file} into {project_json}[/green]") + print_spec_summary(spec) + if do_generate: + _generate(root, spec) diff --git a/chipfoundry_cli/openframe/generate.py b/chipfoundry_cli/openframe/generate.py new file mode 100644 index 0000000..85f2845 --- /dev/null +++ b/chipfoundry_cli/openframe/generate.py @@ -0,0 +1,44 @@ +"""Render and write every file derived from the openframe spec.""" + +from pathlib import Path +from typing import Dict, List + +from chipfoundry_cli.openframe.gpio_gen import GPIO_RTL_REL, generate_gpio_rtl +from chipfoundry_cli.openframe.power_gen import POWER_JSON_REL, generate_power_json +from chipfoundry_cli.openframe.spec import OpenframeSpec + + +def render_outputs(spec: OpenframeSpec) -> Dict[str, str]: + """Relative path -> file content for every generated file.""" + return { + GPIO_RTL_REL: generate_gpio_rtl(spec), + POWER_JSON_REL: generate_power_json(spec), + } + + +def stale_outputs(project_root, spec: OpenframeSpec) -> List[str]: + """Relative paths whose on-disk content differs from what the spec generates.""" + root = Path(project_root) + stale = [] + for rel, content in render_outputs(spec).items(): + path = root / rel + if not path.exists() or path.read_text() != content: + stale.append(rel) + return stale + + +def write_outputs(project_root, spec: OpenframeSpec) -> List[str]: + """Write generated files and return the relative paths that changed.""" + root = Path(project_root) + changed = [] + for rel, content in render_outputs(spec).items(): + path = root / rel + if not path.parent.is_dir(): + raise FileNotFoundError( + f"{path.parent} does not exist; is {root} an openframe_user_project checkout?" + ) + if path.exists() and path.read_text() == content: + continue + path.write_text(content) + changed.append(rel) + return changed diff --git a/chipfoundry_cli/openframe/gpio_gen.py b/chipfoundry_cli/openframe/gpio_gen.py new file mode 100644 index 0000000..96d5c51 --- /dev/null +++ b/chipfoundry_cli/openframe/gpio_gen.py @@ -0,0 +1,131 @@ +"""Generate verilog/rtl/openframe_gpio.v from a validated openframe spec.""" + +from typing import List + +from chipfoundry_cli.openframe.spec import MODES, NUM_PADS, OVERRIDES, PAD_PORTS, OpenframeSpec, Pad + +GPIO_RTL_REL = "verilog/rtl/openframe_gpio.v" +MODULE_NAME = "openframe_gpio" + + +def _range(width) -> str: + return f"[{width - 1}:0]" if width is not None else "" + + +def _signal_ref(name: str, width, bit: int) -> str: + return f"{name}[{bit}]" if width is not None else name + + +def _pad_label(spec: OpenframeSpec, pad: Pad) -> str: + if pad.unused: + return "(unused)" + if pad.signal is None: + return "(no signal)" + sig = next(s for s in spec.signals if s.name == pad.signal) + return f"{pad.signal}[{pad.bit}]" if sig.width is not None else pad.signal + + +def generate_gpio_rtl(spec: OpenframeSpec) -> str: + lines: List[str] = [] + w = lines.append + + w("// " + "-" * 77) + w(f"// {MODULE_NAME}") + w("//") + w("// GENERATED FILE - DO NOT EDIT.") + w("// Generated by `cf openframe generate` from the") + w("// project.openframe block in .cf/project.json. Edit the spec with") + w("// `cf gpio-config` or `cf openframe import`, then regenerate.") + w(f"// spec-sha256: {spec.sha256}") + if any(p.overrides for p in spec.pads): + w("// Uses CF_gpio_config pad overrides: requires CF_gpio_config v1.2.0 or later.") + w("//") + w("// Pad map:") + for pad in spec.pads: + w(f"// pad {pad.pad:2d} {pad.mode:<8s} {_pad_label(spec, pad)}") + w("// " + "-" * 77) + w("") + w("`default_nettype none") + w("") + w("// io_in is left open on pads that have no input signal.") + w("/* verilator lint_off PINCONNECTEMPTY */") + w(f"module {MODULE_NAME} (") + + port_lines = [] + if spec.signals: + port_lines.append(" // User design ports") + for sig in spec.signals: + for port, direction in sig.ports(): + rng = _range(sig.width) + port_lines.append(f" {direction:<6s} wire {rng:<7s} {port},") + port_lines.append("") + port_lines.append(" // Openframe pad interface: connect to the openframe_project_wrapper ports of the same name") + pad_rng = f"[{NUM_PADS - 1}:0]" + for i, (port, direction) in enumerate(PAD_PORTS): + sep = "," if i < len(PAD_PORTS) - 1 else "" + port_lines.append(f" {direction:<6s} wire {pad_rng:<7s} {port}{sep}") + lines.extend(port_lines) + w(");") + + for pad in spec.pads: + lines.extend(_instance(spec, pad)) + + w("") + w(f"endmodule // {MODULE_NAME}") + w("/* verilator lint_on PINCONNECTEMPTY */") + w("") + w("`default_nettype wire") + return "\n".join(lines) + "\n" + + +def _instance(spec: OpenframeSpec, pad: Pad) -> List[str]: + n = pad.pad + # Unused user inputs are tied to the pad's loopback constants (ignored by CF_gpio_config in these modes). + io_in = "" + io_out = f"gpio_loopback_zero[{n}]" + io_oeb = f"gpio_loopback_one[{n}]" + if pad.signal is not None: + sig = next(s for s in spec.signals if s.name == pad.signal) + bit = pad.bit if pad.bit is not None else 0 + if sig.direction == "in": + io_in = _signal_ref(sig.name, sig.width, bit) + elif sig.direction == "out": + io_out = _signal_ref(sig.name, sig.width, bit) + else: + io_in = _signal_ref(f"{sig.name}_in", sig.width, bit) + io_out = _signal_ref(f"{sig.name}_out", sig.width, bit) + io_oeb = _signal_ref(f"{sig.name}_oeb", sig.width, bit) + + params = [f".MODE(3'd{MODES[pad.mode]})"] + for key, param in OVERRIDES.items(): + if key in pad.overrides: + params.append(f".{param}(1'b{pad.overrides[key]})") + + out = ["", f" // pad {n}: {pad.mode} {_pad_label(spec, pad)}"] + out.append(" CF_gpio_config #(") + out.extend(f" {p}{',' if i < len(params) - 1 else ''}" for i, p in enumerate(params)) + out.append(f" ) gpio_cfg_{n:02d} (") + conns = [ + ("io_out", io_out), + ("io_in", io_in), + ("io_oeb", io_oeb), + ("gpio_zero", f"gpio_loopback_zero[{n}]"), + ("gpio_one", f"gpio_loopback_one[{n}]"), + ("gpio_in", f"gpio_in[{n}]"), + ("gpio_dm", f"{{gpio_dm2[{n}], gpio_dm1[{n}], gpio_dm0[{n}]}}"), + ("gpio_inp_dis", f"gpio_inp_dis[{n}]"), + ("gpio_oeb_out", f"gpio_oeb[{n}]"), + ("gpio_out_val", f"gpio_out[{n}]"), + ("gpio_analog_en", f"gpio_analog_en[{n}]"), + ("gpio_analog_sel", f"gpio_analog_sel[{n}]"), + ("gpio_analog_pol", f"gpio_analog_pol[{n}]"), + ("gpio_ib_mode_sel", f"gpio_ib_mode_sel[{n}]"), + ("gpio_vtrip_sel", f"gpio_vtrip_sel[{n}]"), + ("gpio_slow_sel", f"gpio_slow_sel[{n}]"), + ("gpio_holdover", f"gpio_holdover[{n}]"), + ] + for i, (pin, net) in enumerate(conns): + sep = "," if i < len(conns) - 1 else "" + out.append(f" .{pin:<16s} ({net}){sep}") + out.append(" );") + return out diff --git a/chipfoundry_cli/openframe/openframe.schema.json b/chipfoundry_cli/openframe/openframe.schema.json new file mode 100644 index 0000000..848956d --- /dev/null +++ b/chipfoundry_cli/openframe/openframe.schema.json @@ -0,0 +1,119 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://chipfoundry.io/schemas/openframe/v1.json", + "title": "ChipFoundry openframe project spec", + "description": "GPIO pad modes and power domains for an openframe_user_project. Stored under project.openframe in .cf/project.json.", + "type": "object", + "additionalProperties": false, + "required": ["schema_version", "gpio", "power"], + "properties": { + "schema_version": { + "description": "Version of this schema. Only 1 is defined.", + "const": 1 + }, + "gpio": { + "type": "object", + "additionalProperties": false, + "required": ["pads"], + "properties": { + "unused_mode": { + "description": "Mode given to pads not listed in pads. When omitted, all 44 pads must be listed.", + "enum": ["analog", "input_pd", "input_pu"] + }, + "pads": { + "type": "array", + "maxItems": 44, + "items": { "$ref": "#/definitions/pad" } + } + } + }, + "power": { + "type": "object", + "additionalProperties": false, + "required": ["domains", "macros"], + "properties": { + "domains": { + "description": "Enabled power domains, named by their supply net. The first entry is the primary domain.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { "enum": ["vccd1", "vccd2", "vdda1", "vdda2"] } + }, + "macros": { + "description": "Power domain of each user macro instance in openframe_project_wrapper.", + "type": "array", + "items": { "$ref": "#/definitions/macro" } + } + } + } + }, + "definitions": { + "identifier": { + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" + }, + "bit": { + "type": "integer", + "enum": [0, 1] + }, + "pad": { + "type": "object", + "additionalProperties": false, + "required": ["pad", "mode"], + "properties": { + "pad": { + "description": "Openframe GPIO pad index.", + "type": "integer", + "minimum": 0, + "maximum": 43 + }, + "mode": { + "description": "CF_gpio_config MODE: analog=0, input=1, input_pd=2, input_pu=3, output=4, bidir=5.", + "enum": ["analog", "input", "input_pd", "input_pu", "output", "bidir"] + }, + "signal": { + "description": "User-facing port name on the generated openframe_gpio module.", + "$ref": "#/definitions/identifier" + }, + "bit": { + "description": "Bit index within signal. Omit for a 1-bit signal.", + "type": "integer", + "minimum": 0, + "maximum": 43 + }, + "overrides": { + "description": "Static pad controls. Each value drives the pad control bit directly. Requires CF_gpio_config v1.2.0.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "slow": { "$ref": "#/definitions/bit" }, + "vtrip": { "$ref": "#/definitions/bit" }, + "ib_mode": { "$ref": "#/definitions/bit" }, + "holdover": { "$ref": "#/definitions/bit" }, + "analog_en": { "$ref": "#/definitions/bit" }, + "analog_sel": { "$ref": "#/definitions/bit" }, + "analog_pol": { "$ref": "#/definitions/bit" } + } + } + } + }, + "macro": { + "type": "object", + "additionalProperties": false, + "required": ["instance", "domain", "vdd_pin", "gnd_pin"], + "properties": { + "instance": { + "description": "Macro instance name in openframe_project_wrapper (hierarchy separated by '/').", + "type": "string", + "pattern": "^[A-Za-z_][A-Za-z0-9_]*(/[A-Za-z_][A-Za-z0-9_]*)*$" + }, + "domain": { + "enum": ["vccd1", "vccd2", "vdda1", "vdda2"] + }, + "vdd_pin": { "$ref": "#/definitions/identifier" }, + "gnd_pin": { "$ref": "#/definitions/identifier" } + } + } + } +} diff --git a/chipfoundry_cli/openframe/power_gen.py b/chipfoundry_cli/openframe/power_gen.py new file mode 100644 index 0000000..a76075d --- /dev/null +++ b/chipfoundry_cli/openframe/power_gen.py @@ -0,0 +1,29 @@ +"""Generate openlane/openframe_project_wrapper/power.json (LibreLane 3.x PDN variables).""" + +import json +from typing import Any, Dict + +from chipfoundry_cli.openframe.spec import POWER_DOMAINS, OpenframeSpec + +POWER_JSON_REL = "openlane/openframe_project_wrapper/power.json" + + +def power_config(spec: OpenframeSpec) -> Dict[str, Any]: + """LibreLane config fragment for the wrapper PDN. + + ``PDN_MACRO_CONNECTIONS`` entries follow LibreLane's + `` `` format. + """ + return { + "//": "Generated by `cf openframe generate` from project.openframe.power in .cf/project.json. Do not edit.", + "// spec-sha256": spec.sha256, + "VDD_NETS": list(spec.domains), + "GND_NETS": [POWER_DOMAINS[d] for d in spec.domains], + "PDN_MACRO_CONNECTIONS": [ + f"{m.instance} {m.domain} {m.gnd_net} {m.vdd_pin} {m.gnd_pin}" for m in spec.macros + ], + } + + +def generate_power_json(spec: OpenframeSpec) -> str: + return json.dumps(power_config(spec), indent=4) + "\n" diff --git a/chipfoundry_cli/openframe/spec.py b/chipfoundry_cli/openframe/spec.py new file mode 100644 index 0000000..e2a7764 --- /dev/null +++ b/chipfoundry_cli/openframe/spec.py @@ -0,0 +1,482 @@ +"""Openframe project spec: GPIO pad modes and power domains. + +The spec lives in ``.cf/project.json`` under ``project.openframe`` and is +validated in two passes: the JSON Schema shipped next to this module checks +structure, then :func:`validate_spec` checks the cross-field rules the schema +cannot express (duplicate pads, bus gaps, mixed directions, name clashes). +""" + +import hashlib +import json +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +import jsonschema +import yaml + +SCHEMA_VERSION = 1 +NUM_PADS = 44 +SCHEMA_PATH = Path(__file__).with_name("openframe.schema.json") + +MODES: Dict[str, int] = { + "analog": 0, + "input": 1, + "input_pd": 2, + "input_pu": 3, + "output": 4, + "bidir": 5, +} + +MODE_DESCRIPTIONS: Dict[str, str] = { + "analog": "Analog (digital buffers off)", + "input": "Input, no pull", + "input_pd": "Input, pull-down", + "input_pu": "Input, pull-up", + "output": "Output, push-pull", + "bidir": "Bidirectional", +} + +UNUSED_MODES = ("analog", "input_pd", "input_pu") + +# Direction of the user-facing signal, seen from the user design. +MODE_DIRECTION: Dict[str, Optional[str]] = { + "analog": None, + "input": "in", + "input_pd": "in", + "input_pu": "in", + "output": "out", + "bidir": "inout", +} + +# Spec override key -> CF_gpio_config parameter. +OVERRIDES: Dict[str, str] = { + "slow": "SLOW", + "vtrip": "VTRIP", + "ib_mode": "IB_MODE", + "holdover": "HOLDOVER", + "analog_en": "ANALOG_EN", + "analog_sel": "ANALOG_SEL", + "analog_pol": "ANALOG_POL", +} + +# Power domain (VDD net) -> ground net. +POWER_DOMAINS: Dict[str, str] = { + "vccd1": "vssd1", + "vccd2": "vssd2", + "vdda1": "vssa1", + "vdda2": "vssa2", +} + +# Ports of the generated openframe_gpio module that face the padframe. +PAD_PORTS: Tuple[Tuple[str, str], ...] = ( + ("gpio_in", "input"), + ("gpio_out", "output"), + ("gpio_oeb", "output"), + ("gpio_inp_dis", "output"), + ("gpio_ib_mode_sel", "output"), + ("gpio_vtrip_sel", "output"), + ("gpio_slow_sel", "output"), + ("gpio_holdover", "output"), + ("gpio_analog_en", "output"), + ("gpio_analog_sel", "output"), + ("gpio_analog_pol", "output"), + ("gpio_dm2", "output"), + ("gpio_dm1", "output"), + ("gpio_dm0", "output"), + ("gpio_loopback_one", "input"), + ("gpio_loopback_zero", "input"), +) + +# Remaining openframe_project_wrapper ports; reserved so user signals never shadow them. +_WRAPPER_PORTS = ( + "gpio_in_h", "analog_io", "analog_noesd_io", "porb_h", "porb_l", "por_l", + "resetb_h", "resetb_l", "mask_rev", + "vdda", "vdda1", "vdda2", "vssa", "vssa1", "vssa2", "vccd", "vccd1", "vccd2", + "vssd", "vssd1", "vssd2", "vddio", "vssio", +) + +RESERVED_NAMES = frozenset([name for name, _ in PAD_PORTS] + list(_WRAPPER_PORTS)) + +VERILOG_KEYWORDS = frozenset(""" +always and assign automatic begin buf bufif0 bufif1 case casex casez cell cmos config deassign default +defparam design disable edge else end endcase endconfig endfunction endgenerate endmodule endprimitive +endspecify endtable endtask event for force forever fork function generate genvar highz0 highz1 if ifnone +incdir include initial inout input instance integer join large liblist library localparam macromodule +medium module nand negedge nmos nor noshowcancelled not notif0 notif1 or output parameter pmos posedge +primitive pull0 pull1 pulldown pullup pulsestyle_ondetect pulsestyle_onevent rcmos real realtime reg +release repeat rnmos rpmos rtran rtranif0 rtranif1 scalared showcancelled signed small specify specparam +strong0 strong1 supply0 supply1 table task time tran tranif0 tranif1 tri tri0 tri1 triand trior trireg +unsigned use uwire vectored wait wand weak0 weak1 while wire wor xnor xor +logic bit byte int shortint longint interface endinterface class endclass package endpackage import export +""".split()) + +_IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +class SpecError(ValueError): + """Raised when an openframe spec is missing or invalid. ``errors`` lists every problem found.""" + + def __init__(self, errors: List[str]): + if isinstance(errors, str): + errors = [errors] + self.errors = list(errors) + super().__init__("Invalid openframe spec:\n" + "\n".join(f" - {e}" for e in self.errors)) + + +@dataclass(frozen=True) +class Pad: + pad: int + mode: str + signal: Optional[str] = None + bit: Optional[int] = None + overrides: Dict[str, int] = field(default_factory=dict) + unused: bool = False + + +@dataclass(frozen=True) +class Signal: + """A user-facing signal made of one or more pads.""" + + name: str + direction: str # "in", "out" or "inout" (seen from the user design) + width: Optional[int] # None for a 1-bit signal without a bit index + pads: Tuple[int, ...] # pad index per bit, bit 0 first + + def ports(self) -> List[Tuple[str, str]]: + """(port name, verilog direction) pairs on the generated module.""" + if self.direction == "in": + return [(self.name, "output")] + if self.direction == "out": + return [(self.name, "input")] + return [(f"{self.name}_in", "output"), (f"{self.name}_out", "input"), (f"{self.name}_oeb", "input")] + + +@dataclass(frozen=True) +class MacroPower: + instance: str + domain: str + vdd_pin: str + gnd_pin: str + + @property + def gnd_net(self) -> str: + return POWER_DOMAINS[self.domain] + + +@dataclass(frozen=True) +class OpenframeSpec: + pads: Tuple[Pad, ...] # all 44 pads, index == pad number + signals: Tuple[Signal, ...] # ordered by lowest pad index + domains: Tuple[str, ...] + macros: Tuple[MacroPower, ...] + raw: Dict[str, Any] # normalized spec dict + unused_mode: Optional[str] = None + + @property + def sha256(self) -> str: + return spec_sha256(self.raw) + + +def load_schema() -> Dict[str, Any]: + with open(SCHEMA_PATH, "r") as f: + return json.load(f) + + +def _schema_errors(data: Any) -> List[str]: + validator = jsonschema.Draft7Validator(load_schema()) + errors = [] + for err in sorted(validator.iter_errors(data), key=lambda e: [str(p) for p in e.absolute_path]): + location = "/".join(str(p) for p in err.absolute_path) or "" + errors.append(f"{location}: {err.message}") + return errors + + +def _check_name(name: str, what: str, errors: List[str]) -> None: + if not _IDENT_RE.match(name): + errors.append(f"{what} '{name}' is not a valid Verilog identifier") + elif name in VERILOG_KEYWORDS: + errors.append(f"{what} '{name}' is a Verilog keyword") + elif name in RESERVED_NAMES: + errors.append(f"{what} '{name}' clashes with an openframe_project_wrapper port") + + +def validate_spec(data: Any) -> OpenframeSpec: + """Validate a ``project.openframe`` block and return the parsed spec. + + Raises :class:`SpecError` listing every problem found. + """ + schema_errors = _schema_errors(data) + if schema_errors: + raise SpecError(schema_errors) + + errors: List[str] = [] + gpio = data["gpio"] + unused_mode = gpio.get("unused_mode") + + by_pad: Dict[int, Pad] = {} + for entry in gpio["pads"]: + n = entry["pad"] + if n in by_pad: + errors.append(f"pad {n} is listed more than once") + continue + mode = entry["mode"] + signal = entry.get("signal") + bit = entry.get("bit") + overrides = dict(entry.get("overrides", {})) + if mode == "analog" and signal is not None: + errors.append( + f"pad {n}: analog pads cannot have a signal; use analog_io[{n}] on openframe_project_wrapper directly" + ) + if mode in ("output", "bidir") and signal is None: + errors.append(f"pad {n}: mode '{mode}' requires a signal to drive the pad") + if bit is not None and signal is None: + errors.append(f"pad {n}: 'bit' is set but 'signal' is missing") + if signal is not None: + _check_name(signal, f"pad {n}: signal", errors) + by_pad[n] = Pad(pad=n, mode=mode, signal=signal, bit=bit, overrides=overrides) + + missing = [n for n in range(NUM_PADS) if n not in by_pad] + if missing and unused_mode is None: + errors.append( + f"pads {_format_ranges(missing)} are not configured; list them or set gpio.unused_mode " + f"(one of {', '.join(UNUSED_MODES)})" + ) + for n in missing: + by_pad[n] = Pad(pad=n, mode=unused_mode or "analog", unused=True) + + signals = _build_signals([by_pad[n] for n in sorted(by_pad) if not by_pad[n].unused], errors) + + port_owner: Dict[str, str] = {} + for sig in signals: + for port, _ in sig.ports(): + if port in port_owner: + errors.append( + f"signal '{sig.name}' creates port '{port}', which is already used by signal '{port_owner[port]}'" + ) + else: + if port != sig.name: + _check_name(port, f"signal '{sig.name}' port", errors) + port_owner[port] = sig.name + + power = data["power"] + domains = tuple(power["domains"]) + macros: List[MacroPower] = [] + seen_instances = set() + for m in power["macros"]: + inst = m["instance"] + if inst in seen_instances: + errors.append(f"power: macro instance '{inst}' is listed more than once") + continue + seen_instances.add(inst) + if m["domain"] not in domains: + errors.append( + f"power: macro '{inst}' uses domain '{m['domain']}', " + f"which is not in power.domains ({', '.join(domains)})" + ) + macros.append(MacroPower(instance=inst, domain=m["domain"], vdd_pin=m["vdd_pin"], gnd_pin=m["gnd_pin"])) + + if errors: + raise SpecError(errors) + + return OpenframeSpec( + pads=tuple(by_pad[n] for n in range(NUM_PADS)), + signals=tuple(signals), + domains=domains, + macros=tuple(macros), + raw=normalize_spec(data), + unused_mode=unused_mode, + ) + + +def _build_signals(pads: List[Pad], errors: List[str]) -> List[Signal]: + groups: Dict[str, List[Pad]] = {} + for p in pads: + if p.signal is not None: + groups.setdefault(p.signal, []).append(p) + + signals = [] + for name, members in groups.items(): + directions = {MODE_DIRECTION[p.mode] for p in members} + if len(directions) > 1: + detail = ", ".join(f"pad {p.pad}={p.mode}" for p in members) + errors.append(f"signal '{name}' mixes directions ({detail}); all its pads must be input, output or bidir") + continue + direction = directions.pop() + + with_bit = [p for p in members if p.bit is not None] + if with_bit and len(with_bit) != len(members): + no_bit = [p.pad for p in members if p.bit is None] + errors.append( + f"signal '{name}': pads {_format_ranges(no_bit)} need a 'bit' because other pads of the bus set one" + ) + continue + + if not with_bit: + if len(members) > 1: + errors.append( + f"signal '{name}' is used by pads {_format_ranges([p.pad for p in members])} without bit indices" + ) + continue + signals.append(Signal(name=name, direction=direction, width=None, pads=(members[0].pad,))) + continue + + by_bit: Dict[int, int] = {} + bad = False + for p in members: + if p.bit in by_bit: + errors.append(f"signal '{name}': bit {p.bit} is assigned to both pad {by_bit[p.bit]} and pad {p.pad}") + bad = True + else: + by_bit[p.bit] = p.pad + if bad: + continue + width = max(by_bit) + 1 + gaps = [b for b in range(width) if b not in by_bit] + if gaps: + errors.append(f"signal '{name}[{width - 1}:0]' has no pad for bit(s) {_format_ranges(gaps)}") + continue + signals.append(Signal(name=name, direction=direction, width=width, pads=tuple(by_bit[b] for b in range(width)))) + + signals.sort(key=lambda s: min(s.pads)) + return signals + + +def normalize_spec(data: Dict[str, Any]) -> Dict[str, Any]: + """Canonical form: pads sorted by index, keys in a fixed order.""" + gpio = data["gpio"] + pads = [] + for entry in sorted(gpio["pads"], key=lambda e: e["pad"]): + out: Dict[str, Any] = {"pad": entry["pad"], "mode": entry["mode"]} + if "signal" in entry: + out["signal"] = entry["signal"] + if "bit" in entry: + out["bit"] = entry["bit"] + if entry.get("overrides"): + out["overrides"] = {k: entry["overrides"][k] for k in OVERRIDES if k in entry["overrides"]} + pads.append(out) + norm_gpio: Dict[str, Any] = {} + if "unused_mode" in gpio: + norm_gpio["unused_mode"] = gpio["unused_mode"] + norm_gpio["pads"] = pads + power = data["power"] + return { + "schema_version": data["schema_version"], + "gpio": norm_gpio, + "power": { + "domains": list(power["domains"]), + "macros": [ + {"instance": m["instance"], "domain": m["domain"], "vdd_pin": m["vdd_pin"], "gnd_pin": m["gnd_pin"]} + for m in power["macros"] + ], + }, + } + + +def spec_sha256(data: Dict[str, Any]) -> str: + canonical = json.dumps(normalize_spec(data), sort_keys=True, separators=(",", ":")) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + +def _format_ranges(nums: List[int]) -> str: + nums = sorted(nums) + if not nums: + return "-" + out = [] + start = prev = nums[0] + for n in nums[1:]: + if n == prev + 1: + prev = n + continue + out.append(f"{start}-{prev}" if start != prev else str(start)) + start = prev = n + out.append(f"{start}-{prev}" if start != prev else str(start)) + return ", ".join(out) + + +# --------------------------------------------------------------------------- +# File I/O (standalone YAML/JSON and .cf/project.json) +# --------------------------------------------------------------------------- + +def _reject_duplicate_keys(pairs): + out = {} + for key, value in pairs: + if key in out: + raise SpecError([f"duplicate key '{key}'"]) + out[key] = value + return out + + +class _StrictSafeLoader(yaml.SafeLoader): + pass + + +def _construct_mapping_no_dupes(loader, node, deep=False): + keys = set() + for key_node, _ in node.value: + key = loader.construct_object(key_node, deep=deep) + if key in keys: + raise SpecError([f"duplicate key '{key}' (line {key_node.start_mark.line + 1})"]) + keys.add(key) + return loader.construct_mapping(node, deep=deep) + + +_StrictSafeLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping_no_dupes) + + +def _file_format(path: Path, fmt: Optional[str]) -> str: + if fmt: + if fmt not in ("json", "yaml"): + raise SpecError([f"unknown format '{fmt}'; use json or yaml"]) + return fmt + suffix = path.suffix.lower() + if suffix == ".json": + return "json" + if suffix in (".yaml", ".yml"): + return "yaml" + raise SpecError([f"cannot infer format from '{path.name}'; use a .json/.yaml/.yml extension or pass --format"]) + + +def read_spec_file(path, fmt: Optional[str] = None) -> Dict[str, Any]: + """Read a standalone spec file. Accepts either the bare block or ``{"openframe": {...}}``.""" + path = Path(path) + fmt = _file_format(path, fmt) + text = path.read_text() + try: + if fmt == "json": + data = json.loads(text, object_pairs_hook=_reject_duplicate_keys) + else: + data = yaml.load(text, Loader=_StrictSafeLoader) + except (json.JSONDecodeError, yaml.YAMLError) as e: + raise SpecError([f"{path}: cannot parse {fmt}: {e}"]) + if isinstance(data, dict) and set(data.keys()) == {"openframe"}: + data = data["openframe"] + if not isinstance(data, dict): + raise SpecError([f"{path}: expected a mapping at the top level"]) + return data + + +def write_spec_file(path, data: Dict[str, Any], fmt: Optional[str] = None) -> None: + path = Path(path) + fmt = _file_format(path, fmt) + data = normalize_spec(data) + if fmt == "json": + path.write_text(json.dumps(data, indent=2) + "\n") + else: + path.write_text(yaml.safe_dump(data, sort_keys=False, default_flow_style=None)) + + +def load_spec_from_project(project_root) -> OpenframeSpec: + """Load and validate ``project.openframe`` from ``/.cf/project.json``.""" + from chipfoundry_cli.utils import CF_PROJECT_JSON_REL, get_openframe_spec_from_project_json + + project_json = Path(project_root) / CF_PROJECT_JSON_REL + if not project_json.exists(): + raise SpecError([f"{project_json} not found; run 'cf init' first"]) + block = get_openframe_spec_from_project_json(str(project_json)) + if block is None: + raise SpecError([ + f"{project_json} has no project.openframe block; run 'cf gpio-config' or 'cf openframe import '" + ]) + return validate_spec(block) diff --git a/chipfoundry_cli/openframe/tui.py b/chipfoundry_cli/openframe/tui.py new file mode 100644 index 0000000..8cd7ef2 --- /dev/null +++ b/chipfoundry_cli/openframe/tui.py @@ -0,0 +1,399 @@ +"""Textual 44-pad grid for editing the openframe spec (``cf gpio-config`` on openframe projects).""" + +import copy +from typing import Any, Dict, List, Optional + +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Grid, Horizontal, Vertical, VerticalScroll +from textual.screen import ModalScreen +from textual.widgets import Button, Checkbox, Footer, Header, Input, Label, RadioButton, RadioSet, Static, TextArea + +from chipfoundry_cli.openframe.spec import ( + MODE_DESCRIPTIONS, + MODES, + NUM_PADS, + OVERRIDES, + POWER_DOMAINS, + UNUSED_MODES, + SpecError, + validate_spec, +) + +MODE_COLORS = { + "analog": "magenta", + "input": "cyan", + "input_pd": "cyan", + "input_pu": "cyan", + "output": "ansi_bright_green", + "bidir": "yellow", +} + +GRID_COLS = 11 + + +def empty_spec() -> Dict[str, Any]: + """A spec with no pads and no power domains; it only validates once the user fills it in.""" + return {"schema_version": 1, "gpio": {"pads": []}, "power": {"domains": [], "macros": []}} + + +class PadCell(Static): + """One pad in the grid.""" + + def __init__(self, pad: int, entry: Optional[Dict[str, Any]], unused_mode: Optional[str]): + super().__init__(id=f"pad_{pad}") + self.pad = pad + self.entry = entry + self.unused_mode = unused_mode + self.is_selected = False + + def on_mount(self) -> None: + self.refresh_view() + + def refresh_view(self) -> None: + entry = self.entry + if entry is None: + mode = self.unused_mode + text = f"[b]{self.pad}[/b]\n[dim]{mode + '*' if mode else 'unset'}[/dim]\n" + color = MODE_COLORS.get(mode, "red") if mode else "red" + else: + mode = entry["mode"] + sig = entry.get("signal", "") + if sig and "bit" in entry: + sig = f"{sig}[{entry['bit']}]" + flag = "+" if entry.get("overrides") else "" + text = f"[b]{self.pad}[/b]\n{mode}{flag}\n{sig}" + color = MODE_COLORS[mode] + self.update(text) + self.set_class(self.is_selected, "selected") + if not self.is_selected: + self.styles.color = color + self.styles.border = ("solid", color) + + def on_click(self) -> None: + self.app.toggle_pad(self.pad) + + +class PadEditScreen(ModalScreen): + """Edit mode, signal, bit and overrides for one or more pads.""" + + BINDINGS = [Binding("escape", "cancel", "Cancel")] + + CSS = """ + PadEditScreen { align: center middle; } + #pad-dialog { width: 90; height: auto; max-height: 95%; padding: 1 2; background: $surface; border: solid cyan; } + #pad-title { text-style: bold; color: cyan; margin-bottom: 1; } + .field-label { margin-top: 1; text-style: bold; } + #overrides { grid-size: 4; height: auto; } + #overrides Checkbox { width: 1fr; } + #pad-buttons { margin-top: 1; height: auto; } + #pad-buttons Button { margin: 0 1; } + """ + + def __init__(self, pads: List[int], template: Optional[Dict[str, Any]]): + super().__init__() + self.pads = sorted(pads) + self.template = template or {} + + def compose(self) -> ComposeResult: + pads_str = ", ".join(str(p) for p in self.pads[:8]) + (" ..." if len(self.pads) > 8 else "") + multi = len(self.pads) > 1 + with VerticalScroll(id="pad-dialog"): + yield Label(f"Configure pad(s): {pads_str}", id="pad-title") + yield Label("Mode", classes="field-label") + with RadioSet(id="mode"): + current = self.template.get("mode", "input") + for mode in MODES: + label = f"{mode:<9s} {MODE_DESCRIPTIONS[mode]}" + yield RadioButton(label, value=(mode == current), id=f"mode_{mode}") + yield Label("Signal (port name on openframe_gpio; empty for none)", classes="field-label") + yield Input(value=self.template.get("signal", ""), placeholder="e.g. uart_tx", id="signal") + if multi: + bit_label = "Start bit (pads get consecutive bits in pad order)" + else: + bit_label = "Bit (empty for a 1-bit signal)" + yield Label(bit_label, classes="field-label") + bit_default = "0" if multi and "bit" not in self.template else str(self.template.get("bit", "")) + yield Input(value=bit_default, placeholder="e.g. 0", id="bit") + yield Label("Pad overrides (CF_gpio_config v1.2.0+)", classes="field-label") + overrides = self.template.get("overrides", {}) + with Grid(id="overrides"): + for key in OVERRIDES: + yield Checkbox(key, value=bool(overrides.get(key, 0)), id=f"ov_{key}") + yield Label("", id="pad-error") + with Horizontal(id="pad-buttons"): + yield Button("Apply", variant="primary", id="apply") + yield Button("Clear pad(s)", id="clear") + yield Button("Cancel", variant="error", id="cancel") + + def _selected_mode(self) -> Optional[str]: + radio = self.query_one("#mode", RadioSet) + if radio.pressed_button is None: + return None + return radio.pressed_button.id[len("mode_"):] + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "cancel": + self.dismiss(None) + elif event.button.id == "clear": + self.dismiss({"clear": True}) + elif event.button.id == "apply": + self._apply() + + def _apply(self) -> None: + error = self.query_one("#pad-error", Label) + mode = self._selected_mode() + if mode is None: + error.update("[red]Select a mode.[/red]") + return + signal = self.query_one("#signal", Input).value.strip() + bit_text = self.query_one("#bit", Input).value.strip() + bit: Optional[int] = None + if bit_text: + if not bit_text.isdigit(): + error.update(f"[red]Bit must be a non-negative integer, got '{bit_text}'.[/red]") + return + bit = int(bit_text) + overrides = {k: 1 for k in OVERRIDES if self.query_one(f"#ov_{k}", Checkbox).value} + self.dismiss({"mode": mode, "signal": signal or None, "bit": bit, "overrides": overrides}) + + def action_cancel(self) -> None: + self.dismiss(None) + + +class PowerScreen(ModalScreen): + """Edit enabled power domains and the per-macro domain mapping.""" + + BINDINGS = [Binding("escape", "cancel", "Cancel")] + + CSS = """ + PowerScreen { align: center middle; } + #power-dialog { width: 80; height: auto; max-height: 95%; padding: 1 2; background: $surface; border: solid cyan; } + #power-title { text-style: bold; color: cyan; } + .field-label { margin-top: 1; text-style: bold; } + #macros { height: 8; } + #power-buttons { margin-top: 1; height: auto; } + #power-buttons Button { margin: 0 1; } + """ + + def __init__(self, power: Dict[str, Any]): + super().__init__() + self.power = power + + def compose(self) -> ComposeResult: + domains = self.power.get("domains", []) + with Vertical(id="power-dialog"): + yield Label("Power domains", id="power-title") + yield Label("Enabled domains (the first enabled one in this order is the primary):", classes="field-label") + for vdd, gnd in POWER_DOMAINS.items(): + yield Checkbox(f"{vdd} / {gnd}", value=vdd in domains, id=f"dom_{vdd}") + yield Label("Macros, one per line: ", classes="field-label") + text = "\n".join( + f"{m['instance']} {m['domain']} {m['vdd_pin']} {m['gnd_pin']}" for m in self.power.get("macros", []) + ) + yield TextArea(text, id="macros") + yield Label("", id="power-error") + with Horizontal(id="power-buttons"): + yield Button("Apply", variant="primary", id="apply") + yield Button("Cancel", variant="error", id="cancel") + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "cancel": + self.dismiss(None) + return + # Preserve the existing domain order; append newly enabled domains in canonical order. + enabled = {d for d in POWER_DOMAINS if self.query_one(f"#dom_{d}", Checkbox).value} + domains = [d for d in self.power.get("domains", []) if d in enabled] + domains += [d for d in POWER_DOMAINS if d in enabled and d not in domains] + macros = [] + for n, line in enumerate(self.query_one("#macros", TextArea).text.splitlines(), start=1): + fields = line.split() + if not fields: + continue + if len(fields) != 4: + self.query_one("#power-error", Label).update( + f"[red]Line {n}: expected 4 fields, got {len(fields)}: '{line.strip()}'[/red]" + ) + return + inst, domain, vdd_pin, gnd_pin = fields + macros.append({"instance": inst, "domain": domain, "vdd_pin": vdd_pin, "gnd_pin": gnd_pin}) + self.dismiss({"domains": domains, "macros": macros}) + + def action_cancel(self) -> None: + self.dismiss(None) + + +class OpenframeGridApp(App): + """Returns the edited spec dict on save, or None when the user quits without saving.""" + + CSS = """ + #main { padding: 0 1; } + #title { text-style: bold; color: cyan; margin-bottom: 1; } + #pad-grid { grid-size: 11; grid-gutter: 0; height: auto; } + PadCell { height: 5; min-width: 11; border: solid grey; content-align: center middle; text-align: center; } + PadCell.current { border: double cyan; background: $primary; } + PadCell.selected { background: $success; color: black; } + #info { margin-top: 1; } + #status { margin-top: 1; color: yellow; } + """ + + BINDINGS = [ + Binding("up", "nav(-11)", "Up", show=False), + Binding("down", "nav(11)", "Down", show=False), + Binding("left", "nav(-1)", "Left", show=False), + Binding("right", "nav(1)", "Right", show=False), + Binding("space", "toggle", "Select"), + Binding("enter", "edit", "Edit pad(s)"), + Binding("a", "select_all", "All"), + Binding("n", "select_none", "None"), + Binding("u", "cycle_unused", "Unused mode"), + Binding("p", "power", "Power"), + Binding("d", "done", "Save & exit"), + Binding("q", "quit_nosave", "Quit (no save)"), + ] + + def __init__(self, spec: Optional[Dict[str, Any]], project_name: str = ""): + super().__init__() + self.spec = copy.deepcopy(spec) if spec else empty_spec() + self.project_name = project_name + self.entries: Dict[int, Dict[str, Any]] = {e["pad"]: dict(e) for e in self.spec["gpio"]["pads"]} + self.cells: Dict[int, PadCell] = {} + self.current = 0 + + @property + def unused_mode(self) -> Optional[str]: + return self.spec["gpio"].get("unused_mode") + + def compose(self) -> ComposeResult: + yield Header(show_clock=False) + with Vertical(id="main"): + yield Label(f"Openframe GPIO configuration {self.project_name}".strip(), id="title") + with Grid(id="pad-grid"): + for pad in range(NUM_PADS): + cell = PadCell(pad, self.entries.get(pad), self.unused_mode) + self.cells[pad] = cell + yield cell + yield Label("", id="info") + yield Label("", id="status") + yield Footer() + + def on_mount(self) -> None: + self._highlight() + self._update_info() + + # -- helpers ----------------------------------------------------------- + def _highlight(self) -> None: + for pad, cell in self.cells.items(): + cell.set_class(pad == self.current, "current") + self.cells[self.current].scroll_visible() + + def _selected(self) -> List[int]: + chosen = [p for p, c in self.cells.items() if c.is_selected] + return chosen or [self.current] + + def _update_info(self) -> None: + power = self.spec["power"] + domains = ", ".join(power.get("domains", [])) or "[red]none[/red]" + macros = ", ".join(f"{m['instance']}->{m['domain']}" for m in power.get("macros", [])) or "none" + unused = self.unused_mode or "[red]unset: every pad must be configured[/red]" + self.query_one("#info", Label).update( + f"Unused pads (*): {unused} Power domains: {domains} Macros: {macros}" + ) + + def _set_status(self, text: str) -> None: + self.query_one("#status", Label).update(text) + + def _refresh_cells(self) -> None: + for pad, cell in self.cells.items(): + cell.entry = self.entries.get(pad) + cell.unused_mode = self.unused_mode + cell.refresh_view() + + def current_spec(self) -> Dict[str, Any]: + spec = copy.deepcopy(self.spec) + spec["gpio"]["pads"] = [self.entries[p] for p in sorted(self.entries)] + return spec + + # -- actions ----------------------------------------------------------- + def toggle_pad(self, pad: int) -> None: + self.current = pad + cell = self.cells[pad] + cell.is_selected = not cell.is_selected + cell.refresh_view() + self._highlight() + + def action_nav(self, delta: int) -> None: + target = self.current + delta + if 0 <= target < NUM_PADS: + self.current = target + self._highlight() + + def action_toggle(self) -> None: + self.toggle_pad(self.current) + + def action_select_all(self) -> None: + for cell in self.cells.values(): + cell.is_selected = True + cell.refresh_view() + + def action_select_none(self) -> None: + for cell in self.cells.values(): + cell.is_selected = False + cell.refresh_view() + + def action_cycle_unused(self) -> None: + order = [None] + list(UNUSED_MODES) + nxt = order[(order.index(self.unused_mode) + 1) % len(order)] + if nxt is None: + self.spec["gpio"].pop("unused_mode", None) + else: + self.spec["gpio"]["unused_mode"] = nxt + self._refresh_cells() + self._update_info() + + def action_edit(self) -> None: + pads = self._selected() + self.push_screen(PadEditScreen(pads, self.entries.get(pads[0])), lambda r: self._apply_edit(pads, r)) + + def _apply_edit(self, pads: List[int], result: Optional[Dict[str, Any]]) -> None: + if result is None: + return + if result.get("clear"): + for pad in pads: + self.entries.pop(pad, None) + else: + for i, pad in enumerate(sorted(pads)): + entry: Dict[str, Any] = {"pad": pad, "mode": result["mode"]} + if result["signal"]: + entry["signal"] = result["signal"] + if result["bit"] is not None: + entry["bit"] = result["bit"] + i if len(pads) > 1 else result["bit"] + if result["overrides"]: + entry["overrides"] = dict(result["overrides"]) + self.entries[pad] = entry + self.action_select_none() + self._refresh_cells() + self._set_status("") + + def action_power(self) -> None: + self.push_screen(PowerScreen(self.spec["power"]), self._apply_power) + + def _apply_power(self, result: Optional[Dict[str, Any]]) -> None: + if result is None: + return + self.spec["power"] = result + self._update_info() + + def action_done(self) -> None: + spec = self.current_spec() + try: + validate_spec(spec) + except SpecError as e: + shown = e.errors[:4] + more = f" (+{len(e.errors) - 4} more)" if len(e.errors) > 4 else "" + self._set_status("[red]Cannot save:[/red] " + "; ".join(shown) + more) + return + self.exit(result=spec) + + def action_quit_nosave(self) -> None: + self.exit(result=None) diff --git a/chipfoundry_cli/utils.py b/chipfoundry_cli/utils.py index 4def908..325b91f 100644 --- a/chipfoundry_cli/utils.py +++ b/chipfoundry_cli/utils.py @@ -794,4 +794,33 @@ def save_gpio_config_to_project_json(project_json_path: str, gpio_configs: Dict[ hex_config[str(gpio_num)] = mode_name data["project"]["gpio_config"] = hex_config + save_project_json(project_json_path, data) + + +def _no_duplicate_keys(pairs): + out = {} + for key, value in pairs: + if key in out: + raise ValueError(f"duplicate key '{key}'") + out[key] = value + return out + + +def get_openframe_spec_from_project_json(project_json_path: str) -> Optional[dict]: + """Return the raw project.openframe block, or None if the project has none. + + Duplicate JSON keys raise ValueError instead of silently keeping the last one. + """ + with open(project_json_path, 'r') as f: + data = json.load(f, object_pairs_hook=_no_duplicate_keys) + return data.get("project", {}).get("openframe") + + +def save_openframe_spec_to_project_json(project_json_path: str, spec_block: dict): + """Write the project.openframe block, leaving every other key (including gpio_config) untouched.""" + if Path(project_json_path).exists(): + data = load_project_json(project_json_path) + else: + data = {"project": {}} + data.setdefault("project", {})["openframe"] = spec_block save_project_json(project_json_path, data) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3f5a3f5..01a2fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chipfoundry-cli" -version = "2.6.0" +version = "2.7.0" description = "CLI tool to automate ChipFoundry project submission to SFTP server" authors = ["ChipFoundry "] readme = "README.md" @@ -17,6 +17,8 @@ paramiko = ">=3.0.0,<4" toml = ">=0.10,<1.0" httpx = ">=0.24.0,<1.0" textual = ">=0.40.0,<1" +pyyaml = ">=6.0,<7" +jsonschema = ">=4.0,<5" cf-ipm = "*" [tool.poetry.group.dev.dependencies] diff --git a/tests/data/CF_gpio_config.v b/tests/data/CF_gpio_config.v new file mode 100644 index 0000000..af46952 --- /dev/null +++ b/tests/data/CF_gpio_config.v @@ -0,0 +1,144 @@ +//============================================================================= +// Module: CF_gpio_config +// Description: Sky130 GPIO Pad Configuration Wrapper for Efabless Openframe +// Simple parameterized module - set MODE and forget about pad details +// License: Apache 2.0 +//============================================================================= + +`default_nettype none + +module CF_gpio_config #( + parameter [2:0] MODE = 3'd1, // 0=ANALOG, 1=INPUT, 2=INPUT_PD, 3=INPUT_PU, 4=OUTPUT, 5=BIDIR + //------------------------------------------------------------------------- + // Static pad control overrides (default 0 keeps the v1.1.x behavior) + //------------------------------------------------------------------------- + parameter [0:0] SLOW = 1'b0, // gpio_slow_sel: 0=fast slew, 1=slow slew + parameter [0:0] VTRIP = 1'b0, // gpio_vtrip_sel: 0=CMOS trip point, 1=LVTTL trip point + parameter [0:0] IB_MODE = 1'b0, // gpio_ib_mode_sel: input buffer mode select + parameter [0:0] HOLDOVER = 1'b0, // gpio_holdover: 1=hold pad state in sleep mode + parameter [0:0] ANALOG_EN = 1'b0, // gpio_analog_en: 1=enable analog mux connection + parameter [0:0] ANALOG_SEL = 1'b0, // gpio_analog_sel: 0=amuxbus_a, 1=amuxbus_b + parameter [0:0] ANALOG_POL = 1'b0 // gpio_analog_pol: analog mux polarity +)( + //------------------------------------------------------------------------- + // User Interface - Connect these to your design + //------------------------------------------------------------------------- + input wire io_out, // Data to drive pad (used in OUTPUT/BIDIR modes) + output wire io_in, // Data from pad (directly from gpio_in) + input wire io_oeb, // Output enable bar (BIDIR mode: 0=drive, 1=hi-z) + + //------------------------------------------------------------------------- + // Openframe Interface - From openframe_project_wrapper + //------------------------------------------------------------------------- + input wire gpio_zero, // Connection for 1'b0 + input wire gpio_one, // Connection for 1'b1 + input wire gpio_in, // Pad input (connect to gpio_in[n]) + + //------------------------------------------------------------------------- + // Openframe Interface - To openframe_project_wrapper pad config + //------------------------------------------------------------------------- + output wire [2:0] gpio_dm, // Drive mode {dm2, dm1, dm0} + output wire gpio_inp_dis, // Input disable + output wire gpio_oeb_out, // Output enable bar (active low) + output wire gpio_out_val, // Output value for gpio_out (includes pull mode values) + output wire gpio_analog_en, + output wire gpio_analog_sel, + output wire gpio_analog_pol, + output wire gpio_ib_mode_sel, + output wire gpio_vtrip_sel, + output wire gpio_slow_sel, + output wire gpio_holdover +); + + //------------------------------------------------------------------------- + // Mode Definitions + //------------------------------------------------------------------------- + localparam [2:0] MODE_ANALOG = 3'd0; // Analog mode - input/output disabled + localparam [2:0] MODE_INPUT = 3'd1; // Digital input, no pull resistor + localparam [2:0] MODE_INPUT_PD = 3'd2; // Digital input with pull-down + localparam [2:0] MODE_INPUT_PU = 3'd3; // Digital input with pull-up + localparam [2:0] MODE_OUTPUT = 3'd4; // Digital output, push-pull + localparam [2:0] MODE_BIDIR = 3'd5; // Bidirectional, direction from io_oeb + + //------------------------------------------------------------------------- + // Drive Mode (dm[2:0]) - Based on Sky130 pad behavioral model + //------------------------------------------------------------------------- + // The Sky130 GPIO pad uses bufif1 primitives with specific strengths: + // dm=110: bufif1(strong1, strong0) - strong 1, strong 0 + // dm=111: bufif1(pull1, pull0) - weak 1, weak 0 + // + // For weak pull behavior: + // - Pull-DOWN (read 0 when floating): dm=111 with out=0 gives weak pull to 0 + // - Pull-UP (read 1 when floating): dm=111 with out=1 gives weak pull to 1 + // + // MODE_ANALOG: dm=000 (Hi-Z, analog mode) + // MODE_INPUT: dm=001 (Input only, no pull) + // MODE_INPUT_PD: dm=111 (Weak pull to 0, requires oeb=0 and out=0) + // MODE_INPUT_PU: dm=111 (Weak pull to 1, requires oeb=0 and out=1) + // MODE_OUTPUT: dm=110 (Strong push-pull) + // MODE_BIDIR: dm=110 (Strong push-pull, direction from io_oeb) + //------------------------------------------------------------------------- + assign gpio_dm = (MODE == MODE_ANALOG) ? {gpio_zero, gpio_zero, gpio_zero} : // 3'b000 + (MODE == MODE_INPUT) ? {gpio_zero, gpio_zero, gpio_one } : // 3'b001 + (MODE == MODE_INPUT_PD) ? {gpio_one, gpio_one, gpio_one } : // 3'b111 Weak output + (MODE == MODE_INPUT_PU) ? {gpio_one, gpio_one, gpio_one } : // 3'b111 Weak output + (MODE == MODE_OUTPUT) ? {gpio_one, gpio_one, gpio_zero} : // 3'b110 Strong output + (MODE == MODE_BIDIR) ? {gpio_one, gpio_one, gpio_zero} : // 3'b110 Strong output + {gpio_zero, gpio_zero, gpio_one }; // 3'b001 Default: INPUT + + //------------------------------------------------------------------------- + // Input Disable + //------------------------------------------------------------------------- + // Disable input buffer for ANALOG and OUTPUT modes (not reading pad) + assign gpio_inp_dis = (MODE == MODE_ANALOG) ? gpio_one : // Disable input buffer for analog mode + (MODE == MODE_OUTPUT) ? gpio_one : // Disable input buffer for output mode + gpio_zero; // Enable input buffer + + //------------------------------------------------------------------------- + // Output Enable Bar (active low: 0=driving, 1=hi-z) + //------------------------------------------------------------------------- + // OUTPUT: always driving (oeb=0) + // BIDIR: controlled by user's io_oeb signal + // INPUT_PD: must drive to enable weak pull (oeb=0) + // INPUT_PU: must drive to enable weak pull (oeb=0) + // Others: always hi-z (oeb=1) + assign gpio_oeb_out = (MODE == MODE_OUTPUT) ? gpio_zero : + (MODE == MODE_BIDIR) ? io_oeb : + (MODE == MODE_INPUT_PD) ? gpio_zero : // Enable weak pull driver + (MODE == MODE_INPUT_PU) ? gpio_zero : // Enable weak pull driver + gpio_one ; + + //------------------------------------------------------------------------- + // Static Configuration (parameter overrides, default 0 for all modes) + //------------------------------------------------------------------------- + // Tied to the pad loopback constants, never to literal 1'b0/1'b1. + assign gpio_analog_en = ANALOG_EN ? gpio_one : gpio_zero; // Enable amuxbus_a/b for ground/power + assign gpio_analog_sel = ANALOG_SEL ? gpio_one : gpio_zero; // Choose amuxbus_a (0) or amuxbus_b (1) + assign gpio_analog_pol = ANALOG_POL ? gpio_one : gpio_zero; // Use amuxbus_a/b as ground (0) or power (1) + assign gpio_ib_mode_sel = IB_MODE ? gpio_one : gpio_zero; // Input buffer mode (0: VDDIO) + assign gpio_vtrip_sel = VTRIP ? gpio_one : gpio_zero; // Trip point (0: CMOS) + assign gpio_slow_sel = SLOW ? gpio_one : gpio_zero; // Slew rate (0: fast) + assign gpio_holdover = HOLDOVER ? gpio_one : gpio_zero; // Holdover (0: none) + + //------------------------------------------------------------------------- + // Output Value + //------------------------------------------------------------------------- + // For pull modes, drive the correct value to activate weak pull: + // INPUT_PD: out=0 to get weak pull to 0 + // INPUT_PU: out=1 to get weak pull to 1 + // For OUTPUT/BIDIR: pass through user's io_out + // For others: drive 0 (doesn't matter since oeb=1) + assign gpio_out_val = (MODE == MODE_OUTPUT) ? io_out : + (MODE == MODE_BIDIR) ? io_out : + (MODE == MODE_INPUT_PD) ? gpio_zero : // Drive 0 for weak pull-down + (MODE == MODE_INPUT_PU) ? gpio_one : // Drive 1 for weak pull-up + gpio_zero ; + + //------------------------------------------------------------------------- + // Input Passthrough + //------------------------------------------------------------------------- + assign io_in = gpio_in; + +endmodule + +`default_nettype wire diff --git a/tests/data/openframe_gpio_timer.v b/tests/data/openframe_gpio_timer.v new file mode 100644 index 0000000..e52a88f --- /dev/null +++ b/tests/data/openframe_gpio_timer.v @@ -0,0 +1,1101 @@ +// ----------------------------------------------------------------------------- +// openframe_gpio +// +// GENERATED FILE - DO NOT EDIT. +// Generated by `cf openframe generate` from the +// project.openframe block in .cf/project.json. Edit the spec with +// `cf gpio-config` or `cf openframe import`, then regenerate. +// spec-sha256: 151733931264b0fe1416e572d7fd11d76a4effdec5bf42e1cd0eb14fcdb1db11 +// +// Pad map: +// pad 0 input clk +// pad 1 input_pu rst +// pad 2 output out[0] +// pad 3 output out[1] +// pad 4 output out[2] +// pad 5 output out[3] +// pad 6 output out[4] +// pad 7 output out[5] +// pad 8 output out[6] +// pad 9 output out[7] +// pad 10 output out[8] +// pad 11 output out[9] +// pad 12 output out[10] +// pad 13 analog (unused) +// pad 14 analog (unused) +// pad 15 analog (unused) +// pad 16 analog (unused) +// pad 17 analog (unused) +// pad 18 analog (unused) +// pad 19 analog (unused) +// pad 20 analog (unused) +// pad 21 analog (unused) +// pad 22 analog (unused) +// pad 23 analog (unused) +// pad 24 analog (unused) +// pad 25 analog (unused) +// pad 26 analog (unused) +// pad 27 analog (unused) +// pad 28 analog (unused) +// pad 29 analog (unused) +// pad 30 analog (unused) +// pad 31 analog (unused) +// pad 32 analog (unused) +// pad 33 analog (unused) +// pad 34 analog (unused) +// pad 35 analog (unused) +// pad 36 analog (unused) +// pad 37 analog (unused) +// pad 38 analog (unused) +// pad 39 analog (unused) +// pad 40 analog (unused) +// pad 41 analog (unused) +// pad 42 analog (unused) +// pad 43 analog (unused) +// ----------------------------------------------------------------------------- + +`default_nettype none + +// io_in is left open on pads that have no input signal. +/* verilator lint_off PINCONNECTEMPTY */ +module openframe_gpio ( + // User design ports + output wire clk, + output wire rst, + input wire [10:0] out, + + // Openframe pad interface: connect to the openframe_project_wrapper ports of the same name + input wire [43:0] gpio_in, + output wire [43:0] gpio_out, + output wire [43:0] gpio_oeb, + output wire [43:0] gpio_inp_dis, + output wire [43:0] gpio_ib_mode_sel, + output wire [43:0] gpio_vtrip_sel, + output wire [43:0] gpio_slow_sel, + output wire [43:0] gpio_holdover, + output wire [43:0] gpio_analog_en, + output wire [43:0] gpio_analog_sel, + output wire [43:0] gpio_analog_pol, + output wire [43:0] gpio_dm2, + output wire [43:0] gpio_dm1, + output wire [43:0] gpio_dm0, + input wire [43:0] gpio_loopback_one, + input wire [43:0] gpio_loopback_zero +); + + // pad 0: input clk + CF_gpio_config #( + .MODE(3'd1) + ) gpio_cfg_00 ( + .io_out (gpio_loopback_zero[0]), + .io_in (clk), + .io_oeb (gpio_loopback_one[0]), + .gpio_zero (gpio_loopback_zero[0]), + .gpio_one (gpio_loopback_one[0]), + .gpio_in (gpio_in[0]), + .gpio_dm ({gpio_dm2[0], gpio_dm1[0], gpio_dm0[0]}), + .gpio_inp_dis (gpio_inp_dis[0]), + .gpio_oeb_out (gpio_oeb[0]), + .gpio_out_val (gpio_out[0]), + .gpio_analog_en (gpio_analog_en[0]), + .gpio_analog_sel (gpio_analog_sel[0]), + .gpio_analog_pol (gpio_analog_pol[0]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[0]), + .gpio_vtrip_sel (gpio_vtrip_sel[0]), + .gpio_slow_sel (gpio_slow_sel[0]), + .gpio_holdover (gpio_holdover[0]) + ); + + // pad 1: input_pu rst + CF_gpio_config #( + .MODE(3'd3) + ) gpio_cfg_01 ( + .io_out (gpio_loopback_zero[1]), + .io_in (rst), + .io_oeb (gpio_loopback_one[1]), + .gpio_zero (gpio_loopback_zero[1]), + .gpio_one (gpio_loopback_one[1]), + .gpio_in (gpio_in[1]), + .gpio_dm ({gpio_dm2[1], gpio_dm1[1], gpio_dm0[1]}), + .gpio_inp_dis (gpio_inp_dis[1]), + .gpio_oeb_out (gpio_oeb[1]), + .gpio_out_val (gpio_out[1]), + .gpio_analog_en (gpio_analog_en[1]), + .gpio_analog_sel (gpio_analog_sel[1]), + .gpio_analog_pol (gpio_analog_pol[1]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[1]), + .gpio_vtrip_sel (gpio_vtrip_sel[1]), + .gpio_slow_sel (gpio_slow_sel[1]), + .gpio_holdover (gpio_holdover[1]) + ); + + // pad 2: output out[0] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_02 ( + .io_out (out[0]), + .io_in (), + .io_oeb (gpio_loopback_one[2]), + .gpio_zero (gpio_loopback_zero[2]), + .gpio_one (gpio_loopback_one[2]), + .gpio_in (gpio_in[2]), + .gpio_dm ({gpio_dm2[2], gpio_dm1[2], gpio_dm0[2]}), + .gpio_inp_dis (gpio_inp_dis[2]), + .gpio_oeb_out (gpio_oeb[2]), + .gpio_out_val (gpio_out[2]), + .gpio_analog_en (gpio_analog_en[2]), + .gpio_analog_sel (gpio_analog_sel[2]), + .gpio_analog_pol (gpio_analog_pol[2]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[2]), + .gpio_vtrip_sel (gpio_vtrip_sel[2]), + .gpio_slow_sel (gpio_slow_sel[2]), + .gpio_holdover (gpio_holdover[2]) + ); + + // pad 3: output out[1] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_03 ( + .io_out (out[1]), + .io_in (), + .io_oeb (gpio_loopback_one[3]), + .gpio_zero (gpio_loopback_zero[3]), + .gpio_one (gpio_loopback_one[3]), + .gpio_in (gpio_in[3]), + .gpio_dm ({gpio_dm2[3], gpio_dm1[3], gpio_dm0[3]}), + .gpio_inp_dis (gpio_inp_dis[3]), + .gpio_oeb_out (gpio_oeb[3]), + .gpio_out_val (gpio_out[3]), + .gpio_analog_en (gpio_analog_en[3]), + .gpio_analog_sel (gpio_analog_sel[3]), + .gpio_analog_pol (gpio_analog_pol[3]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[3]), + .gpio_vtrip_sel (gpio_vtrip_sel[3]), + .gpio_slow_sel (gpio_slow_sel[3]), + .gpio_holdover (gpio_holdover[3]) + ); + + // pad 4: output out[2] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_04 ( + .io_out (out[2]), + .io_in (), + .io_oeb (gpio_loopback_one[4]), + .gpio_zero (gpio_loopback_zero[4]), + .gpio_one (gpio_loopback_one[4]), + .gpio_in (gpio_in[4]), + .gpio_dm ({gpio_dm2[4], gpio_dm1[4], gpio_dm0[4]}), + .gpio_inp_dis (gpio_inp_dis[4]), + .gpio_oeb_out (gpio_oeb[4]), + .gpio_out_val (gpio_out[4]), + .gpio_analog_en (gpio_analog_en[4]), + .gpio_analog_sel (gpio_analog_sel[4]), + .gpio_analog_pol (gpio_analog_pol[4]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[4]), + .gpio_vtrip_sel (gpio_vtrip_sel[4]), + .gpio_slow_sel (gpio_slow_sel[4]), + .gpio_holdover (gpio_holdover[4]) + ); + + // pad 5: output out[3] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_05 ( + .io_out (out[3]), + .io_in (), + .io_oeb (gpio_loopback_one[5]), + .gpio_zero (gpio_loopback_zero[5]), + .gpio_one (gpio_loopback_one[5]), + .gpio_in (gpio_in[5]), + .gpio_dm ({gpio_dm2[5], gpio_dm1[5], gpio_dm0[5]}), + .gpio_inp_dis (gpio_inp_dis[5]), + .gpio_oeb_out (gpio_oeb[5]), + .gpio_out_val (gpio_out[5]), + .gpio_analog_en (gpio_analog_en[5]), + .gpio_analog_sel (gpio_analog_sel[5]), + .gpio_analog_pol (gpio_analog_pol[5]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[5]), + .gpio_vtrip_sel (gpio_vtrip_sel[5]), + .gpio_slow_sel (gpio_slow_sel[5]), + .gpio_holdover (gpio_holdover[5]) + ); + + // pad 6: output out[4] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_06 ( + .io_out (out[4]), + .io_in (), + .io_oeb (gpio_loopback_one[6]), + .gpio_zero (gpio_loopback_zero[6]), + .gpio_one (gpio_loopback_one[6]), + .gpio_in (gpio_in[6]), + .gpio_dm ({gpio_dm2[6], gpio_dm1[6], gpio_dm0[6]}), + .gpio_inp_dis (gpio_inp_dis[6]), + .gpio_oeb_out (gpio_oeb[6]), + .gpio_out_val (gpio_out[6]), + .gpio_analog_en (gpio_analog_en[6]), + .gpio_analog_sel (gpio_analog_sel[6]), + .gpio_analog_pol (gpio_analog_pol[6]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[6]), + .gpio_vtrip_sel (gpio_vtrip_sel[6]), + .gpio_slow_sel (gpio_slow_sel[6]), + .gpio_holdover (gpio_holdover[6]) + ); + + // pad 7: output out[5] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_07 ( + .io_out (out[5]), + .io_in (), + .io_oeb (gpio_loopback_one[7]), + .gpio_zero (gpio_loopback_zero[7]), + .gpio_one (gpio_loopback_one[7]), + .gpio_in (gpio_in[7]), + .gpio_dm ({gpio_dm2[7], gpio_dm1[7], gpio_dm0[7]}), + .gpio_inp_dis (gpio_inp_dis[7]), + .gpio_oeb_out (gpio_oeb[7]), + .gpio_out_val (gpio_out[7]), + .gpio_analog_en (gpio_analog_en[7]), + .gpio_analog_sel (gpio_analog_sel[7]), + .gpio_analog_pol (gpio_analog_pol[7]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[7]), + .gpio_vtrip_sel (gpio_vtrip_sel[7]), + .gpio_slow_sel (gpio_slow_sel[7]), + .gpio_holdover (gpio_holdover[7]) + ); + + // pad 8: output out[6] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_08 ( + .io_out (out[6]), + .io_in (), + .io_oeb (gpio_loopback_one[8]), + .gpio_zero (gpio_loopback_zero[8]), + .gpio_one (gpio_loopback_one[8]), + .gpio_in (gpio_in[8]), + .gpio_dm ({gpio_dm2[8], gpio_dm1[8], gpio_dm0[8]}), + .gpio_inp_dis (gpio_inp_dis[8]), + .gpio_oeb_out (gpio_oeb[8]), + .gpio_out_val (gpio_out[8]), + .gpio_analog_en (gpio_analog_en[8]), + .gpio_analog_sel (gpio_analog_sel[8]), + .gpio_analog_pol (gpio_analog_pol[8]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[8]), + .gpio_vtrip_sel (gpio_vtrip_sel[8]), + .gpio_slow_sel (gpio_slow_sel[8]), + .gpio_holdover (gpio_holdover[8]) + ); + + // pad 9: output out[7] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_09 ( + .io_out (out[7]), + .io_in (), + .io_oeb (gpio_loopback_one[9]), + .gpio_zero (gpio_loopback_zero[9]), + .gpio_one (gpio_loopback_one[9]), + .gpio_in (gpio_in[9]), + .gpio_dm ({gpio_dm2[9], gpio_dm1[9], gpio_dm0[9]}), + .gpio_inp_dis (gpio_inp_dis[9]), + .gpio_oeb_out (gpio_oeb[9]), + .gpio_out_val (gpio_out[9]), + .gpio_analog_en (gpio_analog_en[9]), + .gpio_analog_sel (gpio_analog_sel[9]), + .gpio_analog_pol (gpio_analog_pol[9]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[9]), + .gpio_vtrip_sel (gpio_vtrip_sel[9]), + .gpio_slow_sel (gpio_slow_sel[9]), + .gpio_holdover (gpio_holdover[9]) + ); + + // pad 10: output out[8] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_10 ( + .io_out (out[8]), + .io_in (), + .io_oeb (gpio_loopback_one[10]), + .gpio_zero (gpio_loopback_zero[10]), + .gpio_one (gpio_loopback_one[10]), + .gpio_in (gpio_in[10]), + .gpio_dm ({gpio_dm2[10], gpio_dm1[10], gpio_dm0[10]}), + .gpio_inp_dis (gpio_inp_dis[10]), + .gpio_oeb_out (gpio_oeb[10]), + .gpio_out_val (gpio_out[10]), + .gpio_analog_en (gpio_analog_en[10]), + .gpio_analog_sel (gpio_analog_sel[10]), + .gpio_analog_pol (gpio_analog_pol[10]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[10]), + .gpio_vtrip_sel (gpio_vtrip_sel[10]), + .gpio_slow_sel (gpio_slow_sel[10]), + .gpio_holdover (gpio_holdover[10]) + ); + + // pad 11: output out[9] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_11 ( + .io_out (out[9]), + .io_in (), + .io_oeb (gpio_loopback_one[11]), + .gpio_zero (gpio_loopback_zero[11]), + .gpio_one (gpio_loopback_one[11]), + .gpio_in (gpio_in[11]), + .gpio_dm ({gpio_dm2[11], gpio_dm1[11], gpio_dm0[11]}), + .gpio_inp_dis (gpio_inp_dis[11]), + .gpio_oeb_out (gpio_oeb[11]), + .gpio_out_val (gpio_out[11]), + .gpio_analog_en (gpio_analog_en[11]), + .gpio_analog_sel (gpio_analog_sel[11]), + .gpio_analog_pol (gpio_analog_pol[11]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[11]), + .gpio_vtrip_sel (gpio_vtrip_sel[11]), + .gpio_slow_sel (gpio_slow_sel[11]), + .gpio_holdover (gpio_holdover[11]) + ); + + // pad 12: output out[10] + CF_gpio_config #( + .MODE(3'd4) + ) gpio_cfg_12 ( + .io_out (out[10]), + .io_in (), + .io_oeb (gpio_loopback_one[12]), + .gpio_zero (gpio_loopback_zero[12]), + .gpio_one (gpio_loopback_one[12]), + .gpio_in (gpio_in[12]), + .gpio_dm ({gpio_dm2[12], gpio_dm1[12], gpio_dm0[12]}), + .gpio_inp_dis (gpio_inp_dis[12]), + .gpio_oeb_out (gpio_oeb[12]), + .gpio_out_val (gpio_out[12]), + .gpio_analog_en (gpio_analog_en[12]), + .gpio_analog_sel (gpio_analog_sel[12]), + .gpio_analog_pol (gpio_analog_pol[12]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[12]), + .gpio_vtrip_sel (gpio_vtrip_sel[12]), + .gpio_slow_sel (gpio_slow_sel[12]), + .gpio_holdover (gpio_holdover[12]) + ); + + // pad 13: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_13 ( + .io_out (gpio_loopback_zero[13]), + .io_in (), + .io_oeb (gpio_loopback_one[13]), + .gpio_zero (gpio_loopback_zero[13]), + .gpio_one (gpio_loopback_one[13]), + .gpio_in (gpio_in[13]), + .gpio_dm ({gpio_dm2[13], gpio_dm1[13], gpio_dm0[13]}), + .gpio_inp_dis (gpio_inp_dis[13]), + .gpio_oeb_out (gpio_oeb[13]), + .gpio_out_val (gpio_out[13]), + .gpio_analog_en (gpio_analog_en[13]), + .gpio_analog_sel (gpio_analog_sel[13]), + .gpio_analog_pol (gpio_analog_pol[13]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[13]), + .gpio_vtrip_sel (gpio_vtrip_sel[13]), + .gpio_slow_sel (gpio_slow_sel[13]), + .gpio_holdover (gpio_holdover[13]) + ); + + // pad 14: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_14 ( + .io_out (gpio_loopback_zero[14]), + .io_in (), + .io_oeb (gpio_loopback_one[14]), + .gpio_zero (gpio_loopback_zero[14]), + .gpio_one (gpio_loopback_one[14]), + .gpio_in (gpio_in[14]), + .gpio_dm ({gpio_dm2[14], gpio_dm1[14], gpio_dm0[14]}), + .gpio_inp_dis (gpio_inp_dis[14]), + .gpio_oeb_out (gpio_oeb[14]), + .gpio_out_val (gpio_out[14]), + .gpio_analog_en (gpio_analog_en[14]), + .gpio_analog_sel (gpio_analog_sel[14]), + .gpio_analog_pol (gpio_analog_pol[14]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[14]), + .gpio_vtrip_sel (gpio_vtrip_sel[14]), + .gpio_slow_sel (gpio_slow_sel[14]), + .gpio_holdover (gpio_holdover[14]) + ); + + // pad 15: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_15 ( + .io_out (gpio_loopback_zero[15]), + .io_in (), + .io_oeb (gpio_loopback_one[15]), + .gpio_zero (gpio_loopback_zero[15]), + .gpio_one (gpio_loopback_one[15]), + .gpio_in (gpio_in[15]), + .gpio_dm ({gpio_dm2[15], gpio_dm1[15], gpio_dm0[15]}), + .gpio_inp_dis (gpio_inp_dis[15]), + .gpio_oeb_out (gpio_oeb[15]), + .gpio_out_val (gpio_out[15]), + .gpio_analog_en (gpio_analog_en[15]), + .gpio_analog_sel (gpio_analog_sel[15]), + .gpio_analog_pol (gpio_analog_pol[15]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[15]), + .gpio_vtrip_sel (gpio_vtrip_sel[15]), + .gpio_slow_sel (gpio_slow_sel[15]), + .gpio_holdover (gpio_holdover[15]) + ); + + // pad 16: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_16 ( + .io_out (gpio_loopback_zero[16]), + .io_in (), + .io_oeb (gpio_loopback_one[16]), + .gpio_zero (gpio_loopback_zero[16]), + .gpio_one (gpio_loopback_one[16]), + .gpio_in (gpio_in[16]), + .gpio_dm ({gpio_dm2[16], gpio_dm1[16], gpio_dm0[16]}), + .gpio_inp_dis (gpio_inp_dis[16]), + .gpio_oeb_out (gpio_oeb[16]), + .gpio_out_val (gpio_out[16]), + .gpio_analog_en (gpio_analog_en[16]), + .gpio_analog_sel (gpio_analog_sel[16]), + .gpio_analog_pol (gpio_analog_pol[16]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[16]), + .gpio_vtrip_sel (gpio_vtrip_sel[16]), + .gpio_slow_sel (gpio_slow_sel[16]), + .gpio_holdover (gpio_holdover[16]) + ); + + // pad 17: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_17 ( + .io_out (gpio_loopback_zero[17]), + .io_in (), + .io_oeb (gpio_loopback_one[17]), + .gpio_zero (gpio_loopback_zero[17]), + .gpio_one (gpio_loopback_one[17]), + .gpio_in (gpio_in[17]), + .gpio_dm ({gpio_dm2[17], gpio_dm1[17], gpio_dm0[17]}), + .gpio_inp_dis (gpio_inp_dis[17]), + .gpio_oeb_out (gpio_oeb[17]), + .gpio_out_val (gpio_out[17]), + .gpio_analog_en (gpio_analog_en[17]), + .gpio_analog_sel (gpio_analog_sel[17]), + .gpio_analog_pol (gpio_analog_pol[17]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[17]), + .gpio_vtrip_sel (gpio_vtrip_sel[17]), + .gpio_slow_sel (gpio_slow_sel[17]), + .gpio_holdover (gpio_holdover[17]) + ); + + // pad 18: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_18 ( + .io_out (gpio_loopback_zero[18]), + .io_in (), + .io_oeb (gpio_loopback_one[18]), + .gpio_zero (gpio_loopback_zero[18]), + .gpio_one (gpio_loopback_one[18]), + .gpio_in (gpio_in[18]), + .gpio_dm ({gpio_dm2[18], gpio_dm1[18], gpio_dm0[18]}), + .gpio_inp_dis (gpio_inp_dis[18]), + .gpio_oeb_out (gpio_oeb[18]), + .gpio_out_val (gpio_out[18]), + .gpio_analog_en (gpio_analog_en[18]), + .gpio_analog_sel (gpio_analog_sel[18]), + .gpio_analog_pol (gpio_analog_pol[18]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[18]), + .gpio_vtrip_sel (gpio_vtrip_sel[18]), + .gpio_slow_sel (gpio_slow_sel[18]), + .gpio_holdover (gpio_holdover[18]) + ); + + // pad 19: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_19 ( + .io_out (gpio_loopback_zero[19]), + .io_in (), + .io_oeb (gpio_loopback_one[19]), + .gpio_zero (gpio_loopback_zero[19]), + .gpio_one (gpio_loopback_one[19]), + .gpio_in (gpio_in[19]), + .gpio_dm ({gpio_dm2[19], gpio_dm1[19], gpio_dm0[19]}), + .gpio_inp_dis (gpio_inp_dis[19]), + .gpio_oeb_out (gpio_oeb[19]), + .gpio_out_val (gpio_out[19]), + .gpio_analog_en (gpio_analog_en[19]), + .gpio_analog_sel (gpio_analog_sel[19]), + .gpio_analog_pol (gpio_analog_pol[19]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[19]), + .gpio_vtrip_sel (gpio_vtrip_sel[19]), + .gpio_slow_sel (gpio_slow_sel[19]), + .gpio_holdover (gpio_holdover[19]) + ); + + // pad 20: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_20 ( + .io_out (gpio_loopback_zero[20]), + .io_in (), + .io_oeb (gpio_loopback_one[20]), + .gpio_zero (gpio_loopback_zero[20]), + .gpio_one (gpio_loopback_one[20]), + .gpio_in (gpio_in[20]), + .gpio_dm ({gpio_dm2[20], gpio_dm1[20], gpio_dm0[20]}), + .gpio_inp_dis (gpio_inp_dis[20]), + .gpio_oeb_out (gpio_oeb[20]), + .gpio_out_val (gpio_out[20]), + .gpio_analog_en (gpio_analog_en[20]), + .gpio_analog_sel (gpio_analog_sel[20]), + .gpio_analog_pol (gpio_analog_pol[20]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[20]), + .gpio_vtrip_sel (gpio_vtrip_sel[20]), + .gpio_slow_sel (gpio_slow_sel[20]), + .gpio_holdover (gpio_holdover[20]) + ); + + // pad 21: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_21 ( + .io_out (gpio_loopback_zero[21]), + .io_in (), + .io_oeb (gpio_loopback_one[21]), + .gpio_zero (gpio_loopback_zero[21]), + .gpio_one (gpio_loopback_one[21]), + .gpio_in (gpio_in[21]), + .gpio_dm ({gpio_dm2[21], gpio_dm1[21], gpio_dm0[21]}), + .gpio_inp_dis (gpio_inp_dis[21]), + .gpio_oeb_out (gpio_oeb[21]), + .gpio_out_val (gpio_out[21]), + .gpio_analog_en (gpio_analog_en[21]), + .gpio_analog_sel (gpio_analog_sel[21]), + .gpio_analog_pol (gpio_analog_pol[21]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[21]), + .gpio_vtrip_sel (gpio_vtrip_sel[21]), + .gpio_slow_sel (gpio_slow_sel[21]), + .gpio_holdover (gpio_holdover[21]) + ); + + // pad 22: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_22 ( + .io_out (gpio_loopback_zero[22]), + .io_in (), + .io_oeb (gpio_loopback_one[22]), + .gpio_zero (gpio_loopback_zero[22]), + .gpio_one (gpio_loopback_one[22]), + .gpio_in (gpio_in[22]), + .gpio_dm ({gpio_dm2[22], gpio_dm1[22], gpio_dm0[22]}), + .gpio_inp_dis (gpio_inp_dis[22]), + .gpio_oeb_out (gpio_oeb[22]), + .gpio_out_val (gpio_out[22]), + .gpio_analog_en (gpio_analog_en[22]), + .gpio_analog_sel (gpio_analog_sel[22]), + .gpio_analog_pol (gpio_analog_pol[22]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[22]), + .gpio_vtrip_sel (gpio_vtrip_sel[22]), + .gpio_slow_sel (gpio_slow_sel[22]), + .gpio_holdover (gpio_holdover[22]) + ); + + // pad 23: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_23 ( + .io_out (gpio_loopback_zero[23]), + .io_in (), + .io_oeb (gpio_loopback_one[23]), + .gpio_zero (gpio_loopback_zero[23]), + .gpio_one (gpio_loopback_one[23]), + .gpio_in (gpio_in[23]), + .gpio_dm ({gpio_dm2[23], gpio_dm1[23], gpio_dm0[23]}), + .gpio_inp_dis (gpio_inp_dis[23]), + .gpio_oeb_out (gpio_oeb[23]), + .gpio_out_val (gpio_out[23]), + .gpio_analog_en (gpio_analog_en[23]), + .gpio_analog_sel (gpio_analog_sel[23]), + .gpio_analog_pol (gpio_analog_pol[23]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[23]), + .gpio_vtrip_sel (gpio_vtrip_sel[23]), + .gpio_slow_sel (gpio_slow_sel[23]), + .gpio_holdover (gpio_holdover[23]) + ); + + // pad 24: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_24 ( + .io_out (gpio_loopback_zero[24]), + .io_in (), + .io_oeb (gpio_loopback_one[24]), + .gpio_zero (gpio_loopback_zero[24]), + .gpio_one (gpio_loopback_one[24]), + .gpio_in (gpio_in[24]), + .gpio_dm ({gpio_dm2[24], gpio_dm1[24], gpio_dm0[24]}), + .gpio_inp_dis (gpio_inp_dis[24]), + .gpio_oeb_out (gpio_oeb[24]), + .gpio_out_val (gpio_out[24]), + .gpio_analog_en (gpio_analog_en[24]), + .gpio_analog_sel (gpio_analog_sel[24]), + .gpio_analog_pol (gpio_analog_pol[24]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[24]), + .gpio_vtrip_sel (gpio_vtrip_sel[24]), + .gpio_slow_sel (gpio_slow_sel[24]), + .gpio_holdover (gpio_holdover[24]) + ); + + // pad 25: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_25 ( + .io_out (gpio_loopback_zero[25]), + .io_in (), + .io_oeb (gpio_loopback_one[25]), + .gpio_zero (gpio_loopback_zero[25]), + .gpio_one (gpio_loopback_one[25]), + .gpio_in (gpio_in[25]), + .gpio_dm ({gpio_dm2[25], gpio_dm1[25], gpio_dm0[25]}), + .gpio_inp_dis (gpio_inp_dis[25]), + .gpio_oeb_out (gpio_oeb[25]), + .gpio_out_val (gpio_out[25]), + .gpio_analog_en (gpio_analog_en[25]), + .gpio_analog_sel (gpio_analog_sel[25]), + .gpio_analog_pol (gpio_analog_pol[25]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[25]), + .gpio_vtrip_sel (gpio_vtrip_sel[25]), + .gpio_slow_sel (gpio_slow_sel[25]), + .gpio_holdover (gpio_holdover[25]) + ); + + // pad 26: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_26 ( + .io_out (gpio_loopback_zero[26]), + .io_in (), + .io_oeb (gpio_loopback_one[26]), + .gpio_zero (gpio_loopback_zero[26]), + .gpio_one (gpio_loopback_one[26]), + .gpio_in (gpio_in[26]), + .gpio_dm ({gpio_dm2[26], gpio_dm1[26], gpio_dm0[26]}), + .gpio_inp_dis (gpio_inp_dis[26]), + .gpio_oeb_out (gpio_oeb[26]), + .gpio_out_val (gpio_out[26]), + .gpio_analog_en (gpio_analog_en[26]), + .gpio_analog_sel (gpio_analog_sel[26]), + .gpio_analog_pol (gpio_analog_pol[26]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[26]), + .gpio_vtrip_sel (gpio_vtrip_sel[26]), + .gpio_slow_sel (gpio_slow_sel[26]), + .gpio_holdover (gpio_holdover[26]) + ); + + // pad 27: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_27 ( + .io_out (gpio_loopback_zero[27]), + .io_in (), + .io_oeb (gpio_loopback_one[27]), + .gpio_zero (gpio_loopback_zero[27]), + .gpio_one (gpio_loopback_one[27]), + .gpio_in (gpio_in[27]), + .gpio_dm ({gpio_dm2[27], gpio_dm1[27], gpio_dm0[27]}), + .gpio_inp_dis (gpio_inp_dis[27]), + .gpio_oeb_out (gpio_oeb[27]), + .gpio_out_val (gpio_out[27]), + .gpio_analog_en (gpio_analog_en[27]), + .gpio_analog_sel (gpio_analog_sel[27]), + .gpio_analog_pol (gpio_analog_pol[27]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[27]), + .gpio_vtrip_sel (gpio_vtrip_sel[27]), + .gpio_slow_sel (gpio_slow_sel[27]), + .gpio_holdover (gpio_holdover[27]) + ); + + // pad 28: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_28 ( + .io_out (gpio_loopback_zero[28]), + .io_in (), + .io_oeb (gpio_loopback_one[28]), + .gpio_zero (gpio_loopback_zero[28]), + .gpio_one (gpio_loopback_one[28]), + .gpio_in (gpio_in[28]), + .gpio_dm ({gpio_dm2[28], gpio_dm1[28], gpio_dm0[28]}), + .gpio_inp_dis (gpio_inp_dis[28]), + .gpio_oeb_out (gpio_oeb[28]), + .gpio_out_val (gpio_out[28]), + .gpio_analog_en (gpio_analog_en[28]), + .gpio_analog_sel (gpio_analog_sel[28]), + .gpio_analog_pol (gpio_analog_pol[28]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[28]), + .gpio_vtrip_sel (gpio_vtrip_sel[28]), + .gpio_slow_sel (gpio_slow_sel[28]), + .gpio_holdover (gpio_holdover[28]) + ); + + // pad 29: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_29 ( + .io_out (gpio_loopback_zero[29]), + .io_in (), + .io_oeb (gpio_loopback_one[29]), + .gpio_zero (gpio_loopback_zero[29]), + .gpio_one (gpio_loopback_one[29]), + .gpio_in (gpio_in[29]), + .gpio_dm ({gpio_dm2[29], gpio_dm1[29], gpio_dm0[29]}), + .gpio_inp_dis (gpio_inp_dis[29]), + .gpio_oeb_out (gpio_oeb[29]), + .gpio_out_val (gpio_out[29]), + .gpio_analog_en (gpio_analog_en[29]), + .gpio_analog_sel (gpio_analog_sel[29]), + .gpio_analog_pol (gpio_analog_pol[29]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[29]), + .gpio_vtrip_sel (gpio_vtrip_sel[29]), + .gpio_slow_sel (gpio_slow_sel[29]), + .gpio_holdover (gpio_holdover[29]) + ); + + // pad 30: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_30 ( + .io_out (gpio_loopback_zero[30]), + .io_in (), + .io_oeb (gpio_loopback_one[30]), + .gpio_zero (gpio_loopback_zero[30]), + .gpio_one (gpio_loopback_one[30]), + .gpio_in (gpio_in[30]), + .gpio_dm ({gpio_dm2[30], gpio_dm1[30], gpio_dm0[30]}), + .gpio_inp_dis (gpio_inp_dis[30]), + .gpio_oeb_out (gpio_oeb[30]), + .gpio_out_val (gpio_out[30]), + .gpio_analog_en (gpio_analog_en[30]), + .gpio_analog_sel (gpio_analog_sel[30]), + .gpio_analog_pol (gpio_analog_pol[30]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[30]), + .gpio_vtrip_sel (gpio_vtrip_sel[30]), + .gpio_slow_sel (gpio_slow_sel[30]), + .gpio_holdover (gpio_holdover[30]) + ); + + // pad 31: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_31 ( + .io_out (gpio_loopback_zero[31]), + .io_in (), + .io_oeb (gpio_loopback_one[31]), + .gpio_zero (gpio_loopback_zero[31]), + .gpio_one (gpio_loopback_one[31]), + .gpio_in (gpio_in[31]), + .gpio_dm ({gpio_dm2[31], gpio_dm1[31], gpio_dm0[31]}), + .gpio_inp_dis (gpio_inp_dis[31]), + .gpio_oeb_out (gpio_oeb[31]), + .gpio_out_val (gpio_out[31]), + .gpio_analog_en (gpio_analog_en[31]), + .gpio_analog_sel (gpio_analog_sel[31]), + .gpio_analog_pol (gpio_analog_pol[31]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[31]), + .gpio_vtrip_sel (gpio_vtrip_sel[31]), + .gpio_slow_sel (gpio_slow_sel[31]), + .gpio_holdover (gpio_holdover[31]) + ); + + // pad 32: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_32 ( + .io_out (gpio_loopback_zero[32]), + .io_in (), + .io_oeb (gpio_loopback_one[32]), + .gpio_zero (gpio_loopback_zero[32]), + .gpio_one (gpio_loopback_one[32]), + .gpio_in (gpio_in[32]), + .gpio_dm ({gpio_dm2[32], gpio_dm1[32], gpio_dm0[32]}), + .gpio_inp_dis (gpio_inp_dis[32]), + .gpio_oeb_out (gpio_oeb[32]), + .gpio_out_val (gpio_out[32]), + .gpio_analog_en (gpio_analog_en[32]), + .gpio_analog_sel (gpio_analog_sel[32]), + .gpio_analog_pol (gpio_analog_pol[32]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[32]), + .gpio_vtrip_sel (gpio_vtrip_sel[32]), + .gpio_slow_sel (gpio_slow_sel[32]), + .gpio_holdover (gpio_holdover[32]) + ); + + // pad 33: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_33 ( + .io_out (gpio_loopback_zero[33]), + .io_in (), + .io_oeb (gpio_loopback_one[33]), + .gpio_zero (gpio_loopback_zero[33]), + .gpio_one (gpio_loopback_one[33]), + .gpio_in (gpio_in[33]), + .gpio_dm ({gpio_dm2[33], gpio_dm1[33], gpio_dm0[33]}), + .gpio_inp_dis (gpio_inp_dis[33]), + .gpio_oeb_out (gpio_oeb[33]), + .gpio_out_val (gpio_out[33]), + .gpio_analog_en (gpio_analog_en[33]), + .gpio_analog_sel (gpio_analog_sel[33]), + .gpio_analog_pol (gpio_analog_pol[33]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[33]), + .gpio_vtrip_sel (gpio_vtrip_sel[33]), + .gpio_slow_sel (gpio_slow_sel[33]), + .gpio_holdover (gpio_holdover[33]) + ); + + // pad 34: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_34 ( + .io_out (gpio_loopback_zero[34]), + .io_in (), + .io_oeb (gpio_loopback_one[34]), + .gpio_zero (gpio_loopback_zero[34]), + .gpio_one (gpio_loopback_one[34]), + .gpio_in (gpio_in[34]), + .gpio_dm ({gpio_dm2[34], gpio_dm1[34], gpio_dm0[34]}), + .gpio_inp_dis (gpio_inp_dis[34]), + .gpio_oeb_out (gpio_oeb[34]), + .gpio_out_val (gpio_out[34]), + .gpio_analog_en (gpio_analog_en[34]), + .gpio_analog_sel (gpio_analog_sel[34]), + .gpio_analog_pol (gpio_analog_pol[34]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[34]), + .gpio_vtrip_sel (gpio_vtrip_sel[34]), + .gpio_slow_sel (gpio_slow_sel[34]), + .gpio_holdover (gpio_holdover[34]) + ); + + // pad 35: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_35 ( + .io_out (gpio_loopback_zero[35]), + .io_in (), + .io_oeb (gpio_loopback_one[35]), + .gpio_zero (gpio_loopback_zero[35]), + .gpio_one (gpio_loopback_one[35]), + .gpio_in (gpio_in[35]), + .gpio_dm ({gpio_dm2[35], gpio_dm1[35], gpio_dm0[35]}), + .gpio_inp_dis (gpio_inp_dis[35]), + .gpio_oeb_out (gpio_oeb[35]), + .gpio_out_val (gpio_out[35]), + .gpio_analog_en (gpio_analog_en[35]), + .gpio_analog_sel (gpio_analog_sel[35]), + .gpio_analog_pol (gpio_analog_pol[35]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[35]), + .gpio_vtrip_sel (gpio_vtrip_sel[35]), + .gpio_slow_sel (gpio_slow_sel[35]), + .gpio_holdover (gpio_holdover[35]) + ); + + // pad 36: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_36 ( + .io_out (gpio_loopback_zero[36]), + .io_in (), + .io_oeb (gpio_loopback_one[36]), + .gpio_zero (gpio_loopback_zero[36]), + .gpio_one (gpio_loopback_one[36]), + .gpio_in (gpio_in[36]), + .gpio_dm ({gpio_dm2[36], gpio_dm1[36], gpio_dm0[36]}), + .gpio_inp_dis (gpio_inp_dis[36]), + .gpio_oeb_out (gpio_oeb[36]), + .gpio_out_val (gpio_out[36]), + .gpio_analog_en (gpio_analog_en[36]), + .gpio_analog_sel (gpio_analog_sel[36]), + .gpio_analog_pol (gpio_analog_pol[36]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[36]), + .gpio_vtrip_sel (gpio_vtrip_sel[36]), + .gpio_slow_sel (gpio_slow_sel[36]), + .gpio_holdover (gpio_holdover[36]) + ); + + // pad 37: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_37 ( + .io_out (gpio_loopback_zero[37]), + .io_in (), + .io_oeb (gpio_loopback_one[37]), + .gpio_zero (gpio_loopback_zero[37]), + .gpio_one (gpio_loopback_one[37]), + .gpio_in (gpio_in[37]), + .gpio_dm ({gpio_dm2[37], gpio_dm1[37], gpio_dm0[37]}), + .gpio_inp_dis (gpio_inp_dis[37]), + .gpio_oeb_out (gpio_oeb[37]), + .gpio_out_val (gpio_out[37]), + .gpio_analog_en (gpio_analog_en[37]), + .gpio_analog_sel (gpio_analog_sel[37]), + .gpio_analog_pol (gpio_analog_pol[37]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[37]), + .gpio_vtrip_sel (gpio_vtrip_sel[37]), + .gpio_slow_sel (gpio_slow_sel[37]), + .gpio_holdover (gpio_holdover[37]) + ); + + // pad 38: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_38 ( + .io_out (gpio_loopback_zero[38]), + .io_in (), + .io_oeb (gpio_loopback_one[38]), + .gpio_zero (gpio_loopback_zero[38]), + .gpio_one (gpio_loopback_one[38]), + .gpio_in (gpio_in[38]), + .gpio_dm ({gpio_dm2[38], gpio_dm1[38], gpio_dm0[38]}), + .gpio_inp_dis (gpio_inp_dis[38]), + .gpio_oeb_out (gpio_oeb[38]), + .gpio_out_val (gpio_out[38]), + .gpio_analog_en (gpio_analog_en[38]), + .gpio_analog_sel (gpio_analog_sel[38]), + .gpio_analog_pol (gpio_analog_pol[38]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[38]), + .gpio_vtrip_sel (gpio_vtrip_sel[38]), + .gpio_slow_sel (gpio_slow_sel[38]), + .gpio_holdover (gpio_holdover[38]) + ); + + // pad 39: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_39 ( + .io_out (gpio_loopback_zero[39]), + .io_in (), + .io_oeb (gpio_loopback_one[39]), + .gpio_zero (gpio_loopback_zero[39]), + .gpio_one (gpio_loopback_one[39]), + .gpio_in (gpio_in[39]), + .gpio_dm ({gpio_dm2[39], gpio_dm1[39], gpio_dm0[39]}), + .gpio_inp_dis (gpio_inp_dis[39]), + .gpio_oeb_out (gpio_oeb[39]), + .gpio_out_val (gpio_out[39]), + .gpio_analog_en (gpio_analog_en[39]), + .gpio_analog_sel (gpio_analog_sel[39]), + .gpio_analog_pol (gpio_analog_pol[39]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[39]), + .gpio_vtrip_sel (gpio_vtrip_sel[39]), + .gpio_slow_sel (gpio_slow_sel[39]), + .gpio_holdover (gpio_holdover[39]) + ); + + // pad 40: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_40 ( + .io_out (gpio_loopback_zero[40]), + .io_in (), + .io_oeb (gpio_loopback_one[40]), + .gpio_zero (gpio_loopback_zero[40]), + .gpio_one (gpio_loopback_one[40]), + .gpio_in (gpio_in[40]), + .gpio_dm ({gpio_dm2[40], gpio_dm1[40], gpio_dm0[40]}), + .gpio_inp_dis (gpio_inp_dis[40]), + .gpio_oeb_out (gpio_oeb[40]), + .gpio_out_val (gpio_out[40]), + .gpio_analog_en (gpio_analog_en[40]), + .gpio_analog_sel (gpio_analog_sel[40]), + .gpio_analog_pol (gpio_analog_pol[40]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[40]), + .gpio_vtrip_sel (gpio_vtrip_sel[40]), + .gpio_slow_sel (gpio_slow_sel[40]), + .gpio_holdover (gpio_holdover[40]) + ); + + // pad 41: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_41 ( + .io_out (gpio_loopback_zero[41]), + .io_in (), + .io_oeb (gpio_loopback_one[41]), + .gpio_zero (gpio_loopback_zero[41]), + .gpio_one (gpio_loopback_one[41]), + .gpio_in (gpio_in[41]), + .gpio_dm ({gpio_dm2[41], gpio_dm1[41], gpio_dm0[41]}), + .gpio_inp_dis (gpio_inp_dis[41]), + .gpio_oeb_out (gpio_oeb[41]), + .gpio_out_val (gpio_out[41]), + .gpio_analog_en (gpio_analog_en[41]), + .gpio_analog_sel (gpio_analog_sel[41]), + .gpio_analog_pol (gpio_analog_pol[41]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[41]), + .gpio_vtrip_sel (gpio_vtrip_sel[41]), + .gpio_slow_sel (gpio_slow_sel[41]), + .gpio_holdover (gpio_holdover[41]) + ); + + // pad 42: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_42 ( + .io_out (gpio_loopback_zero[42]), + .io_in (), + .io_oeb (gpio_loopback_one[42]), + .gpio_zero (gpio_loopback_zero[42]), + .gpio_one (gpio_loopback_one[42]), + .gpio_in (gpio_in[42]), + .gpio_dm ({gpio_dm2[42], gpio_dm1[42], gpio_dm0[42]}), + .gpio_inp_dis (gpio_inp_dis[42]), + .gpio_oeb_out (gpio_oeb[42]), + .gpio_out_val (gpio_out[42]), + .gpio_analog_en (gpio_analog_en[42]), + .gpio_analog_sel (gpio_analog_sel[42]), + .gpio_analog_pol (gpio_analog_pol[42]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[42]), + .gpio_vtrip_sel (gpio_vtrip_sel[42]), + .gpio_slow_sel (gpio_slow_sel[42]), + .gpio_holdover (gpio_holdover[42]) + ); + + // pad 43: analog (unused) + CF_gpio_config #( + .MODE(3'd0) + ) gpio_cfg_43 ( + .io_out (gpio_loopback_zero[43]), + .io_in (), + .io_oeb (gpio_loopback_one[43]), + .gpio_zero (gpio_loopback_zero[43]), + .gpio_one (gpio_loopback_one[43]), + .gpio_in (gpio_in[43]), + .gpio_dm ({gpio_dm2[43], gpio_dm1[43], gpio_dm0[43]}), + .gpio_inp_dis (gpio_inp_dis[43]), + .gpio_oeb_out (gpio_oeb[43]), + .gpio_out_val (gpio_out[43]), + .gpio_analog_en (gpio_analog_en[43]), + .gpio_analog_sel (gpio_analog_sel[43]), + .gpio_analog_pol (gpio_analog_pol[43]), + .gpio_ib_mode_sel (gpio_ib_mode_sel[43]), + .gpio_vtrip_sel (gpio_vtrip_sel[43]), + .gpio_slow_sel (gpio_slow_sel[43]), + .gpio_holdover (gpio_holdover[43]) + ); + +endmodule // openframe_gpio +/* verilator lint_on PINCONNECTEMPTY */ + +`default_nettype wire diff --git a/tests/data/openframe_timer.json b/tests/data/openframe_timer.json new file mode 100644 index 0000000..7c4afb4 --- /dev/null +++ b/tests/data/openframe_timer.json @@ -0,0 +1,97 @@ +{ + "schema_version": 1, + "gpio": { + "unused_mode": "analog", + "pads": [ + { + "pad": 0, + "mode": "input", + "signal": "clk" + }, + { + "pad": 1, + "mode": "input_pu", + "signal": "rst" + }, + { + "pad": 2, + "mode": "output", + "signal": "out", + "bit": 0 + }, + { + "pad": 3, + "mode": "output", + "signal": "out", + "bit": 1 + }, + { + "pad": 4, + "mode": "output", + "signal": "out", + "bit": 2 + }, + { + "pad": 5, + "mode": "output", + "signal": "out", + "bit": 3 + }, + { + "pad": 6, + "mode": "output", + "signal": "out", + "bit": 4 + }, + { + "pad": 7, + "mode": "output", + "signal": "out", + "bit": 5 + }, + { + "pad": 8, + "mode": "output", + "signal": "out", + "bit": 6 + }, + { + "pad": 9, + "mode": "output", + "signal": "out", + "bit": 7 + }, + { + "pad": 10, + "mode": "output", + "signal": "out", + "bit": 8 + }, + { + "pad": 11, + "mode": "output", + "signal": "out", + "bit": 9 + }, + { + "pad": 12, + "mode": "output", + "signal": "out", + "bit": 10 + } + ] + }, + "power": { + "domains": [ + "vccd1" + ], + "macros": [ + { + "instance": "mprj", + "domain": "vccd1", + "vdd_pin": "vccd1", + "gnd_pin": "vssd1" + } + ] + } +} diff --git a/tests/test_openframe.py b/tests/test_openframe.py new file mode 100644 index 0000000..4e5af52 --- /dev/null +++ b/tests/test_openframe.py @@ -0,0 +1,575 @@ +"""Tests for the openframe spec validator, generators, CLI commands and TUI.""" + +import asyncio +import copy +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from chipfoundry_cli.main import main +from chipfoundry_cli.openframe import SpecError, read_spec_file, render_outputs, validate_spec, write_spec_file +from chipfoundry_cli.openframe.gpio_gen import GPIO_RTL_REL, generate_gpio_rtl +from chipfoundry_cli.openframe.power_gen import POWER_JSON_REL, power_config + +DATA = Path(__file__).parent / "data" +TIMER_SPEC = json.loads((DATA / "openframe_timer.json").read_text()) +CF_GPIO_CONFIG_RTL = DATA / "CF_gpio_config.v" + + +def timer_spec(): + return copy.deepcopy(TIMER_SPEC) + + +def errors_of(data): + with pytest.raises(SpecError) as exc: + validate_spec(data) + return exc.value.errors + + +def assert_error(data, pattern): + errors = errors_of(data) + assert any(re.search(pattern, e) for e in errors), errors + + +# --------------------------------------------------------------------------- +# Validator +# --------------------------------------------------------------------------- + +class TestValidator: + def test_timer_spec_is_valid(self): + spec = validate_spec(timer_spec()) + assert [(s.name, s.direction, s.width) for s in spec.signals] == [ + ("clk", "in", None), + ("rst", "in", None), + ("out", "out", 11), + ] + assert spec.signals[2].pads == tuple(range(2, 13)) + assert len(spec.pads) == 44 + assert all(p.unused and p.mode == "analog" for p in spec.pads[13:]) + assert spec.domains == ("vccd1",) + assert spec.macros[0].gnd_net == "vssd1" + + def test_full_listing_without_unused_mode(self): + data = timer_spec() + del data["gpio"]["unused_mode"] + data["gpio"]["pads"] += [{"pad": n, "mode": "analog"} for n in range(13, 44)] + spec = validate_spec(data) + assert not any(p.unused for p in spec.pads) + + def test_missing_pads_without_unused_mode(self): + data = timer_spec() + del data["gpio"]["unused_mode"] + assert_error(data, r"pads 13-43 are not configured") + + def test_duplicate_pad(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 0, "mode": "input", "signal": "other"}) + assert_error(data, r"pad 0 is listed more than once") + + def test_bus_gap(self): + data = timer_spec() + data["gpio"]["pads"] = [p for p in data["gpio"]["pads"] if p.get("bit") != 4] + assert_error(data, r"signal 'out\[10:0\]' has no pad for bit\(s\) 4") + + def test_duplicate_bit(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "output", "signal": "out", "bit": 3}) + assert_error(data, r"bit 3 is assigned to both pad 5 and pad 20") + + def test_input_mode_on_output_bus(self): + data = timer_spec() + data["gpio"]["pads"][5]["mode"] = "input" + assert_error(data, r"signal 'out' mixes directions") + + def test_bus_mixing_bit_and_scalar(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "output", "signal": "out"}) + assert_error(data, r"signal 'out': pads 20 need a 'bit'") + + def test_scalar_signal_on_two_pads(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "input", "signal": "clk"}) + assert_error(data, r"signal 'clk' is used by pads 0, 20 without bit indices") + + def test_output_requires_signal(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "output"}) + assert_error(data, r"pad 20: mode 'output' requires a signal") + + def test_analog_rejects_signal(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "analog", "signal": "vin"}) + assert_error(data, r"pad 20: analog pads cannot have a signal") + + def test_bit_without_signal(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "input", "bit": 0}) + assert_error(data, r"'bit' is set but 'signal' is missing") + + def test_input_without_signal_is_allowed(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "input_pd"}) + spec = validate_spec(data) + assert spec.pads[20].mode == "input_pd" and not spec.pads[20].unused + + @pytest.mark.parametrize("name,pattern", [ + ("gpio_in", "clashes with an openframe_project_wrapper port"), + ("analog_io", "clashes with an openframe_project_wrapper port"), + ("wire", "is a Verilog keyword"), + ]) + def test_reserved_signal_names(self, name, pattern): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "input", "signal": name}) + assert_error(data, pattern) + + def test_bidir_port_collision(self): + data = timer_spec() + data["gpio"]["pads"] += [ + {"pad": 20, "mode": "bidir", "signal": "sda"}, + {"pad": 21, "mode": "input", "signal": "sda_in"}, + ] + assert_error(data, r"signal 'sda' creates port 'sda_in', which is already used by signal 'sda_in'|" + r"signal 'sda_in' creates port 'sda_in', which is already used by signal 'sda'") + + def test_bidir_port_clashes_with_pad_port(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 20, "mode": "bidir", "signal": "gpio"}) + assert_error(data, r"signal 'gpio' port 'gpio_in' clashes with an openframe_project_wrapper port") + + def test_macro_domain_not_enabled(self): + data = timer_spec() + data["power"]["macros"][0]["domain"] = "vccd2" + assert_error(data, r"macro 'mprj' uses domain 'vccd2', which is not in power.domains") + + def test_duplicate_macro(self): + data = timer_spec() + data["power"]["macros"].append(dict(data["power"]["macros"][0])) + assert_error(data, r"macro instance 'mprj' is listed more than once") + + @pytest.mark.parametrize("mutate,pattern", [ + (lambda d: d.update(schema_version=2), r"schema_version: 1 was expected"), + (lambda d: d["gpio"]["pads"][0].update(mode="inout"), r"gpio/pads/0/mode: 'inout' is not one of"), + (lambda d: d["gpio"]["pads"][0].update(pad=44), r"gpio/pads/0/pad: 44 is greater than the maximum"), + (lambda d: d["gpio"]["pads"][0].update(overrides={"slow": 2}), r"overrides/slow: 2 is not one of"), + (lambda d: d["gpio"]["pads"][0].update(overrides={"slow": True}), r"overrides/slow: True is not of type"), + (lambda d: d["gpio"]["pads"][0].update(overrides={"fast": 1}), r"Additional properties are not allowed"), + (lambda d: d["gpio"]["pads"][0].update(signal="1bad"), r"gpio/pads/0/signal: '1bad' does not match"), + (lambda d: d["gpio"].update(unused_mode="output"), r"gpio/unused_mode: 'output' is not one of"), + (lambda d: d.pop("power"), r"'power' is a required property"), + (lambda d: d["power"].update(domains=[]), r"power/domains: \[\] should be non-empty"), + (lambda d: d["power"].update(domains=["vccd1", "vccd1"]), r"has non-unique elements"), + (lambda d: d["power"].update(domains=["vddio"]), r"'vddio' is not one of"), + (lambda d: d["power"]["macros"][0].pop("vdd_pin"), r"'vdd_pin' is a required property"), + (lambda d: d.update(extra=1), r"Additional properties are not allowed \('extra'"), + ]) + def test_schema_errors(self, mutate, pattern): + data = timer_spec() + mutate(data) + assert_error(data, pattern) + + def test_errors_are_collected(self): + data = timer_spec() + data["gpio"]["pads"].append({"pad": 0, "mode": "input"}) + data["gpio"]["pads"].append({"pad": 20, "mode": "output"}) + data["power"]["macros"][0]["domain"] = "vdda1" + assert len(errors_of(data)) == 3 + + def test_normalized_and_hash_stable(self): + data = timer_spec() + shuffled = timer_spec() + shuffled["gpio"]["pads"].reverse() + assert validate_spec(data).raw == validate_spec(shuffled).raw + assert validate_spec(data).sha256 == validate_spec(shuffled).sha256 + data["gpio"]["pads"][0]["mode"] = "input_pd" + assert validate_spec(data).sha256 != validate_spec(shuffled).sha256 + + +# --------------------------------------------------------------------------- +# File I/O +# --------------------------------------------------------------------------- + +class TestSpecFiles: + @pytest.mark.parametrize("name", ["spec.json", "spec.yaml", "spec.yml"]) + def test_round_trip(self, tmp_path, name): + path = tmp_path / name + write_spec_file(path, timer_spec()) + assert validate_spec(read_spec_file(path)).raw == validate_spec(timer_spec()).raw + + def test_wrapped_block_is_accepted(self, tmp_path): + path = tmp_path / "spec.json" + path.write_text(json.dumps({"openframe": timer_spec()})) + validate_spec(read_spec_file(path)) + + def test_duplicate_json_key(self, tmp_path): + path = tmp_path / "spec.json" + path.write_text('{"schema_version": 1, "schema_version": 1}') + with pytest.raises(SpecError, match="duplicate key 'schema_version'"): + read_spec_file(path) + + def test_duplicate_yaml_key(self, tmp_path): + path = tmp_path / "spec.yaml" + path.write_text("schema_version: 1\nschema_version: 1\n") + with pytest.raises(SpecError, match="duplicate key 'schema_version'"): + read_spec_file(path) + + def test_unknown_extension(self, tmp_path): + path = tmp_path / "spec.txt" + path.write_text("{}") + with pytest.raises(SpecError, match="cannot infer format"): + read_spec_file(path) + assert read_spec_file(path, "json") == {} + + def test_parse_error(self, tmp_path): + path = tmp_path / "spec.json" + path.write_text("{not json") + with pytest.raises(SpecError, match="cannot parse json"): + read_spec_file(path) + + +# --------------------------------------------------------------------------- +# Generators +# --------------------------------------------------------------------------- + +def _instance_params(rtl): + """pad index -> parameter text of its CF_gpio_config instance.""" + found = re.findall(r"CF_gpio_config #\(\n(.*?)\n \) gpio_cfg_(\d\d) \(", rtl, re.S) + return {int(n): " ".join(p.split()) for p, n in found} + + +class TestGpioGenerator: + def test_timer_matches_golden(self): + rtl = generate_gpio_rtl(validate_spec(timer_spec())) + assert rtl == (DATA / "openframe_gpio_timer.v").read_text() + + def test_timer_pinout(self): + rtl = generate_gpio_rtl(validate_spec(timer_spec())) + params = _instance_params(rtl) + assert len(params) == 44 + assert params[0] == ".MODE(3'd1)" + assert params[1] == ".MODE(3'd3)" + assert all(params[n] == ".MODE(3'd4)" for n in range(2, 13)) + assert all(params[n] == ".MODE(3'd0)" for n in range(13, 44)) + assert ".io_in (clk)" in rtl + assert ".io_in (rst)" in rtl + for i in range(11): + assert f".io_out (out[{i}])," in rtl + assert "input wire [10:0] out," in rtl + assert "CF_gpio_config v1.2.0" not in rtl + + def test_overrides_and_bidir(self): + data = timer_spec() + data["gpio"]["unused_mode"] = "input_pd" + data["gpio"]["pads"] += [ + {"pad": 20, "mode": "bidir", "signal": "sda", "overrides": {"slow": 1, "vtrip": 1}}, + {"pad": 21, "mode": "bidir", "signal": "io", "bit": 1}, + {"pad": 22, "mode": "bidir", "signal": "io", "bit": 0, "overrides": {"holdover": 1}}, + ] + rtl = generate_gpio_rtl(validate_spec(data)) + params = _instance_params(rtl) + assert params[20] == ".MODE(3'd5), .SLOW(1'b1), .VTRIP(1'b1)" + assert params[22] == ".MODE(3'd5), .HOLDOVER(1'b1)" + assert params[30] == ".MODE(3'd2)" + assert "requires CF_gpio_config v1.2.0" in rtl + for port in ("output wire sda_in,", "input wire sda_out,", "input wire sda_oeb,", + "output wire [1:0] io_in,", "input wire [1:0] io_out,", "input wire [1:0] io_oeb,"): + assert port in rtl + assert ".io_oeb (io_oeb[0])," in rtl + + def test_no_signals(self): + data = {"schema_version": 1, "gpio": {"unused_mode": "analog", "pads": []}, + "power": {"domains": ["vccd1"], "macros": []}} + rtl = generate_gpio_rtl(validate_spec(data)) + assert "// User design ports" not in rtl + assert len(_instance_params(rtl)) == 44 + + +class TestPowerGenerator: + def test_timer_power(self): + cfg = power_config(validate_spec(timer_spec())) + assert cfg["VDD_NETS"] == ["vccd1"] + assert cfg["GND_NETS"] == ["vssd1"] + assert cfg["PDN_MACRO_CONNECTIONS"] == ["mprj vccd1 vssd1 vccd1 vssd1"] + + def test_multi_domain_keeps_order(self): + data = timer_spec() + data["power"] = { + "domains": ["vccd2", "vccd1", "vdda1"], + "macros": [ + {"instance": "mprj", "domain": "vccd1", "vdd_pin": "vccd1", "gnd_pin": "vssd1"}, + {"instance": "core2/u_ana", "domain": "vdda1", "vdd_pin": "VDD", "gnd_pin": "VSS"}, + ], + } + cfg = power_config(validate_spec(data)) + assert cfg["VDD_NETS"] == ["vccd2", "vccd1", "vdda1"] + assert cfg["GND_NETS"] == ["vssd2", "vssd1", "vssa1"] + assert cfg["PDN_MACRO_CONNECTIONS"] == ["mprj vccd1 vssd1 vccd1 vssd1", "core2/u_ana vdda1 vssa1 VDD VSS"] + + +LINT_SPECS = { + "timer": timer_spec(), + "mixed": { + "schema_version": 1, + "gpio": {"unused_mode": "input_pu", "pads": [ + {"pad": 0, "mode": "input", "signal": "clk"}, + {"pad": 5, "mode": "bidir", "signal": "sda", "overrides": {"slow": 1, "analog_en": 1}}, + {"pad": 6, "mode": "bidir", "signal": "gp", "bit": 0}, + {"pad": 7, "mode": "bidir", "signal": "gp", "bit": 1}, + {"pad": 8, "mode": "output", "signal": "led"}, + {"pad": 9, "mode": "input_pd"}, + {"pad": 10, "mode": "analog", "overrides": {"analog_sel": 1, "analog_pol": 1}}, + ]}, + "power": {"domains": ["vccd1", "vccd2"], "macros": []}, + }, +} + + +@pytest.mark.parametrize("name", sorted(LINT_SPECS)) +class TestGeneratedRtlLints: + def _write(self, tmp_path, name): + path = tmp_path / "openframe_gpio.v" + path.write_text(generate_gpio_rtl(validate_spec(LINT_SPECS[name]))) + return path + + @pytest.mark.skipif(shutil.which("verilator") is None, reason="verilator not installed") + def test_verilator(self, tmp_path, name): + rtl = self._write(tmp_path, name) + res = subprocess.run( + ["verilator", "--lint-only", "-Wall", "--top-module", "openframe_gpio", str(rtl), str(CF_GPIO_CONFIG_RTL)], + capture_output=True, text=True, + ) + assert res.returncode == 0, res.stderr + + @pytest.mark.skipif(shutil.which("iverilog") is None, reason="iverilog not installed") + def test_iverilog(self, tmp_path, name): + rtl = self._write(tmp_path, name) + res = subprocess.run( + ["iverilog", "-g2005", "-Wall", "-Wno-timescale", "-o", str(tmp_path / "a.out"), str(rtl), + str(CF_GPIO_CONFIG_RTL)], + capture_output=True, text=True, + ) + assert res.returncode == 0, res.stderr + assert "warning" not in res.stderr.lower(), res.stderr + + +# --------------------------------------------------------------------------- +# CLI +# --------------------------------------------------------------------------- + +@pytest.fixture +def openframe_project(tmp_path): + (tmp_path / ".cf").mkdir() + (tmp_path / "verilog" / "rtl").mkdir(parents=True) + (tmp_path / "openlane" / "openframe_project_wrapper").mkdir(parents=True) + project = {"project": {"name": "demo", "type": "openframe", "openframe": timer_spec()}} + (tmp_path / ".cf" / "project.json").write_text(json.dumps(project, indent=2)) + return tmp_path + + +class _Result: + def __init__(self, res): + self.exit_code = res.exit_code + self.output = " ".join(res.output.split()) # undo Rich line wrapping + + +def run(args, **kw): + return _Result(CliRunner().invoke(main, args, catch_exceptions=False, **kw)) + + +class TestCli: + def test_generate_and_check(self, openframe_project): + root = str(openframe_project) + res = run(["openframe", "generate", "--check", "--project-root", root]) + assert res.exit_code == 1 and "missing or out of date" in res.output + + res = run(["openframe", "generate", "--project-root", root]) + assert res.exit_code == 0, res.output + rtl = (openframe_project / GPIO_RTL_REL).read_text() + assert rtl == render_outputs(validate_spec(timer_spec()))[GPIO_RTL_REL] + power = json.loads((openframe_project / POWER_JSON_REL).read_text()) + assert power["PDN_MACRO_CONNECTIONS"] == ["mprj vccd1 vssd1 vccd1 vssd1"] + + assert run(["openframe", "generate", "--check", "--project-root", root]).exit_code == 0 + res = run(["openframe", "generate", "--project-root", root]) + assert "up to date" in res.output + + (openframe_project / POWER_JSON_REL).write_text("{}") + assert run(["openframe", "generate", "--check", "--project-root", root]).exit_code == 1 + + def test_generate_rejects_invalid_spec(self, openframe_project): + pj = openframe_project / ".cf" / "project.json" + data = json.loads(pj.read_text()) + data["project"]["openframe"]["gpio"]["pads"][3]["mode"] = "input" + pj.write_text(json.dumps(data)) + res = run(["openframe", "generate", "--project-root", str(openframe_project)]) + assert res.exit_code != 0 + assert "mixes directions" in res.output + assert not (openframe_project / GPIO_RTL_REL).exists() + + def test_generate_requires_template_layout(self, openframe_project): + shutil.rmtree(openframe_project / "openlane") + res = run(["openframe", "generate", "--project-root", str(openframe_project)]) + assert res.exit_code != 0 and "does not exist" in res.output + + def test_missing_block(self, openframe_project): + pj = openframe_project / ".cf" / "project.json" + pj.write_text(json.dumps({"project": {"type": "openframe"}})) + res = run(["openframe", "validate", "--project-root", str(openframe_project)]) + assert res.exit_code != 0 and "no project.openframe block" in res.output + + def test_rejects_caravel_project(self, openframe_project): + pj = openframe_project / ".cf" / "project.json" + pj.write_text(json.dumps({"project": {"type": "digital"}})) + res = run(["openframe", "generate", "--project-root", str(openframe_project)]) + assert res.exit_code != 0 and "for openframe projects" in res.output + + def test_export_import(self, openframe_project, tmp_path_factory): + root = str(openframe_project) + out = tmp_path_factory.mktemp("x") / "spec.yaml" + assert run(["openframe", "export", str(out), "--project-root", root]).exit_code == 0 + assert validate_spec(read_spec_file(out)).raw == validate_spec(timer_spec()).raw + + # Re-importing the identical spec needs no --force. + res = run(["openframe", "import", str(out), "--project-root", root, "--no-generate"]) + assert res.exit_code == 0, res.output + + edited = read_spec_file(out) + edited["gpio"]["pads"][0]["mode"] = "input_pd" + write_spec_file(out, edited) + res = run(["openframe", "import", str(out), "--project-root", root]) + assert res.exit_code != 0 and "--force" in res.output + + res = run(["openframe", "import", str(out), "--project-root", root, "--force"]) + assert res.exit_code == 0, res.output + stored = json.loads((openframe_project / ".cf" / "project.json").read_text())["project"] + assert stored["openframe"]["gpio"]["pads"][0]["mode"] == "input_pd" + assert stored["name"] == "demo" + assert (openframe_project / GPIO_RTL_REL).exists() + + def test_import_invalid_file(self, openframe_project, tmp_path_factory): + bad = tmp_path_factory.mktemp("x") / "bad.json" + data = timer_spec() + data["gpio"]["pads"].append({"pad": 2, "mode": "output", "signal": "x"}) + bad.write_text(json.dumps(data)) + res = run(["openframe", "import", str(bad), "--project-root", str(openframe_project)]) + assert res.exit_code != 0 and "pad 2 is listed more than once" in res.output + + def test_gpio_config_view_openframe(self, openframe_project): + res = run(["gpio-config", "--view", "--project-root", str(openframe_project)]) + assert res.exit_code == 0, res.output + assert "out[10:0]" in res.output and "13-43" in res.output + + def test_save_keeps_caravel_gpio_config(self, openframe_project): + from chipfoundry_cli.utils import get_openframe_spec_from_project_json, save_openframe_spec_to_project_json + pj = openframe_project / ".cf" / "project.json" + data = json.loads(pj.read_text()) + data["project"]["gpio_config"] = {"5": "13'h1808"} + pj.write_text(json.dumps(data)) + save_openframe_spec_to_project_json(str(pj), {"schema_version": 1}) + stored = json.loads(pj.read_text())["project"] + assert stored["gpio_config"] == {"5": "13'h1808"} + assert get_openframe_spec_from_project_json(str(pj)) == {"schema_version": 1} + + def test_init_seeds_from_template(self, tmp_path, monkeypatch): + (tmp_path / ".cf").mkdir() + (tmp_path / ".cf" / "openframe_default.json").write_text(json.dumps(timer_spec())) + monkeypatch.setattr("chipfoundry_cli.main.load_user_config", lambda: {}) + answers = {"Project name": "demo", "Project type (digital/analog/openframe)": "openframe"} + monkeypatch.setattr("chipfoundry_cli.main._prompt_with_default", + lambda label, current, detected=None: answers.get(label)) + res = run(["init", "--project-root", str(tmp_path), "--description", ""]) + assert res.exit_code == 0, res.output + stored = json.loads((tmp_path / ".cf" / "project.json").read_text())["project"] + assert stored["openframe"] == validate_spec(timer_spec()).raw + + def test_init_rejects_invalid_seed(self, tmp_path, monkeypatch): + (tmp_path / ".cf").mkdir() + bad = timer_spec() + del bad["power"] + (tmp_path / ".cf" / "openframe_default.json").write_text(json.dumps(bad)) + monkeypatch.setattr("chipfoundry_cli.main.load_user_config", lambda: {}) + answers = {"Project name": "demo", "Project type (digital/analog/openframe)": "openframe"} + monkeypatch.setattr("chipfoundry_cli.main._prompt_with_default", + lambda label, current, detected=None: answers.get(label)) + res = run(["init", "--project-root", str(tmp_path), "--description", ""]) + assert res.exit_code != 0 and "'power' is a required property" in res.output + + +# --------------------------------------------------------------------------- +# TUI +# --------------------------------------------------------------------------- + +class TestTui: + def test_edit_pad_and_save(self): + from chipfoundry_cli.openframe.tui import OpenframeGridApp + + async def scenario(): + app = OpenframeGridApp(timer_spec()) + async with app.run_test(size=(200, 60)) as pilot: + app.current = 13 + await pilot.press("enter") + await pilot.pause() + screen = app.screen + screen.query_one("#mode_output").value = True + signal = screen.query_one("#signal") + signal.focus() + await pilot.press(*"led_d") # 'd' must go to the Input, not trigger save + await pilot.pause() + assert app.is_running + screen.query_one("#ov_slow").value = True + await pilot.click("#apply") + await pilot.pause() + await pilot.press("d") + await pilot.pause() + return app.return_value + + result = asyncio.run(scenario()) + assert result is not None + spec = validate_spec(result) + assert spec.pads[13].mode == "output" + assert spec.pads[13].signal == "led_d" + assert spec.pads[13].overrides == {"slow": 1} + + def test_save_blocked_when_invalid(self): + from chipfoundry_cli.openframe.tui import OpenframeGridApp + + async def scenario(): + app = OpenframeGridApp(None) + async with app.run_test(size=(200, 60)) as pilot: + await pilot.press("d") + await pilot.pause() + status = str(app.query_one("#status").renderable) + running = app.is_running + await pilot.press("q") + return status, running, app.return_value + + status, running, result = asyncio.run(scenario()) + assert running and "Cannot save" in status + assert result is None + + def test_multi_select_assigns_consecutive_bits(self): + from chipfoundry_cli.openframe.tui import OpenframeGridApp + + async def scenario(): + app = OpenframeGridApp(timer_spec()) + async with app.run_test(size=(200, 60)) as pilot: + for pad in (20, 21, 22): + app.toggle_pad(pad) + await pilot.press("enter") + await pilot.pause() + screen = app.screen + screen.query_one("#mode_input_pu").value = True + screen.query_one("#signal").value = "btn" + await pilot.click("#apply") + await pilot.pause() + await pilot.press("d") + await pilot.pause() + return app.return_value + + spec = validate_spec(asyncio.run(scenario())) + btn = next(s for s in spec.signals if s.name == "btn") + assert btn.width == 3 and btn.pads == (20, 21, 22) From 1043e17d54af8bb8b550459f928efd90509f8619 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 09:48:36 +0000 Subject: [PATCH 2/6] Document openframe configurator commands and spec format --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/README.md b/README.md index 9caadc4..7d74f59 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,61 @@ 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 a Macro ```bash From 39fdfd9dc8f235b88ee9c56d1cd95755d4aeaf90 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 09:59:57 +0000 Subject: [PATCH 3/6] Pin LibreLane to CI2609 (LibreLane 3.0.14) --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 40c86d9..b5c4641 100644 --- a/versions.json +++ b/versions.json @@ -3,7 +3,7 @@ "sky130A": "2026.2.9", "sky130B": "2026.2.9" }, - "openlane_version": "CI2511", + "openlane_version": "CI2609", "open_pdks_commits": { "sky130A": "3e0e31dcce8519a7dbb82590346db16d91b7244f", "sky130B": "3e0e31dcce8519a7dbb82590346db16d91b7244f" From 651d1f006f3ec05f01f689c088a842bb48d2bd98 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 12:34:00 +0000 Subject: [PATCH 4/6] Fix cf harden step-list probe when LibreLane 3 runs in a terminal LibreLane 3 writes a cursor-restore escape to stdout on exit when attached to a TTY, so parsing the whole probe output as JSON failed with 'Extra data' and cf harden aborted before starting LibreLane. The probe now tags its JSON line and cf harden parses only that line, raising if it is missing. --- chipfoundry_cli/main.py | 19 ++++++++++++++++--- tests/test_harden_command.py | 13 ++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/chipfoundry_cli/main.py b/chipfoundry_cli/main.py index 89a9da4..63df4ff 100644 --- a/chipfoundry_cli/main.py +++ b/chipfoundry_cli/main.py @@ -47,6 +47,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.""" @@ -4185,7 +4199,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], @@ -4197,8 +4211,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) diff --git a/tests/test_harden_command.py b/tests/test_harden_command.py index 2f616d8..ec87fd9 100644 --- a/tests/test_harden_command.py +++ b/tests/test_harden_command.py @@ -3,7 +3,7 @@ """ import pytest from click.testing import CliRunner -from chipfoundry_cli.main import main +from chipfoundry_cli.main import STEP_LIST_MARKER, main, parse_step_list_output from pathlib import Path import tempfile import shutil @@ -87,3 +87,14 @@ def test_harden_with_all_options(self, temp_project_dir): if __name__ == '__main__': pytest.main([__file__, '-v']) + + +class TestStepListProbe: + def test_ignores_terminal_escapes(self): + # LibreLane 3 restores the cursor on stdout at exit when attached to a terminal. + out = STEP_LIST_MARKER + '{"steps": ["Verilator.Lint", "Yosys.Synthesis"]}\n\x1b[?25h' + assert parse_step_list_output(out) == ["Verilator.Lint", "Yosys.Synthesis"] + + def test_missing_marker_is_an_error(self): + with pytest.raises(ValueError, match="step list not found"): + parse_step_list_output('{"steps": []}') From 9ddf0292e966e75304bf46964a18e83f8e48cab1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 12:34:05 +0000 Subject: [PATCH 5/6] cf harden: pass power.json before config.json; warn when openframe files are out of sync - When openlane//power.json exists, cf harden (Nix and Docker) and the shared build_librelane_command pass it before config.* like openlane/Makefile, since LibreLane has no include and the last file's meta wins. - cf harden openframe_project_wrapper and cf precheck print a warning panel when project.openframe and the generated files differ, per file: missing, generated from a different spec (spec-sha256), edited by hand, or not generated by cf; also when the spec is missing or invalid. --- chipfoundry_cli/librelane_run.py | 17 ++- chipfoundry_cli/main.py | 24 ++- chipfoundry_cli/openframe/cli.py | 65 ++++++++- tests/test_openframe_integration.py | 219 ++++++++++++++++++++++++++++ 4 files changed, 319 insertions(+), 6 deletions(-) create mode 100644 tests/test_openframe_integration.py diff --git a/chipfoundry_cli/librelane_run.py b/chipfoundry_cli/librelane_run.py index 1ddf5a4..2df63e5 100644 --- a/chipfoundry_cli/librelane_run.py +++ b/chipfoundry_cli/librelane_run.py @@ -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]] = [] @@ -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 diff --git a/chipfoundry_cli/main.py b/chipfoundry_cli/main.py index 63df4ff..3d5d069 100644 --- a/chipfoundry_cli/main.py +++ b/chipfoundry_cli/main.py @@ -9,7 +9,14 @@ verify_remote_precheck_repo, ) from chipfoundry_cli.version_check import maybe_warn_outdated -from chipfoundry_cli.openframe.cli import register_openframe, run_openframe_gpio_config, seed_openframe_spec +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, @@ -4039,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) @@ -4158,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' @@ -4370,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: @@ -4410,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({ @@ -4473,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 @@ -4698,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.") diff --git a/chipfoundry_cli/openframe/cli.py b/chipfoundry_cli/openframe/cli.py index 95a6e81..5756263 100644 --- a/chipfoundry_cli/openframe/cli.py +++ b/chipfoundry_cli/openframe/cli.py @@ -1,13 +1,17 @@ """`cf openframe ...` commands and the openframe branch of `cf gpio-config`.""" +import re from pathlib import Path -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional import click from rich.console import Console +from rich.panel import Panel from rich.table import Table from chipfoundry_cli.openframe.generate import stale_outputs, write_outputs +from chipfoundry_cli.openframe.gpio_gen import GPIO_RTL_REL +from chipfoundry_cli.openframe.power_gen import POWER_JSON_REL from chipfoundry_cli.openframe.spec import ( OpenframeSpec, SpecError, @@ -29,6 +33,7 @@ # Seed file shipped by the openframe_user_project template; `cf init` imports it once. OPENFRAME_SEED_REL = ".cf/openframe_default.json" +OPENFRAME_WRAPPER = "openframe_project_wrapper" def _print_spec_error(err: SpecError) -> None: @@ -103,6 +108,64 @@ def _generate(root: Path, spec: OpenframeSpec) -> None: console.print("[green]✓ Generated files are up to date[/green]") +_SPEC_HASH_RE = re.compile(r'spec-sha256"?:\s*"?([0-9a-f]{64})') + + +def _stale_reason(path: Path, spec: OpenframeSpec) -> str: + if not path.exists(): + return "missing" + match = _SPEC_HASH_RE.search(path.read_text()) + if match is None: + return "not generated by cf openframe generate (no spec-sha256 header)" + if match.group(1) != spec.sha256: + return "generated from a different spec" + return "edited by hand since it was generated" + + +def openframe_sync_problems(project_root) -> List[str]: + """Reasons the generated openframe files may not match ``project.openframe``. + + Empty when the project is not openframe or everything is in sync. + """ + root = Path(project_root) + project_json = root / CF_PROJECT_JSON_REL + if not project_json.exists(): + return [] + if load_project_json(str(project_json)).get("project", {}).get("type") != "openframe": + return [] + try: + block = get_openframe_spec_from_project_json(str(project_json)) + except ValueError as e: + return [f"cannot read {CF_PROJECT_JSON_REL}: {e}"] + if block is None: + return [ + f"{CF_PROJECT_JSON_REL} has no project.openframe spec, so {GPIO_RTL_REL} and {POWER_JSON_REL} " + "cannot be checked. Run 'cf gpio-config' or 'cf openframe import '." + ] + try: + spec = validate_spec(block) + except SpecError as e: + return ["project.openframe is invalid, so the generated files cannot be checked:"] + [ + f" {msg}" for msg in e.errors + ] + return [f"{rel}: {_stale_reason(root / rel, spec)}" for rel in stale_outputs(root, spec)] + + +def warn_openframe_out_of_sync(project_root, command: str) -> bool: + """Print a warning panel if the openframe spec and generated files disagree. Returns True if it warned.""" + problems = openframe_sync_problems(project_root) + if not problems: + return False + body = "\n".join(f"• {p}" if not p.startswith(" ") else f" {p.strip()}" for p in problems) + body += ( + "\n\nRun [bold]cf openframe generate[/bold] to regenerate from project.openframe " + "([bold]cf openframe generate --check[/bold] re-checks without writing)." + f"\n{command} continues with the files currently on disk." + ) + console.print(Panel(body, title="⚠ Openframe spec and generated files differ", border_style="yellow")) + return True + + def seed_openframe_spec(project_root: Path, proj: Dict[str, Any]) -> None: """Import the template's seed spec into ``proj`` when the project has none. Used by `cf init`.""" if "openframe" in proj: diff --git a/tests/test_openframe_integration.py b/tests/test_openframe_integration.py new file mode 100644 index 0000000..cd2d8a9 --- /dev/null +++ b/tests/test_openframe_integration.py @@ -0,0 +1,219 @@ +"""cf harden / cf precheck integration with the openframe spec: power.json ordering and sync warnings.""" + +import json +import subprocess +from pathlib import Path + +import pytest +from click.testing import CliRunner + +from chipfoundry_cli.librelane_run import build_librelane_command, librelane_config_files +from chipfoundry_cli.main import STEP_LIST_MARKER, main +from chipfoundry_cli.openframe import render_outputs, validate_spec +from chipfoundry_cli.openframe.cli import openframe_sync_problems +from chipfoundry_cli.openframe.gpio_gen import GPIO_RTL_REL +from chipfoundry_cli.openframe.power_gen import POWER_JSON_REL + +DATA = Path(__file__).parent / "data" +TIMER_SPEC = json.loads((DATA / "openframe_timer.json").read_text()) +WARNING_TITLE = "Openframe spec and generated files differ" + + +def flat(text): + """Undo Rich wrapping and panel borders.""" + return " ".join(text.replace("│", " ").split()) + + +@pytest.fixture +def project(tmp_path): + """Minimal openframe project with a wrapper and a timer macro, LibreLane venv and PDK stubs.""" + (tmp_path / ".cf").mkdir() + (tmp_path / ".cf" / "project.json").write_text( + json.dumps({"project": {"name": "demo", "type": "openframe", "openframe": TIMER_SPEC}}) + ) + (tmp_path / "verilog" / "rtl").mkdir(parents=True) + wrapper = tmp_path / "openlane" / "openframe_project_wrapper" + wrapper.mkdir(parents=True) + (wrapper / "config.json").write_text(json.dumps({"DESIGN_NAME": "openframe_project_wrapper"})) + timer = tmp_path / "openlane" / "user_proj_timer" + timer.mkdir() + (timer / "config.json").write_text(json.dumps({"DESIGN_NAME": "user_proj_timer"})) + (tmp_path / "openlane" / ".venv" / "bin").mkdir(parents=True) + (tmp_path / "dependencies" / "pdks" / "sky130A").mkdir(parents=True) + return tmp_path + + +def generate(root): + for rel, content in render_outputs(validate_spec(TIMER_SPEC)).items(): + (root / rel).write_text(content) + + +class FakeProcess: + def __init__(self, cmd, **kwargs): + FakeProcess.cmd = cmd + FakeProcess.cwd = kwargs.get("cwd") + self.pid = 0 + + def wait(self, timeout=None): + return 0 + + +@pytest.fixture +def fake_librelane(monkeypatch): + """Stub everything cf harden shells out to; the LibreLane command lands in FakeProcess.cmd.""" + FakeProcess.cmd = None + real_run = subprocess.run + + def fake_run(cmd, *args, **kwargs): + if cmd[:2] == ["docker", "info"] or cmd[:3] == ["nix", "flake", "metadata"]: + return subprocess.CompletedProcess(cmd, 0, b"", b"") + if len(cmd) > 1 and cmd[1] == "-c": # step list query through the venv python + out = STEP_LIST_MARKER + json.dumps({"steps": ["Yosys.Synthesis"]}) + "\n\x1b[?25h" + return subprocess.CompletedProcess(cmd, 0, out, "") + return real_run(cmd, *args, **kwargs) + + monkeypatch.setattr("chipfoundry_cli.main.subprocess.run", fake_run) + monkeypatch.setattr("chipfoundry_cli.main.subprocess.Popen", FakeProcess) + monkeypatch.setattr( + "chipfoundry_cli.main.fetch_versions_from_upstream", lambda *a, **k: {"openlane_version": "CI2609"} + ) + real_which = __import__("shutil").which + monkeypatch.setattr( + "chipfoundry_cli.main.shutil.which", lambda name: "/usr/bin/nix" if name == "nix" else real_which(name) + ) + return FakeProcess + + +def harden(root, *args): + return CliRunner().invoke(main, ["harden", *args, "--project-root", str(root)], catch_exceptions=False) + + +class TestConfigFiles: + def test_power_json_goes_first(self, project): + wrapper = project / "openlane" / "openframe_project_wrapper" + assert librelane_config_files(wrapper / "config.json") == [wrapper / "config.json"] + generate(project) + assert librelane_config_files(wrapper / "config.json") == [wrapper / "power.json", wrapper / "config.json"] + + def test_power_json_is_per_macro(self, project): + generate(project) + timer = project / "openlane" / "user_proj_timer" / "config.json" + assert librelane_config_files(timer) == [timer] + + @pytest.mark.parametrize("backend", ["nix", "docker"]) + def test_build_librelane_command(self, project, backend): + generate(project) + wrapper = project / "openlane" / "openframe_project_wrapper" + cmd, _ = build_librelane_command( + project_root=project, flow_root=project / "openlane", config_file=wrapper / "config.json", + pdk="sky130A", pdk_root=project / "dependencies" / "pdks", tag="t", openlane_version="CI2609", + backend=backend, + ) + assert cmd[-2:] == [str(wrapper / "power.json"), str(wrapper / "config.json")] + + +class TestHardenCommand: + @pytest.mark.parametrize("flag,first", [("--use-docker", "python3"), ("--use-nix", "nix")]) + def test_wrapper_passes_power_json_before_config(self, project, fake_librelane, flag, first): + generate(project) + res = harden(project, "openframe_project_wrapper", flag) + assert res.exit_code == 0, res.output + wrapper = project / "openlane" / "openframe_project_wrapper" + cmd = fake_librelane.cmd + assert Path(cmd[0]).name == first + assert cmd[-2:] == [str(wrapper / "power.json"), str(wrapper / "config.json")] + assert cmd.count(str(wrapper / "config.json")) == 1 + assert "power.json + config.json" in res.output + assert WARNING_TITLE not in res.output + + def test_macro_without_power_json(self, project, fake_librelane): + generate(project) + res = harden(project, "user_proj_timer", "--use-docker") + assert res.exit_code == 0, res.output + timer = project / "openlane" / "user_proj_timer" + assert fake_librelane.cmd[-1] == str(timer / "config.json") + assert str(timer / "power.json") not in fake_librelane.cmd + + def test_warns_when_generated_files_missing(self, project, fake_librelane): + res = harden(project, "openframe_project_wrapper", "--use-docker") + out = flat(res.output) + assert WARNING_TITLE in out + assert f"{GPIO_RTL_REL}: missing" in out and f"{POWER_JSON_REL}: missing" in out + assert "cf openframe generate" in out + assert "cf harden continues with the files currently on disk" in out + assert res.exit_code == 0 + + def test_timer_macro_does_not_warn(self, project, fake_librelane): + res = harden(project, "user_proj_timer", "--use-docker") + assert WARNING_TITLE not in flat(res.output) + + def test_list_does_not_warn(self, project): + res = harden(project, "--list") + assert WARNING_TITLE not in flat(res.output) + + +class TestSyncProblems: + def test_in_sync(self, project): + generate(project) + assert openframe_sync_problems(project) == [] + + def test_spec_changed_after_generate(self, project): + generate(project) + pj = project / ".cf" / "project.json" + data = json.loads(pj.read_text()) + data["project"]["openframe"]["gpio"]["pads"][0]["mode"] = "input_pd" + pj.write_text(json.dumps(data)) + assert openframe_sync_problems(project) == [ + f"{GPIO_RTL_REL}: generated from a different spec", + f"{POWER_JSON_REL}: generated from a different spec", + ] + + def test_hand_edited(self, project): + generate(project) + rtl = project / GPIO_RTL_REL + rtl.write_text(rtl.read_text().replace(".MODE(3'd3)", ".MODE(3'd2)")) + assert openframe_sync_problems(project) == [f"{GPIO_RTL_REL}: edited by hand since it was generated"] + + def test_foreign_file(self, project): + generate(project) + (project / POWER_JSON_REL).write_text(json.dumps({"VDD_NETS": ["vccd1"]})) + assert openframe_sync_problems(project) == [ + f"{POWER_JSON_REL}: not generated by cf openframe generate (no spec-sha256 header)" + ] + + def test_invalid_spec(self, project): + pj = project / ".cf" / "project.json" + data = json.loads(pj.read_text()) + data["project"]["openframe"]["gpio"]["pads"][3]["mode"] = "input" + pj.write_text(json.dumps(data)) + problems = openframe_sync_problems(project) + assert problems[0] == "project.openframe is invalid, so the generated files cannot be checked:" + assert any("mixes directions" in p for p in problems[1:]) + + def test_missing_block(self, project): + (project / ".cf" / "project.json").write_text(json.dumps({"project": {"type": "openframe"}})) + problems = openframe_sync_problems(project) + assert len(problems) == 1 and "has no project.openframe spec" in problems[0] + + def test_not_openframe(self, project): + (project / ".cf" / "project.json").write_text(json.dumps({"project": {"type": "digital"}})) + assert openframe_sync_problems(project) == [] + + +class TestPrecheckWarning: + def test_precheck_warns_when_stale(self, project): + generate(project) + pj = project / ".cf" / "project.json" + data = json.loads(pj.read_text()) + data["project"]["openframe"]["power"]["domains"] = ["vccd1", "vccd2"] + pj.write_text(json.dumps(data)) + res = CliRunner().invoke(main, ["precheck", "--project-root", str(project), "--dry-run"]) + out = flat(res.output) + assert WARNING_TITLE in out + assert f"{POWER_JSON_REL}: generated from a different spec" in out + assert "cf precheck continues" in out + + def test_precheck_quiet_when_in_sync(self, project): + generate(project) + res = CliRunner().invoke(main, ["precheck", "--project-root", str(project), "--dry-run"]) + assert WARNING_TITLE not in flat(res.output) From 1420940cf69e2f9e753730cfee4fd73b433549c5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 12:34:05 +0000 Subject: [PATCH 6/6] Document the openframe harden/precheck flow; bump version to 2.7.1 --- README.md | 20 +++++++++- docs/OPENFRAME.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 docs/OPENFRAME.md diff --git a/README.md b/README.md index 7d74f59..12e734b 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,17 @@ power: `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 @@ -453,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//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 @@ -528,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 @@ -538,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:** diff --git a/docs/OPENFRAME.md b/docs/OPENFRAME.md new file mode 100644 index 0000000..6103d25 --- /dev/null +++ b/docs/OPENFRAME.md @@ -0,0 +1,95 @@ +# Openframe flow with cf-cli + +This guide covers an `openframe_user_project` from pad configuration to precheck. The GPIO pad modes and +the power domains live in one spec, the `project.openframe` block of `.cf/project.json`. Everything else +is generated from it. + +```mermaid +flowchart LR + tui["cf gpio-config"] --> spec[".cf/project.json project.openframe"] + file["pinout.yaml / .json"] -->|"cf openframe import"| spec + spec -->|"cf openframe generate"| rtl["verilog/rtl/openframe_gpio.v"] + spec -->|"cf openframe generate"| pwr["openlane/openframe_project_wrapper/power.json"] + rtl --> harden["cf harden openframe_project_wrapper"] + pwr --> harden + harden --> precheck["cf precheck"] +``` + +## 1. Initialize + +```bash +cf init # choose type "openframe"; seeds project.openframe from .cf/openframe_default.json +cf setup # PDK, LibreLane (pinned in versions.json), precheck image +ipm install-dep --ip-root ip # CF_gpio_config (v1.2.0+ needed for pad overrides) +``` + +## 2. Configure the pads and power domains + +```bash +cf gpio-config # 44-pad grid +cf gpio-config --view # print the pinout +``` + +In the grid: 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 macro mappings, `d` validates and saves, +and `q` quits without saving. Saving also regenerates the files below. + +To edit by hand or exchange with other tools: + +```bash +cf openframe export pinout.yaml +cf openframe import pinout.yaml # validates, stores, regenerates +cf openframe import pinout.yaml --force # replace a different existing spec +``` + +The spec format, validation rules and JSON Schema are described in the README section +"Configure an Openframe Project". + +## 3. Generate + +```bash +cf openframe generate # write the generated files +cf openframe generate --check # exit 1 if any generated file is missing or stale (CI) +``` + +| File | Content | +|---|---| +| `verilog/rtl/openframe_gpio.v` | One `CF_gpio_config` per pad, with user ports named by the spec. `openframe_project_wrapper.v` instantiates it. | +| `openlane/openframe_project_wrapper/power.json` | `VDD_NETS`, `GND_NETS`, `PDN_MACRO_CONNECTIONS` (LibreLane 3.x) for the wrapper PDN. | + +Both files start with the `spec-sha256` of the spec they were generated from. Do not edit them by hand. + +## 4. Harden + +```bash +cf harden user_proj_timer # your macros first +cf harden openframe_project_wrapper # runs: librelane … power.json config.json +``` + +When a macro directory contains `power.json`, `cf harden` passes it before `config.*` on both the Nix and +Docker backends, exactly like `openlane/Makefile`. LibreLane has no config include and takes `meta` from +the last file, so `config.json` (flow and step reorder) must come last and must not set `VDD_NETS`, +`GND_NETS` or `PDN_MACRO_CONNECTIONS`. If `power.json` is missing, the wrapper PDN fails with an explicit +error instead of guessing nets. + +## 5. Precheck + +```bash +cf precheck +``` + +## Out-of-sync warnings + +`cf harden openframe_project_wrapper` and `cf precheck` compare the generated files with what +`project.openframe` would generate now. On a mismatch they print a warning panel with one line per file: + +| Reason | Meaning | +|---|---| +| `missing` | The file does not exist. | +| `generated from a different spec` | The `spec-sha256` header does not match the current spec (spec edited after generating). | +| `edited by hand since it was generated` | The header matches but the content differs. | +| `not generated by cf openframe generate` | The file has no `spec-sha256` header. | + +They also warn when `project.openframe` is missing or invalid (listing the validation errors). The command +still runs with the files on disk. Run `cf openframe generate` to bring them back in sync. Hardening other +macros does not warn, because they do not use the generated files. diff --git a/pyproject.toml b/pyproject.toml index 01a2fc5..bd0f0c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chipfoundry-cli" -version = "2.7.0" +version = "2.7.1" description = "CLI tool to automate ChipFoundry project submission to SFTP server" authors = ["ChipFoundry "] readme = "README.md"