From krzk at kernel.org Fri May 1 02:37:01 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Fri, 1 May 2026 11:37:01 +0200 Subject: [PATCH 1/2] dt-bindings: i3c: add binding for Realtek RTS490x I3C HUB In-Reply-To: <20260430121354.6253-1-zain_zhou@realsil.com.cn> References: <20260430121354.6253-1-zain_zhou@realsil.com.cn> Message-ID: On 30/04/2026 14:13, zain_zhou at realsil.com.cn wrote: > From: zain_zhou > > Add DT binding schema for Realtek RTS490x series I3C HUB devices. A nit, subject: drop second/last, redundant "bindings". The "dt-bindings" prefix is already stating that these are bindings. See also: https://elixir.bootlin.com/linux/v6.17-rc3/source/Documentation/devicetree/bindings/submitting-patches.rst#L18 > > The binding describes configuration properties for: > - LDO enable/disable and voltage level per port group > - Pull-up resistance per port group > - IO driver strength per port > - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up, > IO mode, SMBus clock frequency and polling interval > - Hub network always-I3C mode > - Hardware identification via CSEL pin (id) and CP1 pins (id-cp1) > > Signed-off-by: zain_zhou Don't use login name as full name, but rather latin transliteration. Name in your native language would also be fine. But login name is not fine. > --- > .../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 410 ++++++++++++++++++ > MAINTAINERS | 6 + > 2 files changed, 416 insertions(+) > create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > > diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > new file mode 100644 > index 000000000000..30295eefee89 > --- /dev/null > +++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > @@ -0,0 +1,410 @@ > +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) > +%YAML 1.2 > +--- > +$id: http://devicetree.org/schemas/i3c/realtek,rts490x-i3c-hub.yaml# > +$schema: http://devicetree.org/meta-schemas/core.yaml# > + > +title: I3C HUB Proper name here of your device > + > +maintainers: > + - zain_zhou > + > +description: | > + I3C HUB is smart device which provides multiple functionality: Are you describing particular device or I3C HUB? > + * enabling voltage compatibility across I3C Controller and Target devices, > + * bus capacitance isolation > + * address conflict isolation > + * I3C port expansion > + * two controllers in a single I3C bus > + * I3C and SMBus device compatibility > + * GPIO expansion > + > + Having such big number of features, there is a need to have some DT knobs to tell the I3C HUB > + driver which features shall be enabled and how they shall be configured. I3C HUB driver read, Please wrap code according to the preferred limit expressed in Kernel coding style (checkpatch is not a coding style description, but only a tool). However don't wrap blindly (see Kernel coding style). > + validate DT knobs and set corresponding registers with the right way to satisfy user requests from > + DT. Irrelevant. This is binding, so describe hardware. Not your drivers. DT is irrelevant as well. > + > + All the DT properties for I3C HUB are located under dedicated (for I3C HUB) DT entry. I3C HUB DT > + entry structure is aligned with regular I3C device DT entry described in i3c.yaml. > + Irrelevant. Describe your hardware. > +allOf: > + - $ref: i3c.yaml# > + > +properties: > + $nodename: > + pattern: "^hub at 0,0$" > + > + cp0-ldo-en: > + enum: > + - disabled > + - enabled Sorry, but what? Your binding does not look like accepted bindings AT ALL. Please read writing bindings carefully or go via DTS101 slides. > + description: | > + I3C HUB Controller Port 0 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + cp1-ldo-en: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Controller Port 1 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + tp0145-ldo-en: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Target Ports 0/1/4/5 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + tp2367-ldo-en: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Target Ports 2/3/6/7 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + cp0-ldo-volt: Nope. There are no such properties. Missing vendor prefix, missing proper unit suffix. Look at other bindings. > + enum: > + - 1.0V > + - 1.1V > + - 1.2V > + - 1.8V > + description: | > + I3C HUB Controller Port 0 LDO setting to control the Controller Port 1 voltage level. This > + property is optional. > + ... > + > +additionalProperties: true No, you cannot have it. Please read writing bindings, writing schema or open ANY other binding. > + > +examples: > + - | > + i3c-master at d040000 { > + #address-cells = <1>; > + #size-cells = <0>; > + > + hub at 0,0 { > + cp0-ldo-en = "disabled"; > + cp1-ldo-en = "enabled"; > + cp0-ldo-volt = "1.0V"; > + cp1-ldo-volt = "1.1V"; > + tp0145-ldo-en = "enabled"; > + tp2367-ldo-en = "disabled"; > + tp0145-ldo-volt = "1.2V"; > + tp2367-ldo-volt = "1.8V"; > + tp0145-pullup = "2k"; > + tp2367-pullup = "500R"; > + tp0145-io-strength = "50Ohms"; > + tp2367-io-strength = "30Ohms"; > + cp0-io-strength = "20Ohms"; > + cp1-io-strength = "40Ohms"; > + > + target-port at 0 { > + mode = "i3c"; > + pullup = "enabled"; > + always_enable; > + }; > + target-port at 1 { > + mode = "smbus"; > + pullup = "enabled"; > + clock-frequency = <1000000>; > + polling-interval-ms = <10>; > + backend at 10{ > + compatible = "i2c-slave-mqueue"; > + reg = <(0x10 | I2C_OWN_SLAVE_ADDRESS)>; > + }; > + }; > + target-port at 2 { > + mode = "gpio"; > + pullup = "disabled"; > + }; > + target-port at 3 { > + mode = "disabled"; > + pullup = "disabled"; > + }; > + }; > + }; > + > + - | > + i3c-master at d040000 { > + #address-cells = <1>; > + #size-cells = <0>; > + > + hub at 70,3C000000100 { No way it passes validation. You just said it CANNOT be anything else than hub at 0,0 > + reg = <0x70 0x3C0 0x00000100>; Lower case hex. Anyway, one example is enough. > + assigned-address = <0x70>; > + dcr = <0xC2>; > + > + cp0-ldo-en = "disabled"; > + cp1-ldo-en = "enabled"; > + cp0-ldo-volt = "1.0V"; > + cp1-ldo-volt = "1.1V"; > + tp0145-ldo-en = "enabled"; > + tp2367-ldo-en = "disabled"; > + tp0145-ldo-volt = "1.2V"; > + tp2367-ldo-volt = "1.8V"; > + tp0145-pullup = "2k"; > + tp2367-pullup = "500R"; > + tp0145-io-strength = "50Ohms"; > + tp2367-io-strength = "30Ohms"; > + cp0-io-strength = "20Ohms"; > + cp1-io-strength = "40Ohms"; > + > + target-port at 0 { > + mode = "i3c"; > + pullup = "enabled"; > + always-enable; > + }; > + target-port at 1 { > + mode = "smbus"; > + pullup = "enabled"; > + backend at 12{ > + compatible = "i2c-slave-mqueue"; > + reg = <(0x12 | I2C_OWN_SLAVE_ADDRESS)>; > + }; > + }; > + target-port at 2 { > + mode = "gpio"; > + pullup = "disabled"; > + }; > + target-port at 3 { > + mode = "disabled"; > + pullup = "disabled"; > + }; > + }; > + }; > diff --git a/MAINTAINERS b/MAINTAINERS > index 2fb1c75afd16..71ee5071ac0f 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -12214,6 +12214,12 @@ S: Supported > F: Documentation/devicetree/bindings/i3c/renesas,i3c.yaml > F: drivers/i3c/master/renesas-i3c.c > > +I3C HUB DRIVER FOR REALTEK RTS490X > +M: zain_zhou > +S: Maintained > +F: Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > +F: drivers/staging/rts490x/ There is no such directory. Best regards, Krzysztof From adrian.hunter at intel.com Mon May 4 04:33:37 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:37 +0300 Subject: [PATCH V3 01/16] i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-2-adrian.hunter@intel.com> Software reset was introduced as a fallback if bus disable failed. The change was made in 2 places: the cleanup path and the suspend path. For the cleanup path (i3c_hci_bus_cleanup()), after software reset the function continues to do cleanup for the current I/O mode. For the suspend path (i3c_hci_rpm_suspend()), after software reset the function returns early. However software reset does not reset any Ring Headers in the Host Controller, so returning early is not the right thing to do. Instead, continue to call suspend for the current I/O mode, which for DMA mode will reset any Ring Headers. Note, although Ring Headers should not be active at this stage, performing this reset follows the procedure defined by the specification and keeps the suspend path consistent with the cleanup path. Note also, i3c_hci_sync_irq_inactive() is still called via the PIO and DMA hci->io->suspend() callbacks. Always return 0 because the device is quiesced as much as possible and returning a negative error code would unnecessarily prevent system suspend. Fixes: 9a258d1336f7 ("i3c: mipi-i3c-hci: Fallback to software reset when bus disable fails") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V3: Add Frank's rev'd-by Changes in V2: Always return 0 from suspend callback Amend commit message drivers/i3c/master/mipi-i3c-hci/core.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index b781dbed2165..afb0764b5e1f 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -762,15 +762,10 @@ static int i3c_hci_reset_and_init(struct i3c_hci *hci) int i3c_hci_rpm_suspend(struct device *dev) { struct i3c_hci *hci = dev_get_drvdata(dev); - int ret; - ret = i3c_hci_bus_disable(hci); - if (ret) { - /* Fall back to software reset to disable the bus */ - ret = i3c_hci_software_reset(hci); - i3c_hci_sync_irq_inactive(hci); - return ret; - } + /* Fall back to software reset to disable the bus */ + if (i3c_hci_bus_disable(hci)) + i3c_hci_software_reset(hci); hci->io->suspend(hci); -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:36 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:36 +0300 Subject: [PATCH V3 00/16] i3c: mipi-i3c-hci: DMA abort, recovery and related improvements Message-ID: <20260504113352.38490-1-adrian.hunter@intel.com> Hi This series improves the robustness of the MIPI I3C HCI DMA mode driver, addressing issues observed during error handling and recovery. Patch 1 ensures suspend always invokes io->suspend. Patches 2-4 fix issues in the existing DMA abort path: preserving the RUN bit during abort per the MIPI specification, blocking enqueue during abort/error, and waiting for ring restart completion. Patches 5-8 improve how partially completed transfer lists are handled during dequeue: moving hci_dma_xfer_done() earlier so completed responses are processed before NoOp replacement, completing transfer lists immediately on error rather than deferring, and detecting when an abort races with transfer completion to avoid restarting the wrong transfer list. Patches 9-10 add Intel-specific quirks for DMA ring abort: a PIO queue reset after abort, and an HC_CONTROL ABORT before the ring-level abort. Patch 11 factors out a reset-and-restore helper from the suspend path for reuse. Patch 12 adds a full DMA recovery path for internal controller errors. When the hardware reports a TID mismatch or the ring becomes stuck, the driver now resets and restores the controller, terminating all in-flight transfers with an error status. Patch 13 makes NoOp command handling observable: instead of discarding NoOp responses, the driver now waits for them to complete and triggers recovery if they fail. Patch 14 adjusts transfer timeout accounting to start from when a transfer actually begins execution rather than when it was queued, preventing premature timeouts behind slow predecessors. Patches 15-16 are minor optimizations: consolidating the DMA command and response ring into a single coherent allocation, and increasing the ring size to the maximum 255 entries to avoid ring-space exhaustion. Changes in V3: i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring Add Frank's rev'd-by i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors When erroring out transfers, ensure the final transfer of a transfer list is processed last Changes in V2: i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset Always return 0 from suspend callback Amend commit message i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring Improve commit message i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error Improve commit message i3c: mipi-i3c-hci: Wait for DMA ring restart to complete None i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition Add Frank's Rev'd-by i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path Add Frank's Rev'd-by i3c: mipi-i3c-hci: Complete transfer lists immediately on error Rename completing_xfer to final_xfer i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer Rename completing_xfer to final_xfer i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers None i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers None i3c: mipi-i3c-hci: Factor out reset-and-restore helper Drop redundant i3c_hci_sync_irq_inactive(hci) from i3c_hci_reset_and_restore() because it is called by hci->io->suspend() anyway i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors Rename completing_xfer to final_xfer Add hci_dma_xfer_done() before checking for an already complete transfer Improve commit message i3c: mipi-i3c-hci: Wait for NoOp commands to complete Rename completing_xfer to final_xfer Add missing reinit_completion() i3c: mipi-i3c-hci: Base timeouts on actual transfer start time Do not flag the next transfer as started when there is an error which halts the controller Instead flag it started at the end of hci_dma_dequeue_xfer() Use hci_start_xfer() in pio.c i3c: mipi-i3c-hci: Consolidate DMA ring allocation Check for failed allocation before assignments to avoid doing arithmetic with NULL pointers i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum None Adrian Hunter (16): i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error i3c: mipi-i3c-hci: Wait for DMA ring restart to complete i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path i3c: mipi-i3c-hci: Complete transfer lists immediately on error i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers i3c: mipi-i3c-hci: Factor out reset-and-restore helper i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors i3c: mipi-i3c-hci: Wait for NoOp commands to complete i3c: mipi-i3c-hci: Base timeouts on actual transfer start time i3c: mipi-i3c-hci: Consolidate DMA ring allocation i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum drivers/i3c/master/mipi-i3c-hci/cmd.h | 6 + drivers/i3c/master/mipi-i3c-hci/core.c | 82 ++++++-- drivers/i3c/master/mipi-i3c-hci/dma.c | 344 +++++++++++++++++++++++++-------- drivers/i3c/master/mipi-i3c-hci/hci.h | 22 +++ drivers/i3c/master/mipi-i3c-hci/pio.c | 1 + 5 files changed, 365 insertions(+), 90 deletions(-) Regards Adrian From adrian.hunter at intel.com Mon May 4 04:33:38 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:38 +0300 Subject: [PATCH V3 02/16] i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-3-adrian.hunter@intel.com> The MIPI I3C HCI specification does not require the DMA ring RUN bit (RUN_STOP) to be cleared when issuing an ABORT. That allows the DMA ring to continue to receive IBIs, although an IBI is anyway not lost because it can be received once the ring restarts if the I3C device has not given up. Note, currently ABORT is only used on a timeout error path so the change has very little effect in practice. In the more common case of a transfer error, the ring (bundle) operation is halted by the controller anyway. Adjust the RING_CONTROL handling to set ABORT without clearing RUN_STOP, bringing the driver into alignment with the specification. Fixes: b795e68bf3073 ("i3c: mipi-i3c-hci: Correct RING_CTRL_ABORT handling in DMA dequeue") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V3: Add Frank's rev'd-by Changes in V2: Improve commit message 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 e487ef52f6b4..4cd32e3afa7b 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -554,7 +554,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, if (ring_status & RING_STATUS_RUNNING) { /* stop the ring */ reinit_completion(&rh->op_done); - rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_ABORT); + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); wait_for_completion_timeout(&rh->op_done, HZ); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:39 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:39 +0300 Subject: [PATCH V3 03/16] i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-4-adrian.hunter@intel.com> Block the DMA enqueue path while a Ring abort is in progress or after an error condition has been detected. Previously, new transfers could be enqueued while the DMA Ring was being aborted or while error handling was underway. This allowed enqueue and error-recovery paths to run concurrently, potentially interfering with each other and corrupting Ring state. Introduce explicit enqueue blocking and a wait queue to serialize access: enqueue operations now wait until abort or error handling has completed before proceeding. Enqueue is unblocked once the Ring is safely restarted. Note, there is only 1 ring bundle configured, and a transfer error causes the controller to halt ring (bundle) operation, so there is only ever 1 outstanding error at a time. Furthermore, a later patch ensures that only the currently active transfer list can time out. Consequently, the DMA queue will not be unblocked while there are outstanding transfer errors or timeouts. Signed-off-by: Adrian Hunter --- Changes in V3: None Changes in V2: Improve commit message drivers/i3c/master/mipi-i3c-hci/core.c | 1 + drivers/i3c/master/mipi-i3c-hci/dma.c | 25 +++++++++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index afb0764b5e1f..44617eb3a3f1 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -973,6 +973,7 @@ static int i3c_hci_probe(struct platform_device *pdev) spin_lock_init(&hci->lock); mutex_init(&hci->control_mutex); + init_waitqueue_head(&hci->enqueue_wait_queue); /* * Multi-bus instances share the same MMIO address range, but not diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 4cd32e3afa7b..314635e6e190 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -484,6 +484,12 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, spin_lock_irq(&hci->lock); + while (unlikely(hci->enqueue_blocked)) { + spin_unlock_irq(&hci->lock); + wait_event(hci->enqueue_wait_queue, !READ_ONCE(hci->enqueue_blocked)); + spin_lock_irq(&hci->lock); + } + if (n > rh->xfer_space) { spin_unlock_irq(&hci->lock); hci_dma_unmap_xfer(hci, xfer_list, n); @@ -539,6 +545,14 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, return 0; } +static void hci_dma_unblock_enqueue(struct i3c_hci *hci) +{ + if (hci->enqueue_blocked) { + hci->enqueue_blocked = false; + wake_up_all(&hci->enqueue_wait_queue); + } +} + static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n) { @@ -550,12 +564,17 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, guard(mutex)(&hci->control_mutex); + spin_lock_irq(&hci->lock); + ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { + hci->enqueue_blocked = true; + spin_unlock_irq(&hci->lock); /* stop the ring */ reinit_completion(&rh->op_done); rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); wait_for_completion_timeout(&rh->op_done, HZ); + spin_lock_irq(&hci->lock); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { /* @@ -567,8 +586,6 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } - spin_lock_irq(&hci->lock); - for (i = 0; i < n; i++) { struct hci_xfer *xfer = xfer_list + i; int idx = xfer->ring_entry; @@ -604,6 +621,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); + hci_dma_unblock_enqueue(hci); + spin_unlock_irq(&hci->lock); return did_unqueue; @@ -647,6 +666,8 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) } if (xfer->completion) complete(xfer->completion); + if (RESP_STATUS(resp)) + hci->enqueue_blocked = true; } done_ptr = (done_ptr + 1) % rh->xfer_entries; diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index f17f43494c1b..d630400ec945 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -54,6 +54,8 @@ struct i3c_hci { struct mutex control_mutex; atomic_t next_cmd_tid; bool irq_inactive; + bool enqueue_blocked; + wait_queue_head_t enqueue_wait_queue; u32 caps; unsigned int quirks; unsigned int DAT_entries; -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:40 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:40 +0300 Subject: [PATCH V3 04/16] i3c: mipi-i3c-hci: Wait for DMA ring restart to complete In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-5-adrian.hunter@intel.com> Although hci_dma_dequeue_xfer() is serialized against itself via control_mutex, this does not guarantee that a DMA ring restart triggered by a previous invocation has fully completed. When the function is called again in rapid succession, the DMA ring may still be transitioning back to the running state, which may confound or disrupt further state changes. Address this by waiting for the DMA ring restart to complete before continuing. Signed-off-by: Adrian Hunter --- Changes in V2 and V3: None drivers/i3c/master/mipi-i3c-hci/dma.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 314635e6e190..28614fdbf558 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -617,6 +617,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } /* restart the ring */ + reinit_completion(&rh->op_done); mipi_i3c_hci_resume(hci); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); @@ -625,6 +626,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, spin_unlock_irq(&hci->lock); + wait_for_completion_timeout(&rh->op_done, HZ); + return did_unqueue; } -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:41 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:41 +0300 Subject: [PATCH V3 05/16] i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-6-adrian.hunter@intel.com> Move hci_dma_xfer_done() earlier in the file to avoid a forward declaration needed by a subsequent change. No functional change. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V3: None Changes in V2: Added Frank's Rev'd-by drivers/i3c/master/mipi-i3c-hci/dma.c | 98 +++++++++++++-------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 28614fdbf558..c9852b85d6b0 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -545,6 +545,55 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, return 0; } +static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + u32 op1_val, op2_val, resp, *ring_resp; + unsigned int tid, done_ptr = rh->done_ptr; + unsigned int done_cnt = 0; + struct hci_xfer *xfer; + + for (;;) { + op2_val = rh_reg_read(RING_OPERATION2); + if (done_ptr == FIELD_GET(RING_OP2_CR_DEQ_PTR, op2_val)) + break; + + ring_resp = rh->resp + rh->resp_struct_sz * done_ptr; + resp = *ring_resp; + tid = RESP_TID(resp); + dev_dbg(&hci->master.dev, "resp = 0x%08x", resp); + + xfer = rh->src_xfers[done_ptr]; + if (!xfer) { + dev_dbg(&hci->master.dev, "orphaned ring entry"); + } else { + hci_dma_unmap_xfer(hci, xfer, 1); + rh->src_xfers[done_ptr] = NULL; + xfer->ring_entry = -1; + xfer->response = resp; + if (tid != xfer->cmd_tid) { + dev_err(&hci->master.dev, + "response tid=%d when expecting %d\n", + tid, xfer->cmd_tid); + /* TODO: do something about it? */ + } + if (xfer->completion) + complete(xfer->completion); + if (RESP_STATUS(resp)) + hci->enqueue_blocked = true; + } + + done_ptr = (done_ptr + 1) % rh->xfer_entries; + rh->done_ptr = done_ptr; + done_cnt += 1; + } + + rh->xfer_space += done_cnt; + op1_val = rh_reg_read(RING_OPERATION1); + op1_val &= ~RING_OP1_CR_SW_DEQ_PTR; + op1_val |= FIELD_PREP(RING_OP1_CR_SW_DEQ_PTR, done_ptr); + rh_reg_write(RING_OPERATION1, op1_val); +} + static void hci_dma_unblock_enqueue(struct i3c_hci *hci) { if (hci->enqueue_blocked) { @@ -636,55 +685,6 @@ static int hci_dma_handle_error(struct i3c_hci *hci, struct hci_xfer *xfer_list, return hci_dma_dequeue_xfer(hci, xfer_list, n) ? -EIO : 0; } -static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) -{ - u32 op1_val, op2_val, resp, *ring_resp; - unsigned int tid, done_ptr = rh->done_ptr; - unsigned int done_cnt = 0; - struct hci_xfer *xfer; - - for (;;) { - op2_val = rh_reg_read(RING_OPERATION2); - if (done_ptr == FIELD_GET(RING_OP2_CR_DEQ_PTR, op2_val)) - break; - - ring_resp = rh->resp + rh->resp_struct_sz * done_ptr; - resp = *ring_resp; - tid = RESP_TID(resp); - dev_dbg(&hci->master.dev, "resp = 0x%08x", resp); - - xfer = rh->src_xfers[done_ptr]; - if (!xfer) { - dev_dbg(&hci->master.dev, "orphaned ring entry"); - } else { - hci_dma_unmap_xfer(hci, xfer, 1); - rh->src_xfers[done_ptr] = NULL; - xfer->ring_entry = -1; - xfer->response = resp; - if (tid != xfer->cmd_tid) { - dev_err(&hci->master.dev, - "response tid=%d when expecting %d\n", - tid, xfer->cmd_tid); - /* TODO: do something about it? */ - } - if (xfer->completion) - complete(xfer->completion); - if (RESP_STATUS(resp)) - hci->enqueue_blocked = true; - } - - done_ptr = (done_ptr + 1) % rh->xfer_entries; - rh->done_ptr = done_ptr; - done_cnt += 1; - } - - rh->xfer_space += done_cnt; - op1_val = rh_reg_read(RING_OPERATION1); - op1_val &= ~RING_OP1_CR_SW_DEQ_PTR; - op1_val |= FIELD_PREP(RING_OP1_CR_SW_DEQ_PTR, done_ptr); - rh_reg_write(RING_OPERATION1, op1_val); -} - static int hci_dma_request_ibi(struct i3c_hci *hci, struct i3c_dev_desc *dev, const struct i3c_ibi_setup *req) { -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:42 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:42 +0300 Subject: [PATCH V3 06/16] i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-7-adrian.hunter@intel.com> hci_dma_dequeue_xfer() relies on state normally updated by the DMA interrupt handler. Ensure that state is current by explicitly invoking hci_dma_xfer_done() from the dequeue path. This handles cases where the interrupt handler has not (yet) run. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V3: None Changes in V2: Added Frank's Rev'd-by drivers/i3c/master/mipi-i3c-hci/dma.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index c9852b85d6b0..28e4d38f55d3 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -635,6 +635,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } + hci_dma_xfer_done(hci, rh); + for (i = 0; i < n; i++) { struct hci_xfer *xfer = xfer_list + i; int idx = xfer->ring_entry; -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:43 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:43 +0300 Subject: [PATCH V3 07/16] i3c: mipi-i3c-hci: Complete transfer lists immediately on error In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-8-adrian.hunter@intel.com> In DMA mode, transfer lists are currently completed only when the final transfer in the list completes. If an earlier transfer fails, the list is left incomplete and callers wait until timeout. There is no need to wait for a timeout, as the completion path in i3c_hci_process_xfer() already checks for error status. Complete the transfer list as soon as any transfer in the list reports an error. This avoids unnecessary delays and spurious timeouts on error. Complete a transfer list completion immediately there is an error. Signed-off-by: Adrian Hunter --- Changes in V3: None Changes in V2: Renamed completing_xfer to final_xfer drivers/i3c/master/mipi-i3c-hci/dma.c | 6 ++++-- drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 28e4d38f55d3..899fdf6555a8 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -502,6 +502,8 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer = xfer_list + i; u32 *ring_data = rh->xfer + rh->xfer_struct_sz * enqueue_ptr; + xfer->final_xfer = xfer_list + n - 1; + /* store cmd descriptor */ *ring_data++ = xfer->cmd_desc[0]; *ring_data++ = xfer->cmd_desc[1]; @@ -576,8 +578,8 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) tid, xfer->cmd_tid); /* TODO: do something about it? */ } - if (xfer->completion) - complete(xfer->completion); + if (xfer == xfer->final_xfer || RESP_STATUS(resp)) + complete(xfer->final_xfer->completion); if (RESP_STATUS(resp)) hci->enqueue_blocked = true; } diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index d630400ec945..f07fc627d4d2 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -104,6 +104,7 @@ struct hci_xfer { struct { /* DMA specific */ struct i3c_dma *dma; + struct hci_xfer *final_xfer; int ring_number; int ring_entry; }; -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:44 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:44 +0300 Subject: [PATCH V3 08/16] i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-9-adrian.hunter@intel.com> Software ABORT of the DMA ring is used to recover from transfer list timeouts, but it is inherently racy. The intended transfer list may complete just before the ABORT takes effect, causing the subsequent transfer list to be aborted instead. In this case, an incomplete transfer list may remain in the ring and has not yet been processed by hci_dma_dequeue_xfer(). Restarting the DMA ring at that point can lead to unpredictable results. Detect when the next queued transfer is not the first entry of a transfer list and does not belong to the list currently being dequeued. In that case, skip restarting the DMA ring and defer recovery until a subsequent call to hci_dma_dequeue_xfer(), which will safely restart the ring once the incomplete list is handled. Signed-off-by: Adrian Hunter --- Changes in V3: None Changes in V2: Renamed completing_xfer to final_xfer drivers/i3c/master/mipi-i3c-hci/dma.c | 15 +++++++++++++++ drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + 2 files changed, 16 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 899fdf6555a8..268f54b32101 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -503,6 +503,7 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, u32 *ring_data = rh->xfer + rh->xfer_struct_sz * enqueue_ptr; xfer->final_xfer = xfer_list + n - 1; + xfer->xfer_list_pos = i; /* store cmd descriptor */ *ring_data++ = xfer->cmd_desc[0]; @@ -669,6 +670,20 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } + /* + * A software ABORT may race with transfer completion and abort the next + * transfer list instead. Detect that case, and do not restart the ring. + * It will be handled by a subsequent dequeue. + */ + if (!did_unqueue) { + struct hci_xfer *xfer = rh->src_xfers[rh->done_ptr]; + + if (xfer && xfer->xfer_list_pos && xfer->final_xfer != xfer_list->final_xfer) { + spin_unlock_irq(&hci->lock); + return false; + } + } + /* restart the ring */ reinit_completion(&rh->op_done); mipi_i3c_hci_resume(hci); diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index f07fc627d4d2..83d4f13a68a3 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -107,6 +107,7 @@ struct hci_xfer { struct hci_xfer *final_xfer; int ring_number; int ring_entry; + int xfer_list_pos; }; }; }; -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:45 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:45 +0300 Subject: [PATCH V3 09/16] i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-10-adrian.hunter@intel.com> Some Intel I3C HCI controllers cannot reliably restart a DMA ring after an ABORT. Additional queue resets are required to recover, and must be performed using PIO reset bits even while operating in DMA mode. This behavior is non-standard. Introduce a controller quirk to opt into the required PIO queue resets after a DMA ring abort, and enable it for Intel LPSS I3C controllers. Signed-off-by: Adrian Hunter --- Changes in V2 and V3: None drivers/i3c/master/mipi-i3c-hci/core.c | 15 ++++++++++++++- drivers/i3c/master/mipi-i3c-hci/dma.c | 9 +++++++++ drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 44617eb3a3f1..770235ad6b25 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -240,6 +240,18 @@ void mipi_i3c_hci_pio_reset(struct i3c_hci *hci) reg_write(RESET_CONTROL, RX_FIFO_RST | TX_FIFO_RST | RESP_QUEUE_RST); } +#define ALL_QUEUES_RST (CMD_QUEUE_RST | RESP_QUEUE_RST | RX_FIFO_RST | TX_FIFO_RST | IBI_QUEUE_RST) + +void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci) +{ + u32 regval; + + reg_write(RESET_CONTROL, ALL_QUEUES_RST); + if (readx_poll_timeout_atomic(reg_read, RESET_CONTROL, regval, + !(regval & ALL_QUEUES_RST), 0, 20)) + dev_err(&hci->master.dev, "%s: Reset queues failed\n", __func__); +} + /* located here rather than dct.c because needed bits are in core reg space */ void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci) { @@ -1040,7 +1052,8 @@ MODULE_DEVICE_TABLE(acpi, i3c_hci_acpi_match); static const struct platform_device_id i3c_hci_driver_ids[] = { { .name = "intel-lpss-i3c", HCI_QUIRK_RPM_ALLOWED | HCI_QUIRK_RPM_IBI_ALLOWED | - HCI_QUIRK_RPM_PARENT_MANAGED }, + HCI_QUIRK_RPM_PARENT_MANAGED | + HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(platform, i3c_hci_driver_ids); diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 268f54b32101..699c6d523eed 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -597,6 +597,13 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) rh_reg_write(RING_OPERATION1, op1_val); } +static void hci_dma_abort_requires_pio_reset_quirk(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + if ((hci->quirks & HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET) && + (rh_reg_read(RING_STATUS) & RING_STATUS_ABORTED)) + mipi_i3c_hci_pio_reset_all_queues(hci); +} + static void hci_dma_unblock_enqueue(struct i3c_hci *hci) { if (hci->enqueue_blocked) { @@ -638,6 +645,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } + hci_dma_abort_requires_pio_reset_quirk(hci, rh); + hci_dma_xfer_done(hci, rh); for (i = 0; i < n; i++) { diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 83d4f13a68a3..01237b12d32e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -156,10 +156,12 @@ struct i3c_hci_dev_data { #define HCI_QUIRK_RPM_ALLOWED BIT(5) /* Runtime PM allowed */ #define HCI_QUIRK_RPM_IBI_ALLOWED BIT(6) /* IBI and Hot-Join allowed while runtime suspended */ #define HCI_QUIRK_RPM_PARENT_MANAGED BIT(7) /* Runtime PM managed by parent device */ +#define HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET BIT(8) /* Do PIO queue SW resets after DMA abort */ /* global functions */ void mipi_i3c_hci_resume(struct i3c_hci *hci); void mipi_i3c_hci_pio_reset(struct i3c_hci *hci); +void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci); void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci); void amd_set_od_pp_timing(struct i3c_hci *hci); void amd_set_resp_buf_thld(struct i3c_hci *hci); -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:46 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:46 +0300 Subject: [PATCH V3 10/16] i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-11-adrian.hunter@intel.com> DMA rings can be aborted either per-ring via RING_CONTROL or globally via HC_CONTROL_ABORT. The driver currently relies on the per-ring mechanism. Some Intel I3C HCI controllers require HC_CONTROL_ABORT to be asserted before a DMA ring abort is effective. This behavior is non-standard. Introduce a controller quirk to select the required abort method and enable it for Intel LPSS I3C controllers. Signed-off-by: Adrian Hunter --- Changes in V2 and V3: None drivers/i3c/master/mipi-i3c-hci/core.c | 18 +++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/dma.c | 27 +++++++++++++++++++++++--- drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 770235ad6b25..8274c84b16be 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -231,7 +231,20 @@ static void i3c_hci_bus_cleanup(struct i3c_master_controller *m) void mipi_i3c_hci_resume(struct i3c_hci *hci) { - reg_set(HC_CONTROL, HC_CONTROL_RESUME); + u32 reg = reg_read(HC_CONTROL); + + reg |= HC_CONTROL_RESUME; + reg &= ~HC_CONTROL_ABORT; + reg_write(HC_CONTROL, reg); +} + +void mipi_i3c_hci_abort(struct i3c_hci *hci) +{ + u32 reg = reg_read(HC_CONTROL); + + reg &= ~HC_CONTROL_RESUME; /* Do not set resume */ + reg |= HC_CONTROL_ABORT; + reg_write(HC_CONTROL, reg); } /* located here rather than pio.c because needed bits are in core reg space */ @@ -1053,7 +1066,8 @@ static const struct platform_device_id i3c_hci_driver_ids[] = { { .name = "intel-lpss-i3c", HCI_QUIRK_RPM_ALLOWED | HCI_QUIRK_RPM_IBI_ALLOWED | HCI_QUIRK_RPM_PARENT_MANAGED | - HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET }, + HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET | + HCI_QUIRK_DMA_REQUIRES_HC_ABORT }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(platform, i3c_hci_driver_ids); diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 699c6d523eed..41bbd912df7f 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -597,6 +597,29 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) rh_reg_write(RING_OPERATION1, op1_val); } +static bool hci_dma_requires_hc_abort_quirk(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + if (!(hci->quirks & HCI_QUIRK_DMA_REQUIRES_HC_ABORT)) + return false; + + reinit_completion(&rh->op_done); + mipi_i3c_hci_abort(hci); + wait_for_completion_timeout(&rh->op_done, HZ); + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); + + return true; +} + +static void hci_dma_abort(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + if (hci_dma_requires_hc_abort_quirk(hci, rh)) + return; + + reinit_completion(&rh->op_done); + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); + wait_for_completion_timeout(&rh->op_done, HZ); +} + static void hci_dma_abort_requires_pio_reset_quirk(struct i3c_hci *hci, struct hci_rh_data *rh) { if ((hci->quirks & HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET) && @@ -630,9 +653,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, hci->enqueue_blocked = true; spin_unlock_irq(&hci->lock); /* stop the ring */ - reinit_completion(&rh->op_done); - rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); - wait_for_completion_timeout(&rh->op_done, HZ); + hci_dma_abort(hci, rh); spin_lock_irq(&hci->lock); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 01237b12d32e..97c31a315a6e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -157,9 +157,11 @@ struct i3c_hci_dev_data { #define HCI_QUIRK_RPM_IBI_ALLOWED BIT(6) /* IBI and Hot-Join allowed while runtime suspended */ #define HCI_QUIRK_RPM_PARENT_MANAGED BIT(7) /* Runtime PM managed by parent device */ #define HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET BIT(8) /* Do PIO queue SW resets after DMA abort */ +#define HCI_QUIRK_DMA_REQUIRES_HC_ABORT BIT(9) /* Use HC_CONTROL ABORT to abort DMA */ /* global functions */ void mipi_i3c_hci_resume(struct i3c_hci *hci); +void mipi_i3c_hci_abort(struct i3c_hci *hci); void mipi_i3c_hci_pio_reset(struct i3c_hci *hci); void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci); void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci); -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:47 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:47 +0300 Subject: [PATCH V3 11/16] i3c: mipi-i3c-hci: Factor out reset-and-restore helper In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-12-adrian.hunter@intel.com> Factor the reset-and-restore sequence out of i3c_hci_rpm_resume() into a separate helper. This allows the same logic to be reused for recovery paths in subsequent changes without duplicating suspend/resume handling. No functional change. Signed-off-by: Adrian Hunter --- Changes in V3: None Changes in V2: Drop redundant i3c_hci_sync_irq_inactive(hci) from i3c_hci_reset_and_restore() because it is called by hci->io->suspend() anyway drivers/i3c/master/mipi-i3c-hci/core.c | 19 +++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 8274c84b16be..12a0122fb709 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -798,9 +798,8 @@ int i3c_hci_rpm_suspend(struct device *dev) } EXPORT_SYMBOL_GPL(i3c_hci_rpm_suspend); -int i3c_hci_rpm_resume(struct device *dev) +static int i3c_hci_do_reset_and_restore(struct i3c_hci *hci) { - struct i3c_hci *hci = dev_get_drvdata(dev); int ret; ret = i3c_hci_reset_and_init(hci); @@ -821,6 +820,22 @@ int i3c_hci_rpm_resume(struct device *dev) return 0; } + +int i3c_hci_reset_and_restore(struct i3c_hci *hci) +{ + i3c_hci_bus_disable(hci); + + hci->io->suspend(hci); + + return i3c_hci_do_reset_and_restore(hci); +} + +int i3c_hci_rpm_resume(struct device *dev) +{ + struct i3c_hci *hci = dev_get_drvdata(dev); + + return i3c_hci_do_reset_and_restore(hci); +} EXPORT_SYMBOL_GPL(i3c_hci_rpm_resume); static int i3c_hci_runtime_suspend(struct device *dev) diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 97c31a315a6e..a3151c26827e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -175,4 +175,6 @@ int i3c_hci_process_xfer(struct i3c_hci *hci, struct hci_xfer *xfer, int n); int i3c_hci_rpm_suspend(struct device *dev); int i3c_hci_rpm_resume(struct device *dev); +int i3c_hci_reset_and_restore(struct i3c_hci *hci); + #endif -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:48 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:48 +0300 Subject: [PATCH V3 12/16] i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-13-adrian.hunter@intel.com> Handle internal I3C HCI errors when operating in DMA mode by adding a simple recovery mechanism. On detection of an internal controller error, mark recovery as needed and attempt to restore operation by performing a software reset followed by state restore. To keep recovery straightforward on this unlikely error path, all currently queued transfers are terminated and completed with an error. This allows the controller to resume operation after internal failures rather than remaining permanently stuck. Note, internal errors indicated by INTR_HC_INTERNAL_ERR, cause the controller to stop. Signed-off-by: Adrian Hunter --- Changes in V3: When erroring out transfers, ensure the final transfer of a transfer list is processed last Changes in V2: Rename completing_xfer to final_xfer Add hci_dma_xfer_done() before checking for an already complete transfer Improve commit message drivers/i3c/master/mipi-i3c-hci/cmd.h | 6 ++ drivers/i3c/master/mipi-i3c-hci/core.c | 1 + drivers/i3c/master/mipi-i3c-hci/dma.c | 93 +++++++++++++++++++++++--- drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd.h b/drivers/i3c/master/mipi-i3c-hci/cmd.h index b1bf87daa651..7bada7b4b2de 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd.h +++ b/drivers/i3c/master/mipi-i3c-hci/cmd.h @@ -65,4 +65,10 @@ struct hci_cmd_ops { extern const struct hci_cmd_ops mipi_i3c_hci_cmd_v1; extern const struct hci_cmd_ops mipi_i3c_hci_cmd_v2; +static inline void hci_cmd_set_resp_err(u32 *response, int resp_err) +{ + *response &= ~RESP_ERR_FIELD; + *response |= FIELD_PREP(RESP_ERR_FIELD, resp_err); +} + #endif diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 12a0122fb709..69dcf5dad3a5 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -668,6 +668,7 @@ static irqreturn_t i3c_hci_irq_handler(int irq, void *dev_id) if (val & INTR_HC_INTERNAL_ERR) { dev_err(&hci->master.dev, "Host Controller Internal Error\n"); val &= ~INTR_HC_INTERNAL_ERR; + hci->recovery_needed = true; } if (val) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 41bbd912df7f..376062c0fcbf 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -9,6 +9,7 @@ */ #include +#include #include #include #include @@ -258,6 +259,10 @@ static void hci_dma_init_rh(struct i3c_hci *hci, struct hci_rh_data *rh, int i) rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); + /* + * Do not clear the entries of rh->src_xfers because the recovery uses + * them. In other cases they should be NULL anyway. + */ rh->done_ptr = 0; rh->ibi_chunk_ptr = 0; rh->xfer_space = rh->xfer_entries; @@ -362,7 +367,7 @@ static int hci_dma_init(struct i3c_hci *hci) rh->resp = dma_alloc_coherent(rings->sysdev, resps_sz, &rh->resp_dma, GFP_KERNEL); rh->src_xfers = - kmalloc_objs(*rh->src_xfers, rh->xfer_entries); + kzalloc_objs(*rh->src_xfers, rh->xfer_entries); ret = -ENOMEM; if (!rh->xfer || !rh->resp || !rh->src_xfers) goto err_out; @@ -572,13 +577,15 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) hci_dma_unmap_xfer(hci, xfer, 1); rh->src_xfers[done_ptr] = NULL; xfer->ring_entry = -1; - xfer->response = resp; if (tid != xfer->cmd_tid) { dev_err(&hci->master.dev, "response tid=%d when expecting %d\n", tid, xfer->cmd_tid); - /* TODO: do something about it? */ + hci->recovery_needed = true; + if (!RESP_STATUS(resp)) + hci_cmd_set_resp_err(&resp, RESP_ERR_HC_TERMINATED); } + xfer->response = resp; if (xfer == xfer->final_xfer || RESP_STATUS(resp)) complete(xfer->final_xfer->completion); if (RESP_STATUS(resp)) @@ -635,6 +642,60 @@ static void hci_dma_unblock_enqueue(struct i3c_hci *hci) } } +static void hci_dma_error_out_rh(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + /* + * The entries of rh->src_xfers are not cleared by + * i3c_hci_reset_and_restore(), so can be used here. Do 2 passes so + * that the final_xfer of an xfer list is always processed last. + */ + for (int pass = 0; pass < 2; pass++) + for (int i = 0; i < rh->xfer_entries; i++) { + struct hci_xfer *xfer = rh->src_xfers[i]; + + if (!xfer || (!pass && xfer == xfer->final_xfer)) + continue; + hci_dma_unmap_xfer(hci, xfer, 1); + rh->src_xfers[i] = NULL; + xfer->ring_entry = -1; + hci_cmd_set_resp_err(&xfer->response, RESP_ERR_HC_TERMINATED); + if (xfer == xfer->final_xfer) + complete(xfer->final_xfer->completion); + } +} + +static void hci_dma_error_out_all(struct i3c_hci *hci) +{ + struct hci_rings_data *rings = hci->io_data; + + for (int i = 0; i < rings->total; i++) + hci_dma_error_out_rh(hci, &rings->headers[i]); +} + +static void hci_dma_recovery(struct i3c_hci *hci) +{ + int ret; + + dev_err(&hci->master.dev, "Attempting to recover from internal errors\n"); + + for (int i = 0; i < 3; i++) { + ret = i3c_hci_reset_and_restore(hci); + if (!ret) + break; + dev_err(&hci->master.dev, "Reset and restore failed, error %d\n", ret); + /* Just in case the controller is busy, give it some time */ + msleep(1000); + } + + spin_lock_irq(&hci->lock); + hci_dma_error_out_all(hci); + hci_dma_unblock_enqueue(hci); + hci->recovery_needed = false; + spin_unlock_irq(&hci->lock); + + dev_err(&hci->master.dev, "Recovery %s\n", ret ? "failed!" : "done"); +} + static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n) { @@ -650,6 +711,17 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { + /* + * The transfer may have already completed, especially + * if recovery has just run. Do nothing in that case. + */ + hci_dma_xfer_done(hci, rh); + if (xfer_list->final_xfer->ring_entry < 0 && + !hci->recovery_needed && !hci->enqueue_blocked && + ring_status == (RING_STATUS_ENABLED | RING_STATUS_RUNNING)) { + spin_unlock_irq(&hci->lock); + return false; + } hci->enqueue_blocked = true; spin_unlock_irq(&hci->lock); /* stop the ring */ @@ -657,12 +729,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, spin_lock_irq(&hci->lock); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { - /* - * We're deep in it if ever this condition is ever met. - * Hardware might still be writing to memory, etc. - */ - dev_crit(&hci->master.dev, "unable to abort the ring\n"); - WARN_ON(1); + dev_err(&hci->master.dev, "Unable to abort the DMA ring\n"); + hci->recovery_needed = true; } } @@ -670,6 +738,13 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, hci_dma_xfer_done(hci, rh); + if (hci->recovery_needed) { + hci->enqueue_blocked = true; + spin_unlock_irq(&hci->lock); + hci_dma_recovery(hci); + return true; + } + for (i = 0; i < n; i++) { struct hci_xfer *xfer = xfer_list + i; int idx = xfer->ring_entry; diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index a3151c26827e..4bf2c66c97b4 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -55,6 +55,7 @@ struct i3c_hci { atomic_t next_cmd_tid; bool irq_inactive; bool enqueue_blocked; + bool recovery_needed; wait_queue_head_t enqueue_wait_queue; u32 caps; unsigned int quirks; -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:49 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:49 +0300 Subject: [PATCH V3 13/16] i3c: mipi-i3c-hci: Wait for NoOp commands to complete In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-14-adrian.hunter@intel.com> When a transfer list is only partially completed due to an error, hci_dma_dequeue_xfer() overwrites the remaining DMA ring entries with NoOp commands and restarts the ring to flush them out. While NoOp commands are expected to complete successfully, they may still fail to complete if the DMA ring is stuck. Explicitly wait for the NoOp commands to finish, and trigger controller recovery if they do not complete or report an error. This ensures that partially completed transfer lists are reliably resolved and that a stuck ring is recovered promptly. Signed-off-by: Adrian Hunter --- Changes in V3: None Changes in V2: Rename completing_xfer to final_xfer Add missing reinit_completion() drivers/i3c/master/mipi-i3c-hci/dma.c | 39 ++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 376062c0fcbf..90fa621c9d56 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -696,11 +696,33 @@ static void hci_dma_recovery(struct i3c_hci *hci) dev_err(&hci->master.dev, "Recovery %s\n", ret ? "failed!" : "done"); } +static bool hci_dma_wait_for_noop(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n, + int noop_pos) +{ + struct completion *done = xfer_list->final_xfer->completion; + bool timeout = !wait_for_completion_timeout(done, HZ); + u32 error = timeout; + + for (int i = noop_pos; i < n && !error; i++) + error = RESP_STATUS(xfer_list[i].response); + + if (!error) + return true; + + if (timeout) + dev_err(&hci->master.dev, "NoOp timeout error\n"); + else + dev_err(&hci->master.dev, "NoOp error %u\n", error); + + return false; +} + static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n) { struct hci_rings_data *rings = hci->io_data; struct hci_rh_data *rh = &rings->headers[xfer_list[0].ring_number]; + int noop_pos = -1; unsigned int i; bool did_unqueue = false; u32 ring_status; @@ -708,7 +730,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, guard(mutex)(&hci->control_mutex); spin_lock_irq(&hci->lock); - +restart: ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { /* @@ -765,11 +787,10 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, *ring_data++ = 0; } - /* disassociate this xfer struct */ - rh->src_xfers[idx] = NULL; - - /* and unmap it */ - hci_dma_unmap_xfer(hci, xfer, 1); + if (noop_pos < 0) { + reinit_completion(xfer->final_xfer->completion); + noop_pos = i; + } did_unqueue = true; } @@ -801,6 +822,12 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, wait_for_completion_timeout(&rh->op_done, HZ); + if (did_unqueue && !hci_dma_wait_for_noop(hci, xfer_list, n, noop_pos)) { + spin_lock_irq(&hci->lock); + hci->recovery_needed = true; + goto restart; + } + return did_unqueue; } -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:50 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:50 +0300 Subject: [PATCH V3 14/16] i3c: mipi-i3c-hci: Base timeouts on actual transfer start time In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-15-adrian.hunter@intel.com> Transfer timeouts are currently measured from the point where a transfer list is queued to the controller. This can cause transfers to time out before they have actually started, if earlier queued transfers consume the timeout interval. Fix this by recording when a transfer reaches the head of the queue and adjusting the timeout calculation to start from that point. The existing low-overhead completion-based timeout mechanism is preserved, but care is taken to ensure the transfer start time is consistently recorded for both PIO and DMA paths. This prevents premature timeouts while retaining efficient timeout handling. Signed-off-by: Adrian Hunter --- Changes in V3: None Changes in V2: Do not flag the next transfer as started when there is an error which halts the controller Instead flag it started at the end of hci_dma_dequeue_xfer() Use hci_start_xfer() in pio.c drivers/i3c/master/mipi-i3c-hci/core.c | 19 ++++++++++++++++++- drivers/i3c/master/mipi-i3c-hci/dma.c | 19 ++++++++++++++++++- drivers/i3c/master/mipi-i3c-hci/hci.h | 11 +++++++++++ drivers/i3c/master/mipi-i3c-hci/pio.c | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 69dcf5dad3a5..2866d599612a 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -275,13 +275,30 @@ int i3c_hci_process_xfer(struct i3c_hci *hci, struct hci_xfer *xfer, int n) { struct completion *done = xfer[n - 1].completion; unsigned long timeout = xfer[n - 1].timeout; + unsigned long remaining_timeout = timeout; + long time_taken; + bool started; int ret; + xfer[0].started = false; + ret = hci->io->queue_xfer(hci, xfer, n); if (ret) return ret; - if (!wait_for_completion_timeout(done, timeout)) { + while (!wait_for_completion_timeout(done, remaining_timeout)) { + scoped_guard(spinlock_irqsave, &hci->lock) { + started = xfer[0].started; + time_taken = jiffies - xfer[0].start_time; + } + /* Keep waiting if xfer has not started */ + if (!started) + continue; + /* Recalculate timeout based on actual start time */ + if (time_taken < timeout) { + remaining_timeout = timeout - time_taken; + continue; + } if (hci->io->dequeue_xfer(hci, xfer, n)) { dev_err(&hci->master.dev, "%s: timeout error\n", __func__); return -ETIMEDOUT; diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 90fa621c9d56..6440302c63ca 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -543,6 +543,9 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, enqueue_ptr = (enqueue_ptr + 1) % rh->xfer_entries; } + if (rh->xfer_space == rh->xfer_entries) + hci_start_xfer(xfer_list); + rh->xfer_space -= n; op1_val &= ~RING_OP1_CR_ENQ_PTR; @@ -558,6 +561,7 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) u32 op1_val, op2_val, resp, *ring_resp; unsigned int tid, done_ptr = rh->done_ptr; unsigned int done_cnt = 0; + bool start_next = false; struct hci_xfer *xfer; for (;;) { @@ -588,8 +592,14 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) xfer->response = resp; if (xfer == xfer->final_xfer || RESP_STATUS(resp)) complete(xfer->final_xfer->completion); - if (RESP_STATUS(resp)) + else + hci_start_xfer(xfer); + if (RESP_STATUS(resp)) { hci->enqueue_blocked = true; + start_next = false; + } else { + start_next = true; + } } done_ptr = (done_ptr + 1) % rh->xfer_entries; @@ -598,6 +608,10 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) } rh->xfer_space += done_cnt; + if (start_next && rh->xfer_space < rh->xfer_entries) { + xfer = rh->src_xfers[done_ptr]; + hci_start_xfer(xfer); + } op1_val = rh_reg_read(RING_OPERATION1); op1_val &= ~RING_OP1_CR_SW_DEQ_PTR; op1_val |= FIELD_PREP(RING_OP1_CR_SW_DEQ_PTR, done_ptr); @@ -818,6 +832,9 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, hci_dma_unblock_enqueue(hci); + if (rh->xfer_space < rh->xfer_entries) + hci_start_xfer(rh->src_xfers[rh->done_ptr]); + spin_unlock_irq(&hci->lock); wait_for_completion_timeout(&rh->op_done, HZ); diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 4bf2c66c97b4..243d7a67f6f6 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -11,6 +11,7 @@ #define HCI_H #include +#include /* 32-bit word aware bit and mask macros */ #define W0_MASK(h, l) GENMASK((h) - 0, (l) - 0) @@ -88,11 +89,13 @@ struct hci_xfer { u32 cmd_desc[4]; u32 response; bool rnw; + bool started; void *data; unsigned int data_len; unsigned int cmd_tid; struct completion *completion; unsigned long timeout; + unsigned long start_time; union { struct { /* PIO specific */ @@ -123,6 +126,14 @@ static inline void hci_free_xfer(struct hci_xfer *xfer, unsigned int n) kfree(xfer); } +static inline void hci_start_xfer(struct hci_xfer *xfer) +{ + if (!xfer->started) { + xfer->started = true; + xfer->start_time = jiffies; + } +} + /* This abstracts PIO vs DMA operations */ struct hci_io_ops { bool (*irq_handler)(struct i3c_hci *hci); diff --git a/drivers/i3c/master/mipi-i3c-hci/pio.c b/drivers/i3c/master/mipi-i3c-hci/pio.c index 8f48a81e65ab..6b8cc5f2b4d2 100644 --- a/drivers/i3c/master/mipi-i3c-hci/pio.c +++ b/drivers/i3c/master/mipi-i3c-hci/pio.c @@ -605,6 +605,7 @@ static bool hci_pio_process_cmd(struct i3c_hci *hci, struct hci_pio_data *pio) * Finally send the command. */ hci_pio_write_cmd(hci, pio->curr_xfer); + hci_start_xfer(pio->curr_xfer); /* * And move on. */ -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:51 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:51 +0300 Subject: [PATCH V3 15/16] i3c: mipi-i3c-hci: Consolidate DMA ring allocation In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-16-adrian.hunter@intel.com> dma_alloc_coherent() allocates memory in whole pages, which can waste space when command and response queues are allocated separately. Allocate the DMA command and response queues from a single coherent allocation instead, while preserving the required 4-byte alignment. This reduces memory overhead without changing behavior. Signed-off-by: Adrian Hunter --- Changes in V3: None Changes in V2: Check for failed allocation before assignments to avoid doing arithmetic with NULL pointers drivers/i3c/master/mipi-i3c-hci/dma.c | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 6440302c63ca..4029d4d9e784 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -186,14 +186,12 @@ static void hci_dma_free(void *data) for (int i = 0; i < rings->total; i++) { rh = &rings->headers[i]; - if (rh->xfer) - dma_free_coherent(rings->sysdev, - rh->xfer_struct_sz * rh->xfer_entries, - rh->xfer, rh->xfer_dma); - if (rh->resp) - dma_free_coherent(rings->sysdev, - rh->resp_struct_sz * rh->xfer_entries, - rh->resp, rh->resp_dma); + if (rh->xfer) { + size_t sz = round_up(rh->xfer_struct_sz * rh->xfer_entries, 4); + + sz += rh->resp_struct_sz * rh->xfer_entries; + dma_free_coherent(rings->sysdev, sz, rh->xfer, rh->xfer_dma); + } kfree(rh->src_xfers); if (rh->ibi_status) dma_free_coherent(rings->sysdev, @@ -359,18 +357,18 @@ static int hci_dma_init(struct i3c_hci *hci) dev_dbg(&hci->master.dev, "xfer_struct_sz = %d, resp_struct_sz = %d", rh->xfer_struct_sz, rh->resp_struct_sz); - xfers_sz = rh->xfer_struct_sz * rh->xfer_entries; + xfers_sz = round_up(rh->xfer_struct_sz * rh->xfer_entries, 4); resps_sz = rh->resp_struct_sz * rh->xfer_entries; - rh->xfer = dma_alloc_coherent(rings->sysdev, xfers_sz, + rh->xfer = dma_alloc_coherent(rings->sysdev, xfers_sz + resps_sz, &rh->xfer_dma, GFP_KERNEL); - rh->resp = dma_alloc_coherent(rings->sysdev, resps_sz, - &rh->resp_dma, GFP_KERNEL); rh->src_xfers = kzalloc_objs(*rh->src_xfers, rh->xfer_entries); ret = -ENOMEM; - if (!rh->xfer || !rh->resp || !rh->src_xfers) + if (!rh->xfer || !rh->src_xfers) goto err_out; + rh->resp = rh->xfer + xfers_sz; + rh->resp_dma = rh->xfer_dma + xfers_sz; /* IBIs */ -- 2.51.0 From adrian.hunter at intel.com Mon May 4 04:33:52 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 4 May 2026 14:33:52 +0300 Subject: [PATCH V3 16/16] i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: <20260504113352.38490-17-adrian.hunter@intel.com> The DMA transfer ring is currently limited to 16 entries, despite the MIPI I3C HCI supporting up to 32 devices. When the ring lacks space for a new transfer list, the driver returns -EBUSY, which can be unexpected for clients. Increase the DMA transfer ring size to the maximum supported value of 255 entries. This effectively eliminates ring-space exhaustion in practice and avoids the complexity of adding secondary queuing mechanisms. Even at the maximum size, the memory overhead remains small (approximately 24 bytes per entry by default). Signed-off-by: Adrian Hunter --- Changes in V2 and V3: None 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 4029d4d9e784..9549d98add4b 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -27,7 +27,7 @@ */ #define XFER_RINGS 1 /* max: 8 */ -#define XFER_RING_ENTRIES 16 /* max: 255 */ +#define XFER_RING_ENTRIES 255 /* max: 255 */ #define IBI_RINGS 1 /* max: 8 */ #define IBI_STATUS_RING_ENTRIES 32 /* max: 255 */ -- 2.51.0 From u.kleine-koenig at baylibre.com Mon May 4 07:33:15 2026 From: u.kleine-koenig at baylibre.com (=?UTF-8?q?Uwe=20Kleine-K=C3=B6nig=20=28The=20Capable=20Hub=29?=) Date: Mon, 4 May 2026 16:33:15 +0200 Subject: [PATCH] i3c: Consistently define pci_device_ids using named initializers Message-ID: <20260504143324.2122737-2-u.kleine-koenig@baylibre.com> The .driver_data member of the various struct pci_device_id arrays were initialized by list expressions. This isn't easily readable if you're not into PCI. Using named initializers is more explicit and thus easier to parse. This change doesn't introduce changes to the compiled pci_device_id arrays. Tested on x86 and arm64. Signed-off-by: Uwe Kleine-K?nig (The Capable Hub) --- Hello, The secret plan is to make struct pci_device_id::driver_data an anonymous union (similar to https://lore.kernel.org/all/cover.1776579304.git.u.kleine-koenig at baylibre.com/) and that requires named initializers. But IMHO it's also a nice cleanup on its own. The anonymous union will allow changes like the following: - { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info }, (together with the respective change in the code when the value is used). This gets rid of a bunch of casts and thus slightly improves type safety. Best regards Uwe .../master/mipi-i3c-hci/mipi-i3c-hci-pci.c | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 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 9468786fb853..5a9e2a43eff8 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 @@ -461,21 +461,21 @@ static const struct dev_pm_ops mipi_i3c_hci_pci_pm_ops = { static const struct pci_device_id mipi_i3c_hci_pci_devices[] = { /* Wildcat Lake-U */ - { PCI_VDEVICE(INTEL, 0x4d7c), (kernel_ulong_t)&intel_mi_1_info}, - { PCI_VDEVICE(INTEL, 0x4d6f), (kernel_ulong_t)&intel_si_2_info}, + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, + { PCI_VDEVICE(INTEL, 0x4d6f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, /* Panther Lake-H */ - { PCI_VDEVICE(INTEL, 0xe37c), (kernel_ulong_t)&intel_mi_1_info}, - { PCI_VDEVICE(INTEL, 0xe36f), (kernel_ulong_t)&intel_si_2_info}, + { PCI_VDEVICE(INTEL, 0xe37c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, + { PCI_VDEVICE(INTEL, 0xe36f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, /* Panther Lake-P */ - { PCI_VDEVICE(INTEL, 0xe47c), (kernel_ulong_t)&intel_mi_1_info}, - { PCI_VDEVICE(INTEL, 0xe46f), (kernel_ulong_t)&intel_si_2_info}, + { PCI_VDEVICE(INTEL, 0xe47c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, + { PCI_VDEVICE(INTEL, 0xe46f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, /* Nova Lake-S */ - { PCI_VDEVICE(INTEL, 0x6e2c), (kernel_ulong_t)&intel_mi_1_info}, - { PCI_VDEVICE(INTEL, 0x6e2d), (kernel_ulong_t)&intel_mi_2_info}, + { PCI_VDEVICE(INTEL, 0x6e2c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, + { PCI_VDEVICE(INTEL, 0x6e2d), .driver_data = (kernel_ulong_t)&intel_mi_2_info }, /* Nova Lake-H */ - { PCI_VDEVICE(INTEL, 0xd37c), (kernel_ulong_t)&intel_mi_1_info}, - { PCI_VDEVICE(INTEL, 0xd36f), (kernel_ulong_t)&intel_mi_2_info}, - { }, + { PCI_VDEVICE(INTEL, 0xd37c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, + { PCI_VDEVICE(INTEL, 0xd36f), .driver_data = (kernel_ulong_t)&intel_mi_2_info }, + { } }; MODULE_DEVICE_TABLE(pci, mipi_i3c_hci_pci_devices); base-commit: 254f49634ee16a731174d2ae34bc50bd5f45e731 -- 2.47.3 From gregkh at linuxfoundation.org Mon May 4 07:25:41 2026 From: gregkh at linuxfoundation.org (Greg KH) Date: Mon, 4 May 2026 16:25:41 +0200 Subject: [PATCH 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver In-Reply-To: <20260430121354.6253-2-zain_zhou@realsil.com.cn> References: <20260430121354.6253-1-zain_zhou@realsil.com.cn> <20260430121354.6253-2-zain_zhou@realsil.com.cn> Message-ID: <2026050412-bush-rosy-959d@gregkh> On Thu, Apr 30, 2026 at 08:13:54PM +0800, zain_zhou at realsil.com.cn wrote: > From: zain_zhou > > Add driver for Realtek RTS490x series I3C HUB devices (RTS4900, > RTS4901, RTS4902, RTS4903, RTS4904, RTS4906). > > The I3C HUB is a smart device that provides: > - voltage compatibility across I3C Controller and Target devices > - bus capacitance isolation > - address conflict isolation > - I3C port expansion (up to 8 target ports) > - dual controller port support > - I3C and SMBus device compatibility > - GPIO expansion via target ports > > The driver supports: > - Device Tree based configuration of LDO, pull-up, IO strength > and per-port mode (I3C/SMBus/GPIO/disabled) > - Logical I3C bus registration per target port > - SMBus agent functionality with IBI and polling modes > - GPIO chip with IRQ support > - DebugFS interface for register access and DT config inspection > - IBI (In-Band Interrupt) handling > > The driver is placed in staging as it has known issues to be resolved > before mainlining; see drivers/staging/rts490x/TODO for details. > > Signed-off-by: zain_zhou We need a real name, not an email alias. And no, please don't add new drivers to drivers/staging/ especially when it is so easy to fix them up properly "first" before adding them to the kernel tree. Your TODO file is pretty easy: > diff --git a/drivers/staging/rts490x/TODO b/drivers/staging/rts490x/TODO > new file mode 100644 > index 000000000000..0be2d7693d68 > --- /dev/null > +++ b/drivers/staging/rts490x/TODO > @@ -0,0 +1,19 @@ > +TODO list for rts490xa-i3c-hub staging driver > +============================================== > + > +- Move driver out of staging once the following are addressed: > + - Add proper DT binding schema validation (dt-schema) > + - Clean up open-coded OF property parsing; use device_property_* APIs > + instead of of_property_read_* where possible > + - Remove use of full_name / sscanf for node name parsing; use > + of_node_name_eq() and fwnode helpers instead > + - Replace global mutex (i3c_hub_regmap_mutex) with per-device locking > + - Add kernel-doc comments for all exported/public functions > + - Resolve TODO comment in i3c_hub_hw_configure_tp() regarding MUX > + connection verification > + - Remove TBD comment in i3c_hub_probe() regarding DEV_CMD security lock > + - Review and fix potential locking issues in i3c_hub_delayed_work() > + when registering logical buses > + - Fix error handling in i3c_hub_delayed_work(): early return on failure > + does not unregister already-registered logical buses, causing resource > + leak; needs proper cleanup on error path All of those you could do this week. Don't add stuff to staging that you are going to maintain, as it will be more work in the end. Just do the needed extra effort and then merge it to the proper place in the tree. thanks, greg k-h From Frank.li at nxp.com Mon May 4 13:34:05 2026 From: Frank.li at nxp.com (Frank Li) Date: Mon, 4 May 2026 16:34:05 -0400 Subject: [PATCH 1/2] dt-bindings: i3c: add binding for Realtek RTS490x I3C HUB In-Reply-To: <20260430121354.6253-1-zain_zhou@realsil.com.cn> References: <20260430121354.6253-1-zain_zhou@realsil.com.cn> Message-ID: On Thu, Apr 30, 2026 at 08:13:53PM +0800, zain_zhou at realsil.com.cn wrote: > From: zain_zhou > > Add DT binding schema for Realtek RTS490x series I3C HUB devices. > > The binding describes configuration properties for: > - LDO enable/disable and voltage level per port group > - Pull-up resistance per port group > - IO driver strength per port > - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up, > IO mode, SMBus clock frequency and polling interval > - Hub network always-I3C mode > - Hardware identification via CSEL pin (id) and CP1 pins (id-cp1) Please base on https://lore.kernel.org/linux-i3c/20260420105222.1562243-1-lakshay.piplani at nxp.com/T/#t which almost done! Frank > > Signed-off-by: zain_zhou > --- > .../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 410 ++++++++++++++++++ > MAINTAINERS | 6 + > 2 files changed, 416 insertions(+) > create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > > diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > new file mode 100644 > index 000000000000..30295eefee89 > --- /dev/null > +++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > @@ -0,0 +1,410 @@ > +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) > +%YAML 1.2 > +--- > +$id: http://devicetree.org/schemas/i3c/realtek,rts490x-i3c-hub.yaml# > +$schema: http://devicetree.org/meta-schemas/core.yaml# > + > +title: I3C HUB > + > +maintainers: > + - zain_zhou > + > +description: | > + I3C HUB is smart device which provides multiple functionality: > + * enabling voltage compatibility across I3C Controller and Target devices, > + * bus capacitance isolation > + * address conflict isolation > + * I3C port expansion > + * two controllers in a single I3C bus > + * I3C and SMBus device compatibility > + * GPIO expansion > + > + Having such big number of features, there is a need to have some DT knobs to tell the I3C HUB > + driver which features shall be enabled and how they shall be configured. I3C HUB driver read, > + validate DT knobs and set corresponding registers with the right way to satisfy user requests from > + DT. > + > + All the DT properties for I3C HUB are located under dedicated (for I3C HUB) DT entry. I3C HUB DT > + entry structure is aligned with regular I3C device DT entry described in i3c.yaml. > + > +allOf: > + - $ref: i3c.yaml# > + > +properties: > + $nodename: > + pattern: "^hub at 0,0$" > + > + cp0-ldo-en: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Controller Port 0 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + cp1-ldo-en: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Controller Port 1 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + tp0145-ldo-en: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Target Ports 0/1/4/5 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + tp2367-ldo-en: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Target Ports 2/3/6/7 LDO disabling/enabling setting. If enabled, voltage produced by > + on-die LDO will be available externally on dedicated pin. This option could be used to supply > + external pull-up resistors or for any other purpose which does not cross LDO capabilities. > + > + This property is optional. If not provided, LDO will be disabled. > + > + cp0-ldo-volt: > + enum: > + - 1.0V > + - 1.1V > + - 1.2V > + - 1.8V > + description: | > + I3C HUB Controller Port 0 LDO setting to control the Controller Port 1 voltage level. This > + property is optional. > + > + If not provided, LDO configuration is not modified in I3C HUB. > + > + cp1-ldo-volt: > + enum: > + - 1.0V > + - 1.1V > + - 1.2V > + - 1.8V > + description: | > + I3C HUB Controller Port 1 LDO setting to control the Controller Port 1 voltage level. This > + property is optional. > + > + If not provided, LDO configuration is not modified in I3C HUB. > + > + tp0145-ldo-volt: > + enum: > + - disabled > + - 1.0V > + - 1.1V > + - 1.2V > + - 1.8V > + description: | > + I3C HUB Target Ports 0/1/4/5 LDO setting to control the Target Ports 0/1/4/5 voltage level. > + > + If not provided, LDO configuration is not modified in I3C HUB. > + > + tp2367-ldo-volt: > + enum: > + - disabled > + - 1.0V > + - 1.1V > + - 1.2V > + - 1.8V > + description: | > + I3C HUB Target Ports 2/3/6/7 LDO setting to control the Target Ports 2/3/6/7 voltage level. > + > + If not provided, LDO configuration is not modified in I3C HUB. > + > + tp0145-pullup: > + enum: > + - disabled > + - 250R > + - 500R > + - 1k > + - 2k > + description: | > + I3C HUB Target Ports 0/1/4/5 pull-up setting to control the Target Ports 0/1/4/5 pull-up > + resistance level. > + > + This property is optional. If not provided, pull-up configuration is not modified in I3C HUB. > + > + tp2367-pullup: > + enum: > + - disabled > + - 250R > + - 500R > + - 1k > + - 2k > + description: | > + I3C HUB Target Ports 2/3/6/7 pull-up setting to control the Target Ports 2/3/6/7 pull-up > + resistance level. > + > + This property is optional. If not provided, pull-up configuration is not modified in I3C HUB. > + > + cp0-io-strength: > + enum: > + - 20Ohms > + - 30Ohms > + - 40Ohms > + - 50Ohms > + description: | > + I3C HUB Controller Port 0 IO strength setting to control the Controller Port 0 output driver > + strength. > + > + This property is optional. If not provided, IO strength configuration is not modified in I3C > + HUB. > + > + cp1-io-strength: > + enum: > + - 20Ohms > + - 30Ohms > + - 40Ohms > + - 50Ohms > + description: | > + I3C HUB Controller Port 1 IO strength setting to control the Controller Port 1 output driver > + strength. > + > + This property is optional. If not provided, IO strength configuration is not modified in I3C > + HUB. > + > + tp0145-io-strength: > + enum: > + - 20Ohms > + - 30Ohms > + - 40Ohms > + - 50Ohms > + description: | > + I3C HUB Target Ports 0/1/4/5 IO strength setting to control the Target Ports 0/1/4/5 output > + driver strength. > + > + This property is optional. If not provided, IO strength configuration is not modified in I3C > + HUB. > + > + tp2367-io-strength: > + enum: > + - 20Ohms > + - 30Ohms > + - 40Ohms > + - 50Ohms > + description: | > + I3C HUB Target Ports 2/3/6/7 IO strength setting to control the Target Ports 2/3/6/7 output > + driver strength. > + > + This property is optional. If not provided, IO strength configuration is not modified in I3C > + HUB. > + > + id: > + enum: > + - 0 > + - 1 > + - 3 > + description: | > + I3C HUB ID based on CSEL pin. There are three possible values: > + 0 - CP0 is selected as primary Controller Port > + 1 - Primary Controller Port is selected by software by writing the REG#56 > + 3 - CP1 is selected as primary Controller Port > + > + I3C HUB driver reads CSEL pin status (REG#121[5:4]) and tries to find DT node with matching > + value in 'id' property. > + > + This property is optional. If not provided, DT node can only be used by the I3C HUB driver if > + there is no others with matching 'id' or 'id-cp1'. If there is a multiple DT nodes with no > + 'id' property - the first one will be chosen by I3C HUB driver. If there is a multiple DT > + nodes with matching 'id' property - the first one will be chosen by I3C HUB driver. > + > + If both 'id' and 'id-cp1' are available, DT node will chosen only when both values match those > + read from I3C HUB. > + > + id-cp1: > + enum: > + - 0 > + - 1 > + - 2 > + - 3 > + description: | > + I3C HUB ID based on CP1 SDA and SCL pins state probed during power on. > + > + I3C HUB driver reads CP1 SDA and SCL pin status and tries to find DT node with matching value > + in 'id-cp1' property. > + > + This property is optional. If not provided, DT node can only be used by the I3C HUB > + driver if there is no others with matching 'id' or 'id-cp1'. If there is a multiple DT nodes > + with no 'id-cp1' property - the first one will be chosen by I3C HUB driver. If there is a > + multiple DT nodes with matching 'id-cp1' property - the first one will be chosen by I3C HUB > + driver. > + > + If both 'id' and 'id-cp1' are available, DT node will chosen only when both values match those > + read from I3C HUB. > + > +patternProperties: > + "@[0-9]$": > + type: object > + description: | > + I3C HUB Target Port child, should be named: target-port@ > + > + properties: > + mode: > + enum: > + - disabled > + - i3c > + - smbus > + - gpio > + description: | > + I3C HUB Target Port mode setting to control Target Port functionality. > + > + This property is optional. If not provided, Target Port mode configuration is not modified > + in I3C HUB. > + > + pullup: > + enum: > + - disabled > + - enabled > + description: | > + I3C HUB Target Port pull-up setting to disable/enable Target Port pull-up. > + > + This property is optional. If not provided, Target Port pull-up configuration is not > + modified in I3C HUB. > + > + always-enable: > + type: boolean > + description: | > + I3C HUB Target Port settings to control the port enable/disable policy. > + > + This property is optional. If not provided, Target Port is enabled only on accessing to > + the devices connected to it and the port is disabled automatically after the accessing > + is done. If provided, the Target Port is always enabled. > + > + polling-interval-ms: > + type: uint32 > + description: | > + I3C HUB Target Port SMBus polling interval in milliseconds. > + > + This property is optional. If not provided or set to 0, polling is disabled and the driver > + uses IBI (In-Band Interrupts). If provided with a positive value, polling is enabled for > + this Target Port with the given period.Note: this positive value only affects the SMBus > + target agent polling. The SMBus controller agent polling interval is computed dynamically > + from clock and data length. > + > + clock-frequency: > + type: uint32 > + enum: > + - 100000 > + - 200000 > + - 400000 > + - 1000000 > + description: | > + I3C HUB Target Port SMBus clock frequency in Hz. > + > + This property follows the standard I2C 'clock-frequency' semantics. > + Applies only when the Target Port mode is set to "smbus". > + Optional; if not provided, the driver uses 400000 Hz by default. > + > +additionalProperties: true > + > +examples: > + - | > + i3c-master at d040000 { > + #address-cells = <1>; > + #size-cells = <0>; > + > + hub at 0,0 { > + cp0-ldo-en = "disabled"; > + cp1-ldo-en = "enabled"; > + cp0-ldo-volt = "1.0V"; > + cp1-ldo-volt = "1.1V"; > + tp0145-ldo-en = "enabled"; > + tp2367-ldo-en = "disabled"; > + tp0145-ldo-volt = "1.2V"; > + tp2367-ldo-volt = "1.8V"; > + tp0145-pullup = "2k"; > + tp2367-pullup = "500R"; > + tp0145-io-strength = "50Ohms"; > + tp2367-io-strength = "30Ohms"; > + cp0-io-strength = "20Ohms"; > + cp1-io-strength = "40Ohms"; > + > + target-port at 0 { > + mode = "i3c"; > + pullup = "enabled"; > + always_enable; > + }; > + target-port at 1 { > + mode = "smbus"; > + pullup = "enabled"; > + clock-frequency = <1000000>; > + polling-interval-ms = <10>; > + backend at 10{ > + compatible = "i2c-slave-mqueue"; > + reg = <(0x10 | I2C_OWN_SLAVE_ADDRESS)>; > + }; > + }; > + target-port at 2 { > + mode = "gpio"; > + pullup = "disabled"; > + }; > + target-port at 3 { > + mode = "disabled"; > + pullup = "disabled"; > + }; > + }; > + }; > + > + - | > + i3c-master at d040000 { > + #address-cells = <1>; > + #size-cells = <0>; > + > + hub at 70,3C000000100 { > + reg = <0x70 0x3C0 0x00000100>; > + assigned-address = <0x70>; > + dcr = <0xC2>; > + > + cp0-ldo-en = "disabled"; > + cp1-ldo-en = "enabled"; > + cp0-ldo-volt = "1.0V"; > + cp1-ldo-volt = "1.1V"; > + tp0145-ldo-en = "enabled"; > + tp2367-ldo-en = "disabled"; > + tp0145-ldo-volt = "1.2V"; > + tp2367-ldo-volt = "1.8V"; > + tp0145-pullup = "2k"; > + tp2367-pullup = "500R"; > + tp0145-io-strength = "50Ohms"; > + tp2367-io-strength = "30Ohms"; > + cp0-io-strength = "20Ohms"; > + cp1-io-strength = "40Ohms"; > + > + target-port at 0 { > + mode = "i3c"; > + pullup = "enabled"; > + always-enable; > + }; > + target-port at 1 { > + mode = "smbus"; > + pullup = "enabled"; > + backend at 12{ > + compatible = "i2c-slave-mqueue"; > + reg = <(0x12 | I2C_OWN_SLAVE_ADDRESS)>; > + }; > + }; > + target-port at 2 { > + mode = "gpio"; > + pullup = "disabled"; > + }; > + target-port at 3 { > + mode = "disabled"; > + pullup = "disabled"; > + }; > + }; > + }; > diff --git a/MAINTAINERS b/MAINTAINERS > index 2fb1c75afd16..71ee5071ac0f 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -12214,6 +12214,12 @@ S: Supported > F: Documentation/devicetree/bindings/i3c/renesas,i3c.yaml > F: drivers/i3c/master/renesas-i3c.c > > +I3C HUB DRIVER FOR REALTEK RTS490X > +M: zain_zhou > +S: Maintained > +F: Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > +F: drivers/staging/rts490x/ > + > I3C DRIVER FOR SYNOPSYS DESIGNWARE > S: Orphan > F: Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > -- > 2.34.1 > From Frank.li at nxp.com Mon May 4 13:42:26 2026 From: Frank.li at nxp.com (Frank Li) Date: Mon, 4 May 2026 16:42:26 -0400 Subject: [PATCH] i3c: Consistently define pci_device_ids using named initializers In-Reply-To: <20260504143324.2122737-2-u.kleine-koenig@baylibre.com> References: <20260504143324.2122737-2-u.kleine-koenig@baylibre.com> Message-ID: On Mon, May 04, 2026 at 04:33:15PM +0200, Uwe Kleine-K?nig (The Capable Hub) wrote: > The .driver_data member of the various struct pci_device_id arrays were > initialized by list expressions. This isn't easily readable if you're > not into PCI. Using named initializers is more explicit and thus easier > to parse. > > This change doesn't introduce changes to the compiled pci_device_id > arrays. Tested on x86 and arm64. > > Signed-off-by: Uwe Kleine-K?nig (The Capable Hub) > --- > Hello, > > The secret plan is to make struct pci_device_id::driver_data an > anonymous union (similar to > https://lore.kernel.org/all/cover.1776579304.git.u.kleine-koenig at baylibre.com/) > and that requires named initializers. But IMHO it's also a nice cleanup > on its own. > > The anonymous union will allow changes like the following: > > - { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info }, I think it is good. Can you directly change to to { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info } I think use anonymous union {.driver_data; .driver_data_ptr} don't impact the current drivers. Frank > > (together with the respective change in the code when the value is > used). This gets rid of a bunch of casts and thus slightly improves type > safety. > > Best regards > Uwe > > .../master/mipi-i3c-hci/mipi-i3c-hci-pci.c | 22 +++++++++---------- > 1 file changed, 11 insertions(+), 11 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 9468786fb853..5a9e2a43eff8 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 > @@ -461,21 +461,21 @@ static const struct dev_pm_ops mipi_i3c_hci_pci_pm_ops = { > > static const struct pci_device_id mipi_i3c_hci_pci_devices[] = { > /* Wildcat Lake-U */ > - { PCI_VDEVICE(INTEL, 0x4d7c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0x4d6f), (kernel_ulong_t)&intel_si_2_info}, > + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0x4d6f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, > /* Panther Lake-H */ > - { PCI_VDEVICE(INTEL, 0xe37c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0xe36f), (kernel_ulong_t)&intel_si_2_info}, > + { PCI_VDEVICE(INTEL, 0xe37c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0xe36f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, > /* Panther Lake-P */ > - { PCI_VDEVICE(INTEL, 0xe47c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0xe46f), (kernel_ulong_t)&intel_si_2_info}, > + { PCI_VDEVICE(INTEL, 0xe47c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0xe46f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, > /* Nova Lake-S */ > - { PCI_VDEVICE(INTEL, 0x6e2c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0x6e2d), (kernel_ulong_t)&intel_mi_2_info}, > + { PCI_VDEVICE(INTEL, 0x6e2c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0x6e2d), .driver_data = (kernel_ulong_t)&intel_mi_2_info }, > /* Nova Lake-H */ > - { PCI_VDEVICE(INTEL, 0xd37c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0xd36f), (kernel_ulong_t)&intel_mi_2_info}, > - { }, > + { PCI_VDEVICE(INTEL, 0xd37c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0xd36f), .driver_data = (kernel_ulong_t)&intel_mi_2_info }, > + { } > }; > MODULE_DEVICE_TABLE(pci, mipi_i3c_hci_pci_devices); > > > base-commit: 254f49634ee16a731174d2ae34bc50bd5f45e731 > -- > 2.47.3 > From manikandan.m at microchip.com Tue May 5 00:13:22 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Tue, 5 May 2026 12:43:22 +0530 Subject: [PATCH v5 0/5] Add microchip sama7d65 SoC I3C support Message-ID: <20260505071327.125787-1-manikandan.m@microchip.com> Add support for microchip sama7d65 SoC I3C master only IP which is based on mipi-i3c-hci from synopsys implementing version 1.0 specification. The platform specific changes are integrated in the mipi-i3c-hci driver using existing quirks I3C in master mode supports up to 12.5MHz, SDR mode data transfer in mixed bus mode (I2C and I3C target devices on same i3c bus). Durai Manickam KR (3): clk: at91: sama7d65: add peripheral clock for I3C ARM: dts: microchip: add I3C controller ARM: configs: at91: sama7: add sama7d65 i3c-hci Manikandan Muralidharan (2): dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk .../devicetree/bindings/i3c/mipi-i3c-hci.yaml | 27 ++++++++++++++++--- arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++ arch/arm/configs/sama7_defconfig | 2 ++ drivers/clk/at91/sama7d65.c | 1 + drivers/i3c/master/mipi-i3c-hci/core.c | 10 +++++++ 5 files changed, 44 insertions(+), 4 deletions(-) -- 2.25.1 From manikandan.m at microchip.com Tue May 5 00:13:23 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Tue, 5 May 2026 12:43:23 +0530 Subject: [PATCH v5 1/5] dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible In-Reply-To: <20260505071327.125787-1-manikandan.m@microchip.com> References: <20260505071327.125787-1-manikandan.m@microchip.com> Message-ID: <20260505071327.125787-2-manikandan.m@microchip.com> Add the microchip,sama7d65-i3c-hci compatible string to the MIPI I3C HCI binding. The Microchip SAMA7D65 I3C controller is based on the MIPI HCI specification but requires two clocks, so add a conditional constraint when this compatible is present. Signed-off-by: Manikandan Muralidharan --- Changes in v5: - drop min/maxItems around clock - use else clause - cosmetic fixes Changes in v4: - Define and describe the clock in the top-level properties --- .../devicetree/bindings/i3c/mipi-i3c-hci.yaml | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml index 39bb1a1784c9..d488fb420945 100644 --- a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml +++ b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml @@ -9,9 +9,6 @@ title: MIPI I3C HCI maintainers: - Nicolas Pitre -allOf: - - $ref: /schemas/i3c/i3c.yaml# - description: | MIPI I3C Host Controller Interface @@ -28,9 +25,17 @@ description: | properties: compatible: - const: mipi-i3c-hci + enum: + - mipi-i3c-hci + - microchip,sama7d65-i3c-hci reg: maxItems: 1 + + clocks: + items: + - description: Peripheral bus clock + - description: System Generic clock + interrupts: maxItems: 1 @@ -39,6 +44,20 @@ required: - reg - interrupts +allOf: + - $ref: /schemas/i3c/i3c.yaml# + - if: + properties: + compatible: + contains: + const: microchip,sama7d65-i3c-hci + then: + required: + - clocks + else: + properties: + clocks: false + unevaluatedProperties: false examples: -- 2.25.1 From manikandan.m at microchip.com Tue May 5 00:13:24 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Tue, 5 May 2026 12:43:24 +0530 Subject: [PATCH v5 2/5] clk: at91: sama7d65: add peripheral clock for I3C In-Reply-To: <20260505071327.125787-1-manikandan.m@microchip.com> References: <20260505071327.125787-1-manikandan.m@microchip.com> Message-ID: <20260505071327.125787-3-manikandan.m@microchip.com> From: Durai Manickam KR Add peripheral clock description for I3C. Signed-off-by: Durai Manickam KR Signed-off-by: Manikandan Muralidharan --- changes in v3: - Fixed indentation issues --- drivers/clk/at91/sama7d65.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/clk/at91/sama7d65.c b/drivers/clk/at91/sama7d65.c index 7dee2b160ffb..ba8ff413fa2c 100644 --- a/drivers/clk/at91/sama7d65.c +++ b/drivers/clk/at91/sama7d65.c @@ -677,6 +677,7 @@ static struct { { .n = "uhphs_clk", .p = PCK_PARENT_HW_MCK5, .id = 101, }, { .n = "dsi_clk", .p = PCK_PARENT_HW_MCK3, .id = 103, }, { .n = "lvdsc_clk", .p = PCK_PARENT_HW_MCK3, .id = 104, }, + { .n = "i3cc_clk", .p = PCK_PARENT_HW_MCK8, .id = 105, }, }; /* -- 2.25.1 From manikandan.m at microchip.com Tue May 5 00:13:25 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Tue, 5 May 2026 12:43:25 +0530 Subject: [PATCH v5 3/5] i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk In-Reply-To: <20260505071327.125787-1-manikandan.m@microchip.com> References: <20260505071327.125787-1-manikandan.m@microchip.com> Message-ID: <20260505071327.125787-4-manikandan.m@microchip.com> Add support for microchip sama7d65 SoC I3C HCI master only IP with additional clock support to enable bulk clock acquisition Signed-off-by: Manikandan Muralidharan --- Changes in v5: - Remove HCI_QUIRK_CLK_SUPPORT quirk and call devm_clk_bulk_get_all_enabled unconditionally Changes in v4: - Remove the clock index variable MCHP_I3C_CLK_IDX Changes in v3: - Make use of existing HCI_QUIRK_* code base - Introduce HCI_QUIRK_CLK_SUPPORT to handle/enable the required Peripheral and system generic clk in bulk Changes in v2: - Platform specific changes are integrated in the existing mipi-i3c-hci driver by introducing separate MCHP_HCI_QUIRK_* quirks and vendor specific quirk files --- drivers/i3c/master/mipi-i3c-hci/core.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index b781dbed2165..20d32a9eb62c 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -8,6 +8,7 @@ */ #include +#include #include #include #include @@ -970,6 +971,7 @@ static int i3c_hci_probe(struct platform_device *pdev) { const struct mipi_i3c_hci_platform_data *pdata = pdev->dev.platform_data; struct i3c_hci *hci; + struct clk_bulk_data *clks; int irq, ret; hci = devm_kzalloc(&pdev->dev, sizeof(*hci), GFP_KERNEL); @@ -1001,6 +1003,11 @@ static int i3c_hci_probe(struct platform_device *pdev) if (!hci->quirks && platform_get_device_id(pdev)) hci->quirks = platform_get_device_id(pdev)->driver_data; + ret = devm_clk_bulk_get_all_enabled(&pdev->dev, &clks); + if (ret < 0) + return dev_err_probe(&pdev->dev, ret, + "Failed to get clocks\n"); + ret = i3c_hci_init(hci); if (ret) return ret; @@ -1031,6 +1038,9 @@ static void i3c_hci_remove(struct platform_device *pdev) static const __maybe_unused struct of_device_id i3c_hci_of_match[] = { { .compatible = "mipi-i3c-hci", }, + { .compatible = "microchip,sama7d65-i3c-hci", + .data = (void *)(HCI_QUIRK_PIO_MODE | HCI_QUIRK_OD_PP_TIMING | + HCI_QUIRK_RESP_BUF_THLD) }, {}, }; MODULE_DEVICE_TABLE(of, i3c_hci_of_match); -- 2.25.1 From manikandan.m at microchip.com Tue May 5 00:13:26 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Tue, 5 May 2026 12:43:26 +0530 Subject: [PATCH v5 4/5] ARM: dts: microchip: add I3C controller In-Reply-To: <20260505071327.125787-1-manikandan.m@microchip.com> References: <20260505071327.125787-1-manikandan.m@microchip.com> Message-ID: <20260505071327.125787-5-manikandan.m@microchip.com> From: Durai Manickam KR Add I3C controller for sama7d65 SoC. Signed-off-by: Durai Manickam KR Signed-off-by: Manikandan Muralidharan --- Changes in v3: - Remove clock-names property as driver enables the clk in bulk --- arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/arch/arm/boot/dts/microchip/sama7d65.dtsi b/arch/arm/boot/dts/microchip/sama7d65.dtsi index 67253bbc08df..ec200848c153 100644 --- a/arch/arm/boot/dts/microchip/sama7d65.dtsi +++ b/arch/arm/boot/dts/microchip/sama7d65.dtsi @@ -1055,5 +1055,13 @@ gic: interrupt-controller at e8c11000 { #address-cells = <0>; interrupt-controller; }; + + i3c: i3c at e9000000 { + compatible = "microchip,sama7d65-i3c-hci"; + reg = <0xe9000000 0x300>; + interrupts = ; + clocks = <&pmc PMC_TYPE_PERIPHERAL 105>, <&pmc PMC_TYPE_GCK 105>; + status = "disabled"; + }; }; }; -- 2.25.1 From manikandan.m at microchip.com Tue May 5 00:13:27 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Tue, 5 May 2026 12:43:27 +0530 Subject: [PATCH v5 5/5] ARM: configs: at91: sama7: add sama7d65 i3c-hci In-Reply-To: <20260505071327.125787-1-manikandan.m@microchip.com> References: <20260505071327.125787-1-manikandan.m@microchip.com> Message-ID: <20260505071327.125787-6-manikandan.m@microchip.com> From: Durai Manickam KR Enable the configs needed for I3C framework and microchip sama7d65 i3c-hci driver. Signed-off-by: Durai Manickam KR Signed-off-by: Manikandan Muralidharan --- arch/arm/configs/sama7_defconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm/configs/sama7_defconfig b/arch/arm/configs/sama7_defconfig index e52f671ccec4..6470c7d3fe8a 100644 --- a/arch/arm/configs/sama7_defconfig +++ b/arch/arm/configs/sama7_defconfig @@ -117,6 +117,8 @@ CONFIG_HW_RANDOM=y CONFIG_I2C=y CONFIG_I2C_CHARDEV=y CONFIG_I2C_AT91=y +CONFIG_I3C=y +CONFIG_MIPI_I3C_HCI=y CONFIG_SPI=y CONFIG_SPI_ATMEL=y CONFIG_SPI_ATMEL_QUADSPI=y -- 2.25.1 From adrian.hunter at intel.com Tue May 5 02:20:46 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 5 May 2026 12:20:46 +0300 Subject: [PATCH] i3c: Consistently define pci_device_ids using named initializers In-Reply-To: <20260504143324.2122737-2-u.kleine-koenig@baylibre.com> References: <20260504143324.2122737-2-u.kleine-koenig@baylibre.com> Message-ID: <79e04591-4263-4f95-ac5e-59e45170d79d@intel.com> On 04/05/2026 17:33, Uwe Kleine-K?nig (The Capable Hub) wrote: > The .driver_data member of the various struct pci_device_id arrays were > initialized by list expressions. This isn't easily readable if you're > not into PCI. Using named initializers is more explicit and thus easier > to parse. > > This change doesn't introduce changes to the compiled pci_device_id > arrays. Tested on x86 and arm64. > > Signed-off-by: Uwe Kleine-K?nig (The Capable Hub) Reviewed-by: Adrian Hunter > --- > Hello, > > The secret plan is to make struct pci_device_id::driver_data an > anonymous union (similar to > https://lore.kernel.org/all/cover.1776579304.git.u.kleine-koenig at baylibre.com/) > and that requires named initializers. But IMHO it's also a nice cleanup > on its own. > > The anonymous union will allow changes like the following: > > - { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info }, > > (together with the respective change in the code when the value is > used). This gets rid of a bunch of casts and thus slightly improves type > safety. > > Best regards > Uwe > > .../master/mipi-i3c-hci/mipi-i3c-hci-pci.c | 22 +++++++++---------- > 1 file changed, 11 insertions(+), 11 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 9468786fb853..5a9e2a43eff8 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 > @@ -461,21 +461,21 @@ static const struct dev_pm_ops mipi_i3c_hci_pci_pm_ops = { > > static const struct pci_device_id mipi_i3c_hci_pci_devices[] = { > /* Wildcat Lake-U */ > - { PCI_VDEVICE(INTEL, 0x4d7c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0x4d6f), (kernel_ulong_t)&intel_si_2_info}, > + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0x4d6f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, > /* Panther Lake-H */ > - { PCI_VDEVICE(INTEL, 0xe37c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0xe36f), (kernel_ulong_t)&intel_si_2_info}, > + { PCI_VDEVICE(INTEL, 0xe37c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0xe36f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, > /* Panther Lake-P */ > - { PCI_VDEVICE(INTEL, 0xe47c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0xe46f), (kernel_ulong_t)&intel_si_2_info}, > + { PCI_VDEVICE(INTEL, 0xe47c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0xe46f), .driver_data = (kernel_ulong_t)&intel_si_2_info }, > /* Nova Lake-S */ > - { PCI_VDEVICE(INTEL, 0x6e2c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0x6e2d), (kernel_ulong_t)&intel_mi_2_info}, > + { PCI_VDEVICE(INTEL, 0x6e2c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0x6e2d), .driver_data = (kernel_ulong_t)&intel_mi_2_info }, > /* Nova Lake-H */ > - { PCI_VDEVICE(INTEL, 0xd37c), (kernel_ulong_t)&intel_mi_1_info}, > - { PCI_VDEVICE(INTEL, 0xd36f), (kernel_ulong_t)&intel_mi_2_info}, > - { }, > + { PCI_VDEVICE(INTEL, 0xd37c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > + { PCI_VDEVICE(INTEL, 0xd36f), .driver_data = (kernel_ulong_t)&intel_mi_2_info }, > + { } > }; > MODULE_DEVICE_TABLE(pci, mipi_i3c_hci_pci_devices); > > > base-commit: 254f49634ee16a731174d2ae34bc50bd5f45e731 From adrian.hunter at intel.com Tue May 5 06:19:09 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 5 May 2026 16:19:09 +0300 Subject: [PATCH v5 3/5] i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk In-Reply-To: <20260505071327.125787-4-manikandan.m@microchip.com> References: <20260505071327.125787-1-manikandan.m@microchip.com> <20260505071327.125787-4-manikandan.m@microchip.com> Message-ID: <13607cc5-b724-4fbc-8595-8fd5c7f1b4c3@intel.com> On 05/05/2026 10:13, Manikandan Muralidharan wrote: > Add support for microchip sama7d65 SoC I3C HCI master only IP > with additional clock support to enable bulk clock acquisition > > Signed-off-by: Manikandan Muralidharan One minor cosmetic comment below, nevertheless: Reviewed-by: Adrian Hunter > --- > Changes in v5: > - Remove HCI_QUIRK_CLK_SUPPORT quirk and call > devm_clk_bulk_get_all_enabled unconditionally > > Changes in v4: > - Remove the clock index variable MCHP_I3C_CLK_IDX > > Changes in v3: > - Make use of existing HCI_QUIRK_* code base > - Introduce HCI_QUIRK_CLK_SUPPORT to handle/enable the required Peripheral > and system generic clk in bulk > > Changes in v2: > - Platform specific changes are integrated in the existing mipi-i3c-hci > driver by introducing separate MCHP_HCI_QUIRK_* quirks and vendor > specific quirk files > --- > drivers/i3c/master/mipi-i3c-hci/core.c | 10 ++++++++++ > 1 file changed, 10 insertions(+) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c > index b781dbed2165..20d32a9eb62c 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/core.c > +++ b/drivers/i3c/master/mipi-i3c-hci/core.c > @@ -8,6 +8,7 @@ > */ > > #include > +#include > #include > #include > #include > @@ -970,6 +971,7 @@ static int i3c_hci_probe(struct platform_device *pdev) > { > const struct mipi_i3c_hci_platform_data *pdata = pdev->dev.platform_data; > struct i3c_hci *hci; > + struct clk_bulk_data *clks; If you roll a new version of this patch, prefer to arrange local variable definitions in descending order of line length e.g. const struct mipi_i3c_hci_platform_data *pdata = pdev->dev.platform_data; struct clk_bulk_data *clks; struct i3c_hci *hci; > int irq, ret; > > hci = devm_kzalloc(&pdev->dev, sizeof(*hci), GFP_KERNEL); > @@ -1001,6 +1003,11 @@ static int i3c_hci_probe(struct platform_device *pdev) > if (!hci->quirks && platform_get_device_id(pdev)) > hci->quirks = platform_get_device_id(pdev)->driver_data; > > + ret = devm_clk_bulk_get_all_enabled(&pdev->dev, &clks); > + if (ret < 0) > + return dev_err_probe(&pdev->dev, ret, > + "Failed to get clocks\n"); > + > ret = i3c_hci_init(hci); > if (ret) > return ret; > @@ -1031,6 +1038,9 @@ static void i3c_hci_remove(struct platform_device *pdev) > > static const __maybe_unused struct of_device_id i3c_hci_of_match[] = { > { .compatible = "mipi-i3c-hci", }, > + { .compatible = "microchip,sama7d65-i3c-hci", > + .data = (void *)(HCI_QUIRK_PIO_MODE | HCI_QUIRK_OD_PP_TIMING | > + HCI_QUIRK_RESP_BUF_THLD) }, > {}, > }; > MODULE_DEVICE_TABLE(of, i3c_hci_of_match); From robh at kernel.org Tue May 5 11:15:02 2026 From: robh at kernel.org (Rob Herring (Arm)) Date: Tue, 5 May 2026 13:15:02 -0500 Subject: [PATCH v9 3/7] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support In-Reply-To: <20260420105222.1562243-4-lakshay.piplani@nxp.com> References: <20260420105222.1562243-1-lakshay.piplani@nxp.com> <20260420105222.1562243-4-lakshay.piplani@nxp.com> Message-ID: <177800490174.3674177.12021771110967093852.robh@kernel.org> On Mon, 20 Apr 2026 16:22:18 +0530, Lakshay Piplani wrote: > From: Aman Kumar Pandey > > Add bindings for the NXP P3H2x4x (P3H2440/P3H2441/P3H2840/P3H2841) > multiport I3C hub family. These devices connect to a host via > I3C/I2C/SMBus and allow communication with multiple downstream > peripherals. > > Signed-off-by: Aman Kumar Pandey > Signed-off-by: Vikash Bansal > Signed-off-by: Lakshay Piplani > > --- > Changes in v9: > - Referenced i3c.yaml and i2c-controller.yaml for child nodes > - Dropped unnecessary #address-cells and #size-cells from child nodes > > Changes in v8: > - Add compatible in i3c example > > Changes in v7: > - Fix schema validation issues > - Adjust required properties > - Add I2C example > > Changes in v6: > - Use a vendor prefix for the attributes > > Changes in v5: > - Removed SW properties: cp0-ldo-microvolt,cp1-ldo-microvolt, > tp0145-ldo-microvolt, tp2367-ldo-microvolt > - Changed supply entries and its descriptions > > Changes in v4: > - Fixed DT binding check warning > - Removed SW properties: ibi-enable, local-dev, and always-enable > > Changes in v3: > - Added MFD (Multi-Function Device) support for I3C hub and on-die regulator > - Added Regulator supply node > > Changes in v2: > - Fixed DT binding check warning > - Revised logic for parsing DTS nodes > --- > --- > .../devicetree/bindings/i3c/nxp,p3h2840.yaml | 291 ++++++++++++++++++ > MAINTAINERS | 9 + > 2 files changed, 300 insertions(+) > create mode 100644 Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml > Reviewed-by: Rob Herring (Arm) From Frank.li at nxp.com Tue May 5 11:48:52 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 5 May 2026 14:48:52 -0400 Subject: [PATCH] i3c: Consistently define pci_device_ids using named initializers In-Reply-To: References: <20260504143324.2122737-2-u.kleine-koenig@baylibre.com> Message-ID: On Tue, May 05, 2026 at 07:50:16AM +0200, Uwe Kleine-K?nig (The Capable Hub) wrote: > On Mon, May 04, 2026 at 04:42:26PM -0400, Frank Li wrote: > > On Mon, May 04, 2026 at 04:33:15PM +0200, Uwe Kleine-K?nig (The Capable Hub) wrote: > > > The .driver_data member of the various struct pci_device_id arrays were > > > initialized by list expressions. This isn't easily readable if you're > > > not into PCI. Using named initializers is more explicit and thus easier > > > to parse. > > > > > > This change doesn't introduce changes to the compiled pci_device_id > > > arrays. Tested on x86 and arm64. > > > > > > Signed-off-by: Uwe Kleine-K?nig (The Capable Hub) > > > --- > > > Hello, > > > > > > The secret plan is to make struct pci_device_id::driver_data an > > > anonymous union (similar to > > > https://lore.kernel.org/all/cover.1776579304.git.u.kleine-koenig at baylibre.com/) > > > and that requires named initializers. But IMHO it's also a nice cleanup > > > on its own. > > > > > > The anonymous union will allow changes like the following: > > > > > > - { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > > > + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info }, > > > > I think it is good. Can you directly change to to > > { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info } > > > > I think use anonymous union {.driver_data; .driver_data_ptr} don't impact > > the current drivers. > > I cannot because pci_device_id with the union cannot be initialized > using > > { PCI_VDEVICE(INTEL, 0x4d7c), (kernel_ulong_t)&intel_mi_1_info }, > > That's why all drivers must be adapted first to use named initializers. Reviewed-by: Frank Li > > Best regards > Uwe From manikandan.m at microchip.com Thu May 7 01:48:00 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Thu, 7 May 2026 14:18:00 +0530 Subject: [PATCH v6 0/5] Add microchip sama7d65 SoC I3C support Message-ID: <20260507084805.481737-1-manikandan.m@microchip.com> Add support for microchip sama7d65 SoC I3C master only IP which is based on mipi-i3c-hci from synopsys implementing version 1.0 specification. The platform specific changes are integrated in the mipi-i3c-hci driver using existing quirks I3C in master mode supports up to 12.5MHz, SDR mode data transfer in mixed bus mode (I2C and I3C target devices on same i3c bus). Please refer to the individual patches for changelogs. base-commit: 8ab992f815d6736b5c7a6f5fd7bfe7bc106bb3dc Durai Manickam KR (2): clk: at91: sama7d65: add peripheral clock for I3C ARM: dts: microchip: add I3C controller Manikandan Muralidharan (3): dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk ARM: configs: at91: sama7: add sama7d65 i3c-hci .../devicetree/bindings/i3c/mipi-i3c-hci.yaml | 27 ++++++++++++++++--- arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++ arch/arm/configs/sama7_defconfig | 2 ++ drivers/clk/at91/sama7d65.c | 1 + drivers/i3c/master/mipi-i3c-hci/core.c | 10 +++++++ 5 files changed, 44 insertions(+), 4 deletions(-) -- 2.25.1 From manikandan.m at microchip.com Thu May 7 01:48:01 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Thu, 7 May 2026 14:18:01 +0530 Subject: [PATCH v6 1/5] dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible In-Reply-To: <20260507084805.481737-1-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> Message-ID: <20260507084805.481737-2-manikandan.m@microchip.com> Add the microchip,sama7d65-i3c-hci compatible string to the MIPI I3C HCI binding. The Microchip SAMA7D65 I3C controller is based on the MIPI HCI specification but requires two clocks, so add a conditional constraint when this compatible is present. Acked-by: Conor Dooley Signed-off-by: Manikandan Muralidharan --- Changes in v5: - drop min/maxItems around clock - use else clause - cosmetic fixes Changes in v4: - Define and describe the clock in the top-level properties .../devicetree/bindings/i3c/mipi-i3c-hci.yaml | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml index 39bb1a1784c9..d488fb420945 100644 --- a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml +++ b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml @@ -9,9 +9,6 @@ title: MIPI I3C HCI maintainers: - Nicolas Pitre -allOf: - - $ref: /schemas/i3c/i3c.yaml# - description: | MIPI I3C Host Controller Interface @@ -28,9 +25,17 @@ description: | properties: compatible: - const: mipi-i3c-hci + enum: + - mipi-i3c-hci + - microchip,sama7d65-i3c-hci reg: maxItems: 1 + + clocks: + items: + - description: Peripheral bus clock + - description: System Generic clock + interrupts: maxItems: 1 @@ -39,6 +44,20 @@ required: - reg - interrupts +allOf: + - $ref: /schemas/i3c/i3c.yaml# + - if: + properties: + compatible: + contains: + const: microchip,sama7d65-i3c-hci + then: + required: + - clocks + else: + properties: + clocks: false + unevaluatedProperties: false examples: -- 2.25.1 From manikandan.m at microchip.com Thu May 7 01:48:02 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Thu, 7 May 2026 14:18:02 +0530 Subject: [PATCH v6 2/5] clk: at91: sama7d65: add peripheral clock for I3C In-Reply-To: <20260507084805.481737-1-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> Message-ID: <20260507084805.481737-3-manikandan.m@microchip.com> From: Durai Manickam KR Add peripheral clock description for I3C. Signed-off-by: Durai Manickam KR Signed-off-by: Manikandan Muralidharan --- Changes in v3: - Fixed indentation issues drivers/clk/at91/sama7d65.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/clk/at91/sama7d65.c b/drivers/clk/at91/sama7d65.c index 7dee2b160ffb..ba8ff413fa2c 100644 --- a/drivers/clk/at91/sama7d65.c +++ b/drivers/clk/at91/sama7d65.c @@ -677,6 +677,7 @@ static struct { { .n = "uhphs_clk", .p = PCK_PARENT_HW_MCK5, .id = 101, }, { .n = "dsi_clk", .p = PCK_PARENT_HW_MCK3, .id = 103, }, { .n = "lvdsc_clk", .p = PCK_PARENT_HW_MCK3, .id = 104, }, + { .n = "i3cc_clk", .p = PCK_PARENT_HW_MCK8, .id = 105, }, }; /* -- 2.25.1 From manikandan.m at microchip.com Thu May 7 01:48:03 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Thu, 7 May 2026 14:18:03 +0530 Subject: [PATCH v6 3/5] i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk In-Reply-To: <20260507084805.481737-1-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> Message-ID: <20260507084805.481737-4-manikandan.m@microchip.com> Add support for microchip sama7d65 SoC I3C HCI master only IP with additional clock support to enable bulk clock acquisition Reviewed-by: Adrian Hunter Signed-off-by: Manikandan Muralidharan --- Changes in v6: - Reorder local variable definitions in i3c_hci_probe in descending order of line length Changes in v5: - Remove HCI_QUIRK_CLK_SUPPORT quirk and call devm_clk_bulk_get_all_enabled unconditionally Changes in v4: - Remove the clock index variable MCHP_I3C_CLK_IDX Changes in v3: - Make use of existing HCI_QUIRK_* code base - Introduce HCI_QUIRK_CLK_SUPPORT to handle/enable the required Peripheral and system generic clk in bulk Changes in v2: - Platform specific changes are integrated in the existing mipi-i3c-hci driver by introducing separate MCHP_HCI_QUIRK_* quirks and vendor specific quirk files drivers/i3c/master/mipi-i3c-hci/core.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index b781dbed2165..093a85eedfcb 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -8,6 +8,7 @@ */ #include +#include #include #include #include @@ -969,6 +970,7 @@ static int i3c_hci_init(struct i3c_hci *hci) static int i3c_hci_probe(struct platform_device *pdev) { const struct mipi_i3c_hci_platform_data *pdata = pdev->dev.platform_data; + struct clk_bulk_data *clks; struct i3c_hci *hci; int irq, ret; @@ -1001,6 +1003,11 @@ static int i3c_hci_probe(struct platform_device *pdev) if (!hci->quirks && platform_get_device_id(pdev)) hci->quirks = platform_get_device_id(pdev)->driver_data; + ret = devm_clk_bulk_get_all_enabled(&pdev->dev, &clks); + if (ret < 0) + return dev_err_probe(&pdev->dev, ret, + "Failed to get clocks\n"); + ret = i3c_hci_init(hci); if (ret) return ret; @@ -1031,6 +1038,9 @@ static void i3c_hci_remove(struct platform_device *pdev) static const __maybe_unused struct of_device_id i3c_hci_of_match[] = { { .compatible = "mipi-i3c-hci", }, + { .compatible = "microchip,sama7d65-i3c-hci", + .data = (void *)(HCI_QUIRK_PIO_MODE | HCI_QUIRK_OD_PP_TIMING | + HCI_QUIRK_RESP_BUF_THLD) }, {}, }; MODULE_DEVICE_TABLE(of, i3c_hci_of_match); -- 2.25.1 From manikandan.m at microchip.com Thu May 7 01:48:04 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Thu, 7 May 2026 14:18:04 +0530 Subject: [PATCH v6 4/5] ARM: dts: microchip: add I3C controller In-Reply-To: <20260507084805.481737-1-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> Message-ID: <20260507084805.481737-5-manikandan.m@microchip.com> From: Durai Manickam KR Add I3C controller for sama7d65 SoC. Signed-off-by: Durai Manickam KR Signed-off-by: Manikandan Muralidharan --- Changes in v3: - Remove clock-names property as driver enables the clk in bulk arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/arch/arm/boot/dts/microchip/sama7d65.dtsi b/arch/arm/boot/dts/microchip/sama7d65.dtsi index 67253bbc08df..ec200848c153 100644 --- a/arch/arm/boot/dts/microchip/sama7d65.dtsi +++ b/arch/arm/boot/dts/microchip/sama7d65.dtsi @@ -1055,5 +1055,13 @@ gic: interrupt-controller at e8c11000 { #address-cells = <0>; interrupt-controller; }; + + i3c: i3c at e9000000 { + compatible = "microchip,sama7d65-i3c-hci"; + reg = <0xe9000000 0x300>; + interrupts = ; + clocks = <&pmc PMC_TYPE_PERIPHERAL 105>, <&pmc PMC_TYPE_GCK 105>; + status = "disabled"; + }; }; }; -- 2.25.1 From manikandan.m at microchip.com Thu May 7 01:48:05 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Thu, 7 May 2026 14:18:05 +0530 Subject: [PATCH v6 5/5] ARM: configs: at91: sama7: add sama7d65 i3c-hci In-Reply-To: <20260507084805.481737-1-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> Message-ID: <20260507084805.481737-6-manikandan.m@microchip.com> Enable the configs needed for I3C framework and microchip sama7d65 i3c-hci driver. Signed-off-by: Durai Manickam KR Signed-off-by: Manikandan Muralidharan --- arch/arm/configs/sama7_defconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm/configs/sama7_defconfig b/arch/arm/configs/sama7_defconfig index e52f671ccec4..6470c7d3fe8a 100644 --- a/arch/arm/configs/sama7_defconfig +++ b/arch/arm/configs/sama7_defconfig @@ -117,6 +117,8 @@ CONFIG_HW_RANDOM=y CONFIG_I2C=y CONFIG_I2C_CHARDEV=y CONFIG_I2C_AT91=y +CONFIG_I3C=y +CONFIG_MIPI_I3C_HCI=y CONFIG_SPI=y CONFIG_SPI_ATMEL=y CONFIG_SPI_ATMEL_QUADSPI=y -- 2.25.1 From Frank.li at nxp.com Thu May 7 09:34:09 2026 From: Frank.li at nxp.com (Frank Li) Date: Thu, 7 May 2026 12:34:09 -0400 Subject: [PATCH v6 3/5] i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk In-Reply-To: <20260507084805.481737-4-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> <20260507084805.481737-4-manikandan.m@microchip.com> Message-ID: On Thu, May 07, 2026 at 02:18:03PM +0530, Manikandan Muralidharan wrote: > Add support for microchip sama7d65 SoC I3C HCI master only IP > with additional clock support to enable bulk clock acquisition add apply the required quirk. > > Reviewed-by: Adrian Hunter > Signed-off-by: Manikandan Muralidharan > --- ... > @@ -1031,6 +1038,9 @@ static void i3c_hci_remove(struct platform_device *pdev) > > static const __maybe_unused struct of_device_id i3c_hci_of_match[] = { > { .compatible = "mipi-i3c-hci", }, > + { .compatible = "microchip,sama7d65-i3c-hci", > + .data = (void *)(HCI_QUIRK_PIO_MODE | HCI_QUIRK_OD_PP_TIMING | > + HCI_QUIRK_RESP_BUF_THLD) }, Now don't prefer directly convert drive data to void *point. ACPI use ulong as driver data. It can be updated later. Frank > {}, > }; > MODULE_DEVICE_TABLE(of, i3c_hci_of_match); > -- > 2.25.1 > From Frank.li at nxp.com Thu May 7 09:34:36 2026 From: Frank.li at nxp.com (Frank Li) Date: Thu, 7 May 2026 12:34:36 -0400 Subject: [PATCH v6 1/5] dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible In-Reply-To: <20260507084805.481737-2-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> <20260507084805.481737-2-manikandan.m@microchip.com> Message-ID: On Thu, May 07, 2026 at 02:18:01PM +0530, Manikandan Muralidharan wrote: > Add the microchip,sama7d65-i3c-hci compatible string to the MIPI I3C > HCI binding. The Microchip SAMA7D65 I3C controller is based on the > MIPI HCI specification but requires two clocks, so add a conditional > constraint when this compatible is present. > > Acked-by: Conor Dooley > Signed-off-by: Manikandan Muralidharan Reviewed-by: Frank Li > --- > Changes in v5: > - drop min/maxItems around clock > - use else clause > - cosmetic fixes > > Changes in v4: > - Define and describe the clock in the top-level properties > > .../devicetree/bindings/i3c/mipi-i3c-hci.yaml | 27 ++++++++++++++++--- > 1 file changed, 23 insertions(+), 4 deletions(-) > > diff --git a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml > index 39bb1a1784c9..d488fb420945 100644 > --- a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml > +++ b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml > @@ -9,9 +9,6 @@ title: MIPI I3C HCI > maintainers: > - Nicolas Pitre > > -allOf: > - - $ref: /schemas/i3c/i3c.yaml# > - > description: | > MIPI I3C Host Controller Interface > > @@ -28,9 +25,17 @@ description: | > > properties: > compatible: > - const: mipi-i3c-hci > + enum: > + - mipi-i3c-hci > + - microchip,sama7d65-i3c-hci > reg: > maxItems: 1 > + > + clocks: > + items: > + - description: Peripheral bus clock > + - description: System Generic clock > + > interrupts: > maxItems: 1 > > @@ -39,6 +44,20 @@ required: > - reg > - interrupts > > +allOf: > + - $ref: /schemas/i3c/i3c.yaml# > + - if: > + properties: > + compatible: > + contains: > + const: microchip,sama7d65-i3c-hci > + then: > + required: > + - clocks > + else: > + properties: > + clocks: false > + > unevaluatedProperties: false > > examples: > -- > 2.25.1 > From jszhang at kernel.org Sun May 10 20:19:42 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 11 May 2026 11:19:42 +0800 Subject: [PATCH v2 0/3] i3c: dw: Add apb reset support Message-ID: <20260511031945.3228-1-jszhang@kernel.org> Add support of apb reset which is to reset the APB interface. The first patch is to document the exisiting reset dt-binding. 2nd patch is to add apb reset dt-binding. The last patch is to add apb reset support. Hi Frank, comments to question "why not name the reset as "apb" instead of "apb_rst": exisiting core reset is named as "core_rst", this is to align with its style. Thanks Since v1: - add dt-binding Jisheng Zhang (3): dt-bindings: i3c: dw: Describe core reset dt-bindings: i3c: dw: Add apb reset i3c: dw: Add apb reset support .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 10 ++++++++++ drivers/i3c/master/dw-i3c-master.c | 7 +++++++ drivers/i3c/master/dw-i3c-master.h | 1 + 3 files changed, 18 insertions(+) -- 2.53.0 From jszhang at kernel.org Sun May 10 20:19:43 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 11 May 2026 11:19:43 +0800 Subject: [PATCH v2 1/3] dt-bindings: i3c: dw: Describe core reset In-Reply-To: <20260511031945.3228-1-jszhang@kernel.org> References: <20260511031945.3228-1-jszhang@kernel.org> Message-ID: <20260511031945.3228-2-jszhang@kernel.org> The core reset support has been in the code from day1, but the dt-binding doesn't exist. Add dt-binding to describe reset property. Signed-off-by: Jisheng Zhang --- .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index e803457d3f55..613dce7757bc 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -35,6 +35,14 @@ properties: - const: core - const: apb + resets: + items: + - description: Reset signal + + reset-names: + items: + - const: core_rst + interrupts: maxItems: 1 -- 2.53.0 From jszhang at kernel.org Sun May 10 20:19:44 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 11 May 2026 11:19:44 +0800 Subject: [PATCH v2 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <20260511031945.3228-1-jszhang@kernel.org> References: <20260511031945.3228-1-jszhang@kernel.org> Message-ID: <20260511031945.3228-3-jszhang@kernel.org> Add dt-binding for support of apb reset which is to reset the APB interface. Signed-off-by: Jisheng Zhang --- Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index 613dce7757bc..2575442b28ff 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -38,10 +38,12 @@ properties: resets: items: - description: Reset signal + - description: APB interface reset signal reset-names: items: - const: core_rst + - const: apb_rst interrupts: maxItems: 1 -- 2.53.0 From jszhang at kernel.org Sun May 10 20:19:45 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 11 May 2026 11:19:45 +0800 Subject: [PATCH v2 3/3] i3c: dw: Add apb reset support In-Reply-To: <20260511031945.3228-1-jszhang@kernel.org> References: <20260511031945.3228-1-jszhang@kernel.org> Message-ID: <20260511031945.3228-4-jszhang@kernel.org> Add support of apb reset which is to reset the APB interface. Signed-off-by: Jisheng Zhang --- drivers/i3c/master/dw-i3c-master.c | 7 +++++++ drivers/i3c/master/dw-i3c-master.h | 1 + 2 files changed, 8 insertions(+) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index 655693a2187e..9de54d584bc3 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1591,6 +1591,11 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, if (IS_ERR(master->core_rst)) return PTR_ERR(master->core_rst); + master->apb_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, + "apb_rst"); + if (IS_ERR(master->apb_rst)) + return PTR_ERR(master->apb_rst); + spin_lock_init(&master->xferqueue.lock); INIT_LIST_HEAD(&master->xferqueue.list); @@ -1765,6 +1770,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->apb_rst); dw_i3c_master_disable_clks(master); pinctrl_pm_select_sleep_state(dev); return 0; @@ -1777,6 +1783,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->apb_rst); 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 c5cb695c16ab..a4ba60043288 100644 --- a/drivers/i3c/master/dw-i3c-master.h +++ b/drivers/i3c/master/dw-i3c-master.h @@ -37,6 +37,7 @@ struct dw_i3c_master { struct dw_i3c_master_caps caps; void __iomem *regs; struct reset_control *core_rst; + struct reset_control *apb_rst; struct clk *core_clk; struct clk *pclk; char version[5]; -- 2.53.0 From frank.li at nxp.com Mon May 11 12:04:07 2026 From: frank.li at nxp.com (Frank Li) Date: Mon, 11 May 2026 19:04:07 +0000 Subject: [EXT] [PATCH v2 0/3] i3c: dw: Add apb reset support In-Reply-To: <20260511031945.3228-1-jszhang@kernel.org> References: <20260511031945.3228-1-jszhang@kernel.org> Message-ID: > > Add support of apb reset which is to reset the APB interface. > The first patch is to document the exisiting reset dt-binding. 2nd patch > is to add apb reset dt-binding. The last patch is to add apb reset > support. > > Hi Frank, > > comments to question "why not name the reset as "apb" instead of "apb_rst": > exisiting core reset is named as "core_rst", this is to align with its > style. You add new bind at path 1. It is new reset name. All needn't "_rst" suffix. Frank > > Thanks > > Since v1: > - add dt-binding > > > Jisheng Zhang (3): > dt-bindings: i3c: dw: Describe core reset > dt-bindings: i3c: dw: Add apb reset > i3c: dw: Add apb reset support > > .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 10 ++++++++++ > drivers/i3c/master/dw-i3c-master.c | 7 +++++++ > drivers/i3c/master/dw-i3c-master.h | 1 + > 3 files changed, 18 insertions(+) > > -- > 2.53.0 From Frank.li at nxp.com Mon May 11 12:27:13 2026 From: Frank.li at nxp.com (Frank Li) Date: Mon, 11 May 2026 15:27:13 -0400 Subject: [PATCH v2 1/3] dt-bindings: i3c: dw: Describe core reset In-Reply-To: <20260511031945.3228-2-jszhang@kernel.org> References: <20260511031945.3228-1-jszhang@kernel.org> <20260511031945.3228-2-jszhang@kernel.org> Message-ID: On Mon, May 11, 2026 at 11:19:43AM +0800, Jisheng Zhang wrote: > The core reset support has been in the code from day1, but the > dt-binding doesn't exist. Add dt-binding to describe reset property. > > Signed-off-by: Jisheng Zhang > --- > .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 8 ++++++++ > 1 file changed, 8 insertions(+) > > diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > index e803457d3f55..613dce7757bc 100644 > --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > @@ -35,6 +35,14 @@ properties: > - const: core > - const: apb > > + resets: > + items: > + - description: Reset signal > + > + reset-names: > + items: > + - const: core_rst > + Remove "_rst" suffix, it is totally reduncted. Frank > interrupts: > maxItems: 1 > > -- > 2.53.0 > From adrian.hunter at intel.com Tue May 12 05:17:25 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:25 +0300 Subject: [PATCH 1/8] i3c: master: Make hot-join workqueue freezable to block hot-join during suspend In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-2-adrian.hunter@intel.com> The I3C master workqueue (master->wq) is used to defer work that needs thread context and the bus maintenance lock, most notably Hot Join processing (which calls i3c_master_do_daa() to assign dynamic addresses to newly joined devices). Currently the workqueue keeps running across system suspend, which can race with the suspend path: - do_daa() may execute after the controller has been suspended, issuing bus transactions on a powered-down or otherwise unusable controller. - New I3C devices can be enumerated and added to the bus mid-suspend, registering driver model objects at a point where the I3C subsystem and its consumers are not prepared to handle them. Mark the workqueue WQ_FREEZABLE so its workers are frozen for the duration of system suspend/hibernate and resumed afterwards. This naturally defers any pending or newly queued Hot Join work until the system (and the controller) is fully resumed, closing both races without adding explicit suspend/resume synchronization in the master drivers. Update the kerneldoc for struct i3c_master_controller::wq to reflect that the workqueue is freezable. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 2 +- include/linux/i3c/master.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 5cd4e5da2233..ab11e2d79aab 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -3079,7 +3079,7 @@ int i3c_master_register(struct i3c_master_controller *master, if (ret) goto err_put_dev; - master->wq = alloc_workqueue("%s", WQ_PERCPU, 0, dev_name(parent)); + master->wq = alloc_workqueue("%s", WQ_PERCPU | WQ_FREEZABLE, 0, dev_name(parent)); if (!master->wq) { ret = -ENOMEM; goto err_put_dev; diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 592b646f6134..e6112e5f6608 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -515,7 +515,7 @@ struct i3c_master_controller_ops { * @boardinfo.i2c: list of I2C boardinfo objects * @boardinfo: board-level information attached to devices connected on the bus * @bus: I3C bus exposed by this master - * @wq: workqueue which can be used by master + * @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 * requires taking the bus lock in maintenance, which in turn, can only -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:17:24 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:24 +0300 Subject: [PATCH 0/8] i3c: Hot-Join improvements and MIPI HCI Hot-Join support Message-ID: <20260512121732.406009-1-adrian.hunter@intel.com> Hi This series tightens the I3C core's handling of Hot-Join across system suspend, shutdown and unregister, consolidates the per-driver Hot-Join worker into the core, and finally wires the MIPI I3C HCI driver into the new Hot-Join framework. It applies on top of: https://lore.kernel.org/linux-i3c/20260504113352.38490-1-adrian.hunter at intel.com/ Patches 1-2 fix latent races in the existing Hot-Join machinery (suspend vs. Hot-Join work, and concurrent sysfs writers). Patches 3-5 consolidate the per-driver Hot-Join worker into the core and add proper teardown via an i3c_bus_type shutdown callback. Patch 6 defers driver-model registration of newly discovered devices out of the DAA caller's context, so resume-time DAA does not push device probing into the controller's resume sequence. Patches 7-8 add Hot-Join support to MIPI I3C HCI. Adrian Hunter (8): i3c: master: Make hot-join workqueue freezable to block hot-join during suspend i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock i3c: master: Consolidate Hot-Join DAA work in the core i3c: master: Ensure Hot-Join operations are stopped on shutdown i3c: dw: Drop redundant Hot-Join cancel_work_sync() in shutdown i3c: master: Defer new-device registration out of DAA caller context i3c: master: Export i3c_master_enec_disec_locked() i3c: mipi-i3c-hci: Add Hot-Join support drivers/i3c/master.c | 123 ++++++++++++++++++++++++++------- drivers/i3c/master/dw-i3c-master.c | 15 +--- drivers/i3c/master/dw-i3c-master.h | 2 - drivers/i3c/master/i3c-master-cdns.c | 14 +--- drivers/i3c/master/mipi-i3c-hci/core.c | 50 +++++++++++++- drivers/i3c/master/mipi-i3c-hci/dma.c | 5 ++ drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + drivers/i3c/master/mipi-i3c-hci/pio.c | 5 ++ drivers/i3c/master/svc-i3c-master.c | 14 +--- include/linux/i3c/master.h | 16 ++++- 10 files changed, 174 insertions(+), 71 deletions(-) Regards Adrian From adrian.hunter at intel.com Tue May 12 05:17:26 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:26 +0300 Subject: [PATCH 2/8] i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-3-adrian.hunter@intel.com> i3c_set_hotjoin() dispatches the controller's enable_hotjoin() or disable_hotjoin() op and updates master->hotjoin under i3c_bus_normaluse_lock(). That lock is a read-side acquisition of bus->lock (down_read()), so it does not exclude concurrent callers. The hotjoin sysfs attribute can be opened multiple times, and writes through different opens are not serialized. Two concurrent writers to "hotjoin" can therefore race in i3c_set_hotjoin(), with the controller op and the master->hotjoin store from one call interleaving with the other. The hardware enable/disable state and the value reported by hotjoin_show() can end up out of sync. Take i3c_bus_maintenance_lock() instead. Toggling Hot Join enable changes bus state and is conceptually a maintenance operation, so the write-side acquisition of bus->lock is the appropriate lock and serializes concurrent callers against each other and against other maintenance operations. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index ab11e2d79aab..38ffc8713167 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -649,7 +649,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) return ret; } - i3c_bus_normaluse_lock(&master->bus); + i3c_bus_maintenance_lock(&master->bus); if (enable) ret = master->ops->enable_hotjoin(master); @@ -659,7 +659,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) if (!ret) master->hotjoin = enable; - i3c_bus_normaluse_unlock(&master->bus); + i3c_bus_maintenance_unlock(&master->bus); if ((enable && ret) || (!enable && !ret) || master->rpm_ibi_allowed) i3c_master_rpm_put(master); -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:17:27 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:27 +0300 Subject: [PATCH 3/8] i3c: master: Consolidate Hot-Join DAA work in the core In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-4-adrian.hunter@intel.com> Three master drivers (dw-i3c-master, i3c-master-cdns, svc-i3c-master) each carry an essentially identical Hot-Join handler: a struct work_struct embedded in their private state, a work function that just calls i3c_master_do_daa() on the embedded i3c_master_controller, plus matching INIT_WORK()/cancel_work_sync() boilerplate in probe/remove (and shutdown for dw-i3c). The IBI/ISR paths then queue that work onto master->wq, which already lives in the core. Move this pattern into the I3C core: - Add struct work_struct hj_work to struct i3c_master_controller and initialise it in i3c_master_register() with a core-provided handler i3c_master_hj_work_fn() that performs i3c_master_do_daa(). - Cancel the work in i3c_master_unregister() so all controllers get correct teardown ordering against the workqueue for free. - Export i3c_master_queue_hotjoin() as the single entry point drivers call from their Hot-Join IBI handler. Convert the three existing users to the new API: drop their private hj_work fields, work functions, INIT_WORK() and cancel_work_sync() calls, and replace the queue_work(master->wq, &drv->hj_work) call sites with i3c_master_queue_hotjoin(&drv->base). The dw-i3c shutdown path still needs to flush pending Hot-Join work before tearing down the hardware, so it is updated to cancel master->base.hj_work directly. No functional change intended: the work is still queued on the same master->wq, runs the same i3c_master_do_daa(), and is cancelled at controller teardown. Future Hot-Join improvements now only need to be made in one place. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 21 +++++++++++++++++++++ drivers/i3c/master/dw-i3c-master.c | 15 ++------------- drivers/i3c/master/dw-i3c-master.h | 2 -- drivers/i3c/master/i3c-master-cdns.c | 14 +------------- drivers/i3c/master/svc-i3c-master.c | 14 +------------- include/linux/i3c/master.h | 4 ++++ 6 files changed, 29 insertions(+), 41 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 38ffc8713167..cdb5cb2aa65d 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -633,6 +633,13 @@ static ssize_t i2c_scl_frequency_show(struct device *dev, } static DEVICE_ATTR_RO(i2c_scl_frequency); +static void i3c_master_hj_work_fn(struct work_struct *work) +{ + struct i3c_master_controller *master = container_of(work, typeof(*master), hj_work); + + i3c_master_do_daa(master); +} + static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) { int ret; @@ -711,6 +718,18 @@ int i3c_master_disable_hotjoin(struct i3c_master_controller *master) } EXPORT_SYMBOL_GPL(i3c_master_disable_hotjoin); +/** + * i3c_master_queue_hotjoin - Queue DAA processing after a Hot-Join event + * @master: I3C master object + * + * Queue the hot-join worker on the master's workqueue. + */ +void i3c_master_queue_hotjoin(struct i3c_master_controller *master) +{ + queue_work(master->wq, &master->hj_work); +} +EXPORT_SYMBOL_GPL(i3c_master_queue_hotjoin); + static ssize_t hotjoin_show(struct device *dev, struct device_attribute *da, char *buf) { struct i3c_bus *i3cbus = dev_to_i3cbus(dev); @@ -3084,6 +3103,7 @@ int i3c_master_register(struct i3c_master_controller *master, ret = -ENOMEM; goto err_put_dev; } + INIT_WORK(&master->hj_work, i3c_master_hj_work_fn); ret = i3c_master_bus_init(master); if (ret) @@ -3146,6 +3166,7 @@ EXPORT_SYMBOL_GPL(i3c_master_register); void i3c_master_unregister(struct i3c_master_controller *master) { i3c_bus_notify(&master->bus, I3C_NOTIFY_BUS_REMOVE); + cancel_work_sync(&master->hj_work); if (master->ops->set_dev_nack_retry) device_remove_file(&master->dev, &dev_attr_dev_nack_retry_count); diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index 655693a2187e..eb9a13a73684 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1445,7 +1445,7 @@ static void dw_i3c_master_irq_handle_ibis(struct dw_i3c_master *master) if (IBI_TYPE_SIRQ(reg)) { dw_i3c_master_handle_ibi_sir(master, reg); } else if (IBI_TYPE_HJ(reg)) { - queue_work(master->base.wq, &master->hj_work); + i3c_master_queue_hotjoin(&master->base); } else { len = IBI_QUEUE_STATUS_DATA_LEN(reg); dev_info(&master->base.dev, @@ -1554,14 +1554,6 @@ 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_hj_work(struct work_struct *work) -{ - struct dw_i3c_master *master = - container_of(work, typeof(*master), hj_work); - - i3c_master_do_daa(&master->base); -} - int dw_i3c_common_probe(struct dw_i3c_master *master, struct platform_device *pdev) { @@ -1636,8 +1628,6 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, if (master->quirks & DW_I3C_DISABLE_RUNTIME_PM_QUIRK) pm_runtime_get_noresume(&pdev->dev); - INIT_WORK(&master->hj_work, dw_i3c_hj_work); - device_set_of_node_from_dev(&master->base.i2c.dev, &pdev->dev); ret = i3c_master_register(&master->base, &pdev->dev, &dw_mipi_i3c_ops, false); @@ -1659,7 +1649,6 @@ EXPORT_SYMBOL_GPL(dw_i3c_common_probe); void dw_i3c_common_remove(struct dw_i3c_master *master) { - cancel_work_sync(&master->hj_work); i3c_master_unregister(&master->base); /* Balance pm_runtime_get_noresume() from probe() */ @@ -1804,7 +1793,7 @@ static void dw_i3c_shutdown(struct platform_device *pdev) return; } - cancel_work_sync(&master->hj_work); + cancel_work_sync(&master->base.hj_work); /* Disable interrupts */ writel((u32)~INTR_ALL, master->regs + INTR_STATUS_EN); diff --git a/drivers/i3c/master/dw-i3c-master.h b/drivers/i3c/master/dw-i3c-master.h index c5cb695c16ab..2f029bd36232 100644 --- a/drivers/i3c/master/dw-i3c-master.h +++ b/drivers/i3c/master/dw-i3c-master.h @@ -68,8 +68,6 @@ struct dw_i3c_master { /* platform-specific data */ const struct dw_i3c_platform_ops *platform_ops; - - struct work_struct hj_work; }; struct dw_i3c_platform_ops { diff --git a/drivers/i3c/master/i3c-master-cdns.c b/drivers/i3c/master/i3c-master-cdns.c index 5cfec6761494..6d221596ea35 100644 --- a/drivers/i3c/master/i3c-master-cdns.c +++ b/drivers/i3c/master/i3c-master-cdns.c @@ -398,7 +398,6 @@ struct cdns_i3c_data { }; struct cdns_i3c_master { - struct work_struct hj_work; struct i3c_master_controller base; u32 free_rr_slots; unsigned int maxdevs; @@ -1357,7 +1356,7 @@ static void cnds_i3c_master_demux_ibis(struct cdns_i3c_master *master) case IBIR_TYPE_HJ: WARN_ON(IBIR_XFER_BYTES(ibir) || (ibir & IBIR_ERROR)); - queue_work(master->base.wq, &master->hj_work); + i3c_master_queue_hotjoin(&master->base); break; case IBIR_TYPE_MR: @@ -1528,15 +1527,6 @@ static const struct i3c_master_controller_ops cdns_i3c_master_ops = { .recycle_ibi_slot = cdns_i3c_master_recycle_ibi_slot, }; -static void cdns_i3c_master_hj(struct work_struct *work) -{ - struct cdns_i3c_master *master = container_of(work, - struct cdns_i3c_master, - hj_work); - - i3c_master_do_daa(&master->base); -} - static struct cdns_i3c_data cdns_i3c_devdata = { .thd_delay_ns = 10, }; @@ -1584,7 +1574,6 @@ static int cdns_i3c_master_probe(struct platform_device *pdev) spin_lock_init(&master->xferqueue.lock); INIT_LIST_HEAD(&master->xferqueue.list); - INIT_WORK(&master->hj_work, cdns_i3c_master_hj); writel(0xffffffff, master->regs + MST_IDR); writel(0xffffffff, master->regs + SLV_IDR); ret = devm_request_irq(&pdev->dev, irq, cdns_i3c_master_interrupt, 0, @@ -1627,7 +1616,6 @@ static void cdns_i3c_master_remove(struct platform_device *pdev) { struct cdns_i3c_master *master = platform_get_drvdata(pdev); - cancel_work_sync(&master->hj_work); i3c_master_unregister(&master->base); } diff --git a/drivers/i3c/master/svc-i3c-master.c b/drivers/i3c/master/svc-i3c-master.c index e2d99a3ac07d..62e5666798c8 100644 --- a/drivers/i3c/master/svc-i3c-master.c +++ b/drivers/i3c/master/svc-i3c-master.c @@ -208,7 +208,6 @@ struct svc_i3c_drvdata { * @free_slots: Bit array of available slots * @addrs: Array containing the dynamic addresses of each attached device * @descs: Array of descriptors, one per attached device - * @hj_work: Hot-join work * @irq: Main interrupt * @num_clks: I3C clock number * @fclk: Fast clock (bus) @@ -235,7 +234,6 @@ struct svc_i3c_master { u32 free_slots; u8 addrs[SVC_I3C_MAX_DEVS]; struct i3c_dev_desc *descs[SVC_I3C_MAX_DEVS]; - struct work_struct hj_work; int irq; int num_clks; struct clk *fclk; @@ -366,14 +364,6 @@ to_svc_i3c_master(struct i3c_master_controller *master) return container_of(master, struct svc_i3c_master, base); } -static void svc_i3c_master_hj_work(struct work_struct *work) -{ - struct svc_i3c_master *master; - - master = container_of(work, struct svc_i3c_master, hj_work); - i3c_master_do_daa(&master->base); -} - static struct i3c_dev_desc * svc_i3c_master_dev_from_addr(struct svc_i3c_master *master, unsigned int ibiaddr) @@ -651,7 +641,7 @@ static void svc_i3c_master_ibi_isr(struct svc_i3c_master *master) case SVC_I3C_MSTATUS_IBITYPE_HOT_JOIN: svc_i3c_master_emit_stop(master); if (is_events_enabled(master, SVC_I3C_EVENT_HOTJOIN)) - queue_work(master->base.wq, &master->hj_work); + i3c_master_queue_hotjoin(&master->base); break; case SVC_I3C_MSTATUS_IBITYPE_MASTER_REQUEST: svc_i3c_master_emit_stop(master); @@ -2022,7 +2012,6 @@ static int svc_i3c_master_probe(struct platform_device *pdev) if (ret) return dev_err_probe(dev, ret, "can't enable I3C clocks\n"); - INIT_WORK(&master->hj_work, svc_i3c_master_hj_work); mutex_init(&master->lock); ret = devm_request_irq(dev, master->irq, svc_i3c_master_irq_handler, @@ -2081,7 +2070,6 @@ static void svc_i3c_master_remove(struct platform_device *pdev) { struct svc_i3c_master *master = platform_get_drvdata(pdev); - cancel_work_sync(&master->hj_work); i3c_master_unregister(&master->base); pm_runtime_dont_use_autosuspend(&pdev->dev); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index e6112e5f6608..eb5c51608bd7 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -520,6 +520,8 @@ struct i3c_master_controller_ops { * in a thread context. Typical examples are Hot Join processing which * requires taking the bus lock in maintenance, which in turn, can only * be done from a sleep-able context + * @hj_work: work item used to run DAA after a Hot-Join event is detected. + * Queued to @wq by i3c_master_queue_hotjoin() * @dev_nack_retry_count: retry count when slave device nack * * A &struct i3c_master_controller has to be registered to the I3C subsystem @@ -543,6 +545,7 @@ struct i3c_master_controller { } boardinfo; struct i3c_bus bus; struct workqueue_struct *wq; + struct work_struct hj_work; unsigned int dev_nack_retry_count; }; @@ -623,6 +626,7 @@ int i3c_master_register(struct i3c_master_controller *master, void i3c_master_unregister(struct i3c_master_controller *master); int i3c_master_enable_hotjoin(struct i3c_master_controller *master); int i3c_master_disable_hotjoin(struct i3c_master_controller *master); +void i3c_master_queue_hotjoin(struct i3c_master_controller *master); /** * i3c_dev_get_master_data() - get master private data attached to an I3C -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:17:28 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:28 +0300 Subject: [PATCH 4/8] i3c: master: Ensure Hot-Join operations are stopped on shutdown In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-5-adrian.hunter@intel.com> System shutdown invokes each device's bus shutdown callback to quiesce hardware, but the I3C bus type does not currently implement one. As a result, on shutdown the controller's Hot-Join work and any in-flight i3c_master_do_daa() can keep running (or be newly triggered) while the rest of the system is being torn down. A similar window exists at i3c_master_unregister() time: cancel_work_sync() on hj_work prevents queued work from completing, but does not stop a fresh Hot-Join IBI from re-queueing the worker, nor a concurrent sysfs writer from toggling Hot-Join via i3c_set_hotjoin(). Introduce a single "shutting down" gate in the I3C core, set under the bus maintenance lock so it is observed by any in-progress DAA path before pending work is cancelled. Install an i3c_bus_type shutdown callback that engages this gate for master devices during system shutdown, and use the same gate in i3c_master_unregister() so both paths get identical guarantees. Once the gate is engaged, the Hot-Join worker, i3c_master_do_daa_ext() and i3c_set_hotjoin() all bail out cleanly, so Hot-Join IBIs that race with shutdown become no-ops, direct DAA callers see -ENODEV, and sysfs writers can no longer re-enable Hot-Join through ops->enable_hotjoin() while the controller is going away. No functional change for the steady-state runtime path; the new checks only take effect once the controller has been marked as shutting down. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 52 +++++++++++++++++++++++++++----------- include/linux/i3c/master.h | 2 ++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index cdb5cb2aa65d..a59c4b744b36 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -368,14 +368,6 @@ static void i3c_device_remove(struct device *dev) driver->remove(i3cdev); } -const struct bus_type i3c_bus_type = { - .name = "i3c", - .match = i3c_device_match, - .probe = i3c_device_probe, - .remove = i3c_device_remove, -}; -EXPORT_SYMBOL_GPL(i3c_bus_type); - static enum i3c_addr_slot_status i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask) { @@ -637,7 +629,8 @@ static void i3c_master_hj_work_fn(struct work_struct *work) { struct i3c_master_controller *master = container_of(work, typeof(*master), hj_work); - i3c_master_do_daa(master); + if (!master->shutting_down) + i3c_master_do_daa(master); } static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) @@ -658,7 +651,9 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) i3c_bus_maintenance_lock(&master->bus); - if (enable) + if (master->shutting_down) + ret = -ENODEV; + else if (enable) ret = master->ops->enable_hotjoin(master); else ret = master->ops->disable_hotjoin(master); @@ -837,6 +832,30 @@ static const struct device_type i3c_masterdev_type = { .groups = i3c_masterdev_groups, }; +static void i3c_master_shutdown(struct i3c_master_controller *master) +{ + i3c_bus_maintenance_lock(&master->bus); + master->shutting_down = true; + i3c_bus_maintenance_unlock(&master->bus); + + cancel_work_sync(&master->hj_work); +} + +static void i3c_device_shutdown(struct device *dev) +{ + if (dev->type == &i3c_masterdev_type) + i3c_master_shutdown(dev_to_i3cmaster(dev)); +} + +const struct bus_type i3c_bus_type = { + .name = "i3c", + .match = i3c_device_match, + .probe = i3c_device_probe, + .remove = i3c_device_remove, + .shutdown = i3c_device_shutdown, +}; +EXPORT_SYMBOL_GPL(i3c_bus_type); + static int i3c_bus_set_mode(struct i3c_bus *i3cbus, enum i3c_bus_mode mode, unsigned long max_i2c_scl_rate) { @@ -1846,10 +1865,13 @@ int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) i3c_bus_maintenance_lock(&master->bus); - if (rstdaa) - rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); - - ret = master->ops->do_daa(master); + if (master->shutting_down) { + ret = -ENODEV; + } else { + if (rstdaa) + rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); + ret = master->ops->do_daa(master); + } i3c_bus_maintenance_unlock(&master->bus); @@ -3166,7 +3188,7 @@ EXPORT_SYMBOL_GPL(i3c_master_register); void i3c_master_unregister(struct i3c_master_controller *master) { i3c_bus_notify(&master->bus, I3C_NOTIFY_BUS_REMOVE); - cancel_work_sync(&master->hj_work); + i3c_master_shutdown(master); if (master->ops->set_dev_nack_retry) device_remove_file(&master->dev, &dev_attr_dev_nack_retry_count); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index eb5c51608bd7..77e63082b06e 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -511,6 +511,7 @@ struct i3c_master_controller_ops { * @hotjoin: true if the master support hotjoin * @rpm_allowed: true if Runtime PM allowed * @rpm_ibi_allowed: true if IBI and Hot-Join allowed while runtime suspended + * @shutting_down: set to true when master begins shutdown or unregister * @boardinfo.i3c: list of I3C boardinfo objects * @boardinfo.i2c: list of I2C boardinfo objects * @boardinfo: board-level information attached to devices connected on the bus @@ -539,6 +540,7 @@ struct i3c_master_controller { unsigned int hotjoin: 1; unsigned int rpm_allowed: 1; unsigned int rpm_ibi_allowed: 1; + bool shutting_down; struct { struct list_head i3c; struct list_head i2c; -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:17:29 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:29 +0300 Subject: [PATCH 5/8] i3c: dw: Drop redundant Hot-Join cancel_work_sync() in shutdown In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-6-adrian.hunter@intel.com> The I3C core now installs an i3c_bus_type shutdown callback that flushes master->hj_work (via i3c_master_shutdown()) before any driver's platform shutdown hook runs. The explicit cancel_work_sync() in dw_i3c_shutdown() is therefore redundant: by the time it executes, the Hot-Join worker has already been cancelled, and the shutting_down gate makes a new worker a no-op. Remove the now-unneeded call. No functional change. Signed-off-by: Adrian Hunter --- drivers/i3c/master/dw-i3c-master.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index eb9a13a73684..c7030d0cd8a6 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1793,8 +1793,6 @@ static void dw_i3c_shutdown(struct platform_device *pdev) return; } - cancel_work_sync(&master->base.hj_work); - /* Disable interrupts */ writel((u32)~INTR_ALL, master->regs + INTR_STATUS_EN); writel((u32)~INTR_ALL, master->regs + INTR_SIGNAL_EN); -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:17:30 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:30 +0300 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-7-adrian.hunter@intel.com> Master drivers may invoke i3c_master_do_daa_ext() during resume to re-run Dynamic Address Assignment. As well as assigning addresses to any newly arrived devices, this restores the dynamic address of devices that lost it across system suspend, so it has to run as part of the controller's resume path. A side effect of i3c_master_do_daa_ext() today is that it also registers any newly discovered I3C devices with the driver model inline, via i3c_master_register_new_i3c_devs(). Doing that from the resume path is problematic: a hot-join-capable device may join the bus during this same DAA, and registering it immediately would push driver model work (probing, sysfs, etc.) into the controller's resume context, where the rest of the system is not yet fully resumed and the controller driver is still partway through its own resume sequence. Decouple discovery from registration: add a reg_work work item to struct i3c_master_controller and have i3c_master_do_daa_ext() queue it on master->wq (the freezable workqueue) instead of calling i3c_master_register_new_i3c_devs() directly. The worker performs the registration only when the controller is not shutting_down, and is cancelled alongside hj_work in i3c_master_shutdown(). Because wq is freezable, any newly observed devices end up being registered after the system has finished resuming. i3c_master_register() also routes its initial post-bus-init registration through reg_work, using flush_work() to keep probe-time behavior synchronous. This keeps a single registration code path and ensures the worker is the only writer of desc->dev. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 21 +++++++++++++++------ include/linux/i3c/master.h | 6 ++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index a59c4b744b36..1eb157c8091d 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -839,6 +839,7 @@ static void i3c_master_shutdown(struct i3c_master_controller *master) i3c_bus_maintenance_unlock(&master->bus); cancel_work_sync(&master->hj_work); + cancel_work_sync(&master->reg_work); } static void i3c_device_shutdown(struct device *dev) @@ -1838,6 +1839,16 @@ i3c_master_register_new_i3c_devs(struct i3c_master_controller *master) } } +static void i3c_master_reg_work_fn(struct work_struct *work) +{ + struct i3c_master_controller *master = container_of(work, typeof(*master), reg_work); + + i3c_bus_normaluse_lock(&master->bus); + if (!master->shutting_down) + i3c_master_register_new_i3c_devs(master); + i3c_bus_normaluse_unlock(&master->bus); +} + /** * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) * @master: controller @@ -1878,9 +1889,7 @@ int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) if (ret) goto out; - i3c_bus_normaluse_lock(&master->bus); - i3c_master_register_new_i3c_devs(master); - i3c_bus_normaluse_unlock(&master->bus); + queue_work(master->wq, &master->reg_work); out: i3c_master_rpm_put(master); @@ -3126,6 +3135,7 @@ int i3c_master_register(struct i3c_master_controller *master, goto err_put_dev; } INIT_WORK(&master->hj_work, i3c_master_hj_work_fn); + INIT_WORK(&master->reg_work, i3c_master_reg_work_fn); ret = i3c_master_bus_init(master); if (ret) @@ -3154,9 +3164,8 @@ int i3c_master_register(struct i3c_master_controller *master, * register I3C devices discovered during the initial DAA. */ master->init_done = true; - i3c_bus_normaluse_lock(&master->bus); - i3c_master_register_new_i3c_devs(master); - i3c_bus_normaluse_unlock(&master->bus); + queue_work(master->wq, &master->reg_work); + flush_work(&master->reg_work); if (master->ops->set_dev_nack_retry) device_create_file(&master->dev, &dev_attr_dev_nack_retry_count); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 77e63082b06e..8cdd7be505d3 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -523,6 +523,11 @@ struct i3c_master_controller_ops { * be done from a sleep-able context * @hj_work: work item used to run DAA after a Hot-Join event is detected. * Queued to @wq by i3c_master_queue_hotjoin() + * @reg_work: work item used to register newly discovered I3C devices with + * the driver model. Queued to @wq by i3c_master_do_daa_ext() so + * that device registration is deferred out of the DAA caller's + * context (notably the resume path), and is skipped if the + * controller is shutting down * @dev_nack_retry_count: retry count when slave device nack * * A &struct i3c_master_controller has to be registered to the I3C subsystem @@ -548,6 +553,7 @@ struct i3c_master_controller { struct i3c_bus bus; struct workqueue_struct *wq; struct work_struct hj_work; + struct work_struct reg_work; unsigned int dev_nack_retry_count; }; -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:17:31 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:31 +0300 Subject: [PATCH 7/8] i3c: master: Export i3c_master_enec_disec_locked() In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-8-adrian.hunter@intel.com> The existing i3c_master_enec_locked() wrapper always treats a NACKed ENEC CCC as a failure (M2 error). However, broadcasting ENEC to enable Hot-Join is legitimately useful even when no I3C devices are currently present on the bus, in which case the broadcast will be NACKed and should not be reported as an error. The underlying helper i3c_master_enec_disec_locked() already accepts a suppress_m2 flag that lets callers ignore such NACKs. Expose it so that a subsequent patch enabling Hot-Join events can issue ENEC with M2 suppression. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 27 ++++++++++++++++++++++++--- include/linux/i3c/master.h | 2 ++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 1eb157c8091d..db46cc12c7db 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1121,9 +1121,29 @@ int i3c_master_entdaa_locked(struct i3c_master_controller *master) } EXPORT_SYMBOL_GPL(i3c_master_entdaa_locked); -static int i3c_master_enec_disec_locked(struct i3c_master_controller *master, - u8 addr, bool enable, u8 evts, - bool suppress_m2) +/** + * i3c_master_enec_disec_locked() - send an ENEC or DISEC CCC command + * @master: master used to send frames on the bus + * @addr: a valid I3C slave address or %I3C_BROADCAST_ADDR + * @enable: true to send ENEC, false to send DISEC + * @evts: events to enable or disable + * @suppress_m2: if true, treat an M2 (NACK) error from the CCC as success + * + * Send an ENEC or DISEC CCC command to enable or disable some or all events + * coming from a specific slave, or all devices if @addr is + * %I3C_BROADCAST_ADDR. + * + * When @suppress_m2 is true, a NACK of the broadcast (which can happen when + * no devices are present on the bus) is not reported as an error. This is + * useful for callers that want to configure event reporting unconditionally, + * regardless of whether any devices are currently on the bus. + * + * This function must be called with the bus lock held in write mode. + * + * Return: 0 in case of success, or a negative error code otherwise. + */ +int i3c_master_enec_disec_locked(struct i3c_master_controller *master, u8 addr, + bool enable, u8 evts, bool suppress_m2) { struct i3c_ccc_events *events; struct i3c_ccc_cmd_dest dest; @@ -1148,6 +1168,7 @@ static int i3c_master_enec_disec_locked(struct i3c_master_controller *master, return ret; } +EXPORT_SYMBOL_GPL(i3c_master_enec_disec_locked); /** * i3c_master_disec_locked() - send a DISEC CCC command diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 8cdd7be505d3..e2c831fb5354 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -607,6 +607,8 @@ int i3c_master_disec_locked(struct i3c_master_controller *master, u8 addr, u8 evts); int i3c_master_enec_locked(struct i3c_master_controller *master, u8 addr, u8 evts); +int i3c_master_enec_disec_locked(struct i3c_master_controller *master, u8 addr, + bool enable, u8 evts, bool suppress_m2); int i3c_master_entdaa_locked(struct i3c_master_controller *master); int i3c_master_defslvs_locked(struct i3c_master_controller *master); -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:17:32 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:17:32 +0300 Subject: [PATCH 8/8] i3c: mipi-i3c-hci: Add Hot-Join support In-Reply-To: <20260512121732.406009-1-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> Message-ID: <20260512121732.406009-9-adrian.hunter@intel.com> Wire the MIPI I3C HCI driver into the I3C core Hot-Join framework to allow targets to dynamically join the bus after initial DAA. HCI hardware ACKs or NACKs Hot-Join requests based on HC_CONTROL.HOT_JOIN_CTRL. This was previously left in the NACK-and-DISEC state, effectively preventing Hot-Join. Implement the ->enable_hotjoin() and ->disable_hotjoin() master operations so the core and user space can control this policy at runtime. Also issue broadcast ENEC HJ when enabling Hot-Join. This is required because the controller may have previously DISEC'ed the Hot-Join event, causing targets that were NACKed once to never retry. Acknowledged Hot-Join requests are delivered as IBIs on the reserved address 0x02. Update both the DMA and PIO IBI paths to recognise this address and forward the event to i3c_master_queue_hotjoin(). To make Hot-Join usable by default, enable it once after the initial DAA. This is gated by rpm_ibi_allowed, since otherwise keeping Hot-Join enabled prevents runtime suspend. A new hj_init_done flag ensures this one-time enablement is not repeated on subsequent DAAs. Signed-off-by: Adrian Hunter --- drivers/i3c/master/mipi-i3c-hci/core.c | 50 ++++++++++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/dma.c | 5 +++ drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + drivers/i3c/master/mipi-i3c-hci/pio.c | 5 +++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 2866d599612a..f8b399d16598 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -392,11 +392,52 @@ static int i3c_hci_send_ccc_cmd(struct i3c_master_controller *m, return ret; } +static int i3c_hci_enable_hotjoin(struct i3c_master_controller *m) +{ + struct i3c_hci *hci = to_i3c_hci(m); + int ret; + + reg_clear(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); + + /* + * Broadcast Hot_join enable, so that an I3C device that has previously + * had its Hot-Join request NACK'ed knows to try again. + */ + ret = i3c_master_enec_disec_locked(m, I3C_BROADCAST_ADDR, true, I3C_CCC_EVENT_HJ, true); + if (ret) { + reg_set(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); + dev_err(&hci->master.dev, "Hot-Join ENEC CCC failed\n"); + } + + return ret; +} + +static int i3c_hci_disable_hotjoin(struct i3c_master_controller *m) +{ + struct i3c_hci *hci = to_i3c_hci(m); + + reg_set(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); + return 0; +} + static int i3c_hci_daa(struct i3c_master_controller *m) { struct i3c_hci *hci = to_i3c_hci(m); + int ret; - return hci->cmd->perform_daa(hci); + ret = hci->cmd->perform_daa(hci); + + if (!hci->hj_init_done) { + hci->hj_init_done = true; + /* + * Enable Hot-Join by default after initial DAA if it does not + * prevent runtime suspend. + */ + if (m->rpm_ibi_allowed && !ret) + m->hotjoin = !i3c_hci_enable_hotjoin(m); + } + + return ret; } static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, @@ -652,6 +693,8 @@ static const struct i3c_master_controller_ops i3c_hci_ops = { .enable_ibi = i3c_hci_enable_ibi, .disable_ibi = i3c_hci_disable_ibi, .recycle_ibi_slot = i3c_hci_recycle_ibi_slot, + .enable_hotjoin = i3c_hci_enable_hotjoin, + .disable_hotjoin = i3c_hci_disable_hotjoin, }; static irqreturn_t i3c_hci_irq_handler(int irq, void *dev_id) @@ -833,8 +876,9 @@ static int i3c_hci_do_reset_and_restore(struct i3c_hci *hci) scoped_guard(spinlock_irqsave, &hci->lock) hci->irq_inactive = false; - /* Enable bus with Hot-Join disabled */ - reg_set(HC_CONTROL, HC_CONTROL_BUS_ENABLE | HC_CONTROL_HOT_JOIN_CTRL); + /* Enable bus, restoring hot-join state */ + reg_set(HC_CONTROL, + HC_CONTROL_BUS_ENABLE | (hci->master.hotjoin ? 0 : HC_CONTROL_HOT_JOIN_CTRL)); return 0; } diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index e5deeba0aa4e..34129ac039dc 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -971,6 +971,11 @@ static void hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh) } /* determine who this is for */ + if (ibi_addr == I3C_HOT_JOIN_ADDR) { + i3c_master_queue_hotjoin(&hci->master); + goto done; + } + dev = i3c_hci_addr_to_dev(hci, ibi_addr); if (!dev) { dev_err(&hci->master.dev, diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 243d7a67f6f6..591eea040b01 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -57,6 +57,7 @@ struct i3c_hci { bool irq_inactive; bool enqueue_blocked; bool recovery_needed; + bool hj_init_done; wait_queue_head_t enqueue_wait_queue; u32 caps; unsigned int quirks; diff --git a/drivers/i3c/master/mipi-i3c-hci/pio.c b/drivers/i3c/master/mipi-i3c-hci/pio.c index 6b8cc5f2b4d2..b5ae1cfaa9e0 100644 --- a/drivers/i3c/master/mipi-i3c-hci/pio.c +++ b/drivers/i3c/master/mipi-i3c-hci/pio.c @@ -862,6 +862,11 @@ static bool hci_pio_prep_new_ibi(struct i3c_hci *hci, struct hci_pio_data *pio) ibi->seg_len = FIELD_GET(IBI_DATA_LENGTH, ibi_status); ibi->seg_cnt = ibi->seg_len; + if (ibi->addr == I3C_HOT_JOIN_ADDR) { + i3c_master_queue_hotjoin(&hci->master); + return true; + } + dev = i3c_hci_addr_to_dev(hci, ibi->addr); if (!dev) { dev_err(&hci->master.dev, -- 2.51.0 From adrian.hunter at intel.com Tue May 12 05:35:18 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Tue, 12 May 2026 15:35:18 +0300 Subject: [PATCH V3 00/16] i3c: mipi-i3c-hci: DMA abort, recovery and related improvements In-Reply-To: <20260504113352.38490-1-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> Message-ID: On 04/05/2026 14:33, Adrian Hunter wrote: > Hi > > This series improves the robustness of the MIPI I3C HCI DMA mode driver, > addressing issues observed during error handling and recovery. Any comments on this? > > Patch 1 ensures suspend always invokes io->suspend. > > Patches 2-4 fix issues in the existing DMA abort path: preserving the RUN > bit during abort per the MIPI specification, blocking enqueue during > abort/error, and waiting for ring restart completion. > > Patches 5-8 improve how partially completed transfer lists are handled > during dequeue: moving hci_dma_xfer_done() earlier so completed > responses are processed before NoOp replacement, completing transfer > lists immediately on error rather than deferring, and detecting when an > abort races with transfer completion to avoid restarting the wrong > transfer list. > > Patches 9-10 add Intel-specific quirks for DMA ring abort: a PIO queue > reset after abort, and an HC_CONTROL ABORT before the ring-level abort. > > Patch 11 factors out a reset-and-restore helper from the suspend path > for reuse. > > Patch 12 adds a full DMA recovery path for internal controller errors. > When the hardware reports a TID mismatch or the ring becomes stuck, the > driver now resets and restores the controller, terminating all in-flight > transfers with an error status. > > Patch 13 makes NoOp command handling observable: instead of discarding > NoOp responses, the driver now waits for them to complete and triggers > recovery if they fail. > > Patch 14 adjusts transfer timeout accounting to start from when a > transfer actually begins execution rather than when it was queued, > preventing premature timeouts behind slow predecessors. > > Patches 15-16 are minor optimizations: consolidating the DMA command and > response ring into a single coherent allocation, and increasing the ring > size to the maximum 255 entries to avoid ring-space exhaustion. > > > Changes in V3: > > i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset > i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring > Add Frank's rev'd-by > > i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors > When erroring out transfers, ensure the final transfer of a > transfer list is processed last > > > Changes in V2: > > i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset > Always return 0 from suspend callback > Amend commit message > > i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring > Improve commit message > > i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error > Improve commit message > > i3c: mipi-i3c-hci: Wait for DMA ring restart to complete > None > > i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition > Add Frank's Rev'd-by > > i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path > Add Frank's Rev'd-by > > i3c: mipi-i3c-hci: Complete transfer lists immediately on error > Rename completing_xfer to final_xfer > > i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer > Rename completing_xfer to final_xfer > > i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers > None > > i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers > None > > i3c: mipi-i3c-hci: Factor out reset-and-restore helper > Drop redundant i3c_hci_sync_irq_inactive(hci) > from i3c_hci_reset_and_restore() because it is called by > hci->io->suspend() anyway > > i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors > Rename completing_xfer to final_xfer > Add hci_dma_xfer_done() before checking for an already complete > transfer > Improve commit message > > i3c: mipi-i3c-hci: Wait for NoOp commands to complete > Rename completing_xfer to final_xfer > Add missing reinit_completion() > > i3c: mipi-i3c-hci: Base timeouts on actual transfer start time > Do not flag the next transfer as started when there is an error > which halts the controller > Instead flag it started at the end of hci_dma_dequeue_xfer() > Use hci_start_xfer() in pio.c > > i3c: mipi-i3c-hci: Consolidate DMA ring allocation > Check for failed allocation before assignments to avoid doing > arithmetic with NULL pointers > > i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum > None > > > Adrian Hunter (16): > i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset > i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring > i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error > i3c: mipi-i3c-hci: Wait for DMA ring restart to complete > i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition > i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path > i3c: mipi-i3c-hci: Complete transfer lists immediately on error > i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer > i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers > i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers > i3c: mipi-i3c-hci: Factor out reset-and-restore helper > i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors > i3c: mipi-i3c-hci: Wait for NoOp commands to complete > i3c: mipi-i3c-hci: Base timeouts on actual transfer start time > i3c: mipi-i3c-hci: Consolidate DMA ring allocation > i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum > > drivers/i3c/master/mipi-i3c-hci/cmd.h | 6 + > drivers/i3c/master/mipi-i3c-hci/core.c | 82 ++++++-- > drivers/i3c/master/mipi-i3c-hci/dma.c | 344 +++++++++++++++++++++++++-------- > drivers/i3c/master/mipi-i3c-hci/hci.h | 22 +++ > drivers/i3c/master/mipi-i3c-hci/pio.c | 1 + > 5 files changed, 365 insertions(+), 90 deletions(-) > > > Regards > Adrian From Frank.li at nxp.com Tue May 12 09:16:08 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:16:08 -0400 Subject: [PATCH 3/8] i3c: master: Consolidate Hot-Join DAA work in the core In-Reply-To: <20260512121732.406009-4-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-4-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:27PM +0300, Adrian Hunter wrote: > Three master drivers (dw-i3c-master, i3c-master-cdns, svc-i3c-master) > each carry an essentially identical Hot-Join handler: a struct > work_struct embedded in their private state, a work function that just > calls i3c_master_do_daa() on the embedded i3c_master_controller, plus > matching INIT_WORK()/cancel_work_sync() boilerplate in probe/remove (and > shutdown for dw-i3c). The IBI/ISR paths then queue that work onto > master->wq, which already lives in the core. > > Move this pattern into the I3C core: > > - Add struct work_struct hj_work to struct i3c_master_controller and > initialise it in i3c_master_register() with a core-provided handler > i3c_master_hj_work_fn() that performs i3c_master_do_daa(). > - Cancel the work in i3c_master_unregister() so all controllers get > correct teardown ordering against the workqueue for free. > - Export i3c_master_queue_hotjoin() as the single entry point drivers > call from their Hot-Join IBI handler. > > Convert the three existing users to the new API: drop their private > hj_work fields, work functions, INIT_WORK() and cancel_work_sync() > calls, and replace the queue_work(master->wq, &drv->hj_work) call sites > with i3c_master_queue_hotjoin(&drv->base). The dw-i3c shutdown path > still needs to flush pending Hot-Join work before tearing down the > hardware, so it is updated to cancel master->base.hj_work directly. > > No functional change intended: the work is still queued on the same > master->wq, runs the same i3c_master_do_daa(), and is cancelled at > controller teardown. Future Hot-Join improvements now only need to > be made in one place. > > Signed-off-by: Adrian Hunter > --- Thank you for do such consolidate work. Reviewed-by: Frank Li > From Frank.li at nxp.com Tue May 12 09:09:23 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:09:23 -0400 Subject: [PATCH 1/8] i3c: master: Make hot-join workqueue freezable to block hot-join during suspend In-Reply-To: <20260512121732.406009-2-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-2-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:25PM +0300, Adrian Hunter wrote: > The I3C master workqueue (master->wq) is used to defer work that needs > thread context and the bus maintenance lock, most notably Hot Join > processing (which calls i3c_master_do_daa() to assign dynamic addresses > to newly joined devices). > > Currently the workqueue keeps running across system suspend, which can > race with the suspend path: > > - do_daa() may execute after the controller has been suspended, > issuing bus transactions on a powered-down or otherwise unusable > controller. > - New I3C devices can be enumerated and added to the bus mid-suspend, > registering driver model objects at a point where the I3C subsystem > and its consumers are not prepared to handle them. > > Mark the workqueue WQ_FREEZABLE so its workers are frozen for the > duration of system suspend/hibernate and resumed afterwards. This > naturally defers any pending or newly queued Hot Join work until the > system (and the controller) is fully resumed, closing both races > without adding explicit suspend/resume synchronization in the master > drivers. > > Update the kerneldoc for struct i3c_master_controller::wq to reflect > that the workqueue is freezable. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > From Frank.li at nxp.com Tue May 12 09:11:51 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:11:51 -0400 Subject: [PATCH 2/8] i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock In-Reply-To: <20260512121732.406009-3-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-3-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:26PM +0300, Adrian Hunter wrote: > i3c_set_hotjoin() dispatches the controller's enable_hotjoin() or > disable_hotjoin() op and updates master->hotjoin under > i3c_bus_normaluse_lock(). That lock is a read-side acquisition of > bus->lock (down_read()), so it does not exclude concurrent callers. > > The hotjoin sysfs attribute can be opened multiple times, and writes > through different opens are not serialized. Two concurrent writers > to "hotjoin" can therefore race in i3c_set_hotjoin(), with the > controller op and the master->hotjoin store from one call interleaving > with the other. The hardware enable/disable state and the value reported > by hotjoin_show() can end up out of sync. > > Take i3c_bus_maintenance_lock() instead. Toggling Hot Join enable > changes bus state and is conceptually a maintenance operation, so the > write-side acquisition of bus->lock is the appropriate lock and > serializes concurrent callers against each other and against other > maintenance operations. It should be bug fix, add fix tag here. Frank > > Signed-off-by: Adrian Hunter > --- > drivers/i3c/master.c | 4 ++-- > 1 file changed, 2 insertions(+), 2 deletions(-) > > diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c > index ab11e2d79aab..38ffc8713167 100644 > --- a/drivers/i3c/master.c > +++ b/drivers/i3c/master.c > @@ -649,7 +649,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) > return ret; > } > > - i3c_bus_normaluse_lock(&master->bus); > + i3c_bus_maintenance_lock(&master->bus); > > if (enable) > ret = master->ops->enable_hotjoin(master); > @@ -659,7 +659,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) > if (!ret) > master->hotjoin = enable; > > - i3c_bus_normaluse_unlock(&master->bus); > + i3c_bus_maintenance_unlock(&master->bus); > > if ((enable && ret) || (!enable && !ret) || master->rpm_ibi_allowed) > i3c_master_rpm_put(master); > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:27:53 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:27:53 -0400 Subject: [PATCH 4/8] i3c: master: Ensure Hot-Join operations are stopped on shutdown In-Reply-To: <20260512121732.406009-5-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-5-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:28PM +0300, Adrian Hunter wrote: > System shutdown invokes each device's bus shutdown callback to quiesce > hardware, but the I3C bus type does not currently implement one. As a > result, on shutdown the controller's Hot-Join work and any in-flight > i3c_master_do_daa() can keep running (or be newly triggered) while the > rest of the system is being torn down. > > A similar window exists at i3c_master_unregister() time: cancel_work_sync() > on hj_work prevents queued work from completing, but does not stop a > fresh Hot-Join IBI from re-queueing the worker, nor a concurrent sysfs > writer from toggling Hot-Join via i3c_set_hotjoin(). > > Introduce a single "shutting down" gate in the I3C core, set under the > bus maintenance lock so it is observed by any in-progress DAA path > before pending work is cancelled. Install an i3c_bus_type shutdown > callback that engages this gate for master devices during system > shutdown, and use the same gate in i3c_master_unregister() so both > paths get identical guarantees. > > Once the gate is engaged, the Hot-Join worker, i3c_master_do_daa_ext() > and i3c_set_hotjoin() all bail out cleanly, so Hot-Join IBIs that race > with shutdown become no-ops, direct DAA callers see -ENODEV, and sysfs > writers can no longer re-enable Hot-Join through ops->enable_hotjoin() > while the controller is going away. > > No functional change for the steady-state runtime path; the new checks > only take effect once the controller has been marked as shutting down. > > Signed-off-by: Adrian Hunter > --- > drivers/i3c/master.c | 52 +++++++++++++++++++++++++++----------- > include/linux/i3c/master.h | 2 ++ > 2 files changed, 39 insertions(+), 15 deletions(-) > > diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c > index cdb5cb2aa65d..a59c4b744b36 100644 > --- a/drivers/i3c/master.c > +++ b/drivers/i3c/master.c > @@ -368,14 +368,6 @@ static void i3c_device_remove(struct device *dev) > driver->remove(i3cdev); > } > > -const struct bus_type i3c_bus_type = { > - .name = "i3c", > - .match = i3c_device_match, > - .probe = i3c_device_probe, > - .remove = i3c_device_remove, > -}; > -EXPORT_SYMBOL_GPL(i3c_bus_type); > - why need move this tunk? > static enum i3c_addr_slot_status > i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask) > { > @@ -637,7 +629,8 @@ static void i3c_master_hj_work_fn(struct work_struct *work) > { > struct i3c_master_controller *master = container_of(work, typeof(*master), hj_work); > > - i3c_master_do_daa(master); > + if (!master->shutting_down) > + i3c_master_do_daa(master); > } > > static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) > @@ -658,7 +651,9 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) > > i3c_bus_maintenance_lock(&master->bus); later, consider change to use cleanup, so if (master->shutting_down) return -ENODEV and avoid use else if branch. but this change is okay for now. Frank From Frank.li at nxp.com Tue May 12 09:30:05 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:30:05 -0400 Subject: [PATCH 5/8] i3c: dw: Drop redundant Hot-Join cancel_work_sync() in shutdown In-Reply-To: <20260512121732.406009-6-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-6-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:29PM +0300, Adrian Hunter wrote: > The I3C core now installs an i3c_bus_type shutdown callback that > flushes master->hj_work (via i3c_master_shutdown()) before any driver's > platform shutdown hook runs. The explicit cancel_work_sync() in > dw_i3c_shutdown() is therefore redundant: by the time it executes, the > Hot-Join worker has already been cancelled, and the shutting_down gate > makes a new worker a no-op. > > Remove the now-unneeded call. No functional change. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > drivers/i3c/master/dw-i3c-master.c | 2 -- > 1 file changed, 2 deletions(-) > > diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c > index eb9a13a73684..c7030d0cd8a6 100644 > --- a/drivers/i3c/master/dw-i3c-master.c > +++ b/drivers/i3c/master/dw-i3c-master.c > @@ -1793,8 +1793,6 @@ static void dw_i3c_shutdown(struct platform_device *pdev) > return; > } > > - cancel_work_sync(&master->base.hj_work); > - > /* Disable interrupts */ > writel((u32)~INTR_ALL, master->regs + INTR_STATUS_EN); > writel((u32)~INTR_ALL, master->regs + INTR_SIGNAL_EN); > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:31:23 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:31:23 -0400 Subject: [PATCH 7/8] i3c: master: Export i3c_master_enec_disec_locked() In-Reply-To: <20260512121732.406009-8-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-8-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:31PM +0300, Adrian Hunter wrote: > The existing i3c_master_enec_locked() wrapper always treats a NACKed > ENEC CCC as a failure (M2 error). However, broadcasting ENEC to enable > Hot-Join is legitimately useful even when no I3C devices are currently > present on the bus, in which case the broadcast will be NACKed and > should not be reported as an error. > > The underlying helper i3c_master_enec_disec_locked() already accepts a > suppress_m2 flag that lets callers ignore such NACKs. Expose it so that > a subsequent patch enabling Hot-Join events can issue ENEC with M2 > suppression. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > drivers/i3c/master.c | 27 ++++++++++++++++++++++++--- > include/linux/i3c/master.h | 2 ++ > 2 files changed, 26 insertions(+), 3 deletions(-) > > diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c > index 1eb157c8091d..db46cc12c7db 100644 > --- a/drivers/i3c/master.c > +++ b/drivers/i3c/master.c > @@ -1121,9 +1121,29 @@ int i3c_master_entdaa_locked(struct i3c_master_controller *master) > } > EXPORT_SYMBOL_GPL(i3c_master_entdaa_locked); > > -static int i3c_master_enec_disec_locked(struct i3c_master_controller *master, > - u8 addr, bool enable, u8 evts, > - bool suppress_m2) > +/** > + * i3c_master_enec_disec_locked() - send an ENEC or DISEC CCC command > + * @master: master used to send frames on the bus > + * @addr: a valid I3C slave address or %I3C_BROADCAST_ADDR > + * @enable: true to send ENEC, false to send DISEC > + * @evts: events to enable or disable > + * @suppress_m2: if true, treat an M2 (NACK) error from the CCC as success > + * > + * Send an ENEC or DISEC CCC command to enable or disable some or all events > + * coming from a specific slave, or all devices if @addr is > + * %I3C_BROADCAST_ADDR. > + * > + * When @suppress_m2 is true, a NACK of the broadcast (which can happen when > + * no devices are present on the bus) is not reported as an error. This is > + * useful for callers that want to configure event reporting unconditionally, > + * regardless of whether any devices are currently on the bus. > + * > + * This function must be called with the bus lock held in write mode. > + * > + * Return: 0 in case of success, or a negative error code otherwise. > + */ > +int i3c_master_enec_disec_locked(struct i3c_master_controller *master, u8 addr, > + bool enable, u8 evts, bool suppress_m2) > { > struct i3c_ccc_events *events; > struct i3c_ccc_cmd_dest dest; > @@ -1148,6 +1168,7 @@ static int i3c_master_enec_disec_locked(struct i3c_master_controller *master, > > return ret; > } > +EXPORT_SYMBOL_GPL(i3c_master_enec_disec_locked); > > /** > * i3c_master_disec_locked() - send a DISEC CCC command > diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h > index 8cdd7be505d3..e2c831fb5354 100644 > --- a/include/linux/i3c/master.h > +++ b/include/linux/i3c/master.h > @@ -607,6 +607,8 @@ int i3c_master_disec_locked(struct i3c_master_controller *master, u8 addr, > u8 evts); > int i3c_master_enec_locked(struct i3c_master_controller *master, u8 addr, > u8 evts); > +int i3c_master_enec_disec_locked(struct i3c_master_controller *master, u8 addr, > + bool enable, u8 evts, bool suppress_m2); > int i3c_master_entdaa_locked(struct i3c_master_controller *master); > int i3c_master_defslvs_locked(struct i3c_master_controller *master); > > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:34:34 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:34:34 -0400 Subject: [PATCH 8/8] i3c: mipi-i3c-hci: Add Hot-Join support In-Reply-To: <20260512121732.406009-9-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-9-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:32PM +0300, Adrian Hunter wrote: > Wire the MIPI I3C HCI driver into the I3C core Hot-Join framework to > allow targets to dynamically join the bus after initial DAA. > > HCI hardware ACKs or NACKs Hot-Join requests based on > HC_CONTROL.HOT_JOIN_CTRL. This was previously left in the > NACK-and-DISEC state, effectively preventing Hot-Join. Implement > the ->enable_hotjoin() and ->disable_hotjoin() master operations > so the core and user space can control this policy at runtime. > > Also issue broadcast ENEC HJ when enabling Hot-Join. This is required > because the controller may have previously DISEC'ed the Hot-Join > event, causing targets that were NACKed once to never retry. > > Acknowledged Hot-Join requests are delivered as IBIs on the reserved > address 0x02. Update both the DMA and PIO IBI paths to recognise this > address and forward the event to i3c_master_queue_hotjoin(). > > To make Hot-Join usable by default, enable it once after the initial > DAA. This is gated by rpm_ibi_allowed, since otherwise keeping Hot-Join > enabled prevents runtime suspend. A new hj_init_done flag ensures this > one-time enablement is not repeated on subsequent DAAs. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > drivers/i3c/master/mipi-i3c-hci/core.c | 50 ++++++++++++++++++++++++-- > drivers/i3c/master/mipi-i3c-hci/dma.c | 5 +++ > drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + > drivers/i3c/master/mipi-i3c-hci/pio.c | 5 +++ > 4 files changed, 58 insertions(+), 3 deletions(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c > index 2866d599612a..f8b399d16598 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/core.c > +++ b/drivers/i3c/master/mipi-i3c-hci/core.c > @@ -392,11 +392,52 @@ static int i3c_hci_send_ccc_cmd(struct i3c_master_controller *m, > return ret; > } > > +static int i3c_hci_enable_hotjoin(struct i3c_master_controller *m) > +{ > + struct i3c_hci *hci = to_i3c_hci(m); > + int ret; > + > + reg_clear(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); > + > + /* > + * Broadcast Hot_join enable, so that an I3C device that has previously > + * had its Hot-Join request NACK'ed knows to try again. > + */ > + ret = i3c_master_enec_disec_locked(m, I3C_BROADCAST_ADDR, true, I3C_CCC_EVENT_HJ, true); > + if (ret) { > + reg_set(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); > + dev_err(&hci->master.dev, "Hot-Join ENEC CCC failed\n"); > + } > + > + return ret; > +} > + > +static int i3c_hci_disable_hotjoin(struct i3c_master_controller *m) > +{ > + struct i3c_hci *hci = to_i3c_hci(m); > + > + reg_set(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); > + return 0; > +} > + > static int i3c_hci_daa(struct i3c_master_controller *m) > { > struct i3c_hci *hci = to_i3c_hci(m); > + int ret; > > - return hci->cmd->perform_daa(hci); > + ret = hci->cmd->perform_daa(hci); > + > + if (!hci->hj_init_done) { > + hci->hj_init_done = true; > + /* > + * Enable Hot-Join by default after initial DAA if it does not > + * prevent runtime suspend. > + */ > + if (m->rpm_ibi_allowed && !ret) > + m->hotjoin = !i3c_hci_enable_hotjoin(m); > + } > + > + return ret; > } > > static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, > @@ -652,6 +693,8 @@ static const struct i3c_master_controller_ops i3c_hci_ops = { > .enable_ibi = i3c_hci_enable_ibi, > .disable_ibi = i3c_hci_disable_ibi, > .recycle_ibi_slot = i3c_hci_recycle_ibi_slot, > + .enable_hotjoin = i3c_hci_enable_hotjoin, > + .disable_hotjoin = i3c_hci_disable_hotjoin, > }; > > static irqreturn_t i3c_hci_irq_handler(int irq, void *dev_id) > @@ -833,8 +876,9 @@ static int i3c_hci_do_reset_and_restore(struct i3c_hci *hci) > scoped_guard(spinlock_irqsave, &hci->lock) > hci->irq_inactive = false; > > - /* Enable bus with Hot-Join disabled */ > - reg_set(HC_CONTROL, HC_CONTROL_BUS_ENABLE | HC_CONTROL_HOT_JOIN_CTRL); > + /* Enable bus, restoring hot-join state */ > + reg_set(HC_CONTROL, > + HC_CONTROL_BUS_ENABLE | (hci->master.hotjoin ? 0 : HC_CONTROL_HOT_JOIN_CTRL)); > > return 0; > } > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index e5deeba0aa4e..34129ac039dc 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -971,6 +971,11 @@ static void hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh) > } > > /* determine who this is for */ > + if (ibi_addr == I3C_HOT_JOIN_ADDR) { > + i3c_master_queue_hotjoin(&hci->master); > + goto done; > + } > + > dev = i3c_hci_addr_to_dev(hci, ibi_addr); > if (!dev) { > dev_err(&hci->master.dev, > diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h > index 243d7a67f6f6..591eea040b01 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/hci.h > +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h > @@ -57,6 +57,7 @@ struct i3c_hci { > bool irq_inactive; > bool enqueue_blocked; > bool recovery_needed; > + bool hj_init_done; > wait_queue_head_t enqueue_wait_queue; > u32 caps; > unsigned int quirks; > diff --git a/drivers/i3c/master/mipi-i3c-hci/pio.c b/drivers/i3c/master/mipi-i3c-hci/pio.c > index 6b8cc5f2b4d2..b5ae1cfaa9e0 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/pio.c > +++ b/drivers/i3c/master/mipi-i3c-hci/pio.c > @@ -862,6 +862,11 @@ static bool hci_pio_prep_new_ibi(struct i3c_hci *hci, struct hci_pio_data *pio) > ibi->seg_len = FIELD_GET(IBI_DATA_LENGTH, ibi_status); > ibi->seg_cnt = ibi->seg_len; > > + if (ibi->addr == I3C_HOT_JOIN_ADDR) { > + i3c_master_queue_hotjoin(&hci->master); > + return true; > + } > + > dev = i3c_hci_addr_to_dev(hci, ibi->addr); > if (!dev) { > dev_err(&hci->master.dev, > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:39:21 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:39:21 -0400 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: <20260512121732.406009-7-adrian.hunter@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> Message-ID: On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: > Master drivers may invoke i3c_master_do_daa_ext() during resume to > re-run Dynamic Address Assignment. As well as assigning addresses to > any newly arrived devices, this restores the dynamic address of devices > that lost it across system suspend, so it has to run as part of the > controller's resume path. > > A side effect of i3c_master_do_daa_ext() today is that it also > registers any newly discovered I3C devices with the driver model > inline, via i3c_master_register_new_i3c_devs(). Doing that from the > resume path is problematic: a hot-join-capable device may join the bus > during this same DAA, and registering it immediately would push driver > model work (probing, sysfs, etc.) into the controller's resume context, > where the rest of the system is not yet fully resumed and the > controller driver is still partway through its own resume sequence. > > Decouple discovery from registration: add a reg_work work item to > struct i3c_master_controller and have i3c_master_do_daa_ext() queue it > on master->wq (the freezable workqueue) instead of calling > i3c_master_register_new_i3c_devs() directly. The worker performs the > registration only when the controller is not shutting_down, and is > cancelled alongside hj_work in i3c_master_shutdown(). Because wq is > freezable, any newly observed devices end up being registered after > the system has finished resuming. > > i3c_master_register() also routes its initial post-bus-init registration > through reg_work, using flush_work() to keep probe-time behavior > synchronous. This keeps a single registration code path and ensures the > worker is the only writer of desc->dev. why not direct use hj_work? Frank > From Frank.li at nxp.com Tue May 12 09:44:59 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:44:59 -0400 Subject: [PATCH V3 03/16] i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error In-Reply-To: <20260504113352.38490-4-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-4-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:39PM +0300, Adrian Hunter wrote: > Block the DMA enqueue path while a Ring abort is in progress or after an > error condition has been detected. > > Previously, new transfers could be enqueued while the DMA Ring was being > aborted or while error handling was underway. This allowed enqueue and > error-recovery paths to run concurrently, potentially interfering with > each other and corrupting Ring state. > > Introduce explicit enqueue blocking and a wait queue to serialize access: > enqueue operations now wait until abort or error handling has completed > before proceeding. Enqueue is unblocked once the Ring is safely restarted. > > Note, there is only 1 ring bundle configured, and a transfer error causes > the controller to halt ring (bundle) operation, so there is only ever 1 > outstanding error at a time. Furthermore, a later patch ensures that only > the currently active transfer list can time out. Consequently, the DMA > queue will not be unblocked while there are outstanding transfer errors or > timeouts. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > > > Changes in V3: > > None > > Changes in V2: > > Improve commit message > > > drivers/i3c/master/mipi-i3c-hci/core.c | 1 + > drivers/i3c/master/mipi-i3c-hci/dma.c | 25 +++++++++++++++++++++++-- > drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ > 3 files changed, 26 insertions(+), 2 deletions(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c > index afb0764b5e1f..44617eb3a3f1 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/core.c > +++ b/drivers/i3c/master/mipi-i3c-hci/core.c > @@ -973,6 +973,7 @@ static int i3c_hci_probe(struct platform_device *pdev) > > spin_lock_init(&hci->lock); > mutex_init(&hci->control_mutex); > + init_waitqueue_head(&hci->enqueue_wait_queue); > > /* > * Multi-bus instances share the same MMIO address range, but not > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 4cd32e3afa7b..314635e6e190 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -484,6 +484,12 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, > > spin_lock_irq(&hci->lock); > > + while (unlikely(hci->enqueue_blocked)) { > + spin_unlock_irq(&hci->lock); > + wait_event(hci->enqueue_wait_queue, !READ_ONCE(hci->enqueue_blocked)); > + spin_lock_irq(&hci->lock); > + } > + > if (n > rh->xfer_space) { > spin_unlock_irq(&hci->lock); > hci_dma_unmap_xfer(hci, xfer_list, n); > @@ -539,6 +545,14 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, > return 0; > } > > +static void hci_dma_unblock_enqueue(struct i3c_hci *hci) > +{ > + if (hci->enqueue_blocked) { > + hci->enqueue_blocked = false; > + wake_up_all(&hci->enqueue_wait_queue); > + } > +} > + > static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > struct hci_xfer *xfer_list, int n) > { > @@ -550,12 +564,17 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > > guard(mutex)(&hci->control_mutex); > > + spin_lock_irq(&hci->lock); > + > ring_status = rh_reg_read(RING_STATUS); > if (ring_status & RING_STATUS_RUNNING) { > + hci->enqueue_blocked = true; > + spin_unlock_irq(&hci->lock); > /* stop the ring */ > reinit_completion(&rh->op_done); > rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); > wait_for_completion_timeout(&rh->op_done, HZ); > + spin_lock_irq(&hci->lock); > ring_status = rh_reg_read(RING_STATUS); > if (ring_status & RING_STATUS_RUNNING) { > /* > @@ -567,8 +586,6 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > } > } > > - spin_lock_irq(&hci->lock); > - > for (i = 0; i < n; i++) { > struct hci_xfer *xfer = xfer_list + i; > int idx = xfer->ring_entry; > @@ -604,6 +621,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); > rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); > > + hci_dma_unblock_enqueue(hci); > + > spin_unlock_irq(&hci->lock); > > return did_unqueue; > @@ -647,6 +666,8 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) > } > if (xfer->completion) > complete(xfer->completion); > + if (RESP_STATUS(resp)) > + hci->enqueue_blocked = true; > } > > done_ptr = (done_ptr + 1) % rh->xfer_entries; > diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h > index f17f43494c1b..d630400ec945 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/hci.h > +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h > @@ -54,6 +54,8 @@ struct i3c_hci { > struct mutex control_mutex; > atomic_t next_cmd_tid; > bool irq_inactive; > + bool enqueue_blocked; > + wait_queue_head_t enqueue_wait_queue; > u32 caps; > unsigned int quirks; > unsigned int DAT_entries; > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:45:48 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:45:48 -0400 Subject: [PATCH V3 04/16] i3c: mipi-i3c-hci: Wait for DMA ring restart to complete In-Reply-To: <20260504113352.38490-5-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-5-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:40PM +0300, Adrian Hunter wrote: > Although hci_dma_dequeue_xfer() is serialized against itself via > control_mutex, this does not guarantee that a DMA ring restart > triggered by a previous invocation has fully completed. > > When the function is called again in rapid succession, the DMA ring may > still be transitioning back to the running state, which may confound or > disrupt further state changes. > > Address this by waiting for the DMA ring restart to complete before > continuing. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > > > Changes in V2 and V3: > > None > > > drivers/i3c/master/mipi-i3c-hci/dma.c | 3 +++ > 1 file changed, 3 insertions(+) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 314635e6e190..28614fdbf558 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -617,6 +617,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > } > > /* restart the ring */ > + reinit_completion(&rh->op_done); > mipi_i3c_hci_resume(hci); > rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); > rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); > @@ -625,6 +626,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > > spin_unlock_irq(&hci->lock); > > + wait_for_completion_timeout(&rh->op_done, HZ); > + > return did_unqueue; > } > > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:46:52 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:46:52 -0400 Subject: [PATCH V3 07/16] i3c: mipi-i3c-hci: Complete transfer lists immediately on error In-Reply-To: <20260504113352.38490-8-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-8-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:43PM +0300, Adrian Hunter wrote: > In DMA mode, transfer lists are currently completed only when the final > transfer in the list completes. If an earlier transfer fails, the list is > left incomplete and callers wait until timeout. > > There is no need to wait for a timeout, as the completion path in > i3c_hci_process_xfer() already checks for error status. Complete the > transfer list as soon as any transfer in the list reports an error. > > This avoids unnecessary delays and spurious timeouts on error. > > Complete a transfer list completion immediately there is an error. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > > > Changes in V3: > > None > > Changes in V2: > > Renamed completing_xfer to final_xfer > > > drivers/i3c/master/mipi-i3c-hci/dma.c | 6 ++++-- > drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + > 2 files changed, 5 insertions(+), 2 deletions(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 28e4d38f55d3..899fdf6555a8 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -502,6 +502,8 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, > struct hci_xfer *xfer = xfer_list + i; > u32 *ring_data = rh->xfer + rh->xfer_struct_sz * enqueue_ptr; > > + xfer->final_xfer = xfer_list + n - 1; > + > /* store cmd descriptor */ > *ring_data++ = xfer->cmd_desc[0]; > *ring_data++ = xfer->cmd_desc[1]; > @@ -576,8 +578,8 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) > tid, xfer->cmd_tid); > /* TODO: do something about it? */ > } > - if (xfer->completion) > - complete(xfer->completion); > + if (xfer == xfer->final_xfer || RESP_STATUS(resp)) > + complete(xfer->final_xfer->completion); > if (RESP_STATUS(resp)) > hci->enqueue_blocked = true; > } > diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h > index d630400ec945..f07fc627d4d2 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/hci.h > +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h > @@ -104,6 +104,7 @@ struct hci_xfer { > struct { > /* DMA specific */ > struct i3c_dma *dma; > + struct hci_xfer *final_xfer; > int ring_number; > int ring_entry; > }; > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:50:25 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:50:25 -0400 Subject: [PATCH V3 08/16] i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer In-Reply-To: <20260504113352.38490-9-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-9-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:44PM +0300, Adrian Hunter wrote: > Software ABORT of the DMA ring is used to recover from transfer list > timeouts, but it is inherently racy. The intended transfer list may > complete just before the ABORT takes effect, causing the subsequent > transfer list to be aborted instead. > > In this case, an incomplete transfer list may remain in the ring and has > not yet been processed by hci_dma_dequeue_xfer(). Restarting the DMA > ring at that point can lead to unpredictable results. > > Detect when the next queued transfer is not the first entry of a transfer > list and does not belong to the list currently being dequeued. In that > case, skip restarting the DMA ring and defer recovery until a subsequent > call to hci_dma_dequeue_xfer(), which will safely restart the ring once > the incomplete list is handled. > > Signed-off-by: Adrian Hunter > --- > > > Changes in V3: > > None > > Changes in V2: > > Renamed completing_xfer to final_xfer > > > drivers/i3c/master/mipi-i3c-hci/dma.c | 15 +++++++++++++++ > drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + > 2 files changed, 16 insertions(+) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 899fdf6555a8..268f54b32101 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -503,6 +503,7 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, > u32 *ring_data = rh->xfer + rh->xfer_struct_sz * enqueue_ptr; > > xfer->final_xfer = xfer_list + n - 1; > + xfer->xfer_list_pos = i; > > /* store cmd descriptor */ > *ring_data++ = xfer->cmd_desc[0]; > @@ -669,6 +670,20 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > } > } > > + /* > + * A software ABORT may race with transfer completion and abort the next > + * transfer list instead. Detect that case, and do not restart the ring. > + * It will be handled by a subsequent dequeue. > + */ > + if (!did_unqueue) { > + struct hci_xfer *xfer = rh->src_xfers[rh->done_ptr]; > + > + if (xfer && xfer->xfer_list_pos && xfer->final_xfer != xfer_list->final_xfer) { > + spin_unlock_irq(&hci->lock); Is it possible to use auto cleanup to handle lock()? Frank > + return false; > + } > + } > + > /* restart the ring */ > reinit_completion(&rh->op_done); > mipi_i3c_hci_resume(hci); > diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h > index f07fc627d4d2..83d4f13a68a3 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/hci.h > +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h > @@ -107,6 +107,7 @@ struct hci_xfer { > struct hci_xfer *final_xfer; > int ring_number; > int ring_entry; > + int xfer_list_pos; > }; > }; > }; > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 09:53:23 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 12:53:23 -0400 Subject: [PATCH V3 09/16] i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers In-Reply-To: <20260504113352.38490-10-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-10-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:45PM +0300, Adrian Hunter wrote: > Some Intel I3C HCI controllers cannot reliably restart a DMA ring after an > ABORT. Additional queue resets are required to recover, and must be > performed using PIO reset bits even while operating in DMA mode. > > This behavior is non-standard. Introduce a controller quirk to opt into > the required PIO queue resets after a DMA ring abort, and enable it for > Intel LPSS I3C controllers. > > Signed-off-by: Adrian Hunter > --- > > > Changes in V2 and V3: > > None > > > drivers/i3c/master/mipi-i3c-hci/core.c | 15 ++++++++++++++- > drivers/i3c/master/mipi-i3c-hci/dma.c | 9 +++++++++ > drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ > 3 files changed, 25 insertions(+), 1 deletion(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c > index 44617eb3a3f1..770235ad6b25 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/core.c > +++ b/drivers/i3c/master/mipi-i3c-hci/core.c > @@ -240,6 +240,18 @@ void mipi_i3c_hci_pio_reset(struct i3c_hci *hci) > reg_write(RESET_CONTROL, RX_FIFO_RST | TX_FIFO_RST | RESP_QUEUE_RST); > } > > +#define ALL_QUEUES_RST (CMD_QUEUE_RST | RESP_QUEUE_RST | RX_FIFO_RST | TX_FIFO_RST | IBI_QUEUE_RST) > + > +void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci) > +{ > + u32 regval; > + > + reg_write(RESET_CONTROL, ALL_QUEUES_RST); > + if (readx_poll_timeout_atomic(reg_read, RESET_CONTROL, regval, > + !(regval & ALL_QUEUES_RST), 0, 20)) > + dev_err(&hci->master.dev, "%s: Reset queues failed\n", __func__); > +} > + > /* located here rather than dct.c because needed bits are in core reg space */ > void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci) > { > @@ -1040,7 +1052,8 @@ MODULE_DEVICE_TABLE(acpi, i3c_hci_acpi_match); > static const struct platform_device_id i3c_hci_driver_ids[] = { > { .name = "intel-lpss-i3c", HCI_QUIRK_RPM_ALLOWED | > HCI_QUIRK_RPM_IBI_ALLOWED | > - HCI_QUIRK_RPM_PARENT_MANAGED }, > + HCI_QUIRK_RPM_PARENT_MANAGED | > + HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET }, > { /* sentinel */ } > }; > MODULE_DEVICE_TABLE(platform, i3c_hci_driver_ids); > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 268f54b32101..699c6d523eed 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -597,6 +597,13 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) > rh_reg_write(RING_OPERATION1, op1_val); > } > > +static void hci_dma_abort_requires_pio_reset_quirk(struct i3c_hci *hci, struct hci_rh_data *rh) > +{ > + if ((hci->quirks & HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET) && > + (rh_reg_read(RING_STATUS) & RING_STATUS_ABORTED)) > + mipi_i3c_hci_pio_reset_all_queues(hci); > +} > + > static void hci_dma_unblock_enqueue(struct i3c_hci *hci) > { > if (hci->enqueue_blocked) { > @@ -638,6 +645,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > } > } > > + hci_dma_abort_requires_pio_reset_quirk(hci, rh); > + If only use once, needn't helper function since this helper function is simple Frank > hci_dma_xfer_done(hci, rh); > > for (i = 0; i < n; i++) { > diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h > index 83d4f13a68a3..01237b12d32e 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/hci.h > +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h > @@ -156,10 +156,12 @@ struct i3c_hci_dev_data { > #define HCI_QUIRK_RPM_ALLOWED BIT(5) /* Runtime PM allowed */ > #define HCI_QUIRK_RPM_IBI_ALLOWED BIT(6) /* IBI and Hot-Join allowed while runtime suspended */ > #define HCI_QUIRK_RPM_PARENT_MANAGED BIT(7) /* Runtime PM managed by parent device */ > +#define HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET BIT(8) /* Do PIO queue SW resets after DMA abort */ > > /* global functions */ > void mipi_i3c_hci_resume(struct i3c_hci *hci); > void mipi_i3c_hci_pio_reset(struct i3c_hci *hci); > +void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci); > void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci); > void amd_set_od_pp_timing(struct i3c_hci *hci); > void amd_set_resp_buf_thld(struct i3c_hci *hci); > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 10:01:34 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 13:01:34 -0400 Subject: [PATCH V3 10/16] i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers In-Reply-To: <20260504113352.38490-11-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-11-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:46PM +0300, Adrian Hunter wrote: > DMA rings can be aborted either per-ring via RING_CONTROL or globally > via HC_CONTROL_ABORT. The driver currently relies on the per-ring > mechanism. > > Some Intel I3C HCI controllers require HC_CONTROL_ABORT to be asserted > before a DMA ring abort is effective. This behavior is non-standard. > Introduce a controller quirk to select the required abort method and > enable it for Intel LPSS I3C controllers. > > Signed-off-by: Adrian Hunter > --- > > > Changes in V2 and V3: > > None > > > drivers/i3c/master/mipi-i3c-hci/core.c | 18 +++++++++++++++-- > drivers/i3c/master/mipi-i3c-hci/dma.c | 27 +++++++++++++++++++++++--- > drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ > 3 files changed, 42 insertions(+), 5 deletions(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c > index 770235ad6b25..8274c84b16be 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/core.c > +++ b/drivers/i3c/master/mipi-i3c-hci/core.c > @@ -231,7 +231,20 @@ static void i3c_hci_bus_cleanup(struct i3c_master_controller *m) > > void mipi_i3c_hci_resume(struct i3c_hci *hci) > { > - reg_set(HC_CONTROL, HC_CONTROL_RESUME); > + u32 reg = reg_read(HC_CONTROL); > + > + reg |= HC_CONTROL_RESUME; > + reg &= ~HC_CONTROL_ABORT; > + reg_write(HC_CONTROL, reg); > +} > + > +void mipi_i3c_hci_abort(struct i3c_hci *hci) > +{ > + u32 reg = reg_read(HC_CONTROL); > + > + reg &= ~HC_CONTROL_RESUME; /* Do not set resume */ > + reg |= HC_CONTROL_ABORT; > + reg_write(HC_CONTROL, reg); > } > > /* located here rather than pio.c because needed bits are in core reg space */ > @@ -1053,7 +1066,8 @@ static const struct platform_device_id i3c_hci_driver_ids[] = { > { .name = "intel-lpss-i3c", HCI_QUIRK_RPM_ALLOWED | > HCI_QUIRK_RPM_IBI_ALLOWED | > HCI_QUIRK_RPM_PARENT_MANAGED | > - HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET }, > + HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET | > + HCI_QUIRK_DMA_REQUIRES_HC_ABORT }, > { /* sentinel */ } > }; > MODULE_DEVICE_TABLE(platform, i3c_hci_driver_ids); > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 699c6d523eed..41bbd912df7f 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -597,6 +597,29 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) > rh_reg_write(RING_OPERATION1, op1_val); > } > > +static bool hci_dma_requires_hc_abort_quirk(struct i3c_hci *hci, struct hci_rh_data *rh) > +{ > + if (!(hci->quirks & HCI_QUIRK_DMA_REQUIRES_HC_ABORT)) > + return false; > + > + reinit_completion(&rh->op_done); > + mipi_i3c_hci_abort(hci); > + wait_for_completion_timeout(&rh->op_done, HZ); > + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); > + > + return true; > +} > + > +static void hci_dma_abort(struct i3c_hci *hci, struct hci_rh_data *rh) > +{ > + if (hci_dma_requires_hc_abort_quirk(hci, rh)) > + return; Move check logic here to make overall logic simple if (hci->quirks & HCI_QUIRK_DMA_REQUIRES_HC_ABORT) { hci_dma_requires_hc_abort_quirk() return; } > + > + reinit_completion(&rh->op_done); > + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); > + wait_for_completion_timeout(&rh->op_done, HZ); move these codes reorg into new patch. Frank > +} > + > static void hci_dma_abort_requires_pio_reset_quirk(struct i3c_hci *hci, struct hci_rh_data *rh) > { > if ((hci->quirks & HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET) && > @@ -630,9 +653,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > hci->enqueue_blocked = true; > spin_unlock_irq(&hci->lock); > /* stop the ring */ > - reinit_completion(&rh->op_done); > - rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); > - wait_for_completion_timeout(&rh->op_done, HZ); > + hci_dma_abort(hci, rh); > spin_lock_irq(&hci->lock); > ring_status = rh_reg_read(RING_STATUS); > if (ring_status & RING_STATUS_RUNNING) { > diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h > index 01237b12d32e..97c31a315a6e 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/hci.h > +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h > @@ -157,9 +157,11 @@ struct i3c_hci_dev_data { > #define HCI_QUIRK_RPM_IBI_ALLOWED BIT(6) /* IBI and Hot-Join allowed while runtime suspended */ > #define HCI_QUIRK_RPM_PARENT_MANAGED BIT(7) /* Runtime PM managed by parent device */ > #define HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET BIT(8) /* Do PIO queue SW resets after DMA abort */ > +#define HCI_QUIRK_DMA_REQUIRES_HC_ABORT BIT(9) /* Use HC_CONTROL ABORT to abort DMA */ > > /* global functions */ > void mipi_i3c_hci_resume(struct i3c_hci *hci); > +void mipi_i3c_hci_abort(struct i3c_hci *hci); > void mipi_i3c_hci_pio_reset(struct i3c_hci *hci); > void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci); > void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci); > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 10:03:36 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 13:03:36 -0400 Subject: [PATCH V3 11/16] i3c: mipi-i3c-hci: Factor out reset-and-restore helper In-Reply-To: <20260504113352.38490-12-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-12-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:47PM +0300, Adrian Hunter wrote: > Factor the reset-and-restore sequence out of i3c_hci_rpm_resume() into > a separate helper. > > This allows the same logic to be reused for recovery paths in subsequent > changes without duplicating suspend/resume handling. > > No functional change. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > > > Changes in V3: > > None > > Changes in V2: > > Drop redundant i3c_hci_sync_irq_inactive(hci) > from i3c_hci_reset_and_restore() because it is called by > hci->io->suspend() anyway > > > drivers/i3c/master/mipi-i3c-hci/core.c | 19 +++++++++++++++++-- > drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ > 2 files changed, 19 insertions(+), 2 deletions(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c > index 8274c84b16be..12a0122fb709 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/core.c > +++ b/drivers/i3c/master/mipi-i3c-hci/core.c > @@ -798,9 +798,8 @@ int i3c_hci_rpm_suspend(struct device *dev) > } > EXPORT_SYMBOL_GPL(i3c_hci_rpm_suspend); > > -int i3c_hci_rpm_resume(struct device *dev) > +static int i3c_hci_do_reset_and_restore(struct i3c_hci *hci) > { > - struct i3c_hci *hci = dev_get_drvdata(dev); > int ret; > > ret = i3c_hci_reset_and_init(hci); > @@ -821,6 +820,22 @@ int i3c_hci_rpm_resume(struct device *dev) > > return 0; > } > + > +int i3c_hci_reset_and_restore(struct i3c_hci *hci) > +{ > + i3c_hci_bus_disable(hci); > + > + hci->io->suspend(hci); > + > + return i3c_hci_do_reset_and_restore(hci); > +} > + > +int i3c_hci_rpm_resume(struct device *dev) > +{ > + struct i3c_hci *hci = dev_get_drvdata(dev); > + > + return i3c_hci_do_reset_and_restore(hci); > +} > EXPORT_SYMBOL_GPL(i3c_hci_rpm_resume); > > static int i3c_hci_runtime_suspend(struct device *dev) > diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h > index 97c31a315a6e..a3151c26827e 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/hci.h > +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h > @@ -175,4 +175,6 @@ int i3c_hci_process_xfer(struct i3c_hci *hci, struct hci_xfer *xfer, int n); > int i3c_hci_rpm_suspend(struct device *dev); > int i3c_hci_rpm_resume(struct device *dev); > > +int i3c_hci_reset_and_restore(struct i3c_hci *hci); > + > #endif > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 11:05:08 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 14:05:08 -0400 Subject: [PATCH V3 13/16] i3c: mipi-i3c-hci: Wait for NoOp commands to complete In-Reply-To: <20260504113352.38490-14-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-14-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:49PM +0300, Adrian Hunter wrote: > When a transfer list is only partially completed due to an error, > hci_dma_dequeue_xfer() overwrites the remaining DMA ring entries with > NoOp commands and restarts the ring to flush them out. > > While NoOp commands are expected to complete successfully, they may still > fail to complete if the DMA ring is stuck. Explicitly wait for the NoOp > commands to finish, and trigger controller recovery if they do not > complete or report an error. > > This ensures that partially completed transfer lists are reliably > resolved and that a stuck ring is recovered promptly. > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > > > Changes in V3: > > None > > Changes in V2: > > Rename completing_xfer to final_xfer > Add missing reinit_completion() > > > drivers/i3c/master/mipi-i3c-hci/dma.c | 39 ++++++++++++++++++++++----- > 1 file changed, 33 insertions(+), 6 deletions(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 376062c0fcbf..90fa621c9d56 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -696,11 +696,33 @@ static void hci_dma_recovery(struct i3c_hci *hci) > dev_err(&hci->master.dev, "Recovery %s\n", ret ? "failed!" : "done"); > } > > +static bool hci_dma_wait_for_noop(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n, > + int noop_pos) > +{ > + struct completion *done = xfer_list->final_xfer->completion; > + bool timeout = !wait_for_completion_timeout(done, HZ); > + u32 error = timeout; > + > + for (int i = noop_pos; i < n && !error; i++) > + error = RESP_STATUS(xfer_list[i].response); > + > + if (!error) > + return true; > + > + if (timeout) > + dev_err(&hci->master.dev, "NoOp timeout error\n"); > + else > + dev_err(&hci->master.dev, "NoOp error %u\n", error); > + > + return false; > +} > + > static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > struct hci_xfer *xfer_list, int n) > { > struct hci_rings_data *rings = hci->io_data; > struct hci_rh_data *rh = &rings->headers[xfer_list[0].ring_number]; > + int noop_pos = -1; > unsigned int i; > bool did_unqueue = false; > u32 ring_status; > @@ -708,7 +730,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > guard(mutex)(&hci->control_mutex); > > spin_lock_irq(&hci->lock); > - > +restart: > ring_status = rh_reg_read(RING_STATUS); > if (ring_status & RING_STATUS_RUNNING) { > /* > @@ -765,11 +787,10 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > *ring_data++ = 0; > } > > - /* disassociate this xfer struct */ > - rh->src_xfers[idx] = NULL; > - > - /* and unmap it */ > - hci_dma_unmap_xfer(hci, xfer, 1); > + if (noop_pos < 0) { > + reinit_completion(xfer->final_xfer->completion); > + noop_pos = i; > + } > > did_unqueue = true; > } > @@ -801,6 +822,12 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, > > wait_for_completion_timeout(&rh->op_done, HZ); > > + if (did_unqueue && !hci_dma_wait_for_noop(hci, xfer_list, n, noop_pos)) { > + spin_lock_irq(&hci->lock); > + hci->recovery_needed = true; > + goto restart; > + } > + > return did_unqueue; > } > > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 11:11:34 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 14:11:34 -0400 Subject: [PATCH V3 14/16] i3c: mipi-i3c-hci: Base timeouts on actual transfer start time In-Reply-To: <20260504113352.38490-15-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-15-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:50PM +0300, Adrian Hunter wrote: > Transfer timeouts are currently measured from the point where a transfer > list is queued to the controller. This can cause transfers to time out > before they have actually started, if earlier queued transfers consume > the timeout interval. > > Fix this by recording when a transfer reaches the head of the queue and > adjusting the timeout calculation to start from that point. The existing > low-overhead completion-based timeout mechanism is preserved, but care is > taken to ensure the transfer start time is consistently recorded for both > PIO and DMA paths. > > This prevents premature timeouts while retaining efficient timeout > handling. > > Signed-off-by: Adrian Hunter > --- > > > Changes in V3: > > None > > Changes in V2: > Do not flag the next transfer as started when there is an error > which halts the controller > Instead flag it started at the end of hci_dma_dequeue_xfer() > Use hci_start_xfer() in pio.c > > > drivers/i3c/master/mipi-i3c-hci/core.c | 19 ++++++++++++++++++- > drivers/i3c/master/mipi-i3c-hci/dma.c | 19 ++++++++++++++++++- > drivers/i3c/master/mipi-i3c-hci/hci.h | 11 +++++++++++ > drivers/i3c/master/mipi-i3c-hci/pio.c | 1 + > 4 files changed, 48 insertions(+), 2 deletions(-) > ... > > #include > +#include > > /* 32-bit word aware bit and mask macros */ > #define W0_MASK(h, l) GENMASK((h) - 0, (l) - 0) > @@ -88,11 +89,13 @@ struct hci_xfer { > u32 cmd_desc[4]; > u32 response; > bool rnw; > + bool started; > void *data; > unsigned int data_len; > unsigned int cmd_tid; > struct completion *completion; > unsigned long timeout; > + unsigned long start_time; it'd better to add unit for start_time Frank > union { > struct { > /* PIO specific */ > @@ -123,6 +126,14 @@ static inline void hci_free_xfer(struct hci_xfer *xfer, unsigned int n) > kfree(xfer); > } > > +static inline void hci_start_xfer(struct hci_xfer *xfer) > +{ > + if (!xfer->started) { > + xfer->started = true; > + xfer->start_time = jiffies; > + } > +} > + > /* This abstracts PIO vs DMA operations */ > struct hci_io_ops { > bool (*irq_handler)(struct i3c_hci *hci); > diff --git a/drivers/i3c/master/mipi-i3c-hci/pio.c b/drivers/i3c/master/mipi-i3c-hci/pio.c > index 8f48a81e65ab..6b8cc5f2b4d2 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/pio.c > +++ b/drivers/i3c/master/mipi-i3c-hci/pio.c > @@ -605,6 +605,7 @@ static bool hci_pio_process_cmd(struct i3c_hci *hci, struct hci_pio_data *pio) > * Finally send the command. > */ > hci_pio_write_cmd(hci, pio->curr_xfer); > + hci_start_xfer(pio->curr_xfer); > /* > * And move on. > */ > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 11:15:02 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 14:15:02 -0400 Subject: [PATCH V3 15/16] i3c: mipi-i3c-hci: Consolidate DMA ring allocation In-Reply-To: <20260504113352.38490-16-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-16-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:51PM +0300, Adrian Hunter wrote: > dma_alloc_coherent() allocates memory in whole pages, which can waste > space when command and response queues are allocated separately. > > Allocate the DMA command and response queues from a single coherent > allocation instead, while preserving the required 4-byte alignment. > > This reduces memory overhead without changing behavior. > > Signed-off-by: Adrian Hunter > --- > > > Changes in V3: > > None > > Changes in V2: > > Check for failed allocation before assignments to avoid doing > arithmetic with NULL pointers > > > drivers/i3c/master/mipi-i3c-hci/dma.c | 24 +++++++++++------------- > 1 file changed, 11 insertions(+), 13 deletions(-) > > diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c > index 6440302c63ca..4029d4d9e784 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -186,14 +186,12 @@ static void hci_dma_free(void *data) > for (int i = 0; i < rings->total; i++) { > rh = &rings->headers[i]; > > - if (rh->xfer) > - dma_free_coherent(rings->sysdev, > - rh->xfer_struct_sz * rh->xfer_entries, > - rh->xfer, rh->xfer_dma); > - if (rh->resp) > - dma_free_coherent(rings->sysdev, > - rh->resp_struct_sz * rh->xfer_entries, > - rh->resp, rh->resp_dma); > + if (rh->xfer) { > + size_t sz = round_up(rh->xfer_struct_sz * rh->xfer_entries, 4); > + > + sz += rh->resp_struct_sz * rh->xfer_entries; > + dma_free_coherent(rings->sysdev, sz, rh->xfer, rh->xfer_dma); Good cleanup. can you save sz into rh when alloc it? Frank > + } > kfree(rh->src_xfers); > if (rh->ibi_status) > dma_free_coherent(rings->sysdev, > @@ -359,18 +357,18 @@ static int hci_dma_init(struct i3c_hci *hci) > dev_dbg(&hci->master.dev, > "xfer_struct_sz = %d, resp_struct_sz = %d", > rh->xfer_struct_sz, rh->resp_struct_sz); > - xfers_sz = rh->xfer_struct_sz * rh->xfer_entries; > + xfers_sz = round_up(rh->xfer_struct_sz * rh->xfer_entries, 4); > resps_sz = rh->resp_struct_sz * rh->xfer_entries; > > - rh->xfer = dma_alloc_coherent(rings->sysdev, xfers_sz, > + rh->xfer = dma_alloc_coherent(rings->sysdev, xfers_sz + resps_sz, > &rh->xfer_dma, GFP_KERNEL); > - rh->resp = dma_alloc_coherent(rings->sysdev, resps_sz, > - &rh->resp_dma, GFP_KERNEL); > rh->src_xfers = > kzalloc_objs(*rh->src_xfers, rh->xfer_entries); > ret = -ENOMEM; > - if (!rh->xfer || !rh->resp || !rh->src_xfers) > + if (!rh->xfer || !rh->src_xfers) > goto err_out; > + rh->resp = rh->xfer + xfers_sz; > + rh->resp_dma = rh->xfer_dma + xfers_sz; > > /* IBIs */ > > -- > 2.51.0 > From Frank.li at nxp.com Tue May 12 11:15:49 2026 From: Frank.li at nxp.com (Frank Li) Date: Tue, 12 May 2026 14:15:49 -0400 Subject: [PATCH V3 16/16] i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum In-Reply-To: <20260504113352.38490-17-adrian.hunter@intel.com> References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-17-adrian.hunter@intel.com> Message-ID: On Mon, May 04, 2026 at 02:33:52PM +0300, Adrian Hunter wrote: > The DMA transfer ring is currently limited to 16 entries, despite the > MIPI I3C HCI supporting up to 32 devices. When the ring lacks space for a > new transfer list, the driver returns -EBUSY, which can be unexpected > for clients. > > Increase the DMA transfer ring size to the maximum supported value of > 255 entries. This effectively eliminates ring-space exhaustion in > practice and avoids the complexity of adding secondary queuing > mechanisms. > > Even at the maximum size, the memory overhead remains small > (approximately 24 bytes per entry by default). > > Signed-off-by: Adrian Hunter > --- Reviewed-by: Frank Li > > > Changes in V2 and V3: > > None > > > 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 4029d4d9e784..9549d98add4b 100644 > --- a/drivers/i3c/master/mipi-i3c-hci/dma.c > +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c > @@ -27,7 +27,7 @@ > */ > > #define XFER_RINGS 1 /* max: 8 */ > -#define XFER_RING_ENTRIES 16 /* max: 255 */ > +#define XFER_RING_ENTRIES 255 /* max: 255 */ > > #define IBI_RINGS 1 /* max: 8 */ > #define IBI_STATUS_RING_ENTRIES 32 /* max: 255 */ > -- > 2.51.0 > From david.nystrom at est.tech Tue May 12 12:42:57 2026 From: david.nystrom at est.tech (=?ISO-8859-15?Q?David_Nystr=F6m?=) Date: Tue, 12 May 2026 21:42:57 +0200 (CEST) Subject: [PATCH 2/8] i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-3-adrian.hunter@intel.com> Message-ID: On Tue, 12 May 2026, Frank Li wrote: > On Tue, May 12, 2026 at 03:17:26PM +0300, Adrian Hunter wrote: >> i3c_set_hotjoin() dispatches the controller's enable_hotjoin() or >> disable_hotjoin() op and updates master->hotjoin under >> i3c_bus_normaluse_lock(). That lock is a read-side acquisition of >> bus->lock (down_read()), so it does not exclude concurrent callers. >> >> The hotjoin sysfs attribute can be opened multiple times, and writes >> through different opens are not serialized. Two concurrent writers >> to "hotjoin" can therefore race in i3c_set_hotjoin(), with the >> controller op and the master->hotjoin store from one call interleaving >> with the other. The hardware enable/disable state and the value reported >> by hotjoin_show() can end up out of sync. >> >> Take i3c_bus_maintenance_lock() instead. Toggling Hot Join enable >> changes bus state and is conceptually a maintenance operation, so the >> write-side acquisition of bus->lock is the appropriate lock and >> serializes concurrent callers against each other and against other >> maintenance operations. > > It should be bug fix, add fix tag here. Agreed, Fixes: "i3c: master: Add sysfs option to rescan bus via entdaa" Is this series headed for 7.1-rc3 ? if not, its probably wise to revert the sysfs addition from 7.1-rc > Frank >> >> Signed-off-by: Adrian Hunter >> --- >> drivers/i3c/master.c | 4 ++-- >> 1 file changed, 2 insertions(+), 2 deletions(-) >> >> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c >> index ab11e2d79aab..38ffc8713167 100644 >> --- a/drivers/i3c/master.c >> +++ b/drivers/i3c/master.c >> @@ -649,7 +649,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >> return ret; >> } >> >> - i3c_bus_normaluse_lock(&master->bus); >> + i3c_bus_maintenance_lock(&master->bus); >> >> if (enable) >> ret = master->ops->enable_hotjoin(master); >> @@ -659,7 +659,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >> if (!ret) >> master->hotjoin = enable; >> >> - i3c_bus_normaluse_unlock(&master->bus); >> + i3c_bus_maintenance_unlock(&master->bus); >> >> if ((enable && ret) || (!enable && !ret) || master->rpm_ibi_allowed) >> i3c_master_rpm_put(master); >> -- >> 2.51.0 >> > > -- > linux-i3c mailing list > linux-i3c at lists.infradead.org > http://lists.infradead.org/mailman/listinfo/linux-i3c > From adrian.hunter at intel.com Tue May 12 22:01:02 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 13 May 2026 08:01:02 +0300 Subject: [PATCH 2/8] i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-3-adrian.hunter@intel.com> Message-ID: <571f1f5e-544e-4c1b-8ed1-4ddf6bfa9493@intel.com> On 12/05/2026 22:42, David Nystr?m wrote: > > > On Tue, 12 May 2026, Frank Li wrote: > >> On Tue, May 12, 2026 at 03:17:26PM +0300, Adrian Hunter wrote: >>> i3c_set_hotjoin() dispatches the controller's enable_hotjoin() or >>> disable_hotjoin() op and updates master->hotjoin under >>> i3c_bus_normaluse_lock(). That lock is a read-side acquisition of >>> bus->lock (down_read()), so it does not exclude concurrent callers. >>> >>> The hotjoin sysfs attribute can be opened multiple times, and writes >>> through different opens are not serialized.? Two concurrent writers >>> to "hotjoin" can therefore race in i3c_set_hotjoin(), with the >>> controller op and the master->hotjoin store from one call interleaving >>> with the other.? The hardware enable/disable state and the value reported >>> by hotjoin_show() can end up out of sync. >>> >>> Take i3c_bus_maintenance_lock() instead. Toggling Hot Join enable >>> changes bus state and is conceptually a maintenance operation, so the >>> write-side acquisition of bus->lock is the appropriate lock and >>> serializes concurrent callers against each other and against other >>> maintenance operations. >> >> It should be bug fix, add fix tag here. > > Agreed, Fixes: "i3c: master: Add sysfs option to rescan bus via entdaa" > Is this series headed for 7.1-rc3 ? if not, its probably wise to revert the sysfs addition from 7.1-rc "i3c: master: Add sysfs option to rescan bus via entdaa" added "do_daa". "hotjoin" is a different sysfs attribute. > >> Frank >>> >>> Signed-off-by: Adrian Hunter >>> --- >>> ?drivers/i3c/master.c | 4 ++-- >>> ?1 file changed, 2 insertions(+), 2 deletions(-) >>> >>> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c >>> index ab11e2d79aab..38ffc8713167 100644 >>> --- a/drivers/i3c/master.c >>> +++ b/drivers/i3c/master.c >>> @@ -649,7 +649,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >>> ???????????? return ret; >>> ???? } >>> >>> -??? i3c_bus_normaluse_lock(&master->bus); >>> +??? i3c_bus_maintenance_lock(&master->bus); >>> >>> ???? if (enable) >>> ???????? ret = master->ops->enable_hotjoin(master); >>> @@ -659,7 +659,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >>> ???? if (!ret) >>> ???????? master->hotjoin = enable; >>> >>> -??? i3c_bus_normaluse_unlock(&master->bus); >>> +??? i3c_bus_maintenance_unlock(&master->bus); >>> >>> ???? if ((enable && ret) || (!enable && !ret) || master->rpm_ibi_allowed) >>> ???????? i3c_master_rpm_put(master); >>> -- >>> 2.51.0 >>> >> >> --? >> linux-i3c mailing list >> linux-i3c at lists.infradead.org >> http://lists.infradead.org/mailman/listinfo/linux-i3c >> From adrian.hunter at intel.com Tue May 12 22:09:07 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 13 May 2026 08:09:07 +0300 Subject: [PATCH 2/8] i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-3-adrian.hunter@intel.com> Message-ID: <21cc860b-e456-4879-b8b4-b699410d3662@intel.com> On 12/05/2026 19:11, Frank Li wrote: > On Tue, May 12, 2026 at 03:17:26PM +0300, Adrian Hunter wrote: >> i3c_set_hotjoin() dispatches the controller's enable_hotjoin() or >> disable_hotjoin() op and updates master->hotjoin under >> i3c_bus_normaluse_lock(). That lock is a read-side acquisition of >> bus->lock (down_read()), so it does not exclude concurrent callers. >> >> The hotjoin sysfs attribute can be opened multiple times, and writes >> through different opens are not serialized. Two concurrent writers >> to "hotjoin" can therefore race in i3c_set_hotjoin(), with the >> controller op and the master->hotjoin store from one call interleaving >> with the other. The hardware enable/disable state and the value reported >> by hotjoin_show() can end up out of sync. >> >> Take i3c_bus_maintenance_lock() instead. Toggling Hot Join enable >> changes bus state and is conceptually a maintenance operation, so the >> write-side acquisition of bus->lock is the appropriate lock and >> serializes concurrent callers against each other and against other >> maintenance operations. > > It should be bug fix, add fix tag here. Ok Note, currently it can only go wrong if user space is trying to enable and disable hotjoin at the same time, which is already broken - user space itself needs a way to synchronize its hotjoin policy. > > Frank >> >> Signed-off-by: Adrian Hunter >> --- >> drivers/i3c/master.c | 4 ++-- >> 1 file changed, 2 insertions(+), 2 deletions(-) >> >> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c >> index ab11e2d79aab..38ffc8713167 100644 >> --- a/drivers/i3c/master.c >> +++ b/drivers/i3c/master.c >> @@ -649,7 +649,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >> return ret; >> } >> >> - i3c_bus_normaluse_lock(&master->bus); >> + i3c_bus_maintenance_lock(&master->bus); >> >> if (enable) >> ret = master->ops->enable_hotjoin(master); >> @@ -659,7 +659,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >> if (!ret) >> master->hotjoin = enable; >> >> - i3c_bus_normaluse_unlock(&master->bus); >> + i3c_bus_maintenance_unlock(&master->bus); >> >> if ((enable && ret) || (!enable && !ret) || master->rpm_ibi_allowed) >> i3c_master_rpm_put(master); >> -- >> 2.51.0 >> From adrian.hunter at intel.com Tue May 12 22:31:56 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 13 May 2026 08:31:56 +0300 Subject: [PATCH 4/8] i3c: master: Ensure Hot-Join operations are stopped on shutdown In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-5-adrian.hunter@intel.com> Message-ID: <9d2a7c42-7eb1-4de2-ac9c-c1777d7fcb13@intel.com> On 12/05/2026 19:27, Frank Li wrote: > On Tue, May 12, 2026 at 03:17:28PM +0300, Adrian Hunter wrote: >> System shutdown invokes each device's bus shutdown callback to quiesce >> hardware, but the I3C bus type does not currently implement one. As a >> result, on shutdown the controller's Hot-Join work and any in-flight >> i3c_master_do_daa() can keep running (or be newly triggered) while the >> rest of the system is being torn down. >> >> A similar window exists at i3c_master_unregister() time: cancel_work_sync() >> on hj_work prevents queued work from completing, but does not stop a >> fresh Hot-Join IBI from re-queueing the worker, nor a concurrent sysfs >> writer from toggling Hot-Join via i3c_set_hotjoin(). >> >> Introduce a single "shutting down" gate in the I3C core, set under the >> bus maintenance lock so it is observed by any in-progress DAA path >> before pending work is cancelled. Install an i3c_bus_type shutdown >> callback that engages this gate for master devices during system >> shutdown, and use the same gate in i3c_master_unregister() so both >> paths get identical guarantees. >> >> Once the gate is engaged, the Hot-Join worker, i3c_master_do_daa_ext() >> and i3c_set_hotjoin() all bail out cleanly, so Hot-Join IBIs that race >> with shutdown become no-ops, direct DAA callers see -ENODEV, and sysfs >> writers can no longer re-enable Hot-Join through ops->enable_hotjoin() >> while the controller is going away. >> >> No functional change for the steady-state runtime path; the new checks >> only take effect once the controller has been marked as shutting down. >> >> Signed-off-by: Adrian Hunter >> --- >> drivers/i3c/master.c | 52 +++++++++++++++++++++++++++----------- >> include/linux/i3c/master.h | 2 ++ >> 2 files changed, 39 insertions(+), 15 deletions(-) >> >> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c >> index cdb5cb2aa65d..a59c4b744b36 100644 >> --- a/drivers/i3c/master.c >> +++ b/drivers/i3c/master.c >> @@ -368,14 +368,6 @@ static void i3c_device_remove(struct device *dev) >> driver->remove(i3cdev); >> } >> >> -const struct bus_type i3c_bus_type = { >> - .name = "i3c", >> - .match = i3c_device_match, >> - .probe = i3c_device_probe, >> - .remove = i3c_device_remove, >> -}; >> -EXPORT_SYMBOL_GPL(i3c_bus_type); >> - > > why need move this tunk? To avoid forward declarations. i3c_device_shutdown() references i3c_masterdev_type so it needs to be after i3c_masterdev_type definition. i3c_device_shutdown() is being added to struct i3c_bus_type, so i3c_bus_type needs to be after i3c_device_shutdown(). > >> static enum i3c_addr_slot_status >> i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask) >> { >> @@ -637,7 +629,8 @@ static void i3c_master_hj_work_fn(struct work_struct *work) >> { >> struct i3c_master_controller *master = container_of(work, typeof(*master), hj_work); >> >> - i3c_master_do_daa(master); >> + if (!master->shutting_down) >> + i3c_master_do_daa(master); >> } >> >> static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >> @@ -658,7 +651,9 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >> >> i3c_bus_maintenance_lock(&master->bus); > > later, consider change to use cleanup, so > if (master->shutting_down) > return -ENODEV > > and avoid use else if branch. > > but this change is okay for now. > > Frank From adrian.hunter at intel.com Tue May 12 22:45:55 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 13 May 2026 08:45:55 +0300 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> Message-ID: <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> On 12/05/2026 19:39, Frank Li wrote: > On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: >> Master drivers may invoke i3c_master_do_daa_ext() during resume to >> re-run Dynamic Address Assignment. As well as assigning addresses to >> any newly arrived devices, this restores the dynamic address of devices >> that lost it across system suspend, so it has to run as part of the >> controller's resume path. >> >> A side effect of i3c_master_do_daa_ext() today is that it also >> registers any newly discovered I3C devices with the driver model >> inline, via i3c_master_register_new_i3c_devs(). Doing that from the >> resume path is problematic: a hot-join-capable device may join the bus >> during this same DAA, and registering it immediately would push driver >> model work (probing, sysfs, etc.) into the controller's resume context, >> where the rest of the system is not yet fully resumed and the >> controller driver is still partway through its own resume sequence. >> >> Decouple discovery from registration: add a reg_work work item to >> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it >> on master->wq (the freezable workqueue) instead of calling >> i3c_master_register_new_i3c_devs() directly. The worker performs the >> registration only when the controller is not shutting_down, and is >> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is >> freezable, any newly observed devices end up being registered after >> the system has finished resuming. >> >> i3c_master_register() also routes its initial post-bus-init registration >> through reg_work, using flush_work() to keep probe-time behavior >> synchronous. This keeps a single registration code path and ensures the >> worker is the only writer of desc->dev. > > why not direct use hj_work? i3c_master_register_new_i3c_devs() use of desc->dev is racy, so i3c_master_register_new_i3c_devs() must not be allowed to race with itself. Having it only ever run via reg_work achieves that. From u.kleine-koenig at baylibre.com Mon May 4 22:50:16 2026 From: u.kleine-koenig at baylibre.com (Uwe =?utf-8?Q?Kleine-K=C3=B6nig_=28The_Capable_Hub=29?=) Date: Tue, 5 May 2026 07:50:16 +0200 Subject: [PATCH] i3c: Consistently define pci_device_ids using named initializers In-Reply-To: References: <20260504143324.2122737-2-u.kleine-koenig@baylibre.com> Message-ID: On Mon, May 04, 2026 at 04:42:26PM -0400, Frank Li wrote: > On Mon, May 04, 2026 at 04:33:15PM +0200, Uwe Kleine-K?nig (The Capable Hub) wrote: > > The .driver_data member of the various struct pci_device_id arrays were > > initialized by list expressions. This isn't easily readable if you're > > not into PCI. Using named initializers is more explicit and thus easier > > to parse. > > > > This change doesn't introduce changes to the compiled pci_device_id > > arrays. Tested on x86 and arm64. > > > > Signed-off-by: Uwe Kleine-K?nig (The Capable Hub) > > --- > > Hello, > > > > The secret plan is to make struct pci_device_id::driver_data an > > anonymous union (similar to > > https://lore.kernel.org/all/cover.1776579304.git.u.kleine-koenig at baylibre.com/) > > and that requires named initializers. But IMHO it's also a nice cleanup > > on its own. > > > > The anonymous union will allow changes like the following: > > > > - { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data = (kernel_ulong_t)&intel_mi_1_info }, > > + { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info }, > > I think it is good. Can you directly change to to > { PCI_VDEVICE(INTEL, 0x4d7c), .driver_data_ptr = &intel_mi_1_info } > > I think use anonymous union {.driver_data; .driver_data_ptr} don't impact > the current drivers. I cannot because pci_device_id with the union cannot be initialized using { PCI_VDEVICE(INTEL, 0x4d7c), (kernel_ulong_t)&intel_mi_1_info }, That's why all drivers must be adapted first to use named initializers. Best regards Uwe -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 488 bytes Desc: not available URL: From conor at kernel.org Tue May 5 09:24:21 2026 From: conor at kernel.org (Conor Dooley) Date: Tue, 5 May 2026 17:24:21 +0100 Subject: [PATCH v5 1/5] dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible In-Reply-To: <20260505071327.125787-2-manikandan.m@microchip.com> References: <20260505071327.125787-1-manikandan.m@microchip.com> <20260505071327.125787-2-manikandan.m@microchip.com> Message-ID: <20260505-prewar-disarm-04392f03d61e@spud> On Tue, May 05, 2026 at 12:43:23PM +0530, Manikandan Muralidharan wrote: > Add the microchip,sama7d65-i3c-hci compatible string to the MIPI I3C > HCI binding. The Microchip SAMA7D65 I3C controller is based on the > MIPI HCI specification but requires two clocks, so add a conditional > constraint when this compatible is present. > > Signed-off-by: Manikandan Muralidharan Acked-by: Conor Dooley pw-bot: not-applicable -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 228 bytes Desc: not available URL: From conor at kernel.org Mon May 11 09:02:18 2026 From: conor at kernel.org (Conor Dooley) Date: Mon, 11 May 2026 17:02:18 +0100 Subject: [PATCH v2 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <20260511031945.3228-3-jszhang@kernel.org> References: <20260511031945.3228-1-jszhang@kernel.org> <20260511031945.3228-3-jszhang@kernel.org> Message-ID: <20260511-amnesty-afoot-84537aafc02c@spud> On Mon, May 11, 2026 at 11:19:44AM +0800, Jisheng Zhang wrote: > Add dt-binding for support of apb reset which is to reset the APB > interface. > > Signed-off-by: Jisheng Zhang Please squash both dt-binding patches. > --- > Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 2 ++ > 1 file changed, 2 insertions(+) > > diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > index 613dce7757bc..2575442b28ff 100644 > --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > @@ -38,10 +38,12 @@ properties: > resets: > items: > - description: Reset signal > + - description: APB interface reset signal > > reset-names: > items: > - const: core_rst > + - const: apb_rst Drop the _rst here please, not as if these can be anything other than a reset! pw-bot: changes-requested Cheers, Conor. > > interrupts: > maxItems: 1 > -- > 2.53.0 > -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 228 bytes Desc: not available URL: From david.nystrom at est.tech Wed May 13 03:20:09 2026 From: david.nystrom at est.tech (=?ISO-8859-15?Q?David_Nystr=F6m?=) Date: Wed, 13 May 2026 12:20:09 +0200 (CEST) Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> Message-ID: On Wed, 13 May 2026, Adrian Hunter wrote: > On 12/05/2026 19:39, Frank Li wrote: >> On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: >>> Master drivers may invoke i3c_master_do_daa_ext() during resume to >>> re-run Dynamic Address Assignment. As well as assigning addresses to >>> any newly arrived devices, this restores the dynamic address of devices >>> that lost it across system suspend, so it has to run as part of the >>> controller's resume path. >>> >>> A side effect of i3c_master_do_daa_ext() today is that it also >>> registers any newly discovered I3C devices with the driver model >>> inline, via i3c_master_register_new_i3c_devs(). Doing that from the >>> resume path is problematic: a hot-join-capable device may join the bus >>> during this same DAA, and registering it immediately would push driver >>> model work (probing, sysfs, etc.) into the controller's resume context, >>> where the rest of the system is not yet fully resumed and the >>> controller driver is still partway through its own resume sequence. >>> >>> Decouple discovery from registration: add a reg_work work item to >>> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it >>> on master->wq (the freezable workqueue) instead of calling >>> i3c_master_register_new_i3c_devs() directly. The worker performs the >>> registration only when the controller is not shutting_down, and is >>> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is >>> freezable, any newly observed devices end up being registered after >>> the system has finished resuming. >>> >>> i3c_master_register() also routes its initial post-bus-init registration >>> through reg_work, using flush_work() to keep probe-time behavior >>> synchronous. This keeps a single registration code path and ensures the >>> worker is the only writer of desc->dev. >> >> why not direct use hj_work? > > i3c_master_register_new_i3c_devs() use of desc->dev is racy, so > i3c_master_register_new_i3c_devs() must not be allowed to race > with itself. Having it only ever run via reg_work achieves that. This race was introduced in 3a379bbcea0a ("i3c: Add core I3C infrastructure") But since this path was exposed via sysfs in latest 7.1-rc via: 8ea0b60bc00d ("i3c: master: Add sysfs option to rescan bus via entdaa") I would argue that we should revert the sysfs addition from 7.1-rc until this fix is in place. > > -- > linux-i3c mailing list > linux-i3c at lists.infradead.org > http://lists.infradead.org/mailman/listinfo/linux-i3c > From david.nystrom at est.tech Wed May 13 03:21:33 2026 From: david.nystrom at est.tech (=?ISO-8859-15?Q?David_Nystr=F6m?=) Date: Wed, 13 May 2026 12:21:33 +0200 (CEST) Subject: [PATCH 2/8] i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock In-Reply-To: <571f1f5e-544e-4c1b-8ed1-4ddf6bfa9493@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-3-adrian.hunter@intel.com> <571f1f5e-544e-4c1b-8ed1-4ddf6bfa9493@intel.com> Message-ID: On Wed, 13 May 2026, Adrian Hunter wrote: > On 12/05/2026 22:42, David Nystr?m wrote: >> >> >> On Tue, 12 May 2026, Frank Li wrote: >> >>> On Tue, May 12, 2026 at 03:17:26PM +0300, Adrian Hunter wrote: >>>> i3c_set_hotjoin() dispatches the controller's enable_hotjoin() or >>>> disable_hotjoin() op and updates master->hotjoin under >>>> i3c_bus_normaluse_lock(). That lock is a read-side acquisition of >>>> bus->lock (down_read()), so it does not exclude concurrent callers. >>>> >>>> The hotjoin sysfs attribute can be opened multiple times, and writes >>>> through different opens are not serialized.? Two concurrent writers >>>> to "hotjoin" can therefore race in i3c_set_hotjoin(), with the >>>> controller op and the master->hotjoin store from one call interleaving >>>> with the other.? The hardware enable/disable state and the value reported >>>> by hotjoin_show() can end up out of sync. >>>> >>>> Take i3c_bus_maintenance_lock() instead. Toggling Hot Join enable >>>> changes bus state and is conceptually a maintenance operation, so the >>>> write-side acquisition of bus->lock is the appropriate lock and >>>> serializes concurrent callers against each other and against other >>>> maintenance operations. >>> >>> It should be bug fix, add fix tag here. >> >> Agreed, Fixes: "i3c: master: Add sysfs option to rescan bus via entdaa" >> Is this series headed for 7.1-rc3 ? if not, its probably wise to revert the sysfs addition from 7.1-rc > > "i3c: master: Add sysfs option to rescan bus via entdaa" added "do_daa". > "hotjoin" is a different sysfs attribute. Replied to wrong patch, correct patch commented now. >> >>> Frank >>>> >>>> Signed-off-by: Adrian Hunter >>>> --- >>>> ?drivers/i3c/master.c | 4 ++-- >>>> ?1 file changed, 2 insertions(+), 2 deletions(-) >>>> >>>> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c >>>> index ab11e2d79aab..38ffc8713167 100644 >>>> --- a/drivers/i3c/master.c >>>> +++ b/drivers/i3c/master.c >>>> @@ -649,7 +649,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >>>> ???????????? return ret; >>>> ???? } >>>> >>>> -??? i3c_bus_normaluse_lock(&master->bus); >>>> +??? i3c_bus_maintenance_lock(&master->bus); >>>> >>>> ???? if (enable) >>>> ???????? ret = master->ops->enable_hotjoin(master); >>>> @@ -659,7 +659,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) >>>> ???? if (!ret) >>>> ???????? master->hotjoin = enable; >>>> >>>> -??? i3c_bus_normaluse_unlock(&master->bus); >>>> +??? i3c_bus_maintenance_unlock(&master->bus); >>>> >>>> ???? if ((enable && ret) || (!enable && !ret) || master->rpm_ibi_allowed) >>>> ???????? i3c_master_rpm_put(master); >>>> -- >>>> 2.51.0 >>>> >>> >>> --? >>> linux-i3c mailing list >>> linux-i3c at lists.infradead.org >>> http://lists.infradead.org/mailman/listinfo/linux-i3c >>> > > From Frank.li at nxp.com Wed May 13 12:03:02 2026 From: Frank.li at nxp.com (Frank Li) Date: Wed, 13 May 2026 15:03:02 -0400 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> Message-ID: On Wed, May 13, 2026 at 08:45:55AM +0300, Adrian Hunter wrote: > On 12/05/2026 19:39, Frank Li wrote: > > On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: > >> Master drivers may invoke i3c_master_do_daa_ext() during resume to > >> re-run Dynamic Address Assignment. As well as assigning addresses to > >> any newly arrived devices, this restores the dynamic address of devices > >> that lost it across system suspend, so it has to run as part of the > >> controller's resume path. > >> > >> A side effect of i3c_master_do_daa_ext() today is that it also > >> registers any newly discovered I3C devices with the driver model > >> inline, via i3c_master_register_new_i3c_devs(). Doing that from the > >> resume path is problematic: a hot-join-capable device may join the bus > >> during this same DAA, and registering it immediately would push driver > >> model work (probing, sysfs, etc.) into the controller's resume context, > >> where the rest of the system is not yet fully resumed and the > >> controller driver is still partway through its own resume sequence. > >> > >> Decouple discovery from registration: add a reg_work work item to > >> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it > >> on master->wq (the freezable workqueue) instead of calling > >> i3c_master_register_new_i3c_devs() directly. The worker performs the > >> registration only when the controller is not shutting_down, and is > >> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is > >> freezable, any newly observed devices end up being registered after > >> the system has finished resuming. > >> > >> i3c_master_register() also routes its initial post-bus-init registration > >> through reg_work, using flush_work() to keep probe-time behavior > >> synchronous. This keeps a single registration code path and ensures the > >> worker is the only writer of desc->dev. > > > > why not direct use hj_work? > > i3c_master_register_new_i3c_devs() use of desc->dev is racy, so > i3c_master_register_new_i3c_devs() must not be allowed to race > with itself. Having it only ever run via reg_work achieves that. Sorry, I have not understand these, Can provide some detail? Frank > From Frank.li at nxp.com Wed May 13 12:04:35 2026 From: Frank.li at nxp.com (Frank Li) Date: Wed, 13 May 2026 15:04:35 -0400 Subject: [PATCH 4/8] i3c: master: Ensure Hot-Join operations are stopped on shutdown In-Reply-To: <9d2a7c42-7eb1-4de2-ac9c-c1777d7fcb13@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-5-adrian.hunter@intel.com> <9d2a7c42-7eb1-4de2-ac9c-c1777d7fcb13@intel.com> Message-ID: On Wed, May 13, 2026 at 08:31:56AM +0300, Adrian Hunter wrote: > On 12/05/2026 19:27, Frank Li wrote: > > On Tue, May 12, 2026 at 03:17:28PM +0300, Adrian Hunter wrote: > >> System shutdown invokes each device's bus shutdown callback to quiesce > >> hardware, but the I3C bus type does not currently implement one. As a > >> result, on shutdown the controller's Hot-Join work and any in-flight > >> i3c_master_do_daa() can keep running (or be newly triggered) while the > >> rest of the system is being torn down. > >> > >> A similar window exists at i3c_master_unregister() time: cancel_work_sync() > >> on hj_work prevents queued work from completing, but does not stop a > >> fresh Hot-Join IBI from re-queueing the worker, nor a concurrent sysfs > >> writer from toggling Hot-Join via i3c_set_hotjoin(). > >> > >> Introduce a single "shutting down" gate in the I3C core, set under the > >> bus maintenance lock so it is observed by any in-progress DAA path > >> before pending work is cancelled. Install an i3c_bus_type shutdown > >> callback that engages this gate for master devices during system > >> shutdown, and use the same gate in i3c_master_unregister() so both > >> paths get identical guarantees. > >> > >> Once the gate is engaged, the Hot-Join worker, i3c_master_do_daa_ext() > >> and i3c_set_hotjoin() all bail out cleanly, so Hot-Join IBIs that race > >> with shutdown become no-ops, direct DAA callers see -ENODEV, and sysfs > >> writers can no longer re-enable Hot-Join through ops->enable_hotjoin() > >> while the controller is going away. > >> > >> No functional change for the steady-state runtime path; the new checks > >> only take effect once the controller has been marked as shutting down. > >> > >> Signed-off-by: Adrian Hunter > >> --- > >> drivers/i3c/master.c | 52 +++++++++++++++++++++++++++----------- > >> include/linux/i3c/master.h | 2 ++ > >> 2 files changed, 39 insertions(+), 15 deletions(-) > >> > >> diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c > >> index cdb5cb2aa65d..a59c4b744b36 100644 > >> --- a/drivers/i3c/master.c > >> +++ b/drivers/i3c/master.c > >> @@ -368,14 +368,6 @@ static void i3c_device_remove(struct device *dev) > >> driver->remove(i3cdev); > >> } > >> > >> -const struct bus_type i3c_bus_type = { > >> - .name = "i3c", > >> - .match = i3c_device_match, > >> - .probe = i3c_device_probe, > >> - .remove = i3c_device_remove, > >> -}; > >> -EXPORT_SYMBOL_GPL(i3c_bus_type); > >> - > > > > why need move this tunk? > > To avoid forward declarations. > > i3c_device_shutdown() references i3c_masterdev_type so it needs to be > after i3c_masterdev_type definition. i3c_device_shutdown() is being > added to struct i3c_bus_type, so i3c_bus_type needs to be after > i3c_device_shutdown(). Okay Reviewed-by: Frank Li > > > > >> static enum i3c_addr_slot_status > >> i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask) > >> { > >> @@ -637,7 +629,8 @@ static void i3c_master_hj_work_fn(struct work_struct *work) > >> { > >> struct i3c_master_controller *master = container_of(work, typeof(*master), hj_work); > >> > >> - i3c_master_do_daa(master); > >> + if (!master->shutting_down) > >> + i3c_master_do_daa(master); > >> } > >> > >> static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) > >> @@ -658,7 +651,9 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) > >> > >> i3c_bus_maintenance_lock(&master->bus); > > > > later, consider change to use cleanup, so > > if (master->shutting_down) > > return -ENODEV > > > > and avoid use else if branch. > > > > but this change is okay for now. > > > > Frank > From krzk at kernel.org Thu May 14 05:17:55 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Thu, 14 May 2026 14:17:55 +0200 Subject: [PATCH v2 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <20260511-amnesty-afoot-84537aafc02c@spud> References: <20260511031945.3228-1-jszhang@kernel.org> <20260511031945.3228-3-jszhang@kernel.org> <20260511-amnesty-afoot-84537aafc02c@spud> Message-ID: <20260514-fat-unyielding-jaguarundi-a5ddc6@quoll> On Mon, May 11, 2026 at 05:02:18PM +0100, Conor Dooley wrote: > On Mon, May 11, 2026 at 11:19:44AM +0800, Jisheng Zhang wrote: > > Add dt-binding for support of apb reset which is to reset the APB > > interface. > > > > Signed-off-by: Jisheng Zhang > > Please squash both dt-binding patches. I think this should stay separate, because first commit is trying to fix undocumented existing ABI. It will have different rationale and could be chosen for backports. Best regards, Krzysztof From adrian.hunter at intel.com Thu May 14 22:30:21 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 08:30:21 +0300 Subject: [PATCH V3 08/16] i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer In-Reply-To: References: <20260504113352.38490-1-adrian.hunter@intel.com> <20260504113352.38490-9-adrian.hunter@intel.com> Message-ID: On 12/05/2026 19:50, Frank Li wrote: > On Mon, May 04, 2026 at 02:33:44PM +0300, Adrian Hunter wrote: >> Software ABORT of the DMA ring is used to recover from transfer list >> timeouts, but it is inherently racy. The intended transfer list may >> complete just before the ABORT takes effect, causing the subsequent >> transfer list to be aborted instead. >> >> In this case, an incomplete transfer list may remain in the ring and has >> not yet been processed by hci_dma_dequeue_xfer(). Restarting the DMA >> ring at that point can lead to unpredictable results. >> >> Detect when the next queued transfer is not the first entry of a transfer >> list and does not belong to the list currently being dequeued. In that >> case, skip restarting the DMA ring and defer recovery until a subsequent >> call to hci_dma_dequeue_xfer(), which will safely restart the ring once >> the incomplete list is handled. >> >> Signed-off-by: Adrian Hunter >> --- >> >> >> Changes in V3: >> >> None >> >> Changes in V2: >> >> Renamed completing_xfer to final_xfer >> >> >> drivers/i3c/master/mipi-i3c-hci/dma.c | 15 +++++++++++++++ >> drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + >> 2 files changed, 16 insertions(+) >> >> diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c >> index 899fdf6555a8..268f54b32101 100644 >> --- a/drivers/i3c/master/mipi-i3c-hci/dma.c >> +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c >> @@ -503,6 +503,7 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, >> u32 *ring_data = rh->xfer + rh->xfer_struct_sz * enqueue_ptr; >> >> xfer->final_xfer = xfer_list + n - 1; >> + xfer->xfer_list_pos = i; >> >> /* store cmd descriptor */ >> *ring_data++ = xfer->cmd_desc[0]; >> @@ -669,6 +670,20 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, >> } >> } >> >> + /* >> + * A software ABORT may race with transfer completion and abort the next >> + * transfer list instead. Detect that case, and do not restart the ring. >> + * It will be handled by a subsequent dequeue. >> + */ >> + if (!did_unqueue) { >> + struct hci_xfer *xfer = rh->src_xfers[rh->done_ptr]; >> + >> + if (xfer && xfer->xfer_list_pos && xfer->final_xfer != xfer_list->final_xfer) { >> + spin_unlock_irq(&hci->lock); > > Is it possible to use auto cleanup to handle lock()? No, there are 2 paths that do not return directly after unlocking and do different things. > > Frank >> + return false; >> + } >> + } >> + >> /* restart the ring */ >> reinit_completion(&rh->op_done); >> mipi_i3c_hci_resume(hci); >> diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h >> index f07fc627d4d2..83d4f13a68a3 100644 >> --- a/drivers/i3c/master/mipi-i3c-hci/hci.h >> +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h >> @@ -107,6 +107,7 @@ struct hci_xfer { >> struct hci_xfer *final_xfer; >> int ring_number; >> int ring_entry; >> + int xfer_list_pos; >> }; >> }; >> }; >> -- >> 2.51.0 >> From hechushiguitu666 at gmail.com Fri May 15 08:56:35 2026 From: hechushiguitu666 at gmail.com (Haoyu Lu) Date: Fri, 15 May 2026 23:56:35 +0800 Subject: [RFC PATCH] i3c: dw: add DMA support for data transfers Message-ID: <20260515155638.1840-1-hechushiguitu666@gmail.com> Add DMA support for the DesignWare I3C master controller to accelerate data transfers. The driver now requests tx and rx DMA channels during probe and uses them for data transfers of 4 bytes or more. Transfers smaller than 4 bytes, or transfers where DMA setup fails, fall back to PIO mode. DMA tail bytes (non-4-byte-aligned remainder) are handled with PIO as well. DMA transfers may sleep, so the xferqueue spinlock is temporarily dropped for DMA operations. This is safe because xferqueue.cur is only changed when a transfer completes, and the calling context serializes simultaneous transfers. The DMA enable bit (DEV_CTRL_DMA_EN, BIT(28)) is set when DMA is used and cleared otherwise, to ensure the hardware uses the correct data transfer path. Signed-off-by: Haoyu Lu --- This is an RFC patch to gather feedback on the overall approach before a formal submission. A few specific questions for reviewers: 1. DMA threshold: The current implementation uses DMA for transfers >= 4 bytes, with tail bytes handled by PIO. Is 4 bytes a reasonable threshold, or should we consider a higher value (e.g. 16 or 32) to avoid DMA setup overhead for very small transfers? 2. Spinlock safety: dw_i3c_master_start_xfer_locked() and dw_i3c_master_end_xfer_locked() temporarily drop xferqueue.lock around DMA operations. The analysis is that xferqueue.cur is only modified on transfer completion, and the caller serializes simultaneous transfers. Is this safe, or should we restructure the locking (e.g., convert to mutex)? 3. DMA channel handling: dw_i3c_dma_request() is called during probe and silently degrades to PIO if DMA channels are unavailable. Is this the right behavior, or should probe fail on DMA errors other than -ENODEV/-EPROBE_DEFER? 4. Error handling for DMA timeouts: when a DMA timeout occurs, dmaengine_terminate_all() is called but the transfer proceeds as if it were successful, potentially reading stale data. Should we instead signal an error to the I3C core? 5. IBI path: dw_i3c_master_read_ibi_fifo() still uses PIO. Should IBI reads also use DMA, or is the IBI payload typically small enough that PIO is preferred? Any other feedback on the design or implementation is welcome. drivers/i3c/master/dw-i3c-master.c | 260 ++++++++++++++++++++++++++++- drivers/i3c/master/dw-i3c-master.h | 17 ++ 2 files changed, 270 insertions(+), 7 deletions(-) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index d6bdb32397fb..7b6c8681ff0c 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,10 @@ #include #include +#include +#include +#include + #include "../internals.h" #include "dw-i3c-master.h" @@ -32,6 +37,7 @@ #define DEV_CTRL_RESUME BIT(30) #define DEV_CTRL_HOT_JOIN_NACK BIT(8) #define DEV_CTRL_I2C_SLAVE_PRESENT BIT(7) +#define DEV_CTRL_DMA_EN BIT(28) #define DEVICE_ADDR 0x4 #define DEV_ADDR_DYNAMIC_ADDR_VALID BIT(31) @@ -359,16 +365,123 @@ static int dw_i3c_master_get_free_pos(struct dw_i3c_master *master) return ffs(master->free_pos) - 1; } +static void dw_i3c_dma_callback(void *arg) +{ + struct dw_i3c_dma *dma = arg; + + dma_unmap_single(dma->chan_using->device->dev, dma->dma_buf, + dma->dma_len, dma->dma_data_dir); + complete(&dma->complete); +} + +static int dw_i3c_master_dma_xfer(struct dw_i3c_dma *dma, const u8 *bytes) +{ + struct device *chan_dev = dma->chan_using->device->dev; + struct dma_async_tx_descriptor *txdesc; + + dma->dma_buf = dma_map_single(chan_dev, (void *)bytes, dma->dma_len, + dma->dma_data_dir); + if (dma_mapping_error(chan_dev, dma->dma_buf)) + return -EINVAL; + + txdesc = dmaengine_prep_slave_single(dma->chan_using, dma->dma_buf, + dma->dma_len, dma->dma_transfer_dir, + DMA_PREP_INTERRUPT); + if (!txdesc) { + dma_unmap_single(chan_dev, dma->dma_buf, dma->dma_len, + dma->dma_data_dir); + return -ENOMEM; + } + + reinit_completion(&dma->complete); + txdesc->callback = dw_i3c_dma_callback; + txdesc->callback_param = dma; + if (dma_submit_error(dmaengine_submit(txdesc))) { + dma_unmap_single(chan_dev, dma->dma_buf, dma->dma_len, + dma->dma_data_dir); + return -EIO; + } + + dma_async_issue_pending(dma->chan_using); + return 0; +} + static void dw_i3c_master_wr_tx_fifo(struct dw_i3c_master *master, const u8 *bytes, int nbytes) { - i3c_writel_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); + struct dw_i3c_dma *dma = master->dma; + unsigned long time_left; + int ret; + + if (!master->use_dma || nbytes < 4) { + i3c_writel_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); + return; + } + + dma->chan_using = dma->chan_tx; + dma->dma_transfer_dir = DMA_MEM_TO_DEV; + dma->dma_data_dir = DMA_TO_DEVICE; + dma->dma_len = ALIGN_DOWN(nbytes, 4); + + ret = dw_i3c_master_dma_xfer(dma, bytes); + if (ret) { + dev_err(&master->base.dev, "DMA TX setup failed (%d)\n", ret); + i3c_writel_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); + return; + } + + time_left = wait_for_completion_timeout(&dma->complete, + XFER_TIMEOUT); + if (!time_left) { + dmaengine_terminate_all(dma->chan_using); + dev_err(&master->base.dev, "DMA TX timeout\n"); + } + + if (nbytes & 3) { + u32 tmp = 0; + + memcpy(&tmp, bytes + (nbytes & ~3), nbytes & 3); + writesl(master->regs + RX_TX_DATA_PORT, &tmp, 1); + } } static void dw_i3c_master_read_rx_fifo(struct dw_i3c_master *master, u8 *bytes, int nbytes) { - i3c_readl_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); + struct dw_i3c_dma *dma = master->dma; + unsigned long time_left; + int ret; + + if (!master->use_dma || nbytes < 4) { + i3c_readl_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); + return; + } + + dma->chan_using = dma->chan_rx; + dma->dma_transfer_dir = DMA_DEV_TO_MEM; + dma->dma_data_dir = DMA_FROM_DEVICE; + dma->dma_len = ALIGN_DOWN(nbytes, 4); + + ret = dw_i3c_master_dma_xfer(dma, bytes); + if (ret) { + dev_err(&master->base.dev, "DMA RX setup failed (%d)\n", ret); + i3c_readl_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); + return; + } + + time_left = wait_for_completion_timeout(&dma->complete, + XFER_TIMEOUT); + if (!time_left) { + dmaengine_terminate_all(dma->chan_using); + dev_err(&master->base.dev, "DMA RX timeout\n"); + } + + if (nbytes & 3) { + u32 tmp; + + readsl(master->regs + RX_TX_DATA_PORT, &tmp, 1); + memcpy(bytes + (nbytes & ~3), &tmp, nbytes & 3); + } } static void dw_i3c_master_read_ibi_fifo(struct dw_i3c_master *master, @@ -403,14 +516,29 @@ static void dw_i3c_master_start_xfer_locked(struct dw_i3c_master *master) struct dw_i3c_xfer *xfer = master->xferqueue.cur; unsigned int i; u32 thld_ctrl; + u32 dev_ctrl; if (!xfer) return; - for (i = 0; i < xfer->ncmds; i++) { - struct dw_i3c_cmd *cmd = &xfer->cmds[i]; - - dw_i3c_master_wr_tx_fifo(master, cmd->tx_buf, cmd->tx_len); + /* + * DMA transfers may sleep, so we must drop the spinlock while + * writing the TX FIFO. The xferqueue lock is held by our caller; + * xferqueue.cur is safe because: + * - New xfers are only added to the list (not cur) when cur is set. + * - Timeout-based dequeue is blocked until the caller calls + * wait_for_completion_timeout(), which happens after we return. + */ + if (master->use_dma) { + spin_unlock(&master->xferqueue.lock); + for (i = 0; i < xfer->ncmds; i++) + dw_i3c_master_wr_tx_fifo(master, xfer->cmds[i].tx_buf, + xfer->cmds[i].tx_len); + spin_lock(&master->xferqueue.lock); + } else { + for (i = 0; i < xfer->ncmds; i++) + dw_i3c_master_wr_tx_fifo(master, xfer->cmds[i].tx_buf, + xfer->cmds[i].tx_len); } thld_ctrl = readl(master->regs + QUEUE_THLD_CTRL); @@ -418,6 +546,13 @@ static void dw_i3c_master_start_xfer_locked(struct dw_i3c_master *master) thld_ctrl |= QUEUE_THLD_CTRL_RESP_BUF(xfer->ncmds); writel(thld_ctrl, master->regs + QUEUE_THLD_CTRL); + dev_ctrl = readl(master->regs + DEVICE_CTRL); + if (master->use_dma) + dev_ctrl |= DEV_CTRL_DMA_EN; + else + dev_ctrl &= ~DEV_CTRL_DMA_EN; + writel(dev_ctrl, master->regs + DEVICE_CTRL); + for (i = 0; i < xfer->ncmds; i++) { struct dw_i3c_cmd *cmd = &xfer->cmds[i]; @@ -486,9 +621,17 @@ static void dw_i3c_master_end_xfer_locked(struct dw_i3c_master *master, u32 isr) cmd = &xfer->cmds[RESPONSE_PORT_TID(resp)]; cmd->rx_len = RESPONSE_PORT_DATA_LEN(resp); cmd->error = RESPONSE_PORT_ERR_STATUS(resp); - if (cmd->rx_len && !cmd->error) + if (cmd->rx_len && !cmd->error) { + /* + * DMA RX may sleep; drop the lock temporarily. + * Safe because xferqueue.cur is not changed + * by other paths while a transfer is in flight. + */ + spin_unlock(&master->xferqueue.lock); dw_i3c_master_read_rx_fifo(master, cmd->rx_buf, cmd->rx_len); + spin_lock(&master->xferqueue.lock); + } } for (i = 0; i < nresp; i++) { @@ -1582,6 +1725,107 @@ static void dw_i3c_hj_work(struct work_struct *work) i3c_master_do_daa(&master->base); } +static void dw_i3c_dma_free(struct dw_i3c_master *master) +{ + struct dw_i3c_dma *dma = master->dma; + + if (!dma) + return; + + if (dma->chan_tx) { + dma_release_channel(dma->chan_tx); + dma->chan_tx = NULL; + } + if (dma->chan_rx) { + dma_release_channel(dma->chan_rx); + dma->chan_rx = NULL; + } + dma->chan_using = NULL; + master->dma = NULL; + master->use_dma = false; +} + +static void dw_i3c_dma_request(struct platform_device *pdev) +{ + struct dw_i3c_dma *dma; + struct dma_slave_config dma_sconfig; + struct dw_i3c_master *master; + struct resource *res; + dma_addr_t phy_addr; + int ret; + + master = platform_get_drvdata(pdev); + + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return; + phy_addr = res->start; + + dma = devm_kzalloc(&pdev->dev, sizeof(*dma), GFP_KERNEL); + if (!dma) + return; + + dma->chan_tx = dma_request_chan(&pdev->dev, "tx"); + if (IS_ERR(dma->chan_tx)) { + ret = PTR_ERR(dma->chan_tx); + if (ret != -ENODEV && ret != -EPROBE_DEFER) + dev_err(&pdev->dev, + "can't request DMA tx channel (%d)\n", ret); + goto fail; + } + + memset(&dma_sconfig, 0, sizeof(dma_sconfig)); + dma_sconfig.dst_addr = phy_addr + RX_TX_DATA_PORT; + dma_sconfig.dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + dma_sconfig.dst_maxburst = 1; + dma_sconfig.direction = DMA_MEM_TO_DEV; + ret = dmaengine_slave_config(dma->chan_tx, &dma_sconfig); + if (ret < 0) { + dev_err(&pdev->dev, + "can't configure DMA tx channel (%d)\n", ret); + goto fail_tx; + } + + dma->chan_rx = dma_request_chan(&pdev->dev, "rx"); + if (IS_ERR(dma->chan_rx)) { + ret = PTR_ERR(dma->chan_rx); + if (ret != -ENODEV && ret != -EPROBE_DEFER) + dev_err(&pdev->dev, + "can't request DMA rx channel (%d)\n", ret); + goto fail_tx; + } + + memset(&dma_sconfig, 0, sizeof(dma_sconfig)); + dma_sconfig.src_addr = phy_addr + RX_TX_DATA_PORT; + dma_sconfig.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES; + dma_sconfig.src_maxburst = 1; + dma_sconfig.direction = DMA_DEV_TO_MEM; + ret = dmaengine_slave_config(dma->chan_rx, &dma_sconfig); + if (ret < 0) { + dev_err(&pdev->dev, + "can't configure DMA rx channel (%d)\n", ret); + goto fail_rx; + } + + init_completion(&dma->complete); + master->dma = dma; + master->use_dma = true; + + dev_info(&pdev->dev, "using %s (tx) and %s (rx) for DMA transfers\n", + dma_chan_name(dma->chan_tx), dma_chan_name(dma->chan_rx)); + + return; + +fail_rx: + dma_release_channel(dma->chan_rx); + dma->chan_rx = NULL; +fail_tx: + dma_release_channel(dma->chan_tx); + dma->chan_tx = NULL; +fail: + devm_kfree(&pdev->dev, dma); +} + int dw_i3c_common_probe(struct dw_i3c_master *master, struct platform_device *pdev) { @@ -1627,6 +1871,7 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, goto err_assert_rst; platform_set_drvdata(pdev, master); + dw_i3c_dma_request(pdev); pm_runtime_set_autosuspend_delay(&pdev->dev, RPM_AUTOSUSPEND_TIMEOUT); pm_runtime_use_autosuspend(&pdev->dev); @@ -1683,6 +1928,7 @@ EXPORT_SYMBOL_GPL(dw_i3c_common_probe); void dw_i3c_common_remove(struct dw_i3c_master *master) { cancel_work_sync(&master->hj_work); + dw_i3c_dma_free(master); i3c_master_unregister(&master->base); /* Balance pm_runtime_get_noresume() from probe() */ diff --git a/drivers/i3c/master/dw-i3c-master.h b/drivers/i3c/master/dw-i3c-master.h index c5cb695c16ab..633b7abdfaae 100644 --- a/drivers/i3c/master/dw-i3c-master.h +++ b/drivers/i3c/master/dw-i3c-master.h @@ -6,12 +6,27 @@ */ #include +#include +#include +#include +#include #include #include #include #define DW_I3C_MAX_DEVS 32 +struct dw_i3c_dma { + struct dma_chan *chan_tx; + struct dma_chan *chan_rx; + struct dma_chan *chan_using; + dma_addr_t dma_buf; + unsigned int dma_len; + enum dma_transfer_direction dma_transfer_dir; + enum dma_data_direction dma_data_dir; + struct completion complete; +}; + struct dw_i3c_master_caps { u8 cmdfifodepth; u8 datafifodepth; @@ -70,6 +85,8 @@ struct dw_i3c_master { const struct dw_i3c_platform_ops *platform_ops; struct work_struct hj_work; + bool use_dma; + struct dw_i3c_dma *dma; }; struct dw_i3c_platform_ops { -- 2.17.1 From adrian.hunter at intel.com Fri May 15 09:26:05 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:05 +0300 Subject: [PATCH V4 01/17] i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-2-adrian.hunter@intel.com> Software reset was introduced as a fallback if bus disable failed. The change was made in 2 places: the cleanup path and the suspend path. For the cleanup path (i3c_hci_bus_cleanup()), after software reset the function continues to do cleanup for the current I/O mode. For the suspend path (i3c_hci_rpm_suspend()), after software reset the function returns early. However software reset does not reset any Ring Headers in the Host Controller, so returning early is not the right thing to do. Instead, continue to call suspend for the current I/O mode, which for DMA mode will reset any Ring Headers. Note, although Ring Headers should not be active at this stage, performing this reset follows the procedure defined by the specification and keeps the suspend path consistent with the cleanup path. Note also, i3c_hci_sync_irq_inactive() is still called via the PIO and DMA hci->io->suspend() callbacks. Always return 0 because the device is quiesced as much as possible and returning a negative error code would unnecessarily prevent system suspend. Fixes: 9a258d1336f7 ("i3c: mipi-i3c-hci: Fallback to software reset when bus disable fails") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: None Changes in V3: Add Frank's rev'd-by Changes in V2: Always return 0 from suspend callback Amend commit message drivers/i3c/master/mipi-i3c-hci/core.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index b781dbed2165..afb0764b5e1f 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -762,15 +762,10 @@ static int i3c_hci_reset_and_init(struct i3c_hci *hci) int i3c_hci_rpm_suspend(struct device *dev) { struct i3c_hci *hci = dev_get_drvdata(dev); - int ret; - ret = i3c_hci_bus_disable(hci); - if (ret) { - /* Fall back to software reset to disable the bus */ - ret = i3c_hci_software_reset(hci); - i3c_hci_sync_irq_inactive(hci); - return ret; - } + /* Fall back to software reset to disable the bus */ + if (i3c_hci_bus_disable(hci)) + i3c_hci_software_reset(hci); hci->io->suspend(hci); -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:06 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:06 +0300 Subject: [PATCH V4 02/17] i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-3-adrian.hunter@intel.com> The MIPI I3C HCI specification does not require the DMA ring RUN bit (RUN_STOP) to be cleared when issuing an ABORT. That allows the DMA ring to continue to receive IBIs, although an IBI is anyway not lost because it can be received once the ring restarts if the I3C device has not given up. Note, currently ABORT is only used on a timeout error path so the change has very little effect in practice. In the more common case of a transfer error, the ring (bundle) operation is halted by the controller anyway. Adjust the RING_CONTROL handling to set ABORT without clearing RUN_STOP, bringing the driver into alignment with the specification. Fixes: b795e68bf3073 ("i3c: mipi-i3c-hci: Correct RING_CTRL_ABORT handling in DMA dequeue") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: None Changes in V3: Add Frank's rev'd-by Changes in V2: Improve commit message 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 e4daaa612055..bdffdd8b8923 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -554,7 +554,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, if (ring_status & RING_STATUS_RUNNING) { /* stop the ring */ reinit_completion(&rh->op_done); - rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_ABORT); + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); wait_for_completion_timeout(&rh->op_done, HZ); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:04 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:04 +0300 Subject: [PATCH V4 00/17] i3c: mipi-i3c-hci: DMA abort, recovery and related improvements Message-ID: <20260515162621.57719-1-adrian.hunter@intel.com> Hi This series improves the robustness of the MIPI I3C HCI DMA mode driver, addressing issues observed during error handling and recovery. Patch 1 ensures suspend always invokes io->suspend. Patches 2-4 fix issues in the existing DMA abort path: preserving the RUN bit during abort per the MIPI specification, blocking enqueue during abort/error, and waiting for ring restart completion. Patches 5-8 improve how partially completed transfer lists are handled during dequeue: moving hci_dma_xfer_done() earlier so completed responses are processed before NoOp replacement, completing transfer lists immediately on error rather than deferring, and detecting when an abort races with transfer completion to avoid restarting the wrong transfer list. Patches 9-11 add Intel-specific quirks for DMA ring abort: a PIO queue reset after abort, and an HC_CONTROL ABORT before the ring-level abort. Patch 12 factors out a reset-and-restore helper from the suspend path for reuse. Patch 13 adds a full DMA recovery path for internal controller errors. When the hardware reports a TID mismatch or the ring becomes stuck, the driver now resets and restores the controller, terminating all in-flight transfers with an error status. Patch 14 makes NoOp command handling observable: instead of discarding NoOp responses, the driver now waits for them to complete and triggers recovery if they fail. Patch 15 adjusts transfer timeout accounting to start from when a transfer actually begins execution rather than when it was queued, preventing premature timeouts behind slow predecessors. Patches 16-17 are minor optimizations: consolidating the DMA command and response ring into a single coherent allocation, and increasing the ring size to the maximum 255 entries to avoid ring-space exhaustion. Changes in V4: i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers Inline HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET check at call site instead of using a helper function i3c: mipi-i3c-hci: Factor out hci_dma_abort() New patch i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers Factor out hci_dma_abort() into a preceding patch Make hci_dma_requires_hc_abort_quirk() void; move quirk check to caller i3c: mipi-i3c-hci: Base timeouts on actual transfer start time Rename start_time to start_jiffies i3c: mipi-i3c-hci: Consolidate DMA ring allocation Cache allocation size in xfer_alloc_sz to avoid recomputing in hci_dma_free() Changes in V3: i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring Add Frank's rev'd-by i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors When erroring out transfers, ensure the final transfer of a transfer list is processed last Changes in V2: i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset Always return 0 from suspend callback Amend commit message i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring Improve commit message i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error Improve commit message i3c: mipi-i3c-hci: Wait for DMA ring restart to complete None i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition Add Frank's Rev'd-by i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path Add Frank's Rev'd-by i3c: mipi-i3c-hci: Complete transfer lists immediately on error Rename completing_xfer to final_xfer i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer Rename completing_xfer to final_xfer i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers None i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers None i3c: mipi-i3c-hci: Factor out reset-and-restore helper Drop redundant i3c_hci_sync_irq_inactive(hci) from i3c_hci_reset_and_restore() because it is called by hci->io->suspend() anyway i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors Rename completing_xfer to final_xfer Add hci_dma_xfer_done() before checking for an already complete transfer Improve commit message i3c: mipi-i3c-hci: Wait for NoOp commands to complete Rename completing_xfer to final_xfer Add missing reinit_completion() i3c: mipi-i3c-hci: Base timeouts on actual transfer start time Do not flag the next transfer as started when there is an error which halts the controller Instead flag it started at the end of hci_dma_dequeue_xfer() Use hci_start_xfer() in pio.c i3c: mipi-i3c-hci: Consolidate DMA ring allocation Check for failed allocation before assignments to avoid doing arithmetic with NULL pointers i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum None Adrian Hunter (17): i3c: mipi-i3c-hci: Fix suspend behavior when bus disable falls back to software reset i3c: mipi-i3c-hci: Preserve RUN bit when aborting DMA ring i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error i3c: mipi-i3c-hci: Wait for DMA ring restart to complete i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path i3c: mipi-i3c-hci: Complete transfer lists immediately on error i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers i3c: mipi-i3c-hci: Factor out hci_dma_abort() i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers i3c: mipi-i3c-hci: Factor out reset-and-restore helper i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors i3c: mipi-i3c-hci: Wait for NoOp commands to complete i3c: mipi-i3c-hci: Base timeouts on actual transfer start time i3c: mipi-i3c-hci: Consolidate DMA ring allocation i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum drivers/i3c/master/mipi-i3c-hci/cmd.h | 6 + drivers/i3c/master/mipi-i3c-hci/core.c | 82 ++++++-- drivers/i3c/master/mipi-i3c-hci/dma.c | 333 +++++++++++++++++++++++++-------- drivers/i3c/master/mipi-i3c-hci/hci.h | 22 +++ drivers/i3c/master/mipi-i3c-hci/pio.c | 1 + 5 files changed, 354 insertions(+), 90 deletions(-) Regards Adrian From adrian.hunter at intel.com Fri May 15 09:26:07 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:07 +0300 Subject: [PATCH V4 03/17] i3c: mipi-i3c-hci: Prevent DMA enqueue while ring is aborting or in error In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-4-adrian.hunter@intel.com> Block the DMA enqueue path while a Ring abort is in progress or after an error condition has been detected. Previously, new transfers could be enqueued while the DMA Ring was being aborted or while error handling was underway. This allowed enqueue and error-recovery paths to run concurrently, potentially interfering with each other and corrupting Ring state. Introduce explicit enqueue blocking and a wait queue to serialize access: enqueue operations now wait until abort or error handling has completed before proceeding. Enqueue is unblocked once the Ring is safely restarted. Note, there is only 1 ring bundle configured, and a transfer error causes the controller to halt ring (bundle) operation, so there is only ever 1 outstanding error at a time. Furthermore, a later patch ensures that only the currently active transfer list can time out. Consequently, the DMA queue will not be unblocked while there are outstanding transfer errors or timeouts. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: Add Frank's Rev'd-by Changes in V3: None Changes in V2: Improve commit message drivers/i3c/master/mipi-i3c-hci/core.c | 1 + drivers/i3c/master/mipi-i3c-hci/dma.c | 25 +++++++++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index afb0764b5e1f..44617eb3a3f1 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -973,6 +973,7 @@ static int i3c_hci_probe(struct platform_device *pdev) spin_lock_init(&hci->lock); mutex_init(&hci->control_mutex); + init_waitqueue_head(&hci->enqueue_wait_queue); /* * Multi-bus instances share the same MMIO address range, but not diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index bdffdd8b8923..c3da6eab8eae 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -484,6 +484,12 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, spin_lock_irq(&hci->lock); + while (unlikely(hci->enqueue_blocked)) { + spin_unlock_irq(&hci->lock); + wait_event(hci->enqueue_wait_queue, !READ_ONCE(hci->enqueue_blocked)); + spin_lock_irq(&hci->lock); + } + if (n > rh->xfer_space) { spin_unlock_irq(&hci->lock); hci_dma_unmap_xfer(hci, xfer_list, n); @@ -539,6 +545,14 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, return 0; } +static void hci_dma_unblock_enqueue(struct i3c_hci *hci) +{ + if (hci->enqueue_blocked) { + hci->enqueue_blocked = false; + wake_up_all(&hci->enqueue_wait_queue); + } +} + static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n) { @@ -550,12 +564,17 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, guard(mutex)(&hci->control_mutex); + spin_lock_irq(&hci->lock); + ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { + hci->enqueue_blocked = true; + spin_unlock_irq(&hci->lock); /* stop the ring */ reinit_completion(&rh->op_done); rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); wait_for_completion_timeout(&rh->op_done, HZ); + spin_lock_irq(&hci->lock); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { /* @@ -567,8 +586,6 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } - spin_lock_irq(&hci->lock); - for (i = 0; i < n; i++) { struct hci_xfer *xfer = xfer_list + i; int idx = xfer->ring_entry; @@ -604,6 +621,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); + hci_dma_unblock_enqueue(hci); + spin_unlock_irq(&hci->lock); return did_unqueue; @@ -647,6 +666,8 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) } if (xfer->completion) complete(xfer->completion); + if (RESP_STATUS(resp)) + hci->enqueue_blocked = true; } done_ptr = (done_ptr + 1) % rh->xfer_entries; diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index f17f43494c1b..d630400ec945 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -54,6 +54,8 @@ struct i3c_hci { struct mutex control_mutex; atomic_t next_cmd_tid; bool irq_inactive; + bool enqueue_blocked; + wait_queue_head_t enqueue_wait_queue; u32 caps; unsigned int quirks; unsigned int DAT_entries; -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:08 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:08 +0300 Subject: [PATCH V4 04/17] i3c: mipi-i3c-hci: Wait for DMA ring restart to complete In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-5-adrian.hunter@intel.com> Although hci_dma_dequeue_xfer() is serialized against itself via control_mutex, this does not guarantee that a DMA ring restart triggered by a previous invocation has fully completed. When the function is called again in rapid succession, the DMA ring may still be transitioning back to the running state, which may confound or disrupt further state changes. Address this by waiting for the DMA ring restart to complete before continuing. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: Add Frank's Rev'd-by Changes in V2 and V3: None drivers/i3c/master/mipi-i3c-hci/dma.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index c3da6eab8eae..3b14bc87bdf6 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -617,6 +617,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } /* restart the ring */ + reinit_completion(&rh->op_done); mipi_i3c_hci_resume(hci); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); @@ -625,6 +626,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, spin_unlock_irq(&hci->lock); + wait_for_completion_timeout(&rh->op_done, HZ); + return did_unqueue; } -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:09 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:09 +0300 Subject: [PATCH V4 05/17] i3c: mipi-i3c-hci: Move hci_dma_xfer_done() definition In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-6-adrian.hunter@intel.com> Move hci_dma_xfer_done() earlier in the file to avoid a forward declaration needed by a subsequent change. No functional change. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V3 and V4: None Changes in V2: Added Frank's Rev'd-by drivers/i3c/master/mipi-i3c-hci/dma.c | 98 +++++++++++++-------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 3b14bc87bdf6..ad47bb2890d6 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -545,6 +545,55 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, return 0; } +static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + u32 op1_val, op2_val, resp, *ring_resp; + unsigned int tid, done_ptr = rh->done_ptr; + unsigned int done_cnt = 0; + struct hci_xfer *xfer; + + for (;;) { + op2_val = rh_reg_read(RING_OPERATION2); + if (done_ptr == FIELD_GET(RING_OP2_CR_DEQ_PTR, op2_val)) + break; + + ring_resp = rh->resp + rh->resp_struct_sz * done_ptr; + resp = *ring_resp; + tid = RESP_TID(resp); + dev_dbg(&hci->master.dev, "resp = 0x%08x", resp); + + xfer = rh->src_xfers[done_ptr]; + if (!xfer) { + dev_dbg(&hci->master.dev, "orphaned ring entry"); + } else { + hci_dma_unmap_xfer(hci, xfer, 1); + rh->src_xfers[done_ptr] = NULL; + xfer->ring_entry = -1; + xfer->response = resp; + if (tid != xfer->cmd_tid) { + dev_err(&hci->master.dev, + "response tid=%d when expecting %d\n", + tid, xfer->cmd_tid); + /* TODO: do something about it? */ + } + if (xfer->completion) + complete(xfer->completion); + if (RESP_STATUS(resp)) + hci->enqueue_blocked = true; + } + + done_ptr = (done_ptr + 1) % rh->xfer_entries; + rh->done_ptr = done_ptr; + done_cnt += 1; + } + + rh->xfer_space += done_cnt; + op1_val = rh_reg_read(RING_OPERATION1); + op1_val &= ~RING_OP1_CR_SW_DEQ_PTR; + op1_val |= FIELD_PREP(RING_OP1_CR_SW_DEQ_PTR, done_ptr); + rh_reg_write(RING_OPERATION1, op1_val); +} + static void hci_dma_unblock_enqueue(struct i3c_hci *hci) { if (hci->enqueue_blocked) { @@ -636,55 +685,6 @@ static int hci_dma_handle_error(struct i3c_hci *hci, struct hci_xfer *xfer_list, return hci_dma_dequeue_xfer(hci, xfer_list, n) ? -EIO : 0; } -static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) -{ - u32 op1_val, op2_val, resp, *ring_resp; - unsigned int tid, done_ptr = rh->done_ptr; - unsigned int done_cnt = 0; - struct hci_xfer *xfer; - - for (;;) { - op2_val = rh_reg_read(RING_OPERATION2); - if (done_ptr == FIELD_GET(RING_OP2_CR_DEQ_PTR, op2_val)) - break; - - ring_resp = rh->resp + rh->resp_struct_sz * done_ptr; - resp = *ring_resp; - tid = RESP_TID(resp); - dev_dbg(&hci->master.dev, "resp = 0x%08x", resp); - - xfer = rh->src_xfers[done_ptr]; - if (!xfer) { - dev_dbg(&hci->master.dev, "orphaned ring entry"); - } else { - hci_dma_unmap_xfer(hci, xfer, 1); - rh->src_xfers[done_ptr] = NULL; - xfer->ring_entry = -1; - xfer->response = resp; - if (tid != xfer->cmd_tid) { - dev_err(&hci->master.dev, - "response tid=%d when expecting %d\n", - tid, xfer->cmd_tid); - /* TODO: do something about it? */ - } - if (xfer->completion) - complete(xfer->completion); - if (RESP_STATUS(resp)) - hci->enqueue_blocked = true; - } - - done_ptr = (done_ptr + 1) % rh->xfer_entries; - rh->done_ptr = done_ptr; - done_cnt += 1; - } - - rh->xfer_space += done_cnt; - op1_val = rh_reg_read(RING_OPERATION1); - op1_val &= ~RING_OP1_CR_SW_DEQ_PTR; - op1_val |= FIELD_PREP(RING_OP1_CR_SW_DEQ_PTR, done_ptr); - rh_reg_write(RING_OPERATION1, op1_val); -} - static int hci_dma_request_ibi(struct i3c_hci *hci, struct i3c_dev_desc *dev, const struct i3c_ibi_setup *req) { -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:10 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:10 +0300 Subject: [PATCH V4 06/17] i3c: mipi-i3c-hci: Call hci_dma_xfer_done() from dequeue path In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-7-adrian.hunter@intel.com> hci_dma_dequeue_xfer() relies on state normally updated by the DMA interrupt handler. Ensure that state is current by explicitly invoking hci_dma_xfer_done() from the dequeue path. This handles cases where the interrupt handler has not (yet) run. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V3 and V4: None Changes in V2: Added Frank's Rev'd-by drivers/i3c/master/mipi-i3c-hci/dma.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index ad47bb2890d6..de0f17706ac8 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -635,6 +635,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } + hci_dma_xfer_done(hci, rh); + for (i = 0; i < n; i++) { struct hci_xfer *xfer = xfer_list + i; int idx = xfer->ring_entry; -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:11 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:11 +0300 Subject: [PATCH V4 07/17] i3c: mipi-i3c-hci: Complete transfer lists immediately on error In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-8-adrian.hunter@intel.com> In DMA mode, transfer lists are currently completed only when the final transfer in the list completes. If an earlier transfer fails, the list is left incomplete and callers wait until timeout. There is no need to wait for a timeout, as the completion path in i3c_hci_process_xfer() already checks for error status. Complete the transfer list as soon as any transfer in the list reports an error. This avoids unnecessary delays and spurious timeouts on error. Complete a transfer list completion immediately there is an error. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: Add Frank's Rev'd-by Changes in V3: None Changes in V2: Renamed completing_xfer to final_xfer drivers/i3c/master/mipi-i3c-hci/dma.c | 6 ++++-- drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index de0f17706ac8..83b553e1ab0b 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -502,6 +502,8 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer = xfer_list + i; u32 *ring_data = rh->xfer + rh->xfer_struct_sz * enqueue_ptr; + xfer->final_xfer = xfer_list + n - 1; + /* store cmd descriptor */ *ring_data++ = xfer->cmd_desc[0]; *ring_data++ = xfer->cmd_desc[1]; @@ -576,8 +578,8 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) tid, xfer->cmd_tid); /* TODO: do something about it? */ } - if (xfer->completion) - complete(xfer->completion); + if (xfer == xfer->final_xfer || RESP_STATUS(resp)) + complete(xfer->final_xfer->completion); if (RESP_STATUS(resp)) hci->enqueue_blocked = true; } diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index d630400ec945..f07fc627d4d2 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -104,6 +104,7 @@ struct hci_xfer { struct { /* DMA specific */ struct i3c_dma *dma; + struct hci_xfer *final_xfer; int ring_number; int ring_entry; }; -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:12 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:12 +0300 Subject: [PATCH V4 08/17] i3c: mipi-i3c-hci: Avoid restarting DMA ring after aborting wrong transfer In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-9-adrian.hunter@intel.com> Software ABORT of the DMA ring is used to recover from transfer list timeouts, but it is inherently racy. The intended transfer list may complete just before the ABORT takes effect, causing the subsequent transfer list to be aborted instead. In this case, an incomplete transfer list may remain in the ring and has not yet been processed by hci_dma_dequeue_xfer(). Restarting the DMA ring at that point can lead to unpredictable results. Detect when the next queued transfer is not the first entry of a transfer list and does not belong to the list currently being dequeued. In that case, skip restarting the DMA ring and defer recovery until a subsequent call to hci_dma_dequeue_xfer(), which will safely restart the ring once the incomplete list is handled. Signed-off-by: Adrian Hunter --- Changes in V3 and V4: None Changes in V2: Renamed completing_xfer to final_xfer drivers/i3c/master/mipi-i3c-hci/dma.c | 15 +++++++++++++++ drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + 2 files changed, 16 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 83b553e1ab0b..8e27fb6f18f5 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -503,6 +503,7 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, u32 *ring_data = rh->xfer + rh->xfer_struct_sz * enqueue_ptr; xfer->final_xfer = xfer_list + n - 1; + xfer->xfer_list_pos = i; /* store cmd descriptor */ *ring_data++ = xfer->cmd_desc[0]; @@ -669,6 +670,20 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } + /* + * A software ABORT may race with transfer completion and abort the next + * transfer list instead. Detect that case, and do not restart the ring. + * It will be handled by a subsequent dequeue. + */ + if (!did_unqueue) { + struct hci_xfer *xfer = rh->src_xfers[rh->done_ptr]; + + if (xfer && xfer->xfer_list_pos && xfer->final_xfer != xfer_list->final_xfer) { + spin_unlock_irq(&hci->lock); + return false; + } + } + /* restart the ring */ reinit_completion(&rh->op_done); mipi_i3c_hci_resume(hci); diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index f07fc627d4d2..83d4f13a68a3 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -107,6 +107,7 @@ struct hci_xfer { struct hci_xfer *final_xfer; int ring_number; int ring_entry; + int xfer_list_pos; }; }; }; -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:13 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:13 +0300 Subject: [PATCH V4 09/17] i3c: mipi-i3c-hci: Add DMA ring abort/reset quirk for Intel controllers In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-10-adrian.hunter@intel.com> Some Intel I3C HCI controllers cannot reliably restart a DMA ring after an ABORT. Additional queue resets are required to recover, and must be performed using PIO reset bits even while operating in DMA mode. This behavior is non-standard. Introduce a controller quirk to opt into the required PIO queue resets after a DMA ring abort, and enable it for Intel LPSS I3C controllers. Signed-off-by: Adrian Hunter --- Changes in V4: Inline HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET check at call site instead of using a helper function Changes in V2 and V3: None drivers/i3c/master/mipi-i3c-hci/core.c | 15 ++++++++++++++- drivers/i3c/master/mipi-i3c-hci/dma.c | 4 ++++ drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 44617eb3a3f1..770235ad6b25 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -240,6 +240,18 @@ void mipi_i3c_hci_pio_reset(struct i3c_hci *hci) reg_write(RESET_CONTROL, RX_FIFO_RST | TX_FIFO_RST | RESP_QUEUE_RST); } +#define ALL_QUEUES_RST (CMD_QUEUE_RST | RESP_QUEUE_RST | RX_FIFO_RST | TX_FIFO_RST | IBI_QUEUE_RST) + +void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci) +{ + u32 regval; + + reg_write(RESET_CONTROL, ALL_QUEUES_RST); + if (readx_poll_timeout_atomic(reg_read, RESET_CONTROL, regval, + !(regval & ALL_QUEUES_RST), 0, 20)) + dev_err(&hci->master.dev, "%s: Reset queues failed\n", __func__); +} + /* located here rather than dct.c because needed bits are in core reg space */ void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci) { @@ -1040,7 +1052,8 @@ MODULE_DEVICE_TABLE(acpi, i3c_hci_acpi_match); static const struct platform_device_id i3c_hci_driver_ids[] = { { .name = "intel-lpss-i3c", HCI_QUIRK_RPM_ALLOWED | HCI_QUIRK_RPM_IBI_ALLOWED | - HCI_QUIRK_RPM_PARENT_MANAGED }, + HCI_QUIRK_RPM_PARENT_MANAGED | + HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(platform, i3c_hci_driver_ids); diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 8e27fb6f18f5..906de7dbfdb5 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -638,6 +638,10 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, } } + if ((hci->quirks & HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET) && + (rh_reg_read(RING_STATUS) & RING_STATUS_ABORTED)) + mipi_i3c_hci_pio_reset_all_queues(hci); + hci_dma_xfer_done(hci, rh); for (i = 0; i < n; i++) { diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 83d4f13a68a3..01237b12d32e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -156,10 +156,12 @@ struct i3c_hci_dev_data { #define HCI_QUIRK_RPM_ALLOWED BIT(5) /* Runtime PM allowed */ #define HCI_QUIRK_RPM_IBI_ALLOWED BIT(6) /* IBI and Hot-Join allowed while runtime suspended */ #define HCI_QUIRK_RPM_PARENT_MANAGED BIT(7) /* Runtime PM managed by parent device */ +#define HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET BIT(8) /* Do PIO queue SW resets after DMA abort */ /* global functions */ void mipi_i3c_hci_resume(struct i3c_hci *hci); void mipi_i3c_hci_pio_reset(struct i3c_hci *hci); +void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci); void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci); void amd_set_od_pp_timing(struct i3c_hci *hci); void amd_set_resp_buf_thld(struct i3c_hci *hci); -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:14 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:14 +0300 Subject: [PATCH V4 10/17] i3c: mipi-i3c-hci: Factor out hci_dma_abort() In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-11-adrian.hunter@intel.com> Factor out hci_dma_abort() from hci_dma_dequeue_xfer() in preparation for further changes. Signed-off-by: Adrian Hunter --- Changes in V4: New patch drivers/i3c/master/mipi-i3c-hci/dma.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 906de7dbfdb5..f2d33068b8df 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -597,6 +597,13 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) rh_reg_write(RING_OPERATION1, op1_val); } +static void hci_dma_abort(struct hci_rh_data *rh) +{ + reinit_completion(&rh->op_done); + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); + wait_for_completion_timeout(&rh->op_done, HZ); +} + static void hci_dma_unblock_enqueue(struct i3c_hci *hci) { if (hci->enqueue_blocked) { @@ -623,9 +630,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, hci->enqueue_blocked = true; spin_unlock_irq(&hci->lock); /* stop the ring */ - reinit_completion(&rh->op_done); - rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); - wait_for_completion_timeout(&rh->op_done, HZ); + hci_dma_abort(rh); spin_lock_irq(&hci->lock); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:15 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:15 +0300 Subject: [PATCH V4 11/17] i3c: mipi-i3c-hci: Add DMA ring abort quirk for Intel controllers In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-12-adrian.hunter@intel.com> DMA rings can be aborted either per-ring via RING_CONTROL or globally via HC_CONTROL_ABORT. The driver currently relies on the per-ring mechanism. Some Intel I3C HCI controllers require HC_CONTROL_ABORT to be asserted before a DMA ring abort is effective. This behavior is non-standard. Introduce a controller quirk to select the required abort method and enable it for Intel LPSS I3C controllers. Signed-off-by: Adrian Hunter --- Changes in V4: Factor out hci_dma_abort() into a preceding patch Make hci_dma_requires_hc_abort_quirk() return void and move quirk check to caller Changes in V2 and V3: None drivers/i3c/master/mipi-i3c-hci/core.c | 18 ++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/dma.c | 17 +++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 770235ad6b25..8274c84b16be 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -231,7 +231,20 @@ static void i3c_hci_bus_cleanup(struct i3c_master_controller *m) void mipi_i3c_hci_resume(struct i3c_hci *hci) { - reg_set(HC_CONTROL, HC_CONTROL_RESUME); + u32 reg = reg_read(HC_CONTROL); + + reg |= HC_CONTROL_RESUME; + reg &= ~HC_CONTROL_ABORT; + reg_write(HC_CONTROL, reg); +} + +void mipi_i3c_hci_abort(struct i3c_hci *hci) +{ + u32 reg = reg_read(HC_CONTROL); + + reg &= ~HC_CONTROL_RESUME; /* Do not set resume */ + reg |= HC_CONTROL_ABORT; + reg_write(HC_CONTROL, reg); } /* located here rather than pio.c because needed bits are in core reg space */ @@ -1053,7 +1066,8 @@ static const struct platform_device_id i3c_hci_driver_ids[] = { { .name = "intel-lpss-i3c", HCI_QUIRK_RPM_ALLOWED | HCI_QUIRK_RPM_IBI_ALLOWED | HCI_QUIRK_RPM_PARENT_MANAGED | - HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET }, + HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET | + HCI_QUIRK_DMA_REQUIRES_HC_ABORT }, { /* sentinel */ } }; MODULE_DEVICE_TABLE(platform, i3c_hci_driver_ids); diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index f2d33068b8df..f9023cb3c5a2 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -597,8 +597,21 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) rh_reg_write(RING_OPERATION1, op1_val); } -static void hci_dma_abort(struct hci_rh_data *rh) +static void hci_dma_requires_hc_abort_quirk(struct i3c_hci *hci, struct hci_rh_data *rh) { + reinit_completion(&rh->op_done); + mipi_i3c_hci_abort(hci); + wait_for_completion_timeout(&rh->op_done, HZ); + rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); +} + +static void hci_dma_abort(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + if (hci->quirks & HCI_QUIRK_DMA_REQUIRES_HC_ABORT) { + hci_dma_requires_hc_abort_quirk(hci, rh); + return; + } + reinit_completion(&rh->op_done); rh_reg_write(RING_CONTROL, rh_reg_read(RING_CONTROL) | RING_CTRL_ABORT); wait_for_completion_timeout(&rh->op_done, HZ); @@ -630,7 +643,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, hci->enqueue_blocked = true; spin_unlock_irq(&hci->lock); /* stop the ring */ - hci_dma_abort(rh); + hci_dma_abort(hci, rh); spin_lock_irq(&hci->lock); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 01237b12d32e..97c31a315a6e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -157,9 +157,11 @@ struct i3c_hci_dev_data { #define HCI_QUIRK_RPM_IBI_ALLOWED BIT(6) /* IBI and Hot-Join allowed while runtime suspended */ #define HCI_QUIRK_RPM_PARENT_MANAGED BIT(7) /* Runtime PM managed by parent device */ #define HCI_QUIRK_DMA_ABORT_REQUIRES_PIO_RESET BIT(8) /* Do PIO queue SW resets after DMA abort */ +#define HCI_QUIRK_DMA_REQUIRES_HC_ABORT BIT(9) /* Use HC_CONTROL ABORT to abort DMA */ /* global functions */ void mipi_i3c_hci_resume(struct i3c_hci *hci); +void mipi_i3c_hci_abort(struct i3c_hci *hci); void mipi_i3c_hci_pio_reset(struct i3c_hci *hci); void mipi_i3c_hci_pio_reset_all_queues(struct i3c_hci *hci); void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci); -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:16 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:16 +0300 Subject: [PATCH V4 12/17] i3c: mipi-i3c-hci: Factor out reset-and-restore helper In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-13-adrian.hunter@intel.com> Factor the reset-and-restore sequence out of i3c_hci_rpm_resume() into a separate helper. This allows the same logic to be reused for recovery paths in subsequent changes without duplicating suspend/resume handling. No functional change. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: Add Frank's Rev'd by Changes in V3: None Changes in V2: Drop redundant i3c_hci_sync_irq_inactive(hci) from i3c_hci_reset_and_restore() because it is called by hci->io->suspend() anyway drivers/i3c/master/mipi-i3c-hci/core.c | 19 +++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/hci.h | 2 ++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 8274c84b16be..12a0122fb709 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -798,9 +798,8 @@ int i3c_hci_rpm_suspend(struct device *dev) } EXPORT_SYMBOL_GPL(i3c_hci_rpm_suspend); -int i3c_hci_rpm_resume(struct device *dev) +static int i3c_hci_do_reset_and_restore(struct i3c_hci *hci) { - struct i3c_hci *hci = dev_get_drvdata(dev); int ret; ret = i3c_hci_reset_and_init(hci); @@ -821,6 +820,22 @@ int i3c_hci_rpm_resume(struct device *dev) return 0; } + +int i3c_hci_reset_and_restore(struct i3c_hci *hci) +{ + i3c_hci_bus_disable(hci); + + hci->io->suspend(hci); + + return i3c_hci_do_reset_and_restore(hci); +} + +int i3c_hci_rpm_resume(struct device *dev) +{ + struct i3c_hci *hci = dev_get_drvdata(dev); + + return i3c_hci_do_reset_and_restore(hci); +} EXPORT_SYMBOL_GPL(i3c_hci_rpm_resume); static int i3c_hci_runtime_suspend(struct device *dev) diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 97c31a315a6e..a3151c26827e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -175,4 +175,6 @@ int i3c_hci_process_xfer(struct i3c_hci *hci, struct hci_xfer *xfer, int n); int i3c_hci_rpm_suspend(struct device *dev); int i3c_hci_rpm_resume(struct device *dev); +int i3c_hci_reset_and_restore(struct i3c_hci *hci); + #endif -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:17 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:17 +0300 Subject: [PATCH V4 13/17] i3c: mipi-i3c-hci: Add DMA-mode recovery for internal controller errors In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-14-adrian.hunter@intel.com> Handle internal I3C HCI errors when operating in DMA mode by adding a simple recovery mechanism. On detection of an internal controller error, mark recovery as needed and attempt to restore operation by performing a software reset followed by state restore. To keep recovery straightforward on this unlikely error path, all currently queued transfers are terminated and completed with an error. This allows the controller to resume operation after internal failures rather than remaining permanently stuck. Note, internal errors indicated by INTR_HC_INTERNAL_ERR, cause the controller to stop. Signed-off-by: Adrian Hunter --- Changes in V4: None Changes in V3: When erroring out transfers, ensure the final transfer of a transfer list is processed last Changes in V2: Rename completing_xfer to final_xfer Add hci_dma_xfer_done() before checking for an already complete transfer Improve commit message drivers/i3c/master/mipi-i3c-hci/cmd.h | 6 ++ drivers/i3c/master/mipi-i3c-hci/core.c | 1 + drivers/i3c/master/mipi-i3c-hci/dma.c | 93 +++++++++++++++++++++++--- drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd.h b/drivers/i3c/master/mipi-i3c-hci/cmd.h index b1bf87daa651..7bada7b4b2de 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd.h +++ b/drivers/i3c/master/mipi-i3c-hci/cmd.h @@ -65,4 +65,10 @@ struct hci_cmd_ops { extern const struct hci_cmd_ops mipi_i3c_hci_cmd_v1; extern const struct hci_cmd_ops mipi_i3c_hci_cmd_v2; +static inline void hci_cmd_set_resp_err(u32 *response, int resp_err) +{ + *response &= ~RESP_ERR_FIELD; + *response |= FIELD_PREP(RESP_ERR_FIELD, resp_err); +} + #endif diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 12a0122fb709..69dcf5dad3a5 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -668,6 +668,7 @@ static irqreturn_t i3c_hci_irq_handler(int irq, void *dev_id) if (val & INTR_HC_INTERNAL_ERR) { dev_err(&hci->master.dev, "Host Controller Internal Error\n"); val &= ~INTR_HC_INTERNAL_ERR; + hci->recovery_needed = true; } if (val) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index f9023cb3c5a2..f39a6ce2aad5 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -9,6 +9,7 @@ */ #include +#include #include #include #include @@ -258,6 +259,10 @@ static void hci_dma_init_rh(struct i3c_hci *hci, struct hci_rh_data *rh, int i) rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE); rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_RUN_STOP); + /* + * Do not clear the entries of rh->src_xfers because the recovery uses + * them. In other cases they should be NULL anyway. + */ rh->done_ptr = 0; rh->ibi_chunk_ptr = 0; rh->xfer_space = rh->xfer_entries; @@ -362,7 +367,7 @@ static int hci_dma_init(struct i3c_hci *hci) rh->resp = dma_alloc_coherent(rings->sysdev, resps_sz, &rh->resp_dma, GFP_KERNEL); rh->src_xfers = - kmalloc_objs(*rh->src_xfers, rh->xfer_entries); + kzalloc_objs(*rh->src_xfers, rh->xfer_entries); ret = -ENOMEM; if (!rh->xfer || !rh->resp || !rh->src_xfers) goto err_out; @@ -572,13 +577,15 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) hci_dma_unmap_xfer(hci, xfer, 1); rh->src_xfers[done_ptr] = NULL; xfer->ring_entry = -1; - xfer->response = resp; if (tid != xfer->cmd_tid) { dev_err(&hci->master.dev, "response tid=%d when expecting %d\n", tid, xfer->cmd_tid); - /* TODO: do something about it? */ + hci->recovery_needed = true; + if (!RESP_STATUS(resp)) + hci_cmd_set_resp_err(&resp, RESP_ERR_HC_TERMINATED); } + xfer->response = resp; if (xfer == xfer->final_xfer || RESP_STATUS(resp)) complete(xfer->final_xfer->completion); if (RESP_STATUS(resp)) @@ -625,6 +632,60 @@ static void hci_dma_unblock_enqueue(struct i3c_hci *hci) } } +static void hci_dma_error_out_rh(struct i3c_hci *hci, struct hci_rh_data *rh) +{ + /* + * The entries of rh->src_xfers are not cleared by + * i3c_hci_reset_and_restore(), so can be used here. Do 2 passes so + * that the final_xfer of an xfer list is always processed last. + */ + for (int pass = 0; pass < 2; pass++) + for (int i = 0; i < rh->xfer_entries; i++) { + struct hci_xfer *xfer = rh->src_xfers[i]; + + if (!xfer || (!pass && xfer == xfer->final_xfer)) + continue; + hci_dma_unmap_xfer(hci, xfer, 1); + rh->src_xfers[i] = NULL; + xfer->ring_entry = -1; + hci_cmd_set_resp_err(&xfer->response, RESP_ERR_HC_TERMINATED); + if (xfer == xfer->final_xfer) + complete(xfer->final_xfer->completion); + } +} + +static void hci_dma_error_out_all(struct i3c_hci *hci) +{ + struct hci_rings_data *rings = hci->io_data; + + for (int i = 0; i < rings->total; i++) + hci_dma_error_out_rh(hci, &rings->headers[i]); +} + +static void hci_dma_recovery(struct i3c_hci *hci) +{ + int ret; + + dev_err(&hci->master.dev, "Attempting to recover from internal errors\n"); + + for (int i = 0; i < 3; i++) { + ret = i3c_hci_reset_and_restore(hci); + if (!ret) + break; + dev_err(&hci->master.dev, "Reset and restore failed, error %d\n", ret); + /* Just in case the controller is busy, give it some time */ + msleep(1000); + } + + spin_lock_irq(&hci->lock); + hci_dma_error_out_all(hci); + hci_dma_unblock_enqueue(hci); + hci->recovery_needed = false; + spin_unlock_irq(&hci->lock); + + dev_err(&hci->master.dev, "Recovery %s\n", ret ? "failed!" : "done"); +} + static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n) { @@ -640,6 +701,17 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { + /* + * The transfer may have already completed, especially + * if recovery has just run. Do nothing in that case. + */ + hci_dma_xfer_done(hci, rh); + if (xfer_list->final_xfer->ring_entry < 0 && + !hci->recovery_needed && !hci->enqueue_blocked && + ring_status == (RING_STATUS_ENABLED | RING_STATUS_RUNNING)) { + spin_unlock_irq(&hci->lock); + return false; + } hci->enqueue_blocked = true; spin_unlock_irq(&hci->lock); /* stop the ring */ @@ -647,12 +719,8 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, spin_lock_irq(&hci->lock); ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { - /* - * We're deep in it if ever this condition is ever met. - * Hardware might still be writing to memory, etc. - */ - dev_crit(&hci->master.dev, "unable to abort the ring\n"); - WARN_ON(1); + dev_err(&hci->master.dev, "Unable to abort the DMA ring\n"); + hci->recovery_needed = true; } } @@ -662,6 +730,13 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, hci_dma_xfer_done(hci, rh); + if (hci->recovery_needed) { + hci->enqueue_blocked = true; + spin_unlock_irq(&hci->lock); + hci_dma_recovery(hci); + return true; + } + for (i = 0; i < n; i++) { struct hci_xfer *xfer = xfer_list + i; int idx = xfer->ring_entry; diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index a3151c26827e..4bf2c66c97b4 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -55,6 +55,7 @@ struct i3c_hci { atomic_t next_cmd_tid; bool irq_inactive; bool enqueue_blocked; + bool recovery_needed; wait_queue_head_t enqueue_wait_queue; u32 caps; unsigned int quirks; -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:18 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:18 +0300 Subject: [PATCH V4 14/17] i3c: mipi-i3c-hci: Wait for NoOp commands to complete In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-15-adrian.hunter@intel.com> When a transfer list is only partially completed due to an error, hci_dma_dequeue_xfer() overwrites the remaining DMA ring entries with NoOp commands and restarts the ring to flush them out. While NoOp commands are expected to complete successfully, they may still fail to complete if the DMA ring is stuck. Explicitly wait for the NoOp commands to finish, and trigger controller recovery if they do not complete or report an error. This ensures that partially completed transfer lists are reliably resolved and that a stuck ring is recovered promptly. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: Add Frank's Rev'd-by Changes in V3: None Changes in V2: Rename completing_xfer to final_xfer Add missing reinit_completion() drivers/i3c/master/mipi-i3c-hci/dma.c | 39 ++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index f39a6ce2aad5..0fd56bbb84ef 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -686,11 +686,33 @@ static void hci_dma_recovery(struct i3c_hci *hci) dev_err(&hci->master.dev, "Recovery %s\n", ret ? "failed!" : "done"); } +static bool hci_dma_wait_for_noop(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n, + int noop_pos) +{ + struct completion *done = xfer_list->final_xfer->completion; + bool timeout = !wait_for_completion_timeout(done, HZ); + u32 error = timeout; + + for (int i = noop_pos; i < n && !error; i++) + error = RESP_STATUS(xfer_list[i].response); + + if (!error) + return true; + + if (timeout) + dev_err(&hci->master.dev, "NoOp timeout error\n"); + else + dev_err(&hci->master.dev, "NoOp error %u\n", error); + + return false; +} + static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_xfer *xfer_list, int n) { struct hci_rings_data *rings = hci->io_data; struct hci_rh_data *rh = &rings->headers[xfer_list[0].ring_number]; + int noop_pos = -1; unsigned int i; bool did_unqueue = false; u32 ring_status; @@ -698,7 +720,7 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, guard(mutex)(&hci->control_mutex); spin_lock_irq(&hci->lock); - +restart: ring_status = rh_reg_read(RING_STATUS); if (ring_status & RING_STATUS_RUNNING) { /* @@ -757,11 +779,10 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, *ring_data++ = 0; } - /* disassociate this xfer struct */ - rh->src_xfers[idx] = NULL; - - /* and unmap it */ - hci_dma_unmap_xfer(hci, xfer, 1); + if (noop_pos < 0) { + reinit_completion(xfer->final_xfer->completion); + noop_pos = i; + } did_unqueue = true; } @@ -793,6 +814,12 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, wait_for_completion_timeout(&rh->op_done, HZ); + if (did_unqueue && !hci_dma_wait_for_noop(hci, xfer_list, n, noop_pos)) { + spin_lock_irq(&hci->lock); + hci->recovery_needed = true; + goto restart; + } + return did_unqueue; } -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:19 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:19 +0300 Subject: [PATCH V4 15/17] i3c: mipi-i3c-hci: Base timeouts on actual transfer start time In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-16-adrian.hunter@intel.com> Transfer timeouts are currently measured from the point where a transfer list is queued to the controller. This can cause transfers to time out before they have actually started, if earlier queued transfers consume the timeout interval. Fix this by recording when a transfer reaches the head of the queue and adjusting the timeout calculation to start from that point. The existing low-overhead completion-based timeout mechanism is preserved, but care is taken to ensure the transfer start time is consistently recorded for both PIO and DMA paths. This prevents premature timeouts while retaining efficient timeout handling. Signed-off-by: Adrian Hunter --- Changes in V4: Rename start_time to start_jiffies Changes in V3: None Changes in V2: Do not flag the next transfer as started when there is an error which halts the controller Instead flag it started at the end of hci_dma_dequeue_xfer() Use hci_start_xfer() in pio.c drivers/i3c/master/mipi-i3c-hci/core.c | 19 ++++++++++++++++++- drivers/i3c/master/mipi-i3c-hci/dma.c | 19 ++++++++++++++++++- drivers/i3c/master/mipi-i3c-hci/hci.h | 11 +++++++++++ drivers/i3c/master/mipi-i3c-hci/pio.c | 1 + 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 69dcf5dad3a5..c6edbbedfdd7 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -275,13 +275,30 @@ int i3c_hci_process_xfer(struct i3c_hci *hci, struct hci_xfer *xfer, int n) { struct completion *done = xfer[n - 1].completion; unsigned long timeout = xfer[n - 1].timeout; + unsigned long remaining_timeout = timeout; + long time_taken; + bool started; int ret; + xfer[0].started = false; + ret = hci->io->queue_xfer(hci, xfer, n); if (ret) return ret; - if (!wait_for_completion_timeout(done, timeout)) { + while (!wait_for_completion_timeout(done, remaining_timeout)) { + scoped_guard(spinlock_irqsave, &hci->lock) { + started = xfer[0].started; + time_taken = jiffies - xfer[0].start_jiffies; + } + /* Keep waiting if xfer has not started */ + if (!started) + continue; + /* Recalculate timeout based on actual start time */ + if (time_taken < timeout) { + remaining_timeout = timeout - time_taken; + continue; + } if (hci->io->dequeue_xfer(hci, xfer, n)) { dev_err(&hci->master.dev, "%s: timeout error\n", __func__); return -ETIMEDOUT; diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 0fd56bbb84ef..9a01c740760f 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -543,6 +543,9 @@ static int hci_dma_queue_xfer(struct i3c_hci *hci, enqueue_ptr = (enqueue_ptr + 1) % rh->xfer_entries; } + if (rh->xfer_space == rh->xfer_entries) + hci_start_xfer(xfer_list); + rh->xfer_space -= n; op1_val &= ~RING_OP1_CR_ENQ_PTR; @@ -558,6 +561,7 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) u32 op1_val, op2_val, resp, *ring_resp; unsigned int tid, done_ptr = rh->done_ptr; unsigned int done_cnt = 0; + bool start_next = false; struct hci_xfer *xfer; for (;;) { @@ -588,8 +592,14 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) xfer->response = resp; if (xfer == xfer->final_xfer || RESP_STATUS(resp)) complete(xfer->final_xfer->completion); - if (RESP_STATUS(resp)) + else + hci_start_xfer(xfer); + if (RESP_STATUS(resp)) { hci->enqueue_blocked = true; + start_next = false; + } else { + start_next = true; + } } done_ptr = (done_ptr + 1) % rh->xfer_entries; @@ -598,6 +608,10 @@ static void hci_dma_xfer_done(struct i3c_hci *hci, struct hci_rh_data *rh) } rh->xfer_space += done_cnt; + if (start_next && rh->xfer_space < rh->xfer_entries) { + xfer = rh->src_xfers[done_ptr]; + hci_start_xfer(xfer); + } op1_val = rh_reg_read(RING_OPERATION1); op1_val &= ~RING_OP1_CR_SW_DEQ_PTR; op1_val |= FIELD_PREP(RING_OP1_CR_SW_DEQ_PTR, done_ptr); @@ -810,6 +824,9 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, hci_dma_unblock_enqueue(hci); + if (rh->xfer_space < rh->xfer_entries) + hci_start_xfer(rh->src_xfers[rh->done_ptr]); + spin_unlock_irq(&hci->lock); wait_for_completion_timeout(&rh->op_done, HZ); diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 4bf2c66c97b4..30297823ca85 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -11,6 +11,7 @@ #define HCI_H #include +#include /* 32-bit word aware bit and mask macros */ #define W0_MASK(h, l) GENMASK((h) - 0, (l) - 0) @@ -88,11 +89,13 @@ struct hci_xfer { u32 cmd_desc[4]; u32 response; bool rnw; + bool started; void *data; unsigned int data_len; unsigned int cmd_tid; struct completion *completion; unsigned long timeout; + unsigned long start_jiffies; union { struct { /* PIO specific */ @@ -123,6 +126,14 @@ static inline void hci_free_xfer(struct hci_xfer *xfer, unsigned int n) kfree(xfer); } +static inline void hci_start_xfer(struct hci_xfer *xfer) +{ + if (!xfer->started) { + xfer->started = true; + xfer->start_jiffies = jiffies; + } +} + /* This abstracts PIO vs DMA operations */ struct hci_io_ops { bool (*irq_handler)(struct i3c_hci *hci); diff --git a/drivers/i3c/master/mipi-i3c-hci/pio.c b/drivers/i3c/master/mipi-i3c-hci/pio.c index 8f48a81e65ab..6b8cc5f2b4d2 100644 --- a/drivers/i3c/master/mipi-i3c-hci/pio.c +++ b/drivers/i3c/master/mipi-i3c-hci/pio.c @@ -605,6 +605,7 @@ static bool hci_pio_process_cmd(struct i3c_hci *hci, struct hci_pio_data *pio) * Finally send the command. */ hci_pio_write_cmd(hci, pio->curr_xfer); + hci_start_xfer(pio->curr_xfer); /* * And move on. */ -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:20 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:20 +0300 Subject: [PATCH V4 16/17] i3c: mipi-i3c-hci: Consolidate DMA ring allocation In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-17-adrian.hunter@intel.com> dma_alloc_coherent() allocates memory in whole pages, which can waste space when command and response queues are allocated separately. Allocate the DMA command and response queues from a single coherent allocation instead, while preserving the required 4-byte alignment. This reduces memory overhead without changing behavior. Signed-off-by: Adrian Hunter --- Changes in V4: Cache allocation size in rh->xfer_alloc_sz to avoid recomputing in hci_dma_free() Changes in V3: None Changes in V2: Check for failed allocation before assignments to avoid doing arithmetic with NULL pointers drivers/i3c/master/mipi-i3c-hci/dma.c | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 9a01c740760f..0136f3064ada 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -130,7 +130,7 @@ struct hci_rh_data { dma_addr_t xfer_dma, resp_dma, ibi_status_dma, ibi_data_dma; unsigned int xfer_entries, ibi_status_entries, ibi_chunks_total; unsigned int xfer_struct_sz, resp_struct_sz, ibi_status_sz, ibi_chunk_sz; - unsigned int done_ptr, ibi_chunk_ptr, xfer_space; + unsigned int xfer_alloc_sz, done_ptr, ibi_chunk_ptr, xfer_space; struct hci_xfer **src_xfers; struct completion op_done; }; @@ -187,13 +187,7 @@ static void hci_dma_free(void *data) rh = &rings->headers[i]; if (rh->xfer) - dma_free_coherent(rings->sysdev, - rh->xfer_struct_sz * rh->xfer_entries, - rh->xfer, rh->xfer_dma); - if (rh->resp) - dma_free_coherent(rings->sysdev, - rh->resp_struct_sz * rh->xfer_entries, - rh->resp, rh->resp_dma); + dma_free_coherent(rings->sysdev, rh->xfer_alloc_sz, rh->xfer, rh->xfer_dma); kfree(rh->src_xfers); if (rh->ibi_status) dma_free_coherent(rings->sysdev, @@ -359,18 +353,19 @@ static int hci_dma_init(struct i3c_hci *hci) dev_dbg(&hci->master.dev, "xfer_struct_sz = %d, resp_struct_sz = %d", rh->xfer_struct_sz, rh->resp_struct_sz); - xfers_sz = rh->xfer_struct_sz * rh->xfer_entries; + xfers_sz = round_up(rh->xfer_struct_sz * rh->xfer_entries, 4); resps_sz = rh->resp_struct_sz * rh->xfer_entries; + rh->xfer_alloc_sz = xfers_sz + resps_sz; - rh->xfer = dma_alloc_coherent(rings->sysdev, xfers_sz, + rh->xfer = dma_alloc_coherent(rings->sysdev, rh->xfer_alloc_sz, &rh->xfer_dma, GFP_KERNEL); - rh->resp = dma_alloc_coherent(rings->sysdev, resps_sz, - &rh->resp_dma, GFP_KERNEL); rh->src_xfers = kzalloc_objs(*rh->src_xfers, rh->xfer_entries); ret = -ENOMEM; - if (!rh->xfer || !rh->resp || !rh->src_xfers) + if (!rh->xfer || !rh->src_xfers) goto err_out; + rh->resp = rh->xfer + xfers_sz; + rh->resp_dma = rh->xfer_dma + xfers_sz; /* IBIs */ -- 2.51.0 From adrian.hunter at intel.com Fri May 15 09:26:21 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:26:21 +0300 Subject: [PATCH V4 17/17] i3c: mipi-i3c-hci: Increase DMA transfer ring size to maximum In-Reply-To: <20260515162621.57719-1-adrian.hunter@intel.com> References: <20260515162621.57719-1-adrian.hunter@intel.com> Message-ID: <20260515162621.57719-18-adrian.hunter@intel.com> The DMA transfer ring is currently limited to 16 entries, despite the MIPI I3C HCI supporting up to 32 devices. When the ring lacks space for a new transfer list, the driver returns -EBUSY, which can be unexpected for clients. Increase the DMA transfer ring size to the maximum supported value of 255 entries. This effectively eliminates ring-space exhaustion in practice and avoids the complexity of adding secondary queuing mechanisms. Even at the maximum size, the memory overhead remains small (approximately 24 bytes per entry by default). Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V4: Add Frank's Rev'd-by Changes in V2 and V3: None 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 0136f3064ada..5c6ae2055618 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -27,7 +27,7 @@ */ #define XFER_RINGS 1 /* max: 8 */ -#define XFER_RING_ENTRIES 16 /* max: 255 */ +#define XFER_RING_ENTRIES 255 /* max: 255 */ #define IBI_RINGS 1 /* max: 8 */ #define IBI_STATUS_RING_ENTRIES 32 /* max: 255 */ -- 2.51.0 From Frank.li at nxp.com Fri May 15 09:38:15 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 15 May 2026 12:38:15 -0400 Subject: [RFC PATCH] i3c: dw: add DMA support for data transfers In-Reply-To: <20260515155638.1840-1-hechushiguitu666@gmail.com> References: <20260515155638.1840-1-hechushiguitu666@gmail.com> Message-ID: On Fri, May 15, 2026 at 11:56:35PM +0800, Haoyu Lu wrote: > Add DMA support for the DesignWare I3C master controller to > accelerate data transfers. > > The driver now requests tx and rx DMA channels during probe and > uses them for data transfers of 4 bytes or more. Transfers smaller > than 4 bytes, or transfers where DMA setup fails, fall back to PIO > mode. DMA tail bytes (non-4-byte-aligned remainder) are handled > with PIO as well. > > DMA transfers may sleep, so the xferqueue spinlock is temporarily > dropped for DMA operations. This is safe because xferqueue.cur is > only changed when a transfer completes, and the calling context > serializes simultaneous transfers. > > The DMA enable bit (DEV_CTRL_DMA_EN, BIT(28)) is set when DMA is > used and cleared otherwise, to ensure the hardware uses the correct > data transfer path. > > Signed-off-by: Haoyu Lu > --- > This is an RFC patch to gather feedback on the overall approach before > a formal submission. A few specific questions for reviewers: > > 1. DMA threshold: The current implementation uses DMA for transfers > >= 4 bytes, with tail bytes handled by PIO. Is 4 bytes a reasonable > threshold, or should we consider a higher value (e.g. 16 or 32) to > avoid DMA setup overhead for very small transfers? > > 2. Spinlock safety: dw_i3c_master_start_xfer_locked() and > dw_i3c_master_end_xfer_locked() temporarily drop xferqueue.lock > around DMA operations. The analysis is that xferqueue.cur is only > modified on transfer completion, and the caller serializes > simultaneous transfers. Is this safe, or should we restructure the > locking (e.g., convert to mutex)? > > 3. DMA channel handling: dw_i3c_dma_request() is called during probe > and silently degrades to PIO if DMA channels are unavailable. Is > this the right behavior, or should probe fail on DMA errors other > than -ENODEV/-EPROBE_DEFER? > > 4. Error handling for DMA timeouts: when a DMA timeout occurs, > dmaengine_terminate_all() is called but the transfer proceeds as if > it were successful, potentially reading stale data. Should we instead > signal an error to the I3C core? > > 5. IBI path: dw_i3c_master_read_ibi_fifo() still uses PIO. Should > IBI reads also use DMA, or is the IBI payload typically small enough > that PIO is preferred? > > Any other feedback on the design or implementation is welcome. > > drivers/i3c/master/dw-i3c-master.c | 260 ++++++++++++++++++++++++++++- > drivers/i3c/master/dw-i3c-master.h | 17 ++ > 2 files changed, 270 insertions(+), 7 deletions(-) > > diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c > index d6bdb32397fb..7b6c8681ff0c 100644 > --- a/drivers/i3c/master/dw-i3c-master.c > +++ b/drivers/i3c/master/dw-i3c-master.c > @@ -13,6 +13,7 @@ > #include > #include > #include > +#include > #include > #include > #include > @@ -24,6 +25,10 @@ > #include > #include > > +#include > +#include > +#include > + > #include "../internals.h" > #include "dw-i3c-master.h" > > @@ -32,6 +37,7 @@ > #define DEV_CTRL_RESUME BIT(30) > #define DEV_CTRL_HOT_JOIN_NACK BIT(8) > #define DEV_CTRL_I2C_SLAVE_PRESENT BIT(7) > +#define DEV_CTRL_DMA_EN BIT(28) > > #define DEVICE_ADDR 0x4 > #define DEV_ADDR_DYNAMIC_ADDR_VALID BIT(31) > @@ -359,16 +365,123 @@ static int dw_i3c_master_get_free_pos(struct dw_i3c_master *master) > return ffs(master->free_pos) - 1; > } > > +static void dw_i3c_dma_callback(void *arg) > +{ > + struct dw_i3c_dma *dma = arg; > + > + dma_unmap_single(dma->chan_using->device->dev, dma->dma_buf, > + dma->dma_len, dma->dma_data_dir); > + complete(&dma->complete); > +} > + > +static int dw_i3c_master_dma_xfer(struct dw_i3c_dma *dma, const u8 *bytes) > +{ > + struct device *chan_dev = dma->chan_using->device->dev; > + struct dma_async_tx_descriptor *txdesc; > + > + dma->dma_buf = dma_map_single(chan_dev, (void *)bytes, dma->dma_len, > + dma->dma_data_dir); > + if (dma_mapping_error(chan_dev, dma->dma_buf)) > + return -EINVAL; > + > + txdesc = dmaengine_prep_slave_single(dma->chan_using, dma->dma_buf, > + dma->dma_len, dma->dma_transfer_dir, > + DMA_PREP_INTERRUPT); > + if (!txdesc) { > + dma_unmap_single(chan_dev, dma->dma_buf, dma->dma_len, > + dma->dma_data_dir); > + return -ENOMEM; > + } There are help function i3c_master_dma_map_single() to help bounce unalign buffer. > + > + reinit_completion(&dma->complete); > + txdesc->callback = dw_i3c_dma_callback; > + txdesc->callback_param = dma; > + if (dma_submit_error(dmaengine_submit(txdesc))) { > + dma_unmap_single(chan_dev, dma->dma_buf, dma->dma_len, > + dma->dma_data_dir); > + return -EIO; > + } > + > + dma_async_issue_pending(dma->chan_using); > + return 0; > +} > + > static void dw_i3c_master_wr_tx_fifo(struct dw_i3c_master *master, > const u8 *bytes, int nbytes) > { > - i3c_writel_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); > + struct dw_i3c_dma *dma = master->dma; > + unsigned long time_left; > + int ret; > + > + if (!master->use_dma || nbytes < 4) { > + i3c_writel_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); > + return; > + } > + > + dma->chan_using = dma->chan_tx; > + dma->dma_transfer_dir = DMA_MEM_TO_DEV; > + dma->dma_data_dir = DMA_TO_DEVICE; > + dma->dma_len = ALIGN_DOWN(nbytes, 4); > + > + ret = dw_i3c_master_dma_xfer(dma, bytes); > + if (ret) { > + dev_err(&master->base.dev, "DMA TX setup failed (%d)\n", ret); > + i3c_writel_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); > + return; > + } > + > + time_left = wait_for_completion_timeout(&dma->complete, > + XFER_TIMEOUT); > + if (!time_left) { > + dmaengine_terminate_all(dma->chan_using); > + dev_err(&master->base.dev, "DMA TX timeout\n"); > + } > + > + if (nbytes & 3) { > + u32 tmp = 0; > + > + memcpy(&tmp, bytes + (nbytes & ~3), nbytes & 3); > + writesl(master->regs + RX_TX_DATA_PORT, &tmp, 1); > + } > } > > static void dw_i3c_master_read_rx_fifo(struct dw_i3c_master *master, > u8 *bytes, int nbytes) > { > - i3c_readl_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); > + struct dw_i3c_dma *dma = master->dma; > + unsigned long time_left; > + int ret; > + > + if (!master->use_dma || nbytes < 4) { > + i3c_readl_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); > + return; > + } > + > + dma->chan_using = dma->chan_rx; > + dma->dma_transfer_dir = DMA_DEV_TO_MEM; > + dma->dma_data_dir = DMA_FROM_DEVICE; > + dma->dma_len = ALIGN_DOWN(nbytes, 4); > + > + ret = dw_i3c_master_dma_xfer(dma, bytes); > + if (ret) { > + dev_err(&master->base.dev, "DMA RX setup failed (%d)\n", ret); > + i3c_readl_fifo(master->regs + RX_TX_DATA_PORT, bytes, nbytes); > + return; > + } > + > + time_left = wait_for_completion_timeout(&dma->complete, > + XFER_TIMEOUT); > + if (!time_left) { > + dmaengine_terminate_all(dma->chan_using); > + dev_err(&master->base.dev, "DMA RX timeout\n"); > + } > + > + if (nbytes & 3) { > + u32 tmp; > + > + readsl(master->regs + RX_TX_DATA_PORT, &tmp, 1); > + memcpy(bytes + (nbytes & ~3), &tmp, nbytes & 3); > + } > } > > static void dw_i3c_master_read_ibi_fifo(struct dw_i3c_master *master, > @@ -403,14 +516,29 @@ static void dw_i3c_master_start_xfer_locked(struct dw_i3c_master *master) > struct dw_i3c_xfer *xfer = master->xferqueue.cur; > unsigned int i; > u32 thld_ctrl; > + u32 dev_ctrl; > > if (!xfer) > return; > > - for (i = 0; i < xfer->ncmds; i++) { > - struct dw_i3c_cmd *cmd = &xfer->cmds[i]; > - > - dw_i3c_master_wr_tx_fifo(master, cmd->tx_buf, cmd->tx_len); > + /* > + * DMA transfers may sleep, so we must drop the spinlock while > + * writing the TX FIFO. The xferqueue lock is held by our caller; > + * xferqueue.cur is safe because: > + * - New xfers are only added to the list (not cur) when cur is set. > + * - Timeout-based dequeue is blocked until the caller calls > + * wait_for_completion_timeout(), which happens after we return. > + */ > + if (master->use_dma) { > + spin_unlock(&master->xferqueue.lock); > + for (i = 0; i < xfer->ncmds; i++) > + dw_i3c_master_wr_tx_fifo(master, xfer->cmds[i].tx_buf, > + xfer->cmds[i].tx_len); Here have risk, you need queue whole transfer once. because 1. First cmd 2. dma transfer 3. dma irq, which may delay or schedule by OS, there are 100us timeout by target. 4. target may timeout, so treat REPEAT start as START, 5. target may raise IBI here, but host side think it is repeat START, don't check address arbitration. Frank From adrian.hunter at intel.com Fri May 15 09:42:20 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 15 May 2026 19:42:20 +0300 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> Message-ID: <417993a7-4a4f-4ba5-a815-aab63ed03a3c@intel.com> On 13/05/2026 22:03, Frank Li wrote: > On Wed, May 13, 2026 at 08:45:55AM +0300, Adrian Hunter wrote: >> On 12/05/2026 19:39, Frank Li wrote: >>> On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: >>>> Master drivers may invoke i3c_master_do_daa_ext() during resume to >>>> re-run Dynamic Address Assignment. As well as assigning addresses to >>>> any newly arrived devices, this restores the dynamic address of devices >>>> that lost it across system suspend, so it has to run as part of the >>>> controller's resume path. >>>> >>>> A side effect of i3c_master_do_daa_ext() today is that it also >>>> registers any newly discovered I3C devices with the driver model >>>> inline, via i3c_master_register_new_i3c_devs(). Doing that from the >>>> resume path is problematic: a hot-join-capable device may join the bus >>>> during this same DAA, and registering it immediately would push driver >>>> model work (probing, sysfs, etc.) into the controller's resume context, >>>> where the rest of the system is not yet fully resumed and the >>>> controller driver is still partway through its own resume sequence. >>>> >>>> Decouple discovery from registration: add a reg_work work item to >>>> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it >>>> on master->wq (the freezable workqueue) instead of calling >>>> i3c_master_register_new_i3c_devs() directly. The worker performs the >>>> registration only when the controller is not shutting_down, and is >>>> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is >>>> freezable, any newly observed devices end up being registered after >>>> the system has finished resuming. >>>> >>>> i3c_master_register() also routes its initial post-bus-init registration >>>> through reg_work, using flush_work() to keep probe-time behavior >>>> synchronous. This keeps a single registration code path and ensures the >>>> worker is the only writer of desc->dev. >>> >>> why not direct use hj_work? >> >> i3c_master_register_new_i3c_devs() use of desc->dev is racy, so >> i3c_master_register_new_i3c_devs() must not be allowed to race >> with itself. Having it only ever run via reg_work achieves that. > > Sorry, I have not understand these, Can provide some detail? >From i3c_master_register_new_i3c_devs(): i3c_bus_for_each_i3cdev(&master->bus, desc) { if (desc->dev || !desc->info.dyn_addr || desc == master->this) continue; desc->dev = kzalloc_obj(*desc->dev); ... ret = device_register(&desc->dev->dev); This is done under the shared i3c_bus_normaluse_lock(), so there can be 2 or more instances of i3c_master_register_new_i3c_devs() running at the same time. They might all see desc->dev is NULL and then all of them try to initialize and register a dev for the same I3C device. From conor at kernel.org Thu May 14 09:22:11 2026 From: conor at kernel.org (Conor Dooley) Date: Thu, 14 May 2026 17:22:11 +0100 Subject: [PATCH v2 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <20260514-fat-unyielding-jaguarundi-a5ddc6@quoll> References: <20260511031945.3228-1-jszhang@kernel.org> <20260511031945.3228-3-jszhang@kernel.org> <20260511-amnesty-afoot-84537aafc02c@spud> <20260514-fat-unyielding-jaguarundi-a5ddc6@quoll> Message-ID: <20260514-gestation-hatred-69029fd05f70@spud> On Thu, May 14, 2026 at 02:17:55PM +0200, Krzysztof Kozlowski wrote: > On Mon, May 11, 2026 at 05:02:18PM +0100, Conor Dooley wrote: > > On Mon, May 11, 2026 at 11:19:44AM +0800, Jisheng Zhang wrote: > > > Add dt-binding for support of apb reset which is to reset the APB > > > interface. > > > > > > Signed-off-by: Jisheng Zhang > > > > Please squash both dt-binding patches. > > I think this should stay separate, because first commit is trying to fix > undocumented existing ABI. It will have different rationale and could be > chosen for backports. Ah, I guess I didn't read the commit message. Sorry about that Jisheng! -------------- next part -------------- A non-text attachment was scrubbed... Name: signature.asc Type: application/pgp-signature Size: 228 bytes Desc: not available URL: From claudiu.beznea at tuxon.dev Sat May 16 09:06:35 2026 From: claudiu.beznea at tuxon.dev (Claudiu Beznea) Date: Sat, 16 May 2026 19:06:35 +0300 Subject: [PATCH v6 2/5] clk: at91: sama7d65: add peripheral clock for I3C In-Reply-To: <20260507084805.481737-3-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> <20260507084805.481737-3-manikandan.m@microchip.com> Message-ID: <2d9dc9dd-f008-4cd3-95a6-4413986f4b51@tuxon.dev> On 5/7/26 11:48, Manikandan Muralidharan wrote: > From: Durai Manickam KR > > Add peripheral clock description for I3C. > > Signed-off-by: Durai Manickam KR > Signed-off-by: Manikandan Muralidharan Reviewed-by: Claudiu Beznea From claudiu.beznea at tuxon.dev Sat May 16 09:07:10 2026 From: claudiu.beznea at tuxon.dev (Claudiu Beznea) Date: Sat, 16 May 2026 19:07:10 +0300 Subject: [PATCH v6 4/5] ARM: dts: microchip: add I3C controller In-Reply-To: <20260507084805.481737-5-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> <20260507084805.481737-5-manikandan.m@microchip.com> Message-ID: Hi, Manikandan, On 5/7/26 11:48, Manikandan Muralidharan wrote: > From: Durai Manickam KR > > Add I3C controller for sama7d65 SoC. > > Signed-off-by: Durai Manickam KR > Signed-off-by: Manikandan Muralidharan > --- > Changes in v3: > - Remove clock-names property as driver enables the clk in bulk > > arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++++ > 1 file changed, 8 insertions(+) > > diff --git a/arch/arm/boot/dts/microchip/sama7d65.dtsi b/arch/arm/boot/dts/microchip/sama7d65.dtsi > index 67253bbc08df..ec200848c153 100644 > --- a/arch/arm/boot/dts/microchip/sama7d65.dtsi > +++ b/arch/arm/boot/dts/microchip/sama7d65.dtsi > @@ -1055,5 +1055,13 @@ gic: interrupt-controller at e8c11000 { > #address-cells = <0>; > interrupt-controller; > }; > + > + i3c: i3c at e9000000 { > + compatible = "microchip,sama7d65-i3c-hci"; > + reg = <0xe9000000 0x300>; From manual at [1] I see the size of I3CC region is 0x1000. Unless that is wrong I think we should use 0x1000 to properly describe de HW. Please let me know and I can do it while applying. Thank you, Claudiu [1] https://ww1.microchip.com/downloads/aemDocuments/documents/MPU32/ProductDocuments/DataSheets/SAMA7D6-Series-Data-Sheet-DS60001851.pdf > + interrupts = ; > + clocks = <&pmc PMC_TYPE_PERIPHERAL 105>, <&pmc PMC_TYPE_GCK 105>; > + status = "disabled"; > + }; > }; > }; From claudiu.beznea at tuxon.dev Sat May 16 09:07:27 2026 From: claudiu.beznea at tuxon.dev (Claudiu Beznea) Date: Sat, 16 May 2026 19:07:27 +0300 Subject: [PATCH v6 5/5] ARM: configs: at91: sama7: add sama7d65 i3c-hci In-Reply-To: <20260507084805.481737-6-manikandan.m@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> <20260507084805.481737-6-manikandan.m@microchip.com> Message-ID: On 5/7/26 11:48, Manikandan Muralidharan wrote: > Enable the configs needed for I3C framework and microchip > sama7d65 i3c-hci driver. > > Signed-off-by: Durai Manickam KR > Signed-off-by: Manikandan Muralidharan Reviewed-by: Claudiu Beznea From Manikandan.M at microchip.com Sun May 17 23:10:19 2026 From: Manikandan.M at microchip.com (Manikandan.M at microchip.com) Date: Mon, 18 May 2026 06:10:19 +0000 Subject: [PATCH v6 4/5] ARM: dts: microchip: add I3C controller In-Reply-To: References: <20260507084805.481737-1-manikandan.m@microchip.com> <20260507084805.481737-5-manikandan.m@microchip.com> Message-ID: <515e89f3-fca9-477c-be4d-be9ed9428d5f@microchip.com> Hi Claudiu, On 16/05/26 9:37 pm, Claudiu Beznea wrote: > EXTERNAL EMAIL: Do not click links or open attachments unless you know > the content is safe > > Hi, Manikandan, > > On 5/7/26 11:48, Manikandan Muralidharan wrote: >> From: Durai Manickam KR >> >> Add I3C controller for sama7d65 SoC. >> >> Signed-off-by: Durai Manickam KR >> Signed-off-by: Manikandan Muralidharan >> --- >> Changes in v3: >> - Remove clock-names property as driver enables the clk in bulk >> >> ? arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++++ >> ? 1 file changed, 8 insertions(+) >> >> diff --git a/arch/arm/boot/dts/microchip/sama7d65.dtsi >> b/arch/arm/boot/dts/microchip/sama7d65.dtsi >> index 67253bbc08df..ec200848c153 100644 >> --- a/arch/arm/boot/dts/microchip/sama7d65.dtsi >> +++ b/arch/arm/boot/dts/microchip/sama7d65.dtsi >> @@ -1055,5 +1055,13 @@ gic: interrupt-controller at e8c11000 { >> ????????????????????? #address-cells = <0>; >> ????????????????????? interrupt-controller; >> ????????????? }; >> + >> +???????????? i3c: i3c at e9000000 { >> +???????????????????? compatible = "microchip,sama7d65-i3c-hci"; >> +???????????????????? reg = <0xe9000000 0x300>; > > From manual at [1] I see the size of I3CC region is 0x1000. Unless that is > wrong I think we should use 0x1000 to properly describe de HW. Please > let me > know and I can do it while applying. > According to Table 78.6 (Register Summary), the I3CC register space extends up to offset 0x258, Ideally the mapping should have been 0x400 (next power of 2 considering the memory region alignment), using 0x1000 is also acceptable. Please advise which value is preferred. > Thank you, > Claudiu > > [1] > https://ww1.microchip.com/downloads/aemDocuments/documents/MPU32/ProductDocuments/DataSheets/SAMA7D6-Series-Data-Sheet-DS60001851.pdf > >> +???????????????????? interrupts = ; >> +???????????????????? clocks = <&pmc PMC_TYPE_PERIPHERAL 105>, <&pmc >> PMC_TYPE_GCK 105>; >> +???????????????????? status = "disabled"; >> +???????????? }; >> ????? }; >> ? }; > -- Thanks and Regards, Manikandan M. From nicolas.ferre at microchip.com Mon May 18 00:27:55 2026 From: nicolas.ferre at microchip.com (Nicolas Ferre) Date: Mon, 18 May 2026 09:27:55 +0200 Subject: [PATCH v6 4/5] ARM: dts: microchip: add I3C controller In-Reply-To: <515e89f3-fca9-477c-be4d-be9ed9428d5f@microchip.com> References: <20260507084805.481737-1-manikandan.m@microchip.com> <20260507084805.481737-5-manikandan.m@microchip.com> <515e89f3-fca9-477c-be4d-be9ed9428d5f@microchip.com> Message-ID: <0ae90352-2099-4d3d-a55c-40a6e090fde4@microchip.com> On 18/05/2026 at 08:10, Manikandan M - I67131 wrote: > Hi Claudiu, > > On 16/05/26 9:37 pm, Claudiu Beznea wrote: >> EXTERNAL EMAIL: Do not click links or open attachments unless you know >> the content is safe >> >> Hi, Manikandan, >> >> On 5/7/26 11:48, Manikandan Muralidharan wrote: >>> From: Durai Manickam KR >>> >>> Add I3C controller for sama7d65 SoC. >>> >>> Signed-off-by: Durai Manickam KR >>> Signed-off-by: Manikandan Muralidharan >>> --- >>> Changes in v3: >>> - Remove clock-names property as driver enables the clk in bulk >>> >>> ? arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++++ >>> ? 1 file changed, 8 insertions(+) >>> >>> diff --git a/arch/arm/boot/dts/microchip/sama7d65.dtsi >>> b/arch/arm/boot/dts/microchip/sama7d65.dtsi >>> index 67253bbc08df..ec200848c153 100644 >>> --- a/arch/arm/boot/dts/microchip/sama7d65.dtsi >>> +++ b/arch/arm/boot/dts/microchip/sama7d65.dtsi >>> @@ -1055,5 +1055,13 @@ gic: interrupt-controller at e8c11000 { >>> ????????????????????? #address-cells = <0>; >>> ????????????????????? interrupt-controller; >>> ????????????? }; >>> + >>> +???????????? i3c: i3c at e9000000 { >>> +???????????????????? compatible = "microchip,sama7d65-i3c-hci"; >>> +???????????????????? reg = <0xe9000000 0x300>; >> >> From manual at [1] I see the size of I3CC region is 0x1000. Unless that is >> wrong I think we should use 0x1000 to properly describe de HW. Please >> let me >> know and I can do it while applying. The memory map simply describes what is the next memory boundary assigned (or void in this case), not the actual size of the IP user interface. So we took the opportunity to avoid mapping unused memory. > According to Table 78.6 (Register Summary), the I3CC register space > extends up to offset 0x258, Ideally the mapping should have been 0x400 The underlying memory mapping certainly does what is best, so I would cling to being the closest to last register described. So your 0x300 value looks very good to me. Best regards, Nicolas > (next power of 2 considering the memory region alignment), using 0x1000 > is also acceptable. Please advise which value is preferred. > >> Thank you, >> Claudiu >> >> [1] >> https://ww1.microchip.com/downloads/aemDocuments/documents/MPU32/ProductDocuments/DataSheets/SAMA7D6-Series-Data-Sheet-DS60001851.pdf >> >>> +???????????????????? interrupts = ; >>> +???????????????????? clocks = <&pmc PMC_TYPE_PERIPHERAL 105>, <&pmc >>> PMC_TYPE_GCK 105>; >>> +???????????????????? status = "disabled"; >>> +???????????? }; >>> ????? }; >>> ? }; >> > From adrian.hunter at intel.com Mon May 18 04:55:11 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:11 +0300 Subject: [PATCH V2 0/8] i3c: Hot-Join improvements and MIPI HCI Hot-Join support Message-ID: <20260518115520.98335-1-adrian.hunter@intel.com> Hi This series tightens the I3C core's handling of Hot-Join across system suspend, shutdown and unregister, consolidates the per-driver Hot-Join worker into the core, and finally wires the MIPI I3C HCI driver into the new Hot-Join framework. It applies on top of: https://lore.kernel.org/linux-i3c/20260504113352.38490-1-adrian.hunter at intel.com/ Patches 1-2 fix latent races in the existing Hot-Join machinery (suspend vs. Hot-Join work, and concurrent sysfs writers). Patches 3-5 consolidate the per-driver Hot-Join worker into the core and add proper teardown via an i3c_bus_type shutdown callback. Patch 6 defers driver-model registration of newly discovered devices out of the DAA caller's context, so resume-time DAA does not push device probing into the controller's resume sequence. Patches 7-8 add Hot-Join support to MIPI I3C HCI. Changes in V2: i3c: master: Make hot-join workqueue freezable to block hot-join during suspend Add Fixes tag Add Frank's Rev'd by i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock Add Fixes tag i3c: master: Consolidate Hot-Join DAA work in the core Add Frank's Rev'd by i3c: master: Ensure Hot-Join operations are stopped on shutdown Add dependency note to commit message Add Fixes tag Add Frank's Rev'd by i3c: dw: Drop redundant Hot-Join cancel_work_sync() in shutdown Add Frank's Rev'd by i3c: master: Defer new-device registration out of DAA caller context Add comment about reg_work use Add Fixes tag i3c: master: Export i3c_master_enec_disec_locked() Add Frank's Rev'd by i3c: mipi-i3c-hci: Add Hot-Join support Add Frank's Rev'd by Adrian Hunter (8): i3c: master: Make hot-join workqueue freezable to block hot-join during suspend i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock i3c: master: Consolidate Hot-Join DAA work in the core i3c: master: Ensure Hot-Join operations are stopped on shutdown i3c: dw: Drop redundant Hot-Join cancel_work_sync() in shutdown i3c: master: Defer new-device registration out of DAA caller context i3c: master: Export i3c_master_enec_disec_locked() i3c: mipi-i3c-hci: Add Hot-Join support drivers/i3c/master.c | 129 ++++++++++++++++++++++++++------- drivers/i3c/master/dw-i3c-master.c | 15 +--- drivers/i3c/master/dw-i3c-master.h | 2 - drivers/i3c/master/i3c-master-cdns.c | 14 +--- drivers/i3c/master/mipi-i3c-hci/core.c | 50 ++++++++++++- drivers/i3c/master/mipi-i3c-hci/dma.c | 5 ++ drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + drivers/i3c/master/mipi-i3c-hci/pio.c | 5 ++ drivers/i3c/master/svc-i3c-master.c | 14 +--- include/linux/i3c/master.h | 16 +++- 10 files changed, 179 insertions(+), 72 deletions(-) Regards Adrian From adrian.hunter at intel.com Mon May 18 04:55:12 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:12 +0300 Subject: [PATCH V2 1/8] i3c: master: Make hot-join workqueue freezable to block hot-join during suspend In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-2-adrian.hunter@intel.com> The I3C master workqueue (master->wq) is used to defer work that needs thread context and the bus maintenance lock, most notably Hot Join processing (which calls i3c_master_do_daa() to assign dynamic addresses to newly joined devices). Currently the workqueue keeps running across system suspend, which can race with the suspend path: - do_daa() may execute after the controller has been suspended, issuing bus transactions on a powered-down or otherwise unusable controller. - New I3C devices can be enumerated and added to the bus mid-suspend, registering driver model objects at a point where the I3C subsystem and its consumers are not prepared to handle them. Mark the workqueue WQ_FREEZABLE so its workers are frozen for the duration of system suspend/hibernate and resumed afterwards. This naturally defers any pending or newly queued Hot Join work until the system (and the controller) is fully resumed, closing both races without adding explicit suspend/resume synchronization in the master drivers. Update the kerneldoc for struct i3c_master_controller::wq to reflect that the workqueue is freezable. Fixes: 3a379bbcea0af ("i3c: Add core I3C infrastructure") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V2: Add Fixes tag Add Frank's Rev'd by drivers/i3c/master.c | 2 +- include/linux/i3c/master.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 5cd4e5da2233..ab11e2d79aab 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -3079,7 +3079,7 @@ int i3c_master_register(struct i3c_master_controller *master, if (ret) goto err_put_dev; - master->wq = alloc_workqueue("%s", WQ_PERCPU, 0, dev_name(parent)); + master->wq = alloc_workqueue("%s", WQ_PERCPU | WQ_FREEZABLE, 0, dev_name(parent)); if (!master->wq) { ret = -ENOMEM; goto err_put_dev; diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 592b646f6134..e6112e5f6608 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -515,7 +515,7 @@ struct i3c_master_controller_ops { * @boardinfo.i2c: list of I2C boardinfo objects * @boardinfo: board-level information attached to devices connected on the bus * @bus: I3C bus exposed by this master - * @wq: workqueue which can be used by master + * @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 * requires taking the bus lock in maintenance, which in turn, can only -- 2.51.0 From adrian.hunter at intel.com Mon May 18 04:55:13 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:13 +0300 Subject: [PATCH V2 2/8] i3c: master: Serialize i3c_set_hotjoin() with the maintenance lock In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-3-adrian.hunter@intel.com> i3c_set_hotjoin() dispatches the controller's enable_hotjoin() or disable_hotjoin() op and updates master->hotjoin under i3c_bus_normaluse_lock(). That lock is a read-side acquisition of bus->lock (down_read()), so it does not exclude concurrent callers. The hotjoin sysfs attribute can be opened multiple times, and writes through different opens are not serialized. Two concurrent writers to "hotjoin" can therefore race in i3c_set_hotjoin(), with the controller op and the master->hotjoin store from one call interleaving with the other. The hardware enable/disable state and the value reported by hotjoin_show() can end up out of sync. Take i3c_bus_maintenance_lock() instead. Toggling Hot Join enable changes bus state and is conceptually a maintenance operation, so the write-side acquisition of bus->lock is the appropriate lock and serializes concurrent callers against each other and against other maintenance operations. Fixes: 317bacf960a48 ("i3c: master: add enable(disable) hot join in sys entry") Signed-off-by: Adrian Hunter --- Changes in V2: Add Fixes tag drivers/i3c/master.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index ab11e2d79aab..38ffc8713167 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -649,7 +649,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) return ret; } - i3c_bus_normaluse_lock(&master->bus); + i3c_bus_maintenance_lock(&master->bus); if (enable) ret = master->ops->enable_hotjoin(master); @@ -659,7 +659,7 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) if (!ret) master->hotjoin = enable; - i3c_bus_normaluse_unlock(&master->bus); + i3c_bus_maintenance_unlock(&master->bus); if ((enable && ret) || (!enable && !ret) || master->rpm_ibi_allowed) i3c_master_rpm_put(master); -- 2.51.0 From adrian.hunter at intel.com Mon May 18 04:55:14 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:14 +0300 Subject: [PATCH V2 3/8] i3c: master: Consolidate Hot-Join DAA work in the core In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-4-adrian.hunter@intel.com> Three master drivers (dw-i3c-master, i3c-master-cdns, svc-i3c-master) each carry an essentially identical Hot-Join handler: a struct work_struct embedded in their private state, a work function that just calls i3c_master_do_daa() on the embedded i3c_master_controller, plus matching INIT_WORK()/cancel_work_sync() boilerplate in probe/remove (and shutdown for dw-i3c). The IBI/ISR paths then queue that work onto master->wq, which already lives in the core. Move this pattern into the I3C core: - Add struct work_struct hj_work to struct i3c_master_controller and initialise it in i3c_master_register() with a core-provided handler i3c_master_hj_work_fn() that performs i3c_master_do_daa(). - Cancel the work in i3c_master_unregister() so all controllers get correct teardown ordering against the workqueue for free. - Export i3c_master_queue_hotjoin() as the single entry point drivers call from their Hot-Join IBI handler. Convert the three existing users to the new API: drop their private hj_work fields, work functions, INIT_WORK() and cancel_work_sync() calls, and replace the queue_work(master->wq, &drv->hj_work) call sites with i3c_master_queue_hotjoin(&drv->base). The dw-i3c shutdown path still needs to flush pending Hot-Join work before tearing down the hardware, so it is updated to cancel master->base.hj_work directly. No functional change intended: the work is still queued on the same master->wq, runs the same i3c_master_do_daa(), and is cancelled at controller teardown. Future Hot-Join improvements now only need to be made in one place. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V2: Add Frank's Rev'd by drivers/i3c/master.c | 21 +++++++++++++++++++++ drivers/i3c/master/dw-i3c-master.c | 15 ++------------- drivers/i3c/master/dw-i3c-master.h | 2 -- drivers/i3c/master/i3c-master-cdns.c | 14 +------------- drivers/i3c/master/svc-i3c-master.c | 14 +------------- include/linux/i3c/master.h | 4 ++++ 6 files changed, 29 insertions(+), 41 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 38ffc8713167..cdb5cb2aa65d 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -633,6 +633,13 @@ static ssize_t i2c_scl_frequency_show(struct device *dev, } static DEVICE_ATTR_RO(i2c_scl_frequency); +static void i3c_master_hj_work_fn(struct work_struct *work) +{ + struct i3c_master_controller *master = container_of(work, typeof(*master), hj_work); + + i3c_master_do_daa(master); +} + static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) { int ret; @@ -711,6 +718,18 @@ int i3c_master_disable_hotjoin(struct i3c_master_controller *master) } EXPORT_SYMBOL_GPL(i3c_master_disable_hotjoin); +/** + * i3c_master_queue_hotjoin - Queue DAA processing after a Hot-Join event + * @master: I3C master object + * + * Queue the hot-join worker on the master's workqueue. + */ +void i3c_master_queue_hotjoin(struct i3c_master_controller *master) +{ + queue_work(master->wq, &master->hj_work); +} +EXPORT_SYMBOL_GPL(i3c_master_queue_hotjoin); + static ssize_t hotjoin_show(struct device *dev, struct device_attribute *da, char *buf) { struct i3c_bus *i3cbus = dev_to_i3cbus(dev); @@ -3084,6 +3103,7 @@ int i3c_master_register(struct i3c_master_controller *master, ret = -ENOMEM; goto err_put_dev; } + INIT_WORK(&master->hj_work, i3c_master_hj_work_fn); ret = i3c_master_bus_init(master); if (ret) @@ -3146,6 +3166,7 @@ EXPORT_SYMBOL_GPL(i3c_master_register); void i3c_master_unregister(struct i3c_master_controller *master) { i3c_bus_notify(&master->bus, I3C_NOTIFY_BUS_REMOVE); + cancel_work_sync(&master->hj_work); if (master->ops->set_dev_nack_retry) device_remove_file(&master->dev, &dev_attr_dev_nack_retry_count); diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index 655693a2187e..eb9a13a73684 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1445,7 +1445,7 @@ static void dw_i3c_master_irq_handle_ibis(struct dw_i3c_master *master) if (IBI_TYPE_SIRQ(reg)) { dw_i3c_master_handle_ibi_sir(master, reg); } else if (IBI_TYPE_HJ(reg)) { - queue_work(master->base.wq, &master->hj_work); + i3c_master_queue_hotjoin(&master->base); } else { len = IBI_QUEUE_STATUS_DATA_LEN(reg); dev_info(&master->base.dev, @@ -1554,14 +1554,6 @@ 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_hj_work(struct work_struct *work) -{ - struct dw_i3c_master *master = - container_of(work, typeof(*master), hj_work); - - i3c_master_do_daa(&master->base); -} - int dw_i3c_common_probe(struct dw_i3c_master *master, struct platform_device *pdev) { @@ -1636,8 +1628,6 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, if (master->quirks & DW_I3C_DISABLE_RUNTIME_PM_QUIRK) pm_runtime_get_noresume(&pdev->dev); - INIT_WORK(&master->hj_work, dw_i3c_hj_work); - device_set_of_node_from_dev(&master->base.i2c.dev, &pdev->dev); ret = i3c_master_register(&master->base, &pdev->dev, &dw_mipi_i3c_ops, false); @@ -1659,7 +1649,6 @@ EXPORT_SYMBOL_GPL(dw_i3c_common_probe); void dw_i3c_common_remove(struct dw_i3c_master *master) { - cancel_work_sync(&master->hj_work); i3c_master_unregister(&master->base); /* Balance pm_runtime_get_noresume() from probe() */ @@ -1804,7 +1793,7 @@ static void dw_i3c_shutdown(struct platform_device *pdev) return; } - cancel_work_sync(&master->hj_work); + cancel_work_sync(&master->base.hj_work); /* Disable interrupts */ writel((u32)~INTR_ALL, master->regs + INTR_STATUS_EN); diff --git a/drivers/i3c/master/dw-i3c-master.h b/drivers/i3c/master/dw-i3c-master.h index c5cb695c16ab..2f029bd36232 100644 --- a/drivers/i3c/master/dw-i3c-master.h +++ b/drivers/i3c/master/dw-i3c-master.h @@ -68,8 +68,6 @@ struct dw_i3c_master { /* platform-specific data */ const struct dw_i3c_platform_ops *platform_ops; - - struct work_struct hj_work; }; struct dw_i3c_platform_ops { diff --git a/drivers/i3c/master/i3c-master-cdns.c b/drivers/i3c/master/i3c-master-cdns.c index 5cfec6761494..6d221596ea35 100644 --- a/drivers/i3c/master/i3c-master-cdns.c +++ b/drivers/i3c/master/i3c-master-cdns.c @@ -398,7 +398,6 @@ struct cdns_i3c_data { }; struct cdns_i3c_master { - struct work_struct hj_work; struct i3c_master_controller base; u32 free_rr_slots; unsigned int maxdevs; @@ -1357,7 +1356,7 @@ static void cnds_i3c_master_demux_ibis(struct cdns_i3c_master *master) case IBIR_TYPE_HJ: WARN_ON(IBIR_XFER_BYTES(ibir) || (ibir & IBIR_ERROR)); - queue_work(master->base.wq, &master->hj_work); + i3c_master_queue_hotjoin(&master->base); break; case IBIR_TYPE_MR: @@ -1528,15 +1527,6 @@ static const struct i3c_master_controller_ops cdns_i3c_master_ops = { .recycle_ibi_slot = cdns_i3c_master_recycle_ibi_slot, }; -static void cdns_i3c_master_hj(struct work_struct *work) -{ - struct cdns_i3c_master *master = container_of(work, - struct cdns_i3c_master, - hj_work); - - i3c_master_do_daa(&master->base); -} - static struct cdns_i3c_data cdns_i3c_devdata = { .thd_delay_ns = 10, }; @@ -1584,7 +1574,6 @@ static int cdns_i3c_master_probe(struct platform_device *pdev) spin_lock_init(&master->xferqueue.lock); INIT_LIST_HEAD(&master->xferqueue.list); - INIT_WORK(&master->hj_work, cdns_i3c_master_hj); writel(0xffffffff, master->regs + MST_IDR); writel(0xffffffff, master->regs + SLV_IDR); ret = devm_request_irq(&pdev->dev, irq, cdns_i3c_master_interrupt, 0, @@ -1627,7 +1616,6 @@ static void cdns_i3c_master_remove(struct platform_device *pdev) { struct cdns_i3c_master *master = platform_get_drvdata(pdev); - cancel_work_sync(&master->hj_work); i3c_master_unregister(&master->base); } diff --git a/drivers/i3c/master/svc-i3c-master.c b/drivers/i3c/master/svc-i3c-master.c index e2d99a3ac07d..62e5666798c8 100644 --- a/drivers/i3c/master/svc-i3c-master.c +++ b/drivers/i3c/master/svc-i3c-master.c @@ -208,7 +208,6 @@ struct svc_i3c_drvdata { * @free_slots: Bit array of available slots * @addrs: Array containing the dynamic addresses of each attached device * @descs: Array of descriptors, one per attached device - * @hj_work: Hot-join work * @irq: Main interrupt * @num_clks: I3C clock number * @fclk: Fast clock (bus) @@ -235,7 +234,6 @@ struct svc_i3c_master { u32 free_slots; u8 addrs[SVC_I3C_MAX_DEVS]; struct i3c_dev_desc *descs[SVC_I3C_MAX_DEVS]; - struct work_struct hj_work; int irq; int num_clks; struct clk *fclk; @@ -366,14 +364,6 @@ to_svc_i3c_master(struct i3c_master_controller *master) return container_of(master, struct svc_i3c_master, base); } -static void svc_i3c_master_hj_work(struct work_struct *work) -{ - struct svc_i3c_master *master; - - master = container_of(work, struct svc_i3c_master, hj_work); - i3c_master_do_daa(&master->base); -} - static struct i3c_dev_desc * svc_i3c_master_dev_from_addr(struct svc_i3c_master *master, unsigned int ibiaddr) @@ -651,7 +641,7 @@ static void svc_i3c_master_ibi_isr(struct svc_i3c_master *master) case SVC_I3C_MSTATUS_IBITYPE_HOT_JOIN: svc_i3c_master_emit_stop(master); if (is_events_enabled(master, SVC_I3C_EVENT_HOTJOIN)) - queue_work(master->base.wq, &master->hj_work); + i3c_master_queue_hotjoin(&master->base); break; case SVC_I3C_MSTATUS_IBITYPE_MASTER_REQUEST: svc_i3c_master_emit_stop(master); @@ -2022,7 +2012,6 @@ static int svc_i3c_master_probe(struct platform_device *pdev) if (ret) return dev_err_probe(dev, ret, "can't enable I3C clocks\n"); - INIT_WORK(&master->hj_work, svc_i3c_master_hj_work); mutex_init(&master->lock); ret = devm_request_irq(dev, master->irq, svc_i3c_master_irq_handler, @@ -2081,7 +2070,6 @@ static void svc_i3c_master_remove(struct platform_device *pdev) { struct svc_i3c_master *master = platform_get_drvdata(pdev); - cancel_work_sync(&master->hj_work); i3c_master_unregister(&master->base); pm_runtime_dont_use_autosuspend(&pdev->dev); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index e6112e5f6608..eb5c51608bd7 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -520,6 +520,8 @@ struct i3c_master_controller_ops { * in a thread context. Typical examples are Hot Join processing which * requires taking the bus lock in maintenance, which in turn, can only * be done from a sleep-able context + * @hj_work: work item used to run DAA after a Hot-Join event is detected. + * Queued to @wq by i3c_master_queue_hotjoin() * @dev_nack_retry_count: retry count when slave device nack * * A &struct i3c_master_controller has to be registered to the I3C subsystem @@ -543,6 +545,7 @@ struct i3c_master_controller { } boardinfo; struct i3c_bus bus; struct workqueue_struct *wq; + struct work_struct hj_work; unsigned int dev_nack_retry_count; }; @@ -623,6 +626,7 @@ int i3c_master_register(struct i3c_master_controller *master, void i3c_master_unregister(struct i3c_master_controller *master); int i3c_master_enable_hotjoin(struct i3c_master_controller *master); int i3c_master_disable_hotjoin(struct i3c_master_controller *master); +void i3c_master_queue_hotjoin(struct i3c_master_controller *master); /** * i3c_dev_get_master_data() - get master private data attached to an I3C -- 2.51.0 From adrian.hunter at intel.com Mon May 18 04:55:15 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:15 +0300 Subject: [PATCH V2 4/8] i3c: master: Ensure Hot-Join operations are stopped on shutdown In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-5-adrian.hunter@intel.com> System shutdown invokes each device's bus shutdown callback to quiesce hardware, but the I3C bus type does not currently implement one. As a result, on shutdown the controller's Hot-Join work and any in-flight i3c_master_do_daa() can keep running (or be newly triggered) while the rest of the system is being torn down. A similar window exists at i3c_master_unregister() time: cancel_work_sync() on hj_work prevents queued work from completing, but does not stop a fresh Hot-Join IBI from re-queueing the worker, nor a concurrent sysfs writer from toggling Hot-Join via i3c_set_hotjoin(). Introduce a single "shutting down" gate in the I3C core, set under the bus maintenance lock so it is observed by any in-progress DAA path before pending work is cancelled. Install an i3c_bus_type shutdown callback that engages this gate for master devices during system shutdown, and use the same gate in i3c_master_unregister() so both paths get identical guarantees. Once the gate is engaged, the Hot-Join worker, i3c_master_do_daa_ext() and i3c_set_hotjoin() all bail out cleanly, so Hot-Join IBIs that race with shutdown become no-ops, direct DAA callers see -ENODEV, and sysfs writers can no longer re-enable Hot-Join through ops->enable_hotjoin() while the controller is going away. No functional change for the steady-state runtime path; the new checks only take effect once the controller has been marked as shutting down. Note, this patch depends on patch "i3c: master: Consolidate Hot-Join DAA work in the core". Fixes: 3a379bbcea0af ("i3c: Add core I3C infrastructure") Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V2: Add dependency note to commit message Add Fixes tag Add Frank's Rev'd by drivers/i3c/master.c | 52 +++++++++++++++++++++++++++----------- include/linux/i3c/master.h | 2 ++ 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index cdb5cb2aa65d..a59c4b744b36 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -368,14 +368,6 @@ static void i3c_device_remove(struct device *dev) driver->remove(i3cdev); } -const struct bus_type i3c_bus_type = { - .name = "i3c", - .match = i3c_device_match, - .probe = i3c_device_probe, - .remove = i3c_device_remove, -}; -EXPORT_SYMBOL_GPL(i3c_bus_type); - static enum i3c_addr_slot_status i3c_bus_get_addr_slot_status_mask(struct i3c_bus *bus, u16 addr, u32 mask) { @@ -637,7 +629,8 @@ static void i3c_master_hj_work_fn(struct work_struct *work) { struct i3c_master_controller *master = container_of(work, typeof(*master), hj_work); - i3c_master_do_daa(master); + if (!master->shutting_down) + i3c_master_do_daa(master); } static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) @@ -658,7 +651,9 @@ static int i3c_set_hotjoin(struct i3c_master_controller *master, bool enable) i3c_bus_maintenance_lock(&master->bus); - if (enable) + if (master->shutting_down) + ret = -ENODEV; + else if (enable) ret = master->ops->enable_hotjoin(master); else ret = master->ops->disable_hotjoin(master); @@ -837,6 +832,30 @@ static const struct device_type i3c_masterdev_type = { .groups = i3c_masterdev_groups, }; +static void i3c_master_shutdown(struct i3c_master_controller *master) +{ + i3c_bus_maintenance_lock(&master->bus); + master->shutting_down = true; + i3c_bus_maintenance_unlock(&master->bus); + + cancel_work_sync(&master->hj_work); +} + +static void i3c_device_shutdown(struct device *dev) +{ + if (dev->type == &i3c_masterdev_type) + i3c_master_shutdown(dev_to_i3cmaster(dev)); +} + +const struct bus_type i3c_bus_type = { + .name = "i3c", + .match = i3c_device_match, + .probe = i3c_device_probe, + .remove = i3c_device_remove, + .shutdown = i3c_device_shutdown, +}; +EXPORT_SYMBOL_GPL(i3c_bus_type); + static int i3c_bus_set_mode(struct i3c_bus *i3cbus, enum i3c_bus_mode mode, unsigned long max_i2c_scl_rate) { @@ -1846,10 +1865,13 @@ int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) i3c_bus_maintenance_lock(&master->bus); - if (rstdaa) - rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); - - ret = master->ops->do_daa(master); + if (master->shutting_down) { + ret = -ENODEV; + } else { + if (rstdaa) + rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); + ret = master->ops->do_daa(master); + } i3c_bus_maintenance_unlock(&master->bus); @@ -3166,7 +3188,7 @@ EXPORT_SYMBOL_GPL(i3c_master_register); void i3c_master_unregister(struct i3c_master_controller *master) { i3c_bus_notify(&master->bus, I3C_NOTIFY_BUS_REMOVE); - cancel_work_sync(&master->hj_work); + i3c_master_shutdown(master); if (master->ops->set_dev_nack_retry) device_remove_file(&master->dev, &dev_attr_dev_nack_retry_count); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index eb5c51608bd7..77e63082b06e 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -511,6 +511,7 @@ struct i3c_master_controller_ops { * @hotjoin: true if the master support hotjoin * @rpm_allowed: true if Runtime PM allowed * @rpm_ibi_allowed: true if IBI and Hot-Join allowed while runtime suspended + * @shutting_down: set to true when master begins shutdown or unregister * @boardinfo.i3c: list of I3C boardinfo objects * @boardinfo.i2c: list of I2C boardinfo objects * @boardinfo: board-level information attached to devices connected on the bus @@ -539,6 +540,7 @@ struct i3c_master_controller { unsigned int hotjoin: 1; unsigned int rpm_allowed: 1; unsigned int rpm_ibi_allowed: 1; + bool shutting_down; struct { struct list_head i3c; struct list_head i2c; -- 2.51.0 From adrian.hunter at intel.com Mon May 18 04:55:16 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:16 +0300 Subject: [PATCH V2 5/8] i3c: dw: Drop redundant Hot-Join cancel_work_sync() in shutdown In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-6-adrian.hunter@intel.com> The I3C core now installs an i3c_bus_type shutdown callback that flushes master->hj_work (via i3c_master_shutdown()) before any driver's platform shutdown hook runs. The explicit cancel_work_sync() in dw_i3c_shutdown() is therefore redundant: by the time it executes, the Hot-Join worker has already been cancelled, and the shutting_down gate makes a new worker a no-op. Remove the now-unneeded call. No functional change. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V2: Add Frank's Rev'd by drivers/i3c/master/dw-i3c-master.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index eb9a13a73684..c7030d0cd8a6 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1793,8 +1793,6 @@ static void dw_i3c_shutdown(struct platform_device *pdev) return; } - cancel_work_sync(&master->base.hj_work); - /* Disable interrupts */ writel((u32)~INTR_ALL, master->regs + INTR_STATUS_EN); writel((u32)~INTR_ALL, master->regs + INTR_SIGNAL_EN); -- 2.51.0 From adrian.hunter at intel.com Mon May 18 04:55:17 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:17 +0300 Subject: [PATCH V2 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-7-adrian.hunter@intel.com> Master drivers may invoke i3c_master_do_daa_ext() during resume to re-run Dynamic Address Assignment. As well as assigning addresses to any newly arrived devices, this restores the dynamic address of devices that lost it across system suspend, so it has to run as part of the controller's resume path. A side effect of i3c_master_do_daa_ext() today is that it also registers any newly discovered I3C devices with the driver model inline, via i3c_master_register_new_i3c_devs(). Doing that from the resume path is problematic: a hot-join-capable device may join the bus during this same DAA, and registering it immediately would push driver model work (probing, sysfs, etc.) into the controller's resume context, where the rest of the system is not yet fully resumed and the controller driver is still partway through its own resume sequence. Decouple discovery from registration: add a reg_work work item to struct i3c_master_controller and have i3c_master_do_daa_ext() queue it on master->wq (the freezable workqueue) instead of calling i3c_master_register_new_i3c_devs() directly. The worker performs the registration only when the controller is not shutting_down, and is cancelled alongside hj_work in i3c_master_shutdown(). Because wq is freezable, any newly observed devices end up being registered after the system has finished resuming. i3c_master_register() also routes its initial post-bus-init registration through reg_work, using flush_work() to keep probe-time behavior synchronous. This keeps a single registration code path and ensures the worker is the only writer of desc->dev. Fixes: 3a379bbcea0af ("i3c: Add core I3C infrastructure") Signed-off-by: Adrian Hunter --- Changes in V2: Add comment about reg_work use Add Fixes tag drivers/i3c/master.c | 27 ++++++++++++++++++++------- include/linux/i3c/master.h | 6 ++++++ 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index a59c4b744b36..c9685379e868 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -839,6 +839,7 @@ static void i3c_master_shutdown(struct i3c_master_controller *master) i3c_bus_maintenance_unlock(&master->bus); cancel_work_sync(&master->hj_work); + cancel_work_sync(&master->reg_work); } static void i3c_device_shutdown(struct device *dev) @@ -1838,6 +1839,16 @@ i3c_master_register_new_i3c_devs(struct i3c_master_controller *master) } } +static void i3c_master_reg_work_fn(struct work_struct *work) +{ + struct i3c_master_controller *master = container_of(work, typeof(*master), reg_work); + + i3c_bus_normaluse_lock(&master->bus); + if (!master->shutting_down) + i3c_master_register_new_i3c_devs(master); + i3c_bus_normaluse_unlock(&master->bus); +} + /** * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) * @master: controller @@ -1878,9 +1889,7 @@ int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) if (ret) goto out; - i3c_bus_normaluse_lock(&master->bus); - i3c_master_register_new_i3c_devs(master); - i3c_bus_normaluse_unlock(&master->bus); + queue_work(master->wq, &master->reg_work); out: i3c_master_rpm_put(master); @@ -3126,6 +3135,7 @@ int i3c_master_register(struct i3c_master_controller *master, goto err_put_dev; } INIT_WORK(&master->hj_work, i3c_master_hj_work_fn); + INIT_WORK(&master->reg_work, i3c_master_reg_work_fn); ret = i3c_master_bus_init(master); if (ret) @@ -3151,12 +3161,15 @@ int i3c_master_register(struct i3c_master_controller *master, /* * We're done initializing the bus and the controller, we can now - * register I3C devices discovered during the initial DAA. + * register I3C devices discovered during the initial DAA. Device + * registration is done via reg_work because that keeps a single + * registration code path and ensures the worker is the only writer + * of desc->dev. Flush the work to preserve synchronous probe-time + * behavior. */ master->init_done = true; - i3c_bus_normaluse_lock(&master->bus); - i3c_master_register_new_i3c_devs(master); - i3c_bus_normaluse_unlock(&master->bus); + queue_work(master->wq, &master->reg_work); + flush_work(&master->reg_work); if (master->ops->set_dev_nack_retry) device_create_file(&master->dev, &dev_attr_dev_nack_retry_count); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 77e63082b06e..8cdd7be505d3 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -523,6 +523,11 @@ struct i3c_master_controller_ops { * be done from a sleep-able context * @hj_work: work item used to run DAA after a Hot-Join event is detected. * Queued to @wq by i3c_master_queue_hotjoin() + * @reg_work: work item used to register newly discovered I3C devices with + * the driver model. Queued to @wq by i3c_master_do_daa_ext() so + * that device registration is deferred out of the DAA caller's + * context (notably the resume path), and is skipped if the + * controller is shutting down * @dev_nack_retry_count: retry count when slave device nack * * A &struct i3c_master_controller has to be registered to the I3C subsystem @@ -548,6 +553,7 @@ struct i3c_master_controller { struct i3c_bus bus; struct workqueue_struct *wq; struct work_struct hj_work; + struct work_struct reg_work; unsigned int dev_nack_retry_count; }; -- 2.51.0 From adrian.hunter at intel.com Mon May 18 04:55:18 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:18 +0300 Subject: [PATCH V2 7/8] i3c: master: Export i3c_master_enec_disec_locked() In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-8-adrian.hunter@intel.com> The existing i3c_master_enec_locked() wrapper always treats a NACKed ENEC CCC as a failure (M2 error). However, broadcasting ENEC to enable Hot-Join is legitimately useful even when no I3C devices are currently present on the bus, in which case the broadcast will be NACKed and should not be reported as an error. The underlying helper i3c_master_enec_disec_locked() already accepts a suppress_m2 flag that lets callers ignore such NACKs. Expose it so that a subsequent patch enabling Hot-Join events can issue ENEC with M2 suppression. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V2: Add Frank's Rev'd by drivers/i3c/master.c | 27 ++++++++++++++++++++++++--- include/linux/i3c/master.h | 2 ++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index c9685379e868..f87bf0099d3c 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1121,9 +1121,29 @@ int i3c_master_entdaa_locked(struct i3c_master_controller *master) } EXPORT_SYMBOL_GPL(i3c_master_entdaa_locked); -static int i3c_master_enec_disec_locked(struct i3c_master_controller *master, - u8 addr, bool enable, u8 evts, - bool suppress_m2) +/** + * i3c_master_enec_disec_locked() - send an ENEC or DISEC CCC command + * @master: master used to send frames on the bus + * @addr: a valid I3C slave address or %I3C_BROADCAST_ADDR + * @enable: true to send ENEC, false to send DISEC + * @evts: events to enable or disable + * @suppress_m2: if true, treat an M2 (NACK) error from the CCC as success + * + * Send an ENEC or DISEC CCC command to enable or disable some or all events + * coming from a specific slave, or all devices if @addr is + * %I3C_BROADCAST_ADDR. + * + * When @suppress_m2 is true, a NACK of the broadcast (which can happen when + * no devices are present on the bus) is not reported as an error. This is + * useful for callers that want to configure event reporting unconditionally, + * regardless of whether any devices are currently on the bus. + * + * This function must be called with the bus lock held in write mode. + * + * Return: 0 in case of success, or a negative error code otherwise. + */ +int i3c_master_enec_disec_locked(struct i3c_master_controller *master, u8 addr, + bool enable, u8 evts, bool suppress_m2) { struct i3c_ccc_events *events; struct i3c_ccc_cmd_dest dest; @@ -1148,6 +1168,7 @@ static int i3c_master_enec_disec_locked(struct i3c_master_controller *master, return ret; } +EXPORT_SYMBOL_GPL(i3c_master_enec_disec_locked); /** * i3c_master_disec_locked() - send a DISEC CCC command diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 8cdd7be505d3..e2c831fb5354 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -607,6 +607,8 @@ int i3c_master_disec_locked(struct i3c_master_controller *master, u8 addr, u8 evts); int i3c_master_enec_locked(struct i3c_master_controller *master, u8 addr, u8 evts); +int i3c_master_enec_disec_locked(struct i3c_master_controller *master, u8 addr, + bool enable, u8 evts, bool suppress_m2); int i3c_master_entdaa_locked(struct i3c_master_controller *master); int i3c_master_defslvs_locked(struct i3c_master_controller *master); -- 2.51.0 From adrian.hunter at intel.com Mon May 18 04:55:19 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 14:55:19 +0300 Subject: [PATCH V2 8/8] i3c: mipi-i3c-hci: Add Hot-Join support In-Reply-To: <20260518115520.98335-1-adrian.hunter@intel.com> References: <20260518115520.98335-1-adrian.hunter@intel.com> Message-ID: <20260518115520.98335-9-adrian.hunter@intel.com> Wire the MIPI I3C HCI driver into the I3C core Hot-Join framework to allow targets to dynamically join the bus after initial DAA. HCI hardware ACKs or NACKs Hot-Join requests based on HC_CONTROL.HOT_JOIN_CTRL. This was previously left in the NACK-and-DISEC state, effectively preventing Hot-Join. Implement the ->enable_hotjoin() and ->disable_hotjoin() master operations so the core and user space can control this policy at runtime. Also issue broadcast ENEC HJ when enabling Hot-Join. This is required because the controller may have previously DISEC'ed the Hot-Join event, causing targets that were NACKed once to never retry. Acknowledged Hot-Join requests are delivered as IBIs on the reserved address 0x02. Update both the DMA and PIO IBI paths to recognise this address and forward the event to i3c_master_queue_hotjoin(). To make Hot-Join usable by default, enable it once after the initial DAA. This is gated by rpm_ibi_allowed, since otherwise keeping Hot-Join enabled prevents runtime suspend. A new hj_init_done flag ensures this one-time enablement is not repeated on subsequent DAAs. Signed-off-by: Adrian Hunter Reviewed-by: Frank Li --- Changes in V2: Add Frank's Rev'd by drivers/i3c/master/mipi-i3c-hci/core.c | 50 ++++++++++++++++++++++++-- drivers/i3c/master/mipi-i3c-hci/dma.c | 5 +++ drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + drivers/i3c/master/mipi-i3c-hci/pio.c | 5 +++ 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index c6edbbedfdd7..53797841b63f 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -392,11 +392,52 @@ static int i3c_hci_send_ccc_cmd(struct i3c_master_controller *m, return ret; } +static int i3c_hci_enable_hotjoin(struct i3c_master_controller *m) +{ + struct i3c_hci *hci = to_i3c_hci(m); + int ret; + + reg_clear(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); + + /* + * Broadcast Hot_join enable, so that an I3C device that has previously + * had its Hot-Join request NACK'ed knows to try again. + */ + ret = i3c_master_enec_disec_locked(m, I3C_BROADCAST_ADDR, true, I3C_CCC_EVENT_HJ, true); + if (ret) { + reg_set(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); + dev_err(&hci->master.dev, "Hot-Join ENEC CCC failed\n"); + } + + return ret; +} + +static int i3c_hci_disable_hotjoin(struct i3c_master_controller *m) +{ + struct i3c_hci *hci = to_i3c_hci(m); + + reg_set(HC_CONTROL, HC_CONTROL_HOT_JOIN_CTRL); + return 0; +} + static int i3c_hci_daa(struct i3c_master_controller *m) { struct i3c_hci *hci = to_i3c_hci(m); + int ret; - return hci->cmd->perform_daa(hci); + ret = hci->cmd->perform_daa(hci); + + if (!hci->hj_init_done) { + hci->hj_init_done = true; + /* + * Enable Hot-Join by default after initial DAA if it does not + * prevent runtime suspend. + */ + if (m->rpm_ibi_allowed && !ret) + m->hotjoin = !i3c_hci_enable_hotjoin(m); + } + + return ret; } static int i3c_hci_i3c_xfers(struct i3c_dev_desc *dev, @@ -652,6 +693,8 @@ static const struct i3c_master_controller_ops i3c_hci_ops = { .enable_ibi = i3c_hci_enable_ibi, .disable_ibi = i3c_hci_disable_ibi, .recycle_ibi_slot = i3c_hci_recycle_ibi_slot, + .enable_hotjoin = i3c_hci_enable_hotjoin, + .disable_hotjoin = i3c_hci_disable_hotjoin, }; static irqreturn_t i3c_hci_irq_handler(int irq, void *dev_id) @@ -833,8 +876,9 @@ static int i3c_hci_do_reset_and_restore(struct i3c_hci *hci) scoped_guard(spinlock_irqsave, &hci->lock) hci->irq_inactive = false; - /* Enable bus with Hot-Join disabled */ - reg_set(HC_CONTROL, HC_CONTROL_BUS_ENABLE | HC_CONTROL_HOT_JOIN_CTRL); + /* Enable bus, restoring hot-join state */ + reg_set(HC_CONTROL, + HC_CONTROL_BUS_ENABLE | (hci->master.hotjoin ? 0 : HC_CONTROL_HOT_JOIN_CTRL)); return 0; } diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 5c6ae2055618..87622d6f817e 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -960,6 +960,11 @@ static void hci_dma_process_ibi(struct i3c_hci *hci, struct hci_rh_data *rh) } /* determine who this is for */ + if (ibi_addr == I3C_HOT_JOIN_ADDR) { + i3c_master_queue_hotjoin(&hci->master); + goto done; + } + dev = i3c_hci_addr_to_dev(hci, ibi_addr); if (!dev) { dev_err(&hci->master.dev, diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 30297823ca85..41d31a53abd3 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -57,6 +57,7 @@ struct i3c_hci { bool irq_inactive; bool enqueue_blocked; bool recovery_needed; + bool hj_init_done; wait_queue_head_t enqueue_wait_queue; u32 caps; unsigned int quirks; diff --git a/drivers/i3c/master/mipi-i3c-hci/pio.c b/drivers/i3c/master/mipi-i3c-hci/pio.c index 6b8cc5f2b4d2..b5ae1cfaa9e0 100644 --- a/drivers/i3c/master/mipi-i3c-hci/pio.c +++ b/drivers/i3c/master/mipi-i3c-hci/pio.c @@ -862,6 +862,11 @@ static bool hci_pio_prep_new_ibi(struct i3c_hci *hci, struct hci_pio_data *pio) ibi->seg_len = FIELD_GET(IBI_DATA_LENGTH, ibi_status); ibi->seg_cnt = ibi->seg_len; + if (ibi->addr == I3C_HOT_JOIN_ADDR) { + i3c_master_queue_hotjoin(&hci->master); + return true; + } + dev = i3c_hci_addr_to_dev(hci, ibi->addr); if (!dev) { dev_err(&hci->master.dev, -- 2.51.0 From adrian.hunter at intel.com Mon May 18 05:04:22 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Mon, 18 May 2026 15:04:22 +0300 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> Message-ID: <316208bc-f430-4f04-b81b-6fbabce3adcc@intel.com> On 13/05/2026 13:20, David Nystr?m wrote: > > > On Wed, 13 May 2026, Adrian Hunter wrote: > >> On 12/05/2026 19:39, Frank Li wrote: >>> On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: >>>> Master drivers may invoke i3c_master_do_daa_ext() during resume to >>>> re-run Dynamic Address Assignment.? As well as assigning addresses to >>>> any newly arrived devices, this restores the dynamic address of devices >>>> that lost it across system suspend, so it has to run as part of the >>>> controller's resume path. >>>> >>>> A side effect of i3c_master_do_daa_ext() today is that it also >>>> registers any newly discovered I3C devices with the driver model >>>> inline, via i3c_master_register_new_i3c_devs().? Doing that from the >>>> resume path is problematic: a hot-join-capable device may join the bus >>>> during this same DAA, and registering it immediately would push driver >>>> model work (probing, sysfs, etc.) into the controller's resume context, >>>> where the rest of the system is not yet fully resumed and the >>>> controller driver is still partway through its own resume sequence. >>>> >>>> Decouple discovery from registration: add a reg_work work item to >>>> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it >>>> on master->wq (the freezable workqueue) instead of calling >>>> i3c_master_register_new_i3c_devs() directly.? The worker performs the >>>> registration only when the controller is not shutting_down, and is >>>> cancelled alongside hj_work in i3c_master_shutdown().? Because wq is >>>> freezable, any newly observed devices end up being registered after >>>> the system has finished resuming. >>>> >>>> i3c_master_register() also routes its initial post-bus-init registration >>>> through reg_work, using flush_work() to keep probe-time behavior >>>> synchronous.? This keeps a single registration code path and ensures the >>>> worker is the only writer of desc->dev. >>> >>> why not direct use hj_work? >> >> i3c_master_register_new_i3c_devs() use of desc->dev is racy, so >> i3c_master_register_new_i3c_devs() must not be allowed to race >> with itself.? Having it only ever run via reg_work achieves that. > > This race was introduced in > 3a379bbcea0a ("i3c: Add core I3C infrastructure") > > But since this path was exposed via sysfs in latest 7.1-rc via: > 8ea0b60bc00d ("i3c: master: Add sysfs option to rescan bus via entdaa") > > I would argue that we should revert the sysfs addition from 7.1-rc until this fix is in place. I guess it is up to you. A couple of options: - submit a revert patch, or - wait until 7.2-rc1 and backport fixes to 7.1 From jszhang at kernel.org Mon May 18 22:51:01 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Tue, 19 May 2026 13:51:01 +0800 Subject: [PATCH v3 0/4] i3c: dw: Add apb reset support Message-ID: <20260519055105.13079-1-jszhang@kernel.org> Add support of apb reset which is to reset the APB interface. The first patch is to document the exisiting reset dt-binding. 2nd patch is to add apb reset dt-binding. The last patch is to add apb reset support. Since v2: - remove "_rst" suffix Since v1: - add dt-binding Jisheng Zhang (4): i3c: dw: Remove core reset "_rst" suffix dt-bindings: i3c: dw: Describe core reset dt-bindings: i3c: dw: Add apb reset i3c: dw: Add apb reset support .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 10 ++++++++++ drivers/i3c/master/dw-i3c-master.c | 9 ++++++++- drivers/i3c/master/dw-i3c-master.h | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) -- 2.53.0 From jszhang at kernel.org Mon May 18 22:51:03 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Tue, 19 May 2026 13:51:03 +0800 Subject: [PATCH v3 2/4] dt-bindings: i3c: dw: Describe core reset In-Reply-To: <20260519055105.13079-1-jszhang@kernel.org> References: <20260519055105.13079-1-jszhang@kernel.org> Message-ID: <20260519055105.13079-3-jszhang@kernel.org> The core reset support has been in the code from day1, but the dt-binding doesn't exist. Add dt-binding to describe reset property. Signed-off-by: Jisheng Zhang --- .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index e803457d3f55..519797c6b4fe 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -35,6 +35,14 @@ properties: - const: core - const: apb + resets: + items: + - description: Reset signal + + reset-names: + items: + - const: core + interrupts: maxItems: 1 -- 2.53.0 From jszhang at kernel.org Mon May 18 22:51:02 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Tue, 19 May 2026 13:51:02 +0800 Subject: [PATCH v3 1/4] i3c: dw: Remove core reset "_rst" suffix In-Reply-To: <20260519055105.13079-1-jszhang@kernel.org> References: <20260519055105.13079-1-jszhang@kernel.org> Message-ID: <20260519055105.13079-2-jszhang@kernel.org> It's redundant. This suffix has been in the code from day1, fortunately there's no such dt property usage in all dw i3c users after grepping all dts files, so we can remove it. Signed-off-by: Jisheng Zhang --- drivers/i3c/master/dw-i3c-master.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index 655693a2187e..c4a848cc978a 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1587,7 +1587,7 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, return PTR_ERR(master->pclk); master->core_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, - "core_rst"); + "core"); if (IS_ERR(master->core_rst)) return PTR_ERR(master->core_rst); -- 2.53.0 From jszhang at kernel.org Mon May 18 22:51:04 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Tue, 19 May 2026 13:51:04 +0800 Subject: [PATCH v3 3/4] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <20260519055105.13079-1-jszhang@kernel.org> References: <20260519055105.13079-1-jszhang@kernel.org> Message-ID: <20260519055105.13079-4-jszhang@kernel.org> Add dt-binding for support of apb reset which is to reset the APB interface. Signed-off-by: Jisheng Zhang --- Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index 519797c6b4fe..12845206772f 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -38,10 +38,12 @@ properties: resets: items: - description: Reset signal + - description: APB interface reset signal reset-names: items: - const: core + - const: apb interrupts: maxItems: 1 -- 2.53.0 From jszhang at kernel.org Mon May 18 22:51:05 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Tue, 19 May 2026 13:51:05 +0800 Subject: [PATCH v3 4/4] i3c: dw: Add apb reset support In-Reply-To: <20260519055105.13079-1-jszhang@kernel.org> References: <20260519055105.13079-1-jszhang@kernel.org> Message-ID: <20260519055105.13079-5-jszhang@kernel.org> Add support of apb reset which is to reset the APB interface. Signed-off-by: Jisheng Zhang --- drivers/i3c/master/dw-i3c-master.c | 7 +++++++ drivers/i3c/master/dw-i3c-master.h | 1 + 2 files changed, 8 insertions(+) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index c4a848cc978a..ae7aa6880e8f 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1591,6 +1591,11 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, if (IS_ERR(master->core_rst)) return PTR_ERR(master->core_rst); + master->apb_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, + "apb"); + if (IS_ERR(master->apb_rst)) + return PTR_ERR(master->apb_rst); + spin_lock_init(&master->xferqueue.lock); INIT_LIST_HEAD(&master->xferqueue.list); @@ -1765,6 +1770,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->apb_rst); dw_i3c_master_disable_clks(master); pinctrl_pm_select_sleep_state(dev); return 0; @@ -1777,6 +1783,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->apb_rst); 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 c5cb695c16ab..a4ba60043288 100644 --- a/drivers/i3c/master/dw-i3c-master.h +++ b/drivers/i3c/master/dw-i3c-master.h @@ -37,6 +37,7 @@ struct dw_i3c_master { struct dw_i3c_master_caps caps; void __iomem *regs; struct reset_control *core_rst; + struct reset_control *apb_rst; struct clk *core_clk; struct clk *pclk; char version[5]; -- 2.53.0 From krzk at kernel.org Wed May 20 00:13:58 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Wed, 20 May 2026 09:13:58 +0200 Subject: [PATCH v3 1/4] i3c: dw: Remove core reset "_rst" suffix In-Reply-To: <20260519055105.13079-2-jszhang@kernel.org> References: <20260519055105.13079-1-jszhang@kernel.org> <20260519055105.13079-2-jszhang@kernel.org> Message-ID: <20260520-scrupulous-notorious-ibis-ab6cbc@quoll> On Tue, May 19, 2026 at 01:51:02PM +0800, Jisheng Zhang wrote: > It's redundant. This suffix has been in the code from day1, fortunately > there's no such dt property usage in all dw i3c users after grepping all > dts files, so we can remove it. Hm, how could you grep all 3rd party / out of tree users of this? > > Signed-off-by: Jisheng Zhang > --- > drivers/i3c/master/dw-i3c-master.c | 2 +- > 1 file changed, 1 insertion(+), 1 deletion(-) > > diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c > index 655693a2187e..c4a848cc978a 100644 > --- a/drivers/i3c/master/dw-i3c-master.c > +++ b/drivers/i3c/master/dw-i3c-master.c > @@ -1587,7 +1587,7 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, > return PTR_ERR(master->pclk); > > master->core_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, > - "core_rst"); > + "core"); ABI impact for something released since 2018. Cleanup of name is not really worth affecting users. core_rst is not the best name but it is not incorrect, either. Best regards, Krzysztof From krzk at kernel.org Wed May 20 00:14:44 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Wed, 20 May 2026 09:14:44 +0200 Subject: [PATCH v3 2/4] dt-bindings: i3c: dw: Describe core reset In-Reply-To: <20260519055105.13079-3-jszhang@kernel.org> References: <20260519055105.13079-1-jszhang@kernel.org> <20260519055105.13079-3-jszhang@kernel.org> Message-ID: <20260520-cocky-thick-wren-900aa0@quoll> On Tue, May 19, 2026 at 01:51:03PM +0800, Jisheng Zhang wrote: > The core reset support has been in the code from day1, but the Well, no. There was no "core" reset from day1, so your entire explanation makes no sense now. Previous version was correct. Best regards, Krzysztof From jszhang at kernel.org Wed May 20 04:43:14 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Wed, 20 May 2026 19:43:14 +0800 Subject: [PATCH v3 1/4] i3c: dw: Remove core reset "_rst" suffix In-Reply-To: <20260520-scrupulous-notorious-ibis-ab6cbc@quoll> References: <20260519055105.13079-1-jszhang@kernel.org> <20260519055105.13079-2-jszhang@kernel.org> <20260520-scrupulous-notorious-ibis-ab6cbc@quoll> Message-ID: On Wed, May 20, 2026 at 09:13:58AM +0200, Krzysztof Kozlowski wrote: > On Tue, May 19, 2026 at 01:51:02PM +0800, Jisheng Zhang wrote: > > It's redundant. This suffix has been in the code from day1, fortunately > > there's no such dt property usage in all dw i3c users after grepping all > > dts files, so we can remove it. > > Hm, how could you grep all 3rd party / out of tree users of this? > > > > > Signed-off-by: Jisheng Zhang > > --- > > drivers/i3c/master/dw-i3c-master.c | 2 +- > > 1 file changed, 1 insertion(+), 1 deletion(-) > > > > diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c > > index 655693a2187e..c4a848cc978a 100644 > > --- a/drivers/i3c/master/dw-i3c-master.c > > +++ b/drivers/i3c/master/dw-i3c-master.c > > @@ -1587,7 +1587,7 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, > > return PTR_ERR(master->pclk); > > > > master->core_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, > > - "core_rst"); > > + "core"); > > ABI impact for something released since 2018. Cleanup of name is not > really worth affecting users. core_rst is not the best name but it is > not incorrect, either. > Hmm make sense. Two questions: if the ABI is introduced but never used by any intree users, is modifying the ABI taken as "ABI breakage"? if the ABI is only used by outtree users, can we modify the ABI? Thanks From jszhang at kernel.org Wed May 20 04:50:44 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Wed, 20 May 2026 19:50:44 +0800 Subject: [PATCH v3 2/4] dt-bindings: i3c: dw: Describe core reset In-Reply-To: <20260520-cocky-thick-wren-900aa0@quoll> References: <20260519055105.13079-1-jszhang@kernel.org> <20260519055105.13079-3-jszhang@kernel.org> <20260520-cocky-thick-wren-900aa0@quoll> Message-ID: On Wed, May 20, 2026 at 09:14:44AM +0200, Krzysztof Kozlowski wrote: > On Tue, May 19, 2026 at 01:51:03PM +0800, Jisheng Zhang wrote: > > The core reset support has been in the code from day1, but the > > Well, no. There was no "core" reset from day1, so your entire aha, by "core reset" I mean the reset signal for i3c core. Not the reset name. But I will update the commit msg. > explanation makes no sense now. Previous version was correct. > OK, so I will send v4. From Frank.li at nxp.com Thu May 21 11:32:40 2026 From: Frank.li at nxp.com (Frank Li) Date: Thu, 21 May 2026 14:32:40 -0400 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: <417993a7-4a4f-4ba5-a815-aab63ed03a3c@intel.com> References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> <417993a7-4a4f-4ba5-a815-aab63ed03a3c@intel.com> Message-ID: On Fri, May 15, 2026 at 07:42:20PM +0300, Adrian Hunter wrote: > On 13/05/2026 22:03, Frank Li wrote: > > On Wed, May 13, 2026 at 08:45:55AM +0300, Adrian Hunter wrote: > >> On 12/05/2026 19:39, Frank Li wrote: > >>> On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: > >>>> Master drivers may invoke i3c_master_do_daa_ext() during resume to > >>>> re-run Dynamic Address Assignment. As well as assigning addresses to > >>>> any newly arrived devices, this restores the dynamic address of devices > >>>> that lost it across system suspend, so it has to run as part of the > >>>> controller's resume path. > >>>> > >>>> A side effect of i3c_master_do_daa_ext() today is that it also > >>>> registers any newly discovered I3C devices with the driver model > >>>> inline, via i3c_master_register_new_i3c_devs(). Doing that from the > >>>> resume path is problematic: a hot-join-capable device may join the bus > >>>> during this same DAA, and registering it immediately would push driver > >>>> model work (probing, sysfs, etc.) into the controller's resume context, > >>>> where the rest of the system is not yet fully resumed and the > >>>> controller driver is still partway through its own resume sequence. > >>>> > >>>> Decouple discovery from registration: add a reg_work work item to > >>>> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it > >>>> on master->wq (the freezable workqueue) instead of calling > >>>> i3c_master_register_new_i3c_devs() directly. The worker performs the > >>>> registration only when the controller is not shutting_down, and is > >>>> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is > >>>> freezable, any newly observed devices end up being registered after > >>>> the system has finished resuming. > >>>> > >>>> i3c_master_register() also routes its initial post-bus-init registration > >>>> through reg_work, using flush_work() to keep probe-time behavior > >>>> synchronous. This keeps a single registration code path and ensures the > >>>> worker is the only writer of desc->dev. > >>> > >>> why not direct use hj_work? > >> > >> i3c_master_register_new_i3c_devs() use of desc->dev is racy, so > >> i3c_master_register_new_i3c_devs() must not be allowed to race > >> with itself. Having it only ever run via reg_work achieves that. > > > > Sorry, I have not understand these, Can provide some detail? > > >From i3c_master_register_new_i3c_devs(): > > i3c_bus_for_each_i3cdev(&master->bus, desc) { > if (desc->dev || !desc->info.dyn_addr || desc == master->this) > continue; > > desc->dev = kzalloc_obj(*desc->dev); > ... > ret = device_register(&desc->dev->dev); > > This is done under the shared i3c_bus_normaluse_lock(), so there can i3c_bus_normaluse_lock() may is wrong, suppose it should be i3c_bus_maintenance_lock(), register new devices change i3c bus's hierarchical structure. Frank > be 2 or more instances of i3c_master_register_new_i3c_devs() running > at the same time. They might all see desc->dev is NULL and then all > of them try to initialize and register a dev for the same I3C device. > From adrian.hunter at intel.com Thu May 21 21:52:17 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Fri, 22 May 2026 07:52:17 +0300 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> <417993a7-4a4f-4ba5-a815-aab63ed03a3c@intel.com> Message-ID: On 21/05/2026 21:32, Frank Li wrote: > On Fri, May 15, 2026 at 07:42:20PM +0300, Adrian Hunter wrote: >> On 13/05/2026 22:03, Frank Li wrote: >>> On Wed, May 13, 2026 at 08:45:55AM +0300, Adrian Hunter wrote: >>>> On 12/05/2026 19:39, Frank Li wrote: >>>>> On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: >>>>>> Master drivers may invoke i3c_master_do_daa_ext() during resume to >>>>>> re-run Dynamic Address Assignment. As well as assigning addresses to >>>>>> any newly arrived devices, this restores the dynamic address of devices >>>>>> that lost it across system suspend, so it has to run as part of the >>>>>> controller's resume path. >>>>>> >>>>>> A side effect of i3c_master_do_daa_ext() today is that it also >>>>>> registers any newly discovered I3C devices with the driver model >>>>>> inline, via i3c_master_register_new_i3c_devs(). Doing that from the >>>>>> resume path is problematic: a hot-join-capable device may join the bus >>>>>> during this same DAA, and registering it immediately would push driver >>>>>> model work (probing, sysfs, etc.) into the controller's resume context, >>>>>> where the rest of the system is not yet fully resumed and the >>>>>> controller driver is still partway through its own resume sequence. >>>>>> >>>>>> Decouple discovery from registration: add a reg_work work item to >>>>>> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it >>>>>> on master->wq (the freezable workqueue) instead of calling >>>>>> i3c_master_register_new_i3c_devs() directly. The worker performs the >>>>>> registration only when the controller is not shutting_down, and is >>>>>> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is >>>>>> freezable, any newly observed devices end up being registered after >>>>>> the system has finished resuming. >>>>>> >>>>>> i3c_master_register() also routes its initial post-bus-init registration >>>>>> through reg_work, using flush_work() to keep probe-time behavior >>>>>> synchronous. This keeps a single registration code path and ensures the >>>>>> worker is the only writer of desc->dev. >>>>> >>>>> why not direct use hj_work? >>>> >>>> i3c_master_register_new_i3c_devs() use of desc->dev is racy, so >>>> i3c_master_register_new_i3c_devs() must not be allowed to race >>>> with itself. Having it only ever run via reg_work achieves that. >>> >>> Sorry, I have not understand these, Can provide some detail? >> >> >From i3c_master_register_new_i3c_devs(): >> >> i3c_bus_for_each_i3cdev(&master->bus, desc) { >> if (desc->dev || !desc->info.dyn_addr || desc == master->this) >> continue; >> >> desc->dev = kzalloc_obj(*desc->dev); >> ... >> ret = device_register(&desc->dev->dev); >> >> This is done under the shared i3c_bus_normaluse_lock(), so there can > > i3c_bus_normaluse_lock() may is wrong, suppose it should be > i3c_bus_maintenance_lock(), register new devices change i3c bus's > hierarchical structure. If device_register() probes the device and the probe tries to access the device, won't it deadlock if i3c_bus_maintenance_lock() is held. > > Frank > >> be 2 or more instances of i3c_master_register_new_i3c_devs() running >> at the same time. They might all see desc->dev is NULL and then all >> of them try to initialize and register a dev for the same I3C device. >> From claudiu.beznea at kernel.org Fri May 22 03:17:58 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:17:58 +0300 Subject: [PATCH 00/17] i3c: renesas: Suspend to RAM with power loss and runtime PM Message-ID: <20260522101815.1722909-1-claudiu.beznea@kernel.org> From: Claudiu Beznea Hi, This series adjusts the suspend to RAM code to handle cases where power to the connected devices is lost during suspend to RAM. The fixes included in this series are required for that support. Along with suspend to RAM support, runtime PM support is also added. Cleanup patches were included to prepare for clean runtime PM support. Thank you, Claudiu Claudiu Beznea (17): i3c: renesas: Check that the transfer is valid before accessing it i3c: renesas: Use the divider 128 i3c: renesas: Restore STDBR and EXTBR registers on resume i3c: renesas: Follow the reset deassert order used in probe i3c: renesas: Fix re-attach i3c: renesas: Reset the controller on resume i3c: renesas: Perform Dynamic Address Assignment on resume i3c: renesas: Clean DATBAS register on detach i3c: renesas: Use reset_control_bulk_{assert, deassert}() i3c: renesas: Return immediately if there is nothing to transfer i3c: renesas: Follow a unified pattern for transfer and command initialization i3c: renesas: Drop the explicit memset() call i3c: renesas: Update HW registers after SW computations are done i3c: renesas: Organize structures to avoid unnecessary padding i3c: renesas: Use the "dev_name:irq_name" format for the interrupt name i3c: renesas: Drop unnecessary tab i3c: renesas: Add runtime PM support drivers/i3c/master/renesas-i3c.c | 378 ++++++++++++++++++++++--------- 1 file changed, 273 insertions(+), 105 deletions(-) -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:17:59 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:17:59 +0300 Subject: [PATCH 01/17] i3c: renesas: Check that the transfer is valid before accessing it In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-2-claudiu.beznea@kernel.org> From: Claudiu Beznea The Renesas I3C driver uses an asynchronous model to transfer data. It prepares a struct renesas_i3c_xfer, enqueues it, and waits for completion. The interrupt handler dequeues the transfer, updates/uses it, and signals the waiting thread. If the completion times out, the waiting thread dequeues the transfer and free it. If an interrupt fires after that, the handler may access freed memory, leading to crashes. Check that the transfer is still valid before accessing it in the interrupt handler. Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index f39c449922ca..36e3ccbe66b0 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -1014,6 +1014,9 @@ static irqreturn_t renesas_i3c_tx_isr(int irq, void *data) scoped_guard(spinlock, &i3c->xferqueue.lock) { xfer = i3c->xferqueue.cur; + if (!xfer) + return IRQ_HANDLED; + cmd = xfer->cmds; if (xfer->is_i2c_xfer) { @@ -1054,6 +1057,9 @@ static irqreturn_t renesas_i3c_resp_isr(int irq, void *data) scoped_guard(spinlock, &i3c->xferqueue.lock) { xfer = i3c->xferqueue.cur; + if (!xfer) + return IRQ_HANDLED; + cmd = xfer->cmds; /* Clear the Respone Queue Full status flag*/ @@ -1138,6 +1144,9 @@ static irqreturn_t renesas_i3c_tend_isr(int irq, void *data) scoped_guard(spinlock, &i3c->xferqueue.lock) { xfer = i3c->xferqueue.cur; + if (!xfer) + return IRQ_HANDLED; + cmd = xfer->cmds; if (xfer->is_i2c_xfer) { @@ -1184,6 +1193,9 @@ static irqreturn_t renesas_i3c_rx_isr(int irq, void *data) scoped_guard(spinlock, &i3c->xferqueue.lock) { xfer = i3c->xferqueue.cur; + if (!xfer) + return IRQ_HANDLED; + cmd = xfer->cmds; if (xfer->is_i2c_xfer) { @@ -1235,6 +1247,8 @@ static irqreturn_t renesas_i3c_stop_isr(int irq, void *data) scoped_guard(spinlock, &i3c->xferqueue.lock) { xfer = i3c->xferqueue.cur; + if (!xfer) + return IRQ_HANDLED; /* read back registers to confirm writes have fully propagated */ renesas_writel(i3c->regs, BST, 0); @@ -1259,6 +1273,9 @@ static irqreturn_t renesas_i3c_start_isr(int irq, void *data) scoped_guard(spinlock, &i3c->xferqueue.lock) { xfer = i3c->xferqueue.cur; + if (!xfer) + return IRQ_HANDLED; + cmd = xfer->cmds; if (xfer->is_i2c_xfer) { -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:00 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:00 +0300 Subject: [PATCH 02/17] i3c: renesas: Use the divider 128 In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-3-claudiu.beznea@kernel.org> From: Claudiu Beznea The REFCKCTL.IREFCKS field is 3 bits wide, and setting it to 7 selects a divider of 128 for the internal reference clock. Use this divider value. Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 36e3ccbe66b0..1917549cf6d5 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -559,7 +559,7 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) i2c_parse_fw_timings(&m->dev, &t, true); - for (cks = 0; cks < 7; cks++) { + for (cks = 0; cks <= 7; cks++) { /* SCL low-period calculation in Open-drain mode */ od_low_ticks = ((i2c_total_ticks * 6) / 10); -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:01 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:01 +0300 Subject: [PATCH 03/17] i3c: renesas: Restore STDBR and EXTBR registers on resume In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-4-claudiu.beznea@kernel.org> From: Claudiu Beznea The Renesas RZ/G3S supports a power saving state where power to the most SoC componentes (including I3C) is lost. The STDBR and EXTBR are configured in initialization phase though the struct i3c_master_controller_ops::bus_init. Set them on resume function as well to keep the same state of the controller after a suspend with power loss and a similar initialization sequence as in bus_init. Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 1917549cf6d5..6c23f956ad2a 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -260,6 +260,7 @@ struct renesas_i3c { u32 dyn_addr; u32 i2c_STDBR; u32 i3c_STDBR; + u32 extbr; unsigned long rate; u8 addrs[RENESAS_I3C_MAX_DEVS]; struct renesas_i3c_xferqueue xferqueue; @@ -607,10 +608,9 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); /* Extended Bit Rate setting */ - renesas_writel(i3c->regs, EXTBR, EXTBR_EBRLO(od_low_ticks) | - EXTBR_EBRHO(od_high_ticks) | - EXTBR_EBRLP(pp_low_ticks) | - EXTBR_EBRHP(pp_high_ticks)); + i3c->extbr = EXTBR_EBRLO(od_low_ticks) | EXTBR_EBRHO(od_high_ticks) | + EXTBR_EBRLP(pp_low_ticks) | EXTBR_EBRHP(pp_high_ticks); + renesas_writel(i3c->regs, EXTBR, i3c->extbr); renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); i3c->refclk_div = cks; @@ -1447,6 +1447,8 @@ static int renesas_i3c_resume_noirq(struct device *dev) goto err_tresetn; /* Re-store I3C registers value. */ + renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); + renesas_writel(i3c->regs, EXTBR, i3c->extbr); renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(i3c->refclk_div)); renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYADV | -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:02 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:02 +0300 Subject: [PATCH 04/17] i3c: renesas: Follow the reset deassert order used in probe In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-5-claudiu.beznea@kernel.org> From: Claudiu Beznea Use the same reset deassert order in the resume and probe paths to avoid potential failures due to ordering differences. Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 6c23f956ad2a..d2f29ed0b6ed 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -1434,17 +1434,17 @@ static int renesas_i3c_resume_noirq(struct device *dev) struct renesas_i3c *i3c = dev_get_drvdata(dev); int i, ret; - ret = reset_control_deassert(i3c->presetn); + ret = reset_control_deassert(i3c->tresetn); if (ret) return ret; - ret = reset_control_deassert(i3c->tresetn); + ret = reset_control_deassert(i3c->presetn); if (ret) - goto err_presetn; + goto err_tresetn; ret = clk_bulk_enable(i3c->num_clks, i3c->clks); if (ret) - goto err_tresetn; + goto err_presetn; /* Re-store I3C registers value. */ renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); @@ -1465,10 +1465,10 @@ static int renesas_i3c_resume_noirq(struct device *dev) return 0; -err_tresetn: - reset_control_assert(i3c->tresetn); err_presetn: reset_control_assert(i3c->presetn); +err_tresetn: + reset_control_assert(i3c->tresetn); return ret; } -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:03 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:03 +0300 Subject: [PATCH 05/17] i3c: renesas: Fix re-attach In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-6-claudiu.beznea@kernel.org> From: Claudiu Beznea During re-attach, the device may change its position in the i3c->addrs[] array. As a result, it may use a different Device Address Table Basic Register (DATBAS), which needs to be reconfigured. Reconfigure the DATBAS register on re-attach. Along with it update software caches. Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index d2f29ed0b6ed..5174a390d668 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -892,10 +892,28 @@ static int renesas_i3c_reattach_i3c_dev(struct i3c_dev_desc *dev, struct i3c_master_controller *m = i3c_dev_get_master(dev); struct renesas_i3c *i3c = to_renesas_i3c(m); struct renesas_i3c_i2c_dev_data *data = i3c_dev_get_master_data(dev); + int pos; + + pos = renesas_i3c_get_free_pos(i3c); + if (pos < 0) + return pos; + + if (data->index != pos) { + renesas_writel(i3c->regs, DATBAS(data->index), 0); + i3c->addrs[data->index] = 0; + i3c->free_pos |= BIT(data->index); + + data->index = pos; + i3c->free_pos &= ~BIT(data->index); + } i3c->addrs[data->index] = dev->info.dyn_addr ? dev->info.dyn_addr : dev->info.static_addr; + renesas_writel(i3c->regs, DATBAS(data->index), + DATBAS_DVSTAD(dev->info.static_addr) | + datbas_dvdyad_with_parity(i3c->addrs[data->index])); + return 0; } -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:04 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:04 +0300 Subject: [PATCH 06/17] i3c: renesas: Reset the controller on resume In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-7-claudiu.beznea@kernel.org> From: Claudiu Beznea Reset the controller on resume after enabling the clocks to follow the same sequence as in probe and avoid potential ordering related failures. Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 5174a390d668..2f3c6ddf75c0 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -1464,6 +1464,10 @@ static int renesas_i3c_resume_noirq(struct device *dev) if (ret) goto err_presetn; + ret = renesas_i3c_reset(i3c); + if (ret) + goto err_clks_disable; + /* Re-store I3C registers value. */ renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); renesas_writel(i3c->regs, EXTBR, i3c->extbr); @@ -1483,6 +1487,8 @@ static int renesas_i3c_resume_noirq(struct device *dev) return 0; +err_clks_disable: + clk_bulk_disable(i3c->num_clks, i3c->clks); err_presetn: reset_control_assert(i3c->presetn); err_tresetn: -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:05 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:05 +0300 Subject: [PATCH 07/17] i3c: renesas: Perform Dynamic Address Assignment on resume In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-8-claudiu.beznea@kernel.org> From: Claudiu Beznea The Renesas RZ/G3S SoC supports a power saving mode where power to most SoC components, including I3C, is turned off. On systems where the I3C devices also loses power during suspend (e.g. NXP P3T1085UK-ARD connected to the PMOD1_6A connector of the RZ SMARC Carrier 2 + Renesas RZ/G3S SMARC SOM), the devices becomes unreachable after resume. Running DAA in the controller resume path restores communication. However, DAA relies on interrupts for TX/RX, which are not available in the noirq suspend/resume phase (unless they are wakeup interrupts). For this, the suspend/resume callbacks were moved out of the noirq phase. Currently, there is no identified use case on either the Renesas RZ/G3S or Renesas RZ/G3E SoCs that requires the controller suspend/resume hooks to be part of the noirq suspend/resume phase. Along with this, struct renesas_i3c::DATBASn and its usage were removed, as they are no longer needed. Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 34 ++++++++++++-------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 2f3c6ddf75c0..c009d0de6a2b 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -265,7 +265,6 @@ struct renesas_i3c { u8 addrs[RENESAS_I3C_MAX_DEVS]; struct renesas_i3c_xferqueue xferqueue; void __iomem *regs; - u32 *DATBASn; struct clk_bulk_data *clks; struct reset_control *presetn; struct reset_control *tresetn; @@ -1400,12 +1399,6 @@ static int renesas_i3c_probe(struct platform_device *pdev) i3c->maxdevs = RENESAS_I3C_MAX_DEVS; i3c->free_pos = GENMASK(i3c->maxdevs - 1, 0); - /* Allocate dynamic Device Address Table backup. */ - i3c->DATBASn = devm_kzalloc(&pdev->dev, sizeof(u32) * i3c->maxdevs, - GFP_KERNEL); - if (!i3c->DATBASn) - return -ENOMEM; - return i3c_master_register(&i3c->base, &pdev->dev, &renesas_i3c_ops, false); } @@ -1416,17 +1409,13 @@ static void renesas_i3c_remove(struct platform_device *pdev) i3c_master_unregister(&i3c->base); } -static int renesas_i3c_suspend_noirq(struct device *dev) +static int renesas_i3c_suspend(struct device *dev) { struct renesas_i3c *i3c = dev_get_drvdata(dev); - int i, ret; + int ret; i2c_mark_adapter_suspended(&i3c->base.i2c); - /* Store Device Address Table values. */ - for (i = 0; i < i3c->maxdevs; i++) - i3c->DATBASn[i] = renesas_readl(i3c->regs, DATBAS(i)); - ret = reset_control_assert(i3c->presetn); if (ret) goto err_mark_resumed; @@ -1447,10 +1436,10 @@ static int renesas_i3c_suspend_noirq(struct device *dev) return ret; } -static int renesas_i3c_resume_noirq(struct device *dev) +static int renesas_i3c_resume(struct device *dev) { struct renesas_i3c *i3c = dev_get_drvdata(dev); - int i, ret; + int ret; ret = reset_control_deassert(i3c->tresetn); if (ret) @@ -1476,15 +1465,19 @@ static int renesas_i3c_resume_noirq(struct device *dev) renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYADV | MSDVAD_MDYAD(i3c->dyn_addr)); - /* Restore Device Address Table values. */ - for (i = 0; i < i3c->maxdevs; i++) - renesas_writel(i3c->regs, DATBAS(i), i3c->DATBASn[i]); - /* I3C hw init. */ renesas_i3c_hw_init(i3c); i2c_mark_adapter_resumed(&i3c->base.i2c); + ret = i3c_master_do_daa_ext(&i3c->base, true); + if (ret) + dev_err(dev, "DAA failed on resume, ret=%d", ret); + + /* + * I3C devices may have retained their dynamic address anyway. Do not + * fail the resume because of DAA error. + */ return 0; err_clks_disable: @@ -1497,8 +1490,7 @@ static int renesas_i3c_resume_noirq(struct device *dev) } static const struct dev_pm_ops renesas_i3c_pm_ops = { - NOIRQ_SYSTEM_SLEEP_PM_OPS(renesas_i3c_suspend_noirq, - renesas_i3c_resume_noirq) + SYSTEM_SLEEP_PM_OPS(renesas_i3c_suspend, renesas_i3c_resume) }; static const struct of_device_id renesas_i3c_of_ids[] = { -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:06 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:06 +0300 Subject: [PATCH 08/17] i3c: renesas: Clean DATBAS register on detach In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-9-claudiu.beznea@kernel.org> From: Claudiu Beznea The controller uses DATBAS registers on TX/RX logic. Clean the DATBAS register for the detached I3C device to avoid issues. Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") Cc: stable at vger.kernel.org Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index c009d0de6a2b..d32646deb69f 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -922,6 +922,8 @@ static void renesas_i3c_detach_i3c_dev(struct i3c_dev_desc *dev) struct i3c_master_controller *m = i3c_dev_get_master(dev); struct renesas_i3c *i3c = to_renesas_i3c(m); + renesas_writel(i3c->regs, DATBAS(data->index), 0); + i3c_dev_set_master_data(dev, NULL); i3c->addrs[data->index] = 0; i3c->free_pos |= BIT(data->index); -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:07 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:07 +0300 Subject: [PATCH 09/17] i3c: renesas: Use reset_control_bulk_{assert, deassert}() In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-10-claudiu.beznea@kernel.org> From: Claudiu Beznea Use reset_control_bulk_assert() and reset_control_bulk_deassert() in the suspend and resume paths to simplify the code. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index d32646deb69f..e5963270d6e5 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -1414,24 +1414,22 @@ static void renesas_i3c_remove(struct platform_device *pdev) static int renesas_i3c_suspend(struct device *dev) { struct renesas_i3c *i3c = dev_get_drvdata(dev); + struct reset_control_bulk_data resets[] = { + { .rstc = i3c->presetn }, + { .rstc = i3c->tresetn }, + }; int ret; i2c_mark_adapter_suspended(&i3c->base.i2c); - ret = reset_control_assert(i3c->presetn); + ret = reset_control_bulk_assert(ARRAY_SIZE(resets), resets); if (ret) goto err_mark_resumed; - ret = reset_control_assert(i3c->tresetn); - if (ret) - goto err_presetn; - clk_bulk_disable(i3c->num_clks, i3c->clks); return 0; -err_presetn: - reset_control_deassert(i3c->presetn); err_mark_resumed: i2c_mark_adapter_resumed(&i3c->base.i2c); @@ -1441,19 +1439,19 @@ static int renesas_i3c_suspend(struct device *dev) static int renesas_i3c_resume(struct device *dev) { struct renesas_i3c *i3c = dev_get_drvdata(dev); + struct reset_control_bulk_data resets[] = { + { .rstc = i3c->presetn }, + { .rstc = i3c->tresetn }, + }; int ret; - ret = reset_control_deassert(i3c->tresetn); + ret = reset_control_bulk_deassert(ARRAY_SIZE(resets), resets); if (ret) return ret; - ret = reset_control_deassert(i3c->presetn); - if (ret) - goto err_tresetn; - ret = clk_bulk_enable(i3c->num_clks, i3c->clks); if (ret) - goto err_presetn; + goto err_resets_asserted; ret = renesas_i3c_reset(i3c); if (ret) @@ -1484,10 +1482,8 @@ static int renesas_i3c_resume(struct device *dev) err_clks_disable: clk_bulk_disable(i3c->num_clks, i3c->clks); -err_presetn: - reset_control_assert(i3c->presetn); -err_tresetn: - reset_control_assert(i3c->tresetn); +err_resets_asserted: + reset_control_bulk_assert(ARRAY_SIZE(resets), resets); return ret; } -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:08 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:08 +0300 Subject: [PATCH 10/17] i3c: renesas: Return immediately if there is nothing to transfer In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-11-claudiu.beznea@kernel.org> From: Claudiu Beznea There is no need to allocate a transfer structure when i2c_nxfers is zero. Return immediately instead of unnecessarily allocating memory. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index e5963270d6e5..de75125eb013 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -940,13 +940,13 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, u8 start_bit = CNDCTL_STCND; int i; + if (!i2c_nxfers) + return 0; + struct renesas_i3c_xfer *xfer __free(kfree) = renesas_i3c_alloc_xfer(i3c, 1); if (!xfer) return -ENOMEM; - if (!i2c_nxfers) - return 0; - renesas_i3c_bus_enable(m, false); init_completion(&xfer->comp); -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:09 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:09 +0300 Subject: [PATCH 11/17] i3c: renesas: Follow a unified pattern for transfer and command initialization In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-12-claudiu.beznea@kernel.org> From: Claudiu Beznea Follow a unified pattern for transfer and command initialization across the driver. This keeps the code cleaner and easier to follow. Also, in some cases the I3C device was enabled before the transfer data structure was even allocated. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index de75125eb013..12bf4797a70d 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -648,6 +648,10 @@ static int renesas_i3c_daa(struct i3c_master_controller *m) if (!xfer) return -ENOMEM; + init_completion(&xfer->comp); + cmd = xfer->cmds; + cmd->rx_count = 0; + /* Enable I3C bus. */ renesas_i3c_bus_enable(m, true); @@ -669,10 +673,6 @@ static int renesas_i3c_daa(struct i3c_master_controller *m) renesas_writel(i3c->regs, DATBAS(pos), datbas_dvdyad_with_parity(ret)); } - init_completion(&xfer->comp); - cmd = xfer->cmds; - cmd->rx_count = 0; - ret = renesas_i3c_get_free_pos(i3c); if (ret < 0) return ret; @@ -760,13 +760,13 @@ static int renesas_i3c_send_ccc_cmd(struct i3c_master_controller *m, if (!xfer) return -ENOMEM; - renesas_i3c_bus_enable(m, true); - init_completion(&xfer->comp); cmd = xfer->cmds; cmd->rnw = ccc->rnw; cmd->cmd0 = 0; + renesas_i3c_bus_enable(m, true); + /* Calculate the command descriptor. */ switch (ccc->id) { case I3C_CCC_SETDASA: @@ -816,15 +816,15 @@ static int renesas_i3c_i3c_xfers(struct i3c_dev_desc *dev, struct i3c_xfer *i3c_ struct renesas_i3c_i2c_dev_data *data = i3c_dev_get_master_data(dev); int i; - /* Enable I3C bus. */ - renesas_i3c_bus_enable(m, true); - struct renesas_i3c_xfer *xfer __free(kfree) = renesas_i3c_alloc_xfer(i3c, 1); if (!xfer) return -ENOMEM; init_completion(&xfer->comp); + /* Enable I3C bus. */ + renesas_i3c_bus_enable(m, true); + for (i = 0; i < i3c_nxfers; i++) { struct renesas_i3c_cmd *cmd = xfer->cmds; @@ -947,12 +947,12 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, if (!xfer) return -ENOMEM; - renesas_i3c_bus_enable(m, false); - init_completion(&xfer->comp); xfer->is_i2c_xfer = true; cmd = xfer->cmds; + renesas_i3c_bus_enable(m, false); + if (!(renesas_readl(i3c->regs, BCST) & BCST_BFREF)) { cmd->err = -EBUSY; return cmd->err; -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:10 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:10 +0300 Subject: [PATCH 12/17] i3c: renesas: Drop the explicit memset() call In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-13-claudiu.beznea@kernel.org> From: Claudiu Beznea Drop the explicit memset() call on struct i3c_device_info object, as it is already initialized at declaration through compiler initialization. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 12bf4797a70d..865e67ac0fd2 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -624,7 +624,6 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) i3c->dyn_addr = ret; renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYAD(ret) | MSDVAD_MDYADV); - memset(&info, 0, sizeof(info)); info.dyn_addr = ret; return i3c_master_set_info(&i3c->base, &info); } -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:11 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:11 +0300 Subject: [PATCH 13/17] i3c: renesas: Update HW registers after SW computations are done In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-14-claudiu.beznea@kernel.org> From: Claudiu Beznea renesas_i3c_bus_init() performs a number of computations and software cache updates, interleaving them with hardware register writes. While this works today, it makes it harder to minimize the time the controller must remain powered when runtime PM is introduced. Perform all software computations and cache updates first, then update the hardware registers. This prepares for future runtime PM support. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 865e67ac0fd2..631c9c5d8038 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -550,10 +550,6 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) if (!i3c->rate) return -EINVAL; - ret = renesas_i3c_reset(i3c); - if (ret) - return ret; - i2c_total_ticks = DIV_ROUND_UP(i3c->rate, bus->scl_rate.i2c); i3c_total_ticks = DIV_ROUND_UP(i3c->rate, bus->scl_rate.i3c); @@ -604,27 +600,31 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) STDBR_SBRHO(double_SBR, od_high_ticks) | STDBR_SBRLP(pp_low_ticks) | STDBR_SBRHP(pp_high_ticks); - renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); /* Extended Bit Rate setting */ i3c->extbr = EXTBR_EBRLO(od_low_ticks) | EXTBR_EBRHO(od_high_ticks) | EXTBR_EBRLP(pp_low_ticks) | EXTBR_EBRHP(pp_high_ticks); - renesas_writel(i3c->regs, EXTBR, i3c->extbr); - - renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); - i3c->refclk_div = cks; - - /* I3C hw init*/ - renesas_i3c_hw_init(i3c); ret = i3c_master_get_free_addr(m, 0); if (ret < 0) return ret; + info.dyn_addr = ret; i3c->dyn_addr = ret; - renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYAD(ret) | MSDVAD_MDYADV); + i3c->refclk_div = cks; + + ret = renesas_i3c_reset(i3c); + if (ret) + return ret; + + renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); + renesas_writel(i3c->regs, EXTBR, i3c->extbr); + renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); + renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYAD(i3c->dyn_addr) | MSDVAD_MDYADV); + + /* I3C hw init*/ + renesas_i3c_hw_init(i3c); - info.dyn_addr = ret; return i3c_master_set_info(&i3c->base, &info); } -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:12 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:12 +0300 Subject: [PATCH 14/17] i3c: renesas: Organize structures to avoid unnecessary padding In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-15-claudiu.beznea@kernel.org> From: Claudiu Beznea Reorder structure members to reduce padding and improve memory layout. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 631c9c5d8038..5614ed99553c 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -221,19 +221,19 @@ enum renesas_i3c_event { }; struct renesas_i3c_cmd { + const void *tx_buf; + void *rx_buf; + /* i2c xfer */ + u8 *i2c_buf; + const struct i2c_msg *msg; + int i2c_bytes_left; + int i2c_is_last; u32 cmd0; u32 len; - const void *tx_buf; u32 tx_count; - void *rx_buf; u32 rx_count; u32 err; u8 rnw; - /* i2c xfer */ - int i2c_bytes_left; - int i2c_is_last; - u8 *i2c_buf; - const struct i2c_msg *msg; }; struct renesas_i3c_xfer { @@ -253,21 +253,21 @@ struct renesas_i3c_xferqueue { }; struct renesas_i3c { + void __iomem *regs; + struct clk_bulk_data *clks; + struct reset_control *presetn; + struct reset_control *tresetn; + struct renesas_i3c_xferqueue xferqueue; struct i3c_master_controller base; + unsigned long rate; enum i3c_internal_state internal_state; - u16 maxdevs; u32 free_pos; u32 dyn_addr; u32 i2c_STDBR; u32 i3c_STDBR; u32 extbr; - unsigned long rate; + u16 maxdevs; u8 addrs[RENESAS_I3C_MAX_DEVS]; - struct renesas_i3c_xferqueue xferqueue; - void __iomem *regs; - struct clk_bulk_data *clks; - struct reset_control *presetn; - struct reset_control *tresetn; u8 num_clks; u8 refclk_div; }; -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:13 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:13 +0300 Subject: [PATCH 15/17] i3c: renesas: Use the "dev_name:irq_name" format for the interrupt name In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-16-claudiu.beznea@kernel.org> From: Claudiu Beznea Use the "dev_name:irq_name" format for the interrupt names. This makes it easier to identify interrupts in systems where multiple devices may request interrupts with the same name. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 5614ed99553c..e6e05ac03082 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -1385,12 +1385,19 @@ static int renesas_i3c_probe(struct platform_device *pdev) return ret; for (i = 0; i < ARRAY_SIZE(renesas_i3c_irqs); i++) { + const char *irqname; + ret = platform_get_irq_byname(pdev, renesas_i3c_irqs[i].name); if (ret < 0) return ret; + irqname = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s:%s", dev_name(&pdev->dev), + renesas_i3c_irqs[i].desc); + if (!irqname) + return -ENOMEM; + ret = devm_request_irq(&pdev->dev, ret, renesas_i3c_irqs[i].isr, - 0, renesas_i3c_irqs[i].desc, i3c); + 0, irqname, i3c); if (ret) return ret; } -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:14 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:14 +0300 Subject: [PATCH 16/17] i3c: renesas: Drop unnecessary tab In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-17-claudiu.beznea@kernel.org> From: Claudiu Beznea Remove an unnecessary tab to make the code cleaner. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index e6e05ac03082..a070db4d2440 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -109,7 +109,7 @@ #define NCMDQP_DATA_LENGTH(x) FIELD_PREP(GENMASK(31, 16), x) #define NRSPQP 0x154 /* Normal Respone Queue */ -#define NRSPQP_NO_ERROR 0 +#define NRSPQP_NO_ERROR 0 #define NRSPQP_ERROR_CRC 1 #define NRSPQP_ERROR_PARITY 2 #define NRSPQP_ERROR_FRAME 3 -- 2.43.0 From claudiu.beznea at kernel.org Fri May 22 03:18:15 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Fri, 22 May 2026 13:18:15 +0300 Subject: [PATCH 17/17] i3c: renesas: Add runtime PM support In-Reply-To: <20260522101815.1722909-1-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> Message-ID: <20260522101815.1722909-18-claudiu.beznea@kernel.org> From: Claudiu Beznea On the SoCs where the Renesas I3C driver is enabled (RZ/G3S and RZ/G3E), the clocks of the IP are managed through a clock PM domain. To keep the I3C code simpler, the explicit clock handling was dropped along with the addition of runtime PM support, in favor of the runtime PM APIs. Only the code for getting tclk was preserved, as it is necessary to compute the I3C clock rate. All the APIs provided to the I3C subsystem through struct i3c_master_controller_ops are guarded with runtime PM APIs to enable/disable the controller at runtime. As the Renesas I3C driver implements an asynchronous transmit model by preparing a transfer and waiting for its completion through the ISR, renesas_i3c_abort_xfer() was added to disable interrupts and synchronize IRQs before runtime suspending the controller. For this, the interrupts were saved in struct renesas_i3c::irqs. Along with this, renesas_i3c_wait_xfer() return type was changed to unsigned long. Along with the clocks, the controller pin configuration is changed through the provided "sleep" pin configuration. Add runtime PM support for the Renesas I3C driver. Signed-off-by: Claudiu Beznea --- drivers/i3c/master/renesas-i3c.c | 183 ++++++++++++++++++++++++++----- 1 file changed, 156 insertions(+), 27 deletions(-) diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index a070db4d2440..3b9807a89b54 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -21,7 +21,9 @@ #include #include #include +#include #include +#include #include #include #include "../internals.h" @@ -199,8 +201,6 @@ #define RENESAS_I3C_MAX_DEVS 8 #define I2C_INIT_MSG -1 -#define RENESAS_I3C_TCLK_IDX 1 - enum i3c_internal_state { I3C_INTERNAL_STATE_DISABLED, I3C_INTERNAL_STATE_CONTROLLER_IDLE, @@ -254,12 +254,15 @@ struct renesas_i3c_xferqueue { struct renesas_i3c { void __iomem *regs; - struct clk_bulk_data *clks; + struct clk *tclk; struct reset_control *presetn; struct reset_control *tresetn; + struct device *dev; + int *irqs; struct renesas_i3c_xferqueue xferqueue; struct i3c_master_controller base; unsigned long rate; + unsigned int num_irqs; enum i3c_internal_state internal_state; u32 free_pos; u32 dyn_addr; @@ -268,7 +271,6 @@ struct renesas_i3c { u32 extbr; u16 maxdevs; u8 addrs[RENESAS_I3C_MAX_DEVS]; - u8 num_clks; u8 refclk_div; }; @@ -433,7 +435,18 @@ static void renesas_i3c_enqueue_xfer(struct renesas_i3c *i3c, struct renesas_i3c } } -static void renesas_i3c_wait_xfer(struct renesas_i3c *i3c, struct renesas_i3c_xfer *xfer) +static void renesas_i3c_abort_xfer(struct renesas_i3c *i3c) +{ + /* Disable all interrupts */ + renesas_writel(i3c->regs, BIE, 0); + renesas_writel(i3c->regs, NTIE, 0); + + /* Synchronize IRQs. */ + for (unsigned int i = 0; i < i3c->num_irqs; i++) + synchronize_irq(i3c->irqs[i]); +} + +static unsigned long renesas_i3c_wait_xfer(struct renesas_i3c *i3c, struct renesas_i3c_xfer *xfer) { unsigned long time_left; @@ -442,6 +455,8 @@ static void renesas_i3c_wait_xfer(struct renesas_i3c *i3c, struct renesas_i3c_xf time_left = wait_for_completion_timeout(&xfer->comp, msecs_to_jiffies(1000)); if (!time_left) renesas_i3c_dequeue_xfer(i3c, xfer); + + return time_left; } static void renesas_i3c_set_prts(struct renesas_i3c *i3c, u32 val) @@ -475,6 +490,12 @@ static void renesas_i3c_bus_enable(struct i3c_master_controller *m, bool i3c_mod static int renesas_i3c_reset(struct renesas_i3c *i3c) { u32 val; + int ret; + + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; renesas_writel(i3c->regs, BCTL, 0); renesas_set_bit(i3c->regs, RSTCTL, RSTCTL_RI3CRST); @@ -546,7 +567,7 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) int od_high_ticks, od_low_ticks, i2c_total_ticks; int ret; - i3c->rate = clk_get_rate(i3c->clks[RENESAS_I3C_TCLK_IDX].clk); + i3c->rate = clk_get_rate(i3c->tclk); if (!i3c->rate) return -EINVAL; @@ -617,6 +638,11 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) if (ret) return ret; + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); renesas_writel(i3c->regs, EXTBR, i3c->extbr); renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); @@ -639,6 +665,7 @@ static int renesas_i3c_daa(struct i3c_master_controller *m) { struct renesas_i3c *i3c = to_renesas_i3c(m); struct renesas_i3c_cmd *cmd; + unsigned long time_left; u32 olddevs, newdevs; u8 last_addr = 0, pos; int ret; @@ -651,6 +678,11 @@ static int renesas_i3c_daa(struct i3c_master_controller *m) cmd = xfer->cmds; cmd->rx_count = 0; + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + /* Enable I3C bus. */ renesas_i3c_bus_enable(m, true); @@ -685,7 +717,9 @@ static int renesas_i3c_daa(struct i3c_master_controller *m) NCMDQP_CMD(I3C_CCC_ENTDAA) | NCMDQP_DEV_INDEX(ret) | NCMDQP_DEV_COUNT(i3c->maxdevs - ret) | NCMDQP_TOC; - renesas_i3c_wait_xfer(i3c, xfer); + time_left = renesas_i3c_wait_xfer(i3c, xfer); + if (!time_left) + renesas_i3c_abort_xfer(i3c); newdevs = GENMASK(i3c->maxdevs - cmd->rx_count - 1, 0); newdevs &= ~olddevs; @@ -747,6 +781,7 @@ static int renesas_i3c_send_ccc_cmd(struct i3c_master_controller *m, { struct renesas_i3c *i3c = to_renesas_i3c(m); struct renesas_i3c_cmd *cmd; + unsigned long time_left; int ret, pos = 0; if (ccc->id & I3C_CCC_DIRECT) { @@ -764,6 +799,11 @@ static int renesas_i3c_send_ccc_cmd(struct i3c_master_controller *m, cmd->rnw = ccc->rnw; cmd->cmd0 = 0; + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + renesas_i3c_bus_enable(m, true); /* Calculate the command descriptor. */ @@ -798,7 +838,9 @@ static int renesas_i3c_send_ccc_cmd(struct i3c_master_controller *m, } } - renesas_i3c_wait_xfer(i3c, xfer); + time_left = renesas_i3c_wait_xfer(i3c, xfer); + if (!time_left) + renesas_i3c_abort_xfer(i3c); ret = xfer->ret; if (ret) @@ -813,7 +855,9 @@ static int renesas_i3c_i3c_xfers(struct i3c_dev_desc *dev, struct i3c_xfer *i3c_ struct i3c_master_controller *m = i3c_dev_get_master(dev); struct renesas_i3c *i3c = to_renesas_i3c(m); struct renesas_i3c_i2c_dev_data *data = i3c_dev_get_master_data(dev); - int i; + unsigned long time_left; + bool abort_xfer = false; + int i, ret; struct renesas_i3c_xfer *xfer __free(kfree) = renesas_i3c_alloc_xfer(i3c, 1); if (!xfer) @@ -821,6 +865,11 @@ static int renesas_i3c_i3c_xfers(struct i3c_dev_desc *dev, struct i3c_xfer *i3c_ init_completion(&xfer->comp); + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + /* Enable I3C bus. */ renesas_i3c_bus_enable(m, true); @@ -852,9 +901,14 @@ static int renesas_i3c_i3c_xfers(struct i3c_dev_desc *dev, struct i3c_xfer *i3c_ renesas_set_bit(i3c->regs, NTIE, NTIE_TDBEIE0); } - renesas_i3c_wait_xfer(i3c, xfer); + time_left = renesas_i3c_wait_xfer(i3c, xfer); + if (!time_left) + abort_xfer = true; } + if (abort_xfer) + renesas_i3c_abort_xfer(i3c); + return 0; } @@ -863,12 +917,17 @@ static int renesas_i3c_attach_i3c_dev(struct i3c_dev_desc *dev) struct i3c_master_controller *m = i3c_dev_get_master(dev); struct renesas_i3c *i3c = to_renesas_i3c(m); struct renesas_i3c_i2c_dev_data *data; - int pos; + int pos, ret; pos = renesas_i3c_get_free_pos(i3c); if (pos < 0) return pos; + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + data = kzalloc_obj(*data); if (!data) return -ENOMEM; @@ -890,12 +949,17 @@ static int renesas_i3c_reattach_i3c_dev(struct i3c_dev_desc *dev, struct i3c_master_controller *m = i3c_dev_get_master(dev); struct renesas_i3c *i3c = to_renesas_i3c(m); struct renesas_i3c_i2c_dev_data *data = i3c_dev_get_master_data(dev); - int pos; + int pos, ret; pos = renesas_i3c_get_free_pos(i3c); if (pos < 0) return pos; + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + if (data->index != pos) { renesas_writel(i3c->regs, DATBAS(data->index), 0); i3c->addrs[data->index] = 0; @@ -920,6 +984,12 @@ static void renesas_i3c_detach_i3c_dev(struct i3c_dev_desc *dev) struct renesas_i3c_i2c_dev_data *data = i3c_dev_get_master_data(dev); struct i3c_master_controller *m = i3c_dev_get_master(dev); struct renesas_i3c *i3c = to_renesas_i3c(m); + int ret; + + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return; renesas_writel(i3c->regs, DATBAS(data->index), 0); @@ -937,7 +1007,9 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, struct renesas_i3c *i3c = to_renesas_i3c(m); struct renesas_i3c_cmd *cmd; u8 start_bit = CNDCTL_STCND; - int i; + unsigned long time_left; + bool abort_xfer = false; + int i, ret; if (!i2c_nxfers) return 0; @@ -950,6 +1022,11 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, xfer->is_i2c_xfer = true; cmd = xfer->cmds; + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); + if (ret) + return ret; + renesas_i3c_bus_enable(m, false); if (!(renesas_readl(i3c->regs, BCST) & BCST_BFREF)) { @@ -976,7 +1053,9 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, renesas_set_bit(i3c->regs, NTSTE, NTSTE_TDBEE0); - wait_for_completion_timeout(&xfer->comp, m->i2c.timeout); + time_left = wait_for_completion_timeout(&xfer->comp, m->i2c.timeout); + if (!time_left) + abort_xfer = true; if (cmd->err) break; @@ -985,6 +1064,10 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, } renesas_i3c_dequeue_xfer(i3c, xfer); + + if (abort_xfer) + renesas_i3c_abort_xfer(i3c); + return cmd->err; } @@ -1347,6 +1430,11 @@ static const struct renesas_i3c_irq_desc renesas_i3c_irqs[] = { { .name = "nack", .isr = renesas_i3c_tend_isr, .desc = "i3c-nack" }, }; +static void renesas_i3c_dont_use_autosuspend(void *data) +{ + pm_runtime_dont_use_autosuspend(data); +} + static int renesas_i3c_probe(struct platform_device *pdev) { struct renesas_i3c *i3c; @@ -1360,12 +1448,21 @@ static int renesas_i3c_probe(struct platform_device *pdev) if (IS_ERR(i3c->regs)) return PTR_ERR(i3c->regs); - ret = devm_clk_bulk_get_all_enabled(&pdev->dev, &i3c->clks); - if (ret <= RENESAS_I3C_TCLK_IDX) - return dev_err_probe(&pdev->dev, ret < 0 ? ret : -EINVAL, - "Failed to get clocks (need > %d, got %d)\n", - RENESAS_I3C_TCLK_IDX, ret); - i3c->num_clks = ret; + i3c->tclk = devm_clk_get(&pdev->dev, "tclk"); + if (IS_ERR(i3c->tclk)) + return dev_err_probe(&pdev->dev, PTR_ERR(i3c->tclk), "Failed to get tclk"); + + i3c->dev = &pdev->dev; + pm_runtime_set_autosuspend_delay(&pdev->dev, 300); + pm_runtime_use_autosuspend(&pdev->dev); + ret = devm_add_action_or_reset(&pdev->dev, renesas_i3c_dont_use_autosuspend, + i3c->dev); + if (ret) + return ret; + + ret = devm_pm_runtime_enable(&pdev->dev); + if (ret) + return ret; i3c->tresetn = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, "tresetn"); if (IS_ERR(i3c->tresetn)) @@ -1384,19 +1481,25 @@ static int renesas_i3c_probe(struct platform_device *pdev) if (ret) return ret; + i3c->num_irqs = ARRAY_SIZE(renesas_i3c_irqs); + i3c->irqs = devm_kcalloc(&pdev->dev, i3c->num_irqs, sizeof(*i3c->irqs), GFP_KERNEL); + if (!i3c->irqs) + return -ENOMEM; + for (i = 0; i < ARRAY_SIZE(renesas_i3c_irqs); i++) { const char *irqname; ret = platform_get_irq_byname(pdev, renesas_i3c_irqs[i].name); if (ret < 0) return ret; + i3c->irqs[i] = ret; irqname = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s:%s", dev_name(&pdev->dev), renesas_i3c_irqs[i].desc); if (!irqname) return -ENOMEM; - ret = devm_request_irq(&pdev->dev, ret, renesas_i3c_irqs[i].isr, + ret = devm_request_irq(&pdev->dev, i3c->irqs[i], renesas_i3c_irqs[i].isr, 0, irqname, i3c); if (ret) return ret; @@ -1432,10 +1535,14 @@ static int renesas_i3c_suspend(struct device *dev) if (ret) goto err_mark_resumed; - clk_bulk_disable(i3c->num_clks, i3c->clks); + ret = pm_runtime_force_suspend(dev); + if (ret) + goto err_resets_deassert; return 0; +err_resets_deassert: + reset_control_bulk_deassert(ARRAY_SIZE(resets), resets); err_mark_resumed: i2c_mark_adapter_resumed(&i3c->base.i2c); @@ -1451,17 +1558,22 @@ static int renesas_i3c_resume(struct device *dev) }; int ret; + ret = pm_runtime_force_resume(dev); + if (ret) + return ret; + ret = reset_control_bulk_deassert(ARRAY_SIZE(resets), resets); if (ret) return ret; - ret = clk_bulk_enable(i3c->num_clks, i3c->clks); + ret = renesas_i3c_reset(i3c); if (ret) goto err_resets_asserted; - ret = renesas_i3c_reset(i3c); + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); + ret = PM_RUNTIME_ACQUIRE_ERR(&pm); if (ret) - goto err_clks_disable; + goto err_resets_asserted; /* Re-store I3C registers value. */ renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); @@ -1486,14 +1598,31 @@ static int renesas_i3c_resume(struct device *dev) */ return 0; -err_clks_disable: - clk_bulk_disable(i3c->num_clks, i3c->clks); err_resets_asserted: + /* + * If this happens, there is no way to recover from this state without + * reloading the driver. We want to avoid keeping the reset line + * deasserted unnecessarily. The runtime paths will still work correctly + * even if the IP registers are accessed while reset is asserted (e.g. + * if a runtime path is triggered after a failed resume). Checked on + * RZ/G3S. + */ reset_control_bulk_assert(ARRAY_SIZE(resets), resets); return ret; } +static int renesas_i3c_runtime_suspend(struct device *dev) +{ + return pinctrl_pm_select_sleep_state(dev); +} + +static int renesas_i3c_runtime_resume(struct device *dev) +{ + return pinctrl_pm_select_default_state(dev); +} + static const struct dev_pm_ops renesas_i3c_pm_ops = { + RUNTIME_PM_OPS(renesas_i3c_runtime_suspend, renesas_i3c_runtime_resume, NULL) SYSTEM_SLEEP_PM_OPS(renesas_i3c_suspend, renesas_i3c_resume) }; -- 2.43.0 From Frank.li at nxp.com Fri May 22 11:59:58 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 14:59:58 -0400 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> <417993a7-4a4f-4ba5-a815-aab63ed03a3c@intel.com> Message-ID: On Fri, May 22, 2026 at 07:52:17AM +0300, Adrian Hunter wrote: > On 21/05/2026 21:32, Frank Li wrote: > > On Fri, May 15, 2026 at 07:42:20PM +0300, Adrian Hunter wrote: > >> On 13/05/2026 22:03, Frank Li wrote: > >>> On Wed, May 13, 2026 at 08:45:55AM +0300, Adrian Hunter wrote: > >>>> On 12/05/2026 19:39, Frank Li wrote: > >>>>> On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: > >>>>>> Master drivers may invoke i3c_master_do_daa_ext() during resume to > >>>>>> re-run Dynamic Address Assignment. As well as assigning addresses to > >>>>>> any newly arrived devices, this restores the dynamic address of devices > >>>>>> that lost it across system suspend, so it has to run as part of the > >>>>>> controller's resume path. > >>>>>> > >>>>>> A side effect of i3c_master_do_daa_ext() today is that it also > >>>>>> registers any newly discovered I3C devices with the driver model > >>>>>> inline, via i3c_master_register_new_i3c_devs(). Doing that from the > >>>>>> resume path is problematic: a hot-join-capable device may join the bus > >>>>>> during this same DAA, and registering it immediately would push driver > >>>>>> model work (probing, sysfs, etc.) into the controller's resume context, > >>>>>> where the rest of the system is not yet fully resumed and the > >>>>>> controller driver is still partway through its own resume sequence. > >>>>>> > >>>>>> Decouple discovery from registration: add a reg_work work item to > >>>>>> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it > >>>>>> on master->wq (the freezable workqueue) instead of calling > >>>>>> i3c_master_register_new_i3c_devs() directly. The worker performs the > >>>>>> registration only when the controller is not shutting_down, and is > >>>>>> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is > >>>>>> freezable, any newly observed devices end up being registered after > >>>>>> the system has finished resuming. > >>>>>> > >>>>>> i3c_master_register() also routes its initial post-bus-init registration > >>>>>> through reg_work, using flush_work() to keep probe-time behavior > >>>>>> synchronous. This keeps a single registration code path and ensures the > >>>>>> worker is the only writer of desc->dev. > >>>>> > >>>>> why not direct use hj_work? > >>>> > >>>> i3c_master_register_new_i3c_devs() use of desc->dev is racy, so > >>>> i3c_master_register_new_i3c_devs() must not be allowed to race > >>>> with itself. Having it only ever run via reg_work achieves that. > >>> > >>> Sorry, I have not understand these, Can provide some detail? > >> > >> >From i3c_master_register_new_i3c_devs(): > >> > >> i3c_bus_for_each_i3cdev(&master->bus, desc) { > >> if (desc->dev || !desc->info.dyn_addr || desc == master->this) > >> continue; > >> > >> desc->dev = kzalloc_obj(*desc->dev); > >> ... > >> ret = device_register(&desc->dev->dev); > >> > >> This is done under the shared i3c_bus_normaluse_lock(), so there can > > > > i3c_bus_normaluse_lock() may is wrong, suppose it should be > > i3c_bus_maintenance_lock(), register new devices change i3c bus's > > hierarchical structure. > > If device_register() probes the device and the probe tries to > access the device, won't it deadlock if i3c_bus_maintenance_lock() > is held. Okay, Reviewed-by: Frank Li > > > > > Frank > > > >> be 2 or more instances of i3c_master_register_new_i3c_devs() running > >> at the same time. They might all see desc->dev is NULL and then all > >> of them try to initialize and register a dev for the same I3C device. > >> > From Frank.li at nxp.com Fri May 22 12:02:44 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:02:44 -0400 Subject: [PATCH 01/17] i3c: renesas: Check that the transfer is valid before accessing it In-Reply-To: <20260522101815.1722909-2-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-2-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:17:59PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > The Renesas I3C driver uses an asynchronous model to transfer data. It > prepares a struct renesas_i3c_xfer, enqueues it, and waits for completion. > The interrupt handler dequeues the transfer, updates/uses it, and signals > the waiting thread. > > If the completion times out, the waiting thread dequeues the transfer and > free it. If an interrupt fires after that, the handler may access freed > memory, leading to crashes. > > Check that the transfer is still valid before accessing it in the > interrupt handler. > > Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 17 +++++++++++++++++ > 1 file changed, 17 insertions(+) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index f39c449922ca..36e3ccbe66b0 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -1014,6 +1014,9 @@ static irqreturn_t renesas_i3c_tx_isr(int irq, void *data) > > scoped_guard(spinlock, &i3c->xferqueue.lock) { > xfer = i3c->xferqueue.cur; > + if (!xfer) > + return IRQ_HANDLED; > + > cmd = xfer->cmds; > > if (xfer->is_i2c_xfer) { > @@ -1054,6 +1057,9 @@ static irqreturn_t renesas_i3c_resp_isr(int irq, void *data) > > scoped_guard(spinlock, &i3c->xferqueue.lock) { > xfer = i3c->xferqueue.cur; > + if (!xfer) > + return IRQ_HANDLED; > + > cmd = xfer->cmds; > > /* Clear the Respone Queue Full status flag*/ > @@ -1138,6 +1144,9 @@ static irqreturn_t renesas_i3c_tend_isr(int irq, void *data) > > scoped_guard(spinlock, &i3c->xferqueue.lock) { > xfer = i3c->xferqueue.cur; > + if (!xfer) > + return IRQ_HANDLED; > + > cmd = xfer->cmds; > > if (xfer->is_i2c_xfer) { > @@ -1184,6 +1193,9 @@ static irqreturn_t renesas_i3c_rx_isr(int irq, void *data) > > scoped_guard(spinlock, &i3c->xferqueue.lock) { > xfer = i3c->xferqueue.cur; > + if (!xfer) > + return IRQ_HANDLED; > + > cmd = xfer->cmds; > > if (xfer->is_i2c_xfer) { > @@ -1235,6 +1247,8 @@ static irqreturn_t renesas_i3c_stop_isr(int irq, void *data) > > scoped_guard(spinlock, &i3c->xferqueue.lock) { > xfer = i3c->xferqueue.cur; > + if (!xfer) > + return IRQ_HANDLED; > > /* read back registers to confirm writes have fully propagated */ > renesas_writel(i3c->regs, BST, 0); > @@ -1259,6 +1273,9 @@ static irqreturn_t renesas_i3c_start_isr(int irq, void *data) > > scoped_guard(spinlock, &i3c->xferqueue.lock) { > xfer = i3c->xferqueue.cur; > + if (!xfer) > + return IRQ_HANDLED; > + > cmd = xfer->cmds; > > if (xfer->is_i2c_xfer) { > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:06:41 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:06:41 -0400 Subject: [PATCH 02/17] i3c: renesas: Use the divider 128 In-Reply-To: <20260522101815.1722909-3-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-3-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:00PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > The REFCKCTL.IREFCKS field is 3 bits wide, and setting it to 7 selects a > divider of 128 for the internal reference clock. Use this divider value. This doesnot reflect what your change, code add one more search for clks = 7 Frank > > Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- > drivers/i3c/master/renesas-i3c.c | 2 +- > 1 file changed, 1 insertion(+), 1 deletion(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 36e3ccbe66b0..1917549cf6d5 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -559,7 +559,7 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) > > i2c_parse_fw_timings(&m->dev, &t, true); > > - for (cks = 0; cks < 7; cks++) { > + for (cks = 0; cks <= 7; cks++) { > /* SCL low-period calculation in Open-drain mode */ > od_low_ticks = ((i2c_total_ticks * 6) / 10); > > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:10:15 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:10:15 -0400 Subject: [PATCH 03/17] i3c: renesas: Restore STDBR and EXTBR registers on resume In-Reply-To: <20260522101815.1722909-4-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-4-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:01PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > The Renesas RZ/G3S supports a power saving state where power to the most > SoC componentes (including I3C) is lost. > > The STDBR and EXTBR are configured in initialization phase though the > struct i3c_master_controller_ops::bus_init. Set them on resume function > as well to keep the same state of the controller after a suspend with > power loss and a similar initialization sequence as in bus_init. > > Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- > drivers/i3c/master/renesas-i3c.c | 10 ++++++---- > 1 file changed, 6 insertions(+), 4 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 1917549cf6d5..6c23f956ad2a 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -260,6 +260,7 @@ struct renesas_i3c { > u32 dyn_addr; > u32 i2c_STDBR; > u32 i3c_STDBR; > + u32 extbr; can you keep consisent with above 2 register, use upcase EXTBR, Frank > unsigned long rate; > u8 addrs[RENESAS_I3C_MAX_DEVS]; > struct renesas_i3c_xferqueue xferqueue; > @@ -607,10 +608,9 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) > renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); > > /* Extended Bit Rate setting */ > - renesas_writel(i3c->regs, EXTBR, EXTBR_EBRLO(od_low_ticks) | > - EXTBR_EBRHO(od_high_ticks) | > - EXTBR_EBRLP(pp_low_ticks) | > - EXTBR_EBRHP(pp_high_ticks)); > + i3c->extbr = EXTBR_EBRLO(od_low_ticks) | EXTBR_EBRHO(od_high_ticks) | > + EXTBR_EBRLP(pp_low_ticks) | EXTBR_EBRHP(pp_high_ticks); > + renesas_writel(i3c->regs, EXTBR, i3c->extbr); > > renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); > i3c->refclk_div = cks; > @@ -1447,6 +1447,8 @@ static int renesas_i3c_resume_noirq(struct device *dev) > goto err_tresetn; > > /* Re-store I3C registers value. */ > + renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); > + renesas_writel(i3c->regs, EXTBR, i3c->extbr); > renesas_writel(i3c->regs, REFCKCTL, > REFCKCTL_IREFCKS(i3c->refclk_div)); > renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYADV | > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:11:20 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:11:20 -0400 Subject: [PATCH 04/17] i3c: renesas: Follow the reset deassert order used in probe In-Reply-To: <20260522101815.1722909-5-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-5-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:02PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Use the same reset deassert order in the resume and probe paths to avoid > potential failures due to ordering differences. > > Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 12 ++++++------ > 1 file changed, 6 insertions(+), 6 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 6c23f956ad2a..d2f29ed0b6ed 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -1434,17 +1434,17 @@ static int renesas_i3c_resume_noirq(struct device *dev) > struct renesas_i3c *i3c = dev_get_drvdata(dev); > int i, ret; > > - ret = reset_control_deassert(i3c->presetn); > + ret = reset_control_deassert(i3c->tresetn); > if (ret) > return ret; > > - ret = reset_control_deassert(i3c->tresetn); > + ret = reset_control_deassert(i3c->presetn); > if (ret) > - goto err_presetn; > + goto err_tresetn; > > ret = clk_bulk_enable(i3c->num_clks, i3c->clks); > if (ret) > - goto err_tresetn; > + goto err_presetn; > > /* Re-store I3C registers value. */ > renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); > @@ -1465,10 +1465,10 @@ static int renesas_i3c_resume_noirq(struct device *dev) > > return 0; > > -err_tresetn: > - reset_control_assert(i3c->tresetn); > err_presetn: > reset_control_assert(i3c->presetn); > +err_tresetn: > + reset_control_assert(i3c->tresetn); > return ret; > } > > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:13:38 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:13:38 -0400 Subject: [PATCH 05/17] i3c: renesas: Fix re-attach In-Reply-To: <20260522101815.1722909-6-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-6-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:03PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea subject should descript what change possible candidate si "Reconfigure the DATBAS register on re-attach" Frank > > During re-attach, the device may change its position in the i3c->addrs[] > array. As a result, it may use a different Device Address Table Basic > Register (DATBAS), which needs to be reconfigured. > > Reconfigure the DATBAS register on re-attach. Along with it update > software caches. > > Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- > drivers/i3c/master/renesas-i3c.c | 18 ++++++++++++++++++ > 1 file changed, 18 insertions(+) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index d2f29ed0b6ed..5174a390d668 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -892,10 +892,28 @@ static int renesas_i3c_reattach_i3c_dev(struct i3c_dev_desc *dev, > struct i3c_master_controller *m = i3c_dev_get_master(dev); > struct renesas_i3c *i3c = to_renesas_i3c(m); > struct renesas_i3c_i2c_dev_data *data = i3c_dev_get_master_data(dev); > + int pos; > + > + pos = renesas_i3c_get_free_pos(i3c); > + if (pos < 0) > + return pos; > + > + if (data->index != pos) { > + renesas_writel(i3c->regs, DATBAS(data->index), 0); > + i3c->addrs[data->index] = 0; > + i3c->free_pos |= BIT(data->index); > + > + data->index = pos; > + i3c->free_pos &= ~BIT(data->index); > + } > > i3c->addrs[data->index] = dev->info.dyn_addr ? dev->info.dyn_addr : > dev->info.static_addr; > > + renesas_writel(i3c->regs, DATBAS(data->index), > + DATBAS_DVSTAD(dev->info.static_addr) | > + datbas_dvdyad_with_parity(i3c->addrs[data->index])); > + > return 0; > } > > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:15:05 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:15:05 -0400 Subject: [PATCH 06/17] i3c: renesas: Reset the controller on resume In-Reply-To: <20260522101815.1722909-7-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-7-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:04PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Reset the controller on resume after enabling the clocks to follow the > same sequence as in probe and avoid potential ordering related failures. > > Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- Can you move these similar stuff to one helper function to avoid duplicate efforts later? Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 6 ++++++ > 1 file changed, 6 insertions(+) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 5174a390d668..2f3c6ddf75c0 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -1464,6 +1464,10 @@ static int renesas_i3c_resume_noirq(struct device *dev) > if (ret) > goto err_presetn; > > + ret = renesas_i3c_reset(i3c); > + if (ret) > + goto err_clks_disable; > + > /* Re-store I3C registers value. */ > renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); > renesas_writel(i3c->regs, EXTBR, i3c->extbr); > @@ -1483,6 +1487,8 @@ static int renesas_i3c_resume_noirq(struct device *dev) > > return 0; > > +err_clks_disable: > + clk_bulk_disable(i3c->num_clks, i3c->clks); > err_presetn: > reset_control_assert(i3c->presetn); > err_tresetn: > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:16:55 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:16:55 -0400 Subject: [PATCH 07/17] i3c: renesas: Perform Dynamic Address Assignment on resume In-Reply-To: <20260522101815.1722909-8-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-8-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:05PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > The Renesas RZ/G3S SoC supports a power saving mode where power to most > SoC components, including I3C, is turned off. > > On systems where the I3C devices also loses power during suspend (e.g. NXP > P3T1085UK-ARD connected to the PMOD1_6A connector of the RZ SMARC Carrier > 2 + Renesas RZ/G3S SMARC SOM), the devices becomes unreachable after > resume. > > Running DAA in the controller resume path restores communication. However, > DAA relies on interrupts for TX/RX, which are not available in the noirq > suspend/resume phase (unless they are wakeup interrupts). For this, the > suspend/resume callbacks were moved out of the noirq phase. Currently, > there is no identified use case on either the Renesas RZ/G3S or Renesas > RZ/G3E SoCs that requires the controller suspend/resume hooks to be part of > the noirq suspend/resume phase. Can you refer https://lore.kernel.org/linux-i3c/20260512121732.406009-1-adrian.hunter at intel.com/T/#mafdc9631a2a18dfebfa5b5efcb8584d32bceba7f which defer DAA to workqueue. Frank > > Along with this, struct renesas_i3c::DATBASn and its usage were removed, > as they are no longer needed. > > Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- > drivers/i3c/master/renesas-i3c.c | 34 ++++++++++++-------------------- > 1 file changed, 13 insertions(+), 21 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 2f3c6ddf75c0..c009d0de6a2b 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -265,7 +265,6 @@ struct renesas_i3c { > u8 addrs[RENESAS_I3C_MAX_DEVS]; > struct renesas_i3c_xferqueue xferqueue; > void __iomem *regs; > - u32 *DATBASn; > struct clk_bulk_data *clks; > struct reset_control *presetn; > struct reset_control *tresetn; > @@ -1400,12 +1399,6 @@ static int renesas_i3c_probe(struct platform_device *pdev) > i3c->maxdevs = RENESAS_I3C_MAX_DEVS; > i3c->free_pos = GENMASK(i3c->maxdevs - 1, 0); > > - /* Allocate dynamic Device Address Table backup. */ > - i3c->DATBASn = devm_kzalloc(&pdev->dev, sizeof(u32) * i3c->maxdevs, > - GFP_KERNEL); > - if (!i3c->DATBASn) > - return -ENOMEM; > - > return i3c_master_register(&i3c->base, &pdev->dev, &renesas_i3c_ops, false); > } > > @@ -1416,17 +1409,13 @@ static void renesas_i3c_remove(struct platform_device *pdev) > i3c_master_unregister(&i3c->base); > } > > -static int renesas_i3c_suspend_noirq(struct device *dev) > +static int renesas_i3c_suspend(struct device *dev) > { > struct renesas_i3c *i3c = dev_get_drvdata(dev); > - int i, ret; > + int ret; > > i2c_mark_adapter_suspended(&i3c->base.i2c); > > - /* Store Device Address Table values. */ > - for (i = 0; i < i3c->maxdevs; i++) > - i3c->DATBASn[i] = renesas_readl(i3c->regs, DATBAS(i)); > - > ret = reset_control_assert(i3c->presetn); > if (ret) > goto err_mark_resumed; > @@ -1447,10 +1436,10 @@ static int renesas_i3c_suspend_noirq(struct device *dev) > return ret; > } > > -static int renesas_i3c_resume_noirq(struct device *dev) > +static int renesas_i3c_resume(struct device *dev) > { > struct renesas_i3c *i3c = dev_get_drvdata(dev); > - int i, ret; > + int ret; > > ret = reset_control_deassert(i3c->tresetn); > if (ret) > @@ -1476,15 +1465,19 @@ static int renesas_i3c_resume_noirq(struct device *dev) > renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYADV | > MSDVAD_MDYAD(i3c->dyn_addr)); > > - /* Restore Device Address Table values. */ > - for (i = 0; i < i3c->maxdevs; i++) > - renesas_writel(i3c->regs, DATBAS(i), i3c->DATBASn[i]); > - > /* I3C hw init. */ > renesas_i3c_hw_init(i3c); > > i2c_mark_adapter_resumed(&i3c->base.i2c); > > + ret = i3c_master_do_daa_ext(&i3c->base, true); > + if (ret) > + dev_err(dev, "DAA failed on resume, ret=%d", ret); > + > + /* > + * I3C devices may have retained their dynamic address anyway. Do not > + * fail the resume because of DAA error. > + */ > return 0; > > err_clks_disable: > @@ -1497,8 +1490,7 @@ static int renesas_i3c_resume_noirq(struct device *dev) > } > > static const struct dev_pm_ops renesas_i3c_pm_ops = { > - NOIRQ_SYSTEM_SLEEP_PM_OPS(renesas_i3c_suspend_noirq, > - renesas_i3c_resume_noirq) > + SYSTEM_SLEEP_PM_OPS(renesas_i3c_suspend, renesas_i3c_resume) > }; > > static const struct of_device_id renesas_i3c_of_ids[] = { > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:17:42 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:17:42 -0400 Subject: [PATCH 08/17] i3c: renesas: Clean DATBAS register on detach In-Reply-To: <20260522101815.1722909-9-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-9-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:06PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > The controller uses DATBAS registers on TX/RX logic. Clean the DATBAS > register for the detached I3C device to avoid issues. > > Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") > Cc: stable at vger.kernel.org > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 2 ++ > 1 file changed, 2 insertions(+) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index c009d0de6a2b..d32646deb69f 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -922,6 +922,8 @@ static void renesas_i3c_detach_i3c_dev(struct i3c_dev_desc *dev) > struct i3c_master_controller *m = i3c_dev_get_master(dev); > struct renesas_i3c *i3c = to_renesas_i3c(m); > > + renesas_writel(i3c->regs, DATBAS(data->index), 0); > + > i3c_dev_set_master_data(dev, NULL); > i3c->addrs[data->index] = 0; > i3c->free_pos |= BIT(data->index); > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:19:21 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:19:21 -0400 Subject: [PATCH 09/17] i3c: renesas: Use reset_control_bulk_{assert, deassert}() In-Reply-To: <20260522101815.1722909-10-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-10-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:07PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Use reset_control_bulk_assert() and reset_control_bulk_deassert() in the > suspend and resume paths to simplify the code. > > Signed-off-by: Claudiu Beznea > --- > drivers/i3c/master/renesas-i3c.c | 30 +++++++++++++----------------- > 1 file changed, 13 insertions(+), 17 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index d32646deb69f..e5963270d6e5 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -1414,24 +1414,22 @@ static void renesas_i3c_remove(struct platform_device *pdev) > static int renesas_i3c_suspend(struct device *dev) > { > struct renesas_i3c *i3c = dev_get_drvdata(dev); > + struct reset_control_bulk_data resets[] = { > + { .rstc = i3c->presetn }, > + { .rstc = i3c->tresetn }, > + }; Suppose it is also used in probe funciton. can move these into renesas_i3c Frank > int ret; > > i2c_mark_adapter_suspended(&i3c->base.i2c); > > - ret = reset_control_assert(i3c->presetn); > + ret = reset_control_bulk_assert(ARRAY_SIZE(resets), resets); > if (ret) > goto err_mark_resumed; > > - ret = reset_control_assert(i3c->tresetn); > - if (ret) > - goto err_presetn; > - > clk_bulk_disable(i3c->num_clks, i3c->clks); > > return 0; > > -err_presetn: > - reset_control_deassert(i3c->presetn); > err_mark_resumed: > i2c_mark_adapter_resumed(&i3c->base.i2c); > > @@ -1441,19 +1439,19 @@ static int renesas_i3c_suspend(struct device *dev) > static int renesas_i3c_resume(struct device *dev) > { > struct renesas_i3c *i3c = dev_get_drvdata(dev); > + struct reset_control_bulk_data resets[] = { > + { .rstc = i3c->presetn }, > + { .rstc = i3c->tresetn }, > + }; > int ret; > > - ret = reset_control_deassert(i3c->tresetn); > + ret = reset_control_bulk_deassert(ARRAY_SIZE(resets), resets); > if (ret) > return ret; > > - ret = reset_control_deassert(i3c->presetn); > - if (ret) > - goto err_tresetn; > - > ret = clk_bulk_enable(i3c->num_clks, i3c->clks); > if (ret) > - goto err_presetn; > + goto err_resets_asserted; > > ret = renesas_i3c_reset(i3c); > if (ret) > @@ -1484,10 +1482,8 @@ static int renesas_i3c_resume(struct device *dev) > > err_clks_disable: > clk_bulk_disable(i3c->num_clks, i3c->clks); > -err_presetn: > - reset_control_assert(i3c->presetn); > -err_tresetn: > - reset_control_assert(i3c->tresetn); > +err_resets_asserted: > + reset_control_bulk_assert(ARRAY_SIZE(resets), resets); > return ret; > } > > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:20:39 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:20:39 -0400 Subject: [PATCH 10/17] i3c: renesas: Return immediately if there is nothing to transfer In-Reply-To: <20260522101815.1722909-11-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-11-claudiu.beznea@kernel.org> Message-ID: subject: Return immediately if nothing transfer Frank On Fri, May 22, 2026 at 01:18:08PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > There is no need to allocate a transfer structure when i2c_nxfers is zero. > Return immediately instead of unnecessarily allocating memory. > > Signed-off-by: Claudiu Beznea > --- > drivers/i3c/master/renesas-i3c.c | 6 +++--- > 1 file changed, 3 insertions(+), 3 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index e5963270d6e5..de75125eb013 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -940,13 +940,13 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, > u8 start_bit = CNDCTL_STCND; > int i; > > + if (!i2c_nxfers) > + return 0; > + > struct renesas_i3c_xfer *xfer __free(kfree) = renesas_i3c_alloc_xfer(i3c, 1); > if (!xfer) > return -ENOMEM; > > - if (!i2c_nxfers) > - return 0; > - > renesas_i3c_bus_enable(m, false); > > init_completion(&xfer->comp); > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:21:48 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:21:48 -0400 Subject: [PATCH 11/17] i3c: renesas: Follow a unified pattern for transfer and command initialization In-Reply-To: <20260522101815.1722909-12-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-12-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:09PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Follow a unified pattern for transfer and command initialization across > the driver. This keeps the code cleaner and easier to follow. Also, in > some cases the I3C device was enabled before the transfer data structure > was even allocated. > > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 22 +++++++++++----------- > 1 file changed, 11 insertions(+), 11 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index de75125eb013..12bf4797a70d 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -648,6 +648,10 @@ static int renesas_i3c_daa(struct i3c_master_controller *m) > if (!xfer) > return -ENOMEM; > > + init_completion(&xfer->comp); > + cmd = xfer->cmds; > + cmd->rx_count = 0; > + > /* Enable I3C bus. */ > renesas_i3c_bus_enable(m, true); > > @@ -669,10 +673,6 @@ static int renesas_i3c_daa(struct i3c_master_controller *m) > renesas_writel(i3c->regs, DATBAS(pos), datbas_dvdyad_with_parity(ret)); > } > > - init_completion(&xfer->comp); > - cmd = xfer->cmds; > - cmd->rx_count = 0; > - > ret = renesas_i3c_get_free_pos(i3c); > if (ret < 0) > return ret; > @@ -760,13 +760,13 @@ static int renesas_i3c_send_ccc_cmd(struct i3c_master_controller *m, > if (!xfer) > return -ENOMEM; > > - renesas_i3c_bus_enable(m, true); > - > init_completion(&xfer->comp); > cmd = xfer->cmds; > cmd->rnw = ccc->rnw; > cmd->cmd0 = 0; > > + renesas_i3c_bus_enable(m, true); > + > /* Calculate the command descriptor. */ > switch (ccc->id) { > case I3C_CCC_SETDASA: > @@ -816,15 +816,15 @@ static int renesas_i3c_i3c_xfers(struct i3c_dev_desc *dev, struct i3c_xfer *i3c_ > struct renesas_i3c_i2c_dev_data *data = i3c_dev_get_master_data(dev); > int i; > > - /* Enable I3C bus. */ > - renesas_i3c_bus_enable(m, true); > - > struct renesas_i3c_xfer *xfer __free(kfree) = renesas_i3c_alloc_xfer(i3c, 1); > if (!xfer) > return -ENOMEM; > > init_completion(&xfer->comp); > > + /* Enable I3C bus. */ > + renesas_i3c_bus_enable(m, true); > + > for (i = 0; i < i3c_nxfers; i++) { > struct renesas_i3c_cmd *cmd = xfer->cmds; > > @@ -947,12 +947,12 @@ static int renesas_i3c_i2c_xfers(struct i2c_dev_desc *dev, > if (!xfer) > return -ENOMEM; > > - renesas_i3c_bus_enable(m, false); > - > init_completion(&xfer->comp); > xfer->is_i2c_xfer = true; > cmd = xfer->cmds; > > + renesas_i3c_bus_enable(m, false); > + > if (!(renesas_readl(i3c->regs, BCST) & BCST_BFREF)) { > cmd->err = -EBUSY; > return cmd->err; > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:43:05 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:43:05 -0400 Subject: [PATCH 12/17] i3c: renesas: Drop the explicit memset() call In-Reply-To: <20260522101815.1722909-13-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-13-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:10PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Drop the explicit memset() call on struct i3c_device_info object, as it is > already initialized at declaration through compiler initialization. > > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 1 - > 1 file changed, 1 deletion(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 12bf4797a70d..865e67ac0fd2 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -624,7 +624,6 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) > i3c->dyn_addr = ret; > renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYAD(ret) | MSDVAD_MDYADV); > > - memset(&info, 0, sizeof(info)); > info.dyn_addr = ret; > return i3c_master_set_info(&i3c->base, &info); > } > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:48:21 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:48:21 -0400 Subject: [PATCH 13/17] i3c: renesas: Update HW registers after SW computations are done In-Reply-To: <20260522101815.1722909-14-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-14-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:11PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > renesas_i3c_bus_init() performs a number of computations and software > cache updates, interleaving them with hardware register writes. While > this works today, it makes it harder to minimize the time the controller > must remain powered when runtime PM is introduced. > > Perform all software computations and cache updates first, then update > the hardware registers. This prepares for future runtime PM support. > > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 28 ++++++++++++++-------------- > 1 file changed, 14 insertions(+), 14 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 865e67ac0fd2..631c9c5d8038 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -550,10 +550,6 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) > if (!i3c->rate) > return -EINVAL; > > - ret = renesas_i3c_reset(i3c); > - if (ret) > - return ret; > - > i2c_total_ticks = DIV_ROUND_UP(i3c->rate, bus->scl_rate.i2c); > i3c_total_ticks = DIV_ROUND_UP(i3c->rate, bus->scl_rate.i3c); > > @@ -604,27 +600,31 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) > STDBR_SBRHO(double_SBR, od_high_ticks) | > STDBR_SBRLP(pp_low_ticks) | > STDBR_SBRHP(pp_high_ticks); > - renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); > > /* Extended Bit Rate setting */ > i3c->extbr = EXTBR_EBRLO(od_low_ticks) | EXTBR_EBRHO(od_high_ticks) | > EXTBR_EBRLP(pp_low_ticks) | EXTBR_EBRHP(pp_high_ticks); > - renesas_writel(i3c->regs, EXTBR, i3c->extbr); > - > - renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); > - i3c->refclk_div = cks; > - > - /* I3C hw init*/ > - renesas_i3c_hw_init(i3c); > > ret = i3c_master_get_free_addr(m, 0); > if (ret < 0) > return ret; > > + info.dyn_addr = ret; > i3c->dyn_addr = ret; > - renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYAD(ret) | MSDVAD_MDYADV); > + i3c->refclk_div = cks; > + > + ret = renesas_i3c_reset(i3c); > + if (ret) > + return ret; > + > + renesas_writel(i3c->regs, STDBR, i3c->i3c_STDBR); > + renesas_writel(i3c->regs, EXTBR, i3c->extbr); > + renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); > + renesas_writel(i3c->regs, MSDVAD, MSDVAD_MDYAD(i3c->dyn_addr) | MSDVAD_MDYADV); > + > + /* I3C hw init*/ > + renesas_i3c_hw_init(i3c); > > - info.dyn_addr = ret; > return i3c_master_set_info(&i3c->base, &info); > } > > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:50:25 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:50:25 -0400 Subject: [PATCH 14/17] i3c: renesas: Organize structures to avoid unnecessary padding In-Reply-To: <20260522101815.1722909-15-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-15-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:12PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Reorder structure members to reduce padding and improve memory layout. > > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 28 ++++++++++++++-------------- > 1 file changed, 14 insertions(+), 14 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 631c9c5d8038..5614ed99553c 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -221,19 +221,19 @@ enum renesas_i3c_event { > }; > > struct renesas_i3c_cmd { > + const void *tx_buf; > + void *rx_buf; > + /* i2c xfer */ > + u8 *i2c_buf; > + const struct i2c_msg *msg; > + int i2c_bytes_left; > + int i2c_is_last; > u32 cmd0; > u32 len; > - const void *tx_buf; > u32 tx_count; > - void *rx_buf; > u32 rx_count; > u32 err; > u8 rnw; > - /* i2c xfer */ > - int i2c_bytes_left; > - int i2c_is_last; > - u8 *i2c_buf; > - const struct i2c_msg *msg; > }; > > struct renesas_i3c_xfer { > @@ -253,21 +253,21 @@ struct renesas_i3c_xferqueue { > }; > > struct renesas_i3c { > + void __iomem *regs; > + struct clk_bulk_data *clks; > + struct reset_control *presetn; > + struct reset_control *tresetn; > + struct renesas_i3c_xferqueue xferqueue; > struct i3c_master_controller base; > + unsigned long rate; > enum i3c_internal_state internal_state; > - u16 maxdevs; > u32 free_pos; > u32 dyn_addr; > u32 i2c_STDBR; > u32 i3c_STDBR; > u32 extbr; > - unsigned long rate; > + u16 maxdevs; > u8 addrs[RENESAS_I3C_MAX_DEVS]; > - struct renesas_i3c_xferqueue xferqueue; > - void __iomem *regs; > - struct clk_bulk_data *clks; > - struct reset_control *presetn; > - struct reset_control *tresetn; > u8 num_clks; > u8 refclk_div; > }; > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:51:48 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:51:48 -0400 Subject: [PATCH 15/17] i3c: renesas: Use the "dev_name:irq_name" format for the interrupt name In-Reply-To: <20260522101815.1722909-16-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-16-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:13PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Use the "dev_name:irq_name" format for the interrupt names. This makes it > easier to identify interrupts in systems where multiple devices may request > interrupts with the same name. > > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 9 ++++++++- > 1 file changed, 8 insertions(+), 1 deletion(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index 5614ed99553c..e6e05ac03082 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -1385,12 +1385,19 @@ static int renesas_i3c_probe(struct platform_device *pdev) > return ret; > > for (i = 0; i < ARRAY_SIZE(renesas_i3c_irqs); i++) { > + const char *irqname; > + > ret = platform_get_irq_byname(pdev, renesas_i3c_irqs[i].name); > if (ret < 0) > return ret; > > + irqname = devm_kasprintf(&pdev->dev, GFP_KERNEL, "%s:%s", dev_name(&pdev->dev), > + renesas_i3c_irqs[i].desc); > + if (!irqname) > + return -ENOMEM; > + > ret = devm_request_irq(&pdev->dev, ret, renesas_i3c_irqs[i].isr, > - 0, renesas_i3c_irqs[i].desc, i3c); > + 0, irqname, i3c); > if (ret) > return ret; > } > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 12:52:25 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 15:52:25 -0400 Subject: [PATCH 16/17] i3c: renesas: Drop unnecessary tab In-Reply-To: <20260522101815.1722909-17-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-17-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:14PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > Remove an unnecessary tab to make the code cleaner. > > Signed-off-by: Claudiu Beznea > --- Reviewed-by: Frank Li > drivers/i3c/master/renesas-i3c.c | 2 +- > 1 file changed, 1 insertion(+), 1 deletion(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index e6e05ac03082..a070db4d2440 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -109,7 +109,7 @@ > #define NCMDQP_DATA_LENGTH(x) FIELD_PREP(GENMASK(31, 16), x) > > #define NRSPQP 0x154 /* Normal Respone Queue */ > -#define NRSPQP_NO_ERROR 0 > +#define NRSPQP_NO_ERROR 0 > #define NRSPQP_ERROR_CRC 1 > #define NRSPQP_ERROR_PARITY 2 > #define NRSPQP_ERROR_FRAME 3 > -- > 2.43.0 > From Frank.li at nxp.com Fri May 22 13:01:57 2026 From: Frank.li at nxp.com (Frank Li) Date: Fri, 22 May 2026 16:01:57 -0400 Subject: [PATCH 17/17] i3c: renesas: Add runtime PM support In-Reply-To: <20260522101815.1722909-18-claudiu.beznea@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-18-claudiu.beznea@kernel.org> Message-ID: On Fri, May 22, 2026 at 01:18:15PM +0300, Claudiu Beznea wrote: > From: Claudiu Beznea > > On the SoCs where the Renesas I3C driver is enabled (RZ/G3S and RZ/G3E), > the clocks of the IP are managed through a clock PM domain. To keep the > I3C code simpler, the explicit clock handling was dropped along with the > addition of runtime PM support, in favor of the runtime PM APIs. Only the > code for getting tclk was preserved, as it is necessary to compute the > I3C clock rate. > > All the APIs provided to the I3C subsystem through struct > i3c_master_controller_ops are guarded with runtime PM APIs to > enable/disable the controller at runtime. > > As the Renesas I3C driver implements an asynchronous transmit model by > preparing a transfer and waiting for its completion through the ISR, > renesas_i3c_abort_xfer() was added to disable interrupts and synchronize > IRQs before runtime suspending the controller. For this, the interrupts > were saved in struct renesas_i3c::irqs. Along with this, > renesas_i3c_wait_xfer() return type was changed to unsigned long. > > Along with the clocks, the controller pin configuration is changed > through the provided "sleep" pin configuration. > > Add runtime PM support for the Renesas I3C driver. > > Signed-off-by: Claudiu Beznea > --- > drivers/i3c/master/renesas-i3c.c | 183 ++++++++++++++++++++++++++----- > 1 file changed, 156 insertions(+), 27 deletions(-) > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > index a070db4d2440..3b9807a89b54 100644 > --- a/drivers/i3c/master/renesas-i3c.c > +++ b/drivers/i3c/master/renesas-i3c.c > @@ -21,7 +21,9 @@ ... > static int renesas_i3c_probe(struct platform_device *pdev) > { > struct renesas_i3c *i3c; > @@ -1360,12 +1448,21 @@ static int renesas_i3c_probe(struct platform_device *pdev) > if (IS_ERR(i3c->regs)) > return PTR_ERR(i3c->regs); > > - ret = devm_clk_bulk_get_all_enabled(&pdev->dev, &i3c->clks); > - if (ret <= RENESAS_I3C_TCLK_IDX) > - return dev_err_probe(&pdev->dev, ret < 0 ? ret : -EINVAL, > - "Failed to get clocks (need > %d, got %d)\n", > - RENESAS_I3C_TCLK_IDX, ret); > - i3c->num_clks = ret; you can still use devm_clk_bulk_get_all(), if need tclk, you iterate clks to find 'tclk', in case in future, need more clocks than tcls. > + i3c->tclk = devm_clk_get(&pdev->dev, "tclk"); > + if (IS_ERR(i3c->tclk)) > + return dev_err_probe(&pdev->dev, PTR_ERR(i3c->tclk), "Failed to get tclk"); > + > + i3c->dev = &pdev->dev; > + pm_runtime_set_autosuspend_delay(&pdev->dev, 300); > + pm_runtime_use_autosuspend(&pdev->dev); > + ret = devm_add_action_or_reset(&pdev->dev, renesas_i3c_dont_use_autosuspend, > + i3c->dev); do you cleanup resource in renesas_i3c_dont_use_autosuspend(), look likes needn't it. > + if (ret) > + return ret; > + > + ret = devm_pm_runtime_enable(&pdev->dev); > + if (ret) > + return ret; > ... > > +static int renesas_i3c_runtime_suspend(struct device *dev) > +{ > + return pinctrl_pm_select_sleep_state(dev); Only change pin state, don't disable clock? Frank > +} > + > +static int renesas_i3c_runtime_resume(struct device *dev) > +{ > + return pinctrl_pm_select_default_state(dev); > +} > + > static const struct dev_pm_ops renesas_i3c_pm_ops = { > + RUNTIME_PM_OPS(renesas_i3c_runtime_suspend, renesas_i3c_runtime_resume, NULL) > SYSTEM_SLEEP_PM_OPS(renesas_i3c_suspend, renesas_i3c_resume) > }; > > -- > 2.43.0 > From samagazaryan at google.com Fri May 22 15:37:35 2026 From: samagazaryan at google.com (Sam Agazaryan) Date: Fri, 22 May 2026 15:37:35 -0700 Subject: [RFC] mipi-i3c-hci: Support for DMA Ring Pipelining / High-throughput Streaming Message-ID: Hello all, I am working on a project using the mipi-i3c-hci driver that involves large packet bursts (exceeding the physical hardware DMA ring size, such as large MCTP-over-I3C payloads). I noticed that the current driver implementation treats these as discrete batches. I am considering implementing a ring pipelining or DMA streaming mechanism to allow for asynchronous refills while the ring is running. This would leverage the standard ENQ_PTR doorbell mechanism (per MIPI HCI v1.2, Section 6.8.2) to continuously feed the hardware. I figured in that case it may be worth while to see how the upstream community feels about this feature. Before I dive into the implementation for upstream, I wanted to check: 1. Is there any existing work or a roadmap for DMA streaming/pipelining in the HCI driver? 2. Is a generic dma streaming mechanism for large transfers something you would be interested in seeing as a contribution to the mainline driver? Currently, my proof-of-concept handles the pipelining at the core transfer level, but I suspect for a generic upstream implementation, this sliding window logic should be moved into dma.c to properly support controllers with multiple Ring Bundles. I would appreciate any thoughts on this approach or the preferred architectural placement. Best regards, Sam From claudiu.beznea at kernel.org Sat May 23 01:14:22 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Sat, 23 May 2026 11:14:22 +0300 Subject: [PATCH 02/17] i3c: renesas: Use the divider 128 In-Reply-To: References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-3-claudiu.beznea@kernel.org> Message-ID: <67bd230e-df9a-47fa-bdd7-d8b1e2ff1649@kernel.org> On 5/22/26 22:06, Frank Li wrote: > On Fri, May 22, 2026 at 01:18:00PM +0300, Claudiu Beznea wrote: >> From: Claudiu Beznea >> >> The REFCKCTL.IREFCKS field is 3 bits wide, and setting it to 7 selects a >> divider of 128 for the internal reference clock. Use this divider value. > > This doesnot reflect what your change, code add one more search for clks = 7 cks is later written in the renesas_i3c_bus_init() to the REFCKCTL.IREFCKS. The following lines are from the renesas_i3c_bus_init() function: renesas_writel(i3c->regs, REFCKCTL, REFCKCTL_IREFCKS(cks)); i3c->refclk_div = cks; > > Frank > >> >> Fixes: d028219a9f14 ("i3c: master: Add basic driver for the Renesas I3C controller") >> Cc: stable at vger.kernel.org >> Signed-off-by: Claudiu Beznea >> --- >> drivers/i3c/master/renesas-i3c.c | 2 +- >> 1 file changed, 1 insertion(+), 1 deletion(-) >> >> diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c >> index 36e3ccbe66b0..1917549cf6d5 100644 >> --- a/drivers/i3c/master/renesas-i3c.c >> +++ b/drivers/i3c/master/renesas-i3c.c >> @@ -559,7 +559,7 @@ static int renesas_i3c_bus_init(struct i3c_master_controller *m) >> >> i2c_parse_fw_timings(&m->dev, &t, true); >> >> - for (cks = 0; cks < 7; cks++) { >> + for (cks = 0; cks <= 7; cks++) { >> /* SCL low-period calculation in Open-drain mode */ >> od_low_ticks = ((i2c_total_ticks * 6) / 10); >> >> -- >> 2.43.0 >> -- Thank you, Claudiu From claudiu.beznea at kernel.org Sat May 23 03:23:33 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Sat, 23 May 2026 13:23:33 +0300 Subject: [PATCH 17/17] i3c: renesas: Add runtime PM support In-Reply-To: References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-18-claudiu.beznea@kernel.org> Message-ID: <8f1cd05b-b866-47ac-8ef4-e5e607247cf7@kernel.org> On 5/22/26 23:01, Frank Li wrote: > On Fri, May 22, 2026 at 01:18:15PM +0300, Claudiu Beznea wrote: >> From: Claudiu Beznea >> >> On the SoCs where the Renesas I3C driver is enabled (RZ/G3S and RZ/G3E), >> the clocks of the IP are managed through a clock PM domain. To keep the >> I3C code simpler, the explicit clock handling was dropped along with the >> addition of runtime PM support, in favor of the runtime PM APIs. Only the >> code for getting tclk was preserved, as it is necessary to compute the >> I3C clock rate. >> >> All the APIs provided to the I3C subsystem through struct >> i3c_master_controller_ops are guarded with runtime PM APIs to >> enable/disable the controller at runtime. >> >> As the Renesas I3C driver implements an asynchronous transmit model by >> preparing a transfer and waiting for its completion through the ISR, >> renesas_i3c_abort_xfer() was added to disable interrupts and synchronize >> IRQs before runtime suspending the controller. For this, the interrupts >> were saved in struct renesas_i3c::irqs. Along with this, >> renesas_i3c_wait_xfer() return type was changed to unsigned long. >> >> Along with the clocks, the controller pin configuration is changed >> through the provided "sleep" pin configuration. >> >> Add runtime PM support for the Renesas I3C driver. >> >> Signed-off-by: Claudiu Beznea >> --- >> drivers/i3c/master/renesas-i3c.c | 183 ++++++++++++++++++++++++++----- >> 1 file changed, 156 insertions(+), 27 deletions(-) >> >> diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c >> index a070db4d2440..3b9807a89b54 100644 >> --- a/drivers/i3c/master/renesas-i3c.c >> +++ b/drivers/i3c/master/renesas-i3c.c >> @@ -21,7 +21,9 @@ > ... >> static int renesas_i3c_probe(struct platform_device *pdev) >> { >> struct renesas_i3c *i3c; >> @@ -1360,12 +1448,21 @@ static int renesas_i3c_probe(struct platform_device *pdev) >> if (IS_ERR(i3c->regs)) >> return PTR_ERR(i3c->regs); >> >> - ret = devm_clk_bulk_get_all_enabled(&pdev->dev, &i3c->clks); >> - if (ret <= RENESAS_I3C_TCLK_IDX) >> - return dev_err_probe(&pdev->dev, ret < 0 ? ret : -EINVAL, >> - "Failed to get clocks (need > %d, got %d)\n", >> - RENESAS_I3C_TCLK_IDX, ret); >> - i3c->num_clks = ret; > > you can still use devm_clk_bulk_get_all(), if need tclk, you iterate clks > to find 'tclk', in case in future, need more clocks than tcls. Indeed, but as they are not needed in the current driver form, I would prefer to drop handling both of them as the final code looks simpler. As the clocks are enabled/disabled through PM domain, getting rid of devm_clk_bulk_get_all_enabled() simplifies the code, FMPOV, it drops 1 pointer and 1 int. So, I would like to keep it like this if all OK. Also, if I'm not wrong, Geert (in cc) prefers having the clock controlled directly through power domains if that is possible. I concluded that from: commit d303ce595cac Author: Geert Uytterhoeven Date: Wed Mar 20 11:30:03 2019 +0100 i2c: riic: Add Runtime PM support - Replace explicit clock handling by Runtime PM calls, - Streamline Runtime PM handling in error paths, - Enable Runtime PM in .probe(), - Disable Runtime PM in .remove(), - Make sure the device is runtime-resumed when disabling interrupts in .remove(). Signed-off-by: Geert Uytterhoeven Reviewed-by: Niklas S?derlund Tested-by: Chris Brandt Signed-off-by: Wolfram Sang > >> + i3c->tclk = devm_clk_get(&pdev->dev, "tclk"); >> + if (IS_ERR(i3c->tclk)) >> + return dev_err_probe(&pdev->dev, PTR_ERR(i3c->tclk), "Failed to get tclk"); >> + >> + i3c->dev = &pdev->dev; >> + pm_runtime_set_autosuspend_delay(&pdev->dev, 300); >> + pm_runtime_use_autosuspend(&pdev->dev); >> + ret = devm_add_action_or_reset(&pdev->dev, renesas_i3c_dont_use_autosuspend, >> + i3c->dev); > > do you cleanup resource in renesas_i3c_dont_use_autosuspend(), look likes > needn't it. According to documentation at [1] this is necessary. [1] https://elixir.bootlin.com/linux/v7.1-rc4/source/Documentation/power/runtime_pm.rst#L616 > >> + if (ret) >> + return ret; >> + >> + ret = devm_pm_runtime_enable(&pdev->dev); >> + if (ret) >> + return ret; >> > ... >> >> +static int renesas_i3c_runtime_suspend(struct device *dev) >> +{ >> + return pinctrl_pm_select_sleep_state(dev); > > Only change pin state, don't disable clock? Clocks are handled though the I3C PM domain. On the Renesas SoCs, where this driver is enabled, clocks are controlled though clock PM domains. Every runtime PM suspend/resume will call clk_disable()/clk_enable() though the PM domains. -- Thank you, Claudiu From claudiu.beznea at kernel.org Sat May 23 03:24:15 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Sat, 23 May 2026 13:24:15 +0300 Subject: [PATCH 06/17] i3c: renesas: Reset the controller on resume In-Reply-To: References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-7-claudiu.beznea@kernel.org> Message-ID: On 5/22/26 22:15, Frank Li wrote: > On Fri, May 22, 2026 at 01:18:04PM +0300, Claudiu Beznea wrote: >> From: Claudiu Beznea >> >> Reset the controller on resume after enabling the clocks to follow the >> same sequence as in probe and avoid potential ordering related failures. >> >> Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") >> Cc: stable at vger.kernel.org >> Signed-off-by: Claudiu Beznea >> --- > > Can you move these similar stuff to one helper function to avoid duplicate > efforts later? If you are talking about moving also the control of the reset signals in the renesas_i3c_reset() I can do that, but, FMPOV, it will complicate the code, especially the initialization and failure paths (see the above diff built on top of this series). Moving the reset de-assert in the renesas_i3c_reset() will involve calling functions to assert back the resets in case of failure. FMPOV, that is a bit unbalanced (wrt the way the code looks) because we are calling deassert in one function and assert in another function. It is a bit difficult to follow. Please see the above diff and let me know your thoughts. diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c index 3b9807a89b54..5f45a024aa54 100644 --- a/drivers/i3c/master/renesas-i3c.c +++ b/drivers/i3c/master/renesas-i3c.c @@ -255,8 +255,7 @@ struct renesas_i3c_xferqueue { struct renesas_i3c { void __iomem *regs; struct clk *tclk; - struct reset_control *presetn; - struct reset_control *tresetn; + struct reset_control_bulk_data *resets; struct device *dev; int *irqs; struct renesas_i3c_xferqueue xferqueue; @@ -264,6 +263,7 @@ struct renesas_i3c { unsigned long rate; unsigned int num_irqs; enum i3c_internal_state internal_state; + u32 num_resets; u32 free_pos; u32 dyn_addr; u32 i2c_STDBR; @@ -492,16 +492,28 @@ static int renesas_i3c_reset(struct renesas_i3c *i3c) u32 val; int ret; + ret = reset_control_bulk_deassert(i3c->num_resets, i3c->resets); + if (ret) + return ret; + PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); ret = PM_RUNTIME_ACQUIRE_ERR(&pm); if (ret) - return ret; + goto assert; renesas_writel(i3c->regs, BCTL, 0); renesas_set_bit(i3c->regs, RSTCTL, RSTCTL_RI3CRST); - return read_poll_timeout(renesas_readl, val, !(val & RSTCTL_RI3CRST), - 0, 1000, false, i3c->regs, RSTCTL); + ret = read_poll_timeout(renesas_readl, val, !(val & RSTCTL_RI3CRST), + 0, 1000, false, i3c->regs, RSTCTL); + if (ret) + goto assert; + + return 0; + +assert: + reset_control_bulk_assert(i3c->num_resets, i3c->resets); + return ret; } static void renesas_i3c_hw_init(struct renesas_i3c *i3c) @@ -1430,11 +1442,20 @@ static const struct renesas_i3c_irq_desc renesas_i3c_irqs[] = { { .name = "nack", .isr = renesas_i3c_tend_isr, .desc = "i3c-nack" }, }; +static const char * const renesas_i3c_resets[] = { "tresetn", "presetn" }; + static void renesas_i3c_dont_use_autosuspend(void *data) { pm_runtime_dont_use_autosuspend(data); } +static void renesas_i3c_resets_assert(void *data) +{ + struct renesas_i3c *i3c = data; + + reset_control_bulk_assert(i3c->num_resets, i3c->resets); +} + static int renesas_i3c_probe(struct platform_device *pdev) { struct renesas_i3c *i3c; @@ -1464,15 +1485,20 @@ static int renesas_i3c_probe(struct platform_device *pdev) if (ret) return ret; - i3c->tresetn = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, "tresetn"); - if (IS_ERR(i3c->tresetn)) - return dev_err_probe(&pdev->dev, PTR_ERR(i3c->tresetn), - "Error: missing tresetn ctrl\n"); + i3c->num_resets = ARRAY_SIZE(renesas_i3c_resets); + i3c->resets = devm_kmalloc_array(&pdev->dev, i3c->num_resets, + sizeof(*i3c->resets), GFP_KERNEL); + if (!i3c->resets) + return -ENOMEM; - i3c->presetn = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, "presetn"); - if (IS_ERR(i3c->presetn)) - return dev_err_probe(&pdev->dev, PTR_ERR(i3c->presetn), - "Error: missing presetn ctrl\n"); + for (unsigned int i = 0; i < i3c->num_resets; i++) + i3c->resets[i].id = renesas_i3c_resets[i]; + + ret = devm_reset_control_bulk_get_optional_exclusive(&pdev->dev, + i3c->num_resets, + i3c->resets); + if (ret) + return ret; spin_lock_init(&i3c->xferqueue.lock); INIT_LIST_HEAD(&i3c->xferqueue.list); @@ -1481,6 +1507,11 @@ static int renesas_i3c_probe(struct platform_device *pdev) if (ret) return ret; + /* Add devm action for resets deasserted in renesas_i3c_reset(). */ + ret = devm_add_action_or_reset(&pdev->dev, renesas_i3c_resets_assert, NULL); + if (ret) + return ret; + i3c->num_irqs = ARRAY_SIZE(renesas_i3c_irqs); i3c->irqs = devm_kcalloc(&pdev->dev, i3c->num_irqs, sizeof(*i3c->irqs), GFP_KERNEL); if (!i3c->irqs) @@ -1523,15 +1554,11 @@ static void renesas_i3c_remove(struct platform_device *pdev) static int renesas_i3c_suspend(struct device *dev) { struct renesas_i3c *i3c = dev_get_drvdata(dev); - struct reset_control_bulk_data resets[] = { - { .rstc = i3c->presetn }, - { .rstc = i3c->tresetn }, - }; int ret; i2c_mark_adapter_suspended(&i3c->base.i2c); - ret = reset_control_bulk_assert(ARRAY_SIZE(resets), resets); + ret = reset_control_bulk_assert(i3c->num_resets, i3c->resets); if (ret) goto err_mark_resumed; @@ -1542,7 +1569,7 @@ static int renesas_i3c_suspend(struct device *dev) return 0; err_resets_deassert: - reset_control_bulk_deassert(ARRAY_SIZE(resets), resets); + reset_control_bulk_deassert(i3c->num_resets, i3c->resets); err_mark_resumed: i2c_mark_adapter_resumed(&i3c->base.i2c); @@ -1552,23 +1579,15 @@ static int renesas_i3c_suspend(struct device *dev) static int renesas_i3c_resume(struct device *dev) { struct renesas_i3c *i3c = dev_get_drvdata(dev); - struct reset_control_bulk_data resets[] = { - { .rstc = i3c->presetn }, - { .rstc = i3c->tresetn }, - }; int ret; ret = pm_runtime_force_resume(dev); if (ret) return ret; - ret = reset_control_bulk_deassert(ARRAY_SIZE(resets), resets); - if (ret) - return ret; - ret = renesas_i3c_reset(i3c); if (ret) - goto err_resets_asserted; + return ret; PM_RUNTIME_ACQUIRE_IF_ENABLED_AUTOSUSPEND(i3c->dev, pm); ret = PM_RUNTIME_ACQUIRE_ERR(&pm); @@ -1607,7 +1626,7 @@ static int renesas_i3c_resume(struct device *dev) * if a runtime path is triggered after a failed resume). Checked on * RZ/G3S. */ - reset_control_bulk_assert(ARRAY_SIZE(resets), resets); + reset_control_bulk_assert(i3c->num_resets, i3c->resets); return ret; } -- Thank you, Claudiu From claudiu.beznea at kernel.org Sat May 23 03:26:09 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Sat, 23 May 2026 13:26:09 +0300 Subject: [PATCH 07/17] i3c: renesas: Perform Dynamic Address Assignment on resume In-Reply-To: References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-8-claudiu.beznea@kernel.org> Message-ID: <4a82fe83-338b-46c0-b783-836256a28858@kernel.org> On 5/22/26 22:16, Frank Li wrote: > On Fri, May 22, 2026 at 01:18:05PM +0300, Claudiu Beznea wrote: >> From: Claudiu Beznea >> >> The Renesas RZ/G3S SoC supports a power saving mode where power to most >> SoC components, including I3C, is turned off. >> >> On systems where the I3C devices also loses power during suspend (e.g. NXP >> P3T1085UK-ARD connected to the PMOD1_6A connector of the RZ SMARC Carrier >> 2 + Renesas RZ/G3S SMARC SOM), the devices becomes unreachable after >> resume. >> >> Running DAA in the controller resume path restores communication. However, >> DAA relies on interrupts for TX/RX, which are not available in the noirq >> suspend/resume phase (unless they are wakeup interrupts). For this, the >> suspend/resume callbacks were moved out of the noirq phase. Currently, >> there is no identified use case on either the Renesas RZ/G3S or Renesas >> RZ/G3E SoCs that requires the controller suspend/resume hooks to be part of >> the noirq suspend/resume phase. > Can you referhttps://lore.kernel.org/linux-i3c/20260512121732.406009-1- > adrian.hunter at intel.com/T/#mafdc9631a2a18dfebfa5b5efcb8584d32bceba7f > > which defer DAA to workqueue. > I've reviewed this series and tested it. Tests passed on my side. According to the following diff from patch 6/8 [1]: /** * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) * @master: controller @@ -1878,9 +1889,7 @@ int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) if (ret) goto out; - i3c_bus_normaluse_lock(&master->bus); - i3c_master_register_new_i3c_devs(master); - i3c_bus_normaluse_unlock(&master->bus); + queue_work(master->wq, &master->reg_work); out: i3c_master_rpm_put(master); only the registration of the new devices is deferred. The RSTDAA command is still sent according to the following code in i3c_master_do_daa_ext(): // ... if (master->shutting_down) { ret = -ENODEV; } else { if (rstdaa) rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); ret = master->ops->do_daa(master); } // ... which is what fixed the communication with the I3C devices I used in my testing, in resume case. If I remove the i3c_master_do_daa_ext() call from renesas_i3c_resume() then the I3C devices are not working anymore after resume on my setup. Also, the i3c_master_do_daa_ext() call in i3c_hci_resume_common() remains unchanged [2] in series [1]. So, could you please let me know if I misunderstood your comment and if there is anything that should be done for this patch? [1] https://lore.kernel.org/all/20260512121732.406009-7-adrian.hunter at intel.com/ [2] https://elixir.bootlin.com/linux/v7.1-rc4/source/drivers/i3c/master/mipi-i3c-hci/core.c#L848 -- Thank you, Claudiu From claudiu.beznea at kernel.org Sat May 23 03:26:51 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Sat, 23 May 2026 13:26:51 +0300 Subject: [PATCH 09/17] i3c: renesas: Use reset_control_bulk_{assert, deassert}() In-Reply-To: References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-10-claudiu.beznea@kernel.org> Message-ID: <05db9476-7f26-4d90-b79c-5a4fa1de0f01@kernel.org> On 5/22/26 22:19, Frank Li wrote: > On Fri, May 22, 2026 at 01:18:07PM +0300, Claudiu Beznea wrote: >> From: Claudiu Beznea >> >> Use reset_control_bulk_assert() and reset_control_bulk_deassert() in the >> suspend and resume paths to simplify the code. >> >> Signed-off-by: Claudiu Beznea >> --- >> drivers/i3c/master/renesas-i3c.c | 30 +++++++++++++----------------- >> 1 file changed, 13 insertions(+), 17 deletions(-) >> >> diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c >> index d32646deb69f..e5963270d6e5 100644 >> --- a/drivers/i3c/master/renesas-i3c.c >> +++ b/drivers/i3c/master/renesas-i3c.c >> @@ -1414,24 +1414,22 @@ static void renesas_i3c_remove(struct platform_device *pdev) >> static int renesas_i3c_suspend(struct device *dev) >> { >> struct renesas_i3c *i3c = dev_get_drvdata(dev); >> + struct reset_control_bulk_data resets[] = { >> + { .rstc = i3c->presetn }, >> + { .rstc = i3c->tresetn }, >> + }; > > Suppose it is also used in probe funciton. can move these into renesas_i3c As explained in patch 06/07 is either this way or complicate other code paths. Please let me know the preferred approach. -- Thank you, Claudiu From Frank.li at nxp.com Sat May 23 07:47:59 2026 From: Frank.li at nxp.com (Frank Li) Date: Sat, 23 May 2026 10:47:59 -0400 Subject: [RFC] mipi-i3c-hci: Support for DMA Ring Pipelining / High-throughput Streaming In-Reply-To: References: Message-ID: On Fri, May 22, 2026 at 03:37:35PM -0700, Sam Agazaryan wrote: > Hello all, > > I am working on a project using the mipi-i3c-hci driver that involves > large packet bursts (exceeding the physical hardware DMA ring size, > such as large MCTP-over-I3C payloads). How large to exceed DMA ring size, can you increase ring size? And if large transfer, it will defer IBI handle for while. IBI check only happen at every START phase. > > I noticed that the current driver implementation treats these as > discrete batches. > > I am considering implementing a ring pipelining or DMA streaming > mechanism to allow for asynchronous refills while the ring is running. > This would leverage the > standard ENQ_PTR doorbell mechanism (per MIPI HCI v1.2, Section 6.8.2) > to continuously feed the hardware. I figured in that case it may be > worth while to see how the upstream community feels about this > feature. > > Before I dive into the implementation for upstream, I wanted to check: > 1. Is there any existing work or a roadmap for DMA > streaming/pipelining in the HCI driver? > 2. Is a generic dma streaming mechanism for large transfers something > you would be interested in seeing as a contribution to the mainline > driver? Usb\network\storage is async. The I3C's framework is sync. > > Currently, my proof-of-concept handles the pipelining at the core > transfer level, but I Can you send your patch as RFC to check what you already did? > suspect for a generic upstream implementation, this sliding window > logic should be moved into dma.c to properly support controllers with > multiple Ring Bundles. > > I would appreciate any thoughts on this approach or the preferred > architectural placement. > > Best regards, > Sam > > -- > linux-i3c mailing list > linux-i3c at lists.infradead.org > http://lists.infradead.org/mailman/listinfo/linux-i3c From krzk at kernel.org Sun May 24 11:41:37 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Sun, 24 May 2026 20:41:37 +0200 Subject: [PATCH v3 1/4] i3c: dw: Remove core reset "_rst" suffix In-Reply-To: References: <20260519055105.13079-1-jszhang@kernel.org> <20260519055105.13079-2-jszhang@kernel.org> <20260520-scrupulous-notorious-ibis-ab6cbc@quoll> Message-ID: <05b2547c-535c-4beb-882f-d424f135acc4@kernel.org> On 20/05/2026 13:43, Jisheng Zhang wrote: > On Wed, May 20, 2026 at 09:13:58AM +0200, Krzysztof Kozlowski wrote: >> On Tue, May 19, 2026 at 01:51:02PM +0800, Jisheng Zhang wrote: >>> It's redundant. This suffix has been in the code from day1, fortunately >>> there's no such dt property usage in all dw i3c users after grepping all >>> dts files, so we can remove it. >> >> Hm, how could you grep all 3rd party / out of tree users of this? >> >>> >>> Signed-off-by: Jisheng Zhang >>> --- >>> drivers/i3c/master/dw-i3c-master.c | 2 +- >>> 1 file changed, 1 insertion(+), 1 deletion(-) >>> >>> diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c >>> index 655693a2187e..c4a848cc978a 100644 >>> --- a/drivers/i3c/master/dw-i3c-master.c >>> +++ b/drivers/i3c/master/dw-i3c-master.c >>> @@ -1587,7 +1587,7 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, >>> return PTR_ERR(master->pclk); >>> >>> master->core_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, >>> - "core_rst"); >>> + "core"); >> >> ABI impact for something released since 2018. Cleanup of name is not >> really worth affecting users. core_rst is not the best name but it is >> not incorrect, either. >> > Hmm make sense. > Two questions: if the ABI is introduced but never used by any intree > users, is modifying the ABI taken as "ABI breakage"? Yes. The out of tree user was working before. Now it is not working. How is it not a "breakage"? > > if the ABI is only used by outtree users, can we modify the ABI? Yes and no. Depends. It's up to platform maintainer and you need reasons. The reasons MUST be documented. See writing-bindings. I do not see here a good reason. Best regards, Krzysztof From lakshay.piplani at nxp.com Sun May 24 23:42:00 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:00 +0530 Subject: [PATCH v10 0/9] Add support for NXP P3H2x4x I3C hub driver Message-ID: <20260525064209.2263045-1-lakshay.piplani@nxp.com> This series adds a driver for the NXP P3H2x4x family of multiport I3C hub devices. This is an MFD driver integrating I3C hub and on-die regulators. The series introduces: - Core I3C master enhancements required for hub support - Generic I3C hub framework - MFD driver with regulator and I3C hub child drivers for the P3H2x4x I3C hub Changes in v10: - Rename i3c_master_reattach_i3c_dev() to *_locked to reflect required bus locking - Rename i3c_master_direct_attach_i3c_dev() and i3c_master_direct_detach_i3c_dev() to *_locked, as these APIs must be called with the bus lock held for write - Drop redundant is_p3h2x4x_in_i3c flag from p3h2840.h - Remove unnecessary ibi_lock handling in request/enable/disable/free IBI APIs - Remove redundant parent pointer from struct i3c_hub and derive upstream master from hub_dev - Split SMBus target/slave mode support, including IBI and MCTP receive handling, into a separate patch - Link to v9: https://lore.kernel.org/linux-i3c/20260420105222.1562243-1-lakshay.piplani at nxp.com/T/#u Changes in v9: - Renamed macros to follow consistent uppercase naming conventions - Made REGMAP selects in the P3H2X4X MFD Kconfig conditional, to avoid I3C/I2C dependency issues - Referenced i3c.yaml and i2c-controller.yaml for child bus nodes - Dropped unnecessary #address-cells and #size-cells from child nodes - Added CONFIG_I2C_SLAVE guards where necessary to avoid build errors when I2C slave support is disabled - Link to v8: https://lore.kernel.org/linux-i3c/20260323062737.886728-1-lakshay.piplani at nxp.com/T/#u Changes in v8: - Add compatible in i3c example - Link to v7: https://lore.kernel.org/linux-i3c/20260319112441.3888957-1-lakshay.piplani at nxp.com/T/#u Changes in v7: - Fix kernel-doc warnings across I3C core and hub code - Rework DT binding schema and examples to pass dt_binding_check - Update MFD Kconfig to use I3C_OR_I2C - Convert CONFIG_I3C_HUB to tristate - Remove unnecessary CONFIG_I2C_SLAVE guards - Replace custom helpers with find_closest() - Use devm_regulator_get_enable_optional() - Link to v6: https://lore.kernel.org/linux-i3c/64c5070c-aa9e-427a-933e-91e168f0510c at kernel.org/T/#u Changes in v6: - Update DT binding with vendor-prefixed properties - Add generic I3C hub support - Remove generic code from P3H2x4x driver - Link to v5: https://lore.kernel.org/linux-i3c/20260206120121.856471-1-aman.kumarpandey at nxp.com/T/#u Changes in v5: - Update supply naming and descriptions - Improve MFD Kconfig/Makefile ordering - Link to v4: https://lore.kernel.org/linux-i3c/20260113114529.1692213-2-aman.kumarpandey at nxp.com/T/#u Changes in v4: - Split driver into MFD, regulator and I3C hub parts - Update I3C master for hub support - Fix DT binding issues - Link to v3: https://lore.kernel.org/linux-i3c/20250811-bittern-of-abstract-prestige-aaeda9 at kuoka/T/#u Changes in v3: - Add MFD support for hub and regulators - Add regulator integration - Link to v2: https://lore.kernel.org/linux-i3c/17145d2f-5d07-4939-8381-74e27cde303c at kernel.org/T/#u Changes in v2: - Fix DT binding warnings - Refine DT parsing logic - Link to v1: https://lore.kernel.org/linux-i3c/822d6dca-b2c6-4439-ade5-219620ebc435 at kernel.org/T/#u Aman Kumar Pandey (6): i3c: master: Expose the APIs to support I3C hub i3c: master: Add APIs for I3C hub support dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality Lakshay Piplani (3): i3c: master: rename i3c_master_reattach_i3c_dev() to *_locked i3c: hub: Add support for the I3C interface in the I3C hub i3c: hub: p3h2x4x: Add SMBus slave mode support .../devicetree/bindings/i3c/nxp,p3h2840.yaml | 291 +++++++++++ MAINTAINERS | 15 + drivers/i3c/Kconfig | 16 + drivers/i3c/Makefile | 2 + drivers/i3c/hub.c | 465 ++++++++++++++++++ drivers/i3c/hub/Kconfig | 11 + drivers/i3c/hub/Makefile | 4 + drivers/i3c/hub/p3h2840_i3c_hub.h | 337 +++++++++++++ drivers/i3c/hub/p3h2840_i3c_hub_common.c | 353 +++++++++++++ drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 141 ++++++ drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 450 +++++++++++++++++ drivers/i3c/master.c | 179 ++++++- drivers/mfd/Kconfig | 13 + drivers/mfd/Makefile | 1 + drivers/mfd/p3h2840.c | 122 +++++ drivers/regulator/Kconfig | 10 + drivers/regulator/Makefile | 1 + drivers/regulator/p3h2840_i3c_hub_regulator.c | 218 ++++++++ include/linux/i3c/device.h | 1 + include/linux/i3c/hub.h | 99 ++++ include/linux/i3c/master.h | 9 + include/linux/mfd/p3h2840.h | 26 + 22 files changed, 2761 insertions(+), 3 deletions(-) create mode 100644 Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml create mode 100644 drivers/i3c/hub.c create mode 100644 drivers/i3c/hub/Kconfig create mode 100644 drivers/i3c/hub/Makefile create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub.h create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_common.c create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_i3c.c create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_smbus.c create mode 100644 drivers/mfd/p3h2840.c create mode 100644 drivers/regulator/p3h2840_i3c_hub_regulator.c create mode 100644 include/linux/i3c/hub.h create mode 100644 include/linux/mfd/p3h2840.h -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:01 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:01 +0530 Subject: [PATCH v10 1/9] i3c: master: rename i3c_master_reattach_i3c_dev() to *_locked In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-2-lakshay.piplani@nxp.com> Rename i3c_master_reattach_i3c_dev() to *_locked() to make the locking requirement explicit and consistent with other I3C core helpers that require the bus lock to be held by the caller. Signed-off-by: Lakshay Piplani --- Changes in v10: - Rename i3c_master_reattach_i3c_dev() to *_locked to reflect required bus locking --- --- drivers/i3c/master.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 5cd4e5da2233..e89d73508b9a 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1652,8 +1652,8 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master, return 0; } -static int i3c_master_reattach_i3c_dev(struct i3c_dev_desc *dev, - u8 old_dyn_addr) +static int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev, + u8 old_dyn_addr) { struct i3c_master_controller *master = i3c_dev_get_master(dev); int ret; @@ -1742,7 +1742,7 @@ static int i3c_master_early_i3c_dev_add(struct i3c_master_controller *master, goto err_detach_dev; i3cdev->info.dyn_addr = i3cdev->boardinfo->init_dyn_addr; - ret = i3c_master_reattach_i3c_dev(i3cdev, 0); + ret = i3c_master_reattach_i3c_dev_locked(i3cdev, 0); if (ret) goto err_rstdaa; @@ -2358,7 +2358,7 @@ int i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, if (!ret) { old_dyn_addr = newdev->info.dyn_addr; newdev->info.dyn_addr = expected_dyn_addr; - i3c_master_reattach_i3c_dev(newdev, old_dyn_addr); + i3c_master_reattach_i3c_dev_locked(newdev, old_dyn_addr); } else { dev_err(&master->dev, "Failed to assign reserved/old address to device %d%llx", -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:02 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:02 +0530 Subject: [PATCH v10 2/9] i3c: master: Expose the APIs to support I3C hub In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-3-lakshay.piplani@nxp.com> From: Aman Kumar Pandey Change the below internal static functions to APIs to allow new I3C hub driver to use them 1) i3c_dev_enable_ibi_locked() 2) i3c_dev_disable_ibi_locked() 3) i3c_dev_request_ibi_locked() 4) i3c_dev_free_ibi_locked() 5) i3c_master_reattach_i3c_dev_locked() Signed-off-by: Aman Kumar Pandey Signed-off-by: Lakshay Piplani --- Changes in v10: - Expose the renamed reattach API. Changes in v9: - No change Changes in v8: - No change Changes in v7: - Fix kernel-doc warnings for *_locked() APIs - Clarify API exposure in commit message Changes in v6: - Split the patch into two parts: 1) expose the existing API 2) add new APIs. Changes in v5: - No change Changes in v4: - Updated I3C master to handle hub support --- --- drivers/i3c/master.c | 70 ++++++++++++++++++++++++++++++++++++-- include/linux/i3c/master.h | 2 ++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index e89d73508b9a..0636e3e21758 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1652,8 +1652,23 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master, return 0; } -static int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev, - u8 old_dyn_addr) +/** + * i3c_master_reattach_i3c_dev_locked() - reattach an I3C device with a new address + * @dev: I3C device descriptor to reattach + * @old_dyn_addr: previous dynamic address of the device + * + * This function reattaches an existing I3C device to the bus when its dynamic + * address has changed. It updates the bus address slot status accordingly: + * - Marks the new dynamic address as occupied by an I3C device. + * - Frees the old dynamic address slot if applicable. + * + * This function must be called with the bus lock held in write mode. + * + * Return: 0 on success, or a negative error code if reattachment fails + * (e.g. -EBUSY if the new address slot is not free). + */ +int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev, + u8 old_dyn_addr) { struct i3c_master_controller *master = i3c_dev_get_master(dev); int ret; @@ -1677,6 +1692,7 @@ static int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev, return 0; } +EXPORT_SYMBOL_GPL(i3c_master_reattach_i3c_dev_locked); static void i3c_master_detach_i3c_dev(struct i3c_dev_desc *dev) { @@ -3195,6 +3211,16 @@ int i3c_dev_do_xfers_locked(struct i3c_dev_desc *dev, struct i3c_xfer *xfers, return master->ops->i3c_xfers(dev, xfers, nxfers, mode); } +/** + * i3c_dev_disable_ibi_locked() - Disable IBIs coming from a specific device + * @dev: device on which IBIs should be disabled + * + * This function disable IBIs coming from a specific device and wait for + * all pending IBIs to be processed. + * + * Context: Must be called with mutex_lock(&dev->desc->ibi_lock) held. + * Return: 0 in case of success, a negative error core otherwise. + */ int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev) { struct i3c_master_controller *master; @@ -3216,7 +3242,22 @@ int i3c_dev_disable_ibi_locked(struct i3c_dev_desc *dev) return 0; } +EXPORT_SYMBOL_GPL(i3c_dev_disable_ibi_locked); +/** + * i3c_dev_enable_ibi_locked() - Enable IBIs from a specific device (lock held) + * @dev: device on which IBIs should be enabled + * + * This function enable IBIs coming from a specific device and wait for + * all pending IBIs to be processed. This should be called on a device + * where i3c_device_request_ibi() has succeeded. + * + * Note that IBIs from this device might be received before this function + * returns to its caller. + * + * Context: Must be called with mutex_lock(&dev->desc->ibi_lock) held. + * Return: 0 on success, or a negative error code on failure. + */ int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev) { struct i3c_master_controller *master = i3c_dev_get_master(dev); @@ -3231,7 +3272,20 @@ int i3c_dev_enable_ibi_locked(struct i3c_dev_desc *dev) return ret; } +EXPORT_SYMBOL_GPL(i3c_dev_enable_ibi_locked); +/** + * i3c_dev_request_ibi_locked() - Request an IBI + * @dev: device for which we should enable IBIs + * @req: setup requested for this IBI + * + * This function is responsible for pre-allocating all resources needed to + * process IBIs coming from @dev. When this function returns, the IBI is not + * enabled until i3c_device_enable_ibi() is called. + * + * Context: Must be called with mutex_lock(&dev->desc->ibi_lock) held. + * Return: 0 in case of success, a negative error core otherwise. + */ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev, const struct i3c_ibi_setup *req) { @@ -3270,7 +3324,18 @@ int i3c_dev_request_ibi_locked(struct i3c_dev_desc *dev, return ret; } +EXPORT_SYMBOL_GPL(i3c_dev_request_ibi_locked); +/** + * i3c_dev_free_ibi_locked() - Free all resources needed for IBI handling + * @dev: device on which you want to release IBI resources + * + * This function is responsible for de-allocating resources previously + * allocated by i3c_device_request_ibi(). It should be called after disabling + * IBIs with i3c_device_disable_ibi(). + * + * Context: Must be called with mutex_lock(&dev->desc->ibi_lock) held. + */ void i3c_dev_free_ibi_locked(struct i3c_dev_desc *dev) { struct i3c_master_controller *master = i3c_dev_get_master(dev); @@ -3301,6 +3366,7 @@ void i3c_dev_free_ibi_locked(struct i3c_dev_desc *dev) kfree(dev->ibi); dev->ibi = NULL; } +EXPORT_SYMBOL_GPL(i3c_dev_free_ibi_locked); static int __init i3c_init(void) { diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 592b646f6134..355e9b3d9ae3 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -613,6 +613,8 @@ void i3c_master_dma_unmap_single(struct i3c_dma *dma_xfer); DEFINE_FREE(i3c_master_dma_unmap_single, void *, if (_T) i3c_master_dma_unmap_single(_T)) +int i3c_master_reattach_i3c_dev_locked(struct i3c_dev_desc *dev, + u8 old_dyn_addr); int i3c_master_set_info(struct i3c_master_controller *master, const struct i3c_device_info *info); -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:03 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:03 +0530 Subject: [PATCH v10 3/9] i3c: master: Add APIs for I3C hub support In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-4-lakshay.piplani@nxp.com> From: Aman Kumar Pandey Add helpers for attaching and detaching I3C devices and CCC helpers to check CCC support and send CCC commands. These additions prepare for I3C hub support. The attach and detach helpers must be called with the bus lock held in write mode. 1) i3c_master_direct_attach_i3c_dev_locked() 2) i3c_master_direct_detach_i3c_dev_locked() 3) i3c_master_send_ccc_cmd() 4) i3c_master_supports_ccc_cmd() Signed-off-by: Aman Kumar Pandey Signed-off-by: Lakshay Piplani --- Changes in v10: - Rename i3c_master_direct_attach_i3c_dev and i3c_master_direct_detach_i3c_dev APIs to *_locked, as these APIs must be called with the bus lock held in write mode Changes in v9: - No change Changes in v8: - No change Changes in v7: - Update commit message to clarify purpose (prepare for I3C hub support) Changes in v6: - Split the patch into two parts: 1) expose the existing API 2) add new APIs. --- --- drivers/i3c/master.c | 107 +++++++++++++++++++++++++++++++++++++ include/linux/i3c/master.h | 7 +++ 2 files changed, 114 insertions(+) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 0636e3e21758..4e659435df72 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1652,6 +1652,63 @@ static int i3c_master_attach_i3c_dev(struct i3c_master_controller *master, return 0; } +/** + * i3c_master_direct_attach_i3c_dev_locked() - attach an I3C device to a master + * @master: I3C master controller to attach the device to + * @dev: I3C device descriptor representing the device + * + * This function attaches an I3C device to its master controller once the + * device has a valid address on the bus. Devices without an assigned address + * are ignored. The master device itself is never attached through this bus. + * + * Context: Caller must hold master->bus.lock in write mode. + * + * Return: 0 on success, or a negative error code if the attach operation + * fails in the master controller driver. + */ +int i3c_master_direct_attach_i3c_dev_locked(struct i3c_master_controller *master, + struct i3c_dev_desc *dev) +{ + int ret = 0; + + /* + * We don't attach devices to the controller until they are + * addressable on the bus. + */ + + if (!dev->info.static_addr && !dev->info.dyn_addr) + return -EINVAL; + + /* Do not attach the master device itself. */ + if (master->this != dev && master->ops->attach_i3c_dev) + ret = master->ops->attach_i3c_dev(dev); + + return ret; +} +EXPORT_SYMBOL_GPL(i3c_master_direct_attach_i3c_dev_locked); + +/** + * i3c_master_direct_detach_i3c_dev_locked() - Detach an I3C device from its + * master controller. + * @dev: I3C device descriptor to be detached + * + * This function detaches an I3C device from its master controller. + * It ensures that the master itself is not detached. If the device is not + * the master and the master controller provides a detach operation, + * the detach callback is invoked to perform the actual removal. + * + * Context: Caller must hold master->bus.lock in write mode. + */ +void i3c_master_direct_detach_i3c_dev_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_direct_detach_i3c_dev_locked); + /** * i3c_master_reattach_i3c_dev_locked() - reattach an I3C device with a new address * @dev: I3C device descriptor to reattach @@ -1816,6 +1873,56 @@ i3c_master_register_new_i3c_devs(struct i3c_master_controller *master) } } +/** + * i3c_master_supports_ccc_cmd() - check CCC command support + * @master: I3C master controller + * @cmd: CCC command to verify + * + * This function verifies whether the given I3C master controller supports + * the specified Common Command Code (CCC). + * + * Return: 0 if the CCC command is supported and executed successfully, + * -EINVAL if arguments are invalid, + * -EOPNOTSUPP if the master does not support CCC commands, + * or another negative error code from the master's operation. + */ +int i3c_master_supports_ccc_cmd(struct i3c_master_controller *master, + const struct i3c_ccc_cmd *cmd) +{ + if (!cmd || !master) + return -EINVAL; + + if (!master->ops->supports_ccc_cmd) + return -EOPNOTSUPP; + + 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; + + i3c_bus_maintenance_lock(&master->bus); + ret = i3c_master_send_ccc_cmd_locked(master, cmd); + i3c_bus_maintenance_unlock(&master->bus); + + return ret; +} +EXPORT_SYMBOL_GPL(i3c_master_send_ccc_cmd); + /** * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) * @master: controller diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index 355e9b3d9ae3..1829f2e08bfb 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -615,6 +615,13 @@ 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_direct_attach_i3c_dev_locked(struct i3c_master_controller *master, + struct i3c_dev_desc *dev); +void i3c_master_direct_detach_i3c_dev_locked(struct i3c_dev_desc *dev); +int i3c_master_send_ccc_cmd(struct i3c_master_controller *master, + struct i3c_ccc_cmd *cmd); +int 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); -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:04 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:04 +0530 Subject: [PATCH v10 4/9] dt-bindings: i3c: Add NXP P3H2x4x i3c-hub support In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-5-lakshay.piplani@nxp.com> From: Aman Kumar Pandey Add bindings for the NXP P3H2x4x (P3H2440/P3H2441/P3H2840/P3H2841) multiport I3C hub family. These devices connect to a host via I3C/I2C/SMBus and allow communication with multiple downstream peripherals. Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Rob Herring (Arm) --- Changes in v10: - No change, added Reviewed-By tag Changes in v9: - Referenced i3c.yaml and i2c-controller.yaml for child nodes - Dropped unnecessary #address-cells and #size-cells from child nodes Changes in v8: - Add compatible in i3c example Changes in v7: - Fix schema validation issues - Adjust required properties - Add I2C example Changes in v6: - Use a vendor prefix for the attributes Changes in v5: - Removed SW properties: cp0-ldo-microvolt,cp1-ldo-microvolt, tp0145-ldo-microvolt, tp2367-ldo-microvolt - Changed supply entries and its descriptions Changes in v4: - Fixed DT binding check warning - Removed SW properties: ibi-enable, local-dev, and always-enable Changes in v3: - Added MFD (Multi-Function Device) support for I3C hub and on-die regulator - Added Regulator supply node Changes in v2: - Fixed DT binding check warning - Revised logic for parsing DTS nodes --- --- .../devicetree/bindings/i3c/nxp,p3h2840.yaml | 291 ++++++++++++++++++ MAINTAINERS | 9 + 2 files changed, 300 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..c080eeb0eeaa --- /dev/null +++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml @@ -0,0 +1,291 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +# Copyright 2025 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 I3C/I2C Target Port. + 4. P3H2440/ P3H2441 are 4 port I3C hub with four I3C/I2C Target Port. + Target ports can be configured as I2C/SMBus, I3C or GPIO and connect to + peripherals. + +properties: + compatible: + 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 + +patternProperties: + "^i3c@[0-7]$": + type: object + $ref: /schemas/i3c/i3c.yaml# + unevaluatedProperties: false + + properties: + reg: + description: + The I3C HUB Target Port number. + maximum: 7 + + nxp,pullup-enable: + type: boolean + description: + Enables the on-die pull-up for Target Port. + + required: + - reg + + "^(i2c|smbus)@[0-7]$": + type: object + $ref: /schemas/i2c/i2c-controller.yaml# + unevaluatedProperties: false + + properties: + reg: + description: + The I3C HUB Target Port number. + maximum: 7 + + nxp,pullup-enable: + type: boolean + description: + Enables the on-die pull-up for Target Port. + + required: + - reg + +unevaluatedProperties: false + +examples: + - | + i3c { + #address-cells = <3>; + #size-cells = <0>; + + hub at 70,236153000c2 { + reg = <0x70 0x236 0x3000c2>; + compatible = "nxp,p3h2840"; + #address-cells = <1>; + #size-cells = <0>; + assigned-address = <0x50>; + + nxp,tp0145-pullup-ohms = <1000>; + nxp,tp2367-pullup-ohms = <1000>; + nxp,cp0-io-strength-ohms = <50>; + nxp,cp1-io-strength-ohms = <50>; + nxp,tp0145-io-strength-ohms = <50>; + nxp,tp2367-io-strength-ohms = <50>; + vcc3-supply = <®_tpg0>; + vcc4-supply = <®_tpg1>; + + regulators { + reg_cp0: ldo-cp0 { + regulator-name = "ldo-cp0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_cp1: ldo-cp1 { + regulator-name = "ldo-cp1"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_tpg0: ldo-tpg0 { + regulator-name = "ldo-tpg0"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + + reg_tpg1: ldo-tpg1 { + regulator-name = "ldo-tpg1"; + regulator-min-microvolt = <1800000>; + regulator-max-microvolt = <1800000>; + }; + }; + + smbus at 0 { + reg = <0x0>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + + i2c at 1 { + reg = <0x1>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + + i3c at 2 { + reg = <0x2>; + #address-cells = <3>; + #size-cells = <0>; + nxp,pullup-enable; + }; + }; + }; + + - | + i2c { + #address-cells = <1>; + #size-cells = <0>; + + hub at 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 at 0 { + reg = <0x0>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + + i2c at 1 { + reg = <0x1>; + #address-cells = <1>; + #size-cells = <0>; + nxp,pullup-enable; + }; + }; + }; diff --git a/MAINTAINERS b/MAINTAINERS index c2c6d79275c6..8ae231f67460 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19303,6 +19303,15 @@ S: Maintained F: Documentation/devicetree/bindings/ptp/nxp,ptp-netc.yaml F: drivers/ptp/ptp_netc.c +NXP P3H2X4X I3C-HUB DRIVER +M: Vikash Bansal +M: Aman Kumar Pandey +M: Lakshay Piplani +L: linux-kernel at vger.kernel.org +L: linux-i3c-owner at 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 -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:05 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:05 +0530 Subject: [PATCH v10 5/9] mfd: p3h2x4x: Add driver for NXP P3H2x4x i3c hub and on-die regulator In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-6-lakshay.piplani@nxp.com> From: Aman Kumar Pandey Add core MFD support for the NXP P3H2x4x (P3H2440/P3H2441/P3H2840/P3H2841) family of multiport I3C hub devices. These devices connect to a host via I3C/I2C/SMBus and expose multiple downstream target ports. Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani --- Changes in v10: - Drop redundant is_p3h2x4x_in_i3c flag Changes in v9: - Renamed macros to follow consistent uppercase naming conventions - Made REGMAP selects in the P3H2X4X MFD Kconfig conditional, to avoid I3C/I2C dependency issues Changes in v8: - No change Changes in v7: - Use new config I3C_OR_I2C Changes in v6: - No change Changes in v5: - Corrected the ordering in the Makefile and Kconfig for MFD_P3H2X4X - Updated dev_err_probe() for regmap_init failure. - Updated module description Changes in v4: - Split the driver into three separate patches(mfd, regulator and I3C hub) - Added support for NXP P3H2x4x MFD functionality --- --- MAINTAINERS | 2 + drivers/mfd/Kconfig | 13 ++++ drivers/mfd/Makefile | 1 + drivers/mfd/p3h2840.c | 122 ++++++++++++++++++++++++++++++++++++ include/linux/i3c/device.h | 1 + include/linux/mfd/p3h2840.h | 26 ++++++++ 6 files changed, 165 insertions(+) create mode 100644 drivers/mfd/p3h2840.c create mode 100644 include/linux/mfd/p3h2840.h diff --git a/MAINTAINERS b/MAINTAINERS index 8ae231f67460..25e578dd74dd 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19311,6 +19311,8 @@ L: linux-kernel at vger.kernel.org L: linux-i3c-owner at lists.infradead.org S: Maintained F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml +F: drivers/mfd/p3h2840.c +F: include/linux/mfd/p3h2840.h NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER M: Woodrow Douglass diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig index 7192c9d1d268..405b50c3c77b 100644 --- a/drivers/mfd/Kconfig +++ b/drivers/mfd/Kconfig @@ -617,6 +617,19 @@ config MFD_MX25_TSADC i.MX25 processors. They consist of a conversion queue for general purpose ADC and a queue for Touchscreens. +config MFD_P3H2X4X + tristate "NXP P3H2X4X I3C Hub Device" + depends on I3C_OR_I2C + select MFD_CORE + select REGMAP_I3C if I3C + select REGMAP_I2C if I2C + help + Enable Support for NXP P3H244x/P3H284x I3C HUB device using I3C/I2C + communication interface. + + This driver provides support for I3C hub and regulator, each subdriver + can be enabled independently depending on the required functionality. + config MFD_PF1550 tristate "NXP PF1550 PMIC Support" depends on I2C=y && OF diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile index e75e8045c28a..a284b22c7b13 100644 --- a/drivers/mfd/Makefile +++ b/drivers/mfd/Makefile @@ -122,6 +122,7 @@ obj-$(CONFIG_MFD_MC13XXX) += mc13xxx-core.o obj-$(CONFIG_MFD_MC13XXX_SPI) += mc13xxx-spi.o obj-$(CONFIG_MFD_MC13XXX_I2C) += mc13xxx-i2c.o +obj-$(CONFIG_MFD_P3H2X4X) += p3h2840.o obj-$(CONFIG_MFD_PF1550) += pf1550.o obj-$(CONFIG_MFD_NCT6694) += nct6694.o diff --git a/drivers/mfd/p3h2840.c b/drivers/mfd/p3h2840.c new file mode 100644 index 000000000000..9bd8cf6980f1 --- /dev/null +++ b/drivers/mfd/p3h2840.c @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * P3H2X4X i3c hub and regulator device. + */ + +#include +#include +#include +#include +#include + +static const struct mfd_cell p3h2x4x_devs[] = { + { + .name = "p3h2x4x-regulator", + }, + { + .name = "p3h2x4x-i3c-hub", + }, +}; + +static const struct regmap_config p3h2x4x_regmap_config = { + .reg_bits = P3H2X4X_REG_BITS, + .val_bits = P3H2X4X_VAL_BITS, + .max_register = 0xFF, +}; + +static int p3h2x4x_device_probe_i3c(struct i3c_device *i3cdev) +{ + struct p3h2x4x_dev *p3h2x4x; + int ret; + + p3h2x4x = devm_kzalloc(&i3cdev->dev, sizeof(*p3h2x4x), GFP_KERNEL); + if (!p3h2x4x) + return -ENOMEM; + + i3cdev_set_drvdata(i3cdev, p3h2x4x); + + p3h2x4x->regmap = devm_regmap_init_i3c(i3cdev, &p3h2x4x_regmap_config); + if (IS_ERR(p3h2x4x->regmap)) + return dev_err_probe(&i3cdev->dev, PTR_ERR(p3h2x4x->regmap), + "Failed to register HUB regmap\n"); + p3h2x4x->i3cdev = i3cdev; + + ret = devm_mfd_add_devices(&i3cdev->dev, PLATFORM_DEVID_AUTO, + p3h2x4x_devs, ARRAY_SIZE(p3h2x4x_devs), + NULL, 0, NULL); + if (ret) + return dev_err_probe(&i3cdev->dev, ret, "Failed to add sub devices\n"); + + return 0; +} + +static int p3h2x4x_device_probe_i2c(struct i2c_client *client) +{ + struct p3h2x4x_dev *p3h2x4x; + int ret; + + p3h2x4x = devm_kzalloc(&client->dev, sizeof(*p3h2x4x), GFP_KERNEL); + if (!p3h2x4x) + return -ENOMEM; + + i2c_set_clientdata(client, p3h2x4x); + + p3h2x4x->regmap = devm_regmap_init_i2c(client, &p3h2x4x_regmap_config); + if (IS_ERR(p3h2x4x->regmap)) + return dev_err_probe(&client->dev, PTR_ERR(p3h2x4x->regmap), + "Failed to register HUB regmap\n"); + + p3h2x4x->i3cdev = NULL; + + ret = devm_mfd_add_devices(&client->dev, PLATFORM_DEVID_AUTO, + p3h2x4x_devs, ARRAY_SIZE(p3h2x4x_devs), + NULL, 0, NULL); + if (ret) + return dev_err_probe(&client->dev, ret, "Failed to add sub devices\n"); + + return 0; +} + +static const struct i3c_device_id p3h2x4x_i3c_ids[] = { + I3C_CLASS(I3C_DCR_HUB, NULL), + { /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(i3c, p3h2x4x_i3c_ids); + +static const struct i2c_device_id p3h2x4x_i2c_id_table[] = { + { "nxp-i3c-hub" }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(i2c, p3h2x4x_i2c_id_table); + +static const struct of_device_id p3h2x4x_i2c_of_match[] = { + { .compatible = "nxp,p3h2840", }, + { /* sentinel */ } +}; +MODULE_DEVICE_TABLE(of, p3h2x4x_i2c_of_match); + +static struct i3c_driver p3h2x4x_i3c = { + .driver = { + .name = "p3h2x4x_i3c_drv", + }, + .probe = p3h2x4x_device_probe_i3c, + .id_table = p3h2x4x_i3c_ids, +}; + +static struct i2c_driver p3h2x4x_i2c = { + .driver = { + .name = "p3h2x4x_i2c_drv", + .of_match_table = p3h2x4x_i2c_of_match, + }, + .probe = p3h2x4x_device_probe_i2c, + .id_table = p3h2x4x_i2c_id_table, +}; + +module_i3c_i2c_driver(p3h2x4x_i3c, &p3h2x4x_i2c); + +MODULE_AUTHOR("Aman Kumar Pandey "); +MODULE_AUTHOR("Vikash Bansal "); +MODULE_AUTHOR("Lakshay Piplani "); +MODULE_DESCRIPTION("NXP P3H2X4X I3C HUB multi function driver"); +MODULE_LICENSE("GPL"); diff --git a/include/linux/i3c/device.h b/include/linux/i3c/device.h index 971d53349b6f..6188082599dd 100644 --- a/include/linux/i3c/device.h +++ b/include/linux/i3c/device.h @@ -85,6 +85,7 @@ struct i3c_xfer { */ enum i3c_dcr { I3C_DCR_GENERIC_DEVICE = 0, + I3C_DCR_HUB = 194, }; #define I3C_PID_MANUF_ID(pid) (((pid) & GENMASK_ULL(47, 33)) >> 33) diff --git a/include/linux/mfd/p3h2840.h b/include/linux/mfd/p3h2840.h new file mode 100644 index 000000000000..18ff15f9e2d9 --- /dev/null +++ b/include/linux/mfd/p3h2840.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright 2025-2026 NXP + * This header file contain private Reg address and its bit mapping etc. + */ + +#ifndef _LINUX_MFD_P3H2840_H +#define _LINUX_MFD_P3H2840_H + +#include + +/* Device Configuration Registers */ +#define P3H2X4X_DEV_REG_PROTECTION_CODE 0x10 +#define P3H2X4X_REGISTERS_LOCK_CODE 0x00 +#define P3H2X4X_REGISTERS_UNLOCK_CODE 0x69 +#define P3H2X4X_CP1_REGISTERS_UNLOCK_CODE 0x6a + +/* Reg config for Regmap */ +#define P3H2X4X_REG_BITS 8 +#define P3H2X4X_VAL_BITS 8 + +struct p3h2x4x_dev { + struct i3c_device *i3cdev; + struct regmap *regmap; +}; +#endif /* _LINUX_MFD_P3H2840_H */ -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:06 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:06 +0530 Subject: [PATCH v10 6/9] regulator: p3h2x4x: Add driver for on-die regulators in NXP P3H2x4x i3c hub In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-7-lakshay.piplani@nxp.com> From: Aman Kumar Pandey The NXP P3H2x4x family integrates on-die regulators alongside I3C hub functionality. This driver registers the regulators using the MFD framework and exposes them via the regulator subsystem. Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani Reviewed-by: Frank Li --- Changes in v10: - No change Changes in v9: - No change Changes in v8: - No change Changes in v7: - No change, added Reviewed-By tag Changes in v6: - Use DEFINE_LOCK_GUARD_1 for reg lock/unlock Changes in v5: - Updated dev_err_probe() for regmap_init failure. - Updated module description Changes in v4: - Split the driver into three separate patches (mfd, regulator and I3C hub) - Introduced driver for on-die regulators in NXP P3H2x4x I3C hub --- --- MAINTAINERS | 1 + drivers/regulator/Kconfig | 10 + drivers/regulator/Makefile | 1 + drivers/regulator/p3h2840_i3c_hub_regulator.c | 218 ++++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 drivers/regulator/p3h2840_i3c_hub_regulator.c diff --git a/MAINTAINERS b/MAINTAINERS index 25e578dd74dd..da698b34873b 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19312,6 +19312,7 @@ L: linux-i3c-owner at lists.infradead.org S: Maintained F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml F: drivers/mfd/p3h2840.c +F: drivers/regulator/p3h2840_i3c_hub_regulator.c F: include/linux/mfd/p3h2840.h NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER diff --git a/drivers/regulator/Kconfig b/drivers/regulator/Kconfig index d71dac9436e3..88809f493fd4 100644 --- a/drivers/regulator/Kconfig +++ b/drivers/regulator/Kconfig @@ -1019,6 +1019,16 @@ config REGULATOR_MTK_DVFSRC of Mediatek. It allows for voting on regulator state between multiple users. +config REGULATOR_P3H2X4X + tristate "NXP P3H2X4X regulator support" + depends on MFD_P3H2X4X + help + This driver provides support for the voltage regulators of the + P3H244x/P3H284x multi-function I3C Hub device. + + Say M here if you want to include support for this regulator as + a module. The module will be named "p3h2840_i3c_hub_regulator". + config REGULATOR_PALMAS tristate "TI Palmas PMIC Regulators" depends on MFD_PALMAS diff --git a/drivers/regulator/Makefile b/drivers/regulator/Makefile index 35639f3115fd..46f586ccde63 100644 --- a/drivers/regulator/Makefile +++ b/drivers/regulator/Makefile @@ -128,6 +128,7 @@ obj-$(CONFIG_REGULATOR_QCOM_RPMH) += qcom-rpmh-regulator.o obj-$(CONFIG_REGULATOR_QCOM_SMD_RPM) += qcom_smd-regulator.o obj-$(CONFIG_REGULATOR_QCOM_SPMI) += qcom_spmi-regulator.o obj-$(CONFIG_REGULATOR_QCOM_USB_VBUS) += qcom_usb_vbus-regulator.o +obj-$(CONFIG_REGULATOR_P3H2X4X) += p3h2840_i3c_hub_regulator.o obj-$(CONFIG_REGULATOR_PALMAS) += palmas-regulator.o obj-$(CONFIG_REGULATOR_PCA9450) += pca9450-regulator.o obj-$(CONFIG_REGULATOR_PF0900) += pf0900-regulator.o diff --git a/drivers/regulator/p3h2840_i3c_hub_regulator.c b/drivers/regulator/p3h2840_i3c_hub_regulator.c new file mode 100644 index 000000000000..4f2514d4d928 --- /dev/null +++ b/drivers/regulator/p3h2840_i3c_hub_regulator.c @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * This P3H2X4X driver file contain functions for enable/disable regulator and voltage set/get. + */ +#include +#include +#include +#include +#include +#include +#include + +#define P3H2X4X_LDO_AND_PULLUP_CONF 0x19 +#define P3H2X4X_LDO_ENABLE_DISABLE_MASK GENMASK(3, 0) +#define P3H2X4X_CP0_EN_LDO BIT(0) +#define P3H2X4X_CP1_EN_LDO BIT(1) +#define P3H2X4X_TP0145_EN_LDO BIT(2) +#define P3H2X4X_TP2367_EN_LDO BIT(3) + +#define P3H2X4X_NET_OPER_MODE_CONF 0x15 +#define P3H2X4X_VCCIO_LDO_CONF 0x16 +#define P3H2X4X_CP0_VCCIO_LDO_VOLTAGE_MASK GENMASK(1, 0) +#define P3H2X4X_CP0_VCCIO_LDO_VOLTAGE(x) \ + FIELD_PREP(P3H2X4X_CP0_VCCIO_LDO_VOLTAGE_MASK, x) +#define P3H2X4X_CP1_VCCIO_LDO_VOLTAGE_MASK GENMASK(3, 2) +#define P3H2X4X_CP1_VCCIO_LDO_VOLTAGE(x) \ + FIELD_PREP(P3H2X4X_CP1_VCCIO_LDO_VOLTAGE_MASK, x) +#define P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE_MASK GENMASK(5, 4) +#define P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE(x) \ + FIELD_PREP(P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE_MASK, x) +#define P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE_MASK GENMASK(7, 6) +#define P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE(x) \ + FIELD_PREP(P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE_MASK, x) +#define P3H2X4X_LDO_COUNT 4 + +struct p3h2x4x_regulator_dev { + struct regulator_dev *rp3h2x4x_dev[P3H2X4X_LDO_COUNT]; + struct regmap *regmap; +}; + +struct p3h2x4x_reg_state { + unsigned int orig; + bool restore; +}; + +static void p3h2x4x_reg_guard_enter(struct regulator_dev *rdev, + struct p3h2x4x_reg_state *state) +{ + state->restore = false; + + if (regmap_read(rdev->regmap, + P3H2X4X_DEV_REG_PROTECTION_CODE, + &state->orig)) + return; + + if (state->orig != P3H2X4X_REGISTERS_UNLOCK_CODE) { + regmap_write(rdev->regmap, + P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_UNLOCK_CODE); + state->restore = true; + } +} + +static void p3h2x4x_reg_guard_exit(struct regulator_dev *rdev, + struct p3h2x4x_reg_state *state) +{ + if (state->restore) + regmap_write(rdev->regmap, + P3H2X4X_DEV_REG_PROTECTION_CODE, + state->orig); +} + +DEFINE_LOCK_GUARD_1(p3h2x4x_reg, struct regulator_dev, + p3h2x4x_reg_guard_enter(_T->lock, &_T->state), + p3h2x4x_reg_guard_exit(_T->lock, &_T->state), + struct p3h2x4x_reg_state state); + +static int p3h2x4x_regulator_enable(struct regulator_dev *rdev) +{ + guard(p3h2x4x_reg)(rdev); + return regulator_enable_regmap(rdev); +} + +static int p3h2x4x_regulator_disable(struct regulator_dev *rdev) +{ + guard(p3h2x4x_reg)(rdev); + return regulator_disable_regmap(rdev); +} + +static int p3h2x4x_regulator_set_voltage_sel(struct regulator_dev *rdev, + unsigned int sel) +{ + guard(p3h2x4x_reg)(rdev); + return regulator_set_voltage_sel_regmap(rdev, sel); +} + +static const struct regulator_ops p3h2x4x_ldo_ops = { + .list_voltage = regulator_list_voltage_table, + .map_voltage = regulator_map_voltage_iterate, + .set_voltage_sel = p3h2x4x_regulator_set_voltage_sel, + .get_voltage_sel = regulator_get_voltage_sel_regmap, + .enable = p3h2x4x_regulator_enable, + .disable = p3h2x4x_regulator_disable, + .is_enabled = regulator_is_enabled_regmap, +}; + +static const unsigned int p3h2x4x_voltage_table[] = { + 1000000, + 1100000, + 1200000, + 1800000, +}; + +static struct regulator_desc p3h2x4x_regulators[] = { + { + .name = "ldo-cp0", + .of_match = of_match_ptr("ldo-cp0"), + .regulators_node = of_match_ptr("regulators"), + .volt_table = p3h2x4x_voltage_table, + .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table), + .ops = &p3h2x4x_ldo_ops, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask = P3H2X4X_CP0_EN_LDO, + .vsel_reg = P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask = P3H2X4X_CP0_VCCIO_LDO_VOLTAGE_MASK, + }, + { + .name = "ldo-cp1", + .of_match = of_match_ptr("ldo-cp1"), + .regulators_node = of_match_ptr("regulators"), + .volt_table = p3h2x4x_voltage_table, + .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table), + .ops = &p3h2x4x_ldo_ops, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask = P3H2X4X_CP1_EN_LDO, + .vsel_reg = P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask = P3H2X4X_CP1_VCCIO_LDO_VOLTAGE_MASK, + }, + { + .name = "ldo-tpg0", + .of_match = of_match_ptr("ldo-tpg0"), + .regulators_node = of_match_ptr("regulators"), + .volt_table = p3h2x4x_voltage_table, + .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table), + .ops = &p3h2x4x_ldo_ops, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask = P3H2X4X_TP0145_EN_LDO, + .vsel_reg = P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask = P3H2X4X_TP0145_VCCIO_LDO_VOLTAGE_MASK, + }, + { + .name = "ldo-tpg1", + .of_match = of_match_ptr("ldo-tpg1"), + .regulators_node = of_match_ptr("regulators"), + .volt_table = p3h2x4x_voltage_table, + .n_voltages = ARRAY_SIZE(p3h2x4x_voltage_table), + .ops = &p3h2x4x_ldo_ops, + .type = REGULATOR_VOLTAGE, + .owner = THIS_MODULE, + .enable_reg = P3H2X4X_LDO_AND_PULLUP_CONF, + .enable_mask = P3H2X4X_TP2367_EN_LDO, + .vsel_reg = P3H2X4X_VCCIO_LDO_CONF, + .vsel_mask = P3H2X4X_TP2367_VCCIO_LDO_VOLTAGE_MASK, + }, +}; + +static int p3h2x4x_regulator_probe(struct platform_device *pdev) +{ + struct p3h2x4x_dev *p3h2x4x = dev_get_drvdata(pdev->dev.parent); + struct p3h2x4x_regulator_dev *p3h2x4x_regulator; + struct regulator_config rcfg = { }; + struct device *dev = &pdev->dev; + struct regulator_dev *rdev; + int i; + + p3h2x4x_regulator = devm_kzalloc(dev, sizeof(*p3h2x4x_regulator), GFP_KERNEL); + if (!p3h2x4x_regulator) + return -ENOMEM; + + platform_set_drvdata(pdev, p3h2x4x_regulator); + + p3h2x4x_regulator->regmap = p3h2x4x->regmap; + + rcfg.dev = dev->parent; + rcfg.regmap = p3h2x4x_regulator->regmap; + rcfg.driver_data = p3h2x4x_regulator; + + for (i = 0; i < ARRAY_SIZE(p3h2x4x_regulators); i++) { + rdev = devm_regulator_register(&pdev->dev, &p3h2x4x_regulators[i], &rcfg); + if (IS_ERR(rdev)) + return dev_err_probe(dev, PTR_ERR(rdev), "Failed to register %s\n", + p3h2x4x_regulators[i].name); + p3h2x4x_regulator->rp3h2x4x_dev[i] = rdev; + } + return 0; +} + +static struct platform_driver p3h2x4x_regulator_driver = { + .driver = { + .name = "p3h2x4x-regulator", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .probe = p3h2x4x_regulator_probe, +}; +module_platform_driver(p3h2x4x_regulator_driver); + +MODULE_AUTHOR("Aman Kumar Pandey "); +MODULE_AUTHOR("Vikash Bansal "); +MODULE_AUTHOR("Lakshay Piplani "); +MODULE_DESCRIPTION("NXP P3H2X4X I3C HUB Regulator driver"); +MODULE_LICENSE("GPL"); -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:07 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:07 +0530 Subject: [PATCH v10 7/9] i3c: hub: Add support for the I3C interface in the I3C hub In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-8-lakshay.piplani@nxp.com> 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 --- Changes in v10: - Remove unnecessary ibi_lock handling in request/enable/disable/free IBI APIs - Remove redundant parent pointer from struct i3c_hub and derive upstream master from hub_dev Changes in v9: - No change Changes in v8: - No change Changes in v7: - Convert Kconfig option to tristate - Fix signedness issue in return value - Fix kernel-doc warnings Changes in v6: - Add support for the generic I3C interface in the I3C Hub --- --- MAINTAINERS | 2 + drivers/i3c/Kconfig | 15 ++ drivers/i3c/Makefile | 1 + drivers/i3c/hub.c | 465 ++++++++++++++++++++++++++++++++++++++++ include/linux/i3c/hub.h | 99 +++++++++ 5 files changed, 582 insertions(+) create mode 100644 drivers/i3c/hub.c create mode 100644 include/linux/i3c/hub.h diff --git a/MAINTAINERS b/MAINTAINERS index da698b34873b..1d4954695f84 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19311,8 +19311,10 @@ L: linux-kernel at vger.kernel.org L: linux-i3c-owner at lists.infradead.org S: Maintained F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml +F: drivers/i3c/hub.c F: drivers/mfd/p3h2840.c F: drivers/regulator/p3h2840_i3c_hub_regulator.c +F: include/linux/i3c/hub.h F: include/linux/mfd/p3h2840.h NXP PF5300/PF5301/PF5302 PMIC REGULATOR DEVICE DRIVER diff --git a/drivers/i3c/Kconfig b/drivers/i3c/Kconfig index 626c54b386d5..65304b416bb4 100644 --- a/drivers/i3c/Kconfig +++ b/drivers/i3c/Kconfig @@ -21,6 +21,21 @@ menuconfig I3C if I3C source "drivers/i3c/master/Kconfig" + +config I3C_HUB + tristate "I3C Hub Support" + depends on I3C + 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..da0e209ee5b7 --- /dev/null +++ b/drivers/i3c/hub.c @@ -0,0 +1,465 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2026 NXP + * Generic I3C Hub core implementing virtual controller operations. + */ +#include +#include + +#include "internals.h" + +/** + * i3c_hub_master_bus_init() - Bind controller to hub device + * @controller: Virtual controller for a hub port + * + * Associates the virtual controller with the hub device descriptor so that + * transfers are executed through the hub on the parent bus. + */ +static int i3c_hub_master_bus_init(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return -ENODEV; + + hub = hub_controller->hub; + + if (!hub->hub_dev) + return -ENODEV; + + controller->this = hub->hub_dev->desc; + return 0; +} + +static void i3c_hub_master_bus_cleanup(struct i3c_master_controller *controller) +{ + controller->this = NULL; +} + +static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev) +{ + return 0; +} + +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev, u8 old_dyn_addr) +{ + return 0; +} + +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev) +{ +} + +/** + * 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; + + i3c_hub_enable_port(controller); + ret = i3c_master_do_daa(parent); + i3c_hub_disable_port(controller); + + return ret; +} + +static bool i3c_hub_supports_ccc_cmd(struct i3c_hub *hub, + const struct i3c_ccc_cmd *cmd) +{ + struct i3c_master_controller *parent; + + if (!hub || !hub->hub_dev) + return false; + + parent = i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return false; + + return i3c_master_supports_ccc_cmd(parent, cmd); +} + +/** + * i3c_hub_send_ccc_cmd() - Send CCC through hub port + * @hub: Hub instance + * @controller: Virtual controller + * @cmd: CCC command + * + * Enables the port connection while issuing CCC on the parent controller. + */ +static int i3c_hub_send_ccc_cmd(struct i3c_hub *hub, + struct i3c_master_controller *controller, + struct i3c_ccc_cmd *cmd) +{ + struct i3c_master_controller *parent; + int ret; + + if (!hub || !hub->hub_dev) + return -ENODEV; + + parent = i3c_dev_get_master(hub->hub_dev->desc); + if (!parent) + return -ENODEV; + + i3c_hub_enable_port(controller); + ret = i3c_master_send_ccc_cmd(parent, cmd); + i3c_hub_disable_port(controller); + + return ret; +} + +/** + * i3c_hub_master_priv_xfers() - Execute private transfers via hub + * @dev: Target device descriptor + * @xfers: Transfer array + * @nxfers: Number of transfers + * @mode: transfer mode (SDR, HDR, etc.) + * + * Handles address adjustment and forwards private transfers through the hub + * device. + */ +static int i3c_hub_master_priv_xfers(struct i3c_dev_desc *dev, + struct i3c_xfer *xfers, + int nxfers, + enum i3c_xfer_mode mode) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(dev); + struct i3c_hub_controller *hub_controller; + struct i3c_dev_desc *hub_dev; + u8 hub_addr, target_addr; + 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; + + hub_dev = hub->hub_dev->desc; + + i3c_hub_enable_port(controller); + + hub_addr = hub_dev->info.dyn_addr ? + hub_dev->info.dyn_addr : hub_dev->info.static_addr; + + target_addr = dev->info.dyn_addr ? + dev->info.dyn_addr : dev->info.static_addr; + + if (hub_addr != target_addr) { + hub_dev->info.dyn_addr = target_addr; + ret = i3c_master_reattach_i3c_dev_locked(hub_dev, target_addr); + if (ret) + goto disable; + } + + ret = i3c_device_do_xfers(hub->hub_dev, xfers, nxfers, mode); + + if (hub_addr != target_addr) { + hub_dev->info.dyn_addr = hub_addr; + ret |= i3c_master_reattach_i3c_dev_locked(hub_dev, hub_addr); + } + +disable: + i3c_hub_disable_port(controller); + return ret; +} + +static int i3c_hub_attach_i2c_dev(struct i2c_dev_desc *dev) +{ + return 0; +} + +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 0; +} + +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; + + 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 + * + * Temporarily updates parent controller context to request IBI for a device + * connected through the hub. + */ +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 *orig_parent; + 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); + + down_write(&parent->bus.lock); + + orig_parent = i3c_hub_update_desc_parent(&desc->common, parent); + + ret = i3c_master_direct_attach_i3c_dev_locked(parent, desc); + if (ret) { + i3c_hub_update_desc_parent(&desc->common, orig_parent); + return ret; + } + + kfree(desc->ibi); + desc->ibi = NULL; + ret = i3c_dev_request_ibi_locked(desc, req); + + i3c_hub_update_desc_parent(&desc->common, orig_parent); + + up_write(&parent->bus.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 *orig_parent; + struct i3c_master_controller *parent; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub = hub_controller->hub; + + parent = i3c_dev_get_master(hub->hub_dev->desc); + + i3c_hub_enable_port(controller); + + down_write(&parent->bus.lock); + orig_parent = i3c_hub_update_desc_parent(&desc->common, parent); + i3c_master_direct_detach_i3c_dev_locked(desc); + i3c_dev_free_ibi_locked(desc); + i3c_hub_update_desc_parent(&desc->common, orig_parent); + up_write(&parent->bus.lock); + + i3c_hub_disable_port(controller); +} + +/** + * 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 *orig_parent; + 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); + + i3c_hub_enable_port(controller); + + orig_parent = i3c_hub_update_desc_parent(&desc->common, parent); + + down_write(&parent->bus.lock); + ret = i3c_dev_enable_ibi_locked(desc); + up_write(&parent->bus.lock); + + i3c_hub_update_desc_parent(&desc->common, orig_parent); + + i3c_hub_disable_port(controller); + + 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 *orig_parent; + 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); + + i3c_hub_enable_port(controller); + + orig_parent = i3c_hub_update_desc_parent(&desc->common, parent); + + down_write(&parent->bus.lock); + ret = i3c_dev_disable_ibi_locked(desc); + up_write(&parent->bus.lock); + + i3c_hub_update_desc_parent(&desc->common, orig_parent); + + i3c_hub_disable_port(controller); + + return ret; +} + +static void i3c_hub_recycle_ibi_slot(struct i3c_dev_desc *desc, + struct i3c_ibi_slot *slot) +{ +} + +static const struct i3c_master_controller_ops i3c_hub_master_ops_data = { + .bus_init = i3c_hub_master_bus_init, + .bus_cleanup = i3c_hub_master_bus_cleanup, + .attach_i3c_dev = i3c_hub_attach_i3c_dev, + .reattach_i3c_dev = i3c_hub_reattach_i3c_dev, + .detach_i3c_dev = i3c_hub_detach_i3c_dev, + .do_daa = i3c_hub_master_do_daa, + .supports_ccc_cmd = i3c_hub_master_supports_ccc_cmd, + .send_ccc_cmd = i3c_hub_master_send_ccc_cmd, + .i3c_xfers = i3c_hub_master_priv_xfers, + .attach_i2c_dev = i3c_hub_attach_i2c_dev, + .detach_i2c_dev = i3c_hub_detach_i2c_dev, + .i2c_xfers = i3c_hub_i2c_xfers, + .request_ibi = i3c_hub_request_ibi, + .free_ibi = i3c_hub_free_ibi, + .enable_ibi = i3c_hub_enable_ibi, + .disable_ibi = i3c_hub_disable_ibi, + .recycle_ibi_slot = i3c_hub_recycle_ibi_slot, +}; + +/** + * i3c_hub_init() - Initialize hub context + * @hub: Hub instance + * @parent: Parent I3C master controller + * @ops: Vendor callbacks + * @hub_dev: I3C hub device + */ +void i3c_hub_init(struct i3c_hub *hub, + const struct i3c_hub_ops *ops, + struct i3c_device *hub_dev) +{ + hub->ops = ops; + hub->hub_dev = hub_dev; +} +EXPORT_SYMBOL_GPL(i3c_hub_init); + +const struct i3c_master_controller_ops *i3c_hub_master_ops(void) +{ + return &i3c_hub_master_ops_data; +} +EXPORT_SYMBOL_GPL(i3c_hub_master_ops); + +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..8b5c162067c3 --- /dev/null +++ b/include/linux/i3c/hub.h @@ -0,0 +1,99 @@ +/* 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 + +static inline struct i3c_master_controller * +i3c_hub_update_desc_parent(struct i3c_i2c_dev_desc *desc, + struct i3c_master_controller *parent) +{ + struct i3c_master_controller *orig_parent = desc->master; + + desc->master = parent; + return orig_parent; +} + +/** + * struct i3c_hub - Generic I3C hub context + * @parent: Parent I3C master controller + * @ops: Vendor callbacks for port connection control + * @hub_dev: I3C device representing the hub on the parent bus + */ +struct i3c_hub { + const struct i3c_hub_ops *ops; + struct i3c_device *hub_dev; +}; + +struct i3c_hub_controller { + struct i3c_master_controller *parent; + struct i3c_master_controller controller; + struct i3c_hub *hub; +}; + +struct i3c_hub_ops { + void (*enable_port)(struct i3c_master_controller *controller); + void (*disable_port)(struct i3c_master_controller *controller); +}; + +/** + * i3c_hub_enable_port() - Enable hub connection for a controller + * @controller: Virtual controller representing a hub port + * + * Retrieves hub context from controller drvdata and invokes the vendor + * callback to enable the associated port connection. + */ +static inline void i3c_hub_enable_port(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub = hub_controller->hub; + + if (hub && hub->ops && hub->ops->enable_port) + hub->ops->enable_port(controller); +} + +/** + * i3c_hub_disable_port() - Disable hub connection for a controller + * @controller: Virtual controller representing a hub port + * + * Retrieves hub context from controller drvdata and invokes the vendor + * callback to disable the associated port connection. + */ +static inline void i3c_hub_disable_port(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + struct i3c_hub *hub; + + hub_controller = dev_get_drvdata(&controller->dev); + if (!hub_controller || !hub_controller->hub) + return; + + hub = hub_controller->hub; + + if (hub && hub->ops && hub->ops->disable_port) + hub->ops->disable_port(controller); +} + +/** + * i3c_hub_master_ops() - Return virtual controller ops for hub ports + * + * Provides i3c_master_controller_ops used by controllers created for hub + * ports. + */ +const struct i3c_master_controller_ops *i3c_hub_master_ops(void); + +void i3c_hub_init(struct i3c_hub *hub, + const struct i3c_hub_ops *ops, + struct i3c_device *hub_dev); + +#endif -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:08 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:08 +0530 Subject: [PATCH v10 8/9] i3c: hub: p3h2x4x: Add support for NXP P3H2x4x I3C hub functionality In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-9-lakshay.piplani@nxp.com> From: Aman Kumar Pandey Add I3C hub functionality for the NXP P3H2x4x family of multiport hubs. These devices support downstream target ports that can be configured as I3C, I2C, or SMBus. This driver enables: - I3C/I2C communication between host and hub - Transparent communication with downstream devices - Target port configuration (I3C/I2C/SMBus) P3H2440/P3H2441 support 4 target ports. P3H2840/P3H2841 support 8 target ports. Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal Signed-off-by: Lakshay Piplani --- Changes in v10: - Split SMBus target/slave mode support, including IBI and MCTP receive handling, into a separate patch Changes in v9: - Added CONFIG_I2C_SLAVE guards where necessary to avoid build issues when I2C slave support is disabled. Changes in v8: - No change Changes in v7: - Remove CONFIG_I2C_SLAVE guards - Use Kernel API find_closest instead of custom helper - Use devm_regulator_get_enable_optional() - Fix kernel-doc warnings Changes in v6: - Remove generic I3C code and keep reg dependent code only. Changes in v5: - Updated supply names. Changes in v4: - Split the driver into three separate patches (mfd, regulator and I3C hub) - Added support for NXP P3H2x4x I3C hub functionality - Integrated hub driver with its on-die regulator Changes in v3: - Added MFD (Multi-Function Device) support for I3C hub and on-die regulator Changes in v2: - Refined coding style and incorporated review feedback - Updated directory structure - Revised logic for parsing DTS nodes --- --- MAINTAINERS | 1 + drivers/i3c/Kconfig | 1 + drivers/i3c/Makefile | 1 + drivers/i3c/hub/Kconfig | 11 + drivers/i3c/hub/Makefile | 4 + drivers/i3c/hub/p3h2840_i3c_hub.h | 327 +++++++++++++++++++++ drivers/i3c/hub/p3h2840_i3c_hub_common.c | 347 +++++++++++++++++++++++ drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 124 ++++++++ drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 269 ++++++++++++++++++ 9 files changed, 1085 insertions(+) create mode 100644 drivers/i3c/hub/Kconfig create mode 100644 drivers/i3c/hub/Makefile create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub.h create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_common.c create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_i3c.c create mode 100644 drivers/i3c/hub/p3h2840_i3c_hub_smbus.c diff --git a/MAINTAINERS b/MAINTAINERS index 1d4954695f84..9aec93c26f85 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -19312,6 +19312,7 @@ L: linux-i3c-owner at lists.infradead.org S: Maintained F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml F: drivers/i3c/hub.c +F: drivers/i3c/hub/* F: drivers/mfd/p3h2840.c F: drivers/regulator/p3h2840_i3c_hub_regulator.c F: include/linux/i3c/hub.h diff --git a/drivers/i3c/Kconfig b/drivers/i3c/Kconfig index 65304b416bb4..74727d614492 100644 --- a/drivers/i3c/Kconfig +++ b/drivers/i3c/Kconfig @@ -36,6 +36,7 @@ config I3C_HUB Say Y here if your platform includes an I3C hub device +source "drivers/i3c/hub/Kconfig" endif # I3C config I3C_OR_I2C diff --git a/drivers/i3c/Makefile b/drivers/i3c/Makefile index 9ddee56a6338..2950820db9ea 100644 --- a/drivers/i3c/Makefile +++ b/drivers/i3c/Makefile @@ -3,3 +3,4 @@ i3c-y := device.o master.o obj-$(CONFIG_I3C) += i3c.o obj-$(CONFIG_I3C) += master/ obj-$(CONFIG_I3C_HUB) += hub.o +obj-$(CONFIG_I3C_HUB) += hub/ diff --git a/drivers/i3c/hub/Kconfig b/drivers/i3c/hub/Kconfig new file mode 100644 index 000000000000..f725f3e2bfbe --- /dev/null +++ b/drivers/i3c/hub/Kconfig @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright 2025 NXP +config P3H2X4X_I3C_HUB + tristate "NXP P3H2X4X I3C HUB support" + depends on MFD_P3H2X4X + select I3C_HUB + help + This enables support for NXP P3H244x/P3H284x I3C HUB. These hubs + connect to a host via I3C/I2C/SMBus and allow communication with + multiple downstream peripherals. The Say Y or M here to use I3C + HUB driver to configure I3C HUB device. diff --git a/drivers/i3c/hub/Makefile b/drivers/i3c/hub/Makefile new file mode 100644 index 000000000000..9dbd8a7b4184 --- /dev/null +++ b/drivers/i3c/hub/Makefile @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0 +# Copyright 2025 NXP +p3h2840_i3c_hub-y := p3h2840_i3c_hub_common.o p3h2840_i3c_hub_i3c.o p3h2840_i3c_hub_smbus.o +obj-$(CONFIG_P3H2X4X_I3C_HUB) += p3h2840_i3c_hub.o diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h new file mode 100644 index 000000000000..d69fafbac584 --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub.h @@ -0,0 +1,327 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Copyright 2025-2026 NXP + * This header file contain private device structure definition. + */ + +#ifndef P3H2840_I3C_HUB_H +#define P3H2840_I3C_HUB_H + +#include +#include +#include +#include +#include +#include +#include + +/* I3C HUB REGISTERS */ + +/* Device Information Registers */ +#define P3H2X4X_DEV_INFO_0 0x00 +#define P3H2X4X_DEV_INFO_1 0x01 +#define P3H2X4X_PID_5 0x02 +#define P3H2X4X_PID_4 0x03 +#define P3H2X4X_PID_3 0x04 +#define P3H2X4X_PID_2 0x05 +#define P3H2X4X_PID_1 0x06 +#define P3H2X4X_PID_0 0x07 +#define P3H2X4X_BCR 0x08 +#define P3H2X4X_DCR 0x09 +#define P3H2X4X_DEV_CAPAB 0x0a +#define P3H2X4X_DEV_REV 0x0b + +/* Device Configuration Registers */ +#define P3H2X4X_CP_CONF 0x11 +#define P3H2X4X_TP_ENABLE 0x12 + +#define P3H2X4X_DEV_CONF 0x13 +#define P3H2X4X_IO_STRENGTH 0x14 +#define P3H2X4X_TP0145_IO_STRENGTH_MASK GENMASK(1, 0) +#define P3H2X4X_TP0145_IO_STRENGTH(x) \ + FIELD_PREP(P3H2X4X_TP0145_IO_STRENGTH_MASK, x) +#define P3H2X4X_TP2367_IO_STRENGTH_MASK GENMASK(3, 2) +#define P3H2X4X_TP2367_IO_STRENGTH(x) \ + FIELD_PREP(P3H2X4X_TP2367_IO_STRENGTH_MASK, x) +#define P3H2X4X_CP0_IO_STRENGTH_MASK GENMASK(5, 4) +#define P3H2X4X_CP0_IO_STRENGTH(x) \ + FIELD_PREP(P3H2X4X_CP0_IO_STRENGTH_MASK, x) +#define P3H2X4X_CP1_IO_STRENGTH_MASK GENMASK(7, 6) +#define P3H2X4X_CP1_IO_STRENGTH(x) \ + FIELD_PREP(P3H2X4X_CP1_IO_STRENGTH_MASK, x) +#define P3H2X4X_IO_STRENGTH_MASK GENMASK(7, 0) + +#define P3H2X4X_TP_IO_MODE_CONF 0x17 +#define P3H2X4X_TP_SMBUS_AGNT_EN 0x18 + +#define P3H2X4X_LDO_AND_PULLUP_CONF 0x19 + +#define P3H2X4X_TP0145_PULLUP_CONF_MASK GENMASK(7, 6) +#define P3H2X4X_TP0145_PULLUP_CONF(x) \ + FIELD_PREP(P3H2X4X_TP0145_PULLUP_CONF_MASK, x) +#define P3H2X4X_TP2367_PULLUP_CONF_MASK GENMASK(5, 4) +#define P3H2X4X_TP2367_PULLUP_CONF(x) \ + FIELD_PREP(P3H2X4X_TP2367_PULLUP_CONF_MASK, x) +#define P3H2X4X_PULLUP_CONF_MASK GENMASK(7, 4) + +#define P3H2X4X_CP_IBI_CONF 0x1a + +#define P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG 0x1b + +#define P3H2X4X_IBI_MDB_CUSTOM 0x1c +#define P3H2X4X_JEDEC_CONTEXT_ID 0x1d +#define P3H2X4X_TP_GPIO_MODE_EN 0x1e + +/* Device Status and IBI Registers */ +#define P3H2X4X_DEV_AND_IBI_STS 0x20 +#define P3H2X4X_TP_SMBUS_AGNT_IBI_STS 0x21 +#define P3H2X4X_SMBUS_AGENT_EVENT_FLAG_STATUS BIT(4) + +/* Controller Port Control/Status Registers */ +#define P3H2X4X_CP_MUX_SET 0x38 +#define P3H2X4X_CONTROLLER_PORT_MUX_REQ BIT(0) +#define P3H2X4X_CP_MUX_STS 0x39 +#define P3H2X4X_CONTROLLER_PORT_MUX_CONNECTION_STATUS BIT(0) + +/* Target Ports Control Registers */ +#define P3H2X4X_TP_SMBUS_AGNT_TRANS_START 0x50 +#define P3H2X4X_TP_NET_CON_CONF 0x51 + +#define P3H2X4X_TP_PULLUP_EN 0x53 + +#define P3H2X4X_TP_SCL_OUT_EN 0x54 +#define P3H2X4X_TP_SDA_OUT_EN 0x55 +#define P3H2X4X_TP_SCL_OUT_LEVEL 0x56 +#define P3H2X4X_TP_SDA_OUT_LEVEL 0x57 +#define P3H2X4X_TP_IN_DETECT_MODE_CONF 0x58 +#define P3H2X4X_TP_SCL_IN_DETECT_IBI_EN 0x59 +#define P3H2X4X_TP_SDA_IN_DETECT_IBI_EN 0x5a + +/* Target Ports Status Registers */ +#define P3H2X4X_TP_SCL_IN_LEVEL_STS 0x60 +#define P3H2X4X_TP_SDA_IN_LEVEL_STS 0x61 +#define P3H2X4X_TP_SCL_IN_DETECT_FLG 0x62 +#define P3H2X4X_TP_SDA_IN_DETECT_FLG 0x63 + +/* SMBus Agent Configuration and Status Registers */ +#define P3H2X4X_TP0_SMBUS_AGNT_STS 0x64 +#define P3H2X4X_TP1_SMBUS_AGNT_STS 0x65 +#define P3H2X4X_TP2_SMBUS_AGNT_STS 0x66 +#define P3H2X4X_TP3_SMBUS_AGNT_STS 0x67 +#define P3H2X4X_TP4_SMBUS_AGNT_STS 0x68 +#define P3H2X4X_TP5_SMBUS_AGNT_STS 0x69 +#define P3H2X4X_TP6_SMBUS_AGNT_STS 0x6a +#define P3H2X4X_TP7_SMBUS_AGNT_STS 0x6b +#define P3H2X4X_ONCHIP_TD_AND_SMBUS_AGNT_CONF 0x6c + +/* buf receive flag set */ +#define P3H2X4X_TARGET_BUF_CA_TF BIT(0) +#define P3H2X4X_TARGET_BUF_0_RECEIVE BIT(1) +#define P3H2X4X_TARGET_BUF_1_RECEIVE BIT(2) +#define P3H2X4X_TARGET_BUF_0_1_RECEIVE GENMASK(2, 1) +#define P3H2X4X_TARGET_BUF_OVRFL GENMASK(3, 1) +#define BUF_RECEIVED_FLAG_MASK GENMASK(3, 1) +#define BUF_RECEIVED_FLAG_TF_MASK GENMASK(3, 0) + +#define P3H2X4X_TARGET_AGENT_LOCAL_DEV 0x11 +#define P3H2X4X_TARGET_BUFF_0_PAGE 0x12 +#define P3H2X4X_TARGET_BUFF_1_PAGE 0x13 + +/* Special Function Registers */ +#define P3H2X4X_LDO_AND_CPSEL_STS 0x79 +#define P3H2X4X_CP_SDA1_LEVEL BIT(7) +#define P3H2X4X_CP_SCL1_LEVEL BIT(6) + +#define P3H2X4X_CP_SEL_PIN_INPUT_CODE_MASK GENMASK(5, 4) +#define P3H2X4X_CP_SEL_PIN_INPUT_CODE_GET(x) \ + (((x) & P3H2X4X_CP_SEL_PIN_INPUT_CODE_MASK) >> 4) +#define P3H2X4X_CP_SDA1_SCL1_PINS_CODE_MASK GENMASK(7, 6) +#define P3H2X4X_CP_SDA1_SCL1_PINS_CODE_GET(x) \ + (((x) & P3H2X4X_CP_SDA1_SCL1_PINS_CODE_MASK) >> 6) +#define P3H2X4X_VCCIO1_PWR_GOOD BIT(3) +#define P3H2X4X_VCCIO0_PWR_GOOD BIT(2) +#define P3H2X4X_CP1_VCCIO_PWR_GOOD BIT(1) +#define P3H2X4X_CP0_VCCIO_PWR_GOOD BIT(0) + +#define P3H2X4X_BUS_RESET_SCL_TIMEOUT 0x7a +#define P3H2X4X_ONCHIP_TD_PROTO_ERR_FLG 0x7b +#define P3H2X4X_DEV_CMD 0x7c +#define P3H2X4X_ONCHIP_TD_STS 0x7d +#define P3H2X4X_ONCHIP_TD_ADDR_CONF 0x7e +#define P3H2X4X_PAGE_PTR 0x7f + +/* Paged Transaction Registers */ +#define P3H2X4X_CONTROLLER_BUFFER_PAGE 0x10 +#define P3H2X4X_CONTROLLER_AGENT_BUFF 0x80 +#define P3H2X4X_CONTROLLER_AGENT_BUFF_DATA 0x84 + +#define P3H2X4X_TARGET_BUFF_LENGTH 0x80 +#define P3H2X4X_TARGET_BUFF_ADDRESS 0x81 +#define P3H2X4X_TARGET_BUFF_DATA 0x82 + +#define P3H2X4X_TP_MAX_COUNT 0x08 +#define P3H2X4X_CP_MAX_COUNT 0x02 +#define P3H2X4X_TP_LOCAL_DEV 0x08 + +/* LDO Disable/Enable DT settings */ +#define P3H2X4X_LDO_VOLT_1_0V 0x00 +#define P3H2X4X_LDO_VOLT_1_1V 0x01 +#define P3H2X4X_LDO_VOLT_1_2V 0x02 +#define P3H2X4X_LDO_VOLT_1_8V 0x03 + +#define P3H2X4X_LDO_DISABLED 0x00 +#define P3H2X4X_LDO_ENABLED 0x01 + +#define P3H2X4X_IBI_DISABLED 0x00 +#define P3H2X4X_IBI_ENABLED 0x01 + +/* Pullup selection DT settings */ +#define P3H2X4X_TP_PULLUP_250R 0x00 +#define P3H2X4X_TP_PULLUP_500R 0x01 +#define P3H2X4X_TP_PULLUP_1000R 0x02 +#define P3H2X4X_TP_PULLUP_2000R 0x03 + +#define P3H2X4X_TP_PULLUP_DISABLED 0x00 +#define P3H2X4X_TP_PULLUP_ENABLED 0x01 + +#define P3H2X4X_IO_STRENGTH_20_OHM 0x00 +#define P3H2X4X_IO_STRENGTH_30_OHM 0x01 +#define P3H2X4X_IO_STRENGTH_40_OHM 0x02 +#define P3H2X4X_IO_STRENGTH_50_OHM 0x03 + +#define P3H2X4X_TP_MODE_I3C 0x00 +#define P3H2X4X_TP_MODE_SMBUS 0x01 +#define P3H2X4X_TP_MODE_GPIO 0x02 +#define P3H2X4X_TP_MODE_I2C 0x03 + +#define ONE_BYTE_SIZE 0x01 + +/* holding SDA low when both SMBus Target Agent received data buffers are full. + * This feature can be used as a flow-control mechanism for MCTP applications to + * avoid MCTP transmitters on Target Ports time out when the SMBus agent buffers + * are not serviced in time by upstream controller and only receives write message + * from its downstream ports. + * SMBUS_AGENT_TX_RX_LOOPBACK_EN/TARGET_AGENT_BUF_FULL_SDA_LOW_EN + */ + +#define P3H2X4X_TARGET_AGENT_DFT_IBI_CONF 0x20 +#define P3H2X4X_TARGET_AGENT_DFT_IBI_CONF_MASK 0x21 + +/* Transaction status checking mask */ + +#define P3H2X4X_SMBUS_TRANSACTION_FINISH_FLAG 1 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SHIFT 4 + +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_OK 0 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ADDR_NAK 1 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_DATA_NAK 2 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_WTR_NAK 3 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SYNC_RCV 4 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SYNC_RCVCLR 5 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_FAULT 6 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ARB_LOSS 7 +#define P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SCL_TO 8 + +#define P3H2X4X_TP_BUFFER_STATUS_MASK 0x0f +#define P3H2X4X_TP_TRANSACTION_CODE_MASK 0xf0 + +/* SMBus transaction types fields */ +#define P3H2X4X_SMBUS_400kHz BIT(2) + +/* SMBus polling */ +#define P3H2X4X_POLLING_ROLL_PERIOD_MS 10 + +/* Hub buffer size */ +#define P3H2X4X_CONTROLLER_BUFFER_SIZE 88 +#define P3H2X4X_TARGET_BUFFER_SIZE 80 +#define P3H2X4X_SMBUS_DESCRIPTOR_SIZE 4 +#define P3H2X4X_SMBUS_PAYLOAD_SIZE \ + (P3H2X4X_CONTROLLER_BUFFER_SIZE - P3H2X4X_SMBUS_DESCRIPTOR_SIZE) +#define P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE (P3H2X4X_TARGET_BUFFER_SIZE - 2) + +/* Hub SMBus transaction time */ +#define P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(x) ((20 * (x)) + 80) + +#define P3H2X4X_NO_PAGE_PER_TP 4 + +#define P3H2X4X_MAX_PAYLOAD_LEN 2 +#define P3H2X4X_NUM_SLOTS 6 + +#define P3H2X4X_HUB_ID 0 + +#define P3H2X4X_SET_BIT(n) BIT(n) + +enum p3h2x4x_tp { + TP_0, + TP_1, + TP_2, + TP_3, + TP_4, + TP_5, + TP_6, + TP_7, +}; + +enum p3h2x4x_rcv_buf { + RCV_BUF_0, + RCV_BUF_1, + RCV_BUF_OF, +}; + +struct tp_configuration { + bool pullup_en; + bool ibi_en; + bool always_enable; + int mode; +}; + +struct hub_configuration { + int tp0145_pullup; + int tp2367_pullup; + int cp0_io_strength; + int cp1_io_strength; + int tp0145_io_strength; + int tp2367_io_strength; + struct tp_configuration tp_config[P3H2X4X_TP_MAX_COUNT]; +}; + +struct tp_bus { + bool is_registered; /* bus was registered in the framework. */ + u8 tp_mask; + u8 tp_port; + struct mutex port_mutex; /* per port mutex */ + struct device_node *of_node; + struct i2c_client *tp_smbus_client; + struct i2c_adapter *tp_smbus_adapter; + struct i3c_hub_controller hub_controller; + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub; +}; + +struct p3h2x4x_i3c_hub_dev { + struct device *dev; + struct regmap *regmap; + struct mutex etx_mutex; /* all port mutex */ + struct i3c_device *i3cdev; + struct i2c_client *i2c_client; + struct hub_configuration hub_config; + struct tp_bus tp_bus[P3H2X4X_TP_MAX_COUNT]; + struct i3c_hub *hub; +}; + +/** + * p3h2x4x_tp_smbus_algo - add i2c adapter for target port configured as SMBus. + * @priv: p3h2x4x device structure. + * @tp: target port. + * Return: 0 in case of success, a negative EINVAL code if the error. + */ +int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub); + +/** + * p3h2x4x_tp_i3c_algo - register i3c controller for target port configured as I3C. + * @priv: p3h2x4x device structure. + * @tp: target port. + * Return: 0 in case of success, a negative EINVAL code if the error. + */ +int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub); + +#endif /* P3H2840_I3C_HUB_H */ diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_common.c b/drivers/i3c/hub/p3h2840_i3c_hub_common.c new file mode 100644 index 000000000000..f1a24a3d3ffa --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c @@ -0,0 +1,347 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * This P3H2X4X driver file implements functions for Hub probe and DT parsing. + */ + +#include +#include +#include +#include +#include + +#include "p3h2840_i3c_hub.h" + +/* LDO voltage DT settings */ +#define P3H2X4X_DT_LDO_VOLT_1_0V 1000000 +#define P3H2X4X_DT_LDO_VOLT_1_1V 1100000 +#define P3H2X4X_DT_LDO_VOLT_1_2V 1200000 +#define P3H2X4X_DT_LDO_VOLT_1_8V 1800000 + +static const int p3h2x4x_pullup_tbl[] = { + 250, 500, 1000, 2000 +}; + +static const int p3h2x4x_io_strength_tbl[] = { + 20, 30, 40, 50 +}; + +static u8 p3h2x4x_pullup_dt_to_reg(int dt_value) +{ + return find_closest(dt_value, p3h2x4x_pullup_tbl, + ARRAY_SIZE(p3h2x4x_pullup_tbl)); +} + +static u8 p3h2x4x_io_strength_dt_to_reg(int dt_value) +{ + return find_closest(dt_value, p3h2x4x_io_strength_tbl, + ARRAY_SIZE(p3h2x4x_io_strength_tbl)); +} + +static int p3h2x4x_configure_pullup(struct device *dev) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev); + u8 pullup; + + pullup = P3H2X4X_TP0145_PULLUP_CONF(p3h2x4x_pullup_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.tp0145_pullup)); + + pullup |= P3H2X4X_TP2367_PULLUP_CONF(p3h2x4x_pullup_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.tp2367_pullup)); + + return regmap_update_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_LDO_AND_PULLUP_CONF, + P3H2X4X_PULLUP_CONF_MASK, pullup); +} + +static int p3h2x4x_configure_io_strength(struct device *dev) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev); + u8 io_strength; + + io_strength = P3H2X4X_CP0_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.cp0_io_strength)); + + io_strength |= P3H2X4X_CP1_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.cp1_io_strength)); + + io_strength |= P3H2X4X_TP0145_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.tp0145_io_strength)); + + io_strength |= P3H2X4X_TP2367_IO_STRENGTH(p3h2x4x_io_strength_dt_to_reg + (p3h2x4x_i3c_hub->hub_config.tp2367_io_strength)); + + return regmap_update_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_IO_STRENGTH, + P3H2X4X_IO_STRENGTH_MASK, io_strength); +} + +static int p3h2x4x_configure_ldo(struct device *dev) +{ + static const char * const supplies[] = { + "vcc1", + "vcc2", + "vcc3", + "vcc4" + }; + int ret, i; + + for (i = 0; i < ARRAY_SIZE(supplies); i++) { + ret = devm_regulator_get_enable_optional(dev->parent, supplies[i]); + if (ret == -EPROBE_DEFER) + return -EPROBE_DEFER; + + if (ret && ret != -ENODEV) + dev_warn(dev, "Failed to enable %s (%d)\n", + supplies[i], ret); + } + + /* This delay is required for the regulator to stabilize its output voltage */ + fsleep(5000); + + return 0; +} + +static int p3h2x4x_configure_tp(struct device *dev) +{ + struct p3h2x4x_i3c_hub_dev *hub = dev_get_drvdata(dev); + u8 mode = 0, smbus = 0, pullup = 0, target_port = 0; + int tp, ret; + + for (tp = 0; tp < P3H2X4X_TP_MAX_COUNT; tp++) { + pullup |= hub->hub_config.tp_config[tp].pullup_en ? P3H2X4X_SET_BIT(tp) : 0; + mode |= (hub->hub_config.tp_config[tp].mode != P3H2X4X_TP_MODE_I3C) ? + P3H2X4X_SET_BIT(tp) : 0; + smbus |= (hub->hub_config.tp_config[tp].mode == P3H2X4X_TP_MODE_SMBUS) ? + P3H2X4X_SET_BIT(tp) : 0; + target_port |= (hub->tp_bus[tp].tp_mask == P3H2X4X_SET_BIT(tp)) ? + hub->tp_bus[tp].tp_mask : 0; + } + + ret = regmap_update_bits(hub->regmap, P3H2X4X_TP_PULLUP_EN, pullup, pullup); + if (ret) + return ret; + + ret = regmap_update_bits(hub->regmap, P3H2X4X_TP_IO_MODE_CONF, mode, mode); + if (ret) + return ret; + + ret = regmap_update_bits(hub->regmap, P3H2X4X_TP_SMBUS_AGNT_EN, smbus, smbus); + if (ret) + return ret; + + if (target_port & ~smbus) { + ret = regmap_write(hub->regmap, P3H2X4X_CP_MUX_SET, + P3H2X4X_CONTROLLER_PORT_MUX_REQ); + if (ret) + return ret; + } + + return regmap_update_bits(hub->regmap, P3H2X4X_TP_ENABLE, target_port, target_port); +} + +static int p3h2x4x_configure_hw(struct device *dev) +{ + int ret; + + ret = p3h2x4x_configure_ldo(dev); + if (ret) + return ret; + + ret = p3h2x4x_configure_pullup(dev); + if (ret) + return ret; + + ret = p3h2x4x_configure_io_strength(dev); + if (ret) + return ret; + + return p3h2x4x_configure_tp(dev); +} + +static void p3h2x4x_get_target_port_dt_conf(struct device *dev, + const struct device_node *node) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev); + u64 tp_port; + + for_each_available_child_of_node_scoped(node, dev_node) { + if (of_property_read_reg(dev_node, 0, &tp_port, NULL)) + continue; + + if (tp_port < P3H2X4X_TP_MAX_COUNT) { + p3h2x4x_i3c_hub->tp_bus[tp_port].of_node = dev_node; + p3h2x4x_i3c_hub->tp_bus[tp_port].tp_mask = P3H2X4X_SET_BIT(tp_port); + p3h2x4x_i3c_hub->tp_bus[tp_port].p3h2x4x_i3c_hub = p3h2x4x_i3c_hub; + p3h2x4x_i3c_hub->tp_bus[tp_port].tp_port = tp_port; + } + } +} + +static void p3h2x4x_parse_tp_dt_settings(struct device *dev, + const struct device_node *node, + struct tp_configuration tp_config[]) +{ + u64 id; + + for_each_available_child_of_node_scoped(node, tp_node) { + if (of_property_read_reg(tp_node, 0, &id, NULL)) + continue; + + if (id >= P3H2X4X_TP_MAX_COUNT) { + dev_warn(dev, "Invalid target port index found in DT: %lli\n", id); + continue; + } + + if (strcmp(tp_node->name, "i3c") == 0) + tp_config[id].mode = P3H2X4X_TP_MODE_I3C; + + if (strcmp(tp_node->name, "i2c") == 0) + tp_config[id].mode = P3H2X4X_TP_MODE_I2C; + + if (strcmp(tp_node->name, "smbus") == 0) + tp_config[id].mode = P3H2X4X_TP_MODE_SMBUS; + + tp_config[id].pullup_en = + of_property_read_bool(tp_node, "nxp,pullup-enable"); + } +} + +static void p3h2x4x_get_hub_dt_conf(struct device *dev, + const struct device_node *node) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev); + + of_property_read_u32(node, "nxp,tp0145-pullup-ohms", + &p3h2x4x_i3c_hub->hub_config.tp0145_pullup); + of_property_read_u32(node, "nxp,tp2367-pullup-ohms", + &p3h2x4x_i3c_hub->hub_config.tp2367_pullup); + of_property_read_u32(node, "nxp,cp0-io-strength-ohms", + &p3h2x4x_i3c_hub->hub_config.cp0_io_strength); + of_property_read_u32(node, "nxp,cp1-io-strength-ohms", + &p3h2x4x_i3c_hub->hub_config.cp1_io_strength); + of_property_read_u32(node, "nxp,tp0145-io-strength-ohms", + &p3h2x4x_i3c_hub->hub_config.tp0145_io_strength); + of_property_read_u32(node, "nxp,tp2367-io-strength-ohms", + &p3h2x4x_i3c_hub->hub_config.tp2367_io_strength); + + p3h2x4x_parse_tp_dt_settings(dev, node, p3h2x4x_i3c_hub->hub_config.tp_config); +} + +static void p3h2x4x_default_configuration(struct device *dev) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dev_get_drvdata(dev); + int tp_count; + + p3h2x4x_i3c_hub->hub_config.tp0145_pullup = P3H2X4X_TP_PULLUP_500R; + p3h2x4x_i3c_hub->hub_config.tp2367_pullup = P3H2X4X_TP_PULLUP_500R; + p3h2x4x_i3c_hub->hub_config.cp0_io_strength = P3H2X4X_IO_STRENGTH_20_OHM; + p3h2x4x_i3c_hub->hub_config.cp1_io_strength = P3H2X4X_IO_STRENGTH_20_OHM; + p3h2x4x_i3c_hub->hub_config.tp0145_io_strength = P3H2X4X_IO_STRENGTH_20_OHM; + p3h2x4x_i3c_hub->hub_config.tp2367_io_strength = P3H2X4X_IO_STRENGTH_20_OHM; + + for (tp_count = 0; tp_count < P3H2X4X_TP_MAX_COUNT; ++tp_count) + p3h2x4x_i3c_hub->hub_config.tp_config[tp_count].mode = P3H2X4X_TP_MODE_I3C; +} + +static int p3h2x4x_i3c_hub_probe(struct platform_device *pdev) +{ + struct p3h2x4x_dev *p3h2x4x = dev_get_drvdata(pdev->dev.parent); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub; + struct device *dev = &pdev->dev; + struct device_node *node; + int ret, i; + + p3h2x4x_i3c_hub = devm_kzalloc(dev, sizeof(*p3h2x4x_i3c_hub), GFP_KERNEL); + if (!p3h2x4x_i3c_hub) + return -ENOMEM; + + p3h2x4x_i3c_hub->regmap = p3h2x4x->regmap; + p3h2x4x_i3c_hub->dev = dev; + + platform_set_drvdata(pdev, p3h2x4x_i3c_hub); + + p3h2x4x_default_configuration(dev); + + ret = devm_mutex_init(dev, &p3h2x4x_i3c_hub->etx_mutex); + if (ret) + return ret; + + for (i = 0; i < P3H2X4X_TP_MAX_COUNT; i++) { + ret = devm_mutex_init(dev, &p3h2x4x_i3c_hub->tp_bus[i].port_mutex); + if (ret) + return ret; + } + + /* get hub node from DT */ + node = dev->parent->of_node; + if (!node) + return dev_err_probe(dev, -ENODEV, "No Device Tree entry found\n"); + + p3h2x4x_get_hub_dt_conf(dev, node); + p3h2x4x_get_target_port_dt_conf(dev, node); + + /* Unlock access to protected registers */ + ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_UNLOCK_CODE); + if (ret) + return dev_err_probe(dev, ret, "Failed to unlock HUB's protected registers\n"); + + ret = p3h2x4x_configure_hw(dev); + if (ret) + return dev_err_probe(dev, ret, "Failed to configure the HUB\n"); + + /* Register virtual I3C master controllers for I3C target ports */ + if (p3h2x4x->i3cdev) { + p3h2x4x_i3c_hub->i3cdev = p3h2x4x->i3cdev; + i3cdev_set_drvdata(p3h2x4x->i3cdev, p3h2x4x_i3c_hub); + ret = p3h2x4x_tp_i3c_algo(p3h2x4x_i3c_hub); + if (ret) + return dev_err_probe(dev, ret, "Failed to register i3c bus\n"); + } + + /* Register virtual I2C adapters for SMBus target ports */ + ret = p3h2x4x_tp_smbus_algo(p3h2x4x_i3c_hub); + if (ret) + return dev_err_probe(dev, ret, "Failed to add i2c adapter\n"); + + /* Lock access to protected registers */ + ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_DEV_REG_PROTECTION_CODE, + P3H2X4X_REGISTERS_LOCK_CODE); + if (ret) + return dev_err_probe(dev, ret, "Failed to lock HUB's protected registers\n"); + + return 0; +} + +static void p3h2x4x_i3c_hub_remove(struct platform_device *pdev) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = platform_get_drvdata(pdev); + struct p3h2x4x_dev *p3h2x4x = dev_get_drvdata(pdev->dev.parent); + u8 i; + + for (i = 0; i < P3H2X4X_TP_MAX_COUNT; i++) { + if (!p3h2x4x_i3c_hub->tp_bus[i].is_registered) + continue; + + if (p3h2x4x_i3c_hub->hub_config.tp_config[i].mode == P3H2X4X_TP_MODE_SMBUS) + i2c_del_adapter(p3h2x4x_i3c_hub->tp_bus[i].tp_smbus_adapter); + else if (p3h2x4x_i3c_hub->hub_config.tp_config[i].mode == P3H2X4X_TP_MODE_I3C) + i3c_master_unregister(&p3h2x4x_i3c_hub->tp_bus[i] + .hub_controller.controller); + } +} + +static struct platform_driver p3h2x4x_i3c_hub_driver = { + .driver = { + .name = "p3h2x4x-i3c-hub", + .probe_type = PROBE_PREFER_ASYNCHRONOUS, + }, + .probe = p3h2x4x_i3c_hub_probe, + .remove = p3h2x4x_i3c_hub_remove, +}; +module_platform_driver(p3h2x4x_i3c_hub_driver); + +MODULE_AUTHOR("Aman Kumar Pandey "); +MODULE_AUTHOR("Vikash Bansal "); +MODULE_AUTHOR("Lakshay Piplani "); +MODULE_DESCRIPTION("P3H2X4X I3C HUB driver"); +MODULE_LICENSE("GPL"); diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c new file mode 100644 index 000000000000..38505dda0e81 --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * This P3H2X4X driver file contain functions for I3C virtual Bus creation, connect/disconnect + * hub network and read/write. + */ +#include +#include +#include + +#include "p3h2840_i3c_hub.h" + +static inline struct tp_bus * +p3h2x4x_bus_from_controller(struct i3c_master_controller *controller) +{ + struct i3c_hub_controller *hub_controller; + + hub_controller = container_of(controller, struct i3c_hub_controller, controller); + + return container_of(hub_controller, struct tp_bus, hub_controller); +} + +static void p3h2x4x_hub_enable_port(struct i3c_master_controller *controller) +{ + struct tp_bus *bus = p3h2x4x_bus_from_controller(controller); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = bus->p3h2x4x_i3c_hub; + + if (p3h2x4x_i3c_hub->hub_config.tp_config[bus->tp_port].always_enable) + return; + + regmap_set_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP_NET_CON_CONF, bus->tp_mask); +} + +static void p3h2x4x_hub_disable_port(struct i3c_master_controller *controller) +{ + struct tp_bus *bus = p3h2x4x_bus_from_controller(controller); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = bus->p3h2x4x_i3c_hub; + + if (p3h2x4x_i3c_hub->hub_config.tp_config[bus->tp_port].always_enable) + return; + + regmap_clear_bits(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP_NET_CON_CONF, bus->tp_mask); +} + +static const struct i3c_hub_ops p3h2x4x_hub_ops = { + .enable_port = p3h2x4x_hub_enable_port, + .disable_port = p3h2x4x_hub_disable_port, +}; + +static void p3h2x4x_unregister_i3c_master(void *data) +{ + struct i3c_master_controller *controller = data; + + i3c_master_unregister(controller); +} + +/** + * p3h2x4x_tp_i3c_algo - register i3c master for target port who + * configured as i3c. + * @p3h2x4x_hub: p3h2x4x device structure. + * Return: 0 in case of success, negative error code on failur. + */ +int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_hub) +{ + struct i3c_master_controller *parent = i3c_dev_get_master(p3h2x4x_hub->i3cdev->desc); + u8 tp, ntwk_mask = 0; + int ret; + + p3h2x4x_hub->hub = devm_kzalloc(p3h2x4x_hub->dev, + sizeof(*p3h2x4x_hub->hub), + GFP_KERNEL); + + if (!p3h2x4x_hub->hub) + return -ENOMEM; + + i3c_hub_init(p3h2x4x_hub->hub, + &p3h2x4x_hub_ops, + p3h2x4x_hub->i3cdev); + + if (IS_ERR(p3h2x4x_hub->hub)) + return PTR_ERR(p3h2x4x_hub->hub); + + for (tp = 0; tp < P3H2X4X_TP_MAX_COUNT; tp++) { + if (!p3h2x4x_hub->tp_bus[tp].of_node || + p3h2x4x_hub->hub_config.tp_config[tp].mode != P3H2X4X_TP_MODE_I3C) + continue; + + /* Assign DT node for this TP */ + p3h2x4x_hub->dev->of_node = p3h2x4x_hub->tp_bus[tp].of_node; + + struct i3c_hub_controller *hub_controller = + &p3h2x4x_hub->tp_bus[tp].hub_controller; + struct i3c_master_controller *controller = &hub_controller->controller; + + hub_controller->parent = parent; + hub_controller->hub = p3h2x4x_hub->hub; + + dev_set_drvdata(&controller->dev, hub_controller); + + ret = i3c_master_register(controller, + p3h2x4x_hub->dev, + i3c_hub_master_ops(), + false); + + if (ret) + return ret; + + ret = devm_add_action_or_reset(p3h2x4x_hub->dev, + p3h2x4x_unregister_i3c_master, + controller); + if (ret) + return ret; + + /* Perform DAA */ + ret = i3c_master_do_daa(parent); + if (ret) + return ret; + + ntwk_mask |= p3h2x4x_hub->tp_bus[tp].tp_mask; + p3h2x4x_hub->tp_bus[tp].is_registered = true; + p3h2x4x_hub->hub_config.tp_config[tp].always_enable = true; + } + return regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_mask); +} diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c new file mode 100644 index 000000000000..43639f04b77a --- /dev/null +++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2025-2026 NXP + * This P3H2X4X driver file contain functions for SMBus/I2C virtual Bus creation and read/write. + */ +#include +#include + +#include "p3h2840_i3c_hub.h" + +enum p3h2x4x_smbus_desc_idx { + P3H2X4X_DESC_ADDR, + P3H2X4X_DESC_TYPE, + P3H2X4X_DESC_WRITE_LEN, + P3H2X4X_DESC_READ_LEN, +}; + +static int p3h2x4x_read_smbus_transaction_status(struct p3h2x4x_i3c_hub_dev *hub, + u8 target_port_status, + u8 data_length) +{ + u32 status_read; + u8 status; + int ret; + + fsleep(P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length)); + + ret = regmap_read(hub->regmap, target_port_status, &status_read); + if (ret) + return ret; + + status = (u8)status_read; + + status = (status & P3H2X4X_TP_TRANSACTION_CODE_MASK) + >> P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SHIFT; + + switch (status) { + case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_OK: + return 0; + case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ADDR_NAK: + return -ENXIO; + case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_DATA_NAK: + return -EIO; + case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_SCL_TO: + return -ETIMEDOUT; + case P3H2X4X_SMBUS_CNTRL_STATUS_TXN_ARB_LOSS: + return -EAGAIN; + default: + return -EIO; + } +} + +/* + * p3h2x4x_tp_i2c_xfer_msg() - This starts a SMBus write transaction by writing a descriptor + * and a message to the p3h2x4x registers. Controller buffer page is determined by multiplying the + * target port index by four and adding the base page number to it. + */ +static int p3h2x4x_tp_i2c_xfer_msg(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub, + struct i2c_msg *xfers, + u8 target_port, + u8 nxfers_i, u8 rw) +{ + u8 controller_buffer_page = P3H2X4X_CONTROLLER_BUFFER_PAGE + 4 * target_port; + u8 target_port_status = P3H2X4X_TP0_SMBUS_AGNT_STS + target_port; + u8 desc[P3H2X4X_SMBUS_DESCRIPTOR_SIZE] = { 0 }; + u8 transaction_type = P3H2X4X_SMBUS_400kHz; + int write_length, read_length; + u8 addr = xfers[nxfers_i].addr; + u8 rw_address = 2 * addr; + int ret, ret2; + + if (rw == 2) { /* write and read */ + write_length = xfers[nxfers_i].len; + read_length = xfers[nxfers_i + 1].len; + } else if (rw == 1) { + rw_address |= P3H2X4X_SET_BIT(0); + write_length = 0; + read_length = xfers[nxfers_i].len; + } else { + write_length = xfers[nxfers_i].len; + read_length = 0; + } + + desc[P3H2X4X_DESC_ADDR] = rw_address; + if (rw == 2) + desc[P3H2X4X_DESC_TYPE] = transaction_type | P3H2X4X_SET_BIT(0); + else + desc[P3H2X4X_DESC_TYPE] = transaction_type; + desc[P3H2X4X_DESC_WRITE_LEN] = write_length; + desc[P3H2X4X_DESC_READ_LEN] = read_length; + + ret = regmap_write(p3h2x4x_i3c_hub->regmap, target_port_status, + P3H2X4X_TP_BUFFER_STATUS_MASK); + if (ret) + goto out; + + ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, controller_buffer_page); + + if (ret) + goto out; + + ret = regmap_bulk_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_CONTROLLER_AGENT_BUFF, + desc, P3H2X4X_SMBUS_DESCRIPTOR_SIZE); + + if (ret) + goto out; + + if (!(rw % 2)) { + ret = regmap_bulk_write(p3h2x4x_i3c_hub->regmap, + P3H2X4X_CONTROLLER_AGENT_BUFF_DATA, + xfers[nxfers_i].buf, xfers[nxfers_i].len); + if (ret) + goto out; + } + + ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP_SMBUS_AGNT_TRANS_START, + p3h2x4x_i3c_hub->tp_bus[target_port].tp_mask); + + if (ret) + goto out; + + ret = p3h2x4x_read_smbus_transaction_status(p3h2x4x_i3c_hub, + target_port_status, + (write_length + read_length)); + if (ret) + goto out; + + if (rw) { + if (rw == 2) + nxfers_i += 1; + + ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap, + P3H2X4X_CONTROLLER_AGENT_BUFF_DATA + write_length, + xfers[nxfers_i].buf, xfers[nxfers_i].len); + if (ret) + goto out; + } +out: + ret2 = regmap_write(p3h2x4x_i3c_hub->regmap, + P3H2X4X_PAGE_PTR, 0x00); + if (!ret && ret2) + ret = ret2; + + return ret; +} + +/* + * This function will be called whenever you call I2C read, write APIs like + * i2c_master_send(), i2c_master_recv() etc. + */ +static s32 p3h2x4x_tp_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) +{ + int ret_sum = 0, ret; + u8 msg_count, rw; + + struct tp_bus *bus = i2c_get_adapdata(adap); + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = bus->p3h2x4x_i3c_hub; + + guard(mutex)(&p3h2x4x_i3c_hub->etx_mutex); + guard(mutex)(&bus->port_mutex); + + for (msg_count = 0; msg_count < num; msg_count++) { + if (msgs[msg_count].len > P3H2X4X_SMBUS_PAYLOAD_SIZE) { + dev_err(p3h2x4x_i3c_hub->dev, + "Message nr. %d not sent - length over %d bytes.\n", + msg_count, P3H2X4X_SMBUS_PAYLOAD_SIZE); + continue; + } + + rw = (msgs[msg_count].flags & I2C_M_RD) ? 1 : 0; + if (!rw) { + /* If a read message is immediately followed by a write message to + * the same address, consider combining them into a single transaction. + */ + if (msg_count + 1 < num && + msgs[msg_count].addr == msgs[msg_count + 1].addr && + (msgs[msg_count + 1].flags & I2C_M_RD)) { + rw = 2; + msg_count += 1; + ret_sum += 1; + } + } + + ret = p3h2x4x_tp_i2c_xfer_msg(p3h2x4x_i3c_hub, + msgs, + bus->tp_port, + (rw == 2) ? (msg_count - 1) : msg_count, + rw); + if (ret) + return ret; + + ret_sum++; + } + return ret_sum; +} + +static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter) +{ + return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA; +} + +/* + * I2C algorithm Structure + */ +static struct i2c_algorithm p3h2x4x_tp_i2c_algorithm = { + .master_xfer = p3h2x4x_tp_i2c_xfer, + .functionality = p3h2x4x_tp_smbus_funcs, +}; + +/** + * p3h2x4x_tp_smbus_algo - add i2c adapter for target port who + * configured as SMBus. + * @hub: p3h2x4x device structure. + * Return: 0 in case of success, negative error code on failur. + */ +int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *hub) +{ + int ret; + u8 tp; + + for (tp = 0; tp < P3H2X4X_TP_MAX_COUNT; tp++) { + if (!hub->tp_bus[tp].of_node || + hub->hub_config.tp_config[tp].mode != P3H2X4X_TP_MODE_SMBUS) + continue; + + /* Allocate adapter */ + struct i2c_adapter *smbus_adapter = + devm_kzalloc(hub->dev, sizeof(*smbus_adapter), GFP_KERNEL); + if (!smbus_adapter) + return -ENOMEM; + + /* Initialize adapter */ + smbus_adapter->owner = THIS_MODULE; + smbus_adapter->class = I2C_CLASS_HWMON; + smbus_adapter->algo = &p3h2x4x_tp_i2c_algorithm; + smbus_adapter->dev.parent = hub->dev; + smbus_adapter->dev.of_node = hub->tp_bus[tp].of_node; + snprintf(smbus_adapter->name, sizeof(smbus_adapter->name), + "p3h2x4x-i3c-hub.tp-port-%d", tp); + + i2c_set_adapdata(smbus_adapter, &hub->tp_bus[tp]); + + /* Register adapter */ + ret = i2c_add_adapter(smbus_adapter); + if (ret) { + devm_kfree(hub->dev, smbus_adapter); + return ret; + } + + hub->tp_bus[tp].is_registered = true; + hub->hub_config.tp_config[tp].ibi_en = false; + hub->tp_bus[tp].tp_smbus_adapter = smbus_adapter; + } + + /* + * holding SDA low when both SMBus Target Agent received data buffers are full. + * This feature can be used as a flow-control mechanism for MCTP applications to + * avoid MCTP transmitters on Target Ports time out when the SMBus agent buffers + * are not serviced in time by upstream controller and only receives write message + * from its downstream ports. + */ + ret = regmap_update_bits(hub->regmap, P3H2X4X_ONCHIP_TD_AND_SMBUS_AGNT_CONF, + P3H2X4X_TARGET_AGENT_DFT_IBI_CONF_MASK, + P3H2X4X_TARGET_AGENT_DFT_IBI_CONF); + if (ret) + return ret; + + return regmap_write(hub->regmap, P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG, P3H2X4X_IBI_DISABLED); +} -- 2.25.1 From lakshay.piplani at nxp.com Sun May 24 23:42:09 2026 From: lakshay.piplani at nxp.com (Lakshay Piplani) Date: Mon, 25 May 2026 12:12:09 +0530 Subject: [PATCH v10 9/9] i3c: hub: p3h2x4x: Add SMBus slave mode support In-Reply-To: <20260525064209.2263045-1-lakshay.piplani@nxp.com> References: <20260525064209.2263045-1-lakshay.piplani@nxp.com> Message-ID: <20260525064209.2263045-10-lakshay.piplani@nxp.com> Add SMBus slave mode support for the P3H2x4x hub SMBus target ports. The hub SMBus slave agent can receive downstream payloads into target buffers and report receive events through IBI. Add CONFIG_I2C_SLAVE to support the receive path and forward the received payloads to the registered I2C slave client through i2c_slave_event(). Signed-off-by: Lakshay Piplani Signed-off-by: Aman Kumar Pandey Signed-off-by: Vikash Bansal --- Changes in v10: - Split SMBus slave mode support into a separate patch --- --- drivers/i3c/hub/p3h2840_i3c_hub.h | 10 ++ drivers/i3c/hub/p3h2840_i3c_hub_common.c | 6 + drivers/i3c/hub/p3h2840_i3c_hub_i3c.c | 17 +++ drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 181 +++++++++++++++++++++++ 4 files changed, 214 insertions(+) diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h index d69fafbac584..84d9c66547c6 100644 --- a/drivers/i3c/hub/p3h2840_i3c_hub.h +++ b/drivers/i3c/hub/p3h2840_i3c_hub.h @@ -324,4 +324,14 @@ int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub); */ int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub); +/** + * p3h2x4x_ibi_handler - IBI handler. + * @i3cdev: i3c device. + * @payload: two byte IBI payload data. + */ +#if IS_ENABLED(CONFIG_I2C_SLAVE) +void p3h2x4x_ibi_handler(struct i3c_device *i3cdev, + const struct i3c_ibi_payload *payload); +#endif + #endif /* P3H2840_I3C_HUB_H */ diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_common.c b/drivers/i3c/hub/p3h2840_i3c_hub_common.c index f1a24a3d3ffa..26d5e13455ca 100644 --- a/drivers/i3c/hub/p3h2840_i3c_hub_common.c +++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c @@ -328,6 +328,12 @@ static void p3h2x4x_i3c_hub_remove(struct platform_device *pdev) i3c_master_unregister(&p3h2x4x_i3c_hub->tp_bus[i] .hub_controller.controller); } +#if IS_ENABLED(CONFIG_I2C_SLAVE) + if (p3h2x4x->i3cdev) { + i3c_device_disable_ibi(p3h2x4x->i3cdev); + i3c_device_free_ibi(p3h2x4x->i3cdev); + } +#endif } static struct platform_driver p3h2x4x_i3c_hub_driver = { diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c index 38505dda0e81..544d961d0b8a 100644 --- a/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c +++ b/drivers/i3c/hub/p3h2840_i3c_hub_i3c.c @@ -10,6 +10,14 @@ #include "p3h2840_i3c_hub.h" +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static const struct i3c_ibi_setup p3h2x4x_ibireq = { + .handler = p3h2x4x_ibi_handler, + .max_payload_len = P3H2X4X_MAX_PAYLOAD_LEN, + .num_slots = P3H2X4X_NUM_SLOTS, +}; +#endif + static inline struct tp_bus * p3h2x4x_bus_from_controller(struct i3c_master_controller *controller) { @@ -120,5 +128,14 @@ int p3h2x4x_tp_i3c_algo(struct p3h2x4x_i3c_hub_dev *p3h2x4x_hub) p3h2x4x_hub->tp_bus[tp].is_registered = true; p3h2x4x_hub->hub_config.tp_config[tp].always_enable = true; } +#if IS_ENABLED(CONFIG_I2C_SLAVE) + ret = i3c_device_request_ibi(p3h2x4x_hub->i3cdev, &p3h2x4x_ibireq); + if (ret) + return ret; + + ret = i3c_device_enable_ibi(p3h2x4x_hub->i3cdev); + if (ret) + return ret; +#endif return regmap_write(p3h2x4x_hub->regmap, P3H2X4X_TP_NET_CON_CONF, ntwk_mask); } diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c index 43639f04b77a..12fac276b041 100644 --- a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c +++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c @@ -15,6 +15,135 @@ enum p3h2x4x_smbus_desc_idx { P3H2X4X_DESC_READ_LEN, }; +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static void p3h2x4x_read_smbus_agent_rx_buf(struct i3c_device *i3cdev, enum p3h2x4x_rcv_buf rfbuf, + enum p3h2x4x_tp tp, bool is_of) +{ + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = i3cdev_get_drvdata(i3cdev); + u8 slave_rx_buffer[P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 }; + u8 target_buffer_page, flag_clear = 0x0f, temp, i; + u32 packet_len, slave_address, ret; + + target_buffer_page = (((rfbuf) ? P3H2X4X_TARGET_BUFF_1_PAGE : P3H2X4X_TARGET_BUFF_0_PAGE) + + (P3H2X4X_NO_PAGE_PER_TP * tp)); + ret = regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, target_buffer_page); + if (ret) + goto ibi_err; + + /* read buffer length */ + ret = regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_LENGTH, &packet_len); + if (ret) + goto ibi_err; + + if (packet_len) + packet_len = packet_len - 1; + + if (packet_len > P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE) { + dev_err(&i3cdev->dev, "Received message too big for p3h2x4x buffer\n"); + goto ibi_err; + } + + /* read slave address */ + ret = regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_ADDRESS, &slave_address); + if (ret) + goto ibi_err; + + /* read data */ + if (packet_len) { + ret = regmap_bulk_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TARGET_BUFF_DATA, + slave_rx_buffer, packet_len); + if (ret) + goto ibi_err; + } + + if (is_of) + flag_clear = BUF_RECEIVED_FLAG_TF_MASK; + else + flag_clear = (((rfbuf == RCV_BUF_0) ? P3H2X4X_TARGET_BUF_0_RECEIVE : + P3H2X4X_TARGET_BUF_1_RECEIVE)); + + /* notify slave driver about received data */ + if ((p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client->addr & 0x7f) == (slave_address >> 1)) { + i2c_slave_event(p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client, + I2C_SLAVE_WRITE_REQUESTED, (u8 *)&slave_address); + for (i = 0; i < packet_len; i++) { + temp = slave_rx_buffer[i]; + i2c_slave_event(p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client, + I2C_SLAVE_WRITE_RECEIVED, &temp); + } + i2c_slave_event(p3h2x4x_i3c_hub->tp_bus[tp].tp_smbus_client, I2C_SLAVE_STOP, &temp); + } + +ibi_err: + regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_PAGE_PTR, 0x00); + regmap_write(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP0_SMBUS_AGNT_STS + tp, flag_clear); +} + +/** + * p3h2x4x_ibi_handler - IBI handler. + * @i3cdev: i3c device. + * @payload: two byte IBI payload data. + * + */ +void p3h2x4x_ibi_handler(struct i3c_device *i3cdev, + const struct i3c_ibi_payload *payload) +{ + u32 target_port_status, payload_byte_one, payload_byte_two; + struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub; + u32 ret, i; + + payload_byte_one = (*(int *)payload->data); + + if (!(payload_byte_one & P3H2X4X_SMBUS_AGENT_EVENT_FLAG_STATUS)) + return; + + p3h2x4x_i3c_hub = i3cdev_get_drvdata(i3cdev); + + if (!p3h2x4x_i3c_hub || !p3h2x4x_i3c_hub->regmap) + return; + + payload_byte_two = (*(int *)(payload->data + 4)); + guard(mutex)(&p3h2x4x_i3c_hub->etx_mutex); + + for (i = 0; i < P3H2X4X_TP_MAX_COUNT; ++i) { + if (p3h2x4x_i3c_hub->tp_bus[i].is_registered && (payload_byte_two >> i) & 0x01) { + ret = regmap_read(p3h2x4x_i3c_hub->regmap, P3H2X4X_TP0_SMBUS_AGNT_STS + i, + &target_port_status); + if (ret) { + dev_err(&i3cdev->dev, "target port read status failed %d\n", ret); + return; + } + + /* process data receive buffer */ + switch (target_port_status & BUF_RECEIVED_FLAG_MASK) { + case P3H2X4X_TARGET_BUF_CA_TF: + break; + case P3H2X4X_TARGET_BUF_0_RECEIVE: + p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_0, i, false); + break; + case P3H2X4X_TARGET_BUF_1_RECEIVE: + p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_1, i, false); + break; + case P3H2X4X_TARGET_BUF_0_1_RECEIVE: + p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_0, i, false); + p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_1, i, false); + break; + case P3H2X4X_TARGET_BUF_OVRFL: + p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_0, i, false); + p3h2x4x_read_smbus_agent_rx_buf(i3cdev, RCV_BUF_1, i, true); + dev_err(&i3cdev->dev, "Overflow, reading buffer zero and one\n"); + break; + default: + regmap_write(p3h2x4x_i3c_hub->regmap, + P3H2X4X_TP0_SMBUS_AGNT_STS + i, + BUF_RECEIVED_FLAG_TF_MASK); + break; + } + } + } +} +#endif + static int p3h2x4x_read_smbus_transaction_status(struct p3h2x4x_i3c_hub_dev *hub, u8 target_port_status, u8 data_length) @@ -199,11 +328,63 @@ static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter) return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA; } +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static int p3h2x4x_tp_i2c_reg_slave(struct i2c_client *slave) +{ + struct tp_bus *bus = i2c_get_adapdata(slave->adapter); + struct p3h2x4x_i3c_hub_dev *hub = bus->p3h2x4x_i3c_hub; + int ret; + + guard(mutex)(&hub->etx_mutex); + + if (bus->tp_smbus_client) + return -EBUSY; + + ret = regmap_set_bits(hub->regmap, + P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG, + bus->tp_mask); + if (ret) + return ret; + + bus->tp_smbus_client = slave; + hub->hub_config.tp_config[bus->tp_port].ibi_en = true; + + return 0; +} + +static int p3h2x4x_tp_i2c_unreg_slave(struct i2c_client *slave) +{ + struct tp_bus *bus = i2c_get_adapdata(slave->adapter); + struct p3h2x4x_i3c_hub_dev *hub = bus->p3h2x4x_i3c_hub; + int ret; + + guard(mutex)(&hub->etx_mutex); + + if (bus->tp_smbus_client != slave) + return -EINVAL; + + ret = regmap_clear_bits(hub->regmap, + P3H2X4X_TP_SMBUS_AGNT_IBI_CONFIG, + bus->tp_mask); + if (ret) + return ret; + + bus->tp_smbus_client = NULL; + hub->hub_config.tp_config[bus->tp_port].ibi_en = false; + + return 0; +} +#endif + /* * I2C algorithm Structure */ static struct i2c_algorithm p3h2x4x_tp_i2c_algorithm = { .master_xfer = p3h2x4x_tp_i2c_xfer, +#if IS_ENABLED(CONFIG_I2C_SLAVE) + .reg_slave = p3h2x4x_tp_i2c_reg_slave, + .unreg_slave = p3h2x4x_tp_i2c_unreg_slave, +#endif .functionality = p3h2x4x_tp_smbus_funcs, }; -- 2.25.1 From manikandan.m at microchip.com Mon May 25 02:24:00 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Mon, 25 May 2026 14:54:00 +0530 Subject: [PATCH v7 0/5] Add microchip sama7d65 SoC I3C support Message-ID: <20260525092405.1514213-1-manikandan.m@microchip.com> Add support for microchip sama7d65 SoC I3C master only IP which is based on mipi-i3c-hci from synopsys implementing version 1.0 specification. The platform specific changes are integrated in the mipi-i3c-hci driver using existing quirks. I3C in master mode supports up to 12.5MHz, SDR mode data transfer in mixed bus mode (I2C and I3C target devices on same i3c bus). Please refer to the individual patches for changelogs. Durai Manickam KR (2): clk: at91: sama7d65: add peripheral clock for I3C ARM: dts: microchip: add I3C controller Manikandan Muralidharan (3): dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk ARM: configs: at91: sama7: add sama7d65 i3c-hci .../devicetree/bindings/i3c/mipi-i3c-hci.yaml | 27 ++++++++++++++++--- arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++ arch/arm/configs/sama7_defconfig | 2 ++ drivers/clk/at91/sama7d65.c | 1 + drivers/i3c/master/mipi-i3c-hci/core.c | 10 +++++++ 5 files changed, 44 insertions(+), 4 deletions(-) -- 2.25.1 From manikandan.m at microchip.com Mon May 25 02:24:01 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Mon, 25 May 2026 14:54:01 +0530 Subject: [PATCH v7 1/5] dt-bindings: i3c: mipi-i3c-hci: add Microchip SAMA7D65 compatible In-Reply-To: <20260525092405.1514213-1-manikandan.m@microchip.com> References: <20260525092405.1514213-1-manikandan.m@microchip.com> Message-ID: <20260525092405.1514213-2-manikandan.m@microchip.com> Add the microchip,sama7d65-i3c-hci compatible string to the MIPI I3C HCI binding. The Microchip SAMA7D65 I3C controller is based on the MIPI HCI specification but requires two clocks, so add a conditional constraint when this compatible is present. Acked-by: Conor Dooley Reviewed-by: Frank Li Signed-off-by: Manikandan Muralidharan --- Changes in v5: - drop min/maxItems around clock entries - use if/then/else clause instead of separate allOf entry - cosmetic fixes for indentation and formatting Changes in v4: - Define and describe the clock property in the top-level properties section rather than inside the if/then conditional .../devicetree/bindings/i3c/mipi-i3c-hci.yaml | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml index 39bb1a1784c9..d488fb420945 100644 --- a/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml +++ b/Documentation/devicetree/bindings/i3c/mipi-i3c-hci.yaml @@ -9,9 +9,6 @@ title: MIPI I3C HCI maintainers: - Nicolas Pitre -allOf: - - $ref: /schemas/i3c/i3c.yaml# - description: | MIPI I3C Host Controller Interface @@ -28,9 +25,17 @@ description: | properties: compatible: - const: mipi-i3c-hci + enum: + - mipi-i3c-hci + - microchip,sama7d65-i3c-hci reg: maxItems: 1 + + clocks: + items: + - description: Peripheral bus clock + - description: System Generic clock + interrupts: maxItems: 1 @@ -39,6 +44,20 @@ required: - reg - interrupts +allOf: + - $ref: /schemas/i3c/i3c.yaml# + - if: + properties: + compatible: + contains: + const: microchip,sama7d65-i3c-hci + then: + required: + - clocks + else: + properties: + clocks: false + unevaluatedProperties: false examples: -- 2.25.1 From manikandan.m at microchip.com Mon May 25 02:24:02 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Mon, 25 May 2026 14:54:02 +0530 Subject: [PATCH v7 2/5] clk: at91: sama7d65: add peripheral clock for I3C In-Reply-To: <20260525092405.1514213-1-manikandan.m@microchip.com> References: <20260525092405.1514213-1-manikandan.m@microchip.com> Message-ID: <20260525092405.1514213-3-manikandan.m@microchip.com> From: Durai Manickam KR Add peripheral clock description for I3C. Signed-off-by: Durai Manickam KR Reviewed-by: Claudiu Beznea Signed-off-by: Manikandan Muralidharan --- Changes in v3: - Fixed indentation issues in the clock table entry drivers/clk/at91/sama7d65.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/clk/at91/sama7d65.c b/drivers/clk/at91/sama7d65.c index 7dee2b160ffb..ba8ff413fa2c 100644 --- a/drivers/clk/at91/sama7d65.c +++ b/drivers/clk/at91/sama7d65.c @@ -677,6 +677,7 @@ static struct { { .n = "uhphs_clk", .p = PCK_PARENT_HW_MCK5, .id = 101, }, { .n = "dsi_clk", .p = PCK_PARENT_HW_MCK3, .id = 103, }, { .n = "lvdsc_clk", .p = PCK_PARENT_HW_MCK3, .id = 104, }, + { .n = "i3cc_clk", .p = PCK_PARENT_HW_MCK8, .id = 105, }, }; /* -- 2.25.1 From manikandan.m at microchip.com Mon May 25 02:24:03 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Mon, 25 May 2026 14:54:03 +0530 Subject: [PATCH v7 3/5] i3c: mipi-i3c-hci: add microchip sama7d65 SoC compatible with the required quirk In-Reply-To: <20260525092405.1514213-1-manikandan.m@microchip.com> References: <20260525092405.1514213-1-manikandan.m@microchip.com> Message-ID: <20260525092405.1514213-4-manikandan.m@microchip.com> Add support for microchip sama7d65 SoC I3C HCI master only IP with additional clock support to enable bulk clock acquisition and apply the required quirks. Reviewed-by: Adrian Hunter Signed-off-by: Manikandan Muralidharan --- Changes in v7: - Use (void *)(ulong) cast instead of direct (void *) cast in of_device_id.data for pointer-size safety across architectures - Update commit message body to explicitly mention quirk application Changes in v6: - Reorder local variable definitions in i3c_hci_probe in descending order of line length Changes in v5: - Remove HCI_QUIRK_CLK_SUPPORT quirk and call devm_clk_bulk_get_all_enabled unconditionally, eliminating the need for a clock-specific quirk flag Changes in v4: - Remove the clock index variable MCHP_I3C_CLK_IDX as it is no longer needed after switching to bulk clock handling Changes in v3: - Make use of existing HCI_QUIRK_* code base instead of introducing separate MCHP_HCI_QUIRK_* flags - Introduce HCI_QUIRK_CLK_SUPPORT to handle peripheral and system generic clk in bulk Changes in v2: - Platform specific changes integrated in the existing mipi-i3c-hci driver by introducing separate MCHP_HCI_QUIRK_* quirks and vendor specific quirk files rather than a standalone driver drivers/i3c/master/mipi-i3c-hci/core.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index b781dbed2165..4cdf2abd4219 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -8,6 +8,7 @@ */ #include +#include #include #include #include @@ -969,6 +970,7 @@ static int i3c_hci_init(struct i3c_hci *hci) static int i3c_hci_probe(struct platform_device *pdev) { const struct mipi_i3c_hci_platform_data *pdata = pdev->dev.platform_data; + struct clk_bulk_data *clks; struct i3c_hci *hci; int irq, ret; @@ -1001,6 +1003,11 @@ static int i3c_hci_probe(struct platform_device *pdev) if (!hci->quirks && platform_get_device_id(pdev)) hci->quirks = platform_get_device_id(pdev)->driver_data; + ret = devm_clk_bulk_get_all_enabled(&pdev->dev, &clks); + if (ret < 0) + return dev_err_probe(&pdev->dev, ret, + "Failed to get clocks\n"); + ret = i3c_hci_init(hci); if (ret) return ret; @@ -1031,6 +1038,9 @@ static void i3c_hci_remove(struct platform_device *pdev) static const __maybe_unused struct of_device_id i3c_hci_of_match[] = { { .compatible = "mipi-i3c-hci", }, + { .compatible = "microchip,sama7d65-i3c-hci", + .data = (void *)(ulong)(HCI_QUIRK_PIO_MODE | HCI_QUIRK_OD_PP_TIMING | + HCI_QUIRK_RESP_BUF_THLD) }, {}, }; MODULE_DEVICE_TABLE(of, i3c_hci_of_match); -- 2.25.1 From manikandan.m at microchip.com Mon May 25 02:24:04 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Mon, 25 May 2026 14:54:04 +0530 Subject: [PATCH v7 4/5] ARM: dts: microchip: add I3C controller In-Reply-To: <20260525092405.1514213-1-manikandan.m@microchip.com> References: <20260525092405.1514213-1-manikandan.m@microchip.com> Message-ID: <20260525092405.1514213-5-manikandan.m@microchip.com> From: Durai Manickam KR Add I3C controller for sama7d65 SoC. Signed-off-by: Durai Manickam KR Signed-off-by: Manikandan Muralidharan --- Changes in v3: - Remove clock-names property as the driver acquires and enables clocks in bulk using devm_clk_bulk_get_all_enabled arch/arm/boot/dts/microchip/sama7d65.dtsi | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/arch/arm/boot/dts/microchip/sama7d65.dtsi b/arch/arm/boot/dts/microchip/sama7d65.dtsi index 67253bbc08df..ec200848c153 100644 --- a/arch/arm/boot/dts/microchip/sama7d65.dtsi +++ b/arch/arm/boot/dts/microchip/sama7d65.dtsi @@ -1055,5 +1055,13 @@ gic: interrupt-controller at e8c11000 { #address-cells = <0>; interrupt-controller; }; + + i3c: i3c at e9000000 { + compatible = "microchip,sama7d65-i3c-hci"; + reg = <0xe9000000 0x300>; + interrupts = ; + clocks = <&pmc PMC_TYPE_PERIPHERAL 105>, <&pmc PMC_TYPE_GCK 105>; + status = "disabled"; + }; }; }; -- 2.25.1 From manikandan.m at microchip.com Mon May 25 02:24:05 2026 From: manikandan.m at microchip.com (Manikandan Muralidharan) Date: Mon, 25 May 2026 14:54:05 +0530 Subject: [PATCH v7 5/5] ARM: configs: at91: sama7: add sama7d65 i3c-hci In-Reply-To: <20260525092405.1514213-1-manikandan.m@microchip.com> References: <20260525092405.1514213-1-manikandan.m@microchip.com> Message-ID: <20260525092405.1514213-6-manikandan.m@microchip.com> Enable the configs needed for I3C framework and microchip sama7d65 i3c-hci driver. Signed-off-by: Durai Manickam KR Reviewed-by: Claudiu Beznea Signed-off-by: Manikandan Muralidharan --- arch/arm/configs/sama7_defconfig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm/configs/sama7_defconfig b/arch/arm/configs/sama7_defconfig index e52f671ccec4..6470c7d3fe8a 100644 --- a/arch/arm/configs/sama7_defconfig +++ b/arch/arm/configs/sama7_defconfig @@ -117,6 +117,8 @@ CONFIG_HW_RANDOM=y CONFIG_I2C=y CONFIG_I2C_CHARDEV=y CONFIG_I2C_AT91=y +CONFIG_I3C=y +CONFIG_MIPI_I3C_HCI=y CONFIG_SPI=y CONFIG_SPI_ATMEL=y CONFIG_SPI_ATMEL_QUADSPI=y -- 2.25.1 From zain_zhou at realsil.com.cn Mon May 25 05:51:27 2026 From: zain_zhou at realsil.com.cn (zain_zhou at realsil.com.cn) Date: Mon, 25 May 2026 20:51:27 +0800 Subject: [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB Message-ID: <20260525125128.297-1-zain_zhou@realsil.com.cn> From: Yin Zhou Add DT binding schema for Realtek RTS490x series I3C HUB devices. The binding describes configuration properties for: - LDO enable/disable and voltage level per port group - Pull-up resistance per port group - IO driver strength per port - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up, IO mode, SMBus clock frequency and polling interval - Hub network always-I3C mode - Hardware identification via CSEL pin (id) and CP1 pins (id-cp1) Signed-off-by: Yin Zhou Changes in v2: - Rework binding per Krzysztof Kozlowski's review: add realtek, vendor prefix to all custom properties; use boolean for enable flags; use u32 with unit suffixes (-microvolt, -ohms) for voltage/resistance; change to unevaluatedProperties: false; fix title, maintainer name, description, $nodename pattern - Consolidate examples; add dt-bindings/i2c/i2c.h include --- .../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml new file mode 100644 index 000000000000..851a433abcd3 --- /dev/null +++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml @@ -0,0 +1,263 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/i3c/realtek,rts490x-i3c-hub.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Realtek RTS490x I3C HUB + +maintainers: + - Yin Zhou + +description: + The Realtek RTS490x is an I3C HUB device that provides voltage level + translation between I3C controller and target devices, bus capacitance + isolation, address conflict isolation, I3C port expansion (up to 8 + target ports), simultaneous dual-controller port support, and per-port + mode selection (I3C, SMBus, GPIO, or disabled). + +properties: + $nodename: + pattern: "^hub@[0-9a-f]+(,[0-9a-f]+)*$" + + compatible: + const: realtek,rts490x-i3c-hub + + reg: + maxItems: 1 + description: + Encodes the static I2C address, manufacturer ID, and part/instance ID + as defined by the I3C specification. + + assigned-address: + $ref: /schemas/types.yaml#/definitions/uint32 + minimum: 0x1 + maximum: 0xff + description: + Dynamic I3C address to assign to this device. + + dcr: + $ref: /schemas/types.yaml#/definitions/uint32 + description: + Device Characteristic Register value of the hub. + + "#address-cells": + const: 1 + + "#size-cells": + const: 0 + + realtek,cp0-ldo-enable: + type: boolean + description: + Enable the on-die LDO for Controller Port 0. + + realtek,cp1-ldo-enable: + type: boolean + description: + Enable the on-die LDO for Controller Port 1. + + realtek,tp0145-ldo-enable: + type: boolean + description: + Enable the on-die LDO for Target Ports 0/1/4/5. + + realtek,tp2367-ldo-enable: + type: boolean + description: + Enable the on-die LDO for Target Ports 2/3/6/7. + + realtek,cp0-ldo-microvolt: + enum: [1000000, 1100000, 1200000, 1800000] + description: + Output voltage of the Controller Port 0 on-die LDO, in microvolts. + + realtek,cp1-ldo-microvolt: + enum: [1000000, 1100000, 1200000, 1800000] + description: + Output voltage of the Controller Port 1 on-die LDO, in microvolts. + + realtek,tp0145-ldo-microvolt: + enum: [1000000, 1100000, 1200000, 1800000] + description: + Output voltage of the Target Ports 0/1/4/5 on-die LDO, in + microvolts. + + realtek,tp2367-ldo-microvolt: + enum: [1000000, 1100000, 1200000, 1800000] + description: + Output voltage of the Target Ports 2/3/6/7 on-die LDO, in + microvolts. + + realtek,tp0145-pullup-ohms: + enum: [0, 250, 500, 1000, 2000] + description: + Pull-up resistance for Target Ports 0/1/4/5, in ohms. 0 disables + the pull-up. + + realtek,tp2367-pullup-ohms: + enum: [0, 250, 500, 1000, 2000] + description: + Pull-up resistance for Target Ports 2/3/6/7, in ohms. 0 disables + the pull-up. + + realtek,cp0-io-strength-ohms: + enum: [20, 30, 40, 50] + description: + Output driver impedance for Controller Port 0, in ohms. + + realtek,cp1-io-strength-ohms: + enum: [20, 30, 40, 50] + description: + Output driver impedance for Controller Port 1, in ohms. + + realtek,tp0145-io-strength-ohms: + enum: [20, 30, 40, 50] + description: + Output driver impedance for Target Ports 0/1/4/5, in ohms. + + realtek,tp2367-io-strength-ohms: + enum: [20, 30, 40, 50] + description: + Output driver impedance for Target Ports 2/3/6/7, in ohms. + + realtek,id: + $ref: /schemas/types.yaml#/definitions/uint32 + enum: [0, 1, 3] + description: | + I3C HUB hardware ID based on CSEL pin state. Values: + 0 - CP0 is selected as primary Controller Port + 1 - Primary Controller Port selected by software + 3 - CP1 is selected as primary Controller Port + + realtek,id-cp1: + $ref: /schemas/types.yaml#/definitions/uint32 + enum: [0, 1, 2, 3] + description: + I3C HUB hardware ID based on CP1 SDA and SCL pin state probed + during power-on. + +patternProperties: + "^target-port@[0-9]+$": + type: object + description: + I3C HUB target port child node, named target-port@. + + properties: + compatible: + const: realtek,rts490x-i3c-hub-port + + reg: + maxItems: 1 + description: + Target port index (0-based). + + "#address-cells": + const: 1 + + "#size-cells": + const: 0 + + realtek,mode: + enum: [disabled, i3c, smbus, gpio] + description: + Operating mode of this target port. + + realtek,pullup-enable: + type: boolean + description: + When present, enables the pull-up for this target port. + + realtek,always-enable: + type: boolean + description: + When present, the target port is always enabled. Otherwise + the port is enabled on demand and disabled after use. + + realtek,polling-interval-ms: + minimum: 0 + description: + SMBus polling interval in milliseconds. If absent or 0, + polling is disabled and IBI is used instead. + + clock-frequency: + enum: [100000, 200000, 400000, 1000000] + description: + SMBus clock frequency in Hz. Applies only when mode is smbus. + Defaults to 400000 if absent. + + additionalProperties: true + +required: + - compatible + - reg + +unevaluatedProperties: false + +examples: + - | + #include + i3c-master at d040000 { + reg = <0xd040000 0x1000>; + #address-cells = <3>; + #size-cells = <0>; + + hub at 70,4ba00000000 { + compatible = "realtek,rts490x-i3c-hub"; + reg = <0x70 0x4ba 0x00000000>; + assigned-address = <0x70>; + dcr = <0xc2>; + #address-cells = <1>; + #size-cells = <0>; + + realtek,cp0-ldo-enable; + realtek,cp1-ldo-enable; + realtek,cp0-ldo-microvolt = <1000000>; + realtek,cp1-ldo-microvolt = <1100000>; + realtek,tp0145-ldo-enable; + realtek,tp0145-ldo-microvolt = <1200000>; + realtek,tp2367-ldo-microvolt = <1800000>; + realtek,tp0145-pullup-ohms = <2000>; + realtek,tp2367-pullup-ohms = <500>; + realtek,tp0145-io-strength-ohms = <50>; + realtek,tp2367-io-strength-ohms = <30>; + realtek,cp0-io-strength-ohms = <20>; + realtek,cp1-io-strength-ohms = <40>; + + target-port at 0 { + compatible = "realtek,rts490x-i3c-hub-port"; + reg = <0>; + realtek,mode = "i3c"; + realtek,pullup-enable; + realtek,always-enable; + }; + + target-port at 1 { + compatible = "realtek,rts490x-i3c-hub-port"; + reg = <1>; + #address-cells = <1>; + #size-cells = <0>; + realtek,mode = "smbus"; + realtek,pullup-enable; + clock-frequency = <1000000>; + realtek,polling-interval-ms = <10>; + + i2c at 10 { + compatible = "i2c-slave-mqueue"; + reg = <(0x10 | I2C_OWN_SLAVE_ADDRESS)>; + }; + }; + + target-port at 2 { + compatible = "realtek,rts490x-i3c-hub-port"; + reg = <2>; + realtek,mode = "gpio"; + }; + + target-port at 3 { + compatible = "realtek,rts490x-i3c-hub-port"; + reg = <3>; + realtek,mode = "disabled"; + }; + }; + }; -- 2.34.1 From zain_zhou at realsil.com.cn Mon May 25 05:51:28 2026 From: zain_zhou at realsil.com.cn (zain_zhou at realsil.com.cn) Date: Mon, 25 May 2026 20:51:28 +0800 Subject: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver In-Reply-To: <20260525125128.297-1-zain_zhou@realsil.com.cn> References: <20260525125128.297-1-zain_zhou@realsil.com.cn> Message-ID: <20260525125128.297-2-zain_zhou@realsil.com.cn> From: Yin Zhou Add driver for Realtek RTS490x series I3C HUB devices. The driver supports: - Device Tree based configuration of LDO, pull-up, IO strength and per-port mode (I3C/SMBus/GPIO/disabled) - Logical I3C bus registration per target port - SMBus agent functionality with IBI and polling modes - GPIO chip with IRQ support - DebugFS interface for register access and DT config inspection - IBI (In-Band Interrupt) handling The driver is placed in staging as it has known issues to be resolved before mainlining; see drivers/staging/rts490x/TODO for details. Signed-off-by: Yin Zhou Changes in v2: - Update driver to match v2 DT binding: parse physical values directly via of_property_read_bool/u32; drop string enum lookup tables; update property names with realtek, prefix and unit suffixes - Fix maintainer name; move MAINTAINERS entry to driver patch - Code style: rename TPn_* macros to TPN_*; rename SMBus frequency constants to UPPER_CASE; add mutex field comments --- MAINTAINERS | 6 + drivers/staging/Kconfig | 2 + drivers/staging/Makefile | 1 + drivers/staging/rts490x/Kconfig | 16 + drivers/staging/rts490x/Makefile | 2 + drivers/staging/rts490x/TODO | 35 + drivers/staging/rts490x/rts490xa-i3c-hub.c | 3039 ++++++++++++++++++++ 7 files changed, 3101 insertions(+) create mode 100644 drivers/staging/rts490x/Kconfig create mode 100644 drivers/staging/rts490x/Makefile create mode 100644 drivers/staging/rts490x/TODO create mode 100644 drivers/staging/rts490x/rts490xa-i3c-hub.c diff --git a/MAINTAINERS b/MAINTAINERS index 2fb1c75afd16..6d0f8cde935d 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -12214,6 +12214,12 @@ S: Supported F: Documentation/devicetree/bindings/i3c/renesas,i3c.yaml F: drivers/i3c/master/renesas-i3c.c +I3C HUB DRIVER FOR REALTEK RTS490X +M: Yin Zhou +S: Maintained +F: Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml +F: drivers/staging/rts490x/ + I3C DRIVER FOR SYNOPSYS DESIGNWARE S: Orphan F: Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig index 2f92cd698bef..f14869cf5af5 100644 --- a/drivers/staging/Kconfig +++ b/drivers/staging/Kconfig @@ -48,4 +48,6 @@ source "drivers/staging/axis-fifo/Kconfig" source "drivers/staging/vme_user/Kconfig" +source "drivers/staging/rts490x/Kconfig" + endif # STAGING diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile index f5b8876aa536..59044d547bf7 100644 --- a/drivers/staging/Makefile +++ b/drivers/staging/Makefile @@ -13,3 +13,4 @@ obj-$(CONFIG_MOST) += most/ obj-$(CONFIG_GREYBUS) += greybus/ obj-$(CONFIG_BCM2835_VCHIQ) += vc04_services/ obj-$(CONFIG_XIL_AXIS_FIFO) += axis-fifo/ +obj-$(CONFIG_RTS490X_I3C_HUB) += rts490x/ diff --git a/drivers/staging/rts490x/Kconfig b/drivers/staging/rts490x/Kconfig new file mode 100644 index 000000000000..282865d1fed9 --- /dev/null +++ b/drivers/staging/rts490x/Kconfig @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-only +config RTS490X_I3C_HUB + tristate "Realtek RTS490x I3C HUB driver" + depends on I3C + depends on REGMAP_I3C + select GPIOLIB + help + Support for Realtek RTS490x series I3C HUB devices (RTS4900, + RTS4901, RTS4902, RTS4903, RTS4904, RTS4906). + + The I3C HUB provides port expansion, voltage level translation, + bus capacitance isolation, address conflict isolation, SMBus + agent functionality and GPIO expansion. + + This driver can also be built as a module. If so, the module + will be called rts490xa-i3c-hub. diff --git a/drivers/staging/rts490x/Makefile b/drivers/staging/rts490x/Makefile new file mode 100644 index 000000000000..4b1d7640fb82 --- /dev/null +++ b/drivers/staging/rts490x/Makefile @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: GPL-2.0-only +obj-$(CONFIG_RTS490X_I3C_HUB) += rts490xa-i3c-hub.o diff --git a/drivers/staging/rts490x/TODO b/drivers/staging/rts490x/TODO new file mode 100644 index 000000000000..0f46620af4c6 --- /dev/null +++ b/drivers/staging/rts490x/TODO @@ -0,0 +1,35 @@ +TODO list for rts490xa-i3c-hub staging driver +============================================== + +Completed in v2 +--------------- +- [x] Add proper DT binding schema validation (dt-schema) + Addressed in v2 dt-bindings patch: all properties carry vendor + prefix, use standard types (boolean/u32 with unit suffixes), + unevaluatedProperties: false, validated via dt-schema. + +Remaining before moving out of staging +--------------------------------------- +- Clean up open-coded OF property parsing; use device_property_* APIs + instead of of_property_read_* where possible +- Remove use of full_name / sscanf for node name parsing; use + of_node_name_eq() and fwnode helpers instead +- Replace global mutex (i3c_hub_regmap_mutex) with per-device locking +- Add kernel-doc comments for all exported/public functions +- Resolve TODO comment in i3c_hub_hw_configure_tp() regarding MUX + connection verification +- Remove TBD comment in i3c_hub_probe() regarding DEV_CMD security lock +- Review and fix potential locking issues in i3c_hub_delayed_work() + when registering logical buses +- Fix error handling in i3c_hub_delayed_work(): early return on failure + does not unregister already-registered logical buses, causing resource + leak; needs proper cleanup on error path + +Rebase on upstream i3c hub framework (pending) +----------------------------------------------- +A generic i3c hub framework is being introduced upstream by the NXP +P3H2x4x patch series (v10, under review): + https://lore.kernel.org/linux-i3c/20260525064209.2263045-1-lakshay.piplani at nxp.com/ + +Once that framework is merged, rebase this driver on it and move +out of staging. diff --git a/drivers/staging/rts490x/rts490xa-i3c-hub.c b/drivers/staging/rts490x/rts490xa-i3c-hub.c new file mode 100644 index 000000000000..fdfff5c6dff5 --- /dev/null +++ b/drivers/staging/rts490x/rts490xa-i3c-hub.c @@ -0,0 +1,3039 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (C) 2021 - 2023 Intel Corporation. */ +/* Copyright (c) 2025 Realtek Semiconductor Corp. */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define I3C_HUB_TP_MAX_COUNT 0x08 + +#define I3C_DCR_HUB 0xC2 + +#define GPIO_BANK_SZ 0x02 +#define GPIO_MAX_BANK I3C_HUB_TP_MAX_COUNT + +/* I3C HUB REGISTERS */ + +/* + * In this driver Controller - Target convention is used. All the abbreviations are + * based on this convention. For instance: CP - Controller Port, TP - Target Port. + */ + +/* Device Information Registers */ +#define I3C_HUB_DEV_INFO_0 0x00 +#define I3C_HUB_DEV_INFO_1 0x01 +#define I3C_HUB_PID_5 0x02 +#define I3C_HUB_PID_4 0x03 +#define I3C_HUB_PID_3 0x04 +#define I3C_HUB_PID_2 0x05 +#define I3C_HUB_PID_1 0x06 +#define I3C_HUB_PID_0 0x07 +#define I3C_HUB_BCR 0x08 +#define I3C_HUB_DCR 0x09 +#define I3C_HUB_DEV_CAPAB 0x0A + +#define I3C_HUB_DEV_REV 0x0B +#define I3C_HUB_DEV_REV_LDO_MASK GENMASK(7, 6) +#define I3C_HUB_DEV_REV_LDO_GET(x) FIELD_GET(I3C_HUB_DEV_REV_LDO_MASK, (x)) + +/* Device Configuration Registers */ +#define I3C_HUB_PROTECTION_CODE 0x10 +#define REGISTERS_LOCK_CODE 0x00 +#define REGISTERS_UNLOCK_CODE 0x69 +#define CP1_REGISTERS_UNLOCK_CODE 0x6A + +#define I3C_HUB_CP_CONF 0x11 +#define I3C_HUB_TP_ENABLE 0x12 +#define TPN_ENABLE(n) BIT(n) + +#define I3C_HUB_DEV_CONF 0x13 +#define I3C_HUB_IO_STRENGTH 0x14 +#define TP0145_IO_STRENGTH_MASK GENMASK(1, 0) +#define TP0145_IO_STRENGTH(x) (((x) << 0) & TP0145_IO_STRENGTH_MASK) +#define TP2367_IO_STRENGTH_MASK GENMASK(3, 2) +#define TP2367_IO_STRENGTH(x) (((x) << 2) & TP2367_IO_STRENGTH_MASK) +#define CP0_IO_STRENGTH_MASK GENMASK(5, 4) +#define CP0_IO_STRENGTH(x) (((x) << 4) & CP0_IO_STRENGTH_MASK) +#define CP1_IO_STRENGTH_MASK GENMASK(7, 6) +#define CP1_IO_STRENGTH(x) (((x) << 6) & CP1_IO_STRENGTH_MASK) +#define IO_STRENGTH_20_OHM 0x00 +#define IO_STRENGTH_30_OHM 0x01 +#define IO_STRENGTH_40_OHM 0x02 +#define IO_STRENGTH_50_OHM 0x03 + +#define I3C_HUB_NET_OPER_MODE_CONF 0x15 +#define I3C_HUB_NET_ALWAYS_I3C_EN BIT(5) + +#define I3C_HUB_LDO_CONF 0x16 +#define CP0_LDO_VOLTAGE_MASK GENMASK(1, 0) +#define CP0_LDO_VOLTAGE(x) (((x) << 0) & CP0_LDO_VOLTAGE_MASK) +#define CP1_LDO_VOLTAGE_MASK GENMASK(3, 2) +#define CP1_LDO_VOLTAGE(x) (((x) << 2) & CP1_LDO_VOLTAGE_MASK) +#define TP0145_LDO_VOLTAGE_MASK GENMASK(5, 4) +#define TP0145_LDO_VOLTAGE(x) (((x) << 4) & TP0145_LDO_VOLTAGE_MASK) +#define TP2367_LDO_VOLTAGE_MASK GENMASK(7, 6) +#define TP2367_LDO_VOLTAGE(x) (((x) << 6) & TP2367_LDO_VOLTAGE_MASK) +#define LDO_VOLTAGE_1_0V 0x00 +#define LDO_VOLTAGE_1_1V 0x01 +#define LDO_VOLTAGE_1_2V 0x02 +#define LDO_VOLTAGE_1_8V 0x03 + +#define I3C_HUB_TP_IO_MODE_CONF 0x17 +#define TPN_IO_MODE_CON(n) BIT(n) +#define I3C_HUB_TP_SMBUS_AGNT_EN 0x18 +#define TPN_SMBUS_MODE_EN(n) BIT(n) + +#define I3C_HUB_LDO_AND_PULLUP_CONF 0x19 +#define LDO_ENABLE_DISABLE_MASK GENMASK(3, 0) +#define CP0_LDO_EN BIT(0) +#define CP1_LDO_EN BIT(1) +/* + * I3C HUB does not provide a way to control LDO or pull-up for individual ports. It is possible + * for group of ports TP0/TP1/TP4/TP5 and TP2/TP3/TP6/TP7. + */ +#define TP0145_LDO_EN BIT(2) +#define TP2367_LDO_EN BIT(3) +#define TP0145_PULLUP_CONF_MASK GENMASK(7, 6) +#define TP0145_PULLUP_CONF(x) (((x) << 6) & TP0145_PULLUP_CONF_MASK) +#define TP2367_PULLUP_CONF_MASK GENMASK(5, 4) +#define TP2367_PULLUP_CONF(x) (((x) << 4) & TP2367_PULLUP_CONF_MASK) +#define PULLUP_250R 0x00 +#define PULLUP_500R 0x01 +#define PULLUP_1K 0x02 +#define PULLUP_2K 0x03 + +#define I3C_HUB_CP_IBI_CONF 0x1A +#define I3C_HUB_TP_IBI_CONF 0x1B +#define I3C_HUB_IBI_MDB_CUSTOM 0x1C +#define I3C_HUB_JEDEC_CONTEXT_ID 0x1D +#define I3C_HUB_TP_GPIO_MODE_EN 0x1E +#define TPN_GPIO_MODE_EN(n) BIT(n) + +/* Device Status and IBI Registers */ +#define I3C_HUB_DEV_AND_IBI_STS 0x20 +#define PEC_ERROR_FLAG BIT(0) +#define PARITY_ERROR_FLAG BIT(1) +#define CONTROLLER_MSG_PENDING_FLAG BIT(2) +#define TP_IO_FLAG_STATUS BIT(3) +#define SMBUS_AGENT_EVENT_FLAG_STATUS BIT(4) + +#define I3C_HUB_TP_SMBUS_AGNT_IBI_STS 0x21 + +/* Controller Port Control/Status Registers */ +#define I3C_HUB_CP_MUX_SET 0x38 +#define CONTROLLER_PORT_MUX_REQ BIT(0) +#define I3C_HUB_CP_MUX_STS 0x39 +#define CONTROLLER_PORT_MUX_CONNECTION_STATUS BIT(0) + +/* Target Dynamic Address Assignment Flag Registers */ +#define I3C_HUB_TARGET_DA_FLAG_BYTE_BASE 0x40 +#define I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT 16 + +/* Target Ports Control Registers */ +#define I3C_HUB_TP_SMBUS_AGNT_TRANS_START 0x50 +#define I3C_HUB_TP_NET_CON_CONF 0x51 +#define TPN_NET_CON(n) BIT(n) + +#define I3C_HUB_TP_PULLUP_EN 0x53 +#define TPN_PULLUP_EN(n) BIT(n) + +#define I3C_HUB_TP_SCL_OUT_EN 0x54 +#define I3C_HUB_TP_SDA_OUT_EN 0x55 +#define I3C_HUB_TP_SCL_OUT_LEVEL 0x56 +#define I3C_HUB_TP_SDA_OUT_LEVEL 0x57 + +#define I3C_HUB_TP_IN_DETECT_MODE_CONF 0x58 +#define SCL0145_IO_IN_DET_CFG_MASK GENMASK(1, 0) +#define SCL0145_IO_IN_DET_CFG(x) (((x) << 0) & SCL0145_IO_IN_DET_CFG_MASK) +#define SDA0145_IO_IN_DET_CFG_MASK GENMASK(3, 2) +#define SDA0145_IO_IN_DET_CFG(x) (((x) << 2) & SDA0145_IO_IN_DET_CFG_MASK) +#define SCL2367_IO_IN_DET_CFG_MASK GENMASK(5, 4) +#define SCL2367_IO_IN_DET_CFG(x) (((x) << 4) & SCL2367_IO_IN_DET_CFG_MASK) +#define SDA2367_IO_IN_DET_CFG_MASK GENMASK(7, 6) +#define SDA2367_IO_IN_DET_CFG(x) (((x) << 6) & SDA2367_IO_IN_DET_CFG_MASK) + +#define I3C_HUB_TP_SCL_IN_DETECT_IBI_EN 0x59 +#define I3C_HUB_TP_SDA_IN_DETECT_IBI_EN 0x5A + +/* Target Ports Status Registers */ +#define I3C_HUB_TP_SCL_IN_LEVEL_STS 0x60 +#define I3C_HUB_TP_SDA_IN_LEVEL_STS 0x61 +#define I3C_HUB_TP_SCL_IN_DETECT_FLG 0x62 +#define I3C_HUB_TP_SDA_IN_DETECT_FLG 0x63 + +/* SMBus Agent Configuration and Status Registers */ +#define I3C_HUB_TP0_SMBUS_AGNT_STS 0x64 +#define I3C_HUB_TP1_SMBUS_AGNT_STS 0x65 +#define I3C_HUB_TP2_SMBUS_AGNT_STS 0x66 +#define I3C_HUB_TP3_SMBUS_AGNT_STS 0x67 +#define I3C_HUB_TP4_SMBUS_AGNT_STS 0x68 +#define I3C_HUB_TP5_SMBUS_AGNT_STS 0x69 +#define I3C_HUB_TP6_SMBUS_AGNT_STS 0x6A +#define I3C_HUB_TP7_SMBUS_AGNT_STS 0x6B + +#define I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF 0x6C +#define TARGET_AGENT_BUF_FULL_SDA_LOW_EN BIT(5) + +/* Transaction status checking mask */ +#define I3C_HUB_CONTROLLER_AGENT_STATUS_MASK (0xF0 | BIT(0)) +#define I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG BIT(0) +/* SMBus Controller Agent Return Codes */ +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT 4 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS 0x0 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK 0x1 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY 0x2 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY 0x3 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED 0x4 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR 0x5 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT 0x6 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST 0x7 +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT 0x8 + +#define I3C_HUB_TARGET_BUF_STATUS_MASK GENMASK(3, 1) +#define I3C_HUB_TARGET_BUF_0_RECEIVE BIT(1) +#define I3C_HUB_TARGET_BUF_1_RECEIVE BIT(2) +#define I3C_HUB_TARGET_BUF_OVRFL BIT(3) + +/* Special Function Registers */ +#define I3C_HUB_LDO_AND_CPSEL_STS 0x79 +#define CP_SDA1_LEVEL BIT(7) +#define CP_SCL1_LEVEL BIT(6) +#define CP_SEL_PIN_INPUT_CODE_MASK GENMASK(5, 4) +#define CP_SEL_PIN_INPUT_CODE_GET(x) (((x) & CP_SEL_PIN_INPUT_CODE_MASK) >> 4) +#define CP_SDA1_SCL1_PINS_CODE_MASK GENMASK(7, 6) +#define CP_SDA1_SCL1_PINS_CODE_GET(x) (((x) & CP_SDA1_SCL1_PINS_CODE_MASK) >> 6) +#define VCCIO1_PWR_GOOD BIT(3) +#define VCCIO0_PWR_GOOD BIT(2) +#define CP1_VCCIO_PWR_GOOD BIT(1) +#define CP0_VCCIO_PWR_GOOD BIT(0) + +#define I3C_HUB_BUS_RESET_SCL_TIMEOUT 0x7A +#define I3C_HUB_ONCHIP_TD_PROTO_ERR_FLG 0x7B +#define I3C_HUB_DEV_CMD 0x7C +#define I3C_HUB_ONCHIP_TD_STS 0x7D +#define I3C_HUB_ONCHIP_TD_ADDR_CONF 0x7E +#define I3C_HUB_PAGE_PTR 0x7F + +/* Paged Transaction Registers */ +#define I3C_HUB_CONTROLLER_BUFFER_PAGE 0x10 +#define I3C_HUB_CONTROLLER_AGENT_BUFF 0x80 +#define I3C_HUB_CONTROLLER_AGENT_BUFF_DATA 0x84 +#define I3C_HUB_TARGET_BUFF_LENGTH 0x80 +#define I3C_HUB_TARGET_BUFF_ADDRESS 0x81 +#define I3C_HUB_TARGET_BUFF_DATA 0x82 + +/* TP DT setting */ +#define I3C_HUB_DT_TP_MODE_DISABLED 0x00 +#define I3C_HUB_DT_TP_MODE_I3C 0x01 +#define I3C_HUB_DT_TP_MODE_SMBUS 0x02 +#define I3C_HUB_DT_TP_MODE_GPIO 0x03 +#define I3C_HUB_DT_TP_MODE_NOT_DEFINED 0xFF + +/* TP IO mode */ +#define I3C_HUB_DT_TP_IO_MODE_OD_PP 0x00 +#define I3C_HUB_DT_TP_IO_MODE_OD 0x01 +#define I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED 0xFF + +/* SMBus transaction types fields */ +#define I3C_HUB_SMBUS_100_KHZ 0x00 +#define I3C_HUB_SMBUS_200_KHZ BIT(1) +#define I3C_HUB_SMBUS_400_KHZ BIT(2) +#define I3C_HUB_SMBUS_1000_KHZ (BIT(1) | BIT(2)) + +/* SMBus xfer type for i3c_hub_smbus_msg xfer_type parameter */ +#define I3C_HUB_SMBUS_XFER_WRITE 0 +#define I3C_HUB_SMBUS_XFER_READ 1 +#define I3C_HUB_SMBUS_XFER_WR_RD 2 + +/* Hub buffer size */ +#define I3C_HUB_CONTROLLER_BUFFER_SIZE 88 +#define I3C_HUB_TARGET_BUFFER_SIZE 80 +#define I3C_HUB_SMBUS_DESCRIPTOR_SIZE 4 +#define I3C_HUB_SMBUS_PAYLOAD_SIZE \ + (I3C_HUB_CONTROLLER_BUFFER_SIZE - I3C_HUB_SMBUS_DESCRIPTOR_SIZE) +#define I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE (I3C_HUB_TARGET_BUFFER_SIZE - 2) + +/* Hub SMBus status register read interval (microseconds, ceil) */ +#define I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL(len, clk_khz) \ + DIV_ROUND_UP(1000U * 9U * (u32)(len), (u32)(clk_khz)) + +/* ID Extraction */ +#define I3C_HUB_ID_CP_SDA_SCL 0x00 +#define I3C_HUB_ID_CP_SEL 0x01 + +/* IBI */ +#define IBI_MAX_PAYLOAD_LEN 2 +#define IBI_SLOT_NUMS 6 + +#define I3C_HUB_IO_CTRL_PAGE 0x81 +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK 0xDB +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN BIT(6) +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_MASK GENMASK(5, 0) +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL 0x18 + +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK 0xDC +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN BIT(4) +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK GENMASK(3, 0) +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL(x) \ + ((x) & I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK) + +#define I3C_HUB_EFUSE_PAGE 0x7B +#define I3C_HUB_EFUSE_OFFSET_A0 0xA0 +#define I3C_HUB_FAST_RSON_EN BIT(5) +#define I3C_HUB_EFUSE_OFFSET_A3 0xA3 +#define I3C_HUB_FAST_DRV_LOOP_DIS BIT(5) + +#define I3C_HUB_EFUSE_OFFSET_9D 0x9D +#define I3C_HUB_TP_OD_VOL_LEVEL BIT(0) +#define I3C_HUB_TP_OD_VREF BIT(1) + +#define I3C_HUB_EFUSE_OFFSET_9E 0x9E +#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK GENMASK(5, 4) +#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL(x) \ + (((x) << 4) & I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK) +#define I3C_HUB_IBI_ACK_RD_CYCLE_MASK GENMASK(3, 0) +#define I3C_HUB_IBI_ACK_RD_CYCLE_VAL (5) + +struct i3c_hub_dev_info { + const char *model; + u16 part_id; + u8 n_ports; +}; + +static const struct i3c_hub_dev_info i3c_hub_dev_info_unknown = { + .model = "Unknown", + .part_id = 0, + .n_ports = 8, +}; + +static const struct i3c_hub_dev_info i3c_hub_dev_info_table[] = { + { "RTS4900", 0x4000, 4 }, { "RTS4901", 0x4100, 4 }, + { "RTS4902", 0x8000, 8 }, { "RTS4903", 0x8100, 8 }, + { "RTS4904", 0x4001, 4 }, { "RTS4906", 0x8001, 8 } +}; + +struct tp_setting { + u8 mode; + bool pullup_en; + u8 io_mode; + bool always_enable; + u32 poll_interval_ms; + u32 clock_frequency; +}; + +struct dt_settings { + bool cp0_ldo_en; + bool cp1_ldo_en; + bool tp0145_ldo_en; + bool tp2367_ldo_en; + u32 cp0_ldo_volt; + u32 cp1_ldo_volt; + u32 tp0145_ldo_volt; + u32 tp2367_ldo_volt; + u32 tp0145_pullup; + u32 tp2367_pullup; + u32 cp0_io_strength; + u32 cp1_io_strength; + u32 tp0145_io_strength; + u32 tp2367_io_strength; + struct tp_setting tp[I3C_HUB_TP_MAX_COUNT]; + bool hub_net_always_i3c; + u8 tp_scl_h_ack_cycles; + bool handshake_optimize; + u8 fast_drv_h_add_cycles; + bool fast_rson_en; + bool tp_od_vol_optimize; + bool tp_od_vref_optimize; +}; + +struct smbus_backend { + struct i2c_client *client; + struct list_head list; +}; + +struct i2c_adapter_group { + struct i2c_adapter i2c; + u8 tp_mask; + u8 tp_port; + u8 used; + struct device_node *of_node; + struct i3c_hub *priv; + struct mutex mutex; /* protects SMBus adapter state and transfers */ + + struct delayed_work delayed_work_polling; + struct list_head backend_entry; + u8 last_processed_buf; + + u8 status; + struct completion completion; +}; + +struct logical_bus { + bool available; /* Logical bus configuration is available in DT. */ + bool registered; /* Logical bus was registered in the framework. */ + u8 tp_id; + u8 tp_map; + struct i3c_master_controller controller; + struct device_node *of_node; + struct i3c_hub *priv; +}; + +struct hub_gpio { + struct gpio_chip chip; + int tp[GPIO_MAX_BANK]; + s8 port_to_index[I3C_HUB_TP_MAX_COUNT]; + int nums; + struct irq_chip irq_chip; + struct mutex irq_mutex; /* protects GPIO IRQ enable/disable operations */ +}; + +struct i3c_hub { + struct i3c_device *i3cdev; + struct i3c_master_controller *driving_master; + struct regmap *regmap; + const struct i3c_hub_dev_info *dev_info; + struct dt_settings settings; + struct delayed_work delayed_work; + int hub_pin_sel_id; + int hub_pin_cp1_id; + int hub_dt_sel_id; + int hub_dt_cp1_id; + + struct logical_bus logical_bus[I3C_HUB_TP_MAX_COUNT]; + struct i2c_adapter_group smbus_port_adapter[I3C_HUB_TP_MAX_COUNT]; + u8 smbus_ibi_en_mask; + struct mutex page_mutex; /* protects regmap page register access */ + + /* Offset for reading HUB's register. */ + u8 reg_addr; + struct dentry *debug_dir; + struct hub_gpio gpio; +}; + +/* Global mutex for serializing regmap access across all i3c hubs. */ +static DEFINE_MUTEX(i3c_hub_regmap_mutex); + +static u8 i3c_hub_ldo_dt_to_reg(u32 microvolt) +{ + switch (microvolt) { + case 1100000: + return LDO_VOLTAGE_1_1V; + case 1200000: + return LDO_VOLTAGE_1_2V; + case 1800000: + return LDO_VOLTAGE_1_8V; + default: + return LDO_VOLTAGE_1_0V; + } +} + +static u8 i3c_hub_pullup_dt_to_reg(u32 ohms) +{ + switch (ohms) { + case 250: + return PULLUP_250R; + case 500: + return PULLUP_500R; + case 1000: + return PULLUP_1K; + default: + return PULLUP_2K; + } +} + +static u8 i3c_hub_io_strength_dt_to_reg(u32 ohms) +{ + switch (ohms) { + case 50: + return IO_STRENGTH_50_OHM; + case 40: + return IO_STRENGTH_40_OHM; + case 30: + return IO_STRENGTH_30_OHM; + default: + return IO_STRENGTH_20_OHM; + } +} + +static bool i3c_hub_smbus_validate_clock_frequency(u32 hz) +{ + switch (hz) { + case 100000: + case 200000: + case 400000: + case 1000000: + return true; + default: + return false; + } +} + +static inline u8 i3c_hub_smbus_rate_bits_from_hz(u32 hz) +{ + switch (hz) { + case 100000: + return I3C_HUB_SMBUS_100_KHZ; + case 200000: + return I3C_HUB_SMBUS_200_KHZ; + case 1000000: + return I3C_HUB_SMBUS_1000_KHZ; + default: + return I3C_HUB_SMBUS_400_KHZ; + } +} + +static void i3c_hub_tp_of_get_setting(struct device *dev, + const struct device_node *node, + struct tp_setting tp_setting[]) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + const char *mode_str, *io_mode_str; + struct device_node *tp_node; + u32 id, val; + + for_each_available_child_of_node(node, tp_node) { + if (!tp_node->name || of_node_cmp(tp_node->name, "target-port")) + continue; + + if (!tp_node->full_name || + (sscanf(tp_node->full_name, "target-port@%u", &id) != 1)) { + dev_warn(dev, + "Invalid target port node found in DT: %s\n", + tp_node->full_name); + continue; + } + + if (id >= priv->dev_info->n_ports) { + dev_warn(dev, + "Invalid target port index found in DT: %i\n", + id); + continue; + } + + priv->smbus_port_adapter[id].of_node = tp_node; + + if (!of_property_read_string(tp_node, "realtek,mode", + &mode_str)) { + if (!strcmp(mode_str, "i3c")) + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_I3C; + else if (!strcmp(mode_str, "smbus")) + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_SMBUS; + else if (!strcmp(mode_str, "gpio")) + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_GPIO; + else if (!strcmp(mode_str, "disabled")) + tp_setting[id].mode = + I3C_HUB_DT_TP_MODE_DISABLED; + } + + if (!of_property_read_string(tp_node, "realtek,io-mode", + &io_mode_str)) { + if (!strcmp(io_mode_str, "od")) + tp_setting[id].io_mode = + I3C_HUB_DT_TP_IO_MODE_OD; + else if (!strcmp(io_mode_str, "od-pp")) + tp_setting[id].io_mode = + I3C_HUB_DT_TP_IO_MODE_OD_PP; + } + + tp_setting[id].pullup_en = + of_property_read_bool(tp_node, "realtek,pullup-enable"); + tp_setting[id].always_enable = + of_property_read_bool(tp_node, "realtek,always-enable"); + + if (!of_property_read_u32(tp_node, + "realtek,polling-interval-ms", &val)) + tp_setting[id].poll_interval_ms = val; + + if (!of_property_read_u32(tp_node, "clock-frequency", &val)) { + if (i3c_hub_smbus_validate_clock_frequency(val)) + tp_setting[id].clock_frequency = val; + else + dev_warn(dev, + "Unsupported TP%d smbus clock-frequency: %u Hz, using default %u Hz\n", + id, val, + tp_setting[id].clock_frequency); + } + } +} + +static void i3c_hub_of_get_conf_static(struct device *dev, + const struct device_node *node) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + u8 u8val; + u32 val; + + priv->settings.cp0_ldo_en = + of_property_read_bool(node, "realtek,cp0-ldo-enable"); + priv->settings.cp1_ldo_en = + of_property_read_bool(node, "realtek,cp1-ldo-enable"); + priv->settings.tp0145_ldo_en = + of_property_read_bool(node, "realtek,tp0145-ldo-enable"); + priv->settings.tp2367_ldo_en = + of_property_read_bool(node, "realtek,tp2367-ldo-enable"); + + if (!of_property_read_u32(node, "realtek,cp0-ldo-microvolt", &val)) + priv->settings.cp0_ldo_volt = val; + if (!of_property_read_u32(node, "realtek,cp1-ldo-microvolt", &val)) + priv->settings.cp1_ldo_volt = val; + if (!of_property_read_u32(node, "realtek,tp0145-ldo-microvolt", &val)) + priv->settings.tp0145_ldo_volt = val; + if (!of_property_read_u32(node, "realtek,tp2367-ldo-microvolt", &val)) + priv->settings.tp2367_ldo_volt = val; + + if (!of_property_read_u32(node, "realtek,tp0145-pullup-ohms", &val)) + priv->settings.tp0145_pullup = val; + if (!of_property_read_u32(node, "realtek,tp2367-pullup-ohms", &val)) + priv->settings.tp2367_pullup = val; + + if (!of_property_read_u32(node, + "realtek,cp0-output-impedance-ohms", &val)) + priv->settings.cp0_io_strength = val; + if (!of_property_read_u32(node, + "realtek,cp1-output-impedance-ohms", &val)) + priv->settings.cp1_io_strength = val; + if (!of_property_read_u32(node, + "realtek,tp0145-output-impedance-ohms", &val)) + priv->settings.tp0145_io_strength = val; + if (!of_property_read_u32(node, + "realtek,tp2367-output-impedance-ohms", &val)) + priv->settings.tp2367_io_strength = val; + + priv->settings.hub_net_always_i3c = + of_property_read_bool(node, "realtek,hub-net-always-i3c"); + + if (!of_property_read_u8(node, "realtek,tp-scl-h-ack-cycles", &u8val)) + priv->settings.tp_scl_h_ack_cycles = u8val; + + i3c_hub_tp_of_get_setting(dev, node, priv->settings.tp); + + priv->settings.handshake_optimize = + of_property_read_bool(node, "realtek,handshake-optimize"); + + if (!of_property_read_u8(node, "realtek,fast-drv-h-add-cycles", + &u8val)) + priv->settings.fast_drv_h_add_cycles = u8val; + + priv->settings.fast_rson_en = + of_property_read_bool(node, "realtek,fast-rson-en"); + + priv->settings.tp_od_vol_optimize = + of_property_read_bool(node, "realtek,tp-od-vol-optimize"); + + priv->settings.tp_od_vref_optimize = + of_property_read_bool(node, "realtek,tp-od-vref-optimize"); +} + +static const struct i3c_hub_dev_info * +i3c_hub_lookup_dev_info(struct i3c_hub *priv) +{ + int i, ret; + u16 part_id = 0; + u32 val = 0; + + ret = regmap_read(priv->regmap, I3C_HUB_DEV_INFO_0, &val); + if (ret) + return ERR_PTR(ret); + + part_id = (val & 0xFF) << 8; + + ret = regmap_read(priv->regmap, I3C_HUB_DEV_REV, &val); + if (ret) + return ERR_PTR(ret); + + part_id |= I3C_HUB_DEV_REV_LDO_GET(val); + + for (i = 0; i < ARRAY_SIZE(i3c_hub_dev_info_table); i++) { + if (i3c_hub_dev_info_table[i].part_id == part_id) + return &i3c_hub_dev_info_table[i]; + } + return &i3c_hub_dev_info_unknown; +} + +static void i3c_hub_of_default_configuration(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + int id; + + priv->settings.cp0_ldo_en = false; + priv->settings.cp1_ldo_en = false; + priv->settings.tp0145_ldo_en = false; + priv->settings.tp2367_ldo_en = false; + priv->settings.cp0_ldo_volt = 0; + priv->settings.cp1_ldo_volt = 0; + priv->settings.tp0145_ldo_volt = 0; + priv->settings.tp2367_ldo_volt = 0; + priv->settings.tp0145_pullup = UINT_MAX; + priv->settings.tp2367_pullup = UINT_MAX; + priv->settings.cp0_io_strength = 0; + priv->settings.cp1_io_strength = 0; + priv->settings.tp0145_io_strength = 0; + priv->settings.tp2367_io_strength = 0; + priv->settings.hub_net_always_i3c = false; + priv->settings.tp_scl_h_ack_cycles = 0; + priv->settings.handshake_optimize = false; + priv->settings.fast_drv_h_add_cycles = 3; + priv->settings.fast_rson_en = false; + priv->settings.tp_od_vol_optimize = false; + priv->settings.tp_od_vref_optimize = false; + + for (id = 0; id < I3C_HUB_TP_MAX_COUNT; ++id) { + priv->settings.tp[id].mode = I3C_HUB_DT_TP_MODE_NOT_DEFINED; + priv->settings.tp[id].pullup_en = false; + priv->settings.tp[id].io_mode = + I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED; + priv->settings.tp[id].poll_interval_ms = 0; + priv->settings.tp[id].clock_frequency = 400000; + } +} + +static int i3c_hub_hw_configure_pullup(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + u8 mask = 0, value = 0; + + if (priv->settings.tp0145_pullup != UINT_MAX) { + mask |= TP0145_PULLUP_CONF_MASK; + if (priv->settings.tp0145_pullup != 0) + value |= TP0145_PULLUP_CONF( + i3c_hub_pullup_dt_to_reg(priv->settings.tp0145_pullup)); + } + + if (priv->settings.tp2367_pullup != UINT_MAX) { + mask |= TP2367_PULLUP_CONF_MASK; + if (priv->settings.tp2367_pullup != 0) + value |= TP2367_PULLUP_CONF( + i3c_hub_pullup_dt_to_reg(priv->settings.tp2367_pullup)); + } + + return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF, + mask, value); +} + +static int i3c_hub_hw_configure_ldo(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + u8 ldo_config_mask = 0, ldo_config_val = 0; + u8 ldo_disable_mask = 0, ldo_en_val = 0; + u32 reg_val; + int ret; + u8 val; + + /* Enable or Disable LDO's. If there is no DT entry - disable LDO for safety reasons */ + if (priv->settings.cp0_ldo_en) + ldo_en_val |= CP0_LDO_EN; + if (priv->settings.cp1_ldo_en) + ldo_en_val |= CP1_LDO_EN; + if (priv->settings.tp0145_ldo_en) + ldo_en_val |= TP0145_LDO_EN; + if (priv->settings.tp2367_ldo_en) + ldo_en_val |= TP2367_LDO_EN; + + /* Get current LDOs configuration */ + ret = regmap_read(priv->regmap, I3C_HUB_LDO_CONF, ®_val); + if (ret) + return ret; + + /* LDOs Voltage level (Skip if not defined in the DT) + * Set the mask only if there is a change from current value + */ + if (priv->settings.cp0_ldo_volt != 0) { + val = CP0_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp0_ldo_volt)); + if ((reg_val & CP0_LDO_VOLTAGE_MASK) != val) { + ldo_config_mask |= CP0_LDO_VOLTAGE_MASK; + ldo_disable_mask |= CP0_LDO_EN; + ldo_config_val |= val; + } + } + if (priv->settings.cp1_ldo_volt != 0) { + val = CP1_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp1_ldo_volt)); + if ((reg_val & CP1_LDO_VOLTAGE_MASK) != val) { + ldo_config_mask |= CP1_LDO_VOLTAGE_MASK; + ldo_disable_mask |= CP1_LDO_EN; + ldo_config_val |= val; + } + } + if (priv->settings.tp0145_ldo_volt != 0) { + val = TP0145_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp0145_ldo_volt)); + if ((reg_val & TP0145_LDO_VOLTAGE_MASK) != val) { + ldo_config_mask |= TP0145_LDO_VOLTAGE_MASK; + ldo_disable_mask |= TP0145_LDO_EN; + ldo_config_val |= val; + } + } + if (priv->settings.tp2367_ldo_volt != 0) { + val = TP2367_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp2367_ldo_volt)); + if ((reg_val & TP2367_LDO_VOLTAGE_MASK) != val) { + ldo_config_mask |= TP2367_LDO_VOLTAGE_MASK; + ldo_disable_mask |= TP2367_LDO_EN; + ldo_config_val |= val; + } + } + + /* + * Update LDO voltage configuration only if value is changed from already existing register + * value. It is a good practice to disable the LDO's before making any voltage changes. + * Presence of config mask indicates voltage change to be applied. + */ + if (ldo_config_mask) { + /* Disable LDO's before making voltage changes */ + ret = regmap_update_bits(priv->regmap, + I3C_HUB_LDO_AND_PULLUP_CONF, + ldo_disable_mask, 0); + if (ret) + return ret; + + /* Update the LDOs configuration */ + ret = regmap_update_bits(priv->regmap, I3C_HUB_LDO_CONF, + ldo_config_mask, ldo_config_val); + if (ret) + return ret; + } + + /* Update the LDOs Enable/disable register. This will enable only LDOs enabled in DT */ + return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF, + LDO_ENABLE_DISABLE_MASK, ldo_en_val); +} + +static int i3c_hub_hw_configure_io_strength(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + u8 mask_all = 0, val_all = 0; + u32 reg_val; + u8 val; + struct dt_settings tmp; + int ret; + + /* Get IO strength configuration to figure out what needs to be changed */ + ret = regmap_read(priv->regmap, I3C_HUB_IO_STRENGTH, ®_val); + if (ret) + return ret; + + tmp = priv->settings; + if (tmp.cp0_io_strength != 0) { + val = CP0_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp0_io_strength)); + mask_all |= CP0_IO_STRENGTH_MASK; + val_all |= val; + } + if (tmp.cp1_io_strength != 0) { + val = CP1_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp1_io_strength)); + mask_all |= CP1_IO_STRENGTH_MASK; + val_all |= val; + } + if (tmp.tp0145_io_strength != 0) { + val = TP0145_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp0145_io_strength)); + mask_all |= TP0145_IO_STRENGTH_MASK; + val_all |= val; + } + if (tmp.tp2367_io_strength != 0) { + val = TP2367_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp2367_io_strength)); + mask_all |= TP2367_IO_STRENGTH_MASK; + val_all |= val; + } + + /* Set IO strength if required */ + return regmap_update_bits(priv->regmap, I3C_HUB_IO_STRENGTH, mask_all, + val_all); +} + +static int i3c_hub_hw_configure_tp(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + u8 pullup_mask = 0, pullup_val = 0; + u8 smbus_mask = 0, smbus_val = 0; + u8 gpio_mask = 0, gpio_val = 0; + u8 i3c_mask = 0, i3c_val = 0; + u8 io_mode_mask = 0, io_mode_val = 0; + int ret; + int i, index; + + memset(priv->gpio.port_to_index, -1, sizeof(priv->gpio.port_to_index)); + + for (i = 0; i < priv->dev_info->n_ports; ++i) { + if (priv->settings.tp[i].mode != + I3C_HUB_DT_TP_MODE_NOT_DEFINED) { + i3c_mask |= TPN_NET_CON(i); + smbus_mask |= TPN_SMBUS_MODE_EN(i); + gpio_mask |= TPN_GPIO_MODE_EN(i); + io_mode_mask |= TPN_IO_MODE_CON(i); + + if (priv->settings.tp[i].mode == + I3C_HUB_DT_TP_MODE_I3C) { + i3c_val |= TPN_NET_CON(i); + } else if (priv->settings.tp[i].mode == + I3C_HUB_DT_TP_MODE_SMBUS) { + smbus_val |= TPN_SMBUS_MODE_EN(i); + } else if (priv->settings.tp[i].mode == + I3C_HUB_DT_TP_MODE_GPIO) { + gpio_val |= TPN_GPIO_MODE_EN(i); + priv->gpio.nums += GPIO_BANK_SZ; + index = priv->gpio.nums / GPIO_BANK_SZ - 1; + priv->gpio.tp[index] = i; + priv->gpio.port_to_index[i] = index; + } + } + pullup_mask |= TPN_PULLUP_EN(i); + if (priv->settings.tp[i].pullup_en) + pullup_val |= TPN_PULLUP_EN(i); + if (priv->settings.tp[i].io_mode != + I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED) { + if (priv->settings.tp[i].io_mode == + I3C_HUB_DT_TP_IO_MODE_OD) + io_mode_val |= TPN_IO_MODE_CON(i); + } else if (priv->settings.tp[i].mode == + I3C_HUB_DT_TP_MODE_SMBUS) { + io_mode_val |= TPN_IO_MODE_CON(i); + } + } + + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_IO_MODE_CONF, + io_mode_mask, io_mode_val); + if (ret) + return ret; + + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_PULLUP_EN, + pullup_mask, pullup_val); + if (ret) + return ret; + + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_SMBUS_AGNT_EN, + smbus_mask, smbus_val); + if (ret) + return ret; + + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_GPIO_MODE_EN, + gpio_mask, gpio_val); + if (ret) + return ret; + + /* Request for HUB Network connection in case any TP is configured in I3C mode */ + if (i3c_val) { + ret = regmap_write(priv->regmap, I3C_HUB_CP_MUX_SET, + CONTROLLER_PORT_MUX_REQ); + if (ret) + return ret; + /* TODO: verify if connection is done */ + } + + /* Enable TP here in case TP was configured */ + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_ENABLE, + i3c_mask | smbus_mask | gpio_mask, + i3c_val | smbus_val | gpio_val); + if (ret) + return ret; + + return regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, i3c_val); +} + +static int i3c_hub_hw_configure_misc(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + int ret; + u8 reg = I3C_HUB_TARGET_DA_FLAG_BYTE_BASE; + u8 val[I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT]; + + if (!priv->settings.hub_net_always_i3c) + return 0; + + memset(val, 0xff, I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT); + + ret = regmap_update_bits(priv->regmap, I3C_HUB_NET_OPER_MODE_CONF, + I3C_HUB_NET_ALWAYS_I3C_EN, + I3C_HUB_NET_ALWAYS_I3C_EN); + if (ret) + return ret; + + ret = regmap_bulk_write(priv->regmap, reg, val, + I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT); + return ret; +} + +typedef int (*i3c_hub_cfg_fn)(struct i3c_hub *priv); + +static int i3c_hub_hw_cfg_with_page(struct i3c_hub *priv, u8 page, + i3c_hub_cfg_fn op) +{ + int ret; + + if (!op) + return -EINVAL; + + mutex_lock(&priv->page_mutex); + ret = regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, page); + if (ret) + goto unlock; + + ret = op(priv); +unlock: + regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, 0x00); + mutex_unlock(&priv->page_mutex); + return ret; +} + +static int i3c_hub_cfg_op_fuse_latch(struct i3c_hub *priv) +{ + int ret; + + if (priv->settings.handshake_optimize) { + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E, + I3C_HUB_IBI_ACK_RD_CYCLE_MASK, + I3C_HUB_IBI_ACK_RD_CYCLE_VAL); + if (ret) + return ret; + } + + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A3, + I3C_HUB_FAST_DRV_LOOP_DIS, + I3C_HUB_FAST_DRV_LOOP_DIS); + if (ret) + return ret; + + if (priv->settings.tp_od_vol_optimize) { + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D, + I3C_HUB_TP_OD_VOL_LEVEL, + I3C_HUB_TP_OD_VOL_LEVEL); + if (ret) + return ret; + } + + if (priv->settings.tp_od_vref_optimize) { + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D, + I3C_HUB_TP_OD_VREF, + I3C_HUB_TP_OD_VREF); + if (ret) + return ret; + } + + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E, + I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK, + I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL( + priv->settings.fast_drv_h_add_cycles)); + if (ret) + return ret; + + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A0, + I3C_HUB_FAST_RSON_EN, + priv->settings.fast_rson_en ? I3C_HUB_FAST_RSON_EN : 0); + return ret; +} + +static int i3c_hub_hw_configure_fuse_latch(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + + return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_EFUSE_PAGE, + i3c_hub_cfg_op_fuse_latch); +} + +static int i3c_hub_cfg_op_io(struct i3c_hub *priv) +{ + int ret; + u8 reg = I3C_HUB_CFG_TP_SCL_L_ACK_CLK; + + /* cfg tp scl low ack clk */ + ret = regmap_write(priv->regmap, reg, + I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN | + I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL); + if (ret) + return ret; + + /* cfg tp scl high ack clk */ + reg = I3C_HUB_CFG_TP_SCL_H_ACK_CLK; + if (priv->settings.tp_scl_h_ack_cycles == 0) + return 0; + + ret = regmap_write(priv->regmap, reg, + I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN | + I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL( + priv->settings.tp_scl_h_ack_cycles)); + + return ret; +} + +static int i3c_hub_hw_configure_io(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + + return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_IO_CTRL_PAGE, + i3c_hub_cfg_op_io); +} + +static int i3c_hub_configure_hw(struct device *dev) +{ + int ret; + struct i3c_hub *priv = dev_get_drvdata(dev); + + ret = i3c_hub_hw_configure_ldo(dev); + if (ret) + return ret; + + ret = i3c_hub_hw_configure_io_strength(dev); + if (ret) + return ret; + + ret = i3c_hub_hw_configure_pullup(dev); + if (ret) + return ret; + + if (priv->dev_info->part_id) { + ret = i3c_hub_hw_configure_misc(dev); + if (ret) + return ret; + + ret = i3c_hub_hw_configure_fuse_latch(dev); + if (ret) + return ret; + + ret = i3c_hub_hw_configure_io(dev); + if (ret) + return ret; + } + + ret = i3c_hub_hw_configure_tp(dev); + return ret; +} + +static void i3c_hub_of_get_conf_runtime(struct device *dev, + const struct device_node *node) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + struct device_node *i3c_node; + int i3c_id; + u8 tp_mask; + + for_each_available_child_of_node(node, i3c_node) { + if (!i3c_node->full_name || + (sscanf(i3c_node->full_name, "i3c%i@%hhx", &i3c_id, + &tp_mask) != 2)) + continue; + + if (i3c_id < priv->dev_info->n_ports) { + priv->logical_bus[i3c_id].available = true; + priv->logical_bus[i3c_id].of_node = i3c_node; + priv->logical_bus[i3c_id].tp_map = tp_mask; + priv->logical_bus[i3c_id].priv = priv; + priv->logical_bus[i3c_id].tp_id = i3c_id; + } + } +} + +static const struct i3c_device_id i3c_hub_ids[] = { + I3C_CLASS(I3C_DCR_HUB, NULL), + {}, +}; + +static int i3c_hub_read_id(struct device *dev) +{ + struct i3c_hub *priv = dev_get_drvdata(dev); + u32 reg_val; + int ret; + + ret = regmap_read(priv->regmap, I3C_HUB_LDO_AND_CPSEL_STS, ®_val); + if (ret) { + dev_err(dev, "Failed to read status register\n"); + return -1; + } + + priv->hub_pin_sel_id = CP_SEL_PIN_INPUT_CODE_GET(reg_val); + priv->hub_pin_cp1_id = CP_SDA1_SCL1_PINS_CODE_GET(reg_val); + return 0; +} + +static struct device_node *i3c_hub_get_dt_hub_node(struct device_node *node, + struct i3c_hub *priv) +{ + struct device_node *hub_node_no_id = NULL; + struct device_node *hub_node; + u32 hub_id; + u32 id_mask; + u32 dt_id; + u32 pin_id; + int found_id = 0; + + for_each_available_child_of_node(node, hub_node) { + id_mask = 0; + priv->hub_dt_sel_id = -1; + priv->hub_dt_cp1_id = -1; + + if (strstr(hub_node->name, "hub")) { + if (!of_property_read_u32(hub_node, "realtek,id", + &hub_id)) { + id_mask |= 0x0f; + priv->hub_dt_sel_id = hub_id; + } + + if (!of_property_read_u32(hub_node, "realtek,id-cp1", + &hub_id)) { + id_mask |= 0xf0; + priv->hub_dt_cp1_id = hub_id; + } + + dt_id = ((u32)priv->hub_dt_cp1_id & 0x0f) << 4 | + ((u32)priv->hub_dt_sel_id & 0x0f); + pin_id = ((u32)priv->hub_pin_cp1_id & 0x0f) << 4 | + ((u32)priv->hub_pin_sel_id & 0x0f); + + if (id_mask != 0 && + (dt_id & id_mask) == (pin_id & id_mask)) + found_id = 1; + + if (!found_id) { + /* + * Just keep reference to first HUB node with no ID in case no ID + * matching + */ + if (!hub_node_no_id && + priv->hub_dt_sel_id == -1 && + priv->hub_dt_cp1_id == -1) + hub_node_no_id = hub_node; + } else { + return hub_node; + } + } + } + + return hub_node_no_id; +} + +static int fops_access_reg_get(void *ctx, u64 *val) +{ + struct i3c_hub *priv = ctx; + u32 reg_val; + int ret; + + ret = regmap_read(priv->regmap, priv->reg_addr, ®_val); + if (ret) + return ret; + + *val = reg_val & 0xFF; + return 0; +} + +static int fops_access_reg_set(void *ctx, u64 val) +{ + struct i3c_hub *priv = ctx; + + return regmap_write(priv->regmap, priv->reg_addr, val & 0xFF); +} + +DEFINE_DEBUGFS_ATTRIBUTE(fops_access_reg, fops_access_reg_get, + fops_access_reg_set, "0x%llX\n"); + +static int i3c_hub_debugfs_init(struct i3c_hub *priv, const char *hub_id) +{ + struct dentry *entry, *dt_conf_dir, *reg_dir; + struct dt_settings *settings = NULL; + int i; + + entry = debugfs_create_dir(hub_id, NULL); + if (IS_ERR(entry)) + return PTR_ERR(entry); + + priv->debug_dir = entry; + + if (priv->dev_info) + debugfs_create_str("model", 0400, priv->debug_dir, + (char **)&priv->dev_info->model); + + entry = debugfs_create_dir("dt-conf", priv->debug_dir); + if (IS_ERR(entry)) + goto err_remove; + + dt_conf_dir = entry; + + settings = &priv->settings; + debugfs_create_bool("cp0-ldo-en", 0400, dt_conf_dir, + &settings->cp0_ldo_en); + debugfs_create_bool("cp1-ldo-en", 0400, dt_conf_dir, + &settings->cp1_ldo_en); + debugfs_create_u32("cp0-ldo-volt", 0400, dt_conf_dir, + &settings->cp0_ldo_volt); + debugfs_create_u32("cp1-ldo-volt", 0400, dt_conf_dir, + &settings->cp1_ldo_volt); + debugfs_create_bool("tp0145-ldo-en", 0400, dt_conf_dir, + &settings->tp0145_ldo_en); + debugfs_create_bool("tp2367-ldo-en", 0400, dt_conf_dir, + &settings->tp2367_ldo_en); + debugfs_create_u32("tp0145-ldo-volt", 0400, dt_conf_dir, + &settings->tp0145_ldo_volt); + debugfs_create_u32("tp2367-ldo-volt", 0400, dt_conf_dir, + &settings->tp2367_ldo_volt); + debugfs_create_u32("tp0145-pullup", 0400, dt_conf_dir, + &settings->tp0145_pullup); + debugfs_create_u32("tp2367-pullup", 0400, dt_conf_dir, + &settings->tp2367_pullup); + debugfs_create_bool("hub-net-always-i3c", 0400, dt_conf_dir, + &settings->hub_net_always_i3c); + debugfs_create_u8("tp-scl-h-ack-cycles", 0400, dt_conf_dir, + &settings->tp_scl_h_ack_cycles); + debugfs_create_bool("handshake-optimize", 0400, dt_conf_dir, + &settings->handshake_optimize); + debugfs_create_u8("fast-drv-h-add-cycles", 0400, dt_conf_dir, + &settings->fast_drv_h_add_cycles); + debugfs_create_bool("fast-rson-en", 0400, dt_conf_dir, + &settings->fast_rson_en); + debugfs_create_bool("tp-od-vol-optimize", 0400, dt_conf_dir, + &settings->tp_od_vol_optimize); + debugfs_create_bool("tp-od-vref-optimize", 0400, dt_conf_dir, + &settings->tp_od_vref_optimize); + + for (i = 0; i < priv->dev_info->n_ports; ++i) { + char file_name[32]; + + sprintf(file_name, "tp%i.mode", i); + debugfs_create_u8(file_name, 0400, dt_conf_dir, + &settings->tp[i].mode); + sprintf(file_name, "tp%i.pullup_en", i); + debugfs_create_bool(file_name, 0400, dt_conf_dir, + &settings->tp[i].pullup_en); + sprintf(file_name, "tp%i.io_mode", i); + debugfs_create_u8(file_name, 0400, dt_conf_dir, + &settings->tp[i].io_mode); + sprintf(file_name, "tp%i.poll_interval_ms", i); + debugfs_create_u32(file_name, 0400, dt_conf_dir, + &settings->tp[i].poll_interval_ms); + sprintf(file_name, "tp%i.clock_frequency", i); + debugfs_create_u32(file_name, 0400, dt_conf_dir, + &settings->tp[i].clock_frequency); + } + + entry = debugfs_create_dir("reg", priv->debug_dir); + if (IS_ERR(entry)) + goto err_remove; + + reg_dir = entry; + + entry = debugfs_create_file_unsafe("access", 0600, reg_dir, priv, + &fops_access_reg); + if (IS_ERR(entry)) + goto err_remove; + + debugfs_create_u8("offset", 0600, reg_dir, &priv->reg_addr); + + return 0; + +err_remove: + debugfs_remove_recursive(priv->debug_dir); + return PTR_ERR(entry); +} + +static void i3c_hub_trans_pre_cb(struct logical_bus *bus) +{ + struct i3c_hub *priv = bus->priv; + struct device *dev = i3cdev_to_dev(priv->i3cdev); + int ret; + + if (priv->settings.tp[bus->tp_id].always_enable) + return; + + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF, + GENMASK(bus->tp_id, bus->tp_id), bus->tp_map); + if (ret) + dev_warn(dev, "Failed to open Target Port(s)\n"); +} + +static void i3c_hub_trans_post_cb(struct logical_bus *bus) +{ + struct i3c_hub *priv = bus->priv; + struct device *dev = i3cdev_to_dev(priv->i3cdev); + int ret; + + if (priv->settings.tp[bus->tp_id].always_enable) + return; + + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF, + GENMASK(bus->tp_id, bus->tp_id), 0x00); + if (ret) + dev_warn(dev, "Failed to close Target Port(s)\n"); +} + +static struct logical_bus *bus_from_i3c_desc(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(desc); + + return container_of(controller, struct logical_bus, controller); +} + +static struct logical_bus *bus_from_i2c_desc(struct i2c_dev_desc *desc) +{ + struct i3c_master_controller *controller = i2c_dev_get_master(desc); + + return container_of(controller, struct logical_bus, controller); +} + +static struct i3c_master_controller * +parent_from_controller(struct i3c_master_controller *controller) +{ + struct logical_bus *bus = + container_of(controller, struct logical_bus, controller); + + return bus->priv->driving_master; +} + +static struct i3c_master_controller * +parent_controller_from_i3c_desc(struct i3c_dev_desc *desc) +{ + struct i3c_master_controller *controller = i3c_dev_get_master(desc); + struct logical_bus *bus = + container_of(controller, struct logical_bus, controller); + + return bus->priv->driving_master; +} + +static struct i3c_master_controller * +parent_controller_from_i2c_desc(struct i2c_dev_desc *desc) +{ + struct i3c_master_controller *controller = desc->common.master; + struct logical_bus *bus = + container_of(controller, struct logical_bus, controller); + + return bus->priv->driving_master; +} + +static struct i3c_master_controller * +update_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc, + struct i3c_master_controller *parent) +{ + struct i3c_master_controller *orig_parent = desc->master; + + desc->master = parent; + + return orig_parent; +} + +static void restore_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc, + struct i3c_master_controller *parent) +{ + desc->master = parent; +} + +static int i3c_hub_read_transaction_status(struct i3c_hub *priv, u8 target_port, + u8 target_port_status, u32 data_len) +{ + unsigned int status_read; + int ret; + struct i2c_adapter_group *smbus = + &priv->smbus_port_adapter[target_port]; + u32 smbus_clk = priv->settings.tp[target_port].clock_frequency / 1000; + u8 status; + u8 ret_code; + + if (!priv->settings.tp[target_port].poll_interval_ms) { + ret = wait_for_completion_timeout(&smbus->completion, + smbus->i2c.timeout); + if (!ret) { + dev_err(&priv->i3cdev->dev, + "Status read timeout reached on target port %d\n", + target_port); + return -ETIMEDOUT; + } + + status = (u8)smbus->status & + I3C_HUB_CONTROLLER_AGENT_STATUS_MASK; + } else { + ret = regmap_read_poll_timeout(priv->regmap, + target_port_status, + status_read, + (u8)status_read & + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG, + I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL( + data_len, smbus_clk), + jiffies_to_usecs(smbus->i2c.timeout)); + + if (ret) { + dev_err(&priv->i3cdev->dev, + "Status read timeout reached on target port %d\n", + target_port); + return ret; + } + + ret = regmap_write(priv->regmap, target_port_status, + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG); + if (ret) + return ret; + + status = (u8)status_read & I3C_HUB_CONTROLLER_AGENT_STATUS_MASK; + } + + ret_code = status >> I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT; + + switch (ret_code) { + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS: + return 0; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK: + dev_dbg(&priv->i3cdev->dev, + "TP%u SMBus: Address NACK (device not present)\n", + target_port); + return -ENXIO; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY: + dev_dbg(&priv->i3cdev->dev, + "TP%u SMBus: Device busy (data NACK after address ACK)\n", + target_port); + return -EREMOTEIO; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY: + dev_dbg(&priv->i3cdev->dev, + "TP%u SMBus: Device read not ready (read address NACK after write)\n", + target_port); + return -ENXIO; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED: + dev_dbg(&priv->i3cdev->dev, + "TP%u SMBus: Sync issue recovered (SDA stuck low, recovered by 9 SCL pulses)\n", + target_port); + return -EAGAIN; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR: + dev_dbg(&priv->i3cdev->dev, + "TP%u SMBus: Sync issue bus clear (recovered by SCL low 35ms)\n", + target_port); + return -EAGAIN; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT: + dev_err(&priv->i3cdev->dev, + "TP%u SMBus: Bus fault (SDA stuck low remains after recovery)\n", + target_port); + return -EIO; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST: + dev_dbg(&priv->i3cdev->dev, "TP%u SMBus: Arbitration lost\n", + target_port); + return -EAGAIN; + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT: + dev_err(&priv->i3cdev->dev, "TP%u SMBus: SCL timeout\n", + target_port); + return -ETIMEDOUT; + default: + dev_err(&priv->i3cdev->dev, + "TP%u SMBus: Reserved/unknown return code 0x%x\n", + target_port, ret_code); + return -EIO; + } +} + +/* + * i3c_hub_smbus_msg() - This starts a smbus transaction by writing a descriptor + * and a message to the hub registers. Controller buffer page is determined by multiplying the + * target port index by four and adding the base page number to it. + * @hub: a pointer to the i3c hub main structure + * @target_port: a number of the port where the transaction will happen + * @xfers: i2c_msg struct received from the master_xfers callback + * @nxfers_i: the number of the current message + * @xfer_type: transfer type: + * - I3C_HUB_SMBUS_XFER_WRITE (0): single write + * - I3C_HUB_SMBUS_XFER_READ (1): single read + * - I3C_HUB_SMBUS_XFER_WR_RD (2): write followed by read + * (uses xfers[nxfers_i] as write and xfers[nxfers_i+1] as read) + * + * Return: 0 on success, negative errno on failure from hub status or regmap ops. + * Note: for WR_RD the caller must ensure xfers[nxfers_i+1] exists, the address + * matches, and write_len + read_len <= I3C_HUB_SMBUS_PAYLOAD_SIZE. + */ +static int i3c_hub_smbus_msg(struct i3c_hub *hub, struct i2c_msg *xfers, + u8 target_port, u8 nxfers_i, u8 xfer_type) +{ + u8 transaction_type = I3C_HUB_SMBUS_400_KHZ; + u8 controller_buffer_page = + I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * target_port; + int write_length = 0, read_length = 0; + u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + target_port; + u8 target_port_code = BIT(target_port); + u8 rw_address = xfers[nxfers_i].addr << 1; + u8 desc[I3C_HUB_SMBUS_DESCRIPTOR_SIZE] = { 0 }; + int ret = 0; + + transaction_type = + i3c_hub_smbus_rate_bits_from_hz(hub->settings.tp[target_port].clock_frequency); + + switch (xfer_type) { + case I3C_HUB_SMBUS_XFER_WRITE: + write_length = xfers[nxfers_i].len; + break; + case I3C_HUB_SMBUS_XFER_READ: + read_length = xfers[nxfers_i].len; + rw_address |= BIT(0); + break; + case I3C_HUB_SMBUS_XFER_WR_RD: + write_length = xfers[nxfers_i].len; + read_length = xfers[nxfers_i + 1].len; + transaction_type |= BIT(0); + break; + default: + return -EINVAL; + } + + /* Assemble descriptor */ + desc[0] = rw_address; + desc[1] = transaction_type; + desc[2] = write_length; + desc[3] = read_length; + + mutex_lock(&hub->page_mutex); + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, + controller_buffer_page); + if (ret) + goto unlock; + + ret = regmap_bulk_write(hub->regmap, I3C_HUB_CONTROLLER_AGENT_BUFF, + desc, I3C_HUB_SMBUS_DESCRIPTOR_SIZE); + if (ret) + goto unlock; + + if (write_length) { + ret = regmap_bulk_write(hub->regmap, + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA, + xfers[nxfers_i].buf, write_length); + if (ret) + goto unlock; + } + + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); + mutex_unlock(&hub->page_mutex); + if (ret) + return ret; + + /* Start transaction */ + ret = regmap_write(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_TRANS_START, + target_port_code); + if (ret) + return ret; + + /* Get transaction status */ + ret = i3c_hub_read_transaction_status(hub, target_port, + target_port_status, + write_length + read_length); + if (ret) + return ret; + + /* if read_length is non-zero, read back the data */ + if (read_length) { + mutex_lock(&hub->page_mutex); + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, + controller_buffer_page); + if (ret) + goto unlock; + + if (xfer_type == I3C_HUB_SMBUS_XFER_READ) { + ret = regmap_bulk_read(hub->regmap, + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA, + xfers[nxfers_i].buf, read_length); + } else { + ret = regmap_bulk_read(hub->regmap, + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA + + write_length, + xfers[nxfers_i + 1].buf, read_length); + } + if (ret) + goto unlock; + + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); + mutex_unlock(&hub->page_mutex); + } + + return ret; +unlock: + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); + mutex_unlock(&hub->page_mutex); + return ret; +} + +static inline bool i3c_hub_can_combine_wr_rd(const struct i2c_msg *w, + const struct i2c_msg *r) +{ + /* w: write, r: read; same addr; total length within payload */ + return !(w->flags & I2C_M_RD) && (r->flags & I2C_M_RD) && + w->addr == r->addr && + (w->len + r->len) <= I3C_HUB_SMBUS_PAYLOAD_SIZE; +} + +/** + * i3c_hub_smbus_port_adapter_xfer() - i3c hub smbus transfer logic + * @adap: i2c_adapter corresponding with single port in the i3c hub + * @xfers: all messages descriptors and data + * @nxfers: amount of single messages in a transfer + * + * Return: function returns the sum of correctly sent messages (only those with hub return + * status 0x01) + */ +static int i3c_hub_smbus_port_adapter_xfer(struct i2c_adapter *adap, + struct i2c_msg *xfers, int nxfers) +{ + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap); + struct i3c_hub *hub = smbus->priv; + int ret_sum = 0, ret, len, type, nxfers_i; + const struct i2c_msg *cur = NULL, *next = NULL; + + for (nxfers_i = 0; nxfers_i < nxfers; nxfers_i++) { + cur = &xfers[nxfers_i]; + len = cur->len; + type = cur->flags & I2C_M_RD ? I3C_HUB_SMBUS_XFER_READ : + I3C_HUB_SMBUS_XFER_WRITE; + + /* Per-message length limit check */ + if (len > I3C_HUB_SMBUS_PAYLOAD_SIZE) { + dev_err(&adap->dev, + "Message nr. %d not sent - length over %d bytes.\n", + nxfers_i, I3C_HUB_SMBUS_PAYLOAD_SIZE); + continue; + } + + /* Try to combine write followed by read to the same address */ + if (type == I3C_HUB_SMBUS_XFER_WRITE && + (nxfers_i + 1) < nxfers) { + next = &xfers[nxfers_i + 1]; + if (i3c_hub_can_combine_wr_rd(cur, next)) + type = I3C_HUB_SMBUS_XFER_WR_RD; + } + + ret = i3c_hub_smbus_msg(hub, xfers, smbus->tp_port, nxfers_i, + type); + if (ret) + return ret; + + if (type == I3C_HUB_SMBUS_XFER_WR_RD) { + ret_sum += 2; + nxfers_i++; /* skip the next read message */ + + } else { + ret_sum++; + } + } + return ret_sum; +} + +static int i3c_hub_bus_init(struct i3c_master_controller *controller) +{ + struct logical_bus *bus = + container_of(controller, struct logical_bus, controller); + + controller->this = bus->priv->i3cdev->desc; + return 0; +} + +static void i3c_hub_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 *parent = + parent_controller_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + int ret; + + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + ret = parent->ops->attach_i3c_dev(dev); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + return ret; +} + +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev, u8 old_dyn_addr) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + int ret; + + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + ret = parent->ops->reattach_i3c_dev(dev, old_dyn_addr); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + return ret; +} + +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + parent->ops->detach_i3c_dev(dev); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); +} + +static int i3c_hub_do_daa(struct i3c_master_controller *controller) +{ + struct i3c_master_controller *parent = + parent_from_controller(controller); + int ret; + + down_write(&parent->bus.lock); + ret = parent->ops->do_daa(parent); + up_write(&parent->bus.lock); + return ret; +} + +static bool i3c_hub_supports_ccc_cmd(struct i3c_master_controller *controller, + const struct i3c_ccc_cmd *cmd) +{ + struct i3c_master_controller *parent = + parent_from_controller(controller); + + return parent->ops->supports_ccc_cmd(parent, cmd); +} + +static int i3c_hub_send_ccc_cmd(struct i3c_master_controller *controller, + struct i3c_ccc_cmd *cmd) +{ + struct i3c_master_controller *parent = + parent_from_controller(controller); + struct logical_bus *bus = + container_of(controller, struct logical_bus, controller); + int ret; + + if (cmd->id == I3C_CCC_RSTDAA(true)) + return 0; + + i3c_hub_trans_pre_cb(bus); + down_read(&parent->bus.lock); + ret = parent->ops->send_ccc_cmd(parent, cmd); + up_read(&parent->bus.lock); + i3c_hub_trans_post_cb(bus); + + return ret; +} + +static int i3c_hub_priv_xfers(struct i3c_dev_desc *dev, + struct i3c_xfer *xfers, int nxfers, + enum i3c_xfer_mode mode) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + struct logical_bus *bus = bus_from_i3c_desc(dev); + int res; + + i3c_hub_trans_pre_cb(bus); + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + down_read(&parent->bus.lock); + res = parent->ops->i3c_xfers(dev, xfers, nxfers, mode); + up_read(&parent->bus.lock); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + i3c_hub_trans_post_cb(bus); + + return res; +} + +static int i3c_hub_attach_i2c_dev(struct i2c_dev_desc *dev) +{ + struct i3c_master_controller *parent = + parent_controller_from_i2c_desc(dev); + struct i3c_master_controller *orig_parent; + int ret; + + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + ret = parent->ops->attach_i2c_dev(dev); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + return ret; +} + +static void i3c_hub_detach_i2c_dev(struct i2c_dev_desc *dev) +{ + struct i3c_master_controller *parent = + parent_controller_from_i2c_desc(dev); + struct i3c_master_controller *orig_parent; + + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + parent->ops->detach_i2c_dev(dev); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); +} + +static int i3c_hub_i2c_xfers(struct i2c_dev_desc *dev, struct i2c_msg *xfers, + int nxfers) +{ + struct i3c_master_controller *parent = + parent_controller_from_i2c_desc(dev); + struct logical_bus *bus = bus_from_i2c_desc(dev); + struct i3c_master_controller *orig_parent; + int ret; + + i3c_hub_trans_pre_cb(bus); + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + ret = parent->ops->i2c_xfers(dev, xfers, nxfers); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + i3c_hub_trans_post_cb(bus); + return ret; +} + +static int i3c_hub_request_ibi(struct i3c_dev_desc *dev, + const struct i3c_ibi_setup *req) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct logical_bus *bus = bus_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + int ret; + + i3c_hub_trans_pre_cb(bus); + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + down_read(&parent->bus.lock); + ret = parent->ops->request_ibi(dev, req); + up_read(&parent->bus.lock); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + i3c_hub_trans_post_cb(bus); + return ret; +} + +static void i3c_hub_free_ibi(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct logical_bus *bus = bus_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + + i3c_hub_trans_pre_cb(bus); + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + down_read(&parent->bus.lock); + parent->ops->free_ibi(dev); + up_read(&parent->bus.lock); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + i3c_hub_trans_post_cb(bus); +} + +static int i3c_hub_enable_ibi(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct logical_bus *bus = bus_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + int ret; + + i3c_hub_trans_pre_cb(bus); + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + down_read(&parent->bus.lock); + ret = parent->ops->enable_ibi(dev); + up_read(&parent->bus.lock); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + i3c_hub_trans_post_cb(bus); + return ret; +} + +static int i3c_hub_disable_ibi(struct i3c_dev_desc *dev) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct logical_bus *bus = bus_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + int ret; + + i3c_hub_trans_pre_cb(bus); + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + down_read(&parent->bus.lock); + ret = parent->ops->disable_ibi(dev); + up_read(&parent->bus.lock); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); + i3c_hub_trans_post_cb(bus); + return ret; +} + +static void i3c_hub_recycle_ibi_slot(struct i3c_dev_desc *dev, + struct i3c_ibi_slot *slot) +{ + struct i3c_master_controller *parent = + parent_controller_from_i3c_desc(dev); + struct i3c_master_controller *orig_parent; + + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); + parent->ops->recycle_ibi_slot(dev, slot); + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); +} + +static const struct i3c_master_controller_ops i3c_hub_i3c_ops = { + .bus_init = i3c_hub_bus_init, + .bus_cleanup = i3c_hub_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_do_daa, + .supports_ccc_cmd = i3c_hub_supports_ccc_cmd, + .send_ccc_cmd = i3c_hub_send_ccc_cmd, + .i3c_xfers = i3c_hub_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, +}; + +static int i3c_hub_logic_register(struct i3c_master_controller *controller, + struct device *parent) +{ + return i3c_master_register(controller, parent, &i3c_hub_i3c_ops, false); +} + +static u32 i3c_hub_smbus_funcs(struct i2c_adapter *adapter) +{ + return (I2C_FUNC_SMBUS_EMUL | I2C_FUNC_I2C) & ~I2C_FUNC_SMBUS_QUICK; +} + +#if IS_ENABLED(CONFIG_I2C_SLAVE) +static int reg_i2c_target(struct i2c_client *client) +{ + struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter); + struct smbus_backend *backend; + int ret = 0; + + if (!smbus) + return -EINVAL; + + mutex_lock(&smbus->mutex); + + list_for_each_entry(backend, &smbus->backend_entry, list) { + if (backend->client->addr == client->addr) { + ret = -EBUSY; + goto out; + } + } + + backend = kzalloc(sizeof(*backend), GFP_KERNEL); + if (!backend) { + ret = -ENOMEM; + goto out; + } + + backend->client = client; + list_add(&backend->list, &smbus->backend_entry); + +out: + mutex_unlock(&smbus->mutex); + return ret; +} + +static int unreg_i2c_target(struct i2c_client *client) +{ + struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter); + struct smbus_backend *backend; + bool found = false; + + if (!smbus) + return -EINVAL; + + mutex_lock(&smbus->mutex); + + list_for_each_entry(backend, &smbus->backend_entry, list) { + if (backend->client->addr == client->addr) { + list_del(&backend->list); + kfree(backend); + found = true; + break; + } + } + + mutex_unlock(&smbus->mutex); + return found ? 0 : -ENODEV; +} +#endif /* CONFIG_I2C_SLAVE */ + +static const struct i2c_algorithm i3c_hub_smbus_algo = { + .master_xfer = i3c_hub_smbus_port_adapter_xfer, + .functionality = i3c_hub_smbus_funcs, +#if IS_ENABLED(CONFIG_I2C_SLAVE) + .reg_slave = reg_i2c_target, + .unreg_slave = unreg_i2c_target, +#endif +}; + +static void i3c_hub_delayed_work(struct work_struct *work) +{ + struct i3c_hub *priv = + container_of(work, typeof(*priv), delayed_work.work); + struct device *dev = i3cdev_to_dev(priv->i3cdev); + struct logical_bus *bus; + struct i2c_adapter_group *smbus; + int ret; + int i; + unsigned int reg_val = 0; + + /* record reg 81: tp hubnetwork connection setting */ + ret = regmap_read(priv->regmap, I3C_HUB_TP_NET_CON_CONF, ®_val); + if (ret) { + dev_warn(dev, "Failed to read hubnetwork connection setting\n"); + return; + } + + ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, 0x00); + if (ret) { + dev_warn(dev, "Failed to close Target Port(s)\n"); + return; + } + + for (i = 0; i < priv->dev_info->n_ports; ++i) { + bus = &priv->logical_bus[i]; + if (bus->available) { + ret = regmap_update_bits(priv->regmap, + I3C_HUB_TP_NET_CON_CONF, + GENMASK(bus->tp_id, bus->tp_id), + bus->tp_map); + if (ret) { + dev_warn(dev, + "Failed to open Target Port(s)\n"); + return; + } + + dev->of_node = bus->of_node; + ret = i3c_hub_logic_register(&bus->controller, dev); + if (ret) { + dev_warn(dev, + "Failed to register i3c controller - bus id:%i\n", + i); + return; + } + bus->registered = true; + + ret = regmap_update_bits(priv->regmap, + I3C_HUB_TP_NET_CON_CONF, + GENMASK(bus->tp_id, bus->tp_id), + 0x00); + if (ret) { + dev_warn(dev, + "Failed to close Target Port(s)\n"); + return; + } + + if (!priv->settings.tp[i].always_enable) + reg_val &= ~GENMASK(bus->tp_id, bus->tp_id); + } + } + + /* update tp hubnetwork connection setting */ + ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, reg_val); + if (ret) { + dev_warn(dev, "Failed to open Target Port(s)\n"); + return; + } + + ret = i3c_master_do_daa(priv->driving_master); + if (ret) { + dev_warn(dev, "Failed to run DAA\n"); + return; + } + + for (i = 0; i < priv->dev_info->n_ports; i++) { + smbus = &priv->smbus_port_adapter[i]; + if (!smbus->used) + continue; + + if (!priv->settings.tp[i].poll_interval_ms) + continue; + + schedule_delayed_work(&smbus->delayed_work_polling, + msecs_to_jiffies(priv->settings.tp[i].poll_interval_ms)); + } +} + +static int send_smbus_target_data_to_backend(struct i2c_adapter_group *smbus, + u8 address, u8 *local_buffer, + u8 len) +{ +#if IS_ENABLED(CONFIG_I2C_SLAVE) + struct smbus_backend *backend; + struct i2c_client *client; + int i, ret; + u8 tmp; + + mutex_lock(&smbus->mutex); + + list_for_each_entry(backend, &smbus->backend_entry, list) { + client = backend->client; + if (client->addr == address >> 1) { + mutex_unlock(&smbus->mutex); + ret = i2c_slave_event(client, I2C_SLAVE_WRITE_REQUESTED, + &address); + if (ret) + return ret; + + for (i = 0; i < len; i++) { + ret = i2c_slave_event(client, + I2C_SLAVE_WRITE_RECEIVED, + &local_buffer[i]); + if (ret) + return ret; + } + + return i2c_slave_event(client, I2C_SLAVE_STOP, &tmp); + } + } + + mutex_unlock(&smbus->mutex); +#endif /* CONFIG_I2C_SLAVE */ + return -ENXIO; +} + +static int read_smbus_target_buffer_page(struct i2c_adapter_group *smbus, + u8 target_buffer_page, u8 *address, + u8 *local_buffer, u8 *len) +{ + struct i3c_hub *hub = smbus->priv; + struct device *dev = i3cdev_to_dev(hub->i3cdev); + u32 status; + int ret; + + mutex_lock(&hub->page_mutex); + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, target_buffer_page); + + ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_LENGTH, &status); + if (ret) + goto error; + + *len = status - 1; + if (!*len) + goto error; + + if (*len > I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE) { + dev_warn_ratelimited(dev, + "Received message too big for hub buffer\n"); + ret = -EMSGSIZE; + *len = 0; + goto error; + } + + ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_ADDRESS, &status); + if (ret) + goto error; + + *address = status; + + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TARGET_BUFF_DATA, + local_buffer, *len); + +error: + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); + mutex_unlock(&hub->page_mutex); + return ret; +} + +static int process_smbus_controller_status(struct i2c_adapter_group *smbus, + u8 reg, u32 status) +{ + struct i3c_hub *hub = smbus->priv; + int ret = 0; + + if (status & I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG) { + smbus->status = status; + ret = regmap_write(hub->regmap, reg, + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG); + complete(&smbus->completion); + } + + return ret; +} + +/** + * Controller buffer page is determined by adding the first buffer page number to port + * index multiplied by four. The two target buffer page numbers are determined the same + * way but they are offset by 2 and 3 from the controller page. + */ +static int process_smbus_target_status(struct i2c_adapter_group *smbus, u8 reg, + u32 status) +{ + struct i3c_hub *hub = smbus->priv; + struct device *dev = i3cdev_to_dev(hub->i3cdev); + u8 controller_buffer_page = + I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * smbus->tp_port; + u8 local_buffer[I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 }; + u8 target_buffer_page, address = 0, len = 0, flag; + int ret; + + if (smbus->last_processed_buf) + status &= ~smbus->last_processed_buf; + + if (status & I3C_HUB_TARGET_BUF_0_RECEIVE) { + target_buffer_page = controller_buffer_page + 2; + flag = I3C_HUB_TARGET_BUF_0_RECEIVE; + } else if (status & I3C_HUB_TARGET_BUF_1_RECEIVE) { + target_buffer_page = controller_buffer_page + 3; + flag = I3C_HUB_TARGET_BUF_1_RECEIVE; + } else { + return -EINVAL; + } + + ret = read_smbus_target_buffer_page(smbus, target_buffer_page, &address, + local_buffer, &len); + if (ret && ret != -EMSGSIZE) { + dev_dbg(dev, "Failed to read target buffer page: %d\n", ret); + return ret; + } + + smbus->last_processed_buf = flag; + + if (status & I3C_HUB_TARGET_BUF_OVRFL) + flag |= I3C_HUB_TARGET_BUF_OVRFL; + + ret = regmap_write(hub->regmap, reg, flag); + if (ret) { + dev_dbg(dev, "Failed to clear target port status\n"); + return ret; + } + + if (len) { + ret = send_smbus_target_data_to_backend(smbus, address, + local_buffer, len); + if (ret) { + dev_dbg(dev, "Failed to send data to backend: %d\n", + ret); + return ret; + } + } + + return 0; +} + +static int i3c_hub_process_smbus_status(struct i2c_adapter_group *smbus) +{ + struct i3c_hub *hub = smbus->priv; + u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + smbus->tp_port; + struct device *dev = i3cdev_to_dev(hub->i3cdev); + u32 status; + int ret = 0; + u32 poll_interval_ms = + hub->settings.tp[smbus->tp_port].poll_interval_ms; + + ret = regmap_read(hub->regmap, target_port_status, &status); + if (ret) + return ret; + + /* smbus controller agent status */ + if (!poll_interval_ms) { + ret = process_smbus_controller_status(smbus, target_port_status, + status); + if (ret) + dev_warn_ratelimited(dev, + "Failed to process smbus controller status\n"); + } + + /* smbus target agent status */ + status &= I3C_HUB_TARGET_BUF_STATUS_MASK; + + while (status) { + ret = process_smbus_target_status(smbus, target_port_status, + status); + if (ret) { + dev_warn_ratelimited(dev, + "Failed to process smbus target status: %d\n", + ret); + break; + } + + if (!poll_interval_ms) + break; + + ret = regmap_read(hub->regmap, target_port_status, &status); + if (ret) + break; + status &= I3C_HUB_TARGET_BUF_STATUS_MASK; + } + + return ret; +} + +/** + * i3c_hub_delayed_work_polling() - This delayed work is a polling mechanism to + * find if any transaction happened. + */ +static void i3c_hub_delayed_work_polling(struct work_struct *work) +{ + struct i2c_adapter_group *smbus = + container_of(work, typeof(*smbus), delayed_work_polling.work); + struct device *dev = i3cdev_to_dev(smbus->priv->i3cdev); + int ret; + + if (!list_empty(&smbus->backend_entry)) { + ret = i3c_hub_process_smbus_status(smbus); + if (ret) + dev_warn_ratelimited(dev, + "Failed to process TP %u smbus status: %d\n", + smbus->tp_port, ret); + } + + schedule_delayed_work(&smbus->delayed_work_polling, + msecs_to_jiffies( + smbus->priv->settings.tp[smbus->tp_port].poll_interval_ms)); +} + +static int i3c_hub_smbus_ibi_handler(struct i3c_hub *hub, + const struct i3c_ibi_payload *payload) +{ + struct i2c_adapter_group *smbus; + u8 tp, tps; + int val, ret = 0, rc; + struct device *dev = i3cdev_to_dev(hub->i3cdev); + + if (payload->len < 2) { + ret = regmap_read(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_IBI_STS, + &val); + if (ret) + return ret; + + tps = (u8)val; + } else { + tps = ((const u8 *)payload->data)[1]; + } + + if (!tps) + return 0; + + while (tps) { + tp = (u8)__ffs((unsigned long)tps); + tps &= (tps - 1); + + if (hub->settings.tp[tp].poll_interval_ms) + continue; + + smbus = &hub->smbus_port_adapter[tp]; + rc = i3c_hub_process_smbus_status(smbus); + if (rc) { + dev_warn_ratelimited(dev, + "Failed to process TP %u smbus status: %d\n", + tp, rc); + } + } + + return 0; +} + +/* + * Sysfs attribute: clock_frequency + * Read/Write the SMBus clock frequency for this adapter's port. + */ +static ssize_t clock_frequency_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct i2c_adapter *adap = to_i2c_adapter(dev); + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap); + struct i3c_hub *hub = smbus->priv; + + return sprintf(buf, "%u\n", + hub->settings.tp[smbus->tp_port].clock_frequency); +} + +static ssize_t clock_frequency_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct i2c_adapter *adap = to_i2c_adapter(dev); + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap); + struct i3c_hub *hub = smbus->priv; + u32 val; + int ret; + + ret = kstrtou32(buf, 0, &val); + if (ret) + return ret; + + if (!i3c_hub_smbus_validate_clock_frequency(val)) + return -EINVAL; + + hub->settings.tp[smbus->tp_port].clock_frequency = val; + + return count; +} +static DEVICE_ATTR_RW(clock_frequency); + +static struct attribute *i3c_hub_smbus_attrs[] = { + &dev_attr_clock_frequency.attr, + NULL, +}; + +ATTRIBUTE_GROUPS(i3c_hub_smbus); + +static int i3c_hub_smbus_tp_algo(struct i3c_hub *priv, int i) +{ + struct device *dev = i3cdev_to_dev(priv->i3cdev); + int ret; + struct i2c_adapter_group *smbus = &priv->smbus_port_adapter[i]; + struct i2c_adapter *i2c = &smbus->i2c; + + mutex_init(&smbus->mutex); + INIT_LIST_HEAD(&smbus->backend_entry); + smbus->priv = priv; + smbus->tp_port = i; + smbus->tp_mask = BIT(i); + + init_completion(&smbus->completion); + i2c->owner = THIS_MODULE; + i2c->algo = &i3c_hub_smbus_algo; + i2c->dev.parent = dev; + i2c->dev.of_node = smbus->of_node; + i2c->dev.groups = i3c_hub_smbus_groups; + i2c->timeout = HZ; + i2c->retries = 3; + snprintf(i2c->name, sizeof(i2c->name), "hub%s.port%d", dev_name(dev), + smbus->tp_port); + + if (priv->settings.tp[i].poll_interval_ms) { + ret = regmap_clear_bits(priv->regmap, I3C_HUB_TP_IBI_CONF, + smbus->tp_mask); + if (ret) + return ret; + INIT_DELAYED_WORK(&smbus->delayed_work_polling, + i3c_hub_delayed_work_polling); + priv->smbus_ibi_en_mask &= ~smbus->tp_mask; + } else { + ret = regmap_set_bits(priv->regmap, I3C_HUB_TP_IBI_CONF, + smbus->tp_mask); + if (ret) + return ret; + priv->smbus_ibi_en_mask |= smbus->tp_mask; + } + + /* Enable SDA hold-low when both SMBus Target Agent buffers are full. + * Used as a flow-control mechanism for MCTP to avoid upstream TX timeout + * when target buffers are not serviced in time. + */ + ret = regmap_set_bits(priv->regmap, + I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF, + TARGET_AGENT_BUF_FULL_SDA_LOW_EN); + if (ret) + return ret; + + i2c_set_adapdata(i2c, smbus); + + ret = i2c_add_adapter(i2c); + if (ret) + return ret; + + smbus->used = 1; + return ret; +} + +static int i3c_hub_gpio_direction_input(struct gpio_chip *gc, unsigned int off) +{ + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + int ret = 0; + u8 reg, mask = 0; + + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN : + I3C_HUB_TP_SCL_OUT_EN; + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]); + + ret = regmap_update_bits(hub->regmap, reg, mask, 0); + return ret; +} + +static int i3c_hub_gpio_direction_output(struct gpio_chip *gc, unsigned int off, + int val) +{ + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + int ret = 0; + u8 reg, mask = 0; + + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN : + I3C_HUB_TP_SCL_OUT_EN; + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]); + + ret = regmap_update_bits(hub->regmap, reg, mask, mask); + if (ret) + return ret; + + ret = regmap_update_bits(hub->regmap, reg + 2, mask, val ? mask : 0); + return ret; +} + +static int i3c_hub_gpio_get_value(struct gpio_chip *gc, unsigned int off) +{ + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + int ret = 0, val = 0, dir; + u8 reg, shift = 0; + + dir = gc->get_direction(gc, off); + if (dir) + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_LEVEL_STS : + I3C_HUB_TP_SCL_IN_LEVEL_STS; + else + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL : + I3C_HUB_TP_SCL_OUT_LEVEL; + + shift = gpio->tp[off / GPIO_BANK_SZ]; + + ret = regmap_read(hub->regmap, reg, &val); + if (ret) + return ret; + + ret = (val >> shift) & 0x01; + return ret; +} + +static int i3c_hub_gpio_set_value(struct gpio_chip *gc, unsigned int off, int val) +{ + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + u8 reg, mask = 0; + + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL : + I3C_HUB_TP_SCL_OUT_LEVEL; + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]); + + return regmap_update_bits(hub->regmap, reg, mask, val ? mask : 0); +} + +static int i3c_hub_gpio_get_direction(struct gpio_chip *gc, unsigned int off) +{ + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + int ret = 0, dir = 0; + u8 reg, shift = 0; + + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN : + I3C_HUB_TP_SCL_OUT_EN; + shift = gpio->tp[off / GPIO_BANK_SZ]; + + ret = regmap_read(hub->regmap, reg, &dir); + if (ret) + return ret; + + ret = ~(dir >> shift) & 0x01; + return ret; +} + +static void i3c_hub_gpio_irq_mask(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + u8 reg, hwirq = 0, mask = 0; + + hwirq = irqd_to_hwirq(d); + + reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN : + I3C_HUB_TP_SCL_IN_DETECT_IBI_EN; + mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]); + + regmap_update_bits(hub->regmap, reg, mask, 0); +} + +static void i3c_hub_gpio_irq_unmask(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + u8 reg, hwirq = 0, mask = 0; + + hwirq = irqd_to_hwirq(d); + + reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN : + I3C_HUB_TP_SCL_IN_DETECT_IBI_EN; + mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]); + + regmap_update_bits(hub->regmap, reg, mask, mask); +} + +static int i3c_hub_gpio_irq_set_type(struct irq_data *d, unsigned int flow_type) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + u8 hwirq = 0, mask = 0, val, tp, reg; + int ret; + + if (!(flow_type & IRQ_TYPE_EDGE_BOTH)) { + dev_err(&hub->i3cdev->dev, "irq %d: unsupported type %d\n", + d->irq, flow_type); + return -EINVAL; + } + + hwirq = irqd_to_hwirq(d); + tp = gpio->tp[hwirq / GPIO_BANK_SZ]; + + if (tp == 0 || tp == 1 || tp == 4 || tp == 5) { + if (hwirq % GPIO_BANK_SZ) { + mask = SDA0145_IO_IN_DET_CFG_MASK; + val = SDA0145_IO_IN_DET_CFG(flow_type); + reg = I3C_HUB_TP_SDA_IN_DETECT_FLG; + } else { + mask = SCL0145_IO_IN_DET_CFG_MASK; + val = SCL0145_IO_IN_DET_CFG(flow_type); + reg = I3C_HUB_TP_SCL_IN_DETECT_FLG; + } + } else { + if (hwirq % GPIO_BANK_SZ) { + mask = SDA2367_IO_IN_DET_CFG_MASK; + val = SDA2367_IO_IN_DET_CFG(flow_type); + reg = I3C_HUB_TP_SDA_IN_DETECT_FLG; + } else { + mask = SCL2367_IO_IN_DET_CFG_MASK; + val = SCL2367_IO_IN_DET_CFG(flow_type); + reg = I3C_HUB_TP_SCL_IN_DETECT_FLG; + } + } + + ret = regmap_write(hub->regmap, reg, BIT(tp)); + if (ret) + return ret; + + ret = regmap_update_bits(hub->regmap, I3C_HUB_TP_IN_DETECT_MODE_CONF, + mask, val); + return ret; +} + +static void i3c_hub_gpio_irq_bus_lock(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + + mutex_lock(&gpio->irq_mutex); +} + +static void i3c_hub_gpio_irq_bus_unlock(struct irq_data *d) +{ + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); + struct i3c_hub *hub = gpiochip_get_data(gc); + struct hub_gpio *gpio = &hub->gpio; + + mutex_unlock(&gpio->irq_mutex); +} + +static void i3c_hub_setup_gpio(struct i3c_hub *hub) +{ + struct hub_gpio *gpio = &hub->gpio; + struct gpio_chip *gc = &gpio->chip; + struct gpio_irq_chip *girq; + + gc->direction_input = i3c_hub_gpio_direction_input; + gc->direction_output = i3c_hub_gpio_direction_output; + gc->get = i3c_hub_gpio_get_value; + gc->set = i3c_hub_gpio_set_value; + gc->get_direction = i3c_hub_gpio_get_direction; + gc->can_sleep = true; + + gc->base = -1; + gc->ngpio = gpio->nums; + gc->label = dev_name(&hub->i3cdev->dev); + gc->parent = &hub->i3cdev->dev; + gc->owner = THIS_MODULE; + + /* irq_chip support */ + mutex_init(&gpio->irq_mutex); + + gpio->irq_chip.name = dev_name(&hub->i3cdev->dev); + gpio->irq_chip.irq_mask = i3c_hub_gpio_irq_mask; + gpio->irq_chip.irq_unmask = i3c_hub_gpio_irq_unmask; + gpio->irq_chip.irq_set_type = i3c_hub_gpio_irq_set_type; + gpio->irq_chip.irq_bus_lock = i3c_hub_gpio_irq_bus_lock; + gpio->irq_chip.irq_bus_sync_unlock = i3c_hub_gpio_irq_bus_unlock; + + girq = &gpio->chip.irq; + + /* This will let us handle the parent IRQ in the driver */ + girq->parent_handler = NULL; + girq->num_parents = 0; + girq->parents = NULL; + girq->default_type = IRQ_TYPE_NONE; + girq->handler = handle_simple_irq; + girq->threaded = true; + girq->first = 0; + + girq->chip = &gpio->irq_chip; +} + +static void i3c_hub_io_ibi_handler(struct i3c_hub *hub, + const struct i3c_ibi_payload *payload) +{ + struct hub_gpio *gpio = &hub->gpio; + struct gpio_chip *gc = &gpio->chip; + u8 level, hwirq, tmp; + u8 pending[GPIO_BANK_SZ]; + u8 tp[GPIO_BANK_SZ]; + int i, irq, ret, index; + + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_OUT_EN, tp, + GPIO_BANK_SZ); + if (ret) { + dev_err(&hub->i3cdev->dev, "Failed to read OUT_EN: %d\n", ret); + return; + } + + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_IN_DETECT_FLG, + pending, GPIO_BANK_SZ); + if (ret) { + dev_err(&hub->i3cdev->dev, "Failed to read DETECT_FLG: %d\n", + ret); + return; + } + + for (i = 0; i < GPIO_BANK_SZ; i++) { + tmp = ~tp[i] & pending[i]; + + while (tmp) { + level = __ffs(tmp); + tmp &= ~(1 << level); + + /* Check if this port is in GPIO mode */ + index = gpio->port_to_index[level]; + if (index < 0) { + /* Non-GPIO mode port, skip without clearing. + * This can happen because IN_DETECT IBI enable is + * configured in groups (e.g., TP0145/TP2367), not + * per individual port. Simply skip - the flag is + * harmless and will be overwritten by next detection. + */ + dev_dbg(&hub->i3cdev->dev, + "IBI detect flag on non-GPIO port %d, skipping\n", + level); + continue; + } + + hwirq = index * 2 + i; + irq = irq_find_mapping(gc->irq.domain, hwirq); + + /* Clear the flag after processing */ + regmap_write(hub->regmap, + I3C_HUB_TP_SCL_IN_DETECT_FLG + i, + BIT(level)); + + if (unlikely(irq <= 0)) { + dev_warn_ratelimited(gc->parent, + "unmapped interrupt %d\n", + hwirq); + } else { + handle_nested_irq(irq); + } + } + } +} + +static void i3c_hub_ibi_handler(struct i3c_device *dev, + const struct i3c_ibi_payload *payload) +{ + struct i3c_hub *priv = i3cdev_get_drvdata(dev); + int ret, val = 0; + u8 status = 0; + + if (!payload->len) { + dev_dbg(&dev->dev, + "Zero-length IBI payload, reading status register\n"); + ret = regmap_read(priv->regmap, I3C_HUB_DEV_AND_IBI_STS, &val); + if (ret) { + dev_warn_ratelimited(&dev->dev, + "Failed to read IBI status: %d\n", + ret); + return; + } + status = (u8)val; + } else { + if (!payload->data) { + dev_warn_ratelimited(&dev->dev, + "IBI payload data is NULL with len=%d\n", + payload->len); + return; + } + status = ((const u8 *)payload->data)[0]; + } + + if (status & TP_IO_FLAG_STATUS) + i3c_hub_io_ibi_handler(priv, payload); + + if (status & SMBUS_AGENT_EVENT_FLAG_STATUS) { + ret = i3c_hub_smbus_ibi_handler(priv, payload); + if (ret) { + dev_warn_ratelimited(&dev->dev, + "Failed to handle SMBus IBI: %d\n", + ret); + return; + } + } +} + +static inline void i3c_hub_regmap_lock(void *ctx) +{ + mutex_lock(&i3c_hub_regmap_mutex); +} + +static inline void i3c_hub_regmap_unlock(void *ctx) +{ + mutex_unlock(&i3c_hub_regmap_mutex); +} + +static int i3c_hub_probe(struct i3c_device *i3cdev) +{ + const struct regmap_config i3c_hub_regmap_config = { + .reg_bits = 8, + .val_bits = 8, + .lock = i3c_hub_regmap_lock, + .unlock = i3c_hub_regmap_unlock, + .lock_arg = NULL, + }; + struct device *dev = &i3cdev->dev; + struct device_node *node = NULL; + struct regmap *regmap; + struct i3c_hub *priv; + char hub_id[32]; + int ret; + int i; + struct i3c_ibi_setup ibireq = {}; + + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); + if (!priv) + return -ENOMEM; + + priv->i3cdev = i3cdev; + priv->driving_master = i3c_dev_get_master(i3cdev->desc); + mutex_init(&priv->page_mutex); + i3cdev_set_drvdata(i3cdev, priv); + INIT_DELAYED_WORK(&priv->delayed_work, i3c_hub_delayed_work); + i3c_hub_of_default_configuration(dev); + + regmap = devm_regmap_init_i3c(i3cdev, &i3c_hub_regmap_config); + if (IS_ERR(regmap)) { + ret = PTR_ERR(regmap); + dev_err(dev, "Failed to register I3C HUB regmap\n"); + return ret; + } + priv->regmap = regmap; + + priv->dev_info = i3c_hub_lookup_dev_info(priv); + if (IS_ERR(priv->dev_info)) { + ret = PTR_ERR(priv->dev_info); + dev_err(dev, "Failed to lookup HUB dev info\n"); + return ret; + } + + sprintf(hub_id, "i3c-hub-%d-%llx", + i3c_dev_get_master(i3cdev->desc)->bus.id, + i3cdev->desc->info.pid); + ret = i3c_hub_debugfs_init(priv, hub_id); + if (ret) + dev_dbg(dev, "Failed to initialized DebugFS.\n"); + + ret = i3c_hub_read_id(dev); + if (ret) + goto error; + + priv->hub_dt_sel_id = -1; + priv->hub_dt_cp1_id = -1; + if (priv->hub_pin_cp1_id >= 0 && priv->hub_pin_sel_id >= 0) + /* Find hub node in DT matching HW ID or just first without ID provided in DT */ + node = i3c_hub_get_dt_hub_node(dev->parent->of_node, priv); + + if (!node) { + dev_info(dev, + "No DT entry - running with hardware defaults.\n"); + } else { + of_node_get(node); + i3c_hub_of_get_conf_static(dev, node); + i3c_hub_of_get_conf_runtime(dev, node); + of_node_put(node); + } + + /* Unlock access to protected registers */ + ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE, + REGISTERS_UNLOCK_CODE); + if (ret) { + dev_err(dev, "Failed to unlock HUB's protected registers\n"); + goto error; + } + + /* Register logic for native smbus ports */ + for (i = 0; i < priv->dev_info->n_ports; i++) { + priv->smbus_port_adapter[i].used = 0; + if (priv->settings.tp[i].mode == I3C_HUB_DT_TP_MODE_SMBUS) { + ret = i3c_hub_smbus_tp_algo(priv, i); + if (ret) + dev_warn(dev, + "Failed to setup SMBus adapter, port: %d\n", + i); + } + } + + ret = i3c_hub_configure_hw(dev); + if (ret) { + dev_err(dev, "Failed to configure the HUB\n"); + goto error; + } + + /* Lock access to protected registers */ + ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE, + REGISTERS_LOCK_CODE); + if (ret) { + dev_err(dev, "Failed to lock HUB's protected registers\n"); + goto error; + } + + /* IBI */ + ibireq.handler = i3c_hub_ibi_handler; + ibireq.max_payload_len = IBI_MAX_PAYLOAD_LEN; + ibireq.num_slots = IBI_SLOT_NUMS; + + ret = i3c_device_request_ibi(i3cdev, &ibireq); + if (ret) { + dev_err(dev, "Failed to requeset ibi!\n"); + goto error; + } + + ret = i3c_device_enable_ibi(i3cdev); + if (ret) { + dev_err(dev, "Failed to enable ibi!\n"); + goto err_free_ibi; + } + + /* TBD: Apply special/security lock here using DEV_CMD register */ + + if (priv->gpio.nums > 0) { + i3c_hub_setup_gpio(priv); + + ret = devm_gpiochip_add_data(dev, &priv->gpio.chip, priv); + if (ret) { + dev_err(dev, "gpiochip add data fail!\n"); + goto err_dis_ibi; + } + } + + schedule_delayed_work(&priv->delayed_work, msecs_to_jiffies(100)); + + return 0; + +err_dis_ibi: + i3c_device_disable_ibi(i3cdev); +err_free_ibi: + i3c_device_free_ibi(i3cdev); +error: + debugfs_remove_recursive(priv->debug_dir); + return ret; +} + +static void i3c_hub_remove(struct i3c_device *i3cdev) +{ + struct i3c_hub *priv = i3cdev_get_drvdata(i3cdev); + int i; + + i3c_device_disable_ibi(i3cdev); + i3c_device_free_ibi(i3cdev); + + for (i = 0; i < priv->dev_info->n_ports; i++) { + if (priv->smbus_port_adapter[i].used) { + cancel_delayed_work_sync(&priv->smbus_port_adapter[i].delayed_work_polling); + i2c_del_adapter(&priv->smbus_port_adapter[i].i2c); + } + + if (priv->logical_bus[i].registered) + i3c_master_unregister(&priv->logical_bus[i].controller); + } + + cancel_delayed_work_sync(&priv->delayed_work); + debugfs_remove_recursive(priv->debug_dir); +} + +static struct i3c_driver i3c_hub = { + .driver.name = "rts490xa-i3c-hub", + .id_table = i3c_hub_ids, + .probe = i3c_hub_probe, + .remove = i3c_hub_remove, +}; + +module_i3c_driver(i3c_hub); + +MODULE_DESCRIPTION("RTS490XA I3C HUB driver"); +MODULE_LICENSE("GPL"); -- 2.34.1 From zain_zhou at realsil.com.cn Mon May 25 05:54:52 2026 From: zain_zhou at realsil.com.cn (=?utf-8?B?5ZGo5a+F?=) Date: Mon, 25 May 2026 12:54:52 +0000 Subject: =?utf-8?B?562U5aSNOiBbUEFUQ0ggMS8yXSBkdC1iaW5kaW5nczogaTNjOiBhZGQgYmluZGluZyBmb3IgUmVhbHRlayBSVFM0OTB4IEkzQyBIVUI=?= In-Reply-To: References: <20260430121354.6253-1-zain_zhou@realsil.com.cn> Message-ID: <9ce3eeed6ec344b88cc7f488493a78ff@realsil.com.cn> Thank you for the review. All issues have been addressed in v2. Best Regards? Zain Zhou ?? Realsil Microelectronics CO. LTD. E-mail? : zain_zhou at realsil.com.cn From zain_zhou at realsil.com.cn Mon May 25 05:58:33 2026 From: zain_zhou at realsil.com.cn (=?utf-8?B?5ZGo5a+F?=) Date: Mon, 25 May 2026 12:58:33 +0000 Subject: =?utf-8?B?562U5aSNOiBbUEFUQ0ggMi8yXSBzdGFnaW5nOiBpM2M6IGFkZCBSZWFsdGVrIFJUUzQ5MHggSTNDIEhVQiBkcml2ZXI=?= In-Reply-To: <2026050412-bush-rosy-959d@gregkh> References: <20260430121354.6253-1-zain_zhou@realsil.com.cn> <20260430121354.6253-2-zain_zhou@realsil.com.cn> <2026050412-bush-rosy-959d@gregkh> Message-ID: <1a79b9c3c6c64e5ba65292efa2f0472f@realsil.com.cn> Thank you for the review. We noticed that the NXP P3H2x4x series (v10) introduces a generic I3C hub framework. Would you recommend waiting for that to land before resubmitting to the proper location? Best Regards? Zain Zhou ?? Realsil Microelectronics CO. LTD. E-mail? : zain_zhou at realsil.com.cn From zain_zhou at realsil.com.cn Mon May 25 06:07:39 2026 From: zain_zhou at realsil.com.cn (=?utf-8?B?5ZGo5a+F?=) Date: Mon, 25 May 2026 13:07:39 +0000 Subject: =?utf-8?B?562U5aSNOiBbUEFUQ0ggMS8yXSBkdC1iaW5kaW5nczogaTNjOiBhZGQgYmluZGluZyBmb3IgUmVhbHRlayBSVFM0OTB4IEkzQyBIVUI=?= In-Reply-To: References: <20260430121354.6253-1-zain_zhou@realsil.com.cn> Message-ID: <871742681d2047c794f0a46c03620dcb@realsil.com.cn> Hi Frank, Thanks for the update ? great to hear the NXP i3c hub framework is nearly done! We plan to rebase on the generic framework once the P3H2x4x series is merged, and move the driver out of staging at that point. Looking forward to it landing upstream. Best Regards? Zain Zhou ?? Realsil Microelectronics CO. LTD. E-mail? : zain_zhou at realsil.com.cn From alexandre.belloni at bootlin.com Mon May 25 06:25:34 2026 From: alexandre.belloni at bootlin.com (Alexandre Belloni) Date: Mon, 25 May 2026 15:25:34 +0200 Subject: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver In-Reply-To: <20260525125128.297-2-zain_zhou@realsil.com.cn> References: <20260525125128.297-1-zain_zhou@realsil.com.cn> <20260525125128.297-2-zain_zhou@realsil.com.cn> Message-ID: <2026052513253446d58746@mail.local> Hello, On 25/05/2026 20:51:28+0800, zain_zhou at realsil.com.cn wrote: > From: Yin Zhou > > Add driver for Realtek RTS490x series I3C HUB devices. > > The driver supports: > - Device Tree based configuration of LDO, pull-up, IO strength > and per-port mode (I3C/SMBus/GPIO/disabled) > - Logical I3C bus registration per target port > - SMBus agent functionality with IBI and polling modes > - GPIO chip with IRQ support > - DebugFS interface for register access and DT config inspection > - IBI (In-Band Interrupt) handling > > The driver is placed in staging as it has known issues to be resolved > before mainlining; see drivers/staging/rts490x/TODO for details. > > Signed-off-by: Yin Zhou > > Changes in v2: > - Update driver to match v2 DT binding: parse physical values > directly via of_property_read_bool/u32; drop string enum lookup > tables; update property names with realtek, prefix and unit suffixes > - Fix maintainer name; move MAINTAINERS entry to driver patch > - Code style: rename TPn_* macros to TPN_*; rename SMBus frequency > constants to UPPER_CASE; add mutex field comments > --- > MAINTAINERS | 6 + > drivers/staging/Kconfig | 2 + > drivers/staging/Makefile | 1 + > drivers/staging/rts490x/Kconfig | 16 + > drivers/staging/rts490x/Makefile | 2 + > drivers/staging/rts490x/TODO | 35 + > drivers/staging/rts490x/rts490xa-i3c-hub.c | 3039 ++++++++++++++++++++ As stated in the previous review, this is never going to enter staging, this has to be submitted to the i3c subsystem once all the TODOs have been taken care of. > 7 files changed, 3101 insertions(+) > create mode 100644 drivers/staging/rts490x/Kconfig > create mode 100644 drivers/staging/rts490x/Makefile > create mode 100644 drivers/staging/rts490x/TODO > create mode 100644 drivers/staging/rts490x/rts490xa-i3c-hub.c > > diff --git a/MAINTAINERS b/MAINTAINERS > index 2fb1c75afd16..6d0f8cde935d 100644 > --- a/MAINTAINERS > +++ b/MAINTAINERS > @@ -12214,6 +12214,12 @@ S: Supported > F: Documentation/devicetree/bindings/i3c/renesas,i3c.yaml > F: drivers/i3c/master/renesas-i3c.c > > +I3C HUB DRIVER FOR REALTEK RTS490X > +M: Yin Zhou > +S: Maintained > +F: Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > +F: drivers/staging/rts490x/ > + > I3C DRIVER FOR SYNOPSYS DESIGNWARE > S: Orphan > F: Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig > index 2f92cd698bef..f14869cf5af5 100644 > --- a/drivers/staging/Kconfig > +++ b/drivers/staging/Kconfig > @@ -48,4 +48,6 @@ source "drivers/staging/axis-fifo/Kconfig" > > source "drivers/staging/vme_user/Kconfig" > > +source "drivers/staging/rts490x/Kconfig" > + > endif # STAGING > diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile > index f5b8876aa536..59044d547bf7 100644 > --- a/drivers/staging/Makefile > +++ b/drivers/staging/Makefile > @@ -13,3 +13,4 @@ obj-$(CONFIG_MOST) += most/ > obj-$(CONFIG_GREYBUS) += greybus/ > obj-$(CONFIG_BCM2835_VCHIQ) += vc04_services/ > obj-$(CONFIG_XIL_AXIS_FIFO) += axis-fifo/ > +obj-$(CONFIG_RTS490X_I3C_HUB) += rts490x/ > diff --git a/drivers/staging/rts490x/Kconfig b/drivers/staging/rts490x/Kconfig > new file mode 100644 > index 000000000000..282865d1fed9 > --- /dev/null > +++ b/drivers/staging/rts490x/Kconfig > @@ -0,0 +1,16 @@ > +# SPDX-License-Identifier: GPL-2.0-only > +config RTS490X_I3C_HUB > + tristate "Realtek RTS490x I3C HUB driver" > + depends on I3C > + depends on REGMAP_I3C > + select GPIOLIB > + help > + Support for Realtek RTS490x series I3C HUB devices (RTS4900, > + RTS4901, RTS4902, RTS4903, RTS4904, RTS4906). > + > + The I3C HUB provides port expansion, voltage level translation, > + bus capacitance isolation, address conflict isolation, SMBus > + agent functionality and GPIO expansion. > + > + This driver can also be built as a module. If so, the module > + will be called rts490xa-i3c-hub. > diff --git a/drivers/staging/rts490x/Makefile b/drivers/staging/rts490x/Makefile > new file mode 100644 > index 000000000000..4b1d7640fb82 > --- /dev/null > +++ b/drivers/staging/rts490x/Makefile > @@ -0,0 +1,2 @@ > +# SPDX-License-Identifier: GPL-2.0-only > +obj-$(CONFIG_RTS490X_I3C_HUB) += rts490xa-i3c-hub.o > diff --git a/drivers/staging/rts490x/TODO b/drivers/staging/rts490x/TODO > new file mode 100644 > index 000000000000..0f46620af4c6 > --- /dev/null > +++ b/drivers/staging/rts490x/TODO > @@ -0,0 +1,35 @@ > +TODO list for rts490xa-i3c-hub staging driver > +============================================== > + > +Completed in v2 > +--------------- > +- [x] Add proper DT binding schema validation (dt-schema) > + Addressed in v2 dt-bindings patch: all properties carry vendor > + prefix, use standard types (boolean/u32 with unit suffixes), > + unevaluatedProperties: false, validated via dt-schema. > + > +Remaining before moving out of staging > +--------------------------------------- > +- Clean up open-coded OF property parsing; use device_property_* APIs > + instead of of_property_read_* where possible > +- Remove use of full_name / sscanf for node name parsing; use > + of_node_name_eq() and fwnode helpers instead > +- Replace global mutex (i3c_hub_regmap_mutex) with per-device locking > +- Add kernel-doc comments for all exported/public functions > +- Resolve TODO comment in i3c_hub_hw_configure_tp() regarding MUX > + connection verification > +- Remove TBD comment in i3c_hub_probe() regarding DEV_CMD security lock > +- Review and fix potential locking issues in i3c_hub_delayed_work() > + when registering logical buses > +- Fix error handling in i3c_hub_delayed_work(): early return on failure > + does not unregister already-registered logical buses, causing resource > + leak; needs proper cleanup on error path > + > +Rebase on upstream i3c hub framework (pending) > +----------------------------------------------- > +A generic i3c hub framework is being introduced upstream by the NXP > +P3H2x4x patch series (v10, under review): > + https://lore.kernel.org/linux-i3c/20260525064209.2263045-1-lakshay.piplani at nxp.com/ > + > +Once that framework is merged, rebase this driver on it and move > +out of staging. > diff --git a/drivers/staging/rts490x/rts490xa-i3c-hub.c b/drivers/staging/rts490x/rts490xa-i3c-hub.c > new file mode 100644 > index 000000000000..fdfff5c6dff5 > --- /dev/null > +++ b/drivers/staging/rts490x/rts490xa-i3c-hub.c > @@ -0,0 +1,3039 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* Copyright (C) 2021 - 2023 Intel Corporation. */ > +/* Copyright (c) 2025 Realtek Semiconductor Corp. */ > + > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > +#include > + > +#include > +#include > + > +#define I3C_HUB_TP_MAX_COUNT 0x08 > + > +#define I3C_DCR_HUB 0xC2 > + > +#define GPIO_BANK_SZ 0x02 > +#define GPIO_MAX_BANK I3C_HUB_TP_MAX_COUNT > + > +/* I3C HUB REGISTERS */ > + > +/* > + * In this driver Controller - Target convention is used. All the abbreviations are > + * based on this convention. For instance: CP - Controller Port, TP - Target Port. > + */ > + > +/* Device Information Registers */ > +#define I3C_HUB_DEV_INFO_0 0x00 > +#define I3C_HUB_DEV_INFO_1 0x01 > +#define I3C_HUB_PID_5 0x02 > +#define I3C_HUB_PID_4 0x03 > +#define I3C_HUB_PID_3 0x04 > +#define I3C_HUB_PID_2 0x05 > +#define I3C_HUB_PID_1 0x06 > +#define I3C_HUB_PID_0 0x07 > +#define I3C_HUB_BCR 0x08 > +#define I3C_HUB_DCR 0x09 > +#define I3C_HUB_DEV_CAPAB 0x0A > + > +#define I3C_HUB_DEV_REV 0x0B > +#define I3C_HUB_DEV_REV_LDO_MASK GENMASK(7, 6) > +#define I3C_HUB_DEV_REV_LDO_GET(x) FIELD_GET(I3C_HUB_DEV_REV_LDO_MASK, (x)) > + > +/* Device Configuration Registers */ > +#define I3C_HUB_PROTECTION_CODE 0x10 > +#define REGISTERS_LOCK_CODE 0x00 > +#define REGISTERS_UNLOCK_CODE 0x69 > +#define CP1_REGISTERS_UNLOCK_CODE 0x6A > + > +#define I3C_HUB_CP_CONF 0x11 > +#define I3C_HUB_TP_ENABLE 0x12 > +#define TPN_ENABLE(n) BIT(n) > + > +#define I3C_HUB_DEV_CONF 0x13 > +#define I3C_HUB_IO_STRENGTH 0x14 > +#define TP0145_IO_STRENGTH_MASK GENMASK(1, 0) > +#define TP0145_IO_STRENGTH(x) (((x) << 0) & TP0145_IO_STRENGTH_MASK) > +#define TP2367_IO_STRENGTH_MASK GENMASK(3, 2) > +#define TP2367_IO_STRENGTH(x) (((x) << 2) & TP2367_IO_STRENGTH_MASK) > +#define CP0_IO_STRENGTH_MASK GENMASK(5, 4) > +#define CP0_IO_STRENGTH(x) (((x) << 4) & CP0_IO_STRENGTH_MASK) > +#define CP1_IO_STRENGTH_MASK GENMASK(7, 6) > +#define CP1_IO_STRENGTH(x) (((x) << 6) & CP1_IO_STRENGTH_MASK) > +#define IO_STRENGTH_20_OHM 0x00 > +#define IO_STRENGTH_30_OHM 0x01 > +#define IO_STRENGTH_40_OHM 0x02 > +#define IO_STRENGTH_50_OHM 0x03 > + > +#define I3C_HUB_NET_OPER_MODE_CONF 0x15 > +#define I3C_HUB_NET_ALWAYS_I3C_EN BIT(5) > + > +#define I3C_HUB_LDO_CONF 0x16 > +#define CP0_LDO_VOLTAGE_MASK GENMASK(1, 0) > +#define CP0_LDO_VOLTAGE(x) (((x) << 0) & CP0_LDO_VOLTAGE_MASK) > +#define CP1_LDO_VOLTAGE_MASK GENMASK(3, 2) > +#define CP1_LDO_VOLTAGE(x) (((x) << 2) & CP1_LDO_VOLTAGE_MASK) > +#define TP0145_LDO_VOLTAGE_MASK GENMASK(5, 4) > +#define TP0145_LDO_VOLTAGE(x) (((x) << 4) & TP0145_LDO_VOLTAGE_MASK) > +#define TP2367_LDO_VOLTAGE_MASK GENMASK(7, 6) > +#define TP2367_LDO_VOLTAGE(x) (((x) << 6) & TP2367_LDO_VOLTAGE_MASK) > +#define LDO_VOLTAGE_1_0V 0x00 > +#define LDO_VOLTAGE_1_1V 0x01 > +#define LDO_VOLTAGE_1_2V 0x02 > +#define LDO_VOLTAGE_1_8V 0x03 > + > +#define I3C_HUB_TP_IO_MODE_CONF 0x17 > +#define TPN_IO_MODE_CON(n) BIT(n) > +#define I3C_HUB_TP_SMBUS_AGNT_EN 0x18 > +#define TPN_SMBUS_MODE_EN(n) BIT(n) > + > +#define I3C_HUB_LDO_AND_PULLUP_CONF 0x19 > +#define LDO_ENABLE_DISABLE_MASK GENMASK(3, 0) > +#define CP0_LDO_EN BIT(0) > +#define CP1_LDO_EN BIT(1) > +/* > + * I3C HUB does not provide a way to control LDO or pull-up for individual ports. It is possible > + * for group of ports TP0/TP1/TP4/TP5 and TP2/TP3/TP6/TP7. > + */ > +#define TP0145_LDO_EN BIT(2) > +#define TP2367_LDO_EN BIT(3) > +#define TP0145_PULLUP_CONF_MASK GENMASK(7, 6) > +#define TP0145_PULLUP_CONF(x) (((x) << 6) & TP0145_PULLUP_CONF_MASK) > +#define TP2367_PULLUP_CONF_MASK GENMASK(5, 4) > +#define TP2367_PULLUP_CONF(x) (((x) << 4) & TP2367_PULLUP_CONF_MASK) > +#define PULLUP_250R 0x00 > +#define PULLUP_500R 0x01 > +#define PULLUP_1K 0x02 > +#define PULLUP_2K 0x03 > + > +#define I3C_HUB_CP_IBI_CONF 0x1A > +#define I3C_HUB_TP_IBI_CONF 0x1B > +#define I3C_HUB_IBI_MDB_CUSTOM 0x1C > +#define I3C_HUB_JEDEC_CONTEXT_ID 0x1D > +#define I3C_HUB_TP_GPIO_MODE_EN 0x1E > +#define TPN_GPIO_MODE_EN(n) BIT(n) > + > +/* Device Status and IBI Registers */ > +#define I3C_HUB_DEV_AND_IBI_STS 0x20 > +#define PEC_ERROR_FLAG BIT(0) > +#define PARITY_ERROR_FLAG BIT(1) > +#define CONTROLLER_MSG_PENDING_FLAG BIT(2) > +#define TP_IO_FLAG_STATUS BIT(3) > +#define SMBUS_AGENT_EVENT_FLAG_STATUS BIT(4) > + > +#define I3C_HUB_TP_SMBUS_AGNT_IBI_STS 0x21 > + > +/* Controller Port Control/Status Registers */ > +#define I3C_HUB_CP_MUX_SET 0x38 > +#define CONTROLLER_PORT_MUX_REQ BIT(0) > +#define I3C_HUB_CP_MUX_STS 0x39 > +#define CONTROLLER_PORT_MUX_CONNECTION_STATUS BIT(0) > + > +/* Target Dynamic Address Assignment Flag Registers */ > +#define I3C_HUB_TARGET_DA_FLAG_BYTE_BASE 0x40 > +#define I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT 16 > + > +/* Target Ports Control Registers */ > +#define I3C_HUB_TP_SMBUS_AGNT_TRANS_START 0x50 > +#define I3C_HUB_TP_NET_CON_CONF 0x51 > +#define TPN_NET_CON(n) BIT(n) > + > +#define I3C_HUB_TP_PULLUP_EN 0x53 > +#define TPN_PULLUP_EN(n) BIT(n) > + > +#define I3C_HUB_TP_SCL_OUT_EN 0x54 > +#define I3C_HUB_TP_SDA_OUT_EN 0x55 > +#define I3C_HUB_TP_SCL_OUT_LEVEL 0x56 > +#define I3C_HUB_TP_SDA_OUT_LEVEL 0x57 > + > +#define I3C_HUB_TP_IN_DETECT_MODE_CONF 0x58 > +#define SCL0145_IO_IN_DET_CFG_MASK GENMASK(1, 0) > +#define SCL0145_IO_IN_DET_CFG(x) (((x) << 0) & SCL0145_IO_IN_DET_CFG_MASK) > +#define SDA0145_IO_IN_DET_CFG_MASK GENMASK(3, 2) > +#define SDA0145_IO_IN_DET_CFG(x) (((x) << 2) & SDA0145_IO_IN_DET_CFG_MASK) > +#define SCL2367_IO_IN_DET_CFG_MASK GENMASK(5, 4) > +#define SCL2367_IO_IN_DET_CFG(x) (((x) << 4) & SCL2367_IO_IN_DET_CFG_MASK) > +#define SDA2367_IO_IN_DET_CFG_MASK GENMASK(7, 6) > +#define SDA2367_IO_IN_DET_CFG(x) (((x) << 6) & SDA2367_IO_IN_DET_CFG_MASK) > + > +#define I3C_HUB_TP_SCL_IN_DETECT_IBI_EN 0x59 > +#define I3C_HUB_TP_SDA_IN_DETECT_IBI_EN 0x5A > + > +/* Target Ports Status Registers */ > +#define I3C_HUB_TP_SCL_IN_LEVEL_STS 0x60 > +#define I3C_HUB_TP_SDA_IN_LEVEL_STS 0x61 > +#define I3C_HUB_TP_SCL_IN_DETECT_FLG 0x62 > +#define I3C_HUB_TP_SDA_IN_DETECT_FLG 0x63 > + > +/* SMBus Agent Configuration and Status Registers */ > +#define I3C_HUB_TP0_SMBUS_AGNT_STS 0x64 > +#define I3C_HUB_TP1_SMBUS_AGNT_STS 0x65 > +#define I3C_HUB_TP2_SMBUS_AGNT_STS 0x66 > +#define I3C_HUB_TP3_SMBUS_AGNT_STS 0x67 > +#define I3C_HUB_TP4_SMBUS_AGNT_STS 0x68 > +#define I3C_HUB_TP5_SMBUS_AGNT_STS 0x69 > +#define I3C_HUB_TP6_SMBUS_AGNT_STS 0x6A > +#define I3C_HUB_TP7_SMBUS_AGNT_STS 0x6B > + > +#define I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF 0x6C > +#define TARGET_AGENT_BUF_FULL_SDA_LOW_EN BIT(5) > + > +/* Transaction status checking mask */ > +#define I3C_HUB_CONTROLLER_AGENT_STATUS_MASK (0xF0 | BIT(0)) > +#define I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG BIT(0) > +/* SMBus Controller Agent Return Codes */ > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT 4 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS 0x0 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK 0x1 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY 0x2 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY 0x3 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED 0x4 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR 0x5 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT 0x6 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST 0x7 > +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT 0x8 > + > +#define I3C_HUB_TARGET_BUF_STATUS_MASK GENMASK(3, 1) > +#define I3C_HUB_TARGET_BUF_0_RECEIVE BIT(1) > +#define I3C_HUB_TARGET_BUF_1_RECEIVE BIT(2) > +#define I3C_HUB_TARGET_BUF_OVRFL BIT(3) > + > +/* Special Function Registers */ > +#define I3C_HUB_LDO_AND_CPSEL_STS 0x79 > +#define CP_SDA1_LEVEL BIT(7) > +#define CP_SCL1_LEVEL BIT(6) > +#define CP_SEL_PIN_INPUT_CODE_MASK GENMASK(5, 4) > +#define CP_SEL_PIN_INPUT_CODE_GET(x) (((x) & CP_SEL_PIN_INPUT_CODE_MASK) >> 4) > +#define CP_SDA1_SCL1_PINS_CODE_MASK GENMASK(7, 6) > +#define CP_SDA1_SCL1_PINS_CODE_GET(x) (((x) & CP_SDA1_SCL1_PINS_CODE_MASK) >> 6) > +#define VCCIO1_PWR_GOOD BIT(3) > +#define VCCIO0_PWR_GOOD BIT(2) > +#define CP1_VCCIO_PWR_GOOD BIT(1) > +#define CP0_VCCIO_PWR_GOOD BIT(0) > + > +#define I3C_HUB_BUS_RESET_SCL_TIMEOUT 0x7A > +#define I3C_HUB_ONCHIP_TD_PROTO_ERR_FLG 0x7B > +#define I3C_HUB_DEV_CMD 0x7C > +#define I3C_HUB_ONCHIP_TD_STS 0x7D > +#define I3C_HUB_ONCHIP_TD_ADDR_CONF 0x7E > +#define I3C_HUB_PAGE_PTR 0x7F > + > +/* Paged Transaction Registers */ > +#define I3C_HUB_CONTROLLER_BUFFER_PAGE 0x10 > +#define I3C_HUB_CONTROLLER_AGENT_BUFF 0x80 > +#define I3C_HUB_CONTROLLER_AGENT_BUFF_DATA 0x84 > +#define I3C_HUB_TARGET_BUFF_LENGTH 0x80 > +#define I3C_HUB_TARGET_BUFF_ADDRESS 0x81 > +#define I3C_HUB_TARGET_BUFF_DATA 0x82 > + > +/* TP DT setting */ > +#define I3C_HUB_DT_TP_MODE_DISABLED 0x00 > +#define I3C_HUB_DT_TP_MODE_I3C 0x01 > +#define I3C_HUB_DT_TP_MODE_SMBUS 0x02 > +#define I3C_HUB_DT_TP_MODE_GPIO 0x03 > +#define I3C_HUB_DT_TP_MODE_NOT_DEFINED 0xFF > + > +/* TP IO mode */ > +#define I3C_HUB_DT_TP_IO_MODE_OD_PP 0x00 > +#define I3C_HUB_DT_TP_IO_MODE_OD 0x01 > +#define I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED 0xFF > + > +/* SMBus transaction types fields */ > +#define I3C_HUB_SMBUS_100_KHZ 0x00 > +#define I3C_HUB_SMBUS_200_KHZ BIT(1) > +#define I3C_HUB_SMBUS_400_KHZ BIT(2) > +#define I3C_HUB_SMBUS_1000_KHZ (BIT(1) | BIT(2)) > + > +/* SMBus xfer type for i3c_hub_smbus_msg xfer_type parameter */ > +#define I3C_HUB_SMBUS_XFER_WRITE 0 > +#define I3C_HUB_SMBUS_XFER_READ 1 > +#define I3C_HUB_SMBUS_XFER_WR_RD 2 > + > +/* Hub buffer size */ > +#define I3C_HUB_CONTROLLER_BUFFER_SIZE 88 > +#define I3C_HUB_TARGET_BUFFER_SIZE 80 > +#define I3C_HUB_SMBUS_DESCRIPTOR_SIZE 4 > +#define I3C_HUB_SMBUS_PAYLOAD_SIZE \ > + (I3C_HUB_CONTROLLER_BUFFER_SIZE - I3C_HUB_SMBUS_DESCRIPTOR_SIZE) > +#define I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE (I3C_HUB_TARGET_BUFFER_SIZE - 2) > + > +/* Hub SMBus status register read interval (microseconds, ceil) */ > +#define I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL(len, clk_khz) \ > + DIV_ROUND_UP(1000U * 9U * (u32)(len), (u32)(clk_khz)) > + > +/* ID Extraction */ > +#define I3C_HUB_ID_CP_SDA_SCL 0x00 > +#define I3C_HUB_ID_CP_SEL 0x01 > + > +/* IBI */ > +#define IBI_MAX_PAYLOAD_LEN 2 > +#define IBI_SLOT_NUMS 6 > + > +#define I3C_HUB_IO_CTRL_PAGE 0x81 > +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK 0xDB > +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN BIT(6) > +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_MASK GENMASK(5, 0) > +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL 0x18 > + > +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK 0xDC > +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN BIT(4) > +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK GENMASK(3, 0) > +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL(x) \ > + ((x) & I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK) > + > +#define I3C_HUB_EFUSE_PAGE 0x7B > +#define I3C_HUB_EFUSE_OFFSET_A0 0xA0 > +#define I3C_HUB_FAST_RSON_EN BIT(5) > +#define I3C_HUB_EFUSE_OFFSET_A3 0xA3 > +#define I3C_HUB_FAST_DRV_LOOP_DIS BIT(5) > + > +#define I3C_HUB_EFUSE_OFFSET_9D 0x9D > +#define I3C_HUB_TP_OD_VOL_LEVEL BIT(0) > +#define I3C_HUB_TP_OD_VREF BIT(1) > + > +#define I3C_HUB_EFUSE_OFFSET_9E 0x9E > +#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK GENMASK(5, 4) > +#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL(x) \ > + (((x) << 4) & I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK) > +#define I3C_HUB_IBI_ACK_RD_CYCLE_MASK GENMASK(3, 0) > +#define I3C_HUB_IBI_ACK_RD_CYCLE_VAL (5) > + > +struct i3c_hub_dev_info { > + const char *model; > + u16 part_id; > + u8 n_ports; > +}; > + > +static const struct i3c_hub_dev_info i3c_hub_dev_info_unknown = { > + .model = "Unknown", > + .part_id = 0, > + .n_ports = 8, > +}; > + > +static const struct i3c_hub_dev_info i3c_hub_dev_info_table[] = { > + { "RTS4900", 0x4000, 4 }, { "RTS4901", 0x4100, 4 }, > + { "RTS4902", 0x8000, 8 }, { "RTS4903", 0x8100, 8 }, > + { "RTS4904", 0x4001, 4 }, { "RTS4906", 0x8001, 8 } > +}; > + > +struct tp_setting { > + u8 mode; > + bool pullup_en; > + u8 io_mode; > + bool always_enable; > + u32 poll_interval_ms; > + u32 clock_frequency; > +}; > + > +struct dt_settings { > + bool cp0_ldo_en; > + bool cp1_ldo_en; > + bool tp0145_ldo_en; > + bool tp2367_ldo_en; > + u32 cp0_ldo_volt; > + u32 cp1_ldo_volt; > + u32 tp0145_ldo_volt; > + u32 tp2367_ldo_volt; > + u32 tp0145_pullup; > + u32 tp2367_pullup; > + u32 cp0_io_strength; > + u32 cp1_io_strength; > + u32 tp0145_io_strength; > + u32 tp2367_io_strength; > + struct tp_setting tp[I3C_HUB_TP_MAX_COUNT]; > + bool hub_net_always_i3c; > + u8 tp_scl_h_ack_cycles; > + bool handshake_optimize; > + u8 fast_drv_h_add_cycles; > + bool fast_rson_en; > + bool tp_od_vol_optimize; > + bool tp_od_vref_optimize; > +}; > + > +struct smbus_backend { > + struct i2c_client *client; > + struct list_head list; > +}; > + > +struct i2c_adapter_group { > + struct i2c_adapter i2c; > + u8 tp_mask; > + u8 tp_port; > + u8 used; > + struct device_node *of_node; > + struct i3c_hub *priv; > + struct mutex mutex; /* protects SMBus adapter state and transfers */ > + > + struct delayed_work delayed_work_polling; > + struct list_head backend_entry; > + u8 last_processed_buf; > + > + u8 status; > + struct completion completion; > +}; > + > +struct logical_bus { > + bool available; /* Logical bus configuration is available in DT. */ > + bool registered; /* Logical bus was registered in the framework. */ > + u8 tp_id; > + u8 tp_map; > + struct i3c_master_controller controller; > + struct device_node *of_node; > + struct i3c_hub *priv; > +}; > + > +struct hub_gpio { > + struct gpio_chip chip; > + int tp[GPIO_MAX_BANK]; > + s8 port_to_index[I3C_HUB_TP_MAX_COUNT]; > + int nums; > + struct irq_chip irq_chip; > + struct mutex irq_mutex; /* protects GPIO IRQ enable/disable operations */ > +}; > + > +struct i3c_hub { > + struct i3c_device *i3cdev; > + struct i3c_master_controller *driving_master; > + struct regmap *regmap; > + const struct i3c_hub_dev_info *dev_info; > + struct dt_settings settings; > + struct delayed_work delayed_work; > + int hub_pin_sel_id; > + int hub_pin_cp1_id; > + int hub_dt_sel_id; > + int hub_dt_cp1_id; > + > + struct logical_bus logical_bus[I3C_HUB_TP_MAX_COUNT]; > + struct i2c_adapter_group smbus_port_adapter[I3C_HUB_TP_MAX_COUNT]; > + u8 smbus_ibi_en_mask; > + struct mutex page_mutex; /* protects regmap page register access */ > + > + /* Offset for reading HUB's register. */ > + u8 reg_addr; > + struct dentry *debug_dir; > + struct hub_gpio gpio; > +}; > + > +/* Global mutex for serializing regmap access across all i3c hubs. */ > +static DEFINE_MUTEX(i3c_hub_regmap_mutex); > + > +static u8 i3c_hub_ldo_dt_to_reg(u32 microvolt) > +{ > + switch (microvolt) { > + case 1100000: > + return LDO_VOLTAGE_1_1V; > + case 1200000: > + return LDO_VOLTAGE_1_2V; > + case 1800000: > + return LDO_VOLTAGE_1_8V; > + default: > + return LDO_VOLTAGE_1_0V; > + } > +} > + > +static u8 i3c_hub_pullup_dt_to_reg(u32 ohms) > +{ > + switch (ohms) { > + case 250: > + return PULLUP_250R; > + case 500: > + return PULLUP_500R; > + case 1000: > + return PULLUP_1K; > + default: > + return PULLUP_2K; > + } > +} > + > +static u8 i3c_hub_io_strength_dt_to_reg(u32 ohms) > +{ > + switch (ohms) { > + case 50: > + return IO_STRENGTH_50_OHM; > + case 40: > + return IO_STRENGTH_40_OHM; > + case 30: > + return IO_STRENGTH_30_OHM; > + default: > + return IO_STRENGTH_20_OHM; > + } > +} > + > +static bool i3c_hub_smbus_validate_clock_frequency(u32 hz) > +{ > + switch (hz) { > + case 100000: > + case 200000: > + case 400000: > + case 1000000: > + return true; > + default: > + return false; > + } > +} > + > +static inline u8 i3c_hub_smbus_rate_bits_from_hz(u32 hz) > +{ > + switch (hz) { > + case 100000: > + return I3C_HUB_SMBUS_100_KHZ; > + case 200000: > + return I3C_HUB_SMBUS_200_KHZ; > + case 1000000: > + return I3C_HUB_SMBUS_1000_KHZ; > + default: > + return I3C_HUB_SMBUS_400_KHZ; > + } > +} > + > +static void i3c_hub_tp_of_get_setting(struct device *dev, > + const struct device_node *node, > + struct tp_setting tp_setting[]) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + const char *mode_str, *io_mode_str; > + struct device_node *tp_node; > + u32 id, val; > + > + for_each_available_child_of_node(node, tp_node) { > + if (!tp_node->name || of_node_cmp(tp_node->name, "target-port")) > + continue; > + > + if (!tp_node->full_name || > + (sscanf(tp_node->full_name, "target-port@%u", &id) != 1)) { > + dev_warn(dev, > + "Invalid target port node found in DT: %s\n", > + tp_node->full_name); > + continue; > + } > + > + if (id >= priv->dev_info->n_ports) { > + dev_warn(dev, > + "Invalid target port index found in DT: %i\n", > + id); > + continue; > + } > + > + priv->smbus_port_adapter[id].of_node = tp_node; > + > + if (!of_property_read_string(tp_node, "realtek,mode", > + &mode_str)) { > + if (!strcmp(mode_str, "i3c")) > + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_I3C; > + else if (!strcmp(mode_str, "smbus")) > + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_SMBUS; > + else if (!strcmp(mode_str, "gpio")) > + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_GPIO; > + else if (!strcmp(mode_str, "disabled")) > + tp_setting[id].mode = > + I3C_HUB_DT_TP_MODE_DISABLED; > + } > + > + if (!of_property_read_string(tp_node, "realtek,io-mode", > + &io_mode_str)) { > + if (!strcmp(io_mode_str, "od")) > + tp_setting[id].io_mode = > + I3C_HUB_DT_TP_IO_MODE_OD; > + else if (!strcmp(io_mode_str, "od-pp")) > + tp_setting[id].io_mode = > + I3C_HUB_DT_TP_IO_MODE_OD_PP; > + } > + > + tp_setting[id].pullup_en = > + of_property_read_bool(tp_node, "realtek,pullup-enable"); > + tp_setting[id].always_enable = > + of_property_read_bool(tp_node, "realtek,always-enable"); > + > + if (!of_property_read_u32(tp_node, > + "realtek,polling-interval-ms", &val)) > + tp_setting[id].poll_interval_ms = val; > + > + if (!of_property_read_u32(tp_node, "clock-frequency", &val)) { > + if (i3c_hub_smbus_validate_clock_frequency(val)) > + tp_setting[id].clock_frequency = val; > + else > + dev_warn(dev, > + "Unsupported TP%d smbus clock-frequency: %u Hz, using default %u Hz\n", > + id, val, > + tp_setting[id].clock_frequency); > + } > + } > +} > + > +static void i3c_hub_of_get_conf_static(struct device *dev, > + const struct device_node *node) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + u8 u8val; > + u32 val; > + > + priv->settings.cp0_ldo_en = > + of_property_read_bool(node, "realtek,cp0-ldo-enable"); > + priv->settings.cp1_ldo_en = > + of_property_read_bool(node, "realtek,cp1-ldo-enable"); > + priv->settings.tp0145_ldo_en = > + of_property_read_bool(node, "realtek,tp0145-ldo-enable"); > + priv->settings.tp2367_ldo_en = > + of_property_read_bool(node, "realtek,tp2367-ldo-enable"); > + > + if (!of_property_read_u32(node, "realtek,cp0-ldo-microvolt", &val)) > + priv->settings.cp0_ldo_volt = val; > + if (!of_property_read_u32(node, "realtek,cp1-ldo-microvolt", &val)) > + priv->settings.cp1_ldo_volt = val; > + if (!of_property_read_u32(node, "realtek,tp0145-ldo-microvolt", &val)) > + priv->settings.tp0145_ldo_volt = val; > + if (!of_property_read_u32(node, "realtek,tp2367-ldo-microvolt", &val)) > + priv->settings.tp2367_ldo_volt = val; > + > + if (!of_property_read_u32(node, "realtek,tp0145-pullup-ohms", &val)) > + priv->settings.tp0145_pullup = val; > + if (!of_property_read_u32(node, "realtek,tp2367-pullup-ohms", &val)) > + priv->settings.tp2367_pullup = val; > + > + if (!of_property_read_u32(node, > + "realtek,cp0-output-impedance-ohms", &val)) > + priv->settings.cp0_io_strength = val; > + if (!of_property_read_u32(node, > + "realtek,cp1-output-impedance-ohms", &val)) > + priv->settings.cp1_io_strength = val; > + if (!of_property_read_u32(node, > + "realtek,tp0145-output-impedance-ohms", &val)) > + priv->settings.tp0145_io_strength = val; > + if (!of_property_read_u32(node, > + "realtek,tp2367-output-impedance-ohms", &val)) > + priv->settings.tp2367_io_strength = val; > + > + priv->settings.hub_net_always_i3c = > + of_property_read_bool(node, "realtek,hub-net-always-i3c"); > + > + if (!of_property_read_u8(node, "realtek,tp-scl-h-ack-cycles", &u8val)) > + priv->settings.tp_scl_h_ack_cycles = u8val; > + > + i3c_hub_tp_of_get_setting(dev, node, priv->settings.tp); > + > + priv->settings.handshake_optimize = > + of_property_read_bool(node, "realtek,handshake-optimize"); > + > + if (!of_property_read_u8(node, "realtek,fast-drv-h-add-cycles", > + &u8val)) > + priv->settings.fast_drv_h_add_cycles = u8val; > + > + priv->settings.fast_rson_en = > + of_property_read_bool(node, "realtek,fast-rson-en"); > + > + priv->settings.tp_od_vol_optimize = > + of_property_read_bool(node, "realtek,tp-od-vol-optimize"); > + > + priv->settings.tp_od_vref_optimize = > + of_property_read_bool(node, "realtek,tp-od-vref-optimize"); > +} > + > +static const struct i3c_hub_dev_info * > +i3c_hub_lookup_dev_info(struct i3c_hub *priv) > +{ > + int i, ret; > + u16 part_id = 0; > + u32 val = 0; > + > + ret = regmap_read(priv->regmap, I3C_HUB_DEV_INFO_0, &val); > + if (ret) > + return ERR_PTR(ret); > + > + part_id = (val & 0xFF) << 8; > + > + ret = regmap_read(priv->regmap, I3C_HUB_DEV_REV, &val); > + if (ret) > + return ERR_PTR(ret); > + > + part_id |= I3C_HUB_DEV_REV_LDO_GET(val); > + > + for (i = 0; i < ARRAY_SIZE(i3c_hub_dev_info_table); i++) { > + if (i3c_hub_dev_info_table[i].part_id == part_id) > + return &i3c_hub_dev_info_table[i]; > + } > + return &i3c_hub_dev_info_unknown; > +} > + > +static void i3c_hub_of_default_configuration(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + int id; > + > + priv->settings.cp0_ldo_en = false; > + priv->settings.cp1_ldo_en = false; > + priv->settings.tp0145_ldo_en = false; > + priv->settings.tp2367_ldo_en = false; > + priv->settings.cp0_ldo_volt = 0; > + priv->settings.cp1_ldo_volt = 0; > + priv->settings.tp0145_ldo_volt = 0; > + priv->settings.tp2367_ldo_volt = 0; > + priv->settings.tp0145_pullup = UINT_MAX; > + priv->settings.tp2367_pullup = UINT_MAX; > + priv->settings.cp0_io_strength = 0; > + priv->settings.cp1_io_strength = 0; > + priv->settings.tp0145_io_strength = 0; > + priv->settings.tp2367_io_strength = 0; > + priv->settings.hub_net_always_i3c = false; > + priv->settings.tp_scl_h_ack_cycles = 0; > + priv->settings.handshake_optimize = false; > + priv->settings.fast_drv_h_add_cycles = 3; > + priv->settings.fast_rson_en = false; > + priv->settings.tp_od_vol_optimize = false; > + priv->settings.tp_od_vref_optimize = false; > + > + for (id = 0; id < I3C_HUB_TP_MAX_COUNT; ++id) { > + priv->settings.tp[id].mode = I3C_HUB_DT_TP_MODE_NOT_DEFINED; > + priv->settings.tp[id].pullup_en = false; > + priv->settings.tp[id].io_mode = > + I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED; > + priv->settings.tp[id].poll_interval_ms = 0; > + priv->settings.tp[id].clock_frequency = 400000; > + } > +} > + > +static int i3c_hub_hw_configure_pullup(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + u8 mask = 0, value = 0; > + > + if (priv->settings.tp0145_pullup != UINT_MAX) { > + mask |= TP0145_PULLUP_CONF_MASK; > + if (priv->settings.tp0145_pullup != 0) > + value |= TP0145_PULLUP_CONF( > + i3c_hub_pullup_dt_to_reg(priv->settings.tp0145_pullup)); > + } > + > + if (priv->settings.tp2367_pullup != UINT_MAX) { > + mask |= TP2367_PULLUP_CONF_MASK; > + if (priv->settings.tp2367_pullup != 0) > + value |= TP2367_PULLUP_CONF( > + i3c_hub_pullup_dt_to_reg(priv->settings.tp2367_pullup)); > + } > + > + return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF, > + mask, value); > +} > + > +static int i3c_hub_hw_configure_ldo(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + u8 ldo_config_mask = 0, ldo_config_val = 0; > + u8 ldo_disable_mask = 0, ldo_en_val = 0; > + u32 reg_val; > + int ret; > + u8 val; > + > + /* Enable or Disable LDO's. If there is no DT entry - disable LDO for safety reasons */ > + if (priv->settings.cp0_ldo_en) > + ldo_en_val |= CP0_LDO_EN; > + if (priv->settings.cp1_ldo_en) > + ldo_en_val |= CP1_LDO_EN; > + if (priv->settings.tp0145_ldo_en) > + ldo_en_val |= TP0145_LDO_EN; > + if (priv->settings.tp2367_ldo_en) > + ldo_en_val |= TP2367_LDO_EN; > + > + /* Get current LDOs configuration */ > + ret = regmap_read(priv->regmap, I3C_HUB_LDO_CONF, ®_val); > + if (ret) > + return ret; > + > + /* LDOs Voltage level (Skip if not defined in the DT) > + * Set the mask only if there is a change from current value > + */ > + if (priv->settings.cp0_ldo_volt != 0) { > + val = CP0_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp0_ldo_volt)); > + if ((reg_val & CP0_LDO_VOLTAGE_MASK) != val) { > + ldo_config_mask |= CP0_LDO_VOLTAGE_MASK; > + ldo_disable_mask |= CP0_LDO_EN; > + ldo_config_val |= val; > + } > + } > + if (priv->settings.cp1_ldo_volt != 0) { > + val = CP1_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp1_ldo_volt)); > + if ((reg_val & CP1_LDO_VOLTAGE_MASK) != val) { > + ldo_config_mask |= CP1_LDO_VOLTAGE_MASK; > + ldo_disable_mask |= CP1_LDO_EN; > + ldo_config_val |= val; > + } > + } > + if (priv->settings.tp0145_ldo_volt != 0) { > + val = TP0145_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp0145_ldo_volt)); > + if ((reg_val & TP0145_LDO_VOLTAGE_MASK) != val) { > + ldo_config_mask |= TP0145_LDO_VOLTAGE_MASK; > + ldo_disable_mask |= TP0145_LDO_EN; > + ldo_config_val |= val; > + } > + } > + if (priv->settings.tp2367_ldo_volt != 0) { > + val = TP2367_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp2367_ldo_volt)); > + if ((reg_val & TP2367_LDO_VOLTAGE_MASK) != val) { > + ldo_config_mask |= TP2367_LDO_VOLTAGE_MASK; > + ldo_disable_mask |= TP2367_LDO_EN; > + ldo_config_val |= val; > + } > + } > + > + /* > + * Update LDO voltage configuration only if value is changed from already existing register > + * value. It is a good practice to disable the LDO's before making any voltage changes. > + * Presence of config mask indicates voltage change to be applied. > + */ > + if (ldo_config_mask) { > + /* Disable LDO's before making voltage changes */ > + ret = regmap_update_bits(priv->regmap, > + I3C_HUB_LDO_AND_PULLUP_CONF, > + ldo_disable_mask, 0); > + if (ret) > + return ret; > + > + /* Update the LDOs configuration */ > + ret = regmap_update_bits(priv->regmap, I3C_HUB_LDO_CONF, > + ldo_config_mask, ldo_config_val); > + if (ret) > + return ret; > + } > + > + /* Update the LDOs Enable/disable register. This will enable only LDOs enabled in DT */ > + return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF, > + LDO_ENABLE_DISABLE_MASK, ldo_en_val); > +} > + > +static int i3c_hub_hw_configure_io_strength(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + u8 mask_all = 0, val_all = 0; > + u32 reg_val; > + u8 val; > + struct dt_settings tmp; > + int ret; > + > + /* Get IO strength configuration to figure out what needs to be changed */ > + ret = regmap_read(priv->regmap, I3C_HUB_IO_STRENGTH, ®_val); > + if (ret) > + return ret; > + > + tmp = priv->settings; > + if (tmp.cp0_io_strength != 0) { > + val = CP0_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp0_io_strength)); > + mask_all |= CP0_IO_STRENGTH_MASK; > + val_all |= val; > + } > + if (tmp.cp1_io_strength != 0) { > + val = CP1_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp1_io_strength)); > + mask_all |= CP1_IO_STRENGTH_MASK; > + val_all |= val; > + } > + if (tmp.tp0145_io_strength != 0) { > + val = TP0145_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp0145_io_strength)); > + mask_all |= TP0145_IO_STRENGTH_MASK; > + val_all |= val; > + } > + if (tmp.tp2367_io_strength != 0) { > + val = TP2367_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp2367_io_strength)); > + mask_all |= TP2367_IO_STRENGTH_MASK; > + val_all |= val; > + } > + > + /* Set IO strength if required */ > + return regmap_update_bits(priv->regmap, I3C_HUB_IO_STRENGTH, mask_all, > + val_all); > +} > + > +static int i3c_hub_hw_configure_tp(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + u8 pullup_mask = 0, pullup_val = 0; > + u8 smbus_mask = 0, smbus_val = 0; > + u8 gpio_mask = 0, gpio_val = 0; > + u8 i3c_mask = 0, i3c_val = 0; > + u8 io_mode_mask = 0, io_mode_val = 0; > + int ret; > + int i, index; > + > + memset(priv->gpio.port_to_index, -1, sizeof(priv->gpio.port_to_index)); > + > + for (i = 0; i < priv->dev_info->n_ports; ++i) { > + if (priv->settings.tp[i].mode != > + I3C_HUB_DT_TP_MODE_NOT_DEFINED) { > + i3c_mask |= TPN_NET_CON(i); > + smbus_mask |= TPN_SMBUS_MODE_EN(i); > + gpio_mask |= TPN_GPIO_MODE_EN(i); > + io_mode_mask |= TPN_IO_MODE_CON(i); > + > + if (priv->settings.tp[i].mode == > + I3C_HUB_DT_TP_MODE_I3C) { > + i3c_val |= TPN_NET_CON(i); > + } else if (priv->settings.tp[i].mode == > + I3C_HUB_DT_TP_MODE_SMBUS) { > + smbus_val |= TPN_SMBUS_MODE_EN(i); > + } else if (priv->settings.tp[i].mode == > + I3C_HUB_DT_TP_MODE_GPIO) { > + gpio_val |= TPN_GPIO_MODE_EN(i); > + priv->gpio.nums += GPIO_BANK_SZ; > + index = priv->gpio.nums / GPIO_BANK_SZ - 1; > + priv->gpio.tp[index] = i; > + priv->gpio.port_to_index[i] = index; > + } > + } > + pullup_mask |= TPN_PULLUP_EN(i); > + if (priv->settings.tp[i].pullup_en) > + pullup_val |= TPN_PULLUP_EN(i); > + if (priv->settings.tp[i].io_mode != > + I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED) { > + if (priv->settings.tp[i].io_mode == > + I3C_HUB_DT_TP_IO_MODE_OD) > + io_mode_val |= TPN_IO_MODE_CON(i); > + } else if (priv->settings.tp[i].mode == > + I3C_HUB_DT_TP_MODE_SMBUS) { > + io_mode_val |= TPN_IO_MODE_CON(i); > + } > + } > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_IO_MODE_CONF, > + io_mode_mask, io_mode_val); > + if (ret) > + return ret; > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_PULLUP_EN, > + pullup_mask, pullup_val); > + if (ret) > + return ret; > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_SMBUS_AGNT_EN, > + smbus_mask, smbus_val); > + if (ret) > + return ret; > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_GPIO_MODE_EN, > + gpio_mask, gpio_val); > + if (ret) > + return ret; > + > + /* Request for HUB Network connection in case any TP is configured in I3C mode */ > + if (i3c_val) { > + ret = regmap_write(priv->regmap, I3C_HUB_CP_MUX_SET, > + CONTROLLER_PORT_MUX_REQ); > + if (ret) > + return ret; > + /* TODO: verify if connection is done */ > + } > + > + /* Enable TP here in case TP was configured */ > + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_ENABLE, > + i3c_mask | smbus_mask | gpio_mask, > + i3c_val | smbus_val | gpio_val); > + if (ret) > + return ret; > + > + return regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, i3c_val); > +} > + > +static int i3c_hub_hw_configure_misc(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + int ret; > + u8 reg = I3C_HUB_TARGET_DA_FLAG_BYTE_BASE; > + u8 val[I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT]; > + > + if (!priv->settings.hub_net_always_i3c) > + return 0; > + > + memset(val, 0xff, I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT); > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_NET_OPER_MODE_CONF, > + I3C_HUB_NET_ALWAYS_I3C_EN, > + I3C_HUB_NET_ALWAYS_I3C_EN); > + if (ret) > + return ret; > + > + ret = regmap_bulk_write(priv->regmap, reg, val, > + I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT); > + return ret; > +} > + > +typedef int (*i3c_hub_cfg_fn)(struct i3c_hub *priv); > + > +static int i3c_hub_hw_cfg_with_page(struct i3c_hub *priv, u8 page, > + i3c_hub_cfg_fn op) > +{ > + int ret; > + > + if (!op) > + return -EINVAL; > + > + mutex_lock(&priv->page_mutex); > + ret = regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, page); > + if (ret) > + goto unlock; > + > + ret = op(priv); > +unlock: > + regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, 0x00); > + mutex_unlock(&priv->page_mutex); > + return ret; > +} > + > +static int i3c_hub_cfg_op_fuse_latch(struct i3c_hub *priv) > +{ > + int ret; > + > + if (priv->settings.handshake_optimize) { > + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E, > + I3C_HUB_IBI_ACK_RD_CYCLE_MASK, > + I3C_HUB_IBI_ACK_RD_CYCLE_VAL); > + if (ret) > + return ret; > + } > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A3, > + I3C_HUB_FAST_DRV_LOOP_DIS, > + I3C_HUB_FAST_DRV_LOOP_DIS); > + if (ret) > + return ret; > + > + if (priv->settings.tp_od_vol_optimize) { > + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D, > + I3C_HUB_TP_OD_VOL_LEVEL, > + I3C_HUB_TP_OD_VOL_LEVEL); > + if (ret) > + return ret; > + } > + > + if (priv->settings.tp_od_vref_optimize) { > + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D, > + I3C_HUB_TP_OD_VREF, > + I3C_HUB_TP_OD_VREF); > + if (ret) > + return ret; > + } > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E, > + I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK, > + I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL( > + priv->settings.fast_drv_h_add_cycles)); > + if (ret) > + return ret; > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A0, > + I3C_HUB_FAST_RSON_EN, > + priv->settings.fast_rson_en ? I3C_HUB_FAST_RSON_EN : 0); > + return ret; > +} > + > +static int i3c_hub_hw_configure_fuse_latch(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + > + return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_EFUSE_PAGE, > + i3c_hub_cfg_op_fuse_latch); > +} > + > +static int i3c_hub_cfg_op_io(struct i3c_hub *priv) > +{ > + int ret; > + u8 reg = I3C_HUB_CFG_TP_SCL_L_ACK_CLK; > + > + /* cfg tp scl low ack clk */ > + ret = regmap_write(priv->regmap, reg, > + I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN | > + I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL); > + if (ret) > + return ret; > + > + /* cfg tp scl high ack clk */ > + reg = I3C_HUB_CFG_TP_SCL_H_ACK_CLK; > + if (priv->settings.tp_scl_h_ack_cycles == 0) > + return 0; > + > + ret = regmap_write(priv->regmap, reg, > + I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN | > + I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL( > + priv->settings.tp_scl_h_ack_cycles)); > + > + return ret; > +} > + > +static int i3c_hub_hw_configure_io(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + > + return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_IO_CTRL_PAGE, > + i3c_hub_cfg_op_io); > +} > + > +static int i3c_hub_configure_hw(struct device *dev) > +{ > + int ret; > + struct i3c_hub *priv = dev_get_drvdata(dev); > + > + ret = i3c_hub_hw_configure_ldo(dev); > + if (ret) > + return ret; > + > + ret = i3c_hub_hw_configure_io_strength(dev); > + if (ret) > + return ret; > + > + ret = i3c_hub_hw_configure_pullup(dev); > + if (ret) > + return ret; > + > + if (priv->dev_info->part_id) { > + ret = i3c_hub_hw_configure_misc(dev); > + if (ret) > + return ret; > + > + ret = i3c_hub_hw_configure_fuse_latch(dev); > + if (ret) > + return ret; > + > + ret = i3c_hub_hw_configure_io(dev); > + if (ret) > + return ret; > + } > + > + ret = i3c_hub_hw_configure_tp(dev); > + return ret; > +} > + > +static void i3c_hub_of_get_conf_runtime(struct device *dev, > + const struct device_node *node) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + struct device_node *i3c_node; > + int i3c_id; > + u8 tp_mask; > + > + for_each_available_child_of_node(node, i3c_node) { > + if (!i3c_node->full_name || > + (sscanf(i3c_node->full_name, "i3c%i@%hhx", &i3c_id, > + &tp_mask) != 2)) > + continue; > + > + if (i3c_id < priv->dev_info->n_ports) { > + priv->logical_bus[i3c_id].available = true; > + priv->logical_bus[i3c_id].of_node = i3c_node; > + priv->logical_bus[i3c_id].tp_map = tp_mask; > + priv->logical_bus[i3c_id].priv = priv; > + priv->logical_bus[i3c_id].tp_id = i3c_id; > + } > + } > +} > + > +static const struct i3c_device_id i3c_hub_ids[] = { > + I3C_CLASS(I3C_DCR_HUB, NULL), > + {}, > +}; > + > +static int i3c_hub_read_id(struct device *dev) > +{ > + struct i3c_hub *priv = dev_get_drvdata(dev); > + u32 reg_val; > + int ret; > + > + ret = regmap_read(priv->regmap, I3C_HUB_LDO_AND_CPSEL_STS, ®_val); > + if (ret) { > + dev_err(dev, "Failed to read status register\n"); > + return -1; > + } > + > + priv->hub_pin_sel_id = CP_SEL_PIN_INPUT_CODE_GET(reg_val); > + priv->hub_pin_cp1_id = CP_SDA1_SCL1_PINS_CODE_GET(reg_val); > + return 0; > +} > + > +static struct device_node *i3c_hub_get_dt_hub_node(struct device_node *node, > + struct i3c_hub *priv) > +{ > + struct device_node *hub_node_no_id = NULL; > + struct device_node *hub_node; > + u32 hub_id; > + u32 id_mask; > + u32 dt_id; > + u32 pin_id; > + int found_id = 0; > + > + for_each_available_child_of_node(node, hub_node) { > + id_mask = 0; > + priv->hub_dt_sel_id = -1; > + priv->hub_dt_cp1_id = -1; > + > + if (strstr(hub_node->name, "hub")) { > + if (!of_property_read_u32(hub_node, "realtek,id", > + &hub_id)) { > + id_mask |= 0x0f; > + priv->hub_dt_sel_id = hub_id; > + } > + > + if (!of_property_read_u32(hub_node, "realtek,id-cp1", > + &hub_id)) { > + id_mask |= 0xf0; > + priv->hub_dt_cp1_id = hub_id; > + } > + > + dt_id = ((u32)priv->hub_dt_cp1_id & 0x0f) << 4 | > + ((u32)priv->hub_dt_sel_id & 0x0f); > + pin_id = ((u32)priv->hub_pin_cp1_id & 0x0f) << 4 | > + ((u32)priv->hub_pin_sel_id & 0x0f); > + > + if (id_mask != 0 && > + (dt_id & id_mask) == (pin_id & id_mask)) > + found_id = 1; > + > + if (!found_id) { > + /* > + * Just keep reference to first HUB node with no ID in case no ID > + * matching > + */ > + if (!hub_node_no_id && > + priv->hub_dt_sel_id == -1 && > + priv->hub_dt_cp1_id == -1) > + hub_node_no_id = hub_node; > + } else { > + return hub_node; > + } > + } > + } > + > + return hub_node_no_id; > +} > + > +static int fops_access_reg_get(void *ctx, u64 *val) > +{ > + struct i3c_hub *priv = ctx; > + u32 reg_val; > + int ret; > + > + ret = regmap_read(priv->regmap, priv->reg_addr, ®_val); > + if (ret) > + return ret; > + > + *val = reg_val & 0xFF; > + return 0; > +} > + > +static int fops_access_reg_set(void *ctx, u64 val) > +{ > + struct i3c_hub *priv = ctx; > + > + return regmap_write(priv->regmap, priv->reg_addr, val & 0xFF); > +} > + > +DEFINE_DEBUGFS_ATTRIBUTE(fops_access_reg, fops_access_reg_get, > + fops_access_reg_set, "0x%llX\n"); > + > +static int i3c_hub_debugfs_init(struct i3c_hub *priv, const char *hub_id) > +{ > + struct dentry *entry, *dt_conf_dir, *reg_dir; > + struct dt_settings *settings = NULL; > + int i; > + > + entry = debugfs_create_dir(hub_id, NULL); > + if (IS_ERR(entry)) > + return PTR_ERR(entry); > + > + priv->debug_dir = entry; > + > + if (priv->dev_info) > + debugfs_create_str("model", 0400, priv->debug_dir, > + (char **)&priv->dev_info->model); > + > + entry = debugfs_create_dir("dt-conf", priv->debug_dir); > + if (IS_ERR(entry)) > + goto err_remove; > + > + dt_conf_dir = entry; > + > + settings = &priv->settings; > + debugfs_create_bool("cp0-ldo-en", 0400, dt_conf_dir, > + &settings->cp0_ldo_en); > + debugfs_create_bool("cp1-ldo-en", 0400, dt_conf_dir, > + &settings->cp1_ldo_en); > + debugfs_create_u32("cp0-ldo-volt", 0400, dt_conf_dir, > + &settings->cp0_ldo_volt); > + debugfs_create_u32("cp1-ldo-volt", 0400, dt_conf_dir, > + &settings->cp1_ldo_volt); > + debugfs_create_bool("tp0145-ldo-en", 0400, dt_conf_dir, > + &settings->tp0145_ldo_en); > + debugfs_create_bool("tp2367-ldo-en", 0400, dt_conf_dir, > + &settings->tp2367_ldo_en); > + debugfs_create_u32("tp0145-ldo-volt", 0400, dt_conf_dir, > + &settings->tp0145_ldo_volt); > + debugfs_create_u32("tp2367-ldo-volt", 0400, dt_conf_dir, > + &settings->tp2367_ldo_volt); > + debugfs_create_u32("tp0145-pullup", 0400, dt_conf_dir, > + &settings->tp0145_pullup); > + debugfs_create_u32("tp2367-pullup", 0400, dt_conf_dir, > + &settings->tp2367_pullup); > + debugfs_create_bool("hub-net-always-i3c", 0400, dt_conf_dir, > + &settings->hub_net_always_i3c); > + debugfs_create_u8("tp-scl-h-ack-cycles", 0400, dt_conf_dir, > + &settings->tp_scl_h_ack_cycles); > + debugfs_create_bool("handshake-optimize", 0400, dt_conf_dir, > + &settings->handshake_optimize); > + debugfs_create_u8("fast-drv-h-add-cycles", 0400, dt_conf_dir, > + &settings->fast_drv_h_add_cycles); > + debugfs_create_bool("fast-rson-en", 0400, dt_conf_dir, > + &settings->fast_rson_en); > + debugfs_create_bool("tp-od-vol-optimize", 0400, dt_conf_dir, > + &settings->tp_od_vol_optimize); > + debugfs_create_bool("tp-od-vref-optimize", 0400, dt_conf_dir, > + &settings->tp_od_vref_optimize); > + > + for (i = 0; i < priv->dev_info->n_ports; ++i) { > + char file_name[32]; > + > + sprintf(file_name, "tp%i.mode", i); > + debugfs_create_u8(file_name, 0400, dt_conf_dir, > + &settings->tp[i].mode); > + sprintf(file_name, "tp%i.pullup_en", i); > + debugfs_create_bool(file_name, 0400, dt_conf_dir, > + &settings->tp[i].pullup_en); > + sprintf(file_name, "tp%i.io_mode", i); > + debugfs_create_u8(file_name, 0400, dt_conf_dir, > + &settings->tp[i].io_mode); > + sprintf(file_name, "tp%i.poll_interval_ms", i); > + debugfs_create_u32(file_name, 0400, dt_conf_dir, > + &settings->tp[i].poll_interval_ms); > + sprintf(file_name, "tp%i.clock_frequency", i); > + debugfs_create_u32(file_name, 0400, dt_conf_dir, > + &settings->tp[i].clock_frequency); > + } > + > + entry = debugfs_create_dir("reg", priv->debug_dir); > + if (IS_ERR(entry)) > + goto err_remove; > + > + reg_dir = entry; > + > + entry = debugfs_create_file_unsafe("access", 0600, reg_dir, priv, > + &fops_access_reg); > + if (IS_ERR(entry)) > + goto err_remove; > + > + debugfs_create_u8("offset", 0600, reg_dir, &priv->reg_addr); > + > + return 0; > + > +err_remove: > + debugfs_remove_recursive(priv->debug_dir); > + return PTR_ERR(entry); > +} > + > +static void i3c_hub_trans_pre_cb(struct logical_bus *bus) > +{ > + struct i3c_hub *priv = bus->priv; > + struct device *dev = i3cdev_to_dev(priv->i3cdev); > + int ret; > + > + if (priv->settings.tp[bus->tp_id].always_enable) > + return; > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF, > + GENMASK(bus->tp_id, bus->tp_id), bus->tp_map); > + if (ret) > + dev_warn(dev, "Failed to open Target Port(s)\n"); > +} > + > +static void i3c_hub_trans_post_cb(struct logical_bus *bus) > +{ > + struct i3c_hub *priv = bus->priv; > + struct device *dev = i3cdev_to_dev(priv->i3cdev); > + int ret; > + > + if (priv->settings.tp[bus->tp_id].always_enable) > + return; > + > + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF, > + GENMASK(bus->tp_id, bus->tp_id), 0x00); > + if (ret) > + dev_warn(dev, "Failed to close Target Port(s)\n"); > +} > + > +static struct logical_bus *bus_from_i3c_desc(struct i3c_dev_desc *desc) > +{ > + struct i3c_master_controller *controller = i3c_dev_get_master(desc); > + > + return container_of(controller, struct logical_bus, controller); > +} > + > +static struct logical_bus *bus_from_i2c_desc(struct i2c_dev_desc *desc) > +{ > + struct i3c_master_controller *controller = i2c_dev_get_master(desc); > + > + return container_of(controller, struct logical_bus, controller); > +} > + > +static struct i3c_master_controller * > +parent_from_controller(struct i3c_master_controller *controller) > +{ > + struct logical_bus *bus = > + container_of(controller, struct logical_bus, controller); > + > + return bus->priv->driving_master; > +} > + > +static struct i3c_master_controller * > +parent_controller_from_i3c_desc(struct i3c_dev_desc *desc) > +{ > + struct i3c_master_controller *controller = i3c_dev_get_master(desc); > + struct logical_bus *bus = > + container_of(controller, struct logical_bus, controller); > + > + return bus->priv->driving_master; > +} > + > +static struct i3c_master_controller * > +parent_controller_from_i2c_desc(struct i2c_dev_desc *desc) > +{ > + struct i3c_master_controller *controller = desc->common.master; > + struct logical_bus *bus = > + container_of(controller, struct logical_bus, controller); > + > + return bus->priv->driving_master; > +} > + > +static struct i3c_master_controller * > +update_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc, > + struct i3c_master_controller *parent) > +{ > + struct i3c_master_controller *orig_parent = desc->master; > + > + desc->master = parent; > + > + return orig_parent; > +} > + > +static void restore_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc, > + struct i3c_master_controller *parent) > +{ > + desc->master = parent; > +} > + > +static int i3c_hub_read_transaction_status(struct i3c_hub *priv, u8 target_port, > + u8 target_port_status, u32 data_len) > +{ > + unsigned int status_read; > + int ret; > + struct i2c_adapter_group *smbus = > + &priv->smbus_port_adapter[target_port]; > + u32 smbus_clk = priv->settings.tp[target_port].clock_frequency / 1000; > + u8 status; > + u8 ret_code; > + > + if (!priv->settings.tp[target_port].poll_interval_ms) { > + ret = wait_for_completion_timeout(&smbus->completion, > + smbus->i2c.timeout); > + if (!ret) { > + dev_err(&priv->i3cdev->dev, > + "Status read timeout reached on target port %d\n", > + target_port); > + return -ETIMEDOUT; > + } > + > + status = (u8)smbus->status & > + I3C_HUB_CONTROLLER_AGENT_STATUS_MASK; > + } else { > + ret = regmap_read_poll_timeout(priv->regmap, > + target_port_status, > + status_read, > + (u8)status_read & > + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG, > + I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL( > + data_len, smbus_clk), > + jiffies_to_usecs(smbus->i2c.timeout)); > + > + if (ret) { > + dev_err(&priv->i3cdev->dev, > + "Status read timeout reached on target port %d\n", > + target_port); > + return ret; > + } > + > + ret = regmap_write(priv->regmap, target_port_status, > + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG); > + if (ret) > + return ret; > + > + status = (u8)status_read & I3C_HUB_CONTROLLER_AGENT_STATUS_MASK; > + } > + > + ret_code = status >> I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT; > + > + switch (ret_code) { > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS: > + return 0; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK: > + dev_dbg(&priv->i3cdev->dev, > + "TP%u SMBus: Address NACK (device not present)\n", > + target_port); > + return -ENXIO; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY: > + dev_dbg(&priv->i3cdev->dev, > + "TP%u SMBus: Device busy (data NACK after address ACK)\n", > + target_port); > + return -EREMOTEIO; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY: > + dev_dbg(&priv->i3cdev->dev, > + "TP%u SMBus: Device read not ready (read address NACK after write)\n", > + target_port); > + return -ENXIO; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED: > + dev_dbg(&priv->i3cdev->dev, > + "TP%u SMBus: Sync issue recovered (SDA stuck low, recovered by 9 SCL pulses)\n", > + target_port); > + return -EAGAIN; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR: > + dev_dbg(&priv->i3cdev->dev, > + "TP%u SMBus: Sync issue bus clear (recovered by SCL low 35ms)\n", > + target_port); > + return -EAGAIN; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT: > + dev_err(&priv->i3cdev->dev, > + "TP%u SMBus: Bus fault (SDA stuck low remains after recovery)\n", > + target_port); > + return -EIO; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST: > + dev_dbg(&priv->i3cdev->dev, "TP%u SMBus: Arbitration lost\n", > + target_port); > + return -EAGAIN; > + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT: > + dev_err(&priv->i3cdev->dev, "TP%u SMBus: SCL timeout\n", > + target_port); > + return -ETIMEDOUT; > + default: > + dev_err(&priv->i3cdev->dev, > + "TP%u SMBus: Reserved/unknown return code 0x%x\n", > + target_port, ret_code); > + return -EIO; > + } > +} > + > +/* > + * i3c_hub_smbus_msg() - This starts a smbus transaction by writing a descriptor > + * and a message to the hub registers. Controller buffer page is determined by multiplying the > + * target port index by four and adding the base page number to it. > + * @hub: a pointer to the i3c hub main structure > + * @target_port: a number of the port where the transaction will happen > + * @xfers: i2c_msg struct received from the master_xfers callback > + * @nxfers_i: the number of the current message > + * @xfer_type: transfer type: > + * - I3C_HUB_SMBUS_XFER_WRITE (0): single write > + * - I3C_HUB_SMBUS_XFER_READ (1): single read > + * - I3C_HUB_SMBUS_XFER_WR_RD (2): write followed by read > + * (uses xfers[nxfers_i] as write and xfers[nxfers_i+1] as read) > + * > + * Return: 0 on success, negative errno on failure from hub status or regmap ops. > + * Note: for WR_RD the caller must ensure xfers[nxfers_i+1] exists, the address > + * matches, and write_len + read_len <= I3C_HUB_SMBUS_PAYLOAD_SIZE. > + */ > +static int i3c_hub_smbus_msg(struct i3c_hub *hub, struct i2c_msg *xfers, > + u8 target_port, u8 nxfers_i, u8 xfer_type) > +{ > + u8 transaction_type = I3C_HUB_SMBUS_400_KHZ; > + u8 controller_buffer_page = > + I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * target_port; > + int write_length = 0, read_length = 0; > + u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + target_port; > + u8 target_port_code = BIT(target_port); > + u8 rw_address = xfers[nxfers_i].addr << 1; > + u8 desc[I3C_HUB_SMBUS_DESCRIPTOR_SIZE] = { 0 }; > + int ret = 0; > + > + transaction_type = > + i3c_hub_smbus_rate_bits_from_hz(hub->settings.tp[target_port].clock_frequency); > + > + switch (xfer_type) { > + case I3C_HUB_SMBUS_XFER_WRITE: > + write_length = xfers[nxfers_i].len; > + break; > + case I3C_HUB_SMBUS_XFER_READ: > + read_length = xfers[nxfers_i].len; > + rw_address |= BIT(0); > + break; > + case I3C_HUB_SMBUS_XFER_WR_RD: > + write_length = xfers[nxfers_i].len; > + read_length = xfers[nxfers_i + 1].len; > + transaction_type |= BIT(0); > + break; > + default: > + return -EINVAL; > + } > + > + /* Assemble descriptor */ > + desc[0] = rw_address; > + desc[1] = transaction_type; > + desc[2] = write_length; > + desc[3] = read_length; > + > + mutex_lock(&hub->page_mutex); > + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, > + controller_buffer_page); > + if (ret) > + goto unlock; > + > + ret = regmap_bulk_write(hub->regmap, I3C_HUB_CONTROLLER_AGENT_BUFF, > + desc, I3C_HUB_SMBUS_DESCRIPTOR_SIZE); > + if (ret) > + goto unlock; > + > + if (write_length) { > + ret = regmap_bulk_write(hub->regmap, > + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA, > + xfers[nxfers_i].buf, write_length); > + if (ret) > + goto unlock; > + } > + > + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); > + mutex_unlock(&hub->page_mutex); > + if (ret) > + return ret; > + > + /* Start transaction */ > + ret = regmap_write(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_TRANS_START, > + target_port_code); > + if (ret) > + return ret; > + > + /* Get transaction status */ > + ret = i3c_hub_read_transaction_status(hub, target_port, > + target_port_status, > + write_length + read_length); > + if (ret) > + return ret; > + > + /* if read_length is non-zero, read back the data */ > + if (read_length) { > + mutex_lock(&hub->page_mutex); > + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, > + controller_buffer_page); > + if (ret) > + goto unlock; > + > + if (xfer_type == I3C_HUB_SMBUS_XFER_READ) { > + ret = regmap_bulk_read(hub->regmap, > + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA, > + xfers[nxfers_i].buf, read_length); > + } else { > + ret = regmap_bulk_read(hub->regmap, > + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA + > + write_length, > + xfers[nxfers_i + 1].buf, read_length); > + } > + if (ret) > + goto unlock; > + > + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); > + mutex_unlock(&hub->page_mutex); > + } > + > + return ret; > +unlock: > + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); > + mutex_unlock(&hub->page_mutex); > + return ret; > +} > + > +static inline bool i3c_hub_can_combine_wr_rd(const struct i2c_msg *w, > + const struct i2c_msg *r) > +{ > + /* w: write, r: read; same addr; total length within payload */ > + return !(w->flags & I2C_M_RD) && (r->flags & I2C_M_RD) && > + w->addr == r->addr && > + (w->len + r->len) <= I3C_HUB_SMBUS_PAYLOAD_SIZE; > +} > + > +/** > + * i3c_hub_smbus_port_adapter_xfer() - i3c hub smbus transfer logic > + * @adap: i2c_adapter corresponding with single port in the i3c hub > + * @xfers: all messages descriptors and data > + * @nxfers: amount of single messages in a transfer > + * > + * Return: function returns the sum of correctly sent messages (only those with hub return > + * status 0x01) > + */ > +static int i3c_hub_smbus_port_adapter_xfer(struct i2c_adapter *adap, > + struct i2c_msg *xfers, int nxfers) > +{ > + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap); > + struct i3c_hub *hub = smbus->priv; > + int ret_sum = 0, ret, len, type, nxfers_i; > + const struct i2c_msg *cur = NULL, *next = NULL; > + > + for (nxfers_i = 0; nxfers_i < nxfers; nxfers_i++) { > + cur = &xfers[nxfers_i]; > + len = cur->len; > + type = cur->flags & I2C_M_RD ? I3C_HUB_SMBUS_XFER_READ : > + I3C_HUB_SMBUS_XFER_WRITE; > + > + /* Per-message length limit check */ > + if (len > I3C_HUB_SMBUS_PAYLOAD_SIZE) { > + dev_err(&adap->dev, > + "Message nr. %d not sent - length over %d bytes.\n", > + nxfers_i, I3C_HUB_SMBUS_PAYLOAD_SIZE); > + continue; > + } > + > + /* Try to combine write followed by read to the same address */ > + if (type == I3C_HUB_SMBUS_XFER_WRITE && > + (nxfers_i + 1) < nxfers) { > + next = &xfers[nxfers_i + 1]; > + if (i3c_hub_can_combine_wr_rd(cur, next)) > + type = I3C_HUB_SMBUS_XFER_WR_RD; > + } > + > + ret = i3c_hub_smbus_msg(hub, xfers, smbus->tp_port, nxfers_i, > + type); > + if (ret) > + return ret; > + > + if (type == I3C_HUB_SMBUS_XFER_WR_RD) { > + ret_sum += 2; > + nxfers_i++; /* skip the next read message */ > + > + } else { > + ret_sum++; > + } > + } > + return ret_sum; > +} > + > +static int i3c_hub_bus_init(struct i3c_master_controller *controller) > +{ > + struct logical_bus *bus = > + container_of(controller, struct logical_bus, controller); > + > + controller->this = bus->priv->i3cdev->desc; > + return 0; > +} > + > +static void i3c_hub_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 *parent = > + parent_controller_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + int ret; > + > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + ret = parent->ops->attach_i3c_dev(dev); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + return ret; > +} > + > +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev, u8 old_dyn_addr) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + int ret; > + > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + ret = parent->ops->reattach_i3c_dev(dev, old_dyn_addr); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + return ret; > +} > + > +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + parent->ops->detach_i3c_dev(dev); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > +} > + > +static int i3c_hub_do_daa(struct i3c_master_controller *controller) > +{ > + struct i3c_master_controller *parent = > + parent_from_controller(controller); > + int ret; > + > + down_write(&parent->bus.lock); > + ret = parent->ops->do_daa(parent); > + up_write(&parent->bus.lock); > + return ret; > +} > + > +static bool i3c_hub_supports_ccc_cmd(struct i3c_master_controller *controller, > + const struct i3c_ccc_cmd *cmd) > +{ > + struct i3c_master_controller *parent = > + parent_from_controller(controller); > + > + return parent->ops->supports_ccc_cmd(parent, cmd); > +} > + > +static int i3c_hub_send_ccc_cmd(struct i3c_master_controller *controller, > + struct i3c_ccc_cmd *cmd) > +{ > + struct i3c_master_controller *parent = > + parent_from_controller(controller); > + struct logical_bus *bus = > + container_of(controller, struct logical_bus, controller); > + int ret; > + > + if (cmd->id == I3C_CCC_RSTDAA(true)) > + return 0; > + > + i3c_hub_trans_pre_cb(bus); > + down_read(&parent->bus.lock); > + ret = parent->ops->send_ccc_cmd(parent, cmd); > + up_read(&parent->bus.lock); > + i3c_hub_trans_post_cb(bus); > + > + return ret; > +} > + > +static int i3c_hub_priv_xfers(struct i3c_dev_desc *dev, > + struct i3c_xfer *xfers, int nxfers, > + enum i3c_xfer_mode mode) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + struct logical_bus *bus = bus_from_i3c_desc(dev); > + int res; > + > + i3c_hub_trans_pre_cb(bus); > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + down_read(&parent->bus.lock); > + res = parent->ops->i3c_xfers(dev, xfers, nxfers, mode); > + up_read(&parent->bus.lock); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + i3c_hub_trans_post_cb(bus); > + > + return res; > +} > + > +static int i3c_hub_attach_i2c_dev(struct i2c_dev_desc *dev) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i2c_desc(dev); > + struct i3c_master_controller *orig_parent; > + int ret; > + > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + ret = parent->ops->attach_i2c_dev(dev); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + return ret; > +} > + > +static void i3c_hub_detach_i2c_dev(struct i2c_dev_desc *dev) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i2c_desc(dev); > + struct i3c_master_controller *orig_parent; > + > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + parent->ops->detach_i2c_dev(dev); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > +} > + > +static int i3c_hub_i2c_xfers(struct i2c_dev_desc *dev, struct i2c_msg *xfers, > + int nxfers) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i2c_desc(dev); > + struct logical_bus *bus = bus_from_i2c_desc(dev); > + struct i3c_master_controller *orig_parent; > + int ret; > + > + i3c_hub_trans_pre_cb(bus); > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + ret = parent->ops->i2c_xfers(dev, xfers, nxfers); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + i3c_hub_trans_post_cb(bus); > + return ret; > +} > + > +static int i3c_hub_request_ibi(struct i3c_dev_desc *dev, > + const struct i3c_ibi_setup *req) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct logical_bus *bus = bus_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + int ret; > + > + i3c_hub_trans_pre_cb(bus); > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + down_read(&parent->bus.lock); > + ret = parent->ops->request_ibi(dev, req); > + up_read(&parent->bus.lock); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + i3c_hub_trans_post_cb(bus); > + return ret; > +} > + > +static void i3c_hub_free_ibi(struct i3c_dev_desc *dev) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct logical_bus *bus = bus_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + > + i3c_hub_trans_pre_cb(bus); > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + down_read(&parent->bus.lock); > + parent->ops->free_ibi(dev); > + up_read(&parent->bus.lock); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + i3c_hub_trans_post_cb(bus); > +} > + > +static int i3c_hub_enable_ibi(struct i3c_dev_desc *dev) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct logical_bus *bus = bus_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + int ret; > + > + i3c_hub_trans_pre_cb(bus); > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + down_read(&parent->bus.lock); > + ret = parent->ops->enable_ibi(dev); > + up_read(&parent->bus.lock); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + i3c_hub_trans_post_cb(bus); > + return ret; > +} > + > +static int i3c_hub_disable_ibi(struct i3c_dev_desc *dev) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct logical_bus *bus = bus_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + int ret; > + > + i3c_hub_trans_pre_cb(bus); > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + down_read(&parent->bus.lock); > + ret = parent->ops->disable_ibi(dev); > + up_read(&parent->bus.lock); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > + i3c_hub_trans_post_cb(bus); > + return ret; > +} > + > +static void i3c_hub_recycle_ibi_slot(struct i3c_dev_desc *dev, > + struct i3c_ibi_slot *slot) > +{ > + struct i3c_master_controller *parent = > + parent_controller_from_i3c_desc(dev); > + struct i3c_master_controller *orig_parent; > + > + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent); > + parent->ops->recycle_ibi_slot(dev, slot); > + restore_i3c_i2c_desc_parent(&dev->common, orig_parent); > +} > + > +static const struct i3c_master_controller_ops i3c_hub_i3c_ops = { > + .bus_init = i3c_hub_bus_init, > + .bus_cleanup = i3c_hub_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_do_daa, > + .supports_ccc_cmd = i3c_hub_supports_ccc_cmd, > + .send_ccc_cmd = i3c_hub_send_ccc_cmd, > + .i3c_xfers = i3c_hub_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, > +}; > + > +static int i3c_hub_logic_register(struct i3c_master_controller *controller, > + struct device *parent) > +{ > + return i3c_master_register(controller, parent, &i3c_hub_i3c_ops, false); > +} > + > +static u32 i3c_hub_smbus_funcs(struct i2c_adapter *adapter) > +{ > + return (I2C_FUNC_SMBUS_EMUL | I2C_FUNC_I2C) & ~I2C_FUNC_SMBUS_QUICK; > +} > + > +#if IS_ENABLED(CONFIG_I2C_SLAVE) > +static int reg_i2c_target(struct i2c_client *client) > +{ > + struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter); > + struct smbus_backend *backend; > + int ret = 0; > + > + if (!smbus) > + return -EINVAL; > + > + mutex_lock(&smbus->mutex); > + > + list_for_each_entry(backend, &smbus->backend_entry, list) { > + if (backend->client->addr == client->addr) { > + ret = -EBUSY; > + goto out; > + } > + } > + > + backend = kzalloc(sizeof(*backend), GFP_KERNEL); > + if (!backend) { > + ret = -ENOMEM; > + goto out; > + } > + > + backend->client = client; > + list_add(&backend->list, &smbus->backend_entry); > + > +out: > + mutex_unlock(&smbus->mutex); > + return ret; > +} > + > +static int unreg_i2c_target(struct i2c_client *client) > +{ > + struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter); > + struct smbus_backend *backend; > + bool found = false; > + > + if (!smbus) > + return -EINVAL; > + > + mutex_lock(&smbus->mutex); > + > + list_for_each_entry(backend, &smbus->backend_entry, list) { > + if (backend->client->addr == client->addr) { > + list_del(&backend->list); > + kfree(backend); > + found = true; > + break; > + } > + } > + > + mutex_unlock(&smbus->mutex); > + return found ? 0 : -ENODEV; > +} > +#endif /* CONFIG_I2C_SLAVE */ > + > +static const struct i2c_algorithm i3c_hub_smbus_algo = { > + .master_xfer = i3c_hub_smbus_port_adapter_xfer, > + .functionality = i3c_hub_smbus_funcs, > +#if IS_ENABLED(CONFIG_I2C_SLAVE) > + .reg_slave = reg_i2c_target, > + .unreg_slave = unreg_i2c_target, > +#endif > +}; > + > +static void i3c_hub_delayed_work(struct work_struct *work) > +{ > + struct i3c_hub *priv = > + container_of(work, typeof(*priv), delayed_work.work); > + struct device *dev = i3cdev_to_dev(priv->i3cdev); > + struct logical_bus *bus; > + struct i2c_adapter_group *smbus; > + int ret; > + int i; > + unsigned int reg_val = 0; > + > + /* record reg 81: tp hubnetwork connection setting */ > + ret = regmap_read(priv->regmap, I3C_HUB_TP_NET_CON_CONF, ®_val); > + if (ret) { > + dev_warn(dev, "Failed to read hubnetwork connection setting\n"); > + return; > + } > + > + ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, 0x00); > + if (ret) { > + dev_warn(dev, "Failed to close Target Port(s)\n"); > + return; > + } > + > + for (i = 0; i < priv->dev_info->n_ports; ++i) { > + bus = &priv->logical_bus[i]; > + if (bus->available) { > + ret = regmap_update_bits(priv->regmap, > + I3C_HUB_TP_NET_CON_CONF, > + GENMASK(bus->tp_id, bus->tp_id), > + bus->tp_map); > + if (ret) { > + dev_warn(dev, > + "Failed to open Target Port(s)\n"); > + return; > + } > + > + dev->of_node = bus->of_node; > + ret = i3c_hub_logic_register(&bus->controller, dev); > + if (ret) { > + dev_warn(dev, > + "Failed to register i3c controller - bus id:%i\n", > + i); > + return; > + } > + bus->registered = true; > + > + ret = regmap_update_bits(priv->regmap, > + I3C_HUB_TP_NET_CON_CONF, > + GENMASK(bus->tp_id, bus->tp_id), > + 0x00); > + if (ret) { > + dev_warn(dev, > + "Failed to close Target Port(s)\n"); > + return; > + } > + > + if (!priv->settings.tp[i].always_enable) > + reg_val &= ~GENMASK(bus->tp_id, bus->tp_id); > + } > + } > + > + /* update tp hubnetwork connection setting */ > + ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, reg_val); > + if (ret) { > + dev_warn(dev, "Failed to open Target Port(s)\n"); > + return; > + } > + > + ret = i3c_master_do_daa(priv->driving_master); > + if (ret) { > + dev_warn(dev, "Failed to run DAA\n"); > + return; > + } > + > + for (i = 0; i < priv->dev_info->n_ports; i++) { > + smbus = &priv->smbus_port_adapter[i]; > + if (!smbus->used) > + continue; > + > + if (!priv->settings.tp[i].poll_interval_ms) > + continue; > + > + schedule_delayed_work(&smbus->delayed_work_polling, > + msecs_to_jiffies(priv->settings.tp[i].poll_interval_ms)); > + } > +} > + > +static int send_smbus_target_data_to_backend(struct i2c_adapter_group *smbus, > + u8 address, u8 *local_buffer, > + u8 len) > +{ > +#if IS_ENABLED(CONFIG_I2C_SLAVE) > + struct smbus_backend *backend; > + struct i2c_client *client; > + int i, ret; > + u8 tmp; > + > + mutex_lock(&smbus->mutex); > + > + list_for_each_entry(backend, &smbus->backend_entry, list) { > + client = backend->client; > + if (client->addr == address >> 1) { > + mutex_unlock(&smbus->mutex); > + ret = i2c_slave_event(client, I2C_SLAVE_WRITE_REQUESTED, > + &address); > + if (ret) > + return ret; > + > + for (i = 0; i < len; i++) { > + ret = i2c_slave_event(client, > + I2C_SLAVE_WRITE_RECEIVED, > + &local_buffer[i]); > + if (ret) > + return ret; > + } > + > + return i2c_slave_event(client, I2C_SLAVE_STOP, &tmp); > + } > + } > + > + mutex_unlock(&smbus->mutex); > +#endif /* CONFIG_I2C_SLAVE */ > + return -ENXIO; > +} > + > +static int read_smbus_target_buffer_page(struct i2c_adapter_group *smbus, > + u8 target_buffer_page, u8 *address, > + u8 *local_buffer, u8 *len) > +{ > + struct i3c_hub *hub = smbus->priv; > + struct device *dev = i3cdev_to_dev(hub->i3cdev); > + u32 status; > + int ret; > + > + mutex_lock(&hub->page_mutex); > + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, target_buffer_page); > + > + ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_LENGTH, &status); > + if (ret) > + goto error; > + > + *len = status - 1; > + if (!*len) > + goto error; > + > + if (*len > I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE) { > + dev_warn_ratelimited(dev, > + "Received message too big for hub buffer\n"); > + ret = -EMSGSIZE; > + *len = 0; > + goto error; > + } > + > + ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_ADDRESS, &status); > + if (ret) > + goto error; > + > + *address = status; > + > + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TARGET_BUFF_DATA, > + local_buffer, *len); > + > +error: > + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00); > + mutex_unlock(&hub->page_mutex); > + return ret; > +} > + > +static int process_smbus_controller_status(struct i2c_adapter_group *smbus, > + u8 reg, u32 status) > +{ > + struct i3c_hub *hub = smbus->priv; > + int ret = 0; > + > + if (status & I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG) { > + smbus->status = status; > + ret = regmap_write(hub->regmap, reg, > + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG); > + complete(&smbus->completion); > + } > + > + return ret; > +} > + > +/** > + * Controller buffer page is determined by adding the first buffer page number to port > + * index multiplied by four. The two target buffer page numbers are determined the same > + * way but they are offset by 2 and 3 from the controller page. > + */ > +static int process_smbus_target_status(struct i2c_adapter_group *smbus, u8 reg, > + u32 status) > +{ > + struct i3c_hub *hub = smbus->priv; > + struct device *dev = i3cdev_to_dev(hub->i3cdev); > + u8 controller_buffer_page = > + I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * smbus->tp_port; > + u8 local_buffer[I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 }; > + u8 target_buffer_page, address = 0, len = 0, flag; > + int ret; > + > + if (smbus->last_processed_buf) > + status &= ~smbus->last_processed_buf; > + > + if (status & I3C_HUB_TARGET_BUF_0_RECEIVE) { > + target_buffer_page = controller_buffer_page + 2; > + flag = I3C_HUB_TARGET_BUF_0_RECEIVE; > + } else if (status & I3C_HUB_TARGET_BUF_1_RECEIVE) { > + target_buffer_page = controller_buffer_page + 3; > + flag = I3C_HUB_TARGET_BUF_1_RECEIVE; > + } else { > + return -EINVAL; > + } > + > + ret = read_smbus_target_buffer_page(smbus, target_buffer_page, &address, > + local_buffer, &len); > + if (ret && ret != -EMSGSIZE) { > + dev_dbg(dev, "Failed to read target buffer page: %d\n", ret); > + return ret; > + } > + > + smbus->last_processed_buf = flag; > + > + if (status & I3C_HUB_TARGET_BUF_OVRFL) > + flag |= I3C_HUB_TARGET_BUF_OVRFL; > + > + ret = regmap_write(hub->regmap, reg, flag); > + if (ret) { > + dev_dbg(dev, "Failed to clear target port status\n"); > + return ret; > + } > + > + if (len) { > + ret = send_smbus_target_data_to_backend(smbus, address, > + local_buffer, len); > + if (ret) { > + dev_dbg(dev, "Failed to send data to backend: %d\n", > + ret); > + return ret; > + } > + } > + > + return 0; > +} > + > +static int i3c_hub_process_smbus_status(struct i2c_adapter_group *smbus) > +{ > + struct i3c_hub *hub = smbus->priv; > + u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + smbus->tp_port; > + struct device *dev = i3cdev_to_dev(hub->i3cdev); > + u32 status; > + int ret = 0; > + u32 poll_interval_ms = > + hub->settings.tp[smbus->tp_port].poll_interval_ms; > + > + ret = regmap_read(hub->regmap, target_port_status, &status); > + if (ret) > + return ret; > + > + /* smbus controller agent status */ > + if (!poll_interval_ms) { > + ret = process_smbus_controller_status(smbus, target_port_status, > + status); > + if (ret) > + dev_warn_ratelimited(dev, > + "Failed to process smbus controller status\n"); > + } > + > + /* smbus target agent status */ > + status &= I3C_HUB_TARGET_BUF_STATUS_MASK; > + > + while (status) { > + ret = process_smbus_target_status(smbus, target_port_status, > + status); > + if (ret) { > + dev_warn_ratelimited(dev, > + "Failed to process smbus target status: %d\n", > + ret); > + break; > + } > + > + if (!poll_interval_ms) > + break; > + > + ret = regmap_read(hub->regmap, target_port_status, &status); > + if (ret) > + break; > + status &= I3C_HUB_TARGET_BUF_STATUS_MASK; > + } > + > + return ret; > +} > + > +/** > + * i3c_hub_delayed_work_polling() - This delayed work is a polling mechanism to > + * find if any transaction happened. > + */ > +static void i3c_hub_delayed_work_polling(struct work_struct *work) > +{ > + struct i2c_adapter_group *smbus = > + container_of(work, typeof(*smbus), delayed_work_polling.work); > + struct device *dev = i3cdev_to_dev(smbus->priv->i3cdev); > + int ret; > + > + if (!list_empty(&smbus->backend_entry)) { > + ret = i3c_hub_process_smbus_status(smbus); > + if (ret) > + dev_warn_ratelimited(dev, > + "Failed to process TP %u smbus status: %d\n", > + smbus->tp_port, ret); > + } > + > + schedule_delayed_work(&smbus->delayed_work_polling, > + msecs_to_jiffies( > + smbus->priv->settings.tp[smbus->tp_port].poll_interval_ms)); > +} > + > +static int i3c_hub_smbus_ibi_handler(struct i3c_hub *hub, > + const struct i3c_ibi_payload *payload) > +{ > + struct i2c_adapter_group *smbus; > + u8 tp, tps; > + int val, ret = 0, rc; > + struct device *dev = i3cdev_to_dev(hub->i3cdev); > + > + if (payload->len < 2) { > + ret = regmap_read(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_IBI_STS, > + &val); > + if (ret) > + return ret; > + > + tps = (u8)val; > + } else { > + tps = ((const u8 *)payload->data)[1]; > + } > + > + if (!tps) > + return 0; > + > + while (tps) { > + tp = (u8)__ffs((unsigned long)tps); > + tps &= (tps - 1); > + > + if (hub->settings.tp[tp].poll_interval_ms) > + continue; > + > + smbus = &hub->smbus_port_adapter[tp]; > + rc = i3c_hub_process_smbus_status(smbus); > + if (rc) { > + dev_warn_ratelimited(dev, > + "Failed to process TP %u smbus status: %d\n", > + tp, rc); > + } > + } > + > + return 0; > +} > + > +/* > + * Sysfs attribute: clock_frequency > + * Read/Write the SMBus clock frequency for this adapter's port. > + */ > +static ssize_t clock_frequency_show(struct device *dev, > + struct device_attribute *attr, char *buf) > +{ > + struct i2c_adapter *adap = to_i2c_adapter(dev); > + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap); > + struct i3c_hub *hub = smbus->priv; > + > + return sprintf(buf, "%u\n", > + hub->settings.tp[smbus->tp_port].clock_frequency); > +} > + > +static ssize_t clock_frequency_store(struct device *dev, > + struct device_attribute *attr, > + const char *buf, size_t count) > +{ > + struct i2c_adapter *adap = to_i2c_adapter(dev); > + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap); > + struct i3c_hub *hub = smbus->priv; > + u32 val; > + int ret; > + > + ret = kstrtou32(buf, 0, &val); > + if (ret) > + return ret; > + > + if (!i3c_hub_smbus_validate_clock_frequency(val)) > + return -EINVAL; > + > + hub->settings.tp[smbus->tp_port].clock_frequency = val; > + > + return count; > +} > +static DEVICE_ATTR_RW(clock_frequency); > + > +static struct attribute *i3c_hub_smbus_attrs[] = { > + &dev_attr_clock_frequency.attr, > + NULL, > +}; > + > +ATTRIBUTE_GROUPS(i3c_hub_smbus); > + > +static int i3c_hub_smbus_tp_algo(struct i3c_hub *priv, int i) > +{ > + struct device *dev = i3cdev_to_dev(priv->i3cdev); > + int ret; > + struct i2c_adapter_group *smbus = &priv->smbus_port_adapter[i]; > + struct i2c_adapter *i2c = &smbus->i2c; > + > + mutex_init(&smbus->mutex); > + INIT_LIST_HEAD(&smbus->backend_entry); > + smbus->priv = priv; > + smbus->tp_port = i; > + smbus->tp_mask = BIT(i); > + > + init_completion(&smbus->completion); > + i2c->owner = THIS_MODULE; > + i2c->algo = &i3c_hub_smbus_algo; > + i2c->dev.parent = dev; > + i2c->dev.of_node = smbus->of_node; > + i2c->dev.groups = i3c_hub_smbus_groups; > + i2c->timeout = HZ; > + i2c->retries = 3; > + snprintf(i2c->name, sizeof(i2c->name), "hub%s.port%d", dev_name(dev), > + smbus->tp_port); > + > + if (priv->settings.tp[i].poll_interval_ms) { > + ret = regmap_clear_bits(priv->regmap, I3C_HUB_TP_IBI_CONF, > + smbus->tp_mask); > + if (ret) > + return ret; > + INIT_DELAYED_WORK(&smbus->delayed_work_polling, > + i3c_hub_delayed_work_polling); > + priv->smbus_ibi_en_mask &= ~smbus->tp_mask; > + } else { > + ret = regmap_set_bits(priv->regmap, I3C_HUB_TP_IBI_CONF, > + smbus->tp_mask); > + if (ret) > + return ret; > + priv->smbus_ibi_en_mask |= smbus->tp_mask; > + } > + > + /* Enable SDA hold-low when both SMBus Target Agent buffers are full. > + * Used as a flow-control mechanism for MCTP to avoid upstream TX timeout > + * when target buffers are not serviced in time. > + */ > + ret = regmap_set_bits(priv->regmap, > + I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF, > + TARGET_AGENT_BUF_FULL_SDA_LOW_EN); > + if (ret) > + return ret; > + > + i2c_set_adapdata(i2c, smbus); > + > + ret = i2c_add_adapter(i2c); > + if (ret) > + return ret; > + > + smbus->used = 1; > + return ret; > +} > + > +static int i3c_hub_gpio_direction_input(struct gpio_chip *gc, unsigned int off) > +{ > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + int ret = 0; > + u8 reg, mask = 0; > + > + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN : > + I3C_HUB_TP_SCL_OUT_EN; > + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]); > + > + ret = regmap_update_bits(hub->regmap, reg, mask, 0); > + return ret; > +} > + > +static int i3c_hub_gpio_direction_output(struct gpio_chip *gc, unsigned int off, > + int val) > +{ > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + int ret = 0; > + u8 reg, mask = 0; > + > + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN : > + I3C_HUB_TP_SCL_OUT_EN; > + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]); > + > + ret = regmap_update_bits(hub->regmap, reg, mask, mask); > + if (ret) > + return ret; > + > + ret = regmap_update_bits(hub->regmap, reg + 2, mask, val ? mask : 0); > + return ret; > +} > + > +static int i3c_hub_gpio_get_value(struct gpio_chip *gc, unsigned int off) > +{ > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + int ret = 0, val = 0, dir; > + u8 reg, shift = 0; > + > + dir = gc->get_direction(gc, off); > + if (dir) > + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_LEVEL_STS : > + I3C_HUB_TP_SCL_IN_LEVEL_STS; > + else > + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL : > + I3C_HUB_TP_SCL_OUT_LEVEL; > + > + shift = gpio->tp[off / GPIO_BANK_SZ]; > + > + ret = regmap_read(hub->regmap, reg, &val); > + if (ret) > + return ret; > + > + ret = (val >> shift) & 0x01; > + return ret; > +} > + > +static int i3c_hub_gpio_set_value(struct gpio_chip *gc, unsigned int off, int val) > +{ > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + u8 reg, mask = 0; > + > + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL : > + I3C_HUB_TP_SCL_OUT_LEVEL; > + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]); > + > + return regmap_update_bits(hub->regmap, reg, mask, val ? mask : 0); > +} > + > +static int i3c_hub_gpio_get_direction(struct gpio_chip *gc, unsigned int off) > +{ > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + int ret = 0, dir = 0; > + u8 reg, shift = 0; > + > + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN : > + I3C_HUB_TP_SCL_OUT_EN; > + shift = gpio->tp[off / GPIO_BANK_SZ]; > + > + ret = regmap_read(hub->regmap, reg, &dir); > + if (ret) > + return ret; > + > + ret = ~(dir >> shift) & 0x01; > + return ret; > +} > + > +static void i3c_hub_gpio_irq_mask(struct irq_data *d) > +{ > + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + u8 reg, hwirq = 0, mask = 0; > + > + hwirq = irqd_to_hwirq(d); > + > + reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN : > + I3C_HUB_TP_SCL_IN_DETECT_IBI_EN; > + mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]); > + > + regmap_update_bits(hub->regmap, reg, mask, 0); > +} > + > +static void i3c_hub_gpio_irq_unmask(struct irq_data *d) > +{ > + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + u8 reg, hwirq = 0, mask = 0; > + > + hwirq = irqd_to_hwirq(d); > + > + reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN : > + I3C_HUB_TP_SCL_IN_DETECT_IBI_EN; > + mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]); > + > + regmap_update_bits(hub->regmap, reg, mask, mask); > +} > + > +static int i3c_hub_gpio_irq_set_type(struct irq_data *d, unsigned int flow_type) > +{ > + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + u8 hwirq = 0, mask = 0, val, tp, reg; > + int ret; > + > + if (!(flow_type & IRQ_TYPE_EDGE_BOTH)) { > + dev_err(&hub->i3cdev->dev, "irq %d: unsupported type %d\n", > + d->irq, flow_type); > + return -EINVAL; > + } > + > + hwirq = irqd_to_hwirq(d); > + tp = gpio->tp[hwirq / GPIO_BANK_SZ]; > + > + if (tp == 0 || tp == 1 || tp == 4 || tp == 5) { > + if (hwirq % GPIO_BANK_SZ) { > + mask = SDA0145_IO_IN_DET_CFG_MASK; > + val = SDA0145_IO_IN_DET_CFG(flow_type); > + reg = I3C_HUB_TP_SDA_IN_DETECT_FLG; > + } else { > + mask = SCL0145_IO_IN_DET_CFG_MASK; > + val = SCL0145_IO_IN_DET_CFG(flow_type); > + reg = I3C_HUB_TP_SCL_IN_DETECT_FLG; > + } > + } else { > + if (hwirq % GPIO_BANK_SZ) { > + mask = SDA2367_IO_IN_DET_CFG_MASK; > + val = SDA2367_IO_IN_DET_CFG(flow_type); > + reg = I3C_HUB_TP_SDA_IN_DETECT_FLG; > + } else { > + mask = SCL2367_IO_IN_DET_CFG_MASK; > + val = SCL2367_IO_IN_DET_CFG(flow_type); > + reg = I3C_HUB_TP_SCL_IN_DETECT_FLG; > + } > + } > + > + ret = regmap_write(hub->regmap, reg, BIT(tp)); > + if (ret) > + return ret; > + > + ret = regmap_update_bits(hub->regmap, I3C_HUB_TP_IN_DETECT_MODE_CONF, > + mask, val); > + return ret; > +} > + > +static void i3c_hub_gpio_irq_bus_lock(struct irq_data *d) > +{ > + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + > + mutex_lock(&gpio->irq_mutex); > +} > + > +static void i3c_hub_gpio_irq_bus_unlock(struct irq_data *d) > +{ > + struct gpio_chip *gc = irq_data_get_irq_chip_data(d); > + struct i3c_hub *hub = gpiochip_get_data(gc); > + struct hub_gpio *gpio = &hub->gpio; > + > + mutex_unlock(&gpio->irq_mutex); > +} > + > +static void i3c_hub_setup_gpio(struct i3c_hub *hub) > +{ > + struct hub_gpio *gpio = &hub->gpio; > + struct gpio_chip *gc = &gpio->chip; > + struct gpio_irq_chip *girq; > + > + gc->direction_input = i3c_hub_gpio_direction_input; > + gc->direction_output = i3c_hub_gpio_direction_output; > + gc->get = i3c_hub_gpio_get_value; > + gc->set = i3c_hub_gpio_set_value; > + gc->get_direction = i3c_hub_gpio_get_direction; > + gc->can_sleep = true; > + > + gc->base = -1; > + gc->ngpio = gpio->nums; > + gc->label = dev_name(&hub->i3cdev->dev); > + gc->parent = &hub->i3cdev->dev; > + gc->owner = THIS_MODULE; > + > + /* irq_chip support */ > + mutex_init(&gpio->irq_mutex); > + > + gpio->irq_chip.name = dev_name(&hub->i3cdev->dev); > + gpio->irq_chip.irq_mask = i3c_hub_gpio_irq_mask; > + gpio->irq_chip.irq_unmask = i3c_hub_gpio_irq_unmask; > + gpio->irq_chip.irq_set_type = i3c_hub_gpio_irq_set_type; > + gpio->irq_chip.irq_bus_lock = i3c_hub_gpio_irq_bus_lock; > + gpio->irq_chip.irq_bus_sync_unlock = i3c_hub_gpio_irq_bus_unlock; > + > + girq = &gpio->chip.irq; > + > + /* This will let us handle the parent IRQ in the driver */ > + girq->parent_handler = NULL; > + girq->num_parents = 0; > + girq->parents = NULL; > + girq->default_type = IRQ_TYPE_NONE; > + girq->handler = handle_simple_irq; > + girq->threaded = true; > + girq->first = 0; > + > + girq->chip = &gpio->irq_chip; > +} > + > +static void i3c_hub_io_ibi_handler(struct i3c_hub *hub, > + const struct i3c_ibi_payload *payload) > +{ > + struct hub_gpio *gpio = &hub->gpio; > + struct gpio_chip *gc = &gpio->chip; > + u8 level, hwirq, tmp; > + u8 pending[GPIO_BANK_SZ]; > + u8 tp[GPIO_BANK_SZ]; > + int i, irq, ret, index; > + > + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_OUT_EN, tp, > + GPIO_BANK_SZ); > + if (ret) { > + dev_err(&hub->i3cdev->dev, "Failed to read OUT_EN: %d\n", ret); > + return; > + } > + > + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_IN_DETECT_FLG, > + pending, GPIO_BANK_SZ); > + if (ret) { > + dev_err(&hub->i3cdev->dev, "Failed to read DETECT_FLG: %d\n", > + ret); > + return; > + } > + > + for (i = 0; i < GPIO_BANK_SZ; i++) { > + tmp = ~tp[i] & pending[i]; > + > + while (tmp) { > + level = __ffs(tmp); > + tmp &= ~(1 << level); > + > + /* Check if this port is in GPIO mode */ > + index = gpio->port_to_index[level]; > + if (index < 0) { > + /* Non-GPIO mode port, skip without clearing. > + * This can happen because IN_DETECT IBI enable is > + * configured in groups (e.g., TP0145/TP2367), not > + * per individual port. Simply skip - the flag is > + * harmless and will be overwritten by next detection. > + */ > + dev_dbg(&hub->i3cdev->dev, > + "IBI detect flag on non-GPIO port %d, skipping\n", > + level); > + continue; > + } > + > + hwirq = index * 2 + i; > + irq = irq_find_mapping(gc->irq.domain, hwirq); > + > + /* Clear the flag after processing */ > + regmap_write(hub->regmap, > + I3C_HUB_TP_SCL_IN_DETECT_FLG + i, > + BIT(level)); > + > + if (unlikely(irq <= 0)) { > + dev_warn_ratelimited(gc->parent, > + "unmapped interrupt %d\n", > + hwirq); > + } else { > + handle_nested_irq(irq); > + } > + } > + } > +} > + > +static void i3c_hub_ibi_handler(struct i3c_device *dev, > + const struct i3c_ibi_payload *payload) > +{ > + struct i3c_hub *priv = i3cdev_get_drvdata(dev); > + int ret, val = 0; > + u8 status = 0; > + > + if (!payload->len) { > + dev_dbg(&dev->dev, > + "Zero-length IBI payload, reading status register\n"); > + ret = regmap_read(priv->regmap, I3C_HUB_DEV_AND_IBI_STS, &val); > + if (ret) { > + dev_warn_ratelimited(&dev->dev, > + "Failed to read IBI status: %d\n", > + ret); > + return; > + } > + status = (u8)val; > + } else { > + if (!payload->data) { > + dev_warn_ratelimited(&dev->dev, > + "IBI payload data is NULL with len=%d\n", > + payload->len); > + return; > + } > + status = ((const u8 *)payload->data)[0]; > + } > + > + if (status & TP_IO_FLAG_STATUS) > + i3c_hub_io_ibi_handler(priv, payload); > + > + if (status & SMBUS_AGENT_EVENT_FLAG_STATUS) { > + ret = i3c_hub_smbus_ibi_handler(priv, payload); > + if (ret) { > + dev_warn_ratelimited(&dev->dev, > + "Failed to handle SMBus IBI: %d\n", > + ret); > + return; > + } > + } > +} > + > +static inline void i3c_hub_regmap_lock(void *ctx) > +{ > + mutex_lock(&i3c_hub_regmap_mutex); > +} > + > +static inline void i3c_hub_regmap_unlock(void *ctx) > +{ > + mutex_unlock(&i3c_hub_regmap_mutex); > +} > + > +static int i3c_hub_probe(struct i3c_device *i3cdev) > +{ > + const struct regmap_config i3c_hub_regmap_config = { > + .reg_bits = 8, > + .val_bits = 8, > + .lock = i3c_hub_regmap_lock, > + .unlock = i3c_hub_regmap_unlock, > + .lock_arg = NULL, > + }; > + struct device *dev = &i3cdev->dev; > + struct device_node *node = NULL; > + struct regmap *regmap; > + struct i3c_hub *priv; > + char hub_id[32]; > + int ret; > + int i; > + struct i3c_ibi_setup ibireq = {}; > + > + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); > + if (!priv) > + return -ENOMEM; > + > + priv->i3cdev = i3cdev; > + priv->driving_master = i3c_dev_get_master(i3cdev->desc); > + mutex_init(&priv->page_mutex); > + i3cdev_set_drvdata(i3cdev, priv); > + INIT_DELAYED_WORK(&priv->delayed_work, i3c_hub_delayed_work); > + i3c_hub_of_default_configuration(dev); > + > + regmap = devm_regmap_init_i3c(i3cdev, &i3c_hub_regmap_config); > + if (IS_ERR(regmap)) { > + ret = PTR_ERR(regmap); > + dev_err(dev, "Failed to register I3C HUB regmap\n"); > + return ret; > + } > + priv->regmap = regmap; > + > + priv->dev_info = i3c_hub_lookup_dev_info(priv); > + if (IS_ERR(priv->dev_info)) { > + ret = PTR_ERR(priv->dev_info); > + dev_err(dev, "Failed to lookup HUB dev info\n"); > + return ret; > + } > + > + sprintf(hub_id, "i3c-hub-%d-%llx", > + i3c_dev_get_master(i3cdev->desc)->bus.id, > + i3cdev->desc->info.pid); > + ret = i3c_hub_debugfs_init(priv, hub_id); > + if (ret) > + dev_dbg(dev, "Failed to initialized DebugFS.\n"); > + > + ret = i3c_hub_read_id(dev); > + if (ret) > + goto error; > + > + priv->hub_dt_sel_id = -1; > + priv->hub_dt_cp1_id = -1; > + if (priv->hub_pin_cp1_id >= 0 && priv->hub_pin_sel_id >= 0) > + /* Find hub node in DT matching HW ID or just first without ID provided in DT */ > + node = i3c_hub_get_dt_hub_node(dev->parent->of_node, priv); > + > + if (!node) { > + dev_info(dev, > + "No DT entry - running with hardware defaults.\n"); > + } else { > + of_node_get(node); > + i3c_hub_of_get_conf_static(dev, node); > + i3c_hub_of_get_conf_runtime(dev, node); > + of_node_put(node); > + } > + > + /* Unlock access to protected registers */ > + ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE, > + REGISTERS_UNLOCK_CODE); > + if (ret) { > + dev_err(dev, "Failed to unlock HUB's protected registers\n"); > + goto error; > + } > + > + /* Register logic for native smbus ports */ > + for (i = 0; i < priv->dev_info->n_ports; i++) { > + priv->smbus_port_adapter[i].used = 0; > + if (priv->settings.tp[i].mode == I3C_HUB_DT_TP_MODE_SMBUS) { > + ret = i3c_hub_smbus_tp_algo(priv, i); > + if (ret) > + dev_warn(dev, > + "Failed to setup SMBus adapter, port: %d\n", > + i); > + } > + } > + > + ret = i3c_hub_configure_hw(dev); > + if (ret) { > + dev_err(dev, "Failed to configure the HUB\n"); > + goto error; > + } > + > + /* Lock access to protected registers */ > + ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE, > + REGISTERS_LOCK_CODE); > + if (ret) { > + dev_err(dev, "Failed to lock HUB's protected registers\n"); > + goto error; > + } > + > + /* IBI */ > + ibireq.handler = i3c_hub_ibi_handler; > + ibireq.max_payload_len = IBI_MAX_PAYLOAD_LEN; > + ibireq.num_slots = IBI_SLOT_NUMS; > + > + ret = i3c_device_request_ibi(i3cdev, &ibireq); > + if (ret) { > + dev_err(dev, "Failed to requeset ibi!\n"); > + goto error; > + } > + > + ret = i3c_device_enable_ibi(i3cdev); > + if (ret) { > + dev_err(dev, "Failed to enable ibi!\n"); > + goto err_free_ibi; > + } > + > + /* TBD: Apply special/security lock here using DEV_CMD register */ > + > + if (priv->gpio.nums > 0) { > + i3c_hub_setup_gpio(priv); > + > + ret = devm_gpiochip_add_data(dev, &priv->gpio.chip, priv); > + if (ret) { > + dev_err(dev, "gpiochip add data fail!\n"); > + goto err_dis_ibi; > + } > + } > + > + schedule_delayed_work(&priv->delayed_work, msecs_to_jiffies(100)); > + > + return 0; > + > +err_dis_ibi: > + i3c_device_disable_ibi(i3cdev); > +err_free_ibi: > + i3c_device_free_ibi(i3cdev); > +error: > + debugfs_remove_recursive(priv->debug_dir); > + return ret; > +} > + > +static void i3c_hub_remove(struct i3c_device *i3cdev) > +{ > + struct i3c_hub *priv = i3cdev_get_drvdata(i3cdev); > + int i; > + > + i3c_device_disable_ibi(i3cdev); > + i3c_device_free_ibi(i3cdev); > + > + for (i = 0; i < priv->dev_info->n_ports; i++) { > + if (priv->smbus_port_adapter[i].used) { > + cancel_delayed_work_sync(&priv->smbus_port_adapter[i].delayed_work_polling); > + i2c_del_adapter(&priv->smbus_port_adapter[i].i2c); > + } > + > + if (priv->logical_bus[i].registered) > + i3c_master_unregister(&priv->logical_bus[i].controller); > + } > + > + cancel_delayed_work_sync(&priv->delayed_work); > + debugfs_remove_recursive(priv->debug_dir); > +} > + > +static struct i3c_driver i3c_hub = { > + .driver.name = "rts490xa-i3c-hub", > + .id_table = i3c_hub_ids, > + .probe = i3c_hub_probe, > + .remove = i3c_hub_remove, > +}; > + > +module_i3c_driver(i3c_hub); > + > +MODULE_DESCRIPTION("RTS490XA I3C HUB driver"); > +MODULE_LICENSE("GPL"); > -- > 2.34.1 > -- Alexandre Belloni, co-owner and COO, Bootlin Embedded Linux and Kernel engineering https://bootlin.com From krzk at kernel.org Mon May 25 06:58:52 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Mon, 25 May 2026 15:58:52 +0200 Subject: [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB In-Reply-To: <20260525125128.297-1-zain_zhou@realsil.com.cn> References: <20260525125128.297-1-zain_zhou@realsil.com.cn> Message-ID: <64a8ba7e-f044-4f51-8619-9419ec7dfc27@kernel.org> On 25/05/2026 14:51, zain_zhou at realsil.com.cn wrote: > From: Yin Zhou > > Add DT binding schema for Realtek RTS490x series I3C HUB devices. > > The binding describes configuration properties for: > - LDO enable/disable and voltage level per port group > - Pull-up resistance per port group > - IO driver strength per port > - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up, > IO mode, SMBus clock frequency and polling interval > - Hub network always-I3C mode > - Hardware identification via CSEL pin (id) and CP1 pins (id-cp1) > > Signed-off-by: Yin Zhou > > Changes in v2: Messed patch. DCO is always the last. And this should tell you that changelog does not belong here - goes under ---. Anyway, did you test the binding this time or we expect another failures? > - Rework binding per Krzysztof Kozlowski's review: > add realtek, vendor prefix to all custom properties; use boolean > for enable flags; use u32 with unit suffixes (-microvolt, -ohms) > for voltage/resistance; change to unevaluatedProperties: false; > fix title, maintainer name, description, $nodename pattern > - Consolidate examples; add dt-bindings/i2c/i2c.h include > --- > .../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 263 ++++++++++++++++++ > 1 file changed, 263 insertions(+) > create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > > diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > new file mode 100644 > index 000000000000..851a433abcd3 > --- /dev/null > +++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > @@ -0,0 +1,263 @@ > +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) > +%YAML 1.2 > +--- > +$id: http://devicetree.org/schemas/i3c/realtek,rts490x-i3c-hub.yaml# > +$schema: http://devicetree.org/meta-schemas/core.yaml# > + > +title: Realtek RTS490x I3C HUB > + > +maintainers: > + - Yin Zhou > + > +description: > + The Realtek RTS490x is an I3C HUB device that provides voltage level > + translation between I3C controller and target devices, bus capacitance > + isolation, address conflict isolation, I3C port expansion (up to 8 > + target ports), simultaneous dual-controller port support, and per-port > + mode selection (I3C, SMBus, GPIO, or disabled). > + > +properties: > + $nodename: > + pattern: "^hub@[0-9a-f]+(,[0-9a-f]+)*$" > + > + compatible: > + const: realtek,rts490x-i3c-hub > + > + reg: > + maxItems: 1 > + description: > + Encodes the static I2C address, manufacturer ID, and part/instance ID > + as defined by the I3C specification. > + > + assigned-address: > + $ref: /schemas/types.yaml#/definitions/uint32 > + minimum: 0x1 > + maximum: 0xff > + description: > + Dynamic I3C address to assign to this device. > + > + dcr: Where is the property defined? Read carefully writing bindings doc and DTS101 slides. I finished review here. You still did not implement things we asked - to follow other bindings style. Best regards, Krzysztof From krzk at kernel.org Mon May 25 07:00:21 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Mon, 25 May 2026 16:00:21 +0200 Subject: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver In-Reply-To: <20260525125128.297-2-zain_zhou@realsil.com.cn> References: <20260525125128.297-1-zain_zhou@realsil.com.cn> <20260525125128.297-2-zain_zhou@realsil.com.cn> Message-ID: On 25/05/2026 14:51, zain_zhou at realsil.com.cn wrote: > From: Yin Zhou > > Add driver for Realtek RTS490x series I3C HUB devices. > > The driver supports: > - Device Tree based configuration of LDO, pull-up, IO strength > and per-port mode (I3C/SMBus/GPIO/disabled) > - Logical I3C bus registration per target port > - SMBus agent functionality with IBI and polling modes > - GPIO chip with IRQ support > - DebugFS interface for register access and DT config inspection > - IBI (In-Band Interrupt) handling > > The driver is placed in staging as it has known issues to be resolved > before mainlining; see drivers/staging/rts490x/TODO for details. > > Signed-off-by: Yin Zhou > > Changes in v2: > - Update driver to match v2 DT binding: parse physical values > directly via of_property_read_bool/u32; drop string enum lookup > tables; update property names with realtek, prefix and unit suffixes > - Fix maintainer name; move MAINTAINERS entry to driver patch > - Code style: rename TPn_* macros to TPN_*; rename SMBus frequency > constants to UPPER_CASE; add mutex field comments > --- > MAINTAINERS | 6 + > drivers/staging/Kconfig | 2 + > drivers/staging/Makefile | 1 + > drivers/staging/rts490x/Kconfig | 16 + > drivers/staging/rts490x/Makefile | 2 + > drivers/staging/rts490x/TODO | 35 + So this is staging driver? Then why do you send us bindings? We don't need them. You must follow staging rules then. Best regards, Krzysztof From jszhang at kernel.org Mon May 25 07:00:15 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 25 May 2026 22:00:15 +0800 Subject: [PATCH v4 0/3] i3c: dw: Add apb reset support Message-ID: <20260525140018.19598-1-jszhang@kernel.org> Add support of apb reset which is to reset the APB interface. The first patch is to document the exisiting reset dt-binding. 2nd patch is to add apb reset dt-binding. The last patch is to add apb reset support. NOTE: to align with "core_rst", the new added apb reset is named as "apb_rst", IOW, the reset names: "core_rst" + "apb_rst". I can modify it if maintainers prefer "core_rst" + "apb" Since v3: - back to v2, I.E keep the "_rst" suffix, since removing it is an ABI breakage. Since v2: - remove "_rst" suffix Since v1: - add dt-binding Jisheng Zhang (3): dt-bindings: i3c: dw: Describe core reset dt-bindings: i3c: dw: Add apb reset i3c: dw: Add apb reset support .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 10 ++++++++++ drivers/i3c/master/dw-i3c-master.c | 7 +++++++ drivers/i3c/master/dw-i3c-master.h | 1 + 3 files changed, 18 insertions(+) -- 2.53.0 From jszhang at kernel.org Mon May 25 07:00:16 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 25 May 2026 22:00:16 +0800 Subject: [PATCH v4 1/3] dt-bindings: i3c: dw: Describe core reset In-Reply-To: <20260525140018.19598-1-jszhang@kernel.org> References: <20260525140018.19598-1-jszhang@kernel.org> Message-ID: <20260525140018.19598-2-jszhang@kernel.org> The "core_rst" reset support has been in the code from day1, but the dt-binding doesn't exist. Add dt-binding to describe reset property. Then why not remove the "_rst" suffix? Krzysztof pointed out "Cleanup of name is not really worth affecting users. core_rst is not the best name but it is not incorrect, either." Signed-off-by: Jisheng Zhang --- .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index e803457d3f55..613dce7757bc 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -35,6 +35,14 @@ properties: - const: core - const: apb + resets: + items: + - description: Reset signal + + reset-names: + items: + - const: core_rst + interrupts: maxItems: 1 -- 2.53.0 From jszhang at kernel.org Mon May 25 07:00:17 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 25 May 2026 22:00:17 +0800 Subject: [PATCH v4 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <20260525140018.19598-1-jszhang@kernel.org> References: <20260525140018.19598-1-jszhang@kernel.org> Message-ID: <20260525140018.19598-3-jszhang@kernel.org> Add dt-binding for support of apb reset which is to reset the APB interface. Signed-off-by: Jisheng Zhang --- Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index 613dce7757bc..2575442b28ff 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -38,10 +38,12 @@ properties: resets: items: - description: Reset signal + - description: APB interface reset signal reset-names: items: - const: core_rst + - const: apb_rst interrupts: maxItems: 1 -- 2.53.0 From jszhang at kernel.org Mon May 25 07:00:18 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Mon, 25 May 2026 22:00:18 +0800 Subject: [PATCH v4 3/3] i3c: dw: Add apb reset support In-Reply-To: <20260525140018.19598-1-jszhang@kernel.org> References: <20260525140018.19598-1-jszhang@kernel.org> Message-ID: <20260525140018.19598-4-jszhang@kernel.org> Add support of apb reset which is to reset the APB interface. Signed-off-by: Jisheng Zhang --- drivers/i3c/master/dw-i3c-master.c | 7 +++++++ drivers/i3c/master/dw-i3c-master.h | 1 + 2 files changed, 8 insertions(+) diff --git a/drivers/i3c/master/dw-i3c-master.c b/drivers/i3c/master/dw-i3c-master.c index 655693a2187e..9de54d584bc3 100644 --- a/drivers/i3c/master/dw-i3c-master.c +++ b/drivers/i3c/master/dw-i3c-master.c @@ -1591,6 +1591,11 @@ int dw_i3c_common_probe(struct dw_i3c_master *master, if (IS_ERR(master->core_rst)) return PTR_ERR(master->core_rst); + master->apb_rst = devm_reset_control_get_optional_exclusive_deasserted(&pdev->dev, + "apb_rst"); + if (IS_ERR(master->apb_rst)) + return PTR_ERR(master->apb_rst); + spin_lock_init(&master->xferqueue.lock); INIT_LIST_HEAD(&master->xferqueue.list); @@ -1765,6 +1770,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->apb_rst); dw_i3c_master_disable_clks(master); pinctrl_pm_select_sleep_state(dev); return 0; @@ -1777,6 +1783,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->apb_rst); 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 c5cb695c16ab..a4ba60043288 100644 --- a/drivers/i3c/master/dw-i3c-master.h +++ b/drivers/i3c/master/dw-i3c-master.h @@ -37,6 +37,7 @@ struct dw_i3c_master { struct dw_i3c_master_caps caps; void __iomem *regs; struct reset_control *core_rst; + struct reset_control *apb_rst; struct clk *core_clk; struct clk *pclk; char version[5]; -- 2.53.0 From krzk at kernel.org Mon May 25 08:23:22 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Mon, 25 May 2026 17:23:22 +0200 Subject: [PATCH v4 1/3] dt-bindings: i3c: dw: Describe core reset In-Reply-To: <20260525140018.19598-2-jszhang@kernel.org> References: <20260525140018.19598-1-jszhang@kernel.org> <20260525140018.19598-2-jszhang@kernel.org> Message-ID: On 25/05/2026 16:00, Jisheng Zhang wrote: > The "core_rst" reset support has been in the code from day1, but the Reset cannot be supported and it is about ABI implemented by the driver. > dt-binding doesn't exist. Add dt-binding to describe reset property. > > Then why not remove the "_rst" suffix? Krzysztof pointed out "Cleanup > of name is not really worth affecting users. core_rst is not the best > name but it is not incorrect, either." This drop, irrelevant. You can provide Link: to previous discussions, OTOH. With above improved: Reviewed-by: Krzysztof Kozlowski Best regards, Krzysztof From krzk at kernel.org Mon May 25 08:25:45 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Mon, 25 May 2026 17:25:45 +0200 Subject: [PATCH v4 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <20260525140018.19598-3-jszhang@kernel.org> References: <20260525140018.19598-1-jszhang@kernel.org> <20260525140018.19598-3-jszhang@kernel.org> Message-ID: On 25/05/2026 16:00, Jisheng Zhang wrote: > Add dt-binding for support of apb reset which is to reset the APB > interface. And this is ABI break, so you must explain WHY breaking ABI is worth doing that or what is the impact. Additionally you should explain which devices have it. Does Altera have it? You really lack explanation WHY you are doing it and which hardware you exactly describe. > > Signed-off-by: Jisheng Zhang > --- > Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 2 ++ > 1 file changed, 2 insertions(+) > > diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > index 613dce7757bc..2575442b28ff 100644 > --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > @@ -38,10 +38,12 @@ properties: > resets: > items: > - description: Reset signal > + - description: APB interface reset signal > > reset-names: > items: > - const: core_rst > + - const: apb_rst apb > > interrupts: > maxItems: 1 Best regards, Krzysztof From robh at kernel.org Mon May 25 11:45:14 2026 From: robh at kernel.org (Rob Herring (Arm)) Date: Mon, 25 May 2026 13:45:14 -0500 Subject: [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB In-Reply-To: <20260525125128.297-1-zain_zhou@realsil.com.cn> References: <20260525125128.297-1-zain_zhou@realsil.com.cn> Message-ID: <177973471463.4085017.2026764711842557373.robh@kernel.org> On Mon, 25 May 2026 20:51:27 +0800, zain_zhou at realsil.com.cn wrote: > From: Yin Zhou > > Add DT binding schema for Realtek RTS490x series I3C HUB devices. > > The binding describes configuration properties for: > - LDO enable/disable and voltage level per port group > - Pull-up resistance per port group > - IO driver strength per port > - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up, > IO mode, SMBus clock frequency and polling interval > - Hub network always-I3C mode > - Hardware identification via CSEL pin (id) and CP1 pins (id-cp1) > > Signed-off-by: Yin Zhou > > Changes in v2: > - Rework binding per Krzysztof Kozlowski's review: > add realtek, vendor prefix to all custom properties; use boolean > for enable flags; use u32 with unit suffixes (-microvolt, -ohms) > for voltage/resistance; change to unevaluatedProperties: false; > fix title, maintainer name, description, $nodename pattern > - Consolidate examples; add dt-bindings/i2c/i2c.h include > --- > .../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 263 ++++++++++++++++++ > 1 file changed, 263 insertions(+) > create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml > My bot found errors running 'make dt_binding_check' on your patch: yamllint warnings/errors: dtschema/dtc warnings/errors: Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.example.dtb: /example-0/i3c-master at d040000/hub at 70,4ba00000000/target-port at 1/i2c at 10: failed to match any schema with compatible: ['i2c-slave-mqueue'] doc reference errors (make refcheckdocs): See https://patchwork.kernel.org/project/devicetree/patch/20260525125128.297-1-zain_zhou at realsil.com.cn The base for the series is generally the latest rc1. A different dependency should be noted in *this* patch. If you already ran 'make dt_binding_check' and didn't see the above error(s), then make sure 'yamllint' is installed and dt-schema is up to date: pip3 install dtschema --upgrade Please check and re-submit after running the above command yourself. Note that DT_SCHEMA_FILES can be set to your schema file to speed up checking your schema. However, it must be unset to test all examples with your schema. From gregkh at linuxfoundation.org Mon May 25 12:11:55 2026 From: gregkh at linuxfoundation.org (Greg KH) Date: Mon, 25 May 2026 21:11:55 +0200 Subject: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver In-Reply-To: <20260525125128.297-2-zain_zhou@realsil.com.cn> References: <20260525125128.297-1-zain_zhou@realsil.com.cn> <20260525125128.297-2-zain_zhou@realsil.com.cn> Message-ID: <2026052527-boat-avid-0a74@gregkh> On Mon, May 25, 2026 at 08:51:28PM +0800, zain_zhou at realsil.com.cn wrote: > From: Yin Zhou > > Add driver for Realtek RTS490x series I3C HUB devices. > > The driver supports: > - Device Tree based configuration of LDO, pull-up, IO strength > and per-port mode (I3C/SMBus/GPIO/disabled) > - Logical I3C bus registration per target port > - SMBus agent functionality with IBI and polling modes > - GPIO chip with IRQ support > - DebugFS interface for register access and DT config inspection > - IBI (In-Band Interrupt) handling > > The driver is placed in staging as it has known issues to be resolved > before mainlining; see drivers/staging/rts490x/TODO for details. No, again, please do this correctly and fix up everything and put it in the real part of the kernel. It will only take you more work if it were to be in staging. thanks, greg k-h From gregkh at linuxfoundation.org Mon May 25 12:12:51 2026 From: gregkh at linuxfoundation.org (Greg KH) Date: Mon, 25 May 2026 21:12:51 +0200 Subject: =?utf-8?B?562U5aSNOiBbUEFUQw==?= =?utf-8?Q?H?= 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver In-Reply-To: <1a79b9c3c6c64e5ba65292efa2f0472f@realsil.com.cn> References: <20260430121354.6253-1-zain_zhou@realsil.com.cn> <20260430121354.6253-2-zain_zhou@realsil.com.cn> <2026050412-bush-rosy-959d@gregkh> <1a79b9c3c6c64e5ba65292efa2f0472f@realsil.com.cn> Message-ID: <2026052533-flattop-carat-c489@gregkh> On Mon, May 25, 2026 at 12:58:33PM +0000, ?? wrote: > Thank you for the review. > > We noticed that the NXP P3H2x4x series (v10) introduces a generic > I3C hub framework. Would you recommend waiting for that to land > before resubmitting to the proper location? I have no context, and do not know, why not ask the i3c maintainer? greg k-h From jszhang at kernel.org Tue May 26 21:18:31 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Wed, 27 May 2026 12:18:31 +0800 Subject: [PATCH v4 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: References: <20260525140018.19598-1-jszhang@kernel.org> <20260525140018.19598-3-jszhang@kernel.org> Message-ID: On Mon, May 25, 2026 at 05:25:45PM +0200, Krzysztof Kozlowski wrote: > On 25/05/2026 16:00, Jisheng Zhang wrote: > > Add dt-binding for support of apb reset which is to reset the APB > > interface. > > And this is ABI break, so you must explain WHY breaking ABI is worth This just adds an optional apb reset, it doesn't break any exisiting ABI. Kindly let me know whether adding new optional binding is also an ABI break. > doing that or what is the impact. Additionally you should explain which > devices have it. Does Altera have it? You really lack explanation WHY > you are doing it and which hardware you exactly describe. I'm preparing one of synaptics SoCs support to uptream, it needs this apb reset signal for i3c. So you mean I delay this series until the SoC upstream series come, right? > > > > > Signed-off-by: Jisheng Zhang > > --- > > Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 2 ++ > > 1 file changed, 2 insertions(+) > > > > diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > > index 613dce7757bc..2575442b28ff 100644 > > --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > > +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml > > @@ -38,10 +38,12 @@ properties: > > resets: > > items: > > - description: Reset signal > > + - description: APB interface reset signal > > > > reset-names: > > items: > > - const: core_rst > > + - const: apb_rst > > apb > > > > > interrupts: > > maxItems: 1 > > > Best regards, > Krzysztof From jianqkang at sina.cn Tue May 26 23:19:33 2026 From: jianqkang at sina.cn (Jianqiang kang) Date: Wed, 27 May 2026 14:19:33 +0800 Subject: [PATCH 6.12.y] i3c: mipi-i3c-hci: Correct RING_CTRL_ABORT handling in DMA dequeue Message-ID: <20260527061933.3612126-1-jianqkang@sina.cn> From: Adrian Hunter [ Upstream commit b795e68bf3073d67bebbb5a44d93f49efc5b8cc7 ] The logic used to abort the DMA ring contains several flaws: 1. The driver unconditionally issues a ring abort even when the ring has already stopped. 2. The completion used to wait for abort completion is never re-initialized, resulting in incorrect wait behavior. 3. The abort sequence unintentionally clears RING_CTRL_ENABLE, which resets hardware ring pointers and disrupts the controller state. 4. If the ring is already stopped, the abort operation should be considered successful without attempting further action. Fix the abort handling by checking whether the ring is running before issuing an abort, re-initializing the completion when needed, ensuring that RING_CTRL_ENABLE remains asserted during abort, and treating an already stopped ring as a successful condition. Fixes: 9ad9a52cce282 ("i3c/master: introduce the mipi-i3c-hci driver") Cc: stable at vger.kernel.org Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260306072451.11131-9-adrian.hunter at intel.com Signed-off-by: Alexandre Belloni Signed-off-by: Jianqiang kang --- drivers/i3c/master/mipi-i3c-hci/dma.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index b9496e8c4784..44461f13b54c 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -457,16 +457,23 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_rh_data *rh = &rings->headers[xfer_list[0].ring_number]; unsigned int i; bool did_unqueue = false; - - /* stop the ring */ - rh_reg_write(RING_CONTROL, RING_CTRL_ABORT); - if (wait_for_completion_timeout(&rh->op_done, HZ) == 0) { - /* - * We're deep in it if ever this condition is ever met. - * Hardware might still be writing to memory, etc. - */ - dev_crit(&hci->master.dev, "unable to abort the ring\n"); - WARN_ON(1); + u32 ring_status; + + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* stop the ring */ + reinit_completion(&rh->op_done); + rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_ABORT); + wait_for_completion_timeout(&rh->op_done, HZ); + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* + * We're deep in it if ever this condition is ever met. + * Hardware might still be writing to memory, etc. + */ + dev_crit(&hci->master.dev, "unable to abort the ring\n"); + WARN_ON(1); + } } for (i = 0; i < n; i++) { -- 2.34.1 From jianqkang at sina.cn Tue May 26 23:20:39 2026 From: jianqkang at sina.cn (Jianqiang kang) Date: Wed, 27 May 2026 14:20:39 +0800 Subject: [PATCH 6.6.y] i3c: mipi-i3c-hci: Correct RING_CTRL_ABORT handling in DMA dequeue Message-ID: <20260527062039.3612331-1-jianqkang@sina.cn> From: Adrian Hunter [ Upstream commit b795e68bf3073d67bebbb5a44d93f49efc5b8cc7 ] The logic used to abort the DMA ring contains several flaws: 1. The driver unconditionally issues a ring abort even when the ring has already stopped. 2. The completion used to wait for abort completion is never re-initialized, resulting in incorrect wait behavior. 3. The abort sequence unintentionally clears RING_CTRL_ENABLE, which resets hardware ring pointers and disrupts the controller state. 4. If the ring is already stopped, the abort operation should be considered successful without attempting further action. Fix the abort handling by checking whether the ring is running before issuing an abort, re-initializing the completion when needed, ensuring that RING_CTRL_ENABLE remains asserted during abort, and treating an already stopped ring as a successful condition. Fixes: 9ad9a52cce282 ("i3c/master: introduce the mipi-i3c-hci driver") Cc: stable at vger.kernel.org Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260306072451.11131-9-adrian.hunter at intel.com Signed-off-by: Alexandre Belloni Signed-off-by: Jianqiang kang --- drivers/i3c/master/mipi-i3c-hci/dma.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 624d00b853a5..61007167606f 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -448,16 +448,23 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_rh_data *rh = &rings->headers[xfer_list[0].ring_number]; unsigned int i; bool did_unqueue = false; - - /* stop the ring */ - rh_reg_write(RING_CONTROL, RING_CTRL_ABORT); - if (wait_for_completion_timeout(&rh->op_done, HZ) == 0) { - /* - * We're deep in it if ever this condition is ever met. - * Hardware might still be writing to memory, etc. - */ - dev_crit(&hci->master.dev, "unable to abort the ring\n"); - WARN_ON(1); + u32 ring_status; + + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* stop the ring */ + reinit_completion(&rh->op_done); + rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_ABORT); + wait_for_completion_timeout(&rh->op_done, HZ); + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* + * We're deep in it if ever this condition is ever met. + * Hardware might still be writing to memory, etc. + */ + dev_crit(&hci->master.dev, "unable to abort the ring\n"); + WARN_ON(1); + } } for (i = 0; i < n; i++) { -- 2.34.1 From jianqkang at sina.cn Tue May 26 23:21:07 2026 From: jianqkang at sina.cn (Jianqiang kang) Date: Wed, 27 May 2026 14:21:07 +0800 Subject: [PATCH 6.1.y] i3c: mipi-i3c-hci: Correct RING_CTRL_ABORT handling in DMA dequeue Message-ID: <20260527062107.3612446-1-jianqkang@sina.cn> From: Adrian Hunter [ Upstream commit b795e68bf3073d67bebbb5a44d93f49efc5b8cc7 ] The logic used to abort the DMA ring contains several flaws: 1. The driver unconditionally issues a ring abort even when the ring has already stopped. 2. The completion used to wait for abort completion is never re-initialized, resulting in incorrect wait behavior. 3. The abort sequence unintentionally clears RING_CTRL_ENABLE, which resets hardware ring pointers and disrupts the controller state. 4. If the ring is already stopped, the abort operation should be considered successful without attempting further action. Fix the abort handling by checking whether the ring is running before issuing an abort, re-initializing the completion when needed, ensuring that RING_CTRL_ENABLE remains asserted during abort, and treating an already stopped ring as a successful condition. Fixes: 9ad9a52cce282 ("i3c/master: introduce the mipi-i3c-hci driver") Cc: stable at vger.kernel.org Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260306072451.11131-9-adrian.hunter at intel.com Signed-off-by: Alexandre Belloni Signed-off-by: Jianqiang kang --- drivers/i3c/master/mipi-i3c-hci/dma.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index e270fcd0f7c3..5616f3a9551b 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -448,16 +448,23 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_rh_data *rh = &rings->headers[xfer_list[0].ring_number]; unsigned int i; bool did_unqueue = false; - - /* stop the ring */ - rh_reg_write(RING_CONTROL, RING_CTRL_ABORT); - if (wait_for_completion_timeout(&rh->op_done, HZ) == 0) { - /* - * We're deep in it if ever this condition is ever met. - * Hardware might still be writing to memory, etc. - */ - dev_crit(&hci->master.dev, "unable to abort the ring\n"); - WARN_ON(1); + u32 ring_status; + + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* stop the ring */ + reinit_completion(&rh->op_done); + rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_ABORT); + wait_for_completion_timeout(&rh->op_done, HZ); + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* + * We're deep in it if ever this condition is ever met. + * Hardware might still be writing to memory, etc. + */ + dev_crit(&hci->master.dev, "unable to abort the ring\n"); + WARN_ON(1); + } } for (i = 0; i < n; i++) { -- 2.34.1 From krzk at kernel.org Wed May 27 00:53:16 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Wed, 27 May 2026 09:53:16 +0200 Subject: [PATCH v4 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: References: <20260525140018.19598-1-jszhang@kernel.org> <20260525140018.19598-3-jszhang@kernel.org> Message-ID: <4a8539d3-704b-4433-b4d6-e5acfe22d512@kernel.org> On 27/05/2026 06:18, Jisheng Zhang wrote: > On Mon, May 25, 2026 at 05:25:45PM +0200, Krzysztof Kozlowski wrote: >> On 25/05/2026 16:00, Jisheng Zhang wrote: >>> Add dt-binding for support of apb reset which is to reset the APB >>> interface. >> >> And this is ABI break, so you must explain WHY breaking ABI is worth > > This just adds an optional apb reset, it doesn't break any exisiting > ABI. Kindly let me know whether adding new optional binding is also > an ABI break. It does. One reset was before. Now all devices must have two resets. Clear ABI impact. > >> doing that or what is the impact. Additionally you should explain which >> devices have it. Does Altera have it? You really lack explanation WHY >> you are doing it and which hardware you exactly describe. > > I'm preparing one of synaptics SoCs support to uptream, it needs this > apb reset signal for i3c. So you mean I delay this series until the SoC > upstream series come, right? No. You just sent patch which basically says that Altera has two resets, without any explanation of that. Write explicit patches with answers WHY you re doing and WHY its impact is correct. Best regards, Krzysztof From jianqkang at sina.cn Wed May 27 01:37:47 2026 From: jianqkang at sina.cn (Jianqiang kang) Date: Wed, 27 May 2026 16:37:47 +0800 Subject: [PATCH 5.15.y] i3c: mipi-i3c-hci: Correct RING_CTRL_ABORT handling in DMA dequeue Message-ID: <20260527083747.3865086-1-jianqkang@sina.cn> From: Adrian Hunter [ Upstream commit b795e68bf3073d67bebbb5a44d93f49efc5b8cc7 ] The logic used to abort the DMA ring contains several flaws: 1. The driver unconditionally issues a ring abort even when the ring has already stopped. 2. The completion used to wait for abort completion is never re-initialized, resulting in incorrect wait behavior. 3. The abort sequence unintentionally clears RING_CTRL_ENABLE, which resets hardware ring pointers and disrupts the controller state. 4. If the ring is already stopped, the abort operation should be considered successful without attempting further action. Fix the abort handling by checking whether the ring is running before issuing an abort, re-initializing the completion when needed, ensuring that RING_CTRL_ENABLE remains asserted during abort, and treating an already stopped ring as a successful condition. Fixes: 9ad9a52cce282 ("i3c/master: introduce the mipi-i3c-hci driver") Cc: stable at vger.kernel.org Signed-off-by: Adrian Hunter Reviewed-by: Frank Li Link: https://patch.msgid.link/20260306072451.11131-9-adrian.hunter at intel.com Signed-off-by: Alexandre Belloni Signed-off-by: Jianqiang kang --- drivers/i3c/master/mipi-i3c-hci/dma.c | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/dma.c b/drivers/i3c/master/mipi-i3c-hci/dma.c index 28f40f805cb5..abfcf4902f01 100644 --- a/drivers/i3c/master/mipi-i3c-hci/dma.c +++ b/drivers/i3c/master/mipi-i3c-hci/dma.c @@ -448,16 +448,23 @@ static bool hci_dma_dequeue_xfer(struct i3c_hci *hci, struct hci_rh_data *rh = &rings->headers[xfer_list[0].ring_number]; unsigned int i; bool did_unqueue = false; - - /* stop the ring */ - rh_reg_write(RING_CONTROL, RING_CTRL_ABORT); - if (wait_for_completion_timeout(&rh->op_done, HZ) == 0) { - /* - * We're deep in it if ever this condition is ever met. - * Hardware might still be writing to memory, etc. - */ - dev_crit(&hci->master.dev, "unable to abort the ring\n"); - WARN_ON(1); + u32 ring_status; + + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* stop the ring */ + reinit_completion(&rh->op_done); + rh_reg_write(RING_CONTROL, RING_CTRL_ENABLE | RING_CTRL_ABORT); + wait_for_completion_timeout(&rh->op_done, HZ); + ring_status = rh_reg_read(RING_STATUS); + if (ring_status & RING_STATUS_RUNNING) { + /* + * We're deep in it if ever this condition is ever met. + * Hardware might still be writing to memory, etc. + */ + dev_crit(&hci->master.dev, "unable to abort the ring\n"); + WARN_ON(1); + } } for (i = 0; i < n; i++) { -- 2.34.1 From adrian.hunter at intel.com Wed May 27 03:47:40 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 13:47:40 +0300 Subject: [PATCH 6/8] i3c: master: Defer new-device registration out of DAA caller context In-Reply-To: References: <20260512121732.406009-1-adrian.hunter@intel.com> <20260512121732.406009-7-adrian.hunter@intel.com> <01df8e0e-9041-401b-ab73-634701c4acdc@intel.com> <417993a7-4a4f-4ba5-a815-aab63ed03a3c@intel.com> Message-ID: <3b0071bf-b82b-4a5e-8a3f-59a7ad165beb@intel.com> On 22/05/2026 21:59, Frank Li wrote: > On Fri, May 22, 2026 at 07:52:17AM +0300, Adrian Hunter wrote: >> On 21/05/2026 21:32, Frank Li wrote: >>> On Fri, May 15, 2026 at 07:42:20PM +0300, Adrian Hunter wrote: >>>> On 13/05/2026 22:03, Frank Li wrote: >>>>> On Wed, May 13, 2026 at 08:45:55AM +0300, Adrian Hunter wrote: >>>>>> On 12/05/2026 19:39, Frank Li wrote: >>>>>>> On Tue, May 12, 2026 at 03:17:30PM +0300, Adrian Hunter wrote: >>>>>>>> Master drivers may invoke i3c_master_do_daa_ext() during resume to >>>>>>>> re-run Dynamic Address Assignment. As well as assigning addresses to >>>>>>>> any newly arrived devices, this restores the dynamic address of devices >>>>>>>> that lost it across system suspend, so it has to run as part of the >>>>>>>> controller's resume path. >>>>>>>> >>>>>>>> A side effect of i3c_master_do_daa_ext() today is that it also >>>>>>>> registers any newly discovered I3C devices with the driver model >>>>>>>> inline, via i3c_master_register_new_i3c_devs(). Doing that from the >>>>>>>> resume path is problematic: a hot-join-capable device may join the bus >>>>>>>> during this same DAA, and registering it immediately would push driver >>>>>>>> model work (probing, sysfs, etc.) into the controller's resume context, >>>>>>>> where the rest of the system is not yet fully resumed and the >>>>>>>> controller driver is still partway through its own resume sequence. >>>>>>>> >>>>>>>> Decouple discovery from registration: add a reg_work work item to >>>>>>>> struct i3c_master_controller and have i3c_master_do_daa_ext() queue it >>>>>>>> on master->wq (the freezable workqueue) instead of calling >>>>>>>> i3c_master_register_new_i3c_devs() directly. The worker performs the >>>>>>>> registration only when the controller is not shutting_down, and is >>>>>>>> cancelled alongside hj_work in i3c_master_shutdown(). Because wq is >>>>>>>> freezable, any newly observed devices end up being registered after >>>>>>>> the system has finished resuming. >>>>>>>> >>>>>>>> i3c_master_register() also routes its initial post-bus-init registration >>>>>>>> through reg_work, using flush_work() to keep probe-time behavior >>>>>>>> synchronous. This keeps a single registration code path and ensures the >>>>>>>> worker is the only writer of desc->dev. >>>>>>> >>>>>>> why not direct use hj_work? >>>>>> >>>>>> i3c_master_register_new_i3c_devs() use of desc->dev is racy, so >>>>>> i3c_master_register_new_i3c_devs() must not be allowed to race >>>>>> with itself. Having it only ever run via reg_work achieves that. >>>>> >>>>> Sorry, I have not understand these, Can provide some detail? >>>> >>>> >From i3c_master_register_new_i3c_devs(): >>>> >>>> i3c_bus_for_each_i3cdev(&master->bus, desc) { >>>> if (desc->dev || !desc->info.dyn_addr || desc == master->this) >>>> continue; >>>> >>>> desc->dev = kzalloc_obj(*desc->dev); >>>> ... >>>> ret = device_register(&desc->dev->dev); >>>> >>>> This is done under the shared i3c_bus_normaluse_lock(), so there can >>> >>> i3c_bus_normaluse_lock() may is wrong, suppose it should be >>> i3c_bus_maintenance_lock(), register new devices change i3c bus's >>> hierarchical structure. >> >> If device_register() probes the device and the probe tries to >> access the device, won't it deadlock if i3c_bus_maintenance_lock() >> is held. > > Okay, > > Reviewed-by: Frank Li Thanks! If you have time, there is still patch 2 of the patch set to review ;-) https://lore.kernel.org/linux-i3c/20260518115520.98335-3-adrian.hunter at intel.com > >> >>> >>> Frank >>> >>>> be 2 or more instances of i3c_master_register_new_i3c_devs() running >>>> at the same time. They might all see desc->dev is NULL and then all >>>> of them try to initialize and register a dev for the same I3C device. >>>> >> From adrian.hunter at intel.com Wed May 27 04:27:52 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:52 +0300 Subject: [PATCH 1/7] i3c: mipi-i3c-hci: Fix race in i3c_hci_addr_to_dev() In-Reply-To: <20260527112758.38530-1-adrian.hunter@intel.com> References: <20260527112758.38530-1-adrian.hunter@intel.com> Message-ID: <20260527112758.38530-2-adrian.hunter@intel.com> i3c_hci_addr_to_dev() walks bus->devs.i3c, which is protected by bus.lock (rwsem). However, it is invoked from the MIPI I3C HCI IRQ handler, which cannot take bus.lock. This allows concurrent device addition/removal in the I3C core to modify the list while it is being traversed, potentially leading to use-after-free or crashes. Remove the dependency on the bus device list and introduce a dedicated lookup table. Add an ibi_devs[] array indexed by DAT entry, maintained under hci->lock. Update the array when IBIs are enabled or disabled, so that it always reflects the set of devices allowed to generate IBIs. Move i3c_hci_addr_to_dev() into core.c, reimplement it using the new array, and add a lockdep assertion to enforce that hci->lock is held by callers. Fixes: 9ad9a52cce282 ("i3c/master: introduce the mipi-i3c-hci driver") Signed-off-by: Adrian Hunter --- drivers/i3c/master/mipi-i3c-hci/core.c | 25 +++++++++++++++++++++++++ drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + drivers/i3c/master/mipi-i3c-hci/ibi.h | 13 +------------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index 53797841b63f..b19bdff5c5e2 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -22,6 +22,7 @@ #include "ext_caps.h" #include "cmd.h" #include "dat.h" +#include "ibi.h" /* * Host Controller Capabilities and Operation Registers @@ -124,6 +125,7 @@ static void i3c_hci_set_master_dyn_addr(struct i3c_hci *hci) static int i3c_hci_bus_init(struct i3c_master_controller *m) { struct i3c_hci *hci = to_i3c_hci(m); + struct device *dev = hci->master.dev.parent; struct i3c_device_info info; int ret; @@ -144,6 +146,10 @@ static int i3c_hci_bus_init(struct i3c_master_controller *m) if (ret) return ret; + hci->ibi_devs = devm_kcalloc(dev, hci->DAT_entries, sizeof(*hci->ibi_devs), GFP_KERNEL); + if (!hci->ibi_devs) + return -ENOMEM; + ret = hci->io->init(hci); if (ret) return ret; @@ -647,6 +653,21 @@ static void i3c_hci_free_ibi(struct i3c_dev_desc *dev) hci->io->free_ibi(hci, dev); } +struct i3c_dev_desc *i3c_hci_addr_to_dev(struct i3c_hci *hci, unsigned int addr) +{ + int dat_idx; + + lockdep_assert_held(&hci->lock); + + for (dat_idx = 0; dat_idx < hci->DAT_entries; dat_idx++) { + struct i3c_dev_desc *dev = hci->ibi_devs[dat_idx]; + + if (dev && dev->info.dyn_addr == addr) + return dev; + } + return NULL; +} + static int i3c_hci_enable_ibi(struct i3c_dev_desc *dev) { struct i3c_master_controller *m = i3c_dev_get_master(dev); @@ -654,6 +675,8 @@ static int i3c_hci_enable_ibi(struct i3c_dev_desc *dev) struct i3c_hci_dev_data *dev_data = i3c_dev_get_master_data(dev); 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); } @@ -664,6 +687,8 @@ static int i3c_hci_disable_ibi(struct i3c_dev_desc *dev) struct i3c_hci_dev_data *dev_data = i3c_dev_get_master_data(dev); mipi_i3c_hci_dat_v1.set_flags(hci, dev_data->dat_idx, DAT_0_SIR_REJECT, 0); + scoped_guard(spinlock_irqsave, &hci->lock) + hci->ibi_devs[dev_data->dat_idx] = NULL; return i3c_master_disec_locked(m, dev->info.dyn_addr, I3C_CCC_EVENT_SIR); } diff --git a/drivers/i3c/master/mipi-i3c-hci/hci.h b/drivers/i3c/master/mipi-i3c-hci/hci.h index 41d31a53abd3..b3d9803b1968 100644 --- a/drivers/i3c/master/mipi-i3c-hci/hci.h +++ b/drivers/i3c/master/mipi-i3c-hci/hci.h @@ -65,6 +65,7 @@ struct i3c_hci { unsigned int DAT_entry_size; void *DAT_data; struct dat_words *DAT; + struct i3c_dev_desc **ibi_devs; unsigned int DCT_entries; unsigned int DCT_entry_size; u8 version_major; diff --git a/drivers/i3c/master/mipi-i3c-hci/ibi.h b/drivers/i3c/master/mipi-i3c-hci/ibi.h index e1f98e264da0..073ca67b7d04 100644 --- a/drivers/i3c/master/mipi-i3c-hci/ibi.h +++ b/drivers/i3c/master/mipi-i3c-hci/ibi.h @@ -26,17 +26,6 @@ #define IBI_DATA_LENGTH GENMASK(7, 0) /* handy helpers */ -static inline struct i3c_dev_desc * -i3c_hci_addr_to_dev(struct i3c_hci *hci, unsigned int addr) -{ - struct i3c_bus *bus = i3c_master_get_bus(&hci->master); - struct i3c_dev_desc *dev; - - i3c_bus_for_each_i3cdev(bus, dev) { - if (dev->info.dyn_addr == addr) - return dev; - } - return NULL; -} +struct i3c_dev_desc *i3c_hci_addr_to_dev(struct i3c_hci *hci, unsigned int addr); #endif -- 2.51.0 From adrian.hunter at intel.com Wed May 27 04:27:51 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:51 +0300 Subject: [PATCH 0/7] i3c: Fix IBI race, address handling, and reconcile DAA Message-ID: <20260527112758.38530-1-adrian.hunter@intel.com> Hi Patches 1-2 fix a use-after-free race in the MIPI I3C HCI driver's IBI handling and make IBI teardown resilient to DISEC failures. Patches 3-7 fix address management issues in the I3C core and HCI driver that arise when Dynamic Address Assignment (DAA) does not complete cleanly, culminating in a reconciliation step that detects and resolves inconsistencies between assigned address slots and registered devices. Patches are based on top of: [PATCH V2 0/8] i3c: Hot-Join improvements and MIPI HCI Hot-Join support https://lore.kernel.org/linux-i3c/20260518115520.98335-1-adrian.hunter at intel.com/T which, in turn, applies on top of: [PATCH V4 00/17] i3c: mipi-i3c-hci: DMA abort, recovery and related improvements https://lore.kernel.org/linux-i3c/20260515162621.57719-1-adrian.hunter at intel.com/ Adrian Hunter (7): i3c: mipi-i3c-hci: Fix race in i3c_hci_addr_to_dev() i3c: mipi-i3c-hci: Ignore DISEC failures when disabling IBIs i3c: master: Prevent reuse of dynamic address on device add failure i3c: mipi-i3c-hci: Tolerate i3c_master_add_i3c_dev_locked() failures in DAA i3c: master: Make i3c_master_add_i3c_dev_locked() return void i3c: master: Move DAA API functions after i3c_master_add_i3c_dev_locked() i3c: master: Reconcile dynamic addresses after DAA drivers/i3c/master.c | 268 +++++++++++++++++++++---------- drivers/i3c/master/mipi-i3c-hci/cmd_v1.c | 4 +- drivers/i3c/master/mipi-i3c-hci/cmd_v2.c | 4 +- drivers/i3c/master/mipi-i3c-hci/core.c | 33 +++- drivers/i3c/master/mipi-i3c-hci/hci.h | 1 + drivers/i3c/master/mipi-i3c-hci/ibi.h | 13 +- include/linux/i3c/master.h | 3 +- 7 files changed, 224 insertions(+), 102 deletions(-) Regards Adrian From adrian.hunter at intel.com Wed May 27 04:27:53 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:53 +0300 Subject: [PATCH 2/7] i3c: mipi-i3c-hci: Ignore DISEC failures when disabling IBIs In-Reply-To: <20260527112758.38530-1-adrian.hunter@intel.com> References: <20260527112758.38530-1-adrian.hunter@intel.com> Message-ID: <20260527112758.38530-3-adrian.hunter@intel.com> Disabling IBIs currently returns the result of the DISEC CCC, causing i3c_hci_disable_ibi() to fail if the transfer errors out. However, the controller has already been programmed to reject IBIs by setting DAT_0_SIR_REJECT, so the target?s IBIs are effectively disabled from the host side regardless of the outcome of the DISEC command. At this point, teardown of the IBI infrastructure can safely proceed even if DISEC fails. Note, from then on, the MIPI I3C HCI not only NACKs the target's IBI but automatically sends another DISEC command. Make i3c_hci_disable_ibi() resilient by ignoring the return value of i3c_master_disec_locked() and always returning success. Signed-off-by: Adrian Hunter --- drivers/i3c/master/mipi-i3c-hci/core.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/core.c b/drivers/i3c/master/mipi-i3c-hci/core.c index b19bdff5c5e2..3ef7faed9957 100644 --- a/drivers/i3c/master/mipi-i3c-hci/core.c +++ b/drivers/i3c/master/mipi-i3c-hci/core.c @@ -689,7 +689,13 @@ static int i3c_hci_disable_ibi(struct i3c_dev_desc *dev) mipi_i3c_hci_dat_v1.set_flags(hci, dev_data->dat_idx, DAT_0_SIR_REJECT, 0); scoped_guard(spinlock_irqsave, &hci->lock) hci->ibi_devs[dev_data->dat_idx] = NULL; - return i3c_master_disec_locked(m, dev->info.dyn_addr, I3C_CCC_EVENT_SIR); + /* + * The DAT entry is now 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); + return 0; } static void i3c_hci_recycle_ibi_slot(struct i3c_dev_desc *dev, -- 2.51.0 From adrian.hunter at intel.com Wed May 27 04:27:54 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:54 +0300 Subject: [PATCH 3/7] i3c: master: Prevent reuse of dynamic address on device add failure In-Reply-To: <20260527112758.38530-1-adrian.hunter@intel.com> References: <20260527112758.38530-1-adrian.hunter@intel.com> Message-ID: <20260527112758.38530-4-adrian.hunter@intel.com> i3c_master_add_i3c_dev_locked() is called after a device has already been assigned a dynamic address. If the function fails, the address remains marked as free and may be reallocated to another device, leading to address conflicts on the bus. Mark the address as in use even on failure by updating the address slot state to prevent the address from being re-used. Emit an error message to inform of the failure. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index f87bf0099d3c..7820f58ab4aa 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -2345,12 +2345,9 @@ int i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, bool enable_ibi = false; int ret; - if (!master) - return -EINVAL; - newdev = i3c_master_alloc_i3c_dev(master, &info); if (IS_ERR(newdev)) - return PTR_ERR(newdev); + goto err_prevent_addr_reuse; ret = i3c_master_attach_i3c_dev(master, newdev); if (ret) @@ -2472,6 +2469,14 @@ int i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, err_free_dev: i3c_master_free_i3c_dev(newdev); +err_prevent_addr_reuse: + /* + * Although the device has not been added, the address has been + * assigned. Prevent the address from being used again. + */ + i3c_bus_set_addr_slot_status(&master->bus, addr, I3C_ADDR_SLOT_I3C_DEV); + dev_err(&master->dev, "Failed to add I3C device at address %u, error %d\n", addr, ret); + return ret; } EXPORT_SYMBOL_GPL(i3c_master_add_i3c_dev_locked); -- 2.51.0 From adrian.hunter at intel.com Wed May 27 04:27:55 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:55 +0300 Subject: [PATCH 4/7] i3c: mipi-i3c-hci: Tolerate i3c_master_add_i3c_dev_locked() failures in DAA In-Reply-To: <20260527112758.38530-1-adrian.hunter@intel.com> References: <20260527112758.38530-1-adrian.hunter@intel.com> Message-ID: <20260527112758.38530-5-adrian.hunter@intel.com> i3c_master_add_i3c_dev_locked() no longer leaves the address marked as free on failure, so aborting the DAA sequence on its error is unnecessary. Failure to register a discovered device does not invalidate the entire Dynamic Address Assignment (DAA) procedure. Align with the behavior of other I3C master drivers by ignoring errors from i3c_master_add_i3c_dev_locked() and continuing enumeration. Signed-off-by: Adrian Hunter --- drivers/i3c/master/mipi-i3c-hci/cmd_v1.c | 4 +--- drivers/i3c/master/mipi-i3c-hci/cmd_v2.c | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c b/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c index 75d452d7f6af..3b9345718d27 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c +++ b/drivers/i3c/master/mipi-i3c-hci/cmd_v1.c @@ -358,9 +358,7 @@ static int hci_cmd_v1_daa(struct i3c_hci *hci) * TODO: Extend the subsystem layer to allow for registering * new device and provide BCR/DCR/PID at the same time. */ - ret = i3c_master_add_i3c_dev_locked(&hci->master, next_addr); - if (ret) - break; + i3c_master_add_i3c_dev_locked(&hci->master, next_addr); } if (dat_idx >= 0) diff --git a/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c b/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c index 39eec26a363c..8d93748e858d 100644 --- a/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c +++ b/drivers/i3c/master/mipi-i3c-hci/cmd_v2.c @@ -296,9 +296,7 @@ static int hci_cmd_v2_daa(struct i3c_hci *hci) * TODO: Extend the subsystem layer to allow for registering * new device and provide BCR/DCR/PID at the same time. */ - ret = i3c_master_add_i3c_dev_locked(&hci->master, next_addr); - if (ret) - break; + i3c_master_add_i3c_dev_locked(&hci->master, next_addr); } hci_free_xfer(xfer, 2); -- 2.51.0 From adrian.hunter at intel.com Wed May 27 04:27:56 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:56 +0300 Subject: [PATCH 5/7] i3c: master: Make i3c_master_add_i3c_dev_locked() return void In-Reply-To: <20260527112758.38530-1-adrian.hunter@intel.com> References: <20260527112758.38530-1-adrian.hunter@intel.com> Message-ID: <20260527112758.38530-6-adrian.hunter@intel.com> The return value of i3c_master_add_i3c_dev_locked() is not used by any caller, and callers are not in a position to recover from failures in this path. Change the function to return void. Amend the kernel-doc accordingly, fix some grammar and remove a stale paragraph. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 17 ++++------------- include/linux/i3c/master.h | 3 +-- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 7820f58ab4aa..c1188c5d1f23 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -2324,19 +2324,12 @@ i3c_master_search_i3c_dev_duplicate(struct i3c_dev_desc *refdev) * @master: master used to send frames on the bus * @addr: I3C slave dynamic address assigned to the device * - * This function is instantiating an I3C device object and adding it to the - * I3C device list. All device information are automatically retrieved using - * standard CCC commands. - * - * The I3C device object is returned in case the master wants to attach - * private data to it using i3c_dev_set_master_data(). + * This function instantiates an I3C device object and adds it to the I3C device + * list. All device information is retrieved using standard CCC commands. * * This function must be called with the bus lock held in write mode. - * - * Return: a 0 in case of success, an negative error code otherwise. */ -int i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, - u8 addr) +void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr) { struct i3c_device_info info = { .dyn_addr = addr }; struct i3c_dev_desc *newdev, *olddev; @@ -2458,7 +2451,7 @@ int i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, mutex_unlock(&newdev->ibi_lock); } - return 0; + return; err_detach_dev: if (newdev->dev && newdev->dev->desc) @@ -2476,8 +2469,6 @@ int i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, */ i3c_bus_set_addr_slot_status(&master->bus, addr, I3C_ADDR_SLOT_I3C_DEV); dev_err(&master->dev, "Failed to add I3C device at address %u, error %d\n", addr, ret); - - return ret; } EXPORT_SYMBOL_GPL(i3c_master_add_i3c_dev_locked); diff --git a/include/linux/i3c/master.h b/include/linux/i3c/master.h index e2c831fb5354..f73cede87d36 100644 --- a/include/linux/i3c/master.h +++ b/include/linux/i3c/master.h @@ -615,8 +615,7 @@ int i3c_master_defslvs_locked(struct i3c_master_controller *master); int i3c_master_get_free_addr(struct i3c_master_controller *master, u8 start_addr); -int i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, - u8 addr); +void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr); int i3c_master_do_daa(struct i3c_master_controller *master); int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa); struct i3c_dma *i3c_master_dma_map_single(struct device *dev, void *ptr, -- 2.51.0 From adrian.hunter at intel.com Wed May 27 04:27:57 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:57 +0300 Subject: [PATCH 6/7] i3c: master: Move DAA API functions after i3c_master_add_i3c_dev_locked() In-Reply-To: <20260527112758.38530-1-adrian.hunter@intel.com> References: <20260527112758.38530-1-adrian.hunter@intel.com> Message-ID: <20260527112758.38530-7-adrian.hunter@intel.com> Relocate i3c_master_do_daa_ext() and i3c_master_do_daa() so they appear after i3c_master_add_i3c_dev_locked(). This ordering is required for upcoming changes where the DAA flow will (indirectly) rely on i3c_master_add_i3c_dev_locked() functionality. Reordering avoids forward dependency issues and keeps related code paths logically arranged. No functional change. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 128 +++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index c1188c5d1f23..69e19c56f9a5 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -1870,70 +1870,6 @@ static void i3c_master_reg_work_fn(struct work_struct *work) i3c_bus_normaluse_unlock(&master->bus); } -/** - * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) - * @master: controller - * @rstdaa: whether to first perform Reset of Dynamic Addresses (RSTDAA) - * - * Perform Dynamic Address Assignment with optional support for System - * Hibernation (@rstdaa is true). - * - * After System Hibernation, Dynamic Addresses can have been reassigned at boot - * time to different values. A simple strategy is followed to handle that. - * Perform a Reset of Dynamic Addresses (RSTDAA) followed by the normal DAA - * procedure which has provision for reassigning addresses that differ from the - * previously recorded addresses. - * - * Return: a 0 in case of success, an negative error code otherwise. - */ -int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) -{ - int rstret = 0; - int ret; - - ret = i3c_master_rpm_get(master); - if (ret) - return ret; - - i3c_bus_maintenance_lock(&master->bus); - - if (master->shutting_down) { - ret = -ENODEV; - } else { - if (rstdaa) - rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); - ret = master->ops->do_daa(master); - } - - i3c_bus_maintenance_unlock(&master->bus); - - if (ret) - goto out; - - queue_work(master->wq, &master->reg_work); -out: - i3c_master_rpm_put(master); - - return rstret ?: ret; -} -EXPORT_SYMBOL_GPL(i3c_master_do_daa_ext); - -/** - * i3c_master_do_daa() - do a DAA (Dynamic Address Assignment) - * @master: master doing the DAA - * - * This function instantiates I3C device objects and adds them to the - * I3C device list. All device information is automatically retrieved using - * standard CCC commands. - * - * Return: a 0 in case of success, an negative error code otherwise. - */ -int i3c_master_do_daa(struct i3c_master_controller *master) -{ - return i3c_master_do_daa_ext(master, false); -} -EXPORT_SYMBOL_GPL(i3c_master_do_daa); - /** * i3c_master_dma_map_single() - Map buffer for single DMA transfer * @dev: device object of a device doing DMA @@ -2472,6 +2408,70 @@ void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr } EXPORT_SYMBOL_GPL(i3c_master_add_i3c_dev_locked); +/** + * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) + * @master: controller + * @rstdaa: whether to first perform Reset of Dynamic Addresses (RSTDAA) + * + * Perform Dynamic Address Assignment with optional support for System + * Hibernation (@rstdaa is true). + * + * After System Hibernation, Dynamic Addresses can have been reassigned at boot + * time to different values. A simple strategy is followed to handle that. + * Perform a Reset of Dynamic Addresses (RSTDAA) followed by the normal DAA + * procedure which has provision for reassigning addresses that differ from the + * previously recorded addresses. + * + * Return: a 0 in case of success, an negative error code otherwise. + */ +int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) +{ + int rstret = 0; + int ret; + + ret = i3c_master_rpm_get(master); + if (ret) + return ret; + + i3c_bus_maintenance_lock(&master->bus); + + if (master->shutting_down) { + ret = -ENODEV; + } else { + if (rstdaa) + rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); + ret = master->ops->do_daa(master); + } + + i3c_bus_maintenance_unlock(&master->bus); + + if (ret) + goto out; + + queue_work(master->wq, &master->reg_work); +out: + i3c_master_rpm_put(master); + + return rstret ?: ret; +} +EXPORT_SYMBOL_GPL(i3c_master_do_daa_ext); + +/** + * i3c_master_do_daa() - do a DAA (Dynamic Address Assignment) + * @master: master doing the DAA + * + * This function instantiates I3C device objects and adds them to the + * I3C device list. All device information is automatically retrieved using + * standard CCC commands. + * + * Return: a 0 in case of success, an negative error code otherwise. + */ +int i3c_master_do_daa(struct i3c_master_controller *master) +{ + return i3c_master_do_daa_ext(master, false); +} +EXPORT_SYMBOL_GPL(i3c_master_do_daa); + #define OF_I3C_REG1_IS_I2C_DEV BIT(31) static int -- 2.51.0 From adrian.hunter at intel.com Wed May 27 04:27:58 2026 From: adrian.hunter at intel.com (Adrian Hunter) Date: Wed, 27 May 2026 14:27:58 +0300 Subject: [PATCH 7/7] i3c: master: Reconcile dynamic addresses after DAA In-Reply-To: <20260527112758.38530-1-adrian.hunter@intel.com> References: <20260527112758.38530-1-adrian.hunter@intel.com> Message-ID: <20260527112758.38530-8-adrian.hunter@intel.com> After Dynamic Address Assignment (DAA), there may be cases where devices have been assigned dynamic addresses on the bus, but are not successfully registered in the device model. This can happen, for example, if errors occur during device addition, leaving the bus state and software state inconsistent. Introduce a reconciliation step to resolve such inconsistencies. Scan all address slots marked as I3C devices by the bus, and compare them against the set of devices currently registered. For any dynamic address that is marked occupied but has no corresponding i3c_dev_desc, probe for device presence using a GETSTATUS CCC. Retry the probe (with exponential backoff delay) to handle transient NACK conditions. If a device responds, register it via i3c_master_add_i3c_dev_locked(). Otherwise, free the address slot so it may be reused in future DAA operations. Note, i3c_master_add_i3c_dev_locked() may fail (again), in which case the dynamic address remains marked as occupied. A future DAA will try again. This also handles a corner case where a device is assigned a dynamic address but not successfully added, and subsequently loses that address (e.g. due to power management). If DAA is run again, the device may receive a new dynamic address while the old one remains marked as occupied. Repeated occurrences of this scenario could eventually exhaust the dynamic address space. The reconciliation step ensures that stale addresses are detected and freed, preventing address leakage. Signed-off-by: Adrian Hunter --- drivers/i3c/master.c | 114 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/drivers/i3c/master.c b/drivers/i3c/master.c index 69e19c56f9a5..9f91c46b52ef 100644 --- a/drivers/i3c/master.c +++ b/drivers/i3c/master.c @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -1613,6 +1614,56 @@ static int i3c_master_retrieve_dev_info(struct i3c_dev_desc *dev) return 0; } +static int i3c_master_getstatus_locked(struct i3c_master_controller *master, + u8 addr, u16 *status) +{ + struct i3c_ccc_getstatus *getstatus; + struct i3c_ccc_cmd_dest dest; + struct i3c_ccc_cmd cmd; + int ret; + + getstatus = i3c_ccc_cmd_dest_init(&dest, addr, sizeof(*getstatus)); + if (!getstatus) + return -ENOMEM; + + i3c_ccc_cmd_init(&cmd, true, I3C_CCC_GETSTATUS, &dest, 1); + ret = i3c_master_send_ccc_cmd_locked(master, &cmd); + if (ret) + goto out; + + if (dest.payload.len != sizeof(*getstatus)) { + ret = -EIO; + goto out; + } + + if (status) + *status = be16_to_cpu(getstatus->status); +out: + i3c_ccc_cmd_dest_cleanup(&dest); + + return ret; +} + +#define I3C_DEV_PROBE_INITIAL_DELAY_US 20 +#define I3C_DEV_PROBE_DELAY_FACTOR 2 +#define I3C_DEV_PROBE_CNT 5 + +static bool i3c_master_i3c_dev_present(struct i3c_master_controller *master, unsigned int addr) +{ + int delay = I3C_DEV_PROBE_INITIAL_DELAY_US; + + for (int i = 0; i < I3C_DEV_PROBE_CNT; i++) { + if (i) { + fsleep(delay); + delay *= I3C_DEV_PROBE_DELAY_FACTOR; + } + if (!i3c_master_getstatus_locked(master, addr, NULL)) + return true; + } + + return false; +} + static void i3c_master_put_i3c_addrs(struct i3c_dev_desc *dev) { struct i3c_master_controller *master = i3c_dev_get_master(dev); @@ -2256,22 +2307,25 @@ i3c_master_search_i3c_dev_duplicate(struct i3c_dev_desc *refdev) } /** - * i3c_master_add_i3c_dev_locked() - add an I3C slave to the bus + * __i3c_master_add_i3c_dev_locked() - add an I3C slave to the bus * @master: master used to send frames on the bus * @addr: I3C slave dynamic address assigned to the device + * @probe: probe to see if the device is really present at @addr * * This function instantiates an I3C device object and adds it to the I3C device * list. All device information is retrieved using standard CCC commands. * * This function must be called with the bus lock held in write mode. */ -void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr) +static void __i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, + u8 addr, bool probe) { struct i3c_device_info info = { .dyn_addr = addr }; struct i3c_dev_desc *newdev, *olddev; u8 old_dyn_addr = addr, expected_dyn_addr; struct i3c_ibi_setup ibireq = { }; bool enable_ibi = false; + bool no_dev = false; int ret; newdev = i3c_master_alloc_i3c_dev(master, &info); @@ -2282,6 +2336,18 @@ void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr if (ret) goto err_free_dev; + /* + * When a dynamic address is first assigned, there is no need to check + * whether it is still assigned, however, if adding the device fails, + * it will be attempted again later, at which point the address may + * have been lost (e.g. due to power management), so for that case, + * probe to see if the device is still present at the assigned address. + */ + if (probe && !i3c_master_i3c_dev_present(master, addr)) { + no_dev = true; + goto err_detach_dev; + } + ret = i3c_master_retrieve_dev_info(newdev); if (ret) goto err_detach_dev; @@ -2399,6 +2465,8 @@ void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr i3c_master_free_i3c_dev(newdev); err_prevent_addr_reuse: + if (no_dev) + return; /* * Although the device has not been added, the address has been * assigned. Prevent the address from being used again. @@ -2406,8 +2474,45 @@ void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr i3c_bus_set_addr_slot_status(&master->bus, addr, I3C_ADDR_SLOT_I3C_DEV); dev_err(&master->dev, "Failed to add I3C device at address %u, error %d\n", addr, ret); } + +/** + * i3c_master_add_i3c_dev_locked() - add an I3C slave to the bus + * @master: master used to send frames on the bus + * @addr: I3C slave dynamic address assigned to the device + * + * This function instantiates an I3C device object and adds it to the + * I3C device list. All device information is automatically retrieved using + * standard CCC commands. + * + * This function must be called with the bus lock held in write mode. + */ +void i3c_master_add_i3c_dev_locked(struct i3c_master_controller *master, u8 addr) +{ + __i3c_master_add_i3c_dev_locked(master, addr, false); +} EXPORT_SYMBOL_GPL(i3c_master_add_i3c_dev_locked); +static void i3c_master_reconcile_dyn_addrs(struct i3c_master_controller *master) +{ + DECLARE_BITMAP(dev_dyn_addrs, I2C_MAX_ADDR + 1); + enum i3c_addr_slot_status status; + struct i3c_dev_desc *desc; + + /* Mark all devices' dynamic addresses in the bitmap */ + bitmap_zero(dev_dyn_addrs, I2C_MAX_ADDR + 1); + i3c_bus_for_each_i3cdev(&master->bus, desc) + __set_bit(desc->info.dyn_addr, dev_dyn_addrs); + /* Reconcile the bitmap with the bus address slot status */ + for (unsigned int addr = 0; addr <= I2C_MAX_ADDR; addr++) { + status = i3c_bus_get_addr_slot_status(&master->bus, addr); + if (status != I3C_ADDR_SLOT_I3C_DEV || test_bit(addr, dev_dyn_addrs)) + continue; + i3c_bus_set_addr_slot_status(&master->bus, addr, I3C_ADDR_SLOT_FREE); + /* Try to add the device, but probe to see if it is really present */ + __i3c_master_add_i3c_dev_locked(master, addr, true); + } +} + /** * i3c_master_do_daa_ext() - Dynamic Address Assignment (extended version) * @master: controller @@ -2441,6 +2546,11 @@ int i3c_master_do_daa_ext(struct i3c_master_controller *master, bool rstdaa) if (rstdaa) rstret = i3c_master_rstdaa_locked(master, I3C_BROADCAST_ADDR); ret = master->ops->do_daa(master); + /* + * Handle cases where a dynamic address was assigned but the + * device was not successfully added. + */ + i3c_master_reconcile_dyn_addrs(master); } i3c_bus_maintenance_unlock(&master->bus); -- 2.51.0 From dinguyen at kernel.org Wed May 27 10:42:20 2026 From: dinguyen at kernel.org (Dinh Nguyen) Date: Wed, 27 May 2026 12:42:20 -0500 Subject: [PATCHv2 2/2] arm64: dts: socfpga: agilex5: popuplate reset properties for I3C In-Reply-To: <20260527174221.79259-1-dinguyen@kernel.org> References: <20260527174221.79259-1-dinguyen@kernel.org> Message-ID: <20260527174221.79259-2-dinguyen@kernel.org> The I3C nodes are missing the resets and reset-name properties that are needed to bring the IP out of reset. Signed-off-by: Dinh Nguyen --- v2: add reset-names, remove reset property for NAND --- arch/arm64/boot/dts/intel/socfpga_agilex5.dtsi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/intel/socfpga_agilex5.dtsi b/arch/arm64/boot/dts/intel/socfpga_agilex5.dtsi index b06c6d5d60eed..fc169b869443b 100644 --- a/arch/arm64/boot/dts/intel/socfpga_agilex5.dtsi +++ b/arch/arm64/boot/dts/intel/socfpga_agilex5.dtsi @@ -244,6 +244,8 @@ i3c0: i3c at 10da0000 { reg = <0x10da0000 0x1000>; #address-cells = <3>; #size-cells = <0>; + resets = <&rst I3C0_RESET>; + reset-names = "core_rst"; interrupts = ; clocks = <&clkmgr AGILEX5_L4_MP_CLK>; status = "disabled"; @@ -255,6 +257,8 @@ i3c1: i3c at 10da1000 { reg = <0x10da1000 0x1000>; #address-cells = <3>; #size-cells = <0>; + resets = <&rst I3C1_RESET>; + reset-names = "core_rst"; interrupts = ; clocks = <&clkmgr AGILEX5_L4_MP_CLK>; status = "disabled"; -- 2.42.0.411.g813d9a9188 From dinguyen at kernel.org Wed May 27 10:42:19 2026 From: dinguyen at kernel.org (Dinh Nguyen) Date: Wed, 27 May 2026 12:42:19 -0500 Subject: [PATCHv2 1/2] dt-bindings: i3c: dw: add resets and reset-names Message-ID: <20260527174221.79259-1-dinguyen@kernel.org> The DW I3C driver is already getting the "core_rst" reset name, but it has not been documented. Signed-off-by: Dinh Nguyen --- v2: Added this patch --- .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml index e803457d3f554..7a39fe62bbbc0 100644 --- a/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml +++ b/Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml @@ -35,6 +35,14 @@ properties: - const: core - const: apb + resets: + items: + - description: Core reset signal + + reset-names: + items: + - const: core_rst + interrupts: maxItems: 1 -- 2.42.0.411.g813d9a9188 From sashal at kernel.org Wed May 27 12:49:10 2026 From: sashal at kernel.org (Sasha Levin) Date: Wed, 27 May 2026 15:49:10 -0400 Subject: [PATCH 6.12.y] i3c: mipi-i3c-hci: Correct RING_CTRL_ABORT handling in DMA dequeue In-Reply-To: <20260527061933.3612126-1-jianqkang@sina.cn> References: <20260527061933.3612126-1-jianqkang@sina.cn> Message-ID: <20260527-agent5-item018-i3c@kernel.org> > [ Upstream commit b795e68bf3073d67bebbb5a44d93f49efc5b8cc7 ] Queued for 6.12.y, 6.6.y, 6.1.y and 5.15.y, thanks. The 6.12 backport required dropping the upstream guard(mutex) usage (control_mutex doesn't exist on these branches); the rest of the change applied cleanly. -- Thanks, Sasha From krzk at kernel.org Thu May 28 00:20:49 2026 From: krzk at kernel.org (Krzysztof Kozlowski) Date: Thu, 28 May 2026 09:20:49 +0200 Subject: [PATCHv2 1/2] dt-bindings: i3c: dw: add resets and reset-names In-Reply-To: <20260527174221.79259-1-dinguyen@kernel.org> References: <20260527174221.79259-1-dinguyen@kernel.org> Message-ID: <03bc8ec6-714f-4dd0-b873-4994d547dcdd@kernel.org> On 27/05/2026 19:42, Dinh Nguyen wrote: > The DW I3C driver is already getting the "core_rst" reset name, but it > has not been documented. > > Signed-off-by: Dinh Nguyen > --- > v2: Added this patch > --- > .../devicetree/bindings/i3c/snps,dw-i3c-master.yaml | 8 ++++++++ > 1 file changed, 8 insertions(+) This was already sent by Jisheng, I believe. Best regards, Krzysztof From claudiu.beznea at kernel.org Thu May 28 01:29:46 2026 From: claudiu.beznea at kernel.org (Claudiu Beznea) Date: Thu, 28 May 2026 11:29:46 +0300 Subject: [PATCH 03/17] i3c: renesas: Restore STDBR and EXTBR registers on resume In-Reply-To: References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-4-claudiu.beznea@kernel.org> Message-ID: <560f3365-8a22-477b-ae3d-61e8f4103e83@kernel.org> Hi, Frank, On 5/22/26 22:10, Frank Li wrote: > On Fri, May 22, 2026 at 01:18:01PM +0300, Claudiu Beznea wrote: >> From: Claudiu Beznea >> >> The Renesas RZ/G3S supports a power saving state where power to the most >> SoC componentes (including I3C) is lost. >> >> The STDBR and EXTBR are configured in initialization phase though the >> struct i3c_master_controller_ops::bus_init. Set them on resume function >> as well to keep the same state of the controller after a suspend with >> power loss and a similar initialization sequence as in bus_init. >> >> Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") >> Cc: stable at vger.kernel.org >> Signed-off-by: Claudiu Beznea >> --- >> drivers/i3c/master/renesas-i3c.c | 10 ++++++---- >> 1 file changed, 6 insertions(+), 4 deletions(-) >> >> diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c >> index 1917549cf6d5..6c23f956ad2a 100644 >> --- a/drivers/i3c/master/renesas-i3c.c >> +++ b/drivers/i3c/master/renesas-i3c.c >> @@ -260,6 +260,7 @@ struct renesas_i3c { >> u32 dyn_addr; >> u32 i2c_STDBR; >> u32 i3c_STDBR; >> + u32 extbr; > > can you keep consisent with above 2 register, use upcase EXTBR, Using upper case for this will mislead the compiler. There is already a macro defined for EXTBR: #define EXTBR 0x78 Defining this member as: u32 EXTBR; will make the compiler try to replace the EXTBR in "i3c->EXTBR" at preprocessing time: CC [M] drivers/i3c/master/renesas-i3c.o ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier or ?(? before numeric constant 65 | #define EXTBR 0x78 | ^~~~ ../drivers/i3c/master/renesas-i3c.c:263:6: note: in expansion of macro ?EXTBR? 263 | u32 EXTBR; | ^~~~~ ../drivers/i3c/master/renesas-i3c.c: In function ?renesas_i3c_bus_init?: ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier before numeric constant 65 | #define EXTBR 0x78 | ^~~~ ../drivers/i3c/master/renesas-i3c.c:611:7: note: in expansion of macro ?EXTBR? 611 | i3c->EXTBR = EXTBR_EBRLO(od_low_ticks) | EXTBR_EBRHO(od_high_ticks) | | ^~~~~ ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier before numeric constant 65 | #define EXTBR 0x78 | ^~~~ ../drivers/i3c/master/renesas-i3c.c:613:40: note: in expansion of macro ?EXTBR? 613 | renesas_writel(i3c->regs, EXTBR, i3c->EXTBR); | ^~~~~ ../drivers/i3c/master/renesas-i3c.c: In function ?renesas_i3c_resume_noirq?: ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier before numeric constant 65 | #define EXTBR 0x78 | ^~~~ ../drivers/i3c/master/renesas-i3c.c:1451:40: note: in expansion of macro ?EXTBR? 1451 | renesas_writel(i3c->regs, EXTBR, i3c->EXTBR); | ^~~~~ The register contains both i3c and i2c specific fields. I'm not sure using i2c_i3c_EXTBR is the best way to go forward for this or just keeping it as is. -- Thank you, Claudiu From Frank.li at nxp.com Thu May 28 12:13:25 2026 From: Frank.li at nxp.com (Frank Li) Date: Thu, 28 May 2026 15:13:25 -0400 Subject: [PATCH 03/17] i3c: renesas: Restore STDBR and EXTBR registers on resume In-Reply-To: <560f3365-8a22-477b-ae3d-61e8f4103e83@kernel.org> References: <20260522101815.1722909-1-claudiu.beznea@kernel.org> <20260522101815.1722909-4-claudiu.beznea@kernel.org> <560f3365-8a22-477b-ae3d-61e8f4103e83@kernel.org> Message-ID: On Thu, May 28, 2026 at 11:29:46AM +0300, Claudiu Beznea wrote: > Hi, Frank, > > On 5/22/26 22:10, Frank Li wrote: > > On Fri, May 22, 2026 at 01:18:01PM +0300, Claudiu Beznea wrote: > > > From: Claudiu Beznea > > > > > > The Renesas RZ/G3S supports a power saving state where power to the most > > > SoC componentes (including I3C) is lost. > > > > > > The STDBR and EXTBR are configured in initialization phase though the > > > struct i3c_master_controller_ops::bus_init. Set them on resume function > > > as well to keep the same state of the controller after a suspend with > > > power loss and a similar initialization sequence as in bus_init. > > > > > > Fixes: e7218986319b ("i3c: renesas: Add suspend/resume support") > > > Cc: stable at vger.kernel.org > > > Signed-off-by: Claudiu Beznea > > > --- > > > drivers/i3c/master/renesas-i3c.c | 10 ++++++---- > > > 1 file changed, 6 insertions(+), 4 deletions(-) > > > > > > diff --git a/drivers/i3c/master/renesas-i3c.c b/drivers/i3c/master/renesas-i3c.c > > > index 1917549cf6d5..6c23f956ad2a 100644 > > > --- a/drivers/i3c/master/renesas-i3c.c > > > +++ b/drivers/i3c/master/renesas-i3c.c > > > @@ -260,6 +260,7 @@ struct renesas_i3c { > > > u32 dyn_addr; > > > u32 i2c_STDBR; > > > u32 i3c_STDBR; > > > + u32 extbr; > > > > can you keep consisent with above 2 register, use upcase EXTBR, > > Using upper case for this will mislead the compiler. There is already a > macro defined for EXTBR: That's fine. Reviewed-by: Frank Li > > #define EXTBR 0x78 > > Defining this member as: > > u32 EXTBR; > > will make the compiler try to replace the EXTBR in "i3c->EXTBR" at > preprocessing time: > > CC [M] drivers/i3c/master/renesas-i3c.o > ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier or ?(? > before numeric constant > 65 | #define EXTBR 0x78 > | ^~~~ > ../drivers/i3c/master/renesas-i3c.c:263:6: note: in expansion of macro ?EXTBR? > 263 | u32 EXTBR; > | ^~~~~ > ../drivers/i3c/master/renesas-i3c.c: In function ?renesas_i3c_bus_init?: > ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier before > numeric constant > 65 | #define EXTBR 0x78 > | ^~~~ > ../drivers/i3c/master/renesas-i3c.c:611:7: note: in expansion of macro ?EXTBR? > 611 | i3c->EXTBR = EXTBR_EBRLO(od_low_ticks) | EXTBR_EBRHO(od_high_ticks) | > | ^~~~~ > ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier before > numeric constant > 65 | #define EXTBR 0x78 > | ^~~~ > ../drivers/i3c/master/renesas-i3c.c:613:40: note: in expansion of macro ?EXTBR? > 613 | renesas_writel(i3c->regs, EXTBR, i3c->EXTBR); > | ^~~~~ > ../drivers/i3c/master/renesas-i3c.c: In function ?renesas_i3c_resume_noirq?: > ../drivers/i3c/master/renesas-i3c.c:65:17: error: expected identifier before > numeric constant > 65 | #define EXTBR 0x78 > | ^~~~ > ../drivers/i3c/master/renesas-i3c.c:1451:40: note: in expansion of macro ?EXTBR? > 1451 | renesas_writel(i3c->regs, EXTBR, i3c->EXTBR); > | ^~~~~ > > The register contains both i3c and i2c specific fields. I'm not sure using > i2c_i3c_EXTBR is the best way to go forward for this or just keeping it as > is. > > -- > Thank you, > Claudiu > From jszhang at kernel.org Sat May 30 05:58:35 2026 From: jszhang at kernel.org (Jisheng Zhang) Date: Sat, 30 May 2026 20:58:35 +0800 Subject: [PATCH v4 2/3] dt-bindings: i3c: dw: Add apb reset In-Reply-To: <4a8539d3-704b-4433-b4d6-e5acfe22d512@kernel.org> References: <20260525140018.19598-1-jszhang@kernel.org> <20260525140018.19598-3-jszhang@kernel.org> <4a8539d3-704b-4433-b4d6-e5acfe22d512@kernel.org> Message-ID: On Wed, May 27, 2026 at 09:53:16AM +0200, Krzysztof Kozlowski wrote: > On 27/05/2026 06:18, Jisheng Zhang wrote: > > On Mon, May 25, 2026 at 05:25:45PM +0200, Krzysztof Kozlowski wrote: > >> On 25/05/2026 16:00, Jisheng Zhang wrote: > >>> Add dt-binding for support of apb reset which is to reset the APB > >>> interface. > >> > >> And this is ABI break, so you must explain WHY breaking ABI is worth > > > > This just adds an optional apb reset, it doesn't break any exisiting > > ABI. Kindly let me know whether adding new optional binding is also > > an ABI break. > > It does. One reset was before. Now all devices must have two resets. > Clear ABI impact. I checked the dts with only one reset by manual dt_binding_check and dtbs_check, it still work, so I'm not sure why those dts "must have two resets". Kindly let me know what's wrong. > > > > >> doing that or what is the impact. Additionally you should explain which > >> devices have it. Does Altera have it? You really lack explanation WHY > >> you are doing it and which hardware you exactly describe. > > > > I'm preparing one of synaptics SoCs support to uptream, it needs this > > apb reset signal for i3c. So you mean I delay this series until the SoC > > upstream series come, right? > > No. You just sent patch which basically says that Altera has two resets, > without any explanation of that. I guess you mixed other series/patches with mine here, no? I didn't mention Altera at all ;) What my series mentioned is adding apb reset which is to reset the APB interface. > > Write explicit patches with answers WHY you re doing and WHY its impact > is correct. > > > Best regards, > Krzysztof From claudiu.beznea at tuxon.dev Sun May 31 07:50:54 2026 From: claudiu.beznea at tuxon.dev (Claudiu Beznea) Date: Sun, 31 May 2026 17:50:54 +0300 Subject: [PATCH v7 2/5] clk: at91: sama7d65: add peripheral clock for I3C In-Reply-To: <20260525092405.1514213-3-manikandan.m@microchip.com> References: <20260525092405.1514213-1-manikandan.m@microchip.com> <20260525092405.1514213-3-manikandan.m@microchip.com> Message-ID: <35c2ea3d-e257-43d9-8d02-4a111259fd5f@tuxon.dev> On 5/25/26 12:24, Manikandan Muralidharan wrote: > From: Durai Manickam KR > > Add peripheral clock description for I3C. > > Signed-off-by: Durai Manickam KR > Reviewed-by: Claudiu Beznea > Signed-off-by: Manikandan Muralidharan Applied to clk-microchip, thanks!