The Unified Device Property Interface fwnode

The firmware nodestruct fwnode_handle, universally abbreviated fwnode — is the Linux kernel’s firmware-agnostic handle to a device’s description. Before it existed, a driver that needed to read a configuration value had to know which firmware described the device: it called of_property_read_u32() on a Device Tree system but a different, ACPI-specific accessor on an ACPI system, often guarding the two with #ifdef CONFIG_OF and if (ACPI_HANDLE(dev)). The fwnode abstraction collapses both into one: a driver calls device_property_read_u32(dev, "clock-frequency", &freq) and the kernel dispatches, through a small virtual-function table (struct fwnode_operations), to whichever backend — Open Firmware/Device Tree, ACPI _DSD, or a kernel-synthesized software node — actually holds the data. The driver never learns which. This unification, introduced by Rafael Wysocki and Mika Westerberg’s 2014 “unified device properties” series (LWN 2014), let a large body of drivers delete their duplicated of_*/acpi_* code paths. This note explains the fwnode_handle structure, the fwnode_operations vtable and its backends, how dev->fwnode and dev->of_node relate, the device_property_* versus fwnode_property_* layers, and software nodes. It is grounded in the Linux 6.12 LTS source tree (the 6.18 LTS layer is structurally the same).

Mental Model

Think of fwnode as a polymorphic interface in C — the kernel’s hand-rolled equivalent of a Java interface or a C++ abstract base class. A struct fwnode_handle is the “base class”: it carries a pointer to a vtable (ops) and almost nothing else. The concrete “subclasses” are the firmware backends — a Device-Tree node, an ACPI device’s _DSD data, or a software node — each of which embeds a fwnode_handle (or is reachable from one) and supplies its own fwnode_operations table. A driver holds only the base pointer and calls through the vtable; the backend does the type-specific work.

flowchart TB
  DRV["Driver code<br/>device_property_read_u32(dev, ...)"]
  DRV -->|"dev_fwnode(dev) picks the handle"| FW["struct fwnode_handle<br/>{ ops, secondary, dev, flags }"]
  FW -->|"fwnode_call_int_op(fwnode, property_read_int_array, ...)"| OPS["const struct fwnode_operations *ops"]
  OPS -->|"of_fwnode_ops"| OF["Device Tree backend<br/>(drivers/of/property.c)<br/>reads struct device_node"]
  OPS -->|"acpi_device_fwnode_ops"| ACPI["ACPI backend<br/>(drivers/acpi/property.c)<br/>reads _DSD package"]
  OPS -->|"software_node_ops"| SW["Software node backend<br/>(drivers/base/swnode.c)<br/>reads property_entry[]"]
  FW -.->|"fwnode->secondary fallback"| FW2["secondary fwnode<br/>(e.g. swnode layered on a DT/ACPI node)"]

The fwnode dispatch path. What it shows: a single driver call resolves to a fwnode_handle, whose ops vtable routes the request to exactly one of three backends — OF/Device Tree, ACPI _DSD, or software node — and a secondary handle provides a fallback chain. The insight to take: the driver above the dashed line is written once and is completely blind to which backend serves it; all firmware-specific knowledge lives below the vtable. This is precisely why the same driver binds on both an ARM Device-Tree board and an x86 ACPI machine.

Anatomy of struct fwnode_handle

The structure is deliberately tiny (fwnode.h, v6.12):

struct fwnode_handle {
	struct fwnode_handle *secondary;
	const struct fwnode_operations *ops;
 
	/* The below is used solely by device links, don't use otherwise */
	struct device *dev;
	struct list_head suppliers;
	struct list_head consumers;
	u8 flags;
};

Field by field. ops is the vtable — the only thing that distinguishes a Device-Tree fwnode from an ACPI one; everything polymorphic flows through it. secondary is a fallback handle: a property lookup that misses on the primary fwnode is retried on secondary, which lets the kernel layer a software node over a firmware-described one (e.g. patch in extra properties the firmware forgot). dev, suppliers, and consumers are reserved for Device Links — the supplier/consumer dependency graph the kernel builds by parsing references between fwnodes (add_links in the vtable feeds this); the comment is explicit that they are otherwise off-limits. flags carries state bits such as FWNODE_FLAG_INITIALIZED (“the hardware corresponding to fwnode has been initialized”), FWNODE_FLAG_NOT_DEVICE (“the fwnode will never be populated as a struct device”), and FWNODE_FLAG_LINKS_ADDED (“the fwnode has already be parsed to add fwnode links”) (fwnode.h, v6.12).

What is not in the struct is as telling as what is: there is no place to store properties. The handle is pure indirection — the data lives in the backend object the ops know how to reach.

The fwnode_operations Vtable

struct fwnode_operations is the interface every backend implements. The 6.12 table has roughly nineteen function pointers (fwnode.h, v6.12). The most important groups:

  • Lifetime: get / put — reference counting for backends whose nodes are refcounted (Device Tree nodes are; statically-defined fwnodes may be no-ops).
  • Presence and match: device_is_available (is this device usable?), device_get_match_data (the per-entry data for the matched ID-table row), device_dma_supported / device_get_dma_attr.
  • Properties: property_present (does this key exist?), property_read_int_array (read a u8/u16/u32/u64 array — one entry point, sized by an elem_size argument), property_read_string_array.
  • Naming and tree walk: get_name, get_name_prefix, get_parent, get_next_child_node, get_named_child_node.
  • References and graphs: get_reference_args (resolve a phandle-style reference with arguments), and the graph_* operations (graph_get_next_endpoint, graph_get_remote_endpoint, graph_get_port_parent, graph_parse_endpoint) that walk the OF/ACPI graph bindings used by media and display pipelines.
  • Resources: iomap, irq_get, and add_links (the device-links dependency parser).

A driver never calls these directly. It calls a wrapper like fwnode_property_present(), which uses helper macros (fwnode_call_bool_op, fwnode_call_int_op, fwnode_call_ptr_op, built on FWNODE_HAS_OP) to safely dispatch only if the backend supplies that operation. The dispatch wrapper for presence shows the secondary-fallback pattern in full (property.c, v6.12):

bool fwnode_property_present(const struct fwnode_handle *fwnode,
			     const char *propname)
{
	bool ret;
 
	if (IS_ERR_OR_NULL(fwnode))
		return false;
 
	ret = fwnode_call_bool_op(fwnode, property_present, propname);
	if (ret)
		return ret;
 
	return fwnode_call_bool_op(fwnode->secondary, property_present, propname);
}

Line by line: it rejects an error/NULL handle (so callers need not guard); it asks the primary backend through the vtable; and on a miss it falls back to secondary — the mechanism that lets a software node augment a firmware node.

Mechanical Walk-Through — From device_property_* to a Backend

There are two parallel API layers. The device_property_* family takes a struct device * and is what most drivers call. The fwnode_property_* family takes a struct fwnode_handle * directly and is used when walking child nodes that have no struct device of their own (e.g. iterating sub-nodes of a parent device). The device-level functions are thin shims that resolve the device to its fwnode and then call the fwnode-level function.

The resolution step is dev_fwnode(), whose core is __dev_fwnode() (property.c, v6.12):

struct fwnode_handle *__dev_fwnode(struct device *dev)
{
	return IS_ENABLED(CONFIG_OF) && dev->of_node ?
		of_fwnode_handle(dev->of_node) : dev->fwnode;
}

This is the crux of how the two firmware worlds coexist on one struct device. A device populated from Device Tree has its dev->of_node set (a struct device_node *); of_fwnode_handle() extracts the fwnode_handle embedded in that node. A device from ACPI (or a software node) has dev->of_node == NULL and uses dev->fwnode directly. So dev->of_node is the typed Device-Tree pointer kept for the many legacy of_* call sites, while dev->fwnode is the generic handle; dev_fwnode() always returns whichever is authoritative. (There is also a const twin, __dev_fwnode_const(), for read-only contexts.)

Once a fwnode is in hand, a typed read such as device_property_read_u32_array() delegates to fwnode_property_read_u32_array(), which calls the shared fwnode_property_read_int_array() helper, which dispatches property_read_int_array through the vtable with elem_size = sizeof(u32). The backend does the type-specific extraction. Return values are uniform across backends: 0 on success, -ENODATA when the property is absent, -EPROTO/-EILSEQ/-EOVERFLOW for type or size mismatches (property.c, v6.12).

The three backends

Device Tree (OF). drivers/of/property.c defines const struct fwnode_operations of_fwnode_ops, wiring each operation to the long-standing of_* routines. For example (of/property.c, v6.12):

static bool of_fwnode_property_present(const struct fwnode_handle *fwnode,
				       const char *propname)
{
	return of_property_read_bool(to_of_node(fwnode), propname);
}

to_of_node() recovers the struct device_node * from the embedded fwnode, then the classic of_property_read_bool() does the work. Child-node lookup (of_fwnode_get_named_child_node) iterates for_each_available_child_of_node(). The OF backend also defines DEFINE_SIMPLE_PROP(...) parsers (parse_clocks, parse_interconnects, …) that the add_links operation uses to discover supplier dependencies from phandle references — feeding Device Links.

ACPI. drivers/acpi/property.c defines acpi_device_fwnode_ops and acpi_data_fwnode_ops (the latter for non-device sub-nodes) via the DECLARE_ACPI_FWNODE_OPS() macro. acpi_fwnode_property_present() calls acpi_node_prop_get(); acpi_fwnode_property_read_int_array() maps the element size to a dev_prop_type and delegates to acpi_node_prop_read()acpi_data_prop_read(). The properties themselves come from the device’s [[ACPI Device Enumeration|_DSD]] package under the Device Properties UUID daffd814-6eba-4d8c-8a91-bc9bbf4aa301 (acpi/property.c, v6.12). Notably, struct acpi_device embeds a struct fwnode_handle fwnode field (acpi_bus.h, v6.12) — that embedded handle is exactly what dev->fwnode points at for an ACPI device.

Software node. drivers/base/swnode.c provides software_node_ops over kernel-synthesized properties (below).

The reason all three return the same error codes and accept the same argument shapes is the whole point: the driver above cannot tell them apart.

Software Nodes — Kernel-Synthesized Properties

Sometimes neither Device Tree nor ACPI describes a device adequately — a discoverable bus like USB or PCI has no firmware node, or a board file/MFD parent must hand structured data to a child driver, or firmware is buggy and the kernel must patch in a property. Software nodes solve this: the kernel builds an in-memory fwnode from a static array of property entries.

The data structures (property.h, v6.12):

enum dev_prop_type { DEV_PROP_U8, DEV_PROP_U16, DEV_PROP_U32,
                     DEV_PROP_U64, DEV_PROP_STRING, DEV_PROP_REF };
 
struct software_node {
	const char *name;
	const struct software_node *parent;
	const struct property_entry *properties;
};

A struct property_entry holds one name/type/value triple; small scalars are stored inline in the entry (the is_inline flag and the union), which is why the PROPERTY_ENTRY_U32("size", 0x80000) family of macros can build a static, const property table with no runtime allocation. A driver, or the bus/MFD parent, defines such a table and attaches it:

static const struct property_entry my_props[] = {
	PROPERTY_ENTRY_U32("clock-frequency", 400000),
	PROPERTY_ENTRY_STRING("compatible", "acme,widget"),
	PROPERTY_ENTRY_BOOL("wakeup-source"),
	{ }   /* terminator */
};
 
/* Attach to a device so device_property_* reads see them: */
device_create_managed_software_node(dev, my_props, NULL);

device_create_managed_software_node() creates a software-node fwnode from the entries and installs it as the device’s secondary fwnode (so it augments any primary firmware node), and ties its lifetime to the device — it is released automatically on unbind, like other devm-style resources. For more control there are fwnode_create_software_node(properties, parent) (returns a standalone handle), software_node_register() (registers a named node into a hierarchy, enabling cross-references), and device_add_software_node() / device_remove_software_node() for explicit attach/detach (property.h, v6.12). The predicate is_software_node(fwnode) and the down-cast to_software_node(fwnode) let code that genuinely needs to know recover the concrete type — but well-behaved drivers never need to.

Walking Child Nodes and References

Many devices describe sub-components as child nodes — a regulator’s outputs, an LED controller’s individual LEDs, a camera’s ports. The fwnode layer offers uniform iterators independent of firmware:

struct fwnode_handle *child;
 
device_for_each_child_node(dev, child) {
	u32 reg;
	if (fwnode_property_read_u32(child, "reg", &reg))
		continue;
	/* configure the sub-component identified by reg */
}

device_for_each_child_node() (and fwnode_for_each_child_node()) hide whether the children are Device-Tree sub-nodes or ACPI _DSD hierarchical data nodes. device_get_named_child_node(dev, "port@0") fetches one by name. A subtlety the kernel docs stress repeatedly: these iterators take a reference on each child fwnode, and “the caller is responsible for calling fwnode_handle_put() on the returned fwnode pointer” (property.c, v6.12). The for_each macros release the reference automatically on a normal loop exit, but a break/return from inside the loop leaks it unless you fwnode_handle_put(child) first.

Cross-references between nodes (Device Tree’s phandles, ACPI’s reference packages) are resolved through fwnode_property_get_reference_args() / fwnode_find_reference(), again uniformly — the GPIO, clock, and regulator subsystems all build on this.

A Full Dispatch Trace and the Resource Operations

It is worth following one read end to end to see that there is no magic — only one indirection. Suppose a driver in .probe() does:

u32 freq;
int ret = device_property_read_u32(dev, "clock-frequency", &freq);

The chain is: device_property_read_u32() (a one-element wrapper) → device_property_read_u32_array(dev, "clock-frequency", &freq, 1)fwnode_property_read_u32_array(dev_fwnode(dev), ...) → the shared fwnode_property_read_int_array() helper with elem_size = 4fwnode_call_int_op(fwnode, property_read_int_array, ...). That last macro checks FWNODE_HAS_OP(fwnode, property_read_int_array) and, if present, calls fwnode->ops->property_read_int_array(...). On a Device-Tree device this lands in of_fwnode_property_read_int_array(), which calls of_property_read_variable_u32_array() on the recovered device_node; on an ACPI device it lands in acpi_fwnode_property_read_int_array(), which reads from the cached _DSD package. Both return 0 and fill freq, or -ENODATA if absent — identical observable behavior. dev_fwnode() is the single point that chose the route, based solely on whether dev->of_node was set.

Beyond properties, two vtable operations expose resources through the same indirection. fwnode_irq_get() (and the named variant fwnode_irq_get_byname(), which the [[ACPI Device Enumeration|ACPI interrupt-names mechanism]] relies on) dispatches irq_get, mapping a firmware-described interrupt to a Linux IRQ number regardless of whether it came from a Device-Tree interrupts property or an ACPI _CRS interrupt resource. fwnode_iomap() dispatches iomap to map a memory region. These let even resource acquisition — not just configuration data — be written firmware-blind, which is why a modern platform driver’s probe can be almost entirely free of of_/acpi_ prefixes.

Failure Modes and Common Misunderstandings

  • dev->of_node vs dev->fwnode confusion. New code should call dev_fwnode(dev) and the device_property_*/fwnode_property_* APIs, not dereference dev->of_node directly — the latter is NULL on ACPI and software-node devices, so a driver that reaches for it silently breaks on x86. The whole abstraction exists to stop this.
  • Reference leaks. Forgetting fwnode_handle_put() after an early exit from a child iteration leaks a Device-Tree node reference. On CONFIG_OF_DYNAMIC systems this prevents the node from ever being freed; it is a real, recurring class of bug.
  • Expecting secondary to be searched for everything. Only the property-style operations cascade to secondary. Tree-walk and graph operations generally do not, so a software node layered as secondary augments properties but does not add child nodes to the primary.
  • device_property_read_bool semantics. A boolean property is present-or-absent, not true/false. device_property_read_bool() returns whether the key exists at all; a PROPERTY_ENTRY_BOOL("x") with no value is “x is true.”
  • Type strictness. Reading a u32 array property that the firmware declared as strings returns -EPROTO, not garbage. Mismatches surface as errors, which is good — but means a driver must check return codes rather than assume success.

Uncertain

Verify: the exact count of function pointers in struct fwnode_operations in 6.12 (stated here as “roughly nineteen”) and the precise set of FWNODE_FLAG_* macros. Reason: the field list was extracted via a summarizing fetch of include/linux/fwnode.h rather than a line-exact local read, so the count and the full flag set are approximate. To resolve: read include/linux/fwnode.h at the v6.12 tag directly and count. uncertain

Alternatives and Historical Context

Before fwnode (pre-3.19, roughly), there was no unified layer: drivers used of_property_read_*() for Device Tree, and ACPI had no general property mechanism at all until [[ACPI Device Enumeration|_DSD]] arrived in ACPI 5.1. The 2014 series by Mika Westerberg and Rafael Wysocki added “a unified device properties API with ACPI and OF backends,” explicitly to provide “firmware agnostic device drivers” and “wrapper functions for most used property types” (LWN 2014). The same series introduced dev_node_xxx() functions “to access firmware node properties without dev pointer” — the fwnode_property_* layer. The payoff, visible across the tree since, is that a single driver with one device_property_*-based probe path binds on Device Tree, ACPI, and software-node systems alike; the PRP0001 bridge (which lets ACPI reuse Device-Tree compatible strings) is the same philosophy extended to matching. There is no real “alternative” to fwnode within modern Linux — it is the convergence point; the only choice is whether a given driver still carries legacy direct of_* calls (technical debt) or has been converted to the unified API (the goal).

Production Notes

The conversion of drivers from of_*/acpi_* to device_property_* has been a multi-year janitorial effort; new drivers are expected to use the unified API and reviewers reject direct of_node poking in generic code. Subsystems built entirely on fwnode include GPIO (gpiod_get() resolves names through fwnode), the regulator and clock frameworks, IIO sensors, and the media/V4L2 graph bindings (which lean on the graph_* operations). Software nodes are heavily used by USB Type-C and Thunderbolt code and by x86 platform glue (drivers/platform/x86/) to feed properties to drivers on buses that have no firmware node of their own. When debugging “my driver reads a property fine on the dev board but gets -ENODATA on the production unit,” the first check is which backend is serving the device — dev->of_node non-NULL means Device Tree, ACPI_COMPANION(dev) non-NULL means ACPI _DSD, neither means a software node or none at all.

See Also