January 29, 2022

Interrupted by Embedded Rust on the Arduino Uno

This article details some solutions to programming interrupts in Rust on the Arduino Uno

Interrupted by Embedded Rust on the Arduino Uno

Programming interrupts on a micro-controller board can be a challenge. One has to deal with lacking documentation, hardware quirks, race conditions and other Trials of Will. Below follows a description on how to program an Arduino Uno in Rust to handle an external interrupt to reverse the sequence of an array of LEDs (see GIF at the end). It is based on the excellent avr-hal crate by Rahix. The code referenced in this article can be found here. (Update: The code was merged into avr-hal as an example)

An Arduino Uno is a great way to get acquainted with the world of embedded programming. The starter kit not only provides an abundance of components to get started, it also provides a very nice booklet with 15 projects to get started. It doesn't presume any prior knowledge on electronics and explains everything in an accessible manner. This whole kit clearly is a product of love.

The Rust language and the embedded world are a good fit, because Rust's design for safety really helps to protect you in the sometimes fickle reality that is micro-controller hardware. To get started with Embedded Rust these links are really helpful:

The Circuit

The circuit for the LED sequencing

All LEDs, except the blue one, are in series with a 220 ohm resistor. The blue one has a 560 ohm resistor behind it, to make it less bright. The circuit is based on the Spaceship Interface project in the book.

The Code

Getting interrupts to work in Rust on the Arduino Uno did require some research. First of all, it is important to use the nightly-2021-07-01 build of Rust as such:

$ rustup override set nightly-2021-01-07

Unfortunately the Rust Language Server is broken in that build, so VSCode will choke a little. (The reason for this goes all the way into Rust's LLVM backend.)

The main idea behind the program is that the LEDs blink in sequence, first decreasing in tempo and then increasing. As can be seen in the main loop:

loop {
    blink_for_range(0..10, &mut leds);
    blink_for_range(10..0, &mut leds);
}

Looping over a set of similar elements like a collection of pins, it is tempting to put these pins directly in an array (remember: fancier data structures like Vec will need the alloc crate). However, every pin is its own type, so they cannot simply be put into an array. Usually pins are used for completely different causes, so this safety net is by design. Luckily the avr-hal does provide a way of achieving an array pins and it is called downgrading:

let mut leds: [Pin<mode::Output>; 4] = [
    pins.d3.into_output().downgrade(),
    pins.d4.into_output().downgrade(),
    pins.d5.into_output().downgrade(),
    pins.d6.into_output().downgrade(),
];

A naive way of reversing the sequence is to use iter_mut() or iter_mut().rev(). The problem here is that forward and backward iterators have a different type. Luckily there is another crate to the rescue. Something that people that have experience with functional languages like Haskell or Scala will recognize, the either crate. It provides the convenience that if both Left and Right are an iterator type, Either will also behave as an iterator type. Here it is in action:

let iter = if is_reversed() {
	Left(leds.iter_mut().rev())
} else {
	Right(leds.iter_mut())
};
iter.for_each(|led| {
    led.toggle();
    arduino_hal::delay_ms(ms as u16);
})

Important for getting the either crate to work is to disable compilation for std as this is a no_std environment. This is achieved by adding the following to your Cargo.toml:

[dependencies.either]
version = "1.6.1"
default-features = false

The interrupt handler itself can be fairly simple. The convention is that the handler has the same name as the interrupt type it's supposed to handle, in this case INT0:

#[avr_device::interrupt(atmega328p)]
fn INT0() {
    let current = REVERSED.load(Ordering::SeqCst);
    REVERSED.store(!current, Ordering::SeqCst);
}

This uses an AtomicBool due to synchronization issues between the Interrupt Service Routine (ISR) and the rest of the code, as detailed in Rahix's blog post. As the ISR is executed there will not be any other interrupts so a critical section is not necessary. When reading the value a critical section is necessary, but this seems to have to do more with address spaces, as the AtomicBool should already provide the proper synchronization:

fn is_reversed() -> bool {
    return avr_device::interrupt::free(|_| {
    	REVERSED.load(Ordering::SeqCst) 
    });
}

The final thing that remains to be done is to switch the D2 pin to EXT0 and to turn on interrupts globally:

// thanks to tsemczyszyn and Rahix: https://github.com/Rahix/avr-hal/issues/240
// Configure INT0 for falling edge. 0x03 would be rising edge.
dp.EXINT.eicra.modify(|_, w| w.isc0().bits(0x02));
// Enable the INT0 interrupt source.
dp.EXINT.eimsk.modify(|_, w| w.int0().set_bit());

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

Letting the code run and pressing the button shows the interrupt in action: