1// SPDX-License-Identifier: GPL-2.0+
2/*
3 * Surface System Aggregator Module (SSAM) tablet mode switch driver.
4 *
5 * Copyright (C) 2022 Maximilian Luz <luzmaximilian@gmail.com>
6 */
7
8#include <asm/unaligned.h>
9#include <linux/input.h>
10#include <linux/kernel.h>
11#include <linux/module.h>
12#include <linux/types.h>
13#include <linux/workqueue.h>
14
15#include <linux/surface_aggregator/controller.h>
16#include <linux/surface_aggregator/device.h>
17
18
19/* -- SSAM generic tablet switch driver framework. -------------------------- */
20
21struct ssam_tablet_sw;
22
23struct ssam_tablet_sw_state {
24 u32 source;
25 u32 state;
26};
27
28struct ssam_tablet_sw_ops {
29 int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state);
30 const char *(*state_name)(struct ssam_tablet_sw *sw,
31 const struct ssam_tablet_sw_state *state);
32 bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw,
33 const struct ssam_tablet_sw_state *state);
34};
35
36struct ssam_tablet_sw {
37 struct ssam_device *sdev;
38
39 struct ssam_tablet_sw_state state;
40 struct work_struct update_work;
41 struct input_dev *mode_switch;
42
43 struct ssam_tablet_sw_ops ops;
44 struct ssam_event_notifier notif;
45};
46
47struct ssam_tablet_sw_desc {
48 struct {
49 const char *name;
50 const char *phys;
51 } dev;
52
53 struct {
54 u32 (*notify)(struct ssam_event_notifier *nf, const struct ssam_event *event);
55 int (*get_state)(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state);
56 const char *(*state_name)(struct ssam_tablet_sw *sw,
57 const struct ssam_tablet_sw_state *state);
58 bool (*state_is_tablet_mode)(struct ssam_tablet_sw *sw,
59 const struct ssam_tablet_sw_state *state);
60 } ops;
61
62 struct {
63 struct ssam_event_registry reg;
64 struct ssam_event_id id;
65 enum ssam_event_mask mask;
66 u8 flags;
67 } event;
68};
69
70static ssize_t state_show(struct device *dev, struct device_attribute *attr, char *buf)
71{
72 struct ssam_tablet_sw *sw = dev_get_drvdata(dev);
73 const char *state = sw->ops.state_name(sw, &sw->state);
74
75 return sysfs_emit(buf, fmt: "%s\n", state);
76}
77static DEVICE_ATTR_RO(state);
78
79static struct attribute *ssam_tablet_sw_attrs[] = {
80 &dev_attr_state.attr,
81 NULL,
82};
83
84static const struct attribute_group ssam_tablet_sw_group = {
85 .attrs = ssam_tablet_sw_attrs,
86};
87
88static void ssam_tablet_sw_update_workfn(struct work_struct *work)
89{
90 struct ssam_tablet_sw *sw = container_of(work, struct ssam_tablet_sw, update_work);
91 struct ssam_tablet_sw_state state;
92 int tablet, status;
93
94 status = sw->ops.get_state(sw, &state);
95 if (status)
96 return;
97
98 if (sw->state.source == state.source && sw->state.state == state.state)
99 return;
100 sw->state = state;
101
102 /* Send SW_TABLET_MODE event. */
103 tablet = sw->ops.state_is_tablet_mode(sw, &state);
104 input_report_switch(dev: sw->mode_switch, SW_TABLET_MODE, value: tablet);
105 input_sync(dev: sw->mode_switch);
106}
107
108static int __maybe_unused ssam_tablet_sw_resume(struct device *dev)
109{
110 struct ssam_tablet_sw *sw = dev_get_drvdata(dev);
111
112 schedule_work(work: &sw->update_work);
113 return 0;
114}
115static SIMPLE_DEV_PM_OPS(ssam_tablet_sw_pm_ops, NULL, ssam_tablet_sw_resume);
116
117static int ssam_tablet_sw_probe(struct ssam_device *sdev)
118{
119 const struct ssam_tablet_sw_desc *desc;
120 struct ssam_tablet_sw *sw;
121 int tablet, status;
122
123 desc = ssam_device_get_match_data(dev: sdev);
124 if (!desc) {
125 WARN(1, "no driver match data specified");
126 return -EINVAL;
127 }
128
129 sw = devm_kzalloc(dev: &sdev->dev, size: sizeof(*sw), GFP_KERNEL);
130 if (!sw)
131 return -ENOMEM;
132
133 sw->sdev = sdev;
134
135 sw->ops.get_state = desc->ops.get_state;
136 sw->ops.state_name = desc->ops.state_name;
137 sw->ops.state_is_tablet_mode = desc->ops.state_is_tablet_mode;
138
139 INIT_WORK(&sw->update_work, ssam_tablet_sw_update_workfn);
140
141 ssam_device_set_drvdata(sdev, data: sw);
142
143 /* Get initial state. */
144 status = sw->ops.get_state(sw, &sw->state);
145 if (status)
146 return status;
147
148 /* Set up tablet mode switch. */
149 sw->mode_switch = devm_input_allocate_device(&sdev->dev);
150 if (!sw->mode_switch)
151 return -ENOMEM;
152
153 sw->mode_switch->name = desc->dev.name;
154 sw->mode_switch->phys = desc->dev.phys;
155 sw->mode_switch->id.bustype = BUS_HOST;
156 sw->mode_switch->dev.parent = &sdev->dev;
157
158 tablet = sw->ops.state_is_tablet_mode(sw, &sw->state);
159 input_set_capability(dev: sw->mode_switch, EV_SW, SW_TABLET_MODE);
160 input_report_switch(dev: sw->mode_switch, SW_TABLET_MODE, value: tablet);
161
162 status = input_register_device(sw->mode_switch);
163 if (status)
164 return status;
165
166 /* Set up notifier. */
167 sw->notif.base.priority = 0;
168 sw->notif.base.fn = desc->ops.notify;
169 sw->notif.event.reg = desc->event.reg;
170 sw->notif.event.id = desc->event.id;
171 sw->notif.event.mask = desc->event.mask;
172 sw->notif.event.flags = SSAM_EVENT_SEQUENCED;
173
174 status = ssam_device_notifier_register(sdev, n: &sw->notif);
175 if (status)
176 return status;
177
178 status = sysfs_create_group(kobj: &sdev->dev.kobj, grp: &ssam_tablet_sw_group);
179 if (status)
180 goto err;
181
182 /* We might have missed events during setup, so check again. */
183 schedule_work(work: &sw->update_work);
184 return 0;
185
186err:
187 ssam_device_notifier_unregister(sdev, n: &sw->notif);
188 cancel_work_sync(work: &sw->update_work);
189 return status;
190}
191
192static void ssam_tablet_sw_remove(struct ssam_device *sdev)
193{
194 struct ssam_tablet_sw *sw = ssam_device_get_drvdata(sdev);
195
196 sysfs_remove_group(kobj: &sdev->dev.kobj, grp: &ssam_tablet_sw_group);
197
198 ssam_device_notifier_unregister(sdev, n: &sw->notif);
199 cancel_work_sync(work: &sw->update_work);
200}
201
202
203/* -- SSAM KIP tablet switch implementation. -------------------------------- */
204
205#define SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED 0x1d
206
207enum ssam_kip_cover_state {
208 SSAM_KIP_COVER_STATE_DISCONNECTED = 0x01,
209 SSAM_KIP_COVER_STATE_CLOSED = 0x02,
210 SSAM_KIP_COVER_STATE_LAPTOP = 0x03,
211 SSAM_KIP_COVER_STATE_FOLDED_CANVAS = 0x04,
212 SSAM_KIP_COVER_STATE_FOLDED_BACK = 0x05,
213 SSAM_KIP_COVER_STATE_BOOK = 0x06,
214};
215
216static const char *ssam_kip_cover_state_name(struct ssam_tablet_sw *sw,
217 const struct ssam_tablet_sw_state *state)
218{
219 switch (state->state) {
220 case SSAM_KIP_COVER_STATE_DISCONNECTED:
221 return "disconnected";
222
223 case SSAM_KIP_COVER_STATE_CLOSED:
224 return "closed";
225
226 case SSAM_KIP_COVER_STATE_LAPTOP:
227 return "laptop";
228
229 case SSAM_KIP_COVER_STATE_FOLDED_CANVAS:
230 return "folded-canvas";
231
232 case SSAM_KIP_COVER_STATE_FOLDED_BACK:
233 return "folded-back";
234
235 case SSAM_KIP_COVER_STATE_BOOK:
236 return "book";
237
238 default:
239 dev_warn(&sw->sdev->dev, "unknown KIP cover state: %u\n", state->state);
240 return "<unknown>";
241 }
242}
243
244static bool ssam_kip_cover_state_is_tablet_mode(struct ssam_tablet_sw *sw,
245 const struct ssam_tablet_sw_state *state)
246{
247 switch (state->state) {
248 case SSAM_KIP_COVER_STATE_DISCONNECTED:
249 case SSAM_KIP_COVER_STATE_FOLDED_CANVAS:
250 case SSAM_KIP_COVER_STATE_FOLDED_BACK:
251 case SSAM_KIP_COVER_STATE_BOOK:
252 return true;
253
254 case SSAM_KIP_COVER_STATE_CLOSED:
255 case SSAM_KIP_COVER_STATE_LAPTOP:
256 return false;
257
258 default:
259 dev_warn(&sw->sdev->dev, "unknown KIP cover state: %d\n", state->state);
260 return true;
261 }
262}
263
264SSAM_DEFINE_SYNC_REQUEST_R(__ssam_kip_get_cover_state, u8, {
265 .target_category = SSAM_SSH_TC_KIP,
266 .target_id = SSAM_SSH_TID_SAM,
267 .command_id = 0x1d,
268 .instance_id = 0x00,
269});
270
271static int ssam_kip_get_cover_state(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state)
272{
273 int status;
274 u8 raw;
275
276 status = ssam_retry(__ssam_kip_get_cover_state, sw->sdev->ctrl, &raw);
277 if (status < 0) {
278 dev_err(&sw->sdev->dev, "failed to query KIP lid state: %d\n", status);
279 return status;
280 }
281
282 state->source = 0; /* Unused for KIP switch. */
283 state->state = raw;
284 return 0;
285}
286
287static u32 ssam_kip_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
288{
289 struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif);
290
291 if (event->command_id != SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED)
292 return 0; /* Return "unhandled". */
293
294 if (event->length < 1)
295 dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length);
296
297 schedule_work(work: &sw->update_work);
298 return SSAM_NOTIF_HANDLED;
299}
300
301static const struct ssam_tablet_sw_desc ssam_kip_sw_desc = {
302 .dev = {
303 .name = "Microsoft Surface KIP Tablet Mode Switch",
304 .phys = "ssam/01:0e:01:00:01/input0",
305 },
306 .ops = {
307 .notify = ssam_kip_sw_notif,
308 .get_state = ssam_kip_get_cover_state,
309 .state_name = ssam_kip_cover_state_name,
310 .state_is_tablet_mode = ssam_kip_cover_state_is_tablet_mode,
311 },
312 .event = {
313 .reg = SSAM_EVENT_REGISTRY_SAM,
314 .id = {
315 .target_category = SSAM_SSH_TC_KIP,
316 .instance = 0,
317 },
318 .mask = SSAM_EVENT_MASK_TARGET,
319 },
320};
321
322
323/* -- SSAM POS tablet switch implementation. -------------------------------- */
324
325static bool tablet_mode_in_slate_state = true;
326module_param(tablet_mode_in_slate_state, bool, 0644);
327MODULE_PARM_DESC(tablet_mode_in_slate_state, "Enable tablet mode in slate device posture, default is 'true'");
328
329#define SSAM_EVENT_POS_CID_POSTURE_CHANGED 0x03
330#define SSAM_POS_MAX_SOURCES 4
331
332enum ssam_pos_source_id {
333 SSAM_POS_SOURCE_COVER = 0x00,
334 SSAM_POS_SOURCE_SLS = 0x03,
335};
336
337enum ssam_pos_state_cover {
338 SSAM_POS_COVER_DISCONNECTED = 0x01,
339 SSAM_POS_COVER_CLOSED = 0x02,
340 SSAM_POS_COVER_LAPTOP = 0x03,
341 SSAM_POS_COVER_FOLDED_CANVAS = 0x04,
342 SSAM_POS_COVER_FOLDED_BACK = 0x05,
343 SSAM_POS_COVER_BOOK = 0x06,
344};
345
346enum ssam_pos_state_sls {
347 SSAM_POS_SLS_LID_CLOSED = 0x00,
348 SSAM_POS_SLS_LAPTOP = 0x01,
349 SSAM_POS_SLS_SLATE = 0x02,
350 SSAM_POS_SLS_TABLET = 0x03,
351};
352
353struct ssam_sources_list {
354 __le32 count;
355 __le32 id[SSAM_POS_MAX_SOURCES];
356} __packed;
357
358static const char *ssam_pos_state_name_cover(struct ssam_tablet_sw *sw, u32 state)
359{
360 switch (state) {
361 case SSAM_POS_COVER_DISCONNECTED:
362 return "disconnected";
363
364 case SSAM_POS_COVER_CLOSED:
365 return "closed";
366
367 case SSAM_POS_COVER_LAPTOP:
368 return "laptop";
369
370 case SSAM_POS_COVER_FOLDED_CANVAS:
371 return "folded-canvas";
372
373 case SSAM_POS_COVER_FOLDED_BACK:
374 return "folded-back";
375
376 case SSAM_POS_COVER_BOOK:
377 return "book";
378
379 default:
380 dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n", state);
381 return "<unknown>";
382 }
383}
384
385static const char *ssam_pos_state_name_sls(struct ssam_tablet_sw *sw, u32 state)
386{
387 switch (state) {
388 case SSAM_POS_SLS_LID_CLOSED:
389 return "closed";
390
391 case SSAM_POS_SLS_LAPTOP:
392 return "laptop";
393
394 case SSAM_POS_SLS_SLATE:
395 return "slate";
396
397 case SSAM_POS_SLS_TABLET:
398 return "tablet";
399
400 default:
401 dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n", state);
402 return "<unknown>";
403 }
404}
405
406static const char *ssam_pos_state_name(struct ssam_tablet_sw *sw,
407 const struct ssam_tablet_sw_state *state)
408{
409 switch (state->source) {
410 case SSAM_POS_SOURCE_COVER:
411 return ssam_pos_state_name_cover(sw, state: state->state);
412
413 case SSAM_POS_SOURCE_SLS:
414 return ssam_pos_state_name_sls(sw, state: state->state);
415
416 default:
417 dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n", state->source);
418 return "<unknown>";
419 }
420}
421
422static bool ssam_pos_state_is_tablet_mode_cover(struct ssam_tablet_sw *sw, u32 state)
423{
424 switch (state) {
425 case SSAM_POS_COVER_DISCONNECTED:
426 case SSAM_POS_COVER_FOLDED_CANVAS:
427 case SSAM_POS_COVER_FOLDED_BACK:
428 case SSAM_POS_COVER_BOOK:
429 return true;
430
431 case SSAM_POS_COVER_CLOSED:
432 case SSAM_POS_COVER_LAPTOP:
433 return false;
434
435 default:
436 dev_warn(&sw->sdev->dev, "unknown device posture for type-cover: %u\n", state);
437 return true;
438 }
439}
440
441static bool ssam_pos_state_is_tablet_mode_sls(struct ssam_tablet_sw *sw, u32 state)
442{
443 switch (state) {
444 case SSAM_POS_SLS_LAPTOP:
445 case SSAM_POS_SLS_LID_CLOSED:
446 return false;
447
448 case SSAM_POS_SLS_SLATE:
449 return tablet_mode_in_slate_state;
450
451 case SSAM_POS_SLS_TABLET:
452 return true;
453
454 default:
455 dev_warn(&sw->sdev->dev, "unknown device posture for SLS: %u\n", state);
456 return true;
457 }
458}
459
460static bool ssam_pos_state_is_tablet_mode(struct ssam_tablet_sw *sw,
461 const struct ssam_tablet_sw_state *state)
462{
463 switch (state->source) {
464 case SSAM_POS_SOURCE_COVER:
465 return ssam_pos_state_is_tablet_mode_cover(sw, state: state->state);
466
467 case SSAM_POS_SOURCE_SLS:
468 return ssam_pos_state_is_tablet_mode_sls(sw, state: state->state);
469
470 default:
471 dev_warn(&sw->sdev->dev, "unknown device posture source: %u\n", state->source);
472 return true;
473 }
474}
475
476static int ssam_pos_get_sources_list(struct ssam_tablet_sw *sw, struct ssam_sources_list *sources)
477{
478 struct ssam_request rqst;
479 struct ssam_response rsp;
480 int status;
481
482 rqst.target_category = SSAM_SSH_TC_POS;
483 rqst.target_id = SSAM_SSH_TID_SAM;
484 rqst.command_id = 0x01;
485 rqst.instance_id = 0x00;
486 rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
487 rqst.length = 0;
488 rqst.payload = NULL;
489
490 rsp.capacity = sizeof(*sources);
491 rsp.length = 0;
492 rsp.pointer = (u8 *)sources;
493
494 status = ssam_retry(ssam_request_do_sync_onstack, sw->sdev->ctrl, &rqst, &rsp, 0);
495 if (status)
496 return status;
497
498 /* We need at least the 'sources->count' field. */
499 if (rsp.length < sizeof(__le32)) {
500 dev_err(&sw->sdev->dev, "received source list response is too small\n");
501 return -EPROTO;
502 }
503
504 /* Make sure 'sources->count' matches with the response length. */
505 if (get_unaligned_le32(p: &sources->count) * sizeof(__le32) + sizeof(__le32) != rsp.length) {
506 dev_err(&sw->sdev->dev, "mismatch between number of sources and response size\n");
507 return -EPROTO;
508 }
509
510 return 0;
511}
512
513static int ssam_pos_get_source(struct ssam_tablet_sw *sw, u32 *source_id)
514{
515 struct ssam_sources_list sources = {};
516 int status;
517
518 status = ssam_pos_get_sources_list(sw, sources: &sources);
519 if (status)
520 return status;
521
522 if (get_unaligned_le32(p: &sources.count) == 0) {
523 dev_err(&sw->sdev->dev, "no posture sources found\n");
524 return -ENODEV;
525 }
526
527 /*
528 * We currently don't know what to do with more than one posture
529 * source. At the moment, only one source seems to be used/provided.
530 * The WARN_ON() here should hopefully let us know quickly once there
531 * is a device that provides multiple sources, at which point we can
532 * then try to figure out how to handle them.
533 */
534 WARN_ON(get_unaligned_le32(&sources.count) > 1);
535
536 *source_id = get_unaligned_le32(p: &sources.id[0]);
537 return 0;
538}
539
540SSAM_DEFINE_SYNC_REQUEST_WR(__ssam_pos_get_posture_for_source, __le32, __le32, {
541 .target_category = SSAM_SSH_TC_POS,
542 .target_id = SSAM_SSH_TID_SAM,
543 .command_id = 0x02,
544 .instance_id = 0x00,
545});
546
547static int ssam_pos_get_posture_for_source(struct ssam_tablet_sw *sw, u32 source_id, u32 *posture)
548{
549 __le32 source_le = cpu_to_le32(source_id);
550 __le32 rspval_le = 0;
551 int status;
552
553 status = ssam_retry(__ssam_pos_get_posture_for_source, sw->sdev->ctrl,
554 &source_le, &rspval_le);
555 if (status)
556 return status;
557
558 *posture = le32_to_cpu(rspval_le);
559 return 0;
560}
561
562static int ssam_pos_get_posture(struct ssam_tablet_sw *sw, struct ssam_tablet_sw_state *state)
563{
564 u32 source_id;
565 u32 source_state;
566 int status;
567
568 status = ssam_pos_get_source(sw, source_id: &source_id);
569 if (status) {
570 dev_err(&sw->sdev->dev, "failed to get posture source ID: %d\n", status);
571 return status;
572 }
573
574 status = ssam_pos_get_posture_for_source(sw, source_id, posture: &source_state);
575 if (status) {
576 dev_err(&sw->sdev->dev, "failed to get posture value for source %u: %d\n",
577 source_id, status);
578 return status;
579 }
580
581 state->source = source_id;
582 state->state = source_state;
583 return 0;
584}
585
586static u32 ssam_pos_sw_notif(struct ssam_event_notifier *nf, const struct ssam_event *event)
587{
588 struct ssam_tablet_sw *sw = container_of(nf, struct ssam_tablet_sw, notif);
589
590 if (event->command_id != SSAM_EVENT_POS_CID_POSTURE_CHANGED)
591 return 0; /* Return "unhandled". */
592
593 if (event->length != sizeof(__le32) * 3)
594 dev_warn(&sw->sdev->dev, "unexpected payload size: %u\n", event->length);
595
596 schedule_work(work: &sw->update_work);
597 return SSAM_NOTIF_HANDLED;
598}
599
600static const struct ssam_tablet_sw_desc ssam_pos_sw_desc = {
601 .dev = {
602 .name = "Microsoft Surface POS Tablet Mode Switch",
603 .phys = "ssam/01:26:01:00:01/input0",
604 },
605 .ops = {
606 .notify = ssam_pos_sw_notif,
607 .get_state = ssam_pos_get_posture,
608 .state_name = ssam_pos_state_name,
609 .state_is_tablet_mode = ssam_pos_state_is_tablet_mode,
610 },
611 .event = {
612 .reg = SSAM_EVENT_REGISTRY_SAM,
613 .id = {
614 .target_category = SSAM_SSH_TC_POS,
615 .instance = 0,
616 },
617 .mask = SSAM_EVENT_MASK_TARGET,
618 },
619};
620
621
622/* -- Driver registration. -------------------------------------------------- */
623
624static const struct ssam_device_id ssam_tablet_sw_match[] = {
625 { SSAM_SDEV(KIP, SAM, 0x00, 0x01), (unsigned long)&ssam_kip_sw_desc },
626 { SSAM_SDEV(POS, SAM, 0x00, 0x01), (unsigned long)&ssam_pos_sw_desc },
627 { },
628};
629MODULE_DEVICE_TABLE(ssam, ssam_tablet_sw_match);
630
631static struct ssam_device_driver ssam_tablet_sw_driver = {
632 .probe = ssam_tablet_sw_probe,
633 .remove = ssam_tablet_sw_remove,
634 .match_table = ssam_tablet_sw_match,
635 .driver = {
636 .name = "surface_aggregator_tablet_mode_switch",
637 .probe_type = PROBE_PREFER_ASYNCHRONOUS,
638 .pm = &ssam_tablet_sw_pm_ops,
639 },
640};
641module_ssam_device_driver(ssam_tablet_sw_driver);
642
643MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
644MODULE_DESCRIPTION("Tablet mode switch driver for Surface devices using the Surface Aggregator Module");
645MODULE_LICENSE("GPL");
646

source code of linux/drivers/platform/surface/surface_aggregator_tabletsw.c