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 and regfile 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 the type attribute is set correctly.

  • In SystemRDL, addrmap components can contain additional addrmap/regfile children, or reg components. When iterating over children(), use isinstance 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"
                }
            ]
        }
    ]
}