Singletons in embedded

Hi, I'm using an MCU that comes with a driver library written in C, and I'm intending to write a safe rust wrapper around this. I've previously used a pure rust HAL and loved the concept that peripherals must be owned to be used rather than enforcing ownership by convention. Unfortunately I have to limit the use of external libraries, so if I can get the vendor-provided wrapper library working safely I would prefer it.

With that said, I still want the concept of peripheral ownership and singletons to show through in the wrapper. Having read the embedded book section on this I have a couple of questions:

  1. Is there any way of enforcing single-ownership of a struct statically (i.e at compile time?)
  2. If the answer to the above is "no", then which option is the best for enforcing it at runtime? Imagine I have a peripheral struct Periph, there's a couple of ways I can see enforcing single ownership:

(a)

static mut PERIPH: Option<Periph> = Some(Periph { ... });

or

(b)

static mut PERIPH_OWNED: bool = false;

Conceptually, I prefer (a) since the Periph::take() function only needs to return the already initialised struct rather than construct a new one. However, holding in an option adds some memory overhead compared to just the bool, namely there will always be size_of::<Periph>() + word_size (I think), rather than just a single bool in flash.

1 Like

In Rust, every value already has to have exactly one owner and this is enforced at compilation time. So I'm not sure what exactly you mean by "single ownership".

1 Like

Sorry, maybe single instance is a better term, essentially:

fn main() {
    let p1 = Periph::new(); // this is OK, it doesn't exist yet
    let p2 = Periph::new(); // this is bad, now we have two instances of Periph
}

Sure, just assign it to a variable. Trying to use it after handing off ownership will fail with a use-after-move compile error.

On a more serious note... when wrapping a native library who's thread-safety status I'm a little... skeptical... of I'll do the following:

  • Create a zero-sized Token type (e.g. struct Token { _unused: () }) that users can't construct themselves
  • Define a Token::new() function which internally uses a boolean (usually AtomicBool, but you can probably get away with just bool)
    • If you try to create the Token when the flag is already set, the constructor can either return an error, or just blow up
  • Write the bindings in such a way that they (directly, or indirectly) require a &mut token for every operation
  • Create a Token and assign it to a local variable somewhere high in the stack (e.g. main), and pass a reference to it to anything that needs to access the peripheral/library

If you want to get really creative, I imagine you could prevent the Token being created at compile time by using some linker tricks.

For example, imagine creating a macro that constructs our Token, and as a side-effect it also defines a hidden unused #[no_mangle] function. Then the linker would blow up at link time with some sort of duplicate symbol error.

macro_rules! get_token {
    () => {{
        #[no_mangle]
        extern "C" fn tokens_can_only_be_created_once() {}
        Token { __private: () }
    }};
}

struct Token {
    #[doc(hidden)]
    pub __private: (),
}


fn main() {
    let token: Token = get_token!();
}

mod some_module_far_away {
    use super::Token;
    fn nested() {
        let duplicate_token: Token = get_token!();
    }
}

Fails to compile with

error: symbol `tokens_can_only_be_created_once` is already defined
  --> src/main.rs:4:9
   |
4  |         extern "C" fn tokens_can_only_be_created_once() {}
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
16 |     let token: Token = get_token!();
   |                        ------------ in this macro invocation
   |
   = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

(playground)

Because the function is never used (you can't even access it without an extern block), the linker should hopefully strip out the symbol and it'll be like it never existed.

8 Likes

Ooohhhh I like that linker trick, certainly a little hacky but preventing this sort of issue at compile time is a massive boost. The Token pattern is nice too, very simple and I'm going to use it.

Thanks for your help, and sorry for the confusion over ownership vs instances!

2 Likes

Just design the API in a way that it won't blow up even if the user does create two instances. Don't try to enforce that the type can't be instantiated more than once – instead make sure that the internal implementation correctly handles more than one instance.

I like this idea. I'm not sure how easy it'd be (I'm guessing a static Mutex would be involved?), but if it's possible I'd go with that one.

Although, now you've got the problem that two different parts of the code are able to interact with the same peripheral. The &mut Token approach can kinda give you this as well when you're polling two functions rapidly in a loop, but the &mut Token (as opposed to using normal function calls) makes it really obvious that you're sharing access to the peripheral and need to be nice.

Maybe. If the issue to be solved is thread safety, then that's plausible. What is the specific problem you're trying to solve? Sorry I just don't find it obvious why any generic peripheral shouldn't be accessed from more than one place.

One key reason is that it allows more local reasoning for the developer. It's the same root reason as wanting to avoid global state in the first place.

If anywhere in the code can touch the io bus, you're never able to be quite sure what state the bus is in. But if it requires a token/handle to interact with, you have all the benefits of ownership, including being able to (for example) not actually poll the hardware and maintain a (mostly transparent) software buffer until the hardware value is invalidated.

It's more design pattern and less necessity. After all, the resource is provided globally.

3 Likes

Sure thing, I'm not questioning its high-level purpose! I'm interested in exactly what the low-level details are, because without that, I can't really recommend "just use a Mutex" because that might not even help.

"It's more design pattern and less necessity." – this makes total sense, maybe I'm just being boneheaded. I guess I'd need to try and design the API myself for this to "click".

With the specific MCU I'm targeting I'm not as worried about this, it's a single core chip and execution should be single threaded (although if this library can be thread safe that would allow for expansion). I think the right pattern for a multithreaded approach is to have only a single task which interacts with the peripheral through this API. Then any other thread or task has to send data to that peripheral task, that way only one thing is in control of the peripheral at any one time.

Say for example there's a GPIO pin which is directly tied into a power supply reset pin. I'd like the end user to be able to say for certain "nothing else besides this one bit of code could ever change the state of this pin when using this library correctly". It provides a level of certainty and, provided it's implemented properly, reliability that dangerous or "mission critical" items are carefully handled.

I imagine it working something like this:

fn main() {

    let mut dpm = DangerousPinManager::new();

    // Expected behaviour, the manager doesn't think it's safe to do this
    assert(dpm.set_high().is_err());

    careless_fn();
}

fn careless_fn() {
    // I'm not going to check that it's safe to do this, so I'm gunna try use the pin directly
    //
    // This is what I want to prevent, two things accessing one pin at the same time, ideally at compile time, but runtime if not.
    driverlib::Gpio::pin_42(/* ... */).unwrap().set_high();
    
}

struct DangerousPinManager {
    dangerous_pin: GpioPin,
    is_safe: bool
}

impl DangerousPinManager {
    pub fn new() -> Self {
        Self {
            dangerous_pin: driverlib::Gpio::pin_42(/* config options */).unwrap(),
            is_safe: false
        }
    }

    pub fn set_high(&mut self) -> Result<(), Error> {
        if self.is_safe {
            self.dangerous_pin.set_high();
            Ok(())
        }
        else {
            Err(/* some error */)
        }
    }
}

I like this explanation.

When doing embedded stuff you usually need stronger guarantees than just being memory safe or free of data races. When peripherals interact with the real world (e.g. your output triggers a motor which moves a conveyor belt in an assembly line) it's possible for logic bugs to cause physical damage or put the mechanical world in an inconsistent state (imagine you start one conveyor but forgot to start the next one, so the items being conveyed start piling up and cause a mess).

Embedded systems are also a lot harder to debug than something running on an OS, because pausing for a couple seconds while you step through in the debugger can cause you to not do things that needed to be done (e.g. a proximity sensor detected an object passing by and you need to start the next conveyor up). Logging also gets a bit tricky because of memory constraints and communication bandwidth.

A slight variant on the Token approach is to create the token statically inside a Mutex<Option<Token>>, which allows code to take and keep a sensitive token such that it’s checked statically by the compiler once acquired without needing to hold the Mutex indefinitely.

In other instances, where the peripheral should be shareable, code can take the lock and use the stored Token without claiming it entirely.

Edit: Or maybe not; apparently there’s no way to produce a Mutex in a const context. It would be possible with a thread-local static Cell<_>, but you’d need some other mechanism to restrict the library to a single thread.

Hi,
let me also share my two cents here. For my bare metal Raspberry Pi related projects I've implemented a Singleton wrapper that allows me to safely share the wrapped device accross threads and cores of the Pi. The interior of the Singleton is safe guarded by a Spinlock that uses atomic operations to aquire/release the lock.
You can find details here. The actual valid build target is aarch64/32 only for the time being but may be it inspire you how you could address your requirements.

A peripheral crate would than only publicly provide access to the static singleton to it's users. And hands out safe mutual access with the .take_for function requiring a closure to be passed that contains the code that actually requires mutual access to the peripheral and can configure it and use it. As an example I've wrapped my GPIO access with a singleton in my implementations and this GPIO struct/impl tracks the usage of it's GPIO pins. So there is a runtime check that there is never the same pin used multiple times by other higher level peripherals like I2C bus etc. as those peripherals require this GPIO singleton to access for their configuration.

Hope this helps.

1 Like

Does that imply there aren't any interrupt service routines (ISRs)?

i guess this depends whether we're talking a singleton that is taken and passed around the code, or a singleton that is expected to be referenced from multiple places (including ISRs)?

in the latter case t is not exactly what you're looking for but is technically possible to create a Mutex in a const context with lazy_static if that happens to be useful, for example.

in the former case, with the static mut PERIPH: Option<Periph> = Some(Periph { ... }); option you could provide a method with the unsafe .take() inside a critical section which afaik would be pretty much functionally equivalent without requiring a mutex.

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.