[reggen] Define a class wrapping the top-level IP block

The bulk of this patch is in ip_block.py, which defines the IpBlock class.
This object replaces the top-level dictionary that we were parsing.
Client code then replaces something like this:

    obj = hjson.load(hjson_file.open('r'),
                     use_decimal=True,
                     object_pairs_hook=OrderedDict)
    if validate.validate(obj, params=[]) != 0:
        log.info("Parsing %s configuration failed." % hjson_file)
        sys.exit(1)

with

    obj = IpBlock.from_path(str(hjson_file), [])

where obj is now an IpBlock object instead of a dict.

Other than some pesky rewrites in the various gen_FOO scripts and
template files, the other big change on the reggen side was to replace
the hierarchical "Block" class that was defined in data.py. Now, we
have a Top class (created by topgen code) and a Top can contain
multiple blocks. We've also now got some validation logic to make sure
that the sub-blocks and memories don't overlap: I'm not sure that was
there before.

As well as changing how we load files (as described above), topgen
also needed a bit of work. We now have to convert various objects to
dicts in the merge stage. (Before, we cloned the dictionaries and
added some keys; now we construct the new dictionary explicitly).

The idea is that in time we'll start to generate objects instead of
dicts in topgen as well. As a bonus, we should be able to get rid of
some of the spurious "dump & load" logic found there.

Signed-off-by: Rupert Swarbrick <rswarbrick@lowrisc.org>
diff --git a/util/reggen/alert.py b/util/reggen/alert.py
index 8f8a3cd..a23ff49 100644
--- a/util/reggen/alert.py
+++ b/util/reggen/alert.py
@@ -10,9 +10,10 @@
 
 
 class Alert(Signal):
-    def __init__(self, name: str, desc: str, bit: int):
+    def __init__(self, name: str, desc: str, bit: int, fatal: bool):
         super().__init__(name, desc, Bits(bit, bit))
         self.bit = bit
+        self.fatal = fatal
 
     @staticmethod
     def from_raw(what: str,
@@ -22,7 +23,20 @@
 
         name = check_name(rd['name'], 'name field of ' + what)
         desc = check_str(rd['desc'], 'desc field of ' + what)
-        return Alert(name, desc, lsb)
+
+        # Make sense of the alert name, which should be prefixed with recov_ or
+        # fatal_.
+        pfx = name.split('_')[0]
+        if pfx == 'recov':
+            fatal = False
+        elif pfx == 'fatal':
+            fatal = True
+        else:
+            raise ValueError('Invalid name field of {}: alert names must be '
+                             'prefixed with "recov_" or "fatal_". Saw {!r}.'
+                             .format(what, name))
+
+        return Alert(name, desc, lsb, fatal)
 
     @staticmethod
     def from_raw_list(what: str, raw: object) -> List['Alert']:
diff --git a/util/reggen/block.py b/util/reggen/block.py
new file mode 100644
index 0000000..7e2afeb
--- /dev/null
+++ b/util/reggen/block.py
@@ -0,0 +1,25 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+'''A subclass that can represent either a single block or a whole chip'''
+
+from typing import Dict
+
+from .reg_block import RegBlock
+
+
+class Block:
+    def __init__(self, name: str, regwidth: int, regs: RegBlock):
+        assert regwidth > 0
+
+        self.name = name
+        self.regwidth = regwidth
+        self.regs = regs
+
+    def _asdict(self) -> Dict[str, object]:
+        return {
+            'name': self.name,
+            'regwidth': self.regwidth,
+            'regs': self.regs.as_dicts(),
+        }
diff --git a/util/reggen/data.py b/util/reggen/data.py
deleted file mode 100644
index 634827d..0000000
--- a/util/reggen/data.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Copyright lowRISC contributors.
-# Licensed under the Apache License, Version 2.0, see LICENSE for details.
-# SPDX-License-Identifier: Apache-2.0
-
-from collections import OrderedDict
-import re
-
-
-# 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)
-    assert match
-    assert match.start() > 0
-    return name[0:match.start()]
-
-
-class Block():
-    def __init__(self):
-        self.width = 32
-        self.addr_width = 12
-        # Key is instance name
-        self.base_addr = OrderedDict()
-        self.name = ""
-        self.hier_path = ""
-        self.reg_block = None
-        self.blocks = []
-        self.params = []
-        self.tags = []
diff --git a/util/reggen/fpv_csr.sv.tpl b/util/reggen/fpv_csr.sv.tpl
index 313440a..20bf4f8 100644
--- a/util/reggen/fpv_csr.sv.tpl
+++ b/util/reggen/fpv_csr.sv.tpl
@@ -10,19 +10,18 @@
   from reggen.register import Register
 
   from topgen import lib
+
+  lblock = block.name.lower()
 %>\
 <%def name="construct_classes(block)">\
-% for b in block.blocks:
-${construct_classes(b)}
-% endfor
 
 `include "prim_assert.sv"
 `ifdef UVM
   import uvm_pkg::*;
 `endif
 
-// Block: ${block.name}
-module ${block.name}_csr_assert_fpv import tlul_pkg::*; import ${block.name}_reg_pkg::*;
+// Block: ${lblock}
+module ${lblock}_csr_assert_fpv import tlul_pkg::*; import ${lblock}_reg_pkg::*;
     import top_pkg::*;(
   input clk_i,
   input rst_ni,
@@ -32,8 +31,8 @@
   input tl_d2h_t d2h,
 
   // reg and hw ports
-  input ${block.name}_reg2hw_t reg2hw,
-  input ${block.name}_hw2reg_t hw2reg
+  input ${lblock}_reg2hw_t reg2hw,
+  input ${lblock}_hw2reg_t hw2reg
 );
 
 `ifndef VERILATOR
@@ -57,9 +56,9 @@
   assign a_mask_bit[31:24] = h2d.a_mask[3] ? '1 : '0;
 
 <%
-  addr_msb   = block.addr_width - 1
-  reg_width  = block.addr_width
-  block_size = ((block.reg_block.flat_regs[-1].offset) >> 2) + 1
+  addr_width = block.regs.get_addr_width()
+  addr_msb  = addr_width - 1
+  block_size = ((block.regs.flat_regs[-1].offset) >> 2) + 1
 %>\
   // store internal expected values for HW ReadOnly registers
   logic [TL_DW-1:0] exp_vals[${block_size}];
@@ -70,7 +69,7 @@
   assign normalized_addr = {h2d.a_address[${addr_msb}:2], 2'b0};
 
 <% hro_regs_list = list(); %>\
-% for r in block.reg_block.flat_regs:
+% for r in block.regs.flat_regs:
   % if not r.hwaccess.allows_write():
 <% hro_regs_list.append(r) %>\
   % endif
@@ -125,7 +124,7 @@
     % if reg_mask != 0:
 <%  reg_mask_hex = format(reg_mask, 'x') %>\
     `ASSERT(${r_name}_rd_A, d2h.d_valid && pend_trans[d2h.d_source].rd_pending &&
-           pend_trans[d2h.d_source].addr == (${reg_width}'h${reg_addr_hex} >> 2) |->
+           pend_trans[d2h.d_source].addr == (${addr_width}'h${reg_addr_hex} >> 2) |->
            d2h.d_error ||
            (d2h.d_data & 'h${reg_mask_hex}) == (exp_vals[${reg_addr >> 2}] & 'h${reg_mask_hex}))
 
diff --git a/util/reggen/gen_cfg_html.py b/util/reggen/gen_cfg_html.py
index a69dfc6..bd47e66 100644
--- a/util/reggen/gen_cfg_html.py
+++ b/util/reggen/gen_cfg_html.py
@@ -13,16 +13,17 @@
 
 
 def name_width(x):
-    if 'width' not in x or x['width'] == '1':
-        return x['name']
-    return x['name'] + '[' + str(int(x['width'], 0) - 1) + ':0]'
+    if x.bits.width() == 1:
+        return x.name
+
+    return '{}[{}:0]'.format(x.name, x.bits.msb)
 
 
 # Must have called cfg_validate, so should have no errors
 
 
 def gen_cfg_html(cfgs, outfile):
-    rnames = cfgs['genrnames']
+    rnames = list(cfgs.regs.name_to_offset.keys())
 
     genout(outfile, "<p>Referring to the \n")
     genout(
@@ -32,31 +33,31 @@
     genout(outfile,
            "Comportable guideline for peripheral device functionality</a>,\n")
     genout(outfile,
-           "the module <b><code>" + cfgs['name'] + "</code></b> has \n")
+           "the module <b><code>" + cfgs.name + "</code></b> has \n")
     genout(outfile, "the following hardware interfaces defined.</p>\n")
     # clocks
     genout(
-        outfile, "<p><i>Primary Clock:</i> <b><code>" + cfgs['clock_primary'] +
+        outfile, "<p><i>Primary Clock:</i> <b><code>" + cfgs.clock_signals[0] +
         "</code></b></p>\n")
-    if 'other_clock_list' in cfgs:
+    if len(cfgs.clock_signals) > 1:
         genout(outfile, "<p><i>Other Clocks:</i></p>\n")
     else:
         genout(outfile, "<p><i>Other Clocks: none</i></p>\n")
     # bus interfaces
     genout(
         outfile, "<p><i>Bus Device Interface:</i> <b><code>" +
-        cfgs['bus_device'] + "</code></b></p>\n")
-    if 'bus_host' in cfgs:
+        (cfgs.bus_device or '') + "</code></b></p>\n")
+    if cfgs.bus_host is not None:
         genout(
             outfile, "<p><i>Bus Host Interface:</i> <b><code>" +
-            cfgs['bus_host'] + "</code></b></p>\n")
+            cfgs.bus_host + "</code></b></p>\n")
     else:
         genout(outfile, "<p><i>Bus Host Interface: none</i></p>\n")
 
     # IO
-    ios = ([('input', x) for x in cfgs.get('available_input_list', [])] +
-           [('output', x) for x in cfgs.get('available_output_list', [])] +
-           [('inout', x) for x in cfgs.get('available_inout_list', [])])
+    ios = ([('input', x) for x in cfgs.xputs[1]] +
+           [('output', x) for x in cfgs.xputs[2]] +
+           [('inout', x) for x in cfgs.xputs[0]])
     if ios:
         genout(outfile, "<p><i>Peripheral Pins for Chip IO:</i></p>\n")
         genout(
@@ -68,37 +69,35 @@
                    '<tr><td>{}</td><td>{}</td>{}</tr>'
                    .format(name_width(x),
                            direction,
-                           render_td(x['desc'], rnames, None)))
+                           render_td(x.desc, rnames, None)))
         genout(outfile, "</table>\n")
     else:
         genout(outfile, "<p><i>Peripheral Pins for Chip IO: none</i></p>\n")
 
-    interrupts = cfgs.get('interrupt_list', [])
-    if not interrupts:
+    if not cfgs.interrupts:
         genout(outfile, "<p><i>Interrupts: none</i></p>\n")
     else:
         genout(outfile, "<p><i>Interrupts:</i></p>\n")
         genout(
             outfile, "<table class=\"cfgtable\"><tr><th>Interrupt Name</th>" +
             "<th>Description</th></tr>\n")
-        for x in interrupts:
+        for x in cfgs.interrupts:
             genout(outfile,
                    '<tr><td>{}</td>{}</tr>'
                    .format(name_width(x),
-                           render_td(x['desc'], rnames, None)))
+                           render_td(x.desc, rnames, None)))
         genout(outfile, "</table>\n")
 
-    alerts = cfgs.get('alert_list', [])
-    if not alerts:
+    if not cfgs.alerts:
         genout(outfile, "<p><i>Security Alerts: none</i></p>\n")
     else:
         genout(outfile, "<p><i>Security Alerts:</i></p>\n")
         genout(
             outfile, "<table class=\"cfgtable\"><tr><th>Alert Name</th>" +
             "<th>Description</th></tr>\n")
-        for x in alerts:
+        for x in cfgs.alerts:
             genout(outfile,
                    '<tr><td>{}</td>{}</tr>'
-                   .format(x['name'],
-                           render_td(x['desc'], rnames, None)))
+                   .format(x.name,
+                           render_td(x.desc, rnames, None)))
         genout(outfile, "</table>\n")
diff --git a/util/reggen/gen_cheader.py b/util/reggen/gen_cheader.py
index 1180c79..cb37a3c 100644
--- a/util/reggen/gen_cheader.py
+++ b/util/reggen/gen_cheader.py
@@ -12,6 +12,7 @@
 import warnings
 
 
+from .ip_block import IpBlock
 from .register import Register
 from .multi_register import MultiRegister
 from .window import Window
@@ -190,10 +191,7 @@
 
 def gen_cdefines_module_params(outstr, module_data, module_name,
                                register_width, existing_defines):
-    module_params = set()
-
-    if 'param_list' in module_data:
-        module_params = module_data['param_list']
+    module_params = module_data.params
 
     for param in module_params.get_localparams():
         gen_cdefines_module_param(outstr, param, module_name, existing_defines)
@@ -272,77 +270,55 @@
                     .format(dname=defname), existing_defines))
 
 
-def gen_cdefines_interrupts(outstr, regs, component, regwidth,
+def gen_cdefines_interrupts(outstr, block, component, regwidth,
                             existing_defines):
-    # no_auto_intr_regs controls whether interrupt registers are automatically
-    # generated from the interrupt_list. This key could be 'true' or 'false',
-    # but might also be True or False (the python booleans).
-    no_auto_i = False
-    if 'no_auto_intr_regs' in regs:
-        no_auto_intr_regs_val = regs['no_auto_intr_regs']
-        if isinstance(no_auto_intr_regs_val, bool):
-            no_auto_i = no_auto_intr_regs_val
-        elif no_auto_intr_regs_val.lower() in ["true", "false"]:
-            no_auto_i = no_auto_intr_regs_val == "true"
-        else:
-            pass
-
     # If no_auto_intr_regs is true, then we do not generate common defines,
     # because the bit offsets for a particular interrupt may differ between
     # the interrupt enable/state/test registers.
-    if no_auto_i:
+    if block.no_auto_intr:
         return
 
-    interrupts = regs.get('interrupt_list', [])
     genout(outstr, format_comment(first_line("Common Interrupt Offsets")))
-    for intr in interrupts:
+    for intr in block.interrupts:
         gen_cdefines_interrupt_field(outstr, intr, component, regwidth,
                                      existing_defines)
     genout(outstr, '\n')
 
 
-# Must have called validate, so should have no errors
-def gen_cdefines(regs, outfile, src_lic, src_copy):
-    component = regs['name']
-    registers = regs['registers']
-    rnames = regs['genrnames']
+def gen_cdefines(block: IpBlock, outfile, src_lic, src_copy):
+    rnames = list(block.regs.name_to_offset.keys())
     outstr = io.StringIO()
 
     # This tracks the defines that have been generated so far, so we
     # can error if we attempt to duplicate a definition
     existing_defines = set()
 
-    if 'regwidth' in regs:
-        regwidth = int(regs['regwidth'], 0)
-    else:
-        regwidth = 32
-
-    gen_cdefines_module_params(outstr, regs, component, regwidth,
+    gen_cdefines_module_params(outstr, block, block.name, block.regwidth,
                                existing_defines)
 
-    gen_cdefines_interrupts(outstr, regs, component, regwidth,
+    gen_cdefines_interrupts(outstr, block, block.name, block.regwidth,
                             existing_defines)
 
-    for x in registers.entries:
+    for x in block.regs.entries:
         if isinstance(x, Register):
-            gen_cdefine_register(outstr, x, component, regwidth, rnames,
+            gen_cdefine_register(outstr, x, block.name, block.regwidth, rnames,
                                  existing_defines)
             continue
 
         if isinstance(x, MultiRegister):
-            gen_cdefine_multireg(outstr, x, component, regwidth, rnames,
+            gen_cdefine_multireg(outstr, x, block.name, block.regwidth, rnames,
                                  existing_defines)
             continue
 
         if isinstance(x, Window):
-            gen_cdefine_window(outstr, x, component, regwidth,
+            gen_cdefine_window(outstr, x, block.name, block.regwidth,
                                rnames, existing_defines)
             continue
 
     generated = outstr.getvalue()
     outstr.close()
 
-    genout(outfile, '// Generated register defines for ' + component + '\n\n')
+    genout(outfile, '// Generated register defines for ' + block.name + '\n\n')
     if src_copy != '':
         genout(outfile, '// Copyright information found in source file:\n')
         genout(outfile, '// ' + src_copy + '\n\n')
@@ -353,8 +329,8 @@
         genout(outfile, '\n')
 
     # Header Include Guard
-    genout(outfile, '#ifndef _' + as_define(component) + '_REG_DEFS_\n')
-    genout(outfile, '#define _' + as_define(component) + '_REG_DEFS_\n\n')
+    genout(outfile, '#ifndef _' + as_define(block.name) + '_REG_DEFS_\n')
+    genout(outfile, '#define _' + as_define(block.name) + '_REG_DEFS_\n\n')
 
     # Header Extern Guard (so header can be used from C and C++)
     genout(outfile, '#ifdef __cplusplus\n')
@@ -369,9 +345,9 @@
     genout(outfile, '#endif\n')
 
     # Header Include Guard
-    genout(outfile, '#endif  // _' + as_define(component) + '_REG_DEFS_\n')
+    genout(outfile, '#endif  // _' + as_define(block.name) + '_REG_DEFS_\n')
 
-    genout(outfile, '// End generated register defines for ' + component)
+    genout(outfile, '// End generated register defines for ' + block.name)
 
     return 0
 
diff --git a/util/reggen/gen_dv.py b/util/reggen/gen_dv.py
index 6053d0b..c6e96e7 100644
--- a/util/reggen/gen_dv.py
+++ b/util/reggen/gen_dv.py
@@ -4,29 +4,30 @@
 '''Generate DV code for an IP block'''
 
 import logging as log
-import sys
 
 from mako import exceptions
 from mako.template import Template
 from pkg_resources import resource_filename
 
 from .access import HwAccess, SwRdAccess, SwWrAccess
-from .gen_rtl import json_to_reg
+from .block import Block
+from .top import Top
+from .ip_block import IpBlock
 
 
 def bcname(b):
     '''Get the name of the dv_base_reg_block subclass for this block'''
-    return b.name + "_reg_block"
+    return b.name.lower() + "_reg_block"
 
 
 def rcname(b, r):
     '''Get the name of the dv_base_reg subclass for this register'''
-    return b.name + "_reg_" + r.name.lower()
+    return b.name.lower() + "_reg_" + r.name.lower()
 
 
 def mcname(b, m):
     '''Get the name of the dv_base_mem subclass for this memory'''
-    return b.name + "_mem_" + m.name.lower()
+    return b.name.lower() + "_mem_" + m.name.lower()
 
 
 def miname(m):
@@ -34,19 +35,19 @@
     return m.name.lower()
 
 
-def sv_base_addr(b, inst):
+def sv_base_addr(top: Top, inst_name: str):
     '''Get the base address of a block in SV syntax'''
-    return "{}'h{:x}".format(b.width, b.base_addr[inst])
+    return "{}'h{:x}".format(top.regwidth,
+                             top.by_inst_name[inst_name][0])
 
 
-def gen_dv(obj, dv_base_prefix, outdir):
-    '''Generate DV files using a raw dict object parsed from hjson'''
-    gen_ral(json_to_reg(obj), dv_base_prefix, outdir)
+def gen_dv(block: IpBlock, dv_base_prefix, outdir):
+    '''Generate DV files for an IpBlock'''
+    gen_ral(block, dv_base_prefix, outdir)
 
 
-def gen_ral(block, dv_base_prefix, outdir):
-    '''Generate DV RAL model from a gen_rtl.Block specification'''
-
+def gen_ral(block: Block, dv_base_prefix, outdir):
+    '''Generate DV RAL model for a Block'''
     # Read template
     tpl_filename = resource_filename('reggen', 'uvm_reg.sv.tpl')
     uvm_reg_tpl = Template(filename=tpl_filename)
@@ -63,7 +64,7 @@
         return 1
 
     # Dump to output file
-    dest_path = '{}/{}_ral_pkg.sv'.format(outdir, block.name)
+    dest_path = '{}/{}_ral_pkg.sv'.format(outdir, block.name.lower())
     with open(dest_path, 'w') as fout:
         fout.write(to_write)
 
diff --git a/util/reggen/gen_fpv.py b/util/reggen/gen_fpv.py
index d29c562..bfacce6 100644
--- a/util/reggen/gen_fpv.py
+++ b/util/reggen/gen_fpv.py
@@ -13,7 +13,7 @@
 from pkg_resources import resource_filename
 
 from .access import HwAccess, SwRdAccess, SwWrAccess
-from .gen_rtl import json_to_reg
+from .ip_block import IpBlock
 
 
 # function get write property name
@@ -26,19 +26,13 @@
     return r.name + "_rd_p"
 
 
-def gen_fpv(obj, outdir):
-    # obj: OrderedDict
-    block = json_to_reg(obj)
-    gen_assertion(block, outdir)
-
-
-def gen_assertion(block, outdir):
+def gen_fpv(block: IpBlock, outdir):
     # Read Register templates
     fpv_csr_tpl = Template(
         filename=resource_filename('reggen', 'fpv_csr.sv.tpl'))
 
     # Generate pkg.sv with block name
-    with open(outdir + "/" + block.name + "_csr_assert_fpv.sv", 'w') as fout:
+    with open(outdir + "/" + block.name.lower() + "_csr_assert_fpv.sv", 'w') as fout:
         try:
             fout.write(
                 fpv_csr_tpl.render(block=block,
diff --git a/util/reggen/gen_html.py b/util/reggen/gen_html.py
index 6e01600..5f7d35d 100644
--- a/util/reggen/gen_html.py
+++ b/util/reggen/gen_html.py
@@ -286,28 +286,21 @@
 # Must have called validate, so should have no errors
 
 
-def gen_html(regs, outfile, toclist=None, toclevel=3):
-    component = regs['name']
-    registers = regs['registers']
-    rnames = regs['genrnames']
+def gen_html(block, outfile, toclist=None, toclevel=3):
+    rnames = list(block.regs.name_to_offset.keys())
 
-    if 'regwidth' in regs:
-        regwidth = int(regs['regwidth'], 0)
-    else:
-        regwidth = 32
-
-    for x in registers.entries:
+    for x in block.regs.entries:
         if isinstance(x, Register):
-            gen_html_register(outfile, x, component, regwidth, rnames, toclist,
-                              toclevel)
+            gen_html_register(outfile, x, block.name, block.regwidth, rnames,
+                              toclist, toclevel)
             continue
         if isinstance(x, MultiRegister):
             for reg in x.regs:
-                gen_html_register(outfile, reg, component, regwidth, rnames,
-                                  toclist, toclevel)
+                gen_html_register(outfile, reg, block.name, block.regwidth,
+                                  rnames, toclist, toclevel)
             continue
         if isinstance(x, Window):
-            gen_html_window(outfile, x, component, regwidth, rnames,
+            gen_html_window(outfile, x, block.name, block.regwidth, rnames,
                             toclist, toclevel)
             continue
 
diff --git a/util/reggen/gen_json.py b/util/reggen/gen_json.py
index 2e65f70..c593cc1 100644
--- a/util/reggen/gen_json.py
+++ b/util/reggen/gen_json.py
@@ -8,14 +8,6 @@
 
 
 def gen_json(obj, outfile, format):
-    # Temporary hack to deal with the fact that the 'registers' and
-    # 'param_list' fields are lists rather than dictionaries. 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()
-    obj['param_list'] = obj['param_list'].as_dicts()
-
     if format == 'json':
         hjson.dumpJSON(obj,
                        outfile,
diff --git a/util/reggen/gen_rtl.py b/util/reggen/gen_rtl.py
index 5bc04dd..a0df901 100644
--- a/util/reggen/gen_rtl.py
+++ b/util/reggen/gen_rtl.py
@@ -11,64 +11,14 @@
 from pkg_resources import resource_filename
 
 from .access import HwAccess, SwRdAccess, SwWrAccess
-from .data import Block
+from .ip_block import IpBlock
 
 
 def escape_name(name):
     return name.lower().replace(' ', '_')
 
 
-def check_field_bool(obj, field, default):
-    if field in obj:
-        return True if obj[field] == "true" else False
-    else:
-        return default
-
-
-def json_to_reg(obj):
-    """Converts JSON OrderedDict into structure having useful information for
-    Template to use.
-
-    Main purpose of this function is:
-        - Add Offset value based on auto calculation
-        - Prepare Systemverilog data structure to generate _pkg file
-    """
-    block = Block()
-
-    # Name
-    block.name = escape_name(obj["name"])
-    log.info("Processing module: %s", block.name)
-
-    block.width = int(obj["regwidth"], 0)
-
-    if block.width != 32 and block.width != 64:
-        log.error(
-            "Current reggen tool doesn't support field width that is not 32 nor 64"
-        )
-
-    log.info("Data Width is set to %d bits", block.width)
-
-    block.params = obj["param_list"] if "param_list" in obj else []
-
-    block.hier_path = obj["hier_path"] if "hier_path" in obj else ""
-
-    block.reg_block = obj['registers']
-
-    # Last offset and calculate space
-    #  Later on, it could use block.regs[-1].genoffset
-    if "space" in obj:
-        block.addr_width = int(obj["space"], 0).bit_length()
-    else:
-        block.addr_width = (obj["gensize"] - 1).bit_length()
-
-    return block
-
-
-def gen_rtl(obj, outdir):
-    # obj: OrderedDict
-
-    block = json_to_reg(obj)
-
+def gen_rtl(block: IpBlock, outdir: str):
     # Read Register templates
     reg_top_tpl = Template(
         filename=resource_filename('reggen', 'reg_top.sv.tpl'))
@@ -76,7 +26,7 @@
         filename=resource_filename('reggen', 'reg_pkg.sv.tpl'))
 
     # Generate pkg.sv with block name
-    with open(outdir + "/" + block.name + "_reg_pkg.sv", 'w',
+    with open(outdir + "/" + block.name.lower() + "_reg_pkg.sv", 'w',
               encoding='UTF-8') as fout:
         try:
             fout.write(
@@ -89,7 +39,7 @@
             return 1
 
     # Generate top.sv
-    with open(outdir + "/" + block.name + "_reg_top.sv", 'w',
+    with open(outdir + "/" + block.name.lower() + "_reg_top.sv", 'w',
               encoding='UTF-8') as fout:
         try:
             fout.write(
diff --git a/util/reggen/gen_selfdoc.py b/util/reggen/gen_selfdoc.py
index 8e46ac9..5f38404 100644
--- a/util/reggen/gen_selfdoc.py
+++ b/util/reggen/gen_selfdoc.py
@@ -7,7 +7,7 @@
 """
 from .access import SWACCESS_PERMITTED, HWACCESS_PERMITTED
 from reggen import (validate,
-                    enum_entry, field,
+                    ip_block, enum_entry, field,
                     register, multi_register, window)
 
 
@@ -248,12 +248,10 @@
         outfile, "\n\nThe top level of the JSON is a group containing "
         "the following keys:\n")
     doc_tbl_head(outfile, 1)
-    for x in validate.top_required:
-        doc_tbl_line(outfile, x, 'r', validate.top_required[x])
-    for x in validate.top_optional:
-        doc_tbl_line(outfile, x, 'o', validate.top_optional[x])
-    for x in validate.top_added:
-        doc_tbl_line(outfile, x, 'a', validate.top_added[x])
+    for k, v in ip_block.REQUIRED_FIELDS.items():
+        doc_tbl_line(outfile, k, 'r', v)
+    for k, v in ip_block.OPTIONAL_FIELDS.items():
+        doc_tbl_line(outfile, k, 'o', v)
     genout(outfile, top_example)
 
     genout(
diff --git a/util/reggen/ip_block.py b/util/reggen/ip_block.py
new file mode 100644
index 0000000..5d7fe3d
--- /dev/null
+++ b/util/reggen/ip_block.py
@@ -0,0 +1,319 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+'''Code representing an IP block for reggen'''
+
+from typing import Dict, List, Optional, Sequence, Tuple
+
+import hjson  # type: ignore
+
+from .alert import Alert
+from .block import Block
+from .inter_signal import InterSignal
+from .lib import (check_keys, check_name, check_int, check_bool,
+                  check_list, check_optional_str, check_name_list)
+from .params import Params, LocalParam
+from .reg_block import RegBlock
+from .signal import Signal
+
+
+REQUIRED_FIELDS = {
+    'name': ['s', "name of the component"],
+    'clock_primary': ['s', "name of the primary clock"],
+    'bus_device': ['s', "name of the bus interface for the device"],
+    'registers': [
+        'l',
+        "list of register definition groups and "
+        "offset control groups"
+    ]
+}
+
+OPTIONAL_FIELDS = {
+    'alert_list': ['lnw', "list of peripheral alerts"],
+    'available_inout_list': ['lnw', "list of available peripheral inouts"],
+    'available_input_list': ['lnw', "list of available peripheral inputs"],
+    'available_output_list': ['lnw', "list of available peripheral outputs"],
+    'bus_host': ['s', "name of the bus interface as host"],
+    'hier_path': [
+        None,
+        'additional hierarchy path before the reg block instance'
+    ],
+    'interrupt_list': ['lnw', "list of peripheral interrupts"],
+    'inter_signal_list': ['l', "list of inter-module signals"],
+    'no_auto_alert_regs': [
+        's', "Set to true to suppress automatic "
+        "generation of alert test registers. "
+        "Defaults to true if no alert_list is present. "
+        "Otherwise this defaults to false. "
+    ],
+    'no_auto_intr_regs': [
+        's', "Set to true to suppress automatic "
+        "generation of interrupt registers. "
+        "Defaults to true if no interrupt_list is present. "
+        "Otherwise this defaults to false. "
+    ],
+    'other_clock_list': ['l', "list of other chip clocks needed"],
+    'other_reset_list': ['l', "list of other resets"],
+    'param_list': ['lp', "list of parameters of the IP"],
+    'regwidth': ['d', "width of registers in bits (default 32)"],
+    'reset_primary': ['s', "primary reset used by the module"],
+    'reset_request_list': ['l', 'list of signals requesting reset'],
+    'scan': ['pb', 'Indicates the module have `scanmode_i`'],
+    'scan_reset': ['pb', 'Indicates the module have `test_rst_ni`'],
+    'SPDX-License-Identifier': [
+        's', "License ientifier (if using pure json) "
+        "Only use this if unable to put this "
+        "information in a comment at the top of the "
+        "file."
+    ],
+    'wakeup_list': ['lnw', "list of peripheral wakeups"]
+}
+
+
+class IpBlock(Block):
+    def __init__(self,
+                 name: str,
+                 regwidth: int,
+                 params: Params,
+                 regs: RegBlock,
+                 interrupts: Sequence[Signal],
+                 no_auto_intr: bool,
+                 alerts: List[Alert],
+                 no_auto_alert: bool,
+                 scan: bool,
+                 inter_signals: List[InterSignal],
+                 bus_device: Optional[str],
+                 bus_host: Optional[str],
+                 hier_path: Optional[str],
+                 clock_signals: List[str],
+                 reset_signals: List[str],
+                 xputs: Tuple[Sequence[Signal],
+                              Sequence[Signal],
+                              Sequence[Signal]],
+                 wakeups: Sequence[Signal],
+                 reset_requests: Sequence[Signal],
+                 scan_reset: bool):
+        assert clock_signals
+        assert reset_signals
+
+        super().__init__(name, regwidth, regs)
+
+        self.params = params
+        self.interrupts = interrupts
+        self.no_auto_intr = no_auto_intr
+        self.alerts = alerts
+        self.no_auto_alert = no_auto_alert
+        self.scan = scan
+        self.inter_signals = inter_signals
+        self.bus_device = bus_device
+        self.bus_host = bus_host
+        self.hier_path = hier_path
+        self.clock_signals = clock_signals
+        self.reset_signals = reset_signals
+        self.xputs = xputs
+        self.wakeups = wakeups
+        self.reset_requests = reset_requests
+        self.scan_reset = scan_reset
+
+    @staticmethod
+    def from_raw(param_defaults: List[Tuple[str, str]],
+                 raw: object,
+                 where: str) -> 'IpBlock':
+
+        rd = check_keys(raw, 'block at ' + where,
+                        list(REQUIRED_FIELDS.keys()),
+                        list(OPTIONAL_FIELDS.keys()))
+
+        name = check_name(rd['name'], 'name of block at ' + where)
+
+        what = '{} block at {}'.format(name, where)
+
+        r_regwidth = rd.get('regwidth')
+        if r_regwidth is None:
+            regwidth = 32
+        else:
+            regwidth = check_int(r_regwidth, 'regwidth field of ' + what)
+            if regwidth <= 0:
+                raise ValueError('Invalid regwidth field for {}: '
+                                 '{} is not positive.'
+                                 .format(what, regwidth))
+
+        params = Params.from_raw('parameter list for ' + what,
+                                 rd.get('param_list', []))
+        try:
+            params.apply_defaults(param_defaults)
+        except (ValueError, KeyError) as err:
+            raise ValueError('Failed to apply defaults to params: {}'
+                             .format(err)) from None
+
+        regs = RegBlock(regwidth, params)
+
+        interrupts = Signal.from_raw_list('interrupt_list for block {}'
+                                          .format(name),
+                                          rd.get('interrupt_list', []))
+        alerts = Alert.from_raw_list('alert_list for block {}'
+                                     .format(name),
+                                     rd.get('alert_list', []))
+
+        no_auto_intr = check_bool(rd.get('no_auto_intr_regs', not interrupts),
+                                  'no_auto_intr_regs field of ' + what)
+
+        no_auto_alert = check_bool(rd.get('no_auto_alert_regs', not alerts),
+                                   'no_auto_alert_regs field of ' + what)
+
+        if interrupts and not no_auto_intr:
+            if interrupts[-1].bits.msb >= regwidth:
+                raise ValueError("Interrupt list for {} is too wide: "
+                                 "msb is {}, which doesn't fit with a "
+                                 "regwidth of {}."
+                                 .format(what,
+                                         interrupts[-1].bits.msb, regwidth))
+            regs.make_intr_regs(interrupts)
+
+        if alerts:
+            if not no_auto_alert:
+                if len(alerts) > regwidth:
+                    raise ValueError("Interrupt list for {} is too wide: "
+                                     "{} alerts don't fit with a regwidth of {}."
+                                     .format(what, len(alerts), regwidth))
+                regs.make_alert_regs(alerts)
+
+            # Generate a NumAlerts parameter
+            existing_param = params.get('NumAlerts')
+            if existing_param is not None:
+                if ((not isinstance(existing_param, LocalParam) or
+                     existing_param.param_type != 'int' or
+                     existing_param.value != str(len(alerts)))):
+                    raise ValueError('Conflicting definition of NumAlerts '
+                                     'parameter.')
+            else:
+                params.add(LocalParam(name='NumAlerts',
+                                      desc='Number of alerts',
+                                      param_type='int',
+                                      value=str(len(alerts))))
+
+        scan = check_bool(rd.get('scan', False), 'scan field of ' + what)
+
+        regs.add_raw_registers(rd['registers'])
+        regs.validate()
+
+        r_inter_signals = check_list(rd.get('inter_signal_list', []),
+                                     'inter_signal_list field')
+        inter_signals = [
+            InterSignal.from_raw('entry {} of the inter_signal_list field'
+                                 .format(idx + 1),
+                                 entry)
+            for idx, entry in enumerate(r_inter_signals)
+        ]
+
+        bus_device = check_optional_str(rd.get('bus_device', None),
+                                        'bus_device field of ' + what)
+        bus_host = check_optional_str(rd.get('bus_host', None),
+                                      'bus_host field of ' + what)
+
+        if bus_device == "tlul":
+            # Add to inter_module_signal
+            port_name = "tl" if bus_host in ["none", "", None] else "tl_d"
+            inter_signals.append(InterSignal(port_name, None, 'tl', 'tlul_pkg',
+                                             'req_rsp', 'rsp', 1, None))
+
+        if bus_host == "tlul":
+            inter_signals.append(InterSignal('tl_h', None, 'tl', 'tlul_pkg',
+                                             'req_rsp', 'rsp', 1, None))
+
+        hier_path = check_optional_str(rd.get('hier_path', None),
+                                       'hier_path field of ' + what)
+
+        clock_primary = check_name(rd['clock_primary'],
+                                   'clock_primary field of ' + what)
+        other_clock_list = check_name_list(rd.get('other_clock_list', []),
+                                           'other_clock_list field of ' + what)
+        clock_signals = [clock_primary] + other_clock_list
+
+        reset_primary = check_name(rd.get('reset_primary', 'rst_ni'),
+                                   'reset_primary field of ' + what)
+        other_reset_list = check_name_list(rd.get('other_reset_list', []),
+                                           'other_reset_list field of ' + what)
+        reset_signals = [reset_primary] + other_reset_list
+
+        xputs = (
+            Signal.from_raw_list('available_inout_list for block ' + name,
+                                 rd.get('available_inout_list', [])),
+            Signal.from_raw_list('available_input_list for block ' + name,
+                                 rd.get('available_input_list', [])),
+            Signal.from_raw_list('available_output_list for block ' + name,
+                                 rd.get('available_output_list', []))
+        )
+        wakeups = Signal.from_raw_list('wakeup_list for block ' + name,
+                                       rd.get('wakeup_list', []))
+        rst_reqs = Signal.from_raw_list('reset_request_list for block ' + name,
+                                        rd.get('reset_request_list', []))
+
+        scan_reset = check_bool(rd.get('scan_reset', False),
+                                'scan_reset field of ' + what)
+
+        return IpBlock(name, regwidth, params, regs,
+                       interrupts, no_auto_intr, alerts, no_auto_alert,
+                       scan, inter_signals, bus_device, bus_host,
+                       hier_path, clock_signals, reset_signals,
+                       xputs, wakeups, rst_reqs, scan_reset)
+
+    @staticmethod
+    def from_text(txt: str,
+                  param_defaults: List[Tuple[str, str]],
+                  where: str) -> 'IpBlock':
+        '''Load an IpBlock from an hjson description in txt'''
+        return IpBlock.from_raw(param_defaults,
+                                hjson.loads(txt, use_decimal=True),
+                                where)
+
+    @staticmethod
+    def from_path(path: str,
+                  param_defaults: List[Tuple[str, str]]) -> 'IpBlock':
+        '''Load an IpBlock from an hjson description in a file at path'''
+        with open(path, 'r') as handle:
+            return IpBlock.from_text(handle.read(), param_defaults,
+                                     'file at {!r}'.format(path))
+
+    def _asdict(self) -> Dict[str, object]:
+        ret = super()._asdict()
+        ret['param_list'] = self.params.as_dicts()
+        ret['interrupt_list'] = self.interrupts
+        ret['no_auto_intr_regs'] = self.no_auto_intr
+        ret['alert_list'] = self.alerts
+        ret['no_auto_alert_regs'] = self.no_auto_alert
+        ret['scan'] = self.scan
+        ret['inter_signal_list'] = self.inter_signals
+
+        if self.bus_device is not None:
+            ret['bus_device'] = self.bus_device
+        if self.bus_host is not None:
+            ret['bus_host'] = self.bus_host
+        if self.hier_path is not None:
+            ret['hier_path'] = self.hier_path
+
+        ret['clock_primary'] = self.clock_signals[0]
+        if len(self.clock_signals) > 1:
+            ret['other_clock_list'] = self.clock_signals[1:]
+
+        ret['reset_primary'] = self.reset_signals[0]
+        if len(self.reset_signals) > 1:
+            ret['other_reset_list'] = self.reset_signals[1:]
+
+        inouts, inputs, outputs = self.xputs
+        if inouts:
+            ret['available_inout_list'] = inouts
+        if inputs:
+            ret['available_input_list'] = inputs
+        if outputs:
+            ret['available_output_list'] = outputs
+
+        if self.wakeups:
+            ret['wakeup_list'] = self.wakeups
+        if self.reset_requests:
+            ret['reset_request_list'] = self.reset_requests
+
+        ret['scan_reset'] = self.scan_reset
+
+        return ret
diff --git a/util/reggen/lib.py b/util/reggen/lib.py
index ddfd573..d72ef3d 100644
--- a/util/reggen/lib.py
+++ b/util/reggen/lib.py
@@ -189,6 +189,19 @@
     return cast(List[str], lst)
 
 
+def check_name_list(obj: object, what: str) -> List[str]:
+    '''Check that the given object is a list of valid names
+
+    If not, raise a ValueError; the what argument names the object.
+
+    '''
+    lst = check_list(obj, what)
+    for idx, elt in enumerate(lst):
+        check_name(elt, 'Element {} of {}'.format(idx + 1, what))
+
+    return cast(List[str], lst)
+
+
 def check_int(obj: object, what: str) -> int:
     '''Check that obj is an integer or a string that parses to an integer.
 
@@ -237,3 +250,13 @@
 def check_optional_str(obj: object, what: str) -> Optional[str]:
     '''Check that obj is a string or None'''
     return None if obj is None else check_str(obj, what)
+
+
+def get_basename(name: str) -> str:
+    '''Strip trailing _number (used as multireg suffix) from name'''
+    # TODO: This is a workaround, should solve this as part of parsing a
+    # multi-reg.
+    match = re.search(r'_[0-9]+$', name)
+    assert match
+    assert match.start() > 0
+    return name[0:match.start()]
diff --git a/util/reggen/params.py b/util/reggen/params.py
index 14f52a0..7dbde24 100644
--- a/util/reggen/params.py
+++ b/util/reggen/params.py
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 import re
-from typing import Dict, List, Optional
+from typing import Dict, List, Optional, Tuple
 
 from .lib import check_keys, check_str, check_int, check_bool, check_list
 
@@ -252,24 +252,15 @@
     def get(self, name: str) -> Optional[BaseParam]:
         return self.by_name.get(name)
 
-    def _apply_default(self, name: str, value: str) -> None:
-        param = self.by_name.get(name)
-        if param is None:
-            raise KeyError('Cannot find parameter '
-                           '{} to set default value.'
-                           .format(name))
+    def apply_defaults(self, defaults: List[Tuple[str, str]]) -> None:
+        for idx, (key, value) in enumerate(defaults):
+            param = self.by_name.get(key)
+            if param is None:
+                raise KeyError('Cannot find parameter '
+                               '{} to set default value.'
+                               .format(key))
 
-        param.apply_default(value)
-
-    def apply_defaults(self, defaults: List[str]) -> None:
-        for idx, entry in enumerate(defaults):
-            tokens = entry.split('=')
-            if len(tokens) != 2:
-                raise ValueError('Entry {} in list of parameter defaults to '
-                                 'apply is {!r}, which is not of the form '
-                                 'param=value.'
-                                 .format(idx, entry))
-            self._apply_default(tokens[0], tokens[1])
+            param.apply_default(value)
 
     def _expand_one(self, value: str, when: str) -> int:
         # Check whether value is already an integer: if so, return that.
diff --git a/util/reggen/reg_block.py b/util/reggen/reg_block.py
index 54cf784..a3c3486 100644
--- a/util/reggen/reg_block.py
+++ b/util/reggen/reg_block.py
@@ -5,8 +5,12 @@
 '''Code representing the registers, windows etc. for a block'''
 
 import re
-from typing import Dict, List, Union
+from typing import Callable, Dict, List, Optional, Sequence, Union
 
+from .alert import Alert
+from .access import SWAccess, HWAccess
+from .field import Field
+from .signal import Signal
 from .lib import check_int, check_list, check_str_dict
 from .multi_register import MultiRegister
 from .params import Params
@@ -15,9 +19,9 @@
 
 
 class RegBlock:
-    def __init__(self, addrsep: int, reg_width: int, params: Params):
+    def __init__(self, reg_width: int, params: Params):
 
-        self._addrsep = addrsep
+        self._addrsep = (reg_width + 7) // 8
         self._reg_width = reg_width
         self._params = params
 
@@ -259,3 +263,112 @@
             offset = entry.next_offset(self._addrsep)
 
         return entries
+
+    _FieldFormatter = Callable[[bool, str], str]
+
+    def _add_intr_alert_reg(self,
+                            signals: Sequence[Signal],
+                            reg_name: str,
+                            reg_desc: str,
+                            field_desc_fmt: Optional[Union[str, _FieldFormatter]],
+                            swaccess: str,
+                            hwaccess: str,
+                            is_testreg: bool,
+                            reg_tags: List[str]) -> None:
+        swaccess_obj = SWAccess('RegBlock._make_intr_alert_reg()', swaccess)
+        hwaccess_obj = HWAccess('RegBlock._make_intr_alert_reg()', hwaccess)
+
+        fields = []
+        for signal in signals:
+            if field_desc_fmt is None:
+                field_desc = signal.desc
+            elif isinstance(field_desc_fmt, str):
+                field_desc = field_desc_fmt
+            else:
+                width = signal.bits.width()
+                field_desc = field_desc_fmt(width > 1, signal.name)
+
+            fields.append(Field(signal.name,
+                                field_desc or signal.desc,
+                                tags=[],
+                                swaccess=swaccess_obj,
+                                hwaccess=hwaccess_obj,
+                                hwqe=is_testreg,
+                                hwre=False,
+                                bits=signal.bits,
+                                resval=0,
+                                enum=None))
+
+        reg = Register(self.offset,
+                       reg_name,
+                       reg_desc,
+                       swaccess_obj,
+                       hwaccess_obj,
+                       hwext=is_testreg,
+                       hwqe=is_testreg,
+                       hwre=False,
+                       regwen=None,
+                       tags=reg_tags,
+                       resval=None,
+                       shadowed=False,
+                       fields=fields,
+                       update_err_alert=None,
+                       storage_err_alert=None)
+        self.add_register(reg)
+
+    def make_intr_regs(self, interrupts: Sequence[Signal]) -> None:
+        assert interrupts
+        assert interrupts[-1].bits.msb < self._reg_width
+
+        self._add_intr_alert_reg(interrupts,
+                                 'INTR_STATE',
+                                 'Interrupt State Register',
+                                 None,
+                                 'rw1c',
+                                 'hrw',
+                                 False,
+                                 # intr_state csr is affected by writes to
+                                 # other csrs - skip write-check
+                                 ["excl:CsrNonInitTests:CsrExclWriteCheck"])
+        self._add_intr_alert_reg(interrupts,
+                                 'INTR_ENABLE',
+                                 'Interrupt Enable Register',
+                                 lambda w, n: ('Enable interrupt when '
+                                               '{}!!INTR_STATE.{} is set.'
+                                               .format('corresponding bit in '
+                                                       if w else '',
+                                                       n)),
+                                 'rw',
+                                 'hro',
+                                 False,
+                                 [])
+        self._add_intr_alert_reg(interrupts,
+                                 'INTR_TEST',
+                                 'Interrupt Test Register',
+                                 lambda w, n: ('Write 1 to force '
+                                               '{}!!INTR_STATE.{} to 1.'
+                                               .format('corresponding bit in '
+                                                       if w else '',
+                                                       n)),
+                                 'wo',
+                                 'hro',
+                                 True,
+                                 # intr_test csr is WO so reads back 0s
+                                 ["excl:CsrNonInitTests:CsrExclWrite"])
+
+    def make_alert_regs(self, alerts: List[Alert]) -> None:
+        assert alerts
+        assert len(alerts) < self._reg_width
+        self._add_intr_alert_reg(alerts,
+                                 'ALERT_TEST',
+                                 'Alert Test Register',
+                                 ('Write 1 to trigger '
+                                  'one alert event of this kind.'),
+                                 'wo',
+                                 'hro',
+                                 True,
+                                 [])
+
+    def get_addr_width(self) -> int:
+        '''Calculate the number of bits to address every byte of the block'''
+        return (self.offset - 1).bit_length()
diff --git a/util/reggen/reg_pkg.sv.tpl b/util/reggen/reg_pkg.sv.tpl
index f97b476..780d9e3 100644
--- a/util/reggen/reg_pkg.sv.tpl
+++ b/util/reggen/reg_pkg.sv.tpl
@@ -9,12 +9,16 @@
   from reggen.register import Register
   from reggen.multi_register import MultiRegister
 
-  flat_regs = block.reg_block.flat_regs
+  flat_regs = block.regs.flat_regs
   num_regs = len(flat_regs)
   max_regs_char = len("{}".format(num_regs - 1))
   localparams = block.params.get_localparams()
+  addr_width = block.regs.get_addr_width()
+
+  lblock = block.name.lower()
+  ublock = lblock.upper()
 %>\
-package ${block.name}_reg_pkg;
+package ${lblock}_reg_pkg;
 % if localparams:
 
   // Param list
@@ -24,12 +28,12 @@
 % endfor
 
   // Address width within the block
-  parameter int BlockAw = ${block.addr_width};
+  parameter int BlockAw = ${addr_width};
 
   ////////////////////////////
   // Typedefs for registers //
   ////////////////////////////
-% for r in block.reg_block.all_regs:
+% for r in block.regs.all_regs:
   % if r.get_n_bits(["q"]):
 <%
     if isinstance(r, Register):
@@ -41,7 +45,7 @@
       type_suff = 'mreg_t'
 
     reg2hw_name = ('{}_reg2hw_{}_{}'
-                   .format(block.name, r0.name.lower(), type_suff))
+                   .format(lblock, r0.name.lower(), type_suff))
 %>\
   typedef struct packed {
     % if r.is_homogeneous():
@@ -97,7 +101,7 @@
   %endif
 % endfor
 
-% for r in block.reg_block.all_regs:
+% for r in block.regs.all_regs:
   % if r.get_n_bits(["d"]):
 <%
     if isinstance(r, Register):
@@ -109,7 +113,7 @@
       type_suff = 'mreg_t'
 
     hw2reg_name = ('{}_hw2reg_{}_{}'
-                   .format(block.name, r0.name.lower(), type_suff))
+                   .format(lblock, r0.name.lower(), type_suff))
 %>\
   typedef struct packed {
     % if r.is_homogeneous():
@@ -155,12 +159,12 @@
   // Register to internal design logic //
   ///////////////////////////////////////
 <%
-nbits = block.reg_block.get_n_bits(["q", "qe", "re"])
+nbits = block.regs.get_n_bits(["q", "qe", "re"])
 packbit = 0
 %>\
 % if nbits > 0:
   typedef struct packed {
-% for r in block.reg_block.all_regs:
+% for r in block.regs.all_regs:
   % if r.get_n_bits(["q"]):
 <%
     if isinstance(r, MultiRegister):
@@ -173,7 +177,7 @@
       type_suff = 'reg_t'
 
     struct_type = ('{}_reg2hw_{}_{}'
-                   .format(block.name, r0.name.lower(), type_suff))
+                   .format(lblock, r0.name.lower(), type_suff))
 
     struct_width = r0.get_n_bits(['q', 'qe', 're']) * repl_count
     msb = nbits - packbit - 1
@@ -183,19 +187,19 @@
     ${struct_type} ${r0.name.lower()}; // [${msb}:${lsb}]
   % endif
 % endfor
-  } ${block.name}_reg2hw_t;
+  } ${lblock}_reg2hw_t;
 % endif
 
   ///////////////////////////////////////
   // Internal design logic to register //
   ///////////////////////////////////////
 <%
-nbits = block.reg_block.get_n_bits(["d", "de"])
+nbits = block.regs.get_n_bits(["d", "de"])
 packbit = 0
 %>\
 % if nbits > 0:
   typedef struct packed {
-% for r in block.reg_block.all_regs:
+% for r in block.regs.all_regs:
   % if r.get_n_bits(["d"]):
 <%
     if isinstance(r, MultiRegister):
@@ -208,7 +212,7 @@
       type_suff = 'reg_t'
 
     struct_type = ('{}_hw2reg_{}_{}'
-                   .format(block.name, r0.name.lower(), type_suff))
+                   .format(lblock, r0.name.lower(), type_suff))
 
     struct_width = r0.get_n_bits(['d', 'de']) * repl_count
     msb = nbits - packbit - 1
@@ -218,12 +222,11 @@
     ${struct_type} ${r0.name.lower()}; // [${msb}:${lsb}]
   % endif
 % endfor
-  } ${block.name}_hw2reg_t;
+  } ${lblock}_hw2reg_t;
 % endif
 
   // Register Address
 <%
-ublock = block.name.upper()
 
 def reg_pfx(reg):
   return '{}_{}'.format(ublock, reg.name.upper())
@@ -236,7 +239,7 @@
 
 %>\
 % for r in flat_regs:
-  parameter logic [BlockAw-1:0] ${reg_pfx(r)}_OFFSET = ${block.addr_width}'h ${"%x" % r.offset};
+  parameter logic [BlockAw-1:0] ${reg_pfx(r)}_OFFSET = ${addr_width}'h ${"%x" % r.offset};
 % endfor
 
 <%
@@ -262,13 +265,13 @@
   % endfor
 
 % endif
-% if len(block.reg_block.windows) > 0:
+% if len(block.regs.windows) > 0:
   // Window parameter
-% for i,w in enumerate(block.reg_block.windows):
+% for i,w in enumerate(block.regs.windows):
 <%
     win_pfx = '{}_{}'.format(ublock, w.name.upper())
-    base_txt_val = "{}'h {:x}".format(block.addr_width, w.offset)
-    size_txt_val = "{}'h {:x}".format(block.addr_width, w.size_in_bytes)
+    base_txt_val = "{}'h {:x}".format(addr_width, w.offset)
+    size_txt_val = "{}'h {:x}".format(addr_width, w.size_in_bytes)
 %>\
   parameter logic [BlockAw-1:0] ${win_pfx}_OFFSET = ${base_txt_val};
   parameter logic [BlockAw-1:0] ${win_pfx}_SIZE   = ${size_txt_val};
@@ -280,7 +283,7 @@
 % for r in flat_regs:
     ${ublock}_${r.name.upper()}${"" if loop.last else ","}
 % endfor
-  } ${block.name}_id_e;
+  } ${lblock}_id_e;
 
   // Register width information to check illegal writes
   parameter logic [3:0] ${ublock}_PERMIT [${len(flat_regs)}] = '{
diff --git a/util/reggen/reg_top.sv.tpl b/util/reggen/reg_top.sv.tpl
index 474f37c..1923d19 100644
--- a/util/reggen/reg_top.sv.tpl
+++ b/util/reggen/reg_top.sv.tpl
@@ -4,19 +4,23 @@
 //
 // Register Top module auto-generated by `reggen`
 <%
-  from reggen.data import get_basename
+  from reggen.lib import get_basename
   from reggen.register import Register
   from reggen.multi_register import MultiRegister
 
-  num_wins = len(block.reg_block.windows)
+  num_wins = len(block.regs.windows)
   num_wins_width = ((num_wins+1).bit_length()) - 1
   num_dsp  = num_wins + 1
-  regs_flat = block.reg_block.flat_regs
+  regs_flat = block.regs.flat_regs
   max_regs_char = len("{}".format(len(regs_flat) - 1))
+  addr_width = block.regs.get_addr_width()
+
+  lblock = block.name.lower()
+  ublock = lblock.upper()
 %>
 `include "prim_assert.sv"
 
-module ${block.name}_reg_top (
+module ${lblock}_reg_top (
   input clk_i,
   input rst_ni,
 
@@ -31,11 +35,11 @@
 
 % endif
   // To HW
-% if block.reg_block.get_n_bits(["q","qe","re"]):
-  output ${block.name}_reg_pkg::${block.name}_reg2hw_t reg2hw, // Write
+% if block.regs.get_n_bits(["q","qe","re"]):
+  output ${lblock}_reg_pkg::${lblock}_reg2hw_t reg2hw, // Write
 % endif
-% if block.reg_block.get_n_bits(["d","de"]):
-  input  ${block.name}_reg_pkg::${block.name}_hw2reg_t hw2reg, // Read
+% if block.regs.get_n_bits(["d","de"]):
+  input  ${lblock}_reg_pkg::${lblock}_hw2reg_t hw2reg, // Read
 % endif
 
   // Integrity check errors
@@ -45,10 +49,10 @@
   input devmode_i // If 1, explicit error return for unmapped register access
 );
 
-  import ${block.name}_reg_pkg::* ;
+  import ${lblock}_reg_pkg::* ;
 
-  localparam int AW = ${block.addr_width};
-  localparam int DW = ${block.width};
+  localparam int AW = ${addr_width};
+  localparam int DW = ${block.regwidth};
   localparam int DBW = DW/8;                    // Byte Width
 
   // register signals
@@ -105,7 +109,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.reg_block.windows):
+  % for i,t in enumerate(block.regs.windows):
   assign tl_win_o[${i}] = tl_socket_h2d[${i}];
   assign tl_socket_d2h[${i}] = tl_win_i[${i}];
   % endfor
@@ -136,12 +140,12 @@
     reg_steer = ${num_dsp-1};       // Default set to register
 
     // TODO: Can below codes be unique case () inside ?
-  % for i,w in enumerate(block.reg_block.windows):
+  % for i,w in enumerate(block.regs.windows):
 <%
     base_addr = w.offset
     limit_addr = w.offset + w.size_in_bytes
 %>\
-      % if limit_addr == 2**block.addr_width:
+      % if limit_addr == 2**addr_width:
     if (tl_i.a_address[AW-1:0] >= ${base_addr}) begin
       // Exceed or meet the address range. Removed the comparison of limit addr 'h ${'{:x}'.format(limit_addr)}
       % else:
@@ -192,7 +196,7 @@
   % endfor
 
   // Register instances
-  % for r in block.reg_block.all_regs:
+  % for r in block.regs.all_regs:
   ######################## multiregister ###########################
     % if isinstance(r, MultiRegister):
 <%
@@ -251,7 +255,7 @@
       % endfor
     % endif
 
-  ## for: block.reg_block.all_regs
+  ## for: block.regs.all_regs
   % endfor
 
 
@@ -259,7 +263,7 @@
   always_comb begin
     addr_hit = '0;
     % for i,r in enumerate(regs_flat):
-    addr_hit[${"{}".format(i).rjust(max_regs_char)}] = (reg_addr == ${block.name.upper()}_${r.name.upper()}_OFFSET);
+    addr_hit[${"{}".format(i).rjust(max_regs_char)}] = (reg_addr == ${ublock}_${r.name.upper()}_OFFSET);
     % endfor
   end
 
@@ -270,7 +274,7 @@
     wr_err = 1'b0;
     % for i,r in enumerate(regs_flat):
 <% index_str = "{}".format(i).rjust(max_regs_char) %>\
-    if (addr_hit[${index_str}] && reg_we && (${block.name.upper()}_PERMIT[${index_str}] != (${block.name.upper()}_PERMIT[${index_str}] & reg_be))) wr_err = 1'b1 ;
+    if (addr_hit[${index_str}] && reg_we && (${ublock}_PERMIT[${index_str}] != (${ublock}_PERMIT[${index_str}] & reg_be))) wr_err = 1'b1 ;
     % endfor
   end
   % for i, r in enumerate(regs_flat):
diff --git a/util/reggen/top.py b/util/reggen/top.py
new file mode 100644
index 0000000..f69d169
--- /dev/null
+++ b/util/reggen/top.py
@@ -0,0 +1,80 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+'''Code representing the entire chip for reggen'''
+
+from typing import Dict, List, Tuple, Union
+
+from .block import Block
+from .ip_block import IpBlock
+from .params import Params
+from .reg_block import RegBlock
+from .window import Window
+
+_Triple = Tuple[int, str, IpBlock]
+
+
+class Top(Block):
+    def __init__(self,
+                 regwidth: int,
+                 blocks: Dict[int, Tuple[str, IpBlock]],
+                 windows: List[Window]):
+        super().__init__('chip',
+                         regwidth,
+                         RegBlock(regwidth, Params()))
+
+        self.regwidth = regwidth
+
+        addrsep = (regwidth + 7) // 8
+
+        # A list of blocks. Each appears just once, even if it has multiple instances
+        self.blocks = []  # type: List[IpBlock]
+
+        # A dictionary mapping instance name to a pair (base_addr, block)
+        self.by_inst_name = {}  # type: Dict[str, Tuple[int, IpBlock]]
+
+        # A list of tuples: (addr, instance_name, block) in increasing order of
+        # address.
+        self.instances = []  # type: List[_Triple]
+
+        # A dictionary mapping block name to instances of that block. Items are
+        # pairs (addr, instance_name, block).
+        self.block_name_to_instances = {}  # type: Dict[str, List[_Triple]]
+
+        # Generate one big map from base address to object (with each value
+        # either a pair (instance_name, block) or a window).
+        addr_to_obj = {}  # type: Dict[int, Union[Tuple[str, IpBlock], Window]]
+        for addr, pr in blocks.items():
+            addr_to_obj[addr] = pr
+        for window in windows:
+            addr_to_obj[window.offset] = window
+
+        # Now walk this big map again, constructing the two views on the blocks
+        # and adding the windows as we go but checking for overlaps the whole
+        # time.
+        offset = 0
+        for base_addr in sorted(addr_to_obj.keys()):
+            obj = addr_to_obj[base_addr]
+
+            # Make sure that this block doesn't overlap with the previous one
+            assert offset <= base_addr
+
+            if isinstance(obj, Window):
+                offset = obj.next_offset(addrsep)
+                self.regs.add_window(obj)
+            else:
+                name, block = blocks[base_addr]
+
+                if block.name not in self.block_name_to_instances:
+                    self.blocks.append(block)
+
+                assert name not in self.by_inst_name
+                self.by_inst_name[name] = (base_addr, block)
+
+                triple = (base_addr, name, block)
+                self.instances.append(triple)
+                insts = self.block_name_to_instances.setdefault(block.name, [])
+                insts.append(triple)
+
+                offset = base_addr + block.regs.offset
diff --git a/util/reggen/uvm_reg.sv.tpl b/util/reggen/uvm_reg.sv.tpl
index a4d932b..58e40f2 100644
--- a/util/reggen/uvm_reg.sv.tpl
+++ b/util/reggen/uvm_reg.sv.tpl
@@ -4,30 +4,40 @@
 
 // UVM Registers auto-generated by `reggen` containing data structure
 // Do Not Edit directly
-<% from reggen import (gen_dv)
+<%
+from reggen import (gen_dv)
+from reggen.ip_block import IpBlock
+from reggen.top import Top
+
 %>\
 <%def name="construct_classes(block)">\
-% for b in block.blocks:
-${construct_classes(b)}
-% endfor
 <%
-regs_flat = block.reg_block.flat_regs
-hier_path = ""
-if (block.hier_path):
-  hier_path = block.hier_path + "."
-%>\
+  if isinstance(block, IpBlock) and block.hier_path is not None:
+    hier_path = block.hier_path + "."
+  else:
+    hier_path = ""
 
-// Block: ${block.name}
-package ${block.name}_ral_pkg;
+  regs_flat = block.regs.flat_regs
+%>\
+% if isinstance(block, Top):
+  % for sub_block in block.blocks:
+${construct_classes(sub_block)}
+  % endfor
+% endif
+
+// Block: ${block.name.lower()}
+package ${block.name.lower()}_ral_pkg;
   // dep packages
   import uvm_pkg::*;
   import dv_base_reg_pkg::*;
 % if dv_base_prefix != "dv_base":
   import ${dv_base_prefix}_reg_pkg::*;
 % endif
-% for b in block.blocks:
-  import ${b.name}_ral_pkg::*;
-% endfor
+% if isinstance(block, Top):
+  % for b in block.blocks:
+  import ${b.name.lower()}_ral_pkg::*;
+  % endfor
+% endif
 
   // macro includes
   `include "uvm_macros.svh"
@@ -36,14 +46,14 @@
 % for r in regs_flat:
   typedef class ${gen_dv.rcname(block, r)};
 % endfor
-% for w in block.reg_block.windows:
+% for w in block.regs.windows:
   typedef class ${gen_dv.mcname(block, w)};
 % endfor
   typedef class ${gen_dv.bcname(block)};
 
 % for r in regs_flat:
 <%
-  reg_width = block.width
+  reg_width = block.regwidth
   reg_name = r.name.lower()
   is_ext = 0
   reg_shadowed = r.shadowed
@@ -152,7 +162,7 @@
   endclass : ${gen_dv.rcname(block, r)}
 
 % endfor
-% for w in block.reg_block.windows:
+% for w in block.regs.windows:
 <%
   mem_name = w.name.lower()
   mem_right = w.swaccess.dv_rights()
@@ -180,24 +190,24 @@
 % endfor
   // Class: ${gen_dv.bcname(block)}
   class ${gen_dv.bcname(block)} extends ${dv_base_prefix}_reg_block;
-% if block.blocks:
+% if isinstance(block, Top):
     // sub blocks
-% endif
-% for b in block.blocks:
-  % for inst in b.base_addr.keys():
+  % for b in block.blocks:
+    % for base_addr, inst, _ in block.block_name_to_instances[b.name]:
     rand ${gen_dv.bcname(b)} ${inst};
+    % endfor
   % endfor
-% endfor
+%endif
 % if regs_flat:
     // registers
 % endif
 % for r in regs_flat:
     rand ${gen_dv.rcname(block, r)} ${r.name.lower()};
 % endfor
-% if block.reg_block.windows:
+% if block.regs.windows:
     // memories
 % endif
-% for w in block.reg_block.windows:
+% for w in block.regs.windows:
     rand ${gen_dv.mcname(block, w)} ${gen_dv.miname(w)};
 % endfor
 
@@ -213,28 +223,28 @@
       // create default map
       this.default_map = create_map(.name("default_map"),
                                     .base_addr(base_addr),
-                                    .n_bytes(${block.width//8}),
+                                    .n_bytes(${block.regwidth//8}),
                                     .endian(UVM_LITTLE_ENDIAN));
       if (csr_excl == null) begin
         csr_excl = csr_excl_item::type_id::create("csr_excl");
         this.csr_excl = csr_excl;
       end
-% if block.blocks:
+% if isinstance(block, Top):
 
       // create sub blocks and add their maps
-% endif
-% for b in block.blocks:
-  % for inst, base_addr in b.base_addr.items():
+  % for b in block.blocks:
+    % for base_addr, inst, _ in block.block_name_to_instances[b.name]:
       ${inst} = ${gen_dv.bcname(b)}::type_id::create("${inst}");
       ${inst}.configure(.parent(this));
-      ${inst}.build(.base_addr(base_addr + ${gen_dv.sv_base_addr(b, inst)}), .csr_excl(csr_excl));
+      ${inst}.build(.base_addr(base_addr + ${gen_dv.sv_base_addr(block, inst)}), .csr_excl(csr_excl));
       ${inst}.set_hdl_path_root("tb.dut.top_earlgrey.u_${inst}", "BkdrRegPathRtl");
       ${inst}.set_hdl_path_root("tb.dut.top_earlgrey.u_${inst}", "BkdrRegPathRtlCommitted");
       ${inst}.set_hdl_path_root("tb.dut.top_earlgrey.u_${inst}", "BkdrRegPathRtlShadow");
       default_map.add_submap(.child_map(${inst}.default_map),
-                             .offset(base_addr + ${gen_dv.sv_base_addr(b, inst)}));
+                             .offset(base_addr + ${gen_dv.sv_base_addr(block, inst)}));
+    % endfor
   % endfor
-% endfor
+% endif
 % if regs_flat:
       set_hdl_path_root("tb.dut", "BkdrRegPathRtl");
       set_hdl_path_root("tb.dut", "BkdrRegPathRtlCommitted");
@@ -245,7 +255,7 @@
 <%
   reg_name = r.name.lower()
   reg_right = r.dv_rights()
-  reg_width = block.width
+  reg_width = block.regwidth
   reg_offset =  str(reg_width) + "'h" + "%x" % r.offset
   reg_tags = r.tags
   reg_shadowed = r.shadowed
@@ -291,15 +301,15 @@
   % endif
 % endfor
 
-% if block.reg_block.windows:
+% if block.regs.windows:
 
       // create memories
 % endif
-% for w in block.reg_block.windows:
+% for w in block.regs.windows:
 <%
   mem_name = w.name.lower()
   mem_right = w.swaccess.dv_rights()
-  mem_offset = str(block.width) + "'h" + "%x" % w.offset
+  mem_offset = str(block.regwidth) + "'h" + "%x" % w.offset
   mem_n_bits = w.validbits
   mem_size = w.items
 %>\
diff --git a/util/reggen/validate.py b/util/reggen/validate.py
index 33f05da..e1cea7f 100644
--- a/util/reggen/validate.py
+++ b/util/reggen/validate.py
@@ -6,33 +6,6 @@
 """
 
 import logging as log
-from typing import List
-
-from .access import SWAccess, HWAccess
-from .alert import Alert
-from .field import Field
-from .inter_signal import InterSignal
-from .lib import check_list
-from .params import LocalParam, Params
-from .reg_block import RegBlock
-from .register import Register
-from .signal import Signal
-
-
-# Routine that can be used for Hjson object_pairs_hook
-# The baseline is dict(pairs) i.e. construct a dictonary from pairs
-# The usual is OrderedDict(pairs) which is redundant in latest python
-# Both of these silently allow repeated keys, which this version detects
-def checking_dict(pairs):
-    d = {}
-    for x in pairs:
-        if x[0] in d:
-            repkey = 'Repeated' + x[0]
-            log.warn("Repeated key " + x[0] + " added as " + repkey)
-            d[repkey] = x[1]
-        else:
-            d[x[0]] = x[1]
-    return d
 
 
 # validating version of int(x, 0)
@@ -156,64 +129,6 @@
     'pe': ["python enum", "Native Python type enum (generated)"]
 }
 
-# Toplevel keys
-top_required = {
-    'name': ['s', "name of the component"],
-    'clock_primary': ['s', "name of the primary clock"],
-    'bus_device': ['s', "name of the bus interface for the device"],
-    'registers':
-    ['l', "list of register definition groups and "
-     "offset control groups"]
-}
-top_optional = {
-    'alert_list': ['ln', "list of peripheral alerts"],
-    'available_inout_list': ['lnw', "list of available peripheral inouts"],
-    'available_input_list': ['lnw', "list of available peripheral inputs"],
-    'available_output_list': ['lnw', "list of available peripheral outputs"],
-    'bus_host': ['s', "name of the bus interface as host"],
-    'hier_path': [
-        None,
-        'additional hierarchy path before the reg block instance'
-    ],
-    'interrupt_list': ['lnw', "list of peripheral interrupts"],
-    'inter_signal_list': ['l', "list of inter-module signals"],
-    'no_auto_alert_regs': [
-        's', "Set to true to suppress automatic "
-        "generation of alert test registers. "
-        "Defaults to true if no alert_list is present. "
-        "Otherwise this defaults to false. "
-    ],
-    'no_auto_intr_regs': [
-        's', "Set to true to suppress automatic "
-        "generation of interrupt registers. "
-        "Defaults to true if no interrupt_list is present. "
-        "Otherwise this defaults to false. "
-    ],
-    'other_clock_list': ['l', "list of other chip clocks needed"],
-    'other_reset_list': ['l', "list of other resets"],
-    'param_list': ['lp', "list of parameters of the IP"],
-    'regwidth': ['d', "width of registers in bits (default 32)"],
-    'reset_primary': ['s', "primary reset used by the module"],
-    'reset_request_list': ['l', 'list of signals requesting reset'],
-    'scan': ['pb', 'Indicates the module have `scanmode_i`'],
-    'scan_reset': ['pb', 'Indicates the module have `test_rst_ni`'],
-    'SPDX-License-Identifier': [
-        's', "License ientifier (if using pure json) "
-        "Only use this if unable to put this "
-        "information in a comment at the top of the "
-        "file."
-    ],
-    'wakeup_list': ['lnw', "list of peripheral wakeups"]
-}
-top_added = {
-    'genrnames': ['pl', "list of register names"],
-    'genautoregs': ['pb', "Registers were generated from config info"],
-    'gensize': [
-        'pi', "address space size needed for registers. "
-        "Generated by tool as next power of 2."
-    ]
-}
-
 # ln type has list of groups with only name and description
 # (was called "subunit" in cfg_validate)
 ln_required = {
@@ -238,287 +153,3 @@
 }
 
 key_use = {'r': "required", 'o': "optional", 'a': "added by tool"}
-
-
-def make_intr_alert_reg(reg_block, signals, name, swaccess, hwaccess, desc):
-    # these names will be converted into test registers
-    testreg_names = ['INTR_TEST', 'ALERT_TEST']
-
-    swaccess_obj = SWAccess('make_intr_alert_reg()', swaccess)
-    hwaccess_obj = HWAccess('make_intr_alert_reg()', hwaccess)
-
-    fields = []
-    for signal in signals:
-        width = signal.bits.width()
-
-        if name == 'INTR_ENABLE':
-            field_desc = ('Enable interrupt when {}!!INTR_STATE.{} is set.'
-                          .format('corresponding bit in ' if width > 1 else '',
-                                  signal.name))
-        elif name == 'INTR_TEST':
-            field_desc = ('Write 1 to force {}!!INTR_STATE.{} to 1.'
-                          .format('corresponding bit in ' if width > 1 else '',
-                                  signal.name))
-        elif name == 'ALERT_TEST':
-            field_desc = 'Write 1 to trigger one alert event of this kind.'
-        else:
-            field_desc = signal.desc
-
-        fields.append(Field(signal.name,
-                            field_desc,
-                            tags=[],
-                            swaccess=swaccess_obj,
-                            hwaccess=hwaccess_obj,
-                            hwqe=name in testreg_names,
-                            hwre=False,
-                            bits=signal.bits,
-                            resval=0,
-                            enum=None))
-
-    hwext = 'true' if name in testreg_names else 'false'
-    if name == 'INTR_TEST':
-        # intr_test csr is WO which - it reads back 0s
-        reg_tags = ["excl:CsrNonInitTests:CsrExclWrite"]
-    elif name == 'INTR_STATE':
-        # intr_state csr is affected by writes to other csrs - skip write-check
-        reg_tags = ["excl:CsrNonInitTests:CsrExclWriteCheck"]
-    else:
-        reg_tags = []
-
-    bool_hwext = hwext.lower() == 'true'
-
-    reg = Register(reg_block.offset,
-                   name,
-                   desc,
-                   swaccess_obj,
-                   hwaccess_obj,
-                   hwext=bool_hwext,
-                   hwqe=bool_hwext,
-                   hwre=False,
-                   regwen=None,
-                   tags=reg_tags,
-                   resval=None,
-                   shadowed=False,
-                   fields=fields,
-                   update_err_alert=None,
-                   storage_err_alert=None)
-    reg_block.add_register(reg)
-    return reg
-
-
-def make_intr_regs(reg_block, interrupt_list, fullwidth):
-    assert interrupt_list
-
-    iregs = []
-    msb = interrupt_list[-1].bits.msb
-    if msb >= fullwidth:
-        log.error('More than {} interrupts in list'.format(fullwidth))
-        return iregs, 1
-
-    try:
-        new_reg = make_intr_alert_reg(reg_block, interrupt_list, 'INTR_STATE', 'rw1c',
-                                      'hrw', 'Interrupt State Register')
-        iregs.append(new_reg)
-        new_reg = make_intr_alert_reg(reg_block, interrupt_list, 'INTR_ENABLE', 'rw',
-                                      'hro', 'Interrupt Enable Register')
-        iregs.append(new_reg)
-        new_reg = make_intr_alert_reg(reg_block, interrupt_list, '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(reg_block, alert_list, fullwidth):
-    assert alert_list
-
-    alert_regs = []
-    if len(alert_list) > fullwidth:
-        log.error('More than {} alerts in list'.format(fullwidth))
-        return alert_regs, 1
-
-    try:
-        new_reg = make_intr_alert_reg(reg_block, alert_list, '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
-
-
-def validate(regs, params: List[str]):
-    if 'name' not in regs:
-        log.error("Component has no name. Aborting.")
-        return 1
-
-    component = regs['name']
-
-    error = check_keys(regs, top_required, top_optional, top_added, component)
-    if (error > 0):
-        log.error("Component has top level errors. Aborting.")
-        return error
-    regs['genrnames'] = []
-    error = 0
-
-    if 'regwidth' in regs:
-        fullwidth, ierr = check_int(regs['regwidth'], "regwidth")
-        if ierr:
-            fullwidth = 32
-            error += 1
-    else:
-        fullwidth = 32
-        log.warning('regwidth not specified, assuming 32.')
-    regs['regwidth'] = str(fullwidth)
-
-    if ((fullwidth % 8) != 0):
-        addrsep = (fullwidth // 8) + 1
-        log.warning("regwidth is not a multiple of 8 bits!")
-    else:
-        addrsep = fullwidth // 8
-
-    param_list = Params.from_raw('block parameter list',
-                                 regs.get('param_list', []))
-    regs['param_list'] = param_list
-
-    reg_block = RegBlock(addrsep, fullwidth, param_list)
-
-    autoregs = []
-
-    # auto header generation would go here and update autoregs
-
-    interrupt_list = Signal.from_raw_list('interrupt_list for block {}'
-                                          .format(component),
-                                          regs.get('interrupt_list', []))
-    alert_list = Alert.from_raw_list('alert_list for block {}'
-                                     .format(component),
-                                     regs.get('alert_list', []))
-
-    regs['interrupt_list'] = interrupt_list
-    regs['alert_list'] = alert_list
-
-    if 'no_auto_intr_regs' in regs:
-        no_auto_intr, err = check_bool(regs['no_auto_intr_regs'],
-                                       'no_auto_intr_regs')
-        if err:
-            error += 1
-    else:
-        no_auto_intr = not interrupt_list
-
-    if 'no_auto_alert_regs' in regs:
-        no_auto_alerts, err = check_bool(regs['no_auto_alert_regs'],
-                                         'no_auto_alert_regs')
-        if err:
-            error += 1
-    else:
-        no_auto_alerts = not alert_list
-
-    if interrupt_list and 'genautoregs' not in regs and not no_auto_intr:
-        iregs, err = make_intr_regs(reg_block, interrupt_list, fullwidth)
-        error += err
-        autoregs.extend(iregs)
-
-    # Generate a NumAlerts parameter for provided alert_list.
-    if alert_list:
-        # Generate alert test registers.
-        if 'genautoregs' not in regs and not no_auto_alerts:
-            aregs, err = make_alert_regs(reg_block, alert_list, fullwidth)
-            error += err
-            autoregs.extend(aregs)
-
-        num_alerts = len(alert_list)
-        for alert in alert_list:
-            # check alert naming scheme
-            if alert.name == "":
-                log.error("{}: Alert name cannot be empty".format(alert.name))
-                error += 1
-            prefix = alert.name.split('_')
-            if prefix[0] not in ['recov', 'fatal']:
-                log.error(
-                    "{}: Alerts must be prefixed with either 'recov_' or "
-                    "'fatal_'.".format(alert.name))
-                error += 1
-
-        if num_alerts:
-            existing_param = param_list.get('NumAlerts')
-            if existing_param is not None:
-                if ((not isinstance(existing_param, LocalParam) or
-                     existing_param.param_type != 'int' or
-                     existing_param.value != str(num_alerts))):
-                    log.error('Conflicting definition of NumAlerts parameter.')
-                    error += 1
-            else:
-                param_list.add(LocalParam(name='NumAlerts',
-                                          desc='Number of alerts',
-                                          param_type='int',
-                                          value=str(num_alerts)))
-
-    try:
-        param_list.apply_defaults(params)
-    except (ValueError, KeyError) as err:
-        log.error(str(err))
-        return error + 1
-
-    if "scan" in regs:
-        scan, err = check_bool(regs["scan"], component + " scan")
-    else:
-        regs["scan"] = "false"
-
-    reg_block.add_raw_registers(regs['registers'])
-
-    regs['gensize'] = 1 << (reg_block.offset - 1).bit_length()
-
-    try:
-        reg_block.validate()
-    except ValueError as err:
-        log.error(str(err))
-        error += 1
-
-    if 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):
-        log.error("Register description had " + str(error) + " error" +
-                  "s" if error > 1 else "")
-
-    try:
-        r_inter_signal_list = check_list(regs.get('inter_signal_list', []),
-                                         'inter_signal_list field')
-        inter_signal_list = [
-            InterSignal.from_raw('entry {} of the inter_signal_list field'
-                                 .format(idx + 1),
-                                 entry)
-            for idx, entry in enumerate(r_inter_signal_list)
-        ]
-    except ValueError as err:
-        log.error(str(err))
-        error += 1
-
-    regs.setdefault('bus_device', '')
-    regs.setdefault('bus_host', '')
-
-    if regs["bus_device"] == "tlul":
-        # Add to inter_module_signal
-        port_name = "tl" if regs["bus_host"] in ["none", ""] else "tl_d"
-
-        inter_signal_list.append(InterSignal(port_name, None, 'tl', 'tlul_pkg',
-                                             'req_rsp', 'rsp', 1, None))
-
-    if regs['bus_host'] == "tlul":
-        port_name = "tl" if regs["bus_host"] in ["none", ""] else "tl_h"
-
-        inter_signal_list.append(InterSignal(port_name, None, 'tl', 'tlul_pkg',
-                                             'req_rsp', 'req', 1, None))
-
-    regs['inter_signal_list'] = inter_signal_list
-
-    return error