| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| """ |
| Register JSON validation |
| """ |
| |
| import logging as log |
| from collections import OrderedDict |
| |
| from .access import SWAccess, HWAccess |
| from .bits import Bits |
| from .field import Field |
| from .reg_block import RegBlock |
| from .register import Register |
| |
| |
| # Routine that can be used for Hjson object_pairs_hook |
| # The baseline is dict(pairs) i.e. construct a dictonary from pairs |
| # The usual is OrderedDict(pairs) which is redundant in latest python |
| # Both of these silently allow repeated keys, which this version detects |
| def checking_dict(pairs): |
| d = {} |
| for x in pairs: |
| if x[0] in d: |
| repkey = 'Repeated' + x[0] |
| log.warn("Repeated key " + x[0] + " added as " + repkey) |
| d[repkey] = x[1] |
| else: |
| d[x[0]] = x[1] |
| return d |
| |
| |
| # validating version of int(x, 0) |
| # returns int value, error flag |
| # if error flag is True value will be zero |
| def check_int(x, err_prefix, suppress_err_msg=False): |
| if isinstance(x, int): |
| return x, False |
| if x[0] == '0' and len(x) > 2: |
| if x[1] in 'bB': |
| validch = '01' |
| elif x[1] in 'oO': |
| validch = '01234567' |
| elif x[1] in 'xX': |
| validch = '0123456789abcdefABCDEF' |
| else: |
| if not suppress_err_msg: |
| log.error(err_prefix + |
| ": int must start digit, 0b, 0B, 0o, 0O, 0x or 0X") |
| return 0, True |
| for c in x[2:]: |
| if c not in validch: |
| if not suppress_err_msg: |
| log.error(err_prefix + ": Bad character " + c + " in " + x) |
| return 0, True |
| else: |
| if not x.isdecimal(): |
| if not suppress_err_msg: |
| log.error(err_prefix + ": Number not valid int " + x) |
| return 0, True |
| return int(x, 0), False |
| |
| |
| def check_bool(x, err_prefix): |
| """check_bool checks if input 'x' is one of the list: |
| "true", "false" |
| |
| It returns value as Bool type and Error condition. |
| """ |
| if isinstance(x, bool): |
| # if Bool returns as it is |
| return x, False |
| if not x.lower() in ["true", "false"]: |
| log.error(err_prefix + ": Bad field value " + x) |
| return False, True |
| else: |
| return (x.lower() == "true"), False |
| |
| |
| def check_ln(obj, x, withwidth, err_prefix): |
| error = 0 |
| if not isinstance(obj[x], list): |
| log.error(err_prefix + ' element ' + x + ' not a list') |
| return 1 |
| for y in obj[x]: |
| error += check_keys(y, ln_required, ln_optional if withwidth else {}, |
| {}, err_prefix + ' element ' + x) |
| if withwidth: |
| if 'width' in y: |
| w, err = check_int(y['width'], err_prefix + ' width in ' + x) |
| if err: |
| error += 1 |
| w = 1 |
| else: |
| w = 1 |
| y['width'] = str(w) |
| |
| return error |
| |
| |
| def check_lp(obj, x, err_prefix): |
| error = 0 |
| if not isinstance(obj[x], list): |
| log.error(err_prefix + ' element ' + x + ' not a list') |
| return 1 |
| |
| for y in obj[x]: |
| error += check_keys(y, lp_required, lp_optional, {}, |
| err_prefix + ' element ' + x) |
| |
| # If this is a random netlist constant, other attributes like local, default and expose |
| # are automatically set. Throw an error if they already exist in the dict. |
| randcount = int(y.setdefault('randcount', "0")) |
| randtype = y.setdefault('randtype', "none") |
| if randtype != "none": |
| |
| if randcount <= 0: |
| log.error(err_prefix + ' randwith for parameter ' + y['name'] + |
| ' must be greater > 0.') |
| return error + 1 |
| |
| if randtype not in ['perm', 'data']: |
| log.error(err_prefix + ' parameter ' + y['name'] + |
| ' has unknown randtype ' + randtype) |
| return error + 1 |
| |
| if y.get('type') is None: |
| log.error( |
| err_prefix + ' parameter ' + y['name'] + |
| ' has undefined type. ' |
| 'It is required to define the type in the IP package.') |
| return error + 1 |
| |
| if not y.get('name').lower().startswith('rndcnst'): |
| log.error( |
| err_prefix + ' parameter ' + y['name'] + |
| ' is defined as a compile-time ' |
| 'random netlist constant. The name must therefore start with RndCnst.' |
| ) |
| return error + 1 |
| |
| overrides = [('local', 'false'), ('default', ''), |
| ('expose', 'false')] |
| |
| for key, value in overrides: |
| if y.setdefault(key, value) != value: |
| log.error( |
| err_prefix + ' ' + key + ' for parameter ' + |
| y['name'] + |
| ' must not be set since it will be defined automatically.' |
| ) |
| return error + 1 |
| |
| # TODO: Check if PascalCase or ALL_CAPS |
| y.setdefault('type', 'int') |
| |
| y.setdefault('local', 'true') |
| local, ierr = check_bool(y["local"], err_prefix + " local") |
| if ierr: |
| error += 1 |
| y["local"] = "true" |
| |
| y.setdefault('expose', 'false') |
| local, ierr = check_bool(y["expose"], err_prefix + " expose") |
| if ierr: |
| error += 1 |
| y["expose"] = "false" |
| |
| if y["local"] == "true" and y["expose"] == "true": |
| log.error(err_prefix + ' element ' + x + '["' + y["name"] + '"]' + |
| ' cannot be local and exposed to top level') |
| return error + 1 |
| |
| if "default" in y: |
| if y["type"][:3] == "int": |
| default, ierr = check_int(y["default"], |
| err_prefix + " default") |
| if ierr: |
| error += 1 |
| y["default"] = "1" |
| elif y["randtype"] != "none": |
| # Don't make assumptions for exposed parameters. These must have |
| # a default. |
| if y["expose"] == "true": |
| log.error(err_prefix + ' element ' + x + '["' + y["name"] + |
| '"]' + ' has no defined default value') |
| elif y["type"][:3] == "int": |
| y["default"] = "1" |
| elif y["type"] == "string": |
| y["default"] = "" |
| else: |
| log.error(err_prefix + ' element ' + x + '["' + y["name"] + |
| '"]' + ' type is not supported') |
| return error + 1 |
| |
| return error |
| |
| |
| def search_param(obj, key): |
| """return the param object if found, else return non zero error |
| """ |
| for p in obj: |
| if p["name"] == key: |
| return p, 0 |
| |
| log.error("Param {} cannot be found".format(key)) |
| return None, 1 |
| |
| |
| def check_keys(obj, required_keys, optional_keys, added_keys, err_prefix): |
| error = 0 |
| for x in required_keys: |
| if x not in obj: |
| error += 1 |
| log.error(err_prefix + " missing required key " + x) |
| for x in obj: |
| type = None |
| if x in required_keys: |
| type = required_keys[x][0] |
| elif x in optional_keys: |
| type = optional_keys[x][0] |
| elif x not in added_keys: |
| log.warning(err_prefix + " contains extra key " + x) |
| if type is not None: |
| if type[:2] == 'ln': |
| error += check_ln(obj, x, type == 'lnw', err_prefix) |
| if type == 'lp': |
| error += check_lp(obj, x, err_prefix) |
| |
| return error |
| |
| |
| val_types = { |
| 'd': ["int", "integer (binary 0b, octal 0o, decimal, hex 0x)"], |
| 'x': ["xint", "x for undefined otherwise int"], |
| 'b': [ |
| "bitrange", "bit number as decimal integer, " |
| "or bit-range as decimal integers msb:lsb" |
| ], |
| 'l': ["list", "comma separated list enclosed in `[]`"], |
| 'ln': [ |
| "name list", 'comma separated list enclosed in `[]` of ' |
| 'one or more groups that have just name and dscr keys.' |
| ' e.g. `{ name: "name", desc: "description"}`' |
| ], |
| 'lnw': ["name list+", 'name list that optionally contains a width'], |
| 'lp': ["parameter list", 'parameter list having default value optionally'], |
| 'g': ["group", "comma separated group of key:value enclosed in `{}`"], |
| 'lg': [ |
| "list of group", "comma separated group of key:value enclosed in `{}`" |
| " the second entry of the list is the sub group format" |
| ], |
| 's': ["string", "string, typically short"], |
| 't': [ |
| "text", "string, may be multi-line enclosed in `'''` " |
| "may use `**bold**`, `*italic*` or `!!Reg` markup" |
| ], |
| 'T': ["tuple", "tuple enclosed in ()"], |
| 'pi': ["python int", "Native Python type int (generated)"], |
| 'pb': ["python Bool", "Native Python type Bool (generated)"], |
| 'pl': ["python list", "Native Python type list (generated)"], |
| 'pe': ["python enum", "Native Python type enum (generated)"] |
| } |
| |
| # Toplevel keys |
| top_required = { |
| 'name': ['s', "name of the component"], |
| 'clock_primary': ['s', "name of the primary clock"], |
| 'bus_device': ['s', "name of the bus interface for the device"], |
| 'registers': |
| ['l', "list of register definition groups and " |
| "offset control groups"] |
| } |
| top_optional = { |
| 'alert_list': ['lnw', "list of peripheral alerts"], |
| 'available_inout_list': ['lnw', "list of available peripheral inouts"], |
| 'available_input_list': ['lnw', "list of available peripheral inputs"], |
| 'available_output_list': ['lnw', "list of available peripheral outputs"], |
| 'bus_host': ['s', "name of the bus interface as host"], |
| 'hier_path': [ |
| None, |
| 'additional hierarchy path before the reg block instance' |
| ], |
| 'interrupt_list': ['lnw', "list of peripheral interrupts"], |
| 'inter_signal_list': ['l', "list of inter-module signals"], |
| 'no_auto_alert_regs': [ |
| 's', "Set to true to suppress automatic " |
| "generation of alert test registers. " |
| "Defaults to true if no alert_list is present. " |
| "Otherwise this defaults to false. " |
| ], |
| 'no_auto_intr_regs': [ |
| 's', "Set to true to suppress automatic " |
| "generation of interrupt registers. " |
| "Defaults to true if no interrupt_list is present. " |
| "Otherwise this defaults to false. " |
| ], |
| 'other_clock_list': ['l', "list of other chip clocks needed"], |
| 'other_reset_list': ['l', "list of other resets"], |
| 'param_list': ['lp', "list of parameters of the IP"], |
| 'regwidth': ['d', "width of registers in bits (default 32)"], |
| 'reset_primary': ['s', "primary reset used by the module"], |
| 'reset_request_list': ['l', 'list of signals requesting reset'], |
| 'scan': ['pb', 'Indicates the module have `scanmode_i`'], |
| 'scan_reset': ['pb', 'Indicates the module have `test_rst_ni`'], |
| 'SPDX-License-Identifier': [ |
| 's', "License ientifier (if using pure json) " |
| "Only use this if unable to put this " |
| "information in a comment at the top of the " |
| "file." |
| ], |
| 'wakeup_list': ['lnw', "list of peripheral wakeups"] |
| } |
| top_added = { |
| 'genrnames': ['pl', "list of register names"], |
| 'genautoregs': ['pb', "Registers were generated from config info"], |
| 'gensize': [ |
| 'pi', "address space size needed for registers. " |
| "Generated by tool as next power of 2." |
| ] |
| } |
| |
| # ln type has list of groups with only name and description |
| # (was called "subunit" in cfg_validate) |
| ln_required = { |
| 'name': ['s', "name of the item"], |
| 'desc': ['s', "description of the item"], |
| } |
| ln_optional = { |
| 'width': ['d', "bit width of the item (if not 1)"], |
| } |
| |
| # lp type |
| lp_required = { |
| 'name': ['s', "name of the item"], |
| } |
| lp_optional = { |
| 'desc': ['s', "description of the item"], |
| 'type': ['s', "item type. int by default"], |
| 'default': ['s', "item default value"], |
| 'local': ['pb', "to be localparam"], |
| 'expose': ['pb', "to be exposed to top"], |
| 'randcount': |
| ['s', "number of bits to randomize in the parameter. 0 by default."], |
| 'randtype': ['s', "type of randomization to perform. none by default"], |
| } |
| |
| # Registers list may have embedded keys |
| list_optone = { |
| 'reserved': ['d', "number of registers to reserve space for"], |
| 'skipto': ['d', "set next register offset to value"], |
| 'window': [ |
| 'g', "group defining an address range " |
| "for something other than standard registers" |
| ], |
| 'multireg': |
| ['g', "group defining registers generated " |
| "from a base instance."] |
| } |
| |
| key_use = {'r': "required", 'o': "optional", 'a': "added by tool"} |
| |
| |
| def make_intr_alert_reg(reg_block, regs, name, swaccess, hwaccess, desc): |
| if name == 'ALERT_TEST': |
| signal_list = regs['alert_list'] |
| else: |
| signal_list = regs['interrupt_list'] |
| # these names will be converted into test registers |
| testreg_names = ['INTR_TEST', 'ALERT_TEST'] |
| |
| swaccess_obj = SWAccess('make_intr_alert_reg()', swaccess) |
| hwaccess_obj = HWAccess('make_intr_alert_reg()', hwaccess) |
| |
| fields = [] |
| cur_bit = 0 |
| for (field_idx, bit) in enumerate(signal_list): |
| w = 1 |
| if 'width' in bit and bit['width'] != '1': |
| w = int(bit['width'], 0) |
| field_bits = Bits(cur_bit + w - 1, cur_bit) |
| else: |
| field_bits = Bits(cur_bit, cur_bit) |
| cur_bit += w |
| |
| field_name = bit['name'] |
| if name == 'INTR_ENABLE': |
| field_desc = ('Enable interrupt when {}!!INTR_STATE.{} is set.' |
| .format('corresponding bit in ' if w > 1 else '', |
| field_name)) |
| elif name == 'INTR_TEST': |
| field_desc = ('Write 1 to force {}!!INTR_STATE.{} to 1.' |
| .format('corresponding bit in ' if w > 1 else '', |
| field_name)) |
| elif name == 'ALERT_TEST': |
| field_desc = 'Write 1 to trigger one alert event of this kind.' |
| else: |
| field_desc = bit['desc'] |
| |
| # Put the automatically generated information back into |
| # `interrupt_list`, so that it can be used to generate C preprocessor |
| # definitions if needed. |
| signal_list[field_idx]['bits'] = field_bits.as_str() |
| signal_list[field_idx]['bitinfo'] = (field_bits.bitmask(), |
| field_bits.width(), |
| field_bits.lsb) |
| |
| fields.append(Field(field_name, |
| field_desc, |
| bit.get('tags', []), |
| swaccess_obj, |
| hwaccess_obj, |
| hwqe=name in testreg_names, |
| hwre=False, |
| bits=field_bits, |
| resval=0, |
| enum=None)) |
| |
| hwext = 'true' if name in testreg_names else 'false' |
| if name == 'INTR_TEST': |
| # intr_test csr is WO which - it reads back 0s |
| reg_tags = ["excl:CsrNonInitTests:CsrExclWrite"] |
| elif name == 'INTR_STATE': |
| # intr_state csr is affected by writes to other csrs - skip write-check |
| reg_tags = ["excl:CsrNonInitTests:CsrExclWriteCheck"] |
| else: |
| reg_tags = [] |
| |
| bool_hwext = hwext.lower() == 'true' |
| |
| reg = Register(reg_block.offset, |
| name, |
| desc, |
| swaccess_obj, |
| hwaccess_obj, |
| hwext=bool_hwext, |
| hwqe=bool_hwext, |
| hwre=False, |
| regwen=None, |
| tags=reg_tags, |
| resval=None, |
| shadowed=False, |
| fields=fields, |
| update_err_alert=None, |
| storage_err_alert=None) |
| reg_block.add_register(reg) |
| return reg |
| |
| |
| def make_intr_regs(reg_block, regs, fullwidth): |
| iregs = [] |
| intrs = regs['interrupt_list'] |
| num_intrs = sum([int(x.get('width', '1'), 0) for x in intrs]) |
| if num_intrs > fullwidth: |
| log.error('More than ' + str(fullwidth) + ' interrupts in list') |
| return iregs, 1 |
| |
| try: |
| new_reg = make_intr_alert_reg(reg_block, regs, 'INTR_STATE', 'rw1c', |
| 'hrw', 'Interrupt State Register') |
| iregs.append(new_reg) |
| new_reg = make_intr_alert_reg(reg_block, regs, 'INTR_ENABLE', 'rw', |
| 'hro', 'Interrupt Enable Register') |
| iregs.append(new_reg) |
| new_reg = make_intr_alert_reg(reg_block, regs, 'INTR_TEST', |
| 'wo', 'hro', 'Interrupt Test Register') |
| iregs.append(new_reg) |
| except ValueError as err: |
| log.error(str(err)) |
| return iregs, 1 |
| |
| return iregs, 0 |
| |
| |
| def make_alert_regs(reg_block, regs, fullwidth): |
| alert_regs = [] |
| alerts = regs['alert_list'] |
| num_alerts = sum([int(x.get('width', '1'), 0) for x in alerts]) |
| if num_alerts > fullwidth: |
| log.error('More than ' + str(fullwidth) + ' alerts in list') |
| return alert_regs, 1 |
| |
| try: |
| new_reg = make_intr_alert_reg(reg_block, regs, 'ALERT_TEST', |
| 'wo', 'hro', 'Alert Test Register') |
| alert_regs.append(new_reg) |
| except ValueError as err: |
| log.error(str(err)) |
| return alert_regs, 1 |
| |
| return alert_regs, 0 |
| |
| |
| def validate(regs, **kwargs): |
| if "params" in kwargs: |
| params = kwargs["params"] |
| else: |
| params = [] |
| |
| if 'name' not in regs: |
| log.error("Component has no name. Aborting.") |
| return 1 |
| |
| component = regs['name'] |
| |
| regs.setdefault('param_list', []) |
| |
| error = check_keys(regs, top_required, top_optional, top_added, component) |
| if (error > 0): |
| log.error("Component has top level errors. Aborting.") |
| return error |
| regs['genrnames'] = [] |
| error = 0 |
| |
| if 'regwidth' in regs: |
| fullwidth, ierr = check_int(regs['regwidth'], "regwidth") |
| if ierr: |
| fullwidth = 32 |
| error += 1 |
| else: |
| fullwidth = 32 |
| log.warning('regwidth not specified, assuming 32.') |
| regs['regwidth'] = str(fullwidth) |
| |
| if ((fullwidth % 8) != 0): |
| addrsep = (fullwidth // 8) + 1 |
| log.warning("regwidth is not a multiple of 8 bits!") |
| else: |
| addrsep = fullwidth // 8 |
| |
| reg_block = RegBlock(addrsep, fullwidth, regs.get('param_list', [])) |
| |
| autoregs = [] |
| |
| # auto header generation would go here and update autoregs |
| |
| if 'no_auto_intr_regs' in regs: |
| no_auto_intr, err = check_bool(regs['no_auto_intr_regs'], |
| 'no_auto_intr_regs') |
| if err: |
| error += 1 |
| else: |
| if 'interrupt_list' not in regs: |
| no_auto_intr = True |
| else: |
| no_auto_intr = False |
| |
| if 'no_auto_alert_regs' in regs: |
| no_auto_alerts, err = check_bool(regs['no_auto_alert_regs'], |
| 'no_auto_alert_regs') |
| if err: |
| error += 1 |
| else: |
| if 'alert_list' not in regs: |
| no_auto_alerts = True |
| else: |
| no_auto_alerts = False |
| |
| if 'interrupt_list' in regs and 'genautoregs' not in regs and not no_auto_intr: |
| iregs, err = make_intr_regs(reg_block, regs, fullwidth) |
| error += err |
| autoregs.extend(iregs) |
| |
| # Generate a NumAlerts parameter for provided alert_list. |
| if regs.setdefault('alert_list', []): |
| # Generate alert test registers. |
| if 'genautoregs' not in regs and not no_auto_alerts: |
| aregs, err = make_alert_regs(reg_block, regs, fullwidth) |
| error += err |
| autoregs.extend(aregs) |
| |
| num_alerts = 0 |
| for alert in regs['alert_list']: |
| alert_width = int(alert.get('width', '1'), 0) |
| num_alerts += alert_width |
| if alert_width > 1: |
| log.warning( |
| "{}: Consider naming each alert individually instead of " |
| "declaring an alert signal with width > 1.".format( |
| alert['name'])) |
| |
| # check alert naming scheme |
| if alert['name'] == "": |
| log.error("{}: Alert name cannot be empty".format(alert['name'])) |
| error += 1 |
| prefix = alert['name'].split('_') |
| if prefix[0] not in ['recov', 'fatal']: |
| log.error( |
| "{}: Alerts must be prefixed with either 'recov_' or " |
| "'fatal_'.".format(alert['name'])) |
| error += 1 |
| |
| if num_alerts != 0: |
| param = '' |
| for p in regs['param_list']: |
| if p['name'] == 'NumAlerts': |
| param = p |
| if param: |
| # We already have an NumAlerts parameter. |
| if (param['type'] != 'int' or |
| param['default'] != str(num_alerts) or |
| param['local'] != 'true'): |
| log.error( |
| 'Conflicting definition of NumAlerts parameter found.') |
| error += 1 |
| else: |
| # Generate the NumAlerts parameter.x |
| regs['param_list'].append({ |
| 'name': 'NumAlerts', |
| 'type': 'int', |
| 'default': str(num_alerts), |
| 'desc': 'Number of alerts', |
| 'local': 'true', |
| 'expose': 'false', |
| }) |
| |
| # Change default param value if exists. |
| # Assumed param list is already validated in above `check_keys` function |
| if "param_list" in regs and len(regs["param_list"]) != 0: |
| for p in params: |
| if p == '': |
| continue |
| |
| tokens = p.split('=') |
| if len(tokens) != 2: |
| error += 1 |
| log.error("Parameter format isn't correct. {}".format(p)) |
| key, value = tokens[0], tokens[1] |
| param, err = search_param(regs["param_list"], key) |
| if err != 0: |
| error += err |
| continue |
| |
| value, err = check_int( |
| value, component + " param[{}]".format(param["name"])) |
| if err != 0: |
| error += err |
| continue |
| |
| param["default"] = value |
| |
| if "scan" in regs: |
| scan, err = check_bool(regs["scan"], component + " scan") |
| else: |
| regs["scan"] = "false" |
| |
| reg_block.add_raw_registers(regs['registers']) |
| |
| regs['gensize'] = 1 << (reg_block.offset - 1).bit_length() |
| |
| try: |
| reg_block.validate() |
| except ValueError as err: |
| log.error(str(err)) |
| error += 1 |
| |
| if autoregs: |
| regs['genautoregs'] = True |
| |
| regs['registers'] = reg_block |
| regs['genrnames'] = list(reg_block.name_to_offset.keys()) |
| |
| log.debug("Validated, size = " + hex(regs['gensize']) + " errors=" + |
| str(error) + " names are " + str(regs['genrnames'])) |
| if (error > 0): |
| log.error("Register description had " + str(error) + " error" + |
| "s" if error > 1 else "") |
| |
| regs.setdefault('inter_signal_list', []) |
| regs.setdefault('bus_device', '') |
| regs.setdefault('bus_host', '') |
| |
| if regs["bus_device"] == "tlul": |
| # Add to inter_module_signal |
| port_name = "tl" if regs["bus_host"] in ["none", ""] else "tl_d" |
| |
| regs["inter_signal_list"].append( |
| OrderedDict([('struct', 'tl'), ('package', 'tlul_pkg'), |
| ('type', 'req_rsp'), ('act', 'rsp'), |
| ('name', port_name)])) |
| |
| if regs['bus_host'] == "tlul": |
| port_name = "tl" if regs["bus_host"] in ["none", ""] else "tl_h" |
| |
| regs["inter_signal_list"].append( |
| OrderedDict([('struct', 'tl'), ('package', 'tlul_pkg'), |
| ('type', 'req_rsp'), ('act', 'req'), |
| ('name', port_name)])) |
| return error |