-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
pwm: Add clock based PWM output driver
Some systems have clocks exposed to external devices. If the clock controller supports duty-cycle configuration, such clocks can be used as pwm outputs. In fact PWM and CLK subsystems are interfaced with in a similar way and an "opposite" driver already exists (clk-pwm). Add a driver that would enable pwm devices to be used via clk subsystem. Acked-by: Uwe Kleine-König <u.kleine-koenig@pengutronix.de> Signed-off-by: Nikita Travkin <nikita@trvn.ru> Signed-off-by: Thierry Reding <thierry.reding@gmail.com>
- Loading branch information
Nikita Travkin
authored and
Thierry Reding
committed
Jul 29, 2022
1 parent
c2252c1
commit 901f8f5
Showing
3 changed files
with
159 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
// SPDX-License-Identifier: GPL-2.0 | ||
/* | ||
* Clock based PWM controller | ||
* | ||
* Copyright (c) 2021 Nikita Travkin <nikita@trvn.ru> | ||
* | ||
* 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 <nikita@trvn.ru>"); | ||
MODULE_DESCRIPTION("Clock based PWM driver"); | ||
MODULE_LICENSE("GPL"); |