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.
Resources and links
- The source code of this book
- The AVR-Rust project homepage
- The AVR-Rust organization on GitHub
- Known compiler issue list
- Known compiler issue list (legacy, read only)
- awesome-avr-rust - a publicly curated list of AVR crates and projects
- New executable template repository
avr-rust/template-bin
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.
- This is required as
NOTE: Make sure to also install the required third party tools. This, importantly, includes the linker.
Installing via Rustup (recommended)
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 builtinavr-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
- The official Rust docs on the unstable
build-std
flag - Tracking repository for the std-aware-cargo Working Group
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
)
- On Linux & macOS, replace
-b115200
is the baud rate-D
disables flash auto-erase-Uflash:w:target/avr-atmega328p/release/blink.elf:e
writes theblink.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.
Step N. - More documentation is on its way. In the meantime, avr-rust/blink serves as a reference implementation
5.1. The target specification JSON file
External resources:
- The Embedonomicon on custom targets
- rust-lang RFC on target specification JSON files
- Upstream Rust documentation on custom target specifications [outdated,
xargo
is no longer required]
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 theatmega328p
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 variantpre-link-args
- the correct-mmcu
option, always equal to thecpu
, 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 Rustmain
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 theCargo.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.
embedded-hal implementations for AVR:
- Rahix/avr-hal
- Supports
atmega32u4
,attiny85
,atmega328p
,atmega1280
- Supports
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