Commit | Line | Data |
---|---|---|
81126222 DM |
1 | // SPDX-License-Identifier: GPL-2.0+ |
2 | /* | |
3 | * exar_wdt.c - Driver for the watchdog present in some | |
4 | * Exar/MaxLinear UART chips like the XR28V38x. | |
5 | * | |
6 | * (c) Copyright 2022 D. Müller <d.mueller@elsoft.ch>. | |
7 | * | |
8 | */ | |
9 | ||
10 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt | |
11 | ||
12 | #include <linux/io.h> | |
13 | #include <linux/list.h> | |
14 | #include <linux/module.h> | |
15 | #include <linux/platform_device.h> | |
16 | #include <linux/slab.h> | |
17 | #include <linux/watchdog.h> | |
18 | ||
19 | #define DRV_NAME "exar_wdt" | |
20 | ||
21 | static const unsigned short sio_config_ports[] = { 0x2e, 0x4e }; | |
22 | static const unsigned char sio_enter_keys[] = { 0x67, 0x77, 0x87, 0xA0 }; | |
23 | #define EXAR_EXIT_KEY 0xAA | |
24 | ||
25 | #define EXAR_LDN 0x07 | |
26 | #define EXAR_DID 0x20 | |
27 | #define EXAR_VID 0x23 | |
28 | #define EXAR_WDT 0x26 | |
29 | #define EXAR_ACT 0x30 | |
30 | #define EXAR_RTBASE 0x60 | |
31 | ||
32 | #define EXAR_WDT_LDEV 0x08 | |
33 | ||
34 | #define EXAR_VEN_ID 0x13A8 | |
35 | #define EXAR_DEV_382 0x0382 | |
36 | #define EXAR_DEV_384 0x0384 | |
37 | ||
38 | /* WDT runtime registers */ | |
39 | #define WDT_CTRL 0x00 | |
40 | #define WDT_VAL 0x01 | |
41 | ||
42 | #define WDT_UNITS_10MS 0x0 /* the 10 millisec unit of the HW is not used */ | |
43 | #define WDT_UNITS_SEC 0x2 | |
44 | #define WDT_UNITS_MIN 0x4 | |
45 | ||
46 | /* default WDT control for WDTOUT signal activ / rearm by read */ | |
47 | #define EXAR_WDT_DEF_CONF 0 | |
48 | ||
49 | struct wdt_pdev_node { | |
50 | struct list_head list; | |
51 | struct platform_device *pdev; | |
52 | const char name[16]; | |
53 | }; | |
54 | ||
55 | struct wdt_priv { | |
56 | /* the lock for WDT io operations */ | |
57 | spinlock_t io_lock; | |
58 | struct resource wdt_res; | |
59 | struct watchdog_device wdt_dev; | |
60 | unsigned short did; | |
61 | unsigned short config_port; | |
62 | unsigned char enter_key; | |
63 | unsigned char unit; | |
64 | unsigned char timeout; | |
65 | }; | |
66 | ||
67 | #define WATCHDOG_TIMEOUT 60 | |
68 | ||
69 | static int timeout = WATCHDOG_TIMEOUT; | |
70 | module_param(timeout, int, 0); | |
71 | MODULE_PARM_DESC(timeout, | |
72 | "Watchdog timeout in seconds. 1<=timeout<=15300, default=" | |
73 | __MODULE_STRING(WATCHDOG_TIMEOUT) "."); | |
74 | ||
75 | static bool nowayout = WATCHDOG_NOWAYOUT; | |
76 | module_param(nowayout, bool, 0); | |
77 | MODULE_PARM_DESC(nowayout, | |
78 | "Watchdog cannot be stopped once started (default=" | |
79 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); | |
80 | ||
81 | static int exar_sio_enter(const unsigned short config_port, | |
82 | const unsigned char key) | |
83 | { | |
84 | if (!request_muxed_region(config_port, 2, DRV_NAME)) | |
85 | return -EBUSY; | |
86 | ||
87 | /* write the ENTER-KEY twice */ | |
88 | outb(key, config_port); | |
89 | outb(key, config_port); | |
90 | ||
91 | return 0; | |
92 | } | |
93 | ||
94 | static void exar_sio_exit(const unsigned short config_port) | |
95 | { | |
96 | outb(EXAR_EXIT_KEY, config_port); | |
97 | release_region(config_port, 2); | |
98 | } | |
99 | ||
100 | static unsigned char exar_sio_read(const unsigned short config_port, | |
101 | const unsigned char reg) | |
102 | { | |
103 | outb(reg, config_port); | |
104 | return inb(config_port + 1); | |
105 | } | |
106 | ||
107 | static void exar_sio_write(const unsigned short config_port, | |
108 | const unsigned char reg, const unsigned char val) | |
109 | { | |
110 | outb(reg, config_port); | |
111 | outb(val, config_port + 1); | |
112 | } | |
113 | ||
114 | static unsigned short exar_sio_read16(const unsigned short config_port, | |
115 | const unsigned char reg) | |
116 | { | |
117 | unsigned char msb, lsb; | |
118 | ||
119 | msb = exar_sio_read(config_port, reg); | |
120 | lsb = exar_sio_read(config_port, reg + 1); | |
121 | ||
122 | return (msb << 8) | lsb; | |
123 | } | |
124 | ||
125 | static void exar_sio_select_wdt(const unsigned short config_port) | |
126 | { | |
127 | exar_sio_write(config_port, EXAR_LDN, EXAR_WDT_LDEV); | |
128 | } | |
129 | ||
130 | static void exar_wdt_arm(const struct wdt_priv *priv) | |
131 | { | |
132 | unsigned short rt_base = priv->wdt_res.start; | |
133 | ||
134 | /* write timeout value twice to arm watchdog */ | |
135 | outb(priv->timeout, rt_base + WDT_VAL); | |
136 | outb(priv->timeout, rt_base + WDT_VAL); | |
137 | } | |
138 | ||
139 | static void exar_wdt_disarm(const struct wdt_priv *priv) | |
140 | { | |
141 | unsigned short rt_base = priv->wdt_res.start; | |
142 | ||
143 | /* | |
144 | * use two accesses with different values to make sure | |
145 | * that a combination of a previous single access and | |
146 | * the ones below with the same value are not falsely | |
147 | * interpreted as "arm watchdog" | |
148 | */ | |
149 | outb(0xFF, rt_base + WDT_VAL); | |
150 | outb(0, rt_base + WDT_VAL); | |
151 | } | |
152 | ||
153 | static int exar_wdt_start(struct watchdog_device *wdog) | |
154 | { | |
155 | struct wdt_priv *priv = watchdog_get_drvdata(wdog); | |
156 | unsigned short rt_base = priv->wdt_res.start; | |
157 | ||
158 | spin_lock(&priv->io_lock); | |
159 | ||
160 | exar_wdt_disarm(priv); | |
161 | outb(priv->unit, rt_base + WDT_CTRL); | |
162 | exar_wdt_arm(priv); | |
163 | ||
164 | spin_unlock(&priv->io_lock); | |
165 | return 0; | |
166 | } | |
167 | ||
168 | static int exar_wdt_stop(struct watchdog_device *wdog) | |
169 | { | |
170 | struct wdt_priv *priv = watchdog_get_drvdata(wdog); | |
171 | ||
172 | spin_lock(&priv->io_lock); | |
173 | ||
174 | exar_wdt_disarm(priv); | |
175 | ||
176 | spin_unlock(&priv->io_lock); | |
177 | return 0; | |
178 | } | |
179 | ||
180 | static int exar_wdt_keepalive(struct watchdog_device *wdog) | |
181 | { | |
182 | struct wdt_priv *priv = watchdog_get_drvdata(wdog); | |
183 | unsigned short rt_base = priv->wdt_res.start; | |
184 | ||
185 | spin_lock(&priv->io_lock); | |
186 | ||
187 | /* reading the WDT_VAL reg will feed the watchdog */ | |
188 | inb(rt_base + WDT_VAL); | |
189 | ||
190 | spin_unlock(&priv->io_lock); | |
191 | return 0; | |
192 | } | |
193 | ||
194 | static int exar_wdt_set_timeout(struct watchdog_device *wdog, unsigned int t) | |
195 | { | |
196 | struct wdt_priv *priv = watchdog_get_drvdata(wdog); | |
197 | bool unit_min = false; | |
198 | ||
199 | /* | |
200 | * if new timeout is bigger then 255 seconds, change the | |
201 | * unit to minutes and round the timeout up to the next whole minute | |
202 | */ | |
203 | if (t > 255) { | |
204 | unit_min = true; | |
205 | t = DIV_ROUND_UP(t, 60); | |
206 | } | |
207 | ||
208 | /* save for later use in exar_wdt_start() */ | |
209 | priv->unit = unit_min ? WDT_UNITS_MIN : WDT_UNITS_SEC; | |
210 | priv->timeout = t; | |
211 | ||
212 | wdog->timeout = unit_min ? t * 60 : t; | |
213 | ||
214 | if (watchdog_hw_running(wdog)) | |
215 | exar_wdt_start(wdog); | |
216 | ||
217 | return 0; | |
218 | } | |
219 | ||
220 | static const struct watchdog_info exar_wdt_info = { | |
221 | .options = WDIOF_KEEPALIVEPING | | |
222 | WDIOF_SETTIMEOUT | | |
223 | WDIOF_MAGICCLOSE, | |
224 | .identity = "Exar/MaxLinear XR28V38x Watchdog", | |
225 | }; | |
226 | ||
227 | static const struct watchdog_ops exar_wdt_ops = { | |
228 | .owner = THIS_MODULE, | |
229 | .start = exar_wdt_start, | |
230 | .stop = exar_wdt_stop, | |
231 | .ping = exar_wdt_keepalive, | |
232 | .set_timeout = exar_wdt_set_timeout, | |
233 | }; | |
234 | ||
235 | static int exar_wdt_config(struct watchdog_device *wdog, | |
236 | const unsigned char conf) | |
237 | { | |
238 | struct wdt_priv *priv = watchdog_get_drvdata(wdog); | |
239 | int ret; | |
240 | ||
241 | ret = exar_sio_enter(priv->config_port, priv->enter_key); | |
242 | if (ret) | |
243 | return ret; | |
244 | ||
245 | exar_sio_select_wdt(priv->config_port); | |
246 | exar_sio_write(priv->config_port, EXAR_WDT, conf); | |
247 | ||
248 | exar_sio_exit(priv->config_port); | |
249 | ||
250 | return 0; | |
251 | } | |
252 | ||
253 | static int __init exar_wdt_probe(struct platform_device *pdev) | |
254 | { | |
255 | struct device *dev = &pdev->dev; | |
256 | struct wdt_priv *priv = dev->platform_data; | |
257 | struct watchdog_device *wdt_dev = &priv->wdt_dev; | |
258 | struct resource *res; | |
259 | int ret; | |
260 | ||
261 | res = platform_get_resource(pdev, IORESOURCE_IO, 0); | |
262 | if (!res) | |
263 | return -ENXIO; | |
264 | ||
265 | spin_lock_init(&priv->io_lock); | |
266 | ||
267 | wdt_dev->info = &exar_wdt_info; | |
268 | wdt_dev->ops = &exar_wdt_ops; | |
269 | wdt_dev->min_timeout = 1; | |
270 | wdt_dev->max_timeout = 255 * 60; | |
271 | ||
272 | watchdog_init_timeout(wdt_dev, timeout, NULL); | |
273 | watchdog_set_nowayout(wdt_dev, nowayout); | |
274 | watchdog_stop_on_reboot(wdt_dev); | |
275 | watchdog_stop_on_unregister(wdt_dev); | |
276 | watchdog_set_drvdata(wdt_dev, priv); | |
277 | ||
278 | ret = exar_wdt_config(wdt_dev, EXAR_WDT_DEF_CONF); | |
279 | if (ret) | |
280 | return ret; | |
281 | ||
282 | exar_wdt_set_timeout(wdt_dev, timeout); | |
283 | /* Make sure that the watchdog is not running */ | |
284 | exar_wdt_stop(wdt_dev); | |
285 | ||
286 | ret = devm_watchdog_register_device(dev, wdt_dev); | |
287 | if (ret) | |
288 | return ret; | |
289 | ||
290 | dev_info(dev, "XR28V%X WDT initialized. timeout=%d sec (nowayout=%d)\n", | |
291 | priv->did, timeout, nowayout); | |
292 | ||
293 | return 0; | |
294 | } | |
295 | ||
296 | static unsigned short __init exar_detect(const unsigned short config_port, | |
297 | const unsigned char key, | |
298 | unsigned short *rt_base) | |
299 | { | |
300 | int ret; | |
301 | unsigned short base = 0; | |
302 | unsigned short vid, did; | |
303 | ||
304 | ret = exar_sio_enter(config_port, key); | |
305 | if (ret) | |
306 | return 0; | |
307 | ||
308 | vid = exar_sio_read16(config_port, EXAR_VID); | |
309 | did = exar_sio_read16(config_port, EXAR_DID); | |
310 | ||
311 | /* check for the vendor and device IDs we currently know about */ | |
312 | if (vid == EXAR_VEN_ID && | |
313 | (did == EXAR_DEV_382 || | |
314 | did == EXAR_DEV_384)) { | |
315 | exar_sio_select_wdt(config_port); | |
316 | /* is device active? */ | |
317 | if (exar_sio_read(config_port, EXAR_ACT) == 0x01) | |
318 | base = exar_sio_read16(config_port, EXAR_RTBASE); | |
319 | } | |
320 | ||
321 | exar_sio_exit(config_port); | |
322 | ||
323 | if (base) { | |
324 | pr_debug("Found a XR28V%X WDT (conf: 0x%x / rt: 0x%04x)\n", | |
325 | did, config_port, base); | |
326 | *rt_base = base; | |
327 | return did; | |
328 | } | |
329 | ||
330 | return 0; | |
331 | } | |
332 | ||
333 | static struct platform_driver exar_wdt_driver = { | |
334 | .driver = { | |
335 | .name = DRV_NAME, | |
336 | }, | |
337 | }; | |
338 | ||
339 | static LIST_HEAD(pdev_list); | |
340 | ||
341 | static int __init exar_wdt_register(struct wdt_priv *priv, const int idx) | |
342 | { | |
343 | struct wdt_pdev_node *n; | |
344 | ||
345 | n = kzalloc(sizeof(*n), GFP_KERNEL); | |
346 | if (!n) | |
347 | return -ENOMEM; | |
348 | ||
349 | INIT_LIST_HEAD(&n->list); | |
350 | ||
351 | scnprintf((char *)n->name, sizeof(n->name), DRV_NAME ".%d", idx); | |
352 | priv->wdt_res.name = n->name; | |
353 | ||
354 | n->pdev = platform_device_register_resndata(NULL, DRV_NAME, idx, | |
355 | &priv->wdt_res, 1, | |
356 | priv, sizeof(*priv)); | |
357 | if (IS_ERR(n->pdev)) { | |
82ebbe65 MP |
358 | int err = PTR_ERR(n->pdev); |
359 | ||
81126222 | 360 | kfree(n); |
82ebbe65 | 361 | return err; |
81126222 DM |
362 | } |
363 | ||
364 | list_add_tail(&n->list, &pdev_list); | |
365 | ||
366 | return 0; | |
367 | } | |
368 | ||
369 | static void exar_wdt_unregister(void) | |
370 | { | |
371 | struct wdt_pdev_node *n, *t; | |
372 | ||
373 | list_for_each_entry_safe(n, t, &pdev_list, list) { | |
374 | platform_device_unregister(n->pdev); | |
375 | list_del(&n->list); | |
376 | kfree(n); | |
377 | } | |
378 | } | |
379 | ||
380 | static int __init exar_wdt_init(void) | |
381 | { | |
382 | int ret, i, j, idx = 0; | |
383 | ||
384 | /* search for active Exar watchdogs on all possible locations */ | |
385 | for (i = 0; i < ARRAY_SIZE(sio_config_ports); i++) { | |
386 | for (j = 0; j < ARRAY_SIZE(sio_enter_keys); j++) { | |
387 | unsigned short did, rt_base = 0; | |
388 | ||
389 | did = exar_detect(sio_config_ports[i], | |
390 | sio_enter_keys[j], | |
391 | &rt_base); | |
392 | ||
393 | if (did) { | |
394 | struct wdt_priv priv = { | |
395 | .wdt_res = DEFINE_RES_IO(rt_base, 2), | |
396 | .did = did, | |
397 | .config_port = sio_config_ports[i], | |
398 | .enter_key = sio_enter_keys[j], | |
399 | }; | |
400 | ||
401 | ret = exar_wdt_register(&priv, idx); | |
402 | if (!ret) | |
403 | idx++; | |
404 | } | |
405 | } | |
406 | } | |
407 | ||
408 | if (!idx) | |
409 | return -ENODEV; | |
410 | ||
411 | ret = platform_driver_probe(&exar_wdt_driver, exar_wdt_probe); | |
412 | if (ret) | |
413 | exar_wdt_unregister(); | |
414 | ||
415 | return ret; | |
416 | } | |
417 | ||
418 | static void __exit exar_wdt_exit(void) | |
419 | { | |
420 | exar_wdt_unregister(); | |
421 | platform_driver_unregister(&exar_wdt_driver); | |
422 | } | |
423 | ||
424 | module_init(exar_wdt_init); | |
425 | module_exit(exar_wdt_exit); | |
426 | ||
427 | MODULE_AUTHOR("David Müller <d.mueller@elsoft.ch>"); | |
428 | MODULE_DESCRIPTION("Exar/MaxLinear Watchdog Driver"); | |
429 | MODULE_LICENSE("GPL"); |