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:
- Create an Embassy executor
- Define a main task for the entry point
- 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.