Some boards bit-bang several I2C buses on GPIOs with one common SCL
line and one SDA line per bus. The MokerLink POE-10G07110GSM, an
RTL9303 PoE switch, clocks its fan controller, its two PoE controllers
and its SFP+ cage this way. i2c-gpio cannot be used, as each instance
would request the shared SCL GPIO.

Request SCL once and register one i2c-algo-bit adapter per SDA child
node. A mutex taken in pre_xfer and released in post_xfer serializes
the transfers across the buses, so only one bus clocks SCL at a time
and the idle SDA lines see clock pulses only without a START.

Signed-off-by: Ross Cawston <[email protected]>
---
 MAINTAINERS                              |   7 +
 drivers/i2c/busses/Kconfig               |  12 ++
 drivers/i2c/busses/Makefile              |   1 +
 drivers/i2c/busses/i2c-gpio-shared-scl.c | 229 +++++++++++++++++++++++
 4 files changed, 249 insertions(+)
 create mode 100644 drivers/i2c/busses/i2c-gpio-shared-scl.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 782389bd5cd..9ac40ad4399 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12250,6 +12250,13 @@ S:     Maintained
 F:     Documentation/i2c/busses/i2c-nvidia-gpu.rst
 F:     drivers/i2c/busses/i2c-nvidia-gpu.c
 
+I2C GPIO BUSES SHARING SCL DRIVER
+M:     Ross Cawston <[email protected]>
+L:     [email protected]
+S:     Maintained
+F:     Documentation/devicetree/bindings/i2c/i2c-gpio-shared-scl.yaml
+F:     drivers/i2c/busses/i2c-gpio-shared-scl.c
+
 I2C MUXES
 M:     Peter Rosin <[email protected]>
 L:     [email protected]
diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig
index d7b89508311..caeed4385d1 100644
--- a/drivers/i2c/busses/Kconfig
+++ b/drivers/i2c/busses/Kconfig
@@ -691,6 +691,18 @@ config I2C_GPIO
          This is a very simple bitbanging I2C driver utilizing the
          arch-neutral GPIO API to control the SCL and SDA lines.
 
+config I2C_GPIO_SHARED_SCL
+       tristate "GPIO-based bitbanging I2C buses sharing one SCL line"
+       depends on GPIOLIB || COMPILE_TEST
+       select I2C_ALGOBIT
+       help
+         Several bitbanged I2C buses with one common SCL GPIO and one SDA
+         GPIO per bus, as found on some Realtek RTL930x switches. Each
+         SDA line becomes its own I2C adapter; transfers are serialized.
+
+         This driver can also be built as a module. If so, the module
+         will be called i2c-gpio-shared-scl.
+
 config I2C_GPIO_FAULT_INJECTOR
        bool "GPIO-based fault injector"
        depends on I2C_GPIO
diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile
index 3755c54b3d8..c3b98e8abc4 100644
--- a/drivers/i2c/busses/Makefile
+++ b/drivers/i2c/busses/Makefile
@@ -66,6 +66,7 @@ obj-$(CONFIG_I2C_EG20T)               += i2c-eg20t.o
 obj-$(CONFIG_I2C_EMEV2)                += i2c-emev2.o
 obj-$(CONFIG_I2C_EXYNOS5)      += i2c-exynos5.o
 obj-$(CONFIG_I2C_GPIO)         += i2c-gpio.o
+obj-$(CONFIG_I2C_GPIO_SHARED_SCL)      += i2c-gpio-shared-scl.o
 obj-$(CONFIG_I2C_HIGHLANDER)   += i2c-highlander.o
 obj-$(CONFIG_I2C_HISI)         += i2c-hisi.o
 obj-$(CONFIG_I2C_HIX5HD2)      += i2c-hix5hd2.o
diff --git a/drivers/i2c/busses/i2c-gpio-shared-scl.c 
b/drivers/i2c/busses/i2c-gpio-shared-scl.c
new file mode 100644
index 00000000000..6f7ee898392
--- /dev/null
+++ b/drivers/i2c/busses/i2c-gpio-shared-scl.c
@@ -0,0 +1,229 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Bit-banged I2C buses that share one SCL line
+ *
+ * Some boards wire several I2C buses to GPIOs with one common SCL line and
+ * a separate SDA line per bus. The vendor firmware bit-bangs them in turn.
+ * i2c-gpio cannot describe this, because every bus would request the same
+ * SCL GPIO. This driver requests SCL once and registers one adapter per SDA
+ * line. Transfers on the adapters are serialized, so only one bus drives
+ * SCL at a time; devices on the other buses see SCL toggle while their SDA
+ * stays high, which is never a START condition, and ignore it.
+ *
+ * Copyright (C) 2026 Ross Cawston <[email protected]>
+ */
+
+#include <linux/delay.h>
+#include <linux/device.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c-algo-bit.h>
+#include <linux/i2c.h>
+#include <linux/jiffies.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/slab.h>
+
+struct i2c_gpio_shared_scl;
+
+struct i2c_gpio_shared_scl_bus {
+       struct i2c_gpio_shared_scl *parent;
+       struct gpio_desc *sda;
+       struct i2c_algo_bit_data bit_data;
+       struct i2c_adapter adap;
+       bool registered;
+};
+
+struct i2c_gpio_shared_scl {
+       struct device *dev;
+       struct gpio_desc *scl;
+       struct mutex lock;      /* one transfer on the shared SCL at a time */
+       unsigned int num_buses;
+       struct i2c_gpio_shared_scl_bus buses[] __counted_by(num_buses);
+};
+
+static void i2c_gpio_shared_scl_setsda(void *data, int state)
+{
+       struct i2c_gpio_shared_scl_bus *bus = data;
+
+       gpiod_set_value_cansleep(bus->sda, state);
+}
+
+static int i2c_gpio_shared_scl_getsda(void *data)
+{
+       struct i2c_gpio_shared_scl_bus *bus = data;
+
+       return gpiod_get_value_cansleep(bus->sda);
+}
+
+static void i2c_gpio_shared_scl_setscl(void *data, int state)
+{
+       struct i2c_gpio_shared_scl_bus *bus = data;
+
+       gpiod_set_value_cansleep(bus->parent->scl, state);
+}
+
+static int i2c_gpio_shared_scl_getscl(void *data)
+{
+       struct i2c_gpio_shared_scl_bus *bus = data;
+
+       return gpiod_get_value_cansleep(bus->parent->scl);
+}
+
+static int i2c_gpio_shared_scl_pre_xfer(struct i2c_adapter *adap)
+{
+       struct i2c_gpio_shared_scl_bus *bus = i2c_get_adapdata(adap);
+
+       mutex_lock(&bus->parent->lock);
+
+       return 0;
+}
+
+static void i2c_gpio_shared_scl_post_xfer(struct i2c_adapter *adap)
+{
+       struct i2c_gpio_shared_scl_bus *bus = i2c_get_adapdata(adap);
+
+       mutex_unlock(&bus->parent->lock);
+}
+
+static void i2c_gpio_shared_scl_del_buses(struct i2c_gpio_shared_scl *priv)
+{
+       unsigned int i;
+
+       for (i = 0; i < priv->num_buses; i++)
+               if (priv->buses[i].registered)
+                       i2c_del_adapter(&priv->buses[i].adap);
+}
+
+static int i2c_gpio_shared_scl_add_bus(struct i2c_gpio_shared_scl *priv,
+                                      struct i2c_gpio_shared_scl_bus *bus,
+                                      struct fwnode_handle *child,
+                                      u32 udelay, u32 timeout_ms)
+{
+       struct device *dev = priv->dev;
+       struct i2c_algo_bit_data *bit_data = &bus->bit_data;
+       struct i2c_adapter *adap = &bus->adap;
+       u32 reg;
+       int ret;
+
+       ret = fwnode_property_read_u32(child, "reg", &reg);
+       if (ret)
+               return dev_err_probe(dev, ret, "%pfwP: missing reg\n", child);
+
+       bus->parent = priv;
+       bus->sda = devm_fwnode_gpiod_get(dev, child, "sda",
+                                        GPIOD_OUT_HIGH_OPEN_DRAIN,
+                                        fwnode_get_name(child));
+       if (IS_ERR(bus->sda))
+               return dev_err_probe(dev, PTR_ERR(bus->sda),
+                                    "%pfwP: cannot get SDA GPIO\n", child);
+
+       bit_data->data = bus;
+       bit_data->setsda = i2c_gpio_shared_scl_setsda;
+       bit_data->getsda = i2c_gpio_shared_scl_getsda;
+       bit_data->setscl = i2c_gpio_shared_scl_setscl;
+       bit_data->getscl = i2c_gpio_shared_scl_getscl;
+       bit_data->pre_xfer = i2c_gpio_shared_scl_pre_xfer;
+       bit_data->post_xfer = i2c_gpio_shared_scl_post_xfer;
+       bit_data->udelay = udelay;
+       bit_data->timeout = msecs_to_jiffies(timeout_ms);
+
+       adap->owner = THIS_MODULE;
+       adap->algo_data = bit_data;
+       adap->dev.parent = dev;
+       device_set_node(&adap->dev, child);
+       snprintf(adap->name, sizeof(adap->name), "%s SDA%u", dev_name(dev), 
reg);
+       i2c_set_adapdata(adap, bus);
+
+       ret = i2c_bit_add_bus(adap);
+       if (ret)
+               return dev_err_probe(dev, ret, "%pfwP: cannot add bus\n", 
child);
+
+       bus->registered = true;
+
+       return 0;
+}
+
+static int i2c_gpio_shared_scl_probe(struct platform_device *pdev)
+{
+       struct device *dev = &pdev->dev;
+       struct i2c_gpio_shared_scl *priv;
+       u32 udelay = 5, timeout_ms = 100;
+       unsigned int num_buses, i = 0;
+       int ret;
+
+       num_buses = device_get_child_node_count(dev);
+       if (!num_buses)
+               return dev_err_probe(dev, -EINVAL, "no buses\n");
+
+       priv = devm_kzalloc(dev, struct_size(priv, buses, num_buses), 
GFP_KERNEL);
+       if (!priv)
+               return -ENOMEM;
+
+       priv->dev = dev;
+       priv->num_buses = num_buses;
+       ret = devm_mutex_init(dev, &priv->lock);
+       if (ret)
+               return ret;
+
+       /* Same properties as i2c-gpio */
+       device_property_read_u32(dev, "i2c-gpio,delay-us", &udelay);
+       device_property_read_u32(dev, "i2c-gpio,timeout-ms", &timeout_ms);
+
+       priv->scl = devm_gpiod_get(dev, "scl", GPIOD_OUT_HIGH_OPEN_DRAIN);
+       if (IS_ERR(priv->scl))
+               return dev_err_probe(dev, PTR_ERR(priv->scl), "cannot get SCL 
GPIO\n");
+
+       platform_set_drvdata(pdev, priv);
+
+       device_for_each_child_node_scoped(dev, child) {
+               ret = i2c_gpio_shared_scl_add_bus(priv, &priv->buses[i++], 
child,
+                                                 udelay, timeout_ms);
+               if (ret) {
+                       i2c_gpio_shared_scl_del_buses(priv);
+                       return ret;
+               }
+       }
+
+       dev_info(dev, "%u buses on shared SCL, %u us half period\n", num_buses, 
udelay);
+
+       return 0;
+}
+
+static void i2c_gpio_shared_scl_remove(struct platform_device *pdev)
+{
+       i2c_gpio_shared_scl_del_buses(platform_get_drvdata(pdev));
+}
+
+static const struct of_device_id i2c_gpio_shared_scl_of_match[] = {
+       { .compatible = "i2c-gpio-shared-scl" },
+       { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, i2c_gpio_shared_scl_of_match);
+
+static struct platform_driver i2c_gpio_shared_scl_driver = {
+       .driver = {
+               .name = "i2c-gpio-shared-scl",
+               .of_match_table = i2c_gpio_shared_scl_of_match,
+       },
+       .probe = i2c_gpio_shared_scl_probe,
+       .remove = i2c_gpio_shared_scl_remove,
+};
+
+/* Registered early, like i2c-gpio, so the buses exist before their clients */
+static int __init i2c_gpio_shared_scl_init(void)
+{
+       return platform_driver_register(&i2c_gpio_shared_scl_driver);
+}
+subsys_initcall(i2c_gpio_shared_scl_init);
+
+static void __exit i2c_gpio_shared_scl_exit(void)
+{
+       platform_driver_unregister(&i2c_gpio_shared_scl_driver);
+}
+module_exit(i2c_gpio_shared_scl_exit);
+
+MODULE_DESCRIPTION("Bit-banged I2C buses sharing one SCL line");
+MODULE_LICENSE("GPL");
-- 
2.55.0.windows.5


Reply via email to