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,407 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in new issue
Block a user