/* * LED driver for WM8350 driven LEDS. * * Copyright(C) 2007, 2008 Wolfson Microelectronics PLC. * * 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/kernel.h> #include <linux/init.h> #include <linux/platform_device.h> #include <linux/leds.h> #include <linux/err.h> #include <linux/mfd/wm8350/pmic.h> #include <linux/regulator/consumer.h> /* Microamps */ static const int isink_cur[] = { 4, 5, 6, 7, 8, 10, 11, 14, 16, 19, 23, 27, 32, 39, 46, 54, 65, 77, 92, 109, 130, 154, 183, 218, 259, 308, 367, 436, 518, 616, 733, 872, 1037, 1233, 1466, 1744, 2073, 2466, 2933, 3487, 4147, 4932, 5865, 6975, 8294, 9864, 11730, 13949, 16589, 19728, 23460, 27899, 33178, 39455, 46920, 55798, 66355, 78910, 93840, 111596, 132710, 157820, 187681, 223191 }; #define to_wm8350_led(led_cdev) \ container_of(led_cdev, struct wm8350_led, cdev) static void wm8350_led_enable(struct wm8350_led *led) { int ret; if (led->enabled) return; ret = regulator_enable(led->isink); if (ret != 0) { dev_err(led->cdev.dev, "Failed to enable ISINK: %d\n", ret); return; } ret = regulator_enable(led->dcdc); if (ret != 0) { dev_err(led->cdev.dev, "Failed to enable DCDC: %d\n", ret); regulator_disable(led->isink); return; } led->enabled = 1; } static void wm8350_led_disable(struct wm8350_led *led) { int ret; if (!led->enabled) return; ret = regulator_disable(led->dcdc); if (ret != 0) { dev_err(led->cdev.dev, "Failed to disable DCDC: %d\n", ret); return; } ret = regulator_disable(led->isink); if (ret != 0) { dev_err(led->cdev.dev, "Failed to disable ISINK: %d\n", ret); regulator_enable(led->dcdc); return; } led->enabled = 0; } static void led_work(struct work_struct *work) { struct wm8350_led *led = container_of(work, struct wm8350_led, work); int ret; int uA; unsigned long flags; mutex_lock(&led->mutex); spin_lock_irqsave(&led->value_lock, flags); if (led->value == LED_OFF) { spin_unlock_irqrestore(&led->value_lock, flags); wm8350_led_disable(led); goto out; } /* This scales linearly into the index of valid current * settings which results in a linear scaling of perceived * brightness due to the non-linear current settings provided * by the hardware. */ uA = (led->max_uA_index * led->value) / LED_FULL; spin_unlock_irqrestore(&led->value_lock, flags); BUG_ON(uA >= ARRAY_SIZE(isink_cur)); ret = regulator_set_current_limit(led->isink, isink_cur[uA], isink_cur[uA]); if (ret != 0) dev_err(led->cdev.dev, "Failed to set %duA: %d\n", isink_cur[uA], ret); wm8350_led_enable(led); out: mutex_unlock(&led->mutex); } static void wm8350_led_set(struct led_classdev *led_cdev, enum led_brightness value) { struct wm8350_led *led = to_wm8350_led(led_cdev); unsigned long flags; spin_lock_irqsave(&led->value_lock, flags); led->value = value; schedule_work(&led->work); spin_unlock_irqrestore(&led->value_lock, flags); } static void wm8350_led_shutdown(struct platform_device *pdev) { struct wm8350_led *led = platform_get_drvdata(pdev); mutex_lock(&led->mutex); led->value = LED_OFF; wm8350_led_disable(led); mutex_unlock(&led->mutex); } static int wm8350_led_probe(struct platform_device *pdev) { struct regulator *isink, *dcdc; struct wm8350_led *led; struct wm8350_led_platform_data *pdata = pdev->dev.platform_data; int ret, i; if (pdata == NULL) { dev_err(&pdev->dev, "no platform data\n"); return -ENODEV; } if (pdata->max_uA < isink_cur[0]) { dev_err(&pdev->dev, "Invalid maximum current %duA\n", pdata->max_uA); return -EINVAL; } isink = regulator_get(&pdev->dev, "led_isink"); if (IS_ERR(isink)) { printk(KERN_ERR "%s: cant get ISINK\n", __func__); return PTR_ERR(isink); } dcdc = regulator_get(&pdev->dev, "led_vcc"); if (IS_ERR(dcdc)) { printk(KERN_ERR "%s: cant get DCDC\n", __func__); ret = PTR_ERR(dcdc); goto err_isink; } led = kzalloc(sizeof(*led), GFP_KERNEL); if (led == NULL) { ret = -ENOMEM; goto err_dcdc; } led->cdev.brightness_set = wm8350_led_set; led->cdev.default_trigger = pdata->default_trigger; led->cdev.name = pdata->name; led->cdev.flags |= LED_CORE_SUSPENDRESUME; led->enabled = regulator_is_enabled(isink); led->isink = isink; led->dcdc = dcdc; for (i = 0; i < ARRAY_SIZE(isink_cur) - 1; i++) if (isink_cur[i] >= pdata->max_uA) break; led->max_uA_index = i; if (pdata->max_uA != isink_cur[i]) dev_warn(&pdev->dev, "Maximum current %duA is not directly supported," " check platform data\n", pdata->max_uA); spin_lock_init(&led->value_lock); mutex_init(&led->mutex); INIT_WORK(&led->work, led_work); led->value = LED_OFF; platform_set_drvdata(pdev, led); ret = led_classdev_register(&pdev->dev, &led->cdev); if (ret < 0) goto err_led; return 0; err_led: kfree(led); err_dcdc: regulator_put(dcdc); err_isink: regulator_put(isink); return ret; } static int wm8350_led_remove(struct platform_device *pdev) { struct wm8350_led *led = platform_get_drvdata(pdev); led_classdev_unregister(&led->cdev); flush_scheduled_work(); wm8350_led_disable(led); regulator_put(led->dcdc); regulator_put(led->isink); kfree(led); return 0; } static struct platform_driver wm8350_led_driver = { .driver = { .name = "wm8350-led", .owner = THIS_MODULE, }, .probe = wm8350_led_probe, .remove = wm8350_led_remove, .shutdown = wm8350_led_shutdown, }; static int __devinit wm8350_led_init(void) { return platform_driver_register(&wm8350_led_driver); } module_init(wm8350_led_init); static void wm8350_led_exit(void) { platform_driver_unregister(&wm8350_led_driver); } module_exit(wm8350_led_exit); MODULE_AUTHOR("Mark Brown"); MODULE_DESCRIPTION("WM8350 LED driver"); MODULE_LICENSE("GPL"); MODULE_ALIAS("platform:wm8350-led");