本文最后更新于 2025年2月1日 下午
Embassy是一个基于Rust的异步嵌入式开发框架:
Embassy: The next-generation framework for embedded applications
它不仅包含了异步运行时,还提供了STM32、RP2xxx,NRF等芯片的异步HAL实现、Bootloader、usb,蓝牙等。
此外,乐鑫官方的esp-rs 也是将embassy作为默认框架使用。
最近研究了embassy-stm32的部分实现,写在博客里作为记录吧。Exti最简单也有点Async味,就先写这个吧。
注意:Embassy尚未1.0,此文可能在您读的时候已经过时。为了博客的清晰,部分代码被简化。
文件路径:embassy/embassy-stm32/src · embassy-rs/embassy
从顶向下看。
1 2 3 4 5 6 7 8 9 10 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 > { 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); assert_eq! (pin.pin (), ch.number ()); Self { pin: Input::new (pin, pull), } } ...
new函数没啥好说的。impl Peripheral<P = T::ExtiChannel>倒是有点意思。
EXTI的定义在_generated.rs
(由build.rs
生成的)中的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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 } ... 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
,我们来看看:
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)); #[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,实现方式是暂时禁用中断就行了。对于多核,实现cs要禁用中断并使用一个spinlock。
new函数初始化Exti中断,就细不看了,最后一行设置了IMR(Interrupt mask register)寄存器,表示未屏蔽(Mask)该位。
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,以便 ExtiInputFuture
可以用于 async
/await
机制。
Future
trait 代表一个异步计算的结果,可以被轮询(poll)以检查是否完成。 在 poll
方法中,我们做了以下几件事:
注册 waker
: EXTI_WAKERS
是一个全局的 AtomicWaker
数组,每个 pin
对应一个 AtomicWaker
,用于存储 waker
。 poll
调用时会将 waker
存入 EXTI_WAKERS[self.pin as usize]
,这样当中断发生时,可以使用这个 waker
唤醒 Future
。
检查 IMR(Interrupt Mask Register) : imr.line(self.pin as _)
读取 IMR 寄存器的对应位,判断该通道是否被屏蔽。
如果 IMR 该位被清零,说明中断已触发(即通道未被屏蔽),返回 Poll::Ready(())
,表示 Future
已完成。
否则返回 Poll::Pending
,等待下一次 wake
调用。
提一下,AtomicWaker这个底层实现在embassy-sync 中,平台有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); }; ... (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 () } }; }
因为EXTI中断比较复杂,有多个外设共用一个中断向量的情况,而且不同的系列共用中断向量的情况还不一样,在exti上难以使用bind_irqs!
这样的模式、embassy_stm32的其它外设,以及embassy_rp等hal都是使用的bind_irqs!
。这其实是将更多的中断访问权交给了用户。
但是exti就不行了,想要让hal不占用中断向量,就只能关闭exti
feature来关闭整个模块。
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 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 ; ... cpu_regs ().imr (0 ).modify (|w| w.0 &= !bits); for pin in BitIter (bits) { EXTI_WAKERS[pin as usize ].wake (); } #[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)); } ... }
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。
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 unsafe fn on_irq() { let bits = EXTI.pr(0) .read () .0 ; let bits = EXTI.rpr(0) .read () .0 | EXTI.fpr(0) .read () .0 ; ... // 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 EXTI.pr(0) .write_value (Lines(bits) ); { EXTI.rpr(0) .write_value (Lines(bits) ); EXTI.fpr(0) .write_value (Lines(bits) ); } ... }
on_irq
函数的主要作用是在外部中断发生时,处理触发的 ExtiChannel
并唤醒相应的 Future
。
读取PR
(Pending Register)或者 RPR/FPR
(Rising/Falling Edge Pending Register)来确定哪些 ExtiChannel
需要被处理。
通过修改 IMR
(Interrupt Mask Register),屏蔽已触发的中断通道,以防止重复触发。
通过 BitIter(bits)
遍历所有触发的 pin
,并调用 EXTI_WAKERS[pin as usize].wake()
唤醒相应的 Future
。这个BitIter
会在下面降到
在 EXTI.pr
或 EXTI.rpr/EXTI.fpr
中清除对应的位,以便后续的中断可以正确触发。
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) } } } }
BitIter
是一个简单的位迭代器,用于遍历 bits
中的所有 1
位。
trailing_zeros()
返回最低有效位(LSB)之前 0
的个数。然后self.0 &= !(1 << b)
清除该位,以便在下一次 next()
调用时继续遍历。
这种方式确保了 on_irq
处理多个 EXTI
事件时能够逐一唤醒对应的 Future
。
embedded_hal 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 (()) } }
然后我们就可以愉快地使用:
button.wait_for_low().await
啦!
啊,好水的文章啊……