In multi core embedded Rust, is it appropriate to use a static mut for one way data sharing from one core to the other core?
Here’s the code (using embassy)
#![no_std]
// …
static mut CORE1_STACK: Stack<4096> = Stack::new();
static EXECUTOR0: StaticCell<Executor> = StaticCell::new();
static EXECUTOR1: StaticCell<Executor> = StaticCell::new();
static mut one_way_data_exchange:u8 = 0;
#[cortex_m_rt::entry]
fn main() -> ! {
spawn_core1(p.CORE1, unsafe { &mut CORE1_STACK }, move || {
let executor1 = EXECUTOR1.init(Executor::new());
executor1.run(|spawner| unwrap!(spawner.spawn(core1_task())));
});
let executor0 = EXECUTOR0.init(Executor::new());
executor0.run(|spawner| unwrap!(spawner.spawn(core0_task())));
}
#[embassy_executor::task]
async fn core0_task() {
info!("Hello from core 0");
loop {
unsafe { one_way_data_exchange = 128; } // sensor value
}
}
#[embassy_executor::task]
async fn core1_task() {
info!("Hello from core 1");
let sensor_val:u8 = 0;
loop {
unsafe { sensor_val = one_way_data_exchange; }
// continue with rest of program
}
}
}
If I were to be writing to the static var from both cores, that would obviously create a race condition. But if I only ever write from one core and only ever read from the other core, does that solve the race condition? Or, is it still problematic for both cores to be accessing it in parallel, even if only one is writing?
The order of read->write or write->read, in this case, doesn’t matter. One core is just creating a stream of IO input and the other dips into that stream whenever it’s ready to process the loop again, even if it misses some intermittent inputs.
No, it is still a problem.
u64takes multiple cpu cycles to write - and therefore the reading side could see only half of the value updated.It is true that accessing very simple primitive types like this can be safe. You don't need
static mutfor it, though - there are mechanisms built into the language / core library so you don't have to resort tostatic mut. In this case, the important one would be atomic.It provides something called interior mutability. This means your value can be
staticwithoutmut, and can be shared normally, and the type itself provides the mutability.Let me demonstrate. As I don't have a microcontroller available right now, I rewrote your example for normal execution:
Here is how this would look like when implemented with
atomic:Note that the code does not contain an
unsafe; this is prefectly valid for the compiler to understand and has (almost) no runtime overhead.To demonstrate how little overhead this really causes:
The code compiles to the following, using the flags
-C opt-level=3 -C linker-plugin-lto --target=thumbv6m-none-eabi:An
AtomicU8in onthumbv6m-none-eabiseems to have almost zero overhead. The only changes are thedmb sy, which are memory barriers that prevent race conditions; usingOrdering::Relaxed(if your problem allows it) should eliminate those, causing actual zero overhead. Other architectures should behave similar.