From: Federico Fuga <f...@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 <f...@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 <f...@studiofuga.com>


Reply via email to