* [PATCH v15 0/8] Add support for NXP P3H2x4x I3C hub driver
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 297 ++++++++
MAINTAINERS | 15 +
drivers/i3c/Kconfig | 15 +
drivers/i3c/Makefile | 2 +
drivers/i3c/hub.c | 675 ++++++++++++++++++
drivers/i3c/hub/Kconfig | 11 +
drivers/i3c/hub/Makefile | 4 +
drivers/i3c/hub/p3h2840_i3c_hub.h | 350 +++++++++
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 428 +++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 165 +++++
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 623 ++++++++++++++++
drivers/i3c/internals.h | 14 +
drivers/i3c/master.c | 383 ++++++++--
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 | 92 +++
include/linux/i3c/master.h | 17 +
include/linux/mfd/p3h2840.h | 41 ++
23 files changed, 3558 insertions(+), 53 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] 38+ messages in thread
* [PATCH v15 0/8] Add support for NXP P3H2x4x I3C hub driver
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 297 ++++++++
MAINTAINERS | 15 +
drivers/i3c/Kconfig | 15 +
drivers/i3c/Makefile | 2 +
drivers/i3c/hub.c | 675 ++++++++++++++++++
drivers/i3c/hub/Kconfig | 11 +
drivers/i3c/hub/Makefile | 4 +
drivers/i3c/hub/p3h2840_i3c_hub.h | 350 +++++++++
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 428 +++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 165 +++++
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 623 ++++++++++++++++
drivers/i3c/internals.h | 14 +
drivers/i3c/master.c | 383 ++++++++--
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 | 92 +++
include/linux/i3c/master.h | 17 +
include/linux/mfd/p3h2840.h | 41 ++
23 files changed, 3558 insertions(+), 53 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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 helpers for attaching and detaching I3C devices and 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 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] 38+ messages in thread
* [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 helpers for attaching and detaching I3C devices and 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 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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 228 ++++++++++++++++++++++++++++++++++------
2 files changed, 212 insertions(+), 30 deletions(-)
diff --git a/drivers/i3c/internals.h b/drivers/i3c/internals.h
index 0f1f3f766623..f56f5f23a394 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(struct i3c_dev_desc *dev);
+int i3c_master_reattach_i3c_dev_controller(struct i3c_dev_desc *dev,
+ u8 old_dyn_addr);
+void i3c_master_detach_i3c_dev_controller(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..7ae6b3aaa9b4 100644
--- a/drivers/i3c/master.c
+++ b/drivers/i3c/master.c
@@ -1764,6 +1764,27 @@ static int i3c_master_get_i3c_addrs(struct i3c_dev_desc *dev)
return -EBUSY;
}
+/**
+ * i3c_master_attach_i3c_dev_controller() - 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.
+ *
+ * Return: 0 on success, or a negative error code returned by the controller.
+ */
+int i3c_master_attach_i3c_dev_controller(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);
+
static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master,
struct i3c_dev_desc *dev)
{
@@ -1781,12 +1802,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(dev);
+ if (ret) {
+ i3c_master_put_i3c_addrs(dev);
+ return ret;
}
list_add_tail(&dev->common.node, &master->bus.devs.i3c);
@@ -1794,6 +1813,28 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master,
return 0;
}
+/**
+ * i3c_master_reattach_i3c_dev_controller() - 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.
+ *
+ * Return: 0 on success, or a negative error code returned by the controller.
+ */
+int i3c_master_reattach_i3c_dev_controller(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);
+
/**
* i3c_master_reattach_i3c_dev_locked() - reattach an I3C device with a new address
* @dev: I3C device descriptor to reattach
@@ -1824,25 +1865,36 @@ 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(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() - 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.
+ */
+void i3c_master_detach_i3c_dev_controller(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);
+
+static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev)
+{
+ i3c_master_detach_i3c_dev_controller(dev);
i3c_master_put_i3c_addrs(dev);
list_del(&dev->common.node);
@@ -3201,6 +3253,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 +3594,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 +3632,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 +3648,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 +3690,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 +3700,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 +3744,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 +3767,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 +3784,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 +3835,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] 38+ messages in thread
* [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 228 ++++++++++++++++++++++++++++++++++------
2 files changed, 212 insertions(+), 30 deletions(-)
diff --git a/drivers/i3c/internals.h b/drivers/i3c/internals.h
index 0f1f3f766623..f56f5f23a394 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(struct i3c_dev_desc *dev);
+int i3c_master_reattach_i3c_dev_controller(struct i3c_dev_desc *dev,
+ u8 old_dyn_addr);
+void i3c_master_detach_i3c_dev_controller(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..7ae6b3aaa9b4 100644
--- a/drivers/i3c/master.c
+++ b/drivers/i3c/master.c
@@ -1764,6 +1764,27 @@ static int i3c_master_get_i3c_addrs(struct i3c_dev_desc *dev)
return -EBUSY;
}
+/**
+ * i3c_master_attach_i3c_dev_controller() - 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.
+ *
+ * Return: 0 on success, or a negative error code returned by the controller.
+ */
+int i3c_master_attach_i3c_dev_controller(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);
+
static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master,
struct i3c_dev_desc *dev)
{
@@ -1781,12 +1802,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(dev);
+ if (ret) {
+ i3c_master_put_i3c_addrs(dev);
+ return ret;
}
list_add_tail(&dev->common.node, &master->bus.devs.i3c);
@@ -1794,6 +1813,28 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master,
return 0;
}
+/**
+ * i3c_master_reattach_i3c_dev_controller() - 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.
+ *
+ * Return: 0 on success, or a negative error code returned by the controller.
+ */
+int i3c_master_reattach_i3c_dev_controller(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);
+
/**
* i3c_master_reattach_i3c_dev_locked() - reattach an I3C device with a new address
* @dev: I3C device descriptor to reattach
@@ -1824,25 +1865,36 @@ 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(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() - 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.
+ */
+void i3c_master_detach_i3c_dev_controller(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);
+
+static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev)
+{
+ i3c_master_detach_i3c_dev_controller(dev);
i3c_master_put_i3c_addrs(dev);
list_del(&dev->common.node);
@@ -3201,6 +3253,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 +3594,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 +3632,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 +3648,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 +3690,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 +3700,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 +3744,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 +3767,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 +3784,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 +3835,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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 297 ++++++++++++++++++
MAINTAINERS | 9 +
2 files changed, 306 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..6b84ccd8ace0
--- /dev/null
+++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
@@ -0,0 +1,297 @@
+# 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
+
+patternProperties:
+ "^i3c@[0-7]$":
+ type: object
+ $ref: /schemas/i3c/i3c.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ description:
+ The I3C HUB Target Port number.
+ items:
+ - 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:
+ - maximum: 7
+
+ nxp,pullup-enable:
+ type: boolean
+ description:
+ Enables the on-die pull-up for Target Port.
+
+ required:
+ - reg
+
+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] 38+ messages in thread
* [PATCH v15 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 297 ++++++++++++++++++
MAINTAINERS | 9 +
2 files changed, 306 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..6b84ccd8ace0
--- /dev/null
+++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
@@ -0,0 +1,297 @@
+# 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
+
+patternProperties:
+ "^i3c@[0-7]$":
+ type: object
+ $ref: /schemas/i3c/i3c.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ description:
+ The I3C HUB Target Port number.
+ items:
+ - 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:
+ - maximum: 7
+
+ nxp,pullup-enable:
+ type: boolean
+ description:
+ Enables the on-die pull-up for Target Port.
+
+ required:
+ - reg
+
+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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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..6c497d84e2bd
--- /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[] = {
+ { "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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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..6c497d84e2bd
--- /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[] = {
+ { "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] 38+ messages in thread
* [PATCH v15 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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..d0c6c5b65557 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..645d46ad7c2f
--- /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 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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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..d0c6c5b65557 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..645d46ad7c2f
--- /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 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] 38+ messages in thread
* [PATCH v15 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 675 ++++++++++++++++++++++++++++++++++++++++
include/linux/i3c/hub.h | 92 ++++++
5 files changed, 784 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..04726393004b
--- /dev/null
+++ b/drivers/i3c/hub.c
@@ -0,0 +1,675 @@
+// 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 "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;
+};
+
+/**
+ * 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;
+
+ 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;
+
+ 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;
+
+ /* Fix the parent-facing descriptor's master to 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(&data->parent_desc);
+ i3c_bus_maintenance_unlock(&parent->bus);
+ if (ret) {
+ mutex_destroy(&data->parent_desc.ibi_lock);
+ kfree(data);
+ return ret;
+ }
+
+ /*
+ * The logical descriptor stores the hub-private data, while the
+ * parent-facing descriptor stores the physical controller's private
+ * 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;
+
+ /* Re-sync device information after the address change. */
+ data->parent_desc.info = dev->info;
+
+ i3c_bus_maintenance_lock(&parent->bus);
+ ret = i3c_master_reattach_i3c_dev_controller(&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);
+
+ /*
+ * The generic IBI lifecycle must be released before detaching the
+ * physical controller state.
+ */
+ WARN_ON(data->parent_desc.ibi);
+
+ if (parent) {
+ i3c_bus_maintenance_lock(&parent->bus);
+ i3c_master_detach_i3c_dev_controller(&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;
+
+ 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.)
+ *
+ * Handles address adjustment and forwards private transfers through the hub
+ * device.
+ */
+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;
+
+ mutex_lock(&hub->lock);
+
+ /*
+ * Parent-facing device information may be refreshed before IBI
+ * resources are requested; once IBI resources are requested, the
+ * information remains immutable while the parent controller may use
+ * the descriptor asynchronously.
+ */
+ 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)
+{
+ hub->ops = ops;
+ hub->hub_dev = hub_dev;
+ mutex_init(&hub->lock);
+}
+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 downstream I3C devices that keep the
+ * same static and assigned dynamic address, so parent DAA does not reuse them.
+ *
+ * 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;
+ u8 static_addr;
+ u32 reg[3];
+ int ret;
+
+ if (!parent || !node)
+ return -ENODEV;
+
+ for_each_available_child_of_node_scoped(node, child) {
+ 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;
+
+ static_addr = reg[0];
+
+ if (!static_addr || !assigned_addr)
+ continue;
+
+ if (static_addr != assigned_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..90f4be0fdfba
--- /dev/null
+++ b/include/linux/i3c/hub.h
@@ -0,0 +1,92 @@
+/* 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
+ */
+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] 38+ messages in thread
* [PATCH v15 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 675 ++++++++++++++++++++++++++++++++++++++++
include/linux/i3c/hub.h | 92 ++++++
5 files changed, 784 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..04726393004b
--- /dev/null
+++ b/drivers/i3c/hub.c
@@ -0,0 +1,675 @@
+// 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 "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;
+};
+
+/**
+ * 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;
+
+ 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;
+
+ 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;
+
+ /* Fix the parent-facing descriptor's master to 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(&data->parent_desc);
+ i3c_bus_maintenance_unlock(&parent->bus);
+ if (ret) {
+ mutex_destroy(&data->parent_desc.ibi_lock);
+ kfree(data);
+ return ret;
+ }
+
+ /*
+ * The logical descriptor stores the hub-private data, while the
+ * parent-facing descriptor stores the physical controller's private
+ * 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;
+
+ /* Re-sync device information after the address change. */
+ data->parent_desc.info = dev->info;
+
+ i3c_bus_maintenance_lock(&parent->bus);
+ ret = i3c_master_reattach_i3c_dev_controller(&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);
+
+ /*
+ * The generic IBI lifecycle must be released before detaching the
+ * physical controller state.
+ */
+ WARN_ON(data->parent_desc.ibi);
+
+ if (parent) {
+ i3c_bus_maintenance_lock(&parent->bus);
+ i3c_master_detach_i3c_dev_controller(&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;
+
+ 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.)
+ *
+ * Handles address adjustment and forwards private transfers through the hub
+ * device.
+ */
+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;
+
+ mutex_lock(&hub->lock);
+
+ /*
+ * Parent-facing device information may be refreshed before IBI
+ * resources are requested; once IBI resources are requested, the
+ * information remains immutable while the parent controller may use
+ * the descriptor asynchronously.
+ */
+ 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)
+{
+ hub->ops = ops;
+ hub->hub_dev = hub_dev;
+ mutex_init(&hub->lock);
+}
+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 downstream I3C devices that keep the
+ * same static and assigned dynamic address, so parent DAA does not reuse them.
+ *
+ * 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;
+ u8 static_addr;
+ u32 reg[3];
+ int ret;
+
+ if (!parent || !node)
+ return -ENODEV;
+
+ for_each_available_child_of_node_scoped(node, child) {
+ 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;
+
+ static_addr = reg[0];
+
+ if (!static_addr || !assigned_addr)
+ continue;
+
+ if (static_addr != assigned_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..90f4be0fdfba
--- /dev/null
+++ b/include/linux/i3c/hub.h
@@ -0,0 +1,92 @@
+/* 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
+ */
+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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 332 ++++++++++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 428 +++++++++++++++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 119 +++++++
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 337 ++++++++++++++++++
9 files changed, 1234 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..816879fda326
--- /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..5e1272bfdc43
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -0,0 +1,332 @@
+/* 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.
+ */
+#define P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(x) ((23 * (x)) + 100)
+
+#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..3bf79f4692f5
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
@@ -0,0 +1,428 @@
+// 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 == -EPROBE_DEFER)
+ return -EPROBE_DEFER;
+
+ if (ret && ret != -ENODEV)
+ dev_warn(dev, "Failed to enable %s (%d)\n",
+ supplies[i], ret);
+ }
+
+ /* 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 */
+ mutex_lock(&p3h2x4x->protected_reg_lock);
+
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ goto out_unlock_mutex;
+
+ ret = p3h2x4x_configure_pullup(dev);
+ if (ret)
+ goto out_lock;
+
+ ret = p3h2x4x_configure_io_strength(dev);
+ if (ret)
+ goto out_lock;
+
+ ret = p3h2x4x_configure_tp(dev);
+ if (ret)
+ goto out_lock;
+
+out_lock:
+ ret2 = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_LOCK_CODE);
+ if (!ret && ret2)
+ ret = ret2;
+
+out_unlock_mutex:
+ mutex_unlock(&p3h2x4x->protected_reg_lock);
+ 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..f6d4ade506e2
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
@@ -0,0 +1,337 @@
+// 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 timeout_us, sleep_us;
+ u32 status_read;
+ u8 status;
+ int ret;
+
+ timeout_us = P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length);
+ sleep_us = clamp(timeout_us / P3H2X4X_SMBUS_POLL_COUNT,
+ P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US,
+ P3H2X4X_SMBUS_POLL_INTERVAL_MAX_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) {
+ ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_CONTROLLER_AGENT_BUFF_DATA + write_length,
+ xfers[nxfers_i].buf, xfers[nxfers_i].len);
+ 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;
+
+ mutex_lock(&p3h2x4x->protected_reg_lock);
+
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ goto out_unlock_mutex;
+
+ 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;
+
+out_unlock_mutex:
+ mutex_unlock(&p3h2x4x->protected_reg_lock);
+ 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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 332 ++++++++++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 428 +++++++++++++++++++++++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 119 +++++++
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 337 ++++++++++++++++++
9 files changed, 1234 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..816879fda326
--- /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..5e1272bfdc43
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -0,0 +1,332 @@
+/* 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.
+ */
+#define P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(x) ((23 * (x)) + 100)
+
+#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..3bf79f4692f5
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
@@ -0,0 +1,428 @@
+// 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 == -EPROBE_DEFER)
+ return -EPROBE_DEFER;
+
+ if (ret && ret != -ENODEV)
+ dev_warn(dev, "Failed to enable %s (%d)\n",
+ supplies[i], ret);
+ }
+
+ /* 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 */
+ mutex_lock(&p3h2x4x->protected_reg_lock);
+
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ goto out_unlock_mutex;
+
+ ret = p3h2x4x_configure_pullup(dev);
+ if (ret)
+ goto out_lock;
+
+ ret = p3h2x4x_configure_io_strength(dev);
+ if (ret)
+ goto out_lock;
+
+ ret = p3h2x4x_configure_tp(dev);
+ if (ret)
+ goto out_lock;
+
+out_lock:
+ ret2 = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_LOCK_CODE);
+ if (!ret && ret2)
+ ret = ret2;
+
+out_unlock_mutex:
+ mutex_unlock(&p3h2x4x->protected_reg_lock);
+ 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..f6d4ade506e2
--- /dev/null
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
@@ -0,0 +1,337 @@
+// 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 timeout_us, sleep_us;
+ u32 status_read;
+ u8 status;
+ int ret;
+
+ timeout_us = P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length);
+ sleep_us = clamp(timeout_us / P3H2X4X_SMBUS_POLL_COUNT,
+ P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US,
+ P3H2X4X_SMBUS_POLL_INTERVAL_MAX_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) {
+ ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_CONTROLLER_AGENT_BUFF_DATA + write_length,
+ xfers[nxfers_i].buf, xfers[nxfers_i].len);
+ 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;
+
+ mutex_lock(&p3h2x4x->protected_reg_lock);
+
+ ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ goto out_unlock_mutex;
+
+ 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;
+
+out_unlock_mutex:
+ mutex_unlock(&p3h2x4x->protected_reg_lock);
+ 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] 38+ messages in thread
* [PATCH v15 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:38 ` Lakshay Piplani
-1 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 18 ++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 48 +++-
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 286 ++++++++++++++++++++++++
3 files changed, 351 insertions(+), 1 deletion(-)
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
index 5e1272bfdc43..bc2d6cd373ab 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
@@ -304,6 +309,9 @@ 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;
+#endif
struct i3c_hub *hub;
};
@@ -329,4 +337,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..1ba8b8748f73 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,33 @@ 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)
+ 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 f6d4ade506e2..c095ae524e66 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,177 @@ 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 slave_rx_buffer[P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 };
+ u8 target_buffer_page, flag_clear, temp = 0, i, addr;
+ u32 packet_len, slave_address;
+ struct i2c_client *client;
+ int ret;
+
+ if (!p3h2x4x || !p3h2x4x->i3c_hub_priv)
+ return;
+
+ p3h2x4x_i3c_hub = p3h2x4x->i3c_hub_priv;
+
+ 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)
@@ -206,6 +379,115 @@ static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter)
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
}
+#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);
+ }
+
+ /*
+ * A relock failure does not re-enable the IBI, 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;
+
+ /*
+ * Clear the software state only after the hardware IBI has been
+ * disabled successfully.
+ */
+ 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,
.max_write_len = P3H2X4X_SMBUS_PAYLOAD_SIZE,
@@ -216,6 +498,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
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply related [flat|nested] 38+ messages in thread
* [PATCH v15 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support
@ 2026-08-17 10:38 ` Lakshay Piplani
0 siblings, 0 replies; 38+ messages in thread
From: Lakshay Piplani @ 2026-08-17 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 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 | 18 ++
drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 48 +++-
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 286 ++++++++++++++++++++++++
3 files changed, 351 insertions(+), 1 deletion(-)
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
index 5e1272bfdc43..bc2d6cd373ab 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
@@ -304,6 +309,9 @@ 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;
+#endif
struct i3c_hub *hub;
};
@@ -329,4 +337,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..1ba8b8748f73 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,33 @@ 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)
+ 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 f6d4ade506e2..c095ae524e66 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,177 @@ 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 slave_rx_buffer[P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 };
+ u8 target_buffer_page, flag_clear, temp = 0, i, addr;
+ u32 packet_len, slave_address;
+ struct i2c_client *client;
+ int ret;
+
+ if (!p3h2x4x || !p3h2x4x->i3c_hub_priv)
+ return;
+
+ p3h2x4x_i3c_hub = p3h2x4x->i3c_hub_priv;
+
+ 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)
@@ -206,6 +379,115 @@ static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter)
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
}
+#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);
+ }
+
+ /*
+ * A relock failure does not re-enable the IBI, 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;
+
+ /*
+ * Clear the software state only after the hardware IBI has been
+ * disabled successfully.
+ */
+ 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,
.max_write_len = P3H2X4X_SMBUS_PAYLOAD_SIZE,
@@ -216,6 +498,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] 38+ messages in thread
* Re: [PATCH v15 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:49 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:49 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-i3c, conor+dt, Frank.Li, devicetree, robh,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] The schema fails to restrict the allowed ports and properties for the 4-port hardware variants (P3H2440/P3H2441), allowing invalid configurations.
- [Medium] The `reg` property constraints for child nodes (`i3c@[0-7]$` and `smbus@[0-7]$`) use an invalid array-level constraint `items: - maximum: 7` which silently bypasses port index validation.
- [Medium] The hub acts as a parent to child bus nodes but does not mandate `#address-cells` and `#size-cells` in its required list.
--
--- Patch [3]: [PATCH v15 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support ---
commit 9450f00b16aca24baa4b725459c27c4b2ab2f58c
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
This commit adds 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.
> diff --git a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
> new file mode 100644
> index 0000000000000..6b84ccd8ace0c
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
[ ... ]
> +properties:
> + compatible:
> + enum:
> + - nxp,p3h2440
> + - nxp,p3h2441
[Severity: Medium]
Should there be an allOf conditional block added to constrain the allowed
ports and properties for the 4-port variants?
The 4-port hardware variants are introduced here but lack conditional blocks
restricting their allowed ports, while the patternProperties later in the
schema unconditionally allows up to 8 ports.
Without an allOf block, device tree configurations that describe nonexistent
ports for the 4-port variants could silently pass schema validation.
> + - nxp,p3h2840
> + - nxp,p3h2841
[ ... ]
> +required:
> + - reg
[Severity: Medium]
Should #address-cells and #size-cells be included in this required list?
Since the hub node acts as a parent bus to child target ports, omitting these
properties allows device trees to define child nodes without proper address
sizing, which might bypass schema validation when they shouldn't.
> +
> +patternProperties:
> + "^i3c@[0-7]$":
> + type: object
> + $ref: /schemas/i3c/i3c.yaml#
> + unevaluatedProperties: false
> +
> + properties:
> + reg:
> + description:
> + The I3C HUB Target Port number.
> + items:
> + - maximum: 7
[Severity: Medium]
Does this constraint correctly validate the port index?
Because dt-schema parses reg as a matrix (an array of cell arrays), JSON
Schema only applies maximum to numeric types and silently ignores this
constraint on the array element. This could allow an out-of-range port number
to bypass port index validation.
To correctly validate the single cell, it might need to use maxItems: 1 and
nested items.
[ ... ]
> + "^smbus@[0-7]$":
> + type: object
> + $ref: /schemas/i2c/i2c-controller.yaml#
> + unevaluatedProperties: false
> +
> + properties:
> + reg:
> + description:
> + The I3C HUB Target Port number.
> + items:
> + - maximum: 7
[Severity: Medium]
Does this have the same validation issue as the i3c child node above?
Like the i3c node, this constraint might fail to validate the port index
because JSON Schema ignores maximum on array elements.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=3
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
@ 2026-08-17 10:49 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:49 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-i3c, conor+dt, Frank.Li, devicetree, robh,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Medium] The schema fails to restrict the allowed ports and properties for the 4-port hardware variants (P3H2440/P3H2441), allowing invalid configurations.
- [Medium] The `reg` property constraints for child nodes (`i3c@[0-7]$` and `smbus@[0-7]$`) use an invalid array-level constraint `items: - maximum: 7` which silently bypasses port index validation.
- [Medium] The hub acts as a parent to child bus nodes but does not mandate `#address-cells` and `#size-cells` in its required list.
--
--- Patch [3]: [PATCH v15 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support ---
commit 9450f00b16aca24baa4b725459c27c4b2ab2f58c
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support
This commit adds 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.
> diff --git a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
> new file mode 100644
> index 0000000000000..6b84ccd8ace0c
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
[ ... ]
> +properties:
> + compatible:
> + enum:
> + - nxp,p3h2440
> + - nxp,p3h2441
[Severity: Medium]
Should there be an allOf conditional block added to constrain the allowed
ports and properties for the 4-port variants?
The 4-port hardware variants are introduced here but lack conditional blocks
restricting their allowed ports, while the patternProperties later in the
schema unconditionally allows up to 8 ports.
Without an allOf block, device tree configurations that describe nonexistent
ports for the 4-port variants could silently pass schema validation.
> + - nxp,p3h2840
> + - nxp,p3h2841
[ ... ]
> +required:
> + - reg
[Severity: Medium]
Should #address-cells and #size-cells be included in this required list?
Since the hub node acts as a parent bus to child target ports, omitting these
properties allows device trees to define child nodes without proper address
sizing, which might bypass schema validation when they shouldn't.
> +
> +patternProperties:
> + "^i3c@[0-7]$":
> + type: object
> + $ref: /schemas/i3c/i3c.yaml#
> + unevaluatedProperties: false
> +
> + properties:
> + reg:
> + description:
> + The I3C HUB Target Port number.
> + items:
> + - maximum: 7
[Severity: Medium]
Does this constraint correctly validate the port index?
Because dt-schema parses reg as a matrix (an array of cell arrays), JSON
Schema only applies maximum to numeric types and silently ignores this
constraint on the array element. This could allow an out-of-range port number
to bypass port index validation.
To correctly validate the single cell, it might need to use maxItems: 1 and
nested items.
[ ... ]
> + "^smbus@[0-7]$":
> + type: object
> + $ref: /schemas/i2c/i2c-controller.yaml#
> + unevaluatedProperties: false
> +
> + properties:
> + reg:
> + description:
> + The I3C HUB Target Port number.
> + items:
> + - maximum: 7
[Severity: Medium]
Does this have the same validation issue as the i3c child node above?
Like the i3c node, this constraint might fail to validate the port index
because JSON Schema ignores maximum on array elements.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=3
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:50 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:50 UTC (permalink / raw)
To: Lakshay Piplani
Cc: devicetree, Alexandre Belloni, robh, Frank.Li, conor+dt,
linux-i3c
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Medium] The commit message claims to add helpers for attaching and detaching I3C devices, but the implementation is completely missing.
Pre-existing issues:
- [High] Accidental removal of ID 0 from `i3c_bus_idr` when `i3c_bus_init()` fails.
--
--- Patch [1]: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support ---
commit 14f349144e98c1c3eedcaa4123a53bf24704569f
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
i3c: master: Add APIs for I3C hub support
This commit adds helpers to prepare for I3C hub support. It exposes
address slot management and CCC command interfaces, and allows registering
virtual masters with an explicit firmware node.
> Add helpers for attaching and detaching I3C devices and CCC helpers
> to check CCC support and send CCC commands, address slot helpers to
> query and update I3C bus address slot state, registering virtual
[Severity: Medium]
The commit message claims to add helpers for attaching and detaching I3C
devices, but the implementation is completely missing from the patch.
The changelog indicates that the attach and detach helpers were dropped in
v15. Should the commit message be updated to reflect this change?
> 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
[ ... ]
> @@ -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 isn't a bug introduced by this patch, but does the error handling in
i3c_master_register_fwnode() corrupt the global IDR state?
If i3c_bus_init() fails at idr_alloc() (e.g., due to duplicate alias in DT),
it returns an error while leaving i3cbus->id as 0:
drivers/i3c/master.c:i3c_bus_init() {
...
ret = idr_alloc(&i3c_bus_idr, i3cbus, start, end, GFP_KERNEL);
if (ret < 0)
return ret;
...
}
Taking the goto err_put_dev path then calls put_device(&master->dev), which
eventually triggers i3c_masterdev_release():
drivers/i3c/master.c:i3c_masterdev_release() {
...
i3c_bus_cleanup(bus);
...
}
This calls idr_remove(&i3c_bus_idr, i3cbus->id). Because i3cbus->id is still 0,
doesn't this accidentally remove the valid bus 0 from the IDR tree?
This could lead to duplicate sysfs names ('i3c-0') and crashes during
device_add() for a new bus.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=1
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support
@ 2026-08-17 10:50 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:50 UTC (permalink / raw)
To: Lakshay Piplani
Cc: devicetree, Alexandre Belloni, robh, Frank.Li, conor+dt,
linux-i3c
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
New issues:
- [Medium] The commit message claims to add helpers for attaching and detaching I3C devices, but the implementation is completely missing.
Pre-existing issues:
- [High] Accidental removal of ID 0 from `i3c_bus_idr` when `i3c_bus_init()` fails.
--
--- Patch [1]: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support ---
commit 14f349144e98c1c3eedcaa4123a53bf24704569f
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
i3c: master: Add APIs for I3C hub support
This commit adds helpers to prepare for I3C hub support. It exposes
address slot management and CCC command interfaces, and allows registering
virtual masters with an explicit firmware node.
> Add helpers for attaching and detaching I3C devices and CCC helpers
> to check CCC support and send CCC commands, address slot helpers to
> query and update I3C bus address slot state, registering virtual
[Severity: Medium]
The commit message claims to add helpers for attaching and detaching I3C
devices, but the implementation is completely missing from the patch.
The changelog indicates that the attach and detach helpers were dropped in
v15. Should the commit message be updated to reflect this change?
> 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
[ ... ]
> @@ -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 isn't a bug introduced by this patch, but does the error handling in
i3c_master_register_fwnode() corrupt the global IDR state?
If i3c_bus_init() fails at idr_alloc() (e.g., due to duplicate alias in DT),
it returns an error while leaving i3cbus->id as 0:
drivers/i3c/master.c:i3c_bus_init() {
...
ret = idr_alloc(&i3c_bus_idr, i3cbus, start, end, GFP_KERNEL);
if (ret < 0)
return ret;
...
}
Taking the goto err_put_dev path then calls put_device(&master->dev), which
eventually triggers i3c_masterdev_release():
drivers/i3c/master.c:i3c_masterdev_release() {
...
i3c_bus_cleanup(bus);
...
}
This calls idr_remove(&i3c_bus_idr, i3cbus->id). Because i3cbus->id is still 0,
doesn't this accidentally remove the valid bus 0 from the IDR tree?
This could lead to duplicate sysfs names ('i3c-0') and crashes during
device_add() for a new bus.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=1
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:50 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:50 UTC (permalink / raw)
To: Lakshay Piplani
Cc: devicetree, robh, Frank.Li, conor+dt, linux-i3c,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [Low] Incomplete refactoring: `i3c_master_handle_ibi()` bypasses the newly introduced `i3c_dev_recycle_ibi_slot_controller()` helper.
Pre-existing issues:
- [High] Missing memory barrier in `i3c_dev_disable_ibi_locked` causes a lost wakeup and indefinite hang.
- [High] Use-after-free on error path in `i3c_dev_free_ibi_locked` when workqueue is drained after resources are freed.
--
--- Patch [2]: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers ---
commit 85e9cb3ce264f7889b2b4406bc379307ad054a14
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. 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 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.
[Severity: Low]
The commit message mentions refactoring the existing generic core paths to
use the new controller-only helpers, including the one for recycling IBI
slots.
However, looking at the code for i3c_master_handle_ibi(), it appears to
still bypass the newly introduced wrapper:
drivers/i3c/master.c:i3c_master_handle_ibi() {
...
if (master->ops->recycle_ibi_slot)
master->ops->recycle_ibi_slot(dev, slot);
...
}
Does this function need to be updated to use
i3c_dev_recycle_ibi_slot_controller() to ensure the virtual controllers
in the new I3C hub framework can properly intercept these operations?
> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
> index 03fb41f0786c5..7ae6b3aaa9b4c 100644
> --- a/drivers/i3c/master.c
> +++ b/drivers/i3c/master.c
[ ... ]
> @@ -3534,14 +3632,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;
[Severity: High]
This is a pre-existing issue, but does this sequence lack a memory barrier
before checking the pending IBIs?
Right after this return, the code does:
drivers/i3c/master.c:i3c_dev_disable_ibi_locked() {
...
reinit_completion(&dev->ibi->all_ibis_handled);
if (atomic_read(&dev->ibi->pending_ibis))
wait_for_completion(&dev->ibi->all_ibis_handled);
...
}
Since reinit_completion() performs a plain store that can be delayed or
reordered after the atomic_read(), could a concurrent IBI worker
decrementing the pending count and calling complete() race with this?
If the worker executes between the load of pending_ibis and the delayed
store, could the store overwrite the completion state back to 0, causing
wait_for_completion() to hang indefinitely?
[ ... ]
> @@ -3667,7 +3835,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: High]
This is a pre-existing issue, but does this cleanup order create a
use-after-free risk during an error path?
If disabling the IBI fails (for example, due to a hardware timeout), the
error path continues and calls i3c_dev_free_ibi_controller_locked(),
which frees the underlying controller resources like the generic IBI pool.
Then destroy_workqueue() is called to drain the workqueue. If there are
pending or executing IBI works still in the workqueue, won't they try to
access the freed slot pool when they run, resulting in a use-after-free?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=2
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers
@ 2026-08-17 10:50 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:50 UTC (permalink / raw)
To: Lakshay Piplani
Cc: devicetree, robh, Frank.Li, conor+dt, linux-i3c,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
New issues:
- [Low] Incomplete refactoring: `i3c_master_handle_ibi()` bypasses the newly introduced `i3c_dev_recycle_ibi_slot_controller()` helper.
Pre-existing issues:
- [High] Missing memory barrier in `i3c_dev_disable_ibi_locked` causes a lost wakeup and indefinite hang.
- [High] Use-after-free on error path in `i3c_dev_free_ibi_locked` when workqueue is drained after resources are freed.
--
--- Patch [2]: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers ---
commit 85e9cb3ce264f7889b2b4406bc379307ad054a14
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. 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 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.
[Severity: Low]
The commit message mentions refactoring the existing generic core paths to
use the new controller-only helpers, including the one for recycling IBI
slots.
However, looking at the code for i3c_master_handle_ibi(), it appears to
still bypass the newly introduced wrapper:
drivers/i3c/master.c:i3c_master_handle_ibi() {
...
if (master->ops->recycle_ibi_slot)
master->ops->recycle_ibi_slot(dev, slot);
...
}
Does this function need to be updated to use
i3c_dev_recycle_ibi_slot_controller() to ensure the virtual controllers
in the new I3C hub framework can properly intercept these operations?
> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
> index 03fb41f0786c5..7ae6b3aaa9b4c 100644
> --- a/drivers/i3c/master.c
> +++ b/drivers/i3c/master.c
[ ... ]
> @@ -3534,14 +3632,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;
[Severity: High]
This is a pre-existing issue, but does this sequence lack a memory barrier
before checking the pending IBIs?
Right after this return, the code does:
drivers/i3c/master.c:i3c_dev_disable_ibi_locked() {
...
reinit_completion(&dev->ibi->all_ibis_handled);
if (atomic_read(&dev->ibi->pending_ibis))
wait_for_completion(&dev->ibi->all_ibis_handled);
...
}
Since reinit_completion() performs a plain store that can be delayed or
reordered after the atomic_read(), could a concurrent IBI worker
decrementing the pending count and calling complete() race with this?
If the worker executes between the load of pending_ibis and the delayed
store, could the store overwrite the completion state back to 0, causing
wait_for_completion() to hang indefinitely?
[ ... ]
> @@ -3667,7 +3835,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: High]
This is a pre-existing issue, but does this cleanup order create a
use-after-free risk during an error path?
If disabling the IBI fails (for example, due to a hardware timeout), the
error path continues and calls i3c_dev_free_ibi_controller_locked(),
which frees the underlying controller resources like the generic IBI pool.
Then destroy_workqueue() is called to drain the workqueue. If there are
pending or executing IBI works still in the workqueue, won't they try to
access the freed slot pool when they run, resulting in a use-after-free?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=2
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:51 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:51 UTC (permalink / raw)
To: Lakshay Piplani
Cc: Frank.Li, devicetree, conor+dt, robh, Alexandre Belloni,
linux-i3c
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Premature freeing of `struct i3c_master_controller` memory via devres while the driver core may still hold references to its embedded `struct device`.
- [Medium] Mixing goto-based error handling with scope-based cleanup helpers in the same function.
- [Medium] The hub driver improperly injects its private context into the parent MFD driver's data structure via a bespoke pointer, tightly coupling the layers.
- [Medium] The SMBus transfer polling timeout is too tight, leading to premature failures on legal target clock stretching.
- [Medium] Ignored failure to enable explicitly defined regulators.
--
--- Patch [7]: [PATCH v15 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality ---
commit e98ff2e22cdda2aa4bb2578e7f060053f335dbde
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
This commit introduces the driver for NXP P3H2x4x multiport I3C hubs. It
supports downstream target ports configured as I3C, I2C, or SMBus,
facilitating communication between the host and downstream devices.
> diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_common.c b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
> --- /dev/null
> +++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
> @@ -0,0 +1,428 @@
[ ... ]
> +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 == -EPROBE_DEFER)
> + return -EPROBE_DEFER;
> +
> + if (ret && ret != -ENODEV)
> + dev_warn(dev, "Failed to enable %s (%d)\n",
> + supplies[i], ret);
[Severity: Medium]
Is it safe to ignore a failure when enabling explicitly defined regulators?
If devm_regulator_get_enable_optional() fails with an error other than
-ENODEV or -EPROBE_DEFER, the driver logs a warning and proceeds. Ignoring an
actual failure could lead the driver to attempt communication with unpowered
hardware, which might cause transaction timeouts or access to undefined
register states.
Should the initialization be aborted and the error propagated instead?
> + }
> +
> + /* This delay is required for the regulator to stabilize its output voltage */
> + fsleep(5000);
> +
> + return 0;
> +}
[ ... ]
> +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);
[Severity: High]
Can the use of devm_kzalloc here lead to a use-after-free?
The p3h2x4x_i3c_hub_dev structure embeds multiple i3c_master_controller
instances, which in turn embed struct device instances registered later via
i3c_master_register_fwnode().
If the driver unbinds, devres will immediately free the p3h2x4x_i3c_hub_dev
block. However, if a user holds a reference to an I3C master sysfs attribute
(e.g., while reading files under /sys/class/i3c/), the driver core might
still hold a reference to the embedded struct device. When the final
reference is dropped and the device release callback is invoked, the core
would access freed memory.
Should this allocation be managed by reference counting instead of devres?
> + if (!p3h2x4x_i3c_hub)
> + return -ENOMEM;
[ ... ]
> + /* 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;
[Severity: Medium]
Does writing the child hub driver's context directly into the parent MFD
driver's data structure violate MFD isolation guidelines?
The driver injects a backpointer into p3h2x4x->i3c_hub_priv. MFD subsystem
guidelines generally mandate that parent-child data sharing must use standard
APIs (like dev_get_drvdata) and prohibit writing bespoke accessors or holding
child state directly in the parent.
Could this create tight coupling between layers and risk use-after-free bugs
if lifetimes mismatch?
> +
> + /*
> + * Register the clear action before enabling IBI so that, on the
[ ... ]
> diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> --- /dev/null
> +++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> @@ -0,0 +1,337 @@
[ ... ]
> +static int p3h2x4x_read_smbus_transaction_status(struct p3h2x4x_i3c_hub_dev *hub,
> + u8 target_port_status,
> + u8 data_length)
> +{
> + unsigned int timeout_us, sleep_us;
> + u32 status_read;
> + u8 status;
> + int ret;
> +
> + timeout_us = P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length);
> + sleep_us = clamp(timeout_us / P3H2X4X_SMBUS_POLL_COUNT,
> + P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US,
> + P3H2X4X_SMBUS_POLL_INTERVAL_MAX_US);
> +
> + ret = regmap_read_poll_timeout(hub->regmap, target_port_status,
> + status_read,
> + status_read & P3H2X4X_SMBUS_TRANSACTION_FINISH_FLAG,
> + sleep_us,
> + timeout_us);
[Severity: Medium]
Could this timeout be too tight for standard SMBus targets?
The timeout_us calculation allows a maximum of 100us overhead for the entire
transaction. However, the SMBus specification permits targets to stretch the
clock for much longer periods (up to 35 milliseconds).
If a target legally stretches the clock, regmap_read_poll_timeout() will hit
the timeout and abort the software transaction while the hardware is still
executing the transfer.
Should a larger, more standard timeout be used here to accommodate clock
stretching?
> + if (ret)
> + return ret;
[ ... ]
> +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;
> +
> + mutex_lock(&p3h2x4x->protected_reg_lock);
> +
> + ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
> + P3H2X4X_REGISTERS_UNLOCK_CODE);
> + if (ret)
> + goto out_unlock_mutex;
> +
> + 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;
> +
> +out_unlock_mutex:
> + mutex_unlock(&p3h2x4x->protected_reg_lock);
> + 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) {
[Severity: Medium]
Is it appropriate to mix goto-based error handling and scoped cleanup macros
in the same function?
This function uses a goto out_unlock_mutex sequence for error handling of
protected_reg_lock, but later uses the scoped_guard() macro for etx_mutex.
According to kernel cleanup guidelines, mixing goto-based error handling with
scope-based cleanup helpers in the same function is discouraged because it can
create confusing ownership semantics and increase the risk of resource leaks
or double-unlocks during future modifications.
Could all resources in this routine be converted to scope-based cleanup, or
none of them?
> + 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 */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=7
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
@ 2026-08-17 10:51 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:51 UTC (permalink / raw)
To: Lakshay Piplani
Cc: Frank.Li, devicetree, conor+dt, robh, Alexandre Belloni,
linux-i3c
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Premature freeing of `struct i3c_master_controller` memory via devres while the driver core may still hold references to its embedded `struct device`.
- [Medium] Mixing goto-based error handling with scope-based cleanup helpers in the same function.
- [Medium] The hub driver improperly injects its private context into the parent MFD driver's data structure via a bespoke pointer, tightly coupling the layers.
- [Medium] The SMBus transfer polling timeout is too tight, leading to premature failures on legal target clock stretching.
- [Medium] Ignored failure to enable explicitly defined regulators.
--
--- Patch [7]: [PATCH v15 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality ---
commit e98ff2e22cdda2aa4bb2578e7f060053f335dbde
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality
This commit introduces the driver for NXP P3H2x4x multiport I3C hubs. It
supports downstream target ports configured as I3C, I2C, or SMBus,
facilitating communication between the host and downstream devices.
> diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_common.c b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
> --- /dev/null
> +++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
> @@ -0,0 +1,428 @@
[ ... ]
> +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 == -EPROBE_DEFER)
> + return -EPROBE_DEFER;
> +
> + if (ret && ret != -ENODEV)
> + dev_warn(dev, "Failed to enable %s (%d)\n",
> + supplies[i], ret);
[Severity: Medium]
Is it safe to ignore a failure when enabling explicitly defined regulators?
If devm_regulator_get_enable_optional() fails with an error other than
-ENODEV or -EPROBE_DEFER, the driver logs a warning and proceeds. Ignoring an
actual failure could lead the driver to attempt communication with unpowered
hardware, which might cause transaction timeouts or access to undefined
register states.
Should the initialization be aborted and the error propagated instead?
> + }
> +
> + /* This delay is required for the regulator to stabilize its output voltage */
> + fsleep(5000);
> +
> + return 0;
> +}
[ ... ]
> +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);
[Severity: High]
Can the use of devm_kzalloc here lead to a use-after-free?
The p3h2x4x_i3c_hub_dev structure embeds multiple i3c_master_controller
instances, which in turn embed struct device instances registered later via
i3c_master_register_fwnode().
If the driver unbinds, devres will immediately free the p3h2x4x_i3c_hub_dev
block. However, if a user holds a reference to an I3C master sysfs attribute
(e.g., while reading files under /sys/class/i3c/), the driver core might
still hold a reference to the embedded struct device. When the final
reference is dropped and the device release callback is invoked, the core
would access freed memory.
Should this allocation be managed by reference counting instead of devres?
> + if (!p3h2x4x_i3c_hub)
> + return -ENOMEM;
[ ... ]
> + /* 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;
[Severity: Medium]
Does writing the child hub driver's context directly into the parent MFD
driver's data structure violate MFD isolation guidelines?
The driver injects a backpointer into p3h2x4x->i3c_hub_priv. MFD subsystem
guidelines generally mandate that parent-child data sharing must use standard
APIs (like dev_get_drvdata) and prohibit writing bespoke accessors or holding
child state directly in the parent.
Could this create tight coupling between layers and risk use-after-free bugs
if lifetimes mismatch?
> +
> + /*
> + * Register the clear action before enabling IBI so that, on the
[ ... ]
> diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> --- /dev/null
> +++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> @@ -0,0 +1,337 @@
[ ... ]
> +static int p3h2x4x_read_smbus_transaction_status(struct p3h2x4x_i3c_hub_dev *hub,
> + u8 target_port_status,
> + u8 data_length)
> +{
> + unsigned int timeout_us, sleep_us;
> + u32 status_read;
> + u8 status;
> + int ret;
> +
> + timeout_us = P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length);
> + sleep_us = clamp(timeout_us / P3H2X4X_SMBUS_POLL_COUNT,
> + P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US,
> + P3H2X4X_SMBUS_POLL_INTERVAL_MAX_US);
> +
> + ret = regmap_read_poll_timeout(hub->regmap, target_port_status,
> + status_read,
> + status_read & P3H2X4X_SMBUS_TRANSACTION_FINISH_FLAG,
> + sleep_us,
> + timeout_us);
[Severity: Medium]
Could this timeout be too tight for standard SMBus targets?
The timeout_us calculation allows a maximum of 100us overhead for the entire
transaction. However, the SMBus specification permits targets to stretch the
clock for much longer periods (up to 35 milliseconds).
If a target legally stretches the clock, regmap_read_poll_timeout() will hit
the timeout and abort the software transaction while the hardware is still
executing the transfer.
Should a larger, more standard timeout be used here to accommodate clock
stretching?
> + if (ret)
> + return ret;
[ ... ]
> +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;
> +
> + mutex_lock(&p3h2x4x->protected_reg_lock);
> +
> + ret = regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE,
> + P3H2X4X_REGISTERS_UNLOCK_CODE);
> + if (ret)
> + goto out_unlock_mutex;
> +
> + 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;
> +
> +out_unlock_mutex:
> + mutex_unlock(&p3h2x4x->protected_reg_lock);
> + 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) {
[Severity: Medium]
Is it appropriate to mix goto-based error handling and scoped cleanup macros
in the same function?
This function uses a goto out_unlock_mutex sequence for error handling of
protected_reg_lock, but later uses the scoped_guard() macro for etx_mutex.
According to kernel cleanup guidelines, mixing goto-based error handling with
scope-based cleanup helpers in the same function is discouraged because it can
create confusing ownership semantics and increase the risk of resource leaks
or double-unlocks during future modifications.
Could all resources in this routine be converted to scope-based cleanup, or
none of them?
> + 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 */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=7
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:51 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:51 UTC (permalink / raw)
To: Lakshay Piplani
Cc: robh, devicetree, conor+dt, linux-i3c, Alexandre Belloni,
Frank.Li
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] MFD subsystem guide violation: driver-specific header in global include directory and use of bespoke parent context structure.
- [Low] The `p3h2x4x_regulators` array of regulator descriptors is not marked `const`.
--
--- Patch [5]: [PATCH v15 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub ---
commit 6bd5ced43bd70804c772227fde0c55395341c77d
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
This patch introduces a new regulator driver for the NXP P3H2x4x I3C hub. It
registers the LDOs using the MFD framework and exposes them via 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
> index 0000000000000..645d46ad7c2f2
> --- /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: Medium]
Should this header be kept local to drivers/mfd/ instead of placing it in the
global include/linux/mfd/ directory? The MFD subsystem guidelines recommend
against placing driver-specific header files in the global include directory
if they are only used by the parent and its immediate children.
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/regmap.h>
> +#include <linux/regulator/driver.h>
[ ... ]
> +static const unsigned int p3h2x4x_voltage_table[] = {
> + 1000000,
> + 1100000,
> + 1200000,
> + 1800000,
> +};
> +
> +static struct regulator_desc p3h2x4x_regulators[] = {
[Severity: Low]
Could this array be marked const? Because devm_regulator_register() accepts a
const struct regulator_desc pointer, making this array const would move it to
the read-only .rodata section.
> + {
> + .name = "ldo-cp0",
> + .of_match = of_match_ptr("ldo-cp0"),
> + .regulators_node = of_match_ptr("regulators"),
[ ... ]
> + {
> + .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);
[Severity: Medium]
Is it possible to use standard APIs to access the required state rather than
exposing the bespoke parent struct p3h2x4x to the child drivers?
The MFD subsystem guidelines discourage exposing bespoke parent structures for
parent-child data sharing to maintain proper layering.
> + struct p3h2x4x_regulator_dev *p3h2x4x_regulator;
> + struct regulator_config rcfg = { };
> + struct device *dev = &pdev->dev;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=5
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
@ 2026-08-17 10:51 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:51 UTC (permalink / raw)
To: Lakshay Piplani
Cc: robh, devicetree, conor+dt, linux-i3c, Alexandre Belloni,
Frank.Li
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] MFD subsystem guide violation: driver-specific header in global include directory and use of bespoke parent context structure.
- [Low] The `p3h2x4x_regulators` array of regulator descriptors is not marked `const`.
--
--- Patch [5]: [PATCH v15 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub ---
commit 6bd5ced43bd70804c772227fde0c55395341c77d
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub
This patch introduces a new regulator driver for the NXP P3H2x4x I3C hub. It
registers the LDOs using the MFD framework and exposes them via 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
> index 0000000000000..645d46ad7c2f2
> --- /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: Medium]
Should this header be kept local to drivers/mfd/ instead of placing it in the
global include/linux/mfd/ directory? The MFD subsystem guidelines recommend
against placing driver-specific header files in the global include directory
if they are only used by the parent and its immediate children.
> +#include <linux/of.h>
> +#include <linux/platform_device.h>
> +#include <linux/regmap.h>
> +#include <linux/regulator/driver.h>
[ ... ]
> +static const unsigned int p3h2x4x_voltage_table[] = {
> + 1000000,
> + 1100000,
> + 1200000,
> + 1800000,
> +};
> +
> +static struct regulator_desc p3h2x4x_regulators[] = {
[Severity: Low]
Could this array be marked const? Because devm_regulator_register() accepts a
const struct regulator_desc pointer, making this array const would move it to
the read-only .rodata section.
> + {
> + .name = "ldo-cp0",
> + .of_match = of_match_ptr("ldo-cp0"),
> + .regulators_node = of_match_ptr("regulators"),
[ ... ]
> + {
> + .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);
[Severity: Medium]
Is it possible to use standard APIs to access the required state rather than
exposing the bespoke parent struct p3h2x4x to the child drivers?
The MFD subsystem guidelines discourage exposing bespoke parent structures for
parent-child data sharing to maintain proper layering.
> + struct p3h2x4x_regulator_dev *p3h2x4x_regulator;
> + struct regulator_config rcfg = { };
> + struct device *dev = &pdev->dev;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=5
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:52 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:52 UTC (permalink / raw)
To: Lakshay Piplani
Cc: robh, conor+dt, Frank.Li, devicetree, linux-i3c,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] Driver-specific shared header `p3h2840.h` is improperly placed in the global `include/linux/mfd/` directory instead of being local to `drivers/mfd/`.
- [Low] `struct i2c_device_id` array uses a positional initializer instead of a named initializer.
--
--- Patch [4]: [PATCH v15 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator ---
commit b12649d8bd1516b7d9cfa69f8a2a7bc8f4a10164
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
This commit adds core MFD support for the NXP P3H2x4x family of multiport
I3C hub devices. It provides driver support using both I3C and I2C
communication interfaces, allowing sub-drivers to be enabled independently.
> diff --git a/drivers/mfd/p3h2840.c b/drivers/mfd/p3h2840.c
> new file mode 100644
> index 0000000000000..6c497d84e2bda
> --- /dev/null
> +++ b/drivers/mfd/p3h2840.c
[ ... ]
> +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[] = {
> + { "nxp-i3c-hub" },
[Severity: Low]
Does this code use a positional initializer instead of a named initializer?
According to I2C subsystem guidelines, initialized arrays of type
struct i2c_device_id must use named initializers (e.g., .name = "nxp-i3c-hub").
> + { /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(i2c, p3h2x4x_i2c_id_table);
[ ... ]
> 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
[ ... ]
> +struct p3h2x4x_i3c_hub_dev;
> +
> +struct p3h2x4x {
> + struct i3c_device *i3cdev;
> + struct regmap *regmap;
[Severity: Low]
Is it necessary to place this driver-specific shared header in the global
include/linux/mfd/ directory?
The MFD subsystem guidelines recommend keeping driver-specific header files
local to drivers/mfd/ if they are only used by the parent and its immediate
children.
> + /* 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 */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=4
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
@ 2026-08-17 10:52 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:52 UTC (permalink / raw)
To: Lakshay Piplani
Cc: robh, conor+dt, Frank.Li, devicetree, linux-i3c,
Alexandre Belloni
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] Driver-specific shared header `p3h2840.h` is improperly placed in the global `include/linux/mfd/` directory instead of being local to `drivers/mfd/`.
- [Low] `struct i2c_device_id` array uses a positional initializer instead of a named initializer.
--
--- Patch [4]: [PATCH v15 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator ---
commit b12649d8bd1516b7d9cfa69f8a2a7bc8f4a10164
Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator
This commit adds core MFD support for the NXP P3H2x4x family of multiport
I3C hub devices. It provides driver support using both I3C and I2C
communication interfaces, allowing sub-drivers to be enabled independently.
> diff --git a/drivers/mfd/p3h2840.c b/drivers/mfd/p3h2840.c
> new file mode 100644
> index 0000000000000..6c497d84e2bda
> --- /dev/null
> +++ b/drivers/mfd/p3h2840.c
[ ... ]
> +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[] = {
> + { "nxp-i3c-hub" },
[Severity: Low]
Does this code use a positional initializer instead of a named initializer?
According to I2C subsystem guidelines, initialized arrays of type
struct i2c_device_id must use named initializers (e.g., .name = "nxp-i3c-hub").
> + { /* sentinel */ }
> +};
> +MODULE_DEVICE_TABLE(i2c, p3h2x4x_i2c_id_table);
[ ... ]
> 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
[ ... ]
> +struct p3h2x4x_i3c_hub_dev;
> +
> +struct p3h2x4x {
> + struct i3c_device *i3cdev;
> + struct regmap *regmap;
[Severity: Low]
Is it necessary to place this driver-specific shared header in the global
include/linux/mfd/ directory?
The MFD subsystem guidelines recommend keeping driver-specific header files
local to drivers/mfd/ if they are only used by the parent and its immediate
children.
> + /* 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 */
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=4
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 10:53 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:53 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-i3c, conor+dt, Alexandre Belloni, devicetree, Frank.Li,
robh
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Delegating DAA to the parent controller incorrectly instantiates downstream devices on the parent bus instead of the virtual bus.
- [High] Address reservation incorrectly skips assigned addresses that differ from the static address, risking dynamic address collisions.
- [High] Freeing hub device data while IBI is active leaves dangling pointers in the parent controller.
- [Medium] Non-atomic overwrite of `parent_desc.info` causes data race with asynchronous IBI handlers.
- [Medium] Nested acquisition of identical lock class triggers lockdep recursive locking warning.
--
--- Patch [6]: [PATCH v15 6/8] i3c: hub: Add support for the I3C interface in the I3C hub ---
commit b929a913a5f82c8ea68936fda262a420d33a7ea4
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..04726393004b7
> --- /dev/null
> +++ b/drivers/i3c/hub.c
> @@ -0,0 +1,675 @@
[ ... ]
> +static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev)
> +{
[ ... ]
> + data->parent_desc.common.master = parent;
> + data->parent_desc.info = dev->info;
> +
> + i3c_bus_maintenance_lock(&parent->bus);
[Severity: Medium]
Will this trigger a lockdep warning for recursive locking?
The core I3C subsystem already holds the virtual controller's bus lock when
invoking operations like attach_i3c_dev or enable_ibi. Since all I3C bus
locks are initialized with the same lockdep class, acquiring the parent's
bus lock here looks like it might require subclass nesting annotations.
> + ret = i3c_master_attach_i3c_dev_controller(&data->parent_desc);
> + i3c_bus_maintenance_unlock(&parent->bus);
> + if (ret) {
[ ... ]
> +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev,
> + u8 old_dyn_addr)
> +{
[ ... ]
> + /* Re-sync device information after the address change. */
> + data->parent_desc.info = dev->info;
[Severity: Medium]
Is it safe to perform a non-atomic update of this structure without holding
the parent controller's bus lock or the hub routing lock?
If the device has an active IBI, could the physical parent controller's
asynchronous IBI interrupt handler concurrently read this descriptor and
see corrupted or torn state?
> +
> + i3c_bus_maintenance_lock(&parent->bus);
> + ret = i3c_master_reattach_i3c_dev_controller(&data->parent_desc,
[ ... ]
> +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev)
> +{
[ ... ]
> + /*
> + * The generic IBI lifecycle must be released before detaching the
> + * physical controller state.
> + */
> + WARN_ON(data->parent_desc.ibi);
> +
> + if (parent) {
> + i3c_bus_maintenance_lock(&parent->bus);
> + i3c_master_detach_i3c_dev_controller(&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: High]
If data->parent_desc.ibi is non-NULL (triggering the warning), does
proceeding to kfree(data) create a use-after-free?
Because i3c_master_detach_i3c_dev_controller() does not automatically flush
or free the generic IBI workqueue resources, lingering asynchronous IBI work
items queued on the parent controller might dereference data->parent_desc
after it is freed.
Should the active IBI resources be explicitly flushed and released before
freeing this memory?
> +}
[ ... ]
> +static int i3c_hub_do_daa(struct i3c_hub *hub,
> + struct i3c_master_controller *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 calling i3c_master_do_daa() on the parent controller cause newly
discovered downstream devices to be instantiated on the parent bus rather
than the virtual hub bus?
If they are bound to the parent controller, won't subsequent client transfers
go directly to parent->ops->i3c_xfers, entirely bypassing
i3c_hub_master_priv_xfers() and hitting the closed hub port?
> + 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)
> +{
[ ... ]
> + ret = of_property_read_u32(child, "assigned-address", &assigned_addr);
> + if (ret)
> + continue;
> +
> + static_addr = reg[0];
> +
> + if (!static_addr || !assigned_addr)
> + continue;
> +
> + if (static_addr != assigned_addr)
> + continue;
[Severity: High]
Why do we skip reserving the assigned address if it differs from the static
address?
If static_addr != assigned_addr (for example, expecting assignment via
SETDASA or SETNEWDA), this leaves the assigned address marked as free on
the parent bus. Could this allow the parent controller to accidentally
allocate this assigned address to another device during ENTDAA, causing a
collision?
> +
> + i3c_bus_maintenance_lock(&parent->bus);
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=6
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 6/8] i3c: hub: Add support for the I3C interface in the I3C hub
@ 2026-08-17 10:53 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 10:53 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-i3c, conor+dt, Alexandre Belloni, devicetree, Frank.Li,
robh
Thank you for your contribution! Sashiko AI review found 5 potential issue(s) to consider:
- [High] Delegating DAA to the parent controller incorrectly instantiates downstream devices on the parent bus instead of the virtual bus.
- [High] Address reservation incorrectly skips assigned addresses that differ from the static address, risking dynamic address collisions.
- [High] Freeing hub device data while IBI is active leaves dangling pointers in the parent controller.
- [Medium] Non-atomic overwrite of `parent_desc.info` causes data race with asynchronous IBI handlers.
- [Medium] Nested acquisition of identical lock class triggers lockdep recursive locking warning.
--
--- Patch [6]: [PATCH v15 6/8] i3c: hub: Add support for the I3C interface in the I3C hub ---
commit b929a913a5f82c8ea68936fda262a420d33a7ea4
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..04726393004b7
> --- /dev/null
> +++ b/drivers/i3c/hub.c
> @@ -0,0 +1,675 @@
[ ... ]
> +static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev)
> +{
[ ... ]
> + data->parent_desc.common.master = parent;
> + data->parent_desc.info = dev->info;
> +
> + i3c_bus_maintenance_lock(&parent->bus);
[Severity: Medium]
Will this trigger a lockdep warning for recursive locking?
The core I3C subsystem already holds the virtual controller's bus lock when
invoking operations like attach_i3c_dev or enable_ibi. Since all I3C bus
locks are initialized with the same lockdep class, acquiring the parent's
bus lock here looks like it might require subclass nesting annotations.
> + ret = i3c_master_attach_i3c_dev_controller(&data->parent_desc);
> + i3c_bus_maintenance_unlock(&parent->bus);
> + if (ret) {
[ ... ]
> +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev,
> + u8 old_dyn_addr)
> +{
[ ... ]
> + /* Re-sync device information after the address change. */
> + data->parent_desc.info = dev->info;
[Severity: Medium]
Is it safe to perform a non-atomic update of this structure without holding
the parent controller's bus lock or the hub routing lock?
If the device has an active IBI, could the physical parent controller's
asynchronous IBI interrupt handler concurrently read this descriptor and
see corrupted or torn state?
> +
> + i3c_bus_maintenance_lock(&parent->bus);
> + ret = i3c_master_reattach_i3c_dev_controller(&data->parent_desc,
[ ... ]
> +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev)
> +{
[ ... ]
> + /*
> + * The generic IBI lifecycle must be released before detaching the
> + * physical controller state.
> + */
> + WARN_ON(data->parent_desc.ibi);
> +
> + if (parent) {
> + i3c_bus_maintenance_lock(&parent->bus);
> + i3c_master_detach_i3c_dev_controller(&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: High]
If data->parent_desc.ibi is non-NULL (triggering the warning), does
proceeding to kfree(data) create a use-after-free?
Because i3c_master_detach_i3c_dev_controller() does not automatically flush
or free the generic IBI workqueue resources, lingering asynchronous IBI work
items queued on the parent controller might dereference data->parent_desc
after it is freed.
Should the active IBI resources be explicitly flushed and released before
freeing this memory?
> +}
[ ... ]
> +static int i3c_hub_do_daa(struct i3c_hub *hub,
> + struct i3c_master_controller *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 calling i3c_master_do_daa() on the parent controller cause newly
discovered downstream devices to be instantiated on the parent bus rather
than the virtual hub bus?
If they are bound to the parent controller, won't subsequent client transfers
go directly to parent->ops->i3c_xfers, entirely bypassing
i3c_hub_master_priv_xfers() and hitting the closed hub port?
> + 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)
> +{
[ ... ]
> + ret = of_property_read_u32(child, "assigned-address", &assigned_addr);
> + if (ret)
> + continue;
> +
> + static_addr = reg[0];
> +
> + if (!static_addr || !assigned_addr)
> + continue;
> +
> + if (static_addr != assigned_addr)
> + continue;
[Severity: High]
Why do we skip reserving the assigned address if it differs from the static
address?
If static_addr != assigned_addr (for example, expecting assignment via
SETDASA or SETNEWDA), this leaves the assigned address marked as free on
the parent bus. Could this allow the parent controller to accidentally
allocate this assigned address to another device during ENTDAA, causing a
collision?
> +
> + i3c_bus_maintenance_lock(&parent->bus);
[ ... ]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=6
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support
2026-08-17 10:38 ` Lakshay Piplani
@ 2026-08-17 11:04 ` sashiko-bot
-1 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 11:04 UTC (permalink / raw)
To: Lakshay Piplani
Cc: devicetree, robh, Alexandre Belloni, linux-i3c, Frank.Li,
conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Failing to clear the software slave state on hardware disable failure causes a use-after-free or invalid code execution when the slave driver unloads.
- [High] A stack-allocated buffer is passed to `regmap_bulk_read()` for an I3C device, violating the I3C core's requirement for DMA-able buffers and leading to DMA corruption.
- [Low] Missing I2C_FUNC_SLAVE flag in adapter functionality mask when CONFIG_I2C_SLAVE is enabled.
--
--- Patch [8]: [PATCH v15 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support ---
commit 1191525da64840a794eb47502525402f71bb5f79
Author: Lakshay Piplani <lakshay.piplani@nxp.com>
i3c: hub: p3h2x4x: Add SMBus slave mode support
This commit adds 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.
> diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> index f6d4ade506e25..c095ae524e664 100644
> --- a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> +++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
[ ... ]
> @@ -15,6 +17,177 @@ 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 slave_rx_buffer[P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 };
[ ... ]
> + /* read data */
> + if (packet_len) {
> + ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_DATA,
> + slave_rx_buffer, packet_len);
[Severity: High]
Is it safe to pass the stack-allocated slave_rx_buffer directly to
regmap_bulk_read()?
Since the underlying regmap is for an I3C device, this resolves to
regmap_i3c_read(), which maps the user buffer into an i3c_xfer struct.
The I3C subsystem mandates that buffers passed to transfers must be DMA-able.
Could passing a stack buffer here cause DMA corruption or kernel panics on
systems where the stack is virtually mapped?
> + if (ret)
> + goto ibi_err;
> + }
> +
> + client = p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client;
> + if (!client)
> + goto ibi_err;
[ ... ]
> @@ -206,6 +379,115 @@ static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter)
> return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
> }
[Severity: Low]
Should I2C_FUNC_SLAVE be included in this mask when CONFIG_I2C_SLAVE is
enabled?
Without it, userspace tools and subsystems querying the adapter capabilities
via the I2C_FUNCS ioctl might incorrectly assume the adapter does not support
slave mode.
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> +static int p3h2x4x_tp_i2c_reg_slave(struct i2c_client *slave)
[ ... ]
> +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);
> + }
> +
> + /*
> + * A relock failure does not re-enable the IBI, 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;
[Severity: High]
If ret is non-zero (for example due to an I3C bus error), this returns early
without clearing bus->tp_smbus_client.
The i2c_slave_unregister() core function propagates the error, but the driver
will typically unbind anyway, freeing its private data and potentially
unloading the module.
If an IBI arrives later, could p3h2x4x_read_smbus_agent_rx_buf() see the
dangling tp_smbus_client pointer and call a freed slave_cb via
i2c_slave_event(), resulting in a use-after-free or kernel panic?
Should bus->tp_smbus_client be cleared unconditionally?
> +
> + /*
> + * Clear the software state only after the hardware IBI has been
> + * disabled successfully.
> + */
> + bus->tp_smbus_client = NULL;
> + hub->hub_config.tp_config[bus->tp_port].ibi_en = false;
> +
> + return 0;
> +}
> +#endif
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=8
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support
@ 2026-08-17 11:04 ` sashiko-bot
0 siblings, 0 replies; 38+ messages in thread
From: sashiko-bot @ 2026-08-17 11:04 UTC (permalink / raw)
To: Lakshay Piplani
Cc: devicetree, robh, Alexandre Belloni, linux-i3c, Frank.Li,
conor+dt
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] Failing to clear the software slave state on hardware disable failure causes a use-after-free or invalid code execution when the slave driver unloads.
- [High] A stack-allocated buffer is passed to `regmap_bulk_read()` for an I3C device, violating the I3C core's requirement for DMA-able buffers and leading to DMA corruption.
- [Low] Missing I2C_FUNC_SLAVE flag in adapter functionality mask when CONFIG_I2C_SLAVE is enabled.
--
--- Patch [8]: [PATCH v15 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support ---
commit 1191525da64840a794eb47502525402f71bb5f79
Author: Lakshay Piplani <lakshay.piplani@nxp.com>
i3c: hub: p3h2x4x: Add SMBus slave mode support
This commit adds 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.
> diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> index f6d4ade506e25..c095ae524e664 100644
> --- a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
> +++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
[ ... ]
> @@ -15,6 +17,177 @@ 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 slave_rx_buffer[P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 };
[ ... ]
> + /* read data */
> + if (packet_len) {
> + ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_DATA,
> + slave_rx_buffer, packet_len);
[Severity: High]
Is it safe to pass the stack-allocated slave_rx_buffer directly to
regmap_bulk_read()?
Since the underlying regmap is for an I3C device, this resolves to
regmap_i3c_read(), which maps the user buffer into an i3c_xfer struct.
The I3C subsystem mandates that buffers passed to transfers must be DMA-able.
Could passing a stack buffer here cause DMA corruption or kernel panics on
systems where the stack is virtually mapped?
> + if (ret)
> + goto ibi_err;
> + }
> +
> + client = p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client;
> + if (!client)
> + goto ibi_err;
[ ... ]
> @@ -206,6 +379,115 @@ static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter)
> return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
> }
[Severity: Low]
Should I2C_FUNC_SLAVE be included in this mask when CONFIG_I2C_SLAVE is
enabled?
Without it, userspace tools and subsystems querying the adapter capabilities
via the I2C_FUNCS ioctl might incorrectly assume the adapter does not support
slave mode.
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> +static int p3h2x4x_tp_i2c_reg_slave(struct i2c_client *slave)
[ ... ]
> +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);
> + }
> +
> + /*
> + * A relock failure does not re-enable the IBI, 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;
[Severity: High]
If ret is non-zero (for example due to an I3C bus error), this returns early
without clearing bus->tp_smbus_client.
The i2c_slave_unregister() core function propagates the error, but the driver
will typically unbind anyway, freeing its private data and potentially
unloading the module.
If an IBI arrives later, could p3h2x4x_read_smbus_agent_rx_buf() see the
dangling tp_smbus_client pointer and call a freed slave_cb via
i2c_slave_event(), resulting in a use-after-free or kernel panic?
Should bus->tp_smbus_client be cleared unconditionally?
> +
> + /*
> + * Clear the software state only after the hardware IBI has been
> + * disabled successfully.
> + */
> + bus->tp_smbus_client = NULL;
> + hub->hub_config.tp_config[bus->tp_port].ibi_en = false;
> +
> + return 0;
> +}
> +#endif
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=8
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support
2026-08-17 10:50 ` sashiko-bot
@ 2026-08-17 20:02 ` Frank Li
-1 siblings, 0 replies; 38+ messages in thread
From: Frank Li @ 2026-08-17 20:02 UTC (permalink / raw)
To: sashiko-reviews
Cc: Lakshay Piplani, devicetree, Alexandre Belloni, robh, Frank.Li,
conor+dt, linux-i3c
On Mon, Aug 17, 2026 at 10:50:58AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
>
> New issues:
> - [Medium] The commit message claims to add helpers for attaching and detaching I3C devices, but the implementation is completely missing.
Please update commit message
Frank
>
> Pre-existing issues:
> - [High] Accidental removal of ID 0 from `i3c_bus_idr` when `i3c_bus_init()` fails.
> --
>
> --- Patch [1]: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support ---
> commit 14f349144e98c1c3eedcaa4123a53bf24704569f
> Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
>
> i3c: master: Add APIs for I3C hub support
>
> This commit adds helpers to prepare for I3C hub support. It exposes
> address slot management and CCC command interfaces, and allows registering
> virtual masters with an explicit firmware node.
>
> > Add helpers for attaching and detaching I3C devices and CCC helpers
> > to check CCC support and send CCC commands, address slot helpers to
> > query and update I3C bus address slot state, registering virtual
>
> [Severity: Medium]
> The commit message claims to add helpers for attaching and detaching I3C
> devices, but the implementation is completely missing from the patch.
>
> The changelog indicates that the attach and detach helpers were dropped in
> v15. Should the commit message be updated to reflect this change?
>
> > 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
>
> [ ... ]
>
> > @@ -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 isn't a bug introduced by this patch, but does the error handling in
> i3c_master_register_fwnode() corrupt the global IDR state?
>
> If i3c_bus_init() fails at idr_alloc() (e.g., due to duplicate alias in DT),
> it returns an error while leaving i3cbus->id as 0:
>
> drivers/i3c/master.c:i3c_bus_init() {
> ...
> ret = idr_alloc(&i3c_bus_idr, i3cbus, start, end, GFP_KERNEL);
> if (ret < 0)
> return ret;
> ...
> }
>
> Taking the goto err_put_dev path then calls put_device(&master->dev), which
> eventually triggers i3c_masterdev_release():
>
> drivers/i3c/master.c:i3c_masterdev_release() {
> ...
> i3c_bus_cleanup(bus);
> ...
> }
>
> This calls idr_remove(&i3c_bus_idr, i3cbus->id). Because i3cbus->id is still 0,
> doesn't this accidentally remove the valid bus 0 from the IDR tree?
>
> This could lead to duplicate sysfs names ('i3c-0') and crashes during
> device_add() for a new bus.
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=1
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support
@ 2026-08-17 20:02 ` Frank Li
0 siblings, 0 replies; 38+ messages in thread
From: Frank Li @ 2026-08-17 20:02 UTC (permalink / raw)
To: sashiko-reviews
Cc: Lakshay Piplani, devicetree, Alexandre Belloni, robh, Frank.Li,
conor+dt, linux-i3c
On Mon, Aug 17, 2026 at 10:50:58AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
>
> New issues:
> - [Medium] The commit message claims to add helpers for attaching and detaching I3C devices, but the implementation is completely missing.
Please update commit message
Frank
>
> Pre-existing issues:
> - [High] Accidental removal of ID 0 from `i3c_bus_idr` when `i3c_bus_init()` fails.
> --
>
> --- Patch [1]: [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support ---
> commit 14f349144e98c1c3eedcaa4123a53bf24704569f
> Author: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
>
> i3c: master: Add APIs for I3C hub support
>
> This commit adds helpers to prepare for I3C hub support. It exposes
> address slot management and CCC command interfaces, and allows registering
> virtual masters with an explicit firmware node.
>
> > Add helpers for attaching and detaching I3C devices and CCC helpers
> > to check CCC support and send CCC commands, address slot helpers to
> > query and update I3C bus address slot state, registering virtual
>
> [Severity: Medium]
> The commit message claims to add helpers for attaching and detaching I3C
> devices, but the implementation is completely missing from the patch.
>
> The changelog indicates that the attach and detach helpers were dropped in
> v15. Should the commit message be updated to reflect this change?
>
> > 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
>
> [ ... ]
>
> > @@ -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 isn't a bug introduced by this patch, but does the error handling in
> i3c_master_register_fwnode() corrupt the global IDR state?
>
> If i3c_bus_init() fails at idr_alloc() (e.g., due to duplicate alias in DT),
> it returns an error while leaving i3cbus->id as 0:
>
> drivers/i3c/master.c:i3c_bus_init() {
> ...
> ret = idr_alloc(&i3c_bus_idr, i3cbus, start, end, GFP_KERNEL);
> if (ret < 0)
> return ret;
> ...
> }
>
> Taking the goto err_put_dev path then calls put_device(&master->dev), which
> eventually triggers i3c_masterdev_release():
>
> drivers/i3c/master.c:i3c_masterdev_release() {
> ...
> i3c_bus_cleanup(bus);
> ...
> }
>
> This calls idr_remove(&i3c_bus_idr, i3cbus->id). Because i3cbus->id is still 0,
> doesn't this accidentally remove the valid bus 0 from the IDR tree?
>
> This could lead to duplicate sysfs names ('i3c-0') and crashes during
> device_add() for a new bus.
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=1
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers
2026-08-17 10:50 ` sashiko-bot
@ 2026-08-17 20:08 ` Frank Li
-1 siblings, 0 replies; 38+ messages in thread
From: Frank Li @ 2026-08-17 20:08 UTC (permalink / raw)
To: sashiko-reviews
Cc: Lakshay Piplani, devicetree, robh, Frank.Li, conor+dt, linux-i3c,
Alexandre Belloni
On Mon, Aug 17, 2026 at 10:50:59AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
>
> New issues:
> - [Low] Incomplete refactoring: `i3c_master_handle_ibi()` bypasses the newly introduced `i3c_dev_recycle_ibi_slot_controller()` helper.
>
> Pre-existing issues:
> - [High] Missing memory barrier in `i3c_dev_disable_ibi_locked` causes a lost wakeup and indefinite hang.
> - [High] Use-after-free on error path in `i3c_dev_free_ibi_locked` when workqueue is drained after resources are freed.
> --
>
> --- Patch [2]: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers ---
> commit 85e9cb3ce264f7889b2b4406bc379307ad054a14
> 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. 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 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.
>
> [Severity: Low]
> The commit message mentions refactoring the existing generic core paths to
> use the new controller-only helpers, including the one for recycling IBI
> slots.
>
> However, looking at the code for i3c_master_handle_ibi(), it appears to
> still bypass the newly introduced wrapper:
>
> drivers/i3c/master.c:i3c_master_handle_ibi() {
> ...
> if (master->ops->recycle_ibi_slot)
> master->ops->recycle_ibi_slot(dev, slot);
> ...
> }
>
> Does this function need to be updated to use
> i3c_dev_recycle_ibi_slot_controller() to ensure the virtual controllers
> in the new I3C hub framework can properly intercept these operations?
Yes, please update it also.
Frank
>
> > diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
> > index 03fb41f0786c5..7ae6b3aaa9b4c 100644
> > --- a/drivers/i3c/master.c
> > +++ b/drivers/i3c/master.c
>
> [ ... ]
>
> > @@ -3534,14 +3632,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;
>
> [Severity: High]
> This is a pre-existing issue, but does this sequence lack a memory barrier
> before checking the pending IBIs?
>
> Right after this return, the code does:
>
> drivers/i3c/master.c:i3c_dev_disable_ibi_locked() {
> ...
> reinit_completion(&dev->ibi->all_ibis_handled);
> if (atomic_read(&dev->ibi->pending_ibis))
> wait_for_completion(&dev->ibi->all_ibis_handled);
> ...
> }
>
> Since reinit_completion() performs a plain store that can be delayed or
> reordered after the atomic_read(), could a concurrent IBI worker
> decrementing the pending count and calling complete() race with this?
>
> If the worker executes between the load of pending_ibis and the delayed
> store, could the store overwrite the completion state back to 0, causing
> wait_for_completion() to hang indefinitely?
>
> [ ... ]
>
> > @@ -3667,7 +3835,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: High]
> This is a pre-existing issue, but does this cleanup order create a
> use-after-free risk during an error path?
>
> If disabling the IBI fails (for example, due to a hardware timeout), the
> error path continues and calls i3c_dev_free_ibi_controller_locked(),
> which frees the underlying controller resources like the generic IBI pool.
>
> Then destroy_workqueue() is called to drain the workqueue. If there are
> pending or executing IBI works still in the workqueue, won't they try to
> access the freed slot pool when they run, resulting in a use-after-free?
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=2
--
linux-i3c mailing list
linux-i3c@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-i3c
^ permalink raw reply [flat|nested] 38+ messages in thread
* Re: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers
@ 2026-08-17 20:08 ` Frank Li
0 siblings, 0 replies; 38+ messages in thread
From: Frank Li @ 2026-08-17 20:08 UTC (permalink / raw)
To: sashiko-reviews
Cc: Lakshay Piplani, devicetree, robh, Frank.Li, conor+dt, linux-i3c,
Alexandre Belloni
On Mon, Aug 17, 2026 at 10:50:59AM +0000, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
>
> New issues:
> - [Low] Incomplete refactoring: `i3c_master_handle_ibi()` bypasses the newly introduced `i3c_dev_recycle_ibi_slot_controller()` helper.
>
> Pre-existing issues:
> - [High] Missing memory barrier in `i3c_dev_disable_ibi_locked` causes a lost wakeup and indefinite hang.
> - [High] Use-after-free on error path in `i3c_dev_free_ibi_locked` when workqueue is drained after resources are freed.
> --
>
> --- Patch [2]: [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers ---
> commit 85e9cb3ce264f7889b2b4406bc379307ad054a14
> 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. 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 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.
>
> [Severity: Low]
> The commit message mentions refactoring the existing generic core paths to
> use the new controller-only helpers, including the one for recycling IBI
> slots.
>
> However, looking at the code for i3c_master_handle_ibi(), it appears to
> still bypass the newly introduced wrapper:
>
> drivers/i3c/master.c:i3c_master_handle_ibi() {
> ...
> if (master->ops->recycle_ibi_slot)
> master->ops->recycle_ibi_slot(dev, slot);
> ...
> }
>
> Does this function need to be updated to use
> i3c_dev_recycle_ibi_slot_controller() to ensure the virtual controllers
> in the new I3C hub framework can properly intercept these operations?
Yes, please update it also.
Frank
>
> > diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c
> > index 03fb41f0786c5..7ae6b3aaa9b4c 100644
> > --- a/drivers/i3c/master.c
> > +++ b/drivers/i3c/master.c
>
> [ ... ]
>
> > @@ -3534,14 +3632,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;
>
> [Severity: High]
> This is a pre-existing issue, but does this sequence lack a memory barrier
> before checking the pending IBIs?
>
> Right after this return, the code does:
>
> drivers/i3c/master.c:i3c_dev_disable_ibi_locked() {
> ...
> reinit_completion(&dev->ibi->all_ibis_handled);
> if (atomic_read(&dev->ibi->pending_ibis))
> wait_for_completion(&dev->ibi->all_ibis_handled);
> ...
> }
>
> Since reinit_completion() performs a plain store that can be delayed or
> reordered after the atomic_read(), could a concurrent IBI worker
> decrementing the pending count and calling complete() race with this?
>
> If the worker executes between the load of pending_ibis and the delayed
> store, could the store overwrite the completion state back to 0, causing
> wait_for_completion() to hang indefinitely?
>
> [ ... ]
>
> > @@ -3667,7 +3835,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: High]
> This is a pre-existing issue, but does this cleanup order create a
> use-after-free risk during an error path?
>
> If disabling the IBI fails (for example, due to a hardware timeout), the
> error path continues and calls i3c_dev_free_ibi_controller_locked(),
> which frees the underlying controller resources like the generic IBI pool.
>
> Then destroy_workqueue() is called to drain the workqueue. If there are
> pending or executing IBI works still in the workqueue, won't they try to
> access the freed slot pool when they run, resulting in a use-after-free?
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/20260817103844.2142802-1-lakshay.piplani@nxp.com?part=2
^ permalink raw reply [flat|nested] 38+ messages in thread
end of thread, other threads:[~2026-08-17 20:09 UTC | newest]
Thread overview: 38+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-17 10:38 [PATCH v15 0/8] Add support for NXP P3H2x4x I3C hub driver Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:38 ` [PATCH v15 1/8] i3c: master: Add APIs for I3C hub support Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:50 ` sashiko-bot
2026-08-17 10:50 ` sashiko-bot
2026-08-17 20:02 ` Frank Li
2026-08-17 20:02 ` Frank Li
2026-08-17 10:38 ` [PATCH v15 2/8] i3c: master: Add controller-only device operation helpers Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:50 ` sashiko-bot
2026-08-17 10:50 ` sashiko-bot
2026-08-17 20:08 ` Frank Li
2026-08-17 20:08 ` Frank Li
2026-08-17 10:38 ` [PATCH v15 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:49 ` sashiko-bot
2026-08-17 10:49 ` sashiko-bot
2026-08-17 10:38 ` [PATCH v15 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:52 ` sashiko-bot
2026-08-17 10:52 ` sashiko-bot
2026-08-17 10:38 ` [PATCH v15 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:51 ` sashiko-bot
2026-08-17 10:51 ` sashiko-bot
2026-08-17 10:38 ` [PATCH v15 6/8] i3c: hub: Add support for the I3C interface in the I3C hub Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:53 ` sashiko-bot
2026-08-17 10:53 ` sashiko-bot
2026-08-17 10:38 ` [PATCH v15 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 10:51 ` sashiko-bot
2026-08-17 10:51 ` sashiko-bot
2026-08-17 10:38 ` [PATCH v15 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support Lakshay Piplani
2026-08-17 10:38 ` Lakshay Piplani
2026-08-17 11:04 ` sashiko-bot
2026-08-17 11:04 ` sashiko-bot
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.