Skip to main content
Glama

place_and_route

Synthesize Verilog code and place-and-route FPGA designs to generate timing reports, resource utilization, and implementation logs for ice40, ecp5, nexus, and gowin targets.

Instructions

Synthesize Verilog with Yosys then place-and-route with nextpnr in one step. If backend=litex, runs LiteX build and ignores Verilog inputs. Returns max frequency, critical path, resource utilization, and full logs. Supported targets: ice40, ecp5, nexus, gowin. Common device/package values: ice40: device=hx1k|hx8k|up5k|lp1k package=tq144|qn84|sg48|cm81 ecp5: device=25k|45k|85k package=CABGA256|CABGA381 nexus: device=LIFCL-40-9BG400C (package embedded in device string) gowin: device=GW1N-UV4LQ144C6/I5 (package embedded in device string)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYesVerilog source code
top_moduleYesTop-level module name
targetYesFPGA family
deviceYesDevice variant, e.g. 'hx1k', '25k', 'LIFCL-40-9BG400C'
packageNoPackage, e.g. 'tq144', 'CABGA256' (not needed for nexus/gowin)
constraintsNoOptional pin constraints (PCF/LPF/PDC/CST text)
timeoutNoPnR timeout in seconds
backendNoPnR backendyosys
litex_boardNoLiteX board target (required if backend=litex)
litex_argsNoExtra LiteX CLI args (backend=litex)

Implementation Reference

  • The main handler function that synthesizes Verilog with Yosys then performs place-and-route with nextpnr. Supports ice40, ecp5, nexus, and gowin FPGA targets. Handles synthesis, constraint files, and returns timing/utilization results.
    def place_and_route(
        code: str,
        top_module: str,
        target: str,
        device: str,
        package: str = "",
        constraints: str = "",
        timeout: int = 300,
        backend: str = "yosys",
        litex_board: str | None = None,
        litex_args: list[str] | None = None,
    ) -> dict:
        """Synthesize Verilog with Yosys then place-and-route with nextpnr.
    
        Common device/package values:
          ice40:  device=hx1k|hx8k|up5k|lp1k  package=tq144|qn84|sg48|cm81
          ecp5:   device=25k|45k|85k           package=CABGA256|CABGA381|CABGA381
          nexus:  device=LIFCL-40-9BG400C      (package embedded in device string)
          gowin:  device=GW1N-UV4LQ144C6/I5   (package embedded in device string)
    
        constraints: optional PCF (ice40), LPF (ecp5), PDC (nexus), or CST (gowin) text.
        """
        if backend == "litex":
            if not litex_board:
                return {"success": False, "error": "litex_board is required for LiteX backend."}
            from tools.litex import litex_build
            result = litex_build(board=litex_board, args=litex_args or [], timeout=max(timeout, 300))
            result["backend"] = "litex"
            result["note"] = "LiteX backend ignores code/top_module/target/device and runs board build."
            return result
    
        if target not in NEXTPNR_BIN:
            supported = list(NEXTPNR_BIN.keys())
            return {"error": f"Unsupported PnR target '{target}'. Supported: {supported}"}
    
        synth_cmd = SYNTH_CMDS.get(target)
        if not synth_cmd:
            return {"error": f"No Yosys synth command for target '{target}'"}
        top_err = validate_top_module(top_module)
        if top_err:
            return {"success": False, "error": top_err}
    
        with tempfile.TemporaryDirectory() as tmpdir:
            src_file     = os.path.join(tmpdir, "design.v")
            ys_script    = os.path.join(tmpdir, "synth.ys")
            netlist_json = os.path.join(tmpdir, "netlist.json")
            out_file     = os.path.join(tmpdir, f"out{OUTPUT_EXT[target]}")
            cst_file     = os.path.join(tmpdir, f"constraints{CONSTRAINTS_EXT[target]}")
    
            with open(src_file, "w", encoding="utf-8") as f:
                f.write(code)
    
            # Forward slashes for Yosys on Windows
            src_yosys     = src_file.replace("\\", "/")
            netlist_yosys = netlist_json.replace("\\", "/")
    
            script = (
                f"read_verilog {src_yosys}\n"
                f"{synth_cmd} -top {top_module} -json {netlist_yosys}\n"
            )
            with open(ys_script, "w") as f:
                f.write(script)
    
            # ------------------------------------------------------------------
            # Stage 1: Synthesis
            # ------------------------------------------------------------------
            try:
                synth = subprocess.run(
                    ["yosys", "-s", ys_script],
                    capture_output=True, text=True, timeout=120,
                )
            except FileNotFoundError:
                return {"error": "'yosys' not found. Install OSS CAD Suite."}
            except subprocess.TimeoutExpired:
                return {"error": "Synthesis timed out after 120 s."}
    
            if synth.returncode != 0:
                return {
                    "success": False,
                    "stage": "synthesis",
                    "stdout": synth.stdout,
                    "stderr": synth.stderr,
                }
    
            if not os.path.exists(netlist_json):
                return {"success": False, "stage": "synthesis",
                        "error": "Yosys did not produce a netlist JSON."}
    
            if constraints:
                with open(cst_file, "w", encoding="utf-8") as f:
                    f.write(constraints)
    
            # ------------------------------------------------------------------
            # Stage 2: Place and route
            # ------------------------------------------------------------------
            cmd = _build_cmd(
                NEXTPNR_BIN[target], target, device, package,
                netlist_json, out_file, cst_file if constraints else None,
            )
    
            try:
                pnr = subprocess.run(
                    cmd, capture_output=True, text=True, timeout=timeout,
                )
            except FileNotFoundError:
                return {"error": f"'{NEXTPNR_BIN[target]}' not found. Install OSS CAD Suite."}
            except subprocess.TimeoutExpired:
                return {"error": f"Place and route timed out after {timeout} s."}
    
            combined_output = pnr.stdout + pnr.stderr
    
            return {
                "success":     pnr.returncode == 0,
                "stage":       "place_and_route",
                "target":      target,
                "device":      device,
                "package":     package,
                "top_module":  top_module,
                "timing":      _parse_timing(combined_output),
                "utilization": _parse_utilization(combined_output, target),
                "synth_log":   synth.stdout,
                "pnr_stdout":  pnr.stdout,
                "pnr_stderr":  pnr.stderr,
            }
  • server.py:94-139 (registration)
    Tool registration defining the 'place_and_route' MCP tool with its name, description, and inputSchema including parameters for code, top_module, target, device, package, constraints, timeout, backend, litex_board, and litex_args.
    types.Tool(
        name="place_and_route",
        description=(
            "Synthesize Verilog with Yosys then place-and-route with nextpnr in one step. "
            "If backend=litex, runs LiteX build and ignores Verilog inputs. "
            "Returns max frequency, critical path, resource utilization, and full logs. "
            "Supported targets: ice40, ecp5, nexus, gowin.\n"
            "Common device/package values:\n"
            "  ice40: device=hx1k|hx8k|up5k|lp1k  package=tq144|qn84|sg48|cm81\n"
            "  ecp5:  device=25k|45k|85k            package=CABGA256|CABGA381\n"
            "  nexus: device=LIFCL-40-9BG400C       (package embedded in device string)\n"
            "  gowin: device=GW1N-UV4LQ144C6/I5     (package embedded in device string)"
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "code":        {"type": "string", "description": "Verilog source code"},
                "top_module":  {"type": "string", "description": "Top-level module name"},
                "target":      {
                    "type": "string",
                    "enum": ["ice40", "ecp5", "nexus", "gowin"],
                    "description": "FPGA family",
                },
                "device":      {"type": "string", "description": "Device variant, e.g. 'hx1k', '25k', 'LIFCL-40-9BG400C'"},
                "package":     {"type": "string", "description": "Package, e.g. 'tq144', 'CABGA256' (not needed for nexus/gowin)"},
                "constraints": {"type": "string", "description": "Optional pin constraints (PCF/LPF/PDC/CST text)"},
                "timeout":     {"type": "integer", "default": 300, "description": "PnR timeout in seconds"},
                "backend": {
                    "type": "string",
                    "enum": ["yosys", "litex"],
                    "default": "yosys",
                    "description": "PnR backend",
                },
                "litex_board": {
                    "type": "string",
                    "description": "LiteX board target (required if backend=litex)",
                },
                "litex_args": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "Extra LiteX CLI args (backend=litex)",
                },
            },
            "required": ["code", "top_module", "target", "device"],
        },
    ),
  • server.py:427-440 (registration)
    Tool dispatch handler that routes 'place_and_route' tool calls to the place_and_route function via asyncio.to_thread, passing all arguments from the MCP request.
    case "place_and_route":
        result = await asyncio.to_thread(
            place_and_route,
            code=arguments["code"],
            top_module=arguments["top_module"],
            target=arguments["target"],
            device=arguments["device"],
            package=arguments.get("package", ""),
            constraints=arguments.get("constraints", ""),
            timeout=arguments.get("timeout", 300),
            backend=arguments.get("backend", "yosys"),
            litex_board=arguments.get("litex_board"),
            litex_args=arguments.get("litex_args"),
        )
  • Helper function _build_cmd that constructs the appropriate nextpnr command line arguments for each FPGA target (ice40, ecp5, nexus, gowin), handling device, package, netlist, output file, and constraints.
    def _build_cmd(
        binary: str, target: str, device: str, package: str,
        netlist: str, out_file: str, constraints: str | None,
    ) -> list[str]:
        cmd = [binary]
    
        if target == "ice40":
            cmd += [f"--{device}"]
            if package:
                cmd += ["--package", package]
            cmd += ["--json", netlist, "--asc", out_file]
            if constraints:
                cmd += ["--pcf", constraints]
    
        elif target == "ecp5":
            cmd += [f"--{device}"]
            if package:
                cmd += ["--package", package]
            cmd += ["--json", netlist, "--textcfg", out_file]
            if constraints:
                cmd += ["--lpf", constraints]
    
        elif target == "nexus":
            cmd += ["--device", device, "--json", netlist, "--fasm", out_file]
            if constraints:
                cmd += ["--pdc", constraints]
    
        elif target == "gowin":
            cmd += ["--device", device, "--json", netlist, "--write", out_file]
            if constraints:
                cmd += ["--cst", constraints]
    
        return cmd
  • Helper functions _parse_timing and _parse_utilization that parse nextpnr output to extract timing metrics (max frequency, critical path) and resource utilization (LUTs, IOs, BRAMs, FFs) using regex patterns specific to each FPGA target.
    def _parse_timing(output: str) -> dict:
        timing: dict = {}
    
        # "Max frequency for clock 'clk': 142.34 MHz (PASS at 12.00 MHz)"
        m = re.search(r"Max frequency for clock[^:]*:\s*([\d.]+)\s*MHz", output)
        if m:
            timing["max_freq_mhz"] = float(m.group(1))
    
        # "Critical path: 7.03 ns"
        m = re.search(r"[Cc]ritical path[^:]*:\s*([\d.]+)\s*ns", output)
        if m:
            timing["critical_path_ns"] = float(m.group(1))
    
        return timing
    
    
    def _parse_utilization(output: str, target: str) -> dict:
        util: dict = {}
    
        patterns = {
            "ice40": [
                (r"ICESTORM_LC[:\s]+([\d]+)/\s*([\d]+)",   "luts"),
                (r"SB_IO[:\s]+([\d]+)/\s*([\d]+)",          "ios"),
                (r"SB_RAM40_4K[:\s]+([\d]+)/\s*([\d]+)",    "brams"),
            ],
            "ecp5": [
                (r"LUT4[:\s]+([\d]+)/\s*([\d]+)",           "luts"),
                (r"TRELLIS_IO[:\s]+([\d]+)/\s*([\d]+)",     "ios"),
                (r"TRELLIS_RAMW[:\s]+([\d]+)/\s*([\d]+)",   "brams"),
            ],
            "nexus": [
                (r"OXIDE_COMB[:\s]+([\d]+)/\s*([\d]+)",     "luts"),
                (r"OXIDE_FF[:\s]+([\d]+)/\s*([\d]+)",       "ffs"),
            ],
            "gowin": [
                (r"LUT[:\s]+([\d]+)/\s*([\d]+)",            "luts"),
                (r"FF[:\s]+([\d]+)/\s*([\d]+)",             "ffs"),
            ],
        }
    
        for pattern, label in patterns.get(target, []):
            m = re.search(pattern, output)
            if m:
                util[f"{label}_used"]  = int(m.group(1))
                util[f"{label}_total"] = int(m.group(2))
    
        return util

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bard0-design/fpgaZeroMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server