Skip to content

Guidance on Altering Hardware Design and Device Tree

Joseph Nobes edited this page Aug 14, 2025 · 18 revisions

LOKI - Guidance on Altering Hardware Design

Summary

#TODO What you might need to do- nothing, pinmaps, even custom firmware

Changing Constraints (Pinouts)

#TODO

Adding Custom Hardware

#TODO

Modifying the LOKI Device Tree

Much of the device tree is auto-generated when you add IP to the hardware design (it is imported and processed by the PetaLinux tools from the XSA file). Therefore, in the first instance try building the project without further modification to see what appears automatically (check the loki/os/petalinux-custom/images/linux/system.dts file after a completed build.

However, you may still want to add nodes to provide additional configuration. One common reason for this is to add custom pin names to AXI GPIO.

Step 1 - Create a new device tree appending recipe, and a custom DTS Include file

The device tree is compiled by PetaLinux, and much of its configuration is either automatic based on information from the XSA, or from information in the core LOKI layer. You will be adding information to the tree, and slightly modifying the recipe with a .bbappend file, which simply adds on to the original.

Create the new recipe in this specific location:

mkdir $YOURLAYERPATH/recipes-bsp/
mkdir $YOURLAYERPATH/recipes-bsp/device-tree
touch $YOURLAYERPATH/recipes-bsp/device-tree/device-tree.bbappend
mkdir $YOURLAYERPATH/recipes-bsp/device-tree/files

Now, create a file that will contain your modifications to the device tree. This example will be for modifying a pin name.

touch $YOURLAYERPATH/recipes-bsp/device-tree/files/custom-pins.dtsi

Now we can populate the contents of the append file to include this DTSI (DTS include) file.

# Contents of device-tree.bbappend
FILESEXTRAPATHS_prepend := "${THISDIR}/files:"

SRC_URI += "file://custom-pins.dtsi \
            "

do_configure_append() {
    # Add an include line to the base device tree for our extra definition
    sed -i '2i /include/ "custom-pins.dtsi"' ${WORKDIR}/system-user.dtsi
}

Note

This line will include the custom DTSI file after the main system one, meaning the modifications in the LOKI DTSI will be applied after it, and as such will override it.

To apply your changes last and override everything otherwise generated, use this line instead: sed -i -e '$a/include/ "<YOUR FILENAME HERE>"' ${WORKDIR}/system-user.dtsi

Step 2 - Add the Device Tree Modification

The new .dsti file will allow you to modify the device tree using the standard DTS include rules. Note that this will be based on a tree you might not have seen, so it is easier if you have already compiled and checked the contents of the system.dts output file to get an idea of what to expect.

There are two main ways to refer to and modify parts of the device tree. Your .dtsi file can use both for different parts. Generally using the symbol name (method 2) is more robust to changes and is therefore recommended, but you must make note of hierarchy.

Method 1 - Direct Referencing with Address

Using this method, you must specify exactly where in the device tree the node will end up. This also requires that you know the memory mapped base address of the peripheral in question (this can be looked up in the address editor in Vivado). Note that if the address changes, the build will break.

In this example, we have a new AXI GPIO peripheral that will appear under the amba_pl@0 node, and we know it will appear with a node named gpio@80010000. Therefore, we can create our new addon field gpio-line-names, which will sit alongside any automatically generated fields from the processing:

/ {
  amba_pl@0 {
     gpio@80010000 {
       gpio-line-names = "TEST_PIN_1","TEST_PIN_2","TEST_PIN_3";
     };
  };
};

This will name the first three pins on the bus.

The first and last lines are very important. Any contents in this file goes between / { on the first line and }; on the last.

Warning

The location in the tree is very important and can change. For example, old systems using the 2020 tools often placed the GPIO peripheral under /amba_pl or amba. In 2023 it has been found to end up in /axi.

The best way to discover where to put your modifications is to look at the output tree of your system first, either the .dts file created during the build or by using the SYSFS on a booted system at /sys/firmware/devicetree/base/<path>.

Method 2 - Referencing by Symbol

The more robust method is to reference the symbol that is automatically generated by the toolchain, which makes for a simpler .dtsi file that will work even if the address of the IP changes. The only complication is that if you don't want to build first and use the .dts to determine the name (see above) pre-empting the name is more complicated as it depends on component hierarchy. If the block is in the top-level design it is simply the IP block name.

The symbol generated will be in the following format: <sub block(s) in hierarchy>_<IP block name>.

The hierarchy can be found easily by looking at the address editor in Vivado.

For example, take a design where there is pulse_generator IP, and the top-level design contains a sub-block called pulse_gen. This block in turn contains three of the pulse_generator IP blocks called pulse_generator_[1:3].

Looking at the address editor:

loki_address_edit_vivado

In this case the block in the device tree for the first block will be called pulse_gen_pulse_generator_1. Its modification in the device tree (in this case to add a UIO definition, see below) will look like this:

/ {
};

&pulse_gen_pulse_generator_1 {
  compatible = "generic-uio";
};

Note that this block is added outside of the root / {}; part of the tree, in contrast to method 1.

Step 3 - Build the Image

Use make as normal to build the image. When it is done, you can check the output text device tree file at loki/os/petalinux-custom/images/linux/system.dts, where the modifications you have made should appear.

When the image is loaded onto a board, you should be able to see your modifications:

# gpioinfo
gpiochip0 - 32 lines:
        line   0: "TEST_PIN_1"       unused input active-high
        line   1: "TEST_PIN_2"       unused   input  active-high
        line   2: "TEST_PIN_3"       unused   input  active-high

Software Drivers for Custom IP

The structure of the IP information received from the hardware build is as follows:

.
├── data
│   ├── fw_sw_mux.mdd
│   └── fw_sw_mux.tcl
└── src
    ├── fw_sw_mux.c
    ├── fw_sw_mux.h
    ├── fw_sw_mux_selftest.c
    └── Makefile

These files do not require manual import; they will be included in the XSA hardware export, and will be imported into the PetaLinux build automatically (to hw-description/drivers/).

Device Tree Modifications

Without any further intervention, the device tree eventually compiled will already include entries for the IP include in the hardware design like this:

fw_sw_mux@80050000 {
	clock-names = "s00_axi_aclk";
	clocks = <0x03 0x47>;
	compatible = "xlnx,fw-sw-mux-1.1";
	reg = <0x00 0x80050000 0x00 0x10000>;
	xlnx,s00-axi-addr-width = <0x04>;
	xlnx,s00-axi-data-width = <0x20>;
	phandle = <0x80>;
};

Note the compatible string

To expose this to userspace, it is typical to use UIO ('Userspace IO'). For this to work, the device must be initialised as a UIO device, which requires a change to the compatible string in the DTSI device tree input file in the PetaLinux build.

In addition, the UIO system needs to be enabled in the PetaLinux kernel configuration (this was actually already the case), and an additional line needs adding to the bootargs, which are also in the DTSI file:

/ {
  chosen {
    bootargs = "earlycon clk_ignore_unused   uio_pdrv_genirq.of_id=generic-uio";
    xlnx,eeprom = &eeprom;
  };
};

&fw_sw_mux_0 {
  compatible = "generic-uio";
};
  
&fw_sw_mux_1 {
  compatible = "generic-uio";
};

The compatible string must be changed for every instance of the IP. This will fail if the IP was not included in the hardware design.

Tip

The name of the block (e.g. fw_sw_mux_0) matches the name of the IP block and any blocks higher in the hierarchy in Vivado. If you have a name that is not found in the hardware design, the OS build will fail at the point it tries to compile the device tree. See 'Method 2' above to see how to determine this name.

This will result in an updated final device tree:

fw_sw_mux@80050000 {
	clock-names = "s00_axi_aclk";
	clocks = <0x03 0x47>;
	compatible = "generic-uio";
	reg = <0x00 0x80050000 0x00 0x10000>;
	xlnx,s00-axi-addr-width = <0x04>;
	xlnx,s00-axi-data-width = <0x20>;
	phandle = <0x80>;
};

Probing UIO Devices

Once UIO devices are set up, they will appear in the system under /dev/uioN.

To find out more information about each device (for example, which IP it is associated with), you can use the SYSFS:

root@petalinux-custom: ~ # cat /sys/class/uio/uio0/name  
fw_sw_mux
root@petalinux-custom: ~ # cat /sys/class/uio/uio0/maps/map0/
addr    name    offset  size
root@petalinux-custom: ~ # cat /sys/class/uio/uio0/maps/map0/*
0x0000000080040000
fw_sw_mux@80040000
0x0
0x0000000000010000

Digilent's PWM as an Example - C Drivers

Digilent produces a firmware called PWM, which can be found here with the corresponding data and src directories build into their system. Remember that this does not require manual import- it will have appeared from the XSA import.

The entry in the device tree (same repo) DTSI is:

&PWM_0 {
	compatible = "generic-uio";
};

The recipe, which can be found here installs a library from the separate Digilent libpwm repository, which implements control using UIO. As is mentioned in the README, this relies on the use of libuio, and requires that the person using the driver knows the correct UIO number and UIO map number.

Python UIO Drivers

Once the UIO device has been installed, it can be accessed in userspace. To do this in the LOKI project, drivers are being written using the py-uio module. This modules allows for the easy access to device registers, which can be mapped with the use of ctypes structures. For example, for the fw_sw_mux:

from uio.utils import fix_ctypes_struct
from ctypes import c_uint8, c_uint16, c_uint32

@fix_ctypes_struct
class _fw_sw_mux_Cfg( ctypes.Structure):
    _fields_ = [
        ("REG0", c_uint32),
        # 1 bit per output, 32 bits. 0 is Software control, 1 is firmware control.

        ("REG1", c_uint32),
        # Reserved

        ("REG2", c_uint32),
        # Reserved

        ("REG3", c_uint32),
        # Reserved
    ]

These fields can then be mapped onto a UIO device:

from uio.device import Uio

class fw_sw_mux():
    def __init__(self, path):
        uio_device = Uio(path)
        self.cfg = uio_device.map(_fw_sw_mux_Cfg)

From here, the fields can be directly written and read. For instantiation, you need only the UIO number of the device.

In this example, you could instantiate the driver and set the value of the register 0 like so:

FSM = fw_sw_mux('/dev/uio0')
FSM.cfg.REG0 = 0xFEED

From here, it is trivial to build up further layers of the driver with more advanced functionality. When operating on parts of registers rather than the full value, read-modify-write processes will be needed.

Tip

You can also nest structures.

This can be useful if your register map includes several repeated parts for different 'channels'. For example, if the end of the above register map then had a repeating set of paired registers, you could define the pair:

@fix_ctypes_struct
class _settings_pair( ctypes.structure):
    _fields_ = [
        ("pair_reg0", c_uint32),
        ("pair_reg1", c_uint32),
    ]

Then include a number of these in the original map:

    _fields_ = [
        ...
        ("channels", _settings_pair * 5),
        ...
    ]

From here, access would be via FSM.cfg.channels[<0/1/2/3/4>].pair_reg0.

Note

If your driver is running on LOKI, the py-uio module will need to be installed. A Yocto recipe has already been included for this, so you must simply specify py-uio in the RDEPENDS of your driver's recipe.

The full driver from this example can be found in the GARUD repository.

Clone this wiki locally