On ARM Cortex-M processors, interrupt handling is driven by a vector table - an array of function pointers located at a fixed memory address (typically 0x00000000). When an interrupt fires, the hardware:
The vector table is typically defined by the PAC (Peripheral Access Crate) using a static array with extern "C" function declarations:
// From a typical PAC (Peripheral Access Crate) #[link_section = ".vector_table.interrupts"] #[no_mangle] pub static __INTERRUPTS: [Vector; N] = [ Vector { handler: uart0 }, Vector { handler: uart1 }, Vector { handler: timer0 }, Vector { handler: spi }, // ... N interrupt vectors ]; extern "C" { fn uart0(); fn uart1(); fn timer0(); fn spi(); // ... declared but NOT defined }
The PAC declares these symbols but does not define them. Someone must provide the actual function implementations.
When building a standalone application with just the driver library:
[PAC] --declares--> uart0()
[Driver] --defines---> #[no_mangle] pub extern "C" fn uart0() { ... }
This works perfectly. The driver provides the ISR, the linker resolves the symbol, interrupts work.
When integrating with a kernel that manages its own vector table:
[PAC] --declares--> uart0()
[Driver] --defines---> #[no_mangle] pub extern "C" fn uart0() { ... } ❌ CONFLICT
[Kernel Entry] --defines--> #[no_mangle] pub extern "C" fn uart0() { ... } ❌ CONFLICT
Result: error: symbol 'uart0' multiply defined
A kernel/RTOS needs to own ISR entry points for several reasons:
The kernel's ISR wrapper typically looks like:
#[no_mangle] pub extern "C" fn uart0() { // Kernel housekeeping kernel::enter_interrupt(); // Dispatch to actual handler my_driver::uart::uart0_irq_handler(); // Kernel housekeeping kernel::exit_interrupt(); }
A #[no_mangle] symbol is a global, unique identifier in the final binary.
When a library unconditionally exports #[no_mangle] symbols, it:
/// UART0 interrupt handler - call this from your ISR #[inline] pub fn uart0_irq_handler() { // Actual interrupt handling logic }
#[cfg(feature = "isr-handlers")] #[no_mangle] pub extern "C" fn uart0() { uart0_irq_handler(); }
[features] default = [] isr-handlers = [] # Export ISR symbols - disable for kernel integration
| Standalone Use | Kernel Integration |
|---|---|
Enable isr-handlers feature | Leave feature disabled |
| Library provides vector table entries | Kernel provides ISR stubs |
| Zero integration work | Kernel calls handler functions |
Kernel entry point calls library handlers:
#[no_mangle] pub extern "C" fn uart0() { my_driver::uart::uart0_irq_handler(); } #[no_mangle] pub extern "C" fn timer0() { my_driver::timer::timer0_irq_handler(); }
#[no_mangle] symbols unconditionally in libraries intended for embedded useA driver crate implementing this pattern for UART handlers:
// src/uart.rs /// UART0 interrupt handler - call this from your ISR #[inline] pub fn uart0_irq_handler() { dispatch_irq(0); } /// UART1 interrupt handler - call this from your ISR #[inline] pub fn uart1_irq_handler() { dispatch_irq(1); } // ISR exports - only when isr-handlers feature is enabled #[cfg(feature = "isr-handlers")] #[no_mangle] pub extern "C" fn uart0() { uart0_irq_handler(); } #[cfg(feature = "isr-handlers")] #[no_mangle] pub extern "C" fn uart1() { uart1_irq_handler(); }
Cargo.toml:
[features] default = [] isr-handlers = [] # Export ISR handlers with #[no_mangle] - disable for kernel integration
Kernel integration (entry.rs):
#[unsafe(no_mangle)] pub extern "C" fn uart0() { my_driver::uart::uart0_irq_handler(); } #[unsafe(no_mangle)] pub extern "C" fn uart1() { my_driver::uart::uart1_irq_handler(); }
An alternative approach is to use weak linkage:
#[linkage = "weak"] #[no_mangle] pub extern "C" fn uart0() { default_handler(); }
This allows the symbol to be overridden. However, this approach has drawbacks:
The feature flag approach is explicit, portable, and provides clear compile-time control.
Test code that defines ISRs should live in a separate crate, not inside the library. This keeps the library clean and avoids leaking test infrastructure to consumers.
┌─────────────────────────────────────────────────────────────────────────────┐
│ aspeed-rust (workspace) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ aspeed-ddk (lib crate) │ │ aspeed-ddk-tests (bin crate) │ │
│ │ │ │ │ │
│ │ src/ │ │ src/ │ │
│ │ ├── lib.rs │ │ ├── main.rs ◄── single entry │ │
│ │ ├── uart.rs │ │ │ │ │ │
│ │ │ └── uart0_irq_handler()│◄────┼───────┤ calls test_uart() │ │
│ │ ├── timer.rs │ │ │ │ calls test_timer() │ │
│ │ │ └── timer0_irq_handler()◄────┼───────┤ calls test_i2c() │ │
│ │ ├── i3c/ │ │ │ │ calls test_i3c() │ │
│ │ │ └── i3c_irq_handler() │◄────┼───────┤ ... │ │
│ │ └── i2c/ │ │ │ └── loop {} │ │
│ │ └── i2c0_irq_handler() │◄────┼───────┤ │ │
│ │ │ │ │ │ │
│ │ (NO test code here) │ │ ├── tests/ │ │
│ │ (NO #[no_mangle] ISRs) │ │ │ ├── mod.rs │ │
│ │ │ │ │ ├── uart_test.rs │ │
│ └─────────────────────────────┘ │ │ ├── timer_test.rs │ │
│ │ │ ├── i2c_test.rs │ │
│ │ │ ├── i3c_test.rs │ │
│ │ │ └── gpio_test.rs │ │
│ │ │ │ │
│ │ └── isr.rs ◄── all ISRs here │ │
│ │ ├── #[no_mangle] uart0() │ │
│ │ ├── #[no_mangle] timer() │ │
│ │ ├── #[no_mangle] i3c() │ │
│ │ └── #[no_mangle] i2c() │ │
│ │ │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
aspeed-rust/ ├── Cargo.toml # workspace definition ├── aspeed-ddk/ # lib crate - clean, no test ISRs │ ├── Cargo.toml │ └── src/ │ ├── lib.rs # handler functions + conditional exports │ ├── uart.rs │ ├── timer.rs │ ├── i3c/ │ └── i2c/ ├── aspeed-ddk-tests/ # bin crate - owns all ISRs │ ├── Cargo.toml # depends on aspeed-ddk │ └── src/ │ ├── main.rs # single test binary entry point │ ├── isr.rs # all #[no_mangle] ISR definitions │ └── tests/ │ ├── mod.rs │ ├── uart_test.rs │ ├── timer_test.rs │ ├── i2c_test.rs │ ├── i3c_test.rs │ └── gpio_test.rs └── xtask/ # optional build tooling
| Inside Library | Separate Crate |
|---|---|
| Test code ships to consumers | Clean library, no test leakage |
| Feature flags guard test modules | No feature complexity for tests |
| Test deps pollute library deps | Isolated dependency trees |
pub mod tests in lib.rs | Tests are a normal bin crate |
[package] name = "aspeed-ddk-tests" version = "0.1.0" edition = "2021" [[bin]] name = "functional-tests" path = "src/main.rs" [dependencies] aspeed-ddk = { path = "../aspeed-ddk" }
#![no_std] #![no_main] use aspeed_ddk::uart; use aspeed_ddk::i3c::ast1060_i3c; mod tests; mod isr; #[cortex_m_rt::entry] fn main() -> ! { // Initialize hardware... // Run tests tests::uart_test::run_uart_tests(); tests::timer_test::run_timer_tests(); tests::i3c_test::run_i3c_tests(); loop {} }
use aspeed_ddk::uart; use aspeed_ddk::i3c::ast1060_i3c; // Test crate owns all ISRs #[no_mangle] pub extern "C" fn uart0() { uart::uart0_irq_handler(); } #[no_mangle] pub extern "C" fn i3c() { ast1060_i3c::i3c_irq_handler(); } #[no_mangle] pub extern "C" fn timer() { // Test-specific timer handling }
This mirrors how kernel integration works - the integrating crate (kernel or test) depends on the driver library and wires up its own ISRs.
// BAD: Test code with ISRs inside the library // src/lib.rs pub mod tests; // Contains #[no_mangle] ISRs!
Fix: Move tests to a separate crate in the workspace.
Libraries often have ISRs for:
Fix: Audit all #[no_mangle] exports with:
grep -rn "#\[no_mangle\]" src/
// BAD: Logic embedded in no_mangle function #[no_mangle] pub extern "C" fn uart0() { let status = read_status_register(); if status & IRQ_PENDING != 0 { // 50 lines of handling logic } }
Fix: Put logic in a callable function, export just calls it:
pub fn uart0_irq_handler() { // All the logic here } #[cfg(feature = "isr-handlers")] #[no_mangle] pub extern "C" fn uart0() { uart0_irq_handler(); }
#[no_mangle] ISR exports in your libraryisr-handlers) that defaults to offThis pattern applies beyond interrupt handlers to any #[no_mangle] symbol:
#[panic_handler])#[entry], main)#[global_allocator])The principle is the same: libraries should provide functionality, not claim global symbols.