Writing embedded firmware using Rust
anyleaf.org
anyleaf.org
Do you have any evidence for this? From what I’ve seen, the Rust community has invested considerably in creating learning resources and so forth to make getting started easier. However there’s only a finite amount of time/people working on it, so rough edges remain for less common use cases
It's definitely a language you can't learn overnight so don't overdo it - it takes time to wrap your head around many of the concepts. While I admit I am a fanboy, I also openly admit that it's not suitable for every purpose. Most certainly not for web development - yes it is fast, yes actix can boost some astonishing benchmarks but truth is it hardly pays off to put in so much effort, especially when you are working on a schedule. It's a question of trade-offs. People address many async-await logical blocks by slapping Arc<RwLock<T>> or Arc<Mutex<T>> everywhere and a ton of read and write locks to get around it. Which if perfectly acceptable if you don't overdo it. Otherwise you will have to spend endless hours trying to catch some lock somewhere which is biting your head. Hence the reason why I'd really avoid it for large and complex web services for instance. There are other areas where it's perfect. Three personal projects I'm working on(which I'll hopefully get to open source over the next 6-8 months) and where I don't regret picking rust:
* Raspberry-pi powered CNC router(though dealing with gcode is a huge pain in the ass).
* Distributed full-text search engine(similar to elasticsearch).
* Automated image pulling from NOAA satellites with an SDR dongle(though in this particular instance I'm taking a shortcut by piggybacking on librtlsdr).
As far as embedded rust. First is what I mentioned earlier - setting up a development environment is an absolute hell, and far not only the first time. You have to go through the same steps each time. At this point I'm somewhat used to it and I can unpack an embedded board and get it going in say 20-30 minutes(having the spec sheet at bay is a must). It also takes an additional layer of tweaking your brain: it takes some time to get used to things such as #![deny(unsafe_code)], #![no_main], #![no_std] and #[entry]. It's nothing like arduinos if you've worked with those, where you plug in the board, open the arduino IDE, select the board model and port and hit "upload".
Look at some of the cases in the article, such as:
let mut dp = pac::Peripherals::take().unwrap();
dp.RCC.ahb2enr.modify(|_, w| w.gpioaen().set_bit());
In C that would be: RCC->AHBENR2 |= RCC_AHBENR2_GPIOAEN;
Okay, you only have to take ownership of the Peripherals object once, but what about when they want to set a UART register in a function? Rust wants mutable pointers to have a trail of ownership outside of 'unsafe' blocks, so now we need: pub fn setup_transmission(regs: &mut R, [...])
where R: Deref<Target = pac::usart1::RegisterBlock> {
[...]
regs.cr2.modify(|_, w| w.stop().bits(config.stop_bits as u8));
[...]
}
This is a bit of a construction, in C(++) we can just do something like: void setup_transmission([...]) {
[...]
USART1->CR2 &= ~(USART_CR2_STOP_BITS);
USART1->CR2 |= ((uint8_t)config.stop_bits << USART_CR2_STOP_BITS_Pos);
[...]
}
The Rust snippet seems like it would be safer in a multi-threaded environment like an RTOS, or in cases where interrupts might modify peripheral registers, but...there are so many syntactical details to remember.I really tried to like embedded Rust, and I still think it compares favorably with the sort of C HALs that most chip manufacturers distribute. But the syntax seems to get in the way of quick simple access to peripheral registers.
As a long time C programmer who occasionally makes mistakes, I find the type safety of svd2rust-generated APIs incredibly gratifying. When the embedded documentation is inscrutable, the type-safe Rust API prevents you from making whole classes of nasty errors. Especially since the embedded device will probably either fail silently or misbehave in bizarre ways if you mess up.
There may be more elegant ways to design the type-safe API but even having to deal with the verbose syntax it was way easier than stumbling around in C.
The goal is exactly right: leverage Rust's expressive type system to keep your programs safely composable -- it's a drag trying to debug an issue caused by allocating one peripheral to two different systems. Particularly since the conflicts between peripherals on different models of the same family are sometimes hard to recognize.
struct Uart {
u8 CR2,
// etc.
}
Instantiate it as 'static with some macro that defines where it lives in memory: #[mcu::loc(0x1002010)] static UART1 = Uart::new();
#[mcu::loc(0x1003010)] static UART2 = Uart::new();
And then have it bound directly to the memory locations on the MCU at compile time/link time.Still learning rust, so apologies if this syntax doesn't make sense.
I think the 'peripheral access crates' try to auto-generate those sorts of struct objects from SVD files, but you still need to obey Rust's ownership rules to get the advantage of its built-in memory safety.
So if a function needs to modify a peripheral's memory, it needs to claim mutable ownership of the relevant peripheral registers. That's a feature of the language which makes it 'safer', but the cost/benefit calculation is a bit different on embedded platforms. They have less memory, and static allocation is usually preferred.
Still, microcontrollers are getting faster quickly. You can even run Linux on some cortex-M4/M7 chips. The verbose syntax might be worthwhile if you're collaborating on complicated firmware.
It's more verbose for sure, but I prefer it for it's explicitness and ownership tracking. Definitely not perfect, but it's also very early times for Rust on embedded devices.
This is the lower end of embedded systems, but the vast majority of them and those don't have any concurrency. Didn't try Rust here, but I would assume you have to jump through some hoops to achieve simple register access or declaring whole blocks as unsafe.
All memory are statically "allocated" as global variables/buffers and never free'd, so there's no ownership or dangling references to worry about ever. This allows me to know at link time how much memory my program uses.m and of course simplify programming.
Only single cores, the only synchronization issue is between interrupts and main program, a case rust doesn't handle afaik(?) and in 99% of the cases a ring buffer or simple flag variable is enough.
Never use much abstractions, so C is fine in terms of ergonomics. Never use third party libraries aside from CMSIS register headers.
Probably the only thing that would help me is some static reflection and compile time programming, but I have python scripts as a code generation step to generate tables, definitions, annotations, etc, and that works fine.
I'm not sure what you're expecting it to handle that it isn't. That static flag or ring buffer needs synchronization and rusyc will absolutely not let you forget that (in safe code).
One technique you get in Rust that's very compelling for embedded is using traits, especially when implemented on zero-sized types, to cleanly express configuration. Personally I abhor all those one-off table generating scripts.
One thing that's killed me a bit is the core::/std:: split, when the core:: versions have the exact same semantics, or perhaps are equivalent for the things that they do implement (for example, the core version might be missing the serde parts, but is otherwise the same). There's a few crates I tried out that wouldn't compile with no_std, but with a simple text substitution, would work. I'm sure there's a million edge cases I'm not aware of, but if cargo or rustc could provide an option to just sub in the core version, that'd be really nice.
[0] https://www.youtube.com/watch?v=cypmufnPfLw
Embedded developers are very conservative, they don't chase the shiny and believe in using old tried and true technology. C++ offers significant benefits over C in the embedded space and its been impossible to gain traction.
With C you have a HAL provided by a vendor that allows very rapid bring up of the hardware which is 90% of the battle in embedded. Switching to Rust you would have to write your own.
With C you have billions of lines C libraries at your disposal to bring your product to market. To use Rust you would have to write your own or deal with the shims and lose the Rust benefits.
I do 4-5 embedded projects a year, using Rust and not having the HAL and C library ecosystem would put those projects ad a competitive disadvantage.
I find the whole hubris and humilty thing is ridiculous. Writing a RTOS in Rust which isn't your core competency is such a waste of resources. Use FreeRTOS and ship the product.
1. Unless your product is mass-produced, as long as your $work has >= 2 projects, having them share a over-spec'd MCU family (i.e. way more pins, MHz, SRAM, ... than you could imagine yourself needing), or even a single part, is very desirable. The BOM is much simpler, and given the current chip shortage situation, you don't have to track incompatible chip families. All the infra work, internal codebases, know-how, even quirks and their workarounds can be shared in-house.
2. The vendor most likely has some BSP (board support package) written in C with examples in C as well, and it most likely will support / assume / integrate with FreeRTOS. Neither of these will be optimized to the bones --- usually the situation is quite on the contrary, with O(n) loops liberally littered around. However, since you picked a overkill MCU, it will more likely than not gladly chug along, given you don't abuse them. When you FFI / shim / start from scratch with registers, usually the happy path does work after some hacking, but when it doesn't work, do you have confidence it's a HW issue, or is it rather your added indirection? With Embedded Rust's prevalent macro-heavy approach, this can be a real time sink.
3. Firmware engineering really works closer to hardware than you'd think. To avoid a catch-22 situation in the co-design, already knowing a lot about what embedded platform breaks the loop and effectively decouples HW and SW, reducing your time-to-market.
* HAL: (Hardware Application Layer) [1]
* RTOS: (Real time Operating System) [2,3]
[1] https://www.beningo.com/embedded-basics-apis-vs-hals/
[2] https://en.wikipedia.org/wiki/Real-time_operating_system
I've been using the Rust stm32 HALs offered by the open-source community and they are stellar. People are doing some really great work so people like myself can come along and knock together a highly performative bare metal application with relative ease. Obviously not all features are supported and some chips are more supported than others but its a work in progress.
Regarding library support, Rust has crates.io and a first class package manager with native support for heapless environments which means that I can easily add a package designed specifically for embedded development to my project. Its pretty awesome not having to mess around with a million linker flags, copy-pasting directories etc to try to get a library to work. Ontop of that with Rusts language features there are standardized traits for peripherals like I2C, SPI etc, meaning that any library can easily consume any HAL's peripheral implementation. Basically I can install an LCD library and have it running with 1 line of code.
Obviously there will be more C libraries out there and it will take time to port them over to Rust but the foundation is very very solid.
That's a rebuttal for your HAL and library criticisms and I still think there are vast safety and ergonomic improvements to using Rust over C which would undoubtedly lead to more reliable code and quicker development times.
Or are the benefits too difficult to quantify?
It seems like if something along the lines of "MISRA C" (but requiring Rust) were to become an industry best practice, we might see the Rust ecosystem develop.
However, automakers are some of the most conservative companies out there when it comes to tech, and I doubt they really care about safety or formal correctness as much as being able to slap a "MISRA" sticker on the code for liability reasons.
It would have to be, decades I imagine, before there's any chance of rust becoming truly widespread in the embedded world. Universities would need to start teaching with it. People who know C and nothing else would need to retire. I agree that it's unlikely to be commercially viable at a large scale any time soon (unless you can find a handful of true believers for your team).
There already is a solution for heavily safety-critical systems and it's got 40 years of industry track record it's called Ada. [0]
I don't mean to sound dismissive but there is such a treasure trouve of battle tested industry learned experience in the Ada standard, it really is worth learning and considering.[1]
Ada has had four standards in almost four decades. For anyone in industry this pace is adequate and all of those standards were truly value adding without any disruption to legacy systems. This is not compatible with the pace at which most Systems and Backend Rust enthusiasts want to move forward.
I don't believe the two end applications/motivations can be merged under a single language or ecosystem despite many similarities.
[0] https://www.adacore.com/industries
[1] http://www.ada-auth.org/standards/12rat/html/Rat12-TOC.html
Medical devices, maybe, but you still have the problem of finding talent. If you were to build a product on Rust, you would have trouble hiring people to maintain it over the lifecycle of the product.
Its hard enough hiring C and C++ developers.
And don't sleep on Sealed Rust. They've surely got a hard road ahead, but there are some dedicated folks working on it.
How do you figure that? That seems highly counterintuitive.
Wasn't it Arduino that tried to establish C++? I could never see the advantages to be honest, but I didn't really use too much C++.
Header files mainly provide semantic descriptors to otherwise pretty hard to memorize memory addresses. A format like SVD would provide the main benefit for other languages though. Even non-ARM-processor manufacturers use it.
I believe many embedded devs long for "high tech features" like unit tests, which of course would require some form of formal simulators and peripherals. That problem goes far beyond the choice of language though.
Validation is an extremely hard thing to do for embedded system outside of a specialized laboratory. Memory safety is almost laughingly trivial compared to that.
You don't have to write your own HAL when the community maintains really excellent ones, written by people who actually want to fucking use it rather than vendor employees who just have to get it out of the door.
> Embedded developers are very conservative, they don't chase the shiny and believe in using old tried and true technology.
This is underselling the point. They are conservative in the same sense as an electrical engineer who refuses to use ECAD software or transistors because drafting tables and vacuum tubes are tried and true. There's a difference between "[not] chas[ing] the shiny" and just being obstinate.
> With C you have a HAL provided by a vendor that allows very rapid bring up of the hardware which is 90% of the battle in embedded. Switching to Rust you would have to write your own.
I'm not sure why vendor supplied HALs - which can be pretty hit or miss in terms of quality - are so important for board bring-up. In the end it's a minor milestone in the development lifecycle for all but the simplest of projects. In the projects I work on practically all of the time is spent on the complexity of the firmware and how it interacts with the various networks of sensors, actuators, cellular, BLE, storage, etc. Besides, last I checked there were many people putting effort into HALs written in Rust.
> With C you have billions of lines C libraries at your disposal to bring your product to market. To use Rust you would have to write your own or deal with the shims and lose the Rust benefits.
First off it has not been my experience that libraries are used often. As you mention later FreeRTOS is generally the only external code I see pulled into a project, everything else is written from scratch or copy pasted from a previous project which was written from scratch or copy pasted from a previous previous project etc. Let's assume that libraries are commonly used though, how exactly do you use them? This is one point of C/C++ that is still absolutely dire. Rust has Cargo whereas C/C++ has a lot of wacky ways to deal with it such as through git submodules, or cmake, or possibly conan. Generally speaking what I see is the code being copy pasted into the project and never updated despite the growing security concerns for connected embedded devices. Of course Rust offers more than mere package management, Cargo is also the build system, it also has support for automated testing and documentation. All of these things are typically afterthoughts at best in many embedded projects.
> I do 4-5 embedded projects a year, using Rust and not having the HAL and C library ecosystem would put those projects ad a competitive disadvantage.
To be clear at this point in time if I were asked if an embedded project should be written in Rust my answer would be "probably not", although given 5-10 years that answer very well may change to the affirmative. The ecosystem is young and growing; as long as the growth continues it will likely be a viable alternative in the future.
Completely agree. I'm starting new embedded projects in Rust mostly because cargo test is a force multiplier. (well and also the expressive type system)
Can you elaborate?
Embedded operating systems are a solved problem, there are many available on the market used in mission critical systems.
At most companies that are trying to ship a product and have a mountain of work to be done, and someone said, "I know, lets build our own RTOS", even though there are already many perfectly fine ones out there, they would be laughed out of the room.
I also emphatically disagree that "embedded operating systems are a solved problem" (or indeed, that operating systems are a solved problem) -- and that in general such proclamations reveal much more about the technologist than the technology that they proclaim to be finished.
I do quite a bit of good old C on Atmel & STMicro chips M0+ chips. When I do, there’s a whole layer/framework of register definitions (structs, enumerated, typedefs, etc) C header files that I’m able to include for my specific chip and package.
If I want to experiment with Rust in place of C on one of these chips (say an Atmel SAMD21 J) what are my options? Do I have to reverse engineer all of those register mappings into Rust constructs? Can Rust import and use C header files?
* https://www.keil.com/pack/doc/CMSIS/SVD/html/index.html
** https://github.com/stm32-rs/stm32-rs
*** https://github.com/stm32-rs/stm32f4xx-hal for the stm32f4xx line
ARM seems to provide some XML register description files called SVDs[0]. They seem to do a decent job describing the registers and peripherals in a way that allows code generation tools to create a register access library, upon which higher level HALs can be built.
For an example, you might look at the esp-rs project on GitHub. The lowest level, if I recall correctly, is the Peripheral Access Crate [1]. The README for this project describes a bit about the SVD file, and svd2rs code generation tool. Sometimes the SVD file needs to be patched to produce high quality Rust bindings.
This PAC is then required as a dependency of the higher level HAL for the esp32 [2]. The higher level HAL also implements some traits from a shared Rust HAL effort [3].
I'm not really an expert in Rust so if I've described any of this wrong, someone please correct me.
[0] https://www.keil.com/pack/doc/CMSIS/SVD/html/index.html
[1] https://github.com/esp-rs/esp32
But for processors this small I currently could not tell you any advantages to be honest.
When / if then sure (assuming it would also be compatible with the megatons of very useful libraries written in C). I would have very little desire to replicate something like LUFA for example.
Yes, after piping them through rust-bindgen, any binary can be statically of dynamically linked. I did this, it's nice that it's possible, but the PAC route others have described here is way better.
While Rust cost per engineer today is higher, is the cost of the project lower, because due to Rust those engineers are much more efficient?
I would suggest that you try using the Arduino or RaspberryPI hardware. Both of these platforms are widely used, so you would be able to easily get help if you get stuck. Also, these are 'simple' platforms that are widely available, so the learning materials available is unmatched.
Building a simple project like a weather station or clock will keep you focused on a "actual" target/goal.
https://create.arduino.cc/projecthub/projects/tags/clock
http://cactus.io/projects/weather/arduino-weather-station
https://howchoo.com/pi/how-to-make-a-raspberry-pi-smart-alar...
https://projects.raspberrypi.org/en/projects/build-your-own-...