[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