Application bare-metal en Rust pour nRF52833

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 comme is_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]

/* define a default panic handler */
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(); /* button 1 */
    let mut led = port0.p0_13.into_push_pull_output(Level::Low); /* led 1 */

    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(); /* button 1 */
    let mut led = port0.p0_13.into_push_pull_output(Level::Low); /* led 1 */

    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.