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 { |
31 | struct clk *clk; |
32 | bool clk_enabled; |
33 | }; |
34 | |
35 | static inline struct pwm_clk_chip *to_pwm_clk_chip(struct pwm_chip *chip) |
36 | { |
37 | return pwmchip_get_drvdata(chip); |
38 | } |
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(clk: pcchip->clk); |
52 | pcchip->clk_enabled = false; |
53 | } |
54 | return 0; |
55 | } else if (!pwm->state.enabled) { |
56 | ret = clk_enable(clk: 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(clk: 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(clk: pcchip->clk, num: duty_cycle, den: period); |
78 | } |
79 | |
80 | static const struct pwm_ops pwm_clk_ops = { |
81 | .apply = pwm_clk_apply, |
82 | }; |
83 | |
84 | static int pwm_clk_probe(struct platform_device *pdev) |
85 | { |
86 | struct pwm_chip *chip; |
87 | struct pwm_clk_chip *pcchip; |
88 | int ret; |
89 | |
90 | chip = devm_pwmchip_alloc(parent: &pdev->dev, npwm: 1, sizeof_priv: sizeof(*pcchip)); |
91 | if (IS_ERR(ptr: chip)) |
92 | return PTR_ERR(ptr: chip); |
93 | pcchip = to_pwm_clk_chip(chip); |
94 | |
95 | pcchip->clk = devm_clk_get_prepared(dev: &pdev->dev, NULL); |
96 | if (IS_ERR(ptr: pcchip->clk)) |
97 | return dev_err_probe(dev: &pdev->dev, err: PTR_ERR(ptr: pcchip->clk), |
98 | fmt: "Failed to get clock\n" ); |
99 | |
100 | chip->ops = &pwm_clk_ops; |
101 | |
102 | ret = pwmchip_add(chip); |
103 | if (ret < 0) |
104 | return dev_err_probe(dev: &pdev->dev, err: ret, fmt: "Failed to add pwm chip\n" ); |
105 | |
106 | platform_set_drvdata(pdev, data: chip); |
107 | return 0; |
108 | } |
109 | |
110 | static void pwm_clk_remove(struct platform_device *pdev) |
111 | { |
112 | struct pwm_chip *chip = platform_get_drvdata(pdev); |
113 | struct pwm_clk_chip *pcchip = to_pwm_clk_chip(chip); |
114 | |
115 | pwmchip_remove(chip); |
116 | |
117 | if (pcchip->clk_enabled) |
118 | clk_disable(clk: pcchip->clk); |
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, |
133 | .remove_new = pwm_clk_remove, |
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" ); |
141 | |