426 lines
11 KiB
C
426 lines
11 KiB
C
/*
|
|
* Copyright (C) 2018-2022 Intel Corporation.
|
|
*
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* WatchDog Timer (WDT): emulate i6300esb PCI wdt Intel SOC devices,
|
|
* used to monitor guest OS
|
|
*/
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdbool.h>
|
|
|
|
#include "vmmapi.h"
|
|
#include "mevent.h"
|
|
#include "pci_core.h"
|
|
#include "timer.h"
|
|
|
|
#define WDT_REG_BAR_SIZE 0x10
|
|
|
|
#define PCI_VENDOR_ID_INTEL 0x8086
|
|
#define PCI_DEVICE_ID_INTEL_ESB 0x25ab
|
|
|
|
#define ESB_CONFIG_REG 0x60 /* Config register*/
|
|
#define ESB_LOCK_REG 0x68 /* WDT lock register*/
|
|
|
|
/* Memory mapped registers */
|
|
#define ESB_TIMER1_REG 0x00 /* Timer1 value after each reset */
|
|
#define ESB_TIMER2_REG 0x04 /* Timer2 value after each reset */
|
|
#define ESB_GIS_REG 0x08 /* General Interrupt Status register */
|
|
#define ESB_RELOAD_REG 0x0c /* Reload register */
|
|
|
|
#define ESB_WDT_ENABLE (0x01 << 1) /* Enable WDT */
|
|
#define ESB_WDT_LOCK (0x01 << 0) /* Lock (nowayout) */
|
|
|
|
#define ESB_WDT_INT_ACT (0x01 << 0) /* WDT INT is active */
|
|
#define ESB_WDT_INT_MSK 0x3 /* WDT INT TYPE mask */
|
|
|
|
#define ESB_WDT_REBOOT (0x01 << 5) /* Enable reboot on timeout */
|
|
#define ESB_WDT_RELOAD (0x01 << 8) /* Ping/kick dog */
|
|
#define ESB_WDT_TIMEOUT (0x01 << 9) /* WDT timeout happened? */
|
|
|
|
/* Per i6300esb spec, in default watchdog timer prescaler
|
|
* the 20-bit Preload Value is loaded into bits 34:15 of the
|
|
* main down counter. The resulting timer clock is the PCI
|
|
* Clock (33 MHz) divided by 2^^15 . The approximate clock
|
|
* generated is 1 KHz, so right shift 10 bits of preload value
|
|
* to get the exact seconds for this timer.
|
|
*/
|
|
#define TIMER_TO_SECONDS(val) (val >> 10)
|
|
|
|
/* Magic constants */
|
|
#define ESB_UNLOCK1 0x80 /* Step 1 to unlock reset registers */
|
|
#define ESB_UNLOCK2 0x86 /* Step 2 to unlock reset registers */
|
|
|
|
/* the default 20-bit preload value */
|
|
#define DEFAULT_MAX_TIMER_VAL 0x000FFFFF
|
|
|
|
/* stage timer is set to 60s after reset, then left shift 10 bits of seconds
|
|
* to transform to the preload value of the watchdog timer which run in
|
|
* 1KHz clock.
|
|
*/
|
|
#define DEFAULT_RESET_TIMER_VAL (60 << 10)
|
|
|
|
/* for debug */
|
|
/* #define WDT_DEBUG */
|
|
#ifdef WDT_DEBUG
|
|
static FILE * dbg_file;
|
|
#define DPRINTF(format, args...) \
|
|
do { fprintf(dbg_file, format, args); fflush(dbg_file); } while (0)
|
|
#else
|
|
#define DPRINTF(format, arg...)
|
|
#endif
|
|
|
|
struct info_wdt {
|
|
struct acrn_timer timer;
|
|
|
|
bool reboot_enabled;/* "reboot" on wdt out */
|
|
bool intr_enabled; /* "intr" on wdt stage 1 time out */
|
|
bool intr_active; /* interrupt is active */
|
|
|
|
bool locked; /* If true, enabled field cannot be changed. */
|
|
bool wdt_enabled; /* If true, watchdog is enabled. */
|
|
bool wdt_armed;
|
|
|
|
uint32_t timer1_val;
|
|
uint32_t timer2_val;
|
|
int stage; /* stage 1 or 2. */
|
|
|
|
int unlock_state; /* unlock states 0 -> 1 -> 2 */
|
|
};
|
|
|
|
/* Whether watchdog is timeout. This info should cross reset. So
|
|
* we didn't add to struct info_wdt.
|
|
*/
|
|
static int wdt_timeout = 0;
|
|
|
|
static struct info_wdt wdt_state;
|
|
|
|
static void start_wdt_timer(void);
|
|
|
|
/*
|
|
* WDT timer, start when guest OS start watchdog service; and re-start for
|
|
* each dog-kick / ping action if time out, it will trigger reboot or other
|
|
* action to guest OS
|
|
*/
|
|
static void
|
|
wdt_expired_handler(void *arg, uint64_t nexp)
|
|
{
|
|
struct pci_vdev *dev = (struct pci_vdev *)arg;
|
|
|
|
DPRINTF("wdt timer out! stage=%d, reboot=%d\n",
|
|
wdt_state.stage, wdt_state.reboot_enabled);
|
|
|
|
if (wdt_state.stage == 1) {
|
|
if (wdt_state.intr_enabled) {
|
|
|
|
if (pci_msi_enabled(dev))
|
|
pci_generate_msi(dev, 0);
|
|
else
|
|
pci_lintr_assert(dev);
|
|
|
|
wdt_state.intr_active = true;
|
|
}
|
|
|
|
wdt_state.stage = 2;
|
|
start_wdt_timer();
|
|
} else {
|
|
if (wdt_state.reboot_enabled) {
|
|
wdt_state.stage = 1;
|
|
wdt_timeout = 1;
|
|
|
|
/* watchdog timer out, set the User VM to reboot */
|
|
#ifdef DM_DEBUG
|
|
pr_info("%s: setting VM state to %s\n", __func__, vm_state_to_str(VM_SUSPEND_SYSTEM_RESET));
|
|
vm_set_suspend_mode(VM_SUSPEND_SYSTEM_RESET);
|
|
/* Notify vm thread to handle VM_SUSPEND_SYSTEM_RESET request */
|
|
notify_vmloop_thread();
|
|
#else
|
|
pr_info("%s: setting VM state to %s\n", __func__, vm_state_to_str(VM_SUSPEND_FULL_RESET));
|
|
vm_set_suspend_mode(VM_SUSPEND_FULL_RESET);
|
|
#endif
|
|
mevent_notify();
|
|
} else {
|
|
/* if not need reboot, just loop timer */
|
|
wdt_state.stage = 1;
|
|
start_wdt_timer();
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
stop_wdt_timer(void)
|
|
{
|
|
struct itimerspec timer_val;
|
|
|
|
DPRINTF("%s: wdt_armed=%d\n", __func__, wdt_state.wdt_armed);
|
|
|
|
if (!wdt_state.wdt_armed)
|
|
return;
|
|
|
|
memset(&timer_val, 0, sizeof(struct itimerspec));
|
|
acrn_timer_settime(&wdt_state.timer, &timer_val);
|
|
wdt_state.wdt_armed = false;
|
|
}
|
|
|
|
static void
|
|
start_wdt_timer(void)
|
|
{
|
|
int seconds;
|
|
struct itimerspec timer_val;
|
|
|
|
if (!wdt_state.wdt_enabled)
|
|
return;
|
|
|
|
if (wdt_state.stage == 1)
|
|
seconds = TIMER_TO_SECONDS(wdt_state.timer1_val);
|
|
else
|
|
seconds = TIMER_TO_SECONDS(wdt_state.timer2_val);
|
|
|
|
DPRINTF("%s: armed=%d, time=%d\n", __func__,
|
|
wdt_state.wdt_armed, seconds);
|
|
|
|
memset(&timer_val, 0, sizeof(struct itimerspec));
|
|
timer_val.it_value.tv_sec = seconds;
|
|
|
|
if (acrn_timer_settime(&wdt_state.timer, &timer_val) == -1) {
|
|
pr_err("WDT timerfd_settime failed.\n");
|
|
wdt_state.wdt_armed = false;
|
|
return;
|
|
}
|
|
|
|
wdt_state.wdt_armed = true;
|
|
}
|
|
|
|
static int
|
|
pci_wdt_cfg_read(struct vmctx *ctx, int vcpu, struct pci_vdev *dev,
|
|
int offset, int bytes, uint32_t *rv)
|
|
{
|
|
int need_cfg = 1;
|
|
|
|
DPRINTF("%s: offset = %x, len = %d\n", __func__, offset, bytes);
|
|
|
|
if (offset == ESB_LOCK_REG && bytes == 1) {
|
|
*rv = (wdt_state.locked ? ESB_WDT_LOCK : 0) |
|
|
(wdt_state.wdt_enabled ? ESB_WDT_ENABLE : 0);
|
|
|
|
need_cfg = 0;
|
|
}
|
|
|
|
return need_cfg;
|
|
}
|
|
|
|
static int
|
|
pci_wdt_cfg_write(struct vmctx *ctx, int vcpu, struct pci_vdev *dev,
|
|
int offset, int bytes, uint32_t val)
|
|
{
|
|
bool old_flag;
|
|
int need_cfg = 1;
|
|
|
|
DPRINTF("%s: offset = %x, len = %d, val = 0x%x\n",
|
|
__func__, offset, bytes, val);
|
|
|
|
if (offset == ESB_CONFIG_REG && bytes == 2) {
|
|
wdt_state.reboot_enabled = ((val & ESB_WDT_REBOOT) == 0);
|
|
wdt_state.intr_enabled = ((val & ESB_WDT_INT_MSK) == 0);
|
|
need_cfg = 0;
|
|
|
|
} else if (offset == ESB_LOCK_REG && bytes == 1) {
|
|
if (!wdt_state.locked) {
|
|
wdt_state.locked = ((val & ESB_WDT_LOCK) != 0);
|
|
old_flag = wdt_state.wdt_enabled;
|
|
wdt_state.wdt_enabled = ((val & ESB_WDT_ENABLE) != 0);
|
|
|
|
if (!old_flag && wdt_state.wdt_enabled) {
|
|
wdt_state.stage = 1;
|
|
start_wdt_timer();
|
|
} else if (!wdt_state.wdt_enabled)
|
|
stop_wdt_timer();
|
|
}
|
|
|
|
need_cfg = 0;
|
|
}
|
|
|
|
return need_cfg;
|
|
}
|
|
|
|
static void
|
|
pci_wdt_bar_write(struct vmctx *ctx, int vcpu, struct pci_vdev *dev,
|
|
int baridx, uint64_t offset, int size, uint64_t value)
|
|
{
|
|
DPRINTF("%s: addr = 0x%x, val = 0x%x, size=%d\n",
|
|
__func__, (int) offset, (int)value, size);
|
|
|
|
if (offset == ESB_GIS_REG) {
|
|
if ((value & ESB_WDT_INT_ACT) == 1) {
|
|
|
|
wdt_state.intr_active = false;
|
|
|
|
if ((wdt_state.intr_enabled == true)
|
|
&& (dev->lintr.state == ASSERTED)) {
|
|
pci_lintr_deassert(dev);
|
|
DPRINTF("%s: intr deasserted\n\r", __func__);
|
|
}
|
|
}
|
|
} else if (offset == ESB_RELOAD_REG) {
|
|
if (size != 2)
|
|
return;
|
|
|
|
if (value == ESB_UNLOCK1)
|
|
wdt_state.unlock_state = 1;
|
|
else if ((value == ESB_UNLOCK2)
|
|
&& (wdt_state.unlock_state == 1))
|
|
wdt_state.unlock_state = 2;
|
|
else if (wdt_state.unlock_state == 2) {
|
|
if (value & ESB_WDT_RELOAD) {
|
|
wdt_state.stage = 1;
|
|
start_wdt_timer();
|
|
}
|
|
|
|
/* write ES_WDT_TIMEOUT bit clear wdt timeout */
|
|
if (value & ESB_WDT_TIMEOUT) {
|
|
DPRINTF("%s: timeout cleaned\n\r", __func__);
|
|
wdt_timeout = 0;
|
|
}
|
|
|
|
wdt_state.unlock_state = 0;
|
|
}
|
|
} else if (wdt_state.unlock_state == 2) {
|
|
if (offset == ESB_TIMER1_REG)
|
|
wdt_state.timer1_val = value & DEFAULT_MAX_TIMER_VAL;
|
|
else if (offset == ESB_TIMER2_REG)
|
|
wdt_state.timer2_val = value & DEFAULT_MAX_TIMER_VAL;
|
|
|
|
wdt_state.unlock_state = 0;
|
|
}
|
|
}
|
|
|
|
uint64_t
|
|
pci_wdt_bar_read(struct vmctx *ctx, int vcpu, struct pci_vdev *dev,
|
|
int baridx, uint64_t offset, int size)
|
|
{
|
|
uint64_t ret = 0;
|
|
|
|
DPRINTF("%s: addr = 0x%x, size=%d\n\r", __func__, (int) offset, size);
|
|
|
|
if (offset == ESB_GIS_REG) {
|
|
if ((wdt_state.intr_enabled == true)
|
|
&& (wdt_state.intr_active == true))
|
|
ret |= ESB_WDT_INT_ACT;
|
|
|
|
} else if (offset == ESB_RELOAD_REG) {
|
|
if (size != 2)
|
|
return 0;
|
|
|
|
DPRINTF("%s: timeout: %d\n\r", __func__, wdt_timeout);
|
|
if (wdt_timeout != 0)
|
|
ret |= ESB_WDT_TIMEOUT;
|
|
|
|
if (wdt_state.stage == 1)
|
|
ret |= ESB_WDT_RELOAD;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
pci_wdt_init(struct vmctx *ctx, struct pci_vdev *dev, char *opts)
|
|
{
|
|
/*the wdt just has one inistance */
|
|
if (wdt_state.reboot_enabled && wdt_state.timer1_val) {
|
|
pr_err("wdt can't be initialized twice, please check!");
|
|
return -1;
|
|
}
|
|
|
|
/* init wdt state info */
|
|
wdt_state.timer.clockid = CLOCK_MONOTONIC;
|
|
if (acrn_timer_init(&wdt_state.timer, wdt_expired_handler, dev) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
wdt_state.reboot_enabled = true;
|
|
wdt_state.intr_enabled = false;
|
|
wdt_state.intr_active = false;
|
|
wdt_state.locked = false;
|
|
wdt_state.wdt_armed = false;
|
|
wdt_state.wdt_enabled = false;
|
|
|
|
wdt_state.stage = 1;
|
|
wdt_state.timer1_val = DEFAULT_MAX_TIMER_VAL;
|
|
wdt_state.timer2_val = DEFAULT_MAX_TIMER_VAL;
|
|
wdt_state.unlock_state = 0;
|
|
|
|
pci_emul_alloc_bar(dev, 0, PCIBAR_MEM32, WDT_REG_BAR_SIZE);
|
|
|
|
/* initialize config space */
|
|
pci_set_cfgdata16(dev, PCIR_VENDOR, PCI_VENDOR_ID_INTEL);
|
|
pci_set_cfgdata16(dev, PCIR_DEVICE, PCI_DEVICE_ID_INTEL_ESB);
|
|
pci_set_cfgdata8(dev, PCIR_CLASS, PCIC_BASEPERIPH);
|
|
pci_set_cfgdata8(dev, PCIR_SUBCLASS, PCIS_BASEPERIPH_OTHER);
|
|
|
|
pci_emul_add_msicap(dev, 1);
|
|
pci_lintr_request(dev);
|
|
|
|
#ifdef WDT_DEBUG
|
|
dbg_file = fopen("/tmp/wdt_log", "w+");
|
|
#endif
|
|
|
|
DPRINTF("%s: iobar =0x%lx, size=%ld\n", __func__,
|
|
dev->bar[0].addr, dev->bar[0].size);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
pci_wdt_deinit(struct vmctx *ctx, struct pci_vdev *dev, char *opts)
|
|
{
|
|
acrn_timer_deinit(&wdt_state.timer);
|
|
|
|
memset(&wdt_state, 0, sizeof(wdt_state));
|
|
|
|
pci_lintr_release(dev);
|
|
|
|
}
|
|
|
|
/* stop/reset watchdog will be invoked during guest enter/exit S3.
|
|
* We stop watchdog timer when guest enter S3 to avoid watchdog trigger
|
|
* guest reset when guest is in S3 state.
|
|
*
|
|
* We reset watchdog with a long peroid (2 * 60s) during guest exit
|
|
* from S3 to handle system hang before watchdog in guest kernel start to
|
|
* work.
|
|
*/
|
|
void
|
|
vm_stop_watchdog(struct vmctx *ctx)
|
|
{
|
|
stop_wdt_timer();
|
|
}
|
|
|
|
void
|
|
vm_reset_watchdog(struct vmctx *ctx)
|
|
{
|
|
wdt_state.stage = 1;
|
|
wdt_state.timer1_val = DEFAULT_RESET_TIMER_VAL;
|
|
wdt_state.timer2_val = DEFAULT_RESET_TIMER_VAL;
|
|
wdt_state.unlock_state = 0;
|
|
|
|
start_wdt_timer();
|
|
}
|
|
|
|
struct pci_vdev_ops pci_ops_wdt = {
|
|
.class_name = "wdt-i6300esb",
|
|
.vdev_init = pci_wdt_init,
|
|
.vdev_deinit = pci_wdt_deinit,
|
|
.vdev_cfgwrite = pci_wdt_cfg_write,
|
|
.vdev_cfgread = pci_wdt_cfg_read,
|
|
.vdev_barwrite = pci_wdt_bar_write,
|
|
.vdev_barread = pci_wdt_bar_read
|
|
};
|
|
|
|
DEFINE_PCI_DEVTYPE(pci_ops_wdt);
|