1. Introduction

AVR is a type of low-frequency (sub-20MHz in general) 8-bit microcontroller developed by Atmel and owned by Microchip.

The AVR-Rust project aims to add support to the Rust ecosystem so that executables can be generated for AVR, taking advantage of Rust's safety guarantees and ergonomics.

The following book describes what you need to know to use Rust with AVR support.

The source code of this book can be found on GitHub, pull requests are welcome.

1.1. Reporting bugs

Bugs in this guide can be reported at the GitHub repository for the book.

Bugs in Rust should be reported to Rust itself, ideally with the [AVR] prefix on the issue title. After creation, the triage team will tag the issue as O-AVR for you.

Bugs in ecosystem crates should be reported directly on the relevant crate's issue tracker where possible.

If unclear, feel free to message the kind folks in the avr-rust Gitter channel for advice. Don't worry if you can't track it down to the right place 100% - know that it is better to report a bug in the wrong place than to not report it at all!

2. Installing the compiler

A few pieces of software must be installed to use Rust with AVR support

  • A Rust compiler with AVR support enabled
  • The source code of the compiler
    • This is required as libcore must be compiled lazily as AVRs are not generally ABI compatible with each other so the core library must be explicitly compiled for the AVR device that is being targeted at compile time.

NOTE: Make sure to also install the required third party tools. This, importantly, includes the linker.

AVR support is included in the official Rust nightly compiler as of July 2020. To use AVR support, it is sufficient to install the official Rust nightly compiler, as well as the rust-src component.

First, make sure that rustup is installed. If it is not, install it from rustup.rs.

Then install the nightly and rust-src components by running this in a terminal:

$ rustup toolchain install nightly
...
$ rustup component add rust-src --toolchain nightly

Installation complete. You can proceed to the next part 3. Building a crate for AVR.

Installing or building from source

AVR support is included in any standard Rust nightly build. The standard Rust development instructions apply here.

NOTE: Compiling Rust/LLVM can be very memory intensive. You may find compilation abruptly stopping on machines with less than ~10GB of RAM due to the operating system out-of-memory killer stopping it. If compilation keeps terminating, check if there is a hard-to-spot "process interrupted by operating system" message near the bottom of the logs. If so, consider lowering parallelism flags (-j N, etc) to reduce memory pressure.

2.1. Installing required third party tools

A number of third party tools are required to use AVR Rust.

  • avr-gcc (only used as a linker frontend)
  • avr-binutils (for linker support)
  • avr-libc (for device-specific runtime libraries)
  • avrdude (for flashing a real AVR chip)

These should be installed by the operating system package manager.

Arch Linux

To install all required dependencies under Arch Linux, run

sudo pacman -S avr-gcc avr-libc avrdude

Fedora

To install all required dependencies under Fedora Linux, run

sudo dnf install avr-gcc avr-libc avrdude

Ubuntu

To install all required dependencies under Ubuntu Linux, run

sudo apt-get install binutils gcc-avr avr-libc avrdude

Linux

If your distribution's package manager doesn't provide these tools, you can install them manually.

AVR Toolchain

The AVR toolchain is necessary for compilation, and can be downloaded for several platforms from the manufacturer's website:

https://www.microchip.com/mplab/avr-support/avr-and-arm-toolchains-c-compilers

Once you uncompress the tar.gz for your system, add the subdirectory {platform}/bin to your PATH. For example, if you uncompressed the archive to ~/Downloads/avr8-gnu-toolchain-3.6.2.1759-linux.any.x86_64/, then you'd add this to your .bashrc file:

PATH=$PATH:$HOME/Downloads/avr8-gnu-toolchain-3.6.2.1759-linux.any.x86_64/avr8-gnu-toolchain-linux_x86_64/bin/

AVRDUDE

AVRDUDE is necessary to flash your compiled binary to an AVR chip. It can be downloaded from the project website:

http://download.savannah.gnu.org/releases/avrdude/

Download and uncompress the latest version for your system, and follow the instructions in the INSTALL file.

macOS

Set up the homebrew-avr tap, then install the packages:

brew install avr-binutils avr-gcc avrdude

Note that avrdude may be installed without the homebrew-avr tap.

3. Building a crate for AVR

After setting up the compiler, you may use it to generate assembly or machine code targeting a specific AVR microcontroller model.

Choosing a --target

The Rust nightly compiler includes a built-in target for ATmega328 named avr-unknown-gnu-atmega328

If you wish to target a microcontroller other than ATmega328, or you want to change any of the default builtin options like the linking parameters, then you will need to export the builtin avr-unknown-gnu-atmega328 target to a custom target specification JSON file and modify it to suit your needs.

This target can be adapted to other microcontrollers as per the instructions in 3.1. The built-in avr-unknown-gnu-atmega328 target.

In summary, there are two options:

  • Use rustc --target=avr-unknown-gnu-atmega328 to use the default, builtin GCC based target for ATmega328
  • Or use rustc --target=my-custom-avr-target.json with either a JSON file adapted from the builtin avr-unknown-gnu-atmega328 target above, or otherwise build the file you wish manually to avoid the default path entirely.

Make sure you use the nightly version of Rust, not the default stable channel

The best way to ensure a crate is using the Nightly compiler is to run rustup override set nightly inside a terminal within the root directory of the crate. After this is done, cargo will by-default use the AVR-enabled Nightly compiler any time cargo is used within the directory tree of the crate.

Compiling a crate

To compile and link an executable crate for AVR, run the following:

Using the builtin avr-unknown-gnu-atmega328 target:

cargo build -Z build-std=core --target avr-unknown-gnu-atmega328 --release

Using a custom target specification JSON:

cargo build -Z build-std=core --target /path/to/my-custom-avr-target.json --release

Either of these generate an AVR ELF file that can be subsequently flashed to a real device or ran inside a simulator. The ELF file will be available at target/<TARGET NAME>/release/<CRATE NAME>.elf.

Notes:

  • -Z build-std=core is required whenever AVR is being targeted. See 3.2. A note about the required Rust -Z build-std=<CRATE,> flag for more details.
  • --release is not strictly required - debug mode should be as correct as release mode - however, debug mode generates SLOW CODE, especially on AVR. Release mode is much better.

Example: An in-context example of compiling a crate is given for the LED blinking example in 3.3. Example - Building the blink program for AVR.

Targeting a different microcontroller model

The recommended way to do this is with a custom target specification JSON file per the instructions in 3.1. The built-in avr-unknown-gnu-atmega328 target.

3.1. The built-in avr-unknown-gnu-atmega328 target

The Rust nightly compiler contains a built-in target, avr-unknown-gnu-atmega328, that generates code for the AVR ATmega328 using the GNU AVR-GCC toolchain for linking support.

Targeting custom microcontrollers by adapting 'avr-unknown-gnu-atmega328'

See the section 5.1. The Target Specification JSON File for more information about how Rust target specification JSON files work.

To generate a Rust target specification JSON file from the builtin:

rustc --print target-spec-json -Z unstable-options --target avr-unknown-gnu-atmega328 > my-custom-avr-unknown-gnu-atmega328.json

This prints the target specification JSON file my-custom-avr-unknown-gnu-atmega328.json:

{
  "arch": "avr",
  "cpu": "atmega328",
  "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,
  "is-builtin": true,
  "late-link-args": {
    "gcc": [
      "-lgcc"
    ]
  },
  "linker": "avr-gcc",
  "linker-flavor": "gcc",
  "linker-is-gnu": true,
  "llvm-target": "avr-unknown-unknown",
  "os": "unknown",
  "pre-link-args": {
    "gcc": [
      "-mmcu=atmega328",
      "-Wl,--as-needed"
    ]
  },
  "target-c-int-width": "16",
  "target-endian": "little",
  "target-pointer-width": "16",
  "vendor": "unknown"
}

To adapt this file to target a different microcontroller:

  • Replace the top-level "cpu": "atmega328" with "cpu": "YOUR-AVR-VARIANT-NAME"
  • Replace "-mmcu=atmega328" with "-mmcu=YOUR-AVR-VARIANT-NAME"

The file can then be passed to Rust via the rustc --target <JSON FILE PATH> instead of rustc --target avr-unknown-gnu-atmega328, which will tailor the generated code to your desired microcontroller.

It is also possible to customize link parameters if desired by modifying the JSON file.

Compiling for AVR without the GNU toolchain

At the moment, the only builtin AVR target avr-unknown-gnu-atmega328 always requires AVR-GCC, AVR-libc and AVR-binutils from the GNU project.

The LLVM LLD linker has some limited support for AVR which in the future could be leveraged to allow compiling AVR Rust binaries without the dependency on the GNU toolchain. Some work on compiler-builtins and others would also be required. At the moment, the GNU toolchain is a hard dependency.

3.2. A note about the required Rust -Z build-std=<CRATE,> flag

AVR-Rust is not distributed with a pre-built libcore crate. Instead, it is compiled on-demand when a crate uses it via the Rust -Z build-std flag.

There are many hundreds of variants of AVR microcontroller, and it is not feasible to distribute runtime libraries for all of them within a regular Rust distribution.

Due to this, any time a crate is built for AVR, -Z build-std=core should be passed to cargo.

Example

cargo build -Z build-std=core --target avr-atmega328p.json --release

What happens if you don't pass it

Then you will get a "cannot find crate for 'core'" error when you compile the crate. The only crates that can avoid this are those that are #[no_core] crates, such as libcore itself.

More information

3.3. Example - Building the blink program for AVR

This shell snippet shows an example that will build the LED blinking example for AVR.

# fetch and prepare the blink crate (only needs to be done once).
$ git clone https://github.com/avr-rust/blink.git
$ cd blink
$ rustup override set nightly

# Configure the 'ruduino' crate with a MCU frequency of 16MHz
$ export AVR_CPU_FREQUENCY_HZ=16000000

# compile the blink crate to an ELF file targeting atmega328p.
$ cargo build -Z build-std=core --target avr-atmega328p.json --release

After compilation, the final ELF executable will be available at target/avr-atmega328p/release/blink.elf, ready to be flashed to a device or ran inside a simulator.

$ file target/avr-atmega328p/release/blink.elf
target/avr-atmega328p/release/blink.elf: ELF 32-bit LSB executable, Atmel AVR 8-bit, version 1 (SYSV), statically linked, with debug_info, not stripped

Other AVR microcontroller models may be targeted by simply copying and modifying avr-atmega328p.json.

4. Flashing a crate to a real AVR chip

The AVRDUDE utility is recommended for flashing the final ELF file to a physical AVR microcontroller.

Flashing a Rust ELF file is no different to flashing a regular AVR-GCC C/C++ generated ELF file.

Arduino Uno

Connect your Arduino Uno to your computer, and use avrdude to flash your crate. The example below uses the output from the blink example.

avrdude -patmega328p -carduino -P[PORT] -b115200 -D -Uflash:w:target/avr-atmega328p/release/blink.elf:e

where

  • -patmega328p is the AVR part number
  • -carduino is the programmer
  • -P[PORT] is the serial port of your connected Arduino
    • On Linux & macOS, replace [PORT] with your Arduino's serial port (like /dev/ttyUSB0)
  • -b115200 is the baud rate
  • -D disables flash auto-erase
  • -Uflash:w:target/avr-atmega328p/release/blink.elf:e writes the blink.elf program to the Arduino's flash memory

For more debugging information, run avrdude with one or more -v flags.

Note: on older Arduino versions, you may get a series of avrdude: stk500_getsync(): not in sync: resp=0x00 errors indicating you need to use a slower baud rate:

avrdude -patmega328p -carduino -P/dev/[PORT] -b57600 -D -Uflash:w:target/avr-atmega328p/release/blink.elf:e

5. Summary of Steps

Initial target-independent steps for creating a new crate

This first step is not AVR specific.

# Create a new binary project. You may also create a new library project with '--lib'.
cargo new --bin my-new-project
cd my-new-project

# AVR requires the nightly compiler
rustup override set nightly

Step 1. Adding a target specification JSON file for AVR

Compared to most Rust targets like X86 or ARM, the AVR family of microcontrollers cannot be cleanly modelled under different "families" of chips that are ELF-compatible. This is due to the fact that there are special variants of chips with nonstandard ISA support, as well as the ELF e_flags field which has a value that also has special cases for certain microcontrollers.

The upstream Rust compiler does not provide built-in targets for specific AVR microcontrollers. Instead, a target specification JSON file targeting a specific AVR device must be passed to Rust and Cargo.

Here are the steps for creating and using a target specification JSON file for AVR.

Step 2. Adding the #[no_std] attribute to your crate root

Like many embedded targets, the standard library is not supported and so it must be opted out of.

Step 3. Creating an executable entry point with #[no_main]

As we are using the device-specific startup runtime libraries for C/C++ under AVR-GCC, we must bypass Rust's usual handling of the main function so that the main function is correctly picked up by the GCC startup libraries.

Step 4. Choosing an input/output library

Use an existing library or write your own - your choice.

Recommended is embedded-hal, which provides a architecture-independent way to expose GPIO pins and peripherals that works across devices such as AVR and ARM.

5.1. The target specification JSON file

External resources:

Rust provides a built-in list of target specifications that are viewable via rustc --print target-list. A custom target specification JSON file may be written to override and tweak various target-specific options, such as linker scripts, flags, and LLVM options.

When compiling for AVR, a specific microcontroller variant/model must be targeted. This allows LLVM to generate instructions that confirm to each particular microcontroller's ABI and supported instruction set.

The target specification JSON file:

  • May live in any directory - most often, committed to version control.
  • Any file name you like, so long as it's JSON
  • Is given to Rust via the --target my-target-spec.json. For example, cargo build --target ./targets/my-target-spec.json.

An example - using the GNU toolchain for linking and compiler support libraries

Here is an example target specification JSON file for the AVR atmega328p.

This target specification:

  • Enables the GNU AVR linker through the avr-gcc compiler frontend
  • Tells the linker to include libgcc (as compiler-rt for AVR is not yet fully supported)
  • Tells Rust not to pass the --eh-frame-hdr argument to the linker, which isn't supported by AVR-GCC and will cause and error if omitted.
  • Instructs LLVM to use the avr-unknown-unknown target specifically for the atmega328p microcontroller
  • Explicitly passes an -mmcu argument to the linker to ensure runtime libraries are not skipped by the linker
{
  "arch": "avr",
  "cpu": "atmega328p",
  "data-layout": "e-P1-p:16:8-i8:8-i16:8-i32:8-i64:8-f32:8-f64:8-n8-a:8",
  "env": "",
  "executables": true,
  "linker": "avr-gcc",
  "linker-flavor": "gcc",
  "linker-is-gnu": true,
  "llvm-target": "avr-unknown-unknown",
  "no-compiler-rt": true,
  "os": "unknown",
  "position-independent-executables": false,
  "exe-suffix": ".elf",
  "eh-frame-header": false,
  "pre-link-args": {
    "gcc": ["-mmcu=atmega328p"]
  },
  "late-link-args": {
    "gcc": ["-lgcc"]
  },
  "target-c-int-width": "16",
  "target-endian": "little",
  "target-pointer-width": "16",
  "vendor": "unknown"
}

Adapting to another microcontroller variant

Adapting a target specification JSON to another AVR variant is trivial.

Options that must be updated to target individual microcontroller variants:

  • cpu - set to the lower-case model name of the desired AVR variant
  • pre-link-args - the correct -mmcu option, always equal to the cpu, must be updated

5.2. Adding the #[no_std] attribute to your crate

External resources

Adding #[no_std]

As an embedded target with no operating system, many faculties that the Rust standard library requires are not and could not be supported on AVR.

Because of this, only #[no_std] crates may be compiled for AVR.

#![allow(unused)]
fn main() {
// src/main.rs or src/lib.rs

#![no_std]

fn my_function() {
    // ...
}

// ...
}

5.3. Creating an executable entry point with #[no_main]

The standard Rust AVR toolchain will use AVR-GCC's linker and chip-specific startup runtime libraries. Due to this, it is necessary to override Rust's default behaviour for the main function and instead write one that looks exactly like a AVR-GCC C/C++ main function so that the regular AVR-GCC libraries will link to it correctly.

To define a main function for AVR:

  • Add the #![no_main] attribute to the top of the crate root (src/main.rs)
  • Add a custom main function callable from C that will be picked up by AVR-GCC's startup libraries
// src/main.rs

#![no_std] // from the previous step
#![no_main]

#[no_mangle]
pub extern fn main() {
}

Notes:

  • The #![no_main] attribute tells Rust to skip its usual expectations on the main function, instead letting the developer manually handle it. In the usual case for AVR, the target specification JSON file will link startup files from AVR-GCC and thus the ABI of the Rust main function must match that of the of AVR-GCC startup files.
  • These steps can be applied to any executable within a crate, including those in the examples/ directory for a library and those injected via [[bin]] attributes in the Cargo.toml.

5.4. Choosing an input/output library

To communicate with the outside world, you will need to use the AVR's built in IO registers to configure and access the electrical pins on the outside of the chip.

External resources

The problem space of IO on AVR

  • Reading from or writing to an IO pin is as easy as reading a byte from or writing a byte to the correct location in memory
  • The main issue is identifying which memory locations assigned to which physical pins and peripherals for each chip

Rather than using bare-metal IO tied to specific AVR chips, it is recommended to use an abstraction layer over the actual I/O register manipulation.

A bare-metal example of IO

This example uses hardcoded IO register locations for the ATMega328p. It may work for some other AVR devices in the same family, but others will use different IO register memory mappings and so will not produce the expected output when ran on these devices.

NOTE: Executables should prefer using an abstraction layer over the actual I/O such as embedded-hal rather than writing to microcontroller-specific I/O registers directly.

#![no_std]
#![no_main]

extern crate avr_std_stub;

/// The data direction register for PORT B, which is mapped to 0x24 in memory on the atmega328.
const DDRB: *mut u8 = 0x24 as *mut u8;
/// The pin status register for PORT B, which is mapped to 0x25 in memory on the atmega328.
const PORTB: *mut u8 = 0x25 as *mut u8;

#[no_mangle]
pub extern fn main() {
    unsafe {
        // Set the upper four physical pins on PORT B to inputs, the lower four to outputs.
        // The AVR interprets '1' in the data direction register as 'output', '0' input
        // for the corresponding pin.
        core::ptr::write_volatile(DDRB, core::ptr::read_volatile(DDRB) | 0b00001111);

        // Write half of the output pins as high, the other half low.
        core::ptr::write_volatile(PORTB, 0b00001010);
    }
}

High level libraries for IO on AVR

The embedded-hal Hardware abstraction layer

The embedded-hal project exposes a device-independent set of traits that can be implemented by crates for specific devices. In this manner, one crate can be written to target multiple different embedded device architectures like ARM, MSP430, and AVR.

The embedded-hal project

embedded-hal implementations for AVR:

  • Rahix/avr-hal
    • Supports atmega32u4, attiny85, atmega328p, atmega1280

The AVR-specific avr-rust/ruduino crate

Caveat: This crate can and will only work for AVR. Depending on this crate directly will lock you out of targeting architectures other than AVR.

The ruduino library provides a high-level type-safe API for interacting with the AVR IO registers.

Driven from the official .atdf AVR device specification files, this crate exposes all available IO registers for any AVR device you wish to target.

At the moment, the high level bindings in ruduino are limited. You have access to all of the peripherals, but you will need to write to the expected IO registers manually.

Others?

Pull requests to github.com/avr-rust/book.avr-rust.com welcome!

6. Quick start from template

Quick start

The quickest way to get going is to clone

# Download and extract the template repository to the current directory.
curl "https://codeload.github.com/avr-rust/template-bin/tar.gz/master" | tar xvzf -

# Then update the package name in Cargo.toml
sed -i 's/template-bin/$YOUR_CRATE_NAME/g' template-bin-master/Cargo.toml
# And rename the folder
mv template-bin-master $YOUR_CRATE_NAME

# Prepare a new Git repository
cd $YOUR_CRATE_NAME
git init

Then, to build an ELF file at target/avr-atmega328p/release/$YOUR_CRATE_NAME.elf, run:

cargo build --target avr-atmega328p.json -Z build-std=core --all --release