Drone is an Embedded Operating System for writing real-time applications in Rust. It aims to bring modern development approaches without compromising performance into the world of embedded programming.

Quick Tour

cargo takes development comfort to the next level, and Drone extends its philosophy to the embedded world. If your target chip and debug probe are supported by Drone, you can flash your first program and get a feedback right away!

$ drone new --toolchain nightly-2020-04-30 --probe bmp --device stm32f103 --flash-size 128K --ram-size 20K hello-world
     Created binary (application) `hello-world` package
     Removed src/main.rs
     Created src/bin.rs
     Created src/lib.rs
     Created src/thr.rs
     Created src/tasks/mod.rs
     Created src/tasks/root.rs
     Patched Cargo.toml
     Created Drone.toml
     Created Justfile
     Created rust-toolchain
     Created .cargo/config
     Patched .gitignore
$ cd hello-world
$ just deps
rustup target add thumbv7m-none-eabi
info: downloading component 'rust-std' for 'thumbv7m-none-eabi'
info: installing component 'rust-std' for 'thumbv7m-none-eabi'

$ just flash drone env thumbv7m-none-eabi -- cargo build --features "" --release Updating crates.io index Compiling proc-macro2 v1.0.12 Compiling unicode-xid v0.2.0 Compiling syn v1.0.21 Compiling serde v1.0.110
Compiling hello-world v0.1.0 (/home/valff/hello-world) Finished release [optimized + debuginfo] target(s) in 1m 20s drone flash target/thumbv7m-none-eabi/release/hello-world
Start address 0x8000040, load size 6712 Transfer rate: 15 KB/sec, 745 bytes/write.
$ just log drone log --reset :0:1
================================== LOG OUTPUT ================================== Hello, world!

Each interrupt is an executor for async tasks. Thanks to Rust's zero-cost asynchronous programming your interrupt handlers look like a conventional synchronous code, except they don't need separate stacks.

pub async fn handler(input: Input) -> Result<(), Error> {
    let Input { mut i2c1, exti4, gpio_b, gpio_c } = input;
    // APDS-9960 interrupt events stream read from B4 pin.
    let mut exti4_stream = exti4.create_saturating_stream();
    let mut apds9960 = Apds9960Drv::init();
    let mut gestures = Gestures::init(&mut apds9960, &mut i2c1).await?;
    // Wait for a falling edge trigger on PB4.
    while exti4_stream.next().await.is_some() {
        // Repeat until PB4 is back to the high level.
        while !gpio_b.gpio_idr.load().idr4() {
            // Read APDS-9960 FIFO buffer.
            match gestures.advance(&mut apds9960, &mut i2c1).await? {
                // Turn on the LED.
                Some(Gesture::Up) => gpio_c.gpio_bsrr.store(|r| r.set_br13()),
                // Turn off the LED.
                Some(Gesture::Down) => gpio_c.gpio_bsrr.store(|r| r.set_bs13()),
                _ => {}
            }
        }
    }
    Ok(())
}

Drone includes a dynamic memory allocator that allows you to use familiar Rust's Box, Vec, String, Arc, and other dynamic types. It is lock-free, deterministic, and has a small code footprint, which makes it useful even on simplest micro-controllers. The cost is that it requires tuning for each particular application. Drone automates this by providing utilities for collecting real-time allocator statistics and calculating an optimized layout configuration.

$ just features=heaptrace flash

$ just heaptrace
^C $ drone heap generate --pools 8 Block Size | Max Load | Total Allocations ------------+----------+------------------- 1 | 7 | 7 8 | 3 | 748 12 | 11 | 756 28 | 3 | 748 56 | 1 | 87 68 | 1 | 220 128 | 1 | 1 152 | 1 | 1 Maximum heap load: 651 / 1.99% =============================== OPTIMIZED LAYOUT =============================== [heap] size = "32K" pools = [ { block = "4", capacity = 522 }, { block = "12", capacity = 515 }, { block = "28", capacity = 289 }, { block = "56", capacity = 143 }, { block = "68", capacity = 90 }, { block = "152", capacity = 15 }, ] # fragmentation: 36 / 0.11% # hint: replace the existing [heap] section in Drone.toml

Drone provides a rich API for working safely with memory-mapped registers. An application starts with a set of zero-sized unique tokens for all available registers. A token can have move or copy semantics, can be shareable with atomic access or non-shareable with non-atomic access, can be split into individual register field tokens.

use crate::consts::{FLASH_WS, PLL_M, PLL_N, PLL_P};
use drone_cortexm::reg::prelude::*;
use drone_stm32_map::reg;

fn init_sys_clk(
    mut rcc_cr: reg::rcc::Cr<Urt>,
    mut rcc_pllcfgr: reg::rcc::Pllcfgr<Urt>,
    mut rcc_cfgr: reg::rcc::Cfgr<Urt>,
    mut flash_acr: reg::flash::Acr<Urt>,
) {
    rcc_cr.modify(|r| r.set_hseon()); // HSE clock enable
    while !rcc_cr.load().hserdy() {} // HSE clock ready flag

    rcc_pllcfgr.store(|r| {
        r
            // HSE oscillator clock selected as PLL and PLLI2S clock entry
            .set_pllsrc()
            // division factor for PLL and PLLI2S input clock
            .write_pllm(PLL_M)
            // PLL multiplication factor for VCO
            .write_plln(PLL_N)
            // PLL division factor for main system clock
            .write_pllp(PLL_P / 2 - 1)
    });
    rcc_cr.modify(|r| r.set_pllon()); // PLL enable
    while !rcc_cr.load().pllrdy() {} // PLL clock ready flag

    flash_acr.store(|r| {
        r
            // the ratio of the CPU clock period to the Flash memory access time
            .write_latency(FLASH_WS)
            // data cache is enabled
            .set_dcen()
            // instruction cache is enabled
            .set_icen()
            // prefetch is enabled
            .set_prften()
    });
    while flash_acr.load().latency() != FLASH_WS {}

    rcc_cfgr.store(|r| {
        r
            // PLL selected as system clock
            .write_sw(0b10)
            // system clock not divided
            .write_hpre(0b0000)
            // APB1 = AHB / 2
            .write_ppre1(0b100)
            // APB2 = AHB / 1
            .write_ppre2(0b000)
    });
    while rcc_cfgr.load().sws() != 0b10 {} // PLL used as the system clock
}

Drone uses interrupt-based preemptive priority scheduling, where tasks with same priorities are executed cooperatively. An application has a predefined number of threads corresponding to hardware interrupts, but each thread can run dynamic number of fibers.

use crate::thr;
use drone_cortexm::{fib, reg::prelude::*, thr::prelude::*};
use drone_stm32_map::reg;

async fn enable_hse_clock(
    rcc_cir: reg::rcc::Cir<Srt>,
    rcc_cr: reg::rcc::Cr<Srt>,
    thr_rcc: thr::Rcc,
) {
    // We need to move ownership of `hserdyc` and `hserdyf` into the following
    // fiber.
    let reg::rcc::Cir { hserdyc, hserdyf, .. } = rcc_cir;
    // Attach a listener that will notify us when RCC_CIR_HSERDYF is asserted.
    let hserdy = thr_rcc.add_future(fib::new_fn(move || {
        if hserdyf.read_bit() {
            hserdyc.set_bit();
            fib::Complete(())
        } else {
            fib::Yielded(())
        }
    }));
    // Enable the HSE clock.
    rcc_cr.modify(|r| r.set_hseon());
    // Sleep until RCC_CIR_HSERDYF is asserted.
    hserdy.await;
}

Registers and individual register fields can be grouped into peripheral blocks. Drone makes a great effort to abstract from different instances of one peripheral type. Even if these instances have minor differences.

use drone_cortexm::reg::prelude::*;
use drone_stm32_map::periph::gpio::{
    periph_gpio_a3, periph_gpio_b14,
    pin::{GpioPinMap, GpioPinPeriph},
};

/// TPS22917 Load Switch.
pub struct LoadSwitch<T: GpioPinMap> {
    pin: GpioPinPeriph<T>,
}

impl<T: GpioPinMap> LoadSwitch<T> {
    /// Sets up a new [`LoadSwitch`].
    pub fn init(pin: GpioPinPeriph<T>) -> Self {
        pin.gpio_moder_moder.write_bits(0b01); // general purpose output
        Self { pin }
    }

    /// Turns the switch on.
    pub fn on(&self) {
        self.pin.gpio_bsrr_bs.set_bit(); // tie the pin to +3.3v
    }

    /// Turns the switch off.
    pub fn off(&self) {
        self.pin.gpio_bsrr_br.set_bit(); // tie the pin to ground
    }
}

pub fn handler(reg: Regs) {
    let sd_card_switch = LoadSwitch::init(periph_gpio_a3!(reg));
    let ble_switch = LoadSwitch::init(periph_gpio_b14!(reg));
    sd_card_switch.on();
    ble_switch.on();
}

Drone provides an abstract logging facade designed after ARM Serial Wire Output. The output can be captured with a generic USB-UART adapter. It has 32 multiplexed streams and supports atomic packets up to 4 bytes. Familiar Rust macros like print!, eprint!, dbg! are mapped to reserved #0 and #1 ports that work as standard output and standard error respectively.

use drone_core::log::Port;

fn handler() {
    // Familiar output macros just work!
    println!("Hello, world!");
    dbg!(123);

    // You can send raw bytes to different ports.
    Port::new(2).write_bytes(&[1, 2, 3]);

    // If a port accessed concurrently, you can send indivisible packets up to 4
    // bytes length.
    Port::new(3).write::<u32>(0xABFF_FFCD).write::<u32>(0xDCFF_FFBA);
}

Drone provides safe API for inherently unsafe low-level operations. The only required unsafe function is the entry-point located at src/bin.rs, which is a separate compilation unit. You can even write a whole micro-controller firmware with #![forbid(unsafe_code)] at the top of src/lib.rs.

#[no_mangle]
#[naked]
pub unsafe extern "C" fn reset() -> ! {
    mem::bss_init();
    mem::data_init();
    processor::fpu_init(true);
    tasks::root(Regs::take(), ThrsInit::take());
    loop {
        processor::wait_for_int();
    }
}

Design Principles