1 | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | /* |
3 | * Copyright (c) 2004 Simtec Electronics |
4 | * Ben Dooks <ben@simtec.co.uk> |
5 | * |
6 | * S3C2410 Watchdog Timer Support |
7 | * |
8 | * Based on, softdog.c by Alan Cox, |
9 | * (c) Copyright 1996 Alan Cox <alan@lxorguk.ukuu.org.uk> |
10 | */ |
11 | |
12 | #include <linux/module.h> |
13 | #include <linux/moduleparam.h> |
14 | #include <linux/types.h> |
15 | #include <linux/timer.h> |
16 | #include <linux/watchdog.h> |
17 | #include <linux/platform_device.h> |
18 | #include <linux/interrupt.h> |
19 | #include <linux/clk.h> |
20 | #include <linux/uaccess.h> |
21 | #include <linux/io.h> |
22 | #include <linux/cpufreq.h> |
23 | #include <linux/slab.h> |
24 | #include <linux/err.h> |
25 | #include <linux/of.h> |
26 | #include <linux/mfd/syscon.h> |
27 | #include <linux/regmap.h> |
28 | #include <linux/delay.h> |
29 | |
30 | #define S3C2410_WTCON 0x00 |
31 | #define S3C2410_WTDAT 0x04 |
32 | #define S3C2410_WTCNT 0x08 |
33 | #define S3C2410_WTCLRINT 0x0c |
34 | |
35 | #define S3C2410_WTCNT_MAXCNT 0xffff |
36 | |
37 | #define S3C2410_WTCON_RSTEN (1 << 0) |
38 | #define S3C2410_WTCON_INTEN (1 << 2) |
39 | #define S3C2410_WTCON_ENABLE (1 << 5) |
40 | |
41 | #define S3C2410_WTCON_DIV16 (0 << 3) |
42 | #define S3C2410_WTCON_DIV32 (1 << 3) |
43 | #define S3C2410_WTCON_DIV64 (2 << 3) |
44 | #define S3C2410_WTCON_DIV128 (3 << 3) |
45 | |
46 | #define S3C2410_WTCON_MAXDIV 0x80 |
47 | |
48 | #define S3C2410_WTCON_PRESCALE(x) ((x) << 8) |
49 | #define S3C2410_WTCON_PRESCALE_MASK (0xff << 8) |
50 | #define S3C2410_WTCON_PRESCALE_MAX 0xff |
51 | |
52 | #define S3C2410_WATCHDOG_ATBOOT (0) |
53 | #define S3C2410_WATCHDOG_DEFAULT_TIME (15) |
54 | |
55 | #define EXYNOS5_RST_STAT_REG_OFFSET 0x0404 |
56 | #define EXYNOS5_WDT_DISABLE_REG_OFFSET 0x0408 |
57 | #define EXYNOS5_WDT_MASK_RESET_REG_OFFSET 0x040c |
58 | #define EXYNOS850_CLUSTER0_NONCPU_OUT 0x1220 |
59 | #define EXYNOS850_CLUSTER0_NONCPU_INT_EN 0x1244 |
60 | #define EXYNOS850_CLUSTER1_NONCPU_OUT 0x1620 |
61 | #define EXYNOS850_CLUSTER1_NONCPU_INT_EN 0x1644 |
62 | #define EXYNOSAUTOV9_CLUSTER1_NONCPU_OUT 0x1520 |
63 | #define EXYNOSAUTOV9_CLUSTER1_NONCPU_INT_EN 0x1544 |
64 | |
65 | #define EXYNOS850_CLUSTER0_WDTRESET_BIT 24 |
66 | #define EXYNOS850_CLUSTER1_WDTRESET_BIT 23 |
67 | #define EXYNOSAUTOV9_CLUSTER0_WDTRESET_BIT 25 |
68 | #define EXYNOSAUTOV9_CLUSTER1_WDTRESET_BIT 24 |
69 | |
70 | /** |
71 | * DOC: Quirk flags for different Samsung watchdog IP-cores |
72 | * |
73 | * This driver supports multiple Samsung SoCs, each of which might have |
74 | * different set of registers and features supported. As watchdog block |
75 | * sometimes requires modifying PMU registers for proper functioning, register |
76 | * differences in both watchdog and PMU IP-cores should be accounted for. Quirk |
77 | * flags described below serve the purpose of telling the driver about mentioned |
78 | * SoC traits, and can be specified in driver data for each particular supported |
79 | * device. |
80 | * |
81 | * %QUIRK_HAS_WTCLRINT_REG: Watchdog block has WTCLRINT register. It's used to |
82 | * clear the interrupt once the interrupt service routine is complete. It's |
83 | * write-only, writing any values to this register clears the interrupt, but |
84 | * reading is not permitted. |
85 | * |
86 | * %QUIRK_HAS_PMU_MASK_RESET: PMU block has the register for disabling/enabling |
87 | * WDT reset request. On old SoCs it's usually called MASK_WDT_RESET_REQUEST, |
88 | * new SoCs have CLUSTERx_NONCPU_INT_EN register, which 'mask_bit' value is |
89 | * inverted compared to the former one. |
90 | * |
91 | * %QUIRK_HAS_PMU_RST_STAT: PMU block has RST_STAT (reset status) register, |
92 | * which contains bits indicating the reason for most recent CPU reset. If |
93 | * present, driver will use this register to check if previous reboot was due to |
94 | * watchdog timer reset. |
95 | * |
96 | * %QUIRK_HAS_PMU_AUTO_DISABLE: PMU block has AUTOMATIC_WDT_RESET_DISABLE |
97 | * register. If 'mask_bit' bit is set, PMU will disable WDT reset when |
98 | * corresponding processor is in reset state. |
99 | * |
100 | * %QUIRK_HAS_PMU_CNT_EN: PMU block has some register (e.g. CLUSTERx_NONCPU_OUT) |
101 | * with "watchdog counter enable" bit. That bit should be set to make watchdog |
102 | * counter running. |
103 | */ |
104 | #define QUIRK_HAS_WTCLRINT_REG (1 << 0) |
105 | #define QUIRK_HAS_PMU_MASK_RESET (1 << 1) |
106 | #define QUIRK_HAS_PMU_RST_STAT (1 << 2) |
107 | #define QUIRK_HAS_PMU_AUTO_DISABLE (1 << 3) |
108 | #define QUIRK_HAS_PMU_CNT_EN (1 << 4) |
109 | |
110 | /* These quirks require that we have a PMU register map */ |
111 | #define QUIRKS_HAVE_PMUREG \ |
112 | (QUIRK_HAS_PMU_MASK_RESET | QUIRK_HAS_PMU_RST_STAT | \ |
113 | QUIRK_HAS_PMU_AUTO_DISABLE | QUIRK_HAS_PMU_CNT_EN) |
114 | |
115 | static bool nowayout = WATCHDOG_NOWAYOUT; |
116 | static int tmr_margin; |
117 | static int tmr_atboot = S3C2410_WATCHDOG_ATBOOT; |
118 | static int soft_noboot; |
119 | |
120 | module_param(tmr_margin, int, 0); |
121 | module_param(tmr_atboot, int, 0); |
122 | module_param(nowayout, bool, 0); |
123 | module_param(soft_noboot, int, 0); |
124 | |
125 | MODULE_PARM_DESC(tmr_margin, "Watchdog tmr_margin in seconds. (default=" |
126 | __MODULE_STRING(S3C2410_WATCHDOG_DEFAULT_TIME) ")" ); |
127 | MODULE_PARM_DESC(tmr_atboot, |
128 | "Watchdog is started at boot time if set to 1, default=" |
129 | __MODULE_STRING(S3C2410_WATCHDOG_ATBOOT)); |
130 | MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" |
131 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")" ); |
132 | MODULE_PARM_DESC(soft_noboot, "Watchdog action, set to 1 to ignore reboots, 0 to reboot (default 0)" ); |
133 | |
134 | /** |
135 | * struct s3c2410_wdt_variant - Per-variant config data |
136 | * |
137 | * @disable_reg: Offset in pmureg for the register that disables the watchdog |
138 | * timer reset functionality. |
139 | * @mask_reset_reg: Offset in pmureg for the register that masks the watchdog |
140 | * timer reset functionality. |
141 | * @mask_reset_inv: If set, mask_reset_reg value will have inverted meaning. |
142 | * @mask_bit: Bit number for the watchdog timer in the disable register and the |
143 | * mask reset register. |
144 | * @rst_stat_reg: Offset in pmureg for the register that has the reset status. |
145 | * @rst_stat_bit: Bit number in the rst_stat register indicating a watchdog |
146 | * reset. |
147 | * @cnt_en_reg: Offset in pmureg for the register that enables WDT counter. |
148 | * @cnt_en_bit: Bit number for "watchdog counter enable" in cnt_en register. |
149 | * @quirks: A bitfield of quirks. |
150 | */ |
151 | |
152 | struct s3c2410_wdt_variant { |
153 | int disable_reg; |
154 | int mask_reset_reg; |
155 | bool mask_reset_inv; |
156 | int mask_bit; |
157 | int rst_stat_reg; |
158 | int rst_stat_bit; |
159 | int cnt_en_reg; |
160 | int cnt_en_bit; |
161 | u32 quirks; |
162 | }; |
163 | |
164 | struct s3c2410_wdt { |
165 | struct device *dev; |
166 | struct clk *bus_clk; /* for register interface (PCLK) */ |
167 | struct clk *src_clk; /* for WDT counter */ |
168 | void __iomem *reg_base; |
169 | unsigned int count; |
170 | spinlock_t lock; |
171 | unsigned long wtcon_save; |
172 | unsigned long wtdat_save; |
173 | struct watchdog_device wdt_device; |
174 | struct notifier_block freq_transition; |
175 | const struct s3c2410_wdt_variant *drv_data; |
176 | struct regmap *pmureg; |
177 | }; |
178 | |
179 | static const struct s3c2410_wdt_variant drv_data_s3c2410 = { |
180 | .quirks = 0 |
181 | }; |
182 | |
183 | #ifdef CONFIG_OF |
184 | static const struct s3c2410_wdt_variant drv_data_s3c6410 = { |
185 | .quirks = QUIRK_HAS_WTCLRINT_REG, |
186 | }; |
187 | |
188 | static const struct s3c2410_wdt_variant drv_data_exynos5250 = { |
189 | .disable_reg = EXYNOS5_WDT_DISABLE_REG_OFFSET, |
190 | .mask_reset_reg = EXYNOS5_WDT_MASK_RESET_REG_OFFSET, |
191 | .mask_bit = 20, |
192 | .rst_stat_reg = EXYNOS5_RST_STAT_REG_OFFSET, |
193 | .rst_stat_bit = 20, |
194 | .quirks = QUIRK_HAS_WTCLRINT_REG | QUIRK_HAS_PMU_MASK_RESET | \ |
195 | QUIRK_HAS_PMU_RST_STAT | QUIRK_HAS_PMU_AUTO_DISABLE, |
196 | }; |
197 | |
198 | static const struct s3c2410_wdt_variant drv_data_exynos5420 = { |
199 | .disable_reg = EXYNOS5_WDT_DISABLE_REG_OFFSET, |
200 | .mask_reset_reg = EXYNOS5_WDT_MASK_RESET_REG_OFFSET, |
201 | .mask_bit = 0, |
202 | .rst_stat_reg = EXYNOS5_RST_STAT_REG_OFFSET, |
203 | .rst_stat_bit = 9, |
204 | .quirks = QUIRK_HAS_WTCLRINT_REG | QUIRK_HAS_PMU_MASK_RESET | \ |
205 | QUIRK_HAS_PMU_RST_STAT | QUIRK_HAS_PMU_AUTO_DISABLE, |
206 | }; |
207 | |
208 | static const struct s3c2410_wdt_variant drv_data_exynos7 = { |
209 | .disable_reg = EXYNOS5_WDT_DISABLE_REG_OFFSET, |
210 | .mask_reset_reg = EXYNOS5_WDT_MASK_RESET_REG_OFFSET, |
211 | .mask_bit = 23, |
212 | .rst_stat_reg = EXYNOS5_RST_STAT_REG_OFFSET, |
213 | .rst_stat_bit = 23, /* A57 WDTRESET */ |
214 | .quirks = QUIRK_HAS_WTCLRINT_REG | QUIRK_HAS_PMU_MASK_RESET | \ |
215 | QUIRK_HAS_PMU_RST_STAT | QUIRK_HAS_PMU_AUTO_DISABLE, |
216 | }; |
217 | |
218 | static const struct s3c2410_wdt_variant drv_data_exynos850_cl0 = { |
219 | .mask_reset_reg = EXYNOS850_CLUSTER0_NONCPU_INT_EN, |
220 | .mask_bit = 2, |
221 | .mask_reset_inv = true, |
222 | .rst_stat_reg = EXYNOS5_RST_STAT_REG_OFFSET, |
223 | .rst_stat_bit = EXYNOS850_CLUSTER0_WDTRESET_BIT, |
224 | .cnt_en_reg = EXYNOS850_CLUSTER0_NONCPU_OUT, |
225 | .cnt_en_bit = 7, |
226 | .quirks = QUIRK_HAS_WTCLRINT_REG | QUIRK_HAS_PMU_MASK_RESET | \ |
227 | QUIRK_HAS_PMU_RST_STAT | QUIRK_HAS_PMU_CNT_EN, |
228 | }; |
229 | |
230 | static const struct s3c2410_wdt_variant drv_data_exynos850_cl1 = { |
231 | .mask_reset_reg = EXYNOS850_CLUSTER1_NONCPU_INT_EN, |
232 | .mask_bit = 2, |
233 | .mask_reset_inv = true, |
234 | .rst_stat_reg = EXYNOS5_RST_STAT_REG_OFFSET, |
235 | .rst_stat_bit = EXYNOS850_CLUSTER1_WDTRESET_BIT, |
236 | .cnt_en_reg = EXYNOS850_CLUSTER1_NONCPU_OUT, |
237 | .cnt_en_bit = 7, |
238 | .quirks = QUIRK_HAS_WTCLRINT_REG | QUIRK_HAS_PMU_MASK_RESET | \ |
239 | QUIRK_HAS_PMU_RST_STAT | QUIRK_HAS_PMU_CNT_EN, |
240 | }; |
241 | |
242 | static const struct s3c2410_wdt_variant drv_data_exynosautov9_cl0 = { |
243 | .mask_reset_reg = EXYNOS850_CLUSTER0_NONCPU_INT_EN, |
244 | .mask_bit = 2, |
245 | .mask_reset_inv = true, |
246 | .rst_stat_reg = EXYNOS5_RST_STAT_REG_OFFSET, |
247 | .rst_stat_bit = EXYNOSAUTOV9_CLUSTER0_WDTRESET_BIT, |
248 | .cnt_en_reg = EXYNOS850_CLUSTER0_NONCPU_OUT, |
249 | .cnt_en_bit = 7, |
250 | .quirks = QUIRK_HAS_WTCLRINT_REG | QUIRK_HAS_PMU_MASK_RESET | |
251 | QUIRK_HAS_PMU_RST_STAT | QUIRK_HAS_PMU_CNT_EN, |
252 | }; |
253 | |
254 | static const struct s3c2410_wdt_variant drv_data_exynosautov9_cl1 = { |
255 | .mask_reset_reg = EXYNOSAUTOV9_CLUSTER1_NONCPU_INT_EN, |
256 | .mask_bit = 2, |
257 | .mask_reset_inv = true, |
258 | .rst_stat_reg = EXYNOS5_RST_STAT_REG_OFFSET, |
259 | .rst_stat_bit = EXYNOSAUTOV9_CLUSTER1_WDTRESET_BIT, |
260 | .cnt_en_reg = EXYNOSAUTOV9_CLUSTER1_NONCPU_OUT, |
261 | .cnt_en_bit = 7, |
262 | .quirks = QUIRK_HAS_WTCLRINT_REG | QUIRK_HAS_PMU_MASK_RESET | |
263 | QUIRK_HAS_PMU_RST_STAT | QUIRK_HAS_PMU_CNT_EN, |
264 | }; |
265 | |
266 | static const struct of_device_id s3c2410_wdt_match[] = { |
267 | { .compatible = "samsung,s3c2410-wdt" , |
268 | .data = &drv_data_s3c2410 }, |
269 | { .compatible = "samsung,s3c6410-wdt" , |
270 | .data = &drv_data_s3c6410 }, |
271 | { .compatible = "samsung,exynos5250-wdt" , |
272 | .data = &drv_data_exynos5250 }, |
273 | { .compatible = "samsung,exynos5420-wdt" , |
274 | .data = &drv_data_exynos5420 }, |
275 | { .compatible = "samsung,exynos7-wdt" , |
276 | .data = &drv_data_exynos7 }, |
277 | { .compatible = "samsung,exynos850-wdt" , |
278 | .data = &drv_data_exynos850_cl0 }, |
279 | { .compatible = "samsung,exynosautov9-wdt" , |
280 | .data = &drv_data_exynosautov9_cl0 }, |
281 | {}, |
282 | }; |
283 | MODULE_DEVICE_TABLE(of, s3c2410_wdt_match); |
284 | #endif |
285 | |
286 | static const struct platform_device_id s3c2410_wdt_ids[] = { |
287 | { |
288 | .name = "s3c2410-wdt" , |
289 | .driver_data = (unsigned long)&drv_data_s3c2410, |
290 | }, |
291 | {} |
292 | }; |
293 | MODULE_DEVICE_TABLE(platform, s3c2410_wdt_ids); |
294 | |
295 | /* functions */ |
296 | |
297 | static inline unsigned long s3c2410wdt_get_freq(struct s3c2410_wdt *wdt) |
298 | { |
299 | return clk_get_rate(clk: wdt->src_clk ? wdt->src_clk : wdt->bus_clk); |
300 | } |
301 | |
302 | static inline unsigned int s3c2410wdt_max_timeout(struct s3c2410_wdt *wdt) |
303 | { |
304 | const unsigned long freq = s3c2410wdt_get_freq(wdt); |
305 | |
306 | return S3C2410_WTCNT_MAXCNT / (freq / (S3C2410_WTCON_PRESCALE_MAX + 1) |
307 | / S3C2410_WTCON_MAXDIV); |
308 | } |
309 | |
310 | static int s3c2410wdt_disable_wdt_reset(struct s3c2410_wdt *wdt, bool mask) |
311 | { |
312 | const u32 mask_val = BIT(wdt->drv_data->mask_bit); |
313 | const u32 val = mask ? mask_val : 0; |
314 | int ret; |
315 | |
316 | ret = regmap_update_bits(map: wdt->pmureg, reg: wdt->drv_data->disable_reg, |
317 | mask: mask_val, val); |
318 | if (ret < 0) |
319 | dev_err(wdt->dev, "failed to update reg(%d)\n" , ret); |
320 | |
321 | return ret; |
322 | } |
323 | |
324 | static int s3c2410wdt_mask_wdt_reset(struct s3c2410_wdt *wdt, bool mask) |
325 | { |
326 | const u32 mask_val = BIT(wdt->drv_data->mask_bit); |
327 | const bool val_inv = wdt->drv_data->mask_reset_inv; |
328 | const u32 val = (mask ^ val_inv) ? mask_val : 0; |
329 | int ret; |
330 | |
331 | ret = regmap_update_bits(map: wdt->pmureg, reg: wdt->drv_data->mask_reset_reg, |
332 | mask: mask_val, val); |
333 | if (ret < 0) |
334 | dev_err(wdt->dev, "failed to update reg(%d)\n" , ret); |
335 | |
336 | return ret; |
337 | } |
338 | |
339 | static int s3c2410wdt_enable_counter(struct s3c2410_wdt *wdt, bool en) |
340 | { |
341 | const u32 mask_val = BIT(wdt->drv_data->cnt_en_bit); |
342 | const u32 val = en ? mask_val : 0; |
343 | int ret; |
344 | |
345 | ret = regmap_update_bits(map: wdt->pmureg, reg: wdt->drv_data->cnt_en_reg, |
346 | mask: mask_val, val); |
347 | if (ret < 0) |
348 | dev_err(wdt->dev, "failed to update reg(%d)\n" , ret); |
349 | |
350 | return ret; |
351 | } |
352 | |
353 | static int s3c2410wdt_enable(struct s3c2410_wdt *wdt, bool en) |
354 | { |
355 | int ret; |
356 | |
357 | if (wdt->drv_data->quirks & QUIRK_HAS_PMU_AUTO_DISABLE) { |
358 | ret = s3c2410wdt_disable_wdt_reset(wdt, mask: !en); |
359 | if (ret < 0) |
360 | return ret; |
361 | } |
362 | |
363 | if (wdt->drv_data->quirks & QUIRK_HAS_PMU_MASK_RESET) { |
364 | ret = s3c2410wdt_mask_wdt_reset(wdt, mask: !en); |
365 | if (ret < 0) |
366 | return ret; |
367 | } |
368 | |
369 | if (wdt->drv_data->quirks & QUIRK_HAS_PMU_CNT_EN) { |
370 | ret = s3c2410wdt_enable_counter(wdt, en); |
371 | if (ret < 0) |
372 | return ret; |
373 | } |
374 | |
375 | return 0; |
376 | } |
377 | |
378 | static int s3c2410wdt_keepalive(struct watchdog_device *wdd) |
379 | { |
380 | struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd); |
381 | unsigned long flags; |
382 | |
383 | spin_lock_irqsave(&wdt->lock, flags); |
384 | writel(val: wdt->count, addr: wdt->reg_base + S3C2410_WTCNT); |
385 | spin_unlock_irqrestore(lock: &wdt->lock, flags); |
386 | |
387 | return 0; |
388 | } |
389 | |
390 | static void __s3c2410wdt_stop(struct s3c2410_wdt *wdt) |
391 | { |
392 | unsigned long wtcon; |
393 | |
394 | wtcon = readl(addr: wdt->reg_base + S3C2410_WTCON); |
395 | wtcon &= ~(S3C2410_WTCON_ENABLE | S3C2410_WTCON_RSTEN); |
396 | writel(val: wtcon, addr: wdt->reg_base + S3C2410_WTCON); |
397 | } |
398 | |
399 | static int s3c2410wdt_stop(struct watchdog_device *wdd) |
400 | { |
401 | struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd); |
402 | unsigned long flags; |
403 | |
404 | spin_lock_irqsave(&wdt->lock, flags); |
405 | __s3c2410wdt_stop(wdt); |
406 | spin_unlock_irqrestore(lock: &wdt->lock, flags); |
407 | |
408 | return 0; |
409 | } |
410 | |
411 | static int s3c2410wdt_start(struct watchdog_device *wdd) |
412 | { |
413 | unsigned long wtcon; |
414 | struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd); |
415 | unsigned long flags; |
416 | |
417 | spin_lock_irqsave(&wdt->lock, flags); |
418 | |
419 | __s3c2410wdt_stop(wdt); |
420 | |
421 | wtcon = readl(addr: wdt->reg_base + S3C2410_WTCON); |
422 | wtcon |= S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV128; |
423 | |
424 | if (soft_noboot) { |
425 | wtcon |= S3C2410_WTCON_INTEN; |
426 | wtcon &= ~S3C2410_WTCON_RSTEN; |
427 | } else { |
428 | wtcon &= ~S3C2410_WTCON_INTEN; |
429 | wtcon |= S3C2410_WTCON_RSTEN; |
430 | } |
431 | |
432 | dev_dbg(wdt->dev, "Starting watchdog: count=0x%08x, wtcon=%08lx\n" , |
433 | wdt->count, wtcon); |
434 | |
435 | writel(val: wdt->count, addr: wdt->reg_base + S3C2410_WTDAT); |
436 | writel(val: wdt->count, addr: wdt->reg_base + S3C2410_WTCNT); |
437 | writel(val: wtcon, addr: wdt->reg_base + S3C2410_WTCON); |
438 | spin_unlock_irqrestore(lock: &wdt->lock, flags); |
439 | |
440 | return 0; |
441 | } |
442 | |
443 | static int s3c2410wdt_set_heartbeat(struct watchdog_device *wdd, |
444 | unsigned int timeout) |
445 | { |
446 | struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd); |
447 | unsigned long freq = s3c2410wdt_get_freq(wdt); |
448 | unsigned int count; |
449 | unsigned int divisor = 1; |
450 | unsigned long wtcon; |
451 | |
452 | if (timeout < 1) |
453 | return -EINVAL; |
454 | |
455 | freq = DIV_ROUND_UP(freq, 128); |
456 | count = timeout * freq; |
457 | |
458 | dev_dbg(wdt->dev, "Heartbeat: count=%d, timeout=%d, freq=%lu\n" , |
459 | count, timeout, freq); |
460 | |
461 | /* if the count is bigger than the watchdog register, |
462 | then work out what we need to do (and if) we can |
463 | actually make this value |
464 | */ |
465 | |
466 | if (count >= 0x10000) { |
467 | divisor = DIV_ROUND_UP(count, 0xffff); |
468 | |
469 | if (divisor > 0x100) { |
470 | dev_err(wdt->dev, "timeout %d too big\n" , timeout); |
471 | return -EINVAL; |
472 | } |
473 | } |
474 | |
475 | dev_dbg(wdt->dev, "Heartbeat: timeout=%d, divisor=%d, count=%d (%08x)\n" , |
476 | timeout, divisor, count, DIV_ROUND_UP(count, divisor)); |
477 | |
478 | count = DIV_ROUND_UP(count, divisor); |
479 | wdt->count = count; |
480 | |
481 | /* update the pre-scaler */ |
482 | wtcon = readl(addr: wdt->reg_base + S3C2410_WTCON); |
483 | wtcon &= ~S3C2410_WTCON_PRESCALE_MASK; |
484 | wtcon |= S3C2410_WTCON_PRESCALE(divisor-1); |
485 | |
486 | writel(val: count, addr: wdt->reg_base + S3C2410_WTDAT); |
487 | writel(val: wtcon, addr: wdt->reg_base + S3C2410_WTCON); |
488 | |
489 | wdd->timeout = (count * divisor) / freq; |
490 | |
491 | return 0; |
492 | } |
493 | |
494 | static int s3c2410wdt_restart(struct watchdog_device *wdd, unsigned long action, |
495 | void *data) |
496 | { |
497 | struct s3c2410_wdt *wdt = watchdog_get_drvdata(wdd); |
498 | void __iomem *wdt_base = wdt->reg_base; |
499 | |
500 | /* disable watchdog, to be safe */ |
501 | writel(val: 0, addr: wdt_base + S3C2410_WTCON); |
502 | |
503 | /* put initial values into count and data */ |
504 | writel(val: 0x80, addr: wdt_base + S3C2410_WTCNT); |
505 | writel(val: 0x80, addr: wdt_base + S3C2410_WTDAT); |
506 | |
507 | /* set the watchdog to go and reset... */ |
508 | writel(S3C2410_WTCON_ENABLE | S3C2410_WTCON_DIV16 | |
509 | S3C2410_WTCON_RSTEN | S3C2410_WTCON_PRESCALE(0x20), |
510 | addr: wdt_base + S3C2410_WTCON); |
511 | |
512 | /* wait for reset to assert... */ |
513 | mdelay(500); |
514 | |
515 | return 0; |
516 | } |
517 | |
518 | #define OPTIONS (WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE) |
519 | |
520 | static const struct watchdog_info s3c2410_wdt_ident = { |
521 | .options = OPTIONS, |
522 | .firmware_version = 0, |
523 | .identity = "S3C2410 Watchdog" , |
524 | }; |
525 | |
526 | static const struct watchdog_ops s3c2410wdt_ops = { |
527 | .owner = THIS_MODULE, |
528 | .start = s3c2410wdt_start, |
529 | .stop = s3c2410wdt_stop, |
530 | .ping = s3c2410wdt_keepalive, |
531 | .set_timeout = s3c2410wdt_set_heartbeat, |
532 | .restart = s3c2410wdt_restart, |
533 | }; |
534 | |
535 | static const struct watchdog_device s3c2410_wdd = { |
536 | .info = &s3c2410_wdt_ident, |
537 | .ops = &s3c2410wdt_ops, |
538 | .timeout = S3C2410_WATCHDOG_DEFAULT_TIME, |
539 | }; |
540 | |
541 | /* interrupt handler code */ |
542 | |
543 | static irqreturn_t s3c2410wdt_irq(int irqno, void *param) |
544 | { |
545 | struct s3c2410_wdt *wdt = platform_get_drvdata(pdev: param); |
546 | |
547 | dev_info(wdt->dev, "watchdog timer expired (irq)\n" ); |
548 | |
549 | s3c2410wdt_keepalive(wdd: &wdt->wdt_device); |
550 | |
551 | if (wdt->drv_data->quirks & QUIRK_HAS_WTCLRINT_REG) |
552 | writel(val: 0x1, addr: wdt->reg_base + S3C2410_WTCLRINT); |
553 | |
554 | return IRQ_HANDLED; |
555 | } |
556 | |
557 | static inline unsigned int s3c2410wdt_get_bootstatus(struct s3c2410_wdt *wdt) |
558 | { |
559 | unsigned int rst_stat; |
560 | int ret; |
561 | |
562 | if (!(wdt->drv_data->quirks & QUIRK_HAS_PMU_RST_STAT)) |
563 | return 0; |
564 | |
565 | ret = regmap_read(map: wdt->pmureg, reg: wdt->drv_data->rst_stat_reg, val: &rst_stat); |
566 | if (ret) |
567 | dev_warn(wdt->dev, "Couldn't get RST_STAT register\n" ); |
568 | else if (rst_stat & BIT(wdt->drv_data->rst_stat_bit)) |
569 | return WDIOF_CARDRESET; |
570 | |
571 | return 0; |
572 | } |
573 | |
574 | static inline int |
575 | s3c2410_get_wdt_drv_data(struct platform_device *pdev, struct s3c2410_wdt *wdt) |
576 | { |
577 | const struct s3c2410_wdt_variant *variant; |
578 | struct device *dev = &pdev->dev; |
579 | |
580 | variant = of_device_get_match_data(dev); |
581 | if (!variant) { |
582 | /* Device matched by platform_device_id */ |
583 | variant = (struct s3c2410_wdt_variant *) |
584 | platform_get_device_id(pdev)->driver_data; |
585 | } |
586 | |
587 | #ifdef CONFIG_OF |
588 | /* Choose Exynos850/ExynosAutov9 driver data w.r.t. cluster index */ |
589 | if (variant == &drv_data_exynos850_cl0 || |
590 | variant == &drv_data_exynosautov9_cl0) { |
591 | u32 index; |
592 | int err; |
593 | |
594 | err = of_property_read_u32(np: dev->of_node, |
595 | propname: "samsung,cluster-index" , out_value: &index); |
596 | if (err) |
597 | return dev_err_probe(dev, err: -EINVAL, fmt: "failed to get cluster index\n" ); |
598 | |
599 | switch (index) { |
600 | case 0: |
601 | break; |
602 | case 1: |
603 | variant = (variant == &drv_data_exynos850_cl0) ? |
604 | &drv_data_exynos850_cl1 : |
605 | &drv_data_exynosautov9_cl1; |
606 | break; |
607 | default: |
608 | return dev_err_probe(dev, err: -EINVAL, fmt: "wrong cluster index: %u\n" , index); |
609 | } |
610 | } |
611 | #endif |
612 | |
613 | wdt->drv_data = variant; |
614 | return 0; |
615 | } |
616 | |
617 | static void s3c2410wdt_wdt_disable_action(void *data) |
618 | { |
619 | s3c2410wdt_enable(wdt: data, en: false); |
620 | } |
621 | |
622 | static int s3c2410wdt_probe(struct platform_device *pdev) |
623 | { |
624 | struct device *dev = &pdev->dev; |
625 | struct s3c2410_wdt *wdt; |
626 | unsigned int wtcon; |
627 | int wdt_irq; |
628 | int ret; |
629 | |
630 | wdt = devm_kzalloc(dev, size: sizeof(*wdt), GFP_KERNEL); |
631 | if (!wdt) |
632 | return -ENOMEM; |
633 | |
634 | wdt->dev = dev; |
635 | spin_lock_init(&wdt->lock); |
636 | wdt->wdt_device = s3c2410_wdd; |
637 | |
638 | ret = s3c2410_get_wdt_drv_data(pdev, wdt); |
639 | if (ret) |
640 | return ret; |
641 | |
642 | if (wdt->drv_data->quirks & QUIRKS_HAVE_PMUREG) { |
643 | wdt->pmureg = syscon_regmap_lookup_by_phandle(np: dev->of_node, |
644 | property: "samsung,syscon-phandle" ); |
645 | if (IS_ERR(ptr: wdt->pmureg)) |
646 | return dev_err_probe(dev, err: PTR_ERR(ptr: wdt->pmureg), |
647 | fmt: "syscon regmap lookup failed.\n" ); |
648 | } |
649 | |
650 | wdt_irq = platform_get_irq(pdev, 0); |
651 | if (wdt_irq < 0) |
652 | return wdt_irq; |
653 | |
654 | /* get the memory region for the watchdog timer */ |
655 | wdt->reg_base = devm_platform_ioremap_resource(pdev, index: 0); |
656 | if (IS_ERR(ptr: wdt->reg_base)) |
657 | return PTR_ERR(ptr: wdt->reg_base); |
658 | |
659 | wdt->bus_clk = devm_clk_get_enabled(dev, id: "watchdog" ); |
660 | if (IS_ERR(ptr: wdt->bus_clk)) |
661 | return dev_err_probe(dev, err: PTR_ERR(ptr: wdt->bus_clk), fmt: "failed to get bus clock\n" ); |
662 | |
663 | /* |
664 | * "watchdog_src" clock is optional; if it's not present -- just skip it |
665 | * and use "watchdog" clock as both bus and source clock. |
666 | */ |
667 | wdt->src_clk = devm_clk_get_optional_enabled(dev, id: "watchdog_src" ); |
668 | if (IS_ERR(ptr: wdt->src_clk)) |
669 | return dev_err_probe(dev, err: PTR_ERR(ptr: wdt->src_clk), fmt: "failed to get source clock\n" ); |
670 | |
671 | wdt->wdt_device.min_timeout = 1; |
672 | wdt->wdt_device.max_timeout = s3c2410wdt_max_timeout(wdt); |
673 | |
674 | watchdog_set_drvdata(wdd: &wdt->wdt_device, data: wdt); |
675 | |
676 | /* see if we can actually set the requested timer margin, and if |
677 | * not, try the default value */ |
678 | |
679 | watchdog_init_timeout(wdd: &wdt->wdt_device, timeout_parm: tmr_margin, dev); |
680 | ret = s3c2410wdt_set_heartbeat(wdd: &wdt->wdt_device, |
681 | timeout: wdt->wdt_device.timeout); |
682 | if (ret) { |
683 | ret = s3c2410wdt_set_heartbeat(wdd: &wdt->wdt_device, |
684 | S3C2410_WATCHDOG_DEFAULT_TIME); |
685 | if (ret == 0) |
686 | dev_warn(dev, "tmr_margin value out of range, default %d used\n" , |
687 | S3C2410_WATCHDOG_DEFAULT_TIME); |
688 | else |
689 | return dev_err_probe(dev, err: ret, fmt: "failed to use default timeout\n" ); |
690 | } |
691 | |
692 | ret = devm_request_irq(dev, irq: wdt_irq, handler: s3c2410wdt_irq, irqflags: 0, |
693 | devname: pdev->name, dev_id: pdev); |
694 | if (ret != 0) |
695 | return dev_err_probe(dev, err: ret, fmt: "failed to install irq (%d)\n" , ret); |
696 | |
697 | watchdog_set_nowayout(wdd: &wdt->wdt_device, nowayout); |
698 | watchdog_set_restart_priority(wdd: &wdt->wdt_device, priority: 128); |
699 | |
700 | wdt->wdt_device.bootstatus = s3c2410wdt_get_bootstatus(wdt); |
701 | wdt->wdt_device.parent = dev; |
702 | |
703 | /* |
704 | * If "tmr_atboot" param is non-zero, start the watchdog right now. Also |
705 | * set WDOG_HW_RUNNING bit, so that watchdog core can kick the watchdog. |
706 | * |
707 | * If we're not enabling the watchdog, then ensure it is disabled if it |
708 | * has been left running from the bootloader or other source. |
709 | */ |
710 | if (tmr_atboot) { |
711 | dev_info(dev, "starting watchdog timer\n" ); |
712 | s3c2410wdt_start(wdd: &wdt->wdt_device); |
713 | set_bit(WDOG_HW_RUNNING, addr: &wdt->wdt_device.status); |
714 | } else { |
715 | s3c2410wdt_stop(wdd: &wdt->wdt_device); |
716 | } |
717 | |
718 | ret = devm_watchdog_register_device(dev, &wdt->wdt_device); |
719 | if (ret) |
720 | return ret; |
721 | |
722 | ret = s3c2410wdt_enable(wdt, en: true); |
723 | if (ret < 0) |
724 | return ret; |
725 | |
726 | ret = devm_add_action_or_reset(dev, s3c2410wdt_wdt_disable_action, wdt); |
727 | if (ret) |
728 | return ret; |
729 | |
730 | platform_set_drvdata(pdev, data: wdt); |
731 | |
732 | /* print out a statement of readiness */ |
733 | |
734 | wtcon = readl(addr: wdt->reg_base + S3C2410_WTCON); |
735 | |
736 | dev_info(dev, "watchdog %sactive, reset %sabled, irq %sabled\n" , |
737 | (wtcon & S3C2410_WTCON_ENABLE) ? "" : "in" , |
738 | (wtcon & S3C2410_WTCON_RSTEN) ? "en" : "dis" , |
739 | (wtcon & S3C2410_WTCON_INTEN) ? "en" : "dis" ); |
740 | |
741 | return 0; |
742 | } |
743 | |
744 | static void s3c2410wdt_shutdown(struct platform_device *dev) |
745 | { |
746 | struct s3c2410_wdt *wdt = platform_get_drvdata(pdev: dev); |
747 | |
748 | s3c2410wdt_enable(wdt, en: false); |
749 | s3c2410wdt_stop(wdd: &wdt->wdt_device); |
750 | } |
751 | |
752 | static int s3c2410wdt_suspend(struct device *dev) |
753 | { |
754 | int ret; |
755 | struct s3c2410_wdt *wdt = dev_get_drvdata(dev); |
756 | |
757 | /* Save watchdog state, and turn it off. */ |
758 | wdt->wtcon_save = readl(addr: wdt->reg_base + S3C2410_WTCON); |
759 | wdt->wtdat_save = readl(addr: wdt->reg_base + S3C2410_WTDAT); |
760 | |
761 | ret = s3c2410wdt_enable(wdt, en: false); |
762 | if (ret < 0) |
763 | return ret; |
764 | |
765 | /* Note that WTCNT doesn't need to be saved. */ |
766 | s3c2410wdt_stop(wdd: &wdt->wdt_device); |
767 | |
768 | return 0; |
769 | } |
770 | |
771 | static int s3c2410wdt_resume(struct device *dev) |
772 | { |
773 | int ret; |
774 | struct s3c2410_wdt *wdt = dev_get_drvdata(dev); |
775 | |
776 | /* Restore watchdog state. */ |
777 | writel(val: wdt->wtdat_save, addr: wdt->reg_base + S3C2410_WTDAT); |
778 | writel(val: wdt->wtdat_save, addr: wdt->reg_base + S3C2410_WTCNT);/* Reset count */ |
779 | writel(val: wdt->wtcon_save, addr: wdt->reg_base + S3C2410_WTCON); |
780 | |
781 | ret = s3c2410wdt_enable(wdt, en: true); |
782 | if (ret < 0) |
783 | return ret; |
784 | |
785 | dev_info(dev, "watchdog %sabled\n" , |
786 | (wdt->wtcon_save & S3C2410_WTCON_ENABLE) ? "en" : "dis" ); |
787 | |
788 | return 0; |
789 | } |
790 | |
791 | static DEFINE_SIMPLE_DEV_PM_OPS(s3c2410wdt_pm_ops, |
792 | s3c2410wdt_suspend, s3c2410wdt_resume); |
793 | |
794 | static struct platform_driver s3c2410wdt_driver = { |
795 | .probe = s3c2410wdt_probe, |
796 | .shutdown = s3c2410wdt_shutdown, |
797 | .id_table = s3c2410_wdt_ids, |
798 | .driver = { |
799 | .name = "s3c2410-wdt" , |
800 | .pm = pm_sleep_ptr(&s3c2410wdt_pm_ops), |
801 | .of_match_table = of_match_ptr(s3c2410_wdt_match), |
802 | }, |
803 | }; |
804 | |
805 | module_platform_driver(s3c2410wdt_driver); |
806 | |
807 | MODULE_AUTHOR("Ben Dooks <ben@simtec.co.uk>, Dimitry Andric <dimitry.andric@tomtom.com>" ); |
808 | MODULE_DESCRIPTION("S3C2410 Watchdog Device Driver" ); |
809 | MODULE_LICENSE("GPL" ); |
810 | |