* [PATCH 0/2] i2c: add GPIO buses sharing one SCL line
@ 2026-10-18 19:43 Ross Cawston
2026-10-18 19:43 ` [PATCH 1/2] dt-bindings: " Ross Cawston
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Ross Cawston @ 2026-10-18 19:43 UTC (permalink / raw)
To: andi.shyti
Cc: robh, krzk+dt, conor+dt, kees, gustavoars, linux-i2c, devicetree,
linux-hardening, linux-kernel, Ross Cawston
Some Realtek RTL930x switches bit-bang several I2C buses on SoC GPIOs
with a single SCL line and one SDA line per bus. The MokerLink
POE-10G07110GSM, for example, has its fan controller, its PoE
controllers and its SFP+ cage on SDA GPIOs 9, 10 and 11, all clocked
by GPIO 8. i2c-gpio cannot describe this: every bus would request the
same SCL GPIO.
This series adds a binding and a small driver that requests SCL once
and registers one i2c-algo-bit adapter per SDA line, serializing the
transfers so that only one bus clocks SCL at a time.
The SoC's own I2C master (i2c-rtl9300) is wired to the same pins, but
it moves at most 16 bytes per transfer, and the PoE controllers on this
board only accept their 25 KiB firmware as one continuous write: sent
in 16-byte writes, they NAK the second one. Tested on the MokerLink
POE-10G07110GSM.
Ross Cawston (2):
dt-bindings: i2c: add GPIO buses sharing one SCL line
i2c: add driver for GPIO buses sharing one SCL line
.../bindings/i2c/i2c-gpio-shared-scl.yaml | 94 +++++++
MAINTAINERS | 7 +
drivers/i2c/busses/Kconfig | 12 +
drivers/i2c/busses/Makefile | 1 +
drivers/i2c/busses/i2c-gpio-shared-scl.c | 229 ++++++++++++++++++
5 files changed, 343 insertions(+)
create mode 100644 Documentation/devicetree/bindings/i2c/i2c-gpio-shared-scl.yaml
create mode 100644 drivers/i2c/busses/i2c-gpio-shared-scl.c
--
2.55.0.windows.5
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 1/2] dt-bindings: i2c: add GPIO buses sharing one SCL line
2026-10-18 19:43 [PATCH 0/2] i2c: add GPIO buses sharing one SCL line Ross Cawston
@ 2026-10-18 19:43 ` Ross Cawston
2026-10-18 19:43 ` [PATCH 2/2] i2c: add driver for " Ross Cawston
2026-10-18 21:20 ` [PATCH 0/2] i2c: add " Ross Cawston
2 siblings, 0 replies; 5+ messages in thread
From: Ross Cawston @ 2026-10-18 19:43 UTC (permalink / raw)
To: andi.shyti
Cc: robh, krzk+dt, conor+dt, kees, gustavoars, linux-i2c, devicetree,
linux-hardening, linux-kernel, Ross Cawston
Add a binding for several bit-banged I2C buses that share one SCL GPIO
and have one SDA GPIO each, as on some Realtek RTL930x switches. Each
child node is a bus with its own SDA line; the parent holds the shared
SCL line and the i2c-gpio style timing properties.
Signed-off-by: Ross Cawston <ross@r-sc.ca>
---
.../bindings/i2c/i2c-gpio-shared-scl.yaml | 94 +++++++++++++++++++
1 file changed, 94 insertions(+)
create mode 100644 Documentation/devicetree/bindings/i2c/i2c-gpio-shared-scl.yaml
diff --git a/Documentation/devicetree/bindings/i2c/i2c-gpio-shared-scl.yaml b/Documentation/devicetree/bindings/i2c/i2c-gpio-shared-scl.yaml
new file mode 100644
index 00000000000..e725120973a
--- /dev/null
+++ b/Documentation/devicetree/bindings/i2c/i2c-gpio-shared-scl.yaml
@@ -0,0 +1,94 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/i2c/i2c-gpio-shared-scl.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Bit-banged I2C buses sharing one SCL line
+
+maintainers:
+ - Ross Cawston <ross@r-sc.ca>
+
+description: |
+ Several I2C buses bit-banged on GPIOs, with one SCL line common to all of
+ them and one SDA line per bus. Each child node is a separate I2C bus.
+ Transfers are serialized, so only one bus clocks SCL at a time.
+
+properties:
+ compatible:
+ const: i2c-gpio-shared-scl
+
+ scl-gpios:
+ description: The shared SCL line, usually open drain.
+ maxItems: 1
+
+ i2c-gpio,delay-us:
+ description: Half the clock period in microseconds, as for i2c-gpio.
+ default: 5
+
+ i2c-gpio,timeout-ms:
+ description: Clock stretching timeout in milliseconds, as for i2c-gpio.
+ default: 100
+
+ "#address-cells":
+ const: 1
+
+ "#size-cells":
+ const: 0
+
+patternProperties:
+ "^i2c@[0-9a-f]+$":
+ $ref: /schemas/i2c/i2c-controller.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ description: Index of the bus, for naming only.
+ maxItems: 1
+
+ sda-gpios:
+ description: The SDA line of this bus, usually open drain.
+ maxItems: 1
+
+ required:
+ - reg
+ - sda-gpios
+
+required:
+ - compatible
+ - scl-gpios
+ - "#address-cells"
+ - "#size-cells"
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+
+ i2c-gpio-shared {
+ compatible = "i2c-gpio-shared-scl";
+ scl-gpios = <&gpio0 8 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>;
+ i2c-gpio,delay-us = <5>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ i2c@0 {
+ reg = <0>;
+ sda-gpios = <&gpio0 9 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ fan-controller@2e {
+ compatible = "adi,adt7476";
+ reg = <0x2e>;
+ };
+ };
+
+ i2c@1 {
+ reg = <1>;
+ sda-gpios = <&gpio0 10 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ };
+ };
--
2.55.0.windows.5
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 2/2] i2c: add driver for GPIO buses sharing one SCL line
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:43 ` Ross Cawston
2026-10-19 6:59 ` Uwe Kleine-König
2026-10-18 21:20 ` [PATCH 0/2] i2c: add " Ross Cawston
2 siblings, 1 reply; 5+ messages in thread
From: Ross Cawston @ 2026-10-18 19:43 UTC (permalink / raw)
To: andi.shyti
Cc: robh, krzk+dt, conor+dt, kees, gustavoars, linux-i2c, devicetree,
linux-hardening, linux-kernel, Ross Cawston
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
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH 0/2] i2c: add GPIO buses sharing one SCL line
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:43 ` [PATCH 2/2] i2c: add driver for " Ross Cawston
@ 2026-10-18 21:20 ` Ross Cawston
2 siblings, 0 replies; 5+ messages in thread
From: Ross Cawston @ 2026-10-18 21:20 UTC (permalink / raw)
To: andi.shyti
Cc: robh, krzk+dt, conor+dt, kees, gustavoars, linux-i2c, devicetree,
linux-hardening, linux-kernel, Ross Cawston
Hi,
Please drop this series. As the automated review points out, i2c-gpio
has handled buses that share one SCL line since commit 645c3d7d07fa
("i2c: i2c-gpio: Enhance driver for buses with shared SCL") in v7.3,
which I had missed. The MokerLink POE-10G07110GSM will use i2c-gpio
instead.
Sorry for the noise.
Ross
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH 2/2] i2c: add driver for GPIO buses sharing one SCL line
2026-10-18 19:43 ` [PATCH 2/2] i2c: add driver for " Ross Cawston
@ 2026-10-19 6:59 ` Uwe Kleine-König
0 siblings, 0 replies; 5+ messages in thread
From: Uwe Kleine-König @ 2026-10-19 6:59 UTC (permalink / raw)
To: Ross Cawston
Cc: andi.shyti, robh, krzk+dt, conor+dt, kees, gustavoars, linux-i2c,
devicetree, linux-hardening, linux-kernel
[-- Attachment #1: Type: text/plain, Size: 337 bytes --]
On Sun, Oct 18, 2026 at 12:43:15PM -0700, Ross Cawston wrote:
> +#include <linux/mod_devicetable.h>
> [...]
> +#include <linux/platform_device.h>
<linux/platform_device.h> already provides of_device_id, so there is no
need to also #include <linux/mod_devicetable.h> which also pulls in a
lot of unneeded dependencies.
Best regards
Uwe
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-10-19 6:59 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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:43 ` [PATCH 2/2] i2c: add driver for " Ross Cawston
2026-10-19 6:59 ` Uwe Kleine-König
2026-10-18 21:20 ` [PATCH 0/2] i2c: add " Ross Cawston
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®