[PATCH] SUNXI / TWSI: Implement I2C bus unlocking for stuck devices during initialization.

Federico Fuga via B4 Relay devnull+fuga.studiofuga.com at kernel.org
Wed Sep 3 18:31:36 CEST 2025


From: Federico Fuga <fuga at studiofuga.com>

This patch introduces an I2C bus recovery mechanism for the Marvell mvtwsi controller, limited to sunxi versions.

Under certain conditions, such as a system reset during an active I2C transaction, a slave device can be left holding the SDA line low. This occurs because the slave is still waiting for the master to complete the transfer with clock pulses, but the master (CPU) has been reset.

While the mvtwsi peripheral is reset during boot, this does not affect the state of the slave device on the bus. The bus remains stuck, which can be detected during driver initialization by observing that the I2C Status Register is not in its expected IDLE state (0xF8).

This patchset adds logic to check for this condition during init. If a non-IDLE state is detected, it triggers a bus recovery procedure to unlock the bus and return it to a functional state.

This code is applicable for SUNXI devices only, that allow direct control of SDA/SCL signals.

Testing
This implementation was successfully tested on an older version of U-Boot, where it reliably fixed a reproducible bus-hang issue. It has not been tested on the current master branch, but a review of the driver shows that the relevant code sections are very similar. Community testing on current hardware would be greatly appreciated.

---
Federico Fuga

Signed-off-by: Federico Fuga <fuga at studiofuga.com>
---
 drivers/i2c/mvtwsi.c | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 79 insertions(+), 2 deletions(-)

diff --git a/drivers/i2c/mvtwsi.c b/drivers/i2c/mvtwsi.c
index 44e8e191b0392b9a91e19f2e32c3df2039789064..b574bbe3173888a6202c057495c2d786b952bc33 100644
--- a/drivers/i2c/mvtwsi.c
+++ b/drivers/i2c/mvtwsi.c
@@ -63,6 +63,9 @@ struct  mvtwsi_registers {
 	u32 status;
 	u32 baudrate;
 	u32 soft_reset;
+	u32 enhanced_features;
+	u32 twi_line;
+	u32 twi_dvfs;
 	u32 debug; /* Dummy field for build compatibility with mvebu */
 };
 
@@ -446,12 +449,75 @@ static uint twsi_calc_freq(const int n, const int m)
  */
 static void twsi_reset(struct mvtwsi_registers *twsi)
 {
+	int i;
+
 	/* Reset controller */
 	writel(0, &twsi->soft_reset);
-	/* Wait 2 ms -- this is what the Marvell LSP does */
-	udelay(20000);
+	for (i = 10; i > 0; --i) {
+		/* poll the controller for reset. */
+		if (readl(&twsi->soft_reset) == 0)
+			break;
+		udelay(2000);
+	}
+}
+
+#if defined(CONFIG_ARCH_SUNXI)
+/*
+ * twsi_bus_unblock() - Forcibly unblock i2c devices stuck on transactions.
+ *
+ * If a i2c/twsi transaction is aborted by resetting the controller, the bus might be locked by the
+ * active slave device being stuck waiting for more clock pulse to complete the transaction. This
+ * might happen if the CPU is reset during a transaction.
+ *
+ * In this case, the reset of the bus controller is uneffective. A special procedure is required:
+ * the SCL signal must be pulsed 8 or more times while keeping the SDA signal weak (high), and
+ * then by issuing a STOP signal.
+ *
+ * This function perform this procedure for SUNXI devices that has control of the SDA/SCL signals
+ * through special registers.
+ *
+ * @twsi: The MVTWSI register structure to use.
+ */
+static void twsi_bus_unblock(struct mvtwsi_registers *twsi)
+{
+	int v;
+
+	// Take control of SDA and SCL
+	writel(readl(&twsi->twi_line) | 0x05, &twsi->twi_line);
+
+	// Pull SCL and SDA high
+	writel(readl(&twsi->twi_line) | 0x0a, &twsi->twi_line);
+	ndelay(1000);
+
+	// Pulse SCL 16 times
+	for (v = 0; v < 16; ++v) {
+		writel(readl(&twsi->twi_line) | 0x08, &twsi->twi_line);
+		ndelay(1000);
+		writel(readl(&twsi->twi_line) & 0xf7, &twsi->twi_line);
+		ndelay(1000);
+	}
+
+	// Isse a stop: pull SDA low, then SCL up, then SDA up.
+	writel(readl(&twsi->twi_line) & 0xfe, &twsi->twi_line);
+	ndelay(1000);
+	writel(readl(&twsi->twi_line) | 0x08, &twsi->twi_line);
+	ndelay(1000);
+	writel(readl(&twsi->twi_line) | 0x02, &twsi->twi_line);
+	ndelay(1000);
+
+	// release control of SDA and SCL. Then proceed with init.
+	writel(readl(&twsi->twi_line) & 0x0a, &twsi->twi_line);
+	ndelay(1000);
+}
+
+#else
+
+static void twsi_bus_unblock(struct mvtwsi_registers *twsi)
+{
 }
 
+#endif
+
 /*
  * __twsi_i2c_set_bus_speed() - Set the speed of the I2C controller.
  *
@@ -510,9 +576,20 @@ static void __twsi_i2c_init(struct mvtwsi_registers *twsi, int speed,
 			    int slaveadd, uint *actual_speed)
 {
 	uint tmp_speed;
+	uint v;
 
 	/* Reset controller */
 	twsi_reset(twsi);
+
+	/* Check for Status Register. The register should be set at MVTWSI_STATUS_IDLE (0xf8) after reset.
+	 * If this is not the case, the bus might be hard locked by a stuck device.
+	 * Issue an emergency unlock procedure
+	 */
+	v = readl(&twsi->status);
+	if (v != MVTWSI_STATUS_IDLE) {
+		twsi_bus_unblock(twsi);
+	}
+
 	/* Set speed */
 	tmp_speed = __twsi_i2c_set_bus_speed(twsi, speed);
 	if (actual_speed)

---
base-commit: 9ba067aea83404243de08390e9177dd8f59e5e02
change-id: 20250903-fix-i2c_stuck_bus-4053d37a60d5

Best regards,
-- 
Federico Fuga <fuga at studiofuga.com>




More information about the U-Boot mailing list