| /* |
| * leds_an30259a.c - driver for panasonic AN30259A led control chip |
| * |
| * Copyright (C) 2011, Samsung Electronics Co. Ltd. All Rights Reserved. |
| * |
| * Contact: Yufi Li <[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; version 2 of the License. |
| * |
| */ |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/device.h> |
| #include <linux/i2c.h> |
| #include <linux/delay.h> |
| #include <linux/slab.h> |
| #include <linux/fs.h> |
| #include <linux/uaccess.h> |
| #include <linux/miscdevice.h> |
| #include <linux/leds-an30259a.h> |
| |
| /* AN30259A register map */ |
| #define AN30259A_REG_SRESET 0x00 |
| #define AN30259A_REG_LEDON 0x01 |
| #define AN30259A_REG_SEL 0x02 |
| |
| #define AN30259A_REG_LED1CC 0x03 |
| #define AN30259A_REG_LED2CC 0x04 |
| #define AN30259A_REG_LED3CC 0x05 |
| |
| #define AN30259A_REG_LED1SLP 0x06 |
| #define AN30259A_REG_LED2SLP 0x07 |
| #define AN30259A_REG_LED3SLP 0x08 |
| |
| #define AN30259A_REG_LED1CNT1 0x09 |
| #define AN30259A_REG_LED1CNT2 0x0a |
| #define AN30259A_REG_LED1CNT3 0x0b |
| #define AN30259A_REG_LED1CNT4 0x0c |
| |
| #define AN30259A_REG_LED2CNT1 0x0d |
| #define AN30259A_REG_LED2CNT2 0x0e |
| #define AN30259A_REG_LED2CNT3 0x0f |
| #define AN30259A_REG_LED2CNT4 0x10 |
| |
| #define AN30259A_REG_LED3CNT1 0x11 |
| #define AN30259A_REG_LED3CNT2 0x12 |
| #define AN30259A_REG_LED3CNT3 0x13 |
| #define AN30259A_REG_LED3CNT4 0x14 |
| #define AN30259A_REG_MAX 0x15 |
| /* MASK */ |
| #define AN30259A_MASK_IMAX 0xc0 |
| #define AN30259A_MASK_DELAY 0xf0 |
| #define AN30259A_SRESET 0x01 |
| #define LED_SLOPE_MODE 0x10 |
| #define LED_ON 0x01 |
| |
| #define DUTYMAX_MAX_VALUE 0x7f |
| #define DUTYMIN_MIN_VALUE 0x00 |
| #define SLPTT_MAX_VALUE 0x0f |
| |
| #define DETENTION_MAX_VALUE 60 |
| #define DELAY_MAX_VALUE 7500 |
| #define AN30259A_TIME_UNIT 500 |
| #define AN30259A_DT_TIME_UNIT 4 |
| |
| #define LED_R_MASK 0x00ff0000 |
| #define LED_G_MASK 0x0000ff00 |
| #define LED_B_MASK 0x000000ff |
| #define LED_R_SHIFT 16 |
| #define LED_G_SHIFT 8 |
| #define LED_IMAX_SHIFT 6 |
| #define AN30259A_CTN_RW_FLG 0x80 |
| |
| enum an30259a_led { |
| LED_R, |
| LED_G, |
| LED_B, |
| }; |
| |
| struct an30259a_data { |
| struct i2c_client *client; |
| struct miscdevice dev; |
| struct mutex mutex; |
| u8 shadow_reg[AN30259A_REG_MAX]; |
| }; |
| |
| static int leds_i2c_write_all(struct i2c_client *client) |
| { |
| struct an30259a_data *data = i2c_get_clientdata(client); |
| int ret; |
| |
| /*we need to set all the configs setting first, then LEDON later*/ |
| ret = i2c_smbus_write_i2c_block_data(client, |
| AN30259A_REG_SEL | AN30259A_CTN_RW_FLG, |
| AN30259A_REG_MAX - AN30259A_REG_SEL, |
| &data->shadow_reg[AN30259A_REG_SEL]); |
| if (ret < 0) { |
| dev_err(&client->adapter->dev, |
| "%s: failure on i2c block write\n", |
| __func__); |
| return ret; |
| } |
| ret = i2c_smbus_write_byte_data(client, AN30259A_REG_LEDON, |
| data->shadow_reg[AN30259A_REG_LEDON]); |
| |
| if (ret < 0) { |
| dev_err(&client->adapter->dev, |
| "%s: failure on i2c byte write\n", |
| __func__); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| /* |
| * leds_set_slope_mode() sets correct values to corresponding shadow registers. |
| * led: stands for LED_R or LED_G or LED_B. |
| * delay: represents for starting delay time in multiple of .5 second. |
| * dutymax: led at slope lighting maximum PWM Duty setting. |
| * dutymid: led at slope lighting middle PWM Duty setting. |
| * dutymin: led at slope lighting minimum PWM Duty Setting. |
| * slptt1: total time of slope operation 1 and 2, in multiple of .5 second. |
| * slptt2: total time of slope operation 3 and 4, in multiple of .5 second. |
| * dt1: detention time at each step in slope operation 1, in multiple of 4ms. |
| * dt2: detention time at each step in slope operation 2, in multiple of 4ms. |
| * dt3: detention time at each step in slope operation 3, in multiple of 4ms. |
| * dt4: detention time at each step in slope operation 4, in multiple of 4ms. |
| */ |
| static void leds_set_slope_mode(struct i2c_client *client, |
| enum an30259a_led led, u8 delay, |
| u8 dutymax, u8 dutymid, u8 dutymin, |
| u8 slptt1, u8 slptt2, |
| u8 dt1, u8 dt2, u8 dt3, u8 dt4) |
| { |
| struct an30259a_data *data = i2c_get_clientdata(client); |
| |
| data->shadow_reg[AN30259A_REG_LED1CNT1 + led * 4] = |
| dutymax << 4 | dutymid; |
| data->shadow_reg[AN30259A_REG_LED1CNT2 + led * 4] = |
| delay << 4 | dutymin; |
| data->shadow_reg[AN30259A_REG_LED1CNT3 + led * 4] = dt2 << 4 | dt1; |
| data->shadow_reg[AN30259A_REG_LED1CNT4 + led * 4] = dt4 << 4 | dt3; |
| data->shadow_reg[AN30259A_REG_LED1SLP + led] = slptt2 << 4 | slptt1; |
| } |
| |
| static void leds_on(struct i2c_client *client, enum an30259a_led led, |
| bool on, bool slopemode, |
| u8 ledcc) |
| { |
| struct an30259a_data *data = i2c_get_clientdata(client); |
| |
| if (on) |
| data->shadow_reg[AN30259A_REG_LEDON] |= LED_ON << led; |
| else { |
| data->shadow_reg[AN30259A_REG_LEDON] &= ~(LED_ON << led); |
| data->shadow_reg[AN30259A_REG_LED1CNT2 + led * 4] &= |
| ~AN30259A_MASK_DELAY; |
| } |
| if (slopemode) |
| data->shadow_reg[AN30259A_REG_LEDON] |= LED_SLOPE_MODE << led; |
| else |
| data->shadow_reg[AN30259A_REG_LEDON] &= |
| ~(LED_SLOPE_MODE << led); |
| |
| data->shadow_reg[AN30259A_REG_LED1CC + led] = ledcc; |
| } |
| |
| /* calculate the detention time for each step, return ms */ |
| static u8 calculate_dt(u8 min, u8 max, u16 time) |
| { |
| u16 step_time; |
| u16 detention_time; |
| |
| if (min >= max) |
| return 0; |
| |
| step_time = time / (u16)(max - min); |
| detention_time = (step_time + 0x03) & 0xfffc; |
| /* the detention time at each step can be set as 4ms, 8ms, ...60ms */ |
| detention_time = (detention_time > DETENTION_MAX_VALUE) ? |
| DETENTION_MAX_VALUE : detention_time; |
| return detention_time; |
| } |
| |
| /* calculate the constant current output */ |
| static u8 calculate_cc(u32 brightness, enum an30259a_led led) |
| { |
| u8 value = 0; |
| switch (led) { |
| case LED_R: |
| value = (brightness & LED_R_MASK) >> LED_R_SHIFT; |
| break; |
| case LED_G: |
| value = (brightness & LED_G_MASK) >> LED_G_SHIFT; |
| break; |
| case LED_B: |
| value = brightness & LED_B_MASK; |
| break; |
| default: |
| break; |
| } |
| return value; |
| } |
| |
| static u8 calculate_slope_tt(u8 mid, u8 dt1, u8 dt2, u16 time) |
| { |
| u8 slptt; |
| u16 tt = (dt1 * (mid - DUTYMIN_MIN_VALUE) + |
| dt2 * (DUTYMAX_MAX_VALUE - mid)) * AN30259A_DT_TIME_UNIT + time; |
| /* round up to the nearest .5 seconds */ |
| slptt = (tt + AN30259A_TIME_UNIT - 1) / AN30259A_TIME_UNIT; |
| slptt = slptt > SLPTT_MAX_VALUE ? SLPTT_MAX_VALUE : slptt; |
| return slptt; |
| } |
| |
| static int leds_handle_cmds(struct i2c_client *client, |
| enum an30259a_led color, |
| struct an30259a_pr_control *leds) |
| { |
| u8 cc, delay, dutymid, dt1, dt2, dt3, dt4, tt1, tt2; |
| |
| cc = calculate_cc(leds->color, color); |
| switch (leds->state) { |
| case LED_LIGHT_OFF: |
| leds_on(client, color, false, false, 0); |
| break; |
| case LED_LIGHT_ON: |
| leds_on(client, color, true, false, cc); |
| break; |
| case LED_LIGHT_PULSE: |
| /* |
| * PULSE is a special case of slope with delay=0, |
| * dutymid = dutymax ,dt1,dt2,dt3,dt4 are all zero |
| */ |
| leds_on(client, color, true, true, cc); |
| leds_set_slope_mode(client, color, 0, |
| DUTYMAX_MAX_VALUE >> 3, DUTYMAX_MAX_VALUE >> 3, |
| DUTYMIN_MIN_VALUE >> 3, |
| (leds->time_on + AN30259A_TIME_UNIT - 1) / |
| AN30259A_TIME_UNIT, |
| (leds->time_off + AN30259A_TIME_UNIT - 1) / |
| AN30259A_TIME_UNIT, |
| 0, 0, 0, 0); |
| |
| break; |
| case LED_LIGHT_SLOPE: |
| if (leds->mid_brightness > DUTYMAX_MAX_VALUE) |
| return -EINVAL; |
| |
| delay = ((leds->start_delay > DELAY_MAX_VALUE) ? |
| DELAY_MAX_VALUE : leds->start_delay) / |
| AN30259A_TIME_UNIT; |
| |
| dutymid = leds->mid_brightness >> 3; |
| dt1 = calculate_dt(DUTYMIN_MIN_VALUE, leds->mid_brightness, |
| leds->time_slope_up_1) / AN30259A_DT_TIME_UNIT; |
| dt2 = calculate_dt(leds->mid_brightness, DUTYMAX_MAX_VALUE, |
| leds->time_slope_up_2) / AN30259A_DT_TIME_UNIT; |
| dt3 = calculate_dt(leds->mid_brightness, DUTYMAX_MAX_VALUE, |
| leds->time_slope_down_1) / |
| AN30259A_DT_TIME_UNIT; |
| dt4 = calculate_dt(DUTYMIN_MIN_VALUE, leds->mid_brightness, |
| leds->time_slope_down_2) / |
| AN30259A_DT_TIME_UNIT; |
| tt1 = calculate_slope_tt(leds->mid_brightness, |
| dt1, dt2, leds->time_on); |
| tt2 = calculate_slope_tt(leds->mid_brightness, |
| dt4, dt3, leds->time_off); |
| |
| leds_on(client, color, true, true, cc); |
| leds_set_slope_mode(client, color, delay, |
| DUTYMAX_MAX_VALUE >> 3, dutymid, DUTYMIN_MIN_VALUE >> 3, |
| tt1, tt2, dt1, dt2, dt3, dt4); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static int leds_set_imax(struct i2c_client *client, u8 imax) |
| { |
| int ret; |
| struct an30259a_data *data = i2c_get_clientdata(client); |
| |
| data->shadow_reg[AN30259A_REG_SEL] &= ~AN30259A_MASK_IMAX; |
| data->shadow_reg[AN30259A_REG_SEL] |= imax << LED_IMAX_SHIFT; |
| |
| ret = i2c_smbus_write_byte_data(client, AN30259A_REG_SEL, |
| data->shadow_reg[AN30259A_REG_SEL]); |
| if (ret < 0) { |
| dev_err(&client->adapter->dev, |
| "%s: failure on i2c write\n", |
| __func__); |
| } |
| return 0; |
| } |
| |
| static long an30250a_ioctl(struct file *file, unsigned int cmd, |
| unsigned long arg) |
| { |
| struct an30259a_data *leds_data = container_of(file->private_data, |
| struct an30259a_data, dev); |
| struct i2c_client *client = leds_data->client; |
| int retval, i; |
| u8 imax; |
| struct an30259a_pr_control leds[3]; |
| |
| mutex_lock(&leds_data->mutex); |
| |
| switch (cmd) { |
| case AN30259A_PR_SET_LED: |
| retval = copy_from_user(leds, (unsigned char __user *)arg, |
| sizeof(struct an30259a_pr_control)); |
| if (retval) |
| break; |
| |
| for (i = LED_R; i <= LED_B; i++) { |
| retval = leds_handle_cmds(client, i, leds); |
| if (retval < 0) |
| goto an30259a_ioctl_failed; |
| } |
| retval = leds_i2c_write_all(client); |
| break; |
| case AN30259A_PR_SET_LEDS: |
| retval = copy_from_user(leds, (unsigned char __user *)arg, |
| 3 * sizeof(struct an30259a_pr_control)); |
| |
| if (retval) |
| break; |
| |
| for (i = LED_R; i <= LED_B; i++) { |
| retval = leds_handle_cmds(client, i, &leds[i]); |
| if (retval < 0) |
| goto an30259a_ioctl_failed; |
| } |
| retval = leds_i2c_write_all(client); |
| break; |
| |
| case AN30259A_PR_SET_IMAX: |
| retval = copy_from_user(&imax, (unsigned char __user *)arg, |
| sizeof(u8)); |
| if (retval) |
| break; |
| |
| retval = leds_set_imax(client, imax); |
| break; |
| |
| default: |
| dev_err(&client->adapter->dev, |
| "%s: Unknown cmd %x, arg %lu\n", |
| __func__, cmd, arg); |
| retval = -EINVAL; |
| break; |
| } |
| |
| an30259a_ioctl_failed: |
| mutex_unlock(&leds_data->mutex); |
| return retval; |
| } |
| |
| static int an30250a_open(struct inode *inode, struct file *file) |
| { |
| return 0; |
| } |
| |
| static const struct file_operations an30259a_fops = { |
| .owner = THIS_MODULE, |
| .unlocked_ioctl = an30250a_ioctl, |
| .open = an30250a_open, |
| }; |
| |
| static int __devinit an30259a_initialize(struct i2c_client *client) |
| { |
| struct an30259a_data *data = i2c_get_clientdata(client); |
| int ret; |
| |
| /* reset an30259a*/ |
| ret = i2c_smbus_write_byte_data(client, AN30259A_REG_SRESET, |
| AN30259A_SRESET); |
| if (ret < 0) { |
| dev_err(&client->adapter->dev, |
| "%s: failure on i2c write (reg = 0x%2x)\n", |
| __func__, AN30259A_REG_SRESET); |
| return ret; |
| } |
| ret = i2c_smbus_read_i2c_block_data(client, |
| AN30259A_REG_SRESET | AN30259A_CTN_RW_FLG, |
| AN30259A_REG_MAX, data->shadow_reg); |
| if (ret < 0) { |
| dev_err(&client->adapter->dev, |
| "%s: failure on i2c read block(ledxcc)\n", |
| __func__); |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int __devinit an30259a_probe(struct i2c_client *client, |
| const struct i2c_device_id *id) |
| { |
| struct an30259a_data *data; |
| int ret; |
| |
| dev_dbg(&client->adapter->dev, "%s\n", __func__); |
| if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) { |
| dev_err(&client->dev, "need I2C_FUNC_I2C.\n"); |
| return -ENODEV; |
| } |
| |
| data = kzalloc(sizeof(*data), GFP_KERNEL); |
| if (!data) { |
| dev_err(&client->adapter->dev, |
| "failed to allocate driver data.\n"); |
| return -ENOMEM; |
| } |
| |
| data->client = client; |
| |
| i2c_set_clientdata(client, data); |
| |
| mutex_init(&data->mutex); |
| |
| /* initialize LED */ |
| ret = an30259a_initialize(client); |
| if (ret < 0) { |
| dev_err(&client->adapter->dev, "failure on initialization\n"); |
| goto exit; |
| } |
| |
| data->dev.minor = MISC_DYNAMIC_MINOR; |
| data->dev.name = "an30259a_leds"; |
| data->dev.fops = &an30259a_fops; |
| ret = misc_register(&data->dev); |
| if (ret < 0) { |
| dev_err(&client->adapter->dev, |
| "%s: ERROR: misc_register returned %d\n", |
| __func__, ret); |
| goto exit; |
| } |
| |
| return 0; |
| exit: |
| mutex_destroy(&data->mutex); |
| kfree(data); |
| return ret; |
| } |
| |
| static int __devexit an30259a_remove(struct i2c_client *client) |
| { |
| struct an30259a_data *data = i2c_get_clientdata(client); |
| |
| dev_dbg(&client->adapter->dev, "%s\n", __func__); |
| misc_deregister(&data->dev); |
| mutex_destroy(&data->mutex); |
| kfree(data); |
| return 0; |
| } |
| |
| static struct i2c_device_id an30259a_id[] = { |
| {"an30259a", 0}, |
| {}, |
| }; |
| |
| MODULE_DEVICE_TABLE(i2c, an30259a_id); |
| |
| static struct i2c_driver an30259a_i2c_driver = { |
| .driver = { |
| .owner = THIS_MODULE, |
| .name = "an30259a", |
| }, |
| .id_table = an30259a_id, |
| .probe = an30259a_probe, |
| .remove = __devexit_p(an30259a_remove), |
| }; |
| |
| static int __init an30259a_init(void) |
| { |
| return i2c_add_driver(&an30259a_i2c_driver); |
| } |
| |
| static void __exit an30259a_exit(void) |
| { |
| i2c_del_driver(&an30259a_i2c_driver); |
| } |
| |
| module_init(an30259a_init); |
| module_exit(an30259a_exit); |
| |
| MODULE_DESCRIPTION("AN30259A LED driver"); |
| MODULE_AUTHOR("Yufi Li <[email protected]"); |
| MODULE_LICENSE("GPL v2"); |