[reggen/topgen] Add support for generating random netlist constants

This extends the instantiation paramter feature of reggen/topgen such
that random netlist constants can be generated and assigned in a
systematic way.

To that end, each IP has to declare in the corresponding HJSON which
parameters are compile time random netlist constants. Topgen then
aggregates and uniquifies those parameters, assigns random data and puts
them into a dedicated top-level package which is only meant for random
netlist constants.

The supported random constant types are either "data" which just
corresponds to a randomized logic of a certain length, or "perm" which
corresponds to a random permutation of linear array indices up to a
certain length.

Topgen allows to specify a custom seed inside top_earlgrey.hjson or on
the commandline using the --seed switch in order to make the random
number generation repeatable, if needed.

Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/hw/top_earlgrey/data/top_earlgrey.hjson b/hw/top_earlgrey/data/top_earlgrey.hjson
index c28e703..c7031ef 100644
--- a/hw/top_earlgrey/data/top_earlgrey.hjson
+++ b/hw/top_earlgrey/data/top_earlgrey.hjson
@@ -6,10 +6,16 @@
 { name: "earlgrey",
   type: "top",
 
-  # 32-bit datawidth
+  /////////////////////////////////////////////////////////////
+  // Seed for compile-time random constants                  //
+  // NOTE: REPLACE THIS WITH A NEW VALUE BEFORE THE TAPEOUT  //
+  /////////////////////////////////////////////////////////////
+  rnd_cnst_seed: 4881560218908238235
+
+  // 32-bit datawidth
   datawidth: "32",
 
-  // This is the clock data strcture of the design.
+  // This is the clock data structure of the design.
   // The hier path refers to the clock reference path (struct / port)
   //   - The top/ext desgination follows the same scheme as inter-module
   // The src key indicates the raw clock sources in the design
diff --git a/hw/top_earlgrey/data/top_earlgrey.sv.tpl b/hw/top_earlgrey/data/top_earlgrey.sv.tpl
index 69b8e28..eeddd41 100644
--- a/hw/top_earlgrey/data/top_earlgrey.sv.tpl
+++ b/hw/top_earlgrey/data/top_earlgrey.sv.tpl
@@ -1,7 +1,7 @@
 // Copyright lowRISC contributors.
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
-
+${gencmd}
 <%
 import re
 import topgen.lib as lib
@@ -99,6 +99,8 @@
   import tlul_pkg::*;
   import top_pkg::*;
   import tl_main_pkg::*;
+  // Compile-time random constants
+  import top_earlgrey_rnd_cnst_pkg::*;
 
   // Signals
   logic [${num_mio_inputs + num_mio_inouts - 1}:0] mio_p2d;
@@ -484,7 +486,7 @@
   % if m["param_list"]:
   ${m["type"]} #(
     % for i in m["param_list"]:
-    .${i["name"]}(${i["name_top" if i["expose"] == "true" else "default"]})${"," if not loop.last else ""}
+    .${i["name"]}(${i["name_top" if i["expose"] == "true" or i["randtype"] != "none" else "default"]})${"," if not loop.last else ""}
     % endfor
   ) u_${m["name"]} (
   % else:
diff --git a/hw/top_earlgrey/data/top_earlgrey_pkg.sv.tpl b/hw/top_earlgrey/data/top_earlgrey_pkg.sv.tpl
index 054d42b..ada5607 100644
--- a/hw/top_earlgrey/data/top_earlgrey_pkg.sv.tpl
+++ b/hw/top_earlgrey/data/top_earlgrey_pkg.sv.tpl
@@ -1,7 +1,7 @@
 // Copyright lowRISC contributors.
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
-
+${gencmd}
 <%
 import topgen.lib as lib
 
diff --git a/hw/top_earlgrey/data/top_earlgrey_rnd_cnst_pkg.sv.tpl b/hw/top_earlgrey/data/top_earlgrey_rnd_cnst_pkg.sv.tpl
new file mode 100644
index 0000000..92fd81b
--- /dev/null
+++ b/hw/top_earlgrey/data/top_earlgrey_rnd_cnst_pkg.sv.tpl
@@ -0,0 +1,43 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+${gencmd}
+<%
+  def make_blocked_sv_literal(hexstr, randwidth):
+    """This chops the random hexstring into manageable blocks of 64 chars such that the
+    lines do not get too long.
+    """
+    # Make all-caps and drop '0x' preamble
+    hexstr = str(hexstr[2:]).upper()
+    # Block width in hex chars
+    blockwidth = 64
+    numbits = randwidth % (4*blockwidth)
+    idx = 0
+    hexblocks = []
+    while randwidth > 0:
+      hexstr = hexstr[idx:]
+      randwidth -= numbits
+      idx = (numbits + 3) // 4
+      hexblocks.append(str(numbits) + "'h" + hexstr[0:idx])
+      numbits = 4*blockwidth
+    return hexblocks
+%>
+package top_earlgrey_rnd_cnst_pkg;
+
+% for m in top["module"]:
+  % for p in filter(lambda p: p["randtype"] in ["data", "perm"], m["param_list"]):
+    % if loop.first:
+  ////////////////////////////////////////////
+  // ${m['name']}
+  ////////////////////////////////////////////
+    % endif
+  // ${p['desc']}
+  parameter ${p["type"]} ${p["name_top"]} = {
+    % for block in make_blocked_sv_literal(p["default"], p["randwidth"]):
+    ${block}${"" if loop.last else ","}
+    % endfor
+  };
+
+  % endfor
+% endfor
+endpackage : top_earlgrey_rnd_cnst_pkg
diff --git a/hw/top_earlgrey/top_earlgrey.core b/hw/top_earlgrey/top_earlgrey.core
index 629156a..3f518c6 100644
--- a/hw/top_earlgrey/top_earlgrey.core
+++ b/hw/top_earlgrey/top_earlgrey.core
@@ -42,6 +42,7 @@
       - lowrisc:tlul:headers
       - lowrisc:prim:all
     files:
+      - rtl/autogen/top_earlgrey_rnd_cnst_pkg.sv
       - rtl/autogen/top_earlgrey.sv
     file_type: systemVerilogSource
 
diff --git a/util/reggen/validate.py b/util/reggen/validate.py
index 5b698e2..49ad340 100644
--- a/util/reggen/validate.py
+++ b/util/reggen/validate.py
@@ -55,6 +55,9 @@
             "desc": "auto added parameter",
             "local": "true",
             "expose": "false",
+            "randtype": "none",
+            "randcount": "0",
+            "randwidth": "0"
         })
         log.debug("Parameter {} is added".format(mreg["name"]))
         # Replace count integer to parameter
@@ -140,6 +143,43 @@
     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')
 
@@ -168,7 +208,7 @@
                 if ierr:
                     error += 1
                     y["default"] = "1"
-        else:
+        elif y["randtype"] != "none":
             # Don't make assumptions for exposed parameters. These must have
             # a default.
             if y["expose"] == "true":
@@ -379,6 +419,8 @@
     '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
diff --git a/util/topgen.py b/util/topgen.py
index c6d11a0..04acd46 100755
--- a/util/topgen.py
+++ b/util/topgen.py
@@ -8,6 +8,8 @@
 import logging as log
 import subprocess
 import sys
+import random
+
 from collections import OrderedDict
 from io import StringIO
 from pathlib import Path
@@ -25,13 +27,14 @@
 from topgen.c import TopGenC
 
 # Common header for generated files
-genhdr = '''// Copyright lowRISC contributors.
-// Licensed under the Apache License, Version 2.0, see LICENSE for details.
-// SPDX-License-Identifier: Apache-2.0
-//
+warnhdr = '''//
 // ------------------- W A R N I N G: A U T O - G E N E R A T E D   C O D E !! -------------------//
 // PLEASE DO NOT HAND-EDIT THIS FILE. IT HAS BEEN AUTO-GENERATED WITH THE FOLLOWING COMMAND:
 '''
+genhdr = '''// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+''' + warnhdr
 
 SRCTREE_TOP = Path(__file__).parent.parent.resolve()
 
@@ -890,6 +893,11 @@
         default=False,
         action='store_true',
         help="If set, the tool generates top level RAL model for DV")
+    # Generator options for compile time random netlist constants
+    parser.add_argument('--rnd_cnst_seed',
+                        type=int,
+                        metavar='<seed>',
+                        help='Custom seed for RNG to compute netlist constants.')
 
     args = parser.parse_args()
 
@@ -1014,6 +1022,19 @@
     log.info("Detected crossbars: %s" %
              (", ".join([x["name"] for x in xbar_objs])))
 
+    # If specified, override the seed for random netlist constant computation.
+    if args.rnd_cnst_seed:
+        log.warning(
+            'Commandline override of rnd_cnst_seed with {}.'.format(args.rnd_cnst_seed))
+        topcfg['rnd_cnst_seed'] = args.rnd_cnst_seed
+    # Otherwise, we either take it from the top_earlgrey.hjson if present, or
+    # randomly generate a new seed if not.
+    else:
+        random.seed()
+        new_seed = random.getrandbits(64)
+        if topcfg.setdefault('rnd_cnst_seed', new_seed) != new_seed:
+            log.warning('No rnd_cnst_seed specified, setting to {}.'.format(new_seed))
+
     topcfg, error = validate_top(topcfg, ip_objs, xbar_objs)
     if error != 0:
         raise SystemExit("Error occured while validating top.hjson")
@@ -1071,9 +1092,14 @@
     hjson_dir = Path(args.topcfg).parent
     genhjson_path = hjson_dir / ("autogen/top_%s.gen.hjson" %
                                  completecfg["name"])
-    gencmd = (
-        "// util/topgen.py -t hw/top_{topname}/data/top_{topname}.hjson --hjson-only "
-        "-o hw/top_{topname}/\n".format(topname=topname))
+
+    # Header for HJSON
+    gencmd = '''//
+// util/topgen.py -t hw/top_{topname}/data/top_{topname}.hjson \\
+//                -o hw/top_{topname}/ \\
+//                --hjson-only \\
+//                --rnd_cnst_seed {seed}
+'''.format(topname=topname, seed=completecfg['rnd_cnst_seed'])
 
     genhjson_path.write_text(genhdr + gencmd +
                              hjson.dumps(completecfg, for_json=True))
@@ -1095,16 +1121,34 @@
 
             return rendered_path.resolve()
 
+        # Header for SV files
+        gencmd = warnhdr + '''//
+// util/topgen.py -t hw/top_{topname}/data/top_{topname}.hjson \\
+//                --tpl hw/top_earlgrey/data/ \\
+//                -o hw/top_{topname}/ \\
+//                --rnd_cnst_seed {seed}
+'''.format(topname=topname, seed=topcfg['rnd_cnst_seed'])
+
         # SystemVerilog Top:
         # 'top_earlgrey.sv.tpl' -> 'rtl/autogen/top_earlgrey.sv'
-        render_template('top_%s.sv', 'rtl/autogen')
+        render_template('top_%s.sv',
+                        'rtl/autogen',
+                        gencmd = gencmd)
 
         # The C / SV file needs some complex information, so we initialize this
         # object to store it.
         c_helper = TopGenC(completecfg)
 
         # 'top_earlgrey_pkg.sv.tpl' -> 'rtl/autogen/top_earlgrey_pkg.sv'
-        render_template('top_%s_pkg.sv', 'rtl/autogen', helper=c_helper)
+        render_template('top_%s_pkg.sv',
+                        'rtl/autogen',
+                        helper=c_helper,
+                        gencmd = gencmd)
+
+        # compile-time random netlist constants
+        render_template('top_%s_rnd_cnst_pkg.sv',
+                        'rtl/autogen',
+                        gencmd = gencmd)
 
         # C Header + C File + Clang-format file
 
diff --git a/util/topgen/merge.py b/util/topgen/merge.py
index 10e698b..d7cc97b 100644
--- a/util/topgen/merge.py
+++ b/util/topgen/merge.py
@@ -3,11 +3,35 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import logging as log
+import random
 from copy import deepcopy
 from functools import partial
 from collections import OrderedDict
+from math import ceil, log2
 
-from topgen import lib
+from topgen import lib, c
+
+
+def _get_random_data_hex_literal(width):
+    """ Fetch 'width' random bits and return them as hex literal"""
+    width = int(width)
+    literal_str = hex(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)]
+    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 amend_ip(top, ip):
@@ -33,6 +57,9 @@
         log.info("TOP doens't use the IP %s. Skip" % ip["name"])
         return
 
+    # Initialize RNG for compile-time netlist constants.
+    random.seed(int(top['rnd_cnst_seed']))
+
     # Needed to detect async alert transitions below
     ah_idx = ip_list_in_top.index("alert_handler")
 
@@ -97,17 +124,32 @@
             for i in ip["param_list"]:
                 if i["local"] == "true":
                     ip_module["param_list"].remove(i)
-            # Removing descriptors, checking for security-relevant parameters
+            # Checking for security-relevant parameters
             # that are not exposed, adding a top-level name.
             for i in ip_module["param_list"]:
-                i.pop("desc", None)
                 par_name = i["name"]
-                if par_name.lower().startswith("sec"):
+                if par_name.lower().startswith("sec") and not i["expose"]:
                     log.warning("{} has security-critical parameter {} "
                                 "not exposed to top".format(mod_name, par_name))
-                i["name_top"] = ("Sec" + mod_name.capitalize() + par_name[3:]
-                                 if par_name.lower().startswith("sec")
-                                 else mod_name.capitalize() + par_name)
+                # Move special prefixes to the beginnining of the parameter name.
+                param_prefixes = ["Sec", "RndCnst"]
+                cc_mod_name = c.Name.from_snake_case(mod_name).as_camel_case()
+                for prefix in param_prefixes:
+                    if par_name.lower().startswith(prefix.lower()):
+                        i["name_top"] = prefix + cc_mod_name + par_name[len(prefix):]
+                        break
+                else:
+                    i["name_top"] = cc_mod_name + par_name
+
+                # Generate random bits or permutation, if needed
+                if i["randtype"] == "data":
+                    i["default"] = _get_random_data_hex_literal(i["randcount"])
+                    # Effective width of the random vector
+                    i["randwidth"] = int(i["randcount"])
+                elif i["randtype"] == "perm":
+                    i["default"] = _get_random_perm_hex_literal(i["randcount"])
+                    # Effective width of the random vector
+                    i["randwidth"] = int(i["randcount"]) * int(ceil(log2(float(i["randcount"]))))
         else:
             ip_module["param_list"] = []
 
diff --git a/util/topgen/validate.py b/util/topgen/validate.py
index 157dd76..f045f18 100644
--- a/util/topgen/validate.py
+++ b/util/topgen/validate.py
@@ -47,6 +47,7 @@
     ['d', 'Base address of RV_DM. Planned to move to \
 module'],
     'xbar': ['l', 'List of the xbar used in the top'],
+    'rnd_cnst_seed': ['int', "Seed for random netlist constant computation"],
 }
 
 top_optional = {