| |
| /* |
| * lm3695_bl.c - Linux kernel module for National Semiconductor |
| * LM3695 backlight driver |
| * |
| * Copyright (C) 2014 Nest Labs, Inc |
| * |
| * 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. |
| * |
| * |
| * Required properties: |
| * compatible : should be "national,lm3695 |
| * reg : i2c slave address (0x63) |
| * enable-gpio : hardware enable gpio. |
| * |
| * Optional properties: |
| * max-brightness : max current allowed through each channel. |
| * iLED = 50uA x 1.003040572^brightness. Defaults to 2047 |
| * default-brightness : Current set on driver probe. defaults to |
| * max-brigtness/2 |
| * ramp-rate : time in microseconds between brightness level steps |
| * during ramping. Defaults to 500. 0 means no ramping. |
| * clock-divider : 0-9, number of dividers to apply to 62.5kHz |
| * dithering clk. e.g. 3 = (2^3) divider = 7.8khz |
| * single-string : (bool) use only string 1 |
| * boost-freq : (bool) double boost switcher freqency from 500kHz to 1MHz |
| * freq-shift : (bool) shift boost and dithering freq by -12%. |
| * high-voltage : (bool) Use 21v OVP instead of 16v OVP |
| * |
| * example: |
| * |
| * lm3695@63{ |
| * compatible= "national,lm3695"; |
| * reg = <0x68>; |
| * max-brightness = <2047>; |
| * default-brightness = <2000>; |
| * single-string; |
| * ramp-rate = <4000>; |
| * enable-gpio = <&gpio2 14 GPIO_ACTIVE_HIGH>; |
| * } |
| * |
| */ |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/backlight.h> |
| #include <linux/i2c.h> |
| #include <linux/platform_device.h> |
| #include <linux/of.h> |
| #include <linux/of_gpio.h> |
| |
| #define LM3695_DEV "lcd-backlight" |
| #define LM3695_NAME "lm3695_bl" |
| |
| #define LM3695_MAX_BRIGHTNESS ((1 << 11) - 1) |
| #define MICROSEC_PER_MILLISEC (1000) |
| |
| #define LM3695_GEN_PURPOSE_REG 0x10 |
| #define LM3695_BOOST_FREQ_SHIFT_M (1<<6) |
| #define LM3695_BOOST_FREQ_SELECT_M (1<<5) |
| #define LM3695_OVP_SELECT_M (1<<4) |
| #define LM3695_STRING_MODE_M (1<<3) |
| #define LM3695_BRIGHTNESS_REG_EN_M (1<<2) |
| #define LM3695_RAMP_DISABLE_M (1<<1) |
| #define LM3695_CHIP_EN_M (1<<0) |
| |
| #define LM3695_DITHER_FREQ_REG 0x11 |
| #define LM3695_DITHER_FREQ_DIV_M 0b1111 |
| |
| #define LM3695_RAMP_RATE_REG 0x12 |
| #define LM3695_RAMP_RATE_0_MS 0b1111 |
| #define LM3695_RAMP_RATE_0_125_MS 0b0000 |
| #define LM3695_RAMP_RATE_0_252_MS 0b0001 |
| #define LM3695_RAMP_RATE_0_5_MS 0b0010 |
| #define LM3695_RAMP_RATE_1_MS 0b0011 |
| #define LM3695_RAMP_RATE_2_MS 0b0100 |
| #define LM3695_RAMP_RATE_4_MS 0b0101 |
| #define LM3695_RAMP_RATE_8_MS 0b0110 |
| #define LM3695_RAMP_RATE_16_MS 0b0111 |
| #define LM3695_RAMP_RATE_32_MS 0b1000 |
| #define LM3695_RAMP_RATE_64_MS 0b1001 |
| #define LM3695_RAMP_RATE_128_MS 0b1010 |
| |
| static const int rates[] = {0, 125, 252, 500, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 128000}; |
| |
| #define LM3695_BRIGHTNESS_L_REG 0x13 |
| #define LM3695_BRIGHTNESS_L_M 0b111 |
| #define LM3695_BRIGHTNESS_L_O 0 |
| #define LM3695_BRIGHTNESS_L_S(x) (((x) & LM3695_BRIGHTNESS_L_M) >> LM3695_BRIGHTNESS_L_O) |
| |
| #define LM3695_BRIGHTNESS_H_REG 0x14 |
| #define LM3695_BRIGHTNESS_H_M 0xFF |
| #define LM3695_BRIGHTNESS_H_O 3 |
| #define LM3695_BRIGHTNESS_H_S(x) ((((x) >> LM3695_BRIGHTNESS_H_O )& LM3695_BRIGHTNESS_H_M)) |
| |
| int default_brightness = 0; |
| module_param(default_brightness, int, 0664); |
| MODULE_PARM_DESC(default_brightness, "default brightness to ramp up to at probe"); |
| |
| struct lm3695_data{ |
| struct i2c_client *client; |
| int max_current; |
| int hwen_gpio; |
| uint16_t max_brightness; |
| uint16_t default_brightness; |
| uint16_t current_brightness; |
| uint16_t next_brightness; |
| uint8_t ramp_rise_rate; |
| uint8_t ramp_fall_rate; |
| uint8_t dither_freq; |
| bool boost_freq; |
| bool boost_shift; |
| bool single_string; |
| bool high_voltage; |
| bool enabled; |
| }; |
| |
| static ssize_t store_ramp_time(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t len); |
| |
| static ssize_t show_ramp_time(struct device *dev, |
| struct device_attribute *attr, char *buf); |
| |
| DEVICE_ATTR(ramp_rise_time, S_IRUGO | S_IWUSR, show_ramp_time, store_ramp_time); |
| DEVICE_ATTR(ramp_fall_time, S_IRUGO | S_IWUSR, show_ramp_time, store_ramp_time); |
| |
| static struct attribute *lm3695_bl_attributes[] = { |
| &dev_attr_ramp_rise_time.attr, |
| &dev_attr_ramp_fall_time.attr, |
| NULL |
| }; |
| |
| static struct attribute_group lm3695_bl_attribute_group = { |
| .attrs = lm3695_bl_attributes |
| }; |
| |
| /* |
| *Converts a full ramp transition time in milliseconds to a ramp rate. |
| * |
| *e.g. 1000 msec ramp: 1000 msec / 2048 ticks = .488 ticks/msec |
| * which is then rounded to the closest tick rate that will |
| * complete the transition in AT LEAST "time" milliseconds. |
| */ |
| |
| uint8_t lm3695_time_to_ramp_rate(uint32_t time) |
| { |
| uint8_t rate; |
| |
| uint32_t usec_rate = ( (time*MICROSEC_PER_MILLISEC) / LM3695_MAX_BRIGHTNESS); |
| |
| switch (usec_rate) { |
| case 0: |
| rate = LM3695_RAMP_RATE_0_MS; |
| break; |
| case 1 ... 125: |
| rate = LM3695_RAMP_RATE_0_125_MS; |
| break; |
| case 126 ... 252: |
| rate = LM3695_RAMP_RATE_0_252_MS; |
| break; |
| case 253 ... 500: |
| rate = LM3695_RAMP_RATE_0_5_MS; |
| break; |
| case 501 ... 1000: |
| rate = LM3695_RAMP_RATE_1_MS; |
| break; |
| case 1001 ... 2000: |
| rate = LM3695_RAMP_RATE_2_MS; |
| break; |
| case 2001 ... 4000: |
| rate = LM3695_RAMP_RATE_4_MS; |
| break; |
| case 4001 ... 8000: |
| rate = LM3695_RAMP_RATE_8_MS; |
| break; |
| case 8001 ... 16000: |
| rate = LM3695_RAMP_RATE_16_MS; |
| break; |
| case 16001 ... 32000: |
| rate = LM3695_RAMP_RATE_32_MS; |
| break; |
| case 32001 ... 64000: |
| rate = LM3695_RAMP_RATE_64_MS; |
| break; |
| default: |
| rate = LM3695_RAMP_RATE_128_MS; |
| } |
| return rate; |
| } |
| |
| static ssize_t store_ramp_time(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t len) |
| { |
| struct backlight_device *bl = to_backlight_device(dev); |
| struct lm3695_data *drvdata = bl_get_data(bl); |
| |
| unsigned long time; |
| |
| if (kstrtoul(buf, 0, &time)) |
| return -EINVAL; |
| |
| dev_info(dev, "Setting %s to %ld msec", attr->attr.name, time); |
| |
| if (attr == &dev_attr_ramp_rise_time) |
| drvdata->ramp_rise_rate = lm3695_time_to_ramp_rate(time); |
| else if (attr == &dev_attr_ramp_fall_time) |
| drvdata->ramp_fall_rate = lm3695_time_to_ramp_rate(time); |
| else |
| return -EINVAL; |
| |
| return len; |
| } |
| |
| /* |
| *Returns ramp time in milliseconds for a full transition. |
| * |
| *e.g. 4 msec/tick rate: 4000 usec * 2048 = 8192 msec |
| * |
| */ |
| |
| static ssize_t show_ramp_time(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct backlight_device *bl = to_backlight_device(dev); |
| struct lm3695_data *drvdata = bl_get_data(bl); |
| |
| uint8_t value; |
| |
| if (attr == &dev_attr_ramp_rise_time) |
| value = drvdata->ramp_rise_rate; |
| else if (attr == &dev_attr_ramp_fall_time) |
| value = drvdata->ramp_fall_rate; |
| else |
| return -EINVAL; |
| |
| if (value == LM3695_RAMP_RATE_0_MS) |
| value = 0; |
| else |
| value += 1; |
| |
| return sprintf(buf, "%d\n", (LM3695_MAX_BRIGHTNESS*rates[value])/MICROSEC_PER_MILLISEC); |
| } |
| |
| |
| |
| int lm3695_configure_device(struct lm3695_data *drvdata, bool enable) |
| { |
| uint8_t data; |
| uint8_t rate; |
| /*send default data to chip*/ |
| if (enable) |
| { |
| if (drvdata->next_brightness < drvdata->current_brightness) |
| rate = drvdata->ramp_fall_rate; |
| else |
| rate = drvdata->ramp_rise_rate; |
| |
| gpio_set_value(drvdata->hwen_gpio, 1); |
| drvdata->enabled = true; |
| data = LM3695_CHIP_EN_M; |
| if (drvdata->boost_freq) |
| data |= LM3695_BOOST_FREQ_SELECT_M; |
| if (!drvdata->boost_shift) /*default to boost*/ |
| data |= LM3695_BOOST_FREQ_SHIFT_M; |
| if (rate == LM3695_RAMP_RATE_0_MS) |
| data |= LM3695_RAMP_DISABLE_M; |
| if (drvdata->single_string) |
| data |= LM3695_STRING_MODE_M; |
| if (drvdata->high_voltage) |
| data |= LM3695_OVP_SELECT_M; |
| data |= LM3695_BRIGHTNESS_REG_EN_M; |
| |
| i2c_smbus_write_byte_data(drvdata->client, LM3695_GEN_PURPOSE_REG, data); |
| |
| data = drvdata->dither_freq; |
| i2c_smbus_write_byte_data(drvdata->client, LM3695_DITHER_FREQ_REG, data); |
| |
| data = rate; |
| i2c_smbus_write_byte_data(drvdata->client, LM3695_RAMP_RATE_REG, data); |
| } else { |
| if (drvdata->enabled) |
| { |
| data = 0; |
| i2c_smbus_write_byte_data(drvdata->client, LM3695_GEN_PURPOSE_REG, data); |
| gpio_set_value(drvdata->hwen_gpio, 0); |
| drvdata->enabled = false; |
| } |
| } |
| return 0; |
| } |
| |
| int lm3695_update_status(struct backlight_device * bl) |
| { |
| struct lm3695_data *drvdata = bl_get_data(bl); |
| bool enabled = 1; |
| drvdata->next_brightness = bl->props.brightness; |
| |
| if (drvdata->next_brightness > drvdata->max_brightness) |
| drvdata->next_brightness = drvdata->max_brightness; |
| |
| if (bl->props.power != FB_BLANK_UNBLANK) { |
| enabled = 0; |
| } |
| |
| if (bl->props.fb_blank != FB_BLANK_UNBLANK) { |
| enabled = 0; |
| } |
| |
| lm3695_configure_device(drvdata, enabled); |
| |
| i2c_smbus_write_byte_data(drvdata->client, LM3695_BRIGHTNESS_L_REG, |
| LM3695_BRIGHTNESS_L_S(drvdata->next_brightness)); |
| i2c_smbus_write_byte_data(drvdata->client, LM3695_BRIGHTNESS_H_REG, |
| LM3695_BRIGHTNESS_H_S(drvdata->next_brightness)); |
| |
| drvdata->current_brightness = drvdata->next_brightness; |
| |
| return 0; |
| } |
| int lm3695_get_brightness(struct backlight_device *bl) |
| { |
| return bl->props.brightness; |
| } |
| |
| |
| static struct backlight_ops lm3695_backlight_ops = { |
| .update_status = lm3695_update_status, |
| .get_brightness = lm3695_get_brightness, |
| }; |
| |
| |
| int lm3695_get_devtree_pdata(struct i2c_client *client, struct lm3695_data *drvdata){ |
| |
| const int *prop; |
| const char *fb_dev; |
| int retval = 0; |
| |
| prop = of_get_property(client->dev.of_node, "max-brightness", NULL); |
| drvdata->max_brightness = prop ? (be32_to_cpu(*prop)) : LM3695_MAX_BRIGHTNESS; |
| |
| prop = of_get_property(client->dev.of_node, "default-brightness", NULL); |
| drvdata->default_brightness = prop ? (be32_to_cpu(*prop)) : drvdata->max_brightness / 2; |
| drvdata->current_brightness = drvdata->default_brightness; |
| drvdata->next_brightness = drvdata->default_brightness; |
| |
| retval = of_property_read_string(client->dev.of_node, "fb-device", &fb_dev); |
| if (!retval) |
| { |
| if(driver_find(fb_dev, &platform_bus_type) == NULL) |
| { |
| dev_warn(&client->dev, "Could not find %s driver\n", fb_dev); |
| retval = -EPROBE_DEFER; |
| goto done; |
| } |
| } |
| |
| if(default_brightness != 0) |
| { |
| /*override default brightness parameter if uboot has one for us*/ |
| drvdata->default_brightness = default_brightness; |
| dev_info(&client->dev, "Using cmdline parameter %d for brightness", default_brightness); |
| } |
| |
| drvdata->hwen_gpio = of_get_named_gpio(client->dev.of_node, "enable-gpio", 0); |
| |
| prop = of_get_property(client->dev.of_node, "ramp-rise-time", NULL); |
| if (prop) |
| { |
| drvdata->ramp_rise_rate = lm3695_time_to_ramp_rate(be32_to_cpu(*prop)); |
| } |
| prop = of_get_property(client->dev.of_node, "ramp-fall-time", NULL); |
| if (prop) |
| { |
| drvdata->ramp_fall_rate = lm3695_time_to_ramp_rate(be32_to_cpu(*prop)); |
| } |
| else |
| { |
| drvdata->ramp_rise_rate = LM3695_RAMP_RATE_0_MS; |
| drvdata->ramp_fall_rate = LM3695_RAMP_RATE_0_MS; |
| } |
| |
| prop = of_get_property(client->dev.of_node, "clock-divider", NULL); |
| if (prop && (be32_to_cpu(*prop) <= 0b1001) && (*prop >= 0)) |
| { |
| drvdata->dither_freq = be32_to_cpu(*prop); |
| } |
| else |
| { |
| drvdata->dither_freq = 0b11; |
| } |
| |
| drvdata->boost_freq = of_property_read_bool(client->dev.of_node, "boost-freq"); |
| drvdata->boost_shift = of_property_read_bool(client->dev.of_node, "freq-shift"); |
| drvdata->single_string = of_property_read_bool(client->dev.of_node, "single-string"); |
| drvdata->high_voltage = of_property_read_bool(client->dev.of_node, "high-voltage"); |
| |
| done: |
| return retval; |
| } |
| |
| static int lm3695_probe(struct i2c_client *client, |
| const struct i2c_device_id *device) |
| { |
| |
| struct lm3695_data *drvdata; |
| struct platform_device *pdev = to_platform_device(&client->dev); |
| struct backlight_device *bl; |
| struct backlight_properties props; |
| int status; |
| drvdata = devm_kzalloc(&client->dev,sizeof(struct lm3695_data), GFP_KERNEL); |
| |
| status = lm3695_get_devtree_pdata(client, drvdata); |
| if (status) |
| { |
| goto cleanup; |
| } |
| |
| drvdata->client = client; |
| |
| i2c_set_clientdata(client, drvdata); |
| |
| status = devm_gpio_request_one(&client->dev, drvdata->hwen_gpio, GPIOF_OUT_INIT_LOW, "lm3695 enable"); |
| if (status) |
| { |
| dev_err(&client->dev, "Could not request GPIO %u: %d\n", drvdata->hwen_gpio, status); |
| goto cleanup; |
| } |
| |
| drvdata->enabled = false; |
| |
| memset(&props, 0, sizeof(struct backlight_properties)); |
| props.max_brightness = LM3695_MAX_BRIGHTNESS; |
| props.type = BACKLIGHT_FIRMWARE; |
| |
| bl = backlight_device_register(dev_name(&client->dev), &client->dev, |
| drvdata, &lm3695_backlight_ops, &props); |
| |
| if (IS_ERR(bl)) |
| { |
| dev_err(&client->dev, "Could not register bl: %ld\n", PTR_ERR(bl)); |
| goto cleanup; |
| } |
| |
| status = sysfs_create_group(&bl->dev.kobj, &lm3695_bl_attribute_group); |
| if (status < 0) { |
| dev_err(&pdev->dev, "failed to create sysfs attributes\n"); |
| goto unregister; |
| } |
| |
| |
| bl->props.max_brightness = drvdata->max_brightness; |
| bl->props.brightness = drvdata->default_brightness; |
| |
| backlight_update_status(bl); |
| |
| platform_set_drvdata(pdev, bl); |
| |
| return 0; |
| unregister: |
| backlight_device_unregister(bl); |
| |
| cleanup: |
| return status; |
| |
| } |
| |
| static int lm3695_remove(struct i2c_client *client) |
| { |
| struct platform_device *pdev = to_platform_device(&client->dev); |
| struct backlight_device *bl = platform_get_drvdata(pdev); |
| struct lm3695_data *drvdata = bl_get_data(bl); |
| |
| lm3695_configure_device(drvdata, 0); |
| backlight_device_unregister(bl); |
| sysfs_remove_group(&bl->dev.kobj, &lm3695_bl_attribute_group); |
| |
| return 0; |
| } |
| |
| static void lm3695_shutdown(struct i2c_client *client) |
| { |
| struct platform_device *pdev = to_platform_device(&client->dev); |
| struct backlight_device *bl = platform_get_drvdata(pdev); |
| struct lm3695_data *drvdata = bl_get_data(bl); |
| |
| lm3695_configure_device(drvdata, 0); |
| |
| } |
| |
| static int lm3695_suspend(struct device *dev){ |
| struct platform_device *pdev = to_platform_device(dev); |
| struct backlight_device *bl = platform_get_drvdata(pdev); |
| struct lm3695_data *drvdata = bl_get_data(bl); |
| |
| lm3695_configure_device(drvdata, 0); |
| return 0; |
| } |
| |
| static int lm3695_resume(struct device *dev){ |
| struct platform_device *pdev = to_platform_device(dev); |
| struct backlight_device *bl = platform_get_drvdata(pdev); |
| |
| backlight_update_status(bl); |
| return 0; |
| } |
| |
| static const struct i2c_device_id lm3695_id[] = { |
| {LM3695_NAME, 0}, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(i2c, lm3695_id); |
| |
| static struct dev_pm_ops lm3695_pm_ops = { |
| .suspend = lm3695_suspend, |
| .resume = lm3695_resume, |
| }; |
| |
| #ifdef CONFIG_OF |
| static const struct of_device_id of_lm3695_bl_match[] = { |
| { .compatible = "national,lm3695", }, |
| {}, |
| }; |
| #endif |
| |
| MODULE_DEVICE_TABLE(of, of_lm3695_bl_match); |
| |
| static struct i2c_driver lm3695_i2c_driver = { |
| .probe = lm3695_probe, |
| .remove = lm3695_remove, |
| .id_table = lm3695_id, |
| .shutdown = lm3695_shutdown, |
| .driver = { |
| .name = LM3695_NAME, |
| .owner = THIS_MODULE, |
| .pm = &lm3695_pm_ops, |
| .of_match_table = of_match_ptr(of_lm3695_bl_match), |
| }, |
| |
| }; |
| |
| static int __init lm3695_init(void) |
| { |
| return i2c_add_driver(&lm3695_i2c_driver); |
| } |
| |
| static void __exit lm3695_exit(void) |
| { |
| i2c_del_driver(&lm3695_i2c_driver); |
| } |
| |
| |
| module_init(lm3695_init); |
| module_exit(lm3695_exit); |
| |
| MODULE_DESCRIPTION("i2c driver for LM3695 backlight driver"); |
| MODULE_LICENSE("GPLv2"); |
| MODULE_AUTHOR("Andrew LeCain<alecain@nestlabs.com>"); |