Changeset View
Changeset View
Standalone View
Standalone View
sys/dev/wdatwd/wdatwd.c
- This file was added.
/*- | |||||
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD | |||||
* | |||||
* 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. | |||||
*/ | |||||
/* | |||||
* ACPI WDAT based watchdog timer driver. The WDAT specification ``Hardware | |||||
* Watchdog Timers Design Specification'' was made public by Microsoft and can | |||||
* be found at the following location. | |||||
* WWW: https://download.microsoft.com/download/a/f/7/af7777e5-7dcd-4800-8a0a-b18336565f5b/HardwareWDTSpec.doc | |||||
*/ | |||||
#include <sys/cdefs.h> | |||||
__FBSDID("$FreeBSD$"); | |||||
#include "opt_acpi.h" | |||||
#include <sys/param.h> | |||||
#include <sys/systm.h> | |||||
#include <sys/bus.h> | |||||
#include <sys/callout.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/eventhandler.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 specifies the corresponding ACPI GAS but | |||||
* more than one instructions access the same or adjacent register region(s) | |||||
* so we need to canonicalize/merge all the specified GASs. | |||||
* | |||||
* type ACPI resource type and later SYS_RES_(IOPORT|MEMORY). | |||||
* start Region start address. | |||||
* end Region end address + 1. | |||||
* rid Resource rid assigned when allocated. | |||||
* res Resource when allocated. | |||||
* link Next/previous resource entry. | |||||
*/ | |||||
struct wdat_res { | |||||
int type; | |||||
uint64_t start, end; | |||||
int rid; | |||||
struct resource *res; | |||||
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 are operated sequencially. | |||||
* | |||||
* 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(). | |||||
* period Number of watchdog ticks per milli-sec. | |||||
* max Max. supported watchdog ticks to be set. | |||||
* min Min. supported watchdog ticks to be set. | |||||
* default_timeout BIOS configured watchdog ticks to fire. | |||||
* timeout User set watchdog timeout in ms or 0 if isn't changed. | |||||
* stop_in_sleep 0 if this watchdog keeps counting down during sleep. | |||||
* running 1 if this watchdog is running or 0 if stopped. | |||||
* 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; | |||||
unsigned int period; | |||||
unsigned int max; | |||||
unsigned int min; | |||||
uint64_t default_timeout; | |||||
uint64_t timeout; | |||||
unsigned int stop_in_sleep; | |||||
unsigned int running; | |||||
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); | |||||
#define wdatwd_acpi_status(rw) { \ | |||||
if (ACPI_FAILURE(status)) {\ | |||||
device_printf(sc->dev, \ | |||||
"action: 0x%02x, %s() returned: %d\n", \ | |||||
action, (rw), status); \ | |||||
return (ENXIO); \ | |||||
}; \ | |||||
} | |||||
/* | |||||
* Do requested action. | |||||
*/ | |||||
static int | |||||
wdatwd_action(struct wdatwd_softc *sc, unsigned int action, uint64_t val, uint64_t *ret) | |||||
{ | |||||
struct wdat_instr *wdat; | |||||
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_STATUS status; | |||||
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); | |||||
wdatwd_acpi_status("AcpiRead"); | |||||
x >>= gas->BitOffset; | |||||
x &= wdat->entry.Mask; | |||||
*ret = (x == wdat->entry.Value) ? 1 : 0; | |||||
break; | |||||
case ACPI_WDAT_READ_COUNTDOWN: | |||||
status = AcpiRead(&x, gas); | |||||
wdatwd_acpi_status("AcpiRead"); | |||||
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); | |||||
wdatwd_acpi_status("AcpiRead"); | |||||
y &= ~(wdat->entry.Mask << gas->BitOffset); | |||||
x |= y; | |||||
} | |||||
status = AcpiWrite(x, gas); | |||||
wdatwd_acpi_status("AcpiWrite"); | |||||
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); | |||||
wdatwd_acpi_status("AcpiRead"); | |||||
y &= ~(wdat->entry.Mask << gas->BitOffset); | |||||
x |= y; | |||||
} | |||||
status = AcpiWrite(x, gas); | |||||
wdatwd_acpi_status("AcpiWrite"); | |||||
break; | |||||
default: | |||||
return (EINVAL); | |||||
} | |||||
} | |||||
return (0); | |||||
} | |||||
/* | |||||
* Reset the watchdog countdown. | |||||
*/ | |||||
static int | |||||
wdatwd_reset_countdown(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, unsigned 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; | |||||
if (! (e = wdatwd_action( | |||||
sc, ACPI_WDAT_SET_COUNTDOWN, timeout, NULL))) { | |||||
sc->timeout = timeout * sc->period; | |||||
} | |||||
return (e); | |||||
} | |||||
/* | |||||
* Get the watchdog current countdown value. | |||||
*/ | |||||
static int | |||||
wdatwd_get_current_countdown(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(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 = 0; | |||||
e = wdatwd_action(sc, ACPI_WDAT_SET_RUNNING_STATE, 0, NULL); | |||||
sc->running = ! e; | |||||
return (e); | |||||
} | |||||
/* | |||||
* Set the watchdog to stopped state. | |||||
*/ | |||||
static int | |||||
wdatwd_set_stop(struct wdatwd_softc *sc) | |||||
{ | |||||
int e = 0; | |||||
e = wdatwd_action(sc, ACPI_WDAT_SET_STOPPED_STATE, 0, NULL); | |||||
sc->running = !! e; | |||||
return (e); | |||||
} | |||||
/* | |||||
* Clear the watchdog's boot status if the current boot was caused by the | |||||
* watchdog firing. | |||||
*/ | |||||
static int | |||||
wdatwd_clear_status(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(struct wdatwd_softc *sc) | |||||
{ | |||||
return wdatwd_action(sc, ACPI_WDAT_SET_REBOOT, 0, NULL); | |||||
} | |||||
/* | |||||
* Watchdog event handler. | |||||
*/ | |||||
static void | |||||
wdatwd_event(void *private, unsigned int cmd, int *error) | |||||
{ | |||||
struct wdatwd_softc *sc = private; | |||||
int run[2]; | |||||
uint64_t cur[2], cnt[2]; | |||||
if (bootverbose) { | |||||
run[0] = sc->running; | |||||
wdatwd_get_countdown(sc, cnt); | |||||
wdatwd_get_current_countdown(sc, cur); | |||||
} | |||||
if ((cmd & WD_INTERVAL)) { | |||||
if (sc->running) | |||||
wdatwd_reset_countdown(sc); | |||||
else { | |||||
// 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 sec, 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 sec after | |||||
* starting, where 2.4 sec 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 sec which is much longer | |||||
* than 2.4 sec timeout. */ | |||||
wdatwd_reset_countdown(sc); | |||||
} | |||||
} else | |||||
wdatwd_set_stop(sc); | |||||
if (bootverbose) { | |||||
run[1] = sc->running; | |||||
wdatwd_get_countdown(sc, cnt + 1); | |||||
wdatwd_get_current_countdown(sc, cur + 1); | |||||
wdatwd_verbose_printf(sc->dev, "cmd: %u, sc->running: " | |||||
"%d -> %d, cnt: %lu -> %lu, cur: %lu -> %lu\n", cmd, | |||||
run[0], run[1], cnt[0], cnt[1], cur[0], cur[1]); | |||||
} | |||||
return; | |||||
} | |||||
static int | |||||
wdat_set_action(struct wdatwd_softc *sc, void *addr, int remaining) | |||||
{ | |||||
ACPI_WDAT_ENTRY *entry = addr; | |||||
struct wdat_instr *wdat; | |||||
if (remaining < (int) sizeof(ACPI_WDAT_ENTRY)) | |||||
return (-1); | |||||
// Skip actions beyond specification. | |||||
if (entry->Action < ACPI_WDAT_ACTION_RESERVED) { | |||||
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; | |||||
char *cp; | |||||
int remaining, consumed; | |||||
remaining = wdat->Header.Length - sizeof(ACPI_TABLE_WDAT); | |||||
while (remaining > 0) { | |||||
cp = (char *)wdat + wdat->Header.Length - remaining; | |||||
consumed = wdat_set_action(sc, cp, remaining); | |||||
if (consumed < 0) | |||||
device_printf(sc->dev, "inconsistent WDAT table.\n"); | |||||
if (consumed <= 0) | |||||
break; | |||||
else | |||||
remaining -= consumed; | |||||
} | |||||
return; | |||||
} | |||||
/* | |||||
* Compare the given GAS rr against the region start and end | |||||
* (actually end + 1). On return, type, start and end are updated as per rr. | |||||
* If not NULL, also overlap is updated depending on how rr and start-end | |||||
* overlap each other. | |||||
*/ | |||||
static int | |||||
wdat_compare_region(ACPI_GENERIC_ADDRESS *rr, int *type, uint64_t *start, uint64_t *end, int *overlap) | |||||
{ | |||||
uint64_t s, e; | |||||
s = e = rr->Address; | |||||
switch (rr->AccessWidth) { | |||||
case 1: | |||||
e += 1; | |||||
break; | |||||
case 2: | |||||
e += 2; | |||||
break; | |||||
case 3: | |||||
e += 4; | |||||
break; | |||||
case 4: | |||||
e += 8; | |||||
break; | |||||
default: | |||||
return (EINVAL); | |||||
} | |||||
/* a) rr is fully covered by start-end. | |||||
* b) rr fully covers start-end. | |||||
* c) rr and start-end overlap partially. | |||||
* d) rr and start-end have no overlap. | |||||
* e) both have different resource type. | |||||
* f) overlap is set to NULL. | |||||
* overlap is a bit-field. bit 1 is for (a), bit 2 and 3 is for (b) and | |||||
* (c), where rr extends start to lower then bit 2 is set, and to upper | |||||
* bit 3 is set. */ | |||||
if (overlap) | |||||
*overlap = 0; | |||||
if ((*type != rr->SpaceId) || (overlap == NULL) // (ef) | |||||
|| (s > *end) || (e < *start)) {} // (d) | |||||
else { | |||||
if ((s >= *start) && (e <= *end)) // (a) | |||||
*overlap = 0x1; | |||||
if (s < *start) // (bc) | |||||
*overlap = 0x2; | |||||
if (e > *end) // (bc) | |||||
*overlap += 0x4; | |||||
} | |||||
*start = s; | |||||
*end = e; | |||||
*type = rr->SpaceId; | |||||
return (0); | |||||
} | |||||
/* | |||||
* Try to merge the given GAS (in wdat) with the existing res in queue. | |||||
*/ | |||||
static void | |||||
wdat_merge_resource(struct wdatwd_softc *sc, struct wdat_instr *wdat) | |||||
{ | |||||
struct wdat_res *res, *r2; | |||||
uint64_t s, e; | |||||
int overlap, type, found = 0; | |||||
/* Try to merge the given GAS with the existing res list by extending | |||||
* one certain res to cover the GAS if such res and GAS overlap each | |||||
* other. If no overlapped res found, insert a new res at appropriate | |||||
* position. */ | |||||
TAILQ_FOREACH(res, &sc->res, link) { | |||||
type = res->type; | |||||
s = res->start; | |||||
e = res->end; | |||||
wdat_compare_region(&wdat->entry.RegisterRegion, | |||||
&type, &s, &e, &overlap); | |||||
// Try next res if GAS isn't mergeable. | |||||
if ((type != res->type) || (! overlap)) | |||||
continue; | |||||
found = 1; | |||||
// This res fully covers the GAS. | |||||
if (overlap == 0x1) | |||||
break; | |||||
// This GAS extends the res to lower address (s < res->start). | |||||
if ((overlap & 0x2)) { | |||||
while ((r2 = TAILQ_PREV(res, res_head, link))) { | |||||
if (type != r2->type) | |||||
continue; | |||||
else if (s <= r2->end) { | |||||
s = r2->start; | |||||
TAILQ_REMOVE(&sc->res, r2, link); | |||||
free(r2, M_DEVBUF); | |||||
} else | |||||
break; | |||||
} | |||||
res->start = s; | |||||
} | |||||
// This GAS extends the res to upper address (e > res->end). | |||||
if ((overlap & 0x4)) { | |||||
while ((r2 = TAILQ_NEXT(res, link))) { | |||||
if (type != r2->type) | |||||
continue; | |||||
else if (e >= r2->start) { | |||||
e = r2->end; | |||||
TAILQ_REMOVE(&sc->res, r2, link); | |||||
free(r2, M_DEVBUF); | |||||
} else | |||||
break; | |||||
} | |||||
res->end = e; | |||||
} | |||||
break; | |||||
} | |||||
// No extendable res at all. Now add new res to the queue. | |||||
if (! found) { | |||||
res = malloc(sizeof(*res), M_DEVBUF, M_WAITOK | M_ZERO); | |||||
res->type = type; | |||||
res->start = s; | |||||
res->end = e; | |||||
TAILQ_FOREACH(r2, &sc->res, link) { | |||||
if (res->type != r2->type) | |||||
continue; | |||||
if (res->start < r2->start) { | |||||
TAILQ_INSERT_BEFORE(r2, res, link); | |||||
found = 1; | |||||
break; | |||||
} | |||||
} | |||||
if (! found) | |||||
TAILQ_INSERT_TAIL(&sc->res, res, link); | |||||
} | |||||
} | |||||
/* | |||||
* Release the already allocated resource. | |||||
*/ | |||||
static void | |||||
wdat_release_resource(device_t dev) | |||||
{ | |||||
struct wdatwd_softc *sc; | |||||
struct wdat_res *res; | |||||
sc = device_get_softc(dev); | |||||
TAILQ_FOREACH(res, &sc->res, link) | |||||
if (res->res) { | |||||
bus_release_resource(dev, res->type, | |||||
res->rid, res->res); | |||||
bus_delete_resource(dev, res->type, res->rid); | |||||
res->res = NULL; | |||||
} | |||||
} | |||||
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 0; | |||||
} | |||||
static int | |||||
wdatwd_attach(device_t dev) | |||||
{ | |||||
struct wdatwd_softc *sc; | |||||
ACPI_STATUS status; | |||||
int i, rid, e = 0; | |||||
struct wdat_instr *wdat; | |||||
struct sysctl_ctx_list *sctx; | |||||
struct sysctl_oid *soid; | |||||
struct wdat_res *res; | |||||
sc = device_get_softc(dev); | |||||
sc->dev = dev; | |||||
for (i = 0; i < ACPI_WDAT_ACTION_RESERVED; ++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); | |||||
// Parse defined watchdog actions. | |||||
wdat_parse_action_table(sc); | |||||
AcpiPutTable((ACPI_TABLE_HEADER *) sc->wdat); | |||||
// Verbose logging. | |||||
for (i = 0; i < ACPI_WDAT_ACTION_RESERVED; ++i) | |||||
STAILQ_FOREACH(wdat, &sc->action[i], next) { | |||||
wdatwd_verbose_printf(dev, "action: 0x%02x, " | |||||
"%s %s at 0x%lx (%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", | |||||
wdat->entry.RegisterRegion.Address, | |||||
wdat->entry.RegisterRegion.BitWidth, | |||||
wdat->entry.RegisterRegion.BitOffset); | |||||
} | |||||
// Canonicalize the requested resources. | |||||
TAILQ_INIT(&sc->res); | |||||
for (i = 0; i < ACPI_WDAT_ACTION_RESERVED; ++i) | |||||
STAILQ_FOREACH(wdat, &sc->action[i], next) { | |||||
if (TAILQ_EMPTY(&sc->res)) { | |||||
res = malloc(sizeof(*res), | |||||
M_DEVBUF, M_WAITOK | M_ZERO); | |||||
wdat_compare_region( | |||||
&wdat->entry.RegisterRegion, | |||||
&res->type, &res->start, &res->end, NULL); | |||||
TAILQ_INSERT_HEAD(&sc->res, res, link); | |||||
} else | |||||
wdat_merge_resource(sc, wdat); | |||||
} | |||||
// 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: | |||||
return (EOPNOTSUPP); | |||||
} | |||||
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); | |||||
e = ENOMEM; | |||||
device_printf(dev, "%s at 0x%lx (%ld byte(s)): " | |||||
"alloc' failed\n", | |||||
res->type == SYS_RES_MEMORY ? "mem" : "io ", | |||||
res->start, res->end - res->start); | |||||
goto fail; | |||||
} | |||||
wdatwd_verbose_printf(dev, "%s at 0x%lx (%ld byte(s)): " | |||||
"alloc'ed\n", | |||||
res->type == SYS_RES_MEMORY ? "mem" : "io ", | |||||
res->start, res->end - res->start); | |||||
} | |||||
// Initialize the watchdog hardware. | |||||
if (((e = wdatwd_set_stop(sc))) | |||||
|| ((e = wdatwd_clear_status(sc)) && (e != EOPNOTSUPP)) | |||||
|| ((e = wdatwd_set_reboot(sc)) && (e != EOPNOTSUPP)) | |||||
|| ((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_ms", CTLFLAG_RD, &sc->timeout, 0, | |||||
"Current watchdog timeout in ms if changed."); | |||||
SYSCTL_ADD_UINT(sctx, SYSCTL_CHILDREN(soid), OID_AUTO, | |||||
"running", CTLFLAG_RD, &sc->running, 0, | |||||
"Watchdog is running now."); | |||||
SYSCTL_ADD_U64(sctx, SYSCTL_CHILDREN(soid), OID_AUTO, | |||||
"default_timeout_ms", CTLFLAG_RD, SYSCTL_NULL_U64_PTR, | |||||
sc->default_timeout * sc->period, | |||||
"BIOS configured watchdog timeout in ms."); | |||||
SYSCTL_ADD_UINT(sctx, SYSCTL_CHILDREN(soid), OID_AUTO, | |||||
"can_set_timeout", CTLFLAG_RD, SYSCTL_NULL_UINT_PTR, | |||||
! STAILQ_EMPTY(&sc->action[ACPI_WDAT_SET_COUNTDOWN]), | |||||
"Watchdog timeout is configurable."); | |||||
sc->ev_tag = EVENTHANDLER_REGISTER(watchdog_list, wdatwd_event, sc, 0); | |||||
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); | |||||
return sc->stop_in_sleep ? 0 : wdatwd_set_stop(sc); | |||||
} | |||||
static int | |||||
wdatwd_resume(device_t dev) | |||||
{ | |||||
struct wdatwd_softc *sc; | |||||
int e = 0; | |||||
sc = device_get_softc(dev); | |||||
if (sc->stop_in_sleep) | |||||
(e = wdatwd_reset_countdown(sc)) | |||||
|| (e = wdatwd_set_running(sc)); | |||||
return (e); | |||||
} | |||||
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); |