File Index
Files and directories
Sign in
FILE

thunderbolt.c

File preview with syntax highlighting.

linux-6.19.14/drivers/usb/typec/altmodes/thunderbolt.c C
  1// SPDX-License-Identifier: GPL-2.0
  2/*
  3 * USB Typec-C Thunderbolt3 Alternate Mode driver
  4 *
  5 * Copyright (C) 2019 Intel Corporation
  6 * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
  7 */
  8
  9#include <linux/lockdep.h>
 10#include <linux/module.h>
 11#include <linux/mutex.h>
 12#include <linux/workqueue.h>
 13#include <linux/usb/pd_vdo.h>
 14#include <linux/usb/typec_altmode.h>
 15#include <linux/usb/typec_tbt.h>
 16
 17enum tbt_state {
 18	TBT_STATE_IDLE,
 19	TBT_STATE_SOP_P_ENTER,
 20	TBT_STATE_SOP_PP_ENTER,
 21	TBT_STATE_ENTER,
 22	TBT_STATE_EXIT,
 23	TBT_STATE_SOP_PP_EXIT,
 24	TBT_STATE_SOP_P_EXIT
 25};
 26
 27struct tbt_altmode {
 28	enum tbt_state state;
 29	struct typec_cable *cable;
 30	struct typec_altmode *alt;
 31	struct typec_altmode *plug[2];
 32	u32 enter_vdo;
 33
 34	struct work_struct work;
 35	struct mutex lock; /* device lock */
 36};
 37
 38static bool tbt_ready(struct typec_altmode *alt);
 39
 40static int tbt_enter_mode(struct tbt_altmode *tbt)
 41{
 42	return typec_altmode_enter(tbt->alt, &tbt->enter_vdo);
 43}
 44
 45static void tbt_altmode_work(struct work_struct *work)
 46{
 47	struct tbt_altmode *tbt = container_of(work, struct tbt_altmode, work);
 48	int ret;
 49
 50	mutex_lock(&tbt->lock);
 51
 52	switch (tbt->state) {
 53	case TBT_STATE_SOP_P_ENTER:
 54		ret = typec_cable_altmode_enter(tbt->alt, TYPEC_PLUG_SOP_P, NULL);
 55		if (ret) {
 56			dev_dbg(&tbt->plug[TYPEC_PLUG_SOP_P]->dev,
 57				"failed to enter mode (%d)\n", ret);
 58			goto disable_plugs;
 59		}
 60		break;
 61	case TBT_STATE_SOP_PP_ENTER:
 62		ret = typec_cable_altmode_enter(tbt->alt, TYPEC_PLUG_SOP_PP,  NULL);
 63		if (ret) {
 64			dev_dbg(&tbt->plug[TYPEC_PLUG_SOP_PP]->dev,
 65				"failed to enter mode (%d)\n", ret);
 66			goto disable_plugs;
 67		}
 68		break;
 69	case TBT_STATE_ENTER:
 70		ret = tbt_enter_mode(tbt);
 71		if (ret)
 72			dev_dbg(&tbt->alt->dev, "failed to enter mode (%d)\n",
 73				ret);
 74		break;
 75	case TBT_STATE_EXIT:
 76		typec_altmode_exit(tbt->alt);
 77		break;
 78	case TBT_STATE_SOP_PP_EXIT:
 79		typec_cable_altmode_exit(tbt->alt, TYPEC_PLUG_SOP_PP);
 80		break;
 81	case TBT_STATE_SOP_P_EXIT:
 82		typec_cable_altmode_exit(tbt->alt, TYPEC_PLUG_SOP_P);
 83		break;
 84	default:
 85		break;
 86	}
 87
 88	tbt->state = TBT_STATE_IDLE;
 89
 90	mutex_unlock(&tbt->lock);
 91	return;
 92
 93disable_plugs:
 94	for (int i = TYPEC_PLUG_SOP_PP; i >= 0; --i) {
 95		if (tbt->plug[i])
 96			typec_altmode_put_plug(tbt->plug[i]);
 97
 98		tbt->plug[i] = NULL;
 99	}
100
101	tbt->state = TBT_STATE_ENTER;
102	schedule_work(&tbt->work);
103	mutex_unlock(&tbt->lock);
104}
105
106/*
107 * If SOP' is available, enter that first (which will trigger a VDM response
108 * that will enter SOP" if available and then the port). If entering SOP' fails,
109 * stop attempting to enter either cable altmode (probably not supported) and
110 * directly enter the port altmode.
111 */
112static int tbt_enter_modes_ordered(struct typec_altmode *alt)
113{
114	struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt);
115	int ret = 0;
116
117	lockdep_assert_held(&tbt->lock);
118
119	if (!tbt_ready(tbt->alt))
120		return -ENODEV;
121
122	if (tbt->plug[TYPEC_PLUG_SOP_P]) {
123		ret = typec_cable_altmode_enter(alt, TYPEC_PLUG_SOP_P, NULL);
124		if (ret < 0) {
125			for (int i = TYPEC_PLUG_SOP_PP; i >= 0; --i) {
126				if (tbt->plug[i])
127					typec_altmode_put_plug(tbt->plug[i]);
128
129				tbt->plug[i] = NULL;
130			}
131		} else {
132			return ret;
133		}
134	}
135
136	return tbt_enter_mode(tbt);
137}
138
139static int tbt_cable_altmode_vdm(struct typec_altmode *alt,
140				 enum typec_plug_index sop, const u32 hdr,
141				 const u32 *vdo, int count)
142{
143	struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt);
144	int cmd_type = PD_VDO_CMDT(hdr);
145	int cmd = PD_VDO_CMD(hdr);
146
147	mutex_lock(&tbt->lock);
148
149	if (tbt->state != TBT_STATE_IDLE) {
150		mutex_unlock(&tbt->lock);
151		return -EBUSY;
152	}
153
154	switch (cmd_type) {
155	case CMDT_RSP_ACK:
156		switch (cmd) {
157		case CMD_ENTER_MODE:
158			/*
159			 * Following the order described in USB Type-C Spec
160			 * R2.0 Section 6.7.3: SOP', SOP", then port.
161			 */
162			if (sop == TYPEC_PLUG_SOP_P) {
163				if (tbt->plug[TYPEC_PLUG_SOP_PP])
164					tbt->state = TBT_STATE_SOP_PP_ENTER;
165				else
166					tbt->state = TBT_STATE_ENTER;
167			} else if (sop == TYPEC_PLUG_SOP_PP)
168				tbt->state = TBT_STATE_ENTER;
169
170			break;
171		case CMD_EXIT_MODE:
172			/* Exit in opposite order: Port, SOP", then SOP'. */
173			if (sop == TYPEC_PLUG_SOP_PP)
174				tbt->state = TBT_STATE_SOP_P_EXIT;
175			break;
176		}
177		break;
178	default:
179		break;
180	}
181
182	if (tbt->state != TBT_STATE_IDLE)
183		schedule_work(&tbt->work);
184
185	mutex_unlock(&tbt->lock);
186	return 0;
187}
188
189static int tbt_altmode_vdm(struct typec_altmode *alt,
190			   const u32 hdr, const u32 *vdo, int count)
191{
192	struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt);
193	struct typec_thunderbolt_data data;
194	int cmd_type = PD_VDO_CMDT(hdr);
195	int cmd = PD_VDO_CMD(hdr);
196
197	mutex_lock(&tbt->lock);
198
199	if (tbt->state != TBT_STATE_IDLE) {
200		mutex_unlock(&tbt->lock);
201		return -EBUSY;
202	}
203
204	switch (cmd_type) {
205	case CMDT_RSP_ACK:
206		/* Port altmode is last to enter and first to exit. */
207		switch (cmd) {
208		case CMD_ENTER_MODE:
209			memset(&data, 0, sizeof(data));
210
211			data.device_mode = tbt->alt->vdo;
212			data.enter_vdo = tbt->enter_vdo;
213			if (tbt->plug[TYPEC_PLUG_SOP_P])
214				data.cable_mode = tbt->plug[TYPEC_PLUG_SOP_P]->vdo;
215
216			typec_altmode_notify(alt, TYPEC_STATE_MODAL, &data);
217			break;
218		case CMD_EXIT_MODE:
219			if (tbt->plug[TYPEC_PLUG_SOP_PP])
220				tbt->state = TBT_STATE_SOP_PP_EXIT;
221			else if (tbt->plug[TYPEC_PLUG_SOP_P])
222				tbt->state = TBT_STATE_SOP_P_EXIT;
223			break;
224		}
225		break;
226	case CMDT_RSP_NAK:
227		switch (cmd) {
228		case CMD_ENTER_MODE:
229			dev_warn(&alt->dev, "Enter Mode refused\n");
230			break;
231		default:
232			break;
233		}
234		break;
235	default:
236		break;
237	}
238
239	if (tbt->state != TBT_STATE_IDLE)
240		schedule_work(&tbt->work);
241
242	mutex_unlock(&tbt->lock);
243
244	return 0;
245}
246
247static int tbt_altmode_activate(struct typec_altmode *alt, int activate)
248{
249	struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt);
250	int ret;
251
252	mutex_lock(&tbt->lock);
253
254	if (activate)
255		ret = tbt_enter_modes_ordered(alt);
256	else
257		ret = typec_altmode_exit(alt);
258
259	mutex_unlock(&tbt->lock);
260
261	return ret;
262}
263
264static const struct typec_altmode_ops tbt_altmode_ops = {
265	.vdm		= tbt_altmode_vdm,
266	.activate	= tbt_altmode_activate
267};
268
269static const struct typec_cable_ops tbt_cable_ops = {
270	.vdm		= tbt_cable_altmode_vdm,
271};
272
273static int tbt_altmode_probe(struct typec_altmode *alt)
274{
275	struct tbt_altmode *tbt;
276
277	tbt = devm_kzalloc(&alt->dev, sizeof(*tbt), GFP_KERNEL);
278	if (!tbt)
279		return -ENOMEM;
280
281	INIT_WORK(&tbt->work, tbt_altmode_work);
282	mutex_init(&tbt->lock);
283	tbt->alt = alt;
284
285	alt->desc = "Thunderbolt3";
286	typec_altmode_set_drvdata(alt, tbt);
287	typec_altmode_set_ops(alt, &tbt_altmode_ops);
288
289	if (tbt_ready(alt)) {
290		if (tbt->plug[TYPEC_PLUG_SOP_P])
291			tbt->state = TBT_STATE_SOP_P_ENTER;
292		else if (tbt->plug[TYPEC_PLUG_SOP_PP])
293			tbt->state = TBT_STATE_SOP_PP_ENTER;
294		else
295			tbt->state = TBT_STATE_ENTER;
296		schedule_work(&tbt->work);
297	}
298
299	return 0;
300}
301
302static void tbt_altmode_remove(struct typec_altmode *alt)
303{
304	struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt);
305
306	for (int i = TYPEC_PLUG_SOP_PP; i >= 0; --i) {
307		if (tbt->plug[i])
308			typec_altmode_put_plug(tbt->plug[i]);
309	}
310
311	if (tbt->cable)
312		typec_cable_put(tbt->cable);
313}
314
315static bool tbt_ready(struct typec_altmode *alt)
316{
317	struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt);
318	struct typec_altmode *plug;
319	u32 vdo;
320
321	if (tbt->cable)
322		return true;
323
324	/* Thunderbolt 3 requires a cable with eMarker */
325	tbt->cable = typec_cable_get(typec_altmode2port(tbt->alt));
326	if (!tbt->cable)
327		return false;
328
329	/* We accept systems without SOP' or SOP''. This means the port altmode
330	 * driver will be responsible for properly ordering entry/exit.
331	 */
332	for (int i = 0; i < TYPEC_PLUG_SOP_PP + 1; i++) {
333		plug = typec_altmode_get_plug(tbt->alt, i);
334		if (!plug)
335			continue;
336
337		if (plug->svid != USB_TYPEC_TBT_SID)
338			break;
339
340		plug->desc = "Thunderbolt3";
341		plug->cable_ops = &tbt_cable_ops;
342		typec_altmode_set_drvdata(plug, tbt);
343
344		tbt->plug[i] = plug;
345	}
346
347	vdo = tbt->alt->vdo & (TBT_VENDOR_SPECIFIC_B0 | TBT_VENDOR_SPECIFIC_B1);
348	vdo |= tbt->alt->vdo & TBT_INTEL_SPECIFIC_B0;
349	vdo |= TBT_MODE;
350	plug = tbt->plug[TYPEC_PLUG_SOP_P];
351
352	if (plug) {
353		if (typec_cable_is_active(tbt->cable))
354			vdo |= TBT_ENTER_MODE_ACTIVE_CABLE;
355
356		vdo |= TBT_ENTER_MODE_CABLE_SPEED(TBT_CABLE_SPEED(plug->vdo));
357		vdo |= plug->vdo & TBT_CABLE_ROUNDED;
358		vdo |= plug->vdo & TBT_CABLE_OPTICAL;
359		vdo |= plug->vdo & TBT_CABLE_RETIMER;
360		vdo |= plug->vdo & TBT_CABLE_LINK_TRAINING;
361	} else {
362		vdo |= TBT_ENTER_MODE_CABLE_SPEED(TBT_CABLE_USB3_PASSIVE);
363	}
364
365	tbt->enter_vdo = vdo;
366
367	return true;
368}
369
370static const struct typec_device_id tbt_typec_id[] = {
371	{ USB_TYPEC_TBT_SID },
372	{ }
373};
374MODULE_DEVICE_TABLE(typec, tbt_typec_id);
375
376static struct typec_altmode_driver tbt_altmode_driver = {
377	.id_table = tbt_typec_id,
378	.probe = tbt_altmode_probe,
379	.remove = tbt_altmode_remove,
380	.driver = {
381		.name = "typec-thunderbolt",
382	}
383};
384module_typec_altmode_driver(tbt_altmode_driver);
385
386MODULE_AUTHOR("Heikki Krogerus <heikki.krogerus@linux.intel.com>");
387MODULE_LICENSE("GPL");
388MODULE_DESCRIPTION("Thunderbolt3 USB Type-C Alternate Mode");
~/ Read-only file preview.