public inbox for linux-kernel@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH v5 0/7] USB Type-C mode selection
@ 2026-01-19 13:18 Andrei Kuchynski
  2026-01-19 13:18 ` [PATCH v5 1/7] usb: typec: Add mode_control field to port property Andrei Kuchynski
                   ` (6 more replies)
  0 siblings, 7 replies; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

This patch series introduces functionality to the USB Type-C Alternate Mode
negotiation process by implementing a priority-based selection mechanism.

Currently, DisplayPort and Thunderbolt drivers initiate a mode entry
separately within their respective probe functions. The Power Delivery
Controller (PDC) retains the ability to activate either USB4 mode or
Alternate Modes based on its internal policy.
The mode selection mechanism disables Alternate Modes to be entered by
their respective drivers and the PDC. Instead, a priority-ordered approach
is used to activate the most desirable mode.

A new `priority` field is added to the `typec_altmode` structure to store
a numerical priority value, with all priorities being unique.
If the port driver supports the mode selection feature, it must set the
`mode_selection` boolean field within the `typec_altmode` structure. This
indicates to the alternate mode drivers that they are not to activate the
altmode separately.

The mode selection process is managed by three API functions:
- `typec_mode_selection_start`
- `typec_altmode_state_update`
- `typec_mode_selection_delete`

When a partner device is connected, the `typec_mode_selection_start`
function executes the following steps:
- It compiles a priority-ordered list of Alternate Modes that are mutually
supported by both the port and the partner.
- A dedicated mode selection task is subsequently initiated on the Work
Queue.
- This task attempts to activate a mode by starting with the
highest-priority altmode on the list. Alternate modes are identified with
their SVIDs. Activation/Deactivation performed via `activate` typec_altmode
operation. The process stops as soon as a mode is successfully entered.
Otherwise, after a timeout or if an error occurs, the next alternative mode
will be activated.

The `typec_altmode_state_update` function is invoked by the port driver to
communicate the current mode of the Type-C connector.

The `typec_mode_selection_delete` function is responsible for stopping the
currently running mode selection process and releasing all associated
system resources.

Mode selection is initiated only once during partner registration, and only
if the port driver provides support for this feature. Subsequent
mode-switching activities can be managed via existing sysfs entries. Any
modifications to altmode priorities are relevant only to future
connections.

This series was tested on an Android OS device with kernel 6.19.0-rc4,
PDC: TI TPS6699, Realtek RTS5453.

Changes in V5:
- Use the no_mode_control field instead of
  con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE
- Squash previous V4 patches 3/8 and 7/8 into a single patch
- Adopt guard(mutex)(&sel->lock) in mode_selection_work_fn()
- Use dev_err instead of dev_dbg, remove the error message when exiting
  the mode
- Update typec_altmode_state_update() to only reschedule the 
  mode_selection_work_fn task if it was successfully cancelled 
- Clean up code style and alignment issues

Andrei Kuchynski (7):
  usb: typec: Add mode_control field to port property
  platform/chrome: cros_ec_typec: Set no_mode_control flag
  usb: typec: Expose alternate mode priority via sysfs
  usb: typec: Implement mode selection
  usb: typec: Introduce mode_selection bit
  usb: typec: ucsi: Support mode selection to activate altmodes
  usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi

 Documentation/ABI/testing/sysfs-class-typec |  11 +
 drivers/platform/chrome/cros_ec_typec.c     |   1 +
 drivers/usb/typec/Makefile                  |   2 +-
 drivers/usb/typec/altmodes/displayport.c    |   6 +-
 drivers/usb/typec/altmodes/thunderbolt.c    |   2 +-
 drivers/usb/typec/class.c                   | 100 ++++++-
 drivers/usb/typec/class.h                   |   3 +
 drivers/usb/typec/mode_selection.c          | 283 ++++++++++++++++++++
 drivers/usb/typec/ucsi/cros_ec_ucsi.c       |  22 ++
 drivers/usb/typec/ucsi/ucsi.c               |  12 +
 drivers/usb/typec/ucsi/ucsi.h               |   4 +
 include/linux/usb/typec.h                   |   3 +
 include/linux/usb/typec_altmode.h           |  42 +++
 13 files changed, 483 insertions(+), 8 deletions(-)
 create mode 100644 drivers/usb/typec/mode_selection.c

-- 
2.52.0.457.g6b5491de43-goog


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

* [PATCH v5 1/7] usb: typec: Add mode_control field to port property
  2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
@ 2026-01-19 13:18 ` Andrei Kuchynski
  2026-01-19 13:18 ` [PATCH v5 2/7] platform/chrome: cros_ec_typec: Set no_mode_control flag Andrei Kuchynski
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

This new field in the port properties dictates whether the Platform Policy
Manager (PPM) allows the OS Policy Manager (OPM) to change the currently
active, negotiated alternate mode.

Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
Reviewed-by: Benson Leung <bleung@chromium.org>
---
 drivers/usb/typec/class.c | 9 ++++++---
 drivers/usb/typec/class.h | 1 +
 include/linux/usb/typec.h | 2 ++
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
index c4ff4310ff58a..0f12d6120511b 100644
--- a/drivers/usb/typec/class.c
+++ b/drivers/usb/typec/class.c
@@ -457,11 +457,13 @@ static umode_t typec_altmode_attr_is_visible(struct kobject *kobj,
 					     struct attribute *attr, int n)
 {
 	struct typec_altmode *adev = to_typec_altmode(kobj_to_dev(kobj));
+	struct typec_port *port = typec_altmode2port(adev);
 
 	if (attr == &dev_attr_active.attr)
-		if (!is_typec_port(adev->dev.parent) &&
-		    (!adev->ops || !adev->ops->activate))
-			return 0444;
+		if (!is_typec_port(adev->dev.parent)) {
+			if (!port->mode_control || !adev->ops || !adev->ops->activate)
+				return 0444;
+		}
 
 	return attr->mode;
 }
@@ -2708,6 +2710,7 @@ struct typec_port *typec_register_port(struct device *parent,
 	}
 
 	port->pd = cap->pd;
+	port->mode_control = !cap->no_mode_control;
 
 	ret = device_add(&port->dev);
 	if (ret) {
diff --git a/drivers/usb/typec/class.h b/drivers/usb/typec/class.h
index db2fe96c48ff0..2e89a83c2eb70 100644
--- a/drivers/usb/typec/class.h
+++ b/drivers/usb/typec/class.h
@@ -62,6 +62,7 @@ struct typec_port {
 	struct mutex			partner_link_lock;
 
 	enum typec_orientation		orientation;
+	bool				mode_control;
 	struct typec_switch		*sw;
 	struct typec_mux		*mux;
 	struct typec_retimer		*retimer;
diff --git a/include/linux/usb/typec.h b/include/linux/usb/typec.h
index c6fd46902fce7..dbb259d885266 100644
--- a/include/linux/usb/typec.h
+++ b/include/linux/usb/typec.h
@@ -290,6 +290,7 @@ enum usb_pd_svdm_ver {
  * @prefer_role: Initial role preference (DRP ports).
  * @accessory: Supported Accessory Modes
  * @usb_capability: Supported USB Modes
+ * @no_mode_control: Ability to manage Alternate Modes
  * @fwnode: Optional fwnode of the port
  * @driver_data: Private pointer for driver specific info
  * @pd: Optional USB Power Delivery Support
@@ -307,6 +308,7 @@ struct typec_capability {
 	enum typec_accessory	accessory[TYPEC_MAX_ACCESSORY];
 	unsigned int		orientation_aware:1;
 	u8			usb_capability;
+	bool			no_mode_control;
 
 	struct fwnode_handle	*fwnode;
 	void			*driver_data;
-- 
2.52.0.457.g6b5491de43-goog


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

* [PATCH v5 2/7] platform/chrome: cros_ec_typec: Set no_mode_control flag
  2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
  2026-01-19 13:18 ` [PATCH v5 1/7] usb: typec: Add mode_control field to port property Andrei Kuchynski
@ 2026-01-19 13:18 ` Andrei Kuchynski
  2026-01-19 13:18 ` [PATCH v5 3/7] usb: typec: Expose alternate mode priority via sysfs Andrei Kuchynski
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

This flag specifies that the Embedded Controller (EC) must receive explicit
approval from the Application Processor (AP) before initiating Type-C
alternate modes or USB4 mode.

Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
Reviewed-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
Acked-by: Tzung-Bi Shih <tzungbi@kernel.org>
Reviewed-by: Benson Leung <bleung@chromium.org>
---
 drivers/platform/chrome/cros_ec_typec.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/drivers/platform/chrome/cros_ec_typec.c b/drivers/platform/chrome/cros_ec_typec.c
index b712bcff6fb26..c0806c562bb93 100644
--- a/drivers/platform/chrome/cros_ec_typec.c
+++ b/drivers/platform/chrome/cros_ec_typec.c
@@ -491,6 +491,7 @@ static int cros_typec_init_ports(struct cros_typec_data *typec)
 
 		cap->driver_data = cros_port;
 		cap->ops = &cros_typec_usb_mode_ops;
+		cap->no_mode_control = !typec->ap_driven_altmode;
 
 		cros_port->port = typec_register_port(dev, cap);
 		if (IS_ERR(cros_port->port)) {
-- 
2.52.0.457.g6b5491de43-goog


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

* [PATCH v5 3/7] usb: typec: Expose alternate mode priority via sysfs
  2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
  2026-01-19 13:18 ` [PATCH v5 1/7] usb: typec: Add mode_control field to port property Andrei Kuchynski
  2026-01-19 13:18 ` [PATCH v5 2/7] platform/chrome: cros_ec_typec: Set no_mode_control flag Andrei Kuchynski
@ 2026-01-19 13:18 ` Andrei Kuchynski
  2026-01-20 15:20   ` Heikki Krogerus
  2026-01-19 13:18 ` [PATCH v5 4/7] usb: typec: Implement mode selection Andrei Kuchynski
                   ` (3 subsequent siblings)
  6 siblings, 1 reply; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

This patch introduces a priority sysfs attribute to the USB Type-C
alternate mode port interface. This new attribute allows user-space to
configure the numeric priority of alternate modes managing their preferred
order of operation. If a new priority value conflicts with an existing
mode's priority, the priorities of the conflicting mode and all subsequent
modes are automatically incremented to ensure uniqueness.

Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
Reviewed-by: Benson Leung <bleung@chromium.org>
---
Changes in V5:
- Clean up code style and alignment issues

 Documentation/ABI/testing/sysfs-class-typec | 11 +++
 drivers/usb/typec/class.c                   | 90 ++++++++++++++++++++-
 include/linux/usb/typec_altmode.h           |  1 +
 3 files changed, 101 insertions(+), 1 deletion(-)

diff --git a/Documentation/ABI/testing/sysfs-class-typec b/Documentation/ABI/testing/sysfs-class-typec
index 38e101c17a004..737b76828b509 100644
--- a/Documentation/ABI/testing/sysfs-class-typec
+++ b/Documentation/ABI/testing/sysfs-class-typec
@@ -162,6 +162,17 @@ Description:	Lists the supported USB Modes. The default USB mode that is used
 		- usb3 (USB 3.2)
 		- usb4 (USB4)
 
+What:		/sys/class/typec/<port>/<alt-mode>/priority
+Date:		July 2025
+Contact:	Andrei Kuchynski <akuchynski@chromium.org>
+Description:
+		Displays and allows setting the priority for a specific alternate mode.
+		The priority is an integer in the range 0-255. A lower numerical value
+		indicates a higher priority (0 is the highest).
+		If the new value is already in use by another mode, the priority of the
+		conflicting mode and any subsequent modes will be incremented until they
+		are all unique.
+
 USB Type-C partner devices (eg. /sys/class/typec/port0-partner/)
 
 What:		/sys/class/typec/<port>-partner/accessory_mode
diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
index 0f12d6120511b..a48c447125184 100644
--- a/drivers/usb/typec/class.c
+++ b/drivers/usb/typec/class.c
@@ -445,11 +445,88 @@ svid_show(struct device *dev, struct device_attribute *attr, char *buf)
 }
 static DEVICE_ATTR_RO(svid);
 
+static int increment_duplicated_priority(struct device *dev, void *data)
+{
+	if (is_typec_port_altmode(dev)) {
+		struct typec_altmode **alt_target = (struct typec_altmode **)data;
+		struct typec_altmode *alt = to_typec_altmode(dev);
+
+		if (alt != *alt_target && alt->priority == (*alt_target)->priority) {
+			alt->priority++;
+			*alt_target = alt;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+static int find_duplicated_priority(struct device *dev, void *data)
+{
+	if (is_typec_port_altmode(dev)) {
+		struct typec_altmode **alt_target = (struct typec_altmode **)data;
+		struct typec_altmode *alt = to_typec_altmode(dev);
+
+		if (alt != *alt_target && alt->priority == (*alt_target)->priority)
+			return 1;
+	}
+	return 0;
+}
+
+static int typec_mode_set_priority(struct typec_altmode *alt, const u8 priority)
+{
+	struct typec_port *port = to_typec_port(alt->dev.parent);
+	const u8 old_priority = alt->priority;
+	int res = 1;
+
+	alt->priority = priority;
+	while (res) {
+		res = device_for_each_child(&port->dev, &alt, find_duplicated_priority);
+		if (res) {
+			alt->priority++;
+			if (alt->priority == 0) {
+				alt->priority = old_priority;
+				return -EOVERFLOW;
+			}
+		}
+	}
+
+	res = 1;
+	alt->priority = priority;
+	while (res)
+		res = device_for_each_child(&port->dev, &alt,
+					    increment_duplicated_priority);
+
+	return 0;
+}
+
+static ssize_t priority_store(struct device *dev,
+			      struct device_attribute *attr,
+			      const char *buf, size_t size)
+{
+	u8 val;
+	int err = kstrtou8(buf, 10, &val);
+
+	if (!err)
+		err = typec_mode_set_priority(to_typec_altmode(dev), val);
+
+	if (!err)
+		return size;
+	return err;
+}
+
+static ssize_t priority_show(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	return sysfs_emit(buf, "%u\n", to_typec_altmode(dev)->priority);
+}
+static DEVICE_ATTR_RW(priority);
+
 static struct attribute *typec_altmode_attrs[] = {
 	&dev_attr_active.attr,
 	&dev_attr_mode.attr,
 	&dev_attr_svid.attr,
 	&dev_attr_vdo.attr,
+	&dev_attr_priority.attr,
 	NULL
 };
 
@@ -459,11 +536,15 @@ static umode_t typec_altmode_attr_is_visible(struct kobject *kobj,
 	struct typec_altmode *adev = to_typec_altmode(kobj_to_dev(kobj));
 	struct typec_port *port = typec_altmode2port(adev);
 
-	if (attr == &dev_attr_active.attr)
+	if (attr == &dev_attr_active.attr) {
 		if (!is_typec_port(adev->dev.parent)) {
 			if (!port->mode_control || !adev->ops || !adev->ops->activate)
 				return 0444;
 		}
+	} else if (attr == &dev_attr_priority.attr) {
+		if (!is_typec_port(adev->dev.parent) || !port->mode_control)
+			return 0;
+	}
 
 	return attr->mode;
 }
@@ -2498,6 +2579,7 @@ typec_port_register_altmode(struct typec_port *port,
 	struct typec_altmode *adev;
 	struct typec_mux *mux;
 	struct typec_retimer *retimer;
+	int ret;
 
 	mux = typec_mux_get(&port->dev);
 	if (IS_ERR(mux))
@@ -2516,6 +2598,12 @@ typec_port_register_altmode(struct typec_port *port,
 	} else {
 		to_altmode(adev)->mux = mux;
 		to_altmode(adev)->retimer = retimer;
+
+		ret = typec_mode_set_priority(adev, 0);
+		if (ret) {
+			typec_unregister_altmode(adev);
+			return ERR_PTR(ret);
+		}
 	}
 
 	return adev;
diff --git a/include/linux/usb/typec_altmode.h b/include/linux/usb/typec_altmode.h
index 9197a4637a938..7e6c02d74b54f 100644
--- a/include/linux/usb/typec_altmode.h
+++ b/include/linux/usb/typec_altmode.h
@@ -36,6 +36,7 @@ struct typec_altmode {
 	int				mode;
 	u32				vdo;
 	unsigned int			active:1;
+	u8				priority;
 
 	char				*desc;
 	const struct typec_altmode_ops	*ops;
-- 
2.52.0.457.g6b5491de43-goog


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

* [PATCH v5 4/7] usb: typec: Implement mode selection
  2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
                   ` (2 preceding siblings ...)
  2026-01-19 13:18 ` [PATCH v5 3/7] usb: typec: Expose alternate mode priority via sysfs Andrei Kuchynski
@ 2026-01-19 13:18 ` Andrei Kuchynski
  2026-01-20 15:24   ` Heikki Krogerus
  2026-01-19 13:18 ` [PATCH v5 5/7] usb: typec: Introduce mode_selection bit Andrei Kuchynski
                   ` (2 subsequent siblings)
  6 siblings, 1 reply; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

The mode selection process is controlled by the following API functions,
which allow to initiate and complete mode entry based on the priority of
each mode:

`typec_mode_selection_start` function compiles a priority list of supported
Alternate Modes.
`typec_altmode_state_update` function is invoked by the port driver to
communicate the current mode of the Type-C connector.
`typec_mode_selection_delete` function stops the currently running mode
selection process and releases all associated system resources.

`mode_selection_work_fn` task attempts to activate modes. The process stops
on success; otherwise, it proceeds to the next mode after a timeout or
error.

Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
---
Changes in V5:
- Adopt guard(mutex)(&sel->lock) in mode_selection_work_fn()
- Use dev_err instead of dev_dbg, remove the error message when exiting
  the mode
- Update typec_altmode_state_update() to only reschedule the
  mode_selection_work_fn task if it was successfully cancelled
- Clean up code style and alignment issues

 drivers/usb/typec/Makefile         |   2 +-
 drivers/usb/typec/class.h          |   2 +
 drivers/usb/typec/mode_selection.c | 283 +++++++++++++++++++++++++++++
 include/linux/usb/typec_altmode.h  |  40 ++++
 4 files changed, 326 insertions(+), 1 deletion(-)
 create mode 100644 drivers/usb/typec/mode_selection.c

diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
index 7a368fea61bc9..8a6a1c663eb69 100644
--- a/drivers/usb/typec/Makefile
+++ b/drivers/usb/typec/Makefile
@@ -1,6 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0
 obj-$(CONFIG_TYPEC)		+= typec.o
-typec-y				:= class.o mux.o bus.o pd.o retimer.o
+typec-y				:= class.o mux.o bus.o pd.o retimer.o mode_selection.o
 typec-$(CONFIG_ACPI)		+= port-mapper.o
 obj-$(CONFIG_TYPEC)		+= altmodes/
 obj-$(CONFIG_TYPEC_TCPM)	+= tcpm/
diff --git a/drivers/usb/typec/class.h b/drivers/usb/typec/class.h
index 2e89a83c2eb70..d3435936ee7c8 100644
--- a/drivers/usb/typec/class.h
+++ b/drivers/usb/typec/class.h
@@ -9,6 +9,7 @@
 struct typec_mux;
 struct typec_switch;
 struct usb_device;
+struct mode_selection;
 
 struct typec_plug {
 	struct device			dev;
@@ -39,6 +40,7 @@ struct typec_partner {
 	u8				usb_capability;
 
 	struct usb_power_delivery	*pd;
+	struct mode_selection	*sel;
 
 	void (*attach)(struct typec_partner *partner, struct device *dev);
 	void (*deattach)(struct typec_partner *partner, struct device *dev);
diff --git a/drivers/usb/typec/mode_selection.c b/drivers/usb/typec/mode_selection.c
new file mode 100644
index 0000000000000..a95b31e21b528
--- /dev/null
+++ b/drivers/usb/typec/mode_selection.c
@@ -0,0 +1,283 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2025 Google LLC.
+ */
+
+#include <linux/types.h>
+#include <linux/list_sort.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/workqueue.h>
+#include <linux/usb/typec_altmode.h>
+
+#include "class.h"
+
+/**
+ * struct mode_state - State tracking for a specific Type-C alternate mode
+ * @svid: Standard or Vendor ID of the Alternate Mode
+ * @priority: Mode priority
+ * @error: Outcome of the last attempt to enter the mode
+ * @list: List head to link this mode state into a prioritized list
+ */
+struct mode_state {
+	u16 svid;
+	u8 priority;
+	int error;
+	struct list_head list;
+};
+
+/**
+ * struct mode_selection - Manages the selection and state of Alternate Modes
+ * @mode_list: Prioritized list of available Alternate Modes
+ * @lock: Mutex to protect mode_list
+ * @work: Work structure
+ * @partner: Handle to the Type-C partner device
+ * @active_svid: svid of currently active mode
+ * @timeout: Timeout for a mode entry attempt, ms
+ * @delay: Delay between mode entry/exit attempts, ms
+ */
+struct mode_selection {
+	struct list_head mode_list;
+	/* Protects the mode_list*/
+	struct mutex lock;
+	struct delayed_work work;
+	struct typec_partner *partner;
+	u16 active_svid;
+	unsigned int timeout;
+	unsigned int delay;
+};
+
+/**
+ * struct mode_order - Mode activation tracking
+ * @svid: Standard or Vendor ID of the Alternate Mode
+ * @enter: Flag indicating if the driver is currently attempting to enter or
+ * exit the mode
+ * @result: Outcome of the attempt to activate the mode
+ */
+struct mode_order {
+	u16 svid;
+	int enter;
+	int result;
+};
+
+static int activate_altmode(struct device *dev, void *data)
+{
+	if (is_typec_partner_altmode(dev)) {
+		struct typec_altmode *alt = to_typec_altmode(dev);
+		struct mode_order *order = (struct mode_order *)data;
+
+		if (order->svid == alt->svid) {
+			if (alt->ops && alt->ops->activate)
+				order->result = alt->ops->activate(alt, order->enter);
+			else
+				order->result = -EOPNOTSUPP;
+			return 1;
+		}
+	}
+	return 0;
+}
+
+static int mode_selection_activate(struct mode_selection *sel,
+				   const u16 svid, const int enter)
+
+	__must_hold(&sel->lock)
+{
+	struct mode_order order = {.svid = svid, .enter = enter, .result = -ENODEV};
+
+	/*
+	 * The port driver may acquire its internal mutex during alternate mode
+	 * activation. Since this is the same mutex that may be held during the
+	 * execution of typec_altmode_state_update(), it is crucial to release
+	 * sel->mutex before activation to avoid potential deadlock.
+	 * Note that sel->mode_list must remain invariant throughout this unlocked
+	 * interval.
+	 */
+	mutex_unlock(&sel->lock);
+	device_for_each_child(&sel->partner->dev, &order, activate_altmode);
+	mutex_lock(&sel->lock);
+
+	return order.result;
+}
+
+static void mode_list_clean(struct mode_selection *sel)
+{
+	struct mode_state *ms, *tmp;
+
+	list_for_each_entry_safe(ms, tmp, &sel->mode_list, list) {
+		list_del(&ms->list);
+		kfree(ms);
+	}
+}
+
+/**
+ * mode_selection_work_fn() - Alternate mode activation task
+ * @work: work structure
+ *
+ * - If the Alternate Mode currently prioritized at the top of the list is already
+ * active, the entire selection process is considered finished.
+ * - If a different Alternate Mode is currently active, the system must exit that
+ * active mode first before attempting any new entry.
+ *
+ * The function then checks the result of the attempt to entre the current mode,
+ * stored in the `ms->error` field:
+ * - if the attempt FAILED, the mode is deactivated and removed from the list.
+ * - `ms->error` value of 0 signifies that the mode has not yet been activated.
+ *
+ * Once successfully activated, the task is scheduled for subsequent entry after
+ * a timeout period. The alternate mode driver is expected to call back with the
+ * actual mode entry result via `typec_altmode_state_update()`.
+ */
+static void mode_selection_work_fn(struct work_struct *work)
+{
+	struct mode_selection *sel = container_of(work,
+				struct mode_selection, work.work);
+	struct mode_state *ms;
+	unsigned int delay = sel->delay;
+	int result;
+
+	guard(mutex)(&sel->lock);
+
+	ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
+	if (!ms)
+		return;
+
+	if (sel->active_svid == ms->svid) {
+		dev_dbg(&sel->partner->dev, "%x altmode is active\n", ms->svid);
+		mode_list_clean(sel);
+	} else if (sel->active_svid != 0) {
+		result = mode_selection_activate(sel, sel->active_svid, 0);
+		if (result)
+			mode_list_clean(sel);
+		else
+			sel->active_svid = 0;
+	} else if (ms->error) {
+		dev_err(&sel->partner->dev, "%x: entry error %pe\n",
+			ms->svid, ERR_PTR(ms->error));
+		mode_selection_activate(sel, ms->svid, 0);
+		list_del(&ms->list);
+		kfree(ms);
+	} else {
+		result = mode_selection_activate(sel, ms->svid, 1);
+		if (result) {
+			dev_err(&sel->partner->dev, "%x: activation error %pe\n",
+				ms->svid, ERR_PTR(result));
+			list_del(&ms->list);
+			kfree(ms);
+		} else {
+			delay = sel->timeout;
+			ms->error = -ETIMEDOUT;
+		}
+	}
+
+	if (!list_empty(&sel->mode_list))
+		schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
+}
+
+void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
+				const int error)
+{
+	struct mode_selection *sel = partner->sel;
+	struct mode_state *ms;
+
+	if (sel) {
+		mutex_lock(&sel->lock);
+		ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
+		if (ms && ms->svid == svid) {
+			ms->error = error;
+			if (cancel_delayed_work(&sel->work))
+				schedule_delayed_work(&sel->work, 0);
+		}
+		if (!error)
+			sel->active_svid = svid;
+		else
+			sel->active_svid = 0;
+		mutex_unlock(&sel->lock);
+	}
+}
+EXPORT_SYMBOL_GPL(typec_altmode_state_update);
+
+static int compare_priorities(void *priv,
+			      const struct list_head *a, const struct list_head *b)
+{
+	const struct mode_state *msa = container_of(a, struct mode_state, list);
+	const struct mode_state *msb = container_of(b, struct mode_state, list);
+
+	if (msa->priority < msb->priority)
+		return -1;
+	return 1;
+}
+
+static int altmode_add_to_list(struct device *dev, void *data)
+{
+	if (is_typec_partner_altmode(dev)) {
+		struct list_head *list = (struct list_head *)data;
+		struct typec_altmode *altmode = to_typec_altmode(dev);
+		const struct typec_altmode *pdev = typec_altmode_get_partner(altmode);
+		struct mode_state *ms;
+
+		if (pdev && altmode->ops && altmode->ops->activate) {
+			ms = kzalloc(sizeof(*ms), GFP_KERNEL);
+			if (!ms)
+				return -ENOMEM;
+			ms->svid = pdev->svid;
+			ms->priority = pdev->priority;
+			INIT_LIST_HEAD(&ms->list);
+			list_add_tail(&ms->list, list);
+		}
+	}
+	return 0;
+}
+
+int typec_mode_selection_start(struct typec_partner *partner,
+			       const unsigned int delay, const unsigned int timeout)
+{
+	struct mode_selection *sel;
+	int ret;
+
+	if (partner->usb_mode == USB_MODE_USB4)
+		return -EBUSY;
+
+	if (partner->sel)
+		return -EALREADY;
+
+	sel = kzalloc(sizeof(*sel), GFP_KERNEL);
+	if (!sel)
+		return -ENOMEM;
+
+	INIT_LIST_HEAD(&sel->mode_list);
+
+	ret = device_for_each_child(&partner->dev, &sel->mode_list,
+				    altmode_add_to_list);
+
+	if (ret || list_empty(&sel->mode_list)) {
+		mode_list_clean(sel);
+		kfree(sel);
+		return ret;
+	}
+
+	list_sort(NULL, &sel->mode_list, compare_priorities);
+	sel->partner = partner;
+	sel->delay = delay;
+	sel->timeout = timeout;
+	mutex_init(&sel->lock);
+	INIT_DELAYED_WORK(&sel->work, mode_selection_work_fn);
+	schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
+	partner->sel = sel;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(typec_mode_selection_start);
+
+void typec_mode_selection_delete(struct typec_partner *partner)
+{
+	struct mode_selection *sel = partner->sel;
+
+	if (sel) {
+		partner->sel = NULL;
+		cancel_delayed_work_sync(&sel->work);
+		mode_list_clean(sel);
+		mutex_destroy(&sel->lock);
+		kfree(sel);
+	}
+}
+EXPORT_SYMBOL_GPL(typec_mode_selection_delete);
diff --git a/include/linux/usb/typec_altmode.h b/include/linux/usb/typec_altmode.h
index 7e6c02d74b54f..70026f5f8f997 100644
--- a/include/linux/usb/typec_altmode.h
+++ b/include/linux/usb/typec_altmode.h
@@ -240,4 +240,44 @@ void typec_altmode_unregister_driver(struct typec_altmode_driver *drv);
 	module_driver(__typec_altmode_driver, typec_altmode_register_driver, \
 		      typec_altmode_unregister_driver)
 
+/**
+ * typec_mode_selection_start - Start an alternate mode selection process
+ * @partner: Handle to the Type-C partner device
+ * @delay: Delay between mode entry/exit attempts, ms
+ * @timeout: Timeout for a mode entry attempt, ms
+ *
+ * This function initiates the process of attempting to enter an Alternate Mode
+ * supported by the connected Type-C partner.
+ * Returns 0 on success, or a negative error code on failure.
+ */
+int typec_mode_selection_start(struct typec_partner *partner,
+			       const unsigned int delay, const unsigned int timeout);
+
+/**
+ * typec_altmode_state_update - Report the current status of an Alternate Mode
+ * negotiation
+ * @partner: Handle to the Type-C partner device
+ * @svid: Standard or Vendor ID of the Alternate Mode. A value of 0 should be
+ * passed if no mode is currently active
+ * @result: Result of the entry operation. This should be 0 on success, or a
+ * negative error code if the negotiation failed
+ *
+ * This function should be called by an Alternate Mode driver to report the
+ * result of an asynchronous alternate mode entry request. It signals what the
+ * current active SVID is (or 0 if none) and the success or failure status of
+ * the last attempt.
+ */
+void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
+				const int result);
+
+/**
+ * typec_mode_selection_delete - Delete an alternate mode selection instance
+ * @partner: Handle to the Type-C partner device.
+ *
+ * This function cancels a pending alternate mode selection request that was
+ * previously started with typec_mode_selection_start().
+ * This is typically called when the partner disconnects.
+ */
+void typec_mode_selection_delete(struct typec_partner *partner);
+
 #endif /* __USB_TYPEC_ALTMODE_H */
-- 
2.52.0.457.g6b5491de43-goog


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

* [PATCH v5 5/7] usb: typec: Introduce mode_selection bit
  2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
                   ` (3 preceding siblings ...)
  2026-01-19 13:18 ` [PATCH v5 4/7] usb: typec: Implement mode selection Andrei Kuchynski
@ 2026-01-19 13:18 ` Andrei Kuchynski
  2026-01-20 15:25   ` Heikki Krogerus
  2026-01-19 13:18 ` [PATCH v5 6/7] usb: typec: ucsi: Support mode selection to activate altmodes Andrei Kuchynski
  2026-01-19 13:18 ` [PATCH v5 7/7] usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi Andrei Kuchynski
  6 siblings, 1 reply; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

The port driver sets this bit for an alternate mode description to indicate
support for the mode selection feature. Once set, individual Alt Mode
drivers will no longer attempt to activate their respective modes within
their probe functions. This prevents race conditions and non-prioritized
activation.
The bit is not set by default. If left unset, the system retains the
current behavior where Alt Mode drivers manage their own activation logic.

Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
---
 drivers/usb/typec/altmodes/displayport.c | 6 ++++--
 drivers/usb/typec/altmodes/thunderbolt.c | 2 +-
 drivers/usb/typec/class.c                | 1 +
 include/linux/usb/typec.h                | 1 +
 include/linux/usb/typec_altmode.h        | 1 +
 5 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/drivers/usb/typec/altmodes/displayport.c b/drivers/usb/typec/altmodes/displayport.c
index d96ab106a980b..d185688a16b13 100644
--- a/drivers/usb/typec/altmodes/displayport.c
+++ b/drivers/usb/typec/altmodes/displayport.c
@@ -804,8 +804,10 @@ int dp_altmode_probe(struct typec_altmode *alt)
 	if (plug)
 		typec_altmode_set_drvdata(plug, dp);
 
-	dp->state = plug ? DP_STATE_ENTER_PRIME : DP_STATE_ENTER;
-	schedule_work(&dp->work);
+	if (!alt->mode_selection) {
+		dp->state = plug ? DP_STATE_ENTER_PRIME : DP_STATE_ENTER;
+		schedule_work(&dp->work);
+	}
 
 	return 0;
 }
diff --git a/drivers/usb/typec/altmodes/thunderbolt.c b/drivers/usb/typec/altmodes/thunderbolt.c
index 6eadf7835f8f6..c4c5da6154da9 100644
--- a/drivers/usb/typec/altmodes/thunderbolt.c
+++ b/drivers/usb/typec/altmodes/thunderbolt.c
@@ -307,7 +307,7 @@ static int tbt_altmode_probe(struct typec_altmode *alt)
 	typec_altmode_set_drvdata(alt, tbt);
 	typec_altmode_set_ops(alt, &tbt_altmode_ops);
 
-	if (tbt_ready(alt)) {
+	if (!alt->mode_selection && tbt_ready(alt)) {
 		if (tbt->plug[TYPEC_PLUG_SOP_P])
 			tbt->state = TBT_STATE_SOP_P_ENTER;
 		else if (tbt->plug[TYPEC_PLUG_SOP_PP])
diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
index a48c447125184..dbba53f024977 100644
--- a/drivers/usb/typec/class.c
+++ b/drivers/usb/typec/class.c
@@ -655,6 +655,7 @@ typec_register_altmode(struct device *parent,
 	alt->adev.svid = desc->svid;
 	alt->adev.mode = desc->mode;
 	alt->adev.vdo = desc->vdo;
+	alt->adev.mode_selection = desc->mode_selection;
 	alt->roles = desc->roles;
 	alt->id = id;
 
diff --git a/include/linux/usb/typec.h b/include/linux/usb/typec.h
index dbb259d885266..d61ec38216fa9 100644
--- a/include/linux/usb/typec.h
+++ b/include/linux/usb/typec.h
@@ -155,6 +155,7 @@ struct typec_altmode_desc {
 	/* Only used with ports */
 	enum typec_port_data	roles;
 	bool			inactive;
+	bool			mode_selection;
 };
 
 void typec_partner_set_pd_revision(struct typec_partner *partner, u16 pd_revision);
diff --git a/include/linux/usb/typec_altmode.h b/include/linux/usb/typec_altmode.h
index 70026f5f8f997..0513d333b7977 100644
--- a/include/linux/usb/typec_altmode.h
+++ b/include/linux/usb/typec_altmode.h
@@ -37,6 +37,7 @@ struct typec_altmode {
 	u32				vdo;
 	unsigned int			active:1;
 	u8				priority;
+	bool			mode_selection;
 
 	char				*desc;
 	const struct typec_altmode_ops	*ops;
-- 
2.52.0.457.g6b5491de43-goog


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

* [PATCH v5 6/7] usb: typec: ucsi: Support mode selection to activate altmodes
  2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
                   ` (4 preceding siblings ...)
  2026-01-19 13:18 ` [PATCH v5 5/7] usb: typec: Introduce mode_selection bit Andrei Kuchynski
@ 2026-01-19 13:18 ` Andrei Kuchynski
  2026-01-20 15:26   ` Heikki Krogerus
  2026-01-19 13:18 ` [PATCH v5 7/7] usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi Andrei Kuchynski
  6 siblings, 1 reply; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

If the ucsi port driver supports modes selection, it should implement
`add_partner_altmodes` and `remove_partner_altmodes` ucsi operations. With
these operations the driver can manage the mode selection process.
Once partner altmodes are registered, `add_partner_altmodes` is called to
start the mode selection. When the partner is unregistered,
`remove_partner_altmodes` is supposed to stop any ongoing processes and
clean up the resources.

`typec_altmode_state_update` informes mode selection about the current mode
of the Type-C connector.

Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
---
Changes in V5:
- Use the no_mode_control field instead of
  con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE
- Squash previous V4 patches 3/8 and 7/8 into a single patch

 drivers/usb/typec/ucsi/ucsi.c | 12 ++++++++++++
 drivers/usb/typec/ucsi/ucsi.h |  4 ++++
 2 files changed, 16 insertions(+)

diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c
index a7b388dc7fa0f..251990475faa7 100644
--- a/drivers/usb/typec/ucsi/ucsi.c
+++ b/drivers/usb/typec/ucsi/ucsi.c
@@ -314,6 +314,7 @@ void ucsi_altmode_update_active(struct ucsi_connector *con)
 {
 	const struct typec_altmode *altmode = NULL;
 	u64 command;
+	u16 svid = 0;
 	int ret;
 	u8 cur;
 	int i;
@@ -335,6 +336,10 @@ void ucsi_altmode_update_active(struct ucsi_connector *con)
 	for (i = 0; con->partner_altmode[i]; i++)
 		typec_altmode_update_active(con->partner_altmode[i],
 					    con->partner_altmode[i] == altmode);
+
+	if (altmode)
+		svid = altmode->svid;
+	typec_altmode_state_update(con->partner, svid, 0);
 }
 
 static int ucsi_altmode_next_mode(struct typec_altmode **alt, u16 svid)
@@ -609,6 +614,8 @@ static int ucsi_register_altmodes(struct ucsi_connector *con, u8 recipient)
 			desc.vdo = alt[j].mid;
 			desc.svid = alt[j].svid;
 			desc.roles = TYPEC_PORT_DRD;
+			desc.mode_selection = con->ucsi->ops->add_partner_altmodes &&
+					!con->typec_cap.no_mode_control;
 
 			ret = ucsi_register_altmode(con, &desc, recipient);
 			if (ret)
@@ -831,6 +838,8 @@ static int ucsi_check_altmodes(struct ucsi_connector *con)
 	if (con->partner_altmode[0]) {
 		num_partner_am = ucsi_get_num_altmode(con->partner_altmode);
 		typec_partner_set_num_altmodes(con->partner, num_partner_am);
+		if (con->ucsi->ops->add_partner_altmodes)
+			con->ucsi->ops->add_partner_altmodes(con);
 		ucsi_altmode_update_active(con);
 		return 0;
 	} else {
@@ -1119,6 +1128,8 @@ static void ucsi_unregister_partner(struct ucsi_connector *con)
 		return;
 
 	typec_set_mode(con->port, TYPEC_STATE_SAFE);
+	if (con->ucsi->ops->remove_partner_altmodes)
+		con->ucsi->ops->remove_partner_altmodes(con);
 
 	typec_partner_set_usb_power_delivery(con->partner, NULL);
 	ucsi_unregister_partner_pdos(con);
@@ -1659,6 +1670,7 @@ static int ucsi_register_port(struct ucsi *ucsi, struct ucsi_connector *con)
 
 	cap->driver_data = con;
 	cap->ops = &ucsi_ops;
+	cap->no_mode_control = !(con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE);
 
 	if (ucsi->version >= UCSI_VERSION_2_0)
 		con->typec_cap.orientation_aware = true;
diff --git a/drivers/usb/typec/ucsi/ucsi.h b/drivers/usb/typec/ucsi/ucsi.h
index 410389ef173ab..4797b4aa1e35b 100644
--- a/drivers/usb/typec/ucsi/ucsi.h
+++ b/drivers/usb/typec/ucsi/ucsi.h
@@ -70,6 +70,8 @@ struct dentry;
  * @update_altmodes: Squashes duplicate DP altmodes
  * @update_connector: Update connector capabilities before registering
  * @connector_status: Updates connector status, called holding connector lock
+ * @add_partner_altmodes: Start mode selection
+ * @remove_partner_altmodes: Clean mode selection
  *
  * Read and write routines for UCSI interface. @sync_write must wait for the
  * Command Completion Event from the PPM before returning, and @async_write must
@@ -88,6 +90,8 @@ struct ucsi_operations {
 				struct ucsi_altmode *updated);
 	void (*update_connector)(struct ucsi_connector *con);
 	void (*connector_status)(struct ucsi_connector *con);
+	void (*add_partner_altmodes)(struct ucsi_connector *con);
+	void (*remove_partner_altmodes)(struct ucsi_connector *con);
 };
 
 struct ucsi *ucsi_create(struct device *dev, const struct ucsi_operations *ops);
-- 
2.52.0.457.g6b5491de43-goog


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

* [PATCH v5 7/7] usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi
  2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
                   ` (5 preceding siblings ...)
  2026-01-19 13:18 ` [PATCH v5 6/7] usb: typec: ucsi: Support mode selection to activate altmodes Andrei Kuchynski
@ 2026-01-19 13:18 ` Andrei Kuchynski
  2026-01-20 15:27   ` Heikki Krogerus
  6 siblings, 1 reply; 13+ messages in thread
From: Andrei Kuchynski @ 2026-01-19 13:18 UTC (permalink / raw)
  To: Heikki Krogerus, Abhishek Pandit-Subedi, Benson Leung,
	Jameson Thies, linux-usb, linux-kernel, chrome-platform
  Cc: Tzung-Bi Shih, Guenter Roeck, Greg Kroah-Hartman,
	Dmitry Baryshkov, Łukasz Bartosik, Pooja Katiyar,
	Johan Hovold, Hsin-Te Yuan, Madhu M, Venkat Jayaraman,
	Andrei Kuchynski

The mode selection sequence is initiated by the driver after all partner
alternate modes have been successfully registered.
When a partner is disconnected, the driver also stops the mode selection
process and releases resources via `typec_mode_selection_delete`.

Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
---
Changes in V5:
- Use the no_mode_control field instead of
  con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE

 drivers/usb/typec/ucsi/cros_ec_ucsi.c | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/drivers/usb/typec/ucsi/cros_ec_ucsi.c b/drivers/usb/typec/ucsi/cros_ec_ucsi.c
index eed2a7d0ebc63..6bca2dce211cd 100644
--- a/drivers/usb/typec/ucsi/cros_ec_ucsi.c
+++ b/drivers/usb/typec/ucsi/cros_ec_ucsi.c
@@ -16,6 +16,7 @@
 #include <linux/platform_device.h>
 #include <linux/slab.h>
 #include <linux/wait.h>
+#include <linux/usb/typec_altmode.h>
 
 #include "ucsi.h"
 
@@ -33,6 +34,11 @@
 /* Number of times to attempt recovery from a write timeout before giving up. */
 #define WRITE_TMO_CTR_MAX	5
 
+/* Delay between mode entry/exit attempts, ms */
+static const unsigned int mode_selection_delay = 1000;
+/* Timeout for a mode entry attempt, ms */
+static const unsigned int mode_selection_timeout = 4000;
+
 struct cros_ucsi_data {
 	struct device *dev;
 	struct ucsi *ucsi;
@@ -134,6 +140,20 @@ static int cros_ucsi_sync_control(struct ucsi *ucsi, u64 cmd, u32 *cci,
 	return ret;
 }
 
+static void cros_ucsi_add_partner_altmodes(struct ucsi_connector *con)
+{
+	if (!con->typec_cap.no_mode_control)
+		typec_mode_selection_start(con->partner,
+					   mode_selection_delay,
+					   mode_selection_timeout);
+}
+
+static void cros_ucsi_remove_partner_altmodes(struct ucsi_connector *con)
+{
+	if (!con->typec_cap.no_mode_control)
+		typec_mode_selection_delete(con->partner);
+}
+
 static const struct ucsi_operations cros_ucsi_ops = {
 	.read_version = cros_ucsi_read_version,
 	.read_cci = cros_ucsi_read_cci,
@@ -141,6 +161,8 @@ static const struct ucsi_operations cros_ucsi_ops = {
 	.read_message_in = cros_ucsi_read_message_in,
 	.async_control = cros_ucsi_async_control,
 	.sync_control = cros_ucsi_sync_control,
+	.add_partner_altmodes = cros_ucsi_add_partner_altmodes,
+	.remove_partner_altmodes = cros_ucsi_remove_partner_altmodes,
 };
 
 static void cros_ucsi_work(struct work_struct *work)
-- 
2.52.0.457.g6b5491de43-goog


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

* Re: [PATCH v5 3/7] usb: typec: Expose alternate mode priority via sysfs
  2026-01-19 13:18 ` [PATCH v5 3/7] usb: typec: Expose alternate mode priority via sysfs Andrei Kuchynski
@ 2026-01-20 15:20   ` Heikki Krogerus
  0 siblings, 0 replies; 13+ messages in thread
From: Heikki Krogerus @ 2026-01-20 15:20 UTC (permalink / raw)
  To: Andrei Kuchynski
  Cc: Abhishek Pandit-Subedi, Benson Leung, Jameson Thies, linux-usb,
	linux-kernel, chrome-platform, Tzung-Bi Shih, Guenter Roeck,
	Greg Kroah-Hartman, Dmitry Baryshkov, Łukasz Bartosik,
	Pooja Katiyar, Johan Hovold, Hsin-Te Yuan, Madhu M,
	Venkat Jayaraman

Mon, Jan 19, 2026 at 01:18:20PM +0000, Andrei Kuchynski kirjoitti:
> This patch introduces a priority sysfs attribute to the USB Type-C
> alternate mode port interface. This new attribute allows user-space to
> configure the numeric priority of alternate modes managing their preferred
> order of operation. If a new priority value conflicts with an existing
> mode's priority, the priorities of the conflicting mode and all subsequent
> modes are automatically incremented to ensure uniqueness.
> 
> Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>
> Reviewed-by: Benson Leung <bleung@chromium.org>

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

> ---
> Changes in V5:
> - Clean up code style and alignment issues
> 
>  Documentation/ABI/testing/sysfs-class-typec | 11 +++
>  drivers/usb/typec/class.c                   | 90 ++++++++++++++++++++-
>  include/linux/usb/typec_altmode.h           |  1 +
>  3 files changed, 101 insertions(+), 1 deletion(-)
> 
> diff --git a/Documentation/ABI/testing/sysfs-class-typec b/Documentation/ABI/testing/sysfs-class-typec
> index 38e101c17a004..737b76828b509 100644
> --- a/Documentation/ABI/testing/sysfs-class-typec
> +++ b/Documentation/ABI/testing/sysfs-class-typec
> @@ -162,6 +162,17 @@ Description:	Lists the supported USB Modes. The default USB mode that is used
>  		- usb3 (USB 3.2)
>  		- usb4 (USB4)
>  
> +What:		/sys/class/typec/<port>/<alt-mode>/priority
> +Date:		July 2025
> +Contact:	Andrei Kuchynski <akuchynski@chromium.org>
> +Description:
> +		Displays and allows setting the priority for a specific alternate mode.
> +		The priority is an integer in the range 0-255. A lower numerical value
> +		indicates a higher priority (0 is the highest).
> +		If the new value is already in use by another mode, the priority of the
> +		conflicting mode and any subsequent modes will be incremented until they
> +		are all unique.
> +
>  USB Type-C partner devices (eg. /sys/class/typec/port0-partner/)
>  
>  What:		/sys/class/typec/<port>-partner/accessory_mode
> diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
> index 0f12d6120511b..a48c447125184 100644
> --- a/drivers/usb/typec/class.c
> +++ b/drivers/usb/typec/class.c
> @@ -445,11 +445,88 @@ svid_show(struct device *dev, struct device_attribute *attr, char *buf)
>  }
>  static DEVICE_ATTR_RO(svid);
>  
> +static int increment_duplicated_priority(struct device *dev, void *data)
> +{
> +	if (is_typec_port_altmode(dev)) {
> +		struct typec_altmode **alt_target = (struct typec_altmode **)data;
> +		struct typec_altmode *alt = to_typec_altmode(dev);
> +
> +		if (alt != *alt_target && alt->priority == (*alt_target)->priority) {
> +			alt->priority++;
> +			*alt_target = alt;
> +			return 1;
> +		}
> +	}
> +	return 0;
> +}
> +
> +static int find_duplicated_priority(struct device *dev, void *data)
> +{
> +	if (is_typec_port_altmode(dev)) {
> +		struct typec_altmode **alt_target = (struct typec_altmode **)data;
> +		struct typec_altmode *alt = to_typec_altmode(dev);
> +
> +		if (alt != *alt_target && alt->priority == (*alt_target)->priority)
> +			return 1;
> +	}
> +	return 0;
> +}
> +
> +static int typec_mode_set_priority(struct typec_altmode *alt, const u8 priority)
> +{
> +	struct typec_port *port = to_typec_port(alt->dev.parent);
> +	const u8 old_priority = alt->priority;
> +	int res = 1;
> +
> +	alt->priority = priority;
> +	while (res) {
> +		res = device_for_each_child(&port->dev, &alt, find_duplicated_priority);
> +		if (res) {
> +			alt->priority++;
> +			if (alt->priority == 0) {
> +				alt->priority = old_priority;
> +				return -EOVERFLOW;
> +			}
> +		}
> +	}
> +
> +	res = 1;
> +	alt->priority = priority;
> +	while (res)
> +		res = device_for_each_child(&port->dev, &alt,
> +					    increment_duplicated_priority);
> +
> +	return 0;
> +}
> +
> +static ssize_t priority_store(struct device *dev,
> +			      struct device_attribute *attr,
> +			      const char *buf, size_t size)
> +{
> +	u8 val;
> +	int err = kstrtou8(buf, 10, &val);
> +
> +	if (!err)
> +		err = typec_mode_set_priority(to_typec_altmode(dev), val);
> +
> +	if (!err)
> +		return size;
> +	return err;
> +}
> +
> +static ssize_t priority_show(struct device *dev,
> +			     struct device_attribute *attr, char *buf)
> +{
> +	return sysfs_emit(buf, "%u\n", to_typec_altmode(dev)->priority);
> +}
> +static DEVICE_ATTR_RW(priority);
> +
>  static struct attribute *typec_altmode_attrs[] = {
>  	&dev_attr_active.attr,
>  	&dev_attr_mode.attr,
>  	&dev_attr_svid.attr,
>  	&dev_attr_vdo.attr,
> +	&dev_attr_priority.attr,
>  	NULL
>  };
>  
> @@ -459,11 +536,15 @@ static umode_t typec_altmode_attr_is_visible(struct kobject *kobj,
>  	struct typec_altmode *adev = to_typec_altmode(kobj_to_dev(kobj));
>  	struct typec_port *port = typec_altmode2port(adev);
>  
> -	if (attr == &dev_attr_active.attr)
> +	if (attr == &dev_attr_active.attr) {
>  		if (!is_typec_port(adev->dev.parent)) {
>  			if (!port->mode_control || !adev->ops || !adev->ops->activate)
>  				return 0444;
>  		}
> +	} else if (attr == &dev_attr_priority.attr) {
> +		if (!is_typec_port(adev->dev.parent) || !port->mode_control)
> +			return 0;
> +	}
>  
>  	return attr->mode;
>  }
> @@ -2498,6 +2579,7 @@ typec_port_register_altmode(struct typec_port *port,
>  	struct typec_altmode *adev;
>  	struct typec_mux *mux;
>  	struct typec_retimer *retimer;
> +	int ret;
>  
>  	mux = typec_mux_get(&port->dev);
>  	if (IS_ERR(mux))
> @@ -2516,6 +2598,12 @@ typec_port_register_altmode(struct typec_port *port,
>  	} else {
>  		to_altmode(adev)->mux = mux;
>  		to_altmode(adev)->retimer = retimer;
> +
> +		ret = typec_mode_set_priority(adev, 0);
> +		if (ret) {
> +			typec_unregister_altmode(adev);
> +			return ERR_PTR(ret);
> +		}
>  	}
>  
>  	return adev;
> diff --git a/include/linux/usb/typec_altmode.h b/include/linux/usb/typec_altmode.h
> index 9197a4637a938..7e6c02d74b54f 100644
> --- a/include/linux/usb/typec_altmode.h
> +++ b/include/linux/usb/typec_altmode.h
> @@ -36,6 +36,7 @@ struct typec_altmode {
>  	int				mode;
>  	u32				vdo;
>  	unsigned int			active:1;
> +	u8				priority;
>  
>  	char				*desc;
>  	const struct typec_altmode_ops	*ops;
> -- 
> 2.52.0.457.g6b5491de43-goog

-- 
heikki

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

* Re: [PATCH v5 4/7] usb: typec: Implement mode selection
  2026-01-19 13:18 ` [PATCH v5 4/7] usb: typec: Implement mode selection Andrei Kuchynski
@ 2026-01-20 15:24   ` Heikki Krogerus
  0 siblings, 0 replies; 13+ messages in thread
From: Heikki Krogerus @ 2026-01-20 15:24 UTC (permalink / raw)
  To: Andrei Kuchynski
  Cc: Abhishek Pandit-Subedi, Benson Leung, Jameson Thies, linux-usb,
	linux-kernel, chrome-platform, Tzung-Bi Shih, Guenter Roeck,
	Greg Kroah-Hartman, Dmitry Baryshkov, Łukasz Bartosik,
	Pooja Katiyar, Johan Hovold, Hsin-Te Yuan, Madhu M,
	Venkat Jayaraman

Mon, Jan 19, 2026 at 01:18:21PM +0000, Andrei Kuchynski kirjoitti:
> The mode selection process is controlled by the following API functions,
> which allow to initiate and complete mode entry based on the priority of
> each mode:
> 
> `typec_mode_selection_start` function compiles a priority list of supported
> Alternate Modes.
> `typec_altmode_state_update` function is invoked by the port driver to
> communicate the current mode of the Type-C connector.
> `typec_mode_selection_delete` function stops the currently running mode
> selection process and releases all associated system resources.
> 
> `mode_selection_work_fn` task attempts to activate modes. The process stops
> on success; otherwise, it proceeds to the next mode after a timeout or
> error.
> 
> Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>

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

> ---
> Changes in V5:
> - Adopt guard(mutex)(&sel->lock) in mode_selection_work_fn()
> - Use dev_err instead of dev_dbg, remove the error message when exiting
>   the mode
> - Update typec_altmode_state_update() to only reschedule the
>   mode_selection_work_fn task if it was successfully cancelled
> - Clean up code style and alignment issues
> 
>  drivers/usb/typec/Makefile         |   2 +-
>  drivers/usb/typec/class.h          |   2 +
>  drivers/usb/typec/mode_selection.c | 283 +++++++++++++++++++++++++++++
>  include/linux/usb/typec_altmode.h  |  40 ++++
>  4 files changed, 326 insertions(+), 1 deletion(-)
>  create mode 100644 drivers/usb/typec/mode_selection.c
> 
> diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
> index 7a368fea61bc9..8a6a1c663eb69 100644
> --- a/drivers/usb/typec/Makefile
> +++ b/drivers/usb/typec/Makefile
> @@ -1,6 +1,6 @@
>  # SPDX-License-Identifier: GPL-2.0
>  obj-$(CONFIG_TYPEC)		+= typec.o
> -typec-y				:= class.o mux.o bus.o pd.o retimer.o
> +typec-y				:= class.o mux.o bus.o pd.o retimer.o mode_selection.o
>  typec-$(CONFIG_ACPI)		+= port-mapper.o
>  obj-$(CONFIG_TYPEC)		+= altmodes/
>  obj-$(CONFIG_TYPEC_TCPM)	+= tcpm/
> diff --git a/drivers/usb/typec/class.h b/drivers/usb/typec/class.h
> index 2e89a83c2eb70..d3435936ee7c8 100644
> --- a/drivers/usb/typec/class.h
> +++ b/drivers/usb/typec/class.h
> @@ -9,6 +9,7 @@
>  struct typec_mux;
>  struct typec_switch;
>  struct usb_device;
> +struct mode_selection;
>  
>  struct typec_plug {
>  	struct device			dev;
> @@ -39,6 +40,7 @@ struct typec_partner {
>  	u8				usb_capability;
>  
>  	struct usb_power_delivery	*pd;
> +	struct mode_selection	*sel;
>  
>  	void (*attach)(struct typec_partner *partner, struct device *dev);
>  	void (*deattach)(struct typec_partner *partner, struct device *dev);
> diff --git a/drivers/usb/typec/mode_selection.c b/drivers/usb/typec/mode_selection.c
> new file mode 100644
> index 0000000000000..a95b31e21b528
> --- /dev/null
> +++ b/drivers/usb/typec/mode_selection.c
> @@ -0,0 +1,283 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * Copyright 2025 Google LLC.
> + */
> +
> +#include <linux/types.h>
> +#include <linux/list_sort.h>
> +#include <linux/slab.h>
> +#include <linux/mutex.h>
> +#include <linux/workqueue.h>
> +#include <linux/usb/typec_altmode.h>
> +
> +#include "class.h"
> +
> +/**
> + * struct mode_state - State tracking for a specific Type-C alternate mode
> + * @svid: Standard or Vendor ID of the Alternate Mode
> + * @priority: Mode priority
> + * @error: Outcome of the last attempt to enter the mode
> + * @list: List head to link this mode state into a prioritized list
> + */
> +struct mode_state {
> +	u16 svid;
> +	u8 priority;
> +	int error;
> +	struct list_head list;
> +};
> +
> +/**
> + * struct mode_selection - Manages the selection and state of Alternate Modes
> + * @mode_list: Prioritized list of available Alternate Modes
> + * @lock: Mutex to protect mode_list
> + * @work: Work structure
> + * @partner: Handle to the Type-C partner device
> + * @active_svid: svid of currently active mode
> + * @timeout: Timeout for a mode entry attempt, ms
> + * @delay: Delay between mode entry/exit attempts, ms
> + */
> +struct mode_selection {
> +	struct list_head mode_list;
> +	/* Protects the mode_list*/
> +	struct mutex lock;
> +	struct delayed_work work;
> +	struct typec_partner *partner;
> +	u16 active_svid;
> +	unsigned int timeout;
> +	unsigned int delay;
> +};
> +
> +/**
> + * struct mode_order - Mode activation tracking
> + * @svid: Standard or Vendor ID of the Alternate Mode
> + * @enter: Flag indicating if the driver is currently attempting to enter or
> + * exit the mode
> + * @result: Outcome of the attempt to activate the mode
> + */
> +struct mode_order {
> +	u16 svid;
> +	int enter;
> +	int result;
> +};
> +
> +static int activate_altmode(struct device *dev, void *data)
> +{
> +	if (is_typec_partner_altmode(dev)) {
> +		struct typec_altmode *alt = to_typec_altmode(dev);
> +		struct mode_order *order = (struct mode_order *)data;
> +
> +		if (order->svid == alt->svid) {
> +			if (alt->ops && alt->ops->activate)
> +				order->result = alt->ops->activate(alt, order->enter);
> +			else
> +				order->result = -EOPNOTSUPP;
> +			return 1;
> +		}
> +	}
> +	return 0;
> +}
> +
> +static int mode_selection_activate(struct mode_selection *sel,
> +				   const u16 svid, const int enter)
> +
> +	__must_hold(&sel->lock)
> +{
> +	struct mode_order order = {.svid = svid, .enter = enter, .result = -ENODEV};
> +
> +	/*
> +	 * The port driver may acquire its internal mutex during alternate mode
> +	 * activation. Since this is the same mutex that may be held during the
> +	 * execution of typec_altmode_state_update(), it is crucial to release
> +	 * sel->mutex before activation to avoid potential deadlock.
> +	 * Note that sel->mode_list must remain invariant throughout this unlocked
> +	 * interval.
> +	 */
> +	mutex_unlock(&sel->lock);
> +	device_for_each_child(&sel->partner->dev, &order, activate_altmode);
> +	mutex_lock(&sel->lock);
> +
> +	return order.result;
> +}
> +
> +static void mode_list_clean(struct mode_selection *sel)
> +{
> +	struct mode_state *ms, *tmp;
> +
> +	list_for_each_entry_safe(ms, tmp, &sel->mode_list, list) {
> +		list_del(&ms->list);
> +		kfree(ms);
> +	}
> +}
> +
> +/**
> + * mode_selection_work_fn() - Alternate mode activation task
> + * @work: work structure
> + *
> + * - If the Alternate Mode currently prioritized at the top of the list is already
> + * active, the entire selection process is considered finished.
> + * - If a different Alternate Mode is currently active, the system must exit that
> + * active mode first before attempting any new entry.
> + *
> + * The function then checks the result of the attempt to entre the current mode,
> + * stored in the `ms->error` field:
> + * - if the attempt FAILED, the mode is deactivated and removed from the list.
> + * - `ms->error` value of 0 signifies that the mode has not yet been activated.
> + *
> + * Once successfully activated, the task is scheduled for subsequent entry after
> + * a timeout period. The alternate mode driver is expected to call back with the
> + * actual mode entry result via `typec_altmode_state_update()`.
> + */
> +static void mode_selection_work_fn(struct work_struct *work)
> +{
> +	struct mode_selection *sel = container_of(work,
> +				struct mode_selection, work.work);
> +	struct mode_state *ms;
> +	unsigned int delay = sel->delay;
> +	int result;
> +
> +	guard(mutex)(&sel->lock);
> +
> +	ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
> +	if (!ms)
> +		return;
> +
> +	if (sel->active_svid == ms->svid) {
> +		dev_dbg(&sel->partner->dev, "%x altmode is active\n", ms->svid);
> +		mode_list_clean(sel);
> +	} else if (sel->active_svid != 0) {
> +		result = mode_selection_activate(sel, sel->active_svid, 0);
> +		if (result)
> +			mode_list_clean(sel);
> +		else
> +			sel->active_svid = 0;
> +	} else if (ms->error) {
> +		dev_err(&sel->partner->dev, "%x: entry error %pe\n",
> +			ms->svid, ERR_PTR(ms->error));
> +		mode_selection_activate(sel, ms->svid, 0);
> +		list_del(&ms->list);
> +		kfree(ms);
> +	} else {
> +		result = mode_selection_activate(sel, ms->svid, 1);
> +		if (result) {
> +			dev_err(&sel->partner->dev, "%x: activation error %pe\n",
> +				ms->svid, ERR_PTR(result));
> +			list_del(&ms->list);
> +			kfree(ms);
> +		} else {
> +			delay = sel->timeout;
> +			ms->error = -ETIMEDOUT;
> +		}
> +	}
> +
> +	if (!list_empty(&sel->mode_list))
> +		schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
> +}
> +
> +void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
> +				const int error)
> +{
> +	struct mode_selection *sel = partner->sel;
> +	struct mode_state *ms;
> +
> +	if (sel) {
> +		mutex_lock(&sel->lock);
> +		ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
> +		if (ms && ms->svid == svid) {
> +			ms->error = error;
> +			if (cancel_delayed_work(&sel->work))
> +				schedule_delayed_work(&sel->work, 0);
> +		}
> +		if (!error)
> +			sel->active_svid = svid;
> +		else
> +			sel->active_svid = 0;
> +		mutex_unlock(&sel->lock);
> +	}
> +}
> +EXPORT_SYMBOL_GPL(typec_altmode_state_update);
> +
> +static int compare_priorities(void *priv,
> +			      const struct list_head *a, const struct list_head *b)
> +{
> +	const struct mode_state *msa = container_of(a, struct mode_state, list);
> +	const struct mode_state *msb = container_of(b, struct mode_state, list);
> +
> +	if (msa->priority < msb->priority)
> +		return -1;
> +	return 1;
> +}
> +
> +static int altmode_add_to_list(struct device *dev, void *data)
> +{
> +	if (is_typec_partner_altmode(dev)) {
> +		struct list_head *list = (struct list_head *)data;
> +		struct typec_altmode *altmode = to_typec_altmode(dev);
> +		const struct typec_altmode *pdev = typec_altmode_get_partner(altmode);
> +		struct mode_state *ms;
> +
> +		if (pdev && altmode->ops && altmode->ops->activate) {
> +			ms = kzalloc(sizeof(*ms), GFP_KERNEL);
> +			if (!ms)
> +				return -ENOMEM;
> +			ms->svid = pdev->svid;
> +			ms->priority = pdev->priority;
> +			INIT_LIST_HEAD(&ms->list);
> +			list_add_tail(&ms->list, list);
> +		}
> +	}
> +	return 0;
> +}
> +
> +int typec_mode_selection_start(struct typec_partner *partner,
> +			       const unsigned int delay, const unsigned int timeout)
> +{
> +	struct mode_selection *sel;
> +	int ret;
> +
> +	if (partner->usb_mode == USB_MODE_USB4)
> +		return -EBUSY;
> +
> +	if (partner->sel)
> +		return -EALREADY;
> +
> +	sel = kzalloc(sizeof(*sel), GFP_KERNEL);
> +	if (!sel)
> +		return -ENOMEM;
> +
> +	INIT_LIST_HEAD(&sel->mode_list);
> +
> +	ret = device_for_each_child(&partner->dev, &sel->mode_list,
> +				    altmode_add_to_list);
> +
> +	if (ret || list_empty(&sel->mode_list)) {
> +		mode_list_clean(sel);
> +		kfree(sel);
> +		return ret;
> +	}
> +
> +	list_sort(NULL, &sel->mode_list, compare_priorities);
> +	sel->partner = partner;
> +	sel->delay = delay;
> +	sel->timeout = timeout;
> +	mutex_init(&sel->lock);
> +	INIT_DELAYED_WORK(&sel->work, mode_selection_work_fn);
> +	schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
> +	partner->sel = sel;
> +
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(typec_mode_selection_start);
> +
> +void typec_mode_selection_delete(struct typec_partner *partner)
> +{
> +	struct mode_selection *sel = partner->sel;
> +
> +	if (sel) {
> +		partner->sel = NULL;
> +		cancel_delayed_work_sync(&sel->work);
> +		mode_list_clean(sel);
> +		mutex_destroy(&sel->lock);
> +		kfree(sel);
> +	}
> +}
> +EXPORT_SYMBOL_GPL(typec_mode_selection_delete);
> diff --git a/include/linux/usb/typec_altmode.h b/include/linux/usb/typec_altmode.h
> index 7e6c02d74b54f..70026f5f8f997 100644
> --- a/include/linux/usb/typec_altmode.h
> +++ b/include/linux/usb/typec_altmode.h
> @@ -240,4 +240,44 @@ void typec_altmode_unregister_driver(struct typec_altmode_driver *drv);
>  	module_driver(__typec_altmode_driver, typec_altmode_register_driver, \
>  		      typec_altmode_unregister_driver)
>  
> +/**
> + * typec_mode_selection_start - Start an alternate mode selection process
> + * @partner: Handle to the Type-C partner device
> + * @delay: Delay between mode entry/exit attempts, ms
> + * @timeout: Timeout for a mode entry attempt, ms
> + *
> + * This function initiates the process of attempting to enter an Alternate Mode
> + * supported by the connected Type-C partner.
> + * Returns 0 on success, or a negative error code on failure.
> + */
> +int typec_mode_selection_start(struct typec_partner *partner,
> +			       const unsigned int delay, const unsigned int timeout);
> +
> +/**
> + * typec_altmode_state_update - Report the current status of an Alternate Mode
> + * negotiation
> + * @partner: Handle to the Type-C partner device
> + * @svid: Standard or Vendor ID of the Alternate Mode. A value of 0 should be
> + * passed if no mode is currently active
> + * @result: Result of the entry operation. This should be 0 on success, or a
> + * negative error code if the negotiation failed
> + *
> + * This function should be called by an Alternate Mode driver to report the
> + * result of an asynchronous alternate mode entry request. It signals what the
> + * current active SVID is (or 0 if none) and the success or failure status of
> + * the last attempt.
> + */
> +void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
> +				const int result);
> +
> +/**
> + * typec_mode_selection_delete - Delete an alternate mode selection instance
> + * @partner: Handle to the Type-C partner device.
> + *
> + * This function cancels a pending alternate mode selection request that was
> + * previously started with typec_mode_selection_start().
> + * This is typically called when the partner disconnects.
> + */
> +void typec_mode_selection_delete(struct typec_partner *partner);
> +
>  #endif /* __USB_TYPEC_ALTMODE_H */
> -- 
> 2.52.0.457.g6b5491de43-goog

-- 
heikki

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

* Re: [PATCH v5 5/7] usb: typec: Introduce mode_selection bit
  2026-01-19 13:18 ` [PATCH v5 5/7] usb: typec: Introduce mode_selection bit Andrei Kuchynski
@ 2026-01-20 15:25   ` Heikki Krogerus
  0 siblings, 0 replies; 13+ messages in thread
From: Heikki Krogerus @ 2026-01-20 15:25 UTC (permalink / raw)
  To: Andrei Kuchynski
  Cc: Abhishek Pandit-Subedi, Benson Leung, Jameson Thies, linux-usb,
	linux-kernel, chrome-platform, Tzung-Bi Shih, Guenter Roeck,
	Greg Kroah-Hartman, Dmitry Baryshkov, Łukasz Bartosik,
	Pooja Katiyar, Johan Hovold, Hsin-Te Yuan, Madhu M,
	Venkat Jayaraman

Mon, Jan 19, 2026 at 01:18:22PM +0000, Andrei Kuchynski kirjoitti:
> The port driver sets this bit for an alternate mode description to indicate
> support for the mode selection feature. Once set, individual Alt Mode
> drivers will no longer attempt to activate their respective modes within
> their probe functions. This prevents race conditions and non-prioritized
> activation.
> The bit is not set by default. If left unset, the system retains the
> current behavior where Alt Mode drivers manage their own activation logic.
> 
> Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>

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

> ---
>  drivers/usb/typec/altmodes/displayport.c | 6 ++++--
>  drivers/usb/typec/altmodes/thunderbolt.c | 2 +-
>  drivers/usb/typec/class.c                | 1 +
>  include/linux/usb/typec.h                | 1 +
>  include/linux/usb/typec_altmode.h        | 1 +
>  5 files changed, 8 insertions(+), 3 deletions(-)
> 
> diff --git a/drivers/usb/typec/altmodes/displayport.c b/drivers/usb/typec/altmodes/displayport.c
> index d96ab106a980b..d185688a16b13 100644
> --- a/drivers/usb/typec/altmodes/displayport.c
> +++ b/drivers/usb/typec/altmodes/displayport.c
> @@ -804,8 +804,10 @@ int dp_altmode_probe(struct typec_altmode *alt)
>  	if (plug)
>  		typec_altmode_set_drvdata(plug, dp);
>  
> -	dp->state = plug ? DP_STATE_ENTER_PRIME : DP_STATE_ENTER;
> -	schedule_work(&dp->work);
> +	if (!alt->mode_selection) {
> +		dp->state = plug ? DP_STATE_ENTER_PRIME : DP_STATE_ENTER;
> +		schedule_work(&dp->work);
> +	}
>  
>  	return 0;
>  }
> diff --git a/drivers/usb/typec/altmodes/thunderbolt.c b/drivers/usb/typec/altmodes/thunderbolt.c
> index 6eadf7835f8f6..c4c5da6154da9 100644
> --- a/drivers/usb/typec/altmodes/thunderbolt.c
> +++ b/drivers/usb/typec/altmodes/thunderbolt.c
> @@ -307,7 +307,7 @@ static int tbt_altmode_probe(struct typec_altmode *alt)
>  	typec_altmode_set_drvdata(alt, tbt);
>  	typec_altmode_set_ops(alt, &tbt_altmode_ops);
>  
> -	if (tbt_ready(alt)) {
> +	if (!alt->mode_selection && tbt_ready(alt)) {
>  		if (tbt->plug[TYPEC_PLUG_SOP_P])
>  			tbt->state = TBT_STATE_SOP_P_ENTER;
>  		else if (tbt->plug[TYPEC_PLUG_SOP_PP])
> diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
> index a48c447125184..dbba53f024977 100644
> --- a/drivers/usb/typec/class.c
> +++ b/drivers/usb/typec/class.c
> @@ -655,6 +655,7 @@ typec_register_altmode(struct device *parent,
>  	alt->adev.svid = desc->svid;
>  	alt->adev.mode = desc->mode;
>  	alt->adev.vdo = desc->vdo;
> +	alt->adev.mode_selection = desc->mode_selection;
>  	alt->roles = desc->roles;
>  	alt->id = id;
>  
> diff --git a/include/linux/usb/typec.h b/include/linux/usb/typec.h
> index dbb259d885266..d61ec38216fa9 100644
> --- a/include/linux/usb/typec.h
> +++ b/include/linux/usb/typec.h
> @@ -155,6 +155,7 @@ struct typec_altmode_desc {
>  	/* Only used with ports */
>  	enum typec_port_data	roles;
>  	bool			inactive;
> +	bool			mode_selection;
>  };
>  
>  void typec_partner_set_pd_revision(struct typec_partner *partner, u16 pd_revision);
> diff --git a/include/linux/usb/typec_altmode.h b/include/linux/usb/typec_altmode.h
> index 70026f5f8f997..0513d333b7977 100644
> --- a/include/linux/usb/typec_altmode.h
> +++ b/include/linux/usb/typec_altmode.h
> @@ -37,6 +37,7 @@ struct typec_altmode {
>  	u32				vdo;
>  	unsigned int			active:1;
>  	u8				priority;
> +	bool			mode_selection;
>  
>  	char				*desc;
>  	const struct typec_altmode_ops	*ops;
> -- 
> 2.52.0.457.g6b5491de43-goog

-- 
heikki

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

* Re: [PATCH v5 6/7] usb: typec: ucsi: Support mode selection to activate altmodes
  2026-01-19 13:18 ` [PATCH v5 6/7] usb: typec: ucsi: Support mode selection to activate altmodes Andrei Kuchynski
@ 2026-01-20 15:26   ` Heikki Krogerus
  0 siblings, 0 replies; 13+ messages in thread
From: Heikki Krogerus @ 2026-01-20 15:26 UTC (permalink / raw)
  To: Andrei Kuchynski
  Cc: Abhishek Pandit-Subedi, Benson Leung, Jameson Thies, linux-usb,
	linux-kernel, chrome-platform, Tzung-Bi Shih, Guenter Roeck,
	Greg Kroah-Hartman, Dmitry Baryshkov, Łukasz Bartosik,
	Pooja Katiyar, Johan Hovold, Hsin-Te Yuan, Madhu M,
	Venkat Jayaraman

Mon, Jan 19, 2026 at 01:18:23PM +0000, Andrei Kuchynski kirjoitti:
> If the ucsi port driver supports modes selection, it should implement
> `add_partner_altmodes` and `remove_partner_altmodes` ucsi operations. With
> these operations the driver can manage the mode selection process.
> Once partner altmodes are registered, `add_partner_altmodes` is called to
> start the mode selection. When the partner is unregistered,
> `remove_partner_altmodes` is supposed to stop any ongoing processes and
> clean up the resources.
> 
> `typec_altmode_state_update` informes mode selection about the current mode
> of the Type-C connector.
> 
> Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>

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

> ---
> Changes in V5:
> - Use the no_mode_control field instead of
>   con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE
> - Squash previous V4 patches 3/8 and 7/8 into a single patch
> 
>  drivers/usb/typec/ucsi/ucsi.c | 12 ++++++++++++
>  drivers/usb/typec/ucsi/ucsi.h |  4 ++++
>  2 files changed, 16 insertions(+)
> 
> diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c
> index a7b388dc7fa0f..251990475faa7 100644
> --- a/drivers/usb/typec/ucsi/ucsi.c
> +++ b/drivers/usb/typec/ucsi/ucsi.c
> @@ -314,6 +314,7 @@ void ucsi_altmode_update_active(struct ucsi_connector *con)
>  {
>  	const struct typec_altmode *altmode = NULL;
>  	u64 command;
> +	u16 svid = 0;
>  	int ret;
>  	u8 cur;
>  	int i;
> @@ -335,6 +336,10 @@ void ucsi_altmode_update_active(struct ucsi_connector *con)
>  	for (i = 0; con->partner_altmode[i]; i++)
>  		typec_altmode_update_active(con->partner_altmode[i],
>  					    con->partner_altmode[i] == altmode);
> +
> +	if (altmode)
> +		svid = altmode->svid;
> +	typec_altmode_state_update(con->partner, svid, 0);
>  }
>  
>  static int ucsi_altmode_next_mode(struct typec_altmode **alt, u16 svid)
> @@ -609,6 +614,8 @@ static int ucsi_register_altmodes(struct ucsi_connector *con, u8 recipient)
>  			desc.vdo = alt[j].mid;
>  			desc.svid = alt[j].svid;
>  			desc.roles = TYPEC_PORT_DRD;
> +			desc.mode_selection = con->ucsi->ops->add_partner_altmodes &&
> +					!con->typec_cap.no_mode_control;
>  
>  			ret = ucsi_register_altmode(con, &desc, recipient);
>  			if (ret)
> @@ -831,6 +838,8 @@ static int ucsi_check_altmodes(struct ucsi_connector *con)
>  	if (con->partner_altmode[0]) {
>  		num_partner_am = ucsi_get_num_altmode(con->partner_altmode);
>  		typec_partner_set_num_altmodes(con->partner, num_partner_am);
> +		if (con->ucsi->ops->add_partner_altmodes)
> +			con->ucsi->ops->add_partner_altmodes(con);
>  		ucsi_altmode_update_active(con);
>  		return 0;
>  	} else {
> @@ -1119,6 +1128,8 @@ static void ucsi_unregister_partner(struct ucsi_connector *con)
>  		return;
>  
>  	typec_set_mode(con->port, TYPEC_STATE_SAFE);
> +	if (con->ucsi->ops->remove_partner_altmodes)
> +		con->ucsi->ops->remove_partner_altmodes(con);
>  
>  	typec_partner_set_usb_power_delivery(con->partner, NULL);
>  	ucsi_unregister_partner_pdos(con);
> @@ -1659,6 +1670,7 @@ static int ucsi_register_port(struct ucsi *ucsi, struct ucsi_connector *con)
>  
>  	cap->driver_data = con;
>  	cap->ops = &ucsi_ops;
> +	cap->no_mode_control = !(con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE);
>  
>  	if (ucsi->version >= UCSI_VERSION_2_0)
>  		con->typec_cap.orientation_aware = true;
> diff --git a/drivers/usb/typec/ucsi/ucsi.h b/drivers/usb/typec/ucsi/ucsi.h
> index 410389ef173ab..4797b4aa1e35b 100644
> --- a/drivers/usb/typec/ucsi/ucsi.h
> +++ b/drivers/usb/typec/ucsi/ucsi.h
> @@ -70,6 +70,8 @@ struct dentry;
>   * @update_altmodes: Squashes duplicate DP altmodes
>   * @update_connector: Update connector capabilities before registering
>   * @connector_status: Updates connector status, called holding connector lock
> + * @add_partner_altmodes: Start mode selection
> + * @remove_partner_altmodes: Clean mode selection
>   *
>   * Read and write routines for UCSI interface. @sync_write must wait for the
>   * Command Completion Event from the PPM before returning, and @async_write must
> @@ -88,6 +90,8 @@ struct ucsi_operations {
>  				struct ucsi_altmode *updated);
>  	void (*update_connector)(struct ucsi_connector *con);
>  	void (*connector_status)(struct ucsi_connector *con);
> +	void (*add_partner_altmodes)(struct ucsi_connector *con);
> +	void (*remove_partner_altmodes)(struct ucsi_connector *con);
>  };
>  
>  struct ucsi *ucsi_create(struct device *dev, const struct ucsi_operations *ops);
> -- 
> 2.52.0.457.g6b5491de43-goog

-- 
heikki

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

* Re: [PATCH v5 7/7] usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi
  2026-01-19 13:18 ` [PATCH v5 7/7] usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi Andrei Kuchynski
@ 2026-01-20 15:27   ` Heikki Krogerus
  0 siblings, 0 replies; 13+ messages in thread
From: Heikki Krogerus @ 2026-01-20 15:27 UTC (permalink / raw)
  To: Andrei Kuchynski
  Cc: Abhishek Pandit-Subedi, Benson Leung, Jameson Thies, linux-usb,
	linux-kernel, chrome-platform, Tzung-Bi Shih, Guenter Roeck,
	Greg Kroah-Hartman, Dmitry Baryshkov, Łukasz Bartosik,
	Pooja Katiyar, Johan Hovold, Hsin-Te Yuan, Madhu M,
	Venkat Jayaraman

Mon, Jan 19, 2026 at 01:18:24PM +0000, Andrei Kuchynski kirjoitti:
> The mode selection sequence is initiated by the driver after all partner
> alternate modes have been successfully registered.
> When a partner is disconnected, the driver also stops the mode selection
> process and releases resources via `typec_mode_selection_delete`.
> 
> Signed-off-by: Andrei Kuchynski <akuchynski@chromium.org>

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

> ---
> Changes in V5:
> - Use the no_mode_control field instead of
>   con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE
> 
>  drivers/usb/typec/ucsi/cros_ec_ucsi.c | 22 ++++++++++++++++++++++
>  1 file changed, 22 insertions(+)
> 
> diff --git a/drivers/usb/typec/ucsi/cros_ec_ucsi.c b/drivers/usb/typec/ucsi/cros_ec_ucsi.c
> index eed2a7d0ebc63..6bca2dce211cd 100644
> --- a/drivers/usb/typec/ucsi/cros_ec_ucsi.c
> +++ b/drivers/usb/typec/ucsi/cros_ec_ucsi.c
> @@ -16,6 +16,7 @@
>  #include <linux/platform_device.h>
>  #include <linux/slab.h>
>  #include <linux/wait.h>
> +#include <linux/usb/typec_altmode.h>
>  
>  #include "ucsi.h"
>  
> @@ -33,6 +34,11 @@
>  /* Number of times to attempt recovery from a write timeout before giving up. */
>  #define WRITE_TMO_CTR_MAX	5
>  
> +/* Delay between mode entry/exit attempts, ms */
> +static const unsigned int mode_selection_delay = 1000;
> +/* Timeout for a mode entry attempt, ms */
> +static const unsigned int mode_selection_timeout = 4000;
> +
>  struct cros_ucsi_data {
>  	struct device *dev;
>  	struct ucsi *ucsi;
> @@ -134,6 +140,20 @@ static int cros_ucsi_sync_control(struct ucsi *ucsi, u64 cmd, u32 *cci,
>  	return ret;
>  }
>  
> +static void cros_ucsi_add_partner_altmodes(struct ucsi_connector *con)
> +{
> +	if (!con->typec_cap.no_mode_control)
> +		typec_mode_selection_start(con->partner,
> +					   mode_selection_delay,
> +					   mode_selection_timeout);
> +}
> +
> +static void cros_ucsi_remove_partner_altmodes(struct ucsi_connector *con)
> +{
> +	if (!con->typec_cap.no_mode_control)
> +		typec_mode_selection_delete(con->partner);
> +}
> +
>  static const struct ucsi_operations cros_ucsi_ops = {
>  	.read_version = cros_ucsi_read_version,
>  	.read_cci = cros_ucsi_read_cci,
> @@ -141,6 +161,8 @@ static const struct ucsi_operations cros_ucsi_ops = {
>  	.read_message_in = cros_ucsi_read_message_in,
>  	.async_control = cros_ucsi_async_control,
>  	.sync_control = cros_ucsi_sync_control,
> +	.add_partner_altmodes = cros_ucsi_add_partner_altmodes,
> +	.remove_partner_altmodes = cros_ucsi_remove_partner_altmodes,
>  };
>  
>  static void cros_ucsi_work(struct work_struct *work)
> -- 
> 2.52.0.457.g6b5491de43-goog

-- 
heikki

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

end of thread, other threads:[~2026-01-20 15:28 UTC | newest]

Thread overview: 13+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-01-19 13:18 [PATCH v5 0/7] USB Type-C mode selection Andrei Kuchynski
2026-01-19 13:18 ` [PATCH v5 1/7] usb: typec: Add mode_control field to port property Andrei Kuchynski
2026-01-19 13:18 ` [PATCH v5 2/7] platform/chrome: cros_ec_typec: Set no_mode_control flag Andrei Kuchynski
2026-01-19 13:18 ` [PATCH v5 3/7] usb: typec: Expose alternate mode priority via sysfs Andrei Kuchynski
2026-01-20 15:20   ` Heikki Krogerus
2026-01-19 13:18 ` [PATCH v5 4/7] usb: typec: Implement mode selection Andrei Kuchynski
2026-01-20 15:24   ` Heikki Krogerus
2026-01-19 13:18 ` [PATCH v5 5/7] usb: typec: Introduce mode_selection bit Andrei Kuchynski
2026-01-20 15:25   ` Heikki Krogerus
2026-01-19 13:18 ` [PATCH v5 6/7] usb: typec: ucsi: Support mode selection to activate altmodes Andrei Kuchynski
2026-01-20 15:26   ` Heikki Krogerus
2026-01-19 13:18 ` [PATCH v5 7/7] usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi Andrei Kuchynski
2026-01-20 15:27   ` Heikki Krogerus

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