[reggen] Define a class to represent the registers in a block

This is used to represent obj['registers'], which was previously a
list of registers, multiregs, windows, reserved and skipto entries.
The commit doesn't replace the top-level object (obj).

Other than removing genwennames and gennextoffset from (h)json export,
the only other change is that we no longer distinguish between
reserved and skipto, which means we have to pick one format or the
other on export. This commit picks reserved (because it is more
"relocatable"), but skipto could work instead.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py b/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py
index c9d05ae..42e0020 100644
--- a/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py
+++ b/hw/ip/otbn/dv/otbnsim/sim/ext_regs.py
@@ -4,7 +4,7 @@
 
 from typing import Callable, Dict, List, Sequence
 
-from shared.otbn_reggen import Field, Register, load_registers
+from shared.otbn_reggen import Field, Register, RegBlock, load_registers
 
 from .trace import Trace
 
@@ -154,17 +154,13 @@
 
 class OTBNExtRegs:
     def __init__(self) -> None:
-        _, reg_list = load_registers()
+        _, reg_block = load_registers()
 
         self.regs = {}  # type: Dict[str, RGReg]
         self.trace = []  # type: List[TraceExtRegChange]
 
-        # We're interested in the proper registers, and don't care about
-        # anything else.
-        for entry in reg_list:
-            if not isinstance(entry, Register):
-                continue
-
+        assert isinstance(reg_block, RegBlock)
+        for entry in reg_block.flat_regs:
             assert isinstance(entry.name, str)
 
             # reggen's validation should have checked that we have no
diff --git a/hw/ip/otbn/util/shared/mem_layout.py b/hw/ip/otbn/util/shared/mem_layout.py
index 23d4b60..6ff65b1 100644
--- a/hw/ip/otbn/util/shared/mem_layout.py
+++ b/hw/ip/otbn/util/shared/mem_layout.py
@@ -18,26 +18,21 @@
 
 '''
 
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, Optional, Tuple
 
-from .otbn_reggen import load_registers, Window
+from .otbn_reggen import load_registers, RegBlock
 
 # A window is represented as (offset, size)
 _Window = Tuple[int, int]
 
 
-def extract_windows(reg_byte_width: int,
-                    registers: List[object]) -> Dict[str, _Window]:
+def extract_windows(reg_byte_width: int, regs: object) -> Dict[str, _Window]:
     '''Make sense of the list of register definitions and extract memories'''
 
-    # Conveniently, reggen's validate method stores 'genoffset' (the offset to
-    # the start) for each window, so we can just look that up.
     windows = {}
 
-    for entry in registers:
-        if not isinstance(entry, Window):
-            continue
-
+    assert isinstance(regs, RegBlock)
+    for entry in regs.windows:
         name = entry.name or 'Window at +{:#x}'.format(entry.offset)
 
         # Should be guaranteed by RegBlock constructor
diff --git a/hw/ip/otbn/util/shared/otbn_reggen.py b/hw/ip/otbn/util/shared/otbn_reggen.py
index 588b1e4..eaa4397 100644
--- a/hw/ip/otbn/util/shared/otbn_reggen.py
+++ b/hw/ip/otbn/util/shared/otbn_reggen.py
@@ -6,7 +6,7 @@
 
 import os
 import sys
-from typing import List, Optional, Tuple
+from typing import Optional, Tuple
 
 import hjson  # type: ignore
 
@@ -23,6 +23,7 @@
     import reggen.field  # type: ignore
     import reggen.register  # type: ignore
     import reggen.window  # type: ignore
+    import reggen.reg_block   # type: ignore
 finally:
     sys.path = _OLD_SYS_PATH
 
@@ -31,11 +32,12 @@
 Register = reggen.register.Register
 Field = reggen.field.Field
 Window = reggen.window.Window
+RegBlock = reggen.reg_block.RegBlock
 
-_LR_RETVAL = None  # type: Optional[Tuple[int, List[object]]]
+_LR_RETVAL = None  # type: Optional[Tuple[int, object]]
 
 
-def load_registers() -> Tuple[int, List[object]]:
+def load_registers() -> Tuple[int, object]:
     '''Load otbn.hjson with reggen
 
     Returns (width, regs) where width is the register width and regs is a
@@ -73,6 +75,6 @@
     # The validation code would also have exploded if it wasn't a list of
     # dictionaries, so we can assert the type safely.
     registers = obj['registers']
-    assert isinstance(registers, list)
+    assert isinstance(registers, RegBlock)
     _LR_RETVAL = (reg_byte_width, registers)
     return _LR_RETVAL
diff --git a/util/reggen/data.py b/util/reggen/data.py
index db05f31..634827d 100644
--- a/util/reggen/data.py
+++ b/util/reggen/data.py
@@ -5,11 +5,8 @@
 from collections import OrderedDict
 import re
 
-from .multi_register import MultiRegister
-from .register import Register
 
-
-# helper funtion that strips trailing _number (used as multireg suffix) from name
+# helper function that strips trailing _number (used as multireg suffix) from name
 # TODO: this is a workaround, should solve this in validate.py
 def get_basename(name):
     match = re.search(r'_[0-9]+$', name)
@@ -26,35 +23,7 @@
         self.base_addr = OrderedDict()
         self.name = ""
         self.hier_path = ""
-        self.regs = []
-        self.wins = []
+        self.reg_block = None
         self.blocks = []
         self.params = []
         self.tags = []
-
-    def get_regs_flat(self):
-        """Returns flattened register list
-        """
-        regs = []
-        for r in self.regs:
-            if isinstance(r, Register):
-                regs.append(r)
-            else:
-                assert isinstance(r, MultiRegister)
-                regs += r.regs
-
-        return regs
-
-    def get_n_bits(self, bittype=["q"]):
-        """Returns number of bits in this block (including all multiregs and
-        fields). By default this function counts read data bits (bittype "q"),
-        but other bits such as "d", qe", "re", "de" can be counted as well by
-        specifying them in the bittype list argument.
-        """
-        n_bits = 0
-        for r in self.regs:
-            n_bits += r.get_n_bits(bittype)
-        return n_bits
-
-    def get_n_regs_flat(self):
-        return len(self.get_regs_flat())
diff --git a/util/reggen/fpv_csr.sv.tpl b/util/reggen/fpv_csr.sv.tpl
index 22f8c53..de96822 100644
--- a/util/reggen/fpv_csr.sv.tpl
+++ b/util/reggen/fpv_csr.sv.tpl
@@ -158,7 +158,7 @@
         (d2h.d_error || (d2h.d_data & mask) >> lsb == exp_data);
   endproperty
 
-% for r in block.regs:
+% for r in block.reg_block.all_regs:
 <%
   has_q  = r.get_n_bits(["q"]) > 0
   has_d  = r.get_n_bits(["d"]) > 0
diff --git a/util/reggen/gen_cheader.py b/util/reggen/gen_cheader.py
index 29e7023..6fdbf7b 100644
--- a/util/reggen/gen_cheader.py
+++ b/util/reggen/gen_cheader.py
@@ -329,7 +329,7 @@
     gen_cdefines_interrupts(outstr, regs, component, regwidth,
                             existing_defines)
 
-    for x in registers:
+    for x in registers.entries:
         if isinstance(x, Register):
             gen_cdefine_register(outstr, x, component, regwidth, rnames,
                                  existing_defines)
diff --git a/util/reggen/gen_html.py b/util/reggen/gen_html.py
index e063913..6e01600 100644
--- a/util/reggen/gen_html.py
+++ b/util/reggen/gen_html.py
@@ -296,7 +296,7 @@
     else:
         regwidth = 32
 
-    for x in registers:
+    for x in registers.entries:
         if isinstance(x, Register):
             gen_html_register(outfile, x, component, regwidth, rnames, toclist,
                               toclevel)
diff --git a/util/reggen/gen_json.py b/util/reggen/gen_json.py
index c593cc1..18c1b5a 100644
--- a/util/reggen/gen_json.py
+++ b/util/reggen/gen_json.py
@@ -8,6 +8,12 @@
 
 
 def gen_json(obj, outfile, format):
+    # Temporary hack to deal with the fact that the 'registers' field is a list
+    # rather than a dictionary. When we convert the top-level object to a class
+    # (with its own _as_dict method), this logic can go in there.
+    obj = obj.copy()
+    obj['registers'] = obj['registers'].as_dicts()
+
     if format == 'json':
         hjson.dumpJSON(obj,
                        outfile,
diff --git a/util/reggen/gen_rtl.py b/util/reggen/gen_rtl.py
index 7a35652..5bc04dd 100644
--- a/util/reggen/gen_rtl.py
+++ b/util/reggen/gen_rtl.py
@@ -12,9 +12,6 @@
 
 from .access import HwAccess, SwRdAccess, SwWrAccess
 from .data import Block
-from .register import Register
-from .multi_register import MultiRegister
-from .window import Window
 
 
 def escape_name(name):
@@ -55,14 +52,7 @@
 
     block.hier_path = obj["hier_path"] if "hier_path" in obj else ""
 
-    for r in obj["registers"]:
-        if isinstance(r, Register) or isinstance(r, MultiRegister):
-            block.regs.append(r)
-            continue
-
-        if isinstance(r, Window):
-            block.wins.append(r)
-            continue
+    block.reg_block = obj['registers']
 
     # Last offset and calculate space
     #  Later on, it could use block.regs[-1].genoffset
diff --git a/util/reggen/multi_register.py b/util/reggen/multi_register.py
index 7e1bbd4..37bc9ee 100644
--- a/util/reggen/multi_register.py
+++ b/util/reggen/multi_register.py
@@ -118,6 +118,9 @@
                                       min_reg_idx, max_reg_idx, self.cname)
             self.regs.append(reg)
 
+    def next_offset(self, addrsep: int) -> int:
+        return self.offset + len(self.regs) * addrsep
+
     def get_n_bits(self, bittype: List[str] = ["q"]) -> int:
         return sum(reg.get_n_bits(bittype) for reg in self.regs)
 
diff --git a/util/reggen/reg_block.py b/util/reggen/reg_block.py
new file mode 100644
index 0000000..5b7ebcc
--- /dev/null
+++ b/util/reggen/reg_block.py
@@ -0,0 +1,263 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+'''Code representing the registers, windows etc. for a block'''
+
+import re
+from typing import Dict, List, Union
+
+from .lib import check_int, check_list, check_str_dict
+from .multi_register import MultiRegister
+from .register import Register
+from .window import Window
+
+
+class RegBlock:
+    def __init__(self,
+                 addrsep: int,
+                 reg_width: int,
+                 params: List[Dict[str, object]]):
+
+        self._addrsep = addrsep
+        self._reg_width = reg_width
+        self._params = params
+
+        self.offset = 0
+        self.multiregs = []  # type: List[MultiRegister]
+        self.registers = []  # type: List[Register]
+        self.windows = []  # type: List[Window]
+
+        # A list of all registers, expanding multiregs, ordered by offset
+        self.flat_regs = []  # type: List[Register]
+
+        # A list of registers and multiregisters (unexpanded)
+        self.all_regs = []  # type: List[Union[Register, MultiRegister]]
+
+        # A list with everything in order
+        self.entries = []  # type: List[object]
+
+        # A dict of named entries, mapping name to offset
+        self.name_to_offset = {}  # type: Dict[str, int]
+
+        # A dict of all registers (expanding multiregs), mapping name to the
+        # register object
+        self.name_to_flat_reg = {}  # type: Dict[str, Register]
+
+        # A list of all write enable names
+        self.wennames = []  # type: List[str]
+
+    def add_raw_registers(self, raw: object) -> None:
+        rl = check_list(raw, 'registers field at top-level')
+        for entry_idx, entry_raw in enumerate(rl):
+            where = ('entry {} of the top-level registers field'
+                     .format(entry_idx + 1))
+            self.add_raw(where, entry_raw)
+
+    def add_raw(self, where: str, raw: object) -> None:
+        entry = check_str_dict(raw, where)
+
+        handlers = {
+            'register': self._handle_register,
+            'reserved': self._handle_reserved,
+            'skipto': self._handle_skipto,
+            'window': self._handle_window,
+            'multireg': self._handle_multireg
+        }
+
+        entry_type = 'register'
+        entry_body = entry  # type: object
+
+        for t in ['reserved', 'skipto', 'window', 'multireg']:
+            t_body = entry.get(t)
+            if t_body is not None:
+                # Special entries look like { window: { ... } }, so if we
+                # get a hit, this should be the only key in entry. Note
+                # that this also checks that nothing has more than one
+                # entry type.
+                if len(entry) != 1:
+                    other_keys = [k for k in entry if k != t]
+                    assert other_keys
+                    raise ValueError('At offset {:#x}, {} has key {}, which '
+                                     'should give its type. But it also has '
+                                     'other keys too: {}.'
+                                     .format(self.offset,
+                                             where, t, ', '.join(other_keys)))
+                entry_type = t
+                entry_body = t_body
+
+        entry_where = ('At offset {:#x}, {}, type {!r}'
+                       .format(self.offset, where, entry_type))
+
+        handlers[entry_type](entry_where, entry_body)
+
+    def _handle_register(self, where: str, body: object) -> None:
+        reg = Register.from_raw(self._reg_width,
+                                self.offset, self._params, body)
+        self.add_register(reg)
+
+    def _handle_reserved(self, where: str, body: object) -> None:
+        nreserved = check_int(body, 'body of ' + where)
+        if nreserved <= 0:
+            raise ValueError('Reserved count in {} is {}, '
+                             'which is not positive.'
+                             .format(where, nreserved))
+
+        self.offset += self._addrsep * nreserved
+
+    def _handle_skipto(self, where: str, body: object) -> None:
+        skipto = check_int(body, 'body of ' + where)
+        if skipto < self.offset:
+            raise ValueError('Destination of skipto in {} is {:#x}, '
+                             'is less than the current offset, {:#x}.'
+                             .format(where, skipto, self.offset))
+        if skipto % self._addrsep:
+            raise ValueError('Destination of skipto in {} is {:#x}, '
+                             'not a multiple of addrsep, {:#x}.'
+                             .format(where, skipto, self._addrsep))
+        self.offset = skipto
+
+    def _handle_window(self, where: str, body: object) -> None:
+        window = Window.from_raw(self.offset,
+                                 self._reg_width, self._params, body)
+        if window.name is not None:
+            lname = window.name.lower()
+            if lname in self.name_to_offset:
+                raise ValueError('Window {} (at offset {:#x}) has the '
+                                 'same name as something at offset {:#x}.'
+                                 .format(window.name, window.offset,
+                                         self.name_to_offset[lname]))
+        self.add_window(window)
+
+    def _handle_multireg(self, where: str, body: object) -> None:
+        mr = MultiRegister(self.offset,
+                           self._addrsep, self._reg_width, self._params, body)
+        for reg in mr.regs:
+            lname = reg.name.lower()
+            if lname in self.name_to_offset:
+                raise ValueError('Multiregister {} (at offset {:#x}) expands '
+                                 'to a register with name {} (at offset '
+                                 '{:#x}), but this already names something at '
+                                 'offset {:#x}.'
+                                 .format(mr.reg.name, mr.reg.offset,
+                                         reg.name, reg.offset,
+                                         self.name_to_offset[lname]))
+            self._add_flat_reg(reg)
+            self.name_to_offset[lname] = reg.offset
+
+        self.multiregs.append(mr)
+        self.all_regs.append(mr)
+        self.entries.append(mr)
+        self.offset = mr.next_offset(self._addrsep)
+
+    def add_register(self, reg: Register) -> None:
+        assert reg.offset == self.offset
+
+        lname = reg.name.lower()
+        if lname in self.name_to_offset:
+            raise ValueError('Register {} (at offset {:#x}) has the same '
+                             'name as something at offset {:#x}.'
+                             .format(reg.name, reg.offset,
+                                     self.name_to_offset[lname]))
+        self._add_flat_reg(reg)
+        self.name_to_offset[lname] = reg.offset
+
+        self.registers.append(reg)
+        self.all_regs.append(reg)
+        self.entries.append(reg)
+        self.offset = reg.next_offset(self._addrsep)
+
+        if reg.regwen is not None and reg.regwen not in self.wennames:
+            self.wennames.append(reg.regwen)
+
+    def _add_flat_reg(self, reg: Register) -> None:
+        # The first assertion is checked at the call site (where we can print
+        # out a nicer message for multiregs). The second assertion should be
+        # implied by the first.
+        assert reg.name not in self.name_to_offset
+        assert reg.name not in self.name_to_flat_reg
+
+        self.flat_regs.append(reg)
+        self.name_to_flat_reg[reg.name.lower()] = reg
+
+    def add_window(self, window: Window) -> None:
+        if window.name is not None:
+            lname = window.name.lower()
+            assert lname not in self.name_to_offset
+            self.name_to_offset[lname] = window.offset
+
+        self.windows.append(window)
+        self.entries.append(window)
+        assert self.offset <= window.offset
+        self.offset = window.next_offset(self._addrsep)
+
+    def validate(self) -> None:
+        '''Run this to check consistency after all registers have been added'''
+
+        # Check that every write-enable register has a good name, a valid reset
+        # value, and valid access permissions.
+        for wenname in self.wennames:
+            # check the REGWEN naming convention
+            if re.fullmatch(r'(.+_)*REGWEN(_[0-9]+)?', wenname) is None:
+                raise ValueError("Regwen name {} must have the suffix '_REGWEN'"
+                                 .format(wenname))
+
+            wen_reg = self.name_to_flat_reg.get(wenname.lower())
+            if wen_reg is None:
+                raise ValueError('One or more registers use {} as a '
+                                 'write-enable, but there is no such register.'
+                                 .format(wenname))
+
+            # If the REGWEN bit is SW controlled, check that the register
+            # defaults to enabled. If this bit is read-only by SW and hence
+            # hardware controlled, we do not enforce this requirement.
+            if wen_reg.swaccess.key != "ro" and not wen_reg.resval:
+                raise ValueError('One or more registers use {} as a '
+                                 'write-enable. Since it is SW-controlled '
+                                 'it should have a nonzero reset value.'
+                                 .format(wenname))
+
+            if wen_reg.swaccess.key == "rw0c":
+                # The register is software managed: all good!
+                continue
+
+            if wen_reg.swaccess.key == "ro" and wen_reg.hwaccess.key == "hwo":
+                # The register is hardware managed: that's fine too.
+                continue
+
+            raise ValueError('One or more registers use {} as a write-enable. '
+                             'However, it has invalid access permissions '
+                             '({} / {}). It should either have swaccess=RW0C '
+                             'or have swaccess=RO and hwaccess=HWO.'
+                             .format(wenname,
+                                     wen_reg.swaccess.key,
+                                     wen_reg.hwaccess.key))
+
+    def get_n_bits(self, bittype: List[str] = ["q"]) -> int:
+        '''Returns number of bits in registers in this block.
+
+        This includes those expanded from multiregs. See Field.get_n_bits for a
+        description of the bittype argument.
+
+        '''
+        return sum(reg.get_n_bits(bittype) for reg in self.flat_regs)
+
+    def as_dicts(self) -> List[object]:
+        entries = []  # type: List[object]
+        offset = 0
+        for entry in self.entries:
+            assert (isinstance(entry, Register) or
+                    isinstance(entry, MultiRegister) or
+                    isinstance(entry, Window))
+
+            next_off = entry.offset
+            assert offset <= next_off
+            res_bytes = next_off - offset
+            if res_bytes:
+                assert res_bytes % self._addrsep == 0
+                entries.append({'reserved': res_bytes // self._addrsep})
+
+            entries.append(entry)
+            offset = entry.next_offset(self._addrsep)
+
+        return entries
diff --git a/util/reggen/reg_pkg.sv.tpl b/util/reggen/reg_pkg.sv.tpl
index 75da1a1..cef644a 100644
--- a/util/reggen/reg_pkg.sv.tpl
+++ b/util/reggen/reg_pkg.sv.tpl
@@ -9,8 +9,9 @@
   from reggen.register import Register
   from reggen.multi_register import MultiRegister
 
-  num_regs = block.get_n_regs_flat()
-  max_regs_char = len("{}".format(num_regs-1))
+  flat_regs = block.reg_block.flat_regs
+  num_regs = len(flat_regs)
+  max_regs_char = len("{}".format(num_regs - 1))
 %>\
 package ${block.name}_reg_pkg;
 % if len(block.params) != 0:
@@ -27,7 +28,7 @@
   ////////////////////////////
   // Typedefs for registers //
   ////////////////////////////
-% for r in block.regs:
+% for r in block.reg_block.all_regs:
   % if r.get_n_bits(["q"]):
 <%
     if isinstance(r, Register):
@@ -95,7 +96,7 @@
   %endif
 % endfor
 
-% for r in block.regs:
+% for r in block.reg_block.all_regs:
   % if r.get_n_bits(["d"]):
 <%
     if isinstance(r, Register):
@@ -153,12 +154,12 @@
   // Register to internal design logic //
   ///////////////////////////////////////
 <%
-nbits = block.get_n_bits(["q", "qe", "re"])
+nbits = block.reg_block.get_n_bits(["q", "qe", "re"])
 packbit = 0
 %>\
 % if nbits > 0:
   typedef struct packed {
-% for r in block.regs:
+% for r in block.reg_block.all_regs:
   % if r.get_n_bits(["q"]):
 <%
     if isinstance(r, MultiRegister):
@@ -188,12 +189,12 @@
   // Internal design logic to register //
   ///////////////////////////////////////
 <%
-nbits = block.get_n_bits(["d", "de"])
+nbits = block.reg_block.get_n_bits(["d", "de"])
 packbit = 0
 %>\
 % if nbits > 0:
   typedef struct packed {
-% for r in block.regs:
+% for r in block.reg_block.all_regs:
   % if r.get_n_bits(["d"]):
 <%
     if isinstance(r, MultiRegister):
@@ -222,7 +223,6 @@
   // Register Address
 <%
 ublock = block.name.upper()
-flat_regs = block.get_regs_flat()
 
 def reg_pfx(reg):
   return '{}_{}'.format(ublock, reg.name.upper())
@@ -261,9 +261,9 @@
   % endfor
 
 % endif
-% if len(block.wins) > 0:
+% if len(block.reg_block.windows) > 0:
   // Window parameter
-% for i,w in enumerate(block.wins):
+% for i,w in enumerate(block.reg_block.windows):
 <%
     win_pfx = '{}_{}'.format(ublock, w.name.upper())
     base_txt_val = "{}'h {:x}".format(block.addr_width, w.offset)
@@ -276,14 +276,14 @@
 % endif
   // Register Index
   typedef enum int {
-% for r in block.get_regs_flat():
+% for r in flat_regs:
     ${ublock}_${r.name.upper()}${"" if loop.last else ","}
 % endfor
   } ${block.name}_id_e;
 
   // Register width information to check illegal writes
-  parameter logic [3:0] ${ublock}_PERMIT [${block.get_n_regs_flat()}] = '{
-% for i,r in enumerate(block.get_regs_flat()):
+  parameter logic [3:0] ${ublock}_PERMIT [${len(flat_regs)}] = '{
+% for i,r in enumerate(flat_regs):
 <%
   index_str = "{}".format(i).rjust(max_regs_char)
   width = r.get_width()
diff --git a/util/reggen/reg_top.sv.tpl b/util/reggen/reg_top.sv.tpl
index ebdbc6d..68995ba 100644
--- a/util/reggen/reg_top.sv.tpl
+++ b/util/reggen/reg_top.sv.tpl
@@ -8,11 +8,11 @@
   from reggen.register import Register
   from reggen.multi_register import MultiRegister
 
-  num_wins = len(block.wins)
+  num_wins = len(block.reg_block.windows)
   num_wins_width = ((num_wins+1).bit_length()) - 1
   num_dsp  = num_wins + 1
-  max_regs_char = len("{}".format(block.get_n_regs_flat()-1))
-  regs_flat = block.get_regs_flat()
+  regs_flat = block.reg_block.flat_regs
+  max_regs_char = len("{}".format(len(regs_flat) - 1))
 %>
 `include "prim_assert.sv"
 
@@ -31,10 +31,10 @@
 
 % endif
   // To HW
-% if block.get_n_bits(["q","qe","re"]):
+% if block.reg_block.get_n_bits(["q","qe","re"]):
   output ${block.name}_reg_pkg::${block.name}_reg2hw_t reg2hw, // Write
 % endif
-% if block.get_n_bits(["d","de"]):
+% if block.reg_block.get_n_bits(["d","de"]):
   input  ${block.name}_reg_pkg::${block.name}_hw2reg_t hw2reg, // Read
 % endif
 
@@ -91,7 +91,7 @@
   assign tl_reg_h2d = tl_socket_h2d[${num_wins}];
   assign tl_socket_d2h[${num_wins}] = tl_reg_d2h;
 
-  % for i,t in enumerate(block.wins):
+  % for i,t in enumerate(block.reg_block.windows):
   assign tl_win_o[${i}] = tl_socket_h2d[${i}];
   assign tl_socket_d2h[${i}] = tl_win_i[${i}];
   % endfor
@@ -122,7 +122,7 @@
     reg_steer = ${num_dsp-1};       // Default set to register
 
     // TODO: Can below codes be unique case () inside ?
-  % for i,w in enumerate(block.wins):
+  % for i,w in enumerate(block.reg_block.windows):
 <%
     base_addr = w.offset
     limit_addr = w.offset + w.size_in_bytes
@@ -178,7 +178,7 @@
   % endfor
 
   // Register instances
-  % for r in block.regs:
+  % for r in block.reg_block.all_regs:
   ######################## multiregister ###########################
     % if isinstance(r, MultiRegister):
 <%
@@ -237,7 +237,7 @@
       % endfor
     % endif
 
-  ## for: block.regs
+  ## for: block.reg_block.all_regs
   % endfor
 
 
diff --git a/util/reggen/register.py b/util/reggen/register.py
index d35fa53..28ff4fc 100644
--- a/util/reggen/register.py
+++ b/util/reggen/register.py
@@ -262,6 +262,9 @@
                         tags, resval, shadowed, fields,
                         update_err_alert, storage_err_alert)
 
+    def next_offset(self, addrsep: int) -> int:
+        return self.offset + addrsep
+
     def sw_readable(self) -> bool:
         return self.swaccess.key not in ['wo', 'r0w1c']
 
diff --git a/util/reggen/uvm_reg.sv.tpl b/util/reggen/uvm_reg.sv.tpl
index 28174f9..e51e692 100644
--- a/util/reggen/uvm_reg.sv.tpl
+++ b/util/reggen/uvm_reg.sv.tpl
@@ -11,7 +11,7 @@
 ${construct_classes(b)}
 % endfor
 <%
-regs_flat = block.get_regs_flat()
+regs_flat = block.reg_block.flat_regs
 hier_path = ""
 if (block.hier_path):
   hier_path = block.hier_path + "."
@@ -36,7 +36,7 @@
 % for r in regs_flat:
   typedef class ${gen_dv.rcname(block, r)};
 % endfor
-% for w in block.wins:
+% for w in block.reg_block.windows:
   typedef class ${gen_dv.mcname(block, w)};
 % endfor
   typedef class ${gen_dv.bcname(block)};
@@ -152,7 +152,7 @@
   endclass : ${gen_dv.rcname(block, r)}
 
 % endfor
-% for w in block.wins:
+% for w in block.reg_block.windows:
 <%
   mem_name = w.name.lower()
   mem_right = w.swaccess.dv_rights()
@@ -194,10 +194,10 @@
 % for r in regs_flat:
     rand ${gen_dv.rcname(block, r)} ${r.name.lower()};
 % endfor
-% if block.wins:
+% if block.reg_block.windows:
     // memories
 % endif
-% for w in block.wins:
+% for w in block.reg_block.windows:
     rand ${gen_dv.mcname(block, w)} ${gen_dv.miname(w)};
 % endfor
 
@@ -279,11 +279,11 @@
   % endif
 % endfor
 
-% if block.wins:
+% if block.reg_block.windows:
 
       // create memories
 % endif
-% for w in block.wins:
+% for w in block.reg_block.windows:
 <%
   mem_name = w.name.lower()
   mem_right = w.swaccess.dv_rights()
diff --git a/util/reggen/validate.py b/util/reggen/validate.py
index 210f05c..dd07b86 100644
--- a/util/reggen/validate.py
+++ b/util/reggen/validate.py
@@ -6,15 +6,13 @@
 """
 
 import logging as log
-import re
 from collections import OrderedDict
 
 from .access import SWAccess, HWAccess
 from .bits import Bits
 from .field import Field
-from .multi_register import MultiRegister
+from .reg_block import RegBlock
 from .register import Register
-from .window import Window
 
 
 # Routine that can be used for Hjson object_pairs_hook
@@ -233,21 +231,6 @@
     return error
 
 
-# Only allow zero or one of the list of keys
-def check_zero_one_key(obj, optone, err_prefix):
-    error = 0
-    seenopt = 0
-    for x in obj:
-        if (x in optone):
-            seenopt += 1
-    if (seenopt > 1) or ((seenopt == 1) and len(obj) > 1):
-        log.error(err_prefix + " only allowed one option key: ")
-        for x in obj:
-            log.error(err_prefix + "   found: " + x)
-            error += 1
-    return error
-
-
 val_types = {
     'd': ["int", "integer (binary 0b, octal 0o, decimal, hex 0x)"],
     'x': ["xint", "x for undefined otherwise int"],
@@ -332,8 +315,6 @@
 top_added = {
     'genrnames': ['pl', "list of register names"],
     'genautoregs': ['pb', "Registers were generated from config info"],
-    'genwennames': ['pl', "list of registers used as write enables"],
-    'gennextoffset': ['pi', "offset next register would use"],
     'gensize': [
         'pi', "address space size needed for registers. "
         "Generated by tool as next power of 2."
@@ -381,23 +362,7 @@
 key_use = {'r': "required", 'o': "optional", 'a': "added by tool"}
 
 
-def _upd_regnames(regs, offset, register):
-    genrnames = regs['genrnames']
-    rname = register.name.lower()
-    err = 0
-    if rname in genrnames:
-        log.error('Duplicate register name {!r} at offset {:#x}.'
-                  .format(offset, rname))
-        err = 1
-    genrnames.append(rname)
-    genwennames = regs['genwennames']
-    if register.regwen is not None and register.regwen not in genwennames:
-        genwennames.append(register.regwen)
-
-    return err
-
-
-def make_intr_alert_reg(regs, name, offset, swaccess, hwaccess, desc):
+def make_intr_alert_reg(reg_block, regs, name, swaccess, hwaccess, desc):
     if name == 'ALERT_TEST':
         signal_list = regs['alert_list']
     else:
@@ -464,7 +429,7 @@
 
     bool_hwext = hwext.lower() == 'true'
 
-    reg = Register(offset,
+    reg = Register(reg_block.offset,
                    name,
                    desc,
                    swaccess_obj,
@@ -479,11 +444,11 @@
                    fields=fields,
                    update_err_alert=None,
                    storage_err_alert=None)
-    _upd_regnames(regs, offset, reg)
+    reg_block.add_register(reg)
     return reg
 
 
-def make_intr_regs(regs, offset, addrsep, fullwidth):
+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])
@@ -491,19 +456,24 @@
         log.error('More than ' + str(fullwidth) + ' interrupts in list')
         return iregs, 1
 
-    new_reg = make_intr_alert_reg(regs, 'INTR_STATE', offset, 'rw1c', 'hrw',
-                                  'Interrupt State Register')
-    iregs.append(new_reg)
-    new_reg = make_intr_alert_reg(regs, 'INTR_ENABLE', offset + addrsep, 'rw',
-                                  'hro', 'Interrupt Enable Register')
-    iregs.append(new_reg)
-    new_reg = make_intr_alert_reg(regs, 'INTR_TEST', offset + 2 * addrsep,
-                                  'wo', 'hro', 'Interrupt Test Register')
-    iregs.append(new_reg)
+    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(regs, offset, addrsep, fullwidth):
+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])
@@ -511,84 +481,17 @@
         log.error('More than ' + str(fullwidth) + ' alerts in list')
         return alert_regs, 1
 
-    new_reg = make_intr_alert_reg(regs, 'ALERT_TEST', offset, 'wo', 'hro',
-                                  'Alert Test Register')
-    alert_regs.append(new_reg)
+    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
 
 
-""" Check that terms specified for regwen exist
-
-Regwen are all assumed to be individual registers.
-"""
-
-
-def check_wen_regs(regs):
-    error = 0
-
-    # Construct a map from register name to a tuple (resval, swaccess, hwaccess)
-    name_to_reg_data = {}
-    for x in regs['registers']:
-        if isinstance(x, Register):
-            x_regs = [x]
-        elif isinstance(x, MultiRegister):
-            x_regs = x.regs
-        else:
-            x_regs = []
-
-        for reg in x_regs:
-            assert isinstance(reg, Register)
-            reg_data = (reg.resval, reg.swaccess, reg.hwaccess)
-            name_to_reg_data[reg.name.lower()] = reg_data
-
-    # check for reset value
-    # both w1c and w0c are acceptable, ro is also acceptable when hwaccess is wo (hw managed regwen)
-    for x in regs['genwennames']:
-
-        # check the REGWEN naming convention
-        if re.fullmatch(r'(.+_)*REGWEN(_[0-9]+)?', x) is None:
-            error += 1
-            log.error("Regwen name %s must have the suffix '_REGWEN'" % x)
-
-        target = x.lower()
-        log.debug("check_wen_regs::Searching for %s" % target)
-
-        reg_data = name_to_reg_data.get(target)
-        if reg_data is None:
-            error += 1
-            log.error("Could not find register name matching %s" % target)
-            continue
-
-        resval, swaccess, hwaccess = reg_data
-
-        # If the REGWEN bit is SW controlled, enfore that this bit defaults to 1.
-        # If this bit is read-only by SW and hence hardware controlled, we do
-        # not enforce this requirement.
-        if swaccess.key != "ro" and not resval:
-            error += 1
-            log.error(x + " used as regwen fails requirement to default " +
-                      "to 1")
-
-        # either the regwen is software managed (must be rw0c)
-        # or it is completely hw managed (sw=r0 and hw=wo)
-        sw_regwen = 0
-        hw_regwen = 0
-
-        if swaccess.key in ["rw0c"]:
-            sw_regwen += 1
-
-        if swaccess.key == "ro" and hwaccess.key == "hwo":
-            hw_regwen += 1
-
-        if (sw_regwen + hw_regwen) == 0:
-            error += 1
-            log.error(
-                "{x} used as regwen fails requirement to be "
-                "swaccess=RW0C or swaccess=RO and hwaccess=HWO".format(x=x))
-
-    return error
-
-
 def validate(regs, **kwargs):
     if "params" in kwargs:
         params = kwargs["params"]
@@ -608,7 +511,6 @@
         log.error("Component has top level errors. Aborting.")
         return error
     regs['genrnames'] = []
-    regs['genwennames'] = []
     error = 0
 
     if 'regwidth' in regs:
@@ -627,7 +529,8 @@
     else:
         addrsep = fullwidth // 8
 
-    offset = 0
+    reg_block = RegBlock(addrsep, fullwidth, regs.get('param_list', []))
+
     autoregs = []
 
     # auto header generation would go here and update autoregs
@@ -655,19 +558,17 @@
             no_auto_alerts = False
 
     if 'interrupt_list' in regs and 'genautoregs' not in regs and not no_auto_intr:
-        iregs, err = make_intr_regs(regs, offset, addrsep, fullwidth)
+        iregs, err = make_intr_regs(reg_block, regs, fullwidth)
         error += err
         autoregs.extend(iregs)
-        offset += addrsep * len(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(regs, offset, addrsep, fullwidth)
+            aregs, err = make_alert_regs(reg_block, regs, fullwidth)
             error += err
             autoregs.extend(aregs)
-            offset += addrsep * len(aregs)
 
         num_alerts = 0
         for alert in regs['alert_list']:
@@ -704,7 +605,7 @@
                         'Conflicting definition of NumAlerts parameter found.')
                     error += 1
             else:
-                # Generate the NumAlerts parameter.
+                # Generate the NumAlerts parameter.x
                 regs['param_list'].append({
                     'name': 'NumAlerts',
                     'type': 'int',
@@ -744,109 +645,22 @@
     else:
         regs["scan"] = "false"
 
-    vld_regs = []
-    for x in regs['registers']:
-        ck_err = check_zero_one_key(x, list_optone, "At " + hex(offset))
-        if ck_err != 0:
-            error += ck_err
-            continue
+    reg_block.add_raw_registers(regs['registers'])
 
-        if 'reserved' in x:
-            nreserved, ierr = check_int(x['reserved'],
-                                        "Reserved at " + hex(offset))
-            if ierr:
-                error += 1
-            else:
-                offset = offset + (addrsep * nreserved)
+    regs['gensize'] = 1 << (reg_block.offset - 1).bit_length()
 
-            vld_regs.append(x)
-            continue
-
-        if 'skipto' in x:
-            skipto, ierr = check_int(x['skipto'], "skipto at " + hex(offset))
-            if ierr:
-                error += 1
-            elif (skipto < offset):
-                log.error("{skipto " + x['skipto'] + "} at " + hex(offset) +
-                          " evaluates as " + hex(skipto) +
-                          " which would move backwards")
-                error += 1
-            elif (skipto % addrsep) != 0:
-                log.error("{skipto " + x['skipto'] + "} at " + hex(offset) +
-                          " evaluates as " + hex(skipto) +
-                          " which is not a multiple of the register size " +
-                          str(addrsep))
-                error += 1
-            else:
-                offset = skipto
-
-            vld_regs.append(x)
-            continue
-
-        if 'window' in x:
-            try:
-                window = Window.from_raw(offset, fullwidth,
-                                         regs.get('param_list', []),
-                                         x['window'])
-                vld_regs.append(window)
-                offset = window.offset + window.size_in_bytes
-
-                if window.name is not None:
-                    if window.name in regs['genrnames']:
-                        log.error('Duplicate window name {!r} at offset {:#x}.'
-                                  .format(offset, window.name))
-                        error += 1
-                    regs['genrnames'].append(window.name.lower())
-            except ValueError as err:
-                log.error('Error in window at offset {:#x}: {}'
-                          .format(offset, err))
-                error += 1
-
-            continue
-
-        if 'multireg' in x:
-            try:
-                multi_reg = MultiRegister(offset, addrsep, fullwidth,
-                                          regs.get('param_list', []),
-                                          x['multireg'])
-                vld_regs.append(multi_reg)
-                for reg in multi_reg.regs:
-                    error += _upd_regnames(regs, offset, reg)
-                offset += addrsep * len(multi_reg.regs)
-            except ValueError as err:
-                log.error('Error in multireg at offset {:#x}: {}'
-                          .format(offset, err))
-                error += 1
-            continue
-
-        try:
-            reg = Register.from_raw(fullwidth, offset,
-                                    regs.get('param_list', []),
-                                    x)
-            vld_regs.append(reg)
-            error += _upd_regnames(regs, offset, reg)
-        except ValueError as err:
-            log.error('Error in register at offset {:#x}: {}'
-                      .format(offset, err))
-            error += 1
-        offset += addrsep
-
-    regs['registers'] = vld_regs
-
-    regs['gennextoffset'] = offset
-    # make the right thing happen if now exactly on power of 2
-    if offset > 0:
-        offset -= 1
-    regs['gensize'] = 1 << offset.bit_length()
-
-    error += check_wen_regs(regs)
+    try:
+        reg_block.validate()
+    except ValueError as err:
+        log.error(str(err))
+        error += 1
 
     if autoregs:
-        # auto generated registers go at the front
-        autoregs.extend(regs['registers'])
-        regs['registers'] = 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):
diff --git a/util/reggen/window.py b/util/reggen/window.py
index b151d85..d30f2c5 100644
--- a/util/reggen/window.py
+++ b/util/reggen/window.py
@@ -147,6 +147,9 @@
         return Window(name, desc, unusual, byte_write,
                       validbits, items, size_in_bytes, offset, swaccess)
 
+    def next_offset(self, addrsep: int) -> int:
+        return self.offset + self.size_in_bytes
+
     def _asdict(self) -> Dict[str, object]:
         rd = {
             'desc': self.desc,
diff --git a/util/topgen.py b/util/topgen.py
index 52cb48e..3ae4c51 100755
--- a/util/topgen.py
+++ b/util/topgen.py
@@ -20,6 +20,7 @@
 
 import tlgen
 from reggen import access, gen_dv, gen_rtl, validate, window
+from reggen.reg_block import RegBlock
 from topgen import amend_clocks, get_hjsonobj_xbars
 from topgen import intermodule as im
 from topgen import merge_top, search_ips, validate_top
@@ -833,7 +834,12 @@
     assert top_block.width % 8 == 0
     reg_width_in_bytes = top_block.width // 8
 
-    # add memories
+    top_block.reg_block = RegBlock(reg_width_in_bytes,
+                                   top_block.width,
+                                   [])
+
+    # Add memories (in order)
+    mems = []
     for item in list(top.get("memory", [])):
         byte_write = ('byte_write' in item and
                       item["byte_write"].lower() == "true")
@@ -842,16 +848,18 @@
         swaccess = access.SWAccess('top-level memory',
                                    item.get('swaccess', 'rw'))
 
-        mem = window.Window(name=item['name'],
-                            desc='(generated from top-level)',
-                            unusual=False,
-                            byte_write=byte_write,
-                            validbits=top_block.width,
-                            items=num_regs,
-                            size_in_bytes=size_in_bytes,
-                            offset=int(item["base_addr"], 0),
-                            swaccess=swaccess)
-        top_block.wins.append(mem)
+        mems.append(window.Window(name=item['name'],
+                                  desc='(generated from top-level)',
+                                  unusual=False,
+                                  byte_write=byte_write,
+                                  validbits=top_block.width,
+                                  items=num_regs,
+                                  size_in_bytes=size_in_bytes,
+                                  offset=int(item["base_addr"], 0),
+                                  swaccess=swaccess))
+    mems.sort(key=lambda w: w.offset)
+    for mem in mems:
+        top_block.reg_block.add_window(mem)
 
     # get sub-block base addresses, instance names from top cfg
     for block in top_block.blocks:
@@ -861,7 +869,6 @@
 
     # sort by the base_addr of 1st instance of the block
     top_block.blocks.sort(key=lambda block: next(iter(block.base_addr))[1])
-    top_block.wins.sort(key=lambda win: win.offset)
 
     # generate the top ral model with template
     gen_dv.gen_ral(top_block, dv_base_prefix, str(out_path))