Rust on Arduino Nano Every Part Two
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 theavrxmega3
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 :-).