[reggen] Pair up clock and reset signals

This commit doesn't really change anything of note (indeed, there are
no changes to the auto-generated SystemVerilog code!), but it changes
the reggen hjson schema so that clocks and resets that belong together
get bundled together.

The idea is that a followup can define idle signals for "hint" clocks
by adding them to the ClockingItems. This is much nicer than depending
on order (it avoids counting, plus not every clock is a "hint" clock,
so not every clock needs an idle signal).

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/util/reggen/clocking.py b/util/reggen/clocking.py
new file mode 100644
index 0000000..4c2d31e
--- /dev/null
+++ b/util/reggen/clocking.py
@@ -0,0 +1,94 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+'''Code representing clocking or resets for an IP block'''
+
+from typing import Dict, List, Optional
+
+from .lib import check_keys, check_list, check_bool, check_optional_name
+
+
+class ClockingItem:
+    def __init__(self, clock: Optional[str], reset: Optional[str], primary: bool):
+        if primary:
+            assert clock is not None
+            assert reset is not None
+
+        self.clock = clock
+        self.reset = reset
+        self.primary = primary
+
+    @staticmethod
+    def from_raw(raw: object, only_item: bool, where: str) -> 'ClockingItem':
+        what = f'clocking item at {where}'
+        rd = check_keys(raw, what, [], ['clock', 'reset', 'primary'])
+
+        clock = check_optional_name(rd.get('clock'), 'clock field of ' + what)
+        reset = check_optional_name(rd.get('reset'), 'reset field of ' + what)
+        primary = check_bool(rd.get('primary', only_item),
+                             'primary field of ' + what)
+
+        if primary:
+            if clock is None:
+                raise ValueError('No clock signal for primary '
+                                 f'clocking item at {what}.')
+            if reset is None:
+                raise ValueError('No reset signal for primary '
+                                 f'clocking item at {what}.')
+
+        return ClockingItem(clock, reset, primary)
+
+    def _asdict(self) -> Dict[str, object]:
+        ret = {}  # type: Dict[str, object]
+        if self.clock is not None:
+            ret['clock'] = self.clock,
+        if self.reset is not None:
+            ret['reset'] = self.reset
+
+        ret['primary'] = self.primary
+        return ret
+
+
+class Clocking:
+    def __init__(self, items: List[ClockingItem], primary: ClockingItem):
+        assert items
+        self.items = items
+        self.primary = primary
+
+    @staticmethod
+    def from_raw(raw: object, where: str) -> 'Clocking':
+        what = f'clocking items at {where}'
+        raw_items = check_list(raw, what)
+        if not raw_items:
+            raise ValueError(f'Empty list of clocking items at {where}.')
+
+        just_one_item = len(raw_items) == 1
+
+        items = []
+        primaries = []
+        for idx, raw_item in enumerate(raw_items):
+            item_where = f'entry {idx} of {what}'
+            item = ClockingItem.from_raw(raw_item, just_one_item, item_where)
+            if item.primary:
+                primaries.append(item)
+            items.append(item)
+
+        if len(primaries) != 1:
+            raise ValueError('There should be exactly one primary clocking '
+                             f'item at {where}, but we saw {len(primaries)}.')
+
+        return Clocking(items, primaries[0])
+
+    def other_clocks(self) -> List[str]:
+        ret = []
+        for item in self.items:
+            if not item.primary and item.clock is not None:
+                ret.append(item.clock)
+        return ret
+
+    def clock_signals(self) -> List[str]:
+        return [item.clock for item in self.items if item.clock is not None]
+
+    def reset_signals(self) -> List[str]:
+        return [item.reset for item in self.items if item.reset is not None]