tinyriscv/tools/regtool/topgen/lib.py

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