RT-Thread RTOS
An open source embedded real-time operating system
Platform bus

Platform bus

Most MMIO / on-SoC peripherals under **RT_USING_DM** bind through the platform bus: a **rt_platform_driver** matches a **rt_platform_device**, then **probe** runs after the bus has applied common setup (pinctrl, clocks, power domain, IOMMU).

  • Header: **components/drivers/include/drivers/platform.h**
  • Bus logic: **components/drivers/core/platform.c**
  • OFW enumeration: **components/drivers/core/platform_ofw.c**

Data structures

struct rt_platform_device {
struct rt_device parent; /* parent.ofw_node, parent.bus, parent.drv */
int dev_id;
const char *name; /* non-OFW matching only */
const struct rt_ofw_node_id *id; /* filled by match; use id->data */
void *priv; /* optional driver private pointer */
};
struct rt_platform_driver {
struct rt_driver parent;
const char *name;
const struct rt_ofw_node_id *ids; /* compatible table, sentinel last */
rt_err_t (*probe)(struct rt_platform_device *pdev);
rt_err_t (*remove)(struct rt_platform_device *pdev);
rt_err_t (*shutdown)(struct rt_platform_device *pdev);
};
Definition: rtdef.h:1369
struct rt_object parent
Definition: rtdef.h:1370

In **probe**, almost all DM helpers take **struct rt_device *dev = &pdev->parent**.


Boot and init order

Init levels (linker sort key, see **rtdef.h**):

Level Typical use
**INIT_CORE_EXPORT** ("1.0") **platform_bus_init** — **rt_bus_register(&platform_bus)**
**INIT_SUBSYS_EXPORT** ("1.1") Early drivers: manual **rt_platform_driver_register()** (e.g. **pin-pl061**, **pinctrl-single**)
**INIT_PLATFORM_EXPORT** ("1.2") **platform_ofw_device_probe** — walk DT, **rt_platform_device_register**
**INIT_DEVICE_EXPORT** ("3") **RT_PLATFORM_DRIVER_EXPORT** — registers driver, then probes all devices already on the bus
INIT_CORE → platform bus exists
INIT_SUBSYS → some drivers registered (optional)
INIT_PLATFORM → DT nodes → platform devices on bus → match + probe if driver already present
INIT_DEVICE → RT_PLATFORM_DRIVER_EXPORT → register driver → probe every unmatched device

Takeaway: DT devices appear at **INIT_PLATFORM**. Drivers using **RT_PLATFORM_DRIVER_EXPORT** register later and then bind via **rt_bus_add_driver -> rt_bus_for_each_dev**. Drivers that must run before the bulk DT scan use **INIT_SUBSYS_EXPORT** + manual **rt_platform_driver_register**.


Registering a driver (important)

Method A — <tt>RT_PLATFORM_DRIVER_EXPORT</tt> (most common)

static const struct rt_ofw_node_id my_ofw_ids[] = {
{ .compatible = "vendor,my-device" },
{ /* sentinel */ }
};
static rt_err_t my_probe(struct rt_platform_device *pdev)
{
struct rt_device *dev = &pdev->parent;
/* iomap, clk, init hardware, register char/block device */
return RT_EOK;
}
static rt_err_t my_remove(struct rt_platform_device *pdev)
{
/* reverse probe: unregister, free */
return RT_EOK;
}
static struct rt_platform_driver my_driver = {
.name = "my-device",
.ids = my_ofw_ids,
.probe = my_probe,
.remove = my_remove,
};
RT_PLATFORM_DRIVER_EXPORT(my_driver);

Macro expansion (**driver.h**):

#define RT_PLATFORM_DRIVER_EXPORT(driver) RT_DRIVER_EXPORT(driver, platform, BUILIN)
/* → static init calls rt_platform_driver_register(&driver) at INIT_DEVICE_EXPORT */

Requirements:

  • **name**: driver name string — used for name-based match (see below); keep unique.
  • **ids**: optional — **compatible** table for DT match; omit or set **RT_NULL** when using name-only binding.
  • **probe**: return **RT_EOK** on success, negative rt_err_t on failure.
  • **remove**: optional but recommended if **probe** allocates resources.

Matching: <tt>ids</tt> (compatible) vs <tt>name</tt>

**platform_match** tries both (in order):

Step Condition Match when
1 **dev->ofw_node** set and **pdrv->ids** non-NULL **compatible** matches an entry in **ids** → **pdev->id** points at that entry
2 **pdev->name** and **pdrv->name** both set Strings equal (same pointer or **rt_strcmp == 0**)

Implications:

  • DT devices (from **INIT_PLATFORM**): normally matched by **ids** / **compatible**. This is what most drivers use.
  • No ids: set **.ids = RT_NULL** (or omit the field) and match by **name** — typical when the device is created in board code with **rt_platform_device_alloc("same-string")** and **pdev->name** equals **pdrv->name**.
  • Both can be filled: compatible is tried first; if it fails, name is still tried (useful only if **pdev->name** is a non-empty string).

Name-only driver + device:

static rt_err_t legacy_probe(struct rt_platform_device *pdev) { ... return RT_EOK; }
static struct rt_platform_driver legacy_driver = {
.name = "board-uart", /* must equal pdev->name */
.ids = RT_NULL, /* no compatible table */
.probe = legacy_probe,
};
RT_PLATFORM_DRIVER_EXPORT(legacy_driver);
/* In board init, after bus is up: */
void board_uart_init(void)
{
struct rt_platform_device *pdev = rt_platform_device_alloc("board-uart");
pdev->name = "board-uart";
rt_platform_device_register(pdev);
}

Method B — manual <tt>rt_platform_driver_register</tt>

Used when the driver must register before **INIT_PLATFORM** (dependency ordering) or the file predates the export macro:

static int my_drv_register(void)
{
return rt_platform_driver_register(&my_driver);
}
INIT_SUBSYS_EXPORT(my_drv_register);

Same **struct rt_platform_driver** layout as method A.

What <tt>rt_platform_driver_register</tt> does

  1. Sets **pdrv->parent.bus = &platform_bus**
  2. Copies **pdrv->name** into **pdrv->parent.parent.name**
  3. **rt_driver_register** → **rt_bus_add_driver**
  4. **rt_bus_for_each_dev** — for each device without **dev->drv**, **platform_match**; on match, **platform_probe**

Registering the driver never creates devices; it only enables binding.


How devices get on the bus

Automatic — device tree (<tt>INIT_PLATFORM</tt>)

**platform_ofw_device_probe** walks the tree (root, **/clocks**, **/firmware**, chosen framebuffer, …). For each available child with **compatible** (or a valid node name):

  1. **alloc_ofw_platform_device(np)** — **rt_platform_device_alloc**, **pdev->parent.ofw_node = np**, **np->dev = &pdev->parent**
  2. **rt_platform_device_register(pdev)** → **rt_bus_add_device** → try all drivers

Bus nodes (simple-bus, simple-mfd, arm,amba-bus, …): the walker recurses into children first without instantiating a device for the bus node itself (unless something else requests it).

Device naming: **ofw_device_rename** — e.g. **fe300000.serial:uart0** from **reg** + node name + alias.

On demand — <tt>rt_platform_ofw_request(np)</tt>

Called when another driver needs a provider (clock, gpio, pinctrl, regulator, phye, …) before or without global probe:

np->dev Action
NULL Allocate platform device, register on bus → match + probe
Set, **dev->drv NULL** **rt_bus_reload_driver_device** — retry match
Set, **dev->drv set** **RT_EOK** (already probed)

Manual — non-OFW / board code

struct rt_platform_device *pdev = rt_platform_device_alloc("my-uart");
pdev->name = "my-uart"; /* must match pdrv->name for name-based match */
rt_platform_device_register(pdev);

Rare on DM + DT boards; used for legacy or test bring-up.

Child node only — <tt>rt_platform_ofw_device_probe_child(np)</tt>

Registers one node as a platform device when it is not under **/** and not already **RT_OFW_F_PLATFORM**. Used by umbrella drivers that expose children explicitly.


Match rules (<tt>platform_match</tt>)

See Matching: ids vs name above. Summary:

  1. OFW / DT: **rt_ofw_node_match(np, pdrv->ids)** when **ids** is set — **pdev->id** → use **pdev->id->data** for SoC variants.
  2. Name: **pdev->name** vs **pdrv->name** — works without **ids**; primary path for manually registered devices.

DT enumeration sets **pdev->name** from **rt_platform_device_alloc("")** (empty); binding those nodes still relies on **compatible** unless board code assigns a non-empty **pdev->name** that matches the driver.

Only one driver binds per device. No match → device stays on **dev_list** until a driver registers later.


<tt>platform_probe</tt> wrapper (before your <tt>probe</tt>)

The bus runs before **pdrv->probe(pdev)**:

Step Function Notes
1 **rt_pin_ctrl_confs_apply_by_name(dev, NULL)** / **pinctrl-0** Pin control (pinctrl)
2 **rt_ofw_clk_set_defaults(dev->ofw_node)** Assigned clocks
3 **rt_dm_power_domain_attach(dev, RT_TRUE)** **-RT_EEMPTY** OK
4 **rt_iommu_attach(dev)** **-RT_EEMPTY** OK if no IOMMU
5 **pdrv->probe(pdev)** Your driver

On **probe failure**: IOMMU detach, power domain detach, **dev->drv cleared**.

On success: **RT_OFW_F_READLY** set on the OFW node.

Your **probe** should: parse resources (**rt_dm_dev_iomap**, **rt_dm_dev_get_irq**, **rt_clk_get_by_name**, …) → enable hardware → register the functional **rt_device** (UART, pin, LED, …). Return negative **rt_err_t** on hard errors and free what you already allocated.


Typical <tt>probe</tt> / <tt>remove</tt> pattern

static rt_err_t my_probe(struct rt_platform_device *pdev)
{
rt_err_t err;
struct rt_device *dev = &pdev->parent;
struct my_dev *priv = rt_calloc(1, sizeof(*priv));
if (!priv)
return -RT_ENOMEM;
priv->regs = rt_dm_dev_iomap(dev, 0);
if (!priv->regs) {
err = -RT_EIO;
goto err_free;
}
priv->clk = rt_clk_get_by_index(dev, 0);
if (rt_is_err(priv->clk)) {
err = rt_ptr_err(priv->clk);
goto err_unmap;
}
if ((err = rt_clk_prepare_enable(priv->clk)))
goto err_clk_put;
/* register rt_device, set rt_ofw_data for providers */
rt_dm_dev_bind_fwdata(dev, RT_NULL, priv);
return RT_EOK;
err_clk_put:
rt_clk_put(priv->clk);
err_unmap:
rt_iounmap(priv->regs);
err_free:
rt_free(priv);
return err;
}
static rt_err_t my_remove(struct rt_platform_device *pdev)
{
struct my_dev *priv = rt_dm_dev_get_fwdata(&pdev->parent);
/* unregister child device, disable clk, iounmap, rt_free(priv) */
return RT_EOK;
}
struct rt_clk * rt_clk_get_by_index(struct rt_device *dev, int index)
Get clock pointer from ofw device node by index.
Definition: clk.c:1872
void rt_clk_put(struct rt_clk *clk)
Put reference count of clock.
Definition: clk.c:1969
rt_err_t rt_clk_prepare_enable(struct rt_clk *clk)
Prepare and enable clock.
Definition: clk.c:663
rt_weak void * rt_calloc(rt_size_t count, rt_size_t size)
This function will contiguously allocate enough space for count objects that are size bytes of memory...
Definition: kservice.c:1210
rt_weak void rt_free(void *ptr)
This function will release the previously allocated memory block by rt_malloc. The released memory bl...
Definition: kservice.c:1231

**remove / shutdown**: bus calls **pdrv->remove**, then **rt_iommu_detach**, **rt_dm_power_domain_detach**, **rt_platform_ofw_free(pdev)** (drops OFW node ref, **rt_free(pdev)**).


<tt>pdev</tt> fields in practice

Field Use
**&pdev->parent** Pass to **rt_dm_dev_***, **rt_pin_get_named_pin**, **rt_ofw_***
**pdev->id** After match: SoC variant via **pdev->id->data** (see **pinctrl-single** **pcs_ofw_ids**)
**pdev->priv** Optional; core does not allocate it — prefer embedding **rt_platform_device** in your private struct or **rt_dm_dev_bind_fwdata**
**pdev->parent.ofw_node** DT node for this instance

Common pattern: embed **struct rt_platform_device** as not used — instead embed nothing and use **struct rt_device *dev = &pdev->parent** only; provider drivers embed **struct rt_device_pin parent** etc.


Examples

A — DT + <tt>ids</tt> (typical)

Device tree:

mydev: device@40010000 {
compatible = "vendor,my-device";
reg = <0x40010000 0x100>;
interrupts = <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>;
};

Driver:

static rt_err_t my_probe(struct rt_platform_device *pdev) { ... }
static const struct rt_ofw_node_id my_ids[] = {
{ .compatible = "vendor,my-device" },
{ },
};
static struct rt_platform_driver my_driver = {
.name = "vendor-my-device",
.ids = my_ids, /* match by compatible */
.probe = my_probe,
};
RT_PLATFORM_DRIVER_EXPORT(my_driver);

**INIT_PLATFORM** creates the device from DT; **INIT_DEVICE** registers the driver and **probe** runs. No **rt_platform_device_register** in board code.

B — name only, no <tt>ids</tt>

No **compatible** binding — driver and device share the same **name** string:

static struct rt_platform_driver board_uart_driver = {
.name = "uart0",
.ids = RT_NULL,
.probe = board_uart_probe,
};
RT_PLATFORM_DRIVER_EXPORT(board_uart_driver);
/* board.c */
struct rt_platform_device *pdev = rt_platform_device_alloc("uart0");
pdev->name = "uart0";
rt_platform_device_register(pdev);

Use for legacy BSP devices not described in DT, or tests. On DM + full DT boards, prefer example A.


When to use platform vs other buses

Use platform Use another bus
SoC DT node with **compatible**, **reg**, **interrupts** PCI endpoint → **rt_pci_driver**
GPIO LED, UART, GPIO controller, syscon, firmware node I2C sensor → **rt_i2c_driver** + client
Provider requested via **rt_platform_ofw_request** SPI slave → **rt_spi_driver**

Platform is the default for on-chip devices described in the board DTS.


Pitfalls

Issue Mitigation
**compatible typo** No match on DT path; device never probes — check logs
Name mismatch **pdev->name** must equal **pdrv->name**; empty **pdev->name** does not match
Expect name match on DT node DT devices need **ids** unless you set **pdev->name** explicitly
Driver after device is OK **RT_PLATFORM_DRIVER_EXPORT** relies on **rt_bus_add_driver** re-walk
Driver too late for early deps Use **INIT_SUBSYS_EXPORT** or split provider driver earlier
**probe success but no driver** Returning **RT_EOK** without work — still marks node **READLY**
Missing **remove** OFW node freed by bus; leaks if you allocated IRQ/DMA without teardown
Provider not probed **rt_platform_ofw_request(np)** from consumer before using phandle
**pdev->priv unset** Do not assume bus sets it; use **rt_calloc** / **bind_fwdata**
Console not ready in early **probe** **INIT_DEVICE** is late; use **LOG_D** or defer prints
Re-probe **rt_bus_reload_driver_device** when driver registers after deferred provider

See also

  • Pin control (pinctrl) — applied in **platform_probe** before driver **probe**
  • Open Firmware (OFW) — DT nodes, phandles
  • Device model helpers (dm.h) — **rt_dm_dev_iomap**, clocks, IRQ
  • documentation/6.components/device-driver/core/bus.md — generic **rt_bus** / **RT_DRIVER_EXPORT**
  • components/drivers/core/platform.c, platform_ofw.c