[topgen] Store set of endpoints in ClockSignal connection

Before, the 'clocks' field stored a map from clock name to source
clock. Now, it also stores the set of endpoints to which the clock is
connected. This will be handy because it means that we can pass the
information to bits of code other than amend_clocks, which wasn't
possible before without hackily chopping up the clock name.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/clkmgr/data/clkmgr.sv.tpl b/hw/ip/clkmgr/data/clkmgr.sv.tpl
index 0a1cd73..71d6918 100644
--- a/hw/ip/clkmgr/data/clkmgr.sv.tpl
+++ b/hw/ip/clkmgr/data/clkmgr.sv.tpl
@@ -182,7 +182,7 @@
   ////////////////////////////////////////////////////
 % for k,v in ft_clks.items():
   prim_clock_buf u_${k}_buf (
-    .clk_i(clk_${v.name}_i),
+    .clk_i(clk_${v.src.name}_i),
     .clk_o(clocks_o.${k})
   );
 % endfor
@@ -291,7 +291,7 @@
   // Clocks with only root gate
   ////////////////////////////////////////////////////
 % for k,v in rg_clks.items():
-  assign clocks_o.${k} = clk_${v.name}_root;
+  assign clocks_o.${k} = clk_${v.src.name}_root;
 % endfor
 
   ////////////////////////////////////////////////////
@@ -306,8 +306,8 @@
   prim_flop_2sync #(
     .Width(1)
   ) u_${k}_sw_en_sync (
-    .clk_i(clk_${v.name}_i),
-    .rst_ni(rst_${v.name}_ni),
+    .clk_i(clk_${v.src.name}_i),
+    .rst_ni(rst_${v.src.name}_ni),
     .d_i(reg2hw.clk_enables.${k}_en.q),
     .q_o(${k}_sw_en)
   );
@@ -326,8 +326,8 @@
   prim_clock_gating #(
     .NoFpgaGate(1'b1)
   ) u_${k}_cg (
-    .clk_i(clk_${v.name}_root),
-    .en_i(${k}_sw_en & clk_${v.name}_en),
+    .clk_i(clk_${v.src.name}_root),
+    .en_i(${k}_sw_en & clk_${v.src.name}_en),
     .test_en_i(${k}_scanmode == lc_ctrl_pkg::On),
     .clk_o(clocks_o.${k})
   );
@@ -340,19 +340,24 @@
   // clock target
   ////////////////////////////////////////////////////
 
-% for k in hint_clks:
+% for k in hint_clks.keys():
   logic ${k}_hint;
   logic ${k}_en;
 % endfor
 
-% for k,v in hint_clks.items():
-  assign ${k}_en = ${k}_hint | ~idle_i[${v["name"].capitalize()}];
+% for k, sig in hint_clks.items():
+<%
+    ## Hint clocks should be connected to exactly one endpoint
+    eps = list(sig.endpoints)
+    assert len(eps) == 1
+%>\
+  assign ${k}_en = ${k}_hint | ~idle_i[${eps[0].capitalize()}];
 
   prim_flop_2sync #(
     .Width(1)
   ) u_${k}_hint_sync (
-    .clk_i(clk_${v["src"].name}_i),
-    .rst_ni(rst_${v["src"].name}_ni),
+    .clk_i(clk_${sig.src.name}_i),
+    .rst_ni(rst_${sig.src.name}_ni),
     .d_i(reg2hw.clk_hints.${k}_hint.q),
     .q_o(${k}_hint)
   );
@@ -371,8 +376,8 @@
   prim_clock_gating #(
     .NoFpgaGate(1'b1)
   ) u_${k}_cg (
-    .clk_i(clk_${v["src"].name}_root),
-    .en_i(${k}_en & clk_${v["src"].name}_en),
+    .clk_i(clk_${sig.src.name}_root),
+    .en_i(${k}_en & clk_${sig.src.name}_en),
     .test_en_i(${k}_scanmode == lc_ctrl_pkg::On),
     .clk_o(clocks_o.${k})
   );
@@ -380,7 +385,7 @@
 % endfor
 
   // state readback
-% for k,v in hint_clks.items():
+% for k in hint_clks.keys():
   assign hw2reg.clk_hints_status.${k}_val.de = 1'b1;
   assign hw2reg.clk_hints_status.${k}_val.d = ${k}_en;
 % endfor
diff --git a/util/topgen.py b/util/topgen.py
index df5cb98..51492dd 100755
--- a/util/topgen.py
+++ b/util/topgen.py
@@ -406,16 +406,11 @@
 
     by_type = clocks.typed_clocks()
 
-    # hint clocks dict.
-    #
-    # The clock is constructed as clk_{src_name}_{module_name}. So to get the
-    # module name we split from the right and pick the last entry
-    hint_clks = {clk: {'name': clk.rsplit('_', 1)[-1], 'src': src}
-                 for clk, src in by_type.hint_clks.items()}
-
-    # The names of blocks that use one or more sw hint clocks (clkmgr has an
+    # The names of endpoints that use one or more sw hint clocks (clkmgr has an
     # "idle" feedback signal from each), in ascending order.
-    hint_blocks = sorted(set([v['name'] for v in hint_clks.values()]))
+    hint_blocks = sorted(set(ep_name
+                             for sig in by_type.hint_clks.values()
+                             for ep_name in sig.endpoints))
 
     for idx, tpl in enumerate(tpls):
         out = ""
@@ -427,8 +422,8 @@
                                  ft_clks=by_type.ft_clks,
                                  rg_clks=by_type.rg_clks,
                                  sw_clks=by_type.sw_clks,
+                                 hint_clks=by_type.hint_clks,
                                  export_clks=top['exported_clks'],
-                                 hint_clks=hint_clks,
                                  hint_blocks=hint_blocks)
             except:  # noqa: E722
                 log.error(exceptions.text_error_template().render())
diff --git a/util/topgen/clocks.py b/util/topgen/clocks.py
index 5823cb1..0387e7e 100644
--- a/util/topgen/clocks.py
+++ b/util/topgen/clocks.py
@@ -2,7 +2,7 @@
 # Licensed under the Apache License, Version 2.0, see LICENSE for details.
 # SPDX-License-Identifier: Apache-2.0
 
-from typing import Dict, List, NamedTuple
+from typing import Dict, List, NamedTuple, Set
 
 
 def _yn_to_bool(yn: object) -> bool:
@@ -62,6 +62,17 @@
         return ret
 
 
+class ClockSignal:
+    '''A clock signal in the design'''
+    def __init__(self, name: str, src: SourceClock):
+        self.name = name
+        self.src = src
+        self.endpoints = set()  # type: Set[str]
+
+    def add_endpoint(self, ep_name: str) -> None:
+        self.endpoints.add(ep_name)
+
+
 class Group:
     def __init__(self,
                  raw: Dict[str, object],
@@ -82,7 +93,7 @@
                              f'combination with sw_cg of {self.sw_cg} and '
                              f'unique set.')
 
-        self.clocks = {}  # type: Dict[str, SourceClock]
+        self.clocks = {}  # type: Dict[str, ClockSignal]
         raw_clocks = raw.get('clocks', {})
         if not isinstance(raw_clocks, dict):
             raise ValueError(f'clocks for {what} is not a dictionary')
@@ -92,19 +103,22 @@
                 raise ValueError(f'The {clk_name} entry of clocks for {what} '
                                  f'has source {src_name}, which is not a '
                                  f'known clock source.')
-            self.clocks[clk_name] = src
+            self.add_clock(clk_name, src)
 
-    def add_clock(self, clk_name: str, src: SourceClock):
+    def add_clock(self, clk_name: str, src: SourceClock) -> ClockSignal:
         # Duplicates are ok, so long as they have the same source.
-        existing_src = self.clocks.get(clk_name)
-        if existing_src is not None:
-            if existing_src is not src:
+        sig = self.clocks.get(clk_name)
+        if sig is not None:
+            if sig.src is not src:
                 raise ValueError(f'Cannot add clock {clk_name} to group '
                                  f'{self.name} with source {src.name}: the '
                                  f'clock is there already with source '
-                                 f'{existing_src.name}.')
+                                 f'{sig.src.name}.')
         else:
-            self.clocks[clk_name] = src
+            sig = ClockSignal(clk_name, src)
+            self.clocks[clk_name] = sig
+
+        return sig
 
     def _asdict(self) -> Dict[str, object]:
         return {
@@ -112,7 +126,8 @@
             'src': self.src,
             'sw_cg': self.sw_cg,
             'unique': _bool_to_yn(self.unique),
-            'clocks': {name: src.name for name, src in self.clocks.items()}
+            'clocks': {name: sig.src.name
+                       for name, sig in self.clocks.items()}
         }
 
 
@@ -122,21 +137,21 @@
     #
     #   - Clocks fed from the always-on source
     #   - Clocks fed to the powerup group
-    ft_clks: Dict[str, SourceClock]
+    ft_clks: Dict[str, ClockSignal]
 
     # Non-feedthrough clocks that have no software control. These clocks are
     # root-gated and the root-gated clock is then exposed directly in clocks_o.
-    rg_clks: Dict[str, SourceClock]
+    rg_clks: Dict[str, ClockSignal]
 
     # Non-feedthrough clocks that have direct software control. These are
     # root-gated, but (unlike rg_clks) then go through a second clock gate
     # which is controlled by software.
-    sw_clks: Dict[str, SourceClock]
+    sw_clks: Dict[str, ClockSignal]
 
     # Non-feedthrough clocks that have "hint" software control (with a feedback
     # mechanism to allow blocks to avoid being suspended when they are not
     # idle).
-    hint_clks: Dict[str, SourceClock]
+    hint_clks: Dict[str, ClockSignal]
 
     # A list of the of non-always-on clock sources that are exposed without
     # division, sorted by name. This doesn't include clock sources that are
@@ -160,6 +175,7 @@
             self.srcs[clk.name] = clk
 
         self.derived_srcs = {}
+        assert isinstance(raw['derived_srcs'], list)
         for r in raw['derived_srcs']:
             clk = DerivedSourceClock(r, self.srcs)
             self.derived_srcs[clk.name] = clk
@@ -182,13 +198,16 @@
             'groups': list(self.groups.values())
         }
 
-    def add_clock_to_group(self, grp: Group, clk_name: str, src_name: str):
+    def add_clock_to_group(self,
+                           grp: Group,
+                           clk_name: str,
+                           src_name: str) -> ClockSignal:
         src = self.all_srcs.get(src_name)
         if src is None:
             raise ValueError(f'Cannot add clock {clk_name} to group '
                              f'{grp.name}: the given source name is '
                              f'{src_name}, which is unknown.')
-        grp.add_clock(clk_name, src)
+        return grp.add_clock(clk_name, src)
 
     def get_clock_by_name(self, name: str) -> object:
         ret = self.all_srcs.get(name)
@@ -225,28 +244,28 @@
                 ft_clks.update(grp.clocks)
                 continue
 
-            for clk, src in grp.clocks.items():
-                if src.aon:
+            for clk, sig in grp.clocks.items():
+                if sig.src.aon:
                     # Any always-on clock is a feedthrough
-                    ft_clks[clk] = src
+                    ft_clks[clk] = sig
                     continue
 
-                rg_srcs_set.add(src.name)
+                rg_srcs_set.add(sig.src.name)
 
                 if grp.sw_cg == 'no':
                     # A non-feedthrough clock with no software control
-                    rg_clks[clk] = src
+                    rg_clks[clk] = sig
                     continue
 
                 if grp.sw_cg == 'yes':
                     # A non-feedthrough clock with direct software control
-                    sw_clks[clk] = src
+                    sw_clks[clk] = sig
                     continue
 
                 # The only other valid value for the sw_cg field is "hint", which
                 # means a non-feedthrough clock with "hint" software control.
                 assert grp.sw_cg == 'hint'
-                hint_clks[clk] = src
+                hint_clks[clk] = sig
                 continue
 
         # Define a canonical ordering for rg_srcs
diff --git a/util/topgen/merge.py b/util/topgen/merge.py
index 6118823..da022b2 100644
--- a/util/topgen/merge.py
+++ b/util/topgen/merge.py
@@ -521,10 +521,6 @@
 
     exported_clks = OrderedDict()
 
-    # A dictionary mapping each clock name to the set of the names of endpoints
-    # using it
-    clock_to_eps = {}
-
     for ep in top['module'] + top['memory'] + top['xbar']:
         clock_connections = OrderedDict()
 
@@ -566,8 +562,8 @@
             clk_name = "clk_" + name
 
             # add clock to a particular group
-            clocks.add_clock_to_group(group, clk_name, clk)
-            clock_to_eps.setdefault(clk_name, set()).add(ep_name)
+            clk_sig = clocks.add_clock_to_group(group, clk_name, clk)
+            clk_sig.add_endpoint(ep_name)
 
             # add clock connections
             clock_connections[port] = hier_name + clk_name
@@ -609,13 +605,13 @@
     # match the ordering of the hint_clks list from clocks.py, since this is
     # also used to derive an enum naming the bits of the connection.
     clkmgr_idle = []
-    for clk_name in clocks.typed_clocks().hint_clks.keys():
-        ep_names = clock_to_eps[clk_name]
+    for sig in clocks.typed_clocks().hint_clks.values():
+        ep_names = list(sig.endpoints)
         if len(ep_names) != 1:
             raise ValueError(f'There are {len(ep_names)} end-points connected '
                              f'to the {clk_name} clock: {ep_names}. Where should the idle '
                              f'signal come from?')
-        ep_name = ep_names.pop()
+        ep_name = ep_names[0]
 
         # TODO: This is a hack that needs replacing properly: see note above
         #       definition of eps_with_idle. (In particular, it only works at