forked from Minki/linux
392369019e
When probing with DT, we add each LED one at a time. If we find a LED
without a PWM device (because it is not available yet) we fail the
initialisation, unregister previous LEDs, and then by way of managed
resources, we free the structure.
The problem with this is we may have a scheduled and active work_struct
in this structure, and this results in a nasty kernel oops.
We need to cancel this work_struct properly upon cleanup - and the
cleanup we require is the same cleanup as we do when the LED platform
device is removed. Rather than writing this same code three times,
move it into a separate function and use it in all three places.
Fixes: c971ff185f
("leds: leds-pwm: Defer led_pwm_set() if PWM can sleep")
Signed-off-by: Russell King <rmk+kernel@arm.linux.org.uk>
Signed-off-by: Bryan Wu <cooloney@gmail.com>
247 lines
5.7 KiB
C
247 lines
5.7 KiB
C
/*
|
|
* linux/drivers/leds-pwm.c
|
|
*
|
|
* simple PWM based LED control
|
|
*
|
|
* Copyright 2009 Luotao Fu @ Pengutronix (l.fu@pengutronix.de)
|
|
*
|
|
* based on leds-gpio.c by Raphael Assenat <raph@8d.com>
|
|
*
|
|
* 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.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/of_platform.h>
|
|
#include <linux/fb.h>
|
|
#include <linux/leds.h>
|
|
#include <linux/err.h>
|
|
#include <linux/pwm.h>
|
|
#include <linux/leds_pwm.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/workqueue.h>
|
|
|
|
struct led_pwm_data {
|
|
struct led_classdev cdev;
|
|
struct pwm_device *pwm;
|
|
struct work_struct work;
|
|
unsigned int active_low;
|
|
unsigned int period;
|
|
int duty;
|
|
bool can_sleep;
|
|
};
|
|
|
|
struct led_pwm_priv {
|
|
int num_leds;
|
|
struct led_pwm_data leds[0];
|
|
};
|
|
|
|
static void __led_pwm_set(struct led_pwm_data *led_dat)
|
|
{
|
|
int new_duty = led_dat->duty;
|
|
|
|
pwm_config(led_dat->pwm, new_duty, led_dat->period);
|
|
|
|
if (new_duty == 0)
|
|
pwm_disable(led_dat->pwm);
|
|
else
|
|
pwm_enable(led_dat->pwm);
|
|
}
|
|
|
|
static void led_pwm_work(struct work_struct *work)
|
|
{
|
|
struct led_pwm_data *led_dat =
|
|
container_of(work, struct led_pwm_data, work);
|
|
|
|
__led_pwm_set(led_dat);
|
|
}
|
|
|
|
static void led_pwm_set(struct led_classdev *led_cdev,
|
|
enum led_brightness brightness)
|
|
{
|
|
struct led_pwm_data *led_dat =
|
|
container_of(led_cdev, struct led_pwm_data, cdev);
|
|
unsigned int max = led_dat->cdev.max_brightness;
|
|
unsigned long long duty = led_dat->period;
|
|
|
|
duty *= brightness;
|
|
do_div(duty, max);
|
|
led_dat->duty = duty;
|
|
|
|
if (led_dat->can_sleep)
|
|
schedule_work(&led_dat->work);
|
|
else
|
|
__led_pwm_set(led_dat);
|
|
}
|
|
|
|
static inline size_t sizeof_pwm_leds_priv(int num_leds)
|
|
{
|
|
return sizeof(struct led_pwm_priv) +
|
|
(sizeof(struct led_pwm_data) * num_leds);
|
|
}
|
|
|
|
static void led_pwm_cleanup(struct led_pwm_priv *priv)
|
|
{
|
|
while (priv->num_leds--) {
|
|
led_classdev_unregister(&priv->leds[priv->num_leds].cdev);
|
|
if (priv->leds[priv->num_leds].can_sleep)
|
|
cancel_work_sync(&priv->leds[priv->num_leds].work);
|
|
}
|
|
}
|
|
|
|
static int led_pwm_create_of(struct platform_device *pdev,
|
|
struct led_pwm_priv *priv)
|
|
{
|
|
struct device_node *child;
|
|
int ret;
|
|
|
|
for_each_child_of_node(pdev->dev.of_node, child) {
|
|
struct led_pwm_data *led_dat = &priv->leds[priv->num_leds];
|
|
|
|
led_dat->cdev.name = of_get_property(child, "label",
|
|
NULL) ? : child->name;
|
|
|
|
led_dat->pwm = devm_of_pwm_get(&pdev->dev, child, NULL);
|
|
if (IS_ERR(led_dat->pwm)) {
|
|
dev_err(&pdev->dev, "unable to request PWM for %s\n",
|
|
led_dat->cdev.name);
|
|
ret = PTR_ERR(led_dat->pwm);
|
|
goto err;
|
|
}
|
|
/* Get the period from PWM core when n*/
|
|
led_dat->period = pwm_get_period(led_dat->pwm);
|
|
|
|
led_dat->cdev.default_trigger = of_get_property(child,
|
|
"linux,default-trigger", NULL);
|
|
of_property_read_u32(child, "max-brightness",
|
|
&led_dat->cdev.max_brightness);
|
|
|
|
led_dat->cdev.brightness_set = led_pwm_set;
|
|
led_dat->cdev.brightness = LED_OFF;
|
|
led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME;
|
|
|
|
led_dat->can_sleep = pwm_can_sleep(led_dat->pwm);
|
|
if (led_dat->can_sleep)
|
|
INIT_WORK(&led_dat->work, led_pwm_work);
|
|
|
|
ret = led_classdev_register(&pdev->dev, &led_dat->cdev);
|
|
if (ret < 0) {
|
|
dev_err(&pdev->dev, "failed to register for %s\n",
|
|
led_dat->cdev.name);
|
|
of_node_put(child);
|
|
goto err;
|
|
}
|
|
priv->num_leds++;
|
|
}
|
|
|
|
return 0;
|
|
err:
|
|
led_pwm_cleanup(priv);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int led_pwm_probe(struct platform_device *pdev)
|
|
{
|
|
struct led_pwm_platform_data *pdata = dev_get_platdata(&pdev->dev);
|
|
struct led_pwm_priv *priv;
|
|
int count, i;
|
|
int ret = 0;
|
|
|
|
if (pdata)
|
|
count = pdata->num_leds;
|
|
else
|
|
count = of_get_child_count(pdev->dev.of_node);
|
|
|
|
if (!count)
|
|
return -EINVAL;
|
|
|
|
priv = devm_kzalloc(&pdev->dev, sizeof_pwm_leds_priv(count),
|
|
GFP_KERNEL);
|
|
if (!priv)
|
|
return -ENOMEM;
|
|
|
|
if (pdata) {
|
|
for (i = 0; i < count; i++) {
|
|
struct led_pwm *cur_led = &pdata->leds[i];
|
|
struct led_pwm_data *led_dat = &priv->leds[i];
|
|
|
|
led_dat->pwm = devm_pwm_get(&pdev->dev, cur_led->name);
|
|
if (IS_ERR(led_dat->pwm)) {
|
|
ret = PTR_ERR(led_dat->pwm);
|
|
dev_err(&pdev->dev,
|
|
"unable to request PWM for %s\n",
|
|
cur_led->name);
|
|
goto err;
|
|
}
|
|
|
|
led_dat->cdev.name = cur_led->name;
|
|
led_dat->cdev.default_trigger = cur_led->default_trigger;
|
|
led_dat->active_low = cur_led->active_low;
|
|
led_dat->period = cur_led->pwm_period_ns;
|
|
led_dat->cdev.brightness_set = led_pwm_set;
|
|
led_dat->cdev.brightness = LED_OFF;
|
|
led_dat->cdev.max_brightness = cur_led->max_brightness;
|
|
led_dat->cdev.flags |= LED_CORE_SUSPENDRESUME;
|
|
|
|
led_dat->can_sleep = pwm_can_sleep(led_dat->pwm);
|
|
if (led_dat->can_sleep)
|
|
INIT_WORK(&led_dat->work, led_pwm_work);
|
|
|
|
ret = led_classdev_register(&pdev->dev, &led_dat->cdev);
|
|
if (ret < 0)
|
|
goto err;
|
|
}
|
|
priv->num_leds = count;
|
|
} else {
|
|
ret = led_pwm_create_of(pdev, priv);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
platform_set_drvdata(pdev, priv);
|
|
|
|
return 0;
|
|
|
|
err:
|
|
priv->num_leds = i;
|
|
led_pwm_cleanup(priv);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int led_pwm_remove(struct platform_device *pdev)
|
|
{
|
|
struct led_pwm_priv *priv = platform_get_drvdata(pdev);
|
|
|
|
led_pwm_cleanup(priv);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id of_pwm_leds_match[] = {
|
|
{ .compatible = "pwm-leds", },
|
|
{},
|
|
};
|
|
MODULE_DEVICE_TABLE(of, of_pwm_leds_match);
|
|
|
|
static struct platform_driver led_pwm_driver = {
|
|
.probe = led_pwm_probe,
|
|
.remove = led_pwm_remove,
|
|
.driver = {
|
|
.name = "leds_pwm",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = of_pwm_leds_match,
|
|
},
|
|
};
|
|
|
|
module_platform_driver(led_pwm_driver);
|
|
|
|
MODULE_AUTHOR("Luotao Fu <l.fu@pengutronix.de>");
|
|
MODULE_DESCRIPTION("PWM LED driver for PXA");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_ALIAS("platform:leds-pwm");
|