From nobody Thu Sep 24 16:08:57 2026 Received: from inva021.nxp.com (inva021.nxp.com [92.121.34.21]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5D7625187C0; Tue, 22 Sep 2026 10:36:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.21 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073376; cv=none; b=lkvOGSFEp45j2A5e2R4+tEWzLbffBqQ0oIK+IpwCLKomZXuGF7AE8F4vpKhAfdZsXjCWZbH7+axRYLkqgpVBzRdUtjItlUXD4fAHoq1TKBeuXF+szQjGxFuvG3OJCYiMJcpDcGxMXIG8Ye54yFJ5rElKMLG6PhzvOSEVPYSUny8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073376; c=relaxed/simple; bh=gUB/F4proIdrRAQ40p0HffKRcH8IceT4yfjhQblF62s=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=ty6LCQNXE1sVpbHj6wMNjadatjC2BNOC37PDyJTasXOhrDZYsoCHz5BjtCGQiqC+RrtDAOcFGGJkhX2CgDRfWnXIIBPpqmjDDmPwNsle/PoGGonQiY487S01ngfU24TM8MQAW3EcjtPW+Y7rv6/KEBbs5G+v3VRzpVd7yJ/RU4U= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.21 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva021.nxp.com (localhost [127.0.0.1]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 6CDB920000F; Tue, 22 Sep 2026 12:36:05 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 0CE00200006; Tue, 22 Sep 2026 12:36:05 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id 93D1818000BA; Tue, 22 Sep 2026 18:36:01 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani Subject: [PATCH v20 1/8] i3c: master: Add APIs for I3C hub support Date: Tue, 22 Sep 2026 16:05:43 +0530 Message-Id: <20260922103551.2754613-2-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" From: Aman Kumar Pandey Add CCC helpers to check CCC support and send CCC commands, address slot helpers to query and update I3C bus address slot state, registering virtual masters with an explicit firmware node, and exposing the bus maintenance lock helpers. These additions prepare for I3C hub support. A hub driver needs to reserve and query parent bus address slots, forward CCC commands, register virtual target port controllers using the target-port firmware node, and serialize operations against the parent bus maintenance lock. The hub also forwards private transfers via i3c_dev_do_xfers_locked() and serializes its IBI and private-transfer paths against the shared lock, so the normal-use lock/unlock pair is exposed alongside the maintenance-lock helpers. i3c_master_register_fwnode() allows virtual I3C masters to register using a firmware node different from their parent device node without temporarily modifying parent->of_node. The new helpers are: 1) i3c_master_send_ccc_cmd() 2) i3c_master_supports_ccc_cmd() 3) i3c_bus_get_addr_slot_status() 4) i3c_bus_set_addr_slot_status() 5) i3c_bus_maintenance_lock() 6) i3c_bus_maintenance_unlock() 7) i3c_master_register_fwnode() 8) i3c_bus_normaluse_lock() 9) i3c_bus_normaluse_unlock() 10) i3c_dev_do_xfers_locked() Signed-off-by: Aman Kumar Pandey Signed-off-by: Lakshay Piplani Signed-off-by: Vikash Bansal Reviewed-by: Frank Li --- Changes in v20: - No change Changes in v19: - No change Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes - Adapted i3c_master_register_fwnode() to the generic fwnode handling introduced in v7.3-rc1 Changes in v17: - No code changes - Added Frank Li's Reviewed-by tag Changes in v16: - Rewrite the commit message to match the code, It now describes only the helpers actually exported Changes in v15: - Drop the direct attach and detach helpers that also modified address-slot state - Export these APIs: - i3c_bus_normaluse_lock() - i3c_bus_normaluse_unlock() - i3c_dev_do_xfers_locked() Changes in v14: - Add i3c_master_register_fwnode() to register virtual I3C masters with an explicit firmware node - Export i3c_bus_maintenance_lock() and i3c_bus_maintenance_unlock() - Add runtime PM get/put around i3c_master_send_ccc_cmd() - Make i3c_master_supports_ccc_cmd() return false when the controller does not implement send_ccc_cmd() Changes in v13: - Fix address handling in direct attach by using i3c_master_get_i3c_addrs(= ) and adding rollback on failure to prevent bus address collisions - Fix detach path by clearing master_priv and releasing addresses to avoid= use-after-free and stale state issues - Export address slot helper APIs and add kernel-doc for them Changes in v12: - Add address check in i3c_master_direct_detach_i3c_dev_locked() to skip detach for unaddressed devices. Changes in v11: - Convert i3c_master_supports_ccc_cmd() to return bool and align semantics with CCC support checks used by the I3C core Changes in v10: - Rename i3c_master_direct_attach_i3c_dev and i3c_master_direct_detach_i3c= _dev APIs to *_locked, as these APIs must be called with the bus lock held in write mode Changes in v9: - No change Changes in v8: - No change Changes in v7: - Update commit message to clarify purpose (prepare for I3C hub support) Changes in v6: - Split the patch into two parts: 1) expose the existing API 2) add new APIs. --- --- drivers/i3c/master.c | 146 +++++++++++++++++++++++++++++++------ include/linux/i3c/master.h | 17 +++++ 2 files changed, 139 insertions(+), 24 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index afcd7a21a3e6..9ea4726495ff 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -49,10 +49,11 @@ static BLOCKING_NOTIFIER_HEAD(i3c_bus_notifier); * logic to rely on I3C device information that could be changed behind th= eir * 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); =20 /** * i3c_bus_maintenance_unlock - Release the bus lock after a maintenance @@ -63,10 +64,11 @@ static void i3c_bus_maintenance_lock(struct i3c_bus *bu= s) * 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); =20 /** * i3c_bus_normaluse_lock - Lock the bus for a normal operation @@ -88,6 +90,7 @@ void i3c_bus_normaluse_lock(struct i3c_bus *bus) { down_read(&bus->lock); } +EXPORT_SYMBOL_GPL(i3c_bus_normaluse_lock); =20 /** * i3c_bus_normaluse_unlock - Release the bus lock after a normal operation @@ -101,6 +104,7 @@ void i3c_bus_normaluse_unlock(struct i3c_bus *bus) { up_read(&bus->lock); } +EXPORT_SYMBOL_GPL(i3c_bus_normaluse_unlock); =20 static struct i3c_master_controller *dev_to_i3cmaster(struct device *dev) { @@ -402,11 +406,19 @@ i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus= , u16 addr, u32 mask) return status & mask; } =20 -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); =20 static void i3c_bus_set_addr_slot_status_mask(struct i3c_bus *bus, u16 add= r, enum i3c_addr_slot_status status, u32 mask) @@ -422,11 +434,18 @@ static void i3c_bus_set_addr_slot_status_mask(struct = i3c_bus *bus, u16 addr, *ptr |=3D ((unsigned long)status & mask) << (bitpos % BITS_PER_LONG); } =20 -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); =20 static bool i3c_bus_dev_addr_is_avail(struct i3c_bus *bus, u8 addr) { @@ -2812,6 +2831,59 @@ static void i3c_master_reconcile_dyn_addrs(struct i3= c_master_controller *master) } } =20 +/** + * 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 t= he + * 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 =3D i3c_master_rpm_get(master); + if (ret) + return ret; + + i3c_bus_maintenance_lock(&master->bus); + ret =3D 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 @@ -3634,30 +3706,23 @@ static int i3c_master_check_ops(const struct i3c_ma= ster_controller_ops *ops) } =20 /** - * i3c_master_register() - register an I3C master + * i3c_master_register_fwnode() - register an I3C master with a custom fwn= ode * @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: - * - * - 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 + * This helper is useful for virtual I3C masters whose firmware node is not + * the same as @parent's firmware node. * * 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 =3D I3C_BUS_I2C_FM_PLUS_SCL_MAX_RATE; struct i3c_bus *i3cbus =3D i3c_master_get_bus(master); @@ -3674,7 +3739,7 @@ int i3c_master_register(struct i3c_master_controller = *master, return ret; =20 master->dev.parent =3D parent; - device_set_node(&master->dev, fwnode_handle_get(dev_fwnode(parent))); + device_set_node(&master->dev, fwnode_handle_get(fwnode)); master->dev.bus =3D &i3c_bus_type; master->dev.type =3D &i3c_masterdev_type; master->dev.release =3D i3c_masterdev_release; @@ -3792,6 +3857,38 @@ int i3c_master_register(struct i3c_master_controller= *master, =20 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, + dev_fwnode(parent), + ops, secondary); +} EXPORT_SYMBOL_GPL(i3c_master_register); =20 /** @@ -3852,6 +3949,7 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev,= struct i3c_xfer *xfers, =20 return master->ops->i3c_xfers(dev, xfers, nxfers, mode); } +EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked); =20 /** * i3c_dev_disable_ibi_locked() - Disable IBIs coming from a specific devi= ce diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index f7ceec2b4477..10876d9ac38c 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -649,9 +649,18 @@ DEFINE_FREE(i3c_master_dma_unmap_single, void *, =20 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); =20 +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, @@ -775,4 +784,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); =20 +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 */ --=20 2.25.1 From nobody Thu Sep 24 16:08:57 2026 Received: from inva021.nxp.com (inva021.nxp.com [92.121.34.21]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CE6195158A5; Tue, 22 Sep 2026 10:36:11 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.21 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073375; cv=none; b=s8EcpjLBWtiMYbXpWS65Y85t3dYsOvCMT0Gipo97JO68Y+KS2rBJ+BfaIbPdmec5xszWjD/bCMkXrQF95SAXZHxSleR00lqZp+zDpIwh37pgVEfmDg04R38XkZG9WxZeuAB1CwCwJa8XUzb0LEgFTOGT6PPjPzdT3Rf75je7xn0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073375; c=relaxed/simple; bh=AzUZ8MRPrQZyIPHeAatGJWIWcNkEKOdNm5odTDb0S7g=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=eyT0wKtcsMUCjhG2zE8yptKLIAJV/N1mYKZ6vOMf/QdZVAtyerdK9FiVlTSnxgjwBxoaNCRD0V0QskINrubUr6rBVxSO4jrbdRTtHJ4Py5cIQMi1M4NA9ooAoXUM+4FV2HcrTS0vGB/pJ7jPwhoRv2W0qzvTUbEkfKMlTfpTpYc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.21 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva021.nxp.com (localhost [127.0.0.1]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 88131200027; Tue, 22 Sep 2026 12:36:09 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 28568200025; Tue, 22 Sep 2026 12:36:09 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id A6CC01800068; Tue, 22 Sep 2026 18:36:05 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani Subject: [PATCH v20 2/8] i3c: master: Add controller-only device operation helpers Date: Tue, 22 Sep 2026 16:05:44 +0530 Message-Id: <20260922103551.2754613-3-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" 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 Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Reviewed-by: Frank Li --- Changes in v20: - No change Changes in v19: - No change Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes Changes in v17: - Document the calling context of i3c_dev_recycle_ibi_slot_controller(): it runs in workqueue context without the bus lock held, while the controller owns and synchronizes its IBI pool - Retain the request_ibi capability check to preserve the pre-series behaviour for regular I3C devices Changes in v16: - Rename the controller-only device helpers with a "_locked" suffix to make the locking contract explicit (caller must hold the parent bus lock) - Route i3c_master_handle_ibi() through i3c_dev_recycle_ibi_slot_controlle= r() instead of calling master->ops->recycle_ibi_slot() directly - Destroy the generic IBI workqueue when the controller request_ibi() callback fails, avoiding a workqueue leak on the request error path Changes in v15: - Rework the patch to introduce controller-only attach, reattach and detach helpers for use by the I3C hub core - Add controller-only helpers for requesting, freeing, enabling, disabling and recycling IBI resources --- --- drivers/i3c/internals.h | 14 +++ drivers/i3c/master.c | 239 +++++++++++++++++++++++++++++++++++----- 2 files changed, 225 insertions(+), 28 deletions(-) diff --git a/drivers/i3c/internals.h b/drivers/i3c/internals.h index 86a36b951e0d..416c37e2f75a 100644 --- a/drivers/i3c/internals.h +++ b/drivers/i3c/internals.h @@ -22,6 +22,20 @@ int i3c_dev_setdasa_locked(struct i3c_dev_desc *dev); int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers, int nxfers, enum i3c_xfer_mode mode); + +int i3c_master_attach_i3c_dev_controller_locked(struct i3c_dev_desc *dev); +int i3c_master_reattach_i3c_dev_controller_locked(struct i3c_dev_desc *dev, + u8 old_dyn_addr); +void i3c_master_detach_i3c_dev_controller_locked(struct i3c_dev_desc *dev); + +int i3c_dev_disable_ibi_controller_locked(struct i3c_dev_desc *dev); +int i3c_dev_enable_ibi_controller_locked(struct i3c_dev_desc *dev); +int i3c_dev_request_ibi_controller_locked(struct i3c_dev_desc *dev, + const struct i3c_ibi_setup *req); +void i3c_dev_free_ibi_controller_locked(struct i3c_dev_desc *dev); +void i3c_dev_recycle_ibi_slot_controller(struct i3c_dev_desc *dev, + struct i3c_ibi_slot *slot); + int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev); int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev); int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev, diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 9ea4726495ff..b864fcbb1a59 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1898,6 +1898,30 @@ static int i3c_master_get_i3c_addrs(struct i3c_dev_d= esc *dev) return -EBUSY; } =20 +/** + * i3c_master_attach_i3c_dev_controller_locked() - Attach device state to + * controller + * @dev: I3C device descriptor + * + * Invoke the current controller's attach callback without changing address + * slot state or adding the device to the controller's device list. + * + * Context: The caller must hold the bus lock. + * + * Return: 0 on success, or a negative error code returned by the controll= er. + */ +int i3c_master_attach_i3c_dev_controller_locked(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *master =3D i3c_dev_get_master(dev); + + /* Do not attach the master device itself. */ + if (master->this !=3D dev && master->ops->attach_i3c_dev) + return master->ops->attach_i3c_dev(dev); + + return 0; +} +EXPORT_SYMBOL_GPL(i3c_master_attach_i3c_dev_controller_locked); + static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master, struct i3c_dev_desc *dev) { @@ -1915,12 +1939,10 @@ static int i3c_master_attach_i3c_dev(struct i3c_mas= ter_controller *master, return ret; =20 /* Do not attach the master device itself. */ - if (master->this !=3D dev && master->ops->attach_i3c_dev) { - ret =3D master->ops->attach_i3c_dev(dev); - if (ret) { - i3c_master_put_i3c_addrs(dev); - return ret; - } + ret =3D i3c_master_attach_i3c_dev_controller_locked(dev); + if (ret) { + i3c_master_put_i3c_addrs(dev); + return ret; } =20 list_add_tail(&dev->common.node, &master->bus.devs.i3c); @@ -1928,6 +1950,31 @@ static int i3c_master_attach_i3c_dev(struct i3c_mast= er_controller *master, return 0; } =20 +/** + * i3c_master_reattach_i3c_dev_controller_locked() - Reattach controller + * device state + * @dev: I3C device descriptor + * @old_dyn_addr: Previous dynamic address + * + * Invoke the current controller's reattach callback without modifying the + * controller's address-slot state. + * + * Context: The caller must hold the bus lock. + * + * Return: 0 on success, or a negative error code returned by the controll= er. + */ +int i3c_master_reattach_i3c_dev_controller_locked(struct i3c_dev_desc *dev, + u8 old_dyn_addr) +{ + struct i3c_master_controller *master =3D i3c_dev_get_master(dev); + + if (master->ops->reattach_i3c_dev) + return master->ops->reattach_i3c_dev(dev, old_dyn_addr); + + return 0; +} +EXPORT_SYMBOL_GPL(i3c_master_reattach_i3c_dev_controller_locked); + /** * i3c_master_reattach_i3c_dev_locked() - reattach an I3C device with a ne= w address * @dev: I3C device descriptor to reattach @@ -1958,25 +2005,39 @@ int i3c_master_reattach_i3c_dev_locked(struct i3c_d= ev_desc *dev, I3C_ADDR_SLOT_FREE); } =20 - if (master->ops->reattach_i3c_dev) { - ret =3D master->ops->reattach_i3c_dev(dev, old_dyn_addr); - if (ret) { - i3c_master_put_i3c_addrs(dev); - return ret; - } + ret =3D i3c_master_reattach_i3c_dev_controller_locked(dev, old_dyn_addr); + if (ret) { + i3c_master_put_i3c_addrs(dev); + return ret; } =20 return 0; } EXPORT_SYMBOL_GPL(i3c_master_reattach_i3c_dev_locked); =20 -static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev) +/** + * i3c_master_detach_i3c_dev_controller_locked() - Detach device state from + * controller + * @dev: I3C device descriptor + * + * Invoke the current controller's detach callback without releasing addre= ss + * slots or removing the device from the controller's device list. + * + * Context: The caller must hold the bus lock. + */ +void i3c_master_detach_i3c_dev_controller_locked(struct i3c_dev_desc *dev) { struct i3c_master_controller *master =3D i3c_dev_get_master(dev); =20 /* Do not detach the master device itself. */ if (master->this !=3D dev && master->ops->detach_i3c_dev) master->ops->detach_i3c_dev(dev); +} +EXPORT_SYMBOL_GPL(i3c_master_detach_i3c_dev_controller_locked); + +static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev) +{ + i3c_master_detach_i3c_dev_controller_locked(dev); =20 i3c_master_put_i3c_addrs(dev); list_del(&dev->common.node); @@ -3520,7 +3581,6 @@ static void i3c_master_handle_ibi(struct work_struct = *work) struct i3c_ibi_slot *slot =3D container_of(work, struct i3c_ibi_slot, work); struct i3c_dev_desc *dev =3D slot->dev; - struct i3c_master_controller *master =3D i3c_dev_get_master(dev); struct i3c_ibi_payload payload; =20 payload.data =3D slot->data; @@ -3529,7 +3589,7 @@ static void i3c_master_handle_ibi(struct work_struct = *work) if (dev->dev) dev->ibi->handler(dev->dev, &payload); =20 - master->ops->recycle_ibi_slot(dev, slot); + i3c_dev_recycle_ibi_slot_controller(dev, slot); if (atomic_dec_and_test(&dev->ibi->pending_ibis)) complete(&dev->ibi->all_ibis_handled); } @@ -3640,6 +3700,29 @@ i3c_generic_ibi_alloc_pool(struct i3c_dev_desc *dev, } EXPORT_SYMBOL_GPL(i3c_generic_ibi_alloc_pool); =20 +/** + * 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. + * + * Context: Called from the generic IBI work handler in workqueue context. + * No bus lock is taken here: the controller owns its IBI pool and is + * responsible for synchronizing access to it. The generic pool + * implementation uses its own spinlock. + */ +void i3c_dev_recycle_ibi_slot_controller(struct i3c_dev_desc *dev, + struct i3c_ibi_slot *slot) +{ + struct i3c_master_controller *master =3D 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 po= ol * @pool: the pool to query an IBI slot on @@ -3951,6 +4034,32 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev= , struct i3c_xfer *xfers, } EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked); =20 +/** + * 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 =3D 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 devi= ce * @dev: device on which IBIs should be disabled @@ -3963,14 +4072,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; =20 - if (!dev->ibi) - return -EINVAL; - - master =3D i3c_dev_get_master(dev); - ret =3D master->ops->disable_ibi(dev); + ret =3D i3c_dev_disable_ibi_controller_locked(dev); if (ret) return ret; =20 @@ -3984,6 +4088,32 @@ int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *= dev) } EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_locked); =20 +/** + * 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 =3D 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 @@ -4000,13 +4130,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 =3D i3c_dev_get_master(dev); int ret; =20 - if (!dev->ibi) - return -EINVAL; - - ret =3D master->ops->enable_ibi(dev); + ret =3D i3c_dev_enable_ibi_controller_locked(dev); if (!ret) dev->ibi->enabled =3D true; =20 @@ -4014,6 +4140,35 @@ int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *d= ev) } EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_locked); =20 +/** + * i3c_dev_request_ibi_controller_locked() - Request controller IBI resour= ces + * @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 =3D 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 @@ -4056,8 +4211,15 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *= dev, ibi->num_slots =3D req->num_slots; =20 dev->ibi =3D ibi; - ret =3D master->ops->request_ibi(dev, req); + ret =3D 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 =3D NULL; } @@ -4066,6 +4228,27 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *= dev, } EXPORT_SYMBOL_GPL(i3c_dev_request_ibi_locked); =20 +/** + * 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 =3D 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 @@ -4096,7 +4279,7 @@ void i3c_dev_free_ibi_locked(struct i3c_dev_desc *dev) dev_err(&master->dev, "Failed to disable IBI before freeing\n"); } =20 - master->ops->free_ibi(dev); + i3c_dev_free_ibi_controller_locked(dev); =20 if (dev->ibi->wq) { destroy_workqueue(dev->ibi->wq); --=20 2.25.1 From nobody Thu Sep 24 16:08:57 2026 Received: from inva021.nxp.com (inva021.nxp.com [92.121.34.21]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C717E51C043; Tue, 22 Sep 2026 10:36:15 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.21 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073379; cv=none; b=Q9ckg1c1wjg9JFJaxJa/loDfSsT8XWPzbQsPJZE9l0lB4vccGUxmTfMYZEKOcqwzSvYcxQ4C+SvXElden0FBfqnwAB05qW78UePmE8phJSu6RNj6ztIBGjyC8XZ1EiYHK53q2X2jy4p7zVDcZirUSkQJmdsfwtos3VyCnU2uv90= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073379; c=relaxed/simple; bh=nar/D1heQYQ7cDPssOhtvZa6G8r1+ZGx+ivdEw71lJc=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=l6j3YkrhqSEUzsOaaHhVugbPgiOYGfM4Vi8bEvYVEOWlevJ5bl6V40TxNXAViXJUAl7FKtISApzuVexBXrEXQj2fmHAVVXDUeOK48GbqWZbcoESsu6IF8KMuS8SMnXoMqgAvOLY7sVj/1Zoljk/rsqFjmdf/yeGV93W+6wu0G1k= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.21 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva021.nxp.com (localhost [127.0.0.1]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 41F5D20000E; Tue, 22 Sep 2026 12:36:13 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id D3CBD200006; Tue, 22 Sep 2026 12:36:12 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id 66BEF180006C; Tue, 22 Sep 2026 18:36:09 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani Subject: [PATCH v20 3/8] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support Date: Tue, 22 Sep 2026 16:05:45 +0530 Message-Id: <20260922103551.2754613-4-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" From: Aman Kumar Pandey 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 Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Rob Herring (Arm) Reviewed-by: Frank Li --- Changes in v20: - No change Changes in v19: - Express P3H2440, P3H2441 and P3H2841 using nxp,p3h2840 as the fallback compatible - Reword the description's closing paragraph to drop the driver reference and state that x0/x1 differ only in power-up defaults - Update the I3C example to demonstrate the fallback-compatible form Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes Changes in v17: - Explain in the binding description that all variants share the same regi= ster interface, software reads the target-port count from DEV_CAPAB, and the x0/x1 variants differ only in power-up defaults that the driver overrides Changes in v16: - Add an allOf/if-then block for the 4-port variants (nxp,p3h2440 and nxp,p3h2441) that limits i3c@[0-3]/smbus@[0-3] and disallows target ports [4-7], so DTs describing nonexistent ports fail validation - Add #address-cells and #size-cells to the required list, since the hub is a parent bus to the target-port child node - Add an explicit minimum of 0 to the target-port reg constraints, retain the maximum of 7, and restrict the maximum to 3 for four-port variants Changes in v15: - Add compatible strings for the P3H2440, P3H2441, P3H2840 and P3H2841 variants - Fix the target-port reg schema to describe the single-cell port index Changes in v14: - No change Changes in v13: - Fix mismatch between example unit-address and reg property by correcting= the reg value to match the node name Changes in v12: - No change, added Reviewed-By tag Changes in v11: - No change Changes in v10: - No change, added Reviewed-By tag Changes in v9: - Referenced i3c.yaml and i2c-controller.yaml for child nodes - Dropped unnecessary #address-cells and #size-cells from child nodes Changes in v8: - Add compatible in i3c example Changes in v7: - Fix schema validation issues - Adjust required properties - Add I2C example Changes in v6: - Use a vendor prefix for the attributes Changes in v5: - Removed SW properties: cp0-ldo-microvolt,cp1-ldo-microvolt, tp0145-ldo-microvolt, tp2367-ldo-microvolt - Changed supply entries and its descriptions Changes in v4: - Fixed DT binding check warning - Removed SW properties: ibi-enable, local-dev, and always-enable Changes in v3: - Added MFD (Multi-Function Device) support for I3C hub and on-die regulat= or - Added Regulator supply node Changes in v2: - Fixed DT binding check warning - Revised logic for parsing DTS nodes --- --- .../devicetree/bindings/i3c/nxp,p3h2840.yaml | 330 ++++++++++++++++++ MAINTAINERS | 9 + 2 files changed, 339 insertions(+) create mode 100644 Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml diff --git a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml b/Docum= entation/devicetree/bindings/i3c/nxp,p3h2840.yaml new file mode 100644 index 000000000000..09508a7bcbeb --- /dev/null +++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml @@ -0,0 +1,330 @@ +# 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 + - Vikash Bansal + - Lakshay Piplani + +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 mult= iple + 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. + + All variants share the same register interface and programmable + functionality. The x0 and x1 variants differ only in power-up defaults. + +properties: + compatible: + oneOf: + - const: nxp,p3h2840 + - items: + - enum: + - nxp,p3h2440 + - nxp,p3h2441 + - nxp,p3h2841 + - const: nxp,p3h2840 + + reg: + maxItems: 1 + + '#address-cells': + const: 1 + + '#size-cells': + const: 0 + + assigned-address: + maximum: 0x7f + + nxp,tp0145-pullup-ohms: + description: + Selects the pull up resistance for target Port 0/1/4/5, in ohms. + enum: [250, 500, 1000, 2000] + default: 500 + + nxp,tp2367-pullup-ohms: + description: + Selects the pull up resistance for target Port 2/3/6/7, in ohms. + enum: [250, 500, 1000, 2000] + default: 500 + + nxp,cp0-io-strength-ohms: + description: + Selects the IO drive strength for controller Port 0, in ohms. + enum: [20, 30, 40, 50] + default: 20 + + nxp,cp1-io-strength-ohms: + description: + Selects the IO drive strength for controller Port 1, in ohms. + enum: [20, 30, 40, 50] + default: 20 + + nxp,tp0145-io-strength-ohms: + description: + Selects the IO drive strength for target port 0/1/4/5, in ohms. + enum: [20, 30, 40, 50] + default: 20 + + nxp,tp2367-io-strength-ohms: + description: + Selects the IO drive strength for target port 2/3/6/7, in ohms. + enum: [20, 30, 40, 50] + default: 20 + + vcc1-supply: + description: Controller port 0 power supply. + + vcc2-supply: + description: Controller port 1 power supply. + + vcc3-supply: + description: Target port 0/1/4/5 power supply. + + vcc4-supply: + description: Target port 2/3/6/7 power supply. + + regulators: + type: object + additionalProperties: false + + properties: + ldo-cp0: + type: object + $ref: /schemas/regulator/regulator.yaml# + unevaluatedProperties: false + + ldo-cp1: + type: object + $ref: /schemas/regulator/regulator.yaml# + unevaluatedProperties: false + + ldo-tpg0: + type: object + $ref: /schemas/regulator/regulator.yaml# + unevaluatedProperties: false + + ldo-tpg1: + type: object + $ref: /schemas/regulator/regulator.yaml# + unevaluatedProperties: false + +required: + - reg + - '#address-cells' + - '#size-cells' + +patternProperties: + "^i3c@[0-7]$": + type: object + $ref: /schemas/i3c/i3c.yaml# + unevaluatedProperties: false + + properties: + reg: + description: + The I3C HUB Target Port number. + items: + - minimum: 0 + maximum: 7 + + nxp,pullup-enable: + type: boolean + description: + Enables the on-die pull-up for Target Port. + + required: + - reg + + "^smbus@[0-7]$": + type: object + $ref: /schemas/i2c/i2c-controller.yaml# + unevaluatedProperties: false + + properties: + reg: + description: + The I3C HUB Target Port number. + items: + - minimum: 0 + maximum: 7 + + nxp,pullup-enable: + type: boolean + description: + Enables the on-die pull-up for Target Port. + + required: + - reg + +allOf: + - if: + properties: + compatible: + contains: + enum: + - nxp,p3h2440 + - nxp,p3h2441 + then: + patternProperties: + "^i3c@[0-3]$": + properties: + reg: + items: + - maximum: 3 + "^smbus@[0-3]$": + properties: + reg: + items: + - maximum: 3 + "^i3c@[4-7]$": false + "^smbus@[4-7]$": false + +unevaluatedProperties: false + +examples: + - | + i3c { + #address-cells =3D <3>; + #size-cells =3D <0>; + + hub@70,236153000c2 { + reg =3D <0x70 0x236 0x153000c2>; + compatible =3D "nxp,p3h2441", "nxp,p3h2840"; + #address-cells =3D <1>; + #size-cells =3D <0>; + assigned-address =3D <0x50>; + + nxp,tp0145-pullup-ohms =3D <1000>; + nxp,tp2367-pullup-ohms =3D <1000>; + nxp,cp0-io-strength-ohms =3D <50>; + nxp,cp1-io-strength-ohms =3D <50>; + nxp,tp0145-io-strength-ohms =3D <50>; + nxp,tp2367-io-strength-ohms =3D <50>; + vcc3-supply =3D <®_tpg0>; + vcc4-supply =3D <®_tpg1>; + + regulators { + reg_cp0: ldo-cp0 { + regulator-name =3D "ldo-cp0"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + + reg_cp1: ldo-cp1 { + regulator-name =3D "ldo-cp1"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + + reg_tpg0: ldo-tpg0 { + regulator-name =3D "ldo-tpg0"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + + reg_tpg1: ldo-tpg1 { + regulator-name =3D "ldo-tpg1"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + }; + + smbus@0 { + reg =3D <0x0>; + #address-cells =3D <1>; + #size-cells =3D <0>; + nxp,pullup-enable; + }; + + smbus@1 { + reg =3D <0x1>; + #address-cells =3D <1>; + #size-cells =3D <0>; + nxp,pullup-enable; + }; + + i3c@2 { + reg =3D <0x2>; + #address-cells =3D <3>; + #size-cells =3D <0>; + nxp,pullup-enable; + }; + }; + }; + + - | + i2c { + #address-cells =3D <1>; + #size-cells =3D <0>; + + hub@70 { + reg =3D <0x70>; + compatible =3D "nxp,p3h2840"; + #address-cells =3D <1>; + #size-cells =3D <0>; + + nxp,tp0145-pullup-ohms =3D <1000>; + nxp,tp2367-pullup-ohms =3D <1000>; + nxp,cp0-io-strength-ohms =3D <50>; + nxp,cp1-io-strength-ohms =3D <50>; + nxp,tp0145-io-strength-ohms =3D <50>; + nxp,tp2367-io-strength-ohms =3D <50>; + vcc3-supply =3D <®_tpg0_i2c>; + vcc4-supply =3D <®_tpg1_i2c>; + + regulators { + reg_cp0_i2c: ldo-cp0 { + regulator-name =3D "ldo-cp0"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + + reg_cp1_i2c: ldo-cp1 { + regulator-name =3D "ldo-cp1"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + + reg_tpg0_i2c: ldo-tpg0 { + regulator-name =3D "ldo-tpg0"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + + reg_tpg1_i2c: ldo-tpg1 { + regulator-name =3D "ldo-tpg1"; + regulator-min-microvolt =3D <1800000>; + regulator-max-microvolt =3D <1800000>; + }; + }; + + smbus@0 { + reg =3D <0x0>; + #address-cells =3D <1>; + #size-cells =3D <0>; + nxp,pullup-enable; + }; + + smbus@1 { + reg =3D <0x1>; + #address-cells =3D <1>; + #size-cells =3D <0>; + nxp,pullup-enable; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index cc3cae2e378b..2ef13babadc7 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19759,6 +19759,15 @@ L: linux-wireless@vger.kernel.org S: Maintained F: drivers/net/wireless/nxp/ =20 +NXP P3H2X4X I3C-HUB DRIVER +M: Vikash Bansal +M: Aman Kumar Pandey +M: Lakshay Piplani +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 S: Maintained --=20 2.25.1 From nobody Thu Sep 24 16:08:57 2026 Received: from inva021.nxp.com (inva021.nxp.com [92.121.34.21]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 624EF44F570; Tue, 22 Sep 2026 10:36:18 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.21 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073383; cv=none; b=g3PSXwKWJvvpjiy6cXGzU8idP9DFDXBJhnSN2oAZJHGsMmD8ibfF1kvzDS3CKqFUhUz/Yzsb+QakctUrGCc18N284CSywdEUX9NLronTI5jbDFYvpjCVLWbqv9AB6mdfXWP9PoUISRMA8UIgqNNaO6emd7mkKoacggxgK7Mbf/Y= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073383; c=relaxed/simple; bh=oZ/D5scynGKiDfgK2iQEJqLMfiPgD3wdVURzSuPO0TI=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=hrbjZ6jh54JjzlEJBMjfzZkTVuRR5tF57wsposXD1fUh1KwFemEiv+ameNjqCgbafumRHTMQezM/3MverBxrf67LnmkZmRNBcsPcHiXgQ+iBb9UH1leKKq1uJHqlOc+/wVXGSpJQvdUdLFBGJmMkE8mf+s445LwDr4x7Fxx5yKQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.21 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva021.nxp.com (localhost [127.0.0.1]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 9CEB6200007; Tue, 22 Sep 2026 12:36:16 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 64D95200006; Tue, 22 Sep 2026 12:36:16 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id 1FE1018000BA; Tue, 22 Sep 2026 18:36:12 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani Subject: [PATCH v20 4/8] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator Date: Tue, 22 Sep 2026 16:05:46 +0530 Message-Id: <20260922103551.2754613-5-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" From: Aman Kumar Pandey 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. All variants share the same register interface. Read the implemented target-port count from DEV_CAPAB, so no per-compatible driver data is required. Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Frank Li --- Changes in v20: - No change Changes in v19: - Retain only nxp,p3h2840 in the OF match table because the other variants use it as their fallback compatible Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes Changes in v17: - No change Changes in v16: - Use a named initializer for struct i2c_device_id ({ .name =3D "nxp-i3c-hub" }) - Keep the shared header in include/linux/mfd/ (struct p3h2x4x is shared by the mfd parent and its regulator/i3c-hub children). The i3c_hub_priv back-pointer is retained for IBI routing, with documented publish/clear ordering Changes in v15: - Read the device capability register to determine whether the device has four or eight target ports - Store the detected target-port count in the shared MFD data - Add OF match entries for all P3H2440, P3H2441, P3H2840 and P3H2841 variants - Add a shared hub-context pointer for use by the IBI handler without overwriting the MFD parent's driver data Changes in v14: - Clean up MFD driver comments, formatting, variable names and author information - Define and document the I3C hub DCR value in hexadecimal form - Use direct regmap bit-width values instead of local macros - Add a shared protected_reg_lock for protected register accesses across P3H2X4X child drivers Changes in v13: - Use i3c_device helpers (i3cdev_to_dev()) instead of direct struct access to maintain API abstraction - Avoid including internal I3C headers and use public device headers inste= ad - Ensure proper device matching by relying on manufacturer ID checks in pr= obe 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 | 164 ++++++++++++++++++++++++++++++++++++ include/linux/i3c/device.h | 2 + include/linux/mfd/p3h2840.h | 41 +++++++++ 6 files changed, 223 insertions(+) create mode 100644 drivers/mfd/p3h2840.c create mode 100644 include/linux/mfd/p3h2840.h diff --git a/MAINTAINERS b/MAINTAINERS index 2ef13babadc7..e460d7bd563b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19767,6 +19767,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 =20 NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER M: Woodrow Douglass diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index e4fd4572472f..ee809b0c1b8f 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. =20 +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=3Dy && OF diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index 72d3944b0ad8..ec4e093f3540 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -122,6 +122,7 @@ obj-$(CONFIG_MFD_MC13XXX) +=3D mc13xxx-core.o obj-$(CONFIG_MFD_MC13XXX_SPI) +=3D mc13xxx-spi.o obj-$(CONFIG_MFD_MC13XXX_I2C) +=3D mc13xxx-i2c.o =20 +obj-$(CONFIG_MFD_P3H2X4X) +=3D p3h2840.o obj-$(CONFIG_MFD_PF1550) +=3D pf1550.o =20 obj-$(CONFIG_MFD_NCT6694) +=3D nct6694.o diff --git a/drivers/mfd/p3h2840.c b/drivers/mfd/p3h2840.c new file mode 100644 index 000000000000..c48984482148 --- /dev/null +++ b/drivers/mfd/p3h2840.c @@ -0,0 +1,164 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * + * Authors: + * Aman Kumar Pandey + * Vikash Bansal + * Lakshay Piplani + * + * NXP P3H2x4x multi-port I3C hub. + */ +#include +#include +#include +#include +#include + +static const struct mfd_cell p3h2x4x_devs[] =3D { + MFD_CELL_NAME("p3h2x4x-regulator"), + MFD_CELL_NAME("p3h2x4x-i3c-hub"), +}; + +static const struct regmap_config p3h2x4x_regmap_config =3D { + .reg_bits =3D 8, + .val_bits =3D 8, + .max_register =3D 0xFF, +}; + +/* Read port count from the device capability register (4- or 8-port varia= nt). */ +static int p3h2x4x_read_num_target_ports(struct device *dev, + struct p3h2x4x *ddata) +{ + unsigned int val; + int ret; + + ret =3D 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 =3D (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 =3D 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) !=3D I3C_MANUF_ID_NXP) + return -ENODEV; + + ddata =3D devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ret =3D devm_mutex_init(dev, &ddata->protected_reg_lock); + if (ret) + return ret; + + i3cdev_set_drvdata(i3cdev, ddata); + + ddata->regmap =3D 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 =3D i3cdev; + + ret =3D p3h2x4x_read_num_target_ports(dev, ddata); + if (ret) + return ret; + + ret =3D 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 =3D devm_kzalloc(&client->dev, sizeof(*ddata), GFP_KERNEL); + if (!ddata) + return -ENOMEM; + + ret =3D devm_mutex_init(&client->dev, &ddata->protected_reg_lock); + if (ret) + return ret; + + i2c_set_clientdata(client, ddata); + + ddata->regmap =3D 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 =3D NULL; + + ret =3D p3h2x4x_read_num_target_ports(&client->dev, ddata); + if (ret) + return ret; + + ret =3D 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[] =3D { + I3C_CLASS(I3C_DCR_HUB, NULL), + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(i3c, p3h2x4x_i3c_ids); + +static const struct i2c_device_id p3h2x4x_i2c_id_table[] =3D { + { .name =3D "nxp-i3c-hub" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(i2c, p3h2x4x_i2c_id_table); + +static const struct of_device_id p3h2x4x_i2c_of_match[] =3D { + { .compatible =3D "nxp,p3h2840", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, p3h2x4x_i2c_of_match); + +static struct i3c_driver p3h2x4x_i3c =3D { + .driver =3D { + .name =3D "p3h2x4x-i3c", + }, + .probe =3D p3h2x4x_device_probe_i3c, + .id_table =3D p3h2x4x_i3c_ids, +}; + +static struct i2c_driver p3h2x4x_i2c =3D { + .driver =3D { + .name =3D "p3h2x4x-i2c", + .of_match_table =3D p3h2x4x_i2c_of_match, + }, + .probe =3D p3h2x4x_device_probe_i2c, + .id_table =3D p3h2x4x_i2c_id_table, +}; +module_i3c_i2c_driver(p3h2x4x_i3c, &p3h2x4x_i2c); + +MODULE_AUTHOR("Aman Kumar Pandey "); +MODULE_AUTHOR("Vikash Bansal "); +MODULE_AUTHOR("Lakshay Piplani "); +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 =3D 0, + I3C_DCR_HUB =3D 0xC2, }; =20 #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 +#include +#include + +/* 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 childre= n. */ + struct mutex protected_reg_lock; + /* Hub context for the IBI handler to reach hub state via the parent i3cd= ev. */ + struct p3h2x4x_i3c_hub_dev *i3c_hub_priv; +}; +#endif /* _LINUX_MFD_P3H2840_H */ --=20 2.25.1 From nobody Thu Sep 24 16:08:57 2026 Received: from inva021.nxp.com (inva021.nxp.com [92.121.34.21]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 31FA551DAF0; Tue, 22 Sep 2026 10:36:22 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.21 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073386; cv=none; b=Q6+LriF9E30Dtq384XkmZQPOupC+uigMnjIWUFLNzPkFjOBtASrIECFy97/aHcICg9MJl9oHbqd3+4+CuOlNpFvw0Zz0uQTzf4qkjr/or6sS0M/DvbZtzk0p0uoGcXAw1y2rIiOProWhQrZ1JAKxVPOVTS71qrwriLTi3slmt70= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073386; c=relaxed/simple; bh=s5p7uf8e3EAviy6z289bQzpK/L+cvEiOcFbVVJ/N2dg=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=udyKyvmq/EEEUklfpIx2oBsYMwN8lNbOwVDu4zEHYfgp1o2lFfinpd34u0+u5UEdpVpCuHNraBsZi8QwEZ44ijXqag2dB3LHnw64+//ElPB4oec/rFSY2UIM7odp6GnF0keLOnwpilyemzhT581q0RjPuYh5dQ1L3hIcB81Q7gg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.21 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva021.nxp.com (localhost [127.0.0.1]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id ABA77200009; Tue, 22 Sep 2026 12:36:20 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva021.eu-rdc02.nxp.com (Postfix) with ESMTP id 493DB200006; Tue, 22 Sep 2026 12:36:20 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id A237E18000BE; Tue, 22 Sep 2026 18:36:16 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani , Frank Li Subject: [PATCH v20 5/8] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub Date: Tue, 22 Sep 2026 16:05:47 +0530 Message-Id: <20260922103551.2754613-6-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" From: Aman Kumar Pandey 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 Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Frank Li Reviewed-by: Mark Brown --- Changes in v20: - No change Changes in v19: - No change Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes Changes in v17: - Add .enable_time =3D 5000 through P3H2X4X_LDO_ENABLE_TIME_US to all four LDO descriptors, so the regulator core applies the post-enable settling delay Changes in v16: - Mark the p3h2x4x_regulators[] descriptor array as const - Keep the standard MFD dev_get_drvdata(pdev->dev.parent) parent-context access and the shared include/linux/mfd/ header (used only by the parent and its children) Changes in v15: - No change Changes in v14: - Use the shared MFD protected_reg_lock instead of a regulator-private mut= ex to serialize protected register access across P3H2X4X child drivers - Rework protected register handling into explicit unprotect/protect helpe= rs 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 mut= ex 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 loc= ked 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 | 289 ++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 drivers/regulator/p3h2840_i3c_hub_regulator.c diff --git a/MAINTAINERS b/MAINTAINERS index e460d7bd563b..3abba881ba82 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19768,6 +19768,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 =20 NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER diff --git a/drivers/regulator/Kconfig b/drivers/regulator/Kconfig index 89789ac7a786..00b152d95d42 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. =20 +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 5a764cec8df8..a07702a49be4 100644 --- a/drivers/regulator/Makefile +++ b/drivers/regulator/Makefile @@ -128,6 +128,7 @@ obj-$(CONFIG_REGULATOR_QCOM_RPMH) +=3D qcom-rpmh-regula= tor.o obj-$(CONFIG_REGULATOR_QCOM_SMD_RPM) +=3D qcom_smd-regulator.o obj-$(CONFIG_REGULATOR_QCOM_SPMI) +=3D qcom_spmi-regulator.o obj-$(CONFIG_REGULATOR_QCOM_USB_VBUS) +=3D qcom_usb_vbus-regulator.o +obj-$(CONFIG_REGULATOR_P3H2X4X) +=3D p3h2840_i3c_hub_regulator.o obj-$(CONFIG_REGULATOR_PALMAS) +=3D palmas-regulator.o obj-$(CONFIG_REGULATOR_PCA9450) +=3D pca9450-regulator.o obj-$(CONFIG_REGULATOR_PF0900) +=3D pf0900-regulator.o diff --git a/drivers/regulator/p3h2840_i3c_hub_regulator.c b/drivers/regula= tor/p3h2840_i3c_hub_regulator.c new file mode 100644 index 000000000000..244f2a4f70f3 --- /dev/null +++ b/drivers/regulator/p3h2840_i3c_hub_regulator.c @@ -0,0 +1,289 @@ +// 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 +#include +#include +#include +#include +#include +#include + +#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 +#define P3H2X4X_LDO_ENABLE_TIME_US 5000 + +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 =3D 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 =3D 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 =3D false; + + ret =3D regmap_read(rdev->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + &state->orig); + if (ret) + return ret; + + if (state->orig =3D=3D P3H2X4X_REGISTERS_UNLOCK_CODE) + return 0; + + ret =3D regmap_write(rdev->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_UNLOCK_CODE); + if (ret) + return ret; + + state->restore =3D 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 =3D p3h2x4x_reg_unprotect(rdev, &state); + if (ret) + return ret; + + ret =3D regulator_enable_regmap(rdev); + ret2 =3D 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 =3D p3h2x4x_reg_unprotect(rdev, &state); + if (ret) + return ret; + + ret =3D regulator_disable_regmap(rdev); + ret2 =3D 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 =3D p3h2x4x_reg_unprotect(rdev, &state); + if (ret) + return ret; + + ret =3D regulator_set_voltage_sel_regmap(rdev, sel); + ret2 =3D p3h2x4x_reg_protect(rdev, &state); + + return ret ? ret : ret2; +} + +static const struct regulator_ops p3h2x4x_ldo_ops =3D { + .list_voltage =3D regulator_list_voltage_table, + .map_voltage =3D regulator_map_voltage_iterate, + .set_voltage_sel =3D p3h2x4x_regulator_set_voltage_sel, + .get_voltage_sel =3D regulator_get_voltage_sel_regmap, + .enable =3D p3h2x4x_regulator_enable, + .disable =3D p3h2x4x_regulator_disable, + .is_enabled =3D regulator_is_enabled_regmap, +}; + +static const unsigned int p3h2x4x_voltage_table[] =3D { + 1000000, + 1100000, + 1200000, + 1800000, +}; + +static const struct regulator_desc p3h2x4x_regulators[] =3D { + { + .name =3D "ldo-cp0", + .of_match =3D of_match_ptr("ldo-cp0"), + .regulators_node =3D of_match_ptr("regulators"), + .volt_table =3D p3h2x4x_voltage_table, + .n_voltages =3D ARRAY_SIZE(p3h2x4x_voltage_table), + .ops =3D &p3h2x4x_ldo_ops, + .type =3D REGULATOR_VOLTAGE, + .owner =3D THIS_MODULE, + .enable_reg =3D P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask =3D P3H2X4X_CP0_EN_LDO, + .enable_time =3D P3H2X4X_LDO_ENABLE_TIME_US, + .vsel_reg =3D P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask =3D P3H2X4X_CP0_VCCIO_LDO_VOLTAGE_MASK, + }, + { + .name =3D "ldo-cp1", + .of_match =3D of_match_ptr("ldo-cp1"), + .regulators_node =3D of_match_ptr("regulators"), + .volt_table =3D p3h2x4x_voltage_table, + .n_voltages =3D ARRAY_SIZE(p3h2x4x_voltage_table), + .ops =3D &p3h2x4x_ldo_ops, + .type =3D REGULATOR_VOLTAGE, + .owner =3D THIS_MODULE, + .enable_reg =3D P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask =3D P3H2X4X_CP1_EN_LDO, + .enable_time =3D P3H2X4X_LDO_ENABLE_TIME_US, + .vsel_reg =3D P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask =3D P3H2X4X_CP1_VCCIO_LDO_VOLTAGE_MASK, + }, + { + .name =3D "ldo-tpg0", + .of_match =3D of_match_ptr("ldo-tpg0"), + .regulators_node =3D of_match_ptr("regulators"), + .volt_table =3D p3h2x4x_voltage_table, + .n_voltages =3D ARRAY_SIZE(p3h2x4x_voltage_table), + .ops =3D &p3h2x4x_ldo_ops, + .type =3D REGULATOR_VOLTAGE, + .owner =3D THIS_MODULE, + .enable_reg =3D P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask =3D P3H2X4X_TP0145_EN_LDO, + .enable_time =3D P3H2X4X_LDO_ENABLE_TIME_US, + .vsel_reg =3D P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask =3D P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE_MASK, + }, + { + .name =3D "ldo-tpg1", + .of_match =3D of_match_ptr("ldo-tpg1"), + .regulators_node =3D of_match_ptr("regulators"), + .volt_table =3D p3h2x4x_voltage_table, + .n_voltages =3D ARRAY_SIZE(p3h2x4x_voltage_table), + .ops =3D &p3h2x4x_ldo_ops, + .type =3D REGULATOR_VOLTAGE, + .owner =3D THIS_MODULE, + .enable_reg =3D P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask =3D P3H2X4X_TP2367_EN_LDO, + .enable_time =3D P3H2X4X_LDO_ENABLE_TIME_US, + .vsel_reg =3D P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask =3D P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE_MASK, + }, +}; + +static int p3h2x4x_regulator_probe(struct platform_device *pdev) +{ + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(pdev->dev.parent); + struct p3h2x4x_regulator_dev *p3h2x4x_regulator; + struct regulator_config rcfg =3D { }; + struct device *dev =3D &pdev->dev; + struct regulator_dev *rdev; + int i; + + p3h2x4x_regulator =3D devm_kzalloc(dev, sizeof(*p3h2x4x_regulator), GFP_K= ERNEL); + if (!p3h2x4x_regulator) + return -ENOMEM; + + p3h2x4x_regulator->p3h2x4x =3D p3h2x4x; + p3h2x4x_regulator->regmap =3D p3h2x4x->regmap; + + platform_set_drvdata(pdev, p3h2x4x_regulator); + + device_set_of_node_from_dev(dev, dev->parent); + + rcfg.dev =3D dev; + rcfg.regmap =3D p3h2x4x_regulator->regmap; + rcfg.driver_data =3D p3h2x4x_regulator; + + for (i =3D 0; i < ARRAY_SIZE(p3h2x4x_regulators); i++) { + rdev =3D devm_regulator_register(&pdev->dev, &p3h2x4x_regulators[i], &rc= fg); + 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] =3D rdev; + } + return 0; +} + +static const struct platform_device_id p3h2x4x_regulator_id[] =3D { + { .name =3D "p3h2x4x-regulator" }, + { } +}; +MODULE_DEVICE_TABLE(platform, p3h2x4x_regulator_id); + +static struct platform_driver p3h2x4x_regulator_driver =3D { + .driver =3D { + .name =3D "p3h2x4x-regulator", + .probe_type =3D PROBE_PREFER_ASYNCHRONOUS, + }, + .probe =3D p3h2x4x_regulator_probe, + .id_table =3D p3h2x4x_regulator_id, +}; +module_platform_driver(p3h2x4x_regulator_driver); + +MODULE_AUTHOR("Aman Kumar Pandey "); +MODULE_AUTHOR("Vikash Bansal "); +MODULE_AUTHOR("Lakshay Piplani "); +MODULE_DESCRIPTION("NXP P3H2X4X I3C HUB Regulator driver"); +MODULE_LICENSE("GPL"); --=20 2.25.1 From nobody Thu Sep 24 16:08:57 2026 Received: from inva020.nxp.com (inva020.nxp.com [92.121.34.13]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 5EC6C192D8A; Tue, 22 Sep 2026 10:36:32 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.13 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073395; cv=none; b=JpYrDgBJ/m4P+kMwaV/uK70lzUnc8lA+DKjg8HklOHQcmWR/m9tsRegFHxKhuicDbWZN5yn+qCAkj+11yffGMUWJnm+c5TTfbcN5zrdCOlO4FvZ5ri7wmbCRFkxQrhH30qyhm9upZBYbS0HFpym8FdsJBxd5dEcf+JXBgW/N/iM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073395; c=relaxed/simple; bh=cnRpeglg8mWgjYA9yUXQO8vxfrNzW7r5ALcf53jHPcM=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=re8Dw6nQn52Bz/QbKV2UY8isnOjN0nRRTj4cwwdioI3Oqvn/NcNbLFuUfh9xxlK5USQawoZhOoDxL8XJIOs8/7B9SN2TmpYGd1D2hwuVlDEE+wuAZfE7Gqvb3VAGhZLjUuEVHuuDYE6TYKg1Khkc4HyheWB3D6OeMU5Xe69p+nI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.13 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva020.nxp.com (localhost [127.0.0.1]) by inva020.eu-rdc02.nxp.com (Postfix) with ESMTP id C3FF41A0010; Tue, 22 Sep 2026 12:36:24 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva020.eu-rdc02.nxp.com (Postfix) with ESMTP id 636431A0005; Tue, 22 Sep 2026 12:36:24 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id 8B6DD1800068; Tue, 22 Sep 2026 18:36:20 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani Subject: [PATCH v20 6/8] i3c: hub: Add support for the I3C interface in the I3C hub Date: Tue, 22 Sep 2026 16:05:48 +0530 Message-Id: <20260922103551.2754613-7-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" Add virtual I3C bus support for the hub and provide interface to enable or disable downstream ports. Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Frank Li --- Changes in v20: - Move the generic I3C hub MAINTAINERS entries to patch 7, allowing this patch to be applied independently of the P3H2x4x driver Changes in v19: - No change Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes Changes in v17: - Move the lockdep-only routing-lock class assignment into a dedicated helper, leaving hub context initialization unconditional when CONFIG_LOCKDEP is disabled - Document that hub_dev must already be registered and remove the WARN_ON_ONCE checks for its descriptor and parent controller - Export i3c_hub_master_ops as a const object and reference it directly instead of returning it through an accessor - Document that downstream ports share the parent controller's dynamic address domain and that independent per-port DAA domains are not provided - Document that a broadcast CCC on a virtual bus reaches the parent bus and devices behind other connected ports; directed CCCs must be used for per-device scope - Document that fixed-DT address reservations remain allocated for the lifetime of the parent bus, are reused across driver reprobes, and that downstream hot-join support would also require reserving static addresses Changes in v16: - Fix the lockdep "recursive locking" false positive on the forwarding path (holding the virtual bus lock while taking the parent bus lock, both sharing one class). Introduce per-nesting-depth lock_class_key arrays for the bus lock and the routing mutex, computed via i3c_hub_controller_depth(), assigned in i3c_hub_master_bus_init() and i3c_hub_init() - Fix the parent-bus dynamic-address reservation: reserve by "assigned-address" regardless of the static address (the old static_addr !=3D assigned_addr skip left addresses free and risked ENTDAA collisions). Reserve only when the slot is currently FREE, under the parent bus maintenance lock - Make the reattach info update atomic w.r.t. asynchronous IBI: update parent_desc.info under i3c_bus_maintenance_lock(&parent->bus) and reject reattach with -EBUSY (WARN_ON_ONCE) while parent_desc.ibi is live - Document and check the detach-time IBI lifecycle invariant: parent_desc.ibi must already have been cleared through i3c_hub_free_ibi() before the parent-facing descriptor is detached and freed. Add WARN_ON_ONCE() to detect a violation. The underlying generic I3C-core unregister/IBI-quiesce behaviour is a known I3C-core limitation - Document that DAA is intentionally run on the parent controller because downstream devices share the parent's dynamic-address space; broadcast RSTDAA is intentionally not forwarded (would reset the hub address) Changes in v15: - Replace temporary descriptor reparenting and hub-address switching with a permanent parent-facing descriptor for each downstream I3C device - Keep the logical device descriptor associated with the virtual hub controller while using the parent-facing descriptor for physical controller operations - Implement downstream device attach, reattach and detach using the new controller-only helpers - Forward IBI slot recycling to the physical parent controller - Remove the helper that temporarily changed the logical descriptor's master pointer Changes in v14: - Add hub route serialization around DAA, CCC, private transfer and IBI pa= ths - 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 forwar= ding 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 matchin= g 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 --- --- drivers/i3c/Kconfig | 14 + drivers/i3c/Makefile | 1 + drivers/i3c/hub.c | 832 ++++++++++++++++++++++++++++++++++++++++ include/linux/i3c/hub.h | 92 +++++ 4 files changed, 939 insertions(+) create mode 100644 drivers/i3c/hub.c create mode 100644 include/linux/i3c/hub.h 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 =20 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 =20 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 :=3D device.o master.o obj-$(CONFIG_I3C) +=3D i3c.o obj-$(CONFIG_I3C) +=3D master/ +obj-$(CONFIG_I3C_HUB) +=3D hub.o diff --git a/drivers/i3c/hub.c b/drivers/i3c/hub.c new file mode 100644 index 000000000000..539028768084 --- /dev/null +++ b/drivers/i3c/hub.c @@ -0,0 +1,832 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2026 NXP + * Generic I3C Hub core implementing virtual controller operations. + */ +#include +#include +#include + +#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 ra= cing + * concurrent readers on the virtual bus. + */ +struct i3c_hub_dev_data { + struct i3c_dev_desc parent_desc; +}; + +/* + * All i3c_bus rw_semaphores are initialized from a single call site in the + * I3C core, so lockdep assigns them one shared class. When a hub forwards= an + * operation it takes the parent bus lock while already holding its own vi= rtual + * bus lock, which lockdep then reports as recursive locking on that shared + * class. The bus maintenance and normal-use helpers use plain down_write(= ) and + * down_read(), which always acquire with subclass 0, so lockdep_set_subcl= ass() + * cannot separate them; a distinct lock_class_key per nesting level is us= ed + * instead. + * + * A top-level hub uses depth 1, a hub behind another hub uses depth 2, an= d so + * on, so a virtual bus lock never shares a class with the parent bus lock= it + * nests under. Sibling ports on the same hub share a class, which is safe + * because they are never nested against each other. The array must stay a + * file-local definition: lockdep keys are identified by their address, so= a + * single set of unique objects is required. + * + * The depth bound is generous; exceeding it only loses lockdep coverage, = not + * correctness. + */ +#define I3C_HUB_MAX_LOCK_DEPTH 8 +static struct lock_class_key i3c_hub_bus_lock_keys[I3C_HUB_MAX_LOCK_DEPTH]; + +/* + * The hub routing mutex (hub->lock) serializes port switching and forward= ing. + * A child hub holds its routing mutex while reaching a parent hub that ta= kes + * its own, so it needs the same per-depth lock_class_key treatment as the= bus + * lock above, keyed identically (top-level hub depth 1, and so on). The c= lass + * is assigned once in i3c_hub_init(), not per port, because all ports on = a hub + * share this single routing mutex. + */ +static struct lock_class_key i3c_hub_routing_lock_keys[I3C_HUB_MAX_LOCK_DE= PTH]; + +/** + * i3c_hub_controller_depth() - Count hub nesting levels above a controller + * @controller: Virtual hub controller being initialized + * + * Walk the parent chain and count how many stacked hub controllers lead to + * @controller. A top-level hub attached to a physical controller returns = 1. + * The walk stops at the first non-hub (physical) controller. + * + * Return: The hub nesting depth (>=3D 1 for a hub controller). + */ +static unsigned int +i3c_hub_controller_depth(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + unsigned int depth =3D 0; + + while (controller && controller->ops =3D=3D &i3c_hub_master_ops) { + hub_controller =3D dev_get_drvdata(&controller->dev); + if (!hub_controller) + break; + controller =3D hub_controller->parent; + depth++; + } + + return depth; +} + +/** + * i3c_hub_master_bus_init() - Bind controller to hub device + * @controller: Virtual controller for a hub port + * + * Associates the virtual controller with the hub device descriptor so that + * transfers are executed through the hub on the parent bus. + */ +static int i3c_hub_master_bus_init(struct i3c_master_controller *controlle= r) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + unsigned int depth; + + hub_controller =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D hub_controller->hub; + + if (!hub->hub_dev) + return -ENODEV; + + /* + * Give this virtual bus lock a lockdep class keyed on its hub nesting + * depth before the core runs the first DAA (which forwards to the + * parent bus and takes the parent lock while this one is held). The + * lock is not held here, and controller->ops is already set, so the + * class can be assigned safely. Deeper hubs than the key array + * supports fall back to the shared class and may warn under lockdep, + * but still function correctly. + */ + depth =3D i3c_hub_controller_depth(controller); + if (depth >=3D 1 && depth <=3D I3C_HUB_MAX_LOCK_DEPTH) + lockdep_set_class(&controller->bus.lock, + &i3c_hub_bus_lock_keys[depth - 1]); + else + WARN_ONCE(1, "i3c-hub: nesting depth %u exceeds lockdep support\n", + depth); + + controller->this =3D hub->hub_dev->desc; + return 0; +} + +static void i3c_hub_master_bus_cleanup(struct i3c_master_controller *contr= oller) +{ + controller->this =3D NULL; +} + +static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *controller =3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D hub_controller->hub; + if (!hub->hub_dev) + return -ENODEV; + + parent =3D i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return -ENODEV; + + data =3D kzalloc_obj(*data); + if (!data) + return -ENOMEM; + + /* Initialize the parent-facing descriptor to target the physical parent.= */ + INIT_LIST_HEAD(&data->parent_desc.common.node); + mutex_init(&data->parent_desc.ibi_lock); + + data->parent_desc.common.master =3D parent; + data->parent_desc.info =3D dev->info; + + i3c_bus_maintenance_lock(&parent->bus); + ret =3D i3c_master_attach_i3c_dev_controller_locked(&data->parent_desc); + i3c_bus_maintenance_unlock(&parent->bus); + if (ret) { + mutex_destroy(&data->parent_desc.ibi_lock); + kfree(data); + return ret; + } + + /* Link the hub-private data (see struct i3c_hub_dev_data). */ + i3c_dev_set_master_data(dev, data); + + return 0; +} + +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev, + u8 old_dyn_addr) +{ + struct i3c_hub_dev_data *data =3D i3c_dev_get_master_data(dev); + struct i3c_master_controller *parent; + int ret; + + if (!data) + return -ENODEV; + + parent =3D i3c_dev_get_master(&data->parent_desc); + if (!parent) + return -ENODEV; + + /* + * Reattach must not race asynchronous IBI delivery on the parent-facing + * descriptor. Once IBI resources are requested the parent controller may + * use parent_desc concurrently, so reject a reattach that arrives while + * the generic IBI object is still live. + */ + if (WARN_ON_ONCE(data->parent_desc.ibi)) + return -EBUSY; + + /* + * Re-sync device information after the address change and reattach + * under the parent bus lock so both updates are applied as one + * operation with respect to parent controller state. + */ + i3c_bus_maintenance_lock(&parent->bus); + data->parent_desc.info =3D dev->info; + ret =3D i3c_master_reattach_i3c_dev_controller_locked(&data->parent_desc, + old_dyn_addr); + i3c_bus_maintenance_unlock(&parent->bus); + + return ret; +} + +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev) +{ + struct i3c_hub_dev_data *data =3D i3c_dev_get_master_data(dev); + struct i3c_master_controller *parent; + + if (!data) + return; + + parent =3D i3c_dev_get_master(&data->parent_desc); + + /* + * parent_desc.ibi should already be cleared by i3c_hub_free_ibi() + * before we get here. If it is still set, the kfree(data) below frees + * a descriptor the parent controller can still reach via un-flushed + * asynchronous IBI work (use-after-free, not just a leak). + */ + WARN_ON_ONCE(data->parent_desc.ibi); + + if (parent) { + i3c_bus_maintenance_lock(&parent->bus); + i3c_master_detach_i3c_dev_controller_locked(&data->parent_desc); + i3c_bus_maintenance_unlock(&parent->bus); + } + + i3c_dev_set_master_data(dev, NULL); + mutex_destroy(&data->parent_desc.ibi_lock); + kfree(data); +} + +/** + * i3c_hub_do_daa() - Perform DAA via hub port + * @hub: Hub instance + * @controller: Virtual controller for a hub port + * + * Enables the port connection, performs DAA on the parent controller, + * then disables the connection. + */ +static int i3c_hub_do_daa(struct i3c_hub *hub, + struct i3c_master_controller *controller) +{ + struct i3c_master_controller *parent; + int ret; + + if (!hub || !hub->hub_dev) + return -ENODEV; + + parent =3D i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return -ENODEV; + + /* + * Lock order: hub routing mutex before the parent bus lock (taken here + * inside i3c_master_do_daa()). The depth-keyed lockdep classes above + * keep this nesting acyclic when the parent is itself a hub. + */ + mutex_lock(&hub->lock); + i3c_hub_enable_port(controller); + + /* + * Downstream devices reachable through hub target-port routes share the + * parent controller's I3C address space. The hub gates access to a + * target-port network, but it does not create an independent dynamic + * address domain per virtual bus. + * + * Run DAA on the parent controller so dynamic addresses remain unique + * across all downstream devices, even when they are behind different + * target ports. + * + * The current hub model shares the parent controller's dynamic-address + * domain and does not support independent per-port DAA domains, so a + * DAA result does not go stale behind a port toggle. Addresses reserved + * from the Device Tree are held for the lifetime of the parent bus and + * are never released on a port toggle, so enabling or disabling a port + * only gates reachability and never causes an address to be handed out + * twice. + */ + ret =3D 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 =3D 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. + * + * A hub does not isolate the CCC address space of its virtual buses from = the + * parent bus. A broadcast CCC issued on one virtual bus is forwarded to t= he + * parent controller and therefore reaches every device on the parent bus,= as + * well as the downstream devices of any other connected target port. Driv= ers + * that need per-device scope must use the directed form of the command. + */ +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 =3D i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return -ENODEV; + + mutex_lock(&hub->lock); + i3c_hub_enable_port(controller); + ret =3D i3c_master_send_ccc_cmd(parent, cmd); + i3c_hub_disable_port(controller); + mutex_unlock(&hub->lock); + + return ret; +} + +/** + * i3c_hub_master_priv_xfers() - Execute private transfers via hub + * @dev: Target device descriptor + * @xfers: Transfer array + * @nxfers: Number of transfers + * @mode: Transfer mode (SDR, HDR, etc.) + * + * Refreshes the parent-facing device info (while no IBI is pending) and + * forwards private transfers through the hub to the parent controller. + */ +static int i3c_hub_master_priv_xfers(struct i3c_dev_desc *dev, + struct i3c_xfer *xfers, + int nxfers, + enum i3c_xfer_mode mode) +{ + struct i3c_master_controller *controller =3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D hub_controller->hub; + + data =3D i3c_dev_get_master_data(dev); + if (!data) + return -ENODEV; + + parent =3D i3c_dev_get_master(&data->parent_desc); + if (!parent) + return -ENODEV; + + /* Lock order: hub routing mutex before the parent bus lock (see do_daa).= */ + mutex_lock(&hub->lock); + + /* + * Only refresh the parent-facing info while no IBI is requested; once + * parent_desc.ibi is set it must stay immutable (see i3c_hub_request_ibi= ()). + */ + if (!data->parent_desc.ibi) + data->parent_desc.info =3D dev->info; + + i3c_hub_enable_port(controller); + + i3c_bus_normaluse_lock(&parent->bus); + ret =3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D hub_controller->hub; + + return i3c_hub_do_daa(hub, controller); +} + +static int i3c_hub_master_send_ccc_cmd(struct i3c_master_controller *contr= oller, + struct i3c_ccc_cmd *cmd) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D 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 =3D=3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return false; + + hub =3D 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 descrip= tor + * and requests IBI for a device connected through the hub. The parent-fac= ing + * descriptor references the same IBI object so the physical controller us= es + * 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 =3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D hub_controller->hub; + + data =3D i3c_dev_get_master_data(desc); + if (!data) + return -ENODEV; + + parent =3D 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 =3D desc->info; + data->parent_desc.dev =3D desc->dev; + data->parent_desc.ibi =3D desc->ibi; + mutex_unlock(&hub->lock); + + i3c_bus_normaluse_lock(&parent->bus); + ret =3D 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 =3D NULL; + data->parent_desc.dev =3D NULL; + mutex_unlock(&hub->lock); + } + + return ret; +} + +static void i3c_hub_free_ibi(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller =3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub =3D hub_controller->hub; + + data =3D i3c_dev_get_master_data(desc); + if (!data || !data->parent_desc.ibi) + return; + + parent =3D 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 =3D NULL; + data->parent_desc.dev =3D 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 pare= nt + * controller. + */ +static int i3c_hub_enable_ibi(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller =3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D hub_controller->hub; + + data =3D i3c_dev_get_master_data(desc); + if (!data || !data->parent_desc.ibi) + return -ENODEV; + + parent =3D 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 =3D 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 par= ent + * controller. + */ +static int i3c_hub_disable_ibi(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller =3D 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 =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub =3D hub_controller->hub; + + data =3D i3c_dev_get_master_data(desc); + if (!data || !data->parent_desc.ibi) + return -ENODEV; + + parent =3D 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 =3D 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 =3D i3c_dev_get_master_data(desc); + + if (!data) + return; + + i3c_dev_recycle_ibi_slot_controller(&data->parent_desc, slot); +} + +const struct i3c_master_controller_ops i3c_hub_master_ops =3D { + .bus_init =3D i3c_hub_master_bus_init, + .bus_cleanup =3D i3c_hub_master_bus_cleanup, + .attach_i3c_dev =3D i3c_hub_attach_i3c_dev, + .reattach_i3c_dev =3D i3c_hub_reattach_i3c_dev, + .detach_i3c_dev =3D i3c_hub_detach_i3c_dev, + .do_daa =3D i3c_hub_master_do_daa, + .supports_ccc_cmd =3D i3c_hub_master_supports_ccc_cmd, + .send_ccc_cmd =3D i3c_hub_master_send_ccc_cmd, + .i3c_xfers =3D i3c_hub_master_priv_xfers, + .attach_i2c_dev =3D i3c_hub_attach_i2c_dev, + .detach_i2c_dev =3D i3c_hub_detach_i2c_dev, + .i2c_xfers =3D i3c_hub_i2c_xfers, + .request_ibi =3D i3c_hub_request_ibi, + .free_ibi =3D i3c_hub_free_ibi, + .enable_ibi =3D i3c_hub_enable_ibi, + .disable_ibi =3D i3c_hub_disable_ibi, + .recycle_ibi_slot =3D i3c_hub_recycle_ibi_slot, +}; +EXPORT_SYMBOL_GPL(i3c_hub_master_ops); + +/** + * i3c_hub_set_routing_lock_class() - Key the routing mutex for lockdep + * @hub: Hub instance whose routing mutex has just been initialized + * + * Give @hub->lock a lockdep class keyed on the hub nesting depth, so that= a + * child hub taking its routing mutex while a parent hub holds its own is = not + * reported as recursive locking. Does nothing when lockdep is disabled. + */ +static void i3c_hub_set_routing_lock_class(struct i3c_hub *hub) +{ + struct i3c_master_controller *parent; + unsigned int depth; + + if (!IS_ENABLED(CONFIG_LOCKDEP)) + return; + + /* + * The routing mutex has the same hub nesting depth as the virtual + * controllers this hub exposes, so the parent controller is one level + * shallower. Keying it once here, rather than per port, avoids + * reclassifying the single shared routing mutex from a later port that + * may already have used it. + */ + parent =3D i3c_dev_get_master(hub->hub_dev->desc); + depth =3D i3c_hub_controller_depth(parent) + 1; + if (WARN_ONCE(depth > I3C_HUB_MAX_LOCK_DEPTH, + "i3c-hub: routing lock depth %u exceeds lockdep support\n", + depth)) + depth =3D I3C_HUB_MAX_LOCK_DEPTH; + + lockdep_set_class(&hub->lock, &i3c_hub_routing_lock_keys[depth - 1]); +} + +/** + * i3c_hub_init() - Initialize hub context + * @hub: Hub instance to initialize + * @ops: Vendor callbacks used to connect and disconnect target ports + * @hub_dev: I3C device representing the hub on the parent bus + * + * Store the vendor callbacks and the hub device in @hub and initialize the + * routing mutex used to serialize port switching and forwarding. + * + * @hub_dev must be a registered I3C device, so it already has a descriptor + * and a parent controller. + */ +void i3c_hub_init(struct i3c_hub *hub, + const struct i3c_hub_ops *ops, + struct i3c_device *hub_dev) +{ + hub->ops =3D ops; + hub->hub_dev =3D hub_dev; + mutex_init(&hub->lock); + + i3c_hub_set_routing_lock_class(hub); +} +EXPORT_SYMBOL_GPL(i3c_hub_init); + +/** + * i3c_hub_reserve_parent_addrslots_from_dt() - Reserve child addresses in= parent bus. + * @hubc: I3C hub controller for a target-port virtual bus. + * @node: Target-port bus Device Tree node. + * + * Reserve parent bus address slots for the assigned dynamic address of ea= ch + * downstream I3C device described in the Device Tree, so that parent DAA = does + * not hand the same address to another device. Downstream devices behind = hub + * target ports share the parent controller's dynamic address space. + * + * The reservations describe a fixed Device Tree topology and are kept for= the + * lifetime of the parent bus; they are not released when a target port is + * disconnected. If the hub is removed and probed again it re-reads the sa= me + * Device Tree and reserves the same addresses, so repeated probing does n= ot + * consume additional address space. + * + * Only the assigned dynamic addresses are reserved here. A hub that suppo= rts + * hot-joining downstream devices would additionally have to reserve the s= tatic + * addresses of every device that may appear. Supporting runtime hub hotpl= ug + * would also require tracking ownership of these reservations and releasi= ng + * them when a hub is removed. + * + * Return: 0 on success, or a negative error code. + */ +int i3c_hub_reserve_parent_addrslots_from_dt(struct i3c_hub_controller *hu= bc, + struct device_node *node) +{ + struct i3c_master_controller *parent =3D hubc->parent; + enum i3c_addr_slot_status status; + u32 assigned_addr; + u32 reg[3]; + int ret; + + if (!parent || !node) + return -ENODEV; + + for_each_available_child_of_node_scoped(node, child) { + /* + * Only consider addressable bus nodes: a valid "reg" is + * required to describe a device, but its static-address value + * does not affect the reservation below. + */ + ret =3D of_property_read_variable_u32_array(child, "reg", reg, 1, 3); + if (ret < 0) + continue; + + ret =3D of_property_read_u32(child, "assigned-address", &assigned_addr); + if (ret) + continue; + + /* Skip nodes without a usable dynamic address. */ + if (!assigned_addr || assigned_addr > I3C_MAX_ADDR) + continue; + + i3c_bus_maintenance_lock(&parent->bus); + status =3D i3c_bus_get_addr_slot_status(&parent->bus, + assigned_addr); + if (status =3D=3D 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 "); +MODULE_AUTHOR("Vikash Bansal "); +MODULE_AUTHOR("Lakshay Piplani "); +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..a368ea9e5ef7 --- /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 +#include + +/** + * struct i3c_hub - Generic I3C hub context + * @ops: Vendor callbacks for port connection control + * @hub_dev: I3C device representing the hub on the parent bus + * @lock: Serializes hub port routing/forwarding; its lockdep class is key= ed + * per hub nesting depth in i3c_hub_init(). + */ +struct i3c_hub { + const struct i3c_hub_ops *ops; + struct i3c_device *hub_dev; + struct mutex lock; /* Serializes hub port routing. */ +}; + +struct i3c_hub_controller { + struct i3c_master_controller *parent; + struct i3c_master_controller controller; + struct i3c_hub *hub; +}; + +struct i3c_hub_ops { + void (*enable_port)(struct i3c_master_controller *controller); + void (*disable_port)(struct i3c_master_controller *controller); +}; + +/** + * i3c_hub_enable_port() - Enable hub connection for a controller + * @controller: Virtual controller representing a hub port + * + * Retrieves hub context from controller drvdata and invokes the vendor + * callback to enable the associated port connection. + */ +static inline void i3c_hub_enable_port(struct i3c_master_controller *contr= oller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub =3D 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 *cont= roller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller =3D dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub =3D hub_controller->hub; + + if (hub && hub->ops && hub->ops->disable_port) + hub->ops->disable_port(controller); +} + +/* + * Controller operations used by the virtual controllers created for hub + * target ports. Hub drivers pass this to i3c_master_register_fwnode(). + */ +extern const struct i3c_master_controller_ops i3c_hub_master_ops; + +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 *hu= bc, + struct device_node *node); +#endif --=20 2.25.1 From nobody Thu Sep 24 16:08:57 2026 Received: from inva020.nxp.com (inva020.nxp.com [92.121.34.13]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 50F2551AED5; Tue, 22 Sep 2026 10:36:36 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.13 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073401; cv=none; b=LRXGPgJDu2l6gsTmoW/bXM+x6PQI5fzYFNrJlmHxSMupd0XwTq6Giw9TXwySlXANE3uow8Qmrb/tOzqGQOXRckh1K16wyQd+gWSo5nQilS2C2xC1tcDSd5/sTLPPXJdWbY2JCz3/aT24m7eXjC5BVaJkbXkOqao31KzekMBZ6Mg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073401; c=relaxed/simple; bh=PiFMd5tOSNH+WhMjrIPzcVESX5UZxyig+iQNZ9bnoBw=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=UXckVom+X9eeOFO7kqcXxj4UIzwkY4SwIcGft/D3+QZCeVkeD2wTLVK/6sKKqPNtMzjuHhqXvKd9Z/H5bjPIfPdEI74osm4LA2u8wybnG4bPmTmIOp+3+Q5zsIAeiOb10YvJmliAoT08P6oFNbDy8cpORCrv7lXhWLLkbI4ArfQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.13 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva020.nxp.com (localhost [127.0.0.1]) by inva020.eu-rdc02.nxp.com (Postfix) with ESMTP id 978F31A0011; Tue, 22 Sep 2026 12:36:29 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva020.eu-rdc02.nxp.com (Postfix) with ESMTP id 0D9991A0005; Tue, 22 Sep 2026 12:36:29 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id 5E1E8180006C; Tue, 22 Sep 2026 18:36:25 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani Subject: [PATCH v20 7/8] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality Date: Tue, 22 Sep 2026 16:05:49 +0530 Message-Id: <20260922103551.2754613-8-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" From: Aman Kumar Pandey 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 Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Frank Li --- Changes in v20: - Add the generic I3C hub MAINTAINERS entries moved from patch 6 Changes in v19: - No change Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes Changes in v17: - Remove the fixed post-enable delay; regulator settling is now handled by the regulator core - Use a named initializer for the platform_device_id table - Document why I3C target ports are connected once and left connected, making their per-operation enable and disable callbacks no-ops - Reference the exported i3c_hub_master_ops object directly Changes in v16: - Do not ignore regulator-enable failures: p3h2x4x_configure_ldo() now returns via dev_err_probe() for any error other than -ENODEV (optional supply), instead of only warning - Widen the SMBus poll deadline: floor the timeout at the datasheet SDA-stuck/SCL-low recovery window (P3H2X4X_SMBUS_SCL_LOW_RECOVERY_US, 50 ms) via max(xfer_us, ...), so an in-spec SDA-stuck/SCL-low bus recove= ry no longer causes spurious timeouts. The transfer-time estimate now only drives the poll interval, not the give-up deadline - Use i2c_get_dma_safe_msg_buf() and i2c_put_dma_safe_msg_buf() for controller-agent read transfers before passing the buffer to regmap_bulk_read() over I3C - Document the i3c_hub_priv publish/clear ordering: publish before IBI is enabled and clear it via devm_add_action_or_reset() so, on unwind, the pointer is cleared only after IBI is disabled and freed - Convert the SMBus register-protection sequence to scoped_guard() to avoid mixing goto- and scope-based cleanup in one function - Keep the virtual controllers in the devm-managed hub struct; teardown is ordered so i3c_master_unregister() runs (via devm action) before the memory is freed Changes in v15: - Use the target-port count detected by the MFD parent and only configure registers and ports implemented by the selected device variant - Validate target-port indices against the detected number of ports - Preserve the MFD parent's driver data and publish the hub context through the shared MFD structure, with managed cleanup - Correct the SMBus transfer timeout calculation for 400 kHz operation - Use I2C adapter quirks to enforce the maximum read and write payload lengths Changes in v14: - Replace temporary parent dev->of_node reassignment with i3c_master_register_fwnode() - Use the shared MFD protected_reg_lock for protected hub configuration writes - Fix SMBus polling interval calculation to avoid oversleeping the computed transaction timeout - Change SMBus transfer loop counters from u8 to int - Clean up already registered SMBus adapters on adapter allocation or registration failure Changes in v13: - Fix SMBus transaction handling by replacing fixed delay with polling (re= ad_poll_timeout) to avoid premature reads and data corruption - Fix DT and of_node handling: prevent duplicate target-port node leaks an= d avoid corrupting parent dev->of_node by restoring it after registration - Add proper cleanup using devm actions (relock registers, release DT node= s, 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 resourc= es 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 regulat= or Changes in v2: - Refined coding style and incorporated review feedback - Updated directory structure - Revised logic for parsing DTS nodes --- --- MAINTAINERS | 3 + drivers/i3c/Kconfig | 1 + drivers/i3c/Makefile | 1 + drivers/i3c/hub/Kconfig | 11 + drivers/i3c/hub/Makefile | 4 + drivers/i3c/hub/p3h2840_i3c_hub.h | 343 +++++++++++++++++++ drivers/i3c/hub/p3h2840_i3c_hub_common.c | 413 +++++++++++++++++++++++ drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 126 +++++++ drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 347 +++++++++++++++++++ 9 files changed, 1249 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 3abba881ba82..603373da9d7e 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19767,8 +19767,11 @@ 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/i3c/hub/* 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 =20 NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER 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 =20 Say Y here if your platform includes an I3C hub device =20 +source "drivers/i3c/hub/Kconfig" endif # I3C =20 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 :=3D device.o master.o obj-$(CONFIG_I3C) +=3D i3c.o obj-$(CONFIG_I3C) +=3D master/ obj-$(CONFIG_I3C_HUB) +=3D hub.o +obj-$(CONFIG_I3C_HUB) +=3D hub/ diff --git a/drivers/i3c/hub/Kconfig b/drivers/i3c/hub/Kconfig new file mode 100644 index 000000000000..969bb1f637dd --- /dev/null +++ b/drivers/i3c/hub/Kconfig @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright 2025 NXP +config P3H2X4X_I3C_HUB + tristate "NXP P3H2X4X I3C HUB support" + depends on MFD_P3H2X4X + select I3C_HUB + help + This enables support for NXP P3H244x/P3H284x I3C HUB. These hubs + connect to a host via I3C/I2C/SMBus and allow communication with + multiple downstream peripherals. Say Y or M here to enable the + P3H2x4x I3C HUB driver. diff --git a/drivers/i3c/hub/Makefile b/drivers/i3c/hub/Makefile new file mode 100644 index 000000000000..9dbd8a7b4184 --- /dev/null +++ b/drivers/i3c/hub/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright 2025 NXP +p3h2840_i3c_hub-y :=3D p3h2840_i3c_hub_common.o p3h2840_i3c_hub_i3c.o p3h2= 840_i3c_hub_smbus.o +obj-$(CONFIG_P3H2X4X_I3C_HUB) +=3D p3h2840_i3c_hub.o diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3= c_hub.h new file mode 100644 index 000000000000..7a1345924e3f --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub.h @@ -0,0 +1,343 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright 2025-2026 NXP + * Private definitions for the NXP P3H2X4X I3C hub driver. + */ + +#ifndef P3H2840_I3C_HUB_H +#define P3H2840_I3C_HUB_H + +#include +#include +#include +#include +#include +#include +#include + +/* 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 applicati= ons to + * avoid MCTP transmitters on Target Ports time out when the SMBus agent b= uffers + * are not serviced in time by upstream controller and only receives write= message + * from its downstream ports. + * SMBUS_AGENT_TX_RX_LOOPBACK_EN/TARGET_AGENT_BUF_FULL_SDA_LOW_EN + */ + +#define P3H2X4X_TARGET_AGENT_DFT_IBI_CONF 0x20 +#define P3H2X4X_TARGET_AGENT_DFT_IBI_CONF_MASK 0x21 + +/* Transaction status checking mask */ +#define P3H2X4X_SMBUS_TRANSACTION_FINISH_FLAG 1 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SHIFT 4 + +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_OK 0 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ADDR_NAK 1 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_DATA_NAK 2 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_WTR_NAK 3 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SYNC_RCV 4 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SYNC_RCVCLR 5 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_FAULT 6 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ARB_LOSS 7 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SCL_TO 8 + +#define P3H2X4X_TP_BUFFER_STATUS_MASK 0x0f +#define P3H2X4X_TP_TRANSACTION_CODE_MASK 0xf0 + +/* SMBus transaction types fields */ +#define P3H2X4X_SMBUS_400kHz BIT(2) + +/* SMBus polling */ +#define P3H2X4X_SMBUS_POLL_COUNT 10 +#define P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US 20 +#define P3H2X4X_SMBUS_POLL_INTERVAL_MAX_US 150 + +/* Hub buffer size */ +#define P3H2X4X_CONTROLLER_BUFFER_SIZE 88 +#define P3H2X4X_TARGET_BUFFER_SIZE 80 +#define P3H2X4X_SMBUS_DESCRIPTOR_SIZE 4 +#define P3H2X4X_SMBUS_PAYLOAD_SIZE \ + (P3H2X4X_CONTROLLER_BUFFER_SIZE - P3H2X4X_SMBUS_DESCRIPTOR_SIZE) +#define P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE (P3H2X4X_TARGET_BUFFER_SIZE - 2) + +/* + * At 400 kHz, one 9-bit I2C byte takes 22.5 us. Round this + * up to 23 us per payload byte and add 100 us for the address + * byte, bus overhead and controller processing time. This models + * the time a healthy transaction takes and is used to derive the + * poll interval, not the overall give-up deadline. + */ +#define P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(x) ((23 * (x)) + 100) + +/* + * On an SDA-stuck condition the SMBus Controller Agent can legitimately + * hold SCL low for up to 35 ms during bus recovery (datasheet 8.11.2, + * Table 21). Floor the overall poll deadline above that so the recovery, + * status posting and the status read-back complete before we give up and + * report a spurious timeout. + */ +#define P3H2X4X_SMBUS_SCL_LOW_RECOVERY_US 50000 + +#define P3H2X4X_NO_PAGE_PER_TP 4 + +#define P3H2X4X_MAX_PAYLOAD_LEN 2 +#define P3H2X4X_NUM_SLOTS 6 + +#define P3H2X4X_HUB_ID 0 + +#define P3H2X4X_SET_BIT(n) BIT(n) + +#define P3H2X4X_TP_MASK GENMASK(P3H2X4X_TP_MAX_COUNT - 1, 0) + +#define P3H2X4X_DFT_TP_PULLUP_OHMS 500 +#define P3H2X4X_DFT_IO_STRENGTH_OHMS 20 + +enum p3h2x4x_tp { + TP_0, + TP_1, + TP_2, + TP_3, + TP_4, + TP_5, + TP_6, + TP_7, +}; + +enum p3h2x4x_rcv_buf { + RCV_BUF_0, + RCV_BUF_1, + RCV_BUF_OF, +}; + +enum p3h2x4x_tp_mode { + P3H2X4X_TP_MODE_I3C, + P3H2X4X_TP_MODE_SMBUS, +}; + +struct tp_configuration { + bool pullup_en; + bool ibi_en; + bool always_enable; + enum p3h2x4x_tp_mode mode; +}; + +struct hub_configuration { + int tp0145_pullup; + int tp2367_pullup; + int cp0_io_strength; + int cp1_io_strength; + int tp0145_io_strength; + int tp2367_io_strength; + struct tp_configuration tp_config[P3H2X4X_TP_MAX_COUNT]; +}; + +struct tp_bus { + bool is_registered; /* bus was registered in the framework. */ + u8 tp_mask; + u8 tp_port; + struct mutex port_mutex; /* per port mutex */ + struct device_node *of_node; + struct i2c_client *tp_smbus_client; + struct i2c_adapter *tp_smbus_adapter; + struct i3c_hub_controller hub_controller; + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub; +}; + +struct p3h2x4x_i3c_hub_dev { + struct device *dev; + struct regmap *regmap; + struct mutex etx_mutex; /* all port mutex */ + struct i3c_device *i3cdev; + struct i2c_client *i2c_client; + struct hub_configuration hub_config; + struct tp_bus tp_bus[P3H2X4X_TP_MAX_COUNT]; + struct i3c_hub *hub; +}; + +/** + * p3h2x4x_unregister_smbus_adapters() - unregister SMBus adapters + * @hub: P3H2x4x hub device + */ +void p3h2x4x_unregister_smbus_adapters(struct p3h2x4x_i3c_hub_dev *hub); + +/** + * p3h2x4x_tp_smbus_algo - add i2c adapter for target port configured as S= MBus. + * @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 configure= d 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/p3h= 2840_i3c_hub_common.c new file mode 100644 index 000000000000..0c18459d7ec6 --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c @@ -0,0 +1,413 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * This P3H2X4X driver file implements functions for Hub probe and DT pars= ing. + */ + +#include +#include +#include +#include +#include + +#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[] =3D { + 250, 500, 1000, 2000 +}; + +static const int p3h2x4x_io_strength_tbl[] =3D { + 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 =3D dev_get_drvdata(dev); + u8 pullup; + + pullup =3D P3H2X4X_TP0145_PULLUP_CONF(p3h2x4x_pullup_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.tp0145_pullup)); + + pullup |=3D 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 =3D dev_get_drvdata(dev); + u8 io_strength; + + io_strength =3D P3H2X4X_CP0_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.cp0_io_strength)); + + io_strength |=3D P3H2X4X_CP1_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.cp1_io_strength)); + + io_strength |=3D P3H2X4X_TP0145_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.tp0145_io_strength)); + + io_strength |=3D 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[] =3D { + "vcc1", + "vcc2", + "vcc3", + "vcc4" + }; + int ret, i; + + for (i =3D 0; i < ARRAY_SIZE(supplies); i++) { + ret =3D devm_regulator_get_enable_optional(dev, supplies[i]); + if (ret && ret !=3D -ENODEV) + return dev_err_probe(dev, ret, "Failed to enable %s\n", + supplies[i]); + } + + return 0; +} + +static int p3h2x4x_configure_tp(struct device *dev) +{ + struct p3h2x4x_i3c_hub_dev *hub =3D dev_get_drvdata(dev); + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(dev->parent); + u8 mode =3D 0, smbus =3D 0, pullup =3D 0, target_port =3D 0; + u8 tp_mask; + int tp, ret; + + for (tp =3D 0; tp < p3h2x4x->num_target_ports; tp++) { + pullup |=3D hub->hub_config.tp_config[tp].pullup_en ? P3H2X4X_SET_BIT(tp= ) : 0; + mode |=3D (hub->hub_config.tp_config[tp].mode !=3D P3H2X4X_TP_MODE_I3C) ? + P3H2X4X_SET_BIT(tp) : 0; + smbus |=3D (hub->hub_config.tp_config[tp].mode =3D=3D P3H2X4X_TP_MODE_SM= BUS) ? + P3H2X4X_SET_BIT(tp) : 0; + target_port |=3D (hub->tp_bus[tp].tp_mask =3D=3D P3H2X4X_SET_BIT(tp)) ? + hub->tp_bus[tp].tp_mask : 0; + } + + /* Only touch the bits for the target ports this variant provides. */ + tp_mask =3D GENMASK(p3h2x4x->num_target_ports - 1, 0); + + ret =3D regmap_update_bits(hub->regmap, P3H2X4X_TP_PULLUP_EN, tp_mask, pu= llup); + if (ret) + return ret; + + ret =3D regmap_update_bits(hub->regmap, P3H2X4X_TP_IO_MODE_CONF, tp_mask,= mode); + if (ret) + return ret; + + ret =3D regmap_update_bits(hub->regmap, P3H2X4X_TP_SMBUS_AGNT_EN, tp_mask= , smbus); + if (ret) + return ret; + + if (target_port & ~smbus) { + ret =3D 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 =3D dev_get_drvdata(dev); + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(dev->parent); + int ret, ret2; + + ret =3D p3h2x4x_configure_ldo(dev); + if (ret) + return ret; + + /* Protect the unlock-modify-lock sequence with the shared MFD lock */ + scoped_guard(mutex, &p3h2x4x->protected_reg_lock) { + ret =3D regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_UNLOCK_CODE); + if (ret) + break; + + ret =3D p3h2x4x_configure_pullup(dev); + if (!ret) + ret =3D p3h2x4x_configure_io_strength(dev); + if (!ret) + ret =3D p3h2x4x_configure_tp(dev); + + ret2 =3D regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_LOCK_CODE); + if (!ret && ret2) + ret =3D ret2; + } + + return ret; +} + +static void p3h2x4x_get_target_port_dt_conf(struct device *dev, + const struct device_node *node) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub =3D dev_get_drvdata(dev); + struct p3h2x4x *p3h2x4x =3D 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 =3D of_node_get(dev_node); + p3h2x4x_i3c_hub->tp_bus[tp_port].tp_mask =3D P3H2X4X_SET_BIT(tp_port); + p3h2x4x_i3c_hub->tp_bus[tp_port].p3h2x4x_i3c_hub =3D p3h2x4x_i3c_hub; + p3h2x4x_i3c_hub->tp_bus[tp_port].tp_port =3D 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 =3D 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 =3D P3H2X4X_TP_MODE_I3C; + else if (of_node_name_eq(tp_node, "smbus")) + mode =3D P3H2X4X_TP_MODE_SMBUS; + else + continue; + + ret =3D 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 >=3D p3h2x4x->num_target_ports) + return dev_err_probe(dev, -EINVAL, + "Invalid target port index %llu\n", + id); + + tp_config[id].mode =3D mode; + tp_config[id].pullup_en =3D + 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 =3D 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 =3D dev_get_drvdata(dev); + int tp_count; + + p3h2x4x_i3c_hub->hub_config.tp0145_pullup =3D P3H2X4X_DFT_TP_PULLUP_OHMS; + p3h2x4x_i3c_hub->hub_config.tp2367_pullup =3D P3H2X4X_DFT_TP_PULLUP_OHMS; + p3h2x4x_i3c_hub->hub_config.cp0_io_strength =3D P3H2X4X_DFT_IO_STRENGTH_O= HMS; + p3h2x4x_i3c_hub->hub_config.cp1_io_strength =3D P3H2X4X_DFT_IO_STRENGTH_O= HMS; + p3h2x4x_i3c_hub->hub_config.tp0145_io_strength =3D P3H2X4X_DFT_IO_STRENGT= H_OHMS; + p3h2x4x_i3c_hub->hub_config.tp2367_io_strength =3D P3H2X4X_DFT_IO_STRENGT= H_OHMS; + + for (tp_count =3D 0; tp_count < P3H2X4X_TP_MAX_COUNT; ++tp_count) + p3h2x4x_i3c_hub->hub_config.tp_config[tp_count].mode =3D 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 =3D data; + int tp; + + for (tp =3D 0; tp < P3H2X4X_TP_MAX_COUNT; tp++) { + of_node_put(hub->tp_bus[tp].of_node); + hub->tp_bus[tp].of_node =3D NULL; + } +} + +static void p3h2x4x_clear_i3c_hub_priv(void *data) +{ + struct p3h2x4x *p3h2x4x =3D data; + + /* Drop the IBI handler backpointer; see the ordering note at the registr= ation site. */ + p3h2x4x->i3c_hub_priv =3D NULL; +} + +static int p3h2x4x_i3c_hub_probe(struct platform_device *pdev) +{ + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(pdev->dev.parent); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub; + struct device *dev =3D &pdev->dev; + struct device_node *node; + int ret, i; + + p3h2x4x_i3c_hub =3D devm_kzalloc(dev, sizeof(*p3h2x4x_i3c_hub), GFP_KERNE= L); + if (!p3h2x4x_i3c_hub) + return -ENOMEM; + + p3h2x4x_i3c_hub->regmap =3D p3h2x4x->regmap; + p3h2x4x_i3c_hub->dev =3D dev; + + platform_set_drvdata(pdev, p3h2x4x_i3c_hub); + device_set_of_node_from_dev(dev, dev->parent); + + p3h2x4x_default_configuration(dev); + + ret =3D devm_mutex_init(dev, &p3h2x4x_i3c_hub->etx_mutex); + if (ret) + return ret; + + for (i =3D 0; i < P3H2X4X_TP_MAX_COUNT; i++) { + ret =3D devm_mutex_init(dev, &p3h2x4x_i3c_hub->tp_bus[i].port_mutex); + if (ret) + return ret; + } + + /* get hub node from DT */ + node =3D dev_of_node(dev); + if (!node) + return dev_err_probe(dev, -ENODEV, "No Device Tree entry found\n"); + + ret =3D p3h2x4x_get_hub_dt_conf(dev, node); + if (ret) + return ret; + + p3h2x4x_get_target_port_dt_conf(dev, node); + + ret =3D devm_add_action_or_reset(dev, + p3h2x4x_put_target_port_of_nodes, + p3h2x4x_i3c_hub); + if (ret) + return ret; + + ret =3D 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 =3D 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 =3D 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 =3D devm_add_action_or_reset(dev, p3h2x4x_clear_i3c_hub_priv, + p3h2x4x); + if (ret) + return ret; + + ret =3D 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 =3D p3h2x4x_tp_smbus_algo(p3h2x4x_i3c_hub); + if (ret) + return dev_err_probe(dev, ret, "Failed to add i2c adapter\n"); + + ret =3D 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[] =3D { + { .name =3D "p3h2x4x-i3c-hub" }, + { } +}; +MODULE_DEVICE_TABLE(platform, p3h2x4x_i3c_hub_id); + +static struct platform_driver p3h2x4x_i3c_hub_driver =3D { + .driver =3D { + .name =3D "p3h2x4x-i3c-hub", + .probe_type =3D PROBE_PREFER_ASYNCHRONOUS, + }, + .probe =3D p3h2x4x_i3c_hub_probe, + .id_table =3D p3h2x4x_i3c_hub_id, +}; +module_platform_driver(p3h2x4x_i3c_hub_driver); + +MODULE_AUTHOR("Aman Kumar Pandey "); +MODULE_AUTHOR("Vikash Bansal "); +MODULE_AUTHOR("Lakshay Piplani "); +MODULE_DESCRIPTION("P3H2X4X I3C HUB driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c b/drivers/i3c/hub/p3h284= 0_i3c_hub_i3c.c new file mode 100644 index 000000000000..3c5db7db48aa --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c @@ -0,0 +1,126 @@ +// 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 +#include +#include + +#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 =3D container_of(controller, struct i3c_hub_controller, co= ntroller); + + return container_of(hub_controller, struct tp_bus, hub_controller); +} + +/* + * All I3C target ports share the parent controller's single dynamic-addre= ss + * domain, so they need no per-port isolation. They are connected once, wh= en + * the virtual controllers are registered, and left connected. Marking them + * always_enable makes the connect and disconnect callbacks no-ops, which + * avoids rewriting the network configuration register on every forwarded + * operation and keeps a target-port network reachable between a DAA and a + * later transfer. + */ +static void p3h2x4x_hub_enable_port(struct i3c_master_controller *controll= er) +{ + struct tp_bus *bus =3D p3h2x4x_bus_from_controller(controller); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub =3D 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 *control= ler) +{ + struct tp_bus *bus =3D p3h2x4x_bus_from_controller(controller); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub =3D 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 =3D { + .enable_port =3D p3h2x4x_hub_enable_port, + .disable_port =3D p3h2x4x_hub_disable_port, +}; + +static void p3h2x4x_unregister_i3c_master(void *data) +{ + struct i3c_master_controller *controller =3D 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 =3D i3c_dev_get_master(p3h2x4x_hub->= i3cdev->desc); + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(p3h2x4x_hub->dev->parent); + u8 tp, ntwk_mask =3D 0; + int ret; + + p3h2x4x_hub->hub =3D 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 =3D 0; tp < p3h2x4x->num_target_ports; tp++) { + if (!p3h2x4x_hub->tp_bus[tp].of_node || + p3h2x4x_hub->hub_config.tp_config[tp].mode !=3D P3H2X4X_TP_MODE_I3C) + continue; + + struct i3c_hub_controller *hub_controller =3D + &p3h2x4x_hub->tp_bus[tp].hub_controller; + struct i3c_master_controller *controller =3D &hub_controller->controller; + + hub_controller->parent =3D parent; + hub_controller->hub =3D p3h2x4x_hub->hub; + + dev_set_drvdata(&controller->dev, hub_controller); + + ret =3D i3c_hub_reserve_parent_addrslots_from_dt(hub_controller, + p3h2x4x_hub->tp_bus[tp].of_node); + if (ret) + return ret; + + ret =3D 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 =3D devm_add_action_or_reset(p3h2x4x_hub->dev, + p3h2x4x_unregister_i3c_master, + controller); + if (ret) + return ret; + + ntwk_mask |=3D p3h2x4x_hub->tp_bus[tp].tp_mask; + p3h2x4x_hub->tp_bus[tp].is_registered =3D true; + p3h2x4x_hub->hub_config.tp_config[tp].always_enable =3D true; + } + return regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_ma= sk); +} diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2= 840_i3c_hub_smbus.c new file mode 100644 index 000000000000..a3324cc36595 --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * This P3H2X4X driver file contain functions for SMBus/I2C virtual Bus cr= eation and read/write. + */ +#include +#include + +#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_de= v *hub, + u8 target_port_status, + u8 data_length) +{ + unsigned int xfer_us, timeout_us, sleep_us; + u32 status_read; + u8 status; + int ret; + + xfer_us =3D P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length); + sleep_us =3D clamp(xfer_us / P3H2X4X_SMBUS_POLL_COUNT, + P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US, + P3H2X4X_SMBUS_POLL_INTERVAL_MAX_US); + + /* + * Floor the deadline at the SDA-stuck recovery window so an in-spec + * bus recovery (agent holding SCL low up to 35 ms) plus status + * posting and read-back completes before we return a timeout. + */ + timeout_us =3D max(xfer_us, P3H2X4X_SMBUS_SCL_LOW_RECOVERY_US); + + ret =3D 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 =3D (u8)status_read; + + status =3D (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 wr= iting a descriptor + * and a message to the p3h2x4x registers. Controller buffer page is deter= mined 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 =3D P3H2X4X_CONTROLLER_BUFFER_PAGE + 4 * target= _port; + u8 target_port_status =3D P3H2X4X_TP0_SMBUS_AGNT_STS + target_port; + u8 desc[P3H2X4X_SMBUS_DESCRIPTOR_SIZE] =3D { 0 }; + u8 transaction_type =3D P3H2X4X_SMBUS_400kHz; + int write_length, read_length; + u8 addr =3D xfers[nxfers_i].addr; + u8 rw_address =3D 2 * addr; + int ret, ret2; + + if (rw =3D=3D 2) { /* write and read */ + write_length =3D xfers[nxfers_i].len; + read_length =3D xfers[nxfers_i + 1].len; + } else if (rw =3D=3D 1) { + rw_address |=3D P3H2X4X_SET_BIT(0); + write_length =3D 0; + read_length =3D xfers[nxfers_i].len; + } else { + write_length =3D xfers[nxfers_i].len; + read_length =3D 0; + } + + desc[P3H2X4X_DESC_ADDR] =3D rw_address; + if (rw =3D=3D 2) + desc[P3H2X4X_DESC_TYPE] =3D transaction_type | P3H2X4X_SET_BIT(0); + else + desc[P3H2X4X_DESC_TYPE] =3D transaction_type; + desc[P3H2X4X_DESC_WRITE_LEN] =3D write_length; + desc[P3H2X4X_DESC_READ_LEN] =3D read_length; + + ret =3D regmap_write(p3h2x4x_i3c_hub->regmap, target_port_status, + P3H2X4X_TP_BUFFER_STATUS_MASK); + if (ret) + goto out; + + ret =3D regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, controlle= r_buffer_page); + if (ret) + goto out; + + ret =3D regmap_bulk_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_CONTROLLER_AGE= NT_BUFF, + desc, P3H2X4X_SMBUS_DESCRIPTOR_SIZE); + if (ret) + goto out; + + if (!(rw % 2) && xfers[nxfers_i].len) { + ret =3D 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 =3D 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 =3D p3h2x4x_read_smbus_transaction_status(p3h2x4x_i3c_hub, + target_port_status, + (write_length + read_length)); + if (ret) + goto out; + + if (rw) { + if (rw =3D=3D 2) + nxfers_i +=3D 1; + + if (xfers[nxfers_i].len) { + u8 *dma_buf =3D i2c_get_dma_safe_msg_buf(&xfers[nxfers_i], 1); + + if (!dma_buf) { + ret =3D -ENOMEM; + goto out; + } + + ret =3D regmap_bulk_read(p3h2x4x_i3c_hub->regmap, + P3H2X4X_CONTROLLER_AGENT_BUFF_DATA + write_length, + dma_buf, xfers[nxfers_i].len); + i2c_put_dma_safe_msg_buf(dma_buf, &xfers[nxfers_i], !ret); + if (ret) + goto out; + } + } +out: + ret2 =3D regmap_write(p3h2x4x_i3c_hub->regmap, + P3H2X4X_PAGE_PTR, 0x00); + if (!ret && ret2) + ret =3D 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 *m= sgs, int num) +{ + int ret_sum =3D 0, ret, msg_count; + u8 rw; + + struct tp_bus *bus =3D i2c_get_adapdata(adap); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub =3D bus->p3h2x4x_i3c_hub; + + guard(mutex)(&p3h2x4x_i3c_hub->etx_mutex); + guard(mutex)(&bus->port_mutex); + + for (msg_count =3D 0; msg_count < num; msg_count++) { + rw =3D (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 =3D=3D 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 =3D 2; + msg_count +=3D 1; + ret_sum +=3D 1; + } + } + + ret =3D p3h2x4x_tp_i2c_xfer_msg(p3h2x4x_i3c_hub, + msgs, + bus->tp_port, + (rw =3D=3D 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 =3D { + .max_read_len =3D P3H2X4X_SMBUS_PAYLOAD_SIZE, + .max_write_len =3D P3H2X4X_SMBUS_PAYLOAD_SIZE, +}; + +/* + * I2C algorithm Structure + */ +static struct i2c_algorithm p3h2x4x_tp_i2c_algorithm =3D { + .master_xfer =3D p3h2x4x_tp_i2c_xfer, + .functionality =3D p3h2x4x_tp_smbus_funcs, +}; + +void p3h2x4x_unregister_smbus_adapters(struct p3h2x4x_i3c_hub_dev *hub) +{ + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(hub->dev->parent); + u8 tp; + + for (tp =3D 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 =3D NULL; + hub->tp_bus[tp].is_registered =3D 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 =3D dev_get_drvdata(hub->dev->parent); + int ret, ret2; + u8 tp; + + scoped_guard(mutex, &p3h2x4x->protected_reg_lock) { + ret =3D regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_UNLOCK_CODE); + if (ret) + break; + + ret =3D regmap_write(hub->regmap, P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG, + P3H2X4X_IBI_DISABLED); + + ret2 =3D regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_LOCK_CODE); + if (!ret && ret2) + ret =3D ret2; + } + if (ret) + return ret; + + for (tp =3D 0; tp < p3h2x4x->num_target_ports; tp++) { + if (!hub->tp_bus[tp].of_node || + hub->hub_config.tp_config[tp].mode !=3D P3H2X4X_TP_MODE_SMBUS) + continue; + + /* Allocate adapter */ + struct i2c_adapter *smbus_adapter =3D + devm_kzalloc(hub->dev, sizeof(*smbus_adapter), GFP_KERNEL); + if (!smbus_adapter) { + p3h2x4x_unregister_smbus_adapters(hub); + return -ENOMEM; + } + + /* Initialize adapter */ + smbus_adapter->owner =3D THIS_MODULE; + smbus_adapter->class =3D I2C_CLASS_HWMON; + smbus_adapter->algo =3D &p3h2x4x_tp_i2c_algorithm; + smbus_adapter->quirks =3D &p3h2x4x_tp_i2c_quirks; + smbus_adapter->dev.parent =3D hub->dev; + smbus_adapter->dev.of_node =3D 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 =3D smbus_adapter; + hub->tp_bus[tp].tp_smbus_client =3D NULL; + hub->tp_bus[tp].is_registered =3D true; + hub->hub_config.tp_config[tp].ibi_en =3D false; + } + + /* Register adapter */ + ret =3D i2c_add_adapter(smbus_adapter); + if (ret) { + scoped_guard(mutex, &hub->etx_mutex) { + hub->tp_bus[tp].is_registered =3D false; + hub->tp_bus[tp].tp_smbus_adapter =3D 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 =3D 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; +} --=20 2.25.1 From nobody Thu Sep 24 16:08:57 2026 Received: from inva020.nxp.com (inva020.nxp.com [92.121.34.13]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7D70C518130; Tue, 22 Sep 2026 10:36:35 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=92.121.34.13 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073398; cv=none; b=Z8p+iCb4FgwtvTqSpQd7rYZJ+Zz+KKDEJ1gn27V2q9c8cWLyOhoZxyBJQn4Xn4gVOhk9pjZRPmoPStF0ZhReXDKubnoyZDUuIU12mGY/Ty7EyVlV5vfCDhyulFt4p2LNU7gyCPk51XSndMasjZtEOz+GH0oouADgI15998FT8kk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1790073398; c=relaxed/simple; bh=Fk52NGB3dwLYlKTU8ZPRRjCZkSpOH+QpS3NCecB5JGw=; h=From:To:Cc:Subject:Date:Message-Id:In-Reply-To:References: MIME-Version; b=hWPs6/JStJWlRcVIO0UUc6Arst7ihI9sYjY8tjk/2FmqiUO80JakrRUV8GHYwVx+S1P+cjE46VPQYuRmc9h7eWIqNEliBvZaR+cEtuTWLbcQyCjB71DIHQBEURA4Ltd157faUSgl+w4MD+caRO3elvtQ1po6yi8/Pe9OYg8uO4Q= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com; spf=pass smtp.mailfrom=nxp.com; arc=none smtp.client-ip=92.121.34.13 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=nxp.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=nxp.com Received: from inva020.nxp.com (localhost [127.0.0.1]) by inva020.eu-rdc02.nxp.com (Postfix) with ESMTP id 8A2991A0005; Tue, 22 Sep 2026 12:36:33 +0200 (CEST) Received: from aprdc01srsp001v.ap-rdc01.nxp.com (aprdc01srsp001v.ap-rdc01.nxp.com [165.114.16.16]) by inva020.eu-rdc02.nxp.com (Postfix) with ESMTP id 2AFF51A0004; Tue, 22 Sep 2026 12:36:33 +0200 (CEST) Received: from lsv03900.swis.in-blr01.nxp.com (lsv03900.swis.in-blr01.nxp.com [10.12.177.15]) by aprdc01srsp001v.ap-rdc01.nxp.com (Postfix) with ESMTP id B2A6D1800068; Tue, 22 Sep 2026 18:36:29 +0800 (+08) From: Lakshay Piplani To: linux-kernel@vger.kernel.org, linux-i3c@lists.infradead.org, alexandre.belloni@bootlin.com, krzk+dt@kernel.org, robh@kernel.org, conor+dt@kernel.org, devicetree@vger.kernel.org, broonie@kernel.org, mfd@lists.linux.dev, lee@kernel.org, Frank.Li@nxp.com, lgirdwood@gmail.com Cc: vikash.bansal@nxp.com, priyanka.jain@nxp.com, aman.kumarpandey@nxp.com, Lakshay Piplani Subject: [PATCH v20 8/8] i3c: hub: p3h2x4x: Add SMBus slave mode support Date: Tue, 22 Sep 2026 16:05:50 +0530 Message-Id: <20260922103551.2754613-9-lakshay.piplani@nxp.com> X-Mailer: git-send-email 2.25.1 In-Reply-To: <20260922103551.2754613-1-lakshay.piplani@nxp.com> References: <20260922103551.2754613-1-lakshay.piplani@nxp.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Virus-Scanned: ClamAV using ClamSMTP Content-Type: text/plain; charset="utf-8" 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 Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Reviewed-by: Frank Li --- Changes in v20: - No change Changes in v19: - No change Changes in v18: - Rebased onto v7.3-rc1; no intended driver behaviour changes Changes in v17: - No change Changes in v16: - Always clear the software slave state on unregister: even if the hardware IBI-disable write fails, log the error but still set bus->tp_smbus_clien= t =3D NULL and return 0, so a later IBI cannot deref a dangling client and call a f= reed slave_cb - Use a heap DMA-safe buffer for the SMBus-agent RX path instead of a stack buffer passed to regmap_bulk_read() over I3C - Advertise I2C_FUNC_SLAVE when CONFIG_I2C_SLAVE is enabled, and reject sl= ave registration with -EOPNOTSUPP unless an I3C upstream device and working = IBI path are available (hub->i3cdev and hub->ibi_ready) - Validate the IBI payload length and bound the target-port loop by num_target_ports Changes in v15: - Retrieve the hub context through the shared MFD data instead of replacing the I3C device's parent driver data - Rework SMBus slave registration and unregistration to use the shared protected-register lock - Report protected-register relock failures separately from the original enable or disable operation Changes in v14: - Clear receive-buffer flags even on SMBus receive error paths to avoid repeated IBI storms - Decode receive-buffer status using FIELD_GET() - Fix overflow status value and explicitly clear overflow after reading bo= th target buffers Changes in v13: - Make IBI setup optional and robust: avoid probe failure when IBI is unsu= pported and add proper cleanup using devm actions - Fix SMBus slave receive path: avoid over-clearing buffer status, handle = unregistered ports, and ensure correct event delivery - Improve safety by adding proper locking around shared state Changes in v12: - Add devm cleanup for IBI request/enable path - Fix NULL pointer dereference before tp_smbus_client check - Clear tp_smbus_client before disabling SMBus-agent IBI in unreg_slave() Changes in v11: - Improve SMBus slave mode payload validation and parsing Changes in v10: - Split SMBus slave mode support into a separate patch --- --- drivers/i3c/hub/p3h2840_i3c_hub.h | 19 ++ drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 54 ++++- drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 306 +++++++++++++++++++++++- 3 files changed, 377 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3= c_hub.h index 7a1345924e3f..0e59351e7b6d 100644 --- a/drivers/i3c/hub/p3h2840_i3c_hub.h +++ b/drivers/i3c/hub/p3h2840_i3c_hub.h @@ -123,6 +123,11 @@ #define BUF_RECEIVED_FLAG_MASK GENMASK(3, 1) #define BUF_RECEIVED_FLAG_TF_MASK GENMASK(3, 0) =20 +#define P3H2X4X_TARGET_BUF_0_RECEIVE_VAL 1 +#define P3H2X4X_TARGET_BUF_1_RECEIVE_VAL 2 +#define P3H2X4X_TARGET_BUF_0_1_RECEIVE_VAL 3 +#define P3H2X4X_TARGET_BUF_OVRFL_VAL 7 + #define P3H2X4X_TARGET_AGENT_LOCAL_DEV 0x11 #define P3H2X4X_TARGET_BUFF_0_PAGE 0x12 #define P3H2X4X_TARGET_BUFF_1_PAGE 0x13 @@ -315,6 +320,10 @@ struct p3h2x4x_i3c_hub_dev { struct i2c_client *i2c_client; struct hub_configuration hub_config; struct tp_bus tp_bus[P3H2X4X_TP_MAX_COUNT]; +#if IS_ENABLED(CONFIG_I2C_SLAVE) + bool ibi_ready; + u8 *slave_rx_buffer; +#endif struct i3c_hub *hub; }; =20 @@ -340,4 +349,14 @@ int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *= p3h2x4x_i3c_hub); */ int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub); =20 +/** + * 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/p3h284= 0_i3c_hub_i3c.c index 3c5db7db48aa..b069467afcc6 100644 --- a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c +++ b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c @@ -10,6 +10,14 @@ =20 #include "p3h2840_i3c_hub.h" =20 +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static const struct i3c_ibi_setup p3h2x4x_ibireq =3D { + .handler =3D p3h2x4x_ibi_handler, + .max_payload_len =3D P3H2X4X_MAX_PAYLOAD_LEN, + .num_slots =3D P3H2X4X_NUM_SLOTS, +}; +#endif + static inline struct tp_bus * p3h2x4x_bus_from_controller(struct i3c_master_controller *controller) { @@ -63,6 +71,16 @@ static void p3h2x4x_unregister_i3c_master(void *data) i3c_master_unregister(controller); } =20 +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static void p3h2x4x_free_ibi(void *data) +{ + struct i3c_device *i3cdev =3D 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. @@ -122,5 +140,39 @@ int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3= h2x4x_hub) p3h2x4x_hub->tp_bus[tp].is_registered =3D true; p3h2x4x_hub->hub_config.tp_config[tp].always_enable =3D true; } - return regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_ma= sk); +#if IS_ENABLED(CONFIG_I2C_SLAVE) + p3h2x4x_hub->slave_rx_buffer =3D devm_kzalloc(p3h2x4x_hub->dev, + P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE, + GFP_KERNEL); + if (!p3h2x4x_hub->slave_rx_buffer) + return -ENOMEM; + + ret =3D 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 =3D false; + } else { + ret =3D 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 =3D false; + } else { + p3h2x4x_hub->ibi_ready =3D true; + + ret =3D devm_add_action_or_reset(p3h2x4x_hub->dev, + p3h2x4x_free_ibi, + p3h2x4x_hub->i3cdev); + if (ret) { + p3h2x4x_hub->ibi_ready =3D false; + return ret; + } + } + } +#endif + ret =3D regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_m= ask); + + return ret; } diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2= 840_i3c_hub_smbus.c index a3324cc36595..7fdfa79dfeb6 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 cr= eation and read/write. */ +#include +#include #include #include =20 @@ -15,6 +17,178 @@ enum p3h2x4x_smbus_desc_idx { P3H2X4X_DESC_READ_LEN, }; =20 +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static void p3h2x4x_read_smbus_agent_rx_buf(struct i3c_device *i3cdev, enu= m p3h2x4x_rcv_buf rfbuf, + enum p3h2x4x_tp tp) +{ + struct p3h2x4x *p3h2x4x =3D i3cdev_get_drvdata(i3cdev); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub; + u8 target_buffer_page, flag_clear, temp =3D 0, i, addr; + u32 packet_len, slave_address; + struct i2c_client *client; + u8 *slave_rx_buffer; + int ret; + + if (!p3h2x4x || !p3h2x4x->i3c_hub_priv) + return; + + p3h2x4x_i3c_hub =3D p3h2x4x->i3c_hub_priv; + slave_rx_buffer =3D p3h2x4x_i3c_hub->slave_rx_buffer; + + switch (rfbuf) { + case RCV_BUF_0: + target_buffer_page =3D P3H2X4X_TARGET_BUFF_0_PAGE; + flag_clear =3D P3H2X4X_TARGET_BUF_0_RECEIVE; + break; + case RCV_BUF_1: + target_buffer_page =3D P3H2X4X_TARGET_BUFF_1_PAGE; + flag_clear =3D P3H2X4X_TARGET_BUF_1_RECEIVE; + break; + default: + return; + } + + target_buffer_page +=3D P3H2X4X_NO_PAGE_PER_TP * tp; + + ret =3D regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, target_bu= ffer_page); + if (ret) + goto ibi_err; + + /* read buffer length */ + ret =3D regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_LENGTH, = &packet_len); + if (ret) + goto ibi_err; + + if (packet_len) + packet_len =3D 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 =3D regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_ADDRESS,= &slave_address); + if (ret) + goto ibi_err; + + /* read data */ + if (packet_len) { + ret =3D regmap_bulk_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_DA= TA, + slave_rx_buffer, packet_len); + if (ret) + goto ibi_err; + } + + client =3D p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client; + if (!client) + goto ibi_err; + + /* notify slave driver about received data */ + if ((client->addr & 0x7f) =3D=3D (slave_address >> 1)) { + addr =3D slave_address >> 1; + i2c_slave_event(client, + I2C_SLAVE_WRITE_REQUESTED, &addr); + for (i =3D 0; i < packet_len; i++) { + temp =3D 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, fl= ag_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 =3D 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 =3D payload->data; + payload_byte_one =3D data[0]; + + if (!(payload_byte_one & P3H2X4X_SMBUS_AGENT_EVENT_FLAG_STATUS)) + return; + + p3h2x4x_i3c_hub =3D p3h2x4x ? p3h2x4x->i3c_hub_priv : NULL; + + if (!p3h2x4x_i3c_hub || !p3h2x4x_i3c_hub->regmap) + return; + + payload_byte_two =3D data[1]; + guard(mutex)(&p3h2x4x_i3c_hub->etx_mutex); + + for (i =3D 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 =3D 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_de= v *hub, u8 target_port_status, u8 data_length) @@ -215,8 +389,134 @@ static s32 p3h2x4x_tp_i2c_xfer(struct i2c_adapter *ad= ap, struct i2c_msg *msgs, i =20 static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter) { - return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA; + u32 funcs =3D I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA; + +#if IS_ENABLED(CONFIG_I2C_SLAVE) + struct tp_bus *bus =3D i2c_get_adapdata(adapter); + struct p3h2x4x_i3c_hub_dev *hub =3D bus->p3h2x4x_i3c_hub; + + /* + * Only advertise slave support when the upstream IBI path is usable. + * Otherwise reg_slave() returns -EOPNOTSUPP while functionality() + * reports I2C_FUNC_SLAVE, which is inconsistent for callers. + */ + if (hub->ibi_ready) + funcs |=3D I2C_FUNC_SLAVE; +#endif + + return funcs; +} + +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static int p3h2x4x_tp_i2c_reg_slave(struct i2c_client *slave) +{ + struct tp_bus *bus =3D i2c_get_adapdata(slave->adapter); + struct p3h2x4x_i3c_hub_dev *hub =3D bus->p3h2x4x_i3c_hub; + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(hub->dev->parent); + int relock_ret =3D 0; + int ret =3D 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 =3D regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_UNLOCK_CODE); + if (ret) + break; + + ret =3D regmap_set_bits(hub->regmap, + P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG, + bus->tp_mask); + + /* Lock access to protected registers */ + relock_ret =3D 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 =3D slave; + hub->hub_config.tp_config[bus->tp_port].ibi_en =3D true; + + return 0; +} + +static int p3h2x4x_tp_i2c_unreg_slave(struct i2c_client *slave) +{ + struct tp_bus *bus =3D i2c_get_adapdata(slave->adapter); + struct p3h2x4x_i3c_hub_dev *hub =3D bus->p3h2x4x_i3c_hub; + struct p3h2x4x *p3h2x4x =3D dev_get_drvdata(hub->dev->parent); + int relock_ret =3D 0; + int ret =3D 0; + + guard(mutex)(&hub->etx_mutex); + + if (bus->tp_smbus_client !=3D slave) + return -EINVAL; + + scoped_guard(mutex, &p3h2x4x->protected_reg_lock) { + /* Unlock access to protected registers */ + ret =3D regmap_write(hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_UNLOCK_CODE); + if (ret) + break; + + ret =3D regmap_clear_bits(hub->regmap, + P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG, + bus->tp_mask); + + /* Lock access to protected registers */ + relock_ret =3D regmap_write(hub->regmap, + P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_LOCK_CODE); + } + + if (relock_ret) + dev_err(hub->dev, + "failed to restore protected register lock: %d\n", + relock_ret); + + if (ret) + dev_err(hub->dev, + "failed to disable SMBus agent IBI on port %u: %d\n", + bus->tp_port, ret); + + /* + * Slave backend removal cannot be rolled back. Detach the + * callback-visible state even if disabling the hardware IBI failed, + * and return 0 so the core completes teardown; the failure is still + * visible in the log above. etx_mutex serializes this with the IBI + * handler, so later events are acknowledged without invoking the + * removed client. + */ + bus->tp_smbus_client =3D NULL; + hub->hub_config.tp_config[bus->tp_port].ibi_en =3D false; + + return 0; } +#endif =20 static const struct i2c_adapter_quirks p3h2x4x_tp_i2c_quirks =3D { .max_read_len =3D P3H2X4X_SMBUS_PAYLOAD_SIZE, @@ -228,6 +528,10 @@ static const struct i2c_adapter_quirks p3h2x4x_tp_i2c_= quirks =3D { */ static struct i2c_algorithm p3h2x4x_tp_i2c_algorithm =3D { .master_xfer =3D p3h2x4x_tp_i2c_xfer, +#if IS_ENABLED(CONFIG_I2C_SLAVE) + .reg_slave =3D p3h2x4x_tp_i2c_reg_slave, + .unreg_slave =3D p3h2x4x_tp_i2c_unreg_slave, +#endif .functionality =3D p3h2x4x_tp_smbus_funcs, }; =20 --=20 2.25.1