// Copyright (c) 2026 Rigless Labs.
// SPDX-License-Identifier: MIT OR Apache-2.0
//! The system control space's exception half: the NVIC, the system control block's exception
//! state and its control registers, and SysTick.
//!
//! The core executes instructions and holds the registers an instruction reads: the masks, the
//! vector table's address, the fault status, the MPU's regions and the floating-point context. This
//! model holds the rest. It decides which exception the core takes next, takes the faults the core
//! raises, and answers the system control space's other registers. The core calls it, and gives
//! it the masks it holds each time, so nothing here reaches into the core.
//!
//! # The manuals
//!
//! * **ARM DDI 0403E.b**, the ARMv7-M architecture reference manual. B1.5 is the exception model,
//! B3.2 the system control space, B3.3 SysTick and B3.4 the NVIC.
//! * **100166\_0001\_04**, the Cortex-M4 technical reference manual, for what this core reports:
//! table 4-1's identification registers and section 7.3's floating-point feature registers.
//!
//! # What is modelled
//!
//! | Behaviour | Source | Test |
//! |---|---|---|
//! | Fixed priorities of Reset, NMI and HardFault, -3, -2 and -1 | B1.5.4 | `fixed_priorities_beat_any_configured_one` |
//! | Execution priority: the highest active group priority, boosted by `BASEPRI`, `PRIMASK` and `FAULTMASK`, with `PRIGROUP` applied to both | B1.5.4 `ExecutionPriority()` | `basepri_masks_by_group_priority`, `primask_and_faultmask_boost_the_execution_priority` |
//! | Priority grouping: preemption by group priority, the subpriority and then the exception number choosing among pending ones | B1.5.4 | `the_subpriority_orders_pending_exceptions_and_never_preempts` |
//! | An exception is taken only when pending, enabled and not active, and its group priority beats the execution priority | B1.5.4, B3.4.1 | `a_pending_interrupt_is_taken_only_when_enabled` |
//! | External inputs: a rising edge pends, an exception entry clears the pending state, a return with the input still high pends it again, and `ICPR` can't clear an input that's high | B3.4.1 pseudocode | `a_level_still_high_at_return_pends_again`, `icpr_cannot_clear_an_input_that_is_high`, `a_pulse_while_active_pends_once` |
//! | `ISER`, `ICER`, `ISPR`, `ICPR`, `IABR` and `IPR`, and `IPR` byte by byte | B3.4.4 to B3.4.9 | `the_nvic_registers_read_back_what_they_hold` |
//! | `STIR` pends an external interrupt by number | B3.2.26 | `stir_pends_an_interrupt_by_number` |
//! | Implemented priority bits: the low bits of every priority field and `BASEPRI` read as zero | B1.5.4, B3.4.9 | `unimplemented_priority_bits_read_as_zero` |
//! | `ICSR`: `NMIPENDSET`, `PENDSVSET`/`CLR`, `PENDSTSET`/`CLR`, `ISRPENDING`, `VECTPENDING` as the highest priority pending and enabled exception, which B3.2.4 says with no word of `PRIMASK`, `BASEPRI` or `FAULTMASK`, less what a held step hides, and `VECTACTIVE` and `RETTOBASE` from the core's `IPSR`, `RETTOBASE` set in Thread mode with nothing active | B3.2.4 | `icsr_reports_and_pends`, `icsr_reads_the_active_exception_from_ipsr` |
//! | `AIRCR`: the key, `PRIGROUP`, `SYSRESETREQ` asking for a reset, and the key read back | B3.2.6 | `aircr_needs_its_key_and_asks_for_a_reset` |
//! | `SCR`, `SHPR1` to `SHPR3` byte by byte, `SHCSR`'s enables, active and pending bits | B3.2.7, B3.2.10 to B3.2.13 | `the_system_handler_registers_hold_their_exceptions` |
//! | `HFSR`'s `FORCED`, `VECTTBL` and `DEBUGEVT`, cleared by writing one | B3.2.16 | `escalation_sets_forced_and_writing_one_clears_it` |
//! | Escalation: a disabled configurable fault, or a synchronous one that can't preempt, becomes HardFault, and a HardFault that can't preempt locks the core up | B1.5.4 "Priority escalation", B1.5.15 | `a_fault_that_cannot_preempt_escalates_or_locks_up` |
//! | A fault while stacking, and a fault reading the vector table, against the exception being entered, a stacking fault taken in its place only with a better group priority | B1.5.11 | `a_stacking_fault_that_beats_the_entered_exception_replaces_it`, `a_stacking_fault_of_the_entered_exceptions_group_waits_for_it` |
//! | Locked up, only NMI is taken | B1.5.15 | `locked_up_only_nmi_is_taken` |
//! | `WFI` wakes on an exception that would preempt if `PRIMASK` were clear | B1.5.19 | `wfi_wakes_regardless_of_primask` |
//! | `DEMCR.MON_PEND` pends DebugMonitor whatever `MON_EN` says, and reads its pending state | C1.6.5 | `mon_pend_is_debugmonitors_pending_state` |
//! | `ICTR`, `ACTLR` with the bits 100166 section 4.2 has, `DEMCR`, the identification registers of 100166 table 4-1, `MVFR0` and `MVFR1`, and the SCS's peripheral and component identification, 100166 table 8-3 | B3.2.24, B3.2.25, C1.6.5, 100166 table 4-1, section 7.3 and table 8-3 | `the_identification_registers_say_what_this_core_is` |
//! | SysTick: counts down at the core clock or the reference clock, reloads from `RVR`, sets `COUNTFLAG` and pends its exception counting from 1 to 0, `COUNTFLAG` clears on a read and on any write of `CVR` but not on a debugger's read, a reload of zero holds the counter at zero once it gets there, and `CALIB` reports the reference clock's ten milliseconds | B3.3.1 to B3.3.6 | `systick_counts_reloads_and_pends`, `systick_with_a_reload_of_zero_reaches_zero_once_and_stays`, `systick_calibration_reports_ten_milliseconds` |
//! | The runtime's debugger steps with PendSV, SysTick and the external interrupts held off, as `C_MASKINTS` holds them | C1.6.2 | `a_held_step_takes_only_what_c_maskints_leaves` |
//! | An exception going from not pending to pending is reported once, however it pends, for the core's `SEVONPEND` | B1.5.18, B3.2.7 | `an_exception_going_pending_is_reported_once` |
//! | Reset puts every register here back, except that `DEMCR` keeps all but its DebugMonitor bits, its vector catch among them, and `DFSR` keeps its own, as a Local reset does | B1.5.5, C1.6.1, C1.6.5 | `a_reset_forgets_all_but_demcrs_trace_enable_and_vector_catch` |
//! | `DEMCR`'s vector catch bits, `VC_*`, which the core reads while halting debug is on | C1.5.2, C1.6.5 | `exception`'s `each_fault_is_caught_by_the_bits_c1_6_5_gives_it` |
//! | `DFSR`, which says what debug events halted the core, and a write of one clears a bit | C1.6.1 | `family`'s `dfsr_says_what_halted_the_core_and_clears_by_writing_one` |
//! | `DHCSR` read: zero with halting debug off, `C_DEBUGEN` alone to the firmware running, `C_HALT`, `S_HALT` and `S_REGRDY` while the core is in Debug state, `C_STEP` and `C_MASKINTS` through a step, and `S_SLEEP` or `S_LOCKUP` while it runs | C1.6.2 | `family`'s `dhcsr_says_whether_the_core_runs_halts_or_steps` |
//!
//! **What the model leaves out, and why.**
//!
//! | Behaviour | Source | Reason |
//! |---|---|---|
//! | The Security Extension, `NSACR` and the banked registers | DDI 0403E.b is v7-M | v7-M has none |
//! | Halting debug: writes to `DHCSR`, its `S_RESET_ST`, `S_RETIRE_ST` and `C_SNAPSTALL`, `DCRSR`, `DCRDR`, `ISRPREEMPT` and `VECTRESET` | C1.6 | The runtime's own debugger stops the core from outside, and reads its registers and the run's progress without a register transfer |
//! | `VECTCLRACTIVE` | B3.2.6 | Only valid in debug state, which the core never enters |
//! | `DEMCR.MON_STEP` | C1.6.5 | Stored and read back, and DebugMonitor isn't taken after each instruction. The runtime's own debugger steps the core |
use serde::{Deserialize, Serialize};
use super::registers::{
actlr, aircr, demcr, fpccr, get, hfsr, icsr, is_set, mask, scr, set, shcsr, stir, syst_calib,
syst_csr, syst_rvr, xpsr,
};
use crate::soc::FieldSpec;
use crate::time::VirtualNanos;
/// Exception numbers B1.5.2 fixes.
pub(crate) mod exception {
/// Reset.
pub(crate) const RESET: u16 = 1;
/// The non-maskable interrupt.
pub(crate) const NMI: u16 = 2;
/// HardFault.
pub(crate) const HARD_FAULT: u16 = 3;
/// MemManage.
pub(crate) const MEM_MANAGE: u16 = 4;
/// BusFault.
pub(crate) const BUS_FAULT: u16 = 5;
/// UsageFault.
pub(crate) const USAGE_FAULT: u16 = 6;
/// SVCall.
pub(crate) const SV_CALL: u16 = 11;
/// DebugMonitor.
pub(crate) const DEBUG_MONITOR: u16 = 12;
/// PendSV.
pub(crate) const PEND_SV: u16 = 14;
/// SysTick.
pub(crate) const SYS_TICK: u16 = 15;
/// The first external interrupt.
pub(crate) const FIRST_INTERRUPT: u16 = 16;
}
use exception::{
BUS_FAULT, DEBUG_MONITOR, FIRST_INTERRUPT, HARD_FAULT, MEM_MANAGE, NMI, PEND_SV, RESET,
SV_CALL, SYS_TICK, USAGE_FAULT,
};
/// The base of the system control space, B3.2.
pub(crate) const SCS_BASE: u64 = 0xE000_E000;
/// Its size: the 4 KB page B3.2 gives it.
pub(crate) const SCS_SIZE: u64 = 0x1000;
/// The execution priority of Thread mode with nothing active and nothing boosting it, B1.5.4.
const BASE_PRIORITY: i16 = 256;
/// The masks the core holds, B1.4.3, as it reports them when the controller decides.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[expect(
clippy::struct_excessive_bools,
reason = "each is one mask bit the core holds"
)]
pub(crate) struct Masks {
/// `PRIMASK<0>`.
pub(crate) primask: bool,
/// `FAULTMASK<0>`.
pub(crate) faultmask: bool,
/// The core is locked up, so only NMI is taken, B1.5.15.
pub(crate) locked_up: bool,
/// A debugger's step holds off PendSV, SysTick and the external interrupts, as
/// `DHCSR.C_MASKINTS` does, C1.6.2.
pub(crate) held: bool,
}
/// What became of a fault the core raised.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Raised {
/// Pending, perhaps escalated to HardFault, to be taken by the usual rules.
Pending,
/// Nothing can take it, and the core locks up.
Lockup,
/// Taken in place of the exception being entered, on that exception's stack frame.
Replaced,
}
/// What became of a fault while the core preserved its floating-point state lazily.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Preserved {
/// Pending, taken by the usual rules once it can be.
Deferred,
/// Taken now.
Taken,
/// Nothing can take it, and the core locks up.
Lockup,
}
/// What a write to the system control space asks of the core besides the model's own state.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct Asked {
/// `AIRCR.SYSRESETREQ`: reset the part, B3.2.6.
pub(crate) reset: bool,
/// `SCR` changed: the core's `SLEEPONEXIT` and `SEVONPEND`, B3.2.7.
pub(crate) sleep: Option<Sleep>,
}
/// The two bits of `SCR` the core acts on.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Sleep {
/// `SLEEPONEXIT`.
pub(crate) on_exit: bool,
/// `SEVONPEND`.
pub(crate) event_on_pending: bool,
}
/// One exception's state.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
#[expect(
clippy::struct_excessive_bools,
reason = "the NVIC keeps each of these as a bit of its own, B3.4"
)]
struct Exception {
enabled: bool,
pending: bool,
active: bool,
/// The external input is high. Only external interrupts have one.
asserted: bool,
/// The configured priority, its unimplemented bits clear. Unused for the fixed ones.
priority: u8,
}
impl Exception {
/// Makes it pending, and answers whether it wasn't already.
fn pend(&mut self) -> bool {
!std::mem::replace(&mut self.pending, true)
}
}
/// The system control space's exception half of one ARMv7-M core.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Scs {
/// By exception number, from 0 to the last external interrupt.
exceptions: Vec<Exception>,
/// Active exceptions, in the order they became active.
active: Vec<u16>,
/// The implemented bits of a priority field, B1.5.4.
priority_mask: u8,
/// `AIRCR.PRIGROUP`.
prigroup: u8,
/// `BASEPRI`, as the core last wrote it.
basepri: u8,
/// `HFSR.FORCED`, `VECTTBL` and `DEBUGEVT`, B3.2.16.
hfsr: u32,
scr: u32,
actlr: u32,
demcr: u32,
/// The debug events that halted the core since a debugger last cleared them, C1.6.1.
dfsr: u32,
/// `AIRCR.ENDIANNESS`, which the part's wiring fixes, B3.2.6.
endian: crate::soc::Endian,
systick: SysTick,
/// An exception went from not pending to pending since the core last asked, which is an
/// event for `WFE` while `SCR.SEVONPEND` is set, B1.5.18. The core asks after every call that
/// can pend one, so a snapshot never holds it.
#[serde(skip)]
pended: bool,
/// Moves on with every call that can change what preempts, so the core asks again only after
/// one. A snapshot never holds it, since a core loading one asks afresh.
#[serde(skip)]
generation: u64,
}
/// `DEMCR`'s bits related to DebugMonitor, the only ones a Local reset clears, C1.6.5.
const DEMCR_DEBUG_MONITOR: u32 =
mask(demcr::MON_EN) | mask(demcr::MON_PEND) | mask(demcr::MON_STEP) | mask(demcr::MON_REQ);
/// `DEMCR`'s vector catch bits, which the core reads only while halting debug is on, C1.6.5.
const DEMCR_VECTOR_CATCH: u32 = mask(demcr::VC_HARDERR)
| mask(demcr::VC_INTERR)
| mask(demcr::VC_BUSERR)
| mask(demcr::VC_STATERR)
| mask(demcr::VC_CHKERR)
| mask(demcr::VC_NOCPERR)
| mask(demcr::VC_MMERR)
| mask(demcr::VC_CORERESET);
/// The bits of `DEMCR` v7-M has, C1.6.5: the vector catch bits, `MON_EN`, `MON_PEND`, `MON_STEP`,
/// `MON_REQ` and `TRCENA`.
const DEMCR_WRITABLE: u32 = DEMCR_VECTOR_CATCH | DEMCR_DEBUG_MONITOR | mask(demcr::TRCENA);
impl Scs {
/// The space of a core with `lines` external interrupts, `priority_bits` implemented bits in
/// each priority field, and SysTick counting the core clock at `clock_hz` or the reference clock
/// at `clock_hz / refclk_divider`.
#[must_use]
pub(crate) fn new(lines: u16, priority_bits: u8, clock_hz: u64, refclk_divider: u32) -> Self {
let mut scs = Self {
exceptions: vec![Exception::default(); usize::from(FIRST_INTERRUPT + lines)],
active: Vec::new(),
priority_mask: !0xFF_u8.checked_shr(u32::from(priority_bits)).unwrap_or(0),
prigroup: 0,
basepri: 0,
hfsr: 0,
scr: 0,
actlr: 0,
demcr: 0,
dfsr: 0,
endian: crate::soc::Endian::Little,
systick: SysTick::new(clock_hz, refclk_divider),
pended: false,
generation: 0,
};
scs.reset();
scs
}
/// The same controller for a part whose data is `endian`, which `AIRCR.ENDIANNESS` reports,
/// A3.3.1 and B3.2.6.
#[must_use]
pub(crate) fn endian(mut self, endian: crate::soc::Endian) -> Self {
self.endian = endian;
self
}
/// Puts every register and every exception back as a Local reset leaves them, B1.5.5, and
/// forgets every input's level: what drives them is reset too. Of `DEMCR` only the bits for
/// DebugMonitor go, C1.6.5, and `DFSR` stays as it is, C1.6.1.
pub(crate) fn reset(&mut self) {
self.generation = self.generation.wrapping_add(1);
for (number, exception) in self.exceptions.iter_mut().enumerate() {
*exception = Exception {
enabled: always_enabled(u16::try_from(number).expect("a handful")),
..Exception::default()
};
}
self.active.clear();
self.prigroup = 0;
self.basepri = 0;
self.hfsr = 0;
self.scr = 0;
self.actlr = 0;
self.demcr &= !DEMCR_DEBUG_MONITOR;
self.systick.reset();
}
/// Whether this, put back from a snapshot, can take the place of `live`: the same machine's
/// table, priority bits and clocks, every active exception inside the table, and a priority
/// grouping of three bits, B3.2.6.
#[must_use]
pub(crate) fn can_replace(&self, live: &Self) -> bool {
self.exceptions.len() == live.exceptions.len()
&& self.priority_mask == live.priority_mask
&& self.systick.clock_hz == live.systick.clock_hz
&& self.systick.refclk_divider == live.systick.refclk_divider
&& self
.active
.iter()
.all(|&number| usize::from(number) < self.exceptions.len())
&& self.prigroup <= 7
}
/// The external interrupts this space has.
#[must_use]
pub(crate) fn lines(&self) -> u16 {
u16::try_from(self.exceptions.len()).expect("checked at construction") - FIRST_INTERRUPT
}
/// Drives external interrupt `line`'s input, B3.4.1: a rising edge pends it.
pub(crate) fn set_line(&mut self, line: u16, high: bool) {
let Some(exception) = self.exceptions.get_mut(usize::from(FIRST_INTERRUPT + line)) else {
return;
};
// The level alone changes nothing that preempts or wakes until the exception returns,
// which looks at it then.
if high && !exception.asserted && exception.pend() {
self.pended = true;
self.generation = self.generation.wrapping_add(1);
}
exception.asserted = high;
}
/// Pends `number`, as software does through `ISPR`, `STIR` or `ICSR`.
pub(crate) fn pend(&mut self, number: u16) {
self.generation = self.generation.wrapping_add(1);
if let Some(exception) = self.exceptions.get_mut(usize::from(number)) {
self.pended |= exception.pend();
}
}
/// Whether an exception went from not pending to pending since this last answered, B1.5.18.
pub(crate) fn take_pended(&mut self) -> bool {
std::mem::take(&mut self.pended)
}
/// `BASEPRI`, which the core writes and the core holds, B1.4.3.
pub(crate) fn write_basepri(&mut self, value: u8) {
self.generation = self.generation.wrapping_add(1);
self.basepri = value & self.priority_mask;
}
/// The pending exception the core would take next, if it can preempt now: the one its interrupt
/// input should say is there.
#[must_use]
pub(crate) fn preempting(&self, masks: Masks) -> Option<u16> {
let best = self.best_pending(masks)?;
(self.group(best) < self.execution_priority(masks, false, None)).then_some(best)
}
/// Whether a core waiting in `WFI` wakes: a pending exception would preempt if `PRIMASK` were
/// clear, B1.5.19.
#[must_use]
pub(crate) fn wakes(&self, masks: Masks) -> bool {
self.best_pending(masks)
.is_some_and(|best| self.group(best) < self.execution_priority(masks, true, None))
}
/// A count that moves on whenever this changes in a way that can change what preempts or
/// what wakes the core.
#[must_use]
pub(crate) fn generation(&self) -> u64 {
self.generation
}
/// `BASEPRI`, as the core last wrote it.
#[must_use]
pub(crate) fn basepri(&self) -> u8 {
self.basepri
}
/// The core takes the exception that preempts, if one still does: it becomes active and stops
/// being pending, B3.4.1's `ExceptionIN`.
pub(crate) fn acknowledge(&mut self, masks: Masks) -> Option<u16> {
let number = self.preempting(masks)?;
self.generation = self.generation.wrapping_add(1);
let exception = &mut self.exceptions[usize::from(number)];
exception.active = true;
exception.pending = false;
self.active.push(number);
Some(number)
}
/// The core returned from `number`, B1.5.8: it stops being active, and an external input still
/// high pends it again, B3.4.1's `ExceptionOUT`. Answers whether it was active, which the core
/// faults on when it wasn't.
pub(crate) fn complete(&mut self, number: u16) -> bool {
let Some(exception) = self.exceptions.get_mut(usize::from(number)) else {
return false;
};
if !exception.active {
return false;
}
self.generation = self.generation.wrapping_add(1);
exception.active = false;
if exception.asserted {
self.pended |= exception.pend();
}
if let Some(position) = self.active.iter().rposition(|&active| active == number) {
self.active.remove(position);
}
true
}
/// A synchronous fault the core raised, or an `SVC`, or a `BKPT` for DebugMonitor: pending as
/// it is, escalated to HardFault when it's disabled or can't preempt, or a lockup when HardFault
/// can't preempt either. B1.5.4 "Priority escalation".
pub(crate) fn raise(&mut self, number: u16, masks: Masks) -> Raised {
self.generation = self.generation.wrapping_add(1);
if !self.escalates(number, masks, None) {
self.pend(number);
return Raised::Pending;
}
if !self.can_become_active(HARD_FAULT, masks, None) {
return Raised::Lockup;
}
self.note_escalation(number);
self.pend(HARD_FAULT);
Raised::Pending
}
/// A fault while the core stacked the frame for `entering`, which it has already acknowledged,
/// B1.5.11: escalated by the priority `entering` had before it was taken, and taken in its place
/// only when its group priority beats `entering`'s, as `DerivedLateArrival()` has it.
pub(crate) fn raise_while_stacking(
&mut self,
number: u16,
entering: u16,
masks: Masks,
) -> Raised {
self.generation = self.generation.wrapping_add(1);
let mut derived = number;
if self.escalates(number, masks, Some(entering)) {
derived = HARD_FAULT;
if !self.can_become_active(HARD_FAULT, masks, Some(entering)) {
return Raised::Lockup;
}
self.note_escalation(number);
}
if self.group(derived) < self.group(entering) {
self.unacknowledge(entering);
self.pend(derived);
return Raised::Replaced;
}
self.pend(derived);
Raised::Pending
}
/// A fault reading `entering`'s vector, B1.5.11: HardFault with `HFSR.VECTTBL`, in place of
/// `entering`.
pub(crate) fn raise_reading_vector(&mut self, entering: u16, masks: Masks) -> Raised {
self.generation = self.generation.wrapping_add(1);
self.hfsr |= mask(hfsr::VECTTBL);
if !self.can_become_active(HARD_FAULT, masks, None) {
return Raised::Lockup;
}
self.unacknowledge(entering);
self.pend(HARD_FAULT);
Raised::Replaced
}
/// `FPCCR`'s ready bits when the core allocates a floating-point frame for `entering`, B3.2.21:
/// whether HardFault, MemManage, BusFault and DebugMonitor could preempt the context that owns
/// the state.
#[must_use]
pub(crate) fn fpccr_ready(&self, entering: u16, masks: Masks) -> u32 {
let ignoring = (entering != 0 && self.active.last() == Some(&entering)).then_some(entering);
let ready = |number: u16| self.can_become_active(number, masks, ignoring);
let mut bits = 0;
if self.execution_priority(masks, false, ignoring) > -1 {
bits |= mask(fpccr::HFRDY);
}
if ready(MEM_MANAGE) {
bits |= mask(fpccr::MMRDY);
}
if ready(BUS_FAULT) {
bits |= mask(fpccr::BFRDY);
}
if is_set(demcr::MON_EN, self.demcr) && ready(DEBUG_MONITOR) {
bits |= mask(fpccr::MONRDY);
}
bits
}
/// A fault while the core preserved its floating-point state lazily, judged by the ready bits
/// `fpccr` saved when the frame was allocated, B3.2.21.
pub(crate) fn raise_while_preserving(
&mut self,
number: u16,
fpccr: u32,
masks: Masks,
) -> Preserved {
self.generation = self.generation.wrapping_add(1);
let ready = match number {
MEM_MANAGE => is_set(fpccr::MMRDY, fpccr),
BUS_FAULT => is_set(fpccr::BFRDY, fpccr),
_ => false,
};
let target = if ready { number } else { HARD_FAULT };
// B1.6.3's TakePreserveFPException() records the escalation before it decides to lock up.
if !ready {
self.note_escalation(number);
}
let now = self.can_become_active(target, masks, None);
if !now && !is_set(fpccr::HFRDY, fpccr) {
return Preserved::Lockup;
}
self.pend(target);
if now {
Preserved::Taken
} else {
Preserved::Deferred
}
}
/// The exception with the lowest number that's pending, enabled, not active and of the highest
/// priority, B1.5.4, which the core's next exception starts from. A core that's `locked_up` has
/// only NMI to take.
fn best_pending(&self, masks: Masks) -> Option<u16> {
self.best(|number, exception| {
!exception.active
&& (!masks.locked_up || number == NMI)
&& (!masks.held || !matches!(number, PEND_SV | SYS_TICK | FIRST_INTERRUPT..))
})
}
/// The exception with the lowest number that's pending, enabled, of the highest priority and
/// one `counts` takes. `VECTPENDING` counts an active one too, B3.2.4.
fn best(&self, counts: impl Fn(u16, &Exception) -> bool) -> Option<u16> {
let mut best: Option<u16> = None;
for (number, exception) in self.exceptions.iter().enumerate().skip(1) {
let number = u16::try_from(number).expect("a handful");
if !(exception.pending && exception.enabled && counts(number, exception)) {
continue;
}
if best.is_none_or(|best| self.priority(number) < self.priority(best)) {
best = Some(number);
}
}
best
}
/// An exception's full priority, B1.5.4.
fn priority(&self, number: u16) -> i16 {
match number {
RESET => -3,
NMI => -2,
HARD_FAULT => -1,
_ => i16::from(self.exceptions[usize::from(number)].priority),
}
}
/// An exception's group priority, `PRIGROUP` applied to a configurable one, B1.5.4.
fn group(&self, number: u16) -> i16 {
self.grouped(self.priority(number))
}
fn grouped(&self, priority: i16) -> i16 {
if priority < 0 {
return priority;
}
let group = 2_i16 << self.prigroup;
priority - priority % group
}
/// B1.5.4's `ExecutionPriority()`, leaving out `PRIMASK` for `WFI` and one active exception for
/// a fault raised while it's being entered.
pub(crate) fn execution_priority(
&self,
masks: Masks,
ignore_primask: bool,
ignoring: Option<u16>,
) -> i16 {
let mut highest = BASE_PRIORITY;
let mut skipped = false;
for &number in &self.active {
if !skipped && ignoring == Some(number) {
skipped = true;
continue;
}
highest = highest.min(self.group(number));
}
let mut boosted = BASE_PRIORITY;
if self.basepri != 0 {
boosted = self.grouped(i16::from(self.basepri));
}
if masks.primask && !ignore_primask {
boosted = 0;
}
if masks.faultmask {
boosted = -1;
}
boosted.min(highest)
}
/// Whether `number` would be taken now if it were pending: enabled, not active, and its group
/// priority beating the execution priority.
fn can_become_active(&self, number: u16, masks: Masks, ignoring: Option<u16>) -> bool {
let exception = &self.exceptions[usize::from(number)];
let active = exception.active && ignoring != Some(number);
exception.enabled
&& !active
&& self.group(number) < self.execution_priority(masks, false, ignoring)
}
/// Whether a fault `number` raised now becomes HardFault: a configurable fault that's
/// disabled, or one that can't preempt, B1.5.4 "Priority escalation".
fn escalates(&self, number: u16, masks: Masks, ignoring: Option<u16>) -> bool {
number != HARD_FAULT && !self.can_become_active(number, masks, ignoring)
}
/// `HFSR`, B3.2.16.
pub(crate) fn hfsr(&self) -> u32 {
self.hfsr
}
/// Whether `DEMCR.TRCENA` turns the DWT and the ITM on, C1.6.5.
pub(crate) fn tracing(&self) -> bool {
is_set(demcr::TRCENA, self.demcr)
}
/// Whether `catch`, one of `DEMCR`'s vector catch bits, is set, C1.6.5.
pub(crate) fn catches(&self, catch: &FieldSpec) -> bool {
is_set(catch, self.demcr)
}
/// `DEMCR`'s vector catch bits, the rest of it as it is.
pub(crate) fn set_vector_catch(&mut self, catches: u32) {
self.demcr = self.demcr & !DEMCR_VECTOR_CATCH | catches & DEMCR_VECTOR_CATCH;
}
/// Notes in `DFSR` the debug events `events`, as its bits, that halted the core, C1.6.1, and
/// answers whether it holds one it didn't.
pub(crate) fn halted_by(&mut self, events: u32) -> bool {
let was = self.dfsr;
self.dfsr |= events;
self.dfsr != was
}
/// `HFSR` says why a fault became HardFault: `DEBUGEVT` for a `BKPT`, `FORCED` otherwise.
fn note_escalation(&mut self, number: u16) {
self.hfsr |= if number == DEBUG_MONITOR {
mask(hfsr::DEBUGEVT)
} else {
mask(hfsr::FORCED)
};
}
/// Undoes the acknowledgement of `number`, which a fault took the place of before its handler ran.
fn unacknowledge(&mut self, number: u16) {
if self.active.last() == Some(&number) {
self.active.pop();
let exception = &mut self.exceptions[usize::from(number)];
exception.active = false;
self.pended |= exception.pend();
}
}
/// [`Scs::read`] as a debugger reads, which changes nothing, B3.3.3.
#[must_use]
pub(crate) fn peek(&self, offset: u64, now: VirtualNanos) -> Option<u32> {
self.clone().read(offset, now)
}
/// Reads the register at `offset` into the space, for the registers this model holds. `None`
/// is a register the core holds or none at all.
#[must_use]
pub(crate) fn read(&mut self, offset: u64, now: VirtualNanos) -> Option<u32> {
let value = match offset {
0x004 => u32::from(self.lines().div_ceil(32).saturating_sub(1)),
0x008 => self.actlr,
0x010..=0x01F => self.systick.read(offset, now),
0x100..=0x13F => self.bits(offset - 0x100, |e| e.enabled),
0x180..=0x1BF => self.bits(offset - 0x180, |e| e.enabled),
0x200..=0x23F => self.bits(offset - 0x200, |e| e.pending),
0x280..=0x2BF => self.bits(offset - 0x280, |e| e.pending),
0x300..=0x33F => self.bits(offset - 0x300, |e| e.active),
0x400..=0x5EF => self.priority_word(FIRST_INTERRUPT, offset - 0x400),
0xD0C => {
let big = u32::from(self.endian == crate::soc::Endian::Big);
set(aircr::VECTKEYSTAT, 0, 0xFA05)
| set(aircr::ENDIANNESS, 0, big)
| set(aircr::PRIGROUP, 0, u32::from(self.prigroup))
}
0xD10 => self.scr,
0xD18..=0xD23 => self.priority_word(4, offset - 0xD18),
0xD24 => self.shcsr(),
0xD2C => self.hfsr,
0xD30 => self.dfsr,
0xD3C | 0xDF0..=0xDF8 | 0xFD4..=0xFDF => 0,
0xD40..=0xD73 => ID_REGISTERS[usize::try_from((offset - 0xD40) / 4).expect("small")],
0xDFC => {
let pending = self.exceptions[usize::from(DEBUG_MONITOR)].pending;
self.demcr | if pending { mask(demcr::MON_PEND) } else { 0 }
}
0xF40 => 0x1011_0021,
0xF44 => 0x1100_0011,
0xFD0 => PID4,
0xFE0..=0xFFF => {
u32::from(COMPONENT_ID[usize::try_from((offset - 0xFE0) / 4).expect("small")])
}
_ => return None,
};
Some(value)
}
/// Writes the register at `offset`, for the registers this model holds, and answers what the
/// write asks of the core. A register it doesn't hold is ignored.
pub(crate) fn write(&mut self, offset: u64, value: u32, now: VirtualNanos) -> Asked {
self.generation = self.generation.wrapping_add(1);
let mut asked = Asked::default();
match offset {
0x008 => self.actlr = value & ACTLR_WRITABLE,
0x010..=0x01F => self.systick.write(offset, value, now),
0x100..=0x13F => self.set_bits(offset - 0x100, value, |e| e.enabled = true),
0x180..=0x1BF => self.set_bits(offset - 0x180, value, |e| e.enabled = false),
0x200..=0x23F => {
let mut pended = false;
self.set_bits(offset - 0x200, value, |e| pended |= e.pend());
self.pended |= pended;
}
0x280..=0x2BF => self.set_bits(offset - 0x280, value, |e| {
// B3.4.1: a write of ICPR clears the pending state only with the input low.
if !e.asserted {
e.pending = false;
}
}),
0xD04 => self.write_icsr(value),
0xD0C if get(aircr::VECTKEY, value) == 0x05FA => {
self.prigroup = u8::try_from(get(aircr::PRIGROUP, value)).expect("three bits");
asked.reset = is_set(aircr::SYSRESETREQ, value);
}
0xD10 => {
self.scr =
value & (mask(scr::SEVONPEND) | mask(scr::SLEEPDEEP) | mask(scr::SLEEPONEXIT));
asked.sleep = Some(Sleep {
on_exit: is_set(scr::SLEEPONEXIT, value),
event_on_pending: is_set(scr::SEVONPEND, value),
});
}
0xD24 => self.write_shcsr(value),
0xD2C => self.hfsr &= !value,
0xD30 => self.dfsr &= !value,
0xDFC => {
// C1.6.5: MON_PEND is DebugMonitor's pending state, whatever MON_EN says.
self.demcr = value & DEMCR_WRITABLE & !mask(demcr::MON_PEND);
if is_set(demcr::MON_PEND, value) {
self.pend(DEBUG_MONITOR);
} else {
self.exceptions[usize::from(DEBUG_MONITOR)].pending = false;
}
}
0xF00 => {
let line = u16::try_from(get(stir::INTID, value)).expect("nine bits");
if line < self.lines() {
self.pend(FIRST_INTERRUPT + line);
}
}
_ => {}
}
asked
}
/// Writes one byte of a byte-addressable priority register, `IPR` or `SHPR`, B3.2.9 and B3.4.9.
/// Answers whether `offset` is one.
pub(crate) fn write_priority_byte(&mut self, offset: u64, value: u8) -> bool {
self.generation = self.generation.wrapping_add(1);
let number = match offset {
0x400..=0x5EF => FIRST_INTERRUPT + u16::try_from(offset - 0x400).expect("small"),
0xD18..=0xD23 => 4 + u16::try_from(offset - 0xD18).expect("small"),
_ => return false,
};
if let Some(exception) = self.exceptions.get_mut(usize::from(number))
&& configurable(number)
{
exception.priority = value & self.priority_mask;
}
true
}
/// The next instant SysTick counts to zero, if it runs.
#[must_use]
pub(crate) fn next_event(&self) -> Option<VirtualNanos> {
self.systick.next_zero()
}
/// SysTick counts to zero at `now`: `COUNTFLAG`, and its exception with `TICKINT`, B3.3.1.
pub(crate) fn on_event(&mut self, now: VirtualNanos) {
self.generation = self.generation.wrapping_add(1);
if self.systick.reach(now) {
self.pend(SYS_TICK);
}
}
/// 32 exceptions' worth of one bit, from the external interrupt `offset` bytes into a bank.
fn bits(&self, offset: u64, bit: impl Fn(&Exception) -> bool) -> u32 {
let first = usize::from(FIRST_INTERRUPT) + usize::try_from(offset / 4 * 32).expect("small");
(0..32).fold(0, |word, index| {
let set = self.exceptions.get(first + index).is_some_and(&bit);
word | (u32::from(set) << index)
})
}
fn set_bits(&mut self, offset: u64, value: u32, mut apply: impl FnMut(&mut Exception)) {
let first = usize::from(FIRST_INTERRUPT) + usize::try_from(offset / 4 * 32).expect("small");
for index in 0..32 {
if value & (1 << index) != 0
&& let Some(exception) = self.exceptions.get_mut(first + index)
{
apply(exception);
}
}
}
/// Four priority bytes from exception `first` plus `offset`, word-aligned.
fn priority_word(&self, first: u16, offset: u64) -> u32 {
let base = first + u16::try_from(offset & !3).expect("small");
(0..4).fold(0, |word, byte| {
let priority = self
.exceptions
.get(usize::from(base + byte))
.filter(|_| configurable(base + byte))
.map_or(0, |exception| exception.priority);
word | u32::from(priority) << (8 * byte)
})
}
/// `ICSR` with the core in the exception `ipsr` holds, B3.2.4: `VECTACTIVE` is `IPSR<8:0>`,
/// whatever order the exceptions went active in, and `RETTOBASE` says nothing else is active.
/// B3.2.4 leaves `RETTOBASE` UNKNOWN in Thread mode, so it reads by the Handler mode rule with
/// `IPSR` 0, set when nothing's active.
pub(crate) fn icsr(&self, ipsr: u32, held: bool) -> u32 {
let current = get(xpsr::EXCEPTION_NUMBER, ipsr);
let alone = self
.active
.iter()
.all(|&number| u32::from(number) == current);
// B3.2.4: a step holding interrupts off hides them here too.
let pending = self
.best(|number, _| !held || !matches!(number, PEND_SV | SYS_TICK | FIRST_INTERRUPT..))
.unwrap_or(0);
let external = self.exceptions[usize::from(FIRST_INTERRUPT)..]
.iter()
.any(|exception| exception.pending);
let state = |number: u16| u32::from(self.exceptions[usize::from(number)].pending);
set(icsr::NMIPENDSET, 0, state(NMI))
| set(icsr::PENDSVSET, 0, state(PEND_SV))
| set(icsr::PENDSTSET, 0, state(SYS_TICK))
| set(icsr::ISRPENDING, 0, u32::from(external))
| set(icsr::VECTPENDING, 0, u32::from(pending))
| set(icsr::RETTOBASE, 0, u32::from(alone))
| set(icsr::VECTACTIVE, 0, current)
}
/// How many exceptions are active, B1.5.8's `ExceptionActiveBitCount()`.
pub(crate) fn active_count(&self) -> usize {
self.active.len()
}
fn write_icsr(&mut self, value: u32) {
if is_set(icsr::NMIPENDSET, value) {
self.pend(NMI);
}
if is_set(icsr::PENDSVSET, value) {
self.pend(PEND_SV);
} else if is_set(icsr::PENDSVCLR, value) {
self.exceptions[usize::from(PEND_SV)].pending = false;
}
if is_set(icsr::PENDSTSET, value) {
self.pend(SYS_TICK);
} else if is_set(icsr::PENDSTCLR, value) {
self.exceptions[usize::from(SYS_TICK)].pending = false;
}
}
fn shcsr(&self) -> u32 {
let e = |number: u16| self.exceptions[usize::from(number)];
set(shcsr::MEMFAULTACT, 0, u32::from(e(MEM_MANAGE).active))
| set(shcsr::BUSFAULTACT, 0, u32::from(e(BUS_FAULT).active))
| set(shcsr::USGFAULTACT, 0, u32::from(e(USAGE_FAULT).active))
| set(shcsr::SVCALLACT, 0, u32::from(e(SV_CALL).active))
| set(shcsr::MONITORACT, 0, u32::from(e(DEBUG_MONITOR).active))
| set(shcsr::PENDSVACT, 0, u32::from(e(PEND_SV).active))
| set(shcsr::SYSTICKACT, 0, u32::from(e(SYS_TICK).active))
| set(shcsr::USGFAULTPENDED, 0, u32::from(e(USAGE_FAULT).pending))
| set(shcsr::MEMFAULTPENDED, 0, u32::from(e(MEM_MANAGE).pending))
| set(shcsr::BUSFAULTPENDED, 0, u32::from(e(BUS_FAULT).pending))
| set(shcsr::SVCALLPENDED, 0, u32::from(e(SV_CALL).pending))
| set(shcsr::MEMFAULTENA, 0, u32::from(e(MEM_MANAGE).enabled))
| set(shcsr::BUSFAULTENA, 0, u32::from(e(BUS_FAULT).enabled))
| set(shcsr::USGFAULTENA, 0, u32::from(e(USAGE_FAULT).enabled))
}
/// B3.2.13: the enables, and the active and pending bits, which software may write too.
fn write_shcsr(&mut self, value: u32) {
let bit = |field| is_set(field, value);
for (number, active, pending, enabled) in [
(
MEM_MANAGE,
shcsr::MEMFAULTACT,
Some(shcsr::MEMFAULTPENDED),
Some(shcsr::MEMFAULTENA),
),
(
BUS_FAULT,
shcsr::BUSFAULTACT,
Some(shcsr::BUSFAULTPENDED),
Some(shcsr::BUSFAULTENA),
),
(
USAGE_FAULT,
shcsr::USGFAULTACT,
Some(shcsr::USGFAULTPENDED),
Some(shcsr::USGFAULTENA),
),
(SV_CALL, shcsr::SVCALLACT, Some(shcsr::SVCALLPENDED), None),
(DEBUG_MONITOR, shcsr::MONITORACT, None, None),
(PEND_SV, shcsr::PENDSVACT, None, None),
(SYS_TICK, shcsr::SYSTICKACT, None, None),
] {
let was_active = self.exceptions[usize::from(number)].active;
let exception = &mut self.exceptions[usize::from(number)];
exception.active = bit(active);
match pending.map(bit) {
Some(true) => self.pended |= exception.pend(),
Some(false) => exception.pending = false,
None => {}
}
if let Some(enabled) = enabled {
exception.enabled = bit(enabled);
}
match (was_active, bit(active)) {
(false, true) => self.active.push(number),
(true, false) => self.active.retain(|&active| active != number),
_ => {}
}
}
}
}
/// Whether `number` is always enabled: everything but the three configurable faults, which
/// `SHCSR` enables, and the external interrupts, which `ISER` does. B3.2.13, B3.4.4.
fn always_enabled(number: u16) -> bool {
matches!(
number,
RESET | NMI | HARD_FAULT | SV_CALL | DEBUG_MONITOR | PEND_SV | SYS_TICK
)
}
/// Whether `number` has a priority software sets: everything from MemManage up but the reserved
/// numbers 7 to 10 and 13, B1.5.2.
fn configurable(number: u16) -> bool {
number >= MEM_MANAGE && !matches!(number, 7..=10 | 13)
}
// ceiling: the Cortex-M4's identification registers for every model, but for what the family
// drops without the DSP or FP extension; upgrade when a Cortex-M3 or M7 manual is cited.
/// 100166 table 4-1's identification registers, from `ID_PFR0` at `0xD40` to `ID_ISAR4` at `0xD70`.
const ID_REGISTERS: [u32; 13] = [
0x0000_0030,
0x0000_0200,
0x0010_0000,
0x0000_0000,
0x0010_0030,
0x0000_0000,
0x0100_0000,
0x0000_0000,
0x0114_1110,
0x0211_2000,
0x2123_2231,
0x0111_1131,
0x0131_0132,
];
/// The system control space's peripheral and component identification, `PID0` to `PID3` and
/// `CID0` to `CID3` at `0xFE0` to `0xFFC`, 100166 table 8-3. `PID0` is a core's without an FPU,
/// and the family answers it for one with.
const COMPONENT_ID: [u8; 8] = [0x00, 0xB0, 0x0B, 0x00, 0x0D, 0xE0, 0x05, 0xB1];
/// `PID4` at `0xFD0`, 100166 table 8-3, with `PID5` to `PID7` after it reading zero.
const PID4: u32 = 0x04;
/// The bits of `ACTLR` the M4 has, 100166 section 4.2.
const ACTLR_WRITABLE: u32 = mask(actlr::DISOOFP)
| mask(actlr::DISFPCA)
| mask(actlr::DISFOLD)
| mask(actlr::DISDEFWBUF)
| mask(actlr::DISMCYCINT);
/// SysTick, B3.3: a 24-bit counter down from `RVR` at the core clock or the reference clock.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[expect(
clippy::struct_excessive_bools,
reason = "SysTick's control register keeps each of these as a bit of its own, B3.3.3"
)]
struct SysTick {
clock_hz: u64,
refclk_divider: u32,
enabled: bool,
tickint: bool,
/// `CLKSOURCE`: the core clock when set, the reference clock otherwise.
core_clock: bool,
countflag: bool,
reload: u32,
/// The counter at `since`, from where it counts.
value: u32,
since: VirtualNanos,
/// Zeros reached since `since`, so none is reached twice.
zeros: u64,
}
impl SysTick {
fn new(clock_hz: u64, refclk_divider: u32) -> Self {
Self {
clock_hz,
refclk_divider,
enabled: false,
tickint: false,
core_clock: false,
countflag: false,
reload: 0,
value: 0,
since: VirtualNanos(0),
zeros: 0,
}
}
fn reset(&mut self) {
*self = Self::new(self.clock_hz, self.refclk_divider);
}
fn hz(&self) -> u64 {
if self.core_clock {
self.clock_hz
} else {
self.clock_hz / u64::from(self.refclk_divider.max(1))
}
}
/// Ticks counted from `since` to `now`.
fn ticks(&self, now: VirtualNanos) -> u64 {
let elapsed = u128::from(now.0.saturating_sub(self.since.0));
u64::try_from(elapsed * u128::from(self.hz()) / 1_000_000_000).unwrap_or(u64::MAX)
}
/// The tick at which zero number `n` (from 0) is reached after `since`: the counter runs `value`
/// down to 0, then `reload` + 1 ticks to each next 0. From 0 the first tick reloads.
/// A reload of zero holds the counter at 0 once it gets there, so there's at most one zero,
/// B3.3.1.
fn zero_tick(&self, n: u64) -> Option<u64> {
if self.reload == 0 {
return (n == 0 && self.value != 0).then_some(u64::from(self.value));
}
let period = u64::from(self.reload) + 1;
let first = if self.value == 0 {
period
} else {
u64::from(self.value)
};
Some(first + n * period)
}
fn counter(&self, now: VirtualNanos) -> u32 {
if !self.enabled {
return self.value;
}
let ticks = self.ticks(now);
let value = u64::from(self.value);
if ticks <= value {
return u32::try_from(value - ticks).expect("24 bits");
}
if self.reload == 0 {
return 0;
}
let period = u64::from(self.reload) + 1;
let into = (ticks - value - 1) % period;
self.reload - u32::try_from(into).expect("24 bits")
}
fn next_zero(&self) -> Option<VirtualNanos> {
if !self.enabled {
return None;
}
let tick = self.zero_tick(self.zeros)?;
let ns = (u128::from(tick) * 1_000_000_000).div_ceil(u128::from(self.hz().max(1)));
Some(VirtualNanos(
self.since.0.saturating_add(u64::try_from(ns).ok()?),
))
}
/// Counts the zero reached at `now`, and answers whether it pends the exception.
fn reach(&mut self, now: VirtualNanos) -> bool {
if self.next_zero().is_none_or(|zero| zero > now) {
return false;
}
self.zeros += 1;
self.countflag = true;
self.tickint
}
/// Starts counting again from what the counter reads at `now`.
fn rebase(&mut self, now: VirtualNanos) {
self.value = self.counter(now);
self.since = now;
self.zeros = 0;
}
fn read(&mut self, offset: u64, now: VirtualNanos) -> u32 {
match offset {
0x010 => {
let csr = set(syst_csr::ENABLE, 0, u32::from(self.enabled))
| set(syst_csr::TICKINT, 0, u32::from(self.tickint))
| set(syst_csr::CLKSOURCE, 0, u32::from(self.core_clock))
| set(syst_csr::COUNTFLAG, 0, u32::from(self.countflag));
self.countflag = false;
csr
}
0x014 => self.reload,
0x018 => self.counter(now),
0x01C => self.calibration(),
_ => 0,
}
}
fn write(&mut self, offset: u64, value: u32, now: VirtualNanos) {
match offset {
0x010 => {
self.rebase(now);
self.enabled = is_set(syst_csr::ENABLE, value);
self.tickint = is_set(syst_csr::TICKINT, value);
self.core_clock = is_set(syst_csr::CLKSOURCE, value);
}
0x014 => {
self.rebase(now);
self.reload = value & mask(syst_rvr::RELOAD);
}
0x018 => {
self.value = 0;
self.since = now;
self.zeros = 0;
self.countflag = false;
}
_ => {}
}
}
/// B3.3.6: `TENMS`, the reference clock's ticks in ten milliseconds less one, with `SKEW` when
/// that isn't exact.
fn calibration(&self) -> u32 {
let hz = self.clock_hz / u64::from(self.refclk_divider.max(1));
let ticks = hz / 100;
let most = mask(syst_calib::TENMS);
let tenms = u32::try_from(ticks.saturating_sub(1)).unwrap_or(most) & most;
let skew = !hz.is_multiple_of(100);
tenms | set(syst_calib::SKEW, 0, u32::from(skew))
}
}
#[cfg(test)]
mod tests {
use super::super::registers::dfsr;
use super::*;
const NONE: Masks = Masks {
primask: false,
faultmask: false,
locked_up: false,
held: false,
};
fn scs() -> Scs {
Scs::new(91, 4, 168_000_000, 8)
}
fn enable(scs: &mut Scs, line: u16) {
scs.write(
0x100 + u64::from(line / 32) * 4,
1 << (line % 32),
VirtualNanos(0),
);
}
/// A write of `AIRCR` with its key and `prigroup`, B3.2.6.
fn aircr_write(prigroup: u32) -> u32 {
set(aircr::PRIGROUP, set(aircr::VECTKEY, 0, 0x05FA), prigroup)
}
/// What `AIRCR` reads with `prigroup`, B3.2.6.
fn aircr_value(prigroup: u32) -> u32 {
set(
aircr::PRIGROUP,
set(aircr::VECTKEYSTAT, 0, 0xFA05),
prigroup,
)
}
fn set_priority(scs: &mut Scs, number: u16, priority: u8) {
let offset = if number >= FIRST_INTERRUPT {
0x400 + u64::from(number - FIRST_INTERRUPT)
} else {
0xD18 + u64::from(number - 4)
};
assert!(scs.write_priority_byte(offset, priority));
}
#[test]
fn fixed_priorities_beat_any_configured_one() {
let mut scs = scs();
enable(&mut scs, 0);
set_priority(&mut scs, FIRST_INTERRUPT, 0);
scs.set_line(0, true);
scs.pend(NMI);
assert_eq!(scs.acknowledge(NONE), Some(NMI));
assert_eq!(scs.preempting(NONE), None, "nothing preempts NMI");
}
#[test]
fn a_pending_interrupt_is_taken_only_when_enabled() {
let mut scs = scs();
scs.set_line(3, true);
assert_eq!(scs.preempting(NONE), None);
enable(&mut scs, 3);
assert_eq!(scs.preempting(NONE), Some(FIRST_INTERRUPT + 3));
assert_eq!(scs.acknowledge(NONE), Some(FIRST_INTERRUPT + 3));
assert_eq!(scs.preempting(NONE), None, "active and no longer pending");
}
#[test]
fn a_level_still_high_at_return_pends_again() {
let mut scs = scs();
enable(&mut scs, 5);
scs.set_line(5, true);
assert_eq!(scs.acknowledge(NONE), Some(21));
assert!(scs.complete(21));
assert_eq!(
scs.preempting(NONE),
Some(21),
"still high, so pending again"
);
scs.acknowledge(NONE);
scs.set_line(5, false);
assert!(scs.complete(21));
assert_eq!(scs.preempting(NONE), None);
}
#[test]
fn icpr_cannot_clear_an_input_that_is_high() {
let mut scs = scs();
scs.set_line(2, true);
scs.write(0x280, 1 << 2, VirtualNanos(0));
assert_eq!(scs.read(0x200, VirtualNanos(0)), Some(1 << 2));
scs.set_line(2, false);
scs.write(0x280, 1 << 2, VirtualNanos(0));
assert_eq!(scs.read(0x200, VirtualNanos(0)), Some(0));
}
#[test]
fn a_pulse_while_active_pends_once() {
let mut scs = scs();
enable(&mut scs, 1);
scs.set_line(1, true);
scs.set_line(1, false);
assert_eq!(scs.acknowledge(NONE), Some(17));
for _ in 0..3 {
scs.set_line(1, true);
scs.set_line(1, false);
}
assert!(scs.complete(17));
assert_eq!(scs.acknowledge(NONE), Some(17), "pending once more");
assert!(scs.complete(17));
assert_eq!(scs.preempting(NONE), None, "and only once");
}
#[test]
fn primask_and_faultmask_boost_the_execution_priority() {
let mut scs = scs();
enable(&mut scs, 0);
scs.set_line(0, true);
let primask = Masks {
primask: true,
..NONE
};
assert_eq!(scs.preempting(primask), None);
scs.pend(NMI);
assert_eq!(scs.preempting(primask), Some(NMI));
scs.acknowledge(primask);
let faultmask = Masks {
faultmask: true,
..NONE
};
scs.complete(NMI);
scs.pend(HARD_FAULT);
assert_eq!(
scs.preempting(faultmask),
None,
"FAULTMASK holds HardFault off"
);
}
#[test]
fn basepri_masks_by_group_priority() {
let mut scs = scs();
enable(&mut scs, 0);
enable(&mut scs, 1);
set_priority(&mut scs, 16, 0x40);
set_priority(&mut scs, 17, 0x20);
scs.set_line(0, true);
scs.set_line(1, true);
scs.write_basepri(0x30);
assert_eq!(
scs.preempting(NONE),
Some(17),
"0x20 beats a BASEPRI of 0x30"
);
scs.write_basepri(0x20);
assert_eq!(scs.preempting(NONE), None, "and 0x20 doesn't");
}
#[test]
fn the_subpriority_orders_pending_exceptions_and_never_preempts() {
let mut scs = scs();
// PRIGROUP 5: bits [7:6] are the group and [5:0] the subpriority.
scs.write(0xD0C, aircr_write(5), VirtualNanos(0));
for line in 0..3 {
enable(&mut scs, line);
}
set_priority(&mut scs, 16, 0x40);
set_priority(&mut scs, 17, 0x50);
set_priority(&mut scs, 18, 0x60);
scs.set_line(1, true);
scs.set_line(0, true);
assert_eq!(scs.acknowledge(NONE), Some(16), "subpriority 0x00 first");
scs.set_line(2, true);
assert_eq!(scs.preempting(NONE), None, "the same group doesn't preempt");
scs.set_line(0, false);
scs.complete(16);
assert_eq!(scs.acknowledge(NONE), Some(17));
}
#[test]
fn unimplemented_priority_bits_read_as_zero() {
let mut scs = scs();
set_priority(&mut scs, 16, 0xFF);
assert_eq!(scs.read(0x400, VirtualNanos(0)), Some(0xF0));
scs.write_basepri(0x1F);
assert_eq!(scs.basepri, 0x10);
}
#[test]
fn eight_priority_bits_keep_every_bit() {
let mut scs = Scs::new(4, 8, 1_000_000, 8);
set_priority(&mut scs, 16, 0xA5);
assert_eq!(scs.read(0x400, VirtualNanos(0)), Some(0xA5));
}
#[test]
fn the_nvic_registers_read_back_what_they_hold() {
let mut scs = scs();
scs.write(0x104, 0b101, VirtualNanos(0));
assert_eq!(scs.read(0x104, VirtualNanos(0)), Some(0b101));
assert_eq!(scs.read(0x184, VirtualNanos(0)), Some(0b101));
scs.write(0x184, 0b001, VirtualNanos(0));
assert_eq!(scs.read(0x104, VirtualNanos(0)), Some(0b100));
scs.write(0x204, 0b10, VirtualNanos(0));
assert_eq!(scs.read(0x284, VirtualNanos(0)), Some(0b10));
scs.set_line(34, true);
scs.acknowledge(NONE);
assert_eq!(scs.read(0x304, VirtualNanos(0)), Some(0b100));
for byte in 0..4 {
assert!(
scs.write_priority_byte(0x404 + byte, 0x10 * (u8::try_from(byte).unwrap() + 1))
);
}
assert_eq!(scs.read(0x404, VirtualNanos(0)), Some(0x4030_2010));
assert_eq!(
scs.read(0x004, VirtualNanos(0)),
Some(2),
"91 lines is 3 words"
);
}
/// B1.5.18: what goes from not pending to pending is reported once, however it pends.
#[test]
fn an_exception_going_pending_is_reported_once() {
let mut scs = scs();
let now = VirtualNanos(0);
scs.set_line(0, true);
assert!(scs.take_pended());
scs.set_line(0, false);
scs.set_line(0, true);
scs.write(0x200, 1, now);
scs.pend(FIRST_INTERRUPT);
assert!(!scs.take_pended(), "already pending");
scs.write(0x200, 1 << 1, now);
assert!(scs.take_pended(), "ISPR");
scs.write(0xD24, mask(shcsr::SVCALLPENDED), now);
assert!(scs.take_pended(), "SHCSR.SVCALLPENDED");
scs.write(0xD04, mask(icsr::PENDSVSET), now);
assert!(scs.take_pended(), "ICSR.PENDSVSET");
assert!(!scs.take_pended());
}
#[test]
fn stir_pends_an_interrupt_by_number() {
let mut scs = scs();
scs.write(0xF00, 40, VirtualNanos(0));
assert_eq!(scs.read(0x204, VirtualNanos(0)), Some(1 << 8));
}
#[test]
fn icsr_reports_and_pends() {
let mut scs = scs();
scs.write(0xD04, mask(icsr::PENDSVSET), VirtualNanos(0));
let value = scs.icsr(0, false);
assert!(is_set(icsr::PENDSVSET, value), "PENDSVSET reads pending");
assert_eq!(
get(icsr::VECTPENDING, value),
u32::from(PEND_SV),
"VECTPENDING"
);
scs.acknowledge(NONE);
let value = scs.icsr(u32::from(PEND_SV), false);
assert_eq!(
get(icsr::VECTACTIVE, value),
u32::from(PEND_SV),
"VECTACTIVE"
);
assert!(
is_set(icsr::RETTOBASE, value),
"RETTOBASE: the only active one"
);
scs.write(0xD04, mask(icsr::PENDSTSET), VirtualNanos(0));
scs.write(0xD04, mask(icsr::PENDSTCLR), VirtualNanos(0));
assert!(!is_set(icsr::PENDSTSET, scs.icsr(0, false)));
scs.set_line(0, true);
enable(&mut scs, 0);
let value = scs.icsr(0, false);
assert_eq!(
get(icsr::VECTPENDING, value),
16,
"VECTPENDING, whatever the core masks"
);
assert!(is_set(icsr::ISRPENDING, value), "ISRPENDING");
}
/// B3.2.4: `VECTPENDING` is the highest priority exception that's pending and enabled, an
/// active one pending again included.
#[test]
fn vectpending_counts_an_active_exception_pending_again() {
let mut scs = scs();
for (line, priority) in [(0, 0x40), (1, 0x80)] {
scs.write_priority_byte(0x400 + u64::from(line), priority);
enable(&mut scs, line);
}
scs.write(0x200, 1, VirtualNanos(0));
assert_eq!(scs.acknowledge(NONE), Some(16));
scs.write(0x200, 0b11, VirtualNanos(0));
assert_eq!(get(icsr::VECTPENDING, scs.icsr(16, false)), 16);
}
/// B3.2.4: `VECTACTIVE` is `IPSR`'s, and `RETTOBASE` reads 1 only while no
/// exception but the executing one is active. Thread mode with one still active, as
/// `CCR.NONBASETHRDENA` allows, reads 0 for both.
#[test]
fn icsr_reports_the_executing_exception_and_any_other_active() {
let fields = |value| (get(icsr::VECTACTIVE, value), is_set(icsr::RETTOBASE, value));
let mut scs = scs();
assert_eq!(
fields(scs.icsr(0, false)),
(0, true),
"Thread mode, nothing active"
);
scs.pend(PEND_SV);
scs.acknowledge(NONE);
assert_eq!(
fields(scs.icsr(u32::from(PEND_SV), false)),
(u32::from(PEND_SV), true)
);
assert_eq!(
fields(scs.icsr(0, false)),
(0, false),
"Thread mode, PendSV active"
);
scs.pend(NMI);
scs.acknowledge(NONE);
assert_eq!(
fields(scs.icsr(u32::from(NMI), false)),
(u32::from(NMI), false)
);
}
#[test]
fn aircr_needs_its_key_and_asks_for_a_reset() {
let mut scs = scs();
assert!(
!scs.write(
0xD0C,
set(aircr::PRIGROUP, mask(aircr::SYSRESETREQ), 5),
VirtualNanos(0)
)
.reset,
"no key"
);
assert_eq!(scs.read(0xD0C, VirtualNanos(0)), Some(aircr_value(0)));
let asked = scs.write(
0xD0C,
aircr_write(3) | mask(aircr::SYSRESETREQ),
VirtualNanos(0),
);
assert!(asked.reset);
assert_eq!(scs.read(0xD0C, VirtualNanos(0)), Some(aircr_value(3)));
}
#[test]
fn the_system_handler_registers_hold_their_exceptions() {
let mut scs = scs();
assert!(scs.write_priority_byte(0xD22, 0xFF));
assert_eq!(
scs.read(0xD20, VirtualNanos(0)),
Some(0x00F0_0000),
"PendSV's byte"
);
let enables =
mask(shcsr::MEMFAULTENA) | mask(shcsr::BUSFAULTENA) | mask(shcsr::USGFAULTENA);
scs.write(0xD24, enables, VirtualNanos(0));
assert_eq!(scs.read(0xD24, VirtualNanos(0)), Some(enables));
assert!(scs.exceptions[usize::from(USAGE_FAULT)].enabled);
let asked = scs.write(
0xD10,
mask(scr::SEVONPEND) | mask(scr::SLEEPONEXIT),
VirtualNanos(0),
);
assert_eq!(
asked.sleep,
Some(Sleep {
on_exit: true,
event_on_pending: true
})
);
}
#[test]
fn escalation_sets_forced_and_writing_one_clears_it() {
let mut scs = scs();
assert_eq!(scs.raise(USAGE_FAULT, NONE), Raised::Pending);
assert_eq!(
scs.preempting(NONE),
Some(HARD_FAULT),
"disabled, so HardFault"
);
assert_eq!(scs.read(0xD2C, VirtualNanos(0)), Some(mask(hfsr::FORCED)));
scs.write(0xD2C, mask(hfsr::FORCED), VirtualNanos(0));
assert_eq!(scs.read(0xD2C, VirtualNanos(0)), Some(0));
}
#[test]
fn a_fault_that_cannot_preempt_escalates_or_locks_up() {
let mut scs = scs();
let primask = Masks {
primask: true,
..NONE
};
assert_eq!(scs.raise(SV_CALL, primask), Raised::Pending);
assert_eq!(
scs.acknowledge(primask),
Some(HARD_FAULT),
"SVC under PRIMASK escalates"
);
assert_eq!(
scs.raise(USAGE_FAULT, NONE),
Raised::Lockup,
"in HardFault already"
);
}
#[test]
fn a_stacking_fault_that_beats_the_entered_exception_replaces_it() {
let mut scs = scs();
scs.write(0xD24, mask(shcsr::BUSFAULTENA), VirtualNanos(0));
enable(&mut scs, 0);
set_priority(&mut scs, 16, 0x80);
scs.set_line(0, true);
assert_eq!(scs.acknowledge(NONE), Some(16));
assert_eq!(
scs.raise_while_stacking(BUS_FAULT, 16, NONE),
Raised::Replaced
);
assert_eq!(scs.acknowledge(NONE), Some(BUS_FAULT));
assert_eq!(
scs.preempting(NONE),
None,
"the interrupt waits, pending again"
);
assert!(scs.exceptions[16].pending);
}
/// B1.5.11's `DerivedLateArrival()` compares group priorities, so a BusFault of the
/// interrupt's group pends and the interrupt is taken, though its subpriority is better.
#[test]
fn a_stacking_fault_of_the_entered_exceptions_group_waits_for_it() {
let mut scs = scs();
// PRIGROUP 5: bits [7:6] are the group, so 0x40 and 0x50 are one group.
scs.write(0xD0C, aircr_write(5), VirtualNanos(0));
scs.write(0xD24, mask(shcsr::BUSFAULTENA), VirtualNanos(0));
set_priority(&mut scs, BUS_FAULT, 0x40);
enable(&mut scs, 0);
set_priority(&mut scs, 16, 0x50);
scs.set_line(0, true);
assert_eq!(scs.acknowledge(NONE), Some(16));
assert_eq!(
scs.raise_while_stacking(BUS_FAULT, 16, NONE),
Raised::Pending
);
assert_eq!(
get(icsr::VECTACTIVE, scs.icsr(16, false)),
16,
"the interrupt stays"
);
assert!(scs.exceptions[usize::from(BUS_FAULT)].pending);
assert_eq!(scs.preempting(NONE), None, "and the fault can't preempt it");
}
/// B3.2.4: `VECTACTIVE` is `IPSR<8:0>` and `RETTOBASE` says whether anything else is
/// active, however the active exceptions came to be. Thread mode, where it's UNKNOWN, reads
/// it the same way with `IPSR` 0.
#[test]
fn icsr_reads_the_active_exception_from_ipsr() {
let mut scs = scs();
assert!(is_set(icsr::RETTOBASE, scs.icsr(0, false)));
enable(&mut scs, 0);
scs.set_line(0, true);
assert_eq!(scs.acknowledge(NONE), Some(16));
assert!(is_set(icsr::RETTOBASE, scs.icsr(16, false)));
scs.write(0xD24, mask(shcsr::SYSTICKACT), VirtualNanos(0));
let value = scs.icsr(16, false);
assert_eq!(get(icsr::VECTACTIVE, value), 16, "SysTick went active last");
assert!(!is_set(icsr::RETTOBASE, value));
assert_eq!(
scs.icsr(0, false) & (mask(icsr::VECTACTIVE) | mask(icsr::RETTOBASE)),
0
);
}
/// C1.6.5: `MON_PEND` pends DebugMonitor whatever `MON_EN` says, reads its pending state,
/// and clears it written as zero.
#[test]
fn mon_pend_is_debugmonitors_pending_state() {
let mut scs = scs();
let now = VirtualNanos(0);
scs.write(0xDFC, mask(demcr::MON_PEND), now);
assert!(scs.take_pended());
assert_eq!(scs.read(0xDFC, now), Some(mask(demcr::MON_PEND)));
assert_eq!(scs.preempting(NONE), Some(DEBUG_MONITOR));
scs.write(0xDFC, mask(demcr::MON_EN), now);
assert_eq!(scs.read(0xDFC, now), Some(mask(demcr::MON_EN)));
assert_eq!(scs.preempting(NONE), None);
scs.pend(DEBUG_MONITOR);
assert_eq!(
scs.read(0xDFC, now),
Some(mask(demcr::MON_EN) | mask(demcr::MON_PEND)),
"pending however it pended"
);
assert_eq!(scs.acknowledge(NONE), Some(DEBUG_MONITOR));
assert_eq!(scs.read(0xDFC, now), Some(mask(demcr::MON_EN)));
}
#[test]
fn locked_up_only_nmi_is_taken() {
let mut scs = scs();
enable(&mut scs, 0);
scs.set_line(0, true);
let locked_up = Masks {
locked_up: true,
..NONE
};
assert_eq!(scs.preempting(locked_up), None);
scs.pend(NMI);
assert_eq!(scs.preempting(locked_up), Some(NMI));
}
/// C1.6.2: a debugger's step holds off PendSV, SysTick and the external interrupts, and
/// nothing else.
#[test]
fn a_held_step_takes_only_what_c_maskints_leaves() {
let mut scs = scs();
enable(&mut scs, 0);
scs.set_line(0, true);
scs.pend(PEND_SV);
scs.pend(SYS_TICK);
let held = Masks { held: true, ..NONE };
assert_eq!(scs.preempting(held), None);
assert!(!scs.wakes(held));
assert_eq!(
(scs.icsr(0, true) >> 12) & 0x1FF,
0,
"VECTPENDING hides them too"
);
assert_ne!((scs.icsr(0, false) >> 12) & 0x1FF, 0);
scs.pend(SV_CALL);
assert_eq!(scs.preempting(held), Some(SV_CALL));
assert_eq!(
scs.preempting(NONE),
Some(SV_CALL),
"first by number at priority 0"
);
}
#[test]
fn wfi_wakes_regardless_of_primask() {
let mut scs = scs();
enable(&mut scs, 0);
scs.set_line(0, true);
let primask = Masks {
primask: true,
..NONE
};
assert_eq!(scs.preempting(primask), None);
assert!(scs.wakes(primask));
scs.write_basepri(0x10);
set_priority(&mut scs, 16, 0x20);
assert!(!scs.wakes(primask), "BASEPRI still holds it");
}
#[test]
fn the_identification_registers_say_what_this_core_is() {
let mut scs = scs();
let read = |scs: &mut Scs, offset| scs.read(offset, VirtualNanos(0));
assert_eq!(read(&mut scs, 0xD40), Some(0x30));
assert_eq!(read(&mut scs, 0xD60), Some(0x0114_1110));
assert_eq!(read(&mut scs, 0xD70), Some(0x0131_0132));
assert_eq!(read(&mut scs, 0xF40), Some(0x1011_0021));
let identification: Vec<_> = (0xFD0..=0xFFC)
.step_by(4)
.map(|offset| read(&mut scs, offset).unwrap())
.collect();
assert_eq!(
identification,
[4, 0, 0, 0, 0x00, 0xB0, 0x0B, 0x00, 0x0D, 0xE0, 0x05, 0xB1],
"100166 table 8-3, on a core without an FPU"
);
scs.write(0x008, 0xFFFF_FFFF, VirtualNanos(0));
assert_eq!(
read(&mut scs, 0x008),
Some(0x307),
"ACTLR, 100166 section 4.2"
);
scs.write(0xDFC, 0xFFFF_FFFF, VirtualNanos(0));
assert_eq!(
read(&mut scs, 0xDFC),
Some(0x010F_07F1),
"DEMCR's TRCENA, DebugMonitor and vector catch bits, C1.6.5"
);
}
#[test]
fn systick_counts_reloads_and_pends() {
let mut scs = scs();
// The core clock at 168 MHz: 168 ticks a microsecond.
scs.write(0x014, 167, VirtualNanos(0));
scs.write(0x018, 0, VirtualNanos(0));
scs.write(
0x010,
mask(syst_csr::ENABLE) | mask(syst_csr::TICKINT) | mask(syst_csr::CLKSOURCE),
VirtualNanos(0),
);
let zero = scs.next_event().unwrap();
assert_eq!(
zero,
VirtualNanos(1000),
"168 ticks from 0: a reload, then 167 down to 0"
);
assert_eq!(scs.read(0x018, VirtualNanos(500)), Some(84));
scs.on_event(zero);
assert!(scs.exceptions[usize::from(SYS_TICK)].pending);
assert_eq!(
scs.peek(0x010, zero)
.map(|csr| is_set(syst_csr::COUNTFLAG, csr)),
Some(true),
"a debugger's read leaves it"
);
assert_eq!(
scs.read(0x010, zero)
.map(|csr| is_set(syst_csr::COUNTFLAG, csr)),
Some(true)
);
assert_eq!(
scs.read(0x010, zero)
.map(|csr| is_set(syst_csr::COUNTFLAG, csr)),
Some(false),
"read clears it"
);
assert_eq!(scs.next_event(), Some(VirtualNanos(2000)));
}
#[test]
fn systick_with_a_reload_of_zero_reaches_zero_once_and_stays() {
let mut scs = scs();
scs.write(0x014, 167, VirtualNanos(0));
scs.write(
0x010,
mask(syst_csr::ENABLE) | mask(syst_csr::CLKSOURCE),
VirtualNanos(0),
);
scs.write(0x014, 0, VirtualNanos(500));
let zero = scs.next_event().unwrap();
assert_eq!(zero, VirtualNanos(1000), "the 84 ticks left down to 0");
scs.on_event(zero);
assert_eq!(scs.next_event(), None, "held at zero");
assert_eq!(scs.read(0x018, VirtualNanos(5000)), Some(0));
}
#[test]
fn a_model_from_another_machine_or_past_its_own_table_cant_replace_one() {
let live = scs();
assert!(live.clone().can_replace(&live));
let mut active = live.clone();
active.active.push(500);
assert!(
!active.can_replace(&live),
"an active exception past the table"
);
let mut grouped = live.clone();
grouped.prigroup = 15;
assert!(!grouped.can_replace(&live), "PRIGROUP is three bits");
let other = Scs::new(90, 4, 168_000_000, 8);
assert!(!other.can_replace(&live), "another machine's table");
}
#[test]
fn systick_calibration_reports_ten_milliseconds() {
let mut scs = scs();
assert_eq!(scs.read(0x01C, VirtualNanos(0)), Some(209_999));
}
#[test]
fn a_reset_forgets_all_but_demcrs_trace_enable_and_vector_catch() {
let mut scs = scs();
enable(&mut scs, 0);
scs.set_line(0, true);
scs.acknowledge(NONE);
scs.write(0xD0C, aircr_write(3), VirtualNanos(0));
scs.write(0xDFC, DEMCR_WRITABLE, VirtualNanos(0));
scs.halted_by(mask(dfsr::BKPT));
scs.reset();
assert_eq!(
scs.read(0xDFC, VirtualNanos(0)),
Some(mask(demcr::TRCENA) | DEMCR_VECTOR_CATCH),
"TRCENA and vector catch kept"
);
assert_eq!(
scs.read(0xD30, VirtualNanos(0)),
Some(mask(dfsr::BKPT)),
"DFSR as it was, C1.6.1"
);
assert_eq!(scs.read(0x100, VirtualNanos(0)), Some(0));
assert_eq!(scs.read(0x300, VirtualNanos(0)), Some(0));
assert_eq!(scs.read(0xD0C, VirtualNanos(0)), Some(aircr_value(0)));
scs.set_line(0, true);
assert!(
scs.exceptions[16].pending,
"the input was forgotten, so high again is an edge"
);
}
}