2016-06-05

Sleeping

When we last looked into sleeping, our sleep function was very primitive. It just waited busily until the required amount of time had passed, before it returned to the caller. While this was fine to get things up and running, it wasn't ideal. There are much ways to do that.

The SAM3X8E has several low-power modes that we can use in the sleep function to save power while we're waiting.[1] Instead of producing heat and wasting power (which means burning through the battery, if our system isn't connected to the grid), we can just put most of the microcontrollers functions to sleep, until the right time has come to wake up again.

Before we can get into the sleeping itself, we need to make a fundamental decision: When to allow interrupts to occur. An interrupt, well, interrupts the normal program execution, which can cause problems in some code. The code currently running might be time-sensitive, or it might be modifying data structures that are also accessed by an interrupt handler. Both cases would lead to problems, if the code were to be interrupted.

One solution to this problem is generally allowing interrupts, but defining critical sections during which they are disabled.[2] Unfortunately this approach is quite error-prone. If you forget a critical section where one would be needed, this can cause very subtle bugs.

I opted for a safer, but possibly less efficient approach: Disabling all interrupts at the beginning of the program, and only enabling them in specific sections of the code. This leaves us free not to worry about interrupts in most of the code, making it easier to write correct code, preventing lots of potential bugs outright.

Here's the function that is used to disable the interrupts:

pub fn disable() {
    unsafe {
        asm!("cpsid i" :::: "volatile");
    }
}
            

This is our first use of inline assembly.[3] The cpsid i tell the microcontroller to disable interrupts[4], while volatile tells the compiler that this instruction has an unspecified effect and must not be reordered in regards to neighbouring instructions for optimization purposes. We'll examine later where this fits into our program.

Another interrupt-related piece of functionality we need is enabling the RTT (Real-time Timer) interrupts. Please don't worry too much about how this fits into the bigger picture. I'm going to explain that in a moment. For now, here's the code we need to do it:

pub struct Nvic(());

impl Nvic {
    pub unsafe fn new() -> Self {
        Nvic(())
    }

    pub fn enable(&mut self, peripheral: &Peripheral) {
        unsafe {
            (*ISER)[peripheral.index()].write(peripheral.mask());
        }
    }
}
            

This is a safe interface that represents the NVIC (Nested Vector Interrupt Controller). I will not go into the low-level interface that it wraps, as it looks a lot (altough not exactly) like the other ones that I've shown in past articles.

The important thing to understand here is that we write to one of the Interrupt Set-enable Registers to activate interrupts for a specific peripheral. Because there are more than 32 peripherals (which means a 32-bit mask is no sufficient to represent them), there are two of those registers, and we need an index to access them.[5]

To help us with this, I wrote an enum with variants for each of the SAM3X8E's peripherals. Here's how that looks like:

use hardware::base::peripherals;


#[derive(Clone, Copy)]
#[repr(u32)]
pub enum Peripheral {
    Supc      = peripherals::SUPC,
    Rstc      = peripherals::RSTC,

    // Other peripheral variants omitted for brevity
}

impl Peripheral {
    pub fn id(&self) -> u32 {
        *self as u32
    }

    pub fn index(&self) -> usize {
        self.id() as usize / 32
    }

    pub fn mask(&self) -> u32 {
        0x1 << (self.id() % 32)
    }
}

            

As you can see there's not a lot going on here. Each peripheral has an id, which is just a 32-bit number (e.g. the Supply Controller has id 0, the Reset Controller has id 1). As mentioned above, there are more than 32 peripherals, making it impossible to represent them all in a single 32-bit mask. Therefore, various registers (like ISER above) come in two variants. The index method gives us the index of the correct one, for a given peripheral. Finally, the mask method converts the id into a mask that can be used with those registers.[6]

Now we're ready to get into the meat of this article: The new sleeping code. I've decided to make things a bit simpler than before and move everything into a single function that does all the set-up and sleeping.[7] First, let's take a look at the set-up code:

pub fn sleep_ms(milliseconds: u32, nvic: &mut Nvic) {
    let prescaler_value = 0x00000020; // millisecond resolution (roughly)
    unsafe {
        (*RTT).mode.write(
            rtt::RTTRST | rtt::ALMIEN | prescaler_value
        );

        // The reset is only effective after two slow clock cycles. Let's
        // just wait until that has happened.
        // See data sheet, section 13.4.
        while (*RTT).value.read() != 0 {}

        (*RTT).alarm.write(milliseconds);
    }

    nvic.enable(&Peripheral::Rtt);

    // Actual sleep code goes here
}
            

We first write to the mode register to reset the timer, enable the alarm interrupt, and write a prescaler value that sets the timer to a resolution of roughly a millisecond. Then we wait for timer to actually reset (this is one of the places where we would need a critical section, had we not disabled interrupts), and set the alarm by writing to the alarm register.[8]

The final step of preparation is to activate the RTT interrupt using the NVIC interface shown above. How does this fit together with the fact that we've disabled interrupts in general? Well, there are two steps to configuring interrupts: We can enable/disable interrupts by peripheral, and we can enable/disable interrupt handler execution for all peripherals.[9]

As far as I can tell, if interrupts are disabled for a peripheral, it's as if those interrupts don't exist. You have to enable the RTT interrupts for example, if you want to do anything with them at all. Once you've done that, you can check whether they occured, and see various other effects from that. By itself, that doesn't execute the interrupt handler functions though. For that to happen, interrupt handler execution needs to be enabled using the cps instruction family (see above).

One fact to take note of is that interrupt execution in general is enabled by default, but interrupts for specific peripherals are disabled by default.

Ok, now that we've done the set-up for the sleep, let's actually sleep.

pub fn sleep_ms(milliseconds: u32, nvic: &mut Nvic) {
    // Set-up code goes here

    unsafe {
        while (*RTT).status.read() & rtt::ALMS == 0 {
            asm!(
                "
                    dsb
                    wfi
                "
                :::: "volatile"
            );
        }
    }
}
            

In a loop, we check whether the alarm interrupt that we've enabled above has occured. If it hasn't yet, we put the program to sleep with the wfi instruction.[10] It will wake up again if any interrupt occurs, even if no interrupt handler is actually executed. The dsb instruction before wfi is a Data Synchronization Barrier. This is an instruction that won't complete until all the memory accesses that come before it have been completed.[11] Those memory accesses might have been buffered, so them completing before the processor goes to sleep is not a given.

The Data Synchronization Barrier is not actually necessary here, but it's better to have it, to cover edge cases and ease porting to other ARM processors. It's generally recommended to have it.[12]

Finally, here's our updated program:

interrupts::disable();

let mut nvic = unsafe { Nvic::new() };

let led = unsafe { pio::b().pin_27() };
let mut led = led
    .enable()
    .enable_output();

let uart_tx = unsafe { pio::a().pin_9() };
unsafe { debug::init(uart_tx) };

loop {
    println!("Start main loop iteration");

    restart_watchdog();

    led.set_output();
    sleep_ms(200, &mut nvic);
    led.clear_output();
    sleep_ms(800, &mut nvic);
            

And that's it for today! As always, the full code is available on GitHub. See you next time!