Building a JSON exporter
This example walks through how to export a compiled SystemRDL register model into a simple JSON data object.
The full example code can be found in the systemrdl-compiler
repository at:
examples/export_json.py
Defining a schema
Before we start, we need to define a schema for our custom JSON output. Since there isn’t really a “standard” JSON schema available, we’ll make up our own simple one. For the sake of this example, it will be very limited:
Each SystemRDL component type will be represented as its own JSON object.
The type of the object is represented by a “type” string.
addrmap
andregfile
components will be treated interchangeably.Arrays will not be supported.
Only the field’s “reset” and “sw” properties are encoded. All other properties will be ignored.
Each object’s key/value mappings will be as follows:
- field
type: "field", inst_name: <string>, lsb: <integer>, msb: <integer>, reset: <integer> sw_access: <string>
- reg
type: "reg", inst_name: <string>, addr_offset: <integer>, children: <array of field objects>
- addrmap or regfile
type: "addrmap" or "regfile", inst_name: <string>, addr_offset: <integer>, children: <array of any object>
A note on register model traversal
In the previous example, we used the
RDLWalker
& RDLListener
.
This let us automatically traverse the design, and trigger callbacks. This is an
easy way to traverse the design, but only in situations where keeping track of
the register model’s hierarchical context is not needed.
For a JSON exporter we want to convert each node in the hierarchy and keep
track of parent/child relationships. Doing so with the walker/listener method
would be cumbersome, so instead we will explicitly visit child nodes using the
Node.children()
method.
Walkthrough
Python has an excellent JSON serializer in its standard library. This means that all we need to do is distill the information from the register model into primitive datatypes that convert well to JSON (python dictionaries & lists). [1]
Per-component conversion functions
Each component type will have a function that converts the corresponding
Node
object into a Python dictionary.
The function to convert a FieldNode
is pretty straightforward:
def convert_field(rdlc: RDLCompiler, obj: node.FieldNode) -> dict:
json_obj = dict()
json_obj['type'] = 'field'
json_obj['inst_name'] = obj.inst_name
json_obj['lsb'] = obj.lsb
json_obj['msb'] = obj.msb
json_obj['reset'] = obj.get_property('reset')
json_obj['sw_access'] = obj.get_property('sw').name
return json_obj
Next, we write the function to convert a RegNode
. Remember
how the schema we defined doesn’t support arrays? This is a good time to check
if the register instance is an array, and throw an error. We will use the
compiler’s message handler to emit a message to the user, as well as a reference
to the offending location in the RDL source file:
def convert_reg(rdlc: RDLCompiler, obj: node.RegNode) -> dict:
if obj.is_array:
# Use the RDL Compiler message system to print an error
# fatal() raises RDLCompileError
rdlc.msg.fatal(
"JSON export does not support arrays",
obj.inst.inst_src_ref
)
After validating the register is not an array, we can continue and distill the
RegNode
into a Python dictionary. Note how this calls
the fields()
method to fetch all fields of this
register.
# Convert information about the register
json_obj = dict()
json_obj['type'] = 'reg'
json_obj['inst_name'] = obj.inst_name
json_obj['addr_offset'] = obj.address_offset
# Iterate over all the fields in this reg and convert them
json_obj['children'] = []
for field in obj.fields():
json_field = convert_field(rdlc, field)
json_obj['children'].append(json_field)
return json_obj
Next, we create a common function to convert both AddrmapNode
and RegfileNode
objects.
Since this function is reused for both node types, use
isinstance
to make sure thetype
attribute is set correctly.In SystemRDL,
addrmap
components can contain additionaladdrmap
/regfile
children, orreg
components. When iterating overchildren()
, useisinstance
again to call the appropriate conversion function.
def convert_addrmap_or_regfile(rdlc: RDLCompiler, obj: Union[node.AddrmapNode, node.RegfileNode]) -> dict:
if obj.is_array:
rdlc.msg.fatal(
"JSON export does not support arrays",
obj.inst.inst_src_ref
)
json_obj = dict()
if isinstance(obj, node.AddrmapNode):
json_obj['type'] = 'addrmap'
elif isinstance(obj, node.RegfileNode):
json_obj['type'] = 'regfile'
else:
raise RuntimeError
json_obj['inst_name'] = obj.inst_name
json_obj['addr_offset'] = obj.address_offset
json_obj['children'] = []
for child in obj.children():
if isinstance(child, (node.AddrmapNode, node.RegfileNode)):
json_child = convert_addrmap_or_regfile(rdlc, child)
elif isinstance(child, node.RegNode):
json_child = convert_reg(rdlc, child)
json_obj['children'].append(json_child)
return json_obj
Dumping to JSON
Finally, we need a function that starts the conversion process at the top-level, and then serializes the resulting tree of Python dictionaries/lists into proper JSON.
def convert_to_json(rdlc: RDLCompiler, obj: node.RootNode, path: str):
# Convert entire register model to primitive datatypes (a dict/list tree)
json_obj = convert_addrmap_or_regfile(rdlc, obj.top)
# Write to a JSON file
with open(path, "w", encoding='utf-8') as f:
json.dump(json_obj, f, indent=4)
Bringing it all together
Now that we have all our utility functions defined, we can put it all together.
First, compile and elaborate input files provided from the command line, as was done in the previous example:
import sys
# Compile and elaborate files provided from the command line
input_files = sys.argv[1:]
rdlc = RDLCompiler()
try:
for input_file in input_files:
rdlc.compile_file(input_file)
root = rdlc.elaborate()
except RDLCompileError:
sys.exit(1)
Finally, call the top-level conversion function which writes out the JSON file:
# Dump the register model to a JSON file
convert_to_json(rdlc, root, "out.json")
Output
Given the input file tiny.rdl
:
addrmap tiny {
reg {
field {
sw=rw;
hw=r;
} f1[8] = 123;
field {
sw=r;
hw=w;
} f2[8];
}r1;
};
converting to JSON produces the following:
{
"type": "addrmap",
"inst_name": "tiny",
"addr_offset": 0,
"children": [
{
"type": "reg",
"inst_name": "r1",
"addr_offset": 0,
"children": [
{
"type": "field",
"inst_name": "f1",
"lsb": 0,
"msb": 7,
"reset": 123,
"sw_access": "rw"
},
{
"type": "field",
"inst_name": "f2",
"lsb": 8,
"msb": 15,
"reset": null,
"sw_access": "r"
}
]
}
]
}