* [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB
@ 2026-05-25 12:51 zain_zhou
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
` (3 more replies)
0 siblings, 4 replies; 9+ messages in thread
From: zain_zhou @ 2026-05-25 12:51 UTC (permalink / raw)
To: linux-staging, linux-i3c, devicetree
Cc: gregkh, alexandre.belloni, Frank.Li, robh, krzk+dt, conor+dt,
linusw, brgl, linux-gpio, linux-kernel, wei_wang, Yin Zhou
From: Yin Zhou <zain_zhou@realsil.com.cn>
Add DT binding schema for Realtek RTS490x series I3C HUB devices.
The binding describes configuration properties for:
- LDO enable/disable and voltage level per port group
- Pull-up resistance per port group
- IO driver strength per port
- Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up,
IO mode, SMBus clock frequency and polling interval
- Hub network always-I3C mode
- Hardware identification via CSEL pin (id) and CP1 pins (id-cp1)
Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
Changes in v2:
- Rework binding per Krzysztof Kozlowski's review:
add realtek, vendor prefix to all custom properties; use boolean
for enable flags; use u32 with unit suffixes (-microvolt, -ohms)
for voltage/resistance; change to unevaluatedProperties: false;
fix title, maintainer name, description, $nodename pattern
- Consolidate examples; add dt-bindings/i2c/i2c.h include
---
.../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 263 ++++++++++++++++++
1 file changed, 263 insertions(+)
create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
new file mode 100644
index 000000000000..851a433abcd3
--- /dev/null
+++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
@@ -0,0 +1,263 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/i3c/realtek,rts490x-i3c-hub.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 is an I3C HUB device that provides voltage level
+ translation between I3C controller and target devices, bus capacitance
+ isolation, address conflict isolation, I3C port expansion (up to 8
+ target ports), simultaneous dual-controller port support, and per-port
+ mode selection (I3C, SMBus, GPIO, or disabled).
+
+properties:
+ $nodename:
+ pattern: "^hub@[0-9a-f]+(,[0-9a-f]+)*$"
+
+ compatible:
+ const: realtek,rts490x-i3c-hub
+
+ reg:
+ maxItems: 1
+ description:
+ Encodes the static I2C address, manufacturer ID, and part/instance ID
+ as defined by the I3C specification.
+
+ assigned-address:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ minimum: 0x1
+ maximum: 0xff
+ description:
+ Dynamic I3C address to assign to this device.
+
+ dcr:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ description:
+ Device Characteristic Register value of the hub.
+
+ "#address-cells":
+ const: 1
+
+ "#size-cells":
+ const: 0
+
+ realtek,cp0-ldo-enable:
+ type: boolean
+ description:
+ Enable the on-die LDO for Controller Port 0.
+
+ realtek,cp1-ldo-enable:
+ type: boolean
+ description:
+ Enable the on-die LDO for Controller Port 1.
+
+ realtek,tp0145-ldo-enable:
+ type: boolean
+ description:
+ Enable the on-die LDO for Target Ports 0/1/4/5.
+
+ realtek,tp2367-ldo-enable:
+ type: boolean
+ description:
+ Enable the on-die LDO for Target Ports 2/3/6/7.
+
+ realtek,cp0-ldo-microvolt:
+ enum: [1000000, 1100000, 1200000, 1800000]
+ description:
+ Output voltage of the Controller Port 0 on-die LDO, in microvolts.
+
+ realtek,cp1-ldo-microvolt:
+ enum: [1000000, 1100000, 1200000, 1800000]
+ description:
+ Output voltage of the Controller Port 1 on-die LDO, in microvolts.
+
+ realtek,tp0145-ldo-microvolt:
+ enum: [1000000, 1100000, 1200000, 1800000]
+ description:
+ Output voltage of the Target Ports 0/1/4/5 on-die LDO, in
+ microvolts.
+
+ realtek,tp2367-ldo-microvolt:
+ enum: [1000000, 1100000, 1200000, 1800000]
+ description:
+ Output voltage of the Target Ports 2/3/6/7 on-die LDO, in
+ microvolts.
+
+ realtek,tp0145-pullup-ohms:
+ enum: [0, 250, 500, 1000, 2000]
+ description:
+ Pull-up resistance for Target Ports 0/1/4/5, in ohms. 0 disables
+ the pull-up.
+
+ realtek,tp2367-pullup-ohms:
+ enum: [0, 250, 500, 1000, 2000]
+ description:
+ Pull-up resistance for Target Ports 2/3/6/7, in ohms. 0 disables
+ the pull-up.
+
+ realtek,cp0-io-strength-ohms:
+ enum: [20, 30, 40, 50]
+ description:
+ Output driver impedance for Controller Port 0, in ohms.
+
+ realtek,cp1-io-strength-ohms:
+ enum: [20, 30, 40, 50]
+ description:
+ Output driver impedance for Controller Port 1, in ohms.
+
+ realtek,tp0145-io-strength-ohms:
+ enum: [20, 30, 40, 50]
+ description:
+ Output driver impedance for Target Ports 0/1/4/5, in ohms.
+
+ realtek,tp2367-io-strength-ohms:
+ enum: [20, 30, 40, 50]
+ description:
+ Output driver impedance for Target Ports 2/3/6/7, in ohms.
+
+ realtek,id:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [0, 1, 3]
+ description: |
+ I3C HUB hardware ID based on CSEL pin state. Values:
+ 0 - CP0 is selected as primary Controller Port
+ 1 - Primary Controller Port selected by software
+ 3 - CP1 is selected as primary Controller Port
+
+ realtek,id-cp1:
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [0, 1, 2, 3]
+ description:
+ I3C HUB hardware ID based on CP1 SDA and SCL pin state probed
+ during power-on.
+
+patternProperties:
+ "^target-port@[0-9]+$":
+ type: object
+ description:
+ I3C HUB target port child node, named target-port@<id>.
+
+ properties:
+ compatible:
+ const: realtek,rts490x-i3c-hub-port
+
+ reg:
+ maxItems: 1
+ description:
+ Target port index (0-based).
+
+ "#address-cells":
+ const: 1
+
+ "#size-cells":
+ const: 0
+
+ realtek,mode:
+ enum: [disabled, i3c, smbus, gpio]
+ description:
+ Operating mode of this target port.
+
+ realtek,pullup-enable:
+ type: boolean
+ description:
+ When present, enables the pull-up for this target port.
+
+ realtek,always-enable:
+ type: boolean
+ description:
+ When present, the target port is always enabled. Otherwise
+ the port is enabled on demand and disabled after use.
+
+ realtek,polling-interval-ms:
+ minimum: 0
+ description:
+ SMBus polling interval in milliseconds. If absent or 0,
+ polling is disabled and IBI is used instead.
+
+ clock-frequency:
+ enum: [100000, 200000, 400000, 1000000]
+ description:
+ SMBus clock frequency in Hz. Applies only when mode is smbus.
+ Defaults to 400000 if absent.
+
+ additionalProperties: true
+
+required:
+ - compatible
+ - reg
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/i2c/i2c.h>
+ i3c-master@d040000 {
+ reg = <0xd040000 0x1000>;
+ #address-cells = <3>;
+ #size-cells = <0>;
+
+ hub@70,4ba00000000 {
+ compatible = "realtek,rts490x-i3c-hub";
+ reg = <0x70 0x4ba 0x00000000>;
+ assigned-address = <0x70>;
+ dcr = <0xc2>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ realtek,cp0-ldo-enable;
+ realtek,cp1-ldo-enable;
+ realtek,cp0-ldo-microvolt = <1000000>;
+ realtek,cp1-ldo-microvolt = <1100000>;
+ realtek,tp0145-ldo-enable;
+ realtek,tp0145-ldo-microvolt = <1200000>;
+ realtek,tp2367-ldo-microvolt = <1800000>;
+ realtek,tp0145-pullup-ohms = <2000>;
+ realtek,tp2367-pullup-ohms = <500>;
+ realtek,tp0145-io-strength-ohms = <50>;
+ realtek,tp2367-io-strength-ohms = <30>;
+ realtek,cp0-io-strength-ohms = <20>;
+ realtek,cp1-io-strength-ohms = <40>;
+
+ target-port@0 {
+ compatible = "realtek,rts490x-i3c-hub-port";
+ reg = <0>;
+ realtek,mode = "i3c";
+ realtek,pullup-enable;
+ realtek,always-enable;
+ };
+
+ target-port@1 {
+ compatible = "realtek,rts490x-i3c-hub-port";
+ reg = <1>;
+ #address-cells = <1>;
+ #size-cells = <0>;
+ realtek,mode = "smbus";
+ realtek,pullup-enable;
+ clock-frequency = <1000000>;
+ realtek,polling-interval-ms = <10>;
+
+ i2c@10 {
+ compatible = "i2c-slave-mqueue";
+ reg = <(0x10 | I2C_OWN_SLAVE_ADDRESS)>;
+ };
+ };
+
+ target-port@2 {
+ compatible = "realtek,rts490x-i3c-hub-port";
+ reg = <2>;
+ realtek,mode = "gpio";
+ };
+
+ target-port@3 {
+ compatible = "realtek,rts490x-i3c-hub-port";
+ reg = <3>;
+ realtek,mode = "disabled";
+ };
+ };
+ };
--
2.34.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver
2026-05-25 12:51 [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB zain_zhou
@ 2026-05-25 12:51 ` zain_zhou
2026-05-25 13:25 ` Alexandre Belloni
` (3 more replies)
2026-05-25 13:02 ` [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB sashiko-bot
` (2 subsequent siblings)
3 siblings, 4 replies; 9+ messages in thread
From: zain_zhou @ 2026-05-25 12:51 UTC (permalink / raw)
To: linux-staging, linux-i3c, devicetree
Cc: gregkh, alexandre.belloni, Frank.Li, robh, krzk+dt, conor+dt,
linusw, brgl, linux-gpio, linux-kernel, wei_wang, Yin Zhou
From: Yin Zhou <zain_zhou@realsil.com.cn>
Add driver for Realtek RTS490x series I3C HUB devices.
The driver supports:
- Device Tree based configuration of LDO, pull-up, IO strength
and per-port mode (I3C/SMBus/GPIO/disabled)
- Logical I3C bus registration per target port
- SMBus agent functionality with IBI and polling modes
- GPIO chip with IRQ support
- DebugFS interface for register access and DT config inspection
- IBI (In-Band Interrupt) handling
The driver is placed in staging as it has known issues to be resolved
before mainlining; see drivers/staging/rts490x/TODO for details.
Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
Changes in v2:
- Update driver to match v2 DT binding: parse physical values
directly via of_property_read_bool/u32; drop string enum lookup
tables; update property names with realtek, prefix and unit suffixes
- Fix maintainer name; move MAINTAINERS entry to driver patch
- Code style: rename TPn_* macros to TPN_*; rename SMBus frequency
constants to UPPER_CASE; add mutex field comments
---
MAINTAINERS | 6 +
drivers/staging/Kconfig | 2 +
drivers/staging/Makefile | 1 +
drivers/staging/rts490x/Kconfig | 16 +
drivers/staging/rts490x/Makefile | 2 +
drivers/staging/rts490x/TODO | 35 +
drivers/staging/rts490x/rts490xa-i3c-hub.c | 3039 ++++++++++++++++++++
7 files changed, 3101 insertions(+)
create mode 100644 drivers/staging/rts490x/Kconfig
create mode 100644 drivers/staging/rts490x/Makefile
create mode 100644 drivers/staging/rts490x/TODO
create mode 100644 drivers/staging/rts490x/rts490xa-i3c-hub.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 2fb1c75afd16..6d0f8cde935d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12214,6 +12214,12 @@ S: Supported
F: Documentation/devicetree/bindings/i3c/renesas,i3c.yaml
F: drivers/i3c/master/renesas-i3c.c
+I3C HUB DRIVER FOR REALTEK RTS490X
+M: Yin Zhou <zain_zhou@realsil.com.cn>
+S: Maintained
+F: Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
+F: drivers/staging/rts490x/
+
I3C DRIVER FOR SYNOPSYS DESIGNWARE
S: Orphan
F: Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml
diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig
index 2f92cd698bef..f14869cf5af5 100644
--- a/drivers/staging/Kconfig
+++ b/drivers/staging/Kconfig
@@ -48,4 +48,6 @@ source "drivers/staging/axis-fifo/Kconfig"
source "drivers/staging/vme_user/Kconfig"
+source "drivers/staging/rts490x/Kconfig"
+
endif # STAGING
diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile
index f5b8876aa536..59044d547bf7 100644
--- a/drivers/staging/Makefile
+++ b/drivers/staging/Makefile
@@ -13,3 +13,4 @@ obj-$(CONFIG_MOST) += most/
obj-$(CONFIG_GREYBUS) += greybus/
obj-$(CONFIG_BCM2835_VCHIQ) += vc04_services/
obj-$(CONFIG_XIL_AXIS_FIFO) += axis-fifo/
+obj-$(CONFIG_RTS490X_I3C_HUB) += rts490x/
diff --git a/drivers/staging/rts490x/Kconfig b/drivers/staging/rts490x/Kconfig
new file mode 100644
index 000000000000..282865d1fed9
--- /dev/null
+++ b/drivers/staging/rts490x/Kconfig
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: GPL-2.0-only
+config RTS490X_I3C_HUB
+ tristate "Realtek RTS490x I3C HUB driver"
+ depends on I3C
+ depends on REGMAP_I3C
+ select GPIOLIB
+ help
+ Support for Realtek RTS490x series I3C HUB devices (RTS4900,
+ RTS4901, RTS4902, RTS4903, RTS4904, RTS4906).
+
+ The I3C HUB provides port expansion, voltage level translation,
+ bus capacitance isolation, address conflict isolation, SMBus
+ agent functionality and GPIO expansion.
+
+ This driver can also be built as a module. If so, the module
+ will be called rts490xa-i3c-hub.
diff --git a/drivers/staging/rts490x/Makefile b/drivers/staging/rts490x/Makefile
new file mode 100644
index 000000000000..4b1d7640fb82
--- /dev/null
+++ b/drivers/staging/rts490x/Makefile
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: GPL-2.0-only
+obj-$(CONFIG_RTS490X_I3C_HUB) += rts490xa-i3c-hub.o
diff --git a/drivers/staging/rts490x/TODO b/drivers/staging/rts490x/TODO
new file mode 100644
index 000000000000..0f46620af4c6
--- /dev/null
+++ b/drivers/staging/rts490x/TODO
@@ -0,0 +1,35 @@
+TODO list for rts490xa-i3c-hub staging driver
+==============================================
+
+Completed in v2
+---------------
+- [x] Add proper DT binding schema validation (dt-schema)
+ Addressed in v2 dt-bindings patch: all properties carry vendor
+ prefix, use standard types (boolean/u32 with unit suffixes),
+ unevaluatedProperties: false, validated via dt-schema.
+
+Remaining before moving out of staging
+---------------------------------------
+- Clean up open-coded OF property parsing; use device_property_* APIs
+ instead of of_property_read_* where possible
+- Remove use of full_name / sscanf for node name parsing; use
+ of_node_name_eq() and fwnode helpers instead
+- Replace global mutex (i3c_hub_regmap_mutex) with per-device locking
+- Add kernel-doc comments for all exported/public functions
+- Resolve TODO comment in i3c_hub_hw_configure_tp() regarding MUX
+ connection verification
+- Remove TBD comment in i3c_hub_probe() regarding DEV_CMD security lock
+- Review and fix potential locking issues in i3c_hub_delayed_work()
+ when registering logical buses
+- Fix error handling in i3c_hub_delayed_work(): early return on failure
+ does not unregister already-registered logical buses, causing resource
+ leak; needs proper cleanup on error path
+
+Rebase on upstream i3c hub framework (pending)
+-----------------------------------------------
+A generic i3c hub framework is being introduced upstream by the NXP
+P3H2x4x patch series (v10, under review):
+ https://lore.kernel.org/linux-i3c/20260525064209.2263045-1-lakshay.piplani@nxp.com/
+
+Once that framework is merged, rebase this driver on it and move
+out of staging.
diff --git a/drivers/staging/rts490x/rts490xa-i3c-hub.c b/drivers/staging/rts490x/rts490xa-i3c-hub.c
new file mode 100644
index 000000000000..fdfff5c6dff5
--- /dev/null
+++ b/drivers/staging/rts490x/rts490xa-i3c-hub.c
@@ -0,0 +1,3039 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (C) 2021 - 2023 Intel Corporation. */
+/* Copyright (c) 2025 Realtek Semiconductor Corp. */
+
+#include <linux/bits.h>
+#include <linux/kernel.h>
+#include <linux/ktime.h>
+#include <linux/bitfield.h>
+#include <linux/debugfs.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/list.h>
+#include <linux/gpio/driver.h>
+
+#include <linux/i3c/device.h>
+#include <linux/i3c/master.h>
+
+#define I3C_HUB_TP_MAX_COUNT 0x08
+
+#define I3C_DCR_HUB 0xC2
+
+#define GPIO_BANK_SZ 0x02
+#define GPIO_MAX_BANK I3C_HUB_TP_MAX_COUNT
+
+/* I3C HUB REGISTERS */
+
+/*
+ * In this driver Controller - Target convention is used. All the abbreviations are
+ * based on this convention. For instance: CP - Controller Port, TP - Target Port.
+ */
+
+/* Device Information Registers */
+#define I3C_HUB_DEV_INFO_0 0x00
+#define I3C_HUB_DEV_INFO_1 0x01
+#define I3C_HUB_PID_5 0x02
+#define I3C_HUB_PID_4 0x03
+#define I3C_HUB_PID_3 0x04
+#define I3C_HUB_PID_2 0x05
+#define I3C_HUB_PID_1 0x06
+#define I3C_HUB_PID_0 0x07
+#define I3C_HUB_BCR 0x08
+#define I3C_HUB_DCR 0x09
+#define I3C_HUB_DEV_CAPAB 0x0A
+
+#define I3C_HUB_DEV_REV 0x0B
+#define I3C_HUB_DEV_REV_LDO_MASK GENMASK(7, 6)
+#define I3C_HUB_DEV_REV_LDO_GET(x) FIELD_GET(I3C_HUB_DEV_REV_LDO_MASK, (x))
+
+/* Device Configuration Registers */
+#define I3C_HUB_PROTECTION_CODE 0x10
+#define REGISTERS_LOCK_CODE 0x00
+#define REGISTERS_UNLOCK_CODE 0x69
+#define CP1_REGISTERS_UNLOCK_CODE 0x6A
+
+#define I3C_HUB_CP_CONF 0x11
+#define I3C_HUB_TP_ENABLE 0x12
+#define TPN_ENABLE(n) BIT(n)
+
+#define I3C_HUB_DEV_CONF 0x13
+#define I3C_HUB_IO_STRENGTH 0x14
+#define TP0145_IO_STRENGTH_MASK GENMASK(1, 0)
+#define TP0145_IO_STRENGTH(x) (((x) << 0) & TP0145_IO_STRENGTH_MASK)
+#define TP2367_IO_STRENGTH_MASK GENMASK(3, 2)
+#define TP2367_IO_STRENGTH(x) (((x) << 2) & TP2367_IO_STRENGTH_MASK)
+#define CP0_IO_STRENGTH_MASK GENMASK(5, 4)
+#define CP0_IO_STRENGTH(x) (((x) << 4) & CP0_IO_STRENGTH_MASK)
+#define CP1_IO_STRENGTH_MASK GENMASK(7, 6)
+#define CP1_IO_STRENGTH(x) (((x) << 6) & CP1_IO_STRENGTH_MASK)
+#define IO_STRENGTH_20_OHM 0x00
+#define IO_STRENGTH_30_OHM 0x01
+#define IO_STRENGTH_40_OHM 0x02
+#define IO_STRENGTH_50_OHM 0x03
+
+#define I3C_HUB_NET_OPER_MODE_CONF 0x15
+#define I3C_HUB_NET_ALWAYS_I3C_EN BIT(5)
+
+#define I3C_HUB_LDO_CONF 0x16
+#define CP0_LDO_VOLTAGE_MASK GENMASK(1, 0)
+#define CP0_LDO_VOLTAGE(x) (((x) << 0) & CP0_LDO_VOLTAGE_MASK)
+#define CP1_LDO_VOLTAGE_MASK GENMASK(3, 2)
+#define CP1_LDO_VOLTAGE(x) (((x) << 2) & CP1_LDO_VOLTAGE_MASK)
+#define TP0145_LDO_VOLTAGE_MASK GENMASK(5, 4)
+#define TP0145_LDO_VOLTAGE(x) (((x) << 4) & TP0145_LDO_VOLTAGE_MASK)
+#define TP2367_LDO_VOLTAGE_MASK GENMASK(7, 6)
+#define TP2367_LDO_VOLTAGE(x) (((x) << 6) & TP2367_LDO_VOLTAGE_MASK)
+#define LDO_VOLTAGE_1_0V 0x00
+#define LDO_VOLTAGE_1_1V 0x01
+#define LDO_VOLTAGE_1_2V 0x02
+#define LDO_VOLTAGE_1_8V 0x03
+
+#define I3C_HUB_TP_IO_MODE_CONF 0x17
+#define TPN_IO_MODE_CON(n) BIT(n)
+#define I3C_HUB_TP_SMBUS_AGNT_EN 0x18
+#define TPN_SMBUS_MODE_EN(n) BIT(n)
+
+#define I3C_HUB_LDO_AND_PULLUP_CONF 0x19
+#define LDO_ENABLE_DISABLE_MASK GENMASK(3, 0)
+#define CP0_LDO_EN BIT(0)
+#define CP1_LDO_EN BIT(1)
+/*
+ * I3C HUB does not provide a way to control LDO or pull-up for individual ports. It is possible
+ * for group of ports TP0/TP1/TP4/TP5 and TP2/TP3/TP6/TP7.
+ */
+#define TP0145_LDO_EN BIT(2)
+#define TP2367_LDO_EN BIT(3)
+#define TP0145_PULLUP_CONF_MASK GENMASK(7, 6)
+#define TP0145_PULLUP_CONF(x) (((x) << 6) & TP0145_PULLUP_CONF_MASK)
+#define TP2367_PULLUP_CONF_MASK GENMASK(5, 4)
+#define TP2367_PULLUP_CONF(x) (((x) << 4) & TP2367_PULLUP_CONF_MASK)
+#define PULLUP_250R 0x00
+#define PULLUP_500R 0x01
+#define PULLUP_1K 0x02
+#define PULLUP_2K 0x03
+
+#define I3C_HUB_CP_IBI_CONF 0x1A
+#define I3C_HUB_TP_IBI_CONF 0x1B
+#define I3C_HUB_IBI_MDB_CUSTOM 0x1C
+#define I3C_HUB_JEDEC_CONTEXT_ID 0x1D
+#define I3C_HUB_TP_GPIO_MODE_EN 0x1E
+#define TPN_GPIO_MODE_EN(n) BIT(n)
+
+/* Device Status and IBI Registers */
+#define I3C_HUB_DEV_AND_IBI_STS 0x20
+#define PEC_ERROR_FLAG BIT(0)
+#define PARITY_ERROR_FLAG BIT(1)
+#define CONTROLLER_MSG_PENDING_FLAG BIT(2)
+#define TP_IO_FLAG_STATUS BIT(3)
+#define SMBUS_AGENT_EVENT_FLAG_STATUS BIT(4)
+
+#define I3C_HUB_TP_SMBUS_AGNT_IBI_STS 0x21
+
+/* Controller Port Control/Status Registers */
+#define I3C_HUB_CP_MUX_SET 0x38
+#define CONTROLLER_PORT_MUX_REQ BIT(0)
+#define I3C_HUB_CP_MUX_STS 0x39
+#define CONTROLLER_PORT_MUX_CONNECTION_STATUS BIT(0)
+
+/* Target Dynamic Address Assignment Flag Registers */
+#define I3C_HUB_TARGET_DA_FLAG_BYTE_BASE 0x40
+#define I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT 16
+
+/* Target Ports Control Registers */
+#define I3C_HUB_TP_SMBUS_AGNT_TRANS_START 0x50
+#define I3C_HUB_TP_NET_CON_CONF 0x51
+#define TPN_NET_CON(n) BIT(n)
+
+#define I3C_HUB_TP_PULLUP_EN 0x53
+#define TPN_PULLUP_EN(n) BIT(n)
+
+#define I3C_HUB_TP_SCL_OUT_EN 0x54
+#define I3C_HUB_TP_SDA_OUT_EN 0x55
+#define I3C_HUB_TP_SCL_OUT_LEVEL 0x56
+#define I3C_HUB_TP_SDA_OUT_LEVEL 0x57
+
+#define I3C_HUB_TP_IN_DETECT_MODE_CONF 0x58
+#define SCL0145_IO_IN_DET_CFG_MASK GENMASK(1, 0)
+#define SCL0145_IO_IN_DET_CFG(x) (((x) << 0) & SCL0145_IO_IN_DET_CFG_MASK)
+#define SDA0145_IO_IN_DET_CFG_MASK GENMASK(3, 2)
+#define SDA0145_IO_IN_DET_CFG(x) (((x) << 2) & SDA0145_IO_IN_DET_CFG_MASK)
+#define SCL2367_IO_IN_DET_CFG_MASK GENMASK(5, 4)
+#define SCL2367_IO_IN_DET_CFG(x) (((x) << 4) & SCL2367_IO_IN_DET_CFG_MASK)
+#define SDA2367_IO_IN_DET_CFG_MASK GENMASK(7, 6)
+#define SDA2367_IO_IN_DET_CFG(x) (((x) << 6) & SDA2367_IO_IN_DET_CFG_MASK)
+
+#define I3C_HUB_TP_SCL_IN_DETECT_IBI_EN 0x59
+#define I3C_HUB_TP_SDA_IN_DETECT_IBI_EN 0x5A
+
+/* Target Ports Status Registers */
+#define I3C_HUB_TP_SCL_IN_LEVEL_STS 0x60
+#define I3C_HUB_TP_SDA_IN_LEVEL_STS 0x61
+#define I3C_HUB_TP_SCL_IN_DETECT_FLG 0x62
+#define I3C_HUB_TP_SDA_IN_DETECT_FLG 0x63
+
+/* SMBus Agent Configuration and Status Registers */
+#define I3C_HUB_TP0_SMBUS_AGNT_STS 0x64
+#define I3C_HUB_TP1_SMBUS_AGNT_STS 0x65
+#define I3C_HUB_TP2_SMBUS_AGNT_STS 0x66
+#define I3C_HUB_TP3_SMBUS_AGNT_STS 0x67
+#define I3C_HUB_TP4_SMBUS_AGNT_STS 0x68
+#define I3C_HUB_TP5_SMBUS_AGNT_STS 0x69
+#define I3C_HUB_TP6_SMBUS_AGNT_STS 0x6A
+#define I3C_HUB_TP7_SMBUS_AGNT_STS 0x6B
+
+#define I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF 0x6C
+#define TARGET_AGENT_BUF_FULL_SDA_LOW_EN BIT(5)
+
+/* Transaction status checking mask */
+#define I3C_HUB_CONTROLLER_AGENT_STATUS_MASK (0xF0 | BIT(0))
+#define I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG BIT(0)
+/* SMBus Controller Agent Return Codes */
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT 4
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS 0x0
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK 0x1
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY 0x2
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY 0x3
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED 0x4
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR 0x5
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT 0x6
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST 0x7
+#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT 0x8
+
+#define I3C_HUB_TARGET_BUF_STATUS_MASK GENMASK(3, 1)
+#define I3C_HUB_TARGET_BUF_0_RECEIVE BIT(1)
+#define I3C_HUB_TARGET_BUF_1_RECEIVE BIT(2)
+#define I3C_HUB_TARGET_BUF_OVRFL BIT(3)
+
+/* Special Function Registers */
+#define I3C_HUB_LDO_AND_CPSEL_STS 0x79
+#define CP_SDA1_LEVEL BIT(7)
+#define CP_SCL1_LEVEL BIT(6)
+#define CP_SEL_PIN_INPUT_CODE_MASK GENMASK(5, 4)
+#define CP_SEL_PIN_INPUT_CODE_GET(x) (((x) & CP_SEL_PIN_INPUT_CODE_MASK) >> 4)
+#define CP_SDA1_SCL1_PINS_CODE_MASK GENMASK(7, 6)
+#define CP_SDA1_SCL1_PINS_CODE_GET(x) (((x) & CP_SDA1_SCL1_PINS_CODE_MASK) >> 6)
+#define VCCIO1_PWR_GOOD BIT(3)
+#define VCCIO0_PWR_GOOD BIT(2)
+#define CP1_VCCIO_PWR_GOOD BIT(1)
+#define CP0_VCCIO_PWR_GOOD BIT(0)
+
+#define I3C_HUB_BUS_RESET_SCL_TIMEOUT 0x7A
+#define I3C_HUB_ONCHIP_TD_PROTO_ERR_FLG 0x7B
+#define I3C_HUB_DEV_CMD 0x7C
+#define I3C_HUB_ONCHIP_TD_STS 0x7D
+#define I3C_HUB_ONCHIP_TD_ADDR_CONF 0x7E
+#define I3C_HUB_PAGE_PTR 0x7F
+
+/* Paged Transaction Registers */
+#define I3C_HUB_CONTROLLER_BUFFER_PAGE 0x10
+#define I3C_HUB_CONTROLLER_AGENT_BUFF 0x80
+#define I3C_HUB_CONTROLLER_AGENT_BUFF_DATA 0x84
+#define I3C_HUB_TARGET_BUFF_LENGTH 0x80
+#define I3C_HUB_TARGET_BUFF_ADDRESS 0x81
+#define I3C_HUB_TARGET_BUFF_DATA 0x82
+
+/* TP DT setting */
+#define I3C_HUB_DT_TP_MODE_DISABLED 0x00
+#define I3C_HUB_DT_TP_MODE_I3C 0x01
+#define I3C_HUB_DT_TP_MODE_SMBUS 0x02
+#define I3C_HUB_DT_TP_MODE_GPIO 0x03
+#define I3C_HUB_DT_TP_MODE_NOT_DEFINED 0xFF
+
+/* TP IO mode */
+#define I3C_HUB_DT_TP_IO_MODE_OD_PP 0x00
+#define I3C_HUB_DT_TP_IO_MODE_OD 0x01
+#define I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED 0xFF
+
+/* SMBus transaction types fields */
+#define I3C_HUB_SMBUS_100_KHZ 0x00
+#define I3C_HUB_SMBUS_200_KHZ BIT(1)
+#define I3C_HUB_SMBUS_400_KHZ BIT(2)
+#define I3C_HUB_SMBUS_1000_KHZ (BIT(1) | BIT(2))
+
+/* SMBus xfer type for i3c_hub_smbus_msg xfer_type parameter */
+#define I3C_HUB_SMBUS_XFER_WRITE 0
+#define I3C_HUB_SMBUS_XFER_READ 1
+#define I3C_HUB_SMBUS_XFER_WR_RD 2
+
+/* Hub buffer size */
+#define I3C_HUB_CONTROLLER_BUFFER_SIZE 88
+#define I3C_HUB_TARGET_BUFFER_SIZE 80
+#define I3C_HUB_SMBUS_DESCRIPTOR_SIZE 4
+#define I3C_HUB_SMBUS_PAYLOAD_SIZE \
+ (I3C_HUB_CONTROLLER_BUFFER_SIZE - I3C_HUB_SMBUS_DESCRIPTOR_SIZE)
+#define I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE (I3C_HUB_TARGET_BUFFER_SIZE - 2)
+
+/* Hub SMBus status register read interval (microseconds, ceil) */
+#define I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL(len, clk_khz) \
+ DIV_ROUND_UP(1000U * 9U * (u32)(len), (u32)(clk_khz))
+
+/* ID Extraction */
+#define I3C_HUB_ID_CP_SDA_SCL 0x00
+#define I3C_HUB_ID_CP_SEL 0x01
+
+/* IBI */
+#define IBI_MAX_PAYLOAD_LEN 2
+#define IBI_SLOT_NUMS 6
+
+#define I3C_HUB_IO_CTRL_PAGE 0x81
+#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK 0xDB
+#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN BIT(6)
+#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_MASK GENMASK(5, 0)
+#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL 0x18
+
+#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK 0xDC
+#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN BIT(4)
+#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK GENMASK(3, 0)
+#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL(x) \
+ ((x) & I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK)
+
+#define I3C_HUB_EFUSE_PAGE 0x7B
+#define I3C_HUB_EFUSE_OFFSET_A0 0xA0
+#define I3C_HUB_FAST_RSON_EN BIT(5)
+#define I3C_HUB_EFUSE_OFFSET_A3 0xA3
+#define I3C_HUB_FAST_DRV_LOOP_DIS BIT(5)
+
+#define I3C_HUB_EFUSE_OFFSET_9D 0x9D
+#define I3C_HUB_TP_OD_VOL_LEVEL BIT(0)
+#define I3C_HUB_TP_OD_VREF BIT(1)
+
+#define I3C_HUB_EFUSE_OFFSET_9E 0x9E
+#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK GENMASK(5, 4)
+#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL(x) \
+ (((x) << 4) & I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK)
+#define I3C_HUB_IBI_ACK_RD_CYCLE_MASK GENMASK(3, 0)
+#define I3C_HUB_IBI_ACK_RD_CYCLE_VAL (5)
+
+struct i3c_hub_dev_info {
+ const char *model;
+ u16 part_id;
+ u8 n_ports;
+};
+
+static const struct i3c_hub_dev_info i3c_hub_dev_info_unknown = {
+ .model = "Unknown",
+ .part_id = 0,
+ .n_ports = 8,
+};
+
+static const struct i3c_hub_dev_info i3c_hub_dev_info_table[] = {
+ { "RTS4900", 0x4000, 4 }, { "RTS4901", 0x4100, 4 },
+ { "RTS4902", 0x8000, 8 }, { "RTS4903", 0x8100, 8 },
+ { "RTS4904", 0x4001, 4 }, { "RTS4906", 0x8001, 8 }
+};
+
+struct tp_setting {
+ u8 mode;
+ bool pullup_en;
+ u8 io_mode;
+ bool always_enable;
+ u32 poll_interval_ms;
+ u32 clock_frequency;
+};
+
+struct dt_settings {
+ bool cp0_ldo_en;
+ bool cp1_ldo_en;
+ bool tp0145_ldo_en;
+ bool tp2367_ldo_en;
+ u32 cp0_ldo_volt;
+ u32 cp1_ldo_volt;
+ u32 tp0145_ldo_volt;
+ u32 tp2367_ldo_volt;
+ u32 tp0145_pullup;
+ u32 tp2367_pullup;
+ u32 cp0_io_strength;
+ u32 cp1_io_strength;
+ u32 tp0145_io_strength;
+ u32 tp2367_io_strength;
+ struct tp_setting tp[I3C_HUB_TP_MAX_COUNT];
+ bool hub_net_always_i3c;
+ u8 tp_scl_h_ack_cycles;
+ bool handshake_optimize;
+ u8 fast_drv_h_add_cycles;
+ bool fast_rson_en;
+ bool tp_od_vol_optimize;
+ bool tp_od_vref_optimize;
+};
+
+struct smbus_backend {
+ struct i2c_client *client;
+ struct list_head list;
+};
+
+struct i2c_adapter_group {
+ struct i2c_adapter i2c;
+ u8 tp_mask;
+ u8 tp_port;
+ u8 used;
+ struct device_node *of_node;
+ struct i3c_hub *priv;
+ struct mutex mutex; /* protects SMBus adapter state and transfers */
+
+ struct delayed_work delayed_work_polling;
+ struct list_head backend_entry;
+ u8 last_processed_buf;
+
+ u8 status;
+ struct completion completion;
+};
+
+struct logical_bus {
+ bool available; /* Logical bus configuration is available in DT. */
+ bool registered; /* Logical bus was registered in the framework. */
+ u8 tp_id;
+ u8 tp_map;
+ struct i3c_master_controller controller;
+ struct device_node *of_node;
+ struct i3c_hub *priv;
+};
+
+struct hub_gpio {
+ struct gpio_chip chip;
+ int tp[GPIO_MAX_BANK];
+ s8 port_to_index[I3C_HUB_TP_MAX_COUNT];
+ int nums;
+ struct irq_chip irq_chip;
+ struct mutex irq_mutex; /* protects GPIO IRQ enable/disable operations */
+};
+
+struct i3c_hub {
+ struct i3c_device *i3cdev;
+ struct i3c_master_controller *driving_master;
+ struct regmap *regmap;
+ const struct i3c_hub_dev_info *dev_info;
+ struct dt_settings settings;
+ struct delayed_work delayed_work;
+ int hub_pin_sel_id;
+ int hub_pin_cp1_id;
+ int hub_dt_sel_id;
+ int hub_dt_cp1_id;
+
+ struct logical_bus logical_bus[I3C_HUB_TP_MAX_COUNT];
+ struct i2c_adapter_group smbus_port_adapter[I3C_HUB_TP_MAX_COUNT];
+ u8 smbus_ibi_en_mask;
+ struct mutex page_mutex; /* protects regmap page register access */
+
+ /* Offset for reading HUB's register. */
+ u8 reg_addr;
+ struct dentry *debug_dir;
+ struct hub_gpio gpio;
+};
+
+/* Global mutex for serializing regmap access across all i3c hubs. */
+static DEFINE_MUTEX(i3c_hub_regmap_mutex);
+
+static u8 i3c_hub_ldo_dt_to_reg(u32 microvolt)
+{
+ switch (microvolt) {
+ case 1100000:
+ return LDO_VOLTAGE_1_1V;
+ case 1200000:
+ return LDO_VOLTAGE_1_2V;
+ case 1800000:
+ return LDO_VOLTAGE_1_8V;
+ default:
+ return LDO_VOLTAGE_1_0V;
+ }
+}
+
+static u8 i3c_hub_pullup_dt_to_reg(u32 ohms)
+{
+ switch (ohms) {
+ case 250:
+ return PULLUP_250R;
+ case 500:
+ return PULLUP_500R;
+ case 1000:
+ return PULLUP_1K;
+ default:
+ return PULLUP_2K;
+ }
+}
+
+static u8 i3c_hub_io_strength_dt_to_reg(u32 ohms)
+{
+ switch (ohms) {
+ case 50:
+ return IO_STRENGTH_50_OHM;
+ case 40:
+ return IO_STRENGTH_40_OHM;
+ case 30:
+ return IO_STRENGTH_30_OHM;
+ default:
+ return IO_STRENGTH_20_OHM;
+ }
+}
+
+static bool i3c_hub_smbus_validate_clock_frequency(u32 hz)
+{
+ switch (hz) {
+ case 100000:
+ case 200000:
+ case 400000:
+ case 1000000:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static inline u8 i3c_hub_smbus_rate_bits_from_hz(u32 hz)
+{
+ switch (hz) {
+ case 100000:
+ return I3C_HUB_SMBUS_100_KHZ;
+ case 200000:
+ return I3C_HUB_SMBUS_200_KHZ;
+ case 1000000:
+ return I3C_HUB_SMBUS_1000_KHZ;
+ default:
+ return I3C_HUB_SMBUS_400_KHZ;
+ }
+}
+
+static void i3c_hub_tp_of_get_setting(struct device *dev,
+ const struct device_node *node,
+ struct tp_setting tp_setting[])
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ const char *mode_str, *io_mode_str;
+ struct device_node *tp_node;
+ u32 id, val;
+
+ for_each_available_child_of_node(node, tp_node) {
+ if (!tp_node->name || of_node_cmp(tp_node->name, "target-port"))
+ continue;
+
+ if (!tp_node->full_name ||
+ (sscanf(tp_node->full_name, "target-port@%u", &id) != 1)) {
+ dev_warn(dev,
+ "Invalid target port node found in DT: %s\n",
+ tp_node->full_name);
+ continue;
+ }
+
+ if (id >= priv->dev_info->n_ports) {
+ dev_warn(dev,
+ "Invalid target port index found in DT: %i\n",
+ id);
+ continue;
+ }
+
+ priv->smbus_port_adapter[id].of_node = tp_node;
+
+ if (!of_property_read_string(tp_node, "realtek,mode",
+ &mode_str)) {
+ if (!strcmp(mode_str, "i3c"))
+ tp_setting[id].mode = I3C_HUB_DT_TP_MODE_I3C;
+ else if (!strcmp(mode_str, "smbus"))
+ tp_setting[id].mode = I3C_HUB_DT_TP_MODE_SMBUS;
+ else if (!strcmp(mode_str, "gpio"))
+ tp_setting[id].mode = I3C_HUB_DT_TP_MODE_GPIO;
+ else if (!strcmp(mode_str, "disabled"))
+ tp_setting[id].mode =
+ I3C_HUB_DT_TP_MODE_DISABLED;
+ }
+
+ if (!of_property_read_string(tp_node, "realtek,io-mode",
+ &io_mode_str)) {
+ if (!strcmp(io_mode_str, "od"))
+ tp_setting[id].io_mode =
+ I3C_HUB_DT_TP_IO_MODE_OD;
+ else if (!strcmp(io_mode_str, "od-pp"))
+ tp_setting[id].io_mode =
+ I3C_HUB_DT_TP_IO_MODE_OD_PP;
+ }
+
+ tp_setting[id].pullup_en =
+ of_property_read_bool(tp_node, "realtek,pullup-enable");
+ tp_setting[id].always_enable =
+ of_property_read_bool(tp_node, "realtek,always-enable");
+
+ if (!of_property_read_u32(tp_node,
+ "realtek,polling-interval-ms", &val))
+ tp_setting[id].poll_interval_ms = val;
+
+ if (!of_property_read_u32(tp_node, "clock-frequency", &val)) {
+ if (i3c_hub_smbus_validate_clock_frequency(val))
+ tp_setting[id].clock_frequency = val;
+ else
+ dev_warn(dev,
+ "Unsupported TP%d smbus clock-frequency: %u Hz, using default %u Hz\n",
+ id, val,
+ tp_setting[id].clock_frequency);
+ }
+ }
+}
+
+static void i3c_hub_of_get_conf_static(struct device *dev,
+ const struct device_node *node)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ u8 u8val;
+ u32 val;
+
+ priv->settings.cp0_ldo_en =
+ of_property_read_bool(node, "realtek,cp0-ldo-enable");
+ priv->settings.cp1_ldo_en =
+ of_property_read_bool(node, "realtek,cp1-ldo-enable");
+ priv->settings.tp0145_ldo_en =
+ of_property_read_bool(node, "realtek,tp0145-ldo-enable");
+ priv->settings.tp2367_ldo_en =
+ of_property_read_bool(node, "realtek,tp2367-ldo-enable");
+
+ if (!of_property_read_u32(node, "realtek,cp0-ldo-microvolt", &val))
+ priv->settings.cp0_ldo_volt = val;
+ if (!of_property_read_u32(node, "realtek,cp1-ldo-microvolt", &val))
+ priv->settings.cp1_ldo_volt = val;
+ if (!of_property_read_u32(node, "realtek,tp0145-ldo-microvolt", &val))
+ priv->settings.tp0145_ldo_volt = val;
+ if (!of_property_read_u32(node, "realtek,tp2367-ldo-microvolt", &val))
+ priv->settings.tp2367_ldo_volt = val;
+
+ if (!of_property_read_u32(node, "realtek,tp0145-pullup-ohms", &val))
+ priv->settings.tp0145_pullup = val;
+ if (!of_property_read_u32(node, "realtek,tp2367-pullup-ohms", &val))
+ priv->settings.tp2367_pullup = val;
+
+ if (!of_property_read_u32(node,
+ "realtek,cp0-output-impedance-ohms", &val))
+ priv->settings.cp0_io_strength = val;
+ if (!of_property_read_u32(node,
+ "realtek,cp1-output-impedance-ohms", &val))
+ priv->settings.cp1_io_strength = val;
+ if (!of_property_read_u32(node,
+ "realtek,tp0145-output-impedance-ohms", &val))
+ priv->settings.tp0145_io_strength = val;
+ if (!of_property_read_u32(node,
+ "realtek,tp2367-output-impedance-ohms", &val))
+ priv->settings.tp2367_io_strength = val;
+
+ priv->settings.hub_net_always_i3c =
+ of_property_read_bool(node, "realtek,hub-net-always-i3c");
+
+ if (!of_property_read_u8(node, "realtek,tp-scl-h-ack-cycles", &u8val))
+ priv->settings.tp_scl_h_ack_cycles = u8val;
+
+ i3c_hub_tp_of_get_setting(dev, node, priv->settings.tp);
+
+ priv->settings.handshake_optimize =
+ of_property_read_bool(node, "realtek,handshake-optimize");
+
+ if (!of_property_read_u8(node, "realtek,fast-drv-h-add-cycles",
+ &u8val))
+ priv->settings.fast_drv_h_add_cycles = u8val;
+
+ priv->settings.fast_rson_en =
+ of_property_read_bool(node, "realtek,fast-rson-en");
+
+ priv->settings.tp_od_vol_optimize =
+ of_property_read_bool(node, "realtek,tp-od-vol-optimize");
+
+ priv->settings.tp_od_vref_optimize =
+ of_property_read_bool(node, "realtek,tp-od-vref-optimize");
+}
+
+static const struct i3c_hub_dev_info *
+i3c_hub_lookup_dev_info(struct i3c_hub *priv)
+{
+ int i, ret;
+ u16 part_id = 0;
+ u32 val = 0;
+
+ ret = regmap_read(priv->regmap, I3C_HUB_DEV_INFO_0, &val);
+ if (ret)
+ return ERR_PTR(ret);
+
+ part_id = (val & 0xFF) << 8;
+
+ ret = regmap_read(priv->regmap, I3C_HUB_DEV_REV, &val);
+ if (ret)
+ return ERR_PTR(ret);
+
+ part_id |= I3C_HUB_DEV_REV_LDO_GET(val);
+
+ for (i = 0; i < ARRAY_SIZE(i3c_hub_dev_info_table); i++) {
+ if (i3c_hub_dev_info_table[i].part_id == part_id)
+ return &i3c_hub_dev_info_table[i];
+ }
+ return &i3c_hub_dev_info_unknown;
+}
+
+static void i3c_hub_of_default_configuration(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ int id;
+
+ priv->settings.cp0_ldo_en = false;
+ priv->settings.cp1_ldo_en = false;
+ priv->settings.tp0145_ldo_en = false;
+ priv->settings.tp2367_ldo_en = false;
+ priv->settings.cp0_ldo_volt = 0;
+ priv->settings.cp1_ldo_volt = 0;
+ priv->settings.tp0145_ldo_volt = 0;
+ priv->settings.tp2367_ldo_volt = 0;
+ priv->settings.tp0145_pullup = UINT_MAX;
+ priv->settings.tp2367_pullup = UINT_MAX;
+ priv->settings.cp0_io_strength = 0;
+ priv->settings.cp1_io_strength = 0;
+ priv->settings.tp0145_io_strength = 0;
+ priv->settings.tp2367_io_strength = 0;
+ priv->settings.hub_net_always_i3c = false;
+ priv->settings.tp_scl_h_ack_cycles = 0;
+ priv->settings.handshake_optimize = false;
+ priv->settings.fast_drv_h_add_cycles = 3;
+ priv->settings.fast_rson_en = false;
+ priv->settings.tp_od_vol_optimize = false;
+ priv->settings.tp_od_vref_optimize = false;
+
+ for (id = 0; id < I3C_HUB_TP_MAX_COUNT; ++id) {
+ priv->settings.tp[id].mode = I3C_HUB_DT_TP_MODE_NOT_DEFINED;
+ priv->settings.tp[id].pullup_en = false;
+ priv->settings.tp[id].io_mode =
+ I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED;
+ priv->settings.tp[id].poll_interval_ms = 0;
+ priv->settings.tp[id].clock_frequency = 400000;
+ }
+}
+
+static int i3c_hub_hw_configure_pullup(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ u8 mask = 0, value = 0;
+
+ if (priv->settings.tp0145_pullup != UINT_MAX) {
+ mask |= TP0145_PULLUP_CONF_MASK;
+ if (priv->settings.tp0145_pullup != 0)
+ value |= TP0145_PULLUP_CONF(
+ i3c_hub_pullup_dt_to_reg(priv->settings.tp0145_pullup));
+ }
+
+ if (priv->settings.tp2367_pullup != UINT_MAX) {
+ mask |= TP2367_PULLUP_CONF_MASK;
+ if (priv->settings.tp2367_pullup != 0)
+ value |= TP2367_PULLUP_CONF(
+ i3c_hub_pullup_dt_to_reg(priv->settings.tp2367_pullup));
+ }
+
+ return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF,
+ mask, value);
+}
+
+static int i3c_hub_hw_configure_ldo(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ u8 ldo_config_mask = 0, ldo_config_val = 0;
+ u8 ldo_disable_mask = 0, ldo_en_val = 0;
+ u32 reg_val;
+ int ret;
+ u8 val;
+
+ /* Enable or Disable LDO's. If there is no DT entry - disable LDO for safety reasons */
+ if (priv->settings.cp0_ldo_en)
+ ldo_en_val |= CP0_LDO_EN;
+ if (priv->settings.cp1_ldo_en)
+ ldo_en_val |= CP1_LDO_EN;
+ if (priv->settings.tp0145_ldo_en)
+ ldo_en_val |= TP0145_LDO_EN;
+ if (priv->settings.tp2367_ldo_en)
+ ldo_en_val |= TP2367_LDO_EN;
+
+ /* Get current LDOs configuration */
+ ret = regmap_read(priv->regmap, I3C_HUB_LDO_CONF, ®_val);
+ if (ret)
+ return ret;
+
+ /* LDOs Voltage level (Skip if not defined in the DT)
+ * Set the mask only if there is a change from current value
+ */
+ if (priv->settings.cp0_ldo_volt != 0) {
+ val = CP0_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp0_ldo_volt));
+ if ((reg_val & CP0_LDO_VOLTAGE_MASK) != val) {
+ ldo_config_mask |= CP0_LDO_VOLTAGE_MASK;
+ ldo_disable_mask |= CP0_LDO_EN;
+ ldo_config_val |= val;
+ }
+ }
+ if (priv->settings.cp1_ldo_volt != 0) {
+ val = CP1_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp1_ldo_volt));
+ if ((reg_val & CP1_LDO_VOLTAGE_MASK) != val) {
+ ldo_config_mask |= CP1_LDO_VOLTAGE_MASK;
+ ldo_disable_mask |= CP1_LDO_EN;
+ ldo_config_val |= val;
+ }
+ }
+ if (priv->settings.tp0145_ldo_volt != 0) {
+ val = TP0145_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp0145_ldo_volt));
+ if ((reg_val & TP0145_LDO_VOLTAGE_MASK) != val) {
+ ldo_config_mask |= TP0145_LDO_VOLTAGE_MASK;
+ ldo_disable_mask |= TP0145_LDO_EN;
+ ldo_config_val |= val;
+ }
+ }
+ if (priv->settings.tp2367_ldo_volt != 0) {
+ val = TP2367_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp2367_ldo_volt));
+ if ((reg_val & TP2367_LDO_VOLTAGE_MASK) != val) {
+ ldo_config_mask |= TP2367_LDO_VOLTAGE_MASK;
+ ldo_disable_mask |= TP2367_LDO_EN;
+ ldo_config_val |= val;
+ }
+ }
+
+ /*
+ * Update LDO voltage configuration only if value is changed from already existing register
+ * value. It is a good practice to disable the LDO's before making any voltage changes.
+ * Presence of config mask indicates voltage change to be applied.
+ */
+ if (ldo_config_mask) {
+ /* Disable LDO's before making voltage changes */
+ ret = regmap_update_bits(priv->regmap,
+ I3C_HUB_LDO_AND_PULLUP_CONF,
+ ldo_disable_mask, 0);
+ if (ret)
+ return ret;
+
+ /* Update the LDOs configuration */
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_LDO_CONF,
+ ldo_config_mask, ldo_config_val);
+ if (ret)
+ return ret;
+ }
+
+ /* Update the LDOs Enable/disable register. This will enable only LDOs enabled in DT */
+ return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF,
+ LDO_ENABLE_DISABLE_MASK, ldo_en_val);
+}
+
+static int i3c_hub_hw_configure_io_strength(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ u8 mask_all = 0, val_all = 0;
+ u32 reg_val;
+ u8 val;
+ struct dt_settings tmp;
+ int ret;
+
+ /* Get IO strength configuration to figure out what needs to be changed */
+ ret = regmap_read(priv->regmap, I3C_HUB_IO_STRENGTH, ®_val);
+ if (ret)
+ return ret;
+
+ tmp = priv->settings;
+ if (tmp.cp0_io_strength != 0) {
+ val = CP0_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp0_io_strength));
+ mask_all |= CP0_IO_STRENGTH_MASK;
+ val_all |= val;
+ }
+ if (tmp.cp1_io_strength != 0) {
+ val = CP1_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp1_io_strength));
+ mask_all |= CP1_IO_STRENGTH_MASK;
+ val_all |= val;
+ }
+ if (tmp.tp0145_io_strength != 0) {
+ val = TP0145_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp0145_io_strength));
+ mask_all |= TP0145_IO_STRENGTH_MASK;
+ val_all |= val;
+ }
+ if (tmp.tp2367_io_strength != 0) {
+ val = TP2367_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp2367_io_strength));
+ mask_all |= TP2367_IO_STRENGTH_MASK;
+ val_all |= val;
+ }
+
+ /* Set IO strength if required */
+ return regmap_update_bits(priv->regmap, I3C_HUB_IO_STRENGTH, mask_all,
+ val_all);
+}
+
+static int i3c_hub_hw_configure_tp(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ u8 pullup_mask = 0, pullup_val = 0;
+ u8 smbus_mask = 0, smbus_val = 0;
+ u8 gpio_mask = 0, gpio_val = 0;
+ u8 i3c_mask = 0, i3c_val = 0;
+ u8 io_mode_mask = 0, io_mode_val = 0;
+ int ret;
+ int i, index;
+
+ memset(priv->gpio.port_to_index, -1, sizeof(priv->gpio.port_to_index));
+
+ for (i = 0; i < priv->dev_info->n_ports; ++i) {
+ if (priv->settings.tp[i].mode !=
+ I3C_HUB_DT_TP_MODE_NOT_DEFINED) {
+ i3c_mask |= TPN_NET_CON(i);
+ smbus_mask |= TPN_SMBUS_MODE_EN(i);
+ gpio_mask |= TPN_GPIO_MODE_EN(i);
+ io_mode_mask |= TPN_IO_MODE_CON(i);
+
+ if (priv->settings.tp[i].mode ==
+ I3C_HUB_DT_TP_MODE_I3C) {
+ i3c_val |= TPN_NET_CON(i);
+ } else if (priv->settings.tp[i].mode ==
+ I3C_HUB_DT_TP_MODE_SMBUS) {
+ smbus_val |= TPN_SMBUS_MODE_EN(i);
+ } else if (priv->settings.tp[i].mode ==
+ I3C_HUB_DT_TP_MODE_GPIO) {
+ gpio_val |= TPN_GPIO_MODE_EN(i);
+ priv->gpio.nums += GPIO_BANK_SZ;
+ index = priv->gpio.nums / GPIO_BANK_SZ - 1;
+ priv->gpio.tp[index] = i;
+ priv->gpio.port_to_index[i] = index;
+ }
+ }
+ pullup_mask |= TPN_PULLUP_EN(i);
+ if (priv->settings.tp[i].pullup_en)
+ pullup_val |= TPN_PULLUP_EN(i);
+ if (priv->settings.tp[i].io_mode !=
+ I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED) {
+ if (priv->settings.tp[i].io_mode ==
+ I3C_HUB_DT_TP_IO_MODE_OD)
+ io_mode_val |= TPN_IO_MODE_CON(i);
+ } else if (priv->settings.tp[i].mode ==
+ I3C_HUB_DT_TP_MODE_SMBUS) {
+ io_mode_val |= TPN_IO_MODE_CON(i);
+ }
+ }
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_IO_MODE_CONF,
+ io_mode_mask, io_mode_val);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_PULLUP_EN,
+ pullup_mask, pullup_val);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_SMBUS_AGNT_EN,
+ smbus_mask, smbus_val);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_GPIO_MODE_EN,
+ gpio_mask, gpio_val);
+ if (ret)
+ return ret;
+
+ /* Request for HUB Network connection in case any TP is configured in I3C mode */
+ if (i3c_val) {
+ ret = regmap_write(priv->regmap, I3C_HUB_CP_MUX_SET,
+ CONTROLLER_PORT_MUX_REQ);
+ if (ret)
+ return ret;
+ /* TODO: verify if connection is done */
+ }
+
+ /* Enable TP here in case TP was configured */
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_ENABLE,
+ i3c_mask | smbus_mask | gpio_mask,
+ i3c_val | smbus_val | gpio_val);
+ if (ret)
+ return ret;
+
+ return regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, i3c_val);
+}
+
+static int i3c_hub_hw_configure_misc(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ int ret;
+ u8 reg = I3C_HUB_TARGET_DA_FLAG_BYTE_BASE;
+ u8 val[I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT];
+
+ if (!priv->settings.hub_net_always_i3c)
+ return 0;
+
+ memset(val, 0xff, I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT);
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_NET_OPER_MODE_CONF,
+ I3C_HUB_NET_ALWAYS_I3C_EN,
+ I3C_HUB_NET_ALWAYS_I3C_EN);
+ if (ret)
+ return ret;
+
+ ret = regmap_bulk_write(priv->regmap, reg, val,
+ I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT);
+ return ret;
+}
+
+typedef int (*i3c_hub_cfg_fn)(struct i3c_hub *priv);
+
+static int i3c_hub_hw_cfg_with_page(struct i3c_hub *priv, u8 page,
+ i3c_hub_cfg_fn op)
+{
+ int ret;
+
+ if (!op)
+ return -EINVAL;
+
+ mutex_lock(&priv->page_mutex);
+ ret = regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, page);
+ if (ret)
+ goto unlock;
+
+ ret = op(priv);
+unlock:
+ regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, 0x00);
+ mutex_unlock(&priv->page_mutex);
+ return ret;
+}
+
+static int i3c_hub_cfg_op_fuse_latch(struct i3c_hub *priv)
+{
+ int ret;
+
+ if (priv->settings.handshake_optimize) {
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E,
+ I3C_HUB_IBI_ACK_RD_CYCLE_MASK,
+ I3C_HUB_IBI_ACK_RD_CYCLE_VAL);
+ if (ret)
+ return ret;
+ }
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A3,
+ I3C_HUB_FAST_DRV_LOOP_DIS,
+ I3C_HUB_FAST_DRV_LOOP_DIS);
+ if (ret)
+ return ret;
+
+ if (priv->settings.tp_od_vol_optimize) {
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D,
+ I3C_HUB_TP_OD_VOL_LEVEL,
+ I3C_HUB_TP_OD_VOL_LEVEL);
+ if (ret)
+ return ret;
+ }
+
+ if (priv->settings.tp_od_vref_optimize) {
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D,
+ I3C_HUB_TP_OD_VREF,
+ I3C_HUB_TP_OD_VREF);
+ if (ret)
+ return ret;
+ }
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E,
+ I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK,
+ I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL(
+ priv->settings.fast_drv_h_add_cycles));
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A0,
+ I3C_HUB_FAST_RSON_EN,
+ priv->settings.fast_rson_en ? I3C_HUB_FAST_RSON_EN : 0);
+ return ret;
+}
+
+static int i3c_hub_hw_configure_fuse_latch(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+
+ return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_EFUSE_PAGE,
+ i3c_hub_cfg_op_fuse_latch);
+}
+
+static int i3c_hub_cfg_op_io(struct i3c_hub *priv)
+{
+ int ret;
+ u8 reg = I3C_HUB_CFG_TP_SCL_L_ACK_CLK;
+
+ /* cfg tp scl low ack clk */
+ ret = regmap_write(priv->regmap, reg,
+ I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN |
+ I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL);
+ if (ret)
+ return ret;
+
+ /* cfg tp scl high ack clk */
+ reg = I3C_HUB_CFG_TP_SCL_H_ACK_CLK;
+ if (priv->settings.tp_scl_h_ack_cycles == 0)
+ return 0;
+
+ ret = regmap_write(priv->regmap, reg,
+ I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN |
+ I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL(
+ priv->settings.tp_scl_h_ack_cycles));
+
+ return ret;
+}
+
+static int i3c_hub_hw_configure_io(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+
+ return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_IO_CTRL_PAGE,
+ i3c_hub_cfg_op_io);
+}
+
+static int i3c_hub_configure_hw(struct device *dev)
+{
+ int ret;
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+
+ ret = i3c_hub_hw_configure_ldo(dev);
+ if (ret)
+ return ret;
+
+ ret = i3c_hub_hw_configure_io_strength(dev);
+ if (ret)
+ return ret;
+
+ ret = i3c_hub_hw_configure_pullup(dev);
+ if (ret)
+ return ret;
+
+ if (priv->dev_info->part_id) {
+ ret = i3c_hub_hw_configure_misc(dev);
+ if (ret)
+ return ret;
+
+ ret = i3c_hub_hw_configure_fuse_latch(dev);
+ if (ret)
+ return ret;
+
+ ret = i3c_hub_hw_configure_io(dev);
+ if (ret)
+ return ret;
+ }
+
+ ret = i3c_hub_hw_configure_tp(dev);
+ return ret;
+}
+
+static void i3c_hub_of_get_conf_runtime(struct device *dev,
+ const struct device_node *node)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ struct device_node *i3c_node;
+ int i3c_id;
+ u8 tp_mask;
+
+ for_each_available_child_of_node(node, i3c_node) {
+ if (!i3c_node->full_name ||
+ (sscanf(i3c_node->full_name, "i3c%i@%hhx", &i3c_id,
+ &tp_mask) != 2))
+ continue;
+
+ if (i3c_id < priv->dev_info->n_ports) {
+ priv->logical_bus[i3c_id].available = true;
+ priv->logical_bus[i3c_id].of_node = i3c_node;
+ priv->logical_bus[i3c_id].tp_map = tp_mask;
+ priv->logical_bus[i3c_id].priv = priv;
+ priv->logical_bus[i3c_id].tp_id = i3c_id;
+ }
+ }
+}
+
+static const struct i3c_device_id i3c_hub_ids[] = {
+ I3C_CLASS(I3C_DCR_HUB, NULL),
+ {},
+};
+
+static int i3c_hub_read_id(struct device *dev)
+{
+ struct i3c_hub *priv = dev_get_drvdata(dev);
+ u32 reg_val;
+ int ret;
+
+ ret = regmap_read(priv->regmap, I3C_HUB_LDO_AND_CPSEL_STS, ®_val);
+ if (ret) {
+ dev_err(dev, "Failed to read status register\n");
+ return -1;
+ }
+
+ priv->hub_pin_sel_id = CP_SEL_PIN_INPUT_CODE_GET(reg_val);
+ priv->hub_pin_cp1_id = CP_SDA1_SCL1_PINS_CODE_GET(reg_val);
+ return 0;
+}
+
+static struct device_node *i3c_hub_get_dt_hub_node(struct device_node *node,
+ struct i3c_hub *priv)
+{
+ struct device_node *hub_node_no_id = NULL;
+ struct device_node *hub_node;
+ u32 hub_id;
+ u32 id_mask;
+ u32 dt_id;
+ u32 pin_id;
+ int found_id = 0;
+
+ for_each_available_child_of_node(node, hub_node) {
+ id_mask = 0;
+ priv->hub_dt_sel_id = -1;
+ priv->hub_dt_cp1_id = -1;
+
+ if (strstr(hub_node->name, "hub")) {
+ if (!of_property_read_u32(hub_node, "realtek,id",
+ &hub_id)) {
+ id_mask |= 0x0f;
+ priv->hub_dt_sel_id = hub_id;
+ }
+
+ if (!of_property_read_u32(hub_node, "realtek,id-cp1",
+ &hub_id)) {
+ id_mask |= 0xf0;
+ priv->hub_dt_cp1_id = hub_id;
+ }
+
+ dt_id = ((u32)priv->hub_dt_cp1_id & 0x0f) << 4 |
+ ((u32)priv->hub_dt_sel_id & 0x0f);
+ pin_id = ((u32)priv->hub_pin_cp1_id & 0x0f) << 4 |
+ ((u32)priv->hub_pin_sel_id & 0x0f);
+
+ if (id_mask != 0 &&
+ (dt_id & id_mask) == (pin_id & id_mask))
+ found_id = 1;
+
+ if (!found_id) {
+ /*
+ * Just keep reference to first HUB node with no ID in case no ID
+ * matching
+ */
+ if (!hub_node_no_id &&
+ priv->hub_dt_sel_id == -1 &&
+ priv->hub_dt_cp1_id == -1)
+ hub_node_no_id = hub_node;
+ } else {
+ return hub_node;
+ }
+ }
+ }
+
+ return hub_node_no_id;
+}
+
+static int fops_access_reg_get(void *ctx, u64 *val)
+{
+ struct i3c_hub *priv = ctx;
+ u32 reg_val;
+ int ret;
+
+ ret = regmap_read(priv->regmap, priv->reg_addr, ®_val);
+ if (ret)
+ return ret;
+
+ *val = reg_val & 0xFF;
+ return 0;
+}
+
+static int fops_access_reg_set(void *ctx, u64 val)
+{
+ struct i3c_hub *priv = ctx;
+
+ return regmap_write(priv->regmap, priv->reg_addr, val & 0xFF);
+}
+
+DEFINE_DEBUGFS_ATTRIBUTE(fops_access_reg, fops_access_reg_get,
+ fops_access_reg_set, "0x%llX\n");
+
+static int i3c_hub_debugfs_init(struct i3c_hub *priv, const char *hub_id)
+{
+ struct dentry *entry, *dt_conf_dir, *reg_dir;
+ struct dt_settings *settings = NULL;
+ int i;
+
+ entry = debugfs_create_dir(hub_id, NULL);
+ if (IS_ERR(entry))
+ return PTR_ERR(entry);
+
+ priv->debug_dir = entry;
+
+ if (priv->dev_info)
+ debugfs_create_str("model", 0400, priv->debug_dir,
+ (char **)&priv->dev_info->model);
+
+ entry = debugfs_create_dir("dt-conf", priv->debug_dir);
+ if (IS_ERR(entry))
+ goto err_remove;
+
+ dt_conf_dir = entry;
+
+ settings = &priv->settings;
+ debugfs_create_bool("cp0-ldo-en", 0400, dt_conf_dir,
+ &settings->cp0_ldo_en);
+ debugfs_create_bool("cp1-ldo-en", 0400, dt_conf_dir,
+ &settings->cp1_ldo_en);
+ debugfs_create_u32("cp0-ldo-volt", 0400, dt_conf_dir,
+ &settings->cp0_ldo_volt);
+ debugfs_create_u32("cp1-ldo-volt", 0400, dt_conf_dir,
+ &settings->cp1_ldo_volt);
+ debugfs_create_bool("tp0145-ldo-en", 0400, dt_conf_dir,
+ &settings->tp0145_ldo_en);
+ debugfs_create_bool("tp2367-ldo-en", 0400, dt_conf_dir,
+ &settings->tp2367_ldo_en);
+ debugfs_create_u32("tp0145-ldo-volt", 0400, dt_conf_dir,
+ &settings->tp0145_ldo_volt);
+ debugfs_create_u32("tp2367-ldo-volt", 0400, dt_conf_dir,
+ &settings->tp2367_ldo_volt);
+ debugfs_create_u32("tp0145-pullup", 0400, dt_conf_dir,
+ &settings->tp0145_pullup);
+ debugfs_create_u32("tp2367-pullup", 0400, dt_conf_dir,
+ &settings->tp2367_pullup);
+ debugfs_create_bool("hub-net-always-i3c", 0400, dt_conf_dir,
+ &settings->hub_net_always_i3c);
+ debugfs_create_u8("tp-scl-h-ack-cycles", 0400, dt_conf_dir,
+ &settings->tp_scl_h_ack_cycles);
+ debugfs_create_bool("handshake-optimize", 0400, dt_conf_dir,
+ &settings->handshake_optimize);
+ debugfs_create_u8("fast-drv-h-add-cycles", 0400, dt_conf_dir,
+ &settings->fast_drv_h_add_cycles);
+ debugfs_create_bool("fast-rson-en", 0400, dt_conf_dir,
+ &settings->fast_rson_en);
+ debugfs_create_bool("tp-od-vol-optimize", 0400, dt_conf_dir,
+ &settings->tp_od_vol_optimize);
+ debugfs_create_bool("tp-od-vref-optimize", 0400, dt_conf_dir,
+ &settings->tp_od_vref_optimize);
+
+ for (i = 0; i < priv->dev_info->n_ports; ++i) {
+ char file_name[32];
+
+ sprintf(file_name, "tp%i.mode", i);
+ debugfs_create_u8(file_name, 0400, dt_conf_dir,
+ &settings->tp[i].mode);
+ sprintf(file_name, "tp%i.pullup_en", i);
+ debugfs_create_bool(file_name, 0400, dt_conf_dir,
+ &settings->tp[i].pullup_en);
+ sprintf(file_name, "tp%i.io_mode", i);
+ debugfs_create_u8(file_name, 0400, dt_conf_dir,
+ &settings->tp[i].io_mode);
+ sprintf(file_name, "tp%i.poll_interval_ms", i);
+ debugfs_create_u32(file_name, 0400, dt_conf_dir,
+ &settings->tp[i].poll_interval_ms);
+ sprintf(file_name, "tp%i.clock_frequency", i);
+ debugfs_create_u32(file_name, 0400, dt_conf_dir,
+ &settings->tp[i].clock_frequency);
+ }
+
+ entry = debugfs_create_dir("reg", priv->debug_dir);
+ if (IS_ERR(entry))
+ goto err_remove;
+
+ reg_dir = entry;
+
+ entry = debugfs_create_file_unsafe("access", 0600, reg_dir, priv,
+ &fops_access_reg);
+ if (IS_ERR(entry))
+ goto err_remove;
+
+ debugfs_create_u8("offset", 0600, reg_dir, &priv->reg_addr);
+
+ return 0;
+
+err_remove:
+ debugfs_remove_recursive(priv->debug_dir);
+ return PTR_ERR(entry);
+}
+
+static void i3c_hub_trans_pre_cb(struct logical_bus *bus)
+{
+ struct i3c_hub *priv = bus->priv;
+ struct device *dev = i3cdev_to_dev(priv->i3cdev);
+ int ret;
+
+ if (priv->settings.tp[bus->tp_id].always_enable)
+ return;
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF,
+ GENMASK(bus->tp_id, bus->tp_id), bus->tp_map);
+ if (ret)
+ dev_warn(dev, "Failed to open Target Port(s)\n");
+}
+
+static void i3c_hub_trans_post_cb(struct logical_bus *bus)
+{
+ struct i3c_hub *priv = bus->priv;
+ struct device *dev = i3cdev_to_dev(priv->i3cdev);
+ int ret;
+
+ if (priv->settings.tp[bus->tp_id].always_enable)
+ return;
+
+ ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF,
+ GENMASK(bus->tp_id, bus->tp_id), 0x00);
+ if (ret)
+ dev_warn(dev, "Failed to close Target Port(s)\n");
+}
+
+static struct logical_bus *bus_from_i3c_desc(struct i3c_dev_desc *desc)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(desc);
+
+ return container_of(controller, struct logical_bus, controller);
+}
+
+static struct logical_bus *bus_from_i2c_desc(struct i2c_dev_desc *desc)
+{
+ struct i3c_master_controller *controller = i2c_dev_get_master(desc);
+
+ return container_of(controller, struct logical_bus, controller);
+}
+
+static struct i3c_master_controller *
+parent_from_controller(struct i3c_master_controller *controller)
+{
+ struct logical_bus *bus =
+ container_of(controller, struct logical_bus, controller);
+
+ return bus->priv->driving_master;
+}
+
+static struct i3c_master_controller *
+parent_controller_from_i3c_desc(struct i3c_dev_desc *desc)
+{
+ struct i3c_master_controller *controller = i3c_dev_get_master(desc);
+ struct logical_bus *bus =
+ container_of(controller, struct logical_bus, controller);
+
+ return bus->priv->driving_master;
+}
+
+static struct i3c_master_controller *
+parent_controller_from_i2c_desc(struct i2c_dev_desc *desc)
+{
+ struct i3c_master_controller *controller = desc->common.master;
+ struct logical_bus *bus =
+ container_of(controller, struct logical_bus, controller);
+
+ return bus->priv->driving_master;
+}
+
+static struct i3c_master_controller *
+update_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc,
+ struct i3c_master_controller *parent)
+{
+ struct i3c_master_controller *orig_parent = desc->master;
+
+ desc->master = parent;
+
+ return orig_parent;
+}
+
+static void restore_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc,
+ struct i3c_master_controller *parent)
+{
+ desc->master = parent;
+}
+
+static int i3c_hub_read_transaction_status(struct i3c_hub *priv, u8 target_port,
+ u8 target_port_status, u32 data_len)
+{
+ unsigned int status_read;
+ int ret;
+ struct i2c_adapter_group *smbus =
+ &priv->smbus_port_adapter[target_port];
+ u32 smbus_clk = priv->settings.tp[target_port].clock_frequency / 1000;
+ u8 status;
+ u8 ret_code;
+
+ if (!priv->settings.tp[target_port].poll_interval_ms) {
+ ret = wait_for_completion_timeout(&smbus->completion,
+ smbus->i2c.timeout);
+ if (!ret) {
+ dev_err(&priv->i3cdev->dev,
+ "Status read timeout reached on target port %d\n",
+ target_port);
+ return -ETIMEDOUT;
+ }
+
+ status = (u8)smbus->status &
+ I3C_HUB_CONTROLLER_AGENT_STATUS_MASK;
+ } else {
+ ret = regmap_read_poll_timeout(priv->regmap,
+ target_port_status,
+ status_read,
+ (u8)status_read &
+ I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG,
+ I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL(
+ data_len, smbus_clk),
+ jiffies_to_usecs(smbus->i2c.timeout));
+
+ if (ret) {
+ dev_err(&priv->i3cdev->dev,
+ "Status read timeout reached on target port %d\n",
+ target_port);
+ return ret;
+ }
+
+ ret = regmap_write(priv->regmap, target_port_status,
+ I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG);
+ if (ret)
+ return ret;
+
+ status = (u8)status_read & I3C_HUB_CONTROLLER_AGENT_STATUS_MASK;
+ }
+
+ ret_code = status >> I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT;
+
+ switch (ret_code) {
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS:
+ return 0;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK:
+ dev_dbg(&priv->i3cdev->dev,
+ "TP%u SMBus: Address NACK (device not present)\n",
+ target_port);
+ return -ENXIO;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY:
+ dev_dbg(&priv->i3cdev->dev,
+ "TP%u SMBus: Device busy (data NACK after address ACK)\n",
+ target_port);
+ return -EREMOTEIO;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY:
+ dev_dbg(&priv->i3cdev->dev,
+ "TP%u SMBus: Device read not ready (read address NACK after write)\n",
+ target_port);
+ return -ENXIO;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED:
+ dev_dbg(&priv->i3cdev->dev,
+ "TP%u SMBus: Sync issue recovered (SDA stuck low, recovered by 9 SCL pulses)\n",
+ target_port);
+ return -EAGAIN;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR:
+ dev_dbg(&priv->i3cdev->dev,
+ "TP%u SMBus: Sync issue bus clear (recovered by SCL low 35ms)\n",
+ target_port);
+ return -EAGAIN;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT:
+ dev_err(&priv->i3cdev->dev,
+ "TP%u SMBus: Bus fault (SDA stuck low remains after recovery)\n",
+ target_port);
+ return -EIO;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST:
+ dev_dbg(&priv->i3cdev->dev, "TP%u SMBus: Arbitration lost\n",
+ target_port);
+ return -EAGAIN;
+ case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT:
+ dev_err(&priv->i3cdev->dev, "TP%u SMBus: SCL timeout\n",
+ target_port);
+ return -ETIMEDOUT;
+ default:
+ dev_err(&priv->i3cdev->dev,
+ "TP%u SMBus: Reserved/unknown return code 0x%x\n",
+ target_port, ret_code);
+ return -EIO;
+ }
+}
+
+/*
+ * i3c_hub_smbus_msg() - This starts a smbus transaction by writing a descriptor
+ * and a message to the hub registers. Controller buffer page is determined by multiplying the
+ * target port index by four and adding the base page number to it.
+ * @hub: a pointer to the i3c hub main structure
+ * @target_port: a number of the port where the transaction will happen
+ * @xfers: i2c_msg struct received from the master_xfers callback
+ * @nxfers_i: the number of the current message
+ * @xfer_type: transfer type:
+ * - I3C_HUB_SMBUS_XFER_WRITE (0): single write
+ * - I3C_HUB_SMBUS_XFER_READ (1): single read
+ * - I3C_HUB_SMBUS_XFER_WR_RD (2): write followed by read
+ * (uses xfers[nxfers_i] as write and xfers[nxfers_i+1] as read)
+ *
+ * Return: 0 on success, negative errno on failure from hub status or regmap ops.
+ * Note: for WR_RD the caller must ensure xfers[nxfers_i+1] exists, the address
+ * matches, and write_len + read_len <= I3C_HUB_SMBUS_PAYLOAD_SIZE.
+ */
+static int i3c_hub_smbus_msg(struct i3c_hub *hub, struct i2c_msg *xfers,
+ u8 target_port, u8 nxfers_i, u8 xfer_type)
+{
+ u8 transaction_type = I3C_HUB_SMBUS_400_KHZ;
+ u8 controller_buffer_page =
+ I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * target_port;
+ int write_length = 0, read_length = 0;
+ u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + target_port;
+ u8 target_port_code = BIT(target_port);
+ u8 rw_address = xfers[nxfers_i].addr << 1;
+ u8 desc[I3C_HUB_SMBUS_DESCRIPTOR_SIZE] = { 0 };
+ int ret = 0;
+
+ transaction_type =
+ i3c_hub_smbus_rate_bits_from_hz(hub->settings.tp[target_port].clock_frequency);
+
+ switch (xfer_type) {
+ case I3C_HUB_SMBUS_XFER_WRITE:
+ write_length = xfers[nxfers_i].len;
+ break;
+ case I3C_HUB_SMBUS_XFER_READ:
+ read_length = xfers[nxfers_i].len;
+ rw_address |= BIT(0);
+ break;
+ case I3C_HUB_SMBUS_XFER_WR_RD:
+ write_length = xfers[nxfers_i].len;
+ read_length = xfers[nxfers_i + 1].len;
+ transaction_type |= BIT(0);
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ /* Assemble descriptor */
+ desc[0] = rw_address;
+ desc[1] = transaction_type;
+ desc[2] = write_length;
+ desc[3] = read_length;
+
+ mutex_lock(&hub->page_mutex);
+ ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR,
+ controller_buffer_page);
+ if (ret)
+ goto unlock;
+
+ ret = regmap_bulk_write(hub->regmap, I3C_HUB_CONTROLLER_AGENT_BUFF,
+ desc, I3C_HUB_SMBUS_DESCRIPTOR_SIZE);
+ if (ret)
+ goto unlock;
+
+ if (write_length) {
+ ret = regmap_bulk_write(hub->regmap,
+ I3C_HUB_CONTROLLER_AGENT_BUFF_DATA,
+ xfers[nxfers_i].buf, write_length);
+ if (ret)
+ goto unlock;
+ }
+
+ ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
+ mutex_unlock(&hub->page_mutex);
+ if (ret)
+ return ret;
+
+ /* Start transaction */
+ ret = regmap_write(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_TRANS_START,
+ target_port_code);
+ if (ret)
+ return ret;
+
+ /* Get transaction status */
+ ret = i3c_hub_read_transaction_status(hub, target_port,
+ target_port_status,
+ write_length + read_length);
+ if (ret)
+ return ret;
+
+ /* if read_length is non-zero, read back the data */
+ if (read_length) {
+ mutex_lock(&hub->page_mutex);
+ ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR,
+ controller_buffer_page);
+ if (ret)
+ goto unlock;
+
+ if (xfer_type == I3C_HUB_SMBUS_XFER_READ) {
+ ret = regmap_bulk_read(hub->regmap,
+ I3C_HUB_CONTROLLER_AGENT_BUFF_DATA,
+ xfers[nxfers_i].buf, read_length);
+ } else {
+ ret = regmap_bulk_read(hub->regmap,
+ I3C_HUB_CONTROLLER_AGENT_BUFF_DATA +
+ write_length,
+ xfers[nxfers_i + 1].buf, read_length);
+ }
+ if (ret)
+ goto unlock;
+
+ ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
+ mutex_unlock(&hub->page_mutex);
+ }
+
+ return ret;
+unlock:
+ regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
+ mutex_unlock(&hub->page_mutex);
+ return ret;
+}
+
+static inline bool i3c_hub_can_combine_wr_rd(const struct i2c_msg *w,
+ const struct i2c_msg *r)
+{
+ /* w: write, r: read; same addr; total length within payload */
+ return !(w->flags & I2C_M_RD) && (r->flags & I2C_M_RD) &&
+ w->addr == r->addr &&
+ (w->len + r->len) <= I3C_HUB_SMBUS_PAYLOAD_SIZE;
+}
+
+/**
+ * i3c_hub_smbus_port_adapter_xfer() - i3c hub smbus transfer logic
+ * @adap: i2c_adapter corresponding with single port in the i3c hub
+ * @xfers: all messages descriptors and data
+ * @nxfers: amount of single messages in a transfer
+ *
+ * Return: function returns the sum of correctly sent messages (only those with hub return
+ * status 0x01)
+ */
+static int i3c_hub_smbus_port_adapter_xfer(struct i2c_adapter *adap,
+ struct i2c_msg *xfers, int nxfers)
+{
+ struct i2c_adapter_group *smbus = i2c_get_adapdata(adap);
+ struct i3c_hub *hub = smbus->priv;
+ int ret_sum = 0, ret, len, type, nxfers_i;
+ const struct i2c_msg *cur = NULL, *next = NULL;
+
+ for (nxfers_i = 0; nxfers_i < nxfers; nxfers_i++) {
+ cur = &xfers[nxfers_i];
+ len = cur->len;
+ type = cur->flags & I2C_M_RD ? I3C_HUB_SMBUS_XFER_READ :
+ I3C_HUB_SMBUS_XFER_WRITE;
+
+ /* Per-message length limit check */
+ if (len > I3C_HUB_SMBUS_PAYLOAD_SIZE) {
+ dev_err(&adap->dev,
+ "Message nr. %d not sent - length over %d bytes.\n",
+ nxfers_i, I3C_HUB_SMBUS_PAYLOAD_SIZE);
+ continue;
+ }
+
+ /* Try to combine write followed by read to the same address */
+ if (type == I3C_HUB_SMBUS_XFER_WRITE &&
+ (nxfers_i + 1) < nxfers) {
+ next = &xfers[nxfers_i + 1];
+ if (i3c_hub_can_combine_wr_rd(cur, next))
+ type = I3C_HUB_SMBUS_XFER_WR_RD;
+ }
+
+ ret = i3c_hub_smbus_msg(hub, xfers, smbus->tp_port, nxfers_i,
+ type);
+ if (ret)
+ return ret;
+
+ if (type == I3C_HUB_SMBUS_XFER_WR_RD) {
+ ret_sum += 2;
+ nxfers_i++; /* skip the next read message */
+
+ } else {
+ ret_sum++;
+ }
+ }
+ return ret_sum;
+}
+
+static int i3c_hub_bus_init(struct i3c_master_controller *controller)
+{
+ struct logical_bus *bus =
+ container_of(controller, struct logical_bus, controller);
+
+ controller->this = bus->priv->i3cdev->desc;
+ return 0;
+}
+
+static void i3c_hub_bus_cleanup(struct i3c_master_controller *controller)
+{
+ controller->this = NULL;
+}
+
+static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ int ret;
+
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ ret = parent->ops->attach_i3c_dev(dev);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ return ret;
+}
+
+static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev, u8 old_dyn_addr)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ int ret;
+
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ ret = parent->ops->reattach_i3c_dev(dev, old_dyn_addr);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ return ret;
+}
+
+static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ parent->ops->detach_i3c_dev(dev);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+}
+
+static int i3c_hub_do_daa(struct i3c_master_controller *controller)
+{
+ struct i3c_master_controller *parent =
+ parent_from_controller(controller);
+ int ret;
+
+ down_write(&parent->bus.lock);
+ ret = parent->ops->do_daa(parent);
+ up_write(&parent->bus.lock);
+ return ret;
+}
+
+static bool i3c_hub_supports_ccc_cmd(struct i3c_master_controller *controller,
+ const struct i3c_ccc_cmd *cmd)
+{
+ struct i3c_master_controller *parent =
+ parent_from_controller(controller);
+
+ return parent->ops->supports_ccc_cmd(parent, cmd);
+}
+
+static int i3c_hub_send_ccc_cmd(struct i3c_master_controller *controller,
+ struct i3c_ccc_cmd *cmd)
+{
+ struct i3c_master_controller *parent =
+ parent_from_controller(controller);
+ struct logical_bus *bus =
+ container_of(controller, struct logical_bus, controller);
+ int ret;
+
+ if (cmd->id == I3C_CCC_RSTDAA(true))
+ return 0;
+
+ i3c_hub_trans_pre_cb(bus);
+ down_read(&parent->bus.lock);
+ ret = parent->ops->send_ccc_cmd(parent, cmd);
+ up_read(&parent->bus.lock);
+ i3c_hub_trans_post_cb(bus);
+
+ return ret;
+}
+
+static int i3c_hub_priv_xfers(struct i3c_dev_desc *dev,
+ struct i3c_xfer *xfers, int nxfers,
+ enum i3c_xfer_mode mode)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ struct logical_bus *bus = bus_from_i3c_desc(dev);
+ int res;
+
+ i3c_hub_trans_pre_cb(bus);
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ down_read(&parent->bus.lock);
+ res = parent->ops->i3c_xfers(dev, xfers, nxfers, mode);
+ up_read(&parent->bus.lock);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ i3c_hub_trans_post_cb(bus);
+
+ return res;
+}
+
+static int i3c_hub_attach_i2c_dev(struct i2c_dev_desc *dev)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i2c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ int ret;
+
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ ret = parent->ops->attach_i2c_dev(dev);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ return ret;
+}
+
+static void i3c_hub_detach_i2c_dev(struct i2c_dev_desc *dev)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i2c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ parent->ops->detach_i2c_dev(dev);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+}
+
+static int i3c_hub_i2c_xfers(struct i2c_dev_desc *dev, struct i2c_msg *xfers,
+ int nxfers)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i2c_desc(dev);
+ struct logical_bus *bus = bus_from_i2c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ int ret;
+
+ i3c_hub_trans_pre_cb(bus);
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ ret = parent->ops->i2c_xfers(dev, xfers, nxfers);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ i3c_hub_trans_post_cb(bus);
+ return ret;
+}
+
+static int i3c_hub_request_ibi(struct i3c_dev_desc *dev,
+ const struct i3c_ibi_setup *req)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct logical_bus *bus = bus_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ int ret;
+
+ i3c_hub_trans_pre_cb(bus);
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ down_read(&parent->bus.lock);
+ ret = parent->ops->request_ibi(dev, req);
+ up_read(&parent->bus.lock);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ i3c_hub_trans_post_cb(bus);
+ return ret;
+}
+
+static void i3c_hub_free_ibi(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct logical_bus *bus = bus_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+
+ i3c_hub_trans_pre_cb(bus);
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ down_read(&parent->bus.lock);
+ parent->ops->free_ibi(dev);
+ up_read(&parent->bus.lock);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ i3c_hub_trans_post_cb(bus);
+}
+
+static int i3c_hub_enable_ibi(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct logical_bus *bus = bus_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ int ret;
+
+ i3c_hub_trans_pre_cb(bus);
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ down_read(&parent->bus.lock);
+ ret = parent->ops->enable_ibi(dev);
+ up_read(&parent->bus.lock);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ i3c_hub_trans_post_cb(bus);
+ return ret;
+}
+
+static int i3c_hub_disable_ibi(struct i3c_dev_desc *dev)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct logical_bus *bus = bus_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+ int ret;
+
+ i3c_hub_trans_pre_cb(bus);
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ down_read(&parent->bus.lock);
+ ret = parent->ops->disable_ibi(dev);
+ up_read(&parent->bus.lock);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+ i3c_hub_trans_post_cb(bus);
+ return ret;
+}
+
+static void i3c_hub_recycle_ibi_slot(struct i3c_dev_desc *dev,
+ struct i3c_ibi_slot *slot)
+{
+ struct i3c_master_controller *parent =
+ parent_controller_from_i3c_desc(dev);
+ struct i3c_master_controller *orig_parent;
+
+ orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
+ parent->ops->recycle_ibi_slot(dev, slot);
+ restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
+}
+
+static const struct i3c_master_controller_ops i3c_hub_i3c_ops = {
+ .bus_init = i3c_hub_bus_init,
+ .bus_cleanup = i3c_hub_bus_cleanup,
+ .attach_i3c_dev = i3c_hub_attach_i3c_dev,
+ .reattach_i3c_dev = i3c_hub_reattach_i3c_dev,
+ .detach_i3c_dev = i3c_hub_detach_i3c_dev,
+ .do_daa = i3c_hub_do_daa,
+ .supports_ccc_cmd = i3c_hub_supports_ccc_cmd,
+ .send_ccc_cmd = i3c_hub_send_ccc_cmd,
+ .i3c_xfers = i3c_hub_priv_xfers,
+ .attach_i2c_dev = i3c_hub_attach_i2c_dev,
+ .detach_i2c_dev = i3c_hub_detach_i2c_dev,
+ .i2c_xfers = i3c_hub_i2c_xfers,
+ .request_ibi = i3c_hub_request_ibi,
+ .free_ibi = i3c_hub_free_ibi,
+ .enable_ibi = i3c_hub_enable_ibi,
+ .disable_ibi = i3c_hub_disable_ibi,
+ .recycle_ibi_slot = i3c_hub_recycle_ibi_slot,
+};
+
+static int i3c_hub_logic_register(struct i3c_master_controller *controller,
+ struct device *parent)
+{
+ return i3c_master_register(controller, parent, &i3c_hub_i3c_ops, false);
+}
+
+static u32 i3c_hub_smbus_funcs(struct i2c_adapter *adapter)
+{
+ return (I2C_FUNC_SMBUS_EMUL | I2C_FUNC_I2C) & ~I2C_FUNC_SMBUS_QUICK;
+}
+
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+static int reg_i2c_target(struct i2c_client *client)
+{
+ struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter);
+ struct smbus_backend *backend;
+ int ret = 0;
+
+ if (!smbus)
+ return -EINVAL;
+
+ mutex_lock(&smbus->mutex);
+
+ list_for_each_entry(backend, &smbus->backend_entry, list) {
+ if (backend->client->addr == client->addr) {
+ ret = -EBUSY;
+ goto out;
+ }
+ }
+
+ backend = kzalloc(sizeof(*backend), GFP_KERNEL);
+ if (!backend) {
+ ret = -ENOMEM;
+ goto out;
+ }
+
+ backend->client = client;
+ list_add(&backend->list, &smbus->backend_entry);
+
+out:
+ mutex_unlock(&smbus->mutex);
+ return ret;
+}
+
+static int unreg_i2c_target(struct i2c_client *client)
+{
+ struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter);
+ struct smbus_backend *backend;
+ bool found = false;
+
+ if (!smbus)
+ return -EINVAL;
+
+ mutex_lock(&smbus->mutex);
+
+ list_for_each_entry(backend, &smbus->backend_entry, list) {
+ if (backend->client->addr == client->addr) {
+ list_del(&backend->list);
+ kfree(backend);
+ found = true;
+ break;
+ }
+ }
+
+ mutex_unlock(&smbus->mutex);
+ return found ? 0 : -ENODEV;
+}
+#endif /* CONFIG_I2C_SLAVE */
+
+static const struct i2c_algorithm i3c_hub_smbus_algo = {
+ .master_xfer = i3c_hub_smbus_port_adapter_xfer,
+ .functionality = i3c_hub_smbus_funcs,
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ .reg_slave = reg_i2c_target,
+ .unreg_slave = unreg_i2c_target,
+#endif
+};
+
+static void i3c_hub_delayed_work(struct work_struct *work)
+{
+ struct i3c_hub *priv =
+ container_of(work, typeof(*priv), delayed_work.work);
+ struct device *dev = i3cdev_to_dev(priv->i3cdev);
+ struct logical_bus *bus;
+ struct i2c_adapter_group *smbus;
+ int ret;
+ int i;
+ unsigned int reg_val = 0;
+
+ /* record reg 81: tp hubnetwork connection setting */
+ ret = regmap_read(priv->regmap, I3C_HUB_TP_NET_CON_CONF, ®_val);
+ if (ret) {
+ dev_warn(dev, "Failed to read hubnetwork connection setting\n");
+ return;
+ }
+
+ ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, 0x00);
+ if (ret) {
+ dev_warn(dev, "Failed to close Target Port(s)\n");
+ return;
+ }
+
+ for (i = 0; i < priv->dev_info->n_ports; ++i) {
+ bus = &priv->logical_bus[i];
+ if (bus->available) {
+ ret = regmap_update_bits(priv->regmap,
+ I3C_HUB_TP_NET_CON_CONF,
+ GENMASK(bus->tp_id, bus->tp_id),
+ bus->tp_map);
+ if (ret) {
+ dev_warn(dev,
+ "Failed to open Target Port(s)\n");
+ return;
+ }
+
+ dev->of_node = bus->of_node;
+ ret = i3c_hub_logic_register(&bus->controller, dev);
+ if (ret) {
+ dev_warn(dev,
+ "Failed to register i3c controller - bus id:%i\n",
+ i);
+ return;
+ }
+ bus->registered = true;
+
+ ret = regmap_update_bits(priv->regmap,
+ I3C_HUB_TP_NET_CON_CONF,
+ GENMASK(bus->tp_id, bus->tp_id),
+ 0x00);
+ if (ret) {
+ dev_warn(dev,
+ "Failed to close Target Port(s)\n");
+ return;
+ }
+
+ if (!priv->settings.tp[i].always_enable)
+ reg_val &= ~GENMASK(bus->tp_id, bus->tp_id);
+ }
+ }
+
+ /* update tp hubnetwork connection setting */
+ ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, reg_val);
+ if (ret) {
+ dev_warn(dev, "Failed to open Target Port(s)\n");
+ return;
+ }
+
+ ret = i3c_master_do_daa(priv->driving_master);
+ if (ret) {
+ dev_warn(dev, "Failed to run DAA\n");
+ return;
+ }
+
+ for (i = 0; i < priv->dev_info->n_ports; i++) {
+ smbus = &priv->smbus_port_adapter[i];
+ if (!smbus->used)
+ continue;
+
+ if (!priv->settings.tp[i].poll_interval_ms)
+ continue;
+
+ schedule_delayed_work(&smbus->delayed_work_polling,
+ msecs_to_jiffies(priv->settings.tp[i].poll_interval_ms));
+ }
+}
+
+static int send_smbus_target_data_to_backend(struct i2c_adapter_group *smbus,
+ u8 address, u8 *local_buffer,
+ u8 len)
+{
+#if IS_ENABLED(CONFIG_I2C_SLAVE)
+ struct smbus_backend *backend;
+ struct i2c_client *client;
+ int i, ret;
+ u8 tmp;
+
+ mutex_lock(&smbus->mutex);
+
+ list_for_each_entry(backend, &smbus->backend_entry, list) {
+ client = backend->client;
+ if (client->addr == address >> 1) {
+ mutex_unlock(&smbus->mutex);
+ ret = i2c_slave_event(client, I2C_SLAVE_WRITE_REQUESTED,
+ &address);
+ if (ret)
+ return ret;
+
+ for (i = 0; i < len; i++) {
+ ret = i2c_slave_event(client,
+ I2C_SLAVE_WRITE_RECEIVED,
+ &local_buffer[i]);
+ if (ret)
+ return ret;
+ }
+
+ return i2c_slave_event(client, I2C_SLAVE_STOP, &tmp);
+ }
+ }
+
+ mutex_unlock(&smbus->mutex);
+#endif /* CONFIG_I2C_SLAVE */
+ return -ENXIO;
+}
+
+static int read_smbus_target_buffer_page(struct i2c_adapter_group *smbus,
+ u8 target_buffer_page, u8 *address,
+ u8 *local_buffer, u8 *len)
+{
+ struct i3c_hub *hub = smbus->priv;
+ struct device *dev = i3cdev_to_dev(hub->i3cdev);
+ u32 status;
+ int ret;
+
+ mutex_lock(&hub->page_mutex);
+ regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, target_buffer_page);
+
+ ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_LENGTH, &status);
+ if (ret)
+ goto error;
+
+ *len = status - 1;
+ if (!*len)
+ goto error;
+
+ if (*len > I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE) {
+ dev_warn_ratelimited(dev,
+ "Received message too big for hub buffer\n");
+ ret = -EMSGSIZE;
+ *len = 0;
+ goto error;
+ }
+
+ ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_ADDRESS, &status);
+ if (ret)
+ goto error;
+
+ *address = status;
+
+ ret = regmap_bulk_read(hub->regmap, I3C_HUB_TARGET_BUFF_DATA,
+ local_buffer, *len);
+
+error:
+ regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
+ mutex_unlock(&hub->page_mutex);
+ return ret;
+}
+
+static int process_smbus_controller_status(struct i2c_adapter_group *smbus,
+ u8 reg, u32 status)
+{
+ struct i3c_hub *hub = smbus->priv;
+ int ret = 0;
+
+ if (status & I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG) {
+ smbus->status = status;
+ ret = regmap_write(hub->regmap, reg,
+ I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG);
+ complete(&smbus->completion);
+ }
+
+ return ret;
+}
+
+/**
+ * Controller buffer page is determined by adding the first buffer page number to port
+ * index multiplied by four. The two target buffer page numbers are determined the same
+ * way but they are offset by 2 and 3 from the controller page.
+ */
+static int process_smbus_target_status(struct i2c_adapter_group *smbus, u8 reg,
+ u32 status)
+{
+ struct i3c_hub *hub = smbus->priv;
+ struct device *dev = i3cdev_to_dev(hub->i3cdev);
+ u8 controller_buffer_page =
+ I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * smbus->tp_port;
+ u8 local_buffer[I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 };
+ u8 target_buffer_page, address = 0, len = 0, flag;
+ int ret;
+
+ if (smbus->last_processed_buf)
+ status &= ~smbus->last_processed_buf;
+
+ if (status & I3C_HUB_TARGET_BUF_0_RECEIVE) {
+ target_buffer_page = controller_buffer_page + 2;
+ flag = I3C_HUB_TARGET_BUF_0_RECEIVE;
+ } else if (status & I3C_HUB_TARGET_BUF_1_RECEIVE) {
+ target_buffer_page = controller_buffer_page + 3;
+ flag = I3C_HUB_TARGET_BUF_1_RECEIVE;
+ } else {
+ return -EINVAL;
+ }
+
+ ret = read_smbus_target_buffer_page(smbus, target_buffer_page, &address,
+ local_buffer, &len);
+ if (ret && ret != -EMSGSIZE) {
+ dev_dbg(dev, "Failed to read target buffer page: %d\n", ret);
+ return ret;
+ }
+
+ smbus->last_processed_buf = flag;
+
+ if (status & I3C_HUB_TARGET_BUF_OVRFL)
+ flag |= I3C_HUB_TARGET_BUF_OVRFL;
+
+ ret = regmap_write(hub->regmap, reg, flag);
+ if (ret) {
+ dev_dbg(dev, "Failed to clear target port status\n");
+ return ret;
+ }
+
+ if (len) {
+ ret = send_smbus_target_data_to_backend(smbus, address,
+ local_buffer, len);
+ if (ret) {
+ dev_dbg(dev, "Failed to send data to backend: %d\n",
+ ret);
+ return ret;
+ }
+ }
+
+ return 0;
+}
+
+static int i3c_hub_process_smbus_status(struct i2c_adapter_group *smbus)
+{
+ struct i3c_hub *hub = smbus->priv;
+ u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + smbus->tp_port;
+ struct device *dev = i3cdev_to_dev(hub->i3cdev);
+ u32 status;
+ int ret = 0;
+ u32 poll_interval_ms =
+ hub->settings.tp[smbus->tp_port].poll_interval_ms;
+
+ ret = regmap_read(hub->regmap, target_port_status, &status);
+ if (ret)
+ return ret;
+
+ /* smbus controller agent status */
+ if (!poll_interval_ms) {
+ ret = process_smbus_controller_status(smbus, target_port_status,
+ status);
+ if (ret)
+ dev_warn_ratelimited(dev,
+ "Failed to process smbus controller status\n");
+ }
+
+ /* smbus target agent status */
+ status &= I3C_HUB_TARGET_BUF_STATUS_MASK;
+
+ while (status) {
+ ret = process_smbus_target_status(smbus, target_port_status,
+ status);
+ if (ret) {
+ dev_warn_ratelimited(dev,
+ "Failed to process smbus target status: %d\n",
+ ret);
+ break;
+ }
+
+ if (!poll_interval_ms)
+ break;
+
+ ret = regmap_read(hub->regmap, target_port_status, &status);
+ if (ret)
+ break;
+ status &= I3C_HUB_TARGET_BUF_STATUS_MASK;
+ }
+
+ return ret;
+}
+
+/**
+ * i3c_hub_delayed_work_polling() - This delayed work is a polling mechanism to
+ * find if any transaction happened.
+ */
+static void i3c_hub_delayed_work_polling(struct work_struct *work)
+{
+ struct i2c_adapter_group *smbus =
+ container_of(work, typeof(*smbus), delayed_work_polling.work);
+ struct device *dev = i3cdev_to_dev(smbus->priv->i3cdev);
+ int ret;
+
+ if (!list_empty(&smbus->backend_entry)) {
+ ret = i3c_hub_process_smbus_status(smbus);
+ if (ret)
+ dev_warn_ratelimited(dev,
+ "Failed to process TP %u smbus status: %d\n",
+ smbus->tp_port, ret);
+ }
+
+ schedule_delayed_work(&smbus->delayed_work_polling,
+ msecs_to_jiffies(
+ smbus->priv->settings.tp[smbus->tp_port].poll_interval_ms));
+}
+
+static int i3c_hub_smbus_ibi_handler(struct i3c_hub *hub,
+ const struct i3c_ibi_payload *payload)
+{
+ struct i2c_adapter_group *smbus;
+ u8 tp, tps;
+ int val, ret = 0, rc;
+ struct device *dev = i3cdev_to_dev(hub->i3cdev);
+
+ if (payload->len < 2) {
+ ret = regmap_read(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_IBI_STS,
+ &val);
+ if (ret)
+ return ret;
+
+ tps = (u8)val;
+ } else {
+ tps = ((const u8 *)payload->data)[1];
+ }
+
+ if (!tps)
+ return 0;
+
+ while (tps) {
+ tp = (u8)__ffs((unsigned long)tps);
+ tps &= (tps - 1);
+
+ if (hub->settings.tp[tp].poll_interval_ms)
+ continue;
+
+ smbus = &hub->smbus_port_adapter[tp];
+ rc = i3c_hub_process_smbus_status(smbus);
+ if (rc) {
+ dev_warn_ratelimited(dev,
+ "Failed to process TP %u smbus status: %d\n",
+ tp, rc);
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * Sysfs attribute: clock_frequency
+ * Read/Write the SMBus clock frequency for this adapter's port.
+ */
+static ssize_t clock_frequency_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct i2c_adapter *adap = to_i2c_adapter(dev);
+ struct i2c_adapter_group *smbus = i2c_get_adapdata(adap);
+ struct i3c_hub *hub = smbus->priv;
+
+ return sprintf(buf, "%u\n",
+ hub->settings.tp[smbus->tp_port].clock_frequency);
+}
+
+static ssize_t clock_frequency_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct i2c_adapter *adap = to_i2c_adapter(dev);
+ struct i2c_adapter_group *smbus = i2c_get_adapdata(adap);
+ struct i3c_hub *hub = smbus->priv;
+ u32 val;
+ int ret;
+
+ ret = kstrtou32(buf, 0, &val);
+ if (ret)
+ return ret;
+
+ if (!i3c_hub_smbus_validate_clock_frequency(val))
+ return -EINVAL;
+
+ hub->settings.tp[smbus->tp_port].clock_frequency = val;
+
+ return count;
+}
+static DEVICE_ATTR_RW(clock_frequency);
+
+static struct attribute *i3c_hub_smbus_attrs[] = {
+ &dev_attr_clock_frequency.attr,
+ NULL,
+};
+
+ATTRIBUTE_GROUPS(i3c_hub_smbus);
+
+static int i3c_hub_smbus_tp_algo(struct i3c_hub *priv, int i)
+{
+ struct device *dev = i3cdev_to_dev(priv->i3cdev);
+ int ret;
+ struct i2c_adapter_group *smbus = &priv->smbus_port_adapter[i];
+ struct i2c_adapter *i2c = &smbus->i2c;
+
+ mutex_init(&smbus->mutex);
+ INIT_LIST_HEAD(&smbus->backend_entry);
+ smbus->priv = priv;
+ smbus->tp_port = i;
+ smbus->tp_mask = BIT(i);
+
+ init_completion(&smbus->completion);
+ i2c->owner = THIS_MODULE;
+ i2c->algo = &i3c_hub_smbus_algo;
+ i2c->dev.parent = dev;
+ i2c->dev.of_node = smbus->of_node;
+ i2c->dev.groups = i3c_hub_smbus_groups;
+ i2c->timeout = HZ;
+ i2c->retries = 3;
+ snprintf(i2c->name, sizeof(i2c->name), "hub%s.port%d", dev_name(dev),
+ smbus->tp_port);
+
+ if (priv->settings.tp[i].poll_interval_ms) {
+ ret = regmap_clear_bits(priv->regmap, I3C_HUB_TP_IBI_CONF,
+ smbus->tp_mask);
+ if (ret)
+ return ret;
+ INIT_DELAYED_WORK(&smbus->delayed_work_polling,
+ i3c_hub_delayed_work_polling);
+ priv->smbus_ibi_en_mask &= ~smbus->tp_mask;
+ } else {
+ ret = regmap_set_bits(priv->regmap, I3C_HUB_TP_IBI_CONF,
+ smbus->tp_mask);
+ if (ret)
+ return ret;
+ priv->smbus_ibi_en_mask |= smbus->tp_mask;
+ }
+
+ /* Enable SDA hold-low when both SMBus Target Agent buffers are full.
+ * Used as a flow-control mechanism for MCTP to avoid upstream TX timeout
+ * when target buffers are not serviced in time.
+ */
+ ret = regmap_set_bits(priv->regmap,
+ I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF,
+ TARGET_AGENT_BUF_FULL_SDA_LOW_EN);
+ if (ret)
+ return ret;
+
+ i2c_set_adapdata(i2c, smbus);
+
+ ret = i2c_add_adapter(i2c);
+ if (ret)
+ return ret;
+
+ smbus->used = 1;
+ return ret;
+}
+
+static int i3c_hub_gpio_direction_input(struct gpio_chip *gc, unsigned int off)
+{
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ int ret = 0;
+ u8 reg, mask = 0;
+
+ reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN :
+ I3C_HUB_TP_SCL_OUT_EN;
+ mask = BIT(gpio->tp[off / GPIO_BANK_SZ]);
+
+ ret = regmap_update_bits(hub->regmap, reg, mask, 0);
+ return ret;
+}
+
+static int i3c_hub_gpio_direction_output(struct gpio_chip *gc, unsigned int off,
+ int val)
+{
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ int ret = 0;
+ u8 reg, mask = 0;
+
+ reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN :
+ I3C_HUB_TP_SCL_OUT_EN;
+ mask = BIT(gpio->tp[off / GPIO_BANK_SZ]);
+
+ ret = regmap_update_bits(hub->regmap, reg, mask, mask);
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(hub->regmap, reg + 2, mask, val ? mask : 0);
+ return ret;
+}
+
+static int i3c_hub_gpio_get_value(struct gpio_chip *gc, unsigned int off)
+{
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ int ret = 0, val = 0, dir;
+ u8 reg, shift = 0;
+
+ dir = gc->get_direction(gc, off);
+ if (dir)
+ reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_LEVEL_STS :
+ I3C_HUB_TP_SCL_IN_LEVEL_STS;
+ else
+ reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL :
+ I3C_HUB_TP_SCL_OUT_LEVEL;
+
+ shift = gpio->tp[off / GPIO_BANK_SZ];
+
+ ret = regmap_read(hub->regmap, reg, &val);
+ if (ret)
+ return ret;
+
+ ret = (val >> shift) & 0x01;
+ return ret;
+}
+
+static int i3c_hub_gpio_set_value(struct gpio_chip *gc, unsigned int off, int val)
+{
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ u8 reg, mask = 0;
+
+ reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL :
+ I3C_HUB_TP_SCL_OUT_LEVEL;
+ mask = BIT(gpio->tp[off / GPIO_BANK_SZ]);
+
+ return regmap_update_bits(hub->regmap, reg, mask, val ? mask : 0);
+}
+
+static int i3c_hub_gpio_get_direction(struct gpio_chip *gc, unsigned int off)
+{
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ int ret = 0, dir = 0;
+ u8 reg, shift = 0;
+
+ reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN :
+ I3C_HUB_TP_SCL_OUT_EN;
+ shift = gpio->tp[off / GPIO_BANK_SZ];
+
+ ret = regmap_read(hub->regmap, reg, &dir);
+ if (ret)
+ return ret;
+
+ ret = ~(dir >> shift) & 0x01;
+ return ret;
+}
+
+static void i3c_hub_gpio_irq_mask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ u8 reg, hwirq = 0, mask = 0;
+
+ hwirq = irqd_to_hwirq(d);
+
+ reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN :
+ I3C_HUB_TP_SCL_IN_DETECT_IBI_EN;
+ mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]);
+
+ regmap_update_bits(hub->regmap, reg, mask, 0);
+}
+
+static void i3c_hub_gpio_irq_unmask(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ u8 reg, hwirq = 0, mask = 0;
+
+ hwirq = irqd_to_hwirq(d);
+
+ reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN :
+ I3C_HUB_TP_SCL_IN_DETECT_IBI_EN;
+ mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]);
+
+ regmap_update_bits(hub->regmap, reg, mask, mask);
+}
+
+static int i3c_hub_gpio_irq_set_type(struct irq_data *d, unsigned int flow_type)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+ u8 hwirq = 0, mask = 0, val, tp, reg;
+ int ret;
+
+ if (!(flow_type & IRQ_TYPE_EDGE_BOTH)) {
+ dev_err(&hub->i3cdev->dev, "irq %d: unsupported type %d\n",
+ d->irq, flow_type);
+ return -EINVAL;
+ }
+
+ hwirq = irqd_to_hwirq(d);
+ tp = gpio->tp[hwirq / GPIO_BANK_SZ];
+
+ if (tp == 0 || tp == 1 || tp == 4 || tp == 5) {
+ if (hwirq % GPIO_BANK_SZ) {
+ mask = SDA0145_IO_IN_DET_CFG_MASK;
+ val = SDA0145_IO_IN_DET_CFG(flow_type);
+ reg = I3C_HUB_TP_SDA_IN_DETECT_FLG;
+ } else {
+ mask = SCL0145_IO_IN_DET_CFG_MASK;
+ val = SCL0145_IO_IN_DET_CFG(flow_type);
+ reg = I3C_HUB_TP_SCL_IN_DETECT_FLG;
+ }
+ } else {
+ if (hwirq % GPIO_BANK_SZ) {
+ mask = SDA2367_IO_IN_DET_CFG_MASK;
+ val = SDA2367_IO_IN_DET_CFG(flow_type);
+ reg = I3C_HUB_TP_SDA_IN_DETECT_FLG;
+ } else {
+ mask = SCL2367_IO_IN_DET_CFG_MASK;
+ val = SCL2367_IO_IN_DET_CFG(flow_type);
+ reg = I3C_HUB_TP_SCL_IN_DETECT_FLG;
+ }
+ }
+
+ ret = regmap_write(hub->regmap, reg, BIT(tp));
+ if (ret)
+ return ret;
+
+ ret = regmap_update_bits(hub->regmap, I3C_HUB_TP_IN_DETECT_MODE_CONF,
+ mask, val);
+ return ret;
+}
+
+static void i3c_hub_gpio_irq_bus_lock(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+
+ mutex_lock(&gpio->irq_mutex);
+}
+
+static void i3c_hub_gpio_irq_bus_unlock(struct irq_data *d)
+{
+ struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
+ struct i3c_hub *hub = gpiochip_get_data(gc);
+ struct hub_gpio *gpio = &hub->gpio;
+
+ mutex_unlock(&gpio->irq_mutex);
+}
+
+static void i3c_hub_setup_gpio(struct i3c_hub *hub)
+{
+ struct hub_gpio *gpio = &hub->gpio;
+ struct gpio_chip *gc = &gpio->chip;
+ struct gpio_irq_chip *girq;
+
+ gc->direction_input = i3c_hub_gpio_direction_input;
+ gc->direction_output = i3c_hub_gpio_direction_output;
+ gc->get = i3c_hub_gpio_get_value;
+ gc->set = i3c_hub_gpio_set_value;
+ gc->get_direction = i3c_hub_gpio_get_direction;
+ gc->can_sleep = true;
+
+ gc->base = -1;
+ gc->ngpio = gpio->nums;
+ gc->label = dev_name(&hub->i3cdev->dev);
+ gc->parent = &hub->i3cdev->dev;
+ gc->owner = THIS_MODULE;
+
+ /* irq_chip support */
+ mutex_init(&gpio->irq_mutex);
+
+ gpio->irq_chip.name = dev_name(&hub->i3cdev->dev);
+ gpio->irq_chip.irq_mask = i3c_hub_gpio_irq_mask;
+ gpio->irq_chip.irq_unmask = i3c_hub_gpio_irq_unmask;
+ gpio->irq_chip.irq_set_type = i3c_hub_gpio_irq_set_type;
+ gpio->irq_chip.irq_bus_lock = i3c_hub_gpio_irq_bus_lock;
+ gpio->irq_chip.irq_bus_sync_unlock = i3c_hub_gpio_irq_bus_unlock;
+
+ girq = &gpio->chip.irq;
+
+ /* This will let us handle the parent IRQ in the driver */
+ girq->parent_handler = NULL;
+ girq->num_parents = 0;
+ girq->parents = NULL;
+ girq->default_type = IRQ_TYPE_NONE;
+ girq->handler = handle_simple_irq;
+ girq->threaded = true;
+ girq->first = 0;
+
+ girq->chip = &gpio->irq_chip;
+}
+
+static void i3c_hub_io_ibi_handler(struct i3c_hub *hub,
+ const struct i3c_ibi_payload *payload)
+{
+ struct hub_gpio *gpio = &hub->gpio;
+ struct gpio_chip *gc = &gpio->chip;
+ u8 level, hwirq, tmp;
+ u8 pending[GPIO_BANK_SZ];
+ u8 tp[GPIO_BANK_SZ];
+ int i, irq, ret, index;
+
+ ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_OUT_EN, tp,
+ GPIO_BANK_SZ);
+ if (ret) {
+ dev_err(&hub->i3cdev->dev, "Failed to read OUT_EN: %d\n", ret);
+ return;
+ }
+
+ ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_IN_DETECT_FLG,
+ pending, GPIO_BANK_SZ);
+ if (ret) {
+ dev_err(&hub->i3cdev->dev, "Failed to read DETECT_FLG: %d\n",
+ ret);
+ return;
+ }
+
+ for (i = 0; i < GPIO_BANK_SZ; i++) {
+ tmp = ~tp[i] & pending[i];
+
+ while (tmp) {
+ level = __ffs(tmp);
+ tmp &= ~(1 << level);
+
+ /* Check if this port is in GPIO mode */
+ index = gpio->port_to_index[level];
+ if (index < 0) {
+ /* Non-GPIO mode port, skip without clearing.
+ * This can happen because IN_DETECT IBI enable is
+ * configured in groups (e.g., TP0145/TP2367), not
+ * per individual port. Simply skip - the flag is
+ * harmless and will be overwritten by next detection.
+ */
+ dev_dbg(&hub->i3cdev->dev,
+ "IBI detect flag on non-GPIO port %d, skipping\n",
+ level);
+ continue;
+ }
+
+ hwirq = index * 2 + i;
+ irq = irq_find_mapping(gc->irq.domain, hwirq);
+
+ /* Clear the flag after processing */
+ regmap_write(hub->regmap,
+ I3C_HUB_TP_SCL_IN_DETECT_FLG + i,
+ BIT(level));
+
+ if (unlikely(irq <= 0)) {
+ dev_warn_ratelimited(gc->parent,
+ "unmapped interrupt %d\n",
+ hwirq);
+ } else {
+ handle_nested_irq(irq);
+ }
+ }
+ }
+}
+
+static void i3c_hub_ibi_handler(struct i3c_device *dev,
+ const struct i3c_ibi_payload *payload)
+{
+ struct i3c_hub *priv = i3cdev_get_drvdata(dev);
+ int ret, val = 0;
+ u8 status = 0;
+
+ if (!payload->len) {
+ dev_dbg(&dev->dev,
+ "Zero-length IBI payload, reading status register\n");
+ ret = regmap_read(priv->regmap, I3C_HUB_DEV_AND_IBI_STS, &val);
+ if (ret) {
+ dev_warn_ratelimited(&dev->dev,
+ "Failed to read IBI status: %d\n",
+ ret);
+ return;
+ }
+ status = (u8)val;
+ } else {
+ if (!payload->data) {
+ dev_warn_ratelimited(&dev->dev,
+ "IBI payload data is NULL with len=%d\n",
+ payload->len);
+ return;
+ }
+ status = ((const u8 *)payload->data)[0];
+ }
+
+ if (status & TP_IO_FLAG_STATUS)
+ i3c_hub_io_ibi_handler(priv, payload);
+
+ if (status & SMBUS_AGENT_EVENT_FLAG_STATUS) {
+ ret = i3c_hub_smbus_ibi_handler(priv, payload);
+ if (ret) {
+ dev_warn_ratelimited(&dev->dev,
+ "Failed to handle SMBus IBI: %d\n",
+ ret);
+ return;
+ }
+ }
+}
+
+static inline void i3c_hub_regmap_lock(void *ctx)
+{
+ mutex_lock(&i3c_hub_regmap_mutex);
+}
+
+static inline void i3c_hub_regmap_unlock(void *ctx)
+{
+ mutex_unlock(&i3c_hub_regmap_mutex);
+}
+
+static int i3c_hub_probe(struct i3c_device *i3cdev)
+{
+ const struct regmap_config i3c_hub_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .lock = i3c_hub_regmap_lock,
+ .unlock = i3c_hub_regmap_unlock,
+ .lock_arg = NULL,
+ };
+ struct device *dev = &i3cdev->dev;
+ struct device_node *node = NULL;
+ struct regmap *regmap;
+ struct i3c_hub *priv;
+ char hub_id[32];
+ int ret;
+ int i;
+ struct i3c_ibi_setup ibireq = {};
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->i3cdev = i3cdev;
+ priv->driving_master = i3c_dev_get_master(i3cdev->desc);
+ mutex_init(&priv->page_mutex);
+ i3cdev_set_drvdata(i3cdev, priv);
+ INIT_DELAYED_WORK(&priv->delayed_work, i3c_hub_delayed_work);
+ i3c_hub_of_default_configuration(dev);
+
+ regmap = devm_regmap_init_i3c(i3cdev, &i3c_hub_regmap_config);
+ if (IS_ERR(regmap)) {
+ ret = PTR_ERR(regmap);
+ dev_err(dev, "Failed to register I3C HUB regmap\n");
+ return ret;
+ }
+ priv->regmap = regmap;
+
+ priv->dev_info = i3c_hub_lookup_dev_info(priv);
+ if (IS_ERR(priv->dev_info)) {
+ ret = PTR_ERR(priv->dev_info);
+ dev_err(dev, "Failed to lookup HUB dev info\n");
+ return ret;
+ }
+
+ sprintf(hub_id, "i3c-hub-%d-%llx",
+ i3c_dev_get_master(i3cdev->desc)->bus.id,
+ i3cdev->desc->info.pid);
+ ret = i3c_hub_debugfs_init(priv, hub_id);
+ if (ret)
+ dev_dbg(dev, "Failed to initialized DebugFS.\n");
+
+ ret = i3c_hub_read_id(dev);
+ if (ret)
+ goto error;
+
+ priv->hub_dt_sel_id = -1;
+ priv->hub_dt_cp1_id = -1;
+ if (priv->hub_pin_cp1_id >= 0 && priv->hub_pin_sel_id >= 0)
+ /* Find hub node in DT matching HW ID or just first without ID provided in DT */
+ node = i3c_hub_get_dt_hub_node(dev->parent->of_node, priv);
+
+ if (!node) {
+ dev_info(dev,
+ "No DT entry - running with hardware defaults.\n");
+ } else {
+ of_node_get(node);
+ i3c_hub_of_get_conf_static(dev, node);
+ i3c_hub_of_get_conf_runtime(dev, node);
+ of_node_put(node);
+ }
+
+ /* Unlock access to protected registers */
+ ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE,
+ REGISTERS_UNLOCK_CODE);
+ if (ret) {
+ dev_err(dev, "Failed to unlock HUB's protected registers\n");
+ goto error;
+ }
+
+ /* Register logic for native smbus ports */
+ for (i = 0; i < priv->dev_info->n_ports; i++) {
+ priv->smbus_port_adapter[i].used = 0;
+ if (priv->settings.tp[i].mode == I3C_HUB_DT_TP_MODE_SMBUS) {
+ ret = i3c_hub_smbus_tp_algo(priv, i);
+ if (ret)
+ dev_warn(dev,
+ "Failed to setup SMBus adapter, port: %d\n",
+ i);
+ }
+ }
+
+ ret = i3c_hub_configure_hw(dev);
+ if (ret) {
+ dev_err(dev, "Failed to configure the HUB\n");
+ goto error;
+ }
+
+ /* Lock access to protected registers */
+ ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE,
+ REGISTERS_LOCK_CODE);
+ if (ret) {
+ dev_err(dev, "Failed to lock HUB's protected registers\n");
+ goto error;
+ }
+
+ /* IBI */
+ ibireq.handler = i3c_hub_ibi_handler;
+ ibireq.max_payload_len = IBI_MAX_PAYLOAD_LEN;
+ ibireq.num_slots = IBI_SLOT_NUMS;
+
+ ret = i3c_device_request_ibi(i3cdev, &ibireq);
+ if (ret) {
+ dev_err(dev, "Failed to requeset ibi!\n");
+ goto error;
+ }
+
+ ret = i3c_device_enable_ibi(i3cdev);
+ if (ret) {
+ dev_err(dev, "Failed to enable ibi!\n");
+ goto err_free_ibi;
+ }
+
+ /* TBD: Apply special/security lock here using DEV_CMD register */
+
+ if (priv->gpio.nums > 0) {
+ i3c_hub_setup_gpio(priv);
+
+ ret = devm_gpiochip_add_data(dev, &priv->gpio.chip, priv);
+ if (ret) {
+ dev_err(dev, "gpiochip add data fail!\n");
+ goto err_dis_ibi;
+ }
+ }
+
+ schedule_delayed_work(&priv->delayed_work, msecs_to_jiffies(100));
+
+ return 0;
+
+err_dis_ibi:
+ i3c_device_disable_ibi(i3cdev);
+err_free_ibi:
+ i3c_device_free_ibi(i3cdev);
+error:
+ debugfs_remove_recursive(priv->debug_dir);
+ return ret;
+}
+
+static void i3c_hub_remove(struct i3c_device *i3cdev)
+{
+ struct i3c_hub *priv = i3cdev_get_drvdata(i3cdev);
+ int i;
+
+ i3c_device_disable_ibi(i3cdev);
+ i3c_device_free_ibi(i3cdev);
+
+ for (i = 0; i < priv->dev_info->n_ports; i++) {
+ if (priv->smbus_port_adapter[i].used) {
+ cancel_delayed_work_sync(&priv->smbus_port_adapter[i].delayed_work_polling);
+ i2c_del_adapter(&priv->smbus_port_adapter[i].i2c);
+ }
+
+ if (priv->logical_bus[i].registered)
+ i3c_master_unregister(&priv->logical_bus[i].controller);
+ }
+
+ cancel_delayed_work_sync(&priv->delayed_work);
+ debugfs_remove_recursive(priv->debug_dir);
+}
+
+static struct i3c_driver i3c_hub = {
+ .driver.name = "rts490xa-i3c-hub",
+ .id_table = i3c_hub_ids,
+ .probe = i3c_hub_probe,
+ .remove = i3c_hub_remove,
+};
+
+module_i3c_driver(i3c_hub);
+
+MODULE_DESCRIPTION("RTS490XA I3C HUB driver");
+MODULE_LICENSE("GPL");
--
2.34.1
^ permalink raw reply related [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB
2026-05-25 12:51 [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB zain_zhou
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
@ 2026-05-25 13:02 ` sashiko-bot
2026-05-25 13:58 ` Krzysztof Kozlowski
2026-05-25 18:45 ` Rob Herring (Arm)
3 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-05-25 13:02 UTC (permalink / raw)
To: zain_zhou; +Cc: robh, conor+dt, devicetree
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] The binding supports configuring target ports as GPIOs but is missing the required standard GPIO provider properties (`gpio-controller`, `#gpio-cells`).
- [Medium] The commit message claims configuration properties that are missing from the schema or incorrectly described.
--
commit 7ef74db38a704236da2879cdad3537af86a4d897
Author: Yin Zhou <zain_zhou@realsil.com.cn>
dt-bindings: i3c: add Realtek RTS490x I3C HUB
This commit adds the device tree binding schema for the Realtek RTS490x series
I3C HUB devices. It describes configuration properties such as LDO voltage,
pull-up resistance, IO driver strength, and per-port operating modes.
> The binding describes configuration properties for:
> - LDO enable/disable and voltage level per port group
> - Pull-up resistance per port group
> - IO driver strength per port
> - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up,
> IO mode, SMBus clock frequency and polling interval
> - Hub network always-I3C mode
[Severity: Medium]
Does the commit message accurately reflect the properties added in the schema?
The schema defines IO driver strength per port group (e.g.,
realtek,tp0145-io-strength-ohms) rather than per individual port as claimed.
Additionally, properties for 'Hub network always-I3C mode' and 'IO mode'
appear to be missing from the schema definition completely.
> diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
> new file mode 100644
> index 0000000000000..851a433abcd3e
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
[ ... ]
> +patternProperties:
> + "^target-port@[0-9]+$":
> + type: object
> + description:
> + I3C HUB target port child node, named target-port@<id>.
> +
> + properties:
[ ... ]
> + realtek,mode:
> + enum: [disabled, i3c, smbus, gpio]
[Severity: Medium]
Should the schema define the standard GPIO provider properties here?
According to the dt-bindings subsystem guidelines, when a hardware variant
introduces GPIO provider capabilities (as enabled by the gpio mode here),
the schema must include the standard provider properties gpio-controller
and #gpio-cells in the definition and required lists, as well as in the
examples.
Without these, unevaluatedProperties: false will reject them if provided,
and dependent drivers or subsystems may fail at runtime if they are omitted.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260525125128.297-1-zain_zhou@realsil.com.cn?part=1
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
@ 2026-05-25 13:25 ` Alexandre Belloni
2026-05-25 13:28 ` sashiko-bot
` (2 subsequent siblings)
3 siblings, 0 replies; 9+ messages in thread
From: Alexandre Belloni @ 2026-05-25 13:25 UTC (permalink / raw)
To: zain_zhou
Cc: linux-staging, linux-i3c, devicetree, gregkh, Frank.Li, robh,
krzk+dt, conor+dt, linusw, brgl, linux-gpio, linux-kernel,
wei_wang
Hello,
On 25/05/2026 20:51:28+0800, zain_zhou@realsil.com.cn wrote:
> From: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Add driver for Realtek RTS490x series I3C HUB devices.
>
> The driver supports:
> - Device Tree based configuration of LDO, pull-up, IO strength
> and per-port mode (I3C/SMBus/GPIO/disabled)
> - Logical I3C bus registration per target port
> - SMBus agent functionality with IBI and polling modes
> - GPIO chip with IRQ support
> - DebugFS interface for register access and DT config inspection
> - IBI (In-Band Interrupt) handling
>
> The driver is placed in staging as it has known issues to be resolved
> before mainlining; see drivers/staging/rts490x/TODO for details.
>
> Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Changes in v2:
> - Update driver to match v2 DT binding: parse physical values
> directly via of_property_read_bool/u32; drop string enum lookup
> tables; update property names with realtek, prefix and unit suffixes
> - Fix maintainer name; move MAINTAINERS entry to driver patch
> - Code style: rename TPn_* macros to TPN_*; rename SMBus frequency
> constants to UPPER_CASE; add mutex field comments
> ---
> MAINTAINERS | 6 +
> drivers/staging/Kconfig | 2 +
> drivers/staging/Makefile | 1 +
> drivers/staging/rts490x/Kconfig | 16 +
> drivers/staging/rts490x/Makefile | 2 +
> drivers/staging/rts490x/TODO | 35 +
> drivers/staging/rts490x/rts490xa-i3c-hub.c | 3039 ++++++++++++++++++++
As stated in the previous review, this is never going to enter staging,
this has to be submitted to the i3c subsystem once all the TODOs have
been taken care of.
> 7 files changed, 3101 insertions(+)
> create mode 100644 drivers/staging/rts490x/Kconfig
> create mode 100644 drivers/staging/rts490x/Makefile
> create mode 100644 drivers/staging/rts490x/TODO
> create mode 100644 drivers/staging/rts490x/rts490xa-i3c-hub.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 2fb1c75afd16..6d0f8cde935d 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -12214,6 +12214,12 @@ S: Supported
> F: Documentation/devicetree/bindings/i3c/renesas,i3c.yaml
> F: drivers/i3c/master/renesas-i3c.c
>
> +I3C HUB DRIVER FOR REALTEK RTS490X
> +M: Yin Zhou <zain_zhou@realsil.com.cn>
> +S: Maintained
> +F: Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
> +F: drivers/staging/rts490x/
> +
> I3C DRIVER FOR SYNOPSYS DESIGNWARE
> S: Orphan
> F: Documentation/devicetree/bindings/i3c/snps,dw-i3c-master.yaml
> diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig
> index 2f92cd698bef..f14869cf5af5 100644
> --- a/drivers/staging/Kconfig
> +++ b/drivers/staging/Kconfig
> @@ -48,4 +48,6 @@ source "drivers/staging/axis-fifo/Kconfig"
>
> source "drivers/staging/vme_user/Kconfig"
>
> +source "drivers/staging/rts490x/Kconfig"
> +
> endif # STAGING
> diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile
> index f5b8876aa536..59044d547bf7 100644
> --- a/drivers/staging/Makefile
> +++ b/drivers/staging/Makefile
> @@ -13,3 +13,4 @@ obj-$(CONFIG_MOST) += most/
> obj-$(CONFIG_GREYBUS) += greybus/
> obj-$(CONFIG_BCM2835_VCHIQ) += vc04_services/
> obj-$(CONFIG_XIL_AXIS_FIFO) += axis-fifo/
> +obj-$(CONFIG_RTS490X_I3C_HUB) += rts490x/
> diff --git a/drivers/staging/rts490x/Kconfig b/drivers/staging/rts490x/Kconfig
> new file mode 100644
> index 000000000000..282865d1fed9
> --- /dev/null
> +++ b/drivers/staging/rts490x/Kconfig
> @@ -0,0 +1,16 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +config RTS490X_I3C_HUB
> + tristate "Realtek RTS490x I3C HUB driver"
> + depends on I3C
> + depends on REGMAP_I3C
> + select GPIOLIB
> + help
> + Support for Realtek RTS490x series I3C HUB devices (RTS4900,
> + RTS4901, RTS4902, RTS4903, RTS4904, RTS4906).
> +
> + The I3C HUB provides port expansion, voltage level translation,
> + bus capacitance isolation, address conflict isolation, SMBus
> + agent functionality and GPIO expansion.
> +
> + This driver can also be built as a module. If so, the module
> + will be called rts490xa-i3c-hub.
> diff --git a/drivers/staging/rts490x/Makefile b/drivers/staging/rts490x/Makefile
> new file mode 100644
> index 000000000000..4b1d7640fb82
> --- /dev/null
> +++ b/drivers/staging/rts490x/Makefile
> @@ -0,0 +1,2 @@
> +# SPDX-License-Identifier: GPL-2.0-only
> +obj-$(CONFIG_RTS490X_I3C_HUB) += rts490xa-i3c-hub.o
> diff --git a/drivers/staging/rts490x/TODO b/drivers/staging/rts490x/TODO
> new file mode 100644
> index 000000000000..0f46620af4c6
> --- /dev/null
> +++ b/drivers/staging/rts490x/TODO
> @@ -0,0 +1,35 @@
> +TODO list for rts490xa-i3c-hub staging driver
> +==============================================
> +
> +Completed in v2
> +---------------
> +- [x] Add proper DT binding schema validation (dt-schema)
> + Addressed in v2 dt-bindings patch: all properties carry vendor
> + prefix, use standard types (boolean/u32 with unit suffixes),
> + unevaluatedProperties: false, validated via dt-schema.
> +
> +Remaining before moving out of staging
> +---------------------------------------
> +- Clean up open-coded OF property parsing; use device_property_* APIs
> + instead of of_property_read_* where possible
> +- Remove use of full_name / sscanf for node name parsing; use
> + of_node_name_eq() and fwnode helpers instead
> +- Replace global mutex (i3c_hub_regmap_mutex) with per-device locking
> +- Add kernel-doc comments for all exported/public functions
> +- Resolve TODO comment in i3c_hub_hw_configure_tp() regarding MUX
> + connection verification
> +- Remove TBD comment in i3c_hub_probe() regarding DEV_CMD security lock
> +- Review and fix potential locking issues in i3c_hub_delayed_work()
> + when registering logical buses
> +- Fix error handling in i3c_hub_delayed_work(): early return on failure
> + does not unregister already-registered logical buses, causing resource
> + leak; needs proper cleanup on error path
> +
> +Rebase on upstream i3c hub framework (pending)
> +-----------------------------------------------
> +A generic i3c hub framework is being introduced upstream by the NXP
> +P3H2x4x patch series (v10, under review):
> + https://lore.kernel.org/linux-i3c/20260525064209.2263045-1-lakshay.piplani@nxp.com/
> +
> +Once that framework is merged, rebase this driver on it and move
> +out of staging.
> diff --git a/drivers/staging/rts490x/rts490xa-i3c-hub.c b/drivers/staging/rts490x/rts490xa-i3c-hub.c
> new file mode 100644
> index 000000000000..fdfff5c6dff5
> --- /dev/null
> +++ b/drivers/staging/rts490x/rts490xa-i3c-hub.c
> @@ -0,0 +1,3039 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (C) 2021 - 2023 Intel Corporation. */
> +/* Copyright (c) 2025 Realtek Semiconductor Corp. */
> +
> +#include <linux/bits.h>
> +#include <linux/kernel.h>
> +#include <linux/ktime.h>
> +#include <linux/bitfield.h>
> +#include <linux/debugfs.h>
> +#include <linux/module.h>
> +#include <linux/property.h>
> +#include <linux/regmap.h>
> +#include <linux/list.h>
> +#include <linux/gpio/driver.h>
> +
> +#include <linux/i3c/device.h>
> +#include <linux/i3c/master.h>
> +
> +#define I3C_HUB_TP_MAX_COUNT 0x08
> +
> +#define I3C_DCR_HUB 0xC2
> +
> +#define GPIO_BANK_SZ 0x02
> +#define GPIO_MAX_BANK I3C_HUB_TP_MAX_COUNT
> +
> +/* I3C HUB REGISTERS */
> +
> +/*
> + * In this driver Controller - Target convention is used. All the abbreviations are
> + * based on this convention. For instance: CP - Controller Port, TP - Target Port.
> + */
> +
> +/* Device Information Registers */
> +#define I3C_HUB_DEV_INFO_0 0x00
> +#define I3C_HUB_DEV_INFO_1 0x01
> +#define I3C_HUB_PID_5 0x02
> +#define I3C_HUB_PID_4 0x03
> +#define I3C_HUB_PID_3 0x04
> +#define I3C_HUB_PID_2 0x05
> +#define I3C_HUB_PID_1 0x06
> +#define I3C_HUB_PID_0 0x07
> +#define I3C_HUB_BCR 0x08
> +#define I3C_HUB_DCR 0x09
> +#define I3C_HUB_DEV_CAPAB 0x0A
> +
> +#define I3C_HUB_DEV_REV 0x0B
> +#define I3C_HUB_DEV_REV_LDO_MASK GENMASK(7, 6)
> +#define I3C_HUB_DEV_REV_LDO_GET(x) FIELD_GET(I3C_HUB_DEV_REV_LDO_MASK, (x))
> +
> +/* Device Configuration Registers */
> +#define I3C_HUB_PROTECTION_CODE 0x10
> +#define REGISTERS_LOCK_CODE 0x00
> +#define REGISTERS_UNLOCK_CODE 0x69
> +#define CP1_REGISTERS_UNLOCK_CODE 0x6A
> +
> +#define I3C_HUB_CP_CONF 0x11
> +#define I3C_HUB_TP_ENABLE 0x12
> +#define TPN_ENABLE(n) BIT(n)
> +
> +#define I3C_HUB_DEV_CONF 0x13
> +#define I3C_HUB_IO_STRENGTH 0x14
> +#define TP0145_IO_STRENGTH_MASK GENMASK(1, 0)
> +#define TP0145_IO_STRENGTH(x) (((x) << 0) & TP0145_IO_STRENGTH_MASK)
> +#define TP2367_IO_STRENGTH_MASK GENMASK(3, 2)
> +#define TP2367_IO_STRENGTH(x) (((x) << 2) & TP2367_IO_STRENGTH_MASK)
> +#define CP0_IO_STRENGTH_MASK GENMASK(5, 4)
> +#define CP0_IO_STRENGTH(x) (((x) << 4) & CP0_IO_STRENGTH_MASK)
> +#define CP1_IO_STRENGTH_MASK GENMASK(7, 6)
> +#define CP1_IO_STRENGTH(x) (((x) << 6) & CP1_IO_STRENGTH_MASK)
> +#define IO_STRENGTH_20_OHM 0x00
> +#define IO_STRENGTH_30_OHM 0x01
> +#define IO_STRENGTH_40_OHM 0x02
> +#define IO_STRENGTH_50_OHM 0x03
> +
> +#define I3C_HUB_NET_OPER_MODE_CONF 0x15
> +#define I3C_HUB_NET_ALWAYS_I3C_EN BIT(5)
> +
> +#define I3C_HUB_LDO_CONF 0x16
> +#define CP0_LDO_VOLTAGE_MASK GENMASK(1, 0)
> +#define CP0_LDO_VOLTAGE(x) (((x) << 0) & CP0_LDO_VOLTAGE_MASK)
> +#define CP1_LDO_VOLTAGE_MASK GENMASK(3, 2)
> +#define CP1_LDO_VOLTAGE(x) (((x) << 2) & CP1_LDO_VOLTAGE_MASK)
> +#define TP0145_LDO_VOLTAGE_MASK GENMASK(5, 4)
> +#define TP0145_LDO_VOLTAGE(x) (((x) << 4) & TP0145_LDO_VOLTAGE_MASK)
> +#define TP2367_LDO_VOLTAGE_MASK GENMASK(7, 6)
> +#define TP2367_LDO_VOLTAGE(x) (((x) << 6) & TP2367_LDO_VOLTAGE_MASK)
> +#define LDO_VOLTAGE_1_0V 0x00
> +#define LDO_VOLTAGE_1_1V 0x01
> +#define LDO_VOLTAGE_1_2V 0x02
> +#define LDO_VOLTAGE_1_8V 0x03
> +
> +#define I3C_HUB_TP_IO_MODE_CONF 0x17
> +#define TPN_IO_MODE_CON(n) BIT(n)
> +#define I3C_HUB_TP_SMBUS_AGNT_EN 0x18
> +#define TPN_SMBUS_MODE_EN(n) BIT(n)
> +
> +#define I3C_HUB_LDO_AND_PULLUP_CONF 0x19
> +#define LDO_ENABLE_DISABLE_MASK GENMASK(3, 0)
> +#define CP0_LDO_EN BIT(0)
> +#define CP1_LDO_EN BIT(1)
> +/*
> + * I3C HUB does not provide a way to control LDO or pull-up for individual ports. It is possible
> + * for group of ports TP0/TP1/TP4/TP5 and TP2/TP3/TP6/TP7.
> + */
> +#define TP0145_LDO_EN BIT(2)
> +#define TP2367_LDO_EN BIT(3)
> +#define TP0145_PULLUP_CONF_MASK GENMASK(7, 6)
> +#define TP0145_PULLUP_CONF(x) (((x) << 6) & TP0145_PULLUP_CONF_MASK)
> +#define TP2367_PULLUP_CONF_MASK GENMASK(5, 4)
> +#define TP2367_PULLUP_CONF(x) (((x) << 4) & TP2367_PULLUP_CONF_MASK)
> +#define PULLUP_250R 0x00
> +#define PULLUP_500R 0x01
> +#define PULLUP_1K 0x02
> +#define PULLUP_2K 0x03
> +
> +#define I3C_HUB_CP_IBI_CONF 0x1A
> +#define I3C_HUB_TP_IBI_CONF 0x1B
> +#define I3C_HUB_IBI_MDB_CUSTOM 0x1C
> +#define I3C_HUB_JEDEC_CONTEXT_ID 0x1D
> +#define I3C_HUB_TP_GPIO_MODE_EN 0x1E
> +#define TPN_GPIO_MODE_EN(n) BIT(n)
> +
> +/* Device Status and IBI Registers */
> +#define I3C_HUB_DEV_AND_IBI_STS 0x20
> +#define PEC_ERROR_FLAG BIT(0)
> +#define PARITY_ERROR_FLAG BIT(1)
> +#define CONTROLLER_MSG_PENDING_FLAG BIT(2)
> +#define TP_IO_FLAG_STATUS BIT(3)
> +#define SMBUS_AGENT_EVENT_FLAG_STATUS BIT(4)
> +
> +#define I3C_HUB_TP_SMBUS_AGNT_IBI_STS 0x21
> +
> +/* Controller Port Control/Status Registers */
> +#define I3C_HUB_CP_MUX_SET 0x38
> +#define CONTROLLER_PORT_MUX_REQ BIT(0)
> +#define I3C_HUB_CP_MUX_STS 0x39
> +#define CONTROLLER_PORT_MUX_CONNECTION_STATUS BIT(0)
> +
> +/* Target Dynamic Address Assignment Flag Registers */
> +#define I3C_HUB_TARGET_DA_FLAG_BYTE_BASE 0x40
> +#define I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT 16
> +
> +/* Target Ports Control Registers */
> +#define I3C_HUB_TP_SMBUS_AGNT_TRANS_START 0x50
> +#define I3C_HUB_TP_NET_CON_CONF 0x51
> +#define TPN_NET_CON(n) BIT(n)
> +
> +#define I3C_HUB_TP_PULLUP_EN 0x53
> +#define TPN_PULLUP_EN(n) BIT(n)
> +
> +#define I3C_HUB_TP_SCL_OUT_EN 0x54
> +#define I3C_HUB_TP_SDA_OUT_EN 0x55
> +#define I3C_HUB_TP_SCL_OUT_LEVEL 0x56
> +#define I3C_HUB_TP_SDA_OUT_LEVEL 0x57
> +
> +#define I3C_HUB_TP_IN_DETECT_MODE_CONF 0x58
> +#define SCL0145_IO_IN_DET_CFG_MASK GENMASK(1, 0)
> +#define SCL0145_IO_IN_DET_CFG(x) (((x) << 0) & SCL0145_IO_IN_DET_CFG_MASK)
> +#define SDA0145_IO_IN_DET_CFG_MASK GENMASK(3, 2)
> +#define SDA0145_IO_IN_DET_CFG(x) (((x) << 2) & SDA0145_IO_IN_DET_CFG_MASK)
> +#define SCL2367_IO_IN_DET_CFG_MASK GENMASK(5, 4)
> +#define SCL2367_IO_IN_DET_CFG(x) (((x) << 4) & SCL2367_IO_IN_DET_CFG_MASK)
> +#define SDA2367_IO_IN_DET_CFG_MASK GENMASK(7, 6)
> +#define SDA2367_IO_IN_DET_CFG(x) (((x) << 6) & SDA2367_IO_IN_DET_CFG_MASK)
> +
> +#define I3C_HUB_TP_SCL_IN_DETECT_IBI_EN 0x59
> +#define I3C_HUB_TP_SDA_IN_DETECT_IBI_EN 0x5A
> +
> +/* Target Ports Status Registers */
> +#define I3C_HUB_TP_SCL_IN_LEVEL_STS 0x60
> +#define I3C_HUB_TP_SDA_IN_LEVEL_STS 0x61
> +#define I3C_HUB_TP_SCL_IN_DETECT_FLG 0x62
> +#define I3C_HUB_TP_SDA_IN_DETECT_FLG 0x63
> +
> +/* SMBus Agent Configuration and Status Registers */
> +#define I3C_HUB_TP0_SMBUS_AGNT_STS 0x64
> +#define I3C_HUB_TP1_SMBUS_AGNT_STS 0x65
> +#define I3C_HUB_TP2_SMBUS_AGNT_STS 0x66
> +#define I3C_HUB_TP3_SMBUS_AGNT_STS 0x67
> +#define I3C_HUB_TP4_SMBUS_AGNT_STS 0x68
> +#define I3C_HUB_TP5_SMBUS_AGNT_STS 0x69
> +#define I3C_HUB_TP6_SMBUS_AGNT_STS 0x6A
> +#define I3C_HUB_TP7_SMBUS_AGNT_STS 0x6B
> +
> +#define I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF 0x6C
> +#define TARGET_AGENT_BUF_FULL_SDA_LOW_EN BIT(5)
> +
> +/* Transaction status checking mask */
> +#define I3C_HUB_CONTROLLER_AGENT_STATUS_MASK (0xF0 | BIT(0))
> +#define I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG BIT(0)
> +/* SMBus Controller Agent Return Codes */
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT 4
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS 0x0
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK 0x1
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY 0x2
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY 0x3
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED 0x4
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR 0x5
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT 0x6
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST 0x7
> +#define I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT 0x8
> +
> +#define I3C_HUB_TARGET_BUF_STATUS_MASK GENMASK(3, 1)
> +#define I3C_HUB_TARGET_BUF_0_RECEIVE BIT(1)
> +#define I3C_HUB_TARGET_BUF_1_RECEIVE BIT(2)
> +#define I3C_HUB_TARGET_BUF_OVRFL BIT(3)
> +
> +/* Special Function Registers */
> +#define I3C_HUB_LDO_AND_CPSEL_STS 0x79
> +#define CP_SDA1_LEVEL BIT(7)
> +#define CP_SCL1_LEVEL BIT(6)
> +#define CP_SEL_PIN_INPUT_CODE_MASK GENMASK(5, 4)
> +#define CP_SEL_PIN_INPUT_CODE_GET(x) (((x) & CP_SEL_PIN_INPUT_CODE_MASK) >> 4)
> +#define CP_SDA1_SCL1_PINS_CODE_MASK GENMASK(7, 6)
> +#define CP_SDA1_SCL1_PINS_CODE_GET(x) (((x) & CP_SDA1_SCL1_PINS_CODE_MASK) >> 6)
> +#define VCCIO1_PWR_GOOD BIT(3)
> +#define VCCIO0_PWR_GOOD BIT(2)
> +#define CP1_VCCIO_PWR_GOOD BIT(1)
> +#define CP0_VCCIO_PWR_GOOD BIT(0)
> +
> +#define I3C_HUB_BUS_RESET_SCL_TIMEOUT 0x7A
> +#define I3C_HUB_ONCHIP_TD_PROTO_ERR_FLG 0x7B
> +#define I3C_HUB_DEV_CMD 0x7C
> +#define I3C_HUB_ONCHIP_TD_STS 0x7D
> +#define I3C_HUB_ONCHIP_TD_ADDR_CONF 0x7E
> +#define I3C_HUB_PAGE_PTR 0x7F
> +
> +/* Paged Transaction Registers */
> +#define I3C_HUB_CONTROLLER_BUFFER_PAGE 0x10
> +#define I3C_HUB_CONTROLLER_AGENT_BUFF 0x80
> +#define I3C_HUB_CONTROLLER_AGENT_BUFF_DATA 0x84
> +#define I3C_HUB_TARGET_BUFF_LENGTH 0x80
> +#define I3C_HUB_TARGET_BUFF_ADDRESS 0x81
> +#define I3C_HUB_TARGET_BUFF_DATA 0x82
> +
> +/* TP DT setting */
> +#define I3C_HUB_DT_TP_MODE_DISABLED 0x00
> +#define I3C_HUB_DT_TP_MODE_I3C 0x01
> +#define I3C_HUB_DT_TP_MODE_SMBUS 0x02
> +#define I3C_HUB_DT_TP_MODE_GPIO 0x03
> +#define I3C_HUB_DT_TP_MODE_NOT_DEFINED 0xFF
> +
> +/* TP IO mode */
> +#define I3C_HUB_DT_TP_IO_MODE_OD_PP 0x00
> +#define I3C_HUB_DT_TP_IO_MODE_OD 0x01
> +#define I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED 0xFF
> +
> +/* SMBus transaction types fields */
> +#define I3C_HUB_SMBUS_100_KHZ 0x00
> +#define I3C_HUB_SMBUS_200_KHZ BIT(1)
> +#define I3C_HUB_SMBUS_400_KHZ BIT(2)
> +#define I3C_HUB_SMBUS_1000_KHZ (BIT(1) | BIT(2))
> +
> +/* SMBus xfer type for i3c_hub_smbus_msg xfer_type parameter */
> +#define I3C_HUB_SMBUS_XFER_WRITE 0
> +#define I3C_HUB_SMBUS_XFER_READ 1
> +#define I3C_HUB_SMBUS_XFER_WR_RD 2
> +
> +/* Hub buffer size */
> +#define I3C_HUB_CONTROLLER_BUFFER_SIZE 88
> +#define I3C_HUB_TARGET_BUFFER_SIZE 80
> +#define I3C_HUB_SMBUS_DESCRIPTOR_SIZE 4
> +#define I3C_HUB_SMBUS_PAYLOAD_SIZE \
> + (I3C_HUB_CONTROLLER_BUFFER_SIZE - I3C_HUB_SMBUS_DESCRIPTOR_SIZE)
> +#define I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE (I3C_HUB_TARGET_BUFFER_SIZE - 2)
> +
> +/* Hub SMBus status register read interval (microseconds, ceil) */
> +#define I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL(len, clk_khz) \
> + DIV_ROUND_UP(1000U * 9U * (u32)(len), (u32)(clk_khz))
> +
> +/* ID Extraction */
> +#define I3C_HUB_ID_CP_SDA_SCL 0x00
> +#define I3C_HUB_ID_CP_SEL 0x01
> +
> +/* IBI */
> +#define IBI_MAX_PAYLOAD_LEN 2
> +#define IBI_SLOT_NUMS 6
> +
> +#define I3C_HUB_IO_CTRL_PAGE 0x81
> +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK 0xDB
> +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN BIT(6)
> +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_MASK GENMASK(5, 0)
> +#define I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL 0x18
> +
> +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK 0xDC
> +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN BIT(4)
> +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK GENMASK(3, 0)
> +#define I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL(x) \
> + ((x) & I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_MASK)
> +
> +#define I3C_HUB_EFUSE_PAGE 0x7B
> +#define I3C_HUB_EFUSE_OFFSET_A0 0xA0
> +#define I3C_HUB_FAST_RSON_EN BIT(5)
> +#define I3C_HUB_EFUSE_OFFSET_A3 0xA3
> +#define I3C_HUB_FAST_DRV_LOOP_DIS BIT(5)
> +
> +#define I3C_HUB_EFUSE_OFFSET_9D 0x9D
> +#define I3C_HUB_TP_OD_VOL_LEVEL BIT(0)
> +#define I3C_HUB_TP_OD_VREF BIT(1)
> +
> +#define I3C_HUB_EFUSE_OFFSET_9E 0x9E
> +#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK GENMASK(5, 4)
> +#define I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL(x) \
> + (((x) << 4) & I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK)
> +#define I3C_HUB_IBI_ACK_RD_CYCLE_MASK GENMASK(3, 0)
> +#define I3C_HUB_IBI_ACK_RD_CYCLE_VAL (5)
> +
> +struct i3c_hub_dev_info {
> + const char *model;
> + u16 part_id;
> + u8 n_ports;
> +};
> +
> +static const struct i3c_hub_dev_info i3c_hub_dev_info_unknown = {
> + .model = "Unknown",
> + .part_id = 0,
> + .n_ports = 8,
> +};
> +
> +static const struct i3c_hub_dev_info i3c_hub_dev_info_table[] = {
> + { "RTS4900", 0x4000, 4 }, { "RTS4901", 0x4100, 4 },
> + { "RTS4902", 0x8000, 8 }, { "RTS4903", 0x8100, 8 },
> + { "RTS4904", 0x4001, 4 }, { "RTS4906", 0x8001, 8 }
> +};
> +
> +struct tp_setting {
> + u8 mode;
> + bool pullup_en;
> + u8 io_mode;
> + bool always_enable;
> + u32 poll_interval_ms;
> + u32 clock_frequency;
> +};
> +
> +struct dt_settings {
> + bool cp0_ldo_en;
> + bool cp1_ldo_en;
> + bool tp0145_ldo_en;
> + bool tp2367_ldo_en;
> + u32 cp0_ldo_volt;
> + u32 cp1_ldo_volt;
> + u32 tp0145_ldo_volt;
> + u32 tp2367_ldo_volt;
> + u32 tp0145_pullup;
> + u32 tp2367_pullup;
> + u32 cp0_io_strength;
> + u32 cp1_io_strength;
> + u32 tp0145_io_strength;
> + u32 tp2367_io_strength;
> + struct tp_setting tp[I3C_HUB_TP_MAX_COUNT];
> + bool hub_net_always_i3c;
> + u8 tp_scl_h_ack_cycles;
> + bool handshake_optimize;
> + u8 fast_drv_h_add_cycles;
> + bool fast_rson_en;
> + bool tp_od_vol_optimize;
> + bool tp_od_vref_optimize;
> +};
> +
> +struct smbus_backend {
> + struct i2c_client *client;
> + struct list_head list;
> +};
> +
> +struct i2c_adapter_group {
> + struct i2c_adapter i2c;
> + u8 tp_mask;
> + u8 tp_port;
> + u8 used;
> + struct device_node *of_node;
> + struct i3c_hub *priv;
> + struct mutex mutex; /* protects SMBus adapter state and transfers */
> +
> + struct delayed_work delayed_work_polling;
> + struct list_head backend_entry;
> + u8 last_processed_buf;
> +
> + u8 status;
> + struct completion completion;
> +};
> +
> +struct logical_bus {
> + bool available; /* Logical bus configuration is available in DT. */
> + bool registered; /* Logical bus was registered in the framework. */
> + u8 tp_id;
> + u8 tp_map;
> + struct i3c_master_controller controller;
> + struct device_node *of_node;
> + struct i3c_hub *priv;
> +};
> +
> +struct hub_gpio {
> + struct gpio_chip chip;
> + int tp[GPIO_MAX_BANK];
> + s8 port_to_index[I3C_HUB_TP_MAX_COUNT];
> + int nums;
> + struct irq_chip irq_chip;
> + struct mutex irq_mutex; /* protects GPIO IRQ enable/disable operations */
> +};
> +
> +struct i3c_hub {
> + struct i3c_device *i3cdev;
> + struct i3c_master_controller *driving_master;
> + struct regmap *regmap;
> + const struct i3c_hub_dev_info *dev_info;
> + struct dt_settings settings;
> + struct delayed_work delayed_work;
> + int hub_pin_sel_id;
> + int hub_pin_cp1_id;
> + int hub_dt_sel_id;
> + int hub_dt_cp1_id;
> +
> + struct logical_bus logical_bus[I3C_HUB_TP_MAX_COUNT];
> + struct i2c_adapter_group smbus_port_adapter[I3C_HUB_TP_MAX_COUNT];
> + u8 smbus_ibi_en_mask;
> + struct mutex page_mutex; /* protects regmap page register access */
> +
> + /* Offset for reading HUB's register. */
> + u8 reg_addr;
> + struct dentry *debug_dir;
> + struct hub_gpio gpio;
> +};
> +
> +/* Global mutex for serializing regmap access across all i3c hubs. */
> +static DEFINE_MUTEX(i3c_hub_regmap_mutex);
> +
> +static u8 i3c_hub_ldo_dt_to_reg(u32 microvolt)
> +{
> + switch (microvolt) {
> + case 1100000:
> + return LDO_VOLTAGE_1_1V;
> + case 1200000:
> + return LDO_VOLTAGE_1_2V;
> + case 1800000:
> + return LDO_VOLTAGE_1_8V;
> + default:
> + return LDO_VOLTAGE_1_0V;
> + }
> +}
> +
> +static u8 i3c_hub_pullup_dt_to_reg(u32 ohms)
> +{
> + switch (ohms) {
> + case 250:
> + return PULLUP_250R;
> + case 500:
> + return PULLUP_500R;
> + case 1000:
> + return PULLUP_1K;
> + default:
> + return PULLUP_2K;
> + }
> +}
> +
> +static u8 i3c_hub_io_strength_dt_to_reg(u32 ohms)
> +{
> + switch (ohms) {
> + case 50:
> + return IO_STRENGTH_50_OHM;
> + case 40:
> + return IO_STRENGTH_40_OHM;
> + case 30:
> + return IO_STRENGTH_30_OHM;
> + default:
> + return IO_STRENGTH_20_OHM;
> + }
> +}
> +
> +static bool i3c_hub_smbus_validate_clock_frequency(u32 hz)
> +{
> + switch (hz) {
> + case 100000:
> + case 200000:
> + case 400000:
> + case 1000000:
> + return true;
> + default:
> + return false;
> + }
> +}
> +
> +static inline u8 i3c_hub_smbus_rate_bits_from_hz(u32 hz)
> +{
> + switch (hz) {
> + case 100000:
> + return I3C_HUB_SMBUS_100_KHZ;
> + case 200000:
> + return I3C_HUB_SMBUS_200_KHZ;
> + case 1000000:
> + return I3C_HUB_SMBUS_1000_KHZ;
> + default:
> + return I3C_HUB_SMBUS_400_KHZ;
> + }
> +}
> +
> +static void i3c_hub_tp_of_get_setting(struct device *dev,
> + const struct device_node *node,
> + struct tp_setting tp_setting[])
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + const char *mode_str, *io_mode_str;
> + struct device_node *tp_node;
> + u32 id, val;
> +
> + for_each_available_child_of_node(node, tp_node) {
> + if (!tp_node->name || of_node_cmp(tp_node->name, "target-port"))
> + continue;
> +
> + if (!tp_node->full_name ||
> + (sscanf(tp_node->full_name, "target-port@%u", &id) != 1)) {
> + dev_warn(dev,
> + "Invalid target port node found in DT: %s\n",
> + tp_node->full_name);
> + continue;
> + }
> +
> + if (id >= priv->dev_info->n_ports) {
> + dev_warn(dev,
> + "Invalid target port index found in DT: %i\n",
> + id);
> + continue;
> + }
> +
> + priv->smbus_port_adapter[id].of_node = tp_node;
> +
> + if (!of_property_read_string(tp_node, "realtek,mode",
> + &mode_str)) {
> + if (!strcmp(mode_str, "i3c"))
> + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_I3C;
> + else if (!strcmp(mode_str, "smbus"))
> + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_SMBUS;
> + else if (!strcmp(mode_str, "gpio"))
> + tp_setting[id].mode = I3C_HUB_DT_TP_MODE_GPIO;
> + else if (!strcmp(mode_str, "disabled"))
> + tp_setting[id].mode =
> + I3C_HUB_DT_TP_MODE_DISABLED;
> + }
> +
> + if (!of_property_read_string(tp_node, "realtek,io-mode",
> + &io_mode_str)) {
> + if (!strcmp(io_mode_str, "od"))
> + tp_setting[id].io_mode =
> + I3C_HUB_DT_TP_IO_MODE_OD;
> + else if (!strcmp(io_mode_str, "od-pp"))
> + tp_setting[id].io_mode =
> + I3C_HUB_DT_TP_IO_MODE_OD_PP;
> + }
> +
> + tp_setting[id].pullup_en =
> + of_property_read_bool(tp_node, "realtek,pullup-enable");
> + tp_setting[id].always_enable =
> + of_property_read_bool(tp_node, "realtek,always-enable");
> +
> + if (!of_property_read_u32(tp_node,
> + "realtek,polling-interval-ms", &val))
> + tp_setting[id].poll_interval_ms = val;
> +
> + if (!of_property_read_u32(tp_node, "clock-frequency", &val)) {
> + if (i3c_hub_smbus_validate_clock_frequency(val))
> + tp_setting[id].clock_frequency = val;
> + else
> + dev_warn(dev,
> + "Unsupported TP%d smbus clock-frequency: %u Hz, using default %u Hz\n",
> + id, val,
> + tp_setting[id].clock_frequency);
> + }
> + }
> +}
> +
> +static void i3c_hub_of_get_conf_static(struct device *dev,
> + const struct device_node *node)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + u8 u8val;
> + u32 val;
> +
> + priv->settings.cp0_ldo_en =
> + of_property_read_bool(node, "realtek,cp0-ldo-enable");
> + priv->settings.cp1_ldo_en =
> + of_property_read_bool(node, "realtek,cp1-ldo-enable");
> + priv->settings.tp0145_ldo_en =
> + of_property_read_bool(node, "realtek,tp0145-ldo-enable");
> + priv->settings.tp2367_ldo_en =
> + of_property_read_bool(node, "realtek,tp2367-ldo-enable");
> +
> + if (!of_property_read_u32(node, "realtek,cp0-ldo-microvolt", &val))
> + priv->settings.cp0_ldo_volt = val;
> + if (!of_property_read_u32(node, "realtek,cp1-ldo-microvolt", &val))
> + priv->settings.cp1_ldo_volt = val;
> + if (!of_property_read_u32(node, "realtek,tp0145-ldo-microvolt", &val))
> + priv->settings.tp0145_ldo_volt = val;
> + if (!of_property_read_u32(node, "realtek,tp2367-ldo-microvolt", &val))
> + priv->settings.tp2367_ldo_volt = val;
> +
> + if (!of_property_read_u32(node, "realtek,tp0145-pullup-ohms", &val))
> + priv->settings.tp0145_pullup = val;
> + if (!of_property_read_u32(node, "realtek,tp2367-pullup-ohms", &val))
> + priv->settings.tp2367_pullup = val;
> +
> + if (!of_property_read_u32(node,
> + "realtek,cp0-output-impedance-ohms", &val))
> + priv->settings.cp0_io_strength = val;
> + if (!of_property_read_u32(node,
> + "realtek,cp1-output-impedance-ohms", &val))
> + priv->settings.cp1_io_strength = val;
> + if (!of_property_read_u32(node,
> + "realtek,tp0145-output-impedance-ohms", &val))
> + priv->settings.tp0145_io_strength = val;
> + if (!of_property_read_u32(node,
> + "realtek,tp2367-output-impedance-ohms", &val))
> + priv->settings.tp2367_io_strength = val;
> +
> + priv->settings.hub_net_always_i3c =
> + of_property_read_bool(node, "realtek,hub-net-always-i3c");
> +
> + if (!of_property_read_u8(node, "realtek,tp-scl-h-ack-cycles", &u8val))
> + priv->settings.tp_scl_h_ack_cycles = u8val;
> +
> + i3c_hub_tp_of_get_setting(dev, node, priv->settings.tp);
> +
> + priv->settings.handshake_optimize =
> + of_property_read_bool(node, "realtek,handshake-optimize");
> +
> + if (!of_property_read_u8(node, "realtek,fast-drv-h-add-cycles",
> + &u8val))
> + priv->settings.fast_drv_h_add_cycles = u8val;
> +
> + priv->settings.fast_rson_en =
> + of_property_read_bool(node, "realtek,fast-rson-en");
> +
> + priv->settings.tp_od_vol_optimize =
> + of_property_read_bool(node, "realtek,tp-od-vol-optimize");
> +
> + priv->settings.tp_od_vref_optimize =
> + of_property_read_bool(node, "realtek,tp-od-vref-optimize");
> +}
> +
> +static const struct i3c_hub_dev_info *
> +i3c_hub_lookup_dev_info(struct i3c_hub *priv)
> +{
> + int i, ret;
> + u16 part_id = 0;
> + u32 val = 0;
> +
> + ret = regmap_read(priv->regmap, I3C_HUB_DEV_INFO_0, &val);
> + if (ret)
> + return ERR_PTR(ret);
> +
> + part_id = (val & 0xFF) << 8;
> +
> + ret = regmap_read(priv->regmap, I3C_HUB_DEV_REV, &val);
> + if (ret)
> + return ERR_PTR(ret);
> +
> + part_id |= I3C_HUB_DEV_REV_LDO_GET(val);
> +
> + for (i = 0; i < ARRAY_SIZE(i3c_hub_dev_info_table); i++) {
> + if (i3c_hub_dev_info_table[i].part_id == part_id)
> + return &i3c_hub_dev_info_table[i];
> + }
> + return &i3c_hub_dev_info_unknown;
> +}
> +
> +static void i3c_hub_of_default_configuration(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + int id;
> +
> + priv->settings.cp0_ldo_en = false;
> + priv->settings.cp1_ldo_en = false;
> + priv->settings.tp0145_ldo_en = false;
> + priv->settings.tp2367_ldo_en = false;
> + priv->settings.cp0_ldo_volt = 0;
> + priv->settings.cp1_ldo_volt = 0;
> + priv->settings.tp0145_ldo_volt = 0;
> + priv->settings.tp2367_ldo_volt = 0;
> + priv->settings.tp0145_pullup = UINT_MAX;
> + priv->settings.tp2367_pullup = UINT_MAX;
> + priv->settings.cp0_io_strength = 0;
> + priv->settings.cp1_io_strength = 0;
> + priv->settings.tp0145_io_strength = 0;
> + priv->settings.tp2367_io_strength = 0;
> + priv->settings.hub_net_always_i3c = false;
> + priv->settings.tp_scl_h_ack_cycles = 0;
> + priv->settings.handshake_optimize = false;
> + priv->settings.fast_drv_h_add_cycles = 3;
> + priv->settings.fast_rson_en = false;
> + priv->settings.tp_od_vol_optimize = false;
> + priv->settings.tp_od_vref_optimize = false;
> +
> + for (id = 0; id < I3C_HUB_TP_MAX_COUNT; ++id) {
> + priv->settings.tp[id].mode = I3C_HUB_DT_TP_MODE_NOT_DEFINED;
> + priv->settings.tp[id].pullup_en = false;
> + priv->settings.tp[id].io_mode =
> + I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED;
> + priv->settings.tp[id].poll_interval_ms = 0;
> + priv->settings.tp[id].clock_frequency = 400000;
> + }
> +}
> +
> +static int i3c_hub_hw_configure_pullup(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + u8 mask = 0, value = 0;
> +
> + if (priv->settings.tp0145_pullup != UINT_MAX) {
> + mask |= TP0145_PULLUP_CONF_MASK;
> + if (priv->settings.tp0145_pullup != 0)
> + value |= TP0145_PULLUP_CONF(
> + i3c_hub_pullup_dt_to_reg(priv->settings.tp0145_pullup));
> + }
> +
> + if (priv->settings.tp2367_pullup != UINT_MAX) {
> + mask |= TP2367_PULLUP_CONF_MASK;
> + if (priv->settings.tp2367_pullup != 0)
> + value |= TP2367_PULLUP_CONF(
> + i3c_hub_pullup_dt_to_reg(priv->settings.tp2367_pullup));
> + }
> +
> + return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF,
> + mask, value);
> +}
> +
> +static int i3c_hub_hw_configure_ldo(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + u8 ldo_config_mask = 0, ldo_config_val = 0;
> + u8 ldo_disable_mask = 0, ldo_en_val = 0;
> + u32 reg_val;
> + int ret;
> + u8 val;
> +
> + /* Enable or Disable LDO's. If there is no DT entry - disable LDO for safety reasons */
> + if (priv->settings.cp0_ldo_en)
> + ldo_en_val |= CP0_LDO_EN;
> + if (priv->settings.cp1_ldo_en)
> + ldo_en_val |= CP1_LDO_EN;
> + if (priv->settings.tp0145_ldo_en)
> + ldo_en_val |= TP0145_LDO_EN;
> + if (priv->settings.tp2367_ldo_en)
> + ldo_en_val |= TP2367_LDO_EN;
> +
> + /* Get current LDOs configuration */
> + ret = regmap_read(priv->regmap, I3C_HUB_LDO_CONF, ®_val);
> + if (ret)
> + return ret;
> +
> + /* LDOs Voltage level (Skip if not defined in the DT)
> + * Set the mask only if there is a change from current value
> + */
> + if (priv->settings.cp0_ldo_volt != 0) {
> + val = CP0_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp0_ldo_volt));
> + if ((reg_val & CP0_LDO_VOLTAGE_MASK) != val) {
> + ldo_config_mask |= CP0_LDO_VOLTAGE_MASK;
> + ldo_disable_mask |= CP0_LDO_EN;
> + ldo_config_val |= val;
> + }
> + }
> + if (priv->settings.cp1_ldo_volt != 0) {
> + val = CP1_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.cp1_ldo_volt));
> + if ((reg_val & CP1_LDO_VOLTAGE_MASK) != val) {
> + ldo_config_mask |= CP1_LDO_VOLTAGE_MASK;
> + ldo_disable_mask |= CP1_LDO_EN;
> + ldo_config_val |= val;
> + }
> + }
> + if (priv->settings.tp0145_ldo_volt != 0) {
> + val = TP0145_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp0145_ldo_volt));
> + if ((reg_val & TP0145_LDO_VOLTAGE_MASK) != val) {
> + ldo_config_mask |= TP0145_LDO_VOLTAGE_MASK;
> + ldo_disable_mask |= TP0145_LDO_EN;
> + ldo_config_val |= val;
> + }
> + }
> + if (priv->settings.tp2367_ldo_volt != 0) {
> + val = TP2367_LDO_VOLTAGE(i3c_hub_ldo_dt_to_reg(priv->settings.tp2367_ldo_volt));
> + if ((reg_val & TP2367_LDO_VOLTAGE_MASK) != val) {
> + ldo_config_mask |= TP2367_LDO_VOLTAGE_MASK;
> + ldo_disable_mask |= TP2367_LDO_EN;
> + ldo_config_val |= val;
> + }
> + }
> +
> + /*
> + * Update LDO voltage configuration only if value is changed from already existing register
> + * value. It is a good practice to disable the LDO's before making any voltage changes.
> + * Presence of config mask indicates voltage change to be applied.
> + */
> + if (ldo_config_mask) {
> + /* Disable LDO's before making voltage changes */
> + ret = regmap_update_bits(priv->regmap,
> + I3C_HUB_LDO_AND_PULLUP_CONF,
> + ldo_disable_mask, 0);
> + if (ret)
> + return ret;
> +
> + /* Update the LDOs configuration */
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_LDO_CONF,
> + ldo_config_mask, ldo_config_val);
> + if (ret)
> + return ret;
> + }
> +
> + /* Update the LDOs Enable/disable register. This will enable only LDOs enabled in DT */
> + return regmap_update_bits(priv->regmap, I3C_HUB_LDO_AND_PULLUP_CONF,
> + LDO_ENABLE_DISABLE_MASK, ldo_en_val);
> +}
> +
> +static int i3c_hub_hw_configure_io_strength(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + u8 mask_all = 0, val_all = 0;
> + u32 reg_val;
> + u8 val;
> + struct dt_settings tmp;
> + int ret;
> +
> + /* Get IO strength configuration to figure out what needs to be changed */
> + ret = regmap_read(priv->regmap, I3C_HUB_IO_STRENGTH, ®_val);
> + if (ret)
> + return ret;
> +
> + tmp = priv->settings;
> + if (tmp.cp0_io_strength != 0) {
> + val = CP0_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp0_io_strength));
> + mask_all |= CP0_IO_STRENGTH_MASK;
> + val_all |= val;
> + }
> + if (tmp.cp1_io_strength != 0) {
> + val = CP1_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.cp1_io_strength));
> + mask_all |= CP1_IO_STRENGTH_MASK;
> + val_all |= val;
> + }
> + if (tmp.tp0145_io_strength != 0) {
> + val = TP0145_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp0145_io_strength));
> + mask_all |= TP0145_IO_STRENGTH_MASK;
> + val_all |= val;
> + }
> + if (tmp.tp2367_io_strength != 0) {
> + val = TP2367_IO_STRENGTH(i3c_hub_io_strength_dt_to_reg(tmp.tp2367_io_strength));
> + mask_all |= TP2367_IO_STRENGTH_MASK;
> + val_all |= val;
> + }
> +
> + /* Set IO strength if required */
> + return regmap_update_bits(priv->regmap, I3C_HUB_IO_STRENGTH, mask_all,
> + val_all);
> +}
> +
> +static int i3c_hub_hw_configure_tp(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + u8 pullup_mask = 0, pullup_val = 0;
> + u8 smbus_mask = 0, smbus_val = 0;
> + u8 gpio_mask = 0, gpio_val = 0;
> + u8 i3c_mask = 0, i3c_val = 0;
> + u8 io_mode_mask = 0, io_mode_val = 0;
> + int ret;
> + int i, index;
> +
> + memset(priv->gpio.port_to_index, -1, sizeof(priv->gpio.port_to_index));
> +
> + for (i = 0; i < priv->dev_info->n_ports; ++i) {
> + if (priv->settings.tp[i].mode !=
> + I3C_HUB_DT_TP_MODE_NOT_DEFINED) {
> + i3c_mask |= TPN_NET_CON(i);
> + smbus_mask |= TPN_SMBUS_MODE_EN(i);
> + gpio_mask |= TPN_GPIO_MODE_EN(i);
> + io_mode_mask |= TPN_IO_MODE_CON(i);
> +
> + if (priv->settings.tp[i].mode ==
> + I3C_HUB_DT_TP_MODE_I3C) {
> + i3c_val |= TPN_NET_CON(i);
> + } else if (priv->settings.tp[i].mode ==
> + I3C_HUB_DT_TP_MODE_SMBUS) {
> + smbus_val |= TPN_SMBUS_MODE_EN(i);
> + } else if (priv->settings.tp[i].mode ==
> + I3C_HUB_DT_TP_MODE_GPIO) {
> + gpio_val |= TPN_GPIO_MODE_EN(i);
> + priv->gpio.nums += GPIO_BANK_SZ;
> + index = priv->gpio.nums / GPIO_BANK_SZ - 1;
> + priv->gpio.tp[index] = i;
> + priv->gpio.port_to_index[i] = index;
> + }
> + }
> + pullup_mask |= TPN_PULLUP_EN(i);
> + if (priv->settings.tp[i].pullup_en)
> + pullup_val |= TPN_PULLUP_EN(i);
> + if (priv->settings.tp[i].io_mode !=
> + I3C_HUB_DT_TP_IO_MODE_NOT_DEFINED) {
> + if (priv->settings.tp[i].io_mode ==
> + I3C_HUB_DT_TP_IO_MODE_OD)
> + io_mode_val |= TPN_IO_MODE_CON(i);
> + } else if (priv->settings.tp[i].mode ==
> + I3C_HUB_DT_TP_MODE_SMBUS) {
> + io_mode_val |= TPN_IO_MODE_CON(i);
> + }
> + }
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_IO_MODE_CONF,
> + io_mode_mask, io_mode_val);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_PULLUP_EN,
> + pullup_mask, pullup_val);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_SMBUS_AGNT_EN,
> + smbus_mask, smbus_val);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_GPIO_MODE_EN,
> + gpio_mask, gpio_val);
> + if (ret)
> + return ret;
> +
> + /* Request for HUB Network connection in case any TP is configured in I3C mode */
> + if (i3c_val) {
> + ret = regmap_write(priv->regmap, I3C_HUB_CP_MUX_SET,
> + CONTROLLER_PORT_MUX_REQ);
> + if (ret)
> + return ret;
> + /* TODO: verify if connection is done */
> + }
> +
> + /* Enable TP here in case TP was configured */
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_ENABLE,
> + i3c_mask | smbus_mask | gpio_mask,
> + i3c_val | smbus_val | gpio_val);
> + if (ret)
> + return ret;
> +
> + return regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, i3c_val);
> +}
> +
> +static int i3c_hub_hw_configure_misc(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + int ret;
> + u8 reg = I3C_HUB_TARGET_DA_FLAG_BYTE_BASE;
> + u8 val[I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT];
> +
> + if (!priv->settings.hub_net_always_i3c)
> + return 0;
> +
> + memset(val, 0xff, I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT);
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_NET_OPER_MODE_CONF,
> + I3C_HUB_NET_ALWAYS_I3C_EN,
> + I3C_HUB_NET_ALWAYS_I3C_EN);
> + if (ret)
> + return ret;
> +
> + ret = regmap_bulk_write(priv->regmap, reg, val,
> + I3C_HUB_TARGET_DA_FLAG_BYTE_COUNT);
> + return ret;
> +}
> +
> +typedef int (*i3c_hub_cfg_fn)(struct i3c_hub *priv);
> +
> +static int i3c_hub_hw_cfg_with_page(struct i3c_hub *priv, u8 page,
> + i3c_hub_cfg_fn op)
> +{
> + int ret;
> +
> + if (!op)
> + return -EINVAL;
> +
> + mutex_lock(&priv->page_mutex);
> + ret = regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, page);
> + if (ret)
> + goto unlock;
> +
> + ret = op(priv);
> +unlock:
> + regmap_write(priv->regmap, I3C_HUB_PAGE_PTR, 0x00);
> + mutex_unlock(&priv->page_mutex);
> + return ret;
> +}
> +
> +static int i3c_hub_cfg_op_fuse_latch(struct i3c_hub *priv)
> +{
> + int ret;
> +
> + if (priv->settings.handshake_optimize) {
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E,
> + I3C_HUB_IBI_ACK_RD_CYCLE_MASK,
> + I3C_HUB_IBI_ACK_RD_CYCLE_VAL);
> + if (ret)
> + return ret;
> + }
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A3,
> + I3C_HUB_FAST_DRV_LOOP_DIS,
> + I3C_HUB_FAST_DRV_LOOP_DIS);
> + if (ret)
> + return ret;
> +
> + if (priv->settings.tp_od_vol_optimize) {
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D,
> + I3C_HUB_TP_OD_VOL_LEVEL,
> + I3C_HUB_TP_OD_VOL_LEVEL);
> + if (ret)
> + return ret;
> + }
> +
> + if (priv->settings.tp_od_vref_optimize) {
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9D,
> + I3C_HUB_TP_OD_VREF,
> + I3C_HUB_TP_OD_VREF);
> + if (ret)
> + return ret;
> + }
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_9E,
> + I3C_HUB_FAST_DRV_H_ADD_CYCLE_MASK,
> + I3C_HUB_FAST_DRV_H_ADD_CYCLE_VAL(
> + priv->settings.fast_drv_h_add_cycles));
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_EFUSE_OFFSET_A0,
> + I3C_HUB_FAST_RSON_EN,
> + priv->settings.fast_rson_en ? I3C_HUB_FAST_RSON_EN : 0);
> + return ret;
> +}
> +
> +static int i3c_hub_hw_configure_fuse_latch(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> +
> + return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_EFUSE_PAGE,
> + i3c_hub_cfg_op_fuse_latch);
> +}
> +
> +static int i3c_hub_cfg_op_io(struct i3c_hub *priv)
> +{
> + int ret;
> + u8 reg = I3C_HUB_CFG_TP_SCL_L_ACK_CLK;
> +
> + /* cfg tp scl low ack clk */
> + ret = regmap_write(priv->regmap, reg,
> + I3C_HUB_CFG_TP_SCL_L_ACK_CLK_EN |
> + I3C_HUB_CFG_TP_SCL_L_ACK_CLK_COUNT_VAL);
> + if (ret)
> + return ret;
> +
> + /* cfg tp scl high ack clk */
> + reg = I3C_HUB_CFG_TP_SCL_H_ACK_CLK;
> + if (priv->settings.tp_scl_h_ack_cycles == 0)
> + return 0;
> +
> + ret = regmap_write(priv->regmap, reg,
> + I3C_HUB_CFG_TP_SCL_H_ACK_CLK_EN |
> + I3C_HUB_CFG_TP_SCL_H_ACK_CLK_COUNT_VAL(
> + priv->settings.tp_scl_h_ack_cycles));
> +
> + return ret;
> +}
> +
> +static int i3c_hub_hw_configure_io(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> +
> + return i3c_hub_hw_cfg_with_page(priv, I3C_HUB_IO_CTRL_PAGE,
> + i3c_hub_cfg_op_io);
> +}
> +
> +static int i3c_hub_configure_hw(struct device *dev)
> +{
> + int ret;
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> +
> + ret = i3c_hub_hw_configure_ldo(dev);
> + if (ret)
> + return ret;
> +
> + ret = i3c_hub_hw_configure_io_strength(dev);
> + if (ret)
> + return ret;
> +
> + ret = i3c_hub_hw_configure_pullup(dev);
> + if (ret)
> + return ret;
> +
> + if (priv->dev_info->part_id) {
> + ret = i3c_hub_hw_configure_misc(dev);
> + if (ret)
> + return ret;
> +
> + ret = i3c_hub_hw_configure_fuse_latch(dev);
> + if (ret)
> + return ret;
> +
> + ret = i3c_hub_hw_configure_io(dev);
> + if (ret)
> + return ret;
> + }
> +
> + ret = i3c_hub_hw_configure_tp(dev);
> + return ret;
> +}
> +
> +static void i3c_hub_of_get_conf_runtime(struct device *dev,
> + const struct device_node *node)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + struct device_node *i3c_node;
> + int i3c_id;
> + u8 tp_mask;
> +
> + for_each_available_child_of_node(node, i3c_node) {
> + if (!i3c_node->full_name ||
> + (sscanf(i3c_node->full_name, "i3c%i@%hhx", &i3c_id,
> + &tp_mask) != 2))
> + continue;
> +
> + if (i3c_id < priv->dev_info->n_ports) {
> + priv->logical_bus[i3c_id].available = true;
> + priv->logical_bus[i3c_id].of_node = i3c_node;
> + priv->logical_bus[i3c_id].tp_map = tp_mask;
> + priv->logical_bus[i3c_id].priv = priv;
> + priv->logical_bus[i3c_id].tp_id = i3c_id;
> + }
> + }
> +}
> +
> +static const struct i3c_device_id i3c_hub_ids[] = {
> + I3C_CLASS(I3C_DCR_HUB, NULL),
> + {},
> +};
> +
> +static int i3c_hub_read_id(struct device *dev)
> +{
> + struct i3c_hub *priv = dev_get_drvdata(dev);
> + u32 reg_val;
> + int ret;
> +
> + ret = regmap_read(priv->regmap, I3C_HUB_LDO_AND_CPSEL_STS, ®_val);
> + if (ret) {
> + dev_err(dev, "Failed to read status register\n");
> + return -1;
> + }
> +
> + priv->hub_pin_sel_id = CP_SEL_PIN_INPUT_CODE_GET(reg_val);
> + priv->hub_pin_cp1_id = CP_SDA1_SCL1_PINS_CODE_GET(reg_val);
> + return 0;
> +}
> +
> +static struct device_node *i3c_hub_get_dt_hub_node(struct device_node *node,
> + struct i3c_hub *priv)
> +{
> + struct device_node *hub_node_no_id = NULL;
> + struct device_node *hub_node;
> + u32 hub_id;
> + u32 id_mask;
> + u32 dt_id;
> + u32 pin_id;
> + int found_id = 0;
> +
> + for_each_available_child_of_node(node, hub_node) {
> + id_mask = 0;
> + priv->hub_dt_sel_id = -1;
> + priv->hub_dt_cp1_id = -1;
> +
> + if (strstr(hub_node->name, "hub")) {
> + if (!of_property_read_u32(hub_node, "realtek,id",
> + &hub_id)) {
> + id_mask |= 0x0f;
> + priv->hub_dt_sel_id = hub_id;
> + }
> +
> + if (!of_property_read_u32(hub_node, "realtek,id-cp1",
> + &hub_id)) {
> + id_mask |= 0xf0;
> + priv->hub_dt_cp1_id = hub_id;
> + }
> +
> + dt_id = ((u32)priv->hub_dt_cp1_id & 0x0f) << 4 |
> + ((u32)priv->hub_dt_sel_id & 0x0f);
> + pin_id = ((u32)priv->hub_pin_cp1_id & 0x0f) << 4 |
> + ((u32)priv->hub_pin_sel_id & 0x0f);
> +
> + if (id_mask != 0 &&
> + (dt_id & id_mask) == (pin_id & id_mask))
> + found_id = 1;
> +
> + if (!found_id) {
> + /*
> + * Just keep reference to first HUB node with no ID in case no ID
> + * matching
> + */
> + if (!hub_node_no_id &&
> + priv->hub_dt_sel_id == -1 &&
> + priv->hub_dt_cp1_id == -1)
> + hub_node_no_id = hub_node;
> + } else {
> + return hub_node;
> + }
> + }
> + }
> +
> + return hub_node_no_id;
> +}
> +
> +static int fops_access_reg_get(void *ctx, u64 *val)
> +{
> + struct i3c_hub *priv = ctx;
> + u32 reg_val;
> + int ret;
> +
> + ret = regmap_read(priv->regmap, priv->reg_addr, ®_val);
> + if (ret)
> + return ret;
> +
> + *val = reg_val & 0xFF;
> + return 0;
> +}
> +
> +static int fops_access_reg_set(void *ctx, u64 val)
> +{
> + struct i3c_hub *priv = ctx;
> +
> + return regmap_write(priv->regmap, priv->reg_addr, val & 0xFF);
> +}
> +
> +DEFINE_DEBUGFS_ATTRIBUTE(fops_access_reg, fops_access_reg_get,
> + fops_access_reg_set, "0x%llX\n");
> +
> +static int i3c_hub_debugfs_init(struct i3c_hub *priv, const char *hub_id)
> +{
> + struct dentry *entry, *dt_conf_dir, *reg_dir;
> + struct dt_settings *settings = NULL;
> + int i;
> +
> + entry = debugfs_create_dir(hub_id, NULL);
> + if (IS_ERR(entry))
> + return PTR_ERR(entry);
> +
> + priv->debug_dir = entry;
> +
> + if (priv->dev_info)
> + debugfs_create_str("model", 0400, priv->debug_dir,
> + (char **)&priv->dev_info->model);
> +
> + entry = debugfs_create_dir("dt-conf", priv->debug_dir);
> + if (IS_ERR(entry))
> + goto err_remove;
> +
> + dt_conf_dir = entry;
> +
> + settings = &priv->settings;
> + debugfs_create_bool("cp0-ldo-en", 0400, dt_conf_dir,
> + &settings->cp0_ldo_en);
> + debugfs_create_bool("cp1-ldo-en", 0400, dt_conf_dir,
> + &settings->cp1_ldo_en);
> + debugfs_create_u32("cp0-ldo-volt", 0400, dt_conf_dir,
> + &settings->cp0_ldo_volt);
> + debugfs_create_u32("cp1-ldo-volt", 0400, dt_conf_dir,
> + &settings->cp1_ldo_volt);
> + debugfs_create_bool("tp0145-ldo-en", 0400, dt_conf_dir,
> + &settings->tp0145_ldo_en);
> + debugfs_create_bool("tp2367-ldo-en", 0400, dt_conf_dir,
> + &settings->tp2367_ldo_en);
> + debugfs_create_u32("tp0145-ldo-volt", 0400, dt_conf_dir,
> + &settings->tp0145_ldo_volt);
> + debugfs_create_u32("tp2367-ldo-volt", 0400, dt_conf_dir,
> + &settings->tp2367_ldo_volt);
> + debugfs_create_u32("tp0145-pullup", 0400, dt_conf_dir,
> + &settings->tp0145_pullup);
> + debugfs_create_u32("tp2367-pullup", 0400, dt_conf_dir,
> + &settings->tp2367_pullup);
> + debugfs_create_bool("hub-net-always-i3c", 0400, dt_conf_dir,
> + &settings->hub_net_always_i3c);
> + debugfs_create_u8("tp-scl-h-ack-cycles", 0400, dt_conf_dir,
> + &settings->tp_scl_h_ack_cycles);
> + debugfs_create_bool("handshake-optimize", 0400, dt_conf_dir,
> + &settings->handshake_optimize);
> + debugfs_create_u8("fast-drv-h-add-cycles", 0400, dt_conf_dir,
> + &settings->fast_drv_h_add_cycles);
> + debugfs_create_bool("fast-rson-en", 0400, dt_conf_dir,
> + &settings->fast_rson_en);
> + debugfs_create_bool("tp-od-vol-optimize", 0400, dt_conf_dir,
> + &settings->tp_od_vol_optimize);
> + debugfs_create_bool("tp-od-vref-optimize", 0400, dt_conf_dir,
> + &settings->tp_od_vref_optimize);
> +
> + for (i = 0; i < priv->dev_info->n_ports; ++i) {
> + char file_name[32];
> +
> + sprintf(file_name, "tp%i.mode", i);
> + debugfs_create_u8(file_name, 0400, dt_conf_dir,
> + &settings->tp[i].mode);
> + sprintf(file_name, "tp%i.pullup_en", i);
> + debugfs_create_bool(file_name, 0400, dt_conf_dir,
> + &settings->tp[i].pullup_en);
> + sprintf(file_name, "tp%i.io_mode", i);
> + debugfs_create_u8(file_name, 0400, dt_conf_dir,
> + &settings->tp[i].io_mode);
> + sprintf(file_name, "tp%i.poll_interval_ms", i);
> + debugfs_create_u32(file_name, 0400, dt_conf_dir,
> + &settings->tp[i].poll_interval_ms);
> + sprintf(file_name, "tp%i.clock_frequency", i);
> + debugfs_create_u32(file_name, 0400, dt_conf_dir,
> + &settings->tp[i].clock_frequency);
> + }
> +
> + entry = debugfs_create_dir("reg", priv->debug_dir);
> + if (IS_ERR(entry))
> + goto err_remove;
> +
> + reg_dir = entry;
> +
> + entry = debugfs_create_file_unsafe("access", 0600, reg_dir, priv,
> + &fops_access_reg);
> + if (IS_ERR(entry))
> + goto err_remove;
> +
> + debugfs_create_u8("offset", 0600, reg_dir, &priv->reg_addr);
> +
> + return 0;
> +
> +err_remove:
> + debugfs_remove_recursive(priv->debug_dir);
> + return PTR_ERR(entry);
> +}
> +
> +static void i3c_hub_trans_pre_cb(struct logical_bus *bus)
> +{
> + struct i3c_hub *priv = bus->priv;
> + struct device *dev = i3cdev_to_dev(priv->i3cdev);
> + int ret;
> +
> + if (priv->settings.tp[bus->tp_id].always_enable)
> + return;
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF,
> + GENMASK(bus->tp_id, bus->tp_id), bus->tp_map);
> + if (ret)
> + dev_warn(dev, "Failed to open Target Port(s)\n");
> +}
> +
> +static void i3c_hub_trans_post_cb(struct logical_bus *bus)
> +{
> + struct i3c_hub *priv = bus->priv;
> + struct device *dev = i3cdev_to_dev(priv->i3cdev);
> + int ret;
> +
> + if (priv->settings.tp[bus->tp_id].always_enable)
> + return;
> +
> + ret = regmap_update_bits(priv->regmap, I3C_HUB_TP_NET_CON_CONF,
> + GENMASK(bus->tp_id, bus->tp_id), 0x00);
> + if (ret)
> + dev_warn(dev, "Failed to close Target Port(s)\n");
> +}
> +
> +static struct logical_bus *bus_from_i3c_desc(struct i3c_dev_desc *desc)
> +{
> + struct i3c_master_controller *controller = i3c_dev_get_master(desc);
> +
> + return container_of(controller, struct logical_bus, controller);
> +}
> +
> +static struct logical_bus *bus_from_i2c_desc(struct i2c_dev_desc *desc)
> +{
> + struct i3c_master_controller *controller = i2c_dev_get_master(desc);
> +
> + return container_of(controller, struct logical_bus, controller);
> +}
> +
> +static struct i3c_master_controller *
> +parent_from_controller(struct i3c_master_controller *controller)
> +{
> + struct logical_bus *bus =
> + container_of(controller, struct logical_bus, controller);
> +
> + return bus->priv->driving_master;
> +}
> +
> +static struct i3c_master_controller *
> +parent_controller_from_i3c_desc(struct i3c_dev_desc *desc)
> +{
> + struct i3c_master_controller *controller = i3c_dev_get_master(desc);
> + struct logical_bus *bus =
> + container_of(controller, struct logical_bus, controller);
> +
> + return bus->priv->driving_master;
> +}
> +
> +static struct i3c_master_controller *
> +parent_controller_from_i2c_desc(struct i2c_dev_desc *desc)
> +{
> + struct i3c_master_controller *controller = desc->common.master;
> + struct logical_bus *bus =
> + container_of(controller, struct logical_bus, controller);
> +
> + return bus->priv->driving_master;
> +}
> +
> +static struct i3c_master_controller *
> +update_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc,
> + struct i3c_master_controller *parent)
> +{
> + struct i3c_master_controller *orig_parent = desc->master;
> +
> + desc->master = parent;
> +
> + return orig_parent;
> +}
> +
> +static void restore_i3c_i2c_desc_parent(struct i3c_i2c_dev_desc *desc,
> + struct i3c_master_controller *parent)
> +{
> + desc->master = parent;
> +}
> +
> +static int i3c_hub_read_transaction_status(struct i3c_hub *priv, u8 target_port,
> + u8 target_port_status, u32 data_len)
> +{
> + unsigned int status_read;
> + int ret;
> + struct i2c_adapter_group *smbus =
> + &priv->smbus_port_adapter[target_port];
> + u32 smbus_clk = priv->settings.tp[target_port].clock_frequency / 1000;
> + u8 status;
> + u8 ret_code;
> +
> + if (!priv->settings.tp[target_port].poll_interval_ms) {
> + ret = wait_for_completion_timeout(&smbus->completion,
> + smbus->i2c.timeout);
> + if (!ret) {
> + dev_err(&priv->i3cdev->dev,
> + "Status read timeout reached on target port %d\n",
> + target_port);
> + return -ETIMEDOUT;
> + }
> +
> + status = (u8)smbus->status &
> + I3C_HUB_CONTROLLER_AGENT_STATUS_MASK;
> + } else {
> + ret = regmap_read_poll_timeout(priv->regmap,
> + target_port_status,
> + status_read,
> + (u8)status_read &
> + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG,
> + I3C_HUB_SMBUS_STATUS_READ_INTERVAL_US_CEIL(
> + data_len, smbus_clk),
> + jiffies_to_usecs(smbus->i2c.timeout));
> +
> + if (ret) {
> + dev_err(&priv->i3cdev->dev,
> + "Status read timeout reached on target port %d\n",
> + target_port);
> + return ret;
> + }
> +
> + ret = regmap_write(priv->regmap, target_port_status,
> + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG);
> + if (ret)
> + return ret;
> +
> + status = (u8)status_read & I3C_HUB_CONTROLLER_AGENT_STATUS_MASK;
> + }
> +
> + ret_code = status >> I3C_HUB_CONTROLLER_AGENT_RET_CODE_SHIFT;
> +
> + switch (ret_code) {
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SUCCESS:
> + return 0;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ADDRESS_NACK:
> + dev_dbg(&priv->i3cdev->dev,
> + "TP%u SMBus: Address NACK (device not present)\n",
> + target_port);
> + return -ENXIO;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_DEVICE_BUSY:
> + dev_dbg(&priv->i3cdev->dev,
> + "TP%u SMBus: Device busy (data NACK after address ACK)\n",
> + target_port);
> + return -EREMOTEIO;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_READ_NOT_READY:
> + dev_dbg(&priv->i3cdev->dev,
> + "TP%u SMBus: Device read not ready (read address NACK after write)\n",
> + target_port);
> + return -ENXIO;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_RECOVERED:
> + dev_dbg(&priv->i3cdev->dev,
> + "TP%u SMBus: Sync issue recovered (SDA stuck low, recovered by 9 SCL pulses)\n",
> + target_port);
> + return -EAGAIN;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SYNC_BUS_CLEAR:
> + dev_dbg(&priv->i3cdev->dev,
> + "TP%u SMBus: Sync issue bus clear (recovered by SCL low 35ms)\n",
> + target_port);
> + return -EAGAIN;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_BUS_FAULT:
> + dev_err(&priv->i3cdev->dev,
> + "TP%u SMBus: Bus fault (SDA stuck low remains after recovery)\n",
> + target_port);
> + return -EIO;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_ARBITRATION_LOST:
> + dev_dbg(&priv->i3cdev->dev, "TP%u SMBus: Arbitration lost\n",
> + target_port);
> + return -EAGAIN;
> + case I3C_HUB_CONTROLLER_AGENT_RET_CODE_SCL_TIMEOUT:
> + dev_err(&priv->i3cdev->dev, "TP%u SMBus: SCL timeout\n",
> + target_port);
> + return -ETIMEDOUT;
> + default:
> + dev_err(&priv->i3cdev->dev,
> + "TP%u SMBus: Reserved/unknown return code 0x%x\n",
> + target_port, ret_code);
> + return -EIO;
> + }
> +}
> +
> +/*
> + * i3c_hub_smbus_msg() - This starts a smbus transaction by writing a descriptor
> + * and a message to the hub registers. Controller buffer page is determined by multiplying the
> + * target port index by four and adding the base page number to it.
> + * @hub: a pointer to the i3c hub main structure
> + * @target_port: a number of the port where the transaction will happen
> + * @xfers: i2c_msg struct received from the master_xfers callback
> + * @nxfers_i: the number of the current message
> + * @xfer_type: transfer type:
> + * - I3C_HUB_SMBUS_XFER_WRITE (0): single write
> + * - I3C_HUB_SMBUS_XFER_READ (1): single read
> + * - I3C_HUB_SMBUS_XFER_WR_RD (2): write followed by read
> + * (uses xfers[nxfers_i] as write and xfers[nxfers_i+1] as read)
> + *
> + * Return: 0 on success, negative errno on failure from hub status or regmap ops.
> + * Note: for WR_RD the caller must ensure xfers[nxfers_i+1] exists, the address
> + * matches, and write_len + read_len <= I3C_HUB_SMBUS_PAYLOAD_SIZE.
> + */
> +static int i3c_hub_smbus_msg(struct i3c_hub *hub, struct i2c_msg *xfers,
> + u8 target_port, u8 nxfers_i, u8 xfer_type)
> +{
> + u8 transaction_type = I3C_HUB_SMBUS_400_KHZ;
> + u8 controller_buffer_page =
> + I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * target_port;
> + int write_length = 0, read_length = 0;
> + u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + target_port;
> + u8 target_port_code = BIT(target_port);
> + u8 rw_address = xfers[nxfers_i].addr << 1;
> + u8 desc[I3C_HUB_SMBUS_DESCRIPTOR_SIZE] = { 0 };
> + int ret = 0;
> +
> + transaction_type =
> + i3c_hub_smbus_rate_bits_from_hz(hub->settings.tp[target_port].clock_frequency);
> +
> + switch (xfer_type) {
> + case I3C_HUB_SMBUS_XFER_WRITE:
> + write_length = xfers[nxfers_i].len;
> + break;
> + case I3C_HUB_SMBUS_XFER_READ:
> + read_length = xfers[nxfers_i].len;
> + rw_address |= BIT(0);
> + break;
> + case I3C_HUB_SMBUS_XFER_WR_RD:
> + write_length = xfers[nxfers_i].len;
> + read_length = xfers[nxfers_i + 1].len;
> + transaction_type |= BIT(0);
> + break;
> + default:
> + return -EINVAL;
> + }
> +
> + /* Assemble descriptor */
> + desc[0] = rw_address;
> + desc[1] = transaction_type;
> + desc[2] = write_length;
> + desc[3] = read_length;
> +
> + mutex_lock(&hub->page_mutex);
> + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR,
> + controller_buffer_page);
> + if (ret)
> + goto unlock;
> +
> + ret = regmap_bulk_write(hub->regmap, I3C_HUB_CONTROLLER_AGENT_BUFF,
> + desc, I3C_HUB_SMBUS_DESCRIPTOR_SIZE);
> + if (ret)
> + goto unlock;
> +
> + if (write_length) {
> + ret = regmap_bulk_write(hub->regmap,
> + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA,
> + xfers[nxfers_i].buf, write_length);
> + if (ret)
> + goto unlock;
> + }
> +
> + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
> + mutex_unlock(&hub->page_mutex);
> + if (ret)
> + return ret;
> +
> + /* Start transaction */
> + ret = regmap_write(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_TRANS_START,
> + target_port_code);
> + if (ret)
> + return ret;
> +
> + /* Get transaction status */
> + ret = i3c_hub_read_transaction_status(hub, target_port,
> + target_port_status,
> + write_length + read_length);
> + if (ret)
> + return ret;
> +
> + /* if read_length is non-zero, read back the data */
> + if (read_length) {
> + mutex_lock(&hub->page_mutex);
> + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR,
> + controller_buffer_page);
> + if (ret)
> + goto unlock;
> +
> + if (xfer_type == I3C_HUB_SMBUS_XFER_READ) {
> + ret = regmap_bulk_read(hub->regmap,
> + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA,
> + xfers[nxfers_i].buf, read_length);
> + } else {
> + ret = regmap_bulk_read(hub->regmap,
> + I3C_HUB_CONTROLLER_AGENT_BUFF_DATA +
> + write_length,
> + xfers[nxfers_i + 1].buf, read_length);
> + }
> + if (ret)
> + goto unlock;
> +
> + ret = regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
> + mutex_unlock(&hub->page_mutex);
> + }
> +
> + return ret;
> +unlock:
> + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
> + mutex_unlock(&hub->page_mutex);
> + return ret;
> +}
> +
> +static inline bool i3c_hub_can_combine_wr_rd(const struct i2c_msg *w,
> + const struct i2c_msg *r)
> +{
> + /* w: write, r: read; same addr; total length within payload */
> + return !(w->flags & I2C_M_RD) && (r->flags & I2C_M_RD) &&
> + w->addr == r->addr &&
> + (w->len + r->len) <= I3C_HUB_SMBUS_PAYLOAD_SIZE;
> +}
> +
> +/**
> + * i3c_hub_smbus_port_adapter_xfer() - i3c hub smbus transfer logic
> + * @adap: i2c_adapter corresponding with single port in the i3c hub
> + * @xfers: all messages descriptors and data
> + * @nxfers: amount of single messages in a transfer
> + *
> + * Return: function returns the sum of correctly sent messages (only those with hub return
> + * status 0x01)
> + */
> +static int i3c_hub_smbus_port_adapter_xfer(struct i2c_adapter *adap,
> + struct i2c_msg *xfers, int nxfers)
> +{
> + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap);
> + struct i3c_hub *hub = smbus->priv;
> + int ret_sum = 0, ret, len, type, nxfers_i;
> + const struct i2c_msg *cur = NULL, *next = NULL;
> +
> + for (nxfers_i = 0; nxfers_i < nxfers; nxfers_i++) {
> + cur = &xfers[nxfers_i];
> + len = cur->len;
> + type = cur->flags & I2C_M_RD ? I3C_HUB_SMBUS_XFER_READ :
> + I3C_HUB_SMBUS_XFER_WRITE;
> +
> + /* Per-message length limit check */
> + if (len > I3C_HUB_SMBUS_PAYLOAD_SIZE) {
> + dev_err(&adap->dev,
> + "Message nr. %d not sent - length over %d bytes.\n",
> + nxfers_i, I3C_HUB_SMBUS_PAYLOAD_SIZE);
> + continue;
> + }
> +
> + /* Try to combine write followed by read to the same address */
> + if (type == I3C_HUB_SMBUS_XFER_WRITE &&
> + (nxfers_i + 1) < nxfers) {
> + next = &xfers[nxfers_i + 1];
> + if (i3c_hub_can_combine_wr_rd(cur, next))
> + type = I3C_HUB_SMBUS_XFER_WR_RD;
> + }
> +
> + ret = i3c_hub_smbus_msg(hub, xfers, smbus->tp_port, nxfers_i,
> + type);
> + if (ret)
> + return ret;
> +
> + if (type == I3C_HUB_SMBUS_XFER_WR_RD) {
> + ret_sum += 2;
> + nxfers_i++; /* skip the next read message */
> +
> + } else {
> + ret_sum++;
> + }
> + }
> + return ret_sum;
> +}
> +
> +static int i3c_hub_bus_init(struct i3c_master_controller *controller)
> +{
> + struct logical_bus *bus =
> + container_of(controller, struct logical_bus, controller);
> +
> + controller->this = bus->priv->i3cdev->desc;
> + return 0;
> +}
> +
> +static void i3c_hub_bus_cleanup(struct i3c_master_controller *controller)
> +{
> + controller->this = NULL;
> +}
> +
> +static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + int ret;
> +
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + ret = parent->ops->attach_i3c_dev(dev);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + return ret;
> +}
> +
> +static int i3c_hub_reattach_i3c_dev(struct i3c_dev_desc *dev, u8 old_dyn_addr)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + int ret;
> +
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + ret = parent->ops->reattach_i3c_dev(dev, old_dyn_addr);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + return ret;
> +}
> +
> +static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> +
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + parent->ops->detach_i3c_dev(dev);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> +}
> +
> +static int i3c_hub_do_daa(struct i3c_master_controller *controller)
> +{
> + struct i3c_master_controller *parent =
> + parent_from_controller(controller);
> + int ret;
> +
> + down_write(&parent->bus.lock);
> + ret = parent->ops->do_daa(parent);
> + up_write(&parent->bus.lock);
> + return ret;
> +}
> +
> +static bool i3c_hub_supports_ccc_cmd(struct i3c_master_controller *controller,
> + const struct i3c_ccc_cmd *cmd)
> +{
> + struct i3c_master_controller *parent =
> + parent_from_controller(controller);
> +
> + return parent->ops->supports_ccc_cmd(parent, cmd);
> +}
> +
> +static int i3c_hub_send_ccc_cmd(struct i3c_master_controller *controller,
> + struct i3c_ccc_cmd *cmd)
> +{
> + struct i3c_master_controller *parent =
> + parent_from_controller(controller);
> + struct logical_bus *bus =
> + container_of(controller, struct logical_bus, controller);
> + int ret;
> +
> + if (cmd->id == I3C_CCC_RSTDAA(true))
> + return 0;
> +
> + i3c_hub_trans_pre_cb(bus);
> + down_read(&parent->bus.lock);
> + ret = parent->ops->send_ccc_cmd(parent, cmd);
> + up_read(&parent->bus.lock);
> + i3c_hub_trans_post_cb(bus);
> +
> + return ret;
> +}
> +
> +static int i3c_hub_priv_xfers(struct i3c_dev_desc *dev,
> + struct i3c_xfer *xfers, int nxfers,
> + enum i3c_xfer_mode mode)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + struct logical_bus *bus = bus_from_i3c_desc(dev);
> + int res;
> +
> + i3c_hub_trans_pre_cb(bus);
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + down_read(&parent->bus.lock);
> + res = parent->ops->i3c_xfers(dev, xfers, nxfers, mode);
> + up_read(&parent->bus.lock);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + i3c_hub_trans_post_cb(bus);
> +
> + return res;
> +}
> +
> +static int i3c_hub_attach_i2c_dev(struct i2c_dev_desc *dev)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i2c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + int ret;
> +
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + ret = parent->ops->attach_i2c_dev(dev);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + return ret;
> +}
> +
> +static void i3c_hub_detach_i2c_dev(struct i2c_dev_desc *dev)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i2c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> +
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + parent->ops->detach_i2c_dev(dev);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> +}
> +
> +static int i3c_hub_i2c_xfers(struct i2c_dev_desc *dev, struct i2c_msg *xfers,
> + int nxfers)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i2c_desc(dev);
> + struct logical_bus *bus = bus_from_i2c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + int ret;
> +
> + i3c_hub_trans_pre_cb(bus);
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + ret = parent->ops->i2c_xfers(dev, xfers, nxfers);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + i3c_hub_trans_post_cb(bus);
> + return ret;
> +}
> +
> +static int i3c_hub_request_ibi(struct i3c_dev_desc *dev,
> + const struct i3c_ibi_setup *req)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct logical_bus *bus = bus_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + int ret;
> +
> + i3c_hub_trans_pre_cb(bus);
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + down_read(&parent->bus.lock);
> + ret = parent->ops->request_ibi(dev, req);
> + up_read(&parent->bus.lock);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + i3c_hub_trans_post_cb(bus);
> + return ret;
> +}
> +
> +static void i3c_hub_free_ibi(struct i3c_dev_desc *dev)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct logical_bus *bus = bus_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> +
> + i3c_hub_trans_pre_cb(bus);
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + down_read(&parent->bus.lock);
> + parent->ops->free_ibi(dev);
> + up_read(&parent->bus.lock);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + i3c_hub_trans_post_cb(bus);
> +}
> +
> +static int i3c_hub_enable_ibi(struct i3c_dev_desc *dev)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct logical_bus *bus = bus_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + int ret;
> +
> + i3c_hub_trans_pre_cb(bus);
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + down_read(&parent->bus.lock);
> + ret = parent->ops->enable_ibi(dev);
> + up_read(&parent->bus.lock);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + i3c_hub_trans_post_cb(bus);
> + return ret;
> +}
> +
> +static int i3c_hub_disable_ibi(struct i3c_dev_desc *dev)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct logical_bus *bus = bus_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> + int ret;
> +
> + i3c_hub_trans_pre_cb(bus);
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + down_read(&parent->bus.lock);
> + ret = parent->ops->disable_ibi(dev);
> + up_read(&parent->bus.lock);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> + i3c_hub_trans_post_cb(bus);
> + return ret;
> +}
> +
> +static void i3c_hub_recycle_ibi_slot(struct i3c_dev_desc *dev,
> + struct i3c_ibi_slot *slot)
> +{
> + struct i3c_master_controller *parent =
> + parent_controller_from_i3c_desc(dev);
> + struct i3c_master_controller *orig_parent;
> +
> + orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> + parent->ops->recycle_ibi_slot(dev, slot);
> + restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> +}
> +
> +static const struct i3c_master_controller_ops i3c_hub_i3c_ops = {
> + .bus_init = i3c_hub_bus_init,
> + .bus_cleanup = i3c_hub_bus_cleanup,
> + .attach_i3c_dev = i3c_hub_attach_i3c_dev,
> + .reattach_i3c_dev = i3c_hub_reattach_i3c_dev,
> + .detach_i3c_dev = i3c_hub_detach_i3c_dev,
> + .do_daa = i3c_hub_do_daa,
> + .supports_ccc_cmd = i3c_hub_supports_ccc_cmd,
> + .send_ccc_cmd = i3c_hub_send_ccc_cmd,
> + .i3c_xfers = i3c_hub_priv_xfers,
> + .attach_i2c_dev = i3c_hub_attach_i2c_dev,
> + .detach_i2c_dev = i3c_hub_detach_i2c_dev,
> + .i2c_xfers = i3c_hub_i2c_xfers,
> + .request_ibi = i3c_hub_request_ibi,
> + .free_ibi = i3c_hub_free_ibi,
> + .enable_ibi = i3c_hub_enable_ibi,
> + .disable_ibi = i3c_hub_disable_ibi,
> + .recycle_ibi_slot = i3c_hub_recycle_ibi_slot,
> +};
> +
> +static int i3c_hub_logic_register(struct i3c_master_controller *controller,
> + struct device *parent)
> +{
> + return i3c_master_register(controller, parent, &i3c_hub_i3c_ops, false);
> +}
> +
> +static u32 i3c_hub_smbus_funcs(struct i2c_adapter *adapter)
> +{
> + return (I2C_FUNC_SMBUS_EMUL | I2C_FUNC_I2C) & ~I2C_FUNC_SMBUS_QUICK;
> +}
> +
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> +static int reg_i2c_target(struct i2c_client *client)
> +{
> + struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter);
> + struct smbus_backend *backend;
> + int ret = 0;
> +
> + if (!smbus)
> + return -EINVAL;
> +
> + mutex_lock(&smbus->mutex);
> +
> + list_for_each_entry(backend, &smbus->backend_entry, list) {
> + if (backend->client->addr == client->addr) {
> + ret = -EBUSY;
> + goto out;
> + }
> + }
> +
> + backend = kzalloc(sizeof(*backend), GFP_KERNEL);
> + if (!backend) {
> + ret = -ENOMEM;
> + goto out;
> + }
> +
> + backend->client = client;
> + list_add(&backend->list, &smbus->backend_entry);
> +
> +out:
> + mutex_unlock(&smbus->mutex);
> + return ret;
> +}
> +
> +static int unreg_i2c_target(struct i2c_client *client)
> +{
> + struct i2c_adapter_group *smbus = i2c_get_adapdata(client->adapter);
> + struct smbus_backend *backend;
> + bool found = false;
> +
> + if (!smbus)
> + return -EINVAL;
> +
> + mutex_lock(&smbus->mutex);
> +
> + list_for_each_entry(backend, &smbus->backend_entry, list) {
> + if (backend->client->addr == client->addr) {
> + list_del(&backend->list);
> + kfree(backend);
> + found = true;
> + break;
> + }
> + }
> +
> + mutex_unlock(&smbus->mutex);
> + return found ? 0 : -ENODEV;
> +}
> +#endif /* CONFIG_I2C_SLAVE */
> +
> +static const struct i2c_algorithm i3c_hub_smbus_algo = {
> + .master_xfer = i3c_hub_smbus_port_adapter_xfer,
> + .functionality = i3c_hub_smbus_funcs,
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> + .reg_slave = reg_i2c_target,
> + .unreg_slave = unreg_i2c_target,
> +#endif
> +};
> +
> +static void i3c_hub_delayed_work(struct work_struct *work)
> +{
> + struct i3c_hub *priv =
> + container_of(work, typeof(*priv), delayed_work.work);
> + struct device *dev = i3cdev_to_dev(priv->i3cdev);
> + struct logical_bus *bus;
> + struct i2c_adapter_group *smbus;
> + int ret;
> + int i;
> + unsigned int reg_val = 0;
> +
> + /* record reg 81: tp hubnetwork connection setting */
> + ret = regmap_read(priv->regmap, I3C_HUB_TP_NET_CON_CONF, ®_val);
> + if (ret) {
> + dev_warn(dev, "Failed to read hubnetwork connection setting\n");
> + return;
> + }
> +
> + ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, 0x00);
> + if (ret) {
> + dev_warn(dev, "Failed to close Target Port(s)\n");
> + return;
> + }
> +
> + for (i = 0; i < priv->dev_info->n_ports; ++i) {
> + bus = &priv->logical_bus[i];
> + if (bus->available) {
> + ret = regmap_update_bits(priv->regmap,
> + I3C_HUB_TP_NET_CON_CONF,
> + GENMASK(bus->tp_id, bus->tp_id),
> + bus->tp_map);
> + if (ret) {
> + dev_warn(dev,
> + "Failed to open Target Port(s)\n");
> + return;
> + }
> +
> + dev->of_node = bus->of_node;
> + ret = i3c_hub_logic_register(&bus->controller, dev);
> + if (ret) {
> + dev_warn(dev,
> + "Failed to register i3c controller - bus id:%i\n",
> + i);
> + return;
> + }
> + bus->registered = true;
> +
> + ret = regmap_update_bits(priv->regmap,
> + I3C_HUB_TP_NET_CON_CONF,
> + GENMASK(bus->tp_id, bus->tp_id),
> + 0x00);
> + if (ret) {
> + dev_warn(dev,
> + "Failed to close Target Port(s)\n");
> + return;
> + }
> +
> + if (!priv->settings.tp[i].always_enable)
> + reg_val &= ~GENMASK(bus->tp_id, bus->tp_id);
> + }
> + }
> +
> + /* update tp hubnetwork connection setting */
> + ret = regmap_write(priv->regmap, I3C_HUB_TP_NET_CON_CONF, reg_val);
> + if (ret) {
> + dev_warn(dev, "Failed to open Target Port(s)\n");
> + return;
> + }
> +
> + ret = i3c_master_do_daa(priv->driving_master);
> + if (ret) {
> + dev_warn(dev, "Failed to run DAA\n");
> + return;
> + }
> +
> + for (i = 0; i < priv->dev_info->n_ports; i++) {
> + smbus = &priv->smbus_port_adapter[i];
> + if (!smbus->used)
> + continue;
> +
> + if (!priv->settings.tp[i].poll_interval_ms)
> + continue;
> +
> + schedule_delayed_work(&smbus->delayed_work_polling,
> + msecs_to_jiffies(priv->settings.tp[i].poll_interval_ms));
> + }
> +}
> +
> +static int send_smbus_target_data_to_backend(struct i2c_adapter_group *smbus,
> + u8 address, u8 *local_buffer,
> + u8 len)
> +{
> +#if IS_ENABLED(CONFIG_I2C_SLAVE)
> + struct smbus_backend *backend;
> + struct i2c_client *client;
> + int i, ret;
> + u8 tmp;
> +
> + mutex_lock(&smbus->mutex);
> +
> + list_for_each_entry(backend, &smbus->backend_entry, list) {
> + client = backend->client;
> + if (client->addr == address >> 1) {
> + mutex_unlock(&smbus->mutex);
> + ret = i2c_slave_event(client, I2C_SLAVE_WRITE_REQUESTED,
> + &address);
> + if (ret)
> + return ret;
> +
> + for (i = 0; i < len; i++) {
> + ret = i2c_slave_event(client,
> + I2C_SLAVE_WRITE_RECEIVED,
> + &local_buffer[i]);
> + if (ret)
> + return ret;
> + }
> +
> + return i2c_slave_event(client, I2C_SLAVE_STOP, &tmp);
> + }
> + }
> +
> + mutex_unlock(&smbus->mutex);
> +#endif /* CONFIG_I2C_SLAVE */
> + return -ENXIO;
> +}
> +
> +static int read_smbus_target_buffer_page(struct i2c_adapter_group *smbus,
> + u8 target_buffer_page, u8 *address,
> + u8 *local_buffer, u8 *len)
> +{
> + struct i3c_hub *hub = smbus->priv;
> + struct device *dev = i3cdev_to_dev(hub->i3cdev);
> + u32 status;
> + int ret;
> +
> + mutex_lock(&hub->page_mutex);
> + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, target_buffer_page);
> +
> + ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_LENGTH, &status);
> + if (ret)
> + goto error;
> +
> + *len = status - 1;
> + if (!*len)
> + goto error;
> +
> + if (*len > I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE) {
> + dev_warn_ratelimited(dev,
> + "Received message too big for hub buffer\n");
> + ret = -EMSGSIZE;
> + *len = 0;
> + goto error;
> + }
> +
> + ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_ADDRESS, &status);
> + if (ret)
> + goto error;
> +
> + *address = status;
> +
> + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TARGET_BUFF_DATA,
> + local_buffer, *len);
> +
> +error:
> + regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, 0x00);
> + mutex_unlock(&hub->page_mutex);
> + return ret;
> +}
> +
> +static int process_smbus_controller_status(struct i2c_adapter_group *smbus,
> + u8 reg, u32 status)
> +{
> + struct i3c_hub *hub = smbus->priv;
> + int ret = 0;
> +
> + if (status & I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG) {
> + smbus->status = status;
> + ret = regmap_write(hub->regmap, reg,
> + I3C_HUB_CONTROLLER_AGENT_FINISH_FLAG);
> + complete(&smbus->completion);
> + }
> +
> + return ret;
> +}
> +
> +/**
> + * Controller buffer page is determined by adding the first buffer page number to port
> + * index multiplied by four. The two target buffer page numbers are determined the same
> + * way but they are offset by 2 and 3 from the controller page.
> + */
> +static int process_smbus_target_status(struct i2c_adapter_group *smbus, u8 reg,
> + u32 status)
> +{
> + struct i3c_hub *hub = smbus->priv;
> + struct device *dev = i3cdev_to_dev(hub->i3cdev);
> + u8 controller_buffer_page =
> + I3C_HUB_CONTROLLER_BUFFER_PAGE + 4 * smbus->tp_port;
> + u8 local_buffer[I3C_HUB_SMBUS_TARGET_PAYLOAD_SIZE] = { 0 };
> + u8 target_buffer_page, address = 0, len = 0, flag;
> + int ret;
> +
> + if (smbus->last_processed_buf)
> + status &= ~smbus->last_processed_buf;
> +
> + if (status & I3C_HUB_TARGET_BUF_0_RECEIVE) {
> + target_buffer_page = controller_buffer_page + 2;
> + flag = I3C_HUB_TARGET_BUF_0_RECEIVE;
> + } else if (status & I3C_HUB_TARGET_BUF_1_RECEIVE) {
> + target_buffer_page = controller_buffer_page + 3;
> + flag = I3C_HUB_TARGET_BUF_1_RECEIVE;
> + } else {
> + return -EINVAL;
> + }
> +
> + ret = read_smbus_target_buffer_page(smbus, target_buffer_page, &address,
> + local_buffer, &len);
> + if (ret && ret != -EMSGSIZE) {
> + dev_dbg(dev, "Failed to read target buffer page: %d\n", ret);
> + return ret;
> + }
> +
> + smbus->last_processed_buf = flag;
> +
> + if (status & I3C_HUB_TARGET_BUF_OVRFL)
> + flag |= I3C_HUB_TARGET_BUF_OVRFL;
> +
> + ret = regmap_write(hub->regmap, reg, flag);
> + if (ret) {
> + dev_dbg(dev, "Failed to clear target port status\n");
> + return ret;
> + }
> +
> + if (len) {
> + ret = send_smbus_target_data_to_backend(smbus, address,
> + local_buffer, len);
> + if (ret) {
> + dev_dbg(dev, "Failed to send data to backend: %d\n",
> + ret);
> + return ret;
> + }
> + }
> +
> + return 0;
> +}
> +
> +static int i3c_hub_process_smbus_status(struct i2c_adapter_group *smbus)
> +{
> + struct i3c_hub *hub = smbus->priv;
> + u8 target_port_status = I3C_HUB_TP0_SMBUS_AGNT_STS + smbus->tp_port;
> + struct device *dev = i3cdev_to_dev(hub->i3cdev);
> + u32 status;
> + int ret = 0;
> + u32 poll_interval_ms =
> + hub->settings.tp[smbus->tp_port].poll_interval_ms;
> +
> + ret = regmap_read(hub->regmap, target_port_status, &status);
> + if (ret)
> + return ret;
> +
> + /* smbus controller agent status */
> + if (!poll_interval_ms) {
> + ret = process_smbus_controller_status(smbus, target_port_status,
> + status);
> + if (ret)
> + dev_warn_ratelimited(dev,
> + "Failed to process smbus controller status\n");
> + }
> +
> + /* smbus target agent status */
> + status &= I3C_HUB_TARGET_BUF_STATUS_MASK;
> +
> + while (status) {
> + ret = process_smbus_target_status(smbus, target_port_status,
> + status);
> + if (ret) {
> + dev_warn_ratelimited(dev,
> + "Failed to process smbus target status: %d\n",
> + ret);
> + break;
> + }
> +
> + if (!poll_interval_ms)
> + break;
> +
> + ret = regmap_read(hub->regmap, target_port_status, &status);
> + if (ret)
> + break;
> + status &= I3C_HUB_TARGET_BUF_STATUS_MASK;
> + }
> +
> + return ret;
> +}
> +
> +/**
> + * i3c_hub_delayed_work_polling() - This delayed work is a polling mechanism to
> + * find if any transaction happened.
> + */
> +static void i3c_hub_delayed_work_polling(struct work_struct *work)
> +{
> + struct i2c_adapter_group *smbus =
> + container_of(work, typeof(*smbus), delayed_work_polling.work);
> + struct device *dev = i3cdev_to_dev(smbus->priv->i3cdev);
> + int ret;
> +
> + if (!list_empty(&smbus->backend_entry)) {
> + ret = i3c_hub_process_smbus_status(smbus);
> + if (ret)
> + dev_warn_ratelimited(dev,
> + "Failed to process TP %u smbus status: %d\n",
> + smbus->tp_port, ret);
> + }
> +
> + schedule_delayed_work(&smbus->delayed_work_polling,
> + msecs_to_jiffies(
> + smbus->priv->settings.tp[smbus->tp_port].poll_interval_ms));
> +}
> +
> +static int i3c_hub_smbus_ibi_handler(struct i3c_hub *hub,
> + const struct i3c_ibi_payload *payload)
> +{
> + struct i2c_adapter_group *smbus;
> + u8 tp, tps;
> + int val, ret = 0, rc;
> + struct device *dev = i3cdev_to_dev(hub->i3cdev);
> +
> + if (payload->len < 2) {
> + ret = regmap_read(hub->regmap, I3C_HUB_TP_SMBUS_AGNT_IBI_STS,
> + &val);
> + if (ret)
> + return ret;
> +
> + tps = (u8)val;
> + } else {
> + tps = ((const u8 *)payload->data)[1];
> + }
> +
> + if (!tps)
> + return 0;
> +
> + while (tps) {
> + tp = (u8)__ffs((unsigned long)tps);
> + tps &= (tps - 1);
> +
> + if (hub->settings.tp[tp].poll_interval_ms)
> + continue;
> +
> + smbus = &hub->smbus_port_adapter[tp];
> + rc = i3c_hub_process_smbus_status(smbus);
> + if (rc) {
> + dev_warn_ratelimited(dev,
> + "Failed to process TP %u smbus status: %d\n",
> + tp, rc);
> + }
> + }
> +
> + return 0;
> +}
> +
> +/*
> + * Sysfs attribute: clock_frequency
> + * Read/Write the SMBus clock frequency for this adapter's port.
> + */
> +static ssize_t clock_frequency_show(struct device *dev,
> + struct device_attribute *attr, char *buf)
> +{
> + struct i2c_adapter *adap = to_i2c_adapter(dev);
> + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap);
> + struct i3c_hub *hub = smbus->priv;
> +
> + return sprintf(buf, "%u\n",
> + hub->settings.tp[smbus->tp_port].clock_frequency);
> +}
> +
> +static ssize_t clock_frequency_store(struct device *dev,
> + struct device_attribute *attr,
> + const char *buf, size_t count)
> +{
> + struct i2c_adapter *adap = to_i2c_adapter(dev);
> + struct i2c_adapter_group *smbus = i2c_get_adapdata(adap);
> + struct i3c_hub *hub = smbus->priv;
> + u32 val;
> + int ret;
> +
> + ret = kstrtou32(buf, 0, &val);
> + if (ret)
> + return ret;
> +
> + if (!i3c_hub_smbus_validate_clock_frequency(val))
> + return -EINVAL;
> +
> + hub->settings.tp[smbus->tp_port].clock_frequency = val;
> +
> + return count;
> +}
> +static DEVICE_ATTR_RW(clock_frequency);
> +
> +static struct attribute *i3c_hub_smbus_attrs[] = {
> + &dev_attr_clock_frequency.attr,
> + NULL,
> +};
> +
> +ATTRIBUTE_GROUPS(i3c_hub_smbus);
> +
> +static int i3c_hub_smbus_tp_algo(struct i3c_hub *priv, int i)
> +{
> + struct device *dev = i3cdev_to_dev(priv->i3cdev);
> + int ret;
> + struct i2c_adapter_group *smbus = &priv->smbus_port_adapter[i];
> + struct i2c_adapter *i2c = &smbus->i2c;
> +
> + mutex_init(&smbus->mutex);
> + INIT_LIST_HEAD(&smbus->backend_entry);
> + smbus->priv = priv;
> + smbus->tp_port = i;
> + smbus->tp_mask = BIT(i);
> +
> + init_completion(&smbus->completion);
> + i2c->owner = THIS_MODULE;
> + i2c->algo = &i3c_hub_smbus_algo;
> + i2c->dev.parent = dev;
> + i2c->dev.of_node = smbus->of_node;
> + i2c->dev.groups = i3c_hub_smbus_groups;
> + i2c->timeout = HZ;
> + i2c->retries = 3;
> + snprintf(i2c->name, sizeof(i2c->name), "hub%s.port%d", dev_name(dev),
> + smbus->tp_port);
> +
> + if (priv->settings.tp[i].poll_interval_ms) {
> + ret = regmap_clear_bits(priv->regmap, I3C_HUB_TP_IBI_CONF,
> + smbus->tp_mask);
> + if (ret)
> + return ret;
> + INIT_DELAYED_WORK(&smbus->delayed_work_polling,
> + i3c_hub_delayed_work_polling);
> + priv->smbus_ibi_en_mask &= ~smbus->tp_mask;
> + } else {
> + ret = regmap_set_bits(priv->regmap, I3C_HUB_TP_IBI_CONF,
> + smbus->tp_mask);
> + if (ret)
> + return ret;
> + priv->smbus_ibi_en_mask |= smbus->tp_mask;
> + }
> +
> + /* Enable SDA hold-low when both SMBus Target Agent buffers are full.
> + * Used as a flow-control mechanism for MCTP to avoid upstream TX timeout
> + * when target buffers are not serviced in time.
> + */
> + ret = regmap_set_bits(priv->regmap,
> + I3C_HUB_ONCHIP_TD_AND_SMBUS_AGNT_CONF,
> + TARGET_AGENT_BUF_FULL_SDA_LOW_EN);
> + if (ret)
> + return ret;
> +
> + i2c_set_adapdata(i2c, smbus);
> +
> + ret = i2c_add_adapter(i2c);
> + if (ret)
> + return ret;
> +
> + smbus->used = 1;
> + return ret;
> +}
> +
> +static int i3c_hub_gpio_direction_input(struct gpio_chip *gc, unsigned int off)
> +{
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + int ret = 0;
> + u8 reg, mask = 0;
> +
> + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN :
> + I3C_HUB_TP_SCL_OUT_EN;
> + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]);
> +
> + ret = regmap_update_bits(hub->regmap, reg, mask, 0);
> + return ret;
> +}
> +
> +static int i3c_hub_gpio_direction_output(struct gpio_chip *gc, unsigned int off,
> + int val)
> +{
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + int ret = 0;
> + u8 reg, mask = 0;
> +
> + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN :
> + I3C_HUB_TP_SCL_OUT_EN;
> + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]);
> +
> + ret = regmap_update_bits(hub->regmap, reg, mask, mask);
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(hub->regmap, reg + 2, mask, val ? mask : 0);
> + return ret;
> +}
> +
> +static int i3c_hub_gpio_get_value(struct gpio_chip *gc, unsigned int off)
> +{
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + int ret = 0, val = 0, dir;
> + u8 reg, shift = 0;
> +
> + dir = gc->get_direction(gc, off);
> + if (dir)
> + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_LEVEL_STS :
> + I3C_HUB_TP_SCL_IN_LEVEL_STS;
> + else
> + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL :
> + I3C_HUB_TP_SCL_OUT_LEVEL;
> +
> + shift = gpio->tp[off / GPIO_BANK_SZ];
> +
> + ret = regmap_read(hub->regmap, reg, &val);
> + if (ret)
> + return ret;
> +
> + ret = (val >> shift) & 0x01;
> + return ret;
> +}
> +
> +static int i3c_hub_gpio_set_value(struct gpio_chip *gc, unsigned int off, int val)
> +{
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + u8 reg, mask = 0;
> +
> + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_LEVEL :
> + I3C_HUB_TP_SCL_OUT_LEVEL;
> + mask = BIT(gpio->tp[off / GPIO_BANK_SZ]);
> +
> + return regmap_update_bits(hub->regmap, reg, mask, val ? mask : 0);
> +}
> +
> +static int i3c_hub_gpio_get_direction(struct gpio_chip *gc, unsigned int off)
> +{
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + int ret = 0, dir = 0;
> + u8 reg, shift = 0;
> +
> + reg = off % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_OUT_EN :
> + I3C_HUB_TP_SCL_OUT_EN;
> + shift = gpio->tp[off / GPIO_BANK_SZ];
> +
> + ret = regmap_read(hub->regmap, reg, &dir);
> + if (ret)
> + return ret;
> +
> + ret = ~(dir >> shift) & 0x01;
> + return ret;
> +}
> +
> +static void i3c_hub_gpio_irq_mask(struct irq_data *d)
> +{
> + struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + u8 reg, hwirq = 0, mask = 0;
> +
> + hwirq = irqd_to_hwirq(d);
> +
> + reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN :
> + I3C_HUB_TP_SCL_IN_DETECT_IBI_EN;
> + mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]);
> +
> + regmap_update_bits(hub->regmap, reg, mask, 0);
> +}
> +
> +static void i3c_hub_gpio_irq_unmask(struct irq_data *d)
> +{
> + struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + u8 reg, hwirq = 0, mask = 0;
> +
> + hwirq = irqd_to_hwirq(d);
> +
> + reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN :
> + I3C_HUB_TP_SCL_IN_DETECT_IBI_EN;
> + mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]);
> +
> + regmap_update_bits(hub->regmap, reg, mask, mask);
> +}
> +
> +static int i3c_hub_gpio_irq_set_type(struct irq_data *d, unsigned int flow_type)
> +{
> + struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> + u8 hwirq = 0, mask = 0, val, tp, reg;
> + int ret;
> +
> + if (!(flow_type & IRQ_TYPE_EDGE_BOTH)) {
> + dev_err(&hub->i3cdev->dev, "irq %d: unsupported type %d\n",
> + d->irq, flow_type);
> + return -EINVAL;
> + }
> +
> + hwirq = irqd_to_hwirq(d);
> + tp = gpio->tp[hwirq / GPIO_BANK_SZ];
> +
> + if (tp == 0 || tp == 1 || tp == 4 || tp == 5) {
> + if (hwirq % GPIO_BANK_SZ) {
> + mask = SDA0145_IO_IN_DET_CFG_MASK;
> + val = SDA0145_IO_IN_DET_CFG(flow_type);
> + reg = I3C_HUB_TP_SDA_IN_DETECT_FLG;
> + } else {
> + mask = SCL0145_IO_IN_DET_CFG_MASK;
> + val = SCL0145_IO_IN_DET_CFG(flow_type);
> + reg = I3C_HUB_TP_SCL_IN_DETECT_FLG;
> + }
> + } else {
> + if (hwirq % GPIO_BANK_SZ) {
> + mask = SDA2367_IO_IN_DET_CFG_MASK;
> + val = SDA2367_IO_IN_DET_CFG(flow_type);
> + reg = I3C_HUB_TP_SDA_IN_DETECT_FLG;
> + } else {
> + mask = SCL2367_IO_IN_DET_CFG_MASK;
> + val = SCL2367_IO_IN_DET_CFG(flow_type);
> + reg = I3C_HUB_TP_SCL_IN_DETECT_FLG;
> + }
> + }
> +
> + ret = regmap_write(hub->regmap, reg, BIT(tp));
> + if (ret)
> + return ret;
> +
> + ret = regmap_update_bits(hub->regmap, I3C_HUB_TP_IN_DETECT_MODE_CONF,
> + mask, val);
> + return ret;
> +}
> +
> +static void i3c_hub_gpio_irq_bus_lock(struct irq_data *d)
> +{
> + struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> +
> + mutex_lock(&gpio->irq_mutex);
> +}
> +
> +static void i3c_hub_gpio_irq_bus_unlock(struct irq_data *d)
> +{
> + struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> + struct i3c_hub *hub = gpiochip_get_data(gc);
> + struct hub_gpio *gpio = &hub->gpio;
> +
> + mutex_unlock(&gpio->irq_mutex);
> +}
> +
> +static void i3c_hub_setup_gpio(struct i3c_hub *hub)
> +{
> + struct hub_gpio *gpio = &hub->gpio;
> + struct gpio_chip *gc = &gpio->chip;
> + struct gpio_irq_chip *girq;
> +
> + gc->direction_input = i3c_hub_gpio_direction_input;
> + gc->direction_output = i3c_hub_gpio_direction_output;
> + gc->get = i3c_hub_gpio_get_value;
> + gc->set = i3c_hub_gpio_set_value;
> + gc->get_direction = i3c_hub_gpio_get_direction;
> + gc->can_sleep = true;
> +
> + gc->base = -1;
> + gc->ngpio = gpio->nums;
> + gc->label = dev_name(&hub->i3cdev->dev);
> + gc->parent = &hub->i3cdev->dev;
> + gc->owner = THIS_MODULE;
> +
> + /* irq_chip support */
> + mutex_init(&gpio->irq_mutex);
> +
> + gpio->irq_chip.name = dev_name(&hub->i3cdev->dev);
> + gpio->irq_chip.irq_mask = i3c_hub_gpio_irq_mask;
> + gpio->irq_chip.irq_unmask = i3c_hub_gpio_irq_unmask;
> + gpio->irq_chip.irq_set_type = i3c_hub_gpio_irq_set_type;
> + gpio->irq_chip.irq_bus_lock = i3c_hub_gpio_irq_bus_lock;
> + gpio->irq_chip.irq_bus_sync_unlock = i3c_hub_gpio_irq_bus_unlock;
> +
> + girq = &gpio->chip.irq;
> +
> + /* This will let us handle the parent IRQ in the driver */
> + girq->parent_handler = NULL;
> + girq->num_parents = 0;
> + girq->parents = NULL;
> + girq->default_type = IRQ_TYPE_NONE;
> + girq->handler = handle_simple_irq;
> + girq->threaded = true;
> + girq->first = 0;
> +
> + girq->chip = &gpio->irq_chip;
> +}
> +
> +static void i3c_hub_io_ibi_handler(struct i3c_hub *hub,
> + const struct i3c_ibi_payload *payload)
> +{
> + struct hub_gpio *gpio = &hub->gpio;
> + struct gpio_chip *gc = &gpio->chip;
> + u8 level, hwirq, tmp;
> + u8 pending[GPIO_BANK_SZ];
> + u8 tp[GPIO_BANK_SZ];
> + int i, irq, ret, index;
> +
> + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_OUT_EN, tp,
> + GPIO_BANK_SZ);
> + if (ret) {
> + dev_err(&hub->i3cdev->dev, "Failed to read OUT_EN: %d\n", ret);
> + return;
> + }
> +
> + ret = regmap_bulk_read(hub->regmap, I3C_HUB_TP_SCL_IN_DETECT_FLG,
> + pending, GPIO_BANK_SZ);
> + if (ret) {
> + dev_err(&hub->i3cdev->dev, "Failed to read DETECT_FLG: %d\n",
> + ret);
> + return;
> + }
> +
> + for (i = 0; i < GPIO_BANK_SZ; i++) {
> + tmp = ~tp[i] & pending[i];
> +
> + while (tmp) {
> + level = __ffs(tmp);
> + tmp &= ~(1 << level);
> +
> + /* Check if this port is in GPIO mode */
> + index = gpio->port_to_index[level];
> + if (index < 0) {
> + /* Non-GPIO mode port, skip without clearing.
> + * This can happen because IN_DETECT IBI enable is
> + * configured in groups (e.g., TP0145/TP2367), not
> + * per individual port. Simply skip - the flag is
> + * harmless and will be overwritten by next detection.
> + */
> + dev_dbg(&hub->i3cdev->dev,
> + "IBI detect flag on non-GPIO port %d, skipping\n",
> + level);
> + continue;
> + }
> +
> + hwirq = index * 2 + i;
> + irq = irq_find_mapping(gc->irq.domain, hwirq);
> +
> + /* Clear the flag after processing */
> + regmap_write(hub->regmap,
> + I3C_HUB_TP_SCL_IN_DETECT_FLG + i,
> + BIT(level));
> +
> + if (unlikely(irq <= 0)) {
> + dev_warn_ratelimited(gc->parent,
> + "unmapped interrupt %d\n",
> + hwirq);
> + } else {
> + handle_nested_irq(irq);
> + }
> + }
> + }
> +}
> +
> +static void i3c_hub_ibi_handler(struct i3c_device *dev,
> + const struct i3c_ibi_payload *payload)
> +{
> + struct i3c_hub *priv = i3cdev_get_drvdata(dev);
> + int ret, val = 0;
> + u8 status = 0;
> +
> + if (!payload->len) {
> + dev_dbg(&dev->dev,
> + "Zero-length IBI payload, reading status register\n");
> + ret = regmap_read(priv->regmap, I3C_HUB_DEV_AND_IBI_STS, &val);
> + if (ret) {
> + dev_warn_ratelimited(&dev->dev,
> + "Failed to read IBI status: %d\n",
> + ret);
> + return;
> + }
> + status = (u8)val;
> + } else {
> + if (!payload->data) {
> + dev_warn_ratelimited(&dev->dev,
> + "IBI payload data is NULL with len=%d\n",
> + payload->len);
> + return;
> + }
> + status = ((const u8 *)payload->data)[0];
> + }
> +
> + if (status & TP_IO_FLAG_STATUS)
> + i3c_hub_io_ibi_handler(priv, payload);
> +
> + if (status & SMBUS_AGENT_EVENT_FLAG_STATUS) {
> + ret = i3c_hub_smbus_ibi_handler(priv, payload);
> + if (ret) {
> + dev_warn_ratelimited(&dev->dev,
> + "Failed to handle SMBus IBI: %d\n",
> + ret);
> + return;
> + }
> + }
> +}
> +
> +static inline void i3c_hub_regmap_lock(void *ctx)
> +{
> + mutex_lock(&i3c_hub_regmap_mutex);
> +}
> +
> +static inline void i3c_hub_regmap_unlock(void *ctx)
> +{
> + mutex_unlock(&i3c_hub_regmap_mutex);
> +}
> +
> +static int i3c_hub_probe(struct i3c_device *i3cdev)
> +{
> + const struct regmap_config i3c_hub_regmap_config = {
> + .reg_bits = 8,
> + .val_bits = 8,
> + .lock = i3c_hub_regmap_lock,
> + .unlock = i3c_hub_regmap_unlock,
> + .lock_arg = NULL,
> + };
> + struct device *dev = &i3cdev->dev;
> + struct device_node *node = NULL;
> + struct regmap *regmap;
> + struct i3c_hub *priv;
> + char hub_id[32];
> + int ret;
> + int i;
> + struct i3c_ibi_setup ibireq = {};
> +
> + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> + if (!priv)
> + return -ENOMEM;
> +
> + priv->i3cdev = i3cdev;
> + priv->driving_master = i3c_dev_get_master(i3cdev->desc);
> + mutex_init(&priv->page_mutex);
> + i3cdev_set_drvdata(i3cdev, priv);
> + INIT_DELAYED_WORK(&priv->delayed_work, i3c_hub_delayed_work);
> + i3c_hub_of_default_configuration(dev);
> +
> + regmap = devm_regmap_init_i3c(i3cdev, &i3c_hub_regmap_config);
> + if (IS_ERR(regmap)) {
> + ret = PTR_ERR(regmap);
> + dev_err(dev, "Failed to register I3C HUB regmap\n");
> + return ret;
> + }
> + priv->regmap = regmap;
> +
> + priv->dev_info = i3c_hub_lookup_dev_info(priv);
> + if (IS_ERR(priv->dev_info)) {
> + ret = PTR_ERR(priv->dev_info);
> + dev_err(dev, "Failed to lookup HUB dev info\n");
> + return ret;
> + }
> +
> + sprintf(hub_id, "i3c-hub-%d-%llx",
> + i3c_dev_get_master(i3cdev->desc)->bus.id,
> + i3cdev->desc->info.pid);
> + ret = i3c_hub_debugfs_init(priv, hub_id);
> + if (ret)
> + dev_dbg(dev, "Failed to initialized DebugFS.\n");
> +
> + ret = i3c_hub_read_id(dev);
> + if (ret)
> + goto error;
> +
> + priv->hub_dt_sel_id = -1;
> + priv->hub_dt_cp1_id = -1;
> + if (priv->hub_pin_cp1_id >= 0 && priv->hub_pin_sel_id >= 0)
> + /* Find hub node in DT matching HW ID or just first without ID provided in DT */
> + node = i3c_hub_get_dt_hub_node(dev->parent->of_node, priv);
> +
> + if (!node) {
> + dev_info(dev,
> + "No DT entry - running with hardware defaults.\n");
> + } else {
> + of_node_get(node);
> + i3c_hub_of_get_conf_static(dev, node);
> + i3c_hub_of_get_conf_runtime(dev, node);
> + of_node_put(node);
> + }
> +
> + /* Unlock access to protected registers */
> + ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE,
> + REGISTERS_UNLOCK_CODE);
> + if (ret) {
> + dev_err(dev, "Failed to unlock HUB's protected registers\n");
> + goto error;
> + }
> +
> + /* Register logic for native smbus ports */
> + for (i = 0; i < priv->dev_info->n_ports; i++) {
> + priv->smbus_port_adapter[i].used = 0;
> + if (priv->settings.tp[i].mode == I3C_HUB_DT_TP_MODE_SMBUS) {
> + ret = i3c_hub_smbus_tp_algo(priv, i);
> + if (ret)
> + dev_warn(dev,
> + "Failed to setup SMBus adapter, port: %d\n",
> + i);
> + }
> + }
> +
> + ret = i3c_hub_configure_hw(dev);
> + if (ret) {
> + dev_err(dev, "Failed to configure the HUB\n");
> + goto error;
> + }
> +
> + /* Lock access to protected registers */
> + ret = regmap_write(priv->regmap, I3C_HUB_PROTECTION_CODE,
> + REGISTERS_LOCK_CODE);
> + if (ret) {
> + dev_err(dev, "Failed to lock HUB's protected registers\n");
> + goto error;
> + }
> +
> + /* IBI */
> + ibireq.handler = i3c_hub_ibi_handler;
> + ibireq.max_payload_len = IBI_MAX_PAYLOAD_LEN;
> + ibireq.num_slots = IBI_SLOT_NUMS;
> +
> + ret = i3c_device_request_ibi(i3cdev, &ibireq);
> + if (ret) {
> + dev_err(dev, "Failed to requeset ibi!\n");
> + goto error;
> + }
> +
> + ret = i3c_device_enable_ibi(i3cdev);
> + if (ret) {
> + dev_err(dev, "Failed to enable ibi!\n");
> + goto err_free_ibi;
> + }
> +
> + /* TBD: Apply special/security lock here using DEV_CMD register */
> +
> + if (priv->gpio.nums > 0) {
> + i3c_hub_setup_gpio(priv);
> +
> + ret = devm_gpiochip_add_data(dev, &priv->gpio.chip, priv);
> + if (ret) {
> + dev_err(dev, "gpiochip add data fail!\n");
> + goto err_dis_ibi;
> + }
> + }
> +
> + schedule_delayed_work(&priv->delayed_work, msecs_to_jiffies(100));
> +
> + return 0;
> +
> +err_dis_ibi:
> + i3c_device_disable_ibi(i3cdev);
> +err_free_ibi:
> + i3c_device_free_ibi(i3cdev);
> +error:
> + debugfs_remove_recursive(priv->debug_dir);
> + return ret;
> +}
> +
> +static void i3c_hub_remove(struct i3c_device *i3cdev)
> +{
> + struct i3c_hub *priv = i3cdev_get_drvdata(i3cdev);
> + int i;
> +
> + i3c_device_disable_ibi(i3cdev);
> + i3c_device_free_ibi(i3cdev);
> +
> + for (i = 0; i < priv->dev_info->n_ports; i++) {
> + if (priv->smbus_port_adapter[i].used) {
> + cancel_delayed_work_sync(&priv->smbus_port_adapter[i].delayed_work_polling);
> + i2c_del_adapter(&priv->smbus_port_adapter[i].i2c);
> + }
> +
> + if (priv->logical_bus[i].registered)
> + i3c_master_unregister(&priv->logical_bus[i].controller);
> + }
> +
> + cancel_delayed_work_sync(&priv->delayed_work);
> + debugfs_remove_recursive(priv->debug_dir);
> +}
> +
> +static struct i3c_driver i3c_hub = {
> + .driver.name = "rts490xa-i3c-hub",
> + .id_table = i3c_hub_ids,
> + .probe = i3c_hub_probe,
> + .remove = i3c_hub_remove,
> +};
> +
> +module_i3c_driver(i3c_hub);
> +
> +MODULE_DESCRIPTION("RTS490XA I3C HUB driver");
> +MODULE_LICENSE("GPL");
> --
> 2.34.1
>
--
Alexandre Belloni, co-owner and COO, Bootlin
Embedded Linux and Kernel engineering
https://bootlin.com
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
2026-05-25 13:25 ` Alexandre Belloni
@ 2026-05-25 13:28 ` sashiko-bot
2026-05-25 14:00 ` Krzysztof Kozlowski
2026-05-25 19:11 ` Greg KH
3 siblings, 0 replies; 9+ messages in thread
From: sashiko-bot @ 2026-05-25 13:28 UTC (permalink / raw)
To: zain_zhou; +Cc: conor+dt, devicetree, robh
Thank you for your contribution! Sashiko AI review found 13 potential issue(s) to consider:
- [High] OF node references are not properly managed during DT iteration, causing memory leaks and dangling pointers.
- [High] Probe error paths fail to unregister successfully added I2C adapters, leaving dangling pointers to freed memory.
- [High] Missing `reinit_completion()` before starting SMBus transactions causes premature completion following a timeout.
- [High] The driver temporarily overwrites the hub device's `of_node` during logical bus registration without restoring it.
- [High] Incorrect teardown order in `i3c_hub_remove` allows delayed works to execute after resources are destroyed.
- [High] Concurrent transfers corrupt the shared device descriptor's master pointer and cause bus faults.
- [High] Dynamic Address Assignment (DAA) fails to open target ports, preventing devices from receiving addresses.
- [High] GPIO IRQ callbacks use sleeping regmap operations while the IRQ core holds raw spinlocks.
- [High] Dropping a mutex before calling an I2C slave event callback allows concurrent unregistration and freeing of the target driver.
- [High] Parsing the I3C port ID using `%i` allows negative indices, bypassing bounds checks.
- [Medium] SMBus target logic incorrectly drops 0-byte writes (e.g., SMBus Quick Commands) instead of notifying the I2C slave backend.
- [Medium] Return value of `regmap_write` is ignored before reading buffer length.
- [Medium] Early returns in `i3c_hub_delayed_work` error paths leak dynamically registered logical buses and abort partial initialization.
--
commit 9f58d1e76a5b04c7b2e855b10a1eea1283c0e9ee
Author: Yin Zhou <zain_zhou@realsil.com.cn>
staging: i3c: add Realtek RTS490x I3C HUB driver
Add driver for Realtek RTS490x series I3C HUB devices. It supports DT-based
configuration, LDO and pull-up management, logical I3C bus registration,
SMBus agent functionality, and GPIO expansion.
> diff --git a/drivers/staging/rts490x/rts490xa-i3c-hub.c b/drivers/staging/rts490x/rts490xa-i3c-hub.c
> new file mode 100644
> index 0000000000000..fdfff5c6dff5d
> --- /dev/null
> +++ b/drivers/staging/rts490x/rts490xa-i3c-hub.c
[ ... ]
> @@ -354,12 +354,6 @@ static void i3c_hub_tp_of_get_setting(struct device *dev,
> if (id >= priv->dev_info->n_ports) {
> dev_warn(dev,
> "Invalid target port index found in DT: %i\n",
> id);
> continue;
> }
>
> priv->smbus_port_adapter[id].of_node = tp_node;
[Severity: High]
Could this lead to dangling pointers? The iterator loop drops the reference
to the node when it advances, leaving priv->smbus_port_adapter[id].of_node
pointing to freed memory since of_node_get() isn't called.
[ ... ]
> @@ -818,12 +818,7 @@ static void i3c_hub_of_get_conf_runtime(struct device *dev,
> struct device_node *i3c_node;
> int i3c_id;
> u8 tp_mask;
>
> for_each_available_child_of_node(node, i3c_node) {
> if (!i3c_node->full_name ||
> (sscanf(i3c_node->full_name, "i3c%i@%hhx", &i3c_id,
> &tp_mask) != 2))
> continue;
>
> if (i3c_id < priv->dev_info->n_ports) {
[Severity: High]
Could the use of %i allow negative values like "i3c-1@0" to be parsed?
If i3c_id is negative, it gets promoted to an unsigned value in the bounds
check (i3c_id < priv->dev_info->n_ports), allowing it to bypass the check
and write out of bounds.
> priv->logical_bus[i3c_id].available = true;
> priv->logical_bus[i3c_id].of_node = i3c_node;
[Severity: High]
Is an of_node_get() missing here as well before saving i3c_node?
> priv->logical_bus[i3c_id].tp_map = tp_mask;
> priv->logical_bus[i3c_id].priv = priv;
> priv->logical_bus[i3c_id].tp_id = i3c_id;
> }
> }
> }
[ ... ]
> @@ -861,15 +861,15 @@ static struct device_node *i3c_hub_get_dt_hub_node(struct device_node *node,
>
> if (!found_id) {
> /*
> * Just keep reference to first HUB node with no ID in case no ID
> * matching
> */
> if (!hub_node_no_id &&
> priv->hub_dt_sel_id == -1 &&
> priv->hub_dt_cp1_id == -1)
> hub_node_no_id = hub_node;
> } else {
> return hub_node;
> }
> }
> }
>
> return hub_node_no_id;
> }
[Severity: High]
Can returning the node directly from the iterator cause a memory leak?
Returning hub_node without releasing the reference held by the loop iterator
appears to leak it. Conversely, if it returns hub_node_no_id, isn't that
pointer already freed since the loop has advanced or completed?
[ ... ]
> @@ -1429,20 +1429,19 @@ static int i3c_hub_read_transaction_status(struct i3c_hub *priv, u8 target_port,
> u8 target_port_status, u32 data_len)
> {
> unsigned int status_read;
> int ret;
> struct i2c_adapter_group *smbus =
> &priv->smbus_port_adapter[target_port];
> u32 smbus_clk = priv->settings.tp[target_port].clock_frequency / 1000;
> u8 status;
> u8 ret_code;
>
> if (!priv->settings.tp[target_port].poll_interval_ms) {
> ret = wait_for_completion_timeout(&smbus->completion,
> smbus->i2c.timeout);
[Severity: High]
Should there be a call to reinit_completion() before starting the SMBus
transaction?
If a timeout occurs, a delayed hardware interrupt might call complete()
later. The next transaction would then return immediately from
wait_for_completion_timeout() because the leftover completion count wasn't
consumed.
[ ... ]
> @@ -1664,15 +1664,13 @@ static void i3c_hub_bus_cleanup(struct i3c_master_controller *controller)
> {
> controller->this = NULL;
> }
>
> static int i3c_hub_attach_i3c_dev(struct i3c_dev_desc *dev)
[ ... ]
> @@ -1714,14 +1714,10 @@ static void i3c_hub_detach_i3c_dev(struct i3c_dev_desc *dev)
> static int i3c_hub_do_daa(struct i3c_master_controller *controller)
> {
> struct i3c_master_controller *parent =
> parent_from_controller(controller);
> int ret;
>
> down_write(&parent->bus.lock);
> ret = parent->ops->do_daa(parent);
> up_write(&parent->bus.lock);
> return ret;
> }
[Severity: High]
Is this missing a call to i3c_hub_trans_pre_cb(bus) before doing DAA?
Without opening the logical bus's target port beforehand, devices on that
port wouldn't receive ENTDAA broadcasts or dynamic addresses.
[ ... ]
> @@ -1742,16 +1742,14 @@ static int i3c_hub_send_ccc_cmd(struct i3c_master_controller *controller,
> static int i3c_hub_priv_xfers(struct i3c_dev_desc *dev,
> struct i3c_xfer *xfers, int nxfers,
> enum i3c_xfer_mode mode)
> {
> struct i3c_master_controller *parent =
> parent_controller_from_i3c_desc(dev);
> struct i3c_master_controller *orig_parent;
> struct logical_bus *bus = bus_from_i3c_desc(dev);
> int res;
>
> i3c_hub_trans_pre_cb(bus);
> orig_parent = update_i3c_i2c_desc_parent(&dev->common, parent);
> down_read(&parent->bus.lock);
> res = parent->ops->i3c_xfers(dev, xfers, nxfers, mode);
> up_read(&parent->bus.lock);
> restore_i3c_i2c_desc_parent(&dev->common, orig_parent);
> i3c_hub_trans_post_cb(bus);
>
> return res;
> }
[Severity: High]
Could concurrent transfers to the same device cause data corruption and bus
faults?
Since update_i3c_i2c_desc_parent() modifies the shared desc->master without
synchronization, and the I3C core holds the bus lock only as a reader,
concurrent accesses might restore incorrect pointers.
Additionally, if two threads communicate on the same port concurrently, won't
the first thread to finish close the target port in i3c_hub_trans_post_cb
while the second is still communicating?
[ ... ]
> @@ -2037,21 +2037,18 @@ static void i3c_hub_delayed_work(struct work_struct *work)
> for (i = 0; i < priv->dev_info->n_ports; ++i) {
> bus = &priv->logical_bus[i];
> if (bus->available) {
> ret = regmap_update_bits(priv->regmap,
> I3C_HUB_TP_NET_CON_CONF,
> GENMASK(bus->tp_id, bus->tp_id),
> bus->tp_map);
> if (ret) {
> dev_warn(dev,
> "Failed to open Target Port(s)\n");
> return;
> }
>
> dev->of_node = bus->of_node;
[Severity: High]
Does this permanently overwrite the physical hub device's dev->of_node
pointer?
By not restoring it after registration, it gets left pointing to its last
processed child port, which might break subsequent DT lookups.
> ret = i3c_hub_logic_register(&bus->controller, dev);
> if (ret) {
> dev_warn(dev,
> "Failed to register i3c controller - bus id:%i\n",
> i);
> return;
> }
[Severity: Medium]
Will returning early here leak any logical buses that were successfully
registered in previous loop iterations? The early return also aborts the DAA
sequence and scheduling of the polling works, leaving the hub in a partially
initialized state.
[ ... ]
> @@ -2091,18 +2091,18 @@ static int send_smbus_target_data_to_backend(struct i2c_adapter_group *smbus,
> u8 address, u8 *local_buffer,
> u8 len)
> {
> #if IS_ENABLED(CONFIG_I2C_SLAVE)
> struct smbus_backend *backend;
> struct i2c_client *client;
> int i, ret;
> u8 tmp;
>
> mutex_lock(&smbus->mutex);
>
> list_for_each_entry(backend, &smbus->backend_entry, list) {
> client = backend->client;
> if (client->addr == address >> 1) {
> mutex_unlock(&smbus->mutex);
> ret = i2c_slave_event(client, I2C_SLAVE_WRITE_REQUESTED,
> &address);
[Severity: High]
Can dropping the mutex here lead to a use-after-free?
This breaks synchronization with unreg_i2c_target(), allowing a target
driver to concurrently unregister and free the backend structure while
i2c_slave_event() is invoking the callback using the pointer derived from
the freed backend.
[ ... ]
> @@ -2126,20 +2126,17 @@ static int read_smbus_target_buffer_page(struct i2c_adapter_group *smbus,
> u8 target_buffer_page, u8 *address,
> u8 *local_buffer, u8 *len)
> {
> struct i3c_hub *hub = smbus->priv;
> struct device *dev = i3cdev_to_dev(hub->i3cdev);
> u32 status;
> int ret;
>
> mutex_lock(&hub->page_mutex);
> regmap_write(hub->regmap, I3C_HUB_PAGE_PTR, target_buffer_page);
[Severity: Medium]
Should the return value of regmap_write() be checked here? If this write
fails, the subsequent reads will silently operate on whatever page was
previously selected.
>
> ret = regmap_read(hub->regmap, I3C_HUB_TARGET_BUFF_LENGTH, &status);
> if (ret)
> goto error;
>
> *len = status - 1;
> if (!*len)
> goto error;
[Severity: Medium]
Will this incorrectly drop 0-byte writes, such as SMBus Quick Commands?
For a quick command containing only the address byte, status is 1, making
*len evaluate to 0. Jumping to error skips sending the event to the I2C
slave backend.
[ ... ]
> @@ -2598,16 +2598,12 @@ static void i3c_hub_gpio_irq_mask(struct irq_data *d)
> {
> struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
> struct i3c_hub *hub = gpiochip_get_data(gc);
> struct hub_gpio *gpio = &hub->gpio;
> u8 reg, hwirq = 0, mask = 0;
>
> hwirq = irqd_to_hwirq(d);
>
> reg = hwirq % GPIO_BANK_SZ ? I3C_HUB_TP_SDA_IN_DETECT_IBI_EN :
> I3C_HUB_TP_SCL_IN_DETECT_IBI_EN;
> mask = BIT(gpio->tp[hwirq / GPIO_BANK_SZ]);
>
> regmap_update_bits(hub->regmap, reg, mask, 0);
> }
[Severity: High]
Does calling regmap_update_bits() here cause a "scheduling while atomic" BUG?
The generic IRQ subsystem holds the IRQ descriptor's raw spinlock while
invoking the irq_mask/irq_unmask/irq_set_type callbacks, but regmap over I3C
acquires a mutex and can sleep.
[ ... ]
> @@ -2880,14 +2880,14 @@ static int i3c_hub_probe(struct i3c_device *i3cdev)
>
> /* Register logic for native smbus ports */
> for (i = 0; i < priv->dev_info->n_ports; i++) {
> priv->smbus_port_adapter[i].used = 0;
> if (priv->settings.tp[i].mode == I3C_HUB_DT_TP_MODE_SMBUS) {
> ret = i3c_hub_smbus_tp_algo(priv, i);
> if (ret)
> dev_warn(dev,
> "Failed to setup SMBus adapter, port: %d\n",
> i);
> }
> }
[ ... ]
> /* IBI */
> ibireq.handler = i3c_hub_ibi_handler;
> ibireq.max_payload_len = IBI_MAX_PAYLOAD_LEN;
> ibireq.num_slots = IBI_SLOT_NUMS;
>
> ret = i3c_device_request_ibi(i3cdev, &ibireq);
> if (ret) {
> dev_err(dev, "Failed to requeset ibi!\n");
> goto error;
> }
[Severity: High]
If an error occurs after registering the SMBus adapters, does jumping to the
error labels bypass unregistering the I2C adapters?
Since priv is managed by devm_kzalloc and gets freed when probe fails,
wouldn't the I2C subsystem retain references to freed memory?
[ ... ]
> @@ -2965,11 +2965,11 @@ static void i3c_hub_remove(struct i3c_device *i3cdev)
> {
> struct i3c_hub *priv = i3cdev_get_drvdata(i3cdev);
> int i;
>
> i3c_device_disable_ibi(i3cdev);
> i3c_device_free_ibi(i3cdev);
>
> for (i = 0; i < priv->dev_info->n_ports; i++) {
> if (priv->smbus_port_adapter[i].used) {
> cancel_delayed_work_sync(&priv->smbus_port_adapter[i].delayed_work_polling);
> i2c_del_adapter(&priv->smbus_port_adapter[i].i2c);
> }
>
> if (priv->logical_bus[i].registered)
> i3c_master_unregister(&priv->logical_bus[i].controller);
> }
>
> cancel_delayed_work_sync(&priv->delayed_work);
[Severity: High]
Is this teardown order safe?
By canceling priv->delayed_work after tearing down the polling works and
logical buses, if priv->delayed_work runs concurrently, it could schedule
new polling works or register logical buses right before the device is
removed, leading to resource leaks or use-after-free conditions.
> debugfs_remove_recursive(priv->debug_dir);
> }
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260525125128.297-1-zain_zhou@realsil.com.cn?part=2
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB
2026-05-25 12:51 [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB zain_zhou
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
2026-05-25 13:02 ` [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB sashiko-bot
@ 2026-05-25 13:58 ` Krzysztof Kozlowski
2026-05-25 18:45 ` Rob Herring (Arm)
3 siblings, 0 replies; 9+ messages in thread
From: Krzysztof Kozlowski @ 2026-05-25 13:58 UTC (permalink / raw)
To: zain_zhou, linux-staging, linux-i3c, devicetree
Cc: gregkh, alexandre.belloni, Frank.Li, robh, krzk+dt, conor+dt,
linusw, brgl, linux-gpio, linux-kernel, wei_wang
On 25/05/2026 14:51, zain_zhou@realsil.com.cn wrote:
> From: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Add DT binding schema for Realtek RTS490x series I3C HUB devices.
>
> The binding describes configuration properties for:
> - LDO enable/disable and voltage level per port group
> - Pull-up resistance per port group
> - IO driver strength per port
> - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up,
> IO mode, SMBus clock frequency and polling interval
> - Hub network always-I3C mode
> - Hardware identification via CSEL pin (id) and CP1 pins (id-cp1)
>
> Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Changes in v2:
Messed patch. DCO is always the last. And this should tell you that
changelog does not belong here - goes under ---.
Anyway, did you test the binding this time or we expect another failures?
> - Rework binding per Krzysztof Kozlowski's review:
> add realtek, vendor prefix to all custom properties; use boolean
> for enable flags; use u32 with unit suffixes (-microvolt, -ohms)
> for voltage/resistance; change to unevaluatedProperties: false;
> fix title, maintainer name, description, $nodename pattern
> - Consolidate examples; add dt-bindings/i2c/i2c.h include
> ---
> .../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 263 ++++++++++++++++++
> 1 file changed, 263 insertions(+)
> create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
>
> diff --git a/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
> new file mode 100644
> index 000000000000..851a433abcd3
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
> @@ -0,0 +1,263 @@
> +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
> +%YAML 1.2
> +---
> +$id: http://devicetree.org/schemas/i3c/realtek,rts490x-i3c-hub.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 is an I3C HUB device that provides voltage level
> + translation between I3C controller and target devices, bus capacitance
> + isolation, address conflict isolation, I3C port expansion (up to 8
> + target ports), simultaneous dual-controller port support, and per-port
> + mode selection (I3C, SMBus, GPIO, or disabled).
> +
> +properties:
> + $nodename:
> + pattern: "^hub@[0-9a-f]+(,[0-9a-f]+)*$"
> +
> + compatible:
> + const: realtek,rts490x-i3c-hub
> +
> + reg:
> + maxItems: 1
> + description:
> + Encodes the static I2C address, manufacturer ID, and part/instance ID
> + as defined by the I3C specification.
> +
> + assigned-address:
> + $ref: /schemas/types.yaml#/definitions/uint32
> + minimum: 0x1
> + maximum: 0xff
> + description:
> + Dynamic I3C address to assign to this device.
> +
> + dcr:
Where is the property defined?
Read carefully writing bindings doc and DTS101 slides. I finished review
here. You still did not implement things we asked - to follow other
bindings style.
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
2026-05-25 13:25 ` Alexandre Belloni
2026-05-25 13:28 ` sashiko-bot
@ 2026-05-25 14:00 ` Krzysztof Kozlowski
2026-05-25 19:11 ` Greg KH
3 siblings, 0 replies; 9+ messages in thread
From: Krzysztof Kozlowski @ 2026-05-25 14:00 UTC (permalink / raw)
To: zain_zhou, linux-staging, linux-i3c, devicetree
Cc: gregkh, alexandre.belloni, Frank.Li, robh, krzk+dt, conor+dt,
linusw, brgl, linux-gpio, linux-kernel, wei_wang
On 25/05/2026 14:51, zain_zhou@realsil.com.cn wrote:
> From: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Add driver for Realtek RTS490x series I3C HUB devices.
>
> The driver supports:
> - Device Tree based configuration of LDO, pull-up, IO strength
> and per-port mode (I3C/SMBus/GPIO/disabled)
> - Logical I3C bus registration per target port
> - SMBus agent functionality with IBI and polling modes
> - GPIO chip with IRQ support
> - DebugFS interface for register access and DT config inspection
> - IBI (In-Band Interrupt) handling
>
> The driver is placed in staging as it has known issues to be resolved
> before mainlining; see drivers/staging/rts490x/TODO for details.
>
> Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Changes in v2:
> - Update driver to match v2 DT binding: parse physical values
> directly via of_property_read_bool/u32; drop string enum lookup
> tables; update property names with realtek, prefix and unit suffixes
> - Fix maintainer name; move MAINTAINERS entry to driver patch
> - Code style: rename TPn_* macros to TPN_*; rename SMBus frequency
> constants to UPPER_CASE; add mutex field comments
> ---
> MAINTAINERS | 6 +
> drivers/staging/Kconfig | 2 +
> drivers/staging/Makefile | 1 +
> drivers/staging/rts490x/Kconfig | 16 +
> drivers/staging/rts490x/Makefile | 2 +
> drivers/staging/rts490x/TODO | 35 +
So this is staging driver? Then why do you send us bindings? We don't
need them. You must follow staging rules then.
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB
2026-05-25 12:51 [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB zain_zhou
` (2 preceding siblings ...)
2026-05-25 13:58 ` Krzysztof Kozlowski
@ 2026-05-25 18:45 ` Rob Herring (Arm)
3 siblings, 0 replies; 9+ messages in thread
From: Rob Herring (Arm) @ 2026-05-25 18:45 UTC (permalink / raw)
To: zain_zhou
Cc: krzk+dt, linux-staging, gregkh, devicetree, alexandre.belloni,
Frank.Li, conor+dt, linux-i3c, linux-kernel, brgl, linux-gpio,
linusw, wei_wang
On Mon, 25 May 2026 20:51:27 +0800, zain_zhou@realsil.com.cn wrote:
> From: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Add DT binding schema for Realtek RTS490x series I3C HUB devices.
>
> The binding describes configuration properties for:
> - LDO enable/disable and voltage level per port group
> - Pull-up resistance per port group
> - IO driver strength per port
> - Per target-port mode (I3C/SMBus/GPIO/disabled), pull-up,
> IO mode, SMBus clock frequency and polling interval
> - Hub network always-I3C mode
> - Hardware identification via CSEL pin (id) and CP1 pins (id-cp1)
>
> Signed-off-by: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Changes in v2:
> - Rework binding per Krzysztof Kozlowski's review:
> add realtek, vendor prefix to all custom properties; use boolean
> for enable flags; use u32 with unit suffixes (-microvolt, -ohms)
> for voltage/resistance; change to unevaluatedProperties: false;
> fix title, maintainer name, description, $nodename pattern
> - Consolidate examples; add dt-bindings/i2c/i2c.h include
> ---
> .../bindings/i3c/realtek,rts490x-i3c-hub.yaml | 263 ++++++++++++++++++
> 1 file changed, 263 insertions(+)
> create mode 100644 Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.yaml
>
My bot found errors running 'make dt_binding_check' on your patch:
yamllint warnings/errors:
dtschema/dtc warnings/errors:
Documentation/devicetree/bindings/i3c/realtek,rts490x-i3c-hub.example.dtb: /example-0/i3c-master@d040000/hub@70,4ba00000000/target-port@1/i2c@10: failed to match any schema with compatible: ['i2c-slave-mqueue']
doc reference errors (make refcheckdocs):
See https://patchwork.kernel.org/project/devicetree/patch/20260525125128.297-1-zain_zhou@realsil.com.cn
The base for the series is generally the latest rc1. A different dependency
should be noted in *this* patch.
If you already ran 'make dt_binding_check' and didn't see the above
error(s), then make sure 'yamllint' is installed and dt-schema is up to
date:
pip3 install dtschema --upgrade
Please check and re-submit after running the above command yourself. Note
that DT_SCHEMA_FILES can be set to your schema file to speed up checking
your schema. However, it must be unset to test all examples with your schema.
^ permalink raw reply [flat|nested] 9+ messages in thread
* Re: [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
` (2 preceding siblings ...)
2026-05-25 14:00 ` Krzysztof Kozlowski
@ 2026-05-25 19:11 ` Greg KH
3 siblings, 0 replies; 9+ messages in thread
From: Greg KH @ 2026-05-25 19:11 UTC (permalink / raw)
To: zain_zhou
Cc: linux-staging, linux-i3c, devicetree, alexandre.belloni, Frank.Li,
robh, krzk+dt, conor+dt, linusw, brgl, linux-gpio, linux-kernel,
wei_wang
On Mon, May 25, 2026 at 08:51:28PM +0800, zain_zhou@realsil.com.cn wrote:
> From: Yin Zhou <zain_zhou@realsil.com.cn>
>
> Add driver for Realtek RTS490x series I3C HUB devices.
>
> The driver supports:
> - Device Tree based configuration of LDO, pull-up, IO strength
> and per-port mode (I3C/SMBus/GPIO/disabled)
> - Logical I3C bus registration per target port
> - SMBus agent functionality with IBI and polling modes
> - GPIO chip with IRQ support
> - DebugFS interface for register access and DT config inspection
> - IBI (In-Band Interrupt) handling
>
> The driver is placed in staging as it has known issues to be resolved
> before mainlining; see drivers/staging/rts490x/TODO for details.
No, again, please do this correctly and fix up everything and put it in
the real part of the kernel. It will only take you more work if it were
to be in staging.
thanks,
greg k-h
^ permalink raw reply [flat|nested] 9+ messages in thread
end of thread, other threads:[~2026-05-25 19:12 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-25 12:51 [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB zain_zhou
2026-05-25 12:51 ` [PATCH v2 2/2] staging: i3c: add Realtek RTS490x I3C HUB driver zain_zhou
2026-05-25 13:25 ` Alexandre Belloni
2026-05-25 13:28 ` sashiko-bot
2026-05-25 14:00 ` Krzysztof Kozlowski
2026-05-25 19:11 ` Greg KH
2026-05-25 13:02 ` [PATCH v2 1/2] dt-bindings: i3c: add Realtek RTS490x I3C HUB sashiko-bot
2026-05-25 13:58 ` Krzysztof Kozlowski
2026-05-25 18:45 ` Rob Herring (Arm)
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox