Commit | Line | Data |
---|---|---|
f865c352 PC |
1 | /* |
2 | * Copyright (C) 2010, Paul Cercueil <paul@crapouillou.net> | |
3 | * JZ4740 Watchdog driver | |
4 | * | |
5 | * This program is free software; you can redistribute it and/or modify it | |
6 | * under the terms of the GNU General Public License as published by the | |
7 | * Free Software Foundation; either version 2 of the License, or (at your | |
8 | * option) any later version. | |
9 | * | |
10 | * You should have received a copy of the GNU General Public License along | |
11 | * with this program; if not, write to the Free Software Foundation, Inc., | |
12 | * 675 Mass Ave, Cambridge, MA 02139, USA. | |
13 | * | |
14 | */ | |
15 | ||
16 | #include <linux/module.h> | |
17 | #include <linux/moduleparam.h> | |
18 | #include <linux/types.h> | |
19 | #include <linux/kernel.h> | |
20 | #include <linux/fs.h> | |
21 | #include <linux/miscdevice.h> | |
22 | #include <linux/watchdog.h> | |
23 | #include <linux/init.h> | |
24 | #include <linux/bitops.h> | |
25 | #include <linux/platform_device.h> | |
26 | #include <linux/spinlock.h> | |
27 | #include <linux/uaccess.h> | |
28 | #include <linux/io.h> | |
29 | #include <linux/device.h> | |
30 | #include <linux/clk.h> | |
31 | #include <linux/slab.h> | |
32 | ||
33 | #include <asm/mach-jz4740/timer.h> | |
34 | ||
35 | #define JZ_REG_WDT_TIMER_DATA 0x0 | |
36 | #define JZ_REG_WDT_COUNTER_ENABLE 0x4 | |
37 | #define JZ_REG_WDT_TIMER_COUNTER 0x8 | |
38 | #define JZ_REG_WDT_TIMER_CONTROL 0xC | |
39 | ||
40 | #define JZ_WDT_CLOCK_PCLK 0x1 | |
41 | #define JZ_WDT_CLOCK_RTC 0x2 | |
42 | #define JZ_WDT_CLOCK_EXT 0x4 | |
43 | ||
44 | #define WDT_IN_USE 0 | |
45 | #define WDT_OK_TO_CLOSE 1 | |
46 | ||
47 | #define JZ_WDT_CLOCK_DIV_SHIFT 3 | |
48 | ||
49 | #define JZ_WDT_CLOCK_DIV_1 (0 << JZ_WDT_CLOCK_DIV_SHIFT) | |
50 | #define JZ_WDT_CLOCK_DIV_4 (1 << JZ_WDT_CLOCK_DIV_SHIFT) | |
51 | #define JZ_WDT_CLOCK_DIV_16 (2 << JZ_WDT_CLOCK_DIV_SHIFT) | |
52 | #define JZ_WDT_CLOCK_DIV_64 (3 << JZ_WDT_CLOCK_DIV_SHIFT) | |
53 | #define JZ_WDT_CLOCK_DIV_256 (4 << JZ_WDT_CLOCK_DIV_SHIFT) | |
54 | #define JZ_WDT_CLOCK_DIV_1024 (5 << JZ_WDT_CLOCK_DIV_SHIFT) | |
55 | ||
56 | #define DEFAULT_HEARTBEAT 5 | |
57 | #define MAX_HEARTBEAT 2048 | |
58 | ||
59 | static struct { | |
60 | void __iomem *base; | |
61 | struct resource *mem; | |
62 | struct clk *rtc_clk; | |
63 | unsigned long status; | |
64 | } jz4740_wdt; | |
65 | ||
66 | static int heartbeat = DEFAULT_HEARTBEAT; | |
67 | ||
68 | ||
69 | static void jz4740_wdt_service(void) | |
70 | { | |
71 | writew(0x0, jz4740_wdt.base + JZ_REG_WDT_TIMER_COUNTER); | |
72 | } | |
73 | ||
74 | static void jz4740_wdt_set_heartbeat(int new_heartbeat) | |
75 | { | |
76 | unsigned int rtc_clk_rate; | |
77 | unsigned int timeout_value; | |
78 | unsigned short clock_div = JZ_WDT_CLOCK_DIV_1; | |
79 | ||
80 | heartbeat = new_heartbeat; | |
81 | ||
82 | rtc_clk_rate = clk_get_rate(jz4740_wdt.rtc_clk); | |
83 | ||
84 | timeout_value = rtc_clk_rate * heartbeat; | |
85 | while (timeout_value > 0xffff) { | |
86 | if (clock_div == JZ_WDT_CLOCK_DIV_1024) { | |
87 | /* Requested timeout too high; | |
88 | * use highest possible value. */ | |
89 | timeout_value = 0xffff; | |
90 | break; | |
91 | } | |
92 | timeout_value >>= 2; | |
93 | clock_div += (1 << JZ_WDT_CLOCK_DIV_SHIFT); | |
94 | } | |
95 | ||
96 | writeb(0x0, jz4740_wdt.base + JZ_REG_WDT_COUNTER_ENABLE); | |
97 | writew(clock_div, jz4740_wdt.base + JZ_REG_WDT_TIMER_CONTROL); | |
98 | ||
99 | writew((u16)timeout_value, jz4740_wdt.base + JZ_REG_WDT_TIMER_DATA); | |
100 | writew(0x0, jz4740_wdt.base + JZ_REG_WDT_TIMER_COUNTER); | |
101 | writew(clock_div | JZ_WDT_CLOCK_RTC, | |
102 | jz4740_wdt.base + JZ_REG_WDT_TIMER_CONTROL); | |
103 | ||
104 | writeb(0x1, jz4740_wdt.base + JZ_REG_WDT_COUNTER_ENABLE); | |
105 | } | |
106 | ||
107 | static void jz4740_wdt_enable(void) | |
108 | { | |
109 | jz4740_timer_enable_watchdog(); | |
110 | jz4740_wdt_set_heartbeat(heartbeat); | |
111 | } | |
112 | ||
113 | static void jz4740_wdt_disable(void) | |
114 | { | |
115 | jz4740_timer_disable_watchdog(); | |
116 | writeb(0x0, jz4740_wdt.base + JZ_REG_WDT_COUNTER_ENABLE); | |
117 | } | |
118 | ||
119 | static int jz4740_wdt_open(struct inode *inode, struct file *file) | |
120 | { | |
121 | if (test_and_set_bit(WDT_IN_USE, &jz4740_wdt.status)) | |
122 | return -EBUSY; | |
123 | ||
124 | jz4740_wdt_enable(); | |
125 | ||
126 | return nonseekable_open(inode, file); | |
127 | } | |
128 | ||
129 | static ssize_t jz4740_wdt_write(struct file *file, const char *data, | |
130 | size_t len, loff_t *ppos) | |
131 | { | |
132 | if (len) { | |
742e4b63 | 133 | size_t i; |
f865c352 | 134 | |
742e4b63 WVS |
135 | clear_bit(WDT_OK_TO_CLOSE, &jz4740_wdt.status); |
136 | for (i = 0; i != len; i++) { | |
137 | char c; | |
138 | ||
139 | if (get_user(c, data + i)) | |
140 | return -EFAULT; | |
141 | ||
142 | if (c == 'V') | |
143 | set_bit(WDT_OK_TO_CLOSE, &jz4740_wdt.status); | |
144 | } | |
f865c352 PC |
145 | jz4740_wdt_service(); |
146 | } | |
147 | ||
148 | return len; | |
149 | } | |
150 | ||
151 | static const struct watchdog_info ident = { | |
152 | .options = WDIOF_KEEPALIVEPING, | |
153 | .identity = "jz4740 Watchdog", | |
154 | }; | |
155 | ||
156 | static long jz4740_wdt_ioctl(struct file *file, | |
157 | unsigned int cmd, unsigned long arg) | |
158 | { | |
159 | int ret = -ENOTTY; | |
160 | int heartbeat_seconds; | |
161 | ||
162 | switch (cmd) { | |
163 | case WDIOC_GETSUPPORT: | |
164 | ret = copy_to_user((struct watchdog_info *)arg, &ident, | |
165 | sizeof(ident)) ? -EFAULT : 0; | |
166 | break; | |
167 | ||
168 | case WDIOC_GETSTATUS: | |
169 | case WDIOC_GETBOOTSTATUS: | |
170 | ret = put_user(0, (int *)arg); | |
171 | break; | |
172 | ||
173 | case WDIOC_KEEPALIVE: | |
174 | jz4740_wdt_service(); | |
175 | return 0; | |
176 | ||
177 | case WDIOC_SETTIMEOUT: | |
178 | if (get_user(heartbeat_seconds, (int __user *)arg)) | |
179 | return -EFAULT; | |
180 | ||
181 | jz4740_wdt_set_heartbeat(heartbeat_seconds); | |
182 | return 0; | |
183 | ||
184 | case WDIOC_GETTIMEOUT: | |
185 | return put_user(heartbeat, (int *)arg); | |
186 | ||
187 | default: | |
188 | break; | |
189 | } | |
190 | ||
191 | return ret; | |
192 | } | |
193 | ||
194 | static int jz4740_wdt_release(struct inode *inode, struct file *file) | |
195 | { | |
196 | jz4740_wdt_service(); | |
197 | ||
198 | if (test_and_clear_bit(WDT_OK_TO_CLOSE, &jz4740_wdt.status)) | |
199 | jz4740_wdt_disable(); | |
200 | ||
201 | clear_bit(WDT_IN_USE, &jz4740_wdt.status); | |
202 | return 0; | |
203 | } | |
204 | ||
205 | static const struct file_operations jz4740_wdt_fops = { | |
206 | .owner = THIS_MODULE, | |
207 | .llseek = no_llseek, | |
208 | .write = jz4740_wdt_write, | |
209 | .unlocked_ioctl = jz4740_wdt_ioctl, | |
210 | .open = jz4740_wdt_open, | |
211 | .release = jz4740_wdt_release, | |
212 | }; | |
213 | ||
214 | static struct miscdevice jz4740_wdt_miscdev = { | |
215 | .minor = WATCHDOG_MINOR, | |
216 | .name = "watchdog", | |
217 | .fops = &jz4740_wdt_fops, | |
218 | }; | |
219 | ||
220 | static int __devinit jz4740_wdt_probe(struct platform_device *pdev) | |
221 | { | |
222 | int ret = 0, size; | |
223 | struct resource *res; | |
224 | struct device *dev = &pdev->dev; | |
225 | ||
226 | res = platform_get_resource(pdev, IORESOURCE_MEM, 0); | |
227 | if (res == NULL) { | |
228 | dev_err(dev, "failed to get memory region resource\n"); | |
229 | return -ENXIO; | |
230 | } | |
231 | ||
232 | size = resource_size(res); | |
233 | jz4740_wdt.mem = request_mem_region(res->start, size, pdev->name); | |
234 | if (jz4740_wdt.mem == NULL) { | |
235 | dev_err(dev, "failed to get memory region\n"); | |
236 | return -EBUSY; | |
237 | } | |
238 | ||
239 | jz4740_wdt.base = ioremap_nocache(res->start, size); | |
240 | if (jz4740_wdt.base == NULL) { | |
241 | dev_err(dev, "failed to map memory region\n"); | |
242 | ret = -EBUSY; | |
243 | goto err_release_region; | |
244 | } | |
245 | ||
246 | jz4740_wdt.rtc_clk = clk_get(NULL, "rtc"); | |
247 | if (IS_ERR(jz4740_wdt.rtc_clk)) { | |
248 | dev_err(dev, "cannot find RTC clock\n"); | |
249 | ret = PTR_ERR(jz4740_wdt.rtc_clk); | |
250 | goto err_iounmap; | |
251 | } | |
252 | ||
253 | ret = misc_register(&jz4740_wdt_miscdev); | |
254 | if (ret < 0) { | |
255 | dev_err(dev, "cannot register misc device\n"); | |
256 | goto err_disable_clk; | |
257 | } | |
258 | ||
259 | return 0; | |
260 | ||
261 | err_disable_clk: | |
262 | clk_put(jz4740_wdt.rtc_clk); | |
263 | err_iounmap: | |
264 | iounmap(jz4740_wdt.base); | |
265 | err_release_region: | |
266 | release_mem_region(jz4740_wdt.mem->start, | |
267 | resource_size(jz4740_wdt.mem)); | |
268 | return ret; | |
269 | } | |
270 | ||
271 | ||
272 | static int __devexit jz4740_wdt_remove(struct platform_device *pdev) | |
273 | { | |
274 | jz4740_wdt_disable(); | |
275 | misc_deregister(&jz4740_wdt_miscdev); | |
276 | clk_put(jz4740_wdt.rtc_clk); | |
277 | ||
278 | iounmap(jz4740_wdt.base); | |
279 | jz4740_wdt.base = NULL; | |
280 | ||
281 | release_mem_region(jz4740_wdt.mem->start, | |
282 | resource_size(jz4740_wdt.mem)); | |
283 | jz4740_wdt.mem = NULL; | |
284 | ||
285 | return 0; | |
286 | } | |
287 | ||
288 | ||
289 | static struct platform_driver jz4740_wdt_driver = { | |
290 | .probe = jz4740_wdt_probe, | |
291 | .remove = __devexit_p(jz4740_wdt_remove), | |
292 | .driver = { | |
293 | .name = "jz4740-wdt", | |
294 | .owner = THIS_MODULE, | |
295 | }, | |
296 | }; | |
297 | ||
b8ec6118 | 298 | module_platform_driver(jz4740_wdt_driver); |
f865c352 PC |
299 | |
300 | MODULE_AUTHOR("Paul Cercueil <paul@crapouillou.net>"); | |
301 | MODULE_DESCRIPTION("jz4740 Watchdog Driver"); | |
302 | ||
303 | module_param(heartbeat, int, 0); | |
304 | MODULE_PARM_DESC(heartbeat, | |
305 | "Watchdog heartbeat period in seconds from 1 to " | |
306 | __MODULE_STRING(MAX_HEARTBEAT) ", default " | |
307 | __MODULE_STRING(DEFAULT_HEARTBEAT)); | |
308 | ||
309 | MODULE_LICENSE("GPL"); | |
310 | MODULE_ALIAS_MISCDEV(WATCHDOG_MINOR); | |
311 | MODULE_ALIAS("platform:jz4740-wdt"); |