找回密码
 立即注册
首页 业界区 业界 从EXTI实现看Embassy: 异步Rust嵌入式框架

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

梳踟希 昨天 16:14
从EXTI实现看Embassy: 异步Rust嵌入式框架

原文链接:https://decaday.github.io/blog/embassy-exti/
Embassy是一个基于Rust的异步嵌入式开发框架:
Embassy: The next-generation framework for embedded applications
Embassy不仅包含了异步运行时,还提供了STM32、RP2xxx,NRF等芯片的异步HAL实现、usb、[蓝牙(trouble)](embassy-rs/trouble: A Rust Host BLE stack with a future goal of qualification.)等,乐鑫官方的esp-rs也是将embassy作为默认框架使用。
最近研究了embassy-stm32的部分实现,写在博客里作为记录吧。Exti最简单也有点Async味,就先写这个吧。
注意:本文撰写时,Embassy尚未1.0 release,此文可能在您读的时候已经过时。为了博客的清晰,部分代码被简化。
EXTI

EXTI 是 Extended Interrupts and Events Controller 的缩写,即“扩展中断和事件控制器”。
它的核心作用可以概括为一句话:让STM32能够响应来自外部(或内部通道)的异步信号,如IO上升沿、IO高电平,并在这些事件发生时触发中断或事件请求,从而执行特定的任务,尤其擅长将MCU从低功耗模式中唤醒。
embassy-stm32的exti驱动,我们从顶向下看。
源码链接:embassy/embassy-stm32/src · embassy-rs/embassy
整个代码的逻辑如下:
1.png

ExtiInput,}[/code]这是可被用户直接使用的ExtiInput类型。
其内部包含了一个Input类型(其实Input类型内部也是包含了一个FlexPin类型)
构造函数
  1. /// EXTI input driver.
  2. ///
  3. /// This driver augments a GPIO `Input` with EXTI functionality. EXTI is not
  4. /// built into `Input` itself because it needs to take ownership of the corresponding
  5. /// EXTI channel, which is a limited resource.
  6. ///
  7. /// Pins PA5, PB5, PC5... all use EXTI channel 5, so you can't use EXTI on, say, PA5 and PC5 at the same time.
  8. pub struct ExtiInput<'d> {
  9.   pin: Input<'d>,
  10. }
复制代码
new函数我们主要说一下 impl Peripheral:

  • impl Peripheral: 表明 pin 必须是一个实现了 Peripheral trait 的类型。 Peripheral 用来标记硬件外设所有权,来自embassy-hal-internal。

  • : 这是一个关联类型约束,意味着这个外设的实体类型就是泛型 T(比如 peripherals:A4)。

  • :T::ExtiChannel是Trait T的关联类型,这个我们将在下面看到。它意味着这个外设的实体类型要与 “与T对应的ExtiChannel” 的类型匹配。
  • + 'd: 这是一个生命周期约束,确保传入的外设引用至少和 ExtiInput 实例活得一样长。这在处理外设的可变借用时非常重要。
这个类型限制是这样的:
T是GpioPin,是某个引脚的类型(比如PA4,PA5,都是单独的类型,都可以是T)
pin 参数要走了 T 的所有权,目的是使得用户无法直接将PA4再用作I2C。其形式通常是单例Singleton,也就是传统rust hal库结构的let p = Peripheral.take() 所获得的外设的所有权(以后可能单独写博客讲单例)。
ch 参数限定了其自身必须是T的关联类型ExtiChannel (P = T::ExtiChannel),我们在下面细说,这要求了channel必须与pin对应,比如PA4必须提供EXTI4。
类型系统

EXTI单例(Singleton)类型的定义在_generated.rs(由build.rs生成的)中的embassy_hal_internal::peripherals_definition!宏中。
  1. impl<'d> ExtiInput<'d> {
  2.     /// Create an EXTI input.
  3.     pub fn new<T: GpioPin>(
  4.         pin: impl Peripheral<P = T> + 'd,
  5.         ch: impl Peripheral<P = T::ExtiChannel> + 'd,
  6.         pull: Pull,
  7.     ) -> Self {
  8.         into_ref!(pin, ch);
  9.         // Needed if using AnyPin+AnyChannel.
  10.         assert_eq!(pin.pin(), ch.number());
  11.         Self {
  12.             pin: Input::new(pin, pull),
  13.         }
  14.     }
  15.     ...
复制代码
这些外设信息来自芯片的CubeMX数据库。经过stm32-data和embassy-stm32宏的层层处理,实现了完善的类型限制和不同型号间高度的代码复用。
Channel Trait

Exit的Channel Trait使用了密封(Sealed)Trait,这样可以保证Channel Trait在包外可见,但是不能在外部被实现(因为外部实现privite trait SealedChannel )
  1. // (embassy-stm32/target/thumbv7em-none-eabi/.../out/_generated.rs)
  2. embassy_hal_internal::peripherals_definition!(
  3.     ADC1,
  4.     ...
  5.     EXTI0,
  6.     EXTI1,
  7.     EXTI2,
  8.     EXTI3,
  9.     ...
  10. )
复制代码
在实现上比较简单,embassy-stm32使用宏来简化了代码。
  1. trait SealedChannel {}
  2. #[allow(private_bounds)]
  3. pub trait Channel: SealedChannel + Sized {
  4.     /// Get the EXTI channel number.
  5.     fn number(&self) -> u8;
  6.     /// Type-erase (degrade) this channel into an `AnyChannel`.
  7.     ///
  8.     /// This converts EXTI channel singletons (`EXTI0`, `EXTI1`, ...), which
  9.     /// are all different types, into the same type. It is useful for
  10.     /// creating arrays of channels, or avoiding generics.
  11.     fn degrade(self) -> AnyChannel {
  12.         AnyChannel { number: self.number() as u8, }
  13.     }
  14. }
复制代码
Pin Trait

Pin Trait同样使用了Sealed Trait。AnyPin部分我们先不研究,我们只看Exti部分:Pin Trait设置了一个关联类型,指向exti::Channel Trait。
  1. macro_rules! impl_exti {
  2.     ($type:ident, $number:expr) => {
  3.         impl SealedChannel for peripherals::$type {}
  4.         impl Channel for peripherals::$type {
  5.             fn number(&self) -> u8 {
  6.                 $number
  7.             }
  8.         }
  9.     };
  10. }
  11. impl_exti!(EXTI0, 0);
  12. impl_exti!(EXTI1, 1);
  13. impl_exti!(EXTI2, 2);
  14. impl_exti!(EXTI3, 3);
  15. // ...
复制代码
在Impl上也是用了大量的codegen和宏,其最终是 foreach_pin 这个宏:(foreach_pin的原型在build.rs生成的_macro.rs内,稍微有点绕,不再详细叙述)
  1. // embassy-stm32/src/gpio.rs
  2. pub trait Pin: Peripheral<P = Self> + Into + SealedPin + Sized + 'static {
  3.     /// EXTI channel assigned to this pin. For example, PC4 uses EXTI4.
  4.     #[cfg(feature = "exti")]
  5.     type ExtiChannel: crate::exti::Channel;
  6.    
  7.     #[inline] // Number of the pin within the port (0..31)
  8.     fn pin(&self) -> u8 { self._pin() }
  9.     #[inline] // Port of the pin
  10.     fn port(&self) -> u8 { self._port() }
  11.     /// Type-erase (degrade) this pin into an `AnyPin`.
  12.     ///
  13.     /// This converts pin singletons (`PA5`, `PB6`, ...), which
  14.     /// are all different types, into the same type. It is useful for
  15.     /// creating arrays of pins, or avoiding generics.
  16.     #[inline]
  17.     fn degrade(self) -> AnyPin {
  18.         AnyPin {
  19.             pin_port: self.pin_port(),
  20.         }
  21.     }
  22. }
复制代码
其它IO复用也是通过codegen和宏实现的。比如,经过数据处理后,可能生成这样的代码:
  1. // (embassy-stm32/src/gpio.rs)
  2. foreach_pin!(
  3.     ($pin_name:ident, $port_name:ident, $port_num:expr, $pin_num:expr, $exti_ch:ident) => {
  4.         impl Pin for peripherals::$pin_name {
  5.             #[cfg(feature = "exti")]
  6.             type ExtiChannel = peripherals::$exti_ch;
  7.         }
  8.         impl SealedPin for peripherals::$pin_name { /* ... */}
  9.         impl From<peripherals::$pin_name> for AnyPin { /* ... */}
  10.     };
  11. );
复制代码
这种情况下就限制死了alternate function,从而在编译期就能发现问题,而且通过代码提示就能获知可用的IO而不用翻手册。不得不说,这就是人们希望类型系统所做到的!
wait_for_high
  1. // (_generated.rs)
  2. impl_adc_pin!(ADC3, PC2, 12u8);
  3. impl_adc_pin!(ADC3, PC3, 13u8);
  4. pin_trait_impl!(crate::can::RxPin, CAN1, PA11, 9u8);
  5. pin_trait_impl!(crate::can::TxPin, CAN1, PA12, 9u8);
复制代码
这个self.pin.pin.pin.pin()有够吐槽的。解释起来是这样的: ExtiInput.Input.FlexPin.PeripheralRef.pin()。
我们看见的wait_for_high或是wait_for_rising_edge新建了一个ExtiInputFuture,我们来看看:
ExtiInputFuture {    pin: u8,    phantom: PhantomData Drop for ExtiInputFuture Future for ExtiInputFuture) -> Poll {        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:ending        }    }}[/code]在这里我们实现了 Future trait。使得 ExtiInputFuture 可以用于 async/await 机制。
Future trait 代表一个异步计算/运行的结果,可以被执行器(executor)轮询(poll)以检查是否完成。 在 poll 方法中,我们做了以下几件事:

  • 注册 waker : waker是唤醒器。因为持续的轮询会消耗大量的cpu资源(如果持续poll,那就是nb模式)。所以,一个聪明的executor仅第一次和被waker唤醒后,才会执行一次poll。这里的唤醒者是中断函数。
    EXTI_WAKERS 是一个全局的 AtomicWaker 数组,每个 pin 对应一个 AtomicWaker,用于存储 waker。 poll 调用时会将 waker 存入 EXTI_WAKERS[self.pine],这样当中断发生时,可以使用这个 waker 唤醒 Future。
  • 检查中断是否发生:它通过检查IMR寄存器判断中断是否发生。因为我们的中断函数(on_irq)在触发后会立刻通过imr(0).modify(|w| w.0 &= !bits)来屏蔽该中断线。所以,如果在poll时发现IMR位被清零了(即被屏蔽了),就说明在我们await的这段时间里,中断已经来过了。这时就可以返回Poll::Ready了。如果IMR位仍然是1(未屏蔽),则说明中断还没来,返回Poll:ending继续等待。” 这样就把poll和on_irq的行为联系起来了,逻辑更清晰。
提一下,AtomicWaker这个底层实现在embassy-sync中,平台有Atomic的情况下用AtomicPtr实现,没有的话用Mutex实现。
中断

on_irq
  1. /// Asynchronously wait until the pin is high.
  2. ///
  3. /// This returns immediately if the pin is already high.
  4. pub async fn wait_for_high(&mut self) {
  5.     let fut = ExtiInputFuture::new(self.pin.pin.pin.pin(), self.pin.pin.pin.port(), true, false);
  6.     if self.is_high() {
  7.         return;
  8.     }
  9.     fut.await
  10. }
  11. ...
  12. /// Asynchronously wait until the pin sees a rising edge.
  13. ///
  14. /// If the pin is already high, it will wait for it to go low then back high.
  15. pub async fn wait_for_rising_edge(&mut self) {
  16.     ExtiInputFuture::new(self.pin.pin.pin.pin(), self.pin.pin.pin.port(), true, false).await
  17. }
  18. ...
复制代码
on_irq 函数的主要作用是在外部中断发生时,处理触发的 ExtiChannel 并唤醒相应的 Future。

  • 读取PR(Pending Register)或者 RPR/FPR(Rising/Falling Edge Pending Register)因为多个EXTI线可能共用一个中断向量,所以on_irq首先读取PR来确定具体是哪些线触发了中断。
  • 通过修改 IMR(Interrupt Mask Register),屏蔽已触发的中断通道,以防止重复触发。
  • 为了处理多个Channel都触发的情况,Embassy通过 BitIter(bits) 遍历所有触发的 pin,并调用 EXTI_WAKERS[pin as usize].wake() 唤醒相应的 Future。这个BitIter会在下面讲到。
  • 在 EXTI.pr 或 EXTI.rpr/EXTI.fpr 中清除对应的位,以便后续的中断可以正确触发。
绑定

Embassy通过一系列宏将EXTI中断绑定到on_irq上。
  1. #[must_use = "futures do nothing unless you `.await` or poll them"]
  2. struct ExtiInputFuture<'a> {
  3.     pin: u8,
  4.     phantom: PhantomData<&'a mut AnyPin>,
  5. }
复制代码
因为EXTI中断比较复杂,有多个外设共用一个中断向量的情况,而且不同的系列共用中断向量的情况还不一样,在exti上难以使用bind_irqs!这样的模式、embassy_stm32的其它外设,以及embassy_rp等hal都是使用的bind_irqs!。这其实是将更多的中断访问权交给了用户。
但是exti就不行了,想要让hal不占用中断向量,就只能关闭exti feature来关闭整个模块,或者关闭rt feature,自行管理启动和所有中断。
BitIter
  1.     fn new(pin: u8, port: u8, rising: bool, falling: bool) -> Self {
  2.         critical_section::with(|_| {
  3.             let pin = pin as usize;
  4.             exticr_regs().exticr(pin / 4).modify(|w| w.set_exti(pin % 4, port));
  5.             EXTI.rtsr(0).modify(|w| w.set_line(pin, rising));
  6.             EXTI.ftsr(0).modify(|w| w.set_line(pin, falling));
  7.             // clear pending bit
  8.             #[cfg(not(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50)))]
  9.             EXTI.pr(0).write(|w| w.set_line(pin, true));
  10.             #[cfg(any(exti_c0, exti_g0, exti_u0, exti_l5, exti_u5, exti_h5, exti_h50))]
  11.             {
  12.                 EXTI.rpr(0).write(|w| w.set_line(pin, true));
  13.                 EXTI.fpr(0).write(|w| w.set_line(pin, true));
  14.             }
  15.             cpu_regs().imr(0).modify(|w| w.set_line(pin, true));
  16.         });
  17.         Self {
  18.             pin,
  19.             phantom: PhantomData,
  20.         }
  21.     }
  22. }
  23. impl<'a> Drop for ExtiInputFuture<'a> {
  24.     fn drop(&mut self) {
  25.         critical_section::with(|_| {
  26.             let pin = self.pin as _;
  27.             cpu_regs().imr(0).modify(|w| w.set_line(pin, false));
  28.         });
  29.     }
  30. }
复制代码
然后我们就可以愉快地使用:
button.wait_for_low().await啦!
总结

这个EXTI模块复杂性比较低,主要用于EXTI最低级也是最常用的用法:等待上升沿、等待高电平等。
但是由于stm32系列太多,又有很多EXTI15_10这种共用向量情况,embassy-stm32直接接管了所有EXTI中断(对于普通向量则一般使用bind_interrupts的模式),所以如果用户想用EXTI完成更加复杂和即时的操作,就只能关闭exti feature来关闭整个模块,或者关闭rt feature,自行管理启动和所有中断。
Embassy HAL设计了一套优秀的类型系统和HAL范式,为社区提供了学习榜样。其类型系统一部分在embassy-hal-internal中完成,一部分在HAL内部完成。通过这套类型系统和约束,我们可以避免很多恼人的错误,也能很大程度上简化代码(比如,永远不会设置错、忘设置IO AF,也不用再去查AF表)。
embassy-stm32 的创新主要是其codegen和metapac:使用了复杂的数据预处理和codegen实现了对stm32外设的包罗万象。stm32-data 通过来自CubeMX等的数据,生成带有元数据的PAC:stm32-metapac,避免了像stm32-rs 一样的重复和分散、不统一的代码。
当然,包罗万象是有代价的。我们日后可以详细聊聊。
在Embassy范式的影响下,我编写和维护了py32-hal 和 sifli-rs ,包含了对embassy大量的直接 Copy 借鉴,这两套hal分别针对Puya的低成本MCU如PY32F002和SiFli的M33蓝牙MCU SF32LB52。了解一下?
原文链接:https://decaday.github.io/blog/embassy-exti/
我的github: https://github.com/decaday
本文以CC-BY-NC许可发布,当您转载该文章时,需要保留署名,且不能用于商业用途。特别地,不能转载到C**N平台。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册