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