I mentioned some time ago that I was working on developing a runtime for Rust on the Arduino Nano Every; now I’m extremely pleased to announce that work has come to some sort of fruition.

And so, here it is.

But what is it?

At its heart, AVRoxide is a crate you can add to your application, just as any other Rust library. What AVRoxide will provide you with is:

  • A simple Hardware Abstraction Layer to access the various device components provided by the ATmega4809 processor.
  • Interrupt handling for those devices, that is exposed to your application through a simple on_event(...) style callback model allowing you to get on with writing your application without worrying about low-level hardware details (or interrupt/thread safety.)
  • A simple ‘supervisor’ that is responsible for queuing those events from the hardware devices and dispatching them to your code.
  • Various items of comfort food - like dynamic memory allocation, println! macros for outputting formatted text to serial ports (with formatting provided by the ufmt crate), and a simple data serialisation trait, to make application development simpler and familiar.

But as well as being a crate, AVRoxide also comprises a certain amount of my trial and error experience of getting a working toolchain and build environment for Rust on AVR. By starting with the template project, and following the step by step guide, you can skip most of the pain and get straight down to developing application code.

Further Reading

Rather than go on here, I’ll simply paste the links you need to get started:

Example Application

Over on the project site, I posted a listing of an example application that compiles, and runs correctly on the ATmega4809.

By way of illustration of what AVRoxide can do for you, I reproduce parts of it here with some comments on what it is doing…

Simple Persistence

Through the Persist trait, and its corresponding Derive macro, we have access to simple binary load/save methods, that work with any Reader or Writer - including the provided EEPROM or Serial drivers.

#[derive(Persist)]
#[persist(magicnumber = 2)]
struct MyPersistentData {
  number_of_boots: u16,
  username: Option<Vec<u8>>
}

Boot support

AVRoxide provides your boot code (interrupt vector table, memory initialisation, etc.) so all you need to do is provide a main() method:

#[avr_oxide::main(chip="atmega4809")]
pub fn main() -> ! {

Simple access to hardware devices

Simple single-method access to a struct that gives you access to all the supported hardware devices on your processor, and an (optional) Arduino specific method that translates the ATmega4809 pin naming into Arduino pin naming conventions:

  let hardware = hardware::instance();
  let arduino = arduino::nanoevery::Arduino::from(hardware);

Buffered serial port drivers & println! support

Simple access to serial ports, and the ability to assign a serial port as a global stdout for use with println!. AVRoxide will also optionally output panic!() debug information to a nominated serial port (configured using a Crate feature.)

  // Configure the serial port early so we can get any panic!() messages
  let mut serial= Handle::new(OxideSerialPort::using_port_and_pins(arduino.usb_serial,
                                                                   arduino.usb_serial_tx,
                                                                   arduino.usb_serial_rx).mode(SerialPortMode::Asynch(BaudRate::Baud9600, DataBits::Bits8, Parity::None, StopBits::Bits1)));
  serial.serial_options().set_interactive(true);
  serial.serial_options().set_lf_to_crlf(true);
  avr_oxide::stdout::set_stdout_handle(serial.clone());

  println!("AVRoxide On-Device Test App running...");
  println!("I can do {}, too, thanks to {}!", "formatting", "ufmt");

EEPROM read/write access

Through simple Reader and Writer interfaces and the Persist trait:

  let eeprom = arduino.eeprom;
  let mut my_data : MyPersistentData = Persist::load_with_or_default(eeprom.reader());

  if my_data.number_of_boots == 0 {
    println!("It's the first time I've been booted!\n\n");
  } else {
    println!("I have been booted {} times before.\n\n", my_data.number_of_boots);
  }
  my_data.number_of_boots += 1;
 panic_if_err!(my_data.save_with(eeprom.writer()));

I/O device access

Simple access to IO devices (pins), including software debounced inputs:

  // Buttons and LEDs setup
  let green_button = Handle::new(OxideButton::using(Debouncer::with_pin(arduino.a2)));
  let yellow_button = Handle::new(OxideButton::using(Debouncer::with_pin(arduino.a3)));
  let blue_button = Handle::new(OxideButton::using(Debouncer::with_pin(arduino.a7)));

  let green_led = OxideLed::with_pin(arduino.d7);
  let yellow_led = Handle::new(OxideLed::with_pin(arduino.d8));
  let red_led = OxideLed::with_pin(arduino.d11);
  let blue_led = OxideLed::with_pin(arduino.d12);

Timer device support

Including both general purpose Timer/Counter and Real-Time Clock:

  // Clock setup
  let master_clock = Handle::new(OxideMasterClock::with_timer::<20>(arduino.timer0));
  let wall_clock = Handle::new(OxideWallClock::with_timer(arduino.rtc));

Event-Driven programming

Using an interrupt-driven implementation under the bonnet for power efficiency, you can simply write the code you want executed when the user clicks a button…

  // Set an event handler to be called every time someone presses the button
  green_button.on_click(Box::new(move |_pinid, _state|{
    green_led.toggle();
  }));

  // Print the time every time someone clicks the blue button
  let wall_clock_ref = wall_clock.clone();
  blue_button.on_click(Box::new(move |_pinid, state|{
    match state {
      ButtonState::Pressed => {
        println!("Total runtime: {:?} seconds", wall_clock_ref.runtime());
      }
      _ => {}
    }
  }));

…or on clock events…


  // An event handler every time the master clock ticks
  master_clock.on_tick(Box::new(move |_timerid, _duration|{
    red_led.toggle();
  }));

  // And another one for the once-a-second wallclock ticks
  wall_clock.on_tick(Box::new(move |_timerid, _duration|{
    blue_led.toggle();
  }));

…or after a delay:

  // Let's trigger some callbacks that will occur after a delay
  wall_clock.after_delay(Duration::from_secs(30), Box::new(move|_timerid|{
    println!("Thirty Seconds!\n");
  }));
  wall_clock.after_delay(Duration::from_secs(60), Box::new(move|_timerid|{
    println!("Sixty Seconds!\n");
  }));
  wall_clock.after_delay(Duration::from_secs(120), Box::new(move|_timerid|{
    println!("Two minutes (I'm bored now...)!\n");
  }));

  // How about "After you press the yellow button, I turn on the LED for 5 seconds"?
  let inner_wall_clock = wall_clock.clone();
  let inner_led = yellow_led.clone();
  yellow_button.on_click(Box::new(move |_pinid, state|{
    match state {
      ButtonState::Pressed => {
        let inner_inner_led = inner_led.clone();
        inner_led.set_on();
        inner_wall_clock.after_delay(Duration::from_secs(5), Box::new(move|_timerid|{
          inner_inner_led.set_off();
        }));
      }
      ButtonState::Released => {}
      ButtonState::Unknown => {}
    }
  }));

And, of course, the serial port drivers also support callbacks for serial communications.

Managed by a simple supervisor

Instantiate, tell it what devices to pay attention to, and run - that’s it:

  let supervisor = avr_oxide::oxide::instance();
  supervisor.listen_handle(green_button);
  supervisor.listen_handle(blue_button);
  supervisor.listen_handle(yellow_button);
  supervisor.listen_handle(master_clock);
  supervisor.listen_handle(wall_clock);

  supervisor.run();

More to come…

There is of course a lot more to develop - more serial devices (I2C, SPI etc.,) analogue/PWM output using timer devices, to name but two that are already on my to-do list - and almost certainly many bugs to squish. But, critically, I think the functionality above is the “Minimum Viable Product” to finally say that: AVRoxide is now ready to actually write useful applications.

Good luck!