1 // SPDX-License-Identifier: GPL-2.0+
3 * Surface System Aggregator Module (SSAM) HID transport driver for the legacy
4 * keyboard interface (KBD/TC=0x08 subsystem). Provides support for the
5 * integrated HID keyboard on Surface Laptops 1 and 2.
7 * Copyright (C) 2019-2021 Maximilian Luz <luzmaximilian@gmail.com>
10 #include <linux/unaligned.h>
11 #include <linux/hid.h>
12 #include <linux/kernel.h>
13 #include <linux/module.h>
14 #include <linux/platform_device.h>
15 #include <linux/types.h>
17 #include <linux/surface_aggregator/controller.h>
19 #include "surface_hid_core.h"
22 /* -- SAM interface (KBD). -------------------------------------------------- */
24 #define KBD_FEATURE_REPORT_SIZE 7 /* 6 + report ID */
26 enum surface_kbd_cid
{
27 SURFACE_KBD_CID_GET_DESCRIPTOR
= 0x00,
28 SURFACE_KBD_CID_SET_CAPSLOCK_LED
= 0x01,
29 SURFACE_KBD_CID_EVT_INPUT_GENERIC
= 0x03,
30 SURFACE_KBD_CID_EVT_INPUT_HOTKEYS
= 0x04,
31 SURFACE_KBD_CID_GET_FEATURE_REPORT
= 0x0b,
34 static int ssam_kbd_get_descriptor(struct surface_hid_device
*shid
, u8 entry
, u8
*buf
, size_t len
)
36 struct ssam_request rqst
;
37 struct ssam_response rsp
;
40 rqst
.target_category
= shid
->uid
.category
;
41 rqst
.target_id
= shid
->uid
.target
;
42 rqst
.command_id
= SURFACE_KBD_CID_GET_DESCRIPTOR
;
43 rqst
.instance_id
= shid
->uid
.instance
;
44 rqst
.flags
= SSAM_REQUEST_HAS_RESPONSE
;
45 rqst
.length
= sizeof(entry
);
46 rqst
.payload
= &entry
;
52 status
= ssam_retry(ssam_request_do_sync_onstack
, shid
->ctrl
, &rqst
, &rsp
, sizeof(entry
));
56 if (rsp
.length
!= len
) {
57 dev_err(shid
->dev
, "invalid descriptor length: got %zu, expected, %zu\n",
65 static int ssam_kbd_set_caps_led(struct surface_hid_device
*shid
, bool value
)
67 struct ssam_request rqst
;
70 rqst
.target_category
= shid
->uid
.category
;
71 rqst
.target_id
= shid
->uid
.target
;
72 rqst
.command_id
= SURFACE_KBD_CID_SET_CAPSLOCK_LED
;
73 rqst
.instance_id
= shid
->uid
.instance
;
75 rqst
.length
= sizeof(value_u8
);
76 rqst
.payload
= &value_u8
;
78 return ssam_retry(ssam_request_do_sync_onstack
, shid
->ctrl
, &rqst
, NULL
, sizeof(value_u8
));
81 static int ssam_kbd_get_feature_report(struct surface_hid_device
*shid
, u8
*buf
, size_t len
)
83 struct ssam_request rqst
;
84 struct ssam_response rsp
;
88 rqst
.target_category
= shid
->uid
.category
;
89 rqst
.target_id
= shid
->uid
.target
;
90 rqst
.command_id
= SURFACE_KBD_CID_GET_FEATURE_REPORT
;
91 rqst
.instance_id
= shid
->uid
.instance
;
92 rqst
.flags
= SSAM_REQUEST_HAS_RESPONSE
;
93 rqst
.length
= sizeof(payload
);
94 rqst
.payload
= &payload
;
100 status
= ssam_retry(ssam_request_do_sync_onstack
, shid
->ctrl
, &rqst
, &rsp
, sizeof(payload
));
104 if (rsp
.length
!= len
) {
105 dev_err(shid
->dev
, "invalid feature report length: got %zu, expected, %zu\n",
113 static bool ssam_kbd_is_input_event(const struct ssam_event
*event
)
115 if (event
->command_id
== SURFACE_KBD_CID_EVT_INPUT_GENERIC
)
118 if (event
->command_id
== SURFACE_KBD_CID_EVT_INPUT_HOTKEYS
)
124 static u32
ssam_kbd_event_fn(struct ssam_event_notifier
*nf
, const struct ssam_event
*event
)
126 struct surface_hid_device
*shid
= container_of(nf
, struct surface_hid_device
, notif
);
129 * Check against device UID manually, as registry and device target
130 * category doesn't line up.
133 if (shid
->uid
.category
!= event
->target_category
)
136 if (shid
->uid
.target
!= event
->target_id
)
139 if (shid
->uid
.instance
!= event
->instance_id
)
142 if (!ssam_kbd_is_input_event(event
))
145 hid_input_report(shid
->hid
, HID_INPUT_REPORT
, (u8
*)&event
->data
[0], event
->length
, 0);
146 return SSAM_NOTIF_HANDLED
;
150 /* -- Transport driver (KBD). ----------------------------------------------- */
152 static int skbd_get_caps_led_value(struct hid_device
*hid
, u8 rprt_id
, u8
*buf
, size_t len
)
154 struct hid_field
*field
;
155 unsigned int offset
, size
;
159 field
= hidinput_get_led_field(hid
);
163 /* Check if we got the correct report. */
164 if (len
!= hid_report_len(field
->report
))
167 if (rprt_id
!= field
->report
->id
)
170 /* Get caps lock LED index. */
171 for (i
= 0; i
< field
->report_count
; i
++)
172 if ((field
->usage
[i
].hid
& 0xffff) == 0x02)
175 if (i
== field
->report_count
)
179 size
= field
->report_size
;
180 offset
= field
->report_offset
+ i
* size
;
181 return !!hid_field_extract(hid
, buf
+ 1, size
, offset
);
184 static int skbd_output_report(struct surface_hid_device
*shid
, u8 rprt_id
, u8
*buf
, size_t len
)
189 caps_led
= skbd_get_caps_led_value(shid
->hid
, rprt_id
, buf
, len
);
191 return -EIO
; /* Only caps LED output reports are supported. */
193 status
= ssam_kbd_set_caps_led(shid
, caps_led
);
200 static int skbd_get_feature_report(struct surface_hid_device
*shid
, u8 rprt_id
, u8
*buf
, size_t len
)
202 u8 report
[KBD_FEATURE_REPORT_SIZE
];
206 * The keyboard only has a single hard-coded read-only feature report
207 * of size KBD_FEATURE_REPORT_SIZE. Try to load it and compare its
208 * report ID against the requested one.
211 if (len
< ARRAY_SIZE(report
))
214 status
= ssam_kbd_get_feature_report(shid
, report
, ARRAY_SIZE(report
));
218 if (rprt_id
!= report
[0])
221 memcpy(buf
, report
, ARRAY_SIZE(report
));
225 static int skbd_set_feature_report(struct surface_hid_device
*shid
, u8 rprt_id
, u8
*buf
, size_t len
)
227 /* Not supported. See skbd_get_feature_report() for details. */
232 /* -- Driver setup. --------------------------------------------------------- */
234 static int surface_kbd_probe(struct platform_device
*pdev
)
236 struct ssam_controller
*ctrl
;
237 struct surface_hid_device
*shid
;
239 /* Add device link to EC. */
240 ctrl
= ssam_client_bind(&pdev
->dev
);
242 return PTR_ERR(ctrl
) == -ENODEV
? -EPROBE_DEFER
: PTR_ERR(ctrl
);
244 shid
= devm_kzalloc(&pdev
->dev
, sizeof(*shid
), GFP_KERNEL
);
248 shid
->dev
= &pdev
->dev
;
251 shid
->uid
.domain
= SSAM_DOMAIN_SERIALHUB
;
252 shid
->uid
.category
= SSAM_SSH_TC_KBD
;
253 shid
->uid
.target
= SSAM_SSH_TID_KIP
;
254 shid
->uid
.instance
= 0;
255 shid
->uid
.function
= 0;
257 shid
->notif
.base
.priority
= 1;
258 shid
->notif
.base
.fn
= ssam_kbd_event_fn
;
259 shid
->notif
.event
.reg
= SSAM_EVENT_REGISTRY_SAM
;
260 shid
->notif
.event
.id
.target_category
= shid
->uid
.category
;
261 shid
->notif
.event
.id
.instance
= shid
->uid
.instance
;
262 shid
->notif
.event
.mask
= SSAM_EVENT_MASK_NONE
;
263 shid
->notif
.event
.flags
= 0;
265 shid
->ops
.get_descriptor
= ssam_kbd_get_descriptor
;
266 shid
->ops
.output_report
= skbd_output_report
;
267 shid
->ops
.get_feature_report
= skbd_get_feature_report
;
268 shid
->ops
.set_feature_report
= skbd_set_feature_report
;
270 platform_set_drvdata(pdev
, shid
);
271 return surface_hid_device_add(shid
);
274 static void surface_kbd_remove(struct platform_device
*pdev
)
276 surface_hid_device_destroy(platform_get_drvdata(pdev
));
279 static const struct acpi_device_id surface_kbd_match
[] = {
283 MODULE_DEVICE_TABLE(acpi
, surface_kbd_match
);
285 static struct platform_driver surface_kbd_driver
= {
286 .probe
= surface_kbd_probe
,
287 .remove
= surface_kbd_remove
,
289 .name
= "surface_keyboard",
290 .acpi_match_table
= surface_kbd_match
,
291 .pm
= &surface_hid_pm_ops
,
292 .probe_type
= PROBE_PREFER_ASYNCHRONOUS
,
295 module_platform_driver(surface_kbd_driver
);
297 MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
298 MODULE_DESCRIPTION("HID legacy transport driver for Surface System Aggregator Module");
299 MODULE_LICENSE("GPL");