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