[PATCH v2 3/3] i2c: qcom-geni: Add bus recovery support for GPI DMA mode
From: Aniket Randive
Date: Fri Oct 23 2026 - 08:02:24 EST
I2C transfers in GPI DMA mode can fail with -EIO when a target holds
SDA low, leaving the bus stuck and preventing subsequent transactions.
The GPI completion callback reports a NACK and a genuine bus error
alike as -EIO, and the existing recovery path switches the SE to FIFO
mode which is not valid in GPI DMA mode.
Add bus recovery support using the I2C_BUS_CLEAR and I2C_STOP_ON_BUS
opcodes issued via the GPI TX DMA channel. Guard the -EIO recovery
trigger with a SE_GENI_IOS I2C_BUS_FREE check to skip recovery when both
SDA and SCL are already released. Extend the recovery callback to dispatch
through the GPI DMA path when gi2c->gpi_mode is set.
Use the adapter timeout for both FIFO and GPI recovery commands, and
preserve the DMA configuration error code when GPI setup fails.
Signed-off-by: Aniket Randive <aniket.randive@xxxxxxxxxxxxxxxx>
---
drivers/i2c/busses/i2c-qcom-geni.c | 104 ++++++++++++++++++++++++++++++++-----
1 file changed, 90 insertions(+), 14 deletions(-)
diff --git a/drivers/i2c/busses/i2c-qcom-geni.c b/drivers/i2c/busses/i2c-qcom-geni.c
index 065d7240de04..31f811bdc051 100644
--- a/drivers/i2c/busses/i2c-qcom-geni.c
+++ b/drivers/i2c/busses/i2c-qcom-geni.c
@@ -1034,7 +1034,7 @@ static int geni_i2c_fifo_bus_cmd(struct geni_i2c_dev *gi2c, u32 cmd)
reinit_completion(&gi2c->done);
geni_se_setup_m_cmd(&gi2c->se, cmd, 0);
- time_left = wait_for_completion_timeout(&gi2c->done, XFER_TIMEOUT);
+ time_left = wait_for_completion_timeout(&gi2c->done, gi2c->adap.timeout);
if (!time_left) {
dev_err(gi2c->se.dev, "timeout waiting for bus cmd %u\n", cmd);
geni_i2c_abort_xfer(gi2c);
@@ -1065,13 +1065,82 @@ static bool geni_i2c_needs_bus_recovery(struct geni_i2c_dev *gi2c, int ret)
* -EPROTO and -ETIMEDOUT indicate bus errors where the target may be
* holding a bus line low. ARB_LOST (-EAGAIN) on a single-controller
* bus indicates a stuck target, not a real arbitration loss.
- * GPI DMA mode extends this trigger separately.
+ * In GPI DMA mode the completion callback reports NACK, BUS_PROTO and
+ * ARB_LOST alike as -EIO, so the SE_GENI_IOS I2C_BUS_FREE check
+ * distinguishes a genuinely stuck bus from a benign NACK. Recovery is
+ * skipped when both SDA and SCL are released (high) after the STOP.
*/
- return !gi2c->gpi_mode &&
- (ret == -EPROTO || ret == -ETIMEDOUT || ret == -EAGAIN) &&
+ return (ret == -EPROTO || ret == -ETIMEDOUT || ret == -EAGAIN ||
+ (gi2c->gpi_mode && ret == -EIO)) &&
!geni_i2c_bus_is_free(gi2c);
}
+static int geni_i2c_gpi_bus_cmd(struct geni_i2c_dev *gi2c, enum i2c_op cmd)
+{
+ const struct geni_i2c_clk_fld *itr = gi2c->clk_fld;
+ struct dma_async_tx_descriptor *desc;
+ struct gpi_i2c_config peripheral = {};
+ struct dma_slave_config config = {};
+ unsigned long time_left;
+ unsigned long flags;
+ dma_cookie_t cookie;
+ int ret;
+
+ config.peripheral_config = &peripheral;
+ config.peripheral_size = sizeof(peripheral);
+
+ peripheral.set_config = 1;
+ peripheral.pack_enable = I2C_PACK_TX | I2C_PACK_RX;
+ peripheral.cycle_count = itr->t_cycle_cnt;
+ peripheral.high_count = itr->t_high_cnt;
+ peripheral.low_count = itr->t_low_cnt;
+ peripheral.clk_div = itr->clk_div;
+ peripheral.op = cmd;
+
+ ret = dmaengine_slave_config(gi2c->tx_c, &config);
+ if (ret) {
+ dev_err(gi2c->se.dev, "dma config error %d for bus cmd %u\n", ret, cmd);
+ return ret;
+ }
+
+ desc = dmaengine_prep_slave_single(gi2c->tx_c, 0, 0, DMA_MEM_TO_DEV,
+ DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
+ if (!desc)
+ return -EIO;
+
+ desc->callback_result = i2c_gpi_cb_result;
+ desc->callback_param = gi2c;
+
+ cookie = dmaengine_submit(desc);
+ if (dma_submit_error(cookie))
+ return -EIO;
+
+ /*
+ * cur and err are shared with geni_i2c_irq() and the GPI callback
+ * i2c_gpi_cb_result(); both write err (the IRQ handler under
+ * gi2c->lock). Reset them under the lock before issuing the transfer.
+ */
+ spin_lock_irqsave(&gi2c->lock, flags);
+ gi2c->cur = NULL;
+ gi2c->err = 0;
+ spin_unlock_irqrestore(&gi2c->lock, flags);
+ reinit_completion(&gi2c->done);
+ dma_async_issue_pending(gi2c->tx_c);
+
+ time_left = wait_for_completion_timeout(&gi2c->done, gi2c->adap.timeout);
+ if (!time_left) {
+ dev_err(gi2c->se.dev, "timeout waiting for GPI bus cmd %u\n", cmd);
+ dmaengine_terminate_sync(gi2c->tx_c);
+ return -ETIMEDOUT;
+ }
+
+ /* ARB_LOST and BUS_PROTO may be expected during recovery; treat as success */
+ if (gi2c->err == -EAGAIN || gi2c->err == -EPROTO)
+ return 0;
+
+ return gi2c->err;
+}
+
static int geni_i2c_recover_bus(struct i2c_adapter *adap)
{
struct geni_i2c_dev *gi2c = i2c_get_adapdata(adap);
@@ -1097,14 +1166,23 @@ static int geni_i2c_recover_bus(struct i2c_adapter *adap)
return -EBUSY;
}
- ret = geni_i2c_fifo_bus_cmd(gi2c, I2C_BUS_CLEAR);
- if (!ret)
- ret = geni_i2c_fifo_bus_cmd(gi2c, I2C_STOP_ON_BUS);
+ if (gi2c->gpi_mode) {
+ ret = geni_i2c_gpi_bus_cmd(gi2c, I2C_BUS_CLEAR);
+ if (ret)
+ dev_dbg(gi2c->se.dev, "GPI bus clear returned %d, issuing stop anyway\n",
+ ret);
+
+ ret = geni_i2c_gpi_bus_cmd(gi2c, I2C_STOP_ON_BUS);
+ } else {
+ ret = geni_i2c_fifo_bus_cmd(gi2c, I2C_BUS_CLEAR);
+ if (!ret)
+ ret = geni_i2c_fifo_bus_cmd(gi2c, I2C_STOP_ON_BUS);
+ }
/*
- * Recovery succeeds only once both bus lines are released. The bus
- * state is the authority, while a clean opcode status with either
- * line low is a failed recovery.
+ * Recovery succeeds when both bus lines are released. Trust the bus
+ * state over the opcode return path, which cannot flag a benign
+ * recovery event in GPI mode.
*/
if (geni_i2c_bus_is_free(gi2c))
ret = 0;
@@ -1351,10 +1429,8 @@ static int geni_i2c_probe(struct platform_device *pdev)
if (ret < 0)
return ret;
- if (!gi2c->gpi_mode) {
- gi2c->rinfo.recover_bus = geni_i2c_recover_bus;
- gi2c->adap.bus_recovery_info = &gi2c->rinfo;
- }
+ gi2c->rinfo.recover_bus = geni_i2c_recover_bus;
+ gi2c->adap.bus_recovery_info = &gi2c->rinfo;
ret = i2c_add_adapter(&gi2c->adap);
if (ret) {
--
2.34.1