bbc:microbit i rust
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
microbit
a (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
jakoentry
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ć obiektuPeripherals
use microbit::hal::prelude::*;
- biblioteka microbit wykorzystuje pod spodem (i eksportuje pod zmienioną nazwą), bibliotekęnrf51_hal
. Zamiast pisać dla każdej potrzebnej rzeczy osobneuse
możemy zaimportować [wcześniej przygotowanepredulude
] (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ętrznemicrobit
alet gpio = p.GPIO.split();
- czyli pobranie zmiennej ze wszystkimi definicjami GPIOlet mut led = gpio.pin14.into_push_pull_output();
orazlet _ = 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
- Opis sprzętu microbit
- Szablon do programów na procesor Cortex-M
- Biblioteka do obsługi microbit
- The Embedded Rust - dokumentacja związana z projektem embedded rust
- MicroRust - (mocno niekompletna) książka na temat rust na microbit