From nobody Mon Feb 9 19:25:34 2026 Received: from mail-ej1-f47.google.com (mail-ej1-f47.google.com [209.85.218.47]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 142D8368271 for ; Mon, 19 Jan 2026 13:18:50 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.218.47 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1768828735; cv=none; b=r7cXs91GbYQYwoy2atdL31nySfpNeCqn8pgO9gcvSMBEHrDx98k+gZxF4mdp2NO8bZ15WqDxvgBJvKNg+EijRlVTFP2GdzQm57mu/pn7Z8FcowCvFR6oLWqWFu31ihkGCFcMmJzOteXsPqte/2cIBaTYqYK93uK5nmWnaZWfa8Y= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1768828735; c=relaxed/simple; bh=W5rVUeJ0ZYQc7VJJqv7IN5MlC5nYGPDBCCaodck9cLg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=JD/qCSkCacdW+jEDMIt0qrWUzzlGvZ90DF+aLtseA/V/eEW5+DLM/7m8m3l4jaAcQbVGCgL4MaxaqmW58iHD3W92ToRo1E+qJjnHl+6FhMYKMq/gESB38NmvDjhsUN2NLSfUhCMTOuOmxYyyfip8hCbUZxge9OE9GRMPwtQF5as= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=chromium.org; spf=pass smtp.mailfrom=chromium.org; dkim=pass (1024-bit key) header.d=chromium.org header.i=@chromium.org header.b=Epsk9GYk; arc=none smtp.client-ip=209.85.218.47 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=chromium.org Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=chromium.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=chromium.org header.i=@chromium.org header.b="Epsk9GYk" Received: by mail-ej1-f47.google.com with SMTP id a640c23a62f3a-b87f3c684a1so40651866b.0 for ; Mon, 19 Jan 2026 05:18:50 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=chromium.org; s=google; t=1768828729; x=1769433529; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=6Z12NdnfPArhakjxM92nLWa6k5fhcIcqz5sumyUrY3w=; b=Epsk9GYkvkXwJwGseMkCl4z3b7pX6KQMt9oaqUeWVCvTM5oANgO32qLDcVtmAisf1L Ktm7lDyr7oZPMi2dpCbyBav9Shc9AdAu4oEvTi3B4tGfIVlU/J48ehAR85kbP55U6zLa vnaWtDzd8KYCE2vJZC5VNFJxhL3dJvU9ZWvOI= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1768828729; x=1769433529; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=6Z12NdnfPArhakjxM92nLWa6k5fhcIcqz5sumyUrY3w=; b=CbXVQpTE2UuPmnTplkNLgubp3JoaOqFT9NmisChHEv9JRFwT0G9Dttj63tn6yJz/J/ wLU4Je8/3RHgQ66b1YvkE8TNFJ8tveJC6hkRMwZNUB9AjPIiJO/Vg7HVzv8fILreAdmi 4zbJk9/D9TDXL+qgPYRqk6Ix5KjxtHYzUSnIYGnpvhegy5iElKa1QdRFCjSQsf9/QItX HFRQNRyvicIp+3tH45Y99u5OMhycM2sB1dJDs4ZfrcZhPhTLV+O0KJBl+fTjAOo/fnYH OCpIfsj7AX1i6XFCiBd/crkaZzf9gOGyIJLSIKuP7yqwyKDcqNxrqCvcsPMd9sjk4X1D ITVw== X-Forwarded-Encrypted: i=1; AJvYcCXqWouYn7VEfLdNE6gsXN5szpWjBhz1n8lHbRf/PtpzRQWh6Qir+7JR5EXvbQGAdwnm2+Rv277oahIqXlI=@vger.kernel.org X-Gm-Message-State: AOJu0Yy1siUwDF2u6bFL2BvwTXESgPF645m/Wm/+ZHEnJj1H+05XHkwQ qIZLOc/izCNv6KRhVuCeF1WUcjFWszSavfeNyl88rTOahY+sCMruMigXBjVOj5qxcA== X-Gm-Gg: AY/fxX5CTFXDPo0tXkbsipoFrLGMLbhiQbYJfBj3CCjhoM4R0nObaHZlXLsVfgh0kHZ dSkzZCpnjgJsznzVDeLBWoc7bn4eBIR6VCwOPghits930FBUUZZExN1PzVr4Q4wQy481RGRmoiI MywqNKQbsNdsMUIlE+D17y+TvhWe7IrW34keUM1PSsRjBCptCGVmLgDCfnaeQH0+JOxgpOYMrnL dA59CyaMRHXYtgOaUutMAysmteKHb4XiGz2D/3ihJAV6Jbf9R2Azeg49lS0gJ2fpnjm/sfN22// UOrRP0KcdA9a/uu8J2PHYk5OhgsFVLs2laJVGd/YcDVF+u7qHFVCiz8FNIz1U6G7IK5uQRhi8ER Z0sTkUq7s1W0CVIqsE0x2ngiMrkWO3NFVSV0mkGtOTlGlz/GEOaViJjCjSdawWXnWOdMWGl+mgf gTu+TJiarx+tiE0mtuV2iqAGi/PNXWsVduKofKlGXuJEgWPXfs3cNqniDVGL2izJEcJ4HqZ2Bd9 EvAQrba X-Received: by 2002:a17:906:4793:b0:b87:3809:6982 with SMTP id a640c23a62f3a-b8793254fe1mr1093930866b.57.1768828729213; Mon, 19 Jan 2026 05:18:49 -0800 (PST) Received: from akuchynski.c.googlers.com.com (150.230.32.34.bc.googleusercontent.com. [34.32.230.150]) by smtp.gmail.com with ESMTPSA id a640c23a62f3a-b87959c9a14sm1078801866b.45.2026.01.19.05.18.48 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 19 Jan 2026 05:18:48 -0800 (PST) From: Andrei Kuchynski To: Heikki Krogerus , Abhishek Pandit-Subedi , Benson Leung , Jameson Thies , linux-usb@vger.kernel.org, linux-kernel@vger.kernel.org, chrome-platform@lists.linux.dev Cc: Tzung-Bi Shih , Guenter Roeck , Greg Kroah-Hartman , Dmitry Baryshkov , =?UTF-8?q?=C5=81ukasz=20Bartosik?= , Pooja Katiyar , Johan Hovold , Hsin-Te Yuan , Madhu M , Venkat Jayaraman , Andrei Kuchynski Subject: [PATCH v5 4/7] usb: typec: Implement mode selection Date: Mon, 19 Jan 2026 13:18:21 +0000 Message-ID: <20260119131824.2529334-5-akuchynski@chromium.org> X-Mailer: git-send-email 2.52.0.457.g6b5491de43-goog In-Reply-To: <20260119131824.2529334-1-akuchynski@chromium.org> References: <20260119131824.2529334-1-akuchynski@chromium.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" 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 Reviewed-by: Heikki Krogerus --- 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) +=3D typec.o -typec-y :=3D class.o mux.o bus.o pd.o retimer.o +typec-y :=3D class.o mux.o bus.o pd.o retimer.o mode_selection.o typec-$(CONFIG_ACPI) +=3D port-mapper.o obj-$(CONFIG_TYPEC) +=3D altmodes/ obj-$(CONFIG_TYPEC_TCPM) +=3D 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; =20 struct typec_plug { struct device dev; @@ -39,6 +40,7 @@ struct typec_partner { u8 usb_capability; =20 struct usb_power_delivery *pd; + struct mode_selection *sel; =20 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_se= lection.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 +#include +#include +#include +#include +#include + +#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 Mo= des + * @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 =3D to_typec_altmode(dev); + struct mode_order *order =3D (struct mode_order *)data; + + if (order->svid =3D=3D alt->svid) { + if (alt->ops && alt->ops->activate) + order->result =3D alt->ops->activate(alt, order->enter); + else + order->result =3D -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 =3D {.svid =3D svid, .enter =3D enter, .result = =3D -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 ex= it 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 l= ist. + * - `ms->error` value of 0 signifies that the mode has not yet been activ= ated. + * + * Once successfully activated, the task is scheduled for subsequent entry= after + * a timeout period. The alternate mode driver is expected to call back wi= th the + * actual mode entry result via `typec_altmode_state_update()`. + */ +static void mode_selection_work_fn(struct work_struct *work) +{ + struct mode_selection *sel =3D container_of(work, + struct mode_selection, work.work); + struct mode_state *ms; + unsigned int delay =3D sel->delay; + int result; + + guard(mutex)(&sel->lock); + + ms =3D list_first_entry_or_null(&sel->mode_list, struct mode_state, list); + if (!ms) + return; + + if (sel->active_svid =3D=3D ms->svid) { + dev_dbg(&sel->partner->dev, "%x altmode is active\n", ms->svid); + mode_list_clean(sel); + } else if (sel->active_svid !=3D 0) { + result =3D mode_selection_activate(sel, sel->active_svid, 0); + if (result) + mode_list_clean(sel); + else + sel->active_svid =3D 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 =3D 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 =3D sel->timeout; + ms->error =3D -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 s= vid, + const int error) +{ + struct mode_selection *sel =3D partner->sel; + struct mode_state *ms; + + if (sel) { + mutex_lock(&sel->lock); + ms =3D list_first_entry_or_null(&sel->mode_list, struct mode_state, list= ); + if (ms && ms->svid =3D=3D svid) { + ms->error =3D error; + if (cancel_delayed_work(&sel->work)) + schedule_delayed_work(&sel->work, 0); + } + if (!error) + sel->active_svid =3D svid; + else + sel->active_svid =3D 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 =3D container_of(a, struct mode_state, list); + const struct mode_state *msb =3D 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 =3D (struct list_head *)data; + struct typec_altmode *altmode =3D to_typec_altmode(dev); + const struct typec_altmode *pdev =3D typec_altmode_get_partner(altmode); + struct mode_state *ms; + + if (pdev && altmode->ops && altmode->ops->activate) { + ms =3D kzalloc(sizeof(*ms), GFP_KERNEL); + if (!ms) + return -ENOMEM; + ms->svid =3D pdev->svid; + ms->priority =3D 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 =3D=3D USB_MODE_USB4) + return -EBUSY; + + if (partner->sel) + return -EALREADY; + + sel =3D kzalloc(sizeof(*sel), GFP_KERNEL); + if (!sel) + return -ENOMEM; + + INIT_LIST_HEAD(&sel->mode_list); + + ret =3D 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 =3D partner; + sel->delay =3D delay; + sel->timeout =3D 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 =3D sel; + + return 0; +} +EXPORT_SYMBOL_GPL(typec_mode_selection_start); + +void typec_mode_selection_delete(struct typec_partner *partner) +{ + struct mode_selection *sel =3D partner->sel; + + if (sel) { + partner->sel =3D 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_al= tmode.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_altm= ode_driver *drv); module_driver(__typec_altmode_driver, typec_altmode_register_driver, \ typec_altmode_unregister_driver) =20 +/** + * 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 s= vid, + const int result); + +/** + * typec_mode_selection_delete - Delete an alternate mode selection instan= ce + * @partner: Handle to the Type-C partner device. + * + * This function cancels a pending alternate mode selection request that w= as + * 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 */ --=20 2.52.0.457.g6b5491de43-goog