Expand description

A simple Rust runtime for AVR microcontrollers.

AVRoxide is a simple Hardware Abstraction Layer and runtime for developing software for ATmega AVR microcontrollers.

It was born out of frustration of the lack of options available for Rust development using the ATmega4809 microcontroller present in Arduino Nano Every embedded processor boards, but is intended to grow to support multiple target devices.

Key features include:

  • Abstractions of the key hardware devices provided by the controller, like USARTs, timers, and GPIO ports.
  • Higher-level abstractions - like ‘clock’ and ‘button’ - for simple application programming.
  • An “Arduino Familiar” way of referring to hardware components, while also offering complete access to the underlying chip for regular, non-Arduino, embedded solutions.
  • A simple runtime for event-based application development, using interrupt-driven IO for power-efficient operation.
  • Pre-emptive multithreading supervisor with basic synchronisation primitives.

Feature Flags

Feature flags are used so you can ensure minimal code size by disabling features that are not required, and also to configure some of those features at compile-time (important in an embedded system.) Some of these features are mandatory, and describe the type of hardware you are building for. Others describe optional functionality.

Mandatory Features

It is necessary to tell AVRoxide what device you are supporting by providing both a processor feature, and a clockspeed feature, from the list below. The CPU feature flag determines which hardware devices are exposed by the HAL, while the clockspeed feature is used to calculate certain constants for things like timer and baud rate calculations.

CPU FeatureClockspeed features (pick 1)
atmega480916MHz, 20MHz
atmega328p16MHz

Optional Features

Board Compatibility

Feature flags can be used to enable a board-compatibility module that you can then use to access pins via “board-specific” aliases. For example, if you enable on of the Arduino board features, you can access the A and D pins using the Arduino naming conventions using aliases like so:

use avr_oxide::boards::board;

#[avr_oxide::main(chip="atmega4809",stacksize=512)]
pub fn main() {
  let green_led = OxideLed::with_pin(board::pin_d(7));
}

The relevant feature flags are:

Feature name
arduino_nanoeveryArduino Nano Every with ATmega4809 CPU
arduino_unoArduino Uno with ATmega328P CPU
atmega4809_xplained_proAVR ATMega4809-XPlained-Pro board

Panic handler

If the panic_handler feature is enabled, a default panic handler will be provided.

Panic to serial port support

You can configure a serial port which will be used by the AVRoxide [panic handler] to output error line information on panic.

It is your responsibility to initialise that port with a suitable baud rate/serial protocol as early as possible in your program.

For ProcessorAvailable panic output Features (pick 1)
atmega4809panicout_usart0, panicout_usart1, panicout_usart2, panicout_usart3
atmega328ppanicout_usart0

Important: Because of the way Rust embeds panic information in your binary, enabling this feature can take up a fair amount of Flash memory space. For example, a simple test app that uses 37882 bytes of Flash without this feature uses 44330 bytes with it.

Note also that because Rust embeds absolute pathnames for source files into the binary, the size of your binary can change depending on where (the directory path) you compile it on your development machine!

Often, we need to disable Rust’s built-in panic handling entirely in .cargo/config.toml, like so:

build-std-features = ["compiler-builtins-mangled-names","panic_immediate_abort"]

In this case, there is still value to nominating a panicout port; avr-oxide will still use this serial port to dump thread dumps, or for use with the built-in serial debug methods in the avr_oxide::util::debug module.

Dynamic Allocator

A dynamic allocator implementation is provided; this will allocate a certain amount of the device’s memory as a heap for dynamic data allocation. The size of the heap will be automatically calculated after taking into account space used by statics/global variables.

A certain minimum amount of memory allocatable to heap is required by AVRoxide. If this minimum amount of RAM is not available, the system will halt on boot with an avr_oxide::oserror::OsError::NotEnoughRam error.

The minimum heap size requirement is dependent on the processor:

For ProcessorMinimum memory for heap
atmega48092048 bytes
atmega328p512 bytes
Stack size

Note that the thread stack is allocated from the heap when a thread is created (including your initial main thread.)

Rust memory use with panics

The way Rust currently handles panics results in inordinate use of not just EEPROM, but also RAM. You can help significantly by adding the panic_immediate_abort feature to your .cargo/config.toml:

build-std-features = ["compiler-builtins-mangled-names","panic_immediate_abort"]

If your application will not run, with an OsError::NotEnoughRam error, you may need to do this.

Device Features

Some individual device ‘drivers’ are enabled by feature flags. If you don’t need one, don’t enable it and maybe you can save a few precious bytes of RAM…

For ProcessorOptional Device Feature Flags
atmega4809usart0,usart1,usart2,usart3,tcb0,tcb1,tcb2,tcb3,rtc, twi0
atmega328pusart0

Low-Power Modes

By default, Oxide configures the chip to run without a clock prescaler (i.e. it runs at the full configured clockspeed), and a basic internal clock interrupt frequency of 80Hz (this is the maximum frequency for which you can configure MasterClock device events.)

It is possible however to enable lower-power-use modes through either the power_med or power_low feature flags; these have the following effect:

Power mode flagProcessor clock speedOxide clock frequency
None set1:1 (equal to hardware clock source)80 Hz
power_med1/4th (0.25) of hardware clock source40 Hz
power_low1/8th (0.125) of hardware clock source10 Hz

Pre-Emptive Multithreading

AVRoxide supports multithreading, with threads created/joined using a simplified version of the standard Rust thread::spawn() and thread::yield_now() methods (provided through the avr_oxide::thread module.) Familiar synchronisation primitives are provided in avr_oxide::sync.

Cooperative threading (using yield_now() and/or syncorhonisation primitives which yield when blocked as and when necessary) requires no further configuration.

Pre-emptive multithreading is supported as well, but does require one further step - a timer that will drive thread preemption. This means at least one MasterClock device must be created and running:

#![no_std]
#![no_main]

use avr_oxide::devices::{ UsesPin, OxideLed, OxideMasterClock };
use avr_oxide::boards::board;
use avr_oxide::thread;
use avr_oxide::hardware;
use avr_oxide::StaticWrap;

#[avr_oxide::main(chip="atmega4809",stacksize=512)]
pub fn main() {
  let supervisor = avr_oxide::oxide::instance();

  // Configure a 50Hz master clock device on TCB0
  let master_clock = StaticWrap::new(OxideMasterClock::with_timer::<50>(hardware::timer::tcb0::instance()));
  supervisor.listen(master_clock.borrow());

  // The MasterCLock will by default drive context switches, thus threads
  // will now be pre-emptively multitasked, and this code will work without
  // locking up:
  let _jh = thread::Builder::new().stack_size(32).spawn(||{
    let white_led = OxideLed::with_pin(board::pin_d(10));

    loop {
      white_led.toggle();
    }
  });

  // Note that all the usual functionality of the MasterClock device
  // (delay timers, regular Oxide event handlers) remains available -
  // it will be used for thread preemption *in addition* to its usual
  // function, not instead.

  supervisor.run();
}

Note that by default all MasterClock devices trigger context switches. You can disable this behaviour with the disable_preemption() method on a device that you don’t wish to trigger context switches.

Runtime checks

The kernel performs various sanity checks during runtime, if enabled with the runtime_checks feature. It is strongly encouraged that you do use this feature, however it does imply a certain amount of overhead (in particular during the context-switch interrupt routine, which is when most of these are executed), so if you need to squeeze every last ounce of performance out of the processor you may disable them.

The table below summarises the checks performed:

TestWith runtime_checksWithout runtime_checks
Thread stack overflowHalt with OsError::StackOverflowUndefined behaviour
Kernel structure consistencyHalt with OsError::KernelGuardCrashedUndefined behaviour
Event queue overflowHalt with OsError::OxideEventOverflowEvents silently discarded
Attempt to block inside an ISRHalt with OsError::BadThreadStateUndefined behaviour
A StaticWrap droppedHalt with OsError::StaticDroppedMemory leaked

A Minimal AVRoxide Program

This program shows how to access the devices using the Arduino aliases for an Arduino Nano Every, and how to listen to button events from a button attached to pin A2 to toggle an LED attached to pin D7 every time the button is pressed.

We also show the use of a software debouncer on the pin, and configuring a serial port.

#![no_std]
#![no_main]

use avr_oxide::alloc::boxed::Box;
use avr_oxide::devices::UsesPin;
use avr_oxide::devices::debouncer::Debouncer;
use avr_oxide::devices::{ OxideLed, OxideButton, OxideSerialPort };
use avr_oxide::hal::generic::serial::{BaudRate, DataBits, Parity, SerialPortMode, StopBits};
use avr_oxide::io::Write;
use avr_oxide::boards::board;
use avr_oxide::StaticWrap;

#[avr_oxide::main(chip="atmega4809")]
pub fn main() {
  let supervisor = avr_oxide::oxide::instance();

  // Configure the serial port early so we can get any panic!() messages
  let mut serial= OxideSerialPort::using_port_and_pins(board::usb_serial(),
                                                       board::usb_serial_pins().0,
                                                       board::usb_serial_pins().1).mode(SerialPortMode::Asynch(BaudRate::Baud9600, DataBits::Bits8, Parity::None, StopBits::Bits1));
  serial.write(b"Welcome to AVRoxide\n");

  let green_led = OxideLed::with_pin(board::pin_d(7));
  let mut green_button = StaticWrap::new(OxideButton::using(Debouncer::with_pin(board::pin_a(2))));

  green_button.borrow().on_click(Box::new(move |_pinid, _state|{
    green_led.toggle();
  }));

  // Tell the supervisor which devices to listen to
  supervisor.listen(green_button.borrow());

  // Now enter the event loop
  supervisor.run();
}

Examples and Templates

A more comprehensive example program can be found here.

‘Empty’ templates can be found at the AVRoxide Templates gitlab repo, with all the necessary bits and pieces to get up and running quickly.

The project homepage is at avroxi.de, where you’ll find more getting-started style guides and documentation.

Re-exports

pub use oserror::OxideResult;
pub use avr_oxide::hal::atmega4809 as hardware;
pub use avr_oxide::concurrency::sync;
pub use avr_oxide::concurrency::thread;

Modules

A simple dynamic allocator implementation for our embedded devices.

A helper module that maps standard ATmega pins into equivalent Arduino names through helper functions board::pin_a() and board::pin_d().

Concurrency/threading for AVToxide

Device-specific constants. These are determined by the feature flags configued in your Cargo.toml.

Higher-level device abstractions for things like LEDs, system clocks, buttons.

Oxide system events; these are generated by the low-level device driver interrupt handlers and passed to the OxideSupervisor, which is responsible for then scheduling the appropriate userland code callbacks to handle the events.

Abstraction of the hardware devices attached to an AVR microcontroller

Simple IO traits (a very, very, very cutdown version of std::io). In due course hopefully somewhat /less/ cutdown, and maybe even derived from std::io in the style of the core_io crate, but right now this is what we’ve got.

A way of communicating fatal OS errors that doesn’t have all the baggage that Rust loads Panic with, but still allows for useful debugging.

Simple supervisor implementation for the AVRoxide runtime.

Simple macros and container for a static reference to a global SerialPort instance, so we can use print!()/println!() style macros.

Simplified datastructures for dealing with time, meant to broadly mirror the functionality provided by std::time where possible (and is derived directly from that source.)

General utility types/functions.

Macros

Obtain a reference to the CPU controller instance

A macro you can use as a substitute for .unwrap() on Options that will simply panic if the result is None. You will want this to avoid hauling in all of the garbage associated with the Debug and Formatter types that comes along with the ‘real’ unwrap() implementation. With small EEPROMs/Flash we don’t have the room for that luxury.

Print the given output to the standard out writer device previously set with the set_stdout() function.

Print the given output to the standard out writer device previously set with the set_stdout() function, followed by a newline character.

Obtain a references to the Sleep controller instance

Structs

In an embedded environment, we often need static references to things, for example because we are passing them on to an interrupt service routine.

Traits

Attribute Macros