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:
Pat Deegan
2026-02-12 06:27:29 +01:00
committed by GitHub
parent b4a9e6c7ea
commit 89ad9bfa93
10 changed files with 705 additions and 103 deletions
+37
View File
@@ -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
View File
@@ -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(
+4 -1
View File
@@ -106,11 +106,14 @@ class ProjectInfo:
else:
self.description: str = description
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}")
errors.append(
f"Invalid value for 'tiles' in 'project' section: {tiles}"
)
else:
self.tiles: str = tiles
+3 -1
View File
@@ -16,7 +16,9 @@ dependencies = [
"PyYAML",
"requests",
"yowasp-yosys",
"matplotlib"
"matplotlib",
"configupdater",
"mpremote"
]
[tool.setuptools]
+8 -1
View File
@@ -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
+30 -1
View File
@@ -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(),
}
+239
View File
@@ -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
View File
@@ -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()
+20
View File
@@ -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
View File
@@ -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)