feat(f722): add usb mavlink transport
This commit is contained in:
1 parent
7dc3ccb619
commit
604a7aba86
18 files changed
+1087
-208
No files matched your search
Generated
+72
-3
@@ -98,6 +98,12 @@ dependencies = [
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.7.1"
|
||||
@@ -110,7 +116,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0"
|
||||
dependencies = [
|
||||
"embedded-io",
|
||||
"embedded-io 0.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -125,12 +131,31 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||
|
||||
[[package]]
|
||||
name = "hash32"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad"
|
||||
dependencies = [
|
||||
"hash32",
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -197,11 +222,11 @@ dependencies = [
|
||||
name = "kaptan-data-channel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"kaptan-dma",
|
||||
"kaptan-err-reportable",
|
||||
"kaptan-mavlink",
|
||||
"kaptan-sensor",
|
||||
"kaptan-time",
|
||||
"kaptan-tx",
|
||||
"mavlink-core",
|
||||
]
|
||||
|
||||
@@ -408,7 +433,7 @@ checksum = "b31cc9f930c7edce0c1933659d405a330a28346cdf087bd283a56a6d72eb3d85"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc-any",
|
||||
"embedded-io",
|
||||
"embedded-io 0.7.1",
|
||||
"embedded-io-async",
|
||||
]
|
||||
|
||||
@@ -498,9 +523,13 @@ dependencies = [
|
||||
"kaptan-sensor",
|
||||
"kaptan-spi",
|
||||
"kaptan-time",
|
||||
"kaptan-tx",
|
||||
"panic-halt",
|
||||
"rtic",
|
||||
"stm32f7",
|
||||
"synopsys-usb-otg",
|
||||
"usb-device",
|
||||
"usbd-serial",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -672,6 +701,12 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "stm32f7"
|
||||
version = "0.16.0"
|
||||
@@ -705,6 +740,18 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synopsys-usb-otg"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e948d523b316939545d8b21a48c27aef150ce25321b9f95ff7978647a806a6fe"
|
||||
dependencies = [
|
||||
"cortex-m",
|
||||
"embedded-hal",
|
||||
"usb-device",
|
||||
"vcell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-triple"
|
||||
version = "1.0.1"
|
||||
@@ -800,6 +847,28 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "usb-device"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98816b1accafbb09085168b90f27e93d790b4bfa19d883466b5e53315b5f06a6"
|
||||
dependencies = [
|
||||
"heapless",
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usbd-serial"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "065e4eaf93db81d5adac82d9cef8f8da314cb640fa7f89534b972383f1cf80fc"
|
||||
dependencies = [
|
||||
"embedded-hal",
|
||||
"embedded-io 0.6.1",
|
||||
"nb 1.1.0",
|
||||
"usb-device",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcell"
|
||||
version = "0.1.3"
|
||||
|
||||
@@ -10,10 +10,10 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
kaptan-err-reportable = { path = "../kaptan-err-reportable" }
|
||||
kaptan-dma = { path = "../kaptan-dma" }
|
||||
kaptan-mavlink = { path = "../kaptan-mavlink", default-features = false, features = [
|
||||
"sensor-telemetry",
|
||||
] }
|
||||
kaptan-sensor = { path = "../kaptan-sensor" }
|
||||
kaptan-time = { path = "../kaptan-time" }
|
||||
kaptan-tx = { path = "../kaptan-tx" }
|
||||
mavlink-core = { version = "0.18", default-features = false }
|
||||
@@ -1,21 +1,15 @@
|
||||
//! Complete outbound MAVLink data flow.
|
||||
//! Outbound MAVLink frame production.
|
||||
//!
|
||||
//! This crate owns fixed-rate scheduling, mailbox consumption, vertical-data
|
||||
//! freshness, MAVLink encoding, and bounded queue classification. A platform
|
||||
//! supplies only mailbox references, vertical-data freshness, and a transport
|
||||
//! sink.
|
||||
//! freshness, MAVLink encoding, and frame classification. Platforms retain
|
||||
//! transport ownership and decide where each produced frame is admitted.
|
||||
|
||||
#![no_std]
|
||||
|
||||
use kaptan_dma::TxCompletion;
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
encoder::MavlinkEncoder,
|
||||
generated::kaptan::MavMessage,
|
||||
transmitter::{EnqueueResult, FrameClass},
|
||||
};
|
||||
use kaptan_mavlink::{MavlinkFrame, encoder::MavlinkEncoder, generated::kaptan::MavMessage};
|
||||
use kaptan_sensor::{optical_flow::OpticalFlowSnapshot, range::RangeSnapshot};
|
||||
use kaptan_tx::{FrameClass, PendingFrame};
|
||||
use mavlink_core::MessageData;
|
||||
|
||||
mod mailbox;
|
||||
@@ -39,19 +33,6 @@ pub struct DataChannelRates {
|
||||
pub heartbeat_hz: usize,
|
||||
}
|
||||
|
||||
/// Platform transport used by a data channel.
|
||||
///
|
||||
/// The channel decides exactly which encoded frame is admitted and with which
|
||||
/// delivery class. The platform retains only DMA endpoint ownership.
|
||||
pub trait FrameSink {
|
||||
/// Admits one fully encoded frame to the platform's bounded transport.
|
||||
fn send(&mut self, class: FrameClass, frame: MavlinkFrame) -> EnqueueResult;
|
||||
|
||||
/// Applies one terminal DMA interrupt and starts the next retained frame
|
||||
/// when possible.
|
||||
fn on_dma_complete(&mut self, completion: TxCompletion);
|
||||
}
|
||||
|
||||
/// Mailboxes read by one data-channel worker.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DataSources<'a> {
|
||||
@@ -77,9 +58,8 @@ pub enum DataChannelError {
|
||||
ScheduleFull,
|
||||
}
|
||||
|
||||
/// Complete outbound data flow for one MAVLink endpoint.
|
||||
pub struct DataChannel<'a, Sink, const SERVICE_HZ: usize> {
|
||||
sink: Sink,
|
||||
/// Scheduled outbound MAVLink frame producer.
|
||||
pub struct DataChannel<'a, const SERVICE_HZ: usize> {
|
||||
encoder: MavlinkEncoder,
|
||||
schedule: Schedule<SERVICE_HZ>,
|
||||
sources: DataSources<'a>,
|
||||
@@ -93,13 +73,9 @@ pub struct DataChannel<'a, Sink, const SERVICE_HZ: usize> {
|
||||
optical_flow_age_ticks: u8,
|
||||
}
|
||||
|
||||
impl<'a, Sink, const SERVICE_HZ: usize> DataChannel<'a, Sink, SERVICE_HZ>
|
||||
where
|
||||
Sink: FrameSink,
|
||||
{
|
||||
impl<'a, const SERVICE_HZ: usize> DataChannel<'a, SERVICE_HZ> {
|
||||
/// Creates an outbound data flow from already-validated runtime inputs.
|
||||
pub fn new(
|
||||
sink: Sink,
|
||||
system_id: u8,
|
||||
component_id: u8,
|
||||
rates: DataChannelRates,
|
||||
@@ -108,7 +84,6 @@ where
|
||||
) -> Result<Self, DataChannelError> {
|
||||
let schedule = Schedule::from_rates(rates)?;
|
||||
Ok(Self {
|
||||
sink,
|
||||
encoder: MavlinkEncoder::new(system_id, component_id),
|
||||
schedule,
|
||||
sources,
|
||||
@@ -125,57 +100,53 @@ where
|
||||
|
||||
/// Runs all outbound work due for one platform service tick.
|
||||
///
|
||||
/// No schedule token escapes this method. It refreshes cached vertical
|
||||
/// observations, selects due data, encodes the MAVLink message, and admits
|
||||
/// it with the correct bounded-queue class.
|
||||
pub fn on_service_tick(&mut self) {
|
||||
/// At most one classified frame is returned for transport admission.
|
||||
pub fn on_service_tick(&mut self) -> Option<PendingFrame<MavlinkFrame>> {
|
||||
self.refresh_vertical_data();
|
||||
|
||||
match self.schedule.next_due() {
|
||||
Some(DueData::Imu) => {
|
||||
if let Some(sample) = self.sources.imu.take_newer(&mut self.observed_imu) {
|
||||
let _ = self
|
||||
.sink
|
||||
.send(FrameClass::BestEffort, self.encoder.imu_snapshot(&sample));
|
||||
}
|
||||
self.sources
|
||||
.imu
|
||||
.take_newer(&mut self.observed_imu)
|
||||
.map(|sample| PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: self.encoder.imu_snapshot(&sample),
|
||||
})
|
||||
}
|
||||
Some(DueData::Distance) if self.range_age_ticks <= self.maximum_vertical_age_ticks => {
|
||||
if let Some(range) = self.last_range {
|
||||
let _ = self
|
||||
.sink
|
||||
.send(FrameClass::BestEffort, self.encoder.range_snapshot(&range));
|
||||
}
|
||||
self.last_range.map(|range| PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: self.encoder.range_snapshot(&range),
|
||||
})
|
||||
}
|
||||
Some(DueData::OpticalFlow)
|
||||
if self.optical_flow_age_ticks <= self.maximum_vertical_age_ticks =>
|
||||
{
|
||||
if let Some(flow) = self.last_optical_flow {
|
||||
let _ = self.sink.send(
|
||||
FrameClass::BestEffort,
|
||||
self.encoder.optical_flow_snapshot(&flow),
|
||||
);
|
||||
}
|
||||
self.last_optical_flow.map(|flow| PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: self.encoder.optical_flow_snapshot(&flow),
|
||||
})
|
||||
}
|
||||
Some(DueData::Heartbeat) => {
|
||||
let _ = self.sink.send(FrameClass::Vital, self.encoder.heartbeat());
|
||||
}
|
||||
Some(DueData::Distance | DueData::OpticalFlow) | None => {}
|
||||
Some(DueData::Heartbeat) => Some(PendingFrame {
|
||||
class: FrameClass::Vital,
|
||||
frame: self.encoder.heartbeat(),
|
||||
}),
|
||||
Some(DueData::Distance | DueData::OpticalFlow) | None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Completes the active DMA lifecycle for this channel's transport.
|
||||
pub fn on_dma_complete(&mut self, completion: TxCompletion) {
|
||||
self.sink.on_dma_complete(completion)
|
||||
}
|
||||
|
||||
/// Encodes an adapter-owned generated MAVLink data record through the
|
||||
/// channel's sole encoder and bounded transport sink.
|
||||
pub fn send_message<M: MessageData<Message = MavMessage>>(
|
||||
/// Encodes an adapter-owned generated MAVLink record through the channel's
|
||||
/// sole sequence-number owner.
|
||||
pub fn encode_message<M: MessageData<Message = MavMessage>>(
|
||||
&mut self,
|
||||
class: FrameClass,
|
||||
message: &M,
|
||||
) -> EnqueueResult {
|
||||
self.sink.send(class, self.encoder.encode(message))
|
||||
) -> PendingFrame<MavlinkFrame> {
|
||||
PendingFrame {
|
||||
class,
|
||||
frame: self.encoder.encode(message),
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_vertical_data(&mut self) {
|
||||
@@ -276,39 +247,15 @@ impl<const SERVICE_HZ: usize> Schedule<SERVICE_HZ> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
DataChannel, DataChannelRates, DataSources, FrameSink, LatestImuMailbox,
|
||||
LatestOpticalFlowMailbox, LatestRangeMailbox,
|
||||
};
|
||||
use kaptan_dma::TxCompletion;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
transmitter::{EnqueueResult, FrameClass},
|
||||
DataChannel, DataChannelRates, DataSources, LatestImuMailbox, LatestOpticalFlowMailbox,
|
||||
LatestRangeMailbox,
|
||||
};
|
||||
use kaptan_mavlink::generated::kaptan::MavMessage;
|
||||
use kaptan_sensor::{
|
||||
imu::ImuSnapshot, optical_flow::OpticalFlowSnapshot, range::RangeSnapshot,
|
||||
};
|
||||
use kaptan_time::Timestamp;
|
||||
|
||||
struct Sink {
|
||||
best_effort: u8,
|
||||
critical: u8,
|
||||
completions: u8,
|
||||
}
|
||||
|
||||
impl FrameSink for Sink {
|
||||
fn send(&mut self, class: FrameClass, _: MavlinkFrame) -> EnqueueResult {
|
||||
match class {
|
||||
FrameClass::Vital => self.critical += 1,
|
||||
FrameClass::BestEffort => self.best_effort += 1,
|
||||
FrameClass::Critical => self.critical += 1,
|
||||
}
|
||||
EnqueueResult::Enqueued
|
||||
}
|
||||
|
||||
fn on_dma_complete(&mut self, _: TxCompletion) {
|
||||
self.completions += 1;
|
||||
}
|
||||
}
|
||||
use kaptan_tx::FrameClass;
|
||||
|
||||
const RATES: DataChannelRates = DataChannelRates {
|
||||
imu_hz: 20,
|
||||
@@ -318,16 +265,11 @@ mod tests {
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn owns_rate_selection_encoding_admission_and_completion() {
|
||||
fn owns_rate_selection_encoding_and_classification() {
|
||||
let imu = LatestImuMailbox::new();
|
||||
let range = LatestRangeMailbox::new();
|
||||
let flow = LatestOpticalFlowMailbox::new();
|
||||
let mut channel: DataChannel<'_, Sink, 100> = DataChannel::new(
|
||||
Sink {
|
||||
best_effort: 0,
|
||||
critical: 0,
|
||||
completions: 0,
|
||||
},
|
||||
let mut channel: DataChannel<'_, 100> = DataChannel::new(
|
||||
1,
|
||||
1,
|
||||
RATES,
|
||||
@@ -339,6 +281,8 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let mut best_effort = 0;
|
||||
let mut vital = 0;
|
||||
|
||||
for tick in 0..100 {
|
||||
imu.publish(ImuSnapshot::new(
|
||||
@@ -354,12 +298,17 @@ mod tests {
|
||||
100,
|
||||
100,
|
||||
));
|
||||
channel.on_service_tick();
|
||||
if let Some(frame) = channel.on_service_tick() {
|
||||
assert!(frame.frame.has_valid_crc::<MavMessage>());
|
||||
assert_eq!(frame.frame.sequence(), best_effort + vital);
|
||||
match frame.class {
|
||||
FrameClass::Vital => vital += 1,
|
||||
FrameClass::BestEffort => best_effort += 1,
|
||||
FrameClass::Critical => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
channel.on_dma_complete(TxCompletion::Complete);
|
||||
let sink = channel.sink;
|
||||
assert_eq!(sink.best_effort, 60);
|
||||
assert_eq!(sink.critical, 2);
|
||||
assert_eq!(sink.completions, 1);
|
||||
assert_eq!(best_effort, 60);
|
||||
assert_eq!(vital, 2);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use crate::{MavlinkFrame, decode_message, generated::kaptan::MavMessage};
|
||||
/// An unsigned MAVLink 2 frame whose ID and CRC match the Kaptan dialect.
|
||||
///
|
||||
/// The payload is deliberately not decoded during byte admission. Consumers
|
||||
/// decode it once after taking the frame from the bounded receive queue.
|
||||
/// decode it once at protocol dispatch.
|
||||
#[repr(transparent)]
|
||||
pub struct IntegrityCheckedFrame(MavlinkFrame);
|
||||
|
||||
@@ -32,14 +32,20 @@ impl IntegrityCheckedFrame {
|
||||
}
|
||||
}
|
||||
|
||||
struct IntegrityFramer {
|
||||
/// Incremental, unbuffered MAVLink 2 framing with Kaptan ID and CRC validation.
|
||||
///
|
||||
/// Consumers that can pause byte admission can process each returned frame
|
||||
/// directly. Interrupt-driven byte streams can use [`IntegrityMavlinkReceiver`]
|
||||
/// to queue complete frames instead. Accepted frames remain unauthenticated.
|
||||
pub struct IntegrityMavlinkFramer {
|
||||
frame: MavlinkFrame,
|
||||
received: usize,
|
||||
expected: usize,
|
||||
}
|
||||
|
||||
impl IntegrityFramer {
|
||||
const fn new() -> Self {
|
||||
impl IntegrityMavlinkFramer {
|
||||
/// Creates an empty framer.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
frame: MavlinkFrame::new(),
|
||||
received: 0,
|
||||
@@ -47,7 +53,8 @@ impl IntegrityFramer {
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_byte(&mut self, byte: u8) -> Option<IntegrityCheckedFrame> {
|
||||
/// Consumes one byte, returning a complete integrity-checked frame if ready.
|
||||
pub fn receive_byte(&mut self, byte: u8) -> Option<IntegrityCheckedFrame> {
|
||||
if self.received == 0 {
|
||||
if byte == 0xfd {
|
||||
self.frame.as_mut_slice()[0] = byte;
|
||||
@@ -81,20 +88,27 @@ impl IntegrityFramer {
|
||||
IntegrityCheckedFrame::try_new(frame)
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
/// Discards a partial frame after a stream discontinuity.
|
||||
pub fn reset(&mut self) {
|
||||
self.frame = MavlinkFrame::new();
|
||||
self.received = 0;
|
||||
self.expected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IntegrityMavlinkFramer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Bounded integrity-only MAVLink 2 receiver.
|
||||
///
|
||||
/// It rejects signed, incompatible, corrupt, and MAVLink 1 frames, but an
|
||||
/// accepted frame is still unauthenticated. The composition decides whether
|
||||
/// and how a received request may be admitted.
|
||||
pub struct IntegrityMavlinkReceiver<const N: usize> {
|
||||
framer: IntegrityFramer,
|
||||
framer: IntegrityMavlinkFramer,
|
||||
frames: [Option<IntegrityCheckedFrame>; N],
|
||||
front: usize,
|
||||
length: usize,
|
||||
@@ -105,7 +119,7 @@ impl<const N: usize> IntegrityMavlinkReceiver<N> {
|
||||
pub const fn new() -> Self {
|
||||
assert!(N != 0, "integrity receiver capacity must be nonzero");
|
||||
Self {
|
||||
framer: IntegrityFramer::new(),
|
||||
framer: IntegrityMavlinkFramer::new(),
|
||||
frames: [const { None }; N],
|
||||
front: 0,
|
||||
length: 0,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Bounded protocol-neutral transmit admission and ownership lifecycle.
|
||||
//!
|
||||
//! Protocol encoders own frame bytes. This crate owns only bounded priority
|
||||
//! admission and the transition from pending frame to active DMA ownership.
|
||||
//! admission and the transition from pending frame to active transport ownership.
|
||||
|
||||
#![no_std]
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum FrameClass {
|
||||
/// Heartbeat and current safety/health status. A pending vital frame may
|
||||
/// displace any pending non-vital frame; active DMA storage is untouched.
|
||||
/// displace any pending non-vital frame; active storage is untouched.
|
||||
Vital,
|
||||
/// Fault transitions and authoritative replies that best-effort samples
|
||||
/// cannot displace.
|
||||
@@ -147,12 +147,12 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
Ok(StartOutcome::Started)
|
||||
}
|
||||
|
||||
/// Releases the active frame after a terminal DMA completion.
|
||||
/// Releases the active frame after the transport no longer borrows it.
|
||||
pub fn complete(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
self.active.take()
|
||||
}
|
||||
|
||||
/// Releases the active frame after a terminal DMA fault.
|
||||
/// Releases the active frame after a terminal transport fault.
|
||||
pub fn fail(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
self.active.take()
|
||||
}
|
||||
@@ -162,6 +162,14 @@ impl<Frame: Copy, const N: usize> TransmitQueue<Frame, N> {
|
||||
self.active.is_some()
|
||||
}
|
||||
|
||||
/// Borrows the frame currently owned by the active transport operation.
|
||||
///
|
||||
/// 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()
|
||||
}
|
||||
|
||||
fn take_next(&mut self) -> Option<PendingFrame<Frame>> {
|
||||
let vital = self
|
||||
.pending
|
||||
@@ -203,6 +211,7 @@ mod tests {
|
||||
Ok(StartOutcome::Started)
|
||||
);
|
||||
assert!(queue.is_active());
|
||||
assert_eq!(queue.active().map(|pending| pending.frame), Some(1));
|
||||
|
||||
assert_eq!(
|
||||
queue.enqueue(PendingFrame {
|
||||
@@ -212,6 +221,7 @@ mod tests {
|
||||
EnqueueResult::Enqueued
|
||||
);
|
||||
assert_eq!(queue.complete().unwrap().frame, 1);
|
||||
assert!(queue.active().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -28,6 +28,7 @@ kaptan-mavlink = { path = "../../libs/kaptan-mavlink" }
|
||||
kaptan-sensor = { path = "../../libs/kaptan-sensor" }
|
||||
kaptan-spi = { path = "../../libs/kaptan-spi" }
|
||||
kaptan-time = { path = "../../libs/kaptan-time" }
|
||||
kaptan-tx = { path = "../../libs/kaptan-tx" }
|
||||
cortex-m = "0.7"
|
||||
cortex-m-rt = "0.7"
|
||||
rtic = { version = "2.3", default-features = false, features = [
|
||||
@@ -37,4 +38,7 @@ stm32f7 = { version = "0.16", default-features = false, features = [
|
||||
"rt",
|
||||
"stm32f722",
|
||||
] }
|
||||
synopsys-usb-otg = { version = "0.4", features = ["cortex-m", "fs"] }
|
||||
usb-device = "0.3"
|
||||
usbd-serial = "0.2"
|
||||
panic-halt = "1"
|
||||
@@ -7,7 +7,7 @@ use super::peripherals::{
|
||||
configure_exti3_pin_pc3, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9,
|
||||
configure_spi1_pins_pa5_pa6_pa7, configure_spi3_pins_pc10_pc11_pc12,
|
||||
configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
|
||||
configure_usart2_pins_pa2_pa3,
|
||||
configure_usart2_pins_pa2_pa3, configure_usb_fs_pins_pa11_pa12,
|
||||
};
|
||||
use kaptan_config::Config;
|
||||
use kaptan_data_channel::{
|
||||
@@ -40,7 +40,8 @@ use super::config::{
|
||||
use super::{
|
||||
VerticalRoute, clock,
|
||||
data_channel::{
|
||||
F722MavlinkTransmitter, F722StatusLed, GeprcStatusLed, SERVICE_HZ, SERVICE_TIMER_CONFIG,
|
||||
F722MavlinkLink, F722StatusLed, GeprcStatusLed, MavlinkIngress, SERVICE_HZ,
|
||||
SERVICE_TIMER_CONFIG,
|
||||
},
|
||||
interrupts,
|
||||
sensors::{F722Imu, ImuEvent},
|
||||
@@ -80,17 +81,19 @@ pub type F722ServiceTimer = Tim2Periodic;
|
||||
|
||||
/// RTIC-shared MAVLink state.
|
||||
///
|
||||
/// Only the MAVLink worker accesses this state, so its priority ceiling is 9
|
||||
/// rather than the IMU or vertical-sensor IRQ ceiling.
|
||||
/// The UART and USB handlers do bounded byte/packet admission at priority 8;
|
||||
/// protocol dispatch and transmission policy remain in the priority-9 worker.
|
||||
/// This state therefore cannot inherit the IMU or vertical-sensor IRQ ceiling.
|
||||
pub struct F722MavlinkState<C, I, const MAX_PAYLOAD_BYTES: usize>
|
||||
where
|
||||
C: Clone + Config,
|
||||
I: F7ImmutableConfig,
|
||||
{
|
||||
data_channel: DataChannel<'static, F722MavlinkTransmitter, SERVICE_HZ>,
|
||||
data_channel: DataChannel<'static, SERVICE_HZ>,
|
||||
link: F722MavlinkLink,
|
||||
config_file: F722ConfigFile<C, I, MAX_PAYLOAD_BYTES>,
|
||||
file_service: FileService,
|
||||
receiver: IntegrityMavlinkReceiver<1>,
|
||||
uart_receiver: IntegrityMavlinkReceiver<1>,
|
||||
file_lease_ticks: u16,
|
||||
mavlink_errors: ErrorSink,
|
||||
mavlink_logs: LogSinkQueue<1, 1>,
|
||||
@@ -229,9 +232,16 @@ where
|
||||
.mpu6000_config()
|
||||
.ok_or(BoardError::Initialization)?,
|
||||
);
|
||||
let transmitter =
|
||||
F722MavlinkTransmitter::usart1(&device.RCC, device.USART1, hardware.mavlink_baud())
|
||||
.map_err(|_| BoardError::Initialization)?;
|
||||
let usb = initialize_usb(
|
||||
&device.RCC,
|
||||
&device.GPIOA,
|
||||
device.OTG_FS_GLOBAL,
|
||||
device.OTG_FS_DEVICE,
|
||||
device.OTG_FS_PWRCLK,
|
||||
immutable.f722().name,
|
||||
)?;
|
||||
let link = F722MavlinkLink::usart1(&device.RCC, device.USART1, hardware.mavlink_baud(), usb)
|
||||
.map_err(|_| BoardError::Initialization)?;
|
||||
if hardware.status_led() != F722StatusLedRoute::Pd2ActiveLow {
|
||||
return Err(BoardError::Initialization);
|
||||
}
|
||||
@@ -248,7 +258,7 @@ where
|
||||
range,
|
||||
mtf01,
|
||||
mtf01_uart,
|
||||
transmitter,
|
||||
link,
|
||||
led,
|
||||
configuration,
|
||||
)
|
||||
@@ -299,9 +309,16 @@ where
|
||||
clock.fork(),
|
||||
hardware.imu_mount(),
|
||||
);
|
||||
let transmitter =
|
||||
F722MavlinkTransmitter::usart2(&device.RCC, device.USART2, hardware.mavlink_baud())
|
||||
.map_err(|_| BoardError::Initialization)?;
|
||||
let usb = initialize_usb(
|
||||
&device.RCC,
|
||||
&device.GPIOA,
|
||||
device.OTG_FS_GLOBAL,
|
||||
device.OTG_FS_DEVICE,
|
||||
device.OTG_FS_PWRCLK,
|
||||
immutable.f722().name,
|
||||
)?;
|
||||
let link = F722MavlinkLink::usart2(&device.RCC, device.USART2, hardware.mavlink_baud(), usb)
|
||||
.map_err(|_| BoardError::Initialization)?;
|
||||
if hardware.status_led() != F722StatusLedRoute::Pa13ActiveHigh {
|
||||
return Err(BoardError::Initialization);
|
||||
}
|
||||
@@ -315,7 +332,7 @@ where
|
||||
range,
|
||||
mtf01,
|
||||
mtf01_uart,
|
||||
transmitter,
|
||||
link,
|
||||
led,
|
||||
configuration,
|
||||
)
|
||||
@@ -331,7 +348,7 @@ fn finish<C, I, const MAX_PAYLOAD_BYTES: usize>(
|
||||
range: Option<F722Range>,
|
||||
mtf01: Option<F722Mtf01>,
|
||||
mtf01_uart: Option<Uart4>,
|
||||
transmitter: F722MavlinkTransmitter,
|
||||
link: F722MavlinkLink,
|
||||
led: F722StatusLed,
|
||||
configuration: F722ConfigManager<C, I, MAX_PAYLOAD_BYTES>,
|
||||
) -> Result<F722InitResources<C, I, MAX_PAYLOAD_BYTES>, BoardError>
|
||||
@@ -350,7 +367,6 @@ where
|
||||
};
|
||||
let mavlink_log_filter = runtime.mavlink_log_level_filter;
|
||||
let data_channel = DataChannel::new(
|
||||
transmitter,
|
||||
system_id,
|
||||
component_id,
|
||||
rates,
|
||||
@@ -373,9 +389,10 @@ where
|
||||
},
|
||||
mavlink: F722MavlinkState {
|
||||
data_channel,
|
||||
link,
|
||||
config_file: F722ConfigFile::new(configuration),
|
||||
file_service: FileService::new(Target::new(system_id, component_id)),
|
||||
receiver: IntegrityMavlinkReceiver::new(),
|
||||
uart_receiver: IntegrityMavlinkReceiver::new(),
|
||||
file_lease_ticks: 0,
|
||||
mavlink_errors: ErrorSink::new(),
|
||||
mavlink_logs: LogSinkQueue::new(mavlink_log_filter),
|
||||
@@ -401,6 +418,19 @@ where
|
||||
.map_err(|_| BoardError::Configuration)
|
||||
}
|
||||
|
||||
fn initialize_usb(
|
||||
rcc: &stm32f722::RCC,
|
||||
gpioa: &stm32f722::GPIOA,
|
||||
global: stm32f722::OTG_FS_GLOBAL,
|
||||
device: stm32f722::OTG_FS_DEVICE,
|
||||
power_clock: stm32f722::OTG_FS_PWRCLK,
|
||||
product: &'static str,
|
||||
) -> Result<super::peripherals::F722UsbMavlink, BoardError> {
|
||||
configure_usb_fs_pins_pa11_pa12(rcc, gpioa);
|
||||
super::peripherals::usb_mavlink(global, device, power_clock, product)
|
||||
.map_err(|_| BoardError::Initialization)
|
||||
}
|
||||
|
||||
fn configure_vertical_pins(
|
||||
rcc: &stm32f722::RCC,
|
||||
gpioa: &stm32f722::GPIOA,
|
||||
@@ -583,13 +613,16 @@ where
|
||||
const RECEIVED: u8 = 1 << 1;
|
||||
const DMA: u8 = 1 << 2;
|
||||
|
||||
if events & RECEIVED != 0 {
|
||||
// Poll at least every service tick, including when endpoint input was
|
||||
// backpressured by a packet the worker had not yet consumed.
|
||||
let usb_received = events & TIMER != 0 && self.link.poll_usb();
|
||||
if events & RECEIVED != 0 || usb_received {
|
||||
self.drain_file_frames();
|
||||
}
|
||||
|
||||
if events & DMA != 0 {
|
||||
let completion = take_mavlink_completion();
|
||||
self.data_channel.on_dma_complete(completion);
|
||||
self.link.on_dma_complete(completion);
|
||||
if completion == kaptan_dma::TxCompletion::Complete {
|
||||
self.led.toggle();
|
||||
}
|
||||
@@ -597,27 +630,41 @@ where
|
||||
|
||||
if events & TIMER != 0 {
|
||||
self.tick_file_lease();
|
||||
self.data_channel.on_service_tick();
|
||||
if let Some(frame) = self.data_channel.on_service_tick() {
|
||||
let _ = self.link.send_active(frame);
|
||||
}
|
||||
self.drain_one_mavlink_diagnostic();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn receive_mavlink_interrupt(&mut self, event: ReceiveInterrupt) -> bool {
|
||||
match event {
|
||||
ReceiveInterrupt::Byte(byte) => self.receiver.receive_byte(byte),
|
||||
ReceiveInterrupt::Byte(byte) => self.uart_receiver.receive_byte(byte),
|
||||
ReceiveInterrupt::Fault => {
|
||||
self.receiver.reset();
|
||||
self.file_lease_ticks = 0;
|
||||
self.file_service.expire(&mut self.config_file);
|
||||
kaptan_log::warn!("mavlink receive fault; file transfer expired");
|
||||
self.uart_receiver.reset();
|
||||
kaptan_log::warn!("mavlink receive fault; framing reset");
|
||||
false
|
||||
}
|
||||
ReceiveInterrupt::NotPending => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Acknowledges one OTG FS interrupt and admits USB MAVLink input.
|
||||
pub fn on_usb_interrupt(&mut self) -> bool {
|
||||
self.link.poll_usb()
|
||||
}
|
||||
|
||||
fn drain_file_frames(&mut self) {
|
||||
while let Some(frame) = self.receiver.take_received() {
|
||||
while let Some((ingress, frame)) = self
|
||||
.uart_receiver
|
||||
.take_received()
|
||||
.map(|frame| (MavlinkIngress::Uart, frame))
|
||||
.or_else(|| {
|
||||
self.link
|
||||
.take_usb_received()
|
||||
.map(|frame| (MavlinkIngress::Usb, frame))
|
||||
})
|
||||
{
|
||||
let Some(principal) =
|
||||
Principal::try_new(frame.raw().system_id(), frame.raw().component_id())
|
||||
else {
|
||||
@@ -644,7 +691,7 @@ where
|
||||
&& control.status != FileStatus::RequestConflict
|
||||
}
|
||||
};
|
||||
self.send_file_response(response);
|
||||
self.send_file_response(ingress, response);
|
||||
if refresh_lease {
|
||||
self.file_lease_ticks = FILE_TRANSFER_LEASE_TICKS;
|
||||
}
|
||||
@@ -661,15 +708,17 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn send_file_response(&mut self, response: FileResponse) {
|
||||
fn send_file_response(&mut self, ingress: MavlinkIngress, response: FileResponse) {
|
||||
let message = wire::encode_response(response);
|
||||
let class = kaptan_mavlink::transmitter::FrameClass::Critical;
|
||||
match message {
|
||||
kaptan::MavMessage::FILE_CONTROL(data) => {
|
||||
let _ = self.data_channel.send_message(class, &data);
|
||||
let frame = self.data_channel.encode_message(class, &data);
|
||||
let _ = self.link.send_to(ingress, frame);
|
||||
}
|
||||
kaptan::MavMessage::FILE_DATA(data) => {
|
||||
let _ = self.data_channel.send_message(class, &data);
|
||||
let frame = self.data_channel.encode_message(class, &data);
|
||||
let _ = self.link.send_to(ingress, frame);
|
||||
}
|
||||
_ => unreachable!("file response encoder returned another message"),
|
||||
}
|
||||
@@ -692,25 +741,25 @@ where
|
||||
}
|
||||
|
||||
fn submit_pending_mavlink_error(&mut self) -> bool {
|
||||
let data_channel = &mut self.data_channel;
|
||||
let (data_channel, link) = (&mut self.data_channel, &mut self.link);
|
||||
self.mavlink_errors.submit_next(|record| {
|
||||
let payload = encode_error(record);
|
||||
data_channel
|
||||
.send_message(kaptan_mavlink::transmitter::FrameClass::Critical, &payload)
|
||||
.is_retained()
|
||||
let frame = data_channel
|
||||
.encode_message(kaptan_mavlink::transmitter::FrameClass::Critical, &payload);
|
||||
link.send_active(frame)
|
||||
})
|
||||
}
|
||||
|
||||
fn submit_queued_mavlink_log(&mut self) {
|
||||
let data_channel = &mut self.data_channel;
|
||||
let (data_channel, link) = (&mut self.data_channel, &mut self.link);
|
||||
let _ = self.mavlink_logs.submit_next(|record| {
|
||||
let class = match record.delivery() {
|
||||
DeliveryClass::Critical => kaptan_mavlink::transmitter::FrameClass::Critical,
|
||||
DeliveryClass::BestEffort => kaptan_mavlink::transmitter::FrameClass::BestEffort,
|
||||
};
|
||||
let payload = encode_log(record);
|
||||
let admission = data_channel.send_message(class, &payload);
|
||||
let outcome = if admission.is_retained() {
|
||||
let frame = data_channel.encode_message(class, &payload);
|
||||
let outcome = if link.send_active(frame) {
|
||||
TransportAdmission::Accepted
|
||||
} else {
|
||||
TransportAdmission::Retry
|
||||
|
||||
@@ -6,6 +6,8 @@ use stm32f7::stm32f722;
|
||||
|
||||
/// Core frequency in whole megahertz after selecting the PLL216 board profile.
|
||||
const CORE_CLOCK_MHZ: u32 = 216;
|
||||
/// Core/AHB frequency used by fixed-frequency peripheral drivers.
|
||||
pub(crate) const CORE_FREQUENCY_HZ: u32 = CORE_CLOCK_MHZ * 1_000_000;
|
||||
|
||||
/// A failure while establishing the board's clock tree or monotonic clock.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//! F722 transport and vertical-route adapters for the portable data channel.
|
||||
|
||||
use super::peripherals;
|
||||
use kaptan_data_channel::FrameSink;
|
||||
use kaptan_dma::TxCompletion;
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
transmitter::{EnqueueResult, FrameClass, MavlinkTransmitter},
|
||||
MavlinkFrame, receiver::IntegrityCheckedFrame, transmitter::MavlinkTransmitter,
|
||||
};
|
||||
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
|
||||
@@ -28,16 +28,25 @@ enum F722MavlinkTransport {
|
||||
Usart2(MavlinkTransmitter<peripherals::DcacheDisabledDmaMemory, peripherals::Usart2TxDma, 4>),
|
||||
}
|
||||
|
||||
/// Sole F722 MAVLink DMA owner.
|
||||
pub(crate) struct F722MavlinkTransmitter {
|
||||
inner: F722MavlinkTransport,
|
||||
/// Physical link on which an inbound frame arrived.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum MavlinkIngress {
|
||||
Uart,
|
||||
Usb,
|
||||
}
|
||||
|
||||
impl F722MavlinkTransmitter {
|
||||
/// Concrete F722 MAVLink link ownership.
|
||||
pub(crate) struct F722MavlinkLink {
|
||||
uart: F722MavlinkTransport,
|
||||
usb: F722UsbMavlink,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -45,10 +54,11 @@ impl F722MavlinkTransmitter {
|
||||
// this platform receives a terminal DMA completion notification.
|
||||
let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() };
|
||||
Ok(Self {
|
||||
inner: F722MavlinkTransport::Usart1(MavlinkTransmitter::new(
|
||||
uart: F722MavlinkTransport::Usart1(MavlinkTransmitter::new(
|
||||
memory,
|
||||
peripherals::Usart1TxDma::new(rcc, uart),
|
||||
)),
|
||||
usb,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,45 +66,73 @@ impl F722MavlinkTransmitter {
|
||||
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 {
|
||||
inner: F722MavlinkTransport::Usart2(MavlinkTransmitter::new(
|
||||
uart: F722MavlinkTransport::Usart2(MavlinkTransmitter::new(
|
||||
memory,
|
||||
peripherals::Usart2TxDma::new(rcc, uart),
|
||||
)),
|
||||
usb,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FrameSink for F722MavlinkTransmitter {
|
||||
fn send(&mut self, class: FrameClass, frame: MavlinkFrame) -> EnqueueResult {
|
||||
match &mut self.inner {
|
||||
/// 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)
|
||||
} 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 outcome = transmitter.admit_and_start(class, frame);
|
||||
let _ = outcome.start;
|
||||
outcome.admission
|
||||
let _ = transmitter.on_dma_complete(completion);
|
||||
}
|
||||
F722MavlinkTransport::Usart2(transmitter) => {
|
||||
let outcome = transmitter.admit_and_start(class, frame);
|
||||
let _ = outcome.start;
|
||||
outcome.admission
|
||||
let _ = transmitter.on_dma_complete(completion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_dma_complete(&mut self, completion: TxCompletion) {
|
||||
match &mut self.inner {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,6 +248,17 @@ macro_rules! __f722_flight_application {
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = OTG_FS, priority = 8, shared = [mavlink])]
|
||||
fn mavlink_usb(mut context: mavlink_usb::Context) {
|
||||
if context
|
||||
.shared
|
||||
.mavlink
|
||||
.lock(F722MavlinkState::on_usb_interrupt)
|
||||
{
|
||||
schedule_mavlink(MAVLINK_EVENT_RECEIVED);
|
||||
}
|
||||
}
|
||||
|
||||
#[task(binds = TIM2, priority = 4, local = [timer], shared = [lidar])]
|
||||
fn tim2(mut context: tim2::Context) {
|
||||
context.local.timer.acknowledge_update();
|
||||
|
||||
@@ -15,6 +15,7 @@ mod motor_inhibit;
|
||||
mod peripherals;
|
||||
mod routes;
|
||||
mod sensors;
|
||||
mod usb;
|
||||
|
||||
use kaptan_config::Config;
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
use kaptan_err_reportable::KaptanError;
|
||||
use stm32f7::stm32f722;
|
||||
|
||||
const CLOCK_SWITCH_POLL_LIMIT: u16 = 1_024;
|
||||
const CLOCK_SWITCH_POLL_LIMIT: u32 = 1_024;
|
||||
// Crystal startup is slower than a clock-tree state transition. This remains
|
||||
// bounded so an absent oscillator reaches safe-idle instead of hanging boot.
|
||||
const HSE_STARTUP_POLL_LIMIT: u32 = 1_000_000;
|
||||
|
||||
/// Failure to establish the 216 MHz PLL clock profile.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, KaptanError)]
|
||||
@@ -22,30 +25,44 @@ pub enum ClockProfileError {
|
||||
OverDriveSwitchNotReady,
|
||||
#[kaptan_error(id = 11007)]
|
||||
SystemClockNotPll,
|
||||
#[kaptan_error(id = 11008)]
|
||||
HseNotReady,
|
||||
}
|
||||
|
||||
/// Selects the F722 platform's HSI PLL profile.
|
||||
/// Selects the F722 platform's 8 MHz HSE PLL profile.
|
||||
///
|
||||
/// SYSCLK/HCLK run at 216 MHz; APB1 is 54 MHz (timers run at 108 MHz), and
|
||||
/// APB2 is 108 MHz. HSI is the PLL source, so no external-crystal assumption
|
||||
/// is introduced.
|
||||
/// APB2 is 108 MHz. Both supported flight-controller boards have an 8 MHz
|
||||
/// external oscillator; using it as the PLL source gives USB an accurate
|
||||
/// 48 MHz clock through PLLQ.
|
||||
pub fn select_pll216_profile(
|
||||
rcc: &stm32f722::RCC,
|
||||
pwr: &stm32f722::PWR,
|
||||
flash: &stm32f722::FLASH,
|
||||
) -> Result<(), ClockProfileError> {
|
||||
rcc.cr().modify(|_, write| write.hsion().on());
|
||||
if !wait_until(|| rcc.cr().read().hsirdy().is_ready()) {
|
||||
if !wait_until(CLOCK_SWITCH_POLL_LIMIT, || {
|
||||
rcc.cr().read().hsirdy().is_ready()
|
||||
}) {
|
||||
return Err(ClockProfileError::HsiNotReady);
|
||||
}
|
||||
|
||||
// Return to HSI before changing a PLL left configured by an earlier boot stage.
|
||||
rcc.cfgr().modify(|_, write| write.sw().hsi());
|
||||
if !wait_until(|| rcc.cfgr().read().sws().is_hsi()) {
|
||||
if !wait_until(CLOCK_SWITCH_POLL_LIMIT, || rcc.cfgr().read().sws().is_hsi()) {
|
||||
return Err(ClockProfileError::SystemClockNotHsi);
|
||||
}
|
||||
|
||||
rcc.cr().modify(|_, write| write.hseon().on());
|
||||
if !wait_until(HSE_STARTUP_POLL_LIMIT, || {
|
||||
rcc.cr().read().hserdy().is_ready()
|
||||
}) {
|
||||
return Err(ClockProfileError::HseNotReady);
|
||||
}
|
||||
rcc.cr().modify(|_, write| write.pllon().off());
|
||||
if !wait_until(|| !rcc.cr().read().pllrdy().is_ready()) {
|
||||
if !wait_until(CLOCK_SWITCH_POLL_LIMIT, || {
|
||||
!rcc.cr().read().pllrdy().is_ready()
|
||||
}) {
|
||||
return Err(ClockProfileError::PllNotStopped);
|
||||
}
|
||||
|
||||
@@ -56,11 +73,15 @@ pub fn select_pll216_profile(
|
||||
// over-drive-enable fields; it preserves every other PWR_CR1 bit owned by boot.
|
||||
unsafe { write.bits((read.bits() & !(0b11 << 14)) | (0b11 << 14) | (1 << 16)) }
|
||||
});
|
||||
if !wait_until(|| pwr.csr1().read().odrdy().bit_is_set()) {
|
||||
if !wait_until(CLOCK_SWITCH_POLL_LIMIT, || {
|
||||
pwr.csr1().read().odrdy().bit_is_set()
|
||||
}) {
|
||||
return Err(ClockProfileError::OverDriveNotReady);
|
||||
}
|
||||
pwr.cr1().modify(|_, write| write.odswen().set_bit());
|
||||
if !wait_until(|| pwr.csr1().read().odswrdy().bit_is_set()) {
|
||||
if !wait_until(CLOCK_SWITCH_POLL_LIMIT, || {
|
||||
pwr.csr1().read().odswrdy().bit_is_set()
|
||||
}) {
|
||||
return Err(ClockProfileError::OverDriveSwitchNotReady);
|
||||
}
|
||||
flash.acr().modify(|read, write| {
|
||||
@@ -69,14 +90,16 @@ pub fn select_pll216_profile(
|
||||
unsafe { write.bits((read.bits() & !0x0f) | 7 | (1 << 8) | (1 << 9)) }
|
||||
});
|
||||
|
||||
// HSI / 16 * 432 / 2 = 216 MHz; Q=9 supplies a 48 MHz domain.
|
||||
// HSE / 8 * 432 / 2 = 216 MHz; Q=9 supplies the exact 48 MHz USB domain.
|
||||
rcc.pllcfgr().write(|write| {
|
||||
// SAFETY: 16, 432, P=/2 (the reset encoding), and Q=9 are the valid PLL
|
||||
// divisors described above; the PLL is disabled before this full-register write.
|
||||
unsafe { write.bits(16 | (432 << 6) | (9 << 24)) }
|
||||
// SAFETY: M=8, N=432, P=/2 (the reset encoding), Q=9, and the HSE source
|
||||
// are valid for the 8 MHz board oscillator; the PLL is disabled first.
|
||||
unsafe { write.bits(8 | (432 << 6) | (1 << 22) | (9 << 24)) }
|
||||
});
|
||||
rcc.cr().modify(|_, write| write.pllon().on());
|
||||
if !wait_until(|| rcc.cr().read().pllrdy().is_ready()) {
|
||||
if !wait_until(CLOCK_SWITCH_POLL_LIMIT, || {
|
||||
rcc.cr().read().pllrdy().is_ready()
|
||||
}) {
|
||||
return Err(ClockProfileError::PllNotReady);
|
||||
}
|
||||
rcc.cfgr().modify(|read, write| {
|
||||
@@ -92,14 +115,14 @@ pub fn select_pll216_profile(
|
||||
)
|
||||
}
|
||||
});
|
||||
if !wait_until(|| rcc.cfgr().read().sws().is_pll()) {
|
||||
if !wait_until(CLOCK_SWITCH_POLL_LIMIT, || rcc.cfgr().read().sws().is_pll()) {
|
||||
return Err(ClockProfileError::SystemClockNotPll);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_until(mut ready: impl FnMut() -> bool) -> bool {
|
||||
for _ in 0..CLOCK_SWITCH_POLL_LIMIT {
|
||||
fn wait_until(limit: u32, mut ready: impl FnMut() -> bool) -> bool {
|
||||
for _ in 0..limit {
|
||||
if ready() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -174,6 +174,23 @@ pub fn configure_usart2_pins_pa2_pa3(port: &stm32f722::GPIOA) {
|
||||
.modify(|_, write| write.pupdr2().floating().pupdr3().floating());
|
||||
}
|
||||
|
||||
/// Configures PA11/PA12 for the STM32F722 OTG FS data pair.
|
||||
pub fn configure_usb_fs_pins_pa11_pa12(rcc: &stm32f722::RCC, port: &stm32f722::GPIOA) {
|
||||
rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit());
|
||||
port.moder()
|
||||
.modify(|_, write| write.moder11().alternate().moder12().alternate());
|
||||
port.afrh().modify(|_, write| {
|
||||
// SAFETY: AF10 selects OTG_FS_DM/OTG_FS_DP on PA11/PA12.
|
||||
unsafe { write.afrh11().bits(0b1010).afrh12().bits(0b1010) }
|
||||
});
|
||||
port.otyper()
|
||||
.modify(|_, write| write.ot11().push_pull().ot12().push_pull());
|
||||
port.ospeedr()
|
||||
.modify(|_, write| write.ospeedr11().high_speed().ospeedr12().high_speed());
|
||||
port.pupdr()
|
||||
.modify(|_, write| write.pupdr11().floating().pupdr12().floating());
|
||||
}
|
||||
|
||||
/// Configures PA0/PA1 for UART4 alternate function 8.
|
||||
pub fn configure_uart4_pins_pa0_pa1(port: &stm32f722::GPIOA) {
|
||||
port.moder()
|
||||
|
||||
@@ -14,6 +14,7 @@ mod time;
|
||||
mod usart;
|
||||
mod usart1_tx_dma2_stream7;
|
||||
mod usart2_tx_dma1_stream6;
|
||||
mod usb;
|
||||
|
||||
pub use clock::select_pll216_profile;
|
||||
pub use dma_buffer::DcacheDisabledDmaMemory;
|
||||
@@ -21,7 +22,7 @@ pub use gpio::{
|
||||
ActiveLowLedPd2, configure_exti3_pin_pc3, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9,
|
||||
configure_spi1_pins_pa5_pa6_pa7, configure_spi3_pins_pc10_pc11_pc12,
|
||||
configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9,
|
||||
configure_usart2_pins_pa2_pa3,
|
||||
configure_usart2_pins_pa2_pa3, configure_usb_fs_pins_pa11_pa12,
|
||||
};
|
||||
pub use i2c1::{I2c1, I2c1Config, I2c1Error};
|
||||
pub use spi1::{Spi1, Spi1Error};
|
||||
@@ -31,3 +32,4 @@ pub use time::DwtMicrosecondClock;
|
||||
pub use usart::{ReceiveInterrupt, Uart4, Usart1Tx, Usart2Tx};
|
||||
pub use usart1_tx_dma2_stream7::Usart1TxDma;
|
||||
pub use usart2_tx_dma1_stream6::Usart2TxDma;
|
||||
pub(crate) use usb::{F722UsbMavlink, usb_mavlink};
|
||||
@@ -0,0 +1,121 @@
|
||||
//! STM32F722 OTG FS ownership and CDC-ACM initialization.
|
||||
|
||||
use stm32f7::stm32f722;
|
||||
use synopsys_usb_otg::{UsbBus, UsbPeripheral};
|
||||
use usb_device::{
|
||||
bus::UsbBusAllocator,
|
||||
device::{StringDescriptors, UsbDeviceBuilder, UsbVidPid},
|
||||
};
|
||||
use usbd_serial::CdcAcmClass;
|
||||
|
||||
use super::super::{
|
||||
clock::CORE_FREQUENCY_HZ,
|
||||
usb::{USB_PACKET_BYTES, UsbMavlink},
|
||||
};
|
||||
|
||||
const USB_VID: u16 = 0x1209;
|
||||
// pid.codes reserves 1209:0001 as a shared development/test identity. A
|
||||
// project-specific allocated PID must replace it before product distribution.
|
||||
const USB_PID: u16 = 0x0001;
|
||||
// synopsys-usb-otg uses this RAM only for OUT endpoints. CDC-ACM has one
|
||||
// 64-byte bulk OUT endpoint and EP0 has one 64-byte control OUT endpoint.
|
||||
const ENDPOINT_MEMORY_WORDS: usize = (USB_PACKET_BYTES / 4) * 2;
|
||||
const DEVICE_ID_ADDRESS: *const u32 = 0x1ff0_7a10 as *const u32;
|
||||
const DEVICE_ID_BYTES: usize = 12;
|
||||
const SERIAL_BYTES: usize = DEVICE_ID_BYTES * 2;
|
||||
|
||||
/// Exclusive owner token for the split PAC views of one OTG FS controller.
|
||||
pub(crate) struct F722UsbPeripheral {
|
||||
_global: stm32f722::OTG_FS_GLOBAL,
|
||||
_device: stm32f722::OTG_FS_DEVICE,
|
||||
_power_clock: stm32f722::OTG_FS_PWRCLK,
|
||||
}
|
||||
|
||||
// SAFETY: the token is constructed once from PAC-owned peripherals and moved
|
||||
// into one UsbBus. Runtime register access is synchronized by that bus.
|
||||
unsafe impl Sync for F722UsbPeripheral {}
|
||||
|
||||
// SAFETY: the F722 OTG FS register layout, FIFO depth, and endpoint count are
|
||||
// the values required by synopsys-usb-otg for this device peripheral.
|
||||
unsafe impl UsbPeripheral for F722UsbPeripheral {
|
||||
const REGISTERS: *const () = stm32f722::OTG_FS_GLOBAL::ptr() as *const ();
|
||||
const HIGH_SPEED: bool = false;
|
||||
const FIFO_DEPTH_WORDS: usize = 320;
|
||||
const ENDPOINT_COUNT: usize = 6;
|
||||
|
||||
fn enable() {
|
||||
cortex_m::interrupt::free(|_| {
|
||||
// SAFETY: this static PAC view is used only for the USB clock/reset
|
||||
// fields while the owned RCC token remains in platform init.
|
||||
let rcc = unsafe { &*stm32f722::RCC::ptr() };
|
||||
rcc.dckcfgr2().modify(|_, write| write.ck48msel().pll());
|
||||
rcc.ahb2enr().modify(|_, write| write.otgfsen().set_bit());
|
||||
rcc.ahb2rstr().modify(|_, write| write.otgfsrst().set_bit());
|
||||
rcc.ahb2rstr()
|
||||
.modify(|_, write| write.otgfsrst().clear_bit());
|
||||
});
|
||||
}
|
||||
|
||||
fn ahb_frequency_hz(&self) -> u32 {
|
||||
CORE_FREQUENCY_HZ
|
||||
}
|
||||
}
|
||||
|
||||
type F722UsbBus = UsbBus<F722UsbPeripheral>;
|
||||
type F722UsbBusAllocator = UsbBusAllocator<F722UsbBus>;
|
||||
|
||||
fn device_serial_number(serial: &'static mut [u8; SERIAL_BYTES]) -> Result<&'static str, ()> {
|
||||
const HEX: &[u8; 16] = b"0123456789ABCDEF";
|
||||
for word_index in 0..3 {
|
||||
// SAFETY: STM32F722 exposes a readable 96-bit factory device identifier
|
||||
// at DEVICE_ID_ADDRESS. The singleton buffer is exclusively borrowed.
|
||||
let word = unsafe { core::ptr::read_volatile(DEVICE_ID_ADDRESS.add(word_index)) };
|
||||
for byte_index in 0..4 {
|
||||
let byte = (word >> (byte_index * 8)) as u8;
|
||||
let output = (word_index * 4 + byte_index) * 2;
|
||||
serial[output] = HEX[usize::from(byte >> 4)];
|
||||
serial[output + 1] = HEX[usize::from(byte & 0x0f)];
|
||||
}
|
||||
}
|
||||
core::str::from_utf8(serial).map_err(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) type F722UsbMavlink = UsbMavlink<'static, F722UsbBus>;
|
||||
|
||||
/// Initializes the board's singleton CDC-ACM MAVLink endpoint.
|
||||
pub(crate) fn usb_mavlink(
|
||||
global: stm32f722::OTG_FS_GLOBAL,
|
||||
device: stm32f722::OTG_FS_DEVICE,
|
||||
power_clock: stm32f722::OTG_FS_PWRCLK,
|
||||
product: &'static str,
|
||||
) -> Result<F722UsbMavlink, ()> {
|
||||
let peripheral = F722UsbPeripheral {
|
||||
_global: global,
|
||||
_device: device,
|
||||
_power_clock: power_clock,
|
||||
};
|
||||
let endpoint_memory = cortex_m::singleton!(
|
||||
USB_ENDPOINT_MEMORY: [u32; ENDPOINT_MEMORY_WORDS] = [0; ENDPOINT_MEMORY_WORDS]
|
||||
)
|
||||
.ok_or(())?;
|
||||
let allocator = UsbBus::new(peripheral, endpoint_memory);
|
||||
let allocator = cortex_m::singleton!(
|
||||
USB_BUS_ALLOCATOR: F722UsbBusAllocator = allocator
|
||||
)
|
||||
.ok_or(())?;
|
||||
let serial_memory =
|
||||
cortex_m::singleton!(USB_SERIAL: [u8; SERIAL_BYTES] = [0; SERIAL_BYTES]).ok_or(())?;
|
||||
let serial_number = device_serial_number(serial_memory)?;
|
||||
let cdc = CdcAcmClass::new(allocator, USB_PACKET_BYTES as u16);
|
||||
let device = UsbDeviceBuilder::new(allocator, UsbVidPid(USB_VID, USB_PID))
|
||||
.strings(&[StringDescriptors::default()
|
||||
.manufacturer("Kaptan")
|
||||
.product(product)
|
||||
.serial_number(serial_number)])
|
||||
.map_err(|_| ())?
|
||||
.device_class(usbd_serial::USB_CLASS_CDC)
|
||||
.max_packet_size_0(USB_PACKET_BYTES as u8)
|
||||
.map_err(|_| ())?
|
||||
.build();
|
||||
Ok(UsbMavlink::new(device, cdc))
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//! Bounded MAVLink byte stream over CDC-ACM bulk packets.
|
||||
|
||||
use core::ops::Range;
|
||||
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
receiver::{IntegrityCheckedFrame, IntegrityMavlinkFramer},
|
||||
};
|
||||
use kaptan_tx::{PendingFrame, TransmitQueue};
|
||||
use usb_device::{
|
||||
UsbError,
|
||||
bus::UsbBus,
|
||||
device::{UsbDevice, UsbDeviceState},
|
||||
};
|
||||
use usbd_serial::CdcAcmClass;
|
||||
|
||||
pub(crate) const USB_PACKET_BYTES: usize = 64;
|
||||
|
||||
pub(crate) struct UsbMavlink<'a, B: UsbBus> {
|
||||
device: UsbDevice<'a, B>,
|
||||
cdc: CdcAcmClass<'a, B>,
|
||||
transmit: TransmitQueue<MavlinkFrame, 4>,
|
||||
tx_offset: usize,
|
||||
framer: IntegrityMavlinkFramer,
|
||||
rx_packet: [u8; USB_PACKET_BYTES],
|
||||
rx_remaining: Range<usize>,
|
||||
}
|
||||
|
||||
impl<'a, B: UsbBus> UsbMavlink<'a, B> {
|
||||
pub(crate) fn new(device: UsbDevice<'a, B>, cdc: CdcAcmClass<'a, B>) -> Self {
|
||||
Self {
|
||||
device,
|
||||
cdc,
|
||||
transmit: TransmitQueue::new(),
|
||||
tx_offset: 0,
|
||||
framer: IntegrityMavlinkFramer::new(),
|
||||
rx_packet: [0; USB_PACKET_BYTES],
|
||||
rx_remaining: 0..0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_open(&self) -> bool {
|
||||
self.device.state() == UsbDeviceState::Configured && self.cdc.dtr()
|
||||
}
|
||||
|
||||
/// Returns whether the frame was retained, not whether the host received it.
|
||||
pub(crate) fn send(&mut self, pending: PendingFrame<MavlinkFrame>) -> bool {
|
||||
if !self.is_open() {
|
||||
return false;
|
||||
}
|
||||
let retained = self.transmit.enqueue(pending).is_retained();
|
||||
self.service_transmit();
|
||||
retained
|
||||
}
|
||||
|
||||
/// Services USB and buffers at most one packet for the protocol worker.
|
||||
pub(crate) fn poll(&mut self) -> bool {
|
||||
let previous_state = self.device.state();
|
||||
let previous_dtr = self.cdc.dtr();
|
||||
self.device.poll(&mut [&mut self.cdc]);
|
||||
|
||||
if previous_state != UsbDeviceState::Default
|
||||
&& self.device.state() == UsbDeviceState::Default
|
||||
{
|
||||
// Only bus reset flushes endpoint FIFOs. DTR close, suspend, and
|
||||
// deconfiguration pause TX without invalidating its byte cursor.
|
||||
self.transmit = TransmitQueue::new();
|
||||
self.tx_offset = 0;
|
||||
self.reset_receive();
|
||||
} else if previous_dtr && !self.cdc.dtr() {
|
||||
self.reset_receive();
|
||||
}
|
||||
|
||||
if self.rx_remaining.is_empty() && self.device.state() == UsbDeviceState::Configured {
|
||||
// A previous poll may have left a packet in the bus while our
|
||||
// buffer was occupied. Reading must not depend on poll's result.
|
||||
match self.cdc.read_packet(&mut self.rx_packet) {
|
||||
Ok(length) if self.is_open() => self.rx_remaining = 0..length,
|
||||
Ok(_) | Err(UsbError::WouldBlock) => {}
|
||||
Err(error) => panic!("CDC OUT endpoint invariant: {error:?}"),
|
||||
}
|
||||
}
|
||||
self.service_transmit();
|
||||
!self.rx_remaining.is_empty()
|
||||
}
|
||||
|
||||
/// Parses buffered bytes in the worker, without a second receive queue.
|
||||
pub(crate) fn take_received(&mut self) -> Option<IntegrityCheckedFrame> {
|
||||
for index in self.rx_remaining.by_ref() {
|
||||
if let Some(frame) = self.framer.receive_byte(self.rx_packet[index]) {
|
||||
return Some(frame);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn reset_receive(&mut self) {
|
||||
self.framer.reset();
|
||||
self.rx_remaining = 0..0;
|
||||
}
|
||||
|
||||
fn service_transmit(&mut self) {
|
||||
if !self.is_open() {
|
||||
return;
|
||||
}
|
||||
let mut complete = false;
|
||||
let offset = &mut self.tx_offset;
|
||||
let cdc = &mut self.cdc;
|
||||
let mut write = |frame: &MavlinkFrame| {
|
||||
let bytes = frame.raw_bytes();
|
||||
let end = (*offset + USB_PACKET_BYTES).min(bytes.len());
|
||||
let packet = &bytes[*offset..end];
|
||||
let written = cdc.write_packet(packet)?;
|
||||
assert_eq!(written, packet.len(), "CDC IN endpoint short write");
|
||||
*offset = end;
|
||||
// An exact multiple of 64 needs a ZLP: retain offset == len until
|
||||
// the next empty packet is accepted. The endpoint copies bytes,
|
||||
// so a final short write ends its borrow of queue-owned storage.
|
||||
complete = end == bytes.len() && packet.len() < USB_PACKET_BYTES;
|
||||
Ok(())
|
||||
};
|
||||
let result = match self.transmit.active() {
|
||||
Some(active) => write(&active.frame),
|
||||
None => self.transmit.start_with(write).map(|_| ()),
|
||||
};
|
||||
match result {
|
||||
Ok(()) if complete => {
|
||||
self.transmit.complete();
|
||||
self.tx_offset = 0;
|
||||
}
|
||||
Ok(()) | Err(UsbError::WouldBlock) => {}
|
||||
Err(error) => panic!("CDC IN endpoint invariant: {error:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ fn ordinary_f722_image_has_direct_configuration_after_output_inhibition() {
|
||||
for required in [
|
||||
"config_file: F722ConfigFile",
|
||||
"file_service: FileService",
|
||||
"receiver: IntegrityMavlinkReceiver<1>",
|
||||
"uart_receiver: IntegrityMavlinkReceiver<1>",
|
||||
"file_lease_ticks: u16",
|
||||
"TRANSFER_REPLAY_LEASE_SECONDS",
|
||||
"let Some(message) = frame.decode()",
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use kaptan_mavlink::{
|
||||
MavlinkFrame,
|
||||
encoder::MavlinkEncoder,
|
||||
generated::kaptan::{FILE_DATA_DATA, MavMessage},
|
||||
};
|
||||
use kaptan_tx::{FrameClass, PendingFrame};
|
||||
use usb_device::{
|
||||
UsbDirection, UsbError,
|
||||
bus::{PollResult, UsbBus, UsbBusAllocator},
|
||||
device::{UsbDeviceBuilder, UsbVidPid},
|
||||
endpoint::{EndpointAddress, EndpointType},
|
||||
};
|
||||
use usbd_serial::CdcAcmClass;
|
||||
|
||||
#[path = "../src/f722/usb.rs"]
|
||||
mod usb;
|
||||
use usb::UsbMavlink;
|
||||
|
||||
// One pending packet per endpoint, with host-controlled completion. Device and
|
||||
// CDC control requests run through the real usb-device/usbd-serial code.
|
||||
#[derive(Default)]
|
||||
struct BusState {
|
||||
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,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct Host(Arc<Mutex<BusState>>);
|
||||
|
||||
struct PacketBus(Arc<Mutex<BusState>>);
|
||||
|
||||
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) {}
|
||||
|
||||
fn reset(&self) {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
state.input.fill(None);
|
||||
state.output.fill(None);
|
||||
state.events.clear();
|
||||
}
|
||||
|
||||
fn set_device_address(&self, _: u8) {}
|
||||
|
||||
fn write(&self, address: EndpointAddress, bytes: &[u8]) -> Result<usize, UsbError> {
|
||||
let mut state = self.0.lock().unwrap();
|
||||
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();
|
||||
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();
|
||||
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 {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.events
|
||||
.pop_front()
|
||||
.unwrap_or(PollResult::None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Host {
|
||||
fn event(&self, event: PollResult) {
|
||||
self.0.lock().unwrap().events.push_back(event);
|
||||
}
|
||||
|
||||
fn control(&self, link: &mut UsbMavlink<'_, PacketBus>, request: [u8; 8]) {
|
||||
self.0.lock().unwrap().output[0] = Some(request.to_vec());
|
||||
self.event(PollResult::Data {
|
||||
ep_out: 0,
|
||||
ep_in_complete: 0,
|
||||
ep_setup: 1,
|
||||
});
|
||||
link.poll();
|
||||
assert_eq!(self.take_input(0), Some(Vec::new()));
|
||||
link.poll();
|
||||
}
|
||||
|
||||
fn configure(&self, link: &mut UsbMavlink<'_, PacketBus>, configured: bool) {
|
||||
self.control(link, [0, 9, u8::from(configured), 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
fn dtr(&self, link: &mut UsbMavlink<'_, PacketBus>, open: bool) {
|
||||
self.control(link, [0x21, 0x22, u8::from(open), 0, 0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
fn write_packet(&self, bytes: &[u8]) {
|
||||
assert!(bytes.len() <= 64);
|
||||
let mut state = self.0.lock().unwrap();
|
||||
let endpoint = state.bulk_out;
|
||||
assert!(state.output[endpoint].is_none());
|
||||
state.output[endpoint] = Some(bytes.to_vec());
|
||||
state.events.push_back(PollResult::Data {
|
||||
ep_out: 1 << endpoint,
|
||||
ep_in_complete: 0,
|
||||
ep_setup: 0,
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
Some(packet)
|
||||
}
|
||||
|
||||
fn collect_packets(&self, link: &mut UsbMavlink<'_, PacketBus>) -> Vec<Vec<u8>> {
|
||||
let endpoint = self.0.lock().unwrap().bulk_in;
|
||||
let mut packets = Vec::new();
|
||||
for _ in 0..16 {
|
||||
let Some(packet) = self.take_input(endpoint) else {
|
||||
return packets;
|
||||
};
|
||||
packets.push(packet);
|
||||
link.poll();
|
||||
}
|
||||
panic!("transmission did not finish");
|
||||
}
|
||||
}
|
||||
|
||||
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.event(PollResult::Reset);
|
||||
link.poll();
|
||||
host.configure(&mut link, true);
|
||||
host.dtr(&mut link, true);
|
||||
assert!(link.is_open());
|
||||
test(&host, &mut link);
|
||||
}
|
||||
|
||||
fn pending(frame: MavlinkFrame) -> PendingFrame<MavlinkFrame> {
|
||||
PendingFrame {
|
||||
class: FrameClass::Critical,
|
||||
frame,
|
||||
}
|
||||
}
|
||||
|
||||
fn file_frame(length: usize) -> MavlinkFrame {
|
||||
// FILE_DATA has nine scalar bytes before its trailing data array. Setting
|
||||
// the final data byte controls MAVLink 2's trailing-zero truncation.
|
||||
let mut data = FILE_DATA_DATA::DEFAULT;
|
||||
data.data[..length - 12 - 9].fill(0x5a);
|
||||
let frame = MavlinkEncoder::new(1, 1).encode(&data);
|
||||
assert_eq!(frame.raw_bytes().len(), length);
|
||||
frame
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn receives_split_frame_followed_by_coalesced_frames() {
|
||||
with_link(|host, link| {
|
||||
// HEARTBEAT ID 51027, source 255:1, sequence 0, empty payload,
|
||||
// CRC-16/MCRF4XX with CRC_EXTRA 161. Integrity admission precedes payload
|
||||
// validation, so these 12-byte frames must also survive USB batching.
|
||||
let mut minimum = MavlinkFrame::new();
|
||||
minimum.as_mut_slice()[..12]
|
||||
.copy_from_slice(&[0xfd, 0, 0, 0, 0, 255, 1, 0x53, 0xc7, 0, 0x2e, 0xa8]);
|
||||
assert!(minimum.has_valid_crc::<MavMessage>());
|
||||
let mut frames = [minimum; 6];
|
||||
frames[0] = MavlinkEncoder::new(255, 1).heartbeat();
|
||||
let split = frames[0].raw_bytes().len() - 1;
|
||||
host.write_packet(&frames[0].raw_bytes()[..split]);
|
||||
assert!(link.poll());
|
||||
assert!(link.take_received().is_none());
|
||||
|
||||
// One carried-over CRC byte and five complete frames: six completions
|
||||
// in 61 bytes, exceeding the former five-frame receive queue.
|
||||
let mut packet = frames[0].raw_bytes()[split..].to_vec();
|
||||
for frame in &frames[1..] {
|
||||
packet.extend_from_slice(frame.raw_bytes());
|
||||
}
|
||||
host.write_packet(&packet);
|
||||
assert!(link.poll());
|
||||
for frame in &frames {
|
||||
assert_eq!(link.take_received().unwrap().raw(), frame);
|
||||
}
|
||||
assert!(link.take_received().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drains_endpoint_backlog_after_its_poll_event_was_consumed() {
|
||||
with_link(|host, link| {
|
||||
let mut encoder = MavlinkEncoder::new(255, 1);
|
||||
let first = encoder.heartbeat();
|
||||
let second = encoder.heartbeat();
|
||||
host.write_packet(first.raw_bytes());
|
||||
assert!(link.poll());
|
||||
host.write_packet(second.raw_bytes());
|
||||
assert!(link.poll());
|
||||
assert_eq!(link.take_received().unwrap().raw(), &first);
|
||||
assert!(link.take_received().is_none());
|
||||
|
||||
assert!(host.0.lock().unwrap().events.is_empty());
|
||||
assert!(link.poll());
|
||||
assert_eq!(link.take_received().unwrap().raw(), &second);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn endpoint_backpressure_neither_repeats_nor_interleaves_frame_bytes() {
|
||||
with_link(|host, link| {
|
||||
let frame = file_frame(130);
|
||||
let next = MavlinkEncoder::new(1, 1).heartbeat();
|
||||
assert!(link.send(pending(frame)));
|
||||
assert!(link.send(pending(next)));
|
||||
for _ in 0..4 {
|
||||
link.poll();
|
||||
}
|
||||
let packets = host.collect_packets(link);
|
||||
assert_eq!(
|
||||
packets.iter().map(Vec::len).collect::<Vec<_>>(),
|
||||
[64, 64, 2, 13]
|
||||
);
|
||||
assert_eq!(
|
||||
packets.concat(),
|
||||
[frame.raw_bytes(), next.raw_bytes()].concat()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_size_final_packet_is_followed_by_one_zlp() {
|
||||
for length in [64, 128] {
|
||||
with_link(|host, link| {
|
||||
let frame = file_frame(length);
|
||||
let next = MavlinkEncoder::new(1, 1).heartbeat();
|
||||
assert!(link.send(pending(frame)));
|
||||
assert!(link.send(pending(next)));
|
||||
let packets = host.collect_packets(link);
|
||||
let mut lengths = vec![64; length / 64];
|
||||
lengths.extend([0, 13]);
|
||||
assert_eq!(packets.iter().map(Vec::len).collect::<Vec<_>>(), lengths);
|
||||
assert_eq!(
|
||||
packets.concat(),
|
||||
[frame.raw_bytes(), next.raw_bytes()].concat()
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_admission_preserves_reserved_frame_and_replaces_only_pending_frames() {
|
||||
with_link(|host, link| {
|
||||
let mut encoder = MavlinkEncoder::new(1, 1);
|
||||
let first = encoder.heartbeat();
|
||||
let reserved = encoder.heartbeat();
|
||||
assert!(link.send(pending(first)));
|
||||
// The endpoint still owns the first packet; this start must retry.
|
||||
assert!(link.send(pending(reserved)));
|
||||
for _ in 0..4 {
|
||||
assert!(link.send(PendingFrame {
|
||||
class: FrameClass::BestEffort,
|
||||
frame: encoder.heartbeat(),
|
||||
}));
|
||||
}
|
||||
let critical = std::array::from_fn::<_, 4, _>(|_| encoder.heartbeat());
|
||||
for frame in critical {
|
||||
assert!(link.send(pending(frame)));
|
||||
}
|
||||
assert!(!link.send(pending(encoder.heartbeat())));
|
||||
let vital = encoder.heartbeat();
|
||||
assert!(link.send(PendingFrame {
|
||||
class: FrameClass::Vital,
|
||||
frame: vital,
|
||||
}));
|
||||
|
||||
let packets = host.collect_packets(link);
|
||||
assert_eq!(packets.len(), 6);
|
||||
assert_eq!(packets[0], first.raw_bytes());
|
||||
assert_eq!(packets[1], reserved.raw_bytes());
|
||||
assert_eq!(packets[2], vital.raw_bytes());
|
||||
let mut pending = critical.map(|frame| frame.raw_bytes().to_vec()).to_vec();
|
||||
for packet in &packets[3..] {
|
||||
let index = pending.iter().position(|frame| frame == packet).unwrap();
|
||||
pending.remove(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dtr_suspend_and_deconfiguration_preserve_unfinished_transmission() {
|
||||
enum Pause {
|
||||
Dtr,
|
||||
Suspend,
|
||||
Deconfigure,
|
||||
}
|
||||
for pause in [Pause::Dtr, Pause::Suspend, Pause::Deconfigure] {
|
||||
with_link(|host, link| {
|
||||
let frame = file_frame(130);
|
||||
assert!(link.send(pending(frame)));
|
||||
match pause {
|
||||
Pause::Dtr => host.dtr(link, false),
|
||||
Pause::Suspend => {
|
||||
host.event(PollResult::Suspend);
|
||||
link.poll();
|
||||
}
|
||||
Pause::Deconfigure => host.configure(link, false),
|
||||
}
|
||||
assert!(!link.is_open());
|
||||
assert!(!link.send(pending(MavlinkEncoder::new(1, 1).heartbeat())));
|
||||
match pause {
|
||||
Pause::Dtr => host.dtr(link, true),
|
||||
Pause::Suspend => {
|
||||
host.event(PollResult::Resume);
|
||||
link.poll();
|
||||
}
|
||||
Pause::Deconfigure => host.configure(link, true),
|
||||
}
|
||||
assert!(link.is_open());
|
||||
assert_eq!(host.collect_packets(link).concat(), frame.raw_bytes());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dtr_close_discards_partial_receive_frame() {
|
||||
with_link(|host, link| {
|
||||
let mut encoder = MavlinkEncoder::new(255, 1);
|
||||
let interrupted = encoder.heartbeat();
|
||||
let next = encoder.heartbeat();
|
||||
host.write_packet(&interrupted.raw_bytes()[..8]);
|
||||
link.poll();
|
||||
assert!(link.take_received().is_none());
|
||||
host.dtr(link, false);
|
||||
host.dtr(link, true);
|
||||
host.write_packet(next.raw_bytes());
|
||||
link.poll();
|
||||
assert_eq!(link.take_received().unwrap().raw(), &next);
|
||||
assert!(link.take_received().is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bus_reset_discards_transmit_and_receive_state() {
|
||||
with_link(|host, link| {
|
||||
let frame = file_frame(130);
|
||||
assert!(link.send(pending(frame)));
|
||||
host.write_packet(&frame.raw_bytes()[..8]);
|
||||
link.poll();
|
||||
assert!(link.take_received().is_none());
|
||||
host.event(PollResult::Reset);
|
||||
link.poll();
|
||||
assert!(!link.is_open());
|
||||
host.configure(link, true);
|
||||
host.dtr(link, true);
|
||||
assert!(host.collect_packets(link).is_empty());
|
||||
|
||||
let next = MavlinkEncoder::new(1, 1).heartbeat();
|
||||
assert!(link.send(pending(next)));
|
||||
assert_eq!(host.collect_packets(link).concat(), next.raw_bytes());
|
||||
host.write_packet(next.raw_bytes());
|
||||
link.poll();
|
||||
assert_eq!(link.take_received().unwrap().raw(), &next);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user