* [PATCH v2 0/2] usb: typec: add CIX Type-C PD controller driver
@ 2026-11-09 9:23 Gary Yang
2026-11-09 9:23 ` [PATCH v2 1/2] dt-bindings: usb: add Realtek RTS5453H Type-C port controller Gary Yang
2026-11-09 9:24 ` [PATCH v2 2/2] usb: typec: add CIX Type-C PD controller driver Gary Yang
0 siblings, 2 replies; 3+ messages in thread
From: Gary Yang @ 2026-11-09 9:23 UTC (permalink / raw)
To: Heikki Krogerus, Greg Kroah-Hartman, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Fugang Duan
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
cix-kernel-upstream, gary.yang
This series adds a Type-C PD controller driver for CIX platforms.
The first supported controller is the Realtek RTS5453H, a stand-alone
port controller: the CC and USB Power Delivery state machines run in
its firmware and are configured through vendor registers rather than
the TCPC register interface, so it cannot be driven by the tcpm
framework. The driver translates the controller status registers into
Type-C class notifications and drives the platform PHY's orientation
switch and mode mux.
The driver is named after the CIX platform integration rather than the
RTS5453H so that further register-compatible PD controllers can be
supported without renaming it.
Patch 1 adds the devicetree binding and patch 2 adds the driver and its
MAINTAINERS entry. The two DTS patches from v1 (the Sky1 I2C
#address-cells/#size-cells fix and the Orion O6 board enablement) have
been split out of this series and will be sent separately through the
SoC tree.
Changes since v1:
- Drop the DTS patches from the series.
- binding: describe the single connector as a plain "connector" child
node instead of an addressed node, and stop requiring
"try-power-role", which only applies to dual-role ports.
- driver: add the missing USB_ROLE_SWITCH Kconfig dependency.
- driver: register the DisplayPort partner alternate mode only for
actual DP connections, with a valid VDO, call
typec_altmode_update_active() on attach/detach and set
no_mode_control since the firmware drives mode entry.
- driver: never pass a NULL alternate mode to typec_mux_set(), and
treat debug accessories as disconnected everywhere.
- driver: handle USB 2.0-only connections in orientation, role and mux
state.
- driver: only cache the USB role after the role switch accepted it,
and force a role refresh on resume.
- driver: use typec_get_fw_cap() instead of open-coding the connector
properties.
- driver: acquire the mux and role switch before registering the port,
use dev_err_probe() on probe failures and reset the mux to the safe
state on teardown.
- driver: remove the undocumented disable_pd_interrupt sysfs attribute
and the manual disable_irq()/IRQ_DISABLE_UNLAZY handling that could
leak the shared IRQ's disable depth. This also fixes the build error
reported by the kernel test robot on sparc64 and s390.
- driver: use a named i2c_device_id initializer and add the missing
hibernation PM callbacks.
Tested on a Radxa Orion O6 (Sky1): attach/detach, USB-only and DP-only
alt modes, cable orientation detection and HPD reporting all work.
Gary Yang (2):
dt-bindings: usb: add Realtek RTS5453H Type-C port controller
usb: typec: add CIX Type-C PD controller driver
.../bindings/usb/realtek,rts5453h.yaml | 112 +++
MAINTAINERS | 7 +
drivers/usb/typec/Kconfig | 13 +
drivers/usb/typec/Makefile | 1 +
drivers/usb/typec/cix_pd.c | 687 ++++++++++++++++++
5 files changed, 820 insertions(+)
create mode 100644 Documentation/devicetree/bindings/usb/realtek,rts5453h.yaml
create mode 100644 drivers/usb/typec/cix_pd.c
--
2.50.1
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v2 1/2] dt-bindings: usb: add Realtek RTS5453H Type-C port controller
2026-11-09 9:23 [PATCH v2 0/2] usb: typec: add CIX Type-C PD controller driver Gary Yang
@ 2026-11-09 9:23 ` Gary Yang
2026-11-09 9:24 ` [PATCH v2 2/2] usb: typec: add CIX Type-C PD controller driver Gary Yang
1 sibling, 0 replies; 3+ messages in thread
From: Gary Yang @ 2026-11-09 9:23 UTC (permalink / raw)
To: Heikki Krogerus, Greg Kroah-Hartman, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Fugang Duan
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
cix-kernel-upstream, gary.yang
Document the Realtek RTS5453H stand-alone USB Type-C port controller.
The controller is connected over I2C and manages a single USB Type-C
connector, described with the generic USB connector binding.
The connector's OF graph links to the Type-C orientation switch and
mode mux provided by the platform PHY.
Signed-off-by: Gary Yang <gary.yang@cixtech.com>
---
.../bindings/usb/realtek,rts5453h.yaml | 112 ++++++++++++++++++
1 file changed, 112 insertions(+)
create mode 100644 Documentation/devicetree/bindings/usb/realtek,rts5453h.yaml
diff --git a/Documentation/devicetree/bindings/usb/realtek,rts5453h.yaml b/Documentation/devicetree/bindings/usb/realtek,rts5453h.yaml
new file mode 100644
index 000000000000..14bbd563e66a
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/realtek,rts5453h.yaml
@@ -0,0 +1,112 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/usb/realtek,rts5453h.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Realtek RTS5453H USB Type-C port controller
+
+description: |
+ The RTS5453H is a stand-alone USB Type-C port controller with USB
+ Power Delivery support. It is controlled over I2C and reports
+ changes of the port state through an interrupt line.
+
+ Orientation switching and USB/DisplayPort lane muxing are performed by
+ the platform PHY. The controller finds the Type-C switch and mux
+ through the OF graph of its connector node.
+
+maintainers:
+ - Gary Yang <gary.yang@cixtech.com>
+
+properties:
+ compatible:
+ const: realtek,rts5453h
+
+ reg:
+ maxItems: 1
+
+ interrupts:
+ maxItems: 1
+
+ connector:
+ type: object
+ $ref: /schemas/connector/usb-connector.yaml#
+ unevaluatedProperties: false
+ description:
+ The USB Type-C connector managed by this controller.
+
+ required:
+ - data-role
+ - power-role
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - connector
+
+additionalProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ typec@30 {
+ compatible = "realtek,rts5453h";
+ reg = <0x30>;
+ interrupt-parent = <&gpio0>;
+ interrupts = <8 IRQ_TYPE_LEVEL_LOW>;
+
+ connector {
+ compatible = "usb-c-connector";
+ data-role = "host";
+ power-role = "source";
+
+ ports {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ port@0 {
+ reg = <0>;
+
+ typec_hs_ep: endpoint {
+ remote-endpoint = <&usb_hs_ep>;
+ };
+ };
+
+ port@1 {
+ reg = <1>;
+
+ typec_ss_ep: endpoint {
+ remote-endpoint = <&usb_ss_ep>;
+ };
+ };
+ };
+ };
+ };
+ };
+
+ usb {
+ usb-role-switch;
+
+ port {
+ usb_hs_ep: endpoint {
+ remote-endpoint = <&typec_hs_ep>;
+ };
+ };
+ };
+
+ usb-mux {
+ orientation-switch;
+ mode-switch;
+
+ port {
+ usb_ss_ep: endpoint {
+ remote-endpoint = <&typec_ss_ep>;
+ };
+ };
+ };
--
2.50.1
^ permalink raw reply [flat|nested] 3+ messages in thread
* [PATCH v2 2/2] usb: typec: add CIX Type-C PD controller driver
2026-11-09 9:23 [PATCH v2 0/2] usb: typec: add CIX Type-C PD controller driver Gary Yang
2026-11-09 9:23 ` [PATCH v2 1/2] dt-bindings: usb: add Realtek RTS5453H Type-C port controller Gary Yang
@ 2026-11-09 9:24 ` Gary Yang
1 sibling, 0 replies; 3+ messages in thread
From: Gary Yang @ 2026-11-09 9:24 UTC (permalink / raw)
To: Heikki Krogerus, Greg Kroah-Hartman, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Fugang Duan
Cc: linux-usb, devicetree, linux-arm-kernel, linux-kernel,
cix-kernel-upstream, gary.yang
Add a driver for the CIX Type-C PD controller, which is connected over
I2C and reports power, data and plug orientation changes through a
level-triggered interrupt. The Realtek RTS5453H is the first supported
device.
The controller runs the Type-C and PD state machines in firmware and
exposes the current port status over I2C. The driver translates that
into Type-C class notifications, driving the orientation switch and
mode mux registered by the platform PHY for the DisplayPort alternate
mode. It also drives the platform USB role switch so that the USB
controller follows the negotiated data role.
Several controller instances can share a single interrupt line, so each
instance registers a handler on it with IRQF_SHARED and only accesses
its own registers.
The driver is named after the CIX platform integration rather than the
RTS5453H so that further register-compatible PD controllers can be
supported without renaming it.
Signed-off-by: Gary Yang <gary.yang@cixtech.com>
---
MAINTAINERS | 7 +
drivers/usb/typec/Kconfig | 13 +
drivers/usb/typec/Makefile | 1 +
drivers/usb/typec/cix_pd.c | 687 +++++++++++++++++++++++++++++++++++++
4 files changed, 708 insertions(+)
create mode 100644 drivers/usb/typec/cix_pd.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 93abbb315065..bd99313bf91d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6522,6 +6522,13 @@ M: Satish Kharat <satishkh@cisco.com>
S: Supported
F: drivers/infiniband/hw/usnic/
+CIX USB TYPE-C PD CONTROLLER
+M: Gary Yang <gary.yang@cixtech.com>
+L: linux-usb@vger.kernel.org
+S: Maintained
+F: Documentation/devicetree/bindings/usb/realtek,rts5453h.yaml
+F: drivers/usb/typec/cix_pd.c
+
CLANG CONTEXT ANALYSIS
M: Marco Elver <elver@google.com>
R: Bart Van Assche <bvanassche@acm.org>
diff --git a/drivers/usb/typec/Kconfig b/drivers/usb/typec/Kconfig
index 2f80c2792dbd..4d62f5deeeba 100644
--- a/drivers/usb/typec/Kconfig
+++ b/drivers/usb/typec/Kconfig
@@ -110,6 +110,19 @@ config TYPEC_WUSB3801
If you choose to build this driver as a dynamically linked module, the
module will be called wusb3801.ko.
+config TYPEC_CIX_PD
+ tristate "CIX Type-C PD controller driver"
+ depends on I2C
+ depends on USB_ROLE_SWITCH
+ depends on ARCH_CIX || COMPILE_TEST
+ select REGMAP_I2C
+ help
+ Say Y or M here if your system has a CIX Type-C PD controller.
+ This driver currently supports the Realtek RTS5453H controller.
+
+ If you choose to build this driver as a dynamically linked module, the
+ module will be called cix_pd.ko.
+
source "drivers/usb/typec/mux/Kconfig"
source "drivers/usb/typec/altmodes/Kconfig"
diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
index 8a6a1c663eb6..bb3f3139fd7e 100644
--- a/drivers/usb/typec/Makefile
+++ b/drivers/usb/typec/Makefile
@@ -11,4 +11,5 @@ obj-$(CONFIG_TYPEC_HD3SS3220) += hd3ss3220.o
obj-$(CONFIG_TYPEC_STUSB160X) += stusb160x.o
obj-$(CONFIG_TYPEC_RT1719) += rt1719.o
obj-$(CONFIG_TYPEC_WUSB3801) += wusb3801.o
+obj-$(CONFIG_TYPEC_CIX_PD) += cix_pd.o
obj-$(CONFIG_TYPEC) += mux/
diff --git a/drivers/usb/typec/cix_pd.c b/drivers/usb/typec/cix_pd.c
new file mode 100644
index 000000000000..30c879f976c2
--- /dev/null
+++ b/drivers/usb/typec/cix_pd.c
@@ -0,0 +1,687 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2025 Cix Technology Group Co., Ltd.
+ *
+ * Driver for the CIX Type-C PD controller.
+ */
+
+#include <linux/bits.h>
+#include <linux/device.h>
+#include <linux/err.h>
+#include <linux/errno.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/regmap.h>
+#include <linux/usb/pd.h>
+#include <linux/usb/role.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/typec_altmode.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_mux.h>
+
+#define RTS_DATA_CONTROL 0x50
+#define I2C_INT_ACK_BIT BIT(2)
+#define HPD_IRQ_ACK BIT(5)
+#define RTS_DATA_STATUS 0x5F
+/* byte 1 */
+#define CONNECTION_ORIENTATION BIT(1)
+#define USB2_CONNECTION BIT(4)
+#define USB3_2_CONNECTION BIT(5)
+#define USB_DATA_ROLE BIT(7)
+/* byte 2 */
+#define DP_CONNECTION BIT(0)
+#define DP_SOURCE_SINK BIT(1)
+#define DEBUG_ACCESSORY_ATTACHED BIT(4)
+#define HPD_IRQ BIT(6)
+#define HPD_STATE BIT(7)
+/* byte 3 */
+#define POWER_SOURCE BIT(0)
+#define POWER_SINK BIT(1)
+#define POWER_MASK 3
+
+#define DATA_STATUS_COUNT 6
+
+struct cix_pd {
+ struct device *dev;
+ struct regmap *regmap;
+ struct mutex lock; /* device lock */
+ struct mutex irq_lock; /* protects IRQ state transitions */
+ int id;
+ int irq;
+ u8 data_status[DATA_STATUS_COUNT];
+ struct typec_displayport_data dp_data;
+ struct typec_mux_state state;
+ struct typec_capability caps;
+ struct typec_mux *mux;
+ struct usb_role_switch *role_sw;
+ struct typec_altmode *port_altmode;
+ struct typec_altmode *partner_altmode;
+ struct typec_port *port;
+ enum usb_role role;
+ unsigned long mode;
+ u32 hpd_status;
+ struct typec_partner *partner;
+ struct typec_partner_desc desc;
+ bool wake_enabled;
+};
+
+static int cix_pd_block_read(struct cix_pd *rts, u8 reg, void *val, size_t len)
+{
+ return regmap_bulk_read(rts->regmap, reg, val, len);
+}
+
+static int cix_pd_block_write(struct cix_pd *rts, u8 reg,
+ const void *val, size_t len)
+{
+ return regmap_bulk_write(rts->regmap, reg, val, len);
+}
+
+static const struct regmap_config cix_pd_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0x7F,
+};
+
+static const char *const dp_only_mode = "Alt Mode:DP";
+static const char *const dp_usb_mode = "Alt Mode:DP + USB";
+static const char *const usb_mode = "Alt Mode:USB";
+
+static const char *const power_source = "power source";
+static const char *const power_sink = "power sink";
+
+static const char *const orientation_normal = "orientation normal";
+static const char *const orientation_reverse = "orientation reverse";
+
+static const char *const data_host = "usb host";
+static const char *const data_device = "usb device";
+
+static const char *cix_pd_pwr_role_str(const struct cix_pd *rts)
+{
+ switch (rts->data_status[3] & POWER_MASK) {
+ case POWER_SOURCE:
+ return power_source;
+ case POWER_SINK:
+ return power_sink;
+ default:
+ return "unknown";
+ }
+}
+
+/*
+ * The controller negotiates the DisplayPort alternate mode autonomously,
+ * so the capabilities below describe the fixed set supported by the
+ * platform DP output: a receptacle exposing DFP_D with pin assignments
+ * C, D and E.
+ */
+#define CIX_PD_DP_PIN_ASSIGN (BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D) | \
+ BIT(DP_PIN_ASSIGN_E))
+
+static const struct typec_altmode_desc cix_pd_dp_port_altmode = {
+ .svid = USB_TYPEC_DP_SID,
+ .mode = USB_TYPEC_DP_MODE,
+ .vdo = DP_CAP_DFP_D | DP_CAP_RECEPTACLE |
+ (CIX_PD_DP_PIN_ASSIGN << 8),
+};
+
+static const struct typec_altmode_desc cix_pd_dp_partner_altmode = {
+ .svid = USB_TYPEC_DP_SID,
+ .mode = USB_TYPEC_DP_MODE,
+ .vdo = DP_CAP_UFP_D | DP_CAP_RECEPTACLE |
+ (CIX_PD_DP_PIN_ASSIGN << 16),
+ .mode_selection = true,
+};
+
+static int cix_pd_typec_port_update(struct cix_pd *typec)
+{
+ struct typec_displayport_data *dp_data = &typec->dp_data;
+ enum typec_orientation orientation;
+ enum usb_role role;
+ bool usb2, usb3, dp, connected;
+ int ret, err;
+
+ usb2 = typec->data_status[1] & USB2_CONNECTION;
+ usb3 = typec->data_status[1] & USB3_2_CONNECTION;
+ dp = typec->data_status[2] & DP_CONNECTION;
+
+ /*
+ * Debug accessories are not supported, so they are reported as
+ * disconnected instead of being passed on to the framework.
+ */
+ if (typec->data_status[2] & DEBUG_ACCESSORY_ATTACHED) {
+ dev_err(typec->dev,
+ "debug accessory mode not supported, try another usb cable\n");
+ connected = false;
+ } else {
+ connected = usb2 || usb3 || dp;
+ }
+
+ if (!connected)
+ orientation = TYPEC_ORIENTATION_NONE;
+ else if (typec->data_status[1] & CONNECTION_ORIENTATION)
+ orientation = TYPEC_ORIENTATION_REVERSE;
+ else
+ orientation = TYPEC_ORIENTATION_NORMAL;
+
+ ret = typec_set_orientation(typec->port, orientation);
+ if (ret)
+ return ret;
+
+ if (!connected || !(usb2 || usb3))
+ role = USB_ROLE_NONE;
+ else if (typec->data_status[1] & USB_DATA_ROLE)
+ role = USB_ROLE_DEVICE;
+ else
+ role = USB_ROLE_HOST;
+
+ if (typec->role != role) {
+ err = 0;
+ if (typec->role_sw) {
+ err = usb_role_switch_set_role(typec->role_sw, role);
+ if (err)
+ dev_err(typec->dev,
+ "typec port(%d): failed to set USB role: %d\n",
+ typec->id, err);
+ }
+
+ /* Only cache the role once the switch accepted it. */
+ if (!err) {
+ typec->role = role;
+ if (role == USB_ROLE_DEVICE)
+ typec_set_data_role(typec->port, TYPEC_DEVICE);
+ else if (role == USB_ROLE_HOST)
+ typec_set_data_role(typec->port, TYPEC_HOST);
+ }
+ }
+
+ if ((typec->data_status[3] & POWER_MASK) == POWER_SOURCE) {
+ typec_set_pwr_role(typec->port, TYPEC_SOURCE);
+ } else if ((typec->data_status[3] & POWER_MASK) == POWER_SINK) {
+ typec_set_pwr_role(typec->port, TYPEC_SINK);
+ } else {
+ dev_dbg(typec->dev, "typec port(%d): unknown power state %d\n",
+ typec->id, typec->data_status[3]);
+ }
+
+ if (!connected) {
+ if (typec->partner_altmode) {
+ typec_altmode_update_active(typec->partner_altmode,
+ false);
+ typec_unregister_altmode(typec->partner_altmode);
+ typec->partner_altmode = NULL;
+ }
+ if (typec->partner) {
+ dev_info(typec->dev, "typec port(%d): unregister partner\n",
+ typec->id);
+ typec_unregister_partner(typec->partner);
+ typec->partner = NULL;
+ }
+ } else {
+ if (!typec->partner) {
+ dev_info(typec->dev, "typec port(%d): register partner\n",
+ typec->id);
+ typec->partner = typec_register_partner(typec->port,
+ &typec->desc);
+ if (IS_ERR(typec->partner)) {
+ dev_err(typec->dev,
+ "typec port(%d): failed to register partner: %ld\n",
+ typec->id, PTR_ERR(typec->partner));
+ typec->partner = NULL;
+ }
+ }
+
+ if (typec->partner_altmode && !dp) {
+ typec_altmode_update_active(typec->partner_altmode,
+ false);
+ typec_unregister_altmode(typec->partner_altmode);
+ typec->partner_altmode = NULL;
+ }
+
+ if (typec->partner && dp && !typec->partner_altmode) {
+ typec->partner_altmode =
+ typec_partner_register_altmode(typec->partner,
+ &cix_pd_dp_partner_altmode);
+ if (IS_ERR(typec->partner_altmode)) {
+ dev_err(typec->dev,
+ "typec port(%d): failed to register DP altmode: %ld\n",
+ typec->id,
+ PTR_ERR(typec->partner_altmode));
+ typec->partner_altmode = NULL;
+ } else {
+ typec_altmode_update_active(typec->partner_altmode,
+ true);
+ }
+ }
+ }
+
+ dp_data->status = 0;
+ typec->state.data = NULL;
+ typec->state.alt = NULL;
+
+ /*
+ * Only pass a DisplayPort state to the mux when the alternate mode
+ * was registered successfully; the state points at it.
+ */
+ if (connected && dp && typec->partner_altmode) {
+ dp_data->status = DP_STATUS_ENABLED;
+ if (typec->data_status[2] & HPD_STATE)
+ dp_data->status |= DP_STATUS_HPD_STATE;
+ if (typec->data_status[2] & HPD_IRQ)
+ dp_data->status |= DP_STATUS_IRQ_HPD;
+
+ typec->state.data = dp_data;
+ typec->state.alt = typec->partner_altmode;
+ typec->state.mode = usb3 ? TYPEC_DP_STATE_D :
+ TYPEC_DP_STATE_E;
+ } else if (connected && (usb2 || usb3)) {
+ typec->state.mode = TYPEC_STATE_USB;
+ } else {
+ typec->state.mode = TYPEC_STATE_SAFE;
+ }
+
+ ret = typec_mux_set(typec->mux, &typec->state);
+ if (ret)
+ return ret;
+
+ if (typec->mode == typec->state.mode &&
+ typec->hpd_status == dp_data->status)
+ return 0;
+
+ if (typec->state.mode == TYPEC_DP_STATE_D)
+ dev_info(typec->dev,
+ "typec port(%d): alt mode(%s) orientation(%s), usb_data_role(%s), power role(%s), hpd_state(%d)\n",
+ typec->id, dp_usb_mode,
+ orientation == TYPEC_ORIENTATION_REVERSE ?
+ orientation_reverse : orientation_normal,
+ role == USB_ROLE_DEVICE ? data_device : data_host,
+ cix_pd_pwr_role_str(typec),
+ (int)(dp_data->status & DP_STATUS_HPD_STATE));
+ else if (typec->state.mode == TYPEC_DP_STATE_E)
+ dev_info(typec->dev,
+ "typec port(%d): alt mode(%s) orientation(%s), power role(%s), hpd_state(%d)\n",
+ typec->id, dp_only_mode,
+ orientation == TYPEC_ORIENTATION_REVERSE ?
+ orientation_reverse : orientation_normal,
+ cix_pd_pwr_role_str(typec),
+ (int)(dp_data->status & DP_STATUS_HPD_STATE));
+ else if (typec->state.mode == TYPEC_STATE_USB)
+ dev_info(typec->dev,
+ "typec port(%d): alt mode(%s) orientation(%s), power role(%s), usb_data_role(%s)\n",
+ typec->id, usb_mode,
+ orientation == TYPEC_ORIENTATION_REVERSE ?
+ orientation_reverse : orientation_normal,
+ cix_pd_pwr_role_str(typec),
+ role == USB_ROLE_DEVICE ? data_device : data_host);
+ else if (typec->state.mode == TYPEC_STATE_SAFE)
+ dev_info(typec->dev, "typec port(%d): disconnect state\n",
+ typec->id);
+
+ typec->mode = typec->state.mode;
+ typec->hpd_status = dp_data->status;
+
+ return 0;
+}
+
+static void cix_pd_typec_init_state(struct cix_pd *typec)
+{
+ typec->state.alt = NULL;
+ typec->state.mode = TYPEC_STATE_SAFE;
+ typec->state.data = NULL;
+ typec->partner = NULL;
+ typec->desc.accessory = TYPEC_ACCESSORY_NONE;
+ /*
+ * The data status registers only report the source/sink state, not
+ * whether USB PD is being used, so the partner is not advertised as
+ * PD capable.
+ */
+ typec->desc.usb_pd = 0;
+}
+
+static int cix_pd_event_handler(struct cix_pd *rts, u8 clear_irq)
+{
+ int ret, err = 0;
+ u8 data_control[3] = {0};
+
+ mutex_lock(&rts->lock);
+ if (clear_irq) {
+ data_control[0] = 2;
+ data_control[1] = I2C_INT_ACK_BIT;
+ data_control[2] = 0;
+
+ ret = cix_pd_block_write(rts, RTS_DATA_CONTROL, data_control, 3);
+ if (ret)
+ goto i2c_err;
+ }
+
+ ret = cix_pd_block_read(rts, RTS_DATA_STATUS, rts->data_status, 6);
+ if (ret)
+ goto i2c_err;
+
+ dev_dbg(rts->dev,
+ "typec port(%d): data status = 0x%x, 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n",
+ rts->id, rts->data_status[0], rts->data_status[1],
+ rts->data_status[2], rts->data_status[3], rts->data_status[4],
+ rts->data_status[5]);
+
+ err = cix_pd_typec_port_update(rts);
+ if (err)
+ dev_err(rts->dev, "typec port(%d): failed to update port: %d\n",
+ rts->id, err);
+
+ if (rts->data_status[2] & HPD_IRQ) {
+ data_control[0] = 2;
+ data_control[1] = 0;
+ data_control[2] = HPD_IRQ_ACK;
+ ret = cix_pd_block_write(rts, RTS_DATA_CONTROL, data_control, 3);
+ if (ret)
+ goto i2c_err;
+ }
+
+ mutex_unlock(&rts->lock);
+ return err;
+
+i2c_err:
+ /*
+ * Report the controller as unresponsive; the IRQ handler turns this
+ * into IRQ_NONE so a stuck line is throttled by the IRQ core.
+ */
+ dev_err(rts->dev, "typec port(%d): i2c transfer error = %d\n",
+ rts->id, ret);
+ mutex_unlock(&rts->lock);
+ return -ESHUTDOWN;
+}
+
+static int cix_pd_typec_parse_port_props(struct typec_capability *cap,
+ struct fwnode_handle *fwnode,
+ struct device *dev)
+{
+ int ret;
+
+ memset(cap, 0, sizeof(*cap));
+ ret = typec_get_fw_cap(cap, fwnode);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "failed to parse connector properties\n");
+
+ cap->fwnode = fwnode_handle_get(fwnode);
+ cap->orientation_aware = true;
+ /* Alternate modes are entered and exited by the controller firmware. */
+ cap->no_mode_control = true;
+
+ return 0;
+}
+
+static int cix_pd_typec_get_handles(struct cix_pd *typec,
+ struct fwnode_handle *fwnode,
+ struct device *dev)
+{
+ int ret;
+
+ typec->mux = fwnode_typec_mux_get(fwnode);
+ if (IS_ERR(typec->mux))
+ return dev_err_probe(dev, PTR_ERR(typec->mux),
+ "failed to get mux\n");
+
+ typec->role_sw = fwnode_usb_role_switch_get(fwnode);
+ if (IS_ERR(typec->role_sw)) {
+ ret = PTR_ERR(typec->role_sw);
+ typec->role_sw = NULL;
+
+ /* The role switch is optional. */
+ if (ret != -ENODEV) {
+ typec_mux_put(typec->mux);
+ typec->mux = NULL;
+ return dev_err_probe(dev, ret,
+ "failed to get USB role switch\n");
+ }
+ }
+
+ if (!typec->mux)
+ dev_dbg(dev, "mux not found\n");
+
+ return 0;
+}
+
+static void cix_pd_unregister_ports(struct cix_pd *typec)
+{
+ if (!IS_ERR_OR_NULL(typec->port)) {
+ typec->state.alt = NULL;
+ typec->state.mode = TYPEC_STATE_SAFE;
+ typec->state.data = NULL;
+ usb_role_switch_set_role(typec->role_sw, USB_ROLE_NONE);
+ typec_set_orientation(typec->port, TYPEC_ORIENTATION_NONE);
+ typec_mux_set(typec->mux, &typec->state);
+
+ if (typec->partner_altmode)
+ typec_altmode_update_active(typec->partner_altmode,
+ false);
+ typec_unregister_altmode(typec->partner_altmode);
+ typec->partner_altmode = NULL;
+ typec_unregister_partner(typec->partner);
+ typec->partner = NULL;
+ typec_unregister_altmode(typec->port_altmode);
+ typec->port_altmode = NULL;
+
+ typec_unregister_port(typec->port);
+ typec->port = NULL;
+ }
+
+ usb_role_switch_put(typec->role_sw);
+ typec->role_sw = NULL;
+ typec_mux_put(typec->mux);
+ typec->mux = NULL;
+ fwnode_handle_put(typec->caps.fwnode);
+ typec->caps.fwnode = NULL;
+}
+
+static irqreturn_t cix_pd_irq_handle(int irq, void *data)
+{
+ struct cix_pd *rts = data;
+
+ if (cix_pd_event_handler(rts, 1) == -ESHUTDOWN)
+ return IRQ_NONE;
+
+ return IRQ_HANDLED;
+}
+
+static int cix_pd_init_ports(struct cix_pd *typec)
+{
+ struct device *dev = typec->dev;
+ struct typec_altmode_desc desc = cix_pd_dp_port_altmode;
+ struct fwnode_handle *fwnode;
+ int ret;
+
+ fwnode = device_get_named_child_node(dev, "connector");
+ if (!fwnode)
+ return dev_err_probe(dev, -ENODEV, "no connector node found\n");
+
+ ret = cix_pd_typec_get_handles(typec, fwnode, dev);
+ if (ret)
+ goto out_put_fwnode;
+
+ ret = cix_pd_typec_parse_port_props(&typec->caps, fwnode, dev);
+ if (ret)
+ goto out_unregister_ports;
+
+ typec->port = typec_register_port(dev, &typec->caps);
+ if (IS_ERR(typec->port)) {
+ ret = PTR_ERR(typec->port);
+ typec->port = NULL;
+ dev_err_probe(dev, ret, "failed to register port\n");
+ goto out_unregister_ports;
+ }
+
+ desc.roles = typec->caps.data;
+ typec->port_altmode = typec_port_register_altmode(typec->port, &desc);
+ if (IS_ERR(typec->port_altmode)) {
+ ret = PTR_ERR(typec->port_altmode);
+ typec->port_altmode = NULL;
+ dev_err_probe(dev, ret, "failed to register DP altmode\n");
+ goto out_unregister_ports;
+ }
+
+ fwnode_handle_put(fwnode);
+
+ return 0;
+
+out_unregister_ports:
+ cix_pd_unregister_ports(typec);
+out_put_fwnode:
+ fwnode_handle_put(fwnode);
+ return ret;
+}
+
+static int cix_pd_probe(struct i2c_client *client)
+{
+ struct cix_pd *rts;
+ int ret;
+
+ rts = devm_kzalloc(&client->dev, sizeof(*rts), GFP_KERNEL);
+ if (!rts)
+ return -ENOMEM;
+
+ rts->dev = &client->dev;
+ rts->irq = client->irq;
+ mutex_init(&rts->lock);
+ mutex_init(&rts->irq_lock);
+ cix_pd_typec_init_state(rts);
+
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C))
+ return dev_err_probe(&client->dev, -ENODEV,
+ "i2c check functionality failed\n");
+
+ rts->regmap = devm_regmap_init_i2c(client, &cix_pd_regmap_config);
+ if (IS_ERR(rts->regmap))
+ return dev_err_probe(&client->dev, PTR_ERR(rts->regmap),
+ "failed to init regmap\n");
+
+ i2c_set_clientdata(client, rts);
+ rts->id = client->addr;
+
+ ret = cix_pd_init_ports(rts);
+ if (ret)
+ return ret;
+
+ ret = cix_pd_event_handler(rts, 0);
+ if (ret) {
+ dev_err(&client->dev, "failed to read initial status: %d\n", ret);
+ if (ret == -ESHUTDOWN)
+ ret = -EIO;
+ goto err_unregister_ports;
+ }
+
+ if (client->irq > 0) {
+ ret = request_threaded_irq(client->irq, NULL, cix_pd_irq_handle,
+ IRQF_SHARED | IRQF_ONESHOT,
+ dev_name(&client->dev), rts);
+ if (ret) {
+ dev_err(&client->dev, "failed to request irq: %d\n", ret);
+ goto err_unregister_ports;
+ }
+ }
+
+ device_set_wakeup_capable(&client->dev, true);
+
+ return 0;
+
+err_unregister_ports:
+ cix_pd_unregister_ports(rts);
+ return ret;
+}
+
+static void cix_pd_remove(struct i2c_client *client)
+{
+ struct cix_pd *rts = i2c_get_clientdata(client);
+
+ if (rts->irq > 0) {
+ if (rts->wake_enabled) {
+ disable_irq_wake(rts->irq);
+ rts->wake_enabled = false;
+ }
+ free_irq(rts->irq, rts);
+ }
+
+ cix_pd_unregister_ports(rts);
+}
+
+static const struct of_device_id cix_pd_of_match[] = {
+ { .compatible = "realtek,rts5453h", },
+ {}
+};
+MODULE_DEVICE_TABLE(of, cix_pd_of_match);
+
+static const struct i2c_device_id cix_pd_id[] = {
+ { .name = "rts5453h" },
+ {}
+};
+MODULE_DEVICE_TABLE(i2c, cix_pd_id);
+
+static int __maybe_unused cix_pd_suspend(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct cix_pd *rts = i2c_get_clientdata(client);
+
+ if (client->irq > 0 && device_may_wakeup(dev)) {
+ mutex_lock(&rts->irq_lock);
+ if (!rts->wake_enabled) {
+ enable_irq_wake(client->irq);
+ rts->wake_enabled = true;
+ }
+ mutex_unlock(&rts->irq_lock);
+ }
+
+ return 0;
+}
+
+static int __maybe_unused cix_pd_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct cix_pd *rts = i2c_get_clientdata(client);
+ int ret;
+
+ mutex_lock(&rts->irq_lock);
+ if (rts->wake_enabled) {
+ disable_irq_wake(client->irq);
+ rts->wake_enabled = false;
+ }
+ mutex_unlock(&rts->irq_lock);
+
+ /* Force the USB role to be reprogrammed after system resume. */
+ rts->role = USB_ROLE_NONE;
+
+ ret = cix_pd_event_handler(rts, 0);
+ if (ret) {
+ dev_err(dev, "failed to refresh status: %d\n", ret);
+ if (ret == -ESHUTDOWN)
+ ret = -EIO;
+ }
+
+ return ret;
+}
+
+static const struct dev_pm_ops cix_pd_pm_ops = {
+ .suspend = pm_sleep_ptr(cix_pd_suspend),
+ .resume = pm_sleep_ptr(cix_pd_resume),
+ .freeze = pm_sleep_ptr(cix_pd_suspend),
+ .thaw = pm_sleep_ptr(cix_pd_resume),
+ .poweroff = pm_sleep_ptr(cix_pd_suspend),
+ .restore = pm_sleep_ptr(cix_pd_resume),
+};
+
+static struct i2c_driver cix_pd_i2c_driver = {
+ .driver = {
+ .name = "rts5453h",
+ .of_match_table = cix_pd_of_match,
+ .pm = pm_ptr(&cix_pd_pm_ops),
+ },
+ .id_table = cix_pd_id,
+ .probe = cix_pd_probe,
+ .remove = cix_pd_remove,
+};
+
+module_i2c_driver(cix_pd_i2c_driver);
+MODULE_DESCRIPTION("CIX Type-C PD controller driver");
+MODULE_AUTHOR("Chao Zeng <Chao.Zeng@cixtech.com>");
+MODULE_AUTHOR("Gary Yang <gary.yang@cixtech.com>");
+MODULE_LICENSE("GPL");
--
2.50.1
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-09 9:24 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-09 9:23 [PATCH v2 0/2] usb: typec: add CIX Type-C PD controller driver Gary Yang
2026-11-09 9:23 ` [PATCH v2 1/2] dt-bindings: usb: add Realtek RTS5453H Type-C port controller Gary Yang
2026-11-09 9:24 ` [PATCH v2 2/2] usb: typec: add CIX Type-C PD controller driver Gary Yang
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®