[RFC PATCH 2/7] i3c: hub: add i2c-tools support for SMBus-configured target ports

From: Lakshay Piplani

Date: Fri Oct 16 2026 - 13:21:55 EST


Add support for standard i2c-tools on hub target ports configured
to operate in SMBus mode.

This change implements the .smbus_xfer callback in the i2c_algorithm
structure of the I2C adapters created by the hub driver for each
SMBus-enabled port.

With this support in place, user-space utilities such as i2cdetect,
i2cget, and i2cset can access and operate on I2C/SMBus devices
connected behind the hub target ports.

Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@xxxxxxx>
Signed-off-by: Vikash Bansal <vikash.bansal@xxxxxxx>
Signed-off-by: Lakshay Piplani <lakshay.piplani@xxxxxxx>
---
drivers/i3c/hub/p3h2840_i3c_hub.h | 3 +
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 211 +++++++++++++++++++++++-
2 files changed, 213 insertions(+), 1 deletion(-)

diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
index 5b3fb1752c7b..38f02500b6a9 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub.h
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -188,6 +188,9 @@

#define ONE_BYTE_SIZE 0x01

+/* Payload size of an SMBus word transaction. */
+#define P3H2X4X_SMBUS_WORD_SIZE 0x02
+
/* holding SDA low when both SMBus Target Agent received data buffers are full.
* This feature can be used as a flow-control mechanism for MCTP applications to
* avoid MCTP transmitters on Target Ports time out when the SMBus agent buffers
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
index 7fdfa79dfeb6..2f301134b978 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
@@ -7,6 +7,7 @@
#include <linux/i3c/device.h>
#include <linux/mfd/p3h2840.h>
#include <linux/regmap.h>
+#include <linux/unaligned.h>

#include "p3h2840_i3c_hub.h"

@@ -387,9 +388,216 @@ static s32 p3h2x4x_tp_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, i
return ret_sum;
}

+/*
+ * p3h2x4x_tp_smbus_xfer_msg() - Run a single SMBus transaction through the
+ * SMBus Controller Agent of @target_port. The descriptor and the payload are
+ * written into the port's controller buffer page, which is located by
+ * multiplying the target port index by the number of pages per port and adding
+ * the base page number.
+ *
+ * @read is true for an SMBus read transaction. Note that for I2C_SMBUS_QUICK
+ * the I2C core passes @data as NULL and the transfer direction itself is the
+ * information sent to the device, so @data must not be dereferenced for it.
+ */
+static int p3h2x4x_tp_smbus_xfer_msg(struct p3h2x4x_i3c_hub_dev *hub,
+ u8 target_port,
+ u16 addr,
+ bool read,
+ u8 cmd,
+ int sz,
+ union i2c_smbus_data *data)
+{
+ u8 controller_buffer_page = P3H2X4X_CONTROLLER_BUFFER_PAGE +
+ P3H2X4X_NO_PAGE_PER_TP * target_port;
+ u8 target_port_status = P3H2X4X_TP0_SMBUS_AGNT_STS + target_port;
+ u8 transaction_type = P3H2X4X_SMBUS_400kHz;
+ u8 desc[P3H2X4X_SMBUS_DESCRIPTOR_SIZE] = { 0 };
+ u8 buf[I2C_SMBUS_BLOCK_MAX + 2] = { 0 };
+ u8 read_length = 0, write_length = 0;
+ u8 rw_address = addr << 1;
+ int ret, ret2;
+
+ switch (sz) {
+ case I2C_SMBUS_QUICK:
+ /*
+ * A QUICK transaction carries no data at all: only the address
+ * byte including the R/W bit is sent.
+ */
+ if (read)
+ rw_address |= P3H2X4X_SET_BIT(0);
+ break;
+ case I2C_SMBUS_BYTE:
+ if (read) {
+ rw_address |= P3H2X4X_SET_BIT(0);
+ read_length = ONE_BYTE_SIZE;
+ } else {
+ buf[0] = cmd;
+ write_length = ONE_BYTE_SIZE;
+ }
+ break;
+ case I2C_SMBUS_BYTE_DATA:
+ buf[0] = cmd;
+ write_length = ONE_BYTE_SIZE;
+ if (read) {
+ read_length = ONE_BYTE_SIZE;
+ transaction_type |= P3H2X4X_SET_BIT(0);
+ } else {
+ buf[1] = data->byte;
+ write_length += ONE_BYTE_SIZE;
+ }
+ break;
+ case I2C_SMBUS_WORD_DATA:
+ buf[0] = cmd;
+ write_length = ONE_BYTE_SIZE;
+ if (read) {
+ read_length = P3H2X4X_SMBUS_WORD_SIZE;
+ transaction_type |= P3H2X4X_SET_BIT(0);
+ } else {
+ put_unaligned_le16(data->word, &buf[1]);
+ write_length += P3H2X4X_SMBUS_WORD_SIZE;
+ }
+ break;
+ case I2C_SMBUS_BLOCK_DATA:
+ buf[0] = cmd;
+ write_length = ONE_BYTE_SIZE;
+ if (read) {
+ /*
+ * The agent returns the length byte followed by the
+ * payload, so reserve room for the maximum block size.
+ */
+ read_length = I2C_SMBUS_BLOCK_MAX + 1;
+ transaction_type |= P3H2X4X_SET_BIT(0);
+ } else {
+ if (data->block[0] > I2C_SMBUS_BLOCK_MAX)
+ return -EINVAL;
+
+ memcpy(&buf[1], data->block, data->block[0] + 1);
+ write_length += data->block[0] + 1;
+ }
+ break;
+ case I2C_SMBUS_I2C_BLOCK_DATA:
+ if (data->block[0] > I2C_SMBUS_BLOCK_MAX)
+ return -EINVAL;
+
+ buf[0] = cmd;
+ write_length = ONE_BYTE_SIZE;
+ if (read) {
+ read_length = data->block[0];
+ transaction_type |= P3H2X4X_SET_BIT(0);
+ } else {
+ memcpy(&buf[1], &data->block[1], data->block[0]);
+ write_length += data->block[0];
+ }
+ break;
+ default:
+ dev_dbg(hub->dev, "Unsupported SMBus transaction %d\n", sz);
+ return -EOPNOTSUPP;
+ }
+
+ if (write_length + read_length > P3H2X4X_SMBUS_PAYLOAD_SIZE)
+ return -EINVAL;
+
+ desc[P3H2X4X_DESC_ADDR] = rw_address;
+ desc[P3H2X4X_DESC_TYPE] = transaction_type;
+ desc[P3H2X4X_DESC_WRITE_LEN] = write_length;
+ desc[P3H2X4X_DESC_READ_LEN] = read_length;
+
+ ret = regmap_write(hub->regmap, target_port_status, P3H2X4X_TP_BUFFER_STATUS_MASK);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(hub->regmap, P3H2X4X_PAGE_PTR, controller_buffer_page);
+ if (ret)
+ return ret;
+
+ ret = regmap_bulk_write(hub->regmap, P3H2X4X_CONTROLLER_AGENT_BUFF,
+ desc, P3H2X4X_SMBUS_DESCRIPTOR_SIZE);
+ if (ret)
+ goto out_page;
+
+ if (write_length) {
+ ret = regmap_bulk_write(hub->regmap, P3H2X4X_CONTROLLER_AGENT_BUFF_DATA,
+ buf, write_length);
+ if (ret)
+ goto out_page;
+ }
+
+ ret = regmap_write(hub->regmap, P3H2X4X_TP_SMBUS_AGNT_TRANS_START,
+ hub->tp_bus[target_port].tp_mask);
+ if (ret)
+ goto out_page;
+
+ ret = p3h2x4x_read_smbus_transaction_status(hub, target_port_status,
+ write_length + read_length);
+ if (ret)
+ goto out_page;
+
+ if (read && read_length) {
+ ret = regmap_bulk_read(hub->regmap,
+ P3H2X4X_CONTROLLER_AGENT_BUFF_DATA + write_length,
+ buf, read_length);
+ if (ret)
+ goto out_page;
+
+ switch (sz) {
+ case I2C_SMBUS_BYTE:
+ case I2C_SMBUS_BYTE_DATA:
+ data->byte = buf[0];
+ break;
+ case I2C_SMBUS_WORD_DATA:
+ data->word = get_unaligned_le16(buf);
+ break;
+ case I2C_SMBUS_BLOCK_DATA:
+ /*
+ * The device reports the payload length in the first
+ * byte. Clamp it so a misbehaving device cannot make
+ * the I2C core copy out more than the block buffer.
+ */
+ data->block[0] = min_t(u8, buf[0], I2C_SMBUS_BLOCK_MAX);
+ memcpy(&data->block[1], &buf[1], data->block[0]);
+ break;
+ case I2C_SMBUS_I2C_BLOCK_DATA:
+ memcpy(&data->block[1], buf, read_length);
+ break;
+ }
+ }
+
+out_page:
+ ret2 = regmap_write(hub->regmap, P3H2X4X_PAGE_PTR, 0x00);
+ if (!ret && ret2)
+ ret = ret2;
+
+ return ret;
+}
+
+static s32 p3h2x4x_tp_smbus_xfer(struct i2c_adapter *adap, u16 addr,
+ unsigned short flags,
+ char read_write, u8 command,
+ int size, union i2c_smbus_data *data)
+{
+ struct tp_bus *bus = i2c_get_adapdata(adap);
+ struct p3h2x4x_i3c_hub_dev *hub = bus->p3h2x4x_i3c_hub;
+
+ if (flags & I2C_CLIENT_PEC)
+ return -EOPNOTSUPP;
+
+ guard(mutex)(&hub->etx_mutex);
+ guard(mutex)(&bus->port_mutex);
+
+ return p3h2x4x_tp_smbus_xfer_msg(hub, bus->tp_port, addr,
+ read_write == I2C_SMBUS_READ,
+ command, size, data);
+}
+
static u32 p3h2x4x_tp_smbus_funcs(struct i2c_adapter *adapter)
{
- u32 funcs = I2C_FUNC_I2C | I2C_FUNC_SMBUS_BLOCK_DATA;
+ u32 funcs = I2C_FUNC_SMBUS_QUICK |
+ I2C_FUNC_SMBUS_BYTE |
+ I2C_FUNC_SMBUS_BYTE_DATA |
+ I2C_FUNC_SMBUS_WORD_DATA |
+ I2C_FUNC_SMBUS_BLOCK_DATA |
+ I2C_FUNC_SMBUS_I2C_BLOCK |
+ I2C_FUNC_I2C;

#if IS_ENABLED(CONFIG_I2C_SLAVE)
struct tp_bus *bus = i2c_get_adapdata(adapter);
@@ -528,6 +736,7 @@ static const struct i2c_adapter_quirks p3h2x4x_tp_i2c_quirks = {
*/
static struct i2c_algorithm p3h2x4x_tp_i2c_algorithm = {
.master_xfer = p3h2x4x_tp_i2c_xfer,
+ .smbus_xfer = p3h2x4x_tp_smbus_xfer,
#if IS_ENABLED(CONFIG_I2C_SLAVE)
.reg_slave = p3h2x4x_tp_i2c_reg_slave,
.unreg_slave = p3h2x4x_tp_i2c_unreg_slave,
--
2.25.1