|
| 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); |
0 commit comments