RM0090 timers count from 0 to ARR inclusive, with a period of ARR + 1,
including ARR == 0. Wrap CNT while running, arm an absolute expiry, and
delete the QEMU timer when CEN is cleared.

Freeze CNT in stopped_cnt while CEN is clear so TIM_CNT writes while
stopped are preserved and resume on enable. Return a stopped CNT verbatim
even when it is greater than ARR. Reset also deletes any pending event.

Migration compatibility is not required for these machine types, so use
version 2 as both the current and minimum VMState version and serialize
stopped_cnt directly.

Add qtests covering the timer period, prescaler, stopped CNT, ARR == 0,
update generation, and system reset.

Signed-off-by: Manuele Conti <[email protected]>
---
v2:
- Make hit_time local to stm32f2xx_timer_set_alarm().
- Preserve and return stopped CNT verbatim, including CNT greater than ARR.
- Set both VMState version fields to 2 and serialize stopped_cnt directly.

 MAINTAINERS                        |   1 +
 hw/timer/stm32f2xx_timer.c         |  73 ++++++++++++----
 include/hw/timer/stm32f2xx_timer.h |   2 +-
 tests/qtest/meson.build            |   2 +
 tests/qtest/stm32f405-timer-test.c | 136 +++++++++++++++++++++++++++++
 5 files changed, 195 insertions(+), 19 deletions(-)
 create mode 100644 tests/qtest/stm32f405-timer-test.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 902db77218..7eafedbc47 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1213,6 +1213,7 @@ F: hw/misc/stm32f4xx_syscfg.c
 F: hw/misc/stm32f4xx_exti.c
 F: hw/misc/stm32_rcc.c
 F: include/hw/misc/stm32_rcc.h
+F: tests/qtest/stm32f405*
 
 Netduino 2
 M: Alistair Francis <[email protected]>
diff --git a/hw/timer/stm32f2xx_timer.c b/hw/timer/stm32f2xx_timer.c
index 0442ac64ad..09c30f7af1 100644
--- a/hw/timer/stm32f2xx_timer.c
+++ b/hw/timer/stm32f2xx_timer.c
@@ -53,7 +53,8 @@ static void stm32f2xx_timer_interrupt(void *opaque)
     if (s->tim_dier & TIM_DIER_UIE && s->tim_cr1 & TIM_CR1_CEN) {
         s->tim_sr |= 1;
         qemu_irq_pulse(s->irq);
-        stm32f2xx_timer_set_alarm(s, s->hit_time);
+        stm32f2xx_timer_set_alarm(s,
+                                  qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL));
     }
 
     if (s->tim_ccmr1 & (TIM_CCMR1_OC2M2 | TIM_CCMR1_OC2M1) &&
@@ -71,27 +72,43 @@ static inline int64_t 
stm32f2xx_ns_to_ticks(STM32F2XXTimerState *s, int64_t t)
     return muldiv64(t, s->freq_hz, 1000000000ULL) / (s->tim_psc + 1);
 }
 
+static uint32_t stm32f2xx_timer_get_cnt(STM32F2XXTimerState *s, int64_t now)
+{
+    uint64_t period_ticks = (uint64_t)s->tim_arr + 1;
+
+    if (!(s->tim_cr1 & TIM_CR1_CEN)) {
+        return s->stopped_cnt;
+    }
+
+    return (stm32f2xx_ns_to_ticks(s, now) - s->tick_offset) % period_ticks;
+}
+
 static void stm32f2xx_timer_set_alarm(STM32F2XXTimerState *s, int64_t now)
 {
-    uint64_t ticks;
-    int64_t now_ticks;
+    uint64_t period_ticks;
+    uint64_t remaining_ticks;
+    uint64_t delta_ns;
+    int64_t hit_time;
 
-    if (s->tim_arr == 0) {
+    if (!(s->tim_cr1 & TIM_CR1_CEN)) {
+        timer_del(s->timer);
         return;
     }
 
     DB_PRINT("Alarm set at: 0x%x\n", s->tim_cr1);
 
-    now_ticks = stm32f2xx_ns_to_ticks(s, now);
-    ticks = s->tim_arr - (now_ticks - s->tick_offset);
+    /* STM32 timers count from 0 to ARR inclusive. */
+    period_ticks = (uint64_t)s->tim_arr + 1;
+    remaining_ticks = period_ticks - stm32f2xx_timer_get_cnt(s, now);
 
-    DB_PRINT("Alarm set in %d ticks\n", (int) ticks);
+    DB_PRINT("Alarm set in %" PRIu64 " ticks\n", remaining_ticks);
 
-    s->hit_time = muldiv64((ticks + (uint64_t) now_ticks) * (s->tim_psc + 1),
-                               1000000000ULL, s->freq_hz);
+    delta_ns = muldiv64(remaining_ticks * (s->tim_psc + 1),
+                        1000000000ULL, s->freq_hz);
+    hit_time = now + delta_ns;
+    timer_mod(s->timer, hit_time);
 
-    timer_mod(s->timer, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL) + s->hit_time);
-    DB_PRINT("Wait Time: %" PRId64 " ticks\n", s->hit_time);
+    DB_PRINT("Wait Time: %" PRId64 " ticks\n", hit_time);
 }
 
 static void stm32f2xx_timer_reset(DeviceState *dev)
@@ -117,8 +134,10 @@ static void stm32f2xx_timer_reset(DeviceState *dev)
     s->tim_dcr = 0;
     s->tim_dmar = 0;
     s->tim_or = 0;
+    s->stopped_cnt = 0;
 
     s->tick_offset = stm32f2xx_ns_to_ticks(s, now);
+    timer_del(s->timer);
 }
 
 static uint64_t stm32f2xx_timer_read(void *opaque, hwaddr offset,
@@ -148,8 +167,8 @@ static uint64_t stm32f2xx_timer_read(void *opaque, hwaddr 
offset,
     case TIM_CCER:
         return s->tim_ccer;
     case TIM_CNT:
-        return stm32f2xx_ns_to_ticks(s, qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL)) 
-
-               s->tick_offset;
+        return stm32f2xx_timer_get_cnt(s,
+                                       qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL));
     case TIM_PSC:
         return s->tim_psc;
     case TIM_ARR:
@@ -187,9 +206,23 @@ static void stm32f2xx_timer_write(void *opaque, hwaddr 
offset,
     DB_PRINT("Write 0x%x, 0x%"HWADDR_PRIx"\n", value, offset);
 
     switch (offset) {
-    case TIM_CR1:
+    case TIM_CR1: {
+        bool was_enabled = s->tim_cr1 & TIM_CR1_CEN;
+        bool enable = value & TIM_CR1_CEN;
+
+        if (was_enabled && !enable) {
+            s->stopped_cnt = stm32f2xx_timer_get_cnt(s, now);
+        }
         s->tim_cr1 = value;
+
+        if (!was_enabled && enable) {
+            s->tick_offset = stm32f2xx_ns_to_ticks(s, now) - s->stopped_cnt;
+            stm32f2xx_timer_set_alarm(s, now);
+        } else if (was_enabled && !enable) {
+            timer_del(s->timer);
+        }
         return;
+    }
     case TIM_CR2:
         s->tim_cr2 = value;
         return;
@@ -220,7 +253,7 @@ static void stm32f2xx_timer_write(void *opaque, hwaddr 
offset,
         s->tim_ccer = value;
         return;
     case TIM_PSC:
-        timer_val = stm32f2xx_ns_to_ticks(s, now) - s->tick_offset;
+        timer_val = stm32f2xx_timer_get_cnt(s, now);
         s->tim_psc = value & 0xFFFF;
         break;
     case TIM_CNT:
@@ -228,7 +261,9 @@ static void stm32f2xx_timer_write(void *opaque, hwaddr 
offset,
         break;
     case TIM_ARR:
         s->tim_arr = value;
-        stm32f2xx_timer_set_alarm(s, now);
+        if (s->tim_cr1 & TIM_CR1_CEN) {
+            stm32f2xx_timer_set_alarm(s, now);
+        }
         return;
     case TIM_CCR1:
         s->tim_ccr1 = value;
@@ -261,6 +296,7 @@ static void stm32f2xx_timer_write(void *opaque, hwaddr 
offset,
      * requires a refresh of both tick_offset and the alarm.
      */
     s->tick_offset = stm32f2xx_ns_to_ticks(s, now) - timer_val;
+    s->stopped_cnt = timer_val;
     stm32f2xx_timer_set_alarm(s, now);
 }
 
@@ -272,8 +308,8 @@ static const MemoryRegionOps stm32f2xx_timer_ops = {
 
 static const VMStateDescription vmstate_stm32f2xx_timer = {
     .name = TYPE_STM32F2XX_TIMER,
-    .version_id = 1,
-    .minimum_version_id = 1,
+    .version_id = 2,
+    .minimum_version_id = 2,
     .fields = (const VMStateField[]) {
         VMSTATE_INT64(tick_offset, STM32F2XXTimerState),
         VMSTATE_UINT32(tim_cr1, STM32F2XXTimerState),
@@ -294,6 +330,7 @@ static const VMStateDescription vmstate_stm32f2xx_timer = {
         VMSTATE_UINT32(tim_dcr, STM32F2XXTimerState),
         VMSTATE_UINT32(tim_dmar, STM32F2XXTimerState),
         VMSTATE_UINT32(tim_or, STM32F2XXTimerState),
+        VMSTATE_UINT32(stopped_cnt, STM32F2XXTimerState),
         VMSTATE_END_OF_LIST()
     }
 };
diff --git a/include/hw/timer/stm32f2xx_timer.h 
b/include/hw/timer/stm32f2xx_timer.h
index 040e64d499..6a9aeea998 100644
--- a/include/hw/timer/stm32f2xx_timer.h
+++ b/include/hw/timer/stm32f2xx_timer.h
@@ -76,7 +76,6 @@ struct STM32F2XXTimerState {
     qemu_irq irq;
 
     int64_t tick_offset;
-    uint64_t hit_time;
     uint64_t freq_hz;
 
     uint32_t tim_cr1;
@@ -97,6 +96,7 @@ struct STM32F2XXTimerState {
     uint32_t tim_dcr;
     uint32_t tim_dmar;
     uint32_t tim_or;
+    uint32_t stopped_cnt;
 };
 
 #endif /* HW_STM32F2XX_TIMER_H */
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 78b3010649..fc632c9413 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -238,6 +238,8 @@ qtests_stm32l4x5 = \
    'stm32l4x5_usart-test']
 
 qtests_arm = \
+  (config_all_devices.has_key('CONFIG_OLIMEX_STM32_H405') ? \
+   ['stm32f405-timer-test'] : []) + \
   (config_all_devices.has_key('CONFIG_MPS2') ? ['sse-timer-test'] : []) + \
   (config_all_devices.has_key('CONFIG_CMSDK_APB_DUALTIMER') ? 
['cmsdk-apb-dualtimer-test'] : []) + \
   (config_all_devices.has_key('CONFIG_CMSDK_APB_TIMER') ? 
['cmsdk-apb-timer-test'] : []) + \
diff --git a/tests/qtest/stm32f405-timer-test.c 
b/tests/qtest/stm32f405-timer-test.c
new file mode 100644
index 0000000000..f150da1235
--- /dev/null
+++ b/tests/qtest/stm32f405-timer-test.c
@@ -0,0 +1,136 @@
+/*
+ * STM32F405 timer tests
+ *
+ * Copyright (c) 2026 Manuele Conti <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest-single.h"
+
+#define TIM2_BASE       0x40000000
+#define TIM_CR1         (TIM2_BASE + 0x00)
+#define TIM_DIER        (TIM2_BASE + 0x0c)
+#define TIM_SR          (TIM2_BASE + 0x10)
+#define TIM_EGR         (TIM2_BASE + 0x14)
+#define TIM_CNT         (TIM2_BASE + 0x24)
+#define TIM_PSC         (TIM2_BASE + 0x28)
+#define TIM_ARR         (TIM2_BASE + 0x2c)
+
+#define TIM_CR1_CEN     (1U << 0)
+#define TIM_DIER_UIE    (1U << 0)
+#define TIM_SR_UIF      (1U << 0)
+#define TIM_EGR_UG      (1U << 0)
+
+static void timer_reset(void)
+{
+    qtest_system_reset(global_qtest);
+}
+
+static void test_period_and_update(void)
+{
+    timer_reset();
+    writel(TIM_PSC, 0);
+    writel(TIM_ARR, 4);
+    writel(TIM_CNT, 0);
+    writel(TIM_DIER, TIM_DIER_UIE);
+    writel(TIM_CR1, TIM_CR1_CEN);
+
+    clock_step(4);
+    g_assert_cmphex(readl(TIM_CNT), ==, 4);
+    g_assert_cmphex(readl(TIM_SR), ==, 0);
+
+    clock_step(1);
+    g_assert_cmphex(readl(TIM_CNT), ==, 0);
+    g_assert_cmphex(readl(TIM_SR), ==, TIM_SR_UIF);
+
+    writel(TIM_SR, 0);
+    g_assert_cmphex(readl(TIM_SR), ==, 0);
+}
+
+static void test_prescaler(void)
+{
+    timer_reset();
+    writel(TIM_PSC, 9);
+    writel(TIM_ARR, 4);
+    writel(TIM_CNT, 0);
+    writel(TIM_CR1, TIM_CR1_CEN);
+
+    clock_step(40);
+    g_assert_cmphex(readl(TIM_CNT), ==, 4);
+    clock_step(10);
+    g_assert_cmphex(readl(TIM_CNT), ==, 0);
+}
+
+static void test_stopped_counter(void)
+{
+    timer_reset();
+    writel(TIM_ARR, 100);
+    writel(TIM_CNT, 200);
+    g_assert_cmphex(readl(TIM_CNT), ==, 200);
+
+    writel(TIM_CNT, 37);
+    writel(TIM_CR1, TIM_CR1_CEN);
+    clock_step(10);
+    g_assert_cmphex(readl(TIM_CNT), ==, 47);
+
+    writel(TIM_CR1, 0);
+    clock_step(100);
+    g_assert_cmphex(readl(TIM_CNT), ==, 47);
+
+    writel(TIM_CR1, TIM_CR1_CEN);
+    clock_step(1);
+    g_assert_cmphex(readl(TIM_CNT), ==, 48);
+}
+
+static void test_arr_zero(void)
+{
+    timer_reset();
+    writel(TIM_ARR, 0);
+    writel(TIM_CNT, 0);
+    writel(TIM_DIER, TIM_DIER_UIE);
+    writel(TIM_CR1, TIM_CR1_CEN);
+
+    clock_step(1);
+    g_assert_cmphex(readl(TIM_CNT), ==, 0);
+    g_assert_cmphex(readl(TIM_SR), ==, TIM_SR_UIF);
+}
+
+static void test_update_and_reset(void)
+{
+    timer_reset();
+    writel(TIM_ARR, 20);
+    writel(TIM_CNT, 12);
+    writel(TIM_EGR, TIM_EGR_UG);
+    g_assert_cmphex(readl(TIM_CNT), ==, 0);
+
+    writel(TIM_DIER, TIM_DIER_UIE);
+    writel(TIM_CR1, TIM_CR1_CEN);
+    qtest_system_reset(global_qtest);
+    clock_step(100);
+    g_assert_cmphex(readl(TIM_CR1), ==, 0);
+    g_assert_cmphex(readl(TIM_SR), ==, 0);
+}
+
+int main(int argc, char **argv)
+{
+    int ret;
+
+    g_test_init(&argc, &argv, NULL);
+
+    qtest_add_func("/stm32f405/timer/period-and-update",
+                   test_period_and_update);
+    qtest_add_func("/stm32f405/timer/prescaler", test_prescaler);
+    qtest_add_func("/stm32f405/timer/stopped-counter",
+                   test_stopped_counter);
+    qtest_add_func("/stm32f405/timer/arr-zero", test_arr_zero);
+    qtest_add_func("/stm32f405/timer/update-and-reset",
+                   test_update_and_reset);
+
+    qtest_start("-machine olimex-stm32-h405");
+    ret = g_test_run();
+    qtest_end();
+
+    return ret;
+}
-- 
2.54.0 (Apple Git-157)


Reply via email to