| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Multi-color LED built with monochromatic LED devices |
| * |
| * This driver groups several monochromatic LED devices in a single multicolor LED device. |
| * |
| * Compared to handling this grouping in user-space, the benefits are: |
| * - The state of the monochromatic LED relative to each other is always consistent. |
| * - The sysfs interface of the LEDs can be used for the group as a whole. |
| * |
| * Copyright 2023 Jean-Jacques Hiblot <jjhiblot@traphandler.com> |
| */ |
| |
| #include <linux/err.h> |
| #include <linux/leds.h> |
| #include <linux/led-class-multicolor.h> |
| #include <linux/math.h> |
| #include <linux/module.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/platform_device.h> |
| #include <linux/property.h> |
| |
| struct leds_multicolor { |
| struct led_classdev_mc mc_cdev; |
| struct led_classdev **monochromatics; |
| }; |
| |
| static int leds_gmc_set(struct led_classdev *cdev, enum led_brightness brightness) |
| { |
| struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); |
| struct leds_multicolor *priv = container_of(mc_cdev, struct leds_multicolor, mc_cdev); |
| const unsigned int group_max_brightness = mc_cdev->led_cdev.max_brightness; |
| int i; |
| |
| for (i = 0; i < mc_cdev->num_colors; i++) { |
| struct led_classdev *mono = priv->monochromatics[i]; |
| const unsigned int mono_max_brightness = mono->max_brightness; |
| unsigned int intensity = mc_cdev->subled_info[i].intensity; |
| int mono_brightness; |
| |
| /* |
| * Scale the brightness according to relative intensity of the |
| * color AND the max brightness of the monochromatic LED. |
| */ |
| mono_brightness = DIV_ROUND_CLOSEST(brightness * intensity * mono_max_brightness, |
| group_max_brightness * group_max_brightness); |
| |
| led_set_brightness(mono, mono_brightness); |
| } |
| |
| return 0; |
| } |
| |
| static void restore_sysfs_write_access(void *data) |
| { |
| struct led_classdev *led_cdev = data; |
| |
| /* Restore the write acccess to the LED */ |
| mutex_lock(&led_cdev->led_access); |
| led_sysfs_enable(led_cdev); |
| mutex_unlock(&led_cdev->led_access); |
| } |
| |
| static int leds_gmc_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct led_init_data init_data = {}; |
| struct led_classdev *cdev; |
| struct mc_subled *subled; |
| struct leds_multicolor *priv; |
| unsigned int max_brightness = 0; |
| int i, ret, count = 0, common_flags = 0; |
| |
| priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| for (;;) { |
| struct led_classdev *led_cdev; |
| |
| led_cdev = devm_of_led_get_optional(dev, count); |
| if (IS_ERR(led_cdev)) |
| return dev_err_probe(dev, PTR_ERR(led_cdev), "Unable to get LED #%d", |
| count); |
| if (!led_cdev) |
| break; |
| |
| priv->monochromatics = devm_krealloc_array(dev, priv->monochromatics, |
| count + 1, sizeof(*priv->monochromatics), |
| GFP_KERNEL); |
| if (!priv->monochromatics) |
| return -ENOMEM; |
| |
| common_flags |= led_cdev->flags; |
| priv->monochromatics[count] = led_cdev; |
| |
| max_brightness = max(max_brightness, led_cdev->max_brightness); |
| |
| count++; |
| } |
| |
| subled = devm_kcalloc(dev, count, sizeof(*subled), GFP_KERNEL); |
| if (!subled) |
| return -ENOMEM; |
| priv->mc_cdev.subled_info = subled; |
| |
| for (i = 0; i < count; i++) { |
| struct led_classdev *led_cdev = priv->monochromatics[i]; |
| |
| subled[i].color_index = led_cdev->color; |
| |
| /* Configure the LED intensity to its maximum */ |
| subled[i].intensity = max_brightness; |
| } |
| |
| /* Initialise the multicolor's LED class device */ |
| cdev = &priv->mc_cdev.led_cdev; |
| cdev->brightness_set_blocking = leds_gmc_set; |
| cdev->max_brightness = max_brightness; |
| cdev->color = LED_COLOR_ID_MULTI; |
| priv->mc_cdev.num_colors = count; |
| |
| /* we only need suspend/resume if a sub-led requests it */ |
| if (common_flags & LED_CORE_SUSPENDRESUME) |
| cdev->flags = LED_CORE_SUSPENDRESUME; |
| |
| init_data.fwnode = dev_fwnode(dev); |
| ret = devm_led_classdev_multicolor_register_ext(dev, &priv->mc_cdev, &init_data); |
| if (ret) |
| return dev_err_probe(dev, ret, "failed to register multicolor LED for %s.\n", |
| cdev->name); |
| |
| ret = leds_gmc_set(cdev, cdev->brightness); |
| if (ret) |
| return dev_err_probe(dev, ret, "failed to set LED value for %s.", cdev->name); |
| |
| for (i = 0; i < count; i++) { |
| struct led_classdev *led_cdev = priv->monochromatics[i]; |
| |
| /* |
| * Make the individual LED sysfs interface read-only to prevent the user |
| * to change the brightness of the individual LEDs of the group. |
| */ |
| mutex_lock(&led_cdev->led_access); |
| led_sysfs_disable(led_cdev); |
| mutex_unlock(&led_cdev->led_access); |
| |
| /* Restore the write access to the LED sysfs when the group is destroyed */ |
| devm_add_action_or_reset(dev, restore_sysfs_write_access, led_cdev); |
| } |
| |
| return 0; |
| } |
| |
| static const struct of_device_id of_leds_group_multicolor_match[] = { |
| { .compatible = "leds-group-multicolor" }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, of_leds_group_multicolor_match); |
| |
| static struct platform_driver leds_group_multicolor_driver = { |
| .probe = leds_gmc_probe, |
| .driver = { |
| .name = "leds_group_multicolor", |
| .of_match_table = of_leds_group_multicolor_match, |
| } |
| }; |
| module_platform_driver(leds_group_multicolor_driver); |
| |
| MODULE_AUTHOR("Jean-Jacques Hiblot <jjhiblot@traphandler.com>"); |
| MODULE_DESCRIPTION("LEDs group multicolor driver"); |
| MODULE_LICENSE("GPL"); |
| MODULE_ALIAS("platform:leds-group-multicolor"); |