Skip to content

Commit fb2abc7

Browse files
Andrei Kuchynskigregkh
authored andcommitted
usb: typec: Implement mode selection
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> Link: https://patch.msgid.link/20260119131824.2529334-5-akuchynski@chromium.org Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
1 parent 027b304 commit fb2abc7

4 files changed

Lines changed: 326 additions & 1 deletion

File tree

drivers/usb/typec/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# SPDX-License-Identifier: GPL-2.0
22
obj-$(CONFIG_TYPEC) += typec.o
3-
typec-y := class.o mux.o bus.o pd.o retimer.o
3+
typec-y := class.o mux.o bus.o pd.o retimer.o mode_selection.o
44
typec-$(CONFIG_ACPI) += port-mapper.o
55
obj-$(CONFIG_TYPEC) += altmodes/
66
obj-$(CONFIG_TYPEC_TCPM) += tcpm/

drivers/usb/typec/class.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
struct typec_mux;
1010
struct typec_switch;
1111
struct usb_device;
12+
struct mode_selection;
1213

1314
struct typec_plug {
1415
struct device dev;
@@ -39,6 +40,7 @@ struct typec_partner {
3940
u8 usb_capability;
4041

4142
struct usb_power_delivery *pd;
43+
struct mode_selection *sel;
4244

4345
void (*attach)(struct typec_partner *partner, struct device *dev);
4446
void (*deattach)(struct typec_partner *partner, struct device *dev);

drivers/usb/typec/mode_selection.c

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
// SPDX-License-Identifier: GPL-2.0-only
2+
/*
3+
* Copyright 2025 Google LLC.
4+
*/
5+
6+
#include <linux/types.h>
7+
#include <linux/list_sort.h>
8+
#include <linux/slab.h>
9+
#include <linux/mutex.h>
10+
#include <linux/workqueue.h>
11+
#include <linux/usb/typec_altmode.h>
12+
13+
#include "class.h"
14+
15+
/**
16+
* struct mode_state - State tracking for a specific Type-C alternate mode
17+
* @svid: Standard or Vendor ID of the Alternate Mode
18+
* @priority: Mode priority
19+
* @error: Outcome of the last attempt to enter the mode
20+
* @list: List head to link this mode state into a prioritized list
21+
*/
22+
struct mode_state {
23+
u16 svid;
24+
u8 priority;
25+
int error;
26+
struct list_head list;
27+
};
28+
29+
/**
30+
* struct mode_selection - Manages the selection and state of Alternate Modes
31+
* @mode_list: Prioritized list of available Alternate Modes
32+
* @lock: Mutex to protect mode_list
33+
* @work: Work structure
34+
* @partner: Handle to the Type-C partner device
35+
* @active_svid: svid of currently active mode
36+
* @timeout: Timeout for a mode entry attempt, ms
37+
* @delay: Delay between mode entry/exit attempts, ms
38+
*/
39+
struct mode_selection {
40+
struct list_head mode_list;
41+
/* Protects the mode_list*/
42+
struct mutex lock;
43+
struct delayed_work work;
44+
struct typec_partner *partner;
45+
u16 active_svid;
46+
unsigned int timeout;
47+
unsigned int delay;
48+
};
49+
50+
/**
51+
* struct mode_order - Mode activation tracking
52+
* @svid: Standard or Vendor ID of the Alternate Mode
53+
* @enter: Flag indicating if the driver is currently attempting to enter or
54+
* exit the mode
55+
* @result: Outcome of the attempt to activate the mode
56+
*/
57+
struct mode_order {
58+
u16 svid;
59+
int enter;
60+
int result;
61+
};
62+
63+
static int activate_altmode(struct device *dev, void *data)
64+
{
65+
if (is_typec_partner_altmode(dev)) {
66+
struct typec_altmode *alt = to_typec_altmode(dev);
67+
struct mode_order *order = (struct mode_order *)data;
68+
69+
if (order->svid == alt->svid) {
70+
if (alt->ops && alt->ops->activate)
71+
order->result = alt->ops->activate(alt, order->enter);
72+
else
73+
order->result = -EOPNOTSUPP;
74+
return 1;
75+
}
76+
}
77+
return 0;
78+
}
79+
80+
static int mode_selection_activate(struct mode_selection *sel,
81+
const u16 svid, const int enter)
82+
83+
__must_hold(&sel->lock)
84+
{
85+
struct mode_order order = {.svid = svid, .enter = enter, .result = -ENODEV};
86+
87+
/*
88+
* The port driver may acquire its internal mutex during alternate mode
89+
* activation. Since this is the same mutex that may be held during the
90+
* execution of typec_altmode_state_update(), it is crucial to release
91+
* sel->mutex before activation to avoid potential deadlock.
92+
* Note that sel->mode_list must remain invariant throughout this unlocked
93+
* interval.
94+
*/
95+
mutex_unlock(&sel->lock);
96+
device_for_each_child(&sel->partner->dev, &order, activate_altmode);
97+
mutex_lock(&sel->lock);
98+
99+
return order.result;
100+
}
101+
102+
static void mode_list_clean(struct mode_selection *sel)
103+
{
104+
struct mode_state *ms, *tmp;
105+
106+
list_for_each_entry_safe(ms, tmp, &sel->mode_list, list) {
107+
list_del(&ms->list);
108+
kfree(ms);
109+
}
110+
}
111+
112+
/**
113+
* mode_selection_work_fn() - Alternate mode activation task
114+
* @work: work structure
115+
*
116+
* - If the Alternate Mode currently prioritized at the top of the list is already
117+
* active, the entire selection process is considered finished.
118+
* - If a different Alternate Mode is currently active, the system must exit that
119+
* active mode first before attempting any new entry.
120+
*
121+
* The function then checks the result of the attempt to entre the current mode,
122+
* stored in the `ms->error` field:
123+
* - if the attempt FAILED, the mode is deactivated and removed from the list.
124+
* - `ms->error` value of 0 signifies that the mode has not yet been activated.
125+
*
126+
* Once successfully activated, the task is scheduled for subsequent entry after
127+
* a timeout period. The alternate mode driver is expected to call back with the
128+
* actual mode entry result via `typec_altmode_state_update()`.
129+
*/
130+
static void mode_selection_work_fn(struct work_struct *work)
131+
{
132+
struct mode_selection *sel = container_of(work,
133+
struct mode_selection, work.work);
134+
struct mode_state *ms;
135+
unsigned int delay = sel->delay;
136+
int result;
137+
138+
guard(mutex)(&sel->lock);
139+
140+
ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
141+
if (!ms)
142+
return;
143+
144+
if (sel->active_svid == ms->svid) {
145+
dev_dbg(&sel->partner->dev, "%x altmode is active\n", ms->svid);
146+
mode_list_clean(sel);
147+
} else if (sel->active_svid != 0) {
148+
result = mode_selection_activate(sel, sel->active_svid, 0);
149+
if (result)
150+
mode_list_clean(sel);
151+
else
152+
sel->active_svid = 0;
153+
} else if (ms->error) {
154+
dev_err(&sel->partner->dev, "%x: entry error %pe\n",
155+
ms->svid, ERR_PTR(ms->error));
156+
mode_selection_activate(sel, ms->svid, 0);
157+
list_del(&ms->list);
158+
kfree(ms);
159+
} else {
160+
result = mode_selection_activate(sel, ms->svid, 1);
161+
if (result) {
162+
dev_err(&sel->partner->dev, "%x: activation error %pe\n",
163+
ms->svid, ERR_PTR(result));
164+
list_del(&ms->list);
165+
kfree(ms);
166+
} else {
167+
delay = sel->timeout;
168+
ms->error = -ETIMEDOUT;
169+
}
170+
}
171+
172+
if (!list_empty(&sel->mode_list))
173+
schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
174+
}
175+
176+
void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
177+
const int error)
178+
{
179+
struct mode_selection *sel = partner->sel;
180+
struct mode_state *ms;
181+
182+
if (sel) {
183+
mutex_lock(&sel->lock);
184+
ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
185+
if (ms && ms->svid == svid) {
186+
ms->error = error;
187+
if (cancel_delayed_work(&sel->work))
188+
schedule_delayed_work(&sel->work, 0);
189+
}
190+
if (!error)
191+
sel->active_svid = svid;
192+
else
193+
sel->active_svid = 0;
194+
mutex_unlock(&sel->lock);
195+
}
196+
}
197+
EXPORT_SYMBOL_GPL(typec_altmode_state_update);
198+
199+
static int compare_priorities(void *priv,
200+
const struct list_head *a, const struct list_head *b)
201+
{
202+
const struct mode_state *msa = container_of(a, struct mode_state, list);
203+
const struct mode_state *msb = container_of(b, struct mode_state, list);
204+
205+
if (msa->priority < msb->priority)
206+
return -1;
207+
return 1;
208+
}
209+
210+
static int altmode_add_to_list(struct device *dev, void *data)
211+
{
212+
if (is_typec_partner_altmode(dev)) {
213+
struct list_head *list = (struct list_head *)data;
214+
struct typec_altmode *altmode = to_typec_altmode(dev);
215+
const struct typec_altmode *pdev = typec_altmode_get_partner(altmode);
216+
struct mode_state *ms;
217+
218+
if (pdev && altmode->ops && altmode->ops->activate) {
219+
ms = kzalloc(sizeof(*ms), GFP_KERNEL);
220+
if (!ms)
221+
return -ENOMEM;
222+
ms->svid = pdev->svid;
223+
ms->priority = pdev->priority;
224+
INIT_LIST_HEAD(&ms->list);
225+
list_add_tail(&ms->list, list);
226+
}
227+
}
228+
return 0;
229+
}
230+
231+
int typec_mode_selection_start(struct typec_partner *partner,
232+
const unsigned int delay, const unsigned int timeout)
233+
{
234+
struct mode_selection *sel;
235+
int ret;
236+
237+
if (partner->usb_mode == USB_MODE_USB4)
238+
return -EBUSY;
239+
240+
if (partner->sel)
241+
return -EALREADY;
242+
243+
sel = kzalloc(sizeof(*sel), GFP_KERNEL);
244+
if (!sel)
245+
return -ENOMEM;
246+
247+
INIT_LIST_HEAD(&sel->mode_list);
248+
249+
ret = device_for_each_child(&partner->dev, &sel->mode_list,
250+
altmode_add_to_list);
251+
252+
if (ret || list_empty(&sel->mode_list)) {
253+
mode_list_clean(sel);
254+
kfree(sel);
255+
return ret;
256+
}
257+
258+
list_sort(NULL, &sel->mode_list, compare_priorities);
259+
sel->partner = partner;
260+
sel->delay = delay;
261+
sel->timeout = timeout;
262+
mutex_init(&sel->lock);
263+
INIT_DELAYED_WORK(&sel->work, mode_selection_work_fn);
264+
schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
265+
partner->sel = sel;
266+
267+
return 0;
268+
}
269+
EXPORT_SYMBOL_GPL(typec_mode_selection_start);
270+
271+
void typec_mode_selection_delete(struct typec_partner *partner)
272+
{
273+
struct mode_selection *sel = partner->sel;
274+
275+
if (sel) {
276+
partner->sel = NULL;
277+
cancel_delayed_work_sync(&sel->work);
278+
mode_list_clean(sel);
279+
mutex_destroy(&sel->lock);
280+
kfree(sel);
281+
}
282+
}
283+
EXPORT_SYMBOL_GPL(typec_mode_selection_delete);

include/linux/usb/typec_altmode.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,44 @@ void typec_altmode_unregister_driver(struct typec_altmode_driver *drv);
240240
module_driver(__typec_altmode_driver, typec_altmode_register_driver, \
241241
typec_altmode_unregister_driver)
242242

243+
/**
244+
* typec_mode_selection_start - Start an alternate mode selection process
245+
* @partner: Handle to the Type-C partner device
246+
* @delay: Delay between mode entry/exit attempts, ms
247+
* @timeout: Timeout for a mode entry attempt, ms
248+
*
249+
* This function initiates the process of attempting to enter an Alternate Mode
250+
* supported by the connected Type-C partner.
251+
* Returns 0 on success, or a negative error code on failure.
252+
*/
253+
int typec_mode_selection_start(struct typec_partner *partner,
254+
const unsigned int delay, const unsigned int timeout);
255+
256+
/**
257+
* typec_altmode_state_update - Report the current status of an Alternate Mode
258+
* negotiation
259+
* @partner: Handle to the Type-C partner device
260+
* @svid: Standard or Vendor ID of the Alternate Mode. A value of 0 should be
261+
* passed if no mode is currently active
262+
* @result: Result of the entry operation. This should be 0 on success, or a
263+
* negative error code if the negotiation failed
264+
*
265+
* This function should be called by an Alternate Mode driver to report the
266+
* result of an asynchronous alternate mode entry request. It signals what the
267+
* current active SVID is (or 0 if none) and the success or failure status of
268+
* the last attempt.
269+
*/
270+
void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
271+
const int result);
272+
273+
/**
274+
* typec_mode_selection_delete - Delete an alternate mode selection instance
275+
* @partner: Handle to the Type-C partner device.
276+
*
277+
* This function cancels a pending alternate mode selection request that was
278+
* previously started with typec_mode_selection_start().
279+
* This is typically called when the partner disconnects.
280+
*/
281+
void typec_mode_selection_delete(struct typec_partner *partner);
282+
243283
#endif /* __USB_TYPEC_ALTMODE_H */

0 commit comments

Comments
 (0)