Generated Package

Output Structure

PeakRDL Python generates a python Register Access Layer (RAL) from a System RDL design. The generated python code is contained in a package, structured as shown below. The root_name will be based on the top level address map name with the package was generated

<root_name>
├── lib
├── reg_model
│ └── <root_name>.py
└── tests
└── test_<root_name>.py

In the folder structure above:

  • <root_name>.py - This is the register access layer code for the design

  • lib - This is a package of base classes used by the register access layer

  • test_<root_name>.py - This is a set of autogenerated unittests to verify the register access layer

Running the Unit Tests

There are many ways to run Python Unit tests. A good place to start is the unittest module included in the Python standard installation.

Callbacks

The Register Access Layer will typically interfaced to a driver that allows accesses the chip. However, it can also be interfaced to a simulation of the device.

In order to operate the register access layer typically requires the following:

  • A callback for a single register write, this not required if there is no writable register in the register access layer

  • A callback for a single register read, this not required if there is no writable register in the register access layer

In addition the register access layer can make use of block operations where a block of the address space is read in a single transaction. Not all drivers support these

The examples of these two methods are included within the generated register access layer package so that it can be used from the console:

def read_addr_space(addr: int, width: int, accesswidth: int) -> int:
    """
    Callback to simulate the operation of the package, everytime the read is called, it will
    request the user input the value to be read back.

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimium access width of the register in bits

    Returns:
        value inputted by the used
    """
    assert isinstance(addr, int)
    assert isinstance(width, int)
    assert isinstance(accesswidth, int)
    return input('value to read from address:0x%X' % addr)

def write_addr_space(addr: int, width: int, accesswidth: int, data: int) -> None:
    """
    Callback to simulate the operation of the package, everytime the read is called, it will
    request the user input the value to be read back.

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits
        data: value to be written to the register

    Returns:
        None
    """
    assert isinstance(addr, int)
    assert isinstance(width, int)
    assert isinstance(accesswidth, int)
    assert isinstance(data, int)
    print('write data:0x%X to address:0x%X' % (data, addr))

In a real system these call backs will be connected to a driver.

In addition there is also an option to use async callbacks if the package is built asyncoutput set to True.

Callback Set

The callbacks are passed into the register access layer using either:

  • NormalCallbackSet for standard python function callbacks

  • AsyncCallbackSet for async python function callbacks, these are called from the library using await

Using the Register Access Layer

The register access layer package is intended to integrated into another piece of code. That code could be a simple test script for blinking an LED on a GPIO or it could be a more complex application with a GUI.

The following example is a chip that has a GPIO block. The GPIO block has two registers:

  1. one register that controls the direction of the GPIO pin, at address 0x4

  2. one register that controls driven state of the GPIO pin, at address 0x8

This can be described with the following systemRDL code:

addrmap mychip {

    name="My Chip";

    addrmap GPIO_block {

        name = "GPIO Block";
        desc = "GPIO Block with configurable direction pins";

        enum GPIO_direction {
		    dir_in = 0 { name = "input"; desc="GPIO direction into chip"; };
		    dir_out = 1 { name = "output"; desc="GPIO direction out of chip"; };
    	};

        field GPIO_direction_field_type {
            encode=GPIO_direction;
            fieldwidth = 1;
            sw=rw;
            hw=r;
            reset={GPIO_direction::dir_in}; };
        field GPIO_output_field_type {
            fieldwidth = 1;
            sw=rw;
            hw=r; };

        reg {
            name="GPIO Direction";
            desc="Register to set the direction of each GPIO pin";
            GPIO_direction_field_type PIN_0;
        } GPIO_dir @ 0x4;

        reg {
            name="GPIO Set State";
            desc="Register to set the state of a GPIO Pin";
            GPIO_output_field_type PIN_0;
        } GPIO_state @ 0x8;
    } GPIO;
};

This systemRDL code can be built using the command line tool as follows (assuming it is stored in a file called chip_with_a_GPIO.rdl:

peakrdl python chip_with_a_GPIO.rdl -o python_output
python -m unittest discover -s python_output

Tip

It is always good practice to run the unittests on the generated code.

Once the register access layer has been generated and it can be used. The following example does not actually use a device driver. Instead it chip simulator with a a Tkinter GUI, incorporating a RED circle to represent the LED. The chip simulator has read and write methods ( equivalent to those offered by a device driver), these look at the address of the write and update the internal state of the simulator accordingly, the LED is then updated based on the state of the simulator.

import tkinter as tk

from mychip.reg_model.mychip import mychip_cls
from mychip.lib import NormalCallbackSet

class ChipSim:

    def __init__(self):

        # simulator state variables
        self.PIN_output = False
        self.PIN_state = False

        # basic GUI components
        self.root = tk.Tk()
        self.root.title("My Chip Simulator")
        self.LED_label = tk.Label(master=self.root,
                                  text="LED_0",
                                  foreground="black")  # Set the background color to black
        self.LED_label.pack(fill=tk.X, side=tk.TOP)
        window_frame = tk.Frame(master=self.root, width=400, height=400,bg="black")
        window_frame.pack(fill=tk.BOTH, side=tk.TOP)
        self.LED = tk.Canvas(master=window_frame, width=300, height=300, bg='black')
        self.LED.pack()
        self.LED_inner = self.LED.create_oval(25, 25, 275, 275, fill='black')

    def read_addr_space(self, addr: int, width: int, accesswidth: int) -> int:
        """
        Callback to for the simulation of the chip

        Args:
            addr: Address to write to
            width: Width of the register in bits
            accesswidth: Minimum access width of the register in bits

        Returns:
            simulated register value
        """
        assert isinstance(addr, int)
        assert isinstance(width, int)
        assert isinstance(accesswidth, int)

        if addr == 0x4:
            if self.PIN_output is True:
                return 0x1
            else:
                return 0x0
        elif addr == 0x8:
            if self.PIN_state is True:
                return 0x1
            else:
                return 0x0

    def write_addr_space(self, addr: int, width: int, accesswidth: int, data: int) -> None:
        """
        Callback to for the simulation of the chip

        Args:
            addr: Address to write to
            width: Width of the register in bits
            accesswidth: Minimum access width of the register in bits
            data: value to be written to the register

        Returns:
            None
        """
        assert isinstance(addr, int)
        assert isinstance(width, int)
        assert isinstance(accesswidth, int)
        assert isinstance(data, int)

        if addr == 0x4:
            if (data & 0x1) == 0x1:
                self.PIN_output = True
            else:
                self.PIN_output = False
        elif addr == 0x8:
            if (data & 0x1) == 0x1:
                self.PIN_state = True
            else:
                self.PIN_state = False

        self.update_LED()

    def update_LED(self):

        if self.PIN_output is True:
            # LED is enabled
            if self.PIN_state is True:
                self.LED.itemconfig(self.LED_inner, fill='red')
            else:
                self.LED.itemconfig(self.LED_inner, fill='black')
        else:
            self.LED.itemconfig(self.LED_inner, fill='black')

# these two methods can be put in the simulator Tkinter event queue to perform register writes on
# the register access layer (in turn causing the state of the simulator to change)

def turn_LED_on(chip: mychip_cls, sim_kt_root):

    # write a '1' to the LED state field
    chip.GPIO.GPIO_state.PIN_0.write(1)
    # set up another event to happen
    sim_kt_root.after(2000, turn_LED_off, chip, sim_kt_root)

def turn_LED_off(chip: mychip_cls, sim_kt_root):

    # write a '0' to the LED state field
    chip.GPIO.GPIO_state.PIN_0.write(0)
    # set up another event to happen
    sim_kt_root.after(2000, turn_LED_on, chip, sim_kt_root)


if __name__ == '__main__':

    # make an instance of the chip simulator and then locally defined the callbacks that will be
    # used to by the register access model
    chip_simulator = ChipSim()

    def read_call_back(addr: int, width: int, accesswidth: int):
        return chip_simulator.read_addr_space(addr=addr,
                                              width=width,
                                              accesswidth=accesswidth)
    def write_call_back(addr: int, width: int, accesswidth: int, data: int):
        chip_simulator.write_addr_space(addr=addr,
                                        width=width,
                                        accesswidth=accesswidth,
                                        data=data)

    # create a callback set for the callbacks
    callbacks = NormalCallbackSet(read_callback=read_call_back,
                                  write_callback=write_call_back)

    # created an instance of the register model and connect the callbacks to the simulator
    mychip = mychip_cls(callbacks=callbacks)

    # configure the GPIO.PIN_0 as an output
    mychip.GPIO.GPIO_dir.PIN_0.write(mychip.GPIO.GPIO_dir.PIN_0.enum_cls.dir_out)

    # set up the first event to turn the LED on after 2s (this event will then set-up a follow up
    # event to turn it off. This sequencer repeats forever.
    chip_simulator.root.after(2000, turn_LED_on, mychip, chip_simulator.root)
    # start the GUI (simulator)
    chip_simulator.root.mainloop()

Enumerated Fields

Enumerations are a good practice to implicitly encode that have special meanings which can not be easily understood from the field name. The SystemRDL enumerations are implemented using python

addrmap enumerated_fields {

	enum gpio_direction_enc {
			gpio_in = 0 { desc = "Pin as in direction"; };
			gpio_out = 1 { desc = "Pin as out direction"; };
		};

    field gpio_direction { encode = gpio_direction_enc;
                           fieldwidth=1;
                           reset=gpio_direction_enc::gpio_in; };

    enum gpio_strength_enc {
			strength_8mA = 0 { desc = "8mA drive strength"; };
			strength_12mA = 1 { desc = "12mA drive strength"; };
			strength_16mA = 2 { desc = "16mA drive strength"; };
			strength_20mA = 3 { desc = "20mA drive strength"; };
		};

    field gpio_strength { encode = gpio_strength_enc;
                          fieldwidth=2;
                          reset=gpio_strength_enc::strength_8mA; };

    reg {
        <% for( $i = 0; $i < 8; $i += 1 ) { %>
        gpio_direction gpio_<%=$i%>;
        <% } %>
    } gpio_dir;

    reg {
        <% for( $i = 0; $i < 8; $i += 1 ) { %>
        gpio_strength gpio_<%=$i%>;
        <% } %>
    } gpio_strength;

};

This systemRDL code can be built using the command line tool as follows (assuming it is stored in a file called enumerated_fields.rdl):

peakrdl python enumerated_fields.rdl -o .

The following example shows the usage of the enumeration

Note

In order to set the value of an enumerated field, using the write() method. The correct enumerated class is needed. This can be retrieved from the field itself with the enum_cls property

"""
A demonstration of using enumeration
"""
from enumerated_fields.lib import NormalCallbackSet

from enumerated_fields.reg_model.enumerated_fields import enumerated_fields_cls as GPIO

class HardwareSimulator:
    def __init__(self):
        # use a python dictionary to simulate the hardware
        self._address_space = {0x00: 0, 0x04: 0}

    def read(self, addr: int, width: int = 32, accesswidth: int = 32) -> int:
        """
        function to simulate a device read
        """
        return self._address_space[addr]


    def write(self, addr: int, data: int, width: int=32, accesswidth: int=32) -> None:
        """
        function to simulate a device read
        """
        self._address_space[addr] = data

if __name__ == '__main__':

    # create an instance of the hardware simulator
    hw = HardwareSimulator()
    # create an instance of the RAL with the callbacks directed at the hardware simulator
    gpio = GPIO(callbacks=NormalCallbackSet(read_callback=hw.read, write_callback=hw.write))

    # get the field values
    for field in gpio.gpio_strength.readable_fields:
        print(f'{field.inst_name} has strength {field.read().name} [0x{field.read().value}]')

    # set the field values by retrieving the enum class from the class itself
    CurrentEnum = gpio.gpio_strength.gpio_0.enum_cls
    gpio.gpio_strength.gpio_0.write(CurrentEnum.STRENGTH_12MA)

Array Access

SystemRDL supports multi-dimensional arrays, the following example shows an definition with an 1D and 3D array with various methods to access individual elements of the array and use of the iterators to walk through elements in loops

addrmap array_access {

    reg reg_a {
        field { fieldwidth=1; } field_a;
    };

    reg_a reg_array_1D[2];
    reg_a reg_array_3D[2][2][2];
};

This systemRDL code can be built using the command line tool as follows (assuming it is stored in a file called array_access.rdl):

peakrdl python array_access.rdl -o .
import json
from typing import Union

from array_access.reg_model.array_access import array_access_cls
from array_access.lib.callbacks import NormalCallbackSet

# dummy functions to demonstrate the class
def read_addr_space(addr: int, width: int, accesswidth: int) -> int:
    """
    Callback to simulate the operation of the package, everytime the read is called, it return
    an integer value of 0

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits

    Returns:
        value inputted by the used
    """
    return int(0)

if __name__ == '__main__':

    # create an instance of the class
    regmodel = array_access_cls(callbacks=NormalCallbackSet(read_callback=read_addr_space))

    # access a entry in the 1D array
    print(regmodel.reg_array_1D[0].field_a.read())

    # loop over all the elements in the 1D array, without the index being exposed
    for array_item in regmodel.reg_array_1D:
        print(array_item.field_a.read())

    # loop over all the elements in the 1D array, with the index being exposed
    for index, array_item in regmodel.reg_array_1D.items():
        print(f'item[{index[0]}]={array_item.field_a.read():d}')

    # access a entry in the 3D array with other indexing scheme
    print(regmodel.reg_array_3D[1,1,1].field_a.read())

    # loop over all the elements in the 3D array, without the index being exposed
    for array_item in regmodel.reg_array_3D:
        print(array_item.field_a.read())

    # loop over all the elements in the 1D array, with the index being exposed
    for index, array_item in regmodel.reg_array_3D.items():
        # convert the index which is a tuple of integers to a comma delimited string
        index_str = ','.join([str(index_element) for index_element in index])
        print(f'item[{index_str}]={array_item.field_a.read():d}')

    # perform an operation to one axis of the array
    for array_item in regmodel.reg_array_3D[0,0,0::]:
        print(array_item.field_a.read())

Optimised Access

Working with individual registers

Each time the read or write method for a register field is accessed the hardware is read and or written (a write to a field will normally require a preceding read). When accessing multiple fields in the same register, it may be desirable to use one of the optimised access methods.

Consider the following example of an GPIO block with 4 GPIO pins (configured in a single register):

addrmap optimised_access {

    enum gpio_direction_enc {
			gpio_in = 0;
			gpio_out = 1;
		};

    field gpio_direction { encode = gpio_direction_enc; fieldwidth=1; };
    field gpio_drive_str { fieldwidth=2; };
    field gpio_pullup { fieldwidth=1; };


    reg {
        default sw = rw;
        default hw = r;

        gpio_direction gpio_0_dir;
        gpio_drive_str gpio_0_strength;
        gpio_pullup gpio_0_pullup;

        gpio_direction gpio_1_dir;
        gpio_drive_str gpio_1_strength;
        gpio_pullup gpio_1_pullup;

        gpio_direction gpio_2_dir;
        gpio_drive_str gpio_2_strength;
        gpio_pullup gpio_2_pullup;

        gpio_direction gpio_3_dir;
        gpio_drive_str gpio_3_strength;
        gpio_pullup gpio_3_pullup;

    } gpio_register;
};

In the to configure gpio_0 and gpio_1 whilst leaving the other two unaffected it can be done in two methods:

  • using the write_fields method of the register

  • using the register context manager

Both demonstrated in the following code example:

"""
PeakRDL Python example to show the different methods to access the fields of a register
"""
from optimised_access.reg_model.optimised_access import optimised_access_cls, \
    optimised_access_gpio_direction_enc_enumcls

from optimised_access.lib import NormalCallbackSet

# dummy functions to demonstrate the class
def read_addr_space(addr: int, width: int, accesswidth: int) -> int:
    """
    Callback to simulate the operation of the package, everytime the read is called, it return
    an integer value of 0

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits

    Returns:
        value inputted by the used
    """
    return int(0)


def write_addr_space(addr: int, width: int, accesswidth: int, data: int) -> None:
    """
    Callback to simulate the operation of the package, everytime the read is called, it will
    request the user input the value to be read back.

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits
        data: value to be written to the register

    Returns:
        None
    """
    print(f'0x{data:X} written to 0x{addr:X}')

if __name__ == '__main__':

    # create an instance of the address map with the simulated callback necessary to demonstrate
    # the example
    dut = optimised_access_cls(callbacks=NormalCallbackSet(read_callback=read_addr_space,
                                                           write_callback=write_addr_space))

    # configure the GPIO 0 and GPIO 1 without affecting the state of the GPIO 2 and GPIO 3
    # configuration.

    # This can be done either using the ``write_fields`` method
    dut.gpio_register.write_fields(gpio_0_dir=optimised_access_gpio_direction_enc_enumcls.GPIO_OUT,
                                   gpio_0_pullup=False,
                                   gpio_0_strength=2,
                                   gpio_1_dir=optimised_access_gpio_direction_enc_enumcls.GPIO_IN)

    # It can also be done with the context manager
    with dut.gpio_register.single_read_modify_write() as reg:
        reg.gpio_0_dir.write(optimised_access_gpio_direction_enc_enumcls.GPIO_OUT)
        reg.gpio_0_pullup.write(False)
        reg.gpio_0_strength.write(2)
        reg.gpio_0_dir.write(optimised_access_gpio_direction_enc_enumcls.GPIO_IN)

Working with registers arrays

In many systems it is more efficient to read and write in block operations rather than using individual register access.

Consider the following example of an GPIO block with 8 GPIO pins (configured in a 8 registers):

addrmap optimised_array_access {

    enum gpio_direction_enc {
			gpio_in = 0;
			gpio_out = 1;
		};

    reg {
        default sw = rw;
        default hw = r;

        field { encode = gpio_direction_enc; fieldwidth=1; } gpio_direction;
        field { fieldwidth=2; } gpio_drive_str;
        field { fieldwidth=1; } gpio_pullup;

    } gpio_register[8];
};

In order to configure all the GPIOs a range of operations are shown with the use of the context managers to make more efficient operations

"""
PeakRDL Python example to show the different methods to access the a register array
"""
from array import array as Array

from optimised_array_access.reg_model.optimised_array_access import optimised_array_access_cls

from optimised_array_access.lib import NormalCallbackSet

# dummy functions to demonstrate the class
def read_block(addr: int, width: int, accesswidth: int, length: int) -> Array:
    """
    Callback to simulate the operation of the package, everytime the read block is called
    returns an arroy of zeros

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits

    Returns:
        value inputted by the used
    """
    return Array('L', [0 for _ in range(length)])

def read(addr: int, width: int, accesswidth: int) -> int:
    """
    Callback to simulate the operation of the package, everytime the read is called, it return
    an integer value of 0

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits

    Returns:
        value inputted by the used
    """
    return 0

def write_block(addr: int, width: int, accesswidth: int, data: int) -> None:
    """
    Callback to simulate the operation of the package, everytime the write block is called it
    will print out the contents

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits
        data: value to be written to the register

    Returns:
        None
    """
    value_str = ','.join([f'0x{value:X}' for value in data])
    print(f'data written to address 0x{addr:X} = {value_str}')

def write(addr: int, width: int, accesswidth: int, data: int) -> None:
    """
    Callback to simulate the operation of the package, everytime the write is called it
    will print out the contents

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits
        data: value to be written to the register

    Returns:
        None
    """
    print(f'data written to address 0x{addr:X} = 0x{data:X}')

if __name__ == '__main__':

    # create an instance of the address map with the simulated callback necessary to demonstrate
    # the example
    dut = optimised_array_access_cls(callbacks=NormalCallbackSet(read_block_callback=read_block,
                                                                 write_block_callback=write_block,
                                                                 read_callback=read,
                                                                 write_callback=write))

    # the following will involve 2 reads and 2 writes (one per feild) for each register (8 in total)
    for idx in range(len(dut.gpio_register)):
        dut.gpio_register[idx].gpio_direction.write(dut.gpio_register[idx].gpio_direction.enum_cls.GPIO_OUT)
        dut.gpio_register[idx].gpio_pullup.write(False)
        # leave the drive strength untouched

    # the following will involve 1 reads and 1 writes for each register (8 in total)
    # also note the use of the register array iterator (so you don't need range and indexing)
    for gpio_register in dut.gpio_register:
        with gpio_register.single_read_modify_write() as reg:
            reg.gpio_direction.write(reg.gpio_direction.enum_cls.GPIO_OUT)
            reg.gpio_pullup.write(False)
            # leave the drive strength untouched

    # the following will do one block read and one block write for the same thing
    with dut.gpio_register.single_read_modify_write() as reg_array:
        for reg in reg_array:
            reg.gpio_direction.write(reg.gpio_direction.enum_cls.GPIO_OUT)
            reg.gpio_pullup.write(False)
            # leave the drive strength untouched

Walking the Structure

The following two example show how to use the generators within the register access layer package to traverse the structure.

Both examples use the following register set which has a number of features to demonstrate the structures

addrmap chip_with_registers {

    enum twoBitFieldType {
		value1 = 0 { name = "Value 1"; };
		value2 = 1 { name = "Value 2"; };
	};

    reg reg_Type {
        default sw = rw;
        default hw = r;
        field {} first_field[15:0] = 0;
        field { encode=twoBitFieldType; } second_field[17:16] =  {twoBitFieldType::value1};

    };

    regfile regfile_Type {
        reg_Type single_reg;
        reg_Type reg_array[4];
    };

    regfile_Type regfile_array[2];
    regfile_Type single_regfile;

};

This systemRDL code can be built using the command line tool as follows (assuming it is stored in a file called chip_with_registers.rdl):

peakrdl python chip_with_registers.rdl -o chip_with_registers

Traversing without Unrolling Loops

The first example is reading all the readable registers from the register map and writing them into a JSON file. To exploit the capabilities of a JSON file the arrays of registers and register files must be converted to python lists, therefore the loops must not be unrolled, the array objects are accessed directly.

import json
from typing import Union

from chip_with_registers.reg_model.chip_with_registers import chip_with_registers_cls

from chip_with_registers.lib import NormalCallbackSet, RegReadOnly, RegReadWrite, \
    MemoryReadOnly, MemoryReadWrite, RegFile, AddressMap, RegReadOnlyArray, RegReadWriteArray, \
    AddressMapArray, RegFileArray, MemoryReadOnlyArray, MemoryReadWriteArray


# dummy functions to demonstrate the class
def read_addr_space(addr: int, width: int, accesswidth: int) -> int:
    """
    Callback to simulate the operation of the package, everytime the read is called, it return
    an integer value of 0

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits

    Returns:
        value inputted by the used
    """
    assert isinstance(addr, int)
    assert isinstance(width, int)
    assert isinstance(accesswidth, int)
    return int(0)


def write_addr_space(addr: int, width: int, accesswidth: int, data: int) -> None:
    """
    Callback to simulate the operation of the package, everytime the read is called, it will
    request the user input the value to be read back.

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits
        data: value to be written to the register

    Returns:
        None
    """
    raise NotImplementedError('No register write occur in this example')


class RegisterDumper:

    def __init__(self, node: AddressMap):
        """
        Class to read all the readable registers in a design and write them to a file

        Args:
            node: AddressMap node to process
        """
        self.address_map = node

    @property
    def registers(self):
        """

        Returns: dictionary with all the register and their field values for the top level node

        """
        return self._process_section(self.address_map)

    def json_dump(self, filename):
        """
        Write all the readable registers out to a JSON file

        Args:
            filename: file to be written to

        Returns: None

        """
        with open(filename, encoding='utf-8', mode='w') as fp:
            json.dump(self.registers, fp, indent=4)

    @staticmethod
    def _process_registers(node: Union[MemoryReadOnly, MemoryReadWrite, RegFile, AddressMap]):
        """
        Process all the registers that are in a memory or section

        Args:
            node: a RegFile, AddressMap or Memory to process

        Returns:
            dictionary with all the register and their field values

        """
        registers_dump = {}
        for register in node.get_readable_registers(unroll=False):

            # the register arrays are put into a list
            if isinstance(register, (RegReadOnlyArray, RegReadWriteArray)):
                array_dump = []
                for register_instance in register:
                    array_dump.append(register_instance.read_fields())
                registers_dump[register.inst_name] = array_dump

            elif isinstance(register, (RegReadOnly, RegReadWrite)):
                registers_dump[register.inst_name] = register.read_fields()

            else:
                raise TypeError('unexpected type encoutered')

        return registers_dump

    def _process_memory(self, node: Union[MemoryReadOnly, MemoryReadWrite]):
        """
        In a memory all the registers must be dumped out

        Args:
            node: Memory Node

        Returns:
            dictionary with all the register and their field values

        """
        return self._process_registers(node)

    def _process_section(self, node: Union[RegFile, AddressMap]):
        """
        In a section all the sub-sections and registers must be dumped out

        Args:
            node: a RegFile or AddressMapto process

        Returns:
            dictionary with all the register and their field values

        """
        registers_dump = {}

        # process all the sections in the section
        for section in node.get_sections(unroll=False):
            if isinstance(section, (RegFileArray, AddressMapArray)):
                array_dump = []
                for section_instance in section:
                    array_dump.append(self._process_section(section_instance))
                registers_dump[section.inst_name] = array_dump

            elif isinstance(section, (RegFile, AddressMap)):
                registers_dump[section.inst_name] = self._process_section(section)
            else:
                raise TypeError('unexpected type encoutered')

        # process all the memories in the section, note only AddressMaps can have memories within
        # them
        if isinstance(node, AddressMap):
            for memory in node.get_memories(unroll=False):
                if isinstance(memory, (MemoryReadOnlyArray, MemoryReadWriteArray)):
                    array_dump = []
                    for memory_instance in memory:
                        array_dump.append(self._process_memory(memory_instance))
                    registers_dump[memory.inst_name] = array_dump

                elif isinstance(memory, (MemoryReadOnly, MemoryReadWrite)):
                    registers_dump[memory.inst_name] = self._process_memory(memory)
                else:
                    raise TypeError('unexpected type encoutered')

        # process all the registers in the section
        registers_dump.update(self._process_registers(node))

        return registers_dump


if __name__ == '__main__':

    # create an instance of the address map with the simulated callback necessary to demonstrate
    # the example
    dut = chip_with_registers_cls(callbacks=NormalCallbackSet(read_callback=read_addr_space,
                                                              write_callback=write_addr_space))

    # generate an instance of the RegisterDumper and write the registers to a file
    reg_dumper = RegisterDumper(dut)
    reg_dumper.json_dump('reg_dump.json')

This will create a JSON file as follows:

{
    "regfile_array": [
        {
            "single_reg": {
                "first_field": 0,
                "second_field": 0
            },
            "reg_array": [
                {
                    "first_field": 0,
                    "second_field": 0
                },
                {
                    "first_field": 0,
                    "second_field": 0
                },
                {
                    "first_field": 0,
                    "second_field": 0
                },
                {
                    "first_field": 0,
                    "second_field": 0
                }
            ]
        },
        {
            "single_reg": {
                "first_field": 0,
                "second_field": 0
            },
            "reg_array": [
                {
                    "first_field": 0,
                    "second_field": 0
                },
                {
                    "first_field": 0,
                    "second_field": 0
                },
                {
                    "first_field": 0,
                    "second_field": 0
                },
                {
                    "first_field": 0,
                    "second_field": 0
                }
            ]
        }
    ],
    "single_regfile": {
        "single_reg": {
            "first_field": 0,
            "second_field": 0
        },
        "reg_array": [
            {
                "first_field": 0,
                "second_field": 0
            },
            {
                "first_field": 0,
                "second_field": 0
            },
            {
                "first_field": 0,
                "second_field": 0
            },
            {
                "first_field": 0,
                "second_field": 0
            }
        ]
    }
}

Traversing without Unrolling Loops

The second example is setting every register in the address map back to its default values. In this case the loops are unrolled to conveniently access all the register without needing to worry if they are in an array or not.

import json
from typing import Union

from chip_with_registers.reg_model.chip_with_registers import chip_with_registers_cls

from chip_with_registers.lib import NormalCallbackSet,  RegWriteOnly, RegReadWrite, \
    MemoryWriteOnly, MemoryReadWrite, RegFile, AddressMap


# dummy functions to demonstrate the class
def read_addr_space(addr: int, width: int, accesswidth: int) -> int:
    """
    Callback to simulate the operation of the package, everytime the read is called, it return
    an integer value of 0

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits

    Returns:
        value inputted by the used
    """
    return int(0)


def write_addr_space(addr: int, width: int, accesswidth: int, data: int) -> None:
    """
    Callback to simulate the operation of the package, everytime the read is called, it will
    request the user input the value to be read back.

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits
        data: value to be written to the register

    Returns:
        None
    """
    print(f'0x{data:X} written to 0x{addr:X}')


class chip_with_registers_cls_with_reset(chip_with_registers_cls):
    """
    Extends the chip_with_registers_cls class adding methods to reset all the registers to
    there defined reset values
    """

    @staticmethod
    def _process_registers(node: Union[MemoryWriteOnly, MemoryReadWrite, RegFile, AddressMap]):
        """
        Process all the registers that are in a memory or section

        Args:
            node: a RegFile, AddressMap or Memory to process

        Returns:
            dictionary with all the register and their field values

        """

        for register in node.get_writable_registers(unroll=True):

            if isinstance(register, (RegWriteOnly, RegReadWrite)):
                reset_value_dict = {}
                for field in register.writable_fields:
                    reset_value = field.default
                    if reset_value is not None:
                        reset_value_dict[field.inst_name] = reset_value
                if len(reset_value_dict) > 0:
                    register.write_fields(**reset_value_dict)
            else:
                raise TypeError('unexpected type encoutered')

    def _process_memory(self, node: Union[MemoryWriteOnly, MemoryReadWrite]):
        """
        In a memory all the registers must be dumped out

        Args:
            node: Memory Node

        Returns:
            None

        """
        self._process_registers(node)

    def _process_section(self, node: Union[RegFile, AddressMap]):
        """
        In a section all the sub-sections and registers must be dumped out

        Args:
            node: a RegFile or AddressMapto process

        Returns:
            None

        """

        # process all the sections in the section
        for section in node.get_sections(unroll=True):
            if isinstance(section, (RegFile, AddressMap)):
                self._process_section(section)
            else:
                raise TypeError('unexpected type encoutered')

        # process all the memories in the section, note only AddressMaps can have memories within
        # them
        if isinstance(node, AddressMap):
            for memory in node.get_memories(unroll=True):
                if isinstance(memory, (MemoryWriteOnly, MemoryReadWrite)):
                    self._process_memory(memory)
                else:
                    raise TypeError('unexpected type encoutered')

        # process all the registers in the section
        self._process_registers(node)

    def reset(self):
        """
        Resets all the registers in the address map to their default values

        Returns:
            None

        """
        self._process_section(self)

if __name__ == '__main__':

    # create an instance of the address map with the simulated callback necessary to demonstrate
    # the example
    dut = chip_with_registers_cls_with_reset(
        callbacks=NormalCallbackSet(read_callback=read_addr_space,
                                    write_callback=write_addr_space))

    dut.reset()

Python Safe Names

The systemRDL structure is converted to a python class structure, there are two concerns:

  • if any systemRDL node name is a python keyname

  • if any systemRDL node name clashes with part of the peakrdl_standard types, for example all register nodes have an address property that would clash with a field of that register called address

consider the following example:

addrmap my_addr_map {

    reg {
        default sw = rw;
        default hw = r;
        field { fieldwidth=1; } in;
    } address;
};

This would create an object attribute address which would clash with an existing property of the my_addr_map object. The register field can not be called in as this is a python keyword. Therefore peakrdl python will use the name field_in in the generated code to avoid the clash. The algorithm for renaming node to avoid name clashes does not need to be known to an end user, the names can be looked up.

User Defined Property

PeakRDL Python recognises a SystemRDL User Defined Propery (UDP) that can be used to force the names used in the generated python code for node. In this case following names will be overridden:

  • name of the register will be overridden_reg_a rather than reg_a

  • the name of the field will be overridden_field_a rather than field_a

property python_inst_name { type = string; component = addrmap | regfile | reg | field; };

addrmap over_ridden_names {

    reg {
        default sw = rw;
        default hw = r;
        python_inst_name = "overridden_reg_a";
        field { python_inst_name="overridden_field_a"; fieldwidth=1; } field_a;
    } reg_a;
};

Name lookup

When names have been altered (either to avoid a name clash or by the python_inst_name User Defined Property), attributes can be accessed using the get_child_by_system_rdl_name method of any object in the register model. The following example shows both methods to access the field from the example above

import json
from typing import Union

from over_ridden_names.reg_model.over_ridden_names import over_ridden_names_cls
from over_ridden_names.lib import NormalCallbackSet

# dummy functions to demonstrate the class
def read_addr_space(addr: int, width: int, accesswidth: int) -> int:
    """
    Callback to simulate the operation of the package, everytime the read is called, it return
    an integer value of 0

    Args:
        addr: Address to write to
        width: Width of the register in bits
        accesswidth: Minimum access width of the register in bits

    Returns:
        value inputted by the used
    """
    return int(0)

if __name__ == '__main__':

    # create an instance of the class
    over_ridden_names = over_ridden_names_cls(callbacks=NormalCallbackSet(read_callback=read_addr_space))

    # access the field value directly
    print(over_ridden_names.overridden_reg_a.overridden_field_a.read())

    # access the feild value using the original systemRDL names
    print(over_ridden_names.get_child_by_system_rdl_name('reg_a').get_child_by_system_rdl_name('field_a').read())

Autoformating

The generated code is not perfect it often has lots of spare black lines, over time this will improve but the quickest way to resolve these issue is to include an autoformatter post-generation. Previous versions of peakrdl-python included the option to run an autoformatter to clean up the generated code. This had two issues:

  • It created maintenance issues when the autoformatter changed

  • The choice of autoformatter is an individual one, rather than force an autoformatter on people it is better to let people choose their own.

peakrdl-python uses the Black Black in the CI tests to check that the generated code is compatible with an autoformatter.