Skip to main content

squib_arch/
gic.rs

1//! GIC interrupt-ID newtype.
2//!
3//! The FDT GICv3 cell triple is `<type intid_offset flags>`, where `intid_offset` is
4//! `INTID − 32` for SPIs and `INTID − 16` for PPIs. The "is `SPI 1` cell-1 or INTID-1?"
5//! confusion has bitten this stack at least three times in prior art; the [`IntId`]
6//! newtype below makes the conversion explicit at every call site.
7//!
8//! See [13-arch-and-boot.md §
9//! 2.1](../../../specs/13-arch-and-boot.md#21-gic-interrupt-id-conventions) and [99-key-decisions.
10//! md § D24](../../../specs/99-key-decisions.md#d24-edge-spi-pulse-shape).
11
12use thiserror::Error;
13
14/// Maximum INTID supported by GICv3 SPIs. ARM GICv3 reserves intids 32..1019 for SPIs;
15/// the in-kernel HVF GICv3 supports up to 1019.
16pub const SPI_INTID_MAX: u32 = 1019;
17
18/// PPI INTID range: 16..=31 (16 entries).
19pub const PPI_INTID_MIN: u32 = 16;
20/// PPI INTID range: 16..=31 (16 entries).
21pub const PPI_INTID_MAX: u32 = 31;
22
23/// SPI INTID range: 32..=1019.
24pub const SPI_INTID_MIN: u32 = 32;
25
26/// FDT cell `type` field for SPIs.
27pub const FDT_CELL_TYPE_SPI: u32 = 0;
28/// FDT cell `type` field for PPIs.
29pub const FDT_CELL_TYPE_PPI: u32 = 1;
30
31/// FDT cell `flags` field — edge-rising trigger.
32pub const FDT_CELL_FLAGS_EDGE_RISING: u32 = 1;
33/// FDT cell `flags` field — level-high trigger.
34pub const FDT_CELL_FLAGS_LEVEL_HIGH: u32 = 4;
35/// FDT cell `flags` field — level-low trigger.
36pub const FDT_CELL_FLAGS_LEVEL_LOW: u32 = 8;
37
38/// Trigger semantics for an interrupt line.
39#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
40pub enum Trigger {
41    /// Edge-rising; default for virtio-MMIO. FDT flag `1`.
42    EdgeRising,
43    /// Level-high; default for PL011 UART and timer PPIs. FDT flag `4`.
44    LevelHigh,
45    /// Level-low. FDT flag `8`.
46    LevelLow,
47}
48
49impl Trigger {
50    /// FDT-cell `flags` value.
51    #[must_use]
52    pub const fn fdt_flags(self) -> u32 {
53        match self {
54            Self::EdgeRising => FDT_CELL_FLAGS_EDGE_RISING,
55            Self::LevelHigh => FDT_CELL_FLAGS_LEVEL_HIGH,
56            Self::LevelLow => FDT_CELL_FLAGS_LEVEL_LOW,
57        }
58    }
59}
60
61/// Errors from constructing or converting an `IntId`.
62#[derive(Debug, Clone, Eq, PartialEq, Error)]
63pub enum IntIdError {
64    /// Provided raw INTID is outside the SPI range (`32..=1019`).
65    #[error("invalid SPI intid {0}: must be in range 32..={SPI_INTID_MAX}")]
66    SpiOutOfRange(u32),
67    /// Provided raw INTID is outside the PPI range (`16..=31`).
68    #[error("invalid PPI intid {0}: must be in range 16..=31")]
69    PpiOutOfRange(u32),
70    /// FDT SPI cell value would map to an out-of-range INTID.
71    #[error("invalid SPI cell offset {0}: would map to an INTID > {SPI_INTID_MAX}")]
72    SpiCellOffsetOutOfRange(u32),
73    /// FDT PPI cell value would map to an out-of-range INTID.
74    #[error("invalid PPI cell offset {0}: must be in range 0..=15 (cells map to INTIDs 16..=31)")]
75    PpiCellOffsetOutOfRange(u32),
76}
77
78/// A GIC interrupt identifier (raw INTID).
79///
80/// Use the named constructors to translate from FDT-cell offsets — never reach for a
81/// `u32` directly. The internal value is the **raw INTID**: SPIs are 32..=1019, PPIs
82/// are 16..=31.
83#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
84pub struct IntId(u32);
85
86impl IntId {
87    /// Construct from a raw SPI INTID (32..=1019). For values written as cell-offsets
88    /// in an FDT, prefer [`Self::from_spi_cell`].
89    ///
90    /// # Errors
91    /// [`IntIdError::SpiOutOfRange`] if `intid` is outside the SPI range.
92    pub const fn from_spi_intid(intid: u32) -> Result<Self, IntIdError> {
93        if intid < SPI_INTID_MIN || intid > SPI_INTID_MAX {
94            return Err(IntIdError::SpiOutOfRange(intid));
95        }
96        Ok(Self(intid))
97    }
98
99    /// Construct from a raw PPI INTID (16..=31).
100    ///
101    /// # Errors
102    /// [`IntIdError::PpiOutOfRange`] if `intid` is outside the PPI range.
103    pub const fn from_ppi_intid(intid: u32) -> Result<Self, IntIdError> {
104        if intid < PPI_INTID_MIN || intid > PPI_INTID_MAX {
105            return Err(IntIdError::PpiOutOfRange(intid));
106        }
107        Ok(Self(intid))
108    }
109
110    /// Construct an SPI from an FDT cell offset (raw INTID = `32 + offset`).
111    ///
112    /// Use this whenever you are reading or emitting an FDT — the cell number is what
113    /// appears in the FDT text, the raw INTID is what the kernel and HVF speak.
114    ///
115    /// # Errors
116    /// [`IntIdError::SpiCellOffsetOutOfRange`] if `32 + cell_offset > SPI_INTID_MAX`.
117    pub const fn from_spi_cell(cell_offset: u32) -> Result<Self, IntIdError> {
118        let Some(intid) = SPI_INTID_MIN.checked_add(cell_offset) else {
119            return Err(IntIdError::SpiCellOffsetOutOfRange(cell_offset));
120        };
121        if intid > SPI_INTID_MAX {
122            return Err(IntIdError::SpiCellOffsetOutOfRange(cell_offset));
123        }
124        Ok(Self(intid))
125    }
126
127    /// Construct a PPI from an FDT cell offset (raw INTID = `16 + offset`).
128    ///
129    /// # Errors
130    /// [`IntIdError::PpiCellOffsetOutOfRange`] if `cell_offset > 15`.
131    pub const fn from_ppi_cell(cell_offset: u32) -> Result<Self, IntIdError> {
132        if cell_offset > 15 {
133            return Err(IntIdError::PpiCellOffsetOutOfRange(cell_offset));
134        }
135        Ok(Self(PPI_INTID_MIN + cell_offset))
136    }
137
138    /// Raw INTID — what `hv_gic_set_spi` and friends consume.
139    #[must_use]
140    pub const fn as_raw(self) -> u32 {
141        self.0
142    }
143
144    /// `true` if this is an SPI (32..=1019).
145    #[must_use]
146    pub const fn is_spi(self) -> bool {
147        self.0 >= SPI_INTID_MIN && self.0 <= SPI_INTID_MAX
148    }
149
150    /// `true` if this is a PPI (16..=31).
151    #[must_use]
152    pub const fn is_ppi(self) -> bool {
153        self.0 >= PPI_INTID_MIN && self.0 <= PPI_INTID_MAX
154    }
155
156    /// FDT cell type field (`0` for SPI, `1` for PPI).
157    ///
158    /// # Errors
159    /// Returns the original [`IntIdError`] from the malformed branch — if `self` is
160    /// neither a valid SPI nor PPI (which the constructors disallow), this is treated
161    /// as an SPI out-of-range.
162    #[must_use]
163    pub const fn fdt_cell_type(self) -> u32 {
164        if self.is_ppi() {
165            FDT_CELL_TYPE_PPI
166        } else {
167            FDT_CELL_TYPE_SPI
168        }
169    }
170
171    /// FDT cell offset (`INTID − 32` for SPI, `INTID − 16` for PPI).
172    #[must_use]
173    pub const fn fdt_cell_offset(self) -> u32 {
174        if self.is_ppi() {
175            self.0 - PPI_INTID_MIN
176        } else {
177            self.0 - SPI_INTID_MIN
178        }
179    }
180}
181
182/// Pinned constants for the well-known interrupts the squib boot path emits.
183///
184/// Built via the unchecked private constructor and verified against the cell-offset
185/// constructors at compile time. The static asserts trip the build, not the boot,
186/// if a regression mismatches the FDT-cell ↔ raw-INTID mapping.
187pub mod fixed {
188    use super::{IntId, PPI_INTID_MIN, SPI_INTID_MIN};
189
190    /// PL011 UART → SPI cell 1, raw INTID 33.
191    pub const PL011: IntId = IntId(SPI_INTID_MIN + 1);
192    /// virtio-MMIO slot 0 → SPI cell 16, raw INTID 48.
193    pub const VIRTIO_MMIO_SLOT0: IntId = IntId(SPI_INTID_MIN + 16);
194    /// Virtual timer (CNTV) → PPI cell 11, raw INTID 27.
195    pub const CNTV: IntId = IntId(PPI_INTID_MIN + 11);
196    /// Hypervisor timer (CNTHP) → PPI cell 10, raw INTID 26.
197    pub const CNTHP: IntId = IntId(PPI_INTID_MIN + 10);
198    /// Physical timer EL1 (CNTP) → PPI cell 14, raw INTID 30.
199    pub const CNTP: IntId = IntId(PPI_INTID_MIN + 14);
200    /// Secure physical timer → PPI cell 13, raw INTID 29.
201    pub const CNTPS: IntId = IntId(PPI_INTID_MIN + 13);
202
203    // Compile-time cross-check: every constant agrees with the fallible cell-offset
204    // constructor. A drift in either direction breaks the build, not the boot.
205    const _: () = {
206        assert!(PL011.as_raw() == 33);
207        assert!(VIRTIO_MMIO_SLOT0.as_raw() == 48);
208        assert!(CNTV.as_raw() == 27);
209        assert!(CNTHP.as_raw() == 26);
210        assert!(CNTP.as_raw() == 30);
211        assert!(CNTPS.as_raw() == 29);
212    };
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn pl011_constants_match_spec() {
221        let pl011 = IntId::from_spi_cell(1).unwrap();
222        assert_eq!(pl011.as_raw(), 33);
223        assert_eq!(pl011.fdt_cell_type(), FDT_CELL_TYPE_SPI);
224        assert_eq!(pl011.fdt_cell_offset(), 1);
225        assert_eq!(fixed::PL011, pl011);
226    }
227
228    #[test]
229    fn virtio_slot_n_maps_to_intid_48_plus_n() {
230        for slot in 0..32u32 {
231            let id = IntId::from_spi_cell(16 + slot).unwrap();
232            assert_eq!(id.as_raw(), 48 + slot, "slot {slot}");
233            assert_eq!(id.fdt_cell_offset(), 16 + slot, "slot {slot}");
234        }
235    }
236
237    #[test]
238    fn timer_ppi_constants_match_spec() {
239        assert_eq!(fixed::CNTV.as_raw(), 27);
240        assert_eq!(fixed::CNTHP.as_raw(), 26);
241        assert_eq!(fixed::CNTP.as_raw(), 30);
242        assert_eq!(fixed::CNTPS.as_raw(), 29);
243    }
244
245    #[test]
246    fn ppi_round_trips_through_cell_offset() {
247        for offset in 0..=15u32 {
248            let id = IntId::from_ppi_cell(offset).unwrap();
249            assert!(id.is_ppi());
250            assert_eq!(id.fdt_cell_offset(), offset);
251            assert_eq!(id.fdt_cell_type(), FDT_CELL_TYPE_PPI);
252        }
253    }
254
255    #[test]
256    fn spi_round_trips_through_cell_offset() {
257        for offset in [0, 1, 16, 47, 100, 987] {
258            let id = IntId::from_spi_cell(offset).unwrap();
259            assert!(id.is_spi());
260            assert_eq!(id.fdt_cell_offset(), offset);
261            assert_eq!(id.fdt_cell_type(), FDT_CELL_TYPE_SPI);
262        }
263    }
264
265    #[test]
266    fn ppi_cell_offset_above_15_rejected() {
267        assert!(matches!(
268            IntId::from_ppi_cell(16),
269            Err(IntIdError::PpiCellOffsetOutOfRange(16))
270        ));
271    }
272
273    #[test]
274    fn spi_cell_offset_overflow_rejected() {
275        // 32 + 988 = 1020 > 1019.
276        assert!(matches!(
277            IntId::from_spi_cell(988),
278            Err(IntIdError::SpiCellOffsetOutOfRange(988))
279        ));
280        // 32 + u32::MAX would wrap; the constructor must use checked_add.
281        assert!(matches!(
282            IntId::from_spi_cell(u32::MAX),
283            Err(IntIdError::SpiCellOffsetOutOfRange(_))
284        ));
285    }
286
287    #[test]
288    fn raw_intid_constructors_validate_ranges() {
289        assert!(IntId::from_spi_intid(31).is_err()); // PPI range
290        assert!(IntId::from_spi_intid(32).is_ok());
291        assert!(IntId::from_spi_intid(SPI_INTID_MAX).is_ok());
292        assert!(IntId::from_spi_intid(SPI_INTID_MAX + 1).is_err());
293
294        assert!(IntId::from_ppi_intid(15).is_err());
295        assert!(IntId::from_ppi_intid(16).is_ok());
296        assert!(IntId::from_ppi_intid(31).is_ok());
297        assert!(IntId::from_ppi_intid(32).is_err()); // SPI range
298    }
299
300    #[test]
301    fn trigger_fdt_flags_match_spec() {
302        assert_eq!(Trigger::EdgeRising.fdt_flags(), 1);
303        assert_eq!(Trigger::LevelHigh.fdt_flags(), 4);
304        assert_eq!(Trigger::LevelLow.fdt_flags(), 8);
305    }
306}