[PATCH] i2c: iproc: recover the bus on transfer timeouts

Jonas Gorski <[email protected]> Tue, 4 Aug 2026 11:14:05 +0200
Newsgroups org.kernel.vger.linux-i2c,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
Just resetting the controller is not always enough to recover from a
timed out transfer, the bus may need to be cleared as well.

So add support for generic SCL recovery by switching to bit-banged mode
for manual control of SCL and SDA signals, and using it when SDA is
still low after a timeout.

Clearing SDA does not reset START_BUSY, so regardless of clearing SDA,
the controller might still need to be reset as well.

This is loosely inspired by iproc_smb_sda_low_recovery() from the
non-upstream iproc smbus driver implementation [1].

[1] https://github.com/opencomputeproject/onie/blob/master/patches/kernel/3=
.2.69/driver-iproc-smbus.patch

Signed-off-by: Jonas Gorski <[email protected]>
---
 drivers/i2c/busses/i2c-bcm-iproc.c | 92 ++++++++++++++++++++++++++++++
 1 file changed, 92 insertions(+)

diff --git a/drivers/i2c/busses/i2c-bcm-iproc.c b/drivers/i2c/busses/i2c-bc=
m-iproc.c
index 66bb3bfdaa4c..1d3732c76c09 100644
--- a/drivers/i2c/busses/i2c-bcm-iproc.c
+++ b/drivers/i2c/busses/i2c-bcm-iproc.c
@@ -15,6 +15,7 @@
 #define CFG_OFFSET                   0x00
 #define CFG_RESET_SHIFT              31
 #define CFG_EN_SHIFT                 30
+#define CFG_BITBANG_EN_SHIFT         29
 #define CFG_SLAVE_ADDR_0_SHIFT       28
 #define CFG_M_RETRY_CNT_SHIFT        16
 #define CFG_M_RETRY_CNT_MASK         0x0f
@@ -56,6 +57,12 @@
 #define S_FIFO_RX_THLD_SHIFT         8
 #define S_FIFO_RX_THLD_MASK          0x3f
=20
+#define BITBANG_CTRL_OFFSET          0x14
+#define BITBANG_CLK_IN_SHIFT         31
+#define BITBANG_CLK_OUT_EN_SHIFT     30
+#define BITBANG_DATA_IN_SHIFT        29
+#define BITBANG_DATA_OUT_EN_SHIFT    28
+
 #define M_CMD_OFFSET                 0x30
 #define M_CMD_START_BUSY_SHIFT       31
 #define M_CMD_STATUS_SHIFT           25
@@ -715,6 +722,76 @@ static void bcm_iproc_i2c_init(struct bcm_iproc_i2c_de=
v *iproc_i2c)
 	iproc_i2c_wr_reg(iproc_i2c, IS_OFFSET, 0xffffffff);
 }
=20
+static void bcm_iproc_i2c_prepare_recovery(struct i2c_adapter *adapter)
+{
+	struct bcm_iproc_i2c_dev *iproc_i2c =3D i2c_get_adapdata(adapter);
+	u32 val;
+
+	/* enable bit-bang mode to allow manually driving SDA/SCL */
+	val =3D iproc_i2c_rd_reg(iproc_i2c, CFG_OFFSET);
+	val |=3D BIT(CFG_BITBANG_EN_SHIFT);
+	iproc_i2c_wr_reg(iproc_i2c, CFG_OFFSET, val);
+	usleep_range(50, 75);
+}
+
+static void bcm_iproc_i2c_unprepare_recovery(struct i2c_adapter *adapter)
+{
+	struct bcm_iproc_i2c_dev *iproc_i2c =3D i2c_get_adapdata(adapter);
+	u32 val;
+
+	/* disable bit-bang mode again */
+	val =3D iproc_i2c_rd_reg(iproc_i2c, CFG_OFFSET);
+	val &=3D ~BIT(CFG_BITBANG_EN_SHIFT);
+	iproc_i2c_wr_reg(iproc_i2c, CFG_OFFSET, val);
+	usleep_range(10, 15);
+}
+
+static int bcm_iproc_i2c_get_scl(struct i2c_adapter *adapter)
+{
+	struct bcm_iproc_i2c_dev *iproc_i2c =3D i2c_get_adapdata(adapter);
+	u32 val;
+
+	val =3D iproc_i2c_rd_reg(iproc_i2c, BITBANG_CTRL_OFFSET);
+
+	return !!(val & BIT(BITBANG_CLK_IN_SHIFT));
+}
+
+static void bcm_iproc_i2c_set_scl(struct i2c_adapter *adapter, int scl)
+{
+	struct bcm_iproc_i2c_dev *iproc_i2c =3D i2c_get_adapdata(adapter);
+	u32 val;
+
+	val =3D iproc_i2c_rd_reg(iproc_i2c, BITBANG_CTRL_OFFSET);
+	if (scl)
+		val |=3D BIT(BITBANG_CLK_OUT_EN_SHIFT);
+	else
+		val &=3D ~BIT(BITBANG_CLK_OUT_EN_SHIFT);
+	iproc_i2c_wr_reg(iproc_i2c, BITBANG_CTRL_OFFSET, val);
+}
+
+static int bcm_iproc_i2c_get_sda(struct i2c_adapter *adapter)
+{
+	struct bcm_iproc_i2c_dev *iproc_i2c =3D i2c_get_adapdata(adapter);
+	u32 val;
+
+	val =3D iproc_i2c_rd_reg(iproc_i2c, BITBANG_CTRL_OFFSET);
+
+	return !!(val & BIT(BITBANG_DATA_IN_SHIFT));
+}
+
+static void bcm_iproc_i2c_set_sda(struct i2c_adapter *adapter, int sda)
+{
+	struct bcm_iproc_i2c_dev *iproc_i2c =3D i2c_get_adapdata(adapter);
+	u32 val;
+
+	val =3D iproc_i2c_rd_reg(iproc_i2c, BITBANG_CTRL_OFFSET);
+	if (sda)
+		val |=3D BIT(BITBANG_DATA_OUT_EN_SHIFT);
+	else
+		val &=3D ~BIT(BITBANG_DATA_OUT_EN_SHIFT);
+	iproc_i2c_wr_reg(iproc_i2c, BITBANG_CTRL_OFFSET, val);
+}
+
 static int bcm_iproc_i2c_check_status(struct bcm_iproc_i2c_dev *iproc_i2c,
 				      struct i2c_msg *msg)
 {
@@ -803,6 +880,10 @@ static int bcm_iproc_i2c_xfer_wait(struct bcm_iproc_i2=
c_dev *iproc_i2c,
 	}
=20
 	if (!time_left && !iproc_i2c->xfer_is_done) {
+		/* Recover bus if SDA is held low */
+		if (bcm_iproc_i2c_get_sda(&iproc_i2c->adapter) =3D=3D 0)
+			i2c_recover_bus(&iproc_i2c->adapter);
+
 		/*
 		 * The controller may fail to clear START_BUSY after a timeout,
 		 * reset the controller to recover in that case.
@@ -1064,6 +1145,16 @@ static const struct i2c_adapter_quirks bcm_iproc_i2c=
_quirks =3D {
 	.max_read_len =3D M_RX_MAX_READ_LEN,
 };
=20
+static struct i2c_bus_recovery_info bcm_iproc_i2c_recovery_info =3D {
+	.recover_bus =3D i2c_generic_scl_recovery,
+	.get_scl =3D bcm_iproc_i2c_get_scl,
+	.set_scl =3D bcm_iproc_i2c_set_scl,
+	.get_sda =3D bcm_iproc_i2c_get_sda,
+	.set_sda =3D bcm_iproc_i2c_set_sda,
+	.prepare_recovery =3D bcm_iproc_i2c_prepare_recovery,
+	.unprepare_recovery =3D bcm_iproc_i2c_unprepare_recovery,
+};
+
 static int bcm_iproc_i2c_cfg_speed(struct bcm_iproc_i2c_dev *iproc_i2c)
 {
 	unsigned int bus_speed;
@@ -1163,6 +1254,7 @@ static int bcm_iproc_i2c_probe(struct platform_device=
 *pdev)
 		 of_node_full_name(iproc_i2c->device->of_node));
 	adap->algo =3D &bcm_iproc_algo;
 	adap->quirks =3D &bcm_iproc_i2c_quirks;
+	adap->bus_recovery_info =3D &bcm_iproc_i2c_recovery_info;
 	adap->dev.parent =3D &pdev->dev;
 	adap->dev.of_node =3D pdev->dev.of_node;
=20
--=20
2.55.0


--=20
BISDN GmbH
K=F6rnerstra=DFe 7-10
10785 Berlin
Germany
Phone: +49 30 610 816 100
Managing Directors:=A0Dr.-Ing. Hagen Woesner, Andreas K=F6psel


Commercial=20
register:=A0
Amtsgericht Berlin-Charlottenburg HRB 141569 B
VAT ID No:=A0
DE283257294 <http://goog_1008408251>