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