从裸机到Async Rust

如果您刚接触Embassy,可能会觉得理解所有术语和概念有些困难。本指南旨在阐明Embassy中每一层次的不同,以及他们分别为开发者解决了什么问题。

本指南使用STM32 IOT01A开发板,但切换到其他STM32芯片应该也不难。对于nRF,PAC(Peripheral Access Crate,外设访问包)本身并不在Embassy项目中维护,但概念和层次结构是相似的。

我们将编写一个简单的“按下按钮,闪烁LED”的应用程序,它非常适合用来说明我们将要学习的每个示例中的输入和输出处理。我们将从PAC示例开始,最后介绍async示例。

PAC版本

如果不考虑直接读写内存地址的话,PAC是访问外设和寄存器的最低级API。它提供了不同类型的方式去简化外设寄存器访问,但它并不能防止你编写不安全的代码。

因此,不推荐直接使用PAC编写应用程序,但如果你想使用的功能没有高层实现,那么你就需要使用PAC了。

下面展示了使用PAC的blinky应用:

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

如你所见,需要大量代码来启用外设时钟并配置输入引脚和输出引脚。

这个应用程序的另一个缺点是它在轮询按钮状态时会一直忙等(busy-loop),而不能使用任何睡眠模式来省电。

HAL版本

为了简化我们的应用程序,我们可以改用HAL。HAL提供了更高级别的API,处理诸如这样的事务:

在你使用外设时自动启用外设时钟 从更高级别的类型派生和应用寄存器配置 实现embedded-hal trait以在第三方驱动中使用外设 HAL示例如下所示:

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

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

然而,PAC示例中的缺点仍然存在:应用程序在忙等(busy-loop),消耗不必要的电力。

中断驱动

为了省电,我们需要配置应用程序,以便在按下按钮时通过中断通知它。

一旦配置了中断,应用程序就可以指示MCU进入睡眠模式,消耗非常少的电力。

鉴于Embassy专注于Async Rust(我们将在本示例之后继续说这个),示例应用程序必须结合使用HAL和PAC才能使用中断。因此,应用程序还包含一些访问PAC的辅助函数(没有展示在下面)。

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

在这个过程中,由于需要在全局范围内保持button和LED的状态,以便主应用程序循环和中断处理程序都能访问,应用程序变得更加复杂。

为了这么做,必须通过互斥锁来保护这些类型,并且在访问这些全局状态以获取外设访问权限时,必须禁用中断。

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

Async版本

现在是时候充分利用Embassy的魔法了。Embassy的核心是一个异步执行器,或者说是一个异步任务的运行时环境。执行器会轮询一组在编译时定义的任务,当任何任务阻塞时,执行器将运行另一个任务,或者使MCU进入睡眠状态。

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

Async版本与HAL版本非常相似,除了一些小的细节:

  • main入口点使用不同的宏,并且具有async签名。这个宏创建并启动一个Embassy运行时实例,并启动main应用程序任务。使用 Spawner 实例,应用程序还可以生成其他任务。

  • 外设初始化由main宏完成,并交给main任务。

  • 在检查按钮状态之前,应用程序会等待引脚状态的变化(低→高 或 高→低)。

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

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

总结

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