Rust på RP2040

Eivind Bergem publisert
6 min, 1152 ord

Kategorier: Rust

I januar 2021 lanserte Raspberry Pi Foundation mikrokontrolleren RP2040. Dette er første gang de lager sin egen silikon. Den enkleste måten å komme i gang med RP2040 er å kjøpe en Raspberry Pi Pico, som er et lite dev-kort med USB-connector, 2MB flash, én LED som kan blinkes og headere koblet til restene av pinnene på RP2040.

RP2040 har to ARM Cortex-M0+ kjerner og 256 KB SRAM. Det mest unike med RP2040 er PIO – programmerbar IO – som gjør det mulig å programmere IO-funksjonalitet med et eget instruksjonssett. Det er kraftfullt nok til at UART, I2C og SPI kan implementeres med det.

Rust på RP2040

Vi ønsker selvsagt å kjøre Rust-kode på RP2040, og det er veldig enkelt pågrunn av de gode verktøyene rundt Rust. For å komme i gang kan du ta utgangspunkt i denne malen som gir deg et ferdig eksempel som blinker LED'en på kortet.

Hovedloopen i dette programmet skrur av og på LEDen med pauser på 500ms i mellom:

loop {
    info!("on!");
    led_pin.set_high().unwrap();
    delay.delay_ms(500);
    info!("off!");
    led_pin.set_low().unwrap();
    delay.delay_ms(500);
}

Det å få en LED er kanskje ikke så veldig imponerende, men det er en god måte å verifisere at alt fungerer som det skal.

PIO

I det enkle blinky-eksempelet brukes CPU'en til å styre LED'en. Men vi kan også styre LED'er helt uten bruk av CPU'en. Det finnes flere måter å gjøre det på, blant annet ved bruk av PWM, men det vi skal gjøre er å få den til å blinke ved hjelp av PIO.

Instruksjoner

PIO er unikt for RP2040 og gjør det mulig å programmere GPIO'er med et eget instruksjonssett som gir deg presis kontroll over klokkesykluser. PIO programmeres med en egen assembly – kalt pioasm – som gjøres om til binærkode. Instruksjonssettet består av 9 instruksjoner, men vi skal bare se på to av dem:

  • jmp (<cond>) <target> ([delay]) – Setter programpekeren til en adressen merkelappen target peker på hvis cond er sann. Om cond droppes vil programpekeren alltid settes.
  • set <destination>, <value> ([delay]) – Kan drive en pinne eller skrive til et register. value er opptil 5 bit, så man kan styre opp til 5 pinner i en instruksjon. destination kan for våre formål være:
    • pins – For å styre ut-verdien til pinner.
    • pindir– For å konfigurere pinne som input eller output.

Klokkesykluser

Hver PIO intruksjon tar én klokkesyklus. Det er også mulig å legge til ekstra klokkeyskluser med [delay]. F.eks. kan vi sette en pinne til høy og vente i 5 sykluser:

set pins, 1 [5]

Hele instruksjonen vil ta 6 sykluser, én for selve instruksjonen og 5 sykluser pause. Selve handlingen blir gjort før pausen.

PIO kan styre opp til 5 pinner av gangen. Selve PIO-koden inneholder ingen referanser til spesifikke pinner, dette konfigureres på tilstandsmaskinen som eksekverer programmet.

Merkelapper

Vi trenger også en merkelapp for å markere starten på en løkke. De har følgende syntax:

[public] <symbol>:

Programmering

Da har vi alt vi trenger for å blinke en LED med PIO. Vi starter med å konfigurere pinnen som output:

set pindirs, 1

Deretter kan vi styre pinnen:

set pins, 0 [31]
set pins, 1 [31]

Vi legger inn en pause på 31 sykluser, slik at LED'en er av i 32 sykluser og på i 32 sykluser. Hvor lang en syklus er kommer an på klokkefrekvensen vi kjører på. PIO har en klokkedeler som kan brukes til å kjøre en klokke med lavere frekvens enn systemklokken.

Nå har vi fått LEDen til å skru seg av og på én gang, men for at dette skal være en fullstendig blinky må vi få den til å blinke flere ganger. Det kan vi gjøre ved hjelp av en løkke:

<merkelapp>:
    [... gjør noe her ...]
    jmp <merkelapp>

Når vi kommer til jmp-instruksjonen hopper programmet tilbake til neste instruksjon etter merkelappen.

Da har vi et fullstendig program:

set pindirs, 1
loop:
        set pins, 0 [31]
        set pins, 1 [30]
        jmp loop

Vi har satt delay den siste set-instruksjonen til 30 fordi jmp-instruksjonen også tar én klokkesyklus.

PIO blinky i Rust

Vi tar utgangspunkt i blinky-eksemplet og modifiserer dette til å blinke LED'en ved hjelp av PIO. I eksempelet konfigureres RP2040 til å bruke en ekstern klokke og kjøre på høyeste frekvens (133 MHz):

// External high-speed crystal on the pico board is 12Mhz
let external_xtal_freq_hz = 12_000_000u32;
let clocks = init_clocks_and_plls(
    external_xtal_freq_hz,
    pac.XOSC,
    pac.CLOCKS,
    pac.PLL_SYS,
    pac.PLL_USB,
    &mut pac.RESETS,
    &mut watchdog,
)
.ok()
.unwrap();

Fordi vi skal blinke en LED i menneskelig tid, blir dette alt for graskt. RP2040 har også en intern klokke som er mye mindre nøyaktig enn én ekstern krystall, men mer enn nøyaktig nok for vårt lille eksempel. Ved å kommentere ut klokkekonfigurasjonen, vil RP2040 være konfigurert med den interne klokken og kjøre på rundt 6 MHz.

PIO har en 16-bit integer og 8-bit fraktal klokkedeler, så vi kan maksimalt dele klokken på 65536. Dette gir oss en frekvens på ca. 92 Hz, som vil si at hver syklus varer i ca. 10 millisekunder, noe som er litt i korteste laget for oss mennesker. Men, fordi vi har lagt inn pauser er LEDen på og av i 32 sykluser av gangen, som vil tilsi ca. 350 millisekunder, noe som er ganske så nærme de 500 millisekundene i det originale blinky eksempelet. Merk at fordi den interne klokken er veldig unøyaktig, så er dette grove estimater.

For å gjøre om pioasm til binærkode kan man bruke et verktøy – også kalt pioasm – som følger med i pico-sdk. Men, fordi vi bruker Rust slipper vi å fikle med pioasm fordi vi kan skrive pioasm inline i Rust-kildekode ved hjelp av en makro:

let program = pio_asm!(
    "
    set pindirs, 1
    loop:
        set pins, 0 [31]
        set pins, 1 [30]
        jmp loop
    "
);

Den fullstendige koden finner du på github. Se detaljerte instruksjoner i repoet for hvordan å bygge prosjektet.

Kjøre kode på RP2040

For å kjøre koden vår på RP2040 må vi kopiere den over til flash. RP2040 har en innebygget USB bootloader i ROM, så vi kan programmere den uten en ekstern debug-probe. Vi kan bruke verktøyet elf2uf2-rs for å programmere RP2040 fra kommandolinjen. For at elf2uf2-rs skal fungere må RP2040 være i USB bootloader modus. Dette gjør du ved å holde inne den lille knappen på brettet mens du kobler til USB-kabelen. Etter du har koblet til kan du slippe knappen. RP2040 skal da dukke opp på maskinen din som en USB storage device.

Prosjektet er satt opp slik at vi kan kjøre én enkelt kommando for å bygge og kjøre koden vår fra RP Pico:

$ cargo run

Hvis alt har gått som det skal skal LEDen på brettet begynne å blinke:

Blinky