mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v4 0/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) support for TI TDA54
@ 2026-10-27 11:30 Aniket Limaye
  2026-10-27 11:30 ` [PATCH v4 1/2] dt-bindings: i2c: Add Synopsys DWC_i2c controller Aniket Limaye
  2026-10-27 11:30 ` [PATCH v4 2/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) driver Aniket Limaye
  0 siblings, 2 replies; 3+ messages in thread
From: Aniket Limaye @ 2026-10-27 11:30 UTC (permalink / raw)
  To: Andi Shyti, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Philipp Zabel
  Cc: linux-i2c, devicetree, linux-kernel, Aniket Limaye, lianfeng.ouyang

Add support for the Synopsys Advanced I2C (DWC_i2c [0])
controller present on the TI TDA54 SoC.

This series supports controller mode only: no target mode support. TI
TDA54's DWC_i2c instance does not implement an SMBus block, so the new
driver does not touch any SMBus registers.

* Why a separate driver:

Earlier revisions of this series extended the existing DW_apb_i2c
driver (drivers/i2c/busses/i2c-designware-*) to also handle DWC_i2c,
selecting between the two register layouts at runtime via a new
MODEL_TI_TDA54 flag.

This revision instead adds a new, separate, standalone driver
(drivers/i2c/busses/i2c-dwc-adv.c) and a new binding
(Documentation/devicetree/bindings/i2c/snps,dwc-adv-i2c.yaml), since
DWC_i2c is not register compatible with DW_apb_i2c. Based on review
feedback from previous versions, extending the existing DW_apb_i2c is
not attempted to get away from any hacks and workarounds added there
over the years and to avoid all of the complexity of v3 resulting from
updating each regmap access.

The cost is that the message xfers, abort handling and timing formulas
follow i2c-designware-master.c closely and are duplicated, so fixes
there have to be mirrored by hand. Sharing them would need every
register access, and the interrupt acknowledgment abstracted, with some
possible special casing; same as v3.

* Delta from DW_apb_i2c:

DWC_i2c differs from DW_apb_i2c as follows:

  - Register layout (see below): DW_apb_i2c uses a flat set of fixed
    register offsets. DWC_i2c groups registers into blocks (OPERATION,
    CORE, TIMING, FIFO, INTR, DEBUG, ...) that are discovered at runtime
    by walking a chain of block headers, each encoding the length to the
    next block and an ID that the driver validates.
  - CON register bitfield (see below): DWC_i2c's CON register has no
    RESTART_EN or SLAVE_DISABLE bits; restart is implicitly always
    enabled in hardware.
  - Interrupt acknowledgment: DW_apb_i2c acknowledges interrupts by
    reading from N dedicated CLR_* registers. DWC_i2c instead writes a
    bitmask to a single CLR_INTR register.
  - Shared SS/FS Timing registers: DW_apb_i2c has separate HCNT/LCNT
    register pairs for standard and fast speed. DWC_i2c shares a single
    HCNT/LCNT pair between the two; whichever value set matches the
    configured speed is written at init time.
  - Bus capacitance for HS mode: DW_apb_i2c modelled this through DT,
    while in DWC_i2c this can be read (100/400pf) from
    IC_I2C_CAPABILITIES.IC_CAP_LOADING
  - FIFO depth / high-speed mode: DW_apb_i2c has a COMP_PARAM_1
    register that can be used to autodetect FIFO depth and high-speed
    support. DWC_i2c has no such register; FIFO depth comes from the
    compatible string's driver match data instead. High speed mode is
    always supported.

Register layout differences from i2c-designware (DW_apb_i2c):
(New driver does not make use of flat map like below, and instead only
stores offsets from respective headers)

1.  Reg offsets                         DW_apb_i2c      DWC_i2c (TDA54)

    DWC_ADV_IC_I2C_CAPABILITIES         NA              0x10
    DWC_ADV_IC_I2C_BLOCK_OFFSET         NA              0x14
    DWC_ADV_IC_I2C_CORE_HEADER          NA              0x28
    DWC_ADV_IC_TIMING_CTRL_HEADER       NA              0x44
    DWC_ADV_IC_FIFO_CTRL_HEADER         NA              0x7c
    DWC_ADV_IC_INTR_DEBUG_HEADER        NA              0xbc
    DWC_ADV_IC_HWID_HEADER              NA              0xf8
    [DW_IC_CON]                         0x00            0x2c
    [DW_IC_TAR]                         0x04            0x30
    [DW_IC_SAR]                         0x08            0x34
    [DW_IC_DATA_CMD]                    0x10            0x80
    [DW_IC_SS_SCL_HCNT]                 0x14            0x4c
    [DW_IC_SS_SCL_LCNT]                 0x18            0x50
    [DW_IC_FS_SCL_HCNT]                 0x1c            NA    /* same as SS */
    [DW_IC_FS_SCL_LCNT]                 0x20            NA    /* same as SS */
    [DW_IC_HS_SCL_HCNT]                 0x24            0x54
    [DW_IC_HS_SCL_LCNT]                 0x28            0x58
    [DW_IC_INTR_STAT]                   0x2c            0xc0
    [DW_IC_INTR_MASK]                   0x30            0xc4
    [DW_IC_RAW_INTR_STAT]               0x34            0xc8
    [DW_IC_RX_TL]                       0x38            0x84
    [DW_IC_TX_TL]                       0x3c            0x88
    [DW_IC_CLR_INTR]                    0x40            0xcc
    [DW_IC_CLR_RX_UNDER]                0x44            NA
    [DW_IC_CLR_RX_OVER]                 0x48            NA
    [DW_IC_CLR_TX_OVER]                 0x4c            NA
    [DW_IC_CLR_RD_REQ]                  0x50            NA
    [DW_IC_CLR_TX_ABRT]                 0x54            NA
    [DW_IC_CLR_RX_DONE]                 0x58            NA
    [DW_IC_CLR_ACTIVITY]                0x5c            NA
    [DW_IC_CLR_STOP_DET]                0x60            NA
    [DW_IC_CLR_START_DET]               0x64            NA
    [DW_IC_CLR_GEN_CALL]                0x68            NA
    [DW_IC_ENABLE]                      0x6c            0x04
    [DW_IC_STATUS]                      0x70            0xd8
    [DW_IC_TXFLR]                       0x74            0xdc
    [DW_IC_RXFLR]                       0x78            0xe0
    [DW_IC_SDA_HOLD]                    0x7c            0x5c
    [DW_IC_TX_ABRT_SOURCE]              0x80            0xd4
    [DW_IC_ENABLE_STATUS]               0x9c            0xd0
    [DW_IC_CLR_RESTART_DET]             0xa8            NA
    [DW_IC_SMBUS_INTR_STAT]             0xc8            NA
    [DW_IC_SMBUS_INTR_MASK]             0xcc            NA
    [DW_IC_CLR_SMBUS_INTR]              0xd4            NA
    [DW_IC_COMP_PARAM_1]                0xf4            NA
    [DW_IC_COMP_VERSION]                0xf8            0x100
    [DW_IC_COMP_TYPE]                   0xfc            0x104

2. DW_IC_CON bitfields:

    DW_IC_CON_MASTER                    BIT(0)         BIT(0)
    DW_IC_CON_SPEED_STD                 (1 << 1)       (1 << 4)
    DW_IC_CON_SPEED_FAST                (2 << 1)       (2 << 4)
    DW_IC_CON_SPEED_HIGH                (3 << 1)       (3 << 4)
    DW_IC_CON_SPEED_MASK                GENMASK(2, 1)  GENMASK(5, 4)
    DW_IC_CON_10BITADDR_SLAVE           BIT(3)         BIT(8)
    DW_IC_CON_10BITADDR_MASTER          BIT(4)         BIT(9)
    DW_IC_CON_RESTART_EN                BIT(5)         NA
    DW_IC_CON_SLAVE_DISABLE             BIT(6)         NA
    DW_IC_CON_STOP_DET_IFADDRESSED      BIT(7)         BIT(10)
    DW_IC_CON_TX_EMPTY_CTRL             BIT(8)         BIT(11)
    DW_IC_CON_RX_FIFO_FULL_HLD_CTRL     BIT(9)         BIT(12)
    DW_IC_CON_BUS_CLEAR_CTRL            BIT(11)        BIT(14)

The same IP is also used by the StarFive JHB100 SoC [1]. The block
header walk is meant to accommodate such SoC specific layouts, with
their own compatible and match data.

[0]: Synopsys Advanced I2C Controller Reference Manual (Version 2.00a-lca00)
[1]: https://lore.kernel.org/r/20260527085039.44435-1-lianfeng.ouyang@starfivetech.com

Signed-off-by: Aniket Limaye <a-limaye@ti.com>
---
Changes in v4:
- Drop the approach of extending the existing i2c-designware-* driver
  with a MODEL_TI_TDA54 variant. Instead, add a new standalone driver
  (i2c-dwc-adv.c) and a new binding (snps,dwc-adv-i2c.yaml), since
  DWC_i2c is not register compatible with DW_apb_i2c. (Andy Shevchenko)
- Drop the snps,tx-fifo-depth/snps,rx-fifo-depth DT properties from
  the new binding; FIFO depth is now tied to the compatible string via
  driver match data instead. (Conor Dooley)
- Link to v3: https://lore.kernel.org/r/20260925-tda54-upstream-i2c-v3-0-544d74e992ff@ti.com

Changes in v3:
- [PATCH 2/3] Revert to special handling for the max_register in regmap
  for MODEL_AMD_NAVI_GPU instead of unconditionally including
  AMD_UCSI_INTR_REG in the legal regmap range. (sashiko)
- Link to v2: https://lore.kernel.org/r/20260925-tda54-upstream-i2c-v2-0-261785e58493@ti.com

Changes in v2:
- Rename compatible/flag: snps,dwc-i2c -> ti,tda54-i2c,
  MODEL_DWC_I2C -> MODEL_TI_TDA54.
- Move the Reg ID -> Offset mapping inside the regmap functions such
  that existing calls to reg_read/write are unaltered. This does change
  the reg_read()/reg_write() functions to take reg IDs as input rather
  than their physical offsets, which also requires updates to the
  xe_i2c.c driver to handle the same mapping.
- Remove i2c_dwc_has_smbus(); just skip the SMBUS_INTR_MASK write under
  MODEL_TI_TDA54.
- Link to v1: https://lore.kernel.org/r/20260919-tda54-upstream-i2c-v1-0-b0b9f77be18b@ti.com

---
Aniket Limaye (2):
      dt-bindings: i2c: Add Synopsys DWC_i2c controller
      i2c: Add Synopsys Advanced I2C (DWC_i2c) driver

 .../devicetree/bindings/i2c/snps,dwc-adv-i2c.yaml  |   75 ++
 drivers/i2c/busses/Kconfig                         |   11 +
 drivers/i2c/busses/Makefile                        |    1 +
 drivers/i2c/busses/i2c-dwc-adv.c                   | 1333 ++++++++++++++++++++
 4 files changed, 1420 insertions(+)
---
base-commit: 8f7df005a7564bd7ffa75da9c3085d5b996f1e16
change-id: 20260919-tda54-upstream-i2c-d0c67f16b4fc

Best regards,
-- 
Aniket Limaye <a-limaye@ti.com>


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH v4 1/2] dt-bindings: i2c: Add Synopsys DWC_i2c controller
  2026-10-27 11:30 [PATCH v4 0/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) support for TI TDA54 Aniket Limaye
@ 2026-10-27 11:30 ` Aniket Limaye
  2026-10-27 11:30 ` [PATCH v4 2/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) driver Aniket Limaye
  1 sibling, 0 replies; 3+ messages in thread
From: Aniket Limaye @ 2026-10-27 11:30 UTC (permalink / raw)
  To: Andi Shyti, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Philipp Zabel
  Cc: linux-i2c, devicetree, linux-kernel, Aniket Limaye, lianfeng.ouyang

Add documentation for the Synopsys Advanced I2C (DWC_i2c) controller, as
found on the TI TDA54 SoC.

The block cannot work on its own and needs SoC integration, e.g. the
FIFO depths are not discoverable from the IP. Do not add a generic
compatible and only list SoC specific compatibles. The file is named
after the IP so that other SoCs integrating it can be added here.

Signed-off-by: Aniket Limaye <a-limaye@ti.com>

---

Changes in v4:
- Drop the approach of extending the existing i2c-designware-* driver
  with a MODEL_TI_TDA54 variant. Instead, add a new standalone driver
  (i2c-dwc-adv.c) and a new binding (snps,dwc-adv-i2c.yaml), since
  DWC_i2c is not register compatible with DW_apb_i2c. (Andy Shevchenko)
- Drop the snps,tx-fifo-depth/snps,rx-fifo-depth DT properties from
  the new binding; FIFO depth is now tied to the compatible string via
  driver match data instead. (Conor Dooley)
- Link to v3: https://lore.kernel.org/all/20260925-tda54-upstream-i2c-v3-1-544d74e992ff@ti.com/

Changes in v2:
- Rename snps,dwc-i2c to ti,tda54-i2c
- Link to v1: https://lore.kernel.org/r/20260919-tda54-upstream-i2c-v1-1-b0b9f77be18b@ti.com/
---
 .../devicetree/bindings/i2c/snps,dwc-adv-i2c.yaml  | 75 ++++++++++++++++++++++
 1 file changed, 75 insertions(+)

diff --git a/Documentation/devicetree/bindings/i2c/snps,dwc-adv-i2c.yaml b/Documentation/devicetree/bindings/i2c/snps,dwc-adv-i2c.yaml
new file mode 100644
index 000000000000..7ad22536825e
--- /dev/null
+++ b/Documentation/devicetree/bindings/i2c/snps,dwc-adv-i2c.yaml
@@ -0,0 +1,75 @@
+# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/i2c/snps,dwc-adv-i2c.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Synopsys DesignWare Advanced I2C (DWC_i2c) Controller
+
+maintainers:
+  - Aniket Limaye <a-limaye@ti.com>
+
+allOf:
+  - $ref: /schemas/i2c/i2c-controller.yaml#
+
+properties:
+  compatible:
+    enum:
+      - ti,tda54-i2c
+
+  reg:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  clocks:
+    items:
+      - description: I2C functional clock (ic_clk)
+      - description: Register interface clock (pclk)
+
+  clock-names:
+    items:
+      - const: ref
+      - const: pclk
+
+  resets:
+    maxItems: 1
+
+  clock-frequency:
+    enum: [100000, 400000, 1000000, 3400000]
+    default: 400000
+
+  i2c-sda-hold-time-ns: true
+
+  i2c-scl-falling-time-ns:
+    default: 300
+
+  i2c-sda-falling-time-ns:
+    default: 300
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - clocks
+  - clock-names
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/interrupt-controller/arm-gic.h>
+
+    i2c@53b00000 {
+        compatible = "ti,tda54-i2c";
+        reg = <0x53b00000 0x1000>;
+        interrupts = <GIC_SPI 166 IRQ_TYPE_LEVEL_HIGH>;
+        clocks = <&sysclk>, <&pclk>;
+        clock-names = "ref", "pclk";
+        clock-frequency = <400000>;
+        i2c-sda-hold-time-ns = <300>;
+        #address-cells = <1>;
+        #size-cells = <0>;
+    };
+...

-- 
2.53.0


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [PATCH v4 2/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) driver
  2026-10-27 11:30 [PATCH v4 0/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) support for TI TDA54 Aniket Limaye
  2026-10-27 11:30 ` [PATCH v4 1/2] dt-bindings: i2c: Add Synopsys DWC_i2c controller Aniket Limaye
@ 2026-10-27 11:30 ` Aniket Limaye
  1 sibling, 0 replies; 3+ messages in thread
From: Aniket Limaye @ 2026-10-27 11:30 UTC (permalink / raw)
  To: Andi Shyti, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
	Philipp Zabel
  Cc: linux-i2c, devicetree, linux-kernel, Aniket Limaye, lianfeng.ouyang

Add a new platform driver for the Synopsys Advanced I2C (DWC_i2c),
controller mode only.

The register offsets are defined with respect to header registers for
each block. These header registers are discovered at runtime by walking
the block header chain.

Signed-off-by: Aniket Limaye <a-limaye@ti.com>

---

Changes in v4:
- Drop the approach of extending the existing i2c-designware-* driver
  with a MODEL_TI_TDA54 variant. Instead, add a new standalone driver
  (i2c-dwc-adv.c) and a new binding (snps,dwc-adv-i2c.yaml), since
  DWC_i2c is not register compatible with DW_apb_i2c. (Andy Shevchenko)
- Drop the snps,tx-fifo-depth/snps,rx-fifo-depth DT properties from
  the new binding; FIFO depth is now tied to the compatible string via
  driver match data instead. (Conor Dooley)
- Link to v3: https://lore.kernel.org/all/20260925-tda54-upstream-i2c-v3-2-544d74e992ff@ti.com/

Changes in v3:
- Revert to special handling for the max_register in regmap for
  MODEL_AMD_NAVI_GPU instead of unconditionally including
  AMD_UCSI_INTR_REG in the legal regmap range.
- Link to v2: https://lore.kernel.org/all/20260925-tda54-upstream-i2c-v2-2-261785e58493@ti.com/

Changes in v2:
- Move the Reg ID -> Offset mapping inside the regmap functions such
  that existing calls to reg_read/write are unaltered. This does change
  the reg_read()/reg_write() functions to take reg IDs as input rather
  than their physical offsets, which also requires updates to the
  xe_i2c.c driver to handle the same mapping.
- Link to v1: https://lore.kernel.org/r/20260919-tda54-upstream-i2c-v1-2-b0b9f77be18b@ti.com/
---
 drivers/i2c/busses/Kconfig       |   11 +
 drivers/i2c/busses/Makefile      |    1 +
 drivers/i2c/busses/i2c-dwc-adv.c | 1333 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 1345 insertions(+)

diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig
index 275aaa7f3583..ea91221638e8 100644
--- a/drivers/i2c/busses/Kconfig
+++ b/drivers/i2c/busses/Kconfig
@@ -649,6 +649,17 @@ config I2C_DIGICOLOR
 	  This driver can also be built as a module.  If so, the module
 	  will be called i2c-digicolor.
 
+config I2C_DWC_ADV
+	tristate "Synopsys Advanced I2C (DWC_i2c) driver"
+	depends on ARCH_K3 || COMPILE_TEST
+	help
+	  If you say yes to this option, support will be included for the
+	  Synopsys Advanced I2C controller (DWC_i2c) driver. Only controller
+	  mode is supported.
+
+	  This driver can also be built as a module.  If so, the module
+	  will be called i2c-dwc-adv.
+
 config I2C_EG20T
 	tristate "Intel EG20T PCH/LAPIS Semicon IOH(ML7213/ML7223/ML7831) I2C"
 	depends on PCI && (X86_32 || MIPS || COMPILE_TEST)
diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile
index e5440de0fe14..bf710baa9c11 100644
--- a/drivers/i2c/busses/Makefile
+++ b/drivers/i2c/busses/Makefile
@@ -62,6 +62,7 @@ obj-$(CONFIG_I2C_DESIGNWARE_AMDISP) += i2c-designware-amdisp.o
 obj-$(CONFIG_I2C_DESIGNWARE_PCI)			+= i2c-designware-pci.o
 i2c-designware-pci-y					:= i2c-designware-pcidrv.o
 obj-$(CONFIG_I2C_DIGICOLOR)	+= i2c-digicolor.o
+obj-$(CONFIG_I2C_DWC_ADV)	+= i2c-dwc-adv.o
 obj-$(CONFIG_I2C_EG20T)		+= i2c-eg20t.o
 obj-$(CONFIG_I2C_EMEV2)		+= i2c-emev2.o
 obj-$(CONFIG_I2C_EXYNOS5)	+= i2c-exynos5.o
diff --git a/drivers/i2c/busses/i2c-dwc-adv.c b/drivers/i2c/busses/i2c-dwc-adv.c
new file mode 100644
index 000000000000..1c5b7b154bed
--- /dev/null
+++ b/drivers/i2c/busses/i2c-dwc-adv.c
@@ -0,0 +1,1333 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Synopsys Advanced I2C (DWC_i2c) controller driver
+ *
+ * Based on the Synopsys DesignWare I2C adapter driver (i2c-designware-*),
+ * which is based on the TI DAVINCI I2C adapter driver.
+ *
+ * Copyright (C) 2006 Texas Instruments.
+ * Copyright (C) 2007 MontaVista Software Inc.
+ * Copyright (C) 2009 Provigent Ltd.
+ * Copyright (C) 2026 Texas Instruments Incorporated - https://www.ti.com/
+ */
+
+#include <linux/array_size.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/clk.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/err.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/io.h>
+#include <linux/iopoll.h>
+#include <linux/math.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/pinctrl/consumer.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/property.h>
+#include <linux/reset.h>
+#include <linux/units.h>
+
+#define DWC_ADV_I2C_OP_ENABLE			0x04
+#define DWC_ADV_I2C_OP_I2C_CAPABILITIES		0x10
+#define DWC_ADV_I2C_OP_I2C_CAPABILITIES_LOADING_400	BIT(3)
+#define DWC_ADV_I2C_OP_I2C_BLOCK_OFFSET		0x14
+
+#define DWC_ADV_I2C_HEADER_ID			GENMASK(7, 0)
+#define DWC_ADV_I2C_HEADER_LENGTH		GENMASK(23, 8)
+#define DWC_ADV_I2C_HEADER_ID_CORE		0xa1
+#define DWC_ADV_I2C_HEADER_ID_TIMING		0xa2
+#define DWC_ADV_I2C_HEADER_ID_FIFO		0xa3
+#define DWC_ADV_I2C_HEADER_ID_INTR_DEBUG	0xa4
+#define DWC_ADV_I2C_HEADER_ID_HWID		0xa5
+
+#define DWC_ADV_I2C_CORE_CON			0x04
+#define DWC_ADV_I2C_CORE_TAR			0x08
+
+#define DWC_ADV_I2C_TIMING_HCNT			0x08
+#define DWC_ADV_I2C_TIMING_LCNT			0x0c
+#define DWC_ADV_I2C_TIMING_HS_HCNT		0x10
+#define DWC_ADV_I2C_TIMING_HS_LCNT		0x14
+#define DWC_ADV_I2C_TIMING_SDA_HOLD		0x18
+
+#define DWC_ADV_I2C_FIFO_DATA_CMD		0x04
+#define DWC_ADV_I2C_FIFO_RX_TL			0x08
+#define DWC_ADV_I2C_FIFO_TX_TL			0x0c
+
+#define DWC_ADV_I2C_INTR_INTR_STAT		0x04
+#define DWC_ADV_I2C_INTR_INTR_MASK		0x08
+#define DWC_ADV_I2C_INTR_RAW_INTR_STAT		0x0c
+#define DWC_ADV_I2C_INTR_CLR_INTR		0x10
+#define DWC_ADV_I2C_INTR_ENABLE_STATUS		0x14
+#define DWC_ADV_I2C_INTR_TX_ABRT_SOURCE		0x18
+#define DWC_ADV_I2C_INTR_STATUS			0x1c
+#define DWC_ADV_I2C_INTR_TXFLR			0x20
+#define DWC_ADV_I2C_INTR_RXFLR			0x24
+
+#define DWC_ADV_I2C_HWID_COMP_TYPE		0x0c
+#define DWC_ADV_I2C_COMP_TYPE_VALUE		0x44570140
+
+#define DWC_ADV_I2C_CON_CONTROLLER		BIT(0)
+#define DWC_ADV_I2C_CON_SPEED_MASK		GENMASK(5, 4)
+#define DWC_ADV_I2C_CON_SPEED_STD		BIT(4)
+#define DWC_ADV_I2C_CON_SPEED_FAST		BIT(5)
+#define DWC_ADV_I2C_CON_SPEED_HIGH		(BIT(4) | BIT(5))
+#define DWC_ADV_I2C_CON_10BITADDR_CTRLR		BIT(9)
+
+#define DWC_ADV_I2C_TAR_10BITADDR_CTRLR		BIT(12)
+
+#define DWC_ADV_I2C_DATA_CMD_DAT		GENMASK(7, 0)
+#define DWC_ADV_I2C_DATA_CMD_READ		BIT(8)
+#define DWC_ADV_I2C_DATA_CMD_STOP		BIT(9)
+#define DWC_ADV_I2C_DATA_CMD_RESTART		BIT(10)
+
+#define DWC_ADV_I2C_INTR_RX_UNDER		BIT(0)
+#define DWC_ADV_I2C_INTR_RX_OVER		BIT(1)
+#define DWC_ADV_I2C_INTR_RX_FULL		BIT(2)
+#define DWC_ADV_I2C_INTR_TX_OVER		BIT(3)
+#define DWC_ADV_I2C_INTR_TX_EMPTY		BIT(4)
+#define DWC_ADV_I2C_INTR_RD_REQ			BIT(5)
+#define DWC_ADV_I2C_INTR_TX_ABRT		BIT(6)
+#define DWC_ADV_I2C_INTR_RX_DONE		BIT(7)
+#define DWC_ADV_I2C_INTR_ACTIVITY		BIT(8)
+#define DWC_ADV_I2C_INTR_STOP_DET		BIT(9)
+#define DWC_ADV_I2C_INTR_START_DET		BIT(10)
+#define DWC_ADV_I2C_INTR_GEN_CALL		BIT(11)
+#define DWC_ADV_I2C_INTR_CTRLR_ON_HOLD		BIT(13)
+
+#define DWC_ADV_I2C_INTR_CLR_ALL		BIT(0)
+#define DWC_ADV_I2C_INTR_CLR_RX_UNDER		BIT(1)
+#define DWC_ADV_I2C_INTR_CLR_RX_OVER		BIT(2)
+#define DWC_ADV_I2C_INTR_CLR_TX_OVER		BIT(3)
+#define DWC_ADV_I2C_INTR_CLR_RD_REQ		BIT(4)
+#define DWC_ADV_I2C_INTR_CLR_TX_ABRT		BIT(5)
+#define DWC_ADV_I2C_INTR_CLR_RX_DONE		BIT(6)
+#define DWC_ADV_I2C_INTR_CLR_ACTIVITY		BIT(7)
+#define DWC_ADV_I2C_INTR_CLR_STOP_DET		BIT(8)
+#define DWC_ADV_I2C_INTR_CLR_START_DET		BIT(9)
+#define DWC_ADV_I2C_INTR_CLR_GEN_CALL		BIT(10)
+
+#define DWC_ADV_I2C_CTRLR_INTR_MASK \
+	(DWC_ADV_I2C_INTR_RX_FULL | DWC_ADV_I2C_INTR_TX_ABRT | \
+	 DWC_ADV_I2C_INTR_STOP_DET | DWC_ADV_I2C_INTR_TX_EMPTY)
+
+#define DWC_ADV_I2C_ENABLE_ENABLE		BIT(0)
+#define DWC_ADV_I2C_ENABLE_ABORT		BIT(1)
+
+#define DWC_ADV_I2C_STATUS_ACTIVITY		BIT(0)
+#define DWC_ADV_I2C_STATUS_CTRLR_ACTIVITY	BIT(5)
+#define DWC_ADV_I2C_STATUS_CTRLR_HOLD_TX_FIFO_EMPTY BIT(7)
+
+enum dwc_adv_i2c_abrt_bit {
+	DWC_ADV_I2C_ABRT_7B_ADDR_NOACK = 0,
+	DWC_ADV_I2C_ABRT_10ADDR1_NOACK = 1,
+	DWC_ADV_I2C_ABRT_10ADDR2_NOACK = 2,
+	DWC_ADV_I2C_ABRT_TXDATA_NOACK = 3,
+	DWC_ADV_I2C_ABRT_GCALL_NOACK = 4,
+	DWC_ADV_I2C_ABRT_GCALL_READ = 5,
+	DWC_ADV_I2C_ABRT_HS_ACKDET = 6,
+	DWC_ADV_I2C_ABRT_SBYTE_ACKDET = 7,
+	DWC_ADV_I2C_ABRT_CTRLR_DIS = 11,
+	DWC_ADV_I2C_ARB_LOST = 12,
+	DWC_ADV_I2C_ABRT_USER_TRMNT = 16,
+	DWC_ADV_I2C_ABRT_SDA_STUCK_AT_LOW = 17,
+};
+
+#define TX_ABRT_7B_ADDR_NOACK	BIT(DWC_ADV_I2C_ABRT_7B_ADDR_NOACK)
+#define TX_ABRT_10ADDR1_NOACK	BIT(DWC_ADV_I2C_ABRT_10ADDR1_NOACK)
+#define TX_ABRT_10ADDR2_NOACK	BIT(DWC_ADV_I2C_ABRT_10ADDR2_NOACK)
+#define TX_ABRT_TXDATA_NOACK	BIT(DWC_ADV_I2C_ABRT_TXDATA_NOACK)
+#define TX_ABRT_GCALL_NOACK	BIT(DWC_ADV_I2C_ABRT_GCALL_NOACK)
+#define TX_ABRT_GCALL_READ	BIT(DWC_ADV_I2C_ABRT_GCALL_READ)
+#define TX_ARB_LOST		BIT(DWC_ADV_I2C_ARB_LOST)
+#define TX_ABRT_USER_TRMNT	BIT(DWC_ADV_I2C_ABRT_USER_TRMNT)
+#define TX_ABRT_NOACK \
+	(TX_ABRT_7B_ADDR_NOACK | \
+	 TX_ABRT_10ADDR1_NOACK | \
+	 TX_ABRT_10ADDR2_NOACK | \
+	 TX_ABRT_TXDATA_NOACK | \
+	 TX_ABRT_GCALL_NOACK)
+
+#define DWC_ADV_I2C_STATUS_SW_ACTIVE		BIT(0)
+#define DWC_ADV_I2C_STATUS_SW_WRITE_IN_PROGRESS	BIT(1)
+#define DWC_ADV_I2C_STATUS_SW_READ_IN_PROGRESS	BIT(2)
+#define DWC_ADV_I2C_STATUS_SW_MASK		GENMASK(2, 0)
+
+#define DWC_ADV_I2C_ABORT_POLL_US		10
+#define DWC_ADV_I2C_ABORT_TIMEOUT_US		100
+#define DWC_ADV_I2C_DISABLE_POLL_US		250
+#define DWC_ADV_I2C_DISABLE_TIMEOUT_US		25000
+
+#define DWC_ADV_I2C_XFER_TIMEOUT_MS		1000
+#define DWC_ADV_I2C_AUTOSUSPEND_DELAY_MS	1000
+
+static const char * const dwc_adv_i2c_abort_sources[] = {
+	[DWC_ADV_I2C_ABRT_7B_ADDR_NOACK] =
+		"target address not acknowledged",
+	[DWC_ADV_I2C_ABRT_10ADDR1_NOACK] =
+		"10-bit first address byte not acknowledged",
+	[DWC_ADV_I2C_ABRT_10ADDR2_NOACK] =
+		"10-bit second address byte not acknowledged",
+	[DWC_ADV_I2C_ABRT_TXDATA_NOACK] =
+		"data not acknowledged",
+	[DWC_ADV_I2C_ABRT_GCALL_NOACK] =
+		"general call not acknowledged",
+	[DWC_ADV_I2C_ABRT_GCALL_READ] =
+		"general call read",
+	[DWC_ADV_I2C_ABRT_HS_ACKDET] =
+		"high speed controller code acknowledged",
+	[DWC_ADV_I2C_ABRT_SBYTE_ACKDET] =
+		"start byte acknowledged",
+	[DWC_ADV_I2C_ABRT_CTRLR_DIS] =
+		"disabled while active",
+	[DWC_ADV_I2C_ARB_LOST] =
+		"arbitration lost",
+	[DWC_ADV_I2C_ABRT_USER_TRMNT] =
+		"terminated by user request",
+	[DWC_ADV_I2C_ABRT_SDA_STUCK_AT_LOW] =
+		"SDA stuck at low",
+};
+
+struct dwc_adv_i2c_regs {
+	u32 enable;
+	u32 con;
+	u32 tar;
+	u32 ss_fs_hcnt;
+	u32 ss_fs_lcnt;
+	u32 hs_hcnt;
+	u32 hs_lcnt;
+	u32 sda_hold;
+	u32 data_cmd;
+	u32 rx_tl;
+	u32 tx_tl;
+	u32 intr_stat;
+	u32 intr_mask;
+	u32 raw_intr_stat;
+	u32 clr_intr;
+	u32 enable_status;
+	u32 tx_abrt_source;
+	u32 status;
+	u32 txflr;
+	u32 rxflr;
+};
+
+struct dwc_adv_i2c_dev {
+	struct device *dev;
+	void __iomem *base;
+	struct clk *clk;
+	struct clk *pclk;
+	struct reset_control *rst;
+	struct i2c_adapter adap;
+	struct completion cmd_complete;
+	struct i2c_bus_recovery_info rinfo;
+
+	struct dwc_adv_i2c_regs regs;
+	u32 tx_fifo_depth;
+	u32 rx_fifo_depth;
+
+	struct i2c_timings timings;
+	bool hs_loading_400pf;
+	u32 sda_hold_time;
+
+	u32 ctrlr_cfg;
+	u32 ss_hcnt;
+	u32 ss_lcnt;
+	u32 fs_hcnt;
+	u32 fs_lcnt;
+	u32 hs_hcnt;
+	u32 hs_lcnt;
+
+	struct i2c_msg *msgs;
+	int msgs_num;
+	int msg_write_idx;
+	int msg_read_idx;
+	int msg_err;
+	int cmd_err;
+	unsigned int status;
+	unsigned int abort_source;
+	int rx_outstanding;
+	bool need_restart;
+
+	u8 *tx_buf;
+	size_t tx_buf_len;
+	u8 *rx_buf;
+	size_t rx_buf_len;
+};
+
+struct dwc_adv_i2c_match_data {
+	u32 tx_fifo_depth;
+	u32 rx_fifo_depth;
+};
+
+static const struct dwc_adv_i2c_match_data tda54_i2c_match_data = {
+	.tx_fifo_depth = 32,
+	.rx_fifo_depth = 32,
+};
+
+static inline u32 dwc_adv_reg_read(struct dwc_adv_i2c_dev *dev, u32 offset)
+{
+	return readl(dev->base + offset);
+}
+
+static inline void dwc_adv_reg_write(struct dwc_adv_i2c_dev *dev, u32 offset, u32 val)
+{
+	writel(val, dev->base + offset);
+}
+
+static int dwc_adv_i2c_next_header(struct dwc_adv_i2c_dev *dev, u32 hdr_offset,
+				   u32 expected_id, u32 *next_offset)
+{
+	u32 hdr = dwc_adv_reg_read(dev, hdr_offset);
+	u32 id = FIELD_GET(DWC_ADV_I2C_HEADER_ID, hdr);
+	u32 length_words = FIELD_GET(DWC_ADV_I2C_HEADER_LENGTH, hdr);
+
+	if (id != expected_id) {
+		return dev_err_probe(dev->dev, -ENODEV,
+				     "header at offset 0x%x has ID 0x%x, expected 0x%x\n",
+				     hdr_offset, id, expected_id);
+	}
+
+	if (!length_words) {
+		return dev_err_probe(dev->dev, -ENODEV,
+				     "zero length header at offset 0x%x\n", hdr_offset);
+	}
+
+	if (next_offset)
+		*next_offset = hdr_offset + length_words * 4;
+
+	return 0;
+}
+
+/*
+ * Register offsets are relative to per-block header registers. Walk the
+ * header chain to find each block, starting from the core block offset.
+ */
+static int dwc_adv_i2c_discover_regs(struct dwc_adv_i2c_dev *dev)
+{
+	struct dwc_adv_i2c_regs *regs = &dev->regs;
+	u32 core_header, timing_header, fifo_header, intr_header, hwid_header;
+	u32 caps, comp_type;
+	int ret;
+
+	regs->enable = DWC_ADV_I2C_OP_ENABLE;
+	caps = dwc_adv_reg_read(dev, DWC_ADV_I2C_OP_I2C_CAPABILITIES);
+	dev->hs_loading_400pf = caps & DWC_ADV_I2C_OP_I2C_CAPABILITIES_LOADING_400;
+	core_header = dwc_adv_reg_read(dev, DWC_ADV_I2C_OP_I2C_BLOCK_OFFSET);
+	if (!core_header)
+		return dev_err_probe(dev->dev, -ENODEV, "no I2C block offset\n");
+
+	regs->con = core_header + DWC_ADV_I2C_CORE_CON;
+	regs->tar = core_header + DWC_ADV_I2C_CORE_TAR;
+
+	ret = dwc_adv_i2c_next_header(dev, core_header,
+				      DWC_ADV_I2C_HEADER_ID_CORE, &timing_header);
+	if (ret)
+		return ret;
+
+	regs->ss_fs_hcnt = timing_header + DWC_ADV_I2C_TIMING_HCNT;
+	regs->ss_fs_lcnt = timing_header + DWC_ADV_I2C_TIMING_LCNT;
+	regs->hs_hcnt = timing_header + DWC_ADV_I2C_TIMING_HS_HCNT;
+	regs->hs_lcnt = timing_header + DWC_ADV_I2C_TIMING_HS_LCNT;
+	regs->sda_hold = timing_header + DWC_ADV_I2C_TIMING_SDA_HOLD;
+
+	ret = dwc_adv_i2c_next_header(dev, timing_header,
+				      DWC_ADV_I2C_HEADER_ID_TIMING, &fifo_header);
+	if (ret)
+		return ret;
+
+	regs->data_cmd = fifo_header + DWC_ADV_I2C_FIFO_DATA_CMD;
+	regs->rx_tl = fifo_header + DWC_ADV_I2C_FIFO_RX_TL;
+	regs->tx_tl = fifo_header + DWC_ADV_I2C_FIFO_TX_TL;
+
+	ret = dwc_adv_i2c_next_header(dev, fifo_header,
+				      DWC_ADV_I2C_HEADER_ID_FIFO, &intr_header);
+	if (ret)
+		return ret;
+
+	regs->intr_stat = intr_header + DWC_ADV_I2C_INTR_INTR_STAT;
+	regs->intr_mask = intr_header + DWC_ADV_I2C_INTR_INTR_MASK;
+	regs->raw_intr_stat = intr_header + DWC_ADV_I2C_INTR_RAW_INTR_STAT;
+	regs->clr_intr = intr_header + DWC_ADV_I2C_INTR_CLR_INTR;
+	regs->enable_status = intr_header + DWC_ADV_I2C_INTR_ENABLE_STATUS;
+	regs->tx_abrt_source = intr_header + DWC_ADV_I2C_INTR_TX_ABRT_SOURCE;
+	regs->status = intr_header + DWC_ADV_I2C_INTR_STATUS;
+	regs->txflr = intr_header + DWC_ADV_I2C_INTR_TXFLR;
+	regs->rxflr = intr_header + DWC_ADV_I2C_INTR_RXFLR;
+
+	ret = dwc_adv_i2c_next_header(dev, intr_header,
+				      DWC_ADV_I2C_HEADER_ID_INTR_DEBUG, &hwid_header);
+	if (ret)
+		return ret;
+
+	ret = dwc_adv_i2c_next_header(dev, hwid_header,
+				      DWC_ADV_I2C_HEADER_ID_HWID, NULL);
+	if (ret)
+		return ret;
+
+	comp_type = dwc_adv_reg_read(dev, hwid_header + DWC_ADV_I2C_HWID_COMP_TYPE);
+	if (comp_type != DWC_ADV_I2C_COMP_TYPE_VALUE)
+		return dev_err_probe(dev->dev, -ENODEV,
+				     "unknown Synopsys component type: 0x%08x\n",
+				     comp_type);
+
+	return 0;
+}
+
+static void dwc_adv_i2c_ack_intr(struct dwc_adv_i2c_dev *dev, u32 clr_bit)
+{
+	dwc_adv_reg_write(dev, dev->regs.clr_intr, clr_bit);
+}
+
+static u32 dwc_adv_i2c_scl_hcnt(u32 ic_clk, u32 tsymbol, u32 tf, int offset)
+{
+	return DIV_ROUND_CLOSEST_ULL((u64)ic_clk * (tsymbol + tf), MICRO) - 3 + offset;
+}
+
+static u32 dwc_adv_i2c_scl_lcnt(u32 ic_clk, u32 tlow, u32 tf, int offset)
+{
+	return DIV_ROUND_CLOSEST_ULL((u64)ic_clk * (tlow + tf), MICRO) - 1 + offset;
+}
+
+static void dwc_adv_i2c_set_timings(struct dwc_adv_i2c_dev *dev)
+{
+	struct i2c_timings *t = &dev->timings;
+	u32 ic_clk = clk_get_rate(dev->clk) / 1000;
+	u32 sda_falling_time = t->sda_fall_ns ?: 300;
+	u32 scl_falling_time = t->scl_fall_ns ?: 300;
+	u32 t_high, t_low;
+
+	dev->ss_hcnt = dwc_adv_i2c_scl_hcnt(ic_clk, 4000, sda_falling_time, 0);
+	dev->ss_lcnt = dwc_adv_i2c_scl_lcnt(ic_clk, 4700, scl_falling_time, 0);
+
+	if (t->bus_freq_hz == I2C_MAX_FAST_MODE_PLUS_FREQ) {
+		dev->fs_hcnt = dwc_adv_i2c_scl_hcnt(ic_clk, 260, sda_falling_time, 0);
+		dev->fs_lcnt = dwc_adv_i2c_scl_lcnt(ic_clk, 500, scl_falling_time, 0);
+	} else {
+		dev->fs_hcnt = dwc_adv_i2c_scl_hcnt(ic_clk, 600, sda_falling_time, 0);
+		dev->fs_lcnt = dwc_adv_i2c_scl_lcnt(ic_clk, 1300, scl_falling_time, 0);
+	}
+
+	if (dev->hs_loading_400pf) {
+		t_high = 120;
+		t_low = 320;
+	} else {
+		t_high = 60;
+		t_low = 160;
+	}
+	dev->hs_hcnt = dwc_adv_i2c_scl_hcnt(ic_clk, t_high, sda_falling_time, 0);
+	dev->hs_lcnt = dwc_adv_i2c_scl_lcnt(ic_clk, t_low, scl_falling_time, 0);
+}
+
+static void dwc_adv_i2c_write_timings(struct dwc_adv_i2c_dev *dev)
+{
+	u32 speed = dev->ctrlr_cfg & DWC_ADV_I2C_CON_SPEED_MASK;
+
+	if (speed == DWC_ADV_I2C_CON_SPEED_HIGH) {
+		dwc_adv_reg_write(dev, dev->regs.hs_hcnt, dev->hs_hcnt);
+		dwc_adv_reg_write(dev, dev->regs.hs_lcnt, dev->hs_lcnt);
+		dwc_adv_reg_write(dev, dev->regs.ss_fs_hcnt, dev->fs_hcnt);
+		dwc_adv_reg_write(dev, dev->regs.ss_fs_lcnt, dev->fs_lcnt);
+	} else if (speed == DWC_ADV_I2C_CON_SPEED_STD) {
+		dwc_adv_reg_write(dev, dev->regs.ss_fs_hcnt, dev->ss_hcnt);
+		dwc_adv_reg_write(dev, dev->regs.ss_fs_lcnt, dev->ss_lcnt);
+	} else {
+		dwc_adv_reg_write(dev, dev->regs.ss_fs_hcnt, dev->fs_hcnt);
+		dwc_adv_reg_write(dev, dev->regs.ss_fs_lcnt, dev->fs_lcnt);
+	}
+}
+
+static void __dwc_adv_i2c_enable(struct dwc_adv_i2c_dev *dev)
+{
+	dev->status |= DWC_ADV_I2C_STATUS_SW_ACTIVE;
+	dwc_adv_reg_write(dev, dev->regs.enable, DWC_ADV_I2C_ENABLE_ENABLE);
+}
+
+static void __dwc_adv_i2c_disable_nowait(struct dwc_adv_i2c_dev *dev)
+{
+	u32 enable = dwc_adv_reg_read(dev, dev->regs.enable);
+
+	dwc_adv_reg_write(dev, dev->regs.enable, enable & ~DWC_ADV_I2C_ENABLE_ENABLE);
+	dev->status &= ~DWC_ADV_I2C_STATUS_SW_ACTIVE;
+}
+
+static u32 dwc_adv_i2c_try_disable(struct dwc_adv_i2c_dev *dev)
+{
+	__dwc_adv_i2c_disable_nowait(dev);
+
+	return dwc_adv_reg_read(dev, dev->regs.enable_status);
+}
+
+/*
+ * If the controller is holding the bus with an empty TX FIFO, terminate the
+ * transfer first. IC_ENABLE.TERMINATE is only honoured while ENABLE is set.
+ */
+static void __dwc_adv_i2c_disable(struct dwc_adv_i2c_dev *dev)
+{
+	u32 raw_intr_stat = dwc_adv_reg_read(dev, dev->regs.raw_intr_stat);
+	u32 ic_status = dwc_adv_reg_read(dev, dev->regs.status);
+	u32 enable = dwc_adv_reg_read(dev, dev->regs.enable);
+	bool abort_needed = (raw_intr_stat & DWC_ADV_I2C_INTR_CTRLR_ON_HOLD) ||
+			     (ic_status & DWC_ADV_I2C_STATUS_CTRLR_HOLD_TX_FIFO_EMPTY);
+	u32 status;
+	int ret;
+
+	if (abort_needed) {
+		if (!(enable & DWC_ADV_I2C_ENABLE_ENABLE)) {
+			dwc_adv_reg_write(dev, dev->regs.enable, DWC_ADV_I2C_ENABLE_ENABLE);
+			/* Let ENABLE take effect: 10 signaling periods */
+			fsleep(DIV_ROUND_CLOSEST_ULL(10 * MICRO, dev->timings.bus_freq_hz));
+			enable |= DWC_ADV_I2C_ENABLE_ENABLE;
+		}
+		dwc_adv_reg_write(dev, dev->regs.enable, enable | DWC_ADV_I2C_ENABLE_ABORT);
+
+		ret = readl_poll_timeout(dev->base + dev->regs.enable, enable,
+					 !(enable & DWC_ADV_I2C_ENABLE_ABORT),
+					 DWC_ADV_I2C_ABORT_POLL_US,
+					 DWC_ADV_I2C_ABORT_TIMEOUT_US);
+		if (ret)
+			dev_err(dev->dev, "timeout while trying to abort current transfer\n");
+	}
+
+	ret = read_poll_timeout(dwc_adv_i2c_try_disable, status,
+				!(status & DWC_ADV_I2C_ENABLE_ENABLE),
+				DWC_ADV_I2C_DISABLE_POLL_US,
+				DWC_ADV_I2C_DISABLE_TIMEOUT_US, false, dev);
+	if (ret)
+		dev_warn(dev->dev, "timeout in disabling adapter\n");
+}
+
+static void dwc_adv_i2c_init(struct dwc_adv_i2c_dev *dev)
+{
+	__dwc_adv_i2c_disable(dev);
+
+	dwc_adv_i2c_write_timings(dev);
+
+	if (dev->sda_hold_time)
+		dwc_adv_reg_write(dev, dev->regs.sda_hold, dev->sda_hold_time);
+
+	dwc_adv_reg_write(dev, dev->regs.tx_tl, dev->tx_fifo_depth / 2);
+	dwc_adv_reg_write(dev, dev->regs.rx_tl, 0);
+	dwc_adv_reg_write(dev, dev->regs.con, dev->ctrlr_cfg);
+}
+
+static int dwc_adv_i2c_wait_bus_not_busy(struct dwc_adv_i2c_dev *dev)
+{
+	u32 status;
+	int ret;
+
+	ret = readl_poll_timeout(dev->base + dev->regs.status, status,
+				 !(status & DWC_ADV_I2C_STATUS_ACTIVITY),
+				 1000, 20000);
+	if (ret) {
+		dev_warn(dev->dev, "timeout waiting for bus ready\n");
+		i2c_recover_bus(&dev->adap);
+
+		status = dwc_adv_reg_read(dev, dev->regs.status);
+		if (!(status & DWC_ADV_I2C_STATUS_ACTIVITY))
+			ret = 0;
+	}
+
+	return ret;
+}
+
+/*
+ * Wait for the controller to go idle once the transfer has completed (STOP_DET
+ * or TX_ABRT), so that disabling it does not cut the transfer short.
+ * Return: true if it is still active after the timeout.
+ */
+static bool dwc_adv_i2c_is_controller_active(struct dwc_adv_i2c_dev *dev)
+{
+	u32 status;
+
+	status = dwc_adv_reg_read(dev, dev->regs.status);
+	if (!(status & DWC_ADV_I2C_STATUS_CTRLR_ACTIVITY))
+		return false;
+
+	return readl_poll_timeout(dev->base + dev->regs.status, status,
+				  !(status & DWC_ADV_I2C_STATUS_CTRLR_ACTIVITY),
+				  1100, 20000) != 0;
+}
+
+static void dwc_adv_i2c_xfer_init(struct dwc_adv_i2c_dev *dev)
+{
+	struct i2c_msg *msg = &dev->msgs[dev->msg_write_idx];
+	u32 con;
+
+	__dwc_adv_i2c_disable(dev);
+
+	/* If the target address is ten bit address, enable 10BITADDR */
+	con = dev->ctrlr_cfg;
+	if (msg->flags & I2C_M_TEN)
+		con |= DWC_ADV_I2C_CON_10BITADDR_CTRLR;
+	else
+		con &= ~DWC_ADV_I2C_CON_10BITADDR_CTRLR;
+	dwc_adv_reg_write(dev, dev->regs.con, con);
+
+	/*
+	 * With IC_DYNAMIC_TAR_UPDATE the 10-bit addressing mode is controlled
+	 * by IC_TAR bit 12 and the IC_CTRL bit is read-only. This cannot be
+	 * detected from the registers, so always set it.
+	 */
+	dwc_adv_reg_write(dev, dev->regs.tar, msg->addr |
+			   ((msg->flags & I2C_M_TEN) ? DWC_ADV_I2C_TAR_10BITADDR_CTRLR : 0));
+
+	/*
+	 * Enforce disabled interrupts: IC_INTR_MASK resets with TX_EMPTY
+	 * unmasked, which would fire as soon as the controller is enabled.
+	 */
+	dwc_adv_reg_write(dev, dev->regs.intr_mask, 0);
+
+	/* Enable the adapter, then clear and enable interrupts */
+	__dwc_adv_i2c_enable(dev);
+	dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_ALL);
+	dwc_adv_reg_write(dev, dev->regs.intr_mask, DWC_ADV_I2C_CTRLR_INTR_MASK);
+}
+
+/*
+ * Initiate (and continue) low level controller read/write transaction.
+ * This function is only called from dwc_adv_i2c_isr(), and pumps i2c_msg
+ * messages into the TX FIFO. Even if the size of i2c_msg data is longer
+ * than the TX FIFO, it handles everything.
+ */
+static void dwc_adv_i2c_xfer_msg(struct dwc_adv_i2c_dev *dev)
+{
+	struct i2c_msg *msgs = dev->msgs;
+	u32 intr_mask = DWC_ADV_I2C_CTRLR_INTR_MASK;
+	int tx_limit, rx_limit;
+	u32 buf_len = dev->tx_buf_len;
+	u8 *buf = dev->tx_buf;
+	u32 cmd;
+	int rx_tl;
+
+	for (; dev->msg_write_idx < dev->msgs_num; dev->msg_write_idx++) {
+		u32 flags = msgs[dev->msg_write_idx].flags;
+
+		if (!(dev->status & DWC_ADV_I2C_STATUS_SW_WRITE_IN_PROGRESS)) {
+			buf = msgs[dev->msg_write_idx].buf;
+			buf_len = msgs[dev->msg_write_idx].len;
+		}
+
+		tx_limit = dev->tx_fifo_depth - dwc_adv_reg_read(dev, dev->regs.txflr);
+		rx_limit = dev->rx_fifo_depth - dwc_adv_reg_read(dev, dev->regs.rxflr);
+
+		while (buf_len > 0 && tx_limit > 0 && rx_limit > 0) {
+			cmd = 0;
+
+			/*
+			 * The bus is held while the TX FIFO is empty unless
+			 * STOP is requested in IC_DATA_CMD, so always set it
+			 * on the last byte of the last message.
+			 *
+			 * i2c-core always sets the buffer length of
+			 * I2C_FUNC_SMBUS_BLOCK_DATA to 1. The length will be
+			 * adjusted when receiving the first byte. Thus we
+			 * can't stop the transaction here.
+			 */
+			if (dev->msg_write_idx == dev->msgs_num - 1 &&
+			    buf_len == 1 && !(flags & I2C_M_RECV_LEN))
+				cmd |= DWC_ADV_I2C_DATA_CMD_STOP;
+
+			/*
+			 * A RESTART is only issued automatically when the
+			 * direction changes. Request it explicitly at the
+			 * start of each following message.
+			 */
+			if (dev->need_restart) {
+				cmd |= DWC_ADV_I2C_DATA_CMD_RESTART;
+				dev->need_restart = false;
+			}
+
+			if (msgs[dev->msg_write_idx].flags & I2C_M_RD) {
+				/* Avoid rx buffer overrun */
+				if (dev->rx_outstanding >= dev->rx_fifo_depth)
+					break;
+				dwc_adv_reg_write(dev, dev->regs.data_cmd,
+						  cmd | DWC_ADV_I2C_DATA_CMD_READ);
+				rx_limit--;
+				dev->rx_outstanding++;
+			} else {
+				dwc_adv_reg_write(dev, dev->regs.data_cmd, cmd | *buf++);
+			}
+			tx_limit--;
+			buf_len--;
+		}
+
+		dev->tx_buf = buf;
+		dev->tx_buf_len = buf_len;
+
+		/*
+		 * Because we don't know the buffer length in the
+		 * I2C_FUNC_SMBUS_BLOCK_DATA case, we can't stop the
+		 * transaction here. Also disable the TX_EMPTY IRQ while
+		 * waiting for the data length byte to avoid a flood of
+		 * bogus interrupts.
+		 */
+		if (flags & I2C_M_RECV_LEN) {
+			dev->status |= DWC_ADV_I2C_STATUS_SW_WRITE_IN_PROGRESS;
+			intr_mask &= ~DWC_ADV_I2C_INTR_TX_EMPTY;
+			break;
+		}
+
+		if (buf_len > 0) {
+			dev->status |= DWC_ADV_I2C_STATUS_SW_WRITE_IN_PROGRESS;
+			break;
+		}
+		dev->status &= ~DWC_ADV_I2C_STATUS_SW_WRITE_IN_PROGRESS;
+		dev->need_restart = true;
+	}
+
+	/*
+	 * If i2c_msg index search is completed, we don't need TX_EMPTY
+	 * interrupt any more.
+	 */
+	if (dev->msg_write_idx == dev->msgs_num)
+		intr_mask &= ~DWC_ADV_I2C_INTR_TX_EMPTY;
+
+	if (dev->msg_err)
+		intr_mask = 0;
+
+	/*
+	 * Size the RX FIFO threshold to the reads already queued, so the
+	 * controller raises one RX_FULL for the whole burst instead of one per
+	 * received byte. While messages remain to be queued, cap it so RX_FULL
+	 * still arrives in time to drain the FIFO and let the next TX_EMPTY
+	 * queue the rest.
+	 */
+	rx_tl = dev->rx_outstanding;
+	if (dev->msg_write_idx < dev->msgs_num)
+		rx_tl = min_t(int, rx_tl, dev->rx_fifo_depth / 2);
+	dwc_adv_reg_write(dev, dev->regs.rx_tl, rx_tl ? rx_tl - 1 : 0);
+
+	dwc_adv_reg_write(dev, dev->regs.intr_mask, intr_mask);
+}
+
+static u8 dwc_adv_i2c_recv_len(struct dwc_adv_i2c_dev *dev, u8 len)
+{
+	struct i2c_msg *msgs = dev->msgs;
+	u32 flags = msgs[dev->msg_read_idx].flags;
+	u32 intr_mask;
+
+	/*
+	 * Adjust the buffer length and mask the flag
+	 * after receiving the first byte.
+	 */
+	len += (flags & I2C_CLIENT_PEC) ? 2 : 1;
+	dev->tx_buf_len = len - min_t(int, len, dev->rx_outstanding);
+	msgs[dev->msg_read_idx].len = len;
+	msgs[dev->msg_read_idx].flags &= ~I2C_M_RECV_LEN;
+
+	/*
+	 * Received buffer length, re-enable TX_EMPTY interrupt
+	 * to resume the SMBus transaction.
+	 */
+	intr_mask = dwc_adv_reg_read(dev, dev->regs.intr_mask);
+	intr_mask |= DWC_ADV_I2C_INTR_TX_EMPTY;
+	dwc_adv_reg_write(dev, dev->regs.intr_mask, intr_mask);
+
+	return len;
+}
+
+static void dwc_adv_i2c_read(struct dwc_adv_i2c_dev *dev)
+{
+	struct i2c_msg *msgs = dev->msgs;
+	int rx_valid;
+
+	for (; dev->msg_read_idx < dev->msgs_num; dev->msg_read_idx++) {
+		u32 len;
+		u8 *buf;
+
+		if (!(msgs[dev->msg_read_idx].flags & I2C_M_RD))
+			continue;
+
+		if (!(dev->status & DWC_ADV_I2C_STATUS_SW_READ_IN_PROGRESS)) {
+			len = msgs[dev->msg_read_idx].len;
+			buf = msgs[dev->msg_read_idx].buf;
+		} else {
+			len = dev->rx_buf_len;
+			buf = dev->rx_buf;
+		}
+
+		rx_valid = dwc_adv_reg_read(dev, dev->regs.rxflr);
+
+		for (; len > 0 && rx_valid > 0; len--, rx_valid--) {
+			u32 data = dwc_adv_reg_read(dev, dev->regs.data_cmd) &
+				   DWC_ADV_I2C_DATA_CMD_DAT;
+
+			if (msgs[dev->msg_read_idx].flags & I2C_M_RECV_LEN) {
+				/*
+				 * STOP is only set after receiving the block
+				 * data response length. When that length is
+				 * invalid, read one more byte with STOP set to
+				 * complete the transaction.
+				 */
+				if (!data || data > I2C_SMBUS_BLOCK_MAX)
+					data = 1;
+
+				len = dwc_adv_i2c_recv_len(dev, data);
+			}
+			*buf++ = data;
+			dev->rx_outstanding--;
+		}
+
+		if (len > 0) {
+			dev->status |= DWC_ADV_I2C_STATUS_SW_READ_IN_PROGRESS;
+			dev->rx_buf_len = len;
+			dev->rx_buf = buf;
+			return;
+		}
+		dev->status &= ~DWC_ADV_I2C_STATUS_SW_READ_IN_PROGRESS;
+	}
+}
+
+/*
+ * Clear the sources individually: IC_CLR_INTR could clear interrupts raised
+ * between reading IC_INTR_STAT and the clear. The abort source is saved
+ * before TX_ABRT is cleared, as clearing also resets IC_TX_TRMNT_SOURCE.
+ */
+static u32 dwc_adv_i2c_read_clear_intrbits(struct dwc_adv_i2c_dev *dev)
+{
+	u32 stat = dwc_adv_reg_read(dev, dev->regs.intr_stat);
+
+	if (stat & DWC_ADV_I2C_INTR_RX_UNDER)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_RX_UNDER);
+	if (stat & DWC_ADV_I2C_INTR_RX_OVER)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_RX_OVER);
+	if (stat & DWC_ADV_I2C_INTR_TX_OVER)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_TX_OVER);
+	if (stat & DWC_ADV_I2C_INTR_RD_REQ)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_RD_REQ);
+	if (stat & DWC_ADV_I2C_INTR_TX_ABRT) {
+		dev->abort_source = dwc_adv_reg_read(dev, dev->regs.tx_abrt_source);
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_TX_ABRT);
+	}
+	if (stat & DWC_ADV_I2C_INTR_RX_DONE)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_RX_DONE);
+	if (stat & DWC_ADV_I2C_INTR_ACTIVITY)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_ACTIVITY);
+	if (stat & DWC_ADV_I2C_INTR_STOP_DET &&
+	    (dev->rx_outstanding == 0 || (stat & DWC_ADV_I2C_INTR_RX_FULL)))
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_STOP_DET);
+	if (stat & DWC_ADV_I2C_INTR_START_DET)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_START_DET);
+	if (stat & DWC_ADV_I2C_INTR_GEN_CALL)
+		dwc_adv_i2c_ack_intr(dev, DWC_ADV_I2C_INTR_CLR_GEN_CALL);
+
+	return stat;
+}
+
+static void dwc_adv_i2c_process_transfer(struct dwc_adv_i2c_dev *dev, u32 stat)
+{
+	if (stat & DWC_ADV_I2C_INTR_TX_ABRT) {
+		/* TX_ABRT flushes the TX/RX FIFOs, nothing left to process */
+		dev->cmd_err |= DWC_ADV_I2C_INTR_TX_ABRT;
+		dev->status &= ~DWC_ADV_I2C_STATUS_SW_MASK;
+		dev->rx_outstanding = 0;
+		dwc_adv_reg_write(dev, dev->regs.intr_mask, 0);
+		goto tx_aborted;
+	}
+
+	if (stat & DWC_ADV_I2C_INTR_RX_FULL)
+		dwc_adv_i2c_read(dev);
+
+	if (stat & DWC_ADV_I2C_INTR_TX_EMPTY)
+		dwc_adv_i2c_xfer_msg(dev);
+
+	/* A STOP in the middle of a read or write is an error */
+	if ((stat & DWC_ADV_I2C_INTR_STOP_DET) &&
+	    (dev->status & (DWC_ADV_I2C_STATUS_SW_WRITE_IN_PROGRESS |
+			     DWC_ADV_I2C_STATUS_SW_READ_IN_PROGRESS))) {
+		dev_err(dev->dev, "spurious STOP, status 0x%x\n", dev->status);
+		dev->rx_outstanding = 0;
+		dev->msg_err = -EIO;
+	}
+
+tx_aborted:
+	if (((stat & (DWC_ADV_I2C_INTR_TX_ABRT | DWC_ADV_I2C_INTR_STOP_DET)) ||
+	     dev->msg_err) && dev->rx_outstanding == 0)
+		complete(&dev->cmd_complete);
+}
+
+static irqreturn_t dwc_adv_i2c_handle_irq(struct dwc_adv_i2c_dev *dev)
+{
+	u32 enabled, stat;
+
+	enabled = dwc_adv_reg_read(dev, dev->regs.enable);
+	stat = dwc_adv_reg_read(dev, dev->regs.raw_intr_stat);
+
+	if (!enabled || !(stat & ~DWC_ADV_I2C_INTR_ACTIVITY))
+		return IRQ_NONE;
+
+	if (stat == GENMASK(31, 0))
+		return IRQ_NONE;
+
+	stat = dwc_adv_i2c_read_clear_intrbits(dev);
+
+	if (!(dev->status & DWC_ADV_I2C_STATUS_SW_ACTIVE)) {
+		dwc_adv_reg_write(dev, dev->regs.intr_mask, 0);
+		return IRQ_HANDLED;
+	}
+
+	dwc_adv_i2c_process_transfer(dev, stat);
+
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t dwc_adv_i2c_isr(int irq, void *dev_id)
+{
+	struct dwc_adv_i2c_dev *dev = dev_id;
+	irqreturn_t ret;
+	int pm;
+
+	pm = pm_runtime_get_if_active(dev->dev);
+	if (!pm)
+		return IRQ_NONE;
+
+	ret = dwc_adv_i2c_handle_irq(dev);
+
+	if (pm > 0)
+		pm_runtime_put_autosuspend(dev->dev);
+
+	return ret;
+}
+
+static int dwc_adv_i2c_handle_tx_abort(struct dwc_adv_i2c_dev *dev)
+{
+	u32 abort_source = dev->abort_source;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(dwc_adv_i2c_abort_sources); i++) {
+		if (!(abort_source & BIT(i)) || !dwc_adv_i2c_abort_sources[i])
+			continue;
+		if (BIT(i) & (TX_ABRT_NOACK | TX_ABRT_USER_TRMNT))
+			dev_dbg(dev->dev, "%s\n", dwc_adv_i2c_abort_sources[i]);
+		else
+			dev_err(dev->dev, "%s\n", dwc_adv_i2c_abort_sources[i]);
+	}
+
+	if (abort_source & TX_ABRT_NOACK)
+		return -EREMOTEIO;
+	if (abort_source & TX_ARB_LOST)
+		return -EAGAIN;
+	if (abort_source & TX_ABRT_GCALL_READ)
+		return -EINVAL;
+
+	return -EIO;
+}
+
+static bool dwc_adv_i2c_msg_is_valid(struct dwc_adv_i2c_dev *dev,
+				     struct i2c_msg *msgs, int idx)
+{
+	if (idx == 0)
+		return true;
+
+	if (msgs[idx - 1].addr != msgs[idx].addr) {
+		dev_err(dev->dev, "invalid target address\n");
+		return false;
+	}
+
+	return true;
+}
+
+static int dwc_adv_i2c_xfer_one_part(struct dwc_adv_i2c_dev *dev)
+{
+	int ret;
+
+	reinit_completion(&dev->cmd_complete);
+	dev->cmd_err = 0;
+	dev->msg_write_idx = 0;
+	dev->msg_read_idx = 0;
+	dev->msg_err = 0;
+	dev->status = 0;
+	dev->abort_source = 0;
+	dev->rx_outstanding = 0;
+	dev->need_restart = false;
+
+	ret = dwc_adv_i2c_wait_bus_not_busy(dev);
+	if (ret)
+		return ret;
+
+	dwc_adv_i2c_xfer_init(dev);
+
+	if (!wait_for_completion_timeout(&dev->cmd_complete, dev->adap.timeout)) {
+		dev_err(dev->dev, "controller timed out\n");
+		i2c_recover_bus(&dev->adap);
+		dwc_adv_i2c_init(dev);
+		return -ETIMEDOUT;
+	}
+
+	if (dwc_adv_i2c_is_controller_active(dev)) {
+		/*
+		 * If the controller is still active after the timeout, attempt a
+		 * bus recovery to clear any potentially locked state.
+		 */
+		dev_err(dev->dev, "controller active after xfer, recovering\n");
+		i2c_recover_bus(&dev->adap);
+		dwc_adv_i2c_init(dev);
+	} else {
+		/*
+		 * Disable the controller before returning, so that late
+		 * interrupts do not race with the following transfer.
+		 */
+		__dwc_adv_i2c_disable_nowait(dev);
+	}
+
+	if (dev->msg_err)
+		return dev->msg_err;
+
+	if (!dev->cmd_err && !dev->status)
+		return 0;
+
+	if (dev->cmd_err & DWC_ADV_I2C_INTR_TX_ABRT)
+		return dwc_adv_i2c_handle_tx_abort(dev);
+
+	if (dev->status)
+		dev_err(dev->dev, "transfer terminated early, status 0x%x\n", dev->status);
+
+	return -EIO;
+}
+
+static int dwc_adv_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg msgs[], int num)
+{
+	struct dwc_adv_i2c_dev *dev = i2c_get_adapdata(adap);
+	int ret, idx, start = 0;
+
+	PM_RUNTIME_ACQUIRE_AUTOSUSPEND(dev->dev, pm);
+	if (PM_RUNTIME_ACQUIRE_ERR(&pm))
+		return -ENXIO;
+
+	for (idx = 0; idx < num; idx++) {
+		if (!dwc_adv_i2c_msg_is_valid(dev, &msgs[start], idx - start))
+			return -EINVAL;
+
+		if (!(msgs[idx].flags & I2C_M_STOP) && idx != num - 1)
+			continue;
+
+		dev->msgs = &msgs[start];
+		dev->msgs_num = idx - start + 1;
+
+		ret = dwc_adv_i2c_xfer_one_part(dev);
+		if (ret)
+			return ret;
+
+		start = idx + 1;
+	}
+
+	return num;
+}
+
+static u32 dwc_adv_i2c_func(struct i2c_adapter *adap)
+{
+	return I2C_FUNC_I2C | I2C_FUNC_SMBUS_BYTE | I2C_FUNC_SMBUS_BYTE_DATA |
+	       I2C_FUNC_SMBUS_WORD_DATA | I2C_FUNC_SMBUS_BLOCK_DATA |
+	       I2C_FUNC_SMBUS_I2C_BLOCK | I2C_FUNC_10BIT_ADDR | I2C_FUNC_PROTOCOL_MANGLING;
+}
+
+static const struct i2c_algorithm dwc_adv_i2c_algo = {
+	.xfer = dwc_adv_i2c_xfer,
+	.functionality = dwc_adv_i2c_func,
+};
+
+static const struct i2c_adapter_quirks dwc_adv_i2c_quirks = {
+	.flags = I2C_AQ_NO_ZERO_LEN,
+};
+
+static int dwc_adv_i2c_prepare_clk(struct dwc_adv_i2c_dev *dev, bool prepare)
+{
+	int ret;
+
+	if (!prepare) {
+		clk_disable_unprepare(dev->clk);
+		clk_disable_unprepare(dev->pclk);
+		return 0;
+	}
+
+	ret = clk_prepare_enable(dev->pclk);
+	if (ret)
+		return ret;
+
+	ret = clk_prepare_enable(dev->clk);
+	if (ret)
+		clk_disable_unprepare(dev->pclk);
+
+	return ret;
+}
+
+static void dwc_adv_i2c_prepare_recovery(struct i2c_adapter *adap)
+{
+	struct dwc_adv_i2c_dev *dev = i2c_get_adapdata(adap);
+
+	__dwc_adv_i2c_disable(dev);
+	reset_control_assert(dev->rst);
+	dwc_adv_i2c_prepare_clk(dev, false);
+}
+
+static void dwc_adv_i2c_unprepare_recovery(struct i2c_adapter *adap)
+{
+	struct dwc_adv_i2c_dev *dev = i2c_get_adapdata(adap);
+	int ret;
+
+	ret = dwc_adv_i2c_prepare_clk(dev, true);
+	if (ret) {
+		dev_err(dev->dev, "failed to enable clock for recovery: %d\n", ret);
+		reset_control_deassert(dev->rst);
+		return;
+	}
+
+	reset_control_deassert(dev->rst);
+	dwc_adv_i2c_init(dev);
+}
+
+static int dwc_adv_i2c_init_recovery_info(struct dwc_adv_i2c_dev *dev)
+{
+	struct i2c_bus_recovery_info *rinfo = &dev->rinfo;
+
+	rinfo->scl_gpiod = devm_gpiod_get_optional(dev->dev, "scl", GPIOD_OUT_HIGH);
+	if (IS_ERR_OR_NULL(rinfo->scl_gpiod))
+		return PTR_ERR_OR_ZERO(rinfo->scl_gpiod);
+
+	rinfo->sda_gpiod = devm_gpiod_get_optional(dev->dev, "sda", GPIOD_IN);
+	if (IS_ERR(rinfo->sda_gpiod))
+		return PTR_ERR(rinfo->sda_gpiod);
+
+	rinfo->pinctrl = devm_pinctrl_get(dev->dev);
+	if (IS_ERR(rinfo->pinctrl)) {
+		if (PTR_ERR(rinfo->pinctrl) == -EPROBE_DEFER)
+			return -EPROBE_DEFER;
+		rinfo->pinctrl = NULL;
+	}
+
+	rinfo->recover_bus = i2c_generic_scl_recovery;
+	rinfo->prepare_recovery = dwc_adv_i2c_prepare_recovery;
+	rinfo->unprepare_recovery = dwc_adv_i2c_unprepare_recovery;
+	dev->adap.bus_recovery_info = rinfo;
+
+	return 0;
+}
+
+static void dwc_adv_i2c_configure_controller(struct dwc_adv_i2c_dev *dev)
+{
+	dev->ctrlr_cfg = DWC_ADV_I2C_CON_CONTROLLER;
+
+	switch (dev->timings.bus_freq_hz) {
+	case I2C_MAX_STANDARD_MODE_FREQ:
+		dev->ctrlr_cfg |= DWC_ADV_I2C_CON_SPEED_STD;
+		break;
+	case I2C_MAX_HIGH_SPEED_MODE_FREQ:
+		dev->ctrlr_cfg |= DWC_ADV_I2C_CON_SPEED_HIGH;
+		break;
+	default:
+		dev->ctrlr_cfg |= DWC_ADV_I2C_CON_SPEED_FAST;
+		break;
+	}
+}
+
+static int dwc_adv_i2c_parse_fw(struct dwc_adv_i2c_dev *dev)
+{
+	struct device *device = dev->dev;
+
+	i2c_parse_fw_timings(device, &dev->timings, false);
+	if (!dev->timings.bus_freq_hz)
+		dev->timings.bus_freq_hz = I2C_MAX_FAST_MODE_FREQ;
+
+	switch (dev->timings.bus_freq_hz) {
+	case I2C_MAX_STANDARD_MODE_FREQ:
+	case I2C_MAX_FAST_MODE_FREQ:
+	case I2C_MAX_FAST_MODE_PLUS_FREQ:
+	case I2C_MAX_HIGH_SPEED_MODE_FREQ:
+		break;
+	default:
+		return dev_err_probe(device, -EINVAL, "invalid clock-frequency %u\n",
+				     dev->timings.bus_freq_hz);
+	}
+
+	return 0;
+}
+
+static int dwc_adv_i2c_runtime_suspend(struct device *device)
+{
+	struct dwc_adv_i2c_dev *dev = dev_get_drvdata(device);
+
+	__dwc_adv_i2c_disable(dev);
+	dwc_adv_i2c_prepare_clk(dev, false);
+
+	return 0;
+}
+
+static int dwc_adv_i2c_runtime_resume(struct device *device)
+{
+	struct dwc_adv_i2c_dev *dev = dev_get_drvdata(device);
+	int ret;
+
+	ret = dwc_adv_i2c_prepare_clk(dev, true);
+	if (ret)
+		return ret;
+
+	dwc_adv_i2c_init(dev);
+
+	return 0;
+}
+
+static int dwc_adv_i2c_suspend(struct device *device)
+{
+	struct dwc_adv_i2c_dev *dev = dev_get_drvdata(device);
+
+	i2c_mark_adapter_suspended(&dev->adap);
+
+	return pm_runtime_force_suspend(device);
+}
+
+static int dwc_adv_i2c_resume(struct device *device)
+{
+	struct dwc_adv_i2c_dev *dev = dev_get_drvdata(device);
+	int ret;
+
+	ret = pm_runtime_force_resume(device);
+	if (ret)
+		return ret;
+
+	i2c_mark_adapter_resumed(&dev->adap);
+
+	return 0;
+}
+
+static const struct dev_pm_ops dwc_adv_i2c_pm_ops = {
+	LATE_SYSTEM_SLEEP_PM_OPS(dwc_adv_i2c_suspend, dwc_adv_i2c_resume)
+	RUNTIME_PM_OPS(dwc_adv_i2c_runtime_suspend, dwc_adv_i2c_runtime_resume, NULL)
+};
+
+static void dwc_adv_i2c_pm_teardown(void *data)
+{
+	struct dwc_adv_i2c_dev *dev = data;
+	int ret;
+
+	ret = pm_runtime_resume_and_get(dev->dev);
+	if (!ret)
+		__dwc_adv_i2c_disable(dev);
+	pm_runtime_dont_use_autosuspend(dev->dev);
+	if (!ret)
+		pm_runtime_put_noidle(dev->dev);
+	pm_runtime_disable(dev->dev);
+}
+
+static int dwc_adv_i2c_probe(struct platform_device *pdev)
+{
+	struct device *device = &pdev->dev;
+	const struct dwc_adv_i2c_match_data *match_data;
+	struct dwc_adv_i2c_dev *dev;
+	int irq, ret;
+
+	match_data = device_get_match_data(device);
+	if (!match_data)
+		return -ENODEV;
+
+	dev = devm_kzalloc(device, sizeof(*dev), GFP_KERNEL);
+	if (!dev)
+		return -ENOMEM;
+
+	dev->dev = device;
+	dev->tx_fifo_depth = match_data->tx_fifo_depth;
+	dev->rx_fifo_depth = match_data->rx_fifo_depth;
+
+	dev->base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(dev->base))
+		return PTR_ERR(dev->base);
+
+	irq = platform_get_irq(pdev, 0);
+	if (irq < 0)
+		return irq;
+
+	dev->rst = devm_reset_control_get_optional_exclusive_deasserted(device, NULL);
+	if (IS_ERR(dev->rst))
+		return dev_err_probe(device, PTR_ERR(dev->rst), "failed to get reset\n");
+
+	ret = dwc_adv_i2c_parse_fw(dev);
+	if (ret)
+		return ret;
+
+	dwc_adv_i2c_configure_controller(dev);
+
+	dev->pclk = devm_clk_get_enabled(device, "pclk");
+	if (IS_ERR(dev->pclk))
+		return dev_err_probe(device, PTR_ERR(dev->pclk), "failed to get pclk\n");
+
+	dev->clk = devm_clk_get_enabled(device, "ref");
+	if (IS_ERR(dev->clk))
+		return dev_err_probe(device, PTR_ERR(dev->clk), "failed to get ref clock\n");
+
+	ret = dwc_adv_i2c_discover_regs(dev);
+	if (ret)
+		return ret;
+
+	if (dev->timings.sda_hold_ns)
+		dev->sda_hold_time =
+			DIV_S64_ROUND_CLOSEST((s64)clk_get_rate(dev->clk) / 1000 *
+					      dev->timings.sda_hold_ns, MICRO);
+
+	dwc_adv_i2c_set_timings(dev);
+
+	init_completion(&dev->cmd_complete);
+
+	platform_set_drvdata(pdev, dev);
+
+	ret = devm_request_irq(device, irq, dwc_adv_i2c_isr, 0, dev_name(device), dev);
+	if (ret)
+		return dev_err_probe(device, ret, "failed to request irq %d\n", irq);
+
+	ret = dwc_adv_i2c_init_recovery_info(dev);
+	if (ret)
+		return dev_err_probe(device, ret, "failed to init bus recovery\n");
+
+	i2c_set_adapdata(&dev->adap, dev);
+	dev->adap.owner = THIS_MODULE;
+	strscpy(dev->adap.name, "Synopsys Advanced I2C adapter",
+		sizeof(dev->adap.name));
+	dev->adap.algo = &dwc_adv_i2c_algo;
+	dev->adap.quirks = &dwc_adv_i2c_quirks;
+	dev->adap.dev.parent = device;
+	dev->adap.timeout = msecs_to_jiffies(DWC_ADV_I2C_XFER_TIMEOUT_MS);
+	dev->adap.retries = 3;
+	device_set_node(&dev->adap.dev, dev_fwnode(device));
+
+	dwc_adv_i2c_init(dev);
+
+	pm_runtime_set_autosuspend_delay(device, DWC_ADV_I2C_AUTOSUSPEND_DELAY_MS);
+	pm_runtime_use_autosuspend(device);
+	ret = pm_runtime_set_active(device);
+	if (ret)
+		return dev_err_probe(device, ret, "failed to set runtime PM active\n");
+
+	pm_runtime_enable(device);
+
+	ret = devm_add_action_or_reset(device, dwc_adv_i2c_pm_teardown, dev);
+	if (ret)
+		return ret;
+
+	return devm_i2c_add_adapter(device, &dev->adap);
+}
+
+static const struct of_device_id dwc_adv_i2c_of_match[] = {
+	{ .compatible = "ti,tda54-i2c", .data = &tda54_i2c_match_data },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, dwc_adv_i2c_of_match);
+
+static struct platform_driver dwc_adv_i2c_driver = {
+	.probe = dwc_adv_i2c_probe,
+	.driver = {
+		.name = "i2c-dwc-adv",
+		.of_match_table = dwc_adv_i2c_of_match,
+		.pm = pm_ptr(&dwc_adv_i2c_pm_ops),
+	},
+};
+module_platform_driver(dwc_adv_i2c_driver);
+
+MODULE_AUTHOR("Aniket Limaye <a-limaye@ti.com>");
+MODULE_DESCRIPTION("Synopsys Advanced I2C (DWC_i2c) driver");
+MODULE_LICENSE("GPL");

-- 
2.53.0


^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-10-27 11:32 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-27 11:30 [PATCH v4 0/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) support for TI TDA54 Aniket Limaye
2026-10-27 11:30 ` [PATCH v4 1/2] dt-bindings: i2c: Add Synopsys DWC_i2c controller Aniket Limaye
2026-10-27 11:30 ` [PATCH v4 2/2] i2c: Add Synopsys Advanced I2C (DWC_i2c) driver Aniket Limaye

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®