从裸机到Async Rust

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

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

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

PAC版本

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

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

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

Unresolved include directive in modules/ROOT/pages/layer_by_layer.adoc - include::example$layer-by-layer/blinky-pac/src/main.rs[]

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

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

HAL版本

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

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

Unresolved include directive in modules/ROOT/pages/layer_by_layer.adoc - include::example$layer-by-layer/blinky-hal/src/main.rs[]

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

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

中断驱动

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

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

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

Unresolved include directive in modules/ROOT/pages/layer_by_layer.adoc - include::example$layer-by-layer/blinky-irq/src/main.rs[]

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

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

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

Async版本

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

Unresolved include directive in modules/ROOT/pages/layer_by_layer.adoc - include::example$layer-by-layer/blinky-async/src/main.rs[]

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间接使用中断。