Contributors: 3
Author Tokens Token Proportion Commits Commit Proportion
Andrei Kuchynski 895 98.03% 1 25.00%
Heikki Krogerus 17 1.86% 2 50.00%
Greg Kroah-Hartman 1 0.11% 1 25.00%
Total 913 4


// SPDX-License-Identifier: GPL-2.0
/*
 * UCSI Thunderbolt Alternate Mode Support
 *
 * Copyright 2026 Google LLC
 */

#include <linux/usb/typec_tbt.h>
#include <linux/usb/pd_vdo.h>
#include <linux/err.h>
#include <linux/dev_printk.h>
#include <linux/device/devres.h>
#include <linux/gfp_types.h>
#include <linux/types.h>
#include <linux/usb/typec_altmode.h>
#include <linux/workqueue.h>

#include "ucsi.h"

/**
 * struct ucsi_tbt - Thunderbolt Alternate Mode private data structure
 * @con: Pointer to UCSI connector structure
 * @alt: Pointer to typec altmode structure
 * @work: Work structure
 * @cam: An offset into the list of alternate modes supported by the PPM
 * @header: VDO header
 */
struct ucsi_tbt {
	struct ucsi_connector *con;
	struct typec_altmode *alt;
	struct work_struct work;
	int cam;
	u32 header;
};

static void ucsi_thunderbolt_work(struct work_struct *work)
{
	struct ucsi_tbt *tbt = container_of(work, struct ucsi_tbt, work);

	if (typec_altmode_vdm(tbt->alt, tbt->header, NULL, 0))
		dev_err(&tbt->alt->dev, "VDM 0x%x failed\n", tbt->header);

	tbt->header = 0;
}

static int ucsi_thunderbolt_set_altmode(struct ucsi_tbt *tbt,
					bool enter, u32 vdo)
{
	int svdm_version;
	int cmd;
	int ret;
	u64 command = UCSI_SET_NEW_CAM |
		      UCSI_CONNECTOR_NUMBER(tbt->con->num) |
		      UCSI_SET_NEW_CAM_SET_AM(tbt->cam) |
		      ((u64)vdo << 32);

	if (enter)
		command |= (1 << 23);

	ret = ucsi_send_command(tbt->con->ucsi, command, NULL, 0);
	if (ret < 0)
		return ret;

	svdm_version = typec_altmode_get_svdm_version(tbt->alt);
	if (svdm_version < 0)
		return svdm_version;

	if (enter)
		cmd = CMD_ENTER_MODE;
	else
		cmd = CMD_EXIT_MODE;
	tbt->header = VDO(USB_TYPEC_TBT_SID, 1, svdm_version, cmd);
	tbt->header |= VDO_OPOS(TYPEC_TBT_MODE);
	tbt->header |= VDO_CMDT(CMDT_RSP_ACK);

	schedule_work(&tbt->work);

	return 0;
}

static int ucsi_thunderbolt_enter(struct typec_altmode *alt, u32 *vdo)
{
	struct ucsi_tbt *tbt = typec_altmode_get_drvdata(alt);
	struct ucsi_connector *con = tbt->con;
	u64 command;
	u8 cur = 0;
	int ret;

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

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

	if (cur != 0xff) {
		if (cur >= UCSI_MAX_ALTMODES || con->port_altmode[cur] != alt)
			ret = -EBUSY;
		else
			ret = 0;
		goto err_unlock;
	}

	ret = ucsi_thunderbolt_set_altmode(tbt, true, *vdo);
	ucsi_altmode_update_active(tbt->con);

err_unlock:
	ucsi_con_mutex_unlock(con);

	return ret;
}

static int ucsi_thunderbolt_exit(struct typec_altmode *alt)
{
	struct ucsi_tbt *tbt = typec_altmode_get_drvdata(alt);
	int ret;

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

	ret = ucsi_thunderbolt_set_altmode(tbt, false, 0);

	ucsi_con_mutex_unlock(tbt->con);

	return ret;
}

static int ucsi_thunderbolt_vdm(struct typec_altmode *alt,
				u32 header, const u32 *data, int count)
{
	struct ucsi_tbt *tbt = 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(tbt->con))
		return -ENOTCONN;

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

	switch (cmd_type) {
	case CMDT_INIT:
		if (PD_VDO_SVDM_VER(header) < svdm_version) {
			svdm_version = PD_VDO_SVDM_VER(header);
			typec_partner_set_svdm_version(tbt->con->partner, svdm_version);
		}
		tbt->header = VDO(USB_TYPEC_TBT_SID, 1, svdm_version, cmd);
		tbt->header |= VDO_OPOS(TYPEC_TBT_MODE);
		tbt->header |= VDO_CMDT(CMDT_RSP_ACK);

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

	ucsi_con_mutex_unlock(tbt->con);

	return 0;
}

static const struct typec_altmode_ops ucsi_thunderbolt_ops = {
	.enter = ucsi_thunderbolt_enter,
	.exit = ucsi_thunderbolt_exit,
	.vdm = ucsi_thunderbolt_vdm,
};

struct typec_altmode *ucsi_register_thunderbolt(struct ucsi_connector *con,
						bool override, int offset,
						struct typec_altmode_desc *desc)
{
	struct typec_altmode *alt;
	struct ucsi_tbt *tbt;

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

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

	tbt->cam = offset;
	tbt->con = con;
	tbt->alt = alt;
	INIT_WORK(&tbt->work, ucsi_thunderbolt_work);
	typec_altmode_set_drvdata(alt, tbt);
	typec_altmode_set_ops(alt, &ucsi_thunderbolt_ops);

	return alt;
}

void ucsi_thunderbolt_remove_partner(struct typec_altmode *alt)
{
	struct ucsi_tbt *tbt;

	if (alt) {
		tbt = typec_altmode_get_drvdata(alt);
		if (tbt)
			cancel_work_sync(&tbt->work);
	}
}