Commit | Line | Data |
---|---|---|
a636cd6c | 1 | // SPDX-License-Identifier: GPL-2.0-or-later |
aae03dc9 OR |
2 | /* |
3 | * Watchdog driver for Alphascale ASM9260. | |
4 | * | |
5 | * Copyright (c) 2014 Oleksij Rempel <linux@rempel-privat.de> | |
aae03dc9 OR |
6 | */ |
7 | ||
8 | #include <linux/bitops.h> | |
9 | #include <linux/clk.h> | |
10 | #include <linux/delay.h> | |
11 | #include <linux/interrupt.h> | |
12 | #include <linux/io.h> | |
13 | #include <linux/module.h> | |
14 | #include <linux/of.h> | |
15 | #include <linux/platform_device.h> | |
aae03dc9 OR |
16 | #include <linux/reset.h> |
17 | #include <linux/watchdog.h> | |
18 | ||
19 | #define CLOCK_FREQ 1000000 | |
20 | ||
21 | /* Watchdog Mode register */ | |
22 | #define HW_WDMOD 0x00 | |
23 | /* Wake interrupt. Set by HW, can't be cleared. */ | |
24 | #define BM_MOD_WDINT BIT(3) | |
25 | /* This bit set if timeout reached. Cleared by SW. */ | |
26 | #define BM_MOD_WDTOF BIT(2) | |
27 | /* HW Reset on timeout */ | |
28 | #define BM_MOD_WDRESET BIT(1) | |
29 | /* WD enable */ | |
30 | #define BM_MOD_WDEN BIT(0) | |
31 | ||
32 | /* | |
33 | * Watchdog Timer Constant register | |
34 | * Minimal value is 0xff, the meaning of this value | |
35 | * depends on used clock: T = WDCLK * (0xff + 1) * 4 | |
36 | */ | |
37 | #define HW_WDTC 0x04 | |
38 | #define BM_WDTC_MAX(freq) (0x7fffffff / (freq)) | |
39 | ||
40 | /* Watchdog Feed register */ | |
41 | #define HW_WDFEED 0x08 | |
42 | ||
43 | /* Watchdog Timer Value register */ | |
44 | #define HW_WDTV 0x0c | |
45 | ||
46 | #define ASM9260_WDT_DEFAULT_TIMEOUT 30 | |
47 | ||
48 | enum asm9260_wdt_mode { | |
49 | HW_RESET, | |
50 | SW_RESET, | |
51 | DEBUG, | |
52 | }; | |
53 | ||
54 | struct asm9260_wdt_priv { | |
55 | struct device *dev; | |
56 | struct watchdog_device wdd; | |
57 | struct clk *clk; | |
58 | struct clk *clk_ahb; | |
59 | struct reset_control *rst; | |
aae03dc9 OR |
60 | |
61 | void __iomem *iobase; | |
62 | int irq; | |
63 | unsigned long wdt_freq; | |
64 | enum asm9260_wdt_mode mode; | |
65 | }; | |
66 | ||
67 | static int asm9260_wdt_feed(struct watchdog_device *wdd) | |
68 | { | |
69 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
70 | ||
71 | iowrite32(0xaa, priv->iobase + HW_WDFEED); | |
72 | iowrite32(0x55, priv->iobase + HW_WDFEED); | |
73 | ||
74 | return 0; | |
75 | } | |
76 | ||
77 | static unsigned int asm9260_wdt_gettimeleft(struct watchdog_device *wdd) | |
78 | { | |
79 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
80 | u32 counter; | |
81 | ||
82 | counter = ioread32(priv->iobase + HW_WDTV); | |
83 | ||
d94fa465 | 84 | return counter / priv->wdt_freq; |
aae03dc9 OR |
85 | } |
86 | ||
87 | static int asm9260_wdt_updatetimeout(struct watchdog_device *wdd) | |
88 | { | |
89 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
90 | u32 counter; | |
91 | ||
92 | counter = wdd->timeout * priv->wdt_freq; | |
93 | ||
94 | iowrite32(counter, priv->iobase + HW_WDTC); | |
95 | ||
96 | return 0; | |
97 | } | |
98 | ||
99 | static int asm9260_wdt_enable(struct watchdog_device *wdd) | |
100 | { | |
101 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
102 | u32 mode = 0; | |
103 | ||
104 | if (priv->mode == HW_RESET) | |
105 | mode = BM_MOD_WDRESET; | |
106 | ||
107 | iowrite32(BM_MOD_WDEN | mode, priv->iobase + HW_WDMOD); | |
108 | ||
109 | asm9260_wdt_updatetimeout(wdd); | |
110 | ||
111 | asm9260_wdt_feed(wdd); | |
112 | ||
113 | return 0; | |
114 | } | |
115 | ||
116 | static int asm9260_wdt_disable(struct watchdog_device *wdd) | |
117 | { | |
118 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); | |
119 | ||
120 | /* The only way to disable WD is to reset it. */ | |
121 | reset_control_assert(priv->rst); | |
122 | reset_control_deassert(priv->rst); | |
123 | ||
124 | return 0; | |
125 | } | |
126 | ||
127 | static int asm9260_wdt_settimeout(struct watchdog_device *wdd, unsigned int to) | |
128 | { | |
129 | wdd->timeout = to; | |
130 | asm9260_wdt_updatetimeout(wdd); | |
131 | ||
132 | return 0; | |
133 | } | |
134 | ||
135 | static void asm9260_wdt_sys_reset(struct asm9260_wdt_priv *priv) | |
136 | { | |
137 | /* init WD if it was not started */ | |
138 | ||
139 | iowrite32(BM_MOD_WDEN | BM_MOD_WDRESET, priv->iobase + HW_WDMOD); | |
140 | ||
141 | iowrite32(0xff, priv->iobase + HW_WDTC); | |
142 | /* first pass correct sequence */ | |
143 | asm9260_wdt_feed(&priv->wdd); | |
144 | /* | |
145 | * Then write wrong pattern to the feed to trigger reset | |
146 | * ASAP. | |
147 | */ | |
148 | iowrite32(0xff, priv->iobase + HW_WDFEED); | |
149 | ||
150 | mdelay(1000); | |
151 | } | |
152 | ||
153 | static irqreturn_t asm9260_wdt_irq(int irq, void *devid) | |
154 | { | |
155 | struct asm9260_wdt_priv *priv = devid; | |
156 | u32 stat; | |
157 | ||
158 | stat = ioread32(priv->iobase + HW_WDMOD); | |
159 | if (!(stat & BM_MOD_WDINT)) | |
160 | return IRQ_NONE; | |
161 | ||
162 | if (priv->mode == DEBUG) { | |
163 | dev_info(priv->dev, "Watchdog Timeout. Do nothing.\n"); | |
164 | } else { | |
165 | dev_info(priv->dev, "Watchdog Timeout. Doing SW Reset.\n"); | |
166 | asm9260_wdt_sys_reset(priv); | |
167 | } | |
168 | ||
169 | return IRQ_HANDLED; | |
170 | } | |
171 | ||
55dbe8f3 GR |
172 | static int asm9260_restart(struct watchdog_device *wdd, unsigned long action, |
173 | void *data) | |
aae03dc9 | 174 | { |
55dbe8f3 | 175 | struct asm9260_wdt_priv *priv = watchdog_get_drvdata(wdd); |
aae03dc9 OR |
176 | |
177 | asm9260_wdt_sys_reset(priv); | |
178 | ||
55dbe8f3 | 179 | return 0; |
aae03dc9 OR |
180 | } |
181 | ||
182 | static const struct watchdog_info asm9260_wdt_ident = { | |
183 | .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | |
184 | | WDIOF_MAGICCLOSE, | |
185 | .identity = "Alphascale asm9260 Watchdog", | |
186 | }; | |
187 | ||
b893e344 | 188 | static const struct watchdog_ops asm9260_wdt_ops = { |
aae03dc9 OR |
189 | .owner = THIS_MODULE, |
190 | .start = asm9260_wdt_enable, | |
191 | .stop = asm9260_wdt_disable, | |
192 | .get_timeleft = asm9260_wdt_gettimeleft, | |
193 | .ping = asm9260_wdt_feed, | |
194 | .set_timeout = asm9260_wdt_settimeout, | |
55dbe8f3 | 195 | .restart = asm9260_restart, |
aae03dc9 OR |
196 | }; |
197 | ||
f57df835 GR |
198 | static void asm9260_clk_disable_unprepare(void *data) |
199 | { | |
200 | clk_disable_unprepare(data); | |
201 | } | |
202 | ||
ac36856f | 203 | static int asm9260_wdt_get_dt_clks(struct asm9260_wdt_priv *priv) |
aae03dc9 OR |
204 | { |
205 | int err; | |
206 | unsigned long clk; | |
207 | ||
208 | priv->clk = devm_clk_get(priv->dev, "mod"); | |
209 | if (IS_ERR(priv->clk)) { | |
210 | dev_err(priv->dev, "Failed to get \"mod\" clk\n"); | |
211 | return PTR_ERR(priv->clk); | |
212 | } | |
213 | ||
214 | /* configure AHB clock */ | |
215 | priv->clk_ahb = devm_clk_get(priv->dev, "ahb"); | |
216 | if (IS_ERR(priv->clk_ahb)) { | |
217 | dev_err(priv->dev, "Failed to get \"ahb\" clk\n"); | |
218 | return PTR_ERR(priv->clk_ahb); | |
219 | } | |
220 | ||
221 | err = clk_prepare_enable(priv->clk_ahb); | |
222 | if (err) { | |
223 | dev_err(priv->dev, "Failed to enable ahb_clk!\n"); | |
224 | return err; | |
225 | } | |
f57df835 GR |
226 | err = devm_add_action_or_reset(priv->dev, |
227 | asm9260_clk_disable_unprepare, | |
228 | priv->clk_ahb); | |
229 | if (err) | |
230 | return err; | |
aae03dc9 OR |
231 | |
232 | err = clk_set_rate(priv->clk, CLOCK_FREQ); | |
233 | if (err) { | |
aae03dc9 OR |
234 | dev_err(priv->dev, "Failed to set rate!\n"); |
235 | return err; | |
236 | } | |
237 | ||
238 | err = clk_prepare_enable(priv->clk); | |
239 | if (err) { | |
aae03dc9 OR |
240 | dev_err(priv->dev, "Failed to enable clk!\n"); |
241 | return err; | |
242 | } | |
f57df835 GR |
243 | err = devm_add_action_or_reset(priv->dev, |
244 | asm9260_clk_disable_unprepare, | |
245 | priv->clk); | |
246 | if (err) | |
247 | return err; | |
aae03dc9 OR |
248 | |
249 | /* wdt has internal divider */ | |
250 | clk = clk_get_rate(priv->clk); | |
251 | if (!clk) { | |
aae03dc9 OR |
252 | dev_err(priv->dev, "Failed, clk is 0!\n"); |
253 | return -EINVAL; | |
254 | } | |
255 | ||
256 | priv->wdt_freq = clk / 2; | |
257 | ||
258 | return 0; | |
259 | } | |
260 | ||
ac36856f | 261 | static void asm9260_wdt_get_dt_mode(struct asm9260_wdt_priv *priv) |
aae03dc9 OR |
262 | { |
263 | const char *tmp; | |
264 | int ret; | |
265 | ||
266 | /* default mode */ | |
267 | priv->mode = HW_RESET; | |
268 | ||
269 | ret = of_property_read_string(priv->dev->of_node, | |
270 | "alphascale,mode", &tmp); | |
271 | if (ret < 0) | |
272 | return; | |
273 | ||
274 | if (!strcmp(tmp, "hw")) | |
275 | priv->mode = HW_RESET; | |
276 | else if (!strcmp(tmp, "sw")) | |
277 | priv->mode = SW_RESET; | |
278 | else if (!strcmp(tmp, "debug")) | |
279 | priv->mode = DEBUG; | |
280 | else | |
281 | dev_warn(priv->dev, "unknown reset-type: %s. Using default \"hw\" mode.", | |
282 | tmp); | |
283 | } | |
284 | ||
ac36856f | 285 | static int asm9260_wdt_probe(struct platform_device *pdev) |
aae03dc9 | 286 | { |
f57df835 | 287 | struct device *dev = &pdev->dev; |
aae03dc9 OR |
288 | struct asm9260_wdt_priv *priv; |
289 | struct watchdog_device *wdd; | |
aae03dc9 | 290 | int ret; |
b1bbb0cb | 291 | static const char * const mode_name[] = { "hw", "sw", "debug", }; |
aae03dc9 | 292 | |
f57df835 | 293 | priv = devm_kzalloc(dev, sizeof(struct asm9260_wdt_priv), GFP_KERNEL); |
aae03dc9 OR |
294 | if (!priv) |
295 | return -ENOMEM; | |
296 | ||
f57df835 | 297 | priv->dev = dev; |
aae03dc9 | 298 | |
0f0a6a28 | 299 | priv->iobase = devm_platform_ioremap_resource(pdev, 0); |
aae03dc9 OR |
300 | if (IS_ERR(priv->iobase)) |
301 | return PTR_ERR(priv->iobase); | |
302 | ||
f57df835 | 303 | priv->rst = devm_reset_control_get_exclusive(dev, "wdt_rst"); |
aae03dc9 OR |
304 | if (IS_ERR(priv->rst)) |
305 | return PTR_ERR(priv->rst); | |
306 | ||
3c829f47 AK |
307 | ret = asm9260_wdt_get_dt_clks(priv); |
308 | if (ret) | |
309 | return ret; | |
310 | ||
aae03dc9 OR |
311 | wdd = &priv->wdd; |
312 | wdd->info = &asm9260_wdt_ident; | |
313 | wdd->ops = &asm9260_wdt_ops; | |
314 | wdd->min_timeout = 1; | |
315 | wdd->max_timeout = BM_WDTC_MAX(priv->wdt_freq); | |
f57df835 | 316 | wdd->parent = dev; |
aae03dc9 OR |
317 | |
318 | watchdog_set_drvdata(wdd, priv); | |
319 | ||
320 | /* | |
321 | * If 'timeout-sec' unspecified in devicetree, assume a 30 second | |
322 | * default, unless the max timeout is less than 30 seconds, then use | |
323 | * the max instead. | |
324 | */ | |
325 | wdd->timeout = ASM9260_WDT_DEFAULT_TIMEOUT; | |
f57df835 | 326 | watchdog_init_timeout(wdd, 0, dev); |
aae03dc9 OR |
327 | |
328 | asm9260_wdt_get_dt_mode(priv); | |
329 | ||
330 | if (priv->mode != HW_RESET) | |
331 | priv->irq = platform_get_irq(pdev, 0); | |
332 | ||
333 | if (priv->irq > 0) { | |
334 | /* | |
335 | * Not all supported platforms specify an interrupt for the | |
336 | * watchdog, so let's make it optional. | |
337 | */ | |
f57df835 GR |
338 | ret = devm_request_irq(dev, priv->irq, asm9260_wdt_irq, 0, |
339 | pdev->name, priv); | |
aae03dc9 | 340 | if (ret < 0) |
f57df835 | 341 | dev_warn(dev, "failed to request IRQ\n"); |
aae03dc9 OR |
342 | } |
343 | ||
55dbe8f3 GR |
344 | watchdog_set_restart_priority(wdd, 128); |
345 | ||
f57df835 GR |
346 | watchdog_stop_on_reboot(wdd); |
347 | watchdog_stop_on_unregister(wdd); | |
348 | ret = devm_watchdog_register_device(dev, wdd); | |
aae03dc9 | 349 | if (ret) |
f57df835 | 350 | return ret; |
aae03dc9 OR |
351 | |
352 | platform_set_drvdata(pdev, priv); | |
353 | ||
f57df835 | 354 | dev_info(dev, "Watchdog enabled (timeout: %d sec, mode: %s)\n", |
aae03dc9 OR |
355 | wdd->timeout, mode_name[priv->mode]); |
356 | return 0; | |
aae03dc9 OR |
357 | } |
358 | ||
359 | static const struct of_device_id asm9260_wdt_of_match[] = { | |
360 | { .compatible = "alphascale,asm9260-wdt"}, | |
361 | {}, | |
362 | }; | |
363 | MODULE_DEVICE_TABLE(of, asm9260_wdt_of_match); | |
364 | ||
365 | static struct platform_driver asm9260_wdt_driver = { | |
366 | .driver = { | |
367 | .name = "asm9260-wdt", | |
aae03dc9 OR |
368 | .of_match_table = asm9260_wdt_of_match, |
369 | }, | |
370 | .probe = asm9260_wdt_probe, | |
aae03dc9 OR |
371 | }; |
372 | module_platform_driver(asm9260_wdt_driver); | |
373 | ||
374 | MODULE_DESCRIPTION("asm9260 WatchDog Timer Driver"); | |
375 | MODULE_AUTHOR("Oleksij Rempel <linux@rempel-privat.de>"); | |
376 | MODULE_LICENSE("GPL"); |