Skip to contentSkip to conversation

i2c: rk3x: add support for SCL OE debounce and slave hold recovery

PATCH Superseded Anand Moon 2 participants 3 messages Mar 21 – Mar 31

Subsystems (2)

v2 of 3 Superseded Latest is v3 →

lore
Share & tools
lore

Anand Moon

author
#0 lore raw

PATCH v2 1/1 i2c: rk3x: add support for SCL OE debounce and slave hold recovery

From: David Wu <[email protected]>

As per the RK3399 and RK35xx datasheet, Rockchip I2C controllers feature a SCL_OE_DB register (0x24). This register is used to configure the debounce time for the SCL output enable signal, which helps prevent glitches and ensures timing compliance during bus handover or slave clock stretching.

Introduce a 'has_scl_oe_debounce' flag to rk3x_i2c_soc_data to distinguish between hardware versions. For supported SoCs, calculate the debounce counter dynamically based on the current clock rate and program it during divider adaptation.

Additionally:
- Implement detection for the REG_INT_SLV_HDSCL (Slave Hold SCL) interrupt bit during transfer timeouts.
- Capture the Interrupt Pending (IPD) register state before clearing interrupts during a timeout to check for the slave hold condition.
- Re-apply the clock dividers via rk3x_i2c_adapt_div() if a slave hold is detected on supported SoCs to attempt bus recovery.

---
v1: https://lore.kernel.org/all/[email protected]/
Changes:
v2: Aded the to detect REG_INT_SLV_HDSCL interrupt timeout
it was part of origmal commit below.
[1] https://github.com/radxa/kernel/commit/006c0b1e7710d471119a69d6bd56917a15a85a0b
Fix the order of SoB,
Fix the doc warning reporteed by kernel test robot

---
 drivers/i2c/busses/i2c-rk3x.c | 28 ++++++++++++++++++++++++++++
 1 file changed, 28 insertions(+)
drivers/i2c/busses/i2c-rk3x.c +28 -0
index fcede9f6ed54..57ef31cd96eb 100644
@@ -36,6 +36,7 @@
 #define REG_IEN        0x18 /* interrupt enable */
 #define REG_IPD        0x1c /* interrupt pending */
 #define REG_FCNT       0x20 /* finished count */
+#define REG_SCL_OE_DB  0x24 /* Slave hold scl debounce */
 
 /* Data buffer offsets */
 #define TXBUFFER_BASE 0x100
@@ -74,6 +75,7 @@ enum {
 #define REG_INT_START     BIT(4) /* START condition generated */
 #define REG_INT_STOP      BIT(5) /* STOP condition generated */
 #define REG_INT_NAKRCV    BIT(6) /* NACK received */
+#define REG_INT_SLV_HDSCL BIT(7) /* slave hold scl */
 #define REG_INT_ALL       0x7f
 
 /* Constants */
@@ -161,10 +163,12 @@ enum rk3x_i2c_state {
 
 /**
  * struct rk3x_i2c_soc_data - SOC-specific data
+ * @has_scl_oe_debounce: Support for slave hold SCL debounce
  * @grf_offset: offset inside the grf regmap for setting the i2c type
  * @calc_timings: Callback function for i2c timing information calculated
  */
 struct rk3x_i2c_soc_data {
+	bool has_scl_oe_debounce;
 	int grf_offset;
 	int (*calc_timings)(unsigned long, struct i2c_timings *,
 			    struct rk3x_i2c_calced_timings *);
@@ -876,6 +880,7 @@ static void rk3x_i2c_adapt_div(struct rk3x_i2c *i2c, unsigned long clk_rate)
 {
 	struct i2c_timings *t = &i2c->t;
 	struct rk3x_i2c_calced_timings calc;
+	unsigned long period, time_hold = (WAIT_TIMEOUT / 2) * 1000000;
 	u64 t_low_ns, t_high_ns;
 	unsigned long flags;
 	u32 val;
@@ -893,6 +898,13 @@ static void rk3x_i2c_adapt_div(struct rk3x_i2c *i2c, unsigned long clk_rate)
 	i2c_writel(i2c, val, REG_CON);
 	i2c_writel(i2c, (calc.div_high << 16) | (calc.div_low & 0xffff),
 		   REG_CLKDIV);
+
+	if (i2c->soc_data->has_scl_oe_debounce) {
+		period = DIV_ROUND_UP(1000000000, clk_rate);
+		val = DIV_ROUND_UP(time_hold, period);
+		i2c_writel(i2c, val, REG_SCL_OE_DB);
+	}
+
 	spin_unlock_irqrestore(&i2c->lock, flags);
 
 	clk_disable(i2c->pclk);
@@ -1063,6 +1075,7 @@ static int rk3x_i2c_xfer_common(struct i2c_adapter *adap,
 	unsigned long flags;
 	long time_left;
 	u32 val;
+	u32 ipd = 0; /* To store interrupt pending status for timeout analysis */
 	int ret = 0;
 	int i;
 
@@ -1107,6 +1120,9 @@ static int rk3x_i2c_xfer_common(struct i2c_adapter *adap,
 		spin_lock_irqsave(&i2c->lock, flags);
 
 		if (time_left == 0) {
+			/* Read IPD before clearing to check for Slave Hold SCL */
+			ipd = i2c_readl(i2c, REG_IPD);
+
 			/* Force a STOP condition without interrupt */
 			i2c_writel(i2c, 0, REG_IEN);
 			val = i2c_readl(i2c, REG_CON) & REG_CON_TUNING_MASK;
@@ -1125,6 +1141,17 @@ static int rk3x_i2c_xfer_common(struct i2c_adapter *adap,
 		}
 	}
 
+	/*
+	 * If a timeout occurred and the slave is holding SCL,
+	 * re-apply the timings/dividers to attempt recovery.
+	 */
+	if (ret == -ETIMEDOUT && i2c->soc_data->has_scl_oe_debounce) {
+		if (ipd & REG_INT_SLV_HDSCL) {
+			dev_err(i2c->dev, "SCL hold by slave detected, resetting timings.\n");
+			rk3x_i2c_adapt_div(i2c, clk_get_rate(i2c->clk));
+		}
+	}
+
 	clk_disable(i2c->pclk);
 	clk_disable(i2c->clk);
 
@@ -1198,6 +1225,7 @@ static const struct rk3x_i2c_soc_data rk3288_soc_data = {
 static const struct rk3x_i2c_soc_data rk3399_soc_data = {
 	.grf_offset = -1,
 	.calc_timings = rk3x_i2c_v1_calc_timings,
+	.has_scl_oe_debounce = true,
 };
 
 static const struct of_device_id rk3x_i2c_match[] = {

base-commit: a0c83177734ab98623795e1ba2cf4b72c23de5e7

Signature · 1 line
2.50.1
maintainer
#1 lore raw

Hi Anand,

...

drivers/i2c/busses/i2c-rk3x.c static int rk3x_i2c_xfer_common(struct i2c_adapter *adap, · line 1151

Quoted 12 lines
drivers/i2c/busses/i2c-rk3x.c +8 -0
@@ -1125,6 +1141,17 @@ static int rk3x_i2c_xfer_common(struct i2c_adapter *adap,
 		}
 	}
 
+	/*
+	 * If a timeout occurred and the slave is holding SCL,
+	 * re-apply the timings/dividers to attempt recovery.
+	 */
+	if (ret == -ETIMEDOUT && i2c->soc_data->has_scl_oe_debounce) {
+		if (ipd & REG_INT_SLV_HDSCL) {
+			dev_err(i2c->dev, "SCL hold by slave detected, resetting timings.\n");
+			rk3x_i2c_adapt_div(i2c, clk_get_rate(i2c->clk));

argh! this nests i2c->lock. rk3x_i2c_xfer_common() already holds it when calling rk3x_i2c_adapt_div().
Andi

Quoted 3 lines
drivers/i2c/busses/i2c-rk3x.c +3 -0
+		}
+	}
+
author
#2 lore raw

Hi Andi,

Thanks for your review comments.

Quoted 9 lines · Andi Shyti

Hi Anand,

...

Quoted 4 lines
(quoted diff) +0 -0
@@ -1125,6 +1141,17 @@ static int rk3x_i2c_xfer_common(struct i2c_adapter *adap,
              }
      }
 
Quoted 11 lines

drivers/i2c/busses/i2c-rk3x.c static int rk3x_i2c_xfer_common(struct i2c_adapter *adap, · line 1151

Quoted 8 lines
drivers/i2c/busses/i2c-rk3x.c +8 -0
+     /*
+      * If a timeout occurred and the slave is holding SCL,
+      * re-apply the timings/dividers to attempt recovery.
+      */
+     if (ret == -ETIMEDOUT && i2c->soc_data->has_scl_oe_debounce) {
+             if (ipd & REG_INT_SLV_HDSCL) {
+                     dev_err(i2c->dev, "SCL hold by slave detected, resetting timings.\n");
+                     rk3x_i2c_adapt_div(i2c, clk_get_rate(i2c->clk));

argh! this nests i2c->lock. rk3x_i2c_xfer_common() already holds it when calling rk3x_i2c_adapt_div().

I agree; the current implementation can lead to a deadlock.
To resolve this, I'll split the function into locked and unlocked variants.

Note that this deadlock only occurs if REG_INT_SLV_HDSCL is active in the interrupt enable mask, as shown below:

static void rk3x_i2c_start(struct rk3x_i2c *i2c)

 {
        u32 val = i2c_readl(i2c, REG_CON) & REG_CON_TUNING_MASK;

+ u32 ien = REG_INT_START;

- i2c_writel(i2c, REG_INT_START, REG_IEN);
+ if(i2c->soc_data->has_scl_oe_debounce)
+ ien != REG_INT_SLV_HDSCL;
+
+ i2c_writel(i2c, ien, REG_IEN);

Thanks
-Anand