workspace: replace runtime with F722 platform

This commit is contained in:
Onur Özkan committed 2026-08-01 20:55:02 +03:00
1 parent 82b0f41237
commit c9ca2f4257
7 files changed
+50 -576

No files matched your search

Generated
+46 -35
View File
@@ -139,6 +139,21 @@ dependencies = [
name = "kaptan-actuator" name = "kaptan-actuator"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "kaptan-board"
version = "0.1.0"
[[package]]
name = "kaptan-data-channel"
version = "0.1.0"
dependencies = [
"kaptan-board",
"kaptan-dma",
"kaptan-mavlink",
"kaptan-sensor",
"kaptan-time",
]
[[package]] [[package]]
name = "kaptan-dma" name = "kaptan-dma"
version = "0.1.0" version = "0.1.0"
@@ -153,6 +168,14 @@ dependencies = [
"kaptan-time", "kaptan-time",
] ]
[[package]]
name = "kaptan-geprc-f722-v2"
version = "0.1.0"
dependencies = [
"kaptan-board",
"platform-stm32f7",
]
[[package]] [[package]]
name = "kaptan-i2c" name = "kaptan-i2c"
version = "0.1.0" version = "0.1.0"
@@ -161,18 +184,8 @@ version = "0.1.0"
name = "kaptan-kakute-f7-mini-v3" name = "kaptan-kakute-f7-mini-v3"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"cortex-m", "kaptan-board",
"cortex-m-rt", "platform-stm32f7",
"kaptan-actuator",
"kaptan-dma",
"kaptan-drivers",
"kaptan-mavlink",
"kaptan-runtime",
"kaptan-spi",
"kaptan-stm32f7",
"panic-halt",
"rtic",
"stm32f7",
] ]
[[package]] [[package]]
@@ -188,17 +201,6 @@ dependencies = [
"mavlink-core", "mavlink-core",
] ]
[[package]]
name = "kaptan-runtime"
version = "0.1.0"
dependencies = [
"kaptan-drivers",
"kaptan-i2c",
"kaptan-sensor",
"kaptan-spi",
"kaptan-time",
]
[[package]] [[package]]
name = "kaptan-sensor" name = "kaptan-sensor"
version = "0.1.0" version = "0.1.0"
@@ -210,18 +212,6 @@ dependencies = [
name = "kaptan-spi" name = "kaptan-spi"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "kaptan-stm32f7"
version = "0.1.0"
dependencies = [
"cortex-m",
"kaptan-dma",
"kaptan-i2c",
"kaptan-spi",
"kaptan-time",
"stm32f7",
]
[[package]] [[package]]
name = "kaptan-time" name = "kaptan-time"
version = "0.1.0" version = "0.1.0"
@@ -316,6 +306,27 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11" checksum = "a513e167849a384b7f9b746e517604398518590a9142f4846a32e3c2a4de7b11"
[[package]]
name = "platform-stm32f7"
version = "0.1.0"
dependencies = [
"cortex-m",
"cortex-m-rt",
"kaptan-actuator",
"kaptan-board",
"kaptan-data-channel",
"kaptan-dma",
"kaptan-drivers",
"kaptan-i2c",
"kaptan-mavlink",
"kaptan-sensor",
"kaptan-spi",
"kaptan-time",
"panic-halt",
"rtic",
"stm32f7",
]
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.14.0" version = "1.14.0"
+4 -2
View File
@@ -1,17 +1,19 @@
[workspace] [workspace]
members = [ members = [
"boards/geprc-f722-v2",
"boards/kakute-f7-mini-v3", "boards/kakute-f7-mini-v3",
"libs/kaptan-actuator", "libs/kaptan-actuator",
"libs/kaptan-board",
"libs/kaptan-data-channel",
"libs/kaptan-dma", "libs/kaptan-dma",
"libs/kaptan-drivers", "libs/kaptan-drivers",
"libs/kaptan-i2c", "libs/kaptan-i2c",
"libs/kaptan-mavlink", "libs/kaptan-mavlink",
"libs/kaptan-runtime",
"libs/kaptan-sensor", "libs/kaptan-sensor",
"libs/kaptan-spi", "libs/kaptan-spi",
"libs/kaptan-stm32f7",
"libs/kaptan-time", "libs/kaptan-time",
"libs/kaptan-tx", "libs/kaptan-tx",
"platforms/stm32f7",
] ]
resolver = "2" resolver = "2"
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "kaptan-runtime"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[lints]
workspace = true
[dependencies]
kaptan-drivers = { path = "../kaptan-drivers" }
kaptan-i2c = { path = "../kaptan-i2c" }
kaptan-sensor = { path = "../kaptan-sensor" }
kaptan-spi = { path = "../kaptan-spi" }
kaptan-time = { path = "../kaptan-time" }
-148
View File
@@ -1,148 +0,0 @@
//! Single-owner MPU acquisition service.
use kaptan_drivers::mpu6000::{Error as Mpu6000Error, Mpu6000, Mpu6000Event};
use kaptan_sensor::imu::ImuSnapshot;
use kaptan_spi::{SpiCompletion, SpiDevice};
use kaptan_time::MonotonicClock;
/// One observable result from the IMU worker.
#[derive(Debug)]
pub enum ImuEvent<BusError> {
/// No accepted sample or terminal fault was produced.
Idle,
/// MPU configuration completed and the DRDY route may be enabled.
Initialized,
/// A coherent IMU observation is ready for telemetry or control.
Sample(ImuSnapshot),
/// The selected sensor path entered a terminal fault state.
Fault(Mpu6000Error<BusError>),
}
/// Owns MPU state, its clock, and sample acceptance.
///
/// Exactly one worker invokes this service. Hardware ISRs only report deferred
/// DRDY, timer, and SPI-DMA terminal events to that worker.
pub struct ImuService<Clock, Bus> {
clock: Clock,
imu: Mpu6000<Bus>,
}
impl<Clock, Bus> ImuService<Clock, Bus> {
/// Creates an inactive acquisition service.
pub const fn new(clock: Clock, imu: Mpu6000<Bus>) -> Self {
Self { clock, imu }
}
}
impl<Clock, Bus> ImuService<Clock, Bus>
where
Clock: MonotonicClock,
Bus: SpiDevice,
{
/// Submits the asynchronous sensor reset transaction.
pub fn begin(&mut self) -> Result<(), Mpu6000Error<Bus::StartError>> {
self.imu.begin_initialization()
}
/// Advances the reset-settling state on a board periodic event.
pub fn on_periodic_tick(&mut self) -> ImuEvent<Bus::StartError> {
if self.imu.is_ready() {
return match self.imu.request_sample(self.clock.now()) {
Ok(()) | Err(Mpu6000Error::NotReady) => ImuEvent::Idle,
Err(error) => ImuEvent::Fault(error),
};
}
let _ = self.clock.now();
match self.imu.advance_initialization() {
Ok(()) => ImuEvent::Idle,
Err(error) => ImuEvent::Fault(error),
}
}
/// Handles one coalesced data-ready event.
pub fn on_data_ready(&mut self) -> ImuEvent<Bus::StartError> {
if !self.imu.is_ready() {
return ImuEvent::Idle;
}
match self.imu.request_sample(self.clock.now()) {
Ok(()) | Err(Mpu6000Error::NotReady) => ImuEvent::Idle,
Err(error) => ImuEvent::Fault(error),
}
}
/// Advances the MPU transaction state from a DMA terminal result.
pub fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent<Bus::StartError> {
match self.imu.on_transfer_complete(completion) {
Mpu6000Event::NotPending | Mpu6000Event::Initializing => ImuEvent::Idle,
Mpu6000Event::Initialized => ImuEvent::Initialized,
Mpu6000Event::Sample(sample) => ImuEvent::Sample(sample),
Mpu6000Event::Fault(error) => ImuEvent::Fault(error),
}
}
}
#[cfg(test)]
mod tests {
use super::{ImuEvent, ImuService};
use kaptan_drivers::mpu6000::{Mpu6000, Mpu6000Config};
use kaptan_spi::{SpiCompletion, SpiDevice};
use kaptan_time::{MonotonicClock, Timestamp};
struct Clock;
impl MonotonicClock for Clock {
fn now(&mut self) -> Timestamp {
Timestamp::from_microseconds(42)
}
}
struct Bus {
response: [u8; 15],
}
impl SpiDevice for Bus {
type StartError = ();
fn start_transfer(&mut self, _: &[u8], receive: &mut [u8]) -> Result<(), Self::StartError> {
receive.copy_from_slice(&self.response[..receive.len()]);
Ok(())
}
fn complete_transfer(&mut self, _: SpiCompletion) {}
}
#[test]
fn accepts_a_sample_only_after_initialization() {
let mut response = [0; 15];
response[1] = 0x68;
let mut service = ImuService::new(
Clock,
Mpu6000::new(Bus { response }, Mpu6000Config::default()),
);
service.begin().unwrap();
assert!(matches!(service.on_data_ready(), ImuEvent::Idle));
assert!(matches!(
service.on_transfer_complete(SpiCompletion::Complete),
ImuEvent::Idle
));
assert!(matches!(service.on_periodic_tick(), ImuEvent::Idle));
for _ in 0..9 {
assert!(matches!(
service.on_transfer_complete(SpiCompletion::Complete),
ImuEvent::Idle
));
}
assert!(matches!(
service.on_transfer_complete(SpiCompletion::Complete),
ImuEvent::Initialized
));
assert!(matches!(service.on_periodic_tick(), ImuEvent::Idle));
assert!(matches!(
service.on_transfer_complete(SpiCompletion::Complete),
ImuEvent::Sample(_)
));
}
}
-20
View File
@@ -1,20 +0,0 @@
//! RTIC- and PAC-independent flight services.
//!
//! Acquisition services and board-owned output workers have separate owners.
//! Boards connect hardware interrupts to their dedicated workers through
//! bounded deferred events; they must not place unrelated work behind one
//! priority-ceiling resource.
#![no_std]
/// Single-owner IMU acquisition service.
pub mod imu;
/// Lock-free worker handoff primitives.
pub mod mailbox;
/// Single-owner LIDAR-Lite range acquisition service.
pub mod range;
pub use imu::{ImuEvent, ImuService};
pub use mailbox::LatestImuMailbox;
pub use mailbox::LatestRangeMailbox;
pub use range::{RangeEvent, RangeService};
-189
View File
@@ -1,189 +0,0 @@
//! Lock-free single-producer, single-consumer latest-value mailboxes.
use core::{
marker::PhantomData,
sync::atomic::{AtomicU32, Ordering},
};
use kaptan_sensor::{imu::ImuSnapshot, range::RangeSnapshot};
use kaptan_time::Timestamp;
/// Fixed-width snapshot representation for [`LatestMailbox`].
///
/// The representation consists only of atomic words. Implementations must
/// encode and decode every field without loss so an unread older observation
/// can be safely replaced by a newer one.
pub trait MailboxValue<const WORDS: usize>: Copy {
/// Converts a snapshot into its atomic-word representation.
fn into_words(self) -> [u32; WORDS];
/// Reconstructs a snapshot from its atomic-word representation.
fn from_words(words: [u32; WORDS]) -> Self;
}
/// Lock-free latest-value SPSC mailbox.
///
/// Publishing a newer value intentionally replaces an unread older value: the
/// output path is best-effort and must never delay acquisition. The sequence
/// counter is a seqlock; odd values indicate a writer and even values identify
/// a stable snapshot. Exactly one producer and one consumer may use an
/// instance concurrently. Sequence operations are sequentially consistent so
/// a reader cannot validate an old even sequence around concurrently written
/// atomic payload words; the payload words themselves need only be relaxed.
pub struct LatestMailbox<Value, const WORDS: usize> {
sequence: AtomicU32,
words: [AtomicU32; WORDS],
value: PhantomData<Value>,
}
impl<Value: MailboxValue<WORDS>, const WORDS: usize> LatestMailbox<Value, WORDS> {
/// Creates an empty mailbox.
pub const fn new() -> Self {
Self {
sequence: AtomicU32::new(0),
words: [const { AtomicU32::new(0) }; WORDS],
value: PhantomData,
}
}
/// Publishes a snapshot, replacing any unread older snapshot.
pub fn publish(&self, value: Value) {
let sequence = self.sequence.fetch_add(1, Ordering::SeqCst);
debug_assert_eq!(sequence & 1, 0, "SPSC producer must not reenter");
for (destination, word) in self.words.iter().zip(value.into_words()) {
destination.store(word, Ordering::Relaxed);
}
self.sequence
.store(sequence.wrapping_add(2), Ordering::SeqCst);
}
/// Returns the newest stable snapshot different from `observed_sequence`.
pub fn take_newer(&self, observed_sequence: &mut u32) -> Option<Value> {
let before = self.sequence.load(Ordering::SeqCst);
if before == *observed_sequence || before & 1 != 0 {
return None;
}
let mut words = [0; WORDS];
for (destination, source) in words.iter_mut().zip(&self.words) {
*destination = source.load(Ordering::Relaxed);
}
if self.sequence.load(Ordering::SeqCst) != before {
return None;
}
*observed_sequence = before;
Some(Value::from_words(words))
}
}
impl<Value: MailboxValue<WORDS>, const WORDS: usize> Default for LatestMailbox<Value, WORDS> {
fn default() -> Self {
Self::new()
}
}
impl MailboxValue<9> for ImuSnapshot {
fn into_words(self) -> [u32; 9] {
let timestamp = self.timestamp.as_microseconds();
[
timestamp as u32,
(timestamp >> 32) as u32,
self.acceleration_milligravity[0] as u16 as u32,
self.acceleration_milligravity[1] as u16 as u32,
self.acceleration_milligravity[2] as u16 as u32,
self.angular_rate_milliradians_per_second[0] as u16 as u32,
self.angular_rate_milliradians_per_second[1] as u16 as u32,
self.angular_rate_milliradians_per_second[2] as u16 as u32,
self.temperature_centidegrees_celsius as u16 as u32,
]
}
fn from_words(words: [u32; 9]) -> Self {
ImuSnapshot::new(
timestamp_from_words(words[0], words[1]),
[
words[2] as u16 as i16,
words[3] as u16 as i16,
words[4] as u16 as i16,
],
[
words[5] as u16 as i16,
words[6] as u16 as i16,
words[7] as u16 as i16,
],
words[8] as u16 as i16,
)
}
}
impl MailboxValue<3> for RangeSnapshot {
fn into_words(self) -> [u32; 3] {
let timestamp = self.timestamp.as_microseconds();
[
timestamp as u32,
(timestamp >> 32) as u32,
self.distance_millimetres,
]
}
fn from_words(words: [u32; 3]) -> Self {
RangeSnapshot::new(timestamp_from_words(words[0], words[1]), words[2])
}
}
/// Latest-value mailbox for IMU telemetry snapshots.
pub type LatestImuMailbox = LatestMailbox<ImuSnapshot, 9>;
/// Latest-value mailbox for range telemetry snapshots.
pub type LatestRangeMailbox = LatestMailbox<RangeSnapshot, 3>;
const fn timestamp_from_words(low: u32, high: u32) -> Timestamp {
Timestamp::from_microseconds((low as u64) | ((high as u64) << 32))
}
#[cfg(test)]
mod tests {
use super::{LatestImuMailbox, LatestRangeMailbox};
use kaptan_sensor::{imu::ImuSnapshot, range::RangeSnapshot};
use kaptan_time::Timestamp;
#[test]
fn publishes_only_the_newest_unread_imu_sample() {
let mailbox = LatestImuMailbox::new();
let mut observed = 0;
mailbox.publish(ImuSnapshot::new(
Timestamp::from_microseconds(1),
[1; 3],
[2; 3],
3,
));
mailbox.publish(ImuSnapshot::new(
Timestamp::from_microseconds(2),
[4; 3],
[5; 3],
6,
));
let sample = mailbox.take_newer(&mut observed).unwrap();
assert_eq!(sample.timestamp, Timestamp::from_microseconds(2));
assert_eq!(sample.acceleration_milligravity, [4; 3]);
assert!(mailbox.take_newer(&mut observed).is_none());
}
#[test]
fn publishes_only_the_newest_unread_range_sample() {
let mailbox = LatestRangeMailbox::new();
let mut observed = 0;
mailbox.publish(RangeSnapshot::new(Timestamp::from_microseconds(1), 100));
mailbox.publish(RangeSnapshot::new(Timestamp::from_microseconds(2), 200));
assert_eq!(
mailbox.take_newer(&mut observed),
Some(RangeSnapshot::new(Timestamp::from_microseconds(2), 200))
);
assert!(mailbox.take_newer(&mut observed).is_none());
}
}
-165
View File
@@ -1,165 +0,0 @@
//! Single-owner LIDAR-Lite range acquisition service.
use kaptan_drivers::lidar_lite_v3::{
AcquisitionMode, Error as LidarError, LidarLiteV3, LidarLiteV3Event, StartupError,
};
use kaptan_i2c::{I2cBus, I2cCompletion};
use kaptan_sensor::range::RangeSnapshot;
use kaptan_time::MonotonicClock;
/// One observable result from the range worker.
#[derive(Debug)]
pub enum RangeEvent<BusError> {
/// No observation or terminal fault was produced.
Idle,
/// One coherent range observation is ready for publication.
Measurement(RangeSnapshot),
/// The selected sensor path entered a terminal fault state.
Fault(LidarError<BusError>),
}
/// Owns LIDAR-Lite state, its clock, and measurement acceptance.
pub struct RangeService<Clock, Bus> {
clock: Clock,
lidar: LidarLiteV3<Bus>,
mode: AcquisitionMode,
}
impl<Clock, Bus> RangeService<Clock, Bus> {
/// Creates an inactive range acquisition service.
pub const fn new(clock: Clock, lidar: LidarLiteV3<Bus>, mode: AcquisitionMode) -> Self {
Self { clock, lidar, mode }
}
/// Completes the LIDAR-Lite startup interval selected by the board.
pub fn finish_startup(&mut self) -> Result<(), StartupError> {
self.lidar.finish_startup()
}
}
impl<Clock: MonotonicClock, Bus: I2cBus> RangeService<Clock, Bus> {
/// Starts a new measurement, or polls one already in progress.
pub fn on_periodic_tick(&mut self) -> RangeEvent<Bus::StartError> {
let now = self.clock.now();
let result = if self.lidar.is_ready() {
self.lidar.request_measurement(now, self.mode)
} else {
self.lidar.poll_measurement(now)
};
match result {
Ok(()) | Err(LidarError::NotReady) => RangeEvent::Idle,
Err(error) => RangeEvent::Fault(error),
}
}
/// Advances the LIDAR-Lite transaction state from an I²C terminal event.
pub fn on_transaction_complete(
&mut self,
completion: I2cCompletion,
) -> RangeEvent<Bus::StartError> {
match self.lidar.on_transaction_complete(completion) {
LidarLiteV3Event::Measurement(sample) => RangeEvent::Measurement(sample),
LidarLiteV3Event::Fault(error) => RangeEvent::Fault(error),
_ => RangeEvent::Idle,
}
}
/// Acknowledges a board-controller interrupt and advances the acquisition.
///
/// The closure keeps the controller-specific interrupt handling at the
/// board boundary while preserving this service as the sole owner of the
/// LIDAR state machine.
pub fn on_controller_interrupt<F>(&mut self, acknowledge: F) -> RangeEvent<Bus::StartError>
where
F: FnOnce(&mut Bus) -> I2cCompletion,
{
let completion = self.lidar.acknowledge_with(acknowledge);
self.on_transaction_complete(completion)
}
}
#[cfg(test)]
mod tests {
use super::{RangeEvent, RangeService};
use kaptan_drivers::lidar_lite_v3::LidarLiteV3;
use kaptan_i2c::{I2cBus, I2cCompletion};
use kaptan_time::{MonotonicClock, Timestamp};
struct Clock;
impl MonotonicClock for Clock {
fn now(&mut self) -> Timestamp {
Timestamp::from_microseconds(123)
}
}
struct Bus {
response: [u8; 2],
}
impl I2cBus for Bus {
type StartError = ();
fn start_write(&mut self, _: u8, _: &[u8]) -> Result<(), Self::StartError> {
Ok(())
}
fn start_read(&mut self, _: u8, _: usize) -> Result<(), Self::StartError> {
Ok(())
}
fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>) {
if completion == I2cCompletion::Complete
&& let Some(read) = read
{
read.copy_from_slice(&self.response[..read.len()]);
}
}
fn cancel_transaction(&mut self) {}
}
#[test]
fn produces_a_measurement_after_stop_separated_i2c_transactions() {
let mut service = RangeService::new(
Clock,
LidarLiteV3::new(Bus { response: [0; 2] }),
kaptan_drivers::lidar_lite_v3::AcquisitionMode::WithReceiverBiasCorrection,
);
service.finish_startup().unwrap();
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
assert!(matches!(
service.on_transaction_complete(I2cCompletion::Complete),
RangeEvent::Idle
));
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
assert!(matches!(
service.on_controller_interrupt(|bus| {
bus.response = [1 << 5, 0];
I2cCompletion::Complete
}),
RangeEvent::Idle
));
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
assert!(matches!(
service.on_controller_interrupt(|_| I2cCompletion::Complete),
RangeEvent::Idle
));
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
assert!(matches!(
service.on_controller_interrupt(|bus| {
bus.response = [0x01, 0x23];
I2cCompletion::Complete
}),
RangeEvent::Idle
));
assert!(matches!(service.on_periodic_tick(), RangeEvent::Idle));
assert!(matches!(
service.on_controller_interrupt(|_| I2cCompletion::Complete),
RangeEvent::Measurement(sample)
if sample.timestamp == Timestamp::from_microseconds(123)
&& sample.distance_millimetres == 2_910
));
}
}