Hi, In the past few days I have been writing a driver for the FunLight Chip found in A910. It is a I2C National Semiconductor LP3944[1] and in A910 it is used to drive some RGB leds, the flash light and the displays backlights (FYI A910 has also a CLI display (btw do you know if CLI stands for CallerId here?)).
You can see a test here: http://people.openezx.org/ao2/videos/a910-funlight.avi I attach the code here before committing, this is the first driver I write from scratch, so please _be_rude_ on the first attached patch and make all the comments you can think of. The lp3944 driver is generic, and it is thought to be submitted to mainline linux eventually. I tried to separate the low level part, and then to supply a public interface which abstracts the functionalities, opposed to the original code where the layers were all mixed :P Reading others' code (especially code developed from/for big companies) is somehow a socio-psychological activity, you can even read the corporate pressure inside the code (assuming the coder is a good one) and how the application needs tend to pollute kernel code (which we try to avoid here), it has been interesting, and fun (it was a _fun_ and _light_ driver after all... ;)). The only big doubt I still have is how a device driver should expose an interface to other drivers, for now I'm using EXPORT_SYMBOL_GPL() and then I declare some extern functions in the user driver code (a910-leds, here). I know the externs could be easily avoided providing a header file, but then, where should I put such lp3944.h, provided that lp3944.c is in drivers/i2c/chips/ ? Thanks, Antonio [1] http://www.national.com/pf/LP/LP3944.html -- A: Because it messes up the order in which people normally read text. Q: Why is top-posting such a bad thing? A: Top-posting. Q: What is the most annoying thing in e-mail? Web site: http://www.studenti.unina.it/~ospite Public key: http://www.studenti.unina.it/~ospite/aopubkey.asc
I2C driver for National Semiconductor LP3944 Funlight Chip[1] This helper chip can drive up to 8 leds, with two programmable dim modes; it could aven be used as a gpio expander but this driver assumes it is used as a led controller. LP3944 can be found on Motorola A910 smartphone, where it drives the rgb leds, the camera flash light and the displays backlights. [1] http://www.national.com/pf/LP/LP3944.html Signed-off-by: Antonio Ospite <[EMAIL PROTECTED]> Index: linux-2.6.24.2/drivers/i2c/chips/lp3944.c =================================================================== --- /dev/null +++ linux-2.6.24.2/drivers/i2c/chips/lp3944.c @@ -0,0 +1,382 @@ +/* + * lp3944.c - driver for National Semiconductor LP3944 Funlight Chip + * + * Copyright (C) 2008 Antonio Ospite <[EMAIL PROTECTED]> + * + * 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. + * + * 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., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +/* + * TODO: + * + * check the public interface, is it usable? + * + * use stricter types, u8? + * + * check error return codes andhow they propagate, use EINVAL + * + * check endiannes? It's all about single-byte transfers so it should not be + * needed. Can bit order change from arch to arch? + * + * test lp3944_dim_set() function. + * + */ + +#define DEBUG 1 + +#include <linux/module.h> +#include <linux/moduleparam.h> +#include <linux/i2c.h> + +#define LP3944_REG_INPUT1 0x00 /* LED0-7 Input Register (Read Only) */ +#define LP3944_REG_REGISTER1 0x01 /* None (Read Only) */ +#define LP3944_REG_PSC0 0x02 /* Frequency Prescaler 0 (R/W) */ +#define LP3944_REG_PWM0 0x03 /* PWM Register 0 (R/W) */ +#define LP3944_REG_PSC1 0x04 /* Frequency Prescaler 1 (R/W) */ +#define LP3944_REG_PWM1 0x05 /* PWM Register 1 (R/W) */ +#define LP3944_REG_LS0 0x06 /* LED0-3 Selector (R/W) */ +#define LP3944_REG_LS1 0x07 /* LED4-7 Selector (R/W) */ +#define LP3944_REG_REGISTER8 0x08 /* None (R/W) */ +#define LP3944_REG_REGISTER9 0x09 /* None (R/W) */ + +#define LP3944_LED_STATUS_OFF 0x0 +#define LP3944_LED_STATUS_ON 0x1 +#define LP3944_LED_STATUS_DIM0 0x2 +#define LP3944_LED_STATUS_DIM1 0x3 +#define LP3944_LED_STATUS_MASK 0x3 + +struct i2c_client *save_client; + +static int lp3944_reg_read(struct i2c_client *, int, unsigned int *); +static int lp3944_reg_write(struct i2c_client *, int, unsigned int); + +/* public interface, move to header file? */ +#define LP3944_LED0 0 +#define LP3944_LED1 1 +#define LP3944_LED2 2 +#define LP3944_LED3 3 +#define LP3944_LED4 4 +#define LP3944_LED5 5 +#define LP3944_LED6 6 +#define LP3944_LED7 7 + +#define LP3944_DIM0 0 +#define LP3944_DIM1 1 + +/* period in 1/10 sec */ +#define LP3944_PERIOD_MIN 0 +#define LP3944_PERIOD_MAX 16 + +/* duty cycle is a percentage */ +#define LP3944_DUTY_CYCLE_MIN 0 +#define LP3944_DUTY_CYCLE_MAX 100 + +int lp3944_dim_set(int dim, int period, int duty_cycle) +{ + int psc_reg; + int pwm_reg; + + int psc_value; + int pwm_value; + + if (dim == LP3944_DIM0) { + psc_reg = LP3944_REG_PSC0; + pwm_reg = LP3944_REG_PWM0; + } else if (dim == LP3944_DIM1) { + psc_reg = LP3944_REG_PSC1; + pwm_reg = LP3944_REG_PWM1; + } else + return -1; + + /* Convert period to Prescaler value */ + if (period < LP3944_PERIOD_MIN || period > LP3944_PERIOD_MAX) + return -1; + + if (period == LP3944_PERIOD_MIN) + psc_value = 0; + else + psc_value = (period * LP3944_PERIOD_MAX) - 1; + + /* Convert duty cycle to PWM value */ + if (duty_cycle < LP3944_DUTY_CYCLE_MIN + || duty_cycle > LP3944_DUTY_CYCLE_MAX) + return -1; + + if (duty_cycle == LP3944_DUTY_CYCLE_MAX) + pwm_value = 255; + else + pwm_value = (duty_cycle * 256) / 100; + + lp3944_reg_write(save_client, psc_reg, psc_value); + lp3944_reg_write(save_client, pwm_reg, pwm_value); + + return 0; + +} +EXPORT_SYMBOL_GPL(lp3944_dim_set); + +int lp3944_led_set(int led, int status) +{ + int reg; + int val; + + switch (led) { + case LP3944_LED0: + case LP3944_LED1: + case LP3944_LED2: + case LP3944_LED3: + reg = LP3944_REG_LS0; + break; + case LP3944_LED4: + case LP3944_LED5: + case LP3944_LED6: + case LP3944_LED7: + reg = LP3944_REG_LS1; + led -= LP3944_LED4; + break; + default: + return -1; + } + + if (status < LP3944_LED_STATUS_OFF || status > LP3944_LED_STATUS_DIM1) + return -1; + + lp3944_reg_read(save_client, reg, &val); + + val &= ~(LP3944_LED_STATUS_MASK << (led << 1)); + val |= (status << (led << 1)); + + printk(KERN_DEBUG "lp3944_ld_set, led: %d, status: %d, val: 0x%02x\n", + led, status, val); + + /* set led status */ + lp3944_reg_write(save_client, reg, val); + + return 0; +} +EXPORT_SYMBOL_GPL(lp3944_led_set); + +/* low-level sysfs interface */ + +static ssize_t lp3944_get_reg(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct i2c_client *client = to_i2c_client(dev); + int ret = 0, l = 0; + int reg, val; + + l = strlen("0x00 0x00\n"); + + for (reg = LP3944_REG_INPUT1; reg <= LP3944_REG_REGISTER9; reg++) { + /* we could even use lp3944 auto-increment feature here, but + * that's not very important. + */ + lp3944_reg_read(client, reg, &val); + ret += sprintf(buf + l * reg, "0x%02x 0x%02x\n", reg, val); + } + + return ret; +} + +static ssize_t lp3944_set_reg(struct device *dev, + struct device_attribute *attr, const char *buf, + size_t count) +{ + struct i2c_client *client = to_i2c_client(dev); + int reg, val; + + if (sscanf(buf, "0x%02x 0x%02x", ®, &val) != 2) + return -EINVAL; + + if (reg < LP3944_REG_INPUT1 || reg > LP3944_REG_REGISTER9) + return -EINVAL; + + if (val < 0 || val > 255) + return -EINVAL; + + lp3944_reg_write(client, reg, val); + + return count; +} + +static DEVICE_ATTR(reg, S_IWUSR | S_IRUGO, lp3944_get_reg, lp3944_set_reg); + +static struct attribute *lp3944_attributes[] = { + &dev_attr_reg.attr, + NULL +}; + +static const struct attribute_group lp3944_attr_group = { + .attrs = lp3944_attributes, +}; + +/* private stuff */ + +static int lp3944_reg_read(struct i2c_client *client, int reg, + unsigned int *value) +{ + /* byte-sized register */ + if (reg < 0x10) { + *value = i2c_smbus_read_byte_data(client, reg); + pr_debug("lp3944_reg_read, reg: 0x%08x value: 0x%08x\n", reg, + *value); + } else + return -1; + + return 0; +} + +static int lp3944_reg_write(struct i2c_client *client, int reg, + unsigned int value) +{ + int tmp; + int ret = 0; + + tmp = i2c_smbus_read_byte_data(client, reg); + pr_debug("lp3944_reg_write, before write reg: 0x%08x value: 0x%08x\n", + reg, tmp); + + /* byte-sized register */ + if (reg < 0x10) + ret = i2c_smbus_write_byte_data(client, reg, value); + else + return -1; + + tmp = i2c_smbus_read_byte_data(client, reg); + pr_debug("lp3944_reg_write, after write reg: 0x%08x value: 0x%08x\n", + reg, tmp); + + return ret; +} + +static int lp3944_init_client(struct i2c_client *client) +{ + int tmp; + + /* Try to read a byte, if it fails, return an error */ + tmp = i2c_smbus_read_byte_data(client, LP3944_REG_INPUT1); + if (tmp < 0) + return -1; + + /* default values */ + lp3944_reg_write(client, LP3944_REG_PSC0, 0x00); + lp3944_reg_write(client, LP3944_REG_PWM0, 0x80); + lp3944_reg_write(client, LP3944_REG_PSC1, 0x00); + lp3944_reg_write(client, LP3944_REG_PWM1, 0x80); + lp3944_reg_write(client, LP3944_REG_LS0, 0x00); + lp3944_reg_write(client, LP3944_REG_LS1, 0x00); + + pr_debug("lp3944_init_client\n"); + + return 0; +} + +static int __devinit lp3944_probe(struct i2c_client *client) +{ + int err = 0; + struct i2c_adapter *adapter = to_i2c_adapter(client->dev.parent); + + dev_dbg(&client->dev, "lp3944_probe, adapter %s!\n", adapter->name); + + /* Let's see whether this adapter can support what we need. */ + if (!i2c_check_functionality(adapter, I2C_FUNC_SMBUS_BYTE_DATA)) { + printk(KERN_ERR "lp3944, insufficient functionality!\n"); + err = -EIO; + return err; + } + + /* Initialize the lp3944 chip */ + save_client = client; + err = lp3944_init_client(client); + if (err) + return err; + + /* Register sysfs hooks */ + err = sysfs_create_group(&client->dev.kobj, &lp3944_attr_group); + if (err) + return err; + + dev_info(&client->dev, "lp3944 enabled\n"); + + return 0; +} + +static int __devexit lp3944_remove(struct i2c_client *client) +{ + printk(KERN_DEBUG "lp3944_remove\n"); + sysfs_remove_group(&client->dev.kobj, &lp3944_attr_group); + + lp3944_reg_write(client, LP3944_REG_LS0, 0x00); + lp3944_reg_write(client, LP3944_REG_LS1, 0x00); + + kfree(i2c_get_clientdata(client)); + return 0; +} + +static int lp3944_suspend(struct i2c_client *dev, pm_message_t state) +{ + dev_dbg(&dev->dev, "lp3944_suspend\n"); + return 0; +} + +static int lp3944_resume(struct i2c_client *dev) +{ + dev_dbg(&dev->dev, "lp3944_resume\n"); + return 0; +} + +static void lp3944_shutdown(struct i2c_client *dev) +{ + dev_dbg(&dev->dev, "lp3944_shutdown\n"); +} + +/* lp3944 i2c codec control layer */ +static struct i2c_driver lp3944_driver = { + .driver = { + .name = "lp3944", + .owner = THIS_MODULE, + }, + .probe = lp3944_probe, + .remove = lp3944_remove, + .suspend = lp3944_suspend, + .resume = lp3944_resume, + .shutdown = lp3944_shutdown, + .command = NULL, +}; + +static int __init lp3944_module_init(void) +{ + int ret; + + printk(KERN_DEBUG "lp3944_module_init\n"); + ret = i2c_add_driver(&lp3944_driver); + if (ret != 0) + printk(KERN_ERR "lp3944_module_init, can't add i2c driver\n"); + + return ret; +} + +static void __exit lp3944_module_exit(void) +{ + i2c_del_driver(&lp3944_driver); +} + +module_init(lp3944_module_init); +module_exit(lp3944_module_exit); + +/* Module information */ +MODULE_AUTHOR("Antonio Ospite <[EMAIL PROTECTED]>"); +MODULE_DESCRIPTION("LP3944 Fun Light Chip"); +MODULE_LICENSE("GPL"); Index: linux-2.6.24.2/drivers/i2c/chips/Kconfig =================================================================== --- linux-2.6.24.2.orig/drivers/i2c/chips/Kconfig +++ linux-2.6.24.2/drivers/i2c/chips/Kconfig @@ -153,6 +153,16 @@ This driver can also be built as a module. If so, the module will be called tsl2550. +config LP3944 + tristate "National Semiconductor LP3944 Fun Light Chip" + depends on EXPERIMENTAL + help + If you say yes here you get support for the National Semiconductor LP3944 + Lighting Management Unit (LMU) also known as a Fun Light Chip. + + This driver can also be built as a module. If so, the module + will be called lp3944. + config MENELAUS bool "TWL92330/Menelaus PM chip" depends on I2C=y && ARCH_OMAP24XX Index: linux-2.6.24.2/drivers/i2c/chips/Makefile =================================================================== --- linux-2.6.24.2.orig/drivers/i2c/chips/Makefile +++ linux-2.6.24.2/drivers/i2c/chips/Makefile @@ -15,6 +15,7 @@ obj-$(CONFIG_TPS65010) += tps65010.o obj-$(CONFIG_MENELAUS) += menelaus.o obj-$(CONFIG_SENSORS_TSL2550) += tsl2550.o +obj-$(CONFIG_LP3944) += lp3944.o ifeq ($(CONFIG_I2C_DEBUG_CHIP),y) EXTRA_CFLAGS += -DDEBUG
Index: linux-2.6.24.2/drivers/leds/leds-a910.c =================================================================== --- linux-2.6.24.2.orig/drivers/leds/leds-a910.c +++ linux-2.6.24.2/drivers/leds/leds-a910.c @@ -18,6 +18,9 @@ #include <linux/leds.h> #include <asm/arch/ezx-pcap.h> +extern int lp3944_dim_set(int dim, int period, int duty_cycle); +extern int lp3944_led_set(int led, int status); + static void a910led_keypad_set(struct led_classdev *led_cdev, enum led_brightness value) { @@ -37,6 +40,40 @@ ezx_pcap_bit_set(PCAP_BIT_AUXVREG_V_VIB_EN, 1); } +static void a910led_flash_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + value = ( value != 0 ); + printk( KERN_DEBUG "a910led_flash_set: %d\n", value ); + lp3944_led_set(6, value); + lp3944_led_set(7, value); +} + +static void a910led_red_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + value = ( value != 0 ); + printk( KERN_DEBUG "a910led_red_set: %d\n", value ); + lp3944_led_set(0, value); +} + +static void a910led_green_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + value = ( value != 0 ); + printk( KERN_DEBUG "a910led_green_set: %d\n", value ); + lp3944_led_set(1, value); +} + +static void a910led_blue_set(struct led_classdev *led_cdev, + enum led_brightness value) +{ + value = ( value != 0 ); + printk( KERN_DEBUG "a910led_blue_set: %d\n", value ); + lp3944_led_set(2, value); +} + + static struct led_classdev a910_keypad_led = { .name = "a910:keypad", .default_trigger = "none", @@ -49,11 +86,39 @@ .brightness_set = a910vibrator_set, }; +static struct led_classdev a910_flash = { + .name = "a910:flash", + .default_trigger = "none", + .brightness_set = a910led_flash_set, +}; + +static struct led_classdev a910_red_led = { + .name = "a910:red", + .default_trigger = "none", + .brightness_set = a910led_red_set, +}; + +static struct led_classdev a910_green_led = { + .name = "a910:green", + .default_trigger = "none", + .brightness_set = a910led_green_set, +}; + +static struct led_classdev a910_blue_led = { + .name = "a910:blue", + .default_trigger = "none", + .brightness_set = a910led_blue_set, +}; + #ifdef CONFIG_PM static int a910led_suspend(struct platform_device *dev, pm_message_t state) { led_classdev_suspend(&a910_keypad_led); led_classdev_suspend(&a910_vibrator); + led_classdev_suspend(&a910_flash); + led_classdev_suspend(&a910_red_led); + led_classdev_suspend(&a910_green_led); + led_classdev_suspend(&a910_blue_led); return 0; } @@ -61,6 +126,10 @@ { led_classdev_resume(&a910_keypad_led); led_classdev_resume(&a910_vibrator); + led_classdev_resume(&a910_flash); + led_classdev_resume(&a910_red_led); + led_classdev_resume(&a910_green_led); + led_classdev_resume(&a910_blue_led); return 0; } #endif @@ -74,9 +143,41 @@ return ret; ret = led_classdev_register(&pdev->dev, &a910_vibrator); - if (ret < 0) + if (ret < 0) { + led_classdev_unregister(&a910_keypad_led); + goto exit; + } + + ret = led_classdev_register(&pdev->dev, &a910_flash); + if (ret < 0) { + led_classdev_unregister(&a910_vibrator); + led_classdev_unregister(&a910_keypad_led); + } + + ret = led_classdev_register(&pdev->dev, &a910_red_led); + if (ret < 0) { + led_classdev_unregister(&a910_vibrator); led_classdev_unregister(&a910_keypad_led); + led_classdev_unregister(&a910_flash); + } + ret = led_classdev_register(&pdev->dev, &a910_green_led); + if (ret < 0) { + led_classdev_unregister(&a910_vibrator); + led_classdev_unregister(&a910_keypad_led); + led_classdev_unregister(&a910_flash); + led_classdev_unregister(&a910_red_led); + } + + ret = led_classdev_register(&pdev->dev, &a910_blue_led); + if (ret < 0) { + led_classdev_unregister(&a910_vibrator); + led_classdev_unregister(&a910_keypad_led); + led_classdev_unregister(&a910_flash); + led_classdev_unregister(&a910_red_led); + led_classdev_unregister(&a910_green_led); + } +exit: return ret; } @@ -84,6 +185,10 @@ { led_classdev_unregister(&a910_keypad_led); led_classdev_unregister(&a910_vibrator); + led_classdev_unregister(&a910_flash); + led_classdev_unregister(&a910_red_led); + led_classdev_unregister(&a910_green_led); + led_classdev_unregister(&a910_blue_led); return 0; } @@ -104,6 +209,15 @@ /* Because MBM turns the led on at boot! */ a910led_keypad_set( &a910_keypad_led, 0 ); + /* FIXME: these should be in backlight part */ + + /* led3 is the cli backlight. + * backlight is off when the led is turned to on + */ + lp3944_led_set(3, 1); + /* led4 is main display */ + lp3944_led_set(4, 0); + return platform_driver_register(&a910led_driver); } @@ -111,6 +225,11 @@ { a910led_keypad_set( &a910_keypad_led, 0 ); a910vibrator_set( &a910_vibrator, 0 ); + a910vibrator_set( &a910_flash, 0 ); + a910vibrator_set( &a910_red_led, 0 ); + a910vibrator_set( &a910_green_led, 0 ); + a910vibrator_set( &a910_blue_led, 0 ); + platform_driver_unregister(&a910led_driver); }
Index: linux-2.6.24.2/arch/arm/mach-pxa/ezx-a910.c =================================================================== --- linux-2.6.24.2.orig/arch/arm/mach-pxa/ezx-a910.c +++ linux-2.6.24.2/arch/arm/mach-pxa/ezx-a910.c @@ -244,6 +244,10 @@ I2C_BOARD_INFO("ezx-eoc", 0x17), .type = "ezx-eoc", }, + { + I2C_BOARD_INFO("lp3944", 0x60), + .type = "lp3944", + }, }; #endif
pgpdXrc7ZWlfg.pgp
Description: PGP signature