]>
Commit | Line | Data |
---|---|---|
d2912cb1 | 1 | // SPDX-License-Identifier: GPL-2.0-only |
058dfc76 MW |
2 | /* |
3 | * ACPI Hardware Watchdog (WDAT) driver. | |
4 | * | |
5 | * Copyright (C) 2016, Intel Corporation | |
6 | * Author: Mika Westerberg <mika.westerberg@linux.intel.com> | |
058dfc76 MW |
7 | */ |
8 | ||
9 | #include <linux/acpi.h> | |
10 | #include <linux/ioport.h> | |
11 | #include <linux/module.h> | |
12 | #include <linux/platform_device.h> | |
13 | #include <linux/pm.h> | |
14 | #include <linux/watchdog.h> | |
15 | ||
16 | #define MAX_WDAT_ACTIONS ACPI_WDAT_ACTION_RESERVED | |
17 | ||
18 | /** | |
19 | * struct wdat_instruction - Single ACPI WDAT instruction | |
20 | * @entry: Copy of the ACPI table instruction | |
21 | * @reg: Register the instruction is accessing | |
22 | * @node: Next instruction in action sequence | |
23 | */ | |
24 | struct wdat_instruction { | |
25 | struct acpi_wdat_entry entry; | |
26 | void __iomem *reg; | |
27 | struct list_head node; | |
28 | }; | |
29 | ||
30 | /** | |
31 | * struct wdat_wdt - ACPI WDAT watchdog device | |
32 | * @pdev: Parent platform device | |
33 | * @wdd: Watchdog core device | |
34 | * @period: How long is one watchdog period in ms | |
35 | * @stopped_in_sleep: Is this watchdog stopped by the firmware in S1-S5 | |
36 | * @stopped: Was the watchdog stopped by the driver in suspend | |
37 | * @actions: An array of instruction lists indexed by an action number from | |
38 | * the WDAT table. There can be %NULL entries for not implemented | |
39 | * actions. | |
40 | */ | |
41 | struct wdat_wdt { | |
42 | struct platform_device *pdev; | |
43 | struct watchdog_device wdd; | |
44 | unsigned int period; | |
45 | bool stopped_in_sleep; | |
46 | bool stopped; | |
47 | struct list_head *instructions[MAX_WDAT_ACTIONS]; | |
48 | }; | |
49 | ||
50 | #define to_wdat_wdt(wdd) container_of(wdd, struct wdat_wdt, wdd) | |
51 | ||
52 | static bool nowayout = WATCHDOG_NOWAYOUT; | |
53 | module_param(nowayout, bool, 0); | |
54 | MODULE_PARM_DESC(nowayout, "Watchdog cannot be stopped once started (default=" | |
55 | __MODULE_STRING(WATCHDOG_NOWAYOUT) ")"); | |
56 | ||
cabe17d0 MW |
57 | #define WDAT_DEFAULT_TIMEOUT 30 |
58 | ||
59 | static int timeout = WDAT_DEFAULT_TIMEOUT; | |
60 | module_param(timeout, int, 0); | |
61 | MODULE_PARM_DESC(timeout, "Watchdog timeout in seconds (default=" | |
62 | __MODULE_STRING(WDAT_DEFAULT_TIMEOUT) ")"); | |
63 | ||
058dfc76 MW |
64 | static int wdat_wdt_read(struct wdat_wdt *wdat, |
65 | const struct wdat_instruction *instr, u32 *value) | |
66 | { | |
67 | const struct acpi_generic_address *gas = &instr->entry.register_region; | |
68 | ||
69 | switch (gas->access_width) { | |
70 | case 1: | |
71 | *value = ioread8(instr->reg); | |
72 | break; | |
73 | case 2: | |
74 | *value = ioread16(instr->reg); | |
75 | break; | |
76 | case 3: | |
77 | *value = ioread32(instr->reg); | |
78 | break; | |
79 | default: | |
80 | return -EINVAL; | |
81 | } | |
82 | ||
83 | dev_dbg(&wdat->pdev->dev, "Read %#x from 0x%08llx\n", *value, | |
84 | gas->address); | |
85 | ||
86 | return 0; | |
87 | } | |
88 | ||
89 | static int wdat_wdt_write(struct wdat_wdt *wdat, | |
90 | const struct wdat_instruction *instr, u32 value) | |
91 | { | |
92 | const struct acpi_generic_address *gas = &instr->entry.register_region; | |
93 | ||
94 | switch (gas->access_width) { | |
95 | case 1: | |
96 | iowrite8((u8)value, instr->reg); | |
97 | break; | |
98 | case 2: | |
99 | iowrite16((u16)value, instr->reg); | |
100 | break; | |
101 | case 3: | |
102 | iowrite32(value, instr->reg); | |
103 | break; | |
104 | default: | |
105 | return -EINVAL; | |
106 | } | |
107 | ||
108 | dev_dbg(&wdat->pdev->dev, "Wrote %#x to 0x%08llx\n", value, | |
109 | gas->address); | |
110 | ||
111 | return 0; | |
112 | } | |
113 | ||
114 | static int wdat_wdt_run_action(struct wdat_wdt *wdat, unsigned int action, | |
115 | u32 param, u32 *retval) | |
116 | { | |
117 | struct wdat_instruction *instr; | |
118 | ||
119 | if (action >= ARRAY_SIZE(wdat->instructions)) | |
120 | return -EINVAL; | |
121 | ||
122 | if (!wdat->instructions[action]) | |
123 | return -EOPNOTSUPP; | |
124 | ||
125 | dev_dbg(&wdat->pdev->dev, "Running action %#x\n", action); | |
126 | ||
127 | /* Run each instruction sequentially */ | |
128 | list_for_each_entry(instr, wdat->instructions[action], node) { | |
129 | const struct acpi_wdat_entry *entry = &instr->entry; | |
130 | const struct acpi_generic_address *gas; | |
131 | u32 flags, value, mask, x, y; | |
132 | bool preserve; | |
133 | int ret; | |
134 | ||
135 | gas = &entry->register_region; | |
136 | ||
137 | preserve = entry->instruction & ACPI_WDAT_PRESERVE_REGISTER; | |
138 | flags = entry->instruction & ~ACPI_WDAT_PRESERVE_REGISTER; | |
139 | value = entry->value; | |
140 | mask = entry->mask; | |
141 | ||
142 | switch (flags) { | |
143 | case ACPI_WDAT_READ_VALUE: | |
144 | ret = wdat_wdt_read(wdat, instr, &x); | |
145 | if (ret) | |
146 | return ret; | |
147 | x >>= gas->bit_offset; | |
148 | x &= mask; | |
149 | if (retval) | |
150 | *retval = x == value; | |
151 | break; | |
152 | ||
153 | case ACPI_WDAT_READ_COUNTDOWN: | |
154 | ret = wdat_wdt_read(wdat, instr, &x); | |
155 | if (ret) | |
156 | return ret; | |
157 | x >>= gas->bit_offset; | |
158 | x &= mask; | |
159 | if (retval) | |
160 | *retval = x; | |
161 | break; | |
162 | ||
163 | case ACPI_WDAT_WRITE_VALUE: | |
164 | x = value & mask; | |
165 | x <<= gas->bit_offset; | |
166 | if (preserve) { | |
167 | ret = wdat_wdt_read(wdat, instr, &y); | |
168 | if (ret) | |
169 | return ret; | |
170 | y = y & ~(mask << gas->bit_offset); | |
171 | x |= y; | |
172 | } | |
173 | ret = wdat_wdt_write(wdat, instr, x); | |
174 | if (ret) | |
175 | return ret; | |
176 | break; | |
177 | ||
178 | case ACPI_WDAT_WRITE_COUNTDOWN: | |
179 | x = param; | |
180 | x &= mask; | |
181 | x <<= gas->bit_offset; | |
182 | if (preserve) { | |
183 | ret = wdat_wdt_read(wdat, instr, &y); | |
184 | if (ret) | |
185 | return ret; | |
186 | y = y & ~(mask << gas->bit_offset); | |
187 | x |= y; | |
188 | } | |
189 | ret = wdat_wdt_write(wdat, instr, x); | |
190 | if (ret) | |
191 | return ret; | |
192 | break; | |
193 | ||
194 | default: | |
195 | dev_err(&wdat->pdev->dev, "Unknown instruction: %u\n", | |
196 | flags); | |
197 | return -EINVAL; | |
198 | } | |
199 | } | |
200 | ||
201 | return 0; | |
202 | } | |
203 | ||
204 | static int wdat_wdt_enable_reboot(struct wdat_wdt *wdat) | |
205 | { | |
206 | int ret; | |
207 | ||
208 | /* | |
209 | * WDAT specification says that the watchdog is required to reboot | |
210 | * the system when it fires. However, it also states that it is | |
211 | * recommeded to make it configurable through hardware register. We | |
4c0bfc03 | 212 | * enable reboot now if it is configurable, just in case. |
058dfc76 | 213 | */ |
cda3b917 | 214 | ret = wdat_wdt_run_action(wdat, ACPI_WDAT_SET_REBOOT, 0, NULL); |
058dfc76 MW |
215 | if (ret && ret != -EOPNOTSUPP) { |
216 | dev_err(&wdat->pdev->dev, | |
217 | "Failed to enable reboot when watchdog triggers\n"); | |
218 | return ret; | |
219 | } | |
220 | ||
221 | return 0; | |
222 | } | |
223 | ||
224 | static void wdat_wdt_boot_status(struct wdat_wdt *wdat) | |
225 | { | |
226 | u32 boot_status = 0; | |
227 | int ret; | |
228 | ||
229 | ret = wdat_wdt_run_action(wdat, ACPI_WDAT_GET_STATUS, 0, &boot_status); | |
230 | if (ret && ret != -EOPNOTSUPP) { | |
231 | dev_err(&wdat->pdev->dev, "Failed to read boot status\n"); | |
232 | return; | |
233 | } | |
234 | ||
235 | if (boot_status) | |
236 | wdat->wdd.bootstatus = WDIOF_CARDRESET; | |
237 | ||
238 | /* Clear the boot status in case BIOS did not do it */ | |
cda3b917 | 239 | ret = wdat_wdt_run_action(wdat, ACPI_WDAT_SET_STATUS, 0, NULL); |
058dfc76 MW |
240 | if (ret && ret != -EOPNOTSUPP) |
241 | dev_err(&wdat->pdev->dev, "Failed to clear boot status\n"); | |
242 | } | |
243 | ||
244 | static void wdat_wdt_set_running(struct wdat_wdt *wdat) | |
245 | { | |
246 | u32 running = 0; | |
247 | int ret; | |
248 | ||
249 | ret = wdat_wdt_run_action(wdat, ACPI_WDAT_GET_RUNNING_STATE, 0, | |
250 | &running); | |
251 | if (ret && ret != -EOPNOTSUPP) | |
252 | dev_err(&wdat->pdev->dev, "Failed to read running state\n"); | |
253 | ||
254 | if (running) | |
255 | set_bit(WDOG_HW_RUNNING, &wdat->wdd.status); | |
256 | } | |
257 | ||
258 | static int wdat_wdt_start(struct watchdog_device *wdd) | |
259 | { | |
260 | return wdat_wdt_run_action(to_wdat_wdt(wdd), | |
261 | ACPI_WDAT_SET_RUNNING_STATE, 0, NULL); | |
262 | } | |
263 | ||
264 | static int wdat_wdt_stop(struct watchdog_device *wdd) | |
265 | { | |
266 | return wdat_wdt_run_action(to_wdat_wdt(wdd), | |
267 | ACPI_WDAT_SET_STOPPED_STATE, 0, NULL); | |
268 | } | |
269 | ||
270 | static int wdat_wdt_ping(struct watchdog_device *wdd) | |
271 | { | |
272 | return wdat_wdt_run_action(to_wdat_wdt(wdd), ACPI_WDAT_RESET, 0, NULL); | |
273 | } | |
274 | ||
275 | static int wdat_wdt_set_timeout(struct watchdog_device *wdd, | |
276 | unsigned int timeout) | |
277 | { | |
278 | struct wdat_wdt *wdat = to_wdat_wdt(wdd); | |
279 | unsigned int periods; | |
280 | int ret; | |
281 | ||
282 | periods = timeout * 1000 / wdat->period; | |
283 | ret = wdat_wdt_run_action(wdat, ACPI_WDAT_SET_COUNTDOWN, periods, NULL); | |
284 | if (!ret) | |
285 | wdd->timeout = timeout; | |
286 | return ret; | |
287 | } | |
288 | ||
289 | static unsigned int wdat_wdt_get_timeleft(struct watchdog_device *wdd) | |
290 | { | |
291 | struct wdat_wdt *wdat = to_wdat_wdt(wdd); | |
292 | u32 periods = 0; | |
293 | ||
0a48f239 | 294 | wdat_wdt_run_action(wdat, ACPI_WDAT_GET_CURRENT_COUNTDOWN, 0, &periods); |
058dfc76 MW |
295 | return periods * wdat->period / 1000; |
296 | } | |
297 | ||
298 | static const struct watchdog_info wdat_wdt_info = { | |
299 | .options = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING | WDIOF_MAGICCLOSE, | |
300 | .firmware_version = 0, | |
301 | .identity = "wdat_wdt", | |
302 | }; | |
303 | ||
304 | static const struct watchdog_ops wdat_wdt_ops = { | |
305 | .owner = THIS_MODULE, | |
306 | .start = wdat_wdt_start, | |
307 | .stop = wdat_wdt_stop, | |
308 | .ping = wdat_wdt_ping, | |
309 | .set_timeout = wdat_wdt_set_timeout, | |
310 | .get_timeleft = wdat_wdt_get_timeleft, | |
311 | }; | |
312 | ||
313 | static int wdat_wdt_probe(struct platform_device *pdev) | |
314 | { | |
edaa35b5 | 315 | struct device *dev = &pdev->dev; |
058dfc76 MW |
316 | const struct acpi_wdat_entry *entries; |
317 | const struct acpi_table_wdat *tbl; | |
318 | struct wdat_wdt *wdat; | |
319 | struct resource *res; | |
320 | void __iomem **regs; | |
321 | acpi_status status; | |
322 | int i, ret; | |
323 | ||
324 | status = acpi_get_table(ACPI_SIG_WDAT, 0, | |
325 | (struct acpi_table_header **)&tbl); | |
326 | if (ACPI_FAILURE(status)) | |
327 | return -ENODEV; | |
328 | ||
edaa35b5 | 329 | wdat = devm_kzalloc(dev, sizeof(*wdat), GFP_KERNEL); |
058dfc76 MW |
330 | if (!wdat) |
331 | return -ENOMEM; | |
332 | ||
edaa35b5 | 333 | regs = devm_kcalloc(dev, pdev->num_resources, sizeof(*regs), |
058dfc76 MW |
334 | GFP_KERNEL); |
335 | if (!regs) | |
336 | return -ENOMEM; | |
337 | ||
338 | /* WDAT specification wants to have >= 1ms period */ | |
339 | if (tbl->timer_period < 1) | |
340 | return -EINVAL; | |
341 | if (tbl->min_count > tbl->max_count) | |
342 | return -EINVAL; | |
343 | ||
344 | wdat->period = tbl->timer_period; | |
345 | wdat->wdd.min_hw_heartbeat_ms = wdat->period * tbl->min_count; | |
346 | wdat->wdd.max_hw_heartbeat_ms = wdat->period * tbl->max_count; | |
347 | wdat->stopped_in_sleep = tbl->flags & ACPI_WDAT_STOPPED; | |
348 | wdat->wdd.info = &wdat_wdt_info; | |
349 | wdat->wdd.ops = &wdat_wdt_ops; | |
350 | wdat->pdev = pdev; | |
351 | ||
352 | /* Request and map all resources */ | |
353 | for (i = 0; i < pdev->num_resources; i++) { | |
354 | void __iomem *reg; | |
355 | ||
356 | res = &pdev->resource[i]; | |
357 | if (resource_type(res) == IORESOURCE_MEM) { | |
edaa35b5 | 358 | reg = devm_ioremap_resource(dev, res); |
356ed043 WY |
359 | if (IS_ERR(reg)) |
360 | return PTR_ERR(reg); | |
058dfc76 | 361 | } else if (resource_type(res) == IORESOURCE_IO) { |
edaa35b5 | 362 | reg = devm_ioport_map(dev, res->start, 1); |
356ed043 WY |
363 | if (!reg) |
364 | return -ENOMEM; | |
058dfc76 | 365 | } else { |
edaa35b5 | 366 | dev_err(dev, "Unsupported resource\n"); |
058dfc76 MW |
367 | return -EINVAL; |
368 | } | |
369 | ||
058dfc76 MW |
370 | regs[i] = reg; |
371 | } | |
372 | ||
373 | entries = (struct acpi_wdat_entry *)(tbl + 1); | |
374 | for (i = 0; i < tbl->entries; i++) { | |
375 | const struct acpi_generic_address *gas; | |
376 | struct wdat_instruction *instr; | |
377 | struct list_head *instructions; | |
378 | unsigned int action; | |
379 | struct resource r; | |
380 | int j; | |
381 | ||
382 | action = entries[i].action; | |
383 | if (action >= MAX_WDAT_ACTIONS) { | |
edaa35b5 | 384 | dev_dbg(dev, "Skipping unknown action: %u\n", action); |
058dfc76 MW |
385 | continue; |
386 | } | |
387 | ||
edaa35b5 | 388 | instr = devm_kzalloc(dev, sizeof(*instr), GFP_KERNEL); |
058dfc76 MW |
389 | if (!instr) |
390 | return -ENOMEM; | |
391 | ||
392 | INIT_LIST_HEAD(&instr->node); | |
393 | instr->entry = entries[i]; | |
394 | ||
395 | gas = &entries[i].register_region; | |
396 | ||
397 | memset(&r, 0, sizeof(r)); | |
398 | r.start = gas->address; | |
2ba33a4e | 399 | r.end = r.start + ACPI_ACCESS_BYTE_WIDTH(gas->access_width) - 1; |
058dfc76 MW |
400 | if (gas->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { |
401 | r.flags = IORESOURCE_MEM; | |
402 | } else if (gas->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { | |
403 | r.flags = IORESOURCE_IO; | |
404 | } else { | |
edaa35b5 | 405 | dev_dbg(dev, "Unsupported address space: %d\n", |
058dfc76 MW |
406 | gas->space_id); |
407 | continue; | |
408 | } | |
409 | ||
410 | /* Find the matching resource */ | |
411 | for (j = 0; j < pdev->num_resources; j++) { | |
412 | res = &pdev->resource[j]; | |
413 | if (resource_contains(res, &r)) { | |
414 | instr->reg = regs[j] + r.start - res->start; | |
415 | break; | |
416 | } | |
417 | } | |
418 | ||
419 | if (!instr->reg) { | |
edaa35b5 | 420 | dev_err(dev, "I/O resource not found\n"); |
058dfc76 MW |
421 | return -EINVAL; |
422 | } | |
423 | ||
424 | instructions = wdat->instructions[action]; | |
425 | if (!instructions) { | |
edaa35b5 GR |
426 | instructions = devm_kzalloc(dev, |
427 | sizeof(*instructions), | |
428 | GFP_KERNEL); | |
058dfc76 MW |
429 | if (!instructions) |
430 | return -ENOMEM; | |
431 | ||
432 | INIT_LIST_HEAD(instructions); | |
433 | wdat->instructions[action] = instructions; | |
434 | } | |
435 | ||
436 | list_add_tail(&instr->node, instructions); | |
437 | } | |
438 | ||
439 | wdat_wdt_boot_status(wdat); | |
440 | wdat_wdt_set_running(wdat); | |
441 | ||
442 | ret = wdat_wdt_enable_reboot(wdat); | |
443 | if (ret) | |
444 | return ret; | |
445 | ||
446 | platform_set_drvdata(pdev, wdat); | |
447 | ||
cabe17d0 MW |
448 | /* |
449 | * Set initial timeout so that userspace has time to configure the | |
450 | * watchdog properly after it has opened the device. In some cases | |
451 | * the BIOS default is too short and causes immediate reboot. | |
452 | */ | |
453 | if (timeout * 1000 < wdat->wdd.min_hw_heartbeat_ms || | |
454 | timeout * 1000 > wdat->wdd.max_hw_heartbeat_ms) { | |
455 | dev_warn(dev, "Invalid timeout %d given, using %d\n", | |
456 | timeout, WDAT_DEFAULT_TIMEOUT); | |
457 | timeout = WDAT_DEFAULT_TIMEOUT; | |
458 | } | |
459 | ||
460 | ret = wdat_wdt_set_timeout(&wdat->wdd, timeout); | |
461 | if (ret) | |
462 | return ret; | |
463 | ||
058dfc76 | 464 | watchdog_set_nowayout(&wdat->wdd, nowayout); |
edaa35b5 | 465 | return devm_watchdog_register_device(dev, &wdat->wdd); |
058dfc76 MW |
466 | } |
467 | ||
468 | #ifdef CONFIG_PM_SLEEP | |
469 | static int wdat_wdt_suspend_noirq(struct device *dev) | |
470 | { | |
20745634 | 471 | struct wdat_wdt *wdat = dev_get_drvdata(dev); |
058dfc76 MW |
472 | int ret; |
473 | ||
474 | if (!watchdog_active(&wdat->wdd)) | |
475 | return 0; | |
476 | ||
477 | /* | |
478 | * We need to stop the watchdog if firmare is not doing it or if we | |
479 | * are going suspend to idle (where firmware is not involved). If | |
480 | * firmware is stopping the watchdog we kick it here one more time | |
481 | * to give it some time. | |
482 | */ | |
483 | wdat->stopped = false; | |
484 | if (acpi_target_system_state() == ACPI_STATE_S0 || | |
485 | !wdat->stopped_in_sleep) { | |
486 | ret = wdat_wdt_stop(&wdat->wdd); | |
487 | if (!ret) | |
488 | wdat->stopped = true; | |
489 | } else { | |
490 | ret = wdat_wdt_ping(&wdat->wdd); | |
491 | } | |
492 | ||
493 | return ret; | |
494 | } | |
495 | ||
496 | static int wdat_wdt_resume_noirq(struct device *dev) | |
497 | { | |
20745634 | 498 | struct wdat_wdt *wdat = dev_get_drvdata(dev); |
058dfc76 MW |
499 | int ret; |
500 | ||
501 | if (!watchdog_active(&wdat->wdd)) | |
502 | return 0; | |
503 | ||
504 | if (!wdat->stopped) { | |
505 | /* | |
506 | * Looks like the boot firmware reinitializes the watchdog | |
507 | * before it hands off to the OS on resume from sleep so we | |
508 | * stop and reprogram the watchdog here. | |
509 | */ | |
510 | ret = wdat_wdt_stop(&wdat->wdd); | |
511 | if (ret) | |
512 | return ret; | |
513 | ||
514 | ret = wdat_wdt_set_timeout(&wdat->wdd, wdat->wdd.timeout); | |
515 | if (ret) | |
516 | return ret; | |
517 | ||
518 | ret = wdat_wdt_enable_reboot(wdat); | |
519 | if (ret) | |
520 | return ret; | |
28e3d700 MW |
521 | |
522 | ret = wdat_wdt_ping(&wdat->wdd); | |
523 | if (ret) | |
524 | return ret; | |
058dfc76 MW |
525 | } |
526 | ||
527 | return wdat_wdt_start(&wdat->wdd); | |
528 | } | |
529 | #endif | |
530 | ||
531 | static const struct dev_pm_ops wdat_wdt_pm_ops = { | |
532 | SET_NOIRQ_SYSTEM_SLEEP_PM_OPS(wdat_wdt_suspend_noirq, | |
533 | wdat_wdt_resume_noirq) | |
534 | }; | |
535 | ||
536 | static struct platform_driver wdat_wdt_driver = { | |
537 | .probe = wdat_wdt_probe, | |
538 | .driver = { | |
539 | .name = "wdat_wdt", | |
540 | .pm = &wdat_wdt_pm_ops, | |
541 | }, | |
542 | }; | |
543 | ||
544 | module_platform_driver(wdat_wdt_driver); | |
545 | ||
546 | MODULE_AUTHOR("Mika Westerberg <mika.westerberg@linux.intel.com>"); | |
547 | MODULE_DESCRIPTION("ACPI Hardware Watchdog (WDAT) driver"); | |
548 | MODULE_LICENSE("GPL v2"); | |
549 | MODULE_ALIAS("platform:wdat_wdt"); |