From d9eba364faacac8b832e5ac916d3852b5e5f6455 Mon Sep 17 00:00:00 2001 From: Karolis Stasaitis Date: Tue, 14 Jul 2026 18:21:02 +0200 Subject: [PATCH] cynthion: build gateware bitstreams and moondancer firmware Co-authored-by: sapphire-arches <1514748+sapphire-arches@users.noreply.github.com> --- .../python-modules/cynthion/build_gateware.py | 112 ++++++++++++++++++ .../python-modules/cynthion/default.nix | 67 ++++++++--- .../python-modules/cynthion/moondancer.nix | 54 +++++++++ 3 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 pkgs/development/python-modules/cynthion/build_gateware.py create mode 100644 pkgs/development/python-modules/cynthion/moondancer.nix diff --git a/pkgs/development/python-modules/cynthion/build_gateware.py b/pkgs/development/python-modules/cynthion/build_gateware.py new file mode 100644 index 000000000000..d5c9b11ff534 --- /dev/null +++ b/pkgs/development/python-modules/cynthion/build_gateware.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 + +# Builds the FPGA bitstreams `cynthion flash` needs, one set per revision, into +# assets//{analyzer,facedancer,selftest}.bit + +import asyncio +import inspect +import logging +import os +import sys +from pathlib import Path + +import cynthion.gateware.platform + +logging.basicConfig(level=logging.INFO, format="%(asctime)s:%(name)32s:%(message)s") + +logger = logging.getLogger("build-gateware") + +BITSTREAMS = ["analyzer", "facedancer", "selftest"] + +# Pin the nextpnr placer seed so place-and-route is deterministic. Some +# bitstreams (e.g. CynthionPlatformRev1D3/analyzer) are marginal on timing, and +# without a fixed seed a build can non-deterministically miss the constraint. +NEXTPNR_SEED = 0 + + +async def build_bitstream( + semaphore: asyncio.Semaphore, assets_dir: Path, platform: str, bitstream: str +) -> None: + async with semaphore: + log = logging.getLogger(f"build-gateware.{platform}.{bitstream}") + log.info("build started") + + env = os.environ.copy() + env["LUNA_PLATFORM"] = f"cynthion.gateware.platform:{platform}" + env["AMARANTH_nextpnr_opts"] = f"--seed {NEXTPNR_SEED}" + + out_dir = assets_dir / platform + out_dir.mkdir(parents=True, exist_ok=True) + out_path = out_dir / f"{bitstream}.bit" + + process = await asyncio.create_subprocess_exec( + sys.executable, + "-m", + f"cynthion.gateware.{bitstream}.top", + "--output", + str(out_path), + env=env, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + ) + + stdout, _ = await process.communicate() + log.info("\n%s", stdout.decode("utf-8", errors="replace")) + + if process.returncode != 0: + raise RuntimeError( + f"{platform}/{bitstream} failed with exit code {process.returncode}" + ) + log.info("build complete") + + +def collect_platforms() -> set[str]: + platforms = { + name + for name, obj in inspect.getmembers(cynthion.gateware.platform) + if inspect.isclass(obj) and name.startswith("CynthionPlatform") + } + + # Allow restricting the set of platforms during development/CI, e.g. + # BUILD_GATEWARE_PLATFORMS="CynthionPlatformRev1D4" + only = os.getenv("BUILD_GATEWARE_PLATFORMS") + if only: + requested = set(only.split()) + unknown = requested - platforms + if unknown: + raise ValueError(f"unknown platform(s): {', '.join(sorted(unknown))}") + platforms = requested + + return platforms + + +async def main() -> int: + assets_dir = Path(sys.argv[1]) + max_workers = int(os.getenv("BUILD_GATEWARE_MAX_WORKERS") or 1) + + platforms = collect_platforms() + logger.info( + "Building %d bitstream(s) for platforms: %s", + len(platforms) * len(BITSTREAMS), + ", ".join(sorted(platforms)), + ) + + semaphore = asyncio.Semaphore(max_workers) + logger.info("Building gateware with %d workers", max_workers) + + results = await asyncio.gather( + *( + build_bitstream(semaphore, assets_dir, platform, bitstream) + for platform in sorted(platforms) + for bitstream in BITSTREAMS + ), + return_exceptions=True, + ) + + failures = [r for r in results if isinstance(r, BaseException)] + for failure in failures: + logger.error(failure) + return 1 if failures else 0 + + +sys.exit(asyncio.run(main())) diff --git a/pkgs/development/python-modules/cynthion/default.nix b/pkgs/development/python-modules/cynthion/default.nix index b61edaeda1db..ab508ccc66d6 100644 --- a/pkgs/development/python-modules/cynthion/default.nix +++ b/pkgs/development/python-modules/cynthion/default.nix @@ -2,6 +2,14 @@ lib, fetchFromGitHub, buildPythonPackage, + callPackage, + python, + + # gateware (FPGA toolchain) + nextpnr, + trellis, + which, + yosys, # build-system setuptools, @@ -25,10 +33,8 @@ pytestCheckHook, udevCheckHook, }: -buildPythonPackage rec { - pname = "cynthion"; +let version = "0.2.5"; - pyproject = true; src = fetchFromGitHub { owner = "greatscottgadgets"; @@ -37,6 +43,14 @@ buildPythonPackage rec { hash = "sha256-Ju01eqBVZ7CD0pw4nIFML4LcCPXzC78dLpQru3a+5bU="; }; + # Moondancer SoC firmware, required for `cynthion flash facedancer`. + moondancer = callPackage ./moondancer.nix { inherit src version; }; +in +buildPythonPackage { + pname = "cynthion"; + inherit version src; + pyproject = true; + sourceRoot = "${src.name}/cynthion/python"; postPatch = '' @@ -45,14 +59,19 @@ buildPythonPackage rec { --replace-fail 'dynamic = ["version"]' 'version = "${version}"' ''; - nativeBuildInputs = [ udevCheckHook ]; + nativeBuildInputs = [ + udevCheckHook - build-system = [ - setuptools + # Used by the gateware build in postInstall. + nextpnr + trellis + which + yosys ]; - pythonRelaxDeps = [ "pygreat" ]; + build-system = [ setuptools ]; + pythonRelaxDeps = [ "pygreat" ]; pythonRemoveDeps = [ "future" ]; dependencies = [ @@ -71,18 +90,34 @@ buildPythonPackage rec { tqdm ]; - nativeCheckInputs = [ - pytestCheckHook - ]; + nativeCheckInputs = [ pytestCheckHook ]; + + # Build the per-revision FPGA bitstreams in parallel (see build_gateware.py). + enableParallelBuilding = true; pythonImportsCheck = [ "cynthion" ]; - # Make udev rules available for NixOS option services.udev.packages - postInstall = '' - install -Dm444 \ - -t $out/lib/udev/rules.d \ - build/lib/cynthion/assets/54-cynthion.rules - ''; + postInstall = + let + assets = "$out/${python.sitePackages}/cynthion/assets"; + in + '' + # Build a bitstream set per hardware revision into assets/, where + # `cynthion flash` looks for them. + export BUILD_GATEWARE_MAX_WORKERS="''${NIX_BUILD_CORES:-1}" + PYTHONPATH="$out/${python.sitePackages}''${PYTHONPATH:+:$PYTHONPATH}" \ + python ${./build_gateware.py} "${assets}" + + # Install the moondancer SoC firmware for `cynthion flash facedancer`. + install -Dm444 ${moondancer}/bin/moondancer.bin "${assets}/moondancer.bin" + + # Make udev rules available for NixOS option services.udev.packages + install -Dm444 \ + -t $out/lib/udev/rules.d \ + build/lib/cynthion/assets/54-cynthion.rules + ''; + + passthru = { inherit moondancer; }; meta = { description = "Python package and utilities for the Great Scott Gadgets Cynthion USB Test Instrument"; diff --git a/pkgs/development/python-modules/cynthion/moondancer.nix b/pkgs/development/python-modules/cynthion/moondancer.nix new file mode 100644 index 000000000000..e71d828dda1f --- /dev/null +++ b/pkgs/development/python-modules/cynthion/moondancer.nix @@ -0,0 +1,54 @@ +{ + lib, + src, + version, + stdenv, + llvmPackages, +}: +let + # Moondancer SoC firmware, cross-compiled for the bare-metal riscv32imac + # VexRiscv softcore inside the facedancer gateware. + cross = import ../../../.. { + localSystem = stdenv.hostPlatform.system; + crossSystem = lib.systems.examples.riscv32-embedded // { + rust.rustcTarget = "riscv32imac-unknown-none-elf"; + }; + }; + + inherit (cross) rustPlatform; +in +rustPlatform.buildRustPackage { + pname = "moondancer"; + + inherit version src; + + sourceRoot = "${src.name}/firmware"; + + cargoHash = "sha256-G/9evh3G1xNRaaEh6lgDp3hnVlB3MaCwXuhGnGJCd0Q="; + + # fails to build otherwise + auditable = false; + + nativeBuildInputs = [ + llvmPackages.bintools + ]; + + # lld strips live code without link-dead-code; memory.x (workspace root) and + # link.x (from riscv-rt) are the linker scripts. + RUSTFLAGS = "-C linker=lld -C link-arg=-Tmemory.x -C link-arg=-Tlink.x -C link-dead-code"; + + # cynthion flash needs a raw image, not an ELF (as upstream's + # `cargo objcopy -Obinary`). + postInstall = '' + llvm-objcopy -O binary \ + $out/bin/moondancer \ + $out/bin/moondancer.bin + ''; + + meta = { + description = "Moondancer SoC firmware for the Great Scott Gadgets Cynthion"; + homepage = "https://github.com/greatscottgadgets/cynthion"; + license = lib.licenses.bsd3; + platforms = [ "riscv32-none" ]; + }; +}