summaryrefslogtreecommitdiff
path: root/drivers/usb/typec
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-02-17 09:36:43 -0800
committerLinus Torvalds <torvalds@linux-foundation.org>2026-02-17 09:36:43 -0800
commit17f8d2009367c3da82882f70ccbdca9f8c7b5f20 (patch)
tree80b8dc44dc22336501072cdc3b3e49d14c3867e1 /drivers/usb/typec
parent3ad7945754000d868ed86315d33085a914c422c1 (diff)
parentda87d45b195148d670ab995367d52aa9e8a9a1fa (diff)
Merge tag 'usb-7.0-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb
Pull USB / Thunderbolt updates from Greg KH: "Here is the "big" set of USB and Thunderbolt driver updates for 7.0-rc1. Overall more lines were removed than added, thanks to dropping the obsolete isp1362 USB host controller driver, always a nice change. Other than that, nothing major happening here, highlights are: - lots of dwc3 driver updates and new hardware support added - usb gadget function driver updates - usb phy driver updates - typec driver updates and additions - USB rust binding updates for syntax and formatting changes - more usb serial device ids added - other smaller USB core and driver updates and additions All of these have been in linux-next for a long time, with no reported problems" * tag 'usb-7.0-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb: (77 commits) usb: typec: ucsi: Add Thunderbolt alternate mode support usb: typec: hd3ss3220: Check if regulator needs to be switched usb: phy: tegra: parametrize PORTSC1 register offset usb: phy: tegra: parametrize HSIC PTS value usb: phy: tegra: return error value from utmi_wait_register usb: phy: tegra: cosmetic fixes dt-bindings: usb: renesas,usbhs: Add RZ/G3E SoC support usb: dwc2: fix resume failure if dr_mode is host usb: cdns3: fix role switching during resume usb: dwc3: gadget: Move vbus draw to workqueue context USB: serial: option: add Telit FN920C04 RNDIS compositions usb: dwc3: Log dwc3 address in traces usb: gadget: tegra-xudc: Add handling for BLCG_COREPLL_PWRDN usb: phy: tegra: add HSIC support usb: phy: tegra: use phy type directly usb: typec: ucsi: Enforce mode selection for cros_ec_ucsi usb: typec: ucsi: Support mode selection to activate altmodes usb: typec: Introduce mode_selection bit usb: typec: Implement mode selection usb: typec: Expose alternate mode priority via sysfs ...
Diffstat (limited to 'drivers/usb/typec')
-rw-r--r--drivers/usb/typec/Makefile2
-rw-r--r--drivers/usb/typec/altmodes/displayport.c6
-rw-r--r--drivers/usb/typec/altmodes/thunderbolt.c2
-rw-r--r--drivers/usb/typec/bus.c25
-rw-r--r--drivers/usb/typec/bus.h6
-rw-r--r--drivers/usb/typec/class.c136
-rw-r--r--drivers/usb/typec/class.h3
-rw-r--r--drivers/usb/typec/hd3ss3220.c30
-rw-r--r--drivers/usb/typec/mode_selection.c283
-rw-r--r--drivers/usb/typec/tcpm/tcpm.c2
-rw-r--r--drivers/usb/typec/ucsi/Kconfig1
-rw-r--r--drivers/usb/typec/ucsi/Makefile4
-rw-r--r--drivers/usb/typec/ucsi/cros_ec_ucsi.c22
-rw-r--r--drivers/usb/typec/ucsi/psy.c54
-rw-r--r--drivers/usb/typec/ucsi/thunderbolt.c212
-rw-r--r--drivers/usb/typec/ucsi/ucsi.c30
-rw-r--r--drivers/usb/typec/ucsi/ucsi.h24
17 files changed, 787 insertions, 55 deletions
diff --git a/drivers/usb/typec/Makefile b/drivers/usb/typec/Makefile
index 7a368fea61bc..8a6a1c663eb6 100644
--- a/drivers/usb/typec/Makefile
+++ b/drivers/usb/typec/Makefile
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
obj-$(CONFIG_TYPEC) += typec.o
-typec-y := class.o mux.o bus.o pd.o retimer.o
+typec-y := class.o mux.o bus.o pd.o retimer.o mode_selection.o
typec-$(CONFIG_ACPI) += port-mapper.o
obj-$(CONFIG_TYPEC) += altmodes/
obj-$(CONFIG_TYPEC_TCPM) += tcpm/
diff --git a/drivers/usb/typec/altmodes/displayport.c b/drivers/usb/typec/altmodes/displayport.c
index d96ab106a980..d185688a16b1 100644
--- a/drivers/usb/typec/altmodes/displayport.c
+++ b/drivers/usb/typec/altmodes/displayport.c
@@ -804,8 +804,10 @@ int dp_altmode_probe(struct typec_altmode *alt)
if (plug)
typec_altmode_set_drvdata(plug, dp);
- dp->state = plug ? DP_STATE_ENTER_PRIME : DP_STATE_ENTER;
- schedule_work(&dp->work);
+ if (!alt->mode_selection) {
+ dp->state = plug ? DP_STATE_ENTER_PRIME : DP_STATE_ENTER;
+ schedule_work(&dp->work);
+ }
return 0;
}
diff --git a/drivers/usb/typec/altmodes/thunderbolt.c b/drivers/usb/typec/altmodes/thunderbolt.c
index 6eadf7835f8f..c4c5da6154da 100644
--- a/drivers/usb/typec/altmodes/thunderbolt.c
+++ b/drivers/usb/typec/altmodes/thunderbolt.c
@@ -307,7 +307,7 @@ static int tbt_altmode_probe(struct typec_altmode *alt)
typec_altmode_set_drvdata(alt, tbt);
typec_altmode_set_ops(alt, &tbt_altmode_ops);
- if (tbt_ready(alt)) {
+ 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])
diff --git a/drivers/usb/typec/bus.c b/drivers/usb/typec/bus.c
index a884cec9ab7e..e84b134a3381 100644
--- a/drivers/usb/typec/bus.c
+++ b/drivers/usb/typec/bus.c
@@ -445,7 +445,23 @@ static struct attribute *typec_attrs[] = {
&dev_attr_description.attr,
NULL
};
-ATTRIBUTE_GROUPS(typec);
+
+static umode_t typec_is_visible(struct kobject *kobj, struct attribute *attr, int n)
+{
+ if (is_typec_partner_altmode(kobj_to_dev(kobj)))
+ return attr->mode;
+ return 0;
+}
+
+static const struct attribute_group typec_group = {
+ .is_visible = typec_is_visible,
+ .attrs = typec_attrs,
+};
+
+static const struct attribute_group *typec_groups[] = {
+ &typec_group,
+ NULL
+};
static int typec_match(struct device *dev, const struct device_driver *driver)
{
@@ -453,6 +469,9 @@ static int typec_match(struct device *dev, const struct device_driver *driver)
struct typec_altmode *altmode = to_typec_altmode(dev);
const struct typec_device_id *id;
+ if (!is_typec_partner_altmode(dev))
+ return 0;
+
for (id = drv->id_table; id->svid; id++)
if (id->svid == altmode->svid)
return 1;
@@ -463,6 +482,9 @@ static int typec_uevent(const struct device *dev, struct kobj_uevent_env *env)
{
const struct typec_altmode *altmode = to_typec_altmode(dev);
+ if (!is_typec_partner_altmode(dev))
+ return 0;
+
if (add_uevent_var(env, "SVID=%04X", altmode->svid))
return -ENOMEM;
@@ -547,3 +569,4 @@ const struct bus_type typec_bus = {
.probe = typec_probe,
.remove = typec_remove,
};
+EXPORT_SYMBOL_GPL(typec_bus);
diff --git a/drivers/usb/typec/bus.h b/drivers/usb/typec/bus.h
index 643b8c81786d..7df5deb1dd3a 100644
--- a/drivers/usb/typec/bus.h
+++ b/drivers/usb/typec/bus.h
@@ -5,7 +5,6 @@
#include <linux/usb/typec_altmode.h>
-struct bus_type;
struct typec_mux;
struct typec_retimer;
@@ -28,9 +27,4 @@ struct altmode {
#define to_altmode(d) container_of(d, struct altmode, adev)
-extern const struct bus_type typec_bus;
-extern const struct device_type typec_altmode_dev_type;
-
-#define is_typec_altmode(_dev_) (_dev_->type == &typec_altmode_dev_type)
-
#endif /* __USB_TYPEC_ALTMODE_H__ */
diff --git a/drivers/usb/typec/class.c b/drivers/usb/typec/class.c
index 9b2647cb199b..dbba53f02497 100644
--- a/drivers/usb/typec/class.c
+++ b/drivers/usb/typec/class.c
@@ -235,7 +235,7 @@ static int altmode_match(struct device *dev, const void *data)
struct typec_altmode *adev = to_typec_altmode(dev);
const struct typec_device_id *id = data;
- if (!is_typec_altmode(dev))
+ if (!is_typec_port_altmode(dev))
return 0;
return (adev->svid == id->svid);
@@ -445,11 +445,88 @@ svid_show(struct device *dev, struct device_attribute *attr, char *buf)
}
static DEVICE_ATTR_RO(svid);
+static int increment_duplicated_priority(struct device *dev, void *data)
+{
+ if (is_typec_port_altmode(dev)) {
+ struct typec_altmode **alt_target = (struct typec_altmode **)data;
+ struct typec_altmode *alt = to_typec_altmode(dev);
+
+ if (alt != *alt_target && alt->priority == (*alt_target)->priority) {
+ alt->priority++;
+ *alt_target = alt;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int find_duplicated_priority(struct device *dev, void *data)
+{
+ if (is_typec_port_altmode(dev)) {
+ struct typec_altmode **alt_target = (struct typec_altmode **)data;
+ struct typec_altmode *alt = to_typec_altmode(dev);
+
+ if (alt != *alt_target && alt->priority == (*alt_target)->priority)
+ return 1;
+ }
+ return 0;
+}
+
+static int typec_mode_set_priority(struct typec_altmode *alt, const u8 priority)
+{
+ struct typec_port *port = to_typec_port(alt->dev.parent);
+ const u8 old_priority = alt->priority;
+ int res = 1;
+
+ alt->priority = priority;
+ while (res) {
+ res = device_for_each_child(&port->dev, &alt, find_duplicated_priority);
+ if (res) {
+ alt->priority++;
+ if (alt->priority == 0) {
+ alt->priority = old_priority;
+ return -EOVERFLOW;
+ }
+ }
+ }
+
+ res = 1;
+ alt->priority = priority;
+ while (res)
+ res = device_for_each_child(&port->dev, &alt,
+ increment_duplicated_priority);
+
+ return 0;
+}
+
+static ssize_t priority_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ u8 val;
+ int err = kstrtou8(buf, 10, &val);
+
+ if (!err)
+ err = typec_mode_set_priority(to_typec_altmode(dev), val);
+
+ if (!err)
+ return size;
+ return err;
+}
+
+static ssize_t priority_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ return sysfs_emit(buf, "%u\n", to_typec_altmode(dev)->priority);
+}
+static DEVICE_ATTR_RW(priority);
+
static struct attribute *typec_altmode_attrs[] = {
&dev_attr_active.attr,
&dev_attr_mode.attr,
&dev_attr_svid.attr,
&dev_attr_vdo.attr,
+ &dev_attr_priority.attr,
NULL
};
@@ -457,11 +534,17 @@ static umode_t typec_altmode_attr_is_visible(struct kobject *kobj,
struct attribute *attr, int n)
{
struct typec_altmode *adev = to_typec_altmode(kobj_to_dev(kobj));
+ struct typec_port *port = typec_altmode2port(adev);
- if (attr == &dev_attr_active.attr)
- if (!is_typec_port(adev->dev.parent) &&
- (!adev->ops || !adev->ops->activate))
- return 0444;
+ if (attr == &dev_attr_active.attr) {
+ if (!is_typec_port(adev->dev.parent)) {
+ if (!port->mode_control || !adev->ops || !adev->ops->activate)
+ return 0444;
+ }
+ } else if (attr == &dev_attr_priority.attr) {
+ if (!is_typec_port(adev->dev.parent) || !port->mode_control)
+ return 0;
+ }
return attr->mode;
}
@@ -532,15 +615,31 @@ static void typec_altmode_release(struct device *dev)
kfree(alt);
}
-const struct device_type typec_altmode_dev_type = {
- .name = "typec_alternate_mode",
+const struct device_type typec_port_altmode_dev_type = {
+ .name = "typec_port_alternate_mode",
.groups = typec_altmode_groups,
.release = typec_altmode_release,
};
+EXPORT_SYMBOL_GPL(typec_port_altmode_dev_type);
+
+const struct device_type typec_plug_altmode_dev_type = {
+ .name = "typec_plug_alternate_mode",
+ .groups = typec_altmode_groups,
+ .release = typec_altmode_release,
+};
+EXPORT_SYMBOL_GPL(typec_plug_altmode_dev_type);
+
+const struct device_type typec_partner_altmode_dev_type = {
+ .name = "typec_partner_alternate_mode",
+ .groups = typec_altmode_groups,
+ .release = typec_altmode_release,
+};
+EXPORT_SYMBOL_GPL(typec_partner_altmode_dev_type);
static struct typec_altmode *
typec_register_altmode(struct device *parent,
- const struct typec_altmode_desc *desc)
+ const struct typec_altmode_desc *desc,
+ const struct device_type *type)
{
unsigned int id = altmode_id_get(parent);
bool is_port = is_typec_port(parent);
@@ -556,6 +655,7 @@ typec_register_altmode(struct device *parent,
alt->adev.svid = desc->svid;
alt->adev.mode = desc->mode;
alt->adev.vdo = desc->vdo;
+ alt->adev.mode_selection = desc->mode_selection;
alt->roles = desc->roles;
alt->id = id;
@@ -575,7 +675,7 @@ typec_register_altmode(struct device *parent,
alt->adev.dev.parent = parent;
alt->adev.dev.groups = alt->groups;
- alt->adev.dev.type = &typec_altmode_dev_type;
+ alt->adev.dev.type = type;
dev_set_name(&alt->adev.dev, "%s.%u", dev_name(parent), id);
get_device(alt->adev.dev.parent);
@@ -584,9 +684,7 @@ typec_register_altmode(struct device *parent,
if (!is_port)
typec_altmode_set_partner(alt);
- /* The partners are bind to drivers */
- if (is_typec_partner(parent))
- alt->adev.dev.bus = &typec_bus;
+ alt->adev.dev.bus = &typec_bus;
/* Plug alt modes need a class to generate udev events. */
if (is_typec_plug(parent))
@@ -963,7 +1061,7 @@ struct typec_altmode *
typec_partner_register_altmode(struct typec_partner *partner,
const struct typec_altmode_desc *desc)
{
- return typec_register_altmode(&partner->dev, desc);
+ return typec_register_altmode(&partner->dev, desc, &typec_partner_altmode_dev_type);
}
EXPORT_SYMBOL_GPL(typec_partner_register_altmode);
@@ -1193,7 +1291,7 @@ struct typec_altmode *
typec_plug_register_altmode(struct typec_plug *plug,
const struct typec_altmode_desc *desc)
{
- return typec_register_altmode(&plug->dev, desc);
+ return typec_register_altmode(&plug->dev, desc, &typec_plug_altmode_dev_type);
}
EXPORT_SYMBOL_GPL(typec_plug_register_altmode);
@@ -2482,6 +2580,7 @@ typec_port_register_altmode(struct typec_port *port,
struct typec_altmode *adev;
struct typec_mux *mux;
struct typec_retimer *retimer;
+ int ret;
mux = typec_mux_get(&port->dev);
if (IS_ERR(mux))
@@ -2493,13 +2592,19 @@ typec_port_register_altmode(struct typec_port *port,
return ERR_CAST(retimer);
}
- adev = typec_register_altmode(&port->dev, desc);
+ adev = typec_register_altmode(&port->dev, desc, &typec_port_altmode_dev_type);
if (IS_ERR(adev)) {
typec_retimer_put(retimer);
typec_mux_put(mux);
} else {
to_altmode(adev)->mux = mux;
to_altmode(adev)->retimer = retimer;
+
+ ret = typec_mode_set_priority(adev, 0);
+ if (ret) {
+ typec_unregister_altmode(adev);
+ return ERR_PTR(ret);
+ }
}
return adev;
@@ -2694,6 +2799,7 @@ struct typec_port *typec_register_port(struct device *parent,
}
port->pd = cap->pd;
+ port->mode_control = !cap->no_mode_control;
ret = device_add(&port->dev);
if (ret) {
diff --git a/drivers/usb/typec/class.h b/drivers/usb/typec/class.h
index db2fe96c48ff..d3435936ee7c 100644
--- a/drivers/usb/typec/class.h
+++ b/drivers/usb/typec/class.h
@@ -9,6 +9,7 @@
struct typec_mux;
struct typec_switch;
struct usb_device;
+struct mode_selection;
struct typec_plug {
struct device dev;
@@ -39,6 +40,7 @@ struct typec_partner {
u8 usb_capability;
struct usb_power_delivery *pd;
+ struct mode_selection *sel;
void (*attach)(struct typec_partner *partner, struct device *dev);
void (*deattach)(struct typec_partner *partner, struct device *dev);
@@ -62,6 +64,7 @@ struct typec_port {
struct mutex partner_link_lock;
enum typec_orientation orientation;
+ bool mode_control;
struct typec_switch *sw;
struct typec_mux *mux;
struct typec_retimer *retimer;
diff --git a/drivers/usb/typec/hd3ss3220.c b/drivers/usb/typec/hd3ss3220.c
index 3876f4faead6..3e39b800e6b5 100644
--- a/drivers/usb/typec/hd3ss3220.c
+++ b/drivers/usb/typec/hd3ss3220.c
@@ -204,6 +204,23 @@ static const struct typec_operations hd3ss3220_ops = {
.port_type_set = hd3ss3220_port_type_set,
};
+static void hd3ss3220_regulator_control(struct hd3ss3220 *hd3ss3220, bool on)
+{
+ int ret;
+
+ if (regulator_is_enabled(hd3ss3220->vbus) == on)
+ return;
+
+ if (on)
+ ret = regulator_enable(hd3ss3220->vbus);
+ else
+ ret = regulator_disable(hd3ss3220->vbus);
+
+ if (ret)
+ dev_err(hd3ss3220->dev,
+ "vbus regulator %s failed: %d\n", on ? "disable" : "enable", ret);
+}
+
static void hd3ss3220_set_role(struct hd3ss3220 *hd3ss3220)
{
enum usb_role role_state = hd3ss3220_get_attached_state(hd3ss3220);
@@ -221,6 +238,9 @@ static void hd3ss3220_set_role(struct hd3ss3220 *hd3ss3220)
break;
}
+ if (hd3ss3220->vbus && !hd3ss3220->id_gpiod)
+ hd3ss3220_regulator_control(hd3ss3220, role_state == USB_ROLE_HOST);
+
hd3ss3220->role_state = role_state;
}
@@ -330,18 +350,10 @@ static const struct regmap_config config = {
static irqreturn_t hd3ss3220_id_isr(int irq, void *dev_id)
{
struct hd3ss3220 *hd3ss3220 = dev_id;
- int ret;
int id;
id = gpiod_get_value_cansleep(hd3ss3220->id_gpiod);
- if (!id)
- ret = regulator_enable(hd3ss3220->vbus);
- else
- ret = regulator_disable(hd3ss3220->vbus);
-
- if (ret)
- dev_err(hd3ss3220->dev,
- "vbus regulator %s failed: %d\n", id ? "disable" : "enable", ret);
+ hd3ss3220_regulator_control(hd3ss3220, !id);
return IRQ_HANDLED;
}
diff --git a/drivers/usb/typec/mode_selection.c b/drivers/usb/typec/mode_selection.c
new file mode 100644
index 000000000000..a95b31e21b52
--- /dev/null
+++ b/drivers/usb/typec/mode_selection.c
@@ -0,0 +1,283 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright 2025 Google LLC.
+ */
+
+#include <linux/types.h>
+#include <linux/list_sort.h>
+#include <linux/slab.h>
+#include <linux/mutex.h>
+#include <linux/workqueue.h>
+#include <linux/usb/typec_altmode.h>
+
+#include "class.h"
+
+/**
+ * struct mode_state - State tracking for a specific Type-C alternate mode
+ * @svid: Standard or Vendor ID of the Alternate Mode
+ * @priority: Mode priority
+ * @error: Outcome of the last attempt to enter the mode
+ * @list: List head to link this mode state into a prioritized list
+ */
+struct mode_state {
+ u16 svid;
+ u8 priority;
+ int error;
+ struct list_head list;
+};
+
+/**
+ * struct mode_selection - Manages the selection and state of Alternate Modes
+ * @mode_list: Prioritized list of available Alternate Modes
+ * @lock: Mutex to protect mode_list
+ * @work: Work structure
+ * @partner: Handle to the Type-C partner device
+ * @active_svid: svid of currently active mode
+ * @timeout: Timeout for a mode entry attempt, ms
+ * @delay: Delay between mode entry/exit attempts, ms
+ */
+struct mode_selection {
+ struct list_head mode_list;
+ /* Protects the mode_list*/
+ struct mutex lock;
+ struct delayed_work work;
+ struct typec_partner *partner;
+ u16 active_svid;
+ unsigned int timeout;
+ unsigned int delay;
+};
+
+/**
+ * struct mode_order - Mode activation tracking
+ * @svid: Standard or Vendor ID of the Alternate Mode
+ * @enter: Flag indicating if the driver is currently attempting to enter or
+ * exit the mode
+ * @result: Outcome of the attempt to activate the mode
+ */
+struct mode_order {
+ u16 svid;
+ int enter;
+ int result;
+};
+
+static int activate_altmode(struct device *dev, void *data)
+{
+ if (is_typec_partner_altmode(dev)) {
+ struct typec_altmode *alt = to_typec_altmode(dev);
+ struct mode_order *order = (struct mode_order *)data;
+
+ if (order->svid == alt->svid) {
+ if (alt->ops && alt->ops->activate)
+ order->result = alt->ops->activate(alt, order->enter);
+ else
+ order->result = -EOPNOTSUPP;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+static int mode_selection_activate(struct mode_selection *sel,
+ const u16 svid, const int enter)
+
+ __must_hold(&sel->lock)
+{
+ struct mode_order order = {.svid = svid, .enter = enter, .result = -ENODEV};
+
+ /*
+ * The port driver may acquire its internal mutex during alternate mode
+ * activation. Since this is the same mutex that may be held during the
+ * execution of typec_altmode_state_update(), it is crucial to release
+ * sel->mutex before activation to avoid potential deadlock.
+ * Note that sel->mode_list must remain invariant throughout this unlocked
+ * interval.
+ */
+ mutex_unlock(&sel->lock);
+ device_for_each_child(&sel->partner->dev, &order, activate_altmode);
+ mutex_lock(&sel->lock);
+
+ return order.result;
+}
+
+static void mode_list_clean(struct mode_selection *sel)
+{
+ struct mode_state *ms, *tmp;
+
+ list_for_each_entry_safe(ms, tmp, &sel->mode_list, list) {
+ list_del(&ms->list);
+ kfree(ms);
+ }
+}
+
+/**
+ * mode_selection_work_fn() - Alternate mode activation task
+ * @work: work structure
+ *
+ * - If the Alternate Mode currently prioritized at the top of the list is already
+ * active, the entire selection process is considered finished.
+ * - If a different Alternate Mode is currently active, the system must exit that
+ * active mode first before attempting any new entry.
+ *
+ * The function then checks the result of the attempt to entre the current mode,
+ * stored in the `ms->error` field:
+ * - if the attempt FAILED, the mode is deactivated and removed from the list.
+ * - `ms->error` value of 0 signifies that the mode has not yet been activated.
+ *
+ * Once successfully activated, the task is scheduled for subsequent entry after
+ * a timeout period. The alternate mode driver is expected to call back with the
+ * actual mode entry result via `typec_altmode_state_update()`.
+ */
+static void mode_selection_work_fn(struct work_struct *work)
+{
+ struct mode_selection *sel = container_of(work,
+ struct mode_selection, work.work);
+ struct mode_state *ms;
+ unsigned int delay = sel->delay;
+ int result;
+
+ guard(mutex)(&sel->lock);
+
+ ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
+ if (!ms)
+ return;
+
+ if (sel->active_svid == ms->svid) {
+ dev_dbg(&sel->partner->dev, "%x altmode is active\n", ms->svid);
+ mode_list_clean(sel);
+ } else if (sel->active_svid != 0) {
+ result = mode_selection_activate(sel, sel->active_svid, 0);
+ if (result)
+ mode_list_clean(sel);
+ else
+ sel->active_svid = 0;
+ } else if (ms->error) {
+ dev_err(&sel->partner->dev, "%x: entry error %pe\n",
+ ms->svid, ERR_PTR(ms->error));
+ mode_selection_activate(sel, ms->svid, 0);
+ list_del(&ms->list);
+ kfree(ms);
+ } else {
+ result = mode_selection_activate(sel, ms->svid, 1);
+ if (result) {
+ dev_err(&sel->partner->dev, "%x: activation error %pe\n",
+ ms->svid, ERR_PTR(result));
+ list_del(&ms->list);
+ kfree(ms);
+ } else {
+ delay = sel->timeout;
+ ms->error = -ETIMEDOUT;
+ }
+ }
+
+ if (!list_empty(&sel->mode_list))
+ schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
+}
+
+void typec_altmode_state_update(struct typec_partner *partner, const u16 svid,
+ const int error)
+{
+ struct mode_selection *sel = partner->sel;
+ struct mode_state *ms;
+
+ if (sel) {
+ mutex_lock(&sel->lock);
+ ms = list_first_entry_or_null(&sel->mode_list, struct mode_state, list);
+ if (ms && ms->svid == svid) {
+ ms->error = error;
+ if (cancel_delayed_work(&sel->work))
+ schedule_delayed_work(&sel->work, 0);
+ }
+ if (!error)
+ sel->active_svid = svid;
+ else
+ sel->active_svid = 0;
+ mutex_unlock(&sel->lock);
+ }
+}
+EXPORT_SYMBOL_GPL(typec_altmode_state_update);
+
+static int compare_priorities(void *priv,
+ const struct list_head *a, const struct list_head *b)
+{
+ const struct mode_state *msa = container_of(a, struct mode_state, list);
+ const struct mode_state *msb = container_of(b, struct mode_state, list);
+
+ if (msa->priority < msb->priority)
+ return -1;
+ return 1;
+}
+
+static int altmode_add_to_list(struct device *dev, void *data)
+{
+ if (is_typec_partner_altmode(dev)) {
+ struct list_head *list = (struct list_head *)data;
+ struct typec_altmode *altmode = to_typec_altmode(dev);
+ const struct typec_altmode *pdev = typec_altmode_get_partner(altmode);
+ struct mode_state *ms;
+
+ if (pdev && altmode->ops && altmode->ops->activate) {
+ ms = kzalloc(sizeof(*ms), GFP_KERNEL);
+ if (!ms)
+ return -ENOMEM;
+ ms->svid = pdev->svid;
+ ms->priority = pdev->priority;
+ INIT_LIST_HEAD(&ms->list);
+ list_add_tail(&ms->list, list);
+ }
+ }
+ return 0;
+}
+
+int typec_mode_selection_start(struct typec_partner *partner,
+ const unsigned int delay, const unsigned int timeout)
+{
+ struct mode_selection *sel;
+ int ret;
+
+ if (partner->usb_mode == USB_MODE_USB4)
+ return -EBUSY;
+
+ if (partner->sel)
+ return -EALREADY;
+
+ sel = kzalloc(sizeof(*sel), GFP_KERNEL);
+ if (!sel)
+ return -ENOMEM;
+
+ INIT_LIST_HEAD(&sel->mode_list);
+
+ ret = device_for_each_child(&partner->dev, &sel->mode_list,
+ altmode_add_to_list);
+
+ if (ret || list_empty(&sel->mode_list)) {
+ mode_list_clean(sel);
+ kfree(sel);
+ return ret;
+ }
+
+ list_sort(NULL, &sel->mode_list, compare_priorities);
+ sel->partner = partner;
+ sel->delay = delay;
+ sel->timeout = timeout;
+ mutex_init(&sel->lock);
+ INIT_DELAYED_WORK(&sel->work, mode_selection_work_fn);
+ schedule_delayed_work(&sel->work, msecs_to_jiffies(delay));
+ partner->sel = sel;
+
+ return 0;
+}
+EXPORT_SYMBOL_GPL(typec_mode_selection_start);
+
+void typec_mode_selection_delete(struct typec_partner *partner)
+{
+ struct mode_selection *sel = partner->sel;
+
+ if (sel) {
+ partner->sel = NULL;
+ cancel_delayed_work_sync(&sel->work);
+ mode_list_clean(sel);
+ mutex_destroy(&sel->lock);
+ kfree(sel);
+ }
+}
+EXPORT_SYMBOL_GPL(typec_mode_selection_delete);
diff --git a/drivers/usb/typec/tcpm/tcpm.c b/drivers/usb/typec/tcpm/tcpm.c
index be49a976428f..b7828160b81d 100644
--- a/drivers/usb/typec/tcpm/tcpm.c
+++ b/drivers/usb/typec/tcpm/tcpm.c
@@ -1808,7 +1808,7 @@ static bool svdm_consume_svids(struct tcpm_port *port, const u32 *p, int cnt,
/*
* PD3.0 Spec 6.4.4.3.2: The SVIDs are returned 2 per VDO (see Table
* 6-43), and can be returned maximum 6 VDOs per response (see Figure
- * 6-19). If the Respondersupports 12 or more SVID then the Discover
+ * 6-19). If the Responder supports 12 or more SVID then the Discover
* SVIDs Command Shall be executed multiple times until a Discover
* SVIDs VDO is returned ending either with a SVID value of 0x0000 in
* the last part of the last VDO or with a VDO containing two SVIDs
diff --git a/drivers/usb/typec/ucsi/Kconfig b/drivers/usb/typec/ucsi/Kconfig
index b812be4d0e67..87dd992a4b9e 100644
--- a/drivers/usb/typec/ucsi/Kconfig
+++ b/drivers/usb/typec/ucsi/Kconfig
@@ -73,7 +73,6 @@ config CROS_EC_UCSI
tristate "UCSI Driver for ChromeOS EC"
depends on MFD_CROS_EC_DEV
depends on CROS_USBPD_NOTIFY
- depends on !EXTCON_TCSS_CROS_EC
default MFD_CROS_EC_DEV
help
This driver enables UCSI support for a ChromeOS EC. The EC is
diff --git a/drivers/usb/typec/ucsi/Makefile b/drivers/usb/typec/ucsi/Makefile
index dbc571763eff..c7e38bf01350 100644
--- a/drivers/usb/typec/ucsi/Makefile
+++ b/drivers/usb/typec/ucsi/Makefile
@@ -17,6 +17,10 @@ ifneq ($(CONFIG_TYPEC_DP_ALTMODE),)
typec_ucsi-y += displayport.o
endif
+ifneq ($(CONFIG_TYPEC_TBT_ALTMODE),)
+ typec_ucsi-y += thunderbolt.o
+endif
+
obj-$(CONFIG_UCSI_ACPI) += ucsi_acpi.o
obj-$(CONFIG_UCSI_CCG) += ucsi_ccg.o
obj-$(CONFIG_UCSI_STM32G0) += ucsi_stm32g0.o
diff --git a/drivers/usb/typec/ucsi/cros_ec_ucsi.c b/drivers/usb/typec/ucsi/cros_ec_ucsi.c
index eed2a7d0ebc6..6bca2dce211c 100644
--- a/drivers/usb/typec/ucsi/cros_ec_ucsi.c
+++ b/drivers/usb/typec/ucsi/cros_ec_ucsi.c
@@ -16,6 +16,7 @@
#include <linux/platform_device.h>
#include <linux/slab.h>
#include <linux/wait.h>
+#include <linux/usb/typec_altmode.h>
#include "ucsi.h"
@@ -33,6 +34,11 @@
/* Number of times to attempt recovery from a write timeout before giving up. */
#define WRITE_TMO_CTR_MAX 5
+/* Delay between mode entry/exit attempts, ms */
+static const unsigned int mode_selection_delay = 1000;
+/* Timeout for a mode entry attempt, ms */
+static const unsigned int mode_selection_timeout = 4000;
+
struct cros_ucsi_data {
struct device *dev;
struct ucsi *ucsi;
@@ -134,6 +140,20 @@ static int cros_ucsi_sync_control(struct ucsi *ucsi, u64 cmd, u32 *cci,
return ret;
}
+static void cros_ucsi_add_partner_altmodes(struct ucsi_connector *con)
+{
+ if (!con->typec_cap.no_mode_control)
+ typec_mode_selection_start(con->partner,
+ mode_selection_delay,
+ mode_selection_timeout);
+}
+
+static void cros_ucsi_remove_partner_altmodes(struct ucsi_connector *con)
+{
+ if (!con->typec_cap.no_mode_control)
+ typec_mode_selection_delete(con->partner);
+}
+
static const struct ucsi_operations cros_ucsi_ops = {
.read_version = cros_ucsi_read_version,
.read_cci = cros_ucsi_read_cci,
@@ -141,6 +161,8 @@ static const struct ucsi_operations cros_ucsi_ops = {
.read_message_in = cros_ucsi_read_message_in,
.async_control = cros_ucsi_async_control,
.sync_control = cros_ucsi_sync_control,
+ .add_partner_altmodes = cros_ucsi_add_partner_altmodes,
+ .remove_partner_altmodes = cros_ucsi_remove_partner_altmodes,
};
static void cros_ucsi_work(struct work_struct *work)
diff --git a/drivers/usb/typec/ucsi/psy.c b/drivers/usb/typec/ucsi/psy.c
index 3abe9370ffaa..c8ebab8ba7e7 100644
--- a/drivers/usb/typec/ucsi/psy.c
+++ b/drivers/usb/typec/ucsi/psy.c
@@ -112,15 +112,20 @@ static int ucsi_psy_get_voltage_max(struct ucsi_connector *con,
union power_supply_propval *val)
{
u32 pdo;
+ int max_voltage = 0;
switch (UCSI_CONSTAT(con, PWR_OPMODE)) {
case UCSI_CONSTAT_PWR_OPMODE_PD:
- if (con->num_pdos > 0) {
- pdo = con->src_pdos[con->num_pdos - 1];
- val->intval = pdo_fixed_voltage(pdo) * 1000;
- } else {
- val->intval = 0;
+ for (int i = 0; i < con->num_pdos; i++) {
+ int pdo_voltage = 0;
+
+ pdo = con->src_pdos[i];
+ if (pdo_type(pdo) == PDO_TYPE_FIXED)
+ pdo_voltage = pdo_fixed_voltage(pdo) * 1000;
+ max_voltage = (pdo_voltage > max_voltage) ? pdo_voltage
+ : max_voltage;
}
+ val->intval = max_voltage;
break;
case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
@@ -168,6 +173,7 @@ static int ucsi_psy_get_current_max(struct ucsi_connector *con,
union power_supply_propval *val)
{
u32 pdo;
+ int max_current = 0;
if (!UCSI_CONSTAT(con, CONNECTED)) {
val->intval = 0;
@@ -176,12 +182,16 @@ static int ucsi_psy_get_current_max(struct ucsi_connector *con,
switch (UCSI_CONSTAT(con, PWR_OPMODE)) {
case UCSI_CONSTAT_PWR_OPMODE_PD:
- if (con->num_pdos > 0) {
- pdo = con->src_pdos[con->num_pdos - 1];
- val->intval = pdo_max_current(pdo) * 1000;
- } else {
- val->intval = 0;
+ for (int i = 0; i < con->num_pdos; i++) {
+ int pdo_current = 0;
+
+ pdo = con->src_pdos[i];
+ if (pdo_type(pdo) == PDO_TYPE_FIXED)
+ pdo_current = pdo_max_current(pdo) * 1000;
+ max_current = (pdo_current > max_current) ? pdo_current
+ : max_current;
}
+ val->intval = max_current;
break;
case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
val->intval = UCSI_TYPEC_1_5_CURRENT * 1000;
@@ -202,10 +212,28 @@ static int ucsi_psy_get_current_max(struct ucsi_connector *con,
static int ucsi_psy_get_current_now(struct ucsi_connector *con,
union power_supply_propval *val)
{
- if (UCSI_CONSTAT(con, PWR_OPMODE) == UCSI_CONSTAT_PWR_OPMODE_PD)
- val->intval = rdo_op_current(con->rdo) * 1000;
- else
+ if (!UCSI_CONSTAT(con, CONNECTED)) {
val->intval = 0;
+ return 0;
+ }
+
+ switch (UCSI_CONSTAT(con, PWR_OPMODE)) {
+ case UCSI_CONSTAT_PWR_OPMODE_PD:
+ val->intval = rdo_op_current(con->rdo) * 1000;
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC1_5:
+ val->intval = UCSI_TYPEC_1_5_CURRENT * 1000;
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_TYPEC3_0:
+ val->intval = UCSI_TYPEC_3_0_CURRENT * 1000;
+ break;
+ case UCSI_CONSTAT_PWR_OPMODE_BC:
+ case UCSI_CONSTAT_PWR_OPMODE_DEFAULT:
+ /* UCSI can't tell b/w DCP/CDP or USB2/3x1/3x2 SDP chargers */
+ default:
+ val->intval = UCSI_TYPEC_DEFAULT_CURRENT * 1000;
+ break;
+ }
return 0;
}
diff --git a/drivers/usb/typec/ucsi/thunderbolt.c b/drivers/usb/typec/ucsi/thunderbolt.c
new file mode 100644
index 000000000000..434d3d8ea5b6
--- /dev/null
+++ b/drivers/usb/typec/ucsi/thunderbolt.c
@@ -0,0 +1,212 @@
+// 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);
+ }
+}
diff --git a/drivers/usb/typec/ucsi/ucsi.c b/drivers/usb/typec/ucsi/ucsi.c
index a7b388dc7fa0..91b6c71dd739 100644
--- a/drivers/usb/typec/ucsi/ucsi.c
+++ b/drivers/usb/typec/ucsi/ucsi.c
@@ -13,6 +13,7 @@
#include <linux/delay.h>
#include <linux/slab.h>
#include <linux/usb/typec_dp.h>
+#include <linux/usb/typec_tbt.h>
#include "ucsi.h"
#include "trace.h"
@@ -314,6 +315,7 @@ void ucsi_altmode_update_active(struct ucsi_connector *con)
{
const struct typec_altmode *altmode = NULL;
u64 command;
+ u16 svid = 0;
int ret;
u8 cur;
int i;
@@ -335,6 +337,10 @@ void ucsi_altmode_update_active(struct ucsi_connector *con)
for (i = 0; con->partner_altmode[i]; i++)
typec_altmode_update_active(con->partner_altmode[i],
con->partner_altmode[i] == altmode);
+
+ if (altmode)
+ svid = altmode->svid;
+ typec_altmode_state_update(con->partner, svid, 0);
}
static int ucsi_altmode_next_mode(struct typec_altmode **alt, u16 svid)
@@ -412,6 +418,9 @@ static int ucsi_register_altmode(struct ucsi_connector *con,
alt = ucsi_register_displayport(con, override,
i, desc);
break;
+ case USB_TYPEC_TBT_SID:
+ alt = ucsi_register_thunderbolt(con, override, i, desc);
+ break;
default:
alt = typec_port_register_altmode(con->port, desc);
break;
@@ -609,6 +618,8 @@ static int ucsi_register_altmodes(struct ucsi_connector *con, u8 recipient)
desc.vdo = alt[j].mid;
desc.svid = alt[j].svid;
desc.roles = TYPEC_PORT_DRD;
+ desc.mode_selection = con->ucsi->ops->add_partner_altmodes &&
+ !con->typec_cap.no_mode_control;
ret = ucsi_register_altmode(con, &desc, recipient);
if (ret)
@@ -640,12 +651,15 @@ static void ucsi_unregister_altmodes(struct ucsi_connector *con, u8 recipient)
}
while (adev[i]) {
- if (recipient == UCSI_RECIPIENT_SOP &&
- (adev[i]->svid == USB_TYPEC_DP_SID ||
- (adev[i]->svid == USB_TYPEC_NVIDIA_VLINK_SID &&
- adev[i]->vdo != USB_TYPEC_NVIDIA_VLINK_DBG_VDO))) {
+ if (recipient == UCSI_RECIPIENT_SOP) {
pdev = typec_altmode_get_partner(adev[i]);
- ucsi_displayport_remove_partner((void *)pdev);
+
+ if (adev[i]->svid == USB_TYPEC_DP_SID ||
+ (adev[i]->svid == USB_TYPEC_NVIDIA_VLINK_SID &&
+ adev[i]->vdo != USB_TYPEC_NVIDIA_VLINK_DBG_VDO))
+ ucsi_displayport_remove_partner((void *)pdev);
+ else if (adev[i]->svid == USB_TYPEC_TBT_SID)
+ ucsi_thunderbolt_remove_partner((void *)pdev);
}
typec_unregister_altmode(adev[i]);
adev[i++] = NULL;
@@ -831,6 +845,8 @@ static int ucsi_check_altmodes(struct ucsi_connector *con)
if (con->partner_altmode[0]) {
num_partner_am = ucsi_get_num_altmode(con->partner_altmode);
typec_partner_set_num_altmodes(con->partner, num_partner_am);
+ if (con->ucsi->ops->add_partner_altmodes)
+ con->ucsi->ops->add_partner_altmodes(con);
ucsi_altmode_update_active(con);
return 0;
} else {
@@ -1119,6 +1135,8 @@ static void ucsi_unregister_partner(struct ucsi_connector *con)
return;
typec_set_mode(con->port, TYPEC_STATE_SAFE);
+ if (con->ucsi->ops->remove_partner_altmodes)
+ con->ucsi->ops->remove_partner_altmodes(con);
typec_partner_set_usb_power_delivery(con->partner, NULL);
ucsi_unregister_partner_pdos(con);
@@ -1307,6 +1325,7 @@ static void ucsi_handle_connector_change(struct work_struct *work)
if (con->partner && (change & UCSI_CONSTAT_PARTNER_CHANGE)) {
ucsi_partner_change(con);
+ ucsi_altmode_update_active(con);
/* Complete pending data role swap */
if (!completion_done(&con->complete))
@@ -1659,6 +1678,7 @@ static int ucsi_register_port(struct ucsi *ucsi, struct ucsi_connector *con)
cap->driver_data = con;
cap->ops = &ucsi_ops;
+ cap->no_mode_control = !(con->ucsi->cap.features & UCSI_CAP_ALT_MODE_OVERRIDE);
if (ucsi->version >= UCSI_VERSION_2_0)
con->typec_cap.orientation_aware = true;
diff --git a/drivers/usb/typec/ucsi/ucsi.h b/drivers/usb/typec/ucsi/ucsi.h
index 410389ef173a..43a0d01ade8f 100644
--- a/drivers/usb/typec/ucsi/ucsi.h
+++ b/drivers/usb/typec/ucsi/ucsi.h
@@ -70,6 +70,8 @@ struct dentry;
* @update_altmodes: Squashes duplicate DP altmodes
* @update_connector: Update connector capabilities before registering
* @connector_status: Updates connector status, called holding connector lock
+ * @add_partner_altmodes: Start mode selection
+ * @remove_partner_altmodes: Clean mode selection
*
* Read and write routines for UCSI interface. @sync_write must wait for the
* Command Completion Event from the PPM before returning, and @async_write must
@@ -88,6 +90,8 @@ struct ucsi_operations {
struct ucsi_altmode *updated);
void (*update_connector)(struct ucsi_connector *con);
void (*connector_status)(struct ucsi_connector *con);
+ void (*add_partner_altmodes)(struct ucsi_connector *con);
+ void (*remove_partner_altmodes)(struct ucsi_connector *con);
};
struct ucsi *ucsi_create(struct device *dev, const struct ucsi_operations *ops);
@@ -596,6 +600,26 @@ static inline void
ucsi_displayport_remove_partner(struct typec_altmode *adev) { }
#endif /* CONFIG_TYPEC_DP_ALTMODE */
+#if IS_ENABLED(CONFIG_TYPEC_TBT_ALTMODE)
+struct typec_altmode *
+ucsi_register_thunderbolt(struct ucsi_connector *con,
+ bool override, int offset,
+ struct typec_altmode_desc *desc);
+
+void ucsi_thunderbolt_remove_partner(struct typec_altmode *adev);
+#else
+static inline struct typec_altmode *
+ucsi_register_thunderbolt(struct ucsi_connector *con,
+ bool override, int offset,
+ struct typec_altmode_desc *desc)
+{
+ return typec_port_register_altmode(con->port, desc);
+}
+
+static inline void
+ucsi_thunderbolt_remove_partner(struct typec_altmode *adev) { }
+#endif /* CONFIG_TYPEC_TBT_ALTMODE */
+
#ifdef CONFIG_DEBUG_FS
void ucsi_debugfs_init(void);
void ucsi_debugfs_exit(void);