1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * UCSI DisplayPort Alternate Mode Support |
4 | * |
5 | * Copyright (C) 2018, Intel Corporation |
6 | * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com> |
7 | */ |
8 | |
9 | #include <linux/usb/typec_dp.h> |
10 | #include <linux/usb/pd_vdo.h> |
11 | |
12 | #include "ucsi.h" |
13 | |
14 | #define UCSI_CMD_SET_NEW_CAM(_con_num_, _enter_, _cam_, _am_) \ |
15 | (UCSI_SET_NEW_CAM | ((_con_num_) << 16) | ((_enter_) << 23) | \ |
16 | ((_cam_) << 24) | ((u64)(_am_) << 32)) |
17 | |
18 | struct ucsi_dp { |
19 | struct typec_displayport_data data; |
20 | struct ucsi_connector *con; |
21 | struct typec_altmode *alt; |
22 | struct work_struct work; |
23 | int offset; |
24 | |
25 | bool override; |
26 | bool initialized; |
27 | |
28 | u32 ; |
29 | u32 *vdo_data; |
30 | u8 vdo_size; |
31 | }; |
32 | |
33 | /* |
34 | * Note. Alternate mode control is optional feature in UCSI. It means that even |
35 | * if the system supports alternate modes, the OS may not be aware of them. |
36 | * |
37 | * In most cases however, the OS will be able to see the supported alternate |
38 | * modes, but it may still not be able to configure them, not even enter or exit |
39 | * them. That is because UCSI defines alt mode details and alt mode "overriding" |
40 | * as separate options. |
41 | * |
42 | * In case alt mode details are supported, but overriding is not, the driver |
43 | * will still display the supported pin assignments and configuration, but any |
44 | * changes the user attempts to do will lead into failure with return value of |
45 | * -EOPNOTSUPP. |
46 | */ |
47 | |
48 | static int ucsi_displayport_enter(struct typec_altmode *alt, u32 *vdo) |
49 | { |
50 | struct ucsi_dp *dp = typec_altmode_get_drvdata(altmode: alt); |
51 | struct ucsi *ucsi = dp->con->ucsi; |
52 | int svdm_version; |
53 | u64 command; |
54 | u8 cur = 0; |
55 | int ret; |
56 | |
57 | mutex_lock(&dp->con->lock); |
58 | |
59 | if (!dp->override && dp->initialized) { |
60 | const struct typec_altmode *p = typec_altmode_get_partner(altmode: alt); |
61 | |
62 | dev_warn(&p->dev, |
63 | "firmware doesn't support alternate mode overriding\n" ); |
64 | ret = -EOPNOTSUPP; |
65 | goto err_unlock; |
66 | } |
67 | |
68 | command = UCSI_GET_CURRENT_CAM | UCSI_CONNECTOR_NUMBER(dp->con->num); |
69 | ret = ucsi_send_command(ucsi, command, retval: &cur, size: sizeof(cur)); |
70 | if (ret < 0) { |
71 | if (ucsi->version > 0x0100) |
72 | goto err_unlock; |
73 | cur = 0xff; |
74 | } |
75 | |
76 | if (cur != 0xff) { |
77 | ret = dp->con->port_altmode[cur] == alt ? 0 : -EBUSY; |
78 | goto err_unlock; |
79 | } |
80 | |
81 | /* |
82 | * We can't send the New CAM command yet to the PPM as it needs the |
83 | * configuration value as well. Pretending that we have now entered the |
84 | * mode, and letting the alt mode driver continue. |
85 | */ |
86 | |
87 | svdm_version = typec_altmode_get_svdm_version(altmode: alt); |
88 | if (svdm_version < 0) { |
89 | ret = svdm_version; |
90 | goto err_unlock; |
91 | } |
92 | |
93 | dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, CMD_ENTER_MODE); |
94 | dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE); |
95 | dp->header |= VDO_CMDT(CMDT_RSP_ACK); |
96 | |
97 | dp->vdo_data = NULL; |
98 | dp->vdo_size = 1; |
99 | |
100 | schedule_work(work: &dp->work); |
101 | ret = 0; |
102 | err_unlock: |
103 | mutex_unlock(lock: &dp->con->lock); |
104 | |
105 | return ret; |
106 | } |
107 | |
108 | static int ucsi_displayport_exit(struct typec_altmode *alt) |
109 | { |
110 | struct ucsi_dp *dp = typec_altmode_get_drvdata(altmode: alt); |
111 | int svdm_version; |
112 | u64 command; |
113 | int ret = 0; |
114 | |
115 | mutex_lock(&dp->con->lock); |
116 | |
117 | if (!dp->override) { |
118 | const struct typec_altmode *p = typec_altmode_get_partner(altmode: alt); |
119 | |
120 | dev_warn(&p->dev, |
121 | "firmware doesn't support alternate mode overriding\n" ); |
122 | ret = -EOPNOTSUPP; |
123 | goto out_unlock; |
124 | } |
125 | |
126 | command = UCSI_CMD_SET_NEW_CAM(dp->con->num, 0, dp->offset, 0); |
127 | ret = ucsi_send_command(ucsi: dp->con->ucsi, command, NULL, size: 0); |
128 | if (ret < 0) |
129 | goto out_unlock; |
130 | |
131 | svdm_version = typec_altmode_get_svdm_version(altmode: alt); |
132 | if (svdm_version < 0) { |
133 | ret = svdm_version; |
134 | goto out_unlock; |
135 | } |
136 | |
137 | dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, CMD_EXIT_MODE); |
138 | dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE); |
139 | dp->header |= VDO_CMDT(CMDT_RSP_ACK); |
140 | |
141 | dp->vdo_data = NULL; |
142 | dp->vdo_size = 1; |
143 | |
144 | schedule_work(work: &dp->work); |
145 | |
146 | out_unlock: |
147 | mutex_unlock(lock: &dp->con->lock); |
148 | |
149 | return ret; |
150 | } |
151 | |
152 | /* |
153 | * We do not actually have access to the Status Update VDO, so we have to guess |
154 | * things. |
155 | */ |
156 | static int ucsi_displayport_status_update(struct ucsi_dp *dp) |
157 | { |
158 | u32 cap = dp->alt->vdo; |
159 | |
160 | dp->data.status = DP_STATUS_ENABLED; |
161 | |
162 | /* |
163 | * If pin assignement D is supported, claiming always |
164 | * that Multi-function is preferred. |
165 | */ |
166 | if (DP_CAP_CAPABILITY(cap) & DP_CAP_UFP_D) { |
167 | dp->data.status |= DP_STATUS_CON_UFP_D; |
168 | |
169 | if (DP_CAP_UFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D)) |
170 | dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC; |
171 | } else { |
172 | dp->data.status |= DP_STATUS_CON_DFP_D; |
173 | |
174 | if (DP_CAP_DFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D)) |
175 | dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC; |
176 | } |
177 | |
178 | dp->vdo_data = &dp->data.status; |
179 | dp->vdo_size = 2; |
180 | |
181 | return 0; |
182 | } |
183 | |
184 | static int ucsi_displayport_configure(struct ucsi_dp *dp) |
185 | { |
186 | u32 pins = DP_CONF_GET_PIN_ASSIGN(dp->data.conf); |
187 | u64 command; |
188 | |
189 | if (!dp->override) |
190 | return 0; |
191 | |
192 | command = UCSI_CMD_SET_NEW_CAM(dp->con->num, 1, dp->offset, pins); |
193 | |
194 | return ucsi_send_command(ucsi: dp->con->ucsi, command, NULL, size: 0); |
195 | } |
196 | |
197 | static int ucsi_displayport_vdm(struct typec_altmode *alt, |
198 | u32 , const u32 *data, int count) |
199 | { |
200 | struct ucsi_dp *dp = typec_altmode_get_drvdata(altmode: alt); |
201 | int cmd_type = PD_VDO_CMDT(header); |
202 | int cmd = PD_VDO_CMD(header); |
203 | int svdm_version; |
204 | |
205 | mutex_lock(&dp->con->lock); |
206 | |
207 | if (!dp->override && dp->initialized) { |
208 | const struct typec_altmode *p = typec_altmode_get_partner(altmode: alt); |
209 | |
210 | dev_warn(&p->dev, |
211 | "firmware doesn't support alternate mode overriding\n" ); |
212 | mutex_unlock(lock: &dp->con->lock); |
213 | return -EOPNOTSUPP; |
214 | } |
215 | |
216 | svdm_version = typec_altmode_get_svdm_version(altmode: alt); |
217 | if (svdm_version < 0) { |
218 | mutex_unlock(lock: &dp->con->lock); |
219 | return svdm_version; |
220 | } |
221 | |
222 | switch (cmd_type) { |
223 | case CMDT_INIT: |
224 | if (PD_VDO_SVDM_VER(header) < svdm_version) { |
225 | typec_partner_set_svdm_version(partner: dp->con->partner, PD_VDO_SVDM_VER(header)); |
226 | svdm_version = PD_VDO_SVDM_VER(header); |
227 | } |
228 | |
229 | dp->header = VDO(USB_TYPEC_DP_SID, 1, svdm_version, cmd); |
230 | dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE); |
231 | |
232 | switch (cmd) { |
233 | case DP_CMD_STATUS_UPDATE: |
234 | if (ucsi_displayport_status_update(dp)) |
235 | dp->header |= VDO_CMDT(CMDT_RSP_NAK); |
236 | else |
237 | dp->header |= VDO_CMDT(CMDT_RSP_ACK); |
238 | break; |
239 | case DP_CMD_CONFIGURE: |
240 | dp->data.conf = *data; |
241 | if (ucsi_displayport_configure(dp)) { |
242 | dp->header |= VDO_CMDT(CMDT_RSP_NAK); |
243 | } else { |
244 | dp->header |= VDO_CMDT(CMDT_RSP_ACK); |
245 | if (dp->initialized) |
246 | ucsi_altmode_update_active(con: dp->con); |
247 | else |
248 | dp->initialized = true; |
249 | } |
250 | break; |
251 | default: |
252 | dp->header |= VDO_CMDT(CMDT_RSP_ACK); |
253 | break; |
254 | } |
255 | |
256 | schedule_work(work: &dp->work); |
257 | break; |
258 | default: |
259 | break; |
260 | } |
261 | |
262 | mutex_unlock(lock: &dp->con->lock); |
263 | |
264 | return 0; |
265 | } |
266 | |
267 | static const struct typec_altmode_ops ucsi_displayport_ops = { |
268 | .enter = ucsi_displayport_enter, |
269 | .exit = ucsi_displayport_exit, |
270 | .vdm = ucsi_displayport_vdm, |
271 | }; |
272 | |
273 | static void ucsi_displayport_work(struct work_struct *work) |
274 | { |
275 | struct ucsi_dp *dp = container_of(work, struct ucsi_dp, work); |
276 | int ret; |
277 | |
278 | mutex_lock(&dp->con->lock); |
279 | |
280 | ret = typec_altmode_vdm(altmode: dp->alt, header: dp->header, |
281 | vdo: dp->vdo_data, count: dp->vdo_size); |
282 | if (ret) |
283 | dev_err(&dp->alt->dev, "VDM 0x%x failed\n" , dp->header); |
284 | |
285 | dp->vdo_data = NULL; |
286 | dp->vdo_size = 0; |
287 | dp->header = 0; |
288 | |
289 | mutex_unlock(lock: &dp->con->lock); |
290 | } |
291 | |
292 | void ucsi_displayport_remove_partner(struct typec_altmode *alt) |
293 | { |
294 | struct ucsi_dp *dp; |
295 | |
296 | if (!alt) |
297 | return; |
298 | |
299 | dp = typec_altmode_get_drvdata(altmode: alt); |
300 | if (!dp) |
301 | return; |
302 | |
303 | dp->data.conf = 0; |
304 | dp->data.status = 0; |
305 | dp->initialized = false; |
306 | } |
307 | |
308 | struct typec_altmode *ucsi_register_displayport(struct ucsi_connector *con, |
309 | bool override, int offset, |
310 | struct typec_altmode_desc *desc) |
311 | { |
312 | u8 all_assignments = BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D) | |
313 | BIT(DP_PIN_ASSIGN_E); |
314 | struct typec_altmode *alt; |
315 | struct ucsi_dp *dp; |
316 | |
317 | /* We can't rely on the firmware with the capabilities. */ |
318 | desc->vdo |= DP_CAP_DP_SIGNALLING(0) | DP_CAP_RECEPTACLE; |
319 | |
320 | /* Claiming that we support all pin assignments */ |
321 | desc->vdo |= all_assignments << 8; |
322 | desc->vdo |= all_assignments << 16; |
323 | |
324 | alt = typec_port_register_altmode(port: con->port, desc); |
325 | if (IS_ERR(ptr: alt)) |
326 | return alt; |
327 | |
328 | dp = devm_kzalloc(dev: &alt->dev, size: sizeof(*dp), GFP_KERNEL); |
329 | if (!dp) { |
330 | typec_unregister_altmode(altmode: alt); |
331 | return ERR_PTR(error: -ENOMEM); |
332 | } |
333 | |
334 | INIT_WORK(&dp->work, ucsi_displayport_work); |
335 | dp->override = override; |
336 | dp->offset = offset; |
337 | dp->con = con; |
338 | dp->alt = alt; |
339 | |
340 | alt->ops = &ucsi_displayport_ops; |
341 | typec_altmode_set_drvdata(altmode: alt, data: dp); |
342 | |
343 | return alt; |
344 | } |
345 | |