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
| Name | Required | Description | Default |
|---|---|---|---|
| code | Yes | Verilog source code | |
| top_module | Yes | Top-level module name | |
| target | Yes | FPGA family | |
| device | Yes | Device variant, e.g. 'hx1k', '25k', 'LIFCL-40-9BG400C' | |
| package | No | Package, e.g. 'tq144', 'CABGA256' (not needed for nexus/gowin) | |
| constraints | No | Optional pin constraints (PCF/LPF/PDC/CST text) | |
| timeout | No | PnR timeout in seconds | |
| backend | No | PnR backend | yosys |
| litex_board | No | LiteX board target (required if backend=litex) | |
| litex_args | No | Extra LiteX CLI args (backend=litex) |
Implementation Reference
- tools/pnr.py:37-160 (handler)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"), ) - tools/pnr.py:163-195 (helper)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 - tools/pnr.py:198-244 (helper)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