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.
408 lines
14 KiB
Rust
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);
|
|
}
|