123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148 |
- // SPDX-License-Identifier: GPL-2.0
- /*
- * Clock based PWM controller
- *
- * Copyright (c) 2021 Nikita Travkin <[email protected]>
- *
- * This is an "adapter" driver that allows PWM consumers to use
- * system clocks with duty cycle control as PWM outputs.
- *
- * Limitations:
- * - Due to the fact that exact behavior depends on the underlying
- * clock driver, various limitations are possible.
- * - Underlying clock may not be able to give 0% or 100% duty cycle
- * (constant off or on), exact behavior will depend on the clock.
- * - When the PWM is disabled, the clock will be disabled as well,
- * line state will depend on the clock.
- * - The clk API doesn't expose the necessary calls to implement
- * .get_state().
- */
- #include <linux/kernel.h>
- #include <linux/math64.h>
- #include <linux/err.h>
- #include <linux/module.h>
- #include <linux/of.h>
- #include <linux/platform_device.h>
- #include <linux/clk.h>
- #include <linux/pwm.h>
- struct pwm_clk_chip {
- struct pwm_chip chip;
- struct clk *clk;
- bool clk_enabled;
- };
- #define to_pwm_clk_chip(_chip) container_of(_chip, struct pwm_clk_chip, chip)
- static int pwm_clk_apply(struct pwm_chip *chip, struct pwm_device *pwm,
- const struct pwm_state *state)
- {
- struct pwm_clk_chip *pcchip = to_pwm_clk_chip(chip);
- int ret;
- u32 rate;
- u64 period = state->period;
- u64 duty_cycle = state->duty_cycle;
- if (!state->enabled) {
- if (pwm->state.enabled) {
- clk_disable(pcchip->clk);
- pcchip->clk_enabled = false;
- }
- return 0;
- } else if (!pwm->state.enabled) {
- ret = clk_enable(pcchip->clk);
- if (ret)
- return ret;
- pcchip->clk_enabled = true;
- }
- /*
- * We have to enable the clk before setting the rate and duty_cycle,
- * that however results in a window where the clk is on with a
- * (potentially) different setting. Also setting period and duty_cycle
- * are two separate calls, so that probably isn't atomic either.
- */
- rate = DIV64_U64_ROUND_UP(NSEC_PER_SEC, period);
- ret = clk_set_rate(pcchip->clk, rate);
- if (ret)
- return ret;
- if (state->polarity == PWM_POLARITY_INVERSED)
- duty_cycle = period - duty_cycle;
- return clk_set_duty_cycle(pcchip->clk, duty_cycle, period);
- }
- static const struct pwm_ops pwm_clk_ops = {
- .apply = pwm_clk_apply,
- .owner = THIS_MODULE,
- };
- static int pwm_clk_probe(struct platform_device *pdev)
- {
- struct pwm_clk_chip *pcchip;
- int ret;
- pcchip = devm_kzalloc(&pdev->dev, sizeof(*pcchip), GFP_KERNEL);
- if (!pcchip)
- return -ENOMEM;
- pcchip->clk = devm_clk_get(&pdev->dev, NULL);
- if (IS_ERR(pcchip->clk))
- return dev_err_probe(&pdev->dev, PTR_ERR(pcchip->clk),
- "Failed to get clock\n");
- pcchip->chip.dev = &pdev->dev;
- pcchip->chip.ops = &pwm_clk_ops;
- pcchip->chip.npwm = 1;
- ret = clk_prepare(pcchip->clk);
- if (ret < 0)
- return dev_err_probe(&pdev->dev, ret, "Failed to prepare clock\n");
- ret = pwmchip_add(&pcchip->chip);
- if (ret < 0) {
- clk_unprepare(pcchip->clk);
- return dev_err_probe(&pdev->dev, ret, "Failed to add pwm chip\n");
- }
- platform_set_drvdata(pdev, pcchip);
- return 0;
- }
- static int pwm_clk_remove(struct platform_device *pdev)
- {
- struct pwm_clk_chip *pcchip = platform_get_drvdata(pdev);
- pwmchip_remove(&pcchip->chip);
- if (pcchip->clk_enabled)
- clk_disable(pcchip->clk);
- clk_unprepare(pcchip->clk);
- return 0;
- }
- static const struct of_device_id pwm_clk_dt_ids[] = {
- { .compatible = "clk-pwm", },
- { /* sentinel */ }
- };
- MODULE_DEVICE_TABLE(of, pwm_clk_dt_ids);
- static struct platform_driver pwm_clk_driver = {
- .driver = {
- .name = "pwm-clk",
- .of_match_table = pwm_clk_dt_ids,
- },
- .probe = pwm_clk_probe,
- .remove = pwm_clk_remove,
- };
- module_platform_driver(pwm_clk_driver);
- MODULE_ALIAS("platform:pwm-clk");
- MODULE_AUTHOR("Nikita Travkin <[email protected]>");
- MODULE_DESCRIPTION("Clock based PWM driver");
- MODULE_LICENSE("GPL");
|