diff mbox series

[v3,2/2] leds: Add PWM multicolor driver

Message ID 20220126104844.246068-3-sven@svenschwermer.de
State Superseded
Headers show
Series [v3,1/2] dt-bindings: leds: Add multicolor PWM LED bindings | expand

Commit Message

Sven Schwermer Jan. 26, 2022, 10:48 a.m. UTC
From: Sven Schwermer <sven.schwermer@disruptive-technologies.com>

By allowing to group multiple monochrome PWM LEDs into multicolor LEDs,
all involved LEDs can be controlled in-sync. This enables using effects
using triggers, etc.

Signed-off-by: Sven Schwermer <sven.schwermer@disruptive-technologies.com>
---

Notes:
    Changes in v3:
    * Release fwnode handles
    * Sort header includes
    * Remove deprecated device tree properties
    * Remove deprecated LED_OFF
    * Remove subled channel assignment
    * s/pwmstate/state/

 drivers/leds/Kconfig               |   8 ++
 drivers/leds/Makefile              |   1 +
 drivers/leds/leds-pwm-multicolor.c | 184 +++++++++++++++++++++++++++++
 3 files changed, 193 insertions(+)
 create mode 100644 drivers/leds/leds-pwm-multicolor.c

Comments

Andy Shevchenko Feb. 2, 2022, 12:33 p.m. UTC | #1
On Wed, Jan 26, 2022 at 11:05 PM <sven@svenschwermer.de> wrote:
>
> From: Sven Schwermer <sven.schwermer@disruptive-technologies.com>
>
> By allowing to group multiple monochrome PWM LEDs into multicolor LEDs,
> all involved LEDs can be controlled in-sync. This enables using effects
> using triggers, etc.

...

> +       help
> +         This option enables support for PWM driven monochrome LEDs that are
> +         grouped into multicolor LEDs.

What would be the module name if compiled as a module?

...

> +#include <linux/err.h>
> +#include <linux/kernel.h>
> +#include <linux/led-class-multicolor.h>
> +#include <linux/leds.h>

mod_devicetable.h

> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/platform_device.h>

property.h

> +#include <linux/pwm.h>

...

> +       int i;
> +       unsigned long long duty;
> +       int ret = 0;
> +       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);

Can we order in reversed xmas tree order?

       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;

Same for other functions.

...

> +               dev_err(&pdev->dev, "expected multi-led node\n");
> +               ret = -ENODEV;
> +               goto out;

return dev_err_probe(dev, -ENODEV, ...);

...

> +       /* count the nodes inside the multi-led node */
> +       fwnode_for_each_child_node(mcnode, fwnode)
> +               ++count;

Postincrement shall work the same way.

...

> +                       ret = PTR_ERR(pwmled->pwm);
> +                       dev_err(&pdev->dev, "unable to request PWM: %d\n", ret);
> +                       fwnode_handle_put(fwnode);
> +                       goto destroy_mutex;

fwnode_handle_put();
return dev_err_probe(...);

...

> +                       dev_err(&pdev->dev, "cannot read color: %d\n", ret);
> +                       fwnode_handle_put(fwnode);
> +                       goto destroy_mutex;

fwnode_handle_put();
return dev_err_probe();

> +               }

...

> +               ++priv->mc_cdev.num_colors;

Postincrement shall work the same way.

> +       }

...

> +               dev_err(&pdev->dev,
> +                       "failed to register multicolor PWM led for %s: %d\n",
> +                       cdev->name, ret);
> +               goto destroy_mutex;

return dev_err_probe();

...

> +               dev_err(&pdev->dev, "failed to set led PWM value for %s: %d",
> +                       cdev->name, ret);
> +               goto destroy_mutex;

return dev_err_probe();

...

> +destroy_mutex:
> +       mutex_destroy(&priv->lock);

Wrong ordering here and in ->remove().

Don't mix devm_* with non-devm_* calls.

> +release_mcnode:
> +       fwnode_handle_put(mcnode);

> +out:
> +       return ret;

Return directly.

> +}
> +
> +static int led_pwm_mc_remove(struct platform_device *pdev)
> +{
> +       struct pwm_mc_led *priv = platform_get_drvdata(pdev);
> +
> +       mutex_destroy(&priv->lock);
> +       return 0;
> +}

...

> +static const struct of_device_id of_pwm_leds_mc_match[] = {
> +       { .compatible = "pwm-leds-multicolor", },

> +       {},

No comma needed for terminator entry.

> +};

...

> +static struct platform_driver led_pwm_mc_driver = {
> +       .probe          = led_pwm_mc_probe,
> +       .remove         = led_pwm_mc_remove,
> +       .driver         = {
> +               .name   = "leds_pwm_multicolor",
> +               .of_match_table = of_pwm_leds_mc_match,
> +       },
> +};

> +

Redundant blank line.

> +module_platform_driver(led_pwm_mc_driver);


--
With Best Regards,
Andy Shevchenko
Sven Schwermer Feb. 6, 2022, 9:17 a.m. UTC | #2
Hi Andy,

Thanks for looking through my patch. I have a couple of follow-up 
questions about your feedback:

On 2/2/22 13:33, Andy Shevchenko wrote:
>> +                       ret = PTR_ERR(pwmled->pwm);
>> +                       dev_err(&pdev->dev, "unable to request PWM: %d\n", ret);
>> +                       fwnode_handle_put(fwnode);
>> +                       goto destroy_mutex;
> 
> fwnode_handle_put();
> return dev_err_probe(...);
This would skip the destruction of the mutex and releasing of mcnode. 
Isn't that problematic? The same goes for all of your comments of this kind.

>> +destroy_mutex:
>> +       mutex_destroy(&priv->lock);
> 
> Wrong ordering here and in ->remove().
> 
> Don't mix devm_* with non-devm_* calls.
What do you mean by this?

Best regards,
Sven
diff mbox series

Patch

diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 6090e647daee..bae1f63f6195 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -552,6 +552,14 @@  config LEDS_PWM
 	help
 	  This option enables support for pwm driven LEDs
 
+config LEDS_PWM_MULTICOLOR
+	tristate "PWM driven multi-color LED Support"
+	depends on LEDS_CLASS_MULTICOLOR
+	depends on PWM
+	help
+	  This option enables support for PWM driven monochrome LEDs that are
+	  grouped into multicolor LEDs.
+
 config LEDS_REGULATOR
 	tristate "REGULATOR driven LED support"
 	depends on LEDS_CLASS
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index e58ecb36360f..ba2c2c1edf12 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -73,6 +73,7 @@  obj-$(CONFIG_LEDS_PCA963X)		+= leds-pca963x.o
 obj-$(CONFIG_LEDS_PM8058)		+= leds-pm8058.o
 obj-$(CONFIG_LEDS_POWERNV)		+= leds-powernv.o
 obj-$(CONFIG_LEDS_PWM)			+= leds-pwm.o
+obj-$(CONFIG_LEDS_PWM_MULTICOLOR)	+= leds-pwm-multicolor.o
 obj-$(CONFIG_LEDS_REGULATOR)		+= leds-regulator.o
 obj-$(CONFIG_LEDS_S3C24XX)		+= leds-s3c24xx.o
 obj-$(CONFIG_LEDS_SC27XX_BLTC)		+= leds-sc27xx-bltc.o
diff --git a/drivers/leds/leds-pwm-multicolor.c b/drivers/leds/leds-pwm-multicolor.c
new file mode 100644
index 000000000000..bc4d21ddd74a
--- /dev/null
+++ b/drivers/leds/leds-pwm-multicolor.c
@@ -0,0 +1,184 @@ 
+// 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/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+
+struct pwm_led {
+	struct pwm_device *pwm;
+	struct pwm_state state;
+};
+
+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)
+{
+	int i;
+	unsigned long long duty;
+	int ret = 0;
+	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);
+
+	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);
+
+		priv->leds[i].state.duty_cycle = duty;
+		priv->leds[i].state.enabled = duty > 0;
+		ret = pwm_apply_state(priv->leds[i].pwm,
+				      &priv->leds[i].state);
+		if (ret)
+			break;
+	}
+
+	mutex_unlock(&priv->lock);
+
+	return ret;
+}
+
+static int led_pwm_mc_probe(struct platform_device *pdev)
+{
+	struct fwnode_handle *mcnode, *fwnode;
+	int count = 0;
+	struct pwm_mc_led *priv;
+	struct mc_subled *subled;
+	struct led_classdev *cdev;
+	struct pwm_led *pwmled;
+	u32 color;
+	int ret = 0;
+	struct led_init_data init_data = {};
+
+	mcnode = device_get_named_child_node(&pdev->dev, "multi-led");
+	if (!mcnode) {
+		dev_err(&pdev->dev, "expected multi-led node\n");
+		ret = -ENODEV;
+		goto out;
+	}
+
+	/* 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 destroy_mutex;
+	}
+	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;
+
+	/* 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(&pdev->dev, fwnode, NULL);
+		if (IS_ERR(pwmled->pwm)) {
+			ret = PTR_ERR(pwmled->pwm);
+			dev_err(&pdev->dev, "unable to request PWM: %d\n", ret);
+			fwnode_handle_put(fwnode);
+			goto destroy_mutex;
+		}
+		pwm_init_state(pwmled->pwm, &pwmled->state);
+
+		ret = fwnode_property_read_u32(fwnode, "color", &color);
+		if (ret) {
+			dev_err(&pdev->dev, "cannot read color: %d\n", ret);
+			fwnode_handle_put(fwnode);
+			goto destroy_mutex;
+		}
+
+		subled[priv->mc_cdev.num_colors].color_index = color;
+		++priv->mc_cdev.num_colors;
+	}
+
+	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 destroy_mutex;
+	}
+
+	ret = led_pwm_mc_set(cdev, cdev->brightness);
+	if (ret) {
+		dev_err(&pdev->dev, "failed to set led PWM value for %s: %d",
+			cdev->name, ret);
+		goto destroy_mutex;
+	}
+
+	platform_set_drvdata(pdev, priv);
+	return 0;
+
+destroy_mutex:
+	mutex_destroy(&priv->lock);
+release_mcnode:
+	fwnode_handle_put(mcnode);
+out:
+	return ret;
+}
+
+static int led_pwm_mc_remove(struct platform_device *pdev)
+{
+	struct pwm_mc_led *priv = platform_get_drvdata(pdev);
+
+	mutex_destroy(&priv->lock);
+	return 0;
+}
+
+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,
+	.remove		= led_pwm_mc_remove,
+	.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");