From 469aea0f904da2d5d17353e9bf81de1b1d27cadb Mon Sep 17 00:00:00 2001 From: CordlessCoder Date: Mon, 31 Aug 2026 13:36:32 +0100 Subject: [PATCH 1/6] Mask the NVIC bitmap index so the accessors need no bounds check. Worth 220 bytes on the embassy `rp` `multiprio` example, thumbv6m. --- cortex-m/CHANGELOG.md | 9 +++++++ cortex-m/src/peripheral/nvic.rs | 43 +++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/cortex-m/CHANGELOG.md b/cortex-m/CHANGELOG.md index 3f536f63..df25bc56 100644 --- a/cortex-m/CHANGELOG.md +++ b/cortex-m/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Fixed +- `NVIC`'s interrupt-bitmap accessors no longer emit a bounds check and a panic + path. `mask`, `unmask`, `is_enabled`, `is_active`, `is_pending`, `pend`, + `unpend` and the ARMv8-M `route_to_*`/`is_routed_to_nonsecure` all indexed a + 16-word array with `nr / 32`, which the optimiser cannot prove is in range for + a `u16`. No Cortex-M implements more than 496 external interrupts, so the + index is now masked to the array's own bound, which no reachable interrupt + number reaches. + ## [v0.7.9] - 2026-08-18 ### Fixed diff --git a/cortex-m/src/peripheral/nvic.rs b/cortex-m/src/peripheral/nvic.rs index 21066c46..34c44e3b 100644 --- a/cortex-m/src/peripheral/nvic.rs +++ b/cortex-m/src/peripheral/nvic.rs @@ -82,6 +82,13 @@ pub struct RegisterBlock { pub stir: WO, } +/// `BITMAP_WORDS` describes the arrays above rather than being asserted about itself: the gap from +/// `ISER` to `ICER` is one bitmap plus one reserved bitmap of the same size, so resizing either +/// without the other fails this. +const _: () = assert!( + core::mem::offset_of!(RegisterBlock, icer) == NVIC::BITMAP_WORDS * core::mem::size_of::() * 2 +); + impl NVIC { /// Request an IRQ in software /// @@ -113,7 +120,7 @@ impl NVIC { { let nr = interrupt.number(); // NOTE(unsafe) this is a write to a stateless register - unsafe { (*Self::PTR).icer[usize::from(nr / 32)].write(1 << (nr % 32)) } + unsafe { (*Self::PTR).icer[Self::reg_index(nr)].write(1 << (nr % 32)) } } /// Enables `interrupt` @@ -127,7 +134,7 @@ impl NVIC { unsafe { let nr = interrupt.number(); // NOTE(ptr) this is a write to a stateless register - (*Self::PTR).iser[usize::from(nr / 32)].write(1 << (nr % 32)) + (*Self::PTR).iser[Self::reg_index(nr)].write(1 << (nr % 32)) } } @@ -168,7 +175,7 @@ impl NVIC { let mask = 1 << (nr % 32); // NOTE(unsafe) atomic read with no side effects - unsafe { ((*Self::PTR).iabr[usize::from(nr / 32)].read() & mask) == mask } + unsafe { ((*Self::PTR).iabr[Self::reg_index(nr)].read() & mask) == mask } } /// Checks if `interrupt` is enabled @@ -181,7 +188,7 @@ impl NVIC { let mask = 1 << (nr % 32); // NOTE(unsafe) atomic read with no side effects - unsafe { ((*Self::PTR).iser[usize::from(nr / 32)].read() & mask) == mask } + unsafe { ((*Self::PTR).iser[Self::reg_index(nr)].read() & mask) == mask } } /// Checks if `interrupt` is pending @@ -194,7 +201,7 @@ impl NVIC { let mask = 1 << (nr % 32); // NOTE(unsafe) atomic read with no side effects - unsafe { ((*Self::PTR).ispr[usize::from(nr / 32)].read() & mask) == mask } + unsafe { ((*Self::PTR).ispr[Self::reg_index(nr)].read() & mask) == mask } } /// Forces `interrupt` into pending state @@ -206,7 +213,7 @@ impl NVIC { let nr = interrupt.number(); // NOTE(unsafe) atomic stateless write; ICPR doesn't store any state - unsafe { (*Self::PTR).ispr[usize::from(nr / 32)].write(1 << (nr % 32)) } + unsafe { (*Self::PTR).ispr[Self::reg_index(nr)].write(1 << (nr % 32)) } } /// Sets the "priority" of `interrupt` to `prio` @@ -254,7 +261,7 @@ impl NVIC { let nr = interrupt.number(); // NOTE(unsafe) atomic stateless write; ICPR doesn't store any state - unsafe { (*Self::PTR).icpr[usize::from(nr / 32)].write(1 << (nr % 32)) } + unsafe { (*Self::PTR).icpr[Self::reg_index(nr)].write(1 << (nr % 32)) } } /// Route `interrupt` to the Non-Secure world (ARMv8-M only). @@ -273,7 +280,7 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let group_idx = usize::from(nr / 32); + let group_idx = Self::reg_index(nr); let bit_mask = 1 << (nr % 32); unsafe { self.itns[group_idx].modify(|v| v | bit_mask) } } @@ -292,7 +299,7 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let group_idx = usize::from(nr / 32); + let group_idx = Self::reg_index(nr); let bit_mask = 1 << (nr % 32); unsafe { self.itns[group_idx].modify(|v| v & !bit_mask) } } @@ -305,12 +312,28 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let group_idx = usize::from(nr / 32); + let group_idx = Self::reg_index(nr); let bit_mask = 1 << (nr % 32); // NOTE(unsafe) atomic read with no side effects unsafe { ((*Self::PTR).itns[group_idx].read() & bit_mask) == bit_mask } } + /// Words in each of the NVIC's interrupt bitmaps: `ISER`, `ICER`, `ISPR`, `ICPR`, `IABR` + /// and `ITNS`. + const BITMAP_WORDS: usize = 16; + + /// Which 32-bit word of an interrupt bitmap holds `nr`'s bit. + /// + /// The mask is what lets this be indexed without a bounds check. Each bitmap is + /// [`BITMAP_WORDS`](Self::BITMAP_WORDS) words, spanning 512 interrupts; no Cortex-M implements + /// more than 496, so `nr / 32` is at most 15 and the mask never alters a legal index. Without + /// it the compiler only knows `nr` is a `u16`, so `nr / 32` could be up to 2047, and every + /// accessor below keeps a bounds check and a panic path that no reachable input can take. + #[inline] + const fn reg_index(nr: u16) -> usize { + (nr / 32) as usize & (Self::BITMAP_WORDS - 1) + } + #[cfg(armv6m)] #[inline] fn ipr_index(interrupt: I) -> usize From 784cecc0cea310a1b070e561c1365c72ecc553fd Mon Sep 17 00:00:00 2001 From: CordlessCoder Date: Mon, 31 Aug 2026 13:45:14 +0100 Subject: [PATCH 2/6] Apply rustfmt --- cortex-m/src/peripheral/nvic.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cortex-m/src/peripheral/nvic.rs b/cortex-m/src/peripheral/nvic.rs index 34c44e3b..5cfd05c6 100644 --- a/cortex-m/src/peripheral/nvic.rs +++ b/cortex-m/src/peripheral/nvic.rs @@ -86,7 +86,8 @@ pub struct RegisterBlock { /// `ISER` to `ICER` is one bitmap plus one reserved bitmap of the same size, so resizing either /// without the other fails this. const _: () = assert!( - core::mem::offset_of!(RegisterBlock, icer) == NVIC::BITMAP_WORDS * core::mem::size_of::() * 2 + core::mem::offset_of!(RegisterBlock, icer) + == NVIC::BITMAP_WORDS * core::mem::size_of::() * 2 ); impl NVIC { From f58b2c7ec08c8c18aa6ca5f2394cd2574e622dfd Mon Sep 17 00:00:00 2001 From: CordlessCoder Date: Tue, 1 Sep 2026 10:36:36 +0100 Subject: [PATCH 3/6] changelog: Trim NVIC change entry --- cortex-m/CHANGELOG.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/cortex-m/CHANGELOG.md b/cortex-m/CHANGELOG.md index df25bc56..873289d7 100644 --- a/cortex-m/CHANGELOG.md +++ b/cortex-m/CHANGELOG.md @@ -9,12 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - `NVIC`'s interrupt-bitmap accessors no longer emit a bounds check and a panic - path. `mask`, `unmask`, `is_enabled`, `is_active`, `is_pending`, `pend`, - `unpend` and the ARMv8-M `route_to_*`/`is_routed_to_nonsecure` all indexed a - 16-word array with `nr / 32`, which the optimiser cannot prove is in range for - a `u16`. No Cortex-M implements more than 496 external interrupts, so the - index is now masked to the array's own bound, which no reachable interrupt - number reaches. + path. ## [v0.7.9] - 2026-08-18 From 6a545d4e1992b29680290baa401e683c82bb26ef Mon Sep 17 00:00:00 2001 From: CordlessCoder Date: Tue, 1 Sep 2026 10:50:14 +0100 Subject: [PATCH 4/6] Simplify NVIC mask helper comments, remove redundant assert! --- cortex-m/src/peripheral/nvic.rs | 82 +++++++++++++++------------------ 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/cortex-m/src/peripheral/nvic.rs b/cortex-m/src/peripheral/nvic.rs index 5cfd05c6..8d3cb7ba 100644 --- a/cortex-m/src/peripheral/nvic.rs +++ b/cortex-m/src/peripheral/nvic.rs @@ -11,40 +11,40 @@ use crate::peripheral::NVIC; #[repr(C)] pub struct RegisterBlock { /// Interrupt Set-Enable - pub iser: [RW; 16], + pub iser: [RW; NVIC::BITMAP_WORDS], - _reserved0: [u32; 16], + _reserved0: [u32; NVIC::BITMAP_WORDS], /// Interrupt Clear-Enable - pub icer: [RW; 16], + pub icer: [RW; NVIC::BITMAP_WORDS], - _reserved1: [u32; 16], + _reserved1: [u32; NVIC::BITMAP_WORDS], /// Interrupt Set-Pending - pub ispr: [RW; 16], + pub ispr: [RW; NVIC::BITMAP_WORDS], - _reserved2: [u32; 16], + _reserved2: [u32; NVIC::BITMAP_WORDS], /// Interrupt Clear-Pending - pub icpr: [RW; 16], + pub icpr: [RW; NVIC::BITMAP_WORDS], - _reserved3: [u32; 16], + _reserved3: [u32; NVIC::BITMAP_WORDS], /// Interrupt Active Bit (not present on Cortex-M0 variants) #[cfg(not(armv6m))] - pub iabr: [RO; 16], + pub iabr: [RO; NVIC::BITMAP_WORDS], #[cfg(armv6m)] - _reserved4: [u32; 16], + _reserved4: [u32; NVIC::BITMAP_WORDS], - _reserved5: [u32; 16], + _reserved5: [u32; NVIC::BITMAP_WORDS], #[cfg(armv8m)] /// Interrupt Target Non-secure (only present on Arm v8-M) - pub itns: [RW; 16], + pub itns: [RW; NVIC::BITMAP_WORDS], #[cfg(not(armv8m))] - _reserved6: [u32; 16], + _reserved6: [u32; NVIC::BITMAP_WORDS], - _reserved7: [u32; 16], + _reserved7: [u32; NVIC::BITMAP_WORDS], /// Interrupt Priority /// @@ -82,14 +82,6 @@ pub struct RegisterBlock { pub stir: WO, } -/// `BITMAP_WORDS` describes the arrays above rather than being asserted about itself: the gap from -/// `ISER` to `ICER` is one bitmap plus one reserved bitmap of the same size, so resizing either -/// without the other fails this. -const _: () = assert!( - core::mem::offset_of!(RegisterBlock, icer) - == NVIC::BITMAP_WORDS * core::mem::size_of::() * 2 -); - impl NVIC { /// Request an IRQ in software /// @@ -120,8 +112,9 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); + let (idx, mask) = Self::reg_index_and_mask(nr); // NOTE(unsafe) this is a write to a stateless register - unsafe { (*Self::PTR).icer[Self::reg_index(nr)].write(1 << (nr % 32)) } + unsafe { (*Self::PTR).icer[idx].write(mask) } } /// Enables `interrupt` @@ -134,8 +127,9 @@ impl NVIC { { unsafe { let nr = interrupt.number(); + let (idx, mask) = Self::reg_index_and_mask(nr); // NOTE(ptr) this is a write to a stateless register - (*Self::PTR).iser[Self::reg_index(nr)].write(1 << (nr % 32)) + (*Self::PTR).iser[idx].write(mask) } } @@ -173,10 +167,10 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let mask = 1 << (nr % 32); + let (idx, mask) = Self::reg_index_and_mask(nr); // NOTE(unsafe) atomic read with no side effects - unsafe { ((*Self::PTR).iabr[Self::reg_index(nr)].read() & mask) == mask } + unsafe { ((*Self::PTR).iabr[idx].read() & mask) == mask } } /// Checks if `interrupt` is enabled @@ -186,10 +180,10 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let mask = 1 << (nr % 32); + let (idx, mask) = Self::reg_index_and_mask(nr); // NOTE(unsafe) atomic read with no side effects - unsafe { ((*Self::PTR).iser[Self::reg_index(nr)].read() & mask) == mask } + unsafe { ((*Self::PTR).iser[idx].read() & mask) == mask } } /// Checks if `interrupt` is pending @@ -199,10 +193,10 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let mask = 1 << (nr % 32); + let (idx, mask) = Self::reg_index_and_mask(nr); // NOTE(unsafe) atomic read with no side effects - unsafe { ((*Self::PTR).ispr[Self::reg_index(nr)].read() & mask) == mask } + unsafe { ((*Self::PTR).ispr[idx].read() & mask) == mask } } /// Forces `interrupt` into pending state @@ -212,9 +206,10 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); + let (idx, mask) = Self::reg_index_and_mask(nr); // NOTE(unsafe) atomic stateless write; ICPR doesn't store any state - unsafe { (*Self::PTR).ispr[Self::reg_index(nr)].write(1 << (nr % 32)) } + unsafe { (*Self::PTR).ispr[idx].write(mask) } } /// Sets the "priority" of `interrupt` to `prio` @@ -260,9 +255,10 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); + let (idx, mask) = Self::reg_index_and_mask(nr); // NOTE(unsafe) atomic stateless write; ICPR doesn't store any state - unsafe { (*Self::PTR).icpr[Self::reg_index(nr)].write(1 << (nr % 32)) } + unsafe { (*Self::PTR).icpr[idx].write(mask) } } /// Route `interrupt` to the Non-Secure world (ARMv8-M only). @@ -281,8 +277,7 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let group_idx = Self::reg_index(nr); - let bit_mask = 1 << (nr % 32); + let (group_idx, bit_mask) = Self::reg_index_and_mask(nr); unsafe { self.itns[group_idx].modify(|v| v | bit_mask) } } @@ -300,8 +295,7 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let group_idx = Self::reg_index(nr); - let bit_mask = 1 << (nr % 32); + let (group_idx, bit_mask) = Self::reg_index_and_mask(nr); unsafe { self.itns[group_idx].modify(|v| v & !bit_mask) } } @@ -313,8 +307,7 @@ impl NVIC { I: InterruptNumber, { let nr = interrupt.number(); - let group_idx = Self::reg_index(nr); - let bit_mask = 1 << (nr % 32); + let (group_idx, bit_mask) = Self::reg_index_and_mask(nr); // NOTE(unsafe) atomic read with no side effects unsafe { ((*Self::PTR).itns[group_idx].read() & bit_mask) == bit_mask } } @@ -325,14 +318,13 @@ impl NVIC { /// Which 32-bit word of an interrupt bitmap holds `nr`'s bit. /// - /// The mask is what lets this be indexed without a bounds check. Each bitmap is - /// [`BITMAP_WORDS`](Self::BITMAP_WORDS) words, spanning 512 interrupts; no Cortex-M implements - /// more than 496, so `nr / 32` is at most 15 and the mask never alters a legal index. Without - /// it the compiler only knows `nr` is a `u16`, so `nr / 32` could be up to 2047, and every - /// accessor below keeps a bounds check and a panic path that no reachable input can take. + /// The mask helps remove an expensive bounds check - we know all valid interrupt numbers will be in range. #[inline] - const fn reg_index(nr: u16) -> usize { - (nr / 32) as usize & (Self::BITMAP_WORDS - 1) + const fn reg_index_and_mask(irq: u16) -> (usize, u32) { + debug_assert!(irq < 512); + let idx = (irq / 32) as usize & (Self::BITMAP_WORDS - 1); + let mask = 1 << (irq % 32); + (idx, mask) } #[cfg(armv6m)] From c82127a87faa9e316b0f06a74d37b67bf8636a42 Mon Sep 17 00:00:00 2001 From: CordlessCoder Date: Tue, 1 Sep 2026 11:09:26 +0100 Subject: [PATCH 5/6] Fix ambiguous term in NVIC documentation Co-authored-by: Jonathan Pallant --- cortex-m/src/peripheral/nvic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex-m/src/peripheral/nvic.rs b/cortex-m/src/peripheral/nvic.rs index 8d3cb7ba..6d8c7737 100644 --- a/cortex-m/src/peripheral/nvic.rs +++ b/cortex-m/src/peripheral/nvic.rs @@ -318,7 +318,7 @@ impl NVIC { /// Which 32-bit word of an interrupt bitmap holds `nr`'s bit. /// - /// The mask helps remove an expensive bounds check - we know all valid interrupt numbers will be in range. + /// The `&` helps remove an expensive bounds check - we know all valid interrupt numbers will be in range. #[inline] const fn reg_index_and_mask(irq: u16) -> (usize, u32) { debug_assert!(irq < 512); From 9ed7c42679959e24cab096518188c5fbac36bf6e Mon Sep 17 00:00:00 2001 From: CordlessCoder Date: Tue, 1 Sep 2026 12:45:35 +0100 Subject: [PATCH 6/6] Simplify assert on IRQ number --- cortex-m/src/peripheral/nvic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex-m/src/peripheral/nvic.rs b/cortex-m/src/peripheral/nvic.rs index 6d8c7737..8f0b73d7 100644 --- a/cortex-m/src/peripheral/nvic.rs +++ b/cortex-m/src/peripheral/nvic.rs @@ -321,7 +321,7 @@ impl NVIC { /// The `&` helps remove an expensive bounds check - we know all valid interrupt numbers will be in range. #[inline] const fn reg_index_and_mask(irq: u16) -> (usize, u32) { - debug_assert!(irq < 512); + debug_assert!((irq as usize) < (Self::BITMAP_WORDS * 32)); let idx = (irq / 32) as usize & (Self::BITMAP_WORDS - 1); let mask = 1 << (irq % 32); (idx, mask)