root/sys/dev/wdatwd/wdatwd.c
/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2022 Tetsuya Uemura <t_uemura@macome.co.jp>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/cdefs.h>
#include "opt_acpi.h"

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/callout.h>
#include <sys/eventhandler.h>
#include <sys/interrupt.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/module.h>
#include <sys/queue.h>
#include <sys/rman.h>
#include <sys/sysctl.h>
#include <sys/watchdog.h>
#include <vm/vm.h>
#include <vm/pmap.h>

#include <contrib/dev/acpica/include/acpi.h>
#include <contrib/dev/acpica/include/accommon.h>
#include <contrib/dev/acpica/include/aclocal.h>
#include <contrib/dev/acpica/include/actables.h>

#include <dev/acpica/acpivar.h>

/*
 * Resource entry. Every instruction has the corresponding ACPI GAS but two or
 * more instructions may access the same or adjacent register region(s). So we
 * need to merge all the specified resources.
 *
 * res   Resource when allocated.
 * start Region start address.
 * end   Region end address + 1.
 * rid   Resource rid assigned when allocated.
 * type  ACPI resource type, SYS_RES_IOPORT or SYS_RES_MEMORY.
 * link  Next/previous resource entry.
 */
struct wdat_res {
        struct resource         *res;
        uint64_t                start;
        uint64_t                end;
        int                     rid;
        int                     type;
        TAILQ_ENTRY(wdat_res)   link;
};

/*
 * Instruction entry. Every instruction itself is actually a single register
 * read or write (and subsequent bit operation(s)).
 * 0 or more instructions are tied to every watchdog action and once an action
 * is kicked, the corresponding entries run sequentially.
 *
 * entry Permanent copy of ACPI_WDAT_ENTRY entry (sub-table).
 * next  Next instruction entry.
 */
struct wdat_instr {
        ACPI_WDAT_ENTRY         entry;
        STAILQ_ENTRY(wdat_instr) next;
};

/*
 * dev             Watchdog device.
 * wdat            ACPI WDAT table, can be accessed until AcpiPutTable().
 * default_timeout BIOS configured watchdog ticks to fire.
 * timeout         User configured timeout in millisecond or 0 if isn't set.
 * max             Max. supported watchdog ticks to be set.
 * min             Min. supported watchdog ticks to be set.
 * period          Milliseconds per watchdog tick.
 * running         True if this watchdog is running or false if stopped.
 * stop_in_sleep   False if this watchdog keeps counting down during sleep.
 * ev_tag          Tag for EVENTHANDLER_*().
 * action          Array of watchdog instruction sets, each indexed by action.
 */
struct wdatwd_softc {
        device_t                dev;
        ACPI_TABLE_WDAT         *wdat;
        uint64_t                default_timeout;
        uint64_t                timeout;
        u_int                   max;
        u_int                   min;
        u_int                   period;
        bool                    running;
        bool                    stop_in_sleep;
        eventhandler_tag        ev_tag;
        STAILQ_HEAD(, wdat_instr) action[ACPI_WDAT_ACTION_RESERVED];
        TAILQ_HEAD(res_head, wdat_res) res;
};

#define WDATWD_VERBOSE_PRINTF(dev, ...)                                 \
        do {                                                            \
                if (bootverbose)                                        \
                        device_printf(dev, __VA_ARGS__);                \
        } while (0)

/*
 * Do requested action.
 */
static int
wdatwd_action(const struct wdatwd_softc *sc, const u_int action, const uint64_t val, uint64_t *ret)
{
        struct wdat_instr       *wdat;
        const char              *rw = NULL;
        ACPI_STATUS             status;

        if (STAILQ_EMPTY(&sc->action[action])) {
                WDATWD_VERBOSE_PRINTF(sc->dev,
                    "action not supported: 0x%02x\n", action);
                return (EOPNOTSUPP);
        }

        STAILQ_FOREACH(wdat, &sc->action[action], next) {
                ACPI_GENERIC_ADDRESS    *gas = &wdat->entry.RegisterRegion;
                uint64_t                x, y;

                switch (wdat->entry.Instruction
                    & ~ACPI_WDAT_PRESERVE_REGISTER) {
                    case ACPI_WDAT_READ_VALUE:
                        status = AcpiRead(&x, gas);
                        if (ACPI_FAILURE(status)) {
                                rw = "AcpiRead";
                                goto fail;
                        }
                        x >>= gas->BitOffset;
                        x &= wdat->entry.Mask;
                        *ret = (x == wdat->entry.Value) ? 1 : 0;
                        break;
                    case ACPI_WDAT_READ_COUNTDOWN:
                        status = AcpiRead(&x, gas);
                        if (ACPI_FAILURE(status)) {
                                rw = "AcpiRead";
                                goto fail;
                        }
                        x >>= gas->BitOffset;
                        x &= wdat->entry.Mask;
                        *ret = x;
                        break;
                    case ACPI_WDAT_WRITE_VALUE:
                        x = wdat->entry.Value & wdat->entry.Mask;
                        x <<= gas->BitOffset;
                        if (wdat->entry.Instruction
                            & ACPI_WDAT_PRESERVE_REGISTER) {
                                status = AcpiRead(&y, gas);
                                if (ACPI_FAILURE(status)) {
                                        rw = "AcpiRead";
                                        goto fail;
                                }
                                y &= ~(wdat->entry.Mask << gas->BitOffset);
                                x |= y;
                        }
                        status = AcpiWrite(x, gas);
                        if (ACPI_FAILURE(status)) {
                                rw = "AcpiWrite";
                                goto fail;
                        }
                        break;
                    case ACPI_WDAT_WRITE_COUNTDOWN:
                        x = val & wdat->entry.Mask;
                        x <<= gas->BitOffset;
                        if (wdat->entry.Instruction
                            & ACPI_WDAT_PRESERVE_REGISTER) {
                                status = AcpiRead(&y, gas);
                                if (ACPI_FAILURE(status)) {
                                        rw = "AcpiRead";
                                        goto fail;
                                }
                                y &= ~(wdat->entry.Mask << gas->BitOffset);
                                x |= y;
                        }
                        status = AcpiWrite(x, gas);
                        if (ACPI_FAILURE(status)) {
                                rw = "AcpiWrite";
                                goto fail;
                        }
                        break;
                    default:
                        return (EINVAL);
                }
        }

        return (0);

fail:
        device_printf(sc->dev, "action: 0x%02x, %s() returned: %d\n",
            action, rw, status);
        return (ENXIO);
}

/*
 * Reset the watchdog countdown.
 */
static int
wdatwd_reset_countdown(const struct wdatwd_softc *sc)
{
        return wdatwd_action(sc, ACPI_WDAT_RESET, 0, NULL);
}

/*
 * Set the watchdog countdown value. In WDAT specification, this is optional.
 */
static int
wdatwd_set_countdown(struct wdatwd_softc *sc, u_int cmd)
{
        uint64_t                timeout;
        int                     e;

        cmd &= WD_INTERVAL;
        timeout = ((uint64_t) 1 << cmd) / 1000000 / sc->period;
        if (timeout > sc->max)
                timeout = sc->max;
        else if (timeout < sc->min)
                timeout = sc->min;

        e = wdatwd_action(sc, ACPI_WDAT_SET_COUNTDOWN, timeout, NULL);
        if (e == 0)
                sc->timeout = timeout * sc->period;

        return (e);
}

/*
 * Get the watchdog current countdown value.
 */
static int
wdatwd_get_current_countdown(const struct wdatwd_softc *sc, uint64_t *timeout)
{
        return wdatwd_action(sc, ACPI_WDAT_GET_CURRENT_COUNTDOWN, 0, timeout);
}

/*
 * Get the watchdog countdown value the watchdog is configured to fire.
 */
static int
wdatwd_get_countdown(const struct wdatwd_softc *sc, uint64_t *timeout)
{
        return wdatwd_action(sc, ACPI_WDAT_GET_COUNTDOWN, 0, timeout);
}

/*
 * Set the watchdog to running state.
 */
static int
wdatwd_set_running(struct wdatwd_softc *sc)
{
        int                     e;

        e = wdatwd_action(sc, ACPI_WDAT_SET_RUNNING_STATE, 0, NULL);
        if (e == 0)
                sc->running = true;
        return (e);
}

/*
 * Set the watchdog to stopped state.
 */
static int
wdatwd_set_stop(struct wdatwd_softc *sc)
{
        int                     e;

        e = wdatwd_action(sc, ACPI_WDAT_SET_STOPPED_STATE, 0, NULL);
        if (e == 0)
                sc->running = false;
        return (e);
}

/*
 * Clear the watchdog's boot status if the current boot was caused by the
 * watchdog firing.
 */
static int
wdatwd_clear_status(const struct wdatwd_softc *sc)
{
        return wdatwd_action(sc, ACPI_WDAT_SET_STATUS, 0, NULL);
}

/*
 * Set the watchdog to reboot when it is fired.
 */
static int
wdatwd_set_reboot(const struct wdatwd_softc *sc)
{
        return wdatwd_action(sc, ACPI_WDAT_SET_REBOOT, 0, NULL);
}

/*
 * Watchdog event handler.
 */
static void
wdatwd_event(void *private, u_int cmd, int *error)
{
        struct wdatwd_softc     *sc = private;
        uint64_t                cur[2], cnt[2];
        bool                    run[2];

        if (bootverbose) {
                run[0] = sc->running;
                if (wdatwd_get_countdown(sc, &cnt[0]) != 0) 
                        cnt[0] = 0;
                if (wdatwd_get_current_countdown(sc, &cur[0]) != 0)
                        cur[0] = 0;
        }

        if ((cmd & WD_INTERVAL) == 0)
                wdatwd_set_stop(sc);
        else {
                if (!sc->running) {
                        /* ACPI_WDAT_SET_COUNTDOWN may not be implemented. */
                        wdatwd_set_countdown(sc, cmd);
                        wdatwd_set_running(sc);
                        /*
                         * In the first wdatwd_event() call, it sets the
                         * watchdog timeout to a considerably larger value such
                         * as 137 seconds, then kicks the watchdog to start
                         * counting down. Weirdly though, on a Dell R210 BIOS
                         * 1.12.0, a supplemental reset action must be
                         * triggered for the newly set timeout value to take
                         * effect. Without it, the watchdog fires 2.4 seconds
                         * after starting, where 2.4 seconds is its initially
                         * set timeout. This failure scenario is seen by first
                         * starting watchdogd(8) without wdatwd registered then
                         * kldload it. In steady state, watchdogd pats the
                         * watchdog every 10 or so seconds which is much longer
                         * than 2.4 seconds timeout.
                         */
                }
                wdatwd_reset_countdown(sc);
        }

        if (bootverbose) {
                run[1] = sc->running;
                if (wdatwd_get_countdown(sc, &cnt[1]) != 0)
                        cnt[1] = 0;
                if (wdatwd_get_current_countdown(sc, &cur[1]) != 0)
                        cur[1] = 0;
                WDATWD_VERBOSE_PRINTF(sc->dev, "cmd: %u, sc->running: "
                    "%d -> %d, cnt: %llu -> %llu, cur: %llu -> %llu\n", cmd,
                                      run[0], run[1], 
                                      (unsigned long long) cnt[0],
                                      (unsigned long long) cnt[1],
                                      (unsigned long long)cur[0],
                                      (unsigned long long)cur[1]);
        }

        return;
}

static ssize_t
wdat_set_action(struct wdatwd_softc *sc, ACPI_WDAT_ENTRY *addr, ssize_t remaining)
{
        ACPI_WDAT_ENTRY         *entry = addr;
        struct wdat_instr       *wdat;

        if (remaining < sizeof(ACPI_WDAT_ENTRY))
                return (-EINVAL);

        /* Skip actions beyond specification. */
        if (entry->Action < nitems(sc->action)) {
                wdat = malloc(sizeof(*wdat), M_DEVBUF, M_WAITOK | M_ZERO);
                wdat->entry = *entry;
                STAILQ_INSERT_TAIL(&sc->action[entry->Action], wdat, next);
        }
        return sizeof(ACPI_WDAT_ENTRY);
}

/*
 * Transform every ACPI_WDAT_ENTRY to wdat_instr by calling wdat_set_action().
 */
static void
wdat_parse_action_table(struct wdatwd_softc *sc)
{
        ACPI_TABLE_WDAT         *wdat = sc->wdat;
        ssize_t                 remaining, consumed;
        char                    *cp;

        remaining = wdat->Header.Length - sizeof(ACPI_TABLE_WDAT);
        while (remaining > 0) {
                cp = (char *)wdat + wdat->Header.Length - remaining;
                consumed = wdat_set_action(sc, (ACPI_WDAT_ENTRY *)cp,
                    remaining);
                if (consumed < 0) {
                        device_printf(sc->dev, "inconsistent WDAT table.\n");
                        break;
                }
                        remaining -= consumed;
        }
}

/*
 * Decode the given GAS rr and set its type, start and end (actually end + 1)
 * in the newly malloc()'ed res.
 */
static struct wdat_res *
wdat_alloc_region(ACPI_GENERIC_ADDRESS *rr)
{
        struct wdat_res *res;

        if (rr->AccessWidth < 1 || rr->AccessWidth > 4)
                return (NULL);

        res = malloc(sizeof(*res),
            M_DEVBUF, M_WAITOK | M_ZERO);
        if (res != NULL) {
                res->start = rr->Address;
                res->end   = res->start + (1 << (rr->AccessWidth - 1));
                res->type  = rr->SpaceId;
        }
        return (res);
}

#define OVERLAP_NONE    0x0 // no overlap.
#define OVERLAP_SUBSET  0x1 // res2 is fully covered by res1.
#define OVERLAP_START   0x2 // the start of res2 is overlaped.
#define OVERLAP_END     0x4 // the end of res2 is overlapped.

/*
 * Compare the given res1 and res2, and one of the above OVERLAP_* constant, or
 * in case res2 is larger than res1 at both the start and the end,
 * OVERLAP_START | OVERLAP_END, is returned.
 */
static int
wdat_compare_region(const struct wdat_res *res1, const struct wdat_res *res2)
{
        int overlap;

        /*
         * a) both have different resource type. == OVERLAP_NONE
         * b) res2 and res1 have no overlap.     == OVERLAP_NONE
         * c) res2 is fully covered by res1.     == OVERLAP_SUBSET
         * d) res2 and res1 overlap partially.   == OVERLAP_START or
         *                                          OVERLAP_END
         * e) res2 fully covers res1.            == OVERLAP_START | OVERLAP_END
         */
        overlap = 0;

        if (res1->type != res2->type || res1->start > res2->end
            || res1->end < res2->start)
                overlap |= OVERLAP_NONE;
        else {
                if (res1->start <= res2->start && res1->end >= res2->end)
                        overlap |= OVERLAP_SUBSET;
                if (res1->start > res2->start)
                        overlap |= OVERLAP_START;
                if (res1->end < res2->end)
                        overlap |= OVERLAP_END;
        }

        return (overlap);
}

/*
 * Try to merge the given newres with the existing sc->res.
 */
static void
wdat_merge_region(struct wdatwd_softc *sc, struct wdat_res *newres)
{
        struct wdat_res         *res1, *res2, *res_safe, *res_itr;
        int                     overlap;

        if (TAILQ_EMPTY(&sc->res)) {
                TAILQ_INSERT_HEAD(&sc->res, newres, link);
                return;
        }

        overlap = OVERLAP_NONE;

        TAILQ_FOREACH_SAFE(res1, &sc->res, link, res_safe) {
                overlap = wdat_compare_region(res1, newres);

                /* Try next res if newres isn't mergeable. */
                if (overlap == OVERLAP_NONE)
                        continue;

                /* This res fully covers newres. */
                if (overlap == OVERLAP_SUBSET)
                        break;

                /* Newres extends the existing res res1 to lower. */
                if ((overlap & OVERLAP_START)) {
                        res1->start = newres->start;
                        res_itr = res1;
                        /* Try to merge more res if possible. */
                        while ((res2 = TAILQ_PREV(res_itr, res_head, link))) {
                                if (res1->type != res2->type) {
                                        res_itr = res2;
                                        continue;
                                } else if (res1->start <= res2->end) {
                                        res1->start = res2->start;
                                        TAILQ_REMOVE(&sc->res, res2, link);
                                        free(res2, M_DEVBUF);
                                } else
                                        break;
                        }
                }
                /* Newres extends the existing res res1 to upper. */
                if ((overlap & OVERLAP_END)) {
                        res1->end = newres->end;
                        res_itr = res1;
                        /* Try to merge more res if possible. */
                        while ((res2 = TAILQ_NEXT(res_itr, link))) {
                                if (res1->type != res2->type) {
                                        res_itr = res2;
                                        continue;
                                } else if (res1->end >= res2->start) {
                                        res1->end = res2->end;
                                        TAILQ_REMOVE(&sc->res, res2, link);
                                        free(res2, M_DEVBUF);
                                } else
                                        break;
                        }
                }
                break;
        }

        /*
         * If newres extends the existing res, newres must be free()'ed.
         * Otherwise insert newres into sc->res at appropriate position
         * (the lowest address region appears first).
         */
        if (overlap > OVERLAP_NONE)
                free(newres, M_DEVBUF);
        else {
                TAILQ_FOREACH(res1, &sc->res, link) {
                        if (newres->type != res1->type)
                                continue;
                        if (newres->start < res1->start) {
                                TAILQ_INSERT_BEFORE(res1, newres, link);
                                break;
                        }
                }
                if (res1 == NULL)
                        TAILQ_INSERT_TAIL(&sc->res, newres, link);
        }
}

/*
 * Release the already allocated resource.
 */
static void
wdat_release_resource(device_t dev)
{
        struct wdatwd_softc     *sc;
        struct wdat_instr       *wdat;
        struct wdat_res         *res;
        int                     i;

        sc = device_get_softc(dev);

        TAILQ_FOREACH(res, &sc->res, link)
                if (res->res != NULL) {
                        bus_release_resource(dev, res->type,
                            res->rid, res->res);
                        bus_delete_resource(dev, res->type, res->rid);
                        res->res = NULL;
                }

        for (i = 0; i < nitems(sc->action); ++i)
                while (!STAILQ_EMPTY(&sc->action[i])) {
                        wdat = STAILQ_FIRST(&sc->action[i]);
                        STAILQ_REMOVE_HEAD(&sc->action[i], next);
                        free(wdat, M_DEVBUF);
                }

        while (!TAILQ_EMPTY(&sc->res)) {
                res = TAILQ_FIRST(&sc->res);
                TAILQ_REMOVE(&sc->res, res, link);
                free(res, M_DEVBUF);
        }
}

static int
wdatwd_probe(device_t dev)
{
        ACPI_TABLE_WDAT         *wdat;
        ACPI_STATUS             status;

        /* Without WDAT table we have nothing to do. */
        status = AcpiGetTable(ACPI_SIG_WDAT, 0, (ACPI_TABLE_HEADER **)&wdat);
        if (ACPI_FAILURE(status))
                return (ENXIO);

        /* Try to allocate one resource and assume wdatwd is already attached
         * if it fails. */
        {
                int             type, rid = 0;
                struct resource *res;

                if (acpi_bus_alloc_gas(dev, &type, rid,
                    &((ACPI_WDAT_ENTRY *)(wdat + 1))->RegisterRegion,
                    &res, 0))
                        return (ENXIO);
                bus_release_resource(dev, type, rid, res);
                bus_delete_resource(dev, type, rid);
        }

        WDATWD_VERBOSE_PRINTF(dev, "Flags: 0x%x, TimerPeriod: %d ms/cnt, "
            "MaxCount: %d cnt (%d ms), MinCount: %d cnt (%d ms)\n",
            (int)wdat->Flags, (int)wdat->TimerPeriod,
            (int)wdat->MaxCount, (int)(wdat->MaxCount * wdat->TimerPeriod),
            (int)wdat->MinCount, (int)(wdat->MinCount * wdat->TimerPeriod));
        /* WDAT timer consistency. */
        if ((wdat->TimerPeriod < 1) || (wdat->MinCount > wdat->MaxCount)) {
                device_printf(dev, "inconsistent timer variables.\n");
                return (EINVAL);
        }

        AcpiPutTable((ACPI_TABLE_HEADER *)wdat);

        device_set_desc(dev, "ACPI WDAT Watchdog Interface");
        return (BUS_PROBE_DEFAULT);
}

static int
wdatwd_attach(device_t dev)
{
        struct wdatwd_softc     *sc;
        struct wdat_instr       *wdat;
        struct wdat_res         *res;
        struct sysctl_ctx_list  *sctx;
        struct sysctl_oid       *soid;
        ACPI_STATUS             status;
        int                     e, i, rid;

        sc = device_get_softc(dev);
        sc->dev = dev;

        for (i = 0; i < nitems(sc->action); ++i)
                STAILQ_INIT(&sc->action[i]);

        /* Search and parse WDAT table. */
        status = AcpiGetTable(ACPI_SIG_WDAT, 0,
            (ACPI_TABLE_HEADER **)&sc->wdat);
        if (ACPI_FAILURE(status))
                return (ENXIO);

        /* Parse watchdog variables. */
        sc->period = sc->wdat->TimerPeriod;
        sc->max = sc->wdat->MaxCount;
        sc->min = sc->wdat->MinCount;
        sc->stop_in_sleep = (sc->wdat->Flags & ACPI_WDAT_STOPPED)
            ? true : false;
        /* Parse defined watchdog actions. */
        wdat_parse_action_table(sc);

        AcpiPutTable((ACPI_TABLE_HEADER *)sc->wdat);

        /* Verbose logging. */
        if (bootverbose) {
                for (i = 0; i < nitems(sc->action); ++i)
                        STAILQ_FOREACH(wdat, &sc->action[i], next) {
                                WDATWD_VERBOSE_PRINTF(dev, "action: 0x%02x, "
                                    "%s %s at 0x%llx (%d bit(s), offset %d bit(s))\n",
                                    i,
                                    wdat->entry.RegisterRegion.SpaceId
                                        == ACPI_ADR_SPACE_SYSTEM_MEMORY
                                        ? "mem"
                                        : wdat->entry.RegisterRegion.SpaceId
                                            == ACPI_ADR_SPACE_SYSTEM_IO
                                            ? "io "
                                            : "???",
                                    wdat->entry.RegisterRegion.AccessWidth == 1
                                        ? "byte "
                                        : wdat->entry.RegisterRegion.AccessWidth == 2
                                            ? "word "
                                            : wdat->entry.RegisterRegion.AccessWidth == 3
                                                ? "dword"
                                                : wdat->entry.RegisterRegion.AccessWidth == 4
                                                    ? "qword"
                                                    : "undef",
                                    (unsigned long long )
                                    wdat->entry.RegisterRegion.Address,
                                    wdat->entry.RegisterRegion.BitWidth,
                                    wdat->entry.RegisterRegion.BitOffset);
                }
        }

        /* Canonicalize the requested resources. */
        TAILQ_INIT(&sc->res);
        for (i = 0; i < nitems(sc->action); ++i)
                STAILQ_FOREACH(wdat, &sc->action[i], next) {
                        res = wdat_alloc_region(&wdat->entry.RegisterRegion);
                        if (res == NULL)
                                goto fail;
                        wdat_merge_region(sc, res);
                }

        /* Resource allocation. */
        rid = 0;
        TAILQ_FOREACH(res, &sc->res, link) {
                switch (res->type) {
                    case ACPI_ADR_SPACE_SYSTEM_MEMORY:
                        res->type = SYS_RES_MEMORY;
                        break;
                    case ACPI_ADR_SPACE_SYSTEM_IO:
                        res->type = SYS_RES_IOPORT;
                        break;
                    default:
                        goto fail;
                }

                res->rid = rid++;
                bus_set_resource(dev, res->type, res->rid,
                    res->start, res->end - res->start);
                res->res = bus_alloc_resource_any(
                    dev, res->type, &res->rid, RF_ACTIVE);
                if (res->res == NULL) {
                        bus_delete_resource(dev, res->type, res->rid);
                        device_printf(dev, "%s at 0x%llx (%lld byte(s)): "
                            "alloc' failed\n",
                            res->type == SYS_RES_MEMORY ? "mem" : "io ",
                            (unsigned long long )res->start,
                            (unsigned long long )(res->end - res->start));
                        goto fail;
                }
                WDATWD_VERBOSE_PRINTF(dev, "%s at 0x%llx (%lld byte(s)): "
                    "alloc'ed\n",
                    res->type == SYS_RES_MEMORY ? "mem" : "io ",
                    (unsigned long long )res->start,
                    (unsigned long long) (res->end - res->start));
        }

        /* Initialize the watchdog hardware. */
        if (wdatwd_set_stop(sc) != 0)
                goto fail;
        if ((e = wdatwd_clear_status(sc)) && e != EOPNOTSUPP)
                goto fail;
        if ((e = wdatwd_set_reboot(sc)) && e != EOPNOTSUPP)
                goto fail;
        if ((e = wdatwd_get_countdown(sc, &sc->default_timeout))
            && e != EOPNOTSUPP)
                goto fail;
        WDATWD_VERBOSE_PRINTF(dev, "initialized.\n");

        /* Some sysctls. Most of them should go to WDATWD_VERBOSE_PRINTF(). */
        sctx = device_get_sysctl_ctx(dev);
        soid = device_get_sysctl_tree(dev);
        SYSCTL_ADD_U64(sctx, SYSCTL_CHILDREN(soid), OID_AUTO,
            "timeout_default", CTLFLAG_RD, SYSCTL_NULL_U64_PTR,
            sc->default_timeout * sc->period,
            "The default watchdog timeout in millisecond.");
        SYSCTL_ADD_BOOL(sctx, SYSCTL_CHILDREN(soid), OID_AUTO,
            "timeout_configurable", CTLFLAG_RD, SYSCTL_NULL_BOOL_PTR,
            STAILQ_EMPTY(&sc->action[ACPI_WDAT_SET_COUNTDOWN]) ? false : true,
            "Whether the watchdog timeout is configurable or not.");
        SYSCTL_ADD_U64(sctx, SYSCTL_CHILDREN(soid), OID_AUTO,
            "timeout", CTLFLAG_RD, &sc->timeout, 0,
            "The current watchdog timeout in millisecond. "
            "If 0, the default timeout is used.");
        SYSCTL_ADD_BOOL(sctx, SYSCTL_CHILDREN(soid), OID_AUTO,
            "running", CTLFLAG_RD, &sc->running, 0,
            "Whether the watchdog timer is running or not.");

        sc->ev_tag = EVENTHANDLER_REGISTER(watchdog_list, wdatwd_event, sc,
            EVENTHANDLER_PRI_ANY);
        WDATWD_VERBOSE_PRINTF(dev, "watchdog registered.\n");

        return (0);

fail:
        wdat_release_resource(dev);

        return (ENXIO);
}

static int
wdatwd_detach(device_t dev)
{
        struct wdatwd_softc     *sc;
        int                     e;

        sc = device_get_softc(dev);

        EVENTHANDLER_DEREGISTER(watchdog_list, sc->ev_tag);
        e = wdatwd_set_stop(sc);
        wdat_release_resource(dev);

        return (e);
}

static int
wdatwd_suspend(device_t dev)
{
        struct wdatwd_softc     *sc;

        sc = device_get_softc(dev);

        if (!sc->stop_in_sleep)
                return (0);

        return wdatwd_set_stop(sc);
}

static int
wdatwd_resume(device_t dev)
{
        struct wdatwd_softc     *sc;

        sc = device_get_softc(dev);

        if (!sc->stop_in_sleep)
                return (0);

        return (wdatwd_reset_countdown(sc) || wdatwd_set_running(sc));
}

static device_method_t wdatwd_methods[] = {
        /* Device interface */
        DEVMETHOD(device_probe, wdatwd_probe),
        DEVMETHOD(device_attach, wdatwd_attach),
        DEVMETHOD(device_detach, wdatwd_detach),
        DEVMETHOD(device_shutdown, wdatwd_detach),
        DEVMETHOD(device_suspend, wdatwd_suspend),
        DEVMETHOD(device_resume, wdatwd_resume),
        DEVMETHOD_END
};

static driver_t wdatwd_driver = {
        "wdatwd",
        wdatwd_methods,
        sizeof(struct wdatwd_softc),
};

DRIVER_MODULE(wdatwd, acpi, wdatwd_driver, 0, 0);
MODULE_DEPEND(wdatwd, acpi, 1, 1, 1);