[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20250804090340.3062182-6-akuchynski@chromium.org>
Date: Mon, 4 Aug 2025 09:03:34 +0000
From: Andrei Kuchynski <akuchynski@...omium.org>
To: Heikki Krogerus <heikki.krogerus@...ux.intel.com>,
Abhishek Pandit-Subedi <abhishekpandit@...omium.org>,
Benson Leung <bleung@...omium.org>,
Jameson Thies <jthies@...gle.com>,
Tzung-Bi Shih <tzungbi@...nel.org>,
linux-usb@...r.kernel.org,
chrome-platform@...ts.linux.dev
Cc: Guenter Roeck <groeck@...omium.org>,
Greg Kroah-Hartman <gregkh@...uxfoundation.org>,
Dmitry Baryshkov <dmitry.baryshkov@....qualcomm.com>,
"Christian A. Ehrhardt" <lk@...e.de>,
Venkat Jayaraman <venkat.jayaraman@...el.com>,
linux-kernel@...r.kernel.org,
Andrei Kuchynski <akuchynski@...omium.org>
Subject: [PATCH v3 05/10] usb: typec: Implement automated mode selection
This patch introduces new sysfs attributes to enable user control over
Type-C automated mode selection and provide negotiation feedback.
`mode_selection` attribute shows a prioritized list of supported modes
with the currently entered mode bracketed. Writing boolean 0 or 1 to
this attribute starts or stops the mode selection process,
respectively.
`entry_result`, `usb4_entry_result` read-only attributes show the
result of the last mode selection attempt for a specific mode.
Signed-off-by: Andrei Kuchynski <akuchynski@...omium.org>
---
Documentation/ABI/testing/sysfs-class-typec | 39 ++
drivers/usb/typec/class.c | 95 ++++-
drivers/usb/typec/class.h | 12 +
drivers/usb/typec/mode_selection.c | 445 ++++++++++++++++++++
drivers/usb/typec/mode_selection.h | 31 ++
include/linux/usb/pd_vdo.h | 2 +
include/linux/usb/typec_altmode.h | 5 +
7 files changed, 626 insertions(+), 3 deletions(-)
diff --git a/Documentation/ABI/testing/sysfs-class-typec b/Documentation/ABI/testing/sysfs-class-typec
index 575dd94f33ab..ed89b9880085 100644
--- a/Documentation/ABI/testing/sysfs-class-typec
+++ b/Documentation/ABI/testing/sysfs-class-typec
@@ -280,6 +280,45 @@ Description: The USB Modes that the partner device supports. The active mode
- usb3 (USB 3.2)
- usb4 (USB4)
+What: /sys/class/typec/<port>-partner/mode_selection
+Date: July 2025
+Contact: Andrei Kuchynski <akuchynski@...omium.org>
+Description: Displays a prioritized list of modes that both the port and the
+ partner support with the currently entered mode bracketed. Parentheses
+ indicates a mode currently in progress. Modes listed before the active
+ or in-progress mode have failed.
+ Automated mode selection is activated by writing boolean 1 to the
+ file. Conversely, writing boolean 0 will cancel any ongoing selection
+ process and exit the currently active mode, if any.
+ This attribute is only present if the kernel supports AP driven mode
+ entry, where the Application Processor manages USB Type-C alt-modes.
+
+ Example values:
+ - "USB4 (TBT) DP": USB4 mode entry failed, Thunderbolt alt-mode is in
+ progress, DisplayPort alt-mode is next.
+ - "[USB4] TBT DP": USB4 mode is currently active.
+
+What: /sys/class/typec/<port>-partner/<alt-mode>/entry_result
+Date: July 2025
+Contact: Andrei Kuchynski <akuchynski@...omium.org>
+Description: This read-only file represents the status for a specific
+ alt-mode after the last mode selection process.
+ This attribute is visible only if the kernel supports mode selection.
+
+ Example values:
+ - "none": No mode selection attempt has occurred for this alt-mode.
+ - "in progress": The mode entry process is currently underway.
+ - "active": The alt-mode is currently active.
+ - "cable failed": The connected cable doesn't support the mode.
+ - "timeout": Mode entry failed due to a timeout.
+ - "failed": The attempt to activate the mode failed.
+
+What: /sys/class/typec/<port>-partner/usb4_entry_result
+Date: July 2025
+Contact: Andrei Kuchynski <akuchynski@...omium.org>
+Description: Represents a status for USB4 mode. Its values are identical to
+ the general <alt-mode>/entry_result attributes.
+
USB Type-C cable devices (eg. /sys/class/typec/port0-cable/)
Note: Electronically Marked Cables will have a device also for one cable plug
diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
index 414d94c45ab9..f9515fc594f8 100644
--- a/drivers/usb/typec/class.c
+++ b/drivers/usb/typec/class.c
@@ -479,12 +479,24 @@ static ssize_t priority_show(struct device *dev,
}
static DEVICE_ATTR_RW(priority);
+static ssize_t entry_result_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct typec_altmode *adev = to_typec_altmode(dev);
+ struct typec_partner *partner = to_typec_partner(adev->dev.parent);
+
+ return typec_mode_selection_get_result(partner,
+ typec_svid_to_altmode(adev->svid), buf);
+}
+static DEVICE_ATTR_RO(entry_result);
+
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,
+ &dev_attr_entry_result.attr,
NULL
};
@@ -508,6 +520,17 @@ static umode_t typec_altmode_attr_is_visible(struct kobject *kobj,
if (is_typec_port(adev->dev.parent)) {
struct typec_port *port = to_typec_port(adev->dev.parent);
+ if (!port->alt_mode_override)
+ return 0;
+ } else
+ return 0;
+ } else if (attr == &dev_attr_entry_result.attr) {
+ if (is_typec_partner(adev->dev.parent)) {
+ struct typec_partner *partner =
+ to_typec_partner(adev->dev.parent);
+ struct typec_port *port =
+ to_typec_port(partner->dev.parent);
+
if (!port->alt_mode_override)
return 0;
} else
@@ -584,7 +607,7 @@ static void typec_altmode_release(struct device *dev)
}
const struct device_type typec_altmode_dev_type = {
- .name = "typec_alternate_mode",
+ .name = ALTERNATE_MODE_DEVICE_TYPE_NAME,
.groups = typec_altmode_groups,
.release = typec_altmode_release,
};
@@ -784,6 +807,44 @@ static ssize_t number_of_alternate_modes_show(struct device *dev, struct device_
}
static DEVICE_ATTR_RO(number_of_alternate_modes);
+static ssize_t mode_selection_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct typec_partner *partner = to_typec_partner(dev);
+
+ return typec_mode_selection_get_active(partner, buf);
+}
+
+static ssize_t mode_selection_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct typec_partner *partner = to_typec_partner(dev);
+ bool start;
+ int ret = kstrtobool(buf, &start);
+
+ if (!ret) {
+ if (start)
+ ret = typec_mode_selection_start(partner);
+ else
+ ret = typec_mode_selection_reset(partner);
+ }
+
+ if (ret)
+ return ret;
+
+ return size;
+}
+static DEVICE_ATTR_RW(mode_selection);
+
+static ssize_t usb4_entry_result_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return typec_mode_selection_get_result(to_typec_partner(dev),
+ TYPEC_USB4_MODE, buf);
+}
+static DEVICE_ATTR_RO(usb4_entry_result);
+
static struct attribute *typec_partner_attrs[] = {
&dev_attr_accessory_mode.attr,
&dev_attr_supports_usb_power_delivery.attr,
@@ -791,6 +852,8 @@ static struct attribute *typec_partner_attrs[] = {
&dev_attr_type.attr,
&dev_attr_usb_mode.attr,
&dev_attr_usb_power_delivery_revision.attr,
+ &dev_attr_mode_selection.attr,
+ &dev_attr_usb4_entry_result.attr,
NULL
};
@@ -815,6 +878,16 @@ static umode_t typec_partner_attr_is_visible(struct kobject *kobj, struct attrib
if (!get_pd_product_type(kobj_to_dev(kobj)))
return 0;
+ if (attr == &dev_attr_mode_selection.attr)
+ if (!port->alt_mode_override)
+ return 0;
+
+ if (attr == &dev_attr_usb4_entry_result.attr) {
+ if (!port->alt_mode_override ||
+ !(partner->usb_capability & USB_CAPABILITY_USB4))
+ return 0;
+ }
+
return attr->mode;
}
@@ -893,8 +966,10 @@ int typec_partner_set_identity(struct typec_partner *partner)
usb_capability |= USB_CAPABILITY_USB2;
if (devcap & DEV_USB3_CAPABLE)
usb_capability |= USB_CAPABILITY_USB3;
- if (devcap & DEV_USB4_CAPABLE)
+ if (devcap & DEV_USB4_CAPABLE) {
usb_capability |= USB_CAPABILITY_USB4;
+ typec_mode_selection_add_mode(partner, TYPEC_USB4_MODE);
+ }
} else {
usb_capability = PD_VDO_DFP_HOSTCAP(id->vdo[0]);
}
@@ -1014,7 +1089,12 @@ struct typec_altmode *
typec_partner_register_altmode(struct typec_partner *partner,
const struct typec_altmode_desc *desc)
{
- return typec_register_altmode(&partner->dev, desc);
+ struct typec_altmode *alt = typec_register_altmode(&partner->dev, desc);
+
+ if (alt)
+ typec_mode_selection_add_mode(partner, typec_svid_to_altmode(alt->svid));
+
+ return alt;
}
EXPORT_SYMBOL_GPL(typec_partner_register_altmode);
@@ -1118,6 +1198,8 @@ struct typec_partner *typec_register_partner(struct typec_port *port,
typec_partner_link_device(partner, port->usb3_dev);
mutex_unlock(&port->partner_link_lock);
+ typec_mode_selection_add_partner(partner);
+
return partner;
}
EXPORT_SYMBOL_GPL(typec_register_partner);
@@ -1135,6 +1217,7 @@ void typec_unregister_partner(struct typec_partner *partner)
if (IS_ERR_OR_NULL(partner))
return;
+ typec_mode_selection_remove_partner(partner);
port = to_typec_port(partner->dev.parent);
mutex_lock(&port->partner_link_lock);
@@ -1403,6 +1486,7 @@ int typec_cable_set_identity(struct typec_cable *cable)
}
EXPORT_SYMBOL_GPL(typec_cable_set_identity);
+static struct typec_partner *typec_get_partner(struct typec_port *port);
/**
* typec_register_cable - Register a USB Type-C Cable
* @port: The USB Type-C Port the cable is connected to
@@ -1417,6 +1501,7 @@ struct typec_cable *typec_register_cable(struct typec_port *port,
struct typec_cable_desc *desc)
{
struct typec_cable *cable;
+ struct typec_partner *partner;
int ret;
cable = kzalloc(sizeof(*cable), GFP_KERNEL);
@@ -1448,6 +1533,10 @@ struct typec_cable *typec_register_cable(struct typec_port *port,
return ERR_PTR(ret);
}
+ partner = typec_get_partner(port);
+ typec_mode_selection_add_cable(partner, cable);
+ put_device(&partner->dev);
+
return cable;
}
EXPORT_SYMBOL_GPL(typec_register_cable);
diff --git a/drivers/usb/typec/class.h b/drivers/usb/typec/class.h
index c6467e576569..281dcb6d675c 100644
--- a/drivers/usb/typec/class.h
+++ b/drivers/usb/typec/class.h
@@ -5,6 +5,8 @@
#include <linux/device.h>
#include <linux/usb/typec.h>
+#include <linux/usb/typec_altmode.h>
+#include <linux/kfifo.h>
struct typec_mux;
struct typec_switch;
@@ -26,6 +28,8 @@ struct typec_cable {
enum usb_pd_svdm_ver svdm_version;
};
+struct mode_selection_state;
+
struct typec_partner {
struct device dev;
unsigned int usb_pd:1;
@@ -40,6 +44,12 @@ struct typec_partner {
struct usb_power_delivery *pd;
+ struct delayed_work mode_selection_work;
+ DECLARE_KFIFO(mode_sequence, struct mode_selection_state *,
+ roundup_pow_of_two(TYPEC_MODE_MAX));
+ struct mutex mode_sequence_lock;
+ struct mode_selection_state *active_mode;
+
void (*attach)(struct typec_partner *partner, struct device *dev);
void (*deattach)(struct typec_partner *partner, struct device *dev);
};
@@ -112,4 +122,6 @@ static inline int typec_link_ports(struct typec_port *connector) { return 0; }
static inline void typec_unlink_ports(struct typec_port *connector) { }
#endif
+#define ALTERNATE_MODE_DEVICE_TYPE_NAME "typec_alternate_mode"
+
#endif /* __USB_TYPEC_CLASS__ */
diff --git a/drivers/usb/typec/mode_selection.c b/drivers/usb/typec/mode_selection.c
index 9a7185c07d0c..c7d164478787 100644
--- a/drivers/usb/typec/mode_selection.c
+++ b/drivers/usb/typec/mode_selection.c
@@ -5,10 +5,19 @@
#include <linux/usb/typec_altmode.h>
#include <linux/slab.h>
+#include <linux/usb/pd_vdo.h>
+#include <linux/kfifo.h>
#include <linux/list.h>
#include "mode_selection.h"
#include "class.h"
+/* Timeout for a mode entry attempt, ms */
+static const unsigned int mode_selection_timeout = 4000;
+/* Delay between mode entry/exit attempts, ms */
+static const unsigned int mode_selection_delay = 1000;
+/* Maximum retries for mode entry on busy status */
+static const unsigned int mode_entry_attempts = 4;
+
static const char * const mode_names[TYPEC_MODE_MAX] = {
[TYPEC_DP_ALTMODE] = "DisplayPort",
[TYPEC_TBT_ALTMODE] = "Thunderbolt3",
@@ -21,18 +30,59 @@ static const int default_priorities[TYPEC_MODE_MAX] = {
[TYPEC_USB4_MODE] = 0,
};
+/**
+ * enum ms_state - Specific mode selection states
+ * @MS_STATE_IDLE: The mode entry process has not started
+ * @MS_STATE_INPROGRESS: The mode entry process is currently underway
+ * @MS_STATE_ACTIVE: The mode has been successfully entered
+ * @MS_STATE_CABLE_FAILED: The connected cable doesn't support the mode
+ * @MS_STATE_TIMEOUT: Mode entry failed due to a timeout
+ * @MS_STATE_FAILED: The mode driver reported the error
+ */
+enum ms_state {
+ MS_STATE_IDLE = 0,
+ MS_STATE_INPROGRESS,
+ MS_STATE_ACTIVE,
+ MS_STATE_CABLE_FAILED,
+ MS_STATE_TIMEOUT,
+ MS_STATE_FAILED,
+ MS_STATE_MAX
+};
+static const char * const ms_state_strings[MS_STATE_MAX] = {
+ [MS_STATE_IDLE] = "none",
+ [MS_STATE_INPROGRESS] = "in progress",
+ [MS_STATE_ACTIVE] = "active",
+ [MS_STATE_CABLE_FAILED] = "cable failed",
+ [MS_STATE_TIMEOUT] = "timeout",
+ [MS_STATE_FAILED] = "failed",
+};
+
/**
* struct mode_selection_state - State tracking for a specific Type-C mode
* @mode: The type of mode this instance represents
* @name: Name string pointer
* @priority: The mode priority. Higher values indicate a more preferred mode.
* @list: List head to link this mode state into a prioritized list.
+ * @partner_supported: Flag indicating if this mode is supported by the partner
+ * @cable_supported: Flag indicating if this mode is supported by the cable
+ * @enter: Flag indicating if the driver is currently attempting to enter or
+ * exit the mode
+ * @attempt_count: Number of times the driver has attempted to enter the mode
+ * @state: The current mode selection state
+ * @error: The outcome of the last attempt to enter the mode
*/
struct mode_selection_state {
enum typec_mode_type mode;
const char *name;
int priority;
struct list_head list;
+
+ bool partner_supported;
+ bool cable_supported;
+ bool enter;
+ int attempt_count;
+ enum ms_state state;
+ int error;
};
/* -------------------------------------------------------------------------- */
@@ -128,3 +178,398 @@ ssize_t typec_mode_get_priority_list(struct typec_port *port, char *buf)
return count + sysfs_emit_at(buf, count, "\n");
}
+
+/* -------------------------------------------------------------------------- */
+/* partner 'mod_selection' attribute */
+
+/**
+ * mode_selection_next() - Process mode selection results and schedule next
+ * action
+ * @partner: pointer to the partner structure
+ * @ms: pointer to active mode_selection_state object that is on top in
+ * mode_sequence FIFO.
+ *
+ * The mutex protecting the mode_sequence FIFO must be held by the caller
+ * when invoking this function.
+ *
+ * This function evaluates the outcome of the previous mode entry or exit
+ * attempt. Based on this result, it determines the next mode to process and
+ * schedules `mode_selection_work()` if further actions are required.
+ *
+ * If the previous mode entry was successful, the mode selection sequence is
+ * considered complete for the current cycle.
+ *
+ * If the previous mode entry failed, this function schedules
+ * `mode_selection_work()` to attempt exiting the mode that was partially
+ * activated but not fully entered.
+ *
+ * If the previous operation was an exit (after a failed entry attempt),
+ * `mode_selection_next()` then advances the internal list of candidate
+ * modes to determine the next mode to enter.
+ */
+static void mode_selection_next(
+ struct typec_partner *partner, struct mode_selection_state *ms)
+
+ __must_hold(&partner->mode_sequence_lock)
+{
+ if (!ms->enter) {
+ kfifo_skip(&partner->mode_sequence);
+ } else if (ms->state == MS_STATE_INPROGRESS && !ms->error) {
+ ms->state = MS_STATE_ACTIVE;
+ partner->active_mode = ms;
+ kfifo_reset(&partner->mode_sequence);
+ } else {
+ if (ms->error) {
+ ms->state = MS_STATE_FAILED;
+ dev_dbg(&partner->dev, "%s: entry mode error %pe\n",
+ ms->name, ERR_PTR(ms->error));
+ }
+ if (ms->error != -EBUSY || ms->attempt_count >= mode_entry_attempts)
+ ms->enter = false;
+ }
+
+ if (!kfifo_is_empty(&partner->mode_sequence))
+ schedule_delayed_work(&partner->mode_selection_work,
+ msecs_to_jiffies(mode_selection_delay));
+}
+
+static void mode_selection_complete(struct typec_partner *partner,
+ const enum typec_mode_type mode, const int error)
+{
+ struct mode_selection_state *ms;
+
+ mutex_lock(&partner->mode_sequence_lock);
+ if (kfifo_peek(&partner->mode_sequence, &ms)) {
+ if (ms->mode == mode && ms->state == MS_STATE_INPROGRESS) {
+ ms->error = error;
+ cancel_delayed_work(&partner->mode_selection_work);
+ mode_selection_next(partner, ms);
+ }
+ }
+ mutex_unlock(&partner->mode_sequence_lock);
+}
+
+void typec_mode_selection_altmode_complete(struct typec_altmode *alt,
+ const int error)
+{
+ mode_selection_complete(to_typec_partner(alt->dev.parent),
+ typec_svid_to_altmode(alt->svid), error);
+}
+EXPORT_SYMBOL_GPL(typec_mode_selection_altmode_complete);
+
+void typec_mode_selection_usb4_complete(struct typec_partner *partner,
+ const int error)
+{
+ mode_selection_complete(partner, TYPEC_USB4_MODE, error);
+}
+EXPORT_SYMBOL_GPL(typec_mode_selection_usb4_complete);
+
+static void mode_selection_activate_usb4_mode(struct typec_partner *partner,
+ struct mode_selection_state *ms)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ int error = -EOPNOTSUPP;
+
+ if (port->ops && port->ops->enter_usb_mode) {
+ if (ms->enter && port->usb_mode != USB_MODE_USB4)
+ error = -EPERM;
+ else
+ error = port->ops->enter_usb_mode(port,
+ ms->enter ? USB_MODE_USB4 : USB_MODE_USB3);
+ }
+
+ if (ms->enter)
+ ms->error = error;
+}
+
+static int mode_selection_activate_altmode(struct device *dev, void *data)
+{
+ struct typec_altmode *alt = to_typec_altmode(dev);
+ struct mode_selection_state *ms = (struct mode_selection_state *)data;
+ int error = -ENODEV;
+ int ret = 0;
+
+ if (!strcmp(dev->type->name, ALTERNATE_MODE_DEVICE_TYPE_NAME)) {
+ if (ms->mode == typec_svid_to_altmode(alt->svid)) {
+ if (alt->ops && alt->ops->activate)
+ error = alt->ops->activate(alt, ms->enter);
+ else
+ error = -EOPNOTSUPP;
+ ret = 1;
+ }
+ }
+
+ if (ms->enter)
+ ms->error = error;
+
+ return ret;
+}
+
+static void mode_selection_activate_mode(struct typec_partner *partner,
+ struct mode_selection_state *ms)
+{
+ if (ms->enter)
+ ms->attempt_count++;
+
+ if (ms->mode == TYPEC_USB4_MODE)
+ mode_selection_activate_usb4_mode(partner, ms);
+ else
+ device_for_each_child(&partner->dev, ms,
+ mode_selection_activate_altmode);
+}
+
+/**
+ * mode_selection_work() - Activate entry into the upcoming mode
+ * @work: work structure
+ *
+ * This function works in conjunction with `mode_selection_next()`.
+ * It attempts to activate the next mode in the selection sequence.
+ *
+ * If the mode activation (`mode_selection_activate_mode()`) fails,
+ * `mode_selection_next()` will be called to initiate a new selection cycle.
+ *
+ * Otherwise, the state is set to MS_STATE_INPROGRESS, and
+ * `mode_selection_work()` is scheduled for a subsequent entry after a timeout
+ * period. The alternate mode driver is expected to call back with the actual
+ * mode entry result. Upon this callback, `mode_selection_next()` will determine
+ * the subsequent mode and re-schedule `mode_selection_work()`.
+ */
+static void mode_selection_work(struct work_struct *work)
+{
+ struct typec_partner *partner = container_of(work, struct typec_partner,
+ mode_selection_work.work);
+ struct mode_selection_state *ms;
+
+ mutex_lock(&partner->mode_sequence_lock);
+ if (kfifo_peek(&partner->mode_sequence, &ms)) {
+ if (ms->state == MS_STATE_INPROGRESS) {
+ ms->state = MS_STATE_TIMEOUT;
+ mode_selection_next(partner, ms);
+ } else {
+ mode_selection_activate_mode(partner, ms);
+
+ if (ms->enter && !ms->error) {
+ ms->state = MS_STATE_INPROGRESS;
+ schedule_delayed_work(&partner->mode_selection_work,
+ msecs_to_jiffies(mode_selection_timeout));
+ } else
+ mode_selection_next(partner, ms);
+ }
+ }
+ mutex_unlock(&partner->mode_sequence_lock);
+}
+
+static void mode_selection_clear_results(struct typec_partner *partner)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ struct mode_selection_state *ms;
+
+ list_for_each_entry(ms, &port->mode_list, list) {
+ ms->enter = true;
+ ms->state = MS_STATE_IDLE;
+ ms->error = 0;
+ ms->attempt_count = 0;
+ }
+
+ kfifo_reset(&partner->mode_sequence);
+ partner->active_mode = NULL;
+}
+
+void typec_mode_selection_add_partner(struct typec_partner *partner)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ struct mode_selection_state *ms;
+
+ list_for_each_entry(ms, &port->mode_list, list) {
+ ms->partner_supported = false;
+ ms->cable_supported = false;
+ }
+
+ INIT_KFIFO(partner->mode_sequence);
+ mutex_init(&partner->mode_sequence_lock);
+ mode_selection_clear_results(partner);
+ INIT_DELAYED_WORK(&partner->mode_selection_work, mode_selection_work);
+}
+
+void typec_mode_selection_add_mode(struct typec_partner *partner,
+ const enum typec_mode_type mode)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ struct mode_selection_state *ms;
+
+ list_for_each_entry(ms, &port->mode_list, list) {
+ if (ms->mode == mode) {
+ ms->partner_supported = true;
+ break;
+ }
+ }
+}
+
+void typec_mode_selection_add_cable(struct typec_partner *partner,
+ struct typec_cable *cable)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ struct mode_selection_state *ms;
+ const u32 id_header = cable->identity->id_header;
+ const u32 vdo1 = cable->identity->vdo[0];
+ const u32 type = PD_IDH_PTYPE(id_header);
+ const u32 speed = VDO_TYPEC_CABLE_SPEED(vdo1);
+ /*
+ * Some USB devices supporting DisplayPort lack valid cable VDO.
+ * Allow only DP mode in this case.
+ */
+ bool capability[TYPEC_MODE_MAX] = {
+ [TYPEC_DP_ALTMODE] = true,
+ [TYPEC_TBT_ALTMODE] = false,
+ [TYPEC_USB4_MODE] = false,
+ };
+
+ if (type == IDH_PTYPE_PCABLE) {
+ capability[TYPEC_DP_ALTMODE] = (speed > CABLE_USB2_ONLY);
+ capability[TYPEC_TBT_ALTMODE] = (speed > CABLE_USB2_ONLY);
+ capability[TYPEC_USB4_MODE] = (speed > CABLE_USB2_ONLY);
+ } else if (type == IDH_PTYPE_ACABLE) {
+ const u32 vdo2 = cable->identity->vdo[1];
+ const u32 version = VDO_TYPEC_CABLE_VERSION(vdo1);
+ const bool usb4_support = VDO_TYPEC_CABLE_USB4_SUPP(vdo2);
+ const bool modal_support = PD_IDH_MODAL_SUPP(id_header);
+
+ capability[TYPEC_DP_ALTMODE] = modal_support;
+ capability[TYPEC_TBT_ALTMODE] = true;
+ if (version == CABLE_VDO_VER1_3)
+ capability[TYPEC_USB4_MODE] = usb4_support;
+ else
+ capability[TYPEC_USB4_MODE] = modal_support;
+ }
+
+ list_for_each_entry(ms, &port->mode_list, list)
+ ms->cable_supported = capability[ms->mode];
+}
+
+void typec_mode_selection_remove_partner(struct typec_partner *partner)
+{
+ mutex_lock(&partner->mode_sequence_lock);
+ kfifo_reset(&partner->mode_sequence);
+ mutex_unlock(&partner->mode_sequence_lock);
+
+ cancel_delayed_work_sync(&partner->mode_selection_work);
+ mutex_destroy(&partner->mode_sequence_lock);
+}
+
+/**
+ * typec_mode_selection_start() - Starts the mode selection process.
+ * @partner: pointer to the partner structure
+ *
+ * This function populates a 'mode_sequence' FIFO with pointers to
+ * `struct mode_selection_state` instances. The sequence is generated based on
+ * partner/cable capabilities and prioritized according to the port's settings.
+ */
+int typec_mode_selection_start(struct typec_partner *partner)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ struct mode_selection_state *ms;
+ int ret = 0;
+
+ mutex_lock(&partner->mode_sequence_lock);
+
+ if (!kfifo_is_empty(&partner->mode_sequence))
+ ret = -EINPROGRESS;
+ else if (partner->active_mode)
+ ret = -EALREADY;
+ else {
+ mode_selection_clear_results(partner);
+
+ list_for_each_entry(ms, &port->mode_list, list) {
+ if (ms->partner_supported) {
+ if (ms->cable_supported)
+ kfifo_put(&partner->mode_sequence, ms);
+ else
+ ms->state = MS_STATE_CABLE_FAILED;
+ }
+ }
+
+ if (kfifo_peek(&partner->mode_sequence, &ms))
+ schedule_delayed_work(&partner->mode_selection_work, 0);
+ }
+
+ mutex_unlock(&partner->mode_sequence_lock);
+
+ return ret;
+}
+
+/**
+ * typec_mode_selection_reset() - Reset the mode selection process.
+ * @partner: pointer to the partner structure
+ *
+ * This function cancels ongoing mode selection and exits the currently active
+ * mode, if present.
+ * It returns -EINPROGRESS when a mode exit is already scheduled, or a mode
+ * entry is ongoing, indicating that the reset cannot immediately complete.
+ */
+int typec_mode_selection_reset(struct typec_partner *partner)
+{
+ struct mode_selection_state *ms;
+
+ mutex_lock(&partner->mode_sequence_lock);
+ if (kfifo_peek(&partner->mode_sequence, &ms)) {
+ kfifo_reset(&partner->mode_sequence);
+
+ if (!ms->enter || ms->state != MS_STATE_IDLE) {
+ ms->attempt_count = mode_entry_attempts;
+ kfifo_put(&partner->mode_sequence, ms);
+ mutex_unlock(&partner->mode_sequence_lock);
+
+ return -EINPROGRESS;
+ }
+ }
+
+ if (partner->active_mode) {
+ partner->active_mode->enter = false;
+ mode_selection_activate_mode(partner, partner->active_mode);
+ }
+ mode_selection_clear_results(partner);
+ mutex_unlock(&partner->mode_sequence_lock);
+
+ return 0;
+}
+
+int typec_mode_selection_get_active(struct typec_partner *partner, char *buf)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ struct mode_selection_state *ms, *running_ms;
+ ssize_t count = 0;
+
+ mutex_lock(&partner->mode_sequence_lock);
+ if (!kfifo_peek(&partner->mode_sequence, &running_ms))
+ running_ms = NULL;
+
+ list_for_each_entry(ms, &port->mode_list, list) {
+ if (ms->partner_supported) {
+ if (ms->state == MS_STATE_ACTIVE)
+ count += sysfs_emit_at(buf, count, "[%s] ", ms->name);
+ else if (ms == running_ms)
+ count += sysfs_emit_at(buf, count, "(%s) ", ms->name);
+ else
+ count += sysfs_emit_at(buf, count, "%s ", ms->name);
+ }
+ }
+ mutex_unlock(&partner->mode_sequence_lock);
+
+ if (count)
+ count += sysfs_emit_at(buf, count, "\n");
+
+ return count;
+}
+
+int typec_mode_selection_get_result(struct typec_partner *partner,
+ const enum typec_mode_type mode, char *buf)
+{
+ struct typec_port *port = to_typec_port(partner->dev.parent);
+ struct mode_selection_state *ms;
+
+ list_for_each_entry(ms, &port->mode_list, list)
+ if (ms->mode == mode)
+ return sysfs_emit(buf, "%s\n", ms_state_strings[ms->state]);
+
+ return -EOPNOTSUPP;
+}
diff --git a/drivers/usb/typec/mode_selection.h b/drivers/usb/typec/mode_selection.h
index 151f0f8b6632..2238a7200eae 100644
--- a/drivers/usb/typec/mode_selection.h
+++ b/drivers/usb/typec/mode_selection.h
@@ -21,3 +21,34 @@ int typec_mode_set_priority(struct typec_port *port,
int typec_mode_get_priority(struct typec_port *port,
const enum typec_mode_type mode, int *priority);
ssize_t typec_mode_get_priority_list(struct typec_port *port, char *buf);
+
+/**
+ * The mode selection process follows a lifecycle tied to the USB-C partner
+ * device. The API is designed to first build a set of desired modes and then
+ * trigger the selection process. The expected sequence of calls is as follows:
+ *
+ * Creation and Configuration:
+ * call typec_mode_selection_add_partner() when the partner device is being set
+ * up. After creation, call typec_mode_selection_add_mode() and
+ * typec_mode_selection_add_cable() to define the parameters for the
+ * selection process.
+ *
+ * Execution:
+ * Call typec_mode_selection_start() to trigger the mode selection.
+ * Call typec_mode_selection_reset() to prematurely stop the selection
+ * process and clear any stored results.
+ *
+ * Destruction:
+ * Before destroying a partner, call typec_mode_selection_remove_partner()
+ */
+void typec_mode_selection_add_partner(struct typec_partner *partner);
+void typec_mode_selection_remove_partner(struct typec_partner *partner);
+int typec_mode_selection_start(struct typec_partner *partner);
+int typec_mode_selection_reset(struct typec_partner *partner);
+void typec_mode_selection_add_mode(struct typec_partner *partner,
+ const enum typec_mode_type mode);
+void typec_mode_selection_add_cable(struct typec_partner *partner,
+ struct typec_cable *cable);
+int typec_mode_selection_get_active(struct typec_partner *partner, char *buf);
+int typec_mode_selection_get_result(struct typec_partner *partner,
+ const enum typec_mode_type mode, char *buf);
diff --git a/include/linux/usb/pd_vdo.h b/include/linux/usb/pd_vdo.h
index 5c48e8a81403..20bcf37ad634 100644
--- a/include/linux/usb/pd_vdo.h
+++ b/include/linux/usb/pd_vdo.h
@@ -439,6 +439,8 @@
| (trans) << 11 | (phy) << 10 | (ele) << 9 | (u4) << 8 \
| ((hops) & 0x3) << 6 | (u2) << 5 | (u32) << 4 | (lane) << 3 \
| (iso) << 2 | (gen))
+#define VDO_TYPEC_CABLE_VERSION(vdo) (((vdo) >> 21) & 0x7)
+#define VDO_TYPEC_CABLE_USB4_SUPP(vdo) (((vdo) & BIT(8)) == ACAB2_USB4_SUPP)
/*
* AMA VDO (PD Rev2.0)
diff --git a/include/linux/usb/typec_altmode.h b/include/linux/usb/typec_altmode.h
index 5d14363e02eb..f7fd51b4c23e 100644
--- a/include/linux/usb/typec_altmode.h
+++ b/include/linux/usb/typec_altmode.h
@@ -225,4 +225,9 @@ void typec_altmode_unregister_driver(struct typec_altmode_driver *drv);
module_driver(__typec_altmode_driver, typec_altmode_register_driver, \
typec_altmode_unregister_driver)
+void typec_mode_selection_altmode_complete(struct typec_altmode *alt,
+ const int result);
+void typec_mode_selection_usb4_complete(struct typec_partner *partner,
+ const int result);
+
#endif /* __USB_TYPEC_ALTMODE_H */
--
2.50.1.565.gc32cd1483b-goog
Powered by blists - more mailing lists