mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/3] Add Hynetek HUSB320 Type-C controller support
@ 2026-07-20  8:17 Hongyang Zhao
  2026-07-20  8:17 ` [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller Hongyang Zhao
                   ` (2 more replies)
  0 siblings, 3 replies; 7+ messages in thread
From: Hongyang Zhao @ 2026-07-20  8:17 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
  Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao

Add a Devicetree binding and Type-C class driver for the Hynetek
HUSB320 autonomous Configuration Channel controller. The device handles
the Type-C state machine without USB Power Delivery.

The driver supports fixed source and sink configurations, dual-role
operation, Try.SRC and Try.SNK, orientation and Rp current reporting,
platform VBUS and USB role control, and audio and debug accessories.

Testing was performed on a Thundercomm I615 Development Kit based on
the Qualcomm QCS615 SoC. USB device mode enumerated at 5 Gbit/s in
both plug orientations. In USB host mode, a mouse worked in both
orientations, and a USB 3.0 flash drive enumerated at 5 Gbit/s in both
plug orientations.

The datasheet is available at:
https://item.szlcsc.com/datasheet/HUSB320_BA000-QN12R/8528239.html

Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
Hongyang Zhao (3):
      dt-bindings: usb: Add Hynetek HUSB320 Type-C controller
      usb: typec: Add Hynetek HUSB320 Type-C controller driver
      MAINTAINERS: Add HUSB320 Type-C controller entry

 .../devicetree/bindings/usb/hynetek,husb320.yaml   |  171 +++
 MAINTAINERS                                        |    7 +
 drivers/usb/typec/Kconfig                          |   12 +
 drivers/usb/typec/Makefile                         |    1 +
 drivers/usb/typec/husb320.c                        | 1177 ++++++++++++++++++++
 5 files changed, 1368 insertions(+)
---
base-commit: 80574c40598aedbc1751c528e414d7e224bc6313
change-id: 20260718-husb320-6258aa172ec7

Best regards,
-- 
Hongyang Zhao <hongyang.zhao@163.com>


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller
  2026-07-20  8:17 [PATCH 0/3] Add Hynetek HUSB320 Type-C controller support Hongyang Zhao
@ 2026-07-20  8:17 ` Hongyang Zhao
  2026-07-31 22:53   ` Rob Herring (Arm)
  2026-07-20  8:17 ` [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver Hongyang Zhao
  2026-07-20  8:17 ` [PATCH 3/3] MAINTAINERS: Add HUSB320 Type-C controller entry Hongyang Zhao
  2 siblings, 1 reply; 7+ messages in thread
From: Hongyang Zhao @ 2026-07-20  8:17 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
  Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao

The HUSB320 is an autonomous USB Type-C Configuration Channel
controller supporting source, sink, and dual-role port operation.

Document its two I2C addresses, active-low interrupt, optional supplies
and enable GPIO, and managed USB-C connector.

Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
 .../devicetree/bindings/usb/hynetek,husb320.yaml   | 171 +++++++++++++++++++++
 1 file changed, 171 insertions(+)

diff --git a/Documentation/devicetree/bindings/usb/hynetek,husb320.yaml b/Documentation/devicetree/bindings/usb/hynetek,husb320.yaml
new file mode 100644
index 000000000000..b84b6b725e84
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/hynetek,husb320.yaml
@@ -0,0 +1,171 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/usb/hynetek,husb320.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Hynetek HUSB320 USB Type-C controller
+
+maintainers:
+  - Hongyang Zhao <hongyang.zhao@163.com>
+
+description:
+  The HUSB320 is an autonomous USB Type-C Configuration Channel controller.
+  It supports source, sink and dual-role port configurations, including
+  optional Try.SRC and Try.SNK role preferences. The controller reports cable
+  attachment, orientation, advertised current and accessory detection over
+  I2C and an active-low interrupt.
+
+properties:
+  compatible:
+    const: hynetek,husb320
+
+  reg:
+    description:
+      7-bit I2C address selected by the ADDR/ORIENT pin strap. Ground selects
+      0x21 and VDD selects 0x31. These correspond to the 8-bit write addresses
+      0x42 and 0x62 shown in the datasheet.
+    enum: [0x21, 0x31]
+
+  interrupts:
+    maxItems: 1
+
+  wakeup-source: true
+
+  vdd-supply:
+    description: Power supply for the controller
+
+  enable-gpios:
+    description: Active-low controller enable
+    maxItems: 1
+
+  connector:
+    type: object
+    $ref: /schemas/connector/usb-connector.yaml#
+    unevaluatedProperties: false
+    description:
+      USB Type-C connector managed by the controller. A dual-role data port
+      requires a High-Speed graph connection to a USB role-switch provider.
+
+    properties:
+      compatible:
+        const: usb-c-connector
+
+      try-power-role:
+        enum: [source, sink]
+
+    required:
+      - compatible
+      - pd-disable
+      - power-role
+      - typec-power-opmode
+
+    allOf:
+      - if:
+          properties:
+            power-role:
+              enum: [source, dual]
+        then:
+          required:
+            - vbus-supply
+
+      - if:
+          properties:
+            power-role:
+              const: source
+        then:
+          properties:
+            data-role:
+              const: host
+            try-power-role: false
+
+      - if:
+          properties:
+            power-role:
+              const: sink
+        then:
+          properties:
+            data-role:
+              const: device
+            try-power-role: false
+
+      - if:
+          properties:
+            power-role:
+              const: dual
+        then:
+          properties:
+            data-role:
+              const: dual
+
+      - if:
+          required:
+            - data-role
+          properties:
+            data-role:
+              const: dual
+        then:
+          anyOf:
+            - required:
+                - port
+            - required:
+                - ports
+
+required:
+  - compatible
+  - reg
+  - interrupts
+  - connector
+
+additionalProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        typec@31 {
+            compatible = "hynetek,husb320";
+            reg = <0x31>;
+            vdd-supply = <&vreg_3v3>;
+            enable-gpios = <&gpio 54 GPIO_ACTIVE_LOW>;
+            interrupts-extended = <&gpio 55 IRQ_TYPE_LEVEL_LOW>;
+            wakeup-source;
+
+            connector {
+                compatible = "usb-c-connector";
+                label = "USB-C";
+                vbus-supply = <&usb_vbus>;
+                data-role = "dual";
+                power-role = "dual";
+                try-power-role = "sink";
+                typec-power-opmode = "default";
+                pd-disable;
+
+                ports {
+                    #address-cells = <1>;
+                    #size-cells = <0>;
+
+                    port@0 {
+                        reg = <0>;
+
+                        endpoint {
+                            remote-endpoint = <&usb_role>;
+                        };
+                    };
+
+                    port@1 {
+                        reg = <1>;
+
+                        endpoint {
+                            remote-endpoint = <&usb_ss>;
+                        };
+                    };
+                };
+            };
+        };
+    };

-- 
2.43.0


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver
  2026-07-20  8:17 [PATCH 0/3] Add Hynetek HUSB320 Type-C controller support Hongyang Zhao
  2026-07-20  8:17 ` [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller Hongyang Zhao
@ 2026-07-20  8:17 ` Hongyang Zhao
  2026-07-21 10:27   ` Heikki Krogerus
  2026-08-24 10:35   ` Meng Li
  2026-07-20  8:17 ` [PATCH 3/3] MAINTAINERS: Add HUSB320 Type-C controller entry Hongyang Zhao
  2 siblings, 2 replies; 7+ messages in thread
From: Hongyang Zhao @ 2026-07-20  8:17 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
  Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao

The HUSB320 is an autonomous Configuration Channel controller which
supports fixed source or sink operation and dual-role ports without USB
Power Delivery.

Add a Type-C class driver for attach and detach detection, orientation
and Rp current reporting, platform VBUS control, USB role switching,
Try.SRC and Try.SNK, and audio and debug accessories.

Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
 drivers/usb/typec/Kconfig   |   12 +
 drivers/usb/typec/Makefile  |    1 +
 drivers/usb/typec/husb320.c | 1177 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1190 insertions(+)

diff --git a/drivers/usb/typec/Kconfig b/drivers/usb/typec/Kconfig
index 2f80c2792dbd..22ef8bacacf7 100644
--- a/drivers/usb/typec/Kconfig
+++ b/drivers/usb/typec/Kconfig
@@ -88,6 +88,18 @@ config TYPEC_HD3SS3220
 	  If you choose to build this driver as a dynamically linked module, the
 	  module will be called hd3ss3220.ko.
 
+config TYPEC_HUSB320
+	tristate "Hynetek HUSB320 Type-C controller driver"
+	depends on I2C
+	depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
+	select REGMAP_I2C
+	help
+	  Say Y or M here if your system has a Hynetek HUSB320 autonomous
+	  USB Type-C controller.
+
+	  If you choose to build this driver as a dynamically linked module, the
+	  module will be called husb320.ko.
+
 config TYPEC_STUSB160X
 	tristate "STMicroelectronics STUSB160x Type-C controller driver"
 	depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
index 8a6a1c663eb6..1b61c34d6e66 100644
--- a/drivers/usb/typec/Makefile
+++ b/drivers/usb/typec/Makefile
@@ -8,6 +8,7 @@ obj-$(CONFIG_TYPEC_UCSI)	+= ucsi/
 obj-$(CONFIG_TYPEC_TPS6598X)	+= tipd/
 obj-$(CONFIG_TYPEC_ANX7411)	+= anx7411.o
 obj-$(CONFIG_TYPEC_HD3SS3220)	+= hd3ss3220.o
+obj-$(CONFIG_TYPEC_HUSB320)	+= husb320.o
 obj-$(CONFIG_TYPEC_STUSB160X) 	+= stusb160x.o
 obj-$(CONFIG_TYPEC_RT1719)	+= rt1719.o
 obj-$(CONFIG_TYPEC_WUSB3801)	+= wusb3801.o
diff --git a/drivers/usb/typec/husb320.c b/drivers/usb/typec/husb320.c
new file mode 100644
index 000000000000..fd9f949d9b52
--- /dev/null
+++ b/drivers/usb/typec/husb320.c
@@ -0,0 +1,1177 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hynetek HUSB320 USB Type-C controller driver
+ *
+ * Copyright (C) 2026 Hongyang Zhao <hongyang.zhao@163.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/delay.h>
+#include <linux/gpio/consumer.h>
+#include <linux/i2c.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/usb/role.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/typec_altmode.h>
+
+#define HUSB320_REG_DEVICE_ID		0x01
+#define HUSB320_REG_DEVICE_TYPE		0x02
+#define HUSB320_REG_PORT_ROLE		0x03
+#define HUSB320_REG_CONTROL		0x04
+#define HUSB320_REG_CONTROL1		0x05
+#define HUSB320_REG_MANUAL		0x09
+#define HUSB320_REG_RESET		0x0a
+#define HUSB320_REG_MASK			0x0e
+#define HUSB320_REG_MASK1		0x0f
+#define HUSB320_REG_STATUS		0x11
+#define HUSB320_REG_TYPE			0x13
+#define HUSB320_REG_INTERRUPT		0x14
+#define HUSB320_REG_INTERRUPT1		0x15
+
+#define HUSB320_DEVICE_ID_VERSION	GENMASK(7, 4)
+#define HUSB320_DEVICE_ID_REVISION	GENMASK(3, 0)
+#define HUSB320_DEVICE_ID_VERSION_1	1
+#define HUSB320_DEVICE_TYPE		0x03
+
+#define HUSB320_PORT_ROLE_TRY		GENMASK(5, 4)
+#define HUSB320_PORT_ROLE_MODE		GENMASK(2, 0)
+#define HUSB320_PORT_ROLE_SOURCE		BIT(0)
+#define HUSB320_PORT_ROLE_SINK		BIT(1)
+#define HUSB320_PORT_ROLE_DRP		BIT(2)
+#define HUSB320_TRY_NONE			0
+#define HUSB320_TRY_SINK			1
+#define HUSB320_TRY_SOURCE		2
+
+#define HUSB320_CONTROL_HOST_CURRENT	GENMASK(2, 1)
+#define HUSB320_CONTROL_INT_MASK		BIT(0)
+#define HUSB320_HOST_CURRENT_DEFAULT	1
+#define HUSB320_HOST_CURRENT_1_5A	2
+#define HUSB320_HOST_CURRENT_3_0A	3
+
+#define HUSB320_CONTROL1_ENABLE		BIT(3)
+
+#define HUSB320_MANUAL_ERROR_RECOVERY	BIT(0)
+
+#define HUSB320_RESET_SW			BIT(0)
+
+/* The datasheet defines the fault mask, but reserves the matching IRQ bit. */
+#define HUSB320_MASK_FAULT		BIT(5)
+#define HUSB320_MASK_AUTOSINK		BIT(3)
+#define HUSB320_MASK1_FORCE		GENMASK(2, 1)
+
+#define HUSB320_STATUS_ORIENTATION	GENMASK(5, 4)
+#define HUSB320_STATUS_ORIENTATION_CC1	1
+#define HUSB320_STATUS_ORIENTATION_CC2	2
+#define HUSB320_STATUS_CURRENT		GENMASK(2, 1)
+#define HUSB320_STATUS_ATTACHED		BIT(0)
+
+#define HUSB320_TYPE_DEBUG_SOURCE	BIT(6)
+#define HUSB320_TYPE_DEBUG_SINK		BIT(5)
+#define HUSB320_TYPE_SINK		BIT(4)
+#define HUSB320_TYPE_SOURCE		BIT(3)
+#define HUSB320_TYPE_AUDIO_VBUS		BIT(1)
+#define HUSB320_TYPE_AUDIO		BIT(0)
+
+#define HUSB320_INT_ORIENTATION		BIT(6)
+#define HUSB320_INT_VBUS_CHANGE		BIT(4)
+#define HUSB320_INT_AUTOSINK		BIT(3)
+#define HUSB320_INT_CURRENT_CHANGE	BIT(2)
+#define HUSB320_INT_DETACH		BIT(1)
+#define HUSB320_INT_ATTACH		BIT(0)
+#define HUSB320_INT_VALID		(HUSB320_INT_ORIENTATION | \
+					 HUSB320_INT_VBUS_CHANGE | \
+					 HUSB320_INT_AUTOSINK | \
+					 HUSB320_INT_CURRENT_CHANGE | \
+					 HUSB320_INT_DETACH | \
+					 HUSB320_INT_ATTACH)
+#define HUSB320_INT1_VALID		GENMASK(2, 1)
+
+enum husb320_state {
+	HUSB320_STATE_UNATTACHED,
+	HUSB320_STATE_SOURCE,
+	HUSB320_STATE_SINK,
+	HUSB320_STATE_DEBUG_SOURCE,
+	HUSB320_STATE_DEBUG_SINK,
+	HUSB320_STATE_AUDIO,
+};
+
+struct husb320 {
+	struct device *dev;
+	struct regmap *regmap;
+	struct gpio_desc *enable_gpio;
+	struct regulator *vbus;
+	struct usb_role_switch *role_sw;
+	struct typec_capability cap;
+	struct typec_port *port;
+	struct typec_partner *partner;
+	struct fwnode_handle *connector;
+	/* Serializes controller access and Type-C state updates. */
+	struct mutex lock;
+	enum typec_port_type port_type;
+	enum typec_role preferred_role;
+	enum typec_pwr_opmode source_opmode;
+	enum husb320_state state;
+	enum usb_role usb_role;
+	int connector_mode;
+	bool has_data;
+	bool mode_valid;
+	bool shutting_down;
+	bool state_valid;
+	bool vbus_on;
+};
+
+static const struct regmap_config husb320_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+	.max_register = HUSB320_REG_INTERRUPT1,
+};
+
+static enum typec_role husb320_default_power_role(struct husb320 *husb)
+{
+	switch (husb->port_type) {
+	case TYPEC_PORT_SRC:
+		return TYPEC_SOURCE;
+	case TYPEC_PORT_SNK:
+		return TYPEC_SINK;
+	case TYPEC_PORT_DRP:
+	default:
+		if (husb->preferred_role == TYPEC_SOURCE)
+			return TYPEC_SOURCE;
+
+		return TYPEC_SINK;
+	}
+}
+
+static enum typec_data_role husb320_default_data_role(struct husb320 *husb)
+{
+	switch (husb->cap.data) {
+	case TYPEC_PORT_DFP:
+		return TYPEC_HOST;
+	case TYPEC_PORT_UFP:
+		return TYPEC_DEVICE;
+	case TYPEC_PORT_DRD:
+	default:
+		return husb320_default_power_role(husb) == TYPEC_SOURCE ?
+			TYPEC_HOST : TYPEC_DEVICE;
+	}
+}
+
+static void husb320_set_unattached_roles(struct husb320 *husb)
+{
+	enum typec_role power_role = husb320_default_power_role(husb);
+
+	typec_set_pwr_role(husb->port, power_role);
+	if (husb->has_data)
+		typec_set_data_role(husb->port,
+				    husb320_default_data_role(husb));
+	typec_set_pwr_opmode(husb->port,
+			     power_role == TYPEC_SOURCE ?
+			     husb->source_opmode : TYPEC_PWR_MODE_USB);
+}
+
+static int husb320_set_vbus(struct husb320 *husb, bool enable)
+{
+	int ret;
+
+	if (!husb->vbus || husb->vbus_on == enable)
+		return 0;
+
+	if (enable)
+		ret = regulator_enable(husb->vbus);
+	else
+		ret = regulator_disable(husb->vbus);
+	if (ret)
+		return ret;
+
+	husb->vbus_on = enable;
+
+	return 0;
+}
+
+static int husb320_set_mode(struct husb320 *husb, int mode)
+{
+	int ret;
+
+	if (husb->mode_valid && husb->connector_mode == mode)
+		return 0;
+
+	ret = typec_set_mode(husb->port, mode);
+	if (ret)
+		return ret;
+
+	husb->connector_mode = mode;
+	husb->mode_valid = true;
+
+	return 0;
+}
+
+static void husb320_unregister_partner(struct husb320 *husb)
+{
+	if (!husb->partner)
+		return;
+
+	typec_unregister_partner(husb->partner);
+	husb->partner = NULL;
+}
+
+static int husb320_register_partner(struct husb320 *husb,
+				    enum typec_accessory accessory)
+{
+	struct typec_partner_desc desc = {
+		.accessory = accessory,
+		.usb_pd = false,
+	};
+
+	if (husb->partner)
+		return 0;
+
+	husb->partner = typec_register_partner(husb->port, &desc);
+	if (IS_ERR(husb->partner)) {
+		int ret = PTR_ERR(husb->partner);
+
+		husb->partner = NULL;
+		return ret;
+	}
+
+	return 0;
+}
+
+static int husb320_disconnect(struct husb320 *husb)
+{
+	int err = 0;
+	int ret;
+
+	ret = husb320_set_vbus(husb, false);
+	if (ret)
+		err = ret;
+
+	ret = usb_role_switch_set_role(husb->role_sw, USB_ROLE_NONE);
+	if (ret) {
+		if (!err)
+			err = ret;
+	} else {
+		husb->usb_role = USB_ROLE_NONE;
+	}
+
+	ret = husb320_set_mode(husb, TYPEC_STATE_SAFE);
+	if (ret && !err)
+		err = ret;
+
+	husb320_unregister_partner(husb);
+
+	if (typec_get_orientation(husb->port) != TYPEC_ORIENTATION_NONE) {
+		ret = typec_set_orientation(husb->port, TYPEC_ORIENTATION_NONE);
+		if (ret && !err)
+			err = ret;
+	}
+
+	husb320_set_unattached_roles(husb);
+
+	husb->state = HUSB320_STATE_UNATTACHED;
+	husb->state_valid = !err;
+
+	return err;
+}
+
+static int husb320_hw_disable(struct husb320 *husb);
+
+static void husb320_error_recovery(struct husb320 *husb)
+{
+	int cleanup_ret;
+	int disable_ret;
+	int recovery_ret;
+
+	cleanup_ret = husb320_disconnect(husb);
+	recovery_ret = regmap_write(husb->regmap, HUSB320_REG_MANUAL,
+				    HUSB320_MANUAL_ERROR_RECOVERY);
+	if (cleanup_ret)
+		dev_err_ratelimited(husb->dev,
+				    "failed to clean up port: %d\n", cleanup_ret);
+	if (recovery_ret)
+		dev_err_ratelimited(husb->dev,
+				    "failed to start error recovery: %d\n",
+				    recovery_ret);
+
+	if (!cleanup_ret && !recovery_ret)
+		return;
+
+	husb->shutting_down = true;
+	disable_ret = husb320_hw_disable(husb);
+	if (disable_ret)
+		dev_err_ratelimited(husb->dev,
+				    "failed to disable controller: %d\n",
+				    disable_ret);
+}
+
+static enum typec_pwr_opmode husb320_sink_opmode(unsigned int status)
+{
+	switch (FIELD_GET(HUSB320_STATUS_CURRENT, status)) {
+	case 2:
+		return TYPEC_PWR_MODE_1_5A;
+	case 3:
+		return TYPEC_PWR_MODE_3_0A;
+	case 0:
+	case 1:
+	default:
+		return TYPEC_PWR_MODE_USB;
+	}
+}
+
+static enum typec_role husb320_state_power_role(enum husb320_state state)
+{
+	switch (state) {
+	case HUSB320_STATE_SOURCE:
+	case HUSB320_STATE_DEBUG_SOURCE:
+	case HUSB320_STATE_AUDIO:
+		return TYPEC_SOURCE;
+	case HUSB320_STATE_SINK:
+	case HUSB320_STATE_DEBUG_SINK:
+	case HUSB320_STATE_UNATTACHED:
+	default:
+		return TYPEC_SINK;
+	}
+}
+
+static enum typec_accessory husb320_state_accessory(enum husb320_state state)
+{
+	switch (state) {
+	case HUSB320_STATE_DEBUG_SOURCE:
+	case HUSB320_STATE_DEBUG_SINK:
+		return TYPEC_ACCESSORY_DEBUG;
+	case HUSB320_STATE_AUDIO:
+		return TYPEC_ACCESSORY_AUDIO;
+	default:
+		return TYPEC_ACCESSORY_NONE;
+	}
+}
+
+static enum usb_role husb320_state_usb_role(struct husb320 *husb,
+					    enum husb320_state state)
+{
+	if (!husb->has_data)
+		return USB_ROLE_NONE;
+
+	if ((state == HUSB320_STATE_SOURCE ||
+	     state == HUSB320_STATE_DEBUG_SOURCE) &&
+	    husb->cap.data != TYPEC_PORT_UFP)
+		return USB_ROLE_HOST;
+
+	if ((state == HUSB320_STATE_SINK ||
+	     state == HUSB320_STATE_DEBUG_SINK) &&
+	    husb->cap.data != TYPEC_PORT_DFP)
+		return USB_ROLE_DEVICE;
+
+	return USB_ROLE_NONE;
+}
+
+static int husb320_state_mode(enum husb320_state state)
+{
+	switch (state) {
+	case HUSB320_STATE_DEBUG_SOURCE:
+	case HUSB320_STATE_DEBUG_SINK:
+		return TYPEC_MODE_DEBUG;
+	case HUSB320_STATE_AUDIO:
+		return TYPEC_MODE_AUDIO;
+	case HUSB320_STATE_SOURCE:
+	case HUSB320_STATE_SINK:
+		return TYPEC_STATE_USB;
+	case HUSB320_STATE_UNATTACHED:
+	default:
+		return TYPEC_STATE_SAFE;
+	}
+}
+
+static int husb320_apply_state(struct husb320 *husb,
+			       enum husb320_state state,
+			       enum typec_orientation orientation,
+			       unsigned int status)
+{
+	enum typec_accessory accessory = husb320_state_accessory(state);
+	enum usb_role usb_role = husb320_state_usb_role(husb, state);
+	enum typec_pwr_opmode opmode;
+	enum typec_role power_role;
+	int ret;
+
+	if (state == HUSB320_STATE_UNATTACHED && husb->state_valid &&
+	    husb->state == HUSB320_STATE_UNATTACHED && !husb->partner &&
+	    !husb->vbus_on &&
+	    husb->usb_role == USB_ROLE_NONE)
+		return 0;
+
+	if (state == HUSB320_STATE_UNATTACHED)
+		return husb320_disconnect(husb);
+
+	if (state != husb->state &&
+	    (!husb->state_valid ||
+	     husb->state != HUSB320_STATE_UNATTACHED)) {
+		ret = husb320_disconnect(husb);
+		if (ret)
+			return ret;
+	}
+
+	if (typec_get_orientation(husb->port) != orientation) {
+		ret = typec_set_orientation(husb->port, orientation);
+		if (ret)
+			return ret;
+	}
+
+	/* Accessory states do not request the platform VBUS supply. */
+	ret = husb320_set_vbus(husb, state == HUSB320_STATE_SOURCE);
+	if (ret)
+		return ret;
+
+	if (usb_role != husb->usb_role) {
+		ret = usb_role_switch_set_role(husb->role_sw, usb_role);
+		if (ret)
+			return ret;
+		husb->usb_role = usb_role;
+	}
+
+	ret = husb320_set_mode(husb, husb320_state_mode(state));
+	if (ret)
+		return ret;
+
+	power_role = husb320_state_power_role(state);
+	opmode = state == HUSB320_STATE_SINK ?
+		husb320_sink_opmode(status) :
+		(power_role == TYPEC_SOURCE ? husb->source_opmode :
+		 TYPEC_PWR_MODE_USB);
+
+	if (usb_role == USB_ROLE_HOST)
+		typec_set_data_role(husb->port, TYPEC_HOST);
+	else if (usb_role == USB_ROLE_DEVICE)
+		typec_set_data_role(husb->port, TYPEC_DEVICE);
+	typec_set_pwr_role(husb->port, power_role);
+	typec_set_pwr_opmode(husb->port, opmode);
+
+	ret = husb320_register_partner(husb, accessory);
+	if (ret) {
+		husb->state_valid = false;
+		dev_err_ratelimited(husb->dev,
+				    "failed to register partner: %d\n", ret);
+		return ret;
+	}
+
+	husb->state = state;
+	husb->state_valid = true;
+
+	return 0;
+}
+
+static enum typec_orientation husb320_get_orientation(unsigned int status)
+{
+	switch (FIELD_GET(HUSB320_STATUS_ORIENTATION, status)) {
+	case HUSB320_STATUS_ORIENTATION_CC1:
+		return TYPEC_ORIENTATION_NORMAL;
+	case HUSB320_STATUS_ORIENTATION_CC2:
+		return TYPEC_ORIENTATION_REVERSE;
+	default:
+		return TYPEC_ORIENTATION_NONE;
+	}
+}
+
+static enum husb320_state husb320_get_state(unsigned int status,
+					    unsigned int type)
+{
+	if (!(status & HUSB320_STATUS_ATTACHED))
+		return HUSB320_STATE_UNATTACHED;
+
+	if (type & HUSB320_TYPE_DEBUG_SOURCE)
+		return HUSB320_STATE_DEBUG_SOURCE;
+	if (type & HUSB320_TYPE_DEBUG_SINK)
+		return HUSB320_STATE_DEBUG_SINK;
+	if (type & HUSB320_TYPE_SINK)
+		return HUSB320_STATE_SINK;
+	if (type & HUSB320_TYPE_SOURCE)
+		return HUSB320_STATE_SOURCE;
+	if (type & (HUSB320_TYPE_AUDIO_VBUS | HUSB320_TYPE_AUDIO))
+		return HUSB320_STATE_AUDIO;
+
+	return HUSB320_STATE_UNATTACHED;
+}
+
+static int husb320_update_state(struct husb320 *husb)
+{
+	enum typec_orientation orientation;
+	enum husb320_state state;
+	unsigned int status;
+	unsigned int type;
+	int ret;
+
+	ret = regmap_read(husb->regmap, HUSB320_REG_STATUS, &status);
+	if (ret)
+		return ret;
+
+	ret = regmap_read(husb->regmap, HUSB320_REG_TYPE, &type);
+	if (ret)
+		return ret;
+
+	state = husb320_get_state(status, type);
+	orientation = state == HUSB320_STATE_UNATTACHED ?
+		TYPEC_ORIENTATION_NONE : husb320_get_orientation(status);
+
+	ret = husb320_apply_state(husb, state, orientation, status);
+	if (!ret)
+		dev_dbg(husb->dev, "status=%#x type=%#x state=%u\n",
+			status, type, state);
+
+	return ret;
+}
+
+static irqreturn_t husb320_irq_thread(int irq, void *data)
+{
+	struct husb320 *husb = data;
+	unsigned int interrupt;
+	unsigned int interrupt1;
+	int disable_ret;
+	int ret;
+
+	mutex_lock(&husb->lock);
+	if (husb->shutting_down) {
+		disable_irq_nosync(irq);
+		mutex_unlock(&husb->lock);
+		return IRQ_HANDLED;
+	}
+
+	ret = regmap_read(husb->regmap, HUSB320_REG_INTERRUPT, &interrupt);
+	if (ret)
+		goto err;
+
+	ret = regmap_read(husb->regmap, HUSB320_REG_INTERRUPT1, &interrupt1);
+	if (ret)
+		goto err;
+
+	interrupt &= HUSB320_INT_VALID;
+	interrupt1 &= HUSB320_INT1_VALID;
+	if (!interrupt && !interrupt1) {
+		mutex_unlock(&husb->lock);
+		return IRQ_NONE;
+	}
+
+	if (interrupt) {
+		ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT,
+				   interrupt);
+		if (ret)
+			goto err;
+	}
+
+	if (interrupt1) {
+		ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT1,
+				   interrupt1);
+		if (ret)
+			goto err;
+	}
+
+	if (interrupt & HUSB320_INT_DETACH) {
+		ret = husb320_disconnect(husb);
+		if (ret)
+			dev_err_ratelimited(husb->dev,
+					    "failed to disconnect port: %d\n",
+					    ret);
+	}
+
+	ret = husb320_update_state(husb);
+	if (ret) {
+		dev_err_ratelimited(husb->dev,
+				    "failed to update port state: %d\n", ret);
+		husb320_error_recovery(husb);
+	}
+
+	mutex_unlock(&husb->lock);
+
+	return IRQ_HANDLED;
+
+err:
+	dev_err_ratelimited(husb->dev, "failed to service interrupt: %d\n",
+			    ret);
+	husb->shutting_down = true;
+	husb320_error_recovery(husb);
+	disable_ret = husb320_hw_disable(husb);
+	if (disable_ret)
+		dev_err_ratelimited(husb->dev,
+				    "failed to disable controller: %d\n",
+				    disable_ret);
+	disable_irq_nosync(irq);
+	mutex_unlock(&husb->lock);
+
+	return IRQ_HANDLED;
+}
+
+static unsigned int husb320_port_role(enum typec_port_type type)
+{
+	switch (type) {
+	case TYPEC_PORT_SRC:
+		return HUSB320_PORT_ROLE_SOURCE;
+	case TYPEC_PORT_SNK:
+		return HUSB320_PORT_ROLE_SINK;
+	case TYPEC_PORT_DRP:
+	default:
+		return HUSB320_PORT_ROLE_DRP;
+	}
+}
+
+static unsigned int husb320_try_role(int role)
+{
+	switch (role) {
+	case TYPEC_SINK:
+		return HUSB320_TRY_SINK;
+	case TYPEC_SOURCE:
+		return HUSB320_TRY_SOURCE;
+	case TYPEC_NO_PREFERRED_ROLE:
+	default:
+		return HUSB320_TRY_NONE;
+	}
+}
+
+static int husb320_port_type_set(struct typec_port *port,
+				 enum typec_port_type type)
+{
+	struct husb320 *husb = typec_get_drvdata(port);
+	unsigned int port_role;
+	int ret;
+
+	mutex_lock(&husb->lock);
+	if (husb->shutting_down) {
+		ret = -ESHUTDOWN;
+		goto out_unlock;
+	}
+
+	port_role = husb320_port_role(type);
+	if (type == TYPEC_PORT_DRP)
+		port_role |= FIELD_PREP(HUSB320_PORT_ROLE_TRY,
+					 husb320_try_role(husb->preferred_role));
+
+	ret = husb320_disconnect(husb);
+	if (ret) {
+		husb320_error_recovery(husb);
+		goto out_unlock;
+	}
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+				 HUSB320_PORT_ROLE_TRY |
+				 HUSB320_PORT_ROLE_MODE,
+				 port_role);
+	if (ret) {
+		husb320_error_recovery(husb);
+		goto out_unlock;
+	}
+
+	husb->port_type = type;
+	husb320_set_unattached_roles(husb);
+out_unlock:
+	mutex_unlock(&husb->lock);
+
+	return ret;
+}
+
+static int husb320_try_role_set(struct typec_port *port, int role)
+{
+	struct husb320 *husb = typec_get_drvdata(port);
+	int ret;
+
+	mutex_lock(&husb->lock);
+	if (husb->shutting_down) {
+		ret = -ESHUTDOWN;
+		goto out_unlock;
+	}
+
+	if (husb->port_type == TYPEC_PORT_DRP)
+		ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+					 HUSB320_PORT_ROLE_TRY,
+					 FIELD_PREP(HUSB320_PORT_ROLE_TRY,
+						    husb320_try_role(role)));
+	else
+		ret = 0;
+	if (!ret)
+		husb->preferred_role = role;
+out_unlock:
+	mutex_unlock(&husb->lock);
+
+	return ret;
+}
+
+static const struct typec_operations husb320_typec_ops = {
+	.port_type_set = husb320_port_type_set,
+	.try_role = husb320_try_role_set,
+};
+
+static unsigned int husb320_host_current(enum typec_pwr_opmode opmode)
+{
+	switch (opmode) {
+	case TYPEC_PWR_MODE_1_5A:
+		return HUSB320_HOST_CURRENT_1_5A;
+	case TYPEC_PWR_MODE_3_0A:
+		return HUSB320_HOST_CURRENT_3_0A;
+	case TYPEC_PWR_MODE_USB:
+	default:
+		return HUSB320_HOST_CURRENT_DEFAULT;
+	}
+}
+
+static int husb320_hw_init(struct husb320 *husb)
+{
+	unsigned int port_role;
+	unsigned int control;
+	int ret;
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_RESET,
+				 HUSB320_RESET_SW, HUSB320_RESET_SW);
+	if (ret)
+		return ret;
+
+	/* Wait for stable status and I2C access after the soft reset. */
+	msleep(100);
+
+	control = HUSB320_CONTROL_INT_MASK |
+		  FIELD_PREP(HUSB320_CONTROL_HOST_CURRENT,
+			     husb320_host_current(husb->source_opmode));
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+				 HUSB320_CONTROL_INT_MASK |
+				 HUSB320_CONTROL_HOST_CURRENT,
+				 control);
+	if (ret)
+		return ret;
+
+	port_role = husb320_port_role(husb->port_type);
+	if (husb->port_type == TYPEC_PORT_DRP)
+		port_role |= FIELD_PREP(HUSB320_PORT_ROLE_TRY,
+					 husb320_try_role(husb->preferred_role));
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+				 HUSB320_PORT_ROLE_TRY |
+				 HUSB320_PORT_ROLE_MODE,
+				 port_role);
+	if (ret)
+		return ret;
+
+	ret = regmap_write(husb->regmap, HUSB320_REG_MASK,
+			   HUSB320_MASK_FAULT | HUSB320_MASK_AUTOSINK);
+	if (ret)
+		return ret;
+
+	ret = regmap_write(husb->regmap, HUSB320_REG_MASK1,
+			   HUSB320_MASK1_FORCE);
+	if (ret)
+		return ret;
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
+				 HUSB320_CONTROL1_ENABLE,
+				 HUSB320_CONTROL1_ENABLE);
+	if (ret)
+		return ret;
+
+	ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT,
+			   HUSB320_INT_VALID);
+	if (ret)
+		return ret;
+
+	return regmap_write(husb->regmap, HUSB320_REG_INTERRUPT1,
+			    HUSB320_INT1_VALID);
+}
+
+static int husb320_hw_disable(struct husb320 *husb)
+{
+	int err = 0;
+	int ret;
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+				 HUSB320_CONTROL_INT_MASK,
+				 HUSB320_CONTROL_INT_MASK);
+	if (ret)
+		err = ret;
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
+				 HUSB320_CONTROL1_ENABLE, 0);
+	if (ret && !err)
+		err = ret;
+
+	return err;
+}
+
+static int husb320_set_sink_mode(struct husb320 *husb)
+{
+	int ret;
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+				 HUSB320_CONTROL_INT_MASK,
+				 HUSB320_CONTROL_INT_MASK);
+	if (ret)
+		return ret;
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
+				 HUSB320_PORT_ROLE_TRY |
+				 HUSB320_PORT_ROLE_MODE,
+				 HUSB320_PORT_ROLE_SINK);
+	if (ret)
+		return ret;
+
+	return regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
+				  HUSB320_CONTROL1_ENABLE,
+				  HUSB320_CONTROL1_ENABLE);
+}
+
+static int husb320_parse_connector(struct husb320 *husb)
+{
+	const char *opmode;
+	int ret;
+
+	husb->connector = device_get_named_child_node(husb->dev, "connector");
+	if (!husb->connector)
+		return -ENODEV;
+
+	/* The connector is parsed as a fwnode and is not populated as a device. */
+	fw_devlink_purge_absent_suppliers(husb->connector);
+
+	ret = typec_get_fw_cap(&husb->cap, husb->connector);
+	if (ret)
+		return ret;
+
+	ret = fwnode_property_read_string(husb->connector,
+					  "typec-power-opmode", &opmode);
+	if (ret)
+		return ret;
+
+	ret = typec_find_pwr_opmode(opmode);
+	if (ret < 0)
+		return ret;
+	if (ret == TYPEC_PWR_MODE_PD)
+		return -EINVAL;
+
+	husb->source_opmode = ret;
+	husb->has_data = fwnode_property_present(husb->connector, "data-role");
+	if (husb->has_data &&
+	    ((husb->cap.type == TYPEC_PORT_SRC &&
+	      husb->cap.data != TYPEC_PORT_DFP) ||
+	     (husb->cap.type == TYPEC_PORT_SNK &&
+	      husb->cap.data != TYPEC_PORT_UFP) ||
+	     (husb->cap.type == TYPEC_PORT_DRP &&
+	      husb->cap.data != TYPEC_PORT_DRD))) {
+		return dev_err_probe(husb->dev, -EINVAL,
+				     "power and data roles do not match\n");
+	} else if (!husb->has_data) {
+		if (husb->cap.type == TYPEC_PORT_SNK)
+			husb->cap.data = TYPEC_PORT_UFP;
+		else if (husb->cap.type == TYPEC_PORT_DRP)
+			husb->cap.data = TYPEC_PORT_DRD;
+	}
+
+	husb->cap.revision = USB_TYPEC_REV_2_0;
+	husb->cap.accessory[0] = TYPEC_ACCESSORY_AUDIO;
+	husb->cap.accessory[1] = TYPEC_ACCESSORY_DEBUG;
+	husb->cap.orientation_aware = true;
+	husb->cap.driver_data = husb;
+	husb->cap.ops = &husb320_typec_ops;
+	husb->port_type = husb->cap.type;
+	husb->preferred_role = husb->cap.prefer_role;
+
+	return 0;
+}
+
+static int husb320_get_role_switch(struct husb320 *husb)
+{
+	int ret;
+
+	if (!husb->has_data)
+		return 0;
+
+	husb->role_sw = fwnode_usb_role_switch_get(husb->connector);
+	if (IS_ERR(husb->role_sw)) {
+		ret = PTR_ERR(husb->role_sw);
+		husb->role_sw = NULL;
+		if (ret == -ENODEV && husb->cap.data != TYPEC_PORT_DRD)
+			return 0;
+
+		return dev_err_probe(husb->dev, ret,
+				     "failed to get USB role switch\n");
+	}
+
+	if (!husb->role_sw && husb->cap.data == TYPEC_PORT_DRD)
+		return dev_err_probe(husb->dev, -ENODEV,
+				     "USB data role has no role switch\n");
+
+	return 0;
+}
+
+static int husb320_get_vbus(struct husb320 *husb)
+{
+	struct device_node *node = to_of_node(husb->connector);
+
+	if (node)
+		husb->vbus = devm_of_regulator_get_optional(husb->dev, node, "vbus");
+	else
+		husb->vbus = devm_regulator_get_optional(husb->dev, "vbus");
+
+	if (IS_ERR(husb->vbus)) {
+		int ret = PTR_ERR(husb->vbus);
+
+		husb->vbus = NULL;
+		if (ret != -ENODEV)
+			return dev_err_probe(husb->dev, ret,
+					     "failed to get VBUS supply\n");
+	}
+
+	if (husb->cap.type != TYPEC_PORT_SNK && !husb->vbus)
+		return dev_err_probe(husb->dev, -ENODEV,
+				     "source-capable port requires VBUS supply\n");
+
+	return 0;
+}
+
+static void husb320_disable(void *data)
+{
+	struct husb320 *husb = data;
+
+	gpiod_set_value_cansleep(husb->enable_gpio, 0);
+}
+
+static int husb320_power_on(struct husb320 *husb)
+{
+	bool needs_delay = false;
+	int ret;
+
+	ret = devm_regulator_get_enable_optional(husb->dev, "vdd");
+	if (ret == -ENODEV) {
+		ret = 0;
+	} else if (ret) {
+		return dev_err_probe(husb->dev, ret,
+				     "failed to enable VDD supply\n");
+	} else {
+		needs_delay = true;
+	}
+
+	husb->enable_gpio = devm_gpiod_get_optional(husb->dev, "enable",
+						    GPIOD_OUT_HIGH);
+	if (IS_ERR(husb->enable_gpio))
+		return dev_err_probe(husb->dev, PTR_ERR(husb->enable_gpio),
+				     "failed to enable controller\n");
+
+	if (husb->enable_gpio) {
+		ret = devm_add_action_or_reset(husb->dev, husb320_disable, husb);
+		if (ret)
+			return ret;
+		needs_delay = true;
+	}
+
+	/* Wait for the controller to become I2C-accessible after power-on. */
+	if (needs_delay)
+		msleep(100);
+
+	return 0;
+}
+
+static int husb320_check_device(struct husb320 *husb)
+{
+	unsigned int device_id;
+	unsigned int device_type;
+	int ret;
+
+	ret = regmap_read(husb->regmap, HUSB320_REG_DEVICE_ID, &device_id);
+	if (ret)
+		return dev_err_probe(husb->dev, ret,
+				     "failed to read device ID\n");
+
+	ret = regmap_read(husb->regmap, HUSB320_REG_DEVICE_TYPE, &device_type);
+	if (ret)
+		return dev_err_probe(husb->dev, ret,
+				     "failed to read device type\n");
+
+	if (FIELD_GET(HUSB320_DEVICE_ID_VERSION, device_id) !=
+			HUSB320_DEVICE_ID_VERSION_1 ||
+	    device_type != HUSB320_DEVICE_TYPE)
+		return dev_err_probe(husb->dev, -ENODEV,
+				     "unsupported device ID %#x type %#x\n",
+				     device_id, device_type);
+
+	dev_dbg(husb->dev, "version %lu revision %lu\n",
+		FIELD_GET(HUSB320_DEVICE_ID_VERSION, device_id),
+		FIELD_GET(HUSB320_DEVICE_ID_REVISION, device_id));
+
+	return 0;
+}
+
+static int husb320_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct husb320 *husb;
+	int ret;
+
+	husb = devm_kzalloc(dev, sizeof(*husb), GFP_KERNEL);
+	if (!husb)
+		return -ENOMEM;
+
+	husb->dev = dev;
+	husb->state = HUSB320_STATE_UNATTACHED;
+	husb->usb_role = USB_ROLE_NONE;
+	mutex_init(&husb->lock);
+	i2c_set_clientdata(client, husb);
+
+	ret = husb320_power_on(husb);
+	if (ret)
+		return ret;
+
+	husb->regmap = devm_regmap_init_i2c(client, &husb320_regmap_config);
+	if (IS_ERR(husb->regmap))
+		return dev_err_probe(dev, PTR_ERR(husb->regmap),
+				     "failed to initialize regmap\n");
+
+	ret = husb320_check_device(husb);
+	if (ret)
+		return ret;
+
+	if (client->irq <= 0)
+		return dev_err_probe(dev, client->irq ?: -EINVAL,
+				     "missing interrupt\n");
+
+	ret = husb320_parse_connector(husb);
+	if (ret)
+		goto err_put_connector;
+
+	ret = husb320_get_role_switch(husb);
+	if (ret)
+		goto err_put_connector;
+
+	ret = husb320_get_vbus(husb);
+	if (ret)
+		goto err_put_role;
+
+	ret = husb320_hw_init(husb);
+	if (ret)
+		goto err_hw_disable;
+
+	husb->port = typec_register_port(dev, &husb->cap);
+	if (IS_ERR(husb->port)) {
+		ret = PTR_ERR(husb->port);
+		goto err_hw_disable;
+	}
+
+	ret = devm_request_threaded_irq(dev, client->irq, NULL,
+					husb320_irq_thread, IRQF_ONESHOT,
+					dev_name(dev), husb);
+	if (ret)
+		goto err_unregister_port;
+
+	mutex_lock(&husb->lock);
+	ret = husb320_update_state(husb);
+	mutex_unlock(&husb->lock);
+	if (ret)
+		goto err_free_irq;
+
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+				 HUSB320_CONTROL_INT_MASK, 0);
+	if (ret)
+		goto err_free_irq;
+
+	return 0;
+
+err_free_irq:
+	mutex_lock(&husb->lock);
+	husb->shutting_down = true;
+	mutex_unlock(&husb->lock);
+	devm_free_irq(dev, client->irq, husb);
+err_unregister_port:
+	mutex_lock(&husb->lock);
+	husb->shutting_down = true;
+	husb320_disconnect(husb);
+	mutex_unlock(&husb->lock);
+	typec_unregister_port(husb->port);
+err_hw_disable:
+	husb320_hw_disable(husb);
+err_put_role:
+	usb_role_switch_put(husb->role_sw);
+err_put_connector:
+	fwnode_handle_put(husb->connector);
+
+	return ret;
+}
+
+static void husb320_remove(struct i2c_client *client)
+{
+	struct husb320 *husb = i2c_get_clientdata(client);
+	int ret;
+
+	mutex_lock(&husb->lock);
+	husb->shutting_down = true;
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+				 HUSB320_CONTROL_INT_MASK,
+				 HUSB320_CONTROL_INT_MASK);
+	mutex_unlock(&husb->lock);
+	if (ret)
+		dev_err(husb->dev, "failed to mask interrupts: %d\n", ret);
+
+	devm_free_irq(husb->dev, client->irq, husb);
+
+	mutex_lock(&husb->lock);
+	ret = husb320_disconnect(husb);
+	if (ret)
+		dev_err(husb->dev, "failed to disconnect port: %d\n", ret);
+	ret = husb320_hw_disable(husb);
+	if (ret)
+		dev_err(husb->dev, "failed to disable controller: %d\n", ret);
+	mutex_unlock(&husb->lock);
+
+	typec_unregister_port(husb->port);
+	usb_role_switch_put(husb->role_sw);
+	fwnode_handle_put(husb->connector);
+}
+
+static void husb320_shutdown(struct i2c_client *client)
+{
+	struct husb320 *husb = i2c_get_clientdata(client);
+	int ret;
+
+	mutex_lock(&husb->lock);
+	husb->shutting_down = true;
+	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
+				 HUSB320_CONTROL_INT_MASK,
+				 HUSB320_CONTROL_INT_MASK);
+	mutex_unlock(&husb->lock);
+	if (ret)
+		dev_err(husb->dev, "failed to mask interrupts: %d\n", ret);
+
+	disable_irq(client->irq);
+
+	mutex_lock(&husb->lock);
+	ret = husb320_disconnect(husb);
+	if (ret) {
+		dev_err(husb->dev, "failed to disconnect port: %d\n", ret);
+	} else if (husb->cap.type != TYPEC_PORT_SRC) {
+		ret = husb320_set_sink_mode(husb);
+		if (!ret)
+			goto out_unlock;
+
+		dev_err(husb->dev, "failed to configure shutdown mode: %d\n", ret);
+	}
+
+	ret = husb320_hw_disable(husb);
+	if (ret)
+		dev_err(husb->dev, "failed to disable controller: %d\n", ret);
+
+out_unlock:
+	mutex_unlock(&husb->lock);
+}
+
+static const struct of_device_id husb320_of_match[] = {
+	{ .compatible = "hynetek,husb320" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, husb320_of_match);
+
+static struct i2c_driver husb320_driver = {
+	.driver = {
+		.name = "husb320",
+		.of_match_table = husb320_of_match,
+	},
+	.probe = husb320_probe,
+	.remove = husb320_remove,
+	.shutdown = husb320_shutdown,
+};
+module_i2c_driver(husb320_driver);
+
+MODULE_AUTHOR("Hongyang Zhao <hongyang.zhao@163.com>");
+MODULE_DESCRIPTION("Hynetek HUSB320 USB Type-C controller driver");
+MODULE_LICENSE("GPL");

-- 
2.43.0


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH 3/3] MAINTAINERS: Add HUSB320 Type-C controller entry
  2026-07-20  8:17 [PATCH 0/3] Add Hynetek HUSB320 Type-C controller support Hongyang Zhao
  2026-07-20  8:17 ` [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller Hongyang Zhao
  2026-07-20  8:17 ` [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver Hongyang Zhao
@ 2026-07-20  8:17 ` Hongyang Zhao
  2 siblings, 0 replies; 7+ messages in thread
From: Hongyang Zhao @ 2026-07-20  8:17 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Heikki Krogerus, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, Liam Girdwood, Mark Brown
  Cc: Roger Shimizu, linux-usb, devicetree, linux-kernel, Hongyang Zhao

Add myself as maintainer for the Hynetek HUSB320 binding and driver.

Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
---
 MAINTAINERS | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 806bd2d80d15..d3390b1daa1a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -12056,6 +12056,13 @@ L:	linux-kernel@vger.kernel.org
 S:	Maintained
 F:	arch/x86/kernel/cpu/hygon.c
 
+HYNETEK HUSB320 TYPE-C DRIVER
+M:	Hongyang Zhao <hongyang.zhao@163.com>
+L:	linux-usb@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/usb/hynetek,husb320.yaml
+F:	drivers/usb/typec/husb320.c
+
 HYNIX HI556 SENSOR DRIVER
 M:	Sakari Ailus <sakari.ailus@linux.intel.com>
 L:	linux-media@vger.kernel.org

-- 
2.43.0


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver
  2026-07-20  8:17 ` [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver Hongyang Zhao
@ 2026-07-21 10:27   ` Heikki Krogerus
  2026-08-24 10:35   ` Meng Li
  1 sibling, 0 replies; 7+ messages in thread
From: Heikki Krogerus @ 2026-07-21 10:27 UTC (permalink / raw)
  To: Hongyang Zhao
  Cc: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Liam Girdwood, Mark Brown, Roger Shimizu,
	linux-usb, devicetree, linux-kernel

On Mon, Jul 20, 2026 at 04:17:12PM +0800, Hongyang Zhao wrote:
> The HUSB320 is an autonomous Configuration Channel controller which
> supports fixed source or sink operation and dual-role ports without USB
> Power Delivery.
> 
> Add a Type-C class driver for attach and detach detection, orientation
> and Rp current reporting, platform VBUS control, USB role switching,
> Try.SRC and Try.SNK, and audio and debug accessories.
> 
> Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>

Acked-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>

> ---
>  drivers/usb/typec/Kconfig   |   12 +
>  drivers/usb/typec/Makefile  |    1 +
>  drivers/usb/typec/husb320.c | 1177 +++++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 1190 insertions(+)
> 
> diff --git a/drivers/usb/typec/Kconfig b/drivers/usb/typec/Kconfig
> index 2f80c2792dbd..22ef8bacacf7 100644
> --- a/drivers/usb/typec/Kconfig
> +++ b/drivers/usb/typec/Kconfig
> @@ -88,6 +88,18 @@ config TYPEC_HD3SS3220
>  	  If you choose to build this driver as a dynamically linked module, the
>  	  module will be called hd3ss3220.ko.
>  
> +config TYPEC_HUSB320
> +	tristate "Hynetek HUSB320 Type-C controller driver"
> +	depends on I2C
> +	depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
> +	select REGMAP_I2C
> +	help
> +	  Say Y or M here if your system has a Hynetek HUSB320 autonomous
> +	  USB Type-C controller.
> +
> +	  If you choose to build this driver as a dynamically linked module, the
> +	  module will be called husb320.ko.
> +
>  config TYPEC_STUSB160X
>  	tristate "STMicroelectronics STUSB160x Type-C controller driver"
>  	depends on USB_ROLE_SWITCH || !USB_ROLE_SWITCH
> diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
> index 8a6a1c663eb6..1b61c34d6e66 100644
> --- a/drivers/usb/typec/Makefile
> +++ b/drivers/usb/typec/Makefile
> @@ -8,6 +8,7 @@ obj-$(CONFIG_TYPEC_UCSI)	+= ucsi/
>  obj-$(CONFIG_TYPEC_TPS6598X)	+= tipd/
>  obj-$(CONFIG_TYPEC_ANX7411)	+= anx7411.o
>  obj-$(CONFIG_TYPEC_HD3SS3220)	+= hd3ss3220.o
> +obj-$(CONFIG_TYPEC_HUSB320)	+= husb320.o
>  obj-$(CONFIG_TYPEC_STUSB160X) 	+= stusb160x.o
>  obj-$(CONFIG_TYPEC_RT1719)	+= rt1719.o
>  obj-$(CONFIG_TYPEC_WUSB3801)	+= wusb3801.o
> diff --git a/drivers/usb/typec/husb320.c b/drivers/usb/typec/husb320.c
> new file mode 100644
> index 000000000000..fd9f949d9b52
> --- /dev/null
> +++ b/drivers/usb/typec/husb320.c
> @@ -0,0 +1,1177 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Hynetek HUSB320 USB Type-C controller driver
> + *
> + * Copyright (C) 2026 Hongyang Zhao <hongyang.zhao@163.com>
> + */
> +
> +#include <linux/bitfield.h>
> +#include <linux/delay.h>
> +#include <linux/gpio/consumer.h>
> +#include <linux/i2c.h>
> +#include <linux/interrupt.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/of.h>
> +#include <linux/property.h>
> +#include <linux/regmap.h>
> +#include <linux/regulator/consumer.h>
> +#include <linux/usb/role.h>
> +#include <linux/usb/typec.h>
> +#include <linux/usb/typec_altmode.h>
> +
> +#define HUSB320_REG_DEVICE_ID		0x01
> +#define HUSB320_REG_DEVICE_TYPE		0x02
> +#define HUSB320_REG_PORT_ROLE		0x03
> +#define HUSB320_REG_CONTROL		0x04
> +#define HUSB320_REG_CONTROL1		0x05
> +#define HUSB320_REG_MANUAL		0x09
> +#define HUSB320_REG_RESET		0x0a
> +#define HUSB320_REG_MASK			0x0e
> +#define HUSB320_REG_MASK1		0x0f
> +#define HUSB320_REG_STATUS		0x11
> +#define HUSB320_REG_TYPE			0x13
> +#define HUSB320_REG_INTERRUPT		0x14
> +#define HUSB320_REG_INTERRUPT1		0x15
> +
> +#define HUSB320_DEVICE_ID_VERSION	GENMASK(7, 4)
> +#define HUSB320_DEVICE_ID_REVISION	GENMASK(3, 0)
> +#define HUSB320_DEVICE_ID_VERSION_1	1
> +#define HUSB320_DEVICE_TYPE		0x03
> +
> +#define HUSB320_PORT_ROLE_TRY		GENMASK(5, 4)
> +#define HUSB320_PORT_ROLE_MODE		GENMASK(2, 0)
> +#define HUSB320_PORT_ROLE_SOURCE		BIT(0)
> +#define HUSB320_PORT_ROLE_SINK		BIT(1)
> +#define HUSB320_PORT_ROLE_DRP		BIT(2)
> +#define HUSB320_TRY_NONE			0
> +#define HUSB320_TRY_SINK			1
> +#define HUSB320_TRY_SOURCE		2
> +
> +#define HUSB320_CONTROL_HOST_CURRENT	GENMASK(2, 1)
> +#define HUSB320_CONTROL_INT_MASK		BIT(0)
> +#define HUSB320_HOST_CURRENT_DEFAULT	1
> +#define HUSB320_HOST_CURRENT_1_5A	2
> +#define HUSB320_HOST_CURRENT_3_0A	3
> +
> +#define HUSB320_CONTROL1_ENABLE		BIT(3)
> +
> +#define HUSB320_MANUAL_ERROR_RECOVERY	BIT(0)
> +
> +#define HUSB320_RESET_SW			BIT(0)
> +
> +/* The datasheet defines the fault mask, but reserves the matching IRQ bit. */
> +#define HUSB320_MASK_FAULT		BIT(5)
> +#define HUSB320_MASK_AUTOSINK		BIT(3)
> +#define HUSB320_MASK1_FORCE		GENMASK(2, 1)
> +
> +#define HUSB320_STATUS_ORIENTATION	GENMASK(5, 4)
> +#define HUSB320_STATUS_ORIENTATION_CC1	1
> +#define HUSB320_STATUS_ORIENTATION_CC2	2
> +#define HUSB320_STATUS_CURRENT		GENMASK(2, 1)
> +#define HUSB320_STATUS_ATTACHED		BIT(0)
> +
> +#define HUSB320_TYPE_DEBUG_SOURCE	BIT(6)
> +#define HUSB320_TYPE_DEBUG_SINK		BIT(5)
> +#define HUSB320_TYPE_SINK		BIT(4)
> +#define HUSB320_TYPE_SOURCE		BIT(3)
> +#define HUSB320_TYPE_AUDIO_VBUS		BIT(1)
> +#define HUSB320_TYPE_AUDIO		BIT(0)
> +
> +#define HUSB320_INT_ORIENTATION		BIT(6)
> +#define HUSB320_INT_VBUS_CHANGE		BIT(4)
> +#define HUSB320_INT_AUTOSINK		BIT(3)
> +#define HUSB320_INT_CURRENT_CHANGE	BIT(2)
> +#define HUSB320_INT_DETACH		BIT(1)
> +#define HUSB320_INT_ATTACH		BIT(0)
> +#define HUSB320_INT_VALID		(HUSB320_INT_ORIENTATION | \
> +					 HUSB320_INT_VBUS_CHANGE | \
> +					 HUSB320_INT_AUTOSINK | \
> +					 HUSB320_INT_CURRENT_CHANGE | \
> +					 HUSB320_INT_DETACH | \
> +					 HUSB320_INT_ATTACH)
> +#define HUSB320_INT1_VALID		GENMASK(2, 1)
> +
> +enum husb320_state {
> +	HUSB320_STATE_UNATTACHED,
> +	HUSB320_STATE_SOURCE,
> +	HUSB320_STATE_SINK,
> +	HUSB320_STATE_DEBUG_SOURCE,
> +	HUSB320_STATE_DEBUG_SINK,
> +	HUSB320_STATE_AUDIO,
> +};
> +
> +struct husb320 {
> +	struct device *dev;
> +	struct regmap *regmap;
> +	struct gpio_desc *enable_gpio;
> +	struct regulator *vbus;
> +	struct usb_role_switch *role_sw;
> +	struct typec_capability cap;
> +	struct typec_port *port;
> +	struct typec_partner *partner;
> +	struct fwnode_handle *connector;
> +	/* Serializes controller access and Type-C state updates. */
> +	struct mutex lock;
> +	enum typec_port_type port_type;
> +	enum typec_role preferred_role;
> +	enum typec_pwr_opmode source_opmode;
> +	enum husb320_state state;
> +	enum usb_role usb_role;
> +	int connector_mode;
> +	bool has_data;
> +	bool mode_valid;
> +	bool shutting_down;
> +	bool state_valid;
> +	bool vbus_on;
> +};
> +
> +static const struct regmap_config husb320_regmap_config = {
> +	.reg_bits = 8,
> +	.val_bits = 8,
> +	.max_register = HUSB320_REG_INTERRUPT1,
> +};
> +
> +static enum typec_role husb320_default_power_role(struct husb320 *husb)
> +{
> +	switch (husb->port_type) {
> +	case TYPEC_PORT_SRC:
> +		return TYPEC_SOURCE;
> +	case TYPEC_PORT_SNK:
> +		return TYPEC_SINK;
> +	case TYPEC_PORT_DRP:
> +	default:
> +		if (husb->preferred_role == TYPEC_SOURCE)
> +			return TYPEC_SOURCE;
> +
> +		return TYPEC_SINK;
> +	}
> +}
> +
> +static enum typec_data_role husb320_default_data_role(struct husb320 *husb)
> +{
> +	switch (husb->cap.data) {
> +	case TYPEC_PORT_DFP:
> +		return TYPEC_HOST;
> +	case TYPEC_PORT_UFP:
> +		return TYPEC_DEVICE;
> +	case TYPEC_PORT_DRD:
> +	default:
> +		return husb320_default_power_role(husb) == TYPEC_SOURCE ?
> +			TYPEC_HOST : TYPEC_DEVICE;
> +	}
> +}
> +
> +static void husb320_set_unattached_roles(struct husb320 *husb)
> +{
> +	enum typec_role power_role = husb320_default_power_role(husb);
> +
> +	typec_set_pwr_role(husb->port, power_role);
> +	if (husb->has_data)
> +		typec_set_data_role(husb->port,
> +				    husb320_default_data_role(husb));
> +	typec_set_pwr_opmode(husb->port,
> +			     power_role == TYPEC_SOURCE ?
> +			     husb->source_opmode : TYPEC_PWR_MODE_USB);
> +}
> +
> +static int husb320_set_vbus(struct husb320 *husb, bool enable)
> +{
> +	int ret;
> +
> +	if (!husb->vbus || husb->vbus_on == enable)
> +		return 0;
> +
> +	if (enable)
> +		ret = regulator_enable(husb->vbus);
> +	else
> +		ret = regulator_disable(husb->vbus);
> +	if (ret)
> +		return ret;
> +
> +	husb->vbus_on = enable;
> +
> +	return 0;
> +}
> +
> +static int husb320_set_mode(struct husb320 *husb, int mode)
> +{
> +	int ret;
> +
> +	if (husb->mode_valid && husb->connector_mode == mode)
> +		return 0;
> +
> +	ret = typec_set_mode(husb->port, mode);
> +	if (ret)
> +		return ret;
> +
> +	husb->connector_mode = mode;
> +	husb->mode_valid = true;
> +
> +	return 0;
> +}
> +
> +static void husb320_unregister_partner(struct husb320 *husb)
> +{
> +	if (!husb->partner)
> +		return;
> +
> +	typec_unregister_partner(husb->partner);
> +	husb->partner = NULL;
> +}
> +
> +static int husb320_register_partner(struct husb320 *husb,
> +				    enum typec_accessory accessory)
> +{
> +	struct typec_partner_desc desc = {
> +		.accessory = accessory,
> +		.usb_pd = false,
> +	};
> +
> +	if (husb->partner)
> +		return 0;
> +
> +	husb->partner = typec_register_partner(husb->port, &desc);
> +	if (IS_ERR(husb->partner)) {
> +		int ret = PTR_ERR(husb->partner);
> +
> +		husb->partner = NULL;
> +		return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +static int husb320_disconnect(struct husb320 *husb)
> +{
> +	int err = 0;
> +	int ret;
> +
> +	ret = husb320_set_vbus(husb, false);
> +	if (ret)
> +		err = ret;
> +
> +	ret = usb_role_switch_set_role(husb->role_sw, USB_ROLE_NONE);
> +	if (ret) {
> +		if (!err)
> +			err = ret;
> +	} else {
> +		husb->usb_role = USB_ROLE_NONE;
> +	}
> +
> +	ret = husb320_set_mode(husb, TYPEC_STATE_SAFE);
> +	if (ret && !err)
> +		err = ret;
> +
> +	husb320_unregister_partner(husb);
> +
> +	if (typec_get_orientation(husb->port) != TYPEC_ORIENTATION_NONE) {
> +		ret = typec_set_orientation(husb->port, TYPEC_ORIENTATION_NONE);
> +		if (ret && !err)
> +			err = ret;
> +	}
> +
> +	husb320_set_unattached_roles(husb);
> +
> +	husb->state = HUSB320_STATE_UNATTACHED;
> +	husb->state_valid = !err;
> +
> +	return err;
> +}
> +
> +static int husb320_hw_disable(struct husb320 *husb);
> +
> +static void husb320_error_recovery(struct husb320 *husb)
> +{
> +	int cleanup_ret;
> +	int disable_ret;
> +	int recovery_ret;
> +
> +	cleanup_ret = husb320_disconnect(husb);
> +	recovery_ret = regmap_write(husb->regmap, HUSB320_REG_MANUAL,
> +				    HUSB320_MANUAL_ERROR_RECOVERY);
> +	if (cleanup_ret)
> +		dev_err_ratelimited(husb->dev,
> +				    "failed to clean up port: %d\n", cleanup_ret);
> +	if (recovery_ret)
> +		dev_err_ratelimited(husb->dev,
> +				    "failed to start error recovery: %d\n",
> +				    recovery_ret);
> +
> +	if (!cleanup_ret && !recovery_ret)
> +		return;
> +
> +	husb->shutting_down = true;
> +	disable_ret = husb320_hw_disable(husb);
> +	if (disable_ret)
> +		dev_err_ratelimited(husb->dev,
> +				    "failed to disable controller: %d\n",
> +				    disable_ret);
> +}
> +
> +static enum typec_pwr_opmode husb320_sink_opmode(unsigned int status)
> +{
> +	switch (FIELD_GET(HUSB320_STATUS_CURRENT, status)) {
> +	case 2:
> +		return TYPEC_PWR_MODE_1_5A;
> +	case 3:
> +		return TYPEC_PWR_MODE_3_0A;
> +	case 0:
> +	case 1:
> +	default:
> +		return TYPEC_PWR_MODE_USB;
> +	}
> +}
> +
> +static enum typec_role husb320_state_power_role(enum husb320_state state)
> +{
> +	switch (state) {
> +	case HUSB320_STATE_SOURCE:
> +	case HUSB320_STATE_DEBUG_SOURCE:
> +	case HUSB320_STATE_AUDIO:
> +		return TYPEC_SOURCE;
> +	case HUSB320_STATE_SINK:
> +	case HUSB320_STATE_DEBUG_SINK:
> +	case HUSB320_STATE_UNATTACHED:
> +	default:
> +		return TYPEC_SINK;
> +	}
> +}
> +
> +static enum typec_accessory husb320_state_accessory(enum husb320_state state)
> +{
> +	switch (state) {
> +	case HUSB320_STATE_DEBUG_SOURCE:
> +	case HUSB320_STATE_DEBUG_SINK:
> +		return TYPEC_ACCESSORY_DEBUG;
> +	case HUSB320_STATE_AUDIO:
> +		return TYPEC_ACCESSORY_AUDIO;
> +	default:
> +		return TYPEC_ACCESSORY_NONE;
> +	}
> +}
> +
> +static enum usb_role husb320_state_usb_role(struct husb320 *husb,
> +					    enum husb320_state state)
> +{
> +	if (!husb->has_data)
> +		return USB_ROLE_NONE;
> +
> +	if ((state == HUSB320_STATE_SOURCE ||
> +	     state == HUSB320_STATE_DEBUG_SOURCE) &&
> +	    husb->cap.data != TYPEC_PORT_UFP)
> +		return USB_ROLE_HOST;
> +
> +	if ((state == HUSB320_STATE_SINK ||
> +	     state == HUSB320_STATE_DEBUG_SINK) &&
> +	    husb->cap.data != TYPEC_PORT_DFP)
> +		return USB_ROLE_DEVICE;
> +
> +	return USB_ROLE_NONE;
> +}
> +
> +static int husb320_state_mode(enum husb320_state state)
> +{
> +	switch (state) {
> +	case HUSB320_STATE_DEBUG_SOURCE:
> +	case HUSB320_STATE_DEBUG_SINK:
> +		return TYPEC_MODE_DEBUG;
> +	case HUSB320_STATE_AUDIO:
> +		return TYPEC_MODE_AUDIO;
> +	case HUSB320_STATE_SOURCE:
> +	case HUSB320_STATE_SINK:
> +		return TYPEC_STATE_USB;
> +	case HUSB320_STATE_UNATTACHED:
> +	default:
> +		return TYPEC_STATE_SAFE;
> +	}
> +}
> +
> +static int husb320_apply_state(struct husb320 *husb,
> +			       enum husb320_state state,
> +			       enum typec_orientation orientation,
> +			       unsigned int status)
> +{
> +	enum typec_accessory accessory = husb320_state_accessory(state);
> +	enum usb_role usb_role = husb320_state_usb_role(husb, state);
> +	enum typec_pwr_opmode opmode;
> +	enum typec_role power_role;
> +	int ret;
> +
> +	if (state == HUSB320_STATE_UNATTACHED && husb->state_valid &&
> +	    husb->state == HUSB320_STATE_UNATTACHED && !husb->partner &&
> +	    !husb->vbus_on &&
> +	    husb->usb_role == USB_ROLE_NONE)
> +		return 0;
> +
> +	if (state == HUSB320_STATE_UNATTACHED)
> +		return husb320_disconnect(husb);
> +
> +	if (state != husb->state &&
> +	    (!husb->state_valid ||
> +	     husb->state != HUSB320_STATE_UNATTACHED)) {
> +		ret = husb320_disconnect(husb);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	if (typec_get_orientation(husb->port) != orientation) {
> +		ret = typec_set_orientation(husb->port, orientation);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	/* Accessory states do not request the platform VBUS supply. */
> +	ret = husb320_set_vbus(husb, state == HUSB320_STATE_SOURCE);
> +	if (ret)
> +		return ret;
> +
> +	if (usb_role != husb->usb_role) {
> +		ret = usb_role_switch_set_role(husb->role_sw, usb_role);
> +		if (ret)
> +			return ret;
> +		husb->usb_role = usb_role;
> +	}
> +
> +	ret = husb320_set_mode(husb, husb320_state_mode(state));
> +	if (ret)
> +		return ret;
> +
> +	power_role = husb320_state_power_role(state);
> +	opmode = state == HUSB320_STATE_SINK ?
> +		husb320_sink_opmode(status) :
> +		(power_role == TYPEC_SOURCE ? husb->source_opmode :
> +		 TYPEC_PWR_MODE_USB);
> +
> +	if (usb_role == USB_ROLE_HOST)
> +		typec_set_data_role(husb->port, TYPEC_HOST);
> +	else if (usb_role == USB_ROLE_DEVICE)
> +		typec_set_data_role(husb->port, TYPEC_DEVICE);
> +	typec_set_pwr_role(husb->port, power_role);
> +	typec_set_pwr_opmode(husb->port, opmode);
> +
> +	ret = husb320_register_partner(husb, accessory);
> +	if (ret) {
> +		husb->state_valid = false;
> +		dev_err_ratelimited(husb->dev,
> +				    "failed to register partner: %d\n", ret);
> +		return ret;
> +	}
> +
> +	husb->state = state;
> +	husb->state_valid = true;
> +
> +	return 0;
> +}
> +
> +static enum typec_orientation husb320_get_orientation(unsigned int status)
> +{
> +	switch (FIELD_GET(HUSB320_STATUS_ORIENTATION, status)) {
> +	case HUSB320_STATUS_ORIENTATION_CC1:
> +		return TYPEC_ORIENTATION_NORMAL;
> +	case HUSB320_STATUS_ORIENTATION_CC2:
> +		return TYPEC_ORIENTATION_REVERSE;
> +	default:
> +		return TYPEC_ORIENTATION_NONE;
> +	}
> +}
> +
> +static enum husb320_state husb320_get_state(unsigned int status,
> +					    unsigned int type)
> +{
> +	if (!(status & HUSB320_STATUS_ATTACHED))
> +		return HUSB320_STATE_UNATTACHED;
> +
> +	if (type & HUSB320_TYPE_DEBUG_SOURCE)
> +		return HUSB320_STATE_DEBUG_SOURCE;
> +	if (type & HUSB320_TYPE_DEBUG_SINK)
> +		return HUSB320_STATE_DEBUG_SINK;
> +	if (type & HUSB320_TYPE_SINK)
> +		return HUSB320_STATE_SINK;
> +	if (type & HUSB320_TYPE_SOURCE)
> +		return HUSB320_STATE_SOURCE;
> +	if (type & (HUSB320_TYPE_AUDIO_VBUS | HUSB320_TYPE_AUDIO))
> +		return HUSB320_STATE_AUDIO;
> +
> +	return HUSB320_STATE_UNATTACHED;
> +}
> +
> +static int husb320_update_state(struct husb320 *husb)
> +{
> +	enum typec_orientation orientation;
> +	enum husb320_state state;
> +	unsigned int status;
> +	unsigned int type;
> +	int ret;
> +
> +	ret = regmap_read(husb->regmap, HUSB320_REG_STATUS, &status);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_read(husb->regmap, HUSB320_REG_TYPE, &type);
> +	if (ret)
> +		return ret;
> +
> +	state = husb320_get_state(status, type);
> +	orientation = state == HUSB320_STATE_UNATTACHED ?
> +		TYPEC_ORIENTATION_NONE : husb320_get_orientation(status);
> +
> +	ret = husb320_apply_state(husb, state, orientation, status);
> +	if (!ret)
> +		dev_dbg(husb->dev, "status=%#x type=%#x state=%u\n",
> +			status, type, state);
> +
> +	return ret;
> +}
> +
> +static irqreturn_t husb320_irq_thread(int irq, void *data)
> +{
> +	struct husb320 *husb = data;
> +	unsigned int interrupt;
> +	unsigned int interrupt1;
> +	int disable_ret;
> +	int ret;
> +
> +	mutex_lock(&husb->lock);
> +	if (husb->shutting_down) {
> +		disable_irq_nosync(irq);
> +		mutex_unlock(&husb->lock);
> +		return IRQ_HANDLED;
> +	}
> +
> +	ret = regmap_read(husb->regmap, HUSB320_REG_INTERRUPT, &interrupt);
> +	if (ret)
> +		goto err;
> +
> +	ret = regmap_read(husb->regmap, HUSB320_REG_INTERRUPT1, &interrupt1);
> +	if (ret)
> +		goto err;
> +
> +	interrupt &= HUSB320_INT_VALID;
> +	interrupt1 &= HUSB320_INT1_VALID;
> +	if (!interrupt && !interrupt1) {
> +		mutex_unlock(&husb->lock);
> +		return IRQ_NONE;
> +	}
> +
> +	if (interrupt) {
> +		ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT,
> +				   interrupt);
> +		if (ret)
> +			goto err;
> +	}
> +
> +	if (interrupt1) {
> +		ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT1,
> +				   interrupt1);
> +		if (ret)
> +			goto err;
> +	}
> +
> +	if (interrupt & HUSB320_INT_DETACH) {
> +		ret = husb320_disconnect(husb);
> +		if (ret)
> +			dev_err_ratelimited(husb->dev,
> +					    "failed to disconnect port: %d\n",
> +					    ret);
> +	}
> +
> +	ret = husb320_update_state(husb);
> +	if (ret) {
> +		dev_err_ratelimited(husb->dev,
> +				    "failed to update port state: %d\n", ret);
> +		husb320_error_recovery(husb);
> +	}
> +
> +	mutex_unlock(&husb->lock);
> +
> +	return IRQ_HANDLED;
> +
> +err:
> +	dev_err_ratelimited(husb->dev, "failed to service interrupt: %d\n",
> +			    ret);
> +	husb->shutting_down = true;
> +	husb320_error_recovery(husb);
> +	disable_ret = husb320_hw_disable(husb);
> +	if (disable_ret)
> +		dev_err_ratelimited(husb->dev,
> +				    "failed to disable controller: %d\n",
> +				    disable_ret);
> +	disable_irq_nosync(irq);
> +	mutex_unlock(&husb->lock);
> +
> +	return IRQ_HANDLED;
> +}
> +
> +static unsigned int husb320_port_role(enum typec_port_type type)
> +{
> +	switch (type) {
> +	case TYPEC_PORT_SRC:
> +		return HUSB320_PORT_ROLE_SOURCE;
> +	case TYPEC_PORT_SNK:
> +		return HUSB320_PORT_ROLE_SINK;
> +	case TYPEC_PORT_DRP:
> +	default:
> +		return HUSB320_PORT_ROLE_DRP;
> +	}
> +}
> +
> +static unsigned int husb320_try_role(int role)
> +{
> +	switch (role) {
> +	case TYPEC_SINK:
> +		return HUSB320_TRY_SINK;
> +	case TYPEC_SOURCE:
> +		return HUSB320_TRY_SOURCE;
> +	case TYPEC_NO_PREFERRED_ROLE:
> +	default:
> +		return HUSB320_TRY_NONE;
> +	}
> +}
> +
> +static int husb320_port_type_set(struct typec_port *port,
> +				 enum typec_port_type type)
> +{
> +	struct husb320 *husb = typec_get_drvdata(port);
> +	unsigned int port_role;
> +	int ret;
> +
> +	mutex_lock(&husb->lock);
> +	if (husb->shutting_down) {
> +		ret = -ESHUTDOWN;
> +		goto out_unlock;
> +	}
> +
> +	port_role = husb320_port_role(type);
> +	if (type == TYPEC_PORT_DRP)
> +		port_role |= FIELD_PREP(HUSB320_PORT_ROLE_TRY,
> +					 husb320_try_role(husb->preferred_role));
> +
> +	ret = husb320_disconnect(husb);
> +	if (ret) {
> +		husb320_error_recovery(husb);
> +		goto out_unlock;
> +	}
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
> +				 HUSB320_PORT_ROLE_TRY |
> +				 HUSB320_PORT_ROLE_MODE,
> +				 port_role);
> +	if (ret) {
> +		husb320_error_recovery(husb);
> +		goto out_unlock;
> +	}
> +
> +	husb->port_type = type;
> +	husb320_set_unattached_roles(husb);
> +out_unlock:
> +	mutex_unlock(&husb->lock);
> +
> +	return ret;
> +}
> +
> +static int husb320_try_role_set(struct typec_port *port, int role)
> +{
> +	struct husb320 *husb = typec_get_drvdata(port);
> +	int ret;
> +
> +	mutex_lock(&husb->lock);
> +	if (husb->shutting_down) {
> +		ret = -ESHUTDOWN;
> +		goto out_unlock;
> +	}
> +
> +	if (husb->port_type == TYPEC_PORT_DRP)
> +		ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
> +					 HUSB320_PORT_ROLE_TRY,
> +					 FIELD_PREP(HUSB320_PORT_ROLE_TRY,
> +						    husb320_try_role(role)));
> +	else
> +		ret = 0;
> +	if (!ret)
> +		husb->preferred_role = role;
> +out_unlock:
> +	mutex_unlock(&husb->lock);
> +
> +	return ret;
> +}
> +
> +static const struct typec_operations husb320_typec_ops = {
> +	.port_type_set = husb320_port_type_set,
> +	.try_role = husb320_try_role_set,
> +};
> +
> +static unsigned int husb320_host_current(enum typec_pwr_opmode opmode)
> +{
> +	switch (opmode) {
> +	case TYPEC_PWR_MODE_1_5A:
> +		return HUSB320_HOST_CURRENT_1_5A;
> +	case TYPEC_PWR_MODE_3_0A:
> +		return HUSB320_HOST_CURRENT_3_0A;
> +	case TYPEC_PWR_MODE_USB:
> +	default:
> +		return HUSB320_HOST_CURRENT_DEFAULT;
> +	}
> +}
> +
> +static int husb320_hw_init(struct husb320 *husb)
> +{
> +	unsigned int port_role;
> +	unsigned int control;
> +	int ret;
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_RESET,
> +				 HUSB320_RESET_SW, HUSB320_RESET_SW);
> +	if (ret)
> +		return ret;
> +
> +	/* Wait for stable status and I2C access after the soft reset. */
> +	msleep(100);
> +
> +	control = HUSB320_CONTROL_INT_MASK |
> +		  FIELD_PREP(HUSB320_CONTROL_HOST_CURRENT,
> +			     husb320_host_current(husb->source_opmode));
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
> +				 HUSB320_CONTROL_INT_MASK |
> +				 HUSB320_CONTROL_HOST_CURRENT,
> +				 control);
> +	if (ret)
> +		return ret;
> +
> +	port_role = husb320_port_role(husb->port_type);
> +	if (husb->port_type == TYPEC_PORT_DRP)
> +		port_role |= FIELD_PREP(HUSB320_PORT_ROLE_TRY,
> +					 husb320_try_role(husb->preferred_role));
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
> +				 HUSB320_PORT_ROLE_TRY |
> +				 HUSB320_PORT_ROLE_MODE,
> +				 port_role);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(husb->regmap, HUSB320_REG_MASK,
> +			   HUSB320_MASK_FAULT | HUSB320_MASK_AUTOSINK);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(husb->regmap, HUSB320_REG_MASK1,
> +			   HUSB320_MASK1_FORCE);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
> +				 HUSB320_CONTROL1_ENABLE,
> +				 HUSB320_CONTROL1_ENABLE);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(husb->regmap, HUSB320_REG_INTERRUPT,
> +			   HUSB320_INT_VALID);
> +	if (ret)
> +		return ret;
> +
> +	return regmap_write(husb->regmap, HUSB320_REG_INTERRUPT1,
> +			    HUSB320_INT1_VALID);
> +}
> +
> +static int husb320_hw_disable(struct husb320 *husb)
> +{
> +	int err = 0;
> +	int ret;
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
> +				 HUSB320_CONTROL_INT_MASK,
> +				 HUSB320_CONTROL_INT_MASK);
> +	if (ret)
> +		err = ret;
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
> +				 HUSB320_CONTROL1_ENABLE, 0);
> +	if (ret && !err)
> +		err = ret;
> +
> +	return err;
> +}
> +
> +static int husb320_set_sink_mode(struct husb320 *husb)
> +{
> +	int ret;
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
> +				 HUSB320_CONTROL_INT_MASK,
> +				 HUSB320_CONTROL_INT_MASK);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_PORT_ROLE,
> +				 HUSB320_PORT_ROLE_TRY |
> +				 HUSB320_PORT_ROLE_MODE,
> +				 HUSB320_PORT_ROLE_SINK);
> +	if (ret)
> +		return ret;
> +
> +	return regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL1,
> +				  HUSB320_CONTROL1_ENABLE,
> +				  HUSB320_CONTROL1_ENABLE);
> +}
> +
> +static int husb320_parse_connector(struct husb320 *husb)
> +{
> +	const char *opmode;
> +	int ret;
> +
> +	husb->connector = device_get_named_child_node(husb->dev, "connector");
> +	if (!husb->connector)
> +		return -ENODEV;
> +
> +	/* The connector is parsed as a fwnode and is not populated as a device. */
> +	fw_devlink_purge_absent_suppliers(husb->connector);
> +
> +	ret = typec_get_fw_cap(&husb->cap, husb->connector);
> +	if (ret)
> +		return ret;
> +
> +	ret = fwnode_property_read_string(husb->connector,
> +					  "typec-power-opmode", &opmode);
> +	if (ret)
> +		return ret;
> +
> +	ret = typec_find_pwr_opmode(opmode);
> +	if (ret < 0)
> +		return ret;
> +	if (ret == TYPEC_PWR_MODE_PD)
> +		return -EINVAL;
> +
> +	husb->source_opmode = ret;
> +	husb->has_data = fwnode_property_present(husb->connector, "data-role");
> +	if (husb->has_data &&
> +	    ((husb->cap.type == TYPEC_PORT_SRC &&
> +	      husb->cap.data != TYPEC_PORT_DFP) ||
> +	     (husb->cap.type == TYPEC_PORT_SNK &&
> +	      husb->cap.data != TYPEC_PORT_UFP) ||
> +	     (husb->cap.type == TYPEC_PORT_DRP &&
> +	      husb->cap.data != TYPEC_PORT_DRD))) {
> +		return dev_err_probe(husb->dev, -EINVAL,
> +				     "power and data roles do not match\n");
> +	} else if (!husb->has_data) {
> +		if (husb->cap.type == TYPEC_PORT_SNK)
> +			husb->cap.data = TYPEC_PORT_UFP;
> +		else if (husb->cap.type == TYPEC_PORT_DRP)
> +			husb->cap.data = TYPEC_PORT_DRD;
> +	}
> +
> +	husb->cap.revision = USB_TYPEC_REV_2_0;
> +	husb->cap.accessory[0] = TYPEC_ACCESSORY_AUDIO;
> +	husb->cap.accessory[1] = TYPEC_ACCESSORY_DEBUG;
> +	husb->cap.orientation_aware = true;
> +	husb->cap.driver_data = husb;
> +	husb->cap.ops = &husb320_typec_ops;
> +	husb->port_type = husb->cap.type;
> +	husb->preferred_role = husb->cap.prefer_role;
> +
> +	return 0;
> +}
> +
> +static int husb320_get_role_switch(struct husb320 *husb)
> +{
> +	int ret;
> +
> +	if (!husb->has_data)
> +		return 0;
> +
> +	husb->role_sw = fwnode_usb_role_switch_get(husb->connector);
> +	if (IS_ERR(husb->role_sw)) {
> +		ret = PTR_ERR(husb->role_sw);
> +		husb->role_sw = NULL;
> +		if (ret == -ENODEV && husb->cap.data != TYPEC_PORT_DRD)
> +			return 0;
> +
> +		return dev_err_probe(husb->dev, ret,
> +				     "failed to get USB role switch\n");
> +	}
> +
> +	if (!husb->role_sw && husb->cap.data == TYPEC_PORT_DRD)
> +		return dev_err_probe(husb->dev, -ENODEV,
> +				     "USB data role has no role switch\n");
> +
> +	return 0;
> +}
> +
> +static int husb320_get_vbus(struct husb320 *husb)
> +{
> +	struct device_node *node = to_of_node(husb->connector);
> +
> +	if (node)
> +		husb->vbus = devm_of_regulator_get_optional(husb->dev, node, "vbus");
> +	else
> +		husb->vbus = devm_regulator_get_optional(husb->dev, "vbus");
> +
> +	if (IS_ERR(husb->vbus)) {
> +		int ret = PTR_ERR(husb->vbus);
> +
> +		husb->vbus = NULL;
> +		if (ret != -ENODEV)
> +			return dev_err_probe(husb->dev, ret,
> +					     "failed to get VBUS supply\n");
> +	}
> +
> +	if (husb->cap.type != TYPEC_PORT_SNK && !husb->vbus)
> +		return dev_err_probe(husb->dev, -ENODEV,
> +				     "source-capable port requires VBUS supply\n");
> +
> +	return 0;
> +}
> +
> +static void husb320_disable(void *data)
> +{
> +	struct husb320 *husb = data;
> +
> +	gpiod_set_value_cansleep(husb->enable_gpio, 0);
> +}
> +
> +static int husb320_power_on(struct husb320 *husb)
> +{
> +	bool needs_delay = false;
> +	int ret;
> +
> +	ret = devm_regulator_get_enable_optional(husb->dev, "vdd");
> +	if (ret == -ENODEV) {
> +		ret = 0;
> +	} else if (ret) {
> +		return dev_err_probe(husb->dev, ret,
> +				     "failed to enable VDD supply\n");
> +	} else {
> +		needs_delay = true;
> +	}
> +
> +	husb->enable_gpio = devm_gpiod_get_optional(husb->dev, "enable",
> +						    GPIOD_OUT_HIGH);
> +	if (IS_ERR(husb->enable_gpio))
> +		return dev_err_probe(husb->dev, PTR_ERR(husb->enable_gpio),
> +				     "failed to enable controller\n");
> +
> +	if (husb->enable_gpio) {
> +		ret = devm_add_action_or_reset(husb->dev, husb320_disable, husb);
> +		if (ret)
> +			return ret;
> +		needs_delay = true;
> +	}
> +
> +	/* Wait for the controller to become I2C-accessible after power-on. */
> +	if (needs_delay)
> +		msleep(100);
> +
> +	return 0;
> +}
> +
> +static int husb320_check_device(struct husb320 *husb)
> +{
> +	unsigned int device_id;
> +	unsigned int device_type;
> +	int ret;
> +
> +	ret = regmap_read(husb->regmap, HUSB320_REG_DEVICE_ID, &device_id);
> +	if (ret)
> +		return dev_err_probe(husb->dev, ret,
> +				     "failed to read device ID\n");
> +
> +	ret = regmap_read(husb->regmap, HUSB320_REG_DEVICE_TYPE, &device_type);
> +	if (ret)
> +		return dev_err_probe(husb->dev, ret,
> +				     "failed to read device type\n");
> +
> +	if (FIELD_GET(HUSB320_DEVICE_ID_VERSION, device_id) !=
> +			HUSB320_DEVICE_ID_VERSION_1 ||
> +	    device_type != HUSB320_DEVICE_TYPE)
> +		return dev_err_probe(husb->dev, -ENODEV,
> +				     "unsupported device ID %#x type %#x\n",
> +				     device_id, device_type);
> +
> +	dev_dbg(husb->dev, "version %lu revision %lu\n",
> +		FIELD_GET(HUSB320_DEVICE_ID_VERSION, device_id),
> +		FIELD_GET(HUSB320_DEVICE_ID_REVISION, device_id));
> +
> +	return 0;
> +}
> +
> +static int husb320_probe(struct i2c_client *client)
> +{
> +	struct device *dev = &client->dev;
> +	struct husb320 *husb;
> +	int ret;
> +
> +	husb = devm_kzalloc(dev, sizeof(*husb), GFP_KERNEL);
> +	if (!husb)
> +		return -ENOMEM;
> +
> +	husb->dev = dev;
> +	husb->state = HUSB320_STATE_UNATTACHED;
> +	husb->usb_role = USB_ROLE_NONE;
> +	mutex_init(&husb->lock);
> +	i2c_set_clientdata(client, husb);
> +
> +	ret = husb320_power_on(husb);
> +	if (ret)
> +		return ret;
> +
> +	husb->regmap = devm_regmap_init_i2c(client, &husb320_regmap_config);
> +	if (IS_ERR(husb->regmap))
> +		return dev_err_probe(dev, PTR_ERR(husb->regmap),
> +				     "failed to initialize regmap\n");
> +
> +	ret = husb320_check_device(husb);
> +	if (ret)
> +		return ret;
> +
> +	if (client->irq <= 0)
> +		return dev_err_probe(dev, client->irq ?: -EINVAL,
> +				     "missing interrupt\n");
> +
> +	ret = husb320_parse_connector(husb);
> +	if (ret)
> +		goto err_put_connector;
> +
> +	ret = husb320_get_role_switch(husb);
> +	if (ret)
> +		goto err_put_connector;
> +
> +	ret = husb320_get_vbus(husb);
> +	if (ret)
> +		goto err_put_role;
> +
> +	ret = husb320_hw_init(husb);
> +	if (ret)
> +		goto err_hw_disable;
> +
> +	husb->port = typec_register_port(dev, &husb->cap);
> +	if (IS_ERR(husb->port)) {
> +		ret = PTR_ERR(husb->port);
> +		goto err_hw_disable;
> +	}
> +
> +	ret = devm_request_threaded_irq(dev, client->irq, NULL,
> +					husb320_irq_thread, IRQF_ONESHOT,
> +					dev_name(dev), husb);
> +	if (ret)
> +		goto err_unregister_port;
> +
> +	mutex_lock(&husb->lock);
> +	ret = husb320_update_state(husb);
> +	mutex_unlock(&husb->lock);
> +	if (ret)
> +		goto err_free_irq;
> +
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
> +				 HUSB320_CONTROL_INT_MASK, 0);
> +	if (ret)
> +		goto err_free_irq;
> +
> +	return 0;
> +
> +err_free_irq:
> +	mutex_lock(&husb->lock);
> +	husb->shutting_down = true;
> +	mutex_unlock(&husb->lock);
> +	devm_free_irq(dev, client->irq, husb);
> +err_unregister_port:
> +	mutex_lock(&husb->lock);
> +	husb->shutting_down = true;
> +	husb320_disconnect(husb);
> +	mutex_unlock(&husb->lock);
> +	typec_unregister_port(husb->port);
> +err_hw_disable:
> +	husb320_hw_disable(husb);
> +err_put_role:
> +	usb_role_switch_put(husb->role_sw);
> +err_put_connector:
> +	fwnode_handle_put(husb->connector);
> +
> +	return ret;
> +}
> +
> +static void husb320_remove(struct i2c_client *client)
> +{
> +	struct husb320 *husb = i2c_get_clientdata(client);
> +	int ret;
> +
> +	mutex_lock(&husb->lock);
> +	husb->shutting_down = true;
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
> +				 HUSB320_CONTROL_INT_MASK,
> +				 HUSB320_CONTROL_INT_MASK);
> +	mutex_unlock(&husb->lock);
> +	if (ret)
> +		dev_err(husb->dev, "failed to mask interrupts: %d\n", ret);
> +
> +	devm_free_irq(husb->dev, client->irq, husb);
> +
> +	mutex_lock(&husb->lock);
> +	ret = husb320_disconnect(husb);
> +	if (ret)
> +		dev_err(husb->dev, "failed to disconnect port: %d\n", ret);
> +	ret = husb320_hw_disable(husb);
> +	if (ret)
> +		dev_err(husb->dev, "failed to disable controller: %d\n", ret);
> +	mutex_unlock(&husb->lock);
> +
> +	typec_unregister_port(husb->port);
> +	usb_role_switch_put(husb->role_sw);
> +	fwnode_handle_put(husb->connector);
> +}
> +
> +static void husb320_shutdown(struct i2c_client *client)
> +{
> +	struct husb320 *husb = i2c_get_clientdata(client);
> +	int ret;
> +
> +	mutex_lock(&husb->lock);
> +	husb->shutting_down = true;
> +	ret = regmap_update_bits(husb->regmap, HUSB320_REG_CONTROL,
> +				 HUSB320_CONTROL_INT_MASK,
> +				 HUSB320_CONTROL_INT_MASK);
> +	mutex_unlock(&husb->lock);
> +	if (ret)
> +		dev_err(husb->dev, "failed to mask interrupts: %d\n", ret);
> +
> +	disable_irq(client->irq);
> +
> +	mutex_lock(&husb->lock);
> +	ret = husb320_disconnect(husb);
> +	if (ret) {
> +		dev_err(husb->dev, "failed to disconnect port: %d\n", ret);
> +	} else if (husb->cap.type != TYPEC_PORT_SRC) {
> +		ret = husb320_set_sink_mode(husb);
> +		if (!ret)
> +			goto out_unlock;
> +
> +		dev_err(husb->dev, "failed to configure shutdown mode: %d\n", ret);
> +	}
> +
> +	ret = husb320_hw_disable(husb);
> +	if (ret)
> +		dev_err(husb->dev, "failed to disable controller: %d\n", ret);
> +
> +out_unlock:
> +	mutex_unlock(&husb->lock);
> +}
> +
> +static const struct of_device_id husb320_of_match[] = {
> +	{ .compatible = "hynetek,husb320" },
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(of, husb320_of_match);
> +
> +static struct i2c_driver husb320_driver = {
> +	.driver = {
> +		.name = "husb320",
> +		.of_match_table = husb320_of_match,
> +	},
> +	.probe = husb320_probe,
> +	.remove = husb320_remove,
> +	.shutdown = husb320_shutdown,
> +};
> +module_i2c_driver(husb320_driver);
> +
> +MODULE_AUTHOR("Hongyang Zhao <hongyang.zhao@163.com>");
> +MODULE_DESCRIPTION("Hynetek HUSB320 USB Type-C controller driver");
> +MODULE_LICENSE("GPL");
> 
> -- 
> 2.43.0

-- 
heikki

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller
  2026-07-20  8:17 ` [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller Hongyang Zhao
@ 2026-07-31 22:53   ` Rob Herring (Arm)
  0 siblings, 0 replies; 7+ messages in thread
From: Rob Herring (Arm) @ 2026-07-31 22:53 UTC (permalink / raw)
  To: Hongyang Zhao
  Cc: linux-usb, Liam Girdwood, Roger Shimizu, devicetree,
	linux-kernel, Krzysztof Kozlowski, Conor Dooley,
	Greg Kroah-Hartman, Mark Brown, Heikki Krogerus


On Mon, 20 Jul 2026 16:17:11 +0800, Hongyang Zhao wrote:
> The HUSB320 is an autonomous USB Type-C Configuration Channel
> controller supporting source, sink, and dual-role port operation.
> 
> Document its two I2C addresses, active-low interrupt, optional supplies
> and enable GPIO, and managed USB-C connector.
> 
> Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>
> ---
>  .../devicetree/bindings/usb/hynetek,husb320.yaml   | 171 +++++++++++++++++++++
>  1 file changed, 171 insertions(+)
> 

Reviewed-by: Rob Herring (Arm) <robh@kernel.org>


^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver
  2026-07-20  8:17 ` [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver Hongyang Zhao
  2026-07-21 10:27   ` Heikki Krogerus
@ 2026-08-24 10:35   ` Meng Li
  1 sibling, 0 replies; 7+ messages in thread
From: Meng Li @ 2026-08-24 10:35 UTC (permalink / raw)
  To: hongyang.zhao
  Cc: broonie, conor+dt, devicetree, gregkh, heikki.krogerus, krzk+dt,
	lgirdwood, linux-kernel, linux-usb, robh, rosh, Meng Li

On Mon, Jul 20, 2026 at 04:17:12PM +0800, Hongyang Zhao wrote:
> The HUSB320 is an autonomous Configuration Channel controller which
> supports fixed source or sink operation and dual-role ports without USB
> Power Delivery.
> 
> Add a Type-C class driver for attach and detach detection, orientation
> and Rp current reporting, platform VBUS control, USB role switching,
> Try.SRC and Try.SNK, and audio and debug accessories.
> 
> Signed-off-by: Hongyang Zhao <hongyang.zhao@163.com>

I tested this driver on a Thundercomm I615 DK based on Qualcomm
QCS615, using the following device tree configuration:

&i2c3 {
	clock-frequency = <400000>;

	status = "okay";

	typec@31 {
		compatible = "hynetek,husb320";
		reg = <0x31>;

		interrupts-extended = <&tlmm 55 IRQ_TYPE_LEVEL_LOW>;
		pinctrl-0 = <&husb320_interrupt>;
		pinctrl-names = "default";

		connector {
			compatible = "usb-c-connector";
			label = "USB-C";
			vbus-supply = <&vreg_usb0_vbus>;
			data-role = "dual";
			power-role = "dual";
			typec-power-opmode = "default";
			pd-disable;

			ports {
				#address-cells = <1>;
				#size-cells = <0>;

				port@0 {
					reg = <0>;

					husb320_hs: endpoint {
						remote-endpoint = <&usb_1_hs>;
					};
				};

				port@1 {
					reg = <1>;

					husb320_ss: endpoint {
						remote-endpoint = <&usb_qmpphy_out>;
					};
				};
			};
		};
	};
};

The following tests passed:

- USB device/ADB in both connector orientations at 5 Gbit/s
- USB host with a USB 3.0 flash drive in both orientations at 5 Gbit/s
- 1 GiB read-only direct reads at 115 MB/s and 111 MB/s
- VBUS, data role and orientation switching
- No interrupt storm, USB enumeration error or I/O error was observed

Tested-by: Meng Li <meng.li_g@thundersoft.com>

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-08-24 10:41 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-07-20  8:17 [PATCH 0/3] Add Hynetek HUSB320 Type-C controller support Hongyang Zhao
2026-07-20  8:17 ` [PATCH 1/3] dt-bindings: usb: Add Hynetek HUSB320 Type-C controller Hongyang Zhao
2026-07-31 22:53   ` Rob Herring (Arm)
2026-07-20  8:17 ` [PATCH 2/3] usb: typec: Add Hynetek HUSB320 Type-C controller driver Hongyang Zhao
2026-07-21 10:27   ` Heikki Krogerus
2026-08-24 10:35   ` Meng Li
2026-07-20  8:17 ` [PATCH 3/3] MAINTAINERS: Add HUSB320 Type-C controller entry Hongyang Zhao

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®