feat(firmware): validate configuration and migrate sensor-driven flight APIs

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.
This commit is contained in:
Onur Özkan committed 2026-11-07 18:12:11 +03:00
1 parent fb1b8af2d1
commit f9a61a086f
211 files changed
+45657 -12740

No files matched your search

@@ -0,0 +1,332 @@
//! 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, &registers, 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, &registers, 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, &registers, (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, &registers, 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, &registers, 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, &registers, 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, &registers, 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, &registers, 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);
}