| /* 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"); |