Big Button - Part 10 - Embassy Embedded Rust Framework
Table of Contents
Introduction
💻 HACKER Let’s go over the concept of programming abstractions, levels of programming, programming frameworks, and the framework we will use for our embedded programming adventures. This again will be more of an explanatory-type part before we jump into more code.

From Standard Library to Frameworks
You can program embedded devices using different levels of built-in tools. From using only the language’s standard library (or core library in embedded) where you handle every detail yourself, to using a full framework that provides ready-made solutions for common tasks.
Remember, that no advantage is free and must come at a cost (no free lunch 🥪).

If, for example, you want to use a framework function that quickly and conveniently does 50 things behind the scenes, you are effectively trusting the framework’s code to do what/how/when you need. You also are accepting less control of the underlying system (there might be other functions and configurations you would not know about).
However, in contrast, standard library programming is not necessarily the correct/full answer all the time. The more you build from scratch with just basic tools, the more complex, time consuming, and error-prone programming will get.
Most times, there is a trade-off between framework convenience, ease, programming speed and standard library control, customization, and minimal dependencies.
You CAN program a microcontroller using only Rust’s core library with no frameworks, manually handling every peripheral and interrupt, but it would take a ton of knowledge and effort to exactly get it to do what you want it to do.
This is where frameworks come into play and shine!
Frameworks
Generally speaking, a programming framework could just about be anything structured that makes programming easier, more convenient, and accessible. A set of custom functions could theoretically be considered a simple framework.

Here in our case, we are talking about an embedded hardware programming framework that enables us to program a microcontroller chip efficiently. That is, giving you a lot of pre-written code and ways for a more “plug-and-play” experience.
NOTE
You might be wondering: What is a difference between a framework and a library/package? Generally speaking, a framework itself calls your code and also may call library/package code. However, typically library/package code does not call your code or the framework. (ie. who calls who)
Embassy 🏢
Enter Embassy! - A Rust-based asynchronous embedded programming framework.
The obvious selling point about Embassy is that it is based in Rust programming language. However, there are other great features Embassy offers (check out their website). Let’s discuss some of the more relevant advantages.
Asynchronous Programming
Asynchronous (non-blocking) programming is a programming paradigm that allows operations to execute independently of the main program flow. In the context of embedded systems, this means a microcontroller can start an operation (like reading from a sensor) and then continue with other work while waiting for that operation to complete.
Here is an analogy for an asynchronous program flow:
Imagine trying to do the laundry while cooking dinner. You start the washing machine, then while it runs you chop vegetables and prepare your meal. When the washer beeps, you move clothes to the dryer, then return to cooking. By the time dinner is ready, your laundry is also done. The two tasks were completed in parallel.

For this tutorial series, it is important to at least get a good conceptual grip on async programming. The comprehensive explanation of async programming and the contrast to parallel or concurrent programming is outside of the scope of this tutorial. However, I encourage you to review some great explanations on asynchronous programming: Article 1, Article 2, Video, Short.
Embassy’s asynchronous approach offers:
- Power efficiency: Tasks give up CPU processing while waiting, saving power in battery-operated devices
- No polling: Wait for events without constantly checking status flags
- Task prioritization: Assign importance levels to different operations
- Thread safety: Compile-time guarantees prevent data races between concurrent operations
- Simplified concurrency: Asynchronous code reads sequentially despite concurrent execution
Real-Time Operating System (RTOS)
A Real-Time Operating System (RTOS) is a type of operating system designed to process data and events with minimal delay and precise timing guarantees.
The term “real-time” refers to the fact that the system must very quickly react to events (button presses, network requests, etc) within a small time-constraint. It’s like a chef in a busy restaurant who must respond immediately when the bell rings for a new order - there’s no time to wait or the customers will be unhappy.
If you are interested in anything RTOS, checkout the ROTS wikipedia entry or this video.
The Embassy framework is an RTOS that includes the following advantages:
- Cooperative multitasking: Tasks run until they explicitly give up control
- Lightweight executor: Minimal overhead compared to traditional RTOS solutions
- Deterministic timing: Predictable execution patterns for time-sensitive applications
- No dynamic allocation: Static task allocation eliminates heap fragmentation
- Interrupt integration: Seamless handling of hardware interrupts within the async model
NOTE
Remember, Embassy is not the only or the most popular RTOS framework out there. Others include FreeRTOS, pyRTOS, and many others
Hardware Abstraction Layer (HAL)
The Hardware Abstraction Layer (HAL) is exactly as it sounds: A layer that abstracts the underlying hardware code making the hardware functions more accessible.
The HAL allows the same code to be used across different hardware devices. For example, setting an LED to high/on, would look very similar on a Raspberry Pi Pico or a STM32 microcontroller. This allows programmers to easier update hardware-related code. HAL often allows programmers to write device-independent code.

The Embassy project maintains these HALs for select hardware, but you can still use HALs from other projects with Embassy. That is, if you want more control for the underlying layer you can use the specific HALs (ie. embassy-rp, embassy-stm32)
As a simplified example, here is a program setting an LED to high/on for a Raspberry Pi Pico:
#![no_std]#![no_main]
use embassy_executor::Spawner;use embassy_rp::gpio::{Level, Output}; // <-- NOTEuse {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]async fn main(_spawner: Spawner) { let p = embassy_rp::init(Default::default());
// Configure "PIN_25" as output, set as initially high let mut led = Output::new(p.PIN_25, Level::High);
// Set LED to on led.set_high();}
And here is the same code but coded for a STM32 microcontroller:
#![no_std]#![no_main]
use embassy_executor::Spawner;use embassy_stm32::gpio::{Level, Output, Speed}; // <-- NOTEuse {defmt_rtt as _, panic_probe as _};
#[embassy_executor::main]async fn main(_spawner: Spawner) { let p = embassy_stm32::init(Default::default());
// Configure "PC13" as output, set as initially high let mut led = Output::new(p.PC13, Level::High, Speed::Low);
// Set LED to on led.set_high();}
From these two example code snippets it becomes obvious that Embassy’s
HAL abstraction hides code complexity and tries to keep code consistent across
systems. While there are minor differences (i.e. Speed::Low
), the code is
generally the same.
Some advantages of Embassy’s HAL system include:
- Cross-platform abstractions: Single API works across different microcontroller families
- Peripheral drivers: Ready-made implementations for UART, SPI, I2C, GPIO, and more
- Type-state API: Impossible to misuse peripherals due to compile-time checks
- Target-specific modules: Specialized support for:
- STM32 microcontrollers via
embassy-stm32
- Raspberry Pi microcontrollers via
embassy-rp
- Nordic nRF chips via
embassy-nrf
- ESP32 chips via
embassy-esp
- STM32 microcontrollers via
Resources
OK. Now let’s start CODING!