Devicetree
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Ross Cawston" <ross@r-sc.ca>
Cc: devicetree@vger.kernel.org, robh@kernel.org, conor+dt@kernel.org
Subject: Re: [PATCH 2/2] i2c: add driver for GPIO buses sharing one SCL line
Date: Sun, 18 Oct 2026 20:04:37 +0000	[thread overview]
Message-ID: <sashiko-outbox-176357@kernel.org> (raw)
In-Reply-To: <20261018194315.945-3-ross@r-sc.ca>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [High] i2c: i2c-gpio-shared-scl: sleeping mutex in pre_xfer() breaks atomic xfer
- [Medium] i2c: i2c-gpio-shared-scl: buses registered before all SDA GPIOs driven high
- [Medium] i2c: i2c-gpio-shared-scl: duplicates shared-SCL support in i2c-gpio
--

commit 79f8386f6a7c7a3c6b945869078cb1a7bbcc4a64
Author: Ross Cawston <ross@r-sc.ca>

i2c: add driver for GPIO buses sharing one SCL line

This commit adds a driver for GPIO-bitbanged I2C buses that share a
single SCL GPIO line across multiple SDA child nodes, serializing
transfers with a mutex in pre_xfer and post_xfer.

>     i2c: add driver for GPIO buses sharing one SCL line
>     
>     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>

> 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 0000000000000..6f7ee898392a5
> --- /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.

[Severity: Medium]
The commit message and file header state that i2c-gpio cannot be used
because each instance would request the shared SCL GPIO. Is a separate
driver needed here given that drivers/i2c/busses/i2c-gpio.c already
supports shared SCL lines since commit 645c3d7d07fa ("i2c: i2c-gpio:
Enhance driver for buses with shared SCL")?

In drivers/i2c/busses/i2c-gpio.c, i2c_gpio_lookup_scl() matches shared
scl-gpios phandles and GPIO cells across multiple i2c-gpio DT nodes via
i2c_gpio_scl_matches(), requests the SCL descriptor once, reference-counts
it across bus instances in i2c_gpio_scl_list, sets bit_data->skip_bit_test,
and serializes transfers across all buses sharing that SCL line via
i2c_gpio_lock_ops:

drivers/i2c/busses/i2c-gpio.c:i2c_gpio_lookup_scl() {
	scoped_guard(mutex, &i2c_gpio_scl_list_lock) {
		list_for_each_entry(scl, &i2c_gpio_scl_list, list) {
			if (!i2c_gpio_scl_matches(scl, new_scl))
				continue;

			i2c_gpio_free_scl(new_scl);
			if (!scl->gpio)
				return ERR_PTR(-EPROBE_DEFER);

			refcount_inc(&scl->ref);
			if (scl->args.fwnode)
				dev_dbg(dev, "reusing shared SCL (%pfwP)\n", scl->args.fwnode);

			return scl;
		}
		list_add(&new_scl->list, &i2c_gpio_scl_list);
	}
}

Can the MokerLink RTL9303 switch use the existing i2c-gpio driver and
binding instead?

[ ... ]

> +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);
> +}

[Severity: High]
Will taking bus->parent->lock in i2c_gpio_shared_scl_pre_xfer() sleep in
atomic context during atomic I2C transfers, and allow sibling buses to
interleave during multi-transfer locked sequences?

i2c_gpio_shared_scl_add_bus() registers each adapter with
i2c_bit_add_bus(), which sets adap->algo to i2c_bit_algo (whose
xfer_atomic callback is bit_xfer_atomic()) while leaving adap->lock_ops
unset.

When i2c_in_atomic_xfer_mode() is true (such as during system shutdown,
reboot, poweroff, or panic), __i2c_lock_bus_helper() calls
i2c_trylock_bus(), which only trylocks the per-adapter adap->bus_lock and
cannot see if a sibling bus holds bus->parent->lock. __i2c_transfer()
then invokes:

bit_xfer_atomic()
  -> bit_xfer()
    -> adap->pre_xfer()
      -> i2c_gpio_shared_scl_pre_xfer()
        -> mutex_lock(&bus->parent->lock)

Even though bit_data->can_do_atomic is false, bit_xfer_atomic() only logs
a warning and still proceeds into bit_xfer():

drivers/i2c/algos/i2c-algo-bit.c:bit_xfer_atomic() {
	struct i2c_algo_bit_data *adap = i2c_adap->algo_data;

	if (!adap->can_do_atomic)
		dev_warn(&i2c_adap->dev, "not flagged for atomic transfers\n");

	return bit_xfer(i2c_adap, msgs, num);
}

This unconditionally calls mutex_lock() (followed by
gpiod_set_value_cansleep() and gpiod_get_value_cansleep()) in atomic
context.

In addition, callers that lock an adapter across multiple __i2c_transfer()
calls via i2c_lock_bus() (for example, i2c_transfer_rollball() in
drivers/net/mdio/mdio-i2c.c for SFP+ cages, or i2c-mux operations) only
hold the per-adapter adap->bus_lock while bus->parent->lock is released in
i2c_gpio_shared_scl_post_xfer() after each individual transfer, allowing a
sibling bus to clock the shared SCL line mid-sequence. Normal transfers
also nest struct mutex (bus->parent->lock) inside rt_mutex
(adap->bus_lock).

Should this instead implement struct i2c_lock_operations (lock_bus,
trylock_bus, unlock_bus) on adap->lock_ops backed by an rt_mutex (and set
bit_data->can_do_atomic based on gpiod_cansleep()), similar to how
drivers/i2c/busses/i2c-gpio.c handles shared SCL locking?

[ ... ]

> +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)
> +{
[ ... ]
> +	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;
> +		}
> +	}

[Severity: Medium]
Can registering each adapter in the same loop that requests the per-bus
SDA GPIOs cause spurious START or STOP conditions on sibling buses that
have not yet been initialized?

For devices on idle buses to ignore activity on the shared priv->scl line,
their SDA lines must already be driven high (GPIOD_OUT_HIGH_OPEN_DRAIN).
However, i2c_gpio_shared_scl_probe() calls i2c_gpio_shared_scl_add_bus()
in a single pass, which calls i2c_bit_add_bus() for priv->buses[0] before
priv->buses[1..num_buses - 1] have requested their SDA GPIOs.

During i2c_bit_add_bus(&priv->buses[0].adap), the shared SCL line is
clocked immediately if i2c_algo_bit.bit_test is set or when child client
devices on bus 0 probe via of_i2c_register_devices() and perform transfers
while sibling SDA lines are still in their pre-probe state. Furthermore,
once bus 0 is registered, transfers on bus 0 can toggle priv->scl under
priv->lock concurrently while subsequent iterations of the loop call
devm_fwnode_gpiod_get(..., GPIOD_OUT_HIGH_OPEN_DRAIN, ...) for later
buses without holding priv->lock; transitioning a sibling SDA line while
priv->scl happens to be high creates a START or STOP condition on that
sibling bus.

Would it be safer to request and configure all child SDA GPIOs (and
validate DT properties) in a first pass before registering any of the
adapters with i2c_bit_add_bus()?

[ ... ]

>

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261018194315.945-1-ross@r-sc.ca?part=2

  reply	other threads:[~2026-10-18 20:04 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 ` [PATCH 2/2] i2c: add driver for " Ross Cawston
2026-10-18 20:04   ` sashiko-bot [this message]
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=sashiko-outbox-176357@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=conor+dt@kernel.org \
    --cc=devicetree@vger.kernel.org \
    --cc=robh@kernel.org \
    --cc=ross@r-sc.ca \
    --cc=sashiko-reviews@lists.linux.dev \
    /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