diff options
Diffstat (limited to 'drivers/platform/x86/ideapad-laptop.c')
-rw-r--r-- | drivers/platform/x86/ideapad-laptop.c | 497 |
1 files changed, 497 insertions, 0 deletions
diff --git a/drivers/platform/x86/ideapad-laptop.c b/drivers/platform/x86/ideapad-laptop.c new file mode 100644 index 000000000000..bfdda33feb26 --- /dev/null +++ b/drivers/platform/x86/ideapad-laptop.c | |||
@@ -0,0 +1,497 @@ | |||
1 | /* | ||
2 | * ideapad-laptop.c - Lenovo IdeaPad ACPI Extras | ||
3 | * | ||
4 | * Copyright © 2010 Intel Corporation | ||
5 | * Copyright © 2010 David Woodhouse <dwmw2@infradead.org> | ||
6 | * | ||
7 | * This program is free software; you can redistribute it and/or modify | ||
8 | * it under the terms of the GNU General Public License as published by | ||
9 | * the Free Software Foundation; either version 2 of the License, or | ||
10 | * (at your option) any later version. | ||
11 | * | ||
12 | * This program is distributed in the hope that it will be useful, | ||
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
15 | * GNU General Public License for more details. | ||
16 | * | ||
17 | * You should have received a copy of the GNU General Public License | ||
18 | * along with this program; if not, write to the Free Software | ||
19 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | ||
20 | * 02110-1301, USA. | ||
21 | */ | ||
22 | |||
23 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | ||
24 | |||
25 | #include <linux/kernel.h> | ||
26 | #include <linux/module.h> | ||
27 | #include <linux/init.h> | ||
28 | #include <linux/types.h> | ||
29 | #include <acpi/acpi_bus.h> | ||
30 | #include <acpi/acpi_drivers.h> | ||
31 | #include <linux/rfkill.h> | ||
32 | #include <linux/platform_device.h> | ||
33 | #include <linux/input.h> | ||
34 | #include <linux/input/sparse-keymap.h> | ||
35 | |||
36 | #define IDEAPAD_RFKILL_DEV_NUM (3) | ||
37 | |||
38 | struct ideapad_private { | ||
39 | struct rfkill *rfk[IDEAPAD_RFKILL_DEV_NUM]; | ||
40 | struct platform_device *platform_device; | ||
41 | struct input_dev *inputdev; | ||
42 | }; | ||
43 | |||
44 | static acpi_handle ideapad_handle; | ||
45 | static bool no_bt_rfkill; | ||
46 | module_param(no_bt_rfkill, bool, 0444); | ||
47 | MODULE_PARM_DESC(no_bt_rfkill, "No rfkill for bluetooth."); | ||
48 | |||
49 | /* | ||
50 | * ACPI Helpers | ||
51 | */ | ||
52 | #define IDEAPAD_EC_TIMEOUT (100) /* in ms */ | ||
53 | |||
54 | static int read_method_int(acpi_handle handle, const char *method, int *val) | ||
55 | { | ||
56 | acpi_status status; | ||
57 | unsigned long long result; | ||
58 | |||
59 | status = acpi_evaluate_integer(handle, (char *)method, NULL, &result); | ||
60 | if (ACPI_FAILURE(status)) { | ||
61 | *val = -1; | ||
62 | return -1; | ||
63 | } else { | ||
64 | *val = result; | ||
65 | return 0; | ||
66 | } | ||
67 | } | ||
68 | |||
69 | static int method_vpcr(acpi_handle handle, int cmd, int *ret) | ||
70 | { | ||
71 | acpi_status status; | ||
72 | unsigned long long result; | ||
73 | struct acpi_object_list params; | ||
74 | union acpi_object in_obj; | ||
75 | |||
76 | params.count = 1; | ||
77 | params.pointer = &in_obj; | ||
78 | in_obj.type = ACPI_TYPE_INTEGER; | ||
79 | in_obj.integer.value = cmd; | ||
80 | |||
81 | status = acpi_evaluate_integer(handle, "VPCR", ¶ms, &result); | ||
82 | |||
83 | if (ACPI_FAILURE(status)) { | ||
84 | *ret = -1; | ||
85 | return -1; | ||
86 | } else { | ||
87 | *ret = result; | ||
88 | return 0; | ||
89 | } | ||
90 | } | ||
91 | |||
92 | static int method_vpcw(acpi_handle handle, int cmd, int data) | ||
93 | { | ||
94 | struct acpi_object_list params; | ||
95 | union acpi_object in_obj[2]; | ||
96 | acpi_status status; | ||
97 | |||
98 | params.count = 2; | ||
99 | params.pointer = in_obj; | ||
100 | in_obj[0].type = ACPI_TYPE_INTEGER; | ||
101 | in_obj[0].integer.value = cmd; | ||
102 | in_obj[1].type = ACPI_TYPE_INTEGER; | ||
103 | in_obj[1].integer.value = data; | ||
104 | |||
105 | status = acpi_evaluate_object(handle, "VPCW", ¶ms, NULL); | ||
106 | if (status != AE_OK) | ||
107 | return -1; | ||
108 | return 0; | ||
109 | } | ||
110 | |||
111 | static int read_ec_data(acpi_handle handle, int cmd, unsigned long *data) | ||
112 | { | ||
113 | int val; | ||
114 | unsigned long int end_jiffies; | ||
115 | |||
116 | if (method_vpcw(handle, 1, cmd)) | ||
117 | return -1; | ||
118 | |||
119 | for (end_jiffies = jiffies+(HZ)*IDEAPAD_EC_TIMEOUT/1000+1; | ||
120 | time_before(jiffies, end_jiffies);) { | ||
121 | schedule(); | ||
122 | if (method_vpcr(handle, 1, &val)) | ||
123 | return -1; | ||
124 | if (val == 0) { | ||
125 | if (method_vpcr(handle, 0, &val)) | ||
126 | return -1; | ||
127 | *data = val; | ||
128 | return 0; | ||
129 | } | ||
130 | } | ||
131 | pr_err("timeout in read_ec_cmd\n"); | ||
132 | return -1; | ||
133 | } | ||
134 | |||
135 | static int write_ec_cmd(acpi_handle handle, int cmd, unsigned long data) | ||
136 | { | ||
137 | int val; | ||
138 | unsigned long int end_jiffies; | ||
139 | |||
140 | if (method_vpcw(handle, 0, data)) | ||
141 | return -1; | ||
142 | if (method_vpcw(handle, 1, cmd)) | ||
143 | return -1; | ||
144 | |||
145 | for (end_jiffies = jiffies+(HZ)*IDEAPAD_EC_TIMEOUT/1000+1; | ||
146 | time_before(jiffies, end_jiffies);) { | ||
147 | schedule(); | ||
148 | if (method_vpcr(handle, 1, &val)) | ||
149 | return -1; | ||
150 | if (val == 0) | ||
151 | return 0; | ||
152 | } | ||
153 | pr_err("timeout in write_ec_cmd\n"); | ||
154 | return -1; | ||
155 | } | ||
156 | |||
157 | /* | ||
158 | * camera power | ||
159 | */ | ||
160 | static ssize_t show_ideapad_cam(struct device *dev, | ||
161 | struct device_attribute *attr, | ||
162 | char *buf) | ||
163 | { | ||
164 | unsigned long result; | ||
165 | |||
166 | if (read_ec_data(ideapad_handle, 0x1D, &result)) | ||
167 | return sprintf(buf, "-1\n"); | ||
168 | return sprintf(buf, "%lu\n", result); | ||
169 | } | ||
170 | |||
171 | static ssize_t store_ideapad_cam(struct device *dev, | ||
172 | struct device_attribute *attr, | ||
173 | const char *buf, size_t count) | ||
174 | { | ||
175 | int ret, state; | ||
176 | |||
177 | if (!count) | ||
178 | return 0; | ||
179 | if (sscanf(buf, "%i", &state) != 1) | ||
180 | return -EINVAL; | ||
181 | ret = write_ec_cmd(ideapad_handle, 0x1E, state); | ||
182 | if (ret < 0) | ||
183 | return ret; | ||
184 | return count; | ||
185 | } | ||
186 | |||
187 | static DEVICE_ATTR(camera_power, 0644, show_ideapad_cam, store_ideapad_cam); | ||
188 | |||
189 | /* | ||
190 | * Rfkill | ||
191 | */ | ||
192 | struct ideapad_rfk_data { | ||
193 | char *name; | ||
194 | int cfgbit; | ||
195 | int opcode; | ||
196 | int type; | ||
197 | }; | ||
198 | |||
199 | const struct ideapad_rfk_data ideapad_rfk_data[] = { | ||
200 | { "ideapad_wlan", 18, 0x15, RFKILL_TYPE_WLAN }, | ||
201 | { "ideapad_bluetooth", 16, 0x17, RFKILL_TYPE_BLUETOOTH }, | ||
202 | { "ideapad_3g", 17, 0x20, RFKILL_TYPE_WWAN }, | ||
203 | }; | ||
204 | |||
205 | static int ideapad_rfk_set(void *data, bool blocked) | ||
206 | { | ||
207 | unsigned long opcode = (unsigned long)data; | ||
208 | |||
209 | return write_ec_cmd(ideapad_handle, opcode, !blocked); | ||
210 | } | ||
211 | |||
212 | static struct rfkill_ops ideapad_rfk_ops = { | ||
213 | .set_block = ideapad_rfk_set, | ||
214 | }; | ||
215 | |||
216 | static void ideapad_sync_rfk_state(struct acpi_device *adevice) | ||
217 | { | ||
218 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); | ||
219 | unsigned long hw_blocked; | ||
220 | int i; | ||
221 | |||
222 | if (read_ec_data(ideapad_handle, 0x23, &hw_blocked)) | ||
223 | return; | ||
224 | hw_blocked = !hw_blocked; | ||
225 | |||
226 | for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) | ||
227 | if (priv->rfk[i]) | ||
228 | rfkill_set_hw_state(priv->rfk[i], hw_blocked); | ||
229 | } | ||
230 | |||
231 | static int __devinit ideapad_register_rfkill(struct acpi_device *adevice, | ||
232 | int dev) | ||
233 | { | ||
234 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); | ||
235 | int ret; | ||
236 | unsigned long sw_blocked; | ||
237 | |||
238 | if (no_bt_rfkill && | ||
239 | (ideapad_rfk_data[dev].type == RFKILL_TYPE_BLUETOOTH)) { | ||
240 | /* Force to enable bluetooth when no_bt_rfkill=1 */ | ||
241 | write_ec_cmd(ideapad_handle, | ||
242 | ideapad_rfk_data[dev].opcode, 1); | ||
243 | return 0; | ||
244 | } | ||
245 | |||
246 | priv->rfk[dev] = rfkill_alloc(ideapad_rfk_data[dev].name, &adevice->dev, | ||
247 | ideapad_rfk_data[dev].type, &ideapad_rfk_ops, | ||
248 | (void *)(long)dev); | ||
249 | if (!priv->rfk[dev]) | ||
250 | return -ENOMEM; | ||
251 | |||
252 | if (read_ec_data(ideapad_handle, ideapad_rfk_data[dev].opcode-1, | ||
253 | &sw_blocked)) { | ||
254 | rfkill_init_sw_state(priv->rfk[dev], 0); | ||
255 | } else { | ||
256 | sw_blocked = !sw_blocked; | ||
257 | rfkill_init_sw_state(priv->rfk[dev], sw_blocked); | ||
258 | } | ||
259 | |||
260 | ret = rfkill_register(priv->rfk[dev]); | ||
261 | if (ret) { | ||
262 | rfkill_destroy(priv->rfk[dev]); | ||
263 | return ret; | ||
264 | } | ||
265 | return 0; | ||
266 | } | ||
267 | |||
268 | static void __devexit ideapad_unregister_rfkill(struct acpi_device *adevice, | ||
269 | int dev) | ||
270 | { | ||
271 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); | ||
272 | |||
273 | if (!priv->rfk[dev]) | ||
274 | return; | ||
275 | |||
276 | rfkill_unregister(priv->rfk[dev]); | ||
277 | rfkill_destroy(priv->rfk[dev]); | ||
278 | } | ||
279 | |||
280 | /* | ||
281 | * Platform device | ||
282 | */ | ||
283 | static struct attribute *ideapad_attributes[] = { | ||
284 | &dev_attr_camera_power.attr, | ||
285 | NULL | ||
286 | }; | ||
287 | |||
288 | static struct attribute_group ideapad_attribute_group = { | ||
289 | .attrs = ideapad_attributes | ||
290 | }; | ||
291 | |||
292 | static int __devinit ideapad_platform_init(struct ideapad_private *priv) | ||
293 | { | ||
294 | int result; | ||
295 | |||
296 | priv->platform_device = platform_device_alloc("ideapad", -1); | ||
297 | if (!priv->platform_device) | ||
298 | return -ENOMEM; | ||
299 | platform_set_drvdata(priv->platform_device, priv); | ||
300 | |||
301 | result = platform_device_add(priv->platform_device); | ||
302 | if (result) | ||
303 | goto fail_platform_device; | ||
304 | |||
305 | result = sysfs_create_group(&priv->platform_device->dev.kobj, | ||
306 | &ideapad_attribute_group); | ||
307 | if (result) | ||
308 | goto fail_sysfs; | ||
309 | return 0; | ||
310 | |||
311 | fail_sysfs: | ||
312 | platform_device_del(priv->platform_device); | ||
313 | fail_platform_device: | ||
314 | platform_device_put(priv->platform_device); | ||
315 | return result; | ||
316 | } | ||
317 | |||
318 | static void ideapad_platform_exit(struct ideapad_private *priv) | ||
319 | { | ||
320 | sysfs_remove_group(&priv->platform_device->dev.kobj, | ||
321 | &ideapad_attribute_group); | ||
322 | platform_device_unregister(priv->platform_device); | ||
323 | } | ||
324 | |||
325 | /* | ||
326 | * input device | ||
327 | */ | ||
328 | static const struct key_entry ideapad_keymap[] = { | ||
329 | { KE_KEY, 0x06, { KEY_SWITCHVIDEOMODE } }, | ||
330 | { KE_KEY, 0x0D, { KEY_WLAN } }, | ||
331 | { KE_END, 0 }, | ||
332 | }; | ||
333 | |||
334 | static int __devinit ideapad_input_init(struct ideapad_private *priv) | ||
335 | { | ||
336 | struct input_dev *inputdev; | ||
337 | int error; | ||
338 | |||
339 | inputdev = input_allocate_device(); | ||
340 | if (!inputdev) { | ||
341 | pr_info("Unable to allocate input device\n"); | ||
342 | return -ENOMEM; | ||
343 | } | ||
344 | |||
345 | inputdev->name = "Ideapad extra buttons"; | ||
346 | inputdev->phys = "ideapad/input0"; | ||
347 | inputdev->id.bustype = BUS_HOST; | ||
348 | inputdev->dev.parent = &priv->platform_device->dev; | ||
349 | |||
350 | error = sparse_keymap_setup(inputdev, ideapad_keymap, NULL); | ||
351 | if (error) { | ||
352 | pr_err("Unable to setup input device keymap\n"); | ||
353 | goto err_free_dev; | ||
354 | } | ||
355 | |||
356 | error = input_register_device(inputdev); | ||
357 | if (error) { | ||
358 | pr_err("Unable to register input device\n"); | ||
359 | goto err_free_keymap; | ||
360 | } | ||
361 | |||
362 | priv->inputdev = inputdev; | ||
363 | return 0; | ||
364 | |||
365 | err_free_keymap: | ||
366 | sparse_keymap_free(inputdev); | ||
367 | err_free_dev: | ||
368 | input_free_device(inputdev); | ||
369 | return error; | ||
370 | } | ||
371 | |||
372 | static void __devexit ideapad_input_exit(struct ideapad_private *priv) | ||
373 | { | ||
374 | sparse_keymap_free(priv->inputdev); | ||
375 | input_unregister_device(priv->inputdev); | ||
376 | priv->inputdev = NULL; | ||
377 | } | ||
378 | |||
379 | static void ideapad_input_report(struct ideapad_private *priv, | ||
380 | unsigned long scancode) | ||
381 | { | ||
382 | sparse_keymap_report_event(priv->inputdev, scancode, 1, true); | ||
383 | } | ||
384 | |||
385 | /* | ||
386 | * module init/exit | ||
387 | */ | ||
388 | static const struct acpi_device_id ideapad_device_ids[] = { | ||
389 | { "VPC2004", 0}, | ||
390 | { "", 0}, | ||
391 | }; | ||
392 | MODULE_DEVICE_TABLE(acpi, ideapad_device_ids); | ||
393 | |||
394 | static int __devinit ideapad_acpi_add(struct acpi_device *adevice) | ||
395 | { | ||
396 | int ret, i, cfg; | ||
397 | struct ideapad_private *priv; | ||
398 | |||
399 | if (read_method_int(adevice->handle, "_CFG", &cfg)) | ||
400 | return -ENODEV; | ||
401 | |||
402 | priv = kzalloc(sizeof(*priv), GFP_KERNEL); | ||
403 | if (!priv) | ||
404 | return -ENOMEM; | ||
405 | dev_set_drvdata(&adevice->dev, priv); | ||
406 | ideapad_handle = adevice->handle; | ||
407 | |||
408 | ret = ideapad_platform_init(priv); | ||
409 | if (ret) | ||
410 | goto platform_failed; | ||
411 | |||
412 | ret = ideapad_input_init(priv); | ||
413 | if (ret) | ||
414 | goto input_failed; | ||
415 | |||
416 | for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) { | ||
417 | if (test_bit(ideapad_rfk_data[i].cfgbit, (unsigned long *)&cfg)) | ||
418 | ideapad_register_rfkill(adevice, i); | ||
419 | else | ||
420 | priv->rfk[i] = NULL; | ||
421 | } | ||
422 | ideapad_sync_rfk_state(adevice); | ||
423 | |||
424 | return 0; | ||
425 | |||
426 | input_failed: | ||
427 | ideapad_platform_exit(priv); | ||
428 | platform_failed: | ||
429 | kfree(priv); | ||
430 | return ret; | ||
431 | } | ||
432 | |||
433 | static int __devexit ideapad_acpi_remove(struct acpi_device *adevice, int type) | ||
434 | { | ||
435 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); | ||
436 | int i; | ||
437 | |||
438 | for (i = 0; i < IDEAPAD_RFKILL_DEV_NUM; i++) | ||
439 | ideapad_unregister_rfkill(adevice, i); | ||
440 | ideapad_input_exit(priv); | ||
441 | ideapad_platform_exit(priv); | ||
442 | dev_set_drvdata(&adevice->dev, NULL); | ||
443 | kfree(priv); | ||
444 | |||
445 | return 0; | ||
446 | } | ||
447 | |||
448 | static void ideapad_acpi_notify(struct acpi_device *adevice, u32 event) | ||
449 | { | ||
450 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); | ||
451 | acpi_handle handle = adevice->handle; | ||
452 | unsigned long vpc1, vpc2, vpc_bit; | ||
453 | |||
454 | if (read_ec_data(handle, 0x10, &vpc1)) | ||
455 | return; | ||
456 | if (read_ec_data(handle, 0x1A, &vpc2)) | ||
457 | return; | ||
458 | |||
459 | vpc1 = (vpc2 << 8) | vpc1; | ||
460 | for (vpc_bit = 0; vpc_bit < 16; vpc_bit++) { | ||
461 | if (test_bit(vpc_bit, &vpc1)) { | ||
462 | if (vpc_bit == 9) | ||
463 | ideapad_sync_rfk_state(adevice); | ||
464 | else if (vpc_bit == 4) | ||
465 | read_ec_data(handle, 0x12, &vpc2); | ||
466 | else | ||
467 | ideapad_input_report(priv, vpc_bit); | ||
468 | } | ||
469 | } | ||
470 | } | ||
471 | |||
472 | static struct acpi_driver ideapad_acpi_driver = { | ||
473 | .name = "ideapad_acpi", | ||
474 | .class = "IdeaPad", | ||
475 | .ids = ideapad_device_ids, | ||
476 | .ops.add = ideapad_acpi_add, | ||
477 | .ops.remove = ideapad_acpi_remove, | ||
478 | .ops.notify = ideapad_acpi_notify, | ||
479 | .owner = THIS_MODULE, | ||
480 | }; | ||
481 | |||
482 | static int __init ideapad_acpi_module_init(void) | ||
483 | { | ||
484 | return acpi_bus_register_driver(&ideapad_acpi_driver); | ||
485 | } | ||
486 | |||
487 | static void __exit ideapad_acpi_module_exit(void) | ||
488 | { | ||
489 | acpi_bus_unregister_driver(&ideapad_acpi_driver); | ||
490 | } | ||
491 | |||
492 | MODULE_AUTHOR("David Woodhouse <dwmw2@infradead.org>"); | ||
493 | MODULE_DESCRIPTION("IdeaPad ACPI Extras"); | ||
494 | MODULE_LICENSE("GPL"); | ||
495 | |||
496 | module_init(ideapad_acpi_module_init); | ||
497 | module_exit(ideapad_acpi_module_exit); | ||