1b05ff100SMaximilian Luz // SPDX-License-Identifier: GPL-2.0+
2b05ff100SMaximilian Luz /*
3b05ff100SMaximilian Luz  * Surface System Aggregator Module (SSAM) HID transport driver for the
4b05ff100SMaximilian Luz  * generic HID interface (HID/TC=0x15 subsystem). Provides support for
5b05ff100SMaximilian Luz  * integrated HID devices on Surface Laptop 3, Book 3, and later.
6b05ff100SMaximilian Luz  *
7b05ff100SMaximilian Luz  * Copyright (C) 2019-2021 Blaž Hrastnik <blaz@mxxn.io>,
8b05ff100SMaximilian Luz  *                         Maximilian Luz <luzmaximilian@gmail.com>
9b05ff100SMaximilian Luz  */
10b05ff100SMaximilian Luz 
11b05ff100SMaximilian Luz #include <asm/unaligned.h>
12b05ff100SMaximilian Luz #include <linux/hid.h>
13b05ff100SMaximilian Luz #include <linux/kernel.h>
14b05ff100SMaximilian Luz #include <linux/module.h>
15b05ff100SMaximilian Luz #include <linux/types.h>
16b05ff100SMaximilian Luz 
17b05ff100SMaximilian Luz #include <linux/surface_aggregator/controller.h>
18b05ff100SMaximilian Luz #include <linux/surface_aggregator/device.h>
19b05ff100SMaximilian Luz 
20b05ff100SMaximilian Luz #include "surface_hid_core.h"
21b05ff100SMaximilian Luz 
22b05ff100SMaximilian Luz 
23b05ff100SMaximilian Luz /* -- SAM interface. -------------------------------------------------------- */
24b05ff100SMaximilian Luz 
25b05ff100SMaximilian Luz struct surface_hid_buffer_slice {
26b05ff100SMaximilian Luz 	__u8 entry;
27b05ff100SMaximilian Luz 	__le32 offset;
28b05ff100SMaximilian Luz 	__le32 length;
29b05ff100SMaximilian Luz 	__u8 end;
30b05ff100SMaximilian Luz 	__u8 data[];
31b05ff100SMaximilian Luz } __packed;
32b05ff100SMaximilian Luz 
33b05ff100SMaximilian Luz static_assert(sizeof(struct surface_hid_buffer_slice) == 10);
34b05ff100SMaximilian Luz 
35b05ff100SMaximilian Luz enum surface_hid_cid {
36b05ff100SMaximilian Luz 	SURFACE_HID_CID_OUTPUT_REPORT      = 0x01,
37b05ff100SMaximilian Luz 	SURFACE_HID_CID_GET_FEATURE_REPORT = 0x02,
38b05ff100SMaximilian Luz 	SURFACE_HID_CID_SET_FEATURE_REPORT = 0x03,
39b05ff100SMaximilian Luz 	SURFACE_HID_CID_GET_DESCRIPTOR     = 0x04,
40b05ff100SMaximilian Luz };
41b05ff100SMaximilian Luz 
ssam_hid_get_descriptor(struct surface_hid_device * shid,u8 entry,u8 * buf,size_t len)42b05ff100SMaximilian Luz static int ssam_hid_get_descriptor(struct surface_hid_device *shid, u8 entry, u8 *buf, size_t len)
43b05ff100SMaximilian Luz {
44b05ff100SMaximilian Luz 	u8 buffer[sizeof(struct surface_hid_buffer_slice) + 0x76];
45b05ff100SMaximilian Luz 	struct surface_hid_buffer_slice *slice;
46b05ff100SMaximilian Luz 	struct ssam_request rqst;
47b05ff100SMaximilian Luz 	struct ssam_response rsp;
48b05ff100SMaximilian Luz 	u32 buffer_len, offset, length;
49b05ff100SMaximilian Luz 	int status;
50b05ff100SMaximilian Luz 
51b05ff100SMaximilian Luz 	/*
52b05ff100SMaximilian Luz 	 * Note: The 0x76 above has been chosen because that's what's used by
53b05ff100SMaximilian Luz 	 * the Windows driver. Together with the header, this leads to a 128
54b05ff100SMaximilian Luz 	 * byte payload in total.
55b05ff100SMaximilian Luz 	 */
56b05ff100SMaximilian Luz 
57b05ff100SMaximilian Luz 	buffer_len = ARRAY_SIZE(buffer) - sizeof(struct surface_hid_buffer_slice);
58b05ff100SMaximilian Luz 
59b05ff100SMaximilian Luz 	rqst.target_category = shid->uid.category;
60b05ff100SMaximilian Luz 	rqst.target_id = shid->uid.target;
61b05ff100SMaximilian Luz 	rqst.command_id = SURFACE_HID_CID_GET_DESCRIPTOR;
62b05ff100SMaximilian Luz 	rqst.instance_id = shid->uid.instance;
63b05ff100SMaximilian Luz 	rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
64b05ff100SMaximilian Luz 	rqst.length = sizeof(struct surface_hid_buffer_slice);
65b05ff100SMaximilian Luz 	rqst.payload = buffer;
66b05ff100SMaximilian Luz 
67b05ff100SMaximilian Luz 	rsp.capacity = ARRAY_SIZE(buffer);
68b05ff100SMaximilian Luz 	rsp.pointer = buffer;
69b05ff100SMaximilian Luz 
70b05ff100SMaximilian Luz 	slice = (struct surface_hid_buffer_slice *)buffer;
71b05ff100SMaximilian Luz 	slice->entry = entry;
72b05ff100SMaximilian Luz 	slice->end = 0;
73b05ff100SMaximilian Luz 
74b05ff100SMaximilian Luz 	offset = 0;
75b05ff100SMaximilian Luz 	length = buffer_len;
76b05ff100SMaximilian Luz 
77b05ff100SMaximilian Luz 	while (!slice->end && offset < len) {
78b05ff100SMaximilian Luz 		put_unaligned_le32(offset, &slice->offset);
79b05ff100SMaximilian Luz 		put_unaligned_le32(length, &slice->length);
80b05ff100SMaximilian Luz 
81b05ff100SMaximilian Luz 		rsp.length = 0;
82b05ff100SMaximilian Luz 
83*b09ee1cdSMaximilian Luz 		status = ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp,
84b05ff100SMaximilian Luz 				    sizeof(*slice));
85b05ff100SMaximilian Luz 		if (status)
86b05ff100SMaximilian Luz 			return status;
87b05ff100SMaximilian Luz 
88b05ff100SMaximilian Luz 		offset = get_unaligned_le32(&slice->offset);
89b05ff100SMaximilian Luz 		length = get_unaligned_le32(&slice->length);
90b05ff100SMaximilian Luz 
91b05ff100SMaximilian Luz 		/* Don't mess stuff up in case we receive garbage. */
92b05ff100SMaximilian Luz 		if (length > buffer_len || offset > len)
93b05ff100SMaximilian Luz 			return -EPROTO;
94b05ff100SMaximilian Luz 
95b05ff100SMaximilian Luz 		if (offset + length > len)
96b05ff100SMaximilian Luz 			length = len - offset;
97b05ff100SMaximilian Luz 
98b05ff100SMaximilian Luz 		memcpy(buf + offset, &slice->data[0], length);
99b05ff100SMaximilian Luz 
100b05ff100SMaximilian Luz 		offset += length;
101b05ff100SMaximilian Luz 		length = buffer_len;
102b05ff100SMaximilian Luz 	}
103b05ff100SMaximilian Luz 
104b05ff100SMaximilian Luz 	if (offset != len) {
105b05ff100SMaximilian Luz 		dev_err(shid->dev, "unexpected descriptor length: got %u, expected %zu\n",
106b05ff100SMaximilian Luz 			offset, len);
107b05ff100SMaximilian Luz 		return -EPROTO;
108b05ff100SMaximilian Luz 	}
109b05ff100SMaximilian Luz 
110b05ff100SMaximilian Luz 	return 0;
111b05ff100SMaximilian Luz }
112b05ff100SMaximilian Luz 
ssam_hid_set_raw_report(struct surface_hid_device * shid,u8 rprt_id,bool feature,u8 * buf,size_t len)113b05ff100SMaximilian Luz static int ssam_hid_set_raw_report(struct surface_hid_device *shid, u8 rprt_id, bool feature,
114b05ff100SMaximilian Luz 				   u8 *buf, size_t len)
115b05ff100SMaximilian Luz {
116b05ff100SMaximilian Luz 	struct ssam_request rqst;
117b05ff100SMaximilian Luz 	u8 cid;
118b05ff100SMaximilian Luz 
119b05ff100SMaximilian Luz 	if (feature)
120b05ff100SMaximilian Luz 		cid = SURFACE_HID_CID_SET_FEATURE_REPORT;
121b05ff100SMaximilian Luz 	else
122b05ff100SMaximilian Luz 		cid = SURFACE_HID_CID_OUTPUT_REPORT;
123b05ff100SMaximilian Luz 
124b05ff100SMaximilian Luz 	rqst.target_category = shid->uid.category;
125b05ff100SMaximilian Luz 	rqst.target_id = shid->uid.target;
126b05ff100SMaximilian Luz 	rqst.instance_id = shid->uid.instance;
127b05ff100SMaximilian Luz 	rqst.command_id = cid;
128b05ff100SMaximilian Luz 	rqst.flags = 0;
129b05ff100SMaximilian Luz 	rqst.length = len;
130b05ff100SMaximilian Luz 	rqst.payload = buf;
131b05ff100SMaximilian Luz 
132b05ff100SMaximilian Luz 	buf[0] = rprt_id;
133b05ff100SMaximilian Luz 
134*b09ee1cdSMaximilian Luz 	return ssam_retry(ssam_request_do_sync, shid->ctrl, &rqst, NULL);
135b05ff100SMaximilian Luz }
136b05ff100SMaximilian Luz 
ssam_hid_get_raw_report(struct surface_hid_device * shid,u8 rprt_id,u8 * buf,size_t len)137b05ff100SMaximilian Luz static int ssam_hid_get_raw_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
138b05ff100SMaximilian Luz {
139b05ff100SMaximilian Luz 	struct ssam_request rqst;
140b05ff100SMaximilian Luz 	struct ssam_response rsp;
141b05ff100SMaximilian Luz 
142b05ff100SMaximilian Luz 	rqst.target_category = shid->uid.category;
143b05ff100SMaximilian Luz 	rqst.target_id = shid->uid.target;
144b05ff100SMaximilian Luz 	rqst.instance_id = shid->uid.instance;
145b05ff100SMaximilian Luz 	rqst.command_id = SURFACE_HID_CID_GET_FEATURE_REPORT;
1462b2bcc76SMaximilian Luz 	rqst.flags = SSAM_REQUEST_HAS_RESPONSE;
147b05ff100SMaximilian Luz 	rqst.length = sizeof(rprt_id);
148b05ff100SMaximilian Luz 	rqst.payload = &rprt_id;
149b05ff100SMaximilian Luz 
150b05ff100SMaximilian Luz 	rsp.capacity = len;
151b05ff100SMaximilian Luz 	rsp.length = 0;
152b05ff100SMaximilian Luz 	rsp.pointer = buf;
153b05ff100SMaximilian Luz 
154*b09ee1cdSMaximilian Luz 	return ssam_retry(ssam_request_do_sync_onstack, shid->ctrl, &rqst, &rsp, sizeof(rprt_id));
155b05ff100SMaximilian Luz }
156b05ff100SMaximilian Luz 
ssam_hid_event_fn(struct ssam_event_notifier * nf,const struct ssam_event * event)157b05ff100SMaximilian Luz static u32 ssam_hid_event_fn(struct ssam_event_notifier *nf, const struct ssam_event *event)
158b05ff100SMaximilian Luz {
159b05ff100SMaximilian Luz 	struct surface_hid_device *shid = container_of(nf, struct surface_hid_device, notif);
160b05ff100SMaximilian Luz 
161b05ff100SMaximilian Luz 	if (event->command_id != 0x00)
162b05ff100SMaximilian Luz 		return 0;
163b05ff100SMaximilian Luz 
164b05ff100SMaximilian Luz 	hid_input_report(shid->hid, HID_INPUT_REPORT, (u8 *)&event->data[0], event->length, 0);
165b05ff100SMaximilian Luz 	return SSAM_NOTIF_HANDLED;
166b05ff100SMaximilian Luz }
167b05ff100SMaximilian Luz 
168b05ff100SMaximilian Luz 
169b05ff100SMaximilian Luz /* -- Transport driver. ----------------------------------------------------- */
170b05ff100SMaximilian Luz 
shid_output_report(struct surface_hid_device * shid,u8 rprt_id,u8 * buf,size_t len)171b05ff100SMaximilian Luz static int shid_output_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
172b05ff100SMaximilian Luz {
173b05ff100SMaximilian Luz 	int status;
174b05ff100SMaximilian Luz 
175b05ff100SMaximilian Luz 	status = ssam_hid_set_raw_report(shid, rprt_id, false, buf, len);
176b05ff100SMaximilian Luz 	return status >= 0 ? len : status;
177b05ff100SMaximilian Luz }
178b05ff100SMaximilian Luz 
shid_get_feature_report(struct surface_hid_device * shid,u8 rprt_id,u8 * buf,size_t len)179b05ff100SMaximilian Luz static int shid_get_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
180b05ff100SMaximilian Luz {
181b05ff100SMaximilian Luz 	int status;
182b05ff100SMaximilian Luz 
183b05ff100SMaximilian Luz 	status = ssam_hid_get_raw_report(shid, rprt_id, buf, len);
184b05ff100SMaximilian Luz 	return status >= 0 ? len : status;
185b05ff100SMaximilian Luz }
186b05ff100SMaximilian Luz 
shid_set_feature_report(struct surface_hid_device * shid,u8 rprt_id,u8 * buf,size_t len)187b05ff100SMaximilian Luz static int shid_set_feature_report(struct surface_hid_device *shid, u8 rprt_id, u8 *buf, size_t len)
188b05ff100SMaximilian Luz {
189b05ff100SMaximilian Luz 	int status;
190b05ff100SMaximilian Luz 
191b05ff100SMaximilian Luz 	status = ssam_hid_set_raw_report(shid, rprt_id, true, buf, len);
192b05ff100SMaximilian Luz 	return status >= 0 ? len : status;
193b05ff100SMaximilian Luz }
194b05ff100SMaximilian Luz 
195b05ff100SMaximilian Luz 
196b05ff100SMaximilian Luz /* -- Driver setup. --------------------------------------------------------- */
197b05ff100SMaximilian Luz 
surface_hid_probe(struct ssam_device * sdev)198b05ff100SMaximilian Luz static int surface_hid_probe(struct ssam_device *sdev)
199b05ff100SMaximilian Luz {
200b05ff100SMaximilian Luz 	struct surface_hid_device *shid;
201b05ff100SMaximilian Luz 
202b05ff100SMaximilian Luz 	shid = devm_kzalloc(&sdev->dev, sizeof(*shid), GFP_KERNEL);
203b05ff100SMaximilian Luz 	if (!shid)
204b05ff100SMaximilian Luz 		return -ENOMEM;
205b05ff100SMaximilian Luz 
206b05ff100SMaximilian Luz 	shid->dev = &sdev->dev;
207b05ff100SMaximilian Luz 	shid->ctrl = sdev->ctrl;
208b05ff100SMaximilian Luz 	shid->uid = sdev->uid;
209b05ff100SMaximilian Luz 
210b05ff100SMaximilian Luz 	shid->notif.base.priority = 1;
211b05ff100SMaximilian Luz 	shid->notif.base.fn = ssam_hid_event_fn;
212dc0fd0acSMaximilian Luz 	shid->notif.event.reg = SSAM_EVENT_REGISTRY_REG(sdev->uid.target);
213b05ff100SMaximilian Luz 	shid->notif.event.id.target_category = sdev->uid.category;
214b05ff100SMaximilian Luz 	shid->notif.event.id.instance = sdev->uid.instance;
215b05ff100SMaximilian Luz 	shid->notif.event.mask = SSAM_EVENT_MASK_STRICT;
216b05ff100SMaximilian Luz 	shid->notif.event.flags = 0;
217b05ff100SMaximilian Luz 
218b05ff100SMaximilian Luz 	shid->ops.get_descriptor = ssam_hid_get_descriptor;
219b05ff100SMaximilian Luz 	shid->ops.output_report = shid_output_report;
220b05ff100SMaximilian Luz 	shid->ops.get_feature_report = shid_get_feature_report;
221b05ff100SMaximilian Luz 	shid->ops.set_feature_report = shid_set_feature_report;
222b05ff100SMaximilian Luz 
223b05ff100SMaximilian Luz 	ssam_device_set_drvdata(sdev, shid);
224b05ff100SMaximilian Luz 	return surface_hid_device_add(shid);
225b05ff100SMaximilian Luz }
226b05ff100SMaximilian Luz 
surface_hid_remove(struct ssam_device * sdev)227b05ff100SMaximilian Luz static void surface_hid_remove(struct ssam_device *sdev)
228b05ff100SMaximilian Luz {
229b05ff100SMaximilian Luz 	surface_hid_device_destroy(ssam_device_get_drvdata(sdev));
230b05ff100SMaximilian Luz }
231b05ff100SMaximilian Luz 
232b05ff100SMaximilian Luz static const struct ssam_device_id surface_hid_match[] = {
23378abf1b5SMaximilian Luz 	{ SSAM_SDEV(HID, ANY, SSAM_SSH_IID_ANY, 0x00) },
234b05ff100SMaximilian Luz 	{ },
235b05ff100SMaximilian Luz };
236b05ff100SMaximilian Luz MODULE_DEVICE_TABLE(ssam, surface_hid_match);
237b05ff100SMaximilian Luz 
238b05ff100SMaximilian Luz static struct ssam_device_driver surface_hid_driver = {
239b05ff100SMaximilian Luz 	.probe = surface_hid_probe,
240b05ff100SMaximilian Luz 	.remove = surface_hid_remove,
241b05ff100SMaximilian Luz 	.match_table = surface_hid_match,
242b05ff100SMaximilian Luz 	.driver = {
243b05ff100SMaximilian Luz 		.name = "surface_hid",
244b05ff100SMaximilian Luz 		.pm = &surface_hid_pm_ops,
245b05ff100SMaximilian Luz 		.probe_type = PROBE_PREFER_ASYNCHRONOUS,
246b05ff100SMaximilian Luz 	},
247b05ff100SMaximilian Luz };
248b05ff100SMaximilian Luz module_ssam_device_driver(surface_hid_driver);
249b05ff100SMaximilian Luz 
250b05ff100SMaximilian Luz MODULE_AUTHOR("Blaž Hrastnik <blaz@mxxn.io>");
251b05ff100SMaximilian Luz MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>");
252b05ff100SMaximilian Luz MODULE_DESCRIPTION("HID transport driver for Surface System Aggregator Module");
253b05ff100SMaximilian Luz MODULE_LICENSE("GPL");
254