Embedded programmering i Rust: Introduksjon

Glenn Bitar

Dette innlegget handler om embedded utvikling mot en BBC micro:bit med Embassy-rammeverket i Rust. Dette er noe jeg holder på med på fritida, og har lite å gjøre med arbeidet vi gjør i Amedia, men jeg tror det er mange som vil synes dette er spennende. Innlegget er en introduksjon til temaet, og vil følges opp av ett eller flere innlegg som demonstrerer forskjellige applikasjoner.

BBC micro:bit front BBC micro:bit back
En BBC micro:bit.

Embedded systemer

Embedded, eller «innvevde» systemer er maskinvare med programvare som er utviklet og tilpasset begrensa, spesifikke oppgaver. Uttrykket har i utgangspunktet et nokså stort spenn, og strekker seg fra skreddersydde kretskort med medfølgende mikrokontrollere, minne og komponenter, med fastvare som kjører direkte på prosessoren (bare metal), til mer generiske små datamaskiner med operativsystem. Jeg forbinder embedded med kretskort som har mikrokontroller(e) og meget begrensa ressurser: lite minne, treig prosessor og ingen MMU.

En Arduino Nano En Raspberry Pi versjon 3
Til venstre: En Arduino Nano. Denne har én CPU på 16 MHz, 2 kB RAM og, og kjører all kode på bare metal. Kilde. Til høyre: En Raspberry Pi 3. Denne har fire CPU-kjerner på 1,2 GHz, 1 GB RAM og kjører et fullverdig Linux operativsystem. Kilde. Begge disse kan sies å være embedded systemer, men det er tydelige forskjeller.

Siden man gjerne ikke har et operativsystem må man også håndtere ting som samtidighet (concurrency) og minneallokering selv. Faktisk må man også si ifra til linkeren om hvilke minneadresser som skal benyttes for kode, data, stack og heap ved hjelp av et linker skript.

De aller fleste embedded-systemer på dette nivået programmeres med C, og de som ikke programmeres med C, programmeres med C++. Det finnes mulighet for å bruke andre språk, som f.eks. MicroPython, Ada, Rust, med flere, og micro:bit kan faktisk programmeres med MakeCode, som er et visuelt programmeringsspråk i likhet med Scratch. Dersom du driver med mikrokontrollere profesjonelt, så er det overveiende sannsynlig at du jobber i C eller C++.

Et visuelt program i MakeCode
Et visuelt program i MakeCode.

micro:bit

micro:bit er et eksempel på et embedded system som er meget ressursbegrensa. Med sine 512 kB flash-minne og 128 kB RAM er det lite plass til et eventuelt operativsystem.[1] Dette betyr at programmene man skriver må flashes, som vil si at man overskriver hele mikrokontrollerens vedvarende (persistent) flash-minne.

Ombord på micro:bit finnes en Nordic nRF52833 mikrokontroller, samt mange nyttige komponenter, blant annet:

  • En 5x5 LED-matrise
  • To knapper
  • Mikrofon
  • Kombinert aksellerometer og kompass
  • Høyttaler

Alle disse komponentene er kobla til mikrokontrolleren, og man får tilgang til dem ved å skrive til og lese fra spesielle adresser i minnebussen, kalt registre. Når man jobber mot en mikrokontroller må man ofte bla opp i referansedokumentasjonen for å finne ut adressen til og den presise betydningen av verdiene som leses og skrives. F.eks., for å lese verdien av pinnen som er kobla til den ene av de to knappene på micro:bit, må man lese verdien av bit nr 16 i adresse 0x500000510, siden denne knappen er kobla til mikrokontrollerens GPIO-port 0, pin nr 16. Denne informasjonen er tilgjengelig i mikrokontrollerens datablad.[2]

Embedded-programmering i Rust

Som nevnt, så skjer det meste i embedded-verdenen i C og C++. Dette har delvis historiske grunner, men det er også fordi at, spesielt C, er et enkelt språk (som i grunnleggende, ikke lett å mestre) hvor koblinga mellom hvert uttrykk og de resulterende CPU-instruksjonene er nokså tett.

Det finnes derimot mer moderne språk hvor man kan bytte ut noe av denne «enkelheten» mot en hel del bekvemmelighet. Rust er et av disse, og det finnes flere grupper som produserer rammeverk i Rust som gjør programmering av mikrokontrollere til en morsom opplevelse.

I mitt hobbyprosjekt benytter jeg meg av Embassy-rammeverket, som bygger på async/.await-konseptene i Rust. Disse konseptene gjør det enkelt å bygge opp tilstandsmaskiner som muliggjør samtidighetsprogrammering. Som et eksempel, la oss si at vi har en mikrokontroller med to LEDs hvor vi ønsker at den ene LED-en skal blinke med en frekvens på to ganger i sekundet, mens den andre skal blinke én gang i sekundet. En naiv implementasjon i C kunne sett slik ut:

// implementations not included
void set_up_microcontroller();
void toggle_led_1();
void toggle_led_2();
void sleep_or_spin_for(uint32_t milliseconds);

void main() {
    set_up_microcontroller();
    uint8_t counter = 0;
    while 1 { // loop forever
        toggle_led_1();
        if (counter == 1) {
            toggle_led_2();
        }
        counter += 1;
        if (counter == 2) {
            counter = 0; // avoids overflow
        }
        sleep_or_spin_for(250);
    }
}

Her har vi altså én løkke som for alltid veksler tilstanden til LED-lysa. Man kan se på dette som en tilstandsmaskin, hvor tilstanden count veksles mellom 0 og 1, og verdien bestemmer hvorvidt man skal veksle led_2 eller ikke. Dette er en nokså enkel tilstandsmaskin, men man kan forstå at dersom man har flere oppgaver enn å blinke to LED-lys, kan det bli vanskelig å holde styr på tilstandene.

La oss se på et tilsvarende program i Rust med Embassy:

use embassy_time::Timer;
use embassy_executor::Spawner;
use library::{toggle_led_1, toggle_led_2};

#[embassy_executor::task]
async fn blink_led_1() {
    loop {
        toggle_led_1();
        Timer::after_millis(250).await;
    }
}

#[embassy_executor::task]
async fn blink_led_2() {
    loop {
        toggle_led_2();
        Timer::after_millis(500).await;
    }
}

#[embassy_executor::main]
async fn main(s: Spawner) {
    s.spawn(blink_led_1()).unwrap();
    s.spawn(blink_led_2()).unwrap();
    loop {
        // do nothing
        // could place one of the blink tasks here, but I want to show how we
        // can have multiple tasks
    }
}

I dette eksempelet er hver blinke-oppgave en egen, stand-alone task. Dette gjør at vi ikke trenger å håndtere en tilstand som kobler utførselen av oppgavene sammen.

I Rust-økosystemet og i Embassy-rammeverket finnes det pakker som abstraherer vekk skriving og lesing fra spesifikke registre i mikrokontrolleren (ala les bit 16 fra adresse 0x500000510 noen avsnitt opp) til typer og funksjoner med mer semantisk betydning. Disse pakkene kalles peripheral access crates og hardware abstraction layers. I tillegg finnes det såkalte board support packages som gjør denne jobben for kombinasjoner av komponenter og mikrokontrollere. Denne siden beskriver terminologien. For micro:bit finnes Rust-pakken microbit-bsp. Denne lar oss styre LED-matrisa og de fleste andre komponentene ombord på en enkel måte. Ta f.eks. en titt på dette programmet som viser piler på LED-matrisa avhengig av hvilken knapp trykkes på:

Demonstrasjon av et program som viser fram piler på micro:bit.

Følg med

I neste artikkel skal jeg vise fram en liten robot som heter Bit:Bot XL. Dette er et artig leketøy med to uavhengige hjul, lyssensorer, avstandssensor, med mer. Den styres av en micro:bit, så her er det mulighet for å lage en morsom leke.

En 4tronix Bit:Bot XL
En 4tronix Bit:Bot XL.

Læringsressurser

For å lære om embedded programmering i Rust finnes følgende ressurser:


  1. Man kan installere et sanntidsoperativsystem (RTOS), men det tar opp verdifulle ressurser, og ofte kan oppgaver løses uten et slikt. ↩︎

  2. Databladet til micro:bits nRF52833 er tilgjengelig her (~12 MB). ↩︎