* [PATCH v7 1/2] i2c: core: Add i2c_update_timeout() helper for dynamic transfer timeouts
2026-08-13 5:45 [PATCH v7 0/2] i2c: Add dynamic transfer timeout based on message length and frequency Aniket Randive
@ 2026-08-13 5:45 ` Aniket Randive
2026-08-24 6:19 ` Mukesh Savaliya
2026-08-13 5:45 ` [PATCH v7 2/2] i2c: qcom-geni: Add dynamic transfer timeout based on transfer length and frequency Aniket Randive
1 sibling, 1 reply; 7+ messages in thread
From: Aniket Randive @ 2026-08-13 5:45 UTC (permalink / raw)
To: Wolfram Sang, Andi Shyti, Mukesh Savaliya, Dmitry Guzman
Cc: Viken Dadhaniya, linux-i2c, linux-arm-msm, linux-kernel, Aniket Randive
The transfer timeout for an I2C controller should reflect the actual
message length and bus frequency rather than a static 1-second value.
A static timeout causes unnecessary delays on error paths for short
messages, and may be insufficient for very long transfers.
Add i2c_update_timeout() to i2c-core which computes a transfer-specific
timeout and stores it directly in the standard adap->timeout field. The
formula accounts for 9 bits per byte (8 data + 1 ACK) at the configured
bus frequency. The caller supplies a safety multiplier and a minimum
floor so that each driver retains full control over its timing policy
without those values becoming public API.
Storing the result in adap->timeout makes it visible to all consumers of
that field, including the arbitration-loss retry loop in __i2c_transfer().
The function is gated by CONFIG_I2C_DYNAMIC_TIMEOUT. When the config is
disabled, i2c_update_timeout() compiles to a no-op inline stub so drivers
that call it build cleanly and the existing static 1-second default is
preserved unchanged.
A timeout explicitly configured by userspace via the I2C_TIMEOUT ioctl is
stored in a new adap->user_timeout field and always takes precedence over
the kernel-computed value. When userspace has not configured a timeout,
the computed value is used.
Signed-off-by: Aniket Randive <aniket.randive@oss.qualcomm.com>
---
drivers/i2c/Kconfig | 13 +++++++++++++
drivers/i2c/i2c-core-base.c | 40 ++++++++++++++++++++++++++++++++++++++++
drivers/i2c/i2c-dev.c | 4 ++++
include/linux/i2c.h | 12 ++++++++++++
4 files changed, 69 insertions(+)
diff --git a/drivers/i2c/Kconfig b/drivers/i2c/Kconfig
index c232054fddd6..1d9691ec0e7e 100644
--- a/drivers/i2c/Kconfig
+++ b/drivers/i2c/Kconfig
@@ -136,6 +136,19 @@ config I2C_SLAVE_TESTUNIT
endif
+config I2C_DYNAMIC_TIMEOUT
+ bool "Dynamic per-transfer timeout based on message length"
+ depends on I2C
+ help
+ When enabled, the I2C core computes a per-transfer timeout from the
+ message length and bus frequency instead of using a static 1-second
+ default. A timeout explicitly configured via the I2C_TIMEOUT userspace
+ interface always takes precedence over the computed value.
+
+ When disabled, the existing static 1-second timeout is preserved.
+
+ If unsure, say N.
+
config I2C_DEBUG_CORE
bool "I2C Core debugging messages"
help
diff --git a/drivers/i2c/i2c-core-base.c b/drivers/i2c/i2c-core-base.c
index fb25704219c7..caa0748253ab 100644
--- a/drivers/i2c/i2c-core-base.c
+++ b/drivers/i2c/i2c-core-base.c
@@ -29,6 +29,7 @@
#include <linux/irq.h>
#include <linux/jump_label.h>
#include <linux/kernel.h>
+#include <linux/math64.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/of_device.h>
@@ -2001,6 +2002,45 @@ void i2c_parse_fw_timings(struct device *dev, struct i2c_timings *t, bool use_de
}
EXPORT_SYMBOL_GPL(i2c_parse_fw_timings);
+#ifdef CONFIG_I2C_DYNAMIC_TIMEOUT
+/**
+ * i2c_update_timeout - compute and set a dynamic transfer timeout on an adapter
+ * @adap: the i2c_adapter whose timeout field will be updated
+ * @bus_freq_hz: I2C bus clock frequency in Hz
+ * @len: transfer length in bytes
+ * @safety_coeff: multiplier applied over the theoretical wire time
+ * @min_usec: minimum timeout floor in microseconds
+ *
+ * Computes a transfer-specific timeout from the message length and bus
+ * frequency, applies a safety multiplier and a minimum floor, then stores
+ * the result in adap->timeout (in jiffies). The caller supplies the policy
+ * constants so they remain internal to the driver.
+ *
+ * A timeout explicitly configured by userspace via the I2C_TIMEOUT ioctl
+ * always takes precedence; the computed value is used only when userspace
+ * has not configured one.
+ */
+void i2c_update_timeout(struct i2c_adapter *adap, u32 bus_freq_hz,
+ size_t len, unsigned int safety_coeff,
+ unsigned int min_usec)
+{
+ u64 bit_usec = mul_u64_u32_div(len * 9, USEC_PER_SEC, bus_freq_hz);
+ u64 total_usec = bit_usec * safety_coeff + min_usec;
+ unsigned long jiffies_val;
+
+ /* Userspace-configured timeout always takes precedence. */
+ if (adap->user_timeout > 0) {
+ adap->timeout = adap->user_timeout;
+ return;
+ }
+
+ jiffies_val = usecs_to_jiffies((unsigned int)min_t(u64, total_usec, UINT_MAX));
+ /* adap->timeout is int; guard against signed overflow. */
+ adap->timeout = (int)min_t(unsigned long, jiffies_val, INT_MAX);
+}
+EXPORT_SYMBOL_GPL(i2c_update_timeout);
+#endif /* CONFIG_I2C_DYNAMIC_TIMEOUT */
+
/* ------------------------------------------------------------------------- */
int i2c_for_each_dev(void *data, int (*fn)(struct device *dev, void *data))
diff --git a/drivers/i2c/i2c-dev.c b/drivers/i2c/i2c-dev.c
index ccaac5e29f90..2d918bcbf8f7 100644
--- a/drivers/i2c/i2c-dev.c
+++ b/drivers/i2c/i2c-dev.c
@@ -494,7 +494,11 @@ static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
if (arg > INT_MAX / 10)
return -EINVAL;
+#ifdef CONFIG_I2C_DYNAMIC_TIMEOUT
+ client->adapter->user_timeout = msecs_to_jiffies(arg * 10);
+#else
client->adapter->timeout = msecs_to_jiffies(arg * 10);
+#endif
break;
default:
/* NOTE: returning a fault code here could cause trouble
diff --git a/include/linux/i2c.h b/include/linux/i2c.h
index 14ab4d3055af..068217549266 100644
--- a/include/linux/i2c.h
+++ b/include/linux/i2c.h
@@ -742,6 +742,9 @@ struct i2c_adapter {
struct rt_mutex mux_lock;
int timeout; /* in jiffies */
+#ifdef CONFIG_I2C_DYNAMIC_TIMEOUT
+ int user_timeout; /* I2C_TIMEOUT ioctl value in jiffies; 0 = not set */
+#endif
int retries;
struct device dev; /* the adapter device */
unsigned long locked_flags; /* owned by the I2C core */
@@ -912,6 +915,15 @@ void i2c_put_adapter(struct i2c_adapter *adap);
unsigned int i2c_adapter_depth(struct i2c_adapter *adapter);
void i2c_parse_fw_timings(struct device *dev, struct i2c_timings *t, bool use_defaults);
+#ifdef CONFIG_I2C_DYNAMIC_TIMEOUT
+void i2c_update_timeout(struct i2c_adapter *adap, u32 bus_freq_hz,
+ size_t len, unsigned int safety_coeff,
+ unsigned int min_usec);
+#else
+static inline void i2c_update_timeout(struct i2c_adapter *adap, u32 bus_freq_hz,
+ size_t len, unsigned int safety_coeff,
+ unsigned int min_usec) {}
+#endif
/* Return the functionality mask */
static inline u32 i2c_get_functionality(struct i2c_adapter *adap)
--
2.34.1
^ permalink raw reply [flat|nested] 7+ messages in thread* [PATCH v7 2/2] i2c: qcom-geni: Add dynamic transfer timeout based on transfer length and frequency
2026-08-13 5:45 [PATCH v7 0/2] i2c: Add dynamic transfer timeout based on message length and frequency Aniket Randive
2026-08-13 5:45 ` [PATCH v7 1/2] i2c: core: Add i2c_update_timeout() helper for dynamic transfer timeouts Aniket Randive
@ 2026-08-13 5:45 ` Aniket Randive
2026-08-24 7:11 ` Mukesh Savaliya
1 sibling, 1 reply; 7+ messages in thread
From: Aniket Randive @ 2026-08-13 5:45 UTC (permalink / raw)
To: Wolfram Sang, Andi Shyti, Mukesh Savaliya, Dmitry Guzman
Cc: Viken Dadhaniya, linux-i2c, linux-arm-msm, linux-kernel, Aniket Randive
The driver uses a static XFER_TIMEOUT of HZ (1 second) for all transfers
regardless of message length or bus frequency, causing unnecessary
delays on error paths.
Use i2c_update_timeout() from i2c-core to compute the timeout dynamically
from the message length and the bus frequency, then read adap->timeout at
each wait site. For FIFO, SE-DMA, and GPI single-descriptor paths the
timeout is computed per message using that message's length. For the GPI
multi-descriptor path, the maximum message length across the batch is used
since one completion covers the entire BEI batch and the timeout must
cover the longest individual message.
A 10x safety margin over the theoretical wire time is applied, with a
300ms floor to account for I2C clock stretching and other situations where
a slave may keep SCL asserted for an extended period, including faulty
devices holding the bus. Both constants remain private to this driver.
Signed-off-by: Aniket Randive <aniket.randive@oss.qualcomm.com>
---
drivers/i2c/busses/i2c-qcom-geni.c | 44 ++++++++++++++++++++++++++++++--------
1 file changed, 35 insertions(+), 9 deletions(-)
diff --git a/drivers/i2c/busses/i2c-qcom-geni.c b/drivers/i2c/busses/i2c-qcom-geni.c
index 658636c1ee0e..c98d9e7b250c 100644
--- a/drivers/i2c/busses/i2c-qcom-geni.c
+++ b/drivers/i2c/busses/i2c-qcom-geni.c
@@ -79,9 +79,14 @@ enum geni_i2c_err_code {
#define ABORT_TIMEOUT HZ
#define CANCEL_TIMEOUT HZ
-#define XFER_TIMEOUT HZ
#define RST_TIMEOUT HZ
+/* 9 bits per byte (8 data + 1 ACK), 10x safety margin */
+#define I2C_TIMEOUT_SAFETY_COEFFICIENT 10
+
+/* 300ms floor: budget for clock stretching; slave may hold SCL low indefinitely */
+#define I2C_TIMEOUT_MIN_USEC 300000
+
struct geni_i2c_desc {
bool no_dma_support;
unsigned int tx_fifo_depth;
@@ -513,7 +518,9 @@ static int geni_i2c_rx_one_msg(struct geni_i2c_dev *gi2c, struct i2c_msg *msg,
}
cur = gi2c->cur;
- time_left = wait_for_completion_timeout(&gi2c->done, XFER_TIMEOUT);
+ i2c_update_timeout(&gi2c->adap, gi2c->clk_freq_out, len,
+ I2C_TIMEOUT_SAFETY_COEFFICIENT, I2C_TIMEOUT_MIN_USEC);
+ time_left = wait_for_completion_timeout(&gi2c->done, gi2c->adap.timeout);
if (!time_left || (gi2c->err && gi2c->err != gi2c_log[ADDR_NACK].err))
geni_i2c_cancel_xfer(gi2c);
@@ -555,7 +562,9 @@ static int geni_i2c_tx_one_msg(struct geni_i2c_dev *gi2c, struct i2c_msg *msg,
writel_relaxed(1, se->base + SE_GENI_TX_WATERMARK_REG);
cur = gi2c->cur;
- time_left = wait_for_completion_timeout(&gi2c->done, XFER_TIMEOUT);
+ i2c_update_timeout(&gi2c->adap, gi2c->clk_freq_out, len,
+ I2C_TIMEOUT_SAFETY_COEFFICIENT, I2C_TIMEOUT_MIN_USEC);
+ time_left = wait_for_completion_timeout(&gi2c->done, gi2c->adap.timeout);
if (!time_left || (gi2c->err && gi2c->err != gi2c_log[ADDR_NACK].err))
geni_i2c_cancel_xfer(gi2c);
@@ -633,7 +642,7 @@ static void geni_i2c_gpi_multi_desc_unmap(struct geni_i2c_dev *gi2c, struct i2c_
* geni_i2c_gpi_multi_xfer_timeout_handler() - Handles multi message transfer timeout
* @dev: Pointer to the corresponding dev node
* @multi_xfer: Pointer to the geni_i2c_gpi_multi_desc_xfer
- * @transfer_timeout_msecs: Timeout value in milliseconds
+ * @timeout_jiffies: Per-message completion timeout in jiffies
* @transfer_comp: Completion object of the transfer
*
* This function waits for the completion of each processed transfer messages
@@ -643,18 +652,18 @@ static void geni_i2c_gpi_multi_desc_unmap(struct geni_i2c_dev *gi2c, struct i2c_
*/
static int geni_i2c_gpi_multi_xfer_timeout_handler(struct device *dev,
struct geni_i2c_gpi_multi_desc_xfer *multi_xfer,
- u32 transfer_timeout_msecs,
+ unsigned long timeout_jiffies,
struct completion *transfer_comp)
{
int i;
- u32 time_left;
+ unsigned long time_left;
for (i = 0; i < multi_xfer->msg_idx_cnt - 1; i++) {
reinit_completion(transfer_comp);
if (multi_xfer->msg_idx_cnt != multi_xfer->irq_cnt) {
time_left = wait_for_completion_timeout(transfer_comp,
- transfer_timeout_msecs);
+ timeout_jiffies);
if (!time_left) {
dev_err(dev, "%s: Transfer timeout\n", __func__);
return -ETIMEDOUT;
@@ -778,8 +787,22 @@ static int geni_i2c_gpi(struct geni_i2c_dev *gi2c, struct i2c_msg msgs[],
dma_async_issue_pending(gi2c->tx_c);
if ((msg_idx == (gi2c->num_msgs - 1)) || flags & DMA_PREP_INTERRUPT) {
+ size_t max_len = 0;
+ int j;
+
+ /*
+ * Use the longest message as the timeout base: one completion
+ * covers the whole BEI batch, so the budget must fit the worst
+ * case single-message wire time.
+ */
+ for (j = 0; j < gi2c->num_msgs; j++)
+ max_len = max_t(size_t, max_len, msgs[j].len);
+ i2c_update_timeout(&gi2c->adap, gi2c->clk_freq_out, max_len,
+ I2C_TIMEOUT_SAFETY_COEFFICIENT,
+ I2C_TIMEOUT_MIN_USEC);
ret = geni_i2c_gpi_multi_xfer_timeout_handler(gi2c->se.dev, gi2c_gpi_xfer,
- XFER_TIMEOUT, &gi2c->done);
+ gi2c->adap.timeout,
+ &gi2c->done);
if (ret) {
dev_err(gi2c->se.dev,
"I2C multi write msg transfer timeout: %d\n",
@@ -899,7 +922,10 @@ static int geni_i2c_gpi_xfer(struct geni_i2c_dev *gi2c, struct i2c_msg msgs[], i
if (!gi2c->is_tx_multi_desc_xfer) {
dma_async_issue_pending(gi2c->tx_c);
- time_left = wait_for_completion_timeout(&gi2c->done, XFER_TIMEOUT);
+ i2c_update_timeout(&gi2c->adap, gi2c->clk_freq_out, msgs[i].len,
+ I2C_TIMEOUT_SAFETY_COEFFICIENT,
+ I2C_TIMEOUT_MIN_USEC);
+ time_left = wait_for_completion_timeout(&gi2c->done, gi2c->adap.timeout);
if (!time_left) {
dev_err(gi2c->se.dev, "%s:I2C timeout\n", __func__);
gi2c->err = -ETIMEDOUT;
--
2.34.1
^ permalink raw reply [flat|nested] 7+ messages in thread