Hi all, relatively new Rust user here. I am an embedded C developer evaluating Rust for use at my company. We mainly specialise MCUs (STM32, RP2040, ESP32, etc…) and specifically bare-metal firmware but also some RTOS occasionally.
So far I have been impressed by Rust with the quality of its code output as well as its safety guarantees – though I’m sure I don’t need to espouse Rust’s strong points to this forum since I’m sure you’re all already aware!
However, where I am falling over is trying to build Rust projects of a non-trivial complexity.
The embedded rust book (Introduction - The Embedded Rust Book) is great at explaining how Rust’s safety guarantees are readily applicable to embedded projects, however specific advice on how to organise large projects is lacking. This goes for a lot of other resources out there which are focussed on the hobbyist community and doesn’t go much beyond blinking LEDs.
So many example projects / tutorials out there just have all the firmware in one massive main file – which is not really tenable for a serious project.
For example, during initialisation of the program we create singleton objects to represent the various peripherals (GPIO, UART, etc…). We then call methods on these objects to interact with the peripherals. This is clean and I like this – however it falls apart as soon as you try to have code not in one massive main function.
In C I would create a GPIO module which would handle initialise the pins and provide wrapper functions for other modules to use. Any module which needed GPIO would then just include this GPIO module. My instinct is to pass the GPIO singleton into the GPIO module during initialisation, but this means it goes out of scope as soon as the GPIO initialisation function ends.
What I need therefore are static variables which can be initialised during runtime rather than at compile time, however Rust hates mutable static variables (which is understandable) and so this requires an awful lot of very ugly unsafe code (and having so much unsafe code kind of defeats the purpose of Rust’s safety guarantees in the first place).
Old threads on this topic seem to point to one of two places. OnceCell and LazyStatic. However OnceCell is now included in std, which means I can’t access it since I am running from a no_std context.
LazyStatic seems promising but for some reason I cannot get it to build in a no_std context for the life of me. There is a cargo feature “spin_no_std” to use spin lock mutexes and allow to build without std, however even with this the code fails to build complaining about not having access to std::sync::atomic. I think it worked in the past but seems to have since broken.
I have tried looking for example projects for how to do this sort of thing but have come up empty handed. This thread from a while ago had someone else with exactly the same question as me but it fizzled out without the issue really being solved.
So does anyone have any advice on how to organise a professional Rust project? I realise that I may be blinded by my experience in C and therefore could be failing to see the “Rust” way of doing this. Any help would be appreciated.