From: Marcus Folkesson <marcus.folkesson@gmail.com>
To: Wolfram Sang <wsa+renesas@sang-engineering.com>,
Michael Hennerich <michael.hennerich@analog.com>,
Bartosz Golaszewski <brgl@bgdev.pl>,
Andi Shyti <andi.shyti@kernel.org>,
Andy Shevchenko <andriy.shevchenko@intel.com>,
Bartosz Golaszewski <brgl@kernel.org>,
Peter Rosin <peda@lysator.liu.se>,
Peter Rosin <peda@lysator.liu.se>
Cc: linux-i2c@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-arm-kernel@lists.infradead.org,
Marcus Folkesson <marcus.folkesson@gmail.com>
Subject: [PATCH v13 3/8] i2c: mux: add support for per channel bus frequency
Date: Thu, 15 Oct 2026 13:01:50 +0200 [thread overview]
Message-ID: <20261015-i2c-mux-v13-3-778a12cfe93b@gmail.com> (raw)
In-Reply-To: <20261015-i2c-mux-v13-0-778a12cfe93b@gmail.com>
There may be several reasons why you may need to use a certain speed
on an I2C bus. E.g.
- When several devices are attached to the bus, the speed must be
selected according to the slowest device.
- Electrical conditions may limit the usable speed on the bus for
different reasons.
With an I2C multiplexer, it is possible to group the attached devices
after their preferred speed by e.g. putting all "slow" devices on a
separate channel on the multiplexer.
Consider the following topology:
.----------. 100kHz .--------.
.--------. 400kHz | |--------| dev D1 |
| root |--+-----| I2C MUX | '--------'
'--------' | | |--. 400kHz .--------.
| '----------' '-------| dev D2 |
| .--------. '--------'
'--| dev D3 |
'--------'
One requirement with this design is that a multiplexer may only use the
same or lower bus speed as its parent.
Otherwise, if the multiplexer would have to increase the bus frequency,
then all siblings (D3 in this case) would run into a clock speed it may
not support.
The bus frequency for each channel is set in the devicetree. As the
i2c-mux bindings import the i2c-controller schema, the clock-frequency
property is already allowed.
If no clock-frequency property is set, the channel inherits the parent
bus speed.
The following example uses dt bindings to illustrate the topology above:
i2c {
clock-frequency = <400000>;
i2c-mux {
i2c@0 {
clock-frequency = <100000>;
D1 {
...
};
};
i2c@1 {
D2 {
...
};
};
};
D3 {
...
}
};
Signed-off-by: Marcus Folkesson <marcus.folkesson@gmail.com>
---
drivers/i2c/i2c-mux.c | 195 ++++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 183 insertions(+), 12 deletions(-)
diff --git a/drivers/i2c/i2c-mux.c b/drivers/i2c/i2c-mux.c
index 94cc019b222f..008257624ba8 100644
--- a/drivers/i2c/i2c-mux.c
+++ b/drivers/i2c/i2c-mux.c
@@ -37,21 +37,141 @@ struct i2c_mux_priv {
u32 chan_id;
};
+static inline int
+__i2c_adapter_set_clk_freq(struct i2c_adapter *adapter, u32 clock_Hz)
+{
+ int ret;
+
+ /* Nothing to do if the requested frequency is already set. */
+ if (adapter->clock_Hz == clock_Hz)
+ return 0;
+
+ if (adapter->bus_ops && adapter->bus_ops->set_clk_freq) {
+ ret = adapter->bus_ops->set_clk_freq(adapter, clock_Hz);
+ if (ret < 0) {
+ dev_err(&adapter->dev, "Could not set requested frequency (%uHz)\n",
+ clock_Hz);
+ return ret;
+ }
+
+ /* Everything is fine, update the clock_Hz to the new value. */
+ clock_Hz = ret;
+ }
+
+ /*
+ * If the adapter is a root adapter without .set_clk_freq()
+ * implemented, this feature is not supported.
+ */
+ if (!i2c_parent_is_i2c_adapter(adapter))
+ return -EOPNOTSUPP;
+
+ /*
+ * Update the clock_Hz for non-root adapters, even if .set_clk_freq()
+ * is not implemented, to allow the clock frequency to be propagated
+ * to root adapters that do support it.
+ */
+ adapter->clock_Hz = clock_Hz;
+ return 0;
+}
+
+static inline int
+i2c_adapter_set_clk_freq(struct i2c_adapter *adapter, u32 clock_Hz)
+{
+ int ret;
+
+ i2c_lock_bus(adapter, I2C_LOCK_SEGMENT);
+ ret = __i2c_adapter_set_clk_freq(adapter, clock_Hz);
+ i2c_unlock_bus(adapter, I2C_LOCK_SEGMENT);
+
+ return ret;
+}
+
+/*
+ * Lower the parent bus frequency to what this channel tolerates and select
+ * the channel. On success, @oldclock holds the frequency to restore in
+ * i2c_mux_deselect_chan(), or zero if the frequency was left untouched.
+ */
+static int i2c_mux_select_chan(struct i2c_adapter *adap, u32 *oldclock)
+{
+ struct i2c_mux_priv *priv = adap->algo_data;
+ struct i2c_mux_core *muxc = priv->muxc;
+ struct i2c_adapter *parent = muxc->parent;
+ int ret;
+
+ if (priv->adap.clock_Hz && priv->adap.clock_Hz < parent->clock_Hz) {
+ u32 clock_Hz = parent->clock_Hz;
+
+ if (muxc->mux_locked)
+ ret = i2c_adapter_set_clk_freq(parent,
+ priv->adap.clock_Hz);
+ else
+ ret = __i2c_adapter_set_clk_freq(parent,
+ priv->adap.clock_Hz);
+
+ /*
+ * Never transfer on a segment that is still running too
+ * fast for the devices behind this channel.
+ */
+ if (ret) {
+ dev_err(&adap->dev,
+ "Failed to set clock frequency %uHz on adapter %s: %d\n",
+ priv->adap.clock_Hz, parent->name, ret);
+ return ret;
+ }
+
+ dev_dbg(&adap->dev, "Set clock frequency %uHz on %s\n",
+ priv->adap.clock_Hz, parent->name);
+
+ *oldclock = clock_Hz;
+ }
+
+ return muxc->select(muxc, priv->chan_id);
+}
+
+static void i2c_mux_deselect_chan(struct i2c_adapter *adap, u32 oldclock)
+{
+ struct i2c_mux_priv *priv = adap->algo_data;
+ struct i2c_mux_core *muxc = priv->muxc;
+ struct i2c_adapter *parent = muxc->parent;
+ int ret;
+
+ if (muxc->deselect)
+ muxc->deselect(muxc, priv->chan_id);
+
+ if (oldclock && oldclock != priv->adap.clock_Hz) {
+ if (muxc->mux_locked)
+ ret = i2c_adapter_set_clk_freq(parent, oldclock);
+ else
+ ret = __i2c_adapter_set_clk_freq(parent, oldclock);
+
+ if (ret) {
+ dev_err(&adap->dev,
+ "Failed to restore clock frequency %uHz on adapter %s: %d\n",
+ oldclock, parent->name, ret);
+ return;
+ }
+
+ dev_dbg(&adap->dev, "Restored clock frequency %uHz on %s\n",
+ oldclock, parent->name);
+ }
+}
+
static int __i2c_mux_master_xfer(struct i2c_adapter *adap,
struct i2c_msg msgs[], int num)
{
struct i2c_mux_priv *priv = adap->algo_data;
struct i2c_mux_core *muxc = priv->muxc;
struct i2c_adapter *parent = muxc->parent;
+ u32 oldclock = 0;
int ret;
/* Switch to the right mux port and perform the transfer. */
- ret = muxc->select(muxc, priv->chan_id);
+ ret = i2c_mux_select_chan(adap, &oldclock);
if (ret >= 0)
ret = __i2c_transfer(parent, msgs, num);
- if (muxc->deselect)
- muxc->deselect(muxc, priv->chan_id);
+
+ i2c_mux_deselect_chan(adap, oldclock);
return ret;
}
@@ -62,15 +182,16 @@ static int i2c_mux_master_xfer(struct i2c_adapter *adap,
struct i2c_mux_priv *priv = adap->algo_data;
struct i2c_mux_core *muxc = priv->muxc;
struct i2c_adapter *parent = muxc->parent;
+ u32 oldclock = 0;
int ret;
/* Switch to the right mux port and perform the transfer. */
- ret = muxc->select(muxc, priv->chan_id);
+ ret = i2c_mux_select_chan(adap, &oldclock);
if (ret >= 0)
ret = i2c_transfer(parent, msgs, num);
- if (muxc->deselect)
- muxc->deselect(muxc, priv->chan_id);
+
+ i2c_mux_deselect_chan(adap, oldclock);
return ret;
}
@@ -83,16 +204,17 @@ static int __i2c_mux_smbus_xfer(struct i2c_adapter *adap,
struct i2c_mux_priv *priv = adap->algo_data;
struct i2c_mux_core *muxc = priv->muxc;
struct i2c_adapter *parent = muxc->parent;
+ u32 oldclock = 0;
int ret;
/* Select the right mux port and perform the transfer. */
- ret = muxc->select(muxc, priv->chan_id);
+ ret = i2c_mux_select_chan(adap, &oldclock);
if (ret >= 0)
ret = __i2c_smbus_xfer(parent, addr, flags,
read_write, command, size, data);
- if (muxc->deselect)
- muxc->deselect(muxc, priv->chan_id);
+
+ i2c_mux_deselect_chan(adap, oldclock);
return ret;
}
@@ -105,16 +227,17 @@ static int i2c_mux_smbus_xfer(struct i2c_adapter *adap,
struct i2c_mux_priv *priv = adap->algo_data;
struct i2c_mux_core *muxc = priv->muxc;
struct i2c_adapter *parent = muxc->parent;
+ u32 oldclock = 0;
int ret;
/* Select the right mux port and perform the transfer. */
- ret = muxc->select(muxc, priv->chan_id);
+ ret = i2c_mux_select_chan(adap, &oldclock);
if (ret >= 0)
ret = i2c_smbus_xfer(parent, addr, flags,
read_write, command, size, data);
- if (muxc->deselect)
- muxc->deselect(muxc, priv->chan_id);
+
+ i2c_mux_deselect_chan(adap, oldclock);
return ret;
}
@@ -374,6 +497,54 @@ int i2c_mux_add_adapter(struct i2c_mux_core *muxc,
priv->channel_node = i2c_mux_get_channel_node(muxc, chan_id);
device_set_node(&priv->adap.dev, priv->channel_node);
+ if (dev_fwnode(muxc->dev)) {
+ fwnode_property_read_u32(priv->channel_node, "clock-frequency",
+ &priv->adap.clock_Hz);
+
+ /*
+ * If the mux adapter has no clock-frequency property, the
+ * channel inherits the parent bus speed.
+ */
+ if (!priv->adap.clock_Hz)
+ priv->adap.clock_Hz = parent->clock_Hz;
+
+ /* We don't support mux adapters faster than their parent */
+ if (priv->adap.clock_Hz > parent->clock_Hz) {
+ dev_err(muxc->dev,
+ "channel (%u) is faster (%u) than parent (%u)\n",
+ chan_id, priv->adap.clock_Hz, parent->clock_Hz);
+
+ ret = -EINVAL;
+ goto err_free_priv;
+ }
+
+ if (priv->adap.clock_Hz < parent->clock_Hz) {
+ /*
+ * A slow channel must not be left connected while the
+ * mux is idle, or it is exposed to the full parent
+ * bus speed whenever a sibling is accessed.
+ */
+ if (muxc->idle_state == I2C_MUX_IDLE_UNKNOWN ||
+ muxc->idle_state == I2C_MUX_IDLE_AS_IS ||
+ muxc->idle_state == (int)chan_id) {
+ dev_err(muxc->dev,
+ "channel %u has improper idle state for this configuration\n",
+ chan_id);
+ ret = -EINVAL;
+ goto err_free_priv;
+ }
+
+ /*
+ * Warn if the mux adapter is not parent-locked as
+ * this may cause issues for some hardware topologies.
+ */
+ if (muxc->mux_locked)
+ dev_warn(muxc->dev,
+ "channel %u is slower than parent on a non parent-locked mux\n",
+ chan_id);
+ }
+ }
+
/*
* Associate the mux channel with an ACPI node.
*/
--
2.55.0
next prev parent reply other threads:[~2026-10-15 11:02 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 11:01 [PATCH v13 0/8] I2C Mux per channel bus speed Marcus Folkesson
2026-10-15 11:01 ` [PATCH v13 1/8] i2c: core: add callback to change bus frequency Marcus Folkesson
2026-10-15 11:01 ` [PATCH v13 2/8] i2c: mux: add idle_state property to i2c_mux_core Marcus Folkesson
2026-10-15 11:01 ` Marcus Folkesson [this message]
2026-10-15 11:01 ` [PATCH v13 4/8] i2c: davinci: calculate bus freq from Hz instead of kHz Marcus Folkesson
2026-10-15 11:01 ` [PATCH v13 5/8] i2c: davinci: add support for setting bus frequency Marcus Folkesson
2026-10-15 11:01 ` [PATCH v13 6/8] i2c: mux: ltc4306: set correct idle_state in i2c_mux_core Marcus Folkesson
2026-10-15 11:01 ` [PATCH v13 7/8] docs: i2c: i2c-topology: add section about bus speed Marcus Folkesson
2026-10-15 11:01 ` [PATCH v13 8/8] i2c: mux: pca954x: use idle state from i2c_mux_core Marcus Folkesson
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=20261015-i2c-mux-v13-3-778a12cfe93b@gmail.com \
--to=marcus.folkesson@gmail.com \
--cc=andi.shyti@kernel.org \
--cc=andriy.shevchenko@intel.com \
--cc=brgl@bgdev.pl \
--cc=brgl@kernel.org \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=linux-i2c@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=michael.hennerich@analog.com \
--cc=peda@lysator.liu.se \
--cc=wsa+renesas@sang-engineering.com \
/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
all inboxes | Powered by JetHome®