PeakRDL Python

Introduction

PeakRDL Python is a python package which can be used to generate a register access layer python package from a SystemRDL definition.

SystemRDL and control & status register (CSR) generator toolchain

SystemRDL, more accurately SystemRDL 2.0 is a description language that describes the registers in a device, for example an FPGA or Integrated Circuit (IC). Using this technology allows other parts of the design flow to be automatically generated, avoiding mistakes with inconsistencies and speeding up the design flow.

The suite of tools needed for this flow are called a control & status register (CSR) generator.

Note

This documentation does not attempt to explain all the good reasons for wanted a CSR generator, other people have done a far better job.

PeakRDl Python is intended to be part of CSR generator flow.

What is a Register Access Layer (RAL)

A Register Access Layer is a software component to make writing scripts and software to control a device with hardware registers easier.

Hardware Abstraction Layer (HAL) versus Register Access Layer (RAL)

Note

The Register Access Layer (RAL) is aimed at people who understand the registers in the device.

At some point another software component called a Hardware Abstraction Layer (HAL) will often get produced that abstracts the device function providing functions to do more useful things. The RAL could be used as part of a HAL.

Note

The Hardware Abstraction Layer (HAL) provides abstact functionality and allows people to use the device without needing a full knowledge of how it works.

What does it do

The use of a RAL is best shown with an example

Imagine a script to carry out a simple task on a IC, configure an GPIO Pin as an output and set the state to 1. The device has 8 GPIO pins controlled from two registers.

Register

Address

Bit

Function

7

6

5

4

3

2

1

0

DIR

0x100

GPIO_7_DIR

GPIO_6_DIR

GPIO_5_DIR

GPIO_4_DIR

GPIO_3_DIR

GPIO_2_DIR

GPIO_1_DIR

GPIO_0_DIR

Sets direction of GPIO

Value

Meaning

0

In

1

Out

DATA_OUT

0x104

GPIO_7_OUT

GPIO_6_OUT

GPIO_5_OUT

GPIO_4_OUT

GPIO_3_OUT

GPIO_2_OUT

GPIO_1_OUT

GPIO_0_OUT

Sets the state of a GPIO configured as out

This example uses a simple simulation class to mimic the behaviour of the device offering read and write methods to offer register read and write access to the device. In the real work this would likely go via a device driver or JTAG emulator, if the software is running off chip (i.e. a PC).

"""
A hardware simulator to show how the gpio example works
"""

class HardwareSimulator:
    def __init__(self):
        # use a python dictionary to simulate the hardware
        self._address_space = {0x100: 0, 0x104: 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__':

    pass

To configure pin 0 and set its state you would need to do the following steps:

  1. Read the DIR register (to make sure you preserve the states of other pins)

  2. Take the read value of the DIR register, force bit 0 to 1 then write it back

  3. Read the DATA_OUT register (to make sure you preserve the states of other pins)

  4. Take the read value of the DATA_OUT register, force bit 0 to 1 then write it back

If you had a simple environment that only had register read / write function, the code would be as follows:

"""
An example to turn on GPIO 0 with just read and write
"""
from hardware_sim import HardwareSimulator
# create an instance of the hardware simulator
hw = HardwareSimulator()

if __name__ == '__main__':

    # 1. Read the DIR register (to make sure you preserve the states of other pins)
    dir_reg = hw.read(0x100)
    # 2. Take the read value of the DIR register, force bit 0 to `1` then write it back
    hw.write(0x100, dir_reg | (1 << 0)) # force bit 0 with bitwise OR
    # 3. Read the DATA_OUT register (to make sure you preserve the states of other pins)
    data_out_reg = hw.read(0x104)
    # 4. Take the read value of the DATA_OUT register, force bit 0 to `1` then write it back
    hw.write(0x104, data_out_reg | (1 << 0))  # force bit 0 with bitwise OR

This code requires addresses to be hard coded, remembering to do read/modify/writes and bit manipulations

Warning

A Register Access Layer (RAL) is not for everyone. Some engineers like to see the address of each register and the content of a register as a hex word. You may be quite happy with this, if that is you please stop, the overhead of an extra layer, its opaque nature and inefficency will annoy you.

In order to move on the systemRDL code for the registers needs to exist

addrmap gpio {

    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; };
    field gpio_data { fieldwidth=1; reset=0; };

    reg {
        default sw = rw;
        default hw = r;
        name = "Direction";
        desc = "Sets the direction of each GPIO Pin";

        gpio_direction gpio_7_dir[7:7];
        gpio_direction gpio_6_dir[6:6];
        gpio_direction gpio_5_dir[5:5];
        gpio_direction gpio_4_dir[4:4];
        gpio_direction gpio_3_dir[3:3];
        gpio_direction gpio_2_dir[2:2];
        gpio_direction gpio_1_dir[1:1];
        gpio_direction gpio_0_dir[0:0];

    } dir @ 0x100;

    reg {
        default sw = rw;
        default hw = r;
        name = "Data Out";
        desc = "Sets the state of the GPIO configured as out";

        gpio_data gpio_7_out[7:7];
        gpio_data gpio_6_out[6:6];
        gpio_data gpio_5_out[5:5];
        gpio_data gpio_4_out[4:4];
        gpio_data gpio_3_out[3:3];
        gpio_data gpio_2_out[2:2];
        gpio_data gpio_1_out[1:1];
        gpio_data gpio_0_out[0:0];

    } data_out @ 0x104;
};

In order to build the code (assuming you have everything installed), use the following

peakrdl python gpio.rdl -o .

Once built, a set of test cases can be run on the code to confirm its integrity, this is using the unittest framework that comes with python

python -m unittest discover -s gpio\tests -t .

Using the RAL allows for much simipler to understand code that does the function that was intended

"""
An example to turn on GPIO 0 with the RAL
"""
from gpio.reg_model.gpio import gpio_cls as GPIO
from gpio.reg_model.gpio import gpio_gpio_direction_enc_enumcls as GPIO_DIR_ENUM

from gpio.lib import NormalCallbackSet

from hardware_sim import HardwareSimulator
# create an instance of the hardware simulator
hw = HardwareSimulator()

if __name__ == '__main__':

    # 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))

    # Configure GPIO[0] as out
    gpio.dir.gpio_0_dir.write(GPIO_DIR_ENUM.GPIO_OUT)
    # Configure GPIO[0] state as 1
    gpio.data_out.gpio_0_out.write(1)

The final part of this example shows a Hardware Abstraction Layer (HAL), in this case the GPIO pins are abstracted to look like an array and the direction is automatically configured when the user attempts to set the output state.

"""
An example to turn on GPIO 0 with the RAL
"""
from gpio.reg_model.gpio import gpio_cls as GPIO
from gpio.reg_model.gpio import gpio_gpio_direction_enc_enumcls as GPIO_DIR_ENUM

from gpio.lib import NormalCallbackSet

from hardware_sim import HardwareSimulator
# create an instance of the hardware simulator
hw = HardwareSimulator()

class HAL:

    class GPIO_HAL:
        def __init__(self, ral: GPIO, channel:int):
            self._chn = channel
            self._ral = ral

        @property
        def __direction_field(self):
            return getattr(self._ral.dir, f'gpio_{self._chn}_dir')

        @property
        def __data_out_field(self):
            return getattr(self._ral.data_out, f'gpio_{self._chn}_out')

        @property
        def direction(self) -> GPIO_DIR_ENUM:
            return self.__direction_field.read()

        @direction.setter
        def direction(self, dir: GPIO_DIR_ENUM):
            self.__direction_field.write(dir)

        @property
        def data_out(self) -> bool:
            return self.__data_out_field.read()

        @data_out.setter
        def data_out(self, dir: bool):
            if self.direction is not GPIO_DIR_ENUM.GPIO_OUT:
                self.direction = GPIO_DIR_ENUM.GPIO_OUT
            self.__data_out_field.write(dir)


    def __init__(self, callbacks:NormalCallbackSet):
        self.ral = GPIO(callbacks=callbacks)
        self._gpio = [self.GPIO_HAL(self.ral, chn) for chn in range(8)]

    def __getitem__(self, item):
        return self._gpio[item]

if __name__ == '__main__':

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

    # attempting to set the data_out automatically checks the direction is configured and
    # sets it if necessary
    gpio[0].data_out = True