635: Add documentation about the different embassy abstraction layers r=Dirbaio a=lulf

The guide demonstrates the functionality offered by each
layer in Embassy, using code examples.

Co-authored-by: Ulf Lilleengen <ulf.lilleengen@gmail.com>
This commit is contained in:
bors[bot] 2022-02-23 16:21:21 +00:00 committed by GitHub
commit bc053404ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 398 additions and 0 deletions

4
ci.sh
View File

@ -51,6 +51,10 @@ cargo batch \
--- build --release --manifest-path embassy-stm32/Cargo.toml --target thumbv7m-none-eabi --features nightly,stm32l151cb-a,defmt,exti,time-driver-any,unstable-traits \ --- build --release --manifest-path embassy-stm32/Cargo.toml --target thumbv7m-none-eabi --features nightly,stm32l151cb-a,defmt,exti,time-driver-any,unstable-traits \
--- build --release --manifest-path embassy-stm32/Cargo.toml --target thumbv7m-none-eabi --features nightly,stm32f398ve,defmt,exti,time-driver-any,unstable-traits \ --- build --release --manifest-path embassy-stm32/Cargo.toml --target thumbv7m-none-eabi --features nightly,stm32f398ve,defmt,exti,time-driver-any,unstable-traits \
--- build --release --manifest-path docs/modules/ROOT/examples/basic/Cargo.toml --target thumbv7em-none-eabi \ --- build --release --manifest-path docs/modules/ROOT/examples/basic/Cargo.toml --target thumbv7em-none-eabi \
--- build --release --manifest-path docs/modules/ROOT/examples/layer-by-layer/blinky-pac/Cargo.toml --target thumbv7em-none-eabi \
--- build --release --manifest-path docs/modules/ROOT/examples/layer-by-layer/blinky-hal/Cargo.toml --target thumbv7em-none-eabi \
--- build --release --manifest-path docs/modules/ROOT/examples/layer-by-layer/blinky-irq/Cargo.toml --target thumbv7em-none-eabi \
--- build --release --manifest-path docs/modules/ROOT/examples/layer-by-layer/blinky-async/Cargo.toml --target thumbv7em-none-eabi \
--- build --release --manifest-path examples/std/Cargo.toml --target x86_64-unknown-linux-gnu --out-dir out/examples/std \ --- build --release --manifest-path examples/std/Cargo.toml --target x86_64-unknown-linux-gnu --out-dir out/examples/std \
--- build --release --manifest-path examples/nrf/Cargo.toml --target thumbv7em-none-eabi --out-dir out/examples/nrf \ --- build --release --manifest-path examples/nrf/Cargo.toml --target thumbv7em-none-eabi --out-dir out/examples/nrf \
--- build --release --manifest-path examples/rp/Cargo.toml --target thumbv6m-none-eabi --out-dir out/examples/rp \ --- build --release --manifest-path examples/rp/Cargo.toml --target thumbv6m-none-eabi --out-dir out/examples/rp \

View File

@ -0,0 +1,11 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-run --chip STM32L475VG"
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
]
[build]
target = "thumbv7em-none-eabihf"

View File

@ -0,0 +1,22 @@
[workspace]
resolver = "2"
members = [
"blinky-pac",
"blinky-hal",
"blinky-irq",
"blinky-async",
]
[patch.crates-io]
embassy = { path = "../../../../../embassy" }
embassy-stm32 = { path = "../../../../../embassy-stm32" }
stm32-metapac = { path = "../../../../../stm32-metapac" }
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
lto = "fat"
opt-level = 's'
overflow-checks = false

View File

@ -0,0 +1,14 @@
[package]
name = "blinky-async"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embassy-stm32 = { version = "0.1.0", features = ["stm32l475vg", "memory-x", "exti"], default-features = false }
embassy = { version = "0.1.0", default-features = false, features = ["nightly"] }
defmt = "0.3.0"
defmt-rtt = "0.3.0"
panic-probe = { version = "0.3.0", features = ["print-defmt"] }

View File

@ -0,0 +1,28 @@
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
use defmt_rtt as _;
use panic_probe as _;
use embassy::executor::Spawner;
use embassy_stm32::{
exti::ExtiInput,
gpio::{Input, Level, Output, Pull, Speed},
Peripherals,
};
#[embassy::main]
async fn main(_s: Spawner, p: Peripherals) {
let mut led = Output::new(p.PB14, Level::Low, Speed::VeryHigh);
let mut button = ExtiInput::new(Input::new(p.PC13, Pull::Up), p.EXTI13);
loop {
button.wait_for_any_edge().await;
if button.is_low() {
led.set_high();
} else {
led.set_low();
}
}
}

View File

@ -0,0 +1,13 @@
[package]
name = "blinky-hal"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
embassy-stm32 = { version = "0.1.0", features = ["stm32l475vg", "memory-x"], default-features = false }
defmt = "0.3.0"
defmt-rtt = "0.3.0"
panic-probe = { version = "0.3.0", features = ["print-defmt"] }

View File

@ -0,0 +1,23 @@
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use defmt_rtt as _;
use panic_probe as _;
use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
let mut led = Output::new(p.PB14, Level::High, Speed::VeryHigh);
let button = Input::new(p.PC13, Pull::Up);
loop {
if button.is_low() {
led.set_high();
} else {
led.set_low();
}
}
}

View File

@ -0,0 +1,13 @@
[package]
name = "blinky-irq"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = { version = "0.7" }
embassy-stm32 = { version = "0.1.0", features = ["stm32l475vg", "memory-x", "unstable-pac"] }
defmt = "0.3.0"
defmt-rtt = "0.3.0"
panic-probe = { version = "0.3.0", features = ["print-defmt"] }

View File

@ -0,0 +1,101 @@
#![no_std]
#![no_main]
use defmt_rtt as _;
use panic_probe as _;
use core::cell::RefCell;
use cortex_m::interrupt::Mutex;
use cortex_m::peripheral::NVIC;
use cortex_m_rt::entry;
use embassy_stm32::{
gpio::{Input, Level, Output, Pin, Pull, Speed},
interrupt, pac,
peripherals::{PB14, PC13},
};
static BUTTON: Mutex<RefCell<Option<Input<'static, PC13>>>> = Mutex::new(RefCell::new(None));
static LED: Mutex<RefCell<Option<Output<'static, PB14>>>> = Mutex::new(RefCell::new(None));
#[entry]
fn main() -> ! {
let p = embassy_stm32::init(Default::default());
let led = Output::new(p.PB14, Level::Low, Speed::Low);
let mut button = Input::new(p.PC13, Pull::Up);
cortex_m::interrupt::free(|cs| unsafe {
enable_interrupt(&mut button);
LED.borrow(cs).borrow_mut().replace(led);
BUTTON.borrow(cs).borrow_mut().replace(button);
NVIC::unmask(pac::Interrupt::EXTI15_10);
});
loop {
cortex_m::asm::wfe();
}
}
#[interrupt]
fn EXTI15_10() {
cortex_m::interrupt::free(|cs| {
let mut button = BUTTON.borrow(cs).borrow_mut();
let button = button.as_mut().unwrap();
let mut led = LED.borrow(cs).borrow_mut();
let led = led.as_mut().unwrap();
if check_interrupt(button) {
if button.is_low() {
led.set_high();
} else {
led.set_low();
}
}
clear_interrupt(button);
});
}
//
//
//
//
//
//
// "Hidden" HAL-like methods for doing interrupts with embassy. Hardcode pin just to give audience an idea of what it looks like
const PORT: u8 = 2;
const PIN: usize = 13;
fn check_interrupt<P: Pin>(_pin: &mut Input<'static, P>) -> bool {
let exti = pac::EXTI;
unsafe {
let pin = PIN;
let lines = exti.pr(0).read();
lines.line(pin)
}
}
fn clear_interrupt<P: Pin>(_pin: &mut Input<'static, P>) {
let exti = pac::EXTI;
unsafe {
let pin = PIN;
let mut lines = exti.pr(0).read();
lines.set_line(pin, true);
exti.pr(0).write_value(lines);
}
}
fn enable_interrupt<P: Pin>(_pin: &mut Input<'static, P>) {
cortex_m::interrupt::free(|_| unsafe {
let rcc = pac::RCC;
rcc.apb2enr().modify(|w| w.set_syscfgen(true));
let port = PORT;
let pin = PIN;
let syscfg = pac::SYSCFG;
let exti = pac::EXTI;
syscfg.exticr(pin / 4).modify(|w| w.set_exti(pin % 4, port));
exti.imr(0).modify(|w| w.set_line(pin, true));
exti.rtsr(0).modify(|w| w.set_line(pin, true));
exti.ftsr(0).modify(|w| w.set_line(pin, true));
});
}

View File

@ -0,0 +1,13 @@
[package]
name = "blinky-pac"
version = "0.1.0"
edition = "2021"
[dependencies]
cortex-m = "0.7"
cortex-m-rt = "0.7"
stm32-metapac = { version = "0.1.0", features = ["stm32l475vg", "memory-x"] }
defmt = "0.3.0"
defmt-rtt = "0.3.0"
panic-probe = { version = "0.3.0", features = ["print-defmt"] }

View File

@ -0,0 +1,69 @@
#![no_std]
#![no_main]
use defmt_rtt as _;
use panic_probe as _;
use stm32_metapac as pac;
use pac::gpio::vals;
#[cortex_m_rt::entry]
fn main() -> ! {
// Enable GPIO clock
let rcc = pac::RCC;
unsafe {
rcc.ahb2enr().modify(|w| {
w.set_gpioben(true);
w.set_gpiocen(true);
});
rcc.ahb2rstr().modify(|w| {
w.set_gpiobrst(true);
w.set_gpiocrst(true);
w.set_gpiobrst(false);
w.set_gpiocrst(false);
});
}
// Setup button
let gpioc = pac::GPIOC;
const BUTTON_PIN: usize = 13;
unsafe {
gpioc
.pupdr()
.modify(|w| w.set_pupdr(BUTTON_PIN, vals::Pupdr::PULLUP));
gpioc
.otyper()
.modify(|w| w.set_ot(BUTTON_PIN, vals::Ot::PUSHPULL));
gpioc
.moder()
.modify(|w| w.set_moder(BUTTON_PIN, vals::Moder::INPUT));
}
// Setup LED
let gpiob = pac::GPIOB;
const LED_PIN: usize = 14;
unsafe {
gpiob
.pupdr()
.modify(|w| w.set_pupdr(LED_PIN, vals::Pupdr::FLOATING));
gpiob
.otyper()
.modify(|w| w.set_ot(LED_PIN, vals::Ot::PUSHPULL));
gpiob
.moder()
.modify(|w| w.set_moder(LED_PIN, vals::Moder::OUTPUT));
}
// Main loop
loop {
unsafe {
if gpioc.idr().read().idr(BUTTON_PIN) == vals::Idr::LOW {
gpiob.bsrr().write(|w| w.set_bs(LED_PIN, true));
} else {
gpiob.bsrr().write(|w| w.set_br(LED_PIN, true));
}
}
}
}

View File

@ -5,4 +5,5 @@
** xref:stm32.adoc[STM32] ** xref:stm32.adoc[STM32]
* xref:getting_started.adoc[Getting started] * xref:getting_started.adoc[Getting started]
** xref:basic_application.adoc[Basic application] ** xref:basic_application.adoc[Basic application]
** xref:layer_by_layer.adoc[Layer by Layer]
* xref:examples.adoc[Examples] * xref:examples.adoc[Examples]

View File

@ -0,0 +1,86 @@
= Embassy layer by layer
If you're new to Embassy, it can be overwhelming to grasp all the terminology and concepts. This guide aims to clarify the different layers in Embassy, which problem each layer solves for the application writer.
This guide uses the STM32 IOT01A board, but should be easy to translate to any STM32 chip. For nRF, the PAC itself is not maintained within the Embassy project, but the concepts and the layers are similar.
The application we'll write is a simple 'push button, blink led' application, which is great for illustrating input and output handling for each of the examples we'll go through. We'll start at the Peripheral Access Crate (PAC) example and end at the async example.
== PAC version
The PAC is the lowest API for accessing peripherals and registers, if you don't count reading/writing directly to memory addresses. It provide distinct types
to make accessing peripheral registers easier, but it does not prevent you from writing unsafe code.
Writing an application using the PAC directly is therefore not recommended, but if the functionality you want to use is not exposed in the upper layers, that's what you need to use.
The blinky app using PAC is shown below:
[source,rust]
----
include::example$layer-by-layer/blinky-pac/src/main.rs[]
----
As you can see, there are a lot of code needed to enable the peripheral clocks, configuring the input pins and the output pins of the application.
Another downside of this application is that it is busy-looping while polling the button state. This prevents the microcontroller from utilizing any sleep mode to save power.
== HAL version
To simplify our application, we can use the HAL instead. The HAL exposes higher level APIs that handle details such
* Automatically enabling the peripheral clock when you're using the peripheral
* Deriving and applying register configuration from higher level types
* Implementing the embedded-hal traits to make peripherals useful in third party drivers
The HAL example is shown below:
[source,rust]
----
include::example$layer-by-layer/blinky-hal/src/main.rs[]
----
As you can see, the application becomes a lot simpler, even without using any async code. The `Input` and `Output` hides all the details accessing the GPIO registers, and allow you to use a much simpler API to query the state of the button and toggle the LED output accordingly.
The same downside from the PAC example still applies though: the application is busy looping and consuming more power than necessary.
== Interrupt driven
To save power, we need to configure the application so that it can be notified when the button is pressed using an interrupt.
Once the interrupt is configured, the application can instruct the microcontroller to enter a sleep mode, consuming very little power.
Given Embassy focus on async Rust (which we'll come back to after this example), the example application must use a combination of the HAL and PAC in order to use interrupts. For this reason, the application also contains some helper functions to access the PAC (not shown below).
[source,rust]
----
include::example$layer-by-layer/blinky-irq/src/main.rs[lines="1..57"]
----
The simple application is now more complex again, primarily because of the need to keep the button and LED states in the global scope where it is accessible by the main application loop, as well as the interrupt handler.
To do that, the types must be guarded by a mutex, and interrupts must be disabled whenever we are accessing this global state to gain access to the peripherals.
Luckily, there is an elegant solution to this problem when using Embassy.
== Async version
It's time to use the Embassy capabilities to its fullest. At the core, Embassy has an async excecutor, or a runtime for async tasks if you will. The executor polls a set of tasks (defined at compile time), and whenever a task `blocks`, the executor will run another task, or put the microcontroller to sleep.
[source,rust]
----
include::example$layer-by-layer/blinky-async/src/main.rs[]
----
The async version looks very similar to the HAL version, apart from a few minor details:
* The main entry point is annotated with a different macro and has an async type signature. This macro creates and starts an Embassy runtime instance and launches the main application task. Using the `Spawner` instance, the application may spawn other tasks.
* The peripheral initialization is done by the main macro, and is handed to the main task.
* Before checking the button state, the application is awaiting a transition in the pin state (low -> high or high -> low).
When `button.await_for_any_edge().await` is called, the executor will pause the main task and put the microcontroller in sleep mode, unless there are other tasks that can run. Internally, the Embassy HAL has configured the interrupt handler for the button (in `ExtiButton`), so that whenever an interrupt is raised, the task awaiting the button will be woken up.
The minimal overhead of the executor and the ability to run multiple tasks "concurrently" combined with the enormous simplification of the application, makes `async` a great fit for embedded.
== Summary
We have seen how the same application can be written at the different abstraction levels in Embassy. First starting out at the PAC level, then using the HAL, then using interrupts, and then using interrupts indirectly using async Rust.