| // SPDX-License-Identifier: GPL-2.0-only |
| /* |
| * PWM-based multi-color LED control |
| * |
| * Copyright 2022 Sven Schwermer <sven.schwermer@disruptive-technologies.com> |
| */ |
| |
| #include <linux/err.h> |
| #include <linux/kernel.h> |
| #include <linux/led-class-multicolor.h> |
| #include <linux/leds.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/module.h> |
| #include <linux/mutex.h> |
| #include <linux/platform_device.h> |
| #include <linux/property.h> |
| #include <linux/pwm.h> |
| |
| struct pwm_led { |
| struct pwm_device *pwm; |
| struct pwm_state state; |
| bool active_low; |
| }; |
| |
| struct pwm_mc_led { |
| struct led_classdev_mc mc_cdev; |
| struct mutex lock; |
| struct pwm_led leds[]; |
| }; |
| |
| static int led_pwm_mc_set(struct led_classdev *cdev, |
| enum led_brightness brightness) |
| { |
| struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev); |
| struct pwm_mc_led *priv = container_of(mc_cdev, struct pwm_mc_led, mc_cdev); |
| unsigned long long duty; |
| int ret = 0; |
| int i; |
| |
| led_mc_calc_color_components(mc_cdev, brightness); |
| |
| mutex_lock(&priv->lock); |
| |
| for (i = 0; i < mc_cdev->num_colors; i++) { |
| duty = priv->leds[i].state.period; |
| duty *= mc_cdev->subled_info[i].brightness; |
| do_div(duty, cdev->max_brightness); |
| |
| if (priv->leds[i].active_low) |
| duty = priv->leds[i].state.period - duty; |
| |
| priv->leds[i].state.duty_cycle = duty; |
| priv->leds[i].state.enabled = duty > 0; |
| ret = pwm_apply_might_sleep(priv->leds[i].pwm, |
| &priv->leds[i].state); |
| if (ret) |
| break; |
| } |
| |
| mutex_unlock(&priv->lock); |
| |
| return ret; |
| } |
| |
| static int iterate_subleds(struct device *dev, struct pwm_mc_led *priv, |
| struct fwnode_handle *mcnode) |
| { |
| struct mc_subled *subled = priv->mc_cdev.subled_info; |
| struct fwnode_handle *fwnode; |
| struct pwm_led *pwmled; |
| u32 color; |
| int ret; |
| |
| /* iterate over the nodes inside the multi-led node */ |
| fwnode_for_each_child_node(mcnode, fwnode) { |
| pwmled = &priv->leds[priv->mc_cdev.num_colors]; |
| pwmled->pwm = devm_fwnode_pwm_get(dev, fwnode, NULL); |
| if (IS_ERR(pwmled->pwm)) { |
| ret = dev_err_probe(dev, PTR_ERR(pwmled->pwm), "unable to request PWM\n"); |
| goto release_fwnode; |
| } |
| pwm_init_state(pwmled->pwm, &pwmled->state); |
| pwmled->active_low = fwnode_property_read_bool(fwnode, "active-low"); |
| |
| ret = fwnode_property_read_u32(fwnode, "color", &color); |
| if (ret) { |
| dev_err(dev, "cannot read color: %d\n", ret); |
| goto release_fwnode; |
| } |
| |
| subled[priv->mc_cdev.num_colors].color_index = color; |
| priv->mc_cdev.num_colors++; |
| } |
| |
| return 0; |
| |
| release_fwnode: |
| fwnode_handle_put(fwnode); |
| return ret; |
| } |
| |
| static int led_pwm_mc_probe(struct platform_device *pdev) |
| { |
| struct fwnode_handle *mcnode, *fwnode; |
| struct led_init_data init_data = {}; |
| struct led_classdev *cdev; |
| struct mc_subled *subled; |
| struct pwm_mc_led *priv; |
| int count = 0; |
| int ret = 0; |
| |
| mcnode = device_get_named_child_node(&pdev->dev, "multi-led"); |
| if (!mcnode) |
| return dev_err_probe(&pdev->dev, -ENODEV, |
| "expected multi-led node\n"); |
| |
| /* count the nodes inside the multi-led node */ |
| fwnode_for_each_child_node(mcnode, fwnode) |
| count++; |
| |
| priv = devm_kzalloc(&pdev->dev, struct_size(priv, leds, count), |
| GFP_KERNEL); |
| if (!priv) { |
| ret = -ENOMEM; |
| goto release_mcnode; |
| } |
| mutex_init(&priv->lock); |
| |
| subled = devm_kcalloc(&pdev->dev, count, sizeof(*subled), GFP_KERNEL); |
| if (!subled) { |
| ret = -ENOMEM; |
| goto release_mcnode; |
| } |
| priv->mc_cdev.subled_info = subled; |
| |
| /* init the multicolor's LED class device */ |
| cdev = &priv->mc_cdev.led_cdev; |
| fwnode_property_read_u32(mcnode, "max-brightness", |
| &cdev->max_brightness); |
| cdev->flags = LED_CORE_SUSPENDRESUME; |
| cdev->brightness_set_blocking = led_pwm_mc_set; |
| |
| ret = iterate_subleds(&pdev->dev, priv, mcnode); |
| if (ret) |
| goto release_mcnode; |
| |
| init_data.fwnode = mcnode; |
| ret = devm_led_classdev_multicolor_register_ext(&pdev->dev, |
| &priv->mc_cdev, |
| &init_data); |
| if (ret) { |
| dev_err(&pdev->dev, |
| "failed to register multicolor PWM led for %s: %d\n", |
| cdev->name, ret); |
| goto release_mcnode; |
| } |
| |
| ret = led_pwm_mc_set(cdev, cdev->brightness); |
| if (ret) |
| return dev_err_probe(&pdev->dev, ret, |
| "failed to set led PWM value for %s\n", |
| cdev->name); |
| |
| platform_set_drvdata(pdev, priv); |
| return 0; |
| |
| release_mcnode: |
| fwnode_handle_put(mcnode); |
| return ret; |
| } |
| |
| static const struct of_device_id of_pwm_leds_mc_match[] = { |
| { .compatible = "pwm-leds-multicolor", }, |
| {} |
| }; |
| MODULE_DEVICE_TABLE(of, of_pwm_leds_mc_match); |
| |
| static struct platform_driver led_pwm_mc_driver = { |
| .probe = led_pwm_mc_probe, |
| .driver = { |
| .name = "leds_pwm_multicolor", |
| .of_match_table = of_pwm_leds_mc_match, |
| }, |
| }; |
| module_platform_driver(led_pwm_mc_driver); |
| |
| MODULE_AUTHOR("Sven Schwermer <sven.schwermer@disruptive-technologies.com>"); |
| MODULE_DESCRIPTION("multi-color PWM LED driver"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_ALIAS("platform:leds-pwm-multicolor"); |