// SPDX-License-Identifier: GPL-2.0 /* * USB Typec-C Thunderbolt3 Alternate Mode driver * * Copyright (C) 2019 Intel Corporation * Author: Heikki Krogerus */ #include #include #include #include #include #include #include enum tbt_state { TBT_STATE_IDLE, TBT_STATE_SOP_P_ENTER, TBT_STATE_SOP_PP_ENTER, TBT_STATE_ENTER, TBT_STATE_EXIT, TBT_STATE_SOP_PP_EXIT, TBT_STATE_SOP_P_EXIT }; struct tbt_altmode { enum tbt_state state; struct typec_cable *cable; struct typec_altmode *alt; struct typec_altmode *plug[2]; u32 enter_vdo; struct work_struct work; struct mutex lock; /* device lock */ }; static bool tbt_ready(struct typec_altmode *alt); static int tbt_enter_mode(struct tbt_altmode *tbt) { struct typec_altmode *plug = tbt->plug[TYPEC_PLUG_SOP_P]; u32 vdo; vdo = tbt->alt->vdo & (TBT_VENDOR_SPECIFIC_B0 | TBT_VENDOR_SPECIFIC_B1); vdo |= tbt->alt->vdo & TBT_INTEL_SPECIFIC_B0; vdo |= TBT_MODE; if (plug) { if (typec_cable_is_active(tbt->cable)) vdo |= TBT_ENTER_MODE_ACTIVE_CABLE; vdo |= TBT_ENTER_MODE_CABLE_SPEED(TBT_CABLE_SPEED(plug->vdo)); vdo |= plug->vdo & TBT_CABLE_ROUNDED; vdo |= plug->vdo & TBT_CABLE_OPTICAL; vdo |= plug->vdo & TBT_CABLE_RETIMER; vdo |= plug->vdo & TBT_CABLE_LINK_TRAINING; } else { vdo |= TBT_ENTER_MODE_CABLE_SPEED(TBT_CABLE_USB3_PASSIVE); } tbt->enter_vdo = vdo; return typec_altmode_enter(tbt->alt, &vdo); } static void tbt_altmode_work(struct work_struct *work) { struct tbt_altmode *tbt = container_of(work, struct tbt_altmode, work); int ret; mutex_lock(&tbt->lock); switch (tbt->state) { case TBT_STATE_SOP_P_ENTER: ret = typec_cable_altmode_enter(tbt->alt, TYPEC_PLUG_SOP_P, NULL); if (ret) { dev_dbg(&tbt->plug[TYPEC_PLUG_SOP_P]->dev, "failed to enter mode (%d)\n", ret); goto disable_plugs; } break; case TBT_STATE_SOP_PP_ENTER: ret = typec_cable_altmode_enter(tbt->alt, TYPEC_PLUG_SOP_PP, NULL); if (ret) { dev_dbg(&tbt->plug[TYPEC_PLUG_SOP_PP]->dev, "failed to enter mode (%d)\n", ret); goto disable_plugs; } break; case TBT_STATE_ENTER: ret = tbt_enter_mode(tbt); if (ret) dev_dbg(&tbt->alt->dev, "failed to enter mode (%d)\n", ret); break; case TBT_STATE_EXIT: typec_altmode_exit(tbt->alt); break; case TBT_STATE_SOP_PP_EXIT: typec_cable_altmode_exit(tbt->alt, TYPEC_PLUG_SOP_PP); break; case TBT_STATE_SOP_P_EXIT: typec_cable_altmode_exit(tbt->alt, TYPEC_PLUG_SOP_P); break; default: break; } tbt->state = TBT_STATE_IDLE; mutex_unlock(&tbt->lock); return; disable_plugs: for (int i = TYPEC_PLUG_SOP_PP; i >= 0; --i) { if (tbt->plug[i]) typec_altmode_put_plug(tbt->plug[i]); tbt->plug[i] = NULL; } tbt->state = TBT_STATE_ENTER; schedule_work(&tbt->work); mutex_unlock(&tbt->lock); } /* * If SOP' is available, enter that first (which will trigger a VDM response * that will enter SOP" if available and then the port). If entering SOP' fails, * stop attempting to enter either cable altmode (probably not supported) and * directly enter the port altmode. */ static int tbt_enter_modes_ordered(struct typec_altmode *alt) { struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt); int ret = 0; lockdep_assert_held(&tbt->lock); if (!tbt_ready(tbt->alt)) return -ENODEV; if (tbt->plug[TYPEC_PLUG_SOP_P]) { ret = typec_cable_altmode_enter(alt, TYPEC_PLUG_SOP_P, NULL); if (ret < 0) { for (int i = TYPEC_PLUG_SOP_PP; i >= 0; --i) { if (tbt->plug[i]) typec_altmode_put_plug(tbt->plug[i]); tbt->plug[i] = NULL; } } else { return ret; } } return tbt_enter_mode(tbt); } static int tbt_cable_altmode_vdm(struct typec_altmode *alt, enum typec_plug_index sop, const u32 hdr, const u32 *vdo, int count) { struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt); int cmd_type = PD_VDO_CMDT(hdr); int cmd = PD_VDO_CMD(hdr); mutex_lock(&tbt->lock); if (tbt->state != TBT_STATE_IDLE) { mutex_unlock(&tbt->lock); return -EBUSY; } switch (cmd_type) { case CMDT_RSP_ACK: switch (cmd) { case CMD_ENTER_MODE: /* * Following the order described in USB Type-C Spec * R2.0 Section 6.7.3: SOP', SOP", then port. */ if (sop == TYPEC_PLUG_SOP_P) { if (tbt->plug[TYPEC_PLUG_SOP_PP]) tbt->state = TBT_STATE_SOP_PP_ENTER; else tbt->state = TBT_STATE_ENTER; } else if (sop == TYPEC_PLUG_SOP_PP) tbt->state = TBT_STATE_ENTER; break; case CMD_EXIT_MODE: /* Exit in opposite order: Port, SOP", then SOP'. */ if (sop == TYPEC_PLUG_SOP_PP) tbt->state = TBT_STATE_SOP_P_EXIT; break; } break; default: break; } if (tbt->state != TBT_STATE_IDLE) schedule_work(&tbt->work); mutex_unlock(&tbt->lock); return 0; } static int tbt_altmode_vdm(struct typec_altmode *alt, const u32 hdr, const u32 *vdo, int count) { struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt); struct typec_thunderbolt_data data; int cmd_type = PD_VDO_CMDT(hdr); int cmd = PD_VDO_CMD(hdr); mutex_lock(&tbt->lock); if (tbt->state != TBT_STATE_IDLE) { mutex_unlock(&tbt->lock); return -EBUSY; } switch (cmd_type) { case CMDT_RSP_ACK: /* Port altmode is last to enter and first to exit. */ switch (cmd) { case CMD_ENTER_MODE: memset(&data, 0, sizeof(data)); data.device_mode = tbt->alt->vdo; data.enter_vdo = tbt->enter_vdo; if (tbt->plug[TYPEC_PLUG_SOP_P]) data.cable_mode = tbt->plug[TYPEC_PLUG_SOP_P]->vdo; typec_altmode_notify(alt, TYPEC_STATE_MODAL, &data); break; case CMD_EXIT_MODE: if (tbt->plug[TYPEC_PLUG_SOP_PP]) tbt->state = TBT_STATE_SOP_PP_EXIT; else if (tbt->plug[TYPEC_PLUG_SOP_P]) tbt->state = TBT_STATE_SOP_P_EXIT; break; } break; case CMDT_RSP_NAK: switch (cmd) { case CMD_ENTER_MODE: dev_warn(&alt->dev, "Enter Mode refused\n"); break; default: break; } break; default: break; } if (tbt->state != TBT_STATE_IDLE) schedule_work(&tbt->work); mutex_unlock(&tbt->lock); return 0; } static int tbt_altmode_activate(struct typec_altmode *alt, int activate) { struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt); int ret; mutex_lock(&tbt->lock); if (activate) ret = tbt_enter_modes_ordered(alt); else ret = typec_altmode_exit(alt); mutex_unlock(&tbt->lock); return ret; } static const struct typec_altmode_ops tbt_altmode_ops = { .vdm = tbt_altmode_vdm, .activate = tbt_altmode_activate }; static const struct typec_cable_ops tbt_cable_ops = { .vdm = tbt_cable_altmode_vdm, }; static int tbt_altmode_probe(struct typec_altmode *alt) { struct tbt_altmode *tbt; tbt = devm_kzalloc(&alt->dev, sizeof(*tbt), GFP_KERNEL); if (!tbt) return -ENOMEM; INIT_WORK(&tbt->work, tbt_altmode_work); mutex_init(&tbt->lock); tbt->alt = alt; alt->desc = "Thunderbolt3"; typec_altmode_set_drvdata(alt, tbt); typec_altmode_set_ops(alt, &tbt_altmode_ops); if (tbt_ready(alt)) { if (tbt->plug[TYPEC_PLUG_SOP_P]) tbt->state = TBT_STATE_SOP_P_ENTER; else if (tbt->plug[TYPEC_PLUG_SOP_PP]) tbt->state = TBT_STATE_SOP_PP_ENTER; else tbt->state = TBT_STATE_ENTER; schedule_work(&tbt->work); } return 0; } static void tbt_altmode_remove(struct typec_altmode *alt) { struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt); for (int i = TYPEC_PLUG_SOP_PP; i >= 0; --i) { if (tbt->plug[i]) typec_altmode_put_plug(tbt->plug[i]); } if (tbt->cable) typec_cable_put(tbt->cable); } static bool tbt_ready(struct typec_altmode *alt) { struct tbt_altmode *tbt = typec_altmode_get_drvdata(alt); struct typec_altmode *plug; if (tbt->cable) return true; /* Thunderbolt 3 requires a cable with eMarker */ tbt->cable = typec_cable_get(typec_altmode2port(tbt->alt)); if (!tbt->cable) return false; /* We accept systems without SOP' or SOP''. This means the port altmode * driver will be responsible for properly ordering entry/exit. */ for (int i = 0; i < TYPEC_PLUG_SOP_PP + 1; i++) { plug = typec_altmode_get_plug(tbt->alt, i); if (!plug) continue; if (plug->svid != USB_TYPEC_TBT_SID) break; plug->desc = "Thunderbolt3"; plug->cable_ops = &tbt_cable_ops; typec_altmode_set_drvdata(plug, tbt); tbt->plug[i] = plug; } return true; } static const struct typec_device_id tbt_typec_id[] = { { USB_TYPEC_TBT_SID }, { } }; MODULE_DEVICE_TABLE(typec, tbt_typec_id); static struct typec_altmode_driver tbt_altmode_driver = { .id_table = tbt_typec_id, .probe = tbt_altmode_probe, .remove = tbt_altmode_remove, .driver = { .name = "typec-thunderbolt", } }; module_typec_altmode_driver(tbt_altmode_driver); MODULE_AUTHOR("Heikki Krogerus "); MODULE_LICENSE("GPL"); MODULE_DESCRIPTION("Thunderbolt3 USB Type-C Alternate Mode");