从裸机到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 非常适合嵌入式开发.