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.
§Rust Toolchain Compatibility
Building Rust for AVR requires nightly
. Note that at present there
is a bug in current
versions of Rust which causes an error linking AVR binaries; the latest
toolchain that you must use is therefore nightly-2022-10-22
.
§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 Feature | Clockspeed features (pick 1) |
---|---|
atmega4809 | 16MHz , 20MHz |
atmega328p | 16MHz |
§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_nanoevery | Arduino Nano Every with ATmega4809 CPU |
arduino_uno | Arduino Uno with ATmega328P CPU |
atmega4809_xplained_pro | AVR 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 Processor | Available panic output Features (pick 1) |
---|---|
atmega4809 | panicout_usart0 , panicout_usart1 , panicout_usart2 , panicout_usart3 |
atmega328p | panicout_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 Processor | Minimum memory for heap |
---|---|
atmega4809 | 2048 bytes |
atmega328p | 512 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 Processor | Optional Device Feature Flags |
---|---|
atmega4809 | usart0 ,usart1 ,usart2 ,usart3 ,tcb0 ,tcb1 ,tcb2 ,tcb3 ,rtc , twi0 |
atmega328p | usart0 |
§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 flag | Processor clock speed | Oxide clock frequency |
---|---|---|
None set | 1:1 (equal to hardware clock source) | 80 Hz |
power_med | 1/4th (0.25) of hardware clock source | 40 Hz |
power_low | 1/8th (0.125) of hardware clock source | 10 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:
Test | With runtime_checks | Without runtime_checks |
---|---|---|
Thread stack overflow | Halt with OsError::StackOverflow | Undefined behaviour |
Kernel structure consistency | Halt with OsError::KernelGuardCrashed | Undefined behaviour |
Event queue overflow | Halt with OsError::OxideEventOverflow | Events silently discarded |
Attempt to block inside an ISR | Halt with OsError::BadThreadState | Undefined behaviour |
A StaticWrap dropped | Halt with OsError::StaticDropped | Memory 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()
andboard::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.