Introduction to Embassy

Overview of the Embassy framework.

Previous post: Pico 2 as debugprobe.

Minimal Embassy example

It can be useful to start with a minimal Embassy program. The following does nothing but can serve as a template for future programs.

#![no_std]
#![no_main]

use defmt_rtt as _;
use embassy_executor::{Spawner, main};
use embassy_rp::config::Config;
use panic_probe as _;
use embassy_rp::bind_interrupts;

bind_interrupts!(struct Irqs {
    PIO0_IRQ_0 => InterruptHandler<PIO0>;
});

#[main]
async fn main(_spawner: Spawner) -> ! {
   let p = embassy_rp::init(Config::default());
   loop {
      embassy_futures::yield_now().await;
   }
}

As you can see, there are two notable attributes at the top of the file.

Then there are two use x as _; lines. These crates don’t expose functions or public modules to be used, but they contain setup code that should be included at least once in your embedded program.

There is a macro call embassy_rp::bind_interrupts! that binds hardware interrupts with the Embassy framework. This is necessary to be able to use hardware interrupts in your program. Hardware interrupts can stop the current ongoing computation and jump execution to some handler code elsewhere. Examples of hardware interrupt bindings available on the Pico 2 are:

The spawner argument allows users to spawn asynchronous tasks. Keep in mind, however, that each task should be non-generic and completely specified at compile time. This is because the Embassy framework does not support dynamic task creation at runtime.

The last line loop { yield_now().await } may seem unnecessary. The reason I have to write it is because the return type of main is “never” (written as !). The never return type is the type for a function that never returns.

Because of the signature of main, we cannot simply escape the main function. Running this program is the only thing that happens on the microcontroller. So we have to keep looping, even if we have already finished our work.

Levels of Abstraction in Embedded Rust

This section provides an overview of the different levels of abstraction that can be used when programming microcontrollers in Rust.

Low Level

The lowest level of software abstraction provides direct access to the microcontroller’s hardware registers.

The Embassy framework builds on top of the PAC and HAL to provide a more intuitive and convenient API for accessing the hardware.

Medium Level

If the Embassy framework doesn’t suit your needs, you can fall back to a more conventional level of abstraction without async/await.

The Hardware Abstraction Layer (HAL) is a more convenient way to access the hardware. It provides a higher level of abstraction than the PAC but still allows direct hardware access.

The Pico 2 has rp235x-hal as its HAL crate. You can view the examples, which were used as a reference for this workshop.

Remark: If you need to preempt tasks (i.e., interrupt a lower-priority task to run a higher-priority one), you should consider using RTIC. RTIC provides a different concurrency model based on preemption and priorities, which may be required for real-time applications.

High Level

For commonly used microcontrollers, there is often at least one good Board Support Package (BSP). These are crates that provide a convenient, board-specific API, though they are sometimes less customizable than a HAL. For example, in the case of the Micro:bit controller, the BSP is called microbit and it allows you to draw shapes on the on-board LED array.

For the Raspberry Pi Pico 2 W, embassy (and its embassy-rp plugin) come the closest to a full-featured BSP.

More Reading Material

Interesting books about embedded Rust: