Model the RP2040 watchdog control, load, reason, scratch and tick
registers. Drive the counter from clk_ref with the RP2040-E1 twice-per-tick
behavior and use QEMU's standard watchdog action for timer expiry and
forced resets. Add qtests for reset state, sub-word scratch accesses, timer
expiry and forced triggering.

Signed-off-by: gilles grimaud <[email protected]>
---
 hw/arm/rp2040.c                    |  11 +-
 hw/misc/meson.build                |   5 +-
 hw/misc/rp2040_watchdog.c          | 349 +++++++++++++++++++++++++++++
 include/hw/arm/rp2040.h            |   2 +
 include/hw/misc/rp2040_watchdog.h  |  36 +++
 tests/qtest/meson.build            |   3 +-
 tests/qtest/rp2040-watchdog-test.c | 170 ++++++++++++++
 7 files changed, 573 insertions(+), 3 deletions(-)
 create mode 100644 hw/misc/rp2040_watchdog.c
 create mode 100644 include/hw/misc/rp2040_watchdog.h
 create mode 100644 tests/qtest/rp2040-watchdog-test.c

diff --git a/hw/arm/rp2040.c b/hw/arm/rp2040.c
index fa28859309..140c73ae78 100644
--- a/hw/arm/rp2040.c
+++ b/hw/arm/rp2040.c
@@ -62,7 +62,6 @@ static const struct {
     { "rp2040.adc",      0x4004c000, 0x4000 },
     { "rp2040.pwm",      0x40050000, 0x4000 },
     { "rp2040.timer",    0x40054000, 0x4000 },
-    { "rp2040.watchdog", 0x40058000, 0x4000 },
     { "rp2040.rtc",      0x4005c000, 0x4000 },
     { "rp2040.dma",      0x50000000, 0x1000 },
     { "rp2040.usbctrl_dpram", 0x50100000, 0x10000 },
@@ -153,6 +152,8 @@ static void rp2040_soc_init(Object *obj)
     object_initialize_child(obj, "rosc", &s->rosc, TYPE_RP2040_ROSC);
     object_initialize_child(obj, "tbman", &s->tbman, TYPE_RP2040_TBMAN);
     object_initialize_child(obj, "vreg", &s->vreg, TYPE_RP2040_VREG);
+    object_initialize_child(obj, "watchdog", &s->watchdog,
+                            TYPE_RP2040_WATCHDOG);
     object_initialize_child(obj, "xosc", &s->xosc, TYPE_RP2040_XOSC);
 
     s->irq = qemu_allocate_irqs(rp2040_set_irq, s, RP2040_NUM_IRQS);
@@ -332,6 +333,14 @@ static void rp2040_soc_realize(DeviceState *dev, Error 
**errp)
     }
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->vreg), 0, RP2040_VREG_BASE);
 
+    qdev_connect_clock_in(DEVICE(&s->watchdog), "clk-ref",
+                          qdev_get_clock_out(DEVICE(&s->clocks), "clk-ref"));
+    if (!sysbus_realize(SYS_BUS_DEVICE(&s->watchdog), errp)) {
+        return;
+    }
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->watchdog), 0,
+                    RP2040_WATCHDOG_BASE);
+
     if (!sysbus_realize(SYS_BUS_DEVICE(&s->xosc), errp)) {
         return;
     }
diff --git a/hw/misc/meson.build b/hw/misc/meson.build
index c04e2ffe22..e03f45d667 100644
--- a/hw/misc/meson.build
+++ b/hw/misc/meson.build
@@ -104,9 +104,12 @@ system_ss.add(when: 'CONFIG_RP2040', if_true: 
files('rp2040_psm.c'))
 system_ss.add(when: 'CONFIG_RP2040', if_true: files('rp2040_resets.c'))
 system_ss.add(when: 'CONFIG_RP2040', if_true: files('rp2040_rosc.c'))
 system_ss.add(when: 'CONFIG_RP2040', if_true: files('rp2040_syscfg.c'))
-system_ss.add(when: 'CONFIG_RP2040', if_true: files('rp2040_sysinfo.c'))
+system_ss.add(when: 'CONFIG_RP2040',
+              if_true: files('rp2040_sysinfo.c'))
 system_ss.add(when: 'CONFIG_RP2040', if_true: files('rp2040_tbman.c'))
 system_ss.add(when: 'CONFIG_RP2040', if_true: files('rp2040_vreg.c'))
+system_ss.add(when: 'CONFIG_RP2040',
+              if_true: files('rp2040_watchdog.c'))
 system_ss.add(when: 'CONFIG_RP2040', if_true: files('rp2040_xosc.c'))
 system_ss.add(when: 'CONFIG_SLAVIO', if_true: files('slavio_misc.c'))
 system_ss.add(when: 'CONFIG_ZYNQ', if_true: files('zynq_slcr.c'))
diff --git a/hw/misc/rp2040_watchdog.c b/hw/misc/rp2040_watchdog.c
new file mode 100644
index 0000000000..b15e30a285
--- /dev/null
+++ b/hw/misc/rp2040_watchdog.c
@@ -0,0 +1,349 @@
+/*
+ * RP2040 watchdog emulation
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/core/qdev-clock.h"
+#include "hw/misc/rp2040_watchdog.h"
+#include "hw/misc/rp2040.h"
+#include "migration/vmstate.h"
+#include "qapi/error.h"
+#include "qemu/log.h"
+#include "qemu/module.h"
+#include "qemu/timer.h"
+#include "system/watchdog.h"
+
+#define WATCHDOG_CTRL       0x00
+#define WATCHDOG_LOAD       0x04
+#define WATCHDOG_REASON     0x08
+#define WATCHDOG_SCRATCH0   0x0c
+#define WATCHDOG_SCRATCH7   0x28
+#define WATCHDOG_TICK       0x2c
+
+#define WATCHDOG_CTRL_TRIGGER   BIT(31)
+#define WATCHDOG_CTRL_ENABLE    BIT(30)
+#define WATCHDOG_CTRL_PAUSE_DBG1 BIT(26)
+#define WATCHDOG_CTRL_PAUSE_DBG0 BIT(25)
+#define WATCHDOG_CTRL_PAUSE_JTAG BIT(24)
+#define WATCHDOG_CTRL_PAUSE_MASK \
+    (WATCHDOG_CTRL_PAUSE_DBG1 | WATCHDOG_CTRL_PAUSE_DBG0 | \
+     WATCHDOG_CTRL_PAUSE_JTAG)
+#define WATCHDOG_CTRL_TIME_MASK 0x00ffffff
+#define WATCHDOG_CTRL_RW_MASK \
+    (WATCHDOG_CTRL_ENABLE | WATCHDOG_CTRL_PAUSE_MASK)
+
+#define WATCHDOG_LOAD_MASK  0x00ffffff
+#define WATCHDOG_REASON_FORCE   BIT(1)
+#define WATCHDOG_REASON_TIMER   BIT(0)
+
+#define WATCHDOG_TICK_ENABLE    BIT(9)
+#define WATCHDOG_TICK_RUNNING   BIT(10)
+#define WATCHDOG_TICK_CYCLES_MASK 0x000001ff
+#define WATCHDOG_TICK_RW_MASK \
+    (WATCHDOG_TICK_ENABLE | WATCHDOG_TICK_CYCLES_MASK)
+#define WATCHDOG_TICK_COUNT_SHIFT 11
+
+static uint32_t rp2040_watchdog_expand_write(uint32_t value, unsigned size)
+{
+    switch (size) {
+    case 1:
+        return value * 0x01010101u;
+    case 2:
+        return (value & 0xffffu) * 0x00010001u;
+    default:
+        return value;
+    }
+}
+
+static bool rp2040_watchdog_scratch_offset(hwaddr offset, unsigned *index)
+{
+    if (offset < WATCHDOG_SCRATCH0 || offset > WATCHDOG_SCRATCH7) {
+        return false;
+    }
+
+    *index = (offset - WATCHDOG_SCRATCH0) / sizeof(uint32_t);
+    return true;
+}
+
+static bool rp2040_watchdog_tick_running(RP2040WatchdogState *s)
+{
+    return (s->tick & WATCHDOG_TICK_ENABLE) &&
+           (s->tick & WATCHDOG_TICK_CYCLES_MASK) &&
+           clock_get_hz(s->clk_ref);
+}
+
+static uint32_t rp2040_watchdog_tick_freq_hz(RP2040WatchdogState *s)
+{
+    uint64_t clk_ref_hz = clock_get_hz(s->clk_ref);
+    uint32_t cycles = s->tick & WATCHDOG_TICK_CYCLES_MASK;
+
+    if (!cycles || !clk_ref_hz || !(s->tick & WATCHDOG_TICK_ENABLE)) {
+        return 0;
+    }
+
+    return MIN(UINT32_MAX, (uint32_t)(clk_ref_hz / cycles));
+}
+
+static uint32_t rp2040_watchdog_tick_read(RP2040WatchdogState *s)
+{
+    uint32_t value = s->tick & WATCHDOG_TICK_RW_MASK;
+
+    if (rp2040_watchdog_tick_running(s)) {
+        /*
+         * The precise divider phase is not observable by current firmware
+         * tests. Report a stable running state and a valid non-zero count.
+         */
+        value |= WATCHDOG_TICK_RUNNING;
+        value |= (s->tick & WATCHDOG_TICK_CYCLES_MASK) <<
+                 WATCHDOG_TICK_COUNT_SHIFT;
+    }
+
+    return value;
+}
+
+static void rp2040_watchdog_update(RP2040WatchdogState *s)
+{
+    uint32_t tick_hz = rp2040_watchdog_tick_freq_hz(s);
+    uint64_t watchdog_hz = (uint64_t)tick_hz * 2;
+
+    ptimer_transaction_begin(s->timer);
+    if (!tick_hz) {
+        ptimer_stop(s->timer);
+    } else {
+        /*
+         * RP2040-E1: the watchdog counter decrements twice per watchdog
+         * tick, so a LOAD of N expires after N / 2 generated ticks.
+         */
+        ptimer_set_freq(s->timer, MIN(watchdog_hz, (uint64_t)UINT32_MAX));
+        if (s->ctrl & WATCHDOG_CTRL_ENABLE) {
+            ptimer_run(s->timer, 1);
+        } else {
+            ptimer_stop(s->timer);
+        }
+    }
+    ptimer_transaction_commit(s->timer);
+}
+
+static uint32_t rp2040_watchdog_ctrl_read(RP2040WatchdogState *s)
+{
+    uint32_t time = MIN(ptimer_get_count(s->timer), WATCHDOG_CTRL_TIME_MASK);
+
+    return (s->ctrl & WATCHDOG_CTRL_RW_MASK) | time;
+}
+
+static void rp2040_watchdog_reset_request(RP2040WatchdogState *s,
+                                          uint32_t reason,
+                                          bool in_ptimer_transaction)
+{
+    s->reason = reason;
+    s->preserve_reason_on_reset =
+        get_watchdog_action() == WATCHDOG_ACTION_RESET;
+    if (!in_ptimer_transaction) {
+        ptimer_transaction_begin(s->timer);
+    }
+    ptimer_stop(s->timer);
+    if (!in_ptimer_transaction) {
+        ptimer_transaction_commit(s->timer);
+    }
+    watchdog_perform_action();
+}
+
+static uint64_t rp2040_watchdog_read(void *opaque, hwaddr addr, unsigned size)
+{
+    RP2040WatchdogState *s = opaque;
+    hwaddr offset = addr & 0xfff;
+    hwaddr word_offset = offset & ~3ULL;
+    unsigned scratch;
+    uint64_t value;
+
+    switch (word_offset) {
+    case WATCHDOG_CTRL:
+        value = rp2040_watchdog_ctrl_read(s);
+        break;
+    case WATCHDOG_LOAD:
+        value = 0;
+        break;
+    case WATCHDOG_REASON:
+        value = s->reason;
+        break;
+    case WATCHDOG_TICK:
+        value = rp2040_watchdog_tick_read(s);
+        break;
+    default:
+        if (rp2040_watchdog_scratch_offset(word_offset, &scratch)) {
+            value = s->scratch[scratch];
+        } else {
+            value = 0;
+            qemu_log_mask(LOG_UNIMP,
+                      "%s: unimplemented read at offset 0x%"
+                      HWADDR_PRIx "\n", __func__, addr & 0xfff);
+        }
+        break;
+    }
+
+    return extract64(value, (offset & 3) * 8, size * 8);
+}
+
+static void rp2040_watchdog_write(void *opaque, hwaddr addr,
+                                  uint64_t value64, unsigned size)
+{
+    RP2040WatchdogState *s = opaque;
+    hwaddr alias = addr & RP2040_ATOMIC_ALIAS_MASK;
+    hwaddr offset = addr & 0xfff;
+    hwaddr word_offset = offset & ~3ULL;
+    unsigned scratch;
+    uint32_t value = rp2040_watchdog_expand_write(value64, size);
+
+    switch (word_offset) {
+    case WATCHDOG_CTRL: {
+        uint32_t old = rp2040_watchdog_ctrl_read(s);
+        uint32_t newval = rp2040_atomic_update(old, value, alias);
+
+        s->ctrl = newval & WATCHDOG_CTRL_RW_MASK;
+        rp2040_watchdog_update(s);
+        if (newval & WATCHDOG_CTRL_TRIGGER) {
+            rp2040_watchdog_reset_request(s, WATCHDOG_REASON_FORCE, false);
+        }
+        break;
+    }
+    case WATCHDOG_LOAD:
+        s->load = rp2040_atomic_update(s->load, value, alias) &
+                  WATCHDOG_LOAD_MASK;
+        ptimer_transaction_begin(s->timer);
+        ptimer_set_limit(s->timer, s->load, 1);
+        ptimer_transaction_commit(s->timer);
+        rp2040_watchdog_update(s);
+        break;
+    case WATCHDOG_REASON:
+        break;
+    case WATCHDOG_TICK:
+        s->tick = rp2040_atomic_update(s->tick, value, alias) &
+                  WATCHDOG_TICK_RW_MASK;
+        rp2040_watchdog_update(s);
+        break;
+    default:
+        if (rp2040_watchdog_scratch_offset(word_offset, &scratch)) {
+            s->scratch[scratch] =
+                rp2040_atomic_update(s->scratch[scratch], value,
+                                            alias);
+        } else {
+            qemu_log_mask(LOG_UNIMP,
+                      "%s: unimplemented write at offset 0x%"
+                      HWADDR_PRIx "\n", __func__, addr & 0xfff);
+        }
+        break;
+    }
+}
+
+static const MemoryRegionOps rp2040_watchdog_ops = {
+    .read = rp2040_watchdog_read,
+    .write = rp2040_watchdog_write,
+    .endianness = DEVICE_LITTLE_ENDIAN,
+    .valid = {
+        .min_access_size = 1,
+        .max_access_size = 4,
+        .unaligned = false,
+    },
+};
+
+static void rp2040_watchdog_reset(DeviceState *dev)
+{
+    RP2040WatchdogState *s = RP2040_WATCHDOG(dev);
+
+    if (s->preserve_reason_on_reset) {
+        s->preserve_reason_on_reset = false;
+    } else {
+        s->reason = 0;
+    }
+    s->ctrl = WATCHDOG_CTRL_PAUSE_MASK;
+    s->load = 0;
+    s->tick = WATCHDOG_TICK_ENABLE;
+
+    ptimer_transaction_begin(s->timer);
+    ptimer_stop(s->timer);
+    ptimer_set_limit(s->timer, 0, 1);
+    ptimer_transaction_commit(s->timer);
+}
+
+static void rp2040_watchdog_clk_ref_update(void *opaque, ClockEvent event)
+{
+    rp2040_watchdog_update(opaque);
+}
+
+static void rp2040_watchdog_tick(void *opaque)
+{
+    RP2040WatchdogState *s = RP2040_WATCHDOG(opaque);
+
+    rp2040_watchdog_reset_request(s, WATCHDOG_REASON_TIMER, true);
+}
+
+static void rp2040_watchdog_init(Object *obj)
+{
+    RP2040WatchdogState *s = RP2040_WATCHDOG(obj);
+    DeviceState *dev = DEVICE(obj);
+
+    s->clk_ref = qdev_init_clock_in(dev, "clk-ref",
+                                    rp2040_watchdog_clk_ref_update, s,
+                                    ClockUpdate);
+    memory_region_init_io(&s->iomem, obj, &rp2040_watchdog_ops, s,
+                          "rp2040.watchdog", RP2040_WATCHDOG_SIZE);
+    sysbus_init_mmio(SYS_BUS_DEVICE(obj), &s->iomem);
+}
+
+static void rp2040_watchdog_realize(DeviceState *dev, Error **errp)
+{
+    RP2040WatchdogState *s = RP2040_WATCHDOG(dev);
+
+    if (!clock_has_source(s->clk_ref)) {
+        error_setg(errp, "RP2040 watchdog: clk-ref clock must be connected");
+        return;
+    }
+
+    s->timer = ptimer_init(rp2040_watchdog_tick, s,
+                           PTIMER_POLICY_WRAP_AFTER_ONE_PERIOD |
+                           PTIMER_POLICY_TRIGGER_ONLY_ON_DECREMENT |
+                           PTIMER_POLICY_NO_IMMEDIATE_RELOAD |
+                           PTIMER_POLICY_NO_COUNTER_ROUND_DOWN);
+}
+
+static const VMStateDescription rp2040_watchdog_vmstate = {
+    .name = TYPE_RP2040_WATCHDOG,
+    .version_id = 1,
+    .minimum_version_id = 1,
+    .fields = (const VMStateField[]) {
+        VMSTATE_CLOCK(clk_ref, RP2040WatchdogState),
+        VMSTATE_PTIMER(timer, RP2040WatchdogState),
+        VMSTATE_UINT32(ctrl, RP2040WatchdogState),
+        VMSTATE_UINT32(load, RP2040WatchdogState),
+        VMSTATE_UINT32(reason, RP2040WatchdogState),
+        VMSTATE_BOOL(preserve_reason_on_reset, RP2040WatchdogState),
+        VMSTATE_UINT32_ARRAY(scratch, RP2040WatchdogState, 8),
+        VMSTATE_UINT32(tick, RP2040WatchdogState),
+        VMSTATE_END_OF_LIST()
+    }
+};
+
+static void rp2040_watchdog_class_init(ObjectClass *klass, const void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+
+    dc->realize = rp2040_watchdog_realize;
+    device_class_set_legacy_reset(dc, rp2040_watchdog_reset);
+    dc->vmsd = &rp2040_watchdog_vmstate;
+}
+
+static const TypeInfo rp2040_watchdog_info = {
+    .name          = TYPE_RP2040_WATCHDOG,
+    .parent        = TYPE_SYS_BUS_DEVICE,
+    .instance_size = sizeof(RP2040WatchdogState),
+    .instance_init = rp2040_watchdog_init,
+    .class_init    = rp2040_watchdog_class_init,
+};
+
+static void rp2040_watchdog_register_types(void)
+{
+    type_register_static(&rp2040_watchdog_info);
+}
+type_init(rp2040_watchdog_register_types)
diff --git a/include/hw/arm/rp2040.h b/include/hw/arm/rp2040.h
index 1c69ce089b..c3c6419613 100644
--- a/include/hw/arm/rp2040.h
+++ b/include/hw/arm/rp2040.h
@@ -22,6 +22,7 @@
 #include "hw/misc/rp2040_syscfg.h"
 #include "hw/misc/rp2040_tbman.h"
 #include "hw/misc/rp2040_vreg.h"
+#include "hw/misc/rp2040_watchdog.h"
 #include "hw/misc/rp2040_xosc.h"
 #include "qom/object.h"
 
@@ -55,6 +56,7 @@ struct RP2040State {
     RP2040RoscState rosc;
     RP2040TbmanState tbman;
     RP2040VregState vreg;
+    RP2040WatchdogState watchdog;
     RP2040XoscState xosc;
 
     MemoryRegion *board_memory;
diff --git a/include/hw/misc/rp2040_watchdog.h 
b/include/hw/misc/rp2040_watchdog.h
new file mode 100644
index 0000000000..9c3cb375e6
--- /dev/null
+++ b/include/hw/misc/rp2040_watchdog.h
@@ -0,0 +1,36 @@
+/*
+ * RP2040 watchdog emulation
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_MISC_RP2040_WATCHDOG_H
+#define HW_MISC_RP2040_WATCHDOG_H
+
+#include "hw/core/clock.h"
+#include "hw/core/ptimer.h"
+#include "hw/core/sysbus.h"
+#include "qom/object.h"
+
+#define TYPE_RP2040_WATCHDOG "rp2040-watchdog"
+OBJECT_DECLARE_SIMPLE_TYPE(RP2040WatchdogState, RP2040_WATCHDOG)
+
+#define RP2040_WATCHDOG_BASE 0x40058000
+#define RP2040_WATCHDOG_SIZE 0x4000
+
+struct RP2040WatchdogState {
+    SysBusDevice parent_obj;
+
+    MemoryRegion iomem;
+    Clock *clk_ref;
+    ptimer_state *timer;
+
+    uint32_t ctrl;
+    uint32_t load;
+    uint32_t reason;
+    bool preserve_reason_on_reset;
+    uint32_t scratch[8];
+    uint32_t tick;
+};
+
+#endif
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 23cc9f2503..18120f17bf 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -269,7 +269,8 @@ qtests_arm = \
      'rp2040-resets-test',
      'rp2040-rosc-test',
     'rp2040-tbman-test',
-    'rp2040-vreg-test'] : []) + \
+    'rp2040-vreg-test',
+    'rp2040-watchdog-test'] : []) + \
   (config_all_devices.has_key('CONFIG_STM32L4X5_SOC') ? qtests_stm32l4x5 : []) 
+ \
   (config_all_devices.has_key('CONFIG_FSI_APB2OPB_ASPEED') ? 
['aspeed_fsi-test'] : []) + \
   (config_all_devices.has_key('CONFIG_CAN_FLEXCAN') ? ['flexcan-test'] : []) + 
\
diff --git a/tests/qtest/rp2040-watchdog-test.c 
b/tests/qtest/rp2040-watchdog-test.c
new file mode 100644
index 0000000000..ef54631897
--- /dev/null
+++ b/tests/qtest/rp2040-watchdog-test.c
@@ -0,0 +1,170 @@
+/*
+ * QTest testcase for the RP2040 watchdog.
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest.h"
+#include "qemu/bitops.h"
+#include "qemu/timer.h"
+#include "qobject/qdict.h"
+
+#define WATCHDOG_BASE       0x40058000
+#define WATCHDOG_CTRL       0x00
+#define WATCHDOG_LOAD       0x04
+#define WATCHDOG_REASON     0x08
+#define WATCHDOG_SCRATCH0   0x0c
+#define WATCHDOG_TICK       0x2c
+
+#define CTRL_TRIGGER        BIT(31)
+#define CTRL_ENABLE         BIT(30)
+#define CTRL_PAUSE_DBG1     BIT(26)
+#define CTRL_PAUSE_DBG0     BIT(25)
+#define CTRL_PAUSE_JTAG     BIT(24)
+#define CTRL_TIME_MASK      0x00ffffff
+
+#define REASON_FORCE        BIT(1)
+#define REASON_TIMER        BIT(0)
+
+#define TICK_RUNNING        BIT(10)
+#define TICK_ENABLE         BIT(9)
+
+#define NS_PER_US           1000
+
+static QTestState *rp2040_watchdog_start(void)
+{
+    return qtest_init("-machine raspi-pico -watchdog-action none");
+}
+
+static void check_watchdog_event(QTestState *qts)
+{
+    QDict *event = qtest_qmp_eventwait_ref(qts, "WATCHDOG");
+    QDict *data = qdict_get_qdict(event, "data");
+
+    g_assert_cmpstr(qdict_get_str(data, "action"), ==, "none");
+    qobject_unref(event);
+}
+
+static void test_reset_values(void)
+{
+    QTestState *qts = rp2040_watchdog_start();
+
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_CTRL), ==,
+                    CTRL_PAUSE_DBG1 | CTRL_PAUSE_DBG0 | CTRL_PAUSE_JTAG);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_LOAD), ==, 0);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_REASON), ==, 0);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_TICK), ==,
+                    TICK_ENABLE);
+
+    qtest_quit(qts);
+}
+
+static void test_scratch_registers(void)
+{
+    QTestState *qts = rp2040_watchdog_start();
+
+    qtest_writel(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0, 0x12345678);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0), ==,
+                    0x12345678);
+
+    g_assert_cmphex(qtest_readb(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0), ==,
+                    0x78);
+    g_assert_cmphex(qtest_readb(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0 + 1),
+                    ==, 0x56);
+    g_assert_cmphex(qtest_readb(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0 + 2),
+                    ==, 0x34);
+    g_assert_cmphex(qtest_readb(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0 + 3),
+                    ==, 0x12);
+
+    qtest_writeb(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0, 0xa5);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0), ==,
+                    0xa5a5a5a5);
+
+    qtest_writeb(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0 + 1, 0x3c);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0), ==,
+                    0x3c3c3c3c);
+
+    qtest_writew(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0, 0xf00d);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_SCRATCH0), ==,
+                    0xf00df00d);
+
+    qtest_quit(qts);
+}
+
+static void test_timer_expiry(void)
+{
+    QTestState *qts = rp2040_watchdog_start();
+    uint32_t ctrl;
+
+    /*
+     * The default clk_ref is 6 MHz. CYCLES=6 generates a 1 MHz watchdog
+     * tick; RP2040-E1 makes the counter decrement twice per tick.
+     */
+    qtest_writel(qts, WATCHDOG_BASE + WATCHDOG_TICK, TICK_ENABLE | 6);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_TICK) &
+                    (TICK_RUNNING | TICK_ENABLE), ==,
+                    TICK_RUNNING | TICK_ENABLE);
+
+    qtest_writel(qts, WATCHDOG_BASE + WATCHDOG_LOAD, 20);
+    qtest_writel(qts, WATCHDOG_BASE + WATCHDOG_CTRL, CTRL_ENABLE);
+
+    qtest_clock_step(qts, 5 * NS_PER_US);
+    ctrl = qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_CTRL);
+    g_assert_cmpuint(ctrl & CTRL_TIME_MASK, <, 20);
+    g_assert_cmpuint(ctrl & CTRL_TIME_MASK, >, 0);
+
+    qtest_clock_step(qts, 10 * NS_PER_US);
+    check_watchdog_event(qts);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_REASON), ==,
+                    REASON_TIMER);
+
+    qtest_quit(qts);
+}
+
+static void test_force_trigger(void)
+{
+    QTestState *qts = rp2040_watchdog_start();
+
+    qtest_writel(qts, WATCHDOG_BASE + WATCHDOG_CTRL, CTRL_TRIGGER);
+    check_watchdog_event(qts);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_REASON), ==,
+                    REASON_FORCE);
+
+    qtest_system_reset(qts);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_REASON), ==, 0);
+
+    qtest_quit(qts);
+}
+
+static void test_reason_survives_watchdog_reset(void)
+{
+    QTestState *qts = qtest_init("-machine raspi-pico "
+                                 "-watchdog-action reset");
+
+    qtest_writel(qts, WATCHDOG_BASE + WATCHDOG_CTRL, CTRL_TRIGGER);
+    qtest_qmp_eventwait(qts, "WATCHDOG");
+    qtest_qmp_eventwait(qts, "RESET");
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_REASON), ==,
+                    REASON_FORCE);
+
+    qtest_system_reset(qts);
+    g_assert_cmphex(qtest_readl(qts, WATCHDOG_BASE + WATCHDOG_REASON), ==, 0);
+
+    qtest_quit(qts);
+}
+
+int main(int argc, char **argv)
+{
+    g_test_init(&argc, &argv, NULL);
+
+    qtest_add_func("/rp2040-watchdog/reset-values", test_reset_values);
+    qtest_add_func("/rp2040-watchdog/scratch-registers",
+                   test_scratch_registers);
+    qtest_add_func("/rp2040-watchdog/timer-expiry", test_timer_expiry);
+    qtest_add_func("/rp2040-watchdog/force-trigger", test_force_trigger);
+    qtest_add_func("/rp2040-watchdog/reason-survives-watchdog-reset",
+                   test_reason_survives_watchdog_reset);
+
+    return g_test_run();
+}
-- 
2.55.0


Reply via email to