1 // SPDX-License-Identifier: GPL-2.0
2 /*
3 * UCSI Thunderbolt Alternate Mode Support
4 *
5 * Copyright 2026 Google LLC
6 */
7
8 #include <linux/usb/typec_tbt.h>
9 #include <linux/usb/pd_vdo.h>
10 #include <linux/err.h>
11 #include <linux/dev_printk.h>
12 #include <linux/device/devres.h>
13 #include <linux/gfp_types.h>
14 #include <linux/types.h>
15 #include <linux/usb/typec_altmode.h>
16 #include <linux/workqueue.h>
17
18 #include "ucsi.h"
19
20 /**
21 * struct ucsi_tbt - Thunderbolt Alternate Mode private data structure
22 * @con: Pointer to UCSI connector structure
23 * @alt: Pointer to typec altmode structure
24 * @work: Work structure
25 * @cam: An offset into the list of alternate modes supported by the PPM
26 * @header: VDO header
27 */
28 struct ucsi_tbt {
29 struct ucsi_connector *con;
30 struct typec_altmode *alt;
31 struct work_struct work;
32 int cam;
33 u32 header;
34 };
35
ucsi_thunderbolt_work(struct work_struct * work)36 static void ucsi_thunderbolt_work(struct work_struct *work)
37 {
38 struct ucsi_tbt *tbt = container_of(work, struct ucsi_tbt, work);
39
40 if (typec_altmode_vdm(tbt->alt, tbt->header, NULL, 0))
41 dev_err(&tbt->alt->dev, "VDM 0x%x failed\n", tbt->header);
42
43 tbt->header = 0;
44 }
45
ucsi_thunderbolt_set_altmode(struct ucsi_tbt * tbt,bool enter,u32 vdo)46 static int ucsi_thunderbolt_set_altmode(struct ucsi_tbt *tbt,
47 bool enter, u32 vdo)
48 {
49 int svdm_version;
50 int cmd;
51 int ret;
52 u64 command = UCSI_SET_NEW_CAM |
53 UCSI_CONNECTOR_NUMBER(tbt->con->num) |
54 UCSI_SET_NEW_CAM_SET_AM(tbt->cam) |
55 ((u64)vdo << 32);
56
57 if (enter)
58 command |= (1 << 23);
59
60 ret = ucsi_send_command(tbt->con->ucsi, command, NULL, 0);
61 if (ret < 0)
62 return ret;
63
64 svdm_version = typec_altmode_get_svdm_version(tbt->alt);
65 if (svdm_version < 0)
66 return svdm_version;
67
68 if (enter)
69 cmd = CMD_ENTER_MODE;
70 else
71 cmd = CMD_EXIT_MODE;
72 tbt->header = VDO(USB_TYPEC_TBT_SID, 1, svdm_version, cmd);
73 tbt->header |= VDO_OPOS(TYPEC_TBT_MODE);
74 tbt->header |= VDO_CMDT(CMDT_RSP_ACK);
75
76 schedule_work(&tbt->work);
77
78 return 0;
79 }
80
ucsi_thunderbolt_enter(struct typec_altmode * alt,u32 * vdo)81 static int ucsi_thunderbolt_enter(struct typec_altmode *alt, u32 *vdo)
82 {
83 struct ucsi_tbt *tbt = typec_altmode_get_drvdata(alt);
84 struct ucsi_connector *con = tbt->con;
85 u64 command;
86 u8 cur = 0;
87 int ret;
88
89 if (!ucsi_con_mutex_lock(con))
90 return -ENOTCONN;
91
92 command = UCSI_GET_CURRENT_CAM | UCSI_CONNECTOR_NUMBER(con->num);
93 ret = ucsi_send_command(con->ucsi, command, &cur, sizeof(cur));
94 if (ret < 0) {
95 if (con->ucsi->version > 0x0100)
96 goto err_unlock;
97 cur = 0xff;
98 }
99
100 if (cur != 0xff) {
101 if (cur >= UCSI_MAX_ALTMODES || con->port_altmode[cur] != alt)
102 ret = -EBUSY;
103 else
104 ret = 0;
105 goto err_unlock;
106 }
107
108 ret = ucsi_thunderbolt_set_altmode(tbt, true, *vdo);
109 ucsi_altmode_update_active(tbt->con);
110
111 err_unlock:
112 ucsi_con_mutex_unlock(con);
113
114 return ret;
115 }
116
ucsi_thunderbolt_exit(struct typec_altmode * alt)117 static int ucsi_thunderbolt_exit(struct typec_altmode *alt)
118 {
119 struct ucsi_tbt *tbt = typec_altmode_get_drvdata(alt);
120 int ret;
121
122 if (!ucsi_con_mutex_lock(tbt->con))
123 return -ENOTCONN;
124
125 ret = ucsi_thunderbolt_set_altmode(tbt, false, 0);
126
127 ucsi_con_mutex_unlock(tbt->con);
128
129 return ret;
130 }
131
ucsi_thunderbolt_vdm(struct typec_altmode * alt,u32 header,const u32 * data,int count)132 static int ucsi_thunderbolt_vdm(struct typec_altmode *alt,
133 u32 header, const u32 *data, int count)
134 {
135 struct ucsi_tbt *tbt = typec_altmode_get_drvdata(alt);
136 int cmd_type = PD_VDO_CMDT(header);
137 int cmd = PD_VDO_CMD(header);
138 int svdm_version;
139
140 if (!ucsi_con_mutex_lock(tbt->con))
141 return -ENOTCONN;
142
143 svdm_version = typec_altmode_get_svdm_version(alt);
144 if (svdm_version < 0) {
145 ucsi_con_mutex_unlock(tbt->con);
146 return svdm_version;
147 }
148
149 switch (cmd_type) {
150 case CMDT_INIT:
151 if (PD_VDO_SVDM_VER(header) < svdm_version) {
152 svdm_version = PD_VDO_SVDM_VER(header);
153 typec_partner_set_svdm_version(tbt->con->partner, svdm_version);
154 }
155 tbt->header = VDO(USB_TYPEC_TBT_SID, 1, svdm_version, cmd);
156 tbt->header |= VDO_OPOS(TYPEC_TBT_MODE);
157 tbt->header |= VDO_CMDT(CMDT_RSP_ACK);
158
159 schedule_work(&tbt->work);
160 break;
161 default:
162 break;
163 }
164
165 ucsi_con_mutex_unlock(tbt->con);
166
167 return 0;
168 }
169
170 static const struct typec_altmode_ops ucsi_thunderbolt_ops = {
171 .enter = ucsi_thunderbolt_enter,
172 .exit = ucsi_thunderbolt_exit,
173 .vdm = ucsi_thunderbolt_vdm,
174 };
175
ucsi_register_thunderbolt(struct ucsi_connector * con,bool override,int offset,struct typec_altmode_desc * desc)176 struct typec_altmode *ucsi_register_thunderbolt(struct ucsi_connector *con,
177 bool override, int offset,
178 struct typec_altmode_desc *desc)
179 {
180 struct typec_altmode *alt;
181 struct ucsi_tbt *tbt;
182
183 alt = typec_port_register_altmode(con->port, desc);
184 if (IS_ERR(alt) || !override)
185 return alt;
186
187 tbt = devm_kzalloc(&alt->dev, sizeof(*tbt), GFP_KERNEL);
188 if (!tbt) {
189 typec_unregister_altmode(alt);
190 return ERR_PTR(-ENOMEM);
191 }
192
193 tbt->cam = offset;
194 tbt->con = con;
195 tbt->alt = alt;
196 INIT_WORK(&tbt->work, ucsi_thunderbolt_work);
197 typec_altmode_set_drvdata(alt, tbt);
198 typec_altmode_set_ops(alt, &ucsi_thunderbolt_ops);
199
200 return alt;
201 }
202
ucsi_thunderbolt_remove_partner(struct typec_altmode * alt)203 void ucsi_thunderbolt_remove_partner(struct typec_altmode *alt)
204 {
205 struct ucsi_tbt *tbt;
206
207 if (alt) {
208 tbt = typec_altmode_get_drvdata(alt);
209 if (tbt)
210 cancel_work_sync(&tbt->work);
211 }
212 }
213