How can I access my struct with peripherals in an interrupt?


I'm still on my quest to learn to use rust on a microcontroller (attiny85). I have a struct that describes the hardware used for my task with some methods to initialize it, like this:

pub struct UsiUart<'a> {
    timer0: TC0,                     // Access to the timer 0 device
    usi: USI,                        // Access to the Universal Serial Interface
    pins: Pins,                      // Access to the devices pins
    exint: &'a EXINT,                // Access to the external interrupts
    // [...]

pub extern "C" fn main() {
    let peripherals = attiny_hal::Peripherals::take().unwrap();

    let timer0 = peripherals.TC0;
    let usi = peripherals.USI;
    let exint = peripherals.EXINT;
    let pins = attiny_hal::pins!(peripherals);
    let usiuart_pins = usiuart::Pins {
        rx: pins.pb0.into_pull_up_input(),
        tx: pins.pb1.into_output(),
        rw: pins.pb3.into_output(),

    let mut _uart = usiuart::new(timer0, usi, usiuart_pins, &exint);

As you see, I don't use _uart, yet, but now I 'd like to access the configured peripherals in an ISR, where I can't pass any parameters (of course). Coming from C, I would create something like a global UsiUart instance, initialize that in main() and access the instance in the ISR. I don't know how to do that in rust, since an uninitialized struct is not allowed (or unsafe behaviour). I'm sure it's easy if you know how to do it and hope for your help :slight_smile:
(Creating the ISR is not the problem, I know how to do that).

Use once_cell::sync::Lazy to initialize a value on first access.


That doesn't work, unfortunately: I need to build with out the std lib for the tiny microcontroller which leads to:

   Compiling once_cell v1.18.0
error[E0463]: can't find crate for `std`

Are there alternatives?

I found lazy_static which offers the feature spin_no_std, but even with this feature the build fails:

   Compiling spin v0.5.2
error[E0432]: unresolved import `core::sync::atomic::AtomicUsize`
 --> /home/markus/.cargo/registry/src/
8 | use core::sync::atomic::{spin_loop_hint as cpu_relax, AtomicUsize, Ordering};
  |                                                       ^^^^^^^^^^^ no `AtomicUsize` in `sync::atomic`


Well if your target has no atomics, then that's going to be very difficult.

Well, even without atomics (which I would not take for granted on microprocessors), it should somehow be possible to access peripherals from an interrupt service routine?

Forget my UsiUart for a minute and take a step back: If I can't create a static global instance of a Peripherals object, how on earth could I user any peripheral from an ISR? Say I'd like to toggle a pin using the timer?

Please help me here. If this doesn't work, this will have been a very short excursion to the world of rust for me :frowning:

Maybe @nerditation ?

Thanks for any help,

The Grue

on microcontrollers, mutex or critical section is typically implemented by disable interrupt (globally or selectively), but for more advanced microcontrollers (like rp2040, which has two cortex-m0+ cores), it is basically the same as PCs. see:

I have only used cortex-m microcontrollers, shared states between ISR and main loop is simply critical_section::Mutex<SharedStates>. if the SharedStated type didn't implement Default, I would use Mutex<Option<SharedStates>>.

Thanks, this now seems possible again :slight_smile:

Now, I got something like this compiled, using avr_device::interrupt::Mutex:

use avr_device::interrupt::{free, Mutex};

// ...

type MyPeripherals = Mutex<Option<usiuart::UsiUart>>;
static MP: MyPeripherals = Mutex::new(None);


pub extern "C" fn main() {
    // ...
    free(|cs| {
        let o = MP.borrow(cs);
        // *o = Some(usiuart::new(timer0, usi, usiuart_pins, exint));

But like this, I am not able to assign anything to the Mutex's inner value, because borrow() returns an immutable reference. I see that there's Mutex::get_mut, but I don't understand how to use it…

In the documentation to critical_section, they use a Mutex<Cell<...>>. If I do that, too, I can assign to that, but I don't understand why the Cell is necessary. Shouldn't it be possible to change the value that the Mutex contains? Also, @nerditation doesn't mention the Cell in his example...

If I have to use a Cell-like structure, it should be a RefCell, I guess, but that's a different question.

You don't. The whole point of the mutex is that it provides interior mutability, ie. you can mutate it through a shared reference. You have to call lock().

Well, that's how I would do it, too. But where did I miss that avr_device::interrupt::Mutex has a lock() method?

Ah, so it's not the std Mutex. Well, the documentation says that it's a "mutex", in quotes, and that it's only safe on single-core systems. It looks like this is not a very useful abstraction and probably not what you want.

I'm working on an attiny85. The avr_device is what's available there. For these controllers CLI/STI is as good as it gets...
Oh, and btw: the real question is: Why do I need Mutex<Cell<...>>? Everything seems to work well like this, but I would like to understand why/if Cell is necessary.

(Speaking generally, not on your problem specifically.)

A static value is referable from everywhere, e.g. you can get a &'static shared references to it.

If you need to get a &mut or otherwise overwrite something contained within the static, that would be a aliasing violation should such a &'static shared reference exist... unless there's an intervening UnsafeCell involved, which opts shared references out of the immutability guarantee.

Read more here.

You may not think this matters in a single-threaded context, but it does. Alias violations can happen at different levels of your call stack by creating a reference, calling a function that accesses the same resource, and then using the reference again.

this kind of stuff only really matters for very resource constrained embedded environment. I'll admit the naming is confusing and the somewhat misleading, since the term Mutex already has a well established meaning for a broader range of developers.

the bare metal Mutex is not a locking mechanism as the standard Mutex, but just a type level wrapper to make type Sync. it relies on the implementation of a critical section token, to protect the access. a locking Mutex would necessary add runtime overhead when access the protected data, which might or might not be acceptable on a very low end microcontroller. in the microcontroller realm, it is not uncommon the protected state is just a single byte or single word, adding an extra byte (or even bit) to them might not be desirable (or even viable).

also, the semantic of a "critical section" has subtle difference depending on the context, e.g., for Win32, entering a CriticalSection just grant the current thread the right to access the shared the data , it is NOT guaranteed that the current scope (or function) has exclusive access (the semantic of rust's exclusive reference, a.k.a. mutable borrow). in some sense, it's like how static mut variables needs unsafe to access even in single threaded programs, being single-threaded (race free) merely means "exclusive to current thread", but not "exclusive to current scope".

in other words, a critical section can be nested or re-entrant (Win32 is such an example). the bare metal critical section abstraction enables the possibility for the implementation to be nestable, but it doesn't have to: it's the implementation's choice (and the user's). this is also the reason the critical_section::Impl trait is defined like this:

pub unsafe trait Impl {
    unsafe fn acquire() -> RawRestoreState;
    unsafe fn release(restore_state: RawRestoreState);

the concrete RawRestoreState type is selected using feature flags at compile time. for an un-nested implementation, it can be (), then the critical section token is truly stateless, and compiles down to barely "disable interrupt" and "enable interrupt" instructions. in such cases, nested access is UB, thus the low level acquire() and release() are unsafe, but critical_section::with() is always safe.

when the protected states are simple (say, one or two words, and is Copy), Mutex<Cell<T>> can be used to mutate the states. for more complicated types or the "locking" behavior is desired, use Mutex<RefCell<T>>, which adds the runtime overhead (a borrow flag), and achieves the same functionality of std::sync::Mutex (well, almost, because technically, std::sync::Mutex has more overhead like poisoning, it's std not core after all). this is documented explicitly.

as a side note, there are actually 2 crates that provide CriticalSection types: bare_metal and critical_section. I don't know the full story, but the different is very subtle, critical_section is more abstract and flexible, but needs an concrete implementation to link against, but for embedded systems they are (almost?) always implemented using the same mechanism.

my experience is with cortex-m, I don't know how other platform deals with them, but for cortex-m, bare_metal::CriticalSection is always available (it doesn't even provide an Impl trait), and the "interrupt free context" wrapper function uses it; critical_section::CriticalSection, on the other hand, need to be enabled via a feature flag critical-section-single-core, otherwise you get a link error. however, custom implementation can be provided if the cortex-m crates' implementation doesn't fits.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.