diff --git a/chipcompiler/cli/app.py b/chipcompiler/cli/app.py index 57b60656..145705c7 100644 --- a/chipcompiler/cli/app.py +++ b/chipcompiler/cli/app.py @@ -7,6 +7,7 @@ from chipcompiler.cli.commands.doc import register_doc_commands from chipcompiler.cli.commands.doctor import register_doctor_commands +from chipcompiler.cli.commands.macro import macro_app from chipcompiler.cli.commands.param import param_app from chipcompiler.cli.commands.pdk import pdk_app from chipcompiler.cli.commands.project import register_project_commands @@ -77,6 +78,7 @@ def layout_image_cmd( register_project_commands(app) register_doctor_commands(app) app.add_typer(param_app, name="param") +app.add_typer(macro_app, name="macro") app.add_typer(pdk_app, name="pdk") app.add_typer(project_app, name="project") app.add_typer(workspace_app, name="workspace") diff --git a/chipcompiler/cli/command_handlers/macro.py b/chipcompiler/cli/command_handlers/macro.py new file mode 100644 index 00000000..f73bb8dc --- /dev/null +++ b/chipcompiler/cli/command_handlers/macro.py @@ -0,0 +1,324 @@ +"""Handlers for the manual macro-placement commands.""" + +import math + +from chipcompiler.cli.command_handlers.param import ( + _find_config_path, + _load_toml_overrides, + _manifest_mode_error, + _remove_param_from_toml, + _write_param_to_toml, +) +from chipcompiler.cli.core.records import error_record +from chipcompiler.cli.core.types import CommandContext, CommandResult +from chipcompiler.cli.project.params import lookup_schema +from chipcompiler.cli.project.workspace_params import ( + set_workspace_param, + workspace_param_value, +) +from chipcompiler.data.workspace.macro_location import MACRO_ORIENTATIONS + +MACRO_PARAM = "macro.placements" + + +def macro_set(args, ctx: CommandContext) -> CommandResult: + schema = lookup_schema(MACRO_PARAM) + entry, error = _entry(args) + if error is not None: + return error + + if getattr(args, "workspace", None) is not None: + return _workspace_set(args, ctx, schema, entry) + return _project_set(args, ctx, schema, entry) + + +def macro_remove(args, ctx: CommandContext) -> CommandResult: + schema = lookup_schema(MACRO_PARAM) + + if getattr(args, "workspace", None) is not None: + return _workspace_remove(args, ctx, schema) + return _project_remove(args, ctx, schema) + + +def macro_show(args, ctx: CommandContext) -> CommandResult: + schema = lookup_schema(MACRO_PARAM) + + if getattr(args, "workspace", None) is not None: + from chipcompiler.cli.command_handlers.workspace_params import _load_workspace + + workspace, workspace_error = _load_workspace(ctx) + if workspace_error is not None: + return workspace_error + placements, error = _workspace_placements(workspace, schema) + if error is not None: + return error + from chipcompiler.data.workspace import workspace_config_paths + + return CommandResult.ok( + [ + { + "param": MACRO_PARAM, + "placements": placements, + "file": str(workspace_config_paths(workspace.directory)["macro_location"]), + "source": "workspace", + "workspace": ctx.run_id, + } + ] + ) + + manifest_error = _manifest_mode_error(ctx) + if manifest_error is not None: + return manifest_error + placements, load_error = _project_placements(ctx) + if load_error is not None: + return load_error + return CommandResult.ok( + [ + { + "param": MACRO_PARAM, + "placements": placements, + "source": "ecc.toml", + } + ] + ) + + +# --------------------------------------------------------------------------- +# Workspace scope +# --------------------------------------------------------------------------- + + +def _workspace_set(args, ctx: CommandContext, schema, entry: dict) -> CommandResult: + from chipcompiler.cli.command_handlers.workspace_params import _mutate + + written: dict = {} + + def mutation(workspace): + current, error = _workspace_placements(workspace, schema) + if error is not None: + raise ValueError(error.records[0].get("reason", "invalid macro.placements")) + placements = _upsert(current, entry) + written["placements"] = placements + return set_workspace_param(workspace, schema, placements) + + result = _mutate(ctx, schema, mutation, None, "set") + _annotate(result, args.instance, written.get("placements")) + return result + + +def _workspace_remove(args, ctx: CommandContext, schema) -> CommandResult: + from chipcompiler.cli.command_handlers.workspace_params import _load_workspace, _mutate + + workspace, workspace_error = _load_workspace(ctx) + if workspace_error is not None: + return workspace_error + current, error = _workspace_placements(workspace, schema) + if error is not None: + return error + if not any(entry.get("instance") == args.instance for entry in current): + return CommandResult.ok( + [ + { + "param": MACRO_PARAM, + "instance": args.instance, + "placements": current, + "status": "absent", + "source": "workspace", + "workspace": ctx.run_id, + } + ] + ) + + remaining = [entry for entry in current if entry.get("instance") != args.instance] + written: dict = {} + + def mutation(workspace): + written["placements"] = remaining + return set_workspace_param(workspace, schema, remaining) + + result = _mutate(ctx, schema, mutation, None, "set") + record = _annotate(result, args.instance, written.get("placements")) + if record is not None: + record["status"] = "removed" + return result + + +def _workspace_placements(workspace, schema) -> tuple[list, CommandResult | None]: + value = workspace_param_value(workspace, schema) + if not isinstance(value, list): + return [], CommandResult.err( + [ + error_record( + "invalid_value", + param=MACRO_PARAM, + reason="macro.placements must be a list of placement objects", + ) + ], + exit_code=1, + ) + return value, None + + +# --------------------------------------------------------------------------- +# Project scope +# --------------------------------------------------------------------------- + + +def _project_set(args, ctx: CommandContext, schema, entry: dict) -> CommandResult: + manifest_error = _manifest_mode_error(ctx) + if manifest_error is not None: + return manifest_error + current, load_error = _project_placements(ctx) + if load_error is not None: + return load_error + + placements = _upsert(current, entry) + config_path = _find_config_path(ctx.project_dir) + if config_path is None: + return CommandResult.err([error_record("missing_config")], exit_code=1) + try: + _write_param_to_toml(config_path, schema, placements) + except (OSError, ValueError) as exc: + return CommandResult.err( + [error_record("config_error", param=MACRO_PARAM, reason=str(exc))], exit_code=1 + ) + + return CommandResult.ok( + [ + { + "param": MACRO_PARAM, + "instance": args.instance, + "x": args.x, + "y": args.y, + "orientation": args.orientation, + "placements": placements, + "status": "set", + "source": "ecc.toml", + } + ] + ) + + +def _project_remove(args, ctx: CommandContext, schema) -> CommandResult: + manifest_error = _manifest_mode_error(ctx) + if manifest_error is not None: + return manifest_error + current, load_error = _project_placements(ctx) + if load_error is not None: + return load_error + + if not any(entry.get("instance") == args.instance for entry in current): + return CommandResult.ok( + [ + { + "param": MACRO_PARAM, + "instance": args.instance, + "placements": current, + "status": "absent", + "source": "ecc.toml", + } + ] + ) + + remaining = [entry for entry in current if entry.get("instance") != args.instance] + config_path = _find_config_path(ctx.project_dir) + if config_path is None: + return CommandResult.err([error_record("missing_config")], exit_code=1) + try: + if remaining: + _write_param_to_toml(config_path, schema, remaining) + else: + _remove_param_from_toml(config_path, schema) + except (OSError, ValueError) as exc: + return CommandResult.err( + [error_record("config_error", param=MACRO_PARAM, reason=str(exc))], exit_code=1 + ) + + return CommandResult.ok( + [ + { + "param": MACRO_PARAM, + "instance": args.instance, + "placements": remaining, + "status": "removed", + "source": "ecc.toml", + } + ] + ) + + +def _project_placements(ctx: CommandContext) -> tuple[list, CommandResult | None]: + overrides, param_errors = _load_toml_overrides(ctx.project_dir) + if param_errors: + return [], CommandResult.err( + [error_record("invalid_param_config", reason=e) for e in param_errors] + ) + value = overrides.get(MACRO_PARAM, []) + if not isinstance(value, list): + return [], CommandResult.err( + [ + error_record( + "invalid_value", + param=MACRO_PARAM, + reason="macro.placements must be a list of placement objects", + ) + ], + exit_code=1, + ) + return value, None + + +# --------------------------------------------------------------------------- +# Shared helpers +# --------------------------------------------------------------------------- + + +def _entry(args) -> tuple[dict, CommandResult | None]: + if not args.instance.strip(): + return None, CommandResult.err( + [error_record("invalid_value", param=MACRO_PARAM, reason="instance name is empty")], + exit_code=1, + ) + if args.orientation not in MACRO_ORIENTATIONS: + return None, CommandResult.err( + [ + error_record( + "invalid_value", + param=MACRO_PARAM, + reason="orientation must be one of " + "/".join(sorted(MACRO_ORIENTATIONS)), + ) + ], + exit_code=1, + ) + if not math.isfinite(args.x) or not math.isfinite(args.y): + return None, CommandResult.err( + [error_record("invalid_value", param=MACRO_PARAM, reason="coordinates must be finite")], + exit_code=1, + ) + return ( + { + "instance": args.instance, + "x": args.x, + "y": args.y, + "orientation": args.orientation, + }, + None, + ) + + +def _upsert(placements: list, entry: dict) -> list: + remaining = [item for item in placements if item.get("instance") != entry["instance"]] + remaining.append(entry) + return remaining + + +def _annotate(result: CommandResult, instance: str, placements) -> dict | None: + """Describe the effective macro state on a workspace mutation result.""" + if not result.records or result.exit_code != 0 or placements is None: + return None + record = result.records[0] + record["instance"] = instance + record["placements"] = placements + if record.get("status") != "no_override": + record["status"] = "set" + return record diff --git a/chipcompiler/cli/commands/macro.py b/chipcompiler/cli/commands/macro.py new file mode 100644 index 00000000..7f8b493b --- /dev/null +++ b/chipcompiler/cli/commands/macro.py @@ -0,0 +1,127 @@ +from typing import Annotated + +import typer + +from chipcompiler.cli.command_handlers.macro import macro_remove as macro_remove_handler +from chipcompiler.cli.command_handlers.macro import macro_set as macro_set_handler +from chipcompiler.cli.command_handlers.macro import macro_show as macro_show_handler +from chipcompiler.cli.core.apps import create_app +from chipcompiler.cli.core.inputs import ( + MacroRemoveInput, + MacroSetInput, + MacroShowInput, + output_options, + project_options, +) +from chipcompiler.cli.core.invocation import CommandHandler, CommandInputT, execute_command +from chipcompiler.cli.core.options import ( + PlainOption, + ProjectOption, + WorkspaceOption, +) + +macro_app = create_app(help="Manage manual macro placement (macro_location.tcl)") + + +def _finish_macro( + macro_command: str, + command_input: CommandInputT, + handler: CommandHandler[CommandInputT], +) -> None: + execute_command("macro", command_input, handler, render_key=f"macro:{macro_command}") + + +@macro_app.command("set") +def set_cmd( + *, + instance: Annotated[str, typer.Argument()], + x: Annotated[float, typer.Option("--x", help="X coordinate in micrometers.")], + y: Annotated[float, typer.Option("--y", help="Y coordinate in micrometers.")], + orientation: Annotated[ + str, + typer.Option( + "--orient", + help="Orientation: R0, R90, R180, R270, MX, MY, MX90, or MY90.", + ), + ], + project: ProjectOption = None, + workspace: WorkspaceOption = None, + plain: PlainOption = False, +) -> None: + """Set one macro instance placement. + + Scopes: + + - project (default): stored in `ecc.toml` `[params.macro]`; rendered + into `config/macro_location.tcl` when a workspace is created or + refreshed. + - `--workspace NAME`: written to `home/params.toml`, the Tcl file is + regenerated immediately, and `macroPlacement` and its suffix are + marked pending. + + Coordinates are in micrometers and the instance is committed `fixed`. + While `macro.placements` is non-empty the `macroPlacement` step keeps + its load/save flow but skips DreamPlace macro placement. + + ```bash + ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 + ``` + """ + command_input = MacroSetInput( + output=output_options(plain=plain), + project=project_options(project), + instance=instance, + x=x, + y=y, + orientation=orientation, + workspace=workspace, + ) + _finish_macro("set", command_input, macro_set_handler) + + +@macro_app.command("remove") +def remove_cmd( + *, + instance: Annotated[str, typer.Argument()], + project: ProjectOption = None, + workspace: WorkspaceOption = None, + plain: PlainOption = False, +) -> None: + """Remove one macro instance placement. + + Removing the last entry clears `macro.placements`, so the next + `macroPlacement` run goes back to DreamPlace macro placement. + + ```bash + ecc macro remove u_ram0 + ``` + """ + command_input = MacroRemoveInput( + output=output_options(plain=plain), + project=project_options(project), + instance=instance, + workspace=workspace, + ) + _finish_macro("remove", command_input, macro_remove_handler) + + +@macro_app.command("show") +def show_cmd( + *, + project: ProjectOption = None, + workspace: WorkspaceOption = None, + plain: PlainOption = False, +) -> None: + """Show the manual macro placements and the generated Tcl path. + + ```bash + ecc macro show + ecc macro show --workspace baseline + ``` + """ + command_input = MacroShowInput( + output=output_options(plain=plain), + project=project_options(project), + workspace=workspace, + ) + _finish_macro("show", command_input, macro_show_handler) diff --git a/chipcompiler/cli/core/inputs.py b/chipcompiler/cli/core/inputs.py index 637f7407..63215699 100644 --- a/chipcompiler/cli/core/inputs.py +++ b/chipcompiler/cli/core/inputs.py @@ -222,6 +222,32 @@ class WorkspaceRefreshInput: workspace: str +@dataclass(frozen=True) +class MacroSetInput: + output: OutputOptions + project: ProjectOptions + instance: str + x: float + y: float + orientation: str + workspace: str | None = None + + +@dataclass(frozen=True) +class MacroRemoveInput: + output: OutputOptions + project: ProjectOptions + instance: str + workspace: str | None = None + + +@dataclass(frozen=True) +class MacroShowInput: + output: OutputOptions + project: ProjectOptions + workspace: str | None = None + + def output_options(*, plain: bool) -> OutputOptions: return OutputOptions(plain=plain) diff --git a/chipcompiler/cli/project/effective_config.py b/chipcompiler/cli/project/effective_config.py index 45162449..8e19234e 100644 --- a/chipcompiler/cli/project/effective_config.py +++ b/chipcompiler/cli/project/effective_config.py @@ -410,6 +410,10 @@ def layer_divergences(cfg, assembled: dict, entry) -> list[str]: def _backend_leaf_keys(schema) -> tuple[str, ...]: """The flattened backend key names a schema's maps_to target produces.""" maps_to = schema.maps_to + # Direct config/PDK parameters are applied through their explicit target + # and intentionally have no legacy backend projection. + if maps_to is None: + return () if isinstance(maps_to, str): return (maps_to,) return tuple(".".join((subtree, leaf)) for subtree, leaf in maps_to.items()) @@ -439,7 +443,10 @@ def _diverging_lower_keys(overrides: dict, resolve_lower) -> tuple[list[str], se continue coerced, type_err = _validate_schema_type(lower_value, schema) if type_err or coerced != override_value: - diverging.extend(leaf_keys) + # Direct config/PDK parameters have no backend leaf key. Keep the + # warning useful by identifying the canonical parameter instead + # of silently dropping the divergence or inventing a key. + diverging.extend(leaf_keys or [dotted]) return diverging, compared diff --git a/chipcompiler/cli/project/params.py b/chipcompiler/cli/project/params.py index b4db5c0b..5521a393 100644 --- a/chipcompiler/cli/project/params.py +++ b/chipcompiler/cli/project/params.py @@ -4,6 +4,7 @@ from chipcompiler.data.config_params import CONFIG_PARAM_SCHEMAS from chipcompiler.data.config_params.common import ParamSchema +from chipcompiler.data.config_params.macro import SCHEMAS as MACRO_SCHEMAS _LEGACY_PARAM_REGISTRY: tuple[ParamSchema, ...] = ( ParamSchema( @@ -175,7 +176,7 @@ ), ) -PARAM_REGISTRY = _LEGACY_PARAM_REGISTRY + CONFIG_PARAM_SCHEMAS +PARAM_REGISTRY = _LEGACY_PARAM_REGISTRY + CONFIG_PARAM_SCHEMAS + MACRO_SCHEMAS _REGISTRY_INDEX: dict[str, ParamSchema] = {s.param: s for s in PARAM_REGISTRY} _REQUIRED_FIELDS = ( diff --git a/chipcompiler/cli/project/workspace_params.py b/chipcompiler/cli/project/workspace_params.py index e03481b0..cc56cfa0 100644 --- a/chipcompiler/cli/project/workspace_params.py +++ b/chipcompiler/cli/project/workspace_params.py @@ -17,6 +17,7 @@ "synthesis": "Synthesis", "floorplan": "preFloorplan", "placement": "place", + "macro": "macroPlacement", "cts": "CTS", "routing": "route", "filler": "filler", diff --git a/chipcompiler/data/config_params/macro.py b/chipcompiler/data/config_params/macro.py new file mode 100644 index 00000000..a464aca2 --- /dev/null +++ b/chipcompiler/data/config_params/macro.py @@ -0,0 +1,19 @@ +from .common import ParamSchema + +SCHEMAS = ( + ParamSchema( + param="macro.placements", + group="macro", + name="placements", + type="json", + default=[], + applies="macro", + maps_to={"macro": "placements"}, + description=( + "Manual hard-macro placements rendered into config/macro_location.tcl. " + "Each entry is {instance, x, y, orientation} with micron coordinates; " + "instances are committed fixed. When set, macroPlacement skips DreamPlace." + ), + example='[{"instance": "u0", "x": 10.0, "y": 20.0, "orientation": "R0"}]', + ), +) diff --git a/chipcompiler/data/workspace/__init__.py b/chipcompiler/data/workspace/__init__.py index 247adc4a..e4122767 100644 --- a/chipcompiler/data/workspace/__init__.py +++ b/chipcompiler/data/workspace/__init__.py @@ -30,6 +30,7 @@ ) from .filelist_copy import copy_filelist_with_sources as copy_filelist_with_sources from .layout import EccData, WorkspaceStepBase +from .macro_location import refresh_generated_macro_location from .sdc import create_default_sdc as create_default_sdc from .sdc import refresh_generated_sdc @@ -161,7 +162,7 @@ def log_workspace_step(step: WorkspaceStep, logger: Logger): StepEnum.CTS.value: "cts_ecc.json", StepEnum.DRC.value: "drc_ecc.json", StepEnum.FLOORPLAN.value: "floorplan_ecc.json", - "macro_location": "macro_localtion.tcl", + "macro_location": "macro_location.tcl", StepEnum.ROUTING.value: "route_ecc.json", StepEnum.FILLER.value: "filler_ecc.json", StepEnum.RCX.value: "rcx_ecc.json", @@ -174,6 +175,7 @@ def log_workspace_step(step: WorkspaceStep, logger: Logger): StepEnum.CTS.value: "cts_default_config.json", StepEnum.DRC.value: "drc_default_config.json", StepEnum.FLOORPLAN.value: "fp_default_config.json", + "macro_location": "macro_localtion.tcl", StepEnum.ROUTING.value: "rt_default_config.json", StepEnum.FILLER.value: "pl_default_config.json", StepEnum.RCX.value: "rcx.json", @@ -674,6 +676,9 @@ def init_workspace_config(workspace: Workspace) -> None: ecc_config_dir = root_dir / "tools" / "ecc" / "configs" dreamplace_config = root_dir / "tools" / "ecc_dreamplace" / "configs" / "dreamplace_ecc.json" + if workspace.directory is not None: + migrate_workspace_config_filenames(workspace.directory) + _copy_missing_files(ecc_config_dir, config_dir) if not workspace.config["dreamplace"].exists(): shutil.copy2(dreamplace_config, workspace.config["dreamplace"]) @@ -695,6 +700,7 @@ def refresh_workspace_config(workspace: Workspace) -> None: workspace.config = build_workspace_config_paths(workspace) refresh_generated_sdc(workspace) + refresh_generated_macro_location(workspace) db = json_read(workspace.config["db"]) if "INPUT" not in db or "LayerSettings" not in db: diff --git a/chipcompiler/data/workspace/macro_location.py b/chipcompiler/data/workspace/macro_location.py new file mode 100644 index 00000000..ae3d2e21 --- /dev/null +++ b/chipcompiler/data/workspace/macro_location.py @@ -0,0 +1,87 @@ +"""Workspace macro-location Tcl generation. + +``macro_placements`` reads the manual hard-macro placements from the +workspace parameters; ``refresh_generated_macro_location`` renders them +into ``config/macro_location.tcl``. An empty placement list leaves the +file untouched so the seeded template and DreamPlace's own handoff +survive parameter refreshes. +""" + +import math +from pathlib import Path +from typing import TYPE_CHECKING, Final + +from chipcompiler.utility.file import write_text_atomic + +if TYPE_CHECKING: + from chipcompiler.data import Workspace + +MACRO_LOCATION_MARKER: Final[str] = "# Auto-generated macro location file" + +MACRO_ORIENTATIONS: Final[frozenset[str]] = frozenset( + {"R0", "R90", "R180", "R270", "MX", "MY", "MX90", "MY90"} +) + + +def macro_placements(workspace: "Workspace") -> list[dict]: + """Return the manual macro placement entries stored in the parameters.""" + macro = workspace.parameters.data.get("macro", {}) + if not isinstance(macro, dict): + return [] + placements = macro.get("placements", []) + return placements if isinstance(placements, list) else [] + + +def validate_placements(placements: list[dict]) -> list[str]: + """Return one error message per invalid placement entry.""" + errors = [] + for index, entry in enumerate(placements): + if not isinstance(entry, dict): + errors.append(f"placement #{index + 1} is not an object") + continue + instance = entry.get("instance") + if not isinstance(instance, str) or not instance.strip(): + errors.append(f"placement #{index + 1} has an empty instance name") + for key in ("x", "y"): + value = entry.get(key) + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + ): + errors.append(f"placement #{index + 1} has a non-finite {key} coordinate") + if entry.get("orientation") not in MACRO_ORIENTATIONS: + errors.append( + "placement #{index + 1} orientation must be one of " + + "/".join(sorted(MACRO_ORIENTATIONS)) + ) + return errors + + +def render_macro_location_tcl(placements: list[dict]) -> str: + """Render the placeInstance handoff consumed by postFloorplan (iFP).""" + lines = [MACRO_LOCATION_MARKER, ""] + for entry in placements: + lines.append( + "placeInstance {} {:g} {:g} {}".format( + entry["instance"], entry["x"], entry["y"], entry["orientation"] + ) + ) + lines.append(f"setInstancePlacementStatus -status fixed -name {entry['instance']}") + return "\n".join(lines) + "\n" + + +def refresh_generated_macro_location(workspace: "Workspace") -> None: + """Regenerate config/macro_location.tcl from the manual placements.""" + placements = macro_placements(workspace) + if not placements: + return + + errors = validate_placements(placements) + if errors: + raise ValueError("; ".join(errors)) + + target = workspace.config.get("macro_location") + if not target: + return + write_text_atomic(Path(target), render_macro_location_tcl(placements)) diff --git a/chipcompiler/docs/ecc-config-ref.cn.md b/chipcompiler/docs/ecc-config-ref.cn.md index 0ecfb01b..614fd5bb 100644 --- a/chipcompiler/docs/ecc-config-ref.cn.md +++ b/chipcompiler/docs/ecc-config-ref.cn.md @@ -16,7 +16,7 @@ ├── home/ │ ├── params.toml # 参数中枢:用户参数 + PDK 派生值(见 §1) │ └── flow.json # 步骤状态 -├── config/ # ← 本文档的主角:9 个 JSON +├── config/ # ← 本文档的主角:9 个 JSON + Tcl 宏位置交接文件 │ ├── db_ecc.json # 数据库构建(读入 LEF/DEF/网表/LIB/SDC,每个 ecc 步骤共用) │ ├── floorplan_ecc.json # 布局规划 │ ├── cts_ecc.json # 时钟树综合 @@ -25,7 +25,8 @@ │ ├── filler_ecc.json # 填充单元 │ ├── rcx_ecc.json # 寄生提取 │ ├── sta_ecc.json # 静态时序分析(多 corner) -│ └── dreamplace_ecc.json# DreamPlace 布局/合法化(placement 与 legalization 共用) +│ ├── dreamplace_ecc.json# DreamPlace 布局/合法化(placement 与 legalization 共用) +│ └── macro_location.tcl # 宏摆放 Tcl 交接文件(见 §1.5;由 macroPlacement 或 macro.placements 参数写出) ├── Synthesis_yosys/ │ └── data/global_var.tcl # 综合步骤的"配置"(Tcl 变量,非 JSON) ├── lec_yosys_lec/ # 综合级 LEC(Tcl 脚本驱动) @@ -67,8 +68,8 @@ graph LR | synthesis | — | `global_var.tcl`(Tcl) | Yosys 用 Tcl 变量驱动,不走 JSON | | lec | — | 无(Tcl) | 综合级 Yosys LEC;比较综合网表与 golden 网表;未证明时步骤失败并终止后续流程 | | preFloorplan | ✓ | `floorplan_ecc.json` | 自动宏布局 | -| macroPlacement | — | `dreamplace_ecc.json` + `macro_localtion.tcl` | 写入 Tcl 宏摆放交接文件 | -| postFloorplan | ✓ | `floorplan_ecc.json` + `macro_localtion.tcl` | 读取 Tcl 宏摆放交接文件 | +| macroPlacement | — | `dreamplace_ecc.json` + `macro_location.tcl` | 写入 Tcl 宏摆放交接文件(设置了 `macro.placements` 时跳过 DreamPlace,见 §1.5) | +| postFloorplan | ✓ | `floorplan_ecc.json` + `macro_location.tcl` | 读取 Tcl 宏摆放交接文件 | | placement | — | `dreamplace_ecc.json` | 与 legalization 共用一个文件 | | cts | ✓ | `cts_ecc.json` | | | legalization | — | `dreamplace_ecc.json` | 每步重写 `def_input`/`result_dir` 等 | @@ -187,6 +188,20 @@ tech = "prtech/techLEF/N551P6M_ecos.lef" 完整命令输出示例见 [ECC CLI 用户指南 §9](ecc-user-guide.cn.md)(终端:`ecc doc ug --lang cn`)。 +### 1.5 手工宏摆放(`ecc macro`) + +`macro.placements` 保存手工硬宏摆放,取值为 `{instance, x, y, orientation}` 对象的 JSON 数组(坐标单位微米;实例以 `fixed` 状态提交)。它与 §1.1 的语义参数同类——不写入任何 `config/*.json` 字段,而是在创建或刷新 workspace 配置时渲染进 `config/macro_location.tcl`。只要它非空,`macroPlacement` 保留 load/save 流程但跳过 DreamPlace 宏摆放、保留生成的交接文件;`postFloorplan` 随后依据该文件提交宏。清空该参数即恢复 DreamPlace 自动摆放。 + +| 命令 | 作用 | +|---|---| +| `ecc macro set INSTANCE --x X --y Y --orient ORIENT` | 按 instance 新增/更新一条摆放(方向:`R0`、`R90`、`R180`、`R270`、`MX`、`MY`、`MX90`、`MY90`) | +| `ecc macro remove INSTANCE` | 删除一条摆放;删除最后一个条目会清空该参数 | +| `ecc macro show` | 列出当前摆放与生成的 Tcl 路径 | + +`ecc param` 的两种 scope 都适用:项目 scope(默认)把列表存进 `ecc.toml` `[params.macro]`,在下一次新建 run 或 `ecc workspace refresh` 时生效;`--workspace NAME` 写入 `home/params.toml`,立即重生成 Tcl,并把 `macroPlacement` 及其后缀标记为待重跑。该列表也可以按普通 JSON 参数设置,例如 `ecc param set macro.placements '[{"instance": "u0", "x": 10.0, "y": 20.0, "orientation": "R0"}]'`。 + +交接文件必须覆盖设计中的全部硬宏,否则 `postFloorplan` 会报出缺失的实例名。文件格式详见 [floorplan-flow.cn.md](floorplan-flow.cn.md)。 + ## 2. 公共配置:db_ecc.json 所有 ecc 工具步骤共用。每个步骤启动时先用它把 LEF/DEF/网表/LIB 载入内存数据库(subflow 的 "load data" 阶段)。`INPUT.def_path/verilog_path` 与 `OUTPUT.output_dir_path` 三个字段**每步运行前被重写**,实现步骤间文件链。 @@ -230,7 +245,7 @@ tech = "prtech/techLEF/N551P6M_ecos.lef" ## 4. floorplan(ecc-tools) -配置 `floorplan_ecc.json` 由 `preFloorplan` 和 `postFloorplan` 共享。`preFloorplan` 执行 load data → init simple floorplan → save data,并使用自动宏摆放;`macroPlacement` 执行仅宏单元摆放,并通过 `tcl_save` 写入 `config/macro_localtion.tcl` 形成交接检查点;`postFloorplan` 以 `file` 模式读取该文件,再执行 load data → create tracks → place IO pins → tap cells → PDN → set clock net → save data → analysis。 +配置 `floorplan_ecc.json` 由 `preFloorplan` 和 `postFloorplan` 共享。`preFloorplan` 执行 load data → init simple floorplan → save data,并使用自动宏摆放;`macroPlacement` 执行仅宏单元摆放,并通过 `tcl_save` 写入 `config/macro_location.tcl` 形成交接检查点(设置了 `macro.placements` 参数时跳过 DreamPlace,交接文件改由参数渲染,见 §1.5);`postFloorplan` 以 `file` 模式读取该文件,再执行 load data → create tracks → place IO pins → tap cells → PDN → set clock net → save data → analysis。 ### ifp(iFP 布图引擎) @@ -244,7 +259,7 @@ tech = "prtech/techLEF/N551P6M_ecos.lef" | 参数 | 默认 | 含义 | |---|---|---| | `mode` | `auto` | `auto` 自动摆放宏单元;`file` 从 `file_path` 读取宏位置 | -| `file_path` | `""` | `mode=file` 时使用的宏位置文件 | +| `file_path` | `""` | `mode=file` 时使用的宏位置文件;流程固定指向 `config/macro_location.tcl` | | `macro_placement_halo` | 3.0 | 宏单元布置 halo(µm,禁止标准单元靠近的范围) | | `macro_routing_halo` | 3.0 | 宏单元绕线 halo(µm,禁止绕线的范围) | diff --git a/chipcompiler/docs/ecc-config-ref.en.md b/chipcompiler/docs/ecc-config-ref.en.md index 6de0eb2a..3d88185f 100644 --- a/chipcompiler/docs/ecc-config-ref.en.md +++ b/chipcompiler/docs/ecc-config-ref.en.md @@ -16,7 +16,7 @@ Each run's workspace has a shared `config/` directory where the JSON configurati ├── home/ │ ├── params.toml # parameter hub: user params + PDK-derived values (see §1) │ └── flow.json # step status -├── config/ # ← this document's focus: 9 JSON files +├── config/ # ← this document's focus: 9 JSON files + the Tcl macro-location handoff │ ├── db_ecc.json # database build (loads LEF/DEF/netlist/LIB/SDC; shared by every ecc step) │ ├── floorplan_ecc.json # floorplanning │ ├── cts_ecc.json # clock tree synthesis @@ -25,7 +25,8 @@ Each run's workspace has a shared `config/` directory where the JSON configurati │ ├── filler_ecc.json # filler cells │ ├── rcx_ecc.json # parasitic extraction │ ├── sta_ecc.json # static timing analysis (multi-corner) -│ └── dreamplace_ecc.json# DreamPlace placement/legalization (shared by placement and legalization) +│ ├── dreamplace_ecc.json# DreamPlace placement/legalization (shared by placement and legalization) +│ └── macro_location.tcl # macro-placement Tcl handoff (see §1.5; written by macroPlacement or the macro.placements parameter) ├── Synthesis_yosys/ │ └── data/global_var.tcl # the synthesis step's "config" (Tcl variables, not JSON) ├── lec_yosys_lec/ # synthesis-level LEC (Tcl-script driven) @@ -67,8 +68,8 @@ Distilled from real `ecc config ` output (maps to the source `_STEP_CONFIG | synthesis | — | `global_var.tcl` (Tcl) | Yosys is driven by Tcl variables, not JSON | | lec | — | none (Tcl) | Synthesis-level Yosys LEC; compares the mapped and golden synthesis netlists; an unproven result fails the step and stops the flow | | preFloorplan | ✓ | `floorplan_ecc.json` | automatic macro floorplanning | -| macroPlacement | — | `dreamplace_ecc.json` + `macro_localtion.tcl` | writes the Tcl macro-placement handoff | -| postFloorplan | ✓ | `floorplan_ecc.json` + `macro_localtion.tcl` | reads the Tcl macro-placement handoff | +| macroPlacement | — | `dreamplace_ecc.json` + `macro_location.tcl` | writes the Tcl macro-placement handoff (skips DreamPlace when `macro.placements` is set, see §1.5) | +| postFloorplan | ✓ | `floorplan_ecc.json` + `macro_location.tcl` | reads the Tcl macro-placement handoff | | placement | — | `dreamplace_ecc.json` | shares one file with legalization | | cts | ✓ | `cts_ecc.json` | | | legalization | — | `dreamplace_ecc.json` | `def_input`/`result_dir` etc. rewritten per step | @@ -185,6 +186,20 @@ For one-off overrides use `ecc run --set KEY=VALUE`: it applies only when the wo For full command output examples, see [ECC CLI User Guide §9](ecc-user-guide.en.md) (`ecc doc ug`). +### 1.5 Manual macro placement (`ecc macro`) + +`macro.placements` holds manual hard-macro placements as a JSON array of `{instance, x, y, orientation}` entries (micron coordinates; instances are committed `fixed`). It is a semantic parameter like the §1.1 set — it is not written to any `config/*.json` field but rendered into `config/macro_location.tcl` whenever the workspace configuration is created or refreshed. While it is non-empty, `macroPlacement` keeps its load/save flow but skips DreamPlace macro placement and leaves the generated handoff untouched; `postFloorplan` then commits the macros from the file. Clearing the parameter restores automatic DreamPlace placement. + +| Command | What it does | +|---|---| +| `ecc macro set INSTANCE --x X --y Y --orient ORIENT` | Upsert one instance placement (orientation: `R0`, `R90`, `R180`, `R270`, `MX`, `MY`, `MX90`, `MY90`) | +| `ecc macro remove INSTANCE` | Remove one instance placement; removing the last entry clears the parameter | +| `ecc macro show` | List the placements and the generated Tcl path | + +Both `ecc param` scopes apply: project scope (default) stores the list in `ecc.toml` `[params.macro]` and takes effect on the next fresh run or `ecc workspace refresh`; `--workspace NAME` writes `home/params.toml`, regenerates the Tcl immediately, and marks `macroPlacement` and its suffix pending. The list is also accepted as a plain JSON parameter, e.g. `ecc param set macro.placements '[{"instance": "u0", "x": 10.0, "y": 20.0, "orientation": "R0"}]'`. + +The file must cover every hard macro in the design — `postFloorplan` fails with the missing instance names otherwise. See [floorplan-flow.en.md](floorplan-flow.en.md) for the handoff format. + ## 2. Shared configuration: db_ecc.json Shared by all ecc tool steps. At step startup it is used to load LEF/DEF/netlist/LIB into the in-memory database (the subflow's "load data" phase). `INPUT.def_path/verilog_path` and `OUTPUT.output_dir_path` are **rewritten before every step run**, implementing the file chain between steps. @@ -228,7 +243,7 @@ There is also the environment variable `YOSYS_SYNTH_STRATEGY` (e.g. `DELAY 4` / ## 4. floorplan (ecc-tools) -Configuration file `floorplan_ecc.json` is shared by the `preFloorplan` and `postFloorplan` steps. `preFloorplan` runs load data → init simple floorplan → save data with automatic macro placement; `macroPlacement` runs macro-only placement, writes `config/macro_localtion.tcl` through `tcl_save`, and forms the handoff checkpoint; `postFloorplan` consumes that file in `file` mode, then runs load data → create tracks → place IO pins → tap cells → PDN → set clock net → save data → analysis. +Configuration file `floorplan_ecc.json` is shared by the `preFloorplan` and `postFloorplan` steps. `preFloorplan` runs load data → init simple floorplan → save data with automatic macro placement; `macroPlacement` runs macro-only placement, writes `config/macro_location.tcl` through `tcl_save`, and forms the handoff checkpoint (when the `macro.placements` parameter is set, DreamPlace is skipped and the handoff is rendered from the parameter, see §1.5); `postFloorplan` consumes that file in `file` mode, then runs load data → create tracks → place IO pins → tap cells → PDN → set clock net → save data → analysis. ### ifp (the iFP floorplan engine) @@ -242,7 +257,7 @@ Configuration file `floorplan_ecc.json` is shared by the `preFloorplan` and `pos | Parameter | Default | Meaning | |---|---|---| | `mode` | `auto` | `auto` places macros automatically; `file` reads macro locations from `file_path` | -| `file_path` | `""` | Macro-location file used when `mode=file` | +| `file_path` | `""` | Macro-location file used when `mode=file`; the flow always points it at `config/macro_location.tcl` | | `macro_placement_halo` | 3.0 | Placement halo around macros (µm; region where standard cells may not come close) | | `macro_routing_halo` | 3.0 | Routing halo around macros (µm; region where routing is banned) | diff --git a/chipcompiler/docs/ecc-tutorial.cn.md b/chipcompiler/docs/ecc-tutorial.cn.md index 384d4e1d..ae821d4b 100644 --- a/chipcompiler/docs/ecc-tutorial.cn.md +++ b/chipcompiler/docs/ecc-tutorial.cn.md @@ -268,7 +268,7 @@ ecc run --preset rtl2gds | 1 | synthesis | yosys | RTL 综合、工艺映射(slang 前端读入 SystemVerilog) | | 2 | lec | yosys_lec | 逻辑等价性检查:综合网表与 golden 网表 | | 3 | pre_floorplan | ecc | 执行 simple floorplan,并使用自动 macro placement | -| 4 | macro_placement | dreamplace | 执行仅宏单元摆放;这是宏位置文件被读取前的交接检查点 | +| 4 | macro_placement | dreamplace | 执行仅宏单元摆放(可用 `ecc macro` 手工指定位置,见 §6.5);这是宏位置文件被读取前的交接检查点 | | 5 | post_floorplan | ecc | 读取宏位置文件,完成 tracks、IO pin、tap cell、PDN 和时钟网设置 | | 6 | placement | dreamplace | 全局布局 | | 7 | cts | ecc | 时钟树综合(含扇出约束) | @@ -703,6 +703,29 @@ ecc config placement # 该步在 workspace config/ 下实际用的配置文 ecc config --plain # 项目级配置(键值 + 解析后绝对路径) ``` +### 6.5 手动摆放宏单元(含硬宏的设计) + +gcd 是纯数字设计,没有宏单元。换成带 SRAM/模拟硬宏的设计时,步骤 4 `macro_placement` 默认由 DreamPlace 自动摆放,位置写入 workspace 的 `config/macro_location.tcl`。想自己决定宏的位置(比如让 SRAM 阵列贴着数据通路摆),用 `ecc macro` 命令组,不要手工编辑 Tcl: + +```bash +ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 # 新增/更新一个宏的位置(µm,upsert) +ecc macro set u_ram1 --x 150 --y 20.5 --orient MY # 方向:R0/R90/R180/R270/MX/MY/MX90/MY90 +ecc macro show # 查看已设置的摆放与生成的 Tcl 路径 +ecc macro remove u_ram0 # 删除一条;删空后恢复 DreamPlace 自动摆放 +``` + +按用途选一种方式: + +- **开跑前定好位置**(项目级):直接 `ecc macro set ...`——写入 `ecc.toml` 的 `[params.macro]`,之后新建的 workspace(`ecc run` / `--overwrite` / `ecc workspace refresh`)都会按它渲染 `config/macro_location.tcl`; +- **在已有 workspace 上调整**:加 `--workspace default`——Tcl 立即重生成,`macroPlacement` 及其后缀标记为待重跑;随后 `ecc run --workspace default` 从 `macroPlacement` 续跑:DreamPlace 摆放被跳过,`post_floorplan` 按文件把宏以 `fixed` 提交,place 及之后的步骤全部重跑。 + +```bash +ecc macro set u_ram0 --x 120.0 --y 80.0 --orient MY --workspace default +ecc run --workspace default +``` + +列表必须覆盖设计中的**全部**硬宏且实例名真实存在,否则 `post_floorplan` 按缺失实例名失败。命令细节见[用户指南 §9.5](ecc-user-guide.cn.md#95-macro--手动宏单元摆放),三阶段 floorplan 与交接文件格式见 [floorplan-flow.cn.md](floorplan-flow.cn.md)。 + ## 7. 常见问题 | 症状 | 原因 | 处理 | diff --git a/chipcompiler/docs/ecc-tutorial.en.md b/chipcompiler/docs/ecc-tutorial.en.md index 5e2ff8a0..da6f7cbf 100644 --- a/chipcompiler/docs/ecc-tutorial.en.md +++ b/chipcompiler/docs/ecc-tutorial.en.md @@ -269,7 +269,7 @@ In an interactive terminal the CLI renders live per-step progress and log tails; | 1 | synthesis | yosys | RTL synthesis and technology mapping (slang frontend reads SystemVerilog) | | 2 | lec | yosys_lec | Logic equivalence check: synthesis netlist vs its golden netlist | | 3 | pre_floorplan | ecc | Build the simple floorplan with automatic macro placement | -| 4 | macro_placement | dreamplace | Run macro-only placement; this is the handoff checkpoint before the macro-location file is consumed | +| 4 | macro_placement | dreamplace | Run macro-only placement (positions can also be set manually with `ecc macro`, see §6.5); this is the handoff checkpoint before the macro-location file is consumed | | 5 | post_floorplan | ecc | Read the macro-location file; create tracks, IO pins, tap cells, PDN, and clock-net setup | | 6 | placement | dreamplace | Global placement | | 7 | cts | ecc | Clock tree synthesis (incl. fanout limits) | @@ -704,6 +704,29 @@ ecc config placement # config files actually used by that step under the work ecc config --plain # project-level config (key=value + resolved absolute paths) ``` +### 6.5 Placing macros manually (designs with hard macros) + +gcd is a pure digital design with no macros. For a design with SRAM/analog hard macros, step 4 `macro_placement` places them automatically with DreamPlace and writes the positions to the workspace's `config/macro_location.tcl`. To decide the macro positions yourself (for example, pinning an SRAM array along the datapath), use the `ecc macro` command group instead of editing the Tcl by hand: + +```bash +ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 # add/update one macro position (µm, upsert) +ecc macro set u_ram1 --x 150 --y 20.5 --orient MY # orientations: R0/R90/R180/R270/MX/MY/MX90/MY90 +ecc macro show # list the placements and the generated Tcl path +ecc macro remove u_ram0 # remove one; clearing the list restores DreamPlace auto placement +``` + +Pick the scope that matches your intent: + +- **Fix positions before running** (project scope): run `ecc macro set ...` without a selector — it writes `ecc.toml`'s `[params.macro]`, and every workspace created afterwards (`ecc run` / `--overwrite` / `ecc workspace refresh`) renders `config/macro_location.tcl` from it; +- **Adjust an existing workspace**: add `--workspace default` — the Tcl is regenerated immediately and `macroPlacement` and its suffix are marked pending. The following `ecc run --workspace default` resumes from `macroPlacement`: DreamPlace placement is skipped, `post_floorplan` commits the macros `fixed` from the file, and everything from place onwards re-runs. + +```bash +ecc macro set u_ram0 --x 120.0 --y 80.0 --orient MY --workspace default +ecc run --workspace default +``` + +The list must cover **every** hard macro in the design and use real instance names, otherwise `post_floorplan` fails with the missing instance names. See [User Guide §9.5](ecc-user-guide.en.md#95-macro--manual-macro-placement) for the command details and [floorplan-flow.en.md](floorplan-flow.en.md) for the staged floorplan and the handoff format. + ## 7. Troubleshooting | Symptom | Cause | Fix | diff --git a/chipcompiler/docs/ecc-user-guide.cn.md b/chipcompiler/docs/ecc-user-guide.cn.md index 20d9ccce..e755a266 100644 --- a/chipcompiler/docs/ecc-user-guide.cn.md +++ b/chipcompiler/docs/ecc-user-guide.cn.md @@ -765,6 +765,59 @@ tech = "prtech/techLEF/N551P6M_ecos.lef" 优先级:CLI `--set` > `ecc.toml` `[params.*]` > 模板默认值。`pdk.*` 路径参数写入 `[pdk.overrides]`:`pdk.tech`、`pdk.lefs`、`pdk.libs`、`pdk.mapping_file` 相对 `pdk.root` 解析,`pdk.sdc` 和 `pdk.spef` 相对项目目录解析;六者都会校验文件。 +## 9.5. macro — 手动宏单元摆放 + +```bash +ecc macro set INSTANCE --x X --y Y --orient ORIENT [--project DIR] [--workspace NAME] [--plain] +ecc macro remove INSTANCE [--project DIR] [--workspace NAME] [--plain] +ecc macro show [--project DIR] [--workspace NAME] [--plain] +``` + +含硬宏(SRAM 等)的设计默认在 `macroPlacement` 步骤由 DreamPlace 自动摆放,结果写入 `config/macro_location.tcl`。`ecc macro` 管理手工摆放参数 `macro.placements`:坐标单位微米,实例以 `fixed` 状态提交。参数非空时 `macroPlacement` 保留 load/save 流程但跳过 DreamPlace,由 `postFloorplan` 按该文件提交宏。方向取值 `R0`、`R90`、`R180`、`R270`、`MX`、`MY`、`MX90`、`MY90`;同一实例重复 `set` 为原地更新。 + +与 `ecc param` 相同的两种 scope: + +- 项目(默认):写入 `ecc.toml` `[params.macro]`,在下一次新建 workspace(`ecc run` / `--overwrite`)或 `ecc workspace refresh` 时渲染进 Tcl; +- `--workspace NAME`:写入该 workspace 的 `home/params.toml`,立即重生成 `config/macro_location.tcl`,并把 `macroPlacement` 及其后缀标记为待执行,之后 `ecc run --workspace NAME` 从 `macroPlacement` 续跑。 + +```console +$ ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 +[status] + param: macro.placements + instance: u_ram0 + x: 10.0 + y: 20.5 + orientation: R0 + placements: [{'instance': 'u_ram0', 'x': 10.0, 'y': 20.5, 'orientation': 'R0'}] + status: set + source: ecc.toml + +$ ecc macro set u_ram1 --x 150 --y 20.5 --orient MY +$ ecc macro show +[result] + param: macro.placements + placements: [{'instance': 'u_ram0', 'x': 10.0, 'y': 20.5, 'orientation': 'R0'}, {'instance': 'u_ram1', 'x': 150.0, 'y': 20.5, 'orientation': 'MY'}] + source: ecc.toml + +$ ecc macro remove u_ram1 +[status] + param: macro.placements + instance: u_ram1 + placements: [{'instance': 'u_ram0', 'x': 10.0, 'y': 20.5, 'orientation': 'R0'}] + status: removed + source: ecc.toml +``` + +在已有 workspace 上调整宏位置并重跑受影响片段: + +```bash +ecc macro set u_ram0 --x 120.0 --y 80.0 --orient MY --workspace default +ecc run --workspace default # 从 macroPlacement 续跑,下游步骤一并重跑 +ecc macro remove u_ram0 --workspace default # 删除最后一个条目后恢复 DreamPlace 自动摆放,再续跑即回到自动结果 +``` + +实例名必须存在于设计中,且必须列出全部硬宏——`postFloorplan` 会按缺失实例名报错;`macro_location.tcl` 是生成物,不支持手工编辑(未设置该参数时重跑 `macroPlacement` 会重新生成)。交接文件格式与三阶段 floorplan 细节见 [floorplan-flow.cn.md](floorplan-flow.cn.md),参数说明见[配置参考 §1.5](ecc-config-ref.cn.md)。 + ## 10. pdk — PDK 路径配置 PDK 本体由安装脚本(`--with-toolchain`,见教程)或手动 clone 获取。已就绪的 PDK 用 `ecc pdk set-root` 接入(写入 `ecc.toml` 的 `[pdk] root`,自动展开为绝对路径;目录必须已存在)。内容不完整(如还没 `make unzip`)不阻断设置,会给出提示: @@ -1039,4 +1092,12 @@ ecc report qor --workspace exp1 ecc run --workspace pnr --from prefloorplan --to route ``` +含硬宏的设计可改用手工摆放(完整说明见 §9.5): + +```bash +ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 --workspace default +ecc run --workspace default # 从 macroPlacement 续跑:跳过 DreamPlace,按手工位置提交宏 +ecc macro show --workspace default +``` + `project.json` 生成后,项目级查看、签核和报告命令按已声明的 workspace 选择;只有一个活跃 workspace 时自动选中,多个活跃 workspace 时必须显式传 `--workspace NAME`(否则报 `workspace_required` 并列出可用名称)。不再使用的 workspace 可在 `project.json` 中把其 `status` 改为 `archived`,使其退出自动选择。 diff --git a/chipcompiler/docs/ecc-user-guide.en.md b/chipcompiler/docs/ecc-user-guide.en.md index 88772370..cfa69b0d 100644 --- a/chipcompiler/docs/ecc-user-guide.en.md +++ b/chipcompiler/docs/ecc-user-guide.en.md @@ -807,6 +807,59 @@ Legacy semantic parameters: Priority: CLI `--set` > `ecc.toml` `[params.*]` > template defaults. `pdk.*` path parameters write to `[pdk.overrides]`: `pdk.tech`, `pdk.lefs`, `pdk.libs`, and `pdk.mapping_file` resolve relative to `pdk.root`, while `pdk.sdc` and `pdk.spef` resolve relative to the project directory; all are file-validated. +## 9.5. macro — manual macro placement + +```bash +ecc macro set INSTANCE --x X --y Y --orient ORIENT [--project DIR] [--workspace NAME] [--plain] +ecc macro remove INSTANCE [--project DIR] [--workspace NAME] [--plain] +ecc macro show [--project DIR] [--workspace NAME] [--plain] +``` + +A design with hard macros (SRAMs, analog blocks) places them automatically with DreamPlace in the `macroPlacement` step, writing `config/macro_location.tcl`. `ecc macro` manages the manual-placement parameter `macro.placements`: coordinates are in microns and instances are committed `fixed`. While the parameter is non-empty, `macroPlacement` keeps its load/save flow but skips DreamPlace, and `postFloorplan` commits the macros from the file. Orientations: `R0`, `R90`, `R180`, `R270`, `MX`, `MY`, `MX90`, `MY90`; setting the same instance again updates it in place. + +Both `ecc param` scopes apply: + +- project (default): stored in `ecc.toml` `[params.macro]`, rendered into the Tcl on the next fresh workspace (`ecc run` / `--overwrite`) or `ecc workspace refresh`; +- `--workspace NAME`: written to that workspace's `home/params.toml`; the Tcl is regenerated immediately and `macroPlacement` and its suffix are marked pending, so the next `ecc run --workspace NAME` resumes from `macroPlacement`. + +```console +$ ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 +[status] + param: macro.placements + instance: u_ram0 + x: 10.0 + y: 20.5 + orientation: R0 + placements: [{'instance': 'u_ram0', 'x': 10.0, 'y': 20.5, 'orientation': 'R0'}] + status: set + source: ecc.toml + +$ ecc macro set u_ram1 --x 150 --y 20.5 --orient MY +$ ecc macro show +[result] + param: macro.placements + placements: [{'instance': 'u_ram0', 'x': 10.0, 'y': 20.5, 'orientation': 'R0'}, {'instance': 'u_ram1', 'x': 150.0, 'y': 20.5, 'orientation': 'MY'}] + source: ecc.toml + +$ ecc macro remove u_ram1 +[status] + param: macro.placements + instance: u_ram1 + placements: [{'instance': 'u_ram0', 'x': 10.0, 'y': 20.5, 'orientation': 'R0'}] + status: removed + source: ecc.toml +``` + +Adjusting macro positions on an existing workspace and re-running the affected segment: + +```bash +ecc macro set u_ram0 --x 120.0 --y 80.0 --orient MY --workspace default +ecc run --workspace default # resumes from macroPlacement; downstream steps re-run +ecc macro remove u_ram0 --workspace default # removing the last entry restores DreamPlace auto placement; resume again for the automatic result +``` + +Instance names must exist in the design and every hard macro must be listed — `postFloorplan` fails with the missing instance names otherwise. `macro_location.tcl` is a generated file and hand-editing it is unsupported (re-running `macroPlacement` without the parameter regenerates it). See [floorplan-flow.en.md](floorplan-flow.en.md) for the handoff format and staged-floorplan details, and the [Configuration Reference §1.5](ecc-config-ref.en.md) for the parameter. + ## 10. pdk — PDK path configuration The PDK itself comes from the install script (`--with-toolchain`; see the @@ -1088,4 +1141,12 @@ ecc report qor --workspace exp1 ecc run --workspace pnr --from prefloorplan --to route ``` +A design with hard macros can switch to manual placement instead (full contract in §9.5): + +```bash +ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 --workspace default +ecc run --workspace default # resumes from macroPlacement: DreamPlace is skipped, the manual positions are committed +ecc macro show --workspace default +``` + Once `project.json` exists, project-scoped inspection, signoff, and report commands select among declared workspaces; a single active workspace is auto-selected, while multiple active workspaces require an explicit `--workspace NAME` (otherwise `workspace_required` is reported, listing the available names). A workspace no longer in use can be dropped from auto-selection by changing its `status` to `archived` in `project.json`. diff --git a/chipcompiler/docs/floorplan-flow.cn.md b/chipcompiler/docs/floorplan-flow.cn.md index 9186e783..e61ea8fd 100644 --- a/chipcompiler/docs/floorplan-flow.cn.md +++ b/chipcompiler/docs/floorplan-flow.cn.md @@ -26,8 +26,8 @@ flowchart LR ## 宏位置交接文件 -宏单元摆放成功后,流程会写入 `config/macro_localtion.tcl`。文件名保留现有的 -`localtion` 拼写,以保证兼容性。 +宏单元摆放成功后,流程会写入 `config/macro_location.tcl`。改名之前创建的 +workspace 若仍带有 `macro_localtion.tcl`,打开时会自动迁移。 该文件是 macro placement 和 post-floorplan 之间的 iDB/Tcl 交接文件。每个硬宏 使用如下命令表示,坐标单位为微米: @@ -43,9 +43,19 @@ setInstancePlacementStatus -status fixed -name 交接文件使用。生成的方向值为 `R0`、`R90`、`R180`、`R270`、`MY`、`MX90`、`MX` 和 `MY90`。 -如果 GUI 或用户提供宏位置,也必须使用同一种 Tcl 格式。重新运行 -`macroPlacement` 会重新生成该文件;使用有效的手工交接文件后,应只运行 -`postFloorplan`,不要再次运行宏摆放阶段。 +手工宏位置通过 `macro.placements` 参数和 `ecc macro` 命令管理: + +```bash +ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 +ecc macro remove u_ram0 +ecc macro show +``` + +只要 `macro.placements` 非空,创建或刷新 workspace 配置时都会把它渲染进 +`config/macro_location.tcl`;此时 `macroPlacement` 保留 load/save 流程但跳过 +DreamPlace 宏摆放,由 `postFloorplan` 依据该文件将宏以 fixed 状态提交。删除 +最后一个条目会清空参数并恢复 DreamPlace 自动摆放。不支持手工编辑该文件:未设置 +`macro.placements` 时,重新运行 `macroPlacement` 会重新生成交接文件。 ## 恢复与重跑 diff --git a/chipcompiler/docs/floorplan-flow.en.md b/chipcompiler/docs/floorplan-flow.en.md index faebfc07..1ac93900 100644 --- a/chipcompiler/docs/floorplan-flow.en.md +++ b/chipcompiler/docs/floorplan-flow.en.md @@ -29,8 +29,8 @@ workspace. ## Macro-location handoff After successful macro-only placement, the flow writes -`config/macro_localtion.tcl`. The filename intentionally retains the existing -`localtion` spelling for compatibility. +`config/macro_location.tcl`. Workspaces created before the rename that still +carry `macro_localtion.tcl` are migrated automatically when opened. The file is the iDB/Tcl handoff between macro placement and post-floorplan. Each hard macro is represented by these commands, with coordinates in microns: @@ -46,9 +46,22 @@ macros. Every listed instance must exist in the design; old four-column location text files are not valid handoffs. The generated orientation values are `R0`, `R90`, `R180`, `R270`, `MY`, `MX90`, `MX`, and `MY90`. -If a GUI or a user supplies macro locations, it must write this same Tcl -format. Re-running `macroPlacement` regenerates the handoff, so run only -`postFloorplan` after replacing it with a valid manual handoff. +Manual macro locations are managed through the `macro.placements` parameter +and the `ecc macro` commands: + +```bash +ecc macro set u_ram0 --x 10 --y 20.5 --orient R0 +ecc macro remove u_ram0 +ecc macro show +``` + +While `macro.placements` is non-empty, the placements are rendered into +`config/macro_location.tcl` whenever the workspace configuration is created or +refreshed, and `macroPlacement` keeps its load/save flow but skips DreamPlace +macro placement — `postFloorplan` commits the macros from the file as fixed. +Removing the last entry clears the parameter and restores the automatic +DreamPlace behavior. Hand-editing the file is not supported: without +`macro.placements` set, re-running `macroPlacement` regenerates the handoff. ## Resuming or re-running stages diff --git a/chipcompiler/tools/ecc/configs/macro_localtion.tcl b/chipcompiler/tools/ecc/configs/macro_location.tcl similarity index 100% rename from chipcompiler/tools/ecc/configs/macro_localtion.tcl rename to chipcompiler/tools/ecc/configs/macro_location.tcl diff --git a/chipcompiler/tools/ecc/module.py b/chipcompiler/tools/ecc/module.py index 78ddf590..9be4923c 100644 --- a/chipcompiler/tools/ecc/module.py +++ b/chipcompiler/tools/ecc/module.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -import json import os import shutil from pathlib import Path @@ -569,18 +568,6 @@ def feature_route_read(self, json_path: str): def feature_route(self, json_path: str): self.ecc.feature_route(path=path_text(json_path)) - def is_rt_timing_enable(self, config: str): - if os.path.exists(config): - with open(config, encoding="utf-8") as f_reader: - json_data = json.load(f_reader) - # check if time enable - if ( - json_data is not None - and json_data.get("RT", {}).get("-enable_timing", "0") == "1" - ): - return True - return False - ######################################################################## # RCX api ######################################################################## diff --git a/chipcompiler/tools/ecc/runner.py b/chipcompiler/tools/ecc/runner.py index e5714012..188fcc92 100644 --- a/chipcompiler/tools/ecc/runner.py +++ b/chipcompiler/tools/ecc/runner.py @@ -391,7 +391,6 @@ def save_data( ecc_module: ECCToolsModule, *, feature_step: bool = True, - report_timing: bool = False, ) -> bool: """ module is ecc module from db engine, @@ -424,17 +423,6 @@ def save_data( ecc_module.report_summary(path=step.report.db or "") - if report_timing: - ecc_module.release_sta() - ecc_module.init_sta( - output_dir=(step.data.steps or {}).get("sta", ""), - top_module=workspace.design.top_module, - lib_paths=workspace.pdk.libs, - sdc_path=workspace.pdk.sdc, - ) - ecc_module.report_timing() - ecc_module.release_sta() - # update parameters db_json = json_read(step.feature.db or "") if len(db_json) > 0: @@ -573,24 +561,14 @@ def run_routing( if ecc_module is not None: sub_flow.update_step(step_name=EccSubFlowEnum.load_data.value, state=StateEnum.Success) - if ecc_module.is_rt_timing_enable( - config=workspace.config.get(f"{StepEnum.ROUTING.value}", "") - ): - ecc_module.release_sta() - ecc_module.init_sta( - output_dir=(step.data.steps or {}).get(StepEnum.ROUTING.value, ""), - top_module=workspace.design.top_module, - lib_paths=workspace.pdk.libs, - sdc_path=workspace.pdk.sdc, - ) - + # Timing-driven routing is self-contained in iRT: RTInterface builds its + # own timing engine from the shared db config (lib paths, SDC), so no + # Python-side STA lifecycle is needed before run_routing. ecc_module.run_routing(config=workspace.config.get(f"{StepEnum.ROUTING.value}", "")) sub_flow.update_step(step_name=EccSubFlowEnum.run_routing.value, state=StateEnum.Success) - reslut = save_data( - workspace=workspace, step=step, ecc_module=ecc_module, report_timing=False - ) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -623,7 +601,6 @@ def run_drc(workspace: Workspace, step: EccStep, ecc_module: ECCToolsModule | No step=step, ecc_module=ecc_module, feature_step=False, - report_timing=False, ) if not reslut: return False @@ -670,7 +647,6 @@ def run_lvs(workspace: Workspace, step: EccStep, ecc_module: ECCToolsModule | No step=step, ecc_module=ecc_module, feature_step=False, - report_timing=False, ) if not reslut: return False @@ -703,9 +679,7 @@ def run_filler( sub_flow.update_step(step_name=EccSubFlowEnum.run_filler.value, state=StateEnum.Success) - reslut = save_data( - workspace=workspace, step=step, ecc_module=ecc_module, report_timing=False - ) + reslut = save_data(workspace=workspace, step=step, ecc_module=ecc_module) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -746,7 +720,6 @@ def run_pre_floorplan( step=step, ecc_module=ecc_module, feature_step=False, - report_timing=False, ) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -788,7 +761,6 @@ def run_post_floorplan( step=step, ecc_module=ecc_module, feature_step=False, - report_timing=False, ) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) @@ -893,7 +865,6 @@ def run_rcx(workspace: Workspace, step: EccStep, ecc_module: ECCToolsModule | No step=step, ecc_module=ecc_module, feature_step=False, - report_timing=False, ): workspace.logger.error("Failed to save RCX data") return False @@ -1023,7 +994,6 @@ def run_sta(workspace: Workspace, step: EccStep, ecc_module: ECCToolsModule | No step=step, ecc_module=ecc_module, feature_step=False, - report_timing=False, ) sub_flow.update_step(step_name=EccSubFlowEnum.save_data.value, state=StateEnum.Success) diff --git a/chipcompiler/tools/ecc_dreamplace/runner.py b/chipcompiler/tools/ecc_dreamplace/runner.py index 05037017..8ede17e8 100644 --- a/chipcompiler/tools/ecc_dreamplace/runner.py +++ b/chipcompiler/tools/ecc_dreamplace/runner.py @@ -4,6 +4,7 @@ from pathlib import Path from chipcompiler.data import EccStep, StateEnum, StepEnum, StepInput, Workspace +from chipcompiler.data.workspace.macro_location import macro_placements from chipcompiler.tools.ecc import EccSubFlow, EccSubFlowEnum, ECCToolsModule from chipcompiler.tools.ecc import runner as ecc_runner @@ -47,6 +48,9 @@ def run_macro_placement( workspace: Workspace, step: EccStep, ecc_module: ECCToolsModule | None = None ) -> bool: """Run macro-only placement between the two floorplan phases.""" + import logging + + logger = logging.getLogger(__name__) reslut = False sub_flow = EccSubFlow(workspace=workspace, workspace_step=step) @@ -55,30 +59,43 @@ def run_macro_placement( if ecc_module is not None: sub_flow.update_step(step_name=EccSubFlowEnum.load_data.value, state=StateEnum.Success) - dreamplace_module = DreamplaceModule( - workspace=workspace, - step=step, - ecc_module=ecc_module, - input_def=step.input.def_, - input_verilog=step.input.verilog, - output_def=step.output.def_, - output_verilog=step.output.verilog, - ) - reslut = dreamplace_module.run_macro_placement() - if not reslut: + manual_placements = macro_placements(workspace) + if manual_placements: + logger.info( + "macro.placements set (%d entries); skipping DreamPlace, using %s", + len(manual_placements), + workspace.config.get("macro_location", ""), + ) sub_flow.update_step( - step_name=EccSubFlowEnum.macro_place.value, state=StateEnum.Imcomplete + step_name=EccSubFlowEnum.macro_place.value, state=StateEnum.Success ) - return False + else: + dreamplace_module = DreamplaceModule( + workspace=workspace, + step=step, + ecc_module=ecc_module, + input_def=step.input.def_, + input_verilog=step.input.verilog, + output_def=step.output.def_, + output_verilog=step.output.verilog, + ) + reslut = dreamplace_module.run_macro_placement() + if not reslut: + sub_flow.update_step( + step_name=EccSubFlowEnum.macro_place.value, state=StateEnum.Imcomplete + ) + return False + + reslut = ecc_module.tcl_save(workspace.config.get("macro_location", "")) + if not reslut: + sub_flow.update_step( + step_name=EccSubFlowEnum.macro_place.value, state=StateEnum.Imcomplete + ) + return False - reslut = ecc_module.tcl_save(workspace.config.get("macro_location", "")) - if not reslut: sub_flow.update_step( - step_name=EccSubFlowEnum.macro_place.value, state=StateEnum.Imcomplete + step_name=EccSubFlowEnum.macro_place.value, state=StateEnum.Success ) - return False - - sub_flow.update_step(step_name=EccSubFlowEnum.macro_place.value, state=StateEnum.Success) reslut = ecc_runner.save_data( workspace=workspace, step=step, ecc_module=ecc_module, feature_step=False ) diff --git a/chipcompiler/tools/ecc_sizer/runner.py b/chipcompiler/tools/ecc_sizer/runner.py index e495fc1a..71f45c29 100644 --- a/chipcompiler/tools/ecc_sizer/runner.py +++ b/chipcompiler/tools/ecc_sizer/runner.py @@ -1,6 +1,7 @@ import logging import os import shutil +import signal import subprocess from pathlib import Path @@ -20,6 +21,41 @@ def _has_staging_outputs(step: EccStep) -> bool: return os.path.exists(sizer_staging_def(step)) and os.path.exists(sizer_staging_verilog(step)) +# Native crash banners glibc/libstdc++ write to the inherited stderr before +# aborting (e.g. "*** buffer overflow detected ***: terminated"). A kill or +# timeout prints no such line, so the banner separates tool crashes from +# external termination. +_FATAL_LOG_MARKERS = ( + "*** ", + "terminate called after throwing", +) + + +def _termination_reason(returncode: int) -> str: + """Classify a native exit status as a fatal signal or a tool exit code.""" + if returncode >= 0: + return f"exit_code={returncode}" + signum = -returncode + try: + signame = signal.Signals(signum).name + except ValueError: + signame = "UNKNOWN" + return f"signal={signame}({signum})" + + +def _first_fatal_log_line(log_path: str) -> str: + """The first native fatal banner in the step log, or an empty string.""" + try: + with open(log_path, errors="replace") as log_file: + for line in log_file: + stripped = line.strip() + if stripped.startswith(_FATAL_LOG_MARKERS): + return stripped + except OSError: + pass + return "" + + def _published_paths(step: EccStep) -> list[Path]: output = step.output if not isinstance(output, EccOutput): @@ -127,10 +163,11 @@ def run_step( if result.returncode != 0 or not _has_staging_outputs(step): logger.error( - "Sizer failed for step %s: exit code=%d, staging present=%s", + "Sizer failed for step %s: %s, staging present=%s, fatal_log_line=%r", step.name, - result.returncode, + _termination_reason(result.returncode), _has_staging_outputs(step), + _first_fatal_log_line(log_path), ) sub_flow.update_step(step_name=run_sizer_step, state=StateEnum.Imcomplete) return StateEnum.Imcomplete diff --git a/test/cli/commands/test_effective_config.py b/test/cli/commands/test_effective_config.py index d035086c..876a6cd7 100644 --- a/test/cli/commands/test_effective_config.py +++ b/test/cli/commands/test_effective_config.py @@ -1,6 +1,8 @@ import json from pathlib import Path +import pytest + from chipcompiler.cli import main as cli_main @@ -634,6 +636,111 @@ def test_run_set_overrides_invalid_manifest_value( parameters = flow_mocks.capture["create_kwargs"]["parameters"] assert parameters["frequency_max"] == 100.0 + @pytest.mark.parametrize( + ("param", "value", "expected"), + [ + ( + "place.random_seed", + "3001", + {"dreamplace": {"random_seed": 3001}}, + ), + ( + "route.RT.-enable_timing", + "1", + {"route": {"RT": {"-enable_timing": "1"}}}, + ), + ], + ) + def test_run_set_direct_config_parameter_in_manifest_project( + self, tmp_path, capsys, monkeypatch, flow_mocks, manifest_stubs, param, value, expected + ): + project_dir = _write_manifest_project( + manifest_stubs, + tmp_path, + monkeypatch, + 100, + ecc_toml=self._hybrid_toml(project_dir=tmp_path / "proj", frequency=False), + ) + + rc = cli_main.run( + [ + "run", + "--project", + str(project_dir), + "--from", + "Synth", + "--to", + "Synth", + "--set", + f"{param}={value}", + ] + ) + + assert rc == 0 + assert flow_mocks.capture["create_kwargs"]["parameters"]["config_overrides"] == expected + + def test_run_set_direct_config_parameter_warns_on_different_toml_value( + self, tmp_path, capsys, monkeypatch, flow_mocks, manifest_stubs + ): + project_dir = _write_manifest_project( + manifest_stubs, + tmp_path, + monkeypatch, + 100, + ecc_toml=self._hybrid_toml(project_dir=tmp_path / "proj", frequency=False) + + "\n[params.place]\nrandom_seed = 3000\n", + ) + + rc = cli_main.run( + [ + "run", + "--project", + str(project_dir), + "--from", + "Synth", + "--to", + "Synth", + "--set", + "place.random_seed=3001", + "--plain", + ] + ) + + assert rc == 0 + warnings = [ + record + for record in manifest_stubs.records() + if record.get("warning") == "config_layer_diverged" + ] + assert len(warnings) == 1 + assert "place.random_seed" in warnings[0]["keys"] + + @pytest.mark.parametrize( + ("section", "key", "value"), + [ + ("[params.place]", "random_seed", "3001"), + ("[params.route.RT]", '"-enable_timing"', '"1"'), + ], + ) + def test_check_direct_config_param_override_in_manifest_project( + self, tmp_path, capsys, monkeypatch, manifest_stubs, section, key, value + ): + project_dir = tmp_path / "proj" + toml = self._hybrid_toml(project_dir, frequency=False) + toml += f"\n{section}\n{key} = {value}\n" + _write_manifest_project( + manifest_stubs, + tmp_path, + monkeypatch, + 100, + ecc_toml=toml, + ) + + rc = cli_main.run(["check", "--project", str(project_dir), "--plain"]) + + assert rc == 0 + assert manifest_stubs.records()[0]["status"] == "checked" + class TestManifestBoolTypeSafety: """project.json run_analysis is the first bool registry parameter: its diff --git a/test/cli/params/test_commands.py b/test/cli/params/test_commands.py index abeeac71..74f6dba1 100644 --- a/test/cli/params/test_commands.py +++ b/test/cli/params/test_commands.py @@ -418,7 +418,7 @@ def test_plain_is_line_oriented(self, tmp_path, capsys, create_cli_project): assert rc == 0 out = capsys.readouterr().out lines = [line for line in out.strip().split("\n") if line.strip()] - assert len(lines) == 14 + assert len(lines) == 15 class TestConfigResolved: @@ -440,7 +440,7 @@ def test_config_resolved_includes_param_records( assert rc == 0 records = plain_records(capsys.readouterr().out) param_records = [r for r in records if r.get("kind") == "param"] - assert len(param_records) == 14 + assert len(param_records) == 15 first_param = param_records[0] assert "source" in first_param assert "maps_to" in first_param @@ -509,7 +509,7 @@ def test_param_list_plain_is_one_line_per_record(self, tmp_path, capsys, create_ assert rc == 0 out = capsys.readouterr().out lines = [line for line in out.strip().split("\n") if line.strip()] - assert len(lines) == 14 + assert len(lines) == 15 assert "\033[" not in out def test_param_show_default_is_pretty(self, tmp_path, capsys, create_cli_project): diff --git a/test/cli/params/test_macro_commands.py b/test/cli/params/test_macro_commands.py new file mode 100644 index 00000000..b27d2d83 --- /dev/null +++ b/test/cli/params/test_macro_commands.py @@ -0,0 +1,301 @@ +import json +import tomllib +from pathlib import Path +from types import SimpleNamespace + +from chipcompiler.cli import main as cli_main +from chipcompiler.data.parameter import Parameters + + +class _Flow: + def __init__(self, workspace): + self.workspace = workspace + + def save(self): + return True + + +def _workspace(workspace_dir): + parameters_path = workspace_dir / "home" / "params.toml" + parameters_path.parent.mkdir(parents=True) + return SimpleNamespace( + directory=workspace_dir, + parameters=Parameters( + path=parameters_path, + data={ + "pdk": "ics55", + "design": "gcd", + "top_module": "gcd", + "clock": "clk", + }, + ), + config={}, + flow=SimpleNamespace( + data={ + "steps": [ + {"name": "Synthesis", "state": "Success", "runtime": "", "peak memory (mb)": 0}, + { + "name": "macroPlacement", + "state": "Success", + "runtime": "", + "peak memory (mb)": 0, + }, + {"name": "place", "state": "Success", "runtime": "", "peak memory (mb)": 0}, + ] + } + ), + ) + + +def _write_manifest(project_dir: str) -> None: + project_path = Path(project_dir) + (project_path / "project.json").write_text( + json.dumps( + { + "schema_version": 1, + "design_name": "gcd", + "root_path": project_dir, + "base_design": { + "pdk": "ics55", + "pdk_root": str(project_path / "ics55"), + "top_module": "gcd", + "clock": "clk", + "rtl_list": ["rtl/gcd.v"], + "parameters": {"design": "gcd", "frequency_max": 100}, + }, + "workspaces": [ + { + "workspace_id": "baseline", + "workspace_path": str(project_path / "baseline"), + "status": "success", + } + ], + } + ) + ) + + +def _read_macro_placements(project_dir: str) -> list: + config_path = Path(project_dir) / "ecc.toml" + with config_path.open("rb") as f: + return tomllib.load(f)["params"]["macro"]["placements"] + + +def test_macro_set_project_writes_params_table(capsys, create_cli_project, plain_records): + project_dir = create_cli_project() + + rc = cli_main.run( + [ + "macro", + "set", + "u_ram0", + "--x", + "10", + "--y", + "20.5", + "--orient", + "R0", + "--project", + project_dir, + "--plain", + ] + ) + + assert rc == 0 + record = plain_records(capsys.readouterr().out)[0] + assert record["status"] == "set" + assert record["source"] == "ecc.toml" + assert "'u_ram0'" in record["placements"] + assert _read_macro_placements(project_dir) == [ + {"instance": "u_ram0", "x": 10.0, "y": 20.5, "orientation": "R0"} + ] + + +def test_macro_set_upserts_by_instance(capsys, create_cli_project): + project_dir = create_cli_project() + for args in ( + ["u_ram0", "--x", "10", "--y", "20", "--orient", "R0"], + ["u_ram0", "--x", "30", "--y", "40", "--orient", "MY"], + ["u_ram1", "--x", "1", "--y", "2", "--orient", "MX"], + ): + assert cli_main.run(["macro", "set", *args, "--project", project_dir, "--plain"]) == 0 + + assert _read_macro_placements(project_dir) == [ + {"instance": "u_ram0", "x": 30.0, "y": 40.0, "orientation": "MY"}, + {"instance": "u_ram1", "x": 1.0, "y": 2.0, "orientation": "MX"}, + ] + + +def test_macro_remove_project_filters_and_deletes_empty(capsys, create_cli_project): + project_dir = create_cli_project() + cli_main.run( + [ + "macro", + "set", + "u_ram0", + "--x", + "1", + "--y", + "2", + "--orient", + "R0", + "--project", + project_dir, + "--plain", + ] + ) + cli_main.run( + [ + "macro", + "set", + "u_ram1", + "--x", + "3", + "--y", + "4", + "--orient", + "R0", + "--project", + project_dir, + "--plain", + ] + ) + + assert cli_main.run(["macro", "remove", "u_ram0", "--project", project_dir, "--plain"]) == 0 + assert _read_macro_placements(project_dir) == [ + {"instance": "u_ram1", "x": 3.0, "y": 4.0, "orientation": "R0"} + ] + + assert cli_main.run(["macro", "remove", "u_ram1", "--project", project_dir, "--plain"]) == 0 + with (Path(project_dir) / "ecc.toml").open("rb") as f: + document = tomllib.load(f) + assert "macro" not in document.get("params", {}) + + +def test_macro_set_rejects_bad_orientation(capsys, create_cli_project, plain_records): + project_dir = create_cli_project() + before = (Path(project_dir) / "ecc.toml").read_bytes() + + rc = cli_main.run( + [ + "macro", + "set", + "u_ram0", + "--x", + "1", + "--y", + "2", + "--orient", + "r0", + "--project", + project_dir, + "--plain", + ] + ) + + assert rc == 1 + record = plain_records(capsys.readouterr().out)[0] + assert record["kind"] == "error" + assert record["error"] == "invalid_value" + assert (Path(project_dir) / "ecc.toml").read_bytes() == before + + +def test_macro_set_workspace_persists_and_invalidates_suffix( + capsys, create_cli_project, monkeypatch, plain_records +): + project_dir = create_cli_project() + workspace_dir = Path(project_dir) / "baseline" + _write_manifest(project_dir) + workspace = _workspace(workspace_dir) + monkeypatch.setattr("chipcompiler.data.load_workspace", lambda _path: workspace) + monkeypatch.setattr("chipcompiler.data.refresh_workspace_config", lambda _workspace: None) + monkeypatch.setattr("chipcompiler.engine.EngineFlow", _Flow) + + rc = cli_main.run( + [ + "macro", + "set", + "u_ram0", + "--x", + "10", + "--y", + "20", + "--orient", + "R0", + "--workspace", + "baseline", + "--project", + project_dir, + "--plain", + ] + ) + + assert rc == 0 + record = plain_records(capsys.readouterr().out)[0] + assert record["status"] == "set" + assert record["instance"] == "u_ram0" + assert record["from_step"] == "macroPlacement" + assert "'u_ram0'" in record["placements"] + assert workspace.parameters.data["macro"]["placements"] == [ + {"instance": "u_ram0", "x": 10.0, "y": 20.0, "orientation": "R0"} + ] + assert [step["state"] for step in workspace.flow.data["steps"]] == [ + "Success", + "Unstart", + "Unstart", + ] + assert "workspace_param_overrides" in workspace.parameters.path.read_text() + + +def test_macro_remove_workspace_absent_instance_is_noop( + capsys, create_cli_project, monkeypatch, plain_records +): + project_dir = create_cli_project() + workspace_dir = Path(project_dir) / "baseline" + _write_manifest(project_dir) + workspace = _workspace(workspace_dir) + monkeypatch.setattr("chipcompiler.data.load_workspace", lambda _path: workspace) + monkeypatch.setattr("chipcompiler.data.refresh_workspace_config", lambda _workspace: None) + monkeypatch.setattr("chipcompiler.engine.EngineFlow", _Flow) + workspace.parameters.path.write_text("# baseline params\n") + before = workspace.parameters.path.read_bytes() + + rc = cli_main.run( + [ + "macro", + "remove", + "u_missing", + "--workspace", + "baseline", + "--project", + project_dir, + "--plain", + ] + ) + + assert rc == 0 + record = plain_records(capsys.readouterr().out)[0] + assert record["status"] == "absent" + assert record["placements"] == "[]" + assert workspace.parameters.path.read_bytes() == before + + +def test_macro_show_lists_entries_and_file(capsys, create_cli_project, monkeypatch, plain_records): + project_dir = create_cli_project() + workspace_dir = Path(project_dir) / "baseline" + _write_manifest(project_dir) + workspace = _workspace(workspace_dir) + workspace.parameters.data["macro"] = { + "placements": [{"instance": "u_ram0", "x": 10.0, "y": 20.0, "orientation": "R0"}] + } + monkeypatch.setattr("chipcompiler.data.load_workspace", lambda _path: workspace) + monkeypatch.setattr("chipcompiler.engine.EngineFlow", _Flow) + + rc = cli_main.run( + ["macro", "show", "--workspace", "baseline", "--project", project_dir, "--plain"] + ) + + assert rc == 0 + record = plain_records(capsys.readouterr().out)[0] + assert "'u_ram0'" in record["placements"] + assert record["file"] == str(workspace_dir / "config" / "macro_location.tcl") diff --git a/test/cli/params/test_registry.py b/test/cli/params/test_registry.py index 16ad9c7d..5d78fc5a 100644 --- a/test/cli/params/test_registry.py +++ b/test/cli/params/test_registry.py @@ -1,11 +1,13 @@ import pytest +from chipcompiler.cli.project.effective_config import _diverging_lower_keys from chipcompiler.cli.project.params import ( PARAM_REGISTRY, ParamSchema, ResolvedParam, build_backend_overrides, build_config_overrides, + build_pdk_overrides, coerce_manifest_parameters, is_known_key, list_groups, @@ -399,6 +401,42 @@ def test_mapping_does_not_mutate_schema_defaults(self): build_backend_overrides([rp]) assert schema.default == original_default + @pytest.mark.parametrize("schema", PARAM_REGISTRY, ids=lambda schema: schema.param) + def test_every_schema_default_survives_all_mapping_paths(self, schema): + """Every registered parameter must be safe through resolution and projections.""" + resolved, errors = resolve_parameters(cli_overrides={schema.param: schema.default}) + assert errors == [] + rp = next(item for item in resolved if item.param == schema.param) + + backend = build_backend_overrides([rp]) + config = build_config_overrides([rp]) + pdk = build_pdk_overrides([rp]) + assert isinstance(backend, dict) + assert isinstance(config, dict) + assert isinstance(pdk, dict) + if schema.config_target is not None: + assert config + assert backend == {} + elif schema.pdk_target is not None: + assert pdk == {schema.pdk_target: schema.default} + assert backend == {} + else: + assert backend + + @pytest.mark.parametrize("schema", PARAM_REGISTRY, ids=lambda schema: schema.param) + def test_every_schema_is_safe_in_divergence_projection(self, schema): + diverging, compared = _diverging_lower_keys( + {schema.param: schema.default}, lambda _schema: (None, False) + ) + + assert diverging == [] + if schema.maps_to is None: + assert compared == set() + elif isinstance(schema.maps_to, str): + assert compared == {schema.maps_to} + else: + assert compared == {f"{parent}.{child}" for parent, child in schema.maps_to.items()} + class TestCliOverrides: def test_repeatable_set(self): diff --git a/test/cli/test_cli_module_layout.py b/test/cli/test_cli_module_layout.py index 50b08154..39db6994 100644 --- a/test/cli/test_cli_module_layout.py +++ b/test/cli/test_cli_module_layout.py @@ -51,7 +51,7 @@ def test_core_modules_live_under_core_package(): def test_command_registration_modules_live_under_commands_package(): - for module_name in ("project", "doctor", "param", "pdk", "signoff", "report", "rpc"): + for module_name in ("project", "doctor", "param", "macro", "pdk", "signoff", "report", "rpc"): module = importlib.import_module(f"chipcompiler.cli.commands.{module_name}") assert module.__name__ == f"chipcompiler.cli.commands.{module_name}" diff --git a/test/cli/test_typer_cli.py b/test/cli/test_typer_cli.py index 54efc15e..914cfd22 100644 --- a/test/cli/test_typer_cli.py +++ b/test/cli/test_typer_cli.py @@ -24,6 +24,7 @@ def test_root_help_returns_zero_and_lists_commands(capsys): "config", "doctor", "param", + "macro", "pdk", "project", "workspace", diff --git a/test/data/test_workspace.py b/test/data/test_workspace.py index d394ce00..3635c121 100644 --- a/test/data/test_workspace.py +++ b/test/data/test_workspace.py @@ -27,7 +27,7 @@ StepEnum.CTS.value: "cts_ecc.json", StepEnum.DRC.value: "drc_ecc.json", StepEnum.FLOORPLAN.value: "floorplan_ecc.json", - "macro_location": "macro_localtion.tcl", + "macro_location": "macro_location.tcl", StepEnum.ROUTING.value: "route_ecc.json", StepEnum.FILLER.value: "filler_ecc.json", StepEnum.RCX.value: "rcx_ecc.json", diff --git a/test/data/test_workspace_macro_location.py b/test/data/test_workspace_macro_location.py new file mode 100644 index 00000000..b9610d0d --- /dev/null +++ b/test/data/test_workspace_macro_location.py @@ -0,0 +1,124 @@ +from copy import deepcopy +from pathlib import Path + +import pytest + +from chipcompiler.data import create_workspace, load_workspace, refresh_workspace_config +from chipcompiler.data.parameter import Parameters, load_parameter, save_parameter +from chipcompiler.data.workspace.macro_location import ( + MACRO_LOCATION_MARKER, + render_macro_location_tcl, +) + +PLACEMENTS = [ + {"instance": "u_ram0", "x": 10.0, "y": 20.5, "orientation": "R0"}, + {"instance": "u_ram1", "x": 130, "y": 40.0, "orientation": "MY"}, +] + + +def _create_workspace(tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters, extra): + pdk_root = minimal_ics55_pdk_factory(tmp_path / "ics55") + rtl_path = tmp_path / "gcd.v" + rtl_path.write_text("module gcd(input clk, output y); assign y = clk; endmodule\n") + + workspace_dir = tmp_path / "workspace" + create_workspace( + directory=workspace_dir, + origin_def="", + origin_verilog=rtl_path, + pdk="ics55", + parameters={**deepcopy(default_ics55_parameters), **extra}, + pdk_root=pdk_root, + ) + return workspace_dir + + +def test_render_macro_location_tcl(): + assert render_macro_location_tcl(PLACEMENTS) == ( + f"{MACRO_LOCATION_MARKER}\n" + "\n" + "placeInstance u_ram0 10 20.5 R0\n" + "setInstancePlacementStatus -status fixed -name u_ram0\n" + "placeInstance u_ram1 130 40 MY\n" + "setInstancePlacementStatus -status fixed -name u_ram1\n" + ) + + +def test_create_workspace_materializes_macro_location_tcl( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + workspace_dir = _create_workspace( + tmp_path, + minimal_ics55_pdk_factory, + default_ics55_parameters, + {"macro": {"placements": deepcopy(PLACEMENTS)}}, + ) + + tcl = workspace_dir / "config" / "macro_location.tcl" + assert tcl.read_text() == render_macro_location_tcl(PLACEMENTS) + + +def test_refresh_leaves_macro_location_untouched_without_placements( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + workspace_dir = _create_workspace( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters, {} + ) + tcl = workspace_dir / "config" / "macro_location.tcl" + handwritten = ( + "# Hard-macro placement commands generated by saveMacroTCL().\nplaceInstance u0 1 2 R0\n" + ) + tcl.write_text(handwritten) + + workspace = load_workspace(workspace_dir) + refresh_workspace_config(workspace) + + assert tcl.read_text() == handwritten + + +def test_refresh_rejects_invalid_placement_and_preserves_file( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + workspace_dir = _create_workspace( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters, {} + ) + workspace = load_workspace(workspace_dir) + tcl = Path(workspace.config["macro_location"]) + seeded = tcl.read_text() + + data = dict(workspace.parameters.data) + data["macro"] = {"placements": [{"instance": "u0", "x": 0, "y": 0, "orientation": "r0"}]} + workspace.parameters.data = data + assert save_parameter(workspace.parameters) + + with pytest.raises(ValueError, match="orientation"): + refresh_workspace_config(workspace) + + assert tcl.read_text() == seeded + + +def test_legacy_macro_localtion_filename_migrates_on_load( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters +): + workspace_dir = _create_workspace( + tmp_path, minimal_ics55_pdk_factory, default_ics55_parameters, {} + ) + legacy = workspace_dir / "config" / "macro_localtion.tcl" + canonical = workspace_dir / "config" / "macro_location.tcl" + legacy_content = "# legacy handoff\nplaceInstance u0 1 2 R0\n" + canonical.unlink() + legacy.write_text(legacy_content) + + workspace = load_workspace(workspace_dir) + + assert not legacy.exists() + assert workspace.config["macro_location"] == canonical + assert canonical.read_text() == legacy_content + + +def test_macro_placements_round_trip_through_params_toml(tmp_path): + parameters_path = tmp_path / "params.toml" + data = {"design": "gcd", "macro": {"placements": deepcopy(PLACEMENTS)}} + assert save_parameter(Parameters(path=parameters_path, data=deepcopy(data))) + + assert load_parameter(parameters_path).data == data diff --git a/test/tools/ecc/test_runner.py b/test/tools/ecc/test_runner.py index 0518e01e..3477516a 100644 --- a/test/tools/ecc/test_runner.py +++ b/test/tools/ecc/test_runner.py @@ -159,7 +159,7 @@ def destroy_fp(self): ) ) simple_floorplan_config = floorplan_config.with_stem("floorplan_ecc_simple") - macro_location = tmp_path / "macro_localtion.tcl" + macro_location = tmp_path / "macro_location.tcl" macro_location.write_text("# macro locations\n") workspace = Workspace( config={ diff --git a/test/tools/ecc_dreamplace/test_runner.py b/test/tools/ecc_dreamplace/test_runner.py index 8552ad16..f2d11da8 100644 --- a/test/tools/ecc_dreamplace/test_runner.py +++ b/test/tools/ecc_dreamplace/test_runner.py @@ -1,4 +1,5 @@ from chipcompiler.data import EccStep, StateEnum, StepEnum, Workspace +from chipcompiler.data.parameter import Parameters from chipcompiler.tools.ecc_dreamplace import runner as dreamplace_runner @@ -28,7 +29,7 @@ def tcl_save(self, output_path): calls.append(("tcl_save", output_path)) return True - macro_location = tmp_path / "macro_localtion.tcl" + macro_location = tmp_path / "macro_location.tcl" module = FakeEccModule() step = EccStep(name=StepEnum.MACRO_PLACEMENT.value) @@ -82,7 +83,7 @@ def tcl_save(self, output_path): calls.append(("tcl_save", output_path)) return False - macro_location = tmp_path / "macro_localtion.tcl" + macro_location = tmp_path / "macro_location.tcl" step = EccStep(name=StepEnum.MACRO_PLACEMENT.value) monkeypatch.setattr(dreamplace_runner, "EccSubFlow", lambda **_kwargs: subflow) @@ -109,6 +110,64 @@ def tcl_save(self, output_path): assert subflow.updates[-1]["state"] is StateEnum.Imcomplete +def test_macro_placement_skips_dreamplace_when_manual_placements_set(monkeypatch, tmp_path): + calls = [] + subflow = FakeSubFlow() + + class FakeDreamplaceModule: + def __init__(self, **_kwargs): + calls.append("init") + + def run_macro_placement(self): + calls.append("run") + return True + + class FakeEccModule: + def tcl_save(self, output_path): + calls.append(("tcl_save", output_path)) + return True + + macro_location = tmp_path / "macro_location.tcl" + module = FakeEccModule() + step = EccStep(name=StepEnum.MACRO_PLACEMENT.value) + workspace = Workspace( + config={"macro_location": macro_location}, + parameters=Parameters( + data={ + "macro": { + "placements": [{"instance": "u0", "x": 1.0, "y": 2.0, "orientation": "R0"}] + } + } + ), + ) + + monkeypatch.setattr(dreamplace_runner, "EccSubFlow", lambda **_kwargs: subflow) + monkeypatch.setattr( + dreamplace_runner.ecc_runner, + "get_eda_instance", + lambda **_kwargs: module, + ) + monkeypatch.setattr( + dreamplace_runner.ecc_runner, + "save_data", + lambda **kwargs: calls.append(("save", kwargs["step"].name)) or True, + ) + monkeypatch.setattr(dreamplace_runner, "DreamplaceModule", FakeDreamplaceModule) + + assert dreamplace_runner.run_macro_placement(workspace, step) is True + assert calls == [("save", StepEnum.MACRO_PLACEMENT.value)] + assert [update["step_name"] for update in subflow.updates] == [ + "load data", + "macro placement", + "save data", + ] + assert [update["state"] for update in subflow.updates] == [ + StateEnum.Success, + StateEnum.Success, + StateEnum.Success, + ] + + def test_run_step_dispatches_macro_placement(monkeypatch): monkeypatch.setattr(dreamplace_runner, "is_eda_exist", lambda: True) monkeypatch.setattr(dreamplace_runner, "run_macro_placement", lambda **_kwargs: True) diff --git a/test/tools/ecc_sizer/test_runner.py b/test/tools/ecc_sizer/test_runner.py index 4981bb79..99263b49 100644 --- a/test/tools/ecc_sizer/test_runner.py +++ b/test/tools/ecc_sizer/test_runner.py @@ -1,3 +1,4 @@ +import logging import os import subprocess from pathlib import Path @@ -166,6 +167,88 @@ def test_sizer_runner_marks_subflow_incomplete_when_outputs_are_missing( assert _subflow_states(step)["run sizer"] == StateEnum.Imcomplete.value +def test_sizer_runner_marks_subflow_incomplete_when_tool_is_signal_terminated( + tmp_path, + monkeypatch, + caplog, +): + from chipcompiler.tools.ecc_sizer import builder as sizer_builder + from chipcompiler.tools.ecc_sizer import runner as sizer_runner + + workspace = _workspace(tmp_path) + step = sizer_builder.build_step( + workspace=workspace, + step_name=StepEnum.TIMING_OPT.value, + input_def=Path("input.def"), + input_verilog=Path("input.v"), + ) + sizer_builder.build_step_space(step) + sizer_builder.build_step_config(workspace, step) + Path(step.log.file).write_text( + "Read 527000 / 527400 Instances\n" + "*** buffer overflow detected ***: terminated\n" + "trailing line\n", + encoding="utf-8", + ) + + monkeypatch.setattr(sizer_runner, "get_sizer_command", lambda: ["/fake/sizer"]) + monkeypatch.setattr(sizer_runner, "is_eda_exist", lambda: True) + monkeypatch.setattr(sizer_runner, "is_sizer_runtime_exist", lambda: True) + monkeypatch.setattr(sizer_runner, "is_dreamplace_exist", lambda: True) + monkeypatch.setattr( + subprocess, + "run", + lambda command, cwd, stdout, stderr, check: SimpleNamespace(returncode=-6), + ) + + with caplog.at_level(logging.ERROR, logger=sizer_runner.__name__): + assert sizer_runner.run_step(workspace, step) == StateEnum.Imcomplete + + assert _subflow_states(step)["run sizer"] == StateEnum.Imcomplete.value + assert not sizer_builder.sizer_staging_def(step).exists() + assert not sizer_builder.sizer_staging_verilog(step).exists() + failure = caplog.records[-1].getMessage() + assert "signal=SIGABRT(6)" in failure + assert "*** buffer overflow detected ***: terminated" in failure + + +def test_sizer_runner_reports_plain_exit_code_without_signal_or_fatal_line( + tmp_path, + monkeypatch, + caplog, +): + from chipcompiler.tools.ecc_sizer import builder as sizer_builder + from chipcompiler.tools.ecc_sizer import runner as sizer_runner + + workspace = _workspace(tmp_path) + step = sizer_builder.build_step( + workspace=workspace, + step_name=StepEnum.TIMING_OPT.value, + input_def=Path("input.def"), + input_verilog=Path("input.v"), + ) + sizer_builder.build_step_space(step) + sizer_builder.build_step_config(workspace, step) + + monkeypatch.setattr(sizer_runner, "get_sizer_command", lambda: ["/fake/sizer"]) + monkeypatch.setattr(sizer_runner, "is_eda_exist", lambda: True) + monkeypatch.setattr(sizer_runner, "is_sizer_runtime_exist", lambda: True) + monkeypatch.setattr(sizer_runner, "is_dreamplace_exist", lambda: True) + monkeypatch.setattr( + subprocess, + "run", + lambda command, cwd, stdout, stderr, check: SimpleNamespace(returncode=1), + ) + + with caplog.at_level(logging.ERROR, logger=sizer_runner.__name__): + assert sizer_runner.run_step(workspace, step) == StateEnum.Imcomplete + + failure = caplog.records[-1].getMessage() + assert "exit_code=1" in failure + assert "signal=" not in failure + assert "fatal_log_line=''" in failure + + def test_sizer_runner_inherits_captured_stdio_instead_of_truncating_step_log( tmp_path, monkeypatch,