⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/irq/
mod.rs

1//! Architecture-independent IRQ abstraction layer
2//!
3//! Provides a generic interface for interrupt management that delegates to
4//! per-architecture interrupt controllers:
5//! - x86_64: Local APIC + I/O APIC
6//! - AArch64: GICv2 (Generic Interrupt Controller)
7//! - RISC-V: PLIC (Platform-Level Interrupt Controller)
8//!
9//! This module implements the IRQ object abstraction (H-003) from the
10//! remediation backlog, providing a unified API for registering handlers,
11//! enabling/disabling IRQ lines, and dispatching interrupts.
12
13// IRQ management
14
15#[cfg(feature = "alloc")]
16extern crate alloc;
17
18#[cfg(feature = "alloc")]
19use alloc::collections::BTreeMap;
20
21use spin::Mutex;
22
23use crate::{
24    error::{KernelError, KernelResult},
25    sync::once_lock::GlobalState,
26};
27
28// ---------------------------------------------------------------------------
29// IRQ number newtype
30// ---------------------------------------------------------------------------
31
32/// Architecture-independent IRQ number.
33///
34/// Wraps a `u32` to provide type safety and prevent accidental misuse of
35/// raw integer values as IRQ numbers.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
37pub struct IrqNumber(pub u32);
38
39impl IrqNumber {
40    /// Create a new IRQ number.
41    pub const fn new(irq: u32) -> Self {
42        Self(irq)
43    }
44
45    /// Get the raw IRQ number as a `u32`.
46    #[inline]
47    pub fn as_u32(self) -> u32 {
48        self.0
49    }
50}
51
52impl From<u32> for IrqNumber {
53    fn from(value: u32) -> Self {
54        Self(value)
55    }
56}
57
58impl From<IrqNumber> for u32 {
59    fn from(irq: IrqNumber) -> u32 {
60        irq.0
61    }
62}
63
64impl core::fmt::Display for IrqNumber {
65    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
66        write!(f, "IRQ#{}", self.0)
67    }
68}
69
70// ---------------------------------------------------------------------------
71// IRQ handler type
72// ---------------------------------------------------------------------------
73
74/// Type alias for IRQ handler functions.
75///
76/// An IRQ handler is a function that takes the IRQ number that triggered the
77/// interrupt. Handlers are registered via [`register_handler`] and invoked
78/// by [`dispatch`] when the corresponding interrupt fires.
79pub type IrqHandler = fn(IrqNumber);
80
81// ---------------------------------------------------------------------------
82// IRQ controller trait
83// ---------------------------------------------------------------------------
84
85/// Architecture-independent interrupt controller interface.
86///
87/// Each architecture implements this trait for its hardware interrupt
88/// controller (APIC, GIC, PLIC). The [`IrqManager`] delegates hardware
89/// operations through this trait.
90pub trait IrqController {
91    /// Enable an interrupt line so it can be delivered to the CPU.
92    fn enable(&self, irq: IrqNumber) -> KernelResult<()>;
93
94    /// Disable an interrupt line to prevent delivery.
95    fn disable(&self, irq: IrqNumber) -> KernelResult<()>;
96
97    /// Acknowledge receipt of an interrupt.
98    ///
99    /// On some architectures this is a separate step from EOI.
100    fn acknowledge(&self, irq: IrqNumber) -> KernelResult<()>;
101
102    /// Signal end-of-interrupt to the controller.
103    ///
104    /// Must be called after the interrupt handler has finished processing.
105    fn eoi(&self, irq: IrqNumber) -> KernelResult<()>;
106
107    /// Set the priority of an interrupt line.
108    ///
109    /// The meaning of `priority` is architecture-dependent:
110    /// - x86_64 APIC: Not directly supported per-IRQ (no-op)
111    /// - AArch64 GIC: 0x00 = highest, 0xFF = lowest
112    /// - RISC-V PLIC: 0 = disabled, 1-7 = priority levels
113    fn set_priority(&self, irq: IrqNumber, priority: u8) -> KernelResult<()>;
114
115    /// Check whether an interrupt is pending.
116    fn is_pending(&self, irq: IrqNumber) -> KernelResult<bool>;
117}
118
119// ---------------------------------------------------------------------------
120// IRQ manager
121// ---------------------------------------------------------------------------
122
123/// Maximum number of IRQ lines supported.
124///
125/// This covers the common range across all supported architectures:
126/// - x86_64 I/O APIC: typically 24 lines
127/// - AArch64 GIC: up to 1020 interrupt IDs
128/// - RISC-V PLIC: up to 127 sources
129const MAX_IRQ: u32 = 256;
130
131/// Central IRQ manager that maintains registered handlers and delegates
132/// hardware operations to the architecture-specific controller.
133///
134/// The manager stores a mapping from IRQ numbers to handler functions.
135/// When an interrupt fires, the architecture-specific entry point calls
136/// [`dispatch`] which looks up and invokes the registered handler.
137pub struct IrqManager {
138    /// Registered IRQ handlers, keyed by raw IRQ number.
139    #[cfg(feature = "alloc")]
140    handlers: BTreeMap<u32, IrqHandler>,
141
142    /// Number of interrupts dispatched (for statistics).
143    dispatch_count: u64,
144}
145
146impl IrqManager {
147    /// Create a new IRQ manager with no registered handlers.
148    fn new() -> Self {
149        Self {
150            #[cfg(feature = "alloc")]
151            handlers: BTreeMap::new(),
152            dispatch_count: 0,
153        }
154    }
155
156    /// Register a handler for the given IRQ number.
157    ///
158    /// Returns an error if a handler is already registered for this IRQ.
159    #[cfg(feature = "alloc")]
160    fn register(&mut self, irq: IrqNumber, handler: IrqHandler) -> KernelResult<()> {
161        if irq.0 >= MAX_IRQ {
162            return Err(KernelError::InvalidArgument {
163                name: "irq",
164                value: "IRQ number exceeds maximum",
165            });
166        }
167
168        if self.handlers.contains_key(&irq.0) {
169            return Err(KernelError::AlreadyExists {
170                resource: "IRQ handler",
171                id: irq.0 as u64,
172            });
173        }
174
175        self.handlers.insert(irq.0, handler);
176        Ok(())
177    }
178
179    /// Unregister the handler for the given IRQ number.
180    ///
181    /// Returns an error if no handler is registered for this IRQ.
182    #[cfg(feature = "alloc")]
183    fn unregister(&mut self, irq: IrqNumber) -> KernelResult<()> {
184        if self.handlers.remove(&irq.0).is_none() {
185            return Err(KernelError::NotFound {
186                resource: "IRQ handler",
187                id: irq.0 as u64,
188            });
189        }
190        Ok(())
191    }
192
193    /// Dispatch an interrupt to the registered handler.
194    ///
195    /// If no handler is registered for the given IRQ, this is a no-op
196    /// (spurious interrupts are silently ignored).
197    #[cfg(feature = "alloc")]
198    fn dispatch(&mut self, irq: IrqNumber) {
199        self.dispatch_count += 1;
200        if let Some(&handler) = self.handlers.get(&irq.0) {
201            handler(irq);
202        }
203    }
204
205    /// Get the number of dispatched interrupts.
206    fn dispatch_count(&self) -> u64 {
207        self.dispatch_count
208    }
209}
210
211// ---------------------------------------------------------------------------
212// Global state
213// ---------------------------------------------------------------------------
214
215/// Global IRQ manager instance, protected by a spin::Mutex.
216///
217/// Initialized once by [`init`]. Uses the GlobalState pattern for safe
218/// global access without `static mut`.
219static IRQ_MANAGER: GlobalState<Mutex<IrqManager>> = GlobalState::new();
220
221// ---------------------------------------------------------------------------
222// Architecture-specific delegation
223// ---------------------------------------------------------------------------
224
225/// Enable an IRQ line on the architecture-specific interrupt controller.
226#[cfg(target_arch = "x86_64")]
227fn arch_enable_irq(irq: u32) -> KernelResult<()> {
228    crate::arch::x86_64::apic::unmask_irq(irq as u8)
229}
230
231/// Enable an IRQ line on the architecture-specific interrupt controller.
232#[cfg(target_arch = "aarch64")]
233fn arch_enable_irq(irq: u32) -> KernelResult<()> {
234    crate::arch::aarch64::gic::enable_irq(irq)
235}
236
237/// Enable an IRQ line on the architecture-specific interrupt controller.
238#[cfg(target_arch = "riscv64")]
239fn arch_enable_irq(irq: u32) -> KernelResult<()> {
240    crate::arch::riscv::plic::enable(irq)
241}
242
243/// Disable an IRQ line on the architecture-specific interrupt controller.
244#[cfg(target_arch = "x86_64")]
245fn arch_disable_irq(irq: u32) -> KernelResult<()> {
246    crate::arch::x86_64::apic::mask_irq(irq as u8)
247}
248
249/// Disable an IRQ line on the architecture-specific interrupt controller.
250#[cfg(target_arch = "aarch64")]
251fn arch_disable_irq(irq: u32) -> KernelResult<()> {
252    crate::arch::aarch64::gic::disable_irq(irq)
253}
254
255/// Disable an IRQ line on the architecture-specific interrupt controller.
256#[cfg(target_arch = "riscv64")]
257fn arch_disable_irq(irq: u32) -> KernelResult<()> {
258    crate::arch::riscv::plic::disable(irq)
259}
260
261/// Send end-of-interrupt to the architecture-specific controller.
262#[cfg(target_arch = "x86_64")]
263fn arch_eoi(_irq: u32) -> KernelResult<()> {
264    crate::arch::x86_64::apic::send_eoi();
265    Ok(())
266}
267
268/// Send end-of-interrupt to the architecture-specific controller.
269#[cfg(target_arch = "aarch64")]
270fn arch_eoi(irq: u32) -> KernelResult<()> {
271    crate::arch::aarch64::gic::eoi(irq);
272    Ok(())
273}
274
275/// Send end-of-interrupt to the architecture-specific controller.
276#[cfg(target_arch = "riscv64")]
277fn arch_eoi(irq: u32) -> KernelResult<()> {
278    crate::arch::riscv::plic::complete(irq)
279}
280
281/// Set IRQ priority on the architecture-specific controller.
282#[cfg(target_arch = "x86_64")]
283fn arch_set_priority(_irq: u32, _priority: u8) -> KernelResult<()> {
284    // x86_64 I/O APIC does not support per-IRQ priority in the same way;
285    // priority is managed via the Task Priority Register. This is a no-op.
286    Ok(())
287}
288
289/// Set IRQ priority on the architecture-specific controller.
290#[cfg(target_arch = "aarch64")]
291fn arch_set_priority(irq: u32, priority: u8) -> KernelResult<()> {
292    crate::arch::aarch64::gic::set_irq_priority(irq, priority)
293}
294
295/// Set IRQ priority on the architecture-specific controller.
296#[cfg(target_arch = "riscv64")]
297fn arch_set_priority(irq: u32, priority: u8) -> KernelResult<()> {
298    crate::arch::riscv::plic::set_priority(irq, priority as u32)
299}
300
301/// Check if an IRQ is pending on the architecture-specific controller.
302#[cfg(target_arch = "x86_64")]
303fn arch_is_pending(_irq: u32) -> KernelResult<bool> {
304    // x86_64: Checking individual IRQ pending status requires reading the
305    // IRR (Interrupt Request Register) from the Local APIC, which is not
306    // yet exposed. Return false as a safe default.
307    Ok(false)
308}
309
310/// Check if an IRQ is pending on the architecture-specific controller.
311#[cfg(target_arch = "aarch64")]
312fn arch_is_pending(_irq: u32) -> KernelResult<bool> {
313    // AArch64 GIC: Checking individual pending bits via GICD_ISPENDR is
314    // not yet exposed in the public GIC API. Return false as a safe default.
315    Ok(false)
316}
317
318/// Check if an IRQ is pending on the architecture-specific controller.
319#[cfg(target_arch = "riscv64")]
320fn arch_is_pending(irq: u32) -> KernelResult<bool> {
321    crate::arch::riscv::plic::is_pending(irq)
322}
323
324// ---------------------------------------------------------------------------
325// Public API
326// ---------------------------------------------------------------------------
327
328/// Initialize the IRQ manager subsystem.
329///
330/// Creates the global IRQ manager instance. Must be called after the
331/// architecture-specific interrupt controller has been initialized
332/// (APIC, GIC, or PLIC).
333///
334/// # Errors
335///
336/// Returns `KernelError::AlreadyExists` if the IRQ manager has already
337/// been initialized.
338pub fn init() -> KernelResult<()> {
339    IRQ_MANAGER
340        .init(Mutex::new(IrqManager::new()))
341        .map_err(|_| KernelError::AlreadyExists {
342            resource: "IRQ manager",
343            id: 0,
344        })?;
345
346    kprintln!("[IRQ] IRQ manager initialized");
347    Ok(())
348}
349
350/// Register an IRQ handler for the given interrupt number.
351///
352/// The handler will be invoked when the interrupt fires and [`dispatch`]
353/// is called. Only one handler may be registered per IRQ number.
354///
355/// # Errors
356///
357/// - `KernelError::NotInitialized` if the IRQ manager has not been initialized.
358/// - `KernelError::AlreadyExists` if a handler is already registered for this
359///   IRQ.
360/// - `KernelError::InvalidArgument` if the IRQ number exceeds the maximum.
361#[cfg(feature = "alloc")]
362pub fn register_handler(irq: IrqNumber, handler: IrqHandler) -> KernelResult<()> {
363    IRQ_MANAGER
364        .with_mut(|mtx| {
365            let mut mgr = mtx.lock();
366            mgr.register(irq, handler)
367        })
368        .unwrap_or(Err(KernelError::NotInitialized {
369            subsystem: "IRQ manager",
370        }))
371}
372
373/// Unregister the IRQ handler for the given interrupt number.
374///
375/// # Errors
376///
377/// - `KernelError::NotInitialized` if the IRQ manager has not been initialized.
378/// - `KernelError::NotFound` if no handler is registered for this IRQ.
379#[cfg(feature = "alloc")]
380pub fn unregister_handler(irq: IrqNumber) -> KernelResult<()> {
381    IRQ_MANAGER
382        .with_mut(|mtx| {
383            let mut mgr = mtx.lock();
384            mgr.unregister(irq)
385        })
386        .unwrap_or(Err(KernelError::NotInitialized {
387            subsystem: "IRQ manager",
388        }))
389}
390
391/// Dispatch an interrupt to the registered handler.
392///
393/// Called by the architecture-specific interrupt entry point when an
394/// external interrupt is received. Looks up the handler for the given
395/// IRQ number and invokes it. If no handler is registered, the interrupt
396/// is silently ignored (spurious).
397#[cfg(feature = "alloc")]
398pub fn dispatch(irq: IrqNumber) {
399    IRQ_MANAGER.with_mut(|mtx| {
400        let mut mgr = mtx.lock();
401        mgr.dispatch(irq);
402    });
403}
404
405/// Enable an IRQ line on the hardware interrupt controller.
406///
407/// Delegates to the architecture-specific controller:
408/// - x86_64: unmasks the IRQ in the I/O APIC
409/// - AArch64: enables the interrupt in the GIC distributor
410/// - RISC-V: enables the interrupt source in the PLIC
411///
412/// # Errors
413///
414/// - `KernelError::NotInitialized` if the interrupt controller has not been
415///   initialized.
416pub fn enable_irq(irq: IrqNumber) -> KernelResult<()> {
417    arch_enable_irq(irq.0)
418}
419
420/// Disable an IRQ line on the hardware interrupt controller.
421///
422/// Delegates to the architecture-specific controller:
423/// - x86_64: masks the IRQ in the I/O APIC
424/// - AArch64: disables the interrupt in the GIC distributor
425/// - RISC-V: disables the interrupt source in the PLIC
426///
427/// # Errors
428///
429/// - `KernelError::NotInitialized` if the interrupt controller has not been
430///   initialized.
431pub fn disable_irq(irq: IrqNumber) -> KernelResult<()> {
432    arch_disable_irq(irq.0)
433}
434
435/// Send end-of-interrupt to the hardware controller.
436///
437/// Must be called after the interrupt handler has finished processing.
438///
439/// # Errors
440///
441/// - `KernelError::NotInitialized` if the interrupt controller has not been
442///   initialized.
443pub fn eoi(irq: IrqNumber) -> KernelResult<()> {
444    arch_eoi(irq.0)
445}
446
447/// Set the priority of an IRQ line.
448///
449/// The interpretation of `priority` is architecture-dependent. See
450/// [`IrqController::set_priority`] for details.
451///
452/// # Errors
453///
454/// - `KernelError::NotInitialized` if the interrupt controller has not been
455///   initialized.
456/// - `KernelError::InvalidArgument` if the priority value is out of range.
457pub fn set_priority(irq: IrqNumber, priority: u8) -> KernelResult<()> {
458    arch_set_priority(irq.0, priority)
459}
460
461/// Check whether an IRQ is pending.
462///
463/// # Errors
464///
465/// - `KernelError::NotInitialized` if the interrupt controller has not been
466///   initialized.
467pub fn is_pending(irq: IrqNumber) -> KernelResult<bool> {
468    arch_is_pending(irq.0)
469}
470
471/// Get the number of interrupts dispatched since initialization.
472pub fn dispatch_count() -> u64 {
473    IRQ_MANAGER
474        .with(|mtx| {
475            let mgr = mtx.lock();
476            mgr.dispatch_count()
477        })
478        .unwrap_or(0)
479}
480
481// ---------------------------------------------------------------------------
482// Tests
483// ---------------------------------------------------------------------------
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488    use crate::error::KernelError;
489
490    // -- IrqNumber tests --
491
492    #[test]
493    fn irq_number_new_and_as_u32() {
494        let irq = IrqNumber::new(42);
495        assert_eq!(irq.as_u32(), 42);
496    }
497
498    #[test]
499    fn irq_number_from_u32() {
500        let irq = IrqNumber::from(7u32);
501        assert_eq!(irq.0, 7);
502    }
503
504    #[test]
505    fn irq_number_into_u32() {
506        let irq = IrqNumber::new(99);
507        let raw: u32 = irq.into();
508        assert_eq!(raw, 99);
509    }
510
511    #[test]
512    fn irq_number_display() {
513        use core::fmt::Write;
514        let irq = IrqNumber::new(13);
515        let mut buf = alloc::string::String::new();
516        write!(buf, "{}", irq).unwrap();
517        assert_eq!(buf, "IRQ#13");
518    }
519
520    #[test]
521    fn irq_number_equality() {
522        assert_eq!(IrqNumber::new(5), IrqNumber::new(5));
523        assert_ne!(IrqNumber::new(5), IrqNumber::new(6));
524    }
525
526    #[test]
527    fn irq_number_ordering() {
528        assert!(IrqNumber::new(1) < IrqNumber::new(2));
529        assert!(IrqNumber::new(255) > IrqNumber::new(0));
530    }
531
532    #[test]
533    fn irq_number_clone_copy() {
534        let a = IrqNumber::new(10);
535        let b = a; // Copy
536        let c = a.clone();
537        assert_eq!(a, b);
538        assert_eq!(a, c);
539    }
540
541    // -- IrqManager tests --
542
543    #[test]
544    fn manager_new_has_zero_dispatch_count() {
545        let mgr = IrqManager::new();
546        assert_eq!(mgr.dispatch_count(), 0);
547    }
548
549    #[test]
550    fn manager_register_handler_success() {
551        let mut mgr = IrqManager::new();
552        fn dummy(_irq: IrqNumber) {}
553        let result = mgr.register(IrqNumber::new(1), dummy);
554        assert!(result.is_ok());
555    }
556
557    #[test]
558    fn manager_register_duplicate_returns_error() {
559        let mut mgr = IrqManager::new();
560        fn dummy(_irq: IrqNumber) {}
561        mgr.register(IrqNumber::new(5), dummy).unwrap();
562        let result = mgr.register(IrqNumber::new(5), dummy);
563        assert_eq!(
564            result,
565            Err(KernelError::AlreadyExists {
566                resource: "IRQ handler",
567                id: 5,
568            })
569        );
570    }
571
572    #[test]
573    fn manager_register_invalid_irq_number() {
574        let mut mgr = IrqManager::new();
575        fn dummy(_irq: IrqNumber) {}
576        // MAX_IRQ is 256, so 256 should be rejected
577        let result = mgr.register(IrqNumber::new(256), dummy);
578        assert_eq!(
579            result,
580            Err(KernelError::InvalidArgument {
581                name: "irq",
582                value: "IRQ number exceeds maximum",
583            })
584        );
585    }
586
587    #[test]
588    fn manager_register_max_valid_irq() {
589        let mut mgr = IrqManager::new();
590        fn dummy(_irq: IrqNumber) {}
591        // 255 is the last valid IRQ (MAX_IRQ - 1)
592        let result = mgr.register(IrqNumber::new(255), dummy);
593        assert!(result.is_ok());
594    }
595
596    #[test]
597    fn manager_unregister_success() {
598        let mut mgr = IrqManager::new();
599        fn dummy(_irq: IrqNumber) {}
600        mgr.register(IrqNumber::new(10), dummy).unwrap();
601        let result = mgr.unregister(IrqNumber::new(10));
602        assert!(result.is_ok());
603    }
604
605    #[test]
606    fn manager_unregister_nonexistent_returns_error() {
607        let mut mgr = IrqManager::new();
608        let result = mgr.unregister(IrqNumber::new(99));
609        assert_eq!(
610            result,
611            Err(KernelError::NotFound {
612                resource: "IRQ handler",
613                id: 99,
614            })
615        );
616    }
617
618    #[test]
619    fn manager_unregister_then_reregister() {
620        let mut mgr = IrqManager::new();
621        fn dummy(_irq: IrqNumber) {}
622        mgr.register(IrqNumber::new(3), dummy).unwrap();
623        mgr.unregister(IrqNumber::new(3)).unwrap();
624        // Should be able to register again after unregistering
625        let result = mgr.register(IrqNumber::new(3), dummy);
626        assert!(result.is_ok());
627    }
628
629    #[test]
630    fn manager_dispatch_increments_count() {
631        let mut mgr = IrqManager::new();
632        fn dummy(_irq: IrqNumber) {}
633        mgr.register(IrqNumber::new(1), dummy).unwrap();
634        assert_eq!(mgr.dispatch_count(), 0);
635        mgr.dispatch(IrqNumber::new(1));
636        assert_eq!(mgr.dispatch_count(), 1);
637        mgr.dispatch(IrqNumber::new(1));
638        mgr.dispatch(IrqNumber::new(1));
639        assert_eq!(mgr.dispatch_count(), 3);
640    }
641
642    #[test]
643    fn manager_dispatch_spurious_irq_increments_count() {
644        // Dispatching an IRQ with no handler should still increment the
645        // dispatch count (it counts total dispatches, not handled ones).
646        let mut mgr = IrqManager::new();
647        mgr.dispatch(IrqNumber::new(42));
648        assert_eq!(mgr.dispatch_count(), 1);
649    }
650
651    #[test]
652    fn manager_dispatch_calls_correct_handler() {
653        use core::sync::atomic::{AtomicU32, Ordering};
654        static CALLED_WITH: AtomicU32 = AtomicU32::new(0);
655
656        fn handler(irq: IrqNumber) {
657            CALLED_WITH.store(irq.as_u32(), Ordering::SeqCst);
658        }
659
660        let mut mgr = IrqManager::new();
661        mgr.register(IrqNumber::new(7), handler).unwrap();
662        mgr.dispatch(IrqNumber::new(7));
663        assert_eq!(CALLED_WITH.load(Ordering::SeqCst), 7);
664    }
665
666    #[test]
667    fn manager_multiple_handlers_independent() {
668        use core::sync::atomic::{AtomicU32, Ordering};
669        static HANDLER_A_COUNT: AtomicU32 = AtomicU32::new(0);
670        static HANDLER_B_COUNT: AtomicU32 = AtomicU32::new(0);
671
672        fn handler_a(_irq: IrqNumber) {
673            HANDLER_A_COUNT.fetch_add(1, Ordering::SeqCst);
674        }
675        fn handler_b(_irq: IrqNumber) {
676            HANDLER_B_COUNT.fetch_add(1, Ordering::SeqCst);
677        }
678
679        let mut mgr = IrqManager::new();
680        mgr.register(IrqNumber::new(10), handler_a).unwrap();
681        mgr.register(IrqNumber::new(20), handler_b).unwrap();
682
683        mgr.dispatch(IrqNumber::new(10));
684        mgr.dispatch(IrqNumber::new(10));
685        mgr.dispatch(IrqNumber::new(20));
686
687        assert_eq!(HANDLER_A_COUNT.load(Ordering::SeqCst), 2);
688        assert_eq!(HANDLER_B_COUNT.load(Ordering::SeqCst), 1);
689        assert_eq!(mgr.dispatch_count(), 3);
690    }
691
692    #[test]
693    fn manager_register_irq_zero() {
694        let mut mgr = IrqManager::new();
695        fn dummy(_irq: IrqNumber) {}
696        // IRQ 0 is valid (timer on x86_64)
697        let result = mgr.register(IrqNumber::new(0), dummy);
698        assert!(result.is_ok());
699    }
700}