blob: 10228257fedcd5d614e469d41654b81e28e0ac99 [file] [view] [edit]
# Library Design Recommendation: Interrupt Handler Pattern
## Background: How ARM Cortex-M Interrupts Work
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:
1. Looks up the corresponding entry in the vector table
2. Jumps directly to that address
3. Executes the Interrupt Service Routine (ISR)
The vector table is typically defined by the PAC (Peripheral Access Crate) using a static array with `extern "C"` function declarations:
```rust
// 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.
## The Problem: Symbol Ownership Conflict
### Scenario 1: Standalone Driver Application
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.
### Scenario 2: Driver + Kernel/RTOS
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`
### Why Kernels Need ISR Ownership
A kernel/RTOS needs to own ISR entry points for several reasons:
1. **Context Saving**: The kernel may need to save thread context before handling the interrupt
2. **Priority Management**: The kernel scheduler may need to run after the ISR
3. **Statistics**: Track interrupt counts, latency, etc.
4. **Safety Boundaries**: Validate that the interrupt is expected before dispatching
The kernel's ISR wrapper typically looks like:
```rust
#[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();
}
```
### The Fundamental Issue
**A `#[no_mangle]` symbol is a global, unique identifier in the final binary.**
When a library unconditionally exports `#[no_mangle]` symbols, it:
- Claims exclusive ownership of those symbol names
- Prevents any other code from defining them
- Forces a specific interrupt handling strategy
- Makes the library unusable in any context that needs ISR control
## Solution: Separate Logic from Export
### 1. Provide callable handler functions (always available)
```rust
/// UART0 interrupt handler - call this from your ISR
#[inline]
pub fn uart0_irq_handler() {
// Actual interrupt handling logic
}
```
### 2. Conditionally export ISR symbols via feature flag
```rust
#[cfg(feature = "isr-handlers")]
#[no_mangle]
pub extern "C" fn uart0() {
uart0_irq_handler();
}
```
### 3. Default the feature to off
```toml
[features]
default = []
isr-handlers = [] # Export ISR symbols - disable for kernel integration
```
## Benefits
| 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 |
## Integration Example
Kernel entry point calls library handlers:
```rust
#[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();
}
```
## Guidelines
1. **Never export `#[no_mangle]` symbols unconditionally** in libraries intended for embedded use
2. **Provide public handler functions** that encapsulate interrupt logic
3. **Use feature flags** to control symbol export
4. **Document the pattern** so integrators know how to wire up handlers
5. **Apply consistently** to all ISR exports (timers, peripherals, DMA, etc.)
## Example Implementation
A driver crate implementing this pattern for UART handlers:
```rust
// 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:
```toml
[features]
default = []
isr-handlers = [] # Export ISR handlers with #[no_mangle] - disable for kernel integration
```
Kernel integration (entry.rs):
```rust
#[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();
}
```
## Why Not Use Weak Symbols?
An alternative approach is to use weak linkage:
```rust
#[linkage = "weak"]
#[no_mangle]
pub extern "C" fn uart0() {
default_handler();
}
```
This allows the symbol to be overridden. However, this approach has drawbacks:
1. **Implicit behavior**: It's not obvious that the symbol can/should be overridden
2. **Toolchain support**: Weak linkage behavior varies across linkers
3. **No compile-time feedback**: You won't know if override worked until runtime
4. **Still pollutes symbol namespace**: The library still exports the symbol
The feature flag approach is explicit, portable, and provides clear compile-time control.
## Repository Structure: Separate Test Crates
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.
### Visual Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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() │ │
│ │ │ │
│ └─────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Directory Structure
```
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
```
### Why Separate Crates?
| 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 |
### Test Crate Cargo.toml
```toml
[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" }
```
### Test Crate main.rs
```rust
#![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 {}
}
```
### Test Crate isr.rs
```rust
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.
## Common Mistakes to Avoid
### 1. Embedding test code in the library
```rust
// 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.
### 2. Forgetting peripheral ISRs beyond the obvious ones
Libraries often have ISRs for:
- Timers (timer, timer1, timer2, ...)
- Communication (i2c, spi, uart, ...)
- DMA channels
- Error handlers
**Fix:** Audit all `#[no_mangle]` exports with:
```bash
grep -rn "#\[no_mangle\]" src/
```
### 3. Mixing ISR logic with ISR export
```rust
// 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:
```rust
pub fn uart0_irq_handler() {
// All the logic here
}
#[cfg(feature = "isr-handlers")]
#[no_mangle]
pub extern "C" fn uart0() {
uart0_irq_handler();
}
```
## Checklist for Library Contributors
- [ ] Identify all `#[no_mangle]` ISR exports in your library
- [ ] Create a feature flag (e.g., `isr-handlers`) that defaults to off
- [ ] Refactor each ISR into: handler function + conditional export
- [ ] Move test code with ISRs to a separate crate in the workspace
- [ ] Document the integration pattern in your README
- [ ] Provide example code showing kernel integration
## Related Patterns
This pattern applies beyond interrupt handlers to any `#[no_mangle]` symbol:
- Panic handlers (`#[panic_handler]`)
- Exception handlers (HardFault, MemManage, etc.)
- Entry points (`#[entry]`, `main`)
- Allocator hooks (`#[global_allocator]`)
The principle is the same: **libraries should provide functionality, not claim global symbols**.