Cet exemple est très largement inspiré des exemples dans l’HAL en Rust pour les chips Nordic mais adapté au devkit nRF52833.
Introduction
Rust fonctionne de paire avec cargo
, qui est un outil de gestion de package pour Rust, ainsi qu’un build system qui permet d’appeler le compilateur rustc
ou les binutils.
Voir la documentation de cargo pour en savoir plus.
Ici je veux juste me concentrer sur comment faire un rapide prototypage d’une application bare-metal en Rust en utilisant des packages existant.
Développement avec Rust
Pour créer un nouveau projet avec cargo
:
$ cargo new nrf52833
Cargo génère les fichiers nécessaire pour un nouveau projet, l’arborescence est très simple:
$ cd nrf52833/ $ tree . ├── Cargo.toml └── src └── main.rs
Cargo.toml est un manifeste qui définis tout les metadata et package dépendant à un projet que Cargo doit compiler
[package] name = "nrf52833" version = "0.1.0" authors = ["Sebastien Bourdelin sebastien.bourdelin@gmail.com"] edition = "2018" [dependencies]
Les packages disponibles peuvent être trouvé sur https://crates.io ou en utilisant cargo
directement
$ cargo search <mon_package>
Les packages Rust pour nRF52
Il éxiste notament des packages permettant de nous abstraire la plateforme sur laquelle on développe et permettre de faire un rapide prototype.
La board nRF52833 est basé autour d’un cortex M4 et plusieurs crates maintenue par la cortex M team permette de faciliter le développement sur ce processeur.
- cortex-m fourni une abstration bas niveau au processeur comme la manipulation des interruptions, un setter de breakpoints hardware, le bootstraping, les syscalls, etc, c’est principalement un wrapper vers les call assembleur équivalent avec les barrières mémoires qui vont bien.
- cortex-m-rt lui est plus un facilitateur de développement sur les cortex M, en proposant par example des vecteurs d’interruptions par défaut.
- nRF52833-hal est comme son nom l’indique une HAL cette fois pour la board et ses périphériques, leds, GPIOs, etc..
- embedded-hal fournis des fonctions génériques pour manipuler des GPIOs, en utilisant la feature
unproven
on a accès à des fonctions commeis_high()
,is_low()
,set_high()
,set_low()
pour manipuler les GPIOs. - panic-halt fournis un simple panic handler, plusieurs crates sont disponibles suivant le comportement qu’on veut.
[package] name = "nrf52833" version = "0.1.0" authors = ["Sebastien Bourdelin sebastien.bourdelin@gmail.com"] edition = "2018" See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] cortex-m = "0.7.2" cortex-m-rt = "0.6.14" panic-halt = "0.2.0" nrf52833-hal = "0.12.2" [dependencies.embedded-hal] version = "0.2.3" features = ["unproven"]
Le code Rust de l’application
La source finale ressemble à ça:
#![no_std] #![no_main] use panic_halt as _; use embedded_hal::digital::v2::InputPin; use embedded_hal::digital::v2::OutputPin; use cortex_m_rt::entry; use nrf52833_hal as hal; use nrf52833_hal::gpio::Level; #[entry] fn main() -> ! { let p = hal::pac::Peripherals::take().unwrap(); let port0 = hal::gpio::p0::Parts::new(p.P0); let button = port0.p0_11.into_pullup_input(); let mut led = port0.p0_13.into_push_pull_output(Level::Low); loop { if button.is_high().unwrap() { led.set_high().unwrap(); } else { led.set_low().unwrap(); } } }
Il y a plusieurs choses intéressantes:
#![no_std] #![no_main]
no_std permet de signifier qu’on ne va pas utiliser les primitives de la libstd
, on est sur du bare-metal, il n’y a pas d’OS pour nous fournir l’abstraction de la Librairie Standard
.
no_main permet de ne pas exporter le symbole main
dans la table des symboles de notre application. Pareillement on est sur du bare-metal, notre application sera un blob, pas un ELF, PE ou autre, il n’a pas de loader
d’executable, on définira donc notre entry point à la main.
Le reste du code est assez je pense compréhensible.
On définis nos includes:
use panic_halt as _; use embedded_hal::digital::v2::InputPin; use embedded_hal::digital::v2::OutputPin; use cortex_m_rt::entry; use nrf52833_hal as hal; use nrf52833_hal::gpio::Level;
Notre entry point avec la logique associé:
#[entry] fn main() -> ! { let p = hal::pac::Peripherals::take().unwrap(); let port0 = hal::gpio::p0::Parts::new(p.P0); let button = port0.p0_11.into_pullup_input(); let mut led = port0.p0_13.into_push_pull_output(Level::Low); loop { if button.is_high().unwrap() { led.set_high().unwrap(); } else { led.set_low().unwrap(); } } }
Toute l’abstraction pour accéder aux périphériques est fournis par le crate nrf52833_hal, pour le dev kit nrf52833 la GPIO pour le bouton1 est P0_11 et pour la led1 P0_13, ces informations sont disponibles dans la datasheet.
Un point intéressant est le keyword mut
utilisé pour notre variable qui tiens l’état de la led
.
Par défaut, Rust définis toutes les variables comme immuables, c’est à dire qu’une fois déclaré avec une valeur associé on ne peut pas changer sa valeur sinon le compilateur rustc
va considérer ça comme une erreur. L’utilisation du keyword mut
permet de changer ça.
Memory layout et Linker
Le crate cortex-m-rt s’attends à trouver un fichier memory.x
à la racine du projet qui va définir notre memory layout.
Dans mon cas, pour la board nRF52833, le memory layout est le suivant (note: je n’utilise pas de softdevice):
MEMORY { FLASH : ORIGIN = 0x00000000, LENGTH = 512K RAM : ORIGIN = 0x20000000, LENGTH = 128K }
Le fichier .cargo/config
Enfin il nous manque à définir l’architecture cible de notre board. Basé autour d’un Cortex M4 avec FPU, on utilisera le jeu d’instruction thumbv7em-none-eabihf.
On peut le faire à l’invocation de cargo
, mais pour éviter d’avoir à le répéter à chaque fois on peut utilise le fichier .cargo/config.
Mon fichier config ressemble à ça:
[target.'cfg(all(target_arch = "arm", target_os = "none"))'] rustflags = [ # This is needed if your flash or ram addresses are not aligned to 0x10000 in memory.x # See https://github.com/rust-embedded/cortex-m-quickstart/pull/95 "-C", "link-arg=--nmagic", # LLD (shipped with the Rust toolchain) is used as the default linker "-C", "link-arg=-Tlink.x", ] [build] target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU)
Execution de l’application
Compilation avec Cargo
Pour compiler l’application:
$ cargo build
Cargo va lancer la compilation de toutes les dépendances nécessaire en utilisant le jeu d’instruction que l’on a définis
Pour en apprendre plus sur l’executable généré, il va nous falloir les binutils:
cargo install cargo-binutils
Pour voir les sections (je n’ai pas stripper les symboles de debug):
$ cargo size -- -A Finished dev [unoptimized + debuginfo] target(s) in 0.02s nrf52833 : section size addr .vector_table 256 0x0 .text 4480 0x100 .rodata 688 0x1280 .data 0 0x20000000 .bss 4 0x20000000 .uninit 0 0x20000004 .debug_abbrev 23145 0x0 .debug_info 402649 0x0 .debug_aranges 21944 0x0 .debug_ranges 117568 0x0 .debug_str 457066 0x0 .debug_pubnames 96111 0x0 .debug_pubtypes 103941 0x0 .ARM.attributes 58 0x0 .debug_frame 66420 0x0 .debug_line 261736 0x0 .debug_loc 1748 0x0 .comment 109 0x0 Total 1557923
Flasher la board nRF52833
Le fichier compilé par cargo
est un ELF
:
$ file target/thumbv7em-none-eabihf/debug/nrf52833 target/thumbv7em-none-eabihf/debug/nrf52833: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped
Il nous faut un blob binaire afin que l’application soit exécutable par la target, pour ça on peut utiliser les binutils
pour convertir notre ELF
:
$ cargo objcopy -- -O ihex mon_app.hex
Enfin pour flasher l’application sur la board, j’utilise personnellement l’outils nrfjprog
de nordic:
$ nrfjprog -f nrf52 --program mon_app.hex --chiperase --verify
Il ne reste plus qu’à reset la board:
$ nrfjprog -f nrf52 --reset
Et la led1
devrait s’allumer en utilisant le bouton1
:
Code source
Les sources du projet sont sur github.