From: Ross Cawston <ross@r-sc.ca>
To: andi.shyti@kernel.org
Cc: robh@kernel.org, krzk+dt@kernel.org, conor+dt@kernel.org,
kees@kernel.org, gustavoars@kernel.org,
linux-i2c@vger.kernel.org, devicetree@vger.kernel.org,
linux-hardening@vger.kernel.org, linux-kernel@vger.kernel.org,
Ross Cawston <ross@r-sc.ca>
Subject: [PATCH 2/2] i2c: add driver for GPIO buses sharing one SCL line
Date: Sun, 18 Oct 2026 12:43:15 -0700 [thread overview]
Message-ID: <20261018194315.945-3-ross@r-sc.ca> (raw)
In-Reply-To: <20261018194315.945-1-ross@r-sc.ca>
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 <ross@r-sc.ca>
---
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 <ross@r-sc.ca>
+L: linux-i2c@vger.kernel.org
+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 <peda@lysator.liu.se>
L: linux-i2c@vger.kernel.org
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 <ross@r-sc.ca>
+ */
+
+#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", ®);
+ 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
next prev parent reply other threads:[~2026-10-18 19:49 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-18 19:43 [PATCH 0/2] i2c: add GPIO buses sharing one SCL line Ross Cawston
2026-10-18 19:43 ` [PATCH 1/2] dt-bindings: " Ross Cawston
2026-10-18 19:56 ` sashiko-bot
2026-10-18 19:43 ` Ross Cawston [this message]
2026-10-18 20:04 ` [PATCH 2/2] i2c: add driver for " sashiko-bot
2026-10-19 6:59 ` Uwe Kleine-König
2026-10-18 21:20 ` [PATCH 0/2] i2c: add " Ross Cawston
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261018194315.945-3-ross@r-sc.ca \
--to=ross@r-sc.ca \
--cc=andi.shyti@kernel.org \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=gustavoars@kernel.org \
--cc=kees@kernel.org \
--cc=krzk+dt@kernel.org \
--cc=linux-hardening@vger.kernel.org \
--cc=linux-i2c@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=robh@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox