Embedded system design

There isn't a whole lot of information on the internet around how to architect an embedded system, especially in Rust, so I was wondering:

How would you go about structuring an embedded application in Rust? For example, the firmware for a 3D printer.

Embedded systems tend to be quite different from a normal desktop or server-based application in that you've got tight resource and timing constraints, the application must be able to run continuously without fail for long periods, and all possible outcomes must be handled (when was the last time your washing machine crashed?). These different constraints mean you'll need to design things differently.

(For context, at $JOB we make CNC machines and I've spent a fair amount of time working on the guts of our motion controller, an embedded C program running on an STM32)

Start with the Rust Embedded Book: https://rust-embedded.github.io/book/

That uses the STM32 F3 dev board as it's example but I'm sure its easily adaptable to others.
.

I would recommend starting with a state machine. The general embedded / bare metal program start with some intitilazation and hardware setup and then goes into an idle state waiting for events to occure. You can model events as transitions and the reactions as states. When the state maschine is finished you can start to map the functions to the system's resources, e.g. trigger an ADC in interrupt of timer0. This way you can see in which case which resources are used and where problems could occure.

For myself I have no experience in writting bare metal Rust (yet) but I think the state machine is generally a good point to start.

4 Likes

That's roughly how we've done it in the past. You'll model each "system" in the application as a state machine which is polled from an infinite loop, looking something like this:

trait System<In, Out> {
    fn poll(&mut self, inputs: &In, outputs: &mut Out);
}

Where In will contain the state of the world you care about, and Out is used to do things (e.g. output.set_output_pin(5, true)).

I guess you could say I'm wondering how people would structure the application at the high-level. The embedded book is really good if you want to learn how to get started and learn how particular things are done (e.g. how do interrupts work?), but the closest it gets to showing possible architectures is their Portability page.

It's kinda like when people have finished reading TRPL. You know all these different features of the language, but to take the next step it's useful to see how other people have implemented real-world projects.

Has anyone come across blog posts where people analyse the code behind their real-world embedded projects?

IME Rust on STM32 works but is not as mature as C yet, far from it; for example you won't be able to easily set all the clocks and other registers as you can with ST's tools suite, and don't have the libs to abstract the models differences.
If you're not dependant on power management it may work well though.

Can you clarify more or link to something that explains those limitations? If Rust can do register access what can't it do to completely configure certain features of a micro?

I think @bestouff is referring to the STM HAL and STM32CubeMX and the ecosystem based on them.

While they fill the same role as our embedded_hal and the various svd2rust crates (and arguably the Rust crates are better designed :stuck_out_tongue_winking_eye:), there's still a much bigger C ecosystem around the STM32. I'd say the vast majority of STM32-based products on the market are written in C or C++, and industry tends to have a lot of inertia due to its risk-averse nature ("if it's not broke, don't fix it").

In theory you're completely correct. Both languages/ecosystems are turing complete and you have full reign to do anything with the hardware... However, that still requires someone taking time to build up the nice libraries which make development easier and then thoroughly test them across all supported devices.

2 Likes

I'm a rank noobie to Rust and feel like I may remain a noobie for a long time, there is much to assimilate. But I have done a lot of work on all manner of embedded systems over the years, from process control in assembler to avionics to military communications to the humble traffic light. So here are some random thoughts around the question.

Firstly "embedded system" is a very wide ranging term, encompassing 8 bit micros running around in simple loops to full up Linux running systems. We should perhaps specify further, real-time system, safety critical system, bare metal system, just "small" systems.

My experiments with Rust so far show that it is quite capable of producing code with the performance and small size of C. It can be used without any complex run time. I see little standing in the way of doing whatever can be done in C in Rust. If there are such things one can drop down to C easily, as one can drop down to assembler from C if one has to. Already we have Rust targeted at ARM and RISC V. All this and we get a lot of the safety of Ada, arguably more in some respects. This has inspired great enthusiasm for Rust in me and I will certainly be evaluating it further in the coming months.

I did not like to assume anything about your embedded systems experience and as noted above "embedded" is a huge field. There are as many ways to architect an embedded system properly as there are embedded system problems. As I said, from a simple loop, to state machines driven by timer ticks or some external interrupt, to those using some kind of scheduler, preemptive or cooperative. To those using an RTOS or even Linux.

I suspect this is why it's not so easy to find advice on embedded/real-time/control system architecture techniques. You can find whole books on the subject though.

Now, having said that I think whatever can be done in C can be done in Rust, the issue that I have yet to resolve in my mind is how would adopting Rust upset my usual approaches to tackling such embedded problems. Would all that borrow checking and such cause me a great deal of grief? Will I be able to adapt to what is required by Rust easily or is it too much effort? So far I'm optimistically assuming that would not be such a hurdle.

I'm glossing over the issue of the current state of software support for any particular devices, in terms of HALs and such. It's early days and that will come. Typically I don't use any anyway. As long as we have the chip data sheets and a cross compiler we are good to go.

1 Like

Oh, I forgot. As a practical matter I have yet to see how far one can get with Rust without the std library. I read that one can get a lot of functionality with just the core library. What exactly? How big is it? What if I cannot use that?

Thanks. You touched on what I figured you meant by limitations, mainly existing infrastructure and inertia. That I totally get and understood. I thought maybe there was still some level of language limitation to the things you mentioned.

As someone who has worked on architectures with compilers that only a couple vendors support with badly reskinned gcc and with really high cost, I hope the day comes that rust can drive the embedded ecosystem.

2 Likes

Why do you say in the past? How do you do it now? What approach is considered modern? I'd really love to hear about this more as I myself will soon start asking the same questions.

I recommend reading what has become one of my favorite testimonials about learning Rust. I find particularly noteworthy this observation/question near the end:

1 Like

TomP,

Thanks for the link. I had seen it before but it was good to read again after my few weeks in Rust.

What if Rust is only the means to teach good programming, by forcing good style on its users?

I think that is spot on.

I'm old enough to remember when "structured programming" was introduced as a good idea to us young guys that had been programming in BASIC and assembler. "What you want to ban goto?. What you want data hiding and no globals?. How are we ever supposed to do anything with that? Performance will suck" being the kind of argument one would hear against structured programming.

Just now I find I have been having similar sounding conversations with die hard C/C++ users about Rust, they are resistant to the idea of all the checking it does, the ban on aliased mutable data. They will say "Good programmers don't need safety wheels, you just have to know what you are doing, all that checking will make code long, verbose and hard to write. It will cause performance to suck, etc, ect." They really don't want to even consider that unsafe languages lead to problems, that they personally may ever have inadvertently created such problems, and that there may be a better way.

It reminds me of how Douglas Crockford describes the old generation of assembler programmers putting up a fight against the new ideas of high level languages and compilers.

It reminds me of the Story of Mel. Where Mel says "What use is a programming language that can't modify it's own instructions"
http://www.catb.org/~esr/jargon/html/story-of-mel.html

3 Likes

I think it depends on which part of the application you're looking at. State machines are really nice for things like automation sequences and running jobs (e.g. the "print" part in a 3D printer), but there are other places where a formal state machine isn't the best tool for the job.

I'm currently writing a series of articles on how to develop a proper embedded program. The premise is writing a program which emulates some 3D printer firmware, so we'll go through things like sending data to the main application using interrupts, receiving jobs and doing the motion planning for them, communication with the user over a "serial port", etc. There isn't much content yet but the articles are on an S3 bucket if you want to read through. Feel free to create issues on GitHub if you have suggestions or questions.

Precisely all of this, yes.