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
@@ -1,162 +1,212 @@
|
||||
//! F722 transport and vertical-route adapters for the portable data channel.
|
||||
|
||||
use super::peripherals;
|
||||
use kaptan_dma::TxCompletion;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame, receiver::IntegrityCheckedFrame, transmitter::MavlinkTransmitter,
|
||||
//! Independent per-link scheduling, encoding, framing and bounded transmission.
|
||||
use super::{
|
||||
peripherals::{self, F722UsbMavlink, OwnedUart, ReceiveInterrupt},
|
||||
serial::{ReceiveEvent, RxChunk, SerialDevice, SerialIo},
|
||||
};
|
||||
use crate::SerialInventory;
|
||||
use kaptan_data_channel::{DataChannel, DataChannelRates};
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
receiver::{IntegrityCheckedFrame, IntegrityMavlinkReceiver},
|
||||
};
|
||||
use kaptan_ports::PortId;
|
||||
use kaptan_tx::PendingFrame;
|
||||
use peripherals::F722UsbMavlink;
|
||||
|
||||
// Baud, sensor topology, and mounting remain immutable build-time policy. The
|
||||
// output-inhibited board image may recover endpoint identity and selected
|
||||
// operational rates before this channel is constructed.
|
||||
pub(crate) const UART1_CLOCK_HZ: u32 = 108_000_000;
|
||||
pub(crate) const UART2_CLOCK_HZ: u32 = 54_000_000;
|
||||
pub(crate) const SERVICE_HZ: usize = 100;
|
||||
|
||||
/// Fixed service-timer configuration for the validated Pll216 profile.
|
||||
pub(crate) const SERVICE_TIMER_CONFIG: peripherals::Tim2PeriodicConfig =
|
||||
peripherals::Tim2PeriodicConfig {
|
||||
prescaler: 10_799,
|
||||
auto_reload: 99,
|
||||
};
|
||||
|
||||
/// The two F722 DMA engines behind the portable data-channel transport.
|
||||
enum F722MavlinkTransport {
|
||||
Usart1(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart1TxDma, 4>),
|
||||
Usart2(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart2TxDma, 4>),
|
||||
pub(crate) struct Link {
|
||||
pub(crate) channel: DataChannel<SERVICE_HZ>,
|
||||
pub(crate) receiver: IntegrityMavlinkReceiver<2>,
|
||||
}
|
||||
impl Link {
|
||||
pub(crate) fn new(
|
||||
system: u8,
|
||||
component: u8,
|
||||
rates: DataChannelRates,
|
||||
) -> Result<Self, crate::BoardError> {
|
||||
Ok(Self {
|
||||
channel: DataChannel::new(system, component, rates, 100_000)
|
||||
.map_err(|_| crate::BoardError::Initialization)?,
|
||||
receiver: IntegrityMavlinkReceiver::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Physical link on which an inbound frame arrived.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum MavlinkIngress {
|
||||
Uart,
|
||||
Usb,
|
||||
impl SerialDevice for OwnedUart {
|
||||
fn receive(&mut self) -> ReceiveEvent {
|
||||
match OwnedUart::receive(self) {
|
||||
ReceiveInterrupt::Byte(byte) => ReceiveEvent::Byte(byte),
|
||||
ReceiveInterrupt::Fault => ReceiveEvent::Fault,
|
||||
ReceiveInterrupt::NotPending => ReceiveEvent::Idle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Concrete F722 MAVLink link ownership.
|
||||
pub(crate) struct F722MavlinkLink {
|
||||
uart: F722MavlinkTransport,
|
||||
/// Bounded data exchanged between the worker and I/O owners. Decoding,
|
||||
/// scheduling and persistence cannot borrow the physical owners through this API.
|
||||
#[derive(Default)]
|
||||
pub struct IoBatch {
|
||||
pub(crate) serial: RxChunk,
|
||||
pub(crate) usb: Option<IntegrityCheckedFrame>,
|
||||
pub(crate) accepted: bool,
|
||||
pub(crate) pending: bool,
|
||||
}
|
||||
impl<I: SerialDevice> SerialIo<I> {
|
||||
pub fn exchange(&mut self, frame: Option<PendingFrame<MavlinkFrame>>) -> IoBatch {
|
||||
match frame {
|
||||
Some(frame) => IoBatch {
|
||||
accepted: self.send(frame),
|
||||
..IoBatch::default()
|
||||
},
|
||||
None => {
|
||||
let serial = self.take_chunk();
|
||||
IoBatch {
|
||||
pending: serial.pending,
|
||||
serial,
|
||||
..IoBatch::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One short-lock RTIC resource for the board's declared UARTs. All UART IRQs
|
||||
/// have priority 14. Protocol work stays outside this bounded I/O handoff.
|
||||
pub struct PortIo<const N: usize> {
|
||||
inventory: SerialInventory,
|
||||
pub(crate) ports: [SerialIo<OwnedUart>; N],
|
||||
}
|
||||
impl<const N: usize> PortIo<N> {
|
||||
pub(crate) fn new(inventory: SerialInventory) -> Self {
|
||||
assert_eq!(inventory.len(), N, "validated board port count");
|
||||
Self {
|
||||
inventory,
|
||||
ports: core::array::from_fn(|_| SerialIo::new(None, false)),
|
||||
}
|
||||
}
|
||||
pub fn on_interrupt(&mut self, id: PortId, now: kaptan_time::Timestamp) -> bool {
|
||||
self.inventory
|
||||
.index(id)
|
||||
.is_some_and(|index| self.ports[index].on_interrupt(now))
|
||||
}
|
||||
pub fn exchange(&mut self, id: PortId, frame: Option<PendingFrame<MavlinkFrame>>) -> IoBatch {
|
||||
self.inventory
|
||||
.index(id)
|
||||
.map_or_else(IoBatch::default, |index| self.ports[index].exchange(frame))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UsbIo {
|
||||
usb: F722UsbMavlink,
|
||||
rx_interrupt: RxFifoInterruptGate,
|
||||
}
|
||||
impl UsbIo {
|
||||
pub(crate) fn new(usb: F722UsbMavlink, rx_interrupt: RxFifoInterruptGate) -> Self {
|
||||
Self { usb, rx_interrupt }
|
||||
}
|
||||
pub fn on_interrupt(&mut self) -> bool {
|
||||
self.usb.with_rx_interrupt_gate(
|
||||
|usb| usb.poll(),
|
||||
|enabled| self.rx_interrupt.set_enabled(enabled),
|
||||
)
|
||||
}
|
||||
pub fn exchange(&mut self, frame: Option<PendingFrame<MavlinkFrame>>) -> IoBatch {
|
||||
exchange_usb(&mut self.usb, frame, |enabled| {
|
||||
self.rx_interrupt.set_enabled(enabled)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl F722MavlinkLink {
|
||||
pub(crate) fn usart1(
|
||||
rcc: &stm32f7::stm32f722::RCC,
|
||||
uart: stm32f7::stm32f722::USART1,
|
||||
baud: u32,
|
||||
usb: F722UsbMavlink,
|
||||
) -> Result<Self, ()> {
|
||||
let uart = peripherals::Usart1Tx::new(rcc, uart, UART1_CLOCK_HZ, baud).map_err(|_| ())?;
|
||||
// SAFETY: platform initialization disables D-cache before this object
|
||||
// is created, and the active MAVLink frame remains queue-owned until
|
||||
// this platform receives a terminal DMA completion notification.
|
||||
let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() };
|
||||
Ok(Self {
|
||||
uart: F722MavlinkTransport::Usart1(MavlinkTransmitter::new(
|
||||
memory,
|
||||
peripherals::Usart1TxDma::new(rcc, uart),
|
||||
)),
|
||||
usb,
|
||||
})
|
||||
/// Private F722 capability delegated alongside the UsbPeripheral in its factory.
|
||||
/// The USB I/O owner retains it at runtime. No register-block reference is made.
|
||||
pub(crate) struct RxFifoInterruptGate {
|
||||
_private: (),
|
||||
}
|
||||
impl RxFifoInterruptGate {
|
||||
pub(crate) fn new(_global: &stm32f7::stm32f722::OTG_FS_GLOBAL) -> Self {
|
||||
Self { _private: () }
|
||||
}
|
||||
|
||||
pub(crate) fn usart2(
|
||||
rcc: &stm32f7::stm32f722::RCC,
|
||||
uart: stm32f7::stm32f722::USART2,
|
||||
baud: u32,
|
||||
usb: F722UsbMavlink,
|
||||
) -> Result<Self, ()> {
|
||||
let uart = peripherals::Usart2Tx::new(rcc, uart, UART2_CLOCK_HZ, baud).map_err(|_| ())?;
|
||||
// SAFETY: identical DMA-memory invariant to the USART1 route above;
|
||||
// only the fixed platform DMA endpoint differs.
|
||||
let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() };
|
||||
Ok(Self {
|
||||
uart: F722MavlinkTransport::Usart2(MavlinkTransmitter::new(
|
||||
memory,
|
||||
peripherals::Usart2TxDma::new(rcc, uart),
|
||||
)),
|
||||
usb,
|
||||
})
|
||||
/// Call after bus enable in init, or inside the USB RTIC resource lock.
|
||||
/// Defers RXFLVL only. IN, reset and suspend can still interrupt. EP0 shares
|
||||
/// the RX FIFO and cannot bypass a bulk packet blocked at its head.
|
||||
pub(crate) fn set_enabled(&mut self, enabled: bool) {
|
||||
// RM0431: OTG_FS_GINTMSK is an aligned, 32-bit RW register at +0x18.
|
||||
// Form only its raw address, without borrowing the driver's register block.
|
||||
let gintmsk = (stm32f7::stm32f722::OTG_FS_GLOBAL::ptr() as usize + 0x18) as *mut u32;
|
||||
// SAFETY: the factory consumes the PAC tokens and enables the bus before
|
||||
// use. This single-core device keeps its USB clock on. UsbIo's RTIC lock
|
||||
// excludes every runtime USB caller through this RMW. The pinned 0.4.0
|
||||
// driver writes GINTMSK only in enable(), before this capability is used.
|
||||
unsafe { set_rx_fifo_interrupt_enabled(gintmsk, enabled) };
|
||||
// Complete the device write before unlocking or returning from the IRQ.
|
||||
cortex_m::asm::dsb();
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends publications on the open USB link, or on UART when USB is closed.
|
||||
pub(crate) fn send_active(&mut self, pending: PendingFrame<MavlinkFrame>) -> bool {
|
||||
if self.usb.is_open() {
|
||||
self.usb.send(pending)
|
||||
/// # Safety
|
||||
/// `gintmsk` must permit aligned 32-bit volatile reads/writes, with no concurrent
|
||||
/// writer. The caller must complete the device write before releasing exclusion.
|
||||
unsafe fn set_rx_fifo_interrupt_enabled(gintmsk: *mut u32, enabled: bool) {
|
||||
const RXFLVLM: u32 = 1 << 4;
|
||||
// SAFETY: the caller provides exclusive access to this RW register. GINTMSK
|
||||
// has no read/clear or write-one-to-clear fields. Preserve all other bits.
|
||||
unsafe {
|
||||
let mask = gintmsk.read_volatile();
|
||||
gintmsk.write_volatile(if enabled {
|
||||
mask | RXFLVLM
|
||||
} else {
|
||||
self.send_uart(pending)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a protocol reply only on the link that supplied its request.
|
||||
pub(crate) fn send_to(
|
||||
&mut self,
|
||||
ingress: MavlinkIngress,
|
||||
pending: PendingFrame<MavlinkFrame>,
|
||||
) -> bool {
|
||||
match ingress {
|
||||
MavlinkIngress::Uart => self.send_uart(pending),
|
||||
MavlinkIngress::Usb => self.usb.send(pending),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_uart(&mut self, pending: PendingFrame<MavlinkFrame>) -> bool {
|
||||
match &mut self.uart {
|
||||
F722MavlinkTransport::Usart1(transmitter) => transmitter
|
||||
.admit_and_start(pending.class, pending.frame)
|
||||
.admission
|
||||
.is_retained(),
|
||||
F722MavlinkTransport::Usart2(transmitter) => transmitter
|
||||
.admit_and_start(pending.class, pending.frame)
|
||||
.admission
|
||||
.is_retained(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn on_dma_complete(&mut self, completion: TxCompletion) {
|
||||
match &mut self.uart {
|
||||
F722MavlinkTransport::Usart1(transmitter) => {
|
||||
let _ = transmitter.on_dma_complete(completion);
|
||||
}
|
||||
F722MavlinkTransport::Usart2(transmitter) => {
|
||||
let _ = transmitter.on_dma_complete(completion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Services USB and reports whether the protocol worker has bytes to parse.
|
||||
pub(crate) fn poll_usb(&mut self) -> bool {
|
||||
self.usb.poll()
|
||||
}
|
||||
|
||||
pub(crate) fn take_usb_received(&mut self) -> Option<IntegrityCheckedFrame> {
|
||||
self.usb.take_received()
|
||||
mask & !RXFLVLM
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Board-route status light; it is not part of a board declaration API.
|
||||
pub(crate) fn exchange_usb<B: super::usb::UsbBusHealth>(
|
||||
usb: &mut super::usb::UsbMavlink<'_, B>,
|
||||
frame: Option<PendingFrame<MavlinkFrame>>,
|
||||
set_rx_interrupt_enabled: impl FnOnce(bool),
|
||||
) -> IoBatch {
|
||||
usb.with_rx_interrupt_gate(
|
||||
|usb| match frame {
|
||||
Some(frame) => IoBatch {
|
||||
accepted: usb.send(frame),
|
||||
..IoBatch::default()
|
||||
},
|
||||
None => {
|
||||
usb.poll();
|
||||
// The existing USB adapter consumes at most one 64-byte packet
|
||||
// here, including bounded framing. Message decode/dispatch stays
|
||||
// worker-local. Bus polling has separate hardware wait constraints.
|
||||
let frame = usb.take_received();
|
||||
let pending = usb.poll();
|
||||
IoBatch {
|
||||
usb: frame,
|
||||
pending,
|
||||
..IoBatch::default()
|
||||
}
|
||||
}
|
||||
},
|
||||
set_rx_interrupt_enabled,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) enum F722StatusLed {
|
||||
Kakute(peripherals::ActiveLowLedPd2),
|
||||
Geprc(GeprcStatusLed),
|
||||
Pd2(peripherals::ActiveLowLedPd2),
|
||||
Pa13(GeprcStatusLed),
|
||||
}
|
||||
|
||||
impl F722StatusLed {
|
||||
pub(crate) fn toggle(&mut self) {
|
||||
match self {
|
||||
Self::Kakute(led) => led.toggle(),
|
||||
Self::Geprc(led) => led.toggle(),
|
||||
Self::Pd2(led) => led.toggle(),
|
||||
Self::Pa13(led) => led.toggle(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// GEPRC GEP-F722-HD V2 blue status LED on PA13.
|
||||
pub(crate) struct GeprcStatusLed {
|
||||
port: stm32f7::stm32f722::GPIOA,
|
||||
is_high: bool,
|
||||
}
|
||||
|
||||
impl GeprcStatusLed {
|
||||
pub(crate) fn new(rcc: &stm32f7::stm32f722::RCC, port: stm32f7::stm32f722::GPIOA) -> Self {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit());
|
||||
@@ -171,7 +221,6 @@ impl GeprcStatusLed {
|
||||
is_high: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle(&mut self) {
|
||||
self.is_high = !self.is_high;
|
||||
self.port.bsrr().write(|write| {
|
||||
@@ -183,3 +232,7 @@ impl GeprcStatusLed {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../../tests/support/usb_irq.rs"]
|
||||
mod usb_irq;
|
||||
Reference in new issue
Block a user