Mutex<RefCell<Option<T>>> on STM32 project

Hi,

I am still a novice to Rust, and I've been struggling with the combination of Mutex + RefCell + Option in my STM32 project.

What I want in the code is described in the comments below, and the purpose of doing this, as indicated, is in order to use the GLOBAL_DATA in other functions without having to be passed an argument.

I think this might not be a recommended way in Rust, but is there even a way for that?
I will look forward to any suggestions!

Thanks.

#![no_main]
#![no_std]

use core::cell::RefCell;
use core::panic::PanicInfo;
use core::borrow::{Borrow, BorrowMut};
use core::ops::Deref;
use cortex_m_rt::entry;
use stm32l4xx_hal as hal;
use spin::Mutex;

pub struct Data {
    value: u32,
}

static GLOBAL_DATA: Mutex<RefCell<Option<Data>>> = Mutex::new(RefCell::new(None));

#[entry]

fn main() -> ! {

    unsafe {

        let data = Data{value: 42}; 
        GLOBAL_DATA.lock().replace(Some(data));

        let guard = GLOBAL_DATA.lock(); 
        let ref data_ref = guard.borrow().get_mut().unwrap();

        // Here I would like to get an access to the data.value through GLOBAL_DATA.
        // But, how?
        // Is it even possible?
    }

    loop {
    }
}

#[panic_handler]
fn panic(_panic: &PanicInfo<'_>) -> ! {
    loop {}
}

you should only use mutable global/static variable to share states between the "normal" code and interrupt handler. if you are using it for different purpose, please clarify a little bit the motivation of why you need it.

also, I noticed you are using spin::Mutex, which might have other problems, such as deadlocks, when used incorrectly, e.g. inside a interrupt handler. so it'd be better if you can share more details how you plan to use it.

1 Like

Thanks @nerditation for the reply. So, the code I posted is for an example, and the actual use case is for the use of a serial comm structure instance, which will hopefully be kept as a static variable.

Currently, I have a custom print! macro, which uses SerialSender object. And, all the other functions that are using the macro has SerialSender object as a reference, which I don't like.

fn main() -> ! {
    
    let (timer, mut led3, uart_tx ...) = init_hardware();

    let mut sender = SerialSender::new(uart_tx);

    ...
    cli_clear_screen(&mut sender);
    print!(sender, "CLI: Enter a command...\r\n");
    
    ...
    }
    
fn cli_clear_screen(sender: &mut SerialSenderType) {
    print!(sender, "\x1b[H\x1b[2J");
}

I'm a little confused why you're wrapping a RefCell in a Mutex, since the mutex guarantees exclusive mutable access always, so the cell adds nothing in terms of interior mutability.

2 Likes

for bare metal targets, Mutex works differently (or at least, they can, depending which Mutex you are using).

this only holds for the standard Mutex. you can read the full explanation in critical_section::Mutex, but short answer is, bare metal targets care runtime overhead even more, and many bare metal targets are single threaded, and only thing to worry about is the interrupt handler. thus, Mutex can be implemented statelessly (by simply disabling the interrupt). this means critical sections can be nested or re-entrant, thus it's not sound to guarantee &mut T access.

in other words, it is a Sync wrapper, but doesn't provide interior mutability. interior mutability must be opt-in by the user (e.g. the user can choose to use RefCell with runtime overhead, or to do it unsafely).

4 Likes

although I would prefer explicit arguments personally, I can see how your use case can be justified, since the serial port is truly a singleton resource.

I don't know how your serial port communication is implemented, but personally, I would design the public API to use shared reference &self instead of exclusive reference &mut self, possibly with a wrapper type, and do the synchronization internally.

feel free provide more details and contexts, the most unsafe (and thus with the least runtime overhead) implementation is Mutex<UnsafeCell<MaybeUninit<T>>>, while the safest (and with most runtime overhead) is what's in your original post: Mutex<RefCell<Option<T>>>.

the outer Mutex adds the Sync, RefCell or UnsafeCell add interior mutability, and MaybeUninit or Option provide a way to initialize the data at runtime.

1 Like

Weren't they using spin::Mutex, though? Which is just a standard spinlock mutex? And couldn't you just use a ForceSync-style wrapper, rather than adding a mutex that's not really a mutex? But I feel like I'm probably misunderstanding something here.

1 Like

FWIW, I currently use a log impl for this very purpose. I'm using embassy, so there is a crate for it, but you can see how the static logger is created in the with_class!() macro near the bottom.

After initializing the log crate in this way, it's just a matter of using the info!(), warn!(), error!(), et al., macros like you normally do everywhere else.

1 Like

you are right, my bad. I have not used the spin crate before, but assumed it was like the critical_section::Mutex (which is typical on bare metal targets) since the question is about the stm32 target.

yes, a ForceSync wrapper definitely can work, but it is generally more unsafe, since the user are required to hold all the safety invariants. on the other hand, critical_section delegates the unsafe part to other low level libraries.

typically, critical sections are implemented by hal crates, the architecture support crates, or even BSP, etc, so the user is not forced to use unsafe. at the same time, it allows the user to use custom implementations if desired. e.g. you can (unsafely) create a CriticalSection token out of thin air, making Mutex<T> essentially the same as ForceSync<T>.

2 Likes

Thanks for the info! I never knew all this about critical sections, and assumed (since you said they were stateless) that they were the same as a simple wrapper. Though this is all a little off-topic... :sweat_smile:

1 Like

I should clarify a little bit more on this.

when I say it's stateless, I was referring the Mutex<T> type itself, and it is indeed a simple wrapper over T (UnsafeCell<T>), unlike the standard Mutex<T>, which typically bundles some low level locking primitives together with the T.

however, although critical_section::Mutex<T> contains no extra information itself, the API requires a CriticalSection token to be able to borrow the inner data, and the critical section implementation is allowed (but not required) to save some information (usually the hardware flag of whether interrupt is enabled) when entering a critical section, and restore the saved information when exiting the critical section.

in other words, the states needed to implement the locking mechanism is not stored along side T by Mutex<T>, but locally managed during the transition into/out-of the critical section by the implementator.

this level of fine control does not really matter for normal hosted environments, but for a bare metal mcu with as few as 2KiB (could be even less) of RAM, every bit matters.

the separation of Mutex and critical section suits well for this very typical bare metal use cases, where a single global lock (the interrupt enabled flag) is used to protect every piece of data that might be accessed concurrently from both the app and the interrupt handler.

if you bundle the "bare" Mutex<T> together with the critical section implementation, you can get a Mutex with an API similar to the "standard" one.

2 Likes

So, here's more code, and I wanted to get my sender instance that I get in main into the SENDER, which is a global instance. And, again, I don't know how I can get access to sender through SENDER.

use hal::pac::{Peripherals, USART2, interrupt, Interrupt};
use my_nucleo_l432::serial_sender::SerialSender;
...

type SerialSenderType = SerialSender<Tx<USART2>>;
static SENDER: Mutex<RefCell<Option<SerialSenderType>>> = Mutex::new(RefCell::new(None));

fn main() -> ! {
    
    let (timer, mut led3, uart_tx ...) = init_hardware();

    let mut sender = SerialSender::new(uart_tx);
    ...

    ...
}

The code below is for the SerialSender, which is basically a wrapper for Tx.
At the end of the code is the macro that I've wanted to change into a global function.

use core::fmt::Write;
use hal::pac::USART2;
use hal::prelude::*;
use hal::serial::Tx;
use stm32l4xx_hal as hal;

const SIZE_TX_BUFFER: usize = 128;

pub trait SendByte {
    fn send_bytes(&mut self, bytes: &str);
    fn send_byte(&mut self, byte: u8);
}

impl SendByte for Tx<USART2> {
    fn send_bytes(&mut self, bytes: &str) {
        for byte in bytes.bytes() {
            self.send_byte(byte);
        }
    }

    fn send_byte(&mut self, byte: u8) {
        block!(self.write(byte)).ok();
    }
}

pub struct SerialSender<T: SendByte> {
    tx: T,
}    

impl<T: SendByte> SerialSender<T> {
    pub fn new(tx: T) -> Self {
        SerialSender { tx }
    }

    pub fn send_formatted(&mut self, format: core::fmt::Arguments) {
        let mut buffer = heapless::String::<SIZE_TX_BUFFER>::new(); // Adjust buffer size as needed
        write!(buffer, "{}", format).unwrap();
        self.tx.send_bytes(&buffer);
        buffer.clear();
    }
}

#[macro_export]
macro_rules! print {
    ($sender:expr, $($arg:tt)*) => {{
        $sender.send_formatted(format_args!($($arg)*));
    }};
}```

Thanks! I will check that out for sure!

I see, your SerialSender is blocking, and using a local buffer, this should be relatively simple.

as pointed out by @Kyllingene , the Mutex from the spin crate guarantees exclusive access, so no need for the intermediate RefCell, just use Mutex<Option<...>>, the static variable can be defined like this:

static SENDER: Mutex<Option<SerialSenderType>> = Mutex::new(None);

in main, you initialize it as usual:

let sender = SerialSender::new(...);
// note `MutexGuard` implements `DerefMut`,
// so we can call `Option::replace()` method directly
SENDER.lock().replace(sender);

when you need to send data:

SENDER.lock().as_mut().unwrap().send_formatted(...);

here's a break down in steps:

// lock the mutex, get a guard object
let mut guard: MutexGuard<Option<SerialSenderType>>> = SENDER.lock();
// the guard object implements `Deref` and `DerefMut`
// so we can call methods on the inner `Option<SerialSender>` directly
// `.as_mut()` turns `&mut Option<SerialSender>` into `Option<&mut SerialSender>`
// finally we `.unwrap()` it
let sender: &mut SerialSender = guard.as_mut().unwrap();
// use the sender
sender.send_formatted(format_args!("hello"));
// with these steps, should drop the guard, as soon as possible
// it is implicitly done in the one-liner version above
drop(guard);

you macro can expand directly into the code above:

macro_rules! print {
    ($($arg:tt)*) => {{
        SENDER.lock().as_mut().unwrap().send_formatted(format_args!($($arg)*));
    }};
}

you can create a helper function to reduce the boilerplates, but I checked the spin crate, it doesn't support .map() for MutexGuard, so the helper function has to use callbacks:

fn with_sender<R>(callback: impl FnOnce(&mut SerialSender) -> R) -> R {
    (callback)(SENDER.lock().as_mut().unwrap())
}

some notes:

  • the spin crate is really not a good fit for single core bare metal targets.
    • when the mutex is used in interrupt handlers, it may cause deadlocks.
    • if the mutex is never used interrupt handlers, it's basically a ForceSync wrapper, but with unnecessary overheads.
      • EDIT:
      • actually, it's more like ForceSync<ExclusiveCell<>>, not as bad as I initially though.
      • where ExclusiveCell is a hypothetic runtime checked borrow wrapper like RefCell but only allows exclusive borrow.
  • if you ever want to access the global variable in interrupt handlers, you should check the critical-section crate
  • I see the spin crate has a Lazy wrapper, I think Lazy<RefCell<...>> can be also be used, but in terms of comlexity, it won't be much difference than Mutex<Option<...>>.
  • you can also create a wrapper function specifically for the .send_formatted() method, e.g.:
    fn global_send_formatted(format: core::fmt::Arguments) {
        with_sender(|sender| sender.send_formatted(format));
    }
    
1 Like

Yeah, I would say the problem was basically in,

Mutex<RefCell<Option<T>>> vs Mutex<<Option<T>>

And with your suggestion, it simply works well, and the code has become much cleaner.
Thank you so much for your help!

// main.rs
...

fn main() -> ! {
    let (timer, mut led3, uart_tx, ...) = init_hardware();

    my_nucleo_l432::serial_sender::create_singleton_sender(uart_tx);

    ...
    // start the service
    cli_clear_screen();
    print!("CLI: Enter a command...\r\n");

    ...
}

// serial_sender.rs 
...
pub static SENDER: Mutex<Option<SerialSenderType>> = Mutex::new(None);

...
pub fn create_singleton_sender(tx: Tx<USART2>) {
    let sender = SerialSender::new(tx);
    SENDER.lock().replace(sender); 
}

/**
 * Macro to simplify sending formatted strings.
 */
#[macro_export]
macro_rules! print {
    ($($arg:tt)*) => {{
        let mut guard = SENDER.lock();
        if let Some(sender) = guard.as_mut() {
            sender.send_formatted(format_args!($($arg)*));
        }
    }};
}
1 Like