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