embassy/docs/pages/sharing_peripherals.adoc
2024-07-22 07:26:55 -04:00

135 lines
5.3 KiB
Plaintext

= Sharing peripherals between tasks
Often times, more than one task needs access to the same resource (pin, communication interface, etc.). Embassy provides many different synchronization primitives in the link:https://crates.io/crates/embassy-sync[embassy-sync] crate.
The following examples shows different ways to use the on-board LED on a Raspberry Pi Pico board by two tasks simultaneously.
== Sharing using a Mutex
Using mutual exclusion is the simplest way to share a peripheral.
TIP: Dependencies needed to run this example link:#_the_cargo_toml[can be found here].
[,rust]
----
use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::gpio;
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::mutex::Mutex;
use embassy_time::{Duration, Ticker};
use gpio::{AnyPin, Level, Output};
use {defmt_rtt as _, panic_probe as _};
type LedType = Mutex<ThreadModeRawMutex, Option<Output<'static, AnyPin>>>;
static LED: LedType = Mutex::new(None);
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
// set the content of the global LED reference to the real LED pin
let led = Output::new(AnyPin::from(p.PIN_25), Level::High);
// inner scope is so that once the mutex is written to, the MutexGuard is dropped, thus the
// Mutex is released
{
*(LED.lock().await) = Some(led);
}
let dt = 100 * 1_000_000;
let k = 1.003;
unwrap!(spawner.spawn(toggle_led(&LED, Duration::from_nanos(dt))));
unwrap!(spawner.spawn(toggle_led(&LED, Duration::from_nanos((dt as f64 * k) as u64))));
}
// A pool size of 2 means you can spawn two instances of this task.
#[embassy_executor::task(pool_size = 2)]
async fn toggle_led(led: &'static LedType, delay: Duration) {
let mut ticker = Ticker::every(delay);
loop {
{
let mut led_unlocked = led.lock().await;
if let Some(pin_ref) = led_unlocked.as_mut() {
pin_ref.toggle();
}
}
ticker.next().await;
}
}
----
The structure facilitating access to the resource is the defined `LedType`.
=== Why so complicated
Unwrapping the layers gives insight into why each one is needed.
==== `Mutex<RawMutexType, T>`
The mutex is there so if one task gets the resource first and begins modifying it, all other tasks wanting to write will have to wait (the `led.lock().await` will return immediately if no task has locked the mutex, and will block if it is accessed somewhere else).
==== `Option<T>`
The `LED` variable needs to be defined outside the main task as references accepted by tasks need to be `'static`. However, if it is outside the main task, it cannot be initialised to point to any pin, as the pins themselves are not initialised. Thus, it is set to `None`.
==== `Output<AnyPin>`
To indicate that the pin will be set to an Output. The `AnyPin` could have been `embassy_rp::peripherals::PIN_25`, however this option lets the `toggle_led` function be more generic.
== Sharing using a Channel
A channel is another way to ensure exclusive access to a resource. Using a channel is great in the cases where the access can happen at a later point in time, allowing you to enqueue operations and do other things.
TIP: Dependencies needed to run this example link:#_the_cargo_toml[can be found here].
[,rust]
----
use defmt::*;
use embassy_executor::Spawner;
use embassy_rp::gpio;
use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex;
use embassy_sync::channel::{Channel, Sender};
use embassy_time::{Duration, Ticker};
use gpio::{AnyPin, Level, Output};
use {defmt_rtt as _, panic_probe as _};
enum LedState {
Toggle,
}
static CHANNEL: Channel<ThreadModeRawMutex, LedState, 64> = Channel::new();
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let p = embassy_rp::init(Default::default());
let mut led = Output::new(AnyPin::from(p.PIN_25), Level::High);
let dt = 100 * 1_000_000;
let k = 1.003;
unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos(dt))));
unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos((dt as f64 * k) as u64))));
loop {
match CHANNEL.receive().await {
LedState::Toggle => led.toggle(),
}
}
}
// A pool size of 2 means you can spawn two instances of this task.
#[embassy_executor::task(pool_size = 2)]
async fn toggle_led(control: Sender<'static, ThreadModeRawMutex, LedState, 64>, delay: Duration) {
let mut ticker = Ticker::every(delay);
loop {
control.send(LedState::Toggle).await;
ticker.next().await;
}
}
----
This example replaces the Mutex with a Channel, and uses another task (the main loop) to drive the LED. The advantage of this approach is that only a single task references the peripheral, separating concerns. However, using a Mutex has a lower overhead and might be necessary if you need to ensure
that the operation is completed before continuing to do other work in your task.
An example showcasing more methods for sharing link:https://github.com/embassy-rs/embassy/blob/main/examples/rp/src/bin/sharing.rs[can be found here].
== Sharing an I2C or SPI bus between multiple devices
An example of how to deal with multiple devices sharing a common I2C or SPI bus link:https://github.com/embassy-rs/embassy/blob/main/examples/rp/src/bin/shared_bus.rs[can be found here].