Write your own Arduino millis() in Rust

2020-11-24 rust embedded avr 12 min read

Many people who have written C code for Arduino have at least heard of the millis() function at some point. It returns the number of milliseconds since the program started running.

In avr-hal (a Rust library for AVR microcontrollers) there currently is no equivalent for it. Instead of waiting on someone to add that, let's see what it takes to build our own!

Short Disclaimer: This implementation will work for this specific use-case but is not generic enough to be fit as a solution for avr-hal. It uses hardware interrupts directly, which is a tricky thing to do for HAL code and has tons of implications for more complex scenarios. That's why there is no general solution in avr-hal yet ...

The final code for this blog post can be found in avr-hal here: examples/arduino-uno/src/bin/uno-millis.rs

What does millis() do?

The Arduino documentation has this to say about the function:

Description

Returns the number of milliseconds passed since the Arduino board began running the current program. This number will overflow (go back to zero), after approximately 50 days.

That is a bit vague but at least gives us a clear statement about what the function should be doing when looking from the outside. To implement our own, however, we need to know how it does its job. For this, we need to dive into the code behind its C/C++ implementation. Luckily, it's all available online: https://github.com/arduino/ArduinoCore-avr

In particular, we're after the definition of millis(). It can be found in cores/arduino/wiring.c:65 and looks like this:

unsigned long millis()
{
        unsigned long m;
        uint8_t oldSREG = SREG;

        // disable interrupts while we read timer0_millis or we might get an
        // inconsistent value (e.g. in the middle of a write to timer0_millis)
        cli();
        m = timer0_millis;
        SREG = oldSREG;

        return m;
}

Further, timer0_millis is defined as a volatile (ugh!) global:

volatile unsigned long timer0_millis = 0;

Essentially, the function boils down to this:

  1. Save the current interrupt state (whether interrupts are enabled or disabled)
  2. Disable interrupts with a cli instruction
  3. Read the global timer0_millis which seems to contain the magic we're after
  4. Restore interrupt state

Luckily, steps 1, 2, and 4 are already readily available in Rust in the form of avr_device::interrupt::free(). So, let's start with a skeleton:

fn millis() -> u32 {
    avr_device::interrupt::free(|cs| {
        // TODO: Magic
        unimplemented!()
    })
}

Interrupt Service Routine

Okay, that was the easy part. Now, we need to find out who is writing to this timer0_millis global variable. Just above the definition of millis() we can find it: It is a weirdly defined function which seems to increment the global by some amount every time it is executed (cores/arduino/wiring.c:42):

#define MILLIS_INC (MICROSECONDS_PER_TIMER0_OVERFLOW / 1000)

ISR(TIMER0_OVF_vect)
{
        // copy these to local variables so they can be stored in registers
        // (volatile variables must be read from memory on every access)
        unsigned long m = timer0_millis;
        unsigned char f = timer0_fract;

        m += MILLIS_INC;
        f += FRACT_INC;
        if (f >= FRACT_MAX) {
                f -= FRACT_MAX;
                m += 1;
        }

        timer0_fract = f;
        timer0_millis = m;
        timer0_overflow_count++;
}

What is this all about? Well, ISR() is a magic macro which defines what is called an Interrupt Service Routine. This is a special function that is "called" by the CPU upon a certain hardware interrupt triggering. In this case, it's the service routine for the TIMER0_OVF interrupt which I'm going to explain in more detail later. For now, let's just assume that this interrupt triggers in a certain fixed interval.

Writing such an ISR/interrupt handler in Rust is also possible. But before doing that, a few words of caution:

Interrupts are hard to do right. A lot of C code you'll find "works", but does so by relying on assumptions about the compiler and hardware which are not always guaranteed. Because they can run at absolutely any point in your program (while interrupts are enabled), they are a great sources for subtle, non-obvious race conditions.

In C, this often leads to data races which Rust at least makes much harder to trip up on. But even with memory safety guaranteed, there are also tons of logic errors to accidentally step into.

That all said, let's try our hands on writing an ISR anyway. In Rust, it will look like this:

#[avr_device::interrupt(atmega328p)]
fn TIMER0_COMPA() {
    // TODO: Increment our global
}

(We're using TIMER0_COMPA instead of TIMER0_OVF for reasons explained later.)

Because interrupts on AVR are still experimental, you also need to enable this compiler feature in your project:

#![feature(abi_avr_interrupt)]

Sharing Data

Next step is somehow translating that global variable to Rust. The dirty solution is just using a static mut and hoping for the best:

// BAD
static mut TIMER0_MILLIS: u32 = 0;

// ...
unsafe { TIMER0_MILLIS += 1 };

But this rightfully needs a ton of unsafe and is super easy to get wrong. Remember my warning from above ...

Instead, we can use a safe abstraction for sharing data between the ISR and our millis() function. Said abstraction comes in the form of a Mutex which enforces access to happen inside a critical section - a span of code where interrupts are disabled. This means that no ISR can run during that time and possibly read out a half-modified value.

Actually, we already added a critical section in our millis() function by use of avr_device::interrupt::free(). So adding the mutex isn't too much more effort:

use core::cell;

const MILLIS_INCREMENT: u32 = /* TODO */;

static MILLIS_COUNTER: avr_device::interrupt::Mutex<cell::Cell<u32>> =
    avr_device::interrupt::Mutex::new(cell::Cell::new(0));

#[avr_device::interrupt(atmega328p)]
fn TIMER0_COMPA() {
    avr_device::interrupt::free(|cs| {
        let counter_cell = MILLIS_COUNTER.borrow(cs);
        let counter = counter_cell.get();
        counter_cell.set(counter + MILLIS_INCREMENT);
    })
}

fn millis() -> u32 {
    avr_device::interrupt::free(|cs| {
        MILLIS_COUNTER.borrow(cs).get()
    })
}

cs is a critical section token which we pass to the mutex. Think of it as a "key" that is needed for unlocking. This way, the mutex can be certain that it is only ever unlocked inside a critical section.

Inside the mutex, we store a core::cell::Cell. This is necessary so that we can mutate the value which the mutex contains as it only gives us an immutable reference. For more complex cases, a core::cell::RefCell might be needed but for simple values like our integer, fortunately Cell is enough. You can read more about the differences in the core::cell documentation.

With this all in place, we have recreated the global in only safe Rust. This means that we can be certain there are no memory safety violations we might have missed! For making it actually work, though, we still have some steps remaining:

The Timer

Hardware timers are very common to find in microcontrollers as they make anything related to time and timing much easier to handle. Wikipedia describes these Timers like this:

[...] These are typically digital counters that either increment or decrement at a fixed frequency, which is often configurable, and which interrupt the processor when reaching zero. [...]

More-sophisticated timers may have comparison logic to compare the timer value against a specific value, set by software, that triggers some action when the timer value matches the preset value.

Essentially this means a timer/counter simply has a value which it increments by one whenever the clock signal it's connected to "ticks". When the timer reaches its maximum value (called MAX or TOP), it "overflows" and resets back to zero. You can configure it to trigger an interrupt everytime this happens.

Here is a small diagram to demonstrate this:

counter value:
 MAX                      +-----+                       +-----+
                          |     |                       |     |
                    +-----+     |                 +-----+     |
                    |           |                 |           |
              +-----+           |           +-----+           |           +-----
              |                 |           |                 |           |
        +-----+                 |     +-----+                 |     +-----+
        |                       |     |                       |     |
   0  --+                       +-----+                       +-----+

clock signal:
HIGH    +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+
        |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
 LOW  --+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--+  +--

overflow interrupt:
                                ^                             ^
                                +-fires                       +-fires

The microcontroller used on most Arduino boards, ATmega328P, has 3 such timers: TC0, TC1, and TC2. Each has slightly different features but for this purpose that does not matter much. What we want is just our interrupt triggering periodically, in fixed intervals. Let's choose TC0 as that's also what Arduino uses.

The AVR timers run off the core clock which is usually 16 MHz for Arduino boards. A prescaler (also called clock divider) allows slowing it down by a factor of 8, 64, 256, or 1024. The timer will overflow whenever it counts "past" its maximum value which by default is 255. This means, the timer has 256 steps or counts (including 0) per overflow. One can also configure a lower maximum, in which case the number of counts is smaller and thus overflows happen more quickly.

Putting it into a formula, the time between two overflows can be calculated as

Tovf = Prescale * Counts / Fcore

We're lucky because the available configurations allow us to get an overflow interval that is an exact multiple of 1 millisecond. This means we don't have to plage ourselves with fractional parts as can be seen in the Arduino code. Here is a table to show the possibilities (for TC0):

Prescale FactorTimer Counts (MAX + 1)Overflow Interval
642501 ms
2561252 ms
2562504 ms
10241258 ms
102425016 ms

Depending on your needs you can choose a shorter interval at the cost of more CPU time being used for serving the overflow interrupts or a longer interval at the cost of millis() accuracy.

Armed with this knowledge, we can already fill in the constant value that is added on each overflow interrupt:

const PRESCALER: u32 = 1024;
const TIMER_COUNTS: u32 = 125;

const MILLIS_INCREMENT: u32 = PRESCALER * TIMER_COUNTS / 16000;

Hardware Configuration

Finally, we need to configure the timer with our settings. Going back to the C implementation, this is done in the init() function and roughly looks like this:

// set "WGM" to fast PWM
sbi(TCCR0A, WGM01);
sbi(TCCR0A, WGM00);

// set timer 0 prescale factor to 64
sbi(TCCR0B, CS01);
sbi(TCCR0B, CS00);

// enable timer 0 overflow interrupt
sbi(TIMSK0, TOIE0);

It is not important to understand what exactly is going on here. In simple terms, this code writes some values to peripheral registers which can be thought of as "memory locations" for configuring/interacting with the timer. sbi() is an instruction to set single bits in those registers but we need not care about that. The important part is the overall picture of what is happening:

  1. Configure the overflow behavior of the timer in the TCCR0A register. Arduino configures the timer to overflow upon reaching 255 (called "fast PWM" mode) but we want it to overflow earlier (on TIMER_COUNTS from the table above). Thus we will need a different mode here which is called CTC. That's short for "Clear Timer on Compare Match".

    Choosing CTC mode also means we have to use a different ISR: TIMER0_COMPA instead of TIMER0_OVF. That's because the latter only triggers when the timer reaches 255 which it never will in CTC mode. The former triggers upon reaching our new "maximum" which is what we want.

    With CTC mode, we'll also need to pass the new "maximum" to the timer. This is not shown above, but is done by simply writing the value to the OCR0A register.

  2. Configure the prescaling factor in the TCCR0B register. Arduino chooses 64; we possibly need to adapt for the other prescaler values.

  3. Enable the overflow interrupt in the TIMSK0 register. From this point onwards, whenever the timer overflows and interrupts are enabled globally, our ISR will run.

Doing these register writes in Rust looks quite different from C. A full explanation of the Rust mechanism is too much for this post, but the Embedded Rust WG has excellent resources explaining it in detail. Check out the Embedded Rust Book and the svd2rust API documentation.

The code looks like this:

let tc0 = dp.TC0;
tc0.tccr0a.write(|w| w.wgm0().ctc());
tc0.ocr0a.write(|w| unsafe { w.bits(TIMER_COUNTS as u8) });
tc0.tccr0b.write(|w| match PRESCALER {
    8 => w.cs0().prescale_8(),
    64 => w.cs0().prescale_64(),
    256 => w.cs0().prescale_256(),
    1024 => w.cs0().prescale_1024(),
    _ => panic!(),
});
tc0.timsk0.write(|w| w.ocie0a().set_bit());

Finally, we should reset the counter value, and enable interrupts globally to let our ISR start updating:

avr_device::interrupt::free(|cs| {
    MILLIS_COUNTER.borrow(cs).set(0);
});

unsafe { avr_device::interrupt::enable() };

With this, we have all the pieces in place and can start using this homegrown millis()! I've pushed the full code as an example in avr-hal on GitHub: examples/arduino-uno/src/bin/uno-millis.rs

I hope this helps! If you have any questions, feel free to reach out via mail or open an issue on GitHub!