1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * Topstar Laptop ACPI Extras driver |
4 | * |
5 | * Copyright (c) 2009 Herton Ronaldo Krzesinski <herton@mandriva.com.br> |
6 | * Copyright (c) 2018 Guillaume Douézan-Grard |
7 | * |
8 | * Implementation inspired by existing x86 platform drivers, in special |
9 | * asus/eepc/fujitsu-laptop, thanks to their authors. |
10 | */ |
11 | |
12 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
13 | |
14 | #include <linux/kernel.h> |
15 | #include <linux/module.h> |
16 | #include <linux/init.h> |
17 | #include <linux/slab.h> |
18 | #include <linux/acpi.h> |
19 | #include <linux/dmi.h> |
20 | #include <linux/input.h> |
21 | #include <linux/input/sparse-keymap.h> |
22 | #include <linux/leds.h> |
23 | #include <linux/platform_device.h> |
24 | |
25 | #define TOPSTAR_LAPTOP_CLASS "topstar" |
26 | |
27 | struct topstar_laptop { |
28 | struct acpi_device *device; |
29 | struct platform_device *platform; |
30 | struct input_dev *input; |
31 | struct led_classdev led; |
32 | }; |
33 | |
34 | /* |
35 | * LED |
36 | */ |
37 | |
38 | static enum led_brightness topstar_led_get(struct led_classdev *led) |
39 | { |
40 | return led->brightness; |
41 | } |
42 | |
43 | static int topstar_led_set(struct led_classdev *led, |
44 | enum led_brightness state) |
45 | { |
46 | struct topstar_laptop *topstar = container_of(led, |
47 | struct topstar_laptop, led); |
48 | |
49 | struct acpi_object_list params; |
50 | union acpi_object in_obj; |
51 | unsigned long long int ret; |
52 | acpi_status status; |
53 | |
54 | params.count = 1; |
55 | params.pointer = &in_obj; |
56 | in_obj.type = ACPI_TYPE_INTEGER; |
57 | in_obj.integer.value = 0x83; |
58 | |
59 | /* |
60 | * Topstar ACPI returns 0x30001 when the LED is ON and 0x30000 when it |
61 | * is OFF. |
62 | */ |
63 | status = acpi_evaluate_integer(handle: topstar->device->handle, |
64 | pathname: "GETX" , arguments: ¶ms, data: &ret); |
65 | if (ACPI_FAILURE(status)) |
66 | return -1; |
67 | |
68 | /* |
69 | * FNCX(0x83) toggles the LED (more precisely, it is supposed to |
70 | * act as an hardware switch and disconnect the WLAN adapter but |
71 | * it seems to be faulty on some models like the Topstar U931 |
72 | * Notebook). |
73 | */ |
74 | if ((ret == 0x30001 && state == LED_OFF) |
75 | || (ret == 0x30000 && state != LED_OFF)) { |
76 | status = acpi_execute_simple_method(handle: topstar->device->handle, |
77 | method: "FNCX" , arg: 0x83); |
78 | if (ACPI_FAILURE(status)) |
79 | return -1; |
80 | } |
81 | |
82 | return 0; |
83 | } |
84 | |
85 | static int topstar_led_init(struct topstar_laptop *topstar) |
86 | { |
87 | topstar->led = (struct led_classdev) { |
88 | .default_trigger = "rfkill0" , |
89 | .brightness_get = topstar_led_get, |
90 | .brightness_set_blocking = topstar_led_set, |
91 | .name = TOPSTAR_LAPTOP_CLASS "::wlan" , |
92 | }; |
93 | |
94 | return led_classdev_register(parent: &topstar->platform->dev, led_cdev: &topstar->led); |
95 | } |
96 | |
97 | static void topstar_led_exit(struct topstar_laptop *topstar) |
98 | { |
99 | led_classdev_unregister(led_cdev: &topstar->led); |
100 | } |
101 | |
102 | /* |
103 | * Input |
104 | */ |
105 | |
106 | static const struct key_entry topstar_keymap[] = { |
107 | { KE_KEY, 0x80, { KEY_BRIGHTNESSUP } }, |
108 | { KE_KEY, 0x81, { KEY_BRIGHTNESSDOWN } }, |
109 | { KE_KEY, 0x83, { KEY_VOLUMEUP } }, |
110 | { KE_KEY, 0x84, { KEY_VOLUMEDOWN } }, |
111 | { KE_KEY, 0x85, { KEY_MUTE } }, |
112 | { KE_KEY, 0x86, { KEY_SWITCHVIDEOMODE } }, |
113 | { KE_KEY, 0x87, { KEY_F13 } }, /* touchpad enable/disable key */ |
114 | { KE_KEY, 0x88, { KEY_WLAN } }, |
115 | { KE_KEY, 0x8a, { KEY_WWW } }, |
116 | { KE_KEY, 0x8b, { KEY_MAIL } }, |
117 | { KE_KEY, 0x8c, { KEY_MEDIA } }, |
118 | |
119 | /* Known non hotkey events don't handled or that we don't care yet */ |
120 | { KE_IGNORE, 0x82, }, /* backlight event */ |
121 | { KE_IGNORE, 0x8e, }, |
122 | { KE_IGNORE, 0x8f, }, |
123 | { KE_IGNORE, 0x90, }, |
124 | |
125 | /* |
126 | * 'G key' generate two event codes, convert to only |
127 | * one event/key code for now, consider replacing by |
128 | * a switch (3G switch - SW_3G?) |
129 | */ |
130 | { KE_KEY, 0x96, { KEY_F14 } }, |
131 | { KE_KEY, 0x97, { KEY_F14 } }, |
132 | |
133 | { KE_END, 0 } |
134 | }; |
135 | |
136 | static void topstar_input_notify(struct topstar_laptop *topstar, int event) |
137 | { |
138 | if (!sparse_keymap_report_event(dev: topstar->input, code: event, value: 1, autorelease: true)) |
139 | pr_info("unknown event = 0x%02x\n" , event); |
140 | } |
141 | |
142 | static int topstar_input_init(struct topstar_laptop *topstar) |
143 | { |
144 | struct input_dev *input; |
145 | int err; |
146 | |
147 | input = input_allocate_device(); |
148 | if (!input) |
149 | return -ENOMEM; |
150 | |
151 | input->name = "Topstar Laptop extra buttons" ; |
152 | input->phys = TOPSTAR_LAPTOP_CLASS "/input0" ; |
153 | input->id.bustype = BUS_HOST; |
154 | input->dev.parent = &topstar->platform->dev; |
155 | |
156 | err = sparse_keymap_setup(dev: input, keymap: topstar_keymap, NULL); |
157 | if (err) { |
158 | pr_err("Unable to setup input device keymap\n" ); |
159 | goto err_free_dev; |
160 | } |
161 | |
162 | err = input_register_device(input); |
163 | if (err) { |
164 | pr_err("Unable to register input device\n" ); |
165 | goto err_free_dev; |
166 | } |
167 | |
168 | topstar->input = input; |
169 | return 0; |
170 | |
171 | err_free_dev: |
172 | input_free_device(dev: input); |
173 | return err; |
174 | } |
175 | |
176 | static void topstar_input_exit(struct topstar_laptop *topstar) |
177 | { |
178 | input_unregister_device(topstar->input); |
179 | } |
180 | |
181 | /* |
182 | * Platform |
183 | */ |
184 | |
185 | static struct platform_driver topstar_platform_driver = { |
186 | .driver = { |
187 | .name = TOPSTAR_LAPTOP_CLASS, |
188 | }, |
189 | }; |
190 | |
191 | static int topstar_platform_init(struct topstar_laptop *topstar) |
192 | { |
193 | int err; |
194 | |
195 | topstar->platform = platform_device_alloc(TOPSTAR_LAPTOP_CLASS, PLATFORM_DEVID_NONE); |
196 | if (!topstar->platform) |
197 | return -ENOMEM; |
198 | |
199 | platform_set_drvdata(pdev: topstar->platform, data: topstar); |
200 | |
201 | err = platform_device_add(pdev: topstar->platform); |
202 | if (err) |
203 | goto err_device_put; |
204 | |
205 | return 0; |
206 | |
207 | err_device_put: |
208 | platform_device_put(pdev: topstar->platform); |
209 | return err; |
210 | } |
211 | |
212 | static void topstar_platform_exit(struct topstar_laptop *topstar) |
213 | { |
214 | platform_device_unregister(topstar->platform); |
215 | } |
216 | |
217 | /* |
218 | * ACPI |
219 | */ |
220 | |
221 | static int topstar_acpi_fncx_switch(struct acpi_device *device, bool state) |
222 | { |
223 | acpi_status status; |
224 | u64 arg = state ? 0x86 : 0x87; |
225 | |
226 | status = acpi_execute_simple_method(handle: device->handle, method: "FNCX" , arg); |
227 | if (ACPI_FAILURE(status)) { |
228 | pr_err("Unable to switch FNCX notifications\n" ); |
229 | return -ENODEV; |
230 | } |
231 | |
232 | return 0; |
233 | } |
234 | |
235 | static void topstar_acpi_notify(struct acpi_device *device, u32 event) |
236 | { |
237 | struct topstar_laptop *topstar = acpi_driver_data(d: device); |
238 | static bool dup_evnt[2]; |
239 | bool *dup; |
240 | |
241 | /* 0x83 and 0x84 key events comes duplicated... */ |
242 | if (event == 0x83 || event == 0x84) { |
243 | dup = &dup_evnt[event - 0x83]; |
244 | if (*dup) { |
245 | *dup = false; |
246 | return; |
247 | } |
248 | *dup = true; |
249 | } |
250 | |
251 | topstar_input_notify(topstar, event); |
252 | } |
253 | |
254 | static int topstar_acpi_init(struct topstar_laptop *topstar) |
255 | { |
256 | return topstar_acpi_fncx_switch(device: topstar->device, state: true); |
257 | } |
258 | |
259 | static void topstar_acpi_exit(struct topstar_laptop *topstar) |
260 | { |
261 | topstar_acpi_fncx_switch(device: topstar->device, state: false); |
262 | } |
263 | |
264 | /* |
265 | * Enable software-based WLAN LED control on systems with defective |
266 | * hardware switch. |
267 | */ |
268 | static bool led_workaround; |
269 | |
270 | static int dmi_led_workaround(const struct dmi_system_id *id) |
271 | { |
272 | led_workaround = true; |
273 | return 0; |
274 | } |
275 | |
276 | static const struct dmi_system_id topstar_dmi_ids[] = { |
277 | { |
278 | .callback = dmi_led_workaround, |
279 | .ident = "Topstar U931/RVP7" , |
280 | .matches = { |
281 | DMI_MATCH(DMI_BOARD_NAME, "U931" ), |
282 | DMI_MATCH(DMI_BOARD_VERSION, "RVP7" ), |
283 | }, |
284 | }, |
285 | {} |
286 | }; |
287 | |
288 | static int topstar_acpi_add(struct acpi_device *device) |
289 | { |
290 | struct topstar_laptop *topstar; |
291 | int err; |
292 | |
293 | dmi_check_system(list: topstar_dmi_ids); |
294 | |
295 | topstar = kzalloc(size: sizeof(struct topstar_laptop), GFP_KERNEL); |
296 | if (!topstar) |
297 | return -ENOMEM; |
298 | |
299 | strcpy(acpi_device_name(device), q: "Topstar TPSACPI" ); |
300 | strcpy(acpi_device_class(device), TOPSTAR_LAPTOP_CLASS); |
301 | device->driver_data = topstar; |
302 | topstar->device = device; |
303 | |
304 | err = topstar_acpi_init(topstar); |
305 | if (err) |
306 | goto err_free; |
307 | |
308 | err = topstar_platform_init(topstar); |
309 | if (err) |
310 | goto err_acpi_exit; |
311 | |
312 | err = topstar_input_init(topstar); |
313 | if (err) |
314 | goto err_platform_exit; |
315 | |
316 | if (led_workaround) { |
317 | err = topstar_led_init(topstar); |
318 | if (err) |
319 | goto err_input_exit; |
320 | } |
321 | |
322 | return 0; |
323 | |
324 | err_input_exit: |
325 | topstar_input_exit(topstar); |
326 | err_platform_exit: |
327 | topstar_platform_exit(topstar); |
328 | err_acpi_exit: |
329 | topstar_acpi_exit(topstar); |
330 | err_free: |
331 | kfree(objp: topstar); |
332 | return err; |
333 | } |
334 | |
335 | static void topstar_acpi_remove(struct acpi_device *device) |
336 | { |
337 | struct topstar_laptop *topstar = acpi_driver_data(d: device); |
338 | |
339 | if (led_workaround) |
340 | topstar_led_exit(topstar); |
341 | |
342 | topstar_input_exit(topstar); |
343 | topstar_platform_exit(topstar); |
344 | topstar_acpi_exit(topstar); |
345 | |
346 | kfree(objp: topstar); |
347 | } |
348 | |
349 | static const struct acpi_device_id topstar_device_ids[] = { |
350 | { "TPS0001" , 0 }, |
351 | { "TPSACPI01" , 0 }, |
352 | { "" , 0 }, |
353 | }; |
354 | MODULE_DEVICE_TABLE(acpi, topstar_device_ids); |
355 | |
356 | static struct acpi_driver topstar_acpi_driver = { |
357 | .name = "Topstar laptop ACPI driver" , |
358 | .class = TOPSTAR_LAPTOP_CLASS, |
359 | .ids = topstar_device_ids, |
360 | .ops = { |
361 | .add = topstar_acpi_add, |
362 | .remove = topstar_acpi_remove, |
363 | .notify = topstar_acpi_notify, |
364 | }, |
365 | }; |
366 | |
367 | static int __init topstar_laptop_init(void) |
368 | { |
369 | int ret; |
370 | |
371 | ret = platform_driver_register(&topstar_platform_driver); |
372 | if (ret < 0) |
373 | return ret; |
374 | |
375 | ret = acpi_bus_register_driver(driver: &topstar_acpi_driver); |
376 | if (ret < 0) |
377 | goto err_driver_unreg; |
378 | |
379 | pr_info("ACPI extras driver loaded\n" ); |
380 | return 0; |
381 | |
382 | err_driver_unreg: |
383 | platform_driver_unregister(&topstar_platform_driver); |
384 | return ret; |
385 | } |
386 | |
387 | static void __exit topstar_laptop_exit(void) |
388 | { |
389 | acpi_bus_unregister_driver(driver: &topstar_acpi_driver); |
390 | platform_driver_unregister(&topstar_platform_driver); |
391 | } |
392 | |
393 | module_init(topstar_laptop_init); |
394 | module_exit(topstar_laptop_exit); |
395 | |
396 | MODULE_AUTHOR("Herton Ronaldo Krzesinski" ); |
397 | MODULE_AUTHOR("Guillaume Douézan-Grard" ); |
398 | MODULE_DESCRIPTION("Topstar Laptop ACPI Extras driver" ); |
399 | MODULE_LICENSE("GPL" ); |
400 | |