1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Surface GPE/Lid driver to enable wakeup from suspend via the lid by |
4 | * properly configuring the respective GPEs. Required for wakeup via lid on |
5 | * newer Intel-based Microsoft Surface devices. |
6 | * |
7 | * Copyright (C) 2020-2022 Maximilian Luz <luzmaximilian@gmail.com> |
8 | */ |
9 | |
10 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
11 | |
12 | #include <linux/acpi.h> |
13 | #include <linux/dmi.h> |
14 | #include <linux/kernel.h> |
15 | #include <linux/module.h> |
16 | #include <linux/platform_device.h> |
17 | |
18 | /* |
19 | * Note: The GPE numbers for the lid devices found below have been obtained |
20 | * from ACPI/the DSDT table, specifically from the GPE handler for the |
21 | * lid. |
22 | */ |
23 | |
24 | static const struct property_entry lid_device_props_l17[] = { |
25 | PROPERTY_ENTRY_U32("gpe" , 0x17), |
26 | {}, |
27 | }; |
28 | |
29 | static const struct property_entry lid_device_props_l4B[] = { |
30 | PROPERTY_ENTRY_U32("gpe" , 0x4B), |
31 | {}, |
32 | }; |
33 | |
34 | static const struct property_entry lid_device_props_l4D[] = { |
35 | PROPERTY_ENTRY_U32("gpe" , 0x4D), |
36 | {}, |
37 | }; |
38 | |
39 | static const struct property_entry lid_device_props_l4F[] = { |
40 | PROPERTY_ENTRY_U32("gpe" , 0x4F), |
41 | {}, |
42 | }; |
43 | |
44 | static const struct property_entry lid_device_props_l57[] = { |
45 | PROPERTY_ENTRY_U32("gpe" , 0x57), |
46 | {}, |
47 | }; |
48 | |
49 | /* |
50 | * Note: When changing this, don't forget to check that the MODULE_ALIAS below |
51 | * still fits. |
52 | */ |
53 | static const struct dmi_system_id dmi_lid_device_table[] = { |
54 | { |
55 | .ident = "Surface Pro 4" , |
56 | .matches = { |
57 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
58 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 4" ), |
59 | }, |
60 | .driver_data = (void *)lid_device_props_l17, |
61 | }, |
62 | { |
63 | .ident = "Surface Pro 5" , |
64 | .matches = { |
65 | /* |
66 | * We match for SKU here due to generic product name |
67 | * "Surface Pro". |
68 | */ |
69 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
70 | DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Pro_1796" ), |
71 | }, |
72 | .driver_data = (void *)lid_device_props_l4F, |
73 | }, |
74 | { |
75 | .ident = "Surface Pro 5 (LTE)" , |
76 | .matches = { |
77 | /* |
78 | * We match for SKU here due to generic product name |
79 | * "Surface Pro" |
80 | */ |
81 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
82 | DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Pro_1807" ), |
83 | }, |
84 | .driver_data = (void *)lid_device_props_l4F, |
85 | }, |
86 | { |
87 | .ident = "Surface Pro 6" , |
88 | .matches = { |
89 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
90 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 6" ), |
91 | }, |
92 | .driver_data = (void *)lid_device_props_l4F, |
93 | }, |
94 | { |
95 | .ident = "Surface Pro 7" , |
96 | .matches = { |
97 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
98 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 7" ), |
99 | }, |
100 | .driver_data = (void *)lid_device_props_l4D, |
101 | }, |
102 | { |
103 | .ident = "Surface Pro 8" , |
104 | .matches = { |
105 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
106 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Pro 8" ), |
107 | }, |
108 | .driver_data = (void *)lid_device_props_l4B, |
109 | }, |
110 | { |
111 | .ident = "Surface Book 1" , |
112 | .matches = { |
113 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
114 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book" ), |
115 | }, |
116 | .driver_data = (void *)lid_device_props_l17, |
117 | }, |
118 | { |
119 | .ident = "Surface Book 2" , |
120 | .matches = { |
121 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
122 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book 2" ), |
123 | }, |
124 | .driver_data = (void *)lid_device_props_l17, |
125 | }, |
126 | { |
127 | .ident = "Surface Book 3" , |
128 | .matches = { |
129 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
130 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Book 3" ), |
131 | }, |
132 | .driver_data = (void *)lid_device_props_l4D, |
133 | }, |
134 | { |
135 | .ident = "Surface Laptop 1" , |
136 | .matches = { |
137 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
138 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop" ), |
139 | }, |
140 | .driver_data = (void *)lid_device_props_l57, |
141 | }, |
142 | { |
143 | .ident = "Surface Laptop 2" , |
144 | .matches = { |
145 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
146 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop 2" ), |
147 | }, |
148 | .driver_data = (void *)lid_device_props_l57, |
149 | }, |
150 | { |
151 | .ident = "Surface Laptop 3 (Intel 13\")" , |
152 | .matches = { |
153 | /* |
154 | * We match for SKU here due to different variants: The |
155 | * AMD (15") version does not rely on GPEs. |
156 | */ |
157 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
158 | DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_3_1867:1868" ), |
159 | }, |
160 | .driver_data = (void *)lid_device_props_l4D, |
161 | }, |
162 | { |
163 | .ident = "Surface Laptop 3 (Intel 15\")" , |
164 | .matches = { |
165 | /* |
166 | * We match for SKU here due to different variants: The |
167 | * AMD (15") version does not rely on GPEs. |
168 | */ |
169 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
170 | DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_3_1872" ), |
171 | }, |
172 | .driver_data = (void *)lid_device_props_l4D, |
173 | }, |
174 | { |
175 | .ident = "Surface Laptop 4 (Intel 13\")" , |
176 | .matches = { |
177 | /* |
178 | * We match for SKU here due to different variants: The |
179 | * AMD (15") version does not rely on GPEs. |
180 | */ |
181 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
182 | DMI_EXACT_MATCH(DMI_PRODUCT_SKU, "Surface_Laptop_4_1950:1951" ), |
183 | }, |
184 | .driver_data = (void *)lid_device_props_l4B, |
185 | }, |
186 | { |
187 | .ident = "Surface Laptop Studio" , |
188 | .matches = { |
189 | DMI_EXACT_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
190 | DMI_EXACT_MATCH(DMI_PRODUCT_NAME, "Surface Laptop Studio" ), |
191 | }, |
192 | .driver_data = (void *)lid_device_props_l4B, |
193 | }, |
194 | { } |
195 | }; |
196 | |
197 | struct surface_lid_device { |
198 | u32 gpe_number; |
199 | }; |
200 | |
201 | static int surface_lid_enable_wakeup(struct device *dev, bool enable) |
202 | { |
203 | const struct surface_lid_device *lid = dev_get_drvdata(dev); |
204 | int action = enable ? ACPI_GPE_ENABLE : ACPI_GPE_DISABLE; |
205 | acpi_status status; |
206 | |
207 | status = acpi_set_gpe_wake_mask(NULL, gpe_number: lid->gpe_number, action); |
208 | if (ACPI_FAILURE(status)) { |
209 | dev_err(dev, "failed to set GPE wake mask: %s\n" , |
210 | acpi_format_exception(status)); |
211 | return -EINVAL; |
212 | } |
213 | |
214 | return 0; |
215 | } |
216 | |
217 | static int __maybe_unused surface_gpe_suspend(struct device *dev) |
218 | { |
219 | return surface_lid_enable_wakeup(dev, enable: true); |
220 | } |
221 | |
222 | static int __maybe_unused surface_gpe_resume(struct device *dev) |
223 | { |
224 | return surface_lid_enable_wakeup(dev, enable: false); |
225 | } |
226 | |
227 | static SIMPLE_DEV_PM_OPS(surface_gpe_pm, surface_gpe_suspend, surface_gpe_resume); |
228 | |
229 | static int surface_gpe_probe(struct platform_device *pdev) |
230 | { |
231 | struct surface_lid_device *lid; |
232 | u32 gpe_number; |
233 | acpi_status status; |
234 | int ret; |
235 | |
236 | ret = device_property_read_u32(dev: &pdev->dev, propname: "gpe" , val: &gpe_number); |
237 | if (ret) { |
238 | dev_err(&pdev->dev, "failed to read 'gpe' property: %d\n" , ret); |
239 | return ret; |
240 | } |
241 | |
242 | lid = devm_kzalloc(dev: &pdev->dev, size: sizeof(*lid), GFP_KERNEL); |
243 | if (!lid) |
244 | return -ENOMEM; |
245 | |
246 | lid->gpe_number = gpe_number; |
247 | platform_set_drvdata(pdev, data: lid); |
248 | |
249 | status = acpi_mark_gpe_for_wake(NULL, gpe_number); |
250 | if (ACPI_FAILURE(status)) { |
251 | dev_err(&pdev->dev, "failed to mark GPE for wake: %s\n" , |
252 | acpi_format_exception(status)); |
253 | return -EINVAL; |
254 | } |
255 | |
256 | status = acpi_enable_gpe(NULL, gpe_number); |
257 | if (ACPI_FAILURE(status)) { |
258 | dev_err(&pdev->dev, "failed to enable GPE: %s\n" , |
259 | acpi_format_exception(status)); |
260 | return -EINVAL; |
261 | } |
262 | |
263 | ret = surface_lid_enable_wakeup(dev: &pdev->dev, enable: false); |
264 | if (ret) |
265 | acpi_disable_gpe(NULL, gpe_number); |
266 | |
267 | return ret; |
268 | } |
269 | |
270 | static void surface_gpe_remove(struct platform_device *pdev) |
271 | { |
272 | struct surface_lid_device *lid = dev_get_drvdata(dev: &pdev->dev); |
273 | |
274 | /* restore default behavior without this module */ |
275 | surface_lid_enable_wakeup(dev: &pdev->dev, enable: false); |
276 | acpi_disable_gpe(NULL, gpe_number: lid->gpe_number); |
277 | } |
278 | |
279 | static struct platform_driver surface_gpe_driver = { |
280 | .probe = surface_gpe_probe, |
281 | .remove_new = surface_gpe_remove, |
282 | .driver = { |
283 | .name = "surface_gpe" , |
284 | .pm = &surface_gpe_pm, |
285 | .probe_type = PROBE_PREFER_ASYNCHRONOUS, |
286 | }, |
287 | }; |
288 | |
289 | static struct platform_device *surface_gpe_device; |
290 | |
291 | static int __init surface_gpe_init(void) |
292 | { |
293 | const struct dmi_system_id *match; |
294 | struct platform_device *pdev; |
295 | struct fwnode_handle *fwnode; |
296 | int status; |
297 | |
298 | match = dmi_first_match(list: dmi_lid_device_table); |
299 | if (!match) { |
300 | pr_info("no compatible Microsoft Surface device found, exiting\n" ); |
301 | return -ENODEV; |
302 | } |
303 | |
304 | status = platform_driver_register(&surface_gpe_driver); |
305 | if (status) |
306 | return status; |
307 | |
308 | fwnode = fwnode_create_software_node(properties: match->driver_data, NULL); |
309 | if (IS_ERR(ptr: fwnode)) { |
310 | status = PTR_ERR(ptr: fwnode); |
311 | goto err_node; |
312 | } |
313 | |
314 | pdev = platform_device_alloc(name: "surface_gpe" , PLATFORM_DEVID_NONE); |
315 | if (!pdev) { |
316 | status = -ENOMEM; |
317 | goto err_alloc; |
318 | } |
319 | |
320 | pdev->dev.fwnode = fwnode; |
321 | |
322 | status = platform_device_add(pdev); |
323 | if (status) |
324 | goto err_add; |
325 | |
326 | surface_gpe_device = pdev; |
327 | return 0; |
328 | |
329 | err_add: |
330 | platform_device_put(pdev); |
331 | err_alloc: |
332 | fwnode_remove_software_node(fwnode); |
333 | err_node: |
334 | platform_driver_unregister(&surface_gpe_driver); |
335 | return status; |
336 | } |
337 | module_init(surface_gpe_init); |
338 | |
339 | static void __exit surface_gpe_exit(void) |
340 | { |
341 | struct fwnode_handle *fwnode = surface_gpe_device->dev.fwnode; |
342 | |
343 | platform_device_unregister(surface_gpe_device); |
344 | platform_driver_unregister(&surface_gpe_driver); |
345 | fwnode_remove_software_node(fwnode); |
346 | } |
347 | module_exit(surface_gpe_exit); |
348 | |
349 | MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>" ); |
350 | MODULE_DESCRIPTION("Surface GPE/Lid Driver" ); |
351 | MODULE_LICENSE("GPL" ); |
352 | MODULE_ALIAS("dmi:*:svnMicrosoftCorporation:pnSurface*:*" ); |
353 | |