[util] make topgen run multiple pass.

- This is an UBER hack
- This is needed because the generation process has dependencies on itself.  Normally this can be worked around, but for tops such as top_englishbreakfast that are generated on the fly, it becomes probelmatic.
- The basic idea is that for each dependency the generation process is invoked again to make sure updated information is used to generate new correct modules.
- The final run is the one that is actually used for final merging and deployed by the rest of the script.

Signed-off-by: Timothy Chen <timothytim@google.com>

[util] slightly tweak topgen multi-pass

Signed-off-by: Timothy Chen <timothytim@google.com>
diff --git a/util/topgen.py b/util/topgen.py
index 2bf04c9..b100619 100755
--- a/util/topgen.py
+++ b/util/topgen.py
@@ -838,6 +838,158 @@
     gen_dv.gen_ral(top_block, dv_base_prefix, str(out_path))
 
 
+def _process_top(topcfg, args, cfg_path, out_path, pass_idx):
+    # Create generated list
+    # These modules are generated through topgen
+    generated_list = [
+        module['type'] for module in topcfg['module']
+        if 'generated' in module and module['generated'] == 'true'
+    ]
+    log.info("Filtered list is {}".format(generated_list))
+
+    # These modules are NOT generated but belong to a specific top
+    # and therefore not part of "hw/ip"
+    top_only_list = [
+        module['type'] for module in topcfg['module']
+        if 'top_only' in module and module['top_only'] == 'true'
+    ]
+    log.info("Filtered list is {}".format(top_only_list))
+
+    topname = topcfg["name"]
+
+    # Sweep the IP directory and gather the config files
+    ip_dir = Path(__file__).parents[1] / 'hw/ip'
+    ips = search_ips(ip_dir)
+
+    # exclude filtered IPs (to use top_${topname} one) and
+    exclude_list = generated_list + top_only_list
+    ips = [x for x in ips if not x.parents[1].name in exclude_list]
+
+    # Hack alert
+    # Generate clkmgr.hjson here so that it can be included below
+    # Unlike other generated hjsons, clkmgr thankfully does not require
+    # ip.hjson information.  All the information is embedded within
+    # the top hjson file
+    amend_clocks(topcfg)
+    generate_clkmgr(topcfg, cfg_path, out_path)
+
+    # It may require two passes to check if the module is needed.
+    # TODO: first run of topgen will fail due to the absent of rv_plic.
+    # It needs to run up to amend_interrupt in merge_top function
+    # then creates rv_plic.hjson then run xbar generation.
+    hjson_dir = Path(args.topcfg).parent
+
+    for ip in generated_list:
+        log.info("Appending {}".format(ip))
+        if ip == 'clkmgr' or (pass_idx > 0):
+            ip_hjson = Path(out_path) / "ip/{}/data/autogen/{}.hjson".format(
+                ip, ip)
+        else:
+            ip_hjson = hjson_dir.parent / "ip/{}/data/autogen/{}.hjson".format(
+                ip, ip)
+        ips.append(ip_hjson)
+
+    for ip in top_only_list:
+        log.info("Appending {}".format(ip))
+        ip_hjson = hjson_dir.parent / "ip/{}/data/{}.hjson".format(ip, ip)
+        ips.append(ip_hjson)
+
+    # load Hjson and pass validate from reggen
+    try:
+        ip_objs = []
+        for x in ips:
+            # Skip if it is not in the module list
+            if x.stem not in [ip["type"] for ip in topcfg["module"]]:
+                log.info("Skip module %s as it isn't in the top module list" %
+                         x.stem)
+                continue
+
+            # The auto-generated hjson might not yet exist. It will be created
+            # later, see generate_{ip_name}() calls below. For the initial
+            # validation, use the template in hw/ip/{ip_name}/data .
+            if x.stem in generated_list and not x.is_file():
+                hjson_file = ip_dir / "{}/data/{}.hjson".format(x.stem, x.stem)
+                log.info(
+                    "Auto-generated hjson %s does not yet exist. " % str(x) +
+                    "Falling back to template %s for initial validation." %
+                    str(hjson_file))
+            else:
+                hjson_file = x
+
+            obj = hjson.load(hjson_file.open('r'),
+                             use_decimal=True,
+                             object_pairs_hook=OrderedDict)
+            if validate.validate(obj) != 0:
+                log.info("Parsing IP %s configuration failed. Skip" % x)
+                continue
+            ip_objs.append(obj)
+
+    except ValueError:
+        raise SystemExit(sys.exc_info()[1])
+
+    # Read the crossbars under the top directory
+    xbar_objs = get_hjsonobj_xbars(hjson_dir)
+
+    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_{topname}.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")
+
+    completecfg = merge_top(topcfg, ip_objs, xbar_objs)
+
+    # Generate PLIC
+    if not args.no_plic and \
+       not args.alert_handler_only and \
+       not args.xbar_only:
+        generate_plic(completecfg, out_path)
+        if args.plic_only:
+            sys.exit()
+
+    # Generate Alert Handler
+    if not args.xbar_only:
+        generate_alert_handler(completecfg, out_path)
+        if args.alert_handler_only:
+            sys.exit()
+
+    # Generate Pinmux
+    generate_pinmux_and_padctrl(completecfg, out_path)
+
+    # Generate Pwrmgr
+    generate_pwrmgr(completecfg, out_path)
+
+    # Generate rstmgr
+    generate_rstmgr(completecfg, out_path)
+
+    # Generate flash
+    generate_flash(completecfg, out_path)
+
+    # Generate top only modules
+    # These modules are not templated, but are not in hw/ip
+    generate_top_only(top_only_list, out_path, topname)
+
+    if pass_idx > 0 and args.top_ral:
+        generate_top_ral(completecfg, ip_objs, args.dv_base_prefix, out_path)
+        sys.exit()
+
+    return completecfg
+
+
 def main():
     parser = argparse.ArgumentParser(prog="topgen")
     parser.add_argument('--topcfg',
@@ -951,154 +1103,40 @@
     except ValueError:
         raise SystemExit(sys.exc_info()[1])
 
-    # Create generated list
-    # These modules are generated through topgen
-    generated_list = [
-        module['type'] for module in topcfg['module']
-        if 'generated' in module and module['generated'] == 'true'
-    ]
-    log.info("Filtered list is {}".format(generated_list))
-
-    # These modules are NOT generated but belong to a specific top
-    # and therefore not part of "hw/ip"
-    top_only_list = [
-        module['type'] for module in topcfg['module']
-        if 'top_only' in module and module['top_only'] == 'true'
-    ]
-    log.info("Filtered list is {}".format(top_only_list))
+    # TODO, long term, the levels of dependency should be automatically determined instead
+    # of hardcoded.  The following are a few examples:
+    # Example 1: pinmux depends on amending all modules before calculating the correct number of
+    #            pins.
+    #            This would be 1 level of dependency and require 2 passes.
+    # Example 2: pinmux depends on amending all modules, and pwrmgr depends on pinmux generation to
+    #            know correct number of wakeups.  This would be 2 levels of dependency and require 3
+    #            passes.
+    #
+    # How does mulit-pass work?
+    # In example 1, the first pass gathers all modules and merges them.  However, the merge process
+    # uses a stale pinmux.  The correct pinmux is then generated using the merged configuration. The
+    # second pass now merges all the correct modules (including the generated pinmux) and creates
+    # the final merged config.
+    #
+    # In example 2, the first pass gathers all modules and merges them.  However, the merge process
+    # uses a stale pinmux and pwrmgr. The correct pinmux is then generated using the merged
+    # configuration.  However, since pwrmgr is dependent on this new pinmux, it is still generated
+    # incorrectly.  The second pass merge now has an updated pinmux but stale pwrmgr.  The correct
+    # pwrmgr can now be generated.  The final pass then merges all the correct modules and creates
+    # the final configuration.
+    #
+    # This fix is related to #2083
+    process_dependencies = 1
+    for pass_idx in range(process_dependencies + 1):
+        log.debug("Generation pass {}".format(pass_idx))
+        if pass_idx < process_dependencies:
+            cfg_copy = deepcopy(topcfg)
+            _process_top(cfg_copy, args, cfg_path, out_path, pass_idx)
+        else:
+            completecfg = _process_top(topcfg, args, cfg_path, out_path, pass_idx)
 
     topname = topcfg["name"]
 
-    # Sweep the IP directory and gather the config files
-    ip_dir = Path(__file__).parents[1] / 'hw/ip'
-    ips = search_ips(ip_dir)
-
-    # exclude filtered IPs (to use top_${topname} one) and
-    exclude_list = generated_list + top_only_list
-    ips = [x for x in ips if not x.parents[1].name in exclude_list]
-
-    # Hack alert
-    # Generate clkmgr.hjson here so that it can be included below
-    # Unlike other generated hjsons, clkmgr thankfully does not require
-    # ip.hjson information.  All the information is embedded within
-    # the top hjson file
-    amend_clocks(topcfg)
-    generate_clkmgr(topcfg, cfg_path, out_path)
-
-    # It may require two passes to check if the module is needed.
-    # TODO: first run of topgen will fail due to the absent of rv_plic.
-    # It needs to run up to amend_interrupt in merge_top function
-    # then creates rv_plic.hjson then run xbar generation.
-    hjson_dir = Path(args.topcfg).parent
-
-    for ip in generated_list:
-        log.info("Appending {}".format(ip))
-        if ip == 'clkmgr':
-            ip_hjson = Path(out_path) / "ip/{}/data/autogen/{}.hjson".format(
-                ip, ip)
-        else:
-            ip_hjson = hjson_dir.parent / "ip/{}/data/autogen/{}.hjson".format(
-                ip, ip)
-        ips.append(ip_hjson)
-
-    for ip in top_only_list:
-        log.info("Appending {}".format(ip))
-        ip_hjson = hjson_dir.parent / "ip/{}/data/{}.hjson".format(ip, ip)
-        ips.append(ip_hjson)
-
-    # load Hjson and pass validate from reggen
-    try:
-        ip_objs = []
-        for x in ips:
-            # Skip if it is not in the module list
-            if x.stem not in [ip["type"] for ip in topcfg["module"]]:
-                log.info("Skip module %s as it isn't in the top module list" %
-                         x.stem)
-                continue
-
-            # The auto-generated hjson might not yet exist. It will be created
-            # later, see generate_{ip_name}() calls below. For the initial
-            # validation, use the template in hw/ip/{ip_name}/data .
-            if x.stem in generated_list and not x.is_file():
-                hjson_file = ip_dir / "{}/data/{}.hjson".format(x.stem, x.stem)
-                log.info(
-                    "Auto-generated hjson %s does not yet exist. " % str(x) +
-                    "Falling back to template %s for initial validation." %
-                    str(hjson_file))
-            else:
-                hjson_file = x
-
-            obj = hjson.load(hjson_file.open('r'),
-                             use_decimal=True,
-                             object_pairs_hook=OrderedDict)
-            if validate.validate(obj) != 0:
-                log.info("Parsing IP %s configuration failed. Skip" % x)
-                continue
-            ip_objs.append(obj)
-
-    except ValueError:
-        raise SystemExit(sys.exc_info()[1])
-
-    # Read the crossbars under the top directory
-    xbar_objs = get_hjsonobj_xbars(hjson_dir)
-
-    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_{topname}.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")
-
-    completecfg = merge_top(topcfg, ip_objs, xbar_objs)
-
-    if args.top_ral:
-        generate_top_ral(completecfg, ip_objs, args.dv_base_prefix, out_path)
-        sys.exit()
-
-    # Generate PLIC
-    if not args.no_plic and \
-       not args.alert_handler_only and \
-       not args.xbar_only:
-        generate_plic(completecfg, out_path)
-        if args.plic_only:
-            sys.exit()
-
-    # Generate Alert Handler
-    if not args.xbar_only:
-        generate_alert_handler(completecfg, out_path)
-        if args.alert_handler_only:
-            sys.exit()
-
-    # Generate Pinmux
-    generate_pinmux_and_padctrl(completecfg, out_path)
-
-    # Generate Pwrmgr
-    generate_pwrmgr(completecfg, out_path)
-
-    # Generate rstmgr
-    generate_rstmgr(completecfg, out_path)
-
-    # Generate flash
-    generate_flash(completecfg, out_path)
-
-    # Generate top only modules
-    # These modules are not templated, but are not in hw/ip
-    generate_top_only(top_only_list, out_path, topname)
-
     # Generate xbars
     if not args.no_xbar or args.xbar_only:
         generate_xbars(completecfg, out_path)