embassy book

从裸机到异步 Rust

如果你是 Embassy 的新手,可能会对所有的术语和概念感到不知所措。本指南旨在阐明 Embassy 中不同的层,以及每一层为应用程序编写者解决的问题。

本指南使用 STM32 IOT01A 开发板,但应该很容易转换为任何 STM32 芯片。对于 nRF,PAC 本身不在 Embassy 项目中维护,但概念和层是相似的。

我们将编写的应用程序是一个简单的"按钮按下,LED 闪烁"应用程序,这非常适合说明我们将要介绍的每个示例的输入和输出处理。我们将从外围访问层 (PAC) 示例开始,到异步示例结束。

PAC 版本

PAC 是用于访问外围设备和寄存器的最低级别 API,如果你不直接读/写内存地址的话。它提供了不同的类型,使访问外围设备寄存器更容易,但它并不能阻止你编写不安全的代码。

因此,不建议直接使用 PAC 编写应用程序,但如果你想要使用的功能在上层没有公开,那么这就是你需要使用的。

使用 PAC 的闪烁应用程序如下所示:

{{#include ../examples/layer-by-layer/blinky-pac/src/main.rs}}
#![no_std]
#![no_main]

use pac::gpio::vals;
use {defmt_rtt as _, panic_probe as _, stm32_metapac as pac};

#[cortex_m_rt::entry]
fn main() -> ! {
    // Enable GPIO clock
    let rcc = pac::RCC;
    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;
    gpioc.pupdr().modify(|w| w.set_pupdr(BUTTON_PIN, vals::Pupdr::PULL_UP));
    gpioc.otyper().modify(|w| w.set_ot(BUTTON_PIN, vals::Ot::PUSH_PULL));
    gpioc.moder().modify(|w| w.set_moder(BUTTON_PIN, vals::Moder::INPUT));

    // Setup LED
    let gpiob = pac::GPIOB;
    const LED_PIN: usize = 14;
    gpiob.pupdr().modify(|w| w.set_pupdr(LED_PIN, vals::Pupdr::FLOATING));
    gpiob.otyper().modify(|w| w.set_ot(LED_PIN, vals::Ot::PUSH_PULL));
    gpiob.moder().modify(|w| w.set_moder(LED_PIN, vals::Moder::OUTPUT));

    // Main loop
    loop {
        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));
        }
    }
}

正如你所看到的,需要大量的代码来启用外围时钟并配置应用程序的输入引脚和输出引脚。

这个应用程序的另一个缺点是它在忙循环中轮询按钮状态。这阻止了微控制器利用任何睡眠模式来节省电量。

HAL 版本

为了简化我们的应用程序,我们可以使用 HAL 来代替。HAL 公开了更高级别的 API,可以处理诸如以下细节:

  • 当你使用外围设备时,自动启用外围时钟
  • 从更高级别的类型派生和应用寄存器配置
  • 实现 embedded-hal traits,使外围设备在第三方驱动程序中很有用

HAL 示例代码如下所示:

{{#include ../examples/layer-by-layer/blinky-hal/src/main.rs}}
#![no_std]
#![no_main]

use cortex_m_rt::entry;
use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
use {defmt_rtt as _, panic_probe as _};

#[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();
        }
    }
}

正如你所看到的,即使不使用任何异步代码,应用程序也变得简单得多。InputOutput 类型隐藏了访问 GPIO 寄存器的所有细节,并允许你使用更简单的 API 来查询按钮的状态和切换 LED 输出。

PAC 示例中的相同缺点仍然适用:应用程序正在忙循环,消耗的电量超过必要的电量。

中断驱动版本

为了节省电量,我们需要配置应用程序,以便在使用中断按下按钮时可以得到通知。

一旦配置了中断,应用程序就可以指示微控制器进入睡眠模式,从而消耗非常少的电量。

考虑到 Embassy 专注于异步 Rust(我们将在本示例之后回到这一点),示例应用程序必须结合使用 HAL 和 PAC 才能使用中断。因此,该应用程序还包含一些辅助函数来访问 PAC(下面未显示)。

{{#include ../examples/layer-by-layer/blinky-irq/src/main.rs lines="1..57"}}
#![no_std]
#![no_main]

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, Pull, Speed};
use embassy_stm32::{interrupt, pac};
use {defmt_rtt as _, panic_probe as _};

static BUTTON: Mutex<RefCell<Option<Input<'static>>>> = Mutex::new(RefCell::new(None));
static LED: Mutex<RefCell<Option<Output<'static>>>> = 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| {
        enable_interrupt(&mut button);

        LED.borrow(cs).borrow_mut().replace(led);
        BUTTON.borrow(cs).borrow_mut().replace(button);

        unsafe { 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);
    });
}

现在,简单的应用程序再次变得更加复杂,这主要是因为需要将按钮和 LED 状态保存在全局范围内,以便主应用程序循环以及中断处理程序可以访问它。

为此,类型必须由互斥锁保护,并且每当我们访问此全局状态以访问外围设备时,都必须禁用中断。

幸运的是,使用 Embassy 时,有一个优雅的解决方案来解决这个问题。

异步版本

现在是时候充分利用 Embassy 的功能了。Embassy 的核心是一个异步执行器,或者如果你愿意,可以将其称为异步任务的运行时。执行器轮询一组任务(在编译时定义),并且每当任务"阻塞"时,执行器将运行另一个任务,或使微控制器进入睡眠状态。

{{#include ../examples/layer-by-layer/blinky-async/src/main.rs}}
#![no_std]
#![no_main]

use embassy_executor::Spawner;
use embassy_stm32::exti::ExtiInput;
use embassy_stm32::gpio::{Level, Output, Pull, Speed};
use {defmt_rtt as _, panic_probe as _};

#[embassy_executor::main]
async fn main(_spawner: Spawner) {
    let p = embassy_stm32::init(Default::default());
    let mut led = Output::new(p.PB14, Level::Low, Speed::VeryHigh);
    let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);

    loop {
        button.wait_for_any_edge().await;
        if button.is_low() {
            led.set_high();
        } else {
            led.set_low();
        }
    }
}

除了少数细节外,异步版本看起来与 HAL 版本非常相似:

  • 主入口点使用不同的宏进行注释,并具有异步类型签名。此宏创建并启动 Embassy 运行时实例,并启动主应用程序任务。使用 Spawner 实例,应用程序可以派生其他任务。
  • 外围设备初始化由主宏完成,并移交给主任务。
  • 在检查按钮状态之前,应用程序正在等待引脚状态的转换(低 -> 高 或 高 -> 低)。

当调用 button.await_for_any_edge().await 时,执行器将暂停主任务并使微控制器进入睡眠模式,除非有其他任务可以运行。在内部,Embassy HAL 已为按钮配置了中断处理程序(在 ExtiInput 中),以便每当引发中断时,等待按钮的任务将被唤醒。

执行器的最小开销和"并发"运行多个任务的能力,加上应用程序的巨大简化,使 async 非常适合嵌入式。

总结

我们已经看到了如何在 Embassy 的不同抽象级别编写相同的应用程序。首先从 PAC 级别开始,然后使用 HAL,然后使用中断,然后使用异步 Rust 间接使用中断。