FINALLY, someone writes about this hot topic…

…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 Arduino Nano Every

The painfully named Arduino Nano Every is a small embedded processor board based on the ATmega4809 8-bit AVR1 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 (a language both endearingly beautiful and maddeningly frustrating in about equal measure), so naturally I want to program my AVR in Rust as well.

Why is this Part 1?

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; this is somewhat more complex because of issues with the way the current Rust AVR HAL is built, and needs a little more work…

Let’s get this show on the road

What do we need?

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
An ATmega4809 HAL The hardware abstraction layer; this defines the 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 package manager. If not, go to https://brew.sh and get downloading - it’s invaluable for any developer on MacOS.

So, with that said, let’s get going!

1. An AVR Compiler

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

2. A Rust project

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 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 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.

3. An ATmega4809 HAL

I’m sorry Dave, I’m afraid I can’t do that. Yet.

3. An ATmega328p HAL

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 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.)

A digression: How the HAL is built

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 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. 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…

4. An Arduino Programmer

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), it2 stops talking to your application, and instead enters programmer mode. In this mode, it becomes a programmer, using the UPDI over USB 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.

avrdude

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, 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

Bringing it all together - ane-upload.sh

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

That’s all, folks

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’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, ↩︎