1// SPDX-License-Identifier: GPL-2.0-only
2/*
3 * Alt-mode implementation on ChromeOS EC.
4 *
5 * Copyright 2024 Google LLC
6 * Author: Abhishek Pandit-Subedi <abhishekpandit@chromium.org>
7 */
8#include "cros_ec_typec.h"
9
10#include <linux/mutex.h>
11#include <linux/workqueue.h>
12#include <linux/usb/typec_dp.h>
13#include <linux/usb/typec_tbt.h>
14#include <linux/usb/pd_vdo.h>
15
16#include "cros_typec_altmode.h"
17
18struct cros_typec_altmode_data {
19 struct work_struct work;
20 struct cros_typec_port *port;
21 struct typec_altmode *alt;
22 bool ap_mode_entry;
23
24 struct mutex lock;
25 u32 header;
26 u32 *vdo_data;
27 u8 vdo_size;
28
29 u16 sid;
30 u8 mode;
31};
32
33struct cros_typec_dp_data {
34 struct cros_typec_altmode_data adata;
35 struct typec_displayport_data data;
36 bool configured;
37 bool pending_status_update;
38};
39
40static void cros_typec_altmode_work(struct work_struct *work)
41{
42 struct cros_typec_altmode_data *data =
43 container_of(work, struct cros_typec_altmode_data, work);
44
45 mutex_lock(&data->lock);
46
47 if (typec_altmode_vdm(altmode: data->alt, header: data->header, vdo: data->vdo_data,
48 count: data->vdo_size))
49 dev_err(&data->alt->dev, "VDM 0x%x failed\n", data->header);
50
51 data->header = 0;
52 data->vdo_data = NULL;
53 data->vdo_size = 0;
54
55 mutex_unlock(lock: &data->lock);
56}
57
58static int cros_typec_altmode_enter(struct typec_altmode *alt, u32 *vdo)
59{
60 struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(altmode: alt);
61 struct ec_params_typec_control req = {
62 .port = adata->port->port_num,
63 .command = TYPEC_CONTROL_COMMAND_ENTER_MODE,
64 };
65 int svdm_version;
66 int ret;
67
68 if (!adata->ap_mode_entry) {
69 dev_warn(&alt->dev,
70 "EC does not support AP driven mode entry\n");
71 return -EOPNOTSUPP;
72 }
73
74 if (adata->sid == USB_TYPEC_DP_SID)
75 req.mode_to_enter = CROS_EC_ALTMODE_DP;
76 else if (adata->sid == USB_TYPEC_TBT_SID)
77 req.mode_to_enter = CROS_EC_ALTMODE_TBT;
78 else
79 return -EOPNOTSUPP;
80
81 ret = cros_ec_cmd(ec_dev: adata->port->typec_data->ec, version: 0, EC_CMD_TYPEC_CONTROL,
82 outdata: &req, outsize: sizeof(req), NULL, insize: 0);
83 if (ret < 0)
84 return ret;
85
86 svdm_version = typec_altmode_get_svdm_version(altmode: alt);
87 if (svdm_version < 0)
88 return svdm_version;
89
90 mutex_lock(&adata->lock);
91
92 adata->header = VDO(adata->sid, 1, svdm_version, CMD_ENTER_MODE);
93 adata->header |= VDO_OPOS(adata->mode);
94 adata->header |= VDO_CMDT(CMDT_RSP_ACK);
95 adata->vdo_data = NULL;
96 adata->vdo_size = 1;
97 schedule_work(work: &adata->work);
98
99 mutex_unlock(lock: &adata->lock);
100 return ret;
101}
102
103static int cros_typec_altmode_exit(struct typec_altmode *alt)
104{
105 struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(altmode: alt);
106 struct ec_params_typec_control req = {
107 .port = adata->port->port_num,
108 .command = TYPEC_CONTROL_COMMAND_EXIT_MODES,
109 };
110 int svdm_version;
111 int ret;
112
113 if (!adata->ap_mode_entry) {
114 dev_warn(&alt->dev,
115 "EC does not support AP driven mode exit\n");
116 return -EOPNOTSUPP;
117 }
118
119 ret = cros_ec_cmd(ec_dev: adata->port->typec_data->ec, version: 0, EC_CMD_TYPEC_CONTROL,
120 outdata: &req, outsize: sizeof(req), NULL, insize: 0);
121
122 if (ret < 0)
123 return ret;
124
125 svdm_version = typec_altmode_get_svdm_version(altmode: alt);
126 if (svdm_version < 0)
127 return svdm_version;
128
129 mutex_lock(&adata->lock);
130
131 adata->header = VDO(adata->sid, 1, svdm_version, CMD_EXIT_MODE);
132 adata->header |= VDO_OPOS(adata->mode);
133 adata->header |= VDO_CMDT(CMDT_RSP_ACK);
134 adata->vdo_data = NULL;
135 adata->vdo_size = 1;
136 schedule_work(work: &adata->work);
137
138 mutex_unlock(lock: &adata->lock);
139 return ret;
140}
141
142static int cros_typec_displayport_vdm(struct typec_altmode *alt, u32 header,
143 const u32 *data, int count)
144{
145 struct cros_typec_dp_data *dp_data = typec_altmode_get_drvdata(altmode: alt);
146 struct cros_typec_altmode_data *adata = &dp_data->adata;
147
148
149 int cmd_type = PD_VDO_CMDT(header);
150 int cmd = PD_VDO_CMD(header);
151 int svdm_version;
152
153 svdm_version = typec_altmode_get_svdm_version(altmode: alt);
154 if (svdm_version < 0)
155 return svdm_version;
156
157 mutex_lock(&adata->lock);
158
159 switch (cmd_type) {
160 case CMDT_INIT:
161 if (PD_VDO_SVDM_VER(header) < svdm_version) {
162 typec_partner_set_svdm_version(partner: adata->port->partner,
163 PD_VDO_SVDM_VER(header));
164 svdm_version = PD_VDO_SVDM_VER(header);
165 }
166
167 adata->header = VDO(adata->sid, 1, svdm_version, cmd);
168 adata->header |= VDO_OPOS(adata->mode);
169
170 /*
171 * DP_CMD_CONFIGURE: We can't actually do anything with the
172 * provided VDO yet so just send back an ACK.
173 *
174 * DP_CMD_STATUS_UPDATE: We wait for Mux changes to send
175 * DPStatus Acks.
176 */
177 switch (cmd) {
178 case DP_CMD_CONFIGURE:
179 dp_data->data.conf = *data;
180 adata->header |= VDO_CMDT(CMDT_RSP_ACK);
181 dp_data->configured = true;
182 schedule_work(work: &adata->work);
183 break;
184 case DP_CMD_STATUS_UPDATE:
185 dp_data->pending_status_update = true;
186 break;
187 default:
188 adata->header |= VDO_CMDT(CMDT_RSP_ACK);
189 schedule_work(work: &adata->work);
190 break;
191 }
192
193 break;
194 default:
195 break;
196 }
197
198 mutex_unlock(lock: &adata->lock);
199 return 0;
200}
201
202static int cros_typec_thunderbolt_vdm(struct typec_altmode *alt, u32 header,
203 const u32 *data, int count)
204{
205 struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(altmode: alt);
206
207 int cmd_type = PD_VDO_CMDT(header);
208 int cmd = PD_VDO_CMD(header);
209 int svdm_version;
210
211 svdm_version = typec_altmode_get_svdm_version(altmode: alt);
212 if (svdm_version < 0)
213 return svdm_version;
214
215 mutex_lock(&adata->lock);
216
217 switch (cmd_type) {
218 case CMDT_INIT:
219 if (PD_VDO_SVDM_VER(header) < svdm_version) {
220 typec_partner_set_svdm_version(partner: adata->port->partner,
221 PD_VDO_SVDM_VER(header));
222 svdm_version = PD_VDO_SVDM_VER(header);
223 }
224
225 adata->header = VDO(adata->sid, 1, svdm_version, cmd);
226 adata->header |= VDO_OPOS(adata->mode);
227
228 switch (cmd) {
229 case CMD_ENTER_MODE:
230 /* Don't respond to the enter mode vdm because it
231 * triggers mux configuration. This is handled directly
232 * by the cros_ec_typec driver so the Thunderbolt driver
233 * doesn't need to be involved.
234 */
235 break;
236 default:
237 adata->header |= VDO_CMDT(CMDT_RSP_ACK);
238 schedule_work(work: &adata->work);
239 break;
240 }
241
242 break;
243 default:
244 break;
245 }
246
247 mutex_unlock(lock: &adata->lock);
248 return 0;
249}
250
251
252static int cros_typec_altmode_vdm(struct typec_altmode *alt, u32 header,
253 const u32 *data, int count)
254{
255 struct cros_typec_altmode_data *adata = typec_altmode_get_drvdata(altmode: alt);
256
257 if (!adata->ap_mode_entry)
258 return -EOPNOTSUPP;
259
260 if (adata->sid == USB_TYPEC_DP_SID)
261 return cros_typec_displayport_vdm(alt, header, data, count);
262
263 if (adata->sid == USB_TYPEC_TBT_SID)
264 return cros_typec_thunderbolt_vdm(alt, header, data, count);
265
266 return -EINVAL;
267}
268
269static const struct typec_altmode_ops cros_typec_altmode_ops = {
270 .enter = cros_typec_altmode_enter,
271 .exit = cros_typec_altmode_exit,
272 .vdm = cros_typec_altmode_vdm,
273};
274
275#if IS_ENABLED(CONFIG_TYPEC_DP_ALTMODE)
276int cros_typec_displayport_status_update(struct typec_altmode *altmode,
277 struct typec_displayport_data *data)
278{
279 struct cros_typec_dp_data *dp_data =
280 typec_altmode_get_drvdata(altmode);
281 struct cros_typec_altmode_data *adata = &dp_data->adata;
282
283 if (!dp_data->pending_status_update) {
284 dev_dbg(&altmode->dev,
285 "Got DPStatus without a pending request\n");
286 return 0;
287 }
288
289 if (dp_data->configured && dp_data->data.conf != data->conf)
290 dev_dbg(&altmode->dev,
291 "DP Conf doesn't match. Requested 0x%04x, Actual 0x%04x\n",
292 dp_data->data.conf, data->conf);
293
294 mutex_lock(&adata->lock);
295
296 dp_data->data = *data;
297 dp_data->pending_status_update = false;
298 adata->header |= VDO_CMDT(CMDT_RSP_ACK);
299 adata->vdo_data = &dp_data->data.status;
300 adata->vdo_size = 2;
301 schedule_work(work: &adata->work);
302
303 mutex_unlock(lock: &adata->lock);
304
305 return 0;
306}
307
308struct typec_altmode *
309cros_typec_register_displayport(struct cros_typec_port *port,
310 struct typec_altmode_desc *desc,
311 bool ap_mode_entry)
312{
313 struct typec_altmode *alt;
314 struct cros_typec_dp_data *dp_data;
315 struct cros_typec_altmode_data *adata;
316
317 alt = typec_port_register_altmode(port: port->port, desc);
318 if (IS_ERR(ptr: alt))
319 return alt;
320
321 dp_data = devm_kzalloc(dev: &alt->dev, size: sizeof(*dp_data), GFP_KERNEL);
322 if (!dp_data) {
323 typec_unregister_altmode(altmode: alt);
324 return ERR_PTR(error: -ENOMEM);
325 }
326
327 adata = &dp_data->adata;
328 INIT_WORK(&adata->work, cros_typec_altmode_work);
329 mutex_init(&adata->lock);
330 adata->alt = alt;
331 adata->port = port;
332 adata->ap_mode_entry = ap_mode_entry;
333 adata->sid = desc->svid;
334 adata->mode = desc->mode;
335
336 typec_altmode_set_ops(alt, ops: &cros_typec_altmode_ops);
337 typec_altmode_set_drvdata(altmode: alt, data: adata);
338
339 return alt;
340}
341#endif
342
343#if IS_ENABLED(CONFIG_TYPEC_TBT_ALTMODE)
344struct typec_altmode *
345cros_typec_register_thunderbolt(struct cros_typec_port *port,
346 struct typec_altmode_desc *desc)
347{
348 struct typec_altmode *alt;
349 struct cros_typec_altmode_data *adata;
350
351 alt = typec_port_register_altmode(port: port->port, desc);
352 if (IS_ERR(ptr: alt))
353 return alt;
354
355 adata = devm_kzalloc(dev: &alt->dev, size: sizeof(*adata), GFP_KERNEL);
356 if (!adata) {
357 typec_unregister_altmode(altmode: alt);
358 return ERR_PTR(error: -ENOMEM);
359 }
360
361 INIT_WORK(&adata->work, cros_typec_altmode_work);
362 adata->alt = alt;
363 adata->port = port;
364 adata->ap_mode_entry = true;
365 adata->sid = desc->svid;
366 adata->mode = desc->mode;
367
368 typec_altmode_set_ops(alt, ops: &cros_typec_altmode_ops);
369 typec_altmode_set_drvdata(altmode: alt, data: adata);
370
371 return alt;
372}
373#endif
374

source code of linux/drivers/platform/chrome/cros_typec_altmode.c