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
@@ -57,23 +57,31 @@ impl EnqueueResult {
|
||||
/// Result of attempting to begin a transfer.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum StartOutcome {
|
||||
/// No pending frame was available.
|
||||
/// No transfer started.
|
||||
Idle,
|
||||
/// The engine accepted a frame and owns it until a terminal outcome.
|
||||
/// The transport accepted a frame, which the queue retains until completion or failure.
|
||||
Started,
|
||||
}
|
||||
|
||||
/// Bounded admission and exactly-one-active-transfer state.
|
||||
///
|
||||
/// Pending best-effort traffic is replaceable; active traffic is never
|
||||
/// replaceable. A selected frame moves into its final active storage before
|
||||
/// the engine sees its address. A rejected start moves it back to the
|
||||
/// reservation, so a failed submission cannot manufacture a false in-flight
|
||||
/// frame or discard the selected frame.
|
||||
/// replaceable. The queue retains the selected frame as logically active until
|
||||
/// completion or failure. A rejected start returns it to the reservation for
|
||||
/// retry without discarding it or claiming an active transfer.
|
||||
///
|
||||
/// The queue is movable and provides no stable address for DMA. Transports may
|
||||
/// copy bytes during a callback or borrow the active frame for bounded writes.
|
||||
/// Asynchronous device access requires separately owned, address-stable storage.
|
||||
pub struct TransmitQueue<Frame, const N: usize> {
|
||||
pending: [Option<PendingFrame<Frame>>; N],
|
||||
reserved: Option<PendingFrame<Frame>>,
|
||||
active: Option<PendingFrame<Frame>>,
|
||||
selected: SelectedFrame<Frame>,
|
||||
}
|
||||
|
||||
enum SelectedFrame<Frame> {
|
||||
Idle,
|
||||
Reserved(PendingFrame<Frame>),
|
||||
Active(PendingFrame<Frame>),
|
||||
}
|
||||
|
||||
impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
@@ -81,8 +89,7 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
pending: [None; N],
|
||||
reserved: None,
|
||||
active: None,
|
||||
selected: SelectedFrame::Idle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,43 +130,46 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
}
|
||||
|
||||
/// Selects and starts at most one pending frame.
|
||||
///
|
||||
/// The callback borrows the frame only for this call. Success retains logical
|
||||
/// active ownership, not an address or a borrow for later device access.
|
||||
pub fn start_with<Error>(
|
||||
&mut self,
|
||||
start: impl FnOnce(&Frame) -> Result<(), Error>,
|
||||
) -> Result<StartOutcome, Error> {
|
||||
if self.active.is_some() {
|
||||
return Ok(StartOutcome::Idle);
|
||||
}
|
||||
if self.reserved.is_none() {
|
||||
self.reserved = self.take_next();
|
||||
}
|
||||
self.active = self.reserved.take();
|
||||
|
||||
let Some(active) = self.active.as_ref() else {
|
||||
return Ok(StartOutcome::Idle);
|
||||
let frame = match self.selected {
|
||||
SelectedFrame::Active(_) => return Ok(StartOutcome::Idle),
|
||||
SelectedFrame::Reserved(frame) => frame,
|
||||
SelectedFrame::Idle => match self.take_next() {
|
||||
Some(frame) => frame,
|
||||
None => return Ok(StartOutcome::Idle),
|
||||
},
|
||||
};
|
||||
if let Err(error) = start(&active.frame) {
|
||||
self.reserved = self.active.take();
|
||||
self.selected = SelectedFrame::Active(frame);
|
||||
if let Err(error) = start(&frame.frame) {
|
||||
self.selected = SelectedFrame::Reserved(frame);
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
self.reserved = None;
|
||||
Ok(StartOutcome::Started)
|
||||
}
|
||||
|
||||
/// Releases the active frame after the transport no longer borrows it.
|
||||
/// Releases the logically active frame after transport completion.
|
||||
pub fn complete(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
self.active.take()
|
||||
let SelectedFrame::Active(frame) = self.selected else {
|
||||
return None;
|
||||
};
|
||||
self.selected = SelectedFrame::Idle;
|
||||
Some(frame)
|
||||
}
|
||||
|
||||
/// Releases the active frame after a terminal transport fault.
|
||||
pub fn fail(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
self.active.take()
|
||||
self.complete()
|
||||
}
|
||||
|
||||
/// Returns whether an engine owns a frame.
|
||||
/// Returns whether the queue retains a logically active frame.
|
||||
pub const fn is_active(&self) -> bool {
|
||||
self.active.is_some()
|
||||
matches!(self.selected, SelectedFrame::Active(_))
|
||||
}
|
||||
|
||||
/// Borrows the frame currently owned by the active transport operation.
|
||||
@@ -167,7 +177,10 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
/// Packetized transports use this to retain one selected frame while
|
||||
/// advancing it across multiple bounded endpoint writes.
|
||||
pub const fn active(&self) -> Option<&PendingFrame<Frame>> {
|
||||
self.active.as_ref()
|
||||
match &self.selected {
|
||||
SelectedFrame::Active(frame) => Some(frame),
|
||||
SelectedFrame::Idle | SelectedFrame::Reserved(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn take_next(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
@@ -194,6 +207,8 @@ impl<Frame: Copy, const N: usize> Default for TransmitQueue<Frame, N> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate std;
|
||||
|
||||
use super::{EnqueueResult, FrameClass, PendingFrame, StartOutcome, TransmitQueue};
|
||||
|
||||
#[test]
|
||||
@@ -352,7 +367,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_start_without_claiming_active_ownership() {
|
||||
fn failed_start_retains_selection() {
|
||||
let mut queue = TransmitQueue::<u8, 1>::new();
|
||||
queue.enqueue(PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
@@ -361,6 +376,16 @@ mod tests {
|
||||
|
||||
assert_eq!(queue.start_with(|_| Err::<(), _>(1)), Err(1));
|
||||
assert!(!queue.is_active());
|
||||
assert_eq!(queue.active(), None);
|
||||
assert_eq!(queue.complete(), None);
|
||||
assert_eq!(queue.fail(), None);
|
||||
assert_eq!(
|
||||
queue.enqueue(PendingFrame {
|
||||
class: FrameClass::Vital,
|
||||
frame: 8,
|
||||
}),
|
||||
EnqueueResult::Enqueued
|
||||
);
|
||||
assert_eq!(
|
||||
queue.start_with(|frame| {
|
||||
assert_eq!(*frame, 7);
|
||||
@@ -368,31 +393,80 @@ mod tests {
|
||||
}),
|
||||
Ok(StartOutcome::Started)
|
||||
);
|
||||
assert_eq!(queue.complete().unwrap().frame, 7);
|
||||
queue
|
||||
.start_with(|frame| {
|
||||
assert_eq!(*frame, 8);
|
||||
Ok::<_, ()>(())
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_the_dma_source_at_its_active_address() {
|
||||
let mut queue = TransmitQueue::<[u8; 2], 1>::new();
|
||||
fn panicking_start_retains_active() {
|
||||
let mut queue = TransmitQueue::<u8, 1>::new();
|
||||
queue.enqueue(PendingFrame {
|
||||
class: FrameClass::Critical,
|
||||
frame: 7,
|
||||
});
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _ = queue.start_with::<()>(|_| panic!("transport start panicked"));
|
||||
}));
|
||||
assert!(result.is_err());
|
||||
assert_eq!(queue.active().unwrap().frame, 7);
|
||||
assert_eq!(
|
||||
queue.start_with::<()>(|_| panic!("active transfer cannot restart")),
|
||||
Ok(StartOutcome::Idle)
|
||||
);
|
||||
assert_eq!(queue.fail().unwrap().frame, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retains_active_contents_across_pending_admission_and_queue_move() {
|
||||
let mut queue = TransmitQueue::<[u8; 2], 1>::new();
|
||||
let active = PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: [1, 2],
|
||||
});
|
||||
};
|
||||
queue.enqueue(active);
|
||||
|
||||
let mut submitted_address = 0;
|
||||
assert_eq!(
|
||||
queue.start_with(|frame| {
|
||||
submitted_address = frame.as_ptr() as usize;
|
||||
assert_eq!(*frame, active.frame);
|
||||
Ok::<_, ()>(())
|
||||
}),
|
||||
Ok(StartOutcome::Started)
|
||||
);
|
||||
let active_address = queue
|
||||
.active
|
||||
.as_ref()
|
||||
.expect("successful start must retain the active frame")
|
||||
.frame
|
||||
.as_ptr() as usize;
|
||||
assert_eq!(
|
||||
queue.enqueue(PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: [3, 4],
|
||||
}),
|
||||
EnqueueResult::Enqueued
|
||||
);
|
||||
let pending = PendingFrame {
|
||||
class: FrameClass::Critical,
|
||||
frame: [5, 6],
|
||||
};
|
||||
assert_eq!(queue.enqueue(pending), EnqueueResult::ReplacedBestEffort);
|
||||
assert_eq!(queue.active(), Some(&active));
|
||||
|
||||
assert_eq!(submitted_address, active_address);
|
||||
let mut moved = core::hint::black_box(queue);
|
||||
assert!(moved.is_active());
|
||||
assert_eq!(moved.active(), Some(&active));
|
||||
assert_eq!(
|
||||
moved.start_with(|_| panic!("active frame must finish before another start")),
|
||||
Ok::<_, ()>(StartOutcome::Idle)
|
||||
);
|
||||
assert_eq!(moved.complete(), Some(active));
|
||||
assert_eq!(moved.active(), None);
|
||||
assert_eq!(
|
||||
moved.start_with(|frame| {
|
||||
assert_eq!(*frame, pending.frame);
|
||||
Ok::<_, ()>(())
|
||||
}),
|
||||
Ok(StartOutcome::Started)
|
||||
);
|
||||
assert_eq!(moved.complete(), Some(pending));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user