diff options
author | Boris Brezillon <boris.brezillon@free-electrons.com> | 2014-10-07 09:38:14 -0400 |
---|---|---|
committer | Thierry Reding <thierry.reding@gmail.com> | 2014-11-17 06:20:17 -0500 |
commit | 2b4984bef47a5920bb6ed7f5ede90d8302fee554 (patch) | |
tree | 07319c788ca2bee51ddb822be46e586dad32c746 /drivers/pwm | |
parent | e5a06dc5ac1f686d11b11488a88a63ab12e079cb (diff) |
pwm: add support for atmel-hlcdc-pwm device
The HLCDC IP available in some Atmel SoCs (i.e. at91sam9x5, at91sam9n12
or sama5d3 families for instance) provides a PWM device.
This driver add support for a PWM chip exposing a single PWM device (which
will most likely be used to drive a backlight device).
Signed-off-by: Boris Brezillon <boris.brezillon@free-electrons.com>
Tested-by: Anthony Harivel <anthony.harivel@emtrion.de>
Tested-by: Ludovic Desroches <ludovic.desroches@atmel.com>
Acked-by: Thierry Reding <thierry.reding@gmail.com>
Acked-by: Nicolas Ferre <nicolas.ferre@atmel.com>
Signed-off-by: Thierry Reding <thierry.reding@gmail.com>
Diffstat (limited to 'drivers/pwm')
-rw-r--r-- | drivers/pwm/Kconfig | 11 | ||||
-rw-r--r-- | drivers/pwm/Makefile | 1 | ||||
-rw-r--r-- | drivers/pwm/pwm-atmel-hlcdc.c | 259 |
3 files changed, 271 insertions, 0 deletions
diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig index ddabe3983549..847a57d2cf1c 100644 --- a/drivers/pwm/Kconfig +++ b/drivers/pwm/Kconfig | |||
@@ -50,6 +50,17 @@ config PWM_ATMEL | |||
50 | To compile this driver as a module, choose M here: the module | 50 | To compile this driver as a module, choose M here: the module |
51 | will be called pwm-atmel. | 51 | will be called pwm-atmel. |
52 | 52 | ||
53 | config PWM_ATMEL_HLCDC_PWM | ||
54 | tristate "Atmel HLCDC PWM support" | ||
55 | depends on MFD_ATMEL_HLCDC | ||
56 | help | ||
57 | Generic PWM framework driver for the PWM output of the HLCDC | ||
58 | (Atmel High-end LCD Controller). This PWM output is mainly used | ||
59 | to control the LCD backlight. | ||
60 | |||
61 | To compile this driver as a module, choose M here: the module | ||
62 | will be called pwm-atmel-hlcdc. | ||
63 | |||
53 | config PWM_ATMEL_TCB | 64 | config PWM_ATMEL_TCB |
54 | tristate "Atmel TC Block PWM support" | 65 | tristate "Atmel TC Block PWM support" |
55 | depends on ATMEL_TCLIB && OF | 66 | depends on ATMEL_TCLIB && OF |
diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile index 88be33bbfdf6..65259ac1e8de 100644 --- a/drivers/pwm/Makefile +++ b/drivers/pwm/Makefile | |||
@@ -2,6 +2,7 @@ obj-$(CONFIG_PWM) += core.o | |||
2 | obj-$(CONFIG_PWM_SYSFS) += sysfs.o | 2 | obj-$(CONFIG_PWM_SYSFS) += sysfs.o |
3 | obj-$(CONFIG_PWM_AB8500) += pwm-ab8500.o | 3 | obj-$(CONFIG_PWM_AB8500) += pwm-ab8500.o |
4 | obj-$(CONFIG_PWM_ATMEL) += pwm-atmel.o | 4 | obj-$(CONFIG_PWM_ATMEL) += pwm-atmel.o |
5 | obj-$(CONFIG_PWM_ATMEL_HLCDC_PWM) += pwm-atmel-hlcdc.o | ||
5 | obj-$(CONFIG_PWM_ATMEL_TCB) += pwm-atmel-tcb.o | 6 | obj-$(CONFIG_PWM_ATMEL_TCB) += pwm-atmel-tcb.o |
6 | obj-$(CONFIG_PWM_BCM_KONA) += pwm-bcm-kona.o | 7 | obj-$(CONFIG_PWM_BCM_KONA) += pwm-bcm-kona.o |
7 | obj-$(CONFIG_PWM_BCM2835) += pwm-bcm2835.o | 8 | obj-$(CONFIG_PWM_BCM2835) += pwm-bcm2835.o |
diff --git a/drivers/pwm/pwm-atmel-hlcdc.c b/drivers/pwm/pwm-atmel-hlcdc.c new file mode 100644 index 000000000000..eaf8b12ce1e5 --- /dev/null +++ b/drivers/pwm/pwm-atmel-hlcdc.c | |||
@@ -0,0 +1,259 @@ | |||
1 | /* | ||
2 | * Copyright (C) 2014 Free Electrons | ||
3 | * Copyright (C) 2014 Atmel | ||
4 | * | ||
5 | * Author: Boris BREZILLON <boris.brezillon@free-electrons.com> | ||
6 | * | ||
7 | * This program is free software; you can redistribute it and/or modify it | ||
8 | * under the terms of the GNU General Public License version 2 as published by | ||
9 | * the Free Software Foundation. | ||
10 | * | ||
11 | * This program is distributed in the hope that it will be useful, but WITHOUT | ||
12 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | ||
13 | * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for | ||
14 | * more details. | ||
15 | * | ||
16 | * You should have received a copy of the GNU General Public License along with | ||
17 | * this program. If not, see <http://www.gnu.org/licenses/>. | ||
18 | */ | ||
19 | |||
20 | #include <linux/clk.h> | ||
21 | #include <linux/delay.h> | ||
22 | #include <linux/mfd/atmel-hlcdc.h> | ||
23 | #include <linux/module.h> | ||
24 | #include <linux/platform_device.h> | ||
25 | #include <linux/pwm.h> | ||
26 | #include <linux/regmap.h> | ||
27 | |||
28 | #define ATMEL_HLCDC_PWMCVAL_MASK GENMASK(15, 8) | ||
29 | #define ATMEL_HLCDC_PWMCVAL(x) (((x) << 8) & ATMEL_HLCDC_PWMCVAL_MASK) | ||
30 | #define ATMEL_HLCDC_PWMPOL BIT(4) | ||
31 | #define ATMEL_HLCDC_PWMPS_MASK GENMASK(2, 0) | ||
32 | #define ATMEL_HLCDC_PWMPS_MAX 0x6 | ||
33 | #define ATMEL_HLCDC_PWMPS(x) ((x) & ATMEL_HLCDC_PWMPS_MASK) | ||
34 | |||
35 | struct atmel_hlcdc_pwm { | ||
36 | struct pwm_chip chip; | ||
37 | struct atmel_hlcdc *hlcdc; | ||
38 | struct clk *cur_clk; | ||
39 | }; | ||
40 | |||
41 | static inline struct atmel_hlcdc_pwm *to_atmel_hlcdc_pwm(struct pwm_chip *chip) | ||
42 | { | ||
43 | return container_of(chip, struct atmel_hlcdc_pwm, chip); | ||
44 | } | ||
45 | |||
46 | static int atmel_hlcdc_pwm_config(struct pwm_chip *c, | ||
47 | struct pwm_device *pwm, | ||
48 | int duty_ns, int period_ns) | ||
49 | { | ||
50 | struct atmel_hlcdc_pwm *chip = to_atmel_hlcdc_pwm(c); | ||
51 | struct atmel_hlcdc *hlcdc = chip->hlcdc; | ||
52 | struct clk *new_clk = hlcdc->slow_clk; | ||
53 | u64 pwmcval = duty_ns * 256; | ||
54 | unsigned long clk_freq; | ||
55 | u64 clk_period_ns; | ||
56 | u32 pwmcfg; | ||
57 | int pres; | ||
58 | |||
59 | clk_freq = clk_get_rate(new_clk); | ||
60 | clk_period_ns = (u64)NSEC_PER_SEC * 256; | ||
61 | do_div(clk_period_ns, clk_freq); | ||
62 | |||
63 | if (clk_period_ns > period_ns) { | ||
64 | new_clk = hlcdc->sys_clk; | ||
65 | clk_freq = clk_get_rate(new_clk); | ||
66 | clk_period_ns = (u64)NSEC_PER_SEC * 256; | ||
67 | do_div(clk_period_ns, clk_freq); | ||
68 | } | ||
69 | |||
70 | for (pres = 0; pres <= ATMEL_HLCDC_PWMPS_MAX; pres++) | ||
71 | if ((clk_period_ns << pres) >= period_ns) | ||
72 | break; | ||
73 | |||
74 | if (pres > ATMEL_HLCDC_PWMPS_MAX) | ||
75 | return -EINVAL; | ||
76 | |||
77 | pwmcfg = ATMEL_HLCDC_PWMPS(pres); | ||
78 | |||
79 | if (new_clk != chip->cur_clk) { | ||
80 | u32 gencfg = 0; | ||
81 | int ret; | ||
82 | |||
83 | ret = clk_prepare_enable(new_clk); | ||
84 | if (ret) | ||
85 | return ret; | ||
86 | |||
87 | clk_disable_unprepare(chip->cur_clk); | ||
88 | chip->cur_clk = new_clk; | ||
89 | |||
90 | if (new_clk == hlcdc->sys_clk) | ||
91 | gencfg = ATMEL_HLCDC_CLKPWMSEL; | ||
92 | |||
93 | ret = regmap_update_bits(hlcdc->regmap, ATMEL_HLCDC_CFG(0), | ||
94 | ATMEL_HLCDC_CLKPWMSEL, gencfg); | ||
95 | if (ret) | ||
96 | return ret; | ||
97 | } | ||
98 | |||
99 | do_div(pwmcval, period_ns); | ||
100 | |||
101 | /* | ||
102 | * The PWM duty cycle is configurable from 0/256 to 255/256 of the | ||
103 | * period cycle. Hence we can't set a duty cycle occupying the | ||
104 | * whole period cycle if we're asked to. | ||
105 | * Set it to 255 if pwmcval is greater than 256. | ||
106 | */ | ||
107 | if (pwmcval > 255) | ||
108 | pwmcval = 255; | ||
109 | |||
110 | pwmcfg |= ATMEL_HLCDC_PWMCVAL(pwmcval); | ||
111 | |||
112 | return regmap_update_bits(hlcdc->regmap, ATMEL_HLCDC_CFG(6), | ||
113 | ATMEL_HLCDC_PWMCVAL_MASK | | ||
114 | ATMEL_HLCDC_PWMPS_MASK, | ||
115 | pwmcfg); | ||
116 | } | ||
117 | |||
118 | static int atmel_hlcdc_pwm_set_polarity(struct pwm_chip *c, | ||
119 | struct pwm_device *pwm, | ||
120 | enum pwm_polarity polarity) | ||
121 | { | ||
122 | struct atmel_hlcdc_pwm *chip = to_atmel_hlcdc_pwm(c); | ||
123 | struct atmel_hlcdc *hlcdc = chip->hlcdc; | ||
124 | u32 cfg = 0; | ||
125 | |||
126 | if (polarity == PWM_POLARITY_NORMAL) | ||
127 | cfg = ATMEL_HLCDC_PWMPOL; | ||
128 | |||
129 | return regmap_update_bits(hlcdc->regmap, ATMEL_HLCDC_CFG(6), | ||
130 | ATMEL_HLCDC_PWMPOL, cfg); | ||
131 | } | ||
132 | |||
133 | static int atmel_hlcdc_pwm_enable(struct pwm_chip *c, struct pwm_device *pwm) | ||
134 | { | ||
135 | struct atmel_hlcdc_pwm *chip = to_atmel_hlcdc_pwm(c); | ||
136 | struct atmel_hlcdc *hlcdc = chip->hlcdc; | ||
137 | u32 status; | ||
138 | int ret; | ||
139 | |||
140 | ret = regmap_write(hlcdc->regmap, ATMEL_HLCDC_EN, ATMEL_HLCDC_PWM); | ||
141 | if (ret) | ||
142 | return ret; | ||
143 | |||
144 | while (true) { | ||
145 | ret = regmap_read(hlcdc->regmap, ATMEL_HLCDC_SR, &status); | ||
146 | if (ret) | ||
147 | return ret; | ||
148 | |||
149 | if ((status & ATMEL_HLCDC_PWM) != 0) | ||
150 | break; | ||
151 | |||
152 | usleep_range(1, 10); | ||
153 | } | ||
154 | |||
155 | return 0; | ||
156 | } | ||
157 | |||
158 | static void atmel_hlcdc_pwm_disable(struct pwm_chip *c, | ||
159 | struct pwm_device *pwm) | ||
160 | { | ||
161 | struct atmel_hlcdc_pwm *chip = to_atmel_hlcdc_pwm(c); | ||
162 | struct atmel_hlcdc *hlcdc = chip->hlcdc; | ||
163 | u32 status; | ||
164 | int ret; | ||
165 | |||
166 | ret = regmap_write(hlcdc->regmap, ATMEL_HLCDC_DIS, ATMEL_HLCDC_PWM); | ||
167 | if (ret) | ||
168 | return; | ||
169 | |||
170 | while (true) { | ||
171 | ret = regmap_read(hlcdc->regmap, ATMEL_HLCDC_SR, &status); | ||
172 | if (ret) | ||
173 | return; | ||
174 | |||
175 | if ((status & ATMEL_HLCDC_PWM) == 0) | ||
176 | break; | ||
177 | |||
178 | usleep_range(1, 10); | ||
179 | } | ||
180 | } | ||
181 | |||
182 | static const struct pwm_ops atmel_hlcdc_pwm_ops = { | ||
183 | .config = atmel_hlcdc_pwm_config, | ||
184 | .set_polarity = atmel_hlcdc_pwm_set_polarity, | ||
185 | .enable = atmel_hlcdc_pwm_enable, | ||
186 | .disable = atmel_hlcdc_pwm_disable, | ||
187 | .owner = THIS_MODULE, | ||
188 | }; | ||
189 | |||
190 | static int atmel_hlcdc_pwm_probe(struct platform_device *pdev) | ||
191 | { | ||
192 | struct device *dev = &pdev->dev; | ||
193 | struct atmel_hlcdc_pwm *chip; | ||
194 | struct atmel_hlcdc *hlcdc; | ||
195 | int ret; | ||
196 | |||
197 | hlcdc = dev_get_drvdata(dev->parent); | ||
198 | |||
199 | chip = devm_kzalloc(dev, sizeof(*chip), GFP_KERNEL); | ||
200 | if (!chip) | ||
201 | return -ENOMEM; | ||
202 | |||
203 | ret = clk_prepare_enable(hlcdc->periph_clk); | ||
204 | if (ret) | ||
205 | return ret; | ||
206 | |||
207 | chip->hlcdc = hlcdc; | ||
208 | chip->chip.ops = &atmel_hlcdc_pwm_ops; | ||
209 | chip->chip.dev = dev; | ||
210 | chip->chip.base = -1; | ||
211 | chip->chip.npwm = 1; | ||
212 | chip->chip.of_xlate = of_pwm_xlate_with_flags; | ||
213 | chip->chip.of_pwm_n_cells = 3; | ||
214 | chip->chip.can_sleep = 1; | ||
215 | |||
216 | ret = pwmchip_add(&chip->chip); | ||
217 | if (ret) { | ||
218 | clk_disable_unprepare(hlcdc->periph_clk); | ||
219 | return ret; | ||
220 | } | ||
221 | |||
222 | platform_set_drvdata(pdev, chip); | ||
223 | |||
224 | return 0; | ||
225 | } | ||
226 | |||
227 | static int atmel_hlcdc_pwm_remove(struct platform_device *pdev) | ||
228 | { | ||
229 | struct atmel_hlcdc_pwm *chip = platform_get_drvdata(pdev); | ||
230 | int ret; | ||
231 | |||
232 | ret = pwmchip_remove(&chip->chip); | ||
233 | if (ret) | ||
234 | return ret; | ||
235 | |||
236 | clk_disable_unprepare(chip->hlcdc->periph_clk); | ||
237 | |||
238 | return 0; | ||
239 | } | ||
240 | |||
241 | static const struct of_device_id atmel_hlcdc_pwm_dt_ids[] = { | ||
242 | { .compatible = "atmel,hlcdc-pwm" }, | ||
243 | { /* sentinel */ }, | ||
244 | }; | ||
245 | |||
246 | static struct platform_driver atmel_hlcdc_pwm_driver = { | ||
247 | .driver = { | ||
248 | .name = "atmel-hlcdc-pwm", | ||
249 | .of_match_table = atmel_hlcdc_pwm_dt_ids, | ||
250 | }, | ||
251 | .probe = atmel_hlcdc_pwm_probe, | ||
252 | .remove = atmel_hlcdc_pwm_remove, | ||
253 | }; | ||
254 | module_platform_driver(atmel_hlcdc_pwm_driver); | ||
255 | |||
256 | MODULE_ALIAS("platform:atmel-hlcdc-pwm"); | ||
257 | MODULE_AUTHOR("Boris Brezillon <boris.brezillon@free-electrons.com>"); | ||
258 | MODULE_DESCRIPTION("Atmel HLCDC PWM driver"); | ||
259 | MODULE_LICENSE("GPL v2"); | ||