No to kupiliśmy tego microbit dla dzieci i w ciągu dnia się do niego dopchać nie możemy, ale wieczorem... chyba nie pozwolimy, żeby się marnował? ;) Można w końcu sprawdzić, co do zaoferowania ma 'embedded rust'.

Żeby dowiedzieć się, czy nasz eksperyment się uda, musimy wyprodukować program, który np. będzie zapalał jedną z diod microbita (jeśli program nie będzie dawał oznak życia, ciężko będzie określić czy działa ;)).

Przygotowanie środowiska

Microbit posiada procesor ARM Cortex-M0.
Zgodnie z listą obsługiwanych platform potrzebny nam test target (obsługa specyficznej architektury)thumbv6m-none-eabi, dodajemy go:

rustup target add thumbv6m-none-eabi

Potrzebny nam jeszce projekt:

cargo new microbit-rust

w którym trzeba ustawić, żeby wykorzystywał zainstalowany wyżej target:

# cat microbit-rust/.cargo/config 
[build]
target = "thumbv6m-none-eabi"

Przydałaby się też biblioteka do obsługi microbita, dodajemy więc do [dependencies] w Cargo.toml:

microbit="~0.8.0"

Zmiany w programie pod embedded

no_std

Pierwsza kompilacja i oczywiście błąd:

error[E0463]: can't find crate for `std`
  |
  = note: the `thumbv6m-none-eabi` target may not be installed

Czyli na nasze: kompilator nie znalazł biblioteki standardowej w wersji dla wybranego target. Co więcej, zapewne go nie znajdzie, wybrany target jej nie posiada, w związku z tym potrzebujemy zmian w programie, na początku kodu dodajemy:

#![no_std]

w ten sposób informujemy, że nie będziemy korzystać z biblioteki standardowej. Trzeba także usunąć wywołanie println.

panic_handler

Kolejna kompilacja i kolejny błąd:

error: `#[panic_handler]` function required, but not found

Czyli kolejna funkcjonalność, którą normalnie obsługuje dla nas biblioteka standardowa. W środowiskach gdzie jej nie ma, trzeba ją czymś zastąpić. Funkcje obsługującą panic deklaruje się poprzez zdefiniowanie funkcji oznaczonej #[panic_handler], np:

#[panic_handler]
fn my_panic(_info: &core::panic::PanicInfo) -> ! {
    loop {}
}

można też skorzystać z jednej z gotowych bibliotek definiujących różne zachowania. Ja zdecydowałem się na panic-halt, czyli w Cargo.toml

panic-halt = "~0.2"

i w kodzie programu

extern crate panic_halt;

no_main

Kolejna kompilacja i kolejny błąd:

error: requires `start` lang_item

... już się pewnie domyślacie: nie ma biblioteki standardowej, w związku z tym nie ma nic, co by wiedziało, którą funkcję wywołać jako pierwszą w programie (np. main). Trzeba dać znać kompilatorowi, że sami będziemy o to dbać, poprzez dodanie adnotacji w programie:

#![no_main]

entry

Tak przygotowany program się skompiluje ale.. nie wykona żadnego kodu - nawet naszego main bo przecież chwilę wcześniej powiedzieliśmy mu, że sami zajmiemy się dostarczeniem informacji, która funkcja jest główną...

Rozwiązaniem jest wykorzystanie makra entry z biblioteki cortex-m-rt (bo przecież nasz microbit ma procesor Cortex M):

  • dodatkowa linia w Cargo.toml:
cortex-m-rt="0.6.12"
  • deklaracja użycia w main.rs:
extern crate cortex_m_rt;

use cortex_m_rt::entry;
  • zgodnie z dokumentacją oznaczamy nasz aktualny main jako entry i zmieniamy mu sygnaturę wskazując, że funkcja nigdy się nie skończy (oraz dodajemy pustą pętlę, która to zapewnia):
#[entry]
fn main() -> ! {
 loop {}
}

Całość się kompiluje bez błędów, dla pewności możemy sprawdzić plik wynikowy:

$ file microbit-rust/target/thumbv6m-none-eabi/debug/microbit-rust
 microbit-rust/target/thumbv6m-none-eabi/debug/microbit-rust: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped

Zapalanie led'a

Ledy w microbicie przypięte są do GPIO (General Purpose Input Output) - sterowanie nimi odbywa się poprzez ustawianie odpowiednich lini GPIO. Po szczegóły, która linia co oznacza, odsyłam do dokumentacji microbit lub art na temat Ledów w microbit. Na nasze potrzeby najważniejsza informacja to taka, że aby zapalić środkowego led'a trzeba na GPIO14 ustawić stan wysoki a GPIO6 stan niski.

#![no_std]
#![no_main]

extern crate panic_halt;
extern crate cortex_m_rt;

use cortex_m_rt::entry;

use microbit::Peripherals;
use microbit::hal::prelude::*;

#[entry]
fn main() -> ! {
    let p = Peripherals::take().unwrap();
    let gpio = p.GPIO.split();
    let mut led = gpio.pin14.into_push_pull_output();
    let _ = gpio.pin6.into_push_pull_output();
    let _ = led.set_high();

    loop {}
}

W stosunku do wcześniej omawianych zmian mamy dwie nowe linijki use:

  • use microbit::Peripherals; - żebyśmy mogli użyć obiektu Peripherals
  • use microbit::hal::prelude::*; - biblioteka microbit wykorzystuje pod spodem (i eksportuje pod zmienioną nazwą), bibliotekę nrf51_hal. Zamiast pisać dla każdej potrzebnej rzeczy osobne use możemy zaimportować [wcześniej przygotowane predulude] (https://github.com/nrf-rs/nrf51-hal/blob/master/src/prelude.rs), co spowoduje automatyczny import wszystkich najczęściej używanych klas.

Tak naprawdę wygląda na to, że większość funkcji biblioteki microbit pochodzi z nrf51-hal...

Co do funkcji main:

  • let p = Peripherals::take().unwrap(); - zgodnie z dokumentacją pobieramy strukturę zawierająca wszystkie urządzenia zewnętrzne microbita
  • let gpio = p.GPIO.split(); - czyli pobranie zmiennej ze wszystkimi definicjami GPIO
  • let mut led = gpio.pin14.into_push_pull_output(); oraz let _ = gpio.pin6.into_push_pull_output(); to pobranie obiektów reprezentujących konkretne linie GPIO. Po pobraniu linie mają ustawione stan niski.
  • let _ = led.set_high(); ustawiamy stan wysoki na określonej linii...

Wgrywanie

Wygenerowany plik ELF można przekonwertować do pliku *.hex:

$ arm-none-eabi-objcopy -O ihex microbit-rust microbit-rust.hex

...który już standardowo możemy skopiować na microbita.

na systemach debianopochodnych wymieniona niżej komenda jest częścią pakietu binutils-arm-none-eab

Niestety po skopiowaniu nic nie działa...

Hm... rozmiar pliku hex to całe 13 bajtów, to raczej za mało na jakikolwiek sensowny program. Po dłuższych poszukiwaniach okazuje się, że biblioteka cortex-m-rt wymusza użycie określonych opcji linkera. Dodatkowa zawartość .cargo/config

[target.thumbv6m-none-eabi]
rustflags = [
    "-C", "link-arg=-Tlink.x",
]

link.x to tkzw linker script generowany podczas budowania bibliotek wsparcia dla określonych procesorów/płytek rozwojowych. Biblioteka cortex-m-rt dostarcza taki plik, a w nim defincje układu pamięci, kodu obsługującego przerwania etc

Po następnej kompilacji i konwersji na *.hex plik ma już 9,1K (czyli całkiem realnie), a po skopiowaniu na urządzenie zapala wybrany przez nas led.

Podsumowanie

Mamy zestawione i sprawdzone środowisko do programowania w embedded rust na microbit'a. Idealna baza do dalszych eksperymentów :)

Repozytorium z kodem: microbit-rust

Literatura