Validate configuration through its owning schemas and keep board declarations separate from shared F722 resource ownership. Bound USB transitions, isolate transport failures and retain restart-only activation. Migrate navigation and flight consumers with the sensor and schema contracts. Require explicit installation data and synchronous output authority; board motor outputs remain inhibited. Validation: assembled-prefix host tests, strict Clippy, platform/USB tests and both ARM release builds. Final-source coverage and scoped Miri/Kani evidence are retained separately from hardware qualification.
333 lines
11 KiB
Rust
333 lines
11 KiB
Rust
//! Injected hardware exercises the production shared-state and IRQ mechanism.
|
|
|
|
extern crate std;
|
|
|
|
use super::{SharedTimer, TimerBackend};
|
|
use std::{
|
|
collections::VecDeque,
|
|
sync::{Arc, Mutex, mpsc},
|
|
thread,
|
|
time::Duration,
|
|
vec::Vec,
|
|
};
|
|
|
|
const CC1IF: u32 = 1 << 1;
|
|
const UIF: u32 = 1;
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
enum Access {
|
|
Counter(u32),
|
|
Ack,
|
|
Compare(u32),
|
|
Enable,
|
|
Pend,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct Registers {
|
|
cnt: u32,
|
|
flags: u32,
|
|
compare: u32,
|
|
enabled: bool,
|
|
pended: bool,
|
|
// Advance while writing CCR. `true` means the new compare took effect
|
|
// before CNT passed it, so hardware raises a fresh CC1IF.
|
|
write_delays: VecDeque<(u32, bool)>,
|
|
accesses: Vec<Access>,
|
|
}
|
|
|
|
struct Counter {
|
|
registers: Arc<Mutex<Registers>>,
|
|
read_gate: Option<(mpsc::Sender<()>, mpsc::Receiver<()>)>,
|
|
}
|
|
|
|
impl TimerBackend for Counter {
|
|
fn counter(&mut self) -> u32 {
|
|
let cnt = {
|
|
let mut registers = self.registers.lock().unwrap();
|
|
let cnt = registers.cnt;
|
|
registers.accesses.push(Access::Counter(cnt));
|
|
cnt
|
|
};
|
|
if let Some((sampled, resume)) = self.read_gate.take() {
|
|
sampled.send(()).unwrap();
|
|
resume.recv().unwrap();
|
|
}
|
|
cnt
|
|
}
|
|
|
|
fn compare_pending(&mut self) -> bool {
|
|
self.registers.lock().unwrap().flags & CC1IF != 0
|
|
}
|
|
|
|
fn acknowledge_compare(&mut self) {
|
|
let mut registers = self.registers.lock().unwrap();
|
|
registers.flags &= !CC1IF;
|
|
registers.accesses.push(Access::Ack);
|
|
}
|
|
|
|
fn set_compare(&mut self, counter: u32) {
|
|
let mut registers = self.registers.lock().unwrap();
|
|
registers.compare = counter;
|
|
registers.accesses.push(Access::Compare(counter));
|
|
if let Some((advance, raises_flag)) = registers.write_delays.pop_front() {
|
|
registers.cnt = registers.cnt.wrapping_add(advance);
|
|
if raises_flag {
|
|
registers.flags |= CC1IF;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn enable_compare_interrupt(&mut self) {
|
|
let mut registers = self.registers.lock().unwrap();
|
|
registers.enabled = true;
|
|
registers.accesses.push(Access::Enable);
|
|
}
|
|
|
|
fn pend_interrupt(&mut self) {
|
|
let mut registers = self.registers.lock().unwrap();
|
|
registers.pended = true;
|
|
registers.accesses.push(Access::Pend);
|
|
}
|
|
}
|
|
|
|
fn timer() -> (SharedTimer<Counter>, Arc<Mutex<Registers>>) {
|
|
let registers = Arc::new(Mutex::new(Registers::default()));
|
|
let shared = SharedTimer::new();
|
|
assert!(shared.initialize(|| Counter {
|
|
registers: registers.clone(),
|
|
read_gate: None,
|
|
}));
|
|
(shared, registers)
|
|
}
|
|
|
|
fn tick(shared: &SharedTimer<Counter>, registers: &Mutex<Registers>, cnt: u32) -> bool {
|
|
{
|
|
let mut registers = registers.lock().unwrap();
|
|
registers.cnt = cnt;
|
|
registers.flags |= CC1IF;
|
|
}
|
|
shared.on_interrupt()
|
|
}
|
|
|
|
#[test]
|
|
fn one_time_publication_keeps_irq_disabled_until_successful_init_finishes() {
|
|
let (shared, registers) = timer();
|
|
assert!(!shared.initialize(|| panic!("must not reinitialize hardware")));
|
|
assert!(!registers.lock().unwrap().enabled);
|
|
// Other initialization may take time, or fail without ever enabling IRQs.
|
|
registers.lock().unwrap().cnt = 2_000_000;
|
|
registers.lock().unwrap().flags = CC1IF | UIF;
|
|
assert_eq!(shared.now().as_microseconds(), 2_000_000);
|
|
assert!(!registers.lock().unwrap().enabled);
|
|
registers.lock().unwrap().accesses.clear();
|
|
shared.enable_interrupt();
|
|
let registers = registers.lock().unwrap();
|
|
assert!(registers.enabled);
|
|
assert_eq!(registers.flags, UIF);
|
|
assert_eq!(
|
|
registers.accesses,
|
|
[
|
|
Access::Ack,
|
|
Access::Counter(2_000_000),
|
|
Access::Compare(2_010_000),
|
|
Access::Counter(2_000_000),
|
|
Access::Enable,
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn readers_and_irq_extend_one_epoch_across_rollover() {
|
|
let (shared, registers) = timer();
|
|
let reader_a = &shared;
|
|
let reader_b = &shared;
|
|
registers.lock().unwrap().cnt = u32::MAX - 5;
|
|
assert_eq!(reader_a.now().as_microseconds(), 4_294_967_290);
|
|
assert!(tick(&shared, ®isters, 4));
|
|
assert_eq!(reader_b.now().as_microseconds(), 4_294_967_300);
|
|
assert_eq!(registers.lock().unwrap().compare, 2_704);
|
|
registers.lock().unwrap().cnt = 100;
|
|
assert_eq!(reader_a.now().as_microseconds(), 4_294_967_396);
|
|
}
|
|
|
|
#[test]
|
|
fn future_compare_can_cross_counter_wrap() {
|
|
let (shared, registers) = timer();
|
|
assert!(tick(&shared, ®isters, u32::MAX - 5));
|
|
let registers = registers.lock().unwrap();
|
|
assert_eq!(registers.compare, 2_704);
|
|
assert!(!registers.pended);
|
|
}
|
|
|
|
#[test]
|
|
fn silent_reader_retains_time_across_multiple_irq_serviced_wraps() {
|
|
let (shared, registers) = timer();
|
|
let silent_reader = &shared;
|
|
assert_eq!(silent_reader.now().as_microseconds(), 0);
|
|
// Deliberately delayed service, still below the single-wrap observation
|
|
// bound. The silent consumer misses more than two whole counter wraps.
|
|
for second in 1..=10u64 {
|
|
assert!(tick(&shared, ®isters, (second * 1_000_000_000) as u32));
|
|
}
|
|
assert_eq!(silent_reader.now().as_microseconds(), 10_000_000_000);
|
|
}
|
|
|
|
#[test]
|
|
fn on_time_and_two_second_delayed_irq_each_emit_one_event_and_future_compare() {
|
|
let (shared, registers) = timer();
|
|
for (cnt, expected) in [(10_000, 20_000), (2_010_000, 2_020_000)] {
|
|
registers.lock().unwrap().accesses.clear();
|
|
assert!(tick(&shared, ®isters, cnt));
|
|
assert_eq!(shared.now().as_microseconds(), u64::from(cnt));
|
|
// This represents the consumer callback, after on_interrupt returns.
|
|
assert_eq!(registers.lock().unwrap().compare, expected);
|
|
assert!(!shared.on_interrupt());
|
|
assert_eq!(
|
|
registers
|
|
.lock()
|
|
.unwrap()
|
|
.accesses
|
|
.iter()
|
|
.filter(|access| matches!(access, Access::Compare(_)))
|
|
.count(),
|
|
1
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stale_nvic_pend_without_cc1if_neither_rearms_nor_notifies() {
|
|
let (shared, registers) = timer();
|
|
registers.lock().unwrap().flags = UIF;
|
|
assert!(!shared.on_interrupt());
|
|
let registers = registers.lock().unwrap();
|
|
assert!(registers.accesses.is_empty());
|
|
assert_eq!(registers.flags, UIF);
|
|
}
|
|
|
|
#[test]
|
|
fn target_passed_during_ccr_write_is_retried_before_callbacks() {
|
|
let (shared, registers) = timer();
|
|
registers
|
|
.lock()
|
|
.unwrap()
|
|
.write_delays
|
|
.push_back((10_001, false));
|
|
assert!(tick(&shared, ®isters, 10_000));
|
|
assert_eq!(shared.now().as_microseconds(), 20_001);
|
|
let registers = registers.lock().unwrap();
|
|
assert_eq!(registers.compare, 30_000);
|
|
assert!(!registers.pended);
|
|
assert_eq!(
|
|
registers.accesses,
|
|
[
|
|
Access::Ack,
|
|
Access::Counter(10_000),
|
|
Access::Compare(20_000),
|
|
Access::Counter(20_001),
|
|
Access::Compare(30_000),
|
|
Access::Counter(20_001),
|
|
Access::Counter(20_001),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_missed_writes_pend_bounded_recovery_without_fabricating_an_event() {
|
|
let (shared, registers) = timer();
|
|
registers.lock().unwrap().write_delays = [(10_001, false), (10_000, false)].into();
|
|
assert!(tick(&shared, ®isters, 10_000));
|
|
{
|
|
let mut registers = registers.lock().unwrap();
|
|
assert!(registers.pended);
|
|
assert_eq!(registers.compare, 30_000);
|
|
assert_eq!(registers.cnt, 30_001);
|
|
assert_eq!(registers.flags, 0);
|
|
assert_eq!(
|
|
registers
|
|
.accesses
|
|
.iter()
|
|
.filter(|a| matches!(a, Access::Compare(_)))
|
|
.count(),
|
|
2
|
|
);
|
|
registers.pended = false; // NVIC takes the recovery exception.
|
|
}
|
|
assert!(!shared.on_interrupt());
|
|
assert_eq!(registers.lock().unwrap().compare, 40_000);
|
|
assert!(!registers.lock().unwrap().pended);
|
|
assert!(tick(&shared, ®isters, 40_000));
|
|
assert_eq!(registers.lock().unwrap().compare, 50_000);
|
|
}
|
|
|
|
#[test]
|
|
fn new_compare_flag_during_rearm_and_unrelated_flags_are_preserved() {
|
|
let (shared, registers) = timer();
|
|
registers.lock().unwrap().flags = UIF;
|
|
registers
|
|
.lock()
|
|
.unwrap()
|
|
.write_delays
|
|
.push_back((10_000, true));
|
|
assert!(tick(&shared, ®isters, 10_000));
|
|
assert_eq!(registers.lock().unwrap().flags, UIF | CC1IF);
|
|
assert_eq!(registers.lock().unwrap().compare, 30_000);
|
|
assert!(shared.on_interrupt());
|
|
assert_eq!(registers.lock().unwrap().flags, UIF);
|
|
assert!(!shared.on_interrupt());
|
|
}
|
|
|
|
#[test]
|
|
fn irq_cannot_sample_between_a_readers_cnt_read_and_epoch_update() {
|
|
let registers = Arc::new(Mutex::new(Registers::default()));
|
|
let shared = SharedTimer::new();
|
|
let (sampled_tx, sampled_rx) = mpsc::channel();
|
|
let (resume_tx, resume_rx) = mpsc::channel();
|
|
assert!(shared.initialize(|| Counter {
|
|
registers: registers.clone(),
|
|
read_gate: Some((sampled_tx, resume_rx)),
|
|
}));
|
|
registers.lock().unwrap().cnt = 100;
|
|
thread::scope(|scope| {
|
|
let reader = scope.spawn(|| shared.now());
|
|
sampled_rx.recv().unwrap();
|
|
// Hardware continues ticking with PRIMASK set. The IRQ tries to run
|
|
// while the reader has sampled CNT but has not committed its delta.
|
|
registers.lock().unwrap().cnt = 150;
|
|
registers.lock().unwrap().flags = CC1IF;
|
|
let (attempt_tx, attempt_rx) = mpsc::channel();
|
|
let (done_tx, done_rx) = mpsc::channel();
|
|
let shared = &shared;
|
|
let irq = scope.spawn(move || {
|
|
attempt_tx.send(()).unwrap();
|
|
let event = shared.on_interrupt();
|
|
done_tx.send(()).unwrap();
|
|
event
|
|
});
|
|
attempt_rx.recv().unwrap();
|
|
let blocked = done_rx.recv_timeout(Duration::from_millis(20)).is_err();
|
|
// Release before assertions so a failing test cannot strand either thread.
|
|
resume_tx.send(()).unwrap();
|
|
assert_eq!(reader.join().unwrap().as_microseconds(), 100);
|
|
assert!(irq.join().unwrap());
|
|
assert!(blocked, "IRQ entered while the reader was extending CNT");
|
|
});
|
|
assert_eq!(shared.now().as_microseconds(), 150);
|
|
}
|
|
|
|
#[test]
|
|
fn full_wrap_blackout_reset_and_stopped_clock_are_not_detectable_from_cnt() {
|
|
let (shared, registers) = timer();
|
|
registers.lock().unwrap().cnt = 100;
|
|
assert_eq!(shared.now().as_microseconds(), 100);
|
|
// Stopped/deep-sleep time and a whole unseen wrap both present CNT=100.
|
|
// No software correction is possible. These modes require a system reset.
|
|
assert_eq!(shared.now().as_microseconds(), 100);
|
|
registers.lock().unwrap().cnt = ((1u64 << 32) + 100) as u32;
|
|
assert_eq!(shared.now().as_microseconds(), 100);
|
|
// An illicit reset/regression is indistinguishable from a forward wrap.
|
|
registers.lock().unwrap().cnt = 90;
|
|
assert_eq!(shared.now().as_microseconds(), 4_294_967_386);
|
|
}
|