embassy book

Embassy Basic Application

You have run some examples, what's next? Let's understand Embassy better through a simple Embassy application for nRF52 DK.

Main Program

The complete example can be found here.

> Note: If you are using VS Code and rust-analyzer to view and edit examples, you may need to make some modifications to .vscode/settings.json to tell it which project we are using. Please follow the instructions in the comments in that file to ensure rust-analyzer works properly.

Bare-metal Programming

You will first notice two attributes at the top of the file. These attributes tell the compiler that the program has no standard library access and no main function (because it is not run by the operating system).

#![no_std]
#![no_main]

Error Handling

Next are some declarations about how to handle panic and fault. During development, a good practice is to rely on defmt-rtt and panic-probe to print diagnostic information to the terminal:

use {defmt_rtt as _, panic_probe as _}; // Global logger

Task Declaration

After some import declarations, the task to be run by the application should be declared:

#[embassy_executor::task]
async fn blinker(mut led: Output<'static>, interval: Duration) {
    loop {
        led.set_high();
        Timer::after(interval).await;
        led.set_low();
        Timer::after(interval).await;
    }
}

Embassy tasks must be declared as async and cannot have generic parameters. In this example, we pass in the LED to blink and the blink interval.

> Note: There is no busy waiting in this task. It uses the Embassy timer to yield execution, allowing the microcontroller to sleep between blinks.

Main Function

The main entry point of the Embassy application is defined using the #[embassy_executor::main] macro. The entry point receives a Spawner parameter, which can be used to spawn other tasks.

Then we initialize HAL with the default configuration, which gives us a Peripherals struct, which we can use to access various peripherals of the MCU. In this example, we want to configure one of the pins as a GPIO output to drive the LED:

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_nrf::init(Default::default());

    let led = Output::new(p.P0_13, Level::Low, OutputDrive::Standard);
    unwrap!(spawner.spawn(blinker(led, Duration::from_millis(300))));
}

What happens when the blinker task is spawned and main returns? In fact, the main entry point is like any other task, except that you can only have one, and it requires some specific type parameters. The magic lies in the #[embassy_executor::main] macro. This macro does the following:

  1. Create an Embassy executor
  2. Define a main task for the entry point
  3. Run the executor and spawn the main task

There is also a way to run the executor without using macros, in which case you need to create the Executor instance yourself.

Cargo.toml

The project definition needs to include embassy dependencies:

embassy-executor = { version = "0.7.0", path = "../../../embassy-executor", features = ["defmt", "arch-cortex-m", "executor-thread"] }
embassy-time = { version = "0.4.0", path = "../../../embassy-time", features = ["defmt"] }
embassy-nrf = { version = "0.3.1", path = "../../../embassy-nrf", features = ["defmt", "nrf52840", "time-driver-rtc1", "gpiote"] }

Depending on your microcontroller, you may need to replace embassy-nrf with other libraries (e.g. STM32 uses embassy-stm32. Remember to update the feature flags at the same time).

In this specific example, the nrf52840 chip is selected, and the RTC1 peripheral is used as the time driver.