* [RFC PATCH 0/4] Add support for Realtek RTS490x I3C hub
@ 2026-09-20 10:04 zain_zhou
2026-09-20 10:04 ` [RFC PATCH 1/4] dt-bindings: i3c: Add Realtek RTS490x I3C hub support zain_zhou
` (3 more replies)
0 siblings, 4 replies; 10+ messages in thread
From: zain_zhou @ 2026-09-20 10:04 UTC (permalink / raw)
To: Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Mark Brown,
Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, Wei Wang, Yin Zhou
From: Yin Zhou <zain_zhou@realsil.com.cn>
Hi all,
This series adds support for the Realtek RTS490x family of I3C hubs.
The devices provide four or eight downstream target ports, which can be
configured as I3C buses or SMBus controller/target interfaces. They also
provide four programmable LDO outputs for the controller and target-port
power domains.
This RFC is based on the NXP P3H2x4x generic I3C hub framework series v17:
[PATCH v17 0/8] Add support for NXP P3H2x4x I3C hub driver
The NXP dependency series is not included below and must be applied first.
It applies on top of current Linux mainline; the base commit used to build
and test this RFC is:
940de590b839f71d6dc846160534bf202401b8b7
The dependency status and the final generic-hub API shape are an explicit
part of this RFC. If the generic framework is revised, this series will be
rebased accordingly. NXP's later v19 series will be evaluated for a future
revision of this RTS490x series and is not being used as the basis for this
RFC v1.
Patch overview:
1. dt-bindings: i3c: Add Realtek RTS490x I3C hub support
Add the RTS490x device-tree binding and register the binding with
MAINTAINERS.
2. mfd: Add driver for Realtek RTS490x I3C hub
Add the I3C-only MFD parent, shared regmap, manufacturer matching,
variant detection, protected-register lock, and child devices.
3. regulator: rts490x: Add driver for on-die regulators
Expose the four programmable LDOs through the regulator framework.
Only regulator nodes explicitly present and enabled in DT are
registered; omitted LDOs retain their existing hardware state.
4. i3c: hub: Add Realtek RTS490x I3C hub support
Add downstream I3C ports through the generic I3C hub framework and
SMBus ports through Linux I2C adapters. SMBus controller completion
and target receive handling use the hub self-IBI.
The first RFC intentionally has a limited scope:
- The hub must be connected to the host through I3C.
- GPIO and GPIO IRQ support are deferred to a follow-up series.
- SMBus controller and target paths use IBI; polling fallback is not
included.
- Legacy I2C device discovery behind I3C-mode ports follows the existing
behavior of the generic I3C master framework and has not been
specifically validated on this hardware in this RFC.
Testing:
- checkpatch.pl --strict, 0 errors/0 warnings across the series
- dt_binding_check for the RTS490x binding
- Cross-build validated for the MFD, regulator, and hub composite objects
- git diff --check across the series
- I3C and SMBus downstream ports have been functionally verified on
target hardware.
Regards,
Yin Zhou
Yin Zhou (4):
dt-bindings: i3c: Add Realtek RTS490x I3C hub support
mfd: Add driver for Realtek RTS490x I3C hub
regulator: rts490x: Add driver for on-die regulators
i3c: hub: Add Realtek RTS490x I3C hub support
.../bindings/i3c/realtek,rts490x.yaml | 266 ++++++
MAINTAINERS | 11 +
drivers/i3c/hub/Kconfig | 11 +
drivers/i3c/hub/Makefile | 3 +
drivers/i3c/hub/rts490x-i3c-hub-core.c | 444 +++++++++
drivers/i3c/hub/rts490x-i3c-hub-i3c.c | 150 ++++
drivers/i3c/hub/rts490x-i3c-hub-smbus.c | 839 ++++++++++++++++++
drivers/i3c/hub/rts490x-i3c-hub.h | 150 ++++
drivers/mfd/Kconfig | 11 +
drivers/mfd/Makefile | 1 +
drivers/mfd/rts490x-core.c | 131 +++
drivers/regulator/Kconfig | 9 +
drivers/regulator/Makefile | 1 +
drivers/regulator/rts490x-regulator.c | 284 ++++++
include/linux/mfd/rts490x.h | 53 ++
15 files changed, 2364 insertions(+)
create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-core.c
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-i3c.c
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-smbus.c
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub.h
create mode 100644 drivers/mfd/rts490x-core.c
create mode 100644 drivers/regulator/rts490x-regulator.c
create mode 100644 include/linux/mfd/rts490x.h
--
2.34.1
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 1/4] dt-bindings: i3c: Add Realtek RTS490x I3C hub support
2026-09-20 10:04 [RFC PATCH 0/4] Add support for Realtek RTS490x I3C hub zain_zhou
@ 2026-09-20 10:04 ` zain_zhou
2026-09-20 10:04 ` [RFC PATCH 2/4] mfd: Add driver for Realtek RTS490x I3C hub zain_zhou
` (2 subsequent siblings)
3 siblings, 0 replies; 10+ messages in thread
From: zain_zhou @ 2026-09-20 10:04 UTC (permalink / raw)
To: Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Mark Brown,
Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, Wei Wang, Yin Zhou
From: Yin Zhou <zain_zhou@realsil.com.cn>
Add the device tree binding for the Realtek RTS490x family of I3C
hubs. The hubs provide four or eight downstream target ports that can
operate as I3C buses or SMBus controller and target interfaces, along
with programmable LDO outputs for the controller and target-port
power domains.
Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
---
.../bindings/i3c/realtek,rts490x.yaml | 266 ++++++++++++++++++
MAINTAINERS | 7 +
2 files changed, 273 insertions(+)
create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
new file mode 100644
index 000000000000..92d5d73db435
--- /dev/null
+++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
@@ -0,0 +1,266 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+# Copyright 2025-2026 Realtek Semiconductor Corp.
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/i3c/realtek,rts490x.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Realtek RTS490x I3C hub
+
+maintainers:
+ - Yin Zhou <zain_zhou@realsil.com.cn>
+
+description: |
+ The Realtek RTS490x family provides four or eight downstream target ports.
+ The hub is an I3C target and is accessed through an upstream I3C controller.
+ Each target port can operate as an I3C bus or as an SMBus controller and
+ target interface. The hub also provides four programmable LDO outputs for
+ its controller and target-port power domains.
+
+properties:
+ compatible:
+ enum:
+ - realtek,rts4900
+ - realtek,rts4901
+ - realtek,rts4902
+ - realtek,rts4903
+ - realtek,rts4904
+ - realtek,rts4906
+
+ reg:
+ maxItems: 1
+
+ '#address-cells':
+ const: 1
+
+ '#size-cells':
+ const: 0
+
+ assigned-address:
+ maximum: 0x7f
+
+ realtek,tp0145-pullup-ohms:
+ description:
+ Pull-up resistance for target ports 0, 1, 4 and 5, in ohms. If omitted,
+ the driver retains the current hardware setting.
+ enum: [250, 500, 1000, 2000]
+
+ realtek,tp2367-pullup-ohms:
+ description:
+ Pull-up resistance for target ports 2, 3, 6 and 7, in ohms. If omitted,
+ the driver retains the current hardware setting.
+ enum: [250, 500, 1000, 2000]
+
+ realtek,cp0-io-strength-ohms:
+ description:
+ IO drive strength for controller port 0, in ohms. If omitted, the driver
+ retains the current hardware setting.
+ enum: [20, 30, 40, 50]
+
+ realtek,cp1-io-strength-ohms:
+ description:
+ IO drive strength for controller port 1, in ohms. If omitted, the driver
+ retains the current hardware setting.
+ enum: [20, 30, 40, 50]
+
+ realtek,tp0145-io-strength-ohms:
+ description:
+ IO drive strength for target ports 0, 1, 4 and 5, in ohms. If omitted,
+ the driver retains the current hardware setting.
+ enum: [20, 30, 40, 50]
+
+ realtek,tp2367-io-strength-ohms:
+ description:
+ IO drive strength for target ports 2, 3, 6 and 7, in ohms. If omitted,
+ the driver retains the current hardware setting.
+ enum: [20, 30, 40, 50]
+
+ vcc-cp0-supply:
+ description:
+ Optional external power supply for controller port 0. If omitted, the
+ driver does not request or enable this supply.
+
+ vcc-cp1-supply:
+ description:
+ Optional external power supply for controller port 1. If omitted, the
+ driver does not request or enable this supply.
+
+ vcc-tp0145-supply:
+ description:
+ Optional external power supply for target ports 0, 1, 4 and 5. If
+ omitted, the driver does not request or enable this supply.
+
+ vcc-tp2367-supply:
+ description:
+ Optional external power supply for target ports 2, 3, 6 and 7. If
+ omitted, the driver does not request or enable this supply.
+
+ regulators:
+ type: object
+ description:
+ Optional container for the hub's programmable LDOs. Only enabled LDO
+ child nodes are registered by the driver. An omitted or disabled LDO
+ child is not registered and its hardware state is left unchanged.
+ additionalProperties: false
+
+ properties:
+ ldo-cp0:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+ ldo-cp1:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+ ldo-tp0145:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+ ldo-tp2367:
+ type: object
+ $ref: /schemas/regulator/regulator.yaml#
+ unevaluatedProperties: false
+
+required:
+ - compatible
+ - reg
+ - '#address-cells'
+ - '#size-cells'
+
+patternProperties:
+ '^i3c@[0-7]$':
+ type: object
+ $ref: /schemas/i3c/i3c.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ description: Target-port number.
+ items:
+ - minimum: 0
+ maximum: 7
+
+ realtek,pullup-enable:
+ type: boolean
+ description:
+ Enable the on-chip pull-up for this target port. If omitted, the
+ on-chip pull-up is disabled.
+
+ required:
+ - reg
+
+ '^smbus@[0-7]$':
+ type: object
+ $ref: /schemas/i2c/i2c-controller.yaml#
+ unevaluatedProperties: false
+
+ properties:
+ reg:
+ description: Target-port number.
+ items:
+ - minimum: 0
+ maximum: 7
+
+ realtek,pullup-enable:
+ type: boolean
+ description:
+ Enable the on-chip pull-up for this target port. If omitted, the
+ on-chip pull-up is disabled.
+
+ clock-frequency:
+ description:
+ SMBus controller-agent clock frequency for this target port, in Hz.
+ If omitted, the driver uses 400 kHz.
+ enum: [100000, 200000, 400000, 1000000]
+ default: 400000
+
+ required:
+ - reg
+
+allOf:
+ - if:
+ properties:
+ compatible:
+ contains:
+ enum:
+ - realtek,rts4900
+ - realtek,rts4901
+ - realtek,rts4904
+ then:
+ patternProperties:
+ '^i3c@[0-3]$':
+ properties:
+ reg:
+ items:
+ - maximum: 3
+ '^smbus@[0-3]$':
+ properties:
+ reg:
+ items:
+ - maximum: 3
+ '^i3c@[4-7]$': false
+ '^smbus@[4-7]$': false
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ i3c {
+ #address-cells = <3>;
+ #size-cells = <0>;
+
+ hub@70,4ba00000000 {
+ compatible = "realtek,rts4900";
+ reg = <0x70 0x4ba 0x00000000>;
+ assigned-address = <0x70>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ realtek,tp0145-pullup-ohms = <1000>;
+ realtek,cp0-io-strength-ohms = <20>;
+ realtek,cp1-io-strength-ohms = <20>;
+ realtek,tp0145-io-strength-ohms = <30>;
+
+ vcc-cp0-supply = <&ldo_cp0>;
+ vcc-cp1-supply = <&ldo_cp1>;
+ vcc-tp0145-supply = <&ldo_tp0145>;
+
+ regulators {
+ ldo_cp0: ldo-cp0 {
+ regulator-name = "ldo-cp0";
+ regulator-min-microvolt = <1000000>;
+ regulator-max-microvolt = <1000000>;
+ };
+
+ ldo_cp1: ldo-cp1 {
+ regulator-name = "ldo-cp1";
+ regulator-min-microvolt = <1000000>;
+ regulator-max-microvolt = <1000000>;
+ };
+
+ ldo_tp0145: ldo-tp0145 {
+ regulator-name = "ldo-tp0145";
+ regulator-min-microvolt = <1800000>;
+ regulator-max-microvolt = <1800000>;
+ };
+ };
+
+ i3c@0 {
+ reg = <0>;
+ #address-cells = <3>;
+ #size-cells = <0>;
+ realtek,pullup-enable;
+ };
+
+ smbus@1 {
+ reg = <1>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ clock-frequency = <400000>;
+ realtek,pullup-enable;
+ };
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index f76359fdebbf..a2c171a28cd2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22992,6 +22992,13 @@ S: Maintained
F: include/sound/rt*.h
F: sound/soc/codecs/rt*
+REALTEK RTS490X I3C HUB DRIVER
+M: Yin Zhou <zain_zhou@realsil.com.cn>
+L: linux-i3c@lists.infradead.org
+L: linux-kernel@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
+
REALTEK OTTO WATCHDOG
M: Sander Vanheule <sander@svanheule.net>
L: linux-watchdog@vger.kernel.org
--
2.34.1
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 2/4] mfd: Add driver for Realtek RTS490x I3C hub
2026-09-20 10:04 [RFC PATCH 0/4] Add support for Realtek RTS490x I3C hub zain_zhou
2026-09-20 10:04 ` [RFC PATCH 1/4] dt-bindings: i3c: Add Realtek RTS490x I3C hub support zain_zhou
@ 2026-09-20 10:04 ` zain_zhou
2026-09-20 10:05 ` [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators zain_zhou
2026-09-20 10:05 ` [RFC PATCH 4/4] i3c: hub: Add Realtek RTS490x I3C hub support zain_zhou
3 siblings, 0 replies; 10+ messages in thread
From: zain_zhou @ 2026-09-20 10:04 UTC (permalink / raw)
To: Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Mark Brown,
Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, Wei Wang, Yin Zhou
From: Yin Zhou <zain_zhou@realsil.com.cn>
Add the MFD core driver for the Realtek RTS490x family of I3C hubs.
The driver initializes the shared register map, identifies the hub
variant, and registers the regulator and I3C hub child devices.
Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
---
MAINTAINERS | 2 +
drivers/mfd/Kconfig | 11 +++
drivers/mfd/Makefile | 1 +
drivers/mfd/rts490x-core.c | 131 ++++++++++++++++++++++++++++++++++++
include/linux/mfd/rts490x.h | 53 +++++++++++++++
5 files changed, 198 insertions(+)
create mode 100644 drivers/mfd/rts490x-core.c
create mode 100644 include/linux/mfd/rts490x.h
diff --git a/MAINTAINERS b/MAINTAINERS
index a2c171a28cd2..659f553420eb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22998,6 +22998,8 @@ L: linux-i3c@lists.infradead.org
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
+F: drivers/mfd/rts490x-core.c
+F: include/linux/mfd/rts490x.h
REALTEK OTTO WATCHDOG
M: Sander Vanheule <sander@svanheule.net>
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index ee809b0c1b8f..6da8967de1a2 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -630,6 +630,17 @@ config MFD_P3H2X4X
This driver provides support for I3C hub and regulator, each subdriver
can be enabled independently depending on the required functionality.
+config MFD_RTS490X
+ tristate "Realtek RTS490x I3C hub"
+ depends on I3C
+ select MFD_CORE
+ select REGMAP_I3C
+ help
+ Enable support for the Realtek RTS490x family of I3C hub devices.
+ This driver provides the shared register map and parent state used by
+ the I3C hub and regulator child drivers. The hub is connected to the
+ host through I3C.
+
config MFD_PF1550
tristate "NXP PF1550 PMIC Support"
depends on I2C=y && OF
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index ec4e093f3540..97988277bee9 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -124,6 +124,7 @@ obj-$(CONFIG_MFD_MC13XXX_I2C) += mc13xxx-i2c.o
obj-$(CONFIG_MFD_P3H2X4X) += p3h2840.o
obj-$(CONFIG_MFD_PF1550) += pf1550.o
+obj-$(CONFIG_MFD_RTS490X) += rts490x-core.o
obj-$(CONFIG_MFD_NCT6694) += nct6694.o
diff --git a/drivers/mfd/rts490x-core.c b/drivers/mfd/rts490x-core.c
new file mode 100644
index 000000000000..c10718397167
--- /dev/null
+++ b/drivers/mfd/rts490x-core.c
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
+
+#include <linux/bitfield.h>
+#include <linux/i3c/device.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/rts490x.h>
+#include <linux/regmap.h>
+
+struct rts490x_device_info {
+ const char *model;
+ u16 part_id;
+ u8 num_target_ports;
+};
+
+static const struct rts490x_device_info rts490x_devices[] = {
+ { "RTS4900", 0x4000, 4 },
+ { "RTS4901", 0x4100, 4 },
+ { "RTS4902", 0x8000, 8 },
+ { "RTS4903", 0x8100, 8 },
+ { "RTS4904", 0x4001, 4 },
+ { "RTS4906", 0x8001, 8 },
+};
+
+static const struct mfd_cell rts490x_cells[] = {
+ MFD_CELL_NAME("rts490x-regulator"),
+ MFD_CELL_NAME("rts490x-i3c-hub"),
+};
+
+static const struct regmap_config rts490x_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0xff,
+};
+
+static int rts490x_read_device_info(struct device *dev, struct rts490x *rts490x)
+{
+ unsigned int value;
+ u16 part_id;
+ int ret;
+ int i;
+
+ ret = regmap_read(rts490x->regmap, RTS490X_DEV_INFO_0, &value);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to read device information\n");
+
+ part_id = value << 8;
+
+ ret = regmap_read(rts490x->regmap, RTS490X_DEV_REV, &value);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to read device revision\n");
+
+ part_id |= FIELD_GET(RTS490X_DEV_REV_LDO_MASK, value);
+
+ for (i = 0; i < ARRAY_SIZE(rts490x_devices); i++) {
+ if (rts490x_devices[i].part_id != part_id)
+ continue;
+
+ rts490x->model = rts490x_devices[i].model;
+ rts490x->part_id = part_id;
+ rts490x->num_target_ports = rts490x_devices[i].num_target_ports;
+ return 0;
+ }
+
+ return dev_err_probe(dev, -ENODEV,
+ "unsupported RTS490x device ID 0x%04x\n", part_id);
+}
+
+static int rts490x_probe(struct i3c_device *i3cdev)
+{
+ struct device *dev = i3cdev_to_dev(i3cdev);
+ struct i3c_device_info info;
+ struct rts490x *rts490x;
+ int ret;
+
+ i3c_device_get_info(i3cdev, &info);
+ if (I3C_PID_MANUF_ID(info.pid) != RTS490X_MANUFACTURER_ID)
+ return -ENODEV;
+
+ rts490x = devm_kzalloc(dev, sizeof(*rts490x), GFP_KERNEL);
+ if (!rts490x)
+ return -ENOMEM;
+
+ ret = devm_mutex_init(dev, &rts490x->protected_reg_lock);
+ if (ret)
+ return ret;
+
+ rts490x->regmap = devm_regmap_init_i3c(i3cdev, &rts490x_regmap_config);
+ if (IS_ERR(rts490x->regmap))
+ return dev_err_probe(dev, PTR_ERR(rts490x->regmap),
+ "failed to initialize regmap\n");
+
+ rts490x->i3cdev = i3cdev;
+ i3cdev_set_drvdata(i3cdev, rts490x);
+
+ ret = rts490x_read_device_info(dev, rts490x);
+ if (ret)
+ return ret;
+
+ ret = devm_mfd_add_devices(dev, PLATFORM_DEVID_AUTO,
+ rts490x_cells, ARRAY_SIZE(rts490x_cells),
+ NULL, 0, NULL);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to add child devices\n");
+
+ dev_info(dev, "%s I3C hub with %u target ports\n",
+ rts490x->model, rts490x->num_target_ports);
+
+ return 0;
+}
+
+static const struct i3c_device_id rts490x_i3c_ids[] = {
+ {
+ .match_flags = I3C_MATCH_DCR | I3C_MATCH_MANUF,
+ .dcr = I3C_DCR_HUB,
+ .manuf_id = RTS490X_MANUFACTURER_ID,
+ },
+ { }
+};
+MODULE_DEVICE_TABLE(i3c, rts490x_i3c_ids);
+
+static struct i3c_driver rts490x_driver = {
+ .driver.name = "rts490x",
+ .probe = rts490x_probe,
+ .id_table = rts490x_i3c_ids,
+};
+module_i3c_driver(rts490x_driver);
+
+MODULE_AUTHOR("Yin Zhou <zain_zhou@realsil.com.cn>");
+MODULE_DESCRIPTION("Realtek RTS490x I3C hub MFD driver");
+MODULE_LICENSE("GPL");
diff --git a/include/linux/mfd/rts490x.h b/include/linux/mfd/rts490x.h
new file mode 100644
index 000000000000..2af77d22dab9
--- /dev/null
+++ b/include/linux/mfd/rts490x.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
+#ifndef _LINUX_MFD_RTS490X_H
+#define _LINUX_MFD_RTS490X_H
+
+#include <linux/bits.h>
+#include <linux/mutex.h>
+#include <linux/types.h>
+
+#define RTS490X_MAX_TARGET_PORTS 8
+
+/* Device information registers shared by the MFD children. */
+#define RTS490X_DEV_INFO_0 0x00
+#define RTS490X_DEV_REV 0x0b
+#define RTS490X_DEV_REV_LDO_MASK GENMASK(7, 6)
+
+/* Regulator registers shared with the regulator child. */
+#define RTS490X_LDO_CONF 0x16
+#define RTS490X_LDO_AND_PULLUP_CONF 0x19
+
+/* Protected-register access shared by the MFD children. */
+#define RTS490X_PROTECTION_CODE 0x10
+#define RTS490X_REGISTERS_LOCK_CODE 0x00
+#define RTS490X_REGISTERS_UNLOCK_CODE 0x69
+
+#define RTS490X_MANUFACTURER_ID 0x025d
+
+struct i3c_device;
+struct regmap;
+struct rts490x_hub;
+
+/**
+ * struct rts490x - shared state for RTS490x MFD children
+ * @i3cdev: upstream I3C device
+ * @regmap: register map shared by all child drivers
+ * @protected_reg_lock: serializes protected-register transactions
+ * @model: detected chip model
+ * @part_id: hardware part identifier
+ * @num_target_ports: number of implemented target ports
+ * @hub: hub child state used by the hub-device IBI handler
+ */
+struct rts490x {
+ struct i3c_device *i3cdev;
+ struct regmap *regmap;
+ /* Serializes protected-register transactions across child drivers. */
+ struct mutex protected_reg_lock;
+ const char *model;
+ u16 part_id;
+ u8 num_target_ports;
+ struct rts490x_hub *hub;
+};
+
+#endif /* _LINUX_MFD_RTS490X_H */
--
2.34.1
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators
2026-09-20 10:04 [RFC PATCH 0/4] Add support for Realtek RTS490x I3C hub zain_zhou
2026-09-20 10:04 ` [RFC PATCH 1/4] dt-bindings: i3c: Add Realtek RTS490x I3C hub support zain_zhou
2026-09-20 10:04 ` [RFC PATCH 2/4] mfd: Add driver for Realtek RTS490x I3C hub zain_zhou
@ 2026-09-20 10:05 ` zain_zhou
2026-09-20 13:05 ` Krzysztof Kozlowski
2026-09-21 10:23 ` Mark Brown
2026-09-20 10:05 ` [RFC PATCH 4/4] i3c: hub: Add Realtek RTS490x I3C hub support zain_zhou
3 siblings, 2 replies; 10+ messages in thread
From: zain_zhou @ 2026-09-20 10:05 UTC (permalink / raw)
To: Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Mark Brown,
Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, Wei Wang, Yin Zhou
From: Yin Zhou <zain_zhou@realsil.com.cn>
Add regulator support for the four programmable LDO outputs in the
Realtek RTS490x I3C hub. The driver exposes the LDO voltage and enable
controls through the regulator framework while preserving protected
register state across updates.
Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
---
MAINTAINERS | 1 +
drivers/regulator/Kconfig | 9 +
drivers/regulator/Makefile | 1 +
drivers/regulator/rts490x-regulator.c | 284 ++++++++++++++++++++++++++
4 files changed, 295 insertions(+)
create mode 100644 drivers/regulator/rts490x-regulator.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 659f553420eb..6e7cb4ca92f6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22999,6 +22999,7 @@ L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
F: drivers/mfd/rts490x-core.c
+F: drivers/regulator/rts490x-regulator.c
F: include/linux/mfd/rts490x.h
REALTEK OTTO WATCHDOG
diff --git a/drivers/regulator/Kconfig b/drivers/regulator/Kconfig
index 00b152d95d42..c65c319ee676 100644
--- a/drivers/regulator/Kconfig
+++ b/drivers/regulator/Kconfig
@@ -1030,6 +1030,15 @@ config REGULATOR_P3H2X4X
Say M here if you want to include support for this regulator as
a module. The module will be named "p3h2840_i3c_hub_regulator".
+config REGULATOR_RTS490X
+ tristate "Realtek RTS490x regulator support"
+ depends on MFD_RTS490X
+ help
+ Enable support for the four programmable LDO outputs provided by
+ Realtek RTS490x I3C hub devices. These outputs supply the controller
+ ports and the two target-port groups. Their voltage and enable state
+ are managed through the regulator framework.
+
config REGULATOR_PALMAS
tristate "TI Palmas PMIC Regulators"
depends on MFD_PALMAS
diff --git a/drivers/regulator/Makefile b/drivers/regulator/Makefile
index a07702a49be4..57a2eec77b53 100644
--- a/drivers/regulator/Makefile
+++ b/drivers/regulator/Makefile
@@ -130,6 +130,7 @@ obj-$(CONFIG_REGULATOR_QCOM_SPMI) += qcom_spmi-regulator.o
obj-$(CONFIG_REGULATOR_QCOM_USB_VBUS) += qcom_usb_vbus-regulator.o
obj-$(CONFIG_REGULATOR_P3H2X4X) += p3h2840_i3c_hub_regulator.o
obj-$(CONFIG_REGULATOR_PALMAS) += palmas-regulator.o
+obj-$(CONFIG_REGULATOR_RTS490X) += rts490x-regulator.o
obj-$(CONFIG_REGULATOR_PCA9450) += pca9450-regulator.o
obj-$(CONFIG_REGULATOR_PF0900) += pf0900-regulator.o
obj-$(CONFIG_REGULATOR_PF9453) += pf9453-regulator.o
diff --git a/drivers/regulator/rts490x-regulator.c b/drivers/regulator/rts490x-regulator.c
new file mode 100644
index 000000000000..dda5f2b1bb95
--- /dev/null
+++ b/drivers/regulator/rts490x-regulator.c
@@ -0,0 +1,284 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
+
+#include <linux/cleanup.h>
+#include <linux/mfd/rts490x.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/driver.h>
+
+#define RTS490X_LDO_COUNT 4
+
+#define RTS490X_CP0_LDO_ENABLE BIT(0)
+#define RTS490X_CP1_LDO_ENABLE BIT(1)
+#define RTS490X_TP0145_LDO_ENABLE BIT(2)
+#define RTS490X_TP2367_LDO_ENABLE BIT(3)
+
+#define RTS490X_CP0_LDO_VOLTAGE_MASK GENMASK(1, 0)
+#define RTS490X_CP1_LDO_VOLTAGE_MASK GENMASK(3, 2)
+#define RTS490X_TP0145_LDO_VOLTAGE_MASK GENMASK(5, 4)
+#define RTS490X_TP2367_LDO_VOLTAGE_MASK GENMASK(7, 6)
+
+struct rts490x_regulator {
+ struct rts490x *rts490x;
+};
+
+struct rts490x_reg_state {
+ unsigned int original;
+ bool restore;
+};
+
+static void rts490x_regulator_lock(struct regulator_dev *rdev)
+{
+ struct rts490x_regulator *priv = rdev_get_drvdata(rdev);
+
+ mutex_lock(&priv->rts490x->protected_reg_lock);
+}
+
+static void rts490x_regulator_unlock(struct regulator_dev *rdev)
+{
+ struct rts490x_regulator *priv = rdev_get_drvdata(rdev);
+
+ mutex_unlock(&priv->rts490x->protected_reg_lock);
+}
+
+DEFINE_LOCK_GUARD_1(rts490x_regulator, struct regulator_dev,
+ rts490x_regulator_lock(_T->lock),
+ rts490x_regulator_unlock(_T->lock));
+
+static int rts490x_regulator_unprotect(struct regulator_dev *rdev,
+ struct rts490x_reg_state *state)
+{
+ int ret;
+
+ state->restore = false;
+
+ ret = regmap_read(rdev->regmap, RTS490X_PROTECTION_CODE,
+ &state->original);
+ if (ret)
+ return ret;
+
+ if (state->original == RTS490X_REGISTERS_UNLOCK_CODE)
+ return 0;
+
+ ret = regmap_write(rdev->regmap, RTS490X_PROTECTION_CODE,
+ RTS490X_REGISTERS_UNLOCK_CODE);
+ if (!ret)
+ state->restore = true;
+
+ return ret;
+}
+
+static int rts490x_regulator_protect(struct regulator_dev *rdev,
+ const struct rts490x_reg_state *state)
+{
+ if (!state->restore)
+ return 0;
+
+ return regmap_write(rdev->regmap, RTS490X_PROTECTION_CODE,
+ state->original);
+}
+
+static int rts490x_regulator_enable(struct regulator_dev *rdev)
+{
+ struct rts490x_reg_state state;
+ int ret, ret2;
+
+ guard(rts490x_regulator)(rdev);
+
+ ret = rts490x_regulator_unprotect(rdev, &state);
+ if (ret)
+ return ret;
+
+ ret = regulator_enable_regmap(rdev);
+ ret2 = rts490x_regulator_protect(rdev, &state);
+
+ return ret ?: ret2;
+}
+
+static int rts490x_regulator_disable(struct regulator_dev *rdev)
+{
+ struct rts490x_reg_state state;
+ int ret, ret2;
+
+ guard(rts490x_regulator)(rdev);
+
+ ret = rts490x_regulator_unprotect(rdev, &state);
+ if (ret)
+ return ret;
+
+ ret = regulator_disable_regmap(rdev);
+ ret2 = rts490x_regulator_protect(rdev, &state);
+
+ return ret ?: ret2;
+}
+
+static int rts490x_regulator_set_voltage_sel(struct regulator_dev *rdev,
+ unsigned int selector)
+{
+ struct rts490x_reg_state state;
+ int ret, ret2;
+
+ guard(rts490x_regulator)(rdev);
+
+ ret = rts490x_regulator_unprotect(rdev, &state);
+ if (ret)
+ return ret;
+
+ ret = regulator_set_voltage_sel_regmap(rdev, selector);
+ ret2 = rts490x_regulator_protect(rdev, &state);
+
+ return ret ?: ret2;
+}
+
+static const struct regulator_ops rts490x_regulator_ops = {
+ .list_voltage = regulator_list_voltage_table,
+ .map_voltage = regulator_map_voltage_iterate,
+ .set_voltage_sel = rts490x_regulator_set_voltage_sel,
+ .get_voltage_sel = regulator_get_voltage_sel_regmap,
+ .enable = rts490x_regulator_enable,
+ .disable = rts490x_regulator_disable,
+ .is_enabled = regulator_is_enabled_regmap,
+};
+
+static const unsigned int rts490x_voltage_table[] = {
+ 1000000,
+ 1100000,
+ 1200000,
+ 1800000,
+};
+
+static const struct regulator_desc rts490x_regulators[] = {
+ {
+ .name = "ldo-cp0",
+ .of_match = of_match_ptr("ldo-cp0"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = rts490x_voltage_table,
+ .n_voltages = ARRAY_SIZE(rts490x_voltage_table),
+ .ops = &rts490x_regulator_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = RTS490X_LDO_AND_PULLUP_CONF,
+ .enable_mask = RTS490X_CP0_LDO_ENABLE,
+ .enable_time = 5000,
+ .vsel_reg = RTS490X_LDO_CONF,
+ .vsel_mask = RTS490X_CP0_LDO_VOLTAGE_MASK,
+ },
+ {
+ .name = "ldo-cp1",
+ .of_match = of_match_ptr("ldo-cp1"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = rts490x_voltage_table,
+ .n_voltages = ARRAY_SIZE(rts490x_voltage_table),
+ .ops = &rts490x_regulator_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = RTS490X_LDO_AND_PULLUP_CONF,
+ .enable_mask = RTS490X_CP1_LDO_ENABLE,
+ .enable_time = 5000,
+ .vsel_reg = RTS490X_LDO_CONF,
+ .vsel_mask = RTS490X_CP1_LDO_VOLTAGE_MASK,
+ },
+ {
+ .name = "ldo-tp0145",
+ .of_match = of_match_ptr("ldo-tp0145"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = rts490x_voltage_table,
+ .n_voltages = ARRAY_SIZE(rts490x_voltage_table),
+ .ops = &rts490x_regulator_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = RTS490X_LDO_AND_PULLUP_CONF,
+ .enable_mask = RTS490X_TP0145_LDO_ENABLE,
+ .enable_time = 5000,
+ .vsel_reg = RTS490X_LDO_CONF,
+ .vsel_mask = RTS490X_TP0145_LDO_VOLTAGE_MASK,
+ },
+ {
+ .name = "ldo-tp2367",
+ .of_match = of_match_ptr("ldo-tp2367"),
+ .regulators_node = of_match_ptr("regulators"),
+ .volt_table = rts490x_voltage_table,
+ .n_voltages = ARRAY_SIZE(rts490x_voltage_table),
+ .ops = &rts490x_regulator_ops,
+ .type = REGULATOR_VOLTAGE,
+ .owner = THIS_MODULE,
+ .enable_reg = RTS490X_LDO_AND_PULLUP_CONF,
+ .enable_mask = RTS490X_TP2367_LDO_ENABLE,
+ .enable_time = 5000,
+ .vsel_reg = RTS490X_LDO_CONF,
+ .vsel_mask = RTS490X_TP2367_LDO_VOLTAGE_MASK,
+ },
+};
+
+static int rts490x_regulator_probe(struct platform_device *pdev)
+{
+ struct device_node *regulators __free(device_node) = NULL;
+ struct rts490x *rts490x = dev_get_drvdata(pdev->dev.parent);
+ struct rts490x_regulator *priv;
+ struct regulator_config config = { };
+ struct regulator_dev *rdev;
+ int i;
+
+ priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->rts490x = rts490x;
+ platform_set_drvdata(pdev, priv);
+ device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent);
+
+ if (!pdev->dev.of_node) {
+ dev_warn(&pdev->dev,
+ "no OF node; skipping on-chip LDO registration\n");
+ return 0;
+ }
+
+ regulators = of_get_available_child_by_name(pdev->dev.of_node, "regulators");
+ if (!regulators)
+ return 0;
+
+ config.dev = &pdev->dev;
+ config.regmap = rts490x->regmap;
+ config.driver_data = priv;
+
+ for (i = 0; i < ARRAY_SIZE(rts490x_regulators); i++) {
+ struct device_node *node __free(device_node) = NULL;
+ const char *name = rts490x_regulators[i].of_match;
+
+ node = of_get_available_child_by_name(regulators, name);
+ if (!node)
+ continue;
+
+ config.of_node = node;
+ rdev = devm_regulator_register(&pdev->dev, &rts490x_regulators[i],
+ &config);
+ if (IS_ERR(rdev))
+ return dev_err_probe(&pdev->dev, PTR_ERR(rdev),
+ "failed to register %s\n",
+ rts490x_regulators[i].name);
+ }
+
+ return 0;
+}
+
+static const struct platform_device_id rts490x_regulator_ids[] = {
+ { "rts490x-regulator" },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, rts490x_regulator_ids);
+
+static struct platform_driver rts490x_regulator_driver = {
+ .driver = {
+ .name = "rts490x-regulator",
+ .probe_type = PROBE_PREFER_ASYNCHRONOUS,
+ },
+ .probe = rts490x_regulator_probe,
+ .id_table = rts490x_regulator_ids,
+};
+module_platform_driver(rts490x_regulator_driver);
+
+MODULE_AUTHOR("Yin Zhou <zain_zhou@realsil.com.cn>");
+MODULE_DESCRIPTION("Realtek RTS490x I3C hub regulator driver");
+MODULE_LICENSE("GPL");
--
2.34.1
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 4/4] i3c: hub: Add Realtek RTS490x I3C hub support
2026-09-20 10:04 [RFC PATCH 0/4] Add support for Realtek RTS490x I3C hub zain_zhou
` (2 preceding siblings ...)
2026-09-20 10:05 ` [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators zain_zhou
@ 2026-09-20 10:05 ` zain_zhou
2026-09-21 18:42 ` Frank Li
3 siblings, 1 reply; 10+ messages in thread
From: zain_zhou @ 2026-09-20 10:05 UTC (permalink / raw)
To: Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Mark Brown,
Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, Wei Wang, Yin Zhou
From: Yin Zhou <zain_zhou@realsil.com.cn>
Add I3C and SMBus downstream port support for the Realtek RTS490x
family of I3C hubs.
I3C ports are exposed through the generic I3C hub framework. SMBus
ports are exposed as Linux I2C adapters and support controller-agent
transfers and target receive events through the hub IBI.
Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
---
MAINTAINERS | 1 +
drivers/i3c/hub/Kconfig | 11 +
drivers/i3c/hub/Makefile | 3 +
drivers/i3c/hub/rts490x-i3c-hub-core.c | 444 +++++++++++++
drivers/i3c/hub/rts490x-i3c-hub-i3c.c | 150 +++++
drivers/i3c/hub/rts490x-i3c-hub-smbus.c | 839 ++++++++++++++++++++++++
drivers/i3c/hub/rts490x-i3c-hub.h | 150 +++++
7 files changed, 1598 insertions(+)
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-core.c
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-i3c.c
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-smbus.c
create mode 100644 drivers/i3c/hub/rts490x-i3c-hub.h
diff --git a/MAINTAINERS b/MAINTAINERS
index 6e7cb4ca92f6..761a9d16305b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22998,6 +22998,7 @@ L: linux-i3c@lists.infradead.org
L: linux-kernel@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
+F: drivers/i3c/hub/rts490x*
F: drivers/mfd/rts490x-core.c
F: drivers/regulator/rts490x-regulator.c
F: include/linux/mfd/rts490x.h
diff --git a/drivers/i3c/hub/Kconfig b/drivers/i3c/hub/Kconfig
index 969bb1f637dd..e5f618d08c49 100644
--- a/drivers/i3c/hub/Kconfig
+++ b/drivers/i3c/hub/Kconfig
@@ -9,3 +9,14 @@ config P3H2X4X_I3C_HUB
connect to a host via I3C/I2C/SMBus and allow communication with
multiple downstream peripherals. Say Y or M here to enable the
P3H2x4x I3C HUB driver.
+
+config RTS490X_I3C_HUB
+ tristate "Realtek RTS490x I3C hub support"
+ depends on MFD_RTS490X
+ depends on I2C
+ select I3C_HUB
+ help
+ Enable support for the I3C and SMBus target ports provided by
+ Realtek RTS490x I3C hub devices. I3C ports are exposed through
+ the generic I3C hub framework, while SMBus ports are exposed as
+ Linux I2C adapters.
diff --git a/drivers/i3c/hub/Makefile b/drivers/i3c/hub/Makefile
index 9dbd8a7b4184..e9c8de3c021a 100644
--- a/drivers/i3c/hub/Makefile
+++ b/drivers/i3c/hub/Makefile
@@ -2,3 +2,6 @@
# Copyright 2025 NXP
p3h2840_i3c_hub-y := p3h2840_i3c_hub_common.o p3h2840_i3c_hub_i3c.o p3h2840_i3c_hub_smbus.o
obj-$(CONFIG_P3H2X4X_I3C_HUB) += p3h2840_i3c_hub.o
+rts490x_i3c_hub-y := rts490x-i3c-hub-core.o rts490x-i3c-hub-i3c.o \
+ rts490x-i3c-hub-smbus.o
+obj-$(CONFIG_RTS490X_I3C_HUB) += rts490x_i3c_hub.o
diff --git a/drivers/i3c/hub/rts490x-i3c-hub-core.c b/drivers/i3c/hub/rts490x-i3c-hub-core.c
new file mode 100644
index 000000000000..b26977066ac7
--- /dev/null
+++ b/drivers/i3c/hub/rts490x-i3c-hub-core.c
@@ -0,0 +1,444 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
+
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/i3c/device.h>
+#include <linux/iopoll.h>
+#include <linux/mfd/rts490x.h>
+#include <linux/of.h>
+#include <linux/of_address.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/util_macros.h>
+
+#include "rts490x-i3c-hub.h"
+
+#define RTS490X_TP_ENABLE 0x12
+#define RTS490X_IO_STRENGTH 0x14
+#define RTS490X_TP_IO_MODE_CONF 0x17
+#define RTS490X_TP_SMBUS_AGENT_ENABLE 0x18
+#define RTS490X_CP_MUX_SET 0x38
+#define RTS490X_CP_MUX_STATUS 0x39
+#define RTS490X_CONTROLLER_PORT_MUX_REQ BIT(0)
+#define RTS490X_CONTROLLER_PORT_MUXED BIT(0)
+#define RTS490X_TP_PULLUP_ENABLE 0x53
+#define RTS490X_ONCHIP_TD_AND_SMBUS_AGENT_CONF 0x6c
+#define RTS490X_TARGET_AGENT_BUF_FULL_SDA_LOW_EN BIT(5)
+
+#define RTS490X_TP0145_IO_STRENGTH_MASK GENMASK(1, 0)
+#define RTS490X_TP2367_IO_STRENGTH_MASK GENMASK(3, 2)
+#define RTS490X_CP0_IO_STRENGTH_MASK GENMASK(5, 4)
+#define RTS490X_CP1_IO_STRENGTH_MASK GENMASK(7, 6)
+#define RTS490X_TP0145_PULLUP_MASK GENMASK(7, 6)
+#define RTS490X_TP2367_PULLUP_MASK GENMASK(5, 4)
+
+#define RTS490X_SMBUS_DEFAULT_FREQUENCY 400000
+#define RTS490X_CP_MUX_POLL_US 100
+#define RTS490X_CP_MUX_TIMEOUT_US 100000
+
+static void rts490x_put_port_fwnodes(void *data)
+{
+ struct rts490x_hub *hub = data;
+ int i;
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++) {
+ fwnode_handle_put(hub->ports[i].fwnode);
+ hub->ports[i].fwnode = NULL;
+ }
+}
+
+static void rts490x_clear_hub_backpointer(void *data)
+{
+ struct rts490x *rts490x = data;
+
+ rts490x->hub = NULL;
+}
+
+static int rts490x_read_config(struct rts490x_hub *hub)
+{
+ struct rts490x_config *config = &hub->config;
+ struct device *dev = hub->dev;
+
+ config->tp0145_pullup_configured =
+ !device_property_read_u32(dev, "realtek,tp0145-pullup-ohms",
+ &config->tp0145_pullup_ohms);
+ config->tp2367_pullup_configured =
+ !device_property_read_u32(dev, "realtek,tp2367-pullup-ohms",
+ &config->tp2367_pullup_ohms);
+ config->cp0_io_strength_configured =
+ !device_property_read_u32(dev, "realtek,cp0-io-strength-ohms",
+ &config->cp0_io_strength_ohms);
+ config->cp1_io_strength_configured =
+ !device_property_read_u32(dev, "realtek,cp1-io-strength-ohms",
+ &config->cp1_io_strength_ohms);
+ config->tp0145_io_strength_configured =
+ !device_property_read_u32(dev, "realtek,tp0145-io-strength-ohms",
+ &config->tp0145_io_strength_ohms);
+ config->tp2367_io_strength_configured =
+ !device_property_read_u32(dev, "realtek,tp2367-io-strength-ohms",
+ &config->tp2367_io_strength_ohms);
+
+ return 0;
+}
+
+static int rts490x_parse_ports(struct rts490x_hub *hub)
+{
+ struct device_node *node = dev_of_node(hub->dev);
+ struct rts490x_port *port;
+ u64 index;
+ int ret;
+
+ for_each_available_child_of_node_scoped(node, child) {
+ enum rts490x_port_mode mode;
+
+ if (of_node_name_eq(child, "i3c"))
+ mode = RTS490X_PORT_I3C;
+ else if (of_node_name_eq(child, "smbus"))
+ mode = RTS490X_PORT_SMBUS;
+ else
+ continue;
+
+ ret = of_property_read_reg(child, 0, &index, NULL);
+ if (ret)
+ return dev_err_probe(hub->dev, ret,
+ "failed to read reg for %pOF\n", child);
+
+ if (index >= hub->rts490x->num_target_ports)
+ return dev_err_probe(hub->dev, -EINVAL,
+ "invalid target port %llu\n", index);
+
+ port = &hub->ports[index];
+ if (port->fwnode)
+ return dev_err_probe(hub->dev, -EINVAL,
+ "duplicate target port %llu\n", index);
+
+ port->fwnode = fwnode_handle_get(of_fwnode_handle(child));
+ port->hub = hub;
+ port->index = index;
+ port->mask = BIT(index);
+ port->mode = mode;
+ port->pullup_enabled =
+ of_property_read_bool(child, "realtek,pullup-enable");
+
+ if (mode == RTS490X_PORT_SMBUS) {
+ port->clock_frequency = RTS490X_SMBUS_DEFAULT_FREQUENCY;
+ of_property_read_u32(child, "clock-frequency",
+ &port->clock_frequency);
+ if (!rts490x_smbus_frequency_supported(port->clock_frequency))
+ return dev_err_probe(hub->dev, -EINVAL,
+ "unsupported SMBus frequency %u Hz on port %llu\n",
+ port->clock_frequency, index);
+ }
+
+ if (mode == RTS490X_PORT_I3C)
+ hub->i3c_port_mask |= port->mask;
+ else
+ hub->smbus_port_mask |= port->mask;
+ }
+
+ return 0;
+}
+
+static u8 rts490x_pullup_to_reg(u32 pullup_ohms)
+{
+ static const int pullup_table[] = { 250, 500, 1000, 2000 };
+
+ return find_closest(pullup_ohms, pullup_table,
+ ARRAY_SIZE(pullup_table));
+}
+
+static u8 rts490x_io_strength_to_reg(u32 io_strength_ohms)
+{
+ static const int io_strength_table[] = { 20, 30, 40, 50 };
+
+ return find_closest(io_strength_ohms, io_strength_table,
+ ARRAY_SIZE(io_strength_table));
+}
+
+static int rts490x_enable_supplies(struct rts490x_hub *hub)
+{
+ static const char * const supplies[] = {
+ "vcc-cp0",
+ "vcc-cp1",
+ "vcc-tp0145",
+ "vcc-tp2367",
+ };
+ int ret;
+ int i;
+
+ for (i = 0; i < ARRAY_SIZE(supplies); i++) {
+ ret = devm_regulator_get_enable_optional(hub->dev, supplies[i]);
+ if (ret && ret != -ENODEV)
+ return dev_err_probe(hub->dev, ret,
+ "failed to enable %s supply\n",
+ supplies[i]);
+ }
+
+ return 0;
+}
+
+static int rts490x_configure_pullups(struct rts490x_hub *hub)
+{
+ struct rts490x_config *config = &hub->config;
+ u8 mask = 0;
+ u8 pullup = 0;
+
+ if (config->tp0145_pullup_configured) {
+ mask |= RTS490X_TP0145_PULLUP_MASK;
+ pullup |= FIELD_PREP(RTS490X_TP0145_PULLUP_MASK,
+ rts490x_pullup_to_reg(config->tp0145_pullup_ohms));
+ }
+
+ if (config->tp2367_pullup_configured) {
+ mask |= RTS490X_TP2367_PULLUP_MASK;
+ pullup |= FIELD_PREP(RTS490X_TP2367_PULLUP_MASK,
+ rts490x_pullup_to_reg(config->tp2367_pullup_ohms));
+ }
+
+ return regmap_update_bits(hub->regmap, RTS490X_LDO_AND_PULLUP_CONF,
+ mask, pullup);
+}
+
+static int rts490x_configure_io_strength(struct rts490x_hub *hub)
+{
+ struct rts490x_config *config = &hub->config;
+ u8 strength = 0;
+ u8 mask = 0;
+
+ if (config->cp0_io_strength_configured) {
+ u8 value = rts490x_io_strength_to_reg(config->cp0_io_strength_ohms);
+
+ mask |= RTS490X_CP0_IO_STRENGTH_MASK;
+ strength |= FIELD_PREP(RTS490X_CP0_IO_STRENGTH_MASK, value);
+ }
+
+ if (config->cp1_io_strength_configured) {
+ u8 value = rts490x_io_strength_to_reg(config->cp1_io_strength_ohms);
+
+ mask |= RTS490X_CP1_IO_STRENGTH_MASK;
+ strength |= FIELD_PREP(RTS490X_CP1_IO_STRENGTH_MASK, value);
+ }
+
+ if (config->tp0145_io_strength_configured) {
+ u8 value = rts490x_io_strength_to_reg(config->tp0145_io_strength_ohms);
+
+ mask |= RTS490X_TP0145_IO_STRENGTH_MASK;
+ strength |= FIELD_PREP(RTS490X_TP0145_IO_STRENGTH_MASK, value);
+ }
+
+ if (config->tp2367_io_strength_configured) {
+ u8 value = rts490x_io_strength_to_reg(config->tp2367_io_strength_ohms);
+
+ mask |= RTS490X_TP2367_IO_STRENGTH_MASK;
+ strength |= FIELD_PREP(RTS490X_TP2367_IO_STRENGTH_MASK, value);
+ }
+
+ return regmap_update_bits(hub->regmap, RTS490X_IO_STRENGTH,
+ mask, strength);
+}
+
+static int rts490x_configure_ports(struct rts490x_hub *hub)
+{
+ u8 port_mask = GENMASK(hub->rts490x->num_target_ports - 1, 0);
+ u8 pullup_mask = 0;
+ int ret;
+ int i;
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++) {
+ if (hub->ports[i].pullup_enabled)
+ pullup_mask |= hub->ports[i].mask;
+ }
+
+ ret = regmap_update_bits(hub->regmap, RTS490X_TP_PULLUP_ENABLE,
+ port_mask, pullup_mask);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(hub->regmap, RTS490X_TP_IO_MODE_CONF,
+ port_mask, hub->smbus_port_mask);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(hub->regmap, RTS490X_TP_SMBUS_AGENT_ENABLE,
+ port_mask, hub->smbus_port_mask);
+ if (ret)
+ return ret;
+
+ ret = regmap_clear_bits(hub->regmap,
+ RTS490X_TP_SMBUS_AGENT_IBI_CONFIG, port_mask);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++) {
+ if (!(hub->smbus_port_mask & BIT(i)))
+ continue;
+
+ ret = regmap_write(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + i,
+ RTS490X_TARGET_BUFFER_STATUS_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (hub->i3c_port_mask) {
+ unsigned int status;
+
+ ret = regmap_write(hub->regmap, RTS490X_CP_MUX_SET,
+ RTS490X_CONTROLLER_PORT_MUX_REQ);
+ if (ret)
+ return ret;
+
+ ret = regmap_read_poll_timeout(hub->regmap, RTS490X_CP_MUX_STATUS,
+ status,
+ status & RTS490X_CONTROLLER_PORT_MUXED,
+ RTS490X_CP_MUX_POLL_US,
+ RTS490X_CP_MUX_TIMEOUT_US);
+ if (ret)
+ return ret;
+ }
+
+ return regmap_update_bits(hub->regmap, RTS490X_TP_ENABLE, port_mask,
+ hub->i3c_port_mask | hub->smbus_port_mask);
+}
+
+static int rts490x_configure_hardware(struct rts490x_hub *hub)
+{
+ unsigned int protection;
+ bool restore;
+ int ret, ret2;
+
+ ret = rts490x_enable_supplies(hub);
+ if (ret)
+ return ret;
+
+ mutex_lock(&hub->rts490x->protected_reg_lock);
+
+ ret = regmap_read(hub->regmap, RTS490X_PROTECTION_CODE, &protection);
+ if (ret)
+ goto unlock;
+
+ restore = protection != RTS490X_REGISTERS_UNLOCK_CODE;
+ if (restore) {
+ ret = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE,
+ RTS490X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ goto unlock;
+ }
+
+ ret = rts490x_configure_pullups(hub);
+ if (!ret)
+ ret = rts490x_configure_io_strength(hub);
+ if (!ret)
+ ret = rts490x_configure_ports(hub);
+ if (!ret && hub->smbus_port_mask) {
+ /* Hold SDA low when both target buffers are full. */
+ ret = regmap_set_bits(hub->regmap,
+ RTS490X_ONCHIP_TD_AND_SMBUS_AGENT_CONF,
+ RTS490X_TARGET_AGENT_BUF_FULL_SDA_LOW_EN);
+ }
+
+ if (restore) {
+ ret2 = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE,
+ protection);
+ if (!ret && ret2)
+ ret = ret2;
+ }
+
+unlock:
+ mutex_unlock(&hub->rts490x->protected_reg_lock);
+
+ return ret;
+}
+
+static int rts490x_hub_probe(struct platform_device *pdev)
+{
+ struct rts490x *rts490x = dev_get_drvdata(pdev->dev.parent);
+ struct rts490x_hub *hub;
+ int ret;
+
+ if (!rts490x || !rts490x->i3cdev)
+ return -ENODEV;
+
+ hub = devm_kzalloc(&pdev->dev, sizeof(*hub), GFP_KERNEL);
+ if (!hub)
+ return -ENOMEM;
+
+ hub->dev = &pdev->dev;
+ hub->rts490x = rts490x;
+ hub->regmap = rts490x->regmap;
+ platform_set_drvdata(pdev, hub);
+ device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent);
+
+ ret = devm_mutex_init(&pdev->dev, &hub->page_lock);
+ if (ret)
+ return ret;
+
+ ret = devm_mutex_init(&pdev->dev, &hub->target_lock);
+ if (ret)
+ return ret;
+
+ spin_lock_init(&hub->controller_lock);
+
+ ret = rts490x_read_config(hub);
+ if (ret)
+ return ret;
+
+ ret = rts490x_parse_ports(hub);
+ if (ret)
+ return ret;
+
+ ret = devm_add_action_or_reset(&pdev->dev, rts490x_put_port_fwnodes, hub);
+ if (ret)
+ return ret;
+
+ ret = rts490x_configure_hardware(hub);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "failed to configure hub hardware\n");
+
+ i3c_hub_init(&hub->hub, &rts490x_hub_ops, rts490x->i3cdev);
+
+ /* The IBI handler obtains the child context through the MFD parent. */
+ rts490x->hub = hub;
+ ret = devm_add_action_or_reset(&pdev->dev,
+ rts490x_clear_hub_backpointer, rts490x);
+ if (ret)
+ return ret;
+
+ ret = rts490x_register_i3c_ports(hub);
+ if (ret)
+ return ret;
+
+ ret = rts490x_setup_ibi(hub);
+ if (ret)
+ return ret;
+
+ ret = rts490x_register_smbus_ports(hub);
+ if (ret) {
+ rts490x_unregister_smbus_ports(hub);
+ return ret;
+ }
+
+ return devm_add_action_or_reset(&pdev->dev,
+ rts490x_unregister_smbus_ports, hub);
+}
+
+static const struct platform_device_id rts490x_hub_ids[] = {
+ { "rts490x-i3c-hub" },
+ { }
+};
+MODULE_DEVICE_TABLE(platform, rts490x_hub_ids);
+
+static struct platform_driver rts490x_hub_driver = {
+ .driver.name = "rts490x-i3c-hub",
+ .probe = rts490x_hub_probe,
+ .id_table = rts490x_hub_ids,
+};
+module_platform_driver(rts490x_hub_driver);
+
+MODULE_AUTHOR("Yin Zhou <zain_zhou@realsil.com.cn>");
+MODULE_DESCRIPTION("Realtek RTS490x I3C hub driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/i3c/hub/rts490x-i3c-hub-i3c.c b/drivers/i3c/hub/rts490x-i3c-hub-i3c.c
new file mode 100644
index 000000000000..7ddd7d171ef0
--- /dev/null
+++ b/drivers/i3c/hub/rts490x-i3c-hub-i3c.c
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
+
+#include <linux/i3c/device.h>
+#include <linux/mfd/rts490x.h>
+#include <linux/regmap.h>
+
+#include "rts490x-i3c-hub.h"
+
+#define RTS490X_TP_NET_CON_CONF 0x51
+
+static struct rts490x_port *
+rts490x_port_from_controller(struct i3c_master_controller *controller)
+{
+ struct i3c_hub_controller *hub_controller;
+
+ hub_controller = container_of(controller, struct i3c_hub_controller,
+ controller);
+
+ return container_of(hub_controller, struct rts490x_port, hub_controller);
+}
+
+static int rts490x_set_port_connected(struct rts490x_port *port, bool connected)
+{
+ if (connected)
+ return regmap_set_bits(port->hub->regmap, RTS490X_TP_NET_CON_CONF,
+ port->mask);
+
+ return regmap_clear_bits(port->hub->regmap, RTS490X_TP_NET_CON_CONF,
+ port->mask);
+}
+
+static void rts490x_enable_port(struct i3c_master_controller *controller)
+{
+ struct rts490x_port *port = rts490x_port_from_controller(controller);
+ int ret;
+
+ if (port->always_enable)
+ return;
+
+ ret = rts490x_set_port_connected(port, true);
+ if (ret)
+ dev_warn(port->hub->dev, "failed to connect target port %u: %d\n",
+ port->index, ret);
+}
+
+static void rts490x_disable_port(struct i3c_master_controller *controller)
+{
+ struct rts490x_port *port = rts490x_port_from_controller(controller);
+ int ret;
+
+ if (port->always_enable)
+ return;
+
+ ret = rts490x_set_port_connected(port, false);
+ if (ret)
+ dev_warn(port->hub->dev, "failed to disconnect target port %u: %d\n",
+ port->index, ret);
+}
+
+const struct i3c_hub_ops rts490x_hub_ops = {
+ .enable_port = rts490x_enable_port,
+ .disable_port = rts490x_disable_port,
+};
+
+static void rts490x_unregister_i3c_controller(void *data)
+{
+ struct i3c_master_controller *controller = data;
+
+ i3c_master_unregister(controller);
+}
+
+static void rts490x_disable_i3c_ports(void *data)
+{
+ struct rts490x_hub *hub = data;
+ int i;
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++)
+ hub->ports[i].always_enable = false;
+
+ if (regmap_clear_bits(hub->regmap, RTS490X_TP_NET_CON_CONF,
+ hub->i3c_port_mask))
+ dev_warn(hub->dev, "failed to disconnect I3C target ports\n");
+}
+
+static int rts490x_reserve_port_addresses(struct rts490x_port *port)
+{
+ return i3c_hub_reserve_parent_addrslots_from_dt(&port->hub_controller,
+ to_of_node(port->fwnode));
+}
+
+static int rts490x_register_port_controller(struct rts490x_port *port)
+{
+ return i3c_master_register_fwnode(&port->hub_controller.controller,
+ port->hub->dev, port->fwnode,
+ &i3c_hub_master_ops, false);
+}
+
+int rts490x_register_i3c_ports(struct rts490x_hub *hub)
+{
+ struct i3c_master_controller *parent;
+ struct rts490x_port *port;
+ int ret;
+ int i;
+
+ parent = i3c_dev_get_master(hub->rts490x->i3cdev->desc);
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++) {
+ port = &hub->ports[i];
+ if (port->mode != RTS490X_PORT_I3C)
+ continue;
+
+ port->hub_controller.parent = parent;
+ port->hub_controller.hub = &hub->hub;
+ dev_set_drvdata(&port->hub_controller.controller.dev,
+ &port->hub_controller);
+
+ ret = rts490x_reserve_port_addresses(port);
+ if (ret)
+ return ret;
+
+ ret = rts490x_register_port_controller(port);
+ if (ret)
+ return ret;
+
+ ret = devm_add_action_or_reset(hub->dev,
+ rts490x_unregister_i3c_controller,
+ &port->hub_controller.controller);
+ if (ret)
+ return ret;
+
+ port->registered = true;
+ port->always_enable = true;
+ }
+
+ if (hub->i3c_port_mask) {
+ ret = regmap_set_bits(hub->regmap, RTS490X_TP_NET_CON_CONF,
+ hub->i3c_port_mask);
+ if (ret)
+ return dev_err_probe(hub->dev, ret,
+ "failed to connect I3C target ports\n");
+
+ ret = devm_add_action_or_reset(hub->dev,
+ rts490x_disable_i3c_ports, hub);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
diff --git a/drivers/i3c/hub/rts490x-i3c-hub-smbus.c b/drivers/i3c/hub/rts490x-i3c-hub-smbus.c
new file mode 100644
index 000000000000..88db3879660a
--- /dev/null
+++ b/drivers/i3c/hub/rts490x-i3c-hub-smbus.c
@@ -0,0 +1,839 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
+
+#include <linux/bitfield.h>
+#include <linux/cleanup.h>
+#include <linux/i2c.h>
+#include <linux/i3c/device.h>
+#include <linux/mfd/rts490x.h>
+#include <linux/regmap.h>
+
+#include "rts490x-i3c-hub.h"
+
+#define RTS490X_DEV_AND_IBI_STATUS 0x20
+#define RTS490X_TP_SMBUS_AGENT_IBI_STATUS 0x21
+#define RTS490X_TP_SMBUS_AGENT_TRANS_START 0x50
+#define RTS490X_PAGE_PTR 0x7f
+#define RTS490X_CONTROLLER_BUFFER_PAGE 0x10
+#define RTS490X_CONTROLLER_PAGES_PER_PORT 4
+#define RTS490X_TARGET_BUFFER_0_PAGE_OFFSET 2
+#define RTS490X_TARGET_BUFFER_1_PAGE_OFFSET 3
+#define RTS490X_CONTROLLER_AGENT_BUFFER 0x80
+#define RTS490X_CONTROLLER_AGENT_BUFFER_DATA 0x84
+#define RTS490X_TARGET_BUFFER_LENGTH 0x80
+#define RTS490X_TARGET_BUFFER_ADDRESS 0x81
+#define RTS490X_TARGET_BUFFER_DATA 0x82
+#define RTS490X_CONTROLLER_BUFFER_SIZE 88
+#define RTS490X_SMBUS_DESCRIPTOR_SIZE 4
+#define RTS490X_SMBUS_PAYLOAD_SIZE \
+ (RTS490X_CONTROLLER_BUFFER_SIZE - RTS490X_SMBUS_DESCRIPTOR_SIZE)
+
+#define RTS490X_SMBUS_RATE_100KHZ 0
+#define RTS490X_SMBUS_RATE_200KHZ BIT(1)
+#define RTS490X_SMBUS_RATE_400KHZ BIT(2)
+#define RTS490X_SMBUS_RATE_1MHZ (BIT(1) | BIT(2))
+#define RTS490X_SMBUS_REPEATED_START BIT(0)
+
+#define RTS490X_SMBUS_AGENT_EVENT BIT(4)
+#define RTS490X_CONTROLLER_FINISH BIT(0)
+#define RTS490X_TARGET_BUFFER_0_RECEIVED BIT(1)
+#define RTS490X_TARGET_BUFFER_1_RECEIVED BIT(2)
+#define RTS490X_TARGET_BUFFER_OVERFLOW BIT(3)
+#define RTS490X_CONTROLLER_RETURN_CODE_MASK GENMASK(7, 4)
+#define RTS490X_CONTROLLER_SUCCESS 0
+#define RTS490X_CONTROLLER_ADDRESS_NACK 1
+#define RTS490X_CONTROLLER_DEVICE_BUSY 2
+#define RTS490X_CONTROLLER_READ_NOT_READY 3
+#define RTS490X_CONTROLLER_SYNC_RECOVERED 4
+#define RTS490X_CONTROLLER_SYNC_BUS_CLEAR 5
+#define RTS490X_CONTROLLER_BUS_FAULT 6
+#define RTS490X_CONTROLLER_ARBITRATION_LOST 7
+#define RTS490X_CONTROLLER_SCL_TIMEOUT 8
+
+#define RTS490X_SMBUS_TIMEOUT_MS 1000
+#define RTS490X_IBI_MAX_PAYLOAD_LENGTH 2
+#define RTS490X_IBI_SLOTS 6
+
+#define RTS490X_I2C_SUPPORTED_FLAGS (I2C_M_RD | I2C_M_DMA_SAFE)
+
+enum rts490x_smbus_xfer_type {
+ RTS490X_SMBUS_WRITE,
+ RTS490X_SMBUS_READ,
+ RTS490X_SMBUS_WRITE_READ,
+};
+
+enum rts490x_smbus_descriptor_index {
+ RTS490X_SMBUS_DESC_ADDRESS,
+ RTS490X_SMBUS_DESC_TYPE,
+ RTS490X_SMBUS_DESC_WRITE_LENGTH,
+ RTS490X_SMBUS_DESC_READ_LENGTH,
+};
+
+bool rts490x_smbus_frequency_supported(u32 frequency)
+{
+ switch (frequency) {
+ case 100000:
+ case 200000:
+ case 400000:
+ case 1000000:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static u8 rts490x_smbus_frequency_to_reg(u32 frequency)
+{
+ switch (frequency) {
+ case 100000:
+ return RTS490X_SMBUS_RATE_100KHZ;
+ case 200000:
+ return RTS490X_SMBUS_RATE_200KHZ;
+ case 1000000:
+ return RTS490X_SMBUS_RATE_1MHZ;
+ default:
+ return RTS490X_SMBUS_RATE_400KHZ;
+ }
+}
+
+static int rts490x_smbus_status_to_errno(struct rts490x_port *port,
+ unsigned int status)
+{
+ u8 code = FIELD_GET(RTS490X_CONTROLLER_RETURN_CODE_MASK, status);
+
+ switch (code) {
+ case RTS490X_CONTROLLER_SUCCESS:
+ return 0;
+ case RTS490X_CONTROLLER_ADDRESS_NACK:
+ case RTS490X_CONTROLLER_READ_NOT_READY:
+ return -ENXIO;
+ case RTS490X_CONTROLLER_DEVICE_BUSY:
+ return -EREMOTEIO;
+ case RTS490X_CONTROLLER_SYNC_RECOVERED:
+ case RTS490X_CONTROLLER_SYNC_BUS_CLEAR:
+ case RTS490X_CONTROLLER_ARBITRATION_LOST:
+ return -EAGAIN;
+ case RTS490X_CONTROLLER_SCL_TIMEOUT:
+ return -ETIMEDOUT;
+ case RTS490X_CONTROLLER_BUS_FAULT:
+ default:
+ dev_err(port->hub->dev,
+ "SMBus controller error %#x on target port %u\n",
+ code, port->index);
+ return -EIO;
+ }
+}
+
+static int rts490x_smbus_recover_controller(struct rts490x_port *port)
+{
+ struct rts490x_hub *hub = port->hub;
+ unsigned long flags;
+ unsigned int status;
+ bool recovering;
+ int ret;
+
+ spin_lock_irqsave(&hub->controller_lock, flags);
+ recovering = port->controller_recovering;
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+ if (!recovering)
+ return 0;
+
+ ret = regmap_read(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + port->index, &status);
+ if (ret)
+ return ret;
+
+ if (status & RTS490X_CONTROLLER_FINISH) {
+ ret = regmap_write(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
+ RTS490X_CONTROLLER_FINISH);
+ if (ret)
+ return ret;
+
+ spin_lock_irqsave(&hub->controller_lock, flags);
+ port->controller_recovering = false;
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+ return 0;
+ }
+
+ spin_lock_irqsave(&hub->controller_lock, flags);
+ recovering = port->controller_recovering;
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+
+ return recovering ? -EAGAIN : 0;
+}
+
+static int rts490x_smbus_wait_for_completion(struct rts490x_port *port)
+{
+ struct rts490x_hub *hub = port->hub;
+ unsigned long flags;
+ unsigned long timeout;
+ unsigned int status;
+ bool done;
+ int error;
+
+ timeout = wait_for_completion_timeout(&port->completion,
+ port->smbus_adapter->timeout);
+
+ spin_lock_irqsave(&hub->controller_lock, flags);
+ done = port->controller_done;
+ if (!done) {
+ port->controller_pending = false;
+ if (!timeout)
+ port->controller_recovering = true;
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+ return timeout ? -EIO : -ETIMEDOUT;
+ }
+
+ port->controller_pending = false;
+ port->controller_done = false;
+ status = port->controller_status;
+ error = port->controller_error;
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+
+ if (error)
+ return error;
+
+ return rts490x_smbus_status_to_errno(port, status);
+}
+
+static int
+rts490x_smbus_write_controller_buffer(struct rts490x_port *port,
+ const u8 *descriptor, const u8 *data,
+ unsigned int length)
+{
+ struct rts490x_hub *hub = port->hub;
+ u8 page = RTS490X_CONTROLLER_BUFFER_PAGE +
+ RTS490X_CONTROLLER_PAGES_PER_PORT * port->index;
+ int ret, ret2;
+
+ guard(mutex)(&hub->page_lock);
+
+ ret = regmap_write(hub->regmap, RTS490X_PAGE_PTR, page);
+ if (!ret)
+ ret = regmap_bulk_write(hub->regmap,
+ RTS490X_CONTROLLER_AGENT_BUFFER,
+ descriptor, RTS490X_SMBUS_DESCRIPTOR_SIZE);
+ if (!ret && length)
+ ret = regmap_bulk_write(hub->regmap,
+ RTS490X_CONTROLLER_AGENT_BUFFER_DATA,
+ data, length);
+
+ ret2 = regmap_write(hub->regmap, RTS490X_PAGE_PTR, 0);
+ return ret ?: ret2;
+}
+
+static int
+rts490x_smbus_read_controller_buffer(struct rts490x_port *port, u8 *data,
+ unsigned int offset, unsigned int length)
+{
+ struct rts490x_hub *hub = port->hub;
+ u8 page = RTS490X_CONTROLLER_BUFFER_PAGE +
+ RTS490X_CONTROLLER_PAGES_PER_PORT * port->index;
+ int ret, ret2;
+
+ guard(mutex)(&hub->page_lock);
+
+ ret = regmap_write(hub->regmap, RTS490X_PAGE_PTR, page);
+ if (!ret)
+ ret = regmap_bulk_read(hub->regmap,
+ RTS490X_CONTROLLER_AGENT_BUFFER_DATA + offset,
+ data, length);
+
+ ret2 = regmap_write(hub->regmap, RTS490X_PAGE_PTR, 0);
+ return ret ?: ret2;
+}
+
+static int rts490x_smbus_xfer_one(struct rts490x_port *port,
+ struct i2c_msg *write,
+ struct i2c_msg *read,
+ enum rts490x_smbus_xfer_type type)
+{
+ struct rts490x_hub *hub = port->hub;
+ u8 descriptor[RTS490X_SMBUS_DESCRIPTOR_SIZE] = { };
+ unsigned long flags;
+ u8 *read_buffer = NULL;
+ u8 address;
+ unsigned int write_length = write ? write->len : 0;
+ unsigned int read_length = read ? read->len : 0;
+ unsigned int status_reg = RTS490X_TP0_SMBUS_AGENT_STATUS + port->index;
+ int ret;
+
+ if ((write && write->flags & ~RTS490X_I2C_SUPPORTED_FLAGS) ||
+ (read && read->flags & ~RTS490X_I2C_SUPPORTED_FLAGS))
+ return -EOPNOTSUPP;
+
+ if (write_length + read_length > RTS490X_SMBUS_PAYLOAD_SIZE)
+ return -EOPNOTSUPP;
+
+ address = i2c_8bit_addr_from_msg(write ?: read);
+ descriptor[RTS490X_SMBUS_DESC_ADDRESS] = address;
+ descriptor[RTS490X_SMBUS_DESC_TYPE] =
+ rts490x_smbus_frequency_to_reg(port->clock_frequency);
+ descriptor[RTS490X_SMBUS_DESC_WRITE_LENGTH] = write_length;
+ descriptor[RTS490X_SMBUS_DESC_READ_LENGTH] = read_length;
+
+ if (type == RTS490X_SMBUS_WRITE_READ)
+ descriptor[RTS490X_SMBUS_DESC_TYPE] |= RTS490X_SMBUS_REPEATED_START;
+
+ if (read_length) {
+ read_buffer = i2c_get_dma_safe_msg_buf(read, 1);
+ if (!read_buffer)
+ return -ENOMEM;
+ }
+
+ guard(mutex)(&port->xfer_lock);
+
+ ret = rts490x_smbus_recover_controller(port);
+ if (ret)
+ goto out;
+
+ ret = rts490x_smbus_write_controller_buffer(port, descriptor,
+ write ? write->buf : NULL,
+ write_length);
+ if (ret)
+ goto out;
+
+ /* Clear any completion left by an earlier controller-agent transaction. */
+ ret = regmap_write(hub->regmap, status_reg, RTS490X_CONTROLLER_FINISH);
+ if (ret)
+ goto out;
+
+ reinit_completion(&port->completion);
+ spin_lock_irqsave(&hub->controller_lock, flags);
+ port->controller_status = 0;
+ port->controller_error = 0;
+ port->controller_done = false;
+ port->controller_pending = true;
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+
+ ret = regmap_write(hub->regmap, RTS490X_TP_SMBUS_AGENT_TRANS_START,
+ port->mask);
+ if (ret) {
+ spin_lock_irqsave(&hub->controller_lock, flags);
+ port->controller_pending = false;
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+ goto out;
+ }
+
+ ret = rts490x_smbus_wait_for_completion(port);
+ if (ret)
+ goto out;
+
+ if (read_length)
+ ret = rts490x_smbus_read_controller_buffer(port, read_buffer,
+ write_length,
+ read_length);
+
+out:
+ if (read_length)
+ i2c_put_dma_safe_msg_buf(read_buffer, read, !ret);
+
+ return ret;
+}
+
+static bool rts490x_smbus_can_combine(const struct i2c_msg *write,
+ const struct i2c_msg *read)
+{
+ return !(write->flags & I2C_M_RD) && (read->flags & I2C_M_RD) &&
+ write->addr == read->addr &&
+ write->len + read->len <= RTS490X_SMBUS_PAYLOAD_SIZE;
+}
+
+static int rts490x_smbus_xfer(struct i2c_adapter *adapter,
+ struct i2c_msg *messages, int num)
+{
+ struct rts490x_port *port = i2c_get_adapdata(adapter);
+ struct i2c_msg *read = NULL;
+ struct i2c_msg *write = NULL;
+ enum rts490x_smbus_xfer_type type;
+ int ret;
+
+ if (!num)
+ return 0;
+
+ if (num == 2) {
+ if (!rts490x_smbus_can_combine(&messages[0], &messages[1]))
+ return -EOPNOTSUPP;
+
+ write = &messages[0];
+ read = &messages[1];
+ type = RTS490X_SMBUS_WRITE_READ;
+ } else if (num == 1 && messages[0].flags & I2C_M_RD) {
+ read = &messages[0];
+ type = RTS490X_SMBUS_READ;
+ } else if (num == 1) {
+ write = &messages[0];
+ type = RTS490X_SMBUS_WRITE;
+ } else {
+ return -EOPNOTSUPP;
+ }
+
+ ret = rts490x_smbus_xfer_one(port, write, read, type);
+ return ret ? ret : num;
+}
+
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+static int rts490x_smbus_register_target(struct i2c_client *target)
+{
+ struct rts490x_port *port = i2c_get_adapdata(target->adapter);
+ struct rts490x_hub *hub = port->hub;
+ int ret;
+
+ guard(mutex)(&hub->target_lock);
+
+ if (!hub->ibi_ready)
+ return -EOPNOTSUPP;
+
+ if (target->flags & I2C_CLIENT_TEN)
+ return -EOPNOTSUPP;
+
+ if (port->target)
+ return -EBUSY;
+
+ ret = regmap_write(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
+ RTS490X_TARGET_BUFFER_STATUS_MASK);
+ if (ret)
+ return ret;
+
+ port->target = target;
+
+ return 0;
+}
+
+static int rts490x_smbus_unregister_target(struct i2c_client *target)
+{
+ struct rts490x_port *port = i2c_get_adapdata(target->adapter);
+ struct rts490x_hub *hub = port->hub;
+
+ guard(mutex)(&hub->target_lock);
+
+ if (port->target != target)
+ return -EINVAL;
+
+ port->target = NULL;
+
+ return 0;
+}
+#endif
+
+static u32 rts490x_smbus_functionality(struct i2c_adapter *adapter)
+{
+ u32 functionality;
+
+ functionality = (I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL) &
+ ~I2C_FUNC_SMBUS_PEC;
+
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ struct rts490x_port *port = i2c_get_adapdata(adapter);
+
+ if (port->hub->ibi_ready)
+ functionality |= I2C_FUNC_SLAVE;
+#endif
+
+ return functionality;
+}
+
+static const struct i2c_adapter_quirks rts490x_smbus_quirks = {
+ .flags = I2C_AQ_COMB_WRITE_THEN_READ,
+ .max_read_len = RTS490X_SMBUS_PAYLOAD_SIZE,
+ .max_write_len = RTS490X_SMBUS_PAYLOAD_SIZE,
+ .max_comb_1st_msg_len = RTS490X_SMBUS_PAYLOAD_SIZE - 1,
+ .max_comb_2nd_msg_len = RTS490X_SMBUS_PAYLOAD_SIZE - 1,
+};
+
+static const struct i2c_algorithm rts490x_smbus_algorithm = {
+ .xfer = rts490x_smbus_xfer,
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ .reg_target = rts490x_smbus_register_target,
+ .unreg_target = rts490x_smbus_unregister_target,
+#endif
+ .functionality = rts490x_smbus_functionality,
+};
+
+static int rts490x_read_target_buffer(struct rts490x_port *port,
+ unsigned int page_offset,
+ u8 *address, unsigned int *length)
+{
+ struct rts490x_hub *hub = port->hub;
+ u8 page = RTS490X_CONTROLLER_BUFFER_PAGE +
+ RTS490X_CONTROLLER_PAGES_PER_PORT * port->index + page_offset;
+ unsigned int value;
+ int ret, ret2;
+
+ guard(mutex)(&hub->page_lock);
+
+ ret = regmap_write(hub->regmap, RTS490X_PAGE_PTR, page);
+ if (ret)
+ goto restore_page;
+
+ ret = regmap_read(hub->regmap, RTS490X_TARGET_BUFFER_LENGTH, &value);
+ if (ret)
+ goto restore_page;
+
+ if (!value) {
+ ret = -ENODATA;
+ goto restore_page;
+ }
+
+ *length = value - 1;
+ if (*length > RTS490X_SMBUS_TARGET_PAYLOAD_SIZE) {
+ ret = -EMSGSIZE;
+ goto restore_page;
+ }
+
+ ret = regmap_read(hub->regmap, RTS490X_TARGET_BUFFER_ADDRESS, &value);
+ if (ret)
+ goto restore_page;
+
+ *address = value;
+ if (*length)
+ ret = regmap_bulk_read(hub->regmap, RTS490X_TARGET_BUFFER_DATA,
+ hub->target_buffer, *length);
+
+restore_page:
+ ret2 = regmap_write(hub->regmap, RTS490X_PAGE_PTR, 0);
+ return ret ?: ret2;
+}
+
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+static void rts490x_deliver_target_write(struct rts490x_port *port,
+ u8 address, unsigned int length)
+{
+ struct i2c_client *target = port->target;
+ u8 value = 0;
+ unsigned int i;
+ int ret;
+
+ if (!target || target->addr != address >> 1)
+ return;
+
+ ret = i2c_slave_event(target, I2C_SLAVE_WRITE_REQUESTED, &value);
+ if (!ret) {
+ for (i = 0; i < length; i++) {
+ value = port->hub->target_buffer[i];
+ ret = i2c_slave_event(target, I2C_SLAVE_WRITE_RECEIVED,
+ &value);
+ if (ret)
+ break;
+ }
+ }
+
+ i2c_slave_event(target, I2C_SLAVE_STOP, &value);
+}
+#endif
+
+static void rts490x_process_target_buffer(struct rts490x_port *port,
+ unsigned int page_offset, u8 flag)
+{
+ struct rts490x_hub *hub = port->hub;
+ unsigned int length = 0;
+ u8 address = 0;
+ int ret;
+
+ ret = rts490x_read_target_buffer(port, page_offset, &address, &length);
+ if (ret == -EMSGSIZE)
+ dev_warn_ratelimited(hub->dev,
+ "SMBus target message too large on port %u\n",
+ port->index);
+ else if (ret && ret != -ENODATA)
+ dev_warn_ratelimited(hub->dev,
+ "failed to read SMBus target buffer on port %u: %d\n",
+ port->index, ret);
+
+ if (regmap_write(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + port->index, flag))
+ dev_warn_ratelimited(hub->dev,
+ "failed to acknowledge SMBus target buffer on port %u\n",
+ port->index);
+
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ if (!ret)
+ rts490x_deliver_target_write(port, address, length);
+#endif
+}
+
+static void rts490x_process_smbus_target_status(struct rts490x_port *port,
+ unsigned int status)
+{
+ struct rts490x_hub *hub = port->hub;
+ int ret;
+
+ if (status & RTS490X_TARGET_BUFFER_0_RECEIVED)
+ rts490x_process_target_buffer(port,
+ RTS490X_TARGET_BUFFER_0_PAGE_OFFSET,
+ RTS490X_TARGET_BUFFER_0_RECEIVED);
+
+ if (status & RTS490X_TARGET_BUFFER_1_RECEIVED)
+ rts490x_process_target_buffer(port,
+ RTS490X_TARGET_BUFFER_1_PAGE_OFFSET,
+ RTS490X_TARGET_BUFFER_1_RECEIVED);
+
+ if (status & RTS490X_TARGET_BUFFER_OVERFLOW) {
+ ret = regmap_write(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
+ RTS490X_TARGET_BUFFER_OVERFLOW);
+ if (ret)
+ dev_warn_ratelimited(hub->dev,
+ "failed to clear SMBus target overflow on port %u: %d\n",
+ port->index, ret);
+ else
+ dev_warn_ratelimited(hub->dev,
+ "SMBus target buffer overflow on port %u\n",
+ port->index);
+ }
+}
+
+static void rts490x_complete_smbus_controller(struct rts490x_port *port,
+ unsigned int status)
+{
+ struct rts490x_hub *hub = port->hub;
+ unsigned long flags;
+ bool notify = false;
+ int ret;
+
+ if (!(status & RTS490X_CONTROLLER_FINISH))
+ return;
+
+ ret = regmap_write(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
+ RTS490X_CONTROLLER_FINISH);
+
+ spin_lock_irqsave(&hub->controller_lock, flags);
+ if (port->controller_pending) {
+ port->controller_status = status;
+ port->controller_error = ret;
+ port->controller_done = true;
+ notify = true;
+ } else if (port->controller_recovering && !ret) {
+ port->controller_recovering = false;
+ }
+ spin_unlock_irqrestore(&hub->controller_lock, flags);
+
+ if (ret)
+ dev_warn_ratelimited(hub->dev,
+ "failed to acknowledge SMBus controller completion on port %u: %d\n",
+ port->index, ret);
+
+ if (notify)
+ complete(&port->completion);
+}
+
+static void rts490x_process_smbus_status(struct rts490x_port *port)
+{
+ struct rts490x_hub *hub = port->hub;
+ unsigned int status;
+ int ret;
+
+ ret = regmap_read(hub->regmap,
+ RTS490X_TP0_SMBUS_AGENT_STATUS + port->index, &status);
+ if (ret) {
+ dev_warn_ratelimited(hub->dev,
+ "failed to read SMBus status on port %u: %d\n",
+ port->index, ret);
+ return;
+ }
+
+ rts490x_complete_smbus_controller(port, status);
+
+ guard(mutex)(&hub->target_lock);
+ rts490x_process_smbus_target_status(port, status);
+}
+
+static void rts490x_ibi_handler(struct i3c_device *i3cdev,
+ const struct i3c_ibi_payload *payload)
+{
+ struct rts490x *rts490x = i3cdev_get_drvdata(i3cdev);
+ struct rts490x_hub *hub = rts490x->hub;
+ const u8 *data = payload->data;
+ unsigned int pending;
+ unsigned int status;
+ int ret;
+ int i;
+
+ if (!hub)
+ return;
+
+ if (payload->len && data) {
+ status = data[0];
+ } else {
+ ret = regmap_read(hub->regmap, RTS490X_DEV_AND_IBI_STATUS, &status);
+ if (ret)
+ return;
+ }
+
+ if (!(status & RTS490X_SMBUS_AGENT_EVENT))
+ return;
+
+ if (payload->len >= RTS490X_IBI_MAX_PAYLOAD_LENGTH && data) {
+ pending = data[1];
+ } else {
+ ret = regmap_read(hub->regmap,
+ RTS490X_TP_SMBUS_AGENT_IBI_STATUS, &pending);
+ if (ret)
+ return;
+ }
+
+ pending &= hub->smbus_port_mask;
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++) {
+ if (pending & BIT(i))
+ rts490x_process_smbus_status(&hub->ports[i]);
+ }
+}
+
+static const struct i3c_ibi_setup rts490x_ibi_setup = {
+ .handler = rts490x_ibi_handler,
+ .max_payload_len = RTS490X_IBI_MAX_PAYLOAD_LENGTH,
+ .num_slots = RTS490X_IBI_SLOTS,
+};
+
+static int rts490x_set_smbus_ibi(struct rts490x_hub *hub, bool enable)
+{
+ unsigned int protection;
+ u8 value = enable ? hub->smbus_port_mask : 0;
+ bool restore;
+ int ret, ret2;
+
+ guard(mutex)(&hub->rts490x->protected_reg_lock);
+
+ ret = regmap_read(hub->regmap, RTS490X_PROTECTION_CODE, &protection);
+ if (ret)
+ return ret;
+
+ restore = protection != RTS490X_REGISTERS_UNLOCK_CODE;
+ if (restore) {
+ ret = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE,
+ RTS490X_REGISTERS_UNLOCK_CODE);
+ if (ret)
+ return ret;
+ }
+
+ ret = regmap_update_bits(hub->regmap,
+ RTS490X_TP_SMBUS_AGENT_IBI_CONFIG,
+ hub->smbus_port_mask, value);
+
+ if (restore) {
+ ret2 = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE, protection);
+ if (!ret && ret2)
+ ret = ret2;
+ }
+
+ return ret;
+}
+
+static void rts490x_free_ibi(void *data)
+{
+ struct rts490x_hub *hub = data;
+
+ hub->ibi_ready = false;
+ if (rts490x_set_smbus_ibi(hub, false))
+ dev_warn(hub->dev, "failed to disable SMBus event IBI\n");
+ if (i3c_device_disable_ibi(hub->rts490x->i3cdev))
+ dev_warn(hub->dev, "failed to disable hub IBI\n");
+ i3c_device_free_ibi(hub->rts490x->i3cdev);
+}
+
+int rts490x_setup_ibi(struct rts490x_hub *hub)
+{
+ int ret;
+
+ if (!hub->smbus_port_mask)
+ return 0;
+
+ ret = i3c_device_request_ibi(hub->rts490x->i3cdev, &rts490x_ibi_setup);
+ if (ret)
+ return dev_err_probe(hub->dev, ret, "failed to request hub IBI\n");
+
+ ret = i3c_device_enable_ibi(hub->rts490x->i3cdev);
+ if (ret) {
+ i3c_device_free_ibi(hub->rts490x->i3cdev);
+ return dev_err_probe(hub->dev, ret, "failed to enable hub IBI\n");
+ }
+
+ ret = devm_add_action_or_reset(hub->dev, rts490x_free_ibi, hub);
+ if (ret)
+ return ret;
+
+ ret = rts490x_set_smbus_ibi(hub, true);
+ if (ret)
+ return ret;
+
+ hub->ibi_ready = true;
+ return 0;
+}
+
+static void rts490x_unregister_smbus_adapter(struct rts490x_port *port)
+{
+ struct i2c_adapter *adapter = port->smbus_adapter;
+ struct fwnode_handle *fwnode;
+
+ if (!adapter)
+ return;
+
+ fwnode = dev_fwnode(&adapter->dev);
+ i2c_del_adapter(adapter);
+ fwnode_handle_put(fwnode);
+ port->smbus_adapter = NULL;
+ port->registered = false;
+}
+
+void rts490x_unregister_smbus_ports(void *data)
+{
+ struct rts490x_hub *hub = data;
+ int i;
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++)
+ rts490x_unregister_smbus_adapter(&hub->ports[i]);
+}
+
+int rts490x_register_smbus_ports(struct rts490x_hub *hub)
+{
+ struct rts490x_port *port;
+ struct i2c_adapter *adapter;
+ int ret;
+ int i;
+
+ for (i = 0; i < hub->rts490x->num_target_ports; i++) {
+ port = &hub->ports[i];
+ if (port->mode != RTS490X_PORT_SMBUS)
+ continue;
+
+ ret = devm_mutex_init(hub->dev, &port->xfer_lock);
+ if (ret)
+ return ret;
+
+ init_completion(&port->completion);
+
+ adapter = devm_kzalloc(hub->dev, sizeof(*adapter), GFP_KERNEL);
+ if (!adapter)
+ return -ENOMEM;
+
+ adapter->owner = THIS_MODULE;
+ adapter->algo = &rts490x_smbus_algorithm;
+ adapter->quirks = &rts490x_smbus_quirks;
+ adapter->dev.parent = hub->dev;
+ adapter->timeout = msecs_to_jiffies(RTS490X_SMBUS_TIMEOUT_MS);
+ adapter->retries = 3;
+ device_set_node(&adapter->dev, fwnode_handle_get(port->fwnode));
+ snprintf(adapter->name, sizeof(adapter->name),
+ "rts490x-i3c-hub target port %u", port->index);
+ i2c_set_adapdata(adapter, port);
+
+ port->smbus_adapter = adapter;
+ ret = i2c_add_adapter(adapter);
+ if (ret) {
+ fwnode_handle_put(dev_fwnode(&adapter->dev));
+ device_set_node(&adapter->dev, NULL);
+ port->smbus_adapter = NULL;
+ return dev_err_probe(hub->dev, ret,
+ "failed to register SMBus target port %u\n",
+ port->index);
+ }
+
+ port->registered = true;
+ }
+
+ return 0;
+}
diff --git a/drivers/i3c/hub/rts490x-i3c-hub.h b/drivers/i3c/hub/rts490x-i3c-hub.h
new file mode 100644
index 000000000000..cab489de0aa8
--- /dev/null
+++ b/drivers/i3c/hub/rts490x-i3c-hub.h
@@ -0,0 +1,150 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
+#ifndef _RTS490X_I3C_HUB_H
+#define _RTS490X_I3C_HUB_H
+
+#include <linux/bits.h>
+#include <linux/completion.h>
+#include <linux/i2c.h>
+#include <linux/i3c/hub.h>
+#include <linux/mfd/rts490x.h>
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
+
+#define RTS490X_SMBUS_TARGET_PAYLOAD_SIZE 78
+
+#define RTS490X_TP_SMBUS_AGENT_IBI_CONFIG 0x1b
+#define RTS490X_TP0_SMBUS_AGENT_STATUS 0x64
+#define RTS490X_TARGET_BUFFER_STATUS_MASK GENMASK(3, 1)
+
+struct device;
+struct fwnode_handle;
+struct regmap;
+struct rts490x;
+
+enum rts490x_port_mode {
+ RTS490X_PORT_UNUSED,
+ RTS490X_PORT_I3C,
+ RTS490X_PORT_SMBUS,
+};
+
+/**
+ * struct rts490x_config - static hub configuration
+ * @tp0145_pullup_ohms: pull-up resistance for target ports 0/1/4/5
+ * @tp2367_pullup_ohms: pull-up resistance for target ports 2/3/6/7
+ * @cp0_io_strength_ohms: controller port 0 output impedance
+ * @cp1_io_strength_ohms: controller port 1 output impedance
+ * @tp0145_io_strength_ohms: target ports 0/1/4/5 output impedance
+ * @tp2367_io_strength_ohms: target ports 2/3/6/7 output impedance
+ * @tp0145_pullup_configured: target port group 0/1/4/5 pull-up is configured
+ * @tp2367_pullup_configured: target port group 2/3/6/7 pull-up is configured
+ * @cp0_io_strength_configured: controller port 0 impedance is configured
+ * @cp1_io_strength_configured: controller port 1 impedance is configured
+ * @tp0145_io_strength_configured: target group 0/1/4/5 impedance is configured
+ * @tp2367_io_strength_configured: target group 2/3/6/7 impedance is configured
+ */
+struct rts490x_config {
+ u32 tp0145_pullup_ohms;
+ u32 tp2367_pullup_ohms;
+ u32 cp0_io_strength_ohms;
+ u32 cp1_io_strength_ohms;
+ u32 tp0145_io_strength_ohms;
+ u32 tp2367_io_strength_ohms;
+ bool tp0145_pullup_configured;
+ bool tp2367_pullup_configured;
+ bool cp0_io_strength_configured;
+ bool cp1_io_strength_configured;
+ bool tp0145_io_strength_configured;
+ bool tp2367_io_strength_configured;
+};
+
+/**
+ * struct rts490x_port - per-target-port state
+ * @hub_controller: generic I3C hub controller for an I3C port
+ * @fwnode: firmware node describing the target-port bus
+ * @smbus_adapter: adapter registered for an SMBus port
+ * @target: target client registered on an SMBus port
+ * @hub: parent RTS490x hub state
+ * @xfer_lock: serializes SMBus controller-agent transfers on this port
+ * @completion: signaled when the controller agent reports FINISH through IBI
+ * @controller_status: controller-agent status captured by the IBI handler
+ * @controller_error: IBI completion processing error
+ * @controller_pending: a controller-agent transfer is waiting for IBI
+ * @controller_done: the pending transfer has been completed by IBI
+ * @controller_recovering: reject new transfers until a late completion is acked
+ * @mode: target-port operating mode
+ * @clock_frequency: SMBus controller-agent bus rate in Hz
+ * @index: target-port index
+ * @mask: target-port register mask
+ * @pullup_enabled: use the on-chip target-port pull-up
+ * @registered: an I3C controller or SMBus adapter is registered for this port
+ * @always_enable: skip the enable_port/disable_port hooks; the route stays
+ * connected between operations
+ */
+struct rts490x_port {
+ struct i3c_hub_controller hub_controller;
+ struct fwnode_handle *fwnode;
+ struct i2c_adapter *smbus_adapter;
+ struct i2c_client *target;
+ struct rts490x_hub *hub;
+ /* Serializes controller-agent transactions on this target port. */
+ struct mutex xfer_lock;
+ struct completion completion;
+ unsigned int controller_status;
+ int controller_error;
+ bool controller_pending;
+ bool controller_done;
+ bool controller_recovering;
+ enum rts490x_port_mode mode;
+ u32 clock_frequency;
+ u8 index;
+ u8 mask;
+ bool pullup_enabled;
+ bool registered;
+ bool always_enable;
+};
+
+/**
+ * struct rts490x_hub - Realtek-specific I3C hub child state
+ * @dev: hub child device
+ * @rts490x: shared MFD state
+ * @regmap: register map shared with the MFD parent
+ * @hub: generic I3C hub context
+ * @page_lock: serializes paged-register transactions
+ * @controller_lock: protects controller completion state against timeout/IBI races
+ * @target_lock: serializes target registration and IBI delivery
+ * @target_buffer: shared buffer for SMBus target writes delivered by IBI
+ * @config: static hardware configuration
+ * @ports: per-target-port state
+ * @i3c_port_mask: target ports configured as I3C buses
+ * @smbus_port_mask: target ports configured as SMBus buses
+ * @ibi_ready: hub-device IBI has been requested and enabled
+ */
+struct rts490x_hub {
+ struct device *dev;
+ struct rts490x *rts490x;
+ struct regmap *regmap;
+ struct i3c_hub hub;
+ /* Serializes page selection and access across all target ports. */
+ struct mutex page_lock;
+ /* Protects controller completion state against timeout and IBI races. */
+ spinlock_t controller_lock;
+ /* Serializes target registration and IBI delivery. */
+ struct mutex target_lock;
+ u8 target_buffer[RTS490X_SMBUS_TARGET_PAYLOAD_SIZE];
+ struct rts490x_config config;
+ struct rts490x_port ports[RTS490X_MAX_TARGET_PORTS];
+ u8 i3c_port_mask;
+ u8 smbus_port_mask;
+ bool ibi_ready;
+};
+
+extern const struct i3c_hub_ops rts490x_hub_ops;
+
+int rts490x_register_i3c_ports(struct rts490x_hub *hub);
+int rts490x_setup_ibi(struct rts490x_hub *hub);
+int rts490x_register_smbus_ports(struct rts490x_hub *hub);
+void rts490x_unregister_smbus_ports(void *data);
+bool rts490x_smbus_frequency_supported(u32 frequency);
+
+#endif /* _RTS490X_I3C_HUB_H */
--
2.34.1
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators
2026-09-20 10:05 ` [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators zain_zhou
@ 2026-09-20 13:05 ` Krzysztof Kozlowski
2026-09-21 3:46 ` 回复: " 周寅
2026-09-21 10:23 ` Mark Brown
1 sibling, 1 reply; 10+ messages in thread
From: Krzysztof Kozlowski @ 2026-09-20 13:05 UTC (permalink / raw)
To: zain_zhou, Alexandre Belloni, Frank Li, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Lee Jones, Liam Girdwood,
Mark Brown, Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, Wei Wang
On 20/09/2026 12:05, zain_zhou@realsil.com.cn wrote:
> +
> + regulators = of_get_available_child_by_name(pdev->dev.of_node, "regulators");
> + if (!regulators)
> + return 0;
> +
> + config.dev = &pdev->dev;
> + config.regmap = rts490x->regmap;
> + config.driver_data = priv;
> +
> + for (i = 0; i < ARRAY_SIZE(rts490x_regulators); i++) {
> + struct device_node *node __free(device_node) = NULL;
> + const char *name = rts490x_regulators[i].of_match;
> +
> + node = of_get_available_child_by_name(regulators, name);
> + if (!node)
> + continue;
> +
> + config.of_node = node;
> + rdev = devm_regulator_register(&pdev->dev, &rts490x_regulators[i],
> + &config);
> + if (IS_ERR(rdev))
You leak OF nodes.
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 10+ messages in thread
* 回复: [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators
2026-09-20 13:05 ` Krzysztof Kozlowski
@ 2026-09-21 3:46 ` 周寅
2026-09-21 14:07 ` Krzysztof Kozlowski
0 siblings, 1 reply; 10+ messages in thread
From: 周寅 @ 2026-09-21 3:46 UTC (permalink / raw)
To: Krzysztof Kozlowski, Alexandre Belloni, Frank Li, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Lee Jones, Liam Girdwood,
Mark Brown, Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, 王炜
On Sun, Sep 20, 2026, Krzysztof Kozlowski wrote:
> You leak OF nodes.
Thanks for the review.
Both regulators and node are declared with __free(device_node), so the
references returned by of_get_available_child_by_name() should be
dropped on scope exit, including the continue and error paths.
Could you clarify which reference is leaking?
Best regards,
Zain
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators
2026-09-20 10:05 ` [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators zain_zhou
2026-09-20 13:05 ` Krzysztof Kozlowski
@ 2026-09-21 10:23 ` Mark Brown
1 sibling, 0 replies; 10+ messages in thread
From: Mark Brown @ 2026-09-21 10:23 UTC (permalink / raw)
To: zain_zhou
Cc: Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Vikash Bansal,
Aman Kumar Pandey, Lakshay Piplani, devicetree, linux-i3c,
linux-kernel, mfd, Wei Wang
[-- Attachment #1: Type: text/plain, Size: 1479 bytes --]
On Sun, Sep 20, 2026 at 06:05:00PM +0800, zain_zhou@realsil.com.cn wrote:
> Add regulator support for the four programmable LDO outputs in the
> Realtek RTS490x I3C hub. The driver exposes the LDO voltage and enable
> controls through the regulator framework while preserving protected
> register state across updates.
> +static int rts490x_regulator_unprotect(struct regulator_dev *rdev,
> + struct rts490x_reg_state *state)
> +{
> + int ret;
> +
> + state->restore = false;
> +
> + ret = regmap_read(rdev->regmap, RTS490X_PROTECTION_CODE,
> + &state->original);
> + if (ret)
> + return ret;
> +
> + if (state->original == RTS490X_REGISTERS_UNLOCK_CODE)
> + return 0;
> +
> + ret = regmap_write(rdev->regmap, RTS490X_PROTECTION_CODE,
> + RTS490X_REGISTERS_UNLOCK_CODE);
> + if (!ret)
> + state->restore = true;
> +
> + return ret;
> +}
What's going on with all this restore tracking?
> +static int rts490x_regulator_probe(struct platform_device *pdev)
> +{
> + if (!pdev->dev.of_node) {
> + dev_warn(&pdev->dev,
> + "no OF node; skipping on-chip LDO registration\n");
> + return 0;
> + }
The hardware is there no matter what, you should just load the driver so
the state is visible. Without constraints the hardware state won't be
touched.
> + regulators = of_get_available_child_by_name(pdev->dev.of_node, "regulators");
> + if (!regulators)
> + return 0;
The driver should just register all the regulators the silicon has
unconditionally.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: 回复: [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators
2026-09-21 3:46 ` 回复: " 周寅
@ 2026-09-21 14:07 ` Krzysztof Kozlowski
0 siblings, 0 replies; 10+ messages in thread
From: Krzysztof Kozlowski @ 2026-09-21 14:07 UTC (permalink / raw)
To: 周寅,
Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Mark Brown,
Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani
Cc: devicetree, linux-i3c, linux-kernel, mfd, 王炜
On 21/09/2026 05:46, 周寅 wrote:
> On Sun, Sep 20, 2026, Krzysztof Kozlowski wrote:
>> You leak OF nodes.
>
> Thanks for the review.
>
> Both regulators and node are declared with __free(device_node), so the
> references returned by of_get_available_child_by_name() should be
> dropped on scope exit, including the continue and error paths.
>
> Could you clarify which reference is leaking?
I missed the __free, but then why do you declare __free variable with
NULL as constructor? Don't grow this pattern. See cleanup.h (or
discussions on the lists).
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 4/4] i3c: hub: Add Realtek RTS490x I3C hub support
2026-09-20 10:05 ` [RFC PATCH 4/4] i3c: hub: Add Realtek RTS490x I3C hub support zain_zhou
@ 2026-09-21 18:42 ` Frank Li
0 siblings, 0 replies; 10+ messages in thread
From: Frank Li @ 2026-09-21 18:42 UTC (permalink / raw)
To: zain_zhou
Cc: Alexandre Belloni, Frank Li, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, Lee Jones, Liam Girdwood, Mark Brown,
Vikash Bansal, Aman Kumar Pandey, Lakshay Piplani, devicetree,
linux-i3c, linux-kernel, mfd, Wei Wang
On Sun, Sep 20, 2026 at 06:05:01PM +0800, zain_zhou@realsil.com.cn wrote:
> From: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Add I3C and SMBus downstream port support for the Realtek RTS490x
> family of I3C hubs.
>
> I3C ports are exposed through the generic I3C hub framework. SMBus
> ports are exposed as Linux I2C adapters and support controller-agent
> transfers and target receive events through the hub IBI.
>
> Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
> ---
> MAINTAINERS | 1 +
> drivers/i3c/hub/Kconfig | 11 +
> drivers/i3c/hub/Makefile | 3 +
> drivers/i3c/hub/rts490x-i3c-hub-core.c | 444 +++++++++++++
> drivers/i3c/hub/rts490x-i3c-hub-i3c.c | 150 +++++
> drivers/i3c/hub/rts490x-i3c-hub-smbus.c | 839 ++++++++++++++++++++++++
> drivers/i3c/hub/rts490x-i3c-hub.h | 150 +++++
> 7 files changed, 1598 insertions(+)
> create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-core.c
> create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-i3c.c
> create mode 100644 drivers/i3c/hub/rts490x-i3c-hub-smbus.c
> create mode 100644 drivers/i3c/hub/rts490x-i3c-hub.h
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 6e7cb4ca92f6..761a9d16305b 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -22998,6 +22998,7 @@ L: linux-i3c@lists.infradead.org
> L: linux-kernel@vger.kernel.org
> S: Maintained
> F: Documentation/devicetree/bindings/i3c/realtek,rts490x.yaml
> +F: drivers/i3c/hub/rts490x*
> F: drivers/mfd/rts490x-core.c
> F: drivers/regulator/rts490x-regulator.c
> F: include/linux/mfd/rts490x.h
> diff --git a/drivers/i3c/hub/Kconfig b/drivers/i3c/hub/Kconfig
> index 969bb1f637dd..e5f618d08c49 100644
> --- a/drivers/i3c/hub/Kconfig
> +++ b/drivers/i3c/hub/Kconfig
> @@ -9,3 +9,14 @@ config P3H2X4X_I3C_HUB
> connect to a host via I3C/I2C/SMBus and allow communication with
> multiple downstream peripherals. Say Y or M here to enable the
> P3H2x4x I3C HUB driver.
> +
> +config RTS490X_I3C_HUB
> + tristate "Realtek RTS490x I3C hub support"
> + depends on MFD_RTS490X
> + depends on I2C
> + select I3C_HUB
> + help
> + Enable support for the I3C and SMBus target ports provided by
> + Realtek RTS490x I3C hub devices. I3C ports are exposed through
> + the generic I3C hub framework, while SMBus ports are exposed as
> + Linux I2C adapters.
> diff --git a/drivers/i3c/hub/Makefile b/drivers/i3c/hub/Makefile
> index 9dbd8a7b4184..e9c8de3c021a 100644
> --- a/drivers/i3c/hub/Makefile
> +++ b/drivers/i3c/hub/Makefile
> @@ -2,3 +2,6 @@
> # Copyright 2025 NXP
> p3h2840_i3c_hub-y := p3h2840_i3c_hub_common.o p3h2840_i3c_hub_i3c.o p3h2840_i3c_hub_smbus.o
> obj-$(CONFIG_P3H2X4X_I3C_HUB) += p3h2840_i3c_hub.o
> +rts490x_i3c_hub-y := rts490x-i3c-hub-core.o rts490x-i3c-hub-i3c.o \
> + rts490x-i3c-hub-smbus.o
> +obj-$(CONFIG_RTS490X_I3C_HUB) += rts490x_i3c_hub.o
> diff --git a/drivers/i3c/hub/rts490x-i3c-hub-core.c b/drivers/i3c/hub/rts490x-i3c-hub-core.c
> new file mode 100644
> index 000000000000..b26977066ac7
> --- /dev/null
> +++ b/drivers/i3c/hub/rts490x-i3c-hub-core.c
> @@ -0,0 +1,444 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
> +
> +#include <linux/bitfield.h>
> +#include <linux/cleanup.h>
> +#include <linux/i3c/device.h>
> +#include <linux/iopoll.h>
> +#include <linux/mfd/rts490x.h>
> +#include <linux/of.h>
> +#include <linux/of_address.h>
> +#include <linux/platform_device.h>
> +#include <linux/property.h>
> +#include <linux/regmap.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/util_macros.h>
> +
> +#include "rts490x-i3c-hub.h"
> +
> +#define RTS490X_TP_ENABLE 0x12
> +#define RTS490X_IO_STRENGTH 0x14
> +#define RTS490X_TP_IO_MODE_CONF 0x17
> +#define RTS490X_TP_SMBUS_AGENT_ENABLE 0x18
> +#define RTS490X_CP_MUX_SET 0x38
> +#define RTS490X_CP_MUX_STATUS 0x39
> +#define RTS490X_CONTROLLER_PORT_MUX_REQ BIT(0)
> +#define RTS490X_CONTROLLER_PORT_MUXED BIT(0)
> +#define RTS490X_TP_PULLUP_ENABLE 0x53
> +#define RTS490X_ONCHIP_TD_AND_SMBUS_AGENT_CONF 0x6c
> +#define RTS490X_TARGET_AGENT_BUF_FULL_SDA_LOW_EN BIT(5)
> +
This register is the exactly same as P3H2X4X. Can you direct reuse
p3h2x*'s driver.
Frank
> +#define RTS490X_TP0145_IO_STRENGTH_MASK GENMASK(1, 0)
> +#define RTS490X_TP2367_IO_STRENGTH_MASK GENMASK(3, 2)
> +#define RTS490X_CP0_IO_STRENGTH_MASK GENMASK(5, 4)
> +#define RTS490X_CP1_IO_STRENGTH_MASK GENMASK(7, 6)
> +#define RTS490X_TP0145_PULLUP_MASK GENMASK(7, 6)
> +#define RTS490X_TP2367_PULLUP_MASK GENMASK(5, 4)
> +
> +#define RTS490X_SMBUS_DEFAULT_FREQUENCY 400000
> +#define RTS490X_CP_MUX_POLL_US 100
> +#define RTS490X_CP_MUX_TIMEOUT_US 100000
> +
> +static void rts490x_put_port_fwnodes(void *data)
> +{
> + struct rts490x_hub *hub = data;
> + int i;
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++) {
> + fwnode_handle_put(hub->ports[i].fwnode);
> + hub->ports[i].fwnode = NULL;
> + }
> +}
> +
> +static void rts490x_clear_hub_backpointer(void *data)
> +{
> + struct rts490x *rts490x = data;
> +
> + rts490x->hub = NULL;
> +}
> +
> +static int rts490x_read_config(struct rts490x_hub *hub)
> +{
> + struct rts490x_config *config = &hub->config;
> + struct device *dev = hub->dev;
> +
> + config->tp0145_pullup_configured =
> + !device_property_read_u32(dev, "realtek,tp0145-pullup-ohms",
> + &config->tp0145_pullup_ohms);
> + config->tp2367_pullup_configured =
> + !device_property_read_u32(dev, "realtek,tp2367-pullup-ohms",
> + &config->tp2367_pullup_ohms);
> + config->cp0_io_strength_configured =
> + !device_property_read_u32(dev, "realtek,cp0-io-strength-ohms",
> + &config->cp0_io_strength_ohms);
> + config->cp1_io_strength_configured =
> + !device_property_read_u32(dev, "realtek,cp1-io-strength-ohms",
> + &config->cp1_io_strength_ohms);
> + config->tp0145_io_strength_configured =
> + !device_property_read_u32(dev, "realtek,tp0145-io-strength-ohms",
> + &config->tp0145_io_strength_ohms);
> + config->tp2367_io_strength_configured =
> + !device_property_read_u32(dev, "realtek,tp2367-io-strength-ohms",
> + &config->tp2367_io_strength_ohms);
> +
> + return 0;
> +}
> +
> +static int rts490x_parse_ports(struct rts490x_hub *hub)
> +{
> + struct device_node *node = dev_of_node(hub->dev);
> + struct rts490x_port *port;
> + u64 index;
> + int ret;
> +
> + for_each_available_child_of_node_scoped(node, child) {
> + enum rts490x_port_mode mode;
> +
> + if (of_node_name_eq(child, "i3c"))
> + mode = RTS490X_PORT_I3C;
> + else if (of_node_name_eq(child, "smbus"))
> + mode = RTS490X_PORT_SMBUS;
> + else
> + continue;
> +
> + ret = of_property_read_reg(child, 0, &index, NULL);
> + if (ret)
> + return dev_err_probe(hub->dev, ret,
> + "failed to read reg for %pOF\n", child);
> +
> + if (index >= hub->rts490x->num_target_ports)
> + return dev_err_probe(hub->dev, -EINVAL,
> + "invalid target port %llu\n", index);
> +
> + port = &hub->ports[index];
> + if (port->fwnode)
> + return dev_err_probe(hub->dev, -EINVAL,
> + "duplicate target port %llu\n", index);
> +
> + port->fwnode = fwnode_handle_get(of_fwnode_handle(child));
> + port->hub = hub;
> + port->index = index;
> + port->mask = BIT(index);
> + port->mode = mode;
> + port->pullup_enabled =
> + of_property_read_bool(child, "realtek,pullup-enable");
> +
> + if (mode == RTS490X_PORT_SMBUS) {
> + port->clock_frequency = RTS490X_SMBUS_DEFAULT_FREQUENCY;
> + of_property_read_u32(child, "clock-frequency",
> + &port->clock_frequency);
> + if (!rts490x_smbus_frequency_supported(port->clock_frequency))
> + return dev_err_probe(hub->dev, -EINVAL,
> + "unsupported SMBus frequency %u Hz on port %llu\n",
> + port->clock_frequency, index);
> + }
> +
> + if (mode == RTS490X_PORT_I3C)
> + hub->i3c_port_mask |= port->mask;
> + else
> + hub->smbus_port_mask |= port->mask;
> + }
> +
> + return 0;
> +}
> +
> +static u8 rts490x_pullup_to_reg(u32 pullup_ohms)
> +{
> + static const int pullup_table[] = { 250, 500, 1000, 2000 };
> +
> + return find_closest(pullup_ohms, pullup_table,
> + ARRAY_SIZE(pullup_table));
> +}
> +
> +static u8 rts490x_io_strength_to_reg(u32 io_strength_ohms)
> +{
> + static const int io_strength_table[] = { 20, 30, 40, 50 };
> +
> + return find_closest(io_strength_ohms, io_strength_table,
> + ARRAY_SIZE(io_strength_table));
> +}
> +
> +static int rts490x_enable_supplies(struct rts490x_hub *hub)
> +{
> + static const char * const supplies[] = {
> + "vcc-cp0",
> + "vcc-cp1",
> + "vcc-tp0145",
> + "vcc-tp2367",
> + };
> + int ret;
> + int i;
> +
> + for (i = 0; i < ARRAY_SIZE(supplies); i++) {
> + ret = devm_regulator_get_enable_optional(hub->dev, supplies[i]);
> + if (ret && ret != -ENODEV)
> + return dev_err_probe(hub->dev, ret,
> + "failed to enable %s supply\n",
> + supplies[i]);
> + }
> +
> + return 0;
> +}
> +
> +static int rts490x_configure_pullups(struct rts490x_hub *hub)
> +{
> + struct rts490x_config *config = &hub->config;
> + u8 mask = 0;
> + u8 pullup = 0;
> +
> + if (config->tp0145_pullup_configured) {
> + mask |= RTS490X_TP0145_PULLUP_MASK;
> + pullup |= FIELD_PREP(RTS490X_TP0145_PULLUP_MASK,
> + rts490x_pullup_to_reg(config->tp0145_pullup_ohms));
> + }
> +
> + if (config->tp2367_pullup_configured) {
> + mask |= RTS490X_TP2367_PULLUP_MASK;
> + pullup |= FIELD_PREP(RTS490X_TP2367_PULLUP_MASK,
> + rts490x_pullup_to_reg(config->tp2367_pullup_ohms));
> + }
> +
> + return regmap_update_bits(hub->regmap, RTS490X_LDO_AND_PULLUP_CONF,
> + mask, pullup);
> +}
> +
> +static int rts490x_configure_io_strength(struct rts490x_hub *hub)
> +{
> + struct rts490x_config *config = &hub->config;
> + u8 strength = 0;
> + u8 mask = 0;
> +
> + if (config->cp0_io_strength_configured) {
> + u8 value = rts490x_io_strength_to_reg(config->cp0_io_strength_ohms);
> +
> + mask |= RTS490X_CP0_IO_STRENGTH_MASK;
> + strength |= FIELD_PREP(RTS490X_CP0_IO_STRENGTH_MASK, value);
> + }
> +
> + if (config->cp1_io_strength_configured) {
> + u8 value = rts490x_io_strength_to_reg(config->cp1_io_strength_ohms);
> +
> + mask |= RTS490X_CP1_IO_STRENGTH_MASK;
> + strength |= FIELD_PREP(RTS490X_CP1_IO_STRENGTH_MASK, value);
> + }
> +
> + if (config->tp0145_io_strength_configured) {
> + u8 value = rts490x_io_strength_to_reg(config->tp0145_io_strength_ohms);
> +
> + mask |= RTS490X_TP0145_IO_STRENGTH_MASK;
> + strength |= FIELD_PREP(RTS490X_TP0145_IO_STRENGTH_MASK, value);
> + }
> +
> + if (config->tp2367_io_strength_configured) {
> + u8 value = rts490x_io_strength_to_reg(config->tp2367_io_strength_ohms);
> +
> + mask |= RTS490X_TP2367_IO_STRENGTH_MASK;
> + strength |= FIELD_PREP(RTS490X_TP2367_IO_STRENGTH_MASK, value);
> + }
> +
> + return regmap_update_bits(hub->regmap, RTS490X_IO_STRENGTH,
> + mask, strength);
> +}
> +
> +static int rts490x_configure_ports(struct rts490x_hub *hub)
> +{
> + u8 port_mask = GENMASK(hub->rts490x->num_target_ports - 1, 0);
> + u8 pullup_mask = 0;
> + int ret;
> + int i;
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++) {
> + if (hub->ports[i].pullup_enabled)
> + pullup_mask |= hub->ports[i].mask;
> + }
> +
> + ret = regmap_update_bits(hub->regmap, RTS490X_TP_PULLUP_ENABLE,
> + port_mask, pullup_mask);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(hub->regmap, RTS490X_TP_IO_MODE_CONF,
> + port_mask, hub->smbus_port_mask);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(hub->regmap, RTS490X_TP_SMBUS_AGENT_ENABLE,
> + port_mask, hub->smbus_port_mask);
> + if (ret)
> + return ret;
> +
> + ret = regmap_clear_bits(hub->regmap,
> + RTS490X_TP_SMBUS_AGENT_IBI_CONFIG, port_mask);
> + if (ret)
> + return ret;
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++) {
> + if (!(hub->smbus_port_mask & BIT(i)))
> + continue;
> +
> + ret = regmap_write(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + i,
> + RTS490X_TARGET_BUFFER_STATUS_MASK);
> + if (ret)
> + return ret;
> + }
> +
> + if (hub->i3c_port_mask) {
> + unsigned int status;
> +
> + ret = regmap_write(hub->regmap, RTS490X_CP_MUX_SET,
> + RTS490X_CONTROLLER_PORT_MUX_REQ);
> + if (ret)
> + return ret;
> +
> + ret = regmap_read_poll_timeout(hub->regmap, RTS490X_CP_MUX_STATUS,
> + status,
> + status & RTS490X_CONTROLLER_PORT_MUXED,
> + RTS490X_CP_MUX_POLL_US,
> + RTS490X_CP_MUX_TIMEOUT_US);
> + if (ret)
> + return ret;
> + }
> +
> + return regmap_update_bits(hub->regmap, RTS490X_TP_ENABLE, port_mask,
> + hub->i3c_port_mask | hub->smbus_port_mask);
> +}
> +
> +static int rts490x_configure_hardware(struct rts490x_hub *hub)
> +{
> + unsigned int protection;
> + bool restore;
> + int ret, ret2;
> +
> + ret = rts490x_enable_supplies(hub);
> + if (ret)
> + return ret;
> +
> + mutex_lock(&hub->rts490x->protected_reg_lock);
> +
> + ret = regmap_read(hub->regmap, RTS490X_PROTECTION_CODE, &protection);
> + if (ret)
> + goto unlock;
> +
> + restore = protection != RTS490X_REGISTERS_UNLOCK_CODE;
> + if (restore) {
> + ret = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE,
> + RTS490X_REGISTERS_UNLOCK_CODE);
> + if (ret)
> + goto unlock;
> + }
> +
> + ret = rts490x_configure_pullups(hub);
> + if (!ret)
> + ret = rts490x_configure_io_strength(hub);
> + if (!ret)
> + ret = rts490x_configure_ports(hub);
> + if (!ret && hub->smbus_port_mask) {
> + /* Hold SDA low when both target buffers are full. */
> + ret = regmap_set_bits(hub->regmap,
> + RTS490X_ONCHIP_TD_AND_SMBUS_AGENT_CONF,
> + RTS490X_TARGET_AGENT_BUF_FULL_SDA_LOW_EN);
> + }
> +
> + if (restore) {
> + ret2 = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE,
> + protection);
> + if (!ret && ret2)
> + ret = ret2;
> + }
> +
> +unlock:
> + mutex_unlock(&hub->rts490x->protected_reg_lock);
> +
> + return ret;
> +}
> +
> +static int rts490x_hub_probe(struct platform_device *pdev)
> +{
> + struct rts490x *rts490x = dev_get_drvdata(pdev->dev.parent);
> + struct rts490x_hub *hub;
> + int ret;
> +
> + if (!rts490x || !rts490x->i3cdev)
> + return -ENODEV;
> +
> + hub = devm_kzalloc(&pdev->dev, sizeof(*hub), GFP_KERNEL);
> + if (!hub)
> + return -ENOMEM;
> +
> + hub->dev = &pdev->dev;
> + hub->rts490x = rts490x;
> + hub->regmap = rts490x->regmap;
> + platform_set_drvdata(pdev, hub);
> + device_set_of_node_from_dev(&pdev->dev, pdev->dev.parent);
> +
> + ret = devm_mutex_init(&pdev->dev, &hub->page_lock);
> + if (ret)
> + return ret;
> +
> + ret = devm_mutex_init(&pdev->dev, &hub->target_lock);
> + if (ret)
> + return ret;
> +
> + spin_lock_init(&hub->controller_lock);
> +
> + ret = rts490x_read_config(hub);
> + if (ret)
> + return ret;
> +
> + ret = rts490x_parse_ports(hub);
> + if (ret)
> + return ret;
> +
> + ret = devm_add_action_or_reset(&pdev->dev, rts490x_put_port_fwnodes, hub);
> + if (ret)
> + return ret;
> +
> + ret = rts490x_configure_hardware(hub);
> + if (ret)
> + return dev_err_probe(&pdev->dev, ret,
> + "failed to configure hub hardware\n");
> +
> + i3c_hub_init(&hub->hub, &rts490x_hub_ops, rts490x->i3cdev);
> +
> + /* The IBI handler obtains the child context through the MFD parent. */
> + rts490x->hub = hub;
> + ret = devm_add_action_or_reset(&pdev->dev,
> + rts490x_clear_hub_backpointer, rts490x);
> + if (ret)
> + return ret;
> +
> + ret = rts490x_register_i3c_ports(hub);
> + if (ret)
> + return ret;
> +
> + ret = rts490x_setup_ibi(hub);
> + if (ret)
> + return ret;
> +
> + ret = rts490x_register_smbus_ports(hub);
> + if (ret) {
> + rts490x_unregister_smbus_ports(hub);
> + return ret;
> + }
> +
> + return devm_add_action_or_reset(&pdev->dev,
> + rts490x_unregister_smbus_ports, hub);
> +}
> +
> +static const struct platform_device_id rts490x_hub_ids[] = {
> + { "rts490x-i3c-hub" },
> + { }
> +};
> +MODULE_DEVICE_TABLE(platform, rts490x_hub_ids);
> +
> +static struct platform_driver rts490x_hub_driver = {
> + .driver.name = "rts490x-i3c-hub",
> + .probe = rts490x_hub_probe,
> + .id_table = rts490x_hub_ids,
> +};
> +module_platform_driver(rts490x_hub_driver);
> +
> +MODULE_AUTHOR("Yin Zhou <zain_zhou@realsil.com.cn>");
> +MODULE_DESCRIPTION("Realtek RTS490x I3C hub driver");
> +MODULE_LICENSE("GPL");
> diff --git a/drivers/i3c/hub/rts490x-i3c-hub-i3c.c b/drivers/i3c/hub/rts490x-i3c-hub-i3c.c
> new file mode 100644
> index 000000000000..7ddd7d171ef0
> --- /dev/null
> +++ b/drivers/i3c/hub/rts490x-i3c-hub-i3c.c
> @@ -0,0 +1,150 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
> +
> +#include <linux/i3c/device.h>
> +#include <linux/mfd/rts490x.h>
> +#include <linux/regmap.h>
> +
> +#include "rts490x-i3c-hub.h"
> +
> +#define RTS490X_TP_NET_CON_CONF 0x51
> +
> +static struct rts490x_port *
> +rts490x_port_from_controller(struct i3c_master_controller *controller)
> +{
> + struct i3c_hub_controller *hub_controller;
> +
> + hub_controller = container_of(controller, struct i3c_hub_controller,
> + controller);
> +
> + return container_of(hub_controller, struct rts490x_port, hub_controller);
> +}
> +
> +static int rts490x_set_port_connected(struct rts490x_port *port, bool connected)
> +{
> + if (connected)
> + return regmap_set_bits(port->hub->regmap, RTS490X_TP_NET_CON_CONF,
> + port->mask);
> +
> + return regmap_clear_bits(port->hub->regmap, RTS490X_TP_NET_CON_CONF,
> + port->mask);
> +}
> +
> +static void rts490x_enable_port(struct i3c_master_controller *controller)
> +{
> + struct rts490x_port *port = rts490x_port_from_controller(controller);
> + int ret;
> +
> + if (port->always_enable)
> + return;
> +
> + ret = rts490x_set_port_connected(port, true);
> + if (ret)
> + dev_warn(port->hub->dev, "failed to connect target port %u: %d\n",
> + port->index, ret);
> +}
> +
> +static void rts490x_disable_port(struct i3c_master_controller *controller)
> +{
> + struct rts490x_port *port = rts490x_port_from_controller(controller);
> + int ret;
> +
> + if (port->always_enable)
> + return;
> +
> + ret = rts490x_set_port_connected(port, false);
> + if (ret)
> + dev_warn(port->hub->dev, "failed to disconnect target port %u: %d\n",
> + port->index, ret);
> +}
> +
> +const struct i3c_hub_ops rts490x_hub_ops = {
> + .enable_port = rts490x_enable_port,
> + .disable_port = rts490x_disable_port,
> +};
> +
> +static void rts490x_unregister_i3c_controller(void *data)
> +{
> + struct i3c_master_controller *controller = data;
> +
> + i3c_master_unregister(controller);
> +}
> +
> +static void rts490x_disable_i3c_ports(void *data)
> +{
> + struct rts490x_hub *hub = data;
> + int i;
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++)
> + hub->ports[i].always_enable = false;
> +
> + if (regmap_clear_bits(hub->regmap, RTS490X_TP_NET_CON_CONF,
> + hub->i3c_port_mask))
> + dev_warn(hub->dev, "failed to disconnect I3C target ports\n");
> +}
> +
> +static int rts490x_reserve_port_addresses(struct rts490x_port *port)
> +{
> + return i3c_hub_reserve_parent_addrslots_from_dt(&port->hub_controller,
> + to_of_node(port->fwnode));
> +}
> +
> +static int rts490x_register_port_controller(struct rts490x_port *port)
> +{
> + return i3c_master_register_fwnode(&port->hub_controller.controller,
> + port->hub->dev, port->fwnode,
> + &i3c_hub_master_ops, false);
> +}
> +
> +int rts490x_register_i3c_ports(struct rts490x_hub *hub)
> +{
> + struct i3c_master_controller *parent;
> + struct rts490x_port *port;
> + int ret;
> + int i;
> +
> + parent = i3c_dev_get_master(hub->rts490x->i3cdev->desc);
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++) {
> + port = &hub->ports[i];
> + if (port->mode != RTS490X_PORT_I3C)
> + continue;
> +
> + port->hub_controller.parent = parent;
> + port->hub_controller.hub = &hub->hub;
> + dev_set_drvdata(&port->hub_controller.controller.dev,
> + &port->hub_controller);
> +
> + ret = rts490x_reserve_port_addresses(port);
> + if (ret)
> + return ret;
> +
> + ret = rts490x_register_port_controller(port);
> + if (ret)
> + return ret;
> +
> + ret = devm_add_action_or_reset(hub->dev,
> + rts490x_unregister_i3c_controller,
> + &port->hub_controller.controller);
> + if (ret)
> + return ret;
> +
> + port->registered = true;
> + port->always_enable = true;
> + }
> +
> + if (hub->i3c_port_mask) {
> + ret = regmap_set_bits(hub->regmap, RTS490X_TP_NET_CON_CONF,
> + hub->i3c_port_mask);
> + if (ret)
> + return dev_err_probe(hub->dev, ret,
> + "failed to connect I3C target ports\n");
> +
> + ret = devm_add_action_or_reset(hub->dev,
> + rts490x_disable_i3c_ports, hub);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> diff --git a/drivers/i3c/hub/rts490x-i3c-hub-smbus.c b/drivers/i3c/hub/rts490x-i3c-hub-smbus.c
> new file mode 100644
> index 000000000000..88db3879660a
> --- /dev/null
> +++ b/drivers/i3c/hub/rts490x-i3c-hub-smbus.c
> @@ -0,0 +1,839 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
> +
> +#include <linux/bitfield.h>
> +#include <linux/cleanup.h>
> +#include <linux/i2c.h>
> +#include <linux/i3c/device.h>
> +#include <linux/mfd/rts490x.h>
> +#include <linux/regmap.h>
> +
> +#include "rts490x-i3c-hub.h"
> +
> +#define RTS490X_DEV_AND_IBI_STATUS 0x20
> +#define RTS490X_TP_SMBUS_AGENT_IBI_STATUS 0x21
> +#define RTS490X_TP_SMBUS_AGENT_TRANS_START 0x50
> +#define RTS490X_PAGE_PTR 0x7f
> +#define RTS490X_CONTROLLER_BUFFER_PAGE 0x10
> +#define RTS490X_CONTROLLER_PAGES_PER_PORT 4
> +#define RTS490X_TARGET_BUFFER_0_PAGE_OFFSET 2
> +#define RTS490X_TARGET_BUFFER_1_PAGE_OFFSET 3
> +#define RTS490X_CONTROLLER_AGENT_BUFFER 0x80
> +#define RTS490X_CONTROLLER_AGENT_BUFFER_DATA 0x84
> +#define RTS490X_TARGET_BUFFER_LENGTH 0x80
> +#define RTS490X_TARGET_BUFFER_ADDRESS 0x81
> +#define RTS490X_TARGET_BUFFER_DATA 0x82
> +#define RTS490X_CONTROLLER_BUFFER_SIZE 88
> +#define RTS490X_SMBUS_DESCRIPTOR_SIZE 4
> +#define RTS490X_SMBUS_PAYLOAD_SIZE \
> + (RTS490X_CONTROLLER_BUFFER_SIZE - RTS490X_SMBUS_DESCRIPTOR_SIZE)
> +
> +#define RTS490X_SMBUS_RATE_100KHZ 0
> +#define RTS490X_SMBUS_RATE_200KHZ BIT(1)
> +#define RTS490X_SMBUS_RATE_400KHZ BIT(2)
> +#define RTS490X_SMBUS_RATE_1MHZ (BIT(1) | BIT(2))
> +#define RTS490X_SMBUS_REPEATED_START BIT(0)
> +
> +#define RTS490X_SMBUS_AGENT_EVENT BIT(4)
> +#define RTS490X_CONTROLLER_FINISH BIT(0)
> +#define RTS490X_TARGET_BUFFER_0_RECEIVED BIT(1)
> +#define RTS490X_TARGET_BUFFER_1_RECEIVED BIT(2)
> +#define RTS490X_TARGET_BUFFER_OVERFLOW BIT(3)
> +#define RTS490X_CONTROLLER_RETURN_CODE_MASK GENMASK(7, 4)
> +#define RTS490X_CONTROLLER_SUCCESS 0
> +#define RTS490X_CONTROLLER_ADDRESS_NACK 1
> +#define RTS490X_CONTROLLER_DEVICE_BUSY 2
> +#define RTS490X_CONTROLLER_READ_NOT_READY 3
> +#define RTS490X_CONTROLLER_SYNC_RECOVERED 4
> +#define RTS490X_CONTROLLER_SYNC_BUS_CLEAR 5
> +#define RTS490X_CONTROLLER_BUS_FAULT 6
> +#define RTS490X_CONTROLLER_ARBITRATION_LOST 7
> +#define RTS490X_CONTROLLER_SCL_TIMEOUT 8
> +
> +#define RTS490X_SMBUS_TIMEOUT_MS 1000
> +#define RTS490X_IBI_MAX_PAYLOAD_LENGTH 2
> +#define RTS490X_IBI_SLOTS 6
> +
> +#define RTS490X_I2C_SUPPORTED_FLAGS (I2C_M_RD | I2C_M_DMA_SAFE)
> +
> +enum rts490x_smbus_xfer_type {
> + RTS490X_SMBUS_WRITE,
> + RTS490X_SMBUS_READ,
> + RTS490X_SMBUS_WRITE_READ,
> +};
> +
> +enum rts490x_smbus_descriptor_index {
> + RTS490X_SMBUS_DESC_ADDRESS,
> + RTS490X_SMBUS_DESC_TYPE,
> + RTS490X_SMBUS_DESC_WRITE_LENGTH,
> + RTS490X_SMBUS_DESC_READ_LENGTH,
> +};
> +
> +bool rts490x_smbus_frequency_supported(u32 frequency)
> +{
> + switch (frequency) {
> + case 100000:
> + case 200000:
> + case 400000:
> + case 1000000:
> + return true;
> + default:
> + return false;
> + }
> +}
> +
> +static u8 rts490x_smbus_frequency_to_reg(u32 frequency)
> +{
> + switch (frequency) {
> + case 100000:
> + return RTS490X_SMBUS_RATE_100KHZ;
> + case 200000:
> + return RTS490X_SMBUS_RATE_200KHZ;
> + case 1000000:
> + return RTS490X_SMBUS_RATE_1MHZ;
> + default:
> + return RTS490X_SMBUS_RATE_400KHZ;
> + }
> +}
> +
> +static int rts490x_smbus_status_to_errno(struct rts490x_port *port,
> + unsigned int status)
> +{
> + u8 code = FIELD_GET(RTS490X_CONTROLLER_RETURN_CODE_MASK, status);
> +
> + switch (code) {
> + case RTS490X_CONTROLLER_SUCCESS:
> + return 0;
> + case RTS490X_CONTROLLER_ADDRESS_NACK:
> + case RTS490X_CONTROLLER_READ_NOT_READY:
> + return -ENXIO;
> + case RTS490X_CONTROLLER_DEVICE_BUSY:
> + return -EREMOTEIO;
> + case RTS490X_CONTROLLER_SYNC_RECOVERED:
> + case RTS490X_CONTROLLER_SYNC_BUS_CLEAR:
> + case RTS490X_CONTROLLER_ARBITRATION_LOST:
> + return -EAGAIN;
> + case RTS490X_CONTROLLER_SCL_TIMEOUT:
> + return -ETIMEDOUT;
> + case RTS490X_CONTROLLER_BUS_FAULT:
> + default:
> + dev_err(port->hub->dev,
> + "SMBus controller error %#x on target port %u\n",
> + code, port->index);
> + return -EIO;
> + }
> +}
> +
> +static int rts490x_smbus_recover_controller(struct rts490x_port *port)
> +{
> + struct rts490x_hub *hub = port->hub;
> + unsigned long flags;
> + unsigned int status;
> + bool recovering;
> + int ret;
> +
> + spin_lock_irqsave(&hub->controller_lock, flags);
> + recovering = port->controller_recovering;
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> + if (!recovering)
> + return 0;
> +
> + ret = regmap_read(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + port->index, &status);
> + if (ret)
> + return ret;
> +
> + if (status & RTS490X_CONTROLLER_FINISH) {
> + ret = regmap_write(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
> + RTS490X_CONTROLLER_FINISH);
> + if (ret)
> + return ret;
> +
> + spin_lock_irqsave(&hub->controller_lock, flags);
> + port->controller_recovering = false;
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> + return 0;
> + }
> +
> + spin_lock_irqsave(&hub->controller_lock, flags);
> + recovering = port->controller_recovering;
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> +
> + return recovering ? -EAGAIN : 0;
> +}
> +
> +static int rts490x_smbus_wait_for_completion(struct rts490x_port *port)
> +{
> + struct rts490x_hub *hub = port->hub;
> + unsigned long flags;
> + unsigned long timeout;
> + unsigned int status;
> + bool done;
> + int error;
> +
> + timeout = wait_for_completion_timeout(&port->completion,
> + port->smbus_adapter->timeout);
> +
> + spin_lock_irqsave(&hub->controller_lock, flags);
> + done = port->controller_done;
> + if (!done) {
> + port->controller_pending = false;
> + if (!timeout)
> + port->controller_recovering = true;
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> + return timeout ? -EIO : -ETIMEDOUT;
> + }
> +
> + port->controller_pending = false;
> + port->controller_done = false;
> + status = port->controller_status;
> + error = port->controller_error;
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> +
> + if (error)
> + return error;
> +
> + return rts490x_smbus_status_to_errno(port, status);
> +}
> +
> +static int
> +rts490x_smbus_write_controller_buffer(struct rts490x_port *port,
> + const u8 *descriptor, const u8 *data,
> + unsigned int length)
> +{
> + struct rts490x_hub *hub = port->hub;
> + u8 page = RTS490X_CONTROLLER_BUFFER_PAGE +
> + RTS490X_CONTROLLER_PAGES_PER_PORT * port->index;
> + int ret, ret2;
> +
> + guard(mutex)(&hub->page_lock);
> +
> + ret = regmap_write(hub->regmap, RTS490X_PAGE_PTR, page);
> + if (!ret)
> + ret = regmap_bulk_write(hub->regmap,
> + RTS490X_CONTROLLER_AGENT_BUFFER,
> + descriptor, RTS490X_SMBUS_DESCRIPTOR_SIZE);
> + if (!ret && length)
> + ret = regmap_bulk_write(hub->regmap,
> + RTS490X_CONTROLLER_AGENT_BUFFER_DATA,
> + data, length);
> +
> + ret2 = regmap_write(hub->regmap, RTS490X_PAGE_PTR, 0);
> + return ret ?: ret2;
> +}
> +
> +static int
> +rts490x_smbus_read_controller_buffer(struct rts490x_port *port, u8 *data,
> + unsigned int offset, unsigned int length)
> +{
> + struct rts490x_hub *hub = port->hub;
> + u8 page = RTS490X_CONTROLLER_BUFFER_PAGE +
> + RTS490X_CONTROLLER_PAGES_PER_PORT * port->index;
> + int ret, ret2;
> +
> + guard(mutex)(&hub->page_lock);
> +
> + ret = regmap_write(hub->regmap, RTS490X_PAGE_PTR, page);
> + if (!ret)
> + ret = regmap_bulk_read(hub->regmap,
> + RTS490X_CONTROLLER_AGENT_BUFFER_DATA + offset,
> + data, length);
> +
> + ret2 = regmap_write(hub->regmap, RTS490X_PAGE_PTR, 0);
> + return ret ?: ret2;
> +}
> +
> +static int rts490x_smbus_xfer_one(struct rts490x_port *port,
> + struct i2c_msg *write,
> + struct i2c_msg *read,
> + enum rts490x_smbus_xfer_type type)
> +{
> + struct rts490x_hub *hub = port->hub;
> + u8 descriptor[RTS490X_SMBUS_DESCRIPTOR_SIZE] = { };
> + unsigned long flags;
> + u8 *read_buffer = NULL;
> + u8 address;
> + unsigned int write_length = write ? write->len : 0;
> + unsigned int read_length = read ? read->len : 0;
> + unsigned int status_reg = RTS490X_TP0_SMBUS_AGENT_STATUS + port->index;
> + int ret;
> +
> + if ((write && write->flags & ~RTS490X_I2C_SUPPORTED_FLAGS) ||
> + (read && read->flags & ~RTS490X_I2C_SUPPORTED_FLAGS))
> + return -EOPNOTSUPP;
> +
> + if (write_length + read_length > RTS490X_SMBUS_PAYLOAD_SIZE)
> + return -EOPNOTSUPP;
> +
> + address = i2c_8bit_addr_from_msg(write ?: read);
> + descriptor[RTS490X_SMBUS_DESC_ADDRESS] = address;
> + descriptor[RTS490X_SMBUS_DESC_TYPE] =
> + rts490x_smbus_frequency_to_reg(port->clock_frequency);
> + descriptor[RTS490X_SMBUS_DESC_WRITE_LENGTH] = write_length;
> + descriptor[RTS490X_SMBUS_DESC_READ_LENGTH] = read_length;
> +
> + if (type == RTS490X_SMBUS_WRITE_READ)
> + descriptor[RTS490X_SMBUS_DESC_TYPE] |= RTS490X_SMBUS_REPEATED_START;
> +
> + if (read_length) {
> + read_buffer = i2c_get_dma_safe_msg_buf(read, 1);
> + if (!read_buffer)
> + return -ENOMEM;
> + }
> +
> + guard(mutex)(&port->xfer_lock);
> +
> + ret = rts490x_smbus_recover_controller(port);
> + if (ret)
> + goto out;
> +
> + ret = rts490x_smbus_write_controller_buffer(port, descriptor,
> + write ? write->buf : NULL,
> + write_length);
> + if (ret)
> + goto out;
> +
> + /* Clear any completion left by an earlier controller-agent transaction. */
> + ret = regmap_write(hub->regmap, status_reg, RTS490X_CONTROLLER_FINISH);
> + if (ret)
> + goto out;
> +
> + reinit_completion(&port->completion);
> + spin_lock_irqsave(&hub->controller_lock, flags);
> + port->controller_status = 0;
> + port->controller_error = 0;
> + port->controller_done = false;
> + port->controller_pending = true;
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> +
> + ret = regmap_write(hub->regmap, RTS490X_TP_SMBUS_AGENT_TRANS_START,
> + port->mask);
> + if (ret) {
> + spin_lock_irqsave(&hub->controller_lock, flags);
> + port->controller_pending = false;
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> + goto out;
> + }
> +
> + ret = rts490x_smbus_wait_for_completion(port);
> + if (ret)
> + goto out;
> +
> + if (read_length)
> + ret = rts490x_smbus_read_controller_buffer(port, read_buffer,
> + write_length,
> + read_length);
> +
> +out:
> + if (read_length)
> + i2c_put_dma_safe_msg_buf(read_buffer, read, !ret);
> +
> + return ret;
> +}
> +
> +static bool rts490x_smbus_can_combine(const struct i2c_msg *write,
> + const struct i2c_msg *read)
> +{
> + return !(write->flags & I2C_M_RD) && (read->flags & I2C_M_RD) &&
> + write->addr == read->addr &&
> + write->len + read->len <= RTS490X_SMBUS_PAYLOAD_SIZE;
> +}
> +
> +static int rts490x_smbus_xfer(struct i2c_adapter *adapter,
> + struct i2c_msg *messages, int num)
> +{
> + struct rts490x_port *port = i2c_get_adapdata(adapter);
> + struct i2c_msg *read = NULL;
> + struct i2c_msg *write = NULL;
> + enum rts490x_smbus_xfer_type type;
> + int ret;
> +
> + if (!num)
> + return 0;
> +
> + if (num == 2) {
> + if (!rts490x_smbus_can_combine(&messages[0], &messages[1]))
> + return -EOPNOTSUPP;
> +
> + write = &messages[0];
> + read = &messages[1];
> + type = RTS490X_SMBUS_WRITE_READ;
> + } else if (num == 1 && messages[0].flags & I2C_M_RD) {
> + read = &messages[0];
> + type = RTS490X_SMBUS_READ;
> + } else if (num == 1) {
> + write = &messages[0];
> + type = RTS490X_SMBUS_WRITE;
> + } else {
> + return -EOPNOTSUPP;
> + }
> +
> + ret = rts490x_smbus_xfer_one(port, write, read, type);
> + return ret ? ret : num;
> +}
> +
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> +static int rts490x_smbus_register_target(struct i2c_client *target)
> +{
> + struct rts490x_port *port = i2c_get_adapdata(target->adapter);
> + struct rts490x_hub *hub = port->hub;
> + int ret;
> +
> + guard(mutex)(&hub->target_lock);
> +
> + if (!hub->ibi_ready)
> + return -EOPNOTSUPP;
> +
> + if (target->flags & I2C_CLIENT_TEN)
> + return -EOPNOTSUPP;
> +
> + if (port->target)
> + return -EBUSY;
> +
> + ret = regmap_write(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
> + RTS490X_TARGET_BUFFER_STATUS_MASK);
> + if (ret)
> + return ret;
> +
> + port->target = target;
> +
> + return 0;
> +}
> +
> +static int rts490x_smbus_unregister_target(struct i2c_client *target)
> +{
> + struct rts490x_port *port = i2c_get_adapdata(target->adapter);
> + struct rts490x_hub *hub = port->hub;
> +
> + guard(mutex)(&hub->target_lock);
> +
> + if (port->target != target)
> + return -EINVAL;
> +
> + port->target = NULL;
> +
> + return 0;
> +}
> +#endif
> +
> +static u32 rts490x_smbus_functionality(struct i2c_adapter *adapter)
> +{
> + u32 functionality;
> +
> + functionality = (I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL) &
> + ~I2C_FUNC_SMBUS_PEC;
> +
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> + struct rts490x_port *port = i2c_get_adapdata(adapter);
> +
> + if (port->hub->ibi_ready)
> + functionality |= I2C_FUNC_SLAVE;
> +#endif
> +
> + return functionality;
> +}
> +
> +static const struct i2c_adapter_quirks rts490x_smbus_quirks = {
> + .flags = I2C_AQ_COMB_WRITE_THEN_READ,
> + .max_read_len = RTS490X_SMBUS_PAYLOAD_SIZE,
> + .max_write_len = RTS490X_SMBUS_PAYLOAD_SIZE,
> + .max_comb_1st_msg_len = RTS490X_SMBUS_PAYLOAD_SIZE - 1,
> + .max_comb_2nd_msg_len = RTS490X_SMBUS_PAYLOAD_SIZE - 1,
> +};
> +
> +static const struct i2c_algorithm rts490x_smbus_algorithm = {
> + .xfer = rts490x_smbus_xfer,
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> + .reg_target = rts490x_smbus_register_target,
> + .unreg_target = rts490x_smbus_unregister_target,
> +#endif
> + .functionality = rts490x_smbus_functionality,
> +};
> +
> +static int rts490x_read_target_buffer(struct rts490x_port *port,
> + unsigned int page_offset,
> + u8 *address, unsigned int *length)
> +{
> + struct rts490x_hub *hub = port->hub;
> + u8 page = RTS490X_CONTROLLER_BUFFER_PAGE +
> + RTS490X_CONTROLLER_PAGES_PER_PORT * port->index + page_offset;
> + unsigned int value;
> + int ret, ret2;
> +
> + guard(mutex)(&hub->page_lock);
> +
> + ret = regmap_write(hub->regmap, RTS490X_PAGE_PTR, page);
> + if (ret)
> + goto restore_page;
> +
> + ret = regmap_read(hub->regmap, RTS490X_TARGET_BUFFER_LENGTH, &value);
> + if (ret)
> + goto restore_page;
> +
> + if (!value) {
> + ret = -ENODATA;
> + goto restore_page;
> + }
> +
> + *length = value - 1;
> + if (*length > RTS490X_SMBUS_TARGET_PAYLOAD_SIZE) {
> + ret = -EMSGSIZE;
> + goto restore_page;
> + }
> +
> + ret = regmap_read(hub->regmap, RTS490X_TARGET_BUFFER_ADDRESS, &value);
> + if (ret)
> + goto restore_page;
> +
> + *address = value;
> + if (*length)
> + ret = regmap_bulk_read(hub->regmap, RTS490X_TARGET_BUFFER_DATA,
> + hub->target_buffer, *length);
> +
> +restore_page:
> + ret2 = regmap_write(hub->regmap, RTS490X_PAGE_PTR, 0);
> + return ret ?: ret2;
> +}
> +
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> +static void rts490x_deliver_target_write(struct rts490x_port *port,
> + u8 address, unsigned int length)
> +{
> + struct i2c_client *target = port->target;
> + u8 value = 0;
> + unsigned int i;
> + int ret;
> +
> + if (!target || target->addr != address >> 1)
> + return;
> +
> + ret = i2c_slave_event(target, I2C_SLAVE_WRITE_REQUESTED, &value);
> + if (!ret) {
> + for (i = 0; i < length; i++) {
> + value = port->hub->target_buffer[i];
> + ret = i2c_slave_event(target, I2C_SLAVE_WRITE_RECEIVED,
> + &value);
> + if (ret)
> + break;
> + }
> + }
> +
> + i2c_slave_event(target, I2C_SLAVE_STOP, &value);
> +}
> +#endif
> +
> +static void rts490x_process_target_buffer(struct rts490x_port *port,
> + unsigned int page_offset, u8 flag)
> +{
> + struct rts490x_hub *hub = port->hub;
> + unsigned int length = 0;
> + u8 address = 0;
> + int ret;
> +
> + ret = rts490x_read_target_buffer(port, page_offset, &address, &length);
> + if (ret == -EMSGSIZE)
> + dev_warn_ratelimited(hub->dev,
> + "SMBus target message too large on port %u\n",
> + port->index);
> + else if (ret && ret != -ENODATA)
> + dev_warn_ratelimited(hub->dev,
> + "failed to read SMBus target buffer on port %u: %d\n",
> + port->index, ret);
> +
> + if (regmap_write(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + port->index, flag))
> + dev_warn_ratelimited(hub->dev,
> + "failed to acknowledge SMBus target buffer on port %u\n",
> + port->index);
> +
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> + if (!ret)
> + rts490x_deliver_target_write(port, address, length);
> +#endif
> +}
> +
> +static void rts490x_process_smbus_target_status(struct rts490x_port *port,
> + unsigned int status)
> +{
> + struct rts490x_hub *hub = port->hub;
> + int ret;
> +
> + if (status & RTS490X_TARGET_BUFFER_0_RECEIVED)
> + rts490x_process_target_buffer(port,
> + RTS490X_TARGET_BUFFER_0_PAGE_OFFSET,
> + RTS490X_TARGET_BUFFER_0_RECEIVED);
> +
> + if (status & RTS490X_TARGET_BUFFER_1_RECEIVED)
> + rts490x_process_target_buffer(port,
> + RTS490X_TARGET_BUFFER_1_PAGE_OFFSET,
> + RTS490X_TARGET_BUFFER_1_RECEIVED);
> +
> + if (status & RTS490X_TARGET_BUFFER_OVERFLOW) {
> + ret = regmap_write(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
> + RTS490X_TARGET_BUFFER_OVERFLOW);
> + if (ret)
> + dev_warn_ratelimited(hub->dev,
> + "failed to clear SMBus target overflow on port %u: %d\n",
> + port->index, ret);
> + else
> + dev_warn_ratelimited(hub->dev,
> + "SMBus target buffer overflow on port %u\n",
> + port->index);
> + }
> +}
> +
> +static void rts490x_complete_smbus_controller(struct rts490x_port *port,
> + unsigned int status)
> +{
> + struct rts490x_hub *hub = port->hub;
> + unsigned long flags;
> + bool notify = false;
> + int ret;
> +
> + if (!(status & RTS490X_CONTROLLER_FINISH))
> + return;
> +
> + ret = regmap_write(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + port->index,
> + RTS490X_CONTROLLER_FINISH);
> +
> + spin_lock_irqsave(&hub->controller_lock, flags);
> + if (port->controller_pending) {
> + port->controller_status = status;
> + port->controller_error = ret;
> + port->controller_done = true;
> + notify = true;
> + } else if (port->controller_recovering && !ret) {
> + port->controller_recovering = false;
> + }
> + spin_unlock_irqrestore(&hub->controller_lock, flags);
> +
> + if (ret)
> + dev_warn_ratelimited(hub->dev,
> + "failed to acknowledge SMBus controller completion on port %u: %d\n",
> + port->index, ret);
> +
> + if (notify)
> + complete(&port->completion);
> +}
> +
> +static void rts490x_process_smbus_status(struct rts490x_port *port)
> +{
> + struct rts490x_hub *hub = port->hub;
> + unsigned int status;
> + int ret;
> +
> + ret = regmap_read(hub->regmap,
> + RTS490X_TP0_SMBUS_AGENT_STATUS + port->index, &status);
> + if (ret) {
> + dev_warn_ratelimited(hub->dev,
> + "failed to read SMBus status on port %u: %d\n",
> + port->index, ret);
> + return;
> + }
> +
> + rts490x_complete_smbus_controller(port, status);
> +
> + guard(mutex)(&hub->target_lock);
> + rts490x_process_smbus_target_status(port, status);
> +}
> +
> +static void rts490x_ibi_handler(struct i3c_device *i3cdev,
> + const struct i3c_ibi_payload *payload)
> +{
> + struct rts490x *rts490x = i3cdev_get_drvdata(i3cdev);
> + struct rts490x_hub *hub = rts490x->hub;
> + const u8 *data = payload->data;
> + unsigned int pending;
> + unsigned int status;
> + int ret;
> + int i;
> +
> + if (!hub)
> + return;
> +
> + if (payload->len && data) {
> + status = data[0];
> + } else {
> + ret = regmap_read(hub->regmap, RTS490X_DEV_AND_IBI_STATUS, &status);
> + if (ret)
> + return;
> + }
> +
> + if (!(status & RTS490X_SMBUS_AGENT_EVENT))
> + return;
> +
> + if (payload->len >= RTS490X_IBI_MAX_PAYLOAD_LENGTH && data) {
> + pending = data[1];
> + } else {
> + ret = regmap_read(hub->regmap,
> + RTS490X_TP_SMBUS_AGENT_IBI_STATUS, &pending);
> + if (ret)
> + return;
> + }
> +
> + pending &= hub->smbus_port_mask;
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++) {
> + if (pending & BIT(i))
> + rts490x_process_smbus_status(&hub->ports[i]);
> + }
> +}
> +
> +static const struct i3c_ibi_setup rts490x_ibi_setup = {
> + .handler = rts490x_ibi_handler,
> + .max_payload_len = RTS490X_IBI_MAX_PAYLOAD_LENGTH,
> + .num_slots = RTS490X_IBI_SLOTS,
> +};
> +
> +static int rts490x_set_smbus_ibi(struct rts490x_hub *hub, bool enable)
> +{
> + unsigned int protection;
> + u8 value = enable ? hub->smbus_port_mask : 0;
> + bool restore;
> + int ret, ret2;
> +
> + guard(mutex)(&hub->rts490x->protected_reg_lock);
> +
> + ret = regmap_read(hub->regmap, RTS490X_PROTECTION_CODE, &protection);
> + if (ret)
> + return ret;
> +
> + restore = protection != RTS490X_REGISTERS_UNLOCK_CODE;
> + if (restore) {
> + ret = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE,
> + RTS490X_REGISTERS_UNLOCK_CODE);
> + if (ret)
> + return ret;
> + }
> +
> + ret = regmap_update_bits(hub->regmap,
> + RTS490X_TP_SMBUS_AGENT_IBI_CONFIG,
> + hub->smbus_port_mask, value);
> +
> + if (restore) {
> + ret2 = regmap_write(hub->regmap, RTS490X_PROTECTION_CODE, protection);
> + if (!ret && ret2)
> + ret = ret2;
> + }
> +
> + return ret;
> +}
> +
> +static void rts490x_free_ibi(void *data)
> +{
> + struct rts490x_hub *hub = data;
> +
> + hub->ibi_ready = false;
> + if (rts490x_set_smbus_ibi(hub, false))
> + dev_warn(hub->dev, "failed to disable SMBus event IBI\n");
> + if (i3c_device_disable_ibi(hub->rts490x->i3cdev))
> + dev_warn(hub->dev, "failed to disable hub IBI\n");
> + i3c_device_free_ibi(hub->rts490x->i3cdev);
> +}
> +
> +int rts490x_setup_ibi(struct rts490x_hub *hub)
> +{
> + int ret;
> +
> + if (!hub->smbus_port_mask)
> + return 0;
> +
> + ret = i3c_device_request_ibi(hub->rts490x->i3cdev, &rts490x_ibi_setup);
> + if (ret)
> + return dev_err_probe(hub->dev, ret, "failed to request hub IBI\n");
> +
> + ret = i3c_device_enable_ibi(hub->rts490x->i3cdev);
> + if (ret) {
> + i3c_device_free_ibi(hub->rts490x->i3cdev);
> + return dev_err_probe(hub->dev, ret, "failed to enable hub IBI\n");
> + }
> +
> + ret = devm_add_action_or_reset(hub->dev, rts490x_free_ibi, hub);
> + if (ret)
> + return ret;
> +
> + ret = rts490x_set_smbus_ibi(hub, true);
> + if (ret)
> + return ret;
> +
> + hub->ibi_ready = true;
> + return 0;
> +}
> +
> +static void rts490x_unregister_smbus_adapter(struct rts490x_port *port)
> +{
> + struct i2c_adapter *adapter = port->smbus_adapter;
> + struct fwnode_handle *fwnode;
> +
> + if (!adapter)
> + return;
> +
> + fwnode = dev_fwnode(&adapter->dev);
> + i2c_del_adapter(adapter);
> + fwnode_handle_put(fwnode);
> + port->smbus_adapter = NULL;
> + port->registered = false;
> +}
> +
> +void rts490x_unregister_smbus_ports(void *data)
> +{
> + struct rts490x_hub *hub = data;
> + int i;
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++)
> + rts490x_unregister_smbus_adapter(&hub->ports[i]);
> +}
> +
> +int rts490x_register_smbus_ports(struct rts490x_hub *hub)
> +{
> + struct rts490x_port *port;
> + struct i2c_adapter *adapter;
> + int ret;
> + int i;
> +
> + for (i = 0; i < hub->rts490x->num_target_ports; i++) {
> + port = &hub->ports[i];
> + if (port->mode != RTS490X_PORT_SMBUS)
> + continue;
> +
> + ret = devm_mutex_init(hub->dev, &port->xfer_lock);
> + if (ret)
> + return ret;
> +
> + init_completion(&port->completion);
> +
> + adapter = devm_kzalloc(hub->dev, sizeof(*adapter), GFP_KERNEL);
> + if (!adapter)
> + return -ENOMEM;
> +
> + adapter->owner = THIS_MODULE;
> + adapter->algo = &rts490x_smbus_algorithm;
> + adapter->quirks = &rts490x_smbus_quirks;
> + adapter->dev.parent = hub->dev;
> + adapter->timeout = msecs_to_jiffies(RTS490X_SMBUS_TIMEOUT_MS);
> + adapter->retries = 3;
> + device_set_node(&adapter->dev, fwnode_handle_get(port->fwnode));
> + snprintf(adapter->name, sizeof(adapter->name),
> + "rts490x-i3c-hub target port %u", port->index);
> + i2c_set_adapdata(adapter, port);
> +
> + port->smbus_adapter = adapter;
> + ret = i2c_add_adapter(adapter);
> + if (ret) {
> + fwnode_handle_put(dev_fwnode(&adapter->dev));
> + device_set_node(&adapter->dev, NULL);
> + port->smbus_adapter = NULL;
> + return dev_err_probe(hub->dev, ret,
> + "failed to register SMBus target port %u\n",
> + port->index);
> + }
> +
> + port->registered = true;
> + }
> +
> + return 0;
> +}
> diff --git a/drivers/i3c/hub/rts490x-i3c-hub.h b/drivers/i3c/hub/rts490x-i3c-hub.h
> new file mode 100644
> index 000000000000..cab489de0aa8
> --- /dev/null
> +++ b/drivers/i3c/hub/rts490x-i3c-hub.h
> @@ -0,0 +1,150 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +/* Copyright (c) 2025-2026 Realtek Semiconductor Corp. */
> +#ifndef _RTS490X_I3C_HUB_H
> +#define _RTS490X_I3C_HUB_H
> +
> +#include <linux/bits.h>
> +#include <linux/completion.h>
> +#include <linux/i2c.h>
> +#include <linux/i3c/hub.h>
> +#include <linux/mfd/rts490x.h>
> +#include <linux/mutex.h>
> +#include <linux/spinlock.h>
> +
> +#define RTS490X_SMBUS_TARGET_PAYLOAD_SIZE 78
> +
> +#define RTS490X_TP_SMBUS_AGENT_IBI_CONFIG 0x1b
> +#define RTS490X_TP0_SMBUS_AGENT_STATUS 0x64
> +#define RTS490X_TARGET_BUFFER_STATUS_MASK GENMASK(3, 1)
> +
> +struct device;
> +struct fwnode_handle;
> +struct regmap;
> +struct rts490x;
> +
> +enum rts490x_port_mode {
> + RTS490X_PORT_UNUSED,
> + RTS490X_PORT_I3C,
> + RTS490X_PORT_SMBUS,
> +};
> +
> +/**
> + * struct rts490x_config - static hub configuration
> + * @tp0145_pullup_ohms: pull-up resistance for target ports 0/1/4/5
> + * @tp2367_pullup_ohms: pull-up resistance for target ports 2/3/6/7
> + * @cp0_io_strength_ohms: controller port 0 output impedance
> + * @cp1_io_strength_ohms: controller port 1 output impedance
> + * @tp0145_io_strength_ohms: target ports 0/1/4/5 output impedance
> + * @tp2367_io_strength_ohms: target ports 2/3/6/7 output impedance
> + * @tp0145_pullup_configured: target port group 0/1/4/5 pull-up is configured
> + * @tp2367_pullup_configured: target port group 2/3/6/7 pull-up is configured
> + * @cp0_io_strength_configured: controller port 0 impedance is configured
> + * @cp1_io_strength_configured: controller port 1 impedance is configured
> + * @tp0145_io_strength_configured: target group 0/1/4/5 impedance is configured
> + * @tp2367_io_strength_configured: target group 2/3/6/7 impedance is configured
> + */
> +struct rts490x_config {
> + u32 tp0145_pullup_ohms;
> + u32 tp2367_pullup_ohms;
> + u32 cp0_io_strength_ohms;
> + u32 cp1_io_strength_ohms;
> + u32 tp0145_io_strength_ohms;
> + u32 tp2367_io_strength_ohms;
> + bool tp0145_pullup_configured;
> + bool tp2367_pullup_configured;
> + bool cp0_io_strength_configured;
> + bool cp1_io_strength_configured;
> + bool tp0145_io_strength_configured;
> + bool tp2367_io_strength_configured;
> +};
> +
> +/**
> + * struct rts490x_port - per-target-port state
> + * @hub_controller: generic I3C hub controller for an I3C port
> + * @fwnode: firmware node describing the target-port bus
> + * @smbus_adapter: adapter registered for an SMBus port
> + * @target: target client registered on an SMBus port
> + * @hub: parent RTS490x hub state
> + * @xfer_lock: serializes SMBus controller-agent transfers on this port
> + * @completion: signaled when the controller agent reports FINISH through IBI
> + * @controller_status: controller-agent status captured by the IBI handler
> + * @controller_error: IBI completion processing error
> + * @controller_pending: a controller-agent transfer is waiting for IBI
> + * @controller_done: the pending transfer has been completed by IBI
> + * @controller_recovering: reject new transfers until a late completion is acked
> + * @mode: target-port operating mode
> + * @clock_frequency: SMBus controller-agent bus rate in Hz
> + * @index: target-port index
> + * @mask: target-port register mask
> + * @pullup_enabled: use the on-chip target-port pull-up
> + * @registered: an I3C controller or SMBus adapter is registered for this port
> + * @always_enable: skip the enable_port/disable_port hooks; the route stays
> + * connected between operations
> + */
> +struct rts490x_port {
> + struct i3c_hub_controller hub_controller;
> + struct fwnode_handle *fwnode;
> + struct i2c_adapter *smbus_adapter;
> + struct i2c_client *target;
> + struct rts490x_hub *hub;
> + /* Serializes controller-agent transactions on this target port. */
> + struct mutex xfer_lock;
> + struct completion completion;
> + unsigned int controller_status;
> + int controller_error;
> + bool controller_pending;
> + bool controller_done;
> + bool controller_recovering;
> + enum rts490x_port_mode mode;
> + u32 clock_frequency;
> + u8 index;
> + u8 mask;
> + bool pullup_enabled;
> + bool registered;
> + bool always_enable;
> +};
> +
> +/**
> + * struct rts490x_hub - Realtek-specific I3C hub child state
> + * @dev: hub child device
> + * @rts490x: shared MFD state
> + * @regmap: register map shared with the MFD parent
> + * @hub: generic I3C hub context
> + * @page_lock: serializes paged-register transactions
> + * @controller_lock: protects controller completion state against timeout/IBI races
> + * @target_lock: serializes target registration and IBI delivery
> + * @target_buffer: shared buffer for SMBus target writes delivered by IBI
> + * @config: static hardware configuration
> + * @ports: per-target-port state
> + * @i3c_port_mask: target ports configured as I3C buses
> + * @smbus_port_mask: target ports configured as SMBus buses
> + * @ibi_ready: hub-device IBI has been requested and enabled
> + */
> +struct rts490x_hub {
> + struct device *dev;
> + struct rts490x *rts490x;
> + struct regmap *regmap;
> + struct i3c_hub hub;
> + /* Serializes page selection and access across all target ports. */
> + struct mutex page_lock;
> + /* Protects controller completion state against timeout and IBI races. */
> + spinlock_t controller_lock;
> + /* Serializes target registration and IBI delivery. */
> + struct mutex target_lock;
> + u8 target_buffer[RTS490X_SMBUS_TARGET_PAYLOAD_SIZE];
> + struct rts490x_config config;
> + struct rts490x_port ports[RTS490X_MAX_TARGET_PORTS];
> + u8 i3c_port_mask;
> + u8 smbus_port_mask;
> + bool ibi_ready;
> +};
> +
> +extern const struct i3c_hub_ops rts490x_hub_ops;
> +
> +int rts490x_register_i3c_ports(struct rts490x_hub *hub);
> +int rts490x_setup_ibi(struct rts490x_hub *hub);
> +int rts490x_register_smbus_ports(struct rts490x_hub *hub);
> +void rts490x_unregister_smbus_ports(void *data);
> +bool rts490x_smbus_frequency_supported(u32 frequency);
> +
> +#endif /* _RTS490X_I3C_HUB_H */
> --
> 2.34.1
>
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-09-21 18:42 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-20 10:04 [RFC PATCH 0/4] Add support for Realtek RTS490x I3C hub zain_zhou
2026-09-20 10:04 ` [RFC PATCH 1/4] dt-bindings: i3c: Add Realtek RTS490x I3C hub support zain_zhou
2026-09-20 10:04 ` [RFC PATCH 2/4] mfd: Add driver for Realtek RTS490x I3C hub zain_zhou
2026-09-20 10:05 ` [RFC PATCH 3/4] regulator: rts490x: Add driver for on-die regulators zain_zhou
2026-09-20 13:05 ` Krzysztof Kozlowski
2026-09-21 3:46 ` 回复: " 周寅
2026-09-21 14:07 ` Krzysztof Kozlowski
2026-09-21 10:23 ` Mark Brown
2026-09-20 10:05 ` [RFC PATCH 4/4] i3c: hub: Add Realtek RTS490x I3C hub support zain_zhou
2026-09-21 18:42 ` Frank Li
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®