diff --git a/.gitignore b/.gitignore index 7a6353d..935164a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .envrc +out/ diff --git a/flake.nix b/flake.nix index ab2561c..3e4ec6f 100644 --- a/flake.nix +++ b/flake.nix @@ -27,7 +27,9 @@ nextpnr openfpgaloader picocom - python3 + (python3.withPackages (py-pkgs: [ + py-pkgs.invoke + ])) symbiyosys trellis verilator diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..c3607c7 --- /dev/null +++ b/tasks.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python + +import os.path +from pathlib import Path +from invoke import task +from textwrap import dedent +import shutil + +def ensure_build_dirs(root : Path, *subdirs : list[Path]): + if root.is_file(): + root = root.parent / root.stem + ret = [] + for d in subdirs: + out = "out" / root / d + out.mkdir(mode=0o750, parents=True, exist_ok=True) + ret.append(out) + return ret + +def bsc_root(c): + out = c.run("bsc --help", hide="both") + for l in out.stdout.splitlines(): + dir_prefix = "Bluespec directory: " + if l.startswith(dir_prefix): + return Path(l[len(dir_prefix):]) + raise RuntimeError("Couldn't locate Bluespec root dir") + +def find_verilog_modules(c, modules): + libpaths = [Path("lib"), bsc_root(c) / "Verilog"] + ret = [] + for m in modules: + module_path = None + for p in libpaths: + f = p / Path(module).with_suffix(".v") + if f.is_file(): + module_path = f + break + if module_path is None: + raise RuntimeError(f"Cannot find verilog module {m} in {libpaths}") + ret.append(module_path) + return ret + +def print_filtered_paragraphs(s, *paragraph_prefixes, common_prefix=""): + for block in s.split("\n\n"): + for p in paragraph_prefixes: + if block.removeprefix(common_prefix).startswith(p): + print('\n'.join(s.removeprefix(common_prefix) for s in block.splitlines())) + print("") + +def expand_build_target(target): + t = Path(target) + if t.is_dir(): + yield from t.glob("**/*.bsv") + elif t.is_file() and t.suffix == ".bsv": + yield t + else: + raise ValueError(f"Unknown target type {t}") + +def resolve_synth_target(target): + if '/' not in str(target): + target = "hardware" / Path(target) + if target.is_dir(): + target /= "Top.bsv" + if not target.is_file(): + raise ArgumentError(f"Unknown target type {target}") + return target + +def expand_test_target(target): + t = Path(target) + if t.is_dir(): + yield from t.glob("**/*_Test.bsv") + elif t.is_file() and t.name.endswith("_Test.bsv"): + yield t + else: + raise ValueError(f"Unknown target type {t}") + +@task +def build(c, target="."): + for target in expand_build_target(target): + out_info, out_verilog, out_bsc = ensure_build_dirs(target, "info", "verilog", "bsc") + print(f"Building {target}") + c.run(f"bsc -aggressive-conditions -check-assert -u -verilog -info-dir {out_info} -vdir {out_verilog} -bdir {out_bsc} -p {target.parent}:lib:%/Libraries -show-module-use -show-compiles {target}") + +@task +def synth(c, target): + target = resolve_synth_target(target) + + pin_map = target.parent / "pin_map.lpf" + if not pin_map.is_file(): + raise ArgumentError(f"Pin mapping {pin_map} not found") + + module_name = Path(f"mk{target.stem}") + out_info, out_verilog, out_bsc, out_yosys, out_nextpnr = ensure_build_dirs(target, "info", "verilog", "bsc", "yosys", "nextpnr") + + build(c, target) + + verilog_files = [out_verilog / module_name.with_suffix(".v")] + with open(out_verilog / module_name.with_suffix(".use")) as f: + verilog_files.extend(find_verilog_modules(c, f.read().splitlines())) + + yosys_script = out_yosys / "script.ys" + yosys_json = out_yosys / module_name.with_suffix(".json") + yosys_report = out_yosys / module_name.with_suffix(".stats") + yosys_log = out_yosys / module_name.with_suffix(".log") + with open(yosys_script, "w", encoding="UTF-8") as f: + f.write(dedent(f"""\ + read_verilog -sv -defer {' '.join(str(f) for f in verilog_files)} + hierarchy -top {module_name} + synth_ecp5 -top {module_name} -json {yosys_json} + tee -o {yosys_report} stat + """)) + c.run(f"yosys -q -L {yosys_log} -T {yosys_script}") + with open(yosys_report) as f: + print(f.read()) + + nextpnr_out = out_nextpnr / module_name.with_suffix(".pnr") + out = c.run(f"nextpnr-ecp5 --85k --detailed-timing-report --report {out_nextpnr / "timing.json"} --json {yosys_json} --lpf {pin_map} --package=CABGA381 --textcfg {nextpnr_out}", hide='stderr') + print_filtered_paragraphs(out.stderr, "Device utilization", "Critical path", common_prefix="Info: ") + + bitstream = nextpnr_out.with_suffix(".bit") + c.run(f"ecppack {nextpnr_out} {bitstream}") + print(f"Wrote bitstream to {bitstream}") + +@task +def test(c, target="."): + for target in expand_test_target(target): + out_info, out_sim, out_bsc = ensure_build_dirs(target, "info", "sim", "bsc") + c.run(f"bsc -show-schedule -aggressive-conditions -check-assert -u -sim -info-dir {out_info} -simdir {out_sim} -bdir {out_bsc} -g mkTB -p {target.parent}:lib:%/Libraries {target}") + exec = out_sim / "TB" + c.run(f"bsc -p {out_bsc} -sim -simdir {out_sim} -e mkTB -o {exec}") + testdata = out_sim / "testdata" + testdata_tgt = target.parent / "testdata" + testdata_tgt = testdata_tgt.relative_to(out_sim, walk_up=True) + testdata.unlink(missing_ok=True) + testdata.symlink_to(testdata_tgt, target_is_directory=True) + print("before run") + with c.cd(out_sim): + c.run(f"./TB -V {target.stem}.vcd") + +@task +def clean(c): + shutil.rmtree("out")