[PATCH v2 2/3] i2c: qcom-geni: Add bus recovery support for FIFO mode
From: Aniket Randive
Date: Fri Oct 23 2026 - 07:59:19 EST
I2C transfers in FIFO mode can fail with -EPROTO, -ETIMEDOUT or
-EAGAIN when a target holds SDA low, leaving the bus stuck and
preventing subsequent transactions.
Add bus recovery support using the I2C_BUS_CLEAR and I2C_STOP_ON_BUS
hardware opcodes to restore the bus to an idle state. Check the SDA
line state through SE_GENI_IOS.RX_DATA_IN and skip recovery when the
bus is already free. Trigger recovery automatically from
geni_i2c_xfer() on -EPROTO, -ETIMEDOUT and -EAGAIN (arbitration-lost)
errors, and register the recovery callback through i2c_bus_recovery_info
to allow recovery via i2c_recover_bus().
This adds bus recovery support for FIFO mode only.
Signed-off-by: Aniket Randive <aniket.randive@xxxxxxxxxxxxxxxx>
---
drivers/i2c/busses/i2c-qcom-geni.c | 119 ++++++++++++++++++++++++++++++++++++-
1 file changed, 118 insertions(+), 1 deletion(-)
diff --git a/drivers/i2c/busses/i2c-qcom-geni.c b/drivers/i2c/busses/i2c-qcom-geni.c
index 60b6273f538c..065d7240de04 100644
--- a/drivers/i2c/busses/i2c-qcom-geni.c
+++ b/drivers/i2c/busses/i2c-qcom-geni.c
@@ -34,6 +34,7 @@
#define I2C_READ 0x2
#define I2C_WRITE_READ 0x3
#define I2C_ADDR_ONLY 0x4
+#define I2C_BUS_FREE (RX_DATA_IN | IO2_DATA_IN)
/* M_CMD params for I2C */
#define PRE_CMD_DELAY BIT(0)
#define TIMESTAMP_BEFORE BIT(1)
@@ -145,6 +146,7 @@ struct geni_i2c_dev {
u32 num_msgs;
struct geni_i2c_gpi_multi_desc_xfer i2c_multi_desc_config;
const struct geni_i2c_desc *dev_data;
+ struct i2c_bus_recovery_info rinfo;
};
struct geni_i2c_err_log {
@@ -316,7 +318,7 @@ static void geni_i2c_check_addr_data_nack(struct geni_i2c_dev *gi2c)
{
if (!readl_relaxed(gi2c->se.base + SE_GENI_M_GP_LENGTH))
geni_i2c_err(gi2c, ADDR_NACK);
- else if (!(gi2c->cur->flags & I2C_M_RD))
+ else if (gi2c->cur && !(gi2c->cur->flags & I2C_M_RD))
geni_i2c_err(gi2c, DATA_NACK);
}
@@ -1013,6 +1015,107 @@ static int geni_i2c_fifo_xfer(struct geni_i2c_dev *gi2c,
return num;
}
+static int geni_i2c_fifo_bus_cmd(struct geni_i2c_dev *gi2c, u32 cmd)
+{
+ unsigned long time_left;
+ unsigned long flags;
+
+ /*
+ * Clear cur so the IRQ handler does not attempt FIFO watermark
+ * filling or draining while the recovery opcode is in flight.
+ * cur and err are shared with geni_i2c_irq(), which reads cur and
+ * writes err under gi2c->lock, so take the lock around this reset.
+ */
+ spin_lock_irqsave(&gi2c->lock, flags);
+ gi2c->cur = NULL;
+ gi2c->err = 0;
+ spin_unlock_irqrestore(&gi2c->lock, flags);
+ geni_se_select_mode(&gi2c->se, GENI_SE_FIFO);
+ reinit_completion(&gi2c->done);
+
+ geni_se_setup_m_cmd(&gi2c->se, cmd, 0);
+ time_left = wait_for_completion_timeout(&gi2c->done, XFER_TIMEOUT);
+ if (!time_left) {
+ dev_err(gi2c->se.dev, "timeout waiting for bus cmd %u\n", cmd);
+ geni_i2c_abort_xfer(gi2c);
+
+ return -ETIMEDOUT;
+ }
+
+ /*
+ * ARB_LOST and BUS_PROTO interrupts may be reported while the bus
+ * transitions from stuck to idle during the recovery sequence.
+ * The opcode completed successfully so treat these as success.
+ */
+ if (gi2c->err == -EAGAIN || gi2c->err == -EPROTO)
+ return 0;
+
+ return gi2c->err;
+}
+
+static bool geni_i2c_bus_is_free(struct geni_i2c_dev *gi2c)
+{
+ return (readl_relaxed(gi2c->se.base + SE_GENI_IOS) & I2C_BUS_FREE) ==
+ I2C_BUS_FREE;
+}
+
+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.
+ */
+ return !gi2c->gpi_mode &&
+ (ret == -EPROTO || ret == -ETIMEDOUT || ret == -EAGAIN) &&
+ !geni_i2c_bus_is_free(gi2c);
+}
+
+static int geni_i2c_recover_bus(struct i2c_adapter *adap)
+{
+ struct geni_i2c_dev *gi2c = i2c_get_adapdata(adap);
+ u32 ios;
+ int ret;
+
+ ret = pm_runtime_get_sync(gi2c->se.dev);
+ if (ret < 0) {
+ dev_err(gi2c->se.dev, "bus recovery failed, error turning SE resources:%d\n", ret);
+ pm_runtime_put_noidle(gi2c->se.dev);
+ return ret;
+ }
+
+ if (geni_i2c_bus_is_free(gi2c)) {
+ pm_runtime_put_autosuspend(gi2c->se.dev);
+ return 0;
+ }
+
+ /* Bus clear cannot recover a target holding SCL low. */
+ ios = readl_relaxed(gi2c->se.base + SE_GENI_IOS);
+ if (!(ios & IO2_DATA_IN)) {
+ pm_runtime_put_autosuspend(gi2c->se.dev);
+ 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);
+
+ /*
+ * 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.
+ */
+ if (geni_i2c_bus_is_free(gi2c))
+ ret = 0;
+ else if (!ret)
+ ret = -EBUSY;
+
+ pm_runtime_put_autosuspend(gi2c->se.dev);
+
+ return ret;
+}
+
static int geni_i2c_xfer(struct i2c_adapter *adap,
struct i2c_msg msgs[],
int num)
@@ -1037,6 +1140,15 @@ static int geni_i2c_xfer(struct i2c_adapter *adap,
else
ret = geni_i2c_fifo_xfer(gi2c, msgs, num);
+ if (geni_i2c_needs_bus_recovery(gi2c, ret)) {
+ int recovery_ret = i2c_recover_bus(adap);
+
+ if (recovery_ret)
+ dev_err(gi2c->se.dev,
+ "bus recovery failed: %d (xfer error: %d)\n",
+ recovery_ret, ret);
+ }
+
out_pm:
pm_runtime_put_autosuspend(gi2c->se.dev);
gi2c->cur = NULL;
@@ -1239,6 +1351,11 @@ 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;
+ }
+
ret = i2c_add_adapter(&gi2c->adap);
if (ret) {
release_gpi_dma(gi2c);
--
2.34.1