diff mbox

[RFC] driver for the "Fujitsu Tablet PC Buttons" device

Message ID 200910121436.30732.khnz@gmx.de (mailing list archive)
State Superseded
Headers show

Commit Message

Robert Gerlach Oct. 12, 2009, 12:36 p.m. UTC
None
diff mbox

Patch

--- linux/drivers/input/misc/Kconfig
+++ linux/drivers/input/misc/Kconfig
@@ -54,6 +54,17 @@ 
 	 To compile this driver as a module, choose M here: the module will
 	 be called apanel.
 
+config INPUT_FSC_BTNS
+	tristate "Fujitsu tablet PCs buttons"
+	depends on X86
+	help
+	 Say Y here for support of the tablet buttons and the display
+	 orientation switch, used on many Fujitsu tablet PCs (like
+	 Stylistic ST5xxx, Lifebook P1xxx and Lifebook T-Series).
+
+	 To compile this driver as a module, choose M here: the module will
+	 be called fsc_btns.
+
 config INPUT_IXP4XX_BEEPER
 	tristate "IXP4XX Beeper support"
 	depends on ARCH_IXP4XX

--- linux/drivers/input/misc/fsc_btns.c
+++ linux/drivers/input/misc/fsc_btns.c
@@ -0,0 +1,714 @@ 
+/* driver for FSC tablet PC buttons
+ *
+ * Copyright (C) 2006-2009 Robert Gerlach <khnz@users.sourceforge.net>
+ * Copyright (C) 2005-2006 Jan Rychter <jan@rychter.com>
+ *
+ * You can redistribute and/or modify this program under the terms of the
+ * GNU General Public License version 2 as published by the Free Software
+ * Foundation.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+/* #define DEBUG */
+
+#define REPEAT_RATE 16
+#define REPEAT_DELAY 700
+#define STICKY_TIMEOUT 1400
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/moduleparam.h>
+#include <linux/version.h>
+#include <linux/dmi.h>
+#include <linux/bitops.h>
+#include <linux/io.h>
+#include <linux/ioport.h>
+#include <linux/acpi.h>
+#include <linux/device.h>
+#include <linux/platform_device.h>
+#include <linux/interrupt.h>
+#include <linux/input.h>
+#include <linux/time.h>
+#include <linux/timer.h>
+#include <linux/delay.h>
+#include <linux/jiffies.h>
+
+#define MODULENAME "fsc_btns"
+
+MODULE_AUTHOR("Robert Gerlach <khnz@users.sourceforge.net>");
+MODULE_DESCRIPTION("Fujitsu Siemens tablet button driver");
+MODULE_LICENSE("GPL");
+MODULE_VERSION("2.1.0");
+
+static const struct acpi_device_id fscbtns_ids[] = {
+	{ .id = "FUJ02BD" },
+	{ .id = "FUJ02BF" },
+	{ .id = "" }
+};
+MODULE_DEVICE_TABLE(acpi, fscbtns_ids);
+
+#if defined(STICKY_TIMEOUT) && (STICKY_TIMEOUT > 0)
+static const unsigned long modification_mask[BITS_TO_LONGS(KEY_MAX)] = {
+		[BIT_WORD(KEY_LEFTSHIFT)]	= BIT_MASK(KEY_LEFTSHIFT),
+		[BIT_WORD(KEY_RIGHTSHIFT)]	= BIT_MASK(KEY_RIGHTSHIFT),
+		[BIT_WORD(KEY_LEFTCTRL)]	= BIT_MASK(KEY_LEFTCTRL),
+		[BIT_WORD(KEY_RIGHTCTRL)]	= BIT_MASK(KEY_RIGHTCTRL),
+		[BIT_WORD(KEY_LEFTALT)]		= BIT_MASK(KEY_LEFTALT),
+		[BIT_WORD(KEY_RIGHTALT)]	= BIT_MASK(KEY_RIGHTALT),
+		[BIT_WORD(KEY_LEFTMETA)]	= BIT_MASK(KEY_LEFTMETA),
+		[BIT_WORD(KEY_RIGHTMETA)]	= BIT_MASK(KEY_RIGHTMETA),
+		[BIT_WORD(KEY_COMPOSE)]		= BIT_MASK(KEY_COMPOSE),
+		[BIT_WORD(KEY_LEFTALT)]		= BIT_MASK(KEY_LEFTALT),
+		[BIT_WORD(KEY_FN)]		= BIT_MASK(KEY_FN)};
+#endif
+
+struct fscbtns_config {
+	int invert_orientation_bit;
+	unsigned int keymap[16];
+};
+
+static struct fscbtns_config config_Lifebook_Tseries __initdata = {
+	.invert_orientation_bit = 1,
+	.keymap = {
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_SCROLLDOWN,
+		KEY_SCROLLUP,
+		KEY_DIRECTION,
+		KEY_LEFTCTRL,
+		KEY_BRIGHTNESSUP,
+		KEY_BRIGHTNESSDOWN,
+		KEY_BRIGHTNESS_ZERO,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_LEFTALT
+	}
+};
+
+static struct fscbtns_config config_Lifebook_U810 __initdata = {
+	.invert_orientation_bit = 1,
+	.keymap = {
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_PROG1,
+		KEY_PROG2,
+		KEY_DIRECTION,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_UP,
+		KEY_DOWN,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_FN,
+		KEY_SLEEP
+	}
+};
+
+static struct fscbtns_config config_Stylistic_Tseries __initdata = {
+	.invert_orientation_bit = 0,
+	.keymap = {
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_PRINT,
+		KEY_BACKSPACE,
+		KEY_SPACE,
+		KEY_ENTER,
+		KEY_BRIGHTNESSUP,
+		KEY_BRIGHTNESSDOWN,
+		KEY_DOWN,
+		KEY_UP,
+		KEY_SCROLLUP,
+		KEY_SCROLLDOWN,
+		KEY_FN
+	}
+};
+
+static struct fscbtns_config config_Stylistic_ST5xxx __initdata = {
+	.invert_orientation_bit = 0,
+	.keymap = {
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_RESERVED,
+		KEY_MAIL,
+		KEY_DIRECTION,
+		KEY_ESC,
+		KEY_ENTER,
+		KEY_BRIGHTNESSUP,
+		KEY_BRIGHTNESSDOWN,
+		KEY_DOWN,
+		KEY_UP,
+		KEY_SCROLLUP,
+		KEY_SCROLLDOWN,
+		KEY_FN,
+		KEY_LEFTALT
+	}
+};
+
+static struct {						/* fscbtns_t */
+	struct platform_device *pdev;
+	struct input_dev *idev_b;		/* tablet buttons */
+	struct input_dev *idev_s;		/* orientation switch */
+#if (defined(STICKY_TIMEOUT) && (STICKY_TIMEOUT > 0))
+	struct timer_list timer;
+#endif
+
+	unsigned int interrupt;
+	unsigned int address;
+	struct fscbtns_config config;
+
+	int orientation;
+} fscbtns;
+
+static unsigned int user_model;
+module_param_named(model, user_model, uint, 0);
+MODULE_PARM_DESC(model, "model (1 = Stylistic, 2 = Lifebook T- and P-Series, 3 = Stylistic ST5xxx, 4 = Lifebook U800)");
+
+
+/*** HELPER *******************************************************************/
+
+static inline u8 fscbtns_ack(void)
+{
+	return inb(fscbtns.address+2);
+}
+
+static inline u8 fscbtns_status(void)
+{
+	return inb(fscbtns.address+6);
+}
+
+static inline u8 fscbtns_read_register(const u8 addr)
+{
+	outb(addr, fscbtns.address);
+	return inb(fscbtns.address+4);
+}
+
+static inline void fscbtns_use_config(struct fscbtns_config *config)
+{
+	memcpy(&fscbtns.config, config, sizeof(struct fscbtns_config));
+}
+
+
+/*** INPUT ********************************************************************/
+
+static int __devinit input_fscbtns_setup_buttons(struct device *dev)
+{
+	struct input_dev *idev;
+	int error;
+	int x;
+
+	idev = input_allocate_device();
+	if (!idev)
+		return -ENOMEM;
+
+	idev->dev.parent = dev;
+	idev->phys = "fsc/input0";
+	idev->name = "fsc tablet buttons";
+	idev->id.bustype = BUS_HOST;
+	idev->id.vendor  = 0x1734;
+	idev->id.product = 0x0001;
+	idev->id.version = 0x0101;
+
+	idev->keycode = fscbtns.config.keymap;
+	idev->keycodesize = sizeof(unsigned int);
+	idev->keycodemax = sizeof(fscbtns.config.keymap) / idev->keycodesize;
+
+	set_bit(EV_REP, idev->evbit);
+	set_bit(EV_KEY, idev->evbit);
+
+	for (x = 0; x < idev->keycodemax; x++)
+		if (((unsigned int *)idev->keycode)[x])
+			set_bit(((unsigned int *)idev->keycode)[x],
+					idev->keybit);
+
+	set_bit(EV_MSC, idev->evbit);
+	set_bit(MSC_SCAN, idev->mscbit);
+
+	error = input_register_device(idev);
+	if (error) {
+		input_free_device(idev);
+		return error;
+	}
+
+	idev->rep[REP_DELAY]  = REPEAT_DELAY;
+	idev->rep[REP_PERIOD] = 1000 / REPEAT_RATE;
+
+	fscbtns.idev_b = idev;
+	return 0;
+}
+
+static int __devinit input_fscbtns_setup_switch(struct device *dev)
+{
+	struct input_dev *idev;
+	int error;
+
+	idev = input_allocate_device();
+	if (!idev)
+		return -ENOMEM;
+
+	idev->dev.parent = dev;
+	idev->phys = "fsc/input1";
+	idev->name = "fsc tablet switch";
+	idev->id.bustype = BUS_HOST;
+	idev->id.vendor  = 0x1734;
+	idev->id.product = 0x0002;
+	idev->id.version = 0x0101;
+
+	set_bit(EV_SW, idev->evbit);
+	set_bit(SW_TABLET_MODE, idev->swbit);
+
+	error = input_register_device(idev);
+	if (error) {
+		input_free_device(idev);
+		return error;
+	}
+
+	fscbtns.idev_s = idev;
+	return 0;
+}
+
+static void input_fscbtns_remove(void)
+{
+	if (fscbtns.idev_b)
+		input_unregister_device(fscbtns.idev_b);
+	if (fscbtns.idev_s)
+		input_unregister_device(fscbtns.idev_s);
+}
+
+static void fscbtns_report_orientation(void)
+{
+	int orientation = fscbtns_read_register(0xdd);
+
+	if (orientation & 0x02) {
+		orientation ^= fscbtns.config.invert_orientation_bit;
+		orientation &= 0x01;
+
+		if (orientation != fscbtns.orientation) {
+			input_report_switch(fscbtns.idev_s, SW_TABLET_MODE,
+					fscbtns.orientation = orientation);
+			input_sync(fscbtns.idev_s);
+		}
+	}
+}
+
+#if defined(STICKY_TIMEOUT) && (STICKY_TIMEOUT > 0)
+static void fscbtns_sticky_timeout(unsigned long keycode)
+{
+	input_report_key(fscbtns.idev_b, keycode, 0);
+	input_sync(fscbtns.idev_b);
+	fscbtns.timer.data = 0;
+}
+
+static inline int fscbtns_sticky_report_key(unsigned int keycode, int pressed)
+{
+	if (pressed) {
+		del_timer(&fscbtns.timer);
+		fscbtns.timer.expires = jiffies + (STICKY_TIMEOUT*HZ)/1000;
+
+		if (fscbtns.timer.data == keycode) {
+			input_report_key(fscbtns.idev_b, keycode, 0);
+			input_sync(fscbtns.idev_b);
+		}
+
+		return 0;
+	}
+
+	if ((fscbtns.timer.data) && (fscbtns.timer.data != keycode)) {
+		input_report_key(fscbtns.idev_b, keycode, 0);
+		input_sync(fscbtns.idev_b);
+		input_report_key(fscbtns.idev_b, fscbtns.timer.data, 0);
+		fscbtns.timer.data = 0;
+		return 1;
+	}
+
+	if (test_bit(keycode, modification_mask) &&
+			(fscbtns.timer.expires > jiffies)) {
+		fscbtns.timer.data = keycode;
+		fscbtns.timer.function = fscbtns_sticky_timeout;
+		add_timer(&fscbtns.timer);
+		return 1;
+	}
+
+	return 0;
+}
+#endif
+
+static void fscbtns_report_key(unsigned int kmindex, int pressed)
+{
+	unsigned int keycode = fscbtns.config.keymap[kmindex];
+	if (keycode == KEY_RESERVED)
+		return;
+
+	if (pressed)
+		input_event(fscbtns.idev_b, EV_MSC, MSC_SCAN, kmindex);
+
+#if defined(STICKY_TIMEOUT) && (STICKY_TIMEOUT > 0)
+	if (fscbtns_sticky_report_key(keycode, pressed))
+		return;
+#endif
+
+	input_report_key(fscbtns.idev_b, keycode, pressed);
+	input_sync(fscbtns.idev_b);
+}
+
+static void fscbtns_event(void)
+{
+	unsigned long keymask;
+	unsigned long changed;
+	static unsigned long prev_keymask;
+
+	fscbtns_report_orientation();
+
+	keymask  = fscbtns_read_register(0xde);
+	keymask |= fscbtns_read_register(0xdf) << 8;
+	keymask ^= 0xffff;
+
+	changed = keymask ^ prev_keymask;
+
+	if (changed) {
+		int x = 0;
+		int pressed = !!(keymask & changed);
+
+		/* save current state and filter not changed bits */
+		prev_keymask = keymask;
+
+		/* get number of changed bit */
+		while (!test_bit(x, &changed))
+			x++;
+
+		fscbtns_report_key(x, pressed);
+	}
+}
+
+
+/*** INTERRUPT ****************************************************************/
+
+static void fscbtns_isr_do(struct work_struct *work)
+{
+	fscbtns_event();
+	fscbtns_ack();
+}
+
+static DECLARE_WORK(isr_wq, fscbtns_isr_do);
+
+static irqreturn_t fscbtns_isr(int irq, void *dev_id)
+{
+	if (!(fscbtns_status() & 0x01))
+		return IRQ_NONE;
+
+	schedule_work(&isr_wq);
+	return IRQ_HANDLED;
+}
+
+
+/*** DEVICE *******************************************************************/
+
+static int fscbtns_busywait(void)
+{
+	int timeout_counter = 100;
+
+	while (fscbtns_status() & 0x02 && --timeout_counter)
+		msleep(10);
+
+	return !timeout_counter;
+}
+
+static void fscbtns_reset(void)
+{
+	fscbtns_ack();
+	if (fscbtns_busywait())
+		printk(KERN_WARNING MODULENAME ": timeout, reset needed!\n");
+}
+
+static int __devinit fscbtns_probe(struct platform_device *pdev)
+{
+	int error;
+
+	error = input_fscbtns_setup_buttons(&pdev->dev);
+	if (error)
+		goto err_input;
+
+	error = input_fscbtns_setup_switch(&pdev->dev);
+	if (error)
+		goto err_input;
+
+	if (!request_region(fscbtns.address, 8, MODULENAME)) {
+		printk(KERN_ERR MODULENAME ": region 0x%04x busy\n",
+				fscbtns.address);
+		error = -EBUSY;
+		goto err_input;
+	}
+
+	fscbtns_reset();
+
+	fscbtns_report_orientation();
+	input_sync(fscbtns.idev_b);
+
+	error = request_irq(fscbtns.interrupt, fscbtns_isr,
+			IRQF_SHARED, MODULENAME, fscbtns_isr);
+	if (error) {
+		printk(KERN_ERR MODULENAME ": unable to get irq %d\n",
+				fscbtns.interrupt);
+		goto err_io;
+	}
+
+	return 0;
+
+err_io:
+	release_region(fscbtns.address, 8);
+err_input:
+	input_fscbtns_remove();
+	return error;
+}
+
+static int __devexit fscbtns_remove(struct platform_device *pdev)
+{
+	free_irq(fscbtns.interrupt, fscbtns_isr);
+	release_region(fscbtns.address, 8);
+	input_fscbtns_remove();
+	return 0;
+}
+
+#ifdef CONFIG_PM
+static int fscbtns_resume(struct platform_device *pdev)
+{
+	fscbtns_reset();
+#if 0 /* because Xorg Bug #9623 */
+	fscbtns_report_orientation();
+#endif
+	return 0;
+}
+#endif
+
+static struct platform_driver fscbtns_platform_driver = {
+	.driver		= {
+		.name	= MODULENAME,
+		.owner	= THIS_MODULE,
+	},
+	.probe		= fscbtns_probe,
+	.remove		= __devexit_p(fscbtns_remove),
+#ifdef CONFIG_PM
+	.resume		= fscbtns_resume,
+#endif
+};
+
+
+/*** ACPI *********************************************************************/
+
+static acpi_status fscbtns_walk_resources(struct acpi_resource *res, void *data)
+{
+	switch (res->type) {
+	case ACPI_RESOURCE_TYPE_IRQ:
+		fscbtns.interrupt = res->data.irq.interrupts[0];
+		return AE_OK;
+
+	case ACPI_RESOURCE_TYPE_IO:
+		fscbtns.address = res->data.io.minimum;
+		return AE_OK;
+
+	case ACPI_RESOURCE_TYPE_END_TAG:
+		if (fscbtns.interrupt && fscbtns.address)
+			return AE_OK;
+		else
+			return AE_NOT_FOUND;
+
+	default:
+		return AE_ERROR;
+	}
+}
+
+static int acpi_fscbtns_add(struct acpi_device *adev)
+{
+	acpi_status status;
+
+	if (!adev)
+		return -EINVAL;
+
+	status = acpi_walk_resources(adev->handle, METHOD_NAME__CRS,
+			fscbtns_walk_resources, NULL);
+	if (ACPI_FAILURE(status))
+		return -ENODEV;
+
+	return 0;
+}
+
+static struct acpi_driver acpi_fscbtns_driver = {
+	.name  = MODULENAME,
+	.class = "hotkey",
+	.ids   = fscbtns_ids,
+	.ops   = {
+		.add    = acpi_fscbtns_add
+	}
+};
+
+
+/*** DMI **********************************************************************/
+
+static int __init fscbtns_dmi_matched(const struct dmi_system_id *dmi)
+{
+	printk(KERN_INFO MODULENAME ": found: %s\n", dmi->ident);
+	fscbtns_use_config(dmi->driver_data);
+	return 1;
+}
+
+static struct dmi_system_id dmi_ids[] __initdata = {
+	{
+		.callback = fscbtns_dmi_matched,
+		.ident = "Fujitsu Siemens P/T Series",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK")
+		},
+		.driver_data = &config_Lifebook_Tseries
+	},
+	{
+		.callback = fscbtns_dmi_matched,
+		.ident = "Fujitsu Lifebook T Series",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook T")
+		},
+		.driver_data = &config_Lifebook_Tseries
+	},
+	{
+		.callback = fscbtns_dmi_matched,
+		.ident = "Fujitsu Siemens Stylistic T Series",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "Stylistic T")
+		},
+		.driver_data = &config_Stylistic_Tseries
+	},
+	{
+		.callback = fscbtns_dmi_matched,
+		.ident = "Fujitsu LifeBook U810",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook U810")
+		},
+		.driver_data = &config_Lifebook_U810
+	},
+	{
+		.callback = fscbtns_dmi_matched,
+		.ident = "Fujitsu Siemens Stylistic ST5xxx Series",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "STYLISTIC ST5")
+		},
+		.driver_data = &config_Stylistic_ST5xxx
+	},
+	{
+		.callback = fscbtns_dmi_matched,
+		.ident = "Fujitsu Siemens Stylistic ST5xxx Series",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"),
+			DMI_MATCH(DMI_PRODUCT_NAME, "Stylistic ST5")
+		},
+		.driver_data = &config_Stylistic_ST5xxx
+	},
+	{
+		.callback = fscbtns_dmi_matched,
+		.ident = "Unknown (using defaults)",
+		.matches = {
+			DMI_MATCH(DMI_SYS_VENDOR, ""),
+			DMI_MATCH(DMI_PRODUCT_NAME, "")
+		},
+		.driver_data = &config_Lifebook_Tseries
+	},
+	{ NULL }
+};
+
+
+/*** MODULE *******************************************************************/
+
+static int __init fscbtns_module_init(void)
+{
+	int error;
+
+	switch (user_model) {
+	case 1:
+		fscbtns_use_config(&config_Stylistic_Tseries);
+		break;
+	case 2:
+		fscbtns_use_config(&config_Lifebook_Tseries);
+		break;
+	case 3:
+		fscbtns_use_config(&config_Stylistic_ST5xxx);
+		break;
+	case 4:
+		fscbtns_use_config(&config_Lifebook_U810);
+		break;
+	default:
+		dmi_check_system(dmi_ids);
+	}
+
+	error = acpi_bus_register_driver(&acpi_fscbtns_driver);
+	if (ACPI_FAILURE(error))
+		return -EINVAL;
+
+	if (!fscbtns.interrupt || !fscbtns.address)
+		return -ENODEV;
+
+#if defined(STICKY_TIMEOUT) && (STICKY_TIMEOUT > 0)
+	init_timer(&fscbtns.timer);
+#endif
+
+	error = platform_driver_register(&fscbtns_platform_driver);
+	if (error)
+		goto err;
+
+	fscbtns.pdev = platform_device_alloc(MODULENAME, -1);
+	if (!fscbtns.pdev) {
+		error = -ENOMEM;
+		goto err_pdrv;
+	}
+
+	error = platform_device_add(fscbtns.pdev);
+	if (error)
+		goto err_pdev;
+
+	return 0;
+
+err_pdev:
+	platform_device_put(fscbtns.pdev);
+err_pdrv:
+	platform_driver_unregister(&fscbtns_platform_driver);
+err:
+	acpi_bus_unregister_driver(&acpi_fscbtns_driver);
+
+#if (defined(STICKY_TIMEOUT) && (STICKY_TIMEOUT > 0))
+	del_timer_sync(&fscbtns.timer);
+#endif
+	return error;
+}
+
+static void __exit fscbtns_module_exit(void)
+{
+	platform_device_unregister(fscbtns.pdev);
+	platform_driver_unregister(&fscbtns_platform_driver);
+	acpi_bus_unregister_driver(&acpi_fscbtns_driver);
+
+#if (defined(STICKY_TIMEOUT) && (STICKY_TIMEOUT > 0))
+	del_timer_sync(&fscbtns.timer);
+#endif
+}
+
+module_init(fscbtns_module_init);
+module_exit(fscbtns_module_exit);

--- linux/drivers/input/misc/Makefile
+++ linux/drivers/input/misc/Makefile
@@ -5,6 +5,7 @@ 
 # Each configuration option enables a list of files.
 
 obj-$(CONFIG_INPUT_APANEL)		+= apanel.o
+obj-$(CONFIG_INPUT_FSC_BTNS)		+= fsc_btns.o
 obj-$(CONFIG_INPUT_ATI_REMOTE)		+= ati_remote.o
 obj-$(CONFIG_INPUT_ATI_REMOTE2)		+= ati_remote2.o
 obj-$(CONFIG_INPUT_ATLAS_BTNS)		+= atlas_btns.o