| // SPDX-License-Identifier: GPL-2.0 | |
| // LED Multicolor class interface | |
| // Copyright (C) 2019-20 Texas Instruments Incorporated - http://www.ti.com/ | |
| // Author: Dan Murphy <dmurphy@ti.com> | |
| #include <linux/device.h> | |
| #include <linux/init.h> | |
| #include <linux/led-class-multicolor.h> | |
| #include <linux/math.h> | |
| #include <linux/minmax.h> | |
| #include <linux/module.h> | |
| #include <linux/slab.h> | |
| #include <linux/uaccess.h> | |
| static unsigned int led_mc_get_max_intensity(struct led_classdev_mc *mcled_cdev, size_t index) | |
| { | |
| unsigned int max_intensity; | |
| /* | |
| * The maximum global brightness value might still be changed by | |
| * led_classdev_register_ext() using devicetree properties. This | |
| * prevents us from changing subled_info[X].max_intensity when | |
| * registering a multicolor LED class device, so we have to do | |
| * this during runtime. | |
| */ | |
| max_intensity = mcled_cdev->subled_info[index].max_intensity; | |
| if (max_intensity) | |
| return max_intensity; | |
| return mcled_cdev->led_cdev.max_brightness; | |
| } | |
| int led_mc_calc_color_components(struct led_classdev_mc *mcled_cdev, | |
| enum led_brightness brightness) | |
| { | |
| struct led_classdev *led_cdev = &mcled_cdev->led_cdev; | |
| int i; | |
| for (i = 0; i < mcled_cdev->num_colors; i++) | |
| mcled_cdev->subled_info[i].brightness = | |
| DIV_ROUND_CLOSEST(brightness * | |
| mcled_cdev->subled_info[i].intensity, | |
| led_cdev->max_brightness); | |
| return 0; | |
| } | |
| EXPORT_SYMBOL_GPL(led_mc_calc_color_components); | |
| static ssize_t multi_max_intensity_show(struct device *dev, | |
| struct device_attribute *intensity_attr, char *buf) | |
| { | |
| struct led_classdev *led_cdev = dev_get_drvdata(dev); | |
| struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev); | |
| unsigned int max_intensity; | |
| int len = 0; | |
| for (int i = 0; i < mcled_cdev->num_colors; i++) { | |
| max_intensity = led_mc_get_max_intensity(mcled_cdev, i); | |
| len += sysfs_emit_at(buf, len, "%u", max_intensity); | |
| if (i < mcled_cdev->num_colors - 1) | |
| len += sprintf(buf + len, " "); | |
| } | |
| buf[len++] = '\n'; | |
| return len; | |
| } | |
| static DEVICE_ATTR_RO(multi_max_intensity); | |
| static ssize_t multi_intensity_store(struct device *dev, | |
| struct device_attribute *intensity_attr, | |
| const char *buf, size_t size) | |
| { | |
| struct led_classdev *led_cdev = dev_get_drvdata(dev); | |
| struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev); | |
| int nrchars, offset = 0; | |
| unsigned int intensity_value[LED_COLOR_ID_MAX]; | |
| unsigned int max_intensity; | |
| int i; | |
| ssize_t ret; | |
| mutex_lock(&led_cdev->led_access); | |
| for (i = 0; i < mcled_cdev->num_colors; i++) { | |
| ret = sscanf(buf + offset, "%u%n", | |
| &intensity_value[i], &nrchars); | |
| if (ret != 1) { | |
| ret = -EINVAL; | |
| goto err_out; | |
| } | |
| offset += nrchars; | |
| } | |
| offset++; | |
| if (offset < size) { | |
| ret = -EINVAL; | |
| goto err_out; | |
| } | |
| for (int i = 0; i < mcled_cdev->num_colors; i++) { | |
| max_intensity = led_mc_get_max_intensity(mcled_cdev, i); | |
| mcled_cdev->subled_info[i].intensity = min(intensity_value[i], max_intensity); | |
| } | |
| if (!test_bit(LED_BLINK_SW, &led_cdev->work_flags)) | |
| led_set_brightness(led_cdev, led_cdev->brightness); | |
| ret = size; | |
| err_out: | |
| mutex_unlock(&led_cdev->led_access); | |
| return ret; | |
| } | |
| static ssize_t multi_intensity_show(struct device *dev, | |
| struct device_attribute *intensity_attr, | |
| char *buf) | |
| { | |
| struct led_classdev *led_cdev = dev_get_drvdata(dev); | |
| struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev); | |
| int len = 0; | |
| int i; | |
| for (i = 0; i < mcled_cdev->num_colors; i++) { | |
| len += sprintf(buf + len, "%d", | |
| mcled_cdev->subled_info[i].intensity); | |
| if (i < mcled_cdev->num_colors - 1) | |
| len += sprintf(buf + len, " "); | |
| } | |
| buf[len++] = '\n'; | |
| return len; | |
| } | |
| static DEVICE_ATTR_RW(multi_intensity); | |
| static ssize_t multi_index_show(struct device *dev, | |
| struct device_attribute *multi_index_attr, | |
| char *buf) | |
| { | |
| struct led_classdev *led_cdev = dev_get_drvdata(dev); | |
| struct led_classdev_mc *mcled_cdev = lcdev_to_mccdev(led_cdev); | |
| int len = 0; | |
| int index; | |
| int i; | |
| for (i = 0; i < mcled_cdev->num_colors; i++) { | |
| index = mcled_cdev->subled_info[i].color_index; | |
| len += sprintf(buf + len, "%s", led_get_color_name(index)); | |
| if (i < mcled_cdev->num_colors - 1) | |
| len += sprintf(buf + len, " "); | |
| } | |
| buf[len++] = '\n'; | |
| return len; | |
| } | |
| static DEVICE_ATTR_RO(multi_index); | |
| static struct attribute *led_multicolor_attrs[] = { | |
| &dev_attr_multi_max_intensity.attr, | |
| &dev_attr_multi_intensity.attr, | |
| &dev_attr_multi_index.attr, | |
| NULL, | |
| }; | |
| ATTRIBUTE_GROUPS(led_multicolor); | |
| int led_classdev_multicolor_register_ext(struct device *parent, | |
| struct led_classdev_mc *mcled_cdev, | |
| struct led_init_data *init_data) | |
| { | |
| struct led_classdev *led_cdev; | |
| if (!mcled_cdev) | |
| return -EINVAL; | |
| if (mcled_cdev->num_colors <= 0) | |
| return -EINVAL; | |
| if (mcled_cdev->num_colors > LED_COLOR_ID_MAX) | |
| return -EINVAL; | |
| led_cdev = &mcled_cdev->led_cdev; | |
| led_cdev->flags |= LED_MULTI_COLOR; | |
| mcled_cdev->led_cdev.groups = led_multicolor_groups; | |
| return led_classdev_register_ext(parent, led_cdev, init_data); | |
| } | |
| EXPORT_SYMBOL_GPL(led_classdev_multicolor_register_ext); | |
| void led_classdev_multicolor_unregister(struct led_classdev_mc *mcled_cdev) | |
| { | |
| if (!mcled_cdev) | |
| return; | |
| led_classdev_unregister(&mcled_cdev->led_cdev); | |
| } | |
| EXPORT_SYMBOL_GPL(led_classdev_multicolor_unregister); | |
| static void devm_led_classdev_multicolor_release(struct device *dev, void *res) | |
| { | |
| led_classdev_multicolor_unregister(*(struct led_classdev_mc **)res); | |
| } | |
| int devm_led_classdev_multicolor_register_ext(struct device *parent, | |
| struct led_classdev_mc *mcled_cdev, | |
| struct led_init_data *init_data) | |
| { | |
| struct led_classdev_mc **dr; | |
| int ret; | |
| dr = devres_alloc(devm_led_classdev_multicolor_release, | |
| sizeof(*dr), GFP_KERNEL); | |
| if (!dr) | |
| return -ENOMEM; | |
| ret = led_classdev_multicolor_register_ext(parent, mcled_cdev, | |
| init_data); | |
| if (ret) { | |
| devres_free(dr); | |
| return ret; | |
| } | |
| *dr = mcled_cdev; | |
| devres_add(parent, dr); | |
| return 0; | |
| } | |
| EXPORT_SYMBOL_GPL(devm_led_classdev_multicolor_register_ext); | |
| static int devm_led_classdev_multicolor_match(struct device *dev, | |
| void *res, void *data) | |
| { | |
| struct led_classdev_mc **p = res; | |
| if (WARN_ON(!p || !*p)) | |
| return 0; | |
| return *p == data; | |
| } | |
| void devm_led_classdev_multicolor_unregister(struct device *dev, | |
| struct led_classdev_mc *mcled_cdev) | |
| { | |
| WARN_ON(devres_release(dev, | |
| devm_led_classdev_multicolor_release, | |
| devm_led_classdev_multicolor_match, mcled_cdev)); | |
| } | |
| EXPORT_SYMBOL_GPL(devm_led_classdev_multicolor_unregister); | |
| MODULE_AUTHOR("Dan Murphy <dmurphy@ti.com>"); | |
| MODULE_DESCRIPTION("Multicolor LED class interface"); | |
| MODULE_LICENSE("GPL v2"); |