Rust on AVR: Safer microcontrollers almost here
dylanmckay.io
dylanmckay.io
Right now not a lot of that goes on, but that's changing -- and fast.
Which means every programming language we used to enjoy back in the day are possible to target such environment, assuming someone would bother to port them to it, that is.
Heap memory is still a thing for larger devices running on a RTOS, in addition to threads, mutexes, etc.
The code will run and corrupt data without crashing, due to off by one error.
In the proprocess a few people got cooked.
Although the code was in Assembly, the situation wouldn't have been different if it was coded in C, due to the lack of bounds checking.
Here's a picture of the model used for the Therac-25 the PDP-11/23 http://www.physics.purdue.edu/~jones105/pdp-11/images/IMG_28...
Your example makes it even more obvious that current microcontroller are actually quite powerful, when we look at the hardware resources of the 60 - 70's.
It can be hard to pin down when something switches from a fancy logic circuit to a microcontroller on the low end of the spectrum.
In my opinion, it stops being a microcontroller when the RAM is external to the CPU or it needs to be provided multiple voltage rails.
What useful thing did you conduct with it?
You store constants and your actual program code in Flash. The limitation of Flash memory is that you can only erase a whole page state time. On many of the smaller microcontrollers, one page may be larger than your SRAM!
So you treat Flash as read-only, writing it with a separate hardware programmer. You store state in the limited SRAM. You would also read or modify external state by loading or writing to special function registers outside of the Flash and RAM address space, where the "memory" mapped to that bit or word would do something in hardware. It would tell you whether a digital pin was on or off, or turn it on or off, or tell you the analog voltage on an input pin, or configure the pulse width of a pulse-width modulation timer, or read/write to a communication port, or set up a hardware interrupt timer.
Here's probably the most common example of this class: http://ww1.microchip.com/downloads/en/DeviceDoc/40001239F.pd...
They're often used to 'glue' two separate circuits together. For example, you could use it to measure an input, and in one mode output a high voltage if that input was on, or in another mode output a low voltage. In more physical examples, you could read the position of an analog input knob and control the pulse width of a TRIAC to control the speed of a motor. You could monitor the capacitance of a touch sensor (with an ATTiny) and turn a device on or to standby, or do a hard reset if the button is held down for a long time. You could modulate an RGB LED through a rainbow of colors. You could run a flashlight in a high power, low power, or strobe mode depending on how a button was pressed. And so on.
If it would have had RAM, one could have used it to implement a memory or to implement a 'normal' infix UI.
And no, it isn't cheating that it uses a CPU designed for calculators. At best, that keeps the ROM space needed down, but I doubt that, given its somewhat weird instruction set.
This was my experience back when I first did embedded stuff. These days unless you're programming something that costs less than a dollar to make, you've probably got at least double digit KBs of RAM and a C compiler. Not saying it's good practice to be using malloc() in this environment but you'll get away with it.
Actually, for the sake of keeping code modular and portable, it's fine to malloc() once on boot and never free(). It's the "dynamic" part of memory management that's dangerous on MCUs, especially on those without an MMU (as fragmentation gets in the way very often and you can't really reason about memory usage anymore).
Most of the AVR community tends to code in a mix of C and assembly, I think Rust is possibly not the best language to port, and type safety is not really an issue that's going to sell it.
To put it into perspective, Rust on a more powerful IC like an ESP32 or STMF32 makes sense to me, as you have a lot more to play with, but I've spent the best part of a year and a half working on a design constrained within 512 bytes of dynamic RAM, 512 bytes of SRAM and 512 bytes of EEPROM. I'm not sure what type safety buys me when I'm already trying to squeeze out every last bit.
All of this is not to suggest we shouldn't try new things, of course we should. Hacking things previously thought impossible is a longstanding and welcome tradition. However, I'm struggling to see how this would increase adoption of Rust, or make things significantly easier for AVR users vs say Arduino scratch.
Real-life, but somewhat extreme metric: in my last project, there were two instances of dynamic memory allocation, in about 100k lines of code, and both were in third-party code (we could have replaced them but you know, not broken, nothing to fix). At the other end of the spectrum, I think we had about one dynamic memory allocation at every 6-700 lines of code, but almost all of them were confined to a single scope, and they were largely due to code smell. (Edit: I think at one time we got really pissed when we saw how out of hand it had gotten and we went back and replaced most of these occurences, but I don't remember the metrics.)
In any case, lifetime management at the lexical level should not be a problem in most embedded applications, because there should not be much lifetime to manage. By "lexical level", I mean "things that are exposed to the language and manipulated through its primary means" -- variables, functions and so on. (Edit: there are legitimate exceptions to "should not be much lifetime to manage", but they are very few; the non-legitimate ones are in code of such poor quality that Rust won't help: what those codebases need isn't a new language but different managers, different programmers, or both.)
Unfortunately, this retains a whole class of errors that, to my knowledge, Rust (or any other language, frankly) can't manage. Off-by-one errors, for instance, can be handled if they occur at the end of the buffer (because you'd be spilling out of the container) but there's no way to handle off-by-one errors inside a buffer (you meant to say "read from the beginning of the buffer up to the last character that's been read from the serial line" but you actually ended up including one more character because you wrote off + 1 instead of off).
It's still up to the programmer to manage these things, and sloppy programmers will still write sloppy code that does this sloppily.
However, and this is a very big and very important however, we're talking about a level where any kind of additional safety helps. Returning to the previous example, off-by-one access errors inside a statically-allocated buffer are generally easy to spot and get caught very quickly during QA (since ooh, let's look at the buffer is the first thing you say when you get a bug about repeatedly garbled data when reading data from the serial interface). Off-by-one access errors outside the buffer (accidentally zeroing all the TX buffer, oh, and the first byte of the structure next to it that's used by an entirely different module) are the icky ones that sometimes fly under the QA radar for months.
I don't think the safety gains are on the level that the Rust fandom expects, but no one in their right mind is in a position to not welcome them. No, Rust won't solve everything, and bad programmers will continue to productively shoot themselves in the foot in Rust, too, just as they can productively shoot themselves in the foot in pseudocode. But for people who realize that there's a pretty big chance their next bug is going to get somebody killed, anything that reduces the bug population is a welcome addition.
tl;dr In most embedded scenarios, this will help less than the Rust community wants to think, but enough that sane people welcome it!
It's probably not the best of examples, but the mistakes pop up the same way as for dynamically-allocated buffers -- you pick the wrong offset or count up to the wrong limit, and it doesn't make much of a difference if the buffer was statically allocated or not.
Rust is a surprisingly nice language on bare metal.
If you use '#![no_std]' in Rust, it disables the standard library and substitutes the 'core' library. This has no 'malloc' (unless you supply one, which is easy) and no OS-level features. But you still get iterators, slices, traits, "scoped" closures and generic types. You get a lot of abstraction with basically zero overhead. And don't forget that embedded systems have lot of state machines, which are nice to implement using Rust's "tagged union" support and exhaustive 'match' statement.
Here's an example of a Rust I/O port wrapper (https://github.com/emk/toyos-rs/tree/master/crates/cpuio) and an interrupt controller interface (https://github.com/emk/toyos-rs/tree/master/crates/pic8259_s...). These APIs are reasonably pleasant for something so close to the metal.
Rust would be a very agreeable programming language on an AVR system, IMO. And not necessarily because of pointer safety, either.
I tear my hair out every time I have to wrestle with C for the simplest things, though, whereas Rust is a joy to write.
(My mental model was that once you add a backend to Llvm, every programming language that uses llvm should magically work with it.) Does LLVM's design fail to capture the full problem space? Are there out of band concerns that do not lend themselves to be generally expressed?
> Rust's version of LLVM has a few patches on top of stock LLVM, and it also tended to have extra commits cherry-picked on top.
Apart from that, Rust needs very little per-architecture glue code. And someone needs to run the Rust testsuite on the resulting version of Rust and make sure it passes.
Additionally, it is useful for the front-end to understand things like the layout of types, and the standard library usually also needs changes to paper over any platform differences (although I imagine AVR doesn't need many, mostly in the compiler-support library compiler-rt, given not much of the standard library will work).
Some of the things that needed to be implemented in the frontend was the ABI, unwind support, C bindings for the new architecture. Some of these don't apply as much to rust but you can see that LLVM doesn't do a perfect job. The diff is still quite small however.
In the AVR case, on the biggest chips a RAM address takes 2 bytes while a flash address is 3 bytes, and on the smallest a RAM address is 1 byte while a flash address is 2, and of course the values overlap (flash-address-0xFE points to different memory than RAM-address-0xFE), and the indirection code is different. So how big is a plain 'char *', and how do you dereference it? On a small AVR you don't want to be wasting precious memory storing and copying 2 bytes when 1 will do, and you sure don't want a run-time test on every dereference — if you had the cost/time/battery for that you wouldn't have picked² a small AVR in the first place. So you need to be able to tell the compiler what kind of memory a pointer points to.
The big free toolchains (gcc/llvm) don't pay much attention to non-mainstream processors, which is why a handful of commercial compilers still exist.
I suppose (not being a Rust expert) that a Rust compiler could figure out that a non-mut 'static can go in flash, and track address spaces, and auto-genericize functions that take a reference… but I'll bet it doesn't, yet.
¹ http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1169.pdf
² no pun intended originally, but I'm leaving it and gratuitously calling attention to it.
Just enough to get keyboards working, still a lot of stuff to do, but the foundation is there.
Used like this: https://github.com/intermezzOS/kernel/blob/master/src/main.r...
Note it wont affect me as I mainly use PIC micros
If your C programs are correct, there is no safety problem.
I can't speak to Ada.
That doesn't mean that Rust has no safety properties. It just means you have a TCB of nonzero size.
That's also true with any memory-safe language ever: you have to trust the compiler and VM. So if we accept your claim, then we also have to accept the claim that no memory-safe language has any useful safety properties. Needless to say, this is contrary to all the evidence.
> The reality is that one of the core selling points of the language was discovered to be unsafe due to some particularly complex combination of library features right before the 1.0 release.
Consider the chain of events that would have to happen to cause this unsoundness to lead to real-world problems (say, RCE), and compare that to the chain of events that routinely happen in order to cause a use-after-free in C++ to lead to the same problems. One is vastly more probable than the other.
Although Ada has eventually got an open source compiler, and there are still around 5 vendors available, due to the way it got sold, its key use has been in the area of High Integrity Computing.
So unless that is the domain of the work, where no errors are allowed, which could possibly lead to loss of human lives, very few care of using Ada.
For the young generations, Ada is kind of something they hear it existed but they never seen live, even though it has enjoyed a regular presence at European FOSS conferences like FOSDEM.
So by being modern, Rust can appeal more to the younger generations, also some type system safety rules of Rust for parallel code can only be coded in Ada via SPARK.
Having only recently looked at Ada I don't really understand this. It does look like Ada offers a lot of really nice features that other languages haven't yet caught up to.
For example, SPARK allows formal definition of correct code behaviour within the function definition. This appears to me to mean tests are written into the function at the time you write the function. That's huge from a maintenance standpoint, and it likely provides extra information a compiler could take advantage of.
> where no errors are allowed, which could possibly lead to loss of human lives, very few care of using Ada.
Loss of life is one area, but surely anything financial would benefit from this - as well as anything dealing with personal data (e.g. identity management)?
I don't think that's true. I'm writing an OpenGL library right now, diving into "unsafe" all the time to issue the FFI calls, and there's a measurable difference in the number of memory safety problems I've seen (zero thus far) compared to what I see in C++.
I think the friction of writing "unsafe" encourages small isolated abstractions.
To be concrete: libpng, libjpeg, FreeType, etc. are not written by "morons". Can you write a JPEG decoder faster than libjpeg-turbo?
Sadly in more than 30 years of career I never met one. Not even the BSD and Linux kernel devs, given the amount of entries on the CVE database.
Even Dennis complained about this, regarding the genesis of lint.
So I wonder where do they exist.
I hope that Rust unsafe/safe concept makes the platform/applogic boundary more visible and explicit, so people follow these practices more often.
As for better, that is in the eye of the beholder. I've never used embedded ADA, but I've slung my fair share of ADA code in higher level applications. For me, for reasons I couldn't describe, the syntaxes and grammars of C-like languages (C++, Rust) just feel more comfortable. ADA is a fine language, though, and a real pleasure to work with, too (it reminded me a lot of Pascal, which is what I learned in school).
Here's something else to consider, however: Rust (core rust, even without the stdlib) provides a fantastic set of built-in abstractions that would require lots of up-front development effort if one used C.
If you want to replace C you either need to come with a better "portable assembly" or have a significant change in the hardware architecture that requires the change.
BTW this constant shoving of Rust down the embedded world's throat doesn't seem to be working.
I'd say: 'works okay'
One thing that annoyed me was calling a variadic function like printf was expensive ~100-120 bytes each call. Eight to Ten printf's --> 1k of flash gone. Far as I could tell most of it was pushing and popping stuff on the stack one byte at a time.
I ported a bunch of AVR code to an ARM Cortex[1]. Code size didn't increase much.
[1] ARM Processors are approximately half the cost of an AVR. Seriously not important if you're building 1000's of something. Important if you're building 100,000s.
Then again, stack on 8051 might be sometimes pretty small. From 10 to 128+ bytes.
More options is certainly even better.