How to place complex types into global statics?

Newbie here. How can I place this stuff into a global static?

let spi1hal: Spi<Enabled, SPI1, (Pin<Gpio11, FunctionSpi, PullDown>, Pin<Gpio12, FunctionSpi, PullDown>, Pin<Gpio10, FunctionSpi, PullDown>)>

It's the result of SPI initialization. I'd like to have it in a global static in order to access the peripherals from other functions (ISRs too, maybe?); and without the need to search for changes in the code every time I change a pin or a pullup/pulldown. It should be bus agnostic solution (ie: I've similar complex types for i2c and uarts).

Don't have to necessarily be a global static; but even if I place that nightmare into a function arg, I need to define the type and ... same problem apply: whenever I change 1 pin I must go searching for all the functions using that peripheral.
EDIT: I'm using rtic, so have that variable into the tasks' "Shared" resources, would probably be a better solution than a global static.

I'm missing something important; more than one thing. But I've no idea how to solve this kind of problem. I've been wrestling with structs and traits, lifetimes and the borrow checker, for 2 days before posting. I'm exhausted.

Would it help to define a type alias for your long types?

1 Like

As I said, I'm a newbie but ... It doesn't look like a solution. If I understand correctly the book, an alias is just .... an alias: whenever I change 1 pin, the real type changes and the alias refers to the old one. The problem isn't to beautify the code. The problem is to have a memory location for different type variants (ex: different pins in the type) for the same object (ex: SPI bus instance).

EDIT: maybe using some form of introspection. Like a macro. But it's far from my capabilities. I've been playing with macros about one year ago and ... it has been a lot of fun but a waste of time also. Macros are evil, no matter what language you are using :stuck_out_tongue:

We'll get you over to the dark side eventually. It's only a matter of time. insert evil laugh here

Use static_init - an give yourself a break from the past 48 hours.

Unless you're in #![no_std] kind of an environment?

1 Like

Sorry, I've never done embedded programming and didn't understand what you meant. Probably best to wait for someone with embedded experience to say how they deal with this issue.

Yes, embedded => no_std. There's once_cell and its lazy-equivalent of lazy_static but ... can't handle that as well. The type I pasted above is the result of the initialization code for the SPI bus; it is composited along the way and at the end (ie: when the peripheral is ready to be used) it looks like that: an horror movie. So even using those "reference boxing tools" ... can't get the same flex of C void *.

My bad: didn't notice the "embedded" section, to begin with. Like @jumpnbrownweasel, I'd be way over my head here. Since you're using RTIC, a shared resource is what you need here.

Based off a few things I can gather:

#[rtic::app(no_idea_what_any_of_this_means)]
mod app {
    use cortex_m_semihosting::*;

    // the type of your "stuff"
    type Spi1Hal = Spi<Enabled, SPI1, (Pin<Gpio11, FunctionSpi, PullDown>, Pin<Gpio12, FunctionSpi, PullDown>, Pin<Gpio10, FunctionSpi, PullDown>)>;
    
    #[shared]
    struct Shared {
        // the shared key for your "stuff"
        spi1hal: Spi1Hal 
    }

    #[init]
    fn init(_: init::Context) -> (Shared, Local) {
        some_task::spawn().unwrap();
        // perform your initialization here
        let spi1hal = init_your_stuff();
        // then declare it as shared here
        (Shared { spi1hal }, Local {})
    }

    #[task(shared = [spi1hal])] // <- for every fn that needs your "stuff"
    async fn some_task(mut ctx: some_task::Context) {
        // lock/update/use your "stuff"
        c.shared.spi1hal.lock(|shared| {});
    }

Yep. That's what I've found in rtic's examples code. But, again, it forces me to change that "type ..." line every time I change a pin. Is there some sort of "type container" it can take them all like ... void * (and casting)?

Anyway, I'm arranging some nested type aliases ... it looks like a workaround rather than a solution but it might end up being not too ugly.

Since there haven't been any replies from embedded devs, I did this lookup:
https://lib.rs/search?q=static+no_std
and found this no_std crate for global initialization:
https://lib.rs/crates/static_init

Huh, it does say: every feature with no_std support - after all. Although, there's a catch.

It's going to be tedious one way or another, I'm afraid. Even if you place it behind some generic *const () - the Rust's equivalent of C's void * - you'll have to cast it back to the original Spi<...> whenever/wherever you need to use it. Better safe than sorry: just keep it as is.

        // in mytypes.rs
        type MyUartPin<P> =
            rp2040_hal::gpio::Pin<P, rp2040_hal::gpio::FunctionUart, rp2040_hal::gpio::PullDown>;
        type MyUart<U, PTX, PRX> = UartPeripheral<rp2040_hal::uart::Enabled, U, (PTX, PRX)>;

        type MyUart0Base<PTX, PRX> =
            MyUart<rp2040_hal::uart::Enabled, rp2040_pac::UART0, (PTX, PRX)>;
        type MyUart1Base<PTX, PRX> =
            MyUart<rp2040_hal::uart::Enabled, rp2040_pac::UART1, (PTX, PRX)>;
        // ... same for I2C, SPI, and any other peripheral

        // in pinout.rs
        type MyUart0TxPin = MyUartPin<rp2040_hal::gpio::bank0::Gpio0>;
        let pin_uart0_tx = pins.gpio0;

        type MyUart0RxPin = MyUartPin<rp2040_hal::gpio::bank0::Gpio1>;
        let pin_uart0_rx = pins.gpio1;

        type MyUart1TxPin = MyUartPin<rp2040_hal::gpio::bank0::Gpio4>;
        let pin_uart1_tx = pins.gpio4;

        type MyUart1RxPin = MyUartPin<rp2040_hal::gpio::bank0::Gpio5>;
        let pin_uart1_rx = pins.gpio5;

        // in main.rs and any other module
        type MyUart0 = MyUart0Base<MyUart0TxPin, MyUart0RxPin>;
        type MyUart1 = MyUart1Base<MyUart1TxPin, MyUart1RxPin>;

On other plateform no_stdsupport can be gain by using thespin_loop feature. NB that lock strategies based on spin loop are not system-fair and cause entire system slow-down.

Me too. But I'm thankful to both of you ( @jumpnbrownweasel ) for sparring with me. You gave me a couple of good workarounds anyway. And hopefully with practice I'll get some better solution in time.

One note about the "spin_loop" feature: some mcus (ex: rp2040) have hw spinlocks; by rehauling the static_init to take advantage of the underlying hw the end result might be "fast enough".
But I don't have time to properly address this issue now. I add a note in the (very long) "Optimize" section of my TODO file, and use the type alias trick: it's ugly but it works, it's simple, and it's fast to implement.

Thanks folks! Appreciated.

EDIT: the type aliasing helps to ease changing the pin number. The type issue instead is fixed in this rtic example. Basically it uses core::mem::MaybeUninit in order to initialize the raw device in init() and then make it available in the Shared/Local resources (ie: available to other tasks).

2 Likes

Yeah, that comment is not very reassuring. But apparently you do need some sort of spin lock to get safe globals with no_std. Another example:

1 Like

Just in case you're not aware, once_cell allows you to supply your own mutex:
https://docs.rs/once_cell/latest/once_cell/#faq

Can I bring my own mutex?

There is generic_once_cell to allow just that.

Based on comments around, it looks like once_cell is the next lazy_static. But they are tedious in another new way to be tedious.
To tell you the truth, I'm kind of not insisting much with those "static helpers". It always ends up in unsafer code. I mean, I try those and even when they end up compiling ... well ... looking at the solution, it always look a bit cranky.

This is the second time I approach Rust. Last time was about 1 year ago, and I went "against Rust" (ie: unsafe blocks everywhere; an humongous amount of code to do simple things). After a month of pain, it worked but (1) I had to drop the AVR platform, (2) the end result was more Roast'ed than Rust'ed. It's ok, it was just a learning experience. This time I want to stay "safe" and try to cook something a bit more Rusty. Hopefully within a few years I'll be able to write some decent code in Rust too.

Thanks again!

2 Likes

Yeah, I've seen that yesterday. But I was speaking about rehauling static_init for adding a rp2040 feature in order to get the hw spinlocks at work.
Is static_init based on once_cell ?

Nope, sorry.

If you are willing to carry the pin information at runtime (the pin will no longer be a zero sized type) some HALs (such as the ESP32 HAL) provide type erased AnyPin or similar.

Some other HALs provide this as traits instead, so you could perhaps use a dyn OutputPin or such instead, but that means dynamic dispatch.

That might be an option for you. Or perhaps that is too expensive.

Another option I found myself doing a fair bit in embedded is to define central type aliases. That means I only need to update the IDs for the pins in one location. I find that okay, but even that seems to annoy you.

Wait, don't get me wrong: I'm annoyed for not being proficient enough even for ... simple problems as I don't have 'my tools' (void *). But it's just a matter of getting used to it.

I'm currently working on the thing you defined "central type aliases" (see the code in the solution-tagged post). At the end of the day, yes, one change to the line like

pub type MyUart0TxPin = MyUartPin<rp2040_hal::gpio::bank0::Gpio0>;

and the pin is changed in the whole program. I had to find a solution to define a pinout somehow so I'm doing it and at the same time solving my problem with composite types embedding pins and other pins' attributes.

The problem I have at this very moment is the uart is defined in rtic's init() function, but I can't store it in Shared as rp2040_hal::uart::UartPeripheral doesn't implement Copy. Nor by reference as the uart object dies at the end of init() (ie: in an unsafe world I'd end up with a pointer to garbage) so it can't have a 'static lifetime. EDIT: It looks like the last missing bit is in this rtic example. It uses core::mem::MaybeUninit to make the hal raw device survive init() (note: i2c in the example; but the same applies to Uart, SPI and the other peripherals probably).

I have not used rtic, only embassy, so I'm afraid I can't help much with this. Perhaps ask in whatever forum that rtic uses? I know embassy, esp-rs etc all use Matrix, perhaps there is a matrix channel for RTIC too?

This builds. Work? No idea (yet).

In mytypes.rs

use rp_pico::hal;
use rp_pico::hal::{
gpio::{FunctionI2c, FunctionSpi, FunctionUart, Pin, PullDown, PullUp},
i2c::I2C,
pac,
spi::Spi,
uart::UartPeripheral,
};

// --- UARTs --- //

pub type MyUartPin<P> = Pin<P, FunctionUart, PullDown>;
type MyUart<U, PTX, PRX> = UartPeripheral<hal::uart::Enabled, U, (PTX, PRX)>;
pub type MyUart0Base<PTX, PRX> = MyUart<pac::UART0, PTX, PRX>;
pub type MyUart1Base<PTX, PRX> = MyUart<pac::UART1, PTX, PRX>;

// --- I2C --- //

pub type MyI2CPin<P> = Pin<P, FunctionI2c, PullUp>;
type MyI2C<I, PSDA, PSCL> = I2C<I, (PSDA, PSCL)>;
pub type MyI2C0Base<PSDA, PSCL> = MyI2C<pac::I2C0, PSDA, PSCL>;
pub type MyI2C1Base<PSDA, PSCL> = MyI2C<pac::I2C1, PSDA, PSCL>;

// --- SPI --- //

pub type MySPIPin<P> = Pin<P, FunctionSpi, PullDown>;
type MySPI2<S, SCK, MOSI> = Spi<hal::spi::Enabled, S, (MOSI, SCK)>;
type MySPI3<S, MISO, SCK, MOSI> = Spi<hal::spi::Enabled, S, (MOSI, MISO, SCK)>;
type MySPI4<S, MISO, CS, SCK, MOSI> = Spi<hal::spi::Enabled, S, (MISO, CS, SCK, MOSI)>;
pub type MySPI0Base2<SCK, MOSI> = MySPI2<pac::SPI0, SCK, MOSI>;
pub type MySPI1Base2<SCK, MOSI> = MySPI2<pac::SPI1, SCK, MOSI>;
pub type MySPI0Base3<MISO, SCK, MOSI> = MySPI3<pac::SPI0, MISO, SCK, MOSI>;
pub type MySPI1Base3<MISO, SCK, MOSI> = MySPI3<pac::SPI1, MISO, SCK, MOSI>;
pub type MySPI0Base4<MISO, CS, SCK, MOSI> = MySPI4<pac::SPI0, MISO, CS, SCK, MOSI>;
pub type MySPI1Base4<MISO, CS, SCK, MOSI> = MySPI4<pac::SPI1, MISO, CS, SCK, MOSI>;

In pinout.rs

pub type MyUart0TxPin = MyUartPin<rp2040_hal::gpio::bank0::Gpio0>;
pub type MyUart0RxPin = MyUartPin<rp2040_hal::gpio::bank0::Gpio1>;
pub type MyI2C1SdaPin = MyI2CPin<rp2040_hal::gpio::bank0::Gpio2>;
pub type MyI2C1SclPin = MyI2CPin<rp2040_hal::gpio::bank0::Gpio3>;
pub type MyI2C0SdaPin = MyI2CPin<rp2040_hal::gpio::bank0::Gpio4>;
pub type MyI2C0SclPin = MyI2CPin<rp2040_hal::gpio::bank0::Gpio5>;
pub type MySPI1SckPin = MySPIPin<rp2040_hal::gpio::bank0::Gpio10>;
pub type MySPI1MosiPin = MySPIPin<rp2040_hal::gpio::bank0::Gpio11>;
pub type MySPI1MisoPin = MySPIPin<rp2040_hal::gpio::bank0::Gpio12>;
pub type MySPI0SckPin = MySPIPin<rp2040_hal::gpio::bank0::Gpio18>;
pub type MySPI0MosiPin = MySPIPin<rp2040_hal::gpio::bank0::Gpio19>;
pub type MySPI0MisoPin = MySPIPin<rp2040_hal::gpio::bank0::Gpio16>;

In commchan.rs

type MyCommChanMaker<D, P> = CommChan<D, P>;
pub type MyCommChan<D> = MyCommChanMaker<D, CommChanProtocol<i32>>;

RTIC's Shared resources struct in RTIC's app module:

    #[shared]
    struct Shared {
        status: crate::AppStatus,
        coresbuf: crate::buffer::Buf32,
        uart0: &'static mut MyCommChan<MyUart0Hal>,
        //uart1: &'static mut MyCommChan<MyUart1Hal, CommChanProtocol<i32>>,
        i2c0: &'static mut MyCommChan<MyI2C0Hal>,
        i2c1: &'static mut MyCommChan<MyI2C1Hal>,
        spi0: &'static mut MyCommChan<MySPI0Hal>,
        spi1: &'static mut MyCommChan<MySPI1Hal>,
    }

RTIC's init() in main.rs:

    #[init(local=[uart0_ctx: MaybeUninit<MyCommChan<MyUart0Hal>> = MaybeUninit::uninit(),/* uart1_ctx: MaybeUninit<MyCommChan<MyUart1Hal>> = MaybeUninit::uninit(),*/ i2c0_ctx: MaybeUninit<MyCommChan<MyI2C0Hal>> = MaybeUninit::uninit(), i2c1_ctx: MaybeUninit<MyCommChan<MyI2C1Hal>> = MaybeUninit::uninit(), spi0_ctx: MaybeUninit<MyCommChan<MySPI0Hal>> = MaybeUninit::uninit(), spi1_ctx: MaybeUninit<MyCommChan<MySPI1Hal>> = MaybeUninit::uninit()])]
    fn init0(mut ctx: init0::Context) -> (Shared, Local) {
        [...]
        let pin_uart0_tx = pins.gpio0;
        let pin_uart0_rx = pins.gpio1;
        [ ... it continues for all pins of all periferals: I2C0 ... SPI0...]

        [...]

        let uart0hal: MyUart0Hal = UartPeripheral::new(
            ctx.device.UART0,
            (pin_uart0_tx.into_function(), pin_uart0_rx.into_function()),
            &mut ctx.device.RESETS,
        )
        .enable(
            UartConfig::new(115200.Hz(), DataBits::Eight, None, StopBits::One),
            clocks.peripheral_clock.freq(),
        )
        .unwrap();
        let uart0cc: &'static mut MyCommChan<MyUart0Hal> =
            ctx.local.uart0_ctx.write(MyCommChan::new(
                CommChanDevice::setup(CommChanDevice::CommChanDeviceUart(uart0hal)),
                CommChanBuffer::new(),
                CommChanProtocol::CommChanPlainProtocol(0),
            ));
         [ ... similarly for I2C, SPI ...]

        [...]

        // return shared and local resources
        (
            Shared {
                status: crate::AppStatus {
                    tick: Mono::now().ticks() as u32,
                    delay: 0,
                    gpio_levels_now: 0,
                    gpio_levels_last: 0,
                    gpio_trigger_rt_on_raise: 0,
                    gpio_trigger_rt_on_fall: 0,
                    gpio_trigger_async_on_raise: 0,
                    gpio_trigger_async_on_fall: 0,
                    gpio_trigger_async_remote_on_raise: 0,
                    gpio_trigger_async_remote_on_fall: 0,
                },
                coresbuf: siofifobuf,
                uart0: uart0cc,
                //uart1: uart1cc,
                i2c0: i2c0cc,
                i2c1: i2c1cc,
                spi0: spi0cc,
                spi1: spi1cc,
            },
            Local {},
        )
    }

So, at the end of the day both the buffer's producer in the ISR and the buffer consumer task can access the peripheral:

    #[task(binds = UART0_IRQ, shared = [status, coresbuf, uart0])]
    fn _isr_irq20_uart0(_ctx: _isr_irq20_uart0::Context) {
        [... gets from uart's hw fifo and push it to the buffer ...]
    }

    // external (peripherals: uart, i2c, spi) rx dispatcher
    #[task(priority = 3, shared = [status, uart0,/* uart1,*/ i2c0, i2c1, spi0, spi1])]
    async fn t2(_ctx: t2::Context) {
        loop {
            [... it consumes peripherals's soft buffers ...]
            Mono::delay(0.micros()).await;
        }
    }

I simplified a bit the code posted here (ie: removed all the initialization, inter-task communication, multicore and inter-core communication), but it's complete in terms of accessing the 'static peripherals (and my own add-ons; CommChan = device+buffer+protocol) it's beautified by the type aliasing, and I've been a good boy: I didn't type "unsafe" keyword; not even once. All without the evil macros nor adding crates!

2 Likes