Box, Arc, Mutex in Embedded Rust (RTIC)

Hello.

How to use Box , Arc and Mutex in Embedded Rust (no_std )? I would like to implement a strategy design pattern in Embedded Rust - RTIC.

uC: stm32f103vet6

Box only requires heap allocation (malloc). Check if you can use the alloc crate, which provides Box and Arc.

The other two depend on hardware features, check the data sheet of your processor. If it's a common hardware, chances are people have made crates tailored to specific features.

Also these two might be useful reads

https://docs.rust-embedded.org/book/concurrency/

Thanks for your reply.
It's STM32 - stm32f103vet6.

That specific stm32 is a Cortex-M3 (ARMv7-M), which has tier 2 compiler support "without host tools" (cross-compilation only) and without the standard library.

You can provide your own allocator (or just use an existing one) to use the alloc crate for Box and Arc, and you can use something like spin for a Mutex. As with most things in Rust, it seems, "there's a crate for that" is probably the right answer.

1 Like

It's worth noting that you can dodge some overheads with the strategy pattern by taking advantage of first-class functions.

struct Duck {
    fly_fn: Box<dyn Fn()>
}

fn do_not_fly() {
    println!("I can't fly!");
}

fn fly_with_wings() {
    println!("I can fly using my wings!");
}

let mut my_duck = Duck {
    fly_fn: Box::new(do_not_fly)
};

my_duck.fly_fn = Box::new(fly_with_wings);

The most generic way to do this would be to allow Ducks to be polymorphic over their fly-functions.

struct Duck<F: Fn()> {
    fly_fn: F
}

This would enable ducks with fly-functions known at compile time (Duck<MyFlyFn>), ducks with boxed fly-functions (Duck<Box<dyn FlyFn>>), and ducks with references to fly functions (Duck<&dyn FlyFunction>) at no extra cost. If you use references instead of boxes, you won't even need to use an allocator, which is good news for embedded environments.

2 Likes

Would you write a basic example of how to do it in RTIC?
I have a problem with adding the struct OperationStrategy to Shared.

#[shared]
struct Shared 
{
strategy: OperationStrategy
}

My example (not working).

Can you clarify exactly what you're trying to do? The linked code in the Rust playground doesn't seem to refer to your Shared struct.

#![deny(unsafe_code)]
#![no_std]
#![no_main]

#[cfg(not(test))]
use defmt_rtt as _;

#[cfg(not(test))]
use panic_halt as _;

#[rtic::app(device = stm32f1xx_hal::pac)]
mod app {
    use stm32f1xx_hal::prelude::*;

    pub trait OperationStrategy: Sized + 'static
    {
        fn operate(&mut self);
    }

    struct Di{}
    impl OperationStrategy for Di{
        fn operate(&mut self)
        {
            defmt::println!("Di - operate on inputs (read inputs)");
        }
    }

    struct Do{}
    impl OperationStrategy for Do{
        fn operate(&mut self)
        {
            defmt::println!("Do - operate on outputs (write outputs)");
        }
    }

    #[shared]
    struct Shared {
        dev_type: dyn OperationStrategy //not working
    }

    #[local]
    struct Local {}

    #[init]
    fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) 
    {        
        let mut gpioa = cx.device.GPIOA.split();

        // DI or DO
        let dev_type_selector = gpioa.pa0.into_pull_down_input(&mut gpioa.crl);

        let dev_type;
        if dev_type_selector.is_high() {
            dev_type = Di{};
        }
        else {
            dev_type = Do{};
        }
        
        (Shared {dev_type}, Local {}, init::Monotonics())
    }
}

and code in Github Repo

#[shared]
struct Shared {
    dev_type: dyn OperationStrategy //not working
}

This is the heart of your problem. Because a dyn object is not fixed in size, we can't store it in a static struct. You can really only fix this by boxing the object, since you can't generate a reference with bounded lifetime and store it in a static value.

In general, OOP design patterns are poorly suited for embedded systems. Here are a few ideas as as to alternate implementations which achieve the same result without requiring allocations.

Option 1: Store a function pointer instead of a dyn object.

#[shared]
struct Shared {
    dev_fn: fn();
}

fn do_in() {
    defmt::println!("Di - operate on inputs (read inputs)");
}

fn do_out() {
    defmt::println!("Do - operate on outputs (write outputs)");
}

#[init]
fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) 
{        
    let mut gpioa = cx.device.GPIOA.split();
    // DI or DO
    let dev_type_selector = gpioa.pa0.into_pull_down_input(&mut gpioa.crl);
    let dev_type = if dev_type_selector.is_high() { do_in } else { do_out };
      
    (Shared {dev_fn: dev_type}, Local {}, init::Monotonics())
}

Here, dev_fn is a function pointer which can be called directly. Since your objects in the example are stateless, they don't need to take any data parameters.

Option 2: Use an enum and branch on it every time.

If you know at compile time what all the variants will be, and you don't mind branching on the state every time you call against the dev type, you can just store the dev_type as an enum.

enum DevType {
    Di,
    Do,
}

impl DevType {
    fn operate(&self) {
        match self {
            DevType::Di => defmt::println!("Di - operate on inputs (read inputs)"),
            DevType::Do => defmt::println!("Do - operate on outputs (write outputs)"),
        };
    }
}


#[shared]
struct Shared {
    dev_type: DevType,
}

#[init]
fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) 
{        
    let mut gpioa = cx.device.GPIOA.split();
    // DI or DO
    let dev_type_selector = gpioa.pa0.into_pull_down_input(&mut gpioa.crl);
    let dev_type = if dev_type_selector.is_high() { DevType::Di } else { DevType::Do };
      
    (Shared {dev_type}, Local {}, init::Monotonics())
}

Thanks for your reply.
I thought about an enum, but this solution has overhead (you have to check the value every cycle).
Solution nr 1 is OK. But I need a mutable function (FnMut.. or fn(t: &mut Target)).

    pub struct Target
    {
        pub value: u8
    }

    fn do_in(t: &mut Target) {
        defmt::println!("Di - operate on inputs (read inputs) value: {}", t.value);
    }
    
    fn do_out(t: &mut Target) {
        t.value = t.value + 1;
        defmt::println!("Do - operate on outputs (write outputs) value: {}", t.value);
    }

    #[shared]
    struct Shared {
        dev_fn: fn(t: &mut Target),
    }

or hybrid:

  pub trait OperationStrategy
    {
        fn operate(&mut self);
    }

    pub struct Di {
        inputs: Vec<ErasedPin<stm32f1xx_hal::gpio::Input>, 16>,
    }

    impl Di 
    {
        pub fn new() -> Self
        {
            Self{inputs: Vec::new()}
        }
    }

    pub struct Do {
        outputs: Vec<ErasedPin<stm32f1xx_hal::gpio::Output>, 16>
    }

    impl Do
    {
        pub fn new() -> Self
        {
            Self{outputs: Vec::new()}
        }
    }

    impl OperationStrategy for Di {
        fn operate(&mut self) 
        {
            defmt::println!("Di - operate on inputs (read inputs)")
        }
    }

    impl OperationStrategy for Do {
        fn operate(&mut self) 
        {
            defmt::println!("Do - operate on outputs (write outputs)")
        }
    }

    pub enum DevType {
        Di(Di),
        Do(Do),
    }

    impl DevType
    {
        pub fn new_do() -> Self
        {
            DevType::Do(Do::new())
        }

        pub fn new_di() -> Self
        {
            DevType::Di(Di::new())
        }

        fn operate(&mut self)
        {
            match self {
                DevType::Di(d) => { d.operate(); },
                DevType::Do(d) => { d.operate(); }
            };
        }
    }

What do you think?

Solution1 (enum) - github
Solution2 (function ptr) - github