devlab-fpga 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devlab/__init__.py +6 -0
- devlab/__main__.py +5 -0
- devlab/cli.py +170 -0
- devlab/errors.py +3 -0
- devlab/platforms.py +40 -0
- devlab/project.py +430 -0
- devlab/toolchain.py +242 -0
- devlab_fpga-0.1.0.dist-info/METADATA +188 -0
- devlab_fpga-0.1.0.dist-info/RECORD +13 -0
- devlab_fpga-0.1.0.dist-info/WHEEL +5 -0
- devlab_fpga-0.1.0.dist-info/entry_points.txt +2 -0
- devlab_fpga-0.1.0.dist-info/licenses/LICENSE +21 -0
- devlab_fpga-0.1.0.dist-info/top_level.txt +1 -0
devlab/__init__.py
ADDED
devlab/__main__.py
ADDED
devlab/cli.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import platform
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .errors import DevlabError
|
|
9
|
+
from .platforms import current_platform
|
|
10
|
+
from .project import build_project, create_project, detect_flash, doctor, flash_project
|
|
11
|
+
from .toolchain import (
|
|
12
|
+
OSS_CAD_SUITE_RELEASE_URL,
|
|
13
|
+
archive_path,
|
|
14
|
+
devlab_home,
|
|
15
|
+
install_oss_cad_suite,
|
|
16
|
+
install_path,
|
|
17
|
+
select_asset,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
22
|
+
parser = argparse.ArgumentParser(
|
|
23
|
+
prog="devlab",
|
|
24
|
+
description="Install FPGA toolchains and run local FPGA build flows.",
|
|
25
|
+
)
|
|
26
|
+
parser.add_argument("--version", action="version", version=f"devlab {__version__}")
|
|
27
|
+
|
|
28
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
29
|
+
|
|
30
|
+
doctor_parser = commands.add_parser("doctor", help="Check local FPGA tools.")
|
|
31
|
+
doctor_parser.add_argument(
|
|
32
|
+
"--strict",
|
|
33
|
+
action="store_true",
|
|
34
|
+
help="Exit 1 when a tool is missing.",
|
|
35
|
+
)
|
|
36
|
+
doctor_parser.set_defaults(func=_doctor)
|
|
37
|
+
|
|
38
|
+
install_parser = commands.add_parser("install", help="Install OSS CAD Suite.")
|
|
39
|
+
install_parser.add_argument("--force", action="store_true", help="Re-download and reinstall.")
|
|
40
|
+
install_parser.add_argument(
|
|
41
|
+
"--run-installer",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="On Windows, execute the downloaded OSS CAD Suite installer.",
|
|
44
|
+
)
|
|
45
|
+
install_parser.set_defaults(func=_install)
|
|
46
|
+
|
|
47
|
+
new_parser = commands.add_parser("new", help="Create a new FPGA project.")
|
|
48
|
+
new_parser.add_argument("name", help="Project directory name.")
|
|
49
|
+
new_parser.add_argument("--dir", type=Path, help="Target directory. Defaults to NAME.")
|
|
50
|
+
new_parser.add_argument("--force", action="store_true", help="Overwrite template files.")
|
|
51
|
+
new_parser.add_argument(
|
|
52
|
+
"--hdl",
|
|
53
|
+
choices=("verilog", "vhdl"),
|
|
54
|
+
default="verilog",
|
|
55
|
+
help="Source template language. Defaults to verilog.",
|
|
56
|
+
)
|
|
57
|
+
new_parser.set_defaults(func=_new)
|
|
58
|
+
|
|
59
|
+
build = commands.add_parser("build", help="Build the current FPGA project.")
|
|
60
|
+
build.add_argument("-c", "--config", type=Path, help="Path to devlab.toml.")
|
|
61
|
+
build.add_argument(
|
|
62
|
+
"--dry-run",
|
|
63
|
+
action="store_true",
|
|
64
|
+
help="Print commands without running them.",
|
|
65
|
+
)
|
|
66
|
+
build.set_defaults(func=_build)
|
|
67
|
+
|
|
68
|
+
flash = commands.add_parser("flash", help="Flash the built FPGA bitstream.")
|
|
69
|
+
flash.add_argument("-c", "--config", type=Path, help="Path to devlab.toml.")
|
|
70
|
+
flash.add_argument("--board", help="openFPGALoader board name.")
|
|
71
|
+
flash.add_argument("--artifact", type=Path, help="Bitstream path.")
|
|
72
|
+
flash.add_argument(
|
|
73
|
+
"--mode",
|
|
74
|
+
choices=("sram", "flash"),
|
|
75
|
+
help="Write target. sram is temporary; flash is persistent after reboot.",
|
|
76
|
+
)
|
|
77
|
+
flash.add_argument(
|
|
78
|
+
"--detect",
|
|
79
|
+
action="store_true",
|
|
80
|
+
help="Detect FPGA and flash support without writing a bitstream.",
|
|
81
|
+
)
|
|
82
|
+
flash.add_argument(
|
|
83
|
+
"--verify",
|
|
84
|
+
action="store_true",
|
|
85
|
+
help="Verify flash write when supported by the board/programming path.",
|
|
86
|
+
)
|
|
87
|
+
flash.add_argument(
|
|
88
|
+
"--no-verify",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help="Skip flash verification when using --mode flash.",
|
|
91
|
+
)
|
|
92
|
+
flash.add_argument(
|
|
93
|
+
"--external-flash",
|
|
94
|
+
action="store_true",
|
|
95
|
+
help="Use external flash when the device has internal and external storage.",
|
|
96
|
+
)
|
|
97
|
+
flash.add_argument("--offset", help="Flash offset in bytes.")
|
|
98
|
+
flash.add_argument("--dry-run", action="store_true", help="Print command without running it.")
|
|
99
|
+
flash.set_defaults(func=_flash)
|
|
100
|
+
|
|
101
|
+
return parser
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main(argv: list[str] | None = None) -> int:
|
|
105
|
+
parser = build_parser()
|
|
106
|
+
args = parser.parse_args(argv)
|
|
107
|
+
try:
|
|
108
|
+
return args.func(args)
|
|
109
|
+
except DevlabError as exc:
|
|
110
|
+
parser.exit(2, f"devlab: error: {exc}\n")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _doctor(args: argparse.Namespace) -> int:
|
|
114
|
+
platform_id = current_platform()
|
|
115
|
+
asset = select_asset(platform_id)
|
|
116
|
+
print(f"devlab: {__version__}")
|
|
117
|
+
print(f"python: {platform.python_version()} ({platform.python_implementation()})")
|
|
118
|
+
print(f"platform: {platform_id.key}")
|
|
119
|
+
print(f"home: {devlab_home()}")
|
|
120
|
+
print(f"release: {OSS_CAD_SUITE_RELEASE_URL}")
|
|
121
|
+
print(f"asset: {asset.name}")
|
|
122
|
+
print(f"archive: {archive_path(asset)}")
|
|
123
|
+
print(f"toolchain: {install_path(asset)}")
|
|
124
|
+
return doctor(strict=args.strict)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _install(args: argparse.Namespace) -> int:
|
|
128
|
+
destination = install_oss_cad_suite(
|
|
129
|
+
force=args.force,
|
|
130
|
+
run_windows_installer=args.run_installer,
|
|
131
|
+
)
|
|
132
|
+
print(f"OSS CAD Suite installed at {destination}")
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _new(args: argparse.Namespace) -> int:
|
|
137
|
+
root = create_project(args.name, directory=args.dir, force=args.force, hdl=args.hdl)
|
|
138
|
+
print(f"Created FPGA project at {root}")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _build(args: argparse.Namespace) -> int:
|
|
143
|
+
artifact = build_project(config_path=args.config, dry_run=args.dry_run)
|
|
144
|
+
print(f"Build artifact: {artifact}")
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _flash(args: argparse.Namespace) -> int:
|
|
149
|
+
if args.verify and args.no_verify:
|
|
150
|
+
raise DevlabError("--verify and --no-verify cannot be used together.")
|
|
151
|
+
|
|
152
|
+
if args.detect:
|
|
153
|
+
detect_flash(
|
|
154
|
+
config_path=args.config,
|
|
155
|
+
board=args.board,
|
|
156
|
+
dry_run=args.dry_run,
|
|
157
|
+
)
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
flash_project(
|
|
161
|
+
config_path=args.config,
|
|
162
|
+
board=args.board,
|
|
163
|
+
artifact=args.artifact,
|
|
164
|
+
mode=args.mode,
|
|
165
|
+
verify=True if args.verify else False if args.no_verify else None,
|
|
166
|
+
external_flash=True if args.external_flash else None,
|
|
167
|
+
offset=args.offset,
|
|
168
|
+
dry_run=args.dry_run,
|
|
169
|
+
)
|
|
170
|
+
return 0
|
devlab/errors.py
ADDED
devlab/platforms.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import platform
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from .errors import DevlabError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class PlatformId:
|
|
11
|
+
os_name: str
|
|
12
|
+
arch: str
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def key(self) -> str:
|
|
16
|
+
return f"{self.os_name}-{self.arch}"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def current_platform() -> PlatformId:
|
|
20
|
+
system = platform.system().lower()
|
|
21
|
+
machine = platform.machine().lower()
|
|
22
|
+
|
|
23
|
+
if system == "linux":
|
|
24
|
+
os_name = "linux"
|
|
25
|
+
elif system == "darwin":
|
|
26
|
+
os_name = "darwin"
|
|
27
|
+
elif system in {"windows", "msys", "cygwin"}:
|
|
28
|
+
os_name = "windows"
|
|
29
|
+
else:
|
|
30
|
+
raise DevlabError(f"Unsupported operating system: {platform.system()}")
|
|
31
|
+
|
|
32
|
+
if machine in {"x86_64", "amd64"}:
|
|
33
|
+
arch = "x64"
|
|
34
|
+
elif machine in {"aarch64", "arm64"}:
|
|
35
|
+
arch = "arm64"
|
|
36
|
+
else:
|
|
37
|
+
raise DevlabError(f"Unsupported CPU architecture: {platform.machine()}")
|
|
38
|
+
|
|
39
|
+
return PlatformId(os_name=os_name, arch=arch)
|
|
40
|
+
|
devlab/project.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shlex
|
|
4
|
+
import subprocess
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .errors import DevlabError
|
|
10
|
+
from .toolchain import env_with_toolchain, find_executable, install_path
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import tomllib
|
|
14
|
+
except ModuleNotFoundError: # pragma: no cover - Python < 3.11
|
|
15
|
+
import tomli as tomllib # type: ignore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
DEFAULT_CONFIG = "devlab.toml"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BuildConfig:
|
|
23
|
+
family: str = "GW1N-9C"
|
|
24
|
+
device: str = "GW1NR-LV9QN88PC6/I5"
|
|
25
|
+
package: str = "sg48"
|
|
26
|
+
top: str = "top"
|
|
27
|
+
sources: list[str] = field(default_factory=lambda: ["src/top.v"])
|
|
28
|
+
constraints: str = "pins.cst"
|
|
29
|
+
build_dir: str = "build"
|
|
30
|
+
board: str | None = None
|
|
31
|
+
flash_mode: str = "sram"
|
|
32
|
+
flash_verify: bool = False
|
|
33
|
+
external_flash: bool = False
|
|
34
|
+
flash_offset: str | None = None
|
|
35
|
+
series: str | None = None
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def artifact(self) -> str:
|
|
39
|
+
if self.flow == "ecp5":
|
|
40
|
+
return f"{self.build_dir}/{self.top}.bit"
|
|
41
|
+
if self.flow == "gowin":
|
|
42
|
+
return f"{self.build_dir}/{self.top}.fs"
|
|
43
|
+
return f"{self.build_dir}/{self.top}.bin"
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def flow(self) -> str:
|
|
47
|
+
family = self.family.lower()
|
|
48
|
+
if family in {"gowin", "apicula"} or family.startswith("gw"):
|
|
49
|
+
return "gowin"
|
|
50
|
+
return family
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def gowin_family(self) -> str:
|
|
54
|
+
if self.series:
|
|
55
|
+
return self.series
|
|
56
|
+
if self.family.lower() in {"gowin", "apicula"}:
|
|
57
|
+
raise DevlabError(
|
|
58
|
+
"Gowin builds need [fpga] family = \"GW1N-9C\" "
|
|
59
|
+
"or [fpga] series = \"GW1N-9C\"."
|
|
60
|
+
)
|
|
61
|
+
return self.family
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def load_config(path: Path | None = None) -> BuildConfig:
|
|
65
|
+
config_path = path or Path(DEFAULT_CONFIG)
|
|
66
|
+
if not config_path.exists():
|
|
67
|
+
raise DevlabError(f"Project config not found: {config_path}")
|
|
68
|
+
|
|
69
|
+
with config_path.open("rb") as handle:
|
|
70
|
+
data = tomllib.load(handle)
|
|
71
|
+
|
|
72
|
+
fpga = _section(data, "fpga")
|
|
73
|
+
build = _section(data, "build")
|
|
74
|
+
flash = _section(data, "flash")
|
|
75
|
+
|
|
76
|
+
return BuildConfig(
|
|
77
|
+
family=str(fpga.get("family", "GW1N-9C")),
|
|
78
|
+
device=str(fpga.get("device", "GW1NR-LV9QN88PC6/I5")),
|
|
79
|
+
package=str(fpga.get("package", "sg48")),
|
|
80
|
+
top=str(build.get("top", "top")),
|
|
81
|
+
sources=[str(item) for item in build.get("sources", ["src/top.v"])],
|
|
82
|
+
constraints=str(build.get("constraints", fpga.get("cst", "pins.cst"))),
|
|
83
|
+
build_dir=str(build.get("build_dir", "build")),
|
|
84
|
+
board=flash.get("board"),
|
|
85
|
+
flash_mode=str(flash.get("mode", "sram")).lower(),
|
|
86
|
+
flash_verify=bool(flash.get("verify", False)),
|
|
87
|
+
external_flash=bool(flash.get("external_flash", False)),
|
|
88
|
+
flash_offset=str(flash["offset"]) if "offset" in flash else None,
|
|
89
|
+
series=fpga.get("series"),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_project(
|
|
94
|
+
name: str,
|
|
95
|
+
directory: Path | None = None,
|
|
96
|
+
force: bool = False,
|
|
97
|
+
hdl: str = "verilog",
|
|
98
|
+
) -> Path:
|
|
99
|
+
hdl = hdl.lower()
|
|
100
|
+
if hdl not in {"verilog", "vhdl"}:
|
|
101
|
+
raise DevlabError(f"Unsupported HDL: {hdl}")
|
|
102
|
+
|
|
103
|
+
root = directory or Path(name)
|
|
104
|
+
if root.exists() and any(root.iterdir()) and not force:
|
|
105
|
+
raise DevlabError(f"Directory is not empty: {root}")
|
|
106
|
+
|
|
107
|
+
source_name = "top.vhd" if hdl == "vhdl" else "top.v"
|
|
108
|
+
(root / "src").mkdir(parents=True, exist_ok=True)
|
|
109
|
+
_write(root / "devlab.toml", _template_config(source_name), force)
|
|
110
|
+
_write(root / "src" / source_name, _template_top(hdl), force)
|
|
111
|
+
_write(root / "pins.cst", _template_constraints(), force)
|
|
112
|
+
_write(root / ".gitignore", "build/\n", force)
|
|
113
|
+
return root
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def build_project(config_path: Path | None = None, dry_run: bool = False) -> Path:
|
|
117
|
+
config = load_config(config_path)
|
|
118
|
+
build_dir = Path(config.build_dir)
|
|
119
|
+
build_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
|
|
121
|
+
commands = build_commands(config)
|
|
122
|
+
for command in commands:
|
|
123
|
+
_run(command, dry_run=dry_run)
|
|
124
|
+
|
|
125
|
+
return Path(config.artifact)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def flash_project(
|
|
129
|
+
config_path: Path | None = None,
|
|
130
|
+
board: str | None = None,
|
|
131
|
+
artifact: Path | None = None,
|
|
132
|
+
mode: str | None = None,
|
|
133
|
+
verify: bool | None = None,
|
|
134
|
+
external_flash: bool | None = None,
|
|
135
|
+
offset: str | None = None,
|
|
136
|
+
dry_run: bool = False,
|
|
137
|
+
) -> None:
|
|
138
|
+
config = load_config(config_path)
|
|
139
|
+
selected_board = board or config.board
|
|
140
|
+
if not selected_board:
|
|
141
|
+
raise DevlabError(
|
|
142
|
+
"No flash board configured. Add [flash] board = \"...\" "
|
|
143
|
+
"to devlab.toml or pass --board."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
bitstream = artifact or Path(config.artifact)
|
|
147
|
+
if not dry_run and not bitstream.exists():
|
|
148
|
+
raise DevlabError(f"Bitstream not found: {bitstream}")
|
|
149
|
+
|
|
150
|
+
_run(
|
|
151
|
+
flash_command(
|
|
152
|
+
config,
|
|
153
|
+
selected_board,
|
|
154
|
+
bitstream,
|
|
155
|
+
mode=mode,
|
|
156
|
+
verify=verify,
|
|
157
|
+
external_flash=external_flash,
|
|
158
|
+
offset=offset,
|
|
159
|
+
),
|
|
160
|
+
dry_run=dry_run,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def detect_flash(
|
|
165
|
+
config_path: Path | None = None,
|
|
166
|
+
board: str | None = None,
|
|
167
|
+
dry_run: bool = False,
|
|
168
|
+
) -> None:
|
|
169
|
+
config = load_config(config_path)
|
|
170
|
+
selected_board = board or config.board
|
|
171
|
+
if not selected_board:
|
|
172
|
+
raise DevlabError(
|
|
173
|
+
"No flash board configured. Add [flash] board = \"...\" "
|
|
174
|
+
"to devlab.toml or pass --board."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
_run(["openFPGALoader", "-b", selected_board, "--detect", "-f"], dry_run=dry_run)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def flash_command(
|
|
181
|
+
config: BuildConfig,
|
|
182
|
+
board: str,
|
|
183
|
+
bitstream: Path,
|
|
184
|
+
mode: str | None = None,
|
|
185
|
+
verify: bool | None = None,
|
|
186
|
+
external_flash: bool | None = None,
|
|
187
|
+
offset: str | None = None,
|
|
188
|
+
) -> list[str]:
|
|
189
|
+
selected_mode = (mode or config.flash_mode).lower()
|
|
190
|
+
if selected_mode not in {"sram", "flash"}:
|
|
191
|
+
raise DevlabError(f"Unsupported flash mode: {selected_mode}")
|
|
192
|
+
|
|
193
|
+
command = ["openFPGALoader", "-b", board]
|
|
194
|
+
if selected_mode == "flash":
|
|
195
|
+
command.append("-f")
|
|
196
|
+
selected_verify = verify if verify is not None else config.flash_verify
|
|
197
|
+
if selected_verify:
|
|
198
|
+
command.append("--verify")
|
|
199
|
+
selected_external = (
|
|
200
|
+
external_flash if external_flash is not None else config.external_flash
|
|
201
|
+
)
|
|
202
|
+
if selected_external:
|
|
203
|
+
command.append("--external-flash")
|
|
204
|
+
selected_offset = offset if offset is not None else config.flash_offset
|
|
205
|
+
if selected_offset:
|
|
206
|
+
command.extend(["--offset", selected_offset])
|
|
207
|
+
else:
|
|
208
|
+
command.append("-m")
|
|
209
|
+
|
|
210
|
+
command.append(str(bitstream))
|
|
211
|
+
return command
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_commands(config: BuildConfig) -> list[list[str]]:
|
|
215
|
+
if config.flow == "gowin":
|
|
216
|
+
json_file = f"{config.build_dir}/{config.top}.json"
|
|
217
|
+
pnr_json_file = f"{config.build_dir}/{config.top}_pnr.json"
|
|
218
|
+
return [
|
|
219
|
+
_yosys_command(config, f"synth_gowin -top {config.top} -json {json_file}"),
|
|
220
|
+
[
|
|
221
|
+
"nextpnr-himbaechel",
|
|
222
|
+
"--json",
|
|
223
|
+
json_file,
|
|
224
|
+
"--write",
|
|
225
|
+
pnr_json_file,
|
|
226
|
+
"--device",
|
|
227
|
+
config.device,
|
|
228
|
+
"--vopt",
|
|
229
|
+
f"family={config.gowin_family}",
|
|
230
|
+
"--vopt",
|
|
231
|
+
f"cst={config.constraints}",
|
|
232
|
+
],
|
|
233
|
+
[
|
|
234
|
+
"gowin_pack",
|
|
235
|
+
"-d",
|
|
236
|
+
config.gowin_family,
|
|
237
|
+
"-o",
|
|
238
|
+
config.artifact,
|
|
239
|
+
pnr_json_file,
|
|
240
|
+
],
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
if config.flow == "ice40":
|
|
244
|
+
json_file = f"{config.build_dir}/{config.top}.json"
|
|
245
|
+
asc_file = f"{config.build_dir}/{config.top}.asc"
|
|
246
|
+
return [
|
|
247
|
+
_yosys_command(config, f"synth_ice40 -top {config.top} -json {json_file}"),
|
|
248
|
+
[
|
|
249
|
+
"nextpnr-ice40",
|
|
250
|
+
f"--{config.device}",
|
|
251
|
+
"--package",
|
|
252
|
+
config.package,
|
|
253
|
+
"--json",
|
|
254
|
+
json_file,
|
|
255
|
+
"--pcf",
|
|
256
|
+
config.constraints,
|
|
257
|
+
"--asc",
|
|
258
|
+
asc_file,
|
|
259
|
+
],
|
|
260
|
+
["icepack", asc_file, config.artifact],
|
|
261
|
+
]
|
|
262
|
+
|
|
263
|
+
if config.flow == "ecp5":
|
|
264
|
+
json_file = f"{config.build_dir}/{config.top}.json"
|
|
265
|
+
cfg_file = f"{config.build_dir}/{config.top}.config"
|
|
266
|
+
return [
|
|
267
|
+
_yosys_command(config, f"synth_ecp5 -top {config.top} -json {json_file}"),
|
|
268
|
+
[
|
|
269
|
+
"nextpnr-ecp5",
|
|
270
|
+
"--json",
|
|
271
|
+
json_file,
|
|
272
|
+
"--lpf",
|
|
273
|
+
config.constraints,
|
|
274
|
+
"--textcfg",
|
|
275
|
+
cfg_file,
|
|
276
|
+
"--um5g-85k" if config.device == "um5g-85k" else f"--{config.device}",
|
|
277
|
+
"--package",
|
|
278
|
+
config.package,
|
|
279
|
+
],
|
|
280
|
+
["ecppack", cfg_file, config.artifact],
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
raise DevlabError(f"Unsupported FPGA family: {config.family}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def doctor(strict: bool = False) -> int:
|
|
287
|
+
checks = {
|
|
288
|
+
"yosys": find_executable("yosys", install_path()),
|
|
289
|
+
"ghdl": find_executable("ghdl", install_path()),
|
|
290
|
+
"nextpnr-himbaechel": find_executable("nextpnr-himbaechel", install_path()),
|
|
291
|
+
"gowin_pack": find_executable("gowin_pack", install_path()),
|
|
292
|
+
"nextpnr-ice40": find_executable("nextpnr-ice40", install_path()),
|
|
293
|
+
"nextpnr-ecp5": find_executable("nextpnr-ecp5", install_path()),
|
|
294
|
+
"icepack": find_executable("icepack", install_path()),
|
|
295
|
+
"ecppack": find_executable("ecppack", install_path()),
|
|
296
|
+
"openFPGALoader": find_executable("openFPGALoader", install_path()),
|
|
297
|
+
}
|
|
298
|
+
failed = False
|
|
299
|
+
for name, executable in checks.items():
|
|
300
|
+
if executable:
|
|
301
|
+
print(f"ok {name}: {executable}")
|
|
302
|
+
else:
|
|
303
|
+
failed = True
|
|
304
|
+
print(f"missing {name}")
|
|
305
|
+
if failed:
|
|
306
|
+
print("Run `devlab install` to install OSS CAD Suite.")
|
|
307
|
+
return 1 if strict and failed else 0
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _yosys_command(config: BuildConfig, synth_script: str) -> list[str]:
|
|
311
|
+
hdl = _source_hdl(config.sources)
|
|
312
|
+
if hdl == "vhdl":
|
|
313
|
+
sources = " ".join(shlex.quote(source) for source in config.sources)
|
|
314
|
+
return [
|
|
315
|
+
"yosys",
|
|
316
|
+
"-m",
|
|
317
|
+
"ghdl",
|
|
318
|
+
"-p",
|
|
319
|
+
f"ghdl --std=08 {sources} -e {config.top}; {synth_script}",
|
|
320
|
+
]
|
|
321
|
+
return ["yosys", "-p", synth_script, *config.sources]
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _source_hdl(sources: list[str]) -> str:
|
|
325
|
+
has_vhdl = any(Path(source).suffix.lower() in {".vhd", ".vhdl"} for source in sources)
|
|
326
|
+
has_verilog = any(
|
|
327
|
+
Path(source).suffix.lower() in {".v", ".vh", ".sv", ".svh"} for source in sources
|
|
328
|
+
)
|
|
329
|
+
if has_vhdl and has_verilog:
|
|
330
|
+
raise DevlabError("Mixed VHDL and Verilog sources are not supported yet.")
|
|
331
|
+
if has_vhdl:
|
|
332
|
+
return "vhdl"
|
|
333
|
+
return "verilog"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _section(data: dict[str, Any], name: str) -> dict[str, Any]:
|
|
337
|
+
value = data.get(name, {})
|
|
338
|
+
if not isinstance(value, dict):
|
|
339
|
+
raise DevlabError(f"[{name}] must be a table in devlab.toml")
|
|
340
|
+
return value
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _run(command: list[str], dry_run: bool) -> None:
|
|
344
|
+
resolved = find_executable(command[0], install_path())
|
|
345
|
+
if not resolved:
|
|
346
|
+
if dry_run:
|
|
347
|
+
print("+ " + " ".join(command))
|
|
348
|
+
return
|
|
349
|
+
raise DevlabError(
|
|
350
|
+
f"Required command not found: {command[0]}. Run `devlab install` first."
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
executable_command = [resolved, *command[1:]]
|
|
354
|
+
print("+ " + " ".join(executable_command))
|
|
355
|
+
if dry_run:
|
|
356
|
+
return
|
|
357
|
+
subprocess.check_call(executable_command, env=env_with_toolchain(install_path()))
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _write(path: Path, content: str, force: bool) -> None:
|
|
361
|
+
if path.exists() and not force:
|
|
362
|
+
raise DevlabError(f"File already exists: {path}")
|
|
363
|
+
path.write_text(content, encoding="utf-8")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _template_config(source_name: str = "top.v") -> str:
|
|
367
|
+
return """[fpga]
|
|
368
|
+
family = "GW1N-9C"
|
|
369
|
+
device = "GW1NR-LV9QN88PC6/I5"
|
|
370
|
+
cst = "pins.cst"
|
|
371
|
+
|
|
372
|
+
[build]
|
|
373
|
+
top = "top"
|
|
374
|
+
sources = ["src/%s"]
|
|
375
|
+
constraints = "pins.cst"
|
|
376
|
+
build_dir = "build"
|
|
377
|
+
|
|
378
|
+
[flash]
|
|
379
|
+
board = "tangnano9k"
|
|
380
|
+
mode = "sram"
|
|
381
|
+
verify = false
|
|
382
|
+
""" % source_name
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _template_top(hdl: str = "verilog") -> str:
|
|
386
|
+
if hdl == "vhdl":
|
|
387
|
+
return """library ieee;
|
|
388
|
+
use ieee.std_logic_1164.all;
|
|
389
|
+
use ieee.numeric_std.all;
|
|
390
|
+
|
|
391
|
+
entity top is
|
|
392
|
+
port (
|
|
393
|
+
clk : in std_logic;
|
|
394
|
+
led : out std_logic
|
|
395
|
+
);
|
|
396
|
+
end entity top;
|
|
397
|
+
|
|
398
|
+
architecture rtl of top is
|
|
399
|
+
signal counter : unsigned(23 downto 0) := (others => '0');
|
|
400
|
+
begin
|
|
401
|
+
process (clk)
|
|
402
|
+
begin
|
|
403
|
+
if rising_edge(clk) then
|
|
404
|
+
counter <= counter + 1;
|
|
405
|
+
led <= counter(23);
|
|
406
|
+
end if;
|
|
407
|
+
end process;
|
|
408
|
+
end architecture rtl;
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
return """module top (
|
|
412
|
+
input wire clk,
|
|
413
|
+
output reg led
|
|
414
|
+
);
|
|
415
|
+
reg [23:0] counter = 0;
|
|
416
|
+
|
|
417
|
+
always @(posedge clk) begin
|
|
418
|
+
counter <= counter + 1;
|
|
419
|
+
led <= counter[23];
|
|
420
|
+
end
|
|
421
|
+
endmodule
|
|
422
|
+
"""
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _template_constraints() -> str:
|
|
426
|
+
return """// Update these pins for your GW1NR-LV9QN88PC6/I5 board.
|
|
427
|
+
// Example Gowin CST syntax:
|
|
428
|
+
// IO_LOC "clk" 52;
|
|
429
|
+
// IO_LOC "led" 10;
|
|
430
|
+
"""
|
devlab/toolchain.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
import stat
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
import urllib.request
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .errors import DevlabError
|
|
16
|
+
from .platforms import PlatformId, current_platform
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
OSS_CAD_SUITE_TAG = "2026-07-06"
|
|
20
|
+
OSS_CAD_SUITE_RELEASE_URL = (
|
|
21
|
+
"https://github.com/YosysHQ/oss-cad-suite-build/releases/tag/2026-07-06"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ToolchainAsset:
|
|
27
|
+
platform: str
|
|
28
|
+
name: str
|
|
29
|
+
url: str
|
|
30
|
+
sha256: str
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def suffix(self) -> str:
|
|
34
|
+
return Path(self.name).suffix
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
OSS_CAD_SUITE_ASSETS: dict[str, ToolchainAsset] = {
|
|
38
|
+
"darwin-arm64": ToolchainAsset(
|
|
39
|
+
platform="darwin-arm64",
|
|
40
|
+
name="oss-cad-suite-darwin-arm64-20260706.tgz",
|
|
41
|
+
url="https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-07-06/oss-cad-suite-darwin-arm64-20260706.tgz",
|
|
42
|
+
sha256="8e7677c6876000c19f49582ffcefbbe6ee49ddb802170a9f86eca95dcba7ed67",
|
|
43
|
+
),
|
|
44
|
+
"darwin-x64": ToolchainAsset(
|
|
45
|
+
platform="darwin-x64",
|
|
46
|
+
name="oss-cad-suite-darwin-x64-20260706.tgz",
|
|
47
|
+
url="https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-07-06/oss-cad-suite-darwin-x64-20260706.tgz",
|
|
48
|
+
sha256="5adceaa550b1ce9961ce81f0ea0ef2245fcb665863fb970e068c17aab83779ad",
|
|
49
|
+
),
|
|
50
|
+
"linux-arm64": ToolchainAsset(
|
|
51
|
+
platform="linux-arm64",
|
|
52
|
+
name="oss-cad-suite-linux-arm64-20260706.tgz",
|
|
53
|
+
url="https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-07-06/oss-cad-suite-linux-arm64-20260706.tgz",
|
|
54
|
+
sha256="0943e9eda87ee49b5c96aacc543bbe4301332467f0b2e7a374028608bcf4c961",
|
|
55
|
+
),
|
|
56
|
+
"linux-x64": ToolchainAsset(
|
|
57
|
+
platform="linux-x64",
|
|
58
|
+
name="oss-cad-suite-linux-x64-20260706.tgz",
|
|
59
|
+
url="https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-07-06/oss-cad-suite-linux-x64-20260706.tgz",
|
|
60
|
+
sha256="213a50813e809637f37dd86bb03cad7a58e726f841e7685d3a3062ef2492c315",
|
|
61
|
+
),
|
|
62
|
+
"windows-x64": ToolchainAsset(
|
|
63
|
+
platform="windows-x64",
|
|
64
|
+
name="oss-cad-suite-windows-x64-20260706.exe",
|
|
65
|
+
url="https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-07-06/oss-cad-suite-windows-x64-20260706.exe",
|
|
66
|
+
sha256="e8ab814d490d89163e418dc634842cf086ea305dde0c32f832528194a5b93ac9",
|
|
67
|
+
),
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def devlab_home() -> Path:
|
|
72
|
+
return Path(os.environ.get("DEVLAB_HOME", Path.home() / ".devlab")).expanduser()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def toolchains_dir(home: Path | None = None) -> Path:
|
|
76
|
+
return (home or devlab_home()) / "toolchains"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def cache_dir(home: Path | None = None) -> Path:
|
|
80
|
+
return (home or devlab_home()) / "cache"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def select_asset(platform_id: PlatformId | None = None) -> ToolchainAsset:
|
|
84
|
+
platform_id = platform_id or current_platform()
|
|
85
|
+
try:
|
|
86
|
+
return OSS_CAD_SUITE_ASSETS[platform_id.key]
|
|
87
|
+
except KeyError as exc:
|
|
88
|
+
supported = ", ".join(sorted(OSS_CAD_SUITE_ASSETS))
|
|
89
|
+
raise DevlabError(
|
|
90
|
+
f"Unsupported platform for OSS CAD Suite: {platform_id.key}. "
|
|
91
|
+
f"Supported: {supported}"
|
|
92
|
+
) from exc
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def install_path(asset: ToolchainAsset | None = None, home: Path | None = None) -> Path:
|
|
96
|
+
asset = asset or select_asset()
|
|
97
|
+
return toolchains_dir(home) / f"oss-cad-suite-{OSS_CAD_SUITE_TAG}-{asset.platform}"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def archive_path(asset: ToolchainAsset | None = None, home: Path | None = None) -> Path:
|
|
101
|
+
asset = asset or select_asset()
|
|
102
|
+
return cache_dir(home) / asset.name
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def bin_dir(path: Path | None = None) -> Path:
|
|
106
|
+
path = path or install_path()
|
|
107
|
+
return path / "bin"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def env_with_toolchain(path: Path | None = None) -> dict[str, str]:
|
|
111
|
+
env = os.environ.copy()
|
|
112
|
+
binary_dir = str(bin_dir(path))
|
|
113
|
+
env["PATH"] = binary_dir + os.pathsep + env.get("PATH", "")
|
|
114
|
+
return env
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def find_executable(name: str, path: Path | None = None) -> str | None:
|
|
118
|
+
suffixes = [""]
|
|
119
|
+
if sys.platform.startswith("win"):
|
|
120
|
+
suffixes = [".exe", ".bat", ".cmd", ""]
|
|
121
|
+
|
|
122
|
+
candidates: list[str] = []
|
|
123
|
+
binary_dir = bin_dir(path)
|
|
124
|
+
for suffix in suffixes:
|
|
125
|
+
candidates.append(str(binary_dir / f"{name}{suffix}"))
|
|
126
|
+
|
|
127
|
+
for candidate in candidates:
|
|
128
|
+
if Path(candidate).exists():
|
|
129
|
+
return candidate
|
|
130
|
+
return shutil.which(name, path=env_with_toolchain(path).get("PATH"))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def download_asset(asset: ToolchainAsset, destination: Path, force: bool = False) -> Path:
|
|
134
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
if destination.exists() and not force:
|
|
136
|
+
verify_sha256(destination, asset.sha256)
|
|
137
|
+
return destination
|
|
138
|
+
|
|
139
|
+
tmp_destination = destination.with_suffix(destination.suffix + ".part")
|
|
140
|
+
if tmp_destination.exists():
|
|
141
|
+
tmp_destination.unlink()
|
|
142
|
+
|
|
143
|
+
def report(blocks: int, block_size: int, total_size: int) -> None:
|
|
144
|
+
if total_size <= 0:
|
|
145
|
+
return
|
|
146
|
+
downloaded = min(blocks * block_size, total_size)
|
|
147
|
+
percent = downloaded * 100 / total_size
|
|
148
|
+
print(f"\rDownloading {asset.name}: {percent:5.1f}%", end="", flush=True)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
urllib.request.urlretrieve(asset.url, tmp_destination, reporthook=report)
|
|
152
|
+
print()
|
|
153
|
+
except OSError as exc:
|
|
154
|
+
raise DevlabError(f"Could not download {asset.url}: {exc}") from exc
|
|
155
|
+
|
|
156
|
+
verify_sha256(tmp_destination, asset.sha256)
|
|
157
|
+
tmp_destination.replace(destination)
|
|
158
|
+
return destination
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def verify_sha256(path: Path, expected: str) -> None:
|
|
162
|
+
digest = hashlib.sha256()
|
|
163
|
+
with path.open("rb") as handle:
|
|
164
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
165
|
+
digest.update(chunk)
|
|
166
|
+
actual = digest.hexdigest()
|
|
167
|
+
if actual != expected:
|
|
168
|
+
raise DevlabError(
|
|
169
|
+
f"SHA-256 mismatch for {path.name}: expected {expected}, got {actual}"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def install_oss_cad_suite(
|
|
174
|
+
home: Path | None = None,
|
|
175
|
+
force: bool = False,
|
|
176
|
+
run_windows_installer: bool = False,
|
|
177
|
+
) -> Path:
|
|
178
|
+
home = home or devlab_home()
|
|
179
|
+
asset = select_asset()
|
|
180
|
+
destination = install_path(asset, home)
|
|
181
|
+
|
|
182
|
+
if destination.exists() and not force:
|
|
183
|
+
return destination
|
|
184
|
+
|
|
185
|
+
archive = download_asset(asset, archive_path(asset, home), force=force)
|
|
186
|
+
|
|
187
|
+
if asset.name.endswith(".exe"):
|
|
188
|
+
if not run_windows_installer:
|
|
189
|
+
raise DevlabError(
|
|
190
|
+
"Windows uses the OSS CAD Suite .exe installer. "
|
|
191
|
+
f"The file was downloaded to {archive}. Re-run with "
|
|
192
|
+
"--run-installer to execute it."
|
|
193
|
+
)
|
|
194
|
+
subprocess.check_call([str(archive)])
|
|
195
|
+
return destination
|
|
196
|
+
|
|
197
|
+
_extract_tarball(archive, destination, force=force)
|
|
198
|
+
_mark_executables(bin_dir(destination))
|
|
199
|
+
return destination
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _extract_tarball(archive: Path, destination: Path, force: bool) -> None:
|
|
203
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
if destination.exists():
|
|
205
|
+
if not force:
|
|
206
|
+
raise DevlabError(f"Install destination already exists: {destination}")
|
|
207
|
+
shutil.rmtree(destination)
|
|
208
|
+
|
|
209
|
+
with tempfile.TemporaryDirectory(prefix="devlab-oss-cad-suite-") as tmp:
|
|
210
|
+
tmp_path = Path(tmp)
|
|
211
|
+
with tarfile.open(archive, "r:gz") as tar:
|
|
212
|
+
_safe_extract(tar, tmp_path)
|
|
213
|
+
|
|
214
|
+
children = [child for child in tmp_path.iterdir()]
|
|
215
|
+
if len(children) == 1 and children[0].is_dir():
|
|
216
|
+
shutil.move(str(children[0]), destination)
|
|
217
|
+
else:
|
|
218
|
+
destination.mkdir(parents=True)
|
|
219
|
+
for child in children:
|
|
220
|
+
shutil.move(str(child), destination / child.name)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _safe_extract(tar: tarfile.TarFile, destination: Path) -> None:
|
|
224
|
+
destination = destination.resolve()
|
|
225
|
+
for member in tar.getmembers():
|
|
226
|
+
target = (destination / member.name).resolve()
|
|
227
|
+
if destination != target and destination not in target.parents:
|
|
228
|
+
raise DevlabError(f"Unsafe path in archive: {member.name}")
|
|
229
|
+
try:
|
|
230
|
+
tar.extractall(destination, filter="data")
|
|
231
|
+
except TypeError: # pragma: no cover - Python < 3.12
|
|
232
|
+
tar.extractall(destination)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _mark_executables(path: Path) -> None:
|
|
236
|
+
if not path.exists():
|
|
237
|
+
return
|
|
238
|
+
for file_path in path.iterdir():
|
|
239
|
+
if not file_path.is_file():
|
|
240
|
+
continue
|
|
241
|
+
mode = file_path.stat().st_mode
|
|
242
|
+
file_path.chmod(mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devlab-fpga
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FPGA development helper for installing OSS CAD Suite and running build/flash flows.
|
|
5
|
+
Author-email: Mrju10 <name@email.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/unit-electronics/unit_devlab_lib
|
|
8
|
+
Project-URL: Source, https://github.com/unit-electronics/unit_devlab_lib
|
|
9
|
+
Keywords: fpga,yosys,oss-cad-suite,openfpgaloader,devlab,devlab-fpga
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: tomli>=2.0.1; python_version < "3.11"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# devlab
|
|
26
|
+
|
|
27
|
+
`devlab` is a Python CLI package for FPGA development. It installs the
|
|
28
|
+
matching OSS CAD Suite build for the current operating system, creates a small
|
|
29
|
+
FPGA project, and runs build/flash commands from `devlab.toml`.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install devlab-fpga
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For local development from this repository:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install -e .
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
devlab doctor
|
|
47
|
+
devlab install
|
|
48
|
+
devlab new blink
|
|
49
|
+
devlab new blink-vhdl --hdl vhdl
|
|
50
|
+
cd blink
|
|
51
|
+
devlab build
|
|
52
|
+
devlab flash
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`devlab install` downloads OSS CAD Suite release `2026-07-06` from
|
|
56
|
+
YosysHQ. The installer selects the correct asset for:
|
|
57
|
+
|
|
58
|
+
- Linux x64
|
|
59
|
+
- Linux arm64
|
|
60
|
+
- macOS x64
|
|
61
|
+
- macOS arm64
|
|
62
|
+
- Windows x64
|
|
63
|
+
|
|
64
|
+
The default install location is `~/.devlab`. Set `DEVLAB_HOME` to use a
|
|
65
|
+
different directory.
|
|
66
|
+
|
|
67
|
+
## OSS CAD Suite Source
|
|
68
|
+
|
|
69
|
+
Default release:
|
|
70
|
+
|
|
71
|
+
https://github.com/YosysHQ/oss-cad-suite-build/releases/tag/2026-07-06
|
|
72
|
+
|
|
73
|
+
Example assets:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-07-06/oss-cad-suite-linux-arm64-20260706.tgz
|
|
77
|
+
https://github.com/YosysHQ/oss-cad-suite-build/releases/download/2026-07-06/oss-cad-suite-windows-x64-20260706.exe
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Downloaded archives are verified with the SHA-256 digest published by the
|
|
81
|
+
GitHub release API.
|
|
82
|
+
|
|
83
|
+
## Project Format
|
|
84
|
+
|
|
85
|
+
`devlab new blink` creates:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
blink/
|
|
89
|
+
devlab.toml
|
|
90
|
+
pins.cst
|
|
91
|
+
src/top.v
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use `--hdl vhdl` to create `src/top.vhd` instead:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
devlab new blink-vhdl --hdl vhdl
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Default `devlab.toml`:
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
[fpga]
|
|
104
|
+
family = "GW1N-9C"
|
|
105
|
+
device = "GW1NR-LV9QN88PC6/I5"
|
|
106
|
+
cst = "pins.cst"
|
|
107
|
+
|
|
108
|
+
[build]
|
|
109
|
+
top = "top"
|
|
110
|
+
sources = ["src/top.v"]
|
|
111
|
+
constraints = "pins.cst"
|
|
112
|
+
build_dir = "build"
|
|
113
|
+
|
|
114
|
+
[flash]
|
|
115
|
+
board = "tangnano9k"
|
|
116
|
+
mode = "sram"
|
|
117
|
+
verify = false
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
For Gowin, `family` is the FPGA series used by the packer and place-and-route
|
|
121
|
+
flow, for example `GW1N-9C`. `device` is the complete part number, for example
|
|
122
|
+
`GW1NR-LV9QN88PC6/I5`. `pins.cst` is the Gowin constraints file.
|
|
123
|
+
|
|
124
|
+
Update `pins.cst` and `[flash].board` for the real FPGA board before building
|
|
125
|
+
and flashing hardware.
|
|
126
|
+
|
|
127
|
+
## Build Flows
|
|
128
|
+
|
|
129
|
+
For Gowin `GW1N-9C` / `GW1NR-LV9QN88PC6/I5`, `devlab build` runs:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
yosys
|
|
133
|
+
nextpnr-himbaechel
|
|
134
|
+
gowin_pack
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The iCE40 and ECP5 flows are still available by setting `family = "ice40"` or
|
|
138
|
+
`family = "ecp5"` in `devlab.toml`.
|
|
139
|
+
|
|
140
|
+
For iCE40, it runs:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
yosys
|
|
144
|
+
nextpnr-ice40
|
|
145
|
+
icepack
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`devlab flash` uses `openFPGALoader`. By default it writes SRAM, so the FPGA
|
|
149
|
+
loses the design after power cycling. Use flash mode to write the bitstream to
|
|
150
|
+
non-volatile memory:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
devlab flash --mode flash
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Before writing flash on board variants where flash may or may not be populated,
|
|
157
|
+
run detection:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
devlab flash --detect
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Persistent flash can also be configured in `devlab.toml`:
|
|
164
|
+
|
|
165
|
+
```toml
|
|
166
|
+
[flash]
|
|
167
|
+
board = "tangnano9k"
|
|
168
|
+
mode = "flash"
|
|
169
|
+
verify = false
|
|
170
|
+
# external_flash = true
|
|
171
|
+
# offset = "0"
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
For Tang Nano 9K/Gowin, flash write verification may print
|
|
175
|
+
`writing verification not supported` and fail the CRC check even when the flash
|
|
176
|
+
write completed. Keep `verify = false` unless the selected board/programming
|
|
177
|
+
path explicitly supports flash verification.
|
|
178
|
+
|
|
179
|
+
When sources end in `.vhd` or `.vhdl`, `devlab build` runs Yosys with the
|
|
180
|
+
GHDL plugin before synthesis. OSS CAD Suite is expected to provide that plugin.
|
|
181
|
+
|
|
182
|
+
Use `--dry-run` to print the commands without executing them:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
devlab build --dry-run
|
|
186
|
+
devlab flash --dry-run --board tangnano9k
|
|
187
|
+
devlab flash --dry-run --mode flash
|
|
188
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
devlab/__init__.py,sha256=8YbSz2Aj8SQbG_L7_RgQ-ygB4R-qq6II-Tvv4fiAcD4,112
|
|
2
|
+
devlab/__main__.py,sha256=PSQ4rpL0dG6f-qH4N7H-gD9igQkdHzH4yVZDcW8lfZo,80
|
|
3
|
+
devlab/cli.py,sha256=X5TyrC72x4XefcI3QgJnbBcCcrutdRKGd0z5kQwPwGM,5726
|
|
4
|
+
devlab/errors.py,sha256=RFT2YqJthU2N7wdYZZ5YJXkmrFr34kYyyT2JJL1MXXo,89
|
|
5
|
+
devlab/platforms.py,sha256=xOHG2f1u7G3dB4tXM-PlJgMQ_qrkjTIlPgBnhxLaXNI,946
|
|
6
|
+
devlab/project.py,sha256=dZ2YuGOMV3cqMhgXin-Ny4wa7LB8IpbYGFeJHK1YiBI,13050
|
|
7
|
+
devlab/toolchain.py,sha256=b-GEDbdVcNtGPNHk4zf3ZY279n5KOLGoslEQWMTkLWw,8336
|
|
8
|
+
devlab_fpga-0.1.0.dist-info/licenses/LICENSE,sha256=nrbPcnyVbAcWTBTG_nJt3M2yHrL-zucDeHC02dJ09Fo,1063
|
|
9
|
+
devlab_fpga-0.1.0.dist-info/METADATA,sha256=Mwqi0BP_I673k4lXJ1m9mwNUC13zqNhQp4fA_O3xZKc,4448
|
|
10
|
+
devlab_fpga-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
11
|
+
devlab_fpga-0.1.0.dist-info/entry_points.txt,sha256=5kjH9FbXX8jasuwPZH1hTpVrGc4-jbOje92tLitLYvg,43
|
|
12
|
+
devlab_fpga-0.1.0.dist-info/top_level.txt,sha256=dVqsvPXb6Uy0rMo_xDjEyvHLc-v_B28HCh0bKP3-saA,7
|
|
13
|
+
devlab_fpga-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mrju10
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
devlab
|