1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Driver for the LID cover switch of the Surface 3 |
4 | * |
5 | * Copyright (c) 2016 Red Hat Inc. |
6 | */ |
7 | |
8 | |
9 | #include <linux/kernel.h> |
10 | #include <linux/module.h> |
11 | #include <linux/slab.h> |
12 | |
13 | #include <linux/acpi.h> |
14 | #include <linux/dmi.h> |
15 | #include <linux/input.h> |
16 | #include <linux/mutex.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/spi/spi.h> |
19 | |
20 | MODULE_AUTHOR("Benjamin Tissoires <benjamin.tissoires@redhat.com>" ); |
21 | MODULE_DESCRIPTION("Surface 3 platform driver" ); |
22 | MODULE_LICENSE("GPL" ); |
23 | |
24 | #define ACPI_BUTTON_HID_LID "PNP0C0D" |
25 | #define SPI_CTL_OBJ_NAME "SPI" |
26 | #define SPI_TS_OBJ_NAME "NTRG" |
27 | |
28 | #define SURFACE3_LID_GUID "F7CC25EC-D20B-404C-8903-0ED4359C18AE" |
29 | |
30 | MODULE_ALIAS("wmi:" SURFACE3_LID_GUID); |
31 | |
32 | static const struct dmi_system_id surface3_dmi_table[] = { |
33 | #if defined(CONFIG_X86) |
34 | { |
35 | .matches = { |
36 | DMI_MATCH(DMI_SYS_VENDOR, "Microsoft Corporation" ), |
37 | DMI_MATCH(DMI_PRODUCT_NAME, "Surface 3" ), |
38 | }, |
39 | }, |
40 | #endif |
41 | { } |
42 | }; |
43 | |
44 | struct surface3_wmi { |
45 | struct acpi_device *touchscreen_adev; |
46 | struct acpi_device *pnp0c0d_adev; |
47 | struct acpi_hotplug_context hp; |
48 | struct input_dev *input; |
49 | }; |
50 | |
51 | static struct platform_device *s3_wmi_pdev; |
52 | |
53 | static struct surface3_wmi s3_wmi; |
54 | |
55 | static DEFINE_MUTEX(s3_wmi_lock); |
56 | |
57 | static int s3_wmi_query_block(const char *guid, int instance, int *ret) |
58 | { |
59 | struct acpi_buffer output = { ACPI_ALLOCATE_BUFFER, NULL }; |
60 | union acpi_object *obj = NULL; |
61 | acpi_status status; |
62 | int error = 0; |
63 | |
64 | mutex_lock(&s3_wmi_lock); |
65 | status = wmi_query_block(guid, instance, out: &output); |
66 | if (ACPI_FAILURE(status)) { |
67 | error = -EIO; |
68 | goto out_free_unlock; |
69 | } |
70 | |
71 | obj = output.pointer; |
72 | |
73 | if (!obj || obj->type != ACPI_TYPE_INTEGER) { |
74 | if (obj) { |
75 | pr_err("query block returned object type: %d - buffer length:%d\n" , |
76 | obj->type, |
77 | obj->type == ACPI_TYPE_BUFFER ? |
78 | obj->buffer.length : 0); |
79 | } |
80 | error = -EINVAL; |
81 | goto out_free_unlock; |
82 | } |
83 | *ret = obj->integer.value; |
84 | out_free_unlock: |
85 | kfree(objp: obj); |
86 | mutex_unlock(lock: &s3_wmi_lock); |
87 | return error; |
88 | } |
89 | |
90 | static inline int s3_wmi_query_lid(int *ret) |
91 | { |
92 | return s3_wmi_query_block(SURFACE3_LID_GUID, instance: 0, ret); |
93 | } |
94 | |
95 | static int s3_wmi_send_lid_state(void) |
96 | { |
97 | int ret, lid_sw; |
98 | |
99 | ret = s3_wmi_query_lid(ret: &lid_sw); |
100 | if (ret) |
101 | return ret; |
102 | |
103 | input_report_switch(dev: s3_wmi.input, SW_LID, value: lid_sw); |
104 | input_sync(dev: s3_wmi.input); |
105 | |
106 | return 0; |
107 | } |
108 | |
109 | static int s3_wmi_hp_notify(struct acpi_device *adev, u32 value) |
110 | { |
111 | return s3_wmi_send_lid_state(); |
112 | } |
113 | |
114 | static acpi_status s3_wmi_attach_spi_device(acpi_handle handle, |
115 | u32 level, |
116 | void *data, |
117 | void **return_value) |
118 | { |
119 | struct acpi_device *adev = acpi_fetch_acpi_dev(handle); |
120 | struct acpi_device **ts_adev = data; |
121 | |
122 | if (!adev || strncmp(acpi_device_bid(adev), SPI_TS_OBJ_NAME, |
123 | strlen(SPI_TS_OBJ_NAME))) |
124 | return AE_OK; |
125 | |
126 | if (*ts_adev) { |
127 | pr_err("duplicate entry %s\n" , SPI_TS_OBJ_NAME); |
128 | return AE_OK; |
129 | } |
130 | |
131 | *ts_adev = adev; |
132 | |
133 | return AE_OK; |
134 | } |
135 | |
136 | static int s3_wmi_check_platform_device(struct device *dev, void *data) |
137 | { |
138 | struct acpi_device *adev = ACPI_COMPANION(dev); |
139 | struct acpi_device *ts_adev = NULL; |
140 | acpi_status status; |
141 | |
142 | /* ignore non ACPI devices */ |
143 | if (!adev) |
144 | return 0; |
145 | |
146 | /* check for LID ACPI switch */ |
147 | if (!strcmp(ACPI_BUTTON_HID_LID, acpi_device_hid(device: adev))) { |
148 | s3_wmi.pnp0c0d_adev = adev; |
149 | return 0; |
150 | } |
151 | |
152 | /* ignore non SPI controllers */ |
153 | if (strncmp(acpi_device_bid(adev), SPI_CTL_OBJ_NAME, |
154 | strlen(SPI_CTL_OBJ_NAME))) |
155 | return 0; |
156 | |
157 | status = acpi_walk_namespace(ACPI_TYPE_DEVICE, start_object: adev->handle, max_depth: 1, |
158 | descending_callback: s3_wmi_attach_spi_device, NULL, |
159 | context: &ts_adev, NULL); |
160 | if (ACPI_FAILURE(status)) |
161 | dev_warn(dev, "failed to enumerate SPI slaves\n" ); |
162 | |
163 | if (!ts_adev) |
164 | return 0; |
165 | |
166 | s3_wmi.touchscreen_adev = ts_adev; |
167 | |
168 | return 0; |
169 | } |
170 | |
171 | static int s3_wmi_create_and_register_input(struct platform_device *pdev) |
172 | { |
173 | struct input_dev *input; |
174 | int error; |
175 | |
176 | input = devm_input_allocate_device(&pdev->dev); |
177 | if (!input) |
178 | return -ENOMEM; |
179 | |
180 | input->name = "Lid Switch" ; |
181 | input->phys = "button/input0" ; |
182 | input->id.bustype = BUS_HOST; |
183 | input->id.product = 0x0005; |
184 | |
185 | input_set_capability(dev: input, EV_SW, SW_LID); |
186 | |
187 | error = input_register_device(input); |
188 | if (error) |
189 | return error; |
190 | |
191 | s3_wmi.input = input; |
192 | |
193 | return 0; |
194 | } |
195 | |
196 | static int __init s3_wmi_probe(struct platform_device *pdev) |
197 | { |
198 | int error; |
199 | |
200 | if (!dmi_check_system(list: surface3_dmi_table)) |
201 | return -ENODEV; |
202 | |
203 | memset(&s3_wmi, 0, sizeof(s3_wmi)); |
204 | |
205 | bus_for_each_dev(bus: &platform_bus_type, NULL, NULL, |
206 | fn: s3_wmi_check_platform_device); |
207 | |
208 | if (!s3_wmi.touchscreen_adev) |
209 | return -ENODEV; |
210 | |
211 | acpi_bus_trim(start: s3_wmi.pnp0c0d_adev); |
212 | |
213 | error = s3_wmi_create_and_register_input(pdev); |
214 | if (error) |
215 | goto restore_acpi_lid; |
216 | |
217 | acpi_initialize_hp_context(adev: s3_wmi.touchscreen_adev, hp: &s3_wmi.hp, |
218 | notify: s3_wmi_hp_notify, NULL); |
219 | |
220 | s3_wmi_send_lid_state(); |
221 | |
222 | return 0; |
223 | |
224 | restore_acpi_lid: |
225 | acpi_bus_scan(handle: s3_wmi.pnp0c0d_adev->handle); |
226 | return error; |
227 | } |
228 | |
229 | static void s3_wmi_remove(struct platform_device *device) |
230 | { |
231 | /* remove the hotplug context from the acpi device */ |
232 | s3_wmi.touchscreen_adev->hp = NULL; |
233 | |
234 | /* reinstall the actual PNPC0C0D LID default handle */ |
235 | acpi_bus_scan(handle: s3_wmi.pnp0c0d_adev->handle); |
236 | } |
237 | |
238 | static int __maybe_unused s3_wmi_resume(struct device *dev) |
239 | { |
240 | s3_wmi_send_lid_state(); |
241 | return 0; |
242 | } |
243 | static SIMPLE_DEV_PM_OPS(s3_wmi_pm, NULL, s3_wmi_resume); |
244 | |
245 | static struct platform_driver s3_wmi_driver = { |
246 | .driver = { |
247 | .name = "surface3-wmi" , |
248 | .pm = &s3_wmi_pm, |
249 | }, |
250 | .remove_new = s3_wmi_remove, |
251 | }; |
252 | |
253 | static int __init s3_wmi_init(void) |
254 | { |
255 | int error; |
256 | |
257 | s3_wmi_pdev = platform_device_alloc(name: "surface3-wmi" , id: -1); |
258 | if (!s3_wmi_pdev) |
259 | return -ENOMEM; |
260 | |
261 | error = platform_device_add(pdev: s3_wmi_pdev); |
262 | if (error) |
263 | goto err_device_put; |
264 | |
265 | error = platform_driver_probe(&s3_wmi_driver, s3_wmi_probe); |
266 | if (error) |
267 | goto err_device_del; |
268 | |
269 | pr_info("Surface 3 WMI Extras loaded\n" ); |
270 | return 0; |
271 | |
272 | err_device_del: |
273 | platform_device_del(pdev: s3_wmi_pdev); |
274 | err_device_put: |
275 | platform_device_put(pdev: s3_wmi_pdev); |
276 | return error; |
277 | } |
278 | |
279 | static void __exit s3_wmi_exit(void) |
280 | { |
281 | platform_device_unregister(s3_wmi_pdev); |
282 | platform_driver_unregister(&s3_wmi_driver); |
283 | } |
284 | |
285 | module_init(s3_wmi_init); |
286 | module_exit(s3_wmi_exit); |
287 | |