646 lines · c
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 <linux/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, "%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(sw->mode_switch, SW_TABLET_MODE, tablet);105 input_sync(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(&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(sdev);124 if (!desc) {125 WARN(1, "no driver match data specified");126 return -EINVAL;127 }128 129 sw = devm_kzalloc(&sdev->dev, 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, 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(sw->mode_switch, EV_SW, SW_TABLET_MODE);160 input_report_switch(sw->mode_switch, SW_TABLET_MODE, 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, &sw->notif);175 if (status)176 return status;177 178 status = sysfs_create_group(&sdev->dev.kobj, &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(&sw->update_work);184 return 0;185 186err:187 ssam_device_notifier_unregister(sdev, &sw->notif);188 cancel_work_sync(&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(&sdev->dev.kobj, &ssam_tablet_sw_group);197 198 ssam_device_notifier_unregister(sdev, &sw->notif);199 cancel_work_sync(&sw->update_work);200}201 202 203/* -- SSAM KIP tablet switch implementation. -------------------------------- */204 205#define SSAM_EVENT_KIP_CID_COVER_STATE_CHANGED 0x1d206 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(&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 0x03330#define SSAM_POS_MAX_SOURCES 4331 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);412 413 case SSAM_POS_SOURCE_SLS:414 return ssam_pos_state_name_sls(sw, 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);466 467 case SSAM_POS_SOURCE_SLS:468 return ssam_pos_state_is_tablet_mode_sls(sw, 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(&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);519 if (status)520 return status;521 522 if (get_unaligned_le32(&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 posture529 * source. At the moment, only one source seems to be used/provided.530 * The WARN_ON() here should hopefully let us know quickly once there531 * is a device that provides multiple sources, at which point we can532 * 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(&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);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, &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(&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