[i2c] [PATCH] Lifebook apanel driver

Jean Delvare khali at linux-fr.org
Fri Jan 5 13:25:21 CET 2007


Hi Stephen,

On Thu, 4 Jan 2007 14:27:37 -0800, Stephen Hemminger wrote:
> Revised version based on comments. Also adds suspend/resume hooks.
> 
> This driver supports the applications buttons on the Fujitsu Lifebook laptops.
> These buttons are read via the SMBus for more details see:
> 	http://apanel.sourceforge.net/tech.php
> The buttons are routed as normal special keys to the input system.
> 
> It is based on the earlier apanel driver done by Jochen Eisenger, but with many changes.
> The original driver used ioctl's and a separate user space program, this version hooks
> into the input subsystem so that the normal Gnome/KDE shortcuts work without any
> userspace changes.
> 
> The LED support may need work.
> It has been tested (a little) on the two laptop's listed in the DMI table. 
> It probably works on more but may require some changes.
> 
> Signed-off-by: Stephen Hemminger <shemminger at osdl.org>
> 
> ---
>  drivers/input/misc/Kconfig  |   12 +
>  drivers/input/misc/Makefile |    1 
>  drivers/input/misc/apanel.c |  439 ++++++++++++++++++++++++++++++++++++++++++++
>  3 files changed, 452 insertions(+)
> 
> --- lifebook.orig/drivers/input/misc/Kconfig	2007-01-04 14:16:18.000000000 -0800
> +++ lifebook/drivers/input/misc/Kconfig	2007-01-04 14:17:18.000000000 -0800
> @@ -50,6 +50,18 @@
>  	  To compile this driver as a module, choose M here: the module will
>  	  be called wistron_btns.
>  
> +config INPUT_APANEL
> +	tristate "Fujitsu Lifebook Application Panel buttons"
> +	depends on X86 && !X86_64
> +	select I2C_I801
> +	help
> +	 Say Y here for support of the Application Panel buttons, used on
> +	 Fujitsu Lifebook. THese are attached to the via the mainboard I2C
> +	 interface.
> +
> +	 To compile this driver as a module, choose M here: the module will
> +	 be called apanel.
> +
>  config INPUT_IXP4XX_BEEPER
>  	tristate "IXP4XX Beeper support"
>  	depends on ARCH_IXP4XX
> --- lifebook.orig/drivers/input/misc/Makefile	2007-01-04 14:16:18.000000000 -0800
> +++ lifebook/drivers/input/misc/Makefile	2007-01-04 14:17:18.000000000 -0800
> @@ -9,5 +9,6 @@
>  obj-$(CONFIG_INPUT_M68K_BEEP)		+= m68kspkr.o
>  obj-$(CONFIG_INPUT_UINPUT)		+= uinput.o
>  obj-$(CONFIG_INPUT_WISTRON_BTNS)	+= wistron_btns.o
> +obj-$(CONFIG_INPUT_APANEL)		+= apanel.o
>  obj-$(CONFIG_HP_SDC_RTC)		+= hp_sdc_rtc.o
>  obj-$(CONFIG_INPUT_IXP4XX_BEEPER)	+= ixp4xx-beeper.o
> --- /dev/null	1970-01-01 00:00:00.000000000 +0000
> +++ lifebook/drivers/input/misc/apanel.c	2007-01-04 14:20:40.000000000 -0800
> @@ -0,0 +1,439 @@
> +/*
> + *   SMBus client for the Fujitsu Lifebook Application Panel
> + *
> + *  Copyright (C) 2007 Stephen Hemminger <shemminger at osdl.org>
> + *  Copyright (C) 2001-2003 Jochen Eisinger <jochen at penguin-breeder.org>
> + *
> + * This program is free software; you can redistribute it and/or modify it
> + * under the terms of the GNU General Public License version 2 as published by
> + * the Free Software Foundation.
> + */
> +
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/ioport.h>
> +#include <linux/io.h>
> +#include <linux/module.h>
> +#include <linux/dmi.h>
> +#include <linux/input.h>
> +#include <linux/i2c.h>
> +#include <linux/workqueue.h>
> +
> +/* List of systems known to work */
> +static struct dmi_system_id apanel_dmi_table[] __initdata = {
> +	{
> +		.ident = "Lifebook S",
> +		.matches = {
> +			DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook S Series"),
> +		},
> +	},
> +	{
> +		.ident = "Lifebook B6210",
> +		.matches = {
> +			DMI_MATCH(DMI_PRODUCT_NAME, "Lifebook B6210"),
> +		},
> +	},
> +	{ }
> +};
> +
> +enum apanel_devid {
> +	APANEL_DEV_NONE  = 0,
> +	APANEL_DEV_APPBTN= 1,
> +	APANEL_DEV_CDBTN = 2,
> +	APANEL_DEV_LCD	 = 3,
> +	APANEL_DEV_LED	 = 4,
> +
> +	APANEL_DEV_MAX,
> +};
> +
> +enum apanel_chip {
> +	CHIP_NONE   = 0,
> +	CHIP_OZ992C = 1,
> +	CHIP_OZ163T = 2,
> +	CHIP_OZ711M3= 4,
> +};
> +
> +static u8 device_chip[APANEL_DEV_MAX];
> +
> +static const char *device_names[APANEL_DEV_MAX] = {
> +	[APANEL_DEV_APPBTN] = "Application Buttons",
> +	[APANEL_DEV_LCD]    = "LCD",
> +	[APANEL_DEV_LED]    = "LED",
> +	[APANEL_DEV_CDBTN]  = "CD Buttons",
> +};

All these enums and array are padded with spaces where tabs should be
used...

> +
> +/* Panel is a singleton - only one per laptop */
> +struct apanel {
> +	struct input_dev *input;
> +	struct i2c_client client;
> +	struct delayed_work poll_timer;
> +	struct work_struct led_work;
> +};
> +
> +#define POLL_FREQUENCY 10 /* Number of polls per second */
> +
> +#if POLL_FREQUENCY > HZ
> +#error "POLL_FREQUENCY too high"
> +#endif
> +
> +MODULE_AUTHOR("Stephen Hemminger <shemminger at osdl.org>");
> +MODULE_DESCRIPTION("Fujitsu Lifebook Application Panel driver");
> +MODULE_LICENSE("GPL");
> +MODULE_VERSION("0.2");
> +
> +/* NB: can't call this 'force' because of I2C_INSMOD macro foolishness. */
> +static int dmi_force;
> +module_param(dmi_force, bool, 0);
> +MODULE_PARM_DESC(dmi_force, "Load even if computer is not in database");
> +
> +/* definitions for i2c (smbus) interface */
> +static int apanel_detach_client(struct i2c_client *client);
> +static int apanel_attach_adapter(struct i2c_adapter *adap);
> +#ifdef CONFIG_PM
> +static int apanel_suspend(struct device *, pm_message_t state);
> +static int apanel_resume(struct device *);
> +#endif
> +
> +static struct i2c_driver apanel_driver = {
> +	.driver = {
> +		.name = "apanel",
> +#ifdef CONFIG_PM
> +		.suspend = apanel_suspend,
> +		.resume = apanel_resume,
> +#endif
> +	},
> +	.attach_adapter = &apanel_attach_adapter,
> +	.detach_client  = &apanel_detach_client,
> +};

The usual way to handle this would be to define apanel_suspend and
apanel_resume as NULL when CONFIG_PM isn't set. And moving the
definition of apanel_driver until after all the callbacks are defined
so that you don't need the forward declarations. The benefit is that
the CONFIG_PM conditional is centralized in a single place. See
drivers/hwmon/abituguru.c as an example.

> +
> +/* for now, we only support one address */
> +static unsigned short normal_i2c[] = {0, I2C_CLIENT_END};
> +
> +/* generate some stupid additional structures.  */
> +I2C_CLIENT_INSMOD;
> +
> +/* Poll for key changes every 100ms
> + *
> + * Read Application keys via SMI
> + *  A (0x4), B (0x8), Internet (0x2), Email (0x1).
> + */
> +static const struct keymap {
> +	u32 mask;
> +	u8 keycode;
> +} keymap[] = {
> +	{ 0x1, 	KEY_EMAIL },
> +	{ 0x2,	KEY_WWW},
> +	{ 0x4,	KEY_PROG1},
> +	{ 0x8,	KEY_PROG2},
> +
> +	{ 0x400,KEY_STOPCD},
> +	{ 0x800,KEY_PLAYPAUSE},
> +	{ 0x200,KEY_REWIND},
> +	{ 0x100,KEY_FORWARD},
> +};
> +
> +
> +static void report_key(struct input_dev *input,unsigned keycode)
> +{
> +	input_report_key(input, keycode, 1);
> +	input_sync(input);
> +	input_report_key(input, keycode, 0);
> +	input_sync(input);
> +}
> +
> +static void apanel_poll(struct work_struct *work)
> +{
> +	struct apanel *ap = container_of(work, struct apanel, poll_timer.work);
> +	u8 cmd = device_chip[APANEL_DEV_APPBTN] == CHIP_OZ992C ? 0 : 8;
> +	int i;
> +	s32 data;
> +
> +	data = i2c_smbus_read_word_data(&ap->client, cmd);
> +	if (data == -1) {

I would suggest that you check for "< 0" instead. There is no guarantee
that the error code will be exactly "-1".

> +		pr_debug("apanel:smbus read error??\n");

Missing space between "apanel:" and "smbus" (which would be better
spelled SMBus.) You could also drop the question marks, this _is_ an
SMBus read error.

> +		goto reschedule;
> +	}
> +
> +	i2c_smbus_write_word_data(&ap->client, cmd, 0);
> +	if (data)
> +		pr_debug("apanel: poll data=%#x\n", data);
> +
> +	for (i = 0; i < ARRAY_SIZE(keymap); i++)
> +		if (data & keymap[i].mask)
> +			report_key(ap->input, keymap[i].keycode);
> +
> +reschedule:
> +	schedule_delayed_work(&ap->poll_timer, POLL_FREQUENCY);
> +}
> +
> +/* Track state changes of LED */
> +static void apanel_led(struct work_struct *work)
> +{
> +	struct apanel *ap = container_of(work, struct apanel, led_work);
> +	int onoff = test_bit(LED_MISC, ap->input->led);
> +
> +	pr_debug("apanel: led %s\n", onoff ? "on" : "off");
> +	i2c_smbus_write_word_data(&ap->client, 0x10, onoff ? 0x8000 : 0);
> +}
> +
> +/* Callback from input layer when state change happens.
> + * Used to handle LED control.
> + */
> +static int apanel_event(struct input_dev *dev, unsigned int type,
> +			unsigned int code, int value)
> +{
> +	struct apanel *ap = dev->private;
> +
> +	pr_debug("apanel event type %d\n", type);
> +	if (device_chip[APANEL_DEV_LED] == CHIP_NONE || type != EV_LED)
> +		return -1;
> +
> +	schedule_work(&ap->led_work);
> +	return 0;
> +}
> +
> +static int apanel_input_open(struct input_dev *dev)
> +{
> +	struct apanel *ap = dev->private;
> +
> +	schedule_delayed_work(&ap->poll_timer, POLL_FREQUENCY);
> +	return 0;
> +}
> +
> +static void apanel_input_close(struct input_dev *dev)
> +{
> +	struct apanel *ap = dev->private;
> +
> +	cancel_rearming_delayed_work(&ap->poll_timer);
> +}
> +
> +/*
> +  basically this function should probe the i2c client, but we know that it has
> +  to be the one we're looking for - and I have no idea how I should probe for
> +  it, so we just register...
> + */
> +static int apanel_probe(struct i2c_adapter *adap, int addr, int kind)
> +{
> +	struct apanel *ap;
> +	struct input_dev *input;
> +	int err = -ENOMEM;
> +
> +	pr_debug("apanel: probe adapter %p addr %d kind %d\n",
> +		 adap, addr, kind);
> +
> +	ap = kzalloc(sizeof(*ap), GFP_KERNEL);
> +	if (!ap)
> +		goto out1;
> +
> +	input = input_allocate_device();
> +	if (!input)
> +		goto out1;
> +
> +	ap->input = input;
> +	strlcpy(ap->client.name, "apanel", I2C_NAME_SIZE);
> +	ap->client.driver = &apanel_driver;
> +	ap->client.adapter = adap;
> +	ap->client.addr = addr;
> +	INIT_DELAYED_WORK(&ap->poll_timer, apanel_poll);
> +	INIT_WORK(&ap->led_work, apanel_led);
> +	i2c_set_clientdata(&ap->client, ap);
> +
> +	input->name = "Lifebook Panel buttons";
> +	input->phys = "apanel/input0";
> +	input->id.bustype = BUS_HOST;
> +	input->cdev.dev = &ap->client.dev;
> +	input->private = ap;
> +	input->open = apanel_input_open;
> +	input->close = apanel_input_close;
> +
> +	if (device_chip[APANEL_DEV_APPBTN] != CHIP_NONE) {
> +		input->evbit[LONG(EV_KEY)] = BIT(EV_KEY);
> +		set_bit(KEY_PROG1, input->keybit);
> +		set_bit(KEY_PROG2, input->keybit);
> +		set_bit(KEY_EMAIL, input->keybit);
> +		set_bit(KEY_WWW, input->keybit);
> +	}
> +
> +	if (device_chip[APANEL_DEV_CDBTN] != CHIP_NONE) {
> +		input->evbit[LONG(EV_KEY)] = BIT(EV_KEY);
> +		set_bit(KEY_STOPCD, input->keybit);
> +		set_bit(KEY_PLAYPAUSE, input->keybit);
> +		set_bit(KEY_REWIND, input->keybit);
> +		set_bit(KEY_FORWARD, input->keybit);
> +	}
> +
> +	if (device_chip[APANEL_DEV_LED] != CHIP_NONE) {
> +		input->event = apanel_event;
> +		input->evbit[0] |= BIT(EV_LED);
> +		input->ledbit[0] = BIT(LED_MISC);
> +	}
> +
> +	err = i2c_attach_client(&ap->client);
> +	if (err)
> +		goto out2;
> +
> +	err = input_register_device(input);
> +	if (err)
> +		goto out3;
> +
> +	return 0;
> +out3:
> +	i2c_detach_client(&ap->client);
> +out2:
> +	input_free_device(input);
> +out1:
> +	kfree(ap);
> +	return err;
> +}
> +
> +static int apanel_detach_client(struct i2c_client *client)
> +{
> +	struct apanel *ap = i2c_get_clientdata(client);
> +
> +	pr_debug("detach_client\n");
> +	input_unregister_device(ap->input);
> +	input_free_device(ap->input);
> +
> +	i2c_detach_client(&ap->client);
> +	kfree(ap);
> +	return 0;
> +}
> +
> +/* this function is invoked for every i2c adapter. */
> +static int apanel_attach_adapter(struct i2c_adapter *adap)
> +{
> +	/* Our device is connected only to i801 on laptop */
> +	if (adap->id != I2C_HW_SMBUS_I801)
> +		return -ENODEV;
> +
> +	return i2c_probe(adap, &addr_data, apanel_probe);
> +}
> +
> +/* scan the system rom for the signature "FJKEYINF" */
> +static __init void __iomem *bios_signature(void)
> +{
> +	void __iomem *bios;
> +	ssize_t offset;
> +	const unsigned char signature[] = "FJKEYINF";
> +
> +	bios = ioremap(0xF0000, 0x10000); /* Can't fail */
> +
> +	for (offset = 0; offset < 0x10000; offset += 0x10) {
> +		if (check_signature(bios + offset, signature,
> +				    sizeof(signature)-1))
> +			return bios + offset;
> +	}
> +
> +	printk(KERN_ERR  "apanel: Fujitsu BIOS signature '%s' not found...\n",
> +	       signature);
> +	iounmap(bios);
> +	return NULL;
> +}
> +
> +#ifdef CONFIG_PM
> +/* If input device was opened, then stop polling */
> +static int apanel_suspend(struct device *dev, pm_message_t state)
> +{
> +	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
> +	struct apanel *ap = i2c_get_clientdata(client);
> +	struct input_dev *input = ap->input;
> +
> +	mutex_lock(&input->mutex);
> +	if (input->users)
> +		cancel_rearming_delayed_work(&ap->poll_timer);
> +	mutex_unlock(&input->mutex);
> +	return 0;
> +}
> +
> +/* If input device was opened, then resume polling */
> +static int apanel_resume(struct device *dev)
> +{
> +	struct i2c_client *client = container_of(dev, struct i2c_client, dev);
> +	struct apanel *ap = i2c_get_clientdata(client);
> +	struct input_dev *input = ap->input;
> +
> +	mutex_lock(&input->mutex);
> +	if (input->users)
> +		schedule_delayed_work(&ap->poll_timer, POLL_FREQUENCY);
> +	mutex_unlock(&input->mutex);
> +	return 0;
> +}
> +#endif
> +
> +static int __init apanel_init(void)
> +{
> +	void __iomem *bios;
> +	u8 devno;
> +	int found = 0;
> +
> +	if (!dmi_check_system(apanel_dmi_table)) {
> +		printk(KERN_WARNING "apanel: DMI information does not match\n");
> +		if (!dmi_force)
> +			return -ENODEV;
> +	}
> +
> +	bios = bios_signature();
> +	if (!bios)
> +		return -ENODEV;
> +
> +	bios += 8;
> +
> +	/* just use the first address */
> +	normal_i2c[0] = readb(bios+3) >> 1;
> +
> +	for ( ; (devno = readb(bios)) & 0x7f; bios += 4) {
> +		unsigned char method, slave, chip;
> +
> +		method = readb(bios + 1);
> +		chip = readb(bios + 2);
> +		slave = readb(bios + 3) >> 1;
> +
> +		if (slave != normal_i2c[0]) {
> +			printk(KERN_ERR "apanel: only one SMBus slave "
> +				 "address supported, skiping device...\n");
> +			continue;
> +		}
> +
> +		/* translate alternative device numbers */
> +		switch (devno) {
> +		case 6:
> +			devno = APANEL_DEV_APPBTN;
> +			break;
> +		case 7:
> +			devno = APANEL_DEV_LED;
> +			break;
> +		}
> +
> +		if (devno >= APANEL_DEV_MAX)
> +			printk(KERN_WARNING "apanel: unknown device %d found\n",
> +			       devno);
> +		else if (device_chip[devno] != CHIP_NONE)
> +			printk(KERN_ERR "apanel: duplicate entry for %s\n",
> +			       device_names[devno]);
> +
> +		else if (method != 1 && method != 2 && method != 4) {
> +			printk(KERN_ERR "apanel: unknown  method %u for %s\n",
> +			       method, device_names[devno]);
> +		} else {
> +			pr_debug("apanel: %s found, chip=%d\n",
> +				 device_names[devno], chip);
> +
> +			device_chip[devno] = (enum apanel_chip) chip;
> +			++found;
> +		}
> +	}
> +	iounmap(bios);
> +
> +	if (found == 0) {
> +		printk(KERN_ERR "apanel: no input devices reported by BIOS\n");
> +		return -EIO;
> +	}
> +	return i2c_add_driver(&apanel_driver);
> +}
> +module_init(apanel_init);
> +
> +static void __exit apanel_cleanup(void)
> +{
> +	i2c_del_driver(&apanel_driver);
> +}
> +module_exit(apanel_cleanup);

Other than that, the i2c part of the driver is:
Acked-by: Jean Delvare <khali at linux-fr.org>

-- 
Jean Delvare



More information about the i2c mailing list