...err, yeah. If installing Blender on your own CUDA Docker instance was niche, then this is the gap between the atoms at the bottom of a crack in the niche. But, I decided that trying to get Rust cross-compiling for Arduino would be useful for me, so maybe it'll be useful for someone else.
The painfully named Arduino Nano Every[1] is a small embedded processor board based on the ATmega4809[2] 8-bit AVR[^1] CPU. It has a few things going for it - it's got decent memory for an AVR, runs at a healthy clockspeed, is packaged in a way that makes it really easy to embed on your own circuitboards (you can solder it directly to a PCB, no sockets/pins required,) and most of all it is *absurdly* cheap:
┌──────────────────────┬───────────────────────────────────────────────────────┐ │ Item │ Spec │ ╞══════════════════════╪═══════════════════════════════════════════════════════╡ │ Processor │ ATmega4809 │ ├──────────────────────┼───────────────────────────────────────────────────────┤ │ Clockspeed │ 20 MHz │ ├──────────────────────┼───────────────────────────────────────────────────────┤ │ Flash (code) memory │ 48 KB │ ├──────────────────────┼───────────────────────────────────────────────────────┤ │ Static (data) memory │ 6 KB │ ├──────────────────────┼───────────────────────────────────────────────────────┤ │ Connectivity │ UART, SPI, I2C, 8 analogue inputs, up to 20 digital │ │ │ IO │ ├──────────────────────┼───────────────────────────────────────────────────────┤ │ Cost │ *€9*. (Or €7.60/each in packs of 3) │ └──────────────────────┴───────────────────────────────────────────────────────┘
Specs like these make the Nano Every an absolutely ideal platform for various embedded system projects I have in mind, so naturally I'm quite excited about developing for it.
There is only one tiny problem; I like to make life difficult for myself, so obviously I'm not interested in using Arduino's own programming tools or language. To keep my mind just about working I'm forcing myself to code in Rust[3] (a language both endearingly beautiful and maddeningly frustrating in about equal measure), so naturally I want to program my AVR in Rust as well.
This is only part 1 because I haven't been *entirely* successful in doing what I want to do here. This first part will deal with compiling generic AVR code using Rust, and indeed deploying it to an Arduino Nano Every.
The second part will deal with compiling for the unique features of the ATmega4809[4]; this is somewhat more complex because of issues with the way the current Rust AVR HAL is built, and needs a little more work...
OK, so we need a few things to build the code for our little AVR:
┌───────────────────────┬──────────────────────────────────────────────────────┐ │ What │ Note │ ╞═══════════════════════╪══════════════════════════════════════════════════════╡ │ An AVR compiler │ We'll use the avr-gcc toolchain │ ├───────────────────────┼──────────────────────────────────────────────────────┤ │ A Rust project │ This also needs to be configured to use the AVR │ │ │ toolchain │ ├───────────────────────┼──────────────────────────────────────────────────────┤ │ │ The hardware abstraction layer; this defines the │ │ An ATmega4809 HAL │ Rust entities that map to the hardware provided by │ │ │ the microcontroller │ ├───────────────────────┼──────────────────────────────────────────────────────┤ │ An Arduino programmer │ That is, we need a way to get our compiled code onto │ │ │ our test Arduino Nano Every. │ └───────────────────────┴──────────────────────────────────────────────────────┘
I'll show you how to meet each of these requirements, on Mac OS. There are a couple of pre-requisites I won't go into details on though:
1. As mentioned there, this is for MacOS.
2. I assume you have already installed Rust and have some familiarity with the Rust toolchain (e.g. `cargo`).
3. I assume you have access to the Homebrew[5] package manager. If not, go to https://brew.sh[6] and get downloading - it's invaluable for any developer on MacOS.
So, with that said, let's get going!
This part is super easy; everything you need you can get using Homebrew, from the `osx-cross/avr` repository. We'll install the avr-gcc toolchain:
brew tap osx-cross/avr brew install avr-gcc brew install avr-gdb
We will create our project in the normal way using the `cargo` command. But, there is a little more to it than that. We also need to tell Rust to use the *nightly* version of the Rust toolchain - that's because at the moment AVR cross-compiler support is only enabled in the nightly builds. We do that with `rustup override` to set it just for this project:
cargo new --bin myproject cd myproject rustup override set nightly
That's not everything we need to do though. We now need to tell Rust to compile for the AVR; we do this by providing a *target specification[7]* file, which should be in the root of your project.
At the time of writing, building is only reliable for the ATmega328p
controller. We'll use this for our example at the moment therefore - it's
OK, because the binary we compile will be compatible with our ATmega4809,
but in Part Two I'll explain how to make things work for the 4809
specifically, so you can take advantage of all the 4809's features.
Just as soon as I work out how myself.
So, you will want to create a target specification file named `avr-atmega328p.json`, in the root directory of your Rust project, with the following contents:
{ "arch": "avr", "atomic-cas": false, "cpu": "atmega328p", "data-layout": "e-P1-p:16:8-i8:8-i16:8-i32:8-i64:8-f32:8-f64:8-n8-a:8", "eh-frame-header": false, "env": "", "exe-suffix": ".elf", "executables": true, "late-link-args": { "gcc": [ "-lgcc" ] }, "linker": "avr-gcc", "linker-flavor": "gcc", "linker-is-gnu": true, "llvm-target": "avr-unknown-unknown", "max-atomic-width": 8, "no-default-libraries": false, "os": "unknown", "pre-link-args": { "gcc": [ "-mmcu=atmega328p", "-Wl,--as-needed" ] }, "target-c-int-width": "16", "target-endian": "little", "target-pointer-width": "16", "vendor": "unknown" }
(Most of this is boilerplate that you can find in many places, not my invention.)
We're not quite done though; we also need to tell Rust to actually target this device when it compiles. You can do that on the command line when you `cargo build`, but neater is to put it in a Cargo configuration[8] file.
To do this, create a file named `.cargo/config.toml` in the root of your project that looks like this:
[build] target = "avr-atmega328p.json" [unstable] build-std = ["core"]
The first part of this file tells Rust to compile for your target as specified in the target specification file we created earlier. The second part enables an 'unstable' feature of Cargo; specifically, it tells it to not just rebuild our application, but to also build the core libraries at the same time (since of course we don't have any pre-built libraries for the AVR to link to.)
Now, we are ready to build with `cargo build`!
Before we do that though, we need something to build. And to be able to write something to build, we need a HAL.
I'm sorry Dave, I'm afraid I can't do that. Yet.
The HAL - Hardware Abstraction Layer - is the component, or rather library, that you can use to access the specific hardware features of your target. In the case of a microcontroller like the AVR, that usually means things like registers, ports, and flags that control the inbuilt hardware.
For example, you know that your device has an inbuilt UART, because you read it on the datasheet - but somehow your code also needs to know that it has it, and also where in memory the registers that control it live. This is all provided by the HAL library.
THere are a couple of variant HALs for Rust-on-AVR, but the simplest of them appears to be the `ruduino`[9] crate. You can use this by adding the following dependency to your `cargo.toml` file:
[dependencies] ruduino = "0.2.6"
When you do, you will magically get access to a crate which provides you with the abstraction you can use in your Rust code to access the hardware's features. So, for example, you can now write a `src/main.rs` file that looks something like this:
#![feature(llvm_asm)] #![no_std] #![no_main] use ruduino::cores::atmega328p as avr_core; use ruduino::Register; use avr_core::{DDRB, PORTB}; #[no_mangle] pub extern fn main() { // Set all PORTB pins up as outputs DDRB::set_mask_raw(0xFFu8); loop { // Set all pins on PORTB to high. PORTB::set_mask_raw(0xFF); small_delay(); // Set all pins on PORTB to low. PORTB::unset_mask_raw(0xFF); small_delay(); } } /// A small busy loop. fn small_delay() { for _ in 0..4000 { unsafe { llvm_asm!("" :::: "volatile") } }
Enter `cargo build`, and everything works perfectly! (This simple example code will just blink the LED on your Arduino on and off, once you load it onto the device. We'll get onto how to do that in a moment.)
You don't need to know this to be able to get going with compiling code for your Arduino, but I think it is interesting to look a little bit under the bonnet of the HAL to see what is happening in there.
Microchip[10] sell literally hundreds of variants of the AVR microcontrollers, each with different attributes - different amounts of memory, different numbers of I/O pins, different built-in communication devices, and so on. You want ideally to be able to write your code once, and then compile it for different devices without needing lots of `if(ATmega4809) then (PORT A is at this address)` type code. That's where the HAL comes in.
THe `ruduino` crate though also doesn't want to have to contain that code. So what actually happens is a new version is dynamically assembled and built based on your target processor. If you specify an ATmega4809, then Cargo will actually run some code (`gen.rs` if I remember rightly) in the `ruduino` crate to dynamically *create* the `ruduino::cores::atmega4809` module, and then builds it for you. Kinda neat!
OK then, so how does it do that? Well, `ruduino` depends on another Cargo crate - `avr-mcu`[11]. What this does is parse an XML description of the processor you're trying to build for, and uses that to create the model of that particular device which `ruduino` then uses to create the HAL code. These XML files are provided by Microchip, and are known as *ATDF* files.
In theory therefore, with only the name of the device and Microchip's provided XML files (which are included in the `avr-mcu` crate, so you don't need to get them yourself), you can automatically generate a HAL for that device; that's what `ruduino` tries to do for you.
There is only one problem with this; the ATDF files from Microchip suffer from poor quality control, and also from wildly different formats and approaches between different device families. That means the theory unfortunately breaks down somewhat in practice. This is why throughout this document we're targeting the ATmega189 - a device known to work - and not the ATmega4809 actually in our Arduino Nano Every; right now, the ATDF file for the '4809 produces a broken model that causes `ruduino` to fail to build.
This is what I plan to fix in Part 2 of this. Hopefully.
Anyway, back to our scheduled programming...
The final piece of the puzzle, after we've compiled our Rust application, is getting it onto the device. That means writing our binary into the Flash memory of the AVR device.
This is normally done with a *Device Programmer*. When I was a young embedded software engineer, we used elaborate equipment (or in some cases even more exotic things like In Circuit Emulators) costing thousands of Euros to program these kinds of devices. One of the most attractive features of the Arduino platform though is that the programmer is built right into the Arduino.
On the Arduino Nano Every there is a USB port; this can be used to connect it to power, and is normally also hooked up to the AVR's UART so you can read/write to your program running on the chip as if you had a serial connection.
But, this USB port has a neat feature. If you give it a special signal (a kind of backdoor), it[^2] stops talking to your application, and instead enters programmer mode. In this mode, it becomes a programmer, using the UPDI over USB[12] protocol.
So how do we enable this backdoor? Well, since the Arduino normally presents itself as a USB Serial device, it can see when we open a connection to the device, and things like what baud rate (serial transmission speed) we selected when we did so: So, the magic sequence that triggers programmer mode on the Nano Every is - open, and then immediately close, the USB serial device at 1200 baud.
Once you do this, it will flip into programmer mode, and you can write your application to the device. All you need now is some programmer software.
Fortunately, the software you need is readily available in the form of the command line application `avrdude`. You can even get it from Homebrew - except *stop, don't do that*.
The version of `avrdude` you'll find in Homebrew isn't compatible with the
UPDI-over-USB protocol used by the Arduino Nano Every.
Presumably in time this will change, but for now you need the version
shipped with Arduino's own IDE.
The easiest way to get hold of this is to head over to https://www.arduino.cc/en/software[13],
download and install their IDE, and then from within the IDE install the
module for the Arduino Nano Every:
* Menu: `Tools / Board / Board Manager...`
* Install the `Arduino megaAVR Boards` package
Once you've done this, you'll find a working version of the `avrdude`
binary somewhere in your home directory's `Library` folder; on mine it
is in `~/Library/Arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/bin/avrdude`
Once you have `avrdude` you have a tool that can basically take an ELF file - the binary you compiled with `cargo build` - and can write it to your device, once it's in programmer mode. It can also do a few other things while it's there, like writing the fuse bits that control various features of the chip.
The exact syntax for doing so is a little bit exotic, so to cut a long story short I have put everything together into a simple shell script. Just call this script with the location of your binary, and it will write it to your Arduino Nano Every. Here it is:
#!/bin/bash # Gimme the name of a file to load ELFFILE=${1} # We need to use the `avrdude` that comes with the Arduino IDE, it seems # to have some custom changes not in the version we installed from Brew, that # work with the UPDI-over-USB bootloader on the Arduino Nano Every AVRDUDE=~/Library/Arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/bin/avrdude AVRCONF=~/Library/Arduino15/packages/arduino/tools/avrdude/6.3.0-arduino17/etc/avrdude.conf # Chip option fuses FUSE_OSCCFG=0x82 # 20 MHz FUSE_SYSCFG0=0xC9 # No CRC, Reset is Reset, don't erase EEPROM FUSE_BOOTEND=0x00 # Whole Flash is boot # Device specific flags PART=atmega4809 PROGRAMMER=jtag2updi BAUDRATE=115200 # Where to find it PORT=$(find /dev/cu.usbmodem* | head -n 1) # We reset the Arduino (and put it into UPDI mode) by opening & closing the # serial port at 1200baud (this is some kind of 'backdoor' reset process # built into the USB software that runs on the Nano Every's coprocessor # for handling USB-to-UPDI. stty -f "${PORT}" 1200 # Wait for the port to be available again while [ 1 ]; do sleep 0.5 [ -c "${PORT}" ] && break done # NOW, finally, we can actually upload our code ${AVRDUDE} \ -C ${AVRCONF} \ -v -p${PART} \ -c${PROGRAMMER} \ -P${PORT} -b${BAUDRATE} \ -e -D \ -Uflash:w:${ELFFILE}:e \ -Ufuse2:w:${FUSE_OSCCFG}:m -Ufuse5:w:${FUSE_SYSCFG0}:m -Ufuse8:w:${FUSE_BOOTEND}:m
So, there it is. From this part, you should have enough information to get going with initialising a Rust project for the AVR, compiling it, and deploying it to your Arduino Nano Every.
In the next part (when I get round to it - no promises when) I'll document my battles with getting a correct HAL built for the ATmega4809.
[^1]: AVR is the generic name for Microchip Technology[14]'s line of 8-bit microcontrollers that they picked up with the acquisition of Atmel. [^2]: There is actually a second, tiny, microcontroller on the Arduino Nano Every that has the code to do this written on it. When you send the magic escape signal over the USB device,
1: https://store.arduino.cc/arduino-nano-every
2: https://www.microchip.com/wwwproducts/en/ATMEGA4809
4: https://www.microchip.com/wwwproducts/en/ATMEGA4809
7: https://book.avr-rust.com/005.1-the-target-specification-json-file.html
8: https://doc.rust-lang.org/cargo/reference/config.html
9: https://crates.io/crates/ruduino
11: https://crates.io/crates/avr-mcu
13: https://www.arduino.cc/en/software
--------------------