// 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;
|
|
mutex_lock(&dp->con->lock);
|
|
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:
|
mutex_unlock(&dp->con->lock);
|
|
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;
|
|
mutex_lock(&dp->con->lock);
|
|
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:
|
mutex_unlock(&dp->con->lock);
|
|
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_UFP_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_DFP_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)
|
{
|
u32 pins = DP_CONF_GET_PIN_ASSIGN(dp->data.conf);
|
u64 command;
|
|
if (!dp->override)
|
return 0;
|
|
command = UCSI_CMD_SET_NEW_CAM(dp->con->num, 1, dp->offset, pins);
|
|
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;
|
|
mutex_lock(&dp->con->lock);
|
|
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");
|
mutex_unlock(&dp->con->lock);
|
return -EOPNOTSUPP;
|
}
|
|
svdm_version = typec_altmode_get_svdm_version(alt);
|
if (svdm_version < 0) {
|
mutex_unlock(&dp->con->lock);
|
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:
|
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;
|
}
|
|
mutex_unlock(&dp->con->lock);
|
|
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;
|
|
mutex_lock(&dp->con->lock);
|
|
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;
|
|
mutex_unlock(&dp->con->lock);
|
}
|
|
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;
|
|
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_SIGNALING | 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;
|
|
alt->ops = &ucsi_displayport_ops;
|
typec_altmode_set_drvdata(alt, dp);
|
|
return alt;
|
}
|