[PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub

From: Lakshay Piplani

Date: Wed Aug 26 2026 - 06:43:50 EST


Add virtual I3C bus support for the hub and provide interface to enable
or disable downstream ports.

Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@xxxxxxx>
Signed-off-by: Vikash Bansal <vikash.bansal@xxxxxxx>
Signed-off-by: Lakshay Piplani <lakshay.piplani@xxxxxxx>

---
Changes in v16:
- Fix the lockdep "recursive locking" false positive on the forwarding path
(holding the virtual bus lock while taking the parent bus lock, both
sharing one class). Introduce per-nesting-depth lock_class_key arrays for
the bus lock and the routing mutex, computed via
i3c_hub_controller_depth(), assigned in i3c_hub_master_bus_init() and
i3c_hub_init()
- Fix the parent-bus dynamic-address reservation: reserve by
"assigned-address" regardless of the static address (the old
static_addr != assigned_addr skip left addresses free and risked ENTDAA
collisions). Reserve only when the slot is currently FREE, under the
parent bus maintenance lock
- Make the reattach info update atomic w.r.t. asynchronous IBI: update
parent_desc.info under i3c_bus_maintenance_lock(&parent->bus) and reject
reattach with -EBUSY (WARN_ON_ONCE) while parent_desc.ibi is live
- Document and check the detach-time IBI lifecycle invariant:
parent_desc.ibi must already have been cleared through i3c_hub_free_ibi()
before the parent-facing descriptor is detached and freed. Add
WARN_ON_ONCE() to detect a violation. The underlying generic I3C-core
unregister/IBI-quiesce behaviour is a known I3C-core limitation
- Document that DAA is intentionally run on the parent controller because
downstream devices share the parent's dynamic-address space; broadcast
RSTDAA is intentionally not forwarded (would reset the hub address)

Changes in v15:
- Replace temporary descriptor reparenting and hub-address switching with
a permanent parent-facing descriptor for each downstream I3C device
- Keep the logical device descriptor associated with the virtual hub
controller while using the parent-facing descriptor for physical
controller operations
- Implement downstream device attach, reattach and detach using the new
controller-only helpers
- Forward IBI slot recycling to the physical parent controller
- Remove the helper that temporarily changed the logical descriptor's
master pointer

Changes in v14:
- Add hub route serialization around DAA, CCC, private transfer and IBI paths
- Add comments explaining that downstream devices behind hub target ports
share the parent controller address space
- Use i3c_bus_maintenance_lock()/unlock() instead of raw parent bus lock
operations
- Hold the parent maintenance lock across temporary hub address reattach,
transfer and address restore
- Rework IBI request/free forwarding, so downstream devices are temporarily
attached to the parent controller for IBI setup
- Clean up the preallocated virtual IBI object and workqueue before forwarding
the IBI request to the parent controller
- Avoid NULL dereference in the core IBI free path when the forwarding
callback has already cleared dev->ibi

Changes in v13:
- Reserve parent I3C bus address slots for downstream devices with matching static and
assigned addresses by parsing target-port DT nodes
- Keep broadcast RSTDAA blocked and add explanatory comment
- Add TODO for empty IBI slot recycle callback
- Add comments for intentional no-op callbacks

Changes in v12:
- Drop redundant depends on I3C from config I3C_HUB
- Return -EOPNOTSUPP for unsupported I2C transfers instead of reporting
success.

Changes in v11:
- Fix virtual hub address reattach handling
- Fix parent bus locking during address reattach operations
- Improve transfer and IBI request error handling

Changes in v10:
- Remove unnecessary ibi_lock handling in request/enable/disable/free
IBI APIs
- Remove redundant parent pointer from struct i3c_hub and derive upstream
master from hub_dev

Changes in v9:
- No change

Changes in v8:
- No change

Changes in v7:
- Convert Kconfig option to tristate
- Fix signedness issue in return value
- Fix kernel-doc warnings

Changes in v6:
- Add support for the generic I3C interface in the I3C Hub
---
---
MAINTAINERS | 2 +
drivers/i3c/Kconfig | 14 +
drivers/i3c/Makefile | 1 +
drivers/i3c/hub.c | 798 ++++++++++++++++++++++++++++++++++++++++
include/linux/i3c/hub.h | 94 +++++
5 files changed, 909 insertions(+)
create mode 100644 drivers/i3c/hub.c
create mode 100644 include/linux/i3c/hub.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 448c5017f665..11f231ce05cf 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19552,8 +19552,10 @@ L: linux-kernel@xxxxxxxxxxxxxxx
L: linux-i3c@xxxxxxxxxxxxxxxxxxx
S: Maintained
F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
+F: drivers/i3c/hub.c
F: drivers/mfd/p3h2840.c
F: drivers/regulator/p3h2840_i3c_hub_regulator.c
+F: include/linux/i3c/hub.h
F: include/linux/mfd/p3h2840.h

NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER
diff --git a/drivers/i3c/Kconfig b/drivers/i3c/Kconfig
index 626c54b386d5..7536f3740c94 100644
--- a/drivers/i3c/Kconfig
+++ b/drivers/i3c/Kconfig
@@ -21,6 +21,20 @@ menuconfig I3C

if I3C
source "drivers/i3c/master/Kconfig"
+
+config I3C_HUB
+ tristate "I3C Hub Support"
+ help
+ Enable support for the I3C interface in hub devices.
+
+ This option adds virtual I3C bus support for hubs by creating
+ virtual master controllers for downstream ports and forwarding
+ bus operations through the hub device. It also provides an
+ interface used by hub drivers to enable or disable downstream
+ ports during bus transactions.
+
+ Say Y here if your platform includes an I3C hub device
+
endif # I3C

config I3C_OR_I2C
diff --git a/drivers/i3c/Makefile b/drivers/i3c/Makefile
index 11982efbc6d9..9ddee56a6338 100644
--- a/drivers/i3c/Makefile
+++ b/drivers/i3c/Makefile
@@ -2,3 +2,4 @@
i3c-y := device.o master.o
obj-$(CONFIG_I3C) += i3c.o
obj-$(CONFIG_I3C) += master/
+obj-$(CONFIG_I3C_HUB) += hub.o
diff --git a/drivers/i3c/hub.c b/drivers/i3c/hub.c
new file mode 100644
index 000000000000..6f5227215ae2
--- /dev/null
+++ b/drivers/i3c/hub.c
@@ -0,0 +1,798 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2026 NXP
+ * Generic I3C Hub core implementing virtual controller operations.
+ */
+#include <linux/i3c/device.h>
+#include <linux/i3c/hub.h>
+#include <linux/lockdep.h>
+
+#include "internals.h"
+
+/**
+ * struct i3c_hub_dev_data - Per-downstream-device hub state
+ * @parent_desc: Permanent parent-facing descriptor whose master points at the
+ * physical parent controller, used to forward controller-specific
+ * operations there.
+ *
+ * The logical descriptor on the virtual hub bus keeps its master aimed at the
+ * virtual hub controller and is never modified. This separate descriptor lets
+ * the controller-only core helpers resolve the physical parent without racing
+ * concurrent readers on the virtual bus.
+ */
+struct i3c_hub_dev_data {
+ struct i3c_dev_desc parent_desc;
+};
+
+/*
+ * All i3c_bus rw_semaphores are initialized from a single call site in the
+ * I3C core, so lockdep assigns them one shared class. When a hub forwards an
+ * operation it takes the parent bus lock while already holding its own virtual
+ * bus lock, which lockdep then reports as recursive locking on that shared
+ * class. The bus maintenance and normal-use helpers use plain down_write() and
+ * down_read(), which always acquire with subclass 0, so lockdep_set_subclass()
+ * cannot separate them; a distinct lock_class_key per nesting level is used
+ * instead.
+ *
+ * A top-level hub uses depth 1, a hub behind another hub uses depth 2, and so
+ * on, so a virtual bus lock never shares a class with the parent bus lock it
+ * nests under. Sibling ports on the same hub share a class, which is safe
+ * because they are never nested against each other. The array must stay a
+ * file-local definition: lockdep keys are identified by their address, so a
+ * single set of unique objects is required.
+ *
+ * The depth bound is generous; exceeding it only loses lockdep coverage, not
+ * correctness.
+ */
+#define I3C_HUB_MAX_LOCK_DEPTH 8
+static struct lock_class_key i3c_hub_bus_lock_keys[I3C_HUB_MAX_LOCK_DEPTH];
+
+/*
+ * The hub routing mutex (hub->lock) serializes port switching and forwarding.
+ * A child hub holds its routing mutex while reaching a parent hub that takes
+ * its own, so it needs the same per-depth lock_class_key treatment as the bus
+ * lock above, keyed identically (top-level hub depth 1, and so on). The class
+ * is assigned once in i3c_hub_init(), not per port, because all ports on a hub
+ * share this single routing mutex.
+ */
+static struct lock_class_key i3c_hub_routing_lock_keys[I3C_HUB_MAX_LOCK_DEPTH];
+
+/**
+ * i3c_hub_controller_depth() - Count hub nesting levels above a controller
+ * @controller: Virtual hub controller being initialized
+ *
+ * Walk the parent chain and count how many stacked hub controllers lead to
+ * @controller. A top-level hub attached to a physical controller returns 1.
+ * The walk stops at the first non-hub (physical) controller.
+ *
+ * Return: The hub nesting depth (>= 1 for a hub controller).
+ */
+static unsigned int
+i3c_hub_controller_depth(struct i3c_master_controller *controller)
+{
+ struct i3c_hub_controller *hub_controller;
+ unsigned int depth = 0;
+
+ while (controller && controller->ops == i3c_hub_master_ops()) {
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller)
+ break;
+ controller = hub_controller->parent;
+ depth++;
+ }
+
+ return depth;
+}
+
+/**
+ * i3c_hub_master_bus_init() - Bind controller to hub device
+ * @controller: Virtual controller for a hub port
+ *
+ * Associates the virtual controller with the hub device descriptor so that
+ * transfers are executed through the hub on the parent bus.
+ */
+static int i3c_hub_master_bus_init(struct i3c_master_controller *controller)
+{
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_hub *hub;
+ unsigned int depth;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+
+ if (!hub->hub_dev)
+ return -ENODEV;
+
+ /*
+ * Give this virtual bus lock a lockdep class keyed on its hub nesting
+ * depth before the core runs the first DAA (which forwards to the
+ * parent bus and takes the parent lock while this one is held). The
+ * lock is not held here, and controller->ops is already set, so the
+ * class can be assigned safely. Deeper hubs than the key array
+ * supports fall back to the shared class and may warn under lockdep,
+ * but still function correctly.
+ */
+ depth = i3c_hub_controller_depth(controller);
+ if (depth >= 1 && depth <= I3C_HUB_MAX_LOCK_DEPTH)
+ lockdep_set_class(&controller->bus.lock,
+ &i3c_hub_bus_lock_keys[depth - 1]);
+ else
+ WARN_ONCE(1, "i3c-hub: nesting depth %u exceeds lockdep support\n",
+ depth);
+
+ controller->this = hub->hub_dev->desc;
+ return 0;
+}
+
+static void i3c_hub_master_bus_cleanup(struct i3c_master_controller *controller)
+{
+ controller->this = NULL;
+}
+
+static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(dev);
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_hub_dev_data *data;
+ struct i3c_master_controller *parent;
+ struct i3c_hub *hub;
+ int ret;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+ if (!hub->hub_dev)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(hub->hub_dev->desc);
+ if (!parent)
+ return -ENODEV;
+
+ data = kzalloc_obj(*data);
+ if (!data)
+ return -ENOMEM;
+
+ /* Initialize the parent-facing descriptor to target the physical parent. */
+ INIT_LIST_HEAD(&data->parent_desc.common.node);
+ mutex_init(&data->parent_desc.ibi_lock);
+
+ data->parent_desc.common.master = parent;
+ data->parent_desc.info = dev->info;
+
+ i3c_bus_maintenance_lock(&parent->bus);
+ ret = i3c_master_attach_i3c_dev_controller_locked(&data->parent_desc);
+ i3c_bus_maintenance_unlock(&parent->bus);
+ if (ret) {
+ mutex_destroy(&data->parent_desc.ibi_lock);
+ kfree(data);
+ return ret;
+ }
+
+ /* Link the hub-private data (see struct i3c_hub_dev_data). */
+ i3c_dev_set_master_data(dev, data);
+
+ return 0;
+}
+
+static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev,
+ u8 old_dyn_addr)
+{
+ struct i3c_hub_dev_data *data = i3c_dev_get_master_data(dev);
+ struct i3c_master_controller *parent;
+ int ret;
+
+ if (!data)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(&data->parent_desc);
+ if (!parent)
+ return -ENODEV;
+
+ /*
+ * Reattach must not race asynchronous IBI delivery on the parent-facing
+ * descriptor. Once IBI resources are requested the parent controller may
+ * use parent_desc concurrently, so reject a reattach that arrives while
+ * the generic IBI object is still live.
+ */
+ if (WARN_ON_ONCE(data->parent_desc.ibi))
+ return -EBUSY;
+
+ /*
+ * Re-sync device information after the address change and reattach
+ * under the parent bus lock so both updates are applied as one
+ * operation with respect to parent controller state.
+ */
+ i3c_bus_maintenance_lock(&parent->bus);
+ data->parent_desc.info = dev->info;
+ ret = i3c_master_reattach_i3c_dev_controller_locked(&data->parent_desc,
+ old_dyn_addr);
+ i3c_bus_maintenance_unlock(&parent->bus);
+
+ return ret;
+}
+
+static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev)
+{
+ struct i3c_hub_dev_data *data = i3c_dev_get_master_data(dev);
+ struct i3c_master_controller *parent;
+
+ if (!data)
+ return;
+
+ parent = i3c_dev_get_master(&data->parent_desc);
+
+ /*
+ * parent_desc.ibi should already be cleared by i3c_hub_free_ibi()
+ * before we get here. If it is still set, the kfree(data) below frees
+ * a descriptor the parent controller can still reach via un-flushed
+ * asynchronous IBI work (use-after-free, not just a leak).
+ */
+ WARN_ON_ONCE(data->parent_desc.ibi);
+
+ if (parent) {
+ i3c_bus_maintenance_lock(&parent->bus);
+ i3c_master_detach_i3c_dev_controller_locked(&data->parent_desc);
+ i3c_bus_maintenance_unlock(&parent->bus);
+ }
+
+ i3c_dev_set_master_data(dev, NULL);
+ mutex_destroy(&data->parent_desc.ibi_lock);
+ kfree(data);
+}
+
+/**
+ * i3c_hub_do_daa() - Perform DAA via hub port
+ * @hub: Hub instance
+ * @controller: Virtual controller for a hub port
+ *
+ * Enables the port connection, performs DAA on the parent controller,
+ * then disables the connection.
+ */
+static int i3c_hub_do_daa(struct i3c_hub *hub,
+ struct i3c_master_controller *controller)
+{
+ struct i3c_master_controller *parent;
+ int ret;
+
+ if (!hub || !hub->hub_dev)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(hub->hub_dev->desc);
+ if (!parent)
+ return -ENODEV;
+
+ /*
+ * Lock order: hub routing mutex before the parent bus lock (taken here
+ * inside i3c_master_do_daa()). The depth-keyed lockdep classes above
+ * keep this nesting acyclic when the parent is itself a hub.
+ */
+ mutex_lock(&hub->lock);
+ i3c_hub_enable_port(controller);
+
+ /*
+ * Downstream devices reachable through hub target-port routes share the
+ * parent controller's I3C address space. The hub gates access to a
+ * target-port network, but it does not create an independent dynamic
+ * address domain per virtual bus.
+ *
+ * Run DAA on the parent controller so dynamic addresses remain unique
+ * across all downstream devices, even when they are behind different
+ * target ports.
+ */
+ ret = i3c_master_do_daa(parent);
+ i3c_hub_disable_port(controller);
+ mutex_unlock(&hub->lock);
+
+ return ret;
+}
+
+static bool i3c_hub_supports_ccc_cmd(struct i3c_hub *hub,
+ const struct i3c_ccc_cmd *cmd)
+{
+ struct i3c_master_controller *parent;
+
+ if (!hub || !hub->hub_dev)
+ return false;
+
+ parent = i3c_dev_get_master(hub->hub_dev->desc);
+ if (!parent)
+ return false;
+
+ return i3c_master_supports_ccc_cmd(parent, cmd);
+}
+
+/**
+ * i3c_hub_send_ccc_cmd() - Send CCC through hub port
+ * @hub: Hub instance
+ * @controller: Virtual controller
+ * @cmd: CCC command
+ *
+ * Enables the port connection while issuing CCC on the parent controller.
+ */
+static int i3c_hub_send_ccc_cmd(struct i3c_hub *hub,
+ struct i3c_master_controller *controller,
+ struct i3c_ccc_cmd *cmd)
+{
+ struct i3c_master_controller *parent;
+ int ret;
+
+ if (!hub || !hub->hub_dev)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(hub->hub_dev->desc);
+ if (!parent)
+ return -ENODEV;
+
+ mutex_lock(&hub->lock);
+ i3c_hub_enable_port(controller);
+ ret = i3c_master_send_ccc_cmd(parent, cmd);
+ i3c_hub_disable_port(controller);
+ mutex_unlock(&hub->lock);
+
+ return ret;
+}
+
+/**
+ * i3c_hub_master_priv_xfers() - Execute private transfers via hub
+ * @dev: Target device descriptor
+ * @xfers: Transfer array
+ * @nxfers: Number of transfers
+ * @mode: Transfer mode (SDR, HDR, etc.)
+ *
+ * Refreshes the parent-facing device info (while no IBI is pending) and
+ * forwards private transfers through the hub to the parent controller.
+ */
+static int i3c_hub_master_priv_xfers(struct i3c_dev_desc *dev,
+ struct i3c_xfer *xfers,
+ int nxfers,
+ enum i3c_xfer_mode mode)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(dev);
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_master_controller *parent;
+ struct i3c_hub_dev_data *data;
+ struct i3c_hub *hub;
+ int ret;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+
+ data = i3c_dev_get_master_data(dev);
+ if (!data)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(&data->parent_desc);
+ if (!parent)
+ return -ENODEV;
+
+ /* Lock order: hub routing mutex before the parent bus lock (see do_daa). */
+ mutex_lock(&hub->lock);
+
+ /*
+ * Only refresh the parent-facing info while no IBI is requested; once
+ * parent_desc.ibi is set it must stay immutable (see i3c_hub_request_ibi()).
+ */
+ if (!data->parent_desc.ibi)
+ data->parent_desc.info = dev->info;
+
+ i3c_hub_enable_port(controller);
+
+ i3c_bus_normaluse_lock(&parent->bus);
+ ret = i3c_dev_do_xfers_locked(&data->parent_desc, xfers,
+ nxfers, mode);
+ i3c_bus_normaluse_unlock(&parent->bus);
+
+ i3c_hub_disable_port(controller);
+
+ mutex_unlock(&hub->lock);
+
+ return ret;
+}
+
+static int i3c_hub_attach_i2c_dev(struct i2c_dev_desc *dev)
+{
+ return -EOPNOTSUPP;
+}
+
+static void i3c_hub_detach_i2c_dev(struct i2c_dev_desc *dev)
+{
+}
+
+static int i3c_hub_i2c_xfers(struct i2c_dev_desc *dev,
+ struct i2c_msg *xfers, int nxfers)
+{
+ return -EOPNOTSUPP;
+}
+
+static int i3c_hub_master_do_daa(struct i3c_master_controller *controller)
+{
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_hub *hub;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+
+ return i3c_hub_do_daa(hub, controller);
+}
+
+static int i3c_hub_master_send_ccc_cmd(struct i3c_master_controller *controller,
+ struct i3c_ccc_cmd *cmd)
+{
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_hub *hub;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+
+ if (!hub->hub_dev)
+ return -ENODEV;
+
+ /*
+ * Do not forward broadcast RSTDAA through the hub. The hub itself
+ * is visible on the parent bus, so forwarding RSTDAA would also
+ * reset the hub dynamic address. Downstream RSTDAA is not supported
+ * by the hub virtual-controller model.
+ */
+ if (cmd->id == I3C_CCC_RSTDAA(true))
+ return 0;
+
+ return i3c_hub_send_ccc_cmd(hub, controller, cmd);
+}
+
+static bool i3c_hub_master_supports_ccc_cmd(struct i3c_master_controller *controller,
+ const struct i3c_ccc_cmd *cmd)
+{
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_hub *hub;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return false;
+
+ hub = hub_controller->hub;
+
+ return i3c_hub_supports_ccc_cmd(hub, cmd);
+}
+
+/**
+ * i3c_hub_request_ibi() - Request IBI through parent controller
+ * @desc: Target device descriptor
+ * @req: IBI setup
+ *
+ * Publishes the generic IBI object on the permanent parent-facing descriptor
+ * and requests IBI for a device connected through the hub. The parent-facing
+ * descriptor references the same IBI object so the physical controller uses
+ * the logical workqueue, pending counter and client device during
+ * asynchronous IBI delivery.
+ */
+static int i3c_hub_request_ibi(struct i3c_dev_desc *desc,
+ const struct i3c_ibi_setup *req)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(desc);
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_master_controller *parent;
+ struct i3c_hub_dev_data *data;
+ struct i3c_hub *hub;
+ int ret;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+
+ data = i3c_dev_get_master_data(desc);
+ if (!data)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(&data->parent_desc);
+ if (!parent)
+ return -ENODEV;
+
+ /*
+ * Publish the final device information snapshot together with the
+ * generic IBI object under hub->lock. Keep the parent-facing
+ * information immutable while parent_desc.ibi is set and the parent
+ * controller may use the descriptor asynchronously.
+ */
+ mutex_lock(&hub->lock);
+ data->parent_desc.info = desc->info;
+ data->parent_desc.dev = desc->dev;
+ data->parent_desc.ibi = desc->ibi;
+ mutex_unlock(&hub->lock);
+
+ i3c_bus_normaluse_lock(&parent->bus);
+ ret = i3c_dev_request_ibi_controller_locked(&data->parent_desc, req);
+ i3c_bus_normaluse_unlock(&parent->bus);
+
+ if (ret) {
+ mutex_lock(&hub->lock);
+ data->parent_desc.ibi = NULL;
+ data->parent_desc.dev = NULL;
+ mutex_unlock(&hub->lock);
+ }
+
+ return ret;
+}
+
+static void i3c_hub_free_ibi(struct i3c_dev_desc *desc)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(desc);
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_master_controller *parent;
+ struct i3c_hub_dev_data *data;
+ struct i3c_hub *hub;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return;
+
+ hub = hub_controller->hub;
+
+ data = i3c_dev_get_master_data(desc);
+ if (!data || !data->parent_desc.ibi)
+ return;
+
+ parent = i3c_dev_get_master(&data->parent_desc);
+ if (!parent)
+ return;
+
+ i3c_bus_normaluse_lock(&parent->bus);
+ i3c_dev_free_ibi_controller_locked(&data->parent_desc);
+ i3c_bus_normaluse_unlock(&parent->bus);
+
+ /*
+ * The outer generic IBI free path owns and releases desc->ibi after
+ * this callback returns.
+ */
+ mutex_lock(&hub->lock);
+ data->parent_desc.ibi = NULL;
+ data->parent_desc.dev = NULL;
+ mutex_unlock(&hub->lock);
+}
+
+/**
+ * i3c_hub_enable_ibi() - Enable IBI via hub port
+ * @desc: Target device descriptor
+ *
+ * Enables port connection and forwards the IBI enable request to the parent
+ * controller.
+ */
+static int i3c_hub_enable_ibi(struct i3c_dev_desc *desc)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(desc);
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_master_controller *parent;
+ struct i3c_hub_dev_data *data;
+ struct i3c_hub *hub;
+ int ret;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+
+ data = i3c_dev_get_master_data(desc);
+ if (!data || !data->parent_desc.ibi)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(&data->parent_desc);
+ if (!parent)
+ return -ENODEV;
+
+ mutex_lock(&hub->lock);
+
+ i3c_hub_enable_port(controller);
+
+ i3c_bus_maintenance_lock(&parent->bus);
+ ret = i3c_dev_enable_ibi_controller_locked(&data->parent_desc);
+ i3c_bus_maintenance_unlock(&parent->bus);
+
+ i3c_hub_disable_port(controller);
+
+ mutex_unlock(&hub->lock);
+
+ return ret;
+}
+
+/**
+ * i3c_hub_disable_ibi() - Disable IBI via hub port
+ * @desc: Target device descriptor
+ *
+ * Enables port connection and forwards the IBI disable request to the parent
+ * controller.
+ */
+static int i3c_hub_disable_ibi(struct i3c_dev_desc *desc)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(desc);
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_master_controller *parent;
+ struct i3c_hub_dev_data *data;
+ struct i3c_hub *hub;
+ int ret;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return -ENODEV;
+
+ hub = hub_controller->hub;
+
+ data = i3c_dev_get_master_data(desc);
+ if (!data || !data->parent_desc.ibi)
+ return -ENODEV;
+
+ parent = i3c_dev_get_master(&data->parent_desc);
+ if (!parent)
+ return -ENODEV;
+
+ mutex_lock(&hub->lock);
+
+ i3c_hub_enable_port(controller);
+
+ i3c_bus_maintenance_lock(&parent->bus);
+ ret = i3c_dev_disable_ibi_controller_locked(&data->parent_desc);
+ i3c_bus_maintenance_unlock(&parent->bus);
+
+ i3c_hub_disable_port(controller);
+
+ mutex_unlock(&hub->lock);
+
+ return ret;
+}
+
+static void i3c_hub_recycle_ibi_slot(struct i3c_dev_desc *desc,
+ struct i3c_ibi_slot *slot)
+{
+ struct i3c_hub_dev_data *data = i3c_dev_get_master_data(desc);
+
+ if (!data)
+ return;
+
+ i3c_dev_recycle_ibi_slot_controller(&data->parent_desc, slot);
+}
+
+static const struct i3c_master_controller_ops i3c_hub_master_ops_data = {
+ .bus_init = i3c_hub_master_bus_init,
+ .bus_cleanup = i3c_hub_master_bus_cleanup,
+ .attach_i3c_dev = i3c_hub_attach_i3c_dev,
+ .reattach_i3c_dev = i3c_hub_reattach_i3c_dev,
+ .detach_i3c_dev = i3c_hub_detach_i3c_dev,
+ .do_daa = i3c_hub_master_do_daa,
+ .supports_ccc_cmd = i3c_hub_master_supports_ccc_cmd,
+ .send_ccc_cmd = i3c_hub_master_send_ccc_cmd,
+ .i3c_xfers = i3c_hub_master_priv_xfers,
+ .attach_i2c_dev = i3c_hub_attach_i2c_dev,
+ .detach_i2c_dev = i3c_hub_detach_i2c_dev,
+ .i2c_xfers = i3c_hub_i2c_xfers,
+ .request_ibi = i3c_hub_request_ibi,
+ .free_ibi = i3c_hub_free_ibi,
+ .enable_ibi = i3c_hub_enable_ibi,
+ .disable_ibi = i3c_hub_disable_ibi,
+ .recycle_ibi_slot = i3c_hub_recycle_ibi_slot,
+};
+
+/**
+ * i3c_hub_init() - Initialize hub context
+ * @hub: Hub instance
+ * @ops: Vendor callbacks
+ * @hub_dev: I3C hub device
+ */
+void i3c_hub_init(struct i3c_hub *hub,
+ const struct i3c_hub_ops *ops,
+ struct i3c_device *hub_dev)
+{
+ struct i3c_master_controller *parent;
+ unsigned int depth;
+
+ hub->ops = ops;
+ hub->hub_dev = hub_dev;
+ mutex_init(&hub->lock);
+
+ if (!IS_ENABLED(CONFIG_LOCKDEP))
+ return;
+
+ if (WARN_ON_ONCE(!hub_dev || !hub_dev->desc))
+ return;
+
+ parent = i3c_dev_get_master(hub_dev->desc);
+ if (WARN_ON_ONCE(!parent))
+ return;
+
+ /*
+ * The routing mutex has the same hub nesting depth as the virtual
+ * controllers this hub exposes, so the parent controller is one level
+ * shallower. Keying it once here, rather than per port, avoids
+ * reclassifying the single shared routing mutex from a later port that
+ * may already have used it.
+ */
+ depth = i3c_hub_controller_depth(parent) + 1;
+ if (WARN_ONCE(depth > I3C_HUB_MAX_LOCK_DEPTH,
+ "i3c-hub: routing lock depth %u exceeds lockdep support\n",
+ depth))
+ depth = I3C_HUB_MAX_LOCK_DEPTH;
+
+ lockdep_set_class(&hub->lock, &i3c_hub_routing_lock_keys[depth - 1]);
+}
+EXPORT_SYMBOL_GPL(i3c_hub_init);
+
+const struct i3c_master_controller_ops *i3c_hub_master_ops(void)
+{
+ return &i3c_hub_master_ops_data;
+}
+EXPORT_SYMBOL_GPL(i3c_hub_master_ops);
+
+/**
+ * i3c_hub_reserve_parent_addrslots_from_dt() - Reserve child addresses in parent bus.
+ * @hubc: I3C hub controller for a target-port virtual bus.
+ * @node: Target-port bus Device Tree node.
+ *
+ * Reserve parent bus address slots for the assigned dynamic address of each
+ * downstream I3C device described in the Device Tree, so that parent DAA does
+ * not hand the same address to another device. Downstream devices behind hub
+ * target ports share the parent controller's dynamic address space.
+ *
+ * Return: 0 on success, or a negative error code.
+ */
+int i3c_hub_reserve_parent_addrslots_from_dt(struct i3c_hub_controller *hubc,
+ struct device_node *node)
+{
+ struct i3c_master_controller *parent = hubc->parent;
+ enum i3c_addr_slot_status status;
+ u32 assigned_addr;
+ u32 reg[3];
+ int ret;
+
+ if (!parent || !node)
+ return -ENODEV;
+
+ for_each_available_child_of_node_scoped(node, child) {
+ /*
+ * Only consider addressable bus nodes: a valid "reg" is
+ * required to describe a device, but its static-address value
+ * does not affect the reservation below.
+ */
+ ret = of_property_read_variable_u32_array(child, "reg", reg, 1, 3);
+ if (ret < 0)
+ continue;
+
+ ret = of_property_read_u32(child, "assigned-address", &assigned_addr);
+ if (ret)
+ continue;
+
+ /* Skip nodes without a usable dynamic address. */
+ if (!assigned_addr || assigned_addr > I3C_MAX_ADDR)
+ continue;
+
+ i3c_bus_maintenance_lock(&parent->bus);
+ status = i3c_bus_get_addr_slot_status(&parent->bus,
+ assigned_addr);
+ if (status == I3C_ADDR_SLOT_FREE)
+ i3c_bus_set_addr_slot_status(&parent->bus,
+ assigned_addr,
+ I3C_ADDR_SLOT_I3C_DEV);
+ i3c_bus_maintenance_unlock(&parent->bus);
+ }
+ return 0;
+}
+EXPORT_SYMBOL_GPL(i3c_hub_reserve_parent_addrslots_from_dt);
+
+MODULE_AUTHOR("Aman Kumar Pandey <aman.kumarpandey@xxxxxxx>");
+MODULE_AUTHOR("Vikash Bansal <vikash.bansal@xxxxxxx>");
+MODULE_AUTHOR("Lakshay Piplani <lakshay.piplani@xxxxxxx>");
+MODULE_DESCRIPTION("Generic I3C hub support");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/i3c/hub.h b/include/linux/i3c/hub.h
new file mode 100644
index 000000000000..5de6415342dc
--- /dev/null
+++ b/include/linux/i3c/hub.h
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright 2026 NXP
+ * Generic hub definitions and helper interfaces.
+ */
+#ifndef _LINUX_I3C_HUB_H
+#define _LINUX_I3C_HUB_H
+
+#include <linux/i3c/master.h>
+#include <linux/mutex.h>
+
+/**
+ * struct i3c_hub - Generic I3C hub context
+ * @ops: Vendor callbacks for port connection control
+ * @hub_dev: I3C device representing the hub on the parent bus
+ * @lock: Serializes hub port routing/forwarding; its lockdep class is keyed
+ * per hub nesting depth in i3c_hub_init().
+ */
+struct i3c_hub {
+ const struct i3c_hub_ops *ops;
+ struct i3c_device *hub_dev;
+ struct mutex lock; /* Serializes hub port routing. */
+};
+
+struct i3c_hub_controller {
+ struct i3c_master_controller *parent;
+ struct i3c_master_controller controller;
+ struct i3c_hub *hub;
+};
+
+struct i3c_hub_ops {
+ void (*enable_port)(struct i3c_master_controller *controller);
+ void (*disable_port)(struct i3c_master_controller *controller);
+};
+
+/**
+ * i3c_hub_enable_port() - Enable hub connection for a controller
+ * @controller: Virtual controller representing a hub port
+ *
+ * Retrieves hub context from controller drvdata and invokes the vendor
+ * callback to enable the associated port connection.
+ */
+static inline void i3c_hub_enable_port(struct i3c_master_controller *controller)
+{
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_hub *hub;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return;
+
+ hub = hub_controller->hub;
+
+ if (hub && hub->ops && hub->ops->enable_port)
+ hub->ops->enable_port(controller);
+}
+
+/**
+ * i3c_hub_disable_port() - Disable hub connection for a controller
+ * @controller: Virtual controller representing a hub port
+ *
+ * Retrieves hub context from controller drvdata and invokes the vendor
+ * callback to disable the associated port connection.
+ */
+static inline void i3c_hub_disable_port(struct i3c_master_controller *controller)
+{
+ struct i3c_hub_controller *hub_controller;
+ struct i3c_hub *hub;
+
+ hub_controller = dev_get_drvdata(&controller->dev);
+ if (!hub_controller || !hub_controller->hub)
+ return;
+
+ hub = hub_controller->hub;
+
+ if (hub && hub->ops && hub->ops->disable_port)
+ hub->ops->disable_port(controller);
+}
+
+/**
+ * i3c_hub_master_ops() - Return virtual controller ops for hub ports
+ *
+ * Provides i3c_master_controller_ops used by controllers created for hub
+ * ports.
+ */
+const struct i3c_master_controller_ops *i3c_hub_master_ops(void);
+
+void i3c_hub_init(struct i3c_hub *hub,
+ const struct i3c_hub_ops *ops,
+ struct i3c_device *hub_dev);
+
+int i3c_hub_reserve_parent_addrslots_from_dt(struct i3c_hub_controller *hubc,
+ struct device_node *node);
+#endif
--
2.25.1