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.
392 lines
14 KiB
Rust
392 lines
14 KiB
Rust
//! 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());
|
|
});
|
|
}
|