[doc] Add autogen generator that can run commands

The tool looks for:
<!-- BEGIN AUTOGEN cmd -->
...
<!-- END AUTOGEN -->
and replaces ... with the output of `cmd`. Note that the command is executed
in a shell.

Signed-off-by: Amaury Pouly <amaury.pouly@lowrisc.org>
Co-authored-by: Hugo McNally <hugo.mcnally@gmail.com>
diff --git a/hw/top_earlgrey/doc/design/README.md b/hw/top_earlgrey/doc/design/README.md
index 8cb6fd2..fd03e15 100644
--- a/hw/top_earlgrey/doc/design/README.md
+++ b/hw/top_earlgrey/doc/design/README.md
@@ -379,13 +379,141 @@
 
 For the purpose of `top_earlgrey`, the first option has been chosen to benefit software development and testing
 
-{{< topLevelDoc "earlgrey" "mmap" >}}
+<!-- BEGIN AUTOGEN util/design/gen-top-docs.py -t hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson -g mmap -->
+| Name              | Type          | Byte Address      |
+|:------------------|:--------------|:------------------|
+| uart0             | uart          | 0x40000000 (regs) |
+| uart1             | uart          | 0x40010000 (regs) |
+| uart2             | uart          | 0x40020000 (regs) |
+| uart3             | uart          | 0x40030000 (regs) |
+| gpio              | gpio          | 0x40040000 (regs) |
+| spi_device        | spi_device    | 0x40050000 (regs) |
+| i2c0              | i2c           | 0x40080000 (regs) |
+| i2c1              | i2c           | 0x40090000 (regs) |
+| i2c2              | i2c           | 0x400A0000 (regs) |
+| pattgen           | pattgen       | 0x400E0000 (regs) |
+| rv_timer          | rv_timer      | 0x40100000 (regs) |
+| otp_ctrl          | otp_ctrl      | 0x40130000 (core) |
+|                   |               | 0x40132000 (prim) |
+| lc_ctrl           | lc_ctrl       | 0x40140000 (regs) |
+| alert_handler     | alert_handler | 0x40150000 (regs) |
+| spi_host0         | spi_host      | 0x40300000 (regs) |
+| spi_host1         | spi_host      | 0x40310000 (regs) |
+| usbdev            | usbdev        | 0x40320000 (regs) |
+| pwrmgr_aon        | pwrmgr        | 0x40400000 (regs) |
+| rstmgr_aon        | rstmgr        | 0x40410000 (regs) |
+| clkmgr_aon        | clkmgr        | 0x40420000 (regs) |
+| sysrst_ctrl_aon   | sysrst_ctrl   | 0x40430000 (regs) |
+| adc_ctrl_aon      | adc_ctrl      | 0x40440000 (regs) |
+| pwm_aon           | pwm           | 0x40450000 (regs) |
+| pinmux_aon        | pinmux        | 0x40460000 (regs) |
+| aon_timer_aon     | aon_timer     | 0x40470000 (regs) |
+| ast               | ast           | 0x40480000 (regs) |
+| sensor_ctrl       | sensor_ctrl   | 0x40490000 (regs) |
+| sram_ctrl_ret_aon | sram_ctrl     | 0x40500000 (regs) |
+|                   |               | 0x40600000 (ram)  |
+| flash_ctrl        | flash_ctrl    | 0x41000000 (core) |
+|                   |               | 0x41008000 (prim) |
+|                   |               | 0x20000000 (mem)  |
+| rv_dm             | rv_dm         | 0x00010000 (mem)  |
+|                   |               | 0x41200000 (regs) |
+| rv_plic           | rv_plic       | 0x48000000 (regs) |
+| aes               | aes           | 0x41100000 (regs) |
+| hmac              | hmac          | 0x41110000 (regs) |
+| kmac              | kmac          | 0x41120000 (regs) |
+| otbn              | otbn          | 0x41130000 (regs) |
+| keymgr            | keymgr        | 0x41140000 (regs) |
+| csrng             | csrng         | 0x41150000 (regs) |
+| entropy_src       | entropy_src   | 0x41160000 (regs) |
+| edn0              | edn           | 0x41170000 (regs) |
+| edn1              | edn           | 0x41180000 (regs) |
+| sram_ctrl_main    | sram_ctrl     | 0x411C0000 (regs) |
+|                   |               | 0x10000000 (ram)  |
+| rom_ctrl          | rom_ctrl      | 0x00008000 (rom)  |
+|                   |               | 0x411e0000 (regs) |
+| rv_core_ibex      | rv_core_ibex  | 0x411F0000 (cfg)  |
+
+<!-- END AUTOGEN -->
 
 ## Hardware Interfaces
 
 ### Pinout
 
-{{< topLevelDoc "earlgrey" "pinout" >}}
+<!-- BEGIN AUTOGEN util/design/gen-top-docs.py -t hw/top_earlgrey/data/autogen/top_earlgrey.gen.hjson -g pinout -->
+| ID   | Name             | Bank   | Type         | Connection Type   | Description                                |
+|:-----|:-----------------|:-------|:-------------|:------------------|:-------------------------------------------|
+| 0    | POR_N            | VCC    | InputStd     | manual            | System reset                               |
+| 1    | USB_P            | VCC    | DualBidirTol | manual            | USB P signal                               |
+| 2    | USB_N            | VCC    | DualBidirTol | manual            | USB N signal                               |
+| 3    | CC1              | AVCC   | InputStd     | manual            | ADC input 1                                |
+| 4    | CC2              | AVCC   | InputStd     | manual            | ADC input 2                                |
+| 5    | FLASH_TEST_VOLT  | VCC    | AnalogIn0    | manual            | Flash test voltage input                   |
+| 6    | FLASH_TEST_MODE0 | VCC    | InputStd     | manual            | Flash test mode signal                     |
+| 7    | FLASH_TEST_MODE1 | VCC    | InputStd     | manual            | Flash test mode signal                     |
+| 8    | OTP_EXT_VOLT     | VCC    | AnalogIn1    | manual            | OTP external voltage input                 |
+| 9    | SPI_HOST_D0      | VIOA   | BidirStd     | direct            | SPI host data                              |
+| 10   | SPI_HOST_D1      | VIOA   | BidirStd     | direct            | SPI host data                              |
+| 11   | SPI_HOST_D2      | VIOA   | BidirStd     | direct            | SPI host data                              |
+| 12   | SPI_HOST_D3      | VIOA   | BidirStd     | direct            | SPI host data                              |
+| 13   | SPI_HOST_CLK     | VIOA   | BidirStd     | direct            | SPI host clock                             |
+| 14   | SPI_HOST_CS_L    | VIOA   | BidirStd     | direct            | SPI host chip select                       |
+| 15   | SPI_DEV_D0       | VIOA   | BidirStd     | direct            | SPI device data                            |
+| 16   | SPI_DEV_D1       | VIOA   | BidirStd     | direct            | SPI device data                            |
+| 17   | SPI_DEV_D2       | VIOA   | BidirStd     | direct            | SPI device data                            |
+| 18   | SPI_DEV_D3       | VIOA   | BidirStd     | direct            | SPI device data                            |
+| 19   | SPI_DEV_CLK      | VIOA   | InputStd     | direct            | SPI device clock                           |
+| 20   | SPI_DEV_CS_L     | VIOA   | InputStd     | direct            | SPI device chip select                     |
+| 0    | IOA0             | VIOA   | BidirStd     | muxed             | Muxed IO pad                               |
+| 1    | IOA1             | VIOA   | BidirStd     | muxed             | Muxed IO pad                               |
+| 2    | IOA2             | VIOA   | BidirStd     | muxed             | Muxed IO pad                               |
+| 3    | IOA3             | VIOA   | BidirStd     | muxed             | Muxed IO pad                               |
+| 4    | IOA4             | VIOA   | BidirStd     | muxed             | Muxed IO pad                               |
+| 5    | IOA5             | VIOA   | BidirStd     | muxed             | Muxed IO pad                               |
+| 6    | IOA6             | VIOA   | BidirOd      | muxed             | Muxed IO pad                               |
+| 7    | IOA7             | VIOA   | BidirOd      | muxed             | Muxed IO pad                               |
+| 8    | IOA8             | VIOA   | BidirOd      | muxed             | Muxed IO pad                               |
+| 9    | IOB0             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 10   | IOB1             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 11   | IOB2             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 12   | IOB3             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 13   | IOB4             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 14   | IOB5             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 15   | IOB6             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 16   | IOB7             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 17   | IOB8             | VIOB   | BidirStd     | muxed             | Muxed IO pad                               |
+| 18   | IOB9             | VIOB   | BidirOd      | muxed             | Muxed IO pad                               |
+| 19   | IOB10            | VIOB   | BidirOd      | muxed             | Muxed IO pad                               |
+| 20   | IOB11            | VIOB   | BidirOd      | muxed             | Muxed IO pad                               |
+| 21   | IOB12            | VIOB   | BidirOd      | muxed             | Muxed IO pad                               |
+| 22   | IOC0             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 23   | IOC1             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 24   | IOC2             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 25   | IOC3             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 26   | IOC4             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 27   | IOC5             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 28   | IOC6             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 29   | IOC7             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 30   | IOC8             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 31   | IOC9             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 32   | IOC10            | VCC    | BidirOd      | muxed             | Muxed IO pad                               |
+| 33   | IOC11            | VCC    | BidirOd      | muxed             | Muxed IO pad                               |
+| 34   | IOC12            | VCC    | BidirOd      | muxed             | Muxed IO pad                               |
+| 35   | IOR0             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 36   | IOR1             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 37   | IOR2             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 38   | IOR3             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 39   | IOR4             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 40   | IOR5             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 41   | IOR6             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 42   | IOR7             | VCC    | BidirStd     | muxed             | Muxed IO pad                               |
+| 21   | IOR8             | VCC    | BidirOd      | direct            | Dedicated sysrst_ctrl output (ec_rst_l)    |
+| 22   | IOR9             | VCC    | BidirOd      | direct            | Dedicated sysrst_ctrl output (flash_wp_l)) |
+| 43   | IOR10            | VCC    | BidirOd      | muxed             | Muxed IO pad                               |
+| 44   | IOR11            | VCC    | BidirOd      | muxed             | Muxed IO pad                               |
+| 45   | IOR12            | VCC    | BidirOd      | muxed             | Muxed IO pad                               |
+| 46   | IOR13            | VCC    | BidirOd      | muxed             | Muxed IO pad                               |
+
+<!-- END AUTOGEN -->
 
 # RTL Implementation Notes
 
diff --git a/util/autogen_md.py b/util/autogen_md.py
new file mode 100755
index 0000000..e68bf30
--- /dev/null
+++ b/util/autogen_md.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+"""Go through all markdown files and autogenerate content based on commands
+recorded in the file.
+"""
+
+import sys
+from pathlib import Path
+import re
+import subprocess
+import argparse
+import os
+from typing import Callable
+
+START_MARKER_PATTERN = re.compile(r"\n<!--\s*BEGIN\s*AUTOGEN\s*(?P<cmd>.+)\s*-->\n")
+END_MARKER_PATTERN = re.compile(r"\n<!--\s*END\s*AUTOGEN\s*-->\n")
+
+
+def apply_to_all_files(path: Path, fn: Callable[[Path], None]):
+    for child in path.iterdir():
+        if child.is_file():
+            fn(child)
+        if child.is_dir():
+            apply_to_all_files(child, fn)
+
+
+def autogen_rewrite_md(filepath: Path, dry_run: bool):
+    # only consider .md files
+    if not filepath.name.endswith(".md"):
+        return
+    content = filepath.read_text()
+    modified = False
+    pos = 0
+    while True:
+        # search next start marker
+        match_start = START_MARKER_PATTERN.search(content, pos)
+        if match_start is None:
+            # no more replacements to do
+            break
+        cmd = match_start.group('cmd').strip()
+        # search end marker after the start marker
+        match_end = END_MARKER_PATTERN.search(content, pos)
+        if match_end is None:
+            sys.exit(f"Error in {filepath}: start marker with command '{cmd}' without end marker")
+
+        print(f"In {filepath}: running '{cmd}'")
+        if dry_run:
+            # don't run
+            pos = match_end.end(0)
+            continue
+        res = subprocess.run(cmd, shell=True, capture_output=True)
+        if res.stderr:
+            print(
+                f"The command '{cmd}' output "
+                f"the following error messages:\n{res.stderr.decode()}"
+            )
+        if res.returncode != 0:
+            sys.exit(f"The command '{cmd}' had a non-zero return code.")
+
+        new_content = res.stdout.decode()
+        # replace content
+        content = content[0:match_start.end(0)] + new_content + content[match_end.start(0):]
+        modified = True
+        # resume after end line, adjust for new content size
+        pos = match_end.end(0) + len(new_content) - (match_end.start(0) - match_start.end(0))
+    # write back
+    if modified:
+        filepath.write_text(content)
+
+
+def main(args: argparse.Namespace):
+    this_file_path = Path(__file__)
+    repo_root = this_file_path.parent.parent.resolve()
+    os.chdir(repo_root)
+    apply_to_all_files(repo_root, lambda x: autogen_rewrite_md(x, args.dry_run))
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--dry-run",
+                        help = "print commands to execute but do not execute them",
+                        action="store_true")
+    args = parser.parse_args()
+    main(args)
diff --git a/util/design/gen-top-docs.py b/util/design/gen-top-docs.py
index 784a712..5cceed5 100755
--- a/util/design/gen-top-docs.py
+++ b/util/design/gen-top-docs.py
@@ -4,9 +4,9 @@
 # SPDX-License-Identifier: Apache-2.0
 r"""Generates top level documentation from an hjson configuration file."""
 
+import sys
 import argparse
 import logging as log
-from pathlib import Path
 
 import hjson
 from lib import common
@@ -72,40 +72,30 @@
         description=common.wrapped_docstring(),
         formatter_class=argparse.RawDescriptionHelpFormatter)
 
-    parser.add_argument("--topcfg",
-                    "-t",
-                    required=True,
-                    help="Topgen generated config file `top_{name}.hjson`.")
     parser.add_argument(
-        "--outdir",
-        "-o",
-        help="Target TOP documentation directory.")
-
+        "--topcfg",
+        "-t",
+        required=True,
+        help="Topgen generated config file `top_{name}.hjson`.",
+    )
+    parser.add_argument(
+        "--generator",
+        "-g",
+        help="Select generator",
+    )
     args = parser.parse_args()
+    gen = args.generator
 
-    outdir = Path(args.outdir)
-
-    doc_generators = [
-        {
-            "filename": "mmap.md",
-            "generator": generate_mmap_table,
-        },
-        {
-            "filename": "pinout.md",
-            "generator": generate_pinout_table,
-        },
-    ]
-
+    doc_generators = {
+        "mmap": generate_mmap_table,
+        "pinout": generate_pinout_table,
+    }
     with open(args.topcfg, 'r') as infile:
         top_level = hjson.load(infile)
-        top_outdir = outdir / top_level["name"]
-        top_outdir.mkdir(parents=True, exist_ok=True)
+        if gen not in doc_generators:
+            sys.exit(f"Unknown generator {gen}")
 
-        for doc in doc_generators:
-            outfile = top_outdir / doc["filename"]
-            table = doc["generator"](top_level)
-            with open(outfile, 'w') as f:
-                f.write(to_markdown(table))
+        print(doc_generators[gen](top_level))
 
 
 if __name__ == "__main__":
diff --git a/util/reggen/README.md b/util/reggen/README.md
index 1c84eab..b13a1a6 100644
--- a/util/reggen/README.md
+++ b/util/reggen/README.md
@@ -29,7 +29,344 @@
 
 For more detail on the non-register entries of the Hjson configuration file, see [this section](../../doc/contributing/hw/comportability/README.md#configuration-description-hjson) of the Comportability Specification.
 
-{{% selfdoc "reggen" %}}
+<!-- BEGIN AUTOGEN python3 util/selfdoc.py reggen -->
+
+
+<!-- Start of output generated by `regtool.py --doc` -->
+
+The tables describe each key and the type of the value. The following
+types are used:
+
+Type | Description
+---- | -----------
+int | integer (binary 0b, octal 0o, decimal, hex 0x)
+xint | x for undefined otherwise int
+bitrange | bit number as decimal integer, or bit-range as decimal integers msb:lsb
+list | comma separated list enclosed in `[]`
+name list | comma separated list enclosed in `[]` of one or more groups that have just name and dscr keys. e.g. `{ name: "name", desc: "description"}`
+name list+ | name list that optionally contains a width
+parameter list | parameter list having default value optionally
+group | comma separated group of key:value enclosed in `{}`
+list of group | comma separated group of key:value enclosed in `{}` the second entry of the list is the sub group format
+string | string, typically short
+text | string, may be multi-line enclosed in `'''` may use `**bold**`, `*italic*` or `!!Reg` markup
+tuple | tuple enclosed in ()
+python int | Native Python type int (generated)
+python Bool | Native Python type Bool (generated)
+python list | Native Python type list (generated)
+python enum | Native Python type enum (generated)
+
+
+Register fields are tagged using the swaccess key to describe the
+permitted access and side-effects. This key must have one of these
+values:
+
+
+Key | Description
+--- | -----------
+none | No access
+ro | Read Only
+rc | Read Only, reading clears
+rw | Read/Write
+r0w1c | Read zero, Write with 1 clears
+rw1s | Read, Write with 1 sets
+rw1c | Read, Write with 1 clears
+rw0c | Read, Write with 0 clears
+wo | Write Only
+
+
+Register fields are tagged using the hwaccess key to describe the
+permitted access from hardware logic and side-effects. This key must
+have one of these values:
+
+
+Key | Description
+--- | -----------
+hro | Read Only
+hrw | Read/Write
+hwo | Write Only
+none | No Access Needed
+
+
+The top level of the JSON is a group containing the following keys:
+
+Key | Kind | Type | Description of Value
+--- | ---- | ---- | --------------------
+name | required | string | name of the component
+clocking | required | list | clocking for the device
+bus_interfaces | required | list | bus interfaces for the device
+registers | required | list | list of register definition groups and offset control groups
+one_line_desc | optional | string | one-line description of the component
+one_paragraph_desc | optional | string | one-paragraph description of the component
+revisions | optional | list | list with revision records
+design_spec | optional | string | path to the design specification, relative to repo root
+dv_doc | optional | string | path to the DV document, relative to repo root
+hw_checklist | optional | string | path to the hw_checklist, relative to repo root
+sw_checklist | optional | string | path to the sw_checklist, relative to repo root
+design_stage | optional | string | design stage of module
+dif_stage | optional | string | DIF stage of module
+verification_stage | optional | string | verification stage of module
+notes | optional | string | random notes
+version | optional | string | module version
+life_stage | optional | string | life stage of module
+commit_id | optional | string | commit ID of last stage sign-off
+alert_list | optional | name list+ | list of peripheral alerts
+available_inout_list | optional | name list+ | list of available peripheral inouts
+available_input_list | optional | name list+ | list of available peripheral inputs
+available_output_list | optional | name list+ | list of available peripheral outputs
+expose_reg_if | optional | python Bool | if set, expose reg interface in reg2hw signal
+interrupt_list | optional | name list+ | list of peripheral interrupts
+inter_signal_list | optional | list | list of inter-module signals
+no_auto_alert_regs | optional | string | 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 | optional | string | Set to true to suppress automatic generation of interrupt registers. Defaults to true if no interrupt_list is present. Otherwise this defaults to false.
+param_list | optional | parameter list | list of parameters of the IP
+regwidth | optional | int | width of registers in bits (default 32)
+reset_request_list | optional | list | list of signals requesting reset
+scan | optional | python Bool | Indicates the module have `scanmode_i`
+scan_reset | optional | python Bool | Indicates the module have `scan_rst_ni`
+scan_en | optional | python Bool | Indicates the module has `scan_en_i`
+SPDX-License-Identifier | optional | string | 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 | optional | name list+ | list of peripheral wakeups
+countermeasures | optional | name list | list of countermeasures in this block
+
+The basic structure of a register definition file is thus:
+
+```hjson
+{
+  name: "GP",
+  regwidth: "32",
+  registers: [
+    // register definitions...
+  ]
+}
+
+```
+
+
+
+The list of registers includes register definition groups containing the following keys:
+
+Key | Kind | Type | Description of Value
+--- | ---- | ---- | --------------------
+name | required | string | name of the register
+desc | required | text | description of the register. This field supports the markdown syntax.
+fields | required | list | list of register field description groups
+alias_target | optional | string | name of the register to apply the alias definition to.
+async | optional | string | indicates the register must cross to a different clock domain before use.  The value shown here should correspond to one of the module's clocks.
+sync | optional | string | indicates the register needs to be on another clock/reset domain.The value shown here should correspond to one of the module's clocks.
+swaccess | optional | string | software access permission to use for fields that don't specify swaccess
+hwaccess | optional | string | hardware access permission to use for fields that don't specify hwaccess
+hwext | optional | string | 'true' if the register is stored outside of the register module
+hwqe | optional | string | 'true' if hardware uses 'q' enable signal, which is latched signal of software write pulse.
+hwre | optional | string | 'true' if hardware uses 're' signal, which is latched signal of software read pulse.
+regwen | optional | string | if register is write-protected by another register, that register name should be given here. empty-string for no register write protection
+resval | optional | int | reset value of full register (default 0)
+tags | optional | string | tags for the register, following the format 'tag_name:item1:item2...'
+shadowed | optional | string | 'true' if the register is shadowed
+update_err_alert | optional | string | alert that will be triggered if this shadowed register has update error
+storage_err_alert | optional | string | alert that will be triggered if this shadowed register has storage error
+
+
+The basic register definition group will follow this pattern:
+
+```hjson
+    { name: "REGA",
+      desc: "Description of register",
+      swaccess: "rw",
+      resval: "42",
+      fields: [
+        // bit field definitions...
+      ]
+    }
+```
+
+The name and brief description are required. If the swaccess key is
+provided it describes the access pattern that will be used by all
+bitfields in the register that do not override with their own swaccess
+key. This is a useful shortcut because in most cases a register will
+have the same access restrictions for all fields. The reset value of
+the register may also be provided here or in the individual fields. If
+it is provided in both places then they must match, if it is provided
+in neither place then the reset value defaults to zero for all except
+write-only fields when it defaults to x.
+
+
+
+In the fields list each field definition is a group itself containing the following keys:
+
+Key | Kind | Type | Description of Value
+--- | ---- | ---- | --------------------
+bits | required | bitrange | bit or bit range (msb:lsb)
+name | optional | string | name of the field
+desc | optional | text | description of field (required if the field has a name). This field supports the markdown syntax.
+alias_target | optional | string | name of the field to apply the alias definition to.
+swaccess | optional | string | software access permission, copied from register if not provided in field. (Tool adds if not provided.)
+hwaccess | optional | string | hardware access permission, copied from register if not provided in field. (Tool adds if not provided.)
+hwqe | optional | bitrange | 'true' if hardware uses 'q' enable signal, which is latched signal of software write pulse. Copied from register if not provided in field. (Tool adds if not provided.)
+resval | optional | xint | reset value, comes from register resval if not provided in field. Zero if neither are provided and the field is readable, x if neither are provided and the field is wo. Must match if both are provided.
+enum | optional | list | list of permitted enumeration groups
+tags | optional | string | tags for the field, followed by the format 'tag_name:item1:item2...'
+mubi | optional | bitrange | boolean flag for whether the field is a multi-bit type
+auto_split | optional | bitrange | boolean flag which determines whether the field should be automatically separated into 1-bit sub-fields.This flag is used as a hint for automatically generated software headers with register description.
+
+
+Field names should be relatively short because they will be used
+frequently (and need to fit in the register layout picture!) The field
+description is expected to be longer and will most likely make use of
+the Hjson ability to include multi-line strings. An example with three
+fields:
+
+```hjson
+    fields: [
+      { bits: "15:0",
+        name: "RXS",
+        desc: '''
+        Last 16 oversampled values of RX. These are captured at 16x the baud
+        rate clock. This is a shift register with the most recent bit in
+        bit 0 and the oldest in bit 15. Only valid when ENRXS is set.
+        '''
+      }
+      { bits: "16",
+        name: "ENRXS",
+        desc: '''
+          If this bit is set the receive oversampled data is collected
+          in the RXS field.
+        '''
+      }
+      {bits: "20:19", name: "TXILVL",
+       desc: "Trigger level for TX interrupts",
+       resval: "2",
+       enum: [
+               { value: "0", name: "txlvl1", desc: "1 character" },
+               { value: "1", name: "txlvl4", desc: "4 characters" },
+               { value: "2", name: "txlvl8", desc: "8 characters" },
+               { value: "3", name: "txlvl16", desc: "16 characters" }
+             ]
+      }
+    ]
+```
+
+In all of these the swaccess parameter is inherited from the register
+level, and will be added so this key is always available to the
+backend. The RXS and ENRXS will default to zero reset value (unless
+something different is provided for the register) and will have the
+key added, but TXILVL expicitly sets its reset value as 2.
+
+The missing bits 17 and 18 will be treated as reserved by the tool, as
+will any bits between 21 and the maximum in the register.
+
+The TXILVL is an example using an enumeration to specify all valid
+values for the field. In this case all possible values are described,
+if the list is incomplete then the field is marked with the rsvdenum
+key so the backend can take appropriate action. (If the enum field is
+more than 7 bits then the checking is not done.)
+
+
+
+Definitions in an enumeration group contain:
+
+Key | Kind | Type | Description of Value
+--- | ---- | ---- | --------------------
+name | required | string | name of the member of the enum
+desc | required | text | description when field has this value
+value | required | int | value of this member of the enum
+
+
+The list of registers may include single entry groups to control the offset, open a window or generate registers:
+
+Key | Kind | Type | Description of Value
+--- | ---- | ---- | --------------------
+reserved | optional | int | number of registers to reserve space for
+skipto | optional | int | set next register offset to value
+window | optional | group | group defining an address range for something other than standard registers
+multireg | optional | group | group defining registers generated from a base instance.
+
+
+
+
+Registers can protect themselves from software writes by using the
+register attribute regwen. When not an emptry string (the default
+value), regwen indicates that another register must be true in order
+to allow writes to this register.  This is useful for the prevention
+of software modification.  The register-enable register (call it
+REGWEN) must be one bit in width, and should default to 1 and be rw1c
+for preferred security control.  This allows all writes to proceed
+until at some point software disables future modifications by clearing
+REGWEN. An error is reported if REGWEN does not exist, contains more
+than one bit, is not `rw1c` or does not default to 1. One REGWEN can
+protect multiple registers. The REGWEN register must precede those
+registers that refer to it in the .hjson register list. An example:
+
+```hjson
+    { name: "REGWEN",
+      desc: "Register write enable for a bank of registers",
+      swaccess: "rw1c",
+      fields: [ { bits: "0", resval: "1" } ]
+    }
+    { name: "REGA",
+      swaccess: "rw",
+      regwen: "REGWEN",
+      ...
+    }
+    { name: "REGB",
+      swaccess: "rw",
+      regwen: "REGWEN",
+      ...
+    }
+```
+
+
+A window defines an open region of the register space that can be used
+for things that are not registers (for example access to a buffer ram).
+
+
+Key | Kind | Type | Description of Value
+--- | ---- | ---- | --------------------
+name | required | string | name of the window
+desc | required | text | description of the window
+items | required | int | size in fieldaccess width words of the window
+swaccess | required | string | software access permitted
+data-intg-passthru | optional | string | True if the window has data integrity pass through. Defaults to false if not present.
+byte-write | optional | string | True if byte writes are supported. Defaults to false if not present.
+validbits | optional | int | Number of valid data bits within regwidth sized word. Defaults to regwidth. If smaller than the regwidth then in each word of the window bits [regwidth-1:validbits] are unused and bits [validbits-1:0] are valid.
+unusual | optional | string | True if window has unusual parameters (set to prevent Unusual: errors).Defaults to false if not present.
+
+
+The multireg expands on the register required fields and will generate
+a list of the generated registers (that contain all required and
+generated keys for an actual register).
+
+
+Key | Kind | Type | Description of Value
+--- | ---- | ---- | --------------------
+name | required | string | base name of the registers
+desc | required | text | description of the registers
+count | required | string | number of instances to generate. This field can be integer or string matching from param_list.
+cname | required | string | base name for each instance, mostly useful for referring to instance in messages.
+fields | required | list | list of register field description groups. Describes bit positions used for base instance.
+alias_target | optional | string | name of the register to apply the alias definition to.
+async | optional | string | indicates the register must cross to a different clock domain before use.  The value shown here should correspond to one of the module's clocks.
+sync | optional | string | indicates the register needs to be on another clock/reset domain.The value shown here should correspond to one of the module's clocks.
+swaccess | optional | string | software access permission to use for fields that don't specify swaccess
+hwaccess | optional | string | hardware access permission to use for fields that don't specify hwaccess
+hwext | optional | string | 'true' if the register is stored outside of the register module
+hwqe | optional | string | 'true' if hardware uses 'q' enable signal, which is latched signal of software write pulse.
+hwre | optional | string | 'true' if hardware uses 're' signal, which is latched signal of software read pulse.
+regwen | optional | string | if register is write-protected by another register, that register name should be given here. empty-string for no register write protection
+resval | optional | int | reset value of full register (default 0)
+tags | optional | string | tags for the register, following the format 'tag_name:item1:item2...'
+shadowed | optional | string | 'true' if the register is shadowed
+update_err_alert | optional | string | alert that will be triggered if this shadowed register has update error
+storage_err_alert | optional | string | alert that will be triggered if this shadowed register has storage error
+regwen_multi | optional | python Bool | If true, regwen term increments along with current multireg count.
+compact | optional | python Bool | If true, allow multireg compacting.If false, do not compact.
+cdc | optional | string | indicates the register must cross to a different clock domain before use.  The value shown here should correspond to one of the module's clocks.
+
+
+(end of output generated by `regtool.py --doc`)
+
+
+<!-- END AUTOGEN -->
 
 The tool will normally generate the register address offset by starting from 0 and allocating the registers in the order they are in the input file.
 Between each register the offset is incremented by the number of bytes in the `regwidth` (4 bytes for the default 32-bit `regwidth`), so the registers end up packed into the smallest space.
diff --git a/util/selfdoc.py b/util/selfdoc.py
new file mode 100755
index 0000000..d67ec3a
--- /dev/null
+++ b/util/selfdoc.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+
+import sys
+import reggen.gen_selfdoc as reggen_selfdoc
+import tlgen
+from typing import TextIO
+
+
+def generate_selfdocs(tool: str, fout: TextIO):
+    """Generate documents for the tools in `util/`
+
+    Each tool creates selfdoc differently. Manually invoked.
+    """
+    if tool == "reggen":
+        reggen_selfdoc.document(fout)
+    elif tool == "tlgen":
+        fout.write(tlgen.selfdoc(heading=3, cmd='tlgen.py --doc'))
+    else:
+        sys.exit(f"unknown tool \"{tool}\"")
+
+
+if __name__ == "__main__":
+    if len(sys.argv) != 2:
+        sys.exit("usage: selfdoc <tool>")
+    # running this file as standalone prints the documentation
+    generate_selfdocs(sys.argv[1], sys.stdout)
diff --git a/util/tlgen/README.md b/util/tlgen/README.md
index c050275..e52cbe3 100644
--- a/util/tlgen/README.md
+++ b/util/tlgen/README.md
@@ -53,7 +53,88 @@
 *Optional* keys may be provided in the input files.
 The tool also may insert the optional keys with default value.
 
-{{% selfdoc "tlgen" %}}
+<!-- BEGIN AUTOGEN python3 util/selfdoc.py tlgen -->
+
+
+The tables describe each key and the type of the value. The following
+types are used:
+
+Type | Description
+---- | -----------
+int | integer (binary 0b, octal 0o, decimal, hex 0x)
+xint | x for undefined otherwise int
+bitrange | bit number as decimal integer, or bit-range as decimal integers msb:lsb
+list | comma separated list enclosed in `[]`
+name list | comma separated list enclosed in `[]` of one or more groups that have just name and dscr keys. e.g. `{ name: "name", desc: "description"}`
+name list+ | name list that optionally contains a width
+parameter list | parameter list having default value optionally
+group | comma separated group of key:value enclosed in `{}`
+list of group | comma separated group of key:value enclosed in `{}` the second entry of the list is the sub group format
+string | string, typically short
+text | string, may be multi-line enclosed in `'''` may use `**bold**`, `*italic*` or `!!Reg` markup
+tuple | tuple enclosed in ()
+python int | Native Python type int (generated)
+python Bool | Native Python type Bool (generated)
+python list | Native Python type list (generated)
+python enum | Native Python type enum (generated)
+### Top configuration
+
+
+Crossbar configuration format.
+
+
+
+Field | Kind | Type | Description
+----- | ---- | ---- | ------------
+name | required | string | Name of the crossbar
+clock | required | string | Main clock. Internal components use this clock. If not specified, it is assumed to be in main clock domain
+reset | required | string | Main reset
+connections | required | group | List of edge. Key is host, entry in value list is device
+clock_connections | required | group | list of clocks
+nodes | required | list of group | List of nodes group
+type | optional | string | Indicate Hjson type. "xbar" always if exist
+clock_group | optional | string | Remnant from auto-generation scripts. Ignore.
+clock_srcs | optional | group | Remnant from auto-generation scripts. Ignore.
+domain | optional | string | Power domain for the crossbar
+reset_connections | added by tool | group | Generated by topgen. Key is the reset signal inside IP and value is the top reset signal
+
+
+### Node configuration
+
+
+Crossbar node description. It can be host, device, or internal nodes.
+
+
+
+Field | Kind | Type | Description
+----- | ---- | ---- | ------------
+name | required | string | Module instance name
+stub | required | python Bool | Real node or stub.  Stubs only occupy address ranges
+type | required | string | Module type: {"host", "device", "async", "socket_1n", "socket_m1"}
+clock | optional | string | main clock of the port
+reset | optional | string | main reset of the port
+pipeline | optional | python Bool | If true, pipeline is added in front of the port
+req_fifo_pass | optional | python Bool | If true, pipeline fifo has passthrough behavior on req
+rsp_fifo_pass | optional | python Bool | If true, pipeline fifo has passthrough behavior on rsp
+inst_type | optional | string | Instance type
+xbar | optional | python Bool | If true, the node is connected to another Xbar
+addr_range | optional | list of group | List of addr_range group
+
+
+### Address configuration
+
+Device Node address configuration. It contains the base address and the size in bytes.
+
+
+
+Field | Kind | Type | Description
+----- | ---- | ---- | ------------
+base_addr | required | int | Base address of the device. It is required for the device
+size_byte | required | int | Memory space of the device. It is required for the device
+
+
+
+<!-- END AUTOGEN -->
 
 ## Fabrication process