]> www.infradead.org Git - users/hch/misc.git/commitdiff
usb: typec: Add driver for Thunderbolt 3 Alternate Mode
authorHeikki Krogerus <heikki.krogerus@linux.intel.com>
Fri, 13 Dec 2024 23:35:43 +0000 (15:35 -0800)
committerGreg Kroah-Hartman <gregkh@linuxfoundation.org>
Tue, 24 Dec 2024 07:56:05 +0000 (08:56 +0100)
Thunderbolt 3 Alternate Mode entry flow is described in
USB Type-C Specification Release 2.0.

Signed-off-by: Heikki Krogerus <heikki.krogerus@linux.intel.com>
Co-developed-by: Abhishek Pandit-Subedi <abhishekpandit@chromium.org>
Signed-off-by: Abhishek Pandit-Subedi <abhishekpandit@chromium.org>
Reviewed-by: Benson Leung <bleung@chromium.org>
Link: https://lore.kernel.org/r/20241213153543.v5.2.I3080b036e8de0b9957c57c1c3059db7149c5e549@changeid
Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
drivers/usb/typec/altmodes/Kconfig
drivers/usb/typec/altmodes/Makefile
drivers/usb/typec/altmodes/thunderbolt.c [new file with mode: 0644]
include/linux/usb/typec_tbt.h

index 1a6b5e872b0d98ce5e34d1bc66cc26f02bbc4720..7867fa7c405d3e0f72193edff0314bd2ec7f248f 100644 (file)
@@ -23,4 +23,13 @@ config TYPEC_NVIDIA_ALTMODE
          To compile this driver as a module, choose M here: the
          module will be called typec_nvidia.
 
+config TYPEC_TBT_ALTMODE
+       tristate "Thunderbolt3 Alternate Mode driver"
+       help
+         Select this option if you have Thunderbolt3 hardware on your
+         system.
+
+         To compile this driver as a module, choose M here: the
+         module will be called typec_thunderbolt.
+
 endmenu
index 45717548b396074ddc8a752837f81f9b37a15b46..508a68351bd2075650ddd09b92099a5f1f8f49a2 100644 (file)
@@ -4,3 +4,5 @@ obj-$(CONFIG_TYPEC_DP_ALTMODE)          += typec_displayport.o
 typec_displayport-y                    := displayport.o
 obj-$(CONFIG_TYPEC_NVIDIA_ALTMODE)     += typec_nvidia.o
 typec_nvidia-y                         := nvidia.o
+obj-$(CONFIG_TYPEC_TBT_ALTMODE)                += typec_thunderbolt.o
+typec_thunderbolt-y                    := thunderbolt.o
diff --git a/drivers/usb/typec/altmodes/thunderbolt.c b/drivers/usb/typec/altmodes/thunderbolt.c
new file mode 100644 (file)
index 0000000..1b475b1
--- /dev/null
@@ -0,0 +1,388 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * USB Typec-C Thunderbolt3 Alternate Mode driver
+ *
+ * Copyright (C) 2019 Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/lockdep.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/workqueue.h>
+#include <linux/usb/pd_vdo.h>
+#include <linux/usb/typec_altmode.h>
+#include <linux/usb/typec_tbt.h>
+
+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 (IS_ERR(plug))
+                       continue;
+
+               if (!plug || 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 <heikki.krogerus@linux.intel.com>");
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("Thunderbolt3 USB Type-C Alternate Mode");
index fa97d7e00f5c7896ab23cd9b25a9e6acef309711..55dcea12082c80defeb6f199ad333279b58adcd7 100644 (file)
@@ -44,6 +44,7 @@ struct typec_thunderbolt_data {
 
 #define   TBT_GEN3_NON_ROUNDED                 0
 #define   TBT_GEN3_GEN4_ROUNDED_NON_ROUNDED    1
+#define TBT_CABLE_ROUNDED              BIT(19)
 #define TBT_CABLE_OPTICAL              BIT(21)
 #define TBT_CABLE_RETIMER              BIT(22)
 #define TBT_CABLE_LINK_TRAINING                BIT(23)