Skip to content

Commit 54a153a

Browse files
authored
Merge pull request #4922 from WillaWillNot/anybinding_and_bindable_exti
Bindable EXTI interrupts, and removal of AnyChannel support for ExtiInput
2 parents eb4e410 + b877b0d commit 54a153a

File tree

42 files changed

+357
-124
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+357
-124
lines changed

.vscode/settings.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,8 @@
6161
// "examples/stm32wl/Cargo.toml",
6262
// "examples/wasm/Cargo.toml",
6363
],
64-
}
64+
"rust-analyzer.rustfmt.extraArgs": [
65+
//Uncomment to run rustfmt with nightly-only settings that match the CI
66+
// "+nightly"
67+
],
68+
}

docs/examples/layer-by-layer/blinky-async/src/main.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
#![no_main]
33

44
use embassy_executor::Spawner;
5-
use embassy_stm32::exti::ExtiInput;
5+
use embassy_stm32::exti::{self, ExtiInput};
66
use embassy_stm32::gpio::{Level, Output, Pull, Speed};
7+
use embassy_stm32::{bind_interrupts, interrupt};
78
use {defmt_rtt as _, panic_probe as _};
89

10+
bind_interrupts!(
11+
pub struct Irqs{
12+
EXTI15_10 => exti::InterruptHandler<interrupt::typelevel::EXTI15_10 >;
13+
});
14+
915
#[embassy_executor::main]
1016
async fn main(_spawner: Spawner) {
1117
let p = embassy_stm32::init(Default::default());
1218
let mut led = Output::new(p.PB14, Level::Low, Speed::VeryHigh);
13-
let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);
19+
let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up, Irqs);
1420

1521
loop {
1622
button.wait_for_any_edge().await;

docs/pages/faq.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,11 @@ Note that the git revision should match any other embassy patches or git depende
171171

172172
== Can I use manual ISRs alongside Embassy?
173173

174-
Yes! This can be useful if you need to respond to an event as fast as possible, and the latency caused by the usual “ISR, wake, return from ISR, context switch to woken task” flow is too much for your application. Simply define a `#[interrupt] fn INTERRUPT_NAME() {}` handler as you would link:https://docs.rust-embedded.org/book/start/interrupts.html[in any other embedded rust project].
174+
Yes! This can be useful if you need to respond to an event as fast as possible, and the latency caused by the usual “ISR, wake, return from ISR, context switch to woken task” flow is too much for your application.
175+
176+
You may simply define a `#[interrupt] fn INTERRUPT_NAME() {}` handler as you would link:https://docs.rust-embedded.org/book/start/interrupts.html[in any other embedded rust project].
177+
178+
Or you may define a struct implementing the `embassy-[family]::interrupt::typelevel::Handler` trait with an on_interrupt() method, and bind it to the interrupt vector via the `bind_interrupts!` macro, which introduces only a single indirection. This allows the mixing of manual ISRs with Embassy driver-defined ISRs; handlers will be called directly in the order they appear in the macro.
175179

176180
== How can I measure resource usage (CPU, RAM, etc.)?
177181

docs/pages/layer_by_layer.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ The async version looks very similar to the HAL version, apart from a few minor
7676
* The peripheral initialization is done by the main macro, and is handed to the main task.
7777
* Before checking the button state, the application is awaiting a transition in the pin state (low -> high or high -> low).
7878

79-
When `button.wait_for_any_edge().await` is called, the executor will pause the main task and put the microcontroller in sleep mode, unless there are other tasks that can run. Internally, the Embassy HAL has configured the interrupt handler for the button (in `ExtiInput`), so that whenever an interrupt is raised, the task awaiting the button will be woken up.
79+
When `button.wait_for_any_edge().await` is called, the executor will pause the main task and put the microcontroller in sleep mode, unless there are other tasks that can run. On this chip, interrupt signals on EXTI lines 10-15 (including the button on EXTI line 13) raise the hardware interrupt EXTI15_10. This interrupt handler has been bound (using `bind_interrupts!`) to call the `InterruptHandler` provided by the exti module, so that whenever an interrupt is raised, the task awaiting the button via `wait_for_any_edge()` will be woken up.
8080

8181
The minimal overhead of the executor and the ability to run multiple tasks "concurrently" combined with the enormous simplification of the application, makes `async` a great fit for embedded.
8282

embassy-stm32/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7575
- fix: fixing channel numbers on vbat and vddcore for adc on adc
7676
- adc: adding disable to vbat
7777
- feat: stm32/flash: add async support for h7 family
78+
- feat: exti brought in line with other drivers' interrupt rebinding system ([#4922](https://github.com/embassy-rs/embassy/pull/4922))
79+
- removal: ExtiInput no longer accepts AnyPin/AnyChannel; AnyChannel removed entirely
80+
- fix: build script ensures EXTI2_TSC is listed as the IRQ of EXTI2 even if the PAC doesn't
7881
- feat: stm32/lcd: added implementation
7982

8083
## 0.4.0 - 2025-08-26

embassy-stm32/build.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,8 +353,13 @@ fn main() {
353353
// ========
354354
// Generate interrupt declarations
355355

356+
let mut exti2_tsc_shared_int_present: Option<stm32_metapac::metadata::Interrupt> = None;
356357
let mut irqs = Vec::new();
357358
for irq in METADATA.interrupts {
359+
// The PAC doesn't ensure this is listed as the IRQ of EXTI2, so we must do so
360+
if irq.name == "EXTI2_TSC" {
361+
exti2_tsc_shared_int_present = Some(irq.clone())
362+
}
358363
irqs.push(format_ident!("{}", irq.name));
359364
}
360365

@@ -1812,7 +1817,19 @@ fn main() {
18121817
for p in METADATA.peripherals {
18131818
let mut pt = TokenStream::new();
18141819

1820+
let mut exti2_tsc_injected = false;
1821+
if let Some(ref irq) = exti2_tsc_shared_int_present
1822+
&& p.name == "EXTI"
1823+
{
1824+
exti2_tsc_injected = true;
1825+
let iname = format_ident!("{}", irq.name);
1826+
let sname = format_ident!("{}", "EXTI2");
1827+
pt.extend(quote!(pub type #sname = crate::interrupt::typelevel::#iname;));
1828+
}
18151829
for irq in p.interrupts {
1830+
if exti2_tsc_injected && irq.signal == "EXTI2" {
1831+
continue;
1832+
}
18161833
let iname = format_ident!("{}", irq.interrupt);
18171834
let sname = format_ident!("{}", irq.signal);
18181835
pt.extend(quote!(pub type #sname = crate::interrupt::typelevel::#iname;));

embassy-stm32/src/exti.rs

Lines changed: 55 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ use core::marker::PhantomData;
55
use core::pin::Pin;
66
use core::task::{Context, Poll};
77

8-
use embassy_hal_internal::{PeripheralType, impl_peripheral};
8+
use embassy_hal_internal::PeripheralType;
99
use embassy_sync::waitqueue::AtomicWaker;
1010
use futures_util::FutureExt;
1111

12-
use crate::gpio::{AnyPin, Input, Level, Pin as GpioPin, PinNumber, Pull};
12+
use crate::gpio::{AnyPin, ExtiPin, Input, Level, Pin as GpioPin, PinNumber, Pull};
13+
use crate::interrupt::Interrupt as InterruptEnum;
14+
use crate::interrupt::typelevel::{Binding, Handler, Interrupt as InterruptType};
1315
use crate::pac::EXTI;
1416
use crate::pac::exti::regs::Lines;
15-
use crate::{Peri, interrupt, pac, peripherals};
17+
use crate::{Peri, pac};
1618

1719
const EXTI_COUNT: usize = 16;
1820
static EXTI_WAKERS: [AtomicWaker; EXTI_COUNT] = [const { AtomicWaker::new() }; EXTI_COUNT];
@@ -106,10 +108,17 @@ impl<'d> Unpin for ExtiInput<'d> {}
106108

107109
impl<'d> ExtiInput<'d> {
108110
/// Create an EXTI input.
109-
pub fn new<T: GpioPin>(pin: Peri<'d, T>, ch: Peri<'d, T::ExtiChannel>, pull: Pull) -> Self {
110-
// Needed if using AnyPin+AnyChannel.
111-
assert_eq!(pin.pin(), ch.number());
112-
111+
///
112+
/// The Binding must bind the Channel's IRQ to [InterruptHandler].
113+
pub fn new<T: ExtiPin + GpioPin>(
114+
pin: Peri<'d, T>,
115+
_ch: Peri<'d, T::ExtiChannel>,
116+
pull: Pull,
117+
_irq: impl Binding<
118+
<<T as ExtiPin>::ExtiChannel as Channel>::IRQ,
119+
InterruptHandler<<<T as ExtiPin>::ExtiChannel as Channel>::IRQ>,
120+
>,
121+
) -> Self {
113122
Self {
114123
pin: Input::new(pin, pull),
115124
}
@@ -328,7 +337,7 @@ macro_rules! foreach_exti_irq {
328337
(EXTI15) => { $action!(EXTI15); };
329338

330339
// plus the weird ones
331-
(EXTI0_1) => { $action!( EXTI0_1 ); };
340+
(EXTI0_1) => { $action!(EXTI0_1); };
332341
(EXTI15_10) => { $action!(EXTI15_10); };
333342
(EXTI15_4) => { $action!(EXTI15_4); };
334343
(EXTI1_0) => { $action!(EXTI1_0); };
@@ -341,57 +350,67 @@ macro_rules! foreach_exti_irq {
341350
};
342351
}
343352

344-
macro_rules! impl_irq {
345-
($e:ident) => {
346-
#[allow(non_snake_case)]
347-
#[cfg(feature = "rt")]
348-
#[interrupt]
349-
unsafe fn $e() {
350-
on_irq()
351-
}
352-
};
353+
///EXTI interrupt handler. All EXTI interrupt vectors should be bound to this handler.
354+
///
355+
/// It is generic over the [Interrupt](InterruptType) rather
356+
/// than the [Channel] because it should not be bound multiple
357+
/// times to the same vector on chips which multiplex multiple EXTI interrupts into one vector.
358+
//
359+
// It technically doesn't need to be generic at all, except to satisfy the generic argument
360+
// of [Handler]. All EXTI interrupts eventually land in the same on_irq() function.
361+
pub struct InterruptHandler<T: crate::interrupt::typelevel::Interrupt> {
362+
_phantom: PhantomData<T>,
353363
}
354364

355-
foreach_exti_irq!(impl_irq);
365+
impl<T: InterruptType> Handler<T> for InterruptHandler<T> {
366+
unsafe fn on_interrupt() {
367+
on_irq()
368+
}
369+
}
356370

357371
trait SealedChannel {}
358372

359373
/// EXTI channel trait.
360374
#[allow(private_bounds)]
361375
pub trait Channel: PeripheralType + SealedChannel + Sized {
362-
/// Get the EXTI channel number.
376+
/// EXTI channel number.
363377
fn number(&self) -> PinNumber;
378+
/// [Enum-level Interrupt](InterruptEnum), which may be the same for multiple channels.
379+
fn irq(&self) -> InterruptEnum;
380+
/// [Type-level Interrupt](InterruptType), which may be the same for multiple channels.
381+
type IRQ: InterruptType;
364382
}
365383

366-
/// Type-erased EXTI channel.
384+
//Doc isn't hidden in order to surface the explanation to users, even though it's completely inoperable, not just deprecated.
385+
//Entire type along with doc can probably be removed after deprecation has appeared in a release once.
386+
/// Deprecated type-erased EXTI channel.
367387
///
368-
/// This represents ownership over any EXTI channel, known at runtime.
388+
/// Support for AnyChannel was removed in order to support manually bindable EXTI interrupts via bind_interrupts; [ExtiInput::new()]
389+
/// must know the required IRQ at compile time, and therefore cannot support type-erased channels.
390+
#[deprecated = "type-erased EXTI channels are no longer supported, in order to support manually bindable EXTI interrupts (more info: https://github.com/embassy-rs/embassy/pull/4922)"]
369391
pub struct AnyChannel {
392+
#[allow(unused)]
370393
number: PinNumber,
371394
}
372395

373-
impl_peripheral!(AnyChannel);
374-
impl SealedChannel for AnyChannel {}
375-
impl Channel for AnyChannel {
376-
fn number(&self) -> PinNumber {
377-
self.number
378-
}
379-
}
380-
381396
macro_rules! impl_exti {
382397
($type:ident, $number:expr) => {
383-
impl SealedChannel for peripherals::$type {}
384-
impl Channel for peripherals::$type {
398+
impl SealedChannel for crate::peripherals::$type {}
399+
impl Channel for crate::peripherals::$type {
385400
fn number(&self) -> PinNumber {
386401
$number
387402
}
403+
fn irq(&self) -> InterruptEnum {
404+
crate::_generated::peripheral_interrupts::EXTI::$type::IRQ
405+
}
406+
type IRQ = crate::_generated::peripheral_interrupts::EXTI::$type;
388407
}
389408

390-
impl From<peripherals::$type> for AnyChannel {
391-
fn from(val: peripherals::$type) -> Self {
392-
Self {
393-
number: val.number() as PinNumber,
394-
}
409+
//Still here to surface deprecation messages to the user - remove when removing AnyChannel
410+
#[allow(deprecated)]
411+
impl From<crate::peripherals::$type> for AnyChannel {
412+
fn from(_val: crate::peripherals::$type) -> Self {
413+
Self { number: $number }
395414
}
396415
}
397416
};

embassy-stm32/src/gpio.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -812,15 +812,19 @@ pub type PinNumber = u8;
812812
#[cfg(stm32n6)]
813813
pub type PinNumber = u16;
814814

815-
/// GPIO pin trait.
815+
/// Pin that can be used to configure an [ExtiInput](crate::exti::ExtiInput). This trait is lost when converting to [AnyPin].
816+
#[cfg(feature = "exti")]
816817
#[allow(private_bounds)]
817-
pub trait Pin: PeripheralType + Into<AnyPin> + SealedPin + Sized + 'static {
818+
pub trait ExtiPin: PeripheralType + SealedPin {
818819
/// EXTI channel assigned to this pin.
819820
///
820821
/// For example, PC4 uses EXTI4.
821-
#[cfg(feature = "exti")]
822822
type ExtiChannel: crate::exti::Channel;
823+
}
823824

825+
/// GPIO pin trait.
826+
#[allow(private_bounds)]
827+
pub trait Pin: PeripheralType + Into<AnyPin> + SealedPin + Sized + 'static {
824828
/// Number of the pin within the port (0..31)
825829
#[inline]
826830
fn pin(&self) -> PinNumber {
@@ -834,7 +838,7 @@ pub trait Pin: PeripheralType + Into<AnyPin> + SealedPin + Sized + 'static {
834838
}
835839
}
836840

837-
/// Type-erased GPIO pin
841+
/// Type-erased GPIO pin.
838842
pub struct AnyPin {
839843
pin_port: PinNumber,
840844
}
@@ -862,10 +866,7 @@ impl AnyPin {
862866
}
863867

864868
impl_peripheral!(AnyPin);
865-
impl Pin for AnyPin {
866-
#[cfg(feature = "exti")]
867-
type ExtiChannel = crate::exti::AnyChannel;
868-
}
869+
impl Pin for AnyPin {}
869870
impl SealedPin for AnyPin {
870871
#[inline]
871872
fn pin_port(&self) -> PinNumber {
@@ -878,7 +879,9 @@ impl SealedPin for AnyPin {
878879
foreach_pin!(
879880
($pin_name:ident, $port_name:ident, $port_num:expr, $pin_num:expr, $exti_ch:ident) => {
880881
impl Pin for peripherals::$pin_name {
881-
#[cfg(feature = "exti")]
882+
}
883+
#[cfg(feature = "exti")]
884+
impl ExtiPin for peripherals::$pin_name {
882885
type ExtiChannel = peripherals::$exti_ch;
883886
}
884887
impl SealedPin for peripherals::$pin_name {

embassy-stm32/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ pub use crate::_generated::interrupt;
153153
/// Macro to bind interrupts to handlers.
154154
///
155155
/// This defines the right interrupt handlers, and creates a unit struct (like `struct Irqs;`)
156-
/// and implements the right [`Binding`]s for it. You can pass this struct to drivers to
156+
/// and implements the right [`Binding`](crate::interrupt::typelevel::Binding)s for it. You can pass this struct to drivers to
157157
/// prove at compile-time that the right interrupts have been bound.
158158
///
159159
/// Example of how to bind one interrupt:
@@ -180,6 +180,10 @@ pub use crate::_generated::interrupt;
180180
/// }
181181
/// );
182182
/// ```
183+
///
184+
/// Some chips collate multiple interrupt signals into a single interrupt vector. In the above example, I2C2_3 is a
185+
/// single vector which is activated by events and errors on both peripherals I2C2 and I2C3. Check your chip's list
186+
/// of interrupt vectors if you get an unexpected compile error trying to bind the standard name.
183187
// developer note: this macro can't be in `embassy-hal-internal` due to the use of `$crate`.
184188
#[macro_export]
185189
macro_rules! bind_interrupts {

examples/boot/application/stm32f3/src/bin/a.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@ use defmt_rtt::*;
66
use embassy_boot_stm32::{AlignedBuffer, FirmwareUpdater, FirmwareUpdaterConfig};
77
use embassy_embedded_hal::adapter::BlockingAsync;
88
use embassy_executor::Spawner;
9-
use embassy_stm32::exti::ExtiInput;
9+
use embassy_stm32::exti::{self, ExtiInput};
1010
use embassy_stm32::flash::{Flash, WRITE_SIZE};
1111
use embassy_stm32::gpio::{Level, Output, Pull, Speed};
12+
use embassy_stm32::{bind_interrupts, interrupt};
1213
use embassy_sync::mutex::Mutex;
1314
use panic_reset as _;
1415

16+
bind_interrupts!(
17+
pub struct Irqs{
18+
EXTI15_10 => exti::InterruptHandler<interrupt::typelevel::EXTI15_10>;
19+
});
20+
1521
#[cfg(feature = "skip-include")]
1622
static APP_B: &[u8] = &[0, 1, 2, 3];
1723
#[cfg(not(feature = "skip-include"))]
@@ -23,7 +29,7 @@ async fn main(_spawner: Spawner) {
2329
let flash = Flash::new_blocking(p.FLASH);
2430
let flash = Mutex::new(BlockingAsync::new(flash));
2531

26-
let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up);
32+
let mut button = ExtiInput::new(p.PC13, p.EXTI13, Pull::Up, Irqs);
2733

2834
let mut led = Output::new(p.PA5, Level::Low, Speed::Low);
2935
led.set_high();

0 commit comments

Comments
 (0)