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

A driver has to be initialized with an instance of type that implements a certain trait of the embedded-hal which is ensured via trait bound and provides its own type instance with a custom set of methods allowing to interact with the driven device.

The application binds the various parts together and ensures that the desired functionality is achieved. When porting between different systems, this is the part which requires the most adaptation efforts, since the application needs to correctly initialize the real hardware via the HAL implementation and the initialisation of different hardware differs, sometimes drastically so. Also the user choice often plays a big role, since components can be physically connected to different terminals, hardware buses sometimes need external hardware to match the configuration or there are different trade-offs to be made in the use of internal peripherals (e.g. multiple timers with different capabilities are available or peripherals conflict with others).

Concurrency happens whenever different parts of your program might execute at different times or out of order. In an embedded context, this includes:

   • interrupt handlers, which run whenever the associated interrupt happens,

   • various forms of multithreading, where your microprocessor regularly swaps between parts of your program,

   • and in some systems, multiple-core microprocessors, where each core can be independently running a different part of your program at the same time.

Since many embedded programs need to deal with interrupts, concurrency will usually come up sooner or later, and it's also where many subtle and difficult bugs can occur. Luckily, Rust provides a number of abstractions and safety guarantees to help us write correct code.

The simplest concurrency for an embedded program is no concurrency: your software consists of a single main loop which just keeps running, and there are no interrupts at all. Sometimes this is perfectly suited to the problem at hand! Typically your loop will read some inputs, perform some processing, and write some outputs.

#[entry]

fn main() {

let peripherals = setup_peripherals();

loop {

let inputs = read_inputs(&peripherals);

let outputs = process(inputs);

write_outputs(&peripherals, outputs);

}

}

Since there's no concurrency, there's no need to worry about sharing data between parts of your program or synchronising access to peripherals. If you can get away with such a simple approach this can be a great solution.

Unlike non-embedded Rust, we will not usually have the luxury of creating heap allocations and passing references to that data into a newly-created thread. Instead, our interrupt handlers might be called at any time and must know how to access whatever shared memory we are using. At the lowest level, this means we must have statically allocated mutable memory, which both the interrupt handler and the main code can refer to.

In Rust, such static mut variables are always unsafe to read or write, because without taking special care, you might trigger a race condition, where your access to the variable is interrupted halfway through by an interrupt which also accesses that variable.

For an example of how this behaviour can cause subtle errors in your code, consider an embedded program which counts rising edges of some input signal in each one-second period (a frequency counter):

static mut COUNTER: u32 = 0;

#[entry]

fn main() -> ! {

set_timer_1hz();

let mut last_state = false;

loop {

let state = read_signal_level();

if state && !last_state {

// DANGER - Not actually safe! Could cause data races.

unsafe { COUNTER += 1 };

}

last_state = state;

}

}

#[interrupt]

fn timer() {

unsafe { COUNTER = 0; }

}

Each second, the timer interrupt sets the counter back to 0. Meanwhile, the main loop continually measures the signal, and incremements the counter when it sees a change from low to high. We've had to use unsafe to access COUNTER, as it's static mut, and that means we're promising the compiler we won't cause any undefined behaviour. Can you spot the race condition? The increment on COUNTER is not guaranteed to be atomic — in fact, on most embedded platforms, it will be split into a load, then the increment, then a store. If the interrupt fired after the load but before the store, the reset back to 0 would be ignored after the interrupt returns — and we would count twice as many transitions for that period.

So, what can we do about data races? A simple approach is to use critical sections, a context where interrupts are disabled. By wrapping the access to COUNTER in main in a critical section, we can be sure the timer interrupt will not fire until we're finished incrementing COUNTER:

static mut COUNTER: u32 = 0;

#[entry]

fn main() -> ! {

set_timer_1hz();

let mut last_state = false;

loop {

let state = read_signal_level();

if state && !last_state {

// New critical section ensures synchronised access to COUNTER

cortex_m::interrupt::free(|_| {

unsafe { COUNTER += 1 };

});

}

last_state = state;

}

}

#[interrupt]

fn timer() {

unsafe { COUNTER = 0; }

}

In this example, we use cortex_m::interrupt::free, but other platforms will have similar mechanisms for executing code in a critical section. This is also the same as disabling interrupts, running some code, and then re-enabling interrupts.

Note we didn't need to put a critical section inside the timer interrupt, for two reasons:

   • Writing 0 to COUNTER can't be affected by a race since we don't read it

   • It will never be interrupted by the main thread anyway

If COUNTER was being shared by multiple interrupt handlers that might preempt each other, then each one might require a critical section as well.

This solves our immediate problem, but we're still left writing a lot of unsafe code which we need to carefully reason about, and we might be using critical sections needlessly. Since each critical section temporarily pauses interrupt processing, there is an associated cost of some extra code size and higher interrupt latency and jitter (interrupts may take longer to be processed, and the time until they are processed will be more variable). Whether this is a problem depends on your system, but in general, we'd like to avoid it.