Device Tree Match Style Mixing- Free Linux Device Drivers Course

Device Tree Match Style Mixing

Free Linux Kernel Development Course — Device Tree Chapter, Lecture 12

Kernel 6.x Verified
Original Driver Code
Hands-On Lab
device tree match style mixing
free linux kernel development course
free embedded systems course
free linux device drivers course
free embedded linux course
of_device_id platform_device_id

What You Will Learn

This lecture is part of our free Linux kernel development course and continues the Device Tree chapter of our free Linux device drivers course. Device tree match style mixing is the technique of writing one platform driver that can be probed either through a device tree compatible string or through a legacy platform_device_id table, so the same driver binary works on both modern DT-based boards and older hard-coded board files. By the end of this lecture you will be able to build a dual-match driver, write the fallback logic that picks device-specific data from whichever match table fired, and test it on a current kernel 6.x system.

  • Why a driver needs two match tables
  • of_device_id vs platform_device_id fallback logic
  • Original ep_dualmatch_demo driver
  • Testing on a DT board and a legacy board
  • Common mistakes and best practices

Prerequisites

This lecture builds directly on earlier lectures in this free embedded Linux course: platform driver basics, the of_device_id table and of_match_ptr() macro, and the platform_device_id table used for legacy board-file matching. You should be comfortable writing a probe function, building and loading a kernel module with insmod, and reading dmesg output. A kernel 6.x build environment (kernel headers plus a cross toolchain, or a QEMU virtual board) is assumed.

What Is Match Style Mixing In Device Tree Drivers

A platform driver can be discovered by more than one matching mechanism at the same time. Most drivers today register an of_device_id table for device tree matching, but many real-world drivers still ship a platform_device_id table alongside it. This is not redundant — it lets one driver source file support two very different deployment styles: a modern SoC board that boots from a device tree blob, and an older or simpler board that still registers its platform device by name from board file C code. Device tree match style mixing is simply the practice of keeping both tables in the same driver and writing probe code that works no matter which one matched.

Why Combine of_device_id And platform_device_id

In a pure device tree world you would only need the of_device_id table. In practice, driver maintainers keep the id table too for a few concrete reasons:

  • Backward compatibility — older boards or vendor SDKs that have not migrated to device tree can still bind the same driver by device name.
  • Unit testing — a driver can be instantiated in a test harness with platform_device_register_simple() without needing a DT overlay.
  • Bus core requirement — the platform bus core falls back to name-based matching automatically when DT and ACPI both fail, so having an id table lets you control that fallback explicitly instead of relying on driver name matching.

Platform Bus Match Order On Kernel 6.x

1. Device Tree compatible string (of_device_id)
2. ACPI identifier, on ACPI-based systems
3. platform_device_id table (id_table)
4. Plain driver-name string match (last resort)

Because the core already tries these in order, a driver only has to check which one actually matched inside probe() and pick the right private data from there.

The Fallback Pattern: DT First, Then Legacy ID Table

The fallback pattern has one simple rule: call of_match_device() first. If it returns a non-NULL entry, the device was matched through device tree and its .data pointer holds the per-variant configuration. If it returns NULL, fall back to platform_get_device_id(), which returns the matched platform_device_id entry, and read .driver_data from that instead.

Building The Two Match Tables

The example driver in this lecture, ep_dualmatch_demo, models a small family of LED controller chips. Two variants exist: a basic single-channel controller and an RGB three-channel controller. Each variant needs a different maximum brightness value and channel count, so we store that configuration in a shared array and point both match tables at the same entries.

enum ep_led_variant {
    EP_LED_BASIC,
    EP_LED_RGB,
};

struct ep_led_config {
    const char *label;
    u8 channels;
    u16 max_brightness;
};

static const struct ep_led_config ep_led_configs[] = {
    [EP_LED_BASIC] = { .label = "basic",  .channels = 1, .max_brightness = 255  },
    [EP_LED_RGB]   = { .label = "rgb",    .channels = 3, .max_brightness = 1023 },
};

The device tree match table points its .data field at the corresponding array entry:

static const struct of_device_id ep_dualmatch_of_ids[] = {
    { .compatible = "ep,led-basic", .data = &ep_led_configs[EP_LED_BASIC] },
    { .compatible = "ep,led-rgb",   .data = &ep_led_configs[EP_LED_RGB]   },
    { }
};
MODULE_DEVICE_TABLE(of, ep_dualmatch_of_ids);

The legacy id table uses driver_data instead of data, but the values are cast the same way:

static const struct platform_device_id ep_dualmatch_id_table[] = {
    { .name = "ep-led-basic", .driver_data = (kernel_ulong_t)&ep_led_configs[EP_LED_BASIC] },
    { .name = "ep-led-rgb",   .driver_data = (kernel_ulong_t)&ep_led_configs[EP_LED_RGB]   },
    { }
};
MODULE_DEVICE_TABLE(platform, ep_dualmatch_id_table);

Probe Function Fallback Logic

Inside probe(), the driver tries the device tree match first and only falls back to the id table when there is no DT node:

struct ep_dualmatch_dev {
    struct device *dev;
    const struct ep_led_config *cfg;
};

static int ep_dualmatch_probe(struct platform_device *pdev)
{
    struct ep_dualmatch_dev *edev;
    const struct of_device_id *of_id;
    const struct platform_device_id *pid;

    edev = devm_kzalloc(&pdev->dev, sizeof(*edev), GFP_KERNEL);
    if (!edev)
        return -ENOMEM;

    of_id = of_match_device(ep_dualmatch_of_ids, &pdev->dev);
    if (of_id) {
        edev->cfg = of_id->data;
    } else {
        pid = platform_get_device_id(pdev);
        if (!pid)
            return -ENODEV;
        edev->cfg = (const struct ep_led_config *)pid->driver_data;
    }

    edev->dev = &pdev->dev;
    platform_set_drvdata(pdev, edev);

    dev_info(&pdev->dev, "ep_dualmatch: %s variant, %u channel(s), max brightness %u\n",
             edev->cfg->label, edev->cfg->channels, edev->cfg->max_brightness);

    return 0;
}

static void ep_dualmatch_remove(struct platform_device *pdev)
{
    struct ep_dualmatch_dev *edev = platform_get_drvdata(pdev);

    dev_info(&pdev->dev, "ep_dualmatch: removing %s variant\n", edev->cfg->label);
}

static struct platform_driver ep_dualmatch_driver = {
    .driver = {
        .name = "ep_dualmatch_demo",
        .of_match_table = of_match_ptr(ep_dualmatch_of_ids),
    },
    .id_table = ep_dualmatch_id_table,
    .probe = ep_dualmatch_probe,
    .remove = ep_dualmatch_remove,
};
module_platform_driver(ep_dualmatch_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Dual DT and legacy ID match demo driver");

Note the modern remove() callback returns void, matching the platform driver core signature used from kernel 6.11 onward, so there is nothing to return even on a cleanup path that cannot fail.

Device Tree Node For The DT Path

To exercise the DT branch, add a node like this under your board’s device tree source and rebuild the DTB:

led_rgb0: led-controller@0 {
    compatible = "ep,led-rgb";
    reg = <0x0>;
};

Exercising The Legacy ID Path

To exercise the fallback branch without any device tree at all, register the platform device from a small test module:

static struct platform_device *ep_legacy_pdev;

static int __init ep_legacy_init(void)
{
    ep_legacy_pdev = platform_device_register_simple("ep-led-basic", -1, NULL, 0);
    return PTR_ERR_OR_ZERO(ep_legacy_pdev);
}

static void __exit ep_legacy_exit(void)
{
    platform_device_unregister(ep_legacy_pdev);
}

module_init(ep_legacy_init);
module_exit(ep_legacy_exit);

Testing And Expected Output

Build both modules against your kernel headers, then load them in this order:

$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod ep_dualmatch_demo.ko
$ sudo insmod ep_legacy_pdev.ko
$ dmesg | tail -n 5

Expected output when the legacy module registers ep-led-basic:

ep_dualmatch: basic variant, 1 channel(s), max brightness 255

If instead your board’s device tree already contains the ep,led-rgb node shown above and you rebuild your DTB and reboot, dmesg shows the DT branch firing instead:

ep_dualmatch: rgb variant, 3 channel(s), max brightness 1023

Match Style Comparison Table

Match Style Table Used Typical Use Case
Device Tree of_device_id Modern ARM/RISC-V SoC boards
Legacy ID table platform_device_id Board files, unit tests, vendor SDKs pre-DT
Name fallback driver name string Last resort only, avoid relying on it

Common Mistakes And Troubleshooting

  • Casting driver_data incorrectly: driver_data is a kernel_ulong_t, not a pointer type, so always cast it back explicitly to your config struct pointer type.
  • Forgetting MODULE_DEVICE_TABLE for both tables: without both macros, module autoloading (udev/modprobe) will not work for one of the two match paths.
  • Checking id_table before of_id: always try the device tree match first; boards can have both a DT node and a stale platform_device_id registration, and DT should win.
  • NULL pointer on platform_get_device_id(): if neither table matched, this returns NULL, so always guard against it before dereferencing.

Best Practices

  • Keep variant-specific data in a single shared array and reference it from both tables, so there is only one place to update when a new variant is added.
  • Prefer device_get_match_data() when you do not need to distinguish which table matched, only the resulting data pointer.
  • Document in the driver’s top comment which boards still rely on the legacy id table, so it can be safely removed once every board has migrated to device tree.

Performance Considerations

Match style mixing has no runtime performance cost beyond probe time; the extra branch in probe() executes once per device instantiation, not on any hot path. The device tree walk itself is a one-time cost paid during boot when the platform bus enumerates devices.

Security Considerations

Because a legacy id table can be triggered by any code that calls platform_device_register_simple(), including a malicious or buggy kernel module, do not treat the legacy path as a trusted-only entry point. Validate any resources or memory the probe function acquires the same way regardless of which match table fired.

Real-World Use Cases

This pattern shows up across the mainline kernel tree wherever a driver needs to support both device tree platforms and x86 or legacy ARM boards that still use board files — RTC chips, watchdog drivers, and simple GPIO-based LED or fan controllers are common examples.

Summary And Key Takeaways

  • Device tree match style mixing lets one driver source support both DT boards and legacy board-file boards.
  • of_match_device() should always be checked before platform_get_device_id().
  • Store per-variant configuration once, in a shared array, and reference it from both match tables.
  • Modern kernel 6.x platform drivers use a void-returning remove() callback.

Conclusion

Mixing device tree and legacy id table matching is a small amount of extra code that buys real flexibility: the same driver binary keeps working whether a board boots from a modern device tree blob or from an older hard-coded board file. Understanding the fallback order — device tree first, id table second — is the key idea to carry into the next lecture, where we look at how platform resources such as memory regions and interrupts are pulled out of the device tree automatically.

Frequently Asked Questions

Why does a modern driver still need a platform_device_id table?

Mainly for backward compatibility with boards that register platform devices from C board files instead of device tree, and for lightweight unit testing without a DT overlay.

Which match table does the platform bus try first?

Device tree matching via of_device_id is tried first, then ACPI on ACPI-capable systems, then the platform_device_id table, then a plain driver name string as a last resort.

Can I use device_get_match_data() instead of checking both tables manually?

Yes, if you only need the matched data pointer and do not care which mechanism matched. It is agnostic to DT versus ACPI versus id table matching.

What type is driver_data in platform_device_id?

It is a kernel_ulong_t, so pointers stored in it must be explicitly cast back to the correct pointer type when read in probe.

Does the remove() callback still return an int on kernel 6.x?

No. Since kernel 6.11 the platform driver remove callback returns void, reflecting that a device is always removed regardless of any error condition.

Is it safe to omit MODULE_DEVICE_TABLE for the legacy table?

No. Without it, module autoloading through udev will not find your module for devices that only match through the id table.

What happens if neither match table matches a device?

The probe function is never called; the platform bus simply leaves the device unbound until a matching driver is loaded.

Continue The Free Linux Kernel Development Course

Next up: how platform_get_resource() and platform_get_irq() pull memory and interrupt resources straight out of the device tree.

 

Leave a Reply

Your email address will not be published. Required fields are marked *