| # 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_direction # 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 is_shadowed_port(block: IpBlock, port: str) -> bool: |
| """Return boolean indication whether a port is a shadow reset port |
| """ |
| shadowed_port = block.clocking.primary.reset if block.has_shadowed_reg() \ |
| else None |
| |
| return port == shadowed_port |
| |
| |
| def shadow_name(name: str) -> str: |
| """Return the appropriate shadow reset name based on port name |
| """ |
| match = re.match(r'^rst_([A-Za-z0-9_]+)_ni?', name) |
| if match: |
| return f'rst_{match.group(1)}_shadowed_ni' |
| else: |
| return 'rst_shadowed_ni' |
| |
| |
| def get_reset_path(top: object, reset: str, shadow_sel: bool = False): |
| """Return the appropriate reset path given name |
| """ |
| return top['resets'].get_path(reset['name'], reset['domain'], shadow_sel) |
| |
| |
| def get_reset_lpg_path(top: object, reset: str, shadow_sel: bool = False, domain: bool = None): |
| """Return the appropriate LPG reset path given name |
| """ |
| if domain is not None: |
| return top['resets'].get_lpg_path(reset['name'], domain, shadow_sel) |
| else: |
| return top['resets'].get_lpg_path(reset['name'], reset['domain'], shadow_sel) |
| |
| |
| def get_unused_resets(top): |
| """Return dict of unused resets and associated domain |
| """ |
| return top['resets'].get_unused_resets(top['power']['domains']) |
| |
| |
| def get_templated_modules(top): |
| """Returns list of all templated modules. |
| """ |
| return [m['type'] for m in top['module'] if is_templated(m)] |
| |
| |
| def get_ipgen_modules(top): |
| """Returns list of all ipgen modules. |
| """ |
| return [m['type'] for m in top['module'] if is_ipgen(m)] |
| |
| |
| def get_top_reggen_modules(top): |
| """Returns list of all ipgen modules. |
| """ |
| return [m['type'] for m in top['module'] if is_top_reggen(m)] |
| |
| |
| def is_templated(module): |
| """Returns an indication where a particular module is templated |
| """ |
| return module.get('attr') in ["templated"] |
| |
| |
| def is_ipgen(module): |
| """Returns an indication where a particular module is ipgen |
| """ |
| return module.get('attr') in ["ipgen"] |
| |
| |
| def is_top_reggen(module): |
| """Returns an indication where a particular module is NOT templated |
| and requires top level specific reggen |
| """ |
| return module.get('attr') in ["reggen_top", "reggen_only"] |
| |
| |
| def is_reggen_only(module): |
| """Returns an indication where a particular module is NOT templated, |
| requires top level specific reggen and is NOT instantiated in the |
| top |
| """ |
| return module.get('attr') == "reggen_only" |
| |
| |
| 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", "ipgen", "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]: |
| |
| 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 next power of 2. |
| size_byte = 1 << (bytes_used - 1).bit_length() |
| |
| 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 |
| |
| |
| def is_lc_ctrl(modules): |
| '''Return true if lc_ctrl exists in the design |
| ''' |
| for m in modules: |
| if m['type'] == 'lc_ctrl': |
| return True |
| |
| return False |