Skip to content

fix(rtc): disable compare interrupt instead of stopping the RTC counter#992

Open
QuartzShard wants to merge 2 commits intoatsamd-rs:masterfrom
QuartzShard:fix/rtc-disable-timer-time-loss
Open

fix(rtc): disable compare interrupt instead of stopping the RTC counter#992
QuartzShard wants to merge 2 commits intoatsamd-rs:masterfrom
QuartzShard:fix/rtc-disable-timer-time-loss

Conversation

@QuartzShard
Copy link
Contributor

Summary

disable_timer()/enable_timer() in both RTC monotonic backends (__internal_basic_backend and __internal_half_period_counting_backend) call RtcMode::disable()/enable(), which clears/sets CTRLA.ENABLE — stopping and restarting the entire RTC peripheral. The rtic-time TimerQueue calls disable_timer() whenever the queue is empty, so any period with no pending delays freezes the hardware counter and permanently loses wall-clock time.
This violates the TimerQueueBackend trait contract:

/// Optional. This is used to save power, this is called when the timer queue is empty.
///
/// Enabling and disabling the monotonic needs to propagate to now so that an instant
/// based of now() is still valid.

The RTC is a low-power peripheral (~1 µA in standby per the datasheet) driven by an already-running 32 kHz crystal, so leaving it counting continuously is negligible. For a higher-power timer driven by a faster oscillator, it might make sense to compensate now() for the time elapsed while disabled — but for the RTC, keeping it running seems to me the straightforward approach.

Fix

Toggle only the compare match interrupt (INTENCLR/INTENSET for $rtic_int, i.e. Compare0) instead of the peripheral enable bit. The RTC counter keeps running so that now() returns correct values. Half-period and overflow interrupts are unaffected — they are enabled once in _start() and not touched by this change.

Reproduction

Minimal reproduction crate with side-by-side RTC vs SysTick comparison:
https://github.com/QuartzShard/rtc_mono_bug_demo
Without fix — RTC reports 0 ms for a 5 s busy-wait (counter frozen):

[iter 1]          RTC    SysTick
  delay phase:    1000ms    1000ms
  busy phase:        0ms    5000ms
  total:          1000ms    6000ms

With fix — RTC tracks correctly:

[iter 1]          RTC    SysTick
  delay phase:    1000ms    1000ms
  busy phase:     4977ms    5000ms
  total:          5977ms    6000ms

Testing

  • Verified on hardware (ATSAMD51J20A, XOSC32K 32.768 kHz, RTIC 2.2.0)

Checklist

  • All new or modified code is well documented, especially public items
  • No new warnings or clippy suggestions have been introduced - CI will deny clippy warnings by default! You may #[allow] certain lints where reasonable, but ideally justify those with a short comment.

disable_timer()/enable_timer() were calling RtcMode::disable()/enable(),
which clears/sets CTRLA.ENABLE — stopping and restarting the entire RTC
peripheral. The rtic-time TimerQueue calls disable_timer() whenever the
queue is empty, so any period with no pending delays freezes the hardware
counter and permanently loses wall-clock time.

This violates the TimerQueueBackend trait contract, which states that
enabling/disabling must propagate to now() so instants remain valid.

Fix: toggle the compare match interrupt (INTENCLR/INTENSET) instead of
the peripheral enable bit. The RTC counter keeps running, preserving
monotonicity. Only the Compare0 interrupt used for RTIC task wakeups is
affected — half-period and overflow interrupts remain enabled.
@rnd-ash
Copy link
Contributor

rnd-ash commented Feb 26, 2026

Looks good! Not quite sure what changed that started causing the D2/1x chip pipelines to fail, seems to be nothing from this PR - I'll do some testing on my D21 board to verify its working there as well, though, I don't see any reason why it shouldn't

@QuartzShard
Copy link
Contributor Author

Ran into this one while trying to do thermal control and wondering why the logged timestamps were slipping behind the real time passed over ~20 minutes, turns out I was losing ~5 seconds every 20-or-so while there was no actively awaiting delay calls

@QuartzShard
Copy link
Contributor Author

Looks good! Not quite sure what changed that started causing the D2/1x chip pipelines to fail, seems to be nothing from this PR - I'll do some testing on my D21 board to verify its working there as well, though, I don't see any reason why it shouldn't

Looks like a new lint on nightly, it's not in code I changed as far as I can see?

@rnd-ash
Copy link
Contributor

rnd-ash commented Mar 4, 2026

Ran this branch for a couple days on my E51 and D21 boards (Didn't compare to systick though), just a simple counter. Over 24 hours the time loss appears to be less than 1 second (Not measurable to me), so I'd say this is fine to be merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants