135a927f2SMaximilian Luz // SPDX-License-Identifier: GPL-2.0+ 235a927f2SMaximilian Luz /* 335a927f2SMaximilian Luz * Surface System Aggregator Module (SSAM) HID transport driver for the legacy 435a927f2SMaximilian Luz * keyboard interface (KBD/TC=0x08 subsystem). Provides support for the 535a927f2SMaximilian Luz * integrated HID keyboard on Surface Laptops 1 and 2. 635a927f2SMaximilian Luz * 735a927f2SMaximilian Luz * Copyright (C) 2019-2021 Maximilian Luz <luzmaximilian@gmail.com> 835a927f2SMaximilian Luz */ 935a927f2SMaximilian Luz 1035a927f2SMaximilian Luz #include <asm/unaligned.h> 1135a927f2SMaximilian Luz #include <linux/hid.h> 1235a927f2SMaximilian Luz #include <linux/kernel.h> 1335a927f2SMaximilian Luz #include <linux/module.h> 1435a927f2SMaximilian Luz #include <linux/platform_device.h> 1535a927f2SMaximilian Luz #include <linux/types.h> 1635a927f2SMaximilian Luz 1735a927f2SMaximilian Luz #include <linux/surface_aggregator/controller.h> 1835a927f2SMaximilian Luz 1935a927f2SMaximilian Luz #include "surface_hid_core.h" 2035a927f2SMaximilian Luz 2135a927f2SMaximilian Luz 2235a927f2SMaximilian Luz /* -- SAM interface (KBD). -------------------------------------------------- */ 2335a927f2SMaximilian Luz 2435a927f2SMaximilian Luz #define KBD_FEATURE_REPORT_SIZE 7 /* 6 + report ID */ 2535a927f2SMaximilian Luz 2635a927f2SMaximilian Luz enum surface_kbd_cid { 2735a927f2SMaximilian Luz SURFACE_KBD_CID_GET_DESCRIPTOR = 0x00, 2835a927f2SMaximilian Luz SURFACE_KBD_CID_SET_CAPSLOCK_LED = 0x01, 2935a927f2SMaximilian Luz SURFACE_KBD_CID_EVT_INPUT_GENERIC = 0x03, 3035a927f2SMaximilian Luz SURFACE_KBD_CID_EVT_INPUT_HOTKEYS = 0x04, 3135a927f2SMaximilian Luz SURFACE_KBD_CID_GET_FEATURE_REPORT = 0x0b, 3235a927f2SMaximilian Luz }; 3335a927f2SMaximilian Luz 3435a927f2SMaximilian Luz static int ssam_kbd_get_descriptor(struct surface_hid_device *shid, u8 entry, u8 *buf, size_t len) 3535a927f2SMaximilian Luz { 3635a927f2SMaximilian Luz struct ssam_request rqst; 3735a927f2SMaximilian Luz struct ssam_response rsp; 3835a927f2SMaximilian Luz int status; 3935a927f2SMaximilian Luz 4035a927f2SMaximilian Luz rqst.target_category = shid->uid.category; 4135a927f2SMaximilian Luz rqst.target_id = shid->uid.target; 4235a927f2SMaximilian Luz rqst.command_id = SURFACE_KBD_CID_GET_DESCRIPTOR; 4335a927f2SMaximilian Luz rqst.instance_id = shid->uid.instance; 4435a927f2SMaximilian Luz rqst.flags = SSAM_REQUEST_HAS_RESPONSE; 4535a927f2SMaximilian Luz rqst.length = sizeof(entry); 4635a927f2SMaximilian Luz rqst.payload = &entry; 4735a927f2SMaximilian Luz 4835a927f2SMaximilian Luz rsp.capacity = len; 4935a927f2SMaximilian Luz rsp.length = 0; 5035a927f2SMaximilian Luz rsp.pointer = buf; 5135a927f2SMaximilian Luz 52*b09ee1cdSMaximilian Luz status = ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(entry)); 5335a927f2SMaximilian Luz if (status) 5435a927f2SMaximilian Luz return status; 5535a927f2SMaximilian Luz 5635a927f2SMaximilian Luz if (rsp.length != len) { 5735a927f2SMaximilian Luz dev_err(shid->dev, "invalid descriptor length: got %zu, expected, %zu\n", 5835a927f2SMaximilian Luz rsp.length, len); 5935a927f2SMaximilian Luz return -EPROTO; 6035a927f2SMaximilian Luz } 6135a927f2SMaximilian Luz 6235a927f2SMaximilian Luz return 0; 6335a927f2SMaximilian Luz } 6435a927f2SMaximilian Luz 6535a927f2SMaximilian Luz static int ssam_kbd_set_caps_led(struct surface_hid_device *shid, bool value) 6635a927f2SMaximilian Luz { 6735a927f2SMaximilian Luz struct ssam_request rqst; 6835a927f2SMaximilian Luz u8 value_u8 = value; 6935a927f2SMaximilian Luz 7035a927f2SMaximilian Luz rqst.target_category = shid->uid.category; 7135a927f2SMaximilian Luz rqst.target_id = shid->uid.target; 7235a927f2SMaximilian Luz rqst.command_id = SURFACE_KBD_CID_SET_CAPSLOCK_LED; 7335a927f2SMaximilian Luz rqst.instance_id = shid->uid.instance; 7435a927f2SMaximilian Luz rqst.flags = 0; 7535a927f2SMaximilian Luz rqst.length = sizeof(value_u8); 7635a927f2SMaximilian Luz rqst.payload = &value_u8; 7735a927f2SMaximilian Luz 78*b09ee1cdSMaximilian Luz return ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, NULL, sizeof(value_u8)); 7935a927f2SMaximilian Luz } 8035a927f2SMaximilian Luz 8135a927f2SMaximilian Luz static int ssam_kbd_get_feature_report(struct surface_hid_device *shid, u8 *buf, size_t len) 8235a927f2SMaximilian Luz { 8335a927f2SMaximilian Luz struct ssam_request rqst; 8435a927f2SMaximilian Luz struct ssam_response rsp; 8535a927f2SMaximilian Luz u8 payload = 0; 8635a927f2SMaximilian Luz int status; 8735a927f2SMaximilian Luz 8835a927f2SMaximilian Luz rqst.target_category = shid->uid.category; 8935a927f2SMaximilian Luz rqst.target_id = shid->uid.target; 9035a927f2SMaximilian Luz rqst.command_id = SURFACE_KBD_CID_GET_FEATURE_REPORT; 9135a927f2SMaximilian Luz rqst.instance_id = shid->uid.instance; 9235a927f2SMaximilian Luz rqst.flags = SSAM_REQUEST_HAS_RESPONSE; 9335a927f2SMaximilian Luz rqst.length = sizeof(payload); 9435a927f2SMaximilian Luz rqst.payload = &payload; 9535a927f2SMaximilian Luz 9635a927f2SMaximilian Luz rsp.capacity = len; 9735a927f2SMaximilian Luz rsp.length = 0; 9835a927f2SMaximilian Luz rsp.pointer = buf; 9935a927f2SMaximilian Luz 100*b09ee1cdSMaximilian Luz status = ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(payload)); 10135a927f2SMaximilian Luz if (status) 10235a927f2SMaximilian Luz return status; 10335a927f2SMaximilian Luz 10435a927f2SMaximilian Luz if (rsp.length != len) { 10535a927f2SMaximilian Luz dev_err(shid->dev, "invalid feature report length: got %zu, expected, %zu\n", 10635a927f2SMaximilian Luz rsp.length, len); 10735a927f2SMaximilian Luz return -EPROTO; 10835a927f2SMaximilian Luz } 10935a927f2SMaximilian Luz 11035a927f2SMaximilian Luz return 0; 11135a927f2SMaximilian Luz } 11235a927f2SMaximilian Luz 11335a927f2SMaximilian Luz static bool ssam_kbd_is_input_event(const struct ssam_event *event) 11435a927f2SMaximilian Luz { 11535a927f2SMaximilian Luz if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_GENERIC) 11635a927f2SMaximilian Luz return true; 11735a927f2SMaximilian Luz 11835a927f2SMaximilian Luz if (event->command_id == SURFACE_KBD_CID_EVT_INPUT_HOTKEYS) 11935a927f2SMaximilian Luz return true; 12035a927f2SMaximilian Luz 12135a927f2SMaximilian Luz return false; 12235a927f2SMaximilian Luz } 12335a927f2SMaximilian Luz 12435a927f2SMaximilian Luz static u32 ssam_kbd_event_fn(struct ssam_event_notifier *nf, const struct ssam_event *event) 12535a927f2SMaximilian Luz { 12635a927f2SMaximilian Luz struct surface_hid_device *shid = container_of(nf, struct surface_hid_device, notif); 12735a927f2SMaximilian Luz 12835a927f2SMaximilian Luz /* 12935a927f2SMaximilian Luz * Check against device UID manually, as registry and device target 13035a927f2SMaximilian Luz * category doesn't line up. 13135a927f2SMaximilian Luz */ 13235a927f2SMaximilian Luz 13335a927f2SMaximilian Luz if (shid->uid.category != event->target_category) 13435a927f2SMaximilian Luz return 0; 13535a927f2SMaximilian Luz 13635a927f2SMaximilian Luz if (shid->uid.target != event->target_id) 13735a927f2SMaximilian Luz return 0; 13835a927f2SMaximilian Luz 13935a927f2SMaximilian Luz if (shid->uid.instance != event->instance_id) 14035a927f2SMaximilian Luz return 0; 14135a927f2SMaximilian Luz 14235a927f2SMaximilian Luz if (!ssam_kbd_is_input_event(event)) 14335a927f2SMaximilian Luz return 0; 14435a927f2SMaximilian Luz 14535a927f2SMaximilian Luz hid_input_report(shid->hid, HID_INPUT_REPORT, (u8 *)&event->data[0], event->length, 0); 14635a927f2SMaximilian Luz return SSAM_NOTIF_HANDLED; 14735a927f2SMaximilian Luz } 14835a927f2SMaximilian Luz 14935a927f2SMaximilian Luz 15035a927f2SMaximilian Luz /* -- Transport driver (KBD). ----------------------------------------------- */ 15135a927f2SMaximilian Luz 15235a927f2SMaximilian Luz static int skbd_get_caps_led_value(struct hid_device *hid, u8 rprt_id, u8 *buf, size_t len) 15335a927f2SMaximilian Luz { 15435a927f2SMaximilian Luz struct hid_field *field; 15535a927f2SMaximilian Luz unsigned int offset, size; 15635a927f2SMaximilian Luz int i; 15735a927f2SMaximilian Luz 15835a927f2SMaximilian Luz /* Get LED field. */ 15935a927f2SMaximilian Luz field = hidinput_get_led_field(hid); 16035a927f2SMaximilian Luz if (!field) 16135a927f2SMaximilian Luz return -ENOENT; 16235a927f2SMaximilian Luz 16335a927f2SMaximilian Luz /* Check if we got the correct report. */ 16435a927f2SMaximilian Luz if (len != hid_report_len(field->report)) 16535a927f2SMaximilian Luz return -ENOENT; 16635a927f2SMaximilian Luz 16735a927f2SMaximilian Luz if (rprt_id != field->report->id) 16835a927f2SMaximilian Luz return -ENOENT; 16935a927f2SMaximilian Luz 17035a927f2SMaximilian Luz /* Get caps lock LED index. */ 17135a927f2SMaximilian Luz for (i = 0; i < field->report_count; i++) 17235a927f2SMaximilian Luz if ((field->usage[i].hid & 0xffff) == 0x02) 17335a927f2SMaximilian Luz break; 17435a927f2SMaximilian Luz 17535a927f2SMaximilian Luz if (i == field->report_count) 17635a927f2SMaximilian Luz return -ENOENT; 17735a927f2SMaximilian Luz 17835a927f2SMaximilian Luz /* Extract value. */ 17935a927f2SMaximilian Luz size = field->report_size; 18035a927f2SMaximilian Luz offset = field->report_offset + i * size; 18135a927f2SMaximilian Luz return !!hid_field_extract(hid, buf + 1, size, offset); 18235a927f2SMaximilian Luz } 18335a927f2SMaximilian Luz 18435a927f2SMaximilian Luz static int skbd_output_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) 18535a927f2SMaximilian Luz { 18635a927f2SMaximilian Luz int caps_led; 18735a927f2SMaximilian Luz int status; 18835a927f2SMaximilian Luz 18935a927f2SMaximilian Luz caps_led = skbd_get_caps_led_value(shid->hid, rprt_id, buf, len); 19035a927f2SMaximilian Luz if (caps_led < 0) 19135a927f2SMaximilian Luz return -EIO; /* Only caps LED output reports are supported. */ 19235a927f2SMaximilian Luz 19335a927f2SMaximilian Luz status = ssam_kbd_set_caps_led(shid, caps_led); 19435a927f2SMaximilian Luz if (status < 0) 19535a927f2SMaximilian Luz return status; 19635a927f2SMaximilian Luz 19735a927f2SMaximilian Luz return len; 19835a927f2SMaximilian Luz } 19935a927f2SMaximilian Luz 20035a927f2SMaximilian Luz static int skbd_get_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) 20135a927f2SMaximilian Luz { 20235a927f2SMaximilian Luz u8 report[KBD_FEATURE_REPORT_SIZE]; 20335a927f2SMaximilian Luz int status; 20435a927f2SMaximilian Luz 20535a927f2SMaximilian Luz /* 20635a927f2SMaximilian Luz * The keyboard only has a single hard-coded read-only feature report 20735a927f2SMaximilian Luz * of size KBD_FEATURE_REPORT_SIZE. Try to load it and compare its 20835a927f2SMaximilian Luz * report ID against the requested one. 20935a927f2SMaximilian Luz */ 21035a927f2SMaximilian Luz 21135a927f2SMaximilian Luz if (len < ARRAY_SIZE(report)) 21235a927f2SMaximilian Luz return -ENOSPC; 21335a927f2SMaximilian Luz 21435a927f2SMaximilian Luz status = ssam_kbd_get_feature_report(shid, report, ARRAY_SIZE(report)); 21535a927f2SMaximilian Luz if (status < 0) 21635a927f2SMaximilian Luz return status; 21735a927f2SMaximilian Luz 21835a927f2SMaximilian Luz if (rprt_id != report[0]) 21935a927f2SMaximilian Luz return -ENOENT; 22035a927f2SMaximilian Luz 22135a927f2SMaximilian Luz memcpy(buf, report, ARRAY_SIZE(report)); 22235a927f2SMaximilian Luz return len; 22335a927f2SMaximilian Luz } 22435a927f2SMaximilian Luz 22535a927f2SMaximilian Luz static int skbd_set_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len) 22635a927f2SMaximilian Luz { 22735a927f2SMaximilian Luz /* Not supported. See skbd_get_feature_report() for details. */ 22835a927f2SMaximilian Luz return -EIO; 22935a927f2SMaximilian Luz } 23035a927f2SMaximilian Luz 23135a927f2SMaximilian Luz 23235a927f2SMaximilian Luz /* -- Driver setup. --------------------------------------------------------- */ 23335a927f2SMaximilian Luz 23435a927f2SMaximilian Luz static int surface_kbd_probe(struct platform_device *pdev) 23535a927f2SMaximilian Luz { 23635a927f2SMaximilian Luz struct ssam_controller *ctrl; 23735a927f2SMaximilian Luz struct surface_hid_device *shid; 23835a927f2SMaximilian Luz 23935a927f2SMaximilian Luz /* Add device link to EC. */ 24035a927f2SMaximilian Luz ctrl = ssam_client_bind(&pdev->dev); 24135a927f2SMaximilian Luz if (IS_ERR(ctrl)) 24235a927f2SMaximilian Luz return PTR_ERR(ctrl) == -ENODEV ? -EPROBE_DEFER : PTR_ERR(ctrl); 24335a927f2SMaximilian Luz 24435a927f2SMaximilian Luz shid = devm_kzalloc(&pdev->dev, sizeof(*shid), GFP_KERNEL); 24535a927f2SMaximilian Luz if (!shid) 24635a927f2SMaximilian Luz return -ENOMEM; 24735a927f2SMaximilian Luz 24835a927f2SMaximilian Luz shid->dev = &pdev->dev; 24935a927f2SMaximilian Luz shid->ctrl = ctrl; 25035a927f2SMaximilian Luz 25135a927f2SMaximilian Luz shid->uid.domain = SSAM_DOMAIN_SERIALHUB; 25235a927f2SMaximilian Luz shid->uid.category = SSAM_SSH_TC_KBD; 253ea11bf4eSMaximilian Luz shid->uid.target = SSAM_SSH_TID_KIP; 25435a927f2SMaximilian Luz shid->uid.instance = 0; 25535a927f2SMaximilian Luz shid->uid.function = 0; 25635a927f2SMaximilian Luz 25735a927f2SMaximilian Luz shid->notif.base.priority = 1; 25835a927f2SMaximilian Luz shid->notif.base.fn = ssam_kbd_event_fn; 25935a927f2SMaximilian Luz shid->notif.event.reg = SSAM_EVENT_REGISTRY_SAM; 26035a927f2SMaximilian Luz shid->notif.event.id.target_category = shid->uid.category; 26135a927f2SMaximilian Luz shid->notif.event.id.instance = shid->uid.instance; 26235a927f2SMaximilian Luz shid->notif.event.mask = SSAM_EVENT_MASK_NONE; 26335a927f2SMaximilian Luz shid->notif.event.flags = 0; 26435a927f2SMaximilian Luz 26535a927f2SMaximilian Luz shid->ops.get_descriptor = ssam_kbd_get_descriptor; 26635a927f2SMaximilian Luz shid->ops.output_report = skbd_output_report; 26735a927f2SMaximilian Luz shid->ops.get_feature_report = skbd_get_feature_report; 26835a927f2SMaximilian Luz shid->ops.set_feature_report = skbd_set_feature_report; 26935a927f2SMaximilian Luz 27035a927f2SMaximilian Luz platform_set_drvdata(pdev, shid); 27135a927f2SMaximilian Luz return surface_hid_device_add(shid); 27235a927f2SMaximilian Luz } 27335a927f2SMaximilian Luz 27435a927f2SMaximilian Luz static int surface_kbd_remove(struct platform_device *pdev) 27535a927f2SMaximilian Luz { 27635a927f2SMaximilian Luz surface_hid_device_destroy(platform_get_drvdata(pdev)); 27735a927f2SMaximilian Luz return 0; 27835a927f2SMaximilian Luz } 27935a927f2SMaximilian Luz 28035a927f2SMaximilian Luz static const struct acpi_device_id surface_kbd_match[] = { 28135a927f2SMaximilian Luz { "MSHW0096" }, 28235a927f2SMaximilian Luz { }, 28335a927f2SMaximilian Luz }; 28435a927f2SMaximilian Luz MODULE_DEVICE_TABLE(acpi, surface_kbd_match); 28535a927f2SMaximilian Luz 28635a927f2SMaximilian Luz static struct platform_driver surface_kbd_driver = { 28735a927f2SMaximilian Luz .probe = surface_kbd_probe, 28835a927f2SMaximilian Luz .remove = surface_kbd_remove, 28935a927f2SMaximilian Luz .driver = { 29035a927f2SMaximilian Luz .name = "surface_keyboard", 29135a927f2SMaximilian Luz .acpi_match_table = surface_kbd_match, 29235a927f2SMaximilian Luz .pm = &surface_hid_pm_ops, 29335a927f2SMaximilian Luz .probe_type = PROBE_PREFER_ASYNCHRONOUS, 29435a927f2SMaximilian Luz }, 29535a927f2SMaximilian Luz }; 29635a927f2SMaximilian Luz module_platform_driver(surface_kbd_driver); 29735a927f2SMaximilian Luz 29835a927f2SMaximilian Luz MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); 29935a927f2SMaximilian Luz MODULE_DESCRIPTION("HID legacy transport driver for Surface System Aggregator Module"); 30035a927f2SMaximilian Luz MODULE_LICENSE("GPL"); 301