Files
kaptan/firmware/platforms/stm32f7/tests/support/usb_bus.rs
T
Onur Özkan f9a61a086f 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.
2026-11-07 18:12:11 +03:00

408 lines
14 KiB
Rust

//! Shared packet-level USB hardware fake; runs the real CDC/USB stack.
extern crate std;
use super::UsbMavlink;
use std::{
collections::VecDeque,
sync::{Arc, Mutex},
vec::Vec,
};
use usb_device::{
UsbDirection, UsbError,
bus::{PollResult, UsbBus, UsbBusAllocator},
device::{UsbDeviceBuilder, UsbVidPid},
endpoint::{EndpointAddress, EndpointType},
};
use usbd_serial::CdcAcmClass;
const RXFLVL: u32 = 1 << 4;
const USBSUSP: u32 = 1 << 11;
const USBRST: u32 = 1 << 12;
const ENUMDNE: u32 = 1 << 13;
const IEPINT: u32 = 1 << 18;
const WKUPINT: u32 = 1 << 31;
struct RxPacket {
endpoint: usize,
setup: bool,
bytes: Vec<u8>,
}
#[derive(Default)]
struct BusState {
offline: bool,
fail_poll: bool,
read_error: Option<UsbError>,
write_error: Option<UsbError>,
events: VecDeque<PollResult>,
input: [Option<Vec<u8>>; 4],
output: [Option<Vec<u8>>; 4],
sizes: [[u16; 4]; 2],
stalled: [[bool; 4]; 2],
bulk_in: usize,
bulk_out: usize,
// Hardware FIFO is distinct from the driver's one-packet endpoint buffer
// (`output`). RXFLVL stays asserted if that buffer prevents popping the FIFO.
rx_fifo: VecDeque<RxPacket>,
setup: u16,
gintmsk: u32,
nvic_enabled: bool,
nvic_pending: bool,
endpoints_configured: bool,
}
impl BusState {
fn raw_status(&self) -> u32 {
self.events.iter().fold(
if self.rx_fifo.is_empty() { 0 } else { RXFLVL },
|status, event| {
status
| match event {
// synopsys-usb-otg returns Reset only for ENUMDNE.
PollResult::Reset => ENUMDNE,
PollResult::Suspend => USBSUSP,
PollResult::Resume => WKUPINT,
PollResult::Data { ep_in_complete, .. } if *ep_in_complete != 0 => IEPINT,
_ => 0,
}
},
)
}
fn latch_interrupt(&mut self) {
self.nvic_pending |= self.raw_status() & self.gintmsk != 0;
}
}
#[derive(Clone, Default)]
pub(crate) struct Host(Arc<Mutex<BusState>>);
pub(crate) struct PacketBus(Arc<Mutex<BusState>>);
impl super::UsbBusHealth for PacketBus {
fn is_offline(&self) -> bool {
self.0.lock().unwrap().offline
}
fn disable(&self) {
let mut state = self.0.lock().unwrap();
state.offline = true;
state.nvic_enabled = false;
state.nvic_pending = false;
}
}
impl UsbBus for PacketBus {
fn alloc_ep(
&mut self,
direction: UsbDirection,
address: Option<EndpointAddress>,
kind: EndpointType,
size: u16,
_: u8,
) -> Result<EndpointAddress, UsbError> {
let mut state = self.0.lock().unwrap();
let sizes = &mut state.sizes[usize::from(direction == UsbDirection::In)];
let index = match address {
Some(address) => address.index(),
None => (1..sizes.len())
.find(|index| sizes[*index] == 0)
.ok_or(UsbError::EndpointOverflow)?,
};
if index >= sizes.len() || sizes[index] != 0 {
return Err(UsbError::InvalidEndpoint);
}
sizes[index] = size;
if kind == EndpointType::Bulk {
match direction {
UsbDirection::In => state.bulk_in = index,
UsbDirection::Out => state.bulk_out = index,
}
}
Ok(EndpointAddress::from_parts(index, direction))
}
fn enable(&mut self) {
let mut state = self.0.lock().unwrap();
state.gintmsk = RXFLVL | USBSUSP | USBRST | ENUMDNE | IEPINT | WKUPINT;
state.nvic_enabled = true;
}
fn reset(&self) {
let mut state = self.0.lock().unwrap();
state.input.fill(None);
// configure_all flushes FIFOs but does not reset the bulk OUT software
// EndpointBuffer::has_data. Transport cleanup must discard that packet.
let bulk_out = state.bulk_out;
let buffered = state.output[bulk_out].take();
state.output.fill(None);
state.output[bulk_out] = buffered;
state.events.clear();
state.rx_fifo.clear();
state.setup = 0;
state.endpoints_configured = true;
}
fn set_device_address(&self, _: u8) {}
fn write(&self, address: EndpointAddress, bytes: &[u8]) -> Result<usize, UsbError> {
let mut state = self.0.lock().unwrap();
assert!(!state.offline, "write after terminal USB fault");
if let Some(error) = state.write_error.take() {
return Err(error);
}
if bytes.len() > usize::from(state.sizes[1][address.index()]) {
return Err(UsbError::BufferOverflow);
}
let packet = &mut state.input[address.index()];
if packet.is_some() {
return Err(UsbError::WouldBlock);
}
*packet = Some(bytes.to_vec());
Ok(bytes.len())
}
fn read(&self, address: EndpointAddress, bytes: &mut [u8]) -> Result<usize, UsbError> {
let mut state = self.0.lock().unwrap();
assert!(!state.offline, "read after terminal USB fault");
if let Some(error) = state.read_error.take() {
return Err(error);
}
let packet = state.output[address.index()]
.as_ref()
.ok_or(UsbError::WouldBlock)?;
if packet.len() > bytes.len() {
return Err(UsbError::BufferOverflow);
}
let packet = state.output[address.index()].take().unwrap();
state.setup &= !(1 << address.index());
bytes[..packet.len()].copy_from_slice(&packet);
Ok(packet.len())
}
fn set_stalled(&self, address: EndpointAddress, stalled: bool) {
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()] = stalled;
}
fn is_stalled(&self, address: EndpointAddress) -> bool {
self.0.lock().unwrap().stalled[usize::from(address.is_in())][address.index()]
}
fn suspend(&self) {}
fn resume(&self) {}
fn poll(&self) -> PollResult {
let mut state = self.0.lock().unwrap();
assert!(!state.offline, "poll after terminal USB fault");
if state.fail_poll {
state.offline = true;
state.nvic_enabled = false;
state.nvic_pending = false;
return PollResult::None;
}
// Like synopsys-usb-otg 0.4.0, polling reads raw status, not GINTMSK.
// Non-RX sources remain serviceable even with a blocked FIFO head.
let mut ep_in_complete = 0;
if let Some(event) = state.events.pop_front() {
match event {
PollResult::Data {
ep_in_complete: complete,
..
} => ep_in_complete = complete,
_ => return event,
}
}
if let Some(packet) = state.rx_fifo.front()
&& state.output[packet.endpoint].is_none()
{
let packet = state.rx_fifo.pop_front().unwrap();
if packet.setup {
state.setup |= 1 << packet.endpoint;
}
state.output[packet.endpoint] = Some(packet.bytes);
}
let ep_setup = state.setup;
let ep_out = state
.output
.iter()
.enumerate()
.fold(0, |mask, (index, packet)| {
mask | if packet.is_some() { 1 << index } else { 0 }
})
& !ep_setup;
// A still-asserted level can latch before the following gate write.
state.latch_interrupt();
if ep_out | ep_in_complete | ep_setup != 0 {
PollResult::Data {
ep_out,
ep_in_complete,
ep_setup,
}
} else {
PollResult::None
}
}
}
impl Host {
pub(crate) fn fail_poll(&self) {
self.0.lock().unwrap().fail_poll = true;
}
pub(crate) fn fail_read(&self) {
self.0.lock().unwrap().read_error = Some(UsbError::InvalidState);
}
pub(crate) fn fail_write(&self) {
self.0.lock().unwrap().write_error = Some(UsbError::InvalidState);
}
pub(crate) fn enumeration_done(&self) {
let mut state = self.0.lock().unwrap();
state.events.push_front(PollResult::Reset);
state.latch_interrupt();
}
pub(crate) fn event(&self, event: PollResult) {
let mut state = self.0.lock().unwrap();
state.events.push_back(event);
state.latch_interrupt();
}
#[allow(dead_code)]
pub(crate) fn events_empty(&self) -> bool {
self.0.lock().unwrap().events.is_empty()
}
fn control(&self, link: &mut UsbMavlink<'_, PacketBus>, request: [u8; 8]) {
self.queue_control(request);
link.poll();
assert_eq!(self.take_input(0), Some(Vec::new()));
link.poll();
}
pub(crate) fn queue_control(&self, request: [u8; 8]) {
let mut state = self.0.lock().unwrap();
// EP0 shares the hardware FIFO. Never inject SETUP past a blocked bulk head.
state.rx_fifo.push_back(RxPacket {
endpoint: 0,
setup: true,
bytes: request.to_vec(),
});
state.latch_interrupt();
}
pub(crate) fn configure(&self, link: &mut UsbMavlink<'_, PacketBus>, configured: bool) {
self.control(link, [0, 9, u8::from(configured), 0, 0, 0, 0, 0]);
}
pub(crate) fn dtr(&self, link: &mut UsbMavlink<'_, PacketBus>, open: bool) {
self.control(link, [0x21, 0x22, u8::from(open), 0, 0, 0, 0, 0]);
}
pub(crate) fn write_packet(&self, bytes: &[u8]) {
assert!(
self.try_write_packet(bytes),
"host must retry when the RX FIFO is full"
);
}
pub(crate) fn try_write_packet(&self, bytes: &[u8]) -> bool {
assert!(bytes.len() <= 64);
let mut state = self.0.lock().unwrap();
// Driver allocation: two 16-word OUT buffers + 30 words. Model packet
// data and one receive status word, not cycle-level USB handshakes.
let occupied: usize = state
.rx_fifo
.iter()
.map(|packet| packet.bytes.len().div_ceil(4) + 1)
.sum();
if occupied + bytes.len().div_ceil(4) + 1 > 62 {
return false;
}
let endpoint = state.bulk_out;
state.rx_fifo.push_back(RxPacket {
endpoint,
setup: false,
bytes: bytes.to_vec(),
});
state.latch_interrupt();
true
}
fn take_input(&self, endpoint: usize) -> Option<Vec<u8>> {
let mut state = self.0.lock().unwrap();
let packet = state.input[endpoint].take()?;
state.events.push_back(PollResult::Data {
ep_out: 0,
ep_in_complete: 1 << endpoint,
ep_setup: 0,
});
state.latch_interrupt();
Some(packet)
}
pub(crate) fn set_rx_interrupt_enabled(&self, enabled: bool) {
let mut state = self.0.lock().unwrap();
state.gintmsk = if enabled {
state.gintmsk | RXFLVL
} else {
state.gintmsk & !RXFLVL
};
// Masking never acknowledges pending status. Rearm sees the current level.
state.latch_interrupt();
}
#[allow(dead_code)] // Also shared with transport-only tests without a scheduler.
pub(crate) fn rx_interrupt_enabled(&self) -> bool {
self.0.lock().unwrap().gintmsk & RXFLVL != 0
}
#[allow(dead_code)]
pub(crate) fn nvic_enabled(&self) -> bool {
self.0.lock().unwrap().nvic_enabled
}
#[allow(dead_code)]
pub(crate) fn interrupt_status(&self) -> (u32, u32) {
let state = self.0.lock().unwrap();
(state.raw_status(), state.gintmsk)
}
#[allow(dead_code)]
pub(crate) fn enter_irq(&self) -> bool {
let mut state = self.0.lock().unwrap();
state.latch_interrupt();
if !state.nvic_enabled || !state.nvic_pending {
return false;
}
state.nvic_pending = false; // Exception entry, not an enable/disable side effect.
true
}
#[allow(dead_code)]
pub(crate) fn receive_backlog(&self) -> (usize, bool) {
let state = self.0.lock().unwrap();
(state.rx_fifo.len(), state.output[state.bulk_out].is_some())
}
/// Host ACK without polling device software. The device must service TX completion.
pub(crate) fn take_packet(&self) -> Option<Vec<u8>> {
let state = self.0.lock().unwrap();
if !state.endpoints_configured {
return None;
}
let endpoint = state.bulk_in;
drop(state);
self.take_input(endpoint)
}
pub(crate) fn collect_packets(&self, link: &mut UsbMavlink<'_, PacketBus>) -> Vec<Vec<u8>> {
let mut packets = Vec::new();
for _ in 0..16 {
let Some(packet) = self.take_packet() else {
return packets;
};
packets.push(packet);
link.poll();
}
panic!("transmission did not finish");
}
}
pub(crate) fn with_link(test: impl FnOnce(&Host, &mut UsbMavlink<'_, PacketBus>)) {
let host = Host::default();
let allocator = UsbBusAllocator::new(PacketBus(host.0.clone()));
let class = CdcAcmClass::new(&allocator, 64);
let device = UsbDeviceBuilder::new(&allocator, UsbVidPid(0x1209, 1))
.max_packet_size_0(64)
.unwrap()
.build();
let mut link = UsbMavlink::new(device, class);
host.enumeration_done();
link.poll();
host.configure(&mut link, true);
host.dtr(&mut link, true);
link.with_rx_interrupt_gate(
|link| assert!(link.is_open()),
|enabled| host.set_rx_interrupt_enabled(enabled),
);
// Finish initialization's latched exceptions before scheduling test traffic.
while host.enter_irq() {
link.with_rx_interrupt_gate(
|link| link.poll(),
|enabled| host.set_rx_interrupt_enabled(enabled),
);
}
test(&host, &mut link);
}