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 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 | } | |
6a09f21d | 171 | |
a4b5a279 IP |
172 | /* |
173 | * camera power | |
174 | */ | |
58ac7aa0 DW |
175 | static ssize_t show_ideapad_cam(struct device *dev, |
176 | struct device_attribute *attr, | |
177 | char *buf) | |
178 | { | |
26c81d5c IP |
179 | struct ideapad_private *priv = dev_get_drvdata(dev); |
180 | acpi_handle handle = priv->handle; | |
181 | unsigned long result; | |
58ac7aa0 | 182 | |
26c81d5c IP |
183 | if (read_ec_data(handle, 0x1D, &result)) |
184 | return sprintf(buf, "-1\n"); | |
185 | return sprintf(buf, "%lu\n", result); | |
58ac7aa0 DW |
186 | } |
187 | ||
188 | static ssize_t store_ideapad_cam(struct device *dev, | |
189 | struct device_attribute *attr, | |
190 | const char *buf, size_t count) | |
191 | { | |
26c81d5c IP |
192 | struct ideapad_private *priv = dev_get_drvdata(dev); |
193 | acpi_handle handle = priv->handle; | |
58ac7aa0 DW |
194 | int ret, state; |
195 | ||
196 | if (!count) | |
197 | return 0; | |
198 | if (sscanf(buf, "%i", &state) != 1) | |
199 | return -EINVAL; | |
26c81d5c | 200 | ret = write_ec_cmd(handle, 0x1E, state); |
58ac7aa0 DW |
201 | if (ret < 0) |
202 | return ret; | |
203 | return count; | |
204 | } | |
205 | ||
206 | static DEVICE_ATTR(camera_power, 0644, show_ideapad_cam, store_ideapad_cam); | |
207 | ||
a4b5a279 IP |
208 | /* |
209 | * Rfkill | |
210 | */ | |
58ac7aa0 DW |
211 | static int ideapad_rfk_set(void *data, bool blocked) |
212 | { | |
213 | int device = (unsigned long)data; | |
214 | ||
215 | if (device == IDEAPAD_DEV_KILLSW) | |
216 | return -EINVAL; | |
fa08359e IP |
217 | |
218 | return write_ec_cmd(ideapad_priv->handle, | |
219 | ideapad_rfk_data[device].opcode, | |
220 | !blocked); | |
58ac7aa0 DW |
221 | } |
222 | ||
223 | static struct rfkill_ops ideapad_rfk_ops = { | |
224 | .set_block = ideapad_rfk_set, | |
225 | }; | |
226 | ||
ce326329 | 227 | static void ideapad_sync_rfk_state(struct acpi_device *adevice) |
58ac7aa0 | 228 | { |
ce326329 | 229 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); |
2b7266bd IP |
230 | acpi_handle handle = priv->handle; |
231 | unsigned long hw_blocked; | |
58ac7aa0 DW |
232 | int i; |
233 | ||
2b7266bd | 234 | if (read_ec_data(handle, 0x23, &hw_blocked)) |
58ac7aa0 | 235 | return; |
2b7266bd | 236 | hw_blocked = !hw_blocked; |
58ac7aa0 | 237 | |
2b7266bd | 238 | for (i = IDEAPAD_DEV_WLAN; i <= IDEAPAD_DEV_KILLSW; i++) |
ce326329 | 239 | if (priv->rfk[i]) |
2b7266bd | 240 | rfkill_set_hw_state(priv->rfk[i], hw_blocked); |
58ac7aa0 DW |
241 | } |
242 | ||
a4b5a279 IP |
243 | static int __devinit ideapad_register_rfkill(struct acpi_device *adevice, |
244 | int dev) | |
58ac7aa0 | 245 | { |
ce326329 | 246 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); |
58ac7aa0 | 247 | int ret; |
2b7266bd | 248 | unsigned long sw_blocked; |
58ac7aa0 | 249 | |
bfa97b7d IP |
250 | if (no_bt_rfkill && |
251 | (ideapad_rfk_data[dev].type == RFKILL_TYPE_BLUETOOTH)) { | |
252 | /* Force to enable bluetooth when no_bt_rfkill=1 */ | |
253 | write_ec_cmd(ideapad_priv->handle, | |
254 | ideapad_rfk_data[dev].opcode, 1); | |
255 | return 0; | |
256 | } | |
257 | ||
2b7266bd IP |
258 | priv->rfk[dev] = rfkill_alloc(ideapad_rfk_data[dev].name, &adevice->dev, |
259 | ideapad_rfk_data[dev].type, &ideapad_rfk_ops, | |
ce326329 DW |
260 | (void *)(long)dev); |
261 | if (!priv->rfk[dev]) | |
58ac7aa0 DW |
262 | return -ENOMEM; |
263 | ||
2b7266bd IP |
264 | if (read_ec_data(ideapad_priv->handle, ideapad_rfk_data[dev].opcode-1, |
265 | &sw_blocked)) { | |
266 | rfkill_init_sw_state(priv->rfk[dev], 0); | |
267 | } else { | |
268 | sw_blocked = !sw_blocked; | |
269 | rfkill_init_sw_state(priv->rfk[dev], sw_blocked); | |
270 | } | |
271 | ||
ce326329 | 272 | ret = rfkill_register(priv->rfk[dev]); |
58ac7aa0 | 273 | if (ret) { |
ce326329 | 274 | rfkill_destroy(priv->rfk[dev]); |
58ac7aa0 DW |
275 | return ret; |
276 | } | |
277 | return 0; | |
278 | } | |
279 | ||
a4b5a279 IP |
280 | static void __devexit ideapad_unregister_rfkill(struct acpi_device *adevice, |
281 | int dev) | |
58ac7aa0 | 282 | { |
ce326329 DW |
283 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); |
284 | ||
285 | if (!priv->rfk[dev]) | |
58ac7aa0 DW |
286 | return; |
287 | ||
ce326329 DW |
288 | rfkill_unregister(priv->rfk[dev]); |
289 | rfkill_destroy(priv->rfk[dev]); | |
58ac7aa0 DW |
290 | } |
291 | ||
98ee6919 IP |
292 | /* |
293 | * Platform device | |
294 | */ | |
c9f718d0 IP |
295 | static struct attribute *ideapad_attributes[] = { |
296 | &dev_attr_camera_power.attr, | |
297 | NULL | |
298 | }; | |
299 | ||
300 | static struct attribute_group ideapad_attribute_group = { | |
301 | .attrs = ideapad_attributes | |
302 | }; | |
303 | ||
8693ae84 | 304 | static int __devinit ideapad_platform_init(struct ideapad_private *priv) |
98ee6919 IP |
305 | { |
306 | int result; | |
307 | ||
8693ae84 IP |
308 | priv->platform_device = platform_device_alloc("ideapad", -1); |
309 | if (!priv->platform_device) | |
98ee6919 | 310 | return -ENOMEM; |
8693ae84 | 311 | platform_set_drvdata(priv->platform_device, priv); |
98ee6919 | 312 | |
8693ae84 | 313 | result = platform_device_add(priv->platform_device); |
98ee6919 IP |
314 | if (result) |
315 | goto fail_platform_device; | |
316 | ||
8693ae84 | 317 | result = sysfs_create_group(&priv->platform_device->dev.kobj, |
c9f718d0 IP |
318 | &ideapad_attribute_group); |
319 | if (result) | |
320 | goto fail_sysfs; | |
98ee6919 IP |
321 | return 0; |
322 | ||
c9f718d0 | 323 | fail_sysfs: |
8693ae84 | 324 | platform_device_del(priv->platform_device); |
98ee6919 | 325 | fail_platform_device: |
8693ae84 | 326 | platform_device_put(priv->platform_device); |
98ee6919 IP |
327 | return result; |
328 | } | |
329 | ||
8693ae84 | 330 | static void ideapad_platform_exit(struct ideapad_private *priv) |
98ee6919 | 331 | { |
8693ae84 | 332 | sysfs_remove_group(&priv->platform_device->dev.kobj, |
c9f718d0 | 333 | &ideapad_attribute_group); |
8693ae84 | 334 | platform_device_unregister(priv->platform_device); |
98ee6919 | 335 | } |
98ee6919 | 336 | |
f63409ae IP |
337 | /* |
338 | * input device | |
339 | */ | |
340 | static const struct key_entry ideapad_keymap[] = { | |
341 | { KE_KEY, 0x06, { KEY_SWITCHVIDEOMODE } }, | |
342 | { KE_KEY, 0x0D, { KEY_WLAN } }, | |
343 | { KE_END, 0 }, | |
344 | }; | |
345 | ||
8693ae84 | 346 | static int __devinit ideapad_input_init(struct ideapad_private *priv) |
f63409ae IP |
347 | { |
348 | struct input_dev *inputdev; | |
349 | int error; | |
350 | ||
351 | inputdev = input_allocate_device(); | |
352 | if (!inputdev) { | |
353 | pr_info("Unable to allocate input device\n"); | |
354 | return -ENOMEM; | |
355 | } | |
356 | ||
357 | inputdev->name = "Ideapad extra buttons"; | |
358 | inputdev->phys = "ideapad/input0"; | |
359 | inputdev->id.bustype = BUS_HOST; | |
8693ae84 | 360 | inputdev->dev.parent = &priv->platform_device->dev; |
f63409ae IP |
361 | |
362 | error = sparse_keymap_setup(inputdev, ideapad_keymap, NULL); | |
363 | if (error) { | |
364 | pr_err("Unable to setup input device keymap\n"); | |
365 | goto err_free_dev; | |
366 | } | |
367 | ||
368 | error = input_register_device(inputdev); | |
369 | if (error) { | |
370 | pr_err("Unable to register input device\n"); | |
371 | goto err_free_keymap; | |
372 | } | |
373 | ||
8693ae84 | 374 | priv->inputdev = inputdev; |
f63409ae IP |
375 | return 0; |
376 | ||
377 | err_free_keymap: | |
378 | sparse_keymap_free(inputdev); | |
379 | err_free_dev: | |
380 | input_free_device(inputdev); | |
381 | return error; | |
382 | } | |
383 | ||
8693ae84 | 384 | static void __devexit ideapad_input_exit(struct ideapad_private *priv) |
f63409ae | 385 | { |
8693ae84 IP |
386 | sparse_keymap_free(priv->inputdev); |
387 | input_unregister_device(priv->inputdev); | |
388 | priv->inputdev = NULL; | |
f63409ae IP |
389 | } |
390 | ||
8693ae84 IP |
391 | static void ideapad_input_report(struct ideapad_private *priv, |
392 | unsigned long scancode) | |
f63409ae | 393 | { |
8693ae84 | 394 | sparse_keymap_report_event(priv->inputdev, scancode, 1, true); |
f63409ae | 395 | } |
f63409ae | 396 | |
a4b5a279 IP |
397 | /* |
398 | * module init/exit | |
399 | */ | |
58ac7aa0 DW |
400 | static const struct acpi_device_id ideapad_device_ids[] = { |
401 | { "VPC2004", 0}, | |
402 | { "", 0}, | |
403 | }; | |
404 | MODULE_DEVICE_TABLE(acpi, ideapad_device_ids); | |
405 | ||
a4b5a279 | 406 | static int __devinit ideapad_acpi_add(struct acpi_device *adevice) |
58ac7aa0 | 407 | { |
98ee6919 | 408 | int ret, i, cfg; |
ce326329 | 409 | struct ideapad_private *priv; |
58ac7aa0 | 410 | |
6f8371c0 IP |
411 | if (read_method_int(adevice->handle, "_CFG", &cfg)) |
412 | return -ENODEV; | |
413 | ||
ce326329 DW |
414 | priv = kzalloc(sizeof(*priv), GFP_KERNEL); |
415 | if (!priv) | |
416 | return -ENOMEM; | |
98ee6919 | 417 | ideapad_priv = priv; |
c9f718d0 IP |
418 | priv->handle = adevice->handle; |
419 | dev_set_drvdata(&adevice->dev, priv); | |
98ee6919 | 420 | |
8693ae84 | 421 | ret = ideapad_platform_init(priv); |
98ee6919 IP |
422 | if (ret) |
423 | goto platform_failed; | |
ce326329 | 424 | |
8693ae84 | 425 | ret = ideapad_input_init(priv); |
f63409ae IP |
426 | if (ret) |
427 | goto input_failed; | |
428 | ||
c9f718d0 IP |
429 | for (i = IDEAPAD_DEV_WLAN; i < IDEAPAD_DEV_KILLSW; i++) { |
430 | if (test_bit(ideapad_rfk_data[i].cfgbit, (unsigned long *)&cfg)) | |
431 | ideapad_register_rfkill(adevice, i); | |
58ac7aa0 | 432 | } |
ce326329 | 433 | ideapad_sync_rfk_state(adevice); |
c9f718d0 | 434 | |
58ac7aa0 | 435 | return 0; |
98ee6919 | 436 | |
f63409ae | 437 | input_failed: |
8693ae84 | 438 | ideapad_platform_exit(priv); |
98ee6919 IP |
439 | platform_failed: |
440 | kfree(priv); | |
441 | return ret; | |
58ac7aa0 DW |
442 | } |
443 | ||
a4b5a279 | 444 | static int __devexit ideapad_acpi_remove(struct acpi_device *adevice, int type) |
58ac7aa0 | 445 | { |
ce326329 | 446 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); |
58ac7aa0 | 447 | int i; |
ce326329 | 448 | |
c9f718d0 | 449 | for (i = IDEAPAD_DEV_WLAN; i < IDEAPAD_DEV_KILLSW; i++) |
ce326329 | 450 | ideapad_unregister_rfkill(adevice, i); |
8693ae84 IP |
451 | ideapad_input_exit(priv); |
452 | ideapad_platform_exit(priv); | |
ce326329 DW |
453 | dev_set_drvdata(&adevice->dev, NULL); |
454 | kfree(priv); | |
c9f718d0 | 455 | |
58ac7aa0 DW |
456 | return 0; |
457 | } | |
458 | ||
ce326329 | 459 | static void ideapad_acpi_notify(struct acpi_device *adevice, u32 event) |
58ac7aa0 | 460 | { |
8693ae84 | 461 | struct ideapad_private *priv = dev_get_drvdata(&adevice->dev); |
8e7d3543 IP |
462 | acpi_handle handle = adevice->handle; |
463 | unsigned long vpc1, vpc2, vpc_bit; | |
464 | ||
465 | if (read_ec_data(handle, 0x10, &vpc1)) | |
466 | return; | |
467 | if (read_ec_data(handle, 0x1A, &vpc2)) | |
468 | return; | |
469 | ||
470 | vpc1 = (vpc2 << 8) | vpc1; | |
471 | for (vpc_bit = 0; vpc_bit < 16; vpc_bit++) { | |
472 | if (test_bit(vpc_bit, &vpc1)) { | |
473 | if (vpc_bit == 9) | |
474 | ideapad_sync_rfk_state(adevice); | |
f63409ae | 475 | else |
8693ae84 | 476 | ideapad_input_report(priv, vpc_bit); |
8e7d3543 IP |
477 | } |
478 | } | |
58ac7aa0 DW |
479 | } |
480 | ||
481 | static struct acpi_driver ideapad_acpi_driver = { | |
482 | .name = "ideapad_acpi", | |
483 | .class = "IdeaPad", | |
484 | .ids = ideapad_device_ids, | |
485 | .ops.add = ideapad_acpi_add, | |
486 | .ops.remove = ideapad_acpi_remove, | |
487 | .ops.notify = ideapad_acpi_notify, | |
488 | .owner = THIS_MODULE, | |
489 | }; | |
490 | ||
58ac7aa0 DW |
491 | static int __init ideapad_acpi_module_init(void) |
492 | { | |
a4b5a279 | 493 | return acpi_bus_register_driver(&ideapad_acpi_driver); |
58ac7aa0 DW |
494 | } |
495 | ||
58ac7aa0 DW |
496 | static void __exit ideapad_acpi_module_exit(void) |
497 | { | |
498 | acpi_bus_unregister_driver(&ideapad_acpi_driver); | |
58ac7aa0 DW |
499 | } |
500 | ||
501 | MODULE_AUTHOR("David Woodhouse <dwmw2@infradead.org>"); | |
502 | MODULE_DESCRIPTION("IdeaPad ACPI Extras"); | |
503 | MODULE_LICENSE("GPL"); | |
504 | ||
505 | module_init(ideapad_acpi_module_init); | |
506 | module_exit(ideapad_acpi_module_exit); |