中断绑定
主线位置:中断主线的第 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。
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。
问: 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 位的芯片上相邻几级会合并。