1 | // SPDX-License-Identifier: GPL-2.0 |
2 | /* |
3 | * NANO7240 SBC Watchdog device driver |
4 | * |
5 | * Based on w83877f.c by Scott Jennings, |
6 | * |
7 | * (c) Copyright 2007 Gilles GIGAN <gilles.gigan@jcu.edu.au> |
8 | */ |
9 | |
10 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
11 | |
12 | #include <linux/fs.h> |
13 | #include <linux/init.h> |
14 | #include <linux/ioport.h> |
15 | #include <linux/jiffies.h> |
16 | #include <linux/module.h> |
17 | #include <linux/moduleparam.h> |
18 | #include <linux/miscdevice.h> |
19 | #include <linux/notifier.h> |
20 | #include <linux/reboot.h> |
21 | #include <linux/types.h> |
22 | #include <linux/watchdog.h> |
23 | #include <linux/io.h> |
24 | #include <linux/uaccess.h> |
25 | #include <linux/atomic.h> |
26 | |
27 | #define SBC7240_ENABLE_PORT 0x443 |
28 | #define SBC7240_DISABLE_PORT 0x043 |
29 | #define SBC7240_SET_TIMEOUT_PORT SBC7240_ENABLE_PORT |
30 | #define SBC7240_MAGIC_CHAR 'V' |
31 | |
32 | #define SBC7240_TIMEOUT 30 |
33 | #define SBC7240_MAX_TIMEOUT 255 |
34 | static int timeout = SBC7240_TIMEOUT; /* in seconds */ |
35 | module_param(timeout, int, 0); |
36 | MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds. (1<=timeout<=" |
37 | __MODULE_STRING(SBC7240_MAX_TIMEOUT) ", default=" |
38 | __MODULE_STRING(SBC7240_TIMEOUT) ")" ); |
39 | |
40 | static bool nowayout = WATCHDOG_NOWAYOUT; |
41 | module_param(nowayout, bool, 0); |
42 | MODULE_PARM_DESC(nowayout, "Disable watchdog when closing device file" ); |
43 | |
44 | #define SBC7240_OPEN_STATUS_BIT 0 |
45 | #define SBC7240_ENABLED_STATUS_BIT 1 |
46 | #define SBC7240_EXPECT_CLOSE_STATUS_BIT 2 |
47 | static unsigned long wdt_status; |
48 | |
49 | /* |
50 | * Utility routines |
51 | */ |
52 | |
53 | static void wdt_disable(void) |
54 | { |
55 | /* disable the watchdog */ |
56 | if (test_and_clear_bit(SBC7240_ENABLED_STATUS_BIT, addr: &wdt_status)) { |
57 | inb_p(SBC7240_DISABLE_PORT); |
58 | pr_info("Watchdog timer is now disabled\n" ); |
59 | } |
60 | } |
61 | |
62 | static void wdt_enable(void) |
63 | { |
64 | /* enable the watchdog */ |
65 | if (!test_and_set_bit(SBC7240_ENABLED_STATUS_BIT, addr: &wdt_status)) { |
66 | inb_p(SBC7240_ENABLE_PORT); |
67 | pr_info("Watchdog timer is now enabled\n" ); |
68 | } |
69 | } |
70 | |
71 | static int wdt_set_timeout(int t) |
72 | { |
73 | if (t < 1 || t > SBC7240_MAX_TIMEOUT) { |
74 | pr_err("timeout value must be 1<=x<=%d\n" , SBC7240_MAX_TIMEOUT); |
75 | return -1; |
76 | } |
77 | /* set the timeout */ |
78 | outb_p(value: (unsigned)t, SBC7240_SET_TIMEOUT_PORT); |
79 | timeout = t; |
80 | pr_info("timeout set to %d seconds\n" , t); |
81 | return 0; |
82 | } |
83 | |
84 | /* Whack the dog */ |
85 | static inline void wdt_keepalive(void) |
86 | { |
87 | if (test_bit(SBC7240_ENABLED_STATUS_BIT, &wdt_status)) |
88 | inb_p(SBC7240_ENABLE_PORT); |
89 | } |
90 | |
91 | /* |
92 | * /dev/watchdog handling |
93 | */ |
94 | static ssize_t fop_write(struct file *file, const char __user *buf, |
95 | size_t count, loff_t *ppos) |
96 | { |
97 | size_t i; |
98 | char c; |
99 | |
100 | if (count) { |
101 | if (!nowayout) { |
102 | clear_bit(SBC7240_EXPECT_CLOSE_STATUS_BIT, |
103 | addr: &wdt_status); |
104 | |
105 | /* is there a magic char ? */ |
106 | for (i = 0; i != count; i++) { |
107 | if (get_user(c, buf + i)) |
108 | return -EFAULT; |
109 | if (c == SBC7240_MAGIC_CHAR) { |
110 | set_bit(SBC7240_EXPECT_CLOSE_STATUS_BIT, |
111 | addr: &wdt_status); |
112 | break; |
113 | } |
114 | } |
115 | } |
116 | |
117 | wdt_keepalive(); |
118 | } |
119 | |
120 | return count; |
121 | } |
122 | |
123 | static int fop_open(struct inode *inode, struct file *file) |
124 | { |
125 | if (test_and_set_bit(SBC7240_OPEN_STATUS_BIT, addr: &wdt_status)) |
126 | return -EBUSY; |
127 | |
128 | wdt_enable(); |
129 | |
130 | return stream_open(inode, filp: file); |
131 | } |
132 | |
133 | static int fop_close(struct inode *inode, struct file *file) |
134 | { |
135 | if (test_and_clear_bit(SBC7240_EXPECT_CLOSE_STATUS_BIT, addr: &wdt_status) |
136 | || !nowayout) { |
137 | wdt_disable(); |
138 | } else { |
139 | pr_crit("Unexpected close, not stopping watchdog!\n" ); |
140 | wdt_keepalive(); |
141 | } |
142 | |
143 | clear_bit(SBC7240_OPEN_STATUS_BIT, addr: &wdt_status); |
144 | return 0; |
145 | } |
146 | |
147 | static const struct watchdog_info ident = { |
148 | .options = WDIOF_KEEPALIVEPING| |
149 | WDIOF_SETTIMEOUT| |
150 | WDIOF_MAGICCLOSE, |
151 | .firmware_version = 1, |
152 | .identity = "SBC7240" , |
153 | }; |
154 | |
155 | |
156 | static long fop_ioctl(struct file *file, unsigned int cmd, unsigned long arg) |
157 | { |
158 | switch (cmd) { |
159 | case WDIOC_GETSUPPORT: |
160 | return copy_to_user(to: (void __user *)arg, from: &ident, n: sizeof(ident)) |
161 | ? -EFAULT : 0; |
162 | case WDIOC_GETSTATUS: |
163 | case WDIOC_GETBOOTSTATUS: |
164 | return put_user(0, (int __user *)arg); |
165 | case WDIOC_SETOPTIONS: |
166 | { |
167 | int options; |
168 | int retval = -EINVAL; |
169 | |
170 | if (get_user(options, (int __user *)arg)) |
171 | return -EFAULT; |
172 | |
173 | if (options & WDIOS_DISABLECARD) { |
174 | wdt_disable(); |
175 | retval = 0; |
176 | } |
177 | |
178 | if (options & WDIOS_ENABLECARD) { |
179 | wdt_enable(); |
180 | retval = 0; |
181 | } |
182 | |
183 | return retval; |
184 | } |
185 | case WDIOC_KEEPALIVE: |
186 | wdt_keepalive(); |
187 | return 0; |
188 | case WDIOC_SETTIMEOUT: |
189 | { |
190 | int new_timeout; |
191 | |
192 | if (get_user(new_timeout, (int __user *)arg)) |
193 | return -EFAULT; |
194 | |
195 | if (wdt_set_timeout(t: new_timeout)) |
196 | return -EINVAL; |
197 | } |
198 | fallthrough; |
199 | case WDIOC_GETTIMEOUT: |
200 | return put_user(timeout, (int __user *)arg); |
201 | default: |
202 | return -ENOTTY; |
203 | } |
204 | } |
205 | |
206 | static const struct file_operations wdt_fops = { |
207 | .owner = THIS_MODULE, |
208 | .llseek = no_llseek, |
209 | .write = fop_write, |
210 | .open = fop_open, |
211 | .release = fop_close, |
212 | .unlocked_ioctl = fop_ioctl, |
213 | .compat_ioctl = compat_ptr_ioctl, |
214 | }; |
215 | |
216 | static struct miscdevice wdt_miscdev = { |
217 | .minor = WATCHDOG_MINOR, |
218 | .name = "watchdog" , |
219 | .fops = &wdt_fops, |
220 | }; |
221 | |
222 | /* |
223 | * Notifier for system down |
224 | */ |
225 | |
226 | static int wdt_notify_sys(struct notifier_block *this, unsigned long code, |
227 | void *unused) |
228 | { |
229 | if (code == SYS_DOWN || code == SYS_HALT) |
230 | wdt_disable(); |
231 | return NOTIFY_DONE; |
232 | } |
233 | |
234 | static struct notifier_block wdt_notifier = { |
235 | .notifier_call = wdt_notify_sys, |
236 | }; |
237 | |
238 | static void __exit sbc7240_wdt_unload(void) |
239 | { |
240 | pr_info("Removing watchdog\n" ); |
241 | misc_deregister(misc: &wdt_miscdev); |
242 | |
243 | unregister_reboot_notifier(&wdt_notifier); |
244 | release_region(SBC7240_ENABLE_PORT, 1); |
245 | } |
246 | |
247 | static int __init sbc7240_wdt_init(void) |
248 | { |
249 | int rc = -EBUSY; |
250 | |
251 | if (!request_region(SBC7240_ENABLE_PORT, 1, "SBC7240 WDT" )) { |
252 | pr_err("I/O address 0x%04x already in use\n" , |
253 | SBC7240_ENABLE_PORT); |
254 | rc = -EIO; |
255 | goto err_out; |
256 | } |
257 | |
258 | /* The IO port 0x043 used to disable the watchdog |
259 | * is already claimed by the system timer, so we |
260 | * can't request_region() it ...*/ |
261 | |
262 | if (timeout < 1 || timeout > SBC7240_MAX_TIMEOUT) { |
263 | timeout = SBC7240_TIMEOUT; |
264 | pr_info("timeout value must be 1<=x<=%d, using %d\n" , |
265 | SBC7240_MAX_TIMEOUT, timeout); |
266 | } |
267 | wdt_set_timeout(t: timeout); |
268 | wdt_disable(); |
269 | |
270 | rc = register_reboot_notifier(&wdt_notifier); |
271 | if (rc) { |
272 | pr_err("cannot register reboot notifier (err=%d)\n" , rc); |
273 | goto err_out_region; |
274 | } |
275 | |
276 | rc = misc_register(misc: &wdt_miscdev); |
277 | if (rc) { |
278 | pr_err("cannot register miscdev on minor=%d (err=%d)\n" , |
279 | wdt_miscdev.minor, rc); |
280 | goto err_out_reboot_notifier; |
281 | } |
282 | |
283 | pr_info("Watchdog driver for SBC7240 initialised (nowayout=%d)\n" , |
284 | nowayout); |
285 | |
286 | return 0; |
287 | |
288 | err_out_reboot_notifier: |
289 | unregister_reboot_notifier(&wdt_notifier); |
290 | err_out_region: |
291 | release_region(SBC7240_ENABLE_PORT, 1); |
292 | err_out: |
293 | return rc; |
294 | } |
295 | |
296 | module_init(sbc7240_wdt_init); |
297 | module_exit(sbc7240_wdt_unload); |
298 | |
299 | MODULE_AUTHOR("Gilles Gigan" ); |
300 | MODULE_DESCRIPTION("Watchdog device driver for single board" |
301 | " computers EPIC Nano 7240 from iEi" ); |
302 | MODULE_LICENSE("GPL" ); |
303 | |