Выбрать главу

let state = interrupt::free(|cs| {

let gpioa = MY_GPIO.borrow(cs).borrow();

gpioa.as_ref().unwrap().idr.read().idr0().bit_is_set()

});

if state && !last_state {

// Set PA1 high if we've seen a rising edge on PA0.

interrupt::free(|cs| {

let gpioa = MY_GPIO.borrow(cs).borrow();

gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());

});

}

last_state = state;

}

}

#[interrupt]

fn timer() {

// This time in the interrupt we'll just clear PA0.

interrupt::free(|cs| {

// We can use `unwrap()` because we know the interrupt wasn't enabled

// until after MY_GPIO was set; otherwise we should handle the potential

// for a None value.

let gpioa = MY_GPIO.borrow(cs).borrow();

gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().clear_bit());

});

}

That's quite a lot to take in, so let's break down the important lines.

static MY_GPIO: Mutex<RefCell<Option<stm32f405::GPIOA>>> =

Mutex::new(RefCelclass="underline" :new(None));

Our shared variable is now a Mutex around a RefCell which contains an Option. The Mutex ensures we only have access during a critical section, and therefore makes the variable Sync, even though a plain RefCell would not be Sync. The RefCell gives us interior mutability with references, which we'll need to use our GPIOA. The Option lets us initialise this variable to something empty, and only later actually move the variable in. We cannot access the peripheral singleton statically, only at runtime, so this is required.

interrupt::free(|cs| MY_GPIO.borrow(cs).replace(Some(dp.GPIOA)));

Inside a critical section we can call borrow() on the mutex, which gives us a reference to the RefCell. We then call replace() to move our new value into the RefCell.

interrupt::free(|cs| {

let gpioa = MY_GPIO.borrow(cs).borrow();

gpioa.as_ref().unwrap().odr.modify(|_, w| w.odr1().set_bit());

});

Finally, we use MY_GPIO in a safe and concurrent fashion. The critical section prevents the interrupt firing as usual, and lets us borrow the mutex. The RefCell then gives us an &Option<GPIOA>, and tracks how long it remains borrowed - once that reference goes out of scope, the RefCell will be updated to indicate it is no longer borrowed.

Since we can't move the GPIOA out of the &Option, we need to convert it to an &Option<&GPIOA> with as_ref(), which we can finally unwrap() to obtain the &GPIOA which lets us modify the peripheral.

If we need a mutable reference to a shared resource, then borrow_mut and deref_mut should be used instead. The following code shows an example using the TIM2 timer.

use core::celclass="underline" :RefCell;

use core::ops::DerefMut;

use cortex_m::interrupt::{self, Mutex};

use cortex_m::asm::wfi;

use stm32f4::stm32f405;

static G_TIM: Mutex<RefCell<Option<Timer<stm32::TIM2>>>> =

Mutex::new(RefCelclass="underline" :new(None));

#[entry]

fn main() -> ! {

let mut cp = cm::Peripherals::take().unwrap();

let dp = stm32f405::Peripherals::take().unwrap();

// Some sort of timer configuration function.

// Assume it configures the TIM2 timer, its NVIC interrupt,

// and finally starts the timer.

let tim = configure_timer_interrupt(&mut cp, dp);

interrupt::free(|cs| {

G_TIM.borrow(cs).replace(Some(tim));

});

loop {

wfi();

}

}

#[interrupt]

fn timer() {

interrupt::free(|cs| {

if let Some(ref mut tim)) = G_TIM.borrow(cs).borrow_mut().deref_mut() {

tim.start(1.hz());

}

});

}

Whew! This is safe, but it is also a little unwieldy. Is there anything else we can do?

One alternative is the RTIC framework, short for Real Time Interrupt-driven Concurrency. It enforces static priorities and tracks accesses to static mut variables ("resources") to statically ensure that shared resources are always accessed safely, without requiring the overhead of always entering critical sections and using reference counting (as in RefCell). This has a number of advantages such as guaranteeing no deadlocks and giving extremely low time and memory overhead.

The framework also includes other features like message passing, which reduces the need for explicit shared state, and the ability to schedule tasks to run at a given time, which can be used to implement periodic tasks. Check out the documentation for more information!

Another common model for embedded concurrency is the real-time operating system (RTOS). While currently less well explored in Rust, they are widely used in traditional embedded development. Open source examples include FreeRTOS and ChibiOS. These RTOSs provide support for running multiple application threads which the CPU swaps between, either when the threads yield control (called cooperative multitasking) or based on a regular timer or interrupts (preemptive multitasking). The RTOS typically provide mutexes and other synchronisation primitives, and often interoperate with hardware features such as DMA engines.

At the time of writing, there are not many Rust RTOS examples to point to, but it's an interesting area so watch this space!

It is becoming more common to have two or more cores in embedded processors, which adds an extra layer of complexity to concurrency. All the examples using a critical section (including the cortex_m::interrupt::Mutex) assume the only other execution thread is the interrupt thread, but on a multi-core system that's no longer true. Instead, we'll need synchronisation primitives designed for multiple cores (also called SMP, for symmetric multi-processing).

These typically use the atomic instructions we saw earlier, since the processing system will ensure that atomicity is maintained over all cores.

Covering these topics in detail is currently beyond the scope of this book, but the general patterns are the same as for the single-core case.

Eventually you'll want to use dynamic data structures (AKA collections) in your program. std provides a set of common collections: Vec, String, HashMap, etc. All the collections implemented in std use a global dynamic memory allocator (AKA the heap).