[doc/rm] Crossbar Tool
Add `tlgen.py`, more generally `util/tlgen`, document to help
understanding how the crossbar is being generated in OpenTitan and its
usage.
To generate tlgen selfdoc, validate has checker logic inside. The
checking structure (control group) is slightly changed from its in
reggen. The change makes validate to run recursively and generate the
doc recursively.
This document addresses the issue #340.
diff --git a/util/tlgen/__init__.py b/util/tlgen/__init__.py
index 7534400..50dfac7 100644
--- a/util/tlgen/__init__.py
+++ b/util/tlgen/__init__.py
@@ -7,3 +7,4 @@
from .item import Edge, Node, NodeType
from .validate import validate
from .xbar import Xbar
+from .doc import selfdoc
diff --git a/util/tlgen/doc.py b/util/tlgen/doc.py
new file mode 100644
index 0000000..1c0bf83
--- /dev/null
+++ b/util/tlgen/doc.py
@@ -0,0 +1,94 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+"""TileLink-Uncached Lightweight Xbar self document
+"""
+import logging as log
+
+from .validate import *
+
+doc_intro = """
+
+(start of output generated by `{}`)
+
+The tables describe each key and the type of the value. The following
+types are used:
+
+Type | Description
+---- | -----------
+"""
+
+doc_tail = """
+
+(end of output generated by `{}`)
+
+"""
+
+
+def print_control(control, heading):
+ """Print a control group and its subgroup recursively
+ """
+ subgroup = [] # added if the field hit sub control group
+
+ outstr = '#' * heading + ' ' + control['name'] + '\n'
+ outstr += '\n'
+
+ outstr += control['description']
+ outstr += '\n\n'
+
+ items = {**control['required'], **control['optional'], **control['added']}
+
+ if len(items) > 0:
+ outstr += """
+Field | Kind | Type | Description
+----- | ---- | ---- | ------------
+"""
+ for k, v in items.items():
+ if k in control['required']:
+ kind = "required"
+ elif k in control['optional']:
+ kind = "optional"
+ else:
+ kind = "added by tool"
+
+ v_type = val_types[v[0]][0]
+
+ if v[0] == 'lg':
+ subgroup.append(v[1])
+ log.error(val_types[v[0]])
+ outstr += '{} | {} | {} | List of {} group\n'.format(
+ k, kind, v_type, k)
+ continue
+ elif v[0] == 'g':
+ if not isinstance(v[1], str):
+ subgroup.append(v[1])
+ outstr += '{} | {} | {} | {} group\n'.format(
+ k, kind, v_type, k)
+ continue
+
+ # Generic string print
+ outstr += '{} | {} | {} | {}\n'.format(k, kind, v_type, v[1])
+
+ outstr += "\n\n"
+ # recursive subgroup
+ for e in subgroup:
+ outstr += print_control(e, heading)
+
+ return outstr
+
+
+def selfdoc(heading, cmd=""):
+ # heading : markdown header depth
+ # value type
+ outstr = doc_intro.format(cmd)
+
+ for k, v in val_types.items():
+ outstr += v[0] + " | " + v[1] + "\n"
+
+ # root + subgroup
+ outstr += print_control(root, heading)
+
+ # connections: Needs custom as the key are hosts (can vary)
+ outstr += doc_tail.format(cmd)
+
+ return outstr
diff --git a/util/tlgen/validate.py b/util/tlgen/validate.py
index e573a67..ce232f4 100644
--- a/util/tlgen/validate.py
+++ b/util/tlgen/validate.py
@@ -3,10 +3,154 @@
# SPDX-License-Identifier: Apache-2.0
import logging as log
from collections import OrderedDict
+from functools import partial
from .item import Edge, Node, NodeType
from .xbar import Xbar
+from reggen.validate import val_types, check_int, check_bool, check_ln
+
+# val_types = {
+# 'd': ["int", "integer (binary 0b, octal 0o, decimal, hex 0x)"],
+# 'x': ["xint", "x for undefined otherwise int"],
+# 'b': [
+# "bitrange", "bit number as decimal integer, \
+# or bit-range as decimal integers msb:lsb"
+# ],
+# 'l': ["list", "comma separated list enclosed in `[]`"],
+# 'ln': ["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"}`'],
+# 'lnw': ["name list+", 'name list that optionally contains a width'],
+# 'lp': ["parameter list", 'parameter list having default value optionally'],
+# 'g': ["group", "comma separated group of key:value enclosed in `{}`"],
+# 'lg': ["list of group", "comma separated group of key:value enclosed in `{}`"\
+# " the second entry of the list is the sub group format"],
+# 's': ["string", "string, typically short"],
+# 't': ["text", "string, may be multi-line enclosed in `'''` "\
+# "may use `**bold**`, `*italic*` or `!!Reg` markup"],
+# 'T': ["tuple", "tuple enclosed in ()"],
+# 'pi': ["python int", "Native python type int (generated)"],
+# 'pb': ["python Bool", "Native python type Bool (generated)"],
+# 'pl': ["python list", "Native python type list (generated)"],
+# 'pe': ["python enum", "Native python type enum (generated)"]
+# }
+node = {
+ 'name': 'Node configuration',
+ 'description': '''
+Crossbar node description. It can be host, device, or internal nodes.
+''',
+ 'required': {
+ 'name': ['s', 'Module instance name'],
+ 'type': ['s', 'Module type: {"host", "device", "async", "socket_1n", "socket_m1"}'],
+ },
+ 'optional': {
+ 'clock': ['s', 'main clock of the port'],
+ 'base_addr': ['d', 'Base address of the device'\
+ ' It is required for the device'],
+ 'size_byte': ['d', 'Memory space of the device'\
+ ' It is required for the device'],
+ 'pipeline': ['pb', 'If true, pipeline is added in front of the port'],
+ 'pipeline_byp': ['pb', 'Pipeline bypass. If true, '\
+ 'request/response are not latched']
+ },
+ 'added': {}
+}
+root = {
+ 'name': 'Top configuration',
+ 'description': '''
+Crossbar configuration format.
+''',
+ 'required': {
+ 'name': ['s', 'Name of the crossbar'],
+ 'clock': ['s', 'Main clock. Internal components use this clock.'\
+ ' If not specified, it is assumed to be in main clock domain'],
+ 'connections':
+ ['g', "List of edge. Key is host, entry in value list is device"],
+ 'nodes': ['lg', node]
+ },
+ 'optional': {
+ 'type': ['s', 'Indicate hjson type. "xbar" always if exist']
+ },
+ 'added': {
+ 'reset_connections': ['g', "Generated by topgen. Key is the reset signal inside IP"\
+ " and value is the top reset signal"],
+ }
+}
+
+
+def check_keys(obj, control, prefix=""):
+ """ Check the keys recursively.
+
+ The control parameter is a control group to check obj data structure.
+ """
+ error = 0
+
+ # required
+ for k, v in control["required"].items():
+ if not k in obj:
+ error += 1
+ log.error(prefix + " missing required key " + k)
+
+ # Check every fields' correctness
+ for k, v in obj.items():
+ checker = ['', '']
+ prefix_name = prefix + " " + k
+
+ if k in control["required"]:
+ checker = control["required"][k]
+ elif k in control["optional"]:
+ checker = control["optional"][k]
+ elif k in control["added"]:
+ log.warning(prefix + " contains generated key " + k)
+ checker = control["added"][k]
+ else:
+ log.warning(prefix + " contains extra key " + k)
+ continue
+
+ # Type and value check
+ if not checker[0] in val_types:
+ log.error(prefix +
+ " field {} is undefined type. Check val_types {}".format(
+ k, checker[0]))
+ if checker[0] == 'lg':
+ # List of subgroup
+ error += sum(
+ map(
+ partial(check_keys, control=checker[1],
+ prefix=prefix_name), obj[k]))
+ elif checker[0] == 'g':
+ # if second entry isn't string type, call recursively
+ if isinstance(checker[1], str):
+ log.info(
+ "Skipping {} as no further control group is given".format(
+ prefix_name))
+ continue
+
+ error += check_keys(obj=obj[k],
+ control=checker[1],
+ prefix=prefix_name)
+
+ elif checker[0] == 'd':
+ int_v, err = check_int(obj[k], prefix_name)
+ if err:
+ error += 1
+
+ elif checker[0] == 's' or checker[0] == 't':
+ # don't care the string
+ pass
+
+ elif checker[0] == 'pb':
+ b_v, err = check_bool(obj[k], prefix_name)
+ if err:
+ error += 1
+
+ else:
+ log.error(prefix_name +
+ " is not supported in this configuration format")
+
+ return error
+
def get_nodetype(t): # t: str -> NodeType
if t == "host":
@@ -44,6 +188,11 @@
addr_ranges = []
+ obj, err = validate_hjson(obj) # validate hjson format first
+ if err > 0:
+ log.error("Hjson structure error")
+ return
+
# Nodes
for nodeobj in obj["nodes"]:
clock = nodeobj["clock"].lower() if "clock" in nodeobj.keys(
@@ -78,9 +227,15 @@
node.pipeline = True if nodeobj["pipeline"].lower() in [
"true", "1"
] else False
+ else:
+ node.pipeline = False
+ if node.node_type in [NodeType.DEVICE, NodeType.HOST
+ ] and "pipeline_byp" in nodeobj:
node.pipeline_byp = True if nodeobj["pipeline_byp"].lower() in [
"true", "1"
] else False
+ else:
+ node.pipeline_byp = True
xbar.nodes.append(node)
# Edge
@@ -90,3 +245,19 @@
xbar.connect_nodes(host.lower(), device.lower())
return xbar
+
+
+def validate_hjson(obj):
+ if not "type" in obj:
+ obj["type"] = "xbar"
+ if not "name" in obj:
+ log.error("Component has no name. Aborting.")
+ return None, 1
+
+ component = obj["name"]
+ error = check_keys(obj, root, component)
+
+ if error > 0:
+ log.error("{} has top level error. Aborting".format(component))
+ return None, error
+ return obj, 0