You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
189 lines
4.7 KiB
189 lines
4.7 KiB
/* Copyright (c) 2019-2020, The Linux Foundation. All rights reserved.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*/
|
|
|
|
#include <linux/clk.h>
|
|
#include <linux/leds.h>
|
|
#include <linux/module.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/platform_device.h>
|
|
|
|
#define PINCTRL_ACTIVE "active"
|
|
#define PINCTRL_SLEEP "sleep"
|
|
#define CLK_DUTY_DEN 100
|
|
|
|
struct led_qcom_clk_priv {
|
|
struct led_classdev cdev;
|
|
struct clk *core;
|
|
struct pinctrl *pinctrl;
|
|
struct pinctrl_state *gpio_active;
|
|
struct pinctrl_state *gpio_sleep;
|
|
const char *name;
|
|
int duty_cycle, max_duty;
|
|
bool clk_enabled;
|
|
};
|
|
|
|
static int qcom_clk_led_set(struct led_classdev *led_cdev,
|
|
enum led_brightness brightness)
|
|
{
|
|
struct led_qcom_clk_priv *led_priv =
|
|
container_of(led_cdev, struct led_qcom_clk_priv, cdev);
|
|
int rc;
|
|
|
|
if (brightness == LED_OFF) {
|
|
if (led_priv->clk_enabled) {
|
|
clk_disable_unprepare(led_priv->core);
|
|
pinctrl_select_state(led_priv->pinctrl,
|
|
led_priv->gpio_sleep);
|
|
led_priv->clk_enabled = false;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (!led_priv->clk_enabled) {
|
|
rc = pinctrl_select_state(led_priv->pinctrl,
|
|
led_priv->gpio_active);
|
|
if (rc < 0) {
|
|
dev_err(led_cdev->dev, "Failed to select pinctrl state rc=%d\n",
|
|
rc);
|
|
return rc;
|
|
}
|
|
|
|
rc = clk_prepare_enable(led_priv->core);
|
|
if (rc < 0) {
|
|
dev_err(led_cdev->dev, "Failed to enable clock rc=%d\n",
|
|
rc);
|
|
goto err_enable;
|
|
}
|
|
|
|
led_priv->clk_enabled = true;
|
|
}
|
|
|
|
/*
|
|
* Duty cycle will be configured based on the brightness.
|
|
* Complete clock period/duty cycle is 100 and
|
|
* brightness is the period in which signal is active.
|
|
*/
|
|
led_priv->duty_cycle = brightness;
|
|
rc = clk_set_duty_cycle(led_priv->core,
|
|
led_priv->duty_cycle, CLK_DUTY_DEN);
|
|
if (rc < 0) {
|
|
dev_err(led_cdev->dev, "Failed to set duty cycle rc=%d\n",
|
|
rc);
|
|
goto err_set_duty;
|
|
}
|
|
|
|
return rc;
|
|
|
|
err_set_duty:
|
|
clk_disable_unprepare(led_priv->core);
|
|
led_priv->clk_enabled = false;
|
|
err_enable:
|
|
pinctrl_select_state(led_priv->pinctrl, led_priv->gpio_sleep);
|
|
|
|
return rc;
|
|
}
|
|
|
|
static int qcom_clk_led_parse_dt(struct device *dev,
|
|
struct led_qcom_clk_priv *led)
|
|
{
|
|
int ret;
|
|
|
|
led->name = of_get_property(dev->of_node, "qcom,label", NULL) ? :
|
|
dev->of_node->name;
|
|
|
|
ret = of_property_read_u32(dev->of_node, "qcom,max_duty",
|
|
&led->max_duty);
|
|
if (ret) {
|
|
dev_err(dev, "Failed to read max_duty\n");
|
|
return ret;
|
|
}
|
|
|
|
if (led->max_duty <= 0) {
|
|
dev_err(dev, "Max duty cycle should not be <= 0\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
led->core = devm_clk_get(dev, "core");
|
|
if (IS_ERR(led->core)) {
|
|
ret = PTR_ERR(led->core);
|
|
if (ret != -EPROBE_DEFER)
|
|
dev_err(dev, "Failed to get core source\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
led->pinctrl = devm_pinctrl_get(dev);
|
|
if (IS_ERR_OR_NULL(led->pinctrl)) {
|
|
dev_err(dev, "No pinctrl config specified!\n");
|
|
return PTR_ERR(led->pinctrl);
|
|
}
|
|
|
|
led->gpio_active = pinctrl_lookup_state(led->pinctrl,
|
|
PINCTRL_ACTIVE);
|
|
if (IS_ERR_OR_NULL(led->gpio_active)) {
|
|
dev_err(dev, "No default config specified!\n");
|
|
return PTR_ERR(led->gpio_active);
|
|
}
|
|
|
|
led->gpio_sleep = pinctrl_lookup_state(led->pinctrl,
|
|
PINCTRL_SLEEP);
|
|
if (IS_ERR_OR_NULL(led->gpio_sleep)) {
|
|
dev_err(dev, "No sleep config specified!\n");
|
|
return PTR_ERR(led->gpio_sleep);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int led_qcom_clk_probe(struct platform_device *pdev)
|
|
{
|
|
struct led_qcom_clk_priv *priv;
|
|
int ret;
|
|
|
|
priv = devm_kzalloc(&pdev->dev, sizeof(struct led_qcom_clk_priv),
|
|
GFP_KERNEL);
|
|
if (!priv)
|
|
return -ENOMEM;
|
|
|
|
ret = qcom_clk_led_parse_dt(&pdev->dev, priv);
|
|
if (ret)
|
|
return ret;
|
|
|
|
priv->cdev.name = priv->name;
|
|
priv->cdev.brightness = LED_OFF;
|
|
priv->cdev.max_brightness = priv->max_duty;
|
|
priv->cdev.flags = LED_CORE_SUSPENDRESUME;
|
|
priv->cdev.brightness_set_blocking = qcom_clk_led_set;
|
|
|
|
return devm_led_classdev_register(&pdev->dev, &priv->cdev);
|
|
}
|
|
|
|
static const struct of_device_id of_qcom_clk_leds_match[] = {
|
|
{ .compatible = "qcom,clk-led-pwm", },
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(of, of_qcom_clk_leds_match);
|
|
|
|
static struct platform_driver led_qcom_clk_driver = {
|
|
.probe = led_qcom_clk_probe,
|
|
.driver = {
|
|
.name = "led_qcom_clk_pwm",
|
|
.of_match_table = of_qcom_clk_leds_match,
|
|
},
|
|
};
|
|
|
|
module_platform_driver(led_qcom_clk_driver);
|
|
|
|
MODULE_DESCRIPTION("Generic clock driven LED driver");
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_ALIAS("platform:leds-qcom-clk");
|
|
|