From b6dbb0920e1d13ea5703fb3a6b8b74934b184a52 Mon Sep 17 00:00:00 2001 From: Steffen Trumtrar Date: Fri, 7 Jan 2022 14:42:19 +0100 Subject: watchdog: add support for wdat_wdt Add support for systems with the ACPI Watchdog Action Table (wdat). Based on Linux v5.15-rc1 drivers/watchdog/wdat_wdt.c Signed-off-by: Steffen Trumtrar Link: https://lore.barebox.org/20220107134219.1031552-2-s.trumtrar@pengutronix.de Signed-off-by: Sascha Hauer --- drivers/watchdog/Kconfig | 10 + drivers/watchdog/Makefile | 1 + drivers/watchdog/wdat_wdt.c | 496 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 507 insertions(+) create mode 100644 drivers/watchdog/wdat_wdt.c (limited to 'drivers/watchdog') diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig index d605e62c1e..70c8be7ca9 100644 --- a/drivers/watchdog/Kconfig +++ b/drivers/watchdog/Kconfig @@ -149,4 +149,14 @@ config STARFIVE_WDT If you say yes here you get support for the watchdog device on StarFive SoCs. +config WDAT_WDT + bool "ACPI Watchdog Action Table (WDAT)" + depends on X86 + depends on ACPI + help + This driver adds support for systems with ACPI Watchdog Action + Table (WDAT) table. Servers typically have this but it can be + found on some desktop machines as well. This driver will take + over the native iTCO watchdog driver found on many Intel CPUs. + endif diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile index dbb76a5cb8..0d2f273f78 100644 --- a/drivers/watchdog/Makefile +++ b/drivers/watchdog/Makefile @@ -20,3 +20,4 @@ obj-$(CONFIG_F71808E_WDT) += f71808e_wdt.o obj-$(CONFIG_GPIO_WATCHDOG) += gpio_wdt.o obj-$(CONFIG_ITCO_WDT) += itco_wdt.o obj-$(CONFIG_STARFIVE_WDT) += starfive_wdt.o +obj-$(CONFIG_WDAT_WDT) += wdat_wdt.o diff --git a/drivers/watchdog/wdat_wdt.c b/drivers/watchdog/wdat_wdt.c new file mode 100644 index 0000000000..39e8cc4a3d --- /dev/null +++ b/drivers/watchdog/wdat_wdt.c @@ -0,0 +1,496 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * ACPI Hardware Watchdog (WDAT) driver. + * + * Copyright (C) 2016, Intel Corporation + * Author: Mika Westerberg + */ +#include +#include +#include +#include +#include +#include +#include +#include + +enum acpi_wdat_actions { + ACPI_WDAT_RESET = 1, + ACPI_WDAT_GET_CURRENT_COUNTDOWN = 4, + ACPI_WDAT_GET_COUNTDOWN = 5, + ACPI_WDAT_SET_COUNTDOWN = 6, + ACPI_WDAT_GET_RUNNING_STATE = 8, + ACPI_WDAT_SET_RUNNING_STATE = 9, + ACPI_WDAT_GET_STOPPED_STATE = 10, + ACPI_WDAT_SET_STOPPED_STATE = 11, + ACPI_WDAT_GET_REBOOT = 16, + ACPI_WDAT_SET_REBOOT = 17, + ACPI_WDAT_GET_SHUTDOWN = 18, + ACPI_WDAT_SET_SHUTDOWN = 19, + ACPI_WDAT_GET_STATUS = 32, + ACPI_WDAT_SET_STATUS = 33, + ACPI_WDAT_ACTION_RESERVED = 34 /* 34 and greater are reserved */ +}; + +enum acpi_wdat_instructions { + ACPI_WDAT_READ_VALUE = 0, + ACPI_WDAT_READ_COUNTDOWN = 1, + ACPI_WDAT_WRITE_VALUE = 2, + ACPI_WDAT_WRITE_COUNTDOWN = 3, + ACPI_WDAT_INSTRUCTION_RESERVED = 4, /* 4 and greater are reserved */ + ACPI_WDAT_PRESERVE_REGISTER = 0x80 /* Except for this value */ +}; + +#define MAX_WDAT_ACTIONS ACPI_WDAT_ACTION_RESERVED + +#define WDAT_DEFAULT_TIMEOUT 30 + +/* WDAT Instruction Entries (actions) */ + +struct __packed acpi_wdat_entry { + u8 action; + u8 instruction; + u16 reserved; + struct acpi_generic_address register_region; + u32 value; /* Value used with Read/Write register */ + u32 mask; /* Bitmask required for this register instruction */ +}; + +/** + * struct wdat_instruction - Single ACPI WDAT instruction + * @entry: Copy of the ACPI table instruction + * @reg: Register the instruction is accessing + * @node: Next instruction in action sequence + */ +struct wdat_instruction { + struct acpi_wdat_entry entry; + void __iomem *reg; + struct list_head node; +}; + +/** + * struct wdat_wdt - ACPI WDAT watchdog device + * @dev: Parent platform device + * @wdd: Watchdog core device + * @period: How long is one watchdog period in ms + * @stopped_in_sleep: Is this watchdog stopped by the firmware in S1-S5 + * @stopped: Was the watchdog stopped by the driver in suspend + * @instructions: An array of instruction lists indexed by an action number from + * the WDAT table. There can be %NULL entries for not implemented + * actions. + */ +struct wdat_wdt { + struct watchdog wdd; + unsigned int period; + bool stopped_in_sleep; + bool stopped; + struct list_head *instructions[MAX_WDAT_ACTIONS]; +}; + +struct __packed acpi_table_wdat { + struct acpi_table_header header; /* Common ACPI table header */ + u32 header_length; /* Watchdog Header Length */ + u16 pci_segment; /* PCI Segment number */ + u8 pci_bus; /* PCI Bus number */ + u8 pci_device; /* PCI Device number */ + u8 pci_function; /* PCI Function number */ + u8 reserved[3]; + u32 timer_period; /* Period of one timer count (msec) */ + u32 max_count; /* Maximum counter value supported */ + u32 min_count; /* Minimum counter value */ + u8 flags; + u8 reserved2[3]; + u32 nr_entries; /* Number of watchdog entries that follow */ + struct acpi_wdat_entry entries[]; +}; + +#define ACPI_WDAT_ENABLED (1) +#define ACPI_WDAT_STOPPED 0x80 + +#define IO_COND(instr, is_pio, is_mmio) do { \ + const struct acpi_generic_address *gas = &instr->entry.register_region; \ + unsigned long port = (unsigned long __force)instr->reg; \ + if (gas->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { \ + is_mmio; \ + } else if (gas->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { \ + is_pio; \ + } \ +} while (0) + +static unsigned int read8(const struct wdat_instruction *instr) +{ + IO_COND(instr, return inb(port), return readb(instr->reg)); + return 0xff; +} + +static unsigned int read16(const struct wdat_instruction *instr) +{ + IO_COND(instr, return inw(port), return readw(instr->reg)); + return 0xffff; +} + +static unsigned int read32(const struct wdat_instruction *instr) +{ + IO_COND(instr, return inl(port), return readl(instr->reg)); + return 0xffffffff; +} + +static void write8(u8 val, const struct wdat_instruction *instr) +{ + IO_COND(instr, outb(val,port), writeb(val, instr->reg)); +} + +static void write16(u16 val, const struct wdat_instruction *instr) +{ + IO_COND(instr, outw(val,port), writew(val, instr->reg)); +} + +static void write32(u32 val, const struct wdat_instruction *instr) +{ + IO_COND(instr, outl(val,port), writel(val, instr->reg)); +} + +static int wdat_wdt_read(struct wdat_wdt *wdat, + const struct wdat_instruction *instr, u32 *value) +{ + const struct acpi_generic_address *gas = &instr->entry.register_region; + + switch (gas->access_width) { + case 1: + *value = read8(instr); + break; + case 2: + *value = read16(instr); + break; + case 3: + *value = read32(instr); + break; + default: + return -EINVAL; + } + + dev_dbg(wdat->wdd.hwdev, "Read %#x from 0x%08llx\n", *value, + gas->address); + + return 0; +} + +static int wdat_wdt_write(struct wdat_wdt *wdat, + const struct wdat_instruction *instr, u32 value) +{ + const struct acpi_generic_address *gas = &instr->entry.register_region; + + switch (gas->access_width) { + case 1: + write8((u8)value, instr); + break; + case 2: + write16((u16)value, instr); + break; + case 3: + write32(value, instr); + break; + default: + return -EINVAL; + } + + dev_dbg(wdat->wdd.hwdev, "Wrote %#x to 0x%08llx\n", value, + gas->address); + + return 0; +} + +static int wdat_wdt_run_action(struct wdat_wdt *wdat, unsigned int action, + u32 param, u32 *retval) +{ + struct wdat_instruction *instr; + + if (action >= ARRAY_SIZE(wdat->instructions)) { + dev_dbg(wdat->wdd.hwdev, "Invalid action %#x\n", action); + return -EINVAL; + } + + if (!wdat->instructions[action]) { + dev_dbg(wdat->wdd.hwdev, "Unsupported action %#x\n", action); + return -EOPNOTSUPP; + } + + dev_dbg(wdat->wdd.hwdev, "Running action %#x\n", action); + + /* Run each instruction sequentially */ + list_for_each_entry(instr, wdat->instructions[action], node) { + const struct acpi_wdat_entry *entry = &instr->entry; + const struct acpi_generic_address *gas; + u32 flags, value, mask, x, y; + bool preserve; + int ret; + + gas = &entry->register_region; + + preserve = entry->instruction & ACPI_WDAT_PRESERVE_REGISTER; + flags = entry->instruction & ~ACPI_WDAT_PRESERVE_REGISTER; + value = entry->value; + mask = entry->mask; + + switch (flags) { + case ACPI_WDAT_READ_VALUE: + ret = wdat_wdt_read(wdat, instr, &x); + if (ret) + return ret; + x >>= gas->bit_offset; + x &= mask; + if (retval) + *retval = x == value; + break; + + case ACPI_WDAT_READ_COUNTDOWN: + ret = wdat_wdt_read(wdat, instr, &x); + if (ret) + return ret; + x >>= gas->bit_offset; + x &= mask; + if (retval) + *retval = x; + break; + + case ACPI_WDAT_WRITE_VALUE: + x = value & mask; + x <<= gas->bit_offset; + if (preserve) { + ret = wdat_wdt_read(wdat, instr, &y); + if (ret) + return ret; + y = y & ~(mask << gas->bit_offset); + x |= y; + } + ret = wdat_wdt_write(wdat, instr, x); + if (ret) + return ret; + break; + + case ACPI_WDAT_WRITE_COUNTDOWN: + x = param; + x &= mask; + x <<= gas->bit_offset; + if (preserve) { + ret = wdat_wdt_read(wdat, instr, &y); + if (ret) + return ret; + y = y & ~(mask << gas->bit_offset); + x |= y; + } + ret = wdat_wdt_write(wdat, instr, x); + if (ret) + return ret; + break; + + default: + dev_err(wdat->wdd.hwdev, "Unknown instruction: %u\n", + flags); + return -EINVAL; + } + } + + return 0; +} + +static void wdat_wdt_boot_status(struct wdat_wdt *wdat) +{ + u32 boot_status = 0; + int ret; + + ret = wdat_wdt_run_action(wdat, ACPI_WDAT_GET_STATUS, 0, &boot_status); + if (ret && ret != -EOPNOTSUPP) { + dev_err(wdat->wdd.hwdev, "Failed to read boot status\n"); + return; + } + + /* Clear the boot status in case BIOS did not do it */ + ret = wdat_wdt_run_action(wdat, ACPI_WDAT_SET_STATUS, 0, NULL); + if (ret && ret != -EOPNOTSUPP) + dev_err(wdat->wdd.hwdev, "Failed to clear boot status\n"); +} + +static int wdat_wdt_start(struct watchdog *wdd) +{ + struct wdat_wdt *wdat = container_of(wdd, struct wdat_wdt, wdd); + + return wdat_wdt_run_action(wdat, ACPI_WDAT_SET_RUNNING_STATE, 0, NULL); +} + +static int wdat_wdt_stop(struct watchdog *wdd) +{ + struct wdat_wdt *wdat = container_of(wdd, struct wdat_wdt, wdd); + + return wdat_wdt_run_action(wdat, ACPI_WDAT_SET_STOPPED_STATE, 0, NULL); +} + +static void wdat_wdt_set_running(struct wdat_wdt *wdat) +{ + u32 running = 0; + int ret; + + ret = wdat_wdt_run_action(wdat, ACPI_WDAT_GET_RUNNING_STATE, 0, + &running); + if (ret && ret != -EOPNOTSUPP) + dev_err(wdat->wdd.hwdev, "Failed to read running state\n"); + + dev_dbg(wdat->wdd.hwdev, "Running state: %d\n", running); + + wdat->wdd.running = running ? WDOG_HW_RUNNING : WDOG_HW_NOT_RUNNING; +} + +static int wdat_wdt_set_timeout(struct watchdog *wdd, unsigned int timeout) +{ + struct wdat_wdt *wdat = container_of(wdd, struct wdat_wdt, wdd); + unsigned int periods; + int ret; + + if (timeout == 0) + return wdat_wdt_stop(wdd); + + periods = timeout * 1000 / wdat->period; + ret = wdat_wdt_run_action(wdat, ACPI_WDAT_SET_COUNTDOWN, periods, NULL); + if (ret) + return ret; + + if (wdat->wdd.running == WDOG_HW_NOT_RUNNING) { + wdat_wdt_start(wdd); + wdat->wdd.running = WDOG_HW_RUNNING; + } + + return wdat_wdt_run_action(wdat, ACPI_WDAT_RESET, 0, NULL); +} + +static int wdat_wdt_enable_reboot(struct wdat_wdt *wdat) +{ + int ret; + + /* + * WDAT specification says that the watchdog is required to reboot + * the system when it fires. However, it also states that it is + * recommeded to make it configurable through hardware register. We + * enable reboot now if it is configrable, just in case. + */ + ret = wdat_wdt_run_action(wdat, ACPI_WDAT_SET_REBOOT, 0, NULL); + if (ret && ret != -EOPNOTSUPP) { + dev_err(wdat->wdd.hwdev, + "Failed to enable reboot when watchdog triggers\n"); + return ret; + } + + return 0; +} + +static int wdat_wdt_probe(struct device_d *const dev) +{ + const struct acpi_wdat_entry *entries; + struct acpi_table_wdat *tbl; + struct wdat_wdt *wdat; + int i, ret; + + dev_dbg(dev, "driver initializing...\n"); + + tbl = (struct acpi_table_wdat __force *)dev_request_mem_region_by_name(dev, "SDT"); + if (IS_ERR(tbl)) { + dev_err(dev, "no SDT resource available: %pe\n", tbl); + return PTR_ERR(tbl); + } + + dev_dbg(dev, "SDT is at 0x%p\n", tbl); + + wdat = xzalloc(sizeof(*wdat)); + + /* WDAT specification wants to have >= 1ms period */ + if (tbl->timer_period < 1) { + dev_dbg(dev, "timer_period is less than 1: %d\n", tbl->timer_period); + return -EINVAL; + } + if (tbl->min_count > tbl->max_count) { + dev_dbg(dev, "min_count must be greater than max_count\n"); + return -EINVAL; + } + + wdat->period = tbl->timer_period; + wdat->stopped_in_sleep = tbl->flags & ACPI_WDAT_STOPPED; + wdat->wdd.set_timeout = wdat_wdt_set_timeout; + wdat->wdd.hwdev = dev; + wdat->wdd.timeout_max = U32_MAX; + + entries = tbl->entries; + + for (i = 0; i < tbl->nr_entries; i++) { + const struct acpi_generic_address *gas; + struct wdat_instruction *instr; + struct list_head *instructions; + struct resource *res; + unsigned int action; + struct resource r; + + action = entries[i].action; + if (action >= MAX_WDAT_ACTIONS) { + dev_dbg(dev, "Skipping unknown action: %u\n", action); + continue; + } + + instr = xzalloc(sizeof(*instr)); + + INIT_LIST_HEAD(&instr->node); + instr->entry = entries[i]; + + gas = &entries[i].register_region; + + memset(&r, 0, sizeof(r)); + r.start = gas->address; + r.end = r.start + ACPI_ACCESS_BYTE_WIDTH(gas->access_width) - 1; + if (gas->space_id == ACPI_ADR_SPACE_SYSTEM_MEMORY) { + res = request_iomem_region(dev_name(dev), r.start, r.end); + } else if (gas->space_id == ACPI_ADR_SPACE_SYSTEM_IO) { + res = request_ioport_region(dev_name(dev), r.start, r.end); + } else { + dev_dbg(dev, "Unsupported address space: %d\n", + gas->space_id); + continue; + } + + /* + * Some entries have the same gas->address. + * We want the action but can't request the region multiple times. + */ + if (IS_ERR(res) && (PTR_ERR(res) != -EBUSY)) + return PTR_ERR(res); + + instr->reg = IOMEM(r.start); + + instructions = wdat->instructions[action]; + if (!instructions) { + instructions = xzalloc(sizeof(*instructions)); + if (!instructions) + return -ENOMEM; + + INIT_LIST_HEAD(instructions); + wdat->instructions[action] = instructions; + } + + list_add_tail(&instr->node, instructions); + } + + wdat_wdt_boot_status(wdat); + wdat_wdt_set_running(wdat); + + ret = wdat_wdt_enable_reboot(wdat); + if (ret) + return ret; + + return watchdog_register(&wdat->wdd); +} + + +static struct acpi_driver wdat_wdt_driver = { + .signature = "WDAT", + .driver = { + .name = "wdat-wdt", + .probe = wdat_wdt_probe, + } +}; +device_acpi_driver(wdat_wdt_driver); -- cgit v1.2.3