Story so far

In November last year, I wrote about my early efforts to get Rust to compile on the Arduino Nano Every - the latest, and cheapest/most convenient as an embedded controller - iteration of the Arduino family of Microchip/Atmel AVR 8-bit microcontroller based boards.

The ATmega4809 chip that the Every is based on is in many ways a super nice chip, small, fast, power light, and with more memory for your code and data than chips used in older Arduino boards. Unfortunately, it’s also based on a somewhat different internal architecture - avrxmega3 - than those other boards, meaning that toolchain support is best described as “broken to non existent”. The avr-gcc you installed from a package probably can’t even compile or link properly for it, before you even begin to think about Rust’s limited support for AVR.

What’s New

As evidenced by my last post on this subject being in November 2020, progress is not exactly quick. That’s mostly a factor of the day job presenting many other challenges in the meantime than the complexity of the task though.

I am slowly making some progress on getting useful code to build and run, though. Hopefully some of the things I’ve discovered will be helpful to anyone else taking the same journey…

Dependencies

First off, toolchain dependencies. In my last post, I had managed to get a basic LED-blinking program to run using a hacked version of the Ruduino crate. That was mostly by luck rather than judgement - but it did at least prove that it was possible.

But, since then, I’ve realised the first thing we need to do with this board is give up on ‘standard’ versions of the underlying compiler/linked toolchain, avr-gcc and avr-ld. They don’t understand this chip properly… Fortunately, Microchip maintain their own fork of gcc which does work properly with this chip.

So, my first advice to you is to download and install the Atmel toolchain for your platform from the link below. After installing, modify your PATH so the Atmel version of the commands appears before any other version of avr-gcc you may have installed. (I am succesfully using both the Mac (on my desktop) and Linux (in my CI/CD pipeline) versions of these.)

ATpacks

Atmel also distribute “ATpacks” for each of their microchips. These contain a machine-readable description of the chip’s capabilities and also some support libraries you need to link into your eventual binary, specific to each chip. You will need to download these too, and then you can point avr-gcc to the correct ATpack for the ATmega4809 using the -B command line flag. We’ll see how to do this using Rust in the A Working Build Target section of this post.

Resource Where
ATmega toolchain https://www.microchip.com/en-us/development-tools-tools-and-software/gcc-compilers-avr-and-arm
The ATmega ATpacks http://packs.download.atmel.com

Rust avr-hal crates

Last time round, I used the Ruduino crate, which was a great start and I’m very grateful for it. But, I have discovered that there is a somewhat more recently maintained, and cleaner, HAL crate avr-hal, which it was a little easier to hack 4809 support into.

Note that there will be official support for the ATmega4809 in avr-hal eventually - and it’ll be a great day when it comes :). But at the moment there is a lot of good work happening in that project on more general development and refactoring of the HAL, so in the meantime I am using a slightly older version of the crate which I have hacked to provide basic support for the 4809.

Because this is a hack, and eventually will be deprecated in favour of the ‘official’ suppoort when it arrives, I am not publishing it on the official crates.io Rust crate repo. However, I am publishing it publicly for anyone who does wish to use it on a public Cloudsmith Rust repo.

To use my packaged versions, add the following to the .cargo/config.toml file in your Rust project:

[registries]
snowgoons-crates = { index = "https://dl.cloudsmith.io/public/snowgoons/crates/cargo/index.git" }

You can then reference my 4809-compatible versions of the HAL for the Arduino Nano Every in your Cargo.toml like so:

arduino-nano-every = { version = "0.1.0", registry = "snowgoons-crates" }
avr-hal-generic = { version = "0.1.0", registry = "snowgoons-crates" }

The source is of course there in GitHub as well:

Crate Github
arduino-nano-every, avr-hal-generic https://github.com/timwalls/avr-hal/tree/arduino-nano-every
avr-device https://github.com/timwalls/avr-device/tree/ATmega4809

A working build target

OK, so, now we have a working toolchain, and a HAL that is known to work, we need to give Rust a build target description that tells it to use the right toolchain for the ATmega4809. We do this using a JSON file in the root of our Cargo project.

Last time round, you may remember we used an atmega328p build target; this worked to get basic code working (the instruction set is the same,) but would fail as soon as you tried anything more ambitious because of architecture and memory map differences between the chips. Now we have a basically functioning working description that’s actually right for the ATmega4809.

To use this description, add this to your .cargo/config.toml:

[build]
target = "avr-atmega4809.json"

Now you need to create the avr-atmega4809.json file like so:

{
  "arch": "avr",
  "atomic-cas": false,
  "cpu": "avrxmega3",
  "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=atmega4809",
      "-Wl,--as-needed",
      "-Wl,-Map=target/memory.map",
      "-L","./atmel-atpack/atmega4809/avrxmega3",
      "-B","./atmel-atpack/atmega4809/"
    ]
  },
  "target-c-int-width": "16",
  "target-endian": "little",
  "target-pointer-width": "16",
  "vendor": "unknown"
}

There are a couple of important things to note:

  • If you’re not using the ATmega toolchain, this will likely not work properly; depending on your OS/distribution, the avr-gcc you get from a package repo probably doesn’t understand the avrxmega3 architecture
  • The "-L","./atmel-atpack/atmega4809/avrxmega3" and "-B","./atmel-atpack/atmega4809/" lines need to point to the correct (atmega4809) directory in the ATpacks that you downloaded from Microchip. I copy these folders into my build environment/repo to guarantee consistent compilation across environments, but you could also just point to wherever you unzipped the ATpacks.

Deploying automatically with cargo run

Last time round I gave you a script to upload compiled ELF code to your Arduino. This still works; the only thing to add here is that by adding a couple of lines to your .cargo/config.toml you can have Cargo automatically use this script when you use cargo run, which is a nice convenience:

[target.'cfg(target_arch = "avr")']
runner = "bin/arduino-nano-every-upload.sh"

Bringing it all together

If you got everything right, you should be able to test it’s working with a test programme that looks something like this:

#![no_std]
#![no_main]

use arduino_nano_every::prelude::*;

#[arduino_nano_every::entry]

fn main() -> ! {
  let dp = arduino_nano_every::Peripherals::take().unwrap();

  let mut pins = arduino_nano_every::Pins::new(dp.PORTA, dp.PORTB, dp.PORTC, dp.PORTD, dp.PORTE, dp.PORTF);

  // On the Nano Every, the LED is on pin D13
  // Note - these are the *Arduino* pin references, not ATmega port references
  // it's the avr_hal:arduino_nano_every crate's job to map the Arduino pin refs
  // to the actual port used on the ATmega4809.
  let mut led = pins.d13.into_output(&mut pins.ddr);

  loop {
    led.toggle().void_unwrap();
    arduino_nano_every::delay_ms(500);
  }
}

#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
  loop {}
}

The Cargo.toml would look like:

[package]
name = "blinky"
version = "0.1.0"
edition = "2018"

[dependencies]
arduino-nano-every = { version = "0.1.0", registry = "snowgoons-crates" }
avr-hal-generic = { version = "0.1.0", registry = "snowgoons-crates" }

[profile.dev]
panic = "abort"
lto = true
opt-level = "s"

[profile.release]
panic = "abort"
codegen-units = 1
debug = true
lto = true
opt-level = "s"

Your .cargo/config.toml will look like this:

[build]
target = "avr-atmega4809.json"

[unstable]
build-std = ["core"]

[registries]
snowgoons-crates = { index = "https://dl.cloudsmith.io/public/snowgoons/crates/cargo/index.git" }

[target.'cfg(target_arch = "avr")']
runner = "bin/arduino-nano-every-upload.sh"

You will also need a specific version of the Rust toolchain - nightly-2021-01-07 - because later versions introduce AVR-specific bugs. You can configure this using the rustup command, but you can also just put a config file rust-toolchain.toml into the root of your project as well:

[toolchain]
channel = "nightly-2021-01-07"
components = ["rust-src"]

Don’t forget to include the avr-atmega4809.json file described above, and to include the ATpacks in an appropriate place, and then, fingers crossed, you should be able to build and run!

Good luck :-).