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(value: key, port: config_port); |
89 | outb(value: key, port: 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, port: 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(value: reg, port: config_port); |
104 | return inb(port: 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(value: reg, port: config_port); |
111 | outb(value: val, port: 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: 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(value: priv->timeout, port: rt_base + WDT_VAL); |
136 | outb(value: priv->timeout, port: 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(value: 0xFF, port: rt_base + WDT_VAL); |
150 | outb(value: 0, port: 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(wdd: wdog); |
156 | unsigned short rt_base = priv->wdt_res.start; |
157 | |
158 | spin_lock(lock: &priv->io_lock); |
159 | |
160 | exar_wdt_disarm(priv); |
161 | outb(value: priv->unit, port: rt_base + WDT_CTRL); |
162 | exar_wdt_arm(priv); |
163 | |
164 | spin_unlock(lock: &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(wdd: wdog); |
171 | |
172 | spin_lock(lock: &priv->io_lock); |
173 | |
174 | exar_wdt_disarm(priv); |
175 | |
176 | spin_unlock(lock: &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(wdd: wdog); |
183 | unsigned short rt_base = priv->wdt_res.start; |
184 | |
185 | spin_lock(lock: &priv->io_lock); |
186 | |
187 | /* reading the WDT_VAL reg will feed the watchdog */ |
188 | inb(port: rt_base + WDT_VAL); |
189 | |
190 | spin_unlock(lock: &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(wdd: 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(wdd: 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(wdd: wdog); |
239 | int ret; |
240 | |
241 | ret = exar_sio_enter(config_port: priv->config_port, key: priv->enter_key); |
242 | if (ret) |
243 | return ret; |
244 | |
245 | exar_sio_select_wdt(config_port: priv->config_port); |
246 | exar_sio_write(config_port: priv->config_port, EXAR_WDT, val: conf); |
247 | |
248 | exar_sio_exit(config_port: 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(wdd: wdt_dev, timeout_parm: timeout, NULL); |
273 | watchdog_set_nowayout(wdd: wdt_dev, nowayout); |
274 | watchdog_stop_on_reboot(wdd: wdt_dev); |
275 | watchdog_stop_on_unregister(wdd: wdt_dev); |
276 | watchdog_set_drvdata(wdd: wdt_dev, data: priv); |
277 | |
278 | ret = exar_wdt_config(wdog: wdt_dev, EXAR_WDT_DEF_CONF); |
279 | if (ret) |
280 | return ret; |
281 | |
282 | exar_wdt_set_timeout(wdog: wdt_dev, t: timeout); |
283 | /* Make sure that the watchdog is not running */ |
284 | exar_wdt_stop(wdog: 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(size: sizeof(*n), GFP_KERNEL); |
346 | if (!n) |
347 | return -ENOMEM; |
348 | |
349 | INIT_LIST_HEAD(list: &n->list); |
350 | |
351 | scnprintf(buf: (char *)n->name, size: 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, id: idx, |
355 | res: &priv->wdt_res, num: 1, |
356 | data: priv, size: sizeof(*priv)); |
357 | if (IS_ERR(ptr: n->pdev)) { |
358 | int err = PTR_ERR(ptr: n->pdev); |
359 | |
360 | kfree(objp: n); |
361 | return err; |
362 | } |
363 | |
364 | list_add_tail(new: &n->list, head: &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(entry: &n->list); |
376 | kfree(objp: 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(config_port: sio_config_ports[i], |
390 | key: sio_enter_keys[j], |
391 | rt_base: &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: &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" ); |
430 | |