jcs's openbsd hax
openbsd
1/* $OpenBSD: qcdpc.c,v 1.1 2025/07/17 15:52:10 kettenis Exp $ */
2/*
3 * Copyright (c) 2025 Mark Kettenis <kettenis@openbsd.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18#include <sys/param.h>
19#include <sys/systm.h>
20#include <sys/device.h>
21
22#include <machine/bus.h>
23#include <machine/fdt.h>
24#include <machine/simplebusvar.h>
25
26#include <dev/ofw/openfirm.h>
27#include <dev/ofw/fdt.h>
28
29#include <drm/display/drm_dp_helper.h>
30
31#include <dev/wscons/wsconsio.h>
32#include <dev/wscons/wsdisplayvar.h>
33
34#define DP_HW_REVISION 0x00
35#define DP_INTR_STATUS 0x20
36#define DP_INTR_AUX_XFER_DONE (1U << 3)
37#define DP_INTR_WRONG_ADDR (1U << 6)
38#define DP_INTR_TIMEOUT (1U << 9)
39#define DP_INTR_NACK_DEFER (1U << 12)
40#define DP_INTR_WRONG_DATA_CNT (1U << 15)
41#define DP_INTR_AUX_ERROR (1U << 27)
42#define DP_INTR_STATUS_ACK_SHIFT 1
43#define DP_INTR_STATUS_MASK_SHIFT 2
44#define DP_INTR_ERROR \
45 (DP_INTR_WRONG_ADDR | DP_INTR_TIMEOUT | DP_INTR_NACK_DEFER | \
46 DP_INTR_WRONG_DATA_CNT | DP_INTR_AUX_ERROR)
47
48#define DP_AUX_CTRL 0x30
49#define DP_AUX_DATA 0x34
50#define DP_AUX_DATA_SHIFT 8
51#define DP_AUX_DATA_MASK (0xffU << 8)
52#define DP_AUX_DATA_READ (1U << 0)
53#define DP_AUX_DATA_WRITE (0U << 0)
54#define DP_AUX_DATA_INDEX_SHIFT 16
55#define DP_AUX_DATA_INDEX_MASK (0xffU << 16)
56#define DP_AUX_DATA_INDEX_WRITE (1U << 31)
57#define DP_AUX_TRANS_CTRL 0x38
58#define DP_AUX_TRANS_CTRL_GO (1U << 9)
59#define DP_PHY_AUX_INTERRUPT_CLEAR 0x4c
60#define DP_PHY_AUX_INTERRUPT_STATUS 0xbc
61
62struct qcdpc_softc {
63 struct simplebus_softc sc_sbus;
64 bus_space_tag_t sc_iot;
65 bus_space_handle_t sc_ahb_ioh;
66 bus_space_handle_t sc_aux_ioh;
67 void *sc_ih;
68
69 struct drm_dp_aux sc_aux;
70 uint32_t sc_intr_status;
71
72 struct drm_edp_backlight_info sc_bl;
73 uint32_t sc_bl_level;
74};
75
76struct qcdpc_softc *qcdpc_bl;
77
78int qcdpc_match(struct device *, void *, void *);
79void qcdpc_attach(struct device *, struct device *, void *);
80
81const struct cfattach qcdpc_ca = {
82 sizeof(struct qcdpc_softc), qcdpc_match, qcdpc_attach
83};
84
85struct cfdriver qcdpc_cd = {
86 NULL, "qcdpc", DV_DULL
87};
88
89int qcdpc_print(void *, const char *);
90void qcdpc_attach_backlight(struct qcdpc_softc *);
91int qcdpc_intr(void *);
92ssize_t qcdpc_dp_aux_transfer(struct drm_dp_aux *, struct drm_dp_aux_msg *);
93
94int
95qcdpc_match(struct device *parent, void *match, void *aux)
96{
97 struct fdt_attach_args *faa = aux;
98
99 return (OF_is_compatible(faa->fa_node, "qcom,sc8280xp-dp") ||
100 OF_is_compatible(faa->fa_node, "qcom,sc8280xp-edp") ||
101 OF_is_compatible(faa->fa_node, "qcom,x1e80100-dp"));
102}
103
104void
105qcdpc_attach(struct device *parent, struct device *self, void *aux)
106{
107 struct qcdpc_softc *sc = (struct qcdpc_softc *)self;
108 struct fdt_attach_args *faa = aux;
109 struct fdt_attach_args fa;
110 int node;
111
112 /*
113 * Check that we have an AUX channel as all the functionality
114 * provided by this driver requires one.
115 *
116 * XXX Effectively this means we only attach to eDP
117 * controllers. Since the eDP controller is almost certainly
118 * enabled on all machines we care about, this means we can
119 * get away with not having a clock/reset controller.
120 * Accessing registers on a disabled hardware block tends to
121 * result in an instant reset on Qualcomm SoCs.
122 */
123 node = OF_getnodebyname(faa->fa_node, "aux-bus");
124 if (node == 0) {
125 printf("\n");
126 return;
127 }
128
129 if (faa->fa_nreg < 2) {
130 printf(": no registers\n");
131 return;
132 }
133
134 sc->sc_iot = faa->fa_iot;
135
136 if (bus_space_map(sc->sc_iot, faa->fa_reg[0].addr,
137 faa->fa_reg[0].size, 0, &sc->sc_ahb_ioh)) {
138 printf(": can't map AHB registers\n");
139 return;
140 }
141
142 if (bus_space_map(sc->sc_iot, faa->fa_reg[1].addr,
143 faa->fa_reg[1].size, 0, &sc->sc_aux_ioh)) {
144 printf(": can't map AUX registers\n");
145 bus_space_unmap(sc->sc_iot, sc->sc_ahb_ioh,
146 faa->fa_reg[0].size);
147 return;
148 }
149
150 sc->sc_ih = fdt_intr_establish(faa->fa_node, IPL_BIO, qcdpc_intr,
151 sc, sc->sc_sbus.sc_dev.dv_xname);
152 if (sc->sc_ih == NULL) {
153 printf(": can't establish interrupt\n");
154 bus_space_unmap(sc->sc_iot, sc->sc_ahb_ioh,
155 faa->fa_reg[0].size);
156 bus_space_unmap(sc->sc_iot, sc->sc_aux_ioh,
157 faa->fa_reg[1].size);
158 return;
159 }
160
161 printf("\n");
162
163 sc->sc_aux.name = sc->sc_sbus.sc_dev.dv_xname;
164 sc->sc_aux.transfer = qcdpc_dp_aux_transfer;
165 drm_dp_aux_init(&sc->sc_aux);
166
167 node = OF_getnodebyname(node, "panel");
168 if (node) {
169 /* We have a panel. Try to enable its backlight control. */
170 qcdpc_attach_backlight(sc);
171
172 memset(&fa, 0, sizeof(fa));
173 fa.fa_name = "";
174 fa.fa_node = node;
175 config_found(self, &fa, qcdpc_print);
176 }
177}
178
179int
180qcdpc_print(void *aux, const char *pnp)
181{
182 struct fdt_attach_args *fa = aux;
183 char name[32];
184
185 if (!pnp)
186 return (QUIET);
187
188 if (OF_getprop(fa->fa_node, "name", name, sizeof(name)) > 0) {
189 name[sizeof(name) - 1] = 0;
190 printf("\"%s\"", name);
191 } else
192 printf("node %u", fa->fa_node);
193
194 printf(" at %s", pnp);
195
196 return (UNCONF);
197}
198
199int
200qcdpc_get_param(struct wsdisplay_param *dp)
201{
202 struct qcdpc_softc *sc = qcdpc_bl;
203
204 switch (dp->param) {
205 case WSDISPLAYIO_PARAM_BRIGHTNESS:
206 dp->min = 0;
207 dp->max = sc->sc_bl.max;
208 dp->curval = sc->sc_bl_level;
209 return 0;
210 default:
211 return -1;
212 }
213}
214
215int
216qcdpc_set_param(struct wsdisplay_param *dp)
217{
218 struct qcdpc_softc *sc = qcdpc_bl;
219 int ret;
220
221 switch (dp->param) {
222 case WSDISPLAYIO_PARAM_BRIGHTNESS:
223 ret = drm_edp_backlight_set_level(&sc->sc_aux,
224 &sc->sc_bl, dp->curval);
225 if (ret < 0)
226 return -1;
227 sc->sc_bl_level = dp->curval;
228 return 0;
229 default:
230 return -1;
231 }
232}
233
234void
235qcdpc_attach_backlight(struct qcdpc_softc *sc)
236{
237 uint8_t edp_dpcd[EDP_DISPLAY_CTL_CAP_SIZE];
238 int ret;
239
240 ret = drm_dp_dpcd_read(&sc->sc_aux, DP_EDP_DPCD_REV, edp_dpcd,
241 EDP_DISPLAY_CTL_CAP_SIZE);
242 if (ret < 0)
243 return;
244
245 if (drm_edp_backlight_supported(edp_dpcd)) {
246 uint16_t current_level;
247 uint8_t current_mode;
248
249 ret = drm_edp_backlight_init(&sc->sc_aux, &sc->sc_bl, 0,
250 edp_dpcd, ¤t_level, ¤t_mode);
251 if (ret < 0 || !sc->sc_bl.aux_set)
252 return;
253 sc->sc_bl_level = current_level;
254
255 qcdpc_bl = sc;
256 ws_get_param = qcdpc_get_param;
257 ws_set_param = qcdpc_set_param;
258 }
259}
260
261int
262qcdpc_intr(void *arg)
263{
264 struct qcdpc_softc *sc = arg;
265 int handled = 0;
266 uint32_t status;
267
268 status = bus_space_read_4(sc->sc_iot, sc->sc_ahb_ioh, DP_INTR_STATUS);
269 if (status & (DP_INTR_AUX_XFER_DONE | DP_INTR_ERROR)) {
270 sc->sc_intr_status = status;
271 handled = 1;
272 }
273 bus_space_write_4(sc->sc_iot, sc->sc_ahb_ioh, DP_INTR_STATUS,
274 DP_INTR_AUX_XFER_DONE << DP_INTR_STATUS_ACK_SHIFT |
275 DP_INTR_ERROR << DP_INTR_STATUS_ACK_SHIFT);
276
277 return handled;
278}
279
280/*
281 * The function below implement a DP AUX channel backend for DRM and
282 * therefore follow the Linux convention of returning negative error
283 * values.
284 */
285
286int
287qcdpc_dp_aux_wait(struct qcdpc_softc *sc)
288{
289 uint32_t status;
290 int s, timo;
291
292 if (cold) {
293 for (timo = 250; timo > 0; timo--) {
294 status = bus_space_read_4(sc->sc_iot, sc->sc_ahb_ioh,
295 DP_INTR_STATUS);
296 if (status & (DP_INTR_AUX_XFER_DONE | DP_INTR_ERROR)) {
297 sc->sc_intr_status = status;
298 break;
299 }
300 delay(1000);
301 }
302 } else {
303 s = splbio();
304 if (sc->sc_intr_status == 0) {
305 tsleep_nsec(&sc->sc_intr_status, PWAIT, "qcdpc",
306 MSEC_TO_NSEC(250));
307 }
308 splx(s);
309 }
310
311 return sc->sc_intr_status ? 0 : -ETIMEDOUT;
312}
313
314ssize_t
315qcdpc_dp_aux_write(struct qcdpc_softc *sc, struct drm_dp_aux_msg *msg)
316{
317 uint8_t buf[DP_AUX_MAX_PAYLOAD_BYTES + 4];
318 uint8_t *msgbuf;
319 uint32_t val, stat;
320 size_t len;
321 int i, ret;
322
323 KASSERT(msg->size <= DP_AUX_MAX_PAYLOAD_BYTES);
324
325 if (msg->request == DP_AUX_NATIVE_READ)
326 len = 0;
327 else
328 len = msg->size;
329
330 buf[0] = msg->address >> 16;
331 if (msg->request == DP_AUX_NATIVE_READ)
332 buf[0] |= (1 << 4);
333 buf[1] = msg->address >> 8;
334 buf[2] = msg->address >> 0;
335 buf[3] = msg->size - 1;
336 memcpy(&buf[4], msg->buffer, len);
337
338 for (i = 0; i < len + 4; i++) {
339 val = buf[i] << DP_AUX_DATA_SHIFT;
340 val |= DP_AUX_DATA_WRITE;
341 if (i == 0)
342 val |= DP_AUX_DATA_INDEX_WRITE;
343 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh,
344 DP_AUX_DATA, val);
345 }
346
347 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh, DP_AUX_TRANS_CTRL, 0);
348
349 /* Clear HW interrupts. */
350 bus_space_read_4(sc->sc_iot, sc->sc_aux_ioh,
351 DP_PHY_AUX_INTERRUPT_STATUS);
352 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh,
353 DP_PHY_AUX_INTERRUPT_CLEAR, 0x1f);
354 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh,
355 DP_PHY_AUX_INTERRUPT_CLEAR, 0x9f);
356 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh,
357 DP_PHY_AUX_INTERRUPT_CLEAR, 0);
358
359 sc->sc_intr_status = 0;
360 bus_space_write_4(sc->sc_iot, sc->sc_ahb_ioh, DP_INTR_STATUS,
361 DP_INTR_AUX_XFER_DONE << DP_INTR_STATUS_ACK_SHIFT |
362 DP_INTR_ERROR << DP_INTR_STATUS_ACK_SHIFT |
363 DP_INTR_AUX_XFER_DONE << DP_INTR_STATUS_MASK_SHIFT |
364 DP_INTR_ERROR << DP_INTR_STATUS_MASK_SHIFT);
365
366 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh, DP_AUX_TRANS_CTRL,
367 DP_AUX_TRANS_CTRL_GO);
368
369 ret = qcdpc_dp_aux_wait(sc);
370
371 bus_space_write_4(sc->sc_iot, sc->sc_ahb_ioh, DP_INTR_STATUS,
372 DP_INTR_AUX_XFER_DONE << DP_INTR_STATUS_ACK_SHIFT |
373 DP_INTR_ERROR << DP_INTR_STATUS_ACK_SHIFT);
374
375 if (ret < 0)
376 return ret;
377
378 return len;
379}
380
381ssize_t
382qcdpc_dp_aux_read(struct qcdpc_softc *sc, struct drm_dp_aux_msg *msg)
383{
384 uint8_t *p = msg->buffer;
385 uint32_t val;
386 int i, j;
387
388 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh, DP_AUX_TRANS_CTRL, 0);
389
390 val = DP_AUX_DATA_INDEX_WRITE | DP_AUX_DATA_READ;
391 bus_space_write_4(sc->sc_iot, sc->sc_aux_ioh, DP_AUX_DATA, val);
392
393 /* Discard the first byte. */
394 bus_space_read_4(sc->sc_iot, sc->sc_aux_ioh, DP_AUX_DATA);
395
396 for (i = 0; i < msg->size; i++) {
397 val = bus_space_read_4(sc->sc_iot, sc->sc_aux_ioh,
398 DP_AUX_DATA);
399 *p++ = val >> DP_AUX_DATA_SHIFT;
400 j = (val & DP_AUX_DATA_INDEX_MASK) >> DP_AUX_DATA_INDEX_SHIFT;
401 if (j != i)
402 break;
403 }
404
405 return i;
406}
407
408ssize_t
409qcdpc_dp_aux_transfer(struct drm_dp_aux *aux, struct drm_dp_aux_msg *msg)
410{
411 struct qcdpc_softc *sc = container_of(aux, struct qcdpc_softc, sc_aux);
412 ssize_t ret;
413 int native;
414
415 if (msg->request == DP_AUX_NATIVE_WRITE ||
416 msg->request == DP_AUX_NATIVE_READ)
417 native = 1;
418 else
419 native = 0;
420
421 if (msg->size == 0 || msg->buffer == NULL) {
422 if (native)
423 msg->reply = DP_AUX_NATIVE_REPLY_ACK;
424 else
425 msg->reply = DP_AUX_I2C_REPLY_ACK;
426 return msg->size;
427 }
428
429 if ((native && msg->size > DP_AUX_MAX_PAYLOAD_BYTES) ||
430 msg->size > 128)
431 return -EINVAL;
432
433 /* XXX */
434 if (msg->request != DP_AUX_NATIVE_READ &&
435 msg->request != DP_AUX_NATIVE_WRITE)
436 return -EINVAL;
437
438 ret = qcdpc_dp_aux_write(sc, msg);
439 if (ret < 0)
440 return ret;
441
442 if (msg->request == DP_AUX_NATIVE_READ)
443 ret = qcdpc_dp_aux_read(sc, msg);
444
445 if (sc->sc_intr_status & DP_INTR_TIMEOUT)
446 ret = -ETIMEDOUT;
447 else if (sc->sc_intr_status & DP_INTR_ERROR)
448 msg->reply = DP_AUX_NATIVE_REPLY_NACK;
449 else
450 msg->reply = DP_AUX_NATIVE_REPLY_ACK;
451
452 return ret;
453}