embassy book

任务之间共享外设

通常,多个任务需要访问相同的资源(引脚、通信接口等)。Embassy 在 embassy-sync crate 中提供了许多不同的同步原语。

以下示例展示了两个任务同时使用 Raspberry Pi Pico 板载 LED 的不同方法。

使用互斥锁 (Mutex) 共享

使用互斥是共享外设最简单的方法。

提示:运行此示例所需的依赖项可以在 这里 找到。

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;
    }
}

定义 LedType 的结构有助于访问资源。

为什么这么复杂

解开这些层层结构,可以深入了解为什么每个层都是必要的。

Mutex<RawMutexType, T>

互斥锁的存在是为了防止当一个任务首先获得资源并开始修改它时,所有其他想要写入的任务都必须等待(如果没有任何任务锁定互斥锁,led.lock().await 将立即返回;如果互斥锁在其他地方被访问,则会阻塞)。

Option<T>

LED 变量需要在 main 任务之外定义,因为任务接受的引用需要是 'static 的。但是,如果它在 main 任务之外,则无法初始化以指向任何引脚,因为引脚本身尚未初始化。因此,它被设置为 None

Output<AnyPin>

指示引脚将设置为输出 (Output)。AnyPin 本可以是 embassy_rp::peripherals::PIN_25,但此选项使 toggle_led 函数更通用。

使用通道 (Channel) 共享

通道是另一种确保对资源进行独占访问的方法。在访问可以在稍后的时间点发生的情况下,使用通道非常棒,它允许你将操作排队并执行其他操作。

提示:运行此示例所需的依赖项可以在 这里 找到。

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;
    }
}

此示例用通道替换了互斥锁,并使用另一个任务(主循环)来驱动 LED。这种方法的优点是只有一个任务引用外围设备,从而分离了关注点。但是,使用互斥锁的开销较低,如果需要确保在继续执行任务中的其他工作之前完成操作,则可能是必要的。

此处 可以找到一个展示更多共享方法的示例。

在多个设备之间共享 I2C 或 SPI 总线

此处 可以找到一个关于如何处理多个设备共享公共 I2C 或 SPI 总线的示例。