[doc] create top level HW 'specboard'
diff --git a/hw/index.md b/hw/index.md
index 4c76904..da087c0 100644
--- a/hw/index.md
+++ b/hw/index.md
@@ -1,13 +1,13 @@
 # Hardware Specifications
 
-## Top Earlgrey
+## Available Top Earlgrey Specifications
 
 {{% doctree top_earlgrey }}
 
-## Ibex Core
+## Available Ibex Core Specifications
 
 {{% doctree vendor/lowrisc_ibex }}
 
-## Comportable IP Blocks
+## Available Comportable IP Block Specifications
 
-{{% doctree ip }}
\ No newline at end of file
+{{% specboard ip }}
diff --git a/util/dashboard/gen_dashboard_entry.py b/util/dashboard/gen_dashboard_entry.py
index b6261cd..69b5c3b 100644
--- a/util/dashboard/gen_dashboard_entry.py
+++ b/util/dashboard/gen_dashboard_entry.py
@@ -7,8 +7,10 @@
 
 import hjson
 import html
+import re
 import dashboard.dashboard_validate as dashboard_validate
 import logging as log
+import os.path
 
 
 def genout(outfile, msg):
@@ -34,7 +36,8 @@
     return STAGE_STRINGS.get(stagestr, "UNKNOWN")
 
 
-def gen_html(hjson_path, outfile):
+# Create dashboard of hardware IP development status
+def gen_dashboard_html(hjson_path, outfile):
     with hjson_path:
         prjfile = open(hjson_path)
         try:
@@ -85,3 +88,53 @@
     genout(outfile, "      </tr>\n")
     # yapf: enable
     return
+
+
+# Create table of hardware specifications
+def gen_specboard_html(hjson_path, rel_hjson_path, outfile):
+    with hjson_path:
+        prjfile = open(hjson_path)
+        try:
+            obj = hjson.load(prjfile)
+        except ValueError:
+            raise SystemExit(sys.exc_info()[1])
+    if dashboard_validate.validate(obj) == 0:
+        log.info("Generated dashboard object for " + str(hjson_path))
+    else:
+        log.fail("hjson file import failed")
+
+    # create design spec and DV plan references, check for existence below
+    design_spec_md = re.sub(r'/data/', '/doc/',
+                            re.sub(r'\.prj\.hjson', '.md', str(hjson_path)))
+    dv_plan_md = re.sub(
+        r'/data/', '/doc/',
+        re.sub(r'\.prj\.hjson', '_dv_plan.md', str(hjson_path)))
+    design_spec_html = re.sub(r'/data/', '/doc/',
+        re.sub(r'\.prj\.hjson', '.html', str(rel_hjson_path)))
+    dv_plan_html = re.sub(
+        r'/data/', '/doc/',
+        re.sub(r'\.prj\.hjson', '_dv_plan.html', str(rel_hjson_path)))
+
+    # yapf: disable
+    genout(outfile, "      <tr>\n")
+    genout(outfile, "        <td class=\"fixleft\">" +
+                    html.escape(obj['name']) + "</td>\n")
+    if os.path.exists(design_spec_md):
+        genout(outfile,
+                    "        <td class=\"fixleft\"><a href=\"" +
+                    html.escape(design_spec_html) + "\">" +
+                    "design spec</a>\n")
+    else:
+        genout(outfile,
+                    "        <td>&nbsp;</td>\n")
+    if os.path.exists(dv_plan_md):
+        genout(outfile,
+                    "        <td class=\"fixleft\"><a href=\"" +
+                    html.escape(dv_plan_html) + "\">" +
+                    "DV plan</a>\n")
+    else:
+        genout(outfile,
+                    "        <td>&nbsp;</td>\n")
+    genout(outfile, "      </tr>\n")
+    # yapf: enable
+    return
diff --git a/util/docgen/html_data.py b/util/docgen/html_data.py
index 1e04029..87bcd0c 100644
--- a/util/docgen/html_data.py
+++ b/util/docgen/html_data.py
@@ -105,3 +105,19 @@
     </tbody>
   </table>
 """
+
+specboard_header = """
+  <table class="hw-project-dashboard">
+    <thead>
+      <tr>
+        <th>Module</th>
+        <th>Design Spec</th>
+        <th>DV Plan</th>
+      </tr>
+    </thead>
+    <tbody>
+"""
+specboard_trailer = """
+    </tbody>
+  </table>
+"""
diff --git a/util/docgen/lowrisc_renderer.py b/util/docgen/lowrisc_renderer.py
index 7b110e6..0375a80 100644
--- a/util/docgen/lowrisc_renderer.py
+++ b/util/docgen/lowrisc_renderer.py
@@ -377,11 +377,27 @@
             outbuf = io.StringIO()
             outbuf.write(html_data.dashboard_header)
             for hjson_path in hjson_paths:
-                gen_dashboard_entry.gen_html(hjson_path, outbuf)
+                gen_dashboard_entry.gen_dashboard_html(hjson_path, outbuf)
             outbuf.write(html_data.dashboard_trailer)
             generated = outbuf.getvalue()
             outbuf.close()
             return generated
+        if token.type == "specboard":
+            hjson_paths = []
+            # find all of the .prj.hjson files in the given path
+            hjson_paths.extend(
+                sorted(
+                    Path(path.join(self.basedir,
+                                   token.text)).rglob('*.prj.hjson')))
+            outbuf = io.StringIO()
+            outbuf.write(html_data.specboard_header)
+            for hjson_path in hjson_paths:
+                gen_dashboard_entry.gen_specboard_html(hjson_path,
+                    hjson_path.relative_to(self.basedir), outbuf)
+            outbuf.write(html_data.specboard_trailer)
+            generated = outbuf.getvalue()
+            outbuf.close()
+            return generated
 
         bad_tag = '{{% ' + token.type + ' ' + token.text + ' }}'
         log.warn("Unknown lowRISC tag " + bad_tag)