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