Commit | Line | Data |
---|---|---|
2e62c498 | 1 | // SPDX-License-Identifier: GPL-2.0 |
6adb730d MM |
2 | /* |
3 | * Copyright (C) 2013 Broadcom Corporation | |
4 | * | |
6adb730d MM |
5 | */ |
6 | ||
6e2ac20e | 7 | #include <linux/debugfs.h> |
6adb730d MM |
8 | #include <linux/delay.h> |
9 | #include <linux/err.h> | |
10 | #include <linux/io.h> | |
11 | #include <linux/module.h> | |
12 | #include <linux/of_address.h> | |
13 | #include <linux/platform_device.h> | |
14 | #include <linux/watchdog.h> | |
15 | ||
16 | #define SECWDOG_CTRL_REG 0x00000000 | |
17 | #define SECWDOG_COUNT_REG 0x00000004 | |
18 | ||
19 | #define SECWDOG_RESERVED_MASK 0x1dffffff | |
20 | #define SECWDOG_WD_LOAD_FLAG 0x10000000 | |
21 | #define SECWDOG_EN_MASK 0x08000000 | |
22 | #define SECWDOG_SRSTEN_MASK 0x04000000 | |
23 | #define SECWDOG_RES_MASK 0x00f00000 | |
24 | #define SECWDOG_COUNT_MASK 0x000fffff | |
25 | ||
26 | #define SECWDOG_MAX_COUNT SECWDOG_COUNT_MASK | |
27 | #define SECWDOG_CLKS_SHIFT 20 | |
28 | #define SECWDOG_MAX_RES 15 | |
29 | #define SECWDOG_DEFAULT_RESOLUTION 4 | |
30 | #define SECWDOG_MAX_TRY 1000 | |
31 | ||
32 | #define SECS_TO_TICKS(x, w) ((x) << (w)->resolution) | |
33 | #define TICKS_TO_SECS(x, w) ((x) >> (w)->resolution) | |
34 | ||
35 | #define BCM_KONA_WDT_NAME "bcm_kona_wdt" | |
36 | ||
37 | struct bcm_kona_wdt { | |
38 | void __iomem *base; | |
39 | /* | |
40 | * One watchdog tick is 1/(2^resolution) seconds. Resolution can take | |
41 | * the values 0-15, meaning one tick can be 1s to 30.52us. Our default | |
42 | * resolution of 4 means one tick is 62.5ms. | |
43 | * | |
44 | * The watchdog counter is 20 bits. Depending on resolution, the maximum | |
45 | * counter value of 0xfffff expires after about 12 days (resolution 0) | |
46 | * down to only 32s (resolution 15). The default resolution of 4 gives | |
47 | * us a maximum of about 18 hours and 12 minutes before the watchdog | |
48 | * times out. | |
49 | */ | |
50 | int resolution; | |
51 | spinlock_t lock; | |
6e2ac20e MM |
52 | #ifdef CONFIG_BCM_KONA_WDT_DEBUG |
53 | unsigned long busy_count; | |
54 | struct dentry *debugfs; | |
55 | #endif | |
6adb730d MM |
56 | }; |
57 | ||
6e2ac20e | 58 | static int secure_register_read(struct bcm_kona_wdt *wdt, uint32_t offset) |
6adb730d MM |
59 | { |
60 | uint32_t val; | |
61 | unsigned count = 0; | |
62 | ||
63 | /* | |
64 | * If the WD_LOAD_FLAG is set, the watchdog counter field is being | |
65 | * updated in hardware. Once the WD timer is updated in hardware, it | |
66 | * gets cleared. | |
67 | */ | |
68 | do { | |
69 | if (unlikely(count > 1)) | |
70 | udelay(5); | |
6e2ac20e | 71 | val = readl_relaxed(wdt->base + offset); |
6adb730d MM |
72 | count++; |
73 | } while ((val & SECWDOG_WD_LOAD_FLAG) && count < SECWDOG_MAX_TRY); | |
74 | ||
6e2ac20e MM |
75 | #ifdef CONFIG_BCM_KONA_WDT_DEBUG |
76 | /* Remember the maximum number iterations due to WD_LOAD_FLAG */ | |
77 | if (count > wdt->busy_count) | |
78 | wdt->busy_count = count; | |
79 | #endif | |
80 | ||
6adb730d MM |
81 | /* This is the only place we return a negative value. */ |
82 | if (val & SECWDOG_WD_LOAD_FLAG) | |
83 | return -ETIMEDOUT; | |
84 | ||
85 | /* We always mask out reserved bits. */ | |
86 | val &= SECWDOG_RESERVED_MASK; | |
87 | ||
88 | return val; | |
89 | } | |
90 | ||
6e2ac20e MM |
91 | #ifdef CONFIG_BCM_KONA_WDT_DEBUG |
92 | ||
57808f44 | 93 | static int bcm_kona_show(struct seq_file *s, void *data) |
6e2ac20e | 94 | { |
e1dbde29 | 95 | int ctl_val, cur_val; |
6e2ac20e MM |
96 | unsigned long flags; |
97 | struct bcm_kona_wdt *wdt = s->private; | |
98 | ||
e1dbde29 JP |
99 | if (!wdt) { |
100 | seq_puts(s, "No device pointer\n"); | |
101 | return 0; | |
102 | } | |
6e2ac20e MM |
103 | |
104 | spin_lock_irqsave(&wdt->lock, flags); | |
105 | ctl_val = secure_register_read(wdt, SECWDOG_CTRL_REG); | |
106 | cur_val = secure_register_read(wdt, SECWDOG_COUNT_REG); | |
107 | spin_unlock_irqrestore(&wdt->lock, flags); | |
108 | ||
109 | if (ctl_val < 0 || cur_val < 0) { | |
e1dbde29 | 110 | seq_puts(s, "Error accessing hardware\n"); |
6e2ac20e MM |
111 | } else { |
112 | int ctl, cur, ctl_sec, cur_sec, res; | |
113 | ||
114 | ctl = ctl_val & SECWDOG_COUNT_MASK; | |
115 | res = (ctl_val & SECWDOG_RES_MASK) >> SECWDOG_CLKS_SHIFT; | |
116 | cur = cur_val & SECWDOG_COUNT_MASK; | |
117 | ctl_sec = TICKS_TO_SECS(ctl, wdt); | |
118 | cur_sec = TICKS_TO_SECS(cur, wdt); | |
e1dbde29 JP |
119 | seq_printf(s, |
120 | "Resolution: %d / %d\n" | |
121 | "Control: %d s / %d (%#x) ticks\n" | |
122 | "Current: %d s / %d (%#x) ticks\n" | |
123 | "Busy count: %lu\n", | |
124 | res, wdt->resolution, | |
125 | ctl_sec, ctl, ctl, | |
126 | cur_sec, cur, cur, | |
127 | wdt->busy_count); | |
6e2ac20e MM |
128 | } |
129 | ||
e1dbde29 | 130 | return 0; |
6e2ac20e MM |
131 | } |
132 | ||
57808f44 | 133 | DEFINE_SHOW_ATTRIBUTE(bcm_kona); |
6e2ac20e MM |
134 | |
135 | static void bcm_kona_wdt_debug_init(struct platform_device *pdev) | |
136 | { | |
137 | struct dentry *dir; | |
138 | struct bcm_kona_wdt *wdt = platform_get_drvdata(pdev); | |
139 | ||
140 | if (!wdt) | |
141 | return; | |
142 | ||
143 | wdt->debugfs = NULL; | |
144 | ||
145 | dir = debugfs_create_dir(BCM_KONA_WDT_NAME, NULL); | |
6e2ac20e | 146 | |
1dbb3bb8 GKH |
147 | debugfs_create_file("info", S_IFREG | S_IRUGO, dir, wdt, |
148 | &bcm_kona_fops); | |
149 | wdt->debugfs = dir; | |
6e2ac20e MM |
150 | } |
151 | ||
152 | static void bcm_kona_wdt_debug_exit(struct platform_device *pdev) | |
153 | { | |
154 | struct bcm_kona_wdt *wdt = platform_get_drvdata(pdev); | |
155 | ||
1dbb3bb8 | 156 | if (wdt) |
6e2ac20e | 157 | debugfs_remove_recursive(wdt->debugfs); |
6e2ac20e MM |
158 | } |
159 | ||
160 | #else | |
161 | ||
162 | static void bcm_kona_wdt_debug_init(struct platform_device *pdev) {} | |
163 | static void bcm_kona_wdt_debug_exit(struct platform_device *pdev) {} | |
164 | ||
165 | #endif /* CONFIG_BCM_KONA_WDT_DEBUG */ | |
166 | ||
6adb730d MM |
167 | static int bcm_kona_wdt_ctrl_reg_modify(struct bcm_kona_wdt *wdt, |
168 | unsigned mask, unsigned newval) | |
169 | { | |
170 | int val; | |
171 | unsigned long flags; | |
172 | int ret = 0; | |
173 | ||
174 | spin_lock_irqsave(&wdt->lock, flags); | |
175 | ||
6e2ac20e | 176 | val = secure_register_read(wdt, SECWDOG_CTRL_REG); |
6adb730d MM |
177 | if (val < 0) { |
178 | ret = val; | |
179 | } else { | |
180 | val &= ~mask; | |
181 | val |= newval; | |
182 | writel_relaxed(val, wdt->base + SECWDOG_CTRL_REG); | |
183 | } | |
184 | ||
185 | spin_unlock_irqrestore(&wdt->lock, flags); | |
186 | ||
187 | return ret; | |
188 | } | |
189 | ||
190 | static int bcm_kona_wdt_set_resolution_reg(struct bcm_kona_wdt *wdt) | |
191 | { | |
192 | if (wdt->resolution > SECWDOG_MAX_RES) | |
193 | return -EINVAL; | |
194 | ||
195 | return bcm_kona_wdt_ctrl_reg_modify(wdt, SECWDOG_RES_MASK, | |
196 | wdt->resolution << SECWDOG_CLKS_SHIFT); | |
197 | } | |
198 | ||
199 | static int bcm_kona_wdt_set_timeout_reg(struct watchdog_device *wdog, | |
200 | unsigned watchdog_flags) | |
201 | { | |
202 | struct bcm_kona_wdt *wdt = watchdog_get_drvdata(wdog); | |
203 | ||
204 | return bcm_kona_wdt_ctrl_reg_modify(wdt, SECWDOG_COUNT_MASK, | |
205 | SECS_TO_TICKS(wdog->timeout, wdt) | | |
206 | watchdog_flags); | |
207 | } | |
208 | ||
209 | static int bcm_kona_wdt_set_timeout(struct watchdog_device *wdog, | |
210 | unsigned int t) | |
211 | { | |
212 | wdog->timeout = t; | |
213 | return 0; | |
214 | } | |
215 | ||
216 | static unsigned int bcm_kona_wdt_get_timeleft(struct watchdog_device *wdog) | |
217 | { | |
218 | struct bcm_kona_wdt *wdt = watchdog_get_drvdata(wdog); | |
219 | int val; | |
220 | unsigned long flags; | |
221 | ||
222 | spin_lock_irqsave(&wdt->lock, flags); | |
6e2ac20e | 223 | val = secure_register_read(wdt, SECWDOG_COUNT_REG); |
6adb730d MM |
224 | spin_unlock_irqrestore(&wdt->lock, flags); |
225 | ||
226 | if (val < 0) | |
227 | return val; | |
228 | ||
229 | return TICKS_TO_SECS(val & SECWDOG_COUNT_MASK, wdt); | |
230 | } | |
231 | ||
232 | static int bcm_kona_wdt_start(struct watchdog_device *wdog) | |
233 | { | |
234 | return bcm_kona_wdt_set_timeout_reg(wdog, | |
235 | SECWDOG_EN_MASK | SECWDOG_SRSTEN_MASK); | |
236 | } | |
237 | ||
238 | static int bcm_kona_wdt_stop(struct watchdog_device *wdog) | |
239 | { | |
240 | struct bcm_kona_wdt *wdt = watchdog_get_drvdata(wdog); | |
241 | ||
242 | return bcm_kona_wdt_ctrl_reg_modify(wdt, SECWDOG_EN_MASK | | |
243 | SECWDOG_SRSTEN_MASK, 0); | |
244 | } | |
245 | ||
b893e344 | 246 | static const struct watchdog_ops bcm_kona_wdt_ops = { |
6adb730d MM |
247 | .owner = THIS_MODULE, |
248 | .start = bcm_kona_wdt_start, | |
249 | .stop = bcm_kona_wdt_stop, | |
250 | .set_timeout = bcm_kona_wdt_set_timeout, | |
251 | .get_timeleft = bcm_kona_wdt_get_timeleft, | |
252 | }; | |
253 | ||
6c368932 | 254 | static const struct watchdog_info bcm_kona_wdt_info = { |
6adb730d MM |
255 | .options = WDIOF_SETTIMEOUT | WDIOF_MAGICCLOSE | |
256 | WDIOF_KEEPALIVEPING, | |
257 | .identity = "Broadcom Kona Watchdog Timer", | |
258 | }; | |
259 | ||
260 | static struct watchdog_device bcm_kona_wdt_wdd = { | |
261 | .info = &bcm_kona_wdt_info, | |
262 | .ops = &bcm_kona_wdt_ops, | |
263 | .min_timeout = 1, | |
264 | .max_timeout = SECWDOG_MAX_COUNT >> SECWDOG_DEFAULT_RESOLUTION, | |
265 | .timeout = SECWDOG_MAX_COUNT >> SECWDOG_DEFAULT_RESOLUTION, | |
266 | }; | |
267 | ||
6adb730d MM |
268 | static int bcm_kona_wdt_probe(struct platform_device *pdev) |
269 | { | |
270 | struct device *dev = &pdev->dev; | |
271 | struct bcm_kona_wdt *wdt; | |
6adb730d MM |
272 | int ret; |
273 | ||
274 | wdt = devm_kzalloc(dev, sizeof(*wdt), GFP_KERNEL); | |
275 | if (!wdt) | |
276 | return -ENOMEM; | |
277 | ||
fedf266f EA |
278 | spin_lock_init(&wdt->lock); |
279 | ||
0f0a6a28 | 280 | wdt->base = devm_platform_ioremap_resource(pdev, 0); |
6adb730d | 281 | if (IS_ERR(wdt->base)) |
fd998972 | 282 | return PTR_ERR(wdt->base); |
6adb730d MM |
283 | |
284 | wdt->resolution = SECWDOG_DEFAULT_RESOLUTION; | |
285 | ret = bcm_kona_wdt_set_resolution_reg(wdt); | |
286 | if (ret) { | |
287 | dev_err(dev, "Failed to set resolution (error: %d)", ret); | |
288 | return ret; | |
289 | } | |
290 | ||
6adb730d MM |
291 | platform_set_drvdata(pdev, wdt); |
292 | watchdog_set_drvdata(&bcm_kona_wdt_wdd, wdt); | |
4062ec06 | 293 | bcm_kona_wdt_wdd.parent = dev; |
6adb730d MM |
294 | |
295 | ret = bcm_kona_wdt_set_timeout_reg(&bcm_kona_wdt_wdd, 0); | |
296 | if (ret) { | |
297 | dev_err(dev, "Failed set watchdog timeout"); | |
298 | return ret; | |
299 | } | |
300 | ||
4062ec06 GR |
301 | watchdog_stop_on_reboot(&bcm_kona_wdt_wdd); |
302 | watchdog_stop_on_unregister(&bcm_kona_wdt_wdd); | |
303 | ret = devm_watchdog_register_device(dev, &bcm_kona_wdt_wdd); | |
ae07bdbe | 304 | if (ret) |
6adb730d | 305 | return ret; |
6adb730d | 306 | |
6e2ac20e | 307 | bcm_kona_wdt_debug_init(pdev); |
6adb730d MM |
308 | dev_dbg(dev, "Broadcom Kona Watchdog Timer"); |
309 | ||
310 | return 0; | |
311 | } | |
312 | ||
0eddace4 | 313 | static void bcm_kona_wdt_remove(struct platform_device *pdev) |
6adb730d | 314 | { |
6e2ac20e | 315 | bcm_kona_wdt_debug_exit(pdev); |
6adb730d | 316 | dev_dbg(&pdev->dev, "Watchdog driver disabled"); |
6adb730d MM |
317 | } |
318 | ||
319 | static const struct of_device_id bcm_kona_wdt_of_match[] = { | |
320 | { .compatible = "brcm,kona-wdt", }, | |
321 | {}, | |
322 | }; | |
323 | MODULE_DEVICE_TABLE(of, bcm_kona_wdt_of_match); | |
324 | ||
325 | static struct platform_driver bcm_kona_wdt_driver = { | |
326 | .driver = { | |
327 | .name = BCM_KONA_WDT_NAME, | |
6adb730d MM |
328 | .of_match_table = bcm_kona_wdt_of_match, |
329 | }, | |
330 | .probe = bcm_kona_wdt_probe, | |
0eddace4 | 331 | .remove_new = bcm_kona_wdt_remove, |
6adb730d MM |
332 | }; |
333 | ||
334 | module_platform_driver(bcm_kona_wdt_driver); | |
335 | ||
336 | MODULE_ALIAS("platform:" BCM_KONA_WDT_NAME); | |
337 | MODULE_AUTHOR("Markus Mayer <mmayer@broadcom.com>"); | |
338 | MODULE_DESCRIPTION("Broadcom Kona Watchdog Driver"); | |
339 | MODULE_LICENSE("GPL v2"); |