From 591980aaaa4645e6511f1387cc6092378a6bb84f Mon Sep 17 00:00:00 2001 From: Stanley Chu Date: Mon, 10 Aug 2026 09:30:59 +0800 Subject: [PATCH 01/28] i3c: master: allocate IBI workqueue with WQ_HIGHPRI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IBI (In-Band Interrupt) workqueue is allocated with only WQ_MEM_RECLAIM, which places IBI payload processing at normal worker priority. This is inadequate given the time-sensitive nature of IBI handling. In the I3C protocol, when a target asserts an IBI, the SDA line is held low until the master acknowledges and completes the exchange. The IRQ handler (top half) ACKs the IBI, reads the payload, emits a STOP, and immediately queues the payload processing to the per-device ordered workqueue via i3c_master_queue_ibi() — effectively the bottom half of the IBI interrupt path. If this workqueue worker is delayed by competing normal-priority tasks, the IBI notification reaches the client driver late. For latency-sensitive clients (e.g. sensors reporting alerts, hotplug events), this defeats the purpose of using IBI over polling. Furthermore, because the ordered workqueue serialises slots, a backlog of delayed slots can exhaust the pre-allocated IBI slot pool, causing subsequent IBIs to be dropped at the hardware level. Add WQ_HIGHPRI to ensure IBI bottom-half work is scheduled promptly after the top-half IRQ handler enqueues it, keeping the IBI processing pipeline consistent with the interrupt-like semantics the protocol demands. Signed-off-by: Stanley Chu Acked-by: Mukesh Kumar Savaliya Link: https://patch.msgid.link/20260810013059.3055787-1-yschu@nuvoton.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index afcd7a21a3e6..4fa90013290f 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -3945,7 +3945,8 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev, if (!ibi) return -ENOMEM; - ibi->wq = alloc_ordered_workqueue(dev_name(i3cdev_to_dev(dev->dev)), WQ_MEM_RECLAIM); + ibi->wq = alloc_ordered_workqueue(dev_name(i3cdev_to_dev(dev->dev)), + WQ_MEM_RECLAIM | WQ_HIGHPRI); if (!ibi->wq) { kfree(ibi); return -ENOMEM; From 35b028b25451e17d63b2cd36a11a86c5e18df1b9 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:31 +0300 Subject: [PATCH 02/28] i3c: master: Fix out-of-bounds read in DMA bounce buffer setup When a bounce buffer is required for DMA_TO_DEVICE transfers, i3c_master_dma_map_single() rounds the DMA mapping length up to a cache-line boundary: map_len = ALIGN(len, cache_line_size()); It then allocates the bounce buffer with: kmemdup(buf, map_len, GFP_KERNEL); kmemdup() copies the full allocation size, causing it to read map_len bytes from buf even though only len bytes are valid. This results in an out-of-bounds read of up to cache_line_size() - 1 bytes past the end of the caller's buffer. Fix the issue by allocating the bounce buffer with kzalloc() and copying only len bytes from the original buffer. The remaining bytes up to map_len stay zero-filled, avoiding both the out-of-bounds read and exposure of unrelated memory contents to the DMA engine. Fixes: f8d9e56aeb87 ("i3c: master: Add helpers for DMA mapping and bounce buffer handling") Cc: stable@vger.kernel.org Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-2-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 4fa90013290f..8561d5a040cb 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -2216,12 +2216,11 @@ struct i3c_dma *i3c_master_dma_map_single(struct device *dev, void *buf, if (force_bounce) { dma_xfer->map_len = ALIGN(len, cache_line_size()); - if (dir == DMA_FROM_DEVICE) - bounce = kzalloc(dma_xfer->map_len, GFP_KERNEL); - else - bounce = kmemdup(buf, dma_xfer->map_len, GFP_KERNEL); + bounce = kzalloc(dma_xfer->map_len, GFP_KERNEL); if (!bounce) return NULL; + if (dir != DMA_FROM_DEVICE) + memcpy(bounce, buf, len); dma_buf = bounce; } From b1a25e8e143ef880450e6399fb0733b66a1c29f0 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:32 +0300 Subject: [PATCH 03/28] i3c: mipi-i3c-hci: Bounce short reads irrespective of the IOMMU The controller writes whole DWORDs, so a read whose length is not a multiple of 4 overwrites up to 3 bytes past the end of the destination buffer. That is a property of the controller, not of the IOMMU, but the bounce buffer that works around it was used only when the device was IOMMU mapped. Everywhere else the buffer is left unprotected. Drop the device_iommu_mapped() condition. The overrun is easily seen with CONFIG_SLUB_DEBUG=y and kernel command line options intel_iommu=off slub_debug=FZPU, which reports it as a kmalloc redzone overwrite, like: [kmalloc Redzone overwritten] 0xffff8a354561570e-0xffff8a354561570f @offset=1806. First byte 0x15 instead of 0xcc ============================================================================= BUG kmalloc-8 (Not tainted): Object corrupt Allocated in i3c_master_retrieve_dev_info+0xc1/0x760 age=40 cpu=6 pid=1 ... Freed in i3c_master_enec_disec_locked+0xeb/0x140 age=40 cpu=6 pid=1 ... WARNING: mm/slub.c:1233 at object_err+0x1c1/0x1cf, CPU#6: swapper/0/1 ... Fixes: 9e23897bca62 ("i3c: mipi-i3c-hci: Use physical device pointer with DMA API") Cc: stable@vger.kernel.org Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-3-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/dma.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 7c2b20474130..5b195978f376 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -428,7 +428,7 @@ static void hci_dma_unmap_xfer(struct i3c_hci *hci, static struct i3c_dma *hci_dma_map_xfer(struct device *dev, struct hci_xfer *xfer) { enum dma_data_direction dir = xfer->rnw ? DMA_FROM_DEVICE : DMA_TO_DEVICE; - bool need_bounce = device_iommu_mapped(dev) && xfer->rnw && (xfer->data_len & 3); + bool need_bounce = xfer->rnw && (xfer->data_len & 3); return i3c_master_dma_map_single(dev, xfer->data, xfer->data_len, need_bounce, dir); } From 2bc2b1b89f65b5b75ffae0cf3d67d2415611004c Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:33 +0300 Subject: [PATCH 04/28] i3c: mipi-i3c-hci-pci: Set drvdata before creating LTR sysfs attribute intel_i3c_init() creates the pm_qos_latency_tolerance_us sysfs attribute before probe calls pci_set_drvdata(). A userspace write to the attribute can therefore invoke intel_ltr_set(), which uses pci_get_drvdata(), before drvdata has been initialized. Move pci_set_drvdata() before hci->info->init() so drvdata is available before the sysfs attribute becomes accessible. Fixes: 884a33131f2a ("i3c: mipi-i3c-hci-pci: Add LTR support for Intel controllers") Cc: stable@vger.kernel.org Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-4-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c b/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c index ab595661db1e..af9595009b03 100644 --- a/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c +++ b/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c @@ -445,6 +445,8 @@ static int mipi_i3c_hci_pci_probe(struct pci_dev *pci, if (ret < 0) return ret; + pci_set_drvdata(pci, hci); + hci->info = (const struct mipi_i3c_hci_pci_info *)id->driver_data; ret = hci->info->init ? hci->info->init(hci) : 0; @@ -455,8 +457,6 @@ static int mipi_i3c_hci_pci_probe(struct pci_dev *pci, if (ret) goto err_exit; - pci_set_drvdata(pci, hci); - mipi_i3c_hci_pci_rpm_allow(&pci->dev); return 0; From 25648fb7cebb7e1963afc31c07e650d703cfc140 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:34 +0300 Subject: [PATCH 05/28] i3c: master: Match ACPI targets to the correct bus controller instance For ACPI-enumerated I3C targets, _ADR encodes both the zero-based bus controller instance number in bits 51:48 and the Provisioned ID (PID) in bits 47:0, as defined by ACPI 6.6 Table 6.2 and MIPI DisCo for I3C v1.1 section 5.2. However, i3c_master_add_acpi_dev() treats the entire _ADR value as the PID. On multi-instance controllers this folds the instance number into the PID, causing i3c_master_add_i3c_boardinfo() to reject devices on instance 1 and above with -EINVAL. The problem is exposed by the MIPI I3C HCI PCI driver, which creates one MFD child device per bus controller instance. Since the child devices do not have their own ACPI companions, they all inherit the parent's ACPI node and therefore enumerate the same set of child devices. As a result, a target belonging to any instance other than 0 prevents registration of all controller instances. Add an instance number to struct i3c_master_controller, propagate it from the HCI PCI driver, extract the PID from bits 47:0 of _ADR, and ignore ACPI child nodes that belong to a different controller instance. ACPI-enumerated I2C devices are unaffected. Their controller instance is encoded in the Resource Source Index of the I2cSerialBusV2 descriptor rather than in _ADR and will require separate handling. Fixes: b46a4b3c5d1e ("i3c: master: Support ACPI enumeration of child devices") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-5-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master.c | 19 +++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/core.c | 4 +++- .../master/mipi-i3c-hci/mipi-i3c-hci-pci.c | 1 + include/linux/i3c/master.h | 3 +++ include/linux/platform_data/mipi-i3c-hci.h | 4 ++++ 5 files changed, 28 insertions(+), 3 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 8561d5a040cb..425b98e6efaa 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -3076,6 +3077,13 @@ static int i3c_master_add_of_dev(struct i3c_master_controller *master, return ret; } +#define I3C_ACPI_ADR_PID GENMASK_U64(47, 0) +/* + * Zero-based instance number of the Bus Controller to which the Target is + * connected. + */ +#define I3C_ACPI_ADR_INSTANCE GENMASK_U64(51, 48) + #ifdef CONFIG_ACPI static int i3c_master_add_acpi_dev(struct i3c_master_controller *master, struct fwnode_handle *fwnode) @@ -3083,6 +3091,7 @@ static int i3c_master_add_acpi_dev(struct i3c_master_controller *master, struct acpi_device *adev = to_acpi_device_node(fwnode); acpi_bus_address adr; u32 reg[3] = { 0 }; + u64 pid; int ret; /* @@ -3100,9 +3109,15 @@ static int i3c_master_add_acpi_dev(struct i3c_master_controller *master, adr = acpi_device_adr(adev); + /* Match the multi-bus instance number */ + if (FIELD_GET(I3C_ACPI_ADR_INSTANCE, adr) != master->instance) + return 0; + /* For I3C devices, _ADR will have the 48 bit PID of the device */ - reg[1] = upper_32_bits(adr); - reg[2] = lower_32_bits(adr); + pid = FIELD_GET(I3C_ACPI_ADR_PID, adr); + + reg[1] = upper_32_bits(pid); + reg[2] = lower_32_bits(pid); fwnode_property_read_u32(fwnode, "mipi-i3c-static-address", ®[0]); diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index dadf049bd4b5..8b3a87d8a8bb 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -1142,8 +1142,10 @@ static int i3c_hci_probe(struct platform_device *pdev) * necessarily in separate contiguous sub-ranges. To avoid overlapping * mappings, provide base_regs from the parent mapping. */ - if (pdata) + if (pdata) { hci->base_regs = pdata->base_regs; + hci->master.instance = pdata->instance; + } if (!hci->base_regs) { hci->base_regs = devm_platform_ioremap_resource(pdev, 0); diff --git a/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c b/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c index af9595009b03..5b8e10d05cee 100644 --- a/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c +++ b/drivers/i3c/master/mipi-i3c-hci/mipi-i3c-hci-pci.c @@ -390,6 +390,7 @@ static void mipi_i3c_hci_pci_setup_cell(struct mipi_i3c_hci_pci *hci, int idx, struct mfd_cell *cell) { data->pdata.base_regs = hci->base + hci->info->instance_offset[idx]; + data->pdata.instance = idx; data->res = DEFINE_RES_IRQ(0); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index f7ceec2b4477..72065715feee 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -536,6 +536,8 @@ struct i3c_master_controller_ops { * Bit 0: SETDASA * Bit 1: SETAASA * All other bits are reserved. + * @instance: Zero-based instance number of the Bus Controller as defined by the + * DisCo specification I3C Target Address (_ADR) Encoding * @wq: freezable workqueue which can be used by master * drivers if they need to postpone operations that need to take place * in a thread context. Typical examples are Hot Join processing which @@ -573,6 +575,7 @@ struct i3c_master_controller { } boardinfo; struct i3c_bus bus; u8 addr_method; + u8 instance; struct workqueue_struct *wq; struct work_struct hj_work; struct work_struct reg_work; diff --git a/include/linux/platform_data/mipi-i3c-hci.h b/include/linux/platform_data/mipi-i3c-hci.h index ab7395f455f9..6f61d8a02378 100644 --- a/include/linux/platform_data/mipi-i3c-hci.h +++ b/include/linux/platform_data/mipi-i3c-hci.h @@ -3,13 +3,17 @@ #define INCLUDE_PLATFORM_DATA_MIPI_I3C_HCI_H #include +#include /** * struct mipi_i3c_hci_platform_data - Platform-dependent data for mipi_i3c_hci * @base_regs: Register set base address (to support multi-bus instances) + * @instance: Zero-based instance number of the Bus Controller as defined by the + * DisCo specification I3C Target Address (_ADR) Encoding */ struct mipi_i3c_hci_platform_data { void __iomem *base_regs; + u8 instance; }; #endif From 48d7187c98e80fdb8f22e759355d6edf81cb9bf7 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:35 +0300 Subject: [PATCH 06/28] i3c: master: Remove stale GETSTATUS length check i3c_master_getstatus_locked() verifies dest.payload.len against sizeof(*getstatus) after issuing a GETSTATUS CCC. This check predates the introduction of dest.payload.actual_len. Since then, dest.payload.len has held the requested transfer length and dest.payload.actual_len has held the received length, so the check is always false and can never detect a short response. It also predates the addition of generic read CCC response length validation in i3c_master_send_ccc_cmd_locked(), which now performs the necessary check before returning success. Remove the stale check. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-6-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 425b98e6efaa..c50887b0b37d 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1777,11 +1777,6 @@ static int i3c_master_getstatus_locked(struct i3c_master_controller *master, if (ret) goto out; - if (dest.payload.len != sizeof(*getstatus)) { - ret = -EIO; - goto out; - } - if (status) *status = be16_to_cpu(getstatus->status); out: From ad869331ac616bf192c04081ef7e206e1d106ad8 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:36 +0300 Subject: [PATCH 07/28] i3c: mipi-i3c-hci: Restore controller state if i3c_hci_enable_ibi() returns an error i3c_hci_enable_ibi() performs controller-side setup before sending the ENEC CCC to enable IBIs on the target. If the ENEC CCC fails, the function returns an error but leaves the controller configured as if IBI enabling had succeeded. Fix the error path by undoing the earlier setup when the ENEC CCC fails, ensuring the controller state remains consistent with the failed enable operation. Fixes: 9ad9a52cce282 ("i3c/master: introduce the mipi-i3c-hci driver") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-7-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/core.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 8b3a87d8a8bb..166d9f70f435 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -703,11 +703,15 @@ static int i3c_hci_enable_ibi(struct i3c_dev_desc *dev) struct i3c_master_controller *m = i3c_dev_get_master(dev); struct i3c_hci *hci = to_i3c_hci(m); struct i3c_hci_dev_data *dev_data = i3c_dev_get_master_data(dev); + int ret; mipi_i3c_hci_dat_v1.clear_flags(hci, dev_data->dat_idx, DAT_0_SIR_REJECT, 0); scoped_guard(spinlock_irqsave, &hci->lock) hci->ibi_devs[dev_data->dat_idx] = dev; - return i3c_master_enec_locked(m, dev->info.dyn_addr, I3C_CCC_EVENT_SIR); + ret = i3c_master_enec_locked(m, dev->info.dyn_addr, I3C_CCC_EVENT_SIR); + if (ret) + __i3c_hci_disable_ibi(hci, dev); + return ret; } static int i3c_hci_disable_ibi(struct i3c_dev_desc *dev) From dcaabb080e0c9838f1240265a03fa9a386825b3b Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:37 +0300 Subject: [PATCH 08/28] i3c: mipi-i3c-hci: Send DISEC before disabling IBIs in hardware Reorder i3c_hci_disable_ibi() so that DISEC is sent before IBIs are disabled at the controller. This matches the sequence used by other I3C controller drivers. It also avoids dropping an IBI that races with the disable operation: an IBI received after DISEC is issued but before the controller disables IBIs can still be processed normally. Once DISEC has been sent, the hardware can be updated to NACK subsequent IBIs and complete the teardown. No functional change is intended beyond aligning the ordering with other drivers and handling the race window consistently. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-8-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/core.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 166d9f70f435..f6a34ce7a295 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -719,13 +719,14 @@ static int i3c_hci_disable_ibi(struct i3c_dev_desc *dev) struct i3c_master_controller *m = i3c_dev_get_master(dev); struct i3c_hci *hci = to_i3c_hci(m); - __i3c_hci_disable_ibi(hci, dev); /* - * The DAT entry is now set to NACK and DISEC this target's IBIs, so + * The DAT entry will be set to NACK and DISEC this target's IBIs, so * the IBI teardown can proceed even if DISEC below fails, so ignore * errors. */ i3c_master_disec_locked(m, dev->info.dyn_addr, I3C_CCC_EVENT_SIR); + + __i3c_hci_disable_ibi(hci, dev); return 0; } From 1570265b225757e3684a7c13c36915823f62675d Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:38 +0300 Subject: [PATCH 09/28] i3c: mipi-i3c-hci: Fix runtime PM violation in i3c_hci_free_ibi() i3c_hci_free_ibi() calls __i3c_hci_disable_ibi(), but the latter requires runtime PM to be resumed. The ->free_ibi() callback does not guarantee that condition. The cases where ->free_ibi() may be called without a preceding ->disable_ibi() do not require re-enabling runtime PM handling: - runtime resume failed in i3c_dev_free_ibi_locked(), so it skips the IBI disable but still proceeds to call ->free_ibi() - the device has been re-enumerated under a new dynamic address in __i3c_master_add_i3c_dev_locked(), which tears down the old device descriptor and deliberately clears olddev->ibi->enabled (to avoid sending a DISEC to the stale old address) before calling i3c_dev_free_ibi_locked(). As a result ->free_ibi() is reached with neither a hardware IBI disable nor runtime PM resumed. In both cases the important requirement is to prevent further IBI processing for the device. That can be done without accessing hardware by removing the device from the ibi_devs[] table. Replace the call to __i3c_hci_disable_ibi() with clearing the corresponding ibi_devs[] entry under the controller lock. Fixes: 650716f23eac4 ("i3c: mipi-i3c-hci: Fix race in i3c_hci_addr_to_dev()") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-9-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/core.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index f6a34ce7a295..b9b20797d045 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -675,11 +675,13 @@ static void __i3c_hci_disable_ibi(struct i3c_hci *hci, struct i3c_dev_desc *dev) static void i3c_hci_free_ibi(struct i3c_dev_desc *dev) { + struct i3c_hci_dev_data *dev_data = i3c_dev_get_master_data(dev); struct i3c_master_controller *m = i3c_dev_get_master(dev); struct i3c_hci *hci = to_i3c_hci(m); - /* Must ensure the IBI has been disabled */ - __i3c_hci_disable_ibi(hci, dev); + /* Must ensure IBIs for this device will no longer be processed */ + scoped_guard(spinlock_irqsave, &hci->lock) + hci->ibi_devs[dev_data->dat_idx] = NULL; hci->io->free_ibi(hci, dev); } From 5fbd0790af9484c251341e3ba2ebbb2887c570c8 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:39 +0300 Subject: [PATCH 10/28] i3c: mipi-i3c-hci: Process multiple IBIs per interrupt INTR_IBI_READY indicates that one or more IBI Status Descriptors are present in the IBI ring. After clearing interrupt status, the interrupt handler processes only a single IBI even though additional IBIs may already be queued. The controller does not reassert INTR_IBI_READY solely because entries remain in the ring after interrupt status has been cleared. Consequently, the remaining IBIs are not processed until some later interrupt occurs, and can accumulate if IBIs arrive more often than the interrupt handler runs. Process all IBIs that are pending when the handler runs: capture the IBI enqueue pointer up front and continue until the dequeue pointer reaches that position. Fixes: 9ad9a52cce28 ("i3c/master: introduce the mipi-i3c-hci driver") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-10-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/dma.c | 39 +++++++++++++++++---------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 5b195978f376..a798b0648922 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -868,25 +868,24 @@ static void hci_dma_recycle_ibi_slot(struct i3c_hci *hci, i3c_generic_ibi_recycle_slot(dev_ibi->pool, slot); } -static void hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh) +static bool hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh, + u32 *op1_val, unsigned int enq_ptr) { struct hci_rings_data *rings = hci->io_data; struct i3c_dev_desc *dev; struct i3c_hci_dev_data *dev_data; struct hci_dma_dev_ibi_data *dev_ibi; struct i3c_ibi_slot *slot; - u32 op1_val, op2_val, ibi_status_error; - unsigned int ptr, enq_ptr, deq_ptr; + u32 ibi_status_error; + unsigned int ptr, deq_ptr; unsigned int ibi_size, ibi_chunks, ibi_data_offset, first_part; int ibi_addr, last_ptr; void *ring_ibi_data; dma_addr_t ring_ibi_data_dma; - op1_val = rh_reg_read(RING_OPERATION1); - deq_ptr = FIELD_GET(RING_OP1_IBI_DEQ_PTR, op1_val); - - op2_val = rh_reg_read(RING_OPERATION2); - enq_ptr = FIELD_GET(RING_OP2_IBI_ENQ_PTR, op2_val); + deq_ptr = FIELD_GET(RING_OP1_IBI_DEQ_PTR, *op1_val); + if (deq_ptr == enq_ptr) + return false; ibi_status_error = 0; ibi_addr = -1; @@ -936,7 +935,7 @@ static void hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh) dev_dbg(&hci->master.dev, "no LAST_STATUS available (e=%d d=%d)", enq_ptr, deq_ptr); - return; + return false; } deq_ptr = last_ptr + 1; deq_ptr %= rh->ibi_status_entries; @@ -1015,10 +1014,9 @@ static void hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh) i3c_master_queue_ibi(dev, slot); done: - op1_val = rh_reg_read(RING_OPERATION1); - op1_val &= ~RING_OP1_IBI_DEQ_PTR; - op1_val |= FIELD_PREP(RING_OP1_IBI_DEQ_PTR, deq_ptr); - rh_reg_write(RING_OPERATION1, op1_val); + *op1_val &= ~RING_OP1_IBI_DEQ_PTR; + *op1_val |= FIELD_PREP(RING_OP1_IBI_DEQ_PTR, deq_ptr); + rh_reg_write(RING_OPERATION1, *op1_val); /* update the chunk pointer */ rh->ibi_chunk_ptr += ibi_chunks; @@ -1026,6 +1024,19 @@ static void hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh) /* and tell the hardware about freed chunks */ rh_reg_write(CHUNK_CONTROL, rh_reg_read(CHUNK_CONTROL) + ibi_chunks); + + return true; +} + +static void hci_dma_drain_ibi_ring(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + u32 op1_val = rh_reg_read(RING_OPERATION1); + u32 op2_val = rh_reg_read(RING_OPERATION2); + unsigned int enq_ptr = FIELD_GET(RING_OP2_IBI_ENQ_PTR, op2_val); + + /* Loop is bounded by enq_ptr. Further IBIs will re-assert INTR_IBI_READY */ + while (hci_dma_process_ibi(hci, rh, &op1_val, enq_ptr)) + ; } static bool hci_dma_irq_handler(struct i3c_hci *hci) @@ -1047,7 +1058,7 @@ static bool hci_dma_irq_handler(struct i3c_hci *hci) rh_reg_write(INTR_STATUS, status); if (status & INTR_IBI_READY) - hci_dma_process_ibi(hci, rh); + hci_dma_drain_ibi_ring(hci, rh); if (status & (INTR_TRANSFER_COMPLETION | INTR_TRANSFER_ERR)) hci_dma_xfer_done(hci, rh); if (status & INTR_RING_OP) From ae307311a69ed65c6219915d978ae322950ba906 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:40 +0300 Subject: [PATCH 11/28] i3c: mipi-i3c-hci: Move DMA suspend/resume callbacks Relocate hci_dma_suspend() and hci_dma_resume() closer to the DMA IO operations table in preparation for upcoming changes. No functional change. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-11-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/dma.c | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index a798b0648922..ec4b469abd33 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -273,29 +273,6 @@ static void hci_dma_init_rings(struct i3c_hci *hci) hci_dma_init_rh(hci, &rings->headers[i], i); } -static void hci_dma_suspend(struct i3c_hci *hci) -{ - struct hci_rings_data *rings = hci->io_data; - int n = rings ? rings->total : 0; - - for (int i = 0; i < n; i++) { - struct hci_rh_data *rh = &rings->headers[i]; - - rh_reg_write(INTR_SIGNAL_ENABLE, 0); - rh_reg_write(RING_CONTROL, 0); - } - - i3c_hci_sync_irq_inactive(hci); -} - -static void hci_dma_resume(struct i3c_hci *hci) -{ - struct hci_rings_data *rings = hci->io_data; - - if (rings) - hci_dma_init_rings(hci); -} - static int hci_dma_init(struct i3c_hci *hci) { struct hci_rings_data *rings; @@ -1075,6 +1052,29 @@ static bool hci_dma_irq_handler(struct i3c_hci *hci) return handled; } +static void hci_dma_suspend(struct i3c_hci *hci) +{ + struct hci_rings_data *rings = hci->io_data; + int n = rings ? rings->total : 0; + + for (int i = 0; i < n; i++) { + struct hci_rh_data *rh = &rings->headers[i]; + + rh_reg_write(INTR_SIGNAL_ENABLE, 0); + rh_reg_write(RING_CONTROL, 0); + } + + i3c_hci_sync_irq_inactive(hci); +} + +static void hci_dma_resume(struct i3c_hci *hci) +{ + struct hci_rings_data *rings = hci->io_data; + + if (rings) + hci_dma_init_rings(hci); +} + const struct hci_io_ops mipi_i3c_hci_dma = { .init = hci_dma_init, .cleanup = hci_dma_cleanup, From 9796ccba1254f6bd3b7e6b650ad28ffc56daf6e9 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:41 +0300 Subject: [PATCH 12/28] i3c: mipi-i3c-hci: Stop rings gracefully when suspending hci_dma_suspend() tore the rings down with a single write of zero to RH_RING_CONTROL, clearing the RS and ENABLE fields together and without waiting for the ring to stop. I3C HCI v1.1 section 6.1.2 separates those steps: clear RS for all running Ring Bundles, and only then clear ENABLE for all enabled Ring Bundles. The ring registers were also written outside hci->lock, while the interrupt handler, which takes that lock, could still be running on another CPU. i3c_hci_sync_irq_inactive() was called only afterwards. Finally, an IBI can still be sitting in the IBI Status Ring when suspend runs. Clearing HC_CONTROL.BUS_ENABLE is deferred: per the description of that field, if a disable request occurs while receiving an IBI, the actual disabling does not occur until reception of the IBI is complete. Instead, clear RS under hci->lock, wait for RING_STATUS_RUNNING to clear, and make the interrupt handler inactive. Only then disable the ring interrupt signals, drain anything left in the IBI ring, and clear ENABLE. Fixes: 816958720443 ("i3c: mipi-i3c-hci: Add DMA suspend and resume support") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-12-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/dma.c | 59 ++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index ec4b469abd33..b8fcea0870ab 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -15,6 +15,7 @@ #include #include #include +#include #include "hci.h" #include "cmd.h" @@ -1052,19 +1053,67 @@ static bool hci_dma_irq_handler(struct i3c_hci *hci) return handled; } +/* + * With the bus disabled, a ring should stop within a few microseconds. The + * timeout is therefore only expected to expire if the hardware is stuck. + * Allow sufficient margin for slow systems, but keep the delay acceptable + * during suspend. + */ +#define RING_STOP_TIMEOUT_US (100 * USEC_PER_MSEC) +/* + * The ring is usually already stopped, so polling typically completes on the + * first iteration. Use a modest sleep interval to avoid busy-waiting without + * adding excessive latency. + */ +#define RING_STOP_SLEEP_US 100 + static void hci_dma_suspend(struct i3c_hci *hci) { struct hci_rings_data *rings = hci->io_data; int n = rings ? rings->total : 0; + struct hci_rh_data *rh; + u32 regval; - for (int i = 0; i < n; i++) { - struct hci_rh_data *rh = &rings->headers[i]; - - rh_reg_write(INTR_SIGNAL_ENABLE, 0); - rh_reg_write(RING_CONTROL, 0); + /* Gracefully stop the rings */ + scoped_guard(spinlock_irqsave, &hci->lock) { + for (int i = 0; i < n; i++) { + rh = &rings->headers[i]; + regval = rh_reg_read(RING_CONTROL); + if (regval & RING_CTRL_RUN_STOP) + rh_reg_write(RING_CONTROL, regval & ~RING_CTRL_RUN_STOP); + } } + /* Wait for actual stop */ + for (int i = 0; i < n; i++) { + rh = &rings->headers[i]; + if (readx_poll_timeout(readl, rh->regs + RH_RING_STATUS, regval, + !(regval & RING_STATUS_RUNNING), + RING_STOP_SLEEP_US, RING_STOP_TIMEOUT_US)) + dev_err(&hci->master.dev, "%s: Ring did not stop, status %#x\n", + __func__, regval); + } + + /* + * With the rings stopped, no more IBIs can be received. Flush and make + * the interrupt handler inactive. + */ i3c_hci_sync_irq_inactive(hci); + + /* Disable interrupt signals and disable the rings */ + scoped_guard(spinlock_irqsave, &hci->lock) + for (int i = 0; i < n; i++) { + rh = &rings->headers[i]; + rh_reg_write(INTR_SIGNAL_ENABLE, 0); + /* + * Be absolutely certain there is no unprocessed IBI. + * hci_dma_drain_ibi_ring() will do nothing if there is + * none. + */ + if (i < IBI_RINGS) + hci_dma_drain_ibi_ring(hci, rh); + rh_reg_write(RING_CONTROL, 0); + } } static void hci_dma_resume(struct i3c_hci *hci) From de243c3dd2e57803e36efc5d2de98ae9a76434d2 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:42 +0300 Subject: [PATCH 13/28] i3c: mipi-i3c-hci: Correct RESP_DATA_LENGTH to bits 15:0 The I3C HCI Response Descriptor defines DATA_LENGTH in Bits[15:0], with Bits[23:16] reserved in all published HCI specification versions: v1.0 (Table 91) and v1.1/v1.2 (Section 8.5). However, RESP_DATA_LENGTH extracts Bits[21:0], incorrectly including six reserved bits. Update the mask to match the defined field width. No functional change is expected because compliant controllers return zero in reserved fields. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-13-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/cmd.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd.h b/drivers/i3c/master/mipi-i3c-hci/cmd.h index 7bada7b4b2de..e0d4a6e0e319 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd.h +++ b/drivers/i3c/master/mipi-i3c-hci/cmd.h @@ -24,7 +24,7 @@ */ #define RESP_STATUS(resp) FIELD_GET(GENMASK(31, 28), resp) #define RESP_TID(resp) FIELD_GET(GENMASK(27, 24), resp) -#define RESP_DATA_LENGTH(resp) FIELD_GET(GENMASK(21, 0), resp) +#define RESP_DATA_LENGTH(resp) FIELD_GET(GENMASK(15, 0), resp) #define RESP_ERR_FIELD GENMASK(31, 28) From f3bd76ecb07573b804cf338c9a6f3b6e9910be86 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:43 +0300 Subject: [PATCH 14/28] i3c: mipi-i3c-hci: Remove invalid transfer size limit The driver derives a maximum transfer size from HC_CAP_MAX_DATA_LENGTH in HC_CAPABILITIES, but no published version of the I3C HCI specification defines such a field. HC_CAPABILITIES reserves Bits[31:8] in HCI v1.0 and Bits[27:22] in HCI v1.1 and v1.2, so the bits used by HC_CAP_MAX_DATA_LENGTH are reserved in all released HCI versions. Neither the HCI nor the I3C TCRI specifications define a maximum data length capability. On compliant controllers reserved bits read as zero, making the computed limit 65536 bytes. Since struct i3c_xfer.len is u16, transfers can never reach that size and the resulting -EFBIG check can never trigger. Remove the unused capability definition and the dead size check. The driver's effective limit remains unchanged. HCI specifications define DATA_LENGTH as a 16-bit field and require larger transfers to be split across multiple Transfer Descriptors. The driver already relies on the core's 16-bit length types elsewhere when constructing descriptors. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-14-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/core.c | 7 ------- 1 file changed, 7 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index b9b20797d045..4629a16debc9 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -51,7 +51,6 @@ #define HC_CAP_SG_DC_EN BIT(30) #define HC_CAP_SG_IBI_EN BIT(29) #define HC_CAP_SG_CR_EN BIT(28) -#define HC_CAP_MAX_DATA_LENGTH GENMASK(24, 22) #define HC_CAP_CMD_SIZE GENMASK(21, 20) #define HC_CAP_DIRECT_COMMANDS_EN BIT(18) #define HC_CAP_MULTI_LANE_EN BIT(15) @@ -473,7 +472,6 @@ static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, struct i3c_hci *hci = to_i3c_hci(m); struct hci_xfer *xfer; DECLARE_COMPLETION_ONSTACK(done); - unsigned int size_limit; int i, last, ret = 0; dev_dbg(&hci->master.dev, "nxfers = %d", nxfers); @@ -482,13 +480,8 @@ static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, if (!xfer) return -ENOMEM; - size_limit = 1U << (16 + FIELD_GET(HC_CAP_MAX_DATA_LENGTH, hci->caps)); - for (i = 0; i < nxfers; i++) { xfer[i].data_len = i3c_xfers[i].len; - ret = -EFBIG; - if (xfer[i].data_len >= size_limit) - goto out; xfer[i].rnw = i3c_xfers[i].rnw; if (i3c_xfers[i].rnw) { xfer[i].data = i3c_xfers[i].data.in; From 8fe4a1b93aa909639fb1da76850294b220e3b763 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:44 +0300 Subject: [PATCH 15/28] i3c: mipi-i3c-hci: Remove invalid HDR-BT and Fm/Fm+ definitions Several HCI definitions describe capabilities or modes that are not defined by any released version of the I3C HCI specification. HC_CAP_HDR_BT_EN uses a reserved HC_CAPABILITIES bit. Similarly, MODE_I3C_HDR_BT uses a MODE value reserved for I3C transfers, while MODE_I3C_Fm_FmP cannot be encoded in the 3-bit Command Descriptor MODE field. These definitions are currently unused, so there is no functional change. Remove them to avoid implying support for non-existent HCI features and to prevent future misuse. Assisted-by: GitHub-Copilot:claude-opus-5 Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-15-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/cmd_v1.c | 2 -- drivers/i3c/master/mipi-i3c-hci/core.c | 1 - 2 files changed, 3 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c b/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c index 3b9345718d27..9910df53c013 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c +++ b/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c @@ -108,8 +108,6 @@ enum hci_cmd_mode { MODE_I3C_SDR4 = 0x4, MODE_I3C_HDR_TSx = 0x5, MODE_I3C_HDR_DDR = 0x6, - MODE_I3C_HDR_BT = 0x7, - MODE_I3C_Fm_FmP = 0x8, MODE_I2C_Fm = 0x0, MODE_I2C_FmP = 0x1, MODE_I2C_UD1 = 0x2, diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 4629a16debc9..7a39be64c4e1 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -55,7 +55,6 @@ #define HC_CAP_DIRECT_COMMANDS_EN BIT(18) #define HC_CAP_MULTI_LANE_EN BIT(15) #define HC_CAP_CMD_CCC_DEFBYTE BIT(10) -#define HC_CAP_HDR_BT_EN BIT(8) #define HC_CAP_HDR_TS_EN BIT(7) #define HC_CAP_HDR_DDR_EN BIT(6) #define HC_CAP_NON_CURRENT_MASTER_CAP BIT(5) /* master handoff capable */ From b7e849313eba3c6ecadb89961b0abb431278a5ac Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:45 +0300 Subject: [PATCH 16/28] i3c: mipi-i3c-hci: Support configurable device NACK retries Implement the .set_dev_nack_retry() master operation using the DAT_0_DEV_NACK_RETRY_CNT field in Device Address Table entries. Since the retry count is programmed per DAT entry, update all allocated entries when the setting changes. Also initialize newly allocated entries with the current retry count so that a consistent retry policy is applied across all devices. Add a DAT helper to update the retry count of an individual entry. Return -ERANGE if the requested retry count exceeds the hardware field width and -EOPNOTSUPP when the active command descriptor model does not use DAT entries. In addition, default the retry count to 1. The I3C specification mandates a retry when a Target NACKs its Dynamic Address. HCI v1.1 explicitly preserves that behaviour for Direct CCCs even when DEV_NACK_RETRY_CNT is programmed to 0, but HCI v1.0 defines the field only as a device-specific retry count and does not provide the same exception. As a result, a v1.0 controller left at the reset value of 0 may perform no retry whereas a v1.1 controller will retry once. Promoting 0 to 1 at probe time makes the behaviour consistent across controller versions. Userspace can still select 0 retries explicitly via dev_nack_retry_count. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-16-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/core.c | 32 ++++++++++++++++++++++++ drivers/i3c/master/mipi-i3c-hci/dat.h | 2 ++ drivers/i3c/master/mipi-i3c-hci/dat_v1.c | 13 +++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 7a39be64c4e1..d6a8a7612d46 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -555,6 +555,11 @@ static int i3c_hci_i2c_xfers(struct i2c_dev_desc *dev, return ret; } +static void i3c_hci_dat_v1_set_curr_nack_retry(struct i3c_hci *hci, unsigned int dat_idx) +{ + mipi_i3c_hci_dat_v1.set_nack_retry(hci, dat_idx, hci->master.dev_nack_retry_count); +} + static int i3c_hci_attach_i3c_dev(struct i3c_dev_desc *dev) { struct i3c_master_controller *m = i3c_dev_get_master(dev); @@ -573,6 +578,7 @@ static int i3c_hci_attach_i3c_dev(struct i3c_dev_desc *dev) } mipi_i3c_hci_dat_v1.set_dynamic_addr(hci, ret, dev->info.dyn_addr ?: dev->info.static_addr); + i3c_hci_dat_v1_set_curr_nack_retry(hci, ret); dev_data->dat_idx = ret; } i3c_dev_set_master_data(dev, dev_data); @@ -622,6 +628,7 @@ static int i3c_hci_attach_i2c_dev(struct i2c_dev_desc *dev) } mipi_i3c_hci_dat_v1.set_static_addr(hci, ret, dev->addr); mipi_i3c_hci_dat_v1.set_flags(hci, ret, DAT_0_I2C_DEVICE, 0); + i3c_hci_dat_v1_set_curr_nack_retry(hci, ret); dev_data->dat_idx = ret; i2c_dev_set_master_data(dev, dev_data); return 0; @@ -733,6 +740,23 @@ static void i3c_hci_recycle_ibi_slot(struct i3c_dev_desc *dev, hci->io->recycle_ibi_slot(hci, dev, slot); } +static int i3c_hci_set_dev_nack_retry(struct i3c_master_controller *m, unsigned int cnt) +{ + struct i3c_hci *hci = to_i3c_hci(m); + unsigned int dat_idx; + + if (hci->cmd != &mipi_i3c_hci_cmd_v1) + return -EOPNOTSUPP; + + if (cnt > FIELD_MAX(DAT_0_DEV_NACK_RETRY_CNT)) + return -ERANGE; + + for_each_set_bit(dat_idx, hci->DAT_data, hci->DAT_entries) + mipi_i3c_hci_dat_v1.set_nack_retry(hci, dat_idx, cnt); + + return 0; +} + static const struct i3c_master_controller_ops i3c_hci_ops = { .bus_init = i3c_hci_bus_init, .bus_cleanup = i3c_hci_bus_cleanup, @@ -752,6 +776,7 @@ static const struct i3c_master_controller_ops i3c_hci_ops = { .recycle_ibi_slot = i3c_hci_recycle_ibi_slot, .enable_hotjoin = i3c_hci_enable_hotjoin, .disable_hotjoin = i3c_hci_disable_hotjoin, + .set_dev_nack_retry = i3c_hci_set_dev_nack_retry, }; static irqreturn_t i3c_hci_irq_handler(int irq, void *dev_id) @@ -1186,6 +1211,13 @@ static int i3c_hci_probe(struct platform_device *pdev) if (device_can_wakeup(i3c_hci_sysdev(&pdev->dev))) hci->master.ibi_wakeup = true; + /* + * HCI v1.1 onward does 1 retry for Direct CCCs anyway, so for v1.0 to + * be consistent, promote 0 to 1. + */ + if (!hci->master.dev_nack_retry_count) + hci->master.dev_nack_retry_count = 1; + return i3c_master_register(&hci->master, &pdev->dev, &i3c_hci_ops, false); } diff --git a/drivers/i3c/master/mipi-i3c-hci/dat.h b/drivers/i3c/master/mipi-i3c-hci/dat.h index 6881f19da77f..31becc6f9aec 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dat.h +++ b/drivers/i3c/master/mipi-i3c-hci/dat.h @@ -12,6 +12,7 @@ /* Global DAT flags */ #define DAT_0_I2C_DEVICE W0_BIT_(31) +#define DAT_0_DEV_NACK_RETRY_CNT W0_MASK(30, 29) #define DAT_0_SIR_REJECT W0_BIT_(13) #define DAT_0_IBI_PAYLOAD W0_BIT_(12) @@ -25,6 +26,7 @@ struct hci_dat_ops { void (*clear_flags)(struct i3c_hci *hci, unsigned int dat_idx, u32 w0, u32 w1); int (*get_index)(struct i3c_hci *hci, u8 address); void (*restore)(struct i3c_hci *hci); + void (*set_nack_retry)(struct i3c_hci *hci, unsigned int dat_idx, unsigned int cnt); }; extern const struct hci_dat_ops mipi_i3c_hci_dat_v1; diff --git a/drivers/i3c/master/mipi-i3c-hci/dat_v1.c b/drivers/i3c/master/mipi-i3c-hci/dat_v1.c index 852966aa20d9..ec0764a27c62 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dat_v1.c +++ b/drivers/i3c/master/mipi-i3c-hci/dat_v1.c @@ -24,7 +24,7 @@ #define DAT_1_AUTOCMD_VALUE W1_MASK(47, 40) #define DAT_1_AUTOCMD_MASK W1_MASK(39, 32) /* DAT_0_I2C_DEVICE W0_BIT_(31) */ -#define DAT_0_DEV_NACK_RETRY_CNT W0_MASK(30, 29) +/* DAT_0_DEV_NACK_RETRY_CNT W0_MASK(30, 29) */ #define DAT_0_RING_ID W0_MASK(28, 26) #define DAT_0_DYNADDR_PARITY W0_BIT_(23) #define DAT_0_DYNAMIC_ADDRESS W0_MASK(22, 16) @@ -189,6 +189,16 @@ static void hci_dat_v1_restore(struct i3c_hci *hci) } } +static void hci_dat_v1_set_nack_retry(struct i3c_hci *hci, unsigned int dat_idx, unsigned int cnt) +{ + u32 dat_w0; + + dat_w0 = dat_w0_read(dat_idx); + dat_w0 &= ~DAT_0_DEV_NACK_RETRY_CNT; + dat_w0 |= FIELD_PREP(DAT_0_DEV_NACK_RETRY_CNT, cnt); + dat_w0_write(dat_idx, dat_w0); +} + const struct hci_dat_ops mipi_i3c_hci_dat_v1 = { .init = hci_dat_v1_init, .alloc_entry = hci_dat_v1_alloc_entry, @@ -199,4 +209,5 @@ const struct hci_dat_ops mipi_i3c_hci_dat_v1 = { .clear_flags = hci_dat_v1_clear_flags, .get_index = hci_dat_v1_get_index, .restore = hci_dat_v1_restore, + .set_nack_retry = hci_dat_v1_set_nack_retry, }; From b968be9e719302c6d74beb16224e9d956d26433d Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:46 +0300 Subject: [PATCH 17/28] i3c: Restrict HDR modes to those supported by the bus and target I3C v1.1.1 Table 9 defines which HDR Modes are permitted for each Bus Configuration. A Mixed Slow / Limited Bus permits no HDR Modes because Legacy I2C Devices without a 50 ns SCL spike filter cannot be shielded from HDR traffic. A Mixed Fast Bus permits HDR-DDR and HDR-TSL, but not HDR-TSP, since of the two Ternary Modes only HDR-TSL is defined for a Bus that also has Legacy I2C Devices. However, both i3c_device_get_supported_xfer_mode() and the transfer validation in i3c_dev_do_xfers_locked() currently consider only the controller's hdr_cap. Since hdr_cap reflects controller capability rather than bus or target constraints, HDR Modes can be advertised and used even when the bus configuration forbids them. Likewise, HDR Modes unsupported by the target may be reported as available. Add i3c_dev_supported_xfer_modes_locked(), which derives the usable transfer modes from the intersection of: - HDR Modes supported by the controller - HDR Modes allowed by the bus configuration - HDR Modes supported by the target Use this helper both when reporting supported transfer modes and when validating transfers, ensuring that the advertised and permitted modes remain consistent. GETHDRCAP is queried only when I3C_BCR_HDR_CAP is set, so a target without that bit is treated as supporting no HDR Modes. i3c_device_get_supported_xfer_mode() now takes the normal-use bus lock in order to access the device descriptor, matching i3c_device_get_info(). Also fix the kernel-doc reference to enum i3c_xfer_mode. Fixes: 256a21743d91 ("i3c: Add HDR API support") Assisted-by: GitHub-Copilot:claude-opus-5 Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-17-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/device.c | 15 ++++++++--- drivers/i3c/internals.h | 2 ++ drivers/i3c/master.c | 57 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/drivers/i3c/device.c b/drivers/i3c/device.c index f1ba363b22a1..4df4682af150 100644 --- a/drivers/i3c/device.c +++ b/drivers/i3c/device.c @@ -301,16 +301,23 @@ i3c_device_match_id(struct i3c_device *i3cdev, EXPORT_SYMBOL_GPL(i3c_device_match_id); /** - * i3c_device_get_supported_xfer_mode - Returns the supported transfer mode by - * connected master controller. + * i3c_device_get_supported_xfer_mode - Returns the transfer modes supported by + * the connected master controller and @dev, + * and allowed by the bus configuration. * @dev: I3C device * * Return: a bit mask, which supported transfer mode, bit position is defined at - * enum i3c_hdr_mode + * enum i3c_xfer_mode */ u32 i3c_device_get_supported_xfer_mode(struct i3c_device *dev) { - return i3c_bus_to_i3c_master(dev->bus)->this->info.hdr_cap | BIT(I3C_SDR); + u32 modes; + + i3c_bus_normaluse_lock(dev->bus); + modes = i3c_dev_supported_xfer_modes_locked(dev->desc); + i3c_bus_normaluse_unlock(dev->bus); + + return modes; } EXPORT_SYMBOL_GPL(i3c_device_get_supported_xfer_mode); diff --git a/drivers/i3c/internals.h b/drivers/i3c/internals.h index 86a36b951e0d..9a6936e2c660 100644 --- a/drivers/i3c/internals.h +++ b/drivers/i3c/internals.h @@ -18,6 +18,8 @@ bool i3c_bus_rpm_ibi_allowed(struct i3c_bus *bus); void i3c_bus_normaluse_lock(struct i3c_bus *bus); void i3c_bus_normaluse_unlock(struct i3c_bus *bus); +u32 i3c_dev_supported_xfer_modes_locked(struct i3c_dev_desc *dev); + 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, diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index c50887b0b37d..2cdbffd33fd7 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -938,6 +938,61 @@ static int i3c_bus_set_mode(struct i3c_bus *i3cbus, enum i3c_bus_mode mode, return 0; } +/* + * I3C v1.1.1 Section 5.1.2.4 Table 9 lists the HDR Modes each Bus + * Configuration allows. A Mixed Slow / Limited Bus has Legacy I2C Devices + * without a 50 ns spike filter, so there is no way to hide any HDR Mode from + * them. Of the two Ternary Modes, only HDR-TSL is defined for a Bus that + * also has Legacy I2C Devices; HDR-TSP is defined for a Pure Bus. + */ +static u32 i3c_bus_hdr_modes(struct i3c_bus *bus) +{ + switch (bus->mode) { + case I3C_BUS_MODE_PURE: + return BIT(I3C_HDR_DDR) | BIT(I3C_HDR_TSP) | BIT(I3C_HDR_TSL); + case I3C_BUS_MODE_MIXED_FAST: + return BIT(I3C_HDR_DDR) | BIT(I3C_HDR_TSL); + case I3C_BUS_MODE_MIXED_LIMITED: + case I3C_BUS_MODE_MIXED_SLOW: + break; + } + + return 0; +} + +static u32 i3c_dev_hdr_modes(struct i3c_dev_desc *dev) +{ + if (!(dev->info.bcr & I3C_BCR_HDR_CAP)) + return 0; + + return dev->info.hdr_cap; +} + +/** + * i3c_dev_supported_xfer_modes_locked() - Get the transfer modes usable with a + * device + * @dev: I3C device descriptor + * + * The HDR Modes the controller and @dev both support, restricted to those the + * bus configuration allows. SDR is always supported. + * + * The bus lock must be held in normal use mode. + * + * Return: a bit mask of &enum i3c_xfer_mode values. + */ +u32 i3c_dev_supported_xfer_modes_locked(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *master = i3c_dev_get_master(dev); + + /* + * master->this->info.bcr is ignored because it describes the master's + * target capability, not its controller capability. + */ + return (master->this->info.hdr_cap & + i3c_bus_hdr_modes(&master->bus) & + i3c_dev_hdr_modes(dev)) | BIT(I3C_SDR); +} + static struct i3c_master_controller * i2c_adapter_to_i3c_master(struct i2c_adapter *adap) { @@ -3856,7 +3911,7 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers, if (!master || !xfers) return -EINVAL; - if (mode != I3C_SDR && !(master->this->info.hdr_cap & BIT(mode))) + if (mode != I3C_SDR && !(i3c_dev_supported_xfer_modes_locked(dev) & BIT(mode))) return -EOPNOTSUPP; return master->ops->i3c_xfers(dev, xfers, nxfers, mode); From 9b347cbde81c6fa5b7e7043e2ca38229d6c299c4 Mon Sep 17 00:00:00 2001 From: Adrian Hunter Date: Sun, 20 Sep 2026 18:12:47 +0300 Subject: [PATCH 18/28] i3c: mipi-i3c-hci: Add HDR-DDR support Add support for HDR-DDR private transfers. Encode HDR-DDR Command Descriptors and advertise HDR-DDR capability when supported by the controller. Carry the HDR Command Code in the descriptor CP, CMD and RNW fields. Use the Immediate Data Transfer Command for writes of up to four bytes, as is already done for SDR, and the Regular Data Transfer Command otherwise. The Immediate Data Transfer Command is write-only, which suits HDR because the eighth bit of the HDR Command Code is field RNW, so a write Command Code leaves that field zero as required. Reject transfers shorter than one Data Word or with an odd byte count, as HDR-DDR transfers are defined in whole Data Words and the I3C specification requires at least one Data Word to follow the Command Word. Only advertise HDR-DDR capability when the driver can generate the required descriptor format. Assisted-by: GitHub-Copilot:claude-opus-5 Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260920151248.46936-18-adrian.hunter@intel.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/mipi-i3c-hci/cmd.h | 2 +- drivers/i3c/master/mipi-i3c-hci/cmd_v1.c | 26 +++++++++++--- drivers/i3c/master/mipi-i3c-hci/cmd_v2.c | 4 ++- drivers/i3c/master/mipi-i3c-hci/core.c | 45 +++++++++++++++++++++--- drivers/i3c/master/mipi-i3c-hci/hci.h | 9 ++--- include/linux/i3c/device.h | 4 +++ 6 files changed, 76 insertions(+), 14 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd.h b/drivers/i3c/master/mipi-i3c-hci/cmd.h index e0d4a6e0e319..fa3107df7c51 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd.h +++ b/drivers/i3c/master/mipi-i3c-hci/cmd.h @@ -55,7 +55,7 @@ struct hci_cmd_ops { int (*prep_ccc)(struct i3c_hci *hci, struct hci_xfer *xfer, u8 ccc_addr, u8 ccc_cmd, bool raw); void (*prep_i3c_xfer)(struct i3c_hci *hci, struct i3c_dev_desc *dev, - struct hci_xfer *xfer); + struct hci_xfer *xfer, enum i3c_xfer_mode mode); void (*prep_i2c_xfer)(struct i3c_hci *hci, struct i2c_dev_desc *dev, struct hci_xfer *xfer); int (*perform_daa)(struct i3c_hci *hci); diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c b/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c index 9910df53c013..78ad2ec2bd93 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c +++ b/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c @@ -215,14 +215,30 @@ static int hci_cmd_v1_prep_ccc(struct i3c_hci *hci, static void hci_cmd_v1_prep_i3c_xfer(struct i3c_hci *hci, struct i3c_dev_desc *dev, - struct hci_xfer *xfer) + struct hci_xfer *xfer, + enum i3c_xfer_mode xfer_mode) { struct i3c_hci_dev_data *dev_data = i3c_dev_get_master_data(dev); unsigned int dat_idx = dev_data->dat_idx; - enum hci_cmd_mode mode = get_i3c_mode(hci); + enum hci_cmd_mode mode; u8 *data = xfer->data; unsigned int data_len = xfer->data_len; bool rnw = xfer->rnw; + u32 cp_cmd = 0; + + if (xfer_mode == I3C_SDR) { + mode = get_i3c_mode(hci); + } else { + /* + * HDR-DDR is the only advertised HDR Mode, so a non-SDR + * transfer uses the HDR-DDR Command Code encoding: CP marks CMD + * as valid, CMD carries bits[6:0], and RNW carries bit[7]. CP + * and CMD occupy the same descriptor bits in both command + * formats. + */ + mode = MODE_I3C_HDR_DDR; + cp_cmd = CMD_R0_CP | CMD_R0_CMD(xfer->hdr_cmd & I3C_HDR_CMD_CODE); + } xfer->cmd_tid = hci_get_tid(); @@ -233,7 +249,8 @@ static void hci_cmd_v1_prep_i3c_xfer(struct i3c_hci *hci, CMD_I0_TID(xfer->cmd_tid) | CMD_I0_DEV_INDEX(dat_idx) | CMD_I0_DTT(data_len) | - CMD_I0_MODE(mode); + CMD_I0_MODE(mode) | + cp_cmd; fill_data_bytes(xfer, data, data_len); } else { /* we use a Regular Data Transfer Command */ @@ -242,7 +259,8 @@ static void hci_cmd_v1_prep_i3c_xfer(struct i3c_hci *hci, CMD_R0_TID(xfer->cmd_tid) | CMD_R0_DEV_INDEX(dat_idx) | CMD_R0_MODE(mode) | - (rnw ? CMD_R0_RNW : 0); + (rnw ? CMD_R0_RNW : 0) | + cp_cmd; xfer->cmd_desc[1] = CMD_R1_DATA_LENGTH(data_len); } diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c b/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c index 8d93748e858d..b5e25f05f03e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c +++ b/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c @@ -212,9 +212,11 @@ static int hci_cmd_v2_prep_ccc(struct i3c_hci *hci, struct hci_xfer *xfer, return 0; } +/* For cmd_v2, only SDR capability is advertised */ static void hci_cmd_v2_prep_i3c_xfer(struct i3c_hci *hci, struct i3c_dev_desc *dev, - struct hci_xfer *xfer) + struct hci_xfer *xfer, + enum i3c_xfer_mode xfer_mode) { unsigned int mode = XFERMODE_IDX_I3C_SDR; unsigned int rate = get_i3c_rate_idx(hci); diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index d6a8a7612d46..1b75df2fe34c 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -116,6 +116,13 @@ static inline struct i3c_hci *to_i3c_hci(struct i3c_master_controller *m) return container_of(m, struct i3c_hci, master); } +/* HDR support has been added for cmd_v1 only */ +static bool i3c_hci_hdr_ddr_capable(struct i3c_hci *hci) +{ + return hci->cmd == &mipi_i3c_hci_cmd_v1 && + hci->caps & HC_CAP_HDR_DDR_EN; +} + /** * i3c_hci_sysdev() - Get the device to use for DMA and system PM * @dev: Device the HCI controller is bound to @@ -158,6 +165,8 @@ static int i3c_hci_bus_init(struct i3c_master_controller *m) i3c_hci_set_master_dyn_addr(hci); memset(&info, 0, sizeof(info)); info.dyn_addr = hci->dyn_addr; + if (i3c_hci_hdr_ddr_capable(hci)) + info.hdr_cap = BIT(I3C_HDR_DDR); ret = i3c_master_set_info(m, &info); if (ret) return ret; @@ -463,6 +472,27 @@ static int i3c_hci_daa(struct i3c_master_controller *m) return ret; } +static bool i3c_hci_rnw(struct i3c_xfer *i3c_xfer, enum i3c_xfer_mode mode) +{ + if (mode == I3C_SDR) + return i3c_xfer->rnw; + + return i3c_xfer->cmd & I3C_HDR_CMD_RNW; +} + +static int i3c_hci_check_hdr_ddr_xfers(struct i3c_xfer *i3c_xfers, int nxfers) +{ + /* + * HDR-DDR frames 16-bit Data Words, and at least one Data Word must + * follow the Command Word. + */ + for (int i = 0; i < nxfers; i++) + if (i3c_xfers[i].len < 2 || i3c_xfers[i].len % 2) + return -EINVAL; + + return 0; +} + static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, struct i3c_xfer *i3c_xfers, int nxfers, enum i3c_xfer_mode mode) @@ -475,20 +505,27 @@ static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, dev_dbg(&hci->master.dev, "nxfers = %d", nxfers); + if (mode == I3C_HDR_DDR) { + ret = i3c_hci_check_hdr_ddr_xfers(i3c_xfers, nxfers); + if (ret) + return ret; + } + xfer = hci_alloc_xfer(nxfers); if (!xfer) return -ENOMEM; for (i = 0; i < nxfers; i++) { xfer[i].data_len = i3c_xfers[i].len; - xfer[i].rnw = i3c_xfers[i].rnw; - if (i3c_xfers[i].rnw) { + xfer[i].rnw = i3c_hci_rnw(i3c_xfers + i, mode); + xfer[i].hdr_cmd = i3c_xfers[i].cmd; + if (xfer[i].rnw) { xfer[i].data = i3c_xfers[i].data.in; } else { /* silence the const qualifier warning with a cast */ xfer[i].data = (void *) i3c_xfers[i].data.out; } - hci->cmd->prep_i3c_xfer(hci, dev, &xfer[i]); + hci->cmd->prep_i3c_xfer(hci, dev, &xfer[i], mode); xfer[i].cmd_desc[0] |= CMD_0_ROC; } last = i - 1; @@ -500,7 +537,7 @@ static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, if (ret) goto out; for (i = 0; i < nxfers; i++) { - if (i3c_xfers[i].rnw) + if (xfer[i].rnw) i3c_xfers[i].len = RESP_DATA_LENGTH(xfer[i].response); if (RESP_STATUS(xfer[i].response) != RESP_SUCCESS) { ret = -EIO; diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index ee73f6e6756a..4288a1b5b491 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -85,10 +85,10 @@ struct i3c_hci { /* * Structure to represent a master initiated transfer. - * The rnw, data and data_len fields must be initialized before calling any - * hci->cmd->*() method. The cmd method will initialize cmd_desc[] and - * possibly modify (clear) the data field. Then xfer->cmd_desc[0] can - * be augmented with CMD_0_ROC and/or CMD_0_TOC. + * The rnw, data and data_len (and hdr_cmd for HDR) fields must be initialized + * before calling any hci->cmd->*() method. The cmd method will initialize + * cmd_desc[] and possibly modify (clear) the data field. Then xfer->cmd_desc[0] + * can be augmented with CMD_0_ROC and/or CMD_0_TOC. * The completion field needs to be initialized before queueing with * hci->io->queue_xfer(), and requires CMD_0_ROC to be set. */ @@ -97,6 +97,7 @@ struct hci_xfer { u32 response; bool rnw; bool started; + u8 hdr_cmd; void *data; unsigned int data_len; unsigned int cmd_tid; diff --git a/include/linux/i3c/device.h b/include/linux/i3c/device.h index 0f065b883ee0..e30ea2cb13cb 100644 --- a/include/linux/i3c/device.h +++ b/include/linux/i3c/device.h @@ -79,6 +79,10 @@ struct i3c_xfer { enum i3c_error_code err; }; +/* For splitting 'cmd' from struct i3c_xfer */ +#define I3C_HDR_CMD_RNW BIT(7) +#define I3C_HDR_CMD_CODE GENMASK(6, 0) + /** * enum i3c_dcr - I3C DCR values * @I3C_DCR_GENERIC_DEVICE: generic I3C device From bf9ec06c90dcd8ccba116fbc065b5d918de47644 Mon Sep 17 00:00:00 2001 From: Shubham Patil Date: Tue, 8 Sep 2026 16:42:07 +0530 Subject: [PATCH 19/28] i3c: master: amd: Report direct CCC read length in payload.actual_len The I3C core expects master drivers to leave payload.len unchanged and report the number of bytes received on GET CCCs via payload.actual_len. The AMD driver was overwriting payload.len after direct read CCCs. Set payload.actual_len from the directed command's rx_actual in xi3c_master_send_direct_ccc_cmd() instead of modifying payload.len. Fixes:629a6ddd1d9a ("i3c: master: Add driver for AMD AXI I3C master controller") Signed-off-by: Shubham Patil Reviewed-by: Frank Li Link: https://patch.msgid.link/20260908111207.3274407-1-shubhamsanjay.patil@amd.com Signed-off-by: Alexandre Belloni --- drivers/i3c/master/amd-i3c-master.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/amd-i3c-master.c b/drivers/i3c/master/amd-i3c-master.c index ef5ad5abb788..f205aa06f9b8 100644 --- a/drivers/i3c/master/amd-i3c-master.c +++ b/drivers/i3c/master/amd-i3c-master.c @@ -806,8 +806,8 @@ static int xi3c_master_send_direct_ccc_cmd(struct xi3c_master *master, /* Report actual byte count so the core sees the right length on short reads */ if (!ret && ccc->rnw) - ccc->dests[0].payload.len = min(xfer->cmds[1].rx_actual, - ccc->dests[0].payload.len); + ccc->dests[0].payload.actual_len = min(xfer->cmds[1].rx_actual, + ccc->dests[0].payload.len); return ret; } From 1c1e2d90aa452764df66bb1dd774aa0d06d89cc8 Mon Sep 17 00:00:00 2001 From: Fan Wu Date: Wed, 23 Sep 2026 08:53:40 +0000 Subject: [PATCH 20/28] i3c: master: adi: free the IRQ before unregistering the master adi_i3c_master_irq() is requested with devm_request_irq(), so it stays registered when adi_i3c_master_remove() runs, and the writes masking the controller IRQ sources do not synchronize with a handler that is already running on another CPU. i3c_master_unregister() then frees the struct i3c_dev_desc objects that master->ibi.slots[] still point to, so the IBI path in adi_i3c_master_handle_ibi() can dereference freed memory. Fix this by storing the IRQ number in the private structure and calling devm_free_irq() after the IRQ sources are masked, so a running handler is finished before the master is torn down. This issue was found by an in-house static analysis tool. Fixes: a79ac2cdc91d ("i3c: master: Add driver for Analog Devices I3C Controller IP") Cc: stable@vger.kernel.org Co-developed-by: Song Li Signed-off-by: Song Li Signed-off-by: Fan Wu Reviewed-by: Frank Li Link: https://patch.msgid.link/20260923085340.490776-1-fanwu01@zju.edu.cn Signed-off-by: Alexandre Belloni --- drivers/i3c/master/adi-i3c-master.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/drivers/i3c/master/adi-i3c-master.c b/drivers/i3c/master/adi-i3c-master.c index b35386260350..ad57b7500678 100644 --- a/drivers/i3c/master/adi-i3c-master.c +++ b/drivers/i3c/master/adi-i3c-master.c @@ -115,6 +115,7 @@ struct adi_i3c_master { spinlock_t lock; /* Protect transfer */ } xferqueue; void __iomem *regs; + int irq; struct clk *clk; unsigned long i3c_scl_lim; struct { @@ -957,6 +958,7 @@ static int adi_i3c_master_probe(struct platform_device *pdev) irq = platform_get_irq(pdev, 0); if (irq < 0) return irq; + master->irq = irq; version = readl(master->regs + ADI_AXI_REG_VERSION); if (ADI_AXI_PCORE_VER_MAJOR(version) != 1) @@ -1001,6 +1003,8 @@ static void adi_i3c_master_remove(struct platform_device *pdev) writel(0x00, master->regs + REG_IRQ_MASK); writel(0x01, master->regs + REG_ENABLE); + devm_free_irq(&pdev->dev, master->irq, master); + i3c_master_unregister(&master->base); } From 609611d21b618e6173041cfe0c1670fc1e485c46 Mon Sep 17 00:00:00 2001 From: Pawel Zalewski Date: Wed, 23 Sep 2026 16:47:05 +0100 Subject: [PATCH 21/28] i3c: master: use named initializers for acpi_device_id Use a designated initializer for the acpi_device_id fields which makes the code more readable and consistent with how lists are initialized in the rest of the kernel code base. Unify the list terminator to have a single space between the brackets and no trailing comma. Signed-off-by: Pawel Zalewski Reviewed-by: Frank Li Link: https://patch.msgid.link/20260923-acpi-i3c-v1-1-2e78ebef251e@thegoodpenguin.co.uk Signed-off-by: Alexandre Belloni --- drivers/i3c/master/dw-i3c-master.c | 4 ++-- drivers/i3c/master/mipi-i3c-hci/core.c | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index 4563d8761ba0..05584ddaf104 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1934,8 +1934,8 @@ static const struct of_device_id dw_i3c_master_of_match[] = { MODULE_DEVICE_TABLE(of, dw_i3c_master_of_match); static const struct acpi_device_id dw_i3c_master_acpi_match[] = { - { "AMDI0015", AMD_I3C_OD_PP_TIMING }, - { "NVDA2018", DW_I3C_ACPI_SKIP_CLK_RST }, + { .id = "AMDI0015", .driver_data = AMD_I3C_OD_PP_TIMING }, + { .id = "NVDA2018", .driver_data = DW_I3C_ACPI_SKIP_CLK_RST }, { } }; MODULE_DEVICE_TABLE(acpi, dw_i3c_master_acpi_match); diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 1b75df2fe34c..23853ca134fb 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -1275,8 +1275,10 @@ static const __maybe_unused struct of_device_id i3c_hci_of_match[] = { MODULE_DEVICE_TABLE(of, i3c_hci_of_match); static const struct acpi_device_id i3c_hci_acpi_match[] = { - { "AMDI5017", HCI_QUIRK_PIO_MODE | HCI_QUIRK_OD_PP_TIMING | HCI_QUIRK_RESP_BUF_THLD }, - {} + { .id = "AMDI5017", + .driver_data = HCI_QUIRK_PIO_MODE | HCI_QUIRK_OD_PP_TIMING | + HCI_QUIRK_RESP_BUF_THLD }, + { } }; MODULE_DEVICE_TABLE(acpi, i3c_hci_acpi_match); From 9d60a0eafae188d2189f03501681f7c8f9503404 Mon Sep 17 00:00:00 2001 From: Aman Kumar Pandey Date: Tue, 22 Sep 2026 16:05:43 +0530 Subject: [PATCH 22/28] i3c: master: Add APIs for I3C hub support Add CCC helpers to check CCC support and send CCC commands, address slot helpers to query and update I3C bus address slot state, registering virtual masters with an explicit firmware node, and exposing the bus maintenance lock helpers. 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 Link: https://patch.msgid.link/20260922103551.2754613-2-lakshay.piplani@nxp.com Signed-off-by: Alexandre Belloni --- 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 2cdbffd33fd7..d1e8cae374b9 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -50,10 +50,11 @@ static BLOCKING_NOTIFIER_HEAD(i3c_bus_notifier); * logic to rely on I3C device information that could be changed behind their * back. */ -static void i3c_bus_maintenance_lock(struct i3c_bus *bus) +void i3c_bus_maintenance_lock(struct i3c_bus *bus) { down_write(&bus->lock); } +EXPORT_SYMBOL_GPL(i3c_bus_maintenance_lock); /** * i3c_bus_maintenance_unlock - Release the bus lock after a maintenance @@ -64,10 +65,11 @@ static void i3c_bus_maintenance_lock(struct i3c_bus *bus) * i3c_bus_maintenance_lock() for more details on what these maintenance * operations are. */ -static void i3c_bus_maintenance_unlock(struct i3c_bus *bus) +void i3c_bus_maintenance_unlock(struct i3c_bus *bus) { up_write(&bus->lock); } +EXPORT_SYMBOL_GPL(i3c_bus_maintenance_unlock); /** * i3c_bus_normaluse_lock - Lock the bus for a normal operation @@ -89,6 +91,7 @@ void i3c_bus_normaluse_lock(struct i3c_bus *bus) { down_read(&bus->lock); } +EXPORT_SYMBOL_GPL(i3c_bus_normaluse_lock); /** * i3c_bus_normaluse_unlock - Release the bus lock after a normal operation @@ -102,6 +105,7 @@ void i3c_bus_normaluse_unlock(struct i3c_bus *bus) { up_read(&bus->lock); } +EXPORT_SYMBOL_GPL(i3c_bus_normaluse_unlock); static struct i3c_master_controller *dev_to_i3cmaster(struct device *dev) { @@ -403,11 +407,19 @@ i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask) return status & mask; } -static enum i3c_addr_slot_status +/** + * i3c_bus_get_addr_slot_status() - Get I3C bus address slot status + * @bus: I3C bus. + * @addr: I3C address to query. + * + * Return: Address slot status for @addr. + */ +enum i3c_addr_slot_status i3c_bus_get_addr_slot_status(struct i3c_bus *bus, u16 addr) { return i3c_bus_get_addr_slot_status_mask(bus, addr, I3C_ADDR_SLOT_STATUS_MASK); } +EXPORT_SYMBOL_GPL(i3c_bus_get_addr_slot_status); static void i3c_bus_set_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, enum i3c_addr_slot_status status, u32 mask) @@ -423,11 +435,18 @@ static void i3c_bus_set_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, *ptr |= ((unsigned long)status & mask) << (bitpos % BITS_PER_LONG); } -static void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr, - enum i3c_addr_slot_status status) +/** + * i3c_bus_set_addr_slot_status() - Set I3C bus address slot status + * @bus: I3C bus. + * @addr: I3C address to update. + * @status: Address slot status to set. + */ +void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr, + enum i3c_addr_slot_status status) { i3c_bus_set_addr_slot_status_mask(bus, addr, status, I3C_ADDR_SLOT_STATUS_MASK); } +EXPORT_SYMBOL_GPL(i3c_bus_set_addr_slot_status); static bool i3c_bus_dev_addr_is_avail(struct i3c_bus *bus, u8 addr) { @@ -2862,6 +2881,59 @@ static void i3c_master_reconcile_dyn_addrs(struct i3c_master_controller *master) } } +/** + * i3c_master_supports_ccc_cmd() - check CCC command support + * @master: I3C master controller + * @cmd: CCC command to verify + * + * Return: true if @cmd is supported, false otherwise. + */ +bool i3c_master_supports_ccc_cmd(struct i3c_master_controller *master, + const struct i3c_ccc_cmd *cmd) +{ + if (!master || !cmd) + return false; + + if (!master->ops->send_ccc_cmd) + return false; + + if (!master->ops->supports_ccc_cmd) + return true; + + return master->ops->supports_ccc_cmd(master, cmd); +} +EXPORT_SYMBOL_GPL(i3c_master_supports_ccc_cmd); + +/** + * i3c_master_send_ccc_cmd() - send a CCC command + * @master: I3C master controller issuing the command + * @cmd: CCC command to be sent + * + * This function sends a Common Command Code (CCC) command to devices on the + * I3C bus. It acquires the bus maintenance lock, executes the command, and + * then releases the lock to ensure safe access to the bus. + * + * Return: 0 on success, or a negative error code on failure. + */ +int i3c_master_send_ccc_cmd(struct i3c_master_controller *master, + struct i3c_ccc_cmd *cmd) +{ + int ret; + + ret = i3c_master_rpm_get(master); + if (ret) + return ret; + + i3c_bus_maintenance_lock(&master->bus); + ret = i3c_master_send_ccc_cmd_locked(master, cmd); + i3c_bus_maintenance_unlock(&master->bus); + + i3c_master_rpm_put(master); + + return ret; +} +EXPORT_SYMBOL_GPL(i3c_master_send_ccc_cmd); + /** * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) * @master: controller @@ -3698,30 +3770,23 @@ static int i3c_master_check_ops(const struct i3c_master_controller_ops *ops) } /** - * i3c_master_register() - register an I3C master + * i3c_master_register_fwnode() - register an I3C master with a custom fwnode * @master: master used to send frames on the bus - * @parent: the parent device (the one that provides this I3C master - * controller) + * @parent: the parent device providing this I3C master controller + * @fwnode: firmware node describing this I3C bus, or NULL * @ops: the master controller operations - * @secondary: true if you are registering a secondary master. Will return - * -EOPNOTSUPP if set to true since secondary masters are not yet - * supported + * @secondary: true if registering a secondary master * - * This function takes care of everything for you: - * - * - 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 = I3C_BUS_I2C_FM_PLUS_SCL_MAX_RATE; struct i3c_bus *i3cbus = i3c_master_get_bus(master); @@ -3738,7 +3803,7 @@ int i3c_master_register(struct i3c_master_controller *master, return ret; master->dev.parent = parent; - device_set_node(&master->dev, fwnode_handle_get(dev_fwnode(parent))); + device_set_node(&master->dev, fwnode_handle_get(fwnode)); master->dev.bus = &i3c_bus_type; master->dev.type = &i3c_masterdev_type; master->dev.release = i3c_masterdev_release; @@ -3856,6 +3921,38 @@ int i3c_master_register(struct i3c_master_controller *master, return ret; } +EXPORT_SYMBOL_GPL(i3c_master_register_fwnode); + +/** + * i3c_master_register() - register an I3C master + * @master: master used to send frames on the bus + * @parent: the parent device (the one that provides this I3C master + * controller) + * @ops: the master controller operations + * @secondary: true if you are registering a secondary master. Will return + * -EOPNOTSUPP if set to true since secondary masters are not yet + * supported + * + * This function takes care of everything for you: + * + * - creates and initializes the I3C bus + * - populates the bus with static I2C devs if @parent->of_node is not + * NULL + * - registers all I3C devices added by the controller during bus + * initialization + * - registers the I2C adapter and all I2C devices + * + * Return: 0 in case of success, a negative error code otherwise. + */ +int i3c_master_register(struct i3c_master_controller *master, + struct device *parent, + const struct i3c_master_controller_ops *ops, + bool secondary) +{ + return i3c_master_register_fwnode(master, parent, + dev_fwnode(parent), + ops, secondary); +} EXPORT_SYMBOL_GPL(i3c_master_register); /** @@ -3916,6 +4013,7 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers, return master->ops->i3c_xfers(dev, xfers, nxfers, mode); } +EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked); /** * i3c_dev_disable_ibi_locked() - Disable IBIs coming from a specific device diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 72065715feee..d58070a72a49 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -652,9 +652,18 @@ DEFINE_FREE(i3c_master_dma_unmap_single, void *, int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev, u8 old_dyn_addr); +int i3c_master_send_ccc_cmd(struct i3c_master_controller *master, + struct i3c_ccc_cmd *cmd); +bool i3c_master_supports_ccc_cmd(struct i3c_master_controller *master, + const struct i3c_ccc_cmd *cmd); int i3c_master_set_info(struct i3c_master_controller *master, const struct i3c_device_info *info); +int i3c_master_register_fwnode(struct i3c_master_controller *master, + struct device *parent, + struct fwnode_handle *fwnode, + const struct i3c_master_controller_ops *ops, + bool secondary); int i3c_master_register(struct i3c_master_controller *master, struct device *parent, const struct i3c_master_controller_ops *ops, @@ -778,4 +787,12 @@ void i3c_for_each_bus_locked(int (*fn)(struct i3c_bus *bus, void *data), int i3c_register_notifier(struct notifier_block *nb); int i3c_unregister_notifier(struct notifier_block *nb); +enum i3c_addr_slot_status +i3c_bus_get_addr_slot_status(struct i3c_bus *bus, u16 addr); + +void i3c_bus_set_addr_slot_status(struct i3c_bus *bus, u16 addr, + enum i3c_addr_slot_status status); + +void i3c_bus_maintenance_lock(struct i3c_bus *bus); +void i3c_bus_maintenance_unlock(struct i3c_bus *bus); #endif /* I3C_MASTER_H */ From 8d0008260c32206e41d4c35a99b281da48f7c2cf Mon Sep 17 00:00:00 2001 From: Lakshay Piplani Date: Tue, 22 Sep 2026 16:05:44 +0530 Subject: [PATCH 23/28] i3c: master: Add controller-only device operation helpers The generic I3C device attach and IBI paths perform both core-level bookkeeping and controller-specific operations. An I3C hub, however, maintains a logical device descriptor on the virtual downstream bus and a separate parent-facing descriptor used by the physical parent controller. Add 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 Link: https://patch.msgid.link/20260922103551.2754613-3-lakshay.piplani@nxp.com Signed-off-by: Alexandre Belloni --- 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 9a6936e2c660..24f67fef65bd 100644 --- a/drivers/i3c/internals.h +++ b/drivers/i3c/internals.h @@ -24,6 +24,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 d1e8cae374b9..01e05bad8661 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1949,6 +1949,30 @@ static int i3c_master_get_i3c_addrs(struct i3c_dev_desc *dev) return -EBUSY; } +/** + * i3c_master_attach_i3c_dev_controller_locked() - Attach device state to + * controller + * @dev: I3C device descriptor + * + * Invoke the current controller's attach callback without changing address + * slot state or adding the device to the controller's device list. + * + * Context: The caller must hold the bus lock. + * + * Return: 0 on success, or a negative error code returned by the controller. + */ +int i3c_master_attach_i3c_dev_controller_locked(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *master = i3c_dev_get_master(dev); + + /* Do not attach the master device itself. */ + if (master->this != dev && master->ops->attach_i3c_dev) + return master->ops->attach_i3c_dev(dev); + + return 0; +} +EXPORT_SYMBOL_GPL(i3c_master_attach_i3c_dev_controller_locked); + static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master, struct i3c_dev_desc *dev) { @@ -1966,12 +1990,10 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master, return ret; /* Do not attach the master device itself. */ - if (master->this != dev && master->ops->attach_i3c_dev) { - ret = master->ops->attach_i3c_dev(dev); - if (ret) { - i3c_master_put_i3c_addrs(dev); - return ret; - } + ret = i3c_master_attach_i3c_dev_controller_locked(dev); + if (ret) { + i3c_master_put_i3c_addrs(dev); + return ret; } list_add_tail(&dev->common.node, &master->bus.devs.i3c); @@ -1979,6 +2001,31 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master, return 0; } +/** + * i3c_master_reattach_i3c_dev_controller_locked() - Reattach controller + * device state + * @dev: I3C device descriptor + * @old_dyn_addr: Previous dynamic address + * + * Invoke the current controller's reattach callback without modifying the + * controller's address-slot state. + * + * Context: The caller must hold the bus lock. + * + * Return: 0 on success, or a negative error code returned by the controller. + */ +int i3c_master_reattach_i3c_dev_controller_locked(struct i3c_dev_desc *dev, + u8 old_dyn_addr) +{ + struct i3c_master_controller *master = i3c_dev_get_master(dev); + + if (master->ops->reattach_i3c_dev) + return master->ops->reattach_i3c_dev(dev, old_dyn_addr); + + return 0; +} +EXPORT_SYMBOL_GPL(i3c_master_reattach_i3c_dev_controller_locked); + /** * i3c_master_reattach_i3c_dev_locked() - reattach an I3C device with a new address * @dev: I3C device descriptor to reattach @@ -2009,25 +2056,39 @@ int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev, I3C_ADDR_SLOT_FREE); } - if (master->ops->reattach_i3c_dev) { - ret = master->ops->reattach_i3c_dev(dev, old_dyn_addr); - if (ret) { - i3c_master_put_i3c_addrs(dev); - return ret; - } + ret = i3c_master_reattach_i3c_dev_controller_locked(dev, old_dyn_addr); + if (ret) { + i3c_master_put_i3c_addrs(dev); + return ret; } return 0; } EXPORT_SYMBOL_GPL(i3c_master_reattach_i3c_dev_locked); -static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev) +/** + * i3c_master_detach_i3c_dev_controller_locked() - Detach device state from + * controller + * @dev: I3C device descriptor + * + * Invoke the current controller's detach callback without releasing address + * slots or removing the device from the controller's device list. + * + * Context: The caller must hold the bus lock. + */ +void i3c_master_detach_i3c_dev_controller_locked(struct i3c_dev_desc *dev) { struct i3c_master_controller *master = i3c_dev_get_master(dev); /* Do not detach the master device itself. */ if (master->this != dev && master->ops->detach_i3c_dev) master->ops->detach_i3c_dev(dev); +} +EXPORT_SYMBOL_GPL(i3c_master_detach_i3c_dev_controller_locked); + +static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev) +{ + i3c_master_detach_i3c_dev_controller_locked(dev); i3c_master_put_i3c_addrs(dev); list_del(&dev->common.node); @@ -3584,7 +3645,6 @@ static void i3c_master_handle_ibi(struct work_struct *work) struct i3c_ibi_slot *slot = container_of(work, struct i3c_ibi_slot, work); struct i3c_dev_desc *dev = slot->dev; - struct i3c_master_controller *master = i3c_dev_get_master(dev); struct i3c_ibi_payload payload; payload.data = slot->data; @@ -3593,7 +3653,7 @@ static void i3c_master_handle_ibi(struct work_struct *work) if (dev->dev) dev->ibi->handler(dev->dev, &payload); - master->ops->recycle_ibi_slot(dev, slot); + i3c_dev_recycle_ibi_slot_controller(dev, slot); if (atomic_dec_and_test(&dev->ibi->pending_ibis)) complete(&dev->ibi->all_ibis_handled); } @@ -3704,6 +3764,29 @@ i3c_generic_ibi_alloc_pool(struct i3c_dev_desc *dev, } EXPORT_SYMBOL_GPL(i3c_generic_ibi_alloc_pool); +/** + * i3c_dev_recycle_ibi_slot_controller() - Recycle an IBI slot through + * the current controller + * @dev: I3C device descriptor + * @slot: IBI slot to recycle + * + * Invoke the current controller's IBI slot recycling callback. + * + * 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 = i3c_dev_get_master(dev); + + if (master->ops->recycle_ibi_slot) + master->ops->recycle_ibi_slot(dev, slot); +} +EXPORT_SYMBOL_GPL(i3c_dev_recycle_ibi_slot_controller); + /** * i3c_generic_ibi_get_free_slot() - Get a free slot from a generic IBI pool * @pool: the pool to query an IBI slot on @@ -4015,6 +4098,32 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers, } EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked); +/** + * i3c_dev_disable_ibi_controller_locked() - Disable IBI in the controller + * @dev: I3C device descriptor + * + * Invoke the current controller's IBI disable callback without waiting for + * pending IBIs or updating the generic IBI enabled state. + * + * Context: The caller must serialize access to @dev->ibi and the generic + * IBI lifecycle. + * + * Return: 0 on success, or a negative error code. + */ +int i3c_dev_disable_ibi_controller_locked(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *master = i3c_dev_get_master(dev); + + if (!dev->ibi) + return -EINVAL; + + if (!master->ops->disable_ibi) + return -EOPNOTSUPP; + + return master->ops->disable_ibi(dev); +} +EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_controller_locked); + /** * i3c_dev_disable_ibi_locked() - Disable IBIs coming from a specific device * @dev: device on which IBIs should be disabled @@ -4027,14 +4136,9 @@ EXPORT_SYMBOL_GPL(i3c_dev_do_xfers_locked); */ int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev) { - struct i3c_master_controller *master; int ret; - if (!dev->ibi) - return -EINVAL; - - master = i3c_dev_get_master(dev); - ret = master->ops->disable_ibi(dev); + ret = i3c_dev_disable_ibi_controller_locked(dev); if (ret) return ret; @@ -4048,6 +4152,32 @@ int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev) } EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_locked); +/** + * i3c_dev_enable_ibi_controller_locked() - Enable controller IBI resources + * @dev: I3C device descriptor + * + * Invoke the current controller's IBI enable callback without updating the + * generic IBI enabled state. + * + * Context: The caller must serialize access to @dev->ibi and the generic + * IBI lifecycle. + * + * Return: 0 on success, or a negative error code. + */ +int i3c_dev_enable_ibi_controller_locked(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *master = i3c_dev_get_master(dev); + + if (!dev->ibi) + return -EINVAL; + + if (!master->ops->enable_ibi) + return -EOPNOTSUPP; + + return master->ops->enable_ibi(dev); +} +EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_controller_locked); + /** * i3c_dev_enable_ibi_locked() - Enable IBIs from a specific device (lock held) * @dev: device on which IBIs should be enabled @@ -4064,13 +4194,9 @@ EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_locked); */ int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev) { - struct i3c_master_controller *master = i3c_dev_get_master(dev); int ret; - if (!dev->ibi) - return -EINVAL; - - ret = master->ops->enable_ibi(dev); + ret = i3c_dev_enable_ibi_controller_locked(dev); if (!ret) dev->ibi->enabled = true; @@ -4078,6 +4204,35 @@ int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev) } EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_locked); +/** + * i3c_dev_request_ibi_controller_locked() - Request controller IBI resources + * @dev: I3C device descriptor + * @req: IBI setup request + * + * Invoke the current controller's IBI request callback without allocating the + * generic IBI object or workqueue. The caller must ensure that @dev->ibi has + * already been initialized. + * + * Context: The caller must serialize access to @dev->ibi and the generic + * IBI lifecycle. + * + * Return: 0 on success, or a negative error code. + */ +int i3c_dev_request_ibi_controller_locked(struct i3c_dev_desc *dev, + const struct i3c_ibi_setup *req) +{ + struct i3c_master_controller *master = i3c_dev_get_master(dev); + + if (!dev->ibi) + return -EINVAL; + + if (!master->ops->request_ibi) + return -EOPNOTSUPP; + + return master->ops->request_ibi(dev, req); +} +EXPORT_SYMBOL_GPL(i3c_dev_request_ibi_controller_locked); + /** * i3c_dev_request_ibi_locked() - Request an IBI * @dev: device for which we should enable IBIs @@ -4121,8 +4276,15 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev, ibi->num_slots = req->num_slots; dev->ibi = ibi; - ret = master->ops->request_ibi(dev, req); + ret = i3c_dev_request_ibi_controller_locked(dev, req); if (ret) { + /* + * The controller request callback failed, so tear down the + * workqueue allocated above before freeing the IBI object. + * This is the owner of the workqueue, so it must destroy it + * here to avoid leaking it on the error path. + */ + destroy_workqueue(ibi->wq); kfree(ibi); dev->ibi = NULL; } @@ -4131,6 +4293,27 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev, } EXPORT_SYMBOL_GPL(i3c_dev_request_ibi_locked); +/** + * i3c_dev_free_ibi_controller_locked() - Free controller IBI resources + * @dev: I3C device descriptor + * + * Invoke the current controller's IBI free callback without destroying the + * generic IBI workqueue or freeing @dev->ibi. + * + * Context: The caller must serialize access to @dev->ibi and the generic + * IBI lifecycle. + */ +void i3c_dev_free_ibi_controller_locked(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *master = i3c_dev_get_master(dev); + + if (!dev->ibi) + return; + + master->ops->free_ibi(dev); +} +EXPORT_SYMBOL_GPL(i3c_dev_free_ibi_controller_locked); + /** * i3c_dev_free_ibi_locked() - Free all resources needed for IBI handling * @dev: device on which you want to release IBI resources @@ -4161,7 +4344,7 @@ void i3c_dev_free_ibi_locked(struct i3c_dev_desc *dev) dev_err(&master->dev, "Failed to disable IBI before freeing\n"); } - master->ops->free_ibi(dev); + i3c_dev_free_ibi_controller_locked(dev); if (dev->ibi->wq) { destroy_workqueue(dev->ibi->wq); From 58c018414635dcede72f92541db8f84abd4cf980 Mon Sep 17 00:00:00 2001 From: Aman Kumar Pandey Date: Tue, 22 Sep 2026 16:05:45 +0530 Subject: [PATCH 24/28] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support 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 Link: https://patch.msgid.link/20260922103551.2754613-4-lakshay.piplani@nxp.com Signed-off-by: Alexandre Belloni --- .../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/Documentation/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 multiple + peripheral devices on the downstream side. + 2. Have two Controller Ports which can support either + I2C/SMBus or I3C buses and connect to a CPU, BMC or SOC. + 3. P3H2840/ P3H2841 are 8 port I3C hub with eight Target Ports. + 4. P3H2440/ P3H2441 are 4 port I3C hub with four Target Ports. + Target ports can be configured as SMBus or I3C and connect to + peripherals. + + 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 = <3>; + #size-cells = <0>; + + hub@70,236153000c2 { + reg = <0x70 0x236 0x153000c2>; + compatible = "nxp,p3h2441", "nxp,p3h2840"; + #address-cells = <1>; + #size-cells = <0>; + assigned-address = <0x50>; + + nxp,tp0145-pullup-ohms = <1000>; + nxp,tp2367-pullup-ohms = <1000>; + nxp,cp0-io-strength-ohms = <50>; + nxp,cp1-io-strength-ohms = <50>; + nxp,tp0145-io-strength-ohms = <50>; + nxp,tp2367-io-strength-ohms = <50>; + vcc3-supply = <®_tpg0>; + vcc4-supply = <®_tpg1>; + + regulators { + reg_cp0: ldo-cp0 { + regulator-name = "ldo-cp0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_cp1: ldo-cp1 { + regulator-name = "ldo-cp1"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_tpg0: ldo-tpg0 { + regulator-name = "ldo-tpg0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_tpg1: ldo-tpg1 { + regulator-name = "ldo-tpg1"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + }; + + smbus@0 { + reg = <0x0>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + + smbus@1 { + reg = <0x1>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + + i3c@2 { + reg = <0x2>; + #address-cells = <3>; + #size-cells = <0>; + nxp,pullup-enable; + }; + }; + }; + + - | + i2c { + #address-cells = <1>; + #size-cells = <0>; + + hub@70 { + reg = <0x70>; + compatible = "nxp,p3h2840"; + #address-cells = <1>; + #size-cells = <0>; + + nxp,tp0145-pullup-ohms = <1000>; + nxp,tp2367-pullup-ohms = <1000>; + nxp,cp0-io-strength-ohms = <50>; + nxp,cp1-io-strength-ohms = <50>; + nxp,tp0145-io-strength-ohms = <50>; + nxp,tp2367-io-strength-ohms = <50>; + vcc3-supply = <®_tpg0_i2c>; + vcc4-supply = <®_tpg1_i2c>; + + regulators { + reg_cp0_i2c: ldo-cp0 { + regulator-name = "ldo-cp0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_cp1_i2c: ldo-cp1 { + regulator-name = "ldo-cp1"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_tpg0_i2c: ldo-tpg0 { + regulator-name = "ldo-tpg0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_tpg1_i2c: ldo-tpg1 { + regulator-name = "ldo-tpg1"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + }; + + smbus@0 { + reg = <0x0>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + + smbus@1 { + reg = <0x1>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index 3a19da74d00c..203e6e8ae2f9 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19763,6 +19763,15 @@ L: linux-wireless@vger.kernel.org S: Maintained F: drivers/net/wireless/nxp/ +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 From b0cfe49a2e3b984f94f86dd0888b7102f69d98da Mon Sep 17 00:00:00 2001 From: Lakshay Piplani Date: Tue, 22 Sep 2026 16:05:48 +0530 Subject: [PATCH 25/28] i3c: hub: Add support for the I3C interface in the I3C hub Add virtual I3C bus support for the hub and provide interface to enable or disable downstream ports. Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Frank Li Link: https://patch.msgid.link/20260922103551.2754613-7-lakshay.piplani@nxp.com Signed-off-by: Alexandre Belloni --- 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 if I3C source "drivers/i3c/master/Kconfig" + +config I3C_HUB + tristate "I3C Hub Support" + help + Enable support for the I3C interface in hub devices. + + This option adds virtual I3C bus support for hubs by creating + virtual master controllers for downstream ports and forwarding + bus operations through the hub device. It also provides an + interface used by hub drivers to enable or disable downstream + ports during bus transactions. + + Say Y here if your platform includes an I3C hub device + endif # I3C config I3C_OR_I2C diff --git a/drivers/i3c/Makefile b/drivers/i3c/Makefile index 11982efbc6d9..9ddee56a6338 100644 --- a/drivers/i3c/Makefile +++ b/drivers/i3c/Makefile @@ -2,3 +2,4 @@ i3c-y := device.o master.o obj-$(CONFIG_I3C) += i3c.o obj-$(CONFIG_I3C) += master/ +obj-$(CONFIG_I3C_HUB) += hub.o diff --git a/drivers/i3c/hub.c b/drivers/i3c/hub.c new file mode 100644 index 000000000000..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 racing + * concurrent readers on the virtual bus. + */ +struct i3c_hub_dev_data { + struct i3c_dev_desc parent_desc; +}; + +/* + * All i3c_bus rw_semaphores are initialized from a single call site in the + * I3C core, so lockdep assigns them one shared class. When a hub forwards an + * operation it takes the parent bus lock while already holding its own virtual + * bus lock, which lockdep then reports as recursive locking on that shared + * class. The bus maintenance and normal-use helpers use plain down_write() and + * down_read(), which always acquire with subclass 0, so lockdep_set_subclass() + * cannot separate them; a distinct lock_class_key per nesting level is used + * instead. + * + * A top-level hub uses depth 1, a hub behind another hub uses depth 2, and so + * on, so a virtual bus lock never shares a class with the parent bus lock it + * nests under. Sibling ports on the same hub share a class, which is safe + * because they are never nested against each other. The array must stay a + * file-local definition: lockdep keys are identified by their address, so a + * single set of unique objects is required. + * + * The depth bound is generous; exceeding it only loses lockdep coverage, not + * correctness. + */ +#define I3C_HUB_MAX_LOCK_DEPTH 8 +static struct lock_class_key i3c_hub_bus_lock_keys[I3C_HUB_MAX_LOCK_DEPTH]; + +/* + * The hub routing mutex (hub->lock) serializes port switching and forwarding. + * A child hub holds its routing mutex while reaching a parent hub that takes + * its own, so it needs the same per-depth lock_class_key treatment as the bus + * lock above, keyed identically (top-level hub depth 1, and so on). The class + * is assigned once in i3c_hub_init(), not per port, because all ports on a hub + * share this single routing mutex. + */ +static struct lock_class_key i3c_hub_routing_lock_keys[I3C_HUB_MAX_LOCK_DEPTH]; + +/** + * i3c_hub_controller_depth() - Count hub nesting levels above a controller + * @controller: Virtual hub controller being initialized + * + * Walk the parent chain and count how many stacked hub controllers lead to + * @controller. A top-level hub attached to a physical controller returns 1. + * The walk stops at the first non-hub (physical) controller. + * + * Return: The hub nesting depth (>= 1 for a hub controller). + */ +static unsigned int +i3c_hub_controller_depth(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + unsigned int depth = 0; + + while (controller && controller->ops == &i3c_hub_master_ops) { + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller) + break; + controller = hub_controller->parent; + depth++; + } + + return depth; +} + +/** + * i3c_hub_master_bus_init() - Bind controller to hub device + * @controller: Virtual controller for a hub port + * + * Associates the virtual controller with the hub device descriptor so that + * transfers are executed through the hub on the parent bus. + */ +static int i3c_hub_master_bus_init(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + unsigned int depth; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + if (!hub->hub_dev) + return -ENODEV; + + /* + * Give this virtual bus lock a lockdep class keyed on its hub nesting + * depth before the core runs the first DAA (which forwards to the + * parent bus and takes the parent lock while this one is held). The + * lock is not held here, and controller->ops is already set, so the + * class can be assigned safely. Deeper hubs than the key array + * supports fall back to the shared class and may warn under lockdep, + * but still function correctly. + */ + depth = i3c_hub_controller_depth(controller); + if (depth >= 1 && depth <= I3C_HUB_MAX_LOCK_DEPTH) + lockdep_set_class(&controller->bus.lock, + &i3c_hub_bus_lock_keys[depth - 1]); + else + WARN_ONCE(1, "i3c-hub: nesting depth %u exceeds lockdep support\n", + depth); + + controller->this = hub->hub_dev->desc; + return 0; +} + +static void i3c_hub_master_bus_cleanup(struct i3c_master_controller *controller) +{ + controller->this = NULL; +} + +static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(dev); + struct i3c_hub_controller *hub_controller; + struct i3c_hub_dev_data *data; + struct i3c_master_controller *parent; + struct i3c_hub *hub; + int ret; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + if (!hub->hub_dev) + return -ENODEV; + + parent = i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return -ENODEV; + + data = kzalloc_obj(*data); + if (!data) + return -ENOMEM; + + /* Initialize the parent-facing descriptor to target the physical parent. */ + INIT_LIST_HEAD(&data->parent_desc.common.node); + mutex_init(&data->parent_desc.ibi_lock); + + data->parent_desc.common.master = parent; + data->parent_desc.info = dev->info; + + i3c_bus_maintenance_lock(&parent->bus); + ret = i3c_master_attach_i3c_dev_controller_locked(&data->parent_desc); + i3c_bus_maintenance_unlock(&parent->bus); + if (ret) { + mutex_destroy(&data->parent_desc.ibi_lock); + kfree(data); + return ret; + } + + /* Link the hub-private data (see struct i3c_hub_dev_data). */ + i3c_dev_set_master_data(dev, data); + + return 0; +} + +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev, + u8 old_dyn_addr) +{ + struct i3c_hub_dev_data *data = i3c_dev_get_master_data(dev); + struct i3c_master_controller *parent; + int ret; + + if (!data) + return -ENODEV; + + parent = i3c_dev_get_master(&data->parent_desc); + if (!parent) + return -ENODEV; + + /* + * Reattach must not race asynchronous IBI delivery on the parent-facing + * descriptor. Once IBI resources are requested the parent controller may + * use parent_desc concurrently, so reject a reattach that arrives while + * the generic IBI object is still live. + */ + if (WARN_ON_ONCE(data->parent_desc.ibi)) + return -EBUSY; + + /* + * Re-sync device information after the address change and reattach + * under the parent bus lock so both updates are applied as one + * operation with respect to parent controller state. + */ + i3c_bus_maintenance_lock(&parent->bus); + data->parent_desc.info = dev->info; + ret = i3c_master_reattach_i3c_dev_controller_locked(&data->parent_desc, + old_dyn_addr); + i3c_bus_maintenance_unlock(&parent->bus); + + return ret; +} + +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev) +{ + struct i3c_hub_dev_data *data = i3c_dev_get_master_data(dev); + struct i3c_master_controller *parent; + + if (!data) + return; + + parent = i3c_dev_get_master(&data->parent_desc); + + /* + * parent_desc.ibi should already be cleared by i3c_hub_free_ibi() + * before we get here. If it is still set, the kfree(data) below frees + * a descriptor the parent controller can still reach via un-flushed + * asynchronous IBI work (use-after-free, not just a leak). + */ + WARN_ON_ONCE(data->parent_desc.ibi); + + if (parent) { + i3c_bus_maintenance_lock(&parent->bus); + i3c_master_detach_i3c_dev_controller_locked(&data->parent_desc); + i3c_bus_maintenance_unlock(&parent->bus); + } + + i3c_dev_set_master_data(dev, NULL); + mutex_destroy(&data->parent_desc.ibi_lock); + kfree(data); +} + +/** + * i3c_hub_do_daa() - Perform DAA via hub port + * @hub: Hub instance + * @controller: Virtual controller for a hub port + * + * Enables the port connection, performs DAA on the parent controller, + * then disables the connection. + */ +static int i3c_hub_do_daa(struct i3c_hub *hub, + struct i3c_master_controller *controller) +{ + struct i3c_master_controller *parent; + int ret; + + if (!hub || !hub->hub_dev) + return -ENODEV; + + parent = i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return -ENODEV; + + /* + * Lock order: hub routing mutex before the parent bus lock (taken here + * inside i3c_master_do_daa()). The depth-keyed lockdep classes above + * keep this nesting acyclic when the parent is itself a hub. + */ + mutex_lock(&hub->lock); + i3c_hub_enable_port(controller); + + /* + * Downstream devices reachable through hub target-port routes share the + * parent controller's I3C address space. The hub gates access to a + * target-port network, but it does not create an independent dynamic + * address domain per virtual bus. + * + * Run DAA on the parent controller so dynamic addresses remain unique + * across all downstream devices, even when they are behind different + * target ports. + * + * 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 = i3c_master_do_daa(parent); + i3c_hub_disable_port(controller); + mutex_unlock(&hub->lock); + + return ret; +} + +static bool i3c_hub_supports_ccc_cmd(struct i3c_hub *hub, + const struct i3c_ccc_cmd *cmd) +{ + struct i3c_master_controller *parent; + + if (!hub || !hub->hub_dev) + return false; + + parent = i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return false; + + return i3c_master_supports_ccc_cmd(parent, cmd); +} + +/** + * i3c_hub_send_ccc_cmd() - Send CCC through hub port + * @hub: Hub instance + * @controller: Virtual controller + * @cmd: CCC command + * + * Enables the port connection while issuing CCC on the parent controller. + * + * 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 the + * parent controller and therefore reaches every device on the parent bus, as + * well as the downstream devices of any other connected target port. Drivers + * 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 = i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return -ENODEV; + + mutex_lock(&hub->lock); + i3c_hub_enable_port(controller); + ret = i3c_master_send_ccc_cmd(parent, cmd); + i3c_hub_disable_port(controller); + mutex_unlock(&hub->lock); + + return ret; +} + +/** + * i3c_hub_master_priv_xfers() - Execute private transfers via hub + * @dev: Target device descriptor + * @xfers: Transfer array + * @nxfers: Number of transfers + * @mode: Transfer mode (SDR, HDR, etc.) + * + * Refreshes the parent-facing device info (while no IBI is pending) and + * forwards private transfers through the hub to the parent controller. + */ +static int i3c_hub_master_priv_xfers(struct i3c_dev_desc *dev, + struct i3c_xfer *xfers, + int nxfers, + enum i3c_xfer_mode mode) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(dev); + struct i3c_hub_controller *hub_controller; + struct i3c_master_controller *parent; + struct i3c_hub_dev_data *data; + struct i3c_hub *hub; + int ret; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + data = i3c_dev_get_master_data(dev); + if (!data) + return -ENODEV; + + parent = i3c_dev_get_master(&data->parent_desc); + if (!parent) + return -ENODEV; + + /* Lock order: hub routing mutex before the parent bus lock (see do_daa). */ + mutex_lock(&hub->lock); + + /* + * Only refresh the parent-facing info while no IBI is requested; once + * parent_desc.ibi is set it must stay immutable (see i3c_hub_request_ibi()). + */ + if (!data->parent_desc.ibi) + data->parent_desc.info = dev->info; + + i3c_hub_enable_port(controller); + + i3c_bus_normaluse_lock(&parent->bus); + ret = i3c_dev_do_xfers_locked(&data->parent_desc, xfers, + nxfers, mode); + i3c_bus_normaluse_unlock(&parent->bus); + + i3c_hub_disable_port(controller); + + mutex_unlock(&hub->lock); + + return ret; +} + +static int i3c_hub_attach_i2c_dev(struct i2c_dev_desc *dev) +{ + return -EOPNOTSUPP; +} + +static void i3c_hub_detach_i2c_dev(struct i2c_dev_desc *dev) +{ +} + +static int i3c_hub_i2c_xfers(struct i2c_dev_desc *dev, + struct i2c_msg *xfers, int nxfers) +{ + return -EOPNOTSUPP; +} + +static int i3c_hub_master_do_daa(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + return i3c_hub_do_daa(hub, controller); +} + +static int i3c_hub_master_send_ccc_cmd(struct i3c_master_controller *controller, + struct i3c_ccc_cmd *cmd) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + if (!hub->hub_dev) + return -ENODEV; + + /* + * Do not forward broadcast RSTDAA through the hub. The hub itself + * is visible on the parent bus, so forwarding RSTDAA would also + * reset the hub dynamic address. Downstream RSTDAA is not supported + * by the hub virtual-controller model. + */ + if (cmd->id == I3C_CCC_RSTDAA(true)) + return 0; + + return i3c_hub_send_ccc_cmd(hub, controller, cmd); +} + +static bool i3c_hub_master_supports_ccc_cmd(struct i3c_master_controller *controller, + const struct i3c_ccc_cmd *cmd) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return false; + + hub = hub_controller->hub; + + return i3c_hub_supports_ccc_cmd(hub, cmd); +} + +/** + * i3c_hub_request_ibi() - Request IBI through parent controller + * @desc: Target device descriptor + * @req: IBI setup + * + * Publishes the generic IBI object on the permanent parent-facing descriptor + * and requests IBI for a device connected through the hub. The parent-facing + * descriptor references the same IBI object so the physical controller uses + * the logical workqueue, pending counter and client device during + * asynchronous IBI delivery. + */ +static int i3c_hub_request_ibi(struct i3c_dev_desc *desc, + const struct i3c_ibi_setup *req) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(desc); + struct i3c_hub_controller *hub_controller; + struct i3c_master_controller *parent; + struct i3c_hub_dev_data *data; + struct i3c_hub *hub; + int ret; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + data = i3c_dev_get_master_data(desc); + if (!data) + return -ENODEV; + + parent = i3c_dev_get_master(&data->parent_desc); + if (!parent) + return -ENODEV; + + /* + * Publish the final device information snapshot together with the + * generic IBI object under hub->lock. Keep the parent-facing + * information immutable while parent_desc.ibi is set and the parent + * controller may use the descriptor asynchronously. + */ + mutex_lock(&hub->lock); + data->parent_desc.info = desc->info; + data->parent_desc.dev = desc->dev; + data->parent_desc.ibi = desc->ibi; + mutex_unlock(&hub->lock); + + i3c_bus_normaluse_lock(&parent->bus); + ret = i3c_dev_request_ibi_controller_locked(&data->parent_desc, req); + i3c_bus_normaluse_unlock(&parent->bus); + + if (ret) { + mutex_lock(&hub->lock); + data->parent_desc.ibi = NULL; + data->parent_desc.dev = NULL; + mutex_unlock(&hub->lock); + } + + return ret; +} + +static void i3c_hub_free_ibi(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(desc); + struct i3c_hub_controller *hub_controller; + struct i3c_master_controller *parent; + struct i3c_hub_dev_data *data; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub = hub_controller->hub; + + data = i3c_dev_get_master_data(desc); + if (!data || !data->parent_desc.ibi) + return; + + parent = i3c_dev_get_master(&data->parent_desc); + if (!parent) + return; + + i3c_bus_normaluse_lock(&parent->bus); + i3c_dev_free_ibi_controller_locked(&data->parent_desc); + i3c_bus_normaluse_unlock(&parent->bus); + + /* + * The outer generic IBI free path owns and releases desc->ibi after + * this callback returns. + */ + mutex_lock(&hub->lock); + data->parent_desc.ibi = NULL; + data->parent_desc.dev = NULL; + mutex_unlock(&hub->lock); +} + +/** + * i3c_hub_enable_ibi() - Enable IBI via hub port + * @desc: Target device descriptor + * + * Enables port connection and forwards the IBI enable request to the parent + * controller. + */ +static int i3c_hub_enable_ibi(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(desc); + struct i3c_hub_controller *hub_controller; + struct i3c_master_controller *parent; + struct i3c_hub_dev_data *data; + struct i3c_hub *hub; + int ret; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + data = i3c_dev_get_master_data(desc); + if (!data || !data->parent_desc.ibi) + return -ENODEV; + + parent = i3c_dev_get_master(&data->parent_desc); + if (!parent) + return -ENODEV; + + mutex_lock(&hub->lock); + + i3c_hub_enable_port(controller); + + i3c_bus_maintenance_lock(&parent->bus); + ret = i3c_dev_enable_ibi_controller_locked(&data->parent_desc); + i3c_bus_maintenance_unlock(&parent->bus); + + i3c_hub_disable_port(controller); + + mutex_unlock(&hub->lock); + + return ret; +} + +/** + * i3c_hub_disable_ibi() - Disable IBI via hub port + * @desc: Target device descriptor + * + * Enables port connection and forwards the IBI disable request to the parent + * controller. + */ +static int i3c_hub_disable_ibi(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(desc); + struct i3c_hub_controller *hub_controller; + struct i3c_master_controller *parent; + struct i3c_hub_dev_data *data; + struct i3c_hub *hub; + int ret; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + data = i3c_dev_get_master_data(desc); + if (!data || !data->parent_desc.ibi) + return -ENODEV; + + parent = i3c_dev_get_master(&data->parent_desc); + if (!parent) + return -ENODEV; + + mutex_lock(&hub->lock); + + i3c_hub_enable_port(controller); + + i3c_bus_maintenance_lock(&parent->bus); + ret = i3c_dev_disable_ibi_controller_locked(&data->parent_desc); + i3c_bus_maintenance_unlock(&parent->bus); + + i3c_hub_disable_port(controller); + + mutex_unlock(&hub->lock); + + return ret; +} + +static void i3c_hub_recycle_ibi_slot(struct i3c_dev_desc *desc, + struct i3c_ibi_slot *slot) +{ + struct i3c_hub_dev_data *data = i3c_dev_get_master_data(desc); + + if (!data) + return; + + i3c_dev_recycle_ibi_slot_controller(&data->parent_desc, slot); +} + +const struct i3c_master_controller_ops i3c_hub_master_ops = { + .bus_init = i3c_hub_master_bus_init, + .bus_cleanup = i3c_hub_master_bus_cleanup, + .attach_i3c_dev = i3c_hub_attach_i3c_dev, + .reattach_i3c_dev = i3c_hub_reattach_i3c_dev, + .detach_i3c_dev = i3c_hub_detach_i3c_dev, + .do_daa = i3c_hub_master_do_daa, + .supports_ccc_cmd = i3c_hub_master_supports_ccc_cmd, + .send_ccc_cmd = i3c_hub_master_send_ccc_cmd, + .i3c_xfers = i3c_hub_master_priv_xfers, + .attach_i2c_dev = i3c_hub_attach_i2c_dev, + .detach_i2c_dev = i3c_hub_detach_i2c_dev, + .i2c_xfers = i3c_hub_i2c_xfers, + .request_ibi = i3c_hub_request_ibi, + .free_ibi = i3c_hub_free_ibi, + .enable_ibi = i3c_hub_enable_ibi, + .disable_ibi = i3c_hub_disable_ibi, + .recycle_ibi_slot = i3c_hub_recycle_ibi_slot, +}; +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 = i3c_dev_get_master(hub->hub_dev->desc); + depth = i3c_hub_controller_depth(parent) + 1; + if (WARN_ONCE(depth > I3C_HUB_MAX_LOCK_DEPTH, + "i3c-hub: routing lock depth %u exceeds lockdep support\n", + depth)) + depth = I3C_HUB_MAX_LOCK_DEPTH; + + lockdep_set_class(&hub->lock, &i3c_hub_routing_lock_keys[depth - 1]); +} + +/** + * 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 = ops; + hub->hub_dev = 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 each + * downstream I3C device described in the Device Tree, so that parent DAA does + * not hand the same address to another device. Downstream devices behind hub + * target ports share the parent controller's dynamic address space. + * + * 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 same + * Device Tree and reserves the same addresses, so repeated probing does not + * consume additional address space. + * + * Only the assigned dynamic addresses are reserved here. A hub that supports + * hot-joining downstream devices would additionally have to reserve the static + * addresses of every device that may appear. Supporting runtime hub hotplug + * would also require tracking ownership of these reservations and releasing + * 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 *hubc, + struct device_node *node) +{ + struct i3c_master_controller *parent = hubc->parent; + enum i3c_addr_slot_status status; + u32 assigned_addr; + u32 reg[3]; + int ret; + + if (!parent || !node) + return -ENODEV; + + for_each_available_child_of_node_scoped(node, child) { + /* + * Only consider addressable bus nodes: a valid "reg" is + * required to describe a device, but its static-address value + * does not affect the reservation below. + */ + ret = of_property_read_variable_u32_array(child, "reg", reg, 1, 3); + if (ret < 0) + continue; + + ret = of_property_read_u32(child, "assigned-address", &assigned_addr); + if (ret) + continue; + + /* Skip nodes without a usable dynamic address. */ + if (!assigned_addr || assigned_addr > I3C_MAX_ADDR) + continue; + + i3c_bus_maintenance_lock(&parent->bus); + status = i3c_bus_get_addr_slot_status(&parent->bus, + assigned_addr); + if (status == I3C_ADDR_SLOT_FREE) + i3c_bus_set_addr_slot_status(&parent->bus, + assigned_addr, + I3C_ADDR_SLOT_I3C_DEV); + i3c_bus_maintenance_unlock(&parent->bus); + } + return 0; +} +EXPORT_SYMBOL_GPL(i3c_hub_reserve_parent_addrslots_from_dt); + +MODULE_AUTHOR("Aman Kumar Pandey "); +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 keyed + * per hub nesting depth in i3c_hub_init(). + */ +struct i3c_hub { + const struct i3c_hub_ops *ops; + struct i3c_device *hub_dev; + struct mutex lock; /* Serializes hub port routing. */ +}; + +struct i3c_hub_controller { + struct i3c_master_controller *parent; + struct i3c_master_controller controller; + struct i3c_hub *hub; +}; + +struct i3c_hub_ops { + void (*enable_port)(struct i3c_master_controller *controller); + void (*disable_port)(struct i3c_master_controller *controller); +}; + +/** + * i3c_hub_enable_port() - Enable hub connection for a controller + * @controller: Virtual controller representing a hub port + * + * Retrieves hub context from controller drvdata and invokes the vendor + * callback to enable the associated port connection. + */ +static inline void i3c_hub_enable_port(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub = hub_controller->hub; + + if (hub && hub->ops && hub->ops->enable_port) + hub->ops->enable_port(controller); +} + +/** + * i3c_hub_disable_port() - Disable hub connection for a controller + * @controller: Virtual controller representing a hub port + * + * Retrieves hub context from controller drvdata and invokes the vendor + * callback to disable the associated port connection. + */ +static inline void i3c_hub_disable_port(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub = hub_controller->hub; + + if (hub && hub->ops && hub->ops->disable_port) + hub->ops->disable_port(controller); +} + +/* + * 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 *hubc, + struct device_node *node); +#endif From 6931331f48dc04767a70c4d2dd05af618c46ac64 Mon Sep 17 00:00:00 2001 From: Jisheng Zhang Date: Mon, 5 Oct 2026 23:24:23 +0800 Subject: [PATCH 26/28] dt-bindings: i3c: dw: support up to two reset lines The Synopsys DesignWare I3C master controller can have multiple reset lines, such as core reset, apb reset, and dma reset. Update the DT binding documentation to increase maxItems for the resets property to 2. Signed-off-by: Jisheng Zhang Acked-by: Conor Dooley Link: https://patch.msgid.link/20261005152425.25319-2-jszhang@kernel.org Signed-off-by: Alexandre Belloni --- .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index dc7ee38001d6..6da7d90caa3e 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -12,6 +12,16 @@ maintainers: allOf: - $ref: i3c.yaml# + - if: + properties: + compatible: + contains: + const: altr,agilex5-dw-i3c-master + then: + properties: + resets: + maxItems: 1 + properties: compatible: oneOf: @@ -39,7 +49,7 @@ properties: maxItems: 1 resets: - maxItems: 1 + maxItems: 2 power-domains: maxItems: 1 From 224e3606711df98c9f141d7852d8e5c92647a52c Mon Sep 17 00:00:00 2001 From: Jisheng Zhang Date: Mon, 5 Oct 2026 23:24:24 +0800 Subject: [PATCH 27/28] i3c: dw: switch to array-based exclusive reset control The Synopsys DesignWare I3C master controller can have multiple reset lines, such as a core reset and APB reset. Refactor the driver to use devm_reset_control_array_get_exclusive() instead of a single named reset handle. This automatically manages all resets allocated to the device node sequentially. Signed-off-by: Jisheng Zhang Reviewed-by: Frank Li Link: https://patch.msgid.link/20261005152425.25319-3-jszhang@kernel.org Signed-off-by: Alexandre Belloni --- drivers/i3c/master/dw-i3c-master.c | 24 ++++++++++++++++++------ drivers/i3c/master/dw-i3c-master.h | 2 +- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index 05584ddaf104..d1119f203d1b 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1657,6 +1657,11 @@ static const struct dw_i3c_platform_ops dw_i3c_platform_ops_default = { .set_dat_ibi = dw_i3c_platform_set_dat_ibi_nop, }; +static void dw_i3c_resets_assert(void *data) +{ + reset_control_assert(data); +} + int dw_i3c_common_probe(struct dw_i3c_master *master, struct platform_device *pdev) { @@ -1696,10 +1701,17 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, if (IS_ERR(master->pclk)) return PTR_ERR(master->pclk); - master->core_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, - NULL); - if (IS_ERR(master->core_rst)) - return PTR_ERR(master->core_rst); + master->resets = devm_reset_control_array_get_optional_exclusive(&pdev->dev); + if (IS_ERR(master->resets)) + return PTR_ERR(master->resets); + + ret = reset_control_deassert(master->resets); + if (ret) + return ret; + + ret = devm_add_action_or_reset(&pdev->dev, dw_i3c_resets_assert, master->resets); + if (ret) + return ret; spin_lock_init(&master->xferqueue.lock); INIT_LIST_HEAD(&master->xferqueue.list); @@ -1873,7 +1885,7 @@ static int __maybe_unused dw_i3c_master_runtime_suspend(struct device *dev) dw_i3c_master_disable(master); - reset_control_assert(master->core_rst); + reset_control_assert(master->resets); dw_i3c_master_disable_clks(master); pinctrl_pm_select_sleep_state(dev); return 0; @@ -1885,7 +1897,7 @@ static int __maybe_unused dw_i3c_master_runtime_resume(struct device *dev) pinctrl_pm_select_default_state(dev); dw_i3c_master_enable_clks(master); - reset_control_deassert(master->core_rst); + reset_control_deassert(master->resets); dw_i3c_master_set_intr_regs(master); dw_i3c_master_restore_timing_regs(master); diff --git a/drivers/i3c/master/dw-i3c-master.h b/drivers/i3c/master/dw-i3c-master.h index 17ad817d1f8e..f5b4491b247a 100644 --- a/drivers/i3c/master/dw-i3c-master.h +++ b/drivers/i3c/master/dw-i3c-master.h @@ -36,7 +36,7 @@ struct dw_i3c_master { } xferqueue; struct dw_i3c_master_caps caps; void __iomem *regs; - struct reset_control *core_rst; + struct reset_control *resets; struct clk *core_clk; struct clk *pclk; char version[5]; From fc5eb1b0631aa3b34c530ae962d01699ed22a0ae Mon Sep 17 00:00:00 2001 From: Jisheng Zhang Date: Mon, 5 Oct 2026 23:24:25 +0800 Subject: [PATCH 28/28] dt-bindings: i3c: Add Synaptics sl2610 i3c Add the compatible for the Synaptics SL2610 SoC I3C controller, which is compatible with the Synopsys DesignWare I3C. Signed-off-by: Jisheng Zhang Acked-by: Conor Dooley Link: https://patch.msgid.link/20261005152425.25319-4-jszhang@kernel.org Signed-off-by: Alexandre Belloni --- Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index 6da7d90caa3e..6806a22114f9 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -27,7 +27,9 @@ properties: oneOf: - const: snps,dw-i3c-master-1.00a - items: - - const: altr,agilex5-dw-i3c-master + - enum: + - altr,agilex5-dw-i3c-master + - syna,sl2610-i3c - const: snps,dw-i3c-master-1.00a reg: