Multi-Peripheral Interrupt Coordinator
Problem Statement
Build an interrupt coordinator that safely manages shared state across multiple interrupt sources (UART, timers, GPIO, DMA) with different priorities. Your coordinator must prevent data races, avoid priority inversion, ensure bounded interrupt latency, and provide safe abstractions for interrupt-safe communication between ISRs and main tasks.
Your coordinator should support:
- Safe shared state with critical sections
- Priority-based interrupt management
- Lock-free communication patterns (atomics, lock-free queues)
- Interrupt statistics and profiling
- Deadline monitoring and timeout detection
- Integration with RTIC or Embassy for deterministic scheduling
Why Interrupt Coordination Matters
The Shared State Problem
The Problem: Multiple interrupts need to access shared data, but traditional locks cause deadlocks or priority inversion in interrupt contexts.
#![allow(unused)]
fn main() {
// ❌ DANGEROUS: Mutex in interrupt context
static COUNTER: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
#[interrupt]
fn TIMER_IRQ() {
COUNTER.lock().unwrap().replace_with(|c| *c + 1); // Deadlock!
}
#[interrupt]
fn UART_IRQ() {
let count = *COUNTER.lock().unwrap().borrow(); // Can't wait for lock!
}
// Problem: If TIMER_IRQ is interrupted by UART_IRQ,
// UART_IRQ tries to lock already-locked mutex → DEADLOCK
}
Real-world disaster:
Industrial controller:
├─ UART receives command (low priority interrupt)
├─ Takes mutex to update state
├─ Higher priority TIMER interrupt fires
├─ TIMER needs same mutex → spins forever
└─ System hangs, production line stops
Cost: $50,000/hour downtime
Critical Sections: Safe Interrupt-Main Communication
The Solution: Disable interrupts only around minimal critical sections:
use cortex_m::interrupt::{free, Mutex};
use core::cell::RefCell;
static SHARED: Mutex<RefCell<u32>> = Mutex::new(RefCell::new(0));
#[interrupt]
fn TIMER() {
free(|cs| {
let mut val = SHARED.borrow(cs).borrow_mut();
*val += 1; // Interrupts disabled here
}); // Interrupts re-enabled
}
fn main_task() {
let value = free(|cs| *SHARED.borrow(cs).borrow());
println!("Count: {}", value);
}
Key principle: Critical section duration must be bounded and minimal.
Priority Inversion: The Hidden Danger
Priority Inversion scenario:
Interrupt priorities (higher number = higher priority):
├─ TIMER: Priority 3 (highest)
├─ DMA: Priority 2
└─ UART: Priority 1 (lowest)
Timeline:
1. UART (priority 1) holds lock
2. TIMER (priority 3) preempts, needs lock → BLOCKED
3. DMA (priority 2) preempts UART
4. TIMER waits for UART (priority 1) to finish!
Result: High-priority interrupt blocked by medium-priority work
Impact:
Motor control system:
├─ Control loop: 1ms deadline (priority 3)
├─ Logging: 100ms deadline (priority 2)
├─ Bluetooth: 1s deadline (priority 1)
Priority inversion:
├─ Bluetooth holds shared state
├─ Control loop needs state → misses deadline
└─ Motor spins out of control → physical damage
Prevention: Use lock-free patterns or priority ceiling protocol
Lock-Free Patterns: Zero-Wait Communication
Atomic operations:
use core::sync::atomic::{AtomicU32, Ordering};
static FLAGS: AtomicU32 = AtomicU32::new(0);
#[interrupt]
fn UART_RX() {
FLAGS.fetch_or(1 << 0, Ordering::Release); // Set bit 0
}
#[interrupt]
fn TIMER() {
FLAGS.fetch_or(1 << 1, Ordering::Release); // Set bit 1
}
fn main_loop() {
let flags = FLAGS.swap(0, Ordering::AcqRel); // Atomic read-clear
if flags & (1 << 0) != 0 {
handle_uart();
}
if flags & (1 << 1) != 0 {
handle_timer();
}
}
Performance:
Critical section approach:
├─ Interrupt latency: 50-200 cycles (disable/enable interrupts)
├─ Jitter: Variable (depends on critical section length)
Lock-free atomic approach:
├─ Interrupt latency: 2-5 cycles (single atomic instruction)
├─ Jitter: Minimal (constant time)
10-100x faster!
Use Cases
1. Real-Time Control Systems
- Motor controllers: Position feedback (timer), command input (CAN), safety limits (GPIO)
- Robotics: Sensor fusion from multiple interrupt sources
- Industrial PLCs: Coordinating I/O modules with strict timing
- Challenge: Deterministic response within microseconds
2. Communication Gateways
- Multi-protocol bridges: UART ↔ SPI ↔ I2C ↔ CAN
- IoT gateways: WiFi + LoRa + Cellular with shared packet buffer
- Protocol converters: Real-time translation without data loss
- Challenge: Handle burst traffic without blocking
3. Safety-Critical Systems
- Medical devices: Monitor multiple vitals (ECG, SpO2, pressure)
- Automotive: ADAS sensor fusion with fallback paths
- Aviation: Flight control with redundant sensor validation
- Challenge: Guaranteed response time for all conditions
4. Battery-Powered Systems
- Low-power logging: Wake on any interrupt, process, sleep
- Sensor networks: Coordinate radio, sensors, timers for efficiency
- Wearables: Balance responsiveness with power consumption
- Challenge: Fast wake-up, minimal interrupt overhead
Building the Project
Milestone 1: Safe Shared State with Critical Sections
Goal: Implement safe shared state primitives that work correctly in both interrupt and non-interrupt contexts using critical sections.
Why we start here: Before coordinating interrupts, we need thread-safe primitives. This milestone teaches the foundation: how to safely share data between ISRs and main code.
Architecture
Structs:
-
InterruptSafeCell<T>- Interior mutability for interrupt contexts- Field:
data: UnsafeCell<T>- Inner data storage - Uses:
cortex_m::interrupt::Mutexwrapper
- Field:
-
SharedCounter- Example shared state- Field:
count: Mutex<RefCell<u32>>- Protected counter - Field:
overflows: AtomicU32- Overflow count (lock-free)
- Field:
Functions:
SharedCounter::new() -> Self- Create counterincrement(&self)- Increment from interrupt or mainget(&self) -> u32- Read current valuereset(&self)- Reset to zerooverflow_count(&self) -> u32- Get overflow count
Starter Code:
#![allow(unused)]
fn main() {
use core::cell::{RefCell, UnsafeCell};
use cortex_m::interrupt::{free, Mutex};
use core::sync::atomic::{AtomicU32, Ordering};
/// Safe wrapper for data shared between interrupts and main code
pub struct SharedCounter {
count: Mutex<RefCell<u32>>,
overflows: AtomicU32,
}
impl SharedCounter {
pub const fn new() -> Self {
// TODO: Initialize with Mutex wrapping RefCell
todo!("Implement SharedCounter::new")
}
/// Increment counter (safe to call from interrupt or main)
pub fn increment(&self) {
// TODO: Use free() to create critical section
// TODO: Borrow mutable reference
// TODO: Increment, checking for overflow
// TODO: If overflow, increment overflows atomically
todo!("Implement increment")
}
/// Read current value
pub fn get(&self) -> u32 {
// TODO: Use critical section to safely read
todo!("Implement get")
}
/// Reset counter to zero
pub fn reset(&self) {
// TODO: Use critical section to reset
todo!("Implement reset")
}
/// Get overflow count
pub fn overflow_count(&self) -> u32 {
self.overflows.load(Ordering::Relaxed)
}
}
// Make it safe to share across threads/interrupts
unsafe impl Sync for SharedCounter {}
}
Checkpoint Tests:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_counter_basic() {
let counter = SharedCounter::new();
assert_eq!(counter.get(), 0);
counter.increment();
assert_eq!(counter.get(), 1);
counter.increment();
counter.increment();
assert_eq!(counter.get(), 3);
counter.reset();
assert_eq!(counter.get(), 0);
}
#[test]
fn test_overflow_detection() {
let counter = SharedCounter::new();
// Manually set to max - 1
cortex_m::interrupt::free(|cs| {
*counter.count.borrow(cs).borrow_mut() = u32::MAX - 1;
});
counter.increment(); // Should not overflow
assert_eq!(counter.get(), u32::MAX);
assert_eq!(counter.overflow_count(), 0);
counter.increment(); // Should detect overflow
assert_eq!(counter.get(), 0);
assert_eq!(counter.overflow_count(), 1);
}
#[test]
fn test_concurrent_access() {
use std::sync::Arc;
use std::thread;
let counter = Arc::new(SharedCounter::new());
let mut handles = vec![];
// Spawn 10 threads, each incrementing 1000 times
for _ in 0..10 {
let counter_clone = counter.clone();
let handle = thread::spawn(move || {
for _ in 0..1000 {
counter_clone.increment();
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(counter.get(), 10_000);
}
}
}
Check Your Understanding:
- Why use
Mutex<RefCell<T>>instead of justRefCell<T>? - What’s the difference between
cortex_m::interrupt::freeandcritical_section::with? - Why use
AtomicU32for overflows instead of including it in the Mutex?
Why Milestone 1 Isn’t Enough
Limitation: Critical sections work but they disable ALL interrupts. This causes high-priority interrupts to be delayed unnecessarily.
What we’re adding: Priority-aware locking that only disables lower-priority interrupts, allowing critical work to continue.
Improvement:
- Latency: High-priority interrupts not delayed by low-priority work
- Throughput: More concurrent interrupt handling
- Predictability: Each interrupt class has bounded latency
- Safety: Still prevent races, but with finer granularity
Milestone 2: Priority-Based Interrupt Management
Goal: Implement priority-aware interrupt coordination where high-priority interrupts can preempt low-priority ones, with safe state access based on priority levels.
Why this milestone: Real systems have interrupt hierarchies. This teaches NVIC priority configuration and priority-ceiling protocols.
Architecture
Structs:
-
InterruptPriority- Priority levels- Variant:
Critical = 0- Highest priority (never masked) - Variant:
High = 4- Important interrupts - Variant:
Medium = 8- Normal interrupts - Variant:
Low = 12- Background interrupts
- Variant:
-
PriorityGroup<T>- Data with priority-based access- Field:
data: Mutex<RefCell<T>>- Protected data - Field:
priority: InterruptPriority- Minimum priority to access
- Field:
-
InterruptStats- Per-interrupt statistics- Field:
count: AtomicU32- Invocation count - Field:
max_duration_us: AtomicU32- Longest execution time - Field:
preemption_count: AtomicU32- Times preempted
- Field:
Functions:
configure_interrupt_priority(irq: Interrupt, priority: InterruptPriority)- Set NVIC priorityPriorityGroup::new(data: T, priority: InterruptPriority) -> Self- Create protected dataaccess<F, R>(&self, f: F) -> R- Access data if caller priority is sufficientInterruptStats::record_execution(duration_us: u32)- Update statistics
Starter Code:
#![allow(unused)]
fn main() {
use cortex_m::peripheral::NVIC;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum InterruptPriority {
Critical = 0, // Highest - never preempted
High = 4,
Medium = 8,
Low = 12, // Lowest - can be preempted by all others
}
impl InterruptPriority {
/// Convert to NVIC priority value
pub fn to_nvic_priority(self) -> u8 {
self as u8
}
}
pub struct PriorityGroup<T> {
data: Mutex<RefCell<T>>,
priority: InterruptPriority,
}
impl<T> PriorityGroup<T> {
pub const fn new(data: T, priority: InterruptPriority) -> Self {
// TODO: Initialize with Mutex and priority
todo!("Implement PriorityGroup::new")
}
/// Access data if caller has sufficient priority
pub fn access<F, R>(&self, current_priority: InterruptPriority, f: F) -> Result<R, AccessError>
where
F: FnOnce(&mut T) -> R,
{
// TODO: Check if current_priority >= self.priority
// TODO: If yes, access data in critical section
// TODO: If no, return Err(AccessError::InsufficientPriority)
todo!("Implement access")
}
pub fn priority(&self) -> InterruptPriority {
self.priority
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessError {
InsufficientPriority,
}
#[derive(Debug)]
pub struct InterruptStats {
pub count: AtomicU32,
pub max_duration_us: AtomicU32,
pub preemption_count: AtomicU32,
}
impl InterruptStats {
pub const fn new() -> Self {
Self {
count: AtomicU32::new(0),
max_duration_us: AtomicU32::new(0),
preemption_count: AtomicU32::new(0),
}
}
pub fn record_execution(&self, duration_us: u32) {
// TODO: Increment count
// TODO: Update max_duration if current duration is larger
// HINT: Use compare_exchange in loop for max update
todo!("Implement record_execution")
}
pub fn record_preemption(&self) {
self.preemption_count.fetch_add(1, Ordering::Relaxed);
}
pub fn snapshot(&self) -> StatSnapshot {
StatSnapshot {
count: self.count.load(Ordering::Relaxed),
max_duration_us: self.max_duration_us.load(Ordering::Relaxed),
preemption_count: self.preemption_count.load(Ordering::Relaxed),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct StatSnapshot {
pub count: u32,
pub max_duration_us: u32,
pub preemption_count: u32,
}
}
Checkpoint Tests:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_priority_levels() {
assert!(InterruptPriority::Critical < InterruptPriority::High);
assert!(InterruptPriority::High < InterruptPriority::Medium);
assert!(InterruptPriority::Medium < InterruptPriority::Low);
}
#[test]
fn test_priority_group_access() {
let group = PriorityGroup::new(42u32, InterruptPriority::Medium);
// High priority can access medium priority data
let result = group.access(InterruptPriority::High, |data| *data);
assert_eq!(result, Ok(42));
// Low priority cannot access medium priority data
let result = group.access(InterruptPriority::Low, |data| *data);
assert_eq!(result, Err(AccessError::InsufficientPriority));
// Same priority can access
let result = group.access(InterruptPriority::Medium, |data| {
*data += 1;
*data
});
assert_eq!(result, Ok(43));
}
#[test]
fn test_interrupt_stats() {
let stats = InterruptStats::new();
stats.record_execution(100);
stats.record_execution(150);
stats.record_execution(120);
let snapshot = stats.snapshot();
assert_eq!(snapshot.count, 3);
assert_eq!(snapshot.max_duration_us, 150);
stats.record_preemption();
stats.record_preemption();
let snapshot = stats.snapshot();
assert_eq!(snapshot.preemption_count, 2);
}
#[test]
fn test_max_duration_update() {
let stats = InterruptStats::new();
// Record multiple durations
for duration in [50, 100, 75, 200, 120] {
stats.record_execution(duration);
}
assert_eq!(stats.max_duration_us.load(Ordering::Relaxed), 200);
assert_eq!(stats.count.load(Ordering::Relaxed), 5);
}
}
}
Check Your Understanding:
- How does priority-based access prevent priority inversion?
- Why use atomic compare-exchange for max duration update?
- What’s the trade-off between more priority levels vs. fewer?
Why Milestone 2 Isn’t Enough
Limitation: We can protect individual pieces of data, but real applications need to coordinate multiple interrupts with complex event flows.
What we’re adding: An interrupt event dispatcher that routes events from various interrupt sources to handlers, with queuing and filtering.
Improvement:
- Architecture: Clean separation of interrupt handling from business logic
- Flexibility: Dynamic event routing and filtering
- Testability: Can inject events without hardware
- Debugging: Central point to monitor all interrupt activity
Milestone 3: Event Dispatcher with Lock-Free Queues
Goal: Build an event dispatcher that receives events from multiple interrupt sources and routes them to handlers using lock-free queues.
Why this milestone: Decoupling interrupt sources from handlers improves system architecture. This teaches lock-free SPSC queues and event-driven design.
Architecture
Structs:
-
InterruptEvent- Tagged event from interrupt source- Field:
source: EventSource- Which peripheral generated event - Field:
data: u32- Event-specific data - Field:
timestamp: u64- When event occurred (microseconds)
- Field:
-
EventSource- Event origin- Variant:
Timer1,Timer2,Uart,Gpio(u8),Dma(u8)
- Variant:
-
EventDispatcher<const N: usize>- Central event router- Field:
queue: SpscQueue<InterruptEvent, N>- Event queue - Field:
stats: [InterruptStats; NUM_SOURCES]- Per-source stats
- Field:
Functions:
push_event(&self, event: InterruptEvent) -> Result<(), InterruptEvent>- Add event from ISRpop_event(&self) -> Option<InterruptEvent>- Get event for processingdispatch_all<F>(&self, handler: F)- Process all pending eventsget_source_stats(&self, source: EventSource) -> StatSnapshot- Get statistics
Starter Code:
#![allow(unused)]
fn main() {
use heapless::spsc::Queue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EventSource {
Timer1,
Timer2,
Uart,
Gpio(u8),
Dma(u8),
}
impl EventSource {
fn to_index(&self) -> usize {
match self {
EventSource::Timer1 => 0,
EventSource::Timer2 => 1,
EventSource::Uart => 2,
EventSource::Gpio(pin) => 3 + (*pin as usize % 8),
EventSource::Dma(ch) => 11 + (*ch as usize % 8),
}
}
}
const NUM_SOURCES: usize = 20; // Enough for all variants
#[derive(Debug, Clone, Copy)]
pub struct InterruptEvent {
pub source: EventSource,
pub data: u32,
pub timestamp: u64,
}
pub struct EventDispatcher<const N: usize> {
queue: Mutex<RefCell<Queue<InterruptEvent, N>>>,
stats: [InterruptStats; NUM_SOURCES],
dropped_events: AtomicU32,
}
impl<const N: usize> EventDispatcher<N> {
pub const fn new() -> Self {
// TODO: Initialize queue, stats array, and dropped counter
// HINT: Use array initialization with const fn
todo!("Implement EventDispatcher::new")
}
/// Push event from interrupt (lock-free, fast)
pub fn push_event(&self, event: InterruptEvent) -> Result<(), InterruptEvent> {
// TODO: Try to enqueue event
// TODO: If queue full, increment dropped_events and return Err
// TODO: If successful, update source stats
todo!("Implement push_event")
}
/// Pop event for processing (main loop)
pub fn pop_event(&self) -> Option<InterruptEvent> {
// TODO: Dequeue event
todo!("Implement pop_event")
}
/// Process all pending events
pub fn dispatch_all<F>(&self, mut handler: F)
where
F: FnMut(InterruptEvent),
{
// TODO: Pop and handle all events in queue
todo!("Implement dispatch_all")
}
pub fn get_source_stats(&self, source: EventSource) -> StatSnapshot {
self.stats[source.to_index()].snapshot()
}
pub fn dropped_count(&self) -> u32 {
self.dropped_events.load(Ordering::Relaxed)
}
pub fn pending_count(&self) -> usize {
cortex_m::interrupt::free(|cs| {
self.queue.borrow(cs).borrow().len()
})
}
}
}
Checkpoint Tests:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
fn make_event(source: EventSource, data: u32) -> InterruptEvent {
InterruptEvent {
source,
data,
timestamp: 1000,
}
}
#[test]
fn test_dispatcher_basic() {
let dispatcher: EventDispatcher<16> = EventDispatcher::new();
let event = make_event(EventSource::Timer1, 42);
dispatcher.push_event(event).unwrap();
assert_eq!(dispatcher.pending_count(), 1);
let popped = dispatcher.pop_event().unwrap();
assert_eq!(popped.source, EventSource::Timer1);
assert_eq!(popped.data, 42);
assert_eq!(dispatcher.pending_count(), 0);
}
#[test]
fn test_multiple_sources() {
let dispatcher: EventDispatcher<32> = EventDispatcher::new();
dispatcher.push_event(make_event(EventSource::Timer1, 1)).unwrap();
dispatcher.push_event(make_event(EventSource::Uart, 2)).unwrap();
dispatcher.push_event(make_event(EventSource::Gpio(5), 3)).unwrap();
let events: Vec<_> = core::iter::from_fn(|| dispatcher.pop_event()).collect();
assert_eq!(events.len(), 3);
assert_eq!(events[0].data, 1);
assert_eq!(events[1].data, 2);
assert_eq!(events[2].data, 3);
}
#[test]
fn test_queue_overflow() {
let dispatcher: EventDispatcher<4> = EventDispatcher::new();
// Fill queue
for i in 0..4 {
dispatcher.push_event(make_event(EventSource::Timer1, i)).unwrap();
}
// Should fail - queue full
let result = dispatcher.push_event(make_event(EventSource::Timer1, 999));
assert!(result.is_err());
assert_eq!(dispatcher.dropped_count(), 1);
}
#[test]
fn test_dispatch_all() {
let dispatcher: EventDispatcher<16> = EventDispatcher::new();
for i in 0..5 {
dispatcher.push_event(make_event(EventSource::Uart, i)).unwrap();
}
let mut received = Vec::new();
dispatcher.dispatch_all(|event| {
received.push(event.data);
});
assert_eq!(received, vec![0, 1, 2, 3, 4]);
assert_eq!(dispatcher.pending_count(), 0);
}
#[test]
fn test_source_stats() {
let dispatcher: EventDispatcher<32> = EventDispatcher::new();
// Send events from different sources
for _ in 0..10 {
dispatcher.push_event(make_event(EventSource::Timer1, 0)).unwrap();
}
for _ in 0..5 {
dispatcher.push_event(make_event(EventSource::Uart, 0)).unwrap();
}
let timer_stats = dispatcher.get_source_stats(EventSource::Timer1);
let uart_stats = dispatcher.get_source_stats(EventSource::Uart);
assert_eq!(timer_stats.count, 10);
assert_eq!(uart_stats.count, 5);
}
}
}
Check Your Understanding:
- Why use SPSC queue instead of MPSC queue?
- What happens if main loop can’t keep up with interrupt rate?
- How would you prioritize certain event sources over others?
Why Milestone 3 Isn’t Enough
Limitation: Events are dispatched but there’s no way to monitor deadlines or detect when interrupts are taking too long.
What we’re adding: Deadline monitoring and watchdog integration to detect and handle timing violations.
Improvement:
- Safety: Detect runaway interrupts before they cause system failure
- Observability: Real-time visibility into timing behavior
- Recovery: Graceful handling of deadline violations
- Debugging: Identifies performance bottlenecks
Milestone 4: Deadline Monitoring and Watchdog Integration
Goal: Add deadline monitoring for interrupt handlers and integrate with hardware watchdog to detect and recover from timing violations.
Why this milestone: Real-time systems must guarantee timing. This teaches deadline enforcement and watchdog patterns.
Architecture
Structs:
-
DeadlineMonitor- Tracks timing violations- Field:
deadlines: [(EventSource, u32); N]- Source → deadline (μs) mapping - Field:
violations: [AtomicU32; NUM_SOURCES]- Violation counts - Field:
watchdog_enabled: AtomicBool- Watchdog state
- Field:
-
TimedEvent- Event with deadline information- Field:
event: InterruptEvent- Base event - Field:
deadline_us: u32- Processing deadline - Field:
start_time: u64- When processing started
- Field:
Functions:
DeadlineMonitor::new() -> Self- Create monitorset_deadline(&mut self, source: EventSource, deadline_us: u32)- Configure deadlinestart_processing(&self, event: InterruptEvent) -> TimedEvent- Begin timingfinish_processing(&self, timed: TimedEvent) -> Result<u32, DeadlineViolation>- Check deadlineget_violations(&self, source: EventSource) -> u32- Get violation countreset_watchdog(&self)- Pet the watchdog
Starter Code:
#![allow(unused)]
fn main() {
use cortex_m::peripheral::DWT;
#[derive(Debug, Clone, Copy)]
pub struct DeadlineViolation {
pub source: EventSource,
pub deadline_us: u32,
pub actual_us: u32,
pub overrun_us: u32,
}
pub struct DeadlineMonitor {
deadlines: [(EventSource, u32); NUM_SOURCES],
violations: [AtomicU32; NUM_SOURCES],
watchdog_fed: AtomicU32, // Last watchdog reset time
}
impl DeadlineMonitor {
pub const fn new() -> Self {
// TODO: Initialize deadlines array with default values
// TODO: Initialize violations to zero
todo!("Implement DeadlineMonitor::new")
}
/// Set deadline for a source
pub fn set_deadline(&mut self, source: EventSource, deadline_us: u32) {
// TODO: Store deadline in array
todo!("Implement set_deadline")
}
/// Start timing an event
pub fn start_processing(&self, event: InterruptEvent) -> TimedEvent {
// TODO: Get current timestamp
// TODO: Look up deadline for source
// TODO: Return TimedEvent with start time
todo!("Implement start_processing")
}
/// Finish processing and check deadline
pub fn finish_processing(&self, timed: TimedEvent) -> Result<u32, DeadlineViolation> {
// TODO: Get current timestamp
// TODO: Calculate elapsed time
// TODO: Check against deadline
// TODO: If violated, increment violation counter and return Err
// TODO: If OK, return Ok(elapsed_us)
todo!("Implement finish_processing")
}
pub fn get_violations(&self, source: EventSource) -> u32 {
self.violations[source.to_index()].load(Ordering::Relaxed)
}
/// Reset watchdog timer
pub fn reset_watchdog(&self) {
// TODO: Update watchdog_fed timestamp
// TODO: In real hardware, would write to watchdog peripheral
self.watchdog_fed.store(current_time_us(), Ordering::Relaxed);
}
/// Check if watchdog needs feeding
pub fn needs_watchdog_reset(&self, timeout_us: u32) -> bool {
let last_fed = self.watchdog_fed.load(Ordering::Relaxed);
let now = current_time_us();
(now - last_fed) > timeout_us
}
}
pub struct TimedEvent {
pub event: InterruptEvent,
pub deadline_us: u32,
pub start_time: u64,
}
/// Get current time in microseconds (using DWT cycle counter)
fn current_time_us() -> u32 {
// TODO: Read DWT cycle counter
// TODO: Convert cycles to microseconds based on CPU frequency
// Simplified version:
0 // Placeholder
}
}
Checkpoint Tests:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_deadline_configuration() {
let mut monitor = DeadlineMonitor::new();
monitor.set_deadline(EventSource::Timer1, 1000);
monitor.set_deadline(EventSource::Uart, 5000);
// Verify deadlines stored correctly
assert_eq!(monitor.deadlines[EventSource::Timer1.to_index()].1, 1000);
assert_eq!(monitor.deadlines[EventSource::Uart.to_index()].1, 5000);
}
#[test]
fn test_deadline_met() {
let mut monitor = DeadlineMonitor::new();
monitor.set_deadline(EventSource::Timer1, 10000); // 10ms deadline
let event = InterruptEvent {
source: EventSource::Timer1,
data: 42,
timestamp: 0,
};
let timed = monitor.start_processing(event);
// Simulate fast processing
thread::sleep(Duration::from_micros(100));
let result = monitor.finish_processing(timed);
assert!(result.is_ok());
let elapsed = result.unwrap();
assert!(elapsed < 10000);
}
#[test]
fn test_deadline_violation() {
let mut monitor = DeadlineMonitor::new();
monitor.set_deadline(EventSource::Timer1, 100); // 100μs deadline
let event = InterruptEvent {
source: EventSource::Timer1,
data: 42,
timestamp: 0,
};
let timed = monitor.start_processing(event);
// Simulate slow processing (exceeds deadline)
thread::sleep(Duration::from_micros(200));
let result = monitor.finish_processing(timed);
assert!(result.is_err());
let violation = result.unwrap_err();
assert_eq!(violation.source, EventSource::Timer1);
assert!(violation.actual_us > violation.deadline_us);
assert_eq!(monitor.get_violations(EventSource::Timer1), 1);
}
#[test]
fn test_watchdog_feeding() {
let monitor = DeadlineMonitor::new();
assert!(monitor.needs_watchdog_reset(1000)); // Not fed yet
monitor.reset_watchdog();
assert!(!monitor.needs_watchdog_reset(1000)); // Just fed
thread::sleep(Duration::from_millis(1100));
assert!(monitor.needs_watchdog_reset(1000)); // Timeout expired
}
#[test]
fn test_multiple_violations() {
let mut monitor = DeadlineMonitor::new();
monitor.set_deadline(EventSource::Uart, 50);
for _ in 0..5 {
let event = InterruptEvent {
source: EventSource::Uart,
data: 0,
timestamp: 0,
};
let timed = monitor.start_processing(event);
thread::sleep(Duration::from_micros(100));
let _ = monitor.finish_processing(timed);
}
assert_eq!(monitor.get_violations(EventSource::Uart), 5);
}
}
}
Check Your Understanding:
- Why measure execution time instead of just detecting hangs?
- How does watchdog prevent system lockup?
- What should happen when a deadline is violated?
Why Milestone 4 Isn’t Enough
Limitation: We have all the pieces but they’re not integrated into a cohesive system. Real applications need everything working together.
What we’re adding: Complete interrupt coordinator that integrates event dispatch, priority management, deadline monitoring, and statistics.
Improvement:
- Integration: All features working together
- Production-ready: Complete error handling and monitoring
- Scalable: Can manage 20+ interrupt sources
- Observable: Rich debugging and profiling data
Milestone 5: Integrated Interrupt Coordinator
Goal: Combine all previous milestones into a unified interrupt coordinator that manages event dispatch, priorities, deadlines, and statistics.
Why this milestone: Real systems need all components integrated. This teaches system-level architecture and integration patterns.
Architecture
Structs:
InterruptCoordinator<const N: usize>- Complete coordinator- Field:
dispatcher: EventDispatcher<N>- Event routing - Field:
monitor: DeadlineMonitor- Timing enforcement - Field:
priority_groups: [PriorityGroup<()>; 4]- Priority management
- Field:
Functions:
new() -> Self- Create coordinatorconfigure_source(&mut self, source, priority, deadline)- Setup interrupt sourcehandle_interrupt(&self, event)- Called from ISRprocess_events<F>(&self, handler: F)- Main loop processingget_system_health(&self) -> SystemHealth- Comprehensive status
Starter Code:
#![allow(unused)]
fn main() {
pub struct InterruptCoordinator<const N: usize> {
dispatcher: EventDispatcher<N>,
monitor: DeadlineMonitor,
total_events: AtomicU32,
total_violations: AtomicU32,
}
#[derive(Debug, Clone)]
pub struct SystemHealth {
pub total_events: u32,
pub pending_events: usize,
pub dropped_events: u32,
pub total_violations: u32,
pub source_stats: [(EventSource, StatSnapshot); NUM_SOURCES],
}
impl<const N: usize> InterruptCoordinator<N> {
pub const fn new() -> Self {
// TODO: Initialize all components
todo!("Implement InterruptCoordinator::new")
}
/// Configure an interrupt source
pub fn configure_source(
&mut self,
source: EventSource,
priority: InterruptPriority,
deadline_us: u32,
) {
// TODO: Set deadline in monitor
// TODO: Configure NVIC priority
todo!("Implement configure_source")
}
/// Handle interrupt (call from ISR)
pub fn handle_interrupt(&self, event: InterruptEvent) {
// TODO: Push event to dispatcher
// TODO: Increment total_events counter
// TODO: Reset watchdog if enabled
todo!("Implement handle_interrupt")
}
/// Process all pending events
pub fn process_events<F>(&self, mut handler: F)
where
F: FnMut(InterruptEvent),
{
// TODO: Dispatch all events
// TODO: For each event:
// - Start timing with monitor
// - Call handler
// - Check deadline
// - Update violation count if needed
todo!("Implement process_events")
}
/// Get comprehensive system health
pub fn get_system_health(&self) -> SystemHealth {
// TODO: Collect statistics from all sources
// TODO: Aggregate violation counts
// TODO: Return SystemHealth struct
todo!("Implement get_system_health")
}
pub fn reset_statistics(&self) {
// TODO: Reset all counters
todo!("Implement reset_statistics")
}
}
}
Checkpoint Tests:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coordinator_basic_flow() {
let mut coordinator: InterruptCoordinator<32> = InterruptCoordinator::new();
coordinator.configure_source(
EventSource::Timer1,
InterruptPriority::High,
1000,
);
// Simulate interrupt
let event = InterruptEvent {
source: EventSource::Timer1,
data: 42,
timestamp: 0,
};
coordinator.handle_interrupt(event);
// Process events
let mut received = Vec::new();
coordinator.process_events(|e| {
received.push(e.data);
});
assert_eq!(received, vec![42]);
}
#[test]
fn test_system_health() {
let mut coordinator: InterruptCoordinator<16> = InterruptCoordinator::new();
coordinator.configure_source(EventSource::Timer1, InterruptPriority::High, 1000);
coordinator.configure_source(EventSource::Uart, InterruptPriority::Medium, 5000);
// Generate events
for i in 0..10 {
coordinator.handle_interrupt(InterruptEvent {
source: EventSource::Timer1,
data: i,
timestamp: i as u64 * 1000,
});
}
let health = coordinator.get_system_health();
assert_eq!(health.total_events, 10);
assert_eq!(health.pending_events, 10);
assert_eq!(health.dropped_events, 0);
}
#[test]
fn test_mixed_priority_events() {
let mut coordinator: InterruptCoordinator<64> = InterruptCoordinator::new();
coordinator.configure_source(EventSource::Timer1, InterruptPriority::Critical, 100);
coordinator.configure_source(EventSource::Uart, InterruptPriority::Low, 10000);
// Interleave high and low priority events
for i in 0..20 {
let source = if i % 2 == 0 {
EventSource::Timer1
} else {
EventSource::Uart
};
coordinator.handle_interrupt(InterruptEvent {
source,
data: i,
timestamp: i as u64 * 100,
});
}
let mut processed = Vec::new();
coordinator.process_events(|e| {
processed.push((e.source, e.data));
});
assert_eq!(processed.len(), 20);
}
}
}
Check Your Understanding:
- How does the coordinator prevent priority inversion?
- What’s the benefit of centralizing interrupt handling?
- How would you extend this for multi-core systems?
Why Milestone 5 Isn’t Enough
Limitation: The coordinator works well but doesn’t leverage modern async patterns. Embassy/RTIC provide better abstractions for real-time scheduling.
What we’re adding: Integration with Embassy for async interrupt handling and deterministic task scheduling.
Improvement:
- Modern patterns: Async/await for interrupt coordination
- Framework integration: Works with Embassy ecosystem
- Efficiency: Zero-cost async abstractions
- Developer experience: Easier to reason about concurrent interrupts
Milestone 6: Embassy/RTIC Integration
Goal: Integrate the interrupt coordinator with Embassy’s async runtime for modern, efficient interrupt handling with zero-cost abstractions.
Why this milestone: Embassy is the future of embedded Rust. This teaches how to build interrupt systems on top of async foundations.
Architecture
Key Integration Points:
- Use Embassy channels for event passing
- Interrupt signals for async task waking
- Priority-based task spawning
- Async deadline monitoring
Starter Code:
use embassy_executor::Spawner;
use embassy_sync::channel::Channel;
use embassy_sync::signal::Signal;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::{Duration, Timer, with_timeout};
type EventChannel = Channel<CriticalSectionRawMutex, InterruptEvent, 32>;
static EVENT_CHANNEL: EventChannel = Channel::new();
// Signals for each interrupt source
static TIMER_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
static UART_SIGNAL: Signal<CriticalSectionRawMutex, ()> = Signal::new();
/// High-priority task for critical interrupts
#[embassy_executor::task]
async fn critical_interrupt_handler(coordinator: &'static InterruptCoordinator<32>) {
let receiver = EVENT_CHANNEL.receiver();
loop {
let event = receiver.receive().await;
if event.source == EventSource::Timer1 {
// Critical processing with deadline
let result = with_timeout(
Duration::from_micros(100),
process_critical_event(event),
).await;
if result.is_err() {
defmt::error!("Critical event deadline missed!");
}
}
}
}
async fn process_critical_event(event: InterruptEvent) {
// Critical processing logic
defmt::info!("Processing critical event: {:?}", event);
}
/// Medium-priority task for normal interrupts
#[embassy_executor::task]
async fn normal_interrupt_handler() {
let receiver = EVENT_CHANNEL.receiver();
loop {
let event = receiver.receive().await;
match event.source {
EventSource::Uart => {
process_uart_event(event).await;
}
EventSource::Gpio(pin) => {
process_gpio_event(pin, event).await;
}
_ => {}
}
}
}
async fn process_uart_event(event: InterruptEvent) {
defmt::info!("UART data: {}", event.data);
}
async fn process_gpio_event(pin: u8, event: InterruptEvent) {
defmt::info!("GPIO {} changed: {}", pin, event.data);
}
/// Watchdog task ensures system health
#[embassy_executor::task]
async fn watchdog_task(coordinator: &'static InterruptCoordinator<32>) {
loop {
Timer::after(Duration::from_millis(500)).await;
let health = coordinator.get_system_health();
if health.dropped_events > 0 {
defmt::warn!("Dropped {} events!", health.dropped_events);
}
if health.total_violations > 0 {
defmt::warn!("Deadline violations: {}", health.total_violations);
}
defmt::debug!(
"Health: {} events, {} pending",
health.total_events,
health.pending_events
);
}
}
/// Main application
#[embassy_executor::main]
async fn main(spawner: Spawner) {
defmt::info!("Interrupt Coordinator with Embassy starting...");
// Initialize hardware
let p = embassy_stm32::init(Default::default());
// Create static coordinator
static mut COORDINATOR: Option<InterruptCoordinator<32>> = None;
unsafe {
COORDINATOR = Some(InterruptCoordinator::new());
}
let coordinator = unsafe { COORDINATOR.as_ref().unwrap() };
// Configure interrupt sources
unsafe {
COORDINATOR.as_mut().unwrap().configure_source(
EventSource::Timer1,
InterruptPriority::Critical,
100,
);
COORDINATOR.as_mut().unwrap().configure_source(
EventSource::Uart,
InterruptPriority::Medium,
5000,
);
}
// Spawn interrupt handling tasks
spawner.spawn(critical_interrupt_handler(coordinator)).unwrap();
spawner.spawn(normal_interrupt_handler()).unwrap();
spawner.spawn(watchdog_task(coordinator)).unwrap();
defmt::info!("All tasks spawned, system running");
// Main loop can do other work or just monitor
loop {
Timer::after(Duration::from_secs(10)).await;
defmt::info!("System heartbeat");
}
}
// ===== Actual interrupt handlers (hardware) =====
#[interrupt]
fn TIM2() {
// Timer interrupt - push event and signal task
let event = InterruptEvent {
source: EventSource::Timer1,
data: read_timer_data(),
timestamp: current_time_us() as u64,
};
unsafe {
if let Some(coordinator) = COORDINATOR.as_ref() {
coordinator.handle_interrupt(event);
}
}
TIMER_SIGNAL.signal(());
}
#[interrupt]
fn USART2() {
// UART interrupt
let event = InterruptEvent {
source: EventSource::Uart,
data: read_uart_data(),
timestamp: current_time_us() as u64,
};
unsafe {
if let Some(coordinator) = COORDINATOR.as_ref() {
coordinator.handle_interrupt(event);
}
}
UART_SIGNAL.signal(());
}
Checkpoint Tests:
#![allow(unused)]
fn main() {
#[embassy_executor::test]
async fn test_embassy_integration() {
let coordinator: &'static InterruptCoordinator<64> =
Box::leak(Box::new(InterruptCoordinator::new()));
// Simulate interrupts
for i in 0..5 {
coordinator.handle_interrupt(InterruptEvent {
source: EventSource::Timer1,
data: i,
timestamp: i as u64 * 1000,
});
}
// Process with timeout
let mut count = 0;
let result = with_timeout(Duration::from_millis(100), async {
coordinator.process_events(|_| {
count += 1;
});
}).await;
assert!(result.is_ok());
assert_eq!(count, 5);
}
}
Check Your Understanding:
- How do Embassy channels differ from the SPSC queue?
- Why use
with_timeoutfor deadline enforcement? - What’s the advantage of task-based interrupt handling?
Complete Working Example
Full implementation available in examples/interrupt_coordinator.rs
Features:
- ✅ Safe shared state with critical sections
- ✅ Priority-based interrupt management
- ✅ Lock-free event dispatcher
- ✅ Deadline monitoring with watchdog
- ✅ Comprehensive statistics and profiling
- ✅ Embassy async integration
- ✅ Multi-source coordination
Expected Performance:
- Interrupt latency: <1μs for critical priority
- Event processing: 100,000+ events/sec
- Zero deadlocks or priority inversions
- Deterministic deadline enforcement
Testing Your Implementation
Unit Tests
cargo test --lib
Hardware Tests
# STM32 with multiple interrupt sources
cargo test --features stm32f4 --target thumbv7em-none-eabihf
Stress Tests
# Generate high interrupt load
cargo run --example stress_test --release
Extensions
- Multi-core support: Distribute interrupts across cores
- Dynamic priorities: Adjust priorities based on load
- Advanced scheduling: Rate monotonic or EDF scheduling
- Tracing integration: defmt or RTT for interrupt tracing
- Power management: Sleep between interrupts, wake on event
- Safety certification: MISRA compliance, formal verification
This project demonstrates production-grade interrupt management for safety-critical embedded systems.