mirror of
https://github.com/embassy-rs/embassy.git
synced 2024-11-25 16:23:10 +00:00
Add documentation about the different embassy abstraction layers
The guide demonstrates the functionality offered by each layer in Embassy, using code examples.
This commit is contained in:
parent
4c6e61b3b1
commit
092eef3ae7
4
ci.sh
4
ci.sh
@ -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 \
|
||||||
|
11
docs/modules/ROOT/examples/layer-by-layer/.cargo/config.toml
Normal file
11
docs/modules/ROOT/examples/layer-by-layer/.cargo/config.toml
Normal 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"
|
22
docs/modules/ROOT/examples/layer-by-layer/Cargo.toml
Normal file
22
docs/modules/ROOT/examples/layer-by-layer/Cargo.toml
Normal 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
|
@ -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"] }
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"] }
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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"] }
|
101
docs/modules/ROOT/examples/layer-by-layer/blinky-irq/src/main.rs
Normal file
101
docs/modules/ROOT/examples/layer-by-layer/blinky-irq/src/main.rs
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
@ -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"] }
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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]
|
||||||
|
86
docs/modules/ROOT/pages/layer_by_layer.adoc
Normal file
86
docs/modules/ROOT/pages/layer_by_layer.adoc
Normal 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.awit_for_any_edge().await` is called, the executor will pause the main task and put the microcontroller in sleep mode. 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 overhead of the executor is minimal, and combined with the enormous simplification of the application, `async` is 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.
|
Loading…
Reference in New Issue
Block a user