diff options
Diffstat (limited to 'hw/watchdog/sbsa_gwdt.c')
-rw-r--r-- | hw/watchdog/sbsa_gwdt.c | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/hw/watchdog/sbsa_gwdt.c b/hw/watchdog/sbsa_gwdt.c new file mode 100644 index 000000000..e49cacd0e --- /dev/null +++ b/hw/watchdog/sbsa_gwdt.c @@ -0,0 +1,294 @@ +/* + * Generic watchdog device model for SBSA + * + * The watchdog device has been implemented as revision 1 variant of + * the ARM SBSA specification v6.0 + * (https://developer.arm.com/documentation/den0029/d?lang=en) + * + * Copyright Linaro.org 2020 + * + * Authors: + * Shashi Mallela <shashi.mallela@linaro.org> + * + * This work is licensed under the terms of the GNU GPL, version 2 or (at your + * option) any later version. See the COPYING file in the top-level directory. + * + */ + +#include "qemu/osdep.h" +#include "sysemu/reset.h" +#include "sysemu/watchdog.h" +#include "hw/watchdog/sbsa_gwdt.h" +#include "qemu/timer.h" +#include "migration/vmstate.h" +#include "qemu/log.h" +#include "qemu/module.h" + +static WatchdogTimerModel model = { + .wdt_name = TYPE_WDT_SBSA, + .wdt_description = "SBSA-compliant generic watchdog device", +}; + +static const VMStateDescription vmstate_sbsa_gwdt = { + .name = "sbsa-gwdt", + .version_id = 1, + .minimum_version_id = 1, + .fields = (VMStateField[]) { + VMSTATE_TIMER_PTR(timer, SBSA_GWDTState), + VMSTATE_UINT32(wcs, SBSA_GWDTState), + VMSTATE_UINT32(worl, SBSA_GWDTState), + VMSTATE_UINT32(woru, SBSA_GWDTState), + VMSTATE_UINT32(wcvl, SBSA_GWDTState), + VMSTATE_UINT32(wcvu, SBSA_GWDTState), + VMSTATE_END_OF_LIST() + } +}; + +typedef enum WdtRefreshType { + EXPLICIT_REFRESH = 0, + TIMEOUT_REFRESH = 1, +} WdtRefreshType; + +static uint64_t sbsa_gwdt_rread(void *opaque, hwaddr addr, unsigned int size) +{ + SBSA_GWDTState *s = SBSA_GWDT(opaque); + uint32_t ret = 0; + + switch (addr) { + case SBSA_GWDT_WRR: + /* watch refresh read has no effect and returns 0 */ + ret = 0; + break; + case SBSA_GWDT_W_IIDR: + ret = s->id; + break; + default: + qemu_log_mask(LOG_GUEST_ERROR, "bad address in refresh frame read :" + " 0x%x\n", (int)addr); + } + return ret; +} + +static uint64_t sbsa_gwdt_read(void *opaque, hwaddr addr, unsigned int size) +{ + SBSA_GWDTState *s = SBSA_GWDT(opaque); + uint32_t ret = 0; + + switch (addr) { + case SBSA_GWDT_WCS: + ret = s->wcs; + break; + case SBSA_GWDT_WOR: + ret = s->worl; + break; + case SBSA_GWDT_WORU: + ret = s->woru; + break; + case SBSA_GWDT_WCV: + ret = s->wcvl; + break; + case SBSA_GWDT_WCVU: + ret = s->wcvu; + break; + case SBSA_GWDT_W_IIDR: + ret = s->id; + break; + default: + qemu_log_mask(LOG_GUEST_ERROR, "bad address in control frame read :" + " 0x%x\n", (int)addr); + } + return ret; +} + +static void sbsa_gwdt_update_timer(SBSA_GWDTState *s, WdtRefreshType rtype) +{ + uint64_t timeout = 0; + + timer_del(s->timer); + + if (s->wcs & SBSA_GWDT_WCS_EN) { + /* + * Extract the upper 16 bits from woru & 32 bits from worl + * registers to construct the 48 bit offset value + */ + timeout = s->woru; + timeout <<= 32; + timeout |= s->worl; + timeout = muldiv64(timeout, NANOSECONDS_PER_SECOND, SBSA_TIMER_FREQ); + timeout += qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL); + + if ((rtype == EXPLICIT_REFRESH) || ((rtype == TIMEOUT_REFRESH) && + (!(s->wcs & SBSA_GWDT_WCS_WS0)))) { + /* store the current timeout value into compare registers */ + s->wcvu = timeout >> 32; + s->wcvl = timeout; + } + timer_mod(s->timer, timeout); + } +} + +static void sbsa_gwdt_rwrite(void *opaque, hwaddr offset, uint64_t data, + unsigned size) { + SBSA_GWDTState *s = SBSA_GWDT(opaque); + + if (offset == SBSA_GWDT_WRR) { + s->wcs &= ~(SBSA_GWDT_WCS_WS0 | SBSA_GWDT_WCS_WS1); + + sbsa_gwdt_update_timer(s, EXPLICIT_REFRESH); + } else { + qemu_log_mask(LOG_GUEST_ERROR, "bad address in refresh frame write :" + " 0x%x\n", (int)offset); + } +} + +static void sbsa_gwdt_write(void *opaque, hwaddr offset, uint64_t data, + unsigned size) { + SBSA_GWDTState *s = SBSA_GWDT(opaque); + + switch (offset) { + case SBSA_GWDT_WCS: + s->wcs = data & SBSA_GWDT_WCS_EN; + qemu_set_irq(s->irq, 0); + sbsa_gwdt_update_timer(s, EXPLICIT_REFRESH); + break; + + case SBSA_GWDT_WOR: + s->worl = data; + s->wcs &= ~(SBSA_GWDT_WCS_WS0 | SBSA_GWDT_WCS_WS1); + qemu_set_irq(s->irq, 0); + sbsa_gwdt_update_timer(s, EXPLICIT_REFRESH); + break; + + case SBSA_GWDT_WORU: + s->woru = data & SBSA_GWDT_WOR_MASK; + s->wcs &= ~(SBSA_GWDT_WCS_WS0 | SBSA_GWDT_WCS_WS1); + qemu_set_irq(s->irq, 0); + sbsa_gwdt_update_timer(s, EXPLICIT_REFRESH); + break; + + case SBSA_GWDT_WCV: + s->wcvl = data; + break; + + case SBSA_GWDT_WCVU: + s->wcvu = data; + break; + + default: + qemu_log_mask(LOG_GUEST_ERROR, "bad address in control frame write :" + " 0x%x\n", (int)offset); + } + return; +} + +static void wdt_sbsa_gwdt_reset(DeviceState *dev) +{ + SBSA_GWDTState *s = SBSA_GWDT(dev); + + timer_del(s->timer); + + s->wcs = 0; + s->wcvl = 0; + s->wcvu = 0; + s->worl = 0; + s->woru = 0; + s->id = SBSA_GWDT_ID; +} + +static void sbsa_gwdt_timer_sysinterrupt(void *opaque) +{ + SBSA_GWDTState *s = SBSA_GWDT(opaque); + + if (!(s->wcs & SBSA_GWDT_WCS_WS0)) { + s->wcs |= SBSA_GWDT_WCS_WS0; + sbsa_gwdt_update_timer(s, TIMEOUT_REFRESH); + qemu_set_irq(s->irq, 1); + } else { + s->wcs |= SBSA_GWDT_WCS_WS1; + qemu_log_mask(CPU_LOG_RESET, "Watchdog timer expired.\n"); + /* + * Reset the watchdog only if the guest gets notified about + * expiry. watchdog_perform_action() may temporarily relinquish + * the BQL; reset before triggering the action to avoid races with + * sbsa_gwdt instructions. + */ + switch (get_watchdog_action()) { + case WATCHDOG_ACTION_DEBUG: + case WATCHDOG_ACTION_NONE: + case WATCHDOG_ACTION_PAUSE: + break; + default: + wdt_sbsa_gwdt_reset(DEVICE(s)); + } + watchdog_perform_action(); + } +} + +static const MemoryRegionOps sbsa_gwdt_rops = { + .read = sbsa_gwdt_rread, + .write = sbsa_gwdt_rwrite, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid.min_access_size = 4, + .valid.max_access_size = 4, + .valid.unaligned = false, +}; + +static const MemoryRegionOps sbsa_gwdt_ops = { + .read = sbsa_gwdt_read, + .write = sbsa_gwdt_write, + .endianness = DEVICE_LITTLE_ENDIAN, + .valid.min_access_size = 4, + .valid.max_access_size = 4, + .valid.unaligned = false, +}; + +static void wdt_sbsa_gwdt_realize(DeviceState *dev, Error **errp) +{ + SBSA_GWDTState *s = SBSA_GWDT(dev); + SysBusDevice *sbd = SYS_BUS_DEVICE(dev); + + memory_region_init_io(&s->rmmio, OBJECT(dev), + &sbsa_gwdt_rops, s, + "sbsa_gwdt.refresh", + SBSA_GWDT_RMMIO_SIZE); + + memory_region_init_io(&s->cmmio, OBJECT(dev), + &sbsa_gwdt_ops, s, + "sbsa_gwdt.control", + SBSA_GWDT_CMMIO_SIZE); + + sysbus_init_mmio(sbd, &s->rmmio); + sysbus_init_mmio(sbd, &s->cmmio); + + sysbus_init_irq(sbd, &s->irq); + + s->timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, sbsa_gwdt_timer_sysinterrupt, + dev); +} + +static void wdt_sbsa_gwdt_class_init(ObjectClass *klass, void *data) +{ + DeviceClass *dc = DEVICE_CLASS(klass); + + dc->realize = wdt_sbsa_gwdt_realize; + dc->reset = wdt_sbsa_gwdt_reset; + dc->hotpluggable = false; + set_bit(DEVICE_CATEGORY_WATCHDOG, dc->categories); + dc->vmsd = &vmstate_sbsa_gwdt; + dc->desc = "SBSA-compliant generic watchdog device"; +} + +static const TypeInfo wdt_sbsa_gwdt_info = { + .class_init = wdt_sbsa_gwdt_class_init, + .parent = TYPE_SYS_BUS_DEVICE, + .name = TYPE_WDT_SBSA, + .instance_size = sizeof(SBSA_GWDTState), +}; + +static void wdt_sbsa_gwdt_register_types(void) +{ + watchdog_add_model(&model); + type_register_static(&wdt_sbsa_gwdt_info); +} + +type_init(wdt_sbsa_gwdt_register_types) |