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")
|
return os.path.join(self.local_dir, f"{self.info.top_module}.v")
|
||||||
|
|
||||||
def get_tile_sizes(self):
|
def get_tile_sizes(self):
|
||||||
|
if self.tech.is_fpga:
|
||||||
|
return {}
|
||||||
tile_sizes_yaml = f"tech/{self.pdk}/tile_sizes.yaml"
|
tile_sizes_yaml = f"tech/{self.pdk}/tile_sizes.yaml"
|
||||||
with open(
|
with open(
|
||||||
os.path.join(os.path.dirname(__file__), tile_sizes_yaml), "r"
|
os.path.join(os.path.dirname(__file__), tile_sizes_yaml), "r"
|
||||||
@@ -629,62 +631,6 @@ class Project:
|
|||||||
logging.error("librelane failed")
|
logging.error("librelane failed")
|
||||||
exit(1)
|
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):
|
def create_project_datasheet(self, template_version: str):
|
||||||
template_args = copy.deepcopy(self.info.__dict__)
|
template_args = copy.deepcopy(self.info.__dict__)
|
||||||
template_args.update(
|
template_args.update(
|
||||||
|
|||||||
+10
-7
@@ -106,13 +106,16 @@ class ProjectInfo:
|
|||||||
else:
|
else:
|
||||||
self.description: str = description
|
self.description: str = description
|
||||||
|
|
||||||
tiles = project_section.get("tiles")
|
if tile_sizes:
|
||||||
if tiles is None:
|
tiles = project_section.get("tiles")
|
||||||
errors.append("Missing key 'tiles' in 'project' section")
|
if tiles is None:
|
||||||
elif tiles not in tile_sizes.keys():
|
errors.append("Missing key 'tiles' in 'project' section")
|
||||||
errors.append(f"Invalid value for 'tiles' in 'project' section: {tiles}")
|
elif tiles not in tile_sizes.keys():
|
||||||
else:
|
errors.append(
|
||||||
self.tiles: str = tiles
|
f"Invalid value for 'tiles' in 'project' section: {tiles}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.tiles: str = tiles
|
||||||
|
|
||||||
analog_pins = project_section.get("analog_pins", 0)
|
analog_pins = project_section.get("analog_pins", 0)
|
||||||
if not isinstance(analog_pins, int):
|
if not isinstance(analog_pins, int):
|
||||||
|
|||||||
+3
-1
@@ -16,7 +16,9 @@ dependencies = [
|
|||||||
"PyYAML",
|
"PyYAML",
|
||||||
"requests",
|
"requests",
|
||||||
"yowasp-yosys",
|
"yowasp-yosys",
|
||||||
"matplotlib"
|
"matplotlib",
|
||||||
|
"configupdater",
|
||||||
|
"mpremote"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools]
|
[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:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile
|
# pip-compile
|
||||||
@@ -20,6 +20,8 @@ chevron==0.14.0
|
|||||||
# via tt-support-tools (pyproject.toml)
|
# via tt-support-tools (pyproject.toml)
|
||||||
click==8.2.1
|
click==8.2.1
|
||||||
# via yowasp-yosys
|
# via yowasp-yosys
|
||||||
|
configupdater==3.2
|
||||||
|
# via tt-support-tools (pyproject.toml)
|
||||||
contourpy==1.3.3
|
contourpy==1.3.3
|
||||||
# via matplotlib
|
# via matplotlib
|
||||||
cssselect2==0.8.0
|
cssselect2==0.8.0
|
||||||
@@ -56,6 +58,8 @@ matplotlib==3.10.6
|
|||||||
# via tt-support-tools (pyproject.toml)
|
# via tt-support-tools (pyproject.toml)
|
||||||
mistune==3.1.3
|
mistune==3.1.3
|
||||||
# via tt-support-tools (pyproject.toml)
|
# via tt-support-tools (pyproject.toml)
|
||||||
|
mpremote==1.27.0
|
||||||
|
# via tt-support-tools (pyproject.toml)
|
||||||
nodeenv==1.9.1
|
nodeenv==1.9.1
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
numpy==1.26.4
|
numpy==1.26.4
|
||||||
@@ -74,6 +78,7 @@ pillow==11.3.0
|
|||||||
# matplotlib
|
# matplotlib
|
||||||
platformdirs==4.3.8
|
platformdirs==4.3.8
|
||||||
# via
|
# via
|
||||||
|
# mpremote
|
||||||
# virtualenv
|
# virtualenv
|
||||||
# yowasp-runtime
|
# yowasp-runtime
|
||||||
pluggy==1.6.0
|
pluggy==1.6.0
|
||||||
@@ -86,6 +91,8 @@ pygments==2.19.2
|
|||||||
# via pytest
|
# via pytest
|
||||||
pyparsing==3.2.5
|
pyparsing==3.2.5
|
||||||
# via matplotlib
|
# via matplotlib
|
||||||
|
pyserial==3.5
|
||||||
|
# via mpremote
|
||||||
pytest==8.4.2
|
pytest==8.4.2
|
||||||
# via tt-support-tools (pyproject.toml)
|
# via tt-support-tools (pyproject.toml)
|
||||||
python-dateutil==2.9.0.post0
|
python-dateutil==2.9.0.post0
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
from typing import Dict, List, Literal, Pattern, Protocol, Tuple, TypedDict, Union
|
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):
|
class CellDefinition(TypedDict):
|
||||||
@@ -16,6 +16,7 @@ class PDKVersionInfo(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
class Tech(Protocol):
|
class Tech(Protocol):
|
||||||
|
is_fpga: bool
|
||||||
def_suffix: str
|
def_suffix: str
|
||||||
librelane_pdk_args: str
|
librelane_pdk_args: str
|
||||||
tt_corner: str
|
tt_corner: str
|
||||||
@@ -65,6 +66,7 @@ def parse_openpdks_pdk_version(
|
|||||||
|
|
||||||
|
|
||||||
class Sky130Tech(Tech):
|
class Sky130Tech(Tech):
|
||||||
|
is_fpga = False
|
||||||
def_suffix = "pg"
|
def_suffix = "pg"
|
||||||
librelane_pdk_args = ""
|
librelane_pdk_args = ""
|
||||||
tt_corner = "nom_tt_025C_1v80"
|
tt_corner = "nom_tt_025C_1v80"
|
||||||
@@ -124,6 +126,7 @@ class Sky130Tech(Tech):
|
|||||||
|
|
||||||
|
|
||||||
class IHPTech(Tech):
|
class IHPTech(Tech):
|
||||||
|
is_fpga = False
|
||||||
def_suffix = "pgvdd"
|
def_suffix = "pgvdd"
|
||||||
librelane_pdk_args = "--pdk ihp-sg13g2"
|
librelane_pdk_args = "--pdk ihp-sg13g2"
|
||||||
tt_corner = "nom_typ_1p20V_25C"
|
tt_corner = "nom_typ_1p20V_25C"
|
||||||
@@ -177,6 +180,7 @@ class IHPTech(Tech):
|
|||||||
|
|
||||||
|
|
||||||
class GF180MCUDTech(Tech):
|
class GF180MCUDTech(Tech):
|
||||||
|
is_fpga = False
|
||||||
def_suffix = "pgvdd"
|
def_suffix = "pgvdd"
|
||||||
librelane_pdk_args = "--pdk gf180mcuD"
|
librelane_pdk_args = "--pdk gf180mcuD"
|
||||||
tt_corner = "nom_tt_025C_3v30"
|
tt_corner = "nom_tt_025C_3v30"
|
||||||
@@ -248,8 +252,33 @@ class GF180MCUDTech(Tech):
|
|||||||
return cells
|
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] = {
|
tech_map: dict[TechName, Tech] = {
|
||||||
"ihp-sg13g2": IHPTech(),
|
"ihp-sg13g2": IHPTech(),
|
||||||
"gf180mcuD": GF180MCUDTech(),
|
"gf180mcuD": GF180MCUDTech(),
|
||||||
"sky130A": Sky130Tech(),
|
"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
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import sys
|
|
||||||
|
|
||||||
from project import Project
|
from project import Project
|
||||||
from project_checks import check_project_docs
|
from project_checks import check_project_docs
|
||||||
from tech import TechName
|
from tech import TechName
|
||||||
|
from tt_logging import setup_logging
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="TT setup")
|
parser = argparse.ArgumentParser(description="TT setup")
|
||||||
@@ -143,41 +143,9 @@ if __name__ == "__main__":
|
|||||||
const=True,
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# setup log
|
setup_logging(args.loglevel)
|
||||||
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)
|
|
||||||
|
|
||||||
pdk: TechName = "ihp-sg13g2" if args.ihp else "gf180mcuD" if args.gf else "sky130A"
|
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:
|
if args.create_svg:
|
||||||
project.create_svg()
|
project.create_svg()
|
||||||
|
|
||||||
if args.create_fpga_bitstream:
|
|
||||||
project.create_fpga_bitstream(args)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user