Compiled-in Device Tree / Platform Data

Introduction

Device tree is the standard configuration method in U-Boot. It is used to define what devices are in the system and provide configuration information to these devices.

The overhead of adding devicetree access to U-Boot is fairly modest, approximately 3KB on Thumb 2 (plus the size of the DT itself). This means that in most cases it is best to use devicetree for configuration.

However there are some very constrained environments where U-Boot needs to work. These include SPL with severe memory limitations. For example, some SoCs require a 16KB SPL image which must include a full MMC stack. In this case the overhead of devicetree access may be too great.

It is possible to create platform data manually by defining C structures for it, and reference that data in a U_BOOT_DRVINFO() declaration. This bypasses the use of devicetree completely, effectively creating a parallel configuration mechanism. But it is an available option for SPL.

As an alternative, the ‘of-platdata’ feature is provided. This converts the devicetree contents into C code which can be compiled into the SPL binary. This saves the 3KB of code overhead and perhaps a few hundred more bytes due to more efficient storage of the data.

How it works

The feature is enabled by CONFIG OF_PLATDATA. This is only available in SPL/TPL and should be tested with:

#if CONFIG_IS_ENABLED(OF_PLATDATA)

A tool called ‘dtoc’ converts a devicetree file either into a set of struct declarations, one for each compatible node, and a set of U_BOOT_DRVINFO() declarations along with the actual platform data for each device. As an example, consider this MMC node:

sdmmc: dwmmc@ff0c0000 {
        compatible = "rockchip,rk3288-dw-mshc";
        clock-freq-min-max = <400000 150000000>;
        clocks = <&cru HCLK_SDMMC>, <&cru SCLK_SDMMC>,
                 <&cru SCLK_SDMMC_DRV>, <&cru SCLK_SDMMC_SAMPLE>;
        clock-names = "biu", "ciu", "ciu_drv", "ciu_sample";
        fifo-depth = <0x100>;
        interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
        reg = <0xff0c0000 0x4000>;
        bus-width = <4>;
        cap-mmc-highspeed;
        cap-sd-highspeed;
        card-detect-delay = <200>;
        disable-wp;
        num-slots = <1>;
        pinctrl-names = "default";
        pinctrl-0 = <&sdmmc_clk>, <&sdmmc_cmd>, <&sdmmc_cd>, <&sdmmc_bus4>;
            vmmc-supply = <&vcc_sd>;
            status = "okay";
            bootph-all;
    };

Some of these properties are dropped by U-Boot under control of the CONFIG_OF_SPL_REMOVE_PROPS option. The rest are processed. This will produce the following C struct declaration:

struct dtd_rockchip_rk3288_dw_mshc {
        fdt32_t         bus_width;
        bool            cap_mmc_highspeed;
        bool            cap_sd_highspeed;
        fdt32_t         card_detect_delay;
        fdt32_t         clock_freq_min_max[2];
        struct phandle_1_arg clocks[4];
        bool            disable_wp;
        fdt32_t         fifo_depth;
        fdt32_t         interrupts[3];
        fdt32_t         num_slots;
        fdt32_t         reg[2];
        fdt32_t         vmmc_supply;
};

and the following device declarations:

/* Node /clock-controller@ff760000 index 0 */
...

/* Node /dwmmc@ff0c0000 index 2 */
static struct dtd_rockchip_rk3288_dw_mshc dtv_dwmmc_at_ff0c0000 = {
        .fifo_depth             = 0x100,
        .cap_sd_highspeed       = true,
        .interrupts             = {0x0, 0x20, 0x4},
        .clock_freq_min_max     = {0x61a80, 0x8f0d180},
        .vmmc_supply            = 0xb,
        .num_slots              = 0x1,
        .clocks                 = {{0, 456},
                                   {0, 68},
                                   {0, 114},
                                   {0, 118}},
        .cap_mmc_highspeed      = true,
        .disable_wp             = true,
        .bus_width              = 0x4,
        .u_boot_dm_pre_reloc    = true,
        .reg                    = {0xff0c0000, 0x4000},
        .card_detect_delay      = 0xc8,
};

U_BOOT_DRVINFO(dwmmc_at_ff0c0000) = {
        .name           = "rockchip_rk3288_dw_mshc",
        .plat       = &dtv_dwmmc_at_ff0c0000,
        .plat_size  = sizeof(dtv_dwmmc_at_ff0c0000),
        .parent_idx     = -1,
};

The device is then instantiated at run-time and the platform data can be accessed using:

struct udevice *dev;
struct dtd_rockchip_rk3288_dw_mshc *plat = dev_get_plat(dev);

This avoids the code overhead of converting the devicetree data to platform data in the driver. The of_to_plat() method should therefore do nothing in such a driver.

Note that for the platform data to be matched with a driver, the ‘name’ property of the U_BOOT_DRVINFO() declaration has to match a driver declared via U_BOOT_DRIVER(). This effectively means that a U_BOOT_DRIVER() with a ‘name’ corresponding to the devicetree ‘compatible’ string (after converting it to a valid name for C) is needed, so a dedicated driver is required for each ‘compatible’ string.

In order to make this a bit more flexible, the DM_DRIVER_ALIAS() macro can be used to declare an alias for a driver name, typically a ‘compatible’ string. This macro produces no code, but is used by dtoc tool. It must be located in the same file as its associated driver, ideally just after it.

The parent_idx is the index of the parent driver_info structure within its linker list (instantiated by the U_BOOT_DRVINFO() macro). This is used to support dev_get_parent().

During the build process dtoc parses both U_BOOT_DRIVER() and DM_DRIVER_ALIAS() to build a list of valid driver names and driver aliases. If the ‘compatible’ string used for a device does not not match a valid driver name, it will be checked against the list of driver aliases in order to get the right driver name to use. If in this step there is no match found a warning is issued to avoid run-time failures.

Where a node has multiple compatible strings, dtoc generates a #define to make them equivalent, e.g.:

#define dtd_rockchip_rk3299_dw_mshc dtd_rockchip_rk3288_dw_mshc

Converting of-platdata to a useful form

Of course it would be possible to use the of-platdata directly in your driver whenever configuration information is required. However this means that the driver will not be able to support devicetree, since the of-platdata structure is not available when devicetree is used. It would make no sense to use this structure if devicetree were available, since the structure has all the limitations metioned in caveats below.

Therefore it is recommended that the of-platdata structure should be used only in the probe() method of your driver. It cannot be used in the of_to_plat() method since this is not called when platform data is already present.

How to structure your driver

Drivers should always support devicetree as an option. The of-platdata feature is intended as a add-on to existing drivers.

Your driver should convert the plat struct in its probe() method. The existing devicetree decoding logic should be kept in the of_to_plat() method and wrapped with #if.

For example:

#include <dt-structs.h>

struct mmc_plat {
#if CONFIG_IS_ENABLED(OF_PLATDATA)
        /* Put this first since driver model will copy the data here */
        struct dtd_mmc dtplat;
#endif
        /*
         * Other fields can go here, to be filled in by decoding from
         * the devicetree (or the C structures when of-platdata is used).
         */
        int fifo_depth;
};

static int mmc_of_to_plat(struct udevice *dev)
{
    if (CONFIG_IS_ENABLED(OF_REAL)) {
        /* Decode the devicetree data */
        struct mmc_plat *plat = dev_get_plat(dev);
        const void *blob = gd->fdt_blob;
        int node = dev_of_offset(dev);

        plat->fifo_depth = fdtdec_get_int(blob, node, "fifo-depth", 0);
    }

    return 0;
}

static int mmc_probe(struct udevice *dev)
{
        struct mmc_plat *plat = dev_get_plat(dev);

#if CONFIG_IS_ENABLED(OF_PLATDATA)
        /* Decode the of-platdata from the C structures */
        struct dtd_mmc *dtplat = &plat->dtplat;

        plat->fifo_depth = dtplat->fifo_depth;
#endif
        /* Set up the device from the plat data */
        writel(plat->fifo_depth, ...)
}

static const struct udevice_id mmc_ids[] = {
        { .compatible = "vendor,mmc" },
        { }
};

U_BOOT_DRIVER(mmc_drv) = {
        .name           = "mmc_drv",
        .id             = UCLASS_MMC,
        .of_match       = mmc_ids,
        .of_to_plat = mmc_of_to_plat,
        .probe          = mmc_probe,
        .priv_auto = sizeof(struct mmc_priv),
        .plat_auto = sizeof(struct mmc_plat),
};

DM_DRIVER_ALIAS(mmc_drv, vendor_mmc) /* matches compatible string */

Note that struct mmc_plat is defined in the C file, not in a header. This is to avoid needing to include dt-structs.h in a header file. The idea is to keep the use of each of-platdata struct to the smallest possible code area. There is just one driver C file for each struct, that can convert from the of-platdata struct to the standard one used by the driver.

In the case where SPL_OF_PLATDATA is enabled, plat_auto is still used to allocate space for the platform data. This is different from the normal behaviour and is triggered by the use of of-platdata (strictly speaking it is a non-zero plat_size which triggers this).

The of-platdata struct contents is copied from the C structure data to the start of the newly allocated area. In the case where devicetree is used, the platform data is allocated, and starts zeroed. In this case the of_to_plat() method should still set up the platform data (and the of-platdata struct will not be present).

SPL must use either of-platdata or devicetree. Drivers cannot use both at the same time, but they must support devicetree. Supporting of-platdata is optional.

The devicetree becomes inaccessible when CONFIG_SPL_OF_PLATDATA is enabled, since the devicetree access code is not compiled in. A corollary is that a board can only move to using of-platdata if all the drivers it uses support it. There would be little point in having some drivers require the device tree data, since then libfdt would still be needed for those drivers and there would be no code-size benefit.

Build-time instantiation

Even with of-platdata there is a fair amount of code required in driver model. It is possible to have U-Boot handle the instantiation of devices at build-time, so avoiding the need for the device_bind() code and some parts of device_probe().

The feature is enabled by CONFIG_OF_PLATDATA_INST.

Here is an example device, as generated by dtoc:

/*
 * Node /serial index 6
 * driver sandbox_serial parent root_driver
*/

#include <asm/serial.h>
struct sandbox_serial_plat __attribute__ ((section (".priv_data")))
   _sandbox_serial_plat_serial = {
   .dtplat = {
      .sandbox_text_colour   = "cyan",
   },
};
#include <asm/serial.h>
u8 _sandbox_serial_priv_serial[sizeof(struct sandbox_serial_priv)]
   __attribute__ ((section (".priv_data")));
#include <serial.h>
u8 _sandbox_serial_uc_priv_serial[sizeof(struct serial_dev_priv)]
   __attribute__ ((section (".priv_data")));

DM_DEVICE_INST(serial) = {
   .driver     = DM_DRIVER_REF(sandbox_serial),
   .name       = "sandbox_serial",
   .plat_      = &_sandbox_serial_plat_serial,
   .priv_      = _sandbox_serial_priv_serial,
   .uclass     = DM_UCLASS_REF(serial),
   .uclass_priv_ = _sandbox_serial_uc_priv_serial,
   .uclass_node   = {
      .prev = &DM_UCLASS_REF(serial)->dev_head,
      .next = &DM_UCLASS_REF(serial)->dev_head,
   },
   .child_head   = {
      .prev = &DM_DEVICE_REF(serial)->child_head,
      .next = &DM_DEVICE_REF(serial)->child_head,
   },
   .sibling_node   = {
      .prev = &DM_DEVICE_REF(i2c_at_0)->sibling_node,
      .next = &DM_DEVICE_REF(spl_test)->sibling_node,
   },
   .seq_ = 0,
};

Here is part of the driver, for reference:

static const struct udevice_id sandbox_serial_ids[] = {
   { .compatible = "sandbox,serial" },
   { }
};

U_BOOT_DRIVER(sandbox_serial) = {
   .name   = "sandbox_serial",
   .id   = UCLASS_SERIAL,
   .of_match    = sandbox_serial_ids,
   .of_to_plat  = sandbox_serial_of_to_plat,
   .plat_auto   = sizeof(struct sandbox_serial_plat),
   .priv_auto   = sizeof(struct sandbox_serial_priv),
   .probe = sandbox_serial_probe,
   .remove = sandbox_serial_remove,
   .ops   = &sandbox_serial_ops,
   .flags = DM_FLAG_PRE_RELOC,
};

The DM_DEVICE_INST() macro declares a struct udevice so you can see that the members are from that struct. The private data is declared immediately above, as _sandbox_serial_priv_serial, so there is no need for run-time memory allocation. The #include lines are generated as well, since dtoc searches the U-Boot source code for the definition of struct sandbox_serial_priv and adds the relevant header so that the code will compile without errors.

The plat_ member is set to the dtv data which is declared immediately above the device. This is similar to how it would look without of-platdata-inst, but node that the dtplat member inside is part of the wider _sandbox_serial_plat_serial struct. This is because the driver declares its own platform data, and the part generated by dtoc can only be a portion of it. The dtplat part is always first in the struct. If the device has no .plat_auto field, then a simple dtv struct can be used as with this example:

static struct dtd_sandbox_clk dtv_clk_sbox = {
   .assigned_clock_rates   = 0x141,
   .assigned_clocks   = {0x7, 0x3},
};

#include <asm/clk.h>
u8 _sandbox_clk_priv_clk_sbox[sizeof(struct sandbox_clk_priv)]
   __attribute__ ((section (".priv_data")));

DM_DEVICE_INST(clk_sbox) = {
   .driver    = DM_DRIVER_REF(sandbox_clk),
   .name      = "sandbox_clk",
   .plat_     = &dtv_clk_sbox,

Here is part of the driver, for reference:

static const struct udevice_id sandbox_clk_ids[] = {
   { .compatible = "sandbox,clk" },
   { }
};

U_BOOT_DRIVER(sandbox_clk) = {
   .name       = "sandbox_clk",
   .id         = UCLASS_CLK,
   .of_match   = sandbox_clk_ids,
   .ops        = &sandbox_clk_ops,
   .probe      = sandbox_clk_probe,
   .priv_auto  = sizeof(struct sandbox_clk_priv),
};

You can see that dtv_clk_sbox just has the devicetree contents and there is no need for the dtplat separation, since the driver has no platform data of its own, besides that provided by the devicetree (i.e. no .plat_auto field).

The doubly linked lists are handled by explicitly declaring the value of each node, as you can see with the .prev and .next values in the example above. Since dtoc knows the order of devices it can link them into the appropriate lists correctly.

One of the features of driver model is the ability for a uclass to have a small amount of private data for each device in that uclass. This is used to provide a generic data structure that the uclass can use for all devices, thus allowing generic features to be implemented in common code. An example is I2C, which stores the bus speed there.

Similarly, parent devices can have data associated with each of their children. This is used to provide information common to all children of a particular bus. For an I2C bus, this is used to store the I2C address of each child on the bus.

This is all handled automatically by dtoc:

#include <asm/i2c.h>
u8 _sandbox_i2c_priv_i2c_at_0[sizeof(struct sandbox_i2c_priv)]
   __attribute__ ((section (".priv_data")));
#include <i2c.h>
u8 _sandbox_i2c_uc_priv_i2c_at_0[sizeof(struct dm_i2c_bus)]
   __attribute__ ((section (".priv_data")));

DM_DEVICE_INST(i2c_at_0) = {
   .driver      = DM_DRIVER_REF(sandbox_i2c),
   .name      = "sandbox_i2c",
   .plat_   = &dtv_i2c_at_0,
   .priv_      = _sandbox_i2c_priv_i2c_at_0,
   .uclass   = DM_UCLASS_REF(i2c),
   .uclass_priv_ = _sandbox_i2c_uc_priv_i2c_at_0,
  ...

Part of driver, for reference:

static const struct udevice_id sandbox_i2c_ids[] = {
   { .compatible = "sandbox,i2c" },
   { }
};

U_BOOT_DRIVER(sandbox_i2c) = {
   .name   = "sandbox_i2c",
   .id   = UCLASS_I2C,
   .of_match = sandbox_i2c_ids,
   .ops   = &sandbox_i2c_ops,
   .priv_auto   = sizeof(struct sandbox_i2c_priv),
};

Part of I2C uclass, for reference:

UCLASS_DRIVER(i2c) = {
   .id         = UCLASS_I2C,
   .name       = "i2c",
   .flags      = DM_UC_FLAG_SEQ_ALIAS,
   .post_bind  = i2c_post_bind,
   .pre_probe  = i2c_pre_probe,
   .post_probe = i2c_post_probe,
   .per_device_auto   = sizeof(struct dm_i2c_bus),
   .per_child_plat_auto   = sizeof(struct dm_i2c_chip),
   .child_post_bind = i2c_child_post_bind,
};

Here, _sandbox_i2c_uc_priv_i2c_at_0 is required by the uclass but is declared in the device, as required by driver model. The required header file is included so that the code will compile without errors. A similar mechanism is used for child devices, but is not shown by this example.

It would not be that useful to avoid binding devices but still need to allocate uclasses at runtime. So dtoc generates uclass instances as well:

struct list_head uclass_head = {
   .prev = &DM_UCLASS_REF(serial)->sibling_node,
   .next = &DM_UCLASS_REF(clk)->sibling_node,
};

DM_UCLASS_INST(clk) = {
   .uc_drv      = DM_UCLASS_DRIVER_REF(clk),
   .sibling_node   = {
      .prev = &uclass_head,
      .next = &DM_UCLASS_REF(i2c)->sibling_node,
   },
   .dev_head   = {
      .prev = &DM_DEVICE_REF(clk_sbox)->uclass_node,
      .next = &DM_DEVICE_REF(clk_fixed)->uclass_node,
   },
};

At the top is the list head. Driver model uses this on start-up, instead of creating its own.

Below that are a set of DM_UCLASS_INST() macros, each declaring a struct uclass. The doubly linked lists work as for devices.

All private data is placed into a .priv_data section so that it is contiguous in the resulting output binary.

Indexes

U-Boot stores drivers, devices and many other things in linker_list structures. These are sorted by name, so dtoc knows the order that they will appear when the linker runs. Each driver_info / udevice is referenced by its index in the linker_list array, called ‘idx’ in the code.

When CONFIG_OF_PLATDATA_INST is enabled, idx is the udevice index, otherwise it is the driver_info index. In either case, indexes are used to reference devices using device_get_by_ofplat_idx(). This allows phandles to work as expected.

Phases

U-Boot operates in several phases, typically TPL, SPL and U-Boot proper. The latter does not use dtoc.

In some rare cases different drivers are used for two phases. For example, in TPL it may not be necessary to use the full PCI subsystem, so a simple driver can be used instead.

This works in the build system simply by compiling in one driver or the other (e.g. PCI driver + uclass for SPL; simple_bus for TPL). But dtoc has no way of knowing which code is compiled in for which phase, since it does not inspect Makefiles or dependency graphs.

So to make this work for dtoc, we need to be able to explicitly mark drivers with their phase. This is done by adding a macro to the driver:

/* code in tpl.c only compiled into TPL */
U_BOOT_DRIVER(pci_x86) = {
   .name   = "pci_x86",
   .id   = UCLASS_SIMPLE_BUS,
   .of_match = of_match_ptr(tpl_fake_pci_ids),
   DM_PHASE(tpl)
};


/* code in pci_x86.c compiled into SPL and U-Boot proper */
U_BOOT_DRIVER(pci_x86) = {
   .name   = "pci_x86",
   .id   = UCLASS_PCI,
   .of_match = pci_x86_ids,
   .ops   = &pci_x86_ops,
};

Notice that the second driver has the same name but no DM_PHASE(), so it will be used for SPL and U-Boot.

Note also that this only affects the code generated by dtoc. You still need to make sure that only the required driver is build into each phase.

Header files

With OF_PLATDATA_INST, dtoc must include the correct header file in the generated code for any structs that are used, so that the code will compile. For example, if struct ns16550_plat is used, the code must include the ns16550.h header file.

Typically dtoc can detect the header file needed for a driver by looking for the structs that it uses. For example, if a driver as a .priv_auto that uses struct ns16550_plat, then dtoc can search header files for the definition of that struct and use the file.

In some cases, enums are used in drivers, typically with the .data field of struct udevice_id. Since dtoc does not support searching for these, you must use the DM_HDR() macro to tell dtoc which header to use. This works as a macro included in the driver definition:

static const struct udevice_id apl_syscon_ids[] = {
   { .compatible = "intel,apl-punit", .data = X86_SYSCON_PUNIT },
   { }
};

U_BOOT_DRIVER(intel_apl_punit) = {
   .name       = "intel_apl_punit",
   .id         = UCLASS_SYSCON,
   .of_match   = apl_syscon_ids,
   .probe      = apl_punit_probe,
   DM_HEADER(<asm/cpu.h>)    /* for X86_SYSCON_PUNIT */
};

Problems

This section shows some common problems and how to fix them.

Driver not found

In some cases you will you see something like this:

WARNING: the driver rockchip_rk3188_grf was not found in the driver list

The driver list is a list of drivers, each with a name. The name is in the U_BOOT_DRIVER() declaration, repeated twice, one in brackets and once as the .name member. For example, in the following declaration the driver name is rockchip_rk3188_grf:

U_BOOT_DRIVER(rockchip_rk3188_grf) = {
     .name = "rockchip_rk3188_grf",
     .id = UCLASS_SYSCON,
     .of_match = rk3188_syscon_ids + 1,
     .bind = rk3188_syscon_bind_of_plat,
};

The first name U_BOOT_DRIVER(xx) is used to create a linker symbol so that the driver can be accessed at build-time without any overhead. The second one (.name = “xx”) is used at runtime when something wants to print out the driver name.

The dtoc tool expects to be able to find a driver for each compatible string in the devicetree. For example, if the devicetree has:

grf: grf@20008000 {
   compatible = "rockchip,rk3188-grf", "syscon";
   reg = <0x20008000 0x200>;
   bootph-pre-ram;
};

then dtoc looks at the first compatible string (“rockchip,rk3188-grf”), converts that to a C identifier (rockchip_rk3188_grf) and then looks for that.

Missing .compatible or Missing .id

Various things can cause dtoc to fail to find the driver and it tries to warn about these. For example:

rockchip_rk3188_uart: Missing .compatible in drivers/serial/serial_rockchip.c
                 : WARNING: the driver rockchip_rk3188_uart was not found in the driver list

Without a compatible string a driver cannot be used by dtoc, even if the compatible string is not actually needed at runtime.

If the problem is simply that there are multiple compatible strings, the DM_DRIVER_ALIAS() macro can be used to tell dtoc about this and avoid a problem.

Checks are also made to confirm that the referenced driver has a .compatible member and a .id member. The first provides the array of compatible strings and the second provides the uclass ID.

Missing parent

When a device is used, its parent must be present as well. If you see an error like:

Node '/i2c@0/emul/emul0' requires parent node '/i2c@0/emul' but it is not in
   the valid list

it indicates that you are using a node whose parent is not present in the devicetree. In this example, if you look at the device tree output (e.g. fdtdump tpl/u-boot-tpl.dtb in your build directory), you may see something like this:

emul {
    emul0 {
        compatible = "sandbox,i2c-rtc-emul";
        #emul-cells = <0x00000000>;
        phandle = <0x00000003>;
    };
};

In this example, ‘emul0’ exists but its parent ‘emul’ has no properties. These have been dropped by fdtgrep in an effort to reduce the devicetree size. This indicates that the two nodes have different phase settings. Looking at the source .dts:

i2c_emul: emul {
   bootph-pre-ram;
   reg = <0xff>;
   compatible = "sandbox,i2c-emul-parent";
   emul0: emul0 {
      bootph-all;
      compatible = "sandbox,i2c-rtc-emul";
      #emul-cells = <0>;
   };
};

you can see that the child node ‘emul0’ usees ‘bootph-all’, indicating that the node is present in all SPL builds, but its parent uses ‘bootph-pre-ram’ indicating it is only present in SPL, not TPL. For a TPL build, this will fail with the above message. The fix is to change ‘emul0’ to use the same ‘bootph-pre-ram’ condition, so that it is not present in TPL, like its parent.

Caveats

There are various complications with this feature which mean it should only be used when strictly necessary, i.e. in SPL with limited memory. Notable caveats include:

  • Device tree does not describe data types. But the C code must define a type for each property. These are guessed using heuristics which are wrong in several fairly common cases. For example an 8-byte value is considered to be a 2-item integer array, and is byte-swapped. A boolean value that is not present means ‘false’, but cannot be included in the structures since there is generally no mention of it in the devicetree file.

  • Naming of nodes and properties is automatic. This means that they follow the naming in the devicetree, which may result in C identifiers that look a bit strange.

  • It is not possible to find a value given a property name. Code must use the associated C member variable directly in the code. This makes the code less robust in the face of devicetree changes. To avoid having a second struct with similar members and names you need to explicitly declare it as an alias with DM_DRIVER_ALIAS().

  • The platform data is provided to drivers as a C structure. The driver must use the same structure to access the data. Since a driver normally also supports devicetree it must use #ifdef to separate out this code, since the structures are only available in SPL. This could be fixed fairly easily by making the structs available outside SPL, so that IS_ENABLED() could be used.

  • With CONFIG_OF_PLATDATA_INST all binding happens at build-time, meaning that (by default) it is not possible to call device_bind() from C code. This means that all devices must have an associated devicetree node and compatible string. For example if a GPIO device currently creates child devices in its bind() method, it will not work with CONFIG_OF_PLATDATA_INST. Arguably this is bad practice anyway and the devicetree binding should be updated to declare compatible strings for the child devices. It is possible to disable OF_PLATDATA_NO_BIND but this is not recommended since it increases code size.

Internals

Generated files

When enabled, dtoc generates the following five files:

include/generated/dt-decl.h (OF_PLATDATA_INST only)

Contains declarations for all drivers, devices and uclasses. This allows any struct udevice, struct driver or struct uclass to be located by its name

include/generated/dt-structs-gen.h

Contains the struct definitions for the devicetree nodes that are used. This is the same as without OF_PLATDATA_INST

spl/dts/dt-plat.c (only with !OF_PLATDATA_INST)

Contains the U_BOOT_DRVINFO() declarations that U-Boot uses to bind devices at start-up. See above for an example

spl/dts/dt-device.c (only with OF_PLATDATA_INST)

Contains DM_DEVICE_INST() declarations for each device that can be used at run-time. These are declared in the file along with any private/platform data that they use. Every device has an idx, as above. Since each device must be part of a double-linked list, the nodes are declared in the code as well.

spl/dts/dt-uclass.c (only with OF_PLATDATA_INST)

Contains DM_UCLASS_INST() declarations for each uclass that can be used at run-time. These are declared in the file along with any private data associated with the uclass itself (the .priv_auto member). Since each uclass must be part of a double-linked list, the nodes are declared in the code as well.

The dt-structs.h file includes the generated file (include/generated/dt-structs.h) if CONFIG_SPL_OF_PLATDATA is enabled. Otherwise (such as in U-Boot proper) these structs are not available. This prevents them being used inadvertently. All usage must be bracketed with #if CONFIG_IS_ENABLED(OF_PLATDATA).

The dt-plat.c file contains the device declarations and is is built in spl/dt-plat.c.

CONFIG options

Several CONFIG options are used to control the behaviour of of-platdata, all available for both SPL and TPL:

OF_PLATDATA

This is the main option which enables the of-platdata feature

OF_PLATDATA_PARENT

This allows device_get_parent() to work. Without this, all devices exist as direct children of the root node. This option is highly desirable (if not always absolutely essential) for buses such as I2C.

OF_PLATDATA_INST

This controls the instantiation of devices at build time. With it disabled, only U_BOOT_DRVINFO() records are created, with U-Boot handling the binding in device_bind() on start-up. With it enabled, only DM_DEVICE_INST() and DM_UCLASS_INST() records are created, and device_bind() is not needed at runtime.

OF_PLATDATA_NO_BIND

This controls whether device_bind() is supported. It is enabled by default with OF_PLATDATA_INST since code-size reduction is really the main point of the feature. It can be disabled if needed but is not likely to be supported in the long term.

OF_PLATDATA_DRIVER_RT

This controls whether the struct driver_rt records are used by U-Boot. Normally when a device is bound, U-Boot stores the device pointer in one of these records. There is one for every struct driver_info in the system, i.e. one for every device that is bound from those records. It provides a way to locate a device in the code and is used by device_get_by_ofplat_idx(). This option is always enabled with of-platdata, provided OF_PLATDATA_INST is not. In that case the records are useless since we don’t have any struct driver_info records.

OF_PLATDATA_RT

This controls whether the struct udevice_rt records are used by U-Boot. It moves the updatable fields from struct udevice (currently only flags) into a separate structure, allowing the records to be kept in read-only memory. It is generally enabled if OF_PLATDATA_INST is enabled. This option also controls whether the private data is used in situ, or first copied into an allocated region. Again this is to allow the private data declared by dtoc-generated code to be in read-only memory. Note that access to private data must be done via accessor functions, such as dev_get_priv(), so that the relocation is handled.

READ_ONLY

This indicates that the data generated by dtoc should not be modified. Only a few fields actually do get changed in U-Boot, such as device flags. This option causes those to move into an allocated space (see OF_PLATDATA_RT). Also, since updating doubly linked lists is generally impossible when some of the nodes cannot be updated, OF_PLATDATA_NO_BIND is enabled.

Data structures

A few extra data structures are used with of-platdata:

struct udevice_rt

Run-time information for devices. When OF_PLATDATA_RT is enabled, this holds the flags for each device, so that struct udevice can remain unchanged by U-Boot, and potentially reside in read-only memory. Access to flags is then via functions like dev_get_flags() and dev_or_flags(). This data structure is allocated on start-up, where the private data is also copied. All flags values start at 0 and any changes are handled by dev_or_flags() and dev_bic_flags(). It would be more correct for the flags to be set to DM_FLAG_BOUND, or perhaps DM_FLAG_BOUND | DM_FLAG_ALLOC_PDATA, but since there is no code to bind/unbind devices and no code to allocate/free private data / platform data, it doesn’t matter.

struct driver_rt

Run-time information for struct driver_info records. When OF_PLATDATA_DRIVER_RT is enabled, this holds a pointer to the device created by each record. This is needed so that is it possible to locate a device from C code. Specifically, the code can use DM_DRVINFO_GET(name) to get a reference to a particular struct driver_info, with name being the name of the devicetree node. This is very convenient. It is also fast, since no searching or string comparison is needed. This data structure is allocated on start-up, filled out by device_bind() and used by device_get_by_ofplat_idx().

Other changes

Some other changes are made with of-platdata:

Accessor functions

Accessing private / platform data via functions such as dev_get_priv() has always been encouraged. With OF_PLATDATA_RT this is essential, since the priv_ and plat_ (etc.) values point to the data generated by dtoc, not the read-write copy that is sometimes made on start-up. Changing the private / platform data pointers has always been discouraged (the API is marked internal) but with OF_PLATDATA_RT this is not currently supported in general, since it assumes that all such pointers point to the relocated data. Note also that the renaming of struct members to have a trailing underscore was partly done to make people aware that they should not be accessed directly.

gd->uclass_root_s

Normally U-Boot sets up the head of the uclass list here and makes gd->uclass_root point to it. With OF_PLATDATA_INST, dtoc generates a declaration of uclass_head in dt-uclass.c since it needs to link the head node into the list. In that case, gd->uclass_root_s is not used and U-Boot just makes gd->uclass_root point to uclass_head.

gd->dm_driver_rt

This holds a pointer to a list of struct driver_rt records, one for each struct driver_info. The list is in alphabetical order by the name used in U_BOOT_DRVINFO(name) and indexed by idx, with the first record having an index of 0. It is only used if OF_PLATDATA_INST is not enabled. This is accessed via macros so that it can be used inside IS_ENABLED(), rather than requiring #ifdefs in the C code when it is not present.

gd->dm_udevice_rt

This holds a pointer to a list of struct udevice_rt records, one for each struct udevice. The list is in alphabetical order by the name used in DM_DEVICE_INST(name) (a C version of the devicetree node) and indexed by idx, with the first record having an index of 0. It is only used if OF_PLATDATA_INST is enabled. This is accessed via macros so that it can be used inside IS_ENABLED(), rather than requiring #ifdefs in the C code when it is not present.

gd->dm_priv_base

When OF_PLATDATA_RT is enabled, the private/platform data for each device is copied into an allocated region by U-Boot on start-up. This points to that region. All calls to accessor functions (e.g. dev_get_priv()) then translate from the pointer provided by the caller (assumed to lie between __priv_data_start and __priv_data_end) to the new allocated region. This member is accessed via macros so that it can be used inside IS_ENABLED(), rather than required #ifdefs in the C code when it is not present.

struct udevice->flags_

When OF_PLATDATA_RT is enabled, device flags are no-longer part of struct udevice, but are instead kept in struct udevice_rt, as described above. Flags are accessed via functions, such as dev_get_flags() and dev_or_flags().

struct udevice->node_

When OF_PLATDATA is enabled, there is no devicetree at runtime, so no need for this field. It is removed, just to save space.

DM_PHASE

This macro is used to indicate which phase of U-Boot a driver is intended for. See above for details.

DM_HDR

This macro is used to indicate which header file dtoc should use to allow a driver declaration to compile correctly. See above for details.

device_get_by_ofplat_idx()

There used to be a function called device_get_by_driver_info() which looked up a struct driver_info pointer and returned the struct udevice that was created from it. It was only available for use with of-platdata. This has been removed in favour of device_get_by_ofplat_idx() which uses idx, the index of the struct driver_info or struct udevice in the linker_list. Similarly, the struct phandle_0_arg (etc.) structs have been updated to use this index instead of a pointer to struct driver_info.

DM_DRVINFO_GET

This has been removed since we now use indexes to obtain a driver from struct phandle_0_arg and the like.

Two-pass binding

The original of-platdata tried to order U_BOOT_DRVINFO() in the generated files so as to have parents declared ahead of children. This was convenient as it avoided any special code in U-Boot. With OF_PLATDATA_INST this does not work as the idx value relies on using alphabetical order for everything, so that dtoc and U-Boot’s linker_lists agree on the idx value. Devices are then bound in order of idx, having no regard to parent/child relationships. For this reason, device binding now hapens in multiple passes, with parents being bound before their children. This is important so that children can find their parents in the bind() method if needed.

Root device

The root device is generally bound by U-Boot but with OF_PLATDATA_INST it cannot be, since binding needs to be done at build time. So in this case dtoc sets up a root device using DM_DEVICE_INST() in dt-device.c and U-Boot makes use of that. When OF_PLATDATA_INST is not enabled, U-Boot generally ignores the root node and does not create a U_BOOT_DRVINFO() record for it. This means that the idx numbers used by struct driver_info (when OF_PLATDATA_INST is disabled) and the idx numbers used by struct udevice (when OF_PLATDATA_INST is enabled) differ, since one has a root node and the other does not. This does not actually matter, since only one of them is actually used for any particular build, but it is worth keeping in mind if comparing index values and switching OF_PLATDATA_INST on and off.

__priv_data_start and __priv_data_end

The private/platform data declared by dtoc is all collected together in a linker section and these symbols mark the start and end of it. This allows U-Boot to relocate the area to a new location if needed (with OF_PLATDATA_RT)

dm_priv_to_rw()

This function converts a private- or platform-data pointer value generated by dtoc into one that can be used by U-Boot. It is a NOP unless OF_PLATDATA_RT is enabled, in which case it translates the address to the relocated region. See above for more information.

The dm_populate_phandle_data() function that was previous needed has now been removed, since dtoc can address the drivers directly from dt-plat.c and does not need to fix up things at runtime.

The pylibfdt Python module is used to access the devicetree.

Credits

This is an implementation of an idea by Tom Rini <trini@konsulko.com>.

Future work

  • Consider programmatically reading binding files instead of devicetree contents

  • Allow IS_ENABLED() to be used in the C code instead of #if