//! 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, ) -> 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, ) -> 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::>(), [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::>(), [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()); }); }