mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-02 20:26:10 +01:00
feat(fpga): add support for the new FabricFox devkit
* TT FPGA tool, prelim import * config.ini manager, prelim * bitstream uploads and FPGA config.ini management Contributed by @psychogenic, with minor edits by @htfab
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
# FabricFox PCF
|
||||
set_io -nowarn clk 20
|
||||
set_io -nowarn intclk 35
|
||||
set_io -nowarn rst_n 37
|
||||
|
||||
set_io -nowarn rgb[0] 39
|
||||
set_io -nowarn rgb[1] 40
|
||||
set_io -nowarn rgb[2] 41
|
||||
|
||||
set_io -nowarn ui_in[0] 13
|
||||
set_io -nowarn ui_in[1] 19
|
||||
set_io -nowarn ui_in[2] 18
|
||||
set_io -nowarn ui_in[3] 21
|
||||
set_io -nowarn ui_in[4] 23
|
||||
set_io -nowarn ui_in[5] 25
|
||||
set_io -nowarn ui_in[6] 26
|
||||
set_io -nowarn ui_in[7] 27
|
||||
|
||||
set_io -nowarn uio[0] 2
|
||||
set_io -nowarn uio[1] 4
|
||||
set_io -nowarn uio[2] 3
|
||||
set_io -nowarn uio[3] 6
|
||||
set_io -nowarn uio[4] 9
|
||||
set_io -nowarn uio[5] 10
|
||||
set_io -nowarn uio[6] 11
|
||||
set_io -nowarn uio[7] 12
|
||||
|
||||
set_io -nowarn uo_out[0] 38
|
||||
set_io -nowarn uo_out[1] 42
|
||||
set_io -nowarn uo_out[2] 43
|
||||
set_io -nowarn uo_out[3] 44
|
||||
set_io -nowarn uo_out[4] 45
|
||||
set_io -nowarn uo_out[5] 46
|
||||
set_io -nowarn uo_out[6] 47
|
||||
set_io -nowarn uo_out[7] 48
|
||||
|
||||
set_frequency clk 24.0
|
||||
+2
-56
@@ -353,6 +353,8 @@ class Project:
|
||||
return os.path.join(self.local_dir, f"{self.info.top_module}.v")
|
||||
|
||||
def get_tile_sizes(self):
|
||||
if self.tech.is_fpga:
|
||||
return {}
|
||||
tile_sizes_yaml = f"tech/{self.pdk}/tile_sizes.yaml"
|
||||
with open(
|
||||
os.path.join(os.path.dirname(__file__), tile_sizes_yaml), "r"
|
||||
@@ -629,62 +631,6 @@ class Project:
|
||||
logging.error("librelane failed")
|
||||
exit(1)
|
||||
|
||||
def create_fpga_bitstream(self, args):
|
||||
logging.info(f"Creating FPGA bitstream for {self}")
|
||||
|
||||
target_map = {
|
||||
"classic": {
|
||||
"pcf": "tt_fpga_top.pcf",
|
||||
"def_name": "tt_fpga",
|
||||
},
|
||||
"fabricfox": {"pcf": "tt_fpga_fabricfox.pcf", "def_name": "tt_ff_fpga"},
|
||||
}
|
||||
pcf_file = target_map[args.fpga_breakout_target]["pcf"]
|
||||
|
||||
base_name = args.fpga_bitstream_name
|
||||
if not len(base_name):
|
||||
base_name = target_map[args.fpga_breakout_target]["def_name"]
|
||||
|
||||
with open(os.path.join(SCRIPT_DIR, "fpga/tt_fpga_top.v"), "r") as f:
|
||||
top_module_template = f.read()
|
||||
|
||||
with open(os.path.join(self.src_dir, "_tt_fpga_top.v"), "w") as f:
|
||||
f.write(
|
||||
top_module_template.replace("__tt_um_placeholder", self.info.top_module)
|
||||
)
|
||||
|
||||
build_dir = os.path.join(self.local_dir, "build")
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
|
||||
sources = [os.path.join(self.src_dir, src) for src in self.sources]
|
||||
source_list = " ".join(sources)
|
||||
|
||||
yosys_cmd = f"yosys -l {build_dir}/01-synth.log -DSYNTH -p 'synth_ice40 -top tt_fpga_top -json {build_dir}/{base_name}.json' src/_tt_fpga_top.v {source_list}"
|
||||
logging.debug(yosys_cmd)
|
||||
p = subprocess.run(yosys_cmd, shell=True)
|
||||
if p.returncode != 0:
|
||||
logging.error("synthesis failed")
|
||||
exit(1)
|
||||
|
||||
seed = os.getenv("TT_FPGA_SEED", "10")
|
||||
freq = os.getenv("TT_FPGA_FREQ", "12")
|
||||
|
||||
nextpnr_cmd = f"nextpnr-ice40 -l {build_dir}/02-nextpnr.log --pcf-allow-unconstrained --seed {seed} --freq {freq} --package sg48 --up5k --asc {build_dir}/{base_name}.asc --pcf {SCRIPT_DIR}/fpga/{pcf_file} --json {build_dir}/{base_name}.json"
|
||||
logging.debug(nextpnr_cmd)
|
||||
p = subprocess.run(nextpnr_cmd, shell=True)
|
||||
if p.returncode != 0:
|
||||
logging.error("placement failed")
|
||||
exit(1)
|
||||
|
||||
icepack_cmd = f"icepack {build_dir}/{base_name}.asc {build_dir}/{base_name}.bin"
|
||||
logging.debug(icepack_cmd)
|
||||
p = subprocess.run(icepack_cmd, shell=True)
|
||||
if p.returncode != 0:
|
||||
logging.error("bitstream creation failed failed")
|
||||
exit(1)
|
||||
|
||||
logging.info(f"Bitstream created successfully: {build_dir}/{base_name}.bin")
|
||||
|
||||
def create_project_datasheet(self, template_version: str):
|
||||
template_args = copy.deepcopy(self.info.__dict__)
|
||||
template_args.update(
|
||||
|
||||
+10
-7
@@ -106,13 +106,16 @@ class ProjectInfo:
|
||||
else:
|
||||
self.description: str = description
|
||||
|
||||
tiles = project_section.get("tiles")
|
||||
if tiles is None:
|
||||
errors.append("Missing key 'tiles' in 'project' section")
|
||||
elif tiles not in tile_sizes.keys():
|
||||
errors.append(f"Invalid value for 'tiles' in 'project' section: {tiles}")
|
||||
else:
|
||||
self.tiles: str = tiles
|
||||
if tile_sizes:
|
||||
tiles = project_section.get("tiles")
|
||||
if tiles is None:
|
||||
errors.append("Missing key 'tiles' in 'project' section")
|
||||
elif tiles not in tile_sizes.keys():
|
||||
errors.append(
|
||||
f"Invalid value for 'tiles' in 'project' section: {tiles}"
|
||||
)
|
||||
else:
|
||||
self.tiles: str = tiles
|
||||
|
||||
analog_pins = project_section.get("analog_pins", 0)
|
||||
if not isinstance(analog_pins, int):
|
||||
|
||||
+3
-1
@@ -16,7 +16,9 @@ dependencies = [
|
||||
"PyYAML",
|
||||
"requests",
|
||||
"yowasp-yosys",
|
||||
"matplotlib"
|
||||
"matplotlib",
|
||||
"configupdater",
|
||||
"mpremote"
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
+8
-1
@@ -1,5 +1,5 @@
|
||||
#
|
||||
# This file is autogenerated by pip-compile with Python 3.12
|
||||
# This file is autogenerated by pip-compile with Python 3.13
|
||||
# by the following command:
|
||||
#
|
||||
# pip-compile
|
||||
@@ -20,6 +20,8 @@ chevron==0.14.0
|
||||
# via tt-support-tools (pyproject.toml)
|
||||
click==8.2.1
|
||||
# via yowasp-yosys
|
||||
configupdater==3.2
|
||||
# via tt-support-tools (pyproject.toml)
|
||||
contourpy==1.3.3
|
||||
# via matplotlib
|
||||
cssselect2==0.8.0
|
||||
@@ -56,6 +58,8 @@ matplotlib==3.10.6
|
||||
# via tt-support-tools (pyproject.toml)
|
||||
mistune==3.1.3
|
||||
# via tt-support-tools (pyproject.toml)
|
||||
mpremote==1.27.0
|
||||
# via tt-support-tools (pyproject.toml)
|
||||
nodeenv==1.9.1
|
||||
# via pre-commit
|
||||
numpy==1.26.4
|
||||
@@ -74,6 +78,7 @@ pillow==11.3.0
|
||||
# matplotlib
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# mpremote
|
||||
# virtualenv
|
||||
# yowasp-runtime
|
||||
pluggy==1.6.0
|
||||
@@ -86,6 +91,8 @@ pygments==2.19.2
|
||||
# via pytest
|
||||
pyparsing==3.2.5
|
||||
# via matplotlib
|
||||
pyserial==3.5
|
||||
# via mpremote
|
||||
pytest==8.4.2
|
||||
# via tt-support-tools (pyproject.toml)
|
||||
python-dateutil==2.9.0.post0
|
||||
|
||||
@@ -2,7 +2,7 @@ import json
|
||||
import os
|
||||
from typing import Dict, List, Literal, Pattern, Protocol, Tuple, TypedDict, Union
|
||||
|
||||
TechName = Literal["sky130A", "ihp-sg13g2", "gf180mcuD"]
|
||||
TechName = Literal["sky130A", "ihp-sg13g2", "gf180mcuD", "fpgaUp5k"]
|
||||
|
||||
|
||||
class CellDefinition(TypedDict):
|
||||
@@ -16,6 +16,7 @@ class PDKVersionInfo(TypedDict):
|
||||
|
||||
|
||||
class Tech(Protocol):
|
||||
is_fpga: bool
|
||||
def_suffix: str
|
||||
librelane_pdk_args: str
|
||||
tt_corner: str
|
||||
@@ -65,6 +66,7 @@ def parse_openpdks_pdk_version(
|
||||
|
||||
|
||||
class Sky130Tech(Tech):
|
||||
is_fpga = False
|
||||
def_suffix = "pg"
|
||||
librelane_pdk_args = ""
|
||||
tt_corner = "nom_tt_025C_1v80"
|
||||
@@ -124,6 +126,7 @@ class Sky130Tech(Tech):
|
||||
|
||||
|
||||
class IHPTech(Tech):
|
||||
is_fpga = False
|
||||
def_suffix = "pgvdd"
|
||||
librelane_pdk_args = "--pdk ihp-sg13g2"
|
||||
tt_corner = "nom_typ_1p20V_25C"
|
||||
@@ -177,6 +180,7 @@ class IHPTech(Tech):
|
||||
|
||||
|
||||
class GF180MCUDTech(Tech):
|
||||
is_fpga = False
|
||||
def_suffix = "pgvdd"
|
||||
librelane_pdk_args = "--pdk gf180mcuD"
|
||||
tt_corner = "nom_tt_025C_3v30"
|
||||
@@ -248,8 +252,33 @@ class GF180MCUDTech(Tech):
|
||||
return cells
|
||||
|
||||
|
||||
class FpgaTech(Tech):
|
||||
is_fpga = True
|
||||
# dummy values to keep the type checker happy
|
||||
def_suffix = ""
|
||||
librelane_pdk_args = ""
|
||||
tt_corner = ""
|
||||
cell_regexp = r""
|
||||
netlist_type = "nl"
|
||||
project_top_metal_layer = ""
|
||||
librelane_config = {}
|
||||
label_layers = []
|
||||
buried_layers = []
|
||||
scramble_cells = None
|
||||
mux_config_yaml_name = ""
|
||||
mux_macros = []
|
||||
extra_logo_macros = []
|
||||
prboundary_layer = (0, 0)
|
||||
logo_layer = (0, 0)
|
||||
logo_layer_name = ""
|
||||
logo_pixel_size = 0
|
||||
read_pdk_version = Tech.read_pdk_version
|
||||
load_cell_definitions = Tech.load_cell_definitions
|
||||
|
||||
|
||||
tech_map: dict[TechName, Tech] = {
|
||||
"ihp-sg13g2": IHPTech(),
|
||||
"gf180mcuD": GF180MCUDTech(),
|
||||
"sky130A": Sky130Tech(),
|
||||
"fpgaUp5k": FpgaTech(),
|
||||
}
|
||||
|
||||
Executable
+239
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
|
||||
TT config.ini manager
|
||||
|
||||
# global defaults
|
||||
python tt_db_config.py --project tt_um_factory_test --mode ASIC_MANUAL_INPUTS
|
||||
|
||||
|
||||
# project specific
|
||||
python tt_db_config.py --name tt_um_factory_test project --clockrate 100
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from configupdater import ConfigUpdater
|
||||
|
||||
DefaultPort = "/dev/ttyACM0"
|
||||
CommandProject = "project"
|
||||
CommandDefault = "default"
|
||||
|
||||
|
||||
def getParser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="TT Demoboard Configuration Tool",
|
||||
# formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
# Global options (available to all subcommands)
|
||||
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
help=f"port for connection to DB [{DefaultPort}]",
|
||||
required=False,
|
||||
default=DefaultPort,
|
||||
)
|
||||
|
||||
parser.add_argument("--name", help="project/section name", required=False)
|
||||
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
metavar="LEVEL",
|
||||
help="debug logging",
|
||||
action="store_const",
|
||||
dest="loglevel",
|
||||
const=logging.DEBUG,
|
||||
default=logging.INFO,
|
||||
)
|
||||
|
||||
# Create subparsers
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="command",
|
||||
required=True, # make subcommand mandatory
|
||||
help="Available commands",
|
||||
)
|
||||
|
||||
defaults = subparsers.add_parser(CommandDefault, help="Default settings")
|
||||
|
||||
defaults.add_argument(
|
||||
"--project",
|
||||
metavar="PROJNAME",
|
||||
type=str,
|
||||
help="Set the default project on boot",
|
||||
)
|
||||
|
||||
defaults.add_argument(
|
||||
"--mode",
|
||||
metavar="I/O MODE",
|
||||
type=str,
|
||||
choices=["SAFE", "ASIC_RP_CONTROL", "ASIC_MANUAL_INPUTS"],
|
||||
)
|
||||
|
||||
project = subparsers.add_parser(CommandProject, help="Project overrides")
|
||||
|
||||
project.add_argument(
|
||||
"--clockrate",
|
||||
metavar="RATE",
|
||||
type=int,
|
||||
help="Clock rate in Hz",
|
||||
)
|
||||
project.add_argument(
|
||||
"--set-default",
|
||||
action="store_true",
|
||||
help="set this project as the default on startup",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
class ConfigFile:
|
||||
|
||||
def __init__(self, filepath: str):
|
||||
self.filepath = filepath
|
||||
|
||||
self.updater = ConfigUpdater()
|
||||
self.updater.read(filepath)
|
||||
return
|
||||
|
||||
def save(self):
|
||||
with open(self.filepath, "w") as f:
|
||||
self.updater.write(f)
|
||||
|
||||
def set_default_project(self, name: str):
|
||||
self.updater["DEFAULT"]["project"] = name
|
||||
|
||||
def set_default_mode(self, mode: str):
|
||||
self.updater["DEFAULT"]["mode"] = mode
|
||||
|
||||
def set_project_clockrate(self, name: str, rate: int):
|
||||
if name not in self.updater:
|
||||
self.updater.add_section(name)
|
||||
|
||||
self.updater[name]["clock_frequency"] = str(rate)
|
||||
|
||||
|
||||
class TTDBConfig:
|
||||
|
||||
def __init__(self, port: str = DefaultPort):
|
||||
self.port = port
|
||||
|
||||
@classmethod
|
||||
def get_temp_file(cls, prefix="ttconf-", suffix=".tmp", dir=None, mode="wb"):
|
||||
# path is tmp.name
|
||||
tmp = tempfile.NamedTemporaryFile(
|
||||
mode=mode, # text mode + read/write
|
||||
delete=False, # ← keep file after closing
|
||||
suffix=suffix, # optional but recommended
|
||||
prefix=prefix,
|
||||
dir=dir, # None = system temp dir
|
||||
)
|
||||
return tmp
|
||||
|
||||
@classmethod
|
||||
def get_temp_filename(cls, prefix="ttconf-", suffix=".tmp", dir=None):
|
||||
tmp = cls.get_temp_file(prefix, suffix, dir)
|
||||
tmp.close()
|
||||
return tmp.name
|
||||
|
||||
def reset(self):
|
||||
commands = ["connect", self.port, "+", "reset"]
|
||||
subprocess.run(["mpremote", *commands], check=False)
|
||||
|
||||
def deploy(self, files: list, destination_dir: str, do_reset: bool = True):
|
||||
commands = ["connect", self.port]
|
||||
|
||||
if isinstance(files, str):
|
||||
files = [files]
|
||||
|
||||
for f in files:
|
||||
bname = os.path.basename(f)
|
||||
commands += ["+", "cp", f, f":{destination_dir}/{bname}"]
|
||||
|
||||
if do_reset:
|
||||
commands += ["+", "reset"]
|
||||
|
||||
subprocess.run(["mpremote", *commands], check=True)
|
||||
|
||||
def write_file(
|
||||
self, local_filepath: str, remote_filepath: str, do_reset: bool = False
|
||||
):
|
||||
commands = [
|
||||
"connect",
|
||||
self.port,
|
||||
"+",
|
||||
"cp",
|
||||
local_filepath,
|
||||
f":{remote_filepath}",
|
||||
]
|
||||
|
||||
if do_reset:
|
||||
commands += ["+", "reset"]
|
||||
|
||||
subprocess.run(["mpremote", *commands], check=False)
|
||||
|
||||
def fetch_safe(self, remote_filepath: str):
|
||||
tmp_filepath = self.get_temp_filename()
|
||||
|
||||
commands = ["connect", self.port]
|
||||
commands += ["+", "cp", f":{remote_filepath}", tmp_filepath]
|
||||
|
||||
subprocess.run(["mpremote", *commands], check=True)
|
||||
return tmp_filepath
|
||||
|
||||
def fetch(self, files: list, destination_dir: str = "."):
|
||||
commands = ["connect", self.port]
|
||||
|
||||
if isinstance(files, str):
|
||||
files = [files]
|
||||
|
||||
for f in files:
|
||||
bname = os.path.basename(f)
|
||||
commands += ["+", "cp", f":{f}", f"{destination_dir}/{bname}"]
|
||||
|
||||
subprocess.run(["mpremote", *commands], check=True)
|
||||
|
||||
def get_config_ini(self) -> ConfigFile:
|
||||
tmpfile = self.fetch_safe("/config.ini")
|
||||
return ConfigFile(tmpfile)
|
||||
|
||||
def upload_config_ini(self, local_filepath: str, do_reset: bool = True):
|
||||
self.write_file(local_filepath, "/config.ini", do_reset)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = getParser()
|
||||
args = parser.parse_args()
|
||||
ttconf = TTDBConfig()
|
||||
|
||||
cnf = None
|
||||
if args.command == CommandDefault:
|
||||
cnf = ttconf.get_config_ini()
|
||||
if args.project:
|
||||
cnf.set_default_project(args.project)
|
||||
if args.mode:
|
||||
cnf.set_default_mode(args.mode)
|
||||
|
||||
elif args.command == CommandProject:
|
||||
if not args.name:
|
||||
parser.print_help()
|
||||
print("MUST specify NAME of project this applies to using --name")
|
||||
sys.exit(1)
|
||||
|
||||
cnf = ttconf.get_config_ini()
|
||||
if args.clockrate:
|
||||
cnf.set_project_clockrate(args.name, args.clockrate)
|
||||
|
||||
if args.set_default:
|
||||
cnf.set_default_project(args.name)
|
||||
|
||||
if cnf is not None:
|
||||
cnf.save()
|
||||
ttconf.upload_config_ini(cnf.filepath, do_reset=False)
|
||||
ttconf.reset()
|
||||
Executable
+354
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
tt_fpga -- FPGA breakout management tool
|
||||
@author: Pat Deegan
|
||||
|
||||
In a project based on a TT template, with a configured info.yaml, all you need
|
||||
is to run
|
||||
|
||||
python tt/tt_fpga.py harden
|
||||
|
||||
to generate a bitstream in build/. Then
|
||||
|
||||
python tt/tt_fpga.py configure --upload
|
||||
|
||||
will make the bitstream available through the tt.shuttle object in the DB REPL.
|
||||
|
||||
You may also set this as the default project to load on startup and/or set the
|
||||
auto-clocking rate through the configure command, e.g.
|
||||
|
||||
python tt/tt_fpga.py configure --upload --set-default --clockrate 1000
|
||||
|
||||
to do all three at once.
|
||||
|
||||
|
||||
If you have no info.yaml template, the script will work anyway. But you'll need
|
||||
to specify more command line arguments.
|
||||
|
||||
See
|
||||
python tt/tt_fpga.py harden --help
|
||||
and
|
||||
python tt/tt_fpga.py configure --help
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from project import SCRIPT_DIR, Project
|
||||
from tt_db_config import DefaultPort, TTDBConfig
|
||||
from tt_logging import setup_logging
|
||||
|
||||
CommandHarden = "harden"
|
||||
CommandConfig = "configure"
|
||||
|
||||
|
||||
def getParser():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="TT FPGA tool",
|
||||
# formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
|
||||
parser.add_argument("--project-dir", help="location of the project", default=".")
|
||||
# parser.add_argument("--yaml", help="the project's yaml configuration file", default="info.yaml")
|
||||
|
||||
# Global options (available to all subcommands)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
metavar="LEVEL",
|
||||
help="debug logging",
|
||||
action="store_const",
|
||||
dest="loglevel",
|
||||
const=logging.DEBUG,
|
||||
default=logging.INFO,
|
||||
)
|
||||
|
||||
# Create subparsers
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="command",
|
||||
required=True, # make subcommand mandatory
|
||||
help="Available commands",
|
||||
)
|
||||
|
||||
# ----------------------
|
||||
# Subcommand: hardening
|
||||
# ----------------------
|
||||
harden = subparsers.add_parser(CommandHarden, help="Run hardening process")
|
||||
|
||||
harden.add_argument(
|
||||
"--name",
|
||||
metavar="FILE",
|
||||
help="Output file and project name (default: top_module name)",
|
||||
default="",
|
||||
)
|
||||
|
||||
harden.add_argument(
|
||||
"--breakout-target",
|
||||
help="Select target breakout: classic (TT04), fabricfox (TT ETR db) default: fabricfox",
|
||||
choices=["classic", "fabricfox"],
|
||||
default="fabricfox",
|
||||
)
|
||||
|
||||
harden.add_argument(
|
||||
"--source_dir",
|
||||
metavar="DIR",
|
||||
help="Directory containing source verilog files (default from info.yaml)",
|
||||
)
|
||||
|
||||
harden.add_argument(
|
||||
"--source",
|
||||
action="append",
|
||||
metavar="FILE",
|
||||
help="Source file(s) to harden (specify multiple times for multiple source files, default from info.yaml)",
|
||||
)
|
||||
|
||||
harden.add_argument(
|
||||
"--top_module",
|
||||
metavar="TOP",
|
||||
help="Name of the top module (default from info.yaml)",
|
||||
)
|
||||
|
||||
# ----------------------
|
||||
# Subcommand: configuration
|
||||
# ----------------------
|
||||
config = subparsers.add_parser(
|
||||
CommandConfig, help="Upload bitstream and manage configuration"
|
||||
)
|
||||
config.add_argument(
|
||||
"--port",
|
||||
metavar="COMPORT",
|
||||
type=str,
|
||||
default=DefaultPort,
|
||||
help=f"Port for uploads (default: {DefaultPort})",
|
||||
)
|
||||
|
||||
config.add_argument(
|
||||
"--set-default",
|
||||
action="store_true",
|
||||
help="set this project as the default on startup",
|
||||
)
|
||||
config.add_argument(
|
||||
"--upload", action="store_true", help="upload the bitstream.bin from build/"
|
||||
)
|
||||
config.add_argument(
|
||||
"--clockrate",
|
||||
metavar="RATE",
|
||||
type=int,
|
||||
help="Clock rate in Hz",
|
||||
)
|
||||
|
||||
config.add_argument(
|
||||
"--name",
|
||||
metavar="FILE",
|
||||
help="Output file and project name (default: top_module name)",
|
||||
default="",
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def die_with_error(msg):
|
||||
print(msg)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
class TTFPGA:
|
||||
|
||||
def __init__(self, proj: Project | None, args):
|
||||
self.project = proj
|
||||
self.args = args
|
||||
|
||||
def get_name(self):
|
||||
base_name = self.args.name
|
||||
if not len(base_name):
|
||||
if (
|
||||
"top_module" in self.args
|
||||
and self.args.top_module is not None
|
||||
and len(self.args.top_module)
|
||||
):
|
||||
return self.args.top_module
|
||||
if self.project is not None:
|
||||
base_name = self.project.info.top_module
|
||||
|
||||
if not len(base_name): # shouldn't happen
|
||||
base_name = "tt_fpga_project"
|
||||
|
||||
return base_name
|
||||
|
||||
@property
|
||||
def bitstream_filename(self):
|
||||
return f"{self.get_name()}.bin"
|
||||
|
||||
@property
|
||||
def top_module(self):
|
||||
if "top_module" in self.args and self.args.top_module:
|
||||
# command line overrides yaml
|
||||
return self.args.top_module
|
||||
|
||||
if self.project is not None:
|
||||
return self.project.info.top_module
|
||||
|
||||
if "top_module" in self.args:
|
||||
if not self.args.top_module:
|
||||
die_with_error("No project yaml, must specify top_module")
|
||||
else:
|
||||
return None
|
||||
return self.args.top_module
|
||||
|
||||
@property
|
||||
def source_dir(self):
|
||||
|
||||
if self.args.source_dir:
|
||||
# command line overrides yaml
|
||||
return self.args.source_dir
|
||||
|
||||
if self.project is not None:
|
||||
return self.project.src_dir
|
||||
if not self.args.source_dir:
|
||||
die_with_error("No project yaml, must specify source_dir")
|
||||
return self.args.source_dir
|
||||
|
||||
@property
|
||||
def local_dir(self):
|
||||
|
||||
if self.project is not None:
|
||||
return self.project.local_dir
|
||||
|
||||
return os.path.realpath(self.args.project_dir)
|
||||
|
||||
@property
|
||||
def sources(self):
|
||||
|
||||
if self.args.source and len(self.args.source):
|
||||
# command line overrides yaml
|
||||
return self.args.source
|
||||
|
||||
if self.project is not None:
|
||||
return self.project.sources
|
||||
|
||||
if not self.args.source:
|
||||
die_with_error(
|
||||
"No project yaml, must specify --source A.v --source B.v ..."
|
||||
)
|
||||
|
||||
# print(self.args.source)
|
||||
return self.args.source
|
||||
|
||||
def create_fpga_bitstream(self):
|
||||
logging.info(f"Creating FPGA bitstream for {self.project}")
|
||||
|
||||
target_map = {
|
||||
"classic": {
|
||||
"pcf": "tt_fpga_top.pcf",
|
||||
},
|
||||
"fabricfox": {
|
||||
"pcf": "tt_fpga_fabricfoxv2.pcf",
|
||||
},
|
||||
}
|
||||
args = self.args
|
||||
|
||||
pcf_file = target_map[args.breakout_target]["pcf"]
|
||||
|
||||
base_name = self.get_name()
|
||||
|
||||
with open(os.path.join(SCRIPT_DIR, "fpga/tt_fpga_top.v"), "r") as f:
|
||||
top_module_template = f.read()
|
||||
|
||||
with open(os.path.join(self.source_dir, "_tt_fpga_top.v"), "w") as f:
|
||||
f.write(top_module_template.replace("__tt_um_placeholder", self.top_module))
|
||||
|
||||
build_dir = os.path.join(self.local_dir, "build")
|
||||
os.makedirs(build_dir, exist_ok=True)
|
||||
|
||||
sources = [os.path.join(self.source_dir, src) for src in self.sources]
|
||||
source_list = " ".join(sources)
|
||||
|
||||
yosys_cmd = f"yosys -l {build_dir}/01-synth.log -DSYNTH -p 'read_verilog -sv src/_tt_fpga_top.v {source_list}; synth_ice40 -top tt_fpga_top -json {build_dir}/{base_name}.json'"
|
||||
logging.debug(yosys_cmd)
|
||||
p = subprocess.run(yosys_cmd, shell=True)
|
||||
if p.returncode != 0:
|
||||
logging.error("synthesis failed")
|
||||
exit(1)
|
||||
|
||||
seed = os.getenv("TT_FPGA_SEED", "10")
|
||||
freq = os.getenv("TT_FPGA_FREQ", "12")
|
||||
|
||||
nextpnr_cmd = f"nextpnr-ice40 -l {build_dir}/02-nextpnr.log --pcf-allow-unconstrained --seed {seed} --freq {freq} --package sg48 --up5k --asc {build_dir}/{base_name}.asc --pcf {SCRIPT_DIR}/fpga/{pcf_file} --json {build_dir}/{base_name}.json"
|
||||
logging.debug(nextpnr_cmd)
|
||||
p = subprocess.run(nextpnr_cmd, shell=True)
|
||||
if p.returncode != 0:
|
||||
logging.error("placement failed")
|
||||
exit(1)
|
||||
|
||||
icepack_cmd = f"icepack {build_dir}/{base_name}.asc {build_dir}/{base_name}.bin"
|
||||
logging.debug(icepack_cmd)
|
||||
p = subprocess.run(icepack_cmd, shell=True)
|
||||
if p.returncode != 0:
|
||||
logging.error("bitstream creation failed failed")
|
||||
exit(1)
|
||||
|
||||
logging.info(f"Bitstream created successfully: {build_dir}/{base_name}.bin")
|
||||
|
||||
|
||||
def yaml_file_exists(args):
|
||||
return os.path.exists(os.path.join(os.path.realpath(args.project_dir), "info.yaml"))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
parser = getParser()
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
setup_logging(args.loglevel)
|
||||
|
||||
if args.command != CommandHarden and args.command != CommandConfig:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
project = None
|
||||
if yaml_file_exists(args):
|
||||
project = Project(
|
||||
0, "unknown", args.project_dir, pdk="fpgaUp5k", is_user_project=True
|
||||
)
|
||||
project.post_clone_setup()
|
||||
|
||||
fpga = TTFPGA(project, args)
|
||||
|
||||
if args.command == CommandHarden:
|
||||
fpga.create_fpga_bitstream()
|
||||
elif args.command == CommandConfig:
|
||||
proj_name = fpga.get_name()
|
||||
bitstream_name = f"{proj_name}.bin"
|
||||
fname = f"build/{bitstream_name}"
|
||||
if not os.path.exists(fname):
|
||||
print(f"Can't find {fname}")
|
||||
sys.exit(3)
|
||||
ttdbconf = TTDBConfig(args.port)
|
||||
if args.upload:
|
||||
print(f"Uploading {fname}")
|
||||
ttdbconf.write_file(fname, f"/bitstreams/{bitstream_name}")
|
||||
if args.set_default or args.clockrate is not None:
|
||||
cnf = ttdbconf.get_config_ini()
|
||||
if args.set_default:
|
||||
cnf.set_default_project(proj_name)
|
||||
if args.clockrate is not None:
|
||||
cnf.set_project_clockrate(proj_name, args.clockrate)
|
||||
|
||||
cnf.save() # save local file
|
||||
# upload that config.ini
|
||||
ttdbconf.upload_config_ini(cnf.filepath, do_reset=False)
|
||||
ttdbconf.reset() # restart
|
||||
|
||||
if not (args.upload or args.set_default or args.clockrate):
|
||||
print(
|
||||
"Did nothing. Use configure with --upload, --set-default and/or --clockrate"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,20 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
|
||||
def setup_logging(log_level):
|
||||
|
||||
# setup log
|
||||
log_format = logging.Formatter(
|
||||
"%(asctime)s - %(module)-10s - %(levelname)-8s - %(message)s"
|
||||
)
|
||||
# configure the client logging
|
||||
log = logging.getLogger("")
|
||||
# has to be set to debug as is the root logger
|
||||
log.setLevel(log_level)
|
||||
|
||||
# create console handler and set level to info
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
# create formatter for console
|
||||
ch.setFormatter(log_format)
|
||||
log.addHandler(ch)
|
||||
+2
-37
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from project import Project
|
||||
from project_checks import check_project_docs
|
||||
from tech import TechName
|
||||
from tt_logging import setup_logging
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="TT setup")
|
||||
@@ -143,41 +143,9 @@ if __name__ == "__main__":
|
||||
const=True,
|
||||
)
|
||||
|
||||
# FPGA
|
||||
parser.add_argument(
|
||||
"--create-fpga-bitstream",
|
||||
help="create the FPGA bitstream",
|
||||
action="store_const",
|
||||
const=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fpga-breakout-target",
|
||||
help="Select target breakout: classic (TT04) or fabricfox (TT06+)",
|
||||
choices=["classic", "fabricfox"],
|
||||
default="classic",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fpga-bitstream-name",
|
||||
help="Set the bitstream name",
|
||||
default="",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# setup log
|
||||
log_format = logging.Formatter(
|
||||
"%(asctime)s - %(module)-10s - %(levelname)-8s - %(message)s"
|
||||
)
|
||||
# configure the client logging
|
||||
log = logging.getLogger("")
|
||||
# has to be set to debug as is the root logger
|
||||
log.setLevel(args.loglevel)
|
||||
|
||||
# create console handler and set level to info
|
||||
ch = logging.StreamHandler(sys.stdout)
|
||||
# create formatter for console
|
||||
ch.setFormatter(log_format)
|
||||
log.addHandler(ch)
|
||||
setup_logging(args.loglevel)
|
||||
|
||||
pdk: TechName = "ihp-sg13g2" if args.ihp else "gf180mcuD" if args.gf else "sky130A"
|
||||
|
||||
@@ -228,6 +196,3 @@ if __name__ == "__main__":
|
||||
|
||||
if args.create_svg:
|
||||
project.create_svg()
|
||||
|
||||
if args.create_fpga_bitstream:
|
||||
project.create_fpga_bitstream(args)
|
||||
|
||||
Reference in New Issue
Block a user