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