root/drivers/usb/typec/ucsi/displayport.c
// SPDX-License-Identifier: GPL-2.0
/*
 * UCSI DisplayPort Alternate Mode Support
 *
 * Copyright (C) 2018, Intel Corporation
 * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
 */

#include <linux/usb/typec_dp.h>
#include <linux/usb/pd_vdo.h>

#include "ucsi.h"

#define UCSI_CMD_SET_NEW_CAM(_con_num_, _enter_, _cam_, _am_)           \
         (UCSI_SET_NEW_CAM | ((_con_num_) << 16) | ((_enter_) << 23) |  \
          ((_cam_) << 24) | ((u64)(_am_) << 32))

struct ucsi_dp {
        struct typec_displayport_data data;
        struct ucsi_connector *con;
        struct typec_altmode *alt;
        struct work_struct work;
        int offset;

        bool override;
        bool initialized;

        u32 header;
        u32 *vdo_data;
        u8 vdo_size;
};

/*
 * Note. Alternate mode control is optional feature in UCSI. It means that even
 * if the system supports alternate modes, the OS may not be aware of them.
 *
 * In most cases however, the OS will be able to see the supported alternate
 * modes, but it may still not be able to configure them, not even enter or exit
 * them. That is because UCSI defines alt mode details and alt mode "overriding"
 * as separate options.
 *
 * In case alt mode details are supported, but overriding is not, the driver
 * will still display the supported pin assignments and configuration, but any
 * changes the user attempts to do will lead into failure with return value of
 * -EOPNOTSUPP.
 */

static int ucsi_displayport_enter(struct typec_altmode *alt, u32 *vdo)
{
        struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
        struct ucsi *ucsi = dp->con->ucsi;
        int svdm_version;
        u64 command;
        u8 cur = 0;
        int ret;

        if (!ucsi_con_mutex_lock(dp->con))
                return -ENOTCONN;

        if (!dp->override && dp->initialized) {
                const struct typec_altmode *p = typec_altmode_get_partner(alt);

                dev_warn(&p->dev,
                         "firmware doesn't support alternate mode overriding\n");
                ret = -EOPNOTSUPP;
                goto err_unlock;
        }

        command = UCSI_GET_CURRENT_CAM | UCSI_CONNECTOR_NUMBER(dp->con->num);
        ret = ucsi_send_command(ucsi, command, &cur, sizeof(cur));
        if (ret < 0) {
                if (ucsi->version > 0x0100)
                        goto err_unlock;
                cur = 0xff;
        }

        if (cur != 0xff) {
                ret = dp->con->port_altmode[cur] == alt ? 0 : -EBUSY;
                goto err_unlock;
        }

        /*
         * We can't send the New CAM command yet to the PPM as it needs the
         * configuration value as well. Pretending that we have now entered the
         * mode, and letting the alt mode driver continue.
         */

        svdm_version = typec_altmode_get_svdm_version(alt);
        if (svdm_version < 0) {
                ret = svdm_version;
                goto err_unlock;
        }

        dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, CMD_ENTER_MODE);
        dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
        dp->header |= VDO_CMDT(CMDT_RSP_ACK);

        dp->vdo_data = NULL;
        dp->vdo_size = 1;

        schedule_work(&dp->work);
        ret = 0;
err_unlock:
        ucsi_con_mutex_unlock(dp->con);

        return ret;
}

static int ucsi_displayport_exit(struct typec_altmode *alt)
{
        struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
        int svdm_version;
        u64 command;
        int ret = 0;

        if (!ucsi_con_mutex_lock(dp->con))
                return -ENOTCONN;

        if (!dp->override) {
                const struct typec_altmode *p = typec_altmode_get_partner(alt);

                dev_warn(&p->dev,
                         "firmware doesn't support alternate mode overriding\n");
                ret = -EOPNOTSUPP;
                goto out_unlock;
        }

        command = UCSI_CMD_SET_NEW_CAM(dp->con->num, 0, dp->offset, 0);
        ret = ucsi_send_command(dp->con->ucsi, command, NULL, 0);
        if (ret < 0)
                goto out_unlock;

        svdm_version = typec_altmode_get_svdm_version(alt);
        if (svdm_version < 0) {
                ret = svdm_version;
                goto out_unlock;
        }

        dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, CMD_EXIT_MODE);
        dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
        dp->header |= VDO_CMDT(CMDT_RSP_ACK);

        dp->vdo_data = NULL;
        dp->vdo_size = 1;

        schedule_work(&dp->work);

out_unlock:
        ucsi_con_mutex_unlock(dp->con);

        return ret;
}

/*
 * We do not actually have access to the Status Update VDO, so we have to guess
 * things.
 */
static int ucsi_displayport_status_update(struct ucsi_dp *dp)
{
        u32 cap = dp->alt->vdo;

        dp->data.status = DP_STATUS_ENABLED;

        /*
         * If pin assignement D is supported, claiming always
         * that Multi-function is preferred.
         */
        if (DP_CAP_CAPABILITY(cap) & DP_CAP_UFP_D) {
                dp->data.status |= DP_STATUS_CON_DFP_D;

                if (DP_CAP_UFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D))
                        dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC;
        } else {
                dp->data.status |= DP_STATUS_CON_UFP_D;

                if (DP_CAP_DFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D))
                        dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC;
        }

        dp->vdo_data = &dp->data.status;
        dp->vdo_size = 2;

        return 0;
}

static int ucsi_displayport_configure(struct ucsi_dp *dp)
{
        u64 command;

        if (!dp->override)
                return 0;

        command = UCSI_CMD_SET_NEW_CAM(dp->con->num, 1, dp->offset, dp->data.conf);

        return ucsi_send_command(dp->con->ucsi, command, NULL, 0);
}

static int ucsi_displayport_vdm(struct typec_altmode *alt,
                                u32 header, const u32 *data, int count)
{
        struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
        int cmd_type = PD_VDO_CMDT(header);
        int cmd = PD_VDO_CMD(header);
        int svdm_version;

        if (!ucsi_con_mutex_lock(dp->con))
                return -ENOTCONN;

        if (!dp->override && dp->initialized) {
                const struct typec_altmode *p = typec_altmode_get_partner(alt);

                dev_warn(&p->dev,
                         "firmware doesn't support alternate mode overriding\n");
                ucsi_con_mutex_unlock(dp->con);
                return -EOPNOTSUPP;
        }

        svdm_version = typec_altmode_get_svdm_version(alt);
        if (svdm_version < 0) {
                ucsi_con_mutex_unlock(dp->con);
                return svdm_version;
        }

        switch (cmd_type) {
        case CMDT_INIT:
                if (PD_VDO_SVDM_VER(header) < svdm_version) {
                        typec_partner_set_svdm_version(dp->con->partner, PD_VDO_SVDM_VER(header));
                        svdm_version = PD_VDO_SVDM_VER(header);
                }

                dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, cmd);
                dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);

                switch (cmd) {
                case DP_CMD_STATUS_UPDATE:
                        if (ucsi_displayport_status_update(dp))
                                dp->header |= VDO_CMDT(CMDT_RSP_NAK);
                        else
                                dp->header |= VDO_CMDT(CMDT_RSP_ACK);
                        break;
                case DP_CMD_CONFIGURE:
                        if (count < 2) {
                                dp->header |= VDO_CMDT(CMDT_RSP_NAK);
                                break;
                        }
                        dp->data.conf = *data;
                        if (ucsi_displayport_configure(dp)) {
                                dp->header |= VDO_CMDT(CMDT_RSP_NAK);
                        } else {
                                dp->header |= VDO_CMDT(CMDT_RSP_ACK);
                                if (dp->initialized)
                                        ucsi_altmode_update_active(dp->con);
                                else
                                        dp->initialized = true;
                        }
                        break;
                default:
                        dp->header |= VDO_CMDT(CMDT_RSP_ACK);
                        break;
                }

                schedule_work(&dp->work);
                break;
        default:
                break;
        }

        ucsi_con_mutex_unlock(dp->con);

        return 0;
}

static const struct typec_altmode_ops ucsi_displayport_ops = {
        .enter = ucsi_displayport_enter,
        .exit = ucsi_displayport_exit,
        .vdm = ucsi_displayport_vdm,
};

static void ucsi_displayport_work(struct work_struct *work)
{
        struct ucsi_dp *dp = container_of(work, struct ucsi_dp, work);
        int ret;

        ret = typec_altmode_vdm(dp->alt, dp->header,
                                dp->vdo_data, dp->vdo_size);
        if (ret)
                dev_err(&dp->alt->dev, "VDM 0x%x failed\n", dp->header);

        dp->vdo_data = NULL;
        dp->vdo_size = 0;
        dp->header = 0;
}

void ucsi_displayport_remove_partner(struct typec_altmode *alt)
{
        struct ucsi_dp *dp;

        if (!alt)
                return;

        dp = typec_altmode_get_drvdata(alt);
        if (!dp)
                return;

        cancel_work_sync(&dp->work);

        dp->data.conf = 0;
        dp->data.status = 0;
        dp->initialized = false;
}

struct typec_altmode *ucsi_register_displayport(struct ucsi_connector *con,
                                                bool override, int offset,
                                                struct typec_altmode_desc *desc)
{
        u8 all_assignments = BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D) |
                             BIT(DP_PIN_ASSIGN_E);
        struct typec_altmode *alt;
        struct ucsi_dp *dp;

        /* We can't rely on the firmware with the capabilities. */
        desc->vdo |= DP_CAP_DP_SIGNALLING(0) | DP_CAP_RECEPTACLE;

        /* Claiming that we support all pin assignments */
        desc->vdo |= all_assignments << 8;
        desc->vdo |= all_assignments << 16;

        alt = typec_port_register_altmode(con->port, desc);
        if (IS_ERR(alt))
                return alt;

        dp = devm_kzalloc(&alt->dev, sizeof(*dp), GFP_KERNEL);
        if (!dp) {
                typec_unregister_altmode(alt);
                return ERR_PTR(-ENOMEM);
        }

        INIT_WORK(&dp->work, ucsi_displayport_work);
        dp->override = override;
        dp->offset = offset;
        dp->con = con;
        dp->alt = alt;

        typec_altmode_set_ops(alt, &ucsi_displayport_ops);
        typec_altmode_set_drvdata(alt, dp);

        return alt;
}