从EXTI实现看Embassy异步Rust嵌入式框架

本文最后更新于 2024年10月21日 中午

Embassy是一个基于Rust的异步嵌入式开发框架。

Embassy

它不仅包含了异步运行时,还提供了STM32、RP2xxx,NRF等的异步HAL实现、Bootloader、usb等。

此外,乐鑫官方的esp-rs也是将embassy作为默认框架使用。

最近研究了embassy-stm32的部分实现,写在博客里作为记录吧。Exti最简单也有点Async味,就先写这个吧。

注意:Embassy尚未1.0,此文可能在您读的时候已经过时。

文件路径:embassy-stm32\src\exti.rs

从顶向下看。

ExtiInput<’d>

1
2
3
4
5
6
7
8
9
10
/// EXTI input driver.
///
/// This driver augments a GPIO `Input` with EXTI functionality. EXTI is not
/// built into `Input` itself because it needs to take ownership of the corresponding
/// EXTI channel, which is a limited resource.
///
/// Pins PA5, PB5, PC5... all use EXTI channel 5, so you can't use EXTI on, say, PA5 and PC5 at the same time.
pub struct ExtiInput<'d> {
pin: Input<'d>,
}

ExtiInput类型,内部包含了一个Input类型。其实Input类型也是内部包含了一个FlexPin类型。

new函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
impl<'d> ExtiInput<'d> {
/// Create an EXTI input.
pub fn new<T: GpioPin>(
pin: impl Peripheral<P = T> + 'd,
ch: impl Peripheral<P = T::ExtiChannel> + 'd,
pull: Pull,
) -> Self {
into_ref!(pin, ch);

// Needed if using AnyPin+AnyChannel.
assert_eq!(pin.pin(), ch.number());

Self {
pin: Input::new(pin, pull),
}
}
...

new函数没啥好说的。impl Peripheral<P = T::ExtiChannel>倒是有点意思。

EXTI的定义在_generated.rs中由codegen生成的embassy_hal_internal::peripherals_definition!宏生成。

1
2
3
4
5
6
7
8
9
embassy_hal_internal::peripherals_definition!(
ADC1,
...
EXTI0,
EXTI1,
EXTI2,
EXTI3,
...
)

(_generated.rs)

这些外设信息来自芯片的CubeMX数据库。经过stm32-data和embassy-stm32宏的层层处理,实现了完善的类型限制和不同型号间高度的代码复用。

1
2
3
4
5
6
7
foreach_pin!(
($pin_name:ident, $port_name:ident, $port_num:expr, $pin_num:expr, $exti_ch:ident) => {
impl Pin for peripherals::$pin_name {
#[cfg(feature = "exti")]
type ExtiChannel = peripherals::$exti_ch;
}
...

(embassy-stm32\src\gpio.rs)

EXTI的Channel比较简单,一个IO数字对应一个Channel,一个宏就解决问题了。

而相对复杂的IO复用,也是通过codegen和宏实现的,比如:

1
2
3
4
impl_adc_pin!(ADC3, PC2, 12u8);
impl_adc_pin!(ADC3, PC3, 13u8);
pin_trait_impl!(crate::can::RxPin, CAN1, PA11, 9u8);
pin_trait_impl!(crate::can::TxPin, CAN1, PA12, 9u8);

(_generated.rs)

这种情况下就限制死了alternate function,从而在编译期就能发现问题,而且通过代码提示就能获知可用的IO而不用翻手册。这就是人们希望类型系统所做到的!

wait_for_high, wait_for_rising_edge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   /// Asynchronously wait until the pin is high.
///
/// This returns immediately if the pin is already high.
pub async fn wait_for_high(&mut self) {
let fut = ExtiInputFuture::new(self.pin.pin.pin.pin(), self.pin.pin.pin.port(), true, false);
if self.is_high() {
return;
}
fut.await
}
...
/// Asynchronously wait until the pin sees a rising edge.
///
/// If the pin is already high, it will wait for it to go low then back high.
pub async fn wait_for_rising_edge(&mut self) {
ExtiInputFuture::new(self.pin.pin.pin.pin(), self.pin.pin.pin.port(), true, false).await
}
...

这个self.pin.pin.pin.pin()有够吐槽的。解释起来是这样的:

ExtiInput.Input.FlexPin.PeripheralRef<AnyPin>.pin()

wait_for_high或是wait_for_rising_edge新建了一个ExtiInputFuture,我们来看看:

ExtiInputFuture<’a>

1
2
3
4
5
#[must_use = "futures do nothing unless you `.await` or poll them"]
struct ExtiInputFuture<'a> {
pin: u8,
phantom: PhantomData<&'a mut AnyPin>,
}

ExtiInputFuture并不存储外设实例,而只是寸一个pin_num,这也有利于所有权的编写。实际上,STM32也只有16个Channel嘛。

new和drop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    fn new(pin: u8, port: u8, rising: bool, falling: bool) -> Self {
critical_section::with(|_| {
let pin = pin as usize;
exticr_regs().exticr(pin / 4).modify(|w| w.set_exti(pin % 4, port));
EXTI.rtsr(0).modify(|w| w.set_line(pin, rising));
EXTI.ftsr(0).modify(|w| w.set_line(pin, falling));

// clear pending bit
#[cfg(not(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50)))]
EXTI.pr(0).write(|w| w.set_line(pin, true));
#[cfg(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50))]
{
EXTI.rpr(0).write(|w| w.set_line(pin, true));
EXTI.fpr(0).write(|w| w.set_line(pin, true));
}

cpu_regs().imr(0).modify(|w| w.set_line(pin, true));
});

Self {
pin,
phantom: PhantomData,
}
}
}

impl<'a> Drop for ExtiInputFuture<'a> {
fn drop(&mut self) {
critical_section::with(|_| {
let pin = self.pin as _;
cpu_regs().imr(0).modify(|w| w.set_line(pin, false));
});
}
}

new函数使用了一个critical_section。这是一个对整个线程的全局锁。

对于单核的MCU,实现方式是暂时禁用中断就行了。

对于多核,要禁用中断并使用一个spinlock。

new函数初始化Exti中断,就细不看了,最后一行设置了IMR(Interrupt mask register)寄存器,表示未屏蔽(Mask)该位。

image-20241021110821260

impl Future

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const EXTI_COUNT: usize = 16;
const NEW_AW: AtomicWaker = AtomicWaker::new();
static EXTI_WAKERS: [AtomicWaker; EXTI_COUNT] = [NEW_AW; EXTI_COUNT];
...
...
impl<'a> Future for ExtiInputFuture<'a> {
type Output = ();

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

let imr = cpu_regs().imr(0).read();
if !imr.line(self.pin as _) {
Poll::Ready(())
} else {
Poll::Pending
}
}
}

在这里实现了Future trait。

poll函数将waker注册到全局的EXTI_WAKERS中,然后读上面所说的Interrupt mask register来判断该通道是否已经否被屏蔽。Async Driver每次中断后都需要重新建一个Future,所以poll方法通过判断某位是否被屏蔽来确认是否为此通道。

提一下,AtomicWaker这个底层实现是,有Atomic的情况下用AtomicPtr实现,没有的话用Mutex实现。

中断

绑定

Embassy通过一系列宏将EXTI中断绑定到on_irq上。因为已经有IMR寄存器,所以不用再通过Vector ID确认中断的Channel ID,直接看IMR即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
macro_rules! foreach_exti_irq {
($action:ident) => {
foreach_interrupt!(
(EXTI0) => { $action!(EXTI0); };
(EXTI1) => { $action!(EXTI1); };
...
// plus the weird ones
(EXTI0_1) => { $action!( EXTI0_1 ); };
(EXTI15_10) => { $action!(EXTI15_10); };
...
);
};
}

macro_rules! impl_irq {
($e:ident) => {
#[allow(non_snake_case)]
#[cfg(feature = "rt")]
#[interrupt]
unsafe fn $e() {
on_irq()
}
};
}

on_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
unsafe fn on_irq() {
#[cfg(not(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50)))]
let bits = EXTI.pr(0).read().0;
#[cfg(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50))]
let bits = EXTI.rpr(0).read().0 | EXTI.fpr(0).read().0;

// We don't handle or change any EXTI lines above 16.
let bits = bits & 0x0000FFFF;

// Mask all the channels that fired.
cpu_regs().imr(0).modify(|w| w.0 &= !bits);

// Wake the tasks
for pin in BitIter(bits) {
EXTI_WAKERS[pin as usize].wake();
}

// Clear pending
#[cfg(not(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50)))]
EXTI.pr(0).write_value(Lines(bits));
#[cfg(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50))]
{
EXTI.rpr(0).write_value(Lines(bits));
EXTI.fpr(0).write_value(Lines(bits));
}

#[cfg(feature = "low-power")]
crate::low_power::on_wakeup_irq();
}

on_irq函数首先读了PR(Pending Register)(RPR,FPR,Rising Edge Pending Register,Falling Edge Pending Register),判断出此次需要处理的ExtiChannel。

然后清掉IMR(Interrupt mask register)对应位,防止该通道再次触发。

为了处理多个Channel都触发的情况,Embassy使用了一个BitIter,就在下面。然后迭代并wake对应的waker。

BitIter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct BitIter(u32);

impl Iterator for BitIter {
type Item = u32;

fn next(&mut self) -> Option<Self::Item> {
match self.0.trailing_zeros() {
32 => None,
b => {
self.0 &= !(1 << b);
Some(b)
}
}
}
}

trailing_zeros方法返回二进制表示中低位连续零的数量。如果所有位都是0则返回32。

然后,self.0 &= !(1 << b)将低位第一个非零位清零并返回该位的位置。

embedded_hal_async::digital

exti.rs还位ExtiInput实现了embedded_hal(略) 和 embedded_hal_async Trait:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
impl<'d> embedded_hal_async::digital::Wait for ExtiInput<'d> {
async fn wait_for_high(&mut self) -> Result<(), Self::Error> {
self.wait_for_high().await;
Ok(())
}

async fn wait_for_low(&mut self) -> Result<(), Self::Error> {
self.wait_for_low().await;
Ok(())
}

async fn wait_for_rising_edge(&mut self) -> Result<(), Self::Error> {
self.wait_for_rising_edge().await;
Ok(())
}

async fn wait_for_falling_edge(&mut self) -> Result<(), Self::Error> {
self.wait_for_falling_edge().await;
Ok(())
}

async fn wait_for_any_edge(&mut self) -> Result<(), Self::Error> {
self.wait_for_any_edge().await;
Ok(())
}
}

啊,好水的文章啊……


从EXTI实现看Embassy异步Rust嵌入式框架
https://decaday.github.io/blog/embassy-exti/
作者
星期十
发布于
2024年10月21日
更新于
2024年10月21日
许可协议