From nobody Mon Dec 1 22:03:23 2025 Received: from mail-ej1-f41.google.com (mail-ej1-f41.google.com [209.85.218.41]) (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 4C63330BF52 for ; Mon, 1 Dec 2025 12:26:16 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.218.41 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1764591978; cv=none; b=Ooii/7D2KxhfGQp8tt721ZXePWEJawedD5n6wdP9plj/Ur+cPBbOXCqN8y/tCvdGQDuzK9eaZFHbdEOGC7tOL2vPbxAMI119aBmnvZRwZjADXjcn1A4Aj72LVxS6FyOUnlDT6H6eqrfu44ezj+agP9vjimUGhqbWXFeZRxKuHZo= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1764591978; c=relaxed/simple; bh=mFFOFxlXkYzqkxL/jBvCvI+dfjNbxB2plNZeiEbJmZQ=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=IQjyOMKCpRxRJOu88/7FuquTHEXuz3yAK0mf2dh8d9kP6kToVKXmS9aUv0MepR3FlIuVYfahfQ80R1bXMspxLMngMJEsGzOvvjcJb8Gd+gCqV3LO96UAut+XUnhf47A2Ptc8R6GSU3U2FPaFvM4IG4LdaZTBYB/WRn+prdSuw1Y= 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=ECoPe78S; arc=none smtp.client-ip=209.85.218.41 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="ECoPe78S" Received: by mail-ej1-f41.google.com with SMTP id a640c23a62f3a-b735e278fa1so72635766b.0 for ; Mon, 01 Dec 2025 04:26:16 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=chromium.org; s=google; t=1764591975; x=1765196775; 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=rOzvEryKCkrNpvspLpfMwILhAFa2s2m3Cqxq22OFJJc=; b=ECoPe78SE2XeK7va/XeUzk5On68OcCSSHhWxD3MkXu7JlYnY/77yiRNkqPu1D/iOxu u2Sf71Joz3uwEI33gmbTOBby4C5alH2fQlLJmaDlh1Jf0wf/7sew5ixiR6HhMZgmIRNE enHNNR+WwYVtd0UCDyILkr7Tsy3aSILH9VxIQ= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1764591975; x=1765196775; 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=rOzvEryKCkrNpvspLpfMwILhAFa2s2m3Cqxq22OFJJc=; b=dx/DOmPp0+moYAlcCJiNgTxFPS+bAV9XtWPzE7JQrYhMD4/fPeARWZkDWU0afWkCdq 4yCL3kq7nTScAO5UYsqnsvICVqTpIQxBnUWDYCyj3skGe8umQTEz+iTS7twQ3ULSPHRL Hy3b+kmUJX2Yvc4fyXPDBodftwGu3C+lw2NrVvLkRQJ6XCMi1t8GSUC1fpIKIKpAhFSn F05n959PZQorq3V4w3j4atFmm9EK/vV5U0/HqqeJGtSiY04XZsZZ7Kis+kJNTNg+yy7r +nIH1SS90ZpMjdTpgBpZvg63obYDD+Gy7I73WndfzJykPJoPjbbq4nFmFvquUpmH/omY ESeQ== X-Forwarded-Encrypted: i=1; AJvYcCXA8E9aJtd1wbF9QNwkaTnJQ/v7BXTD3TAvBNGut+lGXex/7DQfF9xEqcOwlS+Gee6ltR1rxD5CcOOYKqA=@vger.kernel.org X-Gm-Message-State: AOJu0YxEFsFUG9HYy06xbI49Nm60BoFpYaCoruvRnk2UPdrLqaUgvEKe LIHV1YfGoaGwqk1eK6rVUI9V/JdGN8xyW9sD+nZ58UmGMmkYx/W2q2BE9lE2GhfdUA== X-Gm-Gg: ASbGncv3N488mFpsyPnaKRd7fFutbZ4y6GtVxhuRc8JmzFmvZL9PitRuUuRe0vtNQF9 A7uQOOc38GazGkEAPhLaSW7g3yKJzHwzxheD7+a7qWKIlMujKn/4r/z8ZESykneo7IckXzrtIKt XiNOqZ5KIHjR/v+eNuUl+aXE7DSKxjHf/E9QuhrNbH4Gw5IgYdQFyhafTpBTFLPIMpAHmx6Zpxe r5QYVjfDn8YhOd6MYER0dMzExnBT5P8FG/dHUhfTJVI8smw2+V9CnMmlquucl2rjFkyPm6kdRmd SSnv1xcl6kG86ueKwa14A2dkjGEVQIIkQOxDLudk3rzNSp+FC/F5h8tAzmMSjMCvmx24+1ENfe6 zlY4yDNX+x971k6fE569Yxipy9c25I7i0qrlZV9ncwsBEaAR0oEED9HFHJynFtkCNL85Ry7N1Zg izaiPlnN4HDvN/Wc0OUz5KCTeXUy4lFeoIoNmpo/2Gqdj+nk2nCbj9+3fItmXdewxzK59cRjkqq 6Peaj/4PlM= X-Google-Smtp-Source: AGHT+IE8DaR7Kik8466aWNYAdspu3ISPD7yBbPMq0lguD6XcCygqXCAx20V+4wMbwRnBnuB9+Z3lcA== X-Received: by 2002:a17:907:cbc6:b0:b76:d882:5aaa with SMTP id a640c23a62f3a-b76d8825d0bmr2269904566b.3.1764591974487; Mon, 01 Dec 2025 04:26:14 -0800 (PST) Received: from akuchynski.c.googlers.com.com (218.127.147.34.bc.googleusercontent.com. [34.147.127.218]) by smtp.gmail.com with ESMTPSA id a640c23a62f3a-b76f519e331sm1229049266b.24.2025.12.01.04.26.13 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Mon, 01 Dec 2025 04:26:14 -0800 (PST) From: Andrei Kuchynski To: Heikki Krogerus , Abhishek Pandit-Subedi , Benson Leung , Jameson Thies , Tzung-Bi Shih , linux-usb@vger.kernel.org, chrome-platform@lists.linux.dev Cc: Guenter Roeck , Greg Kroah-Hartman , Dmitry Baryshkov , "Christian A. Ehrhardt" , Abel Vesa , Pooja Katiyar , Pavan Holla , Madhu M , Venkat Jayaraman , linux-kernel@vger.kernel.org, Andrei Kuchynski Subject: [PATCH RFC 1/8] usb: typec: Implement mode selection Date: Mon, 1 Dec 2025 12:25:57 +0000 Message-ID: <20251201122604.1268071-2-akuchynski@chromium.org> X-Mailer: git-send-email 2.52.0.158.g65b55ccf14-goog In-Reply-To: <20251201122604.1268071-1-akuchynski@chromium.org> References: <20251201122604.1268071-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 --- drivers/usb/typec/Makefile | 2 +- drivers/usb/typec/class.h | 2 + drivers/usb/typec/mode_selection.c | 285 +++++++++++++++++++++++++++++ include/linux/usb/typec_altmode.h | 40 ++++ 4 files changed, 328 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 7a368fea61bc..8a6a1c663eb6 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 2e89a83c2eb7..d3435936ee7c 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 000000000000..1cf8a4dcd742 --- /dev/null +++ b/drivers/usb/typec/mode_selection.c @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copyright 2025 Google LLC. + */ + +#include +#include +#include +#include +#include + +#include "class.h" +#include "bus.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; + struct mutex lock; + struct delayed_work work; + struct typec_partner *partner; + u16 active_svid; + unsigned int timeout; + unsigned int delay; +}; + +struct mode_order { + u16 svid; + int enter; + int result; +}; + +static int activate_altmode(struct device *dev, void *data) +{ + if (is_typec_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; + + mutex_lock(&sel->lock); + + ms =3D list_first_entry_or_null(&sel->mode_list, struct mode_state, list); + if (!ms) { + mutex_unlock(&sel->lock); + 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) { + dev_dbg(&sel->partner->dev, "enable to exit %x altmode\n", + sel->active_svid); + mode_list_clean(sel); + } + } else if (ms->error) { + dev_dbg(&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_dbg(&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)); + mutex_unlock(&sel->lock); +} + +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; + 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 struct mode_state *create_mode_entry(u16 svid, u8 priority) +{ + struct mode_state *ms =3D kzalloc(sizeof(struct mode_state), GFP_KERNEL); + + if (ms) { + ms->svid =3D svid; + ms->priority =3D priority; + INIT_LIST_HEAD(&ms->list); + } + + return ms; +} + +static int altmode_add_to_list(struct device *dev, void *data) +{ + if (is_typec_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 create_mode_entry(pdev->svid, pdev->priority); + if (!ms) + return -ENOMEM; + 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->sel) + return -EALREADY; + + sel =3D kzalloc(sizeof(struct mode_selection), 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); + partner->sel =3D sel; + INIT_DELAYED_WORK(&sel->work, mode_selection_work_fn); + schedule_delayed_work(&sel->work, msecs_to_jiffies(delay)); + + 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 2c3b6bec2eca..a240d8264b92 100644 --- a/include/linux/usb/typec_altmode.h +++ b/include/linux/usb/typec_altmode.h @@ -232,4 +232,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.158.g65b55ccf14-goog