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.
239 lines
8.1 KiB
Rust
239 lines
8.1 KiB
Rust
//! 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;
|
|
|
|
pub(crate) const SERVICE_HZ: usize = 100;
|
|
|
|
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(),
|
|
})
|
|
}
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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)
|
|
})
|
|
}
|
|
}
|
|
|
|
/// 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: () }
|
|
}
|
|
|
|
/// 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();
|
|
}
|
|
}
|
|
|
|
/// # 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 {
|
|
mask & !RXFLVLM
|
|
});
|
|
}
|
|
}
|
|
|
|
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 {
|
|
Pd2(peripherals::ActiveLowLedPd2),
|
|
Pa13(GeprcStatusLed),
|
|
}
|
|
impl F722StatusLed {
|
|
pub(crate) fn toggle(&mut self) {
|
|
match self {
|
|
Self::Pd2(led) => led.toggle(),
|
|
Self::Pa13(led) => led.toggle(),
|
|
}
|
|
}
|
|
}
|
|
|
|
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());
|
|
port.moder().modify(|_, write| write.moder13().output());
|
|
port.otyper().modify(|_, write| write.ot13().push_pull());
|
|
port.ospeedr()
|
|
.modify(|_, write| write.ospeedr13().low_speed());
|
|
port.pupdr().modify(|_, write| write.pupdr13().floating());
|
|
port.bsrr().write(|write| write.br13().set_bit());
|
|
Self {
|
|
port,
|
|
is_high: false,
|
|
}
|
|
}
|
|
fn toggle(&mut self) {
|
|
self.is_high = !self.is_high;
|
|
self.port.bsrr().write(|write| {
|
|
if self.is_high {
|
|
write.bs13().set_bit()
|
|
} else {
|
|
write.br13().set_bit()
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../../tests/support/usb_irq.rs"]
|
|
mod usb_irq;
|