root/drivers/usb/typec/altmodes/thunderbolt.c
// 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)
{
        return typec_altmode_enter(tbt->alt, &tbt->enter_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 (!alt->mode_selection && 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;
        u32 vdo;

        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;
        }

        vdo = tbt->alt->vdo & (TBT_VENDOR_SPECIFIC_B0 | TBT_VENDOR_SPECIFIC_B1);
        vdo |= tbt->alt->vdo & TBT_INTEL_SPECIFIC_B0;
        vdo |= TBT_MODE;
        plug = tbt->plug[TYPEC_PLUG_SOP_P];

        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 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");