i2c: designware: fix __i2c_dw_disable() in case master is holding SCL low
authorYann Sionneau <ysionneau@kalray.eu>
Mon, 11 Sep 2023 14:07:49 +0000 (16:07 +0200)
committerWolfram Sang <wsa@kernel.org>
Tue, 19 Sep 2023 19:57:11 +0000 (21:57 +0200)
The DesignWare IP can be synthesized with the IC_EMPTYFIFO_HOLD_MASTER_EN
parameter.
In this case, when the TX FIFO gets empty and the last command didn't have
the STOP bit (IC_DATA_CMD[9]), the controller will hold SCL low until
a new command is pushed into the TX FIFO or the transfer is aborted.

When the controller is holding SCL low, it cannot be disabled.
The transfer must first be aborted.
Also, the bus recovery won't work because SCL is held low by the master.

Check if the master is holding SCL low in __i2c_dw_disable() before trying
to disable the controller. If SCL is held low, an abort is initiated.
When the abort is done, then proceed with disabling the controller.

This whole situation can happen for instance during SMBus read data block
if the slave just responds with "byte count == 0".
This puts the driver in an unrecoverable state, because the controller is
holding SCL low and the current __i2c_dw_disable() procedure is not
working. In this situation only a SoC reset can fix the i2c bus.

Co-developed-by: Jonathan Borne <jborne@kalray.eu>
Signed-off-by: Jonathan Borne <jborne@kalray.eu>
Signed-off-by: Yann Sionneau <ysionneau@kalray.eu>
Acked-by: Jarkko Nikula <jarkko.nikula@linux.intel.com>
Signed-off-by: Wolfram Sang <wsa@kernel.org>
drivers/i2c/busses/i2c-designware-common.c
drivers/i2c/busses/i2c-designware-core.h

index cdd8c67..affcfb2 100644 (file)
@@ -441,8 +441,25 @@ err_release_lock:
 
 void __i2c_dw_disable(struct dw_i2c_dev *dev)
 {
+       unsigned int raw_intr_stats;
+       unsigned int enable;
        int timeout = 100;
+       bool abort_needed;
        unsigned int status;
+       int ret;
+
+       regmap_read(dev->map, DW_IC_RAW_INTR_STAT, &raw_intr_stats);
+       regmap_read(dev->map, DW_IC_ENABLE, &enable);
+
+       abort_needed = raw_intr_stats & DW_IC_INTR_MST_ON_HOLD;
+       if (abort_needed) {
+               regmap_write(dev->map, DW_IC_ENABLE, enable | DW_IC_ENABLE_ABORT);
+               ret = regmap_read_poll_timeout(dev->map, DW_IC_ENABLE, enable,
+                                              !(enable & DW_IC_ENABLE_ABORT), 10,
+                                              100);
+               if (ret)
+                       dev_err(dev->dev, "timeout while trying to abort current transfer\n");
+       }
 
        do {
                __i2c_dw_disable_nowait(dev);
index cf4f684..a7f6f3e 100644 (file)
@@ -98,6 +98,7 @@
 #define DW_IC_INTR_START_DET                   BIT(10)
 #define DW_IC_INTR_GEN_CALL                    BIT(11)
 #define DW_IC_INTR_RESTART_DET                 BIT(12)
+#define DW_IC_INTR_MST_ON_HOLD                 BIT(13)
 
 #define DW_IC_INTR_DEFAULT_MASK                        (DW_IC_INTR_RX_FULL | \
                                                 DW_IC_INTR_TX_ABRT | \
                                                 DW_IC_INTR_RX_UNDER | \
                                                 DW_IC_INTR_RD_REQ)
 
+#define DW_IC_ENABLE_ABORT                     BIT(1)
+
 #define DW_IC_STATUS_ACTIVITY                  BIT(0)
 #define DW_IC_STATUS_TFE                       BIT(2)
 #define DW_IC_STATUS_RFNE                      BIT(3)