Rust の組込み用非同期フレームワーク Embassy (7)

自分のためだけに Embassy コードリーディングシリーズです。 nRF52840 向けの Waker を wake している実装を見ていきます。

GPIO 割り込みから wake() するのが、おそらく最も単純な実装でしょう、ということで GPIO の example を探します。 examples/nrf52840/src/bin/gpiote_port.rs

中身は下のような感じです。 Task Pool が 4 になっていて、同じタスクを複数 spawn できるようになっているようですが、そこは一旦無視しましょう。 pin.wait_for_low() を見るのが良さそうです。

#[embassy_executor::task(pool_size = 4)]
async fn button_task(n: usize, mut pin: Input<'static, AnyPin>) {
    loop {
        pin.wait_for_low().await;
        info!("Button {:?} pressed!", n);
        pin.wait_for_high().await;
        info!("Button {:?} released!", n);
    }
}

#[embassy_executor::main]
async fn main(spawner: Spawner) {
    let p = embassy_nrf::init(Default::default());
    info!("Starting!");

    let btn1 = Input::new(p.P0_11.degrade(), Pull::Up);
    let btn2 = Input::new(p.P0_12.degrade(), Pull::Up);
    let btn3 = Input::new(p.P0_24.degrade(), Pull::Up);
    let btn4 = Input::new(p.P0_25.degrade(), Pull::Up);

    unwrap!(spawner.spawn(button_task(1, btn1)));
    unwrap!(spawner.spawn(button_task(2, btn2)));
    unwrap!(spawner.spawn(button_task(3, btn3)));
    unwrap!(spawner.spawn(button_task(4, btn4)));
}

InputFlex のラッパーになっています。 wait_for_low() を呼び出すと、Flexwait_for_low() を呼びます。

/// GPIO input driver.
pub struct Input<'d, T: Pin> {
    pub(crate) pin: Flex<'d, T>,
}

impl<'d, T: GpioPin> Input<'d, T> {
    /// Wait until the pin is low. If it is already low, return immediately.
    pub async fn wait_for_low(&mut self) {
        self.pin.wait_for_low().await
    }

ちなみに Flex は Flexible pin のことで、組込み Rust では GPIO ピンの状態を型として表現することが多いのですが、どのような状態も取れるピンとして定義されています。

Flex::wait_for_low() では、GPIO 入力が low になったら割り込みが入るように設定して、PortInputFutureインスタンスを作って await しています。

impl<'d, T: GpioPin> Flex<'d, T> {
    /// Wait until the pin is low. If it is already low, return immediately.
    pub async fn wait_for_low(&mut self) {
        self.pin.conf().modify(|_, w| w.sense().low());
        PortInputFuture::new(&mut self.pin).await
    }

重要なのは Future トレイトを実装している PortInputFuture ですね。 poll の実装は次のようになっています。 PORT ごとに Waker を持っていて、register() で登録するようです。 最後は、GPIO 割り込みが検出されて SENSE が無効になっていたら、Poll::Ready(()) を返しています。

impl<'a> Future for PortInputFuture<'a> {
    type Output = ();

    fn poll(self: core::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        PORT_WAKERS[self.pin.pin_port() as usize].register(cx.waker());

        if self.pin.conf().read().sense().is_disabled() {
            Poll::Ready(())
        } else {
            Poll::Pending
        }
    }
}

register() の部分ですが、PORT_WAKERSAtomicWaker の static な配列になっており、AtomicWaker がこうなので、割り込み待ちしている GPIO ピンの Waker を wake したら、このときに登録したコンテキストが run queue に入る、ということになります。

/// Utility struct to register and wake a waker.
pub struct AtomicWaker {
    waker: Mutex<CriticalSectionRawMutex, Cell<Option<Waker>>>,
}

impl AtomicWaker {
    /// Register a waker. Overwrites the previous waker, if any.
    pub fn register(&self, w: &Waker) {
        critical_section::with(|cs| {
            let cell = self.waker.borrow(cs);
            cell.set(match cell.replace(None) {
                Some(w2) if (w2.will_wake(w)) => Some(w2),
                _ => Some(w.clone()),
            })
        })
    }

    /// Wake the registered waker, if any.
    pub fn wake(&self) {
        critical_section::with(|cs| {
            let cell = self.waker.borrow(cs);
            if let Some(w) = cell.replace(None) {
                w.wake_by_ref();
                cell.set(Some(w));
            }
        })
    }
}

では続いて、GPIO の割り込みハンドラ embassy-nrf/src/gpiote.rs です。 重要なところはコード中にコメントで補足した、wake() しているところです。 この GPIO 割り込みの中で、wait_for_low() している実行コンテキストを run queue に入れて、WFE から復帰すると、無事 wait_for_low() の続きからプログラムが実行されることになります。

// 抜粋
unsafe fn handle_gpiote_interrupt() {
    let g: &pac::gpiote::RegisterBlock = regs();

    if g.events_port.read().bits() != 0 {
        g.events_port.write(|w| w);
        let ports = &[&*pac::P0::ptr(), &*pac::P1::ptr()];

        for (port, &p) in ports.iter().enumerate() {
            let bits = p.latch.read().bits();
            for pin in BitIter(bits) {
                // ここで Waker を wake() している
                p.pin_cnf[pin as usize].modify(|_, w| w.sense().disabled());
                PORT_WAKERS[port * 32 + pin as usize].wake();
            }
            p.latch.write(|w| w.bits(bits));
        }
    }
}

ということで、 embassy 自体には他にも機能がありそうですが、最もベースとなる部分の仕組みは大体わかった気になれましたね。