Rust in Everything
基于 embassy-stm32 0.6.0

中断绑定

主线位置:中断主线的第 1 步。bind_interrupts! 把 EXTI15_10 这个中断向量接到 embassy-stm32 的处理函数,同时生成一个 Irqs 类型证明「已经接上了」;ExtiInput::new(.., Irqs) 在编译期检查这份证明。驱动拿到中断之后怎么登记 waker、处理函数怎么唤醒任务,见 GPIO 与 EXTI。

flowchart TB
  user["bind_interrupts!"] --> fnsym["fn EXTI15_10 no_mangle"]
  user --> irqs["struct Irqs"]
  irqs --> impl["impl Binding"]
  fnsym --> vt["__INTERRUPTS 第 40 项"]
  devx["device.x PROVIDE"] --> vt
  vt --> hw["NVIC 跳转"]
  hw --> fnsym
  fnsym --> handler["Handler::on_interrupt"]
  impl --> drv["ExtiInput::new 检查"]

宏在 embassy-stm32 的 lib.rs(不能放进 hal-internal,宏里要用 $crate 指向 HAL 自己);中断的类型层由 embassy-hal-internal 0.5.0 的 interrupt.rs(870 行)里的 interrupt_mod! 生成;向量表本身在 stm32-metapac 21.0.0 里。实验在 exp-embassy/hal-qemu,thumbv7em-none-eabihf,release。宿主是 M2 Max、macOS 27、rustc 1.99.0。

宏展开成什么

bind_interrupts! 的主体:

        #[derive(Copy, Clone)]
        $(#[$outer])*
        $vis struct $name;

        $(
            #[allow(non_snake_case)]
            #[unsafe(no_mangle)]
            $(#[cfg($cond_irq)])?
            $(#[doc = $doc])*
            unsafe extern "C" fn $irq() {
                unsafe {
                    $(
                        $(#[cfg($cond_handler)])?
                        <$handler as $crate::interrupt::typelevel::Handler<$crate::interrupt::typelevel::$irq>>::on_interrupt();

                    )*
                }
            }

            $(#[cfg($cond_irq)])?
            $crate::bind_interrupts!(@inner
                $(
                    $(#[cfg($cond_handler)])?
                    unsafe impl $crate::interrupt::typelevel::Binding<$crate::interrupt::typelevel::$irq, $handler> for $name {}
                )*
            );
        )*

用户写的是:

bind_interrupts!(struct Irqs {
  EXTI15_10 => exti::InterruptHandler<interrupt::typelevel::EXTI15_10>;
});

没有 cargo-expand,用 RUSTC_BOOTSTRAP=1 cargo rustc --bin exti -- -Zunpretty=expanded 让稳定版 rustc 打印展开结果,这一段原样是:

struct Irqs;
#[automatically_derived]
impl ::core::marker::Copy for Irqs { }
// ... Clone
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
unsafe extern "C" fn EXTI15_10() {
    unsafe {
        <exti::InterruptHandler<interrupt::typelevel::EXTI15_10> as
                ::embassy_stm32::interrupt::typelevel::Handler<::embassy_stm32::interrupt::typelevel::EXTI15_10>>::on_interrupt();
    }
}
unsafe impl ::embassy_stm32::interrupt::typelevel::Binding<::embassy_stm32::interrupt::typelevel::EXTI15_10,
    exti::InterruptHandler<interrupt::typelevel::EXTI15_10>> for Irqs {}

两件事:一个名字恰好叫 EXTI15_10、不做名字修饰的函数;一个空结构体 Irqs,带一条 Binding 实现。一个向量后面可以写多个处理函数(逗号分隔),函数体按顺序逐个调用;一个 Irqs 也可以绑多个向量。

函数怎么进向量表

stm32-metapac 21.0.0 的 src/chips/stm32l475vg/pac.rs 在 rt feature 下声明了 80 个外部函数,并放了一张表:

    #[unsafe(link_section = ".vector_table.interrupts")]
    #[unsafe(no_mangle)]
    pub static __INTERRUPTS: [Vector; 82] = [
        Vector { _handler: WWDG },
        Vector { _handler: PVD_PVM },
        // ...

82 项里有 2 项保留位。同目录的 device.x 给每个名字一个弱默认值,PROVIDE(EXTI15_10 = DefaultHandler);,cortex-m-rt 的链接脚本把它包含进来。用户代码里出现了真正的 EXTI15_10 符号,PROVIDE 就不生效。在 size_exti 程序(下文测体积用的那个)的 ELF 里看 .vector_table:

 80000e0 e1010008 bf170008 bf170008 bf170008  ................
080017be T DefaultHandler
080017be T EXTI0
080001e0 T EXTI15_10
080017be T EXTI9_5

偏移 0xe0 是 16 个系统异常之后的第 40 项(EXTI15_10 = 40),值 0x080001e1 是 EXTI15_10 的地址加 Thumb 位;其余没绑的向量(EXTI0、EXTI9_5……)都指向 DefaultHandler 的 0x080017bf。没有运行期的处理函数表,也没有注册调用:中断来了,硬件直接跳进宏生成的函数。

类型层:每个中断一个类型

interrupt_mod! 由 build.rs 以芯片的全部中断名调用一次(L475 是 80 个)。它生成 interrupt::typelevel 模块,每个中断一个没有值的枚举类型:

                $(
                    #[allow(non_camel_case_types)]
                    #[doc=stringify!($irqs)]
                    #[doc=" typelevel interrupt."]
                    pub enum $irqs {}
                    impl SealedInterrupt for $irqs{}
                    impl Interrupt for $irqs {
                        const IRQ: super::Interrupt = super::Interrupt::$irqs;
                    }
                )*

Interrupt trait 用关联常量 IRQ 连回 PAC 里普通的 Interrupt 枚举,enable、pend、set_priority 这些默认方法都转给 Self::IRQ。同一个模块里还有两个 trait:

                pub trait Handler<I: Interrupt> {
                    // ...
                    unsafe fn on_interrupt();
                }

                // ...
                pub unsafe trait Binding<I: Interrupt, H: Handler<I>>: Copy {}

Handler<I> 由驱动实现:「这个类型知道 I 来了该做什么」。Binding<I, H> 是 unsafe trait,实现它就是向编译器断言「每次 I 触发,H::on_interrupt() 都会被调用」。用户不手写这条 unsafe impl,宏替你写,并且只在它同时生成了那个向量函数的时候写。Binding 要求 Copy,Irqs 是零大小的单元结构体,可以随手按值传给多个驱动。

驱动怎么要求证明

驱动构造函数多一个参数,类型是 impl Binding<..>,值本身不用。ExtiInput::new:

    pub fn new<T: ExtiPin + GpioPin>(
        pin: Peri<'d, T>,
        _ch: Peri<'d, T::ExtiChannel>,
        pull: Pull,
        _irq: impl Binding<
            <<T as ExtiPin>::ExtiChannel as Channel>::IRQ,
            InterruptHandler<<<T as ExtiPin>::ExtiChannel as Channel>::IRQ>,
        >,
    ) -> Self {

PC13 的 ExtiChannel 是 EXTI13,EXTI13 的 IRQ 是 _generated::peripheral_interrupts::EXTI::EXTI13,build.rs 从芯片元数据生成的别名:

  pub mod EXTI {
    pub type EXTI0 = crate::interrupt::typelevel::EXTI0;
    // ...
    pub type EXTI13 = crate::interrupt::typelevel::EXTI15_10;
    // ...
    pub type EXTI5 = crate::interrupt::typelevel::EXTI9_5;

于是「PC13 要绑哪个向量」变成一次类型推导。绑错向量(EXTI9_5)时 rustc 的原文(exp-embassy/hal-qemu/errs/bind_wrong.txt,路径里的 qemu-patched 是模板的 [patch] 指向的副本,和 0.6.0 只差 bd.rs 一行):

error[E0277]: the trait bound `Irqs: Binding<EXTI15_10, InterruptHandler<EXTI15_10>>` is not satisfied
   --> src/bin/err_bind_wrong.rs:21:65
    |
 21 |   let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Down, Irqs);
    |                    --------------                               ^^^^ unsatisfied trait bound
    |                    |
    |                    required by a bound introduced by this call
    |
help: the trait `Binding<embassy_stm32::interrupt::typelevel::EXTI15_10, InterruptHandler<embassy_stm32::interrupt::typelevel::EXTI15_10>>` is not implemented for `Irqs`
      but trait `Binding<embassy_stm32::interrupt::typelevel::EXTI9_5, InterruptHandler<embassy_stm32::interrupt::typelevel::EXTI9_5>>` is implemented for it
   --> src/bin/err_bind_wrong.rs:14:1
    |
 14 | / bind_interrupts!(struct Irqs {
 15 | |   EXTI9_5 => exti::InterruptHandler<interrupt::typelevel::EXTI9_5>;
 16 | | });
    | |__^
note: required by a bound in `ExtiInput::<'d, embassy_stm32::mode::Async>::new`
   --> ~/exp-embassy/qemu-patched/embassy-stm32/src/exti/mod.rs:136:20
...

报错同时说了「要的是什么」(EXTI15_10)和「你给的是什么」(EXTI9_5)。Irqs 里压根没有 USART1 的绑定,却拿去建 BufferedUart(errs/bind_missing.txt):

error[E0277]: the trait bound `Irqs: Binding<USART1, BufferedInterruptHandler<USART1>>` is not satisfied
   --> src/bin/err_bind_missing.rs:23:93
    |
 23 |   let _uart = BufferedUart::new(p.USART1, p.PB7, p.PB6, TX.init([0; 32]), RX.init([0; 32]), Irqs, Config::default());
    |               ----------------- required by a bound introduced by this call                 ^^^^ unsatisfied trait bound
    |
help: the trait `Binding<embassy_stm32::interrupt::typelevel::USART1, BufferedInterruptHandler<embassy_stm32::peripherals::USART1>>` is not implemented for `Irqs`
      but trait `Binding<embassy_stm32::interrupt::typelevel::EXTI15_10, embassy_stm32::exti::InterruptHandler<embassy_stm32::interrupt::typelevel::EXTI15_10>>` is implemented for it
...

同一个向量在两处 bind_interrupts! 里各绑一次,两个 #[no_mangle] 函数同名,rustc 在编译期就拦下(errs/double.txt):

error: symbol `EXTI15_10` is already defined
  --> src/bin/err_double.rs:17:3
   |
17 | /   bind_interrupts!(pub struct Irqs2 {
18 | |     EXTI15_10 => exti::InterruptHandler<interrupt::typelevel::EXTI15_10>;
19 | |   });
   | |____^

一个向量对应多个外设时(L475 的 EXTI15_10 管 EXTI10 到 EXTI15),exti::InterruptHandler 故意对中断泛型而不是对通道泛型:PC13 和 PB10 都要求 Binding<EXTI15_10, InterruptHandler<EXTI15_10>>,同一条绑定满足两者,向量只绑一次。处理函数里统一读挂起寄存器,见 GPIO 与 EXTI。

📝NOTE
冷知识:中断曾经是一个运行期参数

PR #1479(「Move stm32 to bind_interrupts」,2023-05,首个包含它的发布是 embassy-stm32 v0.1.0)之前,stm32 驱动的构造函数多一个参数 irq: impl Peripheral<P = T::Interrupt> + 'd,用户先写 let irq = interrupt::take!(USART3); 把中断当成一个外设单例拿出来,驱动在构造时调 irq.set_handler(on_interrupt::<T>)、irq.unpend(),在运行期把处理函数登记进去。这个提交把 BufferedUart 这类驱动的处理逻辑搬进 impl interrupt::Handler<T::Interrupt> for InterruptHandler<T>,参数改成 Binding,例子里的 interrupt::take! 换成了 bind_interrupts!(struct Irqs { USART3 => usart::BufferedInterruptHandler<peripherals::USART3>; })。宏本身更早出现在 embassy-nrf(2023-02 的提交 42c13c8c3「nrf: add new interrupt binding traits and macro.」)。现在中断向量在链接期就定死,没有处理函数表、没有间接调用。

优先级与开关

驱动在构造时打开自己的中断,例如 BufferedUart 构造的最后两行是 info.interrupt.unpend(); unsafe { info.interrupt.enable() };(info.interrupt 是 PAC 的普通 Interrupt 枚举值,来自类型层的 T::Interrupt::IRQ)。这些方法落到 InterruptExt,对 PAC 的普通 Interrupt 枚举做 NVIC::unmask、NVIC::pend。enable 是 unsafe:一旦打开,处理函数就可能在任何时刻抢占当前代码,调用者要保证数据结构已经准备好。

set_priority 在 ARMv6-M 上要进临界区(优先级寄存器只能按 32 位读改写),在 v7 以上直接一次 8 位写。Priority 的取值由 hal-internal 的 prio-bits-* feature 决定,embassy-stm32 的 Cargo.toml 写死了 features = ["cortex-m", "prio-bits-4"],所以 Priority 永远是这 16 级:

pub enum Priority {
    P0 = 0x0,
    P1 = 0x10,
    P2 = 0x20,
    // ...
    P15 = 0xf0,
}

数字越小越优先,P0 最高。init 用 Config 里的 dma_interrupt_priority / bdma_interrupt_priority(默认 P0)给所有 DMA 通道的中断设优先级并打开;EXTI 的所有向量也在 init 里打开,优先级保持复位值 0。

📝NOTE
没有蠢问题

问: STM32G0 是 Cortex-M0+,NVIC 只有 2 个优先级位,也能用 P1 到 P15 吗?

答: 能编译,但效果不是 16 级。stm32-metapac 21.0.0 里 375 款芯片的 nvic_priority_bits 是 Some(2)、17 款是 Some(3),embassy-stm32 却对所有芯片都开 prio-bits-4。P1 写进去的是 0x10,2 位的 NVIC 只保留最高两位,0x00 到 0x30 都落在同一级,P0 到 P3 没有差别,能区分的只有 P0、P4、P8、P12。PAC 导出了 NVIC_PRIO_BITS(embassy-stm32 的 lib.rs 在 rt 下重新导出),选优先级前可以看一眼。

想把一个中断留给自己写处理函数(例如 ExtiInput::new_blocking 的阻塞用法、或给 InterruptExecutor 借一个空闲中断,见各平台的执行器),就不要在 bind_interrupts! 里写它,改用 embassy_stm32::interrupt 重新导出的 cortex-m-rt #[interrupt] 属性;两种写法定义同一个向量会撞上前面那个「symbol is already defined」。

和 mio 对照:mio 的事件来自内核,register 是一次运行期系统调用;这里的「注册」是链接器把一个函数地址填进 flash 里的一张表,运行期什么也不发生。

小结

  • bind_interrupts! 生成两样东西:名字等于中断名的 #[no_mangle] extern "C" fn,以及空结构体上的 unsafe impl Binding<I, H>。
  • 向量表是 stm32-metapac 里的 __INTERRUPTS 静态数组,没绑的项由 device.x 的 PROVIDE 指向 DefaultHandler;绑了的项由链接器直接填入宏生成的函数,没有运行期注册。
  • interrupt_mod! 给每个中断生成一个类型;驱动的 _irq: impl Binding<..> 参数是零大小的编译期证明,漏绑、绑错向量都是 E0277,报错里同时列出「需要的」和「已有的」绑定。
  • 多路复用的向量(EXTI15_10)由处理函数对中断泛型来保证只绑一次;同一向量绑两次是编译错误。
  • Priority 总按 4 个优先级位生成 16 级,在 2 位或 3 位的芯片上相邻几级会合并。

💬 关联讨论

发起讨论
加载中...