diff mbox

ideapad-laptop: add new driver

Message ID 1281672482-10909-1-git-send-email-ike.pan@canonical.com (mailing list archive)
State New, archived
Headers show

Commit Message

Ike Panhc Aug. 13, 2010, 4:08 a.m. UTC
None
diff mbox

Patch

diff --git a/drivers/platform/x86/Kconfig b/drivers/platform/x86/Kconfig
index 79baa63..e9a203d 100644
--- a/drivers/platform/x86/Kconfig
+++ b/drivers/platform/x86/Kconfig
@@ -581,4 +581,15 @@  config INTEL_IPS
 	  functionality.  If in doubt, say Y here; it will only load on
 	  supported platforms.
 
+config IDEAPAD_LAPTOP
+	tristate "Ideapad Laptop Extras"
+	depends on ACPI
+	depends on INPUT
+	select INPUT_SPARSEKMAP
+	---help---
+	  This is the ACPI extra Linux driver for ideapad laptops which enable
+	  ACPI event based hotkey.
+
+	  If you have an Ideapad laptop, say Y or M here.
+
 endif # X86_PLATFORM_DEVICES
diff --git a/drivers/platform/x86/Makefile b/drivers/platform/x86/Makefile
index 4744c77..47ca2b9 100644
--- a/drivers/platform/x86/Makefile
+++ b/drivers/platform/x86/Makefile
@@ -29,4 +29,5 @@  obj-$(CONFIG_INTEL_SCU_IPC)	+= intel_scu_ipc.o
 obj-$(CONFIG_RAR_REGISTER)	+= intel_rar_register.o
 obj-$(CONFIG_INTEL_IPS)		+= intel_ips.o
 obj-$(CONFIG_GPIO_INTEL_PMIC)	+= intel_pmic_gpio.o
+obj-$(CONFIG_IDEAPAD_LAPTOP)	+= ideapad-laptop.o
 
diff --git a/drivers/platform/x86/ideapad-laptop.c b/drivers/platform/x86/ideapad-laptop.c
new file mode 100644
index 0000000..d666d24
--- /dev/null
+++ b/drivers/platform/x86/ideapad-laptop.c
@@ -0,0 +1,398 @@ 
+/*
+ *  ideapad-laptop.c: ACPI extra driver for ideapad series laptop
+ *
+ *  This driver is based on eeepc-laptop.c
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/init.h>
+#include <linux/types.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <acpi/acpi_drivers.h>
+#include <acpi/acpi_bus.h>
+#include <linux/uaccess.h>
+#include <linux/input.h>
+#include <linux/input/sparse-keymap.h>
+
+#define IDEAPAD_LAPTOP_NAME	"Ideapad ACPI extra driver"
+#define IDEAPAD_LAPTOP_FILE	"ideapad"
+
+#define IDEAPAD_ACPI_CLASS		"hotkey"
+#define IDEAPAD_ACPI_DEVICE_NAME	"Hotkey"
+#define IDEAPAD_ACPI_HID		"VPC2004"
+
+#define IDEAPAD_EC_TIMEOUT	(25)
+
+MODULE_AUTHOR("Ike Panhc");
+MODULE_DESCRIPTION(IDEAPAD_LAPTOP_NAME);
+MODULE_LICENSE("GPL");
+
+/*
+ * This is the main structure, we can use it to store useful information
+ */
+struct ideapad_laptop {
+	acpi_handle handle;		/* the handle of the acpi device */
+	u32 cm_supported;		/* the control methods supported
+					   by this BIOS */
+	u16 event_count[128];		/* count for each event */
+
+	struct platform_device *platform_device;
+	struct input_dev *inputdev;
+
+	int cfg;
+};
+
+/*
+ * ACPI Helpers
+ */
+static int read_method_int(acpi_handle handle, const char *method, int *val)
+{
+	acpi_status status;
+	unsigned long long result;
+
+	status = acpi_evaluate_integer(handle, (char *)method, NULL, &result);
+	if (ACPI_FAILURE(status)) {
+		*val = -1;
+		return -1;
+	} else {
+		*val = result;
+		return 0;
+	}
+}
+
+static int method_vpcr(acpi_handle handle, int cmd, int *ret)
+{
+	acpi_status status;
+	unsigned long long result;
+	struct acpi_object_list params;
+	union acpi_object in_obj;
+
+	params.count = 1;
+	params.pointer = &in_obj;
+	in_obj.type = ACPI_TYPE_INTEGER;
+	in_obj.integer.value = cmd;
+
+	status = acpi_evaluate_integer(handle, "VPCR", &params, &result);
+
+	if (ACPI_FAILURE(status)) {
+		*ret = -1;
+		return -1;
+	} else {
+		*ret = result;
+		return 0;
+	}
+}
+
+static int method_vpcw(acpi_handle handle, int cmd, int data)
+{
+	struct acpi_object_list params;
+	union acpi_object in_obj[2];
+	acpi_status status;
+
+	params.count = 2;
+	params.pointer = in_obj;
+	in_obj[0].type = ACPI_TYPE_INTEGER;
+	in_obj[0].integer.value = cmd;
+	in_obj[1].type = ACPI_TYPE_INTEGER;
+	in_obj[1].integer.value = data;
+	status = acpi_evaluate_object(handle, "VPCW", &params, NULL);
+	if (status != AE_OK)
+		return -1;
+	return 0;
+}
+
+static int read_ec_data(acpi_handle handle, int cmd, unsigned long *data)
+{
+	int val;
+	unsigned long int start_jiffies, now_jiffies;
+
+	if (method_vpcw(handle, 1, cmd))
+		return -1;
+
+	for (start_jiffies = jiffies;; ) {
+		if (method_vpcr(handle, 1, &val))
+			return -1;
+		if (val == 0) {
+			if (method_vpcr(handle, 0, &val))
+				return -1;
+			*data = val;
+			return 0;
+		}
+		now_jiffies = jiffies;
+		if ((now_jiffies-start_jiffies) > (HZ)/IDEAPAD_EC_TIMEOUT+1) {
+			pr_err("timeout in read_ec_cmd\n");
+			return -1;
+		}
+	}
+}
+
+/*
+ * Platform driver
+ */
+static struct platform_driver platform_driver = {
+	.driver = {
+		.name = IDEAPAD_LAPTOP_FILE,
+		.owner = THIS_MODULE,
+	}
+};
+
+static int ideapad_platform_init(struct ideapad_laptop *ideapad)
+{
+	int result;
+
+	ideapad->platform_device = platform_device_alloc(IDEAPAD_LAPTOP_FILE,
+							-1);
+	if (!ideapad->platform_device)
+		return -ENOMEM;
+	platform_set_drvdata(ideapad->platform_device, ideapad);
+
+	result = platform_device_add(ideapad->platform_device);
+	if (result)
+		goto fail_platform_device;
+
+	return 0;
+
+fail_platform_device:
+	platform_device_put(ideapad->platform_device);
+	return result;
+}
+
+static void ideapad_platform_exit(struct ideapad_laptop *ideapad)
+{
+	platform_device_unregister(ideapad->platform_device);
+}
+
+/*
+ * Input device
+ */
+static const struct key_entry ideapad_keymap[] = {
+	{ KE_KEY, 0x02, { KEY_DISPLAY_OFF } },
+	{ KE_KEY, 0x03, { KEY_PROG1 } },
+	{ KE_KEY, 0x04, { KEY_BRIGHTNESS_CYCLE } },
+	{ KE_KEY, 0x05, { KEY_F13 } },
+	{ KE_KEY, 0x06, { KEY_SWITCHVIDEOMODE } },
+	{ KE_KEY, 0x07, { KEY_CAMERA } },
+	{ KE_KEY, 0x0B, { KEY_VIDEO_NEXT } },
+	{ KE_KEY, 0x0D, { KEY_WLAN } },
+	{ KE_END, 0},
+};
+
+static int ideapad_input_init(struct ideapad_laptop *ideapad)
+{
+	struct input_dev *input;
+	int error;
+
+	input = input_allocate_device();
+	if (!input) {
+		pr_info("Unable to allocate input device\n");
+		return -ENOMEM;
+	}
+
+	input->name = "Lenovo Ideapad Hotkeys";
+	input->phys = IDEAPAD_LAPTOP_FILE "/input0";
+	input->id.bustype = BUS_HOST;
+	input->dev.parent = &ideapad->platform_device->dev;
+
+	error = sparse_keymap_setup(input, ideapad_keymap, NULL);
+	if (error) {
+		pr_err("Unable to setup input device keymap\n");
+		goto err_free_dev;
+	}
+
+	error = input_register_device(input);
+	if (error) {
+		pr_err("Unable to register input device\n");
+		goto err_free_keymap;
+	}
+
+	ideapad->inputdev = input;
+	return 0;
+
+err_free_keymap:
+	sparse_keymap_free(input);
+err_free_dev:
+	input_free_device(input);
+	return error;
+}
+
+static void ideapad_input_exit(struct ideapad_laptop *ideapad)
+{
+	if (ideapad->inputdev)
+		input_unregister_device(ideapad->inputdev);
+}
+
+/*
+ * ACPI driver
+ */
+static void ideapad_acpi_notify(struct acpi_device *device, u32 event)
+{
+	struct ideapad_laptop *ideapad = device->driver_data;
+	acpi_handle handle = ideapad->handle;
+	unsigned long vpc1, vpc2, vpc_bit;
+
+	if (read_ec_data(handle, 0x10, &vpc1))
+		return;
+	if (read_ec_data(handle, 0x1A, &vpc2))
+		return;
+
+	vpc1 = (vpc2 << 8) | vpc1;
+	for (vpc_bit = 0; vpc_bit < 16; vpc_bit++) {
+		if (test_bit(vpc_bit, &vpc1)) {
+			if (vpc_bit == 9)
+				continue;
+			sparse_keymap_report_event(ideapad->inputdev, event, 1,
+						true);
+		}
+	}
+}
+
+static int ideapad_acpi_init(struct ideapad_laptop *ideapad,
+			   struct acpi_device *device)
+{
+	int result;
+	acpi_handle handle = ideapad->handle;
+
+	result = acpi_bus_get_status(device);
+	if (result)
+		return result;
+	if (!device->status.present) {
+		pr_err("Hotkey device not present, aborting\n");
+		return -ENODEV;
+	}
+
+	if (read_method_int(handle, "_STA", &result)) {
+		pr_err("Access _STA failed\n");
+		return -ENODEV;
+	}
+	if (result != 0x0F) {
+		pr_err("_STA return unknown value: 0x%X\n", result);
+		return -ENODEV;
+	}
+
+	if (read_method_int(handle, "_CFG", &result)) {
+		pr_err("Access _CFG failed\n");
+		return -ENODEV;
+	}
+	ideapad->cfg = result;
+
+	return 0;
+}
+
+static bool ideapad_device_present;
+
+static int __devinit ideapad_acpi_add(struct acpi_device *device)
+{
+	struct ideapad_laptop *ideapad;
+	int result;
+
+	ideapad = kzalloc(sizeof(struct ideapad_laptop), GFP_KERNEL);
+	if (!ideapad)
+		return -ENOMEM;
+	ideapad->handle = device->handle;
+	strcpy(acpi_device_name(device), IDEAPAD_ACPI_DEVICE_NAME);
+	strcpy(acpi_device_class(device), IDEAPAD_ACPI_CLASS);
+	device->driver_data = ideapad;
+
+	result = ideapad_acpi_init(ideapad, device);
+	if (result)
+		goto fail_platform;
+
+	result = ideapad_platform_init(ideapad);
+	if (result)
+		goto fail_platform;
+
+	result = ideapad_input_init(ideapad);
+	if (result)
+		goto fail_input;
+
+	ideapad_device_present = true;
+	return 0;
+
+fail_input:
+	ideapad_platform_exit(ideapad);
+fail_platform:
+	kfree(ideapad);
+
+	return result;
+}
+
+static int ideapad_acpi_remove(struct acpi_device *device, int type)
+{
+	struct ideapad_laptop *ideapad = acpi_driver_data(device);
+
+	ideapad_input_exit(ideapad);
+	ideapad_platform_exit(ideapad);
+
+	kfree(ideapad);
+	return 0;
+}
+
+
+static const struct acpi_device_id ideapad_device_ids[] = {
+	{IDEAPAD_ACPI_HID, 0},
+	{"", 0},
+};
+MODULE_DEVICE_TABLE(acpi, ideapad_device_ids);
+
+static struct acpi_driver ideapad_acpi_driver = {
+	.name = IDEAPAD_LAPTOP_NAME,
+	.class = IDEAPAD_ACPI_CLASS,
+	.owner = THIS_MODULE,
+	.ids = ideapad_device_ids,
+	.flags = ACPI_DRIVER_ALL_NOTIFY_EVENTS,
+	.ops = {
+		.add = ideapad_acpi_add,
+		.remove = ideapad_acpi_remove,
+		.notify = ideapad_acpi_notify,
+	},
+};
+
+
+static int __init ideapad_laptop_init(void)
+{
+	int result;
+
+	result = platform_driver_register(&platform_driver);
+	if (result < 0)
+		return result;
+
+	result = acpi_bus_register_driver(&ideapad_acpi_driver);
+	if (result < 0)
+		goto fail_acpi_driver;
+
+	if (!ideapad_device_present) {
+		result = -ENODEV;
+		goto fail_no_device;
+	}
+
+	return 0;
+
+fail_no_device:
+	acpi_bus_unregister_driver(&ideapad_acpi_driver);
+fail_acpi_driver:
+	platform_driver_unregister(&platform_driver);
+	return result;
+}
+
+static void __exit ideapad_laptop_exit(void)
+{
+	acpi_bus_unregister_driver(&ideapad_acpi_driver);
+	platform_driver_unregister(&platform_driver);
+}
+
+module_init(ideapad_laptop_init);
+module_exit(ideapad_laptop_exit);