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>liblib_testsim_libreg_model<root_name>.pysim<root_name>.pyteststest_<root_name>.pyIn the folder structure above:
<root_name>.py- This is the register access layer code for the designtest_<root_name>.py- This is a set of autogenerated unittests to verify the register access layer (The unit test generation can be skipped, seeskip_test_case_generation)lib- This is a package of base classes used by the register access layer (The copy of this can be skipped, see Skipping the Library Copy)lib_test- This is a package of base classes autogenerated unit tests (The copy of this can be skipped, see Skipping the Library Copy)sim_lib- This is a package of base classes used by the register access layer simulator (The copy of this can be skipped, see Skipping the Library Copy)
Changed in version 2.0.0: The reg_model was changed in version 2.0.0 to split it out into multiple modules rather
than building the whole register model in a single python module. This helps avoid
excessively large files which helps speed up the generation and loading time.
Changed in version 3.0.0: The auto-generated unit tests were changed in version 3.0.0 to split to make use of a test library, which significantly reduced the size of the generated code.
Top Level Classes#
Changed in version 2.0.0: A new class aliases were added to the reg_model and sim packages to allow the register
model and simulator to be imported more easily. See the example below using RegModel and
Simulator.
Class Names#
Changed in version 2.0.0: In order to reduce duplication within the generated model a hashing algortihm is applied to the nodes in the design to determine which nodes are unique. This hash is appended to the name of all the python classes in the register model
Added in version 2.1.0: There are two possible algorithms for the hashing, this is selectable by the user:
The builtin python
hashfunction, this is fast but is a salted hash so changes hashes export to exportUse the
SHA256hash from the pythonhashlibstandard library, this may slow down the export of large register models but will be consistent, therefore is useful if the resultant code is being checked into a version control system (such as GIT) and the differences are being reviewed
Tip
Use the default builtin hash option unless you need to use the alternative
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.
Tip
The simulator generated with the register model can be used as an alternative to a hardware connection
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: 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 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 write is called, it will
print out the result.
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:
NormalCallbackSetfor standard python function callbacksAsyncCallbackSetfor async python function callbacks, these are called from the library usingawait
Legacy Block Callback and Block Access#
Changed in version 0.9.0: Previous versions of PeakRDL Python used the python array.array for efficiently moving blocks
of data. This was changed in version 0.9.0 in order to accommodate memories which were larger
than 64 bit wide which could not be supported as the array type only support entries of up to
64 bit.
Warning
The developers apologise for making a breaking change, however, not being able to fully the systemRDL specification was determined to be a major limitation that needed to be addressed.
It could have left this as a future compatibility mode before making a breaking change but that would just delay the pain it was felt to be better to get as many users onto the new API as soon as possible whilst PeakRDL Python is in beta.
If you really want to just keep on with the array based interface and make only minimal changes to existing code, there are two simple steps:
The northbound interfaces that are provided by the generated package expect lists of integers rather than array. The old interfaces can be retained by using the
legacy_block_accessbuild option.The southbound interfaces into the callbacks again need to use lists for the
read_block_callbackandwrite_block_callbackmethods. If you want to continue to use the old scheme use the following callback classes which are part of the callbacks: *NormalCallbackSetLegacyfor standard python function callbacks *AsyncCallbackSetLegacyfor async python function callbacks, these are called from the library usingawait
Changed in version 3.0.0: The legacy_block_access will now default to False
Legacy Enumeration Types#
Changed in version 1.2.0: Previous versions of PeakRDL Python used IntEnum for the the field encoding. This had a
limitation that the metadata from the system RDL code, notably the name and desc
property could not be included. A new data type for the enumerations was introduced in
version 1.2.0.
There was a small risk this may impact some users code, in the case of advanced usage of the
enumeration. The old behaviour can be brought back using the legacy_enum_type build option.
Changed in version 3.0.0: The legacy_enum_type will now default to False
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:
one register that controls the direction of the GPIO pin, at address 0x4
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;
reg {
name="GPIO Set State";
desc="Register to set the state of a GPIO Pin";
GPIO_output_field_type PIN_0;
} GPIO_state;
} 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 Tkinter GUI, incorporating a RED circle to represent the LED. The chip simulator has read and write methods ( equivalent to those offered by a hardware device driver), in this case they use the simulator provided by PeakRDL Python.
"""
A demonstration of extended simulator behaviour in peakrdl-python
"""
import tkinter
import tkinter as tk
from mychip.reg_model import RegModel
from mychip.sim import Simulator
from mychip.lib import NormalCallbackSet
class ChipSim(Simulator):
def __init__(self):
# initialise the chip simulation of the registers at base address 0
super().__init__(address=0)
# create an alias to the GPIO[0] (connected to LED) direction control field
self.__gpio_pin_dir_reg_field_sim = self.node_by_full_name('mychip.GPIO.GPIO_dir.PIN_0')
# attach a callback function to the state of the GPIO[0], which is called everytime the
# field is written to
self.node_by_full_name('mychip.GPIO.GPIO_state.PIN_0').write_callback = self.__update_LED
# 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 __update_LED(self, value) -> None:
"""
Call back attached to the state field for the GPIO.
Args:
value: value written to register field
Returns:
None
"""
if self.__gpio_pin_dir_reg_field_sim.value == 1:
# only action the state of the GPIO (external LED if the direction is set to out)
if value == 1:
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')
def timer_event(chip: RegModel, sim_kt_root: tkinter.Tk) -> None:
"""
timer event which will invert the state of the LED and then set the timer event to run
in 2s
Args:
chip: RAL for the chip
sim_kt_root: root of the tkinter object needed for setting up the next timer event
Returns:
None
"""
# invert the current state of the LED
current_state = chip.GPIO.GPIO_state.PIN_0.read()
if current_state == 0:
chip.GPIO.GPIO_state.PIN_0.write(1)
elif current_state == 1:
chip.GPIO.GPIO_state.PIN_0.write(0)
else:
raise ValueError(f'unhandled current state {current_state:d}')
# set up another event to happen
sim_kt_root.after(2000, timer_event, 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()
# create a callback set for the callbacks
callbacks = NormalCallbackSet(read_callback=chip_simulator.read,
write_callback=chip_simulator.write)
# created an instance of the register model and connect the callbacks to the simulator
mychip = RegModel(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 in 2s. This event will then set-up a follow up
# event such that the sequence runs forever.
chip_simulator.root.after(2000, timer_event, 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 for peakrdl-python
"""
from enumerated_fields.lib import NormalCallbackSet
from enumerated_fields.reg_model import RegModel
from enumerated_fields.sim import Simulator
if __name__ == '__main__':
# create an instance of the hardware simulator
hw = Simulator(0)
# create an instance of the RAL with the callbacks directed at the hardware simulator
gpio = RegModel(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 .
"""
A demonstration of array access for peakrdl-python
"""
from array_access.reg_model import RegModel
from array_access.sim import Simulator
from array_access.lib.callbacks import NormalCallbackSet
if __name__ == '__main__':
# setup the simple simulator
sim = Simulator(0)
# create an instance of the class
regmodel = RegModel(callbacks=NormalCallbackSet(read_callback=sim.read))
# 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_fieldsmethod of the registerusing 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 import RegModel
from optimised_access.sim import Simulator
from optimised_access.lib import NormalCallbackSet
if __name__ == '__main__':
# create an instance of the address map with the simulated callback necessary to demonstrate
# the example
sim = Simulator(0)
dut = RegModel(callbacks=NormalCallbackSet(read_callback=sim.read,
write_callback=sim.write))
# configure the GPIO 0 and GPIO 1 without affecting the state of the GPIO 2 and GPIO 3
# configuration.
# the direction field enumeration is needed to, it is found field attribute, note that the
# same enumeration definition can be used for all channels in this case
direction_enum = dut.gpio_register.gpio_0_dir.enum_cls
# This can be done either using the ``write_fields`` method
dut.gpio_register.write_fields(gpio_0_dir=direction_enum.GPIO_OUT,
gpio_0_pullup=False,
gpio_0_strength=2,
gpio_1_dir=direction_enum.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(direction_enum.GPIO_OUT)
reg.gpio_0_pullup.write(False)
reg.gpio_0_strength.write(2)
reg.gpio_0_dir.write(direction_enum.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 optimised_array_access.reg_model import RegModel
from optimised_array_access.sim import Simulator
from optimised_array_access.lib import NormalCallbackSet
if __name__ == '__main__':
# create an instance of the address map with the simulated callback necessary to demonstrate
# the example
sim = Simulator(0)
dut = RegModel(callbacks=NormalCallbackSet(read_block_callback=sim.read_block,
write_block_callback=sim.write_block,
read_callback=sim.read,
write_callback=sim.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.
"""
A demonstration of traversing the register model generated with peakrdl-python
"""
import json
from typing import Union
from chip_with_registers.reg_model import RegModel
from chip_with_registers.sim import Simulator
from chip_with_registers.lib import NormalCallbackSet, RegReadOnly, RegReadWrite, \
MemoryReadOnly, MemoryReadWrite, RegFile, AddressMap, RegReadOnlyArray, RegReadWriteArray, \
AddressMapArray, RegFileArray, MemoryReadOnlyArray, MemoryReadWriteArray
from chip_with_registers.lib import RegisterFieldJSONEncoder
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, cls=RegisterFieldJSONEncoder)
@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
sim = Simulator(0)
dut = RegModel(callbacks=NormalCallbackSet(read_callback=sim.read,
write_callback=sim.write))
# 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.
"""
A demonstration of traversing the register model generated with peakrdl-python
"""
from typing import Union
from chip_with_registers.reg_model import RegModel
from chip_with_registers.sim import Simulator
from chip_with_registers.lib import NormalCallbackSet, RegWriteOnly, RegReadWrite, \
MemoryWriteOnly, MemoryReadWrite, RegFile, AddressMap
class chip_with_registers_cls_with_reset(RegModel):
"""
Extends the chip_with_registers_cls class adding methods to reset all the registers to
their 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 encountered')
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 encountered')
# 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 encountered')
# 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
sim = Simulator(0)
dut = chip_with_registers_cls_with_reset(callbacks=NormalCallbackSet(read_callback=sim.read,
write_callback=sim.write))
dut.reset()
Exposing User Defined Properties#
SystemRDL allows properties to be added to any component (Field, Memory, Register, Register File, Address Map), so called User Defined Properties (UDP).
There are two methods to expose user defined properties:
A list of strings to include in the package
A Regular Expression which will include any UDP which matches the regular expression
Consider the following systemRDL example with a user defined property: component_usage
enum component_type {
test_function = 0 { name = "test the design"; };
device_configuration = 1 { name = "configuring_the_device"; };
normal_use = 2 { desc="normal device usage"; };
};
property component_usage { type = component_type; component = addrmap | regfile | reg | field | mem; };
addrmap user_defined_property {
reg {
field { fieldwidth=4; component_usage=component_type::test_function; } data_loop_back;
field { fieldwidth=1; component_usage=component_type::device_configuration; } power_control;
field { fieldwidth=1; component_usage=component_type::normal_use; } led_control;
} control_register;
};
User Defined Properties are not automatically included they must be specified, as shown:
peakrdl python user_defined_properties.rdl -o . --udp component_usage
Alternatively the User Defined Properties can be included with a regular expression. In the following case all UDPs are included, except the ones used by PeakRDL python
peakrdl python user_defined_properties.rdl -o . --udp_regex "^(?!python_hide$)(?!python_name$).+"
Warning
Attempting to use both the list and regular expression approach is not supported and will generate an error
The user defined properties are stored in a udp property of all component in the generated
register access and can be accessed as follows:
"""
A demonstration of user defined properties generated with peakrdl-python
"""
from user_defined_property.reg_model import RegModel
from user_defined_property.sim_lib.dummy_callbacks import dummy_read, dummy_write
from user_defined_property.lib.callbacks import NormalCallbackSet
if __name__ == '__main__':
# create an instance of the class
regmodel = RegModel(callbacks=NormalCallbackSet(read_callback=dummy_read,
write_callback=dummy_write))
# loop through the fields in the register access model and print out the value of the
# component_usage property
for field in regmodel.control_register.readable_fields:
field_usage = field.udp['component_usage']
print(f"Control register field:{field.inst_name} has recommend usage {field_usage.name}")
Added in version 2.0.0: Regular Expression matching for User Defined Properties was added in version 2.0.0
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
addressproperty that would clash with a field of that register calledaddress
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_arather thanreg_athe name of the field will be
overridden_field_arather thanfield_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
"""
A demonstration of using overridden names for peakrdl-python
In this example there is a register which has one field. These have systemRDL names: reg_a and
field_a respectively. However the python_inst_name has been used on change the names in the
peakrdl-python register model to overridden_reg_a and overridden_field_a respectively
"""
from over_ridden_names.reg_model import RegModel
from over_ridden_names.lib import NormalCallbackSet
from over_ridden_names.sim_lib.dummy_callbacks import dummy_read
if __name__ == '__main__':
# create an instance of the class
over_ridden_names = RegModel(callbacks=NormalCallbackSet(read_callback=dummy_read))
# access the field value directly
print(over_ridden_names.overridden_reg_a.overridden_field_a.read())
# access the field 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())
Autoformatting#
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.
Simulator#
PeakRDL Python also generates an simulator, this can be used to test and develop using the
generated package. The simulator is used in a the examples shown earlier in this section. The
simulator has the option to attach a callback to the read and write operations of either a
register or field. In addition there is a value property that allows access to the register
or field content, this allows the contents to be accessed or updated without activating the
callbacks, this is intended to allow the simulator to be extended with behaviour that is not
fully described by the systemRDL.
Warning
The PeakRDL Python simulator is not intended to replace an RTL simulation of the design. It does not simulate the hardware, it is intended as a simple tool for development and testing of the python wrappers or code that uses them.
Skipping the Library Copy#
In some cases it may be desirable to skip the copy of the library from the generated pacakge. This may be useful in development of PeakRDL-python. It can also be used to avoid distributing licensed code.
Warning
If the libraries are not distributed with the package, it is very important to ensure users download the matching version of the PeakRDL-python package to use it