Devicetree
 help / color / mirror / Atom feed
* Re: [PATCH 1/2] dt-bindings: usb: Add ITE IT885x support
  2026-06-15 13:47 ` [PATCH 1/2] dt-bindings: usb: Add ITE IT885x support Amber Kao
@ 2026-06-15  6:03   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-06-15  6:03 UTC (permalink / raw)
  To: Amber Kao; +Cc: robh, devicetree, conor+dt

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Low] The device tree binding defines a generic `gpios` property and redundantly maps it to the exact same hardware pin as the `interrupts` property, which is a structural anti-pattern.
- [Low] The device tree binding example uses a vendor-specific node name instead of a generic functional name.
--

commit db50e109009ce6583e099c466366e81f9dc8a5ee
Author: Amber Kao <amber.kao@ite.com.tw>

dt-bindings: usb: Add ITE IT885x support

Add device tree binding documentation for the ITE IT885x. The ITE IT885x is
an I2C-based USB Type-C Power Delivery (PD) controller.

> diff --git a/Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml b/Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml
> new file mode 100644
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml
[ ... ]
> +properties:
> +  compatible:
> +    const: ite,itepd-it885x
> +
> +  reg:
> +    maxItems: 1
> +
> +  gpios:
> +    maxItems: 1

[Severity: Low]
Does this property need a functional prefix? Device tree schemas generally
require GPIO consumers to have a prefix, such as irq-gpios, rather than
using the bare gpios property.

[ ... ]
> +examples:
> +  - |
> +    #include <dt-bindings/gpio/gpio.h>
> +    #include <dt-bindings/interrupt-controller/irq.h>
> +
> +    i2c {
> +        #address-cells = <1>;
> +        #size-cells = <0>;
> +
> +        itepd@40 {

[Severity: Low]
Should this node name use a generic functional name instead of a vendor
specific one? A generic name like typec@40 or usb-typec@40 would align
better with the devicetree specification recommendations.

> +            compatible = "ite,itepd-it885x";
> +            reg = <0x40>;
> +            gpios = <&tlmm 129 GPIO_ACTIVE_LOW>;
> +            interrupts-extended = <&tlmm 129 IRQ_TYPE_EDGE_FALLING>;

[Severity: Low]
Is it necessary to map the exact same physical pin to both gpios and
interrupts-extended here? If this is used merely to poll the interrupt line
state during probe, the hardware description shouldn't encode software
workarounds.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260615-ucsi-itepd-feature-v1-0-a826cfd0df6a@ite.com.tw?part=1

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

* Re: [PATCH 2/2] usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver
  2026-06-15 13:47 ` [PATCH 2/2] usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver Amber Kao
@ 2026-06-15  6:09   ` sashiko-bot
  0 siblings, 0 replies; 5+ messages in thread
From: sashiko-bot @ 2026-06-15  6:09 UTC (permalink / raw)
  To: Amber Kao; +Cc: conor+dt, robh, devicetree

Thank you for your contribution! Sashiko AI review found 12 potential issue(s) to consider:
- [High] DMA safety violation passing stack-allocated variables directly to `i2c_transfer()`.
- [High] Incorrect `devm` unwind ordering causes Use-After-Free of Type-C resources during workqueue flush.
- [High] Race condition during driver removal causing Use-After-Free by destroying the UCSI instance and workqueue before unregistering the interrupt callback.
- [High] Out-of-bounds array access due to unvalidated port index extracted from UCSI commands and hardware responses.
- [High] Race condition during probe where premature interrupt callback registration causes a NULL pointer dereference of the `ucsi` instance.
- [High] Uninitialized variables `ret` and `data` in `ucsi_itepd_connector_change_work` lead to undefined behavior and stack data leaks.
- [High] Missing lock protection when reading `itepd->altmode_cb` leads to data races and potential TOCTOU NULL pointer dereference.
- [High] Use of uninitialized stack memory during UCSI response parsing (`msg_in`).
- [High] Multi-byte native types (u64, u32, u16) are copied directly to/from I2C byte buffers, breaking on Big Endian architectures.
- [High] A pending initial hardware interrupt is explicitly acknowledged during probe but the event is permanently dropped.
- [Medium] `free_irq()` is called unconditionally during cleanup, even if the IRQ was never successfully requested.
- [Medium] Data races on `dp_en` and `con_change_processed` flags due to missing synchronization between workqueues.
--

commit 06a7873d268be15ee101449d82fa1a3ba9e341f7
Author: Amber Kao <amber.kao@ite.com.tw>

usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver

Add core, UCSI, and Alternate Mode support for the ITE IT885x
Type-C Power Delivery controller over I2C. The driver uses the
auxiliary bus to spawn UCSI and Alternate Mode child devices from
the main I2C core driver.

> diff --git a/drivers/usb/typec/ucsi/itepd.c b/drivers/usb/typec/ucsi/itepd.c
> new file mode 100644
> index 0000000000000..39ac1c1361572
> --- /dev/null
> +++ b/drivers/usb/typec/ucsi/itepd.c
[ ... ]
> +static int itepd_read_reg(struct itepd *itepd, u8 reg, void *data, u32 len)
> +{
> +	struct i2c_client *client = itepd->client;
> +	struct i2c_msg msg[] = {
> +		{
> +			.addr	= client->addr,
> +			.flags	= 0x0,
> +			.len	= 1,
> +			.buf	= &reg,
> +		},
> +		{
> +			.addr	= client->addr,
> +			.flags	= I2C_M_RD,
> +			.len	= len,
> +			.buf	= (u8 *)data,
> +		}
> +	};

[Severity: High]
Could this cause DMA mapping errors? The stack-allocated msg array passes
the addresses of the local variables reg and data. The I2C subsystem
requires all buffers passed to i2c_transfer to be DMA-safe. If the I2C
adapter utilizes DMA, mapping stack memory can cause cache coherency issues
or trigger a panic if CONFIG_DMA_API_DEBUG is enabled.

[ ... ]
> +static int itepd_write_reg(struct itepd *itepd, u8 reg, const void *data, u32 len)
> +{
[ ... ]
> +	buf[0] = reg;
> +	memcpy(buf + sizeof(reg), (u8 *)data, len);

[Severity: High]
Might this break on big-endian architectures? When a multi-byte native
type like u64 or u32 is passed into data and copied directly into the I2C
buffer via memcpy, the most significant bytes are placed first on a big
endian system. Should this use explicit byte-order conversions like
cpu_to_le64 or le32_to_cpu?

[ ... ]
> +int itepd_mode(struct device *dev, u8 port, u8 mux, u32 config, u32 status)
> +{
[ ... ]
> +		itepd->altmode_data[port].dp_status = status;
> +		if (itepd->altmode_cb)
> +			itepd->altmode_cb->notify(itepd->altmode_cb->priv,
> +						  &itepd->altmode_data[port]);

[Severity: High]
Is there a potential TOCTOU race condition here? The altmode callback pointer
is checked and invoked without holding the cb_lock mutex. If
itepd_altmode_remove unregisters the callback concurrently, altmode_cb
could become NULL right after the check, leading to a dereference panic.
Does itepd_hpd share this same unprotected pattern?

[ ... ]
> +static int itepd_probe(struct i2c_client *client)
> +{
[ ... ]
> +	desc = devm_gpiod_get(dev, NULL, GPIOD_IN);
> +
> +	if (IS_ERR(desc)) {
> +		dev_info(dev, "get gpios from DTS failed\n");
> +	} else {
> +		if (gpiod_get_value(desc))
> +			itepd_irq_process(itepd);
> +	}

[Severity: High]
Could this manually trigger an interrupt drop? At this point in the probe
sequence, the auxiliary devices and callbacks are not yet added. Calling
itepd_irq_process clears the pending hardware events by writing to
ITEPD_VENDOR_WC_INT, but since the callbacks are NULL, the event data is
discarded without notifying the UCSI or altmode subsystems.

[ ... ]
> +out_release_altmode_aux:
> +	if (itepd->client_mask & BIT(ITEPD_CLIENT_ALTMODE))
> +		itepd_del_aux_device(itepd->altmode_aux);
> +out_free_irq:
> +	free_irq(itepd->irq, itepd);
> +	return ret;
> +}
> +
> +static void itepd_remove(struct i2c_client *client)
> +{
> +	struct itepd *itepd = i2c_get_clientdata(client);
> +
> +	if (itepd->client_mask & BIT(ITEPD_CLIENT_UCSI))
> +		itepd_del_aux_device(itepd->ucsi_aux);
> +	if (itepd->client_mask & BIT(ITEPD_CLIENT_ALTMODE))
> +		itepd_del_aux_device(itepd->altmode_aux);
> +	free_irq(itepd->irq, itepd);
> +}

[Severity: Medium]
Is it safe to unconditionally call free_irq here? The probe function correctly
checks if itepd->irq > 0 before calling request_threaded_irq, but the error
handling and remove paths free the IRQ regardless. If the device was probed
without a valid IRQ, wouldn't this attempt to free an unassigned IRQ line
and trigger a warning?

> diff --git a/drivers/usb/typec/ucsi/itepd_altmode.c b/drivers/usb/typec/ucsi/itepd_altmode.c
> new file mode 100644
> index 0000000000000..a88ae2fb6e798
> --- /dev/null
> +++ b/drivers/usb/typec/ucsi/itepd_altmode.c
[ ... ]
> +static int itepd_altmode_probe(struct auxiliary_device *adev,
> +			       const struct auxiliary_device_id *id)
> +{
[ ... ]
> +		alt_port->ordered_wq = alloc_ordered_workqueue("itepd_altmode_%u", 0, port);
> +		if (!alt_port->ordered_wq) {
> +			fwnode_handle_put(fwnode);
> +			return -ENOMEM;
> +		}
> +
> +		ret = devm_add_action_or_reset(dev, itepd_altmode_destroy_wq,
> +					       alt_port->ordered_wq);
> +		if (ret) {
> +			fwnode_handle_put(fwnode);
> +			return ret;
> +		}
[ ... ]
> +		alt_port->typec_mux = fwnode_typec_mux_get(fwnode);
> +		if (IS_ERR(alt_port->typec_mux)) {

[Severity: High]
Does this devm initialization order create a use-after-free risk during
device removal? The action to destroy the workqueue is added before the
typec_mux, typec_retimer, and typec_switch are acquired and their put actions
registered. Since devm unwinds in LIFO order, the workqueue flush will
execute after the Type-C resources have been freed. If a worker is pending,
it might dereference the already freed typec resource pointers.

> diff --git a/drivers/usb/typec/ucsi/ucsi_itepd.c b/drivers/usb/typec/ucsi/ucsi_itepd.c
> new file mode 100644
> index 0000000000000..ea55ff7b866ee
> --- /dev/null
> +++ b/drivers/usb/typec/ucsi/ucsi_itepd.c
[ ... ]
> +static void ucsi_itepd_connector_change_work(struct work_struct *work)
> +{
> +	struct ucsi_itepd_work *worker = container_of(work, struct ucsi_itepd_work, work);
> +	struct ucsi_itepd *ucsi_itepd = worker->ucsi_itepd;
> +	u8 data[11];
> +	u8 num_vdos;
> +	u32 status;
> +	u64 cmd;
> +	int ret;
> +
> +	if (ucsi_itepd->con_change_processed[worker->port])
> +		goto out;
> +
> +	ucsi_itepd->con_change_processed[worker->port] = true;

[Severity: Medium]
Is there a data race on these array flags? The flags dp_en and
con_change_processed are modified in ucsi_itepd_connector_status, which runs
on the core system_long_wq, but they are read and modified here on the local
ordered_wq. Without mutual exclusion, could this result in skipped
evaluations or out-of-order state transitions?

> +	if (ucsi_itepd->dp_en[worker->port]) {
> +		/* UCSI_GET_ATTENTION_VDO (0x16) */
> +		cmd = UCSI_COMMAND(UCSI_GET_ATTENTION_VDO) |
> +		      UCSI_CONNECTOR_NUMBER(worker->port + 1);
> +		ret = ucsi_send_command(ucsi_itepd->ucsi, cmd, data, 11);
> +	}
> +	if (ret < 0)
> +		goto out;

[Severity: High]
Can this lead to uninitialized stack variable usage? If dp_en is false, the
block that calls ucsi_send_command is skipped, leaving ret and data
uninitialized. Depending on the garbage value of ret, it might skip the
connector change notification or proceed to read random stack values from
data to pass into itepd_hpd.

[ ... ]
> +static void ucsi_itepd_response_hook(struct ucsi_itepd *ucsi_itepd,
> +				     u32 *cci, u8 *msg_in)
> +{
> +	u8 recipient;
> +	u8 offset;
> +	struct ucsi_itepd_work *worker;
> +
> +	if (((*cci & UCSI_CCI_COMMAND_COMPLETE) == 0) &&
> +	    UCSI_CCI_CONNECTOR(*cci)) {
> +		worker = kmalloc_obj(*worker, GFP_KERNEL);
> +		if (!worker) {
> +			dev_err(ucsi_itepd->dev,
> +				"out of memory, skip attention check\n");
> +			ucsi_connector_change(ucsi_itepd->ucsi,
> +					      UCSI_CCI_CONNECTOR(*cci));
> +		} else {
> +			worker->port = UCSI_CCI_CONNECTOR(*cci) - 1;
> +			worker->ucsi_itepd = ucsi_itepd;
> +			worker->cci = *cci;

[Severity: High]
Is the worker->port index sufficiently validated? The port index is derived
directly from the hardware CCI register and can be up to 126. It is then
passed to the workqueue where it indexes bounded arrays like
con_change_processed and dp_en which are sized by ITEPD_MAX_PORTS. Would
a faulty or unexpected hardware response cause out-of-bounds memory
corruption here?

[ ... ]
> +		switch (UCSI_COMMAND(ucsi_itepd->cmd)) {
> +		case UCSI_GET_CONNECTOR_STATUS:
> +			ucsi_itepd->connected[ucsi_itepd->cmd_port] =
> +				!!(msg_in[2] & BIT(3));
> +			ucsi_itepd->orientation[ucsi_itepd->cmd_port] =
> +				FIELD_GET(BIT(6), msg_in[10]);
> +			break;

[Severity: High]
Could this read past the end of the populated response data? The driver copies
the I2C payload into a local msg_in buffer based on the length provided in
the CCI register. If the hardware responds with a shorter message, reading
msg_in[10] unconditionally evaluates uninitialized stack memory.

[ ... ]
> +static int ucsi_itepd_probe(struct auxiliary_device *adev,
> +			    const struct auxiliary_device_id *id)
> +{
[ ... ]
> +	ret = itepd_register_cb(dev, ITEPD_CLIENT_UCSI, ucsi_itepd_cb);
> +	if (ret)
> +		goto out_destroy_wq;
> +
> +	ucsi_itepd->ucsi = ucsi_create(dev, &ucsi_itepd_ops);

[Severity: High]
Does registering the callback early create a race condition? The interrupt on
the parent I2C device is already active. If an interrupt fires immediately
after itepd_register_cb, it will invoke ucsi_itepd_notify which ultimately
dereferences ucsi_itepd->ucsi. Since ucsi_create has not been called yet,
could this result in a NULL pointer dereference?

[ ... ]
> +static void ucsi_itepd_remove(struct auxiliary_device *adev)
> +{
> +	struct ucsi_itepd *ucsi_itepd = dev_get_drvdata(&adev->dev);
> +
> +	if (ucsi_itepd->ordered_wq) {
> +		flush_workqueue(ucsi_itepd->ordered_wq);
> +		destroy_workqueue(ucsi_itepd->ordered_wq);
> +	}
> +
> +	ucsi_unregister(ucsi_itepd->ucsi);
> +	ucsi_destroy(ucsi_itepd->ucsi);
> +	usleep_range(2000, 2500);
> +	itepd_register_cb(&adev->dev, ITEPD_CLIENT_UCSI, NULL);
> +}

[Severity: High]
Might this teardown sequence result in a use-after-free? The workqueue is
destroyed and the ucsi instance is freed before the callback is unregistered.
If an interrupt occurs concurrently, the handler could invoke the callback,
attempting to queue a task on the destroyed workqueue or dereference the
freed ucsi structure. Should the callback be cleared before resources are
freed?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260615-ucsi-itepd-feature-v1-0-a826cfd0df6a@ite.com.tw?part=2

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

* [PATCH 0/2] Add support for ITE IT885x USB PD controller
@ 2026-06-15 13:47 Amber Kao
  2026-06-15 13:47 ` [PATCH 1/2] dt-bindings: usb: Add ITE IT885x support Amber Kao
  2026-06-15 13:47 ` [PATCH 2/2] usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver Amber Kao
  0 siblings, 2 replies; 5+ messages in thread
From: Amber Kao @ 2026-06-15 13:47 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Jeson Yang, Yaode Fang, Bling Chiang, Eric Su,
	Doreen Lin, Heikki Krogerus
  Cc: linux-usb, devicetree, linux-kernel, Amber Kao

This series adds Device Tree bindings and driver support for the ITE IT885x
USB Type-C Power Delivery controller over I2C. The driver uses the auxiliary
bus to spawn UCSI and Alternate Mode child devices from the main I2C core
driver.

Note: This driver has not been tested on physical hardware. Runtime
verification was performed using i2c-stub emulation with force-binding
on an x86_64 virtual machine.

Testing performed:
- checkpatch.pl: no errors or warnings
- dtschema (dt_binding_check): no errors or warnings
- Sparse static analysis (C=2 W=1): no errors or warnings
- checkstack.pl: no functions exceed 512-byte stack limit on x86_64 and arm64
- Dependency check (allnoconfig + make): all dependency is been added
- Kconfig tristate: tested =m, =y, =n and allmodconfig
- Cross-compilation: x86_64, arm64, arm32, powerpc64
- CONFIG_SMP and CONFIG_PREEMPT: both on and off
- Strict warning mode (W=1 EXTRA_CFLAGS=-W): no new warnings
- Fault injection (CONFIG_FAILSLAB): error paths verified on modified kernel

Question regarding Fault Injection testing:
Since physical hardware is currently unavailable for testing, my approach to
verifying the error handling paths was to temporarily modify the driver code
(e.g., forcing a function to return -ENOMEM), run the mock tests, and then
revert the code once passed. Is this ad-hoc instrumentation approach
acceptable for submission under these constraints, or is there an officially
recommended fault injection method that the community prefers for this kind of validation?

Signed-off-by: Amber Kao <amber.kao@ite.com.tw>
---
Amber Kao (2):
      dt-bindings: usb: Add ITE IT885x support
      usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver

 .../devicetree/bindings/usb/ite,itepd-it885x.yaml  | 109 ++++
 MAINTAINERS                                        |  15 +
 drivers/usb/typec/ucsi/Kconfig                     |  15 +
 drivers/usb/typec/ucsi/Makefile                    |   1 +
 drivers/usb/typec/ucsi/itepd.c                     | 481 ++++++++++++++++++
 drivers/usb/typec/ucsi/itepd.h                     |  64 +++
 drivers/usb/typec/ucsi/itepd_altmode.c             | 438 ++++++++++++++++
 drivers/usb/typec/ucsi/ucsi_itepd.c                | 558 +++++++++++++++++++++
 8 files changed, 1681 insertions(+)
---
base-commit: 8fde5d1d47f69db6082dfa34500c27f8485389a5
change-id: 20260605-ucsi-itepd-feature-95e6dcee4fc5

Best regards,
--  
Amber Kao <amber.kao@ite.com.tw>


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

* [PATCH 1/2] dt-bindings: usb: Add ITE IT885x support
  2026-06-15 13:47 [PATCH 0/2] Add support for ITE IT885x USB PD controller Amber Kao
@ 2026-06-15 13:47 ` Amber Kao
  2026-06-15  6:03   ` sashiko-bot
  2026-06-15 13:47 ` [PATCH 2/2] usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver Amber Kao
  1 sibling, 1 reply; 5+ messages in thread
From: Amber Kao @ 2026-06-15 13:47 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Jeson Yang, Yaode Fang, Bling Chiang, Eric Su,
	Doreen Lin, Heikki Krogerus
  Cc: linux-usb, devicetree, linux-kernel, Amber Kao

Add device tree binding documentation for the ITE IT885x.
The ITE IT885x is an I2C-based USB Type-C Power Delivery (PD) controller.

Cc: Yaode Fang <Yaode.Fang@ite.com.tw>
Cc: Jeson Yang <jeson.yang@ite.com.tw>
Cc: Bling Chiang <Bling.Chiang@ite.com.tw>
Cc: Eric Su <Eric.Su@ite.com.tw>
Cc: Doreen Lin <doreen.lin@ite.com.tw>
Signed-off-by: Amber Kao <amber.kao@ite.com.tw>
---
 .../devicetree/bindings/usb/ite,itepd-it885x.yaml  | 109 +++++++++++++++++++++
 MAINTAINERS                                        |  11 +++
 2 files changed, 120 insertions(+)

diff --git a/Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml b/Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml
new file mode 100644
index 000000000000..59e4eaa32ff1
--- /dev/null
+++ b/Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml
@@ -0,0 +1,109 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/usb/ite,itepd-it885x.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ITE IT885x USB Type-C Power Delivery Controller
+
+maintainers:
+  - Jeson Yang <jeson.yang@ite.com.tw>
+
+description:
+  The ITE IT885x is an I2C-based USB Type-C Power Delivery (PD) controller.
+
+properties:
+  compatible:
+    const: ite,itepd-it885x
+
+  reg:
+    maxItems: 1
+
+  gpios:
+    maxItems: 1
+
+  interrupts:
+    maxItems: 1
+
+  wakeup-source: true
+
+  pinctrl-names:
+    minItems: 1
+
+  pinctrl-0: true
+
+  '#address-cells':
+    const: 1
+
+  '#size-cells':
+    const: 0
+
+patternProperties:
+  "^connector(@[0-9a-f]+)?$":
+    $ref: /schemas/connector/usb-connector.yaml#
+    unevaluatedProperties: false
+
+required:
+  - compatible
+  - reg
+  - interrupts
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    #include <dt-bindings/gpio/gpio.h>
+    #include <dt-bindings/interrupt-controller/irq.h>
+
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        itepd@40 {
+            compatible = "ite,itepd-it885x";
+            reg = <0x40>;
+            gpios = <&tlmm 129 GPIO_ACTIVE_LOW>;
+            interrupts-extended = <&tlmm 129 IRQ_TYPE_EDGE_FALLING>;
+            wakeup-source;
+
+            pinctrl-names = "default";
+            pinctrl-0 = <&itepd_int_default>;
+
+            #address-cells = <1>;
+            #size-cells = <0>;
+
+            connector@0 {
+                compatible = "usb-c-connector";
+                reg = <0>;
+                label = "USB-C";
+                power-role = "dual";
+                data-role = "dual";
+
+                ports {
+                    #address-cells = <1>;
+                    #size-cells = <0>;
+
+                    port@0 {
+                        reg = <0>;
+                        endpoint {
+                            remote-endpoint = <&eud_con>;
+                        };
+                    };
+
+                    port@1 {
+                        reg = <1>;
+                        endpoint {
+                            remote-endpoint = <&redriver_ss_out>;
+                        };
+                    };
+
+                    port@2 {
+                        reg = <2>;
+                        endpoint {
+                            remote-endpoint = <&fsa4480_sbu_mux>;
+                        };
+                    };
+                };
+            };
+        };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 461a3eed6129..94afe3729059 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13720,6 +13720,17 @@ T:	git https://gitlab.freedesktop.org/drm/misc/kernel.git
 F:	Documentation/devicetree/bindings/display/bridge/ite,it66121.yaml
 F:	drivers/gpu/drm/bridge/ite-it66121.c
 
+ITE IT885x TYPE-C PD CONTROLLER DRIVER
+M:	Amber Kao <amber.kao@ite.com.tw>
+M:	Jeson Yang <jeson.yang@ite.com.tw>
+R:	Yaode Fang <Yaode.Fang@ite.com.tw>
+R:	Bling Chiang <Bling.Chiang@ite.com.tw>
+R:	Eric Su <Eric.Su@ite.com.tw>
+R:	Doreen Lin <doreen.lin@ite.com.tw>
+L:	linux-usb@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml
+
 IVTV VIDEO4LINUX DRIVER
 M:	Andy Walls <awalls@md.metrocast.net>
 L:	linux-media@vger.kernel.org

-- 
2.53.0


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

* [PATCH 2/2] usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver
  2026-06-15 13:47 [PATCH 0/2] Add support for ITE IT885x USB PD controller Amber Kao
  2026-06-15 13:47 ` [PATCH 1/2] dt-bindings: usb: Add ITE IT885x support Amber Kao
@ 2026-06-15 13:47 ` Amber Kao
  2026-06-15  6:09   ` sashiko-bot
  1 sibling, 1 reply; 5+ messages in thread
From: Amber Kao @ 2026-06-15 13:47 UTC (permalink / raw)
  To: Greg Kroah-Hartman, Rob Herring, Krzysztof Kozlowski,
	Conor Dooley, Jeson Yang, Yaode Fang, Bling Chiang, Eric Su,
	Doreen Lin, Heikki Krogerus
  Cc: linux-usb, devicetree, linux-kernel, Amber Kao

Add core, UCSI, and Alternate Mode support for the ITE IT885x
Type-C Power Delivery controller over I2C. The driver uses the
auxiliary bus to spawn UCSI and Alternate Mode child devices from
the main I2C core driver.

Cc: Yaode Fang <Yaode.Fang@ite.com.tw>
Cc: Jeson Yang <jeson.yang@ite.com.tw>
Cc: Bling Chiang <Bling.Chiang@ite.com.tw>
Cc: Eric Su <Eric.Su@ite.com.tw>
Cc: Doreen Lin <doreen.lin@ite.com.tw>

Signed-off-by: Amber Kao <amber.kao@ite.com.tw>
---
 MAINTAINERS                            |   4 +
 drivers/usb/typec/ucsi/Kconfig         |  15 +
 drivers/usb/typec/ucsi/Makefile        |   1 +
 drivers/usb/typec/ucsi/itepd.c         | 481 ++++++++++++++++++++++++++++
 drivers/usb/typec/ucsi/itepd.h         |  64 ++++
 drivers/usb/typec/ucsi/itepd_altmode.c | 438 ++++++++++++++++++++++++++
 drivers/usb/typec/ucsi/ucsi_itepd.c    | 558 +++++++++++++++++++++++++++++++++
 7 files changed, 1561 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 94afe3729059..c936928a7028 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -13730,6 +13730,10 @@ R:	Doreen Lin <doreen.lin@ite.com.tw>
 L:	linux-usb@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/usb/ite,itepd-it885x.yaml
+F:	drivers/usb/typec/ucsi/itepd.c
+F:	drivers/usb/typec/ucsi/itepd.h
+F:	drivers/usb/typec/ucsi/itepd_altmode.c
+F:	drivers/usb/typec/ucsi/ucsi_itepd.c
 
 IVTV VIDEO4LINUX DRIVER
 M:	Andy Walls <awalls@md.metrocast.net>
diff --git a/drivers/usb/typec/ucsi/Kconfig b/drivers/usb/typec/ucsi/Kconfig
index 87dd992a4b9e..470070f7e217 100644
--- a/drivers/usb/typec/ucsi/Kconfig
+++ b/drivers/usb/typec/ucsi/Kconfig
@@ -104,4 +104,19 @@ config UCSI_HUAWEI_GAOKUN
 	  To compile the driver as a module, choose M here: the module will be
 	  called ucsi_huawei_gaokun.
 
+config TYPEC_UCSI_ITEPD
+	tristate "ITE IT885x Type-C PD and UCSI Driver"
+	depends on I2C
+	depends on DRM
+	select AUXILIARY_BUS
+	help
+	  This driver enables core, UCSI, and Alternate Mode support for
+	  the ITE IT885x Type-C Power Delivery controller over I2C.
+
+	  The driver uses the auxiliary bus to spawn the UCSI and Altmode
+	  child devices from the main I2C core driver.
+
+	  To compile the driver as a module, choose M here: the modules
+	  will be called itepd, ucsi_itepd and itepd_altmode.
+
 endif
diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile
index c7e38bf01350..437a23f0031c 100644
--- a/drivers/usb/typec/ucsi/Makefile
+++ b/drivers/usb/typec/ucsi/Makefile
@@ -28,3 +28,4 @@ obj-$(CONFIG_UCSI_PMIC_GLINK)		+= ucsi_glink.o
 obj-$(CONFIG_CROS_EC_UCSI)		+= cros_ec_ucsi.o
 obj-$(CONFIG_UCSI_LENOVO_YOGA_C630)	+= ucsi_yoga_c630.o
 obj-$(CONFIG_UCSI_HUAWEI_GAOKUN)	+= ucsi_huawei_gaokun.o
+obj-$(CONFIG_TYPEC_UCSI_ITEPD)		+= itepd.o ucsi_itepd.o itepd_altmode.o
diff --git a/drivers/usb/typec/ucsi/itepd.c b/drivers/usb/typec/ucsi/itepd.c
new file mode 100644
index 000000000000..39ac1c136157
--- /dev/null
+++ b/drivers/usb/typec/ucsi/itepd.c
@@ -0,0 +1,481 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026, ITE. All Rights Reserved
+ */
+#include <linux/auxiliary_bus.h>
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/gpio/consumer.h>
+
+#include "itepd.h"
+
+#define ITEPD_UCSI_VERSION_REG			(0x80)
+#define ITEPD_UCSI_CCI_REG				(0x84)
+#define ITEPD_UCSI_MSG_IN_REG			(0x88)
+#define ITEPD_UCSI_CONTROL_REG			(0x98)
+#define ITEPD_UCSI_MSG_OUT_REG			(0xA0)
+
+#define ITEPD_VENDOR_WC_INT				(0xBC)
+#define ITEPD_VENDOR_INT				(0xBD)
+	#define ITEPD_ALERT_VDM_EVENT			BIT(0)
+	#define ITEPD_ALERT_UCSI_EVENT			BIT(1)
+
+struct itepd {
+	struct device *dev;
+	struct i2c_client *client;
+	int irq;
+	struct mutex i2c_lock; /* Protects I2C read/write operations */
+	struct mutex cb_lock;  /* Protects concurrent access to callback state */
+	unsigned long client_mask;
+	struct auxiliary_device *ucsi_aux;
+	struct auxiliary_device *altmode_aux;
+	struct itepd_ucsi_cb *ucsi_cb;
+	struct itepd_altmode_cb *altmode_cb;
+
+	struct itepd_altmode_data altmode_data[ITEPD_MAX_PORTS];
+};
+
+/*
+ * ITE Read/Write Function
+ */
+
+static int itepd_read_reg(struct itepd *itepd, u8 reg, void *data, u32 len)
+{
+	struct i2c_client *client = itepd->client;
+	struct i2c_msg msg[] = {
+		{
+			.addr	= client->addr,
+			.flags	= 0x0,
+			.len	= 1,
+			.buf	= &reg,
+		},
+		{
+			.addr	= client->addr,
+			.flags	= I2C_M_RD,
+			.len	= len,
+			.buf	= (u8 *)data,
+		}
+	};
+	int ret;
+
+	ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+	if (ret < 0) {
+		dev_err(itepd->dev, "i2c_transfer read failed %d\n", ret);
+		return ret;
+	}
+
+	return 0;
+}
+
+static int itepd_write_reg(struct itepd *itepd, u8 reg, const void *data, u32 len)
+{
+	struct i2c_client *client = itepd->client;
+	unsigned char *buf;
+	struct i2c_msg msg[] = {
+		{
+			.addr	= client->addr,
+			.flags	= 0x0,
+		}
+	};
+	int ret;
+
+	buf = kzalloc(len + sizeof(reg), GFP_KERNEL);
+	if (!buf)
+		return -ENOMEM;
+
+	buf[0] = reg;
+	memcpy(buf + sizeof(reg), (u8 *)data, len);
+
+	msg[0].len = len + sizeof(reg);
+	msg[0].buf = buf;
+
+	ret = i2c_transfer(client->adapter, msg, ARRAY_SIZE(msg));
+	if (ret < 0) {
+		dev_err(itepd->dev, "i2c_transfer write failed %d\n", ret);
+		kfree(buf);
+		return ret;
+	}
+
+	kfree(buf);
+	return 0;
+}
+
+/**
+ * itepd_cmd_receive() - Receive UCSI command from ITEPD controller
+ * @dev: Pointer to the device structure
+ * @cmd: The command to be executed
+ * @val: Buffer to store the received data
+ * @val_len: Length of the buffer
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+
+int itepd_cmd_receive(struct device *dev, unsigned int cmd, void *val, size_t val_len)
+{
+	struct itepd *itepd = i2c_get_clientdata(to_i2c_client(dev->parent));
+	int ret;
+
+	if (!itepd)
+		return -ENXIO;
+
+	switch (cmd) {
+	case ITEPD_RECEIVE_UCSI_VERSION:
+		mutex_lock(&itepd->i2c_lock);
+		ret = itepd_read_reg(itepd, ITEPD_UCSI_VERSION_REG, val,
+				     min_t(size_t, val_len, 0x28));
+		mutex_unlock(&itepd->i2c_lock);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(itepd_cmd_receive);
+
+int itepd_cmd_send(struct device *dev, unsigned int cmd, const void *val, size_t val_len)
+{
+	struct itepd *itepd = i2c_get_clientdata(to_i2c_client(dev->parent));
+	int ret;
+
+	if (!itepd)
+		return -ENXIO;
+
+	switch (cmd) {
+	case ITEPD_SEND_UCSI_CONTROL:
+		mutex_lock(&itepd->i2c_lock);
+		ret = itepd_write_reg(itepd, ITEPD_UCSI_CONTROL_REG, val,
+				      min_t(size_t, val_len, 8));
+		mutex_unlock(&itepd->i2c_lock);
+		break;
+	case ITEPD_SEND_UCSI_MESSAGE_OUT:
+		mutex_lock(&itepd->i2c_lock);
+		ret = itepd_write_reg(itepd, ITEPD_UCSI_MSG_OUT_REG, val, val_len);
+		mutex_unlock(&itepd->i2c_lock);
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(itepd_cmd_send);
+
+int itepd_register_cb(struct device *dev, u8 id, void *cb)
+{
+	struct itepd *itepd = i2c_get_clientdata(to_i2c_client(dev->parent));
+
+	if (!itepd)
+		return -EPROBE_DEFER;
+
+	switch (id) {
+	case ITEPD_CLIENT_UCSI:
+		if (itepd->ucsi_aux && dev == &itepd->ucsi_aux->dev) {
+			mutex_lock(&itepd->cb_lock);
+			itepd->ucsi_cb = (struct itepd_ucsi_cb *)cb;
+			mutex_unlock(&itepd->cb_lock);
+		} else {
+			return -ENODEV;
+		}
+		break;
+	case ITEPD_CLIENT_ALTMODE:
+		if (itepd->altmode_aux && dev == &itepd->altmode_aux->dev) {
+			mutex_lock(&itepd->cb_lock);
+			itepd->altmode_cb = (struct itepd_altmode_cb *)cb;
+			mutex_unlock(&itepd->cb_lock);
+		} else {
+			return -ENODEV;
+		}
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(itepd_register_cb);
+
+int itepd_mode(struct device *dev, u8 port, u8 mux, u32 config, u32 status)
+{
+	struct itepd *itepd = i2c_get_clientdata(to_i2c_client(dev->parent));
+
+	if (!itepd)
+		return -ENXIO;
+
+	if (itepd->ucsi_aux && dev == &itepd->ucsi_aux->dev) {
+		itepd->altmode_data[port].port = port;
+		itepd->altmode_data[port].mux = mux;
+		itepd->altmode_data[port].dp_config = config;
+		itepd->altmode_data[port].dp_status = status;
+		if (itepd->altmode_cb)
+			itepd->altmode_cb->notify(itepd->altmode_cb->priv,
+						  &itepd->altmode_data[port]);
+	} else {
+		return -ENODEV;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(itepd_mode);
+
+int itepd_hpd(struct device *dev, u8 port, u32 status)
+{
+	struct itepd *itepd = i2c_get_clientdata(to_i2c_client(dev->parent));
+
+	if (!itepd)
+		return -ENXIO;
+
+	if (itepd->ucsi_aux && dev == &itepd->ucsi_aux->dev) {
+		itepd->altmode_data[port].dp_status = status;
+		if (itepd->altmode_cb)
+			itepd->altmode_cb->notify(itepd->altmode_cb->priv,
+						  &itepd->altmode_data[port]);
+	} else {
+		return -ENODEV;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(itepd_hpd);
+
+/*
+ * ITE Interrupt Function
+ */
+
+static irqreturn_t itepd_irq_process(struct itepd *itepd)
+{
+	u8 event;
+	u8 clear = 0;
+	u8 len;
+	u32 cci;
+	u8 msg_in[0x28];
+	bool notify_ucsi = false;
+	int ret;
+
+	mutex_lock(&itepd->i2c_lock);
+
+	ret = itepd_read_reg(itepd, ITEPD_VENDOR_INT, &event, 1);
+	if (ret) {
+		mutex_unlock(&itepd->i2c_lock);
+		return IRQ_HANDLED;
+	}
+
+	mutex_lock(&itepd->cb_lock);
+	if (event & ITEPD_ALERT_VDM_EVENT)
+		clear |= ITEPD_ALERT_VDM_EVENT;
+
+	if (event & ITEPD_ALERT_UCSI_EVENT) {
+		clear |= ITEPD_ALERT_UCSI_EVENT;
+		if (itepd->ucsi_cb) {
+			ret = itepd_read_reg(itepd, ITEPD_UCSI_CCI_REG, &cci, sizeof(cci));
+			if (ret)
+				goto err_mutex_unlock_cb;
+			len = itepd->ucsi_cb->get_len(itepd->ucsi_cb->priv, cci);
+
+			if (len > 0) {
+				ret = itepd_read_reg(itepd, ITEPD_UCSI_MSG_IN_REG, msg_in,
+						     min_t(size_t, len, 0x28));
+				if (ret)
+					goto err_mutex_unlock_cb;
+			}
+			notify_ucsi = true;
+		}
+	}
+
+	if (clear) {
+		ret = itepd_write_reg(itepd, ITEPD_VENDOR_WC_INT, &clear, 1);
+		if (ret)
+			goto err_mutex_unlock_cb;
+	}
+
+	if (notify_ucsi)
+		itepd->ucsi_cb->notify(itepd->ucsi_cb->priv, cci, msg_in);
+
+	mutex_unlock(&itepd->cb_lock);
+	mutex_unlock(&itepd->i2c_lock);
+
+	return IRQ_HANDLED;
+
+err_mutex_unlock_cb:
+	mutex_unlock(&itepd->cb_lock);
+	clear = (ITEPD_ALERT_VDM_EVENT | ITEPD_ALERT_UCSI_EVENT);
+	itepd_write_reg(itepd, ITEPD_VENDOR_WC_INT, &clear, 1);
+	mutex_unlock(&itepd->i2c_lock);
+	return IRQ_HANDLED;
+}
+
+static irqreturn_t itepd_irq_thread_fn(int irq, void *data)
+{
+	struct itepd *itepd = data;
+
+	return itepd_irq_process(itepd);
+}
+
+/*
+ * ITE AUX Function
+ */
+static void itepd_ucsi_aux_release(struct device *dev)
+{
+	struct auxiliary_device *adev = container_of(dev, struct auxiliary_device, dev);
+
+	of_node_put(dev->of_node);
+	kfree(adev);
+}
+
+static void itepd_altmode_aux_release(struct device *dev)
+{
+	struct auxiliary_device *adev = container_of(dev, struct auxiliary_device, dev);
+
+	of_node_put(dev->of_node);
+	kfree(adev);
+}
+
+static int itepd_add_aux_device(struct itepd *itepd,
+				struct auxiliary_device **aux_out,
+				const char *name,
+				void (*release)(struct device *))
+{
+	struct auxiliary_device *aux;
+	int ret;
+
+	aux = kzalloc_obj(*aux, GFP_KERNEL);
+	if (!aux)
+		return -ENOMEM;
+
+	aux->name = name;
+	aux->dev.parent = itepd->dev;
+	aux->dev.release = release;
+	device_set_of_node_from_dev(&aux->dev, itepd->dev);
+
+	ret = auxiliary_device_init(aux);
+	if (ret) {
+		of_node_put(aux->dev.of_node);
+		kfree(aux);
+		return ret;
+	}
+
+	ret = auxiliary_device_add(aux);
+	if (ret) {
+		auxiliary_device_uninit(aux);
+		return ret;
+	}
+
+	*aux_out = aux;
+	return 0;
+}
+
+static void itepd_del_aux_device(struct auxiliary_device *aux)
+{
+	auxiliary_device_delete(aux);
+	auxiliary_device_uninit(aux);
+}
+
+/*
+ * ITE Probe/Remove
+ */
+
+static int itepd_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct itepd *itepd;
+	const unsigned long *match_data;
+	struct gpio_desc *desc;
+	int ret;
+
+	itepd = devm_kzalloc(dev, sizeof(struct itepd), GFP_KERNEL);
+	if (!itepd)
+		return -ENOMEM;
+
+	itepd->dev = dev;
+	itepd->client = client;
+	itepd->irq = client->irq;
+	mutex_init(&itepd->i2c_lock);
+	mutex_init(&itepd->cb_lock);
+
+	match_data = (unsigned long *)of_device_get_match_data(dev);
+
+	if (!match_data)
+		return -EINVAL;
+	itepd->client_mask = *match_data;
+
+	i2c_set_clientdata(client, itepd);
+
+	if (itepd->irq > 0) {
+		ret = request_threaded_irq(itepd->irq, NULL, itepd_irq_thread_fn,
+					   IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
+					   dev_name(dev), itepd);
+		if (ret < 0) {
+			dev_err(dev, "request_threaded_irq failed - %d\n", ret);
+			return ret;
+		}
+	}
+
+	desc = devm_gpiod_get(dev, NULL, GPIOD_IN);
+
+	if (IS_ERR(desc)) {
+		dev_info(dev, "get gpios from DTS failed\n");
+	} else {
+		if (gpiod_get_value(desc))
+			itepd_irq_process(itepd);
+	}
+
+	if (itepd->client_mask & BIT(ITEPD_CLIENT_ALTMODE)) {
+		ret = itepd_add_aux_device(itepd, &itepd->altmode_aux, "altmode",
+					   itepd_altmode_aux_release);
+		if (ret)
+			goto out_free_irq;
+	}
+
+	if (itepd->client_mask & BIT(ITEPD_CLIENT_UCSI)) {
+		ret = itepd_add_aux_device(itepd, &itepd->ucsi_aux, "ucsi", itepd_ucsi_aux_release);
+		if (ret)
+			goto out_release_altmode_aux;
+	}
+
+	return 0;
+
+out_release_altmode_aux:
+	if (itepd->client_mask & BIT(ITEPD_CLIENT_ALTMODE))
+		itepd_del_aux_device(itepd->altmode_aux);
+out_free_irq:
+	free_irq(itepd->irq, itepd);
+	return ret;
+}
+
+static void itepd_remove(struct i2c_client *client)
+{
+	struct itepd *itepd = i2c_get_clientdata(client);
+
+	if (itepd->client_mask & BIT(ITEPD_CLIENT_UCSI))
+		itepd_del_aux_device(itepd->ucsi_aux);
+	if (itepd->client_mask & BIT(ITEPD_CLIENT_ALTMODE))
+		itepd_del_aux_device(itepd->altmode_aux);
+	free_irq(itepd->irq, itepd);
+}
+
+static const unsigned long itepd_rb3gen2_client_mask =
+	BIT(ITEPD_CLIENT_ALTMODE) | BIT(ITEPD_CLIENT_UCSI);
+
+static const struct of_device_id itepd_of_match_table[] = {
+	{ .compatible = "ite,itepd-it885x", .data = &itepd_rb3gen2_client_mask },
+	{}
+};
+MODULE_DEVICE_TABLE(of, itepd_of_match_table);
+
+static struct i2c_driver itepd_driver = {
+	.driver = {
+		.name = "itepd",
+		.of_match_table = itepd_of_match_table,
+	},
+	.probe = itepd_probe,
+	.remove = itepd_remove,
+};
+
+module_i2c_driver(itepd_driver);
+
+MODULE_AUTHOR("Jeson Yang <jeson.yang@ite.com.tw>");
+MODULE_DESCRIPTION("ITEPD driver for ITE Type-C PD controller");
+MODULE_LICENSE("GPL");
diff --git a/drivers/usb/typec/ucsi/itepd.h b/drivers/usb/typec/ucsi/itepd.h
new file mode 100644
index 000000000000..339b3ab0dc88
--- /dev/null
+++ b/drivers/usb/typec/ucsi/itepd.h
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2025-2026, ITE. All Rights Reserved
+ */
+#ifndef __SOC_ITE_ITEPD_H__
+#define __SOC_ITE_ITEPD_H__
+
+#define ITEPD_MAX_PORTS						2
+
+enum {
+	ITEPD_CLIENT_UCSI,
+	ITEPD_CLIENT_ALTMODE,
+};
+
+enum {
+	ITEPD_SEND_UCSI_CONTROL,
+	ITEPD_SEND_UCSI_MESSAGE_OUT,
+};
+
+enum {
+	ITEPD_RECEIVE_UCSI_VERSION,
+};
+
+enum {
+	ITEPD_USBPD_MUX_USB_0 = 0,
+	ITEPD_USBPD_MUX_USB_1,
+	ITEPD_USBPD_MUX_DP_0,
+	ITEPD_USBPD_MUX_DP_1,
+	ITEPD_USBPD_MUX_USB_DP_0,
+	ITEPD_USBPD_MUX_USB_DP_1,
+	ITEPD_USBPD_MUX_TBT_0,
+	ITEPD_USBPD_MUX_TBT_1,
+	ITEPD_USBPD_MUX_USB4_0,
+	ITEPD_USBPD_MUX_USB4_1,
+	ITEPD_USBPD_MUX_OFF
+};
+
+#define ITEPD_USBPD_MUX_FLIPPED				BIT(0)
+
+struct itepd_altmode_data {
+	u8 port;
+	u8 mux;
+	u32 dp_config;
+	u32 dp_status;
+};
+
+struct itepd_ucsi_cb {
+	u8 (*get_len)(void *priv, u32 cci);
+	void (*notify)(void *priv, u32 cci, u8 *data);
+	void *priv;
+};
+
+struct itepd_altmode_cb {
+	void (*notify)(void *priv, struct itepd_altmode_data *data);
+	void *priv;
+};
+
+int itepd_cmd_send(struct device *dev, unsigned int cmd, const void *val, size_t val_len);
+int itepd_cmd_receive(struct device *dev, unsigned int cmd, void *val, size_t val_len);
+int itepd_register_cb(struct device *dev, u8 id, void *cb);
+int itepd_mode(struct device *dev, u8 port, u8 mux, u32 config, u32 status);
+int itepd_hpd(struct device *dev, u8 port, u32 status);
+
+#endif
diff --git a/drivers/usb/typec/ucsi/itepd_altmode.c b/drivers/usb/typec/ucsi/itepd_altmode.c
new file mode 100644
index 000000000000..a88ae2fb6e79
--- /dev/null
+++ b/drivers/usb/typec/ucsi/itepd_altmode.c
@@ -0,0 +1,438 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026, ITE. All Rights Reserved
+ */
+#include <linux/auxiliary_bus.h>
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/workqueue.h>
+#include <linux/slab.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/typec_altmode.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_mux.h>
+#include <linux/usb/typec_retimer.h>
+
+#include <drm/drm_bridge.h>
+
+#include "itepd.h"
+
+struct itepd_altmode;
+
+struct itepd_altmode_port {
+	struct itepd_altmode *altmode;
+	unsigned int index;
+
+	struct workqueue_struct *ordered_wq;
+
+	struct typec_switch *typec_switch;
+	struct typec_mux *typec_mux;
+	struct typec_retimer *typec_retimer;
+	struct drm_bridge bridge;
+
+	enum typec_orientation orientation;
+};
+
+struct itepd_altmode {
+	struct device *dev;
+
+	struct itepd_altmode_port ports[ITEPD_MAX_PORTS];
+};
+
+struct itepd_altmode_work {
+	struct itepd_altmode_port *alt_port;
+	struct itepd_altmode_data data;
+	struct work_struct work;
+
+	/*
+	 * DP mode state buffers kept on the heap (inside this kmalloc'd work
+	 * item) so that itepd_altmode_dp() does not need large local variables
+	 * and avoids triggering CONFIG_FRAME_WARN / checkstack.
+	 */
+	struct typec_altmode		dp_alt;
+	struct typec_displayport_data	dp_data;
+	struct typec_mux_state		mux_state;
+	struct typec_retimer_state	retimer_state;
+};
+
+static enum typec_orientation itepd_altmode_mux_to_orientation(u8 mux)
+{
+	if (mux >= ITEPD_USBPD_MUX_OFF)
+		return TYPEC_ORIENTATION_NONE;
+	else
+		return (mux & ITEPD_USBPD_MUX_FLIPPED) ?
+			TYPEC_ORIENTATION_REVERSE : TYPEC_ORIENTATION_NORMAL;
+}
+
+static void itepd_altmode_safe(struct itepd_altmode_port *alt_port,
+			       struct itepd_altmode_work *worker)
+{
+	struct itepd_altmode *altmode = alt_port->altmode;
+	struct typec_mux_state mux_state = {};
+	struct typec_retimer_state retimer_state = {};
+	int ret;
+
+	mux_state.alt = NULL;
+	mux_state.data = NULL;
+	mux_state.mode = TYPEC_STATE_SAFE;
+
+	ret = typec_mux_set(alt_port->typec_mux, &mux_state);
+	if (ret)
+		dev_err(altmode->dev, "failed to switch mux to safe mode\n");
+
+	retimer_state.alt = NULL;
+	retimer_state.data = NULL;
+	retimer_state.mode = TYPEC_STATE_SAFE;
+
+	ret = typec_retimer_set(alt_port->typec_retimer, &retimer_state);
+	if (ret)
+		dev_err(altmode->dev, "failed to setup retimer to safe mode\n");
+}
+
+static void itepd_altmode_usb(struct itepd_altmode_port *alt_port,
+			      struct itepd_altmode_work *worker)
+{
+	struct itepd_altmode *altmode = alt_port->altmode;
+	struct typec_mux_state mux_state = {};
+	struct typec_retimer_state retimer_state = {};
+	int ret;
+
+	mux_state.alt = NULL;
+	mux_state.data = NULL;
+	mux_state.mode = TYPEC_STATE_USB;
+
+	ret = typec_mux_set(alt_port->typec_mux, &mux_state);
+	if (ret)
+		dev_err(altmode->dev, "failed to switch mux to USB\n");
+
+	retimer_state.alt = NULL;
+	retimer_state.data = NULL;
+	retimer_state.mode = TYPEC_STATE_USB;
+
+	ret = typec_retimer_set(alt_port->typec_retimer, &retimer_state);
+	if (ret)
+		dev_err(altmode->dev, "failed to setup retimer to USB\n");
+}
+
+static void itepd_altmode_dp(struct itepd_altmode_port *alt_port,
+			     struct itepd_altmode_work *worker)
+{
+	struct itepd_altmode *altmode = alt_port->altmode;
+	u32 pin_assign;
+	unsigned int mode;
+	int ret;
+
+	/* Use heap buffers in worker to avoid large stack frames. */
+	memset(&worker->dp_alt, 0, sizeof(worker->dp_alt));
+	memset(&worker->dp_data, 0, sizeof(worker->dp_data));
+	memset(&worker->mux_state, 0, sizeof(worker->mux_state));
+	memset(&worker->retimer_state, 0, sizeof(worker->retimer_state));
+
+	worker->dp_alt.svid   = USB_TYPEC_DP_SID;
+	worker->dp_alt.mode   = USB_TYPEC_DP_MODE;
+	worker->dp_alt.active = 1;
+
+	worker->dp_data.status = worker->data.dp_status &
+				 (DP_STATUS_ENABLED | DP_STATUS_HPD_STATE |
+				  DP_STATUS_IRQ_HPD);
+	worker->dp_data.conf = worker->data.dp_config & DP_CONF_PIN_ASSIGNEMENT_MASK;
+
+	pin_assign = DP_CONF_GET_PIN_ASSIGN(worker->data.dp_config);
+	if (pin_assign == BIT(DP_PIN_ASSIGN_C))
+		mode = DP_PIN_ASSIGN_C;
+	else if (pin_assign == BIT(DP_PIN_ASSIGN_D))
+		mode = DP_PIN_ASSIGN_D;
+	else
+		mode = 0; /* unknown pin assignment — fall back to safe */
+
+	worker->mux_state.alt  = &worker->dp_alt;
+	worker->mux_state.data = &worker->dp_data;
+	worker->mux_state.mode = mode ? TYPEC_MODAL_STATE(mode) : TYPEC_STATE_SAFE;
+
+	ret = typec_mux_set(alt_port->typec_mux, &worker->mux_state);
+	if (ret)
+		dev_err(altmode->dev, "failed to switch mux to DP\n");
+
+	worker->retimer_state.alt  = &worker->dp_alt;
+	worker->retimer_state.data = &worker->dp_data;
+	worker->retimer_state.mode = worker->mux_state.mode;
+
+	ret = typec_retimer_set(alt_port->typec_retimer, &worker->retimer_state);
+	if (ret)
+		dev_err(altmode->dev, "failed to setup retimer to DP\n");
+}
+
+static void itepd_altmode_worker(struct work_struct *work)
+{
+	struct itepd_altmode_work *worker =
+		container_of(work, struct itepd_altmode_work, work);
+	struct itepd_altmode_port *alt_port = worker->alt_port;
+
+	alt_port->orientation = itepd_altmode_mux_to_orientation(worker->data.mux);
+	typec_switch_set(alt_port->typec_switch, alt_port->orientation);
+
+	switch (worker->data.mux) {
+	case ITEPD_USBPD_MUX_OFF:
+		itepd_altmode_safe(alt_port, worker);
+		drm_bridge_hpd_notify(&alt_port->bridge,
+				      connector_status_disconnected);
+		break;
+
+	case ITEPD_USBPD_MUX_DP_0:
+	case ITEPD_USBPD_MUX_DP_1:
+	case ITEPD_USBPD_MUX_USB_DP_0:
+	case ITEPD_USBPD_MUX_USB_DP_1:
+		itepd_altmode_dp(alt_port, worker);
+		if (worker->data.dp_status & DP_STATUS_HPD_STATE)
+			drm_bridge_hpd_notify(&alt_port->bridge,
+					      connector_status_connected);
+		else
+			drm_bridge_hpd_notify(&alt_port->bridge,
+					      connector_status_disconnected);
+		break;
+
+	case ITEPD_USBPD_MUX_USB_0:
+	case ITEPD_USBPD_MUX_USB_1:
+	case ITEPD_USBPD_MUX_TBT_0:
+	case ITEPD_USBPD_MUX_TBT_1:
+	case ITEPD_USBPD_MUX_USB4_0:
+	case ITEPD_USBPD_MUX_USB4_1:
+		itepd_altmode_usb(alt_port, worker);
+		drm_bridge_hpd_notify(&alt_port->bridge,
+				      connector_status_disconnected);
+		break;
+
+	default:
+		dev_err(alt_port->altmode->dev,
+			"unknown mux state %u on port %u, forcing safe mode\n",
+			worker->data.mux, alt_port->index);
+		itepd_altmode_safe(alt_port, worker);
+		drm_bridge_hpd_notify(&alt_port->bridge,
+				      connector_status_disconnected);
+		break;
+	}
+
+	kfree(worker);
+}
+
+static int itepd_altmode_attach(struct drm_bridge *bridge,
+				struct drm_encoder *encoder,
+				enum drm_bridge_attach_flags flags)
+{
+	return flags & DRM_BRIDGE_ATTACH_NO_CONNECTOR ? 0 : -EINVAL;
+}
+
+static const struct drm_bridge_funcs itepd_altmode_bridge_funcs = {
+	.attach = itepd_altmode_attach,
+};
+
+static void itepd_altmode_put_retimer(void *data)
+{
+	typec_retimer_put(data);
+}
+
+static void itepd_altmode_put_mux(void *data)
+{
+	typec_mux_put(data);
+}
+
+static void itepd_altmode_put_switch(void *data)
+{
+	typec_switch_put(data);
+}
+
+static void itepd_altmode_notify(void *priv, struct itepd_altmode_data *data)
+{
+	struct itepd_altmode *altmode = priv;
+	struct itepd_altmode_port *alt_port;
+	struct itepd_altmode_work *worker;
+
+	if (data->port >= ARRAY_SIZE(altmode->ports)) {
+		dev_err(altmode->dev, "invalid connector number, skip notify\n");
+		return;
+	}
+
+	alt_port = &altmode->ports[data->port];
+	if (!alt_port->altmode)
+		return;
+
+	worker = kmalloc_obj(*worker, GFP_KERNEL);
+	if (!worker) {
+		dev_err(altmode->dev, "out of memory, skip notify\n");
+		return;
+	}
+
+	memcpy(&worker->data, data, sizeof(struct itepd_altmode_data));
+	worker->alt_port = alt_port;
+
+	INIT_WORK(&worker->work, itepd_altmode_worker);
+	queue_work(alt_port->ordered_wq, &worker->work);
+}
+
+static void itepd_altmode_destroy_wq(void *data)
+{
+	struct workqueue_struct *wq = data;
+
+	flush_workqueue(wq);
+	destroy_workqueue(wq);
+}
+
+static int itepd_altmode_probe(struct auxiliary_device *adev,
+			       const struct auxiliary_device_id *id)
+{
+	struct itepd_altmode *altmode;
+	struct itepd_altmode_port *alt_port;
+	struct itepd_altmode_cb *cb;
+	struct fwnode_handle *fwnode;
+	struct device *dev = &adev->dev;
+	u32 port;
+	int ret;
+
+	altmode = devm_kzalloc(dev, sizeof(*altmode), GFP_KERNEL);
+	if (!altmode)
+		return -ENOMEM;
+
+	cb = devm_kzalloc(dev, sizeof(*cb), GFP_KERNEL);
+	if (!cb)
+		return -ENOMEM;
+
+	altmode->dev = dev;
+
+	device_for_each_child_node(dev, fwnode) {
+		ret = fwnode_property_read_u32(fwnode, "reg", &port);
+		if (ret < 0) {
+			dev_err(dev, "missing reg property of %pOFn\n", fwnode);
+			fwnode_handle_put(fwnode);
+			return ret;
+		}
+
+		if (port >= ARRAY_SIZE(altmode->ports)) {
+			dev_warn(dev, "invalid connector number, ignoring\n");
+			continue;
+		}
+
+		if (altmode->ports[port].altmode) {
+			dev_err(dev, "multiple connector definition for port %u\n", port);
+			fwnode_handle_put(fwnode);
+			return -EINVAL;
+		}
+
+		alt_port = &altmode->ports[port];
+		alt_port->altmode = altmode;
+		alt_port->index = port;
+
+		alt_port->ordered_wq = alloc_ordered_workqueue("itepd_altmode_%u", 0, port);
+		if (!alt_port->ordered_wq) {
+			fwnode_handle_put(fwnode);
+			return -ENOMEM;
+		}
+
+		ret = devm_add_action_or_reset(dev, itepd_altmode_destroy_wq,
+					       alt_port->ordered_wq);
+		if (ret) {
+			fwnode_handle_put(fwnode);
+			return ret;
+		}
+
+		alt_port->bridge.funcs = &itepd_altmode_bridge_funcs;
+		alt_port->bridge.of_node = to_of_node(fwnode);
+		alt_port->bridge.ops = DRM_BRIDGE_OP_HPD;
+		alt_port->bridge.type = DRM_MODE_CONNECTOR_DisplayPort;
+
+		alt_port->typec_mux = fwnode_typec_mux_get(fwnode);
+		if (IS_ERR(alt_port->typec_mux)) {
+			fwnode_handle_put(fwnode);
+			return dev_err_probe(dev, PTR_ERR(alt_port->typec_mux),
+					     "failed to acquire mode-switch for port: %d\n",
+					     port);
+		}
+
+		ret = devm_add_action_or_reset(dev, itepd_altmode_put_mux,
+					       alt_port->typec_mux);
+		if (ret) {
+			fwnode_handle_put(fwnode);
+			return ret;
+		}
+
+		alt_port->typec_retimer = fwnode_typec_retimer_get(fwnode);
+		if (IS_ERR(alt_port->typec_retimer)) {
+			fwnode_handle_put(fwnode);
+			return dev_err_probe(dev, PTR_ERR(alt_port->typec_retimer),
+					     "failed to acquire retimer-switch for port: %d\n",
+					     port);
+		}
+
+		ret = devm_add_action_or_reset(dev, itepd_altmode_put_retimer,
+					       alt_port->typec_retimer);
+		if (ret) {
+			fwnode_handle_put(fwnode);
+			return ret;
+		}
+
+		alt_port->typec_switch = fwnode_typec_switch_get(fwnode);
+		if (IS_ERR(alt_port->typec_switch)) {
+			fwnode_handle_put(fwnode);
+			return dev_err_probe(dev, PTR_ERR(alt_port->typec_switch),
+					     "failed to acquire orientation-switch for port: %d\n",
+					     port);
+		}
+
+		ret = devm_add_action_or_reset(dev, itepd_altmode_put_switch,
+					       alt_port->typec_switch);
+		if (ret) {
+			fwnode_handle_put(fwnode);
+			return ret;
+		}
+	}
+
+	for (port = 0; port < ARRAY_SIZE(altmode->ports); port++) {
+		alt_port = &altmode->ports[port];
+		if (!alt_port->altmode)
+			continue;
+
+		ret = devm_drm_bridge_add(dev, &alt_port->bridge);
+		if (ret)
+			return ret;
+	}
+
+	dev_set_drvdata(dev, altmode);
+
+	cb->notify = itepd_altmode_notify;
+	cb->priv = altmode;
+
+	ret = itepd_register_cb(dev, ITEPD_CLIENT_ALTMODE, cb);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+static void itepd_altmode_remove(struct auxiliary_device *adev)
+{
+	itepd_register_cb(&adev->dev, ITEPD_CLIENT_ALTMODE, NULL);
+	/*
+	 * devm unwind handles workqueue flush/destroy and typec resource
+	 * release in reverse probe order.
+	 */
+}
+
+static const struct auxiliary_device_id itepd_altmode_id_table[] = {
+	{ .name = "itepd.altmode", },
+	{},
+};
+MODULE_DEVICE_TABLE(auxiliary, itepd_altmode_id_table);
+
+static struct auxiliary_driver itepd_altmode_driver = {
+	.name = "itepd_altmode",
+	.probe = itepd_altmode_probe,
+	.remove = itepd_altmode_remove,
+	.id_table = itepd_altmode_id_table,
+};
+
+module_auxiliary_driver(itepd_altmode_driver);
+
+MODULE_AUTHOR("Jeson Yang <jeson.yang@ite.com.tw>");
+MODULE_DESCRIPTION("USB Type-C alternate mode driver for ITE Type-C PD controller");
+MODULE_LICENSE("GPL");
diff --git a/drivers/usb/typec/ucsi/ucsi_itepd.c b/drivers/usb/typec/ucsi/ucsi_itepd.c
new file mode 100644
index 000000000000..ea55ff7b866e
--- /dev/null
+++ b/drivers/usb/typec/ucsi/ucsi_itepd.c
@@ -0,0 +1,558 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2025-2026, ITE. All Rights Reserved
+ */
+#include <linux/unaligned.h>
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/workqueue.h>
+#include <linux/usb/typec.h>
+#include <linux/usb/typec_dp.h>
+#include <linux/slab.h>
+
+#include "itepd.h"
+#include "ucsi.h"
+
+struct ucsi_itepd {
+	struct device *dev;
+	struct ucsi *ucsi;
+	struct completion complete;
+	struct mutex received_lock; /* Protects command response data:*/
+	struct workqueue_struct *ordered_wq;
+	bool connected[ITEPD_MAX_PORTS];
+	bool dp_en[ITEPD_MAX_PORTS];
+	bool con_change_processed[ITEPD_MAX_PORTS];
+	u8 cmd_port;
+	u8 resp_received;
+	u8 msg_in[0x28];
+	u8 dp_idx[ITEPD_MAX_PORTS];
+	u8 orientation[ITEPD_MAX_PORTS];
+	u8 mux[ITEPD_MAX_PORTS];
+	u32 cci;
+	u64 cmd;
+};
+
+struct ucsi_itepd_work {
+	struct ucsi_itepd *ucsi_itepd;
+	u8 port;
+	u32 cci;
+	struct work_struct work;
+};
+
+static struct ucsi_itepd *__ucsi_itepd;
+
+static void ucsi_itepd_handle_dp_altmode(struct ucsi_itepd *ucsi_itepd,
+					 u8 port, u8 *mux, u32 *config, u32 *status)
+{
+	u8 orientation = ucsi_itepd->orientation[port];
+	u8 data[10];
+	u64 cmd;
+	int ret;
+
+	cmd = UCSI_COMMAND(UCSI_GET_CURRENT_CAM) | UCSI_CONNECTOR_NUMBER(port + 1);
+	ret = ucsi_send_command(ucsi_itepd->ucsi, cmd, data, 1);
+	if (ret < 0)
+		return;
+
+	if (data[0] != ucsi_itepd->dp_idx[port])
+		return;
+
+	cmd = UCSI_COMMAND(UCSI_GET_CAM_CS) |
+	      UCSI_CONNECTOR_NUMBER(port + 1) |
+	      ((u64)(ucsi_itepd->dp_idx[port]) << 24);
+	ret = ucsi_send_command(ucsi_itepd->ucsi, cmd, data, 10);
+	if (ret < 0)
+		return;
+
+	*config = get_unaligned_le32(data + 6);
+	*status = get_unaligned_le32(data + 1);
+
+	if (DP_CONF_GET_PIN_ASSIGN(*config) == BIT(DP_PIN_ASSIGN_C)) {
+		*mux = (orientation == 1) ? ITEPD_USBPD_MUX_DP_1 : ITEPD_USBPD_MUX_DP_0;
+		ucsi_itepd->dp_en[port] = true;
+	} else if (DP_CONF_GET_PIN_ASSIGN(*config) == BIT(DP_PIN_ASSIGN_D)) {
+		*mux = (orientation == 1) ? ITEPD_USBPD_MUX_USB_DP_1 : ITEPD_USBPD_MUX_USB_DP_0;
+		ucsi_itepd->dp_en[port] = true;
+	}
+}
+
+static void ucsi_itepd_connector_partner_change(struct ucsi_itepd *ucsi_itepd,
+						u8 port, struct ucsi_connector *con)
+{
+	u8 orientation = ucsi_itepd->orientation[port];
+	u8 mux = ucsi_itepd->mux[port];
+	u32 config = 0;
+	u32 status = 0;
+
+	if (!(UCSI_CONSTAT(con, CHANGE) &
+	      (UCSI_CONSTAT_PARTNER_CHANGE | UCSI_CONSTAT_CONNECT_CHANGE)))
+		return;
+
+	if (!UCSI_CONSTAT(con, CONNECTED)) {
+		mux = ITEPD_USBPD_MUX_OFF;
+		ucsi_itepd->dp_en[port] = false;
+	} else if (UCSI_CONSTAT(con, PARTNER_FLAG_ALT_MODE)) {
+		ucsi_itepd_handle_dp_altmode(ucsi_itepd, port, &mux, &config, &status);
+	} else {
+		mux = (orientation == 1) ? ITEPD_USBPD_MUX_USB_1 : ITEPD_USBPD_MUX_USB_0;
+		ucsi_itepd->dp_en[port] = false;
+	}
+
+	if (mux != ucsi_itepd->mux[port]) {
+		itepd_mode(ucsi_itepd->dev, port, mux, config, status);
+		ucsi_itepd->mux[port] = mux;
+	}
+}
+
+static void ucsi_itepd_connector_change_work(struct work_struct *work)
+{
+	struct ucsi_itepd_work *worker = container_of(work, struct ucsi_itepd_work, work);
+	struct ucsi_itepd *ucsi_itepd = worker->ucsi_itepd;
+	u8 data[11];
+	u8 num_vdos;
+	u32 status;
+	u64 cmd;
+	int ret;
+
+	if (ucsi_itepd->con_change_processed[worker->port])
+		goto out;
+
+	ucsi_itepd->con_change_processed[worker->port] = true;
+	if (ucsi_itepd->dp_en[worker->port]) {
+		/* UCSI_GET_ATTENTION_VDO (0x16) */
+		cmd = UCSI_COMMAND(UCSI_GET_ATTENTION_VDO) |
+		      UCSI_CONNECTOR_NUMBER(worker->port + 1);
+		ret = ucsi_send_command(ucsi_itepd->ucsi, cmd, data, 11);
+	}
+	if (ret < 0)
+		goto out;
+	num_vdos = data[2] & 0x07;
+	status = get_unaligned_le32(data + 7);
+
+	if (num_vdos)
+		itepd_hpd(ucsi_itepd->dev, worker->port, status);
+
+	ucsi_connector_change(ucsi_itepd->ucsi, UCSI_CCI_CONNECTOR(worker->cci));
+out:
+	kfree(worker);
+}
+
+static void ucsi_itepd_command_hook(struct ucsi_itepd *ucsi_itepd, u64 *cmd)
+{
+	/* Translate UCSI 1.2 commands/fields to ITE PD controller (v2.1) */
+	switch (UCSI_COMMAND(*cmd)) {
+	case UCSI_SET_NOTIFICATION_ENABLE:
+		if (*cmd & UCSI_ENABLE_NTFY_CMD_COMPLETE)
+			/* Enable Attention Notification for alt. mode */
+			*cmd |= FIELD_PREP(GENMASK_ULL(32, 16), BIT(3));
+		break;
+	case UCSI_GET_PDOS:
+		*cmd &= ~GENMASK_ULL(38, 37);
+		break;
+	case UCSI_GET_ERROR_STATUS:
+		*cmd &= ~GENMASK_ULL(22, 16);
+		*cmd |= UCSI_CONNECTOR_NUMBER(ucsi_itepd->cmd_port + 1);
+		break;
+	default:
+		break;
+	}
+
+	/* Track the connector number associated with this command */
+	switch (UCSI_COMMAND(*cmd)) {
+	case UCSI_PPM_RESET:
+	case UCSI_CANCEL:
+	case UCSI_SET_NOTIFICATION_ENABLE:
+	case UCSI_GET_CAPABILITY:
+		ucsi_itepd->cmd_port = 0;
+		break;
+	case UCSI_CONNECTOR_RESET:
+	case UCSI_GET_CONNECTOR_CAPABILITY:
+	case UCSI_SET_CCOM:		/* 0x08 - SET_UOM in older specs */
+	case UCSI_SET_UOR:
+	case UCSI_SET_PDR:
+	case UCSI_GET_CAM_SUPPORTED:
+	case UCSI_GET_CURRENT_CAM:
+	case UCSI_SET_NEW_CAM:
+	case UCSI_GET_PDOS:
+	case UCSI_GET_CABLE_PROPERTY:
+	case UCSI_GET_CONNECTOR_STATUS:
+	case UCSI_SET_POWER_LEVEL:	/* 0x14 */
+	case UCSI_GET_PD_MESSAGE:	/* 0x15 */
+	case UCSI_GET_ATTENTION_VDO:	/* 0x16 */
+	case UCSI_GET_CAM_CS:		/* 0x18 */
+	case 0x19:
+	case 0x1A:
+	case 0x1B:
+	case UCSI_SET_SINK_PATH:	/* 0x1C */
+	case 0x1D:
+	case UCSI_READ_POWER_LEVEL:	/* 0x1E */
+	case 0x1F:
+		ucsi_itepd->cmd_port =
+			FIELD_GET(GENMASK(22, 16), *cmd) - 1;
+		break;
+	case UCSI_GET_ALTERNATE_MODES:
+		ucsi_itepd->cmd_port =
+			FIELD_GET(GENMASK(30, 24), *cmd) - 1;
+		break;
+	}
+
+	ucsi_itepd->cmd = *cmd;
+}
+
+static void ucsi_itepd_response_hook(struct ucsi_itepd *ucsi_itepd,
+				     u32 *cci, u8 *msg_in)
+{
+	u8 recipient;
+	u8 offset;
+	struct ucsi_itepd_work *worker;
+
+	if (((*cci & UCSI_CCI_COMMAND_COMPLETE) == 0) &&
+	    UCSI_CCI_CONNECTOR(*cci)) {
+		worker = kmalloc_obj(*worker, GFP_KERNEL);
+		if (!worker) {
+			dev_err(ucsi_itepd->dev,
+				"out of memory, skip attention check\n");
+			ucsi_connector_change(ucsi_itepd->ucsi,
+					      UCSI_CCI_CONNECTOR(*cci));
+		} else {
+			worker->port = UCSI_CCI_CONNECTOR(*cci) - 1;
+			worker->ucsi_itepd = ucsi_itepd;
+			worker->cci = *cci;
+
+			INIT_WORK(&worker->work,
+				  ucsi_itepd_connector_change_work);
+			queue_work(ucsi_itepd->ordered_wq, &worker->work);
+		}
+	}
+
+	if ((*cci & UCSI_CCI_COMMAND_COMPLETE) &&
+	    ((*cci & UCSI_CCI_ERROR) == 0)) {
+		switch (UCSI_COMMAND(ucsi_itepd->cmd)) {
+		case UCSI_GET_CONNECTOR_STATUS:
+			ucsi_itepd->connected[ucsi_itepd->cmd_port] =
+				!!(msg_in[2] & BIT(3));
+			ucsi_itepd->orientation[ucsi_itepd->cmd_port] =
+				FIELD_GET(BIT(6), msg_in[10]);
+			break;
+
+		case UCSI_GET_ALTERNATE_MODES:
+			recipient = FIELD_GET(GENMASK_ULL(18, 16),
+					      ucsi_itepd->cmd);
+			if (recipient == UCSI_RECIPIENT_CON) {
+				offset = FIELD_GET(GENMASK_ULL(39, 32),
+						   ucsi_itepd->cmd);
+				if (((struct ucsi_altmode *)msg_in)->svid ==
+				    USB_TYPEC_DP_SID) {
+					ucsi_itepd->dp_idx[ucsi_itepd->cmd_port] =
+						offset;
+				} else if (((struct ucsi_altmode *)
+					    (msg_in + 6))->svid ==
+					   USB_TYPEC_DP_SID) {
+					ucsi_itepd->dp_idx[ucsi_itepd->cmd_port] =
+						offset + 1;
+				}
+			}
+			break;
+		default:
+			break;
+		}
+	}
+}
+
+/*
+ * ITE PD notify callback
+ */
+static u8 ucsi_itepd_get_len(void *priv, u32 cci)
+{
+	if (cci & UCSI_CCI_COMMAND_COMPLETE)
+		return UCSI_CCI_LENGTH(cci);
+	return 0;
+}
+
+static void ucsi_itepd_notify(void *priv, u32 cci, u8 *data)
+{
+	struct ucsi_itepd *ucsi_itepd = (struct ucsi_itepd *)priv;
+	bool comp = false;
+	u8 msg_in[0x28];
+	u8 len = UCSI_CCI_LENGTH(cci);
+
+	memcpy(msg_in, data, min_t(u8, len, ARRAY_SIZE(msg_in)));
+	ucsi_itepd_response_hook(ucsi_itepd, &cci, msg_in);
+
+	mutex_lock(&ucsi_itepd->received_lock);
+	if (cci & UCSI_CCI_COMMAND_COMPLETE) {
+		ucsi_itepd->resp_received = 1;
+		ucsi_itepd->cci = cci;
+		memset(ucsi_itepd->msg_in, 0, ARRAY_SIZE(ucsi_itepd->msg_in));
+		if (len)
+			memcpy(ucsi_itepd->msg_in, msg_in,
+			       min_t(u8, len, ARRAY_SIZE(ucsi_itepd->msg_in)));
+		comp = true;
+	}
+	if (cci & UCSI_CCI_RESET_COMPLETE) {
+		ucsi_itepd->resp_received = 1;
+		ucsi_itepd->cci = cci;
+		memset(ucsi_itepd->msg_in, 0, ARRAY_SIZE(ucsi_itepd->msg_in));
+		comp = true;
+	}
+	mutex_unlock(&ucsi_itepd->received_lock);
+
+	if (cci & UCSI_CCI_ACK_COMPLETE)
+		comp = true;
+
+	if (comp)
+		complete(&ucsi_itepd->complete);
+}
+
+/*
+ * New ucsi_operations implementation
+ */
+
+static int ucsi_itepd_read_version(struct ucsi *ucsi, u16 *version)
+{
+	struct ucsi_itepd *ucsi_itepd = ucsi_get_drvdata(ucsi);
+
+	return itepd_cmd_receive(ucsi_itepd->dev,
+				 ITEPD_RECEIVE_UCSI_VERSION,
+				 version, sizeof(*version));
+}
+
+static int ucsi_itepd_read_cci(struct ucsi *ucsi, u32 *cci)
+{
+	struct ucsi_itepd *ucsi_itepd = ucsi_get_drvdata(ucsi);
+
+	mutex_lock(&ucsi_itepd->received_lock);
+	if (ucsi_itepd->resp_received) {
+		ucsi_itepd->resp_received = 0;
+		*cci = ucsi_itepd->cci;
+	} else {
+		*cci = 0;
+	}
+	mutex_unlock(&ucsi_itepd->received_lock);
+
+	return 0;
+}
+
+/*
+ * poll_cci: called when notifications are temporarily disabled (e.g. during
+ * PPM reset).  For this hardware we can reuse read_cci — the firmware always
+ * pushes CCI updates via the notify callback regardless.
+ */
+static int ucsi_itepd_poll_cci(struct ucsi *ucsi, u32 *cci)
+{
+	return ucsi_itepd_read_cci(ucsi, cci);
+}
+
+static int ucsi_itepd_read_message_in(struct ucsi *ucsi,
+				      void *val, size_t val_len)
+{
+	struct ucsi_itepd *ucsi_itepd = ucsi_get_drvdata(ucsi);
+
+	mutex_lock(&ucsi_itepd->received_lock);
+	memcpy(val, ucsi_itepd->msg_in,
+	       min(val_len, ARRAY_SIZE(ucsi_itepd->msg_in)));
+	mutex_unlock(&ucsi_itepd->received_lock);
+
+	return 0;
+}
+
+/*
+ * async_control: fire a command to the PPM and return immediately.
+ * The old async_write(UCSI_CONTROL, …) path, now receiving the raw
+ * u64 command directly.
+ */
+static int ucsi_itepd_async_control(struct ucsi *ucsi, u64 command)
+{
+	struct ucsi_itepd *ucsi_itepd = ucsi_get_drvdata(ucsi);
+
+	ucsi_itepd_command_hook(ucsi_itepd, &command);
+
+	mutex_lock(&ucsi_itepd->received_lock);
+	ucsi_itepd->resp_received = 0;
+	mutex_unlock(&ucsi_itepd->received_lock);
+
+	return itepd_cmd_send(ucsi_itepd->dev,
+			      ITEPD_SEND_UCSI_CONTROL,
+			      &command, sizeof(command));
+}
+
+/*
+ * sync_control: blocking command — send and wait for completion.
+ * On success the caller gets cci and data filled in.
+ */
+static int ucsi_itepd_sync_control(struct ucsi *ucsi, u64 command,
+				   u32 *cci, void *data, size_t size)
+{
+	struct ucsi_itepd *ucsi_itepd = ucsi_get_drvdata(ucsi);
+	int ret;
+
+	reinit_completion(&ucsi_itepd->complete);
+
+	ret = ucsi_itepd_async_control(ucsi, command);
+	if (ret)
+		return ret;
+
+	if (!wait_for_completion_timeout(&ucsi_itepd->complete,
+					 msecs_to_jiffies(5000)))
+		return -ETIMEDOUT;
+
+	/* Hand back CCI and (optionally) message data to the core */
+	if (cci) {
+		mutex_lock(&ucsi_itepd->received_lock);
+		*cci = ucsi_itepd->cci;
+		mutex_unlock(&ucsi_itepd->received_lock);
+	}
+	if (data && size)
+		ucsi_itepd_read_message_in(ucsi, data, size);
+
+	return 0;
+}
+
+static bool ucsi_itepd_update_altmodes(struct ucsi *ucsi, u8 recipient,
+				       struct ucsi_altmode *orig, struct ucsi_altmode *updated)
+{
+	/* No altmode squashing needed for this hardware */
+	return false;
+}
+
+static void ucsi_itepd_update_connector(struct ucsi_connector *con)
+{
+	if (con->num > ITEPD_MAX_PORTS || con->num < 1)
+		return;
+
+	con->typec_cap.orientation_aware = true;
+}
+
+static void ucsi_itepd_connector_status(struct ucsi_connector *con)
+{
+	struct ucsi_itepd *ucsi_itepd = ucsi_get_drvdata(con->ucsi);
+
+	if (con->num > ITEPD_MAX_PORTS || con->num < 1)
+		return;
+
+	if (ucsi_itepd->connected[con->num - 1])
+		typec_set_orientation(con->port,
+				      ucsi_itepd->orientation[con->num - 1] ?
+				      TYPEC_ORIENTATION_REVERSE :
+				      TYPEC_ORIENTATION_NORMAL);
+	else
+		typec_set_orientation(con->port, TYPEC_ORIENTATION_NONE);
+
+	/*
+	 * Pass the ucsi_connector (which now holds the cached bitmap status)
+	 * rather than the old struct ucsi_connector_status pointer.
+	 */
+	ucsi_itepd_connector_partner_change(ucsi_itepd, con->num - 1, con);
+
+	ucsi_itepd->con_change_processed[con->num - 1] = false;
+}
+
+static const struct ucsi_operations ucsi_itepd_ops = {
+	.read_version		= ucsi_itepd_read_version,
+	.read_cci		    = ucsi_itepd_read_cci,
+	.poll_cci		    = ucsi_itepd_poll_cci,
+	.read_message_in	= ucsi_itepd_read_message_in,
+	.sync_control		= ucsi_itepd_sync_control,
+	.async_control		= ucsi_itepd_async_control,
+	.update_altmodes	= ucsi_itepd_update_altmodes,
+	.update_connector	= ucsi_itepd_update_connector,
+	.connector_status	= ucsi_itepd_connector_status,
+};
+
+static int ucsi_itepd_probe(struct auxiliary_device *adev,
+			    const struct auxiliary_device_id *id)
+{
+	struct device *dev = &adev->dev;
+	struct ucsi_itepd *ucsi_itepd;
+	struct itepd_ucsi_cb *ucsi_itepd_cb;
+	int ret;
+
+	ucsi_itepd = devm_kzalloc(dev, sizeof(*ucsi_itepd), GFP_KERNEL);
+	if (!ucsi_itepd)
+		return -ENOMEM;
+
+	ucsi_itepd_cb = devm_kzalloc(dev, sizeof(*ucsi_itepd_cb), GFP_KERNEL);
+	if (!ucsi_itepd_cb)
+		return -ENOMEM;
+
+	ucsi_itepd->dev = dev;
+	init_completion(&ucsi_itepd->complete);
+	mutex_init(&ucsi_itepd->received_lock);
+
+	ucsi_itepd->ordered_wq = alloc_ordered_workqueue("fifo_wq", 0);
+	if (!ucsi_itepd->ordered_wq)
+		return -ENOMEM;
+
+	dev_set_drvdata(dev, ucsi_itepd);
+	__ucsi_itepd = ucsi_itepd;
+
+	ucsi_itepd_cb->get_len = ucsi_itepd_get_len;
+	ucsi_itepd_cb->notify  = ucsi_itepd_notify;
+	ucsi_itepd_cb->priv    = ucsi_itepd;
+
+	ret = itepd_register_cb(dev, ITEPD_CLIENT_UCSI, ucsi_itepd_cb);
+	if (ret)
+		goto out_destroy_wq;
+
+	ucsi_itepd->ucsi = ucsi_create(dev, &ucsi_itepd_ops);
+	if (IS_ERR(ucsi_itepd->ucsi)) {
+		ret = PTR_ERR(ucsi_itepd->ucsi);
+		goto out_unregister_cb;
+	}
+
+	ucsi_set_drvdata(ucsi_itepd->ucsi, ucsi_itepd);
+
+	ret = ucsi_register(ucsi_itepd->ucsi);
+	if (ret)
+		goto out_ucsi_destroy;
+
+	return 0;
+
+out_ucsi_destroy:
+	ucsi_destroy(ucsi_itepd->ucsi);
+out_unregister_cb:
+	itepd_register_cb(dev, ITEPD_CLIENT_UCSI, NULL);
+out_destroy_wq:
+	destroy_workqueue(ucsi_itepd->ordered_wq);
+
+	return ret;
+}
+
+static void ucsi_itepd_remove(struct auxiliary_device *adev)
+{
+	struct ucsi_itepd *ucsi_itepd = dev_get_drvdata(&adev->dev);
+
+	if (ucsi_itepd->ordered_wq) {
+		flush_workqueue(ucsi_itepd->ordered_wq);
+		destroy_workqueue(ucsi_itepd->ordered_wq);
+	}
+
+	ucsi_unregister(ucsi_itepd->ucsi);
+	ucsi_destroy(ucsi_itepd->ucsi);
+	usleep_range(2000, 2500);
+	itepd_register_cb(&adev->dev, ITEPD_CLIENT_UCSI, NULL);
+}
+
+static const struct auxiliary_device_id ucsi_itepd_id_table[] = {
+	{ .name = "itepd.ucsi", },
+	{},
+};
+MODULE_DEVICE_TABLE(auxiliary, ucsi_itepd_id_table);
+
+static struct auxiliary_driver ucsi_itepd_driver = {
+	.name     = "ucsi_itepd",
+	.probe    = ucsi_itepd_probe,
+	.remove   = ucsi_itepd_remove,
+	.id_table = ucsi_itepd_id_table,
+};
+
+module_auxiliary_driver(ucsi_itepd_driver);
+
+MODULE_AUTHOR("Jeson Yang <jeson.yang@ite.com.tw>");
+MODULE_DESCRIPTION("UCSI driver for ITE Type-C PD controller");
+MODULE_LICENSE("GPL");

-- 
2.53.0


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

end of thread, other threads:[~2026-06-15  6:09 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-15 13:47 [PATCH 0/2] Add support for ITE IT885x USB PD controller Amber Kao
2026-06-15 13:47 ` [PATCH 1/2] dt-bindings: usb: Add ITE IT885x support Amber Kao
2026-06-15  6:03   ` sashiko-bot
2026-06-15 13:47 ` [PATCH 2/2] usb: typec: ucsi: Add ITE IT885x Type-C PD controller driver Amber Kao
2026-06-15  6:09   ` sashiko-bot

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox