squib_arch/sysregs.rs
1//! Curated sysreg subset (~100 registers we touch).
2//!
3//! The full ARMv8 sysreg space is enormous; squib only saves and restores what is
4//! necessary for boot, exception handling, the `V1N1` CPU template, and the GIC. New
5//! registers are added with a snapshot version bump (additive contract — never remove).
6//!
7//! See [13-arch-and-boot.md § 3](../../../specs/13-arch-and-boot.md#3-sysreg-subset).
8
9/// Sysreg enum — `#[non_exhaustive]` because the full curated list is iterative.
10#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
11#[non_exhaustive]
12pub enum SysReg {
13 // === Boot setup ===
14 /// SCTLR_EL1 — system control register, EL1.
15 SctlrEl1,
16 /// TTBR0_EL1 — translation table base, EL1.
17 Ttbr0El1,
18 /// TTBR1_EL1 — translation table base 1, EL1.
19 Ttbr1El1,
20 /// MAIR_EL1 — memory attribute indirection register.
21 MairEl1,
22 /// AMAIR_EL1 — auxiliary memory attribute indirection register.
23 AmairEl1,
24 /// TCR_EL1 — translation control register.
25 TcrEl1,
26 /// SP_EL1.
27 SpEl1,
28 /// ELR_EL1 — exception link register.
29 ElrEl1,
30 /// SPSR_EL1 — saved program status register.
31 SpsrEl1,
32 /// VBAR_EL1 — vector base address register.
33 VbarEl1,
34
35 // === ID registers (read-only) ===
36 /// ID_AA64MMFR0_EL1 — memory model feature register 0.
37 IdAa64Mmfr0El1,
38 /// ID_AA64MMFR1_EL1 — memory model feature register 1.
39 IdAa64Mmfr1El1,
40 /// ID_AA64PFR0_EL1 — processor feature register 0.
41 IdAa64Pfr0El1,
42 /// ID_AA64PFR1_EL1 — processor feature register 1.
43 IdAa64Pfr1El1,
44 /// ID_AA64DFR0_EL1 — debug feature register 0.
45 IdAa64Dfr0El1,
46 /// ID_AA64ISAR0_EL1 — instruction set attribute register 0.
47 IdAa64Isar0El1,
48 /// ID_AA64ISAR1_EL1 — instruction set attribute register 1.
49 IdAa64Isar1El1,
50 /// MPIDR_EL1 — multiprocessor affinity register (per-vCPU; identity).
51 MpidrEl1,
52
53 // === Generic timers ===
54 /// CNTV_CTL_EL0 — virtual timer control.
55 CntvCtlEl0,
56 /// CNTV_CVAL_EL0 — virtual timer compare value.
57 CntvCvalEl0,
58 /// CNTV_OFF_EL2 — virtual timer offset.
59 CntvOffEl2,
60 /// CNTFRQ_EL0 — counter frequency.
61 CntFrqEl0,
62 /// CNTKCTL_EL1 — kernel counter control.
63 CntKctlEl1,
64 /// CNTP_CTL_EL0 — physical timer control.
65 CntpCtlEl0,
66 /// CNTP_CVAL_EL0 — physical timer compare value.
67 CntpCvalEl0,
68
69 // === Performance / Pmu ===
70 /// PMCCNTR_EL0 — cycle counter.
71 PmCcntrEl0,
72 /// PMCCFILTR_EL0 — cycle counter filter.
73 PmCcfiltrEl0,
74 /// PMUSERENR_EL0 — user enable register.
75 PmUserEnrEl0,
76 /// PMCR_EL0 — PMU control.
77 PmCrEl0,
78 /// PMCNTENSET_EL0 — counter enable set.
79 PmCntEnSetEl0,
80 /// PMOVSSET_EL0 — overflow status set.
81 PmOvsSetEl0,
82 /// PMSELR_EL0 — event counter selection.
83 PmSelrEl0,
84
85 // === Exception handling ===
86 /// ESR_EL1 — exception syndrome.
87 EsrEl1,
88 /// FAR_EL1 — fault address register.
89 FarEl1,
90 /// AFSR0_EL1 — auxiliary fault status register 0.
91 Afsr0El1,
92 /// AFSR1_EL1 — auxiliary fault status register 1.
93 Afsr1El1,
94
95 // === Memory model / TLB ===
96 /// CONTEXTIDR_EL1 — context ID register.
97 ContextIdrEl1,
98 /// TPIDR_EL0 — thread pointer EL0.
99 TpidrEl0,
100 /// TPIDRRO_EL0 — thread pointer (read-only, EL0).
101 TpidrroEl0,
102 /// TPIDR_EL1 — thread pointer EL1.
103 TpidrEl1,
104 /// PAR_EL1 — physical address register.
105 ParEl1,
106
107 // === FP/SIMD ===
108 /// FPCR — FP control register.
109 Fpcr,
110 /// FPSR — FP status register.
111 Fpsr,
112 /// CPACR_EL1 — architectural feature access control.
113 CpacrEl1,
114
115 // === Debug ===
116 /// MDSCR_EL1 — monitor debug system control.
117 MdscrEl1,
118 /// OSLAR_EL1 — OS lock access register.
119 OslarEl1,
120 /// OSDLR_EL1 — OS double-lock register.
121 OsdlrEl1,
122}
123
124impl SysReg {
125 /// A stable wire-encoding for use as a `BTreeMap<u64, u64>` key in
126 /// `VcpuState::sys_regs`.
127 ///
128 /// Each variant carries an explicitly assigned wire constant — reordering [`Self::all`]
129 /// or inserting a new variant in the middle does **not** change the encoding (the
130 /// previous positional scheme silently re-keyed every register on a reorder, which
131 /// would surface as a snapshot mismatch on the *next* `restore`, not the build).
132 /// `0` is reserved for "unknown" and never assigned. Snapshot consumers must round-trip
133 /// through [`Self::from_encoded`] — the wire shape is squib-private (D6).
134 #[must_use]
135 #[allow(
136 clippy::too_many_lines,
137 reason = "one match arm per curated sysreg — splitting hides the additive contract"
138 )]
139 pub fn as_encoded(self) -> u64 {
140 match self {
141 // boot setup — block 1..=10
142 Self::SctlrEl1 => 1,
143 Self::Ttbr0El1 => 2,
144 Self::Ttbr1El1 => 3,
145 Self::MairEl1 => 4,
146 Self::AmairEl1 => 5,
147 Self::TcrEl1 => 6,
148 Self::SpEl1 => 7,
149 Self::ElrEl1 => 8,
150 Self::SpsrEl1 => 9,
151 Self::VbarEl1 => 10,
152 // ID registers — block 11..=18
153 Self::IdAa64Mmfr0El1 => 11,
154 Self::IdAa64Mmfr1El1 => 12,
155 Self::IdAa64Pfr0El1 => 13,
156 Self::IdAa64Pfr1El1 => 14,
157 Self::IdAa64Dfr0El1 => 15,
158 Self::IdAa64Isar0El1 => 16,
159 Self::IdAa64Isar1El1 => 17,
160 Self::MpidrEl1 => 18,
161 // generic timers — block 19..=25
162 Self::CntvCtlEl0 => 19,
163 Self::CntvCvalEl0 => 20,
164 Self::CntvOffEl2 => 21,
165 Self::CntFrqEl0 => 22,
166 Self::CntKctlEl1 => 23,
167 Self::CntpCtlEl0 => 24,
168 Self::CntpCvalEl0 => 25,
169 // PMU — block 26..=32
170 Self::PmCcntrEl0 => 26,
171 Self::PmCcfiltrEl0 => 27,
172 Self::PmUserEnrEl0 => 28,
173 Self::PmCrEl0 => 29,
174 Self::PmCntEnSetEl0 => 30,
175 Self::PmOvsSetEl0 => 31,
176 Self::PmSelrEl0 => 32,
177 // exceptions — block 33..=36
178 Self::EsrEl1 => 33,
179 Self::FarEl1 => 34,
180 Self::Afsr0El1 => 35,
181 Self::Afsr1El1 => 36,
182 // memory model / TLB — block 37..=41
183 Self::ContextIdrEl1 => 37,
184 Self::TpidrEl0 => 38,
185 Self::TpidrroEl0 => 39,
186 Self::TpidrEl1 => 40,
187 Self::ParEl1 => 41,
188 // FP/SIMD — block 42..=44
189 Self::Fpcr => 42,
190 Self::Fpsr => 43,
191 Self::CpacrEl1 => 44,
192 // debug — block 45..=47
193 Self::MdscrEl1 => 45,
194 Self::OslarEl1 => 46,
195 Self::OsdlrEl1 => 47,
196 }
197 }
198
199 /// Inverse of [`Self::as_encoded`]. Returns `None` for keys not in the curated
200 /// list (forward-compat: a state file from a future squib build that added
201 /// registers we don't know about surfaces as `None` and the loader rejects
202 /// with `SnapshotError::Incompatible`).
203 #[must_use]
204 #[allow(
205 clippy::too_many_lines,
206 reason = "one match arm per curated sysreg — splitting hides the additive contract"
207 )]
208 pub fn from_encoded(key: u64) -> Option<Self> {
209 Some(match key {
210 1 => Self::SctlrEl1,
211 2 => Self::Ttbr0El1,
212 3 => Self::Ttbr1El1,
213 4 => Self::MairEl1,
214 5 => Self::AmairEl1,
215 6 => Self::TcrEl1,
216 7 => Self::SpEl1,
217 8 => Self::ElrEl1,
218 9 => Self::SpsrEl1,
219 10 => Self::VbarEl1,
220 11 => Self::IdAa64Mmfr0El1,
221 12 => Self::IdAa64Mmfr1El1,
222 13 => Self::IdAa64Pfr0El1,
223 14 => Self::IdAa64Pfr1El1,
224 15 => Self::IdAa64Dfr0El1,
225 16 => Self::IdAa64Isar0El1,
226 17 => Self::IdAa64Isar1El1,
227 18 => Self::MpidrEl1,
228 19 => Self::CntvCtlEl0,
229 20 => Self::CntvCvalEl0,
230 21 => Self::CntvOffEl2,
231 22 => Self::CntFrqEl0,
232 23 => Self::CntKctlEl1,
233 24 => Self::CntpCtlEl0,
234 25 => Self::CntpCvalEl0,
235 26 => Self::PmCcntrEl0,
236 27 => Self::PmCcfiltrEl0,
237 28 => Self::PmUserEnrEl0,
238 29 => Self::PmCrEl0,
239 30 => Self::PmCntEnSetEl0,
240 31 => Self::PmOvsSetEl0,
241 32 => Self::PmSelrEl0,
242 33 => Self::EsrEl1,
243 34 => Self::FarEl1,
244 35 => Self::Afsr0El1,
245 36 => Self::Afsr1El1,
246 37 => Self::ContextIdrEl1,
247 38 => Self::TpidrEl0,
248 39 => Self::TpidrroEl0,
249 40 => Self::TpidrEl1,
250 41 => Self::ParEl1,
251 42 => Self::Fpcr,
252 43 => Self::Fpsr,
253 44 => Self::CpacrEl1,
254 45 => Self::MdscrEl1,
255 46 => Self::OslarEl1,
256 47 => Self::OsdlrEl1,
257 _ => return None,
258 })
259 }
260
261 /// All curated sysregs, in canonical order. The order is the additive contract: new
262 /// registers may be appended; never insert in the middle.
263 #[must_use]
264 pub const fn all() -> &'static [SysReg] {
265 &[
266 // boot
267 Self::SctlrEl1,
268 Self::Ttbr0El1,
269 Self::Ttbr1El1,
270 Self::MairEl1,
271 Self::AmairEl1,
272 Self::TcrEl1,
273 Self::SpEl1,
274 Self::ElrEl1,
275 Self::SpsrEl1,
276 Self::VbarEl1,
277 // id
278 Self::IdAa64Mmfr0El1,
279 Self::IdAa64Mmfr1El1,
280 Self::IdAa64Pfr0El1,
281 Self::IdAa64Pfr1El1,
282 Self::IdAa64Dfr0El1,
283 Self::IdAa64Isar0El1,
284 Self::IdAa64Isar1El1,
285 Self::MpidrEl1,
286 // timer
287 Self::CntvCtlEl0,
288 Self::CntvCvalEl0,
289 Self::CntvOffEl2,
290 Self::CntFrqEl0,
291 Self::CntKctlEl1,
292 Self::CntpCtlEl0,
293 Self::CntpCvalEl0,
294 // pmu
295 Self::PmCcntrEl0,
296 Self::PmCcfiltrEl0,
297 Self::PmUserEnrEl0,
298 Self::PmCrEl0,
299 Self::PmCntEnSetEl0,
300 Self::PmOvsSetEl0,
301 Self::PmSelrEl0,
302 // exceptions
303 Self::EsrEl1,
304 Self::FarEl1,
305 Self::Afsr0El1,
306 Self::Afsr1El1,
307 // memory model / TLB
308 Self::ContextIdrEl1,
309 Self::TpidrEl0,
310 Self::TpidrroEl0,
311 Self::TpidrEl1,
312 Self::ParEl1,
313 // FP/SIMD
314 Self::Fpcr,
315 Self::Fpsr,
316 Self::CpacrEl1,
317 // debug
318 Self::MdscrEl1,
319 Self::OslarEl1,
320 Self::OsdlrEl1,
321 ]
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn all_returns_unique_registers() {
331 let all = SysReg::all();
332 for (i, reg) in all.iter().enumerate() {
333 assert!(
334 !all[..i].contains(reg),
335 "duplicate sysreg at index {i}: {reg:?}"
336 );
337 }
338 }
339
340 #[test]
341 fn all_is_non_empty_and_below_two_hundred() {
342 let count = SysReg::all().len();
343 assert!(count >= 30, "got {count} sysregs; expected at least 30");
344 assert!(count <= 200, "got {count} sysregs; spec caps at ~100");
345 }
346
347 #[test]
348 fn additive_contract_starts_with_boot_setup() {
349 // The boot-setup block is listed first per spec § 3; SCTLR_EL1 is the canonical
350 // first entry.
351 assert_eq!(SysReg::all()[0], SysReg::SctlrEl1);
352 }
353
354 #[test]
355 fn test_should_round_trip_as_encoded_through_from_encoded() {
356 for reg in SysReg::all() {
357 let key = reg.as_encoded();
358 assert_ne!(key, 0, "encoding must be non-zero for {reg:?}");
359 assert_eq!(SysReg::from_encoded(key), Some(*reg));
360 }
361 }
362
363 #[test]
364 fn test_should_reject_zero_and_out_of_range_encoded_keys() {
365 assert_eq!(SysReg::from_encoded(0), None);
366 // The wire-encoding is hand-assigned and currently dense up to `all().len()`. A
367 // key one past the dense range is the canonical "unknown to this squib" sentinel —
368 // future variants may extend the dense block, but a value picked far beyond
369 // (a million) is guaranteed to be unrecognised by every published wire schema.
370 assert_eq!(SysReg::from_encoded(1_000_000), None);
371 let beyond = u64::try_from(SysReg::all().len()).expect("len fits in u64") + 1;
372 assert_eq!(SysReg::from_encoded(beyond), None);
373 }
374
375 #[test]
376 fn test_should_assign_distinct_wire_constants_per_variant() {
377 let mut seen = std::collections::BTreeSet::new();
378 for reg in SysReg::all() {
379 let key = reg.as_encoded();
380 assert!(
381 seen.insert(key),
382 "duplicate wire constant {key} hit twice (second hit: {reg:?})"
383 );
384 }
385 }
386}