diff mbox

[Xenial,SRU] thinkpad_acpi: Add support for keyboard backlight

Message ID 1461599173-29234-1-git-send-email-tim.gardner@canonical.com
State New
Headers show

Commit Message

Tim Gardner April 25, 2016, 3:46 p.m. UTC
From: Pali Rohár <pali.rohar@gmail.com>

BugLink: http://bugs.launchpad.net/bugs/1574498

This patch adds support for controlling keyboard backlight via standard
linux led class interface (::kbd_backlight). It uses ACPI HKEY device with
MLCG and MLCS methods.

Signed-off-by: Pali Rohár <pali.rohar@gmail.com>
Tested-by: Fabio D'Urso <fabiodurso@hotmail.it>
Acked-by: Henrique de Moraes Holschuh <hmh@hmh.eng.br>
Signed-off-by: Darren Hart <dvhart@linux.intel.com>
(cherry picked from commit bb28f3d51ff5e1be541d057708011cc1efe6fae9)
Signed-off-by: Tim Gardner <tim.gardner@canonical.com>
---
 drivers/platform/x86/thinkpad_acpi.c | 206 +++++++++++++++++++++++++++++++++++
 1 file changed, 206 insertions(+)

Comments

Kamal Mostafa April 25, 2016, 4:11 p.m. UTC | #1

Stefan Bader April 26, 2016, 8:29 a.m. UTC | #2
On 25.04.2016 17:46, tim.gardner@canonical.com wrote:
> From: Pali Rohár <pali.rohar@gmail.com>
> 
> BugLink: http://bugs.launchpad.net/bugs/1574498

Personally I do not see the reason to SRU. It is enabling a HW feature which is
not terribly critical. But ok, the change is affecting only a very specific
platform driver and mostly adds bits to it. So should be testable against
laptops with and without keyboard backlight.

-Stefan

> 
> This patch adds support for controlling keyboard backlight via standard
> linux led class interface (::kbd_backlight). It uses ACPI HKEY device with
> MLCG and MLCS methods.
> 
> Signed-off-by: Pali Rohár <pali.rohar@gmail.com>
> Tested-by: Fabio D'Urso <fabiodurso@hotmail.it>
> Acked-by: Henrique de Moraes Holschuh <hmh@hmh.eng.br>
> Signed-off-by: Darren Hart <dvhart@linux.intel.com>
> (cherry picked from commit bb28f3d51ff5e1be541d057708011cc1efe6fae9)
> Signed-off-by: Tim Gardner <tim.gardner@canonical.com>
> ---
>  drivers/platform/x86/thinkpad_acpi.c | 206 +++++++++++++++++++++++++++++++++++
>  1 file changed, 206 insertions(+)
> 
> diff --git a/drivers/platform/x86/thinkpad_acpi.c b/drivers/platform/x86/thinkpad_acpi.c
> index 0bed473..a268a7a 100644
> --- a/drivers/platform/x86/thinkpad_acpi.c
> +++ b/drivers/platform/x86/thinkpad_acpi.c
> @@ -303,6 +303,7 @@ static struct {
>  	u32 hotkey_mask:1;
>  	u32 hotkey_wlsw:1;
>  	u32 hotkey_tablet:1;
> +	u32 kbdlight:1;
>  	u32 light:1;
>  	u32 light_status:1;
>  	u32 bright_acpimode:1;
> @@ -4986,6 +4987,207 @@ static struct ibm_struct video_driver_data = {
>  #endif /* CONFIG_THINKPAD_ACPI_VIDEO */
>  
>  /*************************************************************************
> + * Keyboard backlight subdriver
> + */
> +
> +static int kbdlight_set_level(int level)
> +{
> +	if (!hkey_handle)
> +		return -ENXIO;
> +
> +	if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level))
> +		return -EIO;
> +
> +	return 0;
> +}
> +
> +static int kbdlight_get_level(void)
> +{
> +	int status = 0;
> +
> +	if (!hkey_handle)
> +		return -ENXIO;
> +
> +	if (!acpi_evalf(hkey_handle, &status, "MLCG", "dd", 0))
> +		return -EIO;
> +
> +	if (status < 0)
> +		return status;
> +
> +	return status & 0x3;
> +}
> +
> +static bool kbdlight_is_supported(void)
> +{
> +	int status = 0;
> +
> +	if (!hkey_handle)
> +		return false;
> +
> +	if (!acpi_has_method(hkey_handle, "MLCG")) {
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG is unavailable\n");
> +		return false;
> +	}
> +
> +	if (!acpi_evalf(hkey_handle, &status, "MLCG", "qdd", 0)) {
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG failed\n");
> +		return false;
> +	}
> +
> +	if (status < 0) {
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG err: %d\n", status);
> +		return false;
> +	}
> +
> +	vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG returned 0x%x\n", status);
> +	/*
> +	 * Guessed test for keyboard backlight:
> +	 *
> +	 * Machines with backlight keyboard return:
> +	 *   b010100000010000000XX - ThinkPad X1 Carbon 3rd
> +	 *   b110100010010000000XX - ThinkPad x230
> +	 *   b010100000010000000XX - ThinkPad x240
> +	 *   b010100000010000000XX - ThinkPad W541
> +	 * (XX is current backlight level)
> +	 *
> +	 * Machines without backlight keyboard return:
> +	 *   b10100001000000000000 - ThinkPad x230
> +	 *   b10110001000000000000 - ThinkPad E430
> +	 *   b00000000000000000000 - ThinkPad E450
> +	 *
> +	 * Candidate BITs for detection test (XOR):
> +	 *   b01000000001000000000
> +	 *              ^
> +	 */
> +	return status & BIT(9);
> +}
> +
> +static void kbdlight_set_worker(struct work_struct *work)
> +{
> +	struct tpacpi_led_classdev *data =
> +			container_of(work, struct tpacpi_led_classdev, work);
> +
> +	if (likely(tpacpi_lifecycle == TPACPI_LIFE_RUNNING))
> +		kbdlight_set_level(data->new_state);
> +}
> +
> +static void kbdlight_sysfs_set(struct led_classdev *led_cdev,
> +			enum led_brightness brightness)
> +{
> +	struct tpacpi_led_classdev *data =
> +			container_of(led_cdev,
> +				     struct tpacpi_led_classdev,
> +				     led_classdev);
> +	data->new_state = brightness;
> +	queue_work(tpacpi_wq, &data->work);
> +}
> +
> +static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
> +{
> +	int level;
> +
> +	level = kbdlight_get_level();
> +	if (level < 0)
> +		return 0;
> +
> +	return level;
> +}
> +
> +static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
> +	.led_classdev = {
> +		.name		= "tpacpi::kbd_backlight",
> +		.max_brightness	= 2,
> +		.brightness_set	= &kbdlight_sysfs_set,
> +		.brightness_get	= &kbdlight_sysfs_get,
> +		.flags		= LED_CORE_SUSPENDRESUME,
> +	}
> +};
> +
> +static int __init kbdlight_init(struct ibm_init_struct *iibm)
> +{
> +	int rc;
> +
> +	vdbg_printk(TPACPI_DBG_INIT, "initializing kbdlight subdriver\n");
> +
> +	TPACPI_ACPIHANDLE_INIT(hkey);
> +	INIT_WORK(&tpacpi_led_kbdlight.work, kbdlight_set_worker);
> +
> +	if (!kbdlight_is_supported()) {
> +		tp_features.kbdlight = 0;
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight is unsupported\n");
> +		return 1;
> +	}
> +
> +	tp_features.kbdlight = 1;
> +
> +	rc = led_classdev_register(&tpacpi_pdev->dev,
> +				   &tpacpi_led_kbdlight.led_classdev);
> +	if (rc < 0) {
> +		tp_features.kbdlight = 0;
> +		return rc;
> +	}
> +
> +	return 0;
> +}
> +
> +static void kbdlight_exit(void)
> +{
> +	if (tp_features.kbdlight)
> +		led_classdev_unregister(&tpacpi_led_kbdlight.led_classdev);
> +	flush_workqueue(tpacpi_wq);
> +}
> +
> +static int kbdlight_read(struct seq_file *m)
> +{
> +	int level;
> +
> +	if (!tp_features.kbdlight) {
> +		seq_printf(m, "status:\t\tnot supported\n");
> +	} else {
> +		level = kbdlight_get_level();
> +		if (level < 0)
> +			seq_printf(m, "status:\t\terror %d\n", level);
> +		else
> +			seq_printf(m, "status:\t\t%d\n", level);
> +		seq_printf(m, "commands:\t0, 1, 2\n");
> +	}
> +
> +	return 0;
> +}
> +
> +static int kbdlight_write(char *buf)
> +{
> +	char *cmd;
> +	int level = -1;
> +
> +	if (!tp_features.kbdlight)
> +		return -ENODEV;
> +
> +	while ((cmd = next_cmd(&buf))) {
> +		if (strlencmp(cmd, "0") == 0)
> +			level = 0;
> +		else if (strlencmp(cmd, "1") == 0)
> +			level = 1;
> +		else if (strlencmp(cmd, "2") == 0)
> +			level = 2;
> +		else
> +			return -EINVAL;
> +	}
> +
> +	if (level == -1)
> +		return -EINVAL;
> +
> +	return kbdlight_set_level(level);
> +}
> +
> +static struct ibm_struct kbdlight_driver_data = {
> +	.name = "kbdlight",
> +	.read = kbdlight_read,
> +	.write = kbdlight_write,
> +	.exit = kbdlight_exit,
> +};
> +
> +/*************************************************************************
>   * Light (thinklight) subdriver
>   */
>  
> @@ -9207,6 +9409,10 @@ static struct ibm_init_struct ibms_init[] __initdata = {
>  	},
>  #endif
>  	{
> +		.init = kbdlight_init,
> +		.data = &kbdlight_driver_data,
> +	},
> +	{
>  		.init = light_init,
>  		.data = &light_driver_data,
>  	},
>
Andy Whitcroft April 26, 2016, 8:34 a.m. UTC | #3
On Mon, Apr 25, 2016 at 09:46:13AM -0600, tim.gardner@canonical.com wrote:
> From: Pali Rohár <pali.rohar@gmail.com>
> 
> BugLink: http://bugs.launchpad.net/bugs/1574498
> 
> This patch adds support for controlling keyboard backlight via standard
> linux led class interface (::kbd_backlight). It uses ACPI HKEY device with
> MLCG and MLCS methods.
> 
> Signed-off-by: Pali Rohár <pali.rohar@gmail.com>
> Tested-by: Fabio D'Urso <fabiodurso@hotmail.it>
> Acked-by: Henrique de Moraes Holschuh <hmh@hmh.eng.br>
> Signed-off-by: Darren Hart <dvhart@linux.intel.com>
> (cherry picked from commit bb28f3d51ff5e1be541d057708011cc1efe6fae9)
> Signed-off-by: Tim Gardner <tim.gardner@canonical.com>
> ---
>  drivers/platform/x86/thinkpad_acpi.c | 206 +++++++++++++++++++++++++++++++++++
>  1 file changed, 206 insertions(+)
> 
> diff --git a/drivers/platform/x86/thinkpad_acpi.c b/drivers/platform/x86/thinkpad_acpi.c
> index 0bed473..a268a7a 100644
> --- a/drivers/platform/x86/thinkpad_acpi.c
> +++ b/drivers/platform/x86/thinkpad_acpi.c
> @@ -303,6 +303,7 @@ static struct {
>  	u32 hotkey_mask:1;
>  	u32 hotkey_wlsw:1;
>  	u32 hotkey_tablet:1;
> +	u32 kbdlight:1;
>  	u32 light:1;
>  	u32 light_status:1;
>  	u32 bright_acpimode:1;
> @@ -4986,6 +4987,207 @@ static struct ibm_struct video_driver_data = {
>  #endif /* CONFIG_THINKPAD_ACPI_VIDEO */
>  
>  /*************************************************************************
> + * Keyboard backlight subdriver
> + */
> +
> +static int kbdlight_set_level(int level)
> +{
> +	if (!hkey_handle)
> +		return -ENXIO;
> +
> +	if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level))
> +		return -EIO;
> +
> +	return 0;
> +}
> +
> +static int kbdlight_get_level(void)
> +{
> +	int status = 0;
> +
> +	if (!hkey_handle)
> +		return -ENXIO;
> +
> +	if (!acpi_evalf(hkey_handle, &status, "MLCG", "dd", 0))
> +		return -EIO;
> +
> +	if (status < 0)
> +		return status;
> +
> +	return status & 0x3;
> +}
> +
> +static bool kbdlight_is_supported(void)
> +{
> +	int status = 0;
> +
> +	if (!hkey_handle)
> +		return false;
> +
> +	if (!acpi_has_method(hkey_handle, "MLCG")) {
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG is unavailable\n");
> +		return false;
> +	}
> +
> +	if (!acpi_evalf(hkey_handle, &status, "MLCG", "qdd", 0)) {
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG failed\n");
> +		return false;
> +	}
> +
> +	if (status < 0) {
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG err: %d\n", status);
> +		return false;
> +	}
> +
> +	vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG returned 0x%x\n", status);
> +	/*
> +	 * Guessed test for keyboard backlight:
> +	 *
> +	 * Machines with backlight keyboard return:
> +	 *   b010100000010000000XX - ThinkPad X1 Carbon 3rd
> +	 *   b110100010010000000XX - ThinkPad x230
> +	 *   b010100000010000000XX - ThinkPad x240
> +	 *   b010100000010000000XX - ThinkPad W541
> +	 * (XX is current backlight level)
> +	 *
> +	 * Machines without backlight keyboard return:
> +	 *   b10100001000000000000 - ThinkPad x230
> +	 *   b10110001000000000000 - ThinkPad E430
> +	 *   b00000000000000000000 - ThinkPad E450
> +	 *
> +	 * Candidate BITs for detection test (XOR):
> +	 *   b01000000001000000000
> +	 *              ^
> +	 */
> +	return status & BIT(9);
> +}
> +
> +static void kbdlight_set_worker(struct work_struct *work)
> +{
> +	struct tpacpi_led_classdev *data =
> +			container_of(work, struct tpacpi_led_classdev, work);
> +
> +	if (likely(tpacpi_lifecycle == TPACPI_LIFE_RUNNING))
> +		kbdlight_set_level(data->new_state);
> +}
> +
> +static void kbdlight_sysfs_set(struct led_classdev *led_cdev,
> +			enum led_brightness brightness)
> +{
> +	struct tpacpi_led_classdev *data =
> +			container_of(led_cdev,
> +				     struct tpacpi_led_classdev,
> +				     led_classdev);
> +	data->new_state = brightness;
> +	queue_work(tpacpi_wq, &data->work);
> +}
> +
> +static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
> +{
> +	int level;
> +
> +	level = kbdlight_get_level();
> +	if (level < 0)
> +		return 0;
> +
> +	return level;
> +}
> +
> +static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
> +	.led_classdev = {
> +		.name		= "tpacpi::kbd_backlight",
> +		.max_brightness	= 2,
> +		.brightness_set	= &kbdlight_sysfs_set,
> +		.brightness_get	= &kbdlight_sysfs_get,
> +		.flags		= LED_CORE_SUSPENDRESUME,
> +	}
> +};
> +
> +static int __init kbdlight_init(struct ibm_init_struct *iibm)
> +{
> +	int rc;
> +
> +	vdbg_printk(TPACPI_DBG_INIT, "initializing kbdlight subdriver\n");
> +
> +	TPACPI_ACPIHANDLE_INIT(hkey);
> +	INIT_WORK(&tpacpi_led_kbdlight.work, kbdlight_set_worker);
> +
> +	if (!kbdlight_is_supported()) {
> +		tp_features.kbdlight = 0;
> +		vdbg_printk(TPACPI_DBG_INIT, "kbdlight is unsupported\n");
> +		return 1;
> +	}
> +
> +	tp_features.kbdlight = 1;
> +
> +	rc = led_classdev_register(&tpacpi_pdev->dev,
> +				   &tpacpi_led_kbdlight.led_classdev);
> +	if (rc < 0) {
> +		tp_features.kbdlight = 0;
> +		return rc;
> +	}
> +
> +	return 0;
> +}
> +
> +static void kbdlight_exit(void)
> +{
> +	if (tp_features.kbdlight)
> +		led_classdev_unregister(&tpacpi_led_kbdlight.led_classdev);
> +	flush_workqueue(tpacpi_wq);
> +}
> +
> +static int kbdlight_read(struct seq_file *m)
> +{
> +	int level;
> +
> +	if (!tp_features.kbdlight) {
> +		seq_printf(m, "status:\t\tnot supported\n");
> +	} else {
> +		level = kbdlight_get_level();
> +		if (level < 0)
> +			seq_printf(m, "status:\t\terror %d\n", level);
> +		else
> +			seq_printf(m, "status:\t\t%d\n", level);
> +		seq_printf(m, "commands:\t0, 1, 2\n");
> +	}
> +
> +	return 0;
> +}
> +
> +static int kbdlight_write(char *buf)
> +{
> +	char *cmd;
> +	int level = -1;
> +
> +	if (!tp_features.kbdlight)
> +		return -ENODEV;
> +
> +	while ((cmd = next_cmd(&buf))) {
> +		if (strlencmp(cmd, "0") == 0)
> +			level = 0;
> +		else if (strlencmp(cmd, "1") == 0)
> +			level = 1;
> +		else if (strlencmp(cmd, "2") == 0)
> +			level = 2;
> +		else
> +			return -EINVAL;
> +	}
> +
> +	if (level == -1)
> +		return -EINVAL;
> +
> +	return kbdlight_set_level(level);
> +}
> +
> +static struct ibm_struct kbdlight_driver_data = {
> +	.name = "kbdlight",
> +	.read = kbdlight_read,
> +	.write = kbdlight_write,
> +	.exit = kbdlight_exit,
> +};
> +
> +/*************************************************************************
>   * Light (thinklight) subdriver
>   */
>  
> @@ -9207,6 +9409,10 @@ static struct ibm_init_struct ibms_init[] __initdata = {
>  	},
>  #endif
>  	{
> +		.init = kbdlight_init,
> +		.data = &kbdlight_driver_data,
> +	},
> +	{
>  		.init = light_init,
>  		.data = &light_driver_data,
>  	},
> -- 

This is h/w enablement, but nicely self contained in the thinkpad
driver.  It looks reasonable assuming we are getting/already have the
bits to leverage it in gnome/unity.

Acked-by: Andy Whitcroft <apw@canonical.com>

-apw
Kamal Mostafa April 26, 2016, 2:56 p.m. UTC | #4

diff mbox

Patch

diff --git a/drivers/platform/x86/thinkpad_acpi.c b/drivers/platform/x86/thinkpad_acpi.c
index 0bed473..a268a7a 100644
--- a/drivers/platform/x86/thinkpad_acpi.c
+++ b/drivers/platform/x86/thinkpad_acpi.c
@@ -303,6 +303,7 @@  static struct {
 	u32 hotkey_mask:1;
 	u32 hotkey_wlsw:1;
 	u32 hotkey_tablet:1;
+	u32 kbdlight:1;
 	u32 light:1;
 	u32 light_status:1;
 	u32 bright_acpimode:1;
@@ -4986,6 +4987,207 @@  static struct ibm_struct video_driver_data = {
 #endif /* CONFIG_THINKPAD_ACPI_VIDEO */
 
 /*************************************************************************
+ * Keyboard backlight subdriver
+ */
+
+static int kbdlight_set_level(int level)
+{
+	if (!hkey_handle)
+		return -ENXIO;
+
+	if (!acpi_evalf(hkey_handle, NULL, "MLCS", "dd", level))
+		return -EIO;
+
+	return 0;
+}
+
+static int kbdlight_get_level(void)
+{
+	int status = 0;
+
+	if (!hkey_handle)
+		return -ENXIO;
+
+	if (!acpi_evalf(hkey_handle, &status, "MLCG", "dd", 0))
+		return -EIO;
+
+	if (status < 0)
+		return status;
+
+	return status & 0x3;
+}
+
+static bool kbdlight_is_supported(void)
+{
+	int status = 0;
+
+	if (!hkey_handle)
+		return false;
+
+	if (!acpi_has_method(hkey_handle, "MLCG")) {
+		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG is unavailable\n");
+		return false;
+	}
+
+	if (!acpi_evalf(hkey_handle, &status, "MLCG", "qdd", 0)) {
+		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG failed\n");
+		return false;
+	}
+
+	if (status < 0) {
+		vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG err: %d\n", status);
+		return false;
+	}
+
+	vdbg_printk(TPACPI_DBG_INIT, "kbdlight MLCG returned 0x%x\n", status);
+	/*
+	 * Guessed test for keyboard backlight:
+	 *
+	 * Machines with backlight keyboard return:
+	 *   b010100000010000000XX - ThinkPad X1 Carbon 3rd
+	 *   b110100010010000000XX - ThinkPad x230
+	 *   b010100000010000000XX - ThinkPad x240
+	 *   b010100000010000000XX - ThinkPad W541
+	 * (XX is current backlight level)
+	 *
+	 * Machines without backlight keyboard return:
+	 *   b10100001000000000000 - ThinkPad x230
+	 *   b10110001000000000000 - ThinkPad E430
+	 *   b00000000000000000000 - ThinkPad E450
+	 *
+	 * Candidate BITs for detection test (XOR):
+	 *   b01000000001000000000
+	 *              ^
+	 */
+	return status & BIT(9);
+}
+
+static void kbdlight_set_worker(struct work_struct *work)
+{
+	struct tpacpi_led_classdev *data =
+			container_of(work, struct tpacpi_led_classdev, work);
+
+	if (likely(tpacpi_lifecycle == TPACPI_LIFE_RUNNING))
+		kbdlight_set_level(data->new_state);
+}
+
+static void kbdlight_sysfs_set(struct led_classdev *led_cdev,
+			enum led_brightness brightness)
+{
+	struct tpacpi_led_classdev *data =
+			container_of(led_cdev,
+				     struct tpacpi_led_classdev,
+				     led_classdev);
+	data->new_state = brightness;
+	queue_work(tpacpi_wq, &data->work);
+}
+
+static enum led_brightness kbdlight_sysfs_get(struct led_classdev *led_cdev)
+{
+	int level;
+
+	level = kbdlight_get_level();
+	if (level < 0)
+		return 0;
+
+	return level;
+}
+
+static struct tpacpi_led_classdev tpacpi_led_kbdlight = {
+	.led_classdev = {
+		.name		= "tpacpi::kbd_backlight",
+		.max_brightness	= 2,
+		.brightness_set	= &kbdlight_sysfs_set,
+		.brightness_get	= &kbdlight_sysfs_get,
+		.flags		= LED_CORE_SUSPENDRESUME,
+	}
+};
+
+static int __init kbdlight_init(struct ibm_init_struct *iibm)
+{
+	int rc;
+
+	vdbg_printk(TPACPI_DBG_INIT, "initializing kbdlight subdriver\n");
+
+	TPACPI_ACPIHANDLE_INIT(hkey);
+	INIT_WORK(&tpacpi_led_kbdlight.work, kbdlight_set_worker);
+
+	if (!kbdlight_is_supported()) {
+		tp_features.kbdlight = 0;
+		vdbg_printk(TPACPI_DBG_INIT, "kbdlight is unsupported\n");
+		return 1;
+	}
+
+	tp_features.kbdlight = 1;
+
+	rc = led_classdev_register(&tpacpi_pdev->dev,
+				   &tpacpi_led_kbdlight.led_classdev);
+	if (rc < 0) {
+		tp_features.kbdlight = 0;
+		return rc;
+	}
+
+	return 0;
+}
+
+static void kbdlight_exit(void)
+{
+	if (tp_features.kbdlight)
+		led_classdev_unregister(&tpacpi_led_kbdlight.led_classdev);
+	flush_workqueue(tpacpi_wq);
+}
+
+static int kbdlight_read(struct seq_file *m)
+{
+	int level;
+
+	if (!tp_features.kbdlight) {
+		seq_printf(m, "status:\t\tnot supported\n");
+	} else {
+		level = kbdlight_get_level();
+		if (level < 0)
+			seq_printf(m, "status:\t\terror %d\n", level);
+		else
+			seq_printf(m, "status:\t\t%d\n", level);
+		seq_printf(m, "commands:\t0, 1, 2\n");
+	}
+
+	return 0;
+}
+
+static int kbdlight_write(char *buf)
+{
+	char *cmd;
+	int level = -1;
+
+	if (!tp_features.kbdlight)
+		return -ENODEV;
+
+	while ((cmd = next_cmd(&buf))) {
+		if (strlencmp(cmd, "0") == 0)
+			level = 0;
+		else if (strlencmp(cmd, "1") == 0)
+			level = 1;
+		else if (strlencmp(cmd, "2") == 0)
+			level = 2;
+		else
+			return -EINVAL;
+	}
+
+	if (level == -1)
+		return -EINVAL;
+
+	return kbdlight_set_level(level);
+}
+
+static struct ibm_struct kbdlight_driver_data = {
+	.name = "kbdlight",
+	.read = kbdlight_read,
+	.write = kbdlight_write,
+	.exit = kbdlight_exit,
+};
+
+/*************************************************************************
  * Light (thinklight) subdriver
  */
 
@@ -9207,6 +9409,10 @@  static struct ibm_init_struct ibms_init[] __initdata = {
 	},
 #endif
 	{
+		.init = kbdlight_init,
+		.data = &kbdlight_driver_data,
+	},
+	{
 		.init = light_init,
 		.data = &light_driver_data,
 	},