Is there a lightweight alternative for a Mutex in embedded Rust when a value will be written once at startup and then only read?
Asked Answered
D

2

6

According to the Rust Embedded Book about concurrency, one of the better ways to share some data between contexts is using mutexes with refcells. I understand how they work and why this is necessary. But there is a scenario where the overhead cost seems to much.

The mutex of the cortex_m crate works in this way:

cortex_m::interrupt::free(|cs| {
    let my_value = my_mutex.borrow(cs).borrow();
    // Do something with the value
});

The mutex requires the cs (CriticalSection) token before it gives access. In a critical section, no interrupts can happen so we know we're the only ones that can change and read the value. This works well.

The scenario I'm in now, however, has the variable be written to once for initialization (at runtime) and then always be treated as a read-only value. In my case it's the clock speed of the MCU. This cannot be a compile-time constant. An example why is waking up from deep sleep: depending on the state of the hardware, it may be chosen to use a slower clock speed to conserve some energy. So at startup (or rather a wakeup where all the RAM is gone) a different clock speed may be selected every time.

If I simply want to read the value, it seems wasteful to go through the whole critical section setup. If this variable may be changed again, then, yes, it's necessary. But that's not the case here. It will only get read.

Is there a better way of reading a shared variable with less overhead and without using unsafe Rust?

Denadenae answered 20/11, 2019 at 12:0 Comment(12)
This sounds like a case for OnceCell however I am not sure of the applicability to embedded environments.Nikolia
OnceCell would indeed suffice, but I'd need Sync enabled. It only has that with std enabled which makes it unusable in embeddedDenadenae
Why whitout unsafe Rust? If you use it carefully, you can write a performant and safe code. You shouldn't exclude it as a matter of principle.Downtoearth
If a wrapper can be made that uses unsafe, that'd be fine. I know Cells use unsafe as well. But what I don't want is to put unsafe { ... } every time I need to read the variableDenadenae
You can do it such that you need unsafe where you write the value, not where you read it.Helium
As said by @michalsrb, you ensure that it is not read before being writen once (unsafe write) and then, you can read it whenever you want with safe code. Just be sure that you don't do anything UB when writing it as it's not totally trivial to do it correctly.Downtoearth
Why not just make it a constant? Write it once using the constant and then always read from the constant. Zero problems.Universalize
@Universalize That is obviously the best way if it can be constant at compile time. In this case, however, it can not.Denadenae
Why not? It's not like you are going to read the clock speed from the user over the serial bus at device startup.Universalize
An example is waking up from deep sleep. Depending on the state of the hardware, it may be chosen to use a slower clock speed to conserve some energy. So at startup (or rather a wakeup where all the RAM is gone) a different clock speed may be selected every time.Denadenae
@Geoxion: Wait a minute. If the clock speed can differ every time the programs wakes up, doesn't it mean that you need to set your values more than once?Nikolia
@MatthieuM.Only after a new bootup. Technically it's a wakeup, but all the RAM is gone so it's exactly like a bootup.Denadenae
H
1

If a &'static will suffice, I would recommend checking out the static_cell crate (Repo, Lib.rs, Docs.rs).

From the README:

use static_cell::StaticCell;

// Statically allocate memory for a `u32`.
static SOME_INT: StaticCell<u32> = StaticCell::new();

// Initialize it at runtime. This returns a `&'static mut`.
let x: &'static mut u32 = SOME_INT.init(42);
assert_eq!(*x, 42);

// Trying to call `.init()` again would panic, because the StaticCell is already initialized.
// SOME_INT.init(42);

I discovered this crate while looking at an implementation of a CYW43439 wifi chip driver. There's a pretty nifty macro you may find useful:

macro_rules! singleton {
    ($val:expr) => {{
        type T = impl Sized;
        static STATIC_CELL: StaticCell<T> = StaticCell::new();
        STATIC_CELL.init_with(move || $val)
    }};
}

// ...

    // Init network stack
    let stack = &*singleton!(Stack::new(
        net_device,
        config,
        singleton!(StackResources::<1, 2, 8>::new()),
        seed
    ));

Headreach answered 16/9, 2022 at 6:31 Comment(1)
I've marked this as the answer now because I use StaticCell a lot nowadaysDenadenae
D
7

With the help of some of the comments, I came up with this:

use core::cell::UnsafeCell;
use core::sync::atomic::{AtomicBool, Ordering};

/// A cell that can be written to once. After that, the cell is readonly and will panic if written to again.
/// Getting the value will panic if it has not already been set. Try 'try_get(_ref)' to see if it has already been set.
///
/// The cell can be used in embedded environments where a variable is initialized once, but later only written to.
/// This can be used in interrupts as well as it implements Sync.
///
/// Usage:
/// ```rust
/// static MY_VAR: DynamicReadOnlyCell<u32> = DynamicReadOnlyCell::new();
///
/// fn main() {
///     initialize();
///     calculate();
/// }
///
/// fn initialize() {
///     // ...
///     MY_VAR.set(42);
///     // ...
/// }
///
/// fn calculate() {
///     let my_var = MY_VAR.get(); // Will be 42
///     // ...
/// }
/// ```
pub struct DynamicReadOnlyCell<T: Sized> {
    data: UnsafeCell<Option<T>>,
    is_populated: AtomicBool,
}

impl<T: Sized> DynamicReadOnlyCell<T> {
    /// Creates a new unpopulated cell
    pub const fn new() -> Self {
        DynamicReadOnlyCell {
            data: UnsafeCell::new(None),
            is_populated: AtomicBool::new(false),
        }
    }
    /// Creates a new cell that is already populated
    pub const fn from(data: T) -> Self {
        DynamicReadOnlyCell {
            data: UnsafeCell::new(Some(data)),
            is_populated: AtomicBool::new(true),
        }
    }

    /// Populates the cell with data.
    /// Panics if the cell is already populated.
    pub fn set(&self, data: T) {
        cortex_m::interrupt::free(|_| {
            if self.is_populated.load(Ordering::Acquire) {
                panic!("Trying to set when the cell is already populated");
            }
            unsafe {
                *self.data.get() = Some(data);
            }

            self.is_populated.store(true, Ordering::Release);
        });
    }

    /// Gets a reference to the data from the cell.
    /// Panics if the cell is not yet populated.
    #[inline(always)]
    pub fn get_ref(&self) -> &T {
        if let Some(data) = self.try_get_ref() {
            data
        } else {
            panic!("Trying to get when the cell hasn't been populated yet");
        }
    }

    /// Gets a reference to the data from the cell.
    /// Returns Some(T) if the cell is populated.
    /// Returns None if the cell is not populated.
    #[inline(always)]
    pub fn try_get_ref(&self) -> Option<&T> {
        if !self.is_populated.load(Ordering::Acquire) {
            None
        } else {
            Some(unsafe { self.data.get().as_ref().unwrap().as_ref().unwrap() })
        }
    }
}

impl<T: Sized + Copy> DynamicReadOnlyCell<T> {
    /// Gets a copy of the data from the cell.
    /// Panics if the cell is not yet populated.
    #[inline(always)]
    pub fn get(&self) -> T {
        *self.get_ref()
    }

    /// Gets a copy of the data from the cell.
    /// Returns Some(T) if the cell is populated.
    /// Returns None if the cell is not populated.
    #[inline(always)]
    pub fn try_get(&self) -> Option<T> {
        self.try_get_ref().cloned()
    }
}

unsafe impl<T: Sized> Sync for DynamicReadOnlyCell<T> {}

I think this is safe due to the atomic check and the critical section in the set. If you spot anything wrong or dodgy, please let me know.

Denadenae answered 20/11, 2019 at 13:53 Comment(5)
Can your clock speed be represented by an integral, and does your platform support reading/writing values of such an integral atomically?Nikolia
The clock settings are stored in a struct. I guess it could be split up to separate atomic variables, but that would be inconvenient. But there are also more things I want to use this with other than clock settings which are set once and then read only.Denadenae
Sounds good; I've tweaked the memory orderings are Relaxed is too lax, and could allow reading from the UnsafeCell before the write is actually completed -- especially on out-of-order CPUs. Hopefully they are still lowered down to the appropriate instructions on your chip.Nikolia
That seems better indeed. I believe atomics are fully supported on my architecture (Cortex M4). I updated the code with the ability to use non-copy types. You can get them with get_ref() and try_get_ref().Denadenae
Have you tried reducing the duplication by implementing every getter in terms of try_get_ref? get_ref would be if let Some(r) = self.try_get_ref() { r } else { panic!("...") }, try_get would be self.try_get_ref().copied() and get would be *self.get_ref(). Of course, you'd need to just that inlining works properly... possibly sticking a #[inline(always)] on top of try_get_ref.Nikolia
H
1

If a &'static will suffice, I would recommend checking out the static_cell crate (Repo, Lib.rs, Docs.rs).

From the README:

use static_cell::StaticCell;

// Statically allocate memory for a `u32`.
static SOME_INT: StaticCell<u32> = StaticCell::new();

// Initialize it at runtime. This returns a `&'static mut`.
let x: &'static mut u32 = SOME_INT.init(42);
assert_eq!(*x, 42);

// Trying to call `.init()` again would panic, because the StaticCell is already initialized.
// SOME_INT.init(42);

I discovered this crate while looking at an implementation of a CYW43439 wifi chip driver. There's a pretty nifty macro you may find useful:

macro_rules! singleton {
    ($val:expr) => {{
        type T = impl Sized;
        static STATIC_CELL: StaticCell<T> = StaticCell::new();
        STATIC_CELL.init_with(move || $val)
    }};
}

// ...

    // Init network stack
    let stack = &*singleton!(Stack::new(
        net_device,
        config,
        singleton!(StackResources::<1, 2, 8>::new()),
        seed
    ));

Headreach answered 16/9, 2022 at 6:31 Comment(1)
I've marked this as the answer now because I use StaticCell a lot nowadaysDenadenae

© 2022 - 2024 — McMap. All rights reserved.