491 lines
14 KiB
Python
491 lines
14 KiB
Python
# Copyright lowRISC contributors.
|
|
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
import logging as log
|
|
import re
|
|
import sys
|
|
from collections import OrderedDict
|
|
from copy import deepcopy
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
import hjson
|
|
from reggen.ip_block import IpBlock
|
|
|
|
# Ignore flake8 warning as the function is used in the template
|
|
# disable isort formating, as conflicting with flake8
|
|
from .intermodule import find_otherside_modules # noqa : F401 # isort:skip
|
|
from .intermodule import im_portname, im_defname, im_netname # noqa : F401 # isort:skip
|
|
from .intermodule import get_dangling_im_def # noqa : F401 # isort:skip
|
|
|
|
|
|
class Name:
|
|
"""
|
|
We often need to format names in specific ways; this class does so.
|
|
|
|
To simplify parsing and reassembling of name strings, this class
|
|
stores the name parts as a canonical list of strings internally
|
|
(in self.parts).
|
|
|
|
The "from_*" functions parse and split a name string into the canonical
|
|
list, whereas the "as_*" functions reassemble the canonical list in the
|
|
format specified.
|
|
|
|
For example, ex = Name.from_snake_case("example_name") gets split into
|
|
["example", "name"] internally, and ex.as_camel_case() reassembles this
|
|
internal representation into "ExampleName".
|
|
"""
|
|
def __add__(self, other):
|
|
return Name(self.parts + other.parts)
|
|
|
|
@staticmethod
|
|
def from_snake_case(input: str) -> 'Name':
|
|
return Name(input.split("_"))
|
|
|
|
def __init__(self, parts: List[str]):
|
|
self.parts = parts
|
|
for p in parts:
|
|
assert len(p) > 0, "cannot add zero-length name piece"
|
|
|
|
def as_snake_case(self) -> str:
|
|
return "_".join([p.lower() for p in self.parts])
|
|
|
|
def as_camel_case(self) -> str:
|
|
out = ""
|
|
for p in self.parts:
|
|
# If we're about to join two parts which would introduce adjacent
|
|
# numbers, put an underscore between them.
|
|
if out[-1:].isnumeric() and p[:1].isnumeric():
|
|
out += "_" + p
|
|
else:
|
|
out += p.capitalize()
|
|
return out
|
|
|
|
def as_c_define(self) -> str:
|
|
return "_".join([p.upper() for p in self.parts])
|
|
|
|
def as_c_enum(self) -> str:
|
|
return "k" + self.as_camel_case()
|
|
|
|
def as_c_type(self) -> str:
|
|
return self.as_snake_case() + "_t"
|
|
|
|
def remove_part(self, part_to_remove: str) -> "Name":
|
|
return Name([p for p in self.parts if p != part_to_remove])
|
|
|
|
|
|
def is_ipcfg(ip: Path) -> bool: # return bool
|
|
log.info("IP Path: %s" % repr(ip))
|
|
ip_name = ip.parents[1].name
|
|
hjson_name = ip.name
|
|
|
|
log.info("IP Name(%s) and HJSON name (%s)" % (ip_name, hjson_name))
|
|
|
|
if ip_name + ".hjson" == hjson_name or ip_name + "_reg.hjson" == hjson_name:
|
|
return True
|
|
return False
|
|
|
|
|
|
def search_ips(ip_path): # return list of config files
|
|
# list the every Hjson file
|
|
p = ip_path.glob('*/data/*.hjson')
|
|
|
|
# filter only ip_name/data/ip_name{_reg|''}.hjson
|
|
ips = [x for x in p if is_ipcfg(x)]
|
|
|
|
log.info("Filtered-in IP files: %s" % repr(ips))
|
|
return ips
|
|
|
|
|
|
def is_xbarcfg(xbar_obj):
|
|
if "type" in xbar_obj and xbar_obj["type"] == "xbar":
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_hjsonobj_xbars(xbar_path):
|
|
""" Search crossbars Hjson files from given path.
|
|
|
|
Search every Hjson in the directory and check Hjson type.
|
|
It could be type: "top" or type: "xbar"
|
|
returns [(name, obj), ... ]
|
|
"""
|
|
p = xbar_path.glob('*.hjson')
|
|
try:
|
|
xbar_objs = [
|
|
hjson.load(x.open('r'),
|
|
use_decimal=True,
|
|
object_pairs_hook=OrderedDict) for x in p
|
|
]
|
|
except ValueError:
|
|
raise SystemExit(sys.exc_info()[1])
|
|
|
|
xbar_objs = [x for x in xbar_objs if is_xbarcfg(x)]
|
|
|
|
return xbar_objs
|
|
|
|
|
|
def get_module_by_name(top, name):
|
|
"""Search in top["module"] by name
|
|
"""
|
|
module = None
|
|
for m in top["module"]:
|
|
if m["name"] == name:
|
|
module = m
|
|
break
|
|
|
|
return module
|
|
|
|
|
|
def intersignal_to_signalname(top, m_name, s_name) -> str:
|
|
|
|
# TODO: Find the signal in the `inter_module_list` and get the correct signal name
|
|
|
|
return "{m_name}_{s_name}".format(m_name=m_name, s_name=s_name)
|
|
|
|
|
|
def get_package_name_by_intermodule_signal(top, struct) -> str:
|
|
"""Search inter-module signal package with the struct name
|
|
|
|
For instance, if `flash_ctrl` has inter-module signal package,
|
|
this function returns the package name
|
|
"""
|
|
instances = top["module"] + top["memory"]
|
|
|
|
intermodule_instances = [
|
|
x["inter_signal_list"] for x in instances if "inter_signal_list" in x
|
|
]
|
|
|
|
for m in intermodule_instances:
|
|
if m["name"] == struct and "package" in m:
|
|
return m["package"]
|
|
return ""
|
|
|
|
|
|
def get_signal_by_name(module, name):
|
|
"""Return the signal struct with the type input/output/inout
|
|
"""
|
|
result = None
|
|
for s in module["available_input_list"] + module[
|
|
"available_output_list"] + module["available_inout_list"]:
|
|
if s["name"] == name:
|
|
result = s
|
|
break
|
|
|
|
return result
|
|
|
|
|
|
def add_module_prefix_to_signal(signal, module):
|
|
"""Add module prefix to module signal format { name: "sig_name", width: NN }
|
|
"""
|
|
result = deepcopy(signal)
|
|
|
|
if "name" not in signal:
|
|
raise SystemExit("signal {} doesn't have name field".format(signal))
|
|
|
|
result["name"] = module + "_" + signal["name"]
|
|
result["module_name"] = module
|
|
|
|
return result
|
|
|
|
|
|
def get_ms_name(name):
|
|
"""Split module_name.signal_name to module_name , signal_name
|
|
"""
|
|
|
|
tokens = name.split('.')
|
|
|
|
if len(tokens) == 0:
|
|
raise SystemExit("This to be catched in validate.py")
|
|
|
|
module = tokens[0]
|
|
signal = None
|
|
if len(tokens) == 2:
|
|
signal = tokens[1]
|
|
|
|
return module, signal
|
|
|
|
|
|
def parse_pad_field(padstr):
|
|
"""Parse PadName[NN...NN] or PadName[NN] or just PadName
|
|
"""
|
|
match = re.match(r'^([A-Za-z0-9_]+)(\[([0-9]+)(\.\.([0-9]+))?\]|)', padstr)
|
|
return match.group(1), match.group(3), match.group(5)
|
|
|
|
|
|
def get_pad_list(padstr):
|
|
pads = []
|
|
|
|
pad, first, last = parse_pad_field(padstr)
|
|
if first is None:
|
|
first = 0
|
|
last = 0
|
|
elif last is None:
|
|
last = first
|
|
first = int(first, 0)
|
|
last = int(last, 0)
|
|
# width = first - last + 1
|
|
|
|
for p in range(first, last + 1):
|
|
pads.append(OrderedDict([("name", pad), ("index", p)]))
|
|
|
|
return pads
|
|
|
|
|
|
# Template functions
|
|
def ljust(x, width):
|
|
return "{:<{width}}".format(x, width=width)
|
|
|
|
|
|
def bitarray(d, width):
|
|
"""Print Systemverilog bit array
|
|
|
|
@param d the bit width of the signal
|
|
@param width max character width of the signal group
|
|
|
|
For instance, if width is 4, the max d value in the signal group could be
|
|
9999. If d is 2, then this function pads 3 spaces at the end of the bit
|
|
slice.
|
|
|
|
"[1:0] " <- d:=2, width=4
|
|
"[9999:0]" <- max d-1 value
|
|
|
|
If d is 1, it means array slice isn't necessary. So it returns empty spaces
|
|
"""
|
|
|
|
if d <= 0:
|
|
log.error("lib.bitarray: Given value {} is smaller than 1".format(d))
|
|
raise ValueError
|
|
if d == 1:
|
|
return " " * (width + 4) # [x:0] needs 4 more space than char_width
|
|
|
|
out = "[{}:0]".format(d - 1)
|
|
return out + (" " * (width - len(str(d))))
|
|
|
|
|
|
def parameterize(text):
|
|
"""Return the value wrapping with quote if not integer nor bits
|
|
"""
|
|
if re.match(r'(\d+\'[hdb]\s*[0-9a-f_A-F]+|[0-9]+)', text) is None:
|
|
return "\"{}\"".format(text)
|
|
|
|
return text
|
|
|
|
|
|
def index(i: int) -> str:
|
|
"""Return index if it is not -1
|
|
"""
|
|
return "[{}]".format(i) if i != -1 else ""
|
|
|
|
|
|
def get_clk_name(clk):
|
|
"""Return the appropriate clk name
|
|
"""
|
|
if clk == 'main':
|
|
return 'clk_i'
|
|
else:
|
|
return "clk_{}_i".format(clk)
|
|
|
|
|
|
def get_reset_path(reset, domain, top):
|
|
"""Return the appropriate reset path given name
|
|
"""
|
|
return top['resets'].get_path(reset, domain)
|
|
|
|
|
|
def get_unused_resets(top):
|
|
"""Return dict of unused resets and associated domain
|
|
"""
|
|
return top['resets'].get_unused_resets(top['power']['domains'])
|
|
|
|
|
|
def is_templated(module):
|
|
"""Returns an indication where a particular module is templated
|
|
"""
|
|
if "attr" not in module:
|
|
return False
|
|
elif module["attr"] in ["templated"]:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def is_top_reggen(module):
|
|
"""Returns an indication where a particular module is NOT templated
|
|
and requires top level specific reggen
|
|
"""
|
|
if "attr" not in module:
|
|
return False
|
|
elif module["attr"] in ["reggen_top", "reggen_only"]:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def is_inst(module):
|
|
"""Returns an indication where a particular module should be instantiated
|
|
in the top level
|
|
"""
|
|
top_level_module = False
|
|
top_level_mem = False
|
|
|
|
if "attr" not in module:
|
|
top_level_module = True
|
|
elif module["attr"] in ["normal", "templated", "reggen_top"]:
|
|
top_level_module = True
|
|
elif module["attr"] in ["reggen_only"]:
|
|
top_level_module = False
|
|
else:
|
|
raise ValueError('Attribute {} in {} is not valid'
|
|
.format(module['attr'], module['name']))
|
|
|
|
if module['type'] in ['rom', 'ram_1p_scr', 'eflash']:
|
|
top_level_mem = True
|
|
|
|
return top_level_mem or top_level_module
|
|
|
|
|
|
def get_base_and_size(name_to_block: Dict[str, IpBlock],
|
|
inst: Dict[str, object],
|
|
ifname: Optional[str]) -> Tuple[int, int]:
|
|
min_device_spacing = 0x1000
|
|
|
|
block = name_to_block.get(inst['type'])
|
|
if block is None:
|
|
# If inst isn't the instantiation of a block, it came from some memory.
|
|
# Memories have their sizes defined, so we can just look it up there.
|
|
bytes_used = int(inst['size'], 0)
|
|
|
|
# Memories don't have multiple or named interfaces, so this will only
|
|
# work if ifname is None.
|
|
assert ifname is None
|
|
base_addr = inst['base_addr']
|
|
|
|
else:
|
|
# If inst is the instantiation of some block, find the register block
|
|
# that corresponds to ifname
|
|
rb = block.reg_blocks.get(ifname)
|
|
if rb is None:
|
|
raise RuntimeError(
|
|
'Cannot connect to non-existent {} device interface '
|
|
'on {!r} (an instance of the {!r} block).'
|
|
.format('default' if ifname is None else repr(ifname),
|
|
inst['name'], block.name))
|
|
else:
|
|
bytes_used = 1 << rb.get_addr_width()
|
|
|
|
base_addr = inst['base_addrs'][ifname]
|
|
|
|
# If an instance has a nonempty "memory" field, take the memory
|
|
# size configuration from there.
|
|
if "memory" in inst:
|
|
if ifname in inst["memory"]:
|
|
memory_size = int(inst["memory"][ifname]["size"], 0)
|
|
if bytes_used > memory_size:
|
|
raise RuntimeError(
|
|
'Memory region on {} device interface '
|
|
'on {!r} (an instance of the {!r} block) '
|
|
'is smaller than the corresponding register block.'
|
|
.format('default' if ifname is None else repr(ifname),
|
|
inst['name'], block.name))
|
|
|
|
bytes_used = memory_size
|
|
|
|
# Round up to min_device_spacing if necessary
|
|
size_byte = max(bytes_used, min_device_spacing)
|
|
|
|
if isinstance(base_addr, str):
|
|
base_addr = int(base_addr, 0)
|
|
else:
|
|
assert isinstance(base_addr, int)
|
|
|
|
return (base_addr, size_byte)
|
|
|
|
|
|
def get_io_enum_literal(sig: Dict, prefix: str) -> str:
|
|
"""Returns the DIO pin enum literal with value assignment"""
|
|
name = Name.from_snake_case(prefix) + Name.from_snake_case(sig["name"])
|
|
# In this case, the signal is a multibit signal, and hence
|
|
# we have to make the signal index part of the parameter
|
|
# name to uniquify it.
|
|
if sig['width'] > 1:
|
|
name += Name([str(sig['idx'])])
|
|
return name.as_camel_case()
|
|
|
|
|
|
def make_bit_concatenation(sig_name: str,
|
|
indices: List[int],
|
|
end_indent: int) -> str:
|
|
'''Return SV code for concatenating certain indices from a signal
|
|
|
|
sig_name is the name of the signal and indices is a non-empty list of the
|
|
indices to use, MSB first. So
|
|
|
|
make_bit_concatenation("foo", [0, 100, 20])
|
|
|
|
should give
|
|
|
|
{foo[0], foo[100], foo[20]}
|
|
|
|
Adjacent bits turn into a range select. For example:
|
|
|
|
make_bit_concatenation("foo", [0, 1, 2])
|
|
|
|
should give
|
|
|
|
foo[0:2]
|
|
|
|
If there are multiple ranges, they are printed one to a line. end_indent
|
|
gives the indentation of the closing brace and the range selects in between
|
|
get indented to end_indent + 2.
|
|
|
|
'''
|
|
assert 0 <= end_indent
|
|
|
|
ranges = []
|
|
cur_range_start = indices[0]
|
|
cur_range_end = indices[0]
|
|
for idx in indices[1:]:
|
|
if idx == cur_range_end + 1 and cur_range_start <= cur_range_end:
|
|
cur_range_end += 1
|
|
continue
|
|
if idx == cur_range_end - 1 and cur_range_start >= cur_range_end:
|
|
cur_range_end -= 1
|
|
continue
|
|
ranges.append((cur_range_start, cur_range_end))
|
|
cur_range_start = idx
|
|
cur_range_end = idx
|
|
ranges.append((cur_range_start, cur_range_end))
|
|
|
|
items = []
|
|
for range_start, range_end in ranges:
|
|
if range_start == range_end:
|
|
select = str(range_start)
|
|
else:
|
|
select = '{}:{}'.format(range_start, range_end)
|
|
items.append('{}[{}]'.format(sig_name, select))
|
|
|
|
if len(items) == 1:
|
|
return items[0]
|
|
|
|
item_indent = '\n' + (' ' * (end_indent + 2))
|
|
|
|
acc = ['{', item_indent, items[0]]
|
|
for item in items[1:]:
|
|
acc += [',', item_indent, item]
|
|
acc += ['\n', ' ' * end_indent, '}']
|
|
return ''.join(acc)
|
|
|
|
|
|
def is_rom_ctrl(modules):
|
|
'''Return true if rom_ctrl (and thus boot-up rom integrity checking)
|
|
exists in the design
|
|
'''
|
|
for m in modules:
|
|
if m['type'] == 'rom_ctrl':
|
|
return True
|
|
|
|
return False
|