| /* |
| * RGB-led driver for s2mu004 |
| * |
| * Copyright (C) 2015 Samsung Electronics |
| * |
| * 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. |
| */ |
| |
| #define pr_fmt(fmt) "[LED] " fmt |
| |
| #include <linux/kernel.h> |
| #include <linux/module.h> |
| #include <linux/of_gpio.h> |
| #include <linux/slab.h> |
| #include <linux/interrupt.h> |
| #include <linux/init.h> |
| #include <linux/platform_device.h> |
| #include <linux/leds.h> |
| #include <linux/err.h> |
| #include <linux/of.h> |
| #include <linux/sec_sysfs.h> |
| #include <linux/mfd/samsung/s2mu004.h> |
| #include <linux/mfd/samsung/s2mu004-private.h> |
| #include <linux/leds-s2mu004-rgb.h> |
| |
| struct s2mu004_rgb_drvdata { |
| struct s2mu004_rgb_pdata *pdata; |
| struct i2c_client *i2c; |
| struct device *dev; |
| u8 brightness; |
| u8 lpmode; |
| u32 on_delay; |
| u32 off_delay; |
| u32 ratio[S2MU004_LED_NUM]; |
| }; |
| |
| static void s2mu004_rgb_set_state(struct s2mu004_rgb_drvdata *ddata, |
| enum led_color led, u32 brightness, unsigned int led_state) |
| { |
| int ret = 0; |
| int col = ddata->pdata->col[led]; |
| u8 reg, val, mask; |
| u32 tmp; |
| |
| if (brightness) { |
| tmp = (u32)(brightness * ddata->brightness); |
| tmp /= ddata->pdata->brightness; |
| if (tmp == 0) |
| tmp = 1; |
| reg = S2MU004_REG_LED1_CURRENT + col; |
| val = (u8)tmp; |
| pr_info("LED[%d] %u\n", led, brightness); |
| ret = s2mu004_write_reg(ddata->i2c, reg, val); |
| if (IS_ERR_VALUE(ret)) { |
| pr_err("failed to write brightness : %d\n", ret); |
| return; |
| } |
| val = led_state << ((S2MU004_LED_NUM - col - 1) << 1); |
| } else |
| val = LED_DISABLE; |
| |
| reg = S2MU004_REG_LED_EN; |
| mask = RGBLED_ENMASK << ((S2MU004_LED_NUM - col - 1) << 1); |
| ret = s2mu004_update_reg(ddata->i2c, reg, val, mask); |
| if (IS_ERR_VALUE(ret)) |
| pr_err("failed to LEDEN : %d\n", ret); |
| } |
| |
| static int s2mu004_rgb_ramp(struct s2mu004_rgb_drvdata *ddata, |
| enum led_color led, int ramp_up, int ramp_down) |
| { |
| int ret = 0; |
| int value; |
| int col = ddata->pdata->col[led]; |
| |
| if (ramp_up > 800) |
| ramp_up = ((ramp_up - 800) >> 1) + 800; |
| ramp_up /= 100; |
| |
| if (ramp_down > 800) |
| ramp_down = ((ramp_down - 800) >> 1) + 800; |
| ramp_down /= 100; |
| |
| value = (ramp_down) | (ramp_up << 4); |
| ret = s2mu004_write_reg(ddata->i2c, |
| S2MU004_REG_LED1_RAMP + (col << 1), value); |
| if (IS_ERR_VALUE(ret)) { |
| pr_err("failed to write REG_LEDRMP : %d\n", ret); |
| return -ENODEV; |
| } |
| |
| return 0; |
| } |
| |
| static int s2mu004_rgb_blink(struct s2mu004_rgb_drvdata *ddata, |
| enum led_color led, unsigned int on, unsigned int off) |
| { |
| int ret = 0; |
| int value; |
| int col = ddata->pdata->col[led]; |
| |
| value = (LEDBLNK_ON(on) << 4) | LEDBLNK_OFF(off); |
| ret = s2mu004_write_reg(ddata->i2c, |
| S2MU004_REG_LED1_DUR + (col << 1), value); |
| if (IS_ERR_VALUE(ret)) { |
| pr_err("failed to write REG_LEDBLNK : %d\n", ret); |
| return -EINVAL; |
| } |
| |
| pr_info("LED[%d] 0x%x\n", led, value); |
| return ret; |
| } |
| |
| static void s2mu004_rgb_reset(struct s2mu004_rgb_drvdata *ddata) |
| { |
| int i = 0; |
| |
| for (i = 0; i < ddata->pdata->nleds; i++) { |
| s2mu004_rgb_set_state(ddata, i, LED_OFF, LED_DISABLE); |
| s2mu004_rgb_ramp(ddata, i, 0, 0); |
| } |
| } |
| |
| static ssize_t store_s2mu004_rgb_pattern(struct device *dev, |
| struct device_attribute *devattr, const char *buf, size_t count) |
| { |
| struct s2mu004_rgb_drvdata *ddata = dev_get_drvdata(dev); |
| int mode = 0; |
| int ret; |
| |
| ret = sscanf(buf, "%1d", &mode); |
| if (ret == 0) { |
| pr_err("fail to get led_pattern\n"); |
| return count; |
| } |
| pr_info("%s : %d lp : %u\n", __func__, mode, ddata->lpmode); |
| s2mu004_rgb_reset(ddata); |
| switch (mode) { |
| case CHARGING: |
| s2mu004_rgb_set_state(ddata, LED_RED, |
| ddata->ratio[LED_RED], LED_ALWAYS_ON); |
| break; |
| case CHARGING_ERR: |
| s2mu004_rgb_blink(ddata, LED_RED, 500, 500); |
| s2mu004_rgb_set_state(ddata, LED_RED, |
| ddata->ratio[LED_RED], LED_BLINK); |
| break; |
| case MISSED_NOTI: |
| s2mu004_rgb_blink(ddata, LED_BLUE, 500, 5000); |
| s2mu004_rgb_set_state(ddata, LED_BLUE, |
| ddata->ratio[LED_BLUE], LED_BLINK); |
| break; |
| case LOW_BATTERY: |
| s2mu004_rgb_blink(ddata, LED_RED, 500, 5000); |
| s2mu004_rgb_set_state(ddata, LED_RED, |
| ddata->ratio[LED_RED], LED_BLINK); |
| break; |
| case FULLY_CHARGED: |
| s2mu004_rgb_set_state(ddata, LED_GREEN, |
| ddata->ratio[LED_GREEN], LED_ALWAYS_ON); |
| break; |
| case POWERING: |
| s2mu004_rgb_ramp(ddata, LED_GREEN, 800, 800); |
| s2mu004_rgb_blink(ddata, LED_GREEN, 200, 200); |
| s2mu004_rgb_set_state(ddata, LED_BLUE, |
| ddata->ratio[LED_BLUE], LED_ALWAYS_ON); |
| s2mu004_rgb_set_state(ddata, LED_GREEN, |
| ddata->ratio[LED_GREEN], LED_BLINK); |
| break; |
| case PATTERN_OFF: |
| default: |
| break; |
| } |
| |
| return count; |
| } |
| |
| static ssize_t store_s2mu004_rgb_blink(struct device *dev, |
| struct device_attribute *devattr, const char *buf, size_t count) |
| { |
| struct s2mu004_rgb_drvdata *ddata = dev_get_drvdata(dev); |
| int val = 0; |
| int on = 0; |
| int off = 0; |
| int i = 0; |
| u8 br[S2MU004_LED_NUM]; |
| |
| if (sscanf(buf, "0x%8x %5d %5d", &val, &on, &off) != 3) { |
| pr_err("fail to get led_blink value.\n"); |
| return count; |
| } |
| pr_info("%s on: %dms, off: %dms, col: 0x%x, lp: %u\n", |
| __func__, on, off, val, ddata->lpmode); |
| s2mu004_rgb_reset(ddata); |
| for (i = LED_BLUE; i >= LED_RED; i--) { |
| br[i] = val & 0xff; |
| val >>= 8; |
| if (br[i] > ddata->ratio[i]) |
| br[i] = ddata->ratio[i]; |
| } |
| |
| for (i = 0; i < ddata->pdata->nleds; i++) { |
| if (br[i]) { |
| if (!off) |
| s2mu004_rgb_set_state(ddata, i, br[i], LED_ALWAYS_ON); |
| else { |
| s2mu004_rgb_set_state(ddata, i, br[i], LED_BLINK); |
| s2mu004_rgb_blink(ddata, i, on, off); |
| } |
| } |
| } |
| return count; |
| } |
| |
| static ssize_t store_s2mu004_rgb_brightness(struct device *dev, |
| struct device_attribute *devattr, const char *buf, size_t count) |
| { |
| struct s2mu004_rgb_drvdata *ddata = dev_get_drvdata(dev); |
| int ret; |
| u8 brightness; |
| |
| ret = kstrtou8(buf, 0, &brightness); |
| if (ret != 0) { |
| pr_err("fail to get led_brightness.\n"); |
| return count; |
| } |
| ddata->lpmode = 0; |
| if (brightness > LED_MAX_CURRENT) |
| brightness = LED_MAX_CURRENT; |
| ddata->brightness = brightness; |
| pr_info("led brightness set to %u\n", brightness); |
| return count; |
| } |
| |
| static ssize_t store_s2mu004_rgb_lowpower(struct device *dev, |
| struct device_attribute *devattr, const char *buf, size_t count) |
| { |
| struct s2mu004_rgb_drvdata *ddata = dev_get_drvdata(dev); |
| struct s2mu004_rgb_pdata *pdata = ddata->pdata; |
| u8 val; |
| int i = 0; |
| |
| if (kstrtou8(buf, 0, &val)) { |
| pr_err("fail to get led_lowpower.\n"); |
| return count; |
| } |
| ddata->lpmode = val; |
| pr_info("%s set to %u\n", __func__, val); |
| |
| ddata->brightness = !val ? pdata->brightness : pdata->lp_brightness; |
| |
| for (i = 0 ; i < ddata->pdata->nleds; i++) |
| ddata->ratio[i] = !val ? pdata->ratio[i] : pdata->ratio_low[i]; |
| |
| return count; |
| } |
| |
| #define ATTR_STORE_RGB(name, type) \ |
| static ssize_t store_led_##name(struct device *dev, \ |
| struct device_attribute *devattr, const char *buf, size_t count)\ |
| { \ |
| struct s2mu004_rgb_drvdata *ddata = dev_get_drvdata(dev); \ |
| u8 val; \ |
| \ |
| if (kstrtou8(buf, 0, &val)) \ |
| pr_err("fail to get brightness.\n"); \ |
| else \ |
| s2mu004_rgb_set_state(ddata, type, \ |
| val, val ? LED_ALWAYS_ON : LED_DISABLE); \ |
| pr_info("%s %u\n", __func__, val); \ |
| return count; \ |
| } \ |
| static DEVICE_ATTR(led_##name, 0660, NULL, store_led_##name) |
| |
| ATTR_STORE_RGB(r, LED_RED); |
| ATTR_STORE_RGB(g, LED_GREEN); |
| ATTR_STORE_RGB(b, LED_BLUE); |
| static DEVICE_ATTR(led_pattern, 0660, NULL, store_s2mu004_rgb_pattern); |
| static DEVICE_ATTR(led_blink, 0660, NULL, store_s2mu004_rgb_blink); |
| static DEVICE_ATTR(led_brightness, 0660, NULL, store_s2mu004_rgb_brightness); |
| static DEVICE_ATTR(led_lowpower, 0660, NULL, store_s2mu004_rgb_lowpower); |
| |
| static struct attribute *sec_led_attributes[] = { |
| &dev_attr_led_r.attr, |
| &dev_attr_led_g.attr, |
| &dev_attr_led_b.attr, |
| &dev_attr_led_pattern.attr, |
| &dev_attr_led_blink.attr, |
| &dev_attr_led_brightness.attr, |
| &dev_attr_led_lowpower.attr, |
| NULL, |
| }; |
| |
| static struct attribute_group sec_led_attr_group = { |
| .attrs = sec_led_attributes, |
| }; |
| |
| static struct s2mu004_rgb_pdata *s2mu004_rgb_parse_dt(struct device *dev) |
| { |
| struct device_node *node, *pp; |
| struct s2mu004_rgb_pdata *pdata; |
| int i; |
| u32 col; |
| |
| pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); |
| if (unlikely(!pdata)) |
| return ERR_PTR(-ENOMEM); |
| |
| pdata->octa_color = (lcdtype >> 16) & 0x0000000f; |
| node = of_find_node_by_name(dev->parent->of_node, "s2mu004_led"); |
| if (unlikely(!node)) { |
| pr_err("failed to find dt node\n"); |
| return ERR_PTR(-EINVAL); |
| } |
| of_property_read_u32(node, "nleds", &pdata->nleds); |
| pdata->name = (const char *)of_get_property(node, "led_color", NULL); |
| for (i = 0; i < pdata->nleds; i++) { |
| switch (pdata->name[i]) { |
| case 'R': |
| pdata->col[i] = LED_RED; |
| break; |
| case 'G': |
| pdata->col[i] = LED_GREEN; |
| break; |
| case 'B': |
| pdata->col[i] = LED_BLUE; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| of_property_read_u32(node, "brightness", &pdata->brightness); |
| of_property_read_u32(node, "lp_brightness", &pdata->lp_brightness); |
| of_property_read_u32_array(node, "ratio", |
| pdata->ratio, S2MU004_LED_NUM); |
| of_property_read_u32_array(node, "ratio_low", |
| pdata->ratio_low, S2MU004_LED_NUM); |
| for_each_child_of_node(node, pp) { |
| of_property_read_u32(pp, "octa_color", &col); |
| if (pdata->octa_color != col) |
| continue; |
| of_property_read_u32_array(pp, "ratio", |
| pdata->ratio, S2MU004_LED_NUM); |
| of_property_read_u32_array(pp, "ratio_low", |
| pdata->ratio_low, S2MU004_LED_NUM); |
| } |
| pr_info("brightness : %d, lp_brightness : %d\n", |
| pdata->brightness, pdata->lp_brightness); |
| pr_info("octa_color 0x%x\n", pdata->octa_color); |
| for (i = 0; i < pdata->nleds; i++) |
| pr_info("%c ratio : %u ratio_low : %u\n", |
| pdata->name[i], pdata->ratio[i], pdata->ratio_low[i]); |
| return pdata; |
| } |
| |
| static int s2mu004_rgb_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct s2mu004_dev *s2mu004_dev = dev_get_drvdata(dev->parent); |
| struct s2mu004_rgb_pdata *pdata = dev_get_platdata(dev); |
| struct s2mu004_rgb_drvdata *ddata; |
| int ret = 0; |
| int i = 0; |
| |
| if (!pdata) { |
| pdata = s2mu004_rgb_parse_dt(dev); |
| if (unlikely(!pdata)) |
| return PTR_ERR(pdata); |
| } |
| ddata = devm_kzalloc(dev, sizeof(*ddata), GFP_KERNEL); |
| if (unlikely(!ddata)) { |
| pr_err("out of memory drv data\n"); |
| return PTR_ERR(ddata); |
| } |
| platform_set_drvdata(pdev, ddata); |
| ddata->pdata = pdata; |
| ddata->brightness = pdata->brightness; |
| ddata->i2c = s2mu004_dev->i2c; |
| dev_set_drvdata(dev, ddata); |
| for (i = 0; i < pdata->nleds; i++) |
| ddata->ratio[i] = pdata->ratio[i]; |
| ddata->dev = sec_device_create(ddata, "led"); |
| if (IS_ERR(ddata->dev)) { |
| pr_err("Failed to create device for samsung specific led\n"); |
| return PTR_ERR(ddata->dev); |
| } |
| ret = sysfs_create_group(&ddata->dev->kobj, &sec_led_attr_group); |
| if (ret < 0) { |
| pr_err("Failed to create sysfs group for samsung specific led\n"); |
| goto err_sysfs_create; |
| } |
| return 0; |
| |
| err_sysfs_create: |
| sec_device_destroy(ddata->dev->devt); |
| return ret; |
| } |
| |
| static int s2mu004_rgb_remove(struct platform_device *pdev) |
| { |
| struct s2mu004_rgb_drvdata *ddata = platform_get_drvdata(pdev); |
| |
| if (!ddata->i2c) |
| return 0; |
| |
| s2mu004_rgb_reset(ddata); |
| sysfs_remove_group(&ddata->dev->kobj, &sec_led_attr_group); |
| return 0; |
| } |
| |
| static void s2mu004_rgb_shutdown(struct platform_device *pdev) |
| { |
| struct s2mu004_rgb_drvdata *ddata = platform_get_drvdata(pdev); |
| |
| if (!ddata->i2c) |
| return; |
| |
| s2mu004_rgb_reset(ddata); |
| sysfs_remove_group(&ddata->dev->kobj, &sec_led_attr_group); |
| } |
| static struct platform_driver s2mu004_fled_driver = { |
| .driver = { |
| .name = "s2mu004-leds", |
| .owner = THIS_MODULE, |
| }, |
| .probe = s2mu004_rgb_probe, |
| .remove = s2mu004_rgb_remove, |
| .shutdown = s2mu004_rgb_shutdown, |
| }; |
| module_platform_driver(s2mu004_fled_driver); |
| |
| MODULE_DESCRIPTION("s2mu004 LED driver"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_VERSION("1.0"); |