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)
