| # 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 |
| from collections import OrderedDict |
| from copy import deepcopy |
| from math import ceil, log2 |
| from typing import Dict, List, Union, Tuple |
| |
| from topgen import c, lib, strong_random |
| from .clocks import Clocks |
| from .resets import Resets |
| from reggen.ip_block import IpBlock |
| from reggen.params import LocalParam, Parameter, RandParameter, MemSizeParameter |
| |
| |
| def _get_random_data_hex_literal(width): |
| """ Fetch 'width' random bits and return them as hex literal""" |
| width = int(width) |
| literal_str = hex(strong_random.getrandbits(width)) |
| return literal_str |
| |
| |
| def _get_random_perm_hex_literal(numel): |
| """ Compute a random permutation of 'numel' elements and |
| return as packed hex literal""" |
| num_elements = int(numel) |
| width = int(ceil(log2(num_elements))) |
| idx = [x for x in range(num_elements)] |
| strong_random.shuffle(idx) |
| literal_str = "" |
| for k in idx: |
| literal_str += format(k, '0' + str(width) + 'b') |
| # convert to hex for space efficiency |
| literal_str = hex(int(literal_str, 2)) |
| return literal_str |
| |
| |
| def elaborate_instances(top, name_to_block: Dict[str, IpBlock]): |
| '''Add additional fields to the elements of top['module'] |
| |
| These elements represent instantiations of IP blocks. This function adds |
| extra fields to them to carry across information from the IpBlock objects |
| that represent the blocks being instantiated. See elaborate_instance for |
| more details of what gets added. |
| |
| ''' |
| |
| for instance in top['module']: |
| block = name_to_block[instance['type']] |
| elaborate_instance(instance, block) |
| |
| |
| def elaborate_instance(instance, block: IpBlock): |
| """Add additional fields to a single instance of a module. |
| |
| instance is the instance to be filled in. block is the block that it's |
| instantiating. |
| |
| Altered fields: |
| - param_list (list of parameters for the instance) |
| - inter_signal_list (list of inter-module signals) |
| - base_addrs (a map from interface name to its base address) |
| |
| Removed fields: |
| - base_addr (this is reflected in base_addrs) |
| |
| """ |
| |
| # create an empty dict if nothing is there |
| if "param_decl" not in instance: |
| instance["param_decl"] = {} |
| |
| mod_name = instance["name"] |
| cc_mod_name = c.Name.from_snake_case(mod_name).as_camel_case() |
| |
| # Check to see if all declared parameters exist |
| param_decl_accounting = [decl for decl in instance["param_decl"].keys()] |
| |
| # param_list |
| new_params = [] |
| for param in block.params.by_name.values(): |
| if isinstance(param, LocalParam): |
| # Remove local parameters. |
| continue |
| |
| new_param = param.as_dict() |
| |
| param_expose = param.expose if isinstance(param, Parameter) else False |
| |
| # assign an empty entry if this is not present |
| if "memory" not in instance: |
| instance["memory"] = {} |
| |
| # Check for security-relevant parameters that are not exposed, |
| # adding a top-level name. |
| if param.name.lower().startswith("sec") and not param_expose: |
| log.warning("{} has security-critical parameter {} " |
| "not exposed to top".format( |
| mod_name, param.name)) |
| |
| # Move special prefixes to the beginnining of the parameter name. |
| param_prefixes = ["Sec", "RndCnst", "MemSize"] |
| name_top = cc_mod_name + param.name |
| for prefix in param_prefixes: |
| if not param.name.startswith(prefix): |
| continue |
| else: |
| if param.name == prefix: |
| raise ValueError(f'Module instance {mod_name} has a ' |
| f'parameter {param.name} that is equal ' |
| f'to prefix {prefix}.') |
| |
| if re.match(prefix + '[A-Z].+$', param.name): |
| name_top = (prefix + cc_mod_name + |
| param.name[len(prefix):]) |
| break |
| |
| new_param['name_top'] = name_top |
| |
| # Generate random bits or permutation, if needed |
| if isinstance(param, RandParameter): |
| if param.randtype == 'data': |
| new_default = _get_random_data_hex_literal(param.randcount) |
| # Effective width of the random vector |
| randwidth = param.randcount |
| else: |
| assert param.randtype == 'perm' |
| new_default = _get_random_perm_hex_literal(param.randcount) |
| # Effective width of the random vector |
| randwidth = param.randcount * ceil(log2(param.randcount)) |
| |
| new_param['default'] = new_default |
| new_param['randwidth'] = randwidth |
| |
| elif isinstance(param, MemSizeParameter): |
| key = param.name[7:].lower() |
| # Set the parameter to the specified memory size. |
| if key in instance["memory"]: |
| new_default = int(instance["memory"][key]["size"], 0) |
| new_param['default'] = new_default |
| else: |
| log.error("Missing memory configuration for " |
| "memory {} in instance {}" |
| .format(key, instance["name"])) |
| |
| # if this exposed parameter is listed in the `param_decl` dict, |
| # override its default value. |
| elif param.name in instance["param_decl"].keys(): |
| new_param['default'] = instance["param_decl"][param.name] |
| # remove the parameter from the accounting dict |
| param_decl_accounting.remove(param.name) |
| |
| new_params.append(new_param) |
| |
| instance["param_list"] = new_params |
| |
| # for each module declaration, check to see that the parameter actually exists |
| # and can be set |
| for decl in param_decl_accounting: |
| log.error("{} is not a valid parameter of {} that can be " |
| "set from top level".format(decl, block.name)) |
| |
| # These objects get added-to in place by code in intermodule.py, so we have |
| # to convert and copy them here. |
| instance["inter_signal_list"] = [s.as_dict() for s in block.inter_signals] |
| |
| # An instance must either have a 'base_addr' address or a 'base_addrs' |
| # address, but can't have both. |
| base_addrs = instance.get('base_addrs') |
| if base_addrs is None: |
| if 'base_addr' not in instance: |
| log.error('Instance {!r} has neither a base_addr ' |
| 'nor a base_addrs field.' |
| .format(instance['name'])) |
| else: |
| # If the instance has a base_addr field, make sure that the block |
| # has just one device interface. |
| if len(block.reg_blocks) != 1: |
| log.error('Instance {!r} has a base_addr field but it ' |
| 'instantiates the block {!r}, which has {} ' |
| 'device interfaces.' |
| .format(instance['name'], |
| block.name, len(block.reg_blocks))) |
| else: |
| if_name = next(iter(block.reg_blocks)) |
| base_addrs = {if_name: instance['base_addr']} |
| |
| # Fill in a bogus base address (we don't have proper error handling, so |
| # have to do *something*) |
| if base_addrs is None: |
| base_addrs = {None: 0} |
| |
| instance['base_addrs'] = base_addrs |
| else: |
| if 'base_addr' in instance: |
| log.error('Instance {!r} has both a base_addr ' |
| 'and a base_addrs field.' |
| .format(instance['name'])) |
| |
| # Since the instance already has a base_addrs field, make sure that |
| # it's got the same set of keys as the name of the interfaces in the |
| # block. |
| inst_if_names = set(base_addrs.keys()) |
| block_if_names = set(block.reg_blocks.keys()) |
| if block_if_names != inst_if_names: |
| log.error('Instance {!r} has a base_addrs field with keys {} ' |
| 'but the block it instantiates ({!r}) has device ' |
| 'interfaces {}.' |
| .format(instance['name'], inst_if_names, |
| block.name, block_if_names)) |
| |
| if 'base_addr' in instance: |
| del instance['base_addr'] |
| |
| |
| # TODO: Replace this part to be configurable from Hjson or template |
| predefined_modules = { |
| "corei": "rv_core_ibex", |
| "cored": "rv_core_ibex" |
| } |
| |
| |
| def is_xbar(top, name): |
| """Check if the given name is crossbar |
| """ |
| xbars = list(filter(lambda node: node["name"] == name, top["xbar"])) |
| if len(xbars) == 0: |
| return False, None |
| |
| if len(xbars) > 1: |
| log.error("Matching crossbar {} is more than one.".format(name)) |
| raise SystemExit() |
| |
| return True, xbars[0] |
| |
| |
| def xbar_addhost(top, xbar, host): |
| """Add host nodes information |
| |
| - xbar: bool, true if the host port is from another Xbar |
| """ |
| # Check and fetch host if exists in nodes |
| obj = list(filter(lambda node: node["name"] == host, xbar["nodes"])) |
| if len(obj) == 0: |
| log.warning( |
| "host %s doesn't exist in the node list. Using default values" % |
| host) |
| obj = OrderedDict([ |
| ("name", host), |
| ("clock", xbar['clock']), |
| ("reset", xbar['reset']), |
| ("type", "host"), |
| ("inst_type", ""), |
| ("stub", False), |
| # The default matches RTL default |
| # pipeline_byp is don't care if pipeline is false |
| ("pipeline", True), |
| ("req_fifo_pass", True), |
| ("rsp_fifo_pass", True) |
| ]) |
| xbar["nodes"].append(obj) |
| return |
| |
| xbar_bool, xbar_h = is_xbar(top, host) |
| if xbar_bool: |
| log.info("host {} is a crossbar. Nothing to deal with.".format(host)) |
| |
| obj[0]["xbar"] = xbar_bool |
| |
| if 'clock' not in obj[0]: |
| obj[0]["clock"] = xbar['clock'] |
| |
| if 'reset' not in obj[0]: |
| obj[0]["reset"] = xbar["reset"] |
| |
| obj[0]["stub"] = False |
| obj[0]["inst_type"] = predefined_modules[ |
| host] if host in predefined_modules else "" |
| obj[0]["pipeline"] = obj[0]["pipeline"] if "pipeline" in obj[0] else True |
| obj[0]["req_fifo_pass"] = obj[0]["req_fifo_pass"] if obj[0][ |
| "pipeline"] and "req_fifo_pass" in obj[0] else True |
| obj[0]["rsp_fifo_pass"] = obj[0]["rsp_fifo_pass"] if obj[0][ |
| "pipeline"] and "rsp_fifo_pass" in obj[0] else True |
| |
| |
| def process_pipeline_var(node): |
| """Add device nodes pipeline / pipeline_byp information |
| |
| - Supply a default of true / true if not defined by xbar |
| """ |
| node["pipeline"] = node["pipeline"] if "pipeline" in node else True |
| node["req_fifo_pass"] = node[ |
| "req_fifo_pass"] if "req_fifo_pass" in node else True |
| node["req_fifo_pass"] = node[ |
| "req_fifo_pass"] if "req_fifo_pass" in node else True |
| |
| |
| def xbar_adddevice(top: Dict[str, object], |
| name_to_block: Dict[str, IpBlock], |
| xbar: Dict[str, object], |
| other_xbars: List[str], |
| device: str) -> None: |
| """Add or amend an entry in xbar['nodes'] to represent the device interface |
| |
| - clock: comes from module if exist, use xbar default otherwise |
| - reset: comes from module if exist, use xbar default otherwise |
| - inst_type: comes from module or memory if exist. |
| - base_addr: comes from module or memory, or assume rv_plic? |
| - size_byte: comes from module or memory |
| - xbar: bool, true if the device port is another xbar |
| - stub: There is no backing module / memory, instead a tlul port |
| is created and forwarded above the current hierarchy |
| """ |
| device_parts = device.split('.', 1) |
| device_base = device_parts[0] |
| device_ifname = device_parts[1] if len(device_parts) > 1 else None |
| |
| # Try to find a block or memory instance with name device_base. Object |
| # names should be unique, so there should never be more than one hit. |
| instances = [ |
| node for node in top["module"] + top["memory"] |
| if node['name'] == device_base |
| ] |
| assert len(instances) <= 1 |
| inst = instances[0] if instances else None |
| |
| # Try to find a node in the crossbar called device. Node names should be |
| # unique, so there should never be more than one hit. |
| nodes = [ |
| node for node in xbar['nodes'] |
| if node['name'] == device |
| ] |
| assert len(nodes) <= 1 |
| node = nodes[0] if nodes else None |
| |
| log.info("Handling xbar device {} (matches instance? {}; matches node? {})" |
| .format(device, inst is not None, node is not None)) |
| |
| # case 1: another xbar --> check in xbar list |
| if node is None and device in other_xbars: |
| log.error( |
| "Another crossbar %s needs to be specified in the 'nodes' list" % |
| device) |
| return |
| |
| # If there is no module or memory with the right name, this might still be |
| # ok: we might be connecting to another crossbar or to a predefined module. |
| if inst is None: |
| # case 1: Crossbar handling |
| if device in other_xbars: |
| log.info( |
| "device {} in Xbar {} is connected to another Xbar".format( |
| device, xbar["name"])) |
| assert node is not None |
| node["xbar"] = True |
| node["stub"] = False |
| process_pipeline_var(node) |
| return |
| |
| # case 2: predefined_modules (debug_mem, rv_plic) |
| # TODO: Find configurable solution not from predefined but from object? |
| if device in predefined_modules: |
| log.error("device %s shouldn't be host type" % device) |
| |
| return |
| |
| # case 3: not defined |
| # Crossbar check |
| log.error("Device %s doesn't exist in 'module', 'memory', predefined, " |
| "or as a node object" % device) |
| return |
| |
| # If we get here, inst points an instance of some block or memory. It |
| # shouldn't point at a crossbar (because that would imply a naming clash) |
| assert device_base not in other_xbars |
| base_addr, size_byte = lib.get_base_and_size(name_to_block, |
| inst, device_ifname) |
| addr_range = {"base_addr": hex(base_addr), "size_byte": hex(size_byte)} |
| |
| stub = not lib.is_inst(inst) |
| |
| if node is None: |
| log.error('Cannot connect to {!r} because ' |
| 'the crossbar defines no node for {!r}.' |
| .format(device, device_base)) |
| return |
| |
| node["inst_type"] = inst["type"] |
| node["addr_range"] = [addr_range] |
| node["xbar"] = False |
| node["stub"] = stub |
| process_pipeline_var(node) |
| |
| |
| def amend_xbar(top: Dict[str, object], |
| name_to_block: Dict[str, IpBlock], |
| xbar: Dict[str, object]): |
| """Amend crossbar informations to the top list |
| |
| Amended fields |
| - clock: Adopt from module clock if exists |
| - inst_type: Module instance some module will be hard-coded |
| the tool searches module list and memory list then put here |
| - base_addr: from top["module"] |
| - size: from top["module"] |
| """ |
| xbar_list = [x["name"] for x in top["xbar"]] |
| if not xbar["name"] in xbar_list: |
| log.info( |
| "Xbar %s doesn't belong to the top %s. Check if the xbar doesn't need" |
| % (xbar["name"], top["name"])) |
| return |
| |
| topxbar = list( |
| filter(lambda node: node["name"] == xbar["name"], top["xbar"]))[0] |
| |
| topxbar["connections"] = deepcopy(xbar["connections"]) |
| if "nodes" in xbar: |
| topxbar["nodes"] = deepcopy(xbar["nodes"]) |
| else: |
| topxbar["nodes"] = [] |
| |
| # xbar primary clock and reset |
| topxbar["clock"] = xbar["clock_primary"] |
| topxbar["reset"] = xbar["reset_primary"] |
| |
| # Build nodes from 'connections' |
| device_nodes = set() |
| for host, devices in xbar["connections"].items(): |
| # add host first |
| xbar_addhost(top, topxbar, host) |
| |
| # add device if doesn't exist |
| device_nodes.update(devices) |
| |
| other_xbars = [x["name"] |
| for x in top["xbar"] |
| if x["name"] != xbar["name"]] |
| |
| log.info(device_nodes) |
| for device in device_nodes: |
| xbar_adddevice(top, name_to_block, topxbar, other_xbars, device) |
| |
| |
| def xbar_cross(xbar, xbars): |
| """Check if cyclic dependency among xbars |
| |
| And gather the address range for device port (to another Xbar) |
| |
| @param node_name if not "", the function only search downstream |
| devices starting from the node_name |
| @param visited The nodes it visited to reach this port. If any |
| downstream port from node_name in visited, it means |
| circular path exists. It should be fatal error. |
| """ |
| # Step 1: Visit devices (gather the address range) |
| log.info("Processing circular path check for {}".format(xbar["name"])) |
| addr = [] |
| for node in [ |
| x for x in xbar["nodes"] |
| if x["type"] == "device" and "xbar" in x and x["xbar"] is False |
| ]: |
| addr.extend(node["addr_range"]) |
| |
| # Step 2: visit xbar device ports |
| xbar_nodes = [ |
| x for x in xbar["nodes"] |
| if x["type"] == "device" and "xbar" in x and x["xbar"] is True |
| ] |
| |
| # Now call function to get the device range |
| # the node["name"] is used to find the host_xbar and its connection. The |
| # assumption here is that there's only one connection from crossbar A to |
| # crossbar B. |
| # |
| # device_xbar is the crossbar has a device port with name as node["name"]. |
| # host_xbar is the crossbar has a host port with name as node["name"]. |
| for node in xbar_nodes: |
| xbar_addr = xbar_cross_node(node["name"], xbar, xbars, visited=[]) |
| node["addr_range"] = xbar_addr |
| |
| |
| def xbar_cross_node(node_name, device_xbar, xbars, visited=[]): |
| # 1. Get the connected xbar |
| host_xbars = [x for x in xbars if x["name"] == node_name] |
| assert len(host_xbars) == 1 |
| host_xbar = host_xbars[0] |
| |
| log.info("Processing node {} in Xbar {}.".format(node_name, |
| device_xbar["name"])) |
| result = [] # [(base_addr, size), .. ] |
| # Sweep the devices using connections and gather the address. |
| # If the device is another xbar, call recursive |
| visited.append(host_xbar["name"]) |
| devices = host_xbar["connections"][device_xbar["name"]] |
| |
| for node in host_xbar["nodes"]: |
| if not node["name"] in devices: |
| continue |
| if "xbar" in node and node["xbar"] is True: |
| if "addr_range" not in node: |
| # Deeper dive into another crossbar |
| xbar_addr = xbar_cross_node(node["name"], host_xbar, xbars, |
| visited) |
| node["addr_range"] = xbar_addr |
| |
| result.extend(deepcopy(node["addr_range"])) |
| |
| visited.pop() |
| |
| return result |
| |
| |
| # find the first instance name of a given type |
| def _find_module_name(modules, module_type): |
| for m in modules: |
| if m['type'] == module_type: |
| return m['name'] |
| |
| return None |
| |
| |
| def _get_clock_group_name(clk: Union[str, OrderedDict], default_ep_grp) -> Tuple[str, str]: |
| """Return the clock group of a particular clock connection |
| |
| Checks whether there is a specific clock group associated with this |
| connection and returns its name. If not, this returns the default clock |
| group of the clock end point. |
| """ |
| # If the value of a particular connection is a dict, |
| # there are additional attributes to explore |
| if isinstance(clk, str): |
| group_name = default_ep_grp |
| src_name = clk |
| else: |
| assert isinstance(clk, Dict) |
| group_name = clk.get('group', default_ep_grp) |
| src_name = clk['clock'] |
| |
| return group_name, src_name |
| |
| |
| def extract_clocks(top: OrderedDict): |
| '''Add clock exports to top and connections to endpoints |
| |
| This function sets up all the clock-related machinery that is needed to |
| generate the (templated) clkmgr code. This runs before we load up IP blocks |
| with reggen, so can only see top-level configuration. |
| |
| By default each end point (peripheral, memory etc) is in the same clock group. |
| However, it is possible to define the group attribute per clock if required. |
| |
| ''' |
| clocks = top['clocks'] |
| assert isinstance(clocks, Clocks) |
| |
| exported_clks = OrderedDict() |
| |
| for ep in top['module'] + top['memory'] + top['xbar']: |
| clock_connections = OrderedDict() |
| |
| # Ensure each module has a default case |
| export_if = ep.get('clock_reset_export', []) |
| |
| # The clock group attribute in an end point sets the defaut |
| # group for every clock in that end point. |
| # |
| # However, the end point can also override specific clocks to |
| # different groups inside clock_srcs. This is generally not |
| # recommended as it is better to stay consistent. However |
| # if needed, the method is available. |
| ep['clock_group'] = 'secure' if 'clock_group' not in ep else ep[ |
| 'clock_group'] |
| ep_grp = ep['clock_group'] |
| |
| # end point names and clocks |
| ep_name = ep['name'] |
| |
| for port, clk in ep['clock_srcs'].items(): |
| |
| group_name, src_name = _get_clock_group_name(clk, ep_grp) |
| |
| group = clocks.groups[group_name] |
| |
| name = '' |
| hier_name = clocks.hier_paths[group.src] |
| |
| if group.src == 'ext': |
| name = "{}_i".format(src_name) |
| |
| elif group.unique: |
| # new unqiue clock name |
| name = "{}_{}".format(src_name, ep_name) |
| |
| else: |
| # new group clock name |
| name = "{}_{}".format(src_name, group_name) |
| |
| clk_name = "clk_" + name |
| |
| # add clock to a particular group |
| clk_sig = clocks.add_clock_to_group(group, clk_name, src_name) |
| clk_sig.add_endpoint(ep_name, port) |
| |
| # add clock connections |
| clock_connections[port] = hier_name + clk_name |
| |
| # clocks for this module are exported |
| for intf in export_if: |
| log.info("{} export clock name is {}".format(ep_name, name)) |
| |
| # create dict entry if it does not exit |
| if intf not in exported_clks: |
| exported_clks[intf] = OrderedDict() |
| |
| # if first time encounter end point, declare |
| if ep_name not in exported_clks[intf]: |
| exported_clks[intf][ep_name] = [] |
| |
| # append clocks |
| exported_clks[intf][ep_name].append(name) |
| |
| # Add to endpoint structure |
| ep['clock_connections'] = clock_connections |
| |
| # add entry to top level json |
| top['exported_clks'] = exported_clks |
| |
| |
| def connect_clocks(top, name_to_block: Dict[str, IpBlock]): |
| clocks = top['clocks'] |
| assert isinstance(clocks, Clocks) |
| |
| # add entry to inter_module automatically |
| clkmgr_name = _find_module_name(top['module'], 'clkmgr') |
| external = top['inter_module']['external'] |
| for intf in top['exported_clks']: |
| external[f'{clkmgr_name}.clocks_{intf}'] = f"clks_{intf}" |
| |
| typed_clocks = clocks.typed_clocks() |
| |
| # Set up intermodule connections for idle clocks. Iterating over |
| # hint_names() here ensures that we visit the clocks in the same order as |
| # the code that generates the enumeration in clkmgr_pkg.sv: important, |
| # since the order that we add entries to clkmgr_idle below gives the index |
| # of each hint in the "idle" signal bundle. These *must* match, or we'll |
| # have hard-to-debug mis-connections. |
| clkmgr_idle = [] |
| for clk_name in typed_clocks.hint_names().keys(): |
| sig = typed_clocks.hint_clks[clk_name] |
| ep_names = list(set(ep_name for ep_name, ep_port in sig.endpoints)) |
| if len(ep_names) != 1: |
| raise ValueError(f'There are {len(ep_names)} end-points connected ' |
| f'to the {sig.name} clock: {ep_names}. Where ' |
| f'should the idle signal come from?') |
| ep_name = ep_names[0] |
| |
| # We've got the name of the endpoint, but that's not enough: we need to |
| # find the corresponding IpBlock. To do this, we have to do a (linear) |
| # search through top['module'] to find the instance that matches the |
| # endpoint, then use that instance's type as a key in name_to_block. |
| ep_inst = None |
| for inst in top['module']: |
| if inst['name'] == ep_name: |
| ep_inst = inst |
| break |
| if ep_inst is None: |
| raise ValueError(f'No module instance with name {ep_name}: only ' |
| f'modules can have hint clocks. Is this a ' |
| f'crossbar or a memory?') |
| |
| ip_block = name_to_block[ep_inst['type']] |
| |
| # Walk through the clocking items for the block to find the one that |
| # defines each of the ports. |
| idle_signal = None |
| for ep_name, ep_port in sig.endpoints: |
| ep_idle = None |
| for item in ip_block.clocking.items: |
| if item.clock != ep_port: |
| continue |
| if item.idle is None: |
| raise ValueError(f'Cannot connect the {sig.name} clock to ' |
| f'port {ep_port} of {ep_name}. This is a ' |
| f'hint clock, but the clocking item on ' |
| f'the module defines no idle signal.') |
| if idle_signal is not None and item.idle != idle_signal: |
| raise ValueError(f'Cannot connect the {sig.name} clock to ' |
| f'port {ep_port} of {ep_name}. We ' |
| f'already have a connection to another ' |
| f'clock signal which has an assocated ' |
| f'idle signal called {idle_signal}, but ' |
| f'this clocking item has an idle signal ' |
| f'called {item.idle}.') |
| ep_idle = item.idle |
| break |
| if ep_idle is None: |
| raise ValueError(f'Cannot connect the {sig.name} clock to ' |
| f'port {ep_port} of {ep_name}: no such ' |
| f'clocking item.') |
| idle_signal = ep_idle |
| assert idle_signal is not None |
| |
| # At this point, there's a slight problem: we use names like "idle_o" |
| # for signals in the hjson, but the inter-module list expects names |
| # like "idle". Drop the trailing "_o". |
| if not idle_signal.endswith('_o'): |
| raise ValueError(f'Idle signal for {ep_port} of {ep_name} is ' |
| f'{idle_signal}, which is missing the expected ' |
| f'"_o" suffix.') |
| idle_signal = idle_signal[:-2] |
| |
| clkmgr_idle.append(ep_name + '.' + idle_signal) |
| |
| top['inter_module']['connect']['{}.idle'.format(clkmgr_name)] = clkmgr_idle |
| |
| |
| def amend_resets(top, name_to_block): |
| """Generate exported reset structure and automatically connect to |
| intermodule. |
| |
| Also iterate through and determine need for shadowed reset and |
| domains. |
| """ |
| |
| top_resets = Resets(top['resets'], top['clocks']) |
| rstmgr_name = _find_module_name(top['module'], 'rstmgr') |
| |
| # Generate exported reset list |
| exported_rsts = OrderedDict() |
| for module in top["module"]: |
| |
| block = name_to_block[module['type']] |
| block_clock = block.get_primary_clock() |
| primary_reset = module['reset_connections'][block_clock.reset] |
| |
| # shadowed determination |
| if block.has_shadowed_reg(): |
| top_resets.mark_reset_shadowed(primary_reset['name']) |
| |
| # domain determination |
| for r in block.clocking.items: |
| if r.reset: |
| reset = module['reset_connections'][r.reset] |
| top_resets.add_reset_domain(reset['name'], reset['domain']) |
| |
| # This code is here to ensure if amend_clocks/resets switched order |
| # everything would still work |
| export_if = module.get('clock_reset_export', []) |
| |
| # There may be multiple export interfaces |
| for intf in export_if: |
| # create dict entry if it does not exit |
| if intf not in exported_rsts: |
| exported_rsts[intf] = OrderedDict() |
| |
| # grab directly from reset_connections definition |
| rsts = [rst for rst in module['reset_connections'].values()] |
| exported_rsts[intf][module['name']] = rsts |
| |
| # ensure xbar resets are also covered. |
| # unless otherwise stated, xbars always fall into the default power domain. |
| for xbar in top["xbar"]: |
| for reset in xbar['reset_connections'].values(): |
| top_resets.add_reset_domain(reset['name'], top['power']['default']) |
| |
| # add entry to top level json |
| top['exported_rsts'] = exported_rsts |
| |
| # add entry to inter_module automatically |
| for intf in top['exported_rsts']: |
| top['inter_module']['external']['{}.resets_{}'.format( |
| rstmgr_name, intf)] = "rsts_{}".format(intf) |
| |
| # reset class objects |
| top["resets"] = top_resets |
| |
| |
| def create_alert_lpgs(top, name_to_block: Dict[str, IpBlock]): |
| '''Loop over modules and determine number of unique LPGs''' |
| num_lpg = 0 |
| lpg_dict = {} |
| top['alert_lpgs'] = [] |
| |
| # ensure the object is already generated before we attempt to use it |
| assert isinstance(top['clocks'], Clocks) |
| clock_groups = top['clocks'].make_clock_to_group() |
| for module in top["module"]: |
| # the alert senders are attached to the primary clock of this block, |
| # so let's start by getting that primary clock port of an IP (we need |
| # that to look up the clock connection at the top-level). |
| block = name_to_block[module['type']] |
| block_clock = block.get_primary_clock() |
| primary_reset = module['reset_connections'][block_clock.reset] |
| |
| # for the purposes of alert handler LPGs, we need to know: |
| # 1) the clock group of the primary clock |
| # 2) the primary reset name |
| # 3) the domain of the primary reset |
| # |
| # 1) figure out the clock group assignment of the primary clock |
| # Get the full clock name and split the hierarchy path, getting the last element |
| clk = module['clock_connections'][block_clock.clock] |
| clk = clk.split(".")[-1] |
| |
| # Discover what clock group we are related to |
| clock_group = clock_groups[clk] |
| |
| # 2-3) get reset info |
| reset_name = primary_reset['name'] |
| reset_domain = primary_reset['domain'] |
| |
| # using this info, we can create an LPG identifier |
| # and uniquify it via a dict. |
| lpg_name = '_'.join([clock_group.name, reset_name, reset_domain]) |
| unique_cg = clock_group.unique and clock_group.sw_cg != "no" |
| |
| # if clock group is "unique", add some uniquification to the tag |
| lpg_name = f"{module['name']}_{lpg_name}" if unique_cg else lpg_name |
| if lpg_name not in lpg_dict: |
| lpg_dict.update({lpg_name: num_lpg}) |
| # since the alert handler can tolerate timing delays on LPG |
| # indication signals, we can just use the clock / reset signals |
| # of the first block that belongs to a new unique LPG. |
| clock = module['clock_connections'][block_clock.clock] |
| top['alert_lpgs'].append({ |
| 'name': lpg_name, |
| 'clock_group': clock_group, |
| 'clock_connection': clock, |
| 'reset_connection': primary_reset |
| }) |
| num_lpg += 1 |
| |
| # annotate all alerts of this module to use this LPG |
| for alert in top['alert']: |
| if alert['module_name'] == module['name']: |
| alert.update({ |
| 'lpg_name': lpg_name, |
| 'lpg_idx': lpg_dict[lpg_name] |
| }) |
| |
| |
| def ensure_interrupt_modules(top: OrderedDict, name_to_block: Dict[str, IpBlock]): |
| '''Populate top['interrupt_module'] if necessary |
| |
| Do this by adding each module in top['modules'] that defines at least one |
| interrupt. |
| |
| ''' |
| if 'interrupt_module' in top: |
| return |
| |
| modules = [] |
| for module in top['module']: |
| block = name_to_block[module['type']] |
| if block.interrupts: |
| modules.append(module['name']) |
| |
| top['interrupt_module'] = modules |
| |
| |
| def amend_interrupt(top: OrderedDict, name_to_block: Dict[str, IpBlock]): |
| """Check interrupt_module if exists, or just use all modules |
| """ |
| ensure_interrupt_modules(top, name_to_block) |
| |
| if "interrupt" not in top or top["interrupt"] == "": |
| top["interrupt"] = [] |
| |
| for m in top["interrupt_module"]: |
| ips = list(filter(lambda module: module["name"] == m, top["module"])) |
| if len(ips) == 0: |
| log.warning( |
| "Cannot find IP %s which is used in the interrupt_module" % m) |
| continue |
| |
| ip = ips[0] |
| block = name_to_block[ip['type']] |
| |
| log.info("Adding interrupts from module %s" % ip["name"]) |
| for signal in block.interrupts: |
| sig_dict = signal.as_nwt_dict('interrupt') |
| qual = lib.add_module_prefix_to_signal(sig_dict, |
| module=m.lower()) |
| top["interrupt"].append(qual) |
| |
| |
| def ensure_alert_modules(top: OrderedDict, name_to_block: Dict[str, IpBlock]): |
| '''Populate top['alert_module'] if necessary |
| |
| Do this by adding each module in top['modules'] that defines at least one |
| alert. |
| |
| ''' |
| if 'alert_module' in top: |
| return |
| |
| modules = [] |
| for module in top['module']: |
| block = name_to_block[module['type']] |
| if block.alerts: |
| modules.append(module['name']) |
| |
| top['alert_module'] = modules |
| |
| |
| def amend_alert(top: OrderedDict, name_to_block: Dict[str, IpBlock]): |
| """Check interrupt_module if exists, or just use all modules |
| """ |
| ensure_alert_modules(top, name_to_block) |
| |
| if "alert" not in top or top["alert"] == "": |
| top["alert"] = [] |
| |
| for m in top["alert_module"]: |
| ips = list(filter(lambda module: module["name"] == m, top["module"])) |
| if len(ips) == 0: |
| log.warning("Cannot find IP %s which is used in the alert_module" % |
| m) |
| continue |
| |
| ip = ips[0] |
| block = name_to_block[ip['type']] |
| |
| log.info("Adding alert from module %s" % ip["name"]) |
| # Note: we assume that all alerts are asynchronous in order to make the |
| # design homogeneous and more amenable to DV automation and synthesis |
| # constraint scripting. |
| for alert in block.alerts: |
| alert_dict = alert.as_nwt_dict('alert') |
| alert_dict['async'] = '1' |
| qual_sig = lib.add_module_prefix_to_signal(alert_dict, |
| module=m.lower()) |
| top["alert"].append(qual_sig) |
| |
| |
| def amend_wkup(topcfg: OrderedDict, name_to_block: Dict[str, IpBlock]): |
| |
| pwrmgr_name = _find_module_name(topcfg['module'], 'pwrmgr') |
| |
| if "wakeups" not in topcfg or topcfg["wakeups"] == "": |
| topcfg["wakeups"] = [] |
| |
| # create list of wakeup signals |
| for m in topcfg["module"]: |
| log.info("Adding wakeup from module %s" % m["name"]) |
| block = name_to_block[m['type']] |
| for signal in block.wakeups: |
| log.info("Adding signal %s" % signal.name) |
| topcfg["wakeups"].append({ |
| 'name': signal.name, |
| 'width': str(signal.bits.width()), |
| 'module': m["name"] |
| }) |
| |
| # add wakeup signals to pwrmgr connections |
| signal_names = [ |
| "{}.{}".format(s["module"].lower(), s["name"].lower()) |
| for s in topcfg["wakeups"] |
| ] |
| |
| topcfg["inter_module"]["connect"]["{}.wakeups".format(pwrmgr_name)] = signal_names |
| log.info("Intermodule signals: {}".format( |
| topcfg["inter_module"]["connect"])) |
| |
| |
| # Handle reset requests from modules |
| def amend_reset_request(topcfg: OrderedDict, |
| name_to_block: Dict[str, IpBlock]): |
| |
| pwrmgr_name = _find_module_name(topcfg['module'], 'pwrmgr') |
| |
| if "reset_requests" not in topcfg or topcfg["reset_requests"] == "": |
| topcfg["reset_requests"] = {} |
| topcfg["reset_requests"]["peripheral"] = [] |
| |
| # TODO: The reset_request_list of each module needs to be enhanced |
| # to support multiple types in the long run, then we can avoid |
| # hardwiring like this. |
| topcfg["reset_requests"]["int"] = [ |
| { |
| "name": "MainPwr", |
| "desc": "main power glitch reset request", |
| "module": "pwrmgr_aon" |
| }, |
| { |
| "name": "Esc", |
| "desc": "escalation reset request", |
| "module": "alert_handler" |
| } |
| ] |
| topcfg["reset_requests"]["debug"] = [ |
| { |
| "name": "Ndm", |
| "desc": "non-debug-module reset request", |
| "module": "rv_dm" |
| } |
| ] |
| |
| # create list of reset signals |
| for m in topcfg["module"]: |
| log.info("Adding reset requests from module %s" % m["name"]) |
| block = name_to_block[m['type']] |
| for signal in block.reset_requests: |
| log.info("Adding signal %s" % signal.name) |
| topcfg["reset_requests"]["peripheral"].append({ |
| 'name': signal.name, |
| 'width': str(signal.bits.width()), |
| 'module': m["name"], |
| 'desc': signal.desc |
| }) |
| |
| # add reset requests to pwrmgr connections |
| signal_names = [ |
| "{}.{}".format(s["module"].lower(), s["name"].lower()) |
| for s in topcfg["reset_requests"]["peripheral"] |
| ] |
| |
| topcfg["inter_module"]["connect"]["{}.rstreqs".format(pwrmgr_name)] = signal_names |
| log.info("Intermodule signals: {}".format( |
| topcfg["inter_module"]["connect"])) |
| |
| |
| def append_io_signal(temp: Dict, sig_inst: Dict) -> None: |
| '''Appends the signal to the correct list''' |
| if sig_inst['type'] == 'inout': |
| temp['inouts'].append(sig_inst) |
| if sig_inst['type'] == 'input': |
| temp['inputs'].append(sig_inst) |
| if sig_inst['type'] == 'output': |
| temp['outputs'].append(sig_inst) |
| |
| |
| def get_index_and_incr(ctrs: Dict, connection: str, io_dir: str) -> Dict: |
| '''Get correct index counter and increment''' |
| |
| if connection != 'muxed': |
| connection = 'dedicated' |
| |
| if io_dir in 'inout': |
| result = ctrs[connection]['inouts'] |
| ctrs[connection]['inouts'] += 1 |
| elif connection == 'muxed': |
| # For MIOs, the input/output arrays differ in RTL |
| # I.e., the input array contains {inputs, inouts}, whereas |
| # the output array contains {outputs, inouts}. |
| if io_dir == 'input': |
| result = ctrs[connection]['inputs'] + ctrs[connection]['inouts'] |
| ctrs[connection]['inputs'] += 1 |
| elif io_dir == 'output': |
| result = ctrs[connection]['outputs'] + ctrs[connection]['inouts'] |
| ctrs[connection]['outputs'] += 1 |
| else: |
| assert (0) # should not happen |
| else: |
| # For DIOs, the input/output arrays are identical in terms of index layout. |
| # Unused inputs are left unconnected and unused outputs are tied off. |
| if io_dir == 'input': |
| result = ctrs[connection]['inputs'] + ctrs[connection]['inouts'] |
| ctrs[connection]['inputs'] += 1 |
| elif io_dir == 'output': |
| result = (ctrs[connection]['outputs'] + |
| ctrs[connection]['inouts'] + |
| ctrs[connection]['inputs']) |
| ctrs[connection]['outputs'] += 1 |
| else: |
| assert (0) # should not happen |
| |
| return result |
| |
| |
| def amend_pinmux_io(top: Dict, name_to_block: Dict[str, IpBlock]): |
| """ Process pinmux/pinout configuration and assign available IOs |
| """ |
| pinmux = top['pinmux'] |
| pinout = top['pinout'] |
| targets = top['targets'] |
| |
| temp = {} |
| temp['inouts'] = [] |
| temp['inputs'] = [] |
| temp['outputs'] = [] |
| |
| for sig in pinmux['signals']: |
| # Get the signal information from the IP block type of this instance/ |
| mod_name = sig['instance'] |
| m = lib.get_module_by_name(top, mod_name) |
| |
| if m is None: |
| raise SystemExit("Module {} is not searchable.".format(mod_name)) |
| |
| block = name_to_block[m['type']] |
| |
| # If the signal is explicitly named. |
| if sig['port'] != '': |
| |
| # If this is a bus signal with explicit indexes. |
| if '[' in sig['port']: |
| name_split = sig['port'].split('[') |
| sig_name = name_split[0] |
| idx = int(name_split[1][:-1]) |
| else: |
| idx = -1 |
| sig_name = sig['port'] |
| |
| sig_inst = deepcopy(block.get_signal_by_name_as_dict(sig_name)) |
| |
| if idx >= 0 and idx >= sig_inst['width']: |
| raise SystemExit("Index {} is out of bounds for signal {}" |
| " with width {}.".format(idx, sig_name, sig_inst['width'])) |
| if idx == -1 and sig_inst['width'] != 1: |
| raise SystemExit("Bus signal {} requires an index.".format(sig_name)) |
| |
| # If we got this far we know that the signal is valid and exists. |
| # Augment this signal instance with additional information. |
| sig_inst.update({'idx': idx, |
| 'pad': sig['pad'], |
| 'attr': sig['attr'], |
| 'connection': sig['connection'], |
| 'desc': sig['desc']}) |
| sig_inst['name'] = mod_name + '_' + sig_inst['name'] |
| append_io_signal(temp, sig_inst) |
| |
| # Otherwise the name is a wildcard for selecting all available IO signals |
| # of this block and we need to extract them here one by one signals here. |
| else: |
| sig_list = deepcopy(block.get_signals_as_list_of_dicts()) |
| |
| for sig_inst in sig_list: |
| # If this is a multibit signal, unroll the bus and |
| # generate a single bit IO signal entry for each one. |
| if sig_inst['width'] > 1: |
| for idx in range(sig_inst['width']): |
| sig_inst_copy = deepcopy(sig_inst) |
| sig_inst_copy.update({'idx': idx, |
| 'pad': sig['pad'], |
| 'attr': sig['attr'], |
| 'connection': sig['connection'], |
| 'desc': sig['desc']}) |
| sig_inst_copy['name'] = sig['instance'] + '_' + sig_inst_copy['name'] |
| append_io_signal(temp, sig_inst_copy) |
| else: |
| sig_inst.update({'idx': -1, |
| 'pad': sig['pad'], |
| 'attr': sig['attr'], |
| 'connection': sig['connection'], |
| 'desc': sig['desc']}) |
| sig_inst['name'] = sig['instance'] + '_' + sig_inst['name'] |
| append_io_signal(temp, sig_inst) |
| |
| # Now that we've collected all input and output signals, |
| # we can go through once again and stack them into one unified |
| # list, and calculate MIO/DIO global indices. |
| pinmux['ios'] = (temp['inouts'] + |
| temp['inputs'] + |
| temp['outputs']) |
| |
| # Remember these counts to facilitate the RTL generation |
| pinmux['io_counts'] = {'dedicated': {'inouts': 0, 'inputs': 0, 'outputs': 0, 'pads': 0}, |
| 'muxed': {'inouts': 0, 'inputs': 0, 'outputs': 0, 'pads': 0}} |
| |
| for sig in pinmux['ios']: |
| glob_idx = get_index_and_incr(pinmux['io_counts'], sig['connection'], sig['type']) |
| sig['glob_idx'] = glob_idx |
| |
| # Calculate global indices for pads. |
| j = k = 0 |
| for pad in pinout['pads']: |
| if pad['connection'] == 'muxed': |
| pad['idx'] = j |
| j += 1 |
| else: |
| pad['idx'] = k |
| k += 1 |
| pinmux['io_counts']['muxed']['pads'] = j |
| pinmux['io_counts']['dedicated']['pads'] = k |
| |
| # For each target configuration, calculate the special signal indices. |
| known_muxed_pads = {} |
| for pad in pinout['pads']: |
| if pad['connection'] == 'muxed': |
| known_muxed_pads[pad['name']] = pad |
| |
| known_mapped_dio_pads = {} |
| for sig in pinmux['ios']: |
| if sig['connection'] in ['muxed', 'manual']: |
| continue |
| if sig['pad'] in known_mapped_dio_pads: |
| raise SystemExit('Cannot have multiple IOs mapped to the same DIO pad {}' |
| .format(sig['pad'])) |
| known_mapped_dio_pads[sig['pad']] = sig |
| |
| for target in targets: |
| for entry in target['pinmux']['special_signals']: |
| # If this is a muxed pad, the resolution is |
| # straightforward. I.e., we just assign the MIO index. |
| if entry['pad'] in known_muxed_pads: |
| entry['idx'] = known_muxed_pads[entry['pad']]['idx'] |
| # Otherwise we need to find out which DIO this pad is mapped to. |
| # Note that we can't have special_signals that are manual, since |
| # there needs to exist a DIO connection. |
| elif entry['pad'] in known_mapped_dio_pads: |
| # This index refers to the stacked {dio, mio} array |
| # on the chip-level, hence we have to add the amount of MIO pads. |
| idx = (known_mapped_dio_pads[entry['pad']]['glob_idx'] + |
| pinmux['io_counts']['muxed']['pads']) |
| entry['idx'] = idx |
| else: |
| assert (0) # Entry should be guaranteed to exist at this point |
| |
| |
| def merge_top(topcfg: OrderedDict, |
| name_to_block: Dict[str, IpBlock], |
| xbarobjs: OrderedDict) -> OrderedDict: |
| |
| # Combine ip cfg into topcfg |
| elaborate_instances(topcfg, name_to_block) |
| |
| # Create clock connections for each block |
| # Assign clocks into appropriate groups |
| # Note, elaborate_instances references clock information to establish async handling |
| # as part of alerts. |
| # amend_clocks(topcfg) |
| |
| # Combine the wakeups |
| amend_wkup(topcfg, name_to_block) |
| amend_reset_request(topcfg, name_to_block) |
| |
| # Combine the interrupt (should be processed prior to xbar) |
| amend_interrupt(topcfg, name_to_block) |
| |
| # Combine the alert (should be processed prior to xbar) |
| amend_alert(topcfg, name_to_block) |
| |
| # Creates input/output list in the pinmux |
| log.info("Processing PINMUX") |
| amend_pinmux_io(topcfg, name_to_block) |
| |
| # Combine xbar into topcfg |
| for xbar in xbarobjs: |
| amend_xbar(topcfg, name_to_block, xbar) |
| |
| # 2nd phase of xbar (gathering the devices address range) |
| for xbar in topcfg["xbar"]: |
| xbar_cross(xbar, topcfg["xbar"]) |
| |
| # Add path names to declared resets. |
| # Declare structure for exported resets. |
| amend_resets(topcfg, name_to_block) |
| |
| # remove unwanted fields 'debug_mem_base_addr' |
| topcfg.pop('debug_mem_base_addr', None) |
| |
| return topcfg |