On Wed, 23 Sept 2026 at 22:56, Manuele Conti
<[email protected]> wrote:
>
> RM0090 counters count from 0 to ARR inclusive, with period ARR + 1
> for all ARR values including ARR == 0. Fix set_alarm to wrap CNT,
> arm an absolute expiry and delete the QEMU timer when CEN is cleared.
>
> Freeze CNT while CEN == 0 in stopped_cnt so a TIM_CNT write
> programmed while stopped is preserved and resumed on enable, and
> reset deletes any pending timer event.
>
> VMState goes to version 2 to serialize stopped_cnt. Loading a v1
> stream leaves stopped_cnt at 0, so a timer stopped with CNT != 0
> loads CNT as 0; no post_load workaround. The pre-existing QEMUTimer
> expiry limit is unchanged.
>
> Add qtests covering PSC/ARR/CEN/UIF, CNT <= ARR, CNT preset before
> CEN, ARR == 0, stop freeze on CEN=0, restart, and no spurious IRQ
> after reset.
>
> Signed-off-by: Manuele Conti <[email protected]>
> ---
> MAINTAINERS | 1 +
> hw/timer/stm32f2xx_timer.c | 101 ++++++++++++++++----
> include/hw/timer/stm32f2xx_timer.h | 7 ++
> tests/qtest/meson.build | 4 +
> tests/qtest/stm32f405-timer-test.c | 142 +++++++++++++++++++++++++++++
> 5 files changed, 236 insertions(+), 19 deletions(-)
> create mode 100644 tests/qtest/stm32f405-timer-test.c
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 5bf895732c..f1b3d75cc8 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..1639037130 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));
This change means that the only uses of s->hit_time are in the
sequence in stm32f2xx_timer_set_alarm():
s->hit_time = (uint64_t)now + delta_ns;
timer_mod(s->timer, s->hit_time);
DB_PRINT("Wait Time: %" PRId64 " ticks\n", s->hit_time);
That means we can change hit_time to be a simple local variable
in that function; it doesn't need to be a state field any more.
> }
>
> if (s->tim_ccmr1 & (TIM_CCMR1_OC2M2 | TIM_CCMR1_OC2M1) &&
> static uint64_t stm32f2xx_timer_read(void *opaque, hwaddr offset,
> @@ -148,8 +172,11 @@ 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;
> + if (!(s->tim_cr1 & TIM_CR1_CEN)) {
> + return s->stopped_cnt % ((uint64_t)s->tim_arr + 1);
> + }
This looks odd -- I wouldn't expect the auto-reload register to have
any effect if the counter is stopped. That is, if you do:
* set the counter to disabled
* set ARR to 100
* set CNT to 200
* read CNT
I would expect to read back 200 (and then if you enable the counter
it probably rolls back to 0 on the next clock, regardless of
what its value above the auto-reload value is?)
Do you have real hardware to confirm the behaviour against?
> + return stm32f2xx_timer_get_cnt(s,
> + qemu_clock_get_ns(QEMU_CLOCK_VIRTUAL));
> case TIM_PSC:
> return s->tim_psc;
> case TIM_ARR:
> @@ -272,7 +328,7 @@ static const MemoryRegionOps stm32f2xx_timer_ops = {
>
> static const VMStateDescription vmstate_stm32f2xx_timer = {
> .name = TYPE_STM32F2XX_TIMER,
> - .version_id = 1,
> + .version_id = 2,
> .minimum_version_id = 1,
> .fields = (const VMStateField[]) {
> VMSTATE_INT64(tick_offset, STM32F2XXTimerState),
> @@ -294,6 +350,13 @@ static const VMStateDescription vmstate_stm32f2xx_timer
> = {
> VMSTATE_UINT32(tim_dcr, STM32F2XXTimerState),
> VMSTATE_UINT32(tim_dmar, STM32F2XXTimerState),
> VMSTATE_UINT32(tim_or, STM32F2XXTimerState),
> + /*
> + * New in version 2: frozen CNT while CEN == 0. Pre-existing
> + * limitation left unchanged: QEMUTimer expiry (hit_time) is
> + * not migrated and is re-armed by the next register write;
> + * see stm32f2xx_timer_set_alarm.
> + */
> + VMSTATE_UINT32_V(stopped_cnt, STM32F2XXTimerState, 2),
We don't care about migration compatibility on these machine types,
so we could just bump both version_id and minimum_version_id to 2
and define the field as a simple VMSTATE_UINT32().
> VMSTATE_END_OF_LIST()
> }
> };
Otherwise this looks OK, and certainly better than what we have now.
thanks
-- PMM