1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * ALi M7101 PMU Computer Watchdog Timer driver |
4 | * |
5 | * Based on w83877f_wdt.c by Scott Jennings <linuxdrivers@oro.net> |
6 | * and the Cobalt kernel WDT timer driver by Tim Hockin |
7 | * <thockin@cobaltnet.com> |
8 | * |
9 | * (c)2002 Steve Hill <steve@navaho.co.uk> |
10 | * |
11 | * This WDT driver is different from most other Linux WDT |
12 | * drivers in that the driver will ping the watchdog by itself, |
13 | * because this particular WDT has a very short timeout (1.6 |
14 | * seconds) and it would be insane to count on any userspace |
15 | * daemon always getting scheduled within that time frame. |
16 | * |
17 | * Additions: |
18 | * Aug 23, 2004 - Added use_gpio module parameter for use on revision a1d PMUs |
19 | * found on very old cobalt hardware. |
20 | * -- Mike Waychison <michael.waychison@sun.com> |
21 | */ |
22 | |
23 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
24 | |
25 | #include <linux/module.h> |
26 | #include <linux/moduleparam.h> |
27 | #include <linux/types.h> |
28 | #include <linux/timer.h> |
29 | #include <linux/miscdevice.h> |
30 | #include <linux/watchdog.h> |
31 | #include <linux/ioport.h> |
32 | #include <linux/notifier.h> |
33 | #include <linux/reboot.h> |
34 | #include <linux/init.h> |
35 | #include <linux/fs.h> |
36 | #include <linux/pci.h> |
37 | #include <linux/io.h> |
38 | #include <linux/uaccess.h> |
39 | |
40 | |
41 | #define WDT_ENABLE 0x9C |
42 | #define WDT_DISABLE 0x8C |
43 | |
44 | #define ALI_7101_WDT 0x92 |
45 | #define ALI_7101_GPIO 0x7D |
46 | #define ALI_7101_GPIO_O 0x7E |
47 | #define ALI_WDT_ARM 0x01 |
48 | |
49 | /* |
50 | * We're going to use a 1 second timeout. |
51 | * If we reset the watchdog every ~250ms we should be safe. */ |
52 | |
53 | #define WDT_INTERVAL (HZ/4+1) |
54 | |
55 | /* |
56 | * We must not require too good response from the userspace daemon. |
57 | * Here we require the userspace daemon to send us a heartbeat |
58 | * char to /dev/watchdog every 30 seconds. |
59 | */ |
60 | |
61 | #define WATCHDOG_TIMEOUT 30 /* 30 sec default timeout */ |
62 | /* in seconds, will be multiplied by HZ to get seconds to wait for a ping */ |
63 | static int timeout = WATCHDOG_TIMEOUT; |
64 | module_param(timeout, int, 0); |
65 | MODULE_PARM_DESC(timeout, |
66 | "Watchdog timeout in seconds. (1<=timeout<=3600, default=" |
67 | __MODULE_STRING(WATCHDOG_TIMEOUT) ")" ); |
68 | |
69 | static int use_gpio; /* Use the pic (for a1d revision alim7101) */ |
70 | module_param(use_gpio, int, 0); |
71 | MODULE_PARM_DESC(use_gpio, |
72 | "Use the gpio watchdog (required by old cobalt boards)." ); |
73 | |
74 | static void wdt_timer_ping(struct timer_list *); |
75 | static DEFINE_TIMER(timer, wdt_timer_ping); |
76 | static unsigned long next_heartbeat; |
77 | static unsigned long wdt_is_open; |
78 | static char wdt_expect_close; |
79 | static struct pci_dev *alim7101_pmu; |
80 | |
81 | static bool nowayout = WATCHDOG_NOWAYOUT; |
82 | module_param(nowayout, bool, 0); |
83 | MODULE_PARM_DESC(nowayout, |
84 | "Watchdog cannot be stopped once started (default=" |
85 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")" ); |
86 | |
87 | /* |
88 | * Whack the dog |
89 | */ |
90 | |
91 | static void wdt_timer_ping(struct timer_list *unused) |
92 | { |
93 | /* If we got a heartbeat pulse within the WDT_US_INTERVAL |
94 | * we agree to ping the WDT |
95 | */ |
96 | char tmp; |
97 | |
98 | if (time_before(jiffies, next_heartbeat)) { |
99 | /* Ping the WDT (this is actually a disarm/arm sequence) */ |
100 | pci_read_config_byte(dev: alim7101_pmu, where: 0x92, val: &tmp); |
101 | pci_write_config_byte(dev: alim7101_pmu, |
102 | ALI_7101_WDT, val: (tmp & ~ALI_WDT_ARM)); |
103 | pci_write_config_byte(dev: alim7101_pmu, |
104 | ALI_7101_WDT, val: (tmp | ALI_WDT_ARM)); |
105 | if (use_gpio) { |
106 | pci_read_config_byte(dev: alim7101_pmu, |
107 | ALI_7101_GPIO_O, val: &tmp); |
108 | pci_write_config_byte(dev: alim7101_pmu, |
109 | ALI_7101_GPIO_O, val: tmp | 0x20); |
110 | pci_write_config_byte(dev: alim7101_pmu, |
111 | ALI_7101_GPIO_O, val: tmp & ~0x20); |
112 | } |
113 | } else { |
114 | pr_warn("Heartbeat lost! Will not ping the watchdog\n" ); |
115 | } |
116 | /* Re-set the timer interval */ |
117 | mod_timer(timer: &timer, expires: jiffies + WDT_INTERVAL); |
118 | } |
119 | |
120 | /* |
121 | * Utility routines |
122 | */ |
123 | |
124 | static void wdt_change(int writeval) |
125 | { |
126 | char tmp; |
127 | |
128 | pci_read_config_byte(dev: alim7101_pmu, ALI_7101_WDT, val: &tmp); |
129 | if (writeval == WDT_ENABLE) { |
130 | pci_write_config_byte(dev: alim7101_pmu, |
131 | ALI_7101_WDT, val: (tmp | ALI_WDT_ARM)); |
132 | if (use_gpio) { |
133 | pci_read_config_byte(dev: alim7101_pmu, |
134 | ALI_7101_GPIO_O, val: &tmp); |
135 | pci_write_config_byte(dev: alim7101_pmu, |
136 | ALI_7101_GPIO_O, val: tmp & ~0x20); |
137 | } |
138 | |
139 | } else { |
140 | pci_write_config_byte(dev: alim7101_pmu, |
141 | ALI_7101_WDT, val: (tmp & ~ALI_WDT_ARM)); |
142 | if (use_gpio) { |
143 | pci_read_config_byte(dev: alim7101_pmu, |
144 | ALI_7101_GPIO_O, val: &tmp); |
145 | pci_write_config_byte(dev: alim7101_pmu, |
146 | ALI_7101_GPIO_O, val: tmp | 0x20); |
147 | } |
148 | } |
149 | } |
150 | |
151 | static void wdt_startup(void) |
152 | { |
153 | next_heartbeat = jiffies + (timeout * HZ); |
154 | |
155 | /* We must enable before we kick off the timer in case the timer |
156 | occurs as we ping it */ |
157 | |
158 | wdt_change(WDT_ENABLE); |
159 | |
160 | /* Start the timer */ |
161 | mod_timer(timer: &timer, expires: jiffies + WDT_INTERVAL); |
162 | |
163 | pr_info("Watchdog timer is now enabled\n" ); |
164 | } |
165 | |
166 | static void wdt_turnoff(void) |
167 | { |
168 | /* Stop the timer */ |
169 | del_timer_sync(timer: &timer); |
170 | wdt_change(WDT_DISABLE); |
171 | pr_info("Watchdog timer is now disabled...\n" ); |
172 | } |
173 | |
174 | static void wdt_keepalive(void) |
175 | { |
176 | /* user land ping */ |
177 | next_heartbeat = jiffies + (timeout * HZ); |
178 | } |
179 | |
180 | /* |
181 | * /dev/watchdog handling |
182 | */ |
183 | |
184 | static ssize_t fop_write(struct file *file, const char __user *buf, |
185 | size_t count, loff_t *ppos) |
186 | { |
187 | /* See if we got the magic character 'V' and reload the timer */ |
188 | if (count) { |
189 | if (!nowayout) { |
190 | size_t ofs; |
191 | |
192 | /* note: just in case someone wrote the magic character |
193 | * five months ago... */ |
194 | wdt_expect_close = 0; |
195 | |
196 | /* now scan */ |
197 | for (ofs = 0; ofs != count; ofs++) { |
198 | char c; |
199 | if (get_user(c, buf + ofs)) |
200 | return -EFAULT; |
201 | if (c == 'V') |
202 | wdt_expect_close = 42; |
203 | } |
204 | } |
205 | /* someone wrote to us, we should restart timer */ |
206 | wdt_keepalive(); |
207 | } |
208 | return count; |
209 | } |
210 | |
211 | static int fop_open(struct inode *inode, struct file *file) |
212 | { |
213 | /* Just in case we're already talking to someone... */ |
214 | if (test_and_set_bit(nr: 0, addr: &wdt_is_open)) |
215 | return -EBUSY; |
216 | /* Good, fire up the show */ |
217 | wdt_startup(); |
218 | return stream_open(inode, filp: file); |
219 | } |
220 | |
221 | static int fop_close(struct inode *inode, struct file *file) |
222 | { |
223 | if (wdt_expect_close == 42) |
224 | wdt_turnoff(); |
225 | else { |
226 | /* wim: shouldn't there be a: del_timer(&timer); */ |
227 | pr_crit("device file closed unexpectedly. Will not stop the WDT!\n" ); |
228 | } |
229 | clear_bit(nr: 0, addr: &wdt_is_open); |
230 | wdt_expect_close = 0; |
231 | return 0; |
232 | } |
233 | |
234 | static long fop_ioctl(struct file *file, unsigned int cmd, unsigned long arg) |
235 | { |
236 | void __user *argp = (void __user *)arg; |
237 | int __user *p = argp; |
238 | static const struct watchdog_info ident = { |
239 | .options = WDIOF_KEEPALIVEPING | WDIOF_SETTIMEOUT |
240 | | WDIOF_MAGICCLOSE, |
241 | .firmware_version = 1, |
242 | .identity = "ALiM7101" , |
243 | }; |
244 | |
245 | switch (cmd) { |
246 | case WDIOC_GETSUPPORT: |
247 | return copy_to_user(to: argp, from: &ident, n: sizeof(ident)) ? -EFAULT : 0; |
248 | case WDIOC_GETSTATUS: |
249 | case WDIOC_GETBOOTSTATUS: |
250 | return put_user(0, p); |
251 | case WDIOC_SETOPTIONS: |
252 | { |
253 | int new_options, retval = -EINVAL; |
254 | |
255 | if (get_user(new_options, p)) |
256 | return -EFAULT; |
257 | if (new_options & WDIOS_DISABLECARD) { |
258 | wdt_turnoff(); |
259 | retval = 0; |
260 | } |
261 | if (new_options & WDIOS_ENABLECARD) { |
262 | wdt_startup(); |
263 | retval = 0; |
264 | } |
265 | return retval; |
266 | } |
267 | case WDIOC_KEEPALIVE: |
268 | wdt_keepalive(); |
269 | return 0; |
270 | case WDIOC_SETTIMEOUT: |
271 | { |
272 | int new_timeout; |
273 | |
274 | if (get_user(new_timeout, p)) |
275 | return -EFAULT; |
276 | /* arbitrary upper limit */ |
277 | if (new_timeout < 1 || new_timeout > 3600) |
278 | return -EINVAL; |
279 | timeout = new_timeout; |
280 | wdt_keepalive(); |
281 | } |
282 | fallthrough; |
283 | case WDIOC_GETTIMEOUT: |
284 | return put_user(timeout, p); |
285 | default: |
286 | return -ENOTTY; |
287 | } |
288 | } |
289 | |
290 | static const struct file_operations wdt_fops = { |
291 | .owner = THIS_MODULE, |
292 | .llseek = no_llseek, |
293 | .write = fop_write, |
294 | .open = fop_open, |
295 | .release = fop_close, |
296 | .unlocked_ioctl = fop_ioctl, |
297 | .compat_ioctl = compat_ptr_ioctl, |
298 | }; |
299 | |
300 | static struct miscdevice wdt_miscdev = { |
301 | .minor = WATCHDOG_MINOR, |
302 | .name = "watchdog" , |
303 | .fops = &wdt_fops, |
304 | }; |
305 | |
306 | static int wdt_restart_handle(struct notifier_block *this, unsigned long mode, |
307 | void *cmd) |
308 | { |
309 | /* |
310 | * Cobalt devices have no way of rebooting themselves other |
311 | * than getting the watchdog to pull reset, so we restart the |
312 | * watchdog on reboot with no heartbeat. |
313 | */ |
314 | wdt_change(WDT_ENABLE); |
315 | |
316 | /* loop until the watchdog fires */ |
317 | while (true) |
318 | ; |
319 | |
320 | return NOTIFY_DONE; |
321 | } |
322 | |
323 | static struct notifier_block wdt_restart_handler = { |
324 | .notifier_call = wdt_restart_handle, |
325 | .priority = 128, |
326 | }; |
327 | |
328 | /* |
329 | * Notifier for system down |
330 | */ |
331 | |
332 | static int wdt_notify_sys(struct notifier_block *this, |
333 | unsigned long code, void *unused) |
334 | { |
335 | if (code == SYS_DOWN || code == SYS_HALT) |
336 | wdt_turnoff(); |
337 | |
338 | return NOTIFY_DONE; |
339 | } |
340 | |
341 | /* |
342 | * The WDT needs to learn about soft shutdowns in order to |
343 | * turn the timebomb registers off. |
344 | */ |
345 | |
346 | static struct notifier_block wdt_notifier = { |
347 | .notifier_call = wdt_notify_sys, |
348 | }; |
349 | |
350 | static void __exit alim7101_wdt_unload(void) |
351 | { |
352 | wdt_turnoff(); |
353 | /* Deregister */ |
354 | misc_deregister(misc: &wdt_miscdev); |
355 | unregister_reboot_notifier(&wdt_notifier); |
356 | unregister_restart_handler(&wdt_restart_handler); |
357 | pci_dev_put(dev: alim7101_pmu); |
358 | } |
359 | |
360 | static int __init alim7101_wdt_init(void) |
361 | { |
362 | int rc = -EBUSY; |
363 | struct pci_dev *ali1543_south; |
364 | char tmp; |
365 | |
366 | pr_info("Steve Hill <steve@navaho.co.uk>\n" ); |
367 | alim7101_pmu = pci_get_device(PCI_VENDOR_ID_AL, PCI_DEVICE_ID_AL_M7101, |
368 | NULL); |
369 | if (!alim7101_pmu) { |
370 | pr_info("ALi M7101 PMU not present - WDT not set\n" ); |
371 | return -EBUSY; |
372 | } |
373 | |
374 | /* Set the WDT in the PMU to 1 second */ |
375 | pci_write_config_byte(dev: alim7101_pmu, ALI_7101_WDT, val: 0x02); |
376 | |
377 | ali1543_south = pci_get_device(PCI_VENDOR_ID_AL, PCI_DEVICE_ID_AL_M1533, |
378 | NULL); |
379 | if (!ali1543_south) { |
380 | pr_info("ALi 1543 South-Bridge not present - WDT not set\n" ); |
381 | goto err_out; |
382 | } |
383 | pci_read_config_byte(dev: ali1543_south, where: 0x5e, val: &tmp); |
384 | pci_dev_put(dev: ali1543_south); |
385 | if ((tmp & 0x1e) == 0x00) { |
386 | if (!use_gpio) { |
387 | pr_info("Detected old alim7101 revision 'a1d'. If this is a cobalt board, set the 'use_gpio' module parameter.\n" ); |
388 | goto err_out; |
389 | } |
390 | nowayout = 1; |
391 | } else if ((tmp & 0x1e) != 0x12 && (tmp & 0x1e) != 0x00) { |
392 | pr_info("ALi 1543 South-Bridge does not have the correct revision number (???1001?) - WDT not set\n" ); |
393 | goto err_out; |
394 | } |
395 | |
396 | if (timeout < 1 || timeout > 3600) { |
397 | /* arbitrary upper limit */ |
398 | timeout = WATCHDOG_TIMEOUT; |
399 | pr_info("timeout value must be 1 <= x <= 3600, using %d\n" , |
400 | timeout); |
401 | } |
402 | |
403 | rc = register_reboot_notifier(&wdt_notifier); |
404 | if (rc) { |
405 | pr_err("cannot register reboot notifier (err=%d)\n" , rc); |
406 | goto err_out; |
407 | } |
408 | |
409 | rc = register_restart_handler(&wdt_restart_handler); |
410 | if (rc) { |
411 | pr_err("cannot register restart handler (err=%d)\n" , rc); |
412 | goto err_out_reboot; |
413 | } |
414 | |
415 | rc = misc_register(misc: &wdt_miscdev); |
416 | if (rc) { |
417 | pr_err("cannot register miscdev on minor=%d (err=%d)\n" , |
418 | wdt_miscdev.minor, rc); |
419 | goto err_out_restart; |
420 | } |
421 | |
422 | if (nowayout) |
423 | __module_get(THIS_MODULE); |
424 | |
425 | pr_info("WDT driver for ALi M7101 initialised. timeout=%d sec (nowayout=%d)\n" , |
426 | timeout, nowayout); |
427 | return 0; |
428 | |
429 | err_out_restart: |
430 | unregister_restart_handler(&wdt_restart_handler); |
431 | err_out_reboot: |
432 | unregister_reboot_notifier(&wdt_notifier); |
433 | err_out: |
434 | pci_dev_put(dev: alim7101_pmu); |
435 | return rc; |
436 | } |
437 | |
438 | module_init(alim7101_wdt_init); |
439 | module_exit(alim7101_wdt_unload); |
440 | |
441 | static const struct pci_device_id alim7101_pci_tbl[] __used = { |
442 | { PCI_DEVICE(PCI_VENDOR_ID_AL, PCI_DEVICE_ID_AL_M1533) }, |
443 | { PCI_DEVICE(PCI_VENDOR_ID_AL, PCI_DEVICE_ID_AL_M7101) }, |
444 | { } |
445 | }; |
446 | |
447 | MODULE_DEVICE_TABLE(pci, alim7101_pci_tbl); |
448 | |
449 | MODULE_AUTHOR("Steve Hill" ); |
450 | MODULE_DESCRIPTION("ALi M7101 PMU Computer Watchdog Timer driver" ); |
451 | MODULE_LICENSE("GPL" ); |
452 | |