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:
1 parent
fb1b8af2d1
commit
f9a61a086f
211 files changed
+45657
-12740
No files matched your search
@@ -0,0 +1,391 @@
|
||||
//! Level-triggered interrupt scheduling around the real USB transport operations.
|
||||
extern crate std;
|
||||
use super::*;
|
||||
use crate::f722::usb;
|
||||
use kaptan_mavlink::{encoder::MavlinkEncoder, generated::kaptan::FILE_DATA_DATA};
|
||||
use kaptan_tx::FrameClass;
|
||||
use std::{vec, vec::Vec};
|
||||
use usb::test_bus as usb_bus;
|
||||
use usb_bus::{Host, PacketBus, with_link};
|
||||
use usb_device::bus::PollResult;
|
||||
|
||||
fn poll_irq(host: &Host, usb: &mut usb::UsbMavlink<'_, PacketBus>) -> bool {
|
||||
usb.with_rx_interrupt_gate(
|
||||
|usb| usb.poll(),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
)
|
||||
}
|
||||
fn exchange(
|
||||
host: &Host,
|
||||
usb: &mut usb::UsbMavlink<'_, PacketBus>,
|
||||
frame: Option<MavlinkFrame>,
|
||||
) -> IoBatch {
|
||||
exchange_usb(
|
||||
usb,
|
||||
frame.map(|frame| PendingFrame {
|
||||
class: FrameClass::Critical,
|
||||
frame,
|
||||
}),
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
)
|
||||
}
|
||||
fn file_frame(length: usize) -> MavlinkFrame {
|
||||
let mut data = FILE_DATA_DATA::DEFAULT;
|
||||
data.data[..length - 21].fill(0x5a);
|
||||
let frame = MavlinkEncoder::new(1, 1).encode(&data);
|
||||
assert_eq!(frame.raw_bytes().len(), length);
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_rmw_changes_only_rxflvlm_and_preserves_adjacent_registers() {
|
||||
for bit in 0..32 {
|
||||
for initial in [1u32 << bit, !(1u32 << bit)] {
|
||||
let mut registers = [0xa5a5_5a5a, initial, 0x5a5a_a5a5];
|
||||
for enabled in [false, false, true, true, false] {
|
||||
// SAFETY: this aligned RAM word substitutes for the RW register.
|
||||
// No concurrent access exists and RAM needs no device barrier.
|
||||
unsafe { set_rx_fifo_interrupt_enabled(&mut registers[1], enabled) };
|
||||
assert_eq!(registers[1] & !(1 << 4), initial & !(1 << 4));
|
||||
assert_eq!(registers[1] & (1 << 4) != 0, enabled);
|
||||
assert_eq!(registers[0], 0xa5a5_5a5a);
|
||||
assert_eq!(registers[2], 0x5a5a_a5a5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The asserted, enabled priority-8 vector wins over the priority-7 consumer.
|
||||
fn step(
|
||||
host: &Host,
|
||||
usb: &mut usb::UsbMavlink<'_, PacketBus>,
|
||||
ready: &mut bool,
|
||||
frames: &mut Vec<MavlinkFrame>,
|
||||
) -> bool {
|
||||
if host.enter_irq() {
|
||||
*ready |= poll_irq(host, usb);
|
||||
false
|
||||
} else if core::mem::take(ready) {
|
||||
let batch = exchange(host, usb, None);
|
||||
*ready |= batch.pending;
|
||||
if let Some(frame) = batch.usb {
|
||||
frames.push(*frame.raw());
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_triggered_burst_starves_legacy_consumer_but_gated_consumer_recovers_exact_frames() {
|
||||
for gate in [false, true] {
|
||||
with_link(|host, usb| {
|
||||
let mut encoder = MavlinkEncoder::new(42, 7);
|
||||
let expected = core::array::from_fn::<_, 3, _>(|_| encoder.heartbeat());
|
||||
for frame in expected {
|
||||
host.write_packet(frame.raw_bytes());
|
||||
}
|
||||
let mut ready = false;
|
||||
let mut frames = Vec::new();
|
||||
let mut workers = 0;
|
||||
for _ in 0..32 {
|
||||
if gate {
|
||||
workers += usize::from(step(host, usb, &mut ready, &mut frames));
|
||||
} else if host.enter_irq() {
|
||||
ready |= usb.poll(); // Original handler: no RX source gate.
|
||||
} else if core::mem::take(&mut ready) {
|
||||
workers += 1;
|
||||
let batch = exchange(host, usb, None);
|
||||
ready |= batch.pending;
|
||||
if let Some(frame) = batch.usb {
|
||||
frames.push(*frame.raw());
|
||||
}
|
||||
}
|
||||
}
|
||||
if gate {
|
||||
assert_eq!(frames, expected);
|
||||
assert!(workers > 0);
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
} else {
|
||||
assert_eq!(
|
||||
workers, 0,
|
||||
"level IRQ prevents the only consumer from running"
|
||||
);
|
||||
assert!(frames.is_empty());
|
||||
assert_eq!(
|
||||
host.receive_backlog(),
|
||||
(1, true),
|
||||
"hardware FIFO and driver buffer are both occupied"
|
||||
);
|
||||
assert!(
|
||||
host.enter_irq(),
|
||||
"source is still asserted after repeated polls"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_coalesced_bad_crc_and_zlp_packets_preserve_rx_and_live_tx_boundaries() {
|
||||
with_link(|host, usb| {
|
||||
let incoming = file_frame(130);
|
||||
let mut encoder = MavlinkEncoder::new(42, 7);
|
||||
let first = encoder.heartbeat();
|
||||
let mut corrupt = encoder.heartbeat().raw_bytes().to_vec();
|
||||
*corrupt.last_mut().unwrap() ^= 1;
|
||||
let last = encoder.heartbeat();
|
||||
host.write_packet(&incoming.raw_bytes()[..64]);
|
||||
host.write_packet(&[]); // An OUT ZLP must not mask a partial MAVLink frame.
|
||||
host.write_packet(&incoming.raw_bytes()[64..128]);
|
||||
host.write_packet(
|
||||
&[
|
||||
&incoming.raw_bytes()[128..],
|
||||
first.raw_bytes(),
|
||||
&corrupt,
|
||||
last.raw_bytes(),
|
||||
]
|
||||
.concat(),
|
||||
);
|
||||
|
||||
let outgoing = file_frame(128);
|
||||
let after = encoder.heartbeat();
|
||||
assert!(exchange(host, usb, Some(outgoing)).accepted);
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
assert!(!host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
let mut packets = vec![host.take_packet().unwrap()];
|
||||
assert!(
|
||||
host.enter_irq(),
|
||||
"IN completion interrupts while RX is masked"
|
||||
);
|
||||
assert!(poll_irq(host, usb));
|
||||
packets.push(host.take_packet().unwrap());
|
||||
assert_eq!(packets.iter().map(Vec::len).collect::<Vec<_>>(), [64, 64]);
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
packets.push(host.take_packet().unwrap());
|
||||
assert_eq!(packets[2], [], "TX ZLP progresses without the RX consumer");
|
||||
|
||||
let sent = exchange(host, usb, Some(after));
|
||||
assert!(sent.accepted);
|
||||
assert!(
|
||||
!sent.pending,
|
||||
"TX admission's IoBatch flag is not the IRQ policy"
|
||||
);
|
||||
assert!(
|
||||
!host.rx_interrupt_enabled(),
|
||||
"TX must preserve the actual unread RX gate"
|
||||
);
|
||||
if let Some(packet) = host.take_packet() {
|
||||
packets.push(packet);
|
||||
}
|
||||
|
||||
let partial = exchange(host, usb, None);
|
||||
assert!(partial.usb.is_none());
|
||||
assert!(!partial.pending);
|
||||
assert!(
|
||||
host.rx_interrupt_enabled(),
|
||||
"the partial framer is waiting for more bytes, not the worker"
|
||||
);
|
||||
|
||||
let mut ready = false;
|
||||
let mut frames = Vec::new();
|
||||
for _ in 0..64 {
|
||||
step(host, usb, &mut ready, &mut frames);
|
||||
if let Some(packet) = host.take_packet() {
|
||||
packets.push(packet);
|
||||
}
|
||||
}
|
||||
assert_eq!(frames, [incoming, first, last]);
|
||||
assert_eq!(
|
||||
packets.iter().map(Vec::len).collect::<Vec<_>>(),
|
||||
[64, 64, 0, 13]
|
||||
);
|
||||
assert_eq!(
|
||||
packets.concat(),
|
||||
[outgoing.raw_bytes(), after.raw_bytes()].concat()
|
||||
);
|
||||
assert!(host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_poll_refill_stays_masked_and_rearm_preserves_already_pending_irq() {
|
||||
with_link(|host, usb| {
|
||||
let frame = MavlinkEncoder::new(42, 7).heartbeat();
|
||||
host.write_packet(&frame.raw_bytes()[..8]);
|
||||
assert!(host.enter_irq());
|
||||
usb.with_rx_interrupt_gate(
|
||||
|usb| {
|
||||
assert!(usb.poll());
|
||||
// Latch NVIC pending just before masking the peripheral source.
|
||||
host.write_packet(&frame.raw_bytes()[8..]);
|
||||
},
|
||||
|enabled| host.set_rx_interrupt_enabled(enabled),
|
||||
);
|
||||
let partial = exchange(host, usb, None);
|
||||
assert!(partial.usb.is_none());
|
||||
assert!(partial.pending, "the final poll refilled rx_remaining");
|
||||
assert!(!host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
let complete = exchange(host, usb, None);
|
||||
assert_eq!(complete.usb.unwrap().raw(), &frame);
|
||||
assert!(!complete.pending);
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
assert!(
|
||||
host.enter_irq(),
|
||||
"RX rearm preserves latched NVIC pending even after polling consumed the source"
|
||||
);
|
||||
assert!(!poll_irq(host, usb));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrivals_on_either_side_of_rearm_are_not_lost() {
|
||||
for before in [true, false] {
|
||||
with_link(|host, usb| {
|
||||
let mut encoder = MavlinkEncoder::new(42, 7);
|
||||
let first = encoder.heartbeat();
|
||||
let next = encoder.heartbeat();
|
||||
host.write_packet(first.raw_bytes());
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
let batch = exchange_usb(usb, None, |enabled| {
|
||||
assert!(enabled);
|
||||
if before {
|
||||
host.write_packet(next.raw_bytes());
|
||||
}
|
||||
host.set_rx_interrupt_enabled(enabled);
|
||||
if !before {
|
||||
host.write_packet(next.raw_bytes());
|
||||
}
|
||||
});
|
||||
assert_eq!(batch.usb.unwrap().raw(), &first);
|
||||
assert!(!batch.pending);
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
assert_eq!(exchange(host, usb, None).usb.unwrap().raw(), &next);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backpressure_keeps_reset_suspend_and_resume_interrupts_serviceable() {
|
||||
for reset in [true, false] {
|
||||
with_link(|host, usb| {
|
||||
let frame = file_frame(130);
|
||||
assert!(exchange(host, usb, Some(frame)).accepted);
|
||||
for _ in 0..3 {
|
||||
host.write_packet(&frame.raw_bytes()[..8]);
|
||||
}
|
||||
assert!(host.enter_irq());
|
||||
assert!(poll_irq(host, usb));
|
||||
assert!(host.enter_irq(), "one already-pending exception is allowed");
|
||||
assert!(poll_irq(host, usb));
|
||||
assert_eq!(host.receive_backlog(), (1, true));
|
||||
assert!(!host.enter_irq());
|
||||
let (_, original_mask) = host.interrupt_status();
|
||||
host.event(if reset {
|
||||
PollResult::Reset
|
||||
} else {
|
||||
PollResult::Suspend
|
||||
});
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert!(!usb.is_open());
|
||||
assert!(host.events_empty());
|
||||
assert!(host.nvic_enabled());
|
||||
assert_eq!(
|
||||
host.interrupt_status().1 & !(1 << 4),
|
||||
original_mask & !(1 << 4)
|
||||
);
|
||||
if reset {
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
host.configure(usb, true);
|
||||
host.dtr(usb, true);
|
||||
assert!(
|
||||
host.collect_packets(usb).is_empty(),
|
||||
"reset discarded the old TX queue"
|
||||
);
|
||||
} else {
|
||||
assert!(!host.rx_interrupt_enabled(), "suspend retains unread RX");
|
||||
assert_eq!(host.receive_backlog(), (1, true));
|
||||
host.event(PollResult::Resume);
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert!(usb.is_open());
|
||||
assert_eq!(host.collect_packets(usb).concat(), frame.raw_bytes());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ep0_setup_cannot_bypass_a_blocked_bulk_fifo_head() {
|
||||
with_link(|host, usb| {
|
||||
let frame = MavlinkEncoder::new(42, 7).heartbeat();
|
||||
assert!(exchange(host, usb, Some(frame)).accepted);
|
||||
for _ in 0..3 {
|
||||
host.write_packet(frame.raw_bytes());
|
||||
}
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert_eq!(host.receive_backlog(), (1, true));
|
||||
assert!(!host.enter_irq());
|
||||
host.queue_control([0x21, 0x22, 0, 0, 0, 0, 0, 0]); // DTR close behind bulk
|
||||
assert_eq!(host.receive_backlog(), (2, true));
|
||||
assert!(
|
||||
!host.enter_irq(),
|
||||
"EP0 arrival also uses the masked RX source"
|
||||
);
|
||||
let (raw, mask) = host.interrupt_status();
|
||||
assert_ne!(raw & (1 << 4), 0);
|
||||
assert_eq!(raw & mask, 0);
|
||||
host.take_packet().unwrap(); // IN completion can interrupt and poll raw RX.
|
||||
assert!(host.enter_irq());
|
||||
poll_irq(host, usb);
|
||||
assert_eq!(host.receive_backlog(), (2, true));
|
||||
assert!(
|
||||
usb.is_open(),
|
||||
"SETUP cannot jump over the blocked FIFO head"
|
||||
);
|
||||
assert_eq!(exchange(host, usb, None).usb.unwrap().raw(), &frame);
|
||||
exchange(host, usb, None);
|
||||
assert!(!usb.is_open(), "consumer progress finally allows DTR close");
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_fifo_backpressures_host_then_consumer_progress_allows_retry() {
|
||||
with_link(|host, usb| {
|
||||
let frame = file_frame(64);
|
||||
for _ in 0..3 {
|
||||
assert!(host.try_write_packet(frame.raw_bytes()));
|
||||
}
|
||||
assert!(!host.try_write_packet(frame.raw_bytes()));
|
||||
let mut ready = false;
|
||||
let mut frames = Vec::new();
|
||||
for _ in 0..32 {
|
||||
step(host, usb, &mut ready, &mut frames);
|
||||
}
|
||||
assert_eq!(frames, [frame; 3]);
|
||||
assert!(host.try_write_packet(frame.raw_bytes()));
|
||||
for _ in 0..16 {
|
||||
step(host, usb, &mut ready, &mut frames);
|
||||
}
|
||||
assert_eq!(frames, [frame; 4]);
|
||||
assert_eq!(host.receive_backlog(), (0, false));
|
||||
assert!(host.rx_interrupt_enabled());
|
||||
assert!(host.nvic_enabled());
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user