Commit | Line | Data |
---|---|---|
901f8f54 NT |
1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* | |
3 | * Clock based PWM controller | |
4 | * | |
5 | * Copyright (c) 2021 Nikita Travkin <nikita@trvn.ru> | |
6 | * | |
7 | * This is an "adapter" driver that allows PWM consumers to use | |
8 | * system clocks with duty cycle control as PWM outputs. | |
9 | * | |
10 | * Limitations: | |
11 | * - Due to the fact that exact behavior depends on the underlying | |
12 | * clock driver, various limitations are possible. | |
13 | * - Underlying clock may not be able to give 0% or 100% duty cycle | |
14 | * (constant off or on), exact behavior will depend on the clock. | |
15 | * - When the PWM is disabled, the clock will be disabled as well, | |
16 | * line state will depend on the clock. | |
17 | * - The clk API doesn't expose the necessary calls to implement | |
18 | * .get_state(). | |
19 | */ | |
20 | ||
21 | #include <linux/kernel.h> | |
22 | #include <linux/math64.h> | |
23 | #include <linux/err.h> | |
24 | #include <linux/module.h> | |
25 | #include <linux/of.h> | |
26 | #include <linux/platform_device.h> | |
27 | #include <linux/clk.h> | |
28 | #include <linux/pwm.h> | |
29 | ||
30 | struct pwm_clk_chip { | |
901f8f54 NT |
31 | struct clk *clk; |
32 | bool clk_enabled; | |
33 | }; | |
34 | ||
8e87e3dc UKK |
35 | static inline struct pwm_clk_chip *to_pwm_clk_chip(struct pwm_chip *chip) |
36 | { | |
37 | return pwmchip_get_drvdata(chip); | |
38 | } | |
901f8f54 NT |
39 | |
40 | static int pwm_clk_apply(struct pwm_chip *chip, struct pwm_device *pwm, | |
41 | const struct pwm_state *state) | |
42 | { | |
43 | struct pwm_clk_chip *pcchip = to_pwm_clk_chip(chip); | |
44 | int ret; | |
45 | u32 rate; | |
46 | u64 period = state->period; | |
47 | u64 duty_cycle = state->duty_cycle; | |
48 | ||
49 | if (!state->enabled) { | |
50 | if (pwm->state.enabled) { | |
51 | clk_disable(pcchip->clk); | |
52 | pcchip->clk_enabled = false; | |
53 | } | |
54 | return 0; | |
55 | } else if (!pwm->state.enabled) { | |
56 | ret = clk_enable(pcchip->clk); | |
57 | if (ret) | |
58 | return ret; | |
59 | pcchip->clk_enabled = true; | |
60 | } | |
61 | ||
62 | /* | |
63 | * We have to enable the clk before setting the rate and duty_cycle, | |
64 | * that however results in a window where the clk is on with a | |
65 | * (potentially) different setting. Also setting period and duty_cycle | |
66 | * are two separate calls, so that probably isn't atomic either. | |
67 | */ | |
68 | ||
69 | rate = DIV64_U64_ROUND_UP(NSEC_PER_SEC, period); | |
70 | ret = clk_set_rate(pcchip->clk, rate); | |
71 | if (ret) | |
72 | return ret; | |
73 | ||
74 | if (state->polarity == PWM_POLARITY_INVERSED) | |
75 | duty_cycle = period - duty_cycle; | |
76 | ||
77 | return clk_set_duty_cycle(pcchip->clk, duty_cycle, period); | |
78 | } | |
79 | ||
80 | static const struct pwm_ops pwm_clk_ops = { | |
81 | .apply = pwm_clk_apply, | |
901f8f54 NT |
82 | }; |
83 | ||
84 | static int pwm_clk_probe(struct platform_device *pdev) | |
85 | { | |
fc6549a9 | 86 | struct pwm_chip *chip; |
901f8f54 NT |
87 | struct pwm_clk_chip *pcchip; |
88 | int ret; | |
89 | ||
8e87e3dc UKK |
90 | chip = devm_pwmchip_alloc(&pdev->dev, 1, sizeof(*pcchip)); |
91 | if (IS_ERR(chip)) | |
92 | return PTR_ERR(chip); | |
93 | pcchip = to_pwm_clk_chip(chip); | |
901f8f54 | 94 | |
2b8e30b1 | 95 | pcchip->clk = devm_clk_get_prepared(&pdev->dev, NULL); |
901f8f54 NT |
96 | if (IS_ERR(pcchip->clk)) |
97 | return dev_err_probe(&pdev->dev, PTR_ERR(pcchip->clk), | |
98 | "Failed to get clock\n"); | |
99 | ||
fc6549a9 | 100 | chip->ops = &pwm_clk_ops; |
901f8f54 | 101 | |
fc6549a9 | 102 | ret = pwmchip_add(chip); |
2b8e30b1 | 103 | if (ret < 0) |
901f8f54 | 104 | return dev_err_probe(&pdev->dev, ret, "Failed to add pwm chip\n"); |
901f8f54 | 105 | |
fc6549a9 | 106 | platform_set_drvdata(pdev, chip); |
901f8f54 NT |
107 | return 0; |
108 | } | |
109 | ||
d5806ac6 | 110 | static void pwm_clk_remove(struct platform_device *pdev) |
901f8f54 | 111 | { |
fc6549a9 UKK |
112 | struct pwm_chip *chip = platform_get_drvdata(pdev); |
113 | struct pwm_clk_chip *pcchip = to_pwm_clk_chip(chip); | |
901f8f54 | 114 | |
fc6549a9 | 115 | pwmchip_remove(chip); |
901f8f54 NT |
116 | |
117 | if (pcchip->clk_enabled) | |
118 | clk_disable(pcchip->clk); | |
901f8f54 NT |
119 | } |
120 | ||
121 | static const struct of_device_id pwm_clk_dt_ids[] = { | |
122 | { .compatible = "clk-pwm", }, | |
123 | { /* sentinel */ } | |
124 | }; | |
125 | MODULE_DEVICE_TABLE(of, pwm_clk_dt_ids); | |
126 | ||
127 | static struct platform_driver pwm_clk_driver = { | |
128 | .driver = { | |
129 | .name = "pwm-clk", | |
130 | .of_match_table = pwm_clk_dt_ids, | |
131 | }, | |
132 | .probe = pwm_clk_probe, | |
d5806ac6 | 133 | .remove_new = pwm_clk_remove, |
901f8f54 NT |
134 | }; |
135 | module_platform_driver(pwm_clk_driver); | |
136 | ||
137 | MODULE_ALIAS("platform:pwm-clk"); | |
138 | MODULE_AUTHOR("Nikita Travkin <nikita@trvn.ru>"); | |
139 | MODULE_DESCRIPTION("Clock based PWM driver"); | |
140 | MODULE_LICENSE("GPL"); |