Skip to content

Instantly share code, notes, and snippets.

@korken89
Last active February 14, 2023 16:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save korken89/fe94a475726414dd1bce031c76adc3dd to your computer and use it in GitHub Desktop.
Save korken89/fe94a475726414dd1bce031c76adc3dd to your computer and use it in GitHub Desktop.
An RTIC monotonic based on RTC for nRF52 series
// RTIC Monotonic impl for the RTCs
use crate::hal::pac::{rtc0, RTC0, RTC1, RTC2};
pub use fugit::{self, ExtU32};
use rtic_monotonic::Monotonic;
pub struct MonoRtc<T: InstanceRtc> {
overflow: u8,
rtc: T,
}
impl<T: InstanceRtc> MonoRtc<T> {
pub fn new(rtc: T) -> Self {
unsafe { rtc.prescaler.write(|w| w.bits(0)) };
MonoRtc { overflow: 0, rtc }
}
pub fn is_overflow(&self) -> bool {
self.rtc.events_ovrflw.read().bits() == 1
}
}
impl<T: InstanceRtc> Monotonic for MonoRtc<T> {
type Instant = fugit::TimerInstantU32<32_768>;
type Duration = fugit::TimerDurationU32<32_768>;
const DISABLE_INTERRUPT_ON_EMPTY_QUEUE: bool = false;
unsafe fn reset(&mut self) {
self.rtc
.intenset
.write(|w| w.compare0().set().ovrflw().set());
self.rtc
.evtenset
.write(|w| w.compare0().set().ovrflw().set());
self.rtc.tasks_clear.write(|w| w.bits(1));
self.rtc.tasks_start.write(|w| w.bits(1));
}
#[inline(always)]
fn now(&mut self) -> Self::Instant {
let cnt = self.rtc.counter.read().bits();
let ovf = if self.is_overflow() {
self.overflow.wrapping_add(1)
} else {
self.overflow
} as u32;
Self::Instant::from_ticks((ovf << 24) | cnt)
}
fn set_compare(&mut self, instant: Self::Instant) {
let now = self.now();
const MIN_TICKS_FOR_COMPARE: u64 = 3;
// Since the timer may or may not overflow based on the requested compare val, we check
// how many ticks are left.
//
// Note: The RTC cannot have a compare value too close to the current timer value,
// so we use the `MIN_TICKS_FOR_COMPARE` to set a minimum offset from now to the set value.
let val = match instant.checked_duration_since(now) {
Some(x) if x.ticks() <= 0xffffff && x.ticks() > MIN_TICKS_FOR_COMPARE => {
instant.duration_since_epoch().ticks() & 0xffffff
} // Will not overflow
Some(x) => {
(instant.duration_since_epoch().ticks() + (MIN_TICKS_FOR_COMPARE - x.ticks()))
& 0xffffff
} // Will not overflow
_ => 0, // Will overflow or in the past, set the same value as after overflow to not get extra interrupts
};
unsafe { self.rtc.cc[0].write(|w| w.bits(val)) };
}
fn clear_compare_flag(&mut self) {
unsafe { self.rtc.events_compare[0].write(|w| w.bits(0)) };
}
#[inline(always)]
fn zero() -> Self::Instant {
Self::Instant::from_ticks(0)
}
fn on_interrupt(&mut self) {
if self.is_overflow() {
self.overflow = self.overflow.wrapping_add(1);
self.rtc.events_ovrflw.write(|w| unsafe { w.bits(0) });
}
}
}
pub trait InstanceRtc: core::ops::Deref<Target = rtc0::RegisterBlock> {}
impl InstanceRtc for RTC0 {}
impl InstanceRtc for RTC1 {}
impl InstanceRtc for RTC2 {}
@eflukx
Copy link

eflukx commented Apr 21, 2022

With the overflow counter being a u8, wouldn't this RTC Monotonic overflow as a whole a little over 1 year?

32bit counter @32768Hz overflows at 2^(32-15) = 131_072 secs
extending by 8 bits gives 131072 << 8 = 33_554_432secs (~388 days)

@korken89
Copy link
Author

Indeed it would

@eflukx
Copy link

eflukx commented Apr 22, 2022

Hmm after reading the docs on RTC, the rtc clock width is only 24 bit wide, not 32! So, at 32.768Hz, it overflows @512secs only. Extending with 8 bits yields a little over 36 hours.

Haven't tried the code, but I guess just swapping the overflow field to a u32 should do the trick.. (For a veeery looong time at least.. :))
Probably need to change the fugit types to xxU64 as well (e.g. TimerInstantU64)...

@korken89
Copy link
Author

Absolutely, if your need longer time between overflows then extending to 64 bit is a good option :)

@redengin
Copy link

redengin commented Jul 14, 2022

I tried to use this on a Pinetime but couldn't get it to support scheduling.

I see the RTC interrupt fire once, and then never fire again. I've not delved into the RTC implementation, but was hoping you could provide some guidance. I tried RTC0, and RTC1 with the same result.

Cargo configuration:
cortex-m = "0.7"
cortex-m-rt = "0.7"
cortex-m-rtic = "1.0"
nrf52832-hal = { version="0.15.1", features=["rt"], default-features=false }
fugit = "0.3.5"
nrf52832-pac = { version="0.11", optional=true, features=["rt"] }

@eflukx
Copy link

eflukx commented Jul 14, 2022

I tried to use this on a Pinetime but couldn't get it to support scheduling.

I see the RTC interrupt fire once, and then never fire again. I've not delved into the RTC implementation, but was hoping you could provide some guidance. I tried RTC0, and RTC1 with the same result.

Yep, I've created a sample project just for you ;) Showing the RTIC working with the RTC monotonic https://github.com/eflukx/rtic-rtc-example
It includes the patches to u64 as discussed above.

@redengin
Copy link

Thanks, that example worked like a charm. The missing piece was configuring the LFCLK source in init()

@eflukx
Copy link

eflukx commented Sep 23, 2022

Hi @korken89

I'm working on my application, using the RTC monotonic and ran into a problem. For tasks that periodically run, say, every couple 100 of msecs everything runs just fine. I have my "main" app doing its business every few minutes and a faster running task that runs (and samples the ADC) every 250msecs.

I also have a few charlieplexed (RGB) LEDs in the device, that I'd like to control. For this I need much faster spawning tasks, currently aiming for a 100µsecs interval i.e. a 10kHz update/scan rate (1:9 mux doing software modulation). This task is re-spawning itself at the given rate.

When doing this, my app "hangs" after a few seconds (most often within 10 seconds or so). No RTIC tasks as spawned anymore (at least not from the timer queue) everything seems to stop. There is however a task that's bound to the GPIOTE interrupt for handling a button. When the button is pressed, this task is run. Consequently a (secondary) button-handler task normally is spawned after some (debounce) time:

if on_button::spawn_after(50.millis(), uptime()).is_err() {
    defmt::warn!("on_button spawn failed!");
}

My console screen reads: "on_button spawn failed!". So apparently spawning a new (timer-queued) task fails as well. Trying to spawn a task on the normal run queue (i.e. by invoking on_button::spawn(uptime())) works like it should. Somehow all timered stuff is broken.

Do you have any idea what could be amiss here and what might be causing this unexpected behavior? It seems to point to some bug in the MonoRtc code, as when testing my task-spawning-business using the SysTick monotonic (with tick freq at 100secs: Systick<10000>), everything works as it should. (For me the power use of the systick is a problem however, so I need the low power RTC monotonic for this is a battery powered device.)

Hope you can have a look!

@korken89
Copy link
Author

@eflukx Hmm, I have not seen a case like this before.
Do you have a way so I could test it locally?
It does sound like either:

  1. There is a bug in the timer queue handling (however this is very well tested code)
  2. There is a bug in the monotonic
  3. A bug in how task spawns are handled in the app which causes a spawn to fail and the event loop breaks

@eflukx
Copy link

eflukx commented Sep 24, 2022

There is a bug in the timer queue handling (however this is very well tested code)

Agree this seems unlikely... I figured this would be probably a known bug in RTIC, but couldn't find any issues on the repo describing a similar problem. (must confess only did a QuickScan™️)

There is a bug in the monotonic

Seems more likely as it is provided outside the RTIC framework and as such contains self maintained/custom code. (That's the reason I posted issue here :) )

A bug in how task spawns are handled in the app which causes a spawn to fail and the event loop breaks

Could be.. but unrelated (timer) tasks also stop to be queued, that worked perfectly fine before. Furthermore, when using a SysTick-based monotonic the problem doesn't arise. Also, for testing I reduced the tasks in question to do the absolute minimum (essentially empty tasks outside the (self-)spawning 'logic').

In the meantime... I made two new observations:

1. Adding some delay before setting the compare register (rtc.cc[0]) in the MonoRtc set_compare() function seems to alleviate the problem. Like this:

cortex_m::asm::delay(1000); // <-- add some delay cylcles
unsafe { self.rtc.cc[0].write(|w| w.bits(val as u32)) };

I've had the app running overnight and it kept working correctly. Could it be there's some race condition when setting the compare register in the set_compare function?

the trait documentation specifically documents this should not be a problem, i.e. it's handled in the framework.. But this could be a bug of course... From Monotonic trait:

    /// Set the compare value of the timer interrupt.
    ///
    /// **Note:** This method does not need to handle race conditions of the monotonic, the timer
    /// queue in RTIC checks this.
    fn set_compare(&mut self, instant: Self::Instant);

2. Some tasks are run (at some other interrupt?)

It first seemed all 'timered' tasks stopped working altogether, but I noticed tasks are run "sometimes". I have a (sensor-measuring) task that normally runs every 20 seconds, but now (in the "hanging" state) runs every ~1024 seconds. I figured this is probably caused by some other source triggering an (overflow) interrupt. (there is a second RTC instance running from my BSP that is used for keeping uptime (will be removed, but now is still there. Removing/disabling it does not alleviate the problem, so it seems unrelated to the issue at hand, but it is causing periodic overflow interrupts...)).

Do you have a way so I could test it locally?

I'll try to isolate the case for further dissection... :)

@eflukx
Copy link

eflukx commented Sep 24, 2022

Do you have a way so I could test it locally?

I have created an isolated example that shows the problem (on my hardware it is very easily reproduced).
Please have a look at: https://github.com/eflukx/rtic-rtc-example/blob/rtc_fast_spawn/src/bin/rtic_rtc_spawn_fail.rs

Thanks in advance! 👍

@korken89
Copy link
Author

@eflukx Hi, I've been running your code for a while to get a grip on the issue.
I adapted it by changing the HAL to nrf52832 (that's what I have at hand).

So far I've been able to get the error to happen for me as well, I'll give it a deeper look and see what the issue it.
We use this monotonic impl in production, so I'll probably look deeper at this on Monday at work as well. :)

@korken89
Copy link
Author

I've added some debugging code that checks what instant we set the RTC to and at what time we exit the ISR.

INFO  fast_task first spawned!
└─ rtic_rtc_spawn_fail::app::fast_task @ src/bin/rtic_rtc_spawn_fail.rs:63
DEBUG set_compare 98310, now 98308
└─ rtic_nrf_rtc::monotonic_nrf52_rtc::{impl#1}::set_compare @ src/monotonic_nrf52_rtc.rs:66
DEBUG isr, now 98309
└─ rtic_nrf_rtc::monotonic_nrf52_rtc::{impl#1}::on_interrupt @ src/monotonic_nrf52_rtc.rs:81

So we set the compare to a value that is ~2 ticks into the future.
After that the handling takes some time, and we exit the ISR when there is 1 tick left.
Here is the weird part: We don't get any interrupt from the RTC!

I think there is some race-condition with the RTC HW if the wait is too short.

@korken89
Copy link
Author

Aha! The datasheet specifies that if you set the compare to a value that is within 2 cycles the COMPARE event may not happen.
So we need to add a check that sets the value to 2 or more ticks later.

@korken89
Copy link
Author

Here is an update set_compare that solves the issue.
Unfortunately it does add on the minimal possible delay.

    fn set_compare(&mut self, instant: Self::Instant) {
        let now = self.now();

        const MIN_TICKS_FOR_COMPARE: u64 = 3;

        // Since the timer may or may not overflow based on the requested compare val, we check
        // how many ticks are left.
        let val = match instant.checked_duration_since(now) {
            Some(x) if x.ticks() <= 0xffffff && x.ticks() > MIN_TICKS_FOR_COMPARE => {
                instant.duration_since_epoch().ticks() & 0xffffff
            } 
            Some(x) => {
                (instant.duration_since_epoch().ticks() + (MIN_TICKS_FOR_COMPARE - x.ticks()))
                    & 0xffffff
            } 
            _ => 0, // Will overflow or in the past, set the same value as after overflow to not get extra interrupts
        } as u32;

        unsafe { self.rtc.cc[0].write(|w| w.bits(val)) };
    }

@korken89
Copy link
Author

If you come up with a better fix, feel free to ping me!

@korken89
Copy link
Author

Also, remember to set this flag as you are using an extended timer:

    const DISABLE_INTERRUPT_ON_EMPTY_QUEUE: bool = false;

@eflukx
Copy link

eflukx commented Sep 25, 2022

Great to have a working solution! Having a minimum spawn-delay of 3 ticks (~10kHz) would work for me. (and not having an app that hangs "for no apparent reason" works for me as well.. 👍 )

Also, remember to set this flag as you are using an extended timer:

    const DISABLE_INTERRUPT_ON_EMPTY_QUEUE: bool = false;

Good catch! I did not even notice this (probably as it has a default value set in the trait). As the run queue in my specific use case is never empty, having this at false didn't manifest a real problem. Still, a foot gun lurking in the deep.. ;)

If you come up with a better fix, feel free to ping me!

Yep.. I'll dive into the Nordic datasheets and errata. I would expect this behavior probably to be documented in there somewhere...

What isn't exactly clear to me is why the added delay "solution" (using asm::delay()) seemed to solve the issue as well... (as the actual time-to-interrupt only becomes shorter by adding delay)

@korken89
Copy link
Author

The delay solution works because RTIC's timer queue checks if the time has expired and side steps the interrupt handler. So if it's too short RTIC catches that :)

@eflukx
Copy link

eflukx commented Feb 14, 2023

In reaction to

Here is an update set_compare that solves the issue.

There seems to be a logic error that can result in an overflow condition... consider

(instant.duration_since_epoch().ticks() + (MIN_TICKS_FOR_COMPARE - x.ticks()))

The match arm above that is conditional, so our code is executed only if the following evaluates to false:

Some(x) if x.ticks() <= 0xffffff && x.ticks() > MIN_TICKS_FOR_COMPARE => {

It is, however, executed when x.ticks() > 0xffffff, when at the same time x.ticks() > MIN_TICKS_FOR_COMPARE, the subtraction MIN_TICKS_FOR_COMPARE - x.ticks() results in an overflow. Shouldn't just checking for x.ticks() > MIN_TICKS_FOR_COMPARE in the first match arm condition be enough?

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