litex/litex/build/generic_platform.py

556 lines
18 KiB
Python

#
# This file is part of LiteX.
#
# Copyright (c) 2013-2014 Sebastien Bourdeauducq <sb@m-labs.hk>
# Copyright (c) 2014-2019 Florent Kermarrec <florent@enjoy-digital.fr>
# Copyright (c) 2015 Yann Sionneau <ys@m-labs.hk>
# SPDX-License-Identifier: BSD-2-Clause
import sys
import os
import re
from migen.fhdl.structure import Signal, Cat
from migen.genlib.record import Record
from litex.gen.fhdl import verilog
from litex.build.io import CRG
from litex.build import tools
# --------------------------------------------------------------------------------------------------
class ConstraintError(Exception):
pass
# IOS ----------------------------------------------------------------------------------------------
class Pins:
def __init__(self, *identifiers):
self.identifiers = []
for i in identifiers:
if isinstance(i, int):
self.identifiers += ["X"]*i
else:
self.identifiers += i.split()
def __repr__(self):
return "{}('{}')".format(self.__class__.__name__, " ".join(self.identifiers))
class IOStandard:
def __init__(self, name):
self.name = name
def __repr__(self):
return "{}('{}')".format(self.__class__.__name__, self.name)
class Drive:
def __init__(self, strength):
self.strength = strength
def __repr__(self):
return "{}('{}')".format(self.__class__.__name__, self.strength)
class Misc:
def __init__(self, misc):
self.misc = misc
def __repr__(self):
return "{}({})".format(self.__class__.__name__, repr(self.misc))
class Inverted:
def __repr__(self):
return "{}()".format(self.__class__.__name__)
class Subsignal:
def __init__(self, name, *constraints):
self.name = name
self.constraints = list(constraints)
def __repr__(self):
return "{}('{}', {})".format(self.__class__.__name__,
self.name,
", ".join([repr(constr) for constr in self.constraints]))
# Platform -----------------------------------------------------------------------------------------
class PlatformInfo:
def __init__(self, info):
self.info = info
def __repr__(self):
return "{}({})".format(self.__class__.__name__, repr(self.info))
def _lookup(description, name, number, loose=True):
for resource in description:
if resource[0] == name and (number is None or resource[1] == number):
return resource
if loose:
return None
else:
raise ConstraintError("Resource not found: {}:{}".format(name, number))
def _resource_type(resource):
t = None
i = None
for element in resource[2:]:
if isinstance(element, Pins):
assert(t is None)
t = len(element.identifiers)
elif isinstance(element, Subsignal):
if t is None:
t = []
if i is None:
i = []
assert(isinstance(t, list))
n_bits = None
inverted = False
for c in element.constraints:
if isinstance(c, Pins):
assert(n_bits is None)
n_bits = len(c.identifiers)
if isinstance(c, Inverted):
inverted = True
t.append((element.name, n_bits))
i.append((element.name, inverted))
return t, i
# Connector Manager --------------------------------------------------------------------------------
class ConnectorManager:
def __init__(self, connectors):
self.connector_table = dict()
self.add_connector(connectors)
def add_connector(self, connectors):
if isinstance(connectors, tuple):
connectors = [connectors]
for connector in connectors:
cit = iter(connector)
conn_name = next(cit)
if isinstance(connector[1], str):
pin_list = []
for pins in cit:
pin_list += pins.split()
pin_list = [None if pin == "None" else pin for pin in pin_list]
elif isinstance(connector[1], dict):
pin_list = connector[1]
else:
raise ValueError("Unsupported pin list type {} for connector"
" {}".format(type(connector[1]), conn_name))
if conn_name in self.connector_table:
raise ValueError(
"Connector specified more than once: {}".format(conn_name))
self.connector_table[conn_name] = pin_list
def resolve_identifiers(self, identifiers):
r = []
for identifier in identifiers:
if ":" in identifier:
try:
conn, pn = identifier.split(":")
except ValueError as err:
raise ValueError(f"\"{identifier}\" {err}") from err
if pn.isdigit():
pn = int(pn)
assert conn in self.connector_table, f"No connector named '{conn}' is available"
conn_entry = self.connector_table[conn]
if isinstance(conn_entry, dict):
assert pn in conn_entry, f"There is no pin '{pn}' on connector '{conn}'"
else:
assert pn < len(conn_entry), f"There is no pin with number '{pn}' on connector '{conn}', maximum is {len(conn_entry)-1}"
conn_pn = self.connector_table[conn][pn]
if ":" in conn_pn:
conn_pn = self.resolve_identifiers([conn_pn])[0]
r.append(conn_pn)
else:
r.append(identifier)
return r
def _separate_pins(constraints):
pins = None
others = []
for c in constraints:
if isinstance(c, Pins):
assert(pins is None)
pins = c.identifiers
else:
others.append(c)
return pins, others
# Constraint Manager -------------------------------------------------------------------------------
class ConstraintManager:
def __init__(self, io, connectors):
self.available = list(io)
self.matched = []
self.platform_commands = []
self.connector_manager = ConnectorManager(connectors)
def add_extension(self, io):
self.available.extend(io)
def add_connector(self, connectors):
self.connector_manager.add_connector(connectors)
def request(self, name, number=None, loose=False):
resource = _lookup(self.available, name, number, loose)
if resource is None:
return None
rt, ri = _resource_type(resource)
if number is None:
resource_name = name
else:
resource_name = name + str(number)
if isinstance(rt, int):
obj = Signal(rt, name_override=resource_name)
else:
obj = Record(rt, name=resource_name)
for name, inverted in ri:
if inverted:
getattr(obj, name).inverted = True
for element in resource[2:]:
if isinstance(element, Inverted):
if isinstance(obj, Signal):
obj.inverted = True
if isinstance(element, PlatformInfo):
obj.platform_info = element.info
break
self.available.remove(resource)
self.matched.append((resource, obj))
return obj
def request_all(self, name):
r = []
while True:
try:
r.append(self.request(name, len(r)))
except ConstraintError:
break
if not len(r):
raise ValueError(f"Could not request some pin(s) named '{name}'")
return Cat(r)
def request_remaining(self, name):
r = []
while True:
try:
r.append(self.request(name))
except ConstraintError:
break
if not len(r):
raise ValueError(f"Could not request any pins named '{name}'")
return Cat(r)
def lookup_request(self, name, number=None, loose=False):
subname = None
if ":" in name: name, subname = name.split(":")
for resource, obj in self.matched:
if resource[0] == name and (number is None or
resource[1] == number):
if subname is not None:
return getattr(obj, subname)
else:
return obj
if loose:
return None
else:
raise ConstraintError("Resource not found: {}:{}".format(name, number))
def add_platform_command(self, command, **signals):
self.platform_commands.append((command, signals))
def get_io_signals(self):
r = set()
for resource, obj in self.matched:
if isinstance(obj, Signal):
r.add(obj)
else:
r.update(obj.flatten())
return r
def get_sig_constraints(self):
r = []
for resource, obj in self.matched:
name = resource[0]
number = resource[1]
has_subsignals = False
top_constraints = []
for element in resource[2:]:
if isinstance(element, Subsignal):
has_subsignals = True
else:
top_constraints.append(element)
if has_subsignals:
for element in resource[2:]:
if isinstance(element, Subsignal):
# Because we could have removed one Signal From the record
if hasattr(obj, element.name):
sig = getattr(obj, element.name)
pins, others = _separate_pins(top_constraints +
element.constraints)
pins = self.connector_manager.resolve_identifiers(pins)
r.append((sig, pins, others,
(name, number, element.name)))
else:
pins, others = _separate_pins(top_constraints)
pins = self.connector_manager.resolve_identifiers(pins)
r.append((obj, pins, others, (name, number, None)))
return r
def get_platform_commands(self):
return self.platform_commands
# Generic Platform ---------------------------------------------------------------------------------
class GenericPlatform:
device_family = None
_bitstream_ext = None # None by default, overridden by vendor platform, may
# be a string when same extension is used for sram and
# flash. A dict must be provided otherwise
def __init__(self, device, io, connectors=[], name=None):
self.toolchain = None
self.device = device
self.constraint_manager = ConstraintManager(io, connectors)
if name is None:
# Get name from Platform file.
name = self.__module__.split(".")[-1]
if name == "__main__":
# If no Platform file, use script filename,
name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
self.name = name
self.sources = []
self.verilog_include_paths = []
self.output_dir = None
self.finalized = False
self.use_default_clk = False
def request(self, *args, **kwargs):
return self.constraint_manager.request(*args, **kwargs)
def request_all(self, *args, **kwargs):
return self.constraint_manager.request_all(*args, **kwargs)
def request_remaining(self, *args, **kwargs):
return self.constraint_manager.request_remaining(*args, **kwargs)
def lookup_request(self, *args, **kwargs):
return self.constraint_manager.lookup_request(*args, **kwargs)
def add_period_constraint(self, clk, period):
raise NotImplementedError
def add_false_path_constraint(self, from_, to):
raise NotImplementedError
def add_false_path_constraints(self, *clk):
for a in clk:
for b in clk:
if a is not b:
self.add_false_path_constraint(a, b)
def add_platform_command(self, *args, **kwargs):
return self.constraint_manager.add_platform_command(*args, **kwargs)
def add_extension(self, *args, **kwargs):
return self.constraint_manager.add_extension(*args, **kwargs)
def add_connector(self, *args, **kwargs):
self.constraint_manager.add_connector(*args, **kwargs)
def finalize(self, fragment, *args, **kwargs):
if self.finalized:
raise ConstraintError("Already finalized")
# If none exists, create a default clock domain and drive it.
if not fragment.clock_domains:
if not hasattr(self, "default_clk_name"):
raise NotImplementedError(
"No default clock and no clock domain defined")
crg = CRG(self.request(self.default_clk_name))
fragment += crg.get_fragment()
self.use_default_clk = True
self.do_finalize(fragment, *args, **kwargs)
self.finalized = True
def do_finalize(self, fragment, *args, **kwargs):
# Overload this and e.g. add_platform_command()'s after the modules had their say.
if self.use_default_clk and hasattr(self, "default_clk_period"):
try:
self.add_period_constraint(
self.lookup_request(self.default_clk_name),
self.default_clk_period)
except ConstraintError:
pass
def add_source(self, filename, language=None, library=None, copy=False):
filename = os.path.abspath(filename)
if language is None:
language = tools.language_by_filename(filename)
if library is None:
library = "work"
for f, *_ in self.sources:
if f == filename:
return
if copy:
self.sources.append((filename, language, library, True))
else:
self.sources.append((filename, language, library))
def add_sources(self, path, *filenames, language=None, library=None, copy=False):
for f in filenames:
self.add_source(os.path.join(path, f), language, library, copy)
def add_source_dir(self, path, recursive=True, language=None, library=None):
dir_files = []
if recursive:
for root, dirs, files in os.walk(path):
for filename in files:
dir_files.append(os.path.join(root, filename))
else:
for item in os.listdir(path):
if os.path.isfile(os.path.join(path, item)):
dir_files.append(os.path.join(path, item))
for filename in dir_files:
_language = language
if _language is None:
_language = tools.language_by_filename(filename)
if _language is not None:
self.add_source(filename, _language, library)
def add_verilog_include_path(self, path):
self.verilog_include_paths.append(os.path.abspath(path))
def resolve_signals(self, vns):
# Resolve signal names in constraints.
sc = self.constraint_manager.get_sig_constraints()
named_sc = [(vns.get_name(sig), pins, others, resource)
for sig, pins, others, resource in sc]
# Resolve signal names in platform commands.
pc = self.constraint_manager.get_platform_commands()
named_pc = []
for template, args in pc:
name_dict = dict((k, vns.get_name(sig)) for k, sig in args.items())
named_pc.append(template.format(**name_dict))
return named_sc, named_pc
def get_verilog(self, fragment, **kwargs):
return verilog.convert(fragment, platform=self, **kwargs)
def get_edif(self, fragment, cell_library, vendor, device, **kwargs):
return edif.convert(
fragment,
self.constraint_manager.get_io_signals(),
cell_library, vendor, device, **kwargs)
def build(self, fragment):
raise NotImplementedError("GenericPlatform.build must be overloaded")
def get_bitstream_extension(self, mode="sram"):
"""
Return the bitstream's extension according to mode (sram / flash).
The default (generic) implementation check if `self._bitstream_ext`
is a dict or a string. For former case it return extension using `mode`
parameter, in latter case simply return `self._bitstream_ext`'s value.
When this behaviour is not adapted this method must be overriden by
a specific one at vendor level.
Parameters
----------
mode: str
bitstream destination (must be sram or flash)
Returns
-------
bitstream extension: str
"""
if self._bitstream_ext is None:
return None
elif type(self._bitstream_ext) == dict:
return self._bitstream_ext[mode]
else:
return self._bitstream_ext
def create_programmer(self):
raise NotImplementedError
@property
def support_mixed_language(self):
return self.toolchain.support_mixed_language
@classmethod
def fill_args(cls, toolchain, parser):
"""
pass parser to the specific toolchain to
fill this with toolchain args
Parameters
==========
toolchain: str
toolchain name
parser: argparse.ArgumentParser
parser to be filled
"""
pass # pass must be overloaded (if required)
@classmethod
def get_argdict(cls, toolchain, args):
"""
return a dict of args
Parameters
==========
toolchain: str
toolchain name
Return
======
a dict of key/value for each args or an empty dict
"""
return {} # Empty must be overloaded (if required)
@classmethod
def toolchains(cls, device):
"""
Returns list of toolchains compatible with device
Parameters
==========
device: str
device name (ice40, ecp5, nexus)
Return
======
A list of compatible toolchains (str) or an empty list
"""
if type(cls._supported_toolchains) == dict:
assert device is not None
return cls._supported_toolchains[device]
else:
return cls._supported_toolchains