* [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver
@ 2026-08-26 10:38 Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support Lakshay Piplani
` (7 more replies)
0 siblings, 8 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
This series adds support for the NXP P3H2x4x family of multi-port I3C
hub devices, including their on-die regulators and downstream I3C/SMBus
target ports
The series introduces:
- Core I3C master enhancements required for hub support
- Generic I3C hub framework
- MFD driver with regulator and I3C hub child drivers for the P3H2x4x I3C hub
The dependency order within the series is:
- Patches 1 and 2 provide the I3C core helpers required by the generic
I3C hub framework
- Patch 3 provides the P3H2x4x Device Tree binding
- Patch 4 provides the MFD parent required by both the regulator and
P3H2x4x I3C hub child drivers
- Patch 5 provides the regulator child driver and depends on patch 4
- Patch 6 provides the generic I3C hub framework and depends on the
I3C core changes in patches 1 and 2
- Patch 7 provides the P3H2x4x I3C hub child driver and depends on
patches 4 and 6
- Patch 8 extends the P3H2x4x hub driver with SMBus slave mode support
and depends on patch 7
Changes in v16:
- Rename the controller-only helpers with a "_locked" suffix to
make the parent-bus locking contract explicit, route IBI slot recycling
through the controller helper, and destroy the generic IBI workqueue when
request_ibi() fails
- Fix a lockdep "recursive locking" false positive on the hub forwarding path
by giving the virtual bus lock and hub routing mutex per-nesting-depth
lockdep classes
- Fix parent-bus dynamic-address reservation to reserve by assigned-address
and only when the slot is free, avoiding ENTDAA collisions
- Make downstream reattach atomic against asynchronous IBI and reject it while
an IBI is live; detach IBI-lifetime invariant documented and checked with
WARN_ON_ONCE; underlying core IBI teardown fix deferred (known I3C-core
limitation)
- Tighten the target-port reg schema (explicit minimum 0, maximum 7, and
maximum 3 for four-port variants)
- Do not ignore regulator-enable failures during hub configuration
- Widen the SMBus poll deadline to the datasheet SDA-stuck/SCL-low recovery
window and use DMA-safe buffers for controller-agent and SMBus-agent
transfers over I3C
- Advertise I2C_FUNC_SLAVE only when CONFIG_I2C_SLAVE is enabled, reject slave
registration unless the upstream I3C device and IBI path are available, and
always clear the software slave state on unregister
- Validate the SMBus-agent IBI payload length and bound the target-port loop
by num_target_ports before dispatching receive events
- Link to v15: https://lore.kernel.org/linux-i3c/20260817103844.2142802-1-lakshay.piplani@nxp.com/T/#u
Changes in v15:
- Replace the direct attach and detach APIs with controller-only helpers
that do not modify address-slot state, device lists or generic IBI
lifecycle state
- Add controller-only helpers for device attach, reattach and detach,
and for requesting, freeing, enabling, disabling and recycling IBI
resources
- Rework the generic hub architecture to keep downstream logical device
descriptors associated with their virtual controllers
- Add a permanent parent-facing descriptor for each downstream device
and use it for operations handled by the physical parent controller
- Remove temporary descriptor reparenting and temporary hub dynamic
address replacement from downstream transfer paths
- Forward downstream private transfers and the complete IBI lifecycle,
including IBI slot recycling, through the parent-facing descriptor
with appropriate parent-bus locking
- Add binding and MFD support for the P3H2440, P3H2441, P3H2840 and
P3H2841 variants
- Fix the target-port reg schema and update the binding examples to use
SMBus target-port nodes
- Read the device capability register to determine whether the device
provides four or eight target ports
- Configure and register only the target ports implemented by the
selected device variant
- Preserve the MFD parent's driver data and store the hub context in the
shared MFD structure for use by IBI callbacks
- Validate target-port types and indices while ignoring non-target-port
children, such as the regulators node, during Device Tree parsing
- Correct the SMBus transfer timeout calculation for 400 kHz operation
- Use I2C adapter quirks to enforce the maximum supported read and write
payload lengths
- Publish callback-visible SMBus adapter state before adapter
registration and roll it back if registration fails
- Rework SMBus slave registration and unregistration to use the shared
protected-register lock and update software state only after the
corresponding hardware operation succeeds
- Update the MFD source description to identify the device as an NXP
P3H2x4x multi-port I3C hub instead of referring to it as an "MFD
device driver"
- Link to v14: https://lore.kernel.org/linux-i3c/20260714092053.2461482-1-lakshay.piplani@nxp.com/T/#u
Changes in v14:
- Add i3c_master_register_fwnode() for virtual I3C masters and use it instead of temporarily changing
parent dev->of_node
- Add runtime PM handling in i3c_master_send_ccc_cmd()
- Export i3c_bus_maintenance_lock()/unlock() and use them in hub paths
- Fix IBI request/free cleanup to handle forwarded callbacks that clear dev->ibi, and destroy the
allocated IBI workqueue on request failure
- Add a shared MFD protected_reg_lock and use it for protected register accesses across P3H2X4X
child drivers
- Rework regulator protected-register handling to restore the original protection state after
each operation
- Serialize hub route selection around DAA, CCC, private transfer and IBI paths
- Hold the parent maintenance lock across temporary hub address reattach, transfer and address
restore
- Fix SMBus polling interval calculation
- Clean up already registered SMBus adapters on adapter allocation or registration failure
- Fix SMBus slave receive status handling by clearing receive flags on error paths, using FIELD_GET()
and explicitly clearing overflow status
- Link to v13: https://lore.kernel.org/linux-i3c/20260701065755.2067793-1-lakshay.piplani@nxp.com/T/#u
Changes in v13:
- Fix I3C master address management in direct attach/detach paths by using i3c_master_get_i3c_addrs()
adding rollback on failure, skipping master self attach/detach, and properly releasing addresses to
avoid stale state and use-after-free issues.
- Export and document address slot helper APIs for I3C hub support.
- Reserve parent bus address slots for downstream devices with identical static and assigned addresses
by parsing target-port DT nodes prior to virtual controller registration, preventing DAA conflicts.
- Keep broadcast RSTDAA blocked with added documentation, and clarify intentional no-op callbacks
and pending TODOs (e.g., IBI slot recycle).
- Rework SMBus transaction handling to use polling instead of fixed delays
avoiding premature reads and data corruption.
- Fix DT handling issues by preventing duplicate target-port node leaks and restoring dev->of_node
after temporary modification.
- Improve resource management using devm-based cleanup for DT nodes, IBI setup, adapter unregister
and register relock paths.
- Make IBI setup optional and robust to avoid probe failures on unsupported platforms, with proper cleanup.
- Fix SMBus slave receive path by improving buffer handling, event delivery, and handling of unregistered ports.
- Strengthen concurrency handling with proper locking around shared state.
- Link to v12: https://lore.kernel.org/linux-i3c/20260617110355.1591844-1-lakshay.piplani@nxp.com/T/#u
Changes in v12:
- Rebased on i3c/next
- Dropped patches 1/9 and 2/9 from v11 as they are already applied
- Add address check in i3c_master_direct_detach_i3c_dev_locked() to skip
detach for unaddressed devices
- Drop redundant depends on I3C from config I3C_HUB
- Return -EOPNOTSUPP for unsupported I2C transfers in the generic hub ops
- Correct default pull-up and drive-strength values
- Add devm cleanup for the IBI request/enable path
- Remove dead code and simplify cleanup by relying on devm-managed resources
- Fix SMBus slave client NULL handling and unregister cleanup
- Link to v11: https://lore.kernel.org/linux-i3c/20260612111816.3688240-1-lakshay.piplani@nxp.com/T/#u
Changes in v11:
- Convert i3c_master_supports_ccc_cmd() to return bool and align
semantics with CCC support checks used by the I3C core
- Use MFD_CELL_NAME() for child device registration
- Rename driver names to follow subsystem conventions:
- Use '-' instead of '_' in driver names
- Drop the "_drv" suffix from driver names
- Fix virtual hub address reattach handling and parent bus locking
- Fix IBI request and cleanup error paths
- Improve SMBus slave mode payload validation and parsing
- Link to v10: https://lore.kernel.org/linux-i3c/20260525064209.2263045-1-lakshay.piplani@nxp.com/T/#u
Changes in v10:
- Rename i3c_master_reattach_i3c_dev() to *_locked to reflect required
bus locking
- Rename i3c_master_direct_attach_i3c_dev() and i3c_master_direct_detach_i3c_dev()
to *_locked, as these APIs must be called with the bus lock held for write
- Drop redundant is_p3h2x4x_in_i3c flag from p3h2840.h
- 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
- Split SMBus target/slave mode support, including IBI and MCTP receive handling,
into a separate patch
- Link to v9: https://lore.kernel.org/linux-i3c/20260420105222.1562243-1-lakshay.piplani@nxp.com/T/#u
Changes in v9:
- Renamed macros to follow consistent uppercase naming conventions
- Made REGMAP selects in the P3H2X4X MFD Kconfig conditional, to avoid I3C/I2C dependency issues
- Referenced i3c.yaml and i2c-controller.yaml for child bus nodes
- Dropped unnecessary #address-cells and #size-cells from child nodes
- Added CONFIG_I2C_SLAVE guards where necessary to avoid build errors when I2C slave support is disabled
- Link to v8: https://lore.kernel.org/linux-i3c/20260323062737.886728-1-lakshay.piplani@nxp.com/T/#u
Changes in v8:
- Add compatible in i3c example
- Link to v7: https://lore.kernel.org/linux-i3c/20260319112441.3888957-1-lakshay.piplani@nxp.com/T/#u
Changes in v7:
- Fix kernel-doc warnings across I3C core and hub code
- Rework DT binding schema and examples to pass dt_binding_check
- Update MFD Kconfig to use I3C_OR_I2C
- Convert CONFIG_I3C_HUB to tristate
- Remove unnecessary CONFIG_I2C_SLAVE guards
- Replace custom helpers with find_closest()
- Use devm_regulator_get_enable_optional()
- Link to v6: https://lore.kernel.org/linux-i3c/64c5070c-aa9e-427a-933e-91e168f0510c@kernel.org/T/#u
Changes in v6:
- Update DT binding with vendor-prefixed properties
- Add generic I3C hub support
- Remove generic code from P3H2x4x driver
- Link to v5: https://lore.kernel.org/linux-i3c/20260206120121.856471-1-aman.kumarpandey@nxp.com/T/#u
Changes in v5:
- Update supply naming and descriptions
- Improve MFD Kconfig/Makefile ordering
- Link to v4: https://lore.kernel.org/linux-i3c/20260113114529.1692213-2-aman.kumarpandey@nxp.com/T/#u
Changes in v4:
- Split driver into MFD, regulator and I3C hub parts
- Update I3C master for hub support
- Fix DT binding issues
- Link to v3: https://lore.kernel.org/linux-i3c/20250811-bittern-of-abstract-prestige-aaeda9@kuoka/T/#u
Changes in v3:
- Add MFD support for hub and regulators
- Add regulator integration
- Link to v2: https://lore.kernel.org/linux-i3c/17145d2f-5d07-4939-8381-74e27cde303c@kernel.org/T/#u
Changes in v2:
- Fix DT binding warnings
- Refine DT parsing logic
- Link to v1: https://lore.kernel.org/linux-i3c/822d6dca-b2c6-4439-ade5-219620ebc435@kernel.org/T/#u
Aman Kumar Pandey (5):
i3c: master: Add APIs for I3C hub support
dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x
i3c hub
i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
Lakshay Piplani (3):
i3c: master: Add controller-only device operation helpers
i3c: hub: Add support for the I3C interface in the I3C hub
i3c: hub: p3h2x4x: Add SMBus slave mode support
.../devicetree/bindings/i3c/nxp,p3h2840.yaml | 324 +++++++
MAINTAINERS | 15 +
drivers/i3c/Kconfig | 15 +
drivers/i3c/Makefile | 2 +
drivers/i3c/hub.c | 798 ++++++++++++++++++
drivers/i3c/hub/Kconfig | 11 +
drivers/i3c/hub/Makefile | 4 +
drivers/i3c/hub/p3h2840_i3c_hub.h | 362 ++++++++
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 416 +++++++++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 171 ++++
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 654 ++++++++++++++
drivers/i3c/internals.h | 14 +
drivers/i3c/master.c | 395 +++++++--
drivers/mfd/Kconfig | 13 +
drivers/mfd/Makefile | 1 +
drivers/mfd/p3h2840.c | 168 ++++
drivers/regulator/Kconfig | 10 +
drivers/regulator/Makefile | 1 +
drivers/regulator/p3h2840_i3c_hub_regulator.c | 284 +++++++
include/linux/i3c/device.h | 2 +
include/linux/i3c/hub.h | 94 +++
include/linux/i3c/master.h | 17 +
include/linux/mfd/p3h2840.h | 41 +
23 files changed, 3757 insertions(+), 55 deletions(-)
create mode 100644 Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
create mode 100644 drivers/i3c/hub.c
create mode 100644 drivers/i3c/hub/Kconfig
create mode 100644 drivers/i3c/hub/Makefile
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub.h
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_common.c
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_i3c.c
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
create mode 100644 drivers/mfd/p3h2840.c
create mode 100644 drivers/regulator/p3h2840_i3c_hub_regulator.c
create mode 100644 include/linux/i3c/hub.h
create mode 100644 include/linux/mfd/p3h2840.h
--
2.25.1
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
2026-08-26 10:52 ` sashiko-bot
2026-08-26 16:36 ` Frank Li
2026-08-26 10:38 ` [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers Lakshay Piplani
` (6 subsequent siblings)
7 siblings, 2 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
From: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Add CCC helpers to check CCC support and send CCC commands, address slot
helpers to query and update I3C bus address slot state, registering virtual
masters with an explicit firmware node, and exposing the bus maintenance
lock helpers.
These additions prepare for I3C hub support. A hub driver needs to reserve
and query parent bus address slots, forward CCC commands, register virtual
target port controllers using the target-port firmware node, and serialize
operations against the parent bus maintenance lock.
The hub also forwards private transfers via i3c_dev_do_xfers_locked() and
serializes its IBI and private-transfer paths against the shared lock, so
the normal-use lock/unlock pair is exposed alongside the maintenance-lock
helpers.
i3c_master_register_fwnode() allows virtual I3C masters to register using a
firmware node different from their parent device node without temporarily
modifying parent->of_node.
The new helpers are:
1) i3c_master_send_ccc_cmd()
2) i3c_master_supports_ccc_cmd()
3) i3c_bus_get_addr_slot_status()
4) i3c_bus_set_addr_slot_status()
5) i3c_bus_maintenance_lock()
6) i3c_bus_maintenance_unlock()
7) i3c_master_register_fwnode()
8) i3c_bus_normaluse_lock()
9) i3c_bus_normaluse_unlock()
10) i3c_dev_do_xfers_locked()
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
Changes in v16:
- Rewrite the commit message to match the code, It now describes only
the helpers actually exported
Changes in v15:
- Drop the direct attach and detach helpers that also modified address-slot
state
- Export these APIs:
- i3c_bus_normaluse_lock()
- i3c_bus_normaluse_unlock()
- i3c_dev_do_xfers_locked()
Changes in v14:
- Add i3c_master_register_fwnode() to register virtual I3C masters with an
explicit firmware node
- Export i3c_bus_maintenance_lock() and i3c_bus_maintenance_unlock()
- Add runtime PM get/put around i3c_master_send_ccc_cmd()
- Make i3c_master_supports_ccc_cmd() return false when the controller does
not implement send_ccc_cmd()
Changes in v13:
- Fix address handling in direct attach by using i3c_master_get_i3c_addrs() and
adding rollback on failure to prevent bus address collisions
- Fix detach path by clearing master_priv and releasing addresses to avoid use-after-free
and stale state issues
- Export address slot helper APIs and add kernel-doc for them
Changes in v12:
- Add address check in i3c_master_direct_detach_i3c_dev_locked() to skip
detach for unaddressed devices.
Changes in v11:
- Convert i3c_master_supports_ccc_cmd() to return bool and align
semantics with CCC support checks used by the I3C core
Changes in v10:
- Rename i3c_master_direct_attach_i3c_dev and i3c_master_direct_detach_i3c_dev
APIs to *_locked, as these APIs must be called with the bus lock held in
write mode
Changes in v9:
- No change
Changes in v8:
- No change
Changes in v7:
- Update commit message to clarify purpose (prepare for I3C hub support)
Changes in v6:
- Split the patch into two parts:
1) expose the existing API
2) add new APIs.
---
---
drivers/i3c/master.c | 155 +++++++++++++++++++++++++++++++------
include/linux/i3c/master.h | 17 ++++
2 files changed, 149 insertions(+), 23 deletions(-)
diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
index f1be38a640ca..03fb41f0786c 100644
--- a/drivers/i3c/master.c
+++ b/drivers/i3c/master.c
@@ -44,10 +44,11 @@ static BLOCKING_NOTIFIER_HEAD(i3c_bus_notifier);
* logic to rely on I3C device information that could be changed behind their
* back.
*/
-static void i3c_bus_maintenance_lock(struct i3c_bus *bus)
+void i3c_bus_maintenance_lock(struct i3c_bus *bus)
{
down_write(&bus->lock);
}
+EXPORT_SYMBOL_GPL(i3c_bus_maintenance_lock);
/**
* i3c_bus_maintenance_unlock - Release the bus lock after a maintenance
@@ -58,10 +59,11 @@ static void i3c_bus_maintenance_lock(struct i3c_bus *bus)
* i3c_bus_maintenance_lock() for more details on what these maintenance
* operations are.
*/
-static void i3c_bus_maintenance_unlock(struct i3c_bus *bus)
+void i3c_bus_maintenance_unlock(struct i3c_bus *bus)
{
up_write(&bus->lock);
}
+EXPORT_SYMBOL_GPL(i3c_bus_maintenance_unlock);
/**
* i3c_bus_normaluse_lock - Lock the bus for a normal operation
@@ -83,6 +85,7 @@ void i3c_bus_normaluse_lock(struct i3c_bus *bus)
{
down_read(&bus->lock);
}
+EXPORT_SYMBOL_GPL(i3c_bus_normaluse_lock);
/**
* i3c_bus_normaluse_unlock - Release the bus lock after a normal operation
@@ -96,6 +99,7 @@ void i3c_bus_normaluse_unlock(struct i3c_bus *bus)
{
up_read(&bus->lock);
}
+EXPORT_SYMBOL_GPL(i3c_bus_normaluse_unlock);
static struct i3c_master_controller *
i3c_bus_to_i3c_master(struct i3c_bus *i3cbus)
@@ -385,11 +389,19 @@ i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask)
return status & mask;
}
-static enum i3c_addr_slot_status
+/**
+ * i3c_bus_get_addr_slot_status() - Get I3C bus address slot status
+ * @bus: I3C bus.
+ * @addr: I3C address to query.
+ *
+ * Return: Address slot status for @addr.
+ */
+enum i3c_addr_slot_status
i3c_bus_get_addr_slot_status(struct i3c_bus *bus, u16 addr)
{
return i3c_bus_get_addr_slot_status_mask(bus, addr, I3C_ADDR_SLOT_STATUS_MASK);
}
+EXPORT_SYMBOL_GPL(i3c_bus_get_addr_slot_status);
static void i3c_bus_set_addr_slot_status_mask(struct i3c_bus *bus, u16 addr,
enum i3c_addr_slot_status status, u32 mask)
@@ -405,11 +417,18 @@ static void i3c_bus_set_addr_slot_status_mask(struct i3c_bus *bus, u16 addr,
*ptr |= ((unsigned long)status & mask) << (bitpos % BITS_PER_LONG);
}
-static void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr,
- enum i3c_addr_slot_status status)
+/**
+ * i3c_bus_set_addr_slot_status() - Set I3C bus address slot status
+ * @bus: I3C bus.
+ * @addr: I3C address to update.
+ * @status: Address slot status to set.
+ */
+void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr,
+ enum i3c_addr_slot_status status)
{
i3c_bus_set_addr_slot_status_mask(bus, addr, status, I3C_ADDR_SLOT_STATUS_MASK);
}
+EXPORT_SYMBOL_GPL(i3c_bus_set_addr_slot_status);
static bool i3c_bus_dev_addr_is_avail(struct i3c_bus *bus, u8 addr)
{
@@ -2548,6 +2567,59 @@ static void i3c_master_reconcile_dyn_addrs(struct i3c_master_controller *master)
}
}
+/**
+ * i3c_master_supports_ccc_cmd() - check CCC command support
+ * @master: I3C master controller
+ * @cmd: CCC command to verify
+ *
+ * Return: true if @cmd is supported, false otherwise.
+ */
+bool i3c_master_supports_ccc_cmd(struct i3c_master_controller *master,
+ const struct i3c_ccc_cmd *cmd)
+{
+ if (!master || !cmd)
+ return false;
+
+ if (!master->ops->send_ccc_cmd)
+ return false;
+
+ if (!master->ops->supports_ccc_cmd)
+ return true;
+
+ return master->ops->supports_ccc_cmd(master, cmd);
+}
+EXPORT_SYMBOL_GPL(i3c_master_supports_ccc_cmd);
+
+/**
+ * i3c_master_send_ccc_cmd() - send a CCC command
+ * @master: I3C master controller issuing the command
+ * @cmd: CCC command to be sent
+ *
+ * This function sends a Common Command Code (CCC) command to devices on the
+ * I3C bus. It acquires the bus maintenance lock, executes the command, and
+ * then releases the lock to ensure safe access to the bus.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+int i3c_master_send_ccc_cmd(struct i3c_master_controller *master,
+ struct i3c_ccc_cmd *cmd)
+{
+ int ret;
+
+ ret = i3c_master_rpm_get(master);
+ if (ret)
+ return ret;
+
+ i3c_bus_maintenance_lock(&master->bus);
+ ret = i3c_master_send_ccc_cmd_locked(master, cmd);
+ i3c_bus_maintenance_unlock(&master->bus);
+
+ i3c_master_rpm_put(master);
+
+ return ret;
+}
+EXPORT_SYMBOL_GPL(i3c_master_send_ccc_cmd);
+
/**
* i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version)
* @master: controller
@@ -3195,34 +3267,31 @@ static int i3c_master_check_ops(const struct i3c_master_controller_ops *ops)
}
/**
- * i3c_master_register() - register an I3C master
+ * i3c_master_register_fwnode() - register an I3C master with a custom fwnode
* @master: master used to send frames on the bus
- * @parent: the parent device (the one that provides this I3C master
- * controller)
+ * @parent: the parent device providing this I3C master controller
+ * @fwnode: firmware node describing this I3C bus, or NULL
* @ops: the master controller operations
- * @secondary: true if you are registering a secondary master. Will return
- * -EOPNOTSUPP if set to true since secondary masters are not yet
- * supported
+ * @secondary: true if registering a secondary master
*
- * This function takes care of everything for you:
+ * This helper is useful for virtual I3C masters whose firmware node is not
+ * the same as @parent's firmware node.
*
- * - creates and initializes the I3C bus
- * - populates the bus with static I2C devs if @parent->of_node is not
- * NULL
- * - registers all I3C devices added by the controller during bus
- * initialization
- * - registers the I2C adapter and all I2C devices
+ * Only OF-backed fwnodes are supported for now, because the I3C core still
+ * stores the bus node in master->dev.of_node and populates the bus using OF.
*
* Return: 0 in case of success, a negative error code otherwise.
*/
-int i3c_master_register(struct i3c_master_controller *master,
- struct device *parent,
- const struct i3c_master_controller_ops *ops,
- bool secondary)
+int i3c_master_register_fwnode(struct i3c_master_controller *master,
+ struct device *parent,
+ struct fwnode_handle *fwnode,
+ const struct i3c_master_controller_ops *ops,
+ bool secondary)
{
unsigned long i2c_scl_rate = I3C_BUS_I2C_FM_PLUS_SCL_MAX_RATE;
struct i3c_bus *i3cbus = i3c_master_get_bus(master);
enum i3c_bus_mode mode = I3C_BUS_MODE_PURE;
+ struct device_node *np = NULL;
struct i2c_dev_boardinfo *i2cbi;
int ret;
@@ -3234,8 +3303,14 @@ int i3c_master_register(struct i3c_master_controller *master,
if (ret)
return ret;
+ if (fwnode) {
+ np = to_of_node(fwnode);
+ if (!np)
+ return -EINVAL;
+ }
+
master->dev.parent = parent;
- master->dev.of_node = of_node_get(parent->of_node);
+ master->dev.of_node = of_node_get(np);
master->dev.bus = &i3c_bus_type;
master->dev.type = &i3c_masterdev_type;
master->dev.release = i3c_masterdev_release;
@@ -3352,6 +3427,39 @@ int i3c_master_register(struct i3c_master_controller *master,
return ret;
}
+EXPORT_SYMBOL_GPL(i3c_master_register_fwnode);
+
+/**
+ * i3c_master_register() - register an I3C master
+ * @master: master used to send frames on the bus
+ * @parent: the parent device (the one that provides this I3C master
+ * controller)
+ * @ops: the master controller operations
+ * @secondary: true if you are registering a secondary master. Will return
+ * -EOPNOTSUPP if set to true since secondary masters are not yet
+ * supported
+ *
+ * This function takes care of everything for you:
+ *
+ * - creates and initializes the I3C bus
+ * - populates the bus with static I2C devs if @parent->of_node is not
+ * NULL
+ * - registers all I3C devices added by the controller during bus
+ * initialization
+ * - registers the I2C adapter and all I2C devices
+ *
+ * Return: 0 in case of success, a negative error code otherwise.
+ */
+int i3c_master_register(struct i3c_master_controller *master,
+ struct device *parent,
+ const struct i3c_master_controller_ops *ops,
+ bool secondary)
+{
+ return i3c_master_register_fwnode(master, parent,
+ parent->of_node ?
+ of_fwnode_handle(parent->of_node) : NULL,
+ ops, secondary);
+}
EXPORT_SYMBOL_GPL(i3c_master_register);
/**
@@ -3412,6 +3520,7 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers,
return master->ops->i3c_xfers(dev, xfers, nxfers, mode);
}
+EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked);
/**
* i3c_dev_disable_ibi_locked() - Disable IBIs coming from a specific device
diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h
index 4d2a68793324..32c686ae5d62 100644
--- a/include/linux/i3c/master.h
+++ b/include/linux/i3c/master.h
@@ -627,9 +627,18 @@ DEFINE_FREE(i3c_master_dma_unmap_single, void *,
int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev,
u8 old_dyn_addr);
+int i3c_master_send_ccc_cmd(struct i3c_master_controller *master,
+ struct i3c_ccc_cmd *cmd);
+bool i3c_master_supports_ccc_cmd(struct i3c_master_controller *master,
+ const struct i3c_ccc_cmd *cmd);
int i3c_master_set_info(struct i3c_master_controller *master,
const struct i3c_device_info *info);
+int i3c_master_register_fwnode(struct i3c_master_controller *master,
+ struct device *parent,
+ struct fwnode_handle *fwnode,
+ const struct i3c_master_controller_ops *ops,
+ bool secondary);
int i3c_master_register(struct i3c_master_controller *master,
struct device *parent,
const struct i3c_master_controller_ops *ops,
@@ -752,4 +761,12 @@ void i3c_for_each_bus_locked(int (*fn)(struct i3c_bus *bus, void *data),
int i3c_register_notifier(struct notifier_block *nb);
int i3c_unregister_notifier(struct notifier_block *nb);
+enum i3c_addr_slot_status
+i3c_bus_get_addr_slot_status(struct i3c_bus *bus, u16 addr);
+
+void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr,
+ enum i3c_addr_slot_status status);
+
+void i3c_bus_maintenance_lock(struct i3c_bus *bus);
+void i3c_bus_maintenance_unlock(struct i3c_bus *bus);
#endif /* I3C_MASTER_H */
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
2026-08-26 10:50 ` sashiko-bot
2026-08-26 16:42 ` Frank Li
2026-08-26 10:38 ` [PATCH v16 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support Lakshay Piplani
` (5 subsequent siblings)
7 siblings, 2 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
The generic I3C device attach and IBI paths perform both core-level
bookkeeping and controller-specific operations. An I3C hub, however,
maintains a logical device descriptor on the virtual downstream bus and
a separate parent-facing descriptor used by the physical parent
controller.
Add controller-only helpers that invoke the controller callbacks
without updating the bus device list, address-slot state, or generic IBI
lifecycle. This allows the hub framework to keep the logical descriptor
associated with the virtual controller while using a permanent
parent-facing descriptor for operations handled by the physical
controller.
Add helpers for:
- attaching, reattaching, and detaching a device from a controller;
- requesting and freeing controller IBI resources;
- enabling and disabling IBIs at the controller;
- recycling controller IBI slots.
Refactor the existing generic core paths to use these helpers, keeping
the current behaviour unchanged for regular I3C devices.
Also release the generic IBI workqueue and state when the controller
request callback fails.
The helpers are declared in the I3C internal header because they are
intended for use by the generic I3C hub framework rather than by
individual controller drivers.
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
Changes in v16:
- Rename the controller-only device helpers with a "_locked" suffix to make
the locking contract explicit (caller must hold the parent bus lock)
- Route i3c_master_handle_ibi() through i3c_dev_recycle_ibi_slot_controller()
instead of calling master->ops->recycle_ibi_slot() directly
- Destroy the generic IBI workqueue when the controller request_ibi()
callback fails, avoiding a workqueue leak on the request error path
Changes in v15:
- Rework the patch to introduce controller-only attach, reattach and detach
helpers for use by the I3C hub core
- Add controller-only helpers for requesting, freeing, enabling, disabling
and recycling IBI resources
---
---
drivers/i3c/internals.h | 14 +++
drivers/i3c/master.c | 240 ++++++++++++++++++++++++++++++++++------
2 files changed, 222 insertions(+), 32 deletions(-)
diff --git a/drivers/i3c/internals.h b/drivers/i3c/internals.h
index 0f1f3f766623..214682b66f26 100644
--- a/drivers/i3c/internals.h
+++ b/drivers/i3c/internals.h
@@ -22,6 +22,20 @@ int i3c_dev_setdasa_locked(struct i3c_dev_desc *dev);
int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev,
struct i3c_xfer *xfers,
int nxfers, enum i3c_xfer_mode mode);
+
+int i3c_master_attach_i3c_dev_controller_locked(struct i3c_dev_desc *dev);
+int i3c_master_reattach_i3c_dev_controller_locked(struct i3c_dev_desc *dev,
+ u8 old_dyn_addr);
+void i3c_master_detach_i3c_dev_controller_locked(struct i3c_dev_desc *dev);
+
+int i3c_dev_disable_ibi_controller_locked(struct i3c_dev_desc *dev);
+int i3c_dev_enable_ibi_controller_locked(struct i3c_dev_desc *dev);
+int i3c_dev_request_ibi_controller_locked(struct i3c_dev_desc *dev,
+ const struct i3c_ibi_setup *req);
+void i3c_dev_free_ibi_controller_locked(struct i3c_dev_desc *dev);
+void i3c_dev_recycle_ibi_slot_controller(struct i3c_dev_desc *dev,
+ struct i3c_ibi_slot *slot);
+
int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev);
int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev);
int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev,
diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
index 03fb41f0786c..88379b6809a6 100644
--- a/drivers/i3c/master.c
+++ b/drivers/i3c/master.c
@@ -1764,6 +1764,30 @@ static int i3c_master_get_i3c_addrs(struct i3c_dev_desc *dev)
return -EBUSY;
}
+/**
+ * i3c_master_attach_i3c_dev_controller_locked() - Attach device state to
+ * controller
+ * @dev: I3C device descriptor
+ *
+ * Invoke the current controller's attach callback without changing address
+ * slot state or adding the device to the controller's device list.
+ *
+ * Context: The caller must hold the bus lock.
+ *
+ * Return: 0 on success, or a negative error code returned by the controller.
+ */
+int i3c_master_attach_i3c_dev_controller_locked(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *master = i3c_dev_get_master(dev);
+
+ /* Do not attach the master device itself. */
+ if (master->this != dev && master->ops->attach_i3c_dev)
+ return master->ops->attach_i3c_dev(dev);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(i3c_master_attach_i3c_dev_controller_locked);
+
static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master,
struct i3c_dev_desc *dev)
{
@@ -1781,12 +1805,10 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master,
return ret;
/* Do not attach the master device itself. */
- if (master->this != dev && master->ops->attach_i3c_dev) {
- ret = master->ops->attach_i3c_dev(dev);
- if (ret) {
- i3c_master_put_i3c_addrs(dev);
- return ret;
- }
+ ret = i3c_master_attach_i3c_dev_controller_locked(dev);
+ if (ret) {
+ i3c_master_put_i3c_addrs(dev);
+ return ret;
}
list_add_tail(&dev->common.node, &master->bus.devs.i3c);
@@ -1794,6 +1816,31 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master,
return 0;
}
+/**
+ * i3c_master_reattach_i3c_dev_controller_locked() - Reattach controller
+ * device state
+ * @dev: I3C device descriptor
+ * @old_dyn_addr: Previous dynamic address
+ *
+ * Invoke the current controller's reattach callback without modifying the
+ * controller's address-slot state.
+ *
+ * Context: The caller must hold the bus lock.
+ *
+ * Return: 0 on success, or a negative error code returned by the controller.
+ */
+int i3c_master_reattach_i3c_dev_controller_locked(struct i3c_dev_desc *dev,
+ u8 old_dyn_addr)
+{
+ struct i3c_master_controller *master = i3c_dev_get_master(dev);
+
+ if (master->ops->reattach_i3c_dev)
+ return master->ops->reattach_i3c_dev(dev, old_dyn_addr);
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(i3c_master_reattach_i3c_dev_controller_locked);
+
/**
* i3c_master_reattach_i3c_dev_locked() - reattach an I3C device with a new address
* @dev: I3C device descriptor to reattach
@@ -1824,25 +1871,39 @@ int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev,
I3C_ADDR_SLOT_FREE);
}
- if (master->ops->reattach_i3c_dev) {
- ret = master->ops->reattach_i3c_dev(dev, old_dyn_addr);
- if (ret) {
- i3c_master_put_i3c_addrs(dev);
- return ret;
- }
+ ret = i3c_master_reattach_i3c_dev_controller_locked(dev, old_dyn_addr);
+ if (ret) {
+ i3c_master_put_i3c_addrs(dev);
+ return ret;
}
return 0;
}
EXPORT_SYMBOL_GPL(i3c_master_reattach_i3c_dev_locked);
-static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev)
+/**
+ * i3c_master_detach_i3c_dev_controller_locked() - Detach device state from
+ * controller
+ * @dev: I3C device descriptor
+ *
+ * Invoke the current controller's detach callback without releasing address
+ * slots or removing the device from the controller's device list.
+ *
+ * Context: The caller must hold the bus lock.
+ */
+void i3c_master_detach_i3c_dev_controller_locked(struct i3c_dev_desc *dev)
{
struct i3c_master_controller *master = i3c_dev_get_master(dev);
/* Do not detach the master device itself. */
if (master->this != dev && master->ops->detach_i3c_dev)
master->ops->detach_i3c_dev(dev);
+}
+EXPORT_SYMBOL_GPL(i3c_master_detach_i3c_dev_controller_locked);
+
+static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev)
+{
+ i3c_master_detach_i3c_dev_controller_locked(dev);
i3c_master_put_i3c_addrs(dev);
list_del(&dev->common.node);
@@ -3081,7 +3142,6 @@ static void i3c_master_handle_ibi(struct work_struct *work)
struct i3c_ibi_slot *slot = container_of(work, struct i3c_ibi_slot,
work);
struct i3c_dev_desc *dev = slot->dev;
- struct i3c_master_controller *master = i3c_dev_get_master(dev);
struct i3c_ibi_payload payload;
payload.data = slot->data;
@@ -3090,7 +3150,7 @@ static void i3c_master_handle_ibi(struct work_struct *work)
if (dev->dev)
dev->ibi->handler(dev->dev, &payload);
- master->ops->recycle_ibi_slot(dev, slot);
+ i3c_dev_recycle_ibi_slot_controller(dev, slot);
if (atomic_dec_and_test(&dev->ibi->pending_ibis))
complete(&dev->ibi->all_ibis_handled);
}
@@ -3201,6 +3261,26 @@ i3c_generic_ibi_alloc_pool(struct i3c_dev_desc *dev,
}
EXPORT_SYMBOL_GPL(i3c_generic_ibi_alloc_pool);
+/**
+ * i3c_dev_recycle_ibi_slot_controller() - Recycle an IBI slot through
+ * the current controller
+ * @dev: I3C device descriptor
+ * @slot: IBI slot to recycle
+ *
+ * Invoke the current controller's IBI slot recycling callback.
+ *
+ * The controller is responsible for synchronizing access to its IBI pool.
+ */
+void i3c_dev_recycle_ibi_slot_controller(struct i3c_dev_desc *dev,
+ struct i3c_ibi_slot *slot)
+{
+ struct i3c_master_controller *master = i3c_dev_get_master(dev);
+
+ if (master->ops->recycle_ibi_slot)
+ master->ops->recycle_ibi_slot(dev, slot);
+}
+EXPORT_SYMBOL_GPL(i3c_dev_recycle_ibi_slot_controller);
+
/**
* i3c_generic_ibi_get_free_slot() - Get a free slot from a generic IBI pool
* @pool: the pool to query an IBI slot on
@@ -3522,6 +3602,32 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers,
}
EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked);
+/**
+ * i3c_dev_disable_ibi_controller_locked() - Disable IBI in the controller
+ * @dev: I3C device descriptor
+ *
+ * Invoke the current controller's IBI disable callback without waiting for
+ * pending IBIs or updating the generic IBI enabled state.
+ *
+ * Context: The caller must serialize access to @dev->ibi and the generic
+ * IBI lifecycle.
+ *
+ * Return: 0 on success, or a negative error code.
+ */
+int i3c_dev_disable_ibi_controller_locked(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *master = i3c_dev_get_master(dev);
+
+ if (!dev->ibi)
+ return -EINVAL;
+
+ if (!master->ops->disable_ibi)
+ return -EOPNOTSUPP;
+
+ return master->ops->disable_ibi(dev);
+}
+EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_controller_locked);
+
/**
* i3c_dev_disable_ibi_locked() - Disable IBIs coming from a specific device
* @dev: device on which IBIs should be disabled
@@ -3534,14 +3640,9 @@ EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked);
*/
int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev)
{
- struct i3c_master_controller *master;
int ret;
- if (!dev->ibi)
- return -EINVAL;
-
- master = i3c_dev_get_master(dev);
- ret = master->ops->disable_ibi(dev);
+ ret = i3c_dev_disable_ibi_controller_locked(dev);
if (ret)
return ret;
@@ -3555,6 +3656,32 @@ int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev)
}
EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_locked);
+/**
+ * i3c_dev_enable_ibi_controller_locked() - Enable controller IBI resources
+ * @dev: I3C device descriptor
+ *
+ * Invoke the current controller's IBI enable callback without updating the
+ * generic IBI enabled state.
+ *
+ * Context: The caller must serialize access to @dev->ibi and the generic
+ * IBI lifecycle.
+ *
+ * Return: 0 on success, or a negative error code.
+ */
+int i3c_dev_enable_ibi_controller_locked(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *master = i3c_dev_get_master(dev);
+
+ if (!dev->ibi)
+ return -EINVAL;
+
+ if (!master->ops->enable_ibi)
+ return -EOPNOTSUPP;
+
+ return master->ops->enable_ibi(dev);
+}
+EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_controller_locked);
+
/**
* i3c_dev_enable_ibi_locked() - Enable IBIs from a specific device (lock held)
* @dev: device on which IBIs should be enabled
@@ -3571,13 +3698,9 @@ EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_locked);
*/
int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev)
{
- struct i3c_master_controller *master = i3c_dev_get_master(dev);
int ret;
- if (!dev->ibi)
- return -EINVAL;
-
- ret = master->ops->enable_ibi(dev);
+ ret = i3c_dev_enable_ibi_controller_locked(dev);
if (!ret)
dev->ibi->enabled = true;
@@ -3585,6 +3708,35 @@ int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev)
}
EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_locked);
+/**
+ * i3c_dev_request_ibi_controller_locked() - Request controller IBI resources
+ * @dev: I3C device descriptor
+ * @req: IBI setup request
+ *
+ * Invoke the current controller's IBI request callback without allocating the
+ * generic IBI object or workqueue. The caller must ensure that @dev->ibi has
+ * already been initialized.
+ *
+ * Context: The caller must serialize access to @dev->ibi and the generic
+ * IBI lifecycle.
+ *
+ * Return: 0 on success, or a negative error code.
+ */
+int i3c_dev_request_ibi_controller_locked(struct i3c_dev_desc *dev,
+ const struct i3c_ibi_setup *req)
+{
+ struct i3c_master_controller *master = i3c_dev_get_master(dev);
+
+ if (!dev->ibi)
+ return -EINVAL;
+
+ if (!master->ops->request_ibi)
+ return -EOPNOTSUPP;
+
+ return master->ops->request_ibi(dev, req);
+}
+EXPORT_SYMBOL_GPL(i3c_dev_request_ibi_controller_locked);
+
/**
* i3c_dev_request_ibi_locked() - Request an IBI
* @dev: device for which we should enable IBIs
@@ -3600,13 +3752,9 @@ EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_locked);
int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev,
const struct i3c_ibi_setup *req)
{
- struct i3c_master_controller *master = i3c_dev_get_master(dev);
struct i3c_device_ibi_info *ibi;
int ret;
- if (!master->ops->request_ibi)
- return -EOPNOTSUPP;
-
if (dev->ibi)
return -EBUSY;
@@ -3627,8 +3775,15 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev,
ibi->num_slots = req->num_slots;
dev->ibi = ibi;
- ret = master->ops->request_ibi(dev, req);
+ ret = i3c_dev_request_ibi_controller_locked(dev, req);
if (ret) {
+ /*
+ * The controller request callback failed, so tear down the
+ * workqueue allocated above before freeing the IBI object.
+ * This is the owner of the workqueue, so it must destroy it
+ * here to avoid leaking it on the error path.
+ */
+ destroy_workqueue(ibi->wq);
kfree(ibi);
dev->ibi = NULL;
}
@@ -3637,6 +3792,27 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev,
}
EXPORT_SYMBOL_GPL(i3c_dev_request_ibi_locked);
+/**
+ * i3c_dev_free_ibi_controller_locked() - Free controller IBI resources
+ * @dev: I3C device descriptor
+ *
+ * Invoke the current controller's IBI free callback without destroying the
+ * generic IBI workqueue or freeing @dev->ibi.
+ *
+ * Context: The caller must serialize access to @dev->ibi and the generic
+ * IBI lifecycle.
+ */
+void i3c_dev_free_ibi_controller_locked(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *master = i3c_dev_get_master(dev);
+
+ if (!dev->ibi)
+ return;
+
+ master->ops->free_ibi(dev);
+}
+EXPORT_SYMBOL_GPL(i3c_dev_free_ibi_controller_locked);
+
/**
* i3c_dev_free_ibi_locked() - Free all resources needed for IBI handling
* @dev: device on which you want to release IBI resources
@@ -3667,7 +3843,7 @@ void i3c_dev_free_ibi_locked(struct i3c_dev_desc *dev)
dev_err(&master->dev, "Failed to disable IBI before freeing\n");
}
- master->ops->free_ibi(dev);
+ i3c_dev_free_ibi_controller_locked(dev);
if (dev->ibi->wq) {
destroy_workqueue(dev->ibi->wq);
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v16 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator Lakshay Piplani
` (4 subsequent siblings)
7 siblings, 0 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
From: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Add bindings for the NXP P3H2x4x (P3H2440/P3H2441/P3H2840/P3H2841)
multiport I3C hub family. These devices connect to a host via
I3C/I2C/SMBus and allow communication with multiple downstream
peripherals.
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Reviewed-by: Frank Li <Frank.Li@nxp.com>
---
Changes in v16:
- Add an allOf/if-then block for the 4-port variants (nxp,p3h2440 and
nxp,p3h2441) that limits i3c@[0-3]/smbus@[0-3] and disallows target
ports [4-7], so DTs describing nonexistent ports fail validation
- Add #address-cells and #size-cells to the required list, since the hub
is a parent bus to the target-port child node
- Add an explicit minimum of 0 to the target-port reg constraints, retain
the maximum of 7, and restrict the maximum to 3 for four-port variants
Changes in v15:
- Add compatible strings for the P3H2440, P3H2441, P3H2840 and P3H2841
variants
- Fix the target-port reg schema to describe the single-cell port index
Changes in v14:
- No change
Changes in v13:
- Fix mismatch between example unit-address and reg property by correcting the reg value
to match the node name
Changes in v12:
- No change, added Reviewed-By tag
Changes in v11:
- No change
Changes in v10:
- No change, added Reviewed-By tag
Changes in v9:
- Referenced i3c.yaml and i2c-controller.yaml for child nodes
- Dropped unnecessary #address-cells and #size-cells from child nodes
Changes in v8:
- Add compatible in i3c example
Changes in v7:
- Fix schema validation issues
- Adjust required properties
- Add I2C example
Changes in v6:
- Use a vendor prefix for the attributes
Changes in v5:
- Removed SW properties: cp0-ldo-microvolt,cp1-ldo-microvolt,
tp0145-ldo-microvolt, tp2367-ldo-microvolt
- Changed supply entries and its descriptions
Changes in v4:
- Fixed DT binding check warning
- Removed SW properties: ibi-enable, local-dev, and always-enable
Changes in v3:
- Added MFD (Multi-Function Device) support for I3C hub and on-die regulator
- Added Regulator supply node
Changes in v2:
- Fixed DT binding check warning
- Revised logic for parsing DTS nodes
---
---
.../devicetree/bindings/i3c/nxp,p3h2840.yaml | 324 ++++++++++++++++++
MAINTAINERS | 9 +
2 files changed, 333 insertions(+)
create mode 100644 Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
diff --git a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
new file mode 100644
index 000000000000..3fa0b417a6c4
--- /dev/null
+++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
@@ -0,0 +1,324 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+# Copyright 2025-2026 NXP
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/i3c/nxp,p3h2840.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: NXP P3H2X4X I3C HUB
+
+maintainers:
+ - Aman Kumar Pandey <aman.kumarpandey@nxp.com>
+ - Vikash Bansal <vikash.bansal@nxp.com>
+ - Lakshay Piplani <lakshay.piplani@nxp.com>
+
+description: |
+ P3H2x4x (P3H2440/P3H2441/P3H2840/P3H2841) is a family of multiport I3C
+ hub devices that connect to:-
+ 1. A host CPU via I3C/I2C/SMBus bus on upstream side and connect to multiple
+ peripheral devices on the downstream side.
+ 2. Have two Controller Ports which can support either
+ I2C/SMBus or I3C buses and connect to a CPU, BMC or SOC.
+ 3. P3H2840/ P3H2841 are 8 port I3C hub with eight Target Ports.
+ 4. P3H2440/ P3H2441 are 4 port I3C hub with four Target Ports.
+ Target ports can be configured as SMBus or I3C and connect to
+ peripherals.
+
+properties:
+ compatible:
+ enum:
+ - nxp,p3h2440
+ - nxp,p3h2441
+ - nxp,p3h2840
+ - nxp,p3h2841
+
+ reg:
+ maxItems: 1
+
+ '#address-cells':
+ const: 1
+
+ '#size-cells':
+ const: 0
+
+ assigned-address:
+ maximum: 0x7f
+
+ nxp,tp0145-pullup-ohms:
+ description:
+ Selects the pull up resistance for target Port 0/1/4/5, in ohms.
+ enum: [250, 500, 1000, 2000]
+ default: 500
+
+ nxp,tp2367-pullup-ohms:
+ description:
+ Selects the pull up resistance for target Port 2/3/6/7, in ohms.
+ enum: [250, 500, 1000, 2000]
+ default: 500
+
+ nxp,cp0-io-strength-ohms:
+ description:
+ Selects the IO drive strength for controller Port 0, in ohms.
+ enum: [20, 30, 40, 50]
+ default: 20
+
+ nxp,cp1-io-strength-ohms:
+ description:
+ Selects the IO drive strength for controller Port 1, in ohms.
+ enum: [20, 30, 40, 50]
+ default: 20
+
+ nxp,tp0145-io-strength-ohms:
+ description:
+ Selects the IO drive strength for target port 0/1/4/5, in ohms.
+ enum: [20, 30, 40, 50]
+ default: 20
+
+ nxp,tp2367-io-strength-ohms:
+ description:
+ Selects the IO drive strength for target port 2/3/6/7, in ohms.
+ enum: [20, 30, 40, 50]
+ default: 20
+
+ vcc1-supply:
+ description: Controller port 0 power supply.
+
+ vcc2-supply:
+ description: Controller port 1 power supply.
+
+ vcc3-supply:
+ description: Target port 0/1/4/5 power supply.
+
+ vcc4-supply:
+ description: Target port 2/3/6/7 power supply.
+
+ regulators:
+ type: object
+ additionalProperties: false
+
+ properties:
+ ldo-cp0:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+ ldo-cp1:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+ ldo-tpg0:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+ ldo-tpg1:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+required:
+ - reg
+ - '#address-cells'
+ - '#size-cells'
+
+patternProperties:
+ "^i3c@[0-7]$":
+ type: object
+ $ref: /schemas/i3c/i3c.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ description:
+ The I3C HUB Target Port number.
+ items:
+ - minimum: 0
+ maximum: 7
+
+ nxp,pullup-enable:
+ type: boolean
+ description:
+ Enables the on-die pull-up for Target Port.
+
+ required:
+ - reg
+
+ "^smbus@[0-7]$":
+ type: object
+ $ref: /schemas/i2c/i2c-controller.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ description:
+ The I3C HUB Target Port number.
+ items:
+ - minimum: 0
+ maximum: 7
+
+ nxp,pullup-enable:
+ type: boolean
+ description:
+ Enables the on-die pull-up for Target Port.
+
+ required:
+ - reg
+
+allOf:
+ - if:
+ properties:
+ compatible:
+ contains:
+ enum:
+ - nxp,p3h2440
+ - nxp,p3h2441
+ then:
+ patternProperties:
+ "^i3c@[0-3]$":
+ properties:
+ reg:
+ items:
+ - maximum: 3
+ "^smbus@[0-3]$":
+ properties:
+ reg:
+ items:
+ - maximum: 3
+ "^i3c@[4-7]$": false
+ "^smbus@[4-7]$": false
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ i3c {
+ #address-cells = <3>;
+ #size-cells = <0>;
+
+ hub@70,236153000c2 {
+ reg = <0x70 0x236 0x153000c2>;
+ compatible = "nxp,p3h2840";
+ #address-cells = <1>;
+ #size-cells = <0>;
+ assigned-address = <0x50>;
+
+ nxp,tp0145-pullup-ohms = <1000>;
+ nxp,tp2367-pullup-ohms = <1000>;
+ nxp,cp0-io-strength-ohms = <50>;
+ nxp,cp1-io-strength-ohms = <50>;
+ nxp,tp0145-io-strength-ohms = <50>;
+ nxp,tp2367-io-strength-ohms = <50>;
+ vcc3-supply = <®_tpg0>;
+ vcc4-supply = <®_tpg1>;
+
+ regulators {
+ reg_cp0: ldo-cp0 {
+ regulator-name = "ldo-cp0";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+
+ reg_cp1: ldo-cp1 {
+ regulator-name = "ldo-cp1";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+
+ reg_tpg0: ldo-tpg0 {
+ regulator-name = "ldo-tpg0";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+
+ reg_tpg1: ldo-tpg1 {
+ regulator-name = "ldo-tpg1";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+ };
+
+ smbus@0 {
+ reg = <0x0>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ nxp,pullup-enable;
+ };
+
+ smbus@1 {
+ reg = <0x1>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ nxp,pullup-enable;
+ };
+
+ i3c@2 {
+ reg = <0x2>;
+ #address-cells = <3>;
+ #size-cells = <0>;
+ nxp,pullup-enable;
+ };
+ };
+ };
+
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ hub@70 {
+ reg = <0x70>;
+ compatible = "nxp,p3h2840";
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ nxp,tp0145-pullup-ohms = <1000>;
+ nxp,tp2367-pullup-ohms = <1000>;
+ nxp,cp0-io-strength-ohms = <50>;
+ nxp,cp1-io-strength-ohms = <50>;
+ nxp,tp0145-io-strength-ohms = <50>;
+ nxp,tp2367-io-strength-ohms = <50>;
+ vcc3-supply = <®_tpg0_i2c>;
+ vcc4-supply = <®_tpg1_i2c>;
+
+ regulators {
+ reg_cp0_i2c: ldo-cp0 {
+ regulator-name = "ldo-cp0";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+
+ reg_cp1_i2c: ldo-cp1 {
+ regulator-name = "ldo-cp1";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+
+ reg_tpg0_i2c: ldo-tpg0 {
+ regulator-name = "ldo-tpg0";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+
+ reg_tpg1_i2c: ldo-tpg1 {
+ regulator-name = "ldo-tpg1";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+ };
+
+ smbus@0 {
+ reg = <0x0>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ nxp,pullup-enable;
+ };
+
+ smbus@1 {
+ reg = <0x1>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ nxp,pullup-enable;
+ };
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 8014b9f8253e..955b6d0c843d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19544,6 +19544,15 @@ S: Maintained
F: Documentation/devicetree/bindings/ptp/nxp,ptp-netc.yaml
F: drivers/ptp/ptp_netc.c
+NXP P3H2X4X I3C-HUB DRIVER
+M: Vikash Bansal <vikash.bansal@nxp.com>
+M: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
+M: Lakshay Piplani <lakshay.piplani@nxp.com>
+L: linux-kernel@vger.kernel.org
+L: linux-i3c@lists.infradead.org
+S: Maintained
+F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
+
NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER
M: Woodrow Douglass <wdouglass@carnegierobotics.com>
S: Maintained
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
` (2 preceding siblings ...)
2026-08-26 10:38 ` [PATCH v16 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
2026-08-26 10:56 ` sashiko-bot
2026-08-27 10:03 ` Krzysztof Kozlowski
2026-08-26 10:38 ` [PATCH v16 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub Lakshay Piplani
` (3 subsequent siblings)
7 siblings, 2 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
From: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Add core MFD support for the NXP P3H2x4x (P3H2440/P3H2441/P3H2840/P3H2841)
family of multiport I3C hub devices. These devices connect to a host via
I3C/I2C/SMBus and expose multiple downstream target ports.
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Reviewed-by: Frank Li <Frank.Li@nxp.com>
---
Changes in v16:
- Use a named initializer for struct i2c_device_id
({ .name = "nxp-i3c-hub" })
- Keep the shared header in include/linux/mfd/ (struct p3h2x4x is shared by
the mfd parent and its regulator/i3c-hub children). The i3c_hub_priv
back-pointer is retained for IBI routing, with documented publish/clear
ordering
Changes in v15:
- Read the device capability register to determine whether the device has
four or eight target ports
- Store the detected target-port count in the shared MFD data
- Add OF match entries for all P3H2440, P3H2441, P3H2840 and P3H2841
variants
- Add a shared hub-context pointer for use by the IBI handler without
overwriting the MFD parent's driver data
Changes in v14:
- Clean up MFD driver comments, formatting, variable names and author
information
- Define and document the I3C hub DCR value in hexadecimal form
- Use direct regmap bit-width values instead of local macros
- Add a shared protected_reg_lock for protected register accesses across
P3H2X4X child drivers
Changes in v13:
- Use i3c_device helpers (i3cdev_to_dev()) instead of direct struct access
to maintain API abstraction
- Avoid including internal I3C headers and use public device headers instead
- Ensure proper device matching by relying on manufacturer ID checks in probe
Changes in v12:
- No change, added Reviewed-By tag
Changes in v11:
- Use MFD_CELL_NAME() for child device registration
- Rename local variables for consistency
- Rename driver names to follow subsystem conventions:
- Use '-' instead of '_' in driver names
- Drop the "_drv" suffix from driver names
Changes in v10:
- Drop redundant is_p3h2x4x_in_i3c flag
Changes in v9:
- Renamed macros to follow consistent uppercase naming conventions
- Made REGMAP selects in the P3H2X4X MFD Kconfig conditional,
to avoid I3C/I2C dependency issues
Changes in v8:
- No change
Changes in v7:
- Use new config I3C_OR_I2C
Changes in v6:
- No change
Changes in v5:
- Corrected the ordering in the Makefile and Kconfig for MFD_P3H2X4X
- Updated dev_err_probe() for regmap_init failure.
- Updated module description
Changes in v4:
- Split the driver into three separate patches(mfd, regulator and I3C hub)
- Added support for NXP P3H2x4x MFD functionality
---
---
MAINTAINERS | 2 +
drivers/mfd/Kconfig | 13 +++
drivers/mfd/Makefile | 1 +
drivers/mfd/p3h2840.c | 168 ++++++++++++++++++++++++++++++++++++
include/linux/i3c/device.h | 2 +
include/linux/mfd/p3h2840.h | 41 +++++++++
6 files changed, 227 insertions(+)
create mode 100644 drivers/mfd/p3h2840.c
create mode 100644 include/linux/mfd/p3h2840.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 955b6d0c843d..76861839f579 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19552,6 +19552,8 @@ L: linux-kernel@vger.kernel.org
L: linux-i3c@lists.infradead.org
S: Maintained
F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
+F: drivers/mfd/p3h2840.c
+F: include/linux/mfd/p3h2840.h
NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER
M: Woodrow Douglass <wdouglass@carnegierobotics.com>
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 763ce6a34782..fc56407be00a 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -617,6 +617,19 @@ config MFD_MX25_TSADC
i.MX25 processors. They consist of a conversion queue for general
purpose ADC and a queue for Touchscreens.
+config MFD_P3H2X4X
+ tristate "NXP P3H2X4X I3C Hub Device"
+ depends on I3C_OR_I2C
+ select MFD_CORE
+ select REGMAP_I3C if I3C
+ select REGMAP_I2C if I2C
+ help
+ Enable Support for NXP P3H244x/P3H284x I3C HUB device using I3C/I2C
+ communication interface.
+
+ This driver provides support for I3C hub and regulator, each subdriver
+ can be enabled independently depending on the required functionality.
+
config MFD_PF1550
tristate "NXP PF1550 PMIC Support"
depends on I2C=y && OF
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index dd4bb7e77c33..93db86851152 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -122,6 +122,7 @@ obj-$(CONFIG_MFD_MC13XXX) += mc13xxx-core.o
obj-$(CONFIG_MFD_MC13XXX_SPI) += mc13xxx-spi.o
obj-$(CONFIG_MFD_MC13XXX_I2C) += mc13xxx-i2c.o
+obj-$(CONFIG_MFD_P3H2X4X) += p3h2840.o
obj-$(CONFIG_MFD_PF1550) += pf1550.o
obj-$(CONFIG_MFD_NCT6694) += nct6694.o
diff --git a/drivers/mfd/p3h2840.c b/drivers/mfd/p3h2840.c
new file mode 100644
index 000000000000..f8cef54ce706
--- /dev/null
+++ b/drivers/mfd/p3h2840.c
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2025-2026 NXP
+ *
+ * Authors:
+ * Aman Kumar Pandey <aman.kumarpandey@nxp.com>
+ * Vikash Bansal <vikash.bansal@nxp.com>
+ * Lakshay Piplani <lakshay.piplani@nxp.com>
+ *
+ * NXP P3H2x4x multi-port I3C hub.
+ */
+#include <linux/i2c.h>
+#include <linux/i3c/device.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/p3h2840.h>
+#include <linux/regmap.h>
+
+static const struct mfd_cell p3h2x4x_devs[] = {
+ MFD_CELL_NAME("p3h2x4x-regulator"),
+ MFD_CELL_NAME("p3h2x4x-i3c-hub"),
+};
+
+static const struct regmap_config p3h2x4x_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0xFF,
+};
+
+/* Read port count from the device capability register (4- or 8-port variant). */
+static int p3h2x4x_read_num_target_ports(struct device *dev,
+ struct p3h2x4x *ddata)
+{
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(ddata->regmap, P3H2X4X_DEV_CAPAB, &val);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to read device capability\n");
+
+ ddata->num_target_ports = (val & P3H2X4X_TARGET_PORT_COUNT) ?
+ P3H2X4X_TARGET_PORTS_8 : P3H2X4X_TARGET_PORTS_4;
+
+ return 0;
+}
+
+static int p3h2x4x_device_probe_i3c(struct i3c_device *i3cdev)
+{
+ struct device *dev = i3cdev_to_dev(i3cdev);
+ struct i3c_device_info devinfo;
+ struct p3h2x4x *ddata;
+ int ret;
+
+ i3c_device_get_info(i3cdev, &devinfo);
+
+ if (I3C_PID_MANUF_ID(devinfo.pid) != I3C_MANUF_ID_NXP)
+ return -ENODEV;
+
+ ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL);
+ if (!ddata)
+ return -ENOMEM;
+
+ ret = devm_mutex_init(dev, &ddata->protected_reg_lock);
+ if (ret)
+ return ret;
+
+ i3cdev_set_drvdata(i3cdev, ddata);
+
+ ddata->regmap = devm_regmap_init_i3c(i3cdev, &p3h2x4x_regmap_config);
+ if (IS_ERR(ddata->regmap))
+ return dev_err_probe(dev, PTR_ERR(ddata->regmap),
+ "Failed to register HUB regmap\n");
+
+ /* The hub child driver retrieves information from i3cdev. */
+ ddata->i3cdev = i3cdev;
+
+ ret = p3h2x4x_read_num_target_ports(dev, ddata);
+ if (ret)
+ return ret;
+
+ ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_AUTO,
+ p3h2x4x_devs, ARRAY_SIZE(p3h2x4x_devs),
+ NULL, 0, NULL);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to add sub devices\n");
+
+ return 0;
+}
+
+static int p3h2x4x_device_probe_i2c(struct i2c_client *client)
+{
+ struct p3h2x4x *ddata;
+ int ret;
+
+ ddata = devm_kzalloc(&client->dev, sizeof(*ddata), GFP_KERNEL);
+ if (!ddata)
+ return -ENOMEM;
+
+ ret = devm_mutex_init(&client->dev, &ddata->protected_reg_lock);
+ if (ret)
+ return ret;
+
+ i2c_set_clientdata(client, ddata);
+
+ ddata->regmap = devm_regmap_init_i2c(client, &p3h2x4x_regmap_config);
+ if (IS_ERR(ddata->regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(ddata->regmap),
+ "Failed to register HUB regmap\n");
+
+ ddata->i3cdev = NULL;
+
+ ret = p3h2x4x_read_num_target_ports(&client->dev, ddata);
+ if (ret)
+ return ret;
+
+ ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_AUTO,
+ p3h2x4x_devs, ARRAY_SIZE(p3h2x4x_devs),
+ NULL, 0, NULL);
+ if (ret)
+ return dev_err_probe(&client->dev, ret, "Failed to add sub devices\n");
+
+ return 0;
+}
+
+static const struct i3c_device_id p3h2x4x_i3c_ids[] = {
+ I3C_CLASS(I3C_DCR_HUB, NULL),
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(i3c, p3h2x4x_i3c_ids);
+
+static const struct i2c_device_id p3h2x4x_i2c_id_table[] = {
+ { .name = "nxp-i3c-hub" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(i2c, p3h2x4x_i2c_id_table);
+
+static const struct of_device_id p3h2x4x_i2c_of_match[] = {
+ { .compatible = "nxp,p3h2440", },
+ { .compatible = "nxp,p3h2441", },
+ { .compatible = "nxp,p3h2840", },
+ { .compatible = "nxp,p3h2841", },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, p3h2x4x_i2c_of_match);
+
+static struct i3c_driver p3h2x4x_i3c = {
+ .driver = {
+ .name = "p3h2x4x-i3c",
+ },
+ .probe = p3h2x4x_device_probe_i3c,
+ .id_table = p3h2x4x_i3c_ids,
+};
+
+static struct i2c_driver p3h2x4x_i2c = {
+ .driver = {
+ .name = "p3h2x4x-i2c",
+ .of_match_table = p3h2x4x_i2c_of_match,
+ },
+ .probe = p3h2x4x_device_probe_i2c,
+ .id_table = p3h2x4x_i2c_id_table,
+};
+module_i3c_i2c_driver(p3h2x4x_i3c, &p3h2x4x_i2c);
+
+MODULE_AUTHOR("Aman Kumar Pandey <aman.kumarpandey@nxp.com>");
+MODULE_AUTHOR("Vikash Bansal <vikash.bansal@nxp.com>");
+MODULE_AUTHOR("Lakshay Piplani <lakshay.piplani@nxp.com>");
+MODULE_DESCRIPTION("NXP P3H2X4X I3C HUB multi function driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/i3c/device.h b/include/linux/i3c/device.h
index 0f065b883ee0..d008688c96f6 100644
--- a/include/linux/i3c/device.h
+++ b/include/linux/i3c/device.h
@@ -82,9 +82,11 @@ struct i3c_xfer {
/**
* enum i3c_dcr - I3C DCR values
* @I3C_DCR_GENERIC_DEVICE: generic I3C device
+ * @I3C_DCR_HUB: I3C hub device
*/
enum i3c_dcr {
I3C_DCR_GENERIC_DEVICE = 0,
+ I3C_DCR_HUB = 0xC2,
};
#define I3C_PID_MANUF_ID(pid) (((pid) & GENMASK_ULL(47, 33)) >> 33)
diff --git a/include/linux/mfd/p3h2840.h b/include/linux/mfd/p3h2840.h
new file mode 100644
index 000000000000..eb4792de5440
--- /dev/null
+++ b/include/linux/mfd/p3h2840.h
@@ -0,0 +1,41 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright 2025-2026 NXP
+ */
+
+#ifndef _LINUX_MFD_P3H2840_H
+#define _LINUX_MFD_P3H2840_H
+
+#include <linux/bits.h>
+#include <linux/mutex.h>
+#include <linux/types.h>
+
+/* Device Information Registers */
+#define P3H2X4X_DEV_CAPAB 0x0a
+#define P3H2X4X_TARGET_PORT_COUNT BIT(3)
+
+/* Downstream target port counts per variant. */
+#define P3H2X4X_TARGET_PORTS_4 4
+#define P3H2X4X_TARGET_PORTS_8 8
+
+/* Device Configuration Registers */
+#define P3H2X4X_DEV_REG_PROTECTION_CODE 0x10
+#define P3H2X4X_REGISTERS_LOCK_CODE 0x00
+#define P3H2X4X_REGISTERS_UNLOCK_CODE 0x69
+#define P3H2X4X_CP1_REGISTERS_UNLOCK_CODE 0x6a
+
+#define I3C_MANUF_ID_NXP 0x011b
+
+struct p3h2x4x_i3c_hub_dev;
+
+struct p3h2x4x {
+ struct i3c_device *i3cdev;
+ struct regmap *regmap;
+ /* Number of downstream target ports (4 or 8). */
+ u8 num_target_ports;
+ /* Serializes protected register unlock/lock sequences across MFD children. */
+ struct mutex protected_reg_lock;
+ /* Hub context for the IBI handler to reach hub state via the parent i3cdev. */
+ struct p3h2x4x_i3c_hub_dev *i3c_hub_priv;
+};
+#endif /* _LINUX_MFD_P3H2840_H */
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v16 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
` (3 preceding siblings ...)
2026-08-26 10:38 ` [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
2026-08-26 10:48 ` sashiko-bot
2026-08-26 10:38 ` [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub Lakshay Piplani
` (2 subsequent siblings)
7 siblings, 1 reply; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani,
Frank Li
From: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
The NXP P3H2x4x family integrates on-die regulators alongside I3C hub
functionality. This driver registers the regulators using the MFD
framework and exposes them via the regulator subsystem.
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Reviewed-by: Frank Li <frank.li@nxp.com>
Reviewed-by: Mark Brown <broonie@kernel.org>
---
Changes in v16:
- Mark the p3h2x4x_regulators[] descriptor array as const
- Keep the standard MFD dev_get_drvdata(pdev->dev.parent) parent-context
access and the shared include/linux/mfd/ header (used only by the parent
and its children)
Changes in v15:
- No change
Changes in v14:
- Use the shared MFD protected_reg_lock instead of a regulator-private mutex
to serialize protected register access across P3H2X4X child drivers
- Rework protected register handling into explicit unprotect/protect helpers
and restore the original protection state after each regulator operation
- Ensure protected registers are restored even if enable, disable or
set_voltage_sel fails
Changes in v13:
- Serialize register unlock/modify/lock sequences using a device-level mutex to
avoid races between regulators.
- Ensure proper module auto-loading by adding platform device ID table.
- Improve error handling in register guard paths to avoid operating on locked hub.
Changes in v12:
- No change
Changes in v11:
- No change, added Reviewed-By tag
Changes in v10:
- No change
Changes in v9:
- No change
Changes in v8:
- No change
Changes in v7:
- No change, added Reviewed-By tag
Changes in v6:
- Use DEFINE_LOCK_GUARD_1 for reg lock/unlock
Changes in v5:
- Updated dev_err_probe() for regmap_init failure.
- Updated module description
Changes in v4:
- Split the driver into three separate patches (mfd, regulator and I3C hub)
- Introduced driver for on-die regulators in NXP P3H2x4x I3C hub
---
---
MAINTAINERS | 1 +
drivers/regulator/Kconfig | 10 +
drivers/regulator/Makefile | 1 +
drivers/regulator/p3h2840_i3c_hub_regulator.c | 284 ++++++++++++++++++
4 files changed, 296 insertions(+)
create mode 100644 drivers/regulator/p3h2840_i3c_hub_regulator.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 76861839f579..448c5017f665 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19553,6 +19553,7 @@ L: linux-i3c@lists.infradead.org
S: Maintained
F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
F: drivers/mfd/p3h2840.c
+F: drivers/regulator/p3h2840_i3c_hub_regulator.c
F: include/linux/mfd/p3h2840.h
NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER
diff --git a/drivers/regulator/Kconfig b/drivers/regulator/Kconfig
index a54a549196fe..3ee94c5b9288 100644
--- a/drivers/regulator/Kconfig
+++ b/drivers/regulator/Kconfig
@@ -1020,6 +1020,16 @@ config REGULATOR_MTK_DVFSRC
of Mediatek. It allows for voting on regulator state
between multiple users.
+config REGULATOR_P3H2X4X
+ tristate "NXP P3H2X4X regulator support"
+ depends on MFD_P3H2X4X
+ help
+ This driver provides support for the voltage regulators of the
+ P3H244x/P3H284x multi-function I3C Hub device.
+
+ Say M here if you want to include support for this regulator as
+ a module. The module will be named "p3h2840_i3c_hub_regulator".
+
config REGULATOR_PALMAS
tristate "TI Palmas PMIC Regulators"
depends on MFD_PALMAS
diff --git a/drivers/regulator/Makefile b/drivers/regulator/Makefile
index 134eee274dbf..e3e40e3ffd11 100644
--- a/drivers/regulator/Makefile
+++ b/drivers/regulator/Makefile
@@ -128,6 +128,7 @@ obj-$(CONFIG_REGULATOR_QCOM_RPMH) += qcom-rpmh-regulator.o
obj-$(CONFIG_REGULATOR_QCOM_SMD_RPM) += qcom_smd-regulator.o
obj-$(CONFIG_REGULATOR_QCOM_SPMI) += qcom_spmi-regulator.o
obj-$(CONFIG_REGULATOR_QCOM_USB_VBUS) += qcom_usb_vbus-regulator.o
+obj-$(CONFIG_REGULATOR_P3H2X4X) += p3h2840_i3c_hub_regulator.o
obj-$(CONFIG_REGULATOR_PALMAS) += palmas-regulator.o
obj-$(CONFIG_REGULATOR_PCA9450) += pca9450-regulator.o
obj-$(CONFIG_REGULATOR_PF0900) += pf0900-regulator.o
diff --git a/drivers/regulator/p3h2840_i3c_hub_regulator.c b/drivers/regulator/p3h2840_i3c_hub_regulator.c
new file mode 100644
index 000000000000..6a476b6021c2
--- /dev/null
+++ b/drivers/regulator/p3h2840_i3c_hub_regulator.c
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2025-2026 NXP
+ * NXP P3H2X4X regulator driver file contain functions for enable/disable regulator
+ * and voltage set/get.
+ */
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/mfd/p3h2840.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/driver.h>
+
+#define P3H2X4X_LDO_AND_PULLUP_CONF 0x19
+#define P3H2X4X_LDO_ENABLE_DISABLE_MASK GENMASK(3, 0)
+#define P3H2X4X_CP0_EN_LDO BIT(0)
+#define P3H2X4X_CP1_EN_LDO BIT(1)
+#define P3H2X4X_TP0145_EN_LDO BIT(2)
+#define P3H2X4X_TP2367_EN_LDO BIT(3)
+
+#define P3H2X4X_NET_OPER_MODE_CONF 0x15
+#define P3H2X4X_VCCIO_LDO_CONF 0x16
+#define P3H2X4X_CP0_VCCIO_LDO_VOLTAGE_MASK GENMASK(1, 0)
+#define P3H2X4X_CP0_VCCIO_LDO_VOLTAGE(x) \
+ FIELD_PREP(P3H2X4X_CP0_VCCIO_LDO_VOLTAGE_MASK, x)
+#define P3H2X4X_CP1_VCCIO_LDO_VOLTAGE_MASK GENMASK(3, 2)
+#define P3H2X4X_CP1_VCCIO_LDO_VOLTAGE(x) \
+ FIELD_PREP(P3H2X4X_CP1_VCCIO_LDO_VOLTAGE_MASK, x)
+#define P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE_MASK GENMASK(5, 4)
+#define P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE(x) \
+ FIELD_PREP(P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE_MASK, x)
+#define P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE_MASK GENMASK(7, 6)
+#define P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE(x) \
+ FIELD_PREP(P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE_MASK, x)
+#define P3H2X4X_LDO_COUNT 4
+
+struct p3h2x4x_regulator_dev {
+ struct regulator_dev *rp3h2x4x_dev[P3H2X4X_LDO_COUNT];
+ struct p3h2x4x *p3h2x4x;
+ struct regmap *regmap;
+};
+
+struct p3h2x4x_reg_state {
+ unsigned int orig;
+ bool restore;
+};
+
+static void p3h2x4x_reg_guard_enter(struct regulator_dev *rdev)
+{
+ struct p3h2x4x_regulator_dev *priv = rdev_get_drvdata(rdev);
+
+ mutex_lock(&priv->p3h2x4x->protected_reg_lock);
+}
+
+static void p3h2x4x_reg_guard_exit(struct regulator_dev *rdev)
+{
+ struct p3h2x4x_regulator_dev *priv = rdev_get_drvdata(rdev);
+
+ mutex_unlock(&priv->p3h2x4x->protected_reg_lock);
+}
+
+DEFINE_LOCK_GUARD_1(p3h2x4x_reg, struct regulator_dev,
+ p3h2x4x_reg_guard_enter(_T->lock),
+ p3h2x4x_reg_guard_exit(_T->lock));
+
+static int p3h2x4x_reg_unprotect(struct regulator_dev *rdev,
+ struct p3h2x4x_reg_state *state)
+{
+ int ret;
+
+ state->restore = false;
+
+ ret = regmap_read(rdev->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ &state->orig);
+ if (ret)
+ return ret;
+
+ if (state->orig == P3H2X4X_REGISTERS_UNLOCK_CODE)
+ return 0;
+
+ ret = regmap_write(rdev->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ return ret;
+
+ state->restore = true;
+
+ return 0;
+}
+
+static int p3h2x4x_reg_protect(struct regulator_dev *rdev,
+ struct p3h2x4x_reg_state *state)
+{
+ if (!state->restore)
+ return 0;
+
+ return regmap_write(rdev->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ state->orig);
+}
+
+static int p3h2x4x_regulator_enable(struct regulator_dev *rdev)
+{
+ struct p3h2x4x_reg_state state;
+ int ret, ret2;
+
+ guard(p3h2x4x_reg)(rdev);
+
+ ret = p3h2x4x_reg_unprotect(rdev, &state);
+ if (ret)
+ return ret;
+
+ ret = regulator_enable_regmap(rdev);
+ ret2 = p3h2x4x_reg_protect(rdev, &state);
+
+ return ret ? ret : ret2;
+}
+
+static int p3h2x4x_regulator_disable(struct regulator_dev *rdev)
+{
+ struct p3h2x4x_reg_state state;
+ int ret, ret2;
+
+ guard(p3h2x4x_reg)(rdev);
+
+ ret = p3h2x4x_reg_unprotect(rdev, &state);
+ if (ret)
+ return ret;
+
+ ret = regulator_disable_regmap(rdev);
+ ret2 = p3h2x4x_reg_protect(rdev, &state);
+
+ return ret ? ret : ret2;
+}
+
+static int p3h2x4x_regulator_set_voltage_sel(struct regulator_dev *rdev,
+ unsigned int sel)
+{
+ struct p3h2x4x_reg_state state;
+ int ret, ret2;
+
+ guard(p3h2x4x_reg)(rdev);
+
+ ret = p3h2x4x_reg_unprotect(rdev, &state);
+ if (ret)
+ return ret;
+
+ ret = regulator_set_voltage_sel_regmap(rdev, sel);
+ ret2 = p3h2x4x_reg_protect(rdev, &state);
+
+ return ret ? ret : ret2;
+}
+
+static const struct regulator_ops p3h2x4x_ldo_ops = {
+ .list_voltage = regulator_list_voltage_table,
+ .map_voltage = regulator_map_voltage_iterate,
+ .set_voltage_sel = p3h2x4x_regulator_set_voltage_sel,
+ .get_voltage_sel = regulator_get_voltage_sel_regmap,
+ .enable = p3h2x4x_regulator_enable,
+ .disable = p3h2x4x_regulator_disable,
+ .is_enabled = regulator_is_enabled_regmap,
+};
+
+static const unsigned int p3h2x4x_voltage_table[] = {
+ 1000000,
+ 1100000,
+ 1200000,
+ 1800000,
+};
+
+static const struct regulator_desc p3h2x4x_regulators[] = {
+ {
+ .name = "ldo-cp0",
+ .of_match = of_match_ptr("ldo-cp0"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = p3h2x4x_voltage_table,
+ .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table),
+ .ops = &p3h2x4x_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF,
+ .enable_mask = P3H2X4X_CP0_EN_LDO,
+ .vsel_reg = P3H2X4X_VCCIO_LDO_CONF,
+ .vsel_mask = P3H2X4X_CP0_VCCIO_LDO_VOLTAGE_MASK,
+ },
+ {
+ .name = "ldo-cp1",
+ .of_match = of_match_ptr("ldo-cp1"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = p3h2x4x_voltage_table,
+ .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table),
+ .ops = &p3h2x4x_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF,
+ .enable_mask = P3H2X4X_CP1_EN_LDO,
+ .vsel_reg = P3H2X4X_VCCIO_LDO_CONF,
+ .vsel_mask = P3H2X4X_CP1_VCCIO_LDO_VOLTAGE_MASK,
+ },
+ {
+ .name = "ldo-tpg0",
+ .of_match = of_match_ptr("ldo-tpg0"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = p3h2x4x_voltage_table,
+ .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table),
+ .ops = &p3h2x4x_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF,
+ .enable_mask = P3H2X4X_TP0145_EN_LDO,
+ .vsel_reg = P3H2X4X_VCCIO_LDO_CONF,
+ .vsel_mask = P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE_MASK,
+ },
+ {
+ .name = "ldo-tpg1",
+ .of_match = of_match_ptr("ldo-tpg1"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = p3h2x4x_voltage_table,
+ .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table),
+ .ops = &p3h2x4x_ldo_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF,
+ .enable_mask = P3H2X4X_TP2367_EN_LDO,
+ .vsel_reg = P3H2X4X_VCCIO_LDO_CONF,
+ .vsel_mask = P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE_MASK,
+ },
+};
+
+static int p3h2x4x_regulator_probe(struct platform_device *pdev)
+{
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(pdev->dev.parent);
+ struct p3h2x4x_regulator_dev *p3h2x4x_regulator;
+ struct regulator_config rcfg = { };
+ struct device *dev = &pdev->dev;
+ struct regulator_dev *rdev;
+ int i;
+
+ p3h2x4x_regulator = devm_kzalloc(dev, sizeof(*p3h2x4x_regulator), GFP_KERNEL);
+ if (!p3h2x4x_regulator)
+ return -ENOMEM;
+
+ p3h2x4x_regulator->p3h2x4x = p3h2x4x;
+ p3h2x4x_regulator->regmap = p3h2x4x->regmap;
+
+ platform_set_drvdata(pdev, p3h2x4x_regulator);
+
+ device_set_of_node_from_dev(dev, dev->parent);
+
+ rcfg.dev = dev;
+ rcfg.regmap = p3h2x4x_regulator->regmap;
+ rcfg.driver_data = p3h2x4x_regulator;
+
+ for (i = 0; i < ARRAY_SIZE(p3h2x4x_regulators); i++) {
+ rdev = devm_regulator_register(&pdev->dev, &p3h2x4x_regulators[i], &rcfg);
+ if (IS_ERR(rdev))
+ return dev_err_probe(dev, PTR_ERR(rdev), "Failed to register %s\n",
+ p3h2x4x_regulators[i].name);
+ p3h2x4x_regulator->rp3h2x4x_dev[i] = rdev;
+ }
+ return 0;
+}
+
+static const struct platform_device_id p3h2x4x_regulator_id[] = {
+ { "p3h2x4x-regulator" },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, p3h2x4x_regulator_id);
+
+static struct platform_driver p3h2x4x_regulator_driver = {
+ .driver = {
+ .name = "p3h2x4x-regulator",
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ },
+ .probe = p3h2x4x_regulator_probe,
+ .id_table = p3h2x4x_regulator_id,
+};
+module_platform_driver(p3h2x4x_regulator_driver);
+
+MODULE_AUTHOR("Aman Kumar Pandey <aman.kumarpandey@nxp.com>");
+MODULE_AUTHOR("Vikash Bansal <vikash.bansal@nxp.com>");
+MODULE_AUTHOR("Lakshay Piplani <lakshay.piplani@nxp.com>");
+MODULE_DESCRIPTION("NXP P3H2X4X I3C HUB Regulator driver");
+MODULE_LICENSE("GPL");
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
` (4 preceding siblings ...)
2026-08-26 10:38 ` [PATCH v16 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
2026-08-26 11:04 ` sashiko-bot
2026-08-27 10:09 ` Krzysztof Kozlowski
2026-08-26 10:38 ` [PATCH v16 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support Lakshay Piplani
7 siblings, 2 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
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@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
---
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@vger.kernel.org
L: linux-i3c@lists.infradead.org
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@nxp.com>");
+MODULE_AUTHOR("Vikash Bansal <vikash.bansal@nxp.com>");
+MODULE_AUTHOR("Lakshay Piplani <lakshay.piplani@nxp.com>");
+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
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v16 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
` (5 preceding siblings ...)
2026-08-26 10:38 ` [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
2026-08-27 10:16 ` Krzysztof Kozlowski
2026-08-26 10:38 ` [PATCH v16 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support Lakshay Piplani
7 siblings, 1 reply; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
From: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Add I3C hub functionality for the NXP P3H2x4x family of multiport hubs.
These devices support downstream target ports that can be configured
as I3C, I2C, or SMBus.
This driver enables:
- I3C/I2C communication between host and hub
- Transparent communication with downstream devices
- Target port configuration (I3C/I2C/SMBus)
P3H2440/P3H2441 support 4 target ports.
P3H2840/P3H2841 support 8 target ports.
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
---
Changes in v16:
- Do not ignore regulator-enable failures: p3h2x4x_configure_ldo() now
returns via dev_err_probe() for any error other than -ENODEV (optional
supply), instead of only warning
- Widen the SMBus poll deadline: floor the timeout at the datasheet
SDA-stuck/SCL-low recovery window (P3H2X4X_SMBUS_SCL_LOW_RECOVERY_US,
50 ms) via max(xfer_us, ...), so an in-spec SDA-stuck/SCL-low bus recovery
no longer causes spurious timeouts. The transfer-time estimate now only
drives the poll interval, not the give-up deadline
- Use i2c_get_dma_safe_msg_buf() and i2c_put_dma_safe_msg_buf() for
controller-agent read transfers before passing the buffer to
regmap_bulk_read() over I3C
- Document the i3c_hub_priv publish/clear ordering: publish before IBI is
enabled and clear it via devm_add_action_or_reset() so, on unwind,
the pointer is cleared only after IBI is disabled and freed
- Convert the SMBus register-protection sequence to scoped_guard() to avoid
mixing goto- and scope-based cleanup in one function
- Keep the virtual controllers in the devm-managed hub struct; teardown is
ordered so i3c_master_unregister() runs (via devm action) before the
memory is freed
Changes in v15:
- Use the target-port count detected by the MFD parent and only configure
registers and ports implemented by the selected device variant
- Validate target-port indices against the detected number of ports
- Preserve the MFD parent's driver data and publish the hub context through
the shared MFD structure, with managed cleanup
- Correct the SMBus transfer timeout calculation for 400 kHz operation
- Use I2C adapter quirks to enforce the maximum read and write payload
lengths
Changes in v14:
- Replace temporary parent dev->of_node reassignment with
i3c_master_register_fwnode()
- Use the shared MFD protected_reg_lock for protected hub configuration
writes
- Fix SMBus polling interval calculation to avoid oversleeping the computed
transaction timeout
- Change SMBus transfer loop counters from u8 to int
- Clean up already registered SMBus adapters on adapter allocation or
registration failure
Changes in v13:
- Fix SMBus transaction handling by replacing fixed delay with polling (read_poll_timeout)
to avoid premature reads and data corruption
- Fix DT and of_node handling: prevent duplicate target-port node leaks and avoid corrupting
parent dev->of_node by restoring it after registration
- Add proper cleanup using devm actions (relock registers, release DT nodes, unregister adapters)
and fix minor comment mismatch
Changes in v12:
- Fix target-port configuration register updates
- Correct default pull-up and drive-strength values
- Improve OF node and SMBus adapter cleanup
- Remove dead code and simplify cleanup by relying on devm-managed resources
Changes in v11:
- Fix IBI resource cleanup on error paths
- Fix adapter unregister cleanup handling
Changes in v10:
- Split SMBus target/slave mode support, including IBI and MCTP receive
handling, into a separate patch
Changes in v9:
- Added CONFIG_I2C_SLAVE guards where necessary to avoid build issues
when I2C slave support is disabled.
Changes in v8:
- No change
Changes in v7:
- Remove CONFIG_I2C_SLAVE guards
- Use Kernel API find_closest instead of custom helper
- Use devm_regulator_get_enable_optional()
- Fix kernel-doc warnings
Changes in v6:
- Remove generic I3C code and keep reg dependent code only.
Changes in v5:
- Updated supply names.
Changes in v4:
- Split the driver into three separate patches (mfd, regulator and I3C hub)
- Added support for NXP P3H2x4x I3C hub functionality
- Integrated hub driver with its on-die regulator
Changes in v3:
- Added MFD (Multi-Function Device) support for I3C hub and on-die regulator
Changes in v2:
- Refined coding style and incorporated review feedback
- Updated directory structure
- Revised logic for parsing DTS nodes
---
---
MAINTAINERS | 1 +
drivers/i3c/Kconfig | 1 +
drivers/i3c/Makefile | 1 +
drivers/i3c/hub/Kconfig | 11 +
drivers/i3c/hub/Makefile | 4 +
drivers/i3c/hub/p3h2840_i3c_hub.h | 343 +++++++++++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 416 +++++++++++++++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 119 +++++++
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 350 +++++++++++++++++++
9 files changed, 1246 insertions(+)
create mode 100644 drivers/i3c/hub/Kconfig
create mode 100644 drivers/i3c/hub/Makefile
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub.h
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_common.c
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_i3c.c
create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 11f231ce05cf..8e0e38e5b28d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19553,6 +19553,7 @@ L: linux-i3c@lists.infradead.org
S: Maintained
F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
F: drivers/i3c/hub.c
+F: drivers/i3c/hub/*
F: drivers/mfd/p3h2840.c
F: drivers/regulator/p3h2840_i3c_hub_regulator.c
F: include/linux/i3c/hub.h
diff --git a/drivers/i3c/Kconfig b/drivers/i3c/Kconfig
index 7536f3740c94..81d5ebd8025c 100644
--- a/drivers/i3c/Kconfig
+++ b/drivers/i3c/Kconfig
@@ -35,6 +35,7 @@ config I3C_HUB
Say Y here if your platform includes an I3C hub device
+source "drivers/i3c/hub/Kconfig"
endif # I3C
config I3C_OR_I2C
diff --git a/drivers/i3c/Makefile b/drivers/i3c/Makefile
index 9ddee56a6338..2950820db9ea 100644
--- a/drivers/i3c/Makefile
+++ b/drivers/i3c/Makefile
@@ -3,3 +3,4 @@ i3c-y := device.o master.o
obj-$(CONFIG_I3C) += i3c.o
obj-$(CONFIG_I3C) += master/
obj-$(CONFIG_I3C_HUB) += hub.o
+obj-$(CONFIG_I3C_HUB) += hub/
diff --git a/drivers/i3c/hub/Kconfig b/drivers/i3c/hub/Kconfig
new file mode 100644
index 000000000000..969bb1f637dd
--- /dev/null
+++ b/drivers/i3c/hub/Kconfig
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2025 NXP
+config P3H2X4X_I3C_HUB
+ tristate "NXP P3H2X4X I3C HUB support"
+ depends on MFD_P3H2X4X
+ select I3C_HUB
+ help
+ This enables support for NXP P3H244x/P3H284x I3C HUB. These hubs
+ connect to a host via I3C/I2C/SMBus and allow communication with
+ multiple downstream peripherals. Say Y or M here to enable the
+ P3H2x4x I3C HUB driver.
diff --git a/drivers/i3c/hub/Makefile b/drivers/i3c/hub/Makefile
new file mode 100644
index 000000000000..9dbd8a7b4184
--- /dev/null
+++ b/drivers/i3c/hub/Makefile
@@ -0,0 +1,4 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2025 NXP
+p3h2840_i3c_hub-y := p3h2840_i3c_hub_common.o p3h2840_i3c_hub_i3c.o p3h2840_i3c_hub_smbus.o
+obj-$(CONFIG_P3H2X4X_I3C_HUB) += p3h2840_i3c_hub.o
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
new file mode 100644
index 000000000000..7a1345924e3f
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -0,0 +1,343 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright 2025-2026 NXP
+ * Private definitions for the NXP P3H2X4X I3C hub driver.
+ */
+
+#ifndef P3H2840_I3C_HUB_H
+#define P3H2840_I3C_HUB_H
+
+#include <linux/bitfield.h>
+#include <linux/i2c.h>
+#include <linux/i3c/device.h>
+#include <linux/i3c/hub.h>
+#include <linux/i3c/master.h>
+#include <linux/regulator/consumer.h>
+#include <linux/regmap.h>
+
+/* I3C HUB REGISTERS */
+
+/* Device Information Registers */
+#define P3H2X4X_DEV_INFO_0 0x00
+#define P3H2X4X_DEV_INFO_1 0x01
+#define P3H2X4X_PID_5 0x02
+#define P3H2X4X_PID_4 0x03
+#define P3H2X4X_PID_3 0x04
+#define P3H2X4X_PID_2 0x05
+#define P3H2X4X_PID_1 0x06
+#define P3H2X4X_PID_0 0x07
+#define P3H2X4X_BCR 0x08
+#define P3H2X4X_DCR 0x09
+#define P3H2X4X_DEV_CAPAB 0x0a
+#define P3H2X4X_DEV_REV 0x0b
+
+/* Device Configuration Registers */
+#define P3H2X4X_CP_CONF 0x11
+#define P3H2X4X_TP_ENABLE 0x12
+
+#define P3H2X4X_DEV_CONF 0x13
+#define P3H2X4X_IO_STRENGTH 0x14
+#define P3H2X4X_TP0145_IO_STRENGTH_MASK GENMASK(1, 0)
+#define P3H2X4X_TP0145_IO_STRENGTH(x) \
+ FIELD_PREP(P3H2X4X_TP0145_IO_STRENGTH_MASK, x)
+#define P3H2X4X_TP2367_IO_STRENGTH_MASK GENMASK(3, 2)
+#define P3H2X4X_TP2367_IO_STRENGTH(x) \
+ FIELD_PREP(P3H2X4X_TP2367_IO_STRENGTH_MASK, x)
+#define P3H2X4X_CP0_IO_STRENGTH_MASK GENMASK(5, 4)
+#define P3H2X4X_CP0_IO_STRENGTH(x) \
+ FIELD_PREP(P3H2X4X_CP0_IO_STRENGTH_MASK, x)
+#define P3H2X4X_CP1_IO_STRENGTH_MASK GENMASK(7, 6)
+#define P3H2X4X_CP1_IO_STRENGTH(x) \
+ FIELD_PREP(P3H2X4X_CP1_IO_STRENGTH_MASK, x)
+#define P3H2X4X_IO_STRENGTH_MASK GENMASK(7, 0)
+
+#define P3H2X4X_TP_IO_MODE_CONF 0x17
+#define P3H2X4X_TP_SMBUS_AGNT_EN 0x18
+
+#define P3H2X4X_LDO_AND_PULLUP_CONF 0x19
+
+#define P3H2X4X_TP0145_PULLUP_CONF_MASK GENMASK(7, 6)
+#define P3H2X4X_TP0145_PULLUP_CONF(x) \
+ FIELD_PREP(P3H2X4X_TP0145_PULLUP_CONF_MASK, x)
+#define P3H2X4X_TP2367_PULLUP_CONF_MASK GENMASK(5, 4)
+#define P3H2X4X_TP2367_PULLUP_CONF(x) \
+ FIELD_PREP(P3H2X4X_TP2367_PULLUP_CONF_MASK, x)
+#define P3H2X4X_PULLUP_CONF_MASK GENMASK(7, 4)
+
+#define P3H2X4X_CP_IBI_CONF 0x1a
+
+#define P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG 0x1b
+
+#define P3H2X4X_IBI_MDB_CUSTOM 0x1c
+#define P3H2X4X_JEDEC_CONTEXT_ID 0x1d
+#define P3H2X4X_TP_GPIO_MODE_EN 0x1e
+
+/* Device Status and IBI Registers */
+#define P3H2X4X_DEV_AND_IBI_STS 0x20
+#define P3H2X4X_TP_SMBUS_AGNT_IBI_STS 0x21
+#define P3H2X4X_SMBUS_AGENT_EVENT_FLAG_STATUS BIT(4)
+
+/* Controller Port Control/Status Registers */
+#define P3H2X4X_CP_MUX_SET 0x38
+#define P3H2X4X_CONTROLLER_PORT_MUX_REQ BIT(0)
+#define P3H2X4X_CP_MUX_STS 0x39
+#define P3H2X4X_CONTROLLER_PORT_MUX_CONNECTION_STATUS BIT(0)
+
+/* Target Ports Control Registers */
+#define P3H2X4X_TP_SMBUS_AGNT_TRANS_START 0x50
+#define P3H2X4X_TP_NET_CON_CONF 0x51
+
+#define P3H2X4X_TP_PULLUP_EN 0x53
+
+#define P3H2X4X_TP_SCL_OUT_EN 0x54
+#define P3H2X4X_TP_SDA_OUT_EN 0x55
+#define P3H2X4X_TP_SCL_OUT_LEVEL 0x56
+#define P3H2X4X_TP_SDA_OUT_LEVEL 0x57
+#define P3H2X4X_TP_IN_DETECT_MODE_CONF 0x58
+#define P3H2X4X_TP_SCL_IN_DETECT_IBI_EN 0x59
+#define P3H2X4X_TP_SDA_IN_DETECT_IBI_EN 0x5a
+
+/* Target Ports Status Registers */
+#define P3H2X4X_TP_SCL_IN_LEVEL_STS 0x60
+#define P3H2X4X_TP_SDA_IN_LEVEL_STS 0x61
+#define P3H2X4X_TP_SCL_IN_DETECT_FLG 0x62
+#define P3H2X4X_TP_SDA_IN_DETECT_FLG 0x63
+
+/* SMBus Agent Configuration and Status Registers */
+#define P3H2X4X_TP0_SMBUS_AGNT_STS 0x64
+#define P3H2X4X_TP1_SMBUS_AGNT_STS 0x65
+#define P3H2X4X_TP2_SMBUS_AGNT_STS 0x66
+#define P3H2X4X_TP3_SMBUS_AGNT_STS 0x67
+#define P3H2X4X_TP4_SMBUS_AGNT_STS 0x68
+#define P3H2X4X_TP5_SMBUS_AGNT_STS 0x69
+#define P3H2X4X_TP6_SMBUS_AGNT_STS 0x6a
+#define P3H2X4X_TP7_SMBUS_AGNT_STS 0x6b
+#define P3H2X4X_ONCHIP_TD_AND_SMBUS_AGNT_CONF 0x6c
+
+/* buf receive flag set */
+#define P3H2X4X_TARGET_BUF_CA_TF BIT(0)
+#define P3H2X4X_TARGET_BUF_0_RECEIVE BIT(1)
+#define P3H2X4X_TARGET_BUF_1_RECEIVE BIT(2)
+#define P3H2X4X_TARGET_BUF_0_1_RECEIVE GENMASK(2, 1)
+#define P3H2X4X_TARGET_BUF_OVRFL GENMASK(3, 1)
+#define BUF_RECEIVED_FLAG_MASK GENMASK(3, 1)
+#define BUF_RECEIVED_FLAG_TF_MASK GENMASK(3, 0)
+
+#define P3H2X4X_TARGET_AGENT_LOCAL_DEV 0x11
+#define P3H2X4X_TARGET_BUFF_0_PAGE 0x12
+#define P3H2X4X_TARGET_BUFF_1_PAGE 0x13
+
+/* Special Function Registers */
+#define P3H2X4X_LDO_AND_CPSEL_STS 0x79
+#define P3H2X4X_CP_SDA1_LEVEL BIT(7)
+#define P3H2X4X_CP_SCL1_LEVEL BIT(6)
+
+#define P3H2X4X_CP_SEL_PIN_INPUT_CODE_MASK GENMASK(5, 4)
+#define P3H2X4X_CP_SEL_PIN_INPUT_CODE_GET(x) \
+ (((x) & P3H2X4X_CP_SEL_PIN_INPUT_CODE_MASK) >> 4)
+#define P3H2X4X_CP_SDA1_SCL1_PINS_CODE_MASK GENMASK(7, 6)
+#define P3H2X4X_CP_SDA1_SCL1_PINS_CODE_GET(x) \
+ (((x) & P3H2X4X_CP_SDA1_SCL1_PINS_CODE_MASK) >> 6)
+#define P3H2X4X_VCCIO1_PWR_GOOD BIT(3)
+#define P3H2X4X_VCCIO0_PWR_GOOD BIT(2)
+#define P3H2X4X_CP1_VCCIO_PWR_GOOD BIT(1)
+#define P3H2X4X_CP0_VCCIO_PWR_GOOD BIT(0)
+
+#define P3H2X4X_BUS_RESET_SCL_TIMEOUT 0x7a
+#define P3H2X4X_ONCHIP_TD_PROTO_ERR_FLG 0x7b
+#define P3H2X4X_DEV_CMD 0x7c
+#define P3H2X4X_ONCHIP_TD_STS 0x7d
+#define P3H2X4X_ONCHIP_TD_ADDR_CONF 0x7e
+#define P3H2X4X_PAGE_PTR 0x7f
+
+/* Paged Transaction Registers */
+#define P3H2X4X_CONTROLLER_BUFFER_PAGE 0x10
+#define P3H2X4X_CONTROLLER_AGENT_BUFF 0x80
+#define P3H2X4X_CONTROLLER_AGENT_BUFF_DATA 0x84
+
+#define P3H2X4X_TARGET_BUFF_LENGTH 0x80
+#define P3H2X4X_TARGET_BUFF_ADDRESS 0x81
+#define P3H2X4X_TARGET_BUFF_DATA 0x82
+
+#define P3H2X4X_TP_MAX_COUNT 0x08
+#define P3H2X4X_CP_MAX_COUNT 0x02
+#define P3H2X4X_TP_LOCAL_DEV 0x08
+
+/* LDO Disable/Enable DT settings */
+#define P3H2X4X_LDO_VOLT_1_0V 0x00
+#define P3H2X4X_LDO_VOLT_1_1V 0x01
+#define P3H2X4X_LDO_VOLT_1_2V 0x02
+#define P3H2X4X_LDO_VOLT_1_8V 0x03
+
+#define P3H2X4X_LDO_DISABLED 0x00
+#define P3H2X4X_LDO_ENABLED 0x01
+
+#define P3H2X4X_IBI_DISABLED 0x00
+#define P3H2X4X_IBI_ENABLED 0x01
+
+#define P3H2X4X_TP_PULLUP_DISABLED 0x00
+#define P3H2X4X_TP_PULLUP_ENABLED 0x01
+
+#define ONE_BYTE_SIZE 0x01
+
+/* holding SDA low when both SMBus Target Agent received data buffers are full.
+ * This feature can be used as a flow-control mechanism for MCTP applications to
+ * avoid MCTP transmitters on Target Ports time out when the SMBus agent buffers
+ * are not serviced in time by upstream controller and only receives write message
+ * from its downstream ports.
+ * SMBUS_AGENT_TX_RX_LOOPBACK_EN/TARGET_AGENT_BUF_FULL_SDA_LOW_EN
+ */
+
+#define P3H2X4X_TARGET_AGENT_DFT_IBI_CONF 0x20
+#define P3H2X4X_TARGET_AGENT_DFT_IBI_CONF_MASK 0x21
+
+/* Transaction status checking mask */
+#define P3H2X4X_SMBUS_TRANSACTION_FINISH_FLAG 1
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SHIFT 4
+
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_OK 0
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ADDR_NAK 1
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_DATA_NAK 2
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_WTR_NAK 3
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SYNC_RCV 4
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SYNC_RCVCLR 5
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_FAULT 6
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ARB_LOSS 7
+#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SCL_TO 8
+
+#define P3H2X4X_TP_BUFFER_STATUS_MASK 0x0f
+#define P3H2X4X_TP_TRANSACTION_CODE_MASK 0xf0
+
+/* SMBus transaction types fields */
+#define P3H2X4X_SMBUS_400kHz BIT(2)
+
+/* SMBus polling */
+#define P3H2X4X_SMBUS_POLL_COUNT 10
+#define P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US 20
+#define P3H2X4X_SMBUS_POLL_INTERVAL_MAX_US 150
+
+/* Hub buffer size */
+#define P3H2X4X_CONTROLLER_BUFFER_SIZE 88
+#define P3H2X4X_TARGET_BUFFER_SIZE 80
+#define P3H2X4X_SMBUS_DESCRIPTOR_SIZE 4
+#define P3H2X4X_SMBUS_PAYLOAD_SIZE \
+ (P3H2X4X_CONTROLLER_BUFFER_SIZE - P3H2X4X_SMBUS_DESCRIPTOR_SIZE)
+#define P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE (P3H2X4X_TARGET_BUFFER_SIZE - 2)
+
+/*
+ * At 400 kHz, one 9-bit I2C byte takes 22.5 us. Round this
+ * up to 23 us per payload byte and add 100 us for the address
+ * byte, bus overhead and controller processing time. This models
+ * the time a healthy transaction takes and is used to derive the
+ * poll interval, not the overall give-up deadline.
+ */
+#define P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(x) ((23 * (x)) + 100)
+
+/*
+ * On an SDA-stuck condition the SMBus Controller Agent can legitimately
+ * hold SCL low for up to 35 ms during bus recovery (datasheet 8.11.2,
+ * Table 21). Floor the overall poll deadline above that so the recovery,
+ * status posting and the status read-back complete before we give up and
+ * report a spurious timeout.
+ */
+#define P3H2X4X_SMBUS_SCL_LOW_RECOVERY_US 50000
+
+#define P3H2X4X_NO_PAGE_PER_TP 4
+
+#define P3H2X4X_MAX_PAYLOAD_LEN 2
+#define P3H2X4X_NUM_SLOTS 6
+
+#define P3H2X4X_HUB_ID 0
+
+#define P3H2X4X_SET_BIT(n) BIT(n)
+
+#define P3H2X4X_TP_MASK GENMASK(P3H2X4X_TP_MAX_COUNT - 1, 0)
+
+#define P3H2X4X_DFT_TP_PULLUP_OHMS 500
+#define P3H2X4X_DFT_IO_STRENGTH_OHMS 20
+
+enum p3h2x4x_tp {
+ TP_0,
+ TP_1,
+ TP_2,
+ TP_3,
+ TP_4,
+ TP_5,
+ TP_6,
+ TP_7,
+};
+
+enum p3h2x4x_rcv_buf {
+ RCV_BUF_0,
+ RCV_BUF_1,
+ RCV_BUF_OF,
+};
+
+enum p3h2x4x_tp_mode {
+ P3H2X4X_TP_MODE_I3C,
+ P3H2X4X_TP_MODE_SMBUS,
+};
+
+struct tp_configuration {
+ bool pullup_en;
+ bool ibi_en;
+ bool always_enable;
+ enum p3h2x4x_tp_mode mode;
+};
+
+struct hub_configuration {
+ int tp0145_pullup;
+ int tp2367_pullup;
+ int cp0_io_strength;
+ int cp1_io_strength;
+ int tp0145_io_strength;
+ int tp2367_io_strength;
+ struct tp_configuration tp_config[P3H2X4X_TP_MAX_COUNT];
+};
+
+struct tp_bus {
+ bool is_registered; /* bus was registered in the framework. */
+ u8 tp_mask;
+ u8 tp_port;
+ struct mutex port_mutex; /* per port mutex */
+ struct device_node *of_node;
+ struct i2c_client *tp_smbus_client;
+ struct i2c_adapter *tp_smbus_adapter;
+ struct i3c_hub_controller hub_controller;
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub;
+};
+
+struct p3h2x4x_i3c_hub_dev {
+ struct device *dev;
+ struct regmap *regmap;
+ struct mutex etx_mutex; /* all port mutex */
+ struct i3c_device *i3cdev;
+ struct i2c_client *i2c_client;
+ struct hub_configuration hub_config;
+ struct tp_bus tp_bus[P3H2X4X_TP_MAX_COUNT];
+ struct i3c_hub *hub;
+};
+
+/**
+ * p3h2x4x_unregister_smbus_adapters() - unregister SMBus adapters
+ * @hub: P3H2x4x hub device
+ */
+void p3h2x4x_unregister_smbus_adapters(struct p3h2x4x_i3c_hub_dev *hub);
+
+/**
+ * p3h2x4x_tp_smbus_algo - add i2c adapter for target port configured as SMBus.
+ * @p3h2x4x_i3c_hub: P3H2x4x hub device.
+ *
+ * Return: 0 in case of success, negative error code on failure.
+ */
+int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub);
+
+/**
+ * p3h2x4x_tp_i3c_algo - register i3c controller for target port configured as I3C.
+ * @p3h2x4x_i3c_hub: P3H2x4x hub device.
+ *
+ * Return: 0 in case of success, negative error code on failure.
+ */
+int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub);
+
+#endif /* P3H2840_I3C_HUB_H */
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_common.c b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
new file mode 100644
index 000000000000..27beaaaa16a1
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
@@ -0,0 +1,416 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2025-2026 NXP
+ * This P3H2X4X driver file implements functions for Hub probe and DT parsing.
+ */
+
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/platform_device.h>
+#include <linux/mfd/p3h2840.h>
+#include <linux/util_macros.h>
+
+#include "p3h2840_i3c_hub.h"
+
+/* LDO voltage DT settings */
+#define P3H2X4X_DT_LDO_VOLT_1_0V 1000000
+#define P3H2X4X_DT_LDO_VOLT_1_1V 1100000
+#define P3H2X4X_DT_LDO_VOLT_1_2V 1200000
+#define P3H2X4X_DT_LDO_VOLT_1_8V 1800000
+
+static const int p3h2x4x_pullup_tbl[] = {
+ 250, 500, 1000, 2000
+};
+
+static const int p3h2x4x_io_strength_tbl[] = {
+ 20, 30, 40, 50
+};
+
+static u8 p3h2x4x_pullup_dt_to_reg(int dt_value)
+{
+ return find_closest(dt_value, p3h2x4x_pullup_tbl,
+ ARRAY_SIZE(p3h2x4x_pullup_tbl));
+}
+
+static u8 p3h2x4x_io_strength_dt_to_reg(int dt_value)
+{
+ return find_closest(dt_value, p3h2x4x_io_strength_tbl,
+ ARRAY_SIZE(p3h2x4x_io_strength_tbl));
+}
+
+static int p3h2x4x_configure_pullup(struct device *dev)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
+ u8 pullup;
+
+ pullup = P3H2X4X_TP0145_PULLUP_CONF(p3h2x4x_pullup_dt_to_reg
+ (p3h2x4x_i3c_hub->hub_config.tp0145_pullup));
+
+ pullup |= P3H2X4X_TP2367_PULLUP_CONF(p3h2x4x_pullup_dt_to_reg
+ (p3h2x4x_i3c_hub->hub_config.tp2367_pullup));
+
+ return regmap_update_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_LDO_AND_PULLUP_CONF,
+ P3H2X4X_PULLUP_CONF_MASK, pullup);
+}
+
+static int p3h2x4x_configure_io_strength(struct device *dev)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
+ u8 io_strength;
+
+ io_strength = P3H2X4X_CP0_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg
+ (p3h2x4x_i3c_hub->hub_config.cp0_io_strength));
+
+ io_strength |= P3H2X4X_CP1_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg
+ (p3h2x4x_i3c_hub->hub_config.cp1_io_strength));
+
+ io_strength |= P3H2X4X_TP0145_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg
+ (p3h2x4x_i3c_hub->hub_config.tp0145_io_strength));
+
+ io_strength |= P3H2X4X_TP2367_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg
+ (p3h2x4x_i3c_hub->hub_config.tp2367_io_strength));
+
+ return regmap_update_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_IO_STRENGTH,
+ P3H2X4X_IO_STRENGTH_MASK, io_strength);
+}
+
+static int p3h2x4x_configure_ldo(struct device *dev)
+{
+ static const char * const supplies[] = {
+ "vcc1",
+ "vcc2",
+ "vcc3",
+ "vcc4"
+ };
+ int ret, i;
+
+ for (i = 0; i < ARRAY_SIZE(supplies); i++) {
+ ret = devm_regulator_get_enable_optional(dev, supplies[i]);
+ if (ret && ret != -ENODEV)
+ return dev_err_probe(dev, ret, "Failed to enable %s\n",
+ supplies[i]);
+ }
+
+ /* This delay is required for the regulator to stabilize its output voltage */
+ fsleep(5000);
+
+ return 0;
+}
+
+static int p3h2x4x_configure_tp(struct device *dev)
+{
+ struct p3h2x4x_i3c_hub_dev *hub = dev_get_drvdata(dev);
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(dev->parent);
+ u8 mode = 0, smbus = 0, pullup = 0, target_port = 0;
+ u8 tp_mask;
+ int tp, ret;
+
+ for (tp = 0; tp < p3h2x4x->num_target_ports; tp++) {
+ pullup |= hub->hub_config.tp_config[tp].pullup_en ? P3H2X4X_SET_BIT(tp) : 0;
+ mode |= (hub->hub_config.tp_config[tp].mode != P3H2X4X_TP_MODE_I3C) ?
+ P3H2X4X_SET_BIT(tp) : 0;
+ smbus |= (hub->hub_config.tp_config[tp].mode == P3H2X4X_TP_MODE_SMBUS) ?
+ P3H2X4X_SET_BIT(tp) : 0;
+ target_port |= (hub->tp_bus[tp].tp_mask == P3H2X4X_SET_BIT(tp)) ?
+ hub->tp_bus[tp].tp_mask : 0;
+ }
+
+ /* Only touch the bits for the target ports this variant provides. */
+ tp_mask = GENMASK(p3h2x4x->num_target_ports - 1, 0);
+
+ ret = regmap_update_bits(hub->regmap, P3H2X4X_TP_PULLUP_EN, tp_mask, pullup);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(hub->regmap, P3H2X4X_TP_IO_MODE_CONF, tp_mask, mode);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(hub->regmap, P3H2X4X_TP_SMBUS_AGNT_EN, tp_mask, smbus);
+ if (ret)
+ return ret;
+
+ if (target_port & ~smbus) {
+ ret = regmap_write(hub->regmap, P3H2X4X_CP_MUX_SET,
+ P3H2X4X_CONTROLLER_PORT_MUX_REQ);
+ if (ret)
+ return ret;
+ }
+
+ return regmap_update_bits(hub->regmap, P3H2X4X_TP_ENABLE, tp_mask, target_port);
+}
+
+static int p3h2x4x_configure_hw(struct device *dev)
+{
+ struct p3h2x4x_i3c_hub_dev *hub = dev_get_drvdata(dev);
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(dev->parent);
+ int ret, ret2;
+
+ ret = p3h2x4x_configure_ldo(dev);
+ if (ret)
+ return ret;
+
+ /* Protect the unlock-modify-lock sequence with the shared MFD lock */
+ scoped_guard(mutex, &p3h2x4x->protected_reg_lock) {
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ break;
+
+ ret = p3h2x4x_configure_pullup(dev);
+ if (!ret)
+ ret = p3h2x4x_configure_io_strength(dev);
+ if (!ret)
+ ret = p3h2x4x_configure_tp(dev);
+
+ ret2 = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_LOCK_CODE);
+ if (!ret && ret2)
+ ret = ret2;
+ }
+
+ return ret;
+}
+
+static void p3h2x4x_get_target_port_dt_conf(struct device *dev,
+ const struct device_node *node)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(dev->parent);
+ u64 tp_port;
+
+ for_each_available_child_of_node_scoped(node, dev_node) {
+ if (of_property_read_reg(dev_node, 0, &tp_port, NULL))
+ continue;
+
+ if (tp_port < p3h2x4x->num_target_ports) {
+ if (p3h2x4x_i3c_hub->tp_bus[tp_port].of_node) {
+ dev_warn(dev, "Duplicate target port %llu in DT\n", tp_port);
+ continue;
+ }
+
+ p3h2x4x_i3c_hub->tp_bus[tp_port].of_node = of_node_get(dev_node);
+ p3h2x4x_i3c_hub->tp_bus[tp_port].tp_mask = P3H2X4X_SET_BIT(tp_port);
+ p3h2x4x_i3c_hub->tp_bus[tp_port].p3h2x4x_i3c_hub = p3h2x4x_i3c_hub;
+ p3h2x4x_i3c_hub->tp_bus[tp_port].tp_port = tp_port;
+ }
+ }
+}
+
+static int p3h2x4x_parse_tp_dt_settings(struct device *dev,
+ const struct device_node *node,
+ struct tp_configuration tp_config[])
+{
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(dev->parent);
+ u64 id;
+ int ret;
+
+ for_each_available_child_of_node_scoped(node, tp_node) {
+ enum p3h2x4x_tp_mode mode;
+
+ /*
+ * Only "i3c" and "smbus" children describe target ports. Skip any
+ * other child (for example the MFD "regulators" container), which
+ * has no "reg" property.
+ */
+ if (of_node_name_eq(tp_node, "i3c"))
+ mode = P3H2X4X_TP_MODE_I3C;
+ else if (of_node_name_eq(tp_node, "smbus"))
+ mode = P3H2X4X_TP_MODE_SMBUS;
+ else
+ continue;
+
+ ret = of_property_read_reg(tp_node, 0, &id, NULL);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to read reg for %pOF\n",
+ tp_node);
+
+ if (id >= p3h2x4x->num_target_ports)
+ return dev_err_probe(dev, -EINVAL,
+ "Invalid target port index %llu\n",
+ id);
+
+ tp_config[id].mode = mode;
+ tp_config[id].pullup_en =
+ of_property_read_bool(tp_node, "nxp,pullup-enable");
+ }
+
+ return 0;
+}
+
+static int p3h2x4x_get_hub_dt_conf(struct device *dev,
+ const struct device_node *node)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
+
+ of_property_read_u32(node, "nxp,tp0145-pullup-ohms",
+ &p3h2x4x_i3c_hub->hub_config.tp0145_pullup);
+ of_property_read_u32(node, "nxp,tp2367-pullup-ohms",
+ &p3h2x4x_i3c_hub->hub_config.tp2367_pullup);
+ of_property_read_u32(node, "nxp,cp0-io-strength-ohms",
+ &p3h2x4x_i3c_hub->hub_config.cp0_io_strength);
+ of_property_read_u32(node, "nxp,cp1-io-strength-ohms",
+ &p3h2x4x_i3c_hub->hub_config.cp1_io_strength);
+ of_property_read_u32(node, "nxp,tp0145-io-strength-ohms",
+ &p3h2x4x_i3c_hub->hub_config.tp0145_io_strength);
+ of_property_read_u32(node, "nxp,tp2367-io-strength-ohms",
+ &p3h2x4x_i3c_hub->hub_config.tp2367_io_strength);
+
+ return p3h2x4x_parse_tp_dt_settings(dev, node,
+ p3h2x4x_i3c_hub->hub_config.tp_config);
+}
+
+static void p3h2x4x_default_configuration(struct device *dev)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
+ int tp_count;
+
+ p3h2x4x_i3c_hub->hub_config.tp0145_pullup = P3H2X4X_DFT_TP_PULLUP_OHMS;
+ p3h2x4x_i3c_hub->hub_config.tp2367_pullup = P3H2X4X_DFT_TP_PULLUP_OHMS;
+ p3h2x4x_i3c_hub->hub_config.cp0_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
+ p3h2x4x_i3c_hub->hub_config.cp1_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
+ p3h2x4x_i3c_hub->hub_config.tp0145_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
+ p3h2x4x_i3c_hub->hub_config.tp2367_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
+
+ for (tp_count = 0; tp_count < P3H2X4X_TP_MAX_COUNT; ++tp_count)
+ p3h2x4x_i3c_hub->hub_config.tp_config[tp_count].mode = P3H2X4X_TP_MODE_I3C;
+}
+
+static void p3h2x4x_unregister_smbus_adapters_action(void *data)
+{
+ p3h2x4x_unregister_smbus_adapters(data);
+}
+
+static void p3h2x4x_put_target_port_of_nodes(void *data)
+{
+ struct p3h2x4x_i3c_hub_dev *hub = data;
+ int tp;
+
+ for (tp = 0; tp < P3H2X4X_TP_MAX_COUNT; tp++) {
+ of_node_put(hub->tp_bus[tp].of_node);
+ hub->tp_bus[tp].of_node = NULL;
+ }
+}
+
+static void p3h2x4x_clear_i3c_hub_priv(void *data)
+{
+ struct p3h2x4x *p3h2x4x = data;
+
+ /* Drop the IBI handler backpointer; see the ordering note at the registration site. */
+ p3h2x4x->i3c_hub_priv = NULL;
+}
+
+static int p3h2x4x_i3c_hub_probe(struct platform_device *pdev)
+{
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(pdev->dev.parent);
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub;
+ struct device *dev = &pdev->dev;
+ struct device_node *node;
+ int ret, i;
+
+ p3h2x4x_i3c_hub = devm_kzalloc(dev, sizeof(*p3h2x4x_i3c_hub), GFP_KERNEL);
+ if (!p3h2x4x_i3c_hub)
+ return -ENOMEM;
+
+ p3h2x4x_i3c_hub->regmap = p3h2x4x->regmap;
+ p3h2x4x_i3c_hub->dev = dev;
+
+ platform_set_drvdata(pdev, p3h2x4x_i3c_hub);
+ device_set_of_node_from_dev(dev, dev->parent);
+
+ p3h2x4x_default_configuration(dev);
+
+ ret = devm_mutex_init(dev, &p3h2x4x_i3c_hub->etx_mutex);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < P3H2X4X_TP_MAX_COUNT; i++) {
+ ret = devm_mutex_init(dev, &p3h2x4x_i3c_hub->tp_bus[i].port_mutex);
+ if (ret)
+ return ret;
+ }
+
+ /* get hub node from DT */
+ node = dev_of_node(dev);
+ if (!node)
+ return dev_err_probe(dev, -ENODEV, "No Device Tree entry found\n");
+
+ ret = p3h2x4x_get_hub_dt_conf(dev, node);
+ if (ret)
+ return ret;
+
+ p3h2x4x_get_target_port_dt_conf(dev, node);
+
+ ret = devm_add_action_or_reset(dev,
+ p3h2x4x_put_target_port_of_nodes,
+ p3h2x4x_i3c_hub);
+ if (ret)
+ return ret;
+
+ ret = p3h2x4x_configure_hw(dev);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to configure the HUB\n");
+
+ /* Register virtual I3C master controllers for I3C target ports */
+ if (p3h2x4x->i3cdev) {
+ p3h2x4x_i3c_hub->i3cdev = p3h2x4x->i3cdev;
+ /*
+ * Publish the hub context in the MFD parent struct rather than
+ * via i3cdev_set_drvdata(), which would overwrite the parent's
+ * drvdata (struct p3h2x4x) that the IBI handler and other MFD
+ * callbacks rely on. Publish it before p3h2x4x_tp_i3c_algo()
+ * enables IBI, since the IBI handler dereferences it.
+ */
+ p3h2x4x->i3c_hub_priv = p3h2x4x_i3c_hub;
+
+ /*
+ * Register the clear action before enabling IBI so that, on the
+ * devm LIFO unwind (probe failure or removal), the pointer is
+ * cleared only after IBI has been disabled and freed.
+ */
+ ret = devm_add_action_or_reset(dev, p3h2x4x_clear_i3c_hub_priv,
+ p3h2x4x);
+ if (ret)
+ return ret;
+
+ ret = p3h2x4x_tp_i3c_algo(p3h2x4x_i3c_hub);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to register i3c bus\n");
+ }
+
+ /* Register virtual I2C adapters for SMBus target ports */
+ ret = p3h2x4x_tp_smbus_algo(p3h2x4x_i3c_hub);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to add i2c adapter\n");
+
+ ret = devm_add_action_or_reset(dev,
+ p3h2x4x_unregister_smbus_adapters_action,
+ p3h2x4x_i3c_hub);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static const struct platform_device_id p3h2x4x_i3c_hub_id[] = {
+ { "p3h2x4x-i3c-hub" },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, p3h2x4x_i3c_hub_id);
+
+static struct platform_driver p3h2x4x_i3c_hub_driver = {
+ .driver = {
+ .name = "p3h2x4x-i3c-hub",
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ },
+ .probe = p3h2x4x_i3c_hub_probe,
+ .id_table = p3h2x4x_i3c_hub_id,
+};
+module_platform_driver(p3h2x4x_i3c_hub_driver);
+
+MODULE_AUTHOR("Aman Kumar Pandey <aman.kumarpandey@nxp.com>");
+MODULE_AUTHOR("Vikash Bansal <vikash.bansal@nxp.com>");
+MODULE_AUTHOR("Lakshay Piplani <lakshay.piplani@nxp.com>");
+MODULE_DESCRIPTION("P3H2X4X I3C HUB driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c
new file mode 100644
index 000000000000..22c355f60bbf
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2025-2026 NXP
+ * This P3H2X4X driver file contain functions for I3C virtual Bus creation, connect/disconnect
+ * hub network and read/write.
+ */
+#include <linux/i3c/hub.h>
+#include <linux/mfd/p3h2840.h>
+#include <linux/regmap.h>
+
+#include "p3h2840_i3c_hub.h"
+
+static inline struct tp_bus *
+p3h2x4x_bus_from_controller(struct i3c_master_controller *controller)
+{
+ struct i3c_hub_controller *hub_controller;
+
+ hub_controller = container_of(controller, struct i3c_hub_controller, controller);
+
+ return container_of(hub_controller, struct tp_bus, hub_controller);
+}
+
+static void p3h2x4x_hub_enable_port(struct i3c_master_controller *controller)
+{
+ struct tp_bus *bus = p3h2x4x_bus_from_controller(controller);
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = bus->p3h2x4x_i3c_hub;
+
+ if (p3h2x4x_i3c_hub->hub_config.tp_config[bus->tp_port].always_enable)
+ return;
+
+ regmap_set_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP_NET_CON_CONF, bus->tp_mask);
+}
+
+static void p3h2x4x_hub_disable_port(struct i3c_master_controller *controller)
+{
+ struct tp_bus *bus = p3h2x4x_bus_from_controller(controller);
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = bus->p3h2x4x_i3c_hub;
+
+ if (p3h2x4x_i3c_hub->hub_config.tp_config[bus->tp_port].always_enable)
+ return;
+
+ regmap_clear_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP_NET_CON_CONF, bus->tp_mask);
+}
+
+static const struct i3c_hub_ops p3h2x4x_hub_ops = {
+ .enable_port = p3h2x4x_hub_enable_port,
+ .disable_port = p3h2x4x_hub_disable_port,
+};
+
+static void p3h2x4x_unregister_i3c_master(void *data)
+{
+ struct i3c_master_controller *controller = data;
+
+ i3c_master_unregister(controller);
+}
+
+/**
+ * p3h2x4x_tp_i3c_algo - Register I3C virtual masters for I3C target ports.
+ * @p3h2x4x_hub: p3h2x4x device structure.
+ * Return: 0 in case of success, negative error code on failure.
+ */
+int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_hub)
+{
+ struct i3c_master_controller *parent = i3c_dev_get_master(p3h2x4x_hub->i3cdev->desc);
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(p3h2x4x_hub->dev->parent);
+ u8 tp, ntwk_mask = 0;
+ int ret;
+
+ p3h2x4x_hub->hub = devm_kzalloc(p3h2x4x_hub->dev,
+ sizeof(*p3h2x4x_hub->hub),
+ GFP_KERNEL);
+
+ if (!p3h2x4x_hub->hub)
+ return -ENOMEM;
+
+ i3c_hub_init(p3h2x4x_hub->hub,
+ &p3h2x4x_hub_ops,
+ p3h2x4x_hub->i3cdev);
+
+ for (tp = 0; tp < p3h2x4x->num_target_ports; tp++) {
+ if (!p3h2x4x_hub->tp_bus[tp].of_node ||
+ p3h2x4x_hub->hub_config.tp_config[tp].mode != P3H2X4X_TP_MODE_I3C)
+ continue;
+
+ struct i3c_hub_controller *hub_controller =
+ &p3h2x4x_hub->tp_bus[tp].hub_controller;
+ struct i3c_master_controller *controller = &hub_controller->controller;
+
+ hub_controller->parent = parent;
+ hub_controller->hub = p3h2x4x_hub->hub;
+
+ dev_set_drvdata(&controller->dev, hub_controller);
+
+ ret = i3c_hub_reserve_parent_addrslots_from_dt(hub_controller,
+ p3h2x4x_hub->tp_bus[tp].of_node);
+ if (ret)
+ return ret;
+
+ ret = i3c_master_register_fwnode(controller,
+ p3h2x4x_hub->dev,
+ of_fwnode_handle(p3h2x4x_hub->tp_bus[tp].of_node),
+ i3c_hub_master_ops(),
+ false);
+
+ if (ret)
+ return ret;
+
+ ret = devm_add_action_or_reset(p3h2x4x_hub->dev,
+ p3h2x4x_unregister_i3c_master,
+ controller);
+ if (ret)
+ return ret;
+
+ ntwk_mask |= p3h2x4x_hub->tp_bus[tp].tp_mask;
+ p3h2x4x_hub->tp_bus[tp].is_registered = true;
+ p3h2x4x_hub->hub_config.tp_config[tp].always_enable = true;
+ }
+ return regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_mask);
+}
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
new file mode 100644
index 000000000000..107ac4fb7dad
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
@@ -0,0 +1,350 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2025-2026 NXP
+ * This P3H2X4X driver file contain functions for SMBus/I2C virtual Bus creation and read/write.
+ */
+#include <linux/mfd/p3h2840.h>
+#include <linux/regmap.h>
+
+#include "p3h2840_i3c_hub.h"
+
+enum p3h2x4x_smbus_desc_idx {
+ P3H2X4X_DESC_ADDR,
+ P3H2X4X_DESC_TYPE,
+ P3H2X4X_DESC_WRITE_LEN,
+ P3H2X4X_DESC_READ_LEN,
+};
+
+static int p3h2x4x_read_smbus_transaction_status(struct p3h2x4x_i3c_hub_dev *hub,
+ u8 target_port_status,
+ u8 data_length)
+{
+ unsigned int xfer_us, timeout_us, sleep_us;
+ u32 status_read;
+ u8 status;
+ int ret;
+
+ xfer_us = P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length);
+ sleep_us = clamp(xfer_us / P3H2X4X_SMBUS_POLL_COUNT,
+ P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US,
+ P3H2X4X_SMBUS_POLL_INTERVAL_MAX_US);
+
+ /*
+ * Floor the deadline at the SDA-stuck recovery window so an in-spec
+ * bus recovery (agent holding SCL low up to 35 ms) plus status
+ * posting and read-back completes before we return a timeout.
+ */
+ timeout_us = max(xfer_us, P3H2X4X_SMBUS_SCL_LOW_RECOVERY_US);
+
+ ret = regmap_read_poll_timeout(hub->regmap, target_port_status,
+ status_read,
+ status_read & P3H2X4X_SMBUS_TRANSACTION_FINISH_FLAG,
+ sleep_us,
+ timeout_us);
+ if (ret)
+ return ret;
+
+ status = (u8)status_read;
+
+ status = (status & P3H2X4X_TP_TRANSACTION_CODE_MASK)
+ >> P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SHIFT;
+
+ switch (status) {
+ case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_OK:
+ return 0;
+ case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ADDR_NAK:
+ return -ENXIO;
+ case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_DATA_NAK:
+ return -EIO;
+ case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SCL_TO:
+ return -ETIMEDOUT;
+ case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ARB_LOSS:
+ return -EAGAIN;
+ default:
+ return -EIO;
+ }
+}
+
+/*
+ * p3h2x4x_tp_i2c_xfer_msg() - This starts a SMBus write transaction by writing a descriptor
+ * and a message to the p3h2x4x registers. Controller buffer page is determined by multiplying the
+ * target port index by four and adding the base page number to it.
+ */
+static int p3h2x4x_tp_i2c_xfer_msg(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub,
+ struct i2c_msg *xfers,
+ u8 target_port,
+ int nxfers_i, u8 rw)
+{
+ u8 controller_buffer_page = P3H2X4X_CONTROLLER_BUFFER_PAGE + 4 * target_port;
+ u8 target_port_status = P3H2X4X_TP0_SMBUS_AGNT_STS + target_port;
+ u8 desc[P3H2X4X_SMBUS_DESCRIPTOR_SIZE] = { 0 };
+ u8 transaction_type = P3H2X4X_SMBUS_400kHz;
+ int write_length, read_length;
+ u8 addr = xfers[nxfers_i].addr;
+ u8 rw_address = 2 * addr;
+ int ret, ret2;
+
+ if (rw == 2) { /* write and read */
+ write_length = xfers[nxfers_i].len;
+ read_length = xfers[nxfers_i + 1].len;
+ } else if (rw == 1) {
+ rw_address |= P3H2X4X_SET_BIT(0);
+ write_length = 0;
+ read_length = xfers[nxfers_i].len;
+ } else {
+ write_length = xfers[nxfers_i].len;
+ read_length = 0;
+ }
+
+ desc[P3H2X4X_DESC_ADDR] = rw_address;
+ if (rw == 2)
+ desc[P3H2X4X_DESC_TYPE] = transaction_type | P3H2X4X_SET_BIT(0);
+ else
+ desc[P3H2X4X_DESC_TYPE] = transaction_type;
+ desc[P3H2X4X_DESC_WRITE_LEN] = write_length;
+ desc[P3H2X4X_DESC_READ_LEN] = read_length;
+
+ ret = regmap_write(p3h2x4x_i3c_hub->regmap, target_port_status,
+ P3H2X4X_TP_BUFFER_STATUS_MASK);
+ if (ret)
+ goto out;
+
+ ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, controller_buffer_page);
+
+ if (ret)
+ goto out;
+
+ ret = regmap_bulk_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_CONTROLLER_AGENT_BUFF,
+ desc, P3H2X4X_SMBUS_DESCRIPTOR_SIZE);
+
+ if (ret)
+ goto out;
+
+ if (!(rw % 2) && xfers[nxfers_i].len) {
+ ret = regmap_bulk_write(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_CONTROLLER_AGENT_BUFF_DATA,
+ xfers[nxfers_i].buf, xfers[nxfers_i].len);
+ if (ret)
+ goto out;
+ }
+
+ ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP_SMBUS_AGNT_TRANS_START,
+ p3h2x4x_i3c_hub->tp_bus[target_port].tp_mask);
+
+ if (ret)
+ goto out;
+
+ ret = p3h2x4x_read_smbus_transaction_status(p3h2x4x_i3c_hub,
+ target_port_status,
+ (write_length + read_length));
+ if (ret)
+ goto out;
+
+ if (rw) {
+ if (rw == 2)
+ nxfers_i += 1;
+
+ if (xfers[nxfers_i].len) {
+ u8 *dma_buf = i2c_get_dma_safe_msg_buf(&xfers[nxfers_i], 1);
+
+ if (!dma_buf) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_CONTROLLER_AGENT_BUFF_DATA + write_length,
+ dma_buf, xfers[nxfers_i].len);
+ i2c_put_dma_safe_msg_buf(dma_buf, &xfers[nxfers_i], !ret);
+ if (ret)
+ goto out;
+ }
+ }
+out:
+ ret2 = regmap_write(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_PAGE_PTR, 0x00);
+ if (!ret && ret2)
+ ret = ret2;
+
+ return ret;
+}
+
+/*
+ * This function will be called whenever you call I2C read, write APIs like
+ * i2c_master_send(), i2c_master_recv() etc.
+ */
+static s32 p3h2x4x_tp_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
+{
+ int ret_sum = 0, ret, msg_count;
+ u8 rw;
+
+ struct tp_bus *bus = i2c_get_adapdata(adap);
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = bus->p3h2x4x_i3c_hub;
+
+ guard(mutex)(&p3h2x4x_i3c_hub->etx_mutex);
+ guard(mutex)(&bus->port_mutex);
+
+ for (msg_count = 0; msg_count < num; msg_count++) {
+ rw = (msgs[msg_count].flags & I2C_M_RD) ? 1 : 0;
+ if (!rw) {
+ /* If a write message is immediately followed by a read message to
+ * the same address, consider combining them into a single transaction.
+ */
+ if (msg_count + 1 < num &&
+ msgs[msg_count].addr == msgs[msg_count + 1].addr &&
+ (msgs[msg_count + 1].flags & I2C_M_RD)) {
+ if (msgs[msg_count].len + msgs[msg_count + 1].len >
+ P3H2X4X_SMBUS_PAYLOAD_SIZE)
+ return -EINVAL;
+
+ rw = 2;
+ msg_count += 1;
+ ret_sum += 1;
+ }
+ }
+
+ ret = p3h2x4x_tp_i2c_xfer_msg(p3h2x4x_i3c_hub,
+ msgs,
+ bus->tp_port,
+ (rw == 2) ? (msg_count - 1) : msg_count,
+ rw);
+ if (ret)
+ return ret;
+
+ ret_sum++;
+ }
+ return ret_sum;
+}
+
+static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter)
+{
+ return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
+}
+
+static const struct i2c_adapter_quirks p3h2x4x_tp_i2c_quirks = {
+ .max_read_len = P3H2X4X_SMBUS_PAYLOAD_SIZE,
+ .max_write_len = P3H2X4X_SMBUS_PAYLOAD_SIZE,
+};
+
+/*
+ * I2C algorithm Structure
+ */
+static struct i2c_algorithm p3h2x4x_tp_i2c_algorithm = {
+ .master_xfer = p3h2x4x_tp_i2c_xfer,
+ .functionality = p3h2x4x_tp_smbus_funcs,
+};
+
+void p3h2x4x_unregister_smbus_adapters(struct p3h2x4x_i3c_hub_dev *hub)
+{
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(hub->dev->parent);
+ u8 tp;
+
+ for (tp = 0; tp < p3h2x4x->num_target_ports; tp++) {
+ if (!hub->tp_bus[tp].tp_smbus_adapter)
+ continue;
+
+ i2c_del_adapter(hub->tp_bus[tp].tp_smbus_adapter);
+
+ guard(mutex)(&hub->etx_mutex);
+ hub->tp_bus[tp].tp_smbus_adapter = NULL;
+ hub->tp_bus[tp].is_registered = false;
+ }
+}
+
+/**
+ * p3h2x4x_tp_smbus_algo - Register I2C adapters for SMBus target ports.
+ * @hub: p3h2x4x device structure.
+ * Return: 0 in case of success, negative error code on failure.
+ */
+int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *hub)
+{
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(hub->dev->parent);
+ int ret, ret2;
+ u8 tp;
+
+ scoped_guard(mutex, &p3h2x4x->protected_reg_lock) {
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ break;
+
+ ret = regmap_write(hub->regmap, P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG,
+ P3H2X4X_IBI_DISABLED);
+
+ ret2 = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_LOCK_CODE);
+ if (!ret && ret2)
+ ret = ret2;
+ }
+ if (ret)
+ return ret;
+
+ for (tp = 0; tp < p3h2x4x->num_target_ports; tp++) {
+ if (!hub->tp_bus[tp].of_node ||
+ hub->hub_config.tp_config[tp].mode != P3H2X4X_TP_MODE_SMBUS)
+ continue;
+
+ /* Allocate adapter */
+ struct i2c_adapter *smbus_adapter =
+ devm_kzalloc(hub->dev, sizeof(*smbus_adapter), GFP_KERNEL);
+ if (!smbus_adapter) {
+ p3h2x4x_unregister_smbus_adapters(hub);
+ return -ENOMEM;
+ }
+
+ /* Initialize adapter */
+ smbus_adapter->owner = THIS_MODULE;
+ smbus_adapter->class = I2C_CLASS_HWMON;
+ smbus_adapter->algo = &p3h2x4x_tp_i2c_algorithm;
+ smbus_adapter->quirks = &p3h2x4x_tp_i2c_quirks;
+ smbus_adapter->dev.parent = hub->dev;
+ smbus_adapter->dev.of_node = hub->tp_bus[tp].of_node;
+ snprintf(smbus_adapter->name, sizeof(smbus_adapter->name),
+ "p3h2x4x-i3c-hub.tp-port-%d", tp);
+
+ i2c_set_adapdata(smbus_adapter, &hub->tp_bus[tp]);
+
+ /*
+ * Publish the callback-visible state before i2c_add_adapter(),
+ * which can synchronously probe a DT slave and invoke
+ * reg_slave() that inspects is_registered/tp_smbus_client and
+ * sets ibi_en. Seeding defaults here keeps reg_slave()'s view
+ * consistent and avoids clobbering its ibi_en update. Do not
+ * hold etx_mutex across the call, since reg_slave() also takes it.
+ */
+ scoped_guard(mutex, &hub->etx_mutex) {
+ hub->tp_bus[tp].tp_smbus_adapter = smbus_adapter;
+ hub->tp_bus[tp].tp_smbus_client = NULL;
+ hub->tp_bus[tp].is_registered = true;
+ hub->hub_config.tp_config[tp].ibi_en = false;
+ }
+
+ /* Register adapter */
+ ret = i2c_add_adapter(smbus_adapter);
+ if (ret) {
+ scoped_guard(mutex, &hub->etx_mutex) {
+ hub->tp_bus[tp].is_registered = false;
+ hub->tp_bus[tp].tp_smbus_adapter = NULL;
+ }
+ p3h2x4x_unregister_smbus_adapters(hub);
+ return ret;
+ }
+ }
+
+ /*
+ * Configure the SMBus Target Agents to hold SDA low when both of a
+ * port's received-data buffers are full. This provides flow control
+ * for MCTP: it prevents MCTP transmitters on the target ports from
+ * timing out when the upstream controller does not service the agent
+ * buffers in time and the port only receives write messages.
+ */
+ ret = regmap_update_bits(hub->regmap, P3H2X4X_ONCHIP_TD_AND_SMBUS_AGNT_CONF,
+ P3H2X4X_TARGET_AGENT_DFT_IBI_CONF_MASK,
+ P3H2X4X_TARGET_AGENT_DFT_IBI_CONF);
+ if (ret) {
+ p3h2x4x_unregister_smbus_adapters(hub);
+ return ret;
+ }
+
+ return 0;
+}
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH v16 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
` (6 preceding siblings ...)
2026-08-26 10:38 ` [PATCH v16 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality Lakshay Piplani
@ 2026-08-26 10:38 ` Lakshay Piplani
7 siblings, 0 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-26 10:38 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
Add SMBus slave mode support for the P3H2x4x hub SMBus target ports.
The hub SMBus slave agent can receive downstream payloads into target
buffers and report receive events through IBI. Add CONFIG_I2C_SLAVE
to support the receive path and forward the received payloads to the
registered I2C slave client through i2c_slave_event().
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
Changes in v16:
- Always clear the software slave state on unregister: even if the hardware
IBI-disable write fails, log the error but still set bus->tp_smbus_client = NULL
and return 0, so a later IBI cannot deref a dangling client and call a freed slave_cb
- Use a heap DMA-safe buffer for the SMBus-agent RX path
instead of a stack buffer passed to regmap_bulk_read() over I3C
- Advertise I2C_FUNC_SLAVE when CONFIG_I2C_SLAVE is enabled, and reject slave
registration with -EOPNOTSUPP unless an I3C upstream device and working IBI
path are available (hub->i3cdev and hub->ibi_ready)
- Validate the IBI payload length and bound the target-port loop by
num_target_ports
Changes in v15:
- Retrieve the hub context through the shared MFD data instead of replacing
the I3C device's parent driver data
- Rework SMBus slave registration and unregistration to use the shared
protected-register lock
- Report protected-register relock failures separately from the original
enable or disable operation
Changes in v14:
- Clear receive-buffer flags even on SMBus receive error paths to avoid
repeated IBI storms
- Decode receive-buffer status using FIELD_GET()
- Fix overflow status value and explicitly clear overflow after reading both
target buffers
Changes in v13:
- Make IBI setup optional and robust: avoid probe failure when IBI is unsupported and add proper
cleanup using devm actions
- Fix SMBus slave receive path: avoid over-clearing buffer status, handle unregistered ports,
and ensure correct event delivery
- Improve safety by adding proper locking around shared state
Changes in v12:
- Add devm cleanup for IBI request/enable path
- Fix NULL pointer dereference before tp_smbus_client check
- Clear tp_smbus_client before disabling SMBus-agent IBI in unreg_slave()
Changes in v11:
- Improve SMBus slave mode payload validation and parsing
Changes in v10:
- Split SMBus slave mode support into a separate patch
---
---
drivers/i3c/hub/p3h2840_i3c_hub.h | 19 ++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 54 ++++-
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 306 +++++++++++++++++++++++-
3 files changed, 377 insertions(+), 2 deletions(-)
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
index 7a1345924e3f..0e59351e7b6d 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub.h
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -123,6 +123,11 @@
#define BUF_RECEIVED_FLAG_MASK GENMASK(3, 1)
#define BUF_RECEIVED_FLAG_TF_MASK GENMASK(3, 0)
+#define P3H2X4X_TARGET_BUF_0_RECEIVE_VAL 1
+#define P3H2X4X_TARGET_BUF_1_RECEIVE_VAL 2
+#define P3H2X4X_TARGET_BUF_0_1_RECEIVE_VAL 3
+#define P3H2X4X_TARGET_BUF_OVRFL_VAL 7
+
#define P3H2X4X_TARGET_AGENT_LOCAL_DEV 0x11
#define P3H2X4X_TARGET_BUFF_0_PAGE 0x12
#define P3H2X4X_TARGET_BUFF_1_PAGE 0x13
@@ -315,6 +320,10 @@ struct p3h2x4x_i3c_hub_dev {
struct i2c_client *i2c_client;
struct hub_configuration hub_config;
struct tp_bus tp_bus[P3H2X4X_TP_MAX_COUNT];
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ bool ibi_ready;
+ u8 *slave_rx_buffer;
+#endif
struct i3c_hub *hub;
};
@@ -340,4 +349,14 @@ int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub);
*/
int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub);
+/**
+ * p3h2x4x_ibi_handler - IBI handler.
+ * @i3cdev: i3c device.
+ * @payload: two byte IBI payload data.
+ */
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+void p3h2x4x_ibi_handler(struct i3c_device *i3cdev,
+ const struct i3c_ibi_payload *payload);
+#endif
+
#endif /* P3H2840_I3C_HUB_H */
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c
index 22c355f60bbf..03f17785b1c7 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c
@@ -10,6 +10,14 @@
#include "p3h2840_i3c_hub.h"
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+static const struct i3c_ibi_setup p3h2x4x_ibireq = {
+ .handler = p3h2x4x_ibi_handler,
+ .max_payload_len = P3H2X4X_MAX_PAYLOAD_LEN,
+ .num_slots = P3H2X4X_NUM_SLOTS,
+};
+#endif
+
static inline struct tp_bus *
p3h2x4x_bus_from_controller(struct i3c_master_controller *controller)
{
@@ -54,6 +62,16 @@ static void p3h2x4x_unregister_i3c_master(void *data)
i3c_master_unregister(controller);
}
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+static void p3h2x4x_free_ibi(void *data)
+{
+ struct i3c_device *i3cdev = data;
+
+ i3c_device_disable_ibi(i3cdev);
+ i3c_device_free_ibi(i3cdev);
+}
+#endif
+
/**
* p3h2x4x_tp_i3c_algo - Register I3C virtual masters for I3C target ports.
* @p3h2x4x_hub: p3h2x4x device structure.
@@ -115,5 +133,39 @@ int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_hub)
p3h2x4x_hub->tp_bus[tp].is_registered = true;
p3h2x4x_hub->hub_config.tp_config[tp].always_enable = true;
}
- return regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_mask);
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ p3h2x4x_hub->slave_rx_buffer = devm_kzalloc(p3h2x4x_hub->dev,
+ P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE,
+ GFP_KERNEL);
+ if (!p3h2x4x_hub->slave_rx_buffer)
+ return -ENOMEM;
+
+ ret = i3c_device_request_ibi(p3h2x4x_hub->i3cdev, &p3h2x4x_ibireq);
+ if (ret) {
+ dev_warn(p3h2x4x_hub->dev,
+ "IBI not available, SMBus slave mode disabled\n");
+ p3h2x4x_hub->ibi_ready = false;
+ } else {
+ ret = i3c_device_enable_ibi(p3h2x4x_hub->i3cdev);
+ if (ret) {
+ i3c_device_free_ibi(p3h2x4x_hub->i3cdev);
+ dev_warn(p3h2x4x_hub->dev,
+ "Failed to enable IBI, SMBus slave mode disabled\n");
+ p3h2x4x_hub->ibi_ready = false;
+ } else {
+ p3h2x4x_hub->ibi_ready = true;
+
+ ret = devm_add_action_or_reset(p3h2x4x_hub->dev,
+ p3h2x4x_free_ibi,
+ p3h2x4x_hub->i3cdev);
+ if (ret) {
+ p3h2x4x_hub->ibi_ready = false;
+ return ret;
+ }
+ }
+ }
+#endif
+ ret = regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_mask);
+
+ return ret;
}
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
index 107ac4fb7dad..ef5cb27bc338 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
@@ -3,6 +3,8 @@
* Copyright 2025-2026 NXP
* This P3H2X4X driver file contain functions for SMBus/I2C virtual Bus creation and read/write.
*/
+#include <linux/bitfield.h>
+#include <linux/i3c/device.h>
#include <linux/mfd/p3h2840.h>
#include <linux/regmap.h>
@@ -15,6 +17,178 @@ enum p3h2x4x_smbus_desc_idx {
P3H2X4X_DESC_READ_LEN,
};
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+static void p3h2x4x_read_smbus_agent_rx_buf(struct i3c_device *i3cdev, enum p3h2x4x_rcv_buf rfbuf,
+ enum p3h2x4x_tp tp)
+{
+ struct p3h2x4x *p3h2x4x = i3cdev_get_drvdata(i3cdev);
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub;
+ u8 target_buffer_page, flag_clear, temp = 0, i, addr;
+ u32 packet_len, slave_address;
+ struct i2c_client *client;
+ u8 *slave_rx_buffer;
+ int ret;
+
+ if (!p3h2x4x || !p3h2x4x->i3c_hub_priv)
+ return;
+
+ p3h2x4x_i3c_hub = p3h2x4x->i3c_hub_priv;
+ slave_rx_buffer = p3h2x4x_i3c_hub->slave_rx_buffer;
+
+ switch (rfbuf) {
+ case RCV_BUF_0:
+ target_buffer_page = P3H2X4X_TARGET_BUFF_0_PAGE;
+ flag_clear = P3H2X4X_TARGET_BUF_0_RECEIVE;
+ break;
+ case RCV_BUF_1:
+ target_buffer_page = P3H2X4X_TARGET_BUFF_1_PAGE;
+ flag_clear = P3H2X4X_TARGET_BUF_1_RECEIVE;
+ break;
+ default:
+ return;
+ }
+
+ target_buffer_page += P3H2X4X_NO_PAGE_PER_TP * tp;
+
+ ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, target_buffer_page);
+ if (ret)
+ goto ibi_err;
+
+ /* read buffer length */
+ ret = regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_LENGTH, &packet_len);
+ if (ret)
+ goto ibi_err;
+
+ if (packet_len)
+ packet_len = packet_len - 1;
+
+ if (packet_len > P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE) {
+ dev_err(&i3cdev->dev, "Received message too big for p3h2x4x buffer\n");
+ goto ibi_err;
+ }
+
+ /* read slave address */
+ ret = regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_ADDRESS, &slave_address);
+ if (ret)
+ goto ibi_err;
+
+ /* read data */
+ if (packet_len) {
+ ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_DATA,
+ slave_rx_buffer, packet_len);
+ if (ret)
+ goto ibi_err;
+ }
+
+ client = p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client;
+ if (!client)
+ goto ibi_err;
+
+ /* notify slave driver about received data */
+ if ((client->addr & 0x7f) == (slave_address >> 1)) {
+ addr = slave_address >> 1;
+ i2c_slave_event(client,
+ I2C_SLAVE_WRITE_REQUESTED, &addr);
+ for (i = 0; i < packet_len; i++) {
+ temp = slave_rx_buffer[i];
+ i2c_slave_event(client,
+ I2C_SLAVE_WRITE_RECEIVED, &temp);
+ }
+ i2c_slave_event(client, I2C_SLAVE_STOP, &temp);
+ }
+
+ibi_err:
+ regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, 0x00);
+
+ regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP0_SMBUS_AGNT_STS + tp, flag_clear);
+}
+
+/**
+ * p3h2x4x_ibi_handler - IBI handler.
+ * @i3cdev: i3c device.
+ * @payload: two byte IBI payload data.
+ *
+ */
+void p3h2x4x_ibi_handler(struct i3c_device *i3cdev,
+ const struct i3c_ibi_payload *payload)
+{
+ struct p3h2x4x *p3h2x4x = i3cdev_get_drvdata(i3cdev);
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub;
+ u8 payload_byte_one, payload_byte_two;
+ u32 target_port_status;
+ const u8 *data;
+ int ret, i;
+
+ if (!payload || payload->len < P3H2X4X_MAX_PAYLOAD_LEN)
+ return;
+
+ data = payload->data;
+ payload_byte_one = data[0];
+
+ if (!(payload_byte_one & P3H2X4X_SMBUS_AGENT_EVENT_FLAG_STATUS))
+ return;
+
+ p3h2x4x_i3c_hub = p3h2x4x ? p3h2x4x->i3c_hub_priv : NULL;
+
+ if (!p3h2x4x_i3c_hub || !p3h2x4x_i3c_hub->regmap)
+ return;
+
+ payload_byte_two = data[1];
+ guard(mutex)(&p3h2x4x_i3c_hub->etx_mutex);
+
+ for (i = 0; i < p3h2x4x->num_target_ports; ++i) {
+ if (!((payload_byte_two >> i) & 0x01))
+ continue;
+
+ if (!p3h2x4x_i3c_hub->tp_bus[i].is_registered) {
+ dev_dbg(&i3cdev->dev, "IBI for unregistered SMBus port %u\n", i);
+ regmap_write(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_TP0_SMBUS_AGNT_STS + i,
+ BUF_RECEIVED_FLAG_TF_MASK);
+ continue;
+ }
+
+ ret = regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP0_SMBUS_AGNT_STS + i,
+ &target_port_status);
+ if (ret) {
+ dev_err(&i3cdev->dev, "target port read status failed %d\n", ret);
+ continue;
+ }
+
+ if (target_port_status & P3H2X4X_TARGET_BUF_CA_TF)
+ regmap_write(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_TP0_SMBUS_AGNT_STS + i,
+ P3H2X4X_TARGET_BUF_CA_TF);
+
+ /* process data receive buffer */
+ switch (FIELD_GET(BUF_RECEIVED_FLAG_MASK, target_port_status)) {
+ case P3H2X4X_TARGET_BUF_0_RECEIVE_VAL:
+ p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_0, i);
+ break;
+ case P3H2X4X_TARGET_BUF_1_RECEIVE_VAL:
+ p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_1, i);
+ break;
+ case P3H2X4X_TARGET_BUF_0_1_RECEIVE_VAL:
+ p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_0, i);
+ p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_1, i);
+ break;
+ case P3H2X4X_TARGET_BUF_OVRFL_VAL:
+ p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_0, i);
+ p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_1, i);
+ regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP0_SMBUS_AGNT_STS + i,
+ P3H2X4X_TARGET_BUF_OVRFL);
+ dev_err(&i3cdev->dev, "Overflow, reading buffer zero and one\n");
+ break;
+ default:
+ regmap_write(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_TP0_SMBUS_AGNT_STS + i,
+ target_port_status & BUF_RECEIVED_FLAG_MASK);
+ break;
+ }
+ }
+}
+#endif
+
static int p3h2x4x_read_smbus_transaction_status(struct p3h2x4x_i3c_hub_dev *hub,
u8 target_port_status,
u8 data_length)
@@ -218,8 +392,134 @@ static s32 p3h2x4x_tp_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, i
static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter)
{
- return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
+ u32 funcs = I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
+
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ struct tp_bus *bus = i2c_get_adapdata(adapter);
+ struct p3h2x4x_i3c_hub_dev *hub = bus->p3h2x4x_i3c_hub;
+
+ /*
+ * Only advertise slave support when the upstream IBI path is usable.
+ * Otherwise reg_slave() returns -EOPNOTSUPP while functionality()
+ * reports I2C_FUNC_SLAVE, which is inconsistent for callers.
+ */
+ if (hub->ibi_ready)
+ funcs |= I2C_FUNC_SLAVE;
+#endif
+
+ return funcs;
+}
+
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+static int p3h2x4x_tp_i2c_reg_slave(struct i2c_client *slave)
+{
+ struct tp_bus *bus = i2c_get_adapdata(slave->adapter);
+ struct p3h2x4x_i3c_hub_dev *hub = bus->p3h2x4x_i3c_hub;
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(hub->dev->parent);
+ int relock_ret = 0;
+ int ret = 0;
+
+ guard(mutex)(&hub->etx_mutex);
+
+ if (!hub->i3cdev || !hub->ibi_ready)
+ return -EOPNOTSUPP;
+
+ if (bus->tp_smbus_client)
+ return -EBUSY;
+
+ scoped_guard(mutex, &p3h2x4x->protected_reg_lock) {
+ /* Unlock access to protected registers */
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ break;
+
+ ret = regmap_set_bits(hub->regmap,
+ P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG,
+ bus->tp_mask);
+
+ /* Lock access to protected registers */
+ relock_ret = regmap_write(hub->regmap,
+ P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_LOCK_CODE);
+ }
+
+ /*
+ * A relock failure does not undo the IBI enable, so report it
+ * separately and keep the callback result tied to the IBI operation.
+ */
+ if (relock_ret)
+ dev_err(hub->dev,
+ "failed to restore protected register lock: %d\n",
+ relock_ret);
+
+ if (ret)
+ return ret;
+
+ /*
+ * Publish the software state only after the hardware IBI has been
+ * enabled successfully.
+ */
+ bus->tp_smbus_client = slave;
+ hub->hub_config.tp_config[bus->tp_port].ibi_en = true;
+
+ return 0;
+}
+
+static int p3h2x4x_tp_i2c_unreg_slave(struct i2c_client *slave)
+{
+ struct tp_bus *bus = i2c_get_adapdata(slave->adapter);
+ struct p3h2x4x_i3c_hub_dev *hub = bus->p3h2x4x_i3c_hub;
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(hub->dev->parent);
+ int relock_ret = 0;
+ int ret = 0;
+
+ guard(mutex)(&hub->etx_mutex);
+
+ if (bus->tp_smbus_client != slave)
+ return -EINVAL;
+
+ scoped_guard(mutex, &p3h2x4x->protected_reg_lock) {
+ /* Unlock access to protected registers */
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ break;
+
+ ret = regmap_clear_bits(hub->regmap,
+ P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG,
+ bus->tp_mask);
+
+ /* Lock access to protected registers */
+ relock_ret = regmap_write(hub->regmap,
+ P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_LOCK_CODE);
+ }
+
+ if (relock_ret)
+ dev_err(hub->dev,
+ "failed to restore protected register lock: %d\n",
+ relock_ret);
+
+ if (ret)
+ dev_err(hub->dev,
+ "failed to disable SMBus agent IBI on port %u: %d\n",
+ bus->tp_port, ret);
+
+ /*
+ * Slave backend removal cannot be rolled back. Detach the
+ * callback-visible state even if disabling the hardware IBI failed,
+ * and return 0 so the core completes teardown; the failure is still
+ * visible in the log above. etx_mutex serializes this with the IBI
+ * handler, so later events are acknowledged without invoking the
+ * removed client.
+ */
+ bus->tp_smbus_client = NULL;
+ hub->hub_config.tp_config[bus->tp_port].ibi_en = false;
+
+ return 0;
}
+#endif
static const struct i2c_adapter_quirks p3h2x4x_tp_i2c_quirks = {
.max_read_len = P3H2X4X_SMBUS_PAYLOAD_SIZE,
@@ -231,6 +531,10 @@ static const struct i2c_adapter_quirks p3h2x4x_tp_i2c_quirks = {
*/
static struct i2c_algorithm p3h2x4x_tp_i2c_algorithm = {
.master_xfer = p3h2x4x_tp_i2c_xfer,
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ .reg_slave = p3h2x4x_tp_i2c_reg_slave,
+ .unreg_slave = p3h2x4x_tp_i2c_unreg_slave,
+#endif
.functionality = p3h2x4x_tp_smbus_funcs,
};
--
2.25.1
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH v16 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
2026-08-26 10:38 ` [PATCH v16 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub Lakshay Piplani
@ 2026-08-26 10:48 ` sashiko-bot
0 siblings, 0 replies; 22+ messages in thread
From: sashiko-bot @ 2026-08-26 10:48 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-i3c, conor+dt, robh, devicetree, Frank.Li,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Driver-specific header file is incorrectly placed in the global include/linux/mfd/ directory.
--
commit f0356d778985b362783f95ecd394c166098e5c31
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
This commit introduces a driver for the on-die regulators of the NXP P3H2x4x
i3c hub family. It registers the regulators via the MFD framework and exposes
them to the regulator subsystem.
> diff --git a/drivers/regulator/p3h2840_i3c_hub_regulator.c b/drivers/regulator/p3h2840_i3c_hub_regulator.c
> new file mode 100644
> --- /dev/null
> +++ b/drivers/regulator/p3h2840_i3c_hub_regulator.c
> @@ -0,0 +1,284 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Copyright 2025-2026 NXP
> + * NXP P3H2X4X regulator driver file contain functions for enable/disable regulator
> + * and voltage set/get.
> + */
> +#include <linux/bitfield.h>
> +#include <linux/cleanup.h>
> +#include <linux/mfd/p3h2840.h>
[Severity: Low]
Does this driver-specific header need to be placed in the global
include/linux/mfd/ directory?
According to MFD subsystem guidelines, driver-specific header files that are
only used by the parent and its immediate children should be kept local to
drivers/mfd/ rather than cluttering the global include space. The commit
message explicitly notes that this header is "used only by the parent and
its children".
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826103819.1614843-1-lakshay.piplani@nxp.com?part=5
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers
2026-08-26 10:38 ` [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers Lakshay Piplani
@ 2026-08-26 10:50 ` sashiko-bot
2026-08-26 16:42 ` Frank Li
1 sibling, 0 replies; 22+ messages in thread
From: sashiko-bot @ 2026-08-26 10:50 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-i3c, Frank.Li, Alexandre Belloni, conor+dt, robh,
devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Medium] Unnecessary heavy workqueue allocation and teardown when a controller does not support IBIs.
Pre-existing issues:
- [Critical] Potential use-after-free of generic IBI pool slots and `dev->ibi` when disabling an active IBI fails during teardown.
--
commit f38c11b5d94a35f5b99f6a1f23bbb449f122ded4
Author: Lakshay Piplani <lakshay.piplani@nxp.com>
i3c: master: Add controller-only device operation helpers
The generic I3C device attach and IBI paths perform both core-level
bookkeeping and controller-specific operations. Add controller-only helpers
that invoke the controller callbacks without updating the bus device list,
address-slot state, or generic IBI lifecycle. Also release the generic IBI
workqueue and state when the controller request callback fails.
> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
> index 03fb41f0786c5..88379b6809a64 100644
> --- a/drivers/i3c/master.c
> +++ b/drivers/i3c/master.c
[ ... ]
> @@ -3600,13 +3752,9 @@ EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_locked);
> int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev,
> const struct i3c_ibi_setup *req)
> {
> - struct i3c_master_controller *master = i3c_dev_get_master(dev);
> struct i3c_device_ibi_info *ibi;
> int ret;
>
> - if (!master->ops->request_ibi)
> - return -EOPNOTSUPP;
> -
> if (dev->ibi)
> return -EBUSY;
>
[ ... ]
> @@ -3627,8 +3775,15 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev,
> ibi->num_slots = req->num_slots;
>
> dev->ibi = ibi;
> - ret = master->ops->request_ibi(dev, req);
> + ret = i3c_dev_request_ibi_controller_locked(dev, req);
> if (ret) {
> + /*
> + * The controller request callback failed, so tear down the
> + * workqueue allocated above before freeing the IBI object.
> + * This is the owner of the workqueue, so it must destroy it
> + * here to avoid leaking it on the error path.
> + */
> + destroy_workqueue(ibi->wq);
> kfree(ibi);
> dev->ibi = NULL;
> }
[Severity: Medium]
Since the check for master->ops->request_ibi was moved into
i3c_dev_request_ibi_controller_locked(), this code now unconditionally
creates a new ordered workqueue during device probe even when the
underlying controller doesn't support IBIs.
Does this unnecessary allocation and destruction of a workqueue thread
(which involves sysfs registration and RCU synchronization) add a noticeable
performance delay for devices on controllers without IBI support?
[ ... ]
> @@ -3667,7 +3843,7 @@ void i3c_dev_free_ibi_locked(struct i3c_dev_desc *dev)
> dev_err(&master->dev, "Failed to disable IBI before freeing\n");
> }
>
> - master->ops->free_ibi(dev);
> + i3c_dev_free_ibi_controller_locked(dev);
>
> if (dev->ibi->wq) {
> destroy_workqueue(dev->ibi->wq);
[Severity: Critical]
This is a pre-existing issue, but can calling
i3c_dev_free_ibi_controller_locked() before destroy_workqueue(dev->ibi->wq)
cause a use-after-free?
If i3c_dev_disable_ibi_locked() fails (e.g. due to a bus timeout), the
teardown continues and invokes the controller's free_ibi callback, which
immediately frees the generic IBI pool memory.
If there are still pending IBI work items because destroy_workqueue() hasn't
been called to drain them yet, will the workqueue process them and
dereference the freed generic IBI slots?
Additionally, could an active hardware interrupt still fire and race with
the controller's free_ibi callback, leading to memory corruption inside the
interrupt handler?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826103819.1614843-1-lakshay.piplani@nxp.com?part=2
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support
2026-08-26 10:38 ` [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support Lakshay Piplani
@ 2026-08-26 10:52 ` sashiko-bot
2026-08-26 16:36 ` Frank Li
1 sibling, 0 replies; 22+ messages in thread
From: sashiko-bot @ 2026-08-26 10:52 UTC (permalink / raw)
To: Lakshay Piplani
Cc: robh, conor+dt, Alexandre Belloni, Frank.Li, linux-i3c,
devicetree
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
Pre-existing issues:
- [High] Failure in i3c_bus_init() causes i3c_masterdev_release() to erroneously remove ID 0 from the IDR.
--
commit 1f089e3141f5a635df33d2273d7de2114b2f6ef5
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
i3c: master: Add APIs for I3C hub support
Add CCC helpers to check CCC support and send CCC commands, address slot
helpers to query and update I3C bus address slot state, registering virtual
masters with an explicit firmware node, and exposing the bus maintenance
lock helpers.
> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
> index f1be38a640ca1..03fb41f0786c5 100644
> --- a/drivers/i3c/master.c
> +++ b/drivers/i3c/master.c
[ ... ]
> @@ -3195,34 +3267,31 @@ static int i3c_master_check_ops(const struct i3c_master_controller_ops *ops)
> }
>
> /**
> - * i3c_master_register() - register an I3C master
> + * i3c_master_register_fwnode() - register an I3C master with a custom fwnode
> * @master: master used to send frames on the bus
> - * @parent: the parent device (the one that provides this I3C master
> - * controller)
> + * @parent: the parent device providing this I3C master controller
> + * @fwnode: firmware node describing this I3C bus, or NULL
> * @ops: the master controller operations
> - * @secondary: true if you are registering a secondary master. Will return
> - * -EOPNOTSUPP if set to true since secondary masters are not yet
> - * supported
> + * @secondary: true if registering a secondary master
[ ... ]
> @@ -3352,6 +3427,39 @@ int i3c_master_register(struct i3c_master_controller *master,
>
> return ret;
> }
> +EXPORT_SYMBOL_GPL(i3c_master_register_fwnode);
[Severity: High]
This is a pre-existing issue, but can a failure in i3c_bus_init() cause
i3c_masterdev_release() to erroneously remove ID 0 from the IDR?
If idr_alloc() fails in i3c_bus_init(), i3cbus->id remains 0. The error
path in i3c_master_register_fwnode() branches to err_put_dev, calling
put_device(), which invokes i3c_masterdev_release() and unconditionally
calls i3c_bus_cleanup().
This executes idr_remove(&i3c_bus_idr, 0), removing whatever valid bus
happens to own ID 0. This corrupts the IDR, breaking bus lookups for bus 0
and potentially leading to a use-after-free or a double allocation of ID 0
later.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826103819.1614843-1-lakshay.piplani@nxp.com?part=1
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
2026-08-26 10:38 ` [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator Lakshay Piplani
@ 2026-08-26 10:56 ` sashiko-bot
2026-08-27 10:03 ` Krzysztof Kozlowski
1 sibling, 0 replies; 22+ messages in thread
From: sashiko-bot @ 2026-08-26 10:56 UTC (permalink / raw)
To: Lakshay Piplani
Cc: Alexandre Belloni, robh, linux-i3c, conor+dt, Frank.Li,
devicetree
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] MFD Architecture Violation: Global header placement and bespoke intermediate parent structure.
--
commit f74680421e4c370330ed051f68115c488cc48933
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
Add core MFD support for the NXP P3H2x4x (P3H2440/P3H2441/P3H2840/P3H2841)
family of multiport I3C hub devices. These devices connect to a host via
I3C/I2C/SMBus and expose multiple downstream target ports.
> diff --git a/drivers/mfd/p3h2840.c b/drivers/mfd/p3h2840.c
> index 0000000000000..f8cef54ce7062
> --- /dev/null
> +++ b/drivers/mfd/p3h2840.c
> @@ -0,0 +1,168 @@
[ ... ]
> +/* Read port count from the device capability register (4- or 8-port variant). */
> +static int p3h2x4x_read_num_target_ports(struct device *dev,
> + struct p3h2x4x *ddata)
> +{
> + unsigned int val;
> + int ret;
> +
> + ret = regmap_read(ddata->regmap, P3H2X4X_DEV_CAPAB, &val);
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "Failed to read device capability\n");
> +
> + ddata->num_target_ports = (val & P3H2X4X_TARGET_PORT_COUNT) ?
> + P3H2X4X_TARGET_PORTS_8 : P3H2X4X_TARGET_PORTS_4;
[Severity: Low]
Does p3h2x4x_read_num_target_ports() violate the MFD subsystem guidelines by
initializing child-specific resources in the parent driver? Private resources
should be initialized directly in the child driver that consumes them.
> +
> + return 0;
> +}
[ ... ]
> diff --git a/include/linux/mfd/p3h2840.h b/include/linux/mfd/p3h2840.h
> new file mode 100644
> index 0000000000000..eb4792de5440e
> --- /dev/null
> +++ b/include/linux/mfd/p3h2840.h
> @@ -0,0 +1,41 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/*
> + * Copyright 2025-2026 NXP
> + */
> +
> +#ifndef _LINUX_MFD_P3H2840_H
> +#define _LINUX_MFD_P3H2840_H
[Severity: Low]
Is this driver-specific header file inappropriately placed in the global
MFD include path? Driver-specific headers only used by the parent and its
immediate children should remain local to the drivers/mfd/ directory.
> +
> +#include <linux/bits.h>
> +#include <linux/mutex.h>
> +#include <linux/types.h>
[ ... ]
> +struct p3h2x4x_i3c_hub_dev;
> +
> +struct p3h2x4x {
> + struct i3c_device *i3cdev;
> + struct regmap *regmap;
> + /* Number of downstream target ports (4 or 8). */
> + u8 num_target_ports;
> + /* Serializes protected register unlock/lock sequences across MFD children. */
> + struct mutex protected_reg_lock;
> + /* Hub context for the IBI handler to reach hub state via the parent i3cdev. */
> + struct p3h2x4x_i3c_hub_dev *i3c_hub_priv;
> +};
[Severity: Low]
Does struct p3h2x4x aggregate child-specific data and create a bespoke
intermediate parent structure? This creates bidirectional coupling and bypasses
MFD isolation boundaries. Child drivers should directly access parent data using
standard APIs rather than relying on custom parent-initialized private
resources.
> +#endif /* _LINUX_MFD_P3H2840_H */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826103819.1614843-1-lakshay.piplani@nxp.com?part=4
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
2026-08-26 10:38 ` [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub Lakshay Piplani
@ 2026-08-26 11:04 ` sashiko-bot
2026-08-26 17:02 ` Frank Li
2026-08-27 10:09 ` Krzysztof Kozlowski
1 sibling, 1 reply; 22+ messages in thread
From: sashiko-bot @ 2026-08-26 11:04 UTC (permalink / raw)
To: Lakshay Piplani
Cc: Frank.Li, devicetree, conor+dt, robh, linux-i3c,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
- [Critical] Freeing `i3c_hub_dev_data` while it may still be referenced by the parent controller's asynchronous IBI path leads to a Use-After-Free.
- [High] Dynamic downstream devices discovered via DAA are erroneously registered to the parent controller's bus, making them completely unreachable for subsequent transfers on port-switching hubs.
- [High] Reserved address slots on the parent bus are permanently leaked when the hub driver is removed.
- [Medium] Forwarding broadcast CCCs to the parent controller breaks virtual bus isolation by unintentionally affecting the entire parent bus.
--
commit 94c62ce56dcc9d461749adc3db10a8061865dc9b
Author: Lakshay Piplani <lakshay.piplani@nxp.com>
i3c: hub: Add support for the I3C interface in the I3C hub
Add virtual I3C bus support for the hub and provide interface to enable
or disable downstream ports.
> diff --git a/drivers/i3c/hub.c b/drivers/i3c/hub.c
> new file mode 100644
> index 0000000000000..6f5227215ae22
> --- /dev/null
> +++ b/drivers/i3c/hub.c
> @@ -0,0 +1,798 @@
[ ... ]
> +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);
[Severity: Critical]
Does this code introduce a use-after-free when IBI is still active?
If data->parent_desc.ibi is not NULL, as explicitly checked by the
WARN_ON_ONCE, the parent controller's asynchronous IBI path may still
have a work item queued referencing data->parent_desc. By unconditionally
calling kfree(data), we allow the pending async work to dereference
freed memory when it executes.
> +}
> +
> +/**
> + * 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);
[Severity: High]
Does delegating DAA to the physical parent controller break reachability
for downstream devices?
When devices respond to ENTDAA, they are instantiated on the parent bus
rather than the virtual bus. If drivers subsequently initiate transfers, they
use the parent controller's transfer operations directly, bypassing the
hub's i3c_hub_master_priv_xfers(). As a result, the hub target port is
never enabled during the transfer, leaving these downstream devices
permanently unreachable.
> + i3c_hub_disable_port(controller);
> + mutex_unlock(&hub->lock);
> +
> + return ret;
> +}
[ ... ]
> +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);
[Severity: Medium]
Can forwarding unfiltered broadcast CCCs to the parent controller break
virtual bus isolation?
If a driver on a specific virtual bus issues broadcast commands (such as
SETMRL, SETMWL, or ENEC), this forwards them to the parent controller which
broadcasts them globally. This will unintentionally alter the configuration
or event state of all peer devices on the parent bus and other open target
ports.
> + i3c_hub_disable_port(controller);
> + mutex_unlock(&hub->lock);
> +
> + return ret;
> +}
[ ... ]
> +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);
[Severity: High]
Does this code permanently leak parent bus address slots when the hub driver
is removed?
The address slots in the parent bus state are set to I3C_ADDR_SLOT_I3C_DEV.
However, there appears to be no corresponding teardown mechanism provided
to revert these slots to I3C_ADDR_SLOT_FREE when the virtual controllers
are destroyed or the hub driver is unbound. This leak can eventually exhaust
the address space and cause failures for subsequent devices.
> + i3c_bus_maintenance_unlock(&parent->bus);
> + }
> + return 0;
> +}
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260826103819.1614843-1-lakshay.piplani@nxp.com?part=6
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support
2026-08-26 10:38 ` [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support Lakshay Piplani
2026-08-26 10:52 ` sashiko-bot
@ 2026-08-26 16:36 ` Frank Li
1 sibling, 0 replies; 22+ messages in thread
From: Frank Li @ 2026-08-26 16:36 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood,
vikash.bansal, priyanka.jain, aman.kumarpandey
On Wed, Aug 26, 2026 at 04:08:12PM +0530, Lakshay Piplani wrote:
> From: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
>
> Add CCC helpers to check CCC support and send CCC commands, address slot
> helpers to query and update I3C bus address slot state, registering virtual
> masters with an explicit firmware node, and exposing the bus maintenance
> lock helpers.
>
> These additions prepare for I3C hub support. A hub driver needs to reserve
> and query parent bus address slots, forward CCC commands, register virtual
> target port controllers using the target-port firmware node, and serialize
> operations against the parent bus maintenance lock.
>
> The hub also forwards private transfers via i3c_dev_do_xfers_locked() and
> serializes its IBI and private-transfer paths against the shared lock, so
> the normal-use lock/unlock pair is exposed alongside the maintenance-lock
> helpers.
>
> i3c_master_register_fwnode() allows virtual I3C masters to register using a
> firmware node different from their parent device node without temporarily
> modifying parent->of_node.
>
> The new helpers are:
> 1) i3c_master_send_ccc_cmd()
> 2) i3c_master_supports_ccc_cmd()
> 3) i3c_bus_get_addr_slot_status()
> 4) i3c_bus_set_addr_slot_status()
> 5) i3c_bus_maintenance_lock()
> 6) i3c_bus_maintenance_unlock()
> 7) i3c_master_register_fwnode()
> 8) i3c_bus_normaluse_lock()
> 9) i3c_bus_normaluse_unlock()
> 10) i3c_dev_do_xfers_locked()
>
> Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
> Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
> Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Reviewed-by: Frank Li <Frank.Li@nxp.com>
>
> ---
> Changes in v16:
> - Rewrite the commit message to match the code, It now describes only
> the helpers actually exported
>
> Changes in v15:
> - Drop the direct attach and detach helpers that also modified address-slot
> state
> - Export these APIs:
> - i3c_bus_normaluse_lock()
> - i3c_bus_normaluse_unlock()
> - i3c_dev_do_xfers_locked()
>
> Changes in v14:
> - Add i3c_master_register_fwnode() to register virtual I3C masters with an
> explicit firmware node
> - Export i3c_bus_maintenance_lock() and i3c_bus_maintenance_unlock()
> - Add runtime PM get/put around i3c_master_send_ccc_cmd()
> - Make i3c_master_supports_ccc_cmd() return false when the controller does
> not implement send_ccc_cmd()
>
> Changes in v13:
> - Fix address handling in direct attach by using i3c_master_get_i3c_addrs() and
> adding rollback on failure to prevent bus address collisions
> - Fix detach path by clearing master_priv and releasing addresses to avoid use-after-free
> and stale state issues
> - Export address slot helper APIs and add kernel-doc for them
>
> Changes in v12:
> - Add address check in i3c_master_direct_detach_i3c_dev_locked() to skip
> detach for unaddressed devices.
>
> Changes in v11:
> - Convert i3c_master_supports_ccc_cmd() to return bool and align
> semantics with CCC support checks used by the I3C core
>
> Changes in v10:
> - Rename i3c_master_direct_attach_i3c_dev and i3c_master_direct_detach_i3c_dev
> APIs to *_locked, as these APIs must be called with the bus lock held in
> write mode
>
> Changes in v9:
> - No change
>
> Changes in v8:
> - No change
>
> Changes in v7:
> - Update commit message to clarify purpose (prepare for I3C hub support)
>
> Changes in v6:
> - Split the patch into two parts:
> 1) expose the existing API
> 2) add new APIs.
> ---
> ---
> drivers/i3c/master.c | 155 +++++++++++++++++++++++++++++++------
> include/linux/i3c/master.h | 17 ++++
> 2 files changed, 149 insertions(+), 23 deletions(-)
>
> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
> index f1be38a640ca..03fb41f0786c 100644
> --- a/drivers/i3c/master.c
> +++ b/drivers/i3c/master.c
> @@ -44,10 +44,11 @@ static BLOCKING_NOTIFIER_HEAD(i3c_bus_notifier);
> * logic to rely on I3C device information that could be changed behind their
> * back.
> */
> -static void i3c_bus_maintenance_lock(struct i3c_bus *bus)
> +void i3c_bus_maintenance_lock(struct i3c_bus *bus)
> {
> down_write(&bus->lock);
> }
> +EXPORT_SYMBOL_GPL(i3c_bus_maintenance_lock);
>
> /**
> * i3c_bus_maintenance_unlock - Release the bus lock after a maintenance
> @@ -58,10 +59,11 @@ static void i3c_bus_maintenance_lock(struct i3c_bus *bus)
> * i3c_bus_maintenance_lock() for more details on what these maintenance
> * operations are.
> */
> -static void i3c_bus_maintenance_unlock(struct i3c_bus *bus)
> +void i3c_bus_maintenance_unlock(struct i3c_bus *bus)
> {
> up_write(&bus->lock);
> }
> +EXPORT_SYMBOL_GPL(i3c_bus_maintenance_unlock);
>
> /**
> * i3c_bus_normaluse_lock - Lock the bus for a normal operation
> @@ -83,6 +85,7 @@ void i3c_bus_normaluse_lock(struct i3c_bus *bus)
> {
> down_read(&bus->lock);
> }
> +EXPORT_SYMBOL_GPL(i3c_bus_normaluse_lock);
>
> /**
> * i3c_bus_normaluse_unlock - Release the bus lock after a normal operation
> @@ -96,6 +99,7 @@ void i3c_bus_normaluse_unlock(struct i3c_bus *bus)
> {
> up_read(&bus->lock);
> }
> +EXPORT_SYMBOL_GPL(i3c_bus_normaluse_unlock);
>
> static struct i3c_master_controller *
> i3c_bus_to_i3c_master(struct i3c_bus *i3cbus)
> @@ -385,11 +389,19 @@ i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask)
> return status & mask;
> }
>
> -static enum i3c_addr_slot_status
> +/**
> + * i3c_bus_get_addr_slot_status() - Get I3C bus address slot status
> + * @bus: I3C bus.
> + * @addr: I3C address to query.
> + *
> + * Return: Address slot status for @addr.
> + */
> +enum i3c_addr_slot_status
> i3c_bus_get_addr_slot_status(struct i3c_bus *bus, u16 addr)
> {
> return i3c_bus_get_addr_slot_status_mask(bus, addr, I3C_ADDR_SLOT_STATUS_MASK);
> }
> +EXPORT_SYMBOL_GPL(i3c_bus_get_addr_slot_status);
>
> static void i3c_bus_set_addr_slot_status_mask(struct i3c_bus *bus, u16 addr,
> enum i3c_addr_slot_status status, u32 mask)
> @@ -405,11 +417,18 @@ static void i3c_bus_set_addr_slot_status_mask(struct i3c_bus *bus, u16 addr,
> *ptr |= ((unsigned long)status & mask) << (bitpos % BITS_PER_LONG);
> }
>
> -static void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr,
> - enum i3c_addr_slot_status status)
> +/**
> + * i3c_bus_set_addr_slot_status() - Set I3C bus address slot status
> + * @bus: I3C bus.
> + * @addr: I3C address to update.
> + * @status: Address slot status to set.
> + */
> +void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr,
> + enum i3c_addr_slot_status status)
> {
> i3c_bus_set_addr_slot_status_mask(bus, addr, status, I3C_ADDR_SLOT_STATUS_MASK);
> }
> +EXPORT_SYMBOL_GPL(i3c_bus_set_addr_slot_status);
>
> static bool i3c_bus_dev_addr_is_avail(struct i3c_bus *bus, u8 addr)
> {
> @@ -2548,6 +2567,59 @@ static void i3c_master_reconcile_dyn_addrs(struct i3c_master_controller *master)
> }
> }
>
> +/**
> + * i3c_master_supports_ccc_cmd() - check CCC command support
> + * @master: I3C master controller
> + * @cmd: CCC command to verify
> + *
> + * Return: true if @cmd is supported, false otherwise.
> + */
> +bool i3c_master_supports_ccc_cmd(struct i3c_master_controller *master,
> + const struct i3c_ccc_cmd *cmd)
> +{
> + if (!master || !cmd)
> + return false;
> +
> + if (!master->ops->send_ccc_cmd)
> + return false;
> +
> + if (!master->ops->supports_ccc_cmd)
> + return true;
> +
> + return master->ops->supports_ccc_cmd(master, cmd);
> +}
> +EXPORT_SYMBOL_GPL(i3c_master_supports_ccc_cmd);
> +
> +/**
> + * i3c_master_send_ccc_cmd() - send a CCC command
> + * @master: I3C master controller issuing the command
> + * @cmd: CCC command to be sent
> + *
> + * This function sends a Common Command Code (CCC) command to devices on the
> + * I3C bus. It acquires the bus maintenance lock, executes the command, and
> + * then releases the lock to ensure safe access to the bus.
> + *
> + * Return: 0 on success, or a negative error code on failure.
> + */
> +int i3c_master_send_ccc_cmd(struct i3c_master_controller *master,
> + struct i3c_ccc_cmd *cmd)
> +{
> + int ret;
> +
> + ret = i3c_master_rpm_get(master);
> + if (ret)
> + return ret;
> +
> + i3c_bus_maintenance_lock(&master->bus);
> + ret = i3c_master_send_ccc_cmd_locked(master, cmd);
> + i3c_bus_maintenance_unlock(&master->bus);
> +
> + i3c_master_rpm_put(master);
> +
> + return ret;
> +}
> +EXPORT_SYMBOL_GPL(i3c_master_send_ccc_cmd);
> +
> /**
> * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version)
> * @master: controller
> @@ -3195,34 +3267,31 @@ static int i3c_master_check_ops(const struct i3c_master_controller_ops *ops)
> }
>
> /**
> - * i3c_master_register() - register an I3C master
> + * i3c_master_register_fwnode() - register an I3C master with a custom fwnode
> * @master: master used to send frames on the bus
> - * @parent: the parent device (the one that provides this I3C master
> - * controller)
> + * @parent: the parent device providing this I3C master controller
> + * @fwnode: firmware node describing this I3C bus, or NULL
> * @ops: the master controller operations
> - * @secondary: true if you are registering a secondary master. Will return
> - * -EOPNOTSUPP if set to true since secondary masters are not yet
> - * supported
> + * @secondary: true if registering a secondary master
> *
> - * This function takes care of everything for you:
> + * This helper is useful for virtual I3C masters whose firmware node is not
> + * the same as @parent's firmware node.
> *
> - * - creates and initializes the I3C bus
> - * - populates the bus with static I2C devs if @parent->of_node is not
> - * NULL
> - * - registers all I3C devices added by the controller during bus
> - * initialization
> - * - registers the I2C adapter and all I2C devices
> + * Only OF-backed fwnodes are supported for now, because the I3C core still
> + * stores the bus node in master->dev.of_node and populates the bus using OF.
> *
> * Return: 0 in case of success, a negative error code otherwise.
> */
> -int i3c_master_register(struct i3c_master_controller *master,
> - struct device *parent,
> - const struct i3c_master_controller_ops *ops,
> - bool secondary)
> +int i3c_master_register_fwnode(struct i3c_master_controller *master,
> + struct device *parent,
> + struct fwnode_handle *fwnode,
> + const struct i3c_master_controller_ops *ops,
> + bool secondary)
> {
> unsigned long i2c_scl_rate = I3C_BUS_I2C_FM_PLUS_SCL_MAX_RATE;
> struct i3c_bus *i3cbus = i3c_master_get_bus(master);
> enum i3c_bus_mode mode = I3C_BUS_MODE_PURE;
> + struct device_node *np = NULL;
> struct i2c_dev_boardinfo *i2cbi;
> int ret;
>
> @@ -3234,8 +3303,14 @@ int i3c_master_register(struct i3c_master_controller *master,
> if (ret)
> return ret;
>
> + if (fwnode) {
> + np = to_of_node(fwnode);
> + if (!np)
> + return -EINVAL;
> + }
> +
> master->dev.parent = parent;
> - master->dev.of_node = of_node_get(parent->of_node);
> + master->dev.of_node = of_node_get(np);
> master->dev.bus = &i3c_bus_type;
> master->dev.type = &i3c_masterdev_type;
> master->dev.release = i3c_masterdev_release;
> @@ -3352,6 +3427,39 @@ int i3c_master_register(struct i3c_master_controller *master,
>
> return ret;
> }
> +EXPORT_SYMBOL_GPL(i3c_master_register_fwnode);
> +
> +/**
> + * i3c_master_register() - register an I3C master
> + * @master: master used to send frames on the bus
> + * @parent: the parent device (the one that provides this I3C master
> + * controller)
> + * @ops: the master controller operations
> + * @secondary: true if you are registering a secondary master. Will return
> + * -EOPNOTSUPP if set to true since secondary masters are not yet
> + * supported
> + *
> + * This function takes care of everything for you:
> + *
> + * - creates and initializes the I3C bus
> + * - populates the bus with static I2C devs if @parent->of_node is not
> + * NULL
> + * - registers all I3C devices added by the controller during bus
> + * initialization
> + * - registers the I2C adapter and all I2C devices
> + *
> + * Return: 0 in case of success, a negative error code otherwise.
> + */
> +int i3c_master_register(struct i3c_master_controller *master,
> + struct device *parent,
> + const struct i3c_master_controller_ops *ops,
> + bool secondary)
> +{
> + return i3c_master_register_fwnode(master, parent,
> + parent->of_node ?
> + of_fwnode_handle(parent->of_node) : NULL,
> + ops, secondary);
> +}
> EXPORT_SYMBOL_GPL(i3c_master_register);
>
> /**
> @@ -3412,6 +3520,7 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers,
>
> return master->ops->i3c_xfers(dev, xfers, nxfers, mode);
> }
> +EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked);
>
> /**
> * i3c_dev_disable_ibi_locked() - Disable IBIs coming from a specific device
> diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h
> index 4d2a68793324..32c686ae5d62 100644
> --- a/include/linux/i3c/master.h
> +++ b/include/linux/i3c/master.h
> @@ -627,9 +627,18 @@ DEFINE_FREE(i3c_master_dma_unmap_single, void *,
>
> int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev,
> u8 old_dyn_addr);
> +int i3c_master_send_ccc_cmd(struct i3c_master_controller *master,
> + struct i3c_ccc_cmd *cmd);
> +bool i3c_master_supports_ccc_cmd(struct i3c_master_controller *master,
> + const struct i3c_ccc_cmd *cmd);
> int i3c_master_set_info(struct i3c_master_controller *master,
> const struct i3c_device_info *info);
>
> +int i3c_master_register_fwnode(struct i3c_master_controller *master,
> + struct device *parent,
> + struct fwnode_handle *fwnode,
> + const struct i3c_master_controller_ops *ops,
> + bool secondary);
> int i3c_master_register(struct i3c_master_controller *master,
> struct device *parent,
> const struct i3c_master_controller_ops *ops,
> @@ -752,4 +761,12 @@ void i3c_for_each_bus_locked(int (*fn)(struct i3c_bus *bus, void *data),
> int i3c_register_notifier(struct notifier_block *nb);
> int i3c_unregister_notifier(struct notifier_block *nb);
>
> +enum i3c_addr_slot_status
> +i3c_bus_get_addr_slot_status(struct i3c_bus *bus, u16 addr);
> +
> +void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr,
> + enum i3c_addr_slot_status status);
> +
> +void i3c_bus_maintenance_lock(struct i3c_bus *bus);
> +void i3c_bus_maintenance_unlock(struct i3c_bus *bus);
> #endif /* I3C_MASTER_H */
> --
> 2.25.1
>
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers
2026-08-26 10:38 ` [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers Lakshay Piplani
2026-08-26 10:50 ` sashiko-bot
@ 2026-08-26 16:42 ` Frank Li
2026-08-27 7:02 ` Lakshay Piplani
1 sibling, 1 reply; 22+ messages in thread
From: Frank Li @ 2026-08-26 16:42 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, lee, Frank.Li, lgirdwood,
vikash.bansal, priyanka.jain, aman.kumarpandey
On Wed, Aug 26, 2026 at 04:08:13PM +0530, Lakshay Piplani wrote:
> The generic I3C device attach and IBI paths perform both core-level
> bookkeeping and controller-specific operations. An I3C hub, however,
> maintains a logical device descriptor on the virtual downstream bus and
> a separate parent-facing descriptor used by the physical parent
> controller.
>
> Add controller-only helpers that invoke the controller callbacks
> without updating the bus device list, address-slot state, or generic IBI
> lifecycle. This allows the hub framework to keep the logical descriptor
> associated with the virtual controller while using a permanent
> parent-facing descriptor for operations handled by the physical
> controller.
>
> Add helpers for:
> - attaching, reattaching, and detaching a device from a controller;
> - requesting and freeing controller IBI resources;
> - enabling and disabling IBIs at the controller;
> - recycling controller IBI slots.
>
> Refactor the existing generic core paths to use these helpers, keeping
> the current behaviour unchanged for regular I3C devices.
>
> Also release the generic IBI workqueue and state when the controller
> request callback fails.
>
> The helpers are declared in the I3C internal header because they are
> intended for use by the generic I3C hub framework rather than by
> individual controller drivers.
>
> Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
> Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
> Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
>
> ---
...
>
> +/**
> + * i3c_dev_recycle_ibi_slot_controller() - Recycle an IBI slot through
> + * the current controller
> + * @dev: I3C device descriptor
> + * @slot: IBI slot to recycle
> + *
> + * Invoke the current controller's IBI slot recycling callback.
> + *
> + * The controller is responsible for synchronizing access to its IBI pool.
> + */
> +void i3c_dev_recycle_ibi_slot_controller(struct i3c_dev_desc *dev,
> + struct i3c_ibi_slot *slot)
does this one need hold lock?
Frank
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
2026-08-26 11:04 ` sashiko-bot
@ 2026-08-26 17:02 ` Frank Li
2026-08-27 7:07 ` Lakshay Piplani
0 siblings, 1 reply; 22+ messages in thread
From: Frank Li @ 2026-08-26 17:02 UTC (permalink / raw)
To: sashiko-reviews
Cc: Lakshay Piplani, Frank.Li, devicetree, conor+dt, robh, linux-i3c,
Alexandre Belloni
On Wed, Aug 26, 2026 at 11:04:07AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider:
> - [Critical] Freeing `i3c_hub_dev_data` while it may still be referenced by the parent controller's asynchronous IBI path leads to a Use-After-Free.
> - [High] Dynamic downstream devices discovered via DAA are erroneously registered to the parent controller's bus, making them completely unreachable for subsequent transfers on port-switching hubs.
> - [High] Reserved address slots on the parent bus are permanently leaked when the hub driver is removed.
> - [Medium] Forwarding broadcast CCCs to the parent controller breaks virtual bus isolation by unintentionally affecting the entire parent bus.
> --
>
> commit 94c62ce56dcc9d461749adc3db10a8061865dc9b
> Author: Lakshay Piplani <lakshay.piplani@nxp.com>
>
> i3c: hub: Add support for the I3C interface in the I3C hub
>
> Add virtual I3C bus support for the hub and provide interface to enable
> or disable downstream ports.
>
> > diff --git a/drivers/i3c/hub.c b/drivers/i3c/hub.c
> > new file mode 100644
> > index 0000000000000..6f5227215ae22
> > --- /dev/null
> > +++ b/drivers/i3c/hub.c
> > @@ -0,0 +1,798 @@
> [ ... ]
> > +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);
>
> [Severity: Critical]
> Does this code introduce a use-after-free when IBI is still active?
>
> If data->parent_desc.ibi is not NULL, as explicitly checked by the
> WARN_ON_ONCE, the parent controller's asynchronous IBI path may still
> have a work item queued referencing data->parent_desc. By unconditionally
> calling kfree(data), we allow the pending async work to dereference
> freed memory when it executes.
>
> > +}
> > +
> > +/**
> > + * 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);
>
> [Severity: High]
> Does delegating DAA to the physical parent controller break reachability
> for downstream devices?
>
> When devices respond to ENTDAA, they are instantiated on the parent bus
> rather than the virtual bus. If drivers subsequently initiate transfers, they
> use the parent controller's transfer operations directly, bypassing the
> hub's i3c_hub_master_priv_xfers(). As a result, the hub target port is
> never enabled during the transfer, leaving these downstream devices
> permanently unreachable.
where reorder port, is it true?
>
> > + i3c_hub_disable_port(controller);
> > + mutex_unlock(&hub->lock);
> > +
> > + return ret;
> > +}
> [ ... ]
> > +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);
>
> [Severity: Medium]
> Can forwarding unfiltered broadcast CCCs to the parent controller break
> virtual bus isolation?
>
> If a driver on a specific virtual bus issues broadcast commands (such as
> SETMRL, SETMWL, or ENEC), this forwards them to the parent controller which
> broadcasts them globally. This will unintentionally alter the configuration
> or event state of all peer devices on the parent bus and other open target
> ports.
Look like system design limiation, add comments here incase it happen.
>
> > + i3c_hub_disable_port(controller);
> > + mutex_unlock(&hub->lock);
> > +
> > + return ret;
> > +}
> [ ... ]
> > +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);
>
> [Severity: High]
> Does this code permanently leak parent bus address slots when the hub driver
> is removed?
>
> The address slots in the parent bus state are set to I3C_ADDR_SLOT_I3C_DEV.
> However, there appears to be no corresponding teardown mechanism provided
> to revert these slots to I3C_ADDR_SLOT_FREE when the virtual controllers
> are destroyed or the hub driver is unbound. This leak can eventually exhaust
> the address space and cause failures for subsequent devices.
I suppose if hub have hotplug later, we have to reserver all static/assigned
address. Can you add comments here.
Thank you for great work, fixed the most problem.
Frank
>
> > + i3c_bus_maintenance_unlock(&parent->bus);
> > + }
> > + return 0;
> > +}
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260826103819.1614843-1-lakshay.piplani@nxp.com?part=6
^ permalink raw reply [flat|nested] 22+ messages in thread
* RE: [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers
2026-08-26 16:42 ` Frank Li
@ 2026-08-27 7:02 ` Lakshay Piplani
0 siblings, 0 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-27 7:02 UTC (permalink / raw)
To: Frank Li (OSS)
Cc: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org,
alexandre.belloni@bootlin.com, krzk+dt@kernel.org,
robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org,
broonie@kernel.org, lee@kernel.org, Frank Li, lgirdwood@gmail.com,
Vikash Bansal, Priyanka Jain, Aman Kumar Pandey
Hi Frank,
Thanks for the review.
> > +/**
> > + * i3c_dev_recycle_ibi_slot_controller() - Recycle an IBI slot through
> > + * the current controller
> > + * @dev: I3C device descriptor
> > + * @slot: IBI slot to recycle
> > + *
> > + * Invoke the current controller's IBI slot recycling callback.
> > + *
> > + * The controller is responsible for synchronizing access to its IBI pool.
> > + */
> > +void i3c_dev_recycle_ibi_slot_controller(struct i3c_dev_desc *dev,
> > + struct i3c_ibi_slot *slot)
>
> does this one need hold lock?
>
> Frank
No. It's called from i3c_master_handle_ibi() in workqueue context and just forwards to ops->recycle_ibi_slot(); the controller synchronizes its own pool (generic pool uses its own spinlock). I'll document the calling context.
Thanks
Lakshay
NXP Confidential
^ permalink raw reply [flat|nested] 22+ messages in thread
* RE: [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
2026-08-26 17:02 ` Frank Li
@ 2026-08-27 7:07 ` Lakshay Piplani
0 siblings, 0 replies; 22+ messages in thread
From: Lakshay Piplani @ 2026-08-27 7:07 UTC (permalink / raw)
To: Frank Li (OSS), sashiko-reviews@lists.linux.dev
Cc: Frank.Li@kernel.org, devicetree@vger.kernel.org,
conor+dt@kernel.org, robh@kernel.org,
linux-i3c@lists.infradead.org, Alexandre Belloni
Hi Frank,
Thanks for the review.
> > > +/**
> > > + * 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);
> >
> > [Severity: High]
> > Does delegating DAA to the physical parent controller break
> > reachability for downstream devices?
> >
> > When devices respond to ENTDAA, they are instantiated on the parent
> > bus rather than the virtual bus. If drivers subsequently initiate
> > transfers, they use the parent controller's transfer operations
> > directly, bypassing the hub's i3c_hub_master_priv_xfers(). As a
> > result, the hub target port is never enabled during the transfer,
> > leaving these downstream devices permanently unreachable.
>
> where reorder port, is it true?
>
No, P3H2x4x never re-orders or re-switches ports. All I3C ports are connected once at probe and stay connected (always_enable makes enable/disable_port no-ops), so a prior DAA never goes stale.
DAA runs on the parent on purpose: all downstream devices share the parent's single address domain, so addresses stay unique across ports.
DT addresses are reserved for the parent-bus lifetime and aren't freed on a port toggle, so enable/disable only gates reachability, never reallocates. I'll add a comment.
(enable/disable_port are kept as part of the generic hub op set and would be the hook for optional sysfs port control later - out of scope here.)
> >
> > > + i3c_hub_disable_port(controller);
> > > + mutex_unlock(&hub->lock);
> > > +
> > > + return ret;
> > > +}
> > [ ... ]
> > > +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);
> >
> > [Severity: Medium]
> > Can forwarding unfiltered broadcast CCCs to the parent controller
> > break virtual bus isolation?
> >
> > If a driver on a specific virtual bus issues broadcast commands (such
> > as SETMRL, SETMWL, or ENEC), this forwards them to the parent
> > controller which broadcasts them globally. This will unintentionally
> > alter the configuration or event state of all peer devices on the
> > parent bus and other open target ports.
>
> Look like system design limiation, add comments here incase it happen.
>
Agreed - it's a system limitation, I'll note that a broadcast CCC on a virtual bus has parent-bus scope. RSTDAA stays blocked, as forwarding it would reset the hub's own address.
> >
> > > + i3c_hub_disable_port(controller);
> > > + mutex_unlock(&hub->lock);
> > > +
> > > + return ret;
> > > +}
> > [ ... ]
> > > +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);
> >
> > [Severity: High]
> > Does this code permanently leak parent bus address slots when the hub
> > driver is removed?
> >
> > The address slots in the parent bus state are set to I3C_ADDR_SLOT_I3C_DEV.
> > However, there appears to be no corresponding teardown mechanism
> > provided to revert these slots to I3C_ADDR_SLOT_FREE when the virtual
> > controllers are destroyed or the hub driver is unbound. This leak can
> > eventually exhaust the address space and cause failures for subsequent
> devices.
>
> I suppose if hub have hotplug later, we have to reserver all static/assigned
> address. Can you add comments here.
Agreed - I'll add a comment. The reserved addresses come from the fixed DT topology and are held for the parent-bus lifetime. If the hub is removed and comes back, it re-reads the same DT and reserves the same addresses again, so nothing is used up over time.
>
> Thank you for great work, fixed the most problem.
>
> Frank
Thanks, Frank. I appreciate your reviews and feedback throughout this series.
Regards
Lakshay
NXP Confidential
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
2026-08-26 10:38 ` [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator Lakshay Piplani
2026-08-26 10:56 ` sashiko-bot
@ 2026-08-27 10:03 ` Krzysztof Kozlowski
1 sibling, 0 replies; 22+ messages in thread
From: Krzysztof Kozlowski @ 2026-08-27 10:03 UTC (permalink / raw)
To: Lakshay Piplani, linux-kernel, linux-i3c, alexandre.belloni,
krzk+dt, robh, conor+dt, devicetree, broonie, lee, Frank.Li,
lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey
On 26/08/2026 12:38, Lakshay Piplani wrote:
> +
> +static const struct i3c_device_id p3h2x4x_i3c_ids[] = {
> + I3C_CLASS(I3C_DCR_HUB, NULL),
> + { /* sentinel */ },
> +};
> +MODULE_DEVICE_TABLE(i3c, p3h2x4x_i3c_ids);
> +
> +static const struct i2c_device_id p3h2x4x_i2c_id_table[] = {
> + { .name = "nxp-i3c-hub" },
> + { /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(i2c, p3h2x4x_i2c_id_table);
> +
> +static const struct of_device_id p3h2x4x_i2c_of_match[] = {
> + { .compatible = "nxp,p3h2440", },
> + { .compatible = "nxp,p3h2441", },
> + { .compatible = "nxp,p3h2840", },
> + { .compatible = "nxp,p3h2841", },
So devices are fully compatible? Why isn't this expressed in the
binding? Or explained in the commit msg?
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
2026-08-26 10:38 ` [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub Lakshay Piplani
2026-08-26 11:04 ` sashiko-bot
@ 2026-08-27 10:09 ` Krzysztof Kozlowski
1 sibling, 0 replies; 22+ messages in thread
From: Krzysztof Kozlowski @ 2026-08-27 10:09 UTC (permalink / raw)
To: Lakshay Piplani, linux-kernel, linux-i3c, alexandre.belloni,
krzk+dt, robh, conor+dt, devicetree, broonie, lee, Frank.Li,
lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey
On 26/08/2026 12:38, Lakshay Piplani wrote:
> +/**
> + * 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))
So without lockdep you do not initialize? Honestly, kerneldoc is useless
here and explains nothing. Kerneldoc is pretty clear - you initialize
hub context thus why hub context should be uninitialized without lockdep?
> + return;
> +
> + if (WARN_ON_ONCE(!hub_dev || !hub_dev->desc))
> + return;
How is this possible? Why panicking the machine?
> +
> + parent = i3c_dev_get_master(hub_dev->desc);
> + if (WARN_ON_ONCE(!parent))
> + return;
Why panicking here (WARN on panic)?
> +
> + /*
> + * 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);
Do you really need a wrapper call over single variable? Can the i3c hub
core code be NOT present when your I3C hub driver is buillin?
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH v16 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
2026-08-26 10:38 ` [PATCH v16 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality Lakshay Piplani
@ 2026-08-27 10:16 ` Krzysztof Kozlowski
0 siblings, 0 replies; 22+ messages in thread
From: Krzysztof Kozlowski @ 2026-08-27 10:16 UTC (permalink / raw)
To: Lakshay Piplani, linux-kernel, linux-i3c, alexandre.belloni,
krzk+dt, robh, conor+dt, devicetree, broonie, lee, Frank.Li,
lgirdwood
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey
On 26/08/2026 12:38, Lakshay Piplani wrote:
> +
> +static int p3h2x4x_configure_ldo(struct device *dev)
> +{
> + static const char * const supplies[] = {
> + "vcc1",
> + "vcc2",
> + "vcc3",
> + "vcc4"
> + };
> + int ret, i;
> +
> + for (i = 0; i < ARRAY_SIZE(supplies); i++) {
> + ret = devm_regulator_get_enable_optional(dev, supplies[i]);
> + if (ret && ret != -ENODEV)
> + return dev_err_probe(dev, ret, "Failed to enable %s\n",
> + supplies[i]);
> + }
> +
> + /* This delay is required for the regulator to stabilize its output voltage */
> + fsleep(5000);
Instead your regulators miss ramp delays.
> +
> + return 0;
> +}
...
> +
> +static void p3h2x4x_get_target_port_dt_conf(struct device *dev,
> + const struct device_node *node)
> +{
> + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
> + struct p3h2x4x *p3h2x4x = dev_get_drvdata(dev->parent);
> + u64 tp_port;
> +
> + for_each_available_child_of_node_scoped(node, dev_node) {
Why do you need scoped loop?
> + if (of_property_read_reg(dev_node, 0, &tp_port, NULL))
> + continue;
> +
> + if (tp_port < p3h2x4x->num_target_ports) {
> + if (p3h2x4x_i3c_hub->tp_bus[tp_port].of_node) {
> + dev_warn(dev, "Duplicate target port %llu in DT\n", tp_port);
> + continue;
> + }
> +
> + p3h2x4x_i3c_hub->tp_bus[tp_port].of_node = of_node_get(dev_node);
> + p3h2x4x_i3c_hub->tp_bus[tp_port].tp_mask = P3H2X4X_SET_BIT(tp_port);
> + p3h2x4x_i3c_hub->tp_bus[tp_port].p3h2x4x_i3c_hub = p3h2x4x_i3c_hub;
> + p3h2x4x_i3c_hub->tp_bus[tp_port].tp_port = tp_port;
> + }
> + }
> +}
> +
> +static int p3h2x4x_parse_tp_dt_settings(struct device *dev,
> + const struct device_node *node,
> + struct tp_configuration tp_config[])
> +{
> + struct p3h2x4x *p3h2x4x = dev_get_drvdata(dev->parent);
> + u64 id;
> + int ret;
> +
> + for_each_available_child_of_node_scoped(node, tp_node) {
> + enum p3h2x4x_tp_mode mode;
> +
> + /*
> + * Only "i3c" and "smbus" children describe target ports. Skip any
> + * other child (for example the MFD "regulators" container), which
> + * has no "reg" property.
> + */
> + if (of_node_name_eq(tp_node, "i3c"))
> + mode = P3H2X4X_TP_MODE_I3C;
> + else if (of_node_name_eq(tp_node, "smbus"))
> + mode = P3H2X4X_TP_MODE_SMBUS;
> + else
> + continue;
> +
> + ret = of_property_read_reg(tp_node, 0, &id, NULL);
> + if (ret)
> + return dev_err_probe(dev, ret,
> + "Failed to read reg for %pOF\n",
> + tp_node);
> +
> + if (id >= p3h2x4x->num_target_ports)
> + return dev_err_probe(dev, -EINVAL,
> + "Invalid target port index %llu\n",
> + id);
> +
> + tp_config[id].mode = mode;
> + tp_config[id].pullup_en =
> + of_property_read_bool(tp_node, "nxp,pullup-enable");
> + }
> +
> + return 0;
> +}
> +
> +static int p3h2x4x_get_hub_dt_conf(struct device *dev,
> + const struct device_node *node)
> +{
> + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
> +
> + of_property_read_u32(node, "nxp,tp0145-pullup-ohms",
> + &p3h2x4x_i3c_hub->hub_config.tp0145_pullup);
> + of_property_read_u32(node, "nxp,tp2367-pullup-ohms",
> + &p3h2x4x_i3c_hub->hub_config.tp2367_pullup);
> + of_property_read_u32(node, "nxp,cp0-io-strength-ohms",
> + &p3h2x4x_i3c_hub->hub_config.cp0_io_strength);
> + of_property_read_u32(node, "nxp,cp1-io-strength-ohms",
> + &p3h2x4x_i3c_hub->hub_config.cp1_io_strength);
> + of_property_read_u32(node, "nxp,tp0145-io-strength-ohms",
> + &p3h2x4x_i3c_hub->hub_config.tp0145_io_strength);
> + of_property_read_u32(node, "nxp,tp2367-io-strength-ohms",
> + &p3h2x4x_i3c_hub->hub_config.tp2367_io_strength);
> +
> + return p3h2x4x_parse_tp_dt_settings(dev, node,
> + p3h2x4x_i3c_hub->hub_config.tp_config);
> +}
> +
> +static void p3h2x4x_default_configuration(struct device *dev)
> +{
> + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev);
> + int tp_count;
> +
> + p3h2x4x_i3c_hub->hub_config.tp0145_pullup = P3H2X4X_DFT_TP_PULLUP_OHMS;
> + p3h2x4x_i3c_hub->hub_config.tp2367_pullup = P3H2X4X_DFT_TP_PULLUP_OHMS;
> + p3h2x4x_i3c_hub->hub_config.cp0_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
> + p3h2x4x_i3c_hub->hub_config.cp1_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
> + p3h2x4x_i3c_hub->hub_config.tp0145_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
> + p3h2x4x_i3c_hub->hub_config.tp2367_io_strength = P3H2X4X_DFT_IO_STRENGTH_OHMS;
> +
> + for (tp_count = 0; tp_count < P3H2X4X_TP_MAX_COUNT; ++tp_count)
> + p3h2x4x_i3c_hub->hub_config.tp_config[tp_count].mode = P3H2X4X_TP_MODE_I3C;
> +}
> +
> +static void p3h2x4x_unregister_smbus_adapters_action(void *data)
> +{
> + p3h2x4x_unregister_smbus_adapters(data);
> +}
> +
> +static void p3h2x4x_put_target_port_of_nodes(void *data)
> +{
> + struct p3h2x4x_i3c_hub_dev *hub = data;
> + int tp;
> +
> + for (tp = 0; tp < P3H2X4X_TP_MAX_COUNT; tp++) {
> + of_node_put(hub->tp_bus[tp].of_node);
> + hub->tp_bus[tp].of_node = NULL;
> + }
> +}
> +
> +static void p3h2x4x_clear_i3c_hub_priv(void *data)
> +{
> + struct p3h2x4x *p3h2x4x = data;
> +
> + /* Drop the IBI handler backpointer; see the ordering note at the registration site. */
> + p3h2x4x->i3c_hub_priv = NULL;
> +}
> +
> +static int p3h2x4x_i3c_hub_probe(struct platform_device *pdev)
> +{
> + struct p3h2x4x *p3h2x4x = dev_get_drvdata(pdev->dev.parent);
> + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub;
> + struct device *dev = &pdev->dev;
> + struct device_node *node;
> + int ret, i;
> +
> + p3h2x4x_i3c_hub = devm_kzalloc(dev, sizeof(*p3h2x4x_i3c_hub), GFP_KERNEL);
> + if (!p3h2x4x_i3c_hub)
> + return -ENOMEM;
> +
> + p3h2x4x_i3c_hub->regmap = p3h2x4x->regmap;
> + p3h2x4x_i3c_hub->dev = dev;
> +
> + platform_set_drvdata(pdev, p3h2x4x_i3c_hub);
> + device_set_of_node_from_dev(dev, dev->parent);
> +
> + p3h2x4x_default_configuration(dev);
> +
> + ret = devm_mutex_init(dev, &p3h2x4x_i3c_hub->etx_mutex);
> + if (ret)
> + return ret;
> +
> + for (i = 0; i < P3H2X4X_TP_MAX_COUNT; i++) {
> + ret = devm_mutex_init(dev, &p3h2x4x_i3c_hub->tp_bus[i].port_mutex);
> + if (ret)
> + return ret;
> + }
> +
> + /* get hub node from DT */
> + node = dev_of_node(dev);
> + if (!node)
> + return dev_err_probe(dev, -ENODEV, "No Device Tree entry found\n");
> +
> + ret = p3h2x4x_get_hub_dt_conf(dev, node);
> + if (ret)
> + return ret;
> +
> + p3h2x4x_get_target_port_dt_conf(dev, node);
> +
> + ret = devm_add_action_or_reset(dev,
> + p3h2x4x_put_target_port_of_nodes,
> + p3h2x4x_i3c_hub);
> + if (ret)
> + return ret;
> +
> + ret = p3h2x4x_configure_hw(dev);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to configure the HUB\n");
> +
> + /* Register virtual I3C master controllers for I3C target ports */
> + if (p3h2x4x->i3cdev) {
> + p3h2x4x_i3c_hub->i3cdev = p3h2x4x->i3cdev;
> + /*
> + * Publish the hub context in the MFD parent struct rather than
> + * via i3cdev_set_drvdata(), which would overwrite the parent's
> + * drvdata (struct p3h2x4x) that the IBI handler and other MFD
> + * callbacks rely on. Publish it before p3h2x4x_tp_i3c_algo()
> + * enables IBI, since the IBI handler dereferences it.
> + */
> + p3h2x4x->i3c_hub_priv = p3h2x4x_i3c_hub;
> +
> + /*
> + * Register the clear action before enabling IBI so that, on the
> + * devm LIFO unwind (probe failure or removal), the pointer is
> + * cleared only after IBI has been disabled and freed.
> + */
> + ret = devm_add_action_or_reset(dev, p3h2x4x_clear_i3c_hub_priv,
> + p3h2x4x);
> + if (ret)
> + return ret;
> +
> + ret = p3h2x4x_tp_i3c_algo(p3h2x4x_i3c_hub);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to register i3c bus\n");
> + }
> +
> + /* Register virtual I2C adapters for SMBus target ports */
> + ret = p3h2x4x_tp_smbus_algo(p3h2x4x_i3c_hub);
> + if (ret)
> + return dev_err_probe(dev, ret, "Failed to add i2c adapter\n");
> +
> + ret = devm_add_action_or_reset(dev,
> + p3h2x4x_unregister_smbus_adapters_action,
> + p3h2x4x_i3c_hub);
> + if (ret)
> + return ret;
> +
> + return 0;
> +}
> +
> +static const struct platform_device_id p3h2x4x_i3c_hub_id[] = {
> + { "p3h2x4x-i3c-hub" },
Use named initializers. In every patch of yours.
> + { }
> +};
..
> +
> +/**
> + * p3h2x4x_tp_i3c_algo - Register I3C virtual masters for I3C target ports.
> + * @p3h2x4x_hub: p3h2x4x device structure.
> + * Return: 0 in case of success, negative error code on failure.
> + */
> +int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_hub)
> +{
> + struct i3c_master_controller *parent = i3c_dev_get_master(p3h2x4x_hub->i3cdev->desc);
> + struct p3h2x4x *p3h2x4x = dev_get_drvdata(p3h2x4x_hub->dev->parent);
> + u8 tp, ntwk_mask = 0;
> + int ret;
> +
> + p3h2x4x_hub->hub = devm_kzalloc(p3h2x4x_hub->dev,
> + sizeof(*p3h2x4x_hub->hub),
> + GFP_KERNEL);
> +
In multiple places you added blank lines between the call and if()
check. Don't.
> + if (!p3h2x4x_hub->hub)
> + return -ENOMEM;
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 22+ messages in thread
end of thread, other threads:[~2026-08-27 10:16 UTC | newest]
Thread overview: 22+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-26 10:38 [PATCH v16 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 1/8] i3c: master: Add APIs for I3C hub support Lakshay Piplani
2026-08-26 10:52 ` sashiko-bot
2026-08-26 16:36 ` Frank Li
2026-08-26 10:38 ` [PATCH v16 2/8] i3c: master: Add controller-only device operation helpers Lakshay Piplani
2026-08-26 10:50 ` sashiko-bot
2026-08-26 16:42 ` Frank Li
2026-08-27 7:02 ` Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support Lakshay Piplani
2026-08-26 10:38 ` [PATCH v16 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator Lakshay Piplani
2026-08-26 10:56 ` sashiko-bot
2026-08-27 10:03 ` Krzysztof Kozlowski
2026-08-26 10:38 ` [PATCH v16 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub Lakshay Piplani
2026-08-26 10:48 ` sashiko-bot
2026-08-26 10:38 ` [PATCH v16 6/8] i3c: hub: Add support for the I3C interface in the I3C hub Lakshay Piplani
2026-08-26 11:04 ` sashiko-bot
2026-08-26 17:02 ` Frank Li
2026-08-27 7:07 ` Lakshay Piplani
2026-08-27 10:09 ` Krzysztof Kozlowski
2026-08-26 10:38 ` [PATCH v16 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality Lakshay Piplani
2026-08-27 10:16 ` Krzysztof Kozlowski
2026-08-26 10:38 ` [PATCH v16 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support Lakshay Piplani
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox