* [RFC PATCH 1/7] i3c: hub: p3h2x4x: Add support for debugfs
2026-10-16 16:46 [RFC PATCH 0/7] i3c: hub: p3h2x4x: add debugfs, i2c-tools, bus rate and GPIO mode support Lakshay Piplani
@ 2026-10-16 16:46 ` Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 2/7] i3c: hub: add i2c-tools support for SMBus-configured target ports Lakshay Piplani
` (5 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Lakshay Piplani @ 2026-10-16 16:46 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
Add debugfs support to expose configurations and registers information
for the P3H2x4x I3C hub.
This change adds a debugfs interface that allows inspection of current HUB,
control port and target port configurations. It also provides a simple
mechanism to read and write HUB registers.
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
---
drivers/i3c/hub/p3h2840_i3c_hub.h | 7 +
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 188 ++++++++++++++++++++++-
2 files changed, 194 insertions(+), 1 deletion(-)
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
index 0e59351e7b6d..5b3fb1752c7b 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub.h
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -14,6 +14,9 @@
#include <linux/i3c/master.h>
#include <linux/regulator/consumer.h>
#include <linux/regmap.h>
+#ifdef CONFIG_DEBUG_FS
+#include <linux/debugfs.h>
+#endif
/* I3C HUB REGISTERS */
@@ -313,6 +316,10 @@ struct tp_bus {
};
struct p3h2x4x_i3c_hub_dev {
+#ifdef CONFIG_DEBUG_FS
+ u8 reg_addr; /* Offset of the HUB register to read or write. */
+ struct dentry *debugfs_dir;
+#endif
struct device *dev;
struct regmap *regmap;
struct mutex etx_mutex; /* all port mutex */
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_common.c b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
index 0c18459d7ec6..906034632a94 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub_common.c
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
@@ -298,6 +298,192 @@ static void p3h2x4x_clear_i3c_hub_priv(void *data)
p3h2x4x->i3c_hub_priv = NULL;
}
+#ifdef CONFIG_DEBUG_FS
+
+static int p3h2x4x_i3c_hub_access_reg_get(void *dvrdata, u64 *val)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dvrdata;
+ u32 reg_val;
+ int ret;
+
+ ret = regmap_read(p3h2x4x_i3c_hub->regmap, p3h2x4x_i3c_hub->reg_addr, ®_val);
+ if (ret)
+ return ret;
+
+ *val = reg_val & 0xFF;
+ return 0;
+}
+
+static int p3h2x4x_i3c_hub_access_reg_set(void *dvrdata, u64 val)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dvrdata;
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(p3h2x4x_i3c_hub->dev->parent);
+ int ret, ret2;
+
+ /* HUB registers are one byte wide, reject anything that does not fit. */
+ if (val > U8_MAX)
+ return -EINVAL;
+
+ scoped_guard(mutex, &p3h2x4x->protected_reg_lock) {
+ ret = regmap_write(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ break;
+
+ ret = regmap_write(p3h2x4x_i3c_hub->regmap,
+ p3h2x4x_i3c_hub->reg_addr, val);
+
+ ret2 = regmap_write(p3h2x4x_i3c_hub->regmap,
+ P3H2X4X_DEV_REG_PROTECTION_CODE,
+ P3H2X4X_REGISTERS_LOCK_CODE);
+ if (!ret && ret2)
+ ret = ret2;
+ }
+
+ return ret;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(p3h2x4x_i3c_hub_access_reg, p3h2x4x_i3c_hub_access_reg_get,
+ p3h2x4x_i3c_hub_access_reg_set, "%llx\n");
+
+static int p3h2x4x_i3c_hub_offset_reg_get(void *dvrdata, u64 *val)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dvrdata;
+
+ *val = p3h2x4x_i3c_hub->reg_addr;
+
+ return 0;
+}
+
+static int p3h2x4x_i3c_hub_offset_reg_set(void *dvrdata, u64 val)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = dvrdata;
+
+ if (val > U8_MAX)
+ return -EINVAL;
+
+ p3h2x4x_i3c_hub->reg_addr = val;
+
+ return 0;
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(p3h2x4x_i3c_hub_offset_reg, p3h2x4x_i3c_hub_offset_reg_get,
+ p3h2x4x_i3c_hub_offset_reg_set, "%llx\n");
+
+static int p3h2x4x_i3c_hub_tp_mode_show(struct seq_file *s, void *data)
+{
+ const struct tp_configuration *tp_config = s->private;
+
+ seq_printf(s, "%s\n",
+ tp_config->mode == P3H2X4X_TP_MODE_SMBUS ? "smbus" : "i3c");
+
+ return 0;
+}
+DEFINE_SHOW_ATTRIBUTE(p3h2x4x_i3c_hub_tp_mode);
+
+static void p3h2x4x_i3c_hub_debugfs_remove(void *data)
+{
+ struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub = data;
+
+ debugfs_remove_recursive(p3h2x4x_i3c_hub->debugfs_dir);
+}
+
+static int p3h2x4x_i3c_hub_debugfs_init(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub)
+{
+ struct device *dev = p3h2x4x_i3c_hub->dev;
+ struct dentry *conf_dir;
+ struct dentry *tp_dir;
+ struct dentry *cp_dir;
+ struct dentry *reg_dir;
+ char hub_id[64];
+ char dir_name[32];
+ int i;
+
+ if (p3h2x4x_i3c_hub->i3cdev) {
+ snprintf(hub_id, sizeof(hub_id), "i3c-hub-device-%d-%llx",
+ i3c_dev_get_master(p3h2x4x_i3c_hub->i3cdev->desc)->bus.id,
+ p3h2x4x_i3c_hub->i3cdev->desc->info.pid);
+ } else {
+ struct i2c_client *client = to_i2c_client(dev->parent);
+
+ /*
+ * Include the client address, the adapter number alone is not
+ * unique when more than one HUB sits on the same I2C adapter
+ * and the resulting debugfs directory name would collide.
+ */
+ snprintf(hub_id, sizeof(hub_id), "i3c-hub-device-%d-%02x",
+ client->adapter->nr, client->addr);
+ }
+
+ p3h2x4x_i3c_hub->debugfs_dir = debugfs_create_dir(hub_id, NULL);
+
+ conf_dir = debugfs_create_dir("dt-p3h2x4x-conf", p3h2x4x_i3c_hub->debugfs_dir);
+
+ cp_dir = debugfs_create_dir("p3h2x4x-control-ports", conf_dir);
+
+ debugfs_create_u32("cp0-io-strength-ohms", 0400, cp_dir,
+ &p3h2x4x_i3c_hub->hub_config.cp0_io_strength);
+
+ debugfs_create_u32("cp1-io-strength-ohms", 0400, cp_dir,
+ &p3h2x4x_i3c_hub->hub_config.cp1_io_strength);
+
+ for (i = 0; i < P3H2X4X_TP_MAX_COUNT; ++i) {
+ struct tp_configuration *tp_config = &p3h2x4x_i3c_hub->hub_config.tp_config[i];
+
+ snprintf(dir_name, sizeof(dir_name), "target-port-%d", i);
+
+ tp_dir = debugfs_create_dir(dir_name, conf_dir);
+
+ debugfs_create_file("mode", 0400, tp_dir, tp_config,
+ &p3h2x4x_i3c_hub_tp_mode_fops);
+
+ debugfs_create_bool("pullup-enable", 0400, tp_dir, &tp_config->pullup_en);
+
+ debugfs_create_bool("ibi-enable", 0400, tp_dir, &tp_config->ibi_en);
+
+ debugfs_create_bool("always-enable", 0400, tp_dir, &tp_config->always_enable);
+
+ /*
+ * Target ports are grouped in fours (0145 and 2367) and each
+ * group shares a single I/O strength and pull-up setting.
+ */
+ if ((i / 2) % 2 == 0) {
+ debugfs_create_u32("io-strength-ohms", 0400, tp_dir,
+ &p3h2x4x_i3c_hub->hub_config.tp0145_io_strength);
+
+ debugfs_create_u32("internal-pullups-ohms", 0400, tp_dir,
+ &p3h2x4x_i3c_hub->hub_config.tp0145_pullup);
+ } else {
+ debugfs_create_u32("io-strength-ohms", 0400, tp_dir,
+ &p3h2x4x_i3c_hub->hub_config.tp2367_io_strength);
+
+ debugfs_create_u32("internal-pullups-ohms", 0400, tp_dir,
+ &p3h2x4x_i3c_hub->hub_config.tp2367_pullup);
+ }
+ }
+
+ reg_dir = debugfs_create_dir("reg", p3h2x4x_i3c_hub->debugfs_dir);
+
+ debugfs_create_file_unsafe("access", 0600, reg_dir, p3h2x4x_i3c_hub,
+ &p3h2x4x_i3c_hub_access_reg);
+
+ debugfs_create_file_unsafe("offset", 0600, reg_dir, p3h2x4x_i3c_hub,
+ &p3h2x4x_i3c_hub_offset_reg);
+
+ return devm_add_action_or_reset(dev, p3h2x4x_i3c_hub_debugfs_remove,
+ p3h2x4x_i3c_hub);
+}
+
+#else
+
+static inline int p3h2x4x_i3c_hub_debugfs_init(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub)
+{
+ return 0;
+}
+
+#endif /* CONFIG_DEBUG_FS */
+
static int p3h2x4x_i3c_hub_probe(struct platform_device *pdev)
{
struct p3h2x4x *p3h2x4x = dev_get_drvdata(pdev->dev.parent);
@@ -387,7 +573,7 @@ static int p3h2x4x_i3c_hub_probe(struct platform_device *pdev)
if (ret)
return ret;
- return 0;
+ return p3h2x4x_i3c_hub_debugfs_init(p3h2x4x_i3c_hub);
}
static const struct platform_device_id p3h2x4x_i3c_hub_id[] = {
--
2.25.1
^ permalink raw reply [flat|nested] 10+ messages in thread* [RFC PATCH 2/7] i3c: hub: add i2c-tools support for SMBus-configured target ports
2026-10-16 16:46 [RFC PATCH 0/7] i3c: hub: p3h2x4x: add debugfs, i2c-tools, bus rate and GPIO mode support Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 1/7] i3c: hub: p3h2x4x: Add support for debugfs Lakshay Piplani
@ 2026-10-16 16:46 ` Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 3/7] dt-bindings: i3c: nxp,p3h2840: add clock-frequency for SMBus " Lakshay Piplani
` (4 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Lakshay Piplani @ 2026-10-16 16:46 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
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@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
---
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
^ permalink raw reply [flat|nested] 10+ messages in thread* [RFC PATCH 3/7] dt-bindings: i3c: nxp,p3h2840: add clock-frequency for SMBus target ports
2026-10-16 16:46 [RFC PATCH 0/7] i3c: hub: p3h2x4x: add debugfs, i2c-tools, bus rate and GPIO mode support Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 1/7] i3c: hub: p3h2x4x: Add support for debugfs Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 2/7] i3c: hub: add i2c-tools support for SMBus-configured target ports Lakshay Piplani
@ 2026-10-16 16:46 ` Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 4/7] i3c: hub: p3h2x4x: add clock-frequency DTS support Lakshay Piplani
` (3 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Lakshay Piplani @ 2026-10-16 16:46 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
The SMBus Controller Agent selects the bus speed per transaction through
the transaction descriptor, and supports four discrete rates: 100 kHz,
200 kHz, 400 kHz and 1 MHz.
Describe the standard 'clock-frequency' property for the smbus@ target
port nodes so the rate can be specified per port. The property is already
available through the i2c-controller.yaml reference; restrict it to the
four rates the hardware implements so an unsupported value is rejected at
build time rather than being silently rounded. Target ports default to
400 kHz when the property is absent.
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
.../devicetree/bindings/i3c/nxp,p3h2840.yaml | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
index 09508a7bcbeb..66f9e2b982b1 100644
--- a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
+++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
@@ -168,6 +168,14 @@ patternProperties:
description:
Enables the on-die pull-up for Target Port.
+ clock-frequency:
+ enum: [100000, 200000, 400000, 1000000]
+ default: 400000
+ description:
+ SMBus clock rate for this Target Port. The SMBus Controller Agent
+ supports four discrete speeds, selected per transaction. When this
+ property is absent the Target Port runs at 400 kHz.
+
required:
- reg
@@ -248,6 +256,7 @@ examples:
reg = <0x0>;
#address-cells = <1>;
#size-cells = <0>;
+ clock-frequency = <400000>;
nxp,pullup-enable;
};
@@ -255,6 +264,7 @@ examples:
reg = <0x1>;
#address-cells = <1>;
#size-cells = <0>;
+ clock-frequency = <100000>;
nxp,pullup-enable;
};
@@ -317,6 +327,7 @@ examples:
reg = <0x0>;
#address-cells = <1>;
#size-cells = <0>;
+ clock-frequency = <1000000>;
nxp,pullup-enable;
};
--
2.25.1
^ permalink raw reply [flat|nested] 10+ messages in thread* [RFC PATCH 4/7] i3c: hub: p3h2x4x: add clock-frequency DTS support
2026-10-16 16:46 [RFC PATCH 0/7] i3c: hub: p3h2x4x: add debugfs, i2c-tools, bus rate and GPIO mode support Lakshay Piplani
` (2 preceding siblings ...)
2026-10-16 16:46 ` [RFC PATCH 3/7] dt-bindings: i3c: nxp,p3h2840: add clock-frequency for SMBus " Lakshay Piplani
@ 2026-10-16 16:46 ` Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 5/7] dt-bindings: i3c: nxp,p3h2840: add GPIO mode target ports Lakshay Piplani
` (2 subsequent siblings)
6 siblings, 0 replies; 10+ messages in thread
From: Lakshay Piplani @ 2026-10-16 16:46 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
Read the standard 'clock-frequency' DT property for each SMBus target
port and store it in bus_clk_rate. Map it to one of four hardware speeds
using nearest-neighbour rounding (datasheet Table 20, descriptor byte 1
bits[2:1]):
<= 150 kHz -> 100 kHz (bits = 00)
<= 300 kHz -> 200 kHz (bits = 01)
<= 700 kHz -> 400 kHz (bits = 10, also the default when absent)
> 700 kHz -> 1 MHz (bits = 11)
The port rate also scales the expected transaction time, which is used
to derive the status poll interval. Replace the fixed 400 kHz estimate
with one computed from the port rate, and keep flooring the overall poll
deadline at the SDA-stuck recovery window so a slow bus does not report
a spurious timeout. An out-of-range or missing rate falls back to
400 kHz, so the transaction time never divides by zero.
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
drivers/i3c/hub/p3h2840_i3c_hub.h | 30 +++++++++---
drivers/i3c/hub/p3h2840_i3c_hub_smbus.c | 65 ++++++++++++++++++++++---
2 files changed, 82 insertions(+), 13 deletions(-)
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
index 38f02500b6a9..ad2e7cb7f5ea 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub.h
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -219,8 +219,20 @@
#define P3H2X4X_TP_BUFFER_STATUS_MASK 0x0f
#define P3H2X4X_TP_TRANSACTION_CODE_MASK 0xf0
-/* SMBus transaction types fields */
+/* SMBus transaction types fields - descriptor byte 1, bits [2:1] */
+#define P3H2X4X_SMBUS_100kHz 0
+#define P3H2X4X_SMBUS_200kHz BIT(1)
#define P3H2X4X_SMBUS_400kHz BIT(2)
+#define P3H2X4X_SMBUS_1MHz (BIT(2) | BIT(1))
+
+/* SMBus clock rates, in Hz, supported by the SMBus Controller Agent */
+#define P3H2X4X_SMBUS_CLK_RATE_100kHz 100000
+#define P3H2X4X_SMBUS_CLK_RATE_200kHz 200000
+#define P3H2X4X_SMBUS_CLK_RATE_400kHz 400000
+#define P3H2X4X_SMBUS_CLK_RATE_1MHz 1000000
+
+/* Target ports run at 400 kHz unless the DT requests another rate. */
+#define P3H2X4X_SMBUS_CLK_RATE_DFT P3H2X4X_SMBUS_CLK_RATE_400kHz
/* SMBus polling */
#define P3H2X4X_SMBUS_POLL_COUNT 10
@@ -236,13 +248,16 @@
#define P3H2X4X_SMBUS_TARGET_PAYLOAD_SIZE (P3H2X4X_TARGET_BUFFER_SIZE - 2)
/*
- * At 400 kHz, one 9-bit I2C byte takes 22.5 us. Round this
- * up to 23 us per payload byte and add 100 us for the address
- * byte, bus overhead and controller processing time. This models
- * the time a healthy transaction takes and is used to derive the
- * poll interval, not the overall give-up deadline.
+ * One I2C byte on the wire is 9 bits (8 data + ACK). Model a healthy
+ * transaction as the payload plus four bytes of address and descriptor
+ * overhead, and scale it by the port clock rate. The result is used to
+ * derive the status poll interval, not the overall give-up deadline.
*/
-#define P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(x) ((23 * (x)) + 100)
+#define P3H2X4X_SMBUS_BITS_PER_BYTE 9
+#define P3H2X4X_SMBUS_XFER_OVERHEAD_BYTES 4
+#define P3H2X4X_SMBUS_TRANSFER_US(rate, len) \
+ (DIV_ROUND_UP(P3H2X4X_SMBUS_BITS_PER_BYTE * USEC_PER_SEC * \
+ ((len) + P3H2X4X_SMBUS_XFER_OVERHEAD_BYTES), (rate)))
/*
* On an SDA-stuck condition the SMBus Controller Agent can legitimately
@@ -310,6 +325,7 @@ struct tp_bus {
bool is_registered; /* bus was registered in the framework. */
u8 tp_mask;
u8 tp_port;
+ u32 bus_clk_rate; /* SMBus clock rate for this port, in Hz. */
struct mutex port_mutex; /* per port mutex */
struct device_node *of_node;
struct i2c_client *tp_smbus_client;
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
index 2f301134b978..406bf9f15446 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_smbus.c
@@ -18,6 +18,47 @@ enum p3h2x4x_smbus_desc_idx {
P3H2X4X_DESC_READ_LEN,
};
+/*
+ * p3h2x4x_tp_speed_type() - Map a bus clock rate to the transaction descriptor
+ * speed field. The SMBus Controller Agent supports four discrete rates, so pick
+ * the nearest one for rates the device tree may have rounded.
+ */
+static u8 p3h2x4x_tp_speed_type(u32 clk_rate)
+{
+ if (clk_rate > 700000)
+ return P3H2X4X_SMBUS_1MHz;
+ if (clk_rate > 300000)
+ return P3H2X4X_SMBUS_400kHz;
+ if (clk_rate > 150000)
+ return P3H2X4X_SMBUS_200kHz;
+
+ return P3H2X4X_SMBUS_100kHz;
+}
+
+/*
+ * p3h2x4x_tp_get_clk_rate() - Read the requested SMBus clock rate of a target
+ * port from the device tree. The rate also scales the transaction time estimate,
+ * so reject a zero or out-of-range value rather than dividing by it later.
+ */
+static u32 p3h2x4x_tp_get_clk_rate(struct p3h2x4x_i3c_hub_dev *hub, u8 target_port)
+{
+ u32 clk_rate;
+
+ if (of_property_read_u32(hub->tp_bus[target_port].of_node, "clock-frequency",
+ &clk_rate))
+ return P3H2X4X_SMBUS_CLK_RATE_DFT;
+
+ if (clk_rate < P3H2X4X_SMBUS_CLK_RATE_100kHz ||
+ clk_rate > P3H2X4X_SMBUS_CLK_RATE_1MHz) {
+ dev_warn(hub->dev,
+ "port %u: unsupported clock-frequency %u Hz, using %u Hz\n",
+ target_port, clk_rate, P3H2X4X_SMBUS_CLK_RATE_DFT);
+ return P3H2X4X_SMBUS_CLK_RATE_DFT;
+ }
+
+ return clk_rate;
+}
+
#if IS_ENABLED(CONFIG_I2C_SLAVE)
static void p3h2x4x_read_smbus_agent_rx_buf(struct i3c_device *i3cdev, enum p3h2x4x_rcv_buf rfbuf,
enum p3h2x4x_tp tp)
@@ -192,14 +233,15 @@ void p3h2x4x_ibi_handler(struct i3c_device *i3cdev,
static int p3h2x4x_read_smbus_transaction_status(struct p3h2x4x_i3c_hub_dev *hub,
u8 target_port_status,
- u8 data_length)
+ u8 data_length,
+ u32 clk_rate)
{
unsigned int xfer_us, timeout_us, sleep_us;
u32 status_read;
u8 status;
int ret;
- xfer_us = P3H2X4X_SMBUS_400kHz_TRANSFER_TIMEOUT(data_length);
+ xfer_us = P3H2X4X_SMBUS_TRANSFER_US(clk_rate, data_length);
sleep_us = clamp(xfer_us / P3H2X4X_SMBUS_POLL_COUNT,
P3H2X4X_SMBUS_POLL_INTERVAL_MIN_US,
P3H2X4X_SMBUS_POLL_INTERVAL_MAX_US);
@@ -252,8 +294,9 @@ static int p3h2x4x_tp_i2c_xfer_msg(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub,
{
u8 controller_buffer_page = P3H2X4X_CONTROLLER_BUFFER_PAGE + 4 * target_port;
u8 target_port_status = P3H2X4X_TP0_SMBUS_AGNT_STS + target_port;
+ u32 clk_rate = p3h2x4x_i3c_hub->tp_bus[target_port].bus_clk_rate;
+ u8 transaction_type = p3h2x4x_tp_speed_type(clk_rate);
u8 desc[P3H2X4X_SMBUS_DESCRIPTOR_SIZE] = { 0 };
- u8 transaction_type = P3H2X4X_SMBUS_400kHz;
int write_length, read_length;
u8 addr = xfers[nxfers_i].addr;
u8 rw_address = 2 * addr;
@@ -308,7 +351,8 @@ static int p3h2x4x_tp_i2c_xfer_msg(struct p3h2x4x_i3c_hub_dev *p3h2x4x_i3c_hub,
ret = p3h2x4x_read_smbus_transaction_status(p3h2x4x_i3c_hub,
target_port_status,
- (write_length + read_length));
+ (write_length + read_length),
+ clk_rate);
if (ret)
goto out;
@@ -410,7 +454,8 @@ static int p3h2x4x_tp_smbus_xfer_msg(struct p3h2x4x_i3c_hub_dev *hub,
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;
+ u32 clk_rate = hub->tp_bus[target_port].bus_clk_rate;
+ u8 transaction_type = p3h2x4x_tp_speed_type(clk_rate);
u8 desc[P3H2X4X_SMBUS_DESCRIPTOR_SIZE] = { 0 };
u8 buf[I2C_SMBUS_BLOCK_MAX + 2] = { 0 };
u8 read_length = 0, write_length = 0;
@@ -528,7 +573,8 @@ static int p3h2x4x_tp_smbus_xfer_msg(struct p3h2x4x_i3c_hub_dev *hub,
goto out_page;
ret = p3h2x4x_read_smbus_transaction_status(hub, target_port_status,
- write_length + read_length);
+ write_length + read_length,
+ clk_rate);
if (ret)
goto out_page;
@@ -814,6 +860,13 @@ int p3h2x4x_tp_smbus_algo(struct p3h2x4x_i3c_hub_dev *hub)
i2c_set_adapdata(smbus_adapter, &hub->tp_bus[tp]);
+ /*
+ * Determine the port clock rate before the adapter goes live, so
+ * a DT child probed from i2c_add_adapter() already transfers at
+ * the requested speed.
+ */
+ hub->tp_bus[tp].bus_clk_rate = p3h2x4x_tp_get_clk_rate(hub, tp);
+
/*
* Publish the callback-visible state before i2c_add_adapter(),
* which can synchronously probe a DT slave and invoke
--
2.25.1
^ permalink raw reply [flat|nested] 10+ messages in thread* [RFC PATCH 5/7] dt-bindings: i3c: nxp,p3h2840: add GPIO mode target ports
2026-10-16 16:46 [RFC PATCH 0/7] i3c: hub: p3h2x4x: add debugfs, i2c-tools, bus rate and GPIO mode support Lakshay Piplani
` (3 preceding siblings ...)
2026-10-16 16:46 ` [RFC PATCH 4/7] i3c: hub: p3h2x4x: add clock-frequency DTS support Lakshay Piplani
@ 2026-10-16 16:46 ` Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 6/7] i3c: hub: p3h2x4x: configure target ports placed in GPIO mode Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 7/7] gpio: p3h2x4x: add GPIO expander for the NXP P3H2x4x I3C hub Lakshay Piplani
6 siblings, 0 replies; 10+ messages in thread
From: Lakshay Piplani @ 2026-10-16 16:46 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
Add a "gpio@N" child node for target ports whose SCL and SDA pins are used
as general purpose IOs instead of carrying bus traffic, and mark the hub as
a gpio-controller.
GPIO mode has higher priority than the other port functions and leaves the
port disconnected from the Hub Network, so a port is either a bus port or a
GPIO port. Each port contributes two lines, the SCL pin first, so target
port N owns the lines 2 * N and 2 * N + 1.
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
.../devicetree/bindings/i3c/nxp,p3h2840.yaml | 61 +++++++++++++++++++
1 file changed, 61 insertions(+)
diff --git a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
index 66f9e2b982b1..2cd52d142a17 100644
--- a/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
+++ b/Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
@@ -47,6 +47,19 @@ properties:
'#size-cells':
const: 0
+ gpio-controller: true
+
+ '#gpio-cells':
+ const: 2
+
+ gpio-line-names:
+ description:
+ Names for the lines exposed by Target Ports placed in GPIO mode. Target
+ Port N owns the lines 2 * N (SCL pin) and 2 * N + 1 (SDA pin), so the
+ list covers up to two lines per Target Port the variant provides.
+ minItems: 1
+ maxItems: 16
+
assigned-address:
maximum: 0x7f
@@ -150,6 +163,34 @@ patternProperties:
required:
- reg
+ "^gpio@[0-7]$":
+ type: object
+ additionalProperties: false
+ description:
+ A Target Port whose SCL and SDA pins are used as general purpose IOs
+ instead of carrying bus traffic. GPIO mode takes priority over the other
+ port functions and leaves the port disconnected from the Hub Network.
+
+ The two pins of the port are exposed by the hub gpio-controller as two
+ consecutive lines, the SCL pin first. Target Port N therefore owns the
+ lines numbered 2 * N and 2 * N + 1.
+
+ properties:
+ reg:
+ description:
+ The I3C HUB Target Port number.
+ items:
+ - minimum: 0
+ maximum: 7
+
+ nxp,pullup-enable:
+ type: boolean
+ description:
+ Enables the on-die pull-up for Target Port.
+
+ required:
+ - reg
+
"^smbus@[0-7]$":
type: object
$ref: /schemas/i2c/i2c-controller.yaml#
@@ -199,8 +240,14 @@ allOf:
reg:
items:
- maximum: 3
+ "^gpio@[0-3]$":
+ properties:
+ reg:
+ items:
+ - maximum: 3
"^i3c@[4-7]$": false
"^smbus@[4-7]$": false
+ "^gpio@[4-7]$": false
unevaluatedProperties: false
@@ -216,6 +263,8 @@ examples:
#address-cells = <1>;
#size-cells = <0>;
assigned-address = <0x50>;
+ gpio-controller;
+ #gpio-cells = <2>;
nxp,tp0145-pullup-ohms = <1000>;
nxp,tp2367-pullup-ohms = <1000>;
@@ -274,6 +323,11 @@ examples:
#size-cells = <0>;
nxp,pullup-enable;
};
+
+ gpio@3 {
+ reg = <0x3>;
+ nxp,pullup-enable;
+ };
};
};
@@ -287,6 +341,8 @@ examples:
compatible = "nxp,p3h2840";
#address-cells = <1>;
#size-cells = <0>;
+ gpio-controller;
+ #gpio-cells = <2>;
nxp,tp0145-pullup-ohms = <1000>;
nxp,tp2367-pullup-ohms = <1000>;
@@ -337,5 +393,10 @@ examples:
#size-cells = <0>;
nxp,pullup-enable;
};
+
+ gpio@4 {
+ reg = <0x4>;
+ nxp,pullup-enable;
+ };
};
};
--
2.25.1
^ permalink raw reply [flat|nested] 10+ messages in thread* [RFC PATCH 6/7] i3c: hub: p3h2x4x: configure target ports placed in GPIO mode
2026-10-16 16:46 [RFC PATCH 0/7] i3c: hub: p3h2x4x: add debugfs, i2c-tools, bus rate and GPIO mode support Lakshay Piplani
` (4 preceding siblings ...)
2026-10-16 16:46 ` [RFC PATCH 5/7] dt-bindings: i3c: nxp,p3h2840: add GPIO mode target ports Lakshay Piplani
@ 2026-10-16 16:46 ` Lakshay Piplani
2026-10-16 16:46 ` [RFC PATCH 7/7] gpio: p3h2x4x: add GPIO expander for the NXP P3H2x4x I3C hub Lakshay Piplani
6 siblings, 0 replies; 10+ messages in thread
From: Lakshay Piplani @ 2026-10-16 16:46 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
Add a GPIO target port mode, selected by a "gpio@N" device tree node, and
program the target port GPIO mode register that was defined but never
written.
GPIO mode has higher priority than the SMBus agent and the Hub Network
connection and leaves the port disconnected from the network, so commit it
with the other port mode bits while the protected registers are unlocked,
and exclude the port from the Controller Port multiplexer request along
with the SMBus ports.
The mode enable is a protected register owned by this driver, while the
direction and level registers are driven by the GPIO child, so move the
mode enable to the shared MFD header.
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
drivers/i3c/hub/p3h2840_i3c_hub.h | 8 +-----
drivers/i3c/hub/p3h2840_i3c_hub_common.c | 34 +++++++++++++++++++-----
include/linux/mfd/p3h2840.h | 8 ++++++
3 files changed, 36 insertions(+), 14 deletions(-)
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub.h b/drivers/i3c/hub/p3h2840_i3c_hub.h
index ad2e7cb7f5ea..595e3ec60fd2 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub.h
+++ b/drivers/i3c/hub/p3h2840_i3c_hub.h
@@ -73,7 +73,6 @@
#define P3H2X4X_IBI_MDB_CUSTOM 0x1c
#define P3H2X4X_JEDEC_CONTEXT_ID 0x1d
-#define P3H2X4X_TP_GPIO_MODE_EN 0x1e
/* Device Status and IBI Registers */
#define P3H2X4X_DEV_AND_IBI_STS 0x20
@@ -92,17 +91,11 @@
#define P3H2X4X_TP_PULLUP_EN 0x53
-#define P3H2X4X_TP_SCL_OUT_EN 0x54
-#define P3H2X4X_TP_SDA_OUT_EN 0x55
-#define P3H2X4X_TP_SCL_OUT_LEVEL 0x56
-#define P3H2X4X_TP_SDA_OUT_LEVEL 0x57
#define P3H2X4X_TP_IN_DETECT_MODE_CONF 0x58
#define P3H2X4X_TP_SCL_IN_DETECT_IBI_EN 0x59
#define P3H2X4X_TP_SDA_IN_DETECT_IBI_EN 0x5a
/* Target Ports Status Registers */
-#define P3H2X4X_TP_SCL_IN_LEVEL_STS 0x60
-#define P3H2X4X_TP_SDA_IN_LEVEL_STS 0x61
#define P3H2X4X_TP_SCL_IN_DETECT_FLG 0x62
#define P3H2X4X_TP_SDA_IN_DETECT_FLG 0x63
@@ -302,6 +295,7 @@ enum p3h2x4x_rcv_buf {
enum p3h2x4x_tp_mode {
P3H2X4X_TP_MODE_I3C,
P3H2X4X_TP_MODE_SMBUS,
+ P3H2X4X_TP_MODE_GPIO,
};
struct tp_configuration {
diff --git a/drivers/i3c/hub/p3h2840_i3c_hub_common.c b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
index 906034632a94..0caf69b3cc1d 100644
--- a/drivers/i3c/hub/p3h2840_i3c_hub_common.c
+++ b/drivers/i3c/hub/p3h2840_i3c_hub_common.c
@@ -98,7 +98,7 @@ static int p3h2x4x_configure_tp(struct device *dev)
{
struct p3h2x4x_i3c_hub_dev *hub = dev_get_drvdata(dev);
struct p3h2x4x *p3h2x4x = dev_get_drvdata(dev->parent);
- u8 mode = 0, smbus = 0, pullup = 0, target_port = 0;
+ u8 mode = 0, smbus = 0, pullup = 0, target_port = 0, gpio = 0;
u8 tp_mask;
int tp, ret;
@@ -110,6 +110,8 @@ static int p3h2x4x_configure_tp(struct device *dev)
P3H2X4X_SET_BIT(tp) : 0;
target_port |= (hub->tp_bus[tp].tp_mask == P3H2X4X_SET_BIT(tp)) ?
hub->tp_bus[tp].tp_mask : 0;
+ gpio |= (hub->hub_config.tp_config[tp].mode == P3H2X4X_TP_MODE_GPIO) ?
+ P3H2X4X_SET_BIT(tp) : 0;
}
/* Only touch the bits for the target ports this variant provides. */
@@ -127,7 +129,19 @@ static int p3h2x4x_configure_tp(struct device *dev)
if (ret)
return ret;
- if (target_port & ~smbus) {
+ /*
+ * GPIO mode takes priority over the SMBus agent and the Hub Network
+ * connection, and leaves the port disconnected from the network
+ * (datasheet Table 10). Program it here, together with the other port
+ * mode bits, so the port function is committed in one place. The GPIO
+ * child driver then only touches the unprotected direction and level
+ * registers.
+ */
+ ret = regmap_update_bits(hub->regmap, P3H2X4X_TP_GPIO_MODE_EN, tp_mask, gpio);
+ if (ret)
+ return ret;
+
+ if (target_port & ~(smbus | gpio)) {
ret = regmap_write(hub->regmap, P3H2X4X_CP_MUX_SET,
P3H2X4X_CONTROLLER_PORT_MUX_REQ);
if (ret)
@@ -206,14 +220,16 @@ static int p3h2x4x_parse_tp_dt_settings(struct device *dev,
enum p3h2x4x_tp_mode mode;
/*
- * Only "i3c" and "smbus" children describe target ports. Skip any
- * other child (for example the MFD "regulators" container), which
- * has no "reg" property.
+ * Only "i3c", "smbus" and "gpio" children describe target ports.
+ * Skip any other child (for example the MFD "regulators"
+ * container), which has no "reg" property.
*/
if (of_node_name_eq(tp_node, "i3c"))
mode = P3H2X4X_TP_MODE_I3C;
else if (of_node_name_eq(tp_node, "smbus"))
mode = P3H2X4X_TP_MODE_SMBUS;
+ else if (of_node_name_eq(tp_node, "gpio"))
+ mode = P3H2X4X_TP_MODE_GPIO;
else
continue;
@@ -373,10 +389,14 @@ DEFINE_DEBUGFS_ATTRIBUTE(p3h2x4x_i3c_hub_offset_reg, p3h2x4x_i3c_hub_offset_reg_
static int p3h2x4x_i3c_hub_tp_mode_show(struct seq_file *s, void *data)
{
+ static const char * const mode_names[] = {
+ [P3H2X4X_TP_MODE_I3C] = "i3c",
+ [P3H2X4X_TP_MODE_SMBUS] = "smbus",
+ [P3H2X4X_TP_MODE_GPIO] = "gpio",
+ };
const struct tp_configuration *tp_config = s->private;
- seq_printf(s, "%s\n",
- tp_config->mode == P3H2X4X_TP_MODE_SMBUS ? "smbus" : "i3c");
+ seq_printf(s, "%s\n", mode_names[tp_config->mode]);
return 0;
}
diff --git a/include/linux/mfd/p3h2840.h b/include/linux/mfd/p3h2840.h
index eb4792de5440..537ff7d28690 100644
--- a/include/linux/mfd/p3h2840.h
+++ b/include/linux/mfd/p3h2840.h
@@ -24,6 +24,14 @@
#define P3H2X4X_REGISTERS_UNLOCK_CODE 0x69
#define P3H2X4X_CP1_REGISTERS_UNLOCK_CODE 0x6a
+/*
+ * Target port GPIO mode enable, one bit per target port. Shared because the
+ * hub driver owns this protected register while the GPIO driver depends on it
+ * to know which ports it may drive. The direction and level registers are not
+ * protected and are private to the GPIO driver.
+ */
+#define P3H2X4X_TP_GPIO_MODE_EN 0x1e
+
#define I3C_MANUF_ID_NXP 0x011b
struct p3h2x4x_i3c_hub_dev;
--
2.25.1
^ permalink raw reply [flat|nested] 10+ messages in thread* [RFC PATCH 7/7] gpio: p3h2x4x: add GPIO expander for the NXP P3H2x4x I3C hub
2026-10-16 16:46 [RFC PATCH 0/7] i3c: hub: p3h2x4x: add debugfs, i2c-tools, bus rate and GPIO mode support Lakshay Piplani
` (5 preceding siblings ...)
2026-10-16 16:46 ` [RFC PATCH 6/7] i3c: hub: p3h2x4x: configure target ports placed in GPIO mode Lakshay Piplani
@ 2026-10-16 16:46 ` Lakshay Piplani
2026-10-19 6:53 ` Uwe Kleine-König
2026-10-19 14:25 ` Lee Jones
6 siblings, 2 replies; 10+ messages in thread
From: Lakshay Piplani @ 2026-10-16 16:46 UTC (permalink / raw)
To: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl
Cc: vikash.bansal, priyanka.jain, aman.kumarpandey, Lakshay Piplani
Expose the pins of the target ports placed in GPIO mode as a gpio_chip, so
the hub can be used as an IO expander.
Each port contributes two lines, the SCL pin first, so target port N owns
the lines 2 * N and 2 * N + 1, and the line number is also the port bit
position in the direction and level registers. Ports that carry bus traffic
are masked out through init_valid_mask. The port function is taken from the
device tree rather than from the GPIO mode register, because the hub child
that programs that register and this child are independent MFD devices with
no probe ordering guarantee.
Direction is the output enable: clearing it leaves the pin in Hi-Z. An
output drives the level before the buffer is enabled to avoid a glitch, and
the input level register is used for reads in both directions. These
registers are not protected, so the callbacks do not take the shared
register lock. Implement get_multiple and set_multiple so an access to all
lines of the hub costs two register accesses instead of one per line.
Interrupts on input level changes are not supported yet: they arrive as an
I3C in-band interrupt and the edge detection configuration is shared by
groups of four ports.
Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
---
MAINTAINERS | 1 +
drivers/gpio/Kconfig | 11 ++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-p3h2x4x.c | 294 ++++++++++++++++++++++++++++++++++++
drivers/mfd/p3h2840.c | 5 +
5 files changed, 312 insertions(+)
create mode 100644 drivers/gpio/gpio-p3h2x4x.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 33d6e5178955..2578dadaa021 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -19766,6 +19766,7 @@ L: linux-kernel@vger.kernel.org
L: linux-i3c@lists.infradead.org
S: Maintained
F: Documentation/devicetree/bindings/i3c/nxp,p3h2840.yaml
+F: drivers/gpio/gpio-p3h2x4x.c
F: drivers/i3c/hub.c
F: drivers/i3c/hub/*
F: drivers/mfd/p3h2840.c
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index a48586bb8edb..996555c61a53 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -1624,6 +1624,17 @@ config GPIO_PALMAS
Select this option to enable GPIO driver for the TI PALMAS
series chip family.
+config GPIO_P3H2X4X
+ tristate "NXP P3H2X4X I3C HUB GPIO support"
+ depends on MFD_P3H2X4X
+ help
+ Select this option to enable the GPIO driver for the NXP
+ P3H244x/P3H284x I3C hub. Target ports placed in GPIO mode expose
+ their SCL and SDA pins as two general purpose IOs each.
+
+ This driver can also be built as a module. If so, the module will
+ be called gpio-p3h2x4x.
+
config GPIO_PMIC_EIC_SPRD
tristate "Spreadtrum PMIC EIC support"
depends on MFD_SC27XX_PMIC || COMPILE_TEST
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index dc9e6d643b5b..ee6d764310d8 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -138,6 +138,7 @@ obj-$(CONFIG_GPIO_NOVALAKE) += gpio-novalake-events.o
obj-$(CONFIG_GPIO_NPCM_SGPIO) += gpio-npcm-sgpio.o
obj-$(CONFIG_GPIO_OCTEON) += gpio-octeon.o
obj-$(CONFIG_GPIO_OMAP) += gpio-omap.o
+obj-$(CONFIG_GPIO_P3H2X4X) += gpio-p3h2x4x.o
obj-$(CONFIG_GPIO_PALMAS) += gpio-palmas.o
obj-$(CONFIG_GPIO_PCA953X) += gpio-pca953x.o
obj-$(CONFIG_GPIO_PCA9570) += gpio-pca9570.o
diff --git a/drivers/gpio/gpio-p3h2x4x.c b/drivers/gpio/gpio-p3h2x4x.c
new file mode 100644
index 000000000000..5e944965487d
--- /dev/null
+++ b/drivers/gpio/gpio-p3h2x4x.c
@@ -0,0 +1,294 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2026 NXP
+ *
+ * GPIO expander driver for the NXP P3H2x4x I3C hub.
+ *
+ * Each target port that is put in GPIO mode contributes its two bus pins as
+ * generic IOs: SCL becomes GPIOn0 and SDA becomes GPIOn1 (datasheet 8.8.5).
+ * Ports are placed in GPIO mode by the hub driver, which owns the protected
+ * port mode register; this driver only drives the direction and level
+ * registers, which are not protected.
+ */
+#include <linux/bitops.h>
+#include <linux/gpio/driver.h>
+#include <linux/mfd/p3h2840.h>
+#include <linux/mod_devicetable.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+/*
+ * Two GPIOs per target port. The SCL pin is the even line and the SDA pin the
+ * odd one, matching the GPIOn0 and GPIOn1 names used by the datasheet.
+ */
+#define P3H2X4X_GPIO_PER_PORT 2
+#define P3H2X4X_GPIO_PIN_SCL 0
+
+/*
+ * Target port direction and level registers, one bit per target port. These are
+ * not protected, so unlike the GPIO mode enable in the shared MFD header they
+ * are private to this driver. The output enable doubles as the direction: with
+ * it cleared the pin stays in Hi-Z, and the input level register reads back the
+ * pin in both directions.
+ */
+#define P3H2X4X_TP_SCL_OUT_EN 0x54
+#define P3H2X4X_TP_SDA_OUT_EN 0x55
+#define P3H2X4X_TP_SCL_OUT_LEVEL 0x56
+#define P3H2X4X_TP_SDA_OUT_LEVEL 0x57
+#define P3H2X4X_TP_SCL_IN_LEVEL_STS 0x60
+#define P3H2X4X_TP_SDA_IN_LEVEL_STS 0x61
+
+struct p3h2x4x_gpio {
+ struct gpio_chip chip;
+ struct regmap *regmap;
+};
+
+static unsigned int p3h2x4x_gpio_port(unsigned int offset)
+{
+ return offset / P3H2X4X_GPIO_PER_PORT;
+}
+
+static bool p3h2x4x_gpio_is_scl(unsigned int offset)
+{
+ return (offset % P3H2X4X_GPIO_PER_PORT) == P3H2X4X_GPIO_PIN_SCL;
+}
+
+static unsigned int p3h2x4x_gpio_out_en_reg(unsigned int offset)
+{
+ return p3h2x4x_gpio_is_scl(offset) ? P3H2X4X_TP_SCL_OUT_EN :
+ P3H2X4X_TP_SDA_OUT_EN;
+}
+
+static unsigned int p3h2x4x_gpio_out_level_reg(unsigned int offset)
+{
+ return p3h2x4x_gpio_is_scl(offset) ? P3H2X4X_TP_SCL_OUT_LEVEL :
+ P3H2X4X_TP_SDA_OUT_LEVEL;
+}
+
+static unsigned int p3h2x4x_gpio_in_level_reg(unsigned int offset)
+{
+ return p3h2x4x_gpio_is_scl(offset) ? P3H2X4X_TP_SCL_IN_LEVEL_STS :
+ P3H2X4X_TP_SDA_IN_LEVEL_STS;
+}
+
+static int p3h2x4x_gpio_get_direction(struct gpio_chip *chip,
+ unsigned int offset)
+{
+ struct p3h2x4x_gpio *gpio = gpiochip_get_data(chip);
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(gpio->regmap, p3h2x4x_gpio_out_en_reg(offset), &val);
+ if (ret)
+ return ret;
+
+ return (val & BIT(p3h2x4x_gpio_port(offset))) ? GPIO_LINE_DIRECTION_OUT :
+ GPIO_LINE_DIRECTION_IN;
+}
+
+static int p3h2x4x_gpio_direction_input(struct gpio_chip *chip,
+ unsigned int offset)
+{
+ struct p3h2x4x_gpio *gpio = gpiochip_get_data(chip);
+
+ /* Clearing the output enable puts the pin in Hi-Z. */
+ return regmap_clear_bits(gpio->regmap, p3h2x4x_gpio_out_en_reg(offset),
+ BIT(p3h2x4x_gpio_port(offset)));
+}
+
+static int p3h2x4x_gpio_set(struct gpio_chip *chip, unsigned int offset,
+ int value)
+{
+ struct p3h2x4x_gpio *gpio = gpiochip_get_data(chip);
+
+ return regmap_update_bits(gpio->regmap,
+ p3h2x4x_gpio_out_level_reg(offset),
+ BIT(p3h2x4x_gpio_port(offset)),
+ value ? BIT(p3h2x4x_gpio_port(offset)) : 0);
+}
+
+static int p3h2x4x_gpio_direction_output(struct gpio_chip *chip,
+ unsigned int offset, int value)
+{
+ struct p3h2x4x_gpio *gpio = gpiochip_get_data(chip);
+ int ret;
+
+ /* Drive the requested level before enabling the output buffer. */
+ ret = p3h2x4x_gpio_set(chip, offset, value);
+ if (ret)
+ return ret;
+
+ return regmap_set_bits(gpio->regmap, p3h2x4x_gpio_out_en_reg(offset),
+ BIT(p3h2x4x_gpio_port(offset)));
+}
+
+static int p3h2x4x_gpio_get(struct gpio_chip *chip, unsigned int offset)
+{
+ struct p3h2x4x_gpio *gpio = gpiochip_get_data(chip);
+ unsigned int val;
+ int ret;
+
+ /*
+ * The input level status register reflects the pin level in both
+ * directions, so it also reads back what an output is driving.
+ */
+ ret = regmap_read(gpio->regmap, p3h2x4x_gpio_in_level_reg(offset), &val);
+ if (ret)
+ return ret;
+
+ return !!(val & BIT(p3h2x4x_gpio_port(offset)));
+}
+
+static int p3h2x4x_gpio_get_multiple(struct gpio_chip *chip,
+ unsigned long *mask, unsigned long *bits)
+{
+ struct p3h2x4x_gpio *gpio = gpiochip_get_data(chip);
+ unsigned int scl, sda, off;
+ int ret;
+
+ /* One register per pin type covers every port, so two reads suffice. */
+ ret = regmap_read(gpio->regmap, P3H2X4X_TP_SCL_IN_LEVEL_STS, &scl);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(gpio->regmap, P3H2X4X_TP_SDA_IN_LEVEL_STS, &sda);
+ if (ret)
+ return ret;
+
+ for_each_set_bit(off, mask, chip->ngpio) {
+ unsigned int val = p3h2x4x_gpio_is_scl(off) ? scl : sda;
+
+ __assign_bit(off, bits, val & BIT(p3h2x4x_gpio_port(off)));
+ }
+
+ return 0;
+}
+
+static int p3h2x4x_gpio_set_multiple(struct gpio_chip *chip,
+ unsigned long *mask, unsigned long *bits)
+{
+ struct p3h2x4x_gpio *gpio = gpiochip_get_data(chip);
+ unsigned int scl_mask = 0, scl_val = 0;
+ unsigned int sda_mask = 0, sda_val = 0;
+ unsigned int off;
+ int ret;
+
+ /* Coalesce the requested lines into one write per pin type. */
+ for_each_set_bit(off, mask, chip->ngpio) {
+ unsigned int bit = BIT(p3h2x4x_gpio_port(off));
+
+ if (p3h2x4x_gpio_is_scl(off)) {
+ scl_mask |= bit;
+ if (test_bit(off, bits))
+ scl_val |= bit;
+ } else {
+ sda_mask |= bit;
+ if (test_bit(off, bits))
+ sda_val |= bit;
+ }
+ }
+
+ if (scl_mask) {
+ ret = regmap_update_bits(gpio->regmap,
+ P3H2X4X_TP_SCL_OUT_LEVEL,
+ scl_mask, scl_val);
+ if (ret)
+ return ret;
+ }
+
+ if (sda_mask)
+ return regmap_update_bits(gpio->regmap,
+ P3H2X4X_TP_SDA_OUT_LEVEL,
+ sda_mask, sda_val);
+
+ return 0;
+}
+
+/*
+ * Only the ports the device tree describes as "gpio" are usable as GPIOs. The
+ * remaining lines belong to ports running I2C, I3C or SMBus traffic and must
+ * not be handed out to consumers.
+ *
+ * The port function is derived from the device tree rather than from
+ * P3H2X4X_TP_GPIO_MODE_EN, because the hub child that programs that register
+ * and this child are independent MFD devices with no probe ordering
+ * guarantee. Reading the register here could therefore observe its reset
+ * value and mask off every line.
+ */
+static int p3h2x4x_gpio_init_valid_mask(struct gpio_chip *chip,
+ unsigned long *valid_mask,
+ unsigned int ngpios)
+{
+ unsigned int num_ports = ngpios / P3H2X4X_GPIO_PER_PORT;
+ struct device_node *np = dev_of_node(chip->parent);
+
+ bitmap_zero(valid_mask, ngpios);
+
+ for_each_available_child_of_node_scoped(np, child) {
+ u32 tp;
+
+ if (!of_node_name_eq(child, "gpio"))
+ continue;
+
+ if (of_property_read_u32(child, "reg", &tp) || tp >= num_ports)
+ continue;
+
+ bitmap_set(valid_mask, tp * P3H2X4X_GPIO_PER_PORT,
+ P3H2X4X_GPIO_PER_PORT);
+ }
+
+ return 0;
+}
+
+static int p3h2x4x_gpio_probe(struct platform_device *pdev)
+{
+ struct p3h2x4x *p3h2x4x = dev_get_drvdata(pdev->dev.parent);
+ struct device *dev = &pdev->dev;
+ struct p3h2x4x_gpio *gpio;
+
+ gpio = devm_kzalloc(dev, sizeof(*gpio), GFP_KERNEL);
+ if (!gpio)
+ return -ENOMEM;
+
+ gpio->regmap = p3h2x4x->regmap;
+
+ device_set_of_node_from_dev(dev, dev->parent);
+
+ gpio->chip.label = dev_name(dev);
+ gpio->chip.parent = dev;
+ gpio->chip.owner = THIS_MODULE;
+ gpio->chip.base = -1;
+ gpio->chip.ngpio = p3h2x4x->num_target_ports * P3H2X4X_GPIO_PER_PORT;
+ gpio->chip.can_sleep = true;
+ gpio->chip.get_direction = p3h2x4x_gpio_get_direction;
+ gpio->chip.direction_input = p3h2x4x_gpio_direction_input;
+ gpio->chip.direction_output = p3h2x4x_gpio_direction_output;
+ gpio->chip.get = p3h2x4x_gpio_get;
+ gpio->chip.get_multiple = p3h2x4x_gpio_get_multiple;
+ gpio->chip.set = p3h2x4x_gpio_set;
+ gpio->chip.set_multiple = p3h2x4x_gpio_set_multiple;
+ gpio->chip.init_valid_mask = p3h2x4x_gpio_init_valid_mask;
+
+ return devm_gpiochip_add_data(dev, &gpio->chip, gpio);
+}
+
+static const struct platform_device_id p3h2x4x_gpio_id[] = {
+ { .name = "p3h2x4x-gpio" },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, p3h2x4x_gpio_id);
+
+static struct platform_driver p3h2x4x_gpio_driver = {
+ .driver = {
+ .name = "p3h2x4x-gpio",
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ },
+ .probe = p3h2x4x_gpio_probe,
+ .id_table = p3h2x4x_gpio_id,
+};
+module_platform_driver(p3h2x4x_gpio_driver);
+
+MODULE_AUTHOR("Lakshay Piplani <lakshay.piplani@nxp.com>");
+MODULE_DESCRIPTION("NXP P3H2X4X I3C HUB GPIO driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/mfd/p3h2840.c b/drivers/mfd/p3h2840.c
index c48984482148..87c0329e6272 100644
--- a/drivers/mfd/p3h2840.c
+++ b/drivers/mfd/p3h2840.c
@@ -15,9 +15,14 @@
#include <linux/mfd/p3h2840.h>
#include <linux/regmap.h>
+/*
+ * The hub cell owns the protected target port mode registers, including the
+ * GPIO mode enable, so it is listed before the GPIO cell.
+ */
static const struct mfd_cell p3h2x4x_devs[] = {
MFD_CELL_NAME("p3h2x4x-regulator"),
MFD_CELL_NAME("p3h2x4x-i3c-hub"),
+ MFD_CELL_NAME("p3h2x4x-gpio"),
};
static const struct regmap_config p3h2x4x_regmap_config = {
--
2.25.1
^ permalink raw reply [flat|nested] 10+ messages in thread* Re: [RFC PATCH 7/7] gpio: p3h2x4x: add GPIO expander for the NXP P3H2x4x I3C hub
2026-10-16 16:46 ` [RFC PATCH 7/7] gpio: p3h2x4x: add GPIO expander for the NXP P3H2x4x I3C hub Lakshay Piplani
@ 2026-10-19 6:53 ` Uwe Kleine-König
2026-10-19 14:25 ` Lee Jones
1 sibling, 0 replies; 10+ messages in thread
From: Uwe Kleine-König @ 2026-10-19 6:53 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, lee, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl, vikash.bansal, priyanka.jain,
aman.kumarpandey
[-- Attachment #1: Type: text/plain, Size: 269 bytes --]
On Fri, Oct 16, 2026 at 10:16:25PM +0530, Lakshay Piplani wrote:
> +#include <linux/mod_devicetable.h>
Please drop this header, <linux/platform_device.h> is enough to provide
platform_device_id without pulling in a plethora of unneeded
dependencies.
Best regards
Uwe
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 7/7] gpio: p3h2x4x: add GPIO expander for the NXP P3H2x4x I3C hub
2026-10-16 16:46 ` [RFC PATCH 7/7] gpio: p3h2x4x: add GPIO expander for the NXP P3H2x4x I3C hub Lakshay Piplani
2026-10-19 6:53 ` Uwe Kleine-König
@ 2026-10-19 14:25 ` Lee Jones
1 sibling, 0 replies; 10+ messages in thread
From: Lee Jones @ 2026-10-19 14:25 UTC (permalink / raw)
To: Lakshay Piplani
Cc: linux-kernel, linux-i3c, alexandre.belloni, krzk+dt, robh,
conor+dt, devicetree, broonie, mfd, Frank.Li, lgirdwood,
linux-gpio, linusw, brgl, vikash.bansal, priyanka.jain,
aman.kumarpandey
On Fri, 16 Oct 2026, Lakshay Piplani wrote:
> Expose the pins of the target ports placed in GPIO mode as a gpio_chip, so
> the hub can be used as an IO expander.
>
> Each port contributes two lines, the SCL pin first, so target port N owns
> the lines 2 * N and 2 * N + 1, and the line number is also the port bit
> position in the direction and level registers. Ports that carry bus traffic
> are masked out through init_valid_mask. The port function is taken from the
> device tree rather than from the GPIO mode register, because the hub child
> that programs that register and this child are independent MFD devices with
> no probe ordering guarantee.
>
> Direction is the output enable: clearing it leaves the pin in Hi-Z. An
> output drives the level before the buffer is enabled to avoid a glitch, and
> the input level register is used for reads in both directions. These
> registers are not protected, so the callbacks do not take the shared
> register lock. Implement get_multiple and set_multiple so an access to all
> lines of the hub costs two register accesses instead of one per line.
>
> Interrupts on input level changes are not supported yet: they arrive as an
> I3C in-band interrupt and the edge detection configuration is shared by
> groups of four ports.
>
> Signed-off-by: Lakshay Piplani <lakshay.piplani@nxp.com>
> Signed-off-by: Aman Kumar Pandey <aman.kumarpandey@nxp.com>
> Signed-off-by: Vikash Bansal <vikash.bansal@nxp.com>
> ---
> MAINTAINERS | 1 +
> drivers/gpio/Kconfig | 11 ++
> drivers/gpio/Makefile | 1 +
> drivers/gpio/gpio-p3h2x4x.c | 294 ++++++++++++++++++++++++++++++++++++
> drivers/mfd/p3h2840.c | 5 +
This needs to be a separate patch.
> 5 files changed, 312 insertions(+)
> create mode 100644 drivers/gpio/gpio-p3h2x4x.c
--
Lee Jones
^ permalink raw reply [flat|nested] 10+ messages in thread