From e6e1b8caf121cf1063d15309961fe24edd1ca6b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 19 Aug 2026 14:31:48 +0200 Subject: [PATCH 1/7] filters: specialize three-state Kalman algebra --- statime/src/filters/kalman.rs | 530 +++++++++++++++++++++++++++------- statime/src/filters/mod.rs | 1 + 2 files changed, 429 insertions(+), 102 deletions(-) diff --git a/statime/src/filters/kalman.rs b/statime/src/filters/kalman.rs index 8f9a047e1..295ecb669 100644 --- a/statime/src/filters/kalman.rs +++ b/statime/src/filters/kalman.rs @@ -1,7 +1,4 @@ -use super::{ - matrix::{Matrix, Vector}, - FilterEstimate, -}; +use super::FilterEstimate; #[allow(unused_imports)] use crate::float_polyfill::FloatPolyfill; use crate::{ @@ -236,26 +233,47 @@ impl MeasurementErrorEstimator { } } +#[derive(Clone, Copy, Debug)] +struct State { + offset: f64, + frequency: f64, + delay: f64, +} + +/// Symmetric covariance of offset, fractional frequency, and path delay. +#[derive(Clone, Copy, Debug)] +struct Covariance { + offset: f64, + offset_frequency: f64, + offset_delay: f64, + frequency: f64, + frequency_delay: f64, + delay: f64, +} + #[derive(Clone, Debug)] struct InnerFilter { - state: Vector<3>, - uncertainty: Matrix<3, 3>, + state: State, + uncertainty: Covariance, filter_time: Time, } impl InnerFilter { - const MEASUREMENT_SYNC: Matrix<1, 3> = Matrix::new([[1.0, 0.0, 1.0]]); - const MEASUREMENT_DELAY: Matrix<1, 3> = Matrix::new([[1.0, 0.0, -1.0]]); - const MEASUREMENT_PEER_DELAY: Matrix<1, 3> = Matrix::new([[0.0, 0.0, 1.0]]); - fn new(initial_offset: f64, time: Time, config: &KalmanConfiguration) -> Self { Self { - state: Vector::new_vector([initial_offset, 0.0, 0.0]), - uncertainty: Matrix::new([ - [sqr(config.step_threshold.seconds()), 0.0, 0.0], - [0.0, sqr(config.initial_frequency_uncertainty), 0.0], - [0.0, 0.0, sqr(config.step_threshold.seconds())], - ]), + state: State { + offset: initial_offset, + frequency: 0.0, + delay: 0.0, + }, + uncertainty: Covariance { + offset: sqr(config.step_threshold.seconds()), + offset_frequency: 0.0, + offset_delay: 0.0, + frequency: sqr(config.initial_frequency_uncertainty), + frequency_delay: 0.0, + delay: sqr(config.step_threshold.seconds()), + }, filter_time: time, } } @@ -267,64 +285,81 @@ impl InnerFilter { } let delta_t = (time - self.filter_time).seconds(); - let update = Matrix::new([[1.0, delta_t, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]); - let process_noise = Matrix::new([ - [ - wander * delta_t * delta_t * delta_t / 3., - wander * delta_t * delta_t / 2., - 0., - ], - [wander * delta_t * delta_t / 2., wander * delta_t, 0.], - [ - 0., - 0., - config.delay_wander * delta_t * sqr(self.state.ventry(2)), - ], - ]); - - self.state = update * self.state; - self.uncertainty = update * self.uncertainty * update.transpose() + process_noise; + let delta_t2 = delta_t * delta_t; + + self.state.offset += delta_t * self.state.frequency; + self.uncertainty.offset += 2.0 * delta_t * self.uncertainty.offset_frequency + + delta_t2 * self.uncertainty.frequency + + wander * delta_t2 * delta_t / 3.0; + self.uncertainty.offset_frequency += + delta_t * self.uncertainty.frequency + wander * delta_t2 / 2.0; + self.uncertainty.offset_delay += delta_t * self.uncertainty.frequency_delay; + self.uncertainty.frequency += wander * delta_t; + self.uncertainty.delay += config.delay_wander * delta_t * sqr(self.state.delay); self.filter_time = time; } fn absorb_sync_offset(&mut self, sync_offset: f64, variance: f64) { - let measurement_vec = Vector::new_vector([sync_offset]); - let measurement_noise = Matrix::new([[variance]]); - self.absorb_measurement(measurement_vec, Self::MEASUREMENT_SYNC, measurement_noise); + // h = [1, 0, 1]. + let projected = [ + self.uncertainty.offset + self.uncertainty.offset_delay, + self.uncertainty.offset_frequency + self.uncertainty.frequency_delay, + self.uncertainty.offset_delay + self.uncertainty.delay, + ]; + self.absorb_measurement( + sync_offset - self.state.offset - self.state.delay, + variance + projected[0] + projected[2], + projected, + ); } fn absorb_delay_offset(&mut self, delay_offset: f64, variance: f64) { - let measurement_vec = Vector::new_vector([delay_offset]); - let measurement_noise = Matrix::new([[variance]]); - self.absorb_measurement(measurement_vec, Self::MEASUREMENT_DELAY, measurement_noise); + // h = [1, 0, -1]. + let projected = [ + self.uncertainty.offset - self.uncertainty.offset_delay, + self.uncertainty.offset_frequency - self.uncertainty.frequency_delay, + self.uncertainty.offset_delay - self.uncertainty.delay, + ]; + self.absorb_measurement( + delay_offset - self.state.offset + self.state.delay, + variance + projected[0] - projected[2], + projected, + ); } fn absorb_peer_delay(&mut self, peer_delay: f64, variance: f64) { - let measurement_vec = Vector::new_vector([peer_delay]); - let measurement_noise = Matrix::new([[variance]]); + // h = [0, 0, 1]. + let projected = [ + self.uncertainty.offset_delay, + self.uncertainty.frequency_delay, + self.uncertainty.delay, + ]; self.absorb_measurement( - measurement_vec, - Self::MEASUREMENT_PEER_DELAY, - measurement_noise, - ) + peer_delay - self.state.delay, + variance + projected[2], + projected, + ); } fn absorb_measurement( &mut self, - measurement_vec: Vector<1>, - measurement_transform: Matrix<1, 3>, - measurement_noise: Matrix<1, 1>, + innovation: f64, + innovation_variance: f64, + projected: [f64; 3], ) { - let (prediction, uncertainty) = self.predict(measurement_transform); - - let difference = measurement_vec - prediction; - let difference_covariance = uncertainty + measurement_noise; - let update_strength = - self.uncertainty * measurement_transform.transpose() * difference_covariance.inverse(); - self.state = self.state + update_strength * difference; - self.uncertainty = ((Matrix::unit() - update_strength * measurement_transform) - * self.uncertainty) - .symmetrize(); + // projected = P h^T. The scalar Kalman gain is projected divided by + // the innovation variance. + let scale = 1.0 / innovation_variance; + self.state.offset += projected[0] * innovation * scale; + self.state.frequency += projected[1] * innovation * scale; + self.state.delay += projected[2] * innovation * scale; + + self.uncertainty.offset -= projected[0] * projected[0] * scale; + self.uncertainty.offset_frequency -= projected[0] * projected[1] * scale; + self.uncertainty.offset_delay -= projected[0] * projected[2] * scale; + self.uncertainty.frequency -= projected[1] * projected[1] * scale; + self.uncertainty.frequency_delay -= projected[1] * projected[2] * scale; + self.uncertainty.delay -= projected[2] * projected[2] * scale; } fn absorb_frequency_steer( @@ -335,32 +370,26 @@ impl InnerFilter { config: &KalmanConfiguration, ) { self.progress_filtertime(time, wander, config); - self.state = self.state + Vector::new_vector([0., steer * 1e-6, 0.]); + self.state.frequency += steer * 1e-6; } fn absorb_offset_steer(&mut self, steer: f64) { - self.state = self.state + Vector::new_vector([steer, 0., 0.]); + self.state.offset += steer; self.filter_time += Duration::from_seconds(steer); } - fn predict( - &self, - measurement_transform: Matrix, - ) -> (Vector, Matrix) { - let prediction = measurement_transform * self.state; - let uncertainty = - measurement_transform * self.uncertainty * measurement_transform.transpose(); - (prediction, uncertainty) - } - fn predict_sync_offset(&self) -> (f64, f64) { - let (prediction, uncertainty) = self.predict(Self::MEASUREMENT_SYNC); - (prediction.entry(0, 0), uncertainty.entry(0, 0)) + ( + self.state.offset + self.state.delay, + self.uncertainty.offset + 2.0 * self.uncertainty.offset_delay + self.uncertainty.delay, + ) } fn predict_delay_offset(&self) -> (f64, f64) { - let (prediction, uncertainty) = self.predict(Self::MEASUREMENT_DELAY); - (prediction.entry(0, 0), uncertainty.entry(0, 0)) + ( + self.state.offset - self.state.delay, + self.uncertainty.offset - 2.0 * self.uncertainty.offset_delay + self.uncertainty.delay, + ) } } @@ -386,7 +415,7 @@ impl BaseFilter { config: &KalmanConfiguration, ) { if let Some(inner) = &mut self.0 { - if (sync_offset - inner.state.ventry(0)).abs() > config.step_threshold.seconds() { + if (sync_offset - inner.state.offset).abs() > config.step_threshold.seconds() { log::info!("Measurement too far from state, resetting"); *inner = InnerFilter::new(sync_offset, inner.filter_time, config); } else { @@ -402,7 +431,7 @@ impl BaseFilter { config: &KalmanConfiguration, ) { if let Some(inner) = &mut self.0 { - if (delay_offset - inner.state.ventry(0)).abs() > config.step_threshold.seconds() { + if (delay_offset - inner.state.offset).abs() > config.step_threshold.seconds() { log::info!("Measurement too far from state, resetting"); *inner = InnerFilter::new(delay_offset, inner.filter_time, config); } else { @@ -439,42 +468,42 @@ impl BaseFilter { fn offset(&self) -> f64 { self.0 .as_ref() - .map(|inner| inner.state.ventry(0)) + .map(|inner| inner.state.offset) .unwrap_or(0.0) } fn offset_uncertainty(&self, config: &KalmanConfiguration) -> f64 { self.0 .as_ref() - .map(|inner| inner.uncertainty.entry(0, 0).sqrt()) + .map(|inner| inner.uncertainty.offset.sqrt()) .unwrap_or(config.step_threshold.seconds()) } fn freq_offset(&self) -> f64 { self.0 .as_ref() - .map(|inner| inner.state.ventry(1)) + .map(|inner| inner.state.frequency) .unwrap_or(0.0) } fn freq_offset_uncertainty(&self, config: &KalmanConfiguration) -> f64 { self.0 .as_ref() - .map(|inner| inner.uncertainty.entry(1, 1).sqrt()) + .map(|inner| inner.uncertainty.frequency.sqrt()) .unwrap_or(config.initial_frequency_uncertainty) } fn mean_delay(&self) -> f64 { self.0 .as_ref() - .map(|inner| inner.state.ventry(2)) + .map(|inner| inner.state.delay) .unwrap_or(0.0) } fn mean_delay_uncertainty(&self, config: &KalmanConfiguration) -> f64 { self.0 .as_ref() - .map(|inner| inner.uncertainty.entry(2, 2).sqrt()) + .map(|inner| inner.uncertainty.delay.sqrt()) .unwrap_or(config.step_threshold.seconds()) } @@ -790,11 +819,324 @@ impl KalmanFilter { } } +#[cfg(test)] +#[rustfmt::skip] +mod dense_reference { + use super::*; + use crate::filters::matrix::{Matrix, Vector}; + + // This is the previous matrix implementation, retained verbatim as the + // executable reference for the specialized scalar algebra. + #[derive(Clone, Debug)] + struct InnerFilter { + state: Vector<3>, + uncertainty: Matrix<3, 3>, + filter_time: Time, + } + + impl InnerFilter { + const MEASUREMENT_SYNC: Matrix<1, 3> = Matrix::new([[1.0, 0.0, 1.0]]); + const MEASUREMENT_DELAY: Matrix<1, 3> = Matrix::new([[1.0, 0.0, -1.0]]); + const MEASUREMENT_PEER_DELAY: Matrix<1, 3> = Matrix::new([[0.0, 0.0, 1.0]]); + + fn new(initial_offset: f64, time: Time, config: &KalmanConfiguration) -> Self { + Self { + state: Vector::new_vector([initial_offset, 0.0, 0.0]), + uncertainty: Matrix::new([ + [sqr(config.step_threshold.seconds()), 0.0, 0.0], + [0.0, sqr(config.initial_frequency_uncertainty), 0.0], + [0.0, 0.0, sqr(config.step_threshold.seconds())], + ]), + filter_time: time, + } + } + + fn progress_filtertime(&mut self, time: Time, wander: f64, config: &KalmanConfiguration) { + debug_assert!(time >= self.filter_time); + if time < self.filter_time { + return; + } + + let delta_t = (time - self.filter_time).seconds(); + let update = Matrix::new([[1.0, delta_t, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]); + let process_noise = Matrix::new([ + [ + wander * delta_t * delta_t * delta_t / 3., + wander * delta_t * delta_t / 2., + 0., + ], + [wander * delta_t * delta_t / 2., wander * delta_t, 0.], + [ + 0., + 0., + config.delay_wander * delta_t * sqr(self.state.ventry(2)), + ], + ]); + + self.state = update * self.state; + self.uncertainty = update * self.uncertainty * update.transpose() + process_noise; + self.filter_time = time; + } + + fn absorb_sync_offset(&mut self, sync_offset: f64, variance: f64) { + let measurement_vec = Vector::new_vector([sync_offset]); + let measurement_noise = Matrix::new([[variance]]); + self.absorb_measurement(measurement_vec, Self::MEASUREMENT_SYNC, measurement_noise); + } + + fn absorb_delay_offset(&mut self, delay_offset: f64, variance: f64) { + let measurement_vec = Vector::new_vector([delay_offset]); + let measurement_noise = Matrix::new([[variance]]); + self.absorb_measurement(measurement_vec, Self::MEASUREMENT_DELAY, measurement_noise); + } + + fn absorb_peer_delay(&mut self, peer_delay: f64, variance: f64) { + let measurement_vec = Vector::new_vector([peer_delay]); + let measurement_noise = Matrix::new([[variance]]); + self.absorb_measurement( + measurement_vec, + Self::MEASUREMENT_PEER_DELAY, + measurement_noise, + ) + } + + fn absorb_measurement( + &mut self, + measurement_vec: Vector<1>, + measurement_transform: Matrix<1, 3>, + measurement_noise: Matrix<1, 1>, + ) { + let (prediction, uncertainty) = self.predict(measurement_transform); + + let difference = measurement_vec - prediction; + let difference_covariance = uncertainty + measurement_noise; + let update_strength = + self.uncertainty * measurement_transform.transpose() * difference_covariance.inverse(); + self.state = self.state + update_strength * difference; + self.uncertainty = ((Matrix::unit() - update_strength * measurement_transform) + * self.uncertainty) + .symmetrize(); + } + + fn absorb_frequency_steer( + &mut self, + steer: f64, + time: Time, + wander: f64, + config: &KalmanConfiguration, + ) { + self.progress_filtertime(time, wander, config); + self.state = self.state + Vector::new_vector([0., steer * 1e-6, 0.]); + } + + fn absorb_offset_steer(&mut self, steer: f64) { + self.state = self.state + Vector::new_vector([steer, 0., 0.]); + self.filter_time += Duration::from_seconds(steer); + } + + fn predict( + &self, + measurement_transform: Matrix, + ) -> (Vector, Matrix) { + let prediction = measurement_transform * self.state; + let uncertainty = + measurement_transform * self.uncertainty * measurement_transform.transpose(); + (prediction, uncertainty) + } + + fn predict_sync_offset(&self) -> (f64, f64) { + let (prediction, uncertainty) = self.predict(Self::MEASUREMENT_SYNC); + (prediction.entry(0, 0), uncertainty.entry(0, 0)) + } + + fn predict_delay_offset(&self) -> (f64, f64) { + let (prediction, uncertainty) = self.predict(Self::MEASUREMENT_DELAY); + (prediction.entry(0, 0), uncertainty.entry(0, 0)) + } + } + + struct TestRng(u64); + + impl TestRng { + fn next(&mut self) -> u64 { + self.0 ^= self.0 << 13; + self.0 ^= self.0 >> 7; + self.0 ^= self.0 << 17; + self.0 + } + + fn unit(&mut self) -> f64 { + (self.next() >> 11) as f64 / (1_u64 << 53) as f64 + } + + fn signed(&mut self) -> f64 { + 2.0 * self.unit() - 1.0 + } + } + + fn assert_close(actual: f64, expected: f64) { + let scale = actual.abs().max(expected.abs()).max(1.0); + assert!( + (actual - expected).abs() <= 1e-12 * scale, + "actual {actual:e}, expected {expected:e}" + ); + } + + fn assert_equivalent(actual: &super::InnerFilter, expected: &InnerFilter) { + let actual_state = [ + actual.state.offset, + actual.state.frequency, + actual.state.delay, + ]; + let actual_covariance = [ + [ + actual.uncertainty.offset, + actual.uncertainty.offset_frequency, + actual.uncertainty.offset_delay, + ], + [ + actual.uncertainty.offset_frequency, + actual.uncertainty.frequency, + actual.uncertainty.frequency_delay, + ], + [ + actual.uncertainty.offset_delay, + actual.uncertainty.frequency_delay, + actual.uncertainty.delay, + ], + ]; + + for (i, (actual_state, actual_covariance)) in + actual_state.iter().zip(&actual_covariance).enumerate() + { + assert_close(*actual_state, expected.state.ventry(i)); + for (j, actual_covariance) in actual_covariance.iter().enumerate() { + assert_close(*actual_covariance, expected.uncertainty.entry(i, j)); + } + } + assert_eq!(actual.filter_time, expected.filter_time); + } + + #[test] + fn scalar_algebra_matches_previous_matrix_implementation() { + let mut rng = TestRng(0x4d59_5df4_d0f3_3173); + + for _ in 0..128 { + let state = [rng.signed(), rng.signed() * 1e-3, rng.signed()]; + let factor: [[f64; 3]; 3] = + core::array::from_fn(|_| core::array::from_fn(|_| rng.signed())); + let covariance = core::array::from_fn::<_, 3, _>(|i| { + core::array::from_fn::<_, 3, _>(|j| { + (0..3).map(|k| factor[i][k] * factor[j][k]).sum::() + + if i == j { 0.1 } else { 0.0 } + }) + }); + let timebase = Time::from_nanos(0); + let mut actual = super::InnerFilter { + state: State { + offset: state[0], + frequency: state[1], + delay: state[2], + }, + uncertainty: Covariance { + offset: covariance[0][0], + offset_frequency: covariance[0][1], + offset_delay: covariance[0][2], + frequency: covariance[1][1], + frequency_delay: covariance[1][2], + delay: covariance[2][2], + }, + filter_time: timebase, + }; + let mut expected = InnerFilter { + state: Vector::new_vector(state), + uncertainty: Matrix::new(covariance), + filter_time: timebase, + }; + let config = KalmanConfiguration { + delay_wander: rng.unit() * 1e-3, + ..Default::default() + }; + let wander = rng.unit() * 1e-6; + let time = Time::from_nanos(1 + rng.next() % 10_000_000_000); + + actual.progress_filtertime(time, wander, &config); + expected.progress_filtertime(time, wander, &config); + assert_equivalent(&actual, &expected); + + for (actual_prediction, expected_prediction) in [ + (actual.predict_sync_offset(), expected.predict_sync_offset()), + ( + actual.predict_delay_offset(), + expected.predict_delay_offset(), + ), + ] { + assert_close(actual_prediction.0, expected_prediction.0); + assert_close(actual_prediction.1, expected_prediction.1); + } + + let value = rng.signed(); + let variance = 0.1 + rng.unit(); + actual.absorb_sync_offset(value, variance); + expected.absorb_sync_offset(value, variance); + assert_equivalent(&actual, &expected); + + let value = rng.signed(); + let variance = 0.1 + rng.unit(); + actual.absorb_delay_offset(value, variance); + expected.absorb_delay_offset(value, variance); + assert_equivalent(&actual, &expected); + + let value = rng.signed(); + let variance = 0.1 + rng.unit(); + actual.absorb_peer_delay(value, variance); + expected.absorb_peer_delay(value, variance); + assert_equivalent(&actual, &expected); + + let next_time = time + Duration::from_fixed_nanos(rng.unit() * 1e9); + let steer = rng.signed() * 100.0; + actual.absorb_frequency_steer(steer, next_time, wander, &config); + expected.absorb_frequency_steer(steer, next_time, wander, &config); + assert_equivalent(&actual, &expected); + + let steer = rng.signed() * 1e-3; + actual.absorb_offset_steer(steer); + expected.absorb_offset_steer(steer); + assert_equivalent(&actual, &expected); + } + + // Keep the constructor in the reference coverage as well. + let config = KalmanConfiguration::default(); + let actual = super::InnerFilter::new(0.25, Time::from_nanos(42), &config); + let expected = InnerFilter::new(0.25, Time::from_nanos(42), &config); + assert_equivalent(&actual, &expected); + } +} + #[cfg(test)] mod tests { use super::*; use crate::Clock; + fn inner_filter(time: Time) -> InnerFilter { + InnerFilter { + state: State { + offset: 0.0, + frequency: 0.0, + delay: 0.0, + }, + uncertainty: Covariance { + offset: 1e-17, + offset_frequency: 0.0, + offset_delay: 0.0, + frequency: 1e-16, + frequency_delay: 0.0, + delay: 1e-18, + }, + filter_time: time, + } + } + #[derive(Default)] struct TestClock { last_freq: Option, @@ -832,11 +1174,7 @@ mod tests { max_freq_offset: 10.0, ..Default::default() }, - running_filter: BaseFilter(Some(InnerFilter { - state: Vector::new_vector([0.0, 0.0, 0.0]), - uncertainty: Matrix::new([[1e-17, 0.0, 0.0], [0.0, 1e-16, 0.0], [0.0, 0.0, 1e-18]]), - filter_time: timebase, - })), + running_filter: BaseFilter(Some(inner_filter(timebase))), wander_filter: BaseFilter(None), wander_score: 0, wander: KalmanConfiguration::default().initial_wander, @@ -858,11 +1196,7 @@ mod tests { max_freq_offset: 10.0, ..Default::default() }, - running_filter: BaseFilter(Some(InnerFilter { - state: Vector::new_vector([0.0, 0.0, 0.0]), - uncertainty: Matrix::new([[1e-17, 0.0, 0.0], [0.0, 1e-16, 0.0], [0.0, 0.0, 1e-18]]), - filter_time: timebase, - })), + running_filter: BaseFilter(Some(inner_filter(timebase))), wander_filter: BaseFilter(None), wander_score: 0, wander: KalmanConfiguration::default().initial_wander, @@ -884,11 +1218,7 @@ mod tests { max_freq_offset: 10.0, ..Default::default() }, - running_filter: BaseFilter(Some(InnerFilter { - state: Vector::new_vector([0.0, 0.0, 0.0]), - uncertainty: Matrix::new([[1e-17, 0.0, 0.0], [0.0, 1e-16, 0.0], [0.0, 0.0, 1e-18]]), - filter_time: timebase, - })), + running_filter: BaseFilter(Some(inner_filter(timebase))), wander_filter: BaseFilter(None), wander_score: 0, wander: KalmanConfiguration::default().initial_wander, @@ -906,11 +1236,7 @@ mod tests { max_freq_offset: 10.0, ..Default::default() }, - running_filter: BaseFilter(Some(InnerFilter { - state: Vector::new_vector([0.0, 0.0, 0.0]), - uncertainty: Matrix::new([[1e-17, 0.0, 0.0], [0.0, 1e-16, 0.0], [0.0, 0.0, 1e-18]]), - filter_time: timebase, - })), + running_filter: BaseFilter(Some(inner_filter(timebase))), wander_filter: BaseFilter(None), wander_score: 0, wander: KalmanConfiguration::default().initial_wander, diff --git a/statime/src/filters/mod.rs b/statime/src/filters/mod.rs index 7102ba4f5..b15414298 100644 --- a/statime/src/filters/mod.rs +++ b/statime/src/filters/mod.rs @@ -2,6 +2,7 @@ mod basic; mod kalman; +#[cfg(test)] mod matrix; pub use basic::BasicFilter; From 354a8b6a964d9e558950fcf0f6c5c2df5e517865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 19 Aug 2026 14:59:24 +0200 Subject: [PATCH 2/7] filters: add fixed-wander Kalman servo --- statime/src/filters/fixed_wander.rs | 532 ++++++++++++++++++++++++++++ statime/src/filters/kalman.rs | 103 ++++-- statime/src/filters/mod.rs | 2 + 3 files changed, 611 insertions(+), 26 deletions(-) create mode 100644 statime/src/filters/fixed_wander.rs diff --git a/statime/src/filters/fixed_wander.rs b/statime/src/filters/fixed_wander.rs new file mode 100644 index 000000000..5209f2363 --- /dev/null +++ b/statime/src/filters/fixed_wander.rs @@ -0,0 +1,532 @@ +//! A compact Kalman clock servo for systems with a characterized oscillator. +//! +//! Unlike [`super::KalmanFilter`], this filter uses one estimator and a fixed, +//! configured oscillator-wander model. It still estimates network measurement +//! noise online. This removes the second estimator and its convergence state, +//! but makes the quality of `frequency_wander` part of the application tuning. +//! +//! The [noise-estimation design used by Statime][paper] needs its second, +//! temporarily open-loop estimator specifically because network noise obscures +//! oscillator wander at short intervals. Omitting that estimator is justified +//! when oscillator wander has instead been characterized or conservatively +//! bounded for the target and environment; it is not a generally equivalent +//! replacement for online wander estimation. +//! +//! [paper]: https://tweedegolf.nl/images/estimating-noise-for-clock-synchronizing-kalman-filters-copyright.pdf + +use super::{kalman::InnerFilter, Filter, FilterEstimate, FilterUpdate}; +use crate::{ + port::Measurement, + time::{Duration, Time}, + Clock, +}; + +const ERROR_SAMPLES: usize = 32; + +fn sqr(value: f64) -> f64 { + value * value +} + +/// Configuration for [`FixedWanderKalmanFilter`]. +/// +/// The defaults are a reference preset for an embedded ordinary clock with an +/// uncompensated crystal oscillator, hardware packet timestamps, and one-Hz +/// Sync. They deliberately favor conservative startup over trusting the first +/// few network observations. Applications with oscillator characterization +/// should override `frequency_wander`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct FixedWanderKalmanConfig { + /// Offset above which the clock is stepped instead of slewed. + pub step_threshold: Duration, + /// Time over which an estimated offset is removed by frequency steering. + pub steer_time: Duration, + /// Maximum phase-removal frequency correction, in ppm. + pub max_steer: f64, + /// Maximum total clock frequency correction, in ppm. + pub max_frequency: f64, + /// Initial one-sigma fractional-frequency uncertainty. + /// + /// The 100 ppm default covers the initial tolerance of common XOs without + /// immediately saturating the default clock-actuator range. + pub initial_frequency_uncertainty: f64, + /// Initial one-sigma timestamp-measurement uncertainty. + /// + /// This conservative value is used until four closely paired Sync and + /// DelayReq observations allow measurement noise to be estimated online. + pub initial_measurement_uncertainty: Duration, + /// Fractional-frequency random-walk variance per second. + /// + /// This is the `A` term in the process covariance. In the cited paper, an + /// Intel I210 oscillator measured independently and estimated online under + /// good network conditions both gave approximately `6.25e-18`. The default + /// is the next, four-times-larger estimator bin as a conservative reference; + /// it is not a substitute for target and environment characterization. + pub frequency_wander: f64, + /// Relative path-delay random-walk variance per second. + /// + /// The default grows an initially exact delay estimate to one-percent + /// standard uncertainty after one hour without further observations. + pub delay_wander: f64, +} + +impl Default for FixedWanderKalmanConfig { + fn default() -> Self { + Self { + step_threshold: Duration::from_seconds(1e-3), + steer_time: Duration::from_seconds(2.0), + max_steer: 200.0, + max_frequency: 400.0, + initial_frequency_uncertainty: 100e-6, + initial_measurement_uncertainty: Duration::from_seconds(1e-3), + frequency_wander: 2.5e-17, + delay_wander: 1e-4 / 3600.0, + } + } +} + +#[derive(Clone, Copy, Debug, Default)] +struct MeasurementNoise { + data: [f64; ERROR_SAMPLES], + next: usize, + len: usize, + last_sync: Option<(Time, Duration)>, + last_delay: Option<(Time, Duration)>, + peer_delay: bool, +} + +impl MeasurementNoise { + const RANGE_SAMPLES: usize = 4; + const VARIANCE_SAMPLES: usize = 8; + + fn observe(&mut self, measurement: Measurement, frequency: f64) { + if let Some(sync) = measurement.raw_sync_offset { + if let Some((time, delay)) = self.last_delay.take() { + if (measurement.event_time - time).abs() < Duration::from_millis(200) { + self.push( + sync.seconds() - delay.seconds() + + (time - measurement.event_time).seconds() * frequency, + ); + } else { + self.last_sync = Some((measurement.event_time, sync)); + } + } else { + self.last_sync = Some((measurement.event_time, sync)); + } + } + + if let Some(delay) = measurement.raw_delay_offset { + if let Some((time, sync)) = self.last_sync.take() { + if (measurement.event_time - time).abs() < Duration::from_millis(200) { + self.push( + sync.seconds() - delay.seconds() + + (measurement.event_time - time).seconds() * frequency, + ); + } else { + self.last_delay = Some((measurement.event_time, delay)); + } + } else { + self.last_delay = Some((measurement.event_time, delay)); + } + } + + if let Some(delay) = measurement.peer_delay { + self.last_sync = None; + self.last_delay = None; + self.peer_delay = true; + self.push(delay.seconds()); + } + } + + fn push(&mut self, value: f64) { + self.data[self.next] = value; + self.next = (self.next + 1) % self.data.len(); + self.len = (self.len + 1).min(self.data.len()); + } + + fn variance(&self, config: &FixedWanderKalmanConfig) -> f64 { + if self.len < Self::RANGE_SAMPLES { + sqr(config.initial_measurement_uncertainty.seconds()) + } else if self.len < Self::VARIANCE_SAMPLES { + let values = &self.data[..self.len]; + let (min, max) = values + .iter() + .copied() + .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), value| { + (min.min(value), max.max(value)) + }); + sqr(max - min) + } else { + let values = &self.data[..self.len]; + let mean = values.iter().sum::() / self.len as f64; + // Sync-minus-DelayReq contains two independent one-way errors, so + // its sample variance is twice either observation's variance. + values.iter().map(|value| sqr(value - mean)).sum::() + / (2.0 * (self.len - 1) as f64) + } + } +} + +/// Three-state Kalman clock servo with fixed oscillator-wander covariance. +/// +/// The filter estimates local-minus-master phase, residual fractional +/// frequency, and mean path delay. It uses one estimator and specialized scalar +/// algebra, making it smaller than [`super::KalmanFilter`], which runs a second +/// estimator to learn oscillator wander. +/// +/// Use this filter when the oscillator and its operating environment are known +/// well enough to configure [`FixedWanderKalmanConfig::frequency_wander`], or +/// when deterministic memory, code size, and startup behavior matter more than +/// adapting across unknown hardware. A poor wander value can make the filter +/// either sluggish and overconfident (too small) or noisy (too large); prefer +/// [`super::KalmanFilter`] for general-purpose systems without that knowledge. +pub struct FixedWanderKalmanFilter { + config: FixedWanderKalmanConfig, + estimate: Option, + noise: MeasurementNoise, + frequency: Option, +} + +impl Filter for FixedWanderKalmanFilter { + type Config = FixedWanderKalmanConfig; + + fn new(config: Self::Config) -> Self { + Self { + config, + estimate: None, + noise: MeasurementNoise::default(), + frequency: None, + } + } + + fn measurement(&mut self, measurement: Measurement, clock: &mut C) -> FilterUpdate { + if let Some(estimate) = self.estimate.as_ref() { + if measurement.event_time < estimate.time() { + return FilterUpdate::default(); + } + } + + self.noise.observe( + measurement, + self.estimate.as_ref().map_or(0.0, InnerFilter::frequency), + ); + let variance = + self.noise.variance(&self.config) * if self.noise.peer_delay { 2.0 } else { 1.0 }; + + if measurement.raw_sync_offset.is_some() || measurement.raw_delay_offset.is_some() { + self.ensure_frequency(clock); + } + + let estimate = self.estimate.get_or_insert_with(|| { + InnerFilter::new( + 0.0, + measurement.event_time, + self.config.step_threshold, + self.config.initial_frequency_uncertainty, + ) + }); + estimate.progress_filtertime( + measurement.event_time, + self.config.frequency_wander, + self.config.delay_wander, + ); + if let Some(value) = measurement.raw_sync_offset { + let value = value.seconds(); + if (value - estimate.offset()).abs() > self.config.step_threshold.seconds() { + *estimate = InnerFilter::new( + value, + estimate.time(), + self.config.step_threshold, + self.config.initial_frequency_uncertainty, + ); + } else { + estimate.absorb_sync_offset(value, variance); + } + } + if let Some(value) = measurement.raw_delay_offset { + let value = value.seconds(); + if (value - estimate.offset()).abs() > self.config.step_threshold.seconds() { + *estimate = InnerFilter::new( + value, + estimate.time(), + self.config.step_threshold, + self.config.initial_frequency_uncertainty, + ); + } else { + estimate.absorb_delay_offset(value, variance); + } + } + if let Some(value) = measurement.peer_delay { + estimate.absorb_peer_delay(value.seconds(), variance); + } + + self.steer(clock) + } + + fn update(&mut self, clock: &mut C) -> FilterUpdate { + self.change_frequency(0.0, clock); + FilterUpdate { + next_update: None, + mean_delay: self.mean_delay(), + } + } + + fn demobilize(mut self, clock: &mut C) { + self.change_frequency(0.0, clock); + } + + fn current_estimates(&self) -> FilterEstimate { + FilterEstimate { + offset_from_master: Duration::from_seconds( + self.estimate.as_ref().map_or(0.0, InnerFilter::offset), + ), + mean_delay: self.mean_delay().unwrap_or(Duration::ZERO), + } + } +} + +impl FixedWanderKalmanFilter { + fn ensure_frequency(&mut self, clock: &mut C) { + if self.frequency.is_none() && clock.set_frequency(0.0).is_ok() { + self.frequency = Some(0.0); + } + } + + fn change_frequency(&mut self, target: f64, clock: &mut C) { + let (Some(current), Some(estimate)) = (self.frequency, self.estimate.as_mut()) else { + return; + }; + let requested = target - estimate.frequency() * 1e6; + let next = + (current + requested).clamp(-self.config.max_frequency, self.config.max_frequency); + let applied = next - current; + if let Ok(time) = clock.set_frequency(next) { + self.frequency = Some(next); + estimate.absorb_frequency_steer( + applied, + time, + self.config.frequency_wander, + self.config.delay_wander, + ); + } + } + + fn steer(&mut self, clock: &mut C) -> FilterUpdate { + let Some(estimate) = self.estimate.as_ref() else { + return FilterUpdate::default(); + }; + let offset = estimate.offset(); + if offset.abs() < self.config.step_threshold.seconds() { + let target = (-offset * 1e6 / self.config.steer_time.seconds()) + .clamp(-self.config.max_steer, self.config.max_steer); + self.change_frequency(target, clock); + FilterUpdate { + next_update: Some(core::time::Duration::from_secs_f64( + self.config.steer_time.seconds(), + )), + mean_delay: self.mean_delay(), + } + } else { + if clock.step_clock(Duration::from_seconds(-offset)).is_ok() { + if let Some(estimate) = self.estimate.as_mut() { + estimate.absorb_offset_steer(-offset); + } + } + FilterUpdate { + next_update: None, + mean_delay: self.mean_delay(), + } + } + } + + fn mean_delay(&self) -> Option { + self.estimate + .as_ref() + .map(|estimate| Duration::from_seconds(estimate.delay())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::TimePropertiesDS; + + #[derive(Debug)] + struct TestError; + + struct TestClock { + time: Time, + frequency: f64, + last_step: Option, + fail_frequency: bool, + } + + impl Clock for TestClock { + type Error = TestError; + + fn now(&self) -> Time { + self.time + } + + fn step_clock(&mut self, offset: Duration) -> Result { + self.last_step = Some(offset); + self.time += offset; + Ok(self.time) + } + + fn set_frequency(&mut self, ppm: f64) -> Result { + if self.fail_frequency { + return Err(TestError); + } + self.frequency = ppm; + Ok(self.time) + } + + fn set_properties(&mut self, _: &TimePropertiesDS) -> Result<(), Self::Error> { + Ok(()) + } + } + + fn test_clock(time: Time) -> TestClock { + TestClock { + time, + frequency: 0.0, + last_step: None, + fail_frequency: false, + } + } + + fn assert_close(actual: f64, expected: f64) { + assert!((actual - expected).abs() < 1e-12 * expected.abs().max(1.0)); + } + + #[test] + fn measurement_noise_uses_startup_range_then_ring_variance() { + let config = FixedWanderKalmanConfig { + initial_measurement_uncertainty: Duration::from_seconds(0.5), + ..Default::default() + }; + let mut noise = MeasurementNoise::default(); + + for value in 0..3 { + noise.push(value as f64); + } + assert_close(noise.variance(&config), 0.25); + + noise.push(3.0); + assert_close(noise.variance(&config), 9.0); + + for value in 4..8 { + noise.push(value as f64); + } + assert_close(noise.variance(&config), 3.0); + + for value in 8..33 { + noise.push(value as f64); + } + // The ring now contains 1..=32. Their sample variance is 88, and + // Sync-minus-DelayReq variance is twice the one-way variance. + assert_close(noise.variance(&config), 44.0); + } + + #[test] + fn measurement_noise_pairs_sync_and_delay_with_frequency_correction() { + let mut noise = MeasurementNoise::default(); + noise.observe( + Measurement { + event_time: Time::from_nanos(1_000_000_000), + raw_sync_offset: Some(Duration::from_nanos(10)), + ..Measurement::default() + }, + 1e-6, + ); + noise.observe( + Measurement { + event_time: Time::from_nanos(1_100_000_000), + raw_delay_offset: Some(Duration::from_nanos(2)), + ..Measurement::default() + }, + 1e-6, + ); + + assert_eq!(noise.len, 1); + assert_close(noise.data[0], 108e-9); + } + + #[test] + fn positive_local_phase_error_commands_negative_frequency() { + let time = Time::from_nanos(1_000_000_000); + let mut clock = test_clock(time); + let mut filter = FixedWanderKalmanFilter::new(FixedWanderKalmanConfig::default()); + filter.measurement( + Measurement { + event_time: time, + raw_sync_offset: Some(Duration::from_nanos(100)), + ..Measurement::default() + }, + &mut clock, + ); + assert!(clock.frequency < 0.0); + } + + #[test] + fn frequency_command_respects_actuator_limit() { + let time = Time::from_nanos(1_000_000_000); + let mut clock = test_clock(time); + let mut filter = FixedWanderKalmanFilter::new(FixedWanderKalmanConfig { + max_frequency: 5.0, + ..Default::default() + }); + + filter.measurement( + Measurement { + event_time: time, + raw_sync_offset: Some(Duration::from_micros(100)), + ..Measurement::default() + }, + &mut clock, + ); + + assert_eq!(clock.frequency, -5.0); + } + + #[test] + fn large_phase_error_steps_the_clock() { + let time = Time::from_nanos(1_000_000_000); + let mut clock = test_clock(time); + let mut filter = FixedWanderKalmanFilter::new(FixedWanderKalmanConfig::default()); + + filter.measurement( + Measurement { + event_time: time, + raw_sync_offset: Some(Duration::from_millis(2)), + ..Measurement::default() + }, + &mut clock, + ); + + assert!( + (clock.last_step.unwrap() - Duration::from_millis(-2)).abs() < Duration::from_nanos(1) + ); + assert!(filter.current_estimates().offset_from_master.abs() < Duration::from_nanos(1)); + } + + #[test] + fn failed_frequency_initialization_is_not_assumed_applied() { + let time = Time::from_nanos(1_000_000_000); + let mut clock = test_clock(time); + clock.fail_frequency = true; + let mut filter = FixedWanderKalmanFilter::new(FixedWanderKalmanConfig::default()); + + filter.measurement( + Measurement { + event_time: time, + raw_sync_offset: Some(Duration::from_nanos(100)), + ..Measurement::default() + }, + &mut clock, + ); + + assert_eq!(filter.frequency, None); + assert_eq!(clock.frequency, 0.0); + } +} diff --git a/statime/src/filters/kalman.rs b/statime/src/filters/kalman.rs index 295ecb669..1e2fa34a0 100644 --- a/statime/src/filters/kalman.rs +++ b/statime/src/filters/kalman.rs @@ -233,6 +233,7 @@ impl MeasurementErrorEstimator { } } +/// State `x = [offset, residual fractional frequency, mean path delay]`. #[derive(Clone, Copy, Debug)] struct State { offset: f64, @@ -240,7 +241,7 @@ struct State { delay: f64, } -/// Symmetric covariance of offset, fractional frequency, and path delay. +/// Independent entries of the symmetric covariance `P` for [`State`]. #[derive(Clone, Copy, Debug)] struct Covariance { offset: f64, @@ -252,14 +253,19 @@ struct Covariance { } #[derive(Clone, Debug)] -struct InnerFilter { +pub(super) struct InnerFilter { state: State, uncertainty: Covariance, filter_time: Time, } impl InnerFilter { - fn new(initial_offset: f64, time: Time, config: &KalmanConfiguration) -> Self { + pub(super) fn new( + initial_offset: f64, + time: Time, + initial_offset_uncertainty: Duration, + initial_frequency_uncertainty: f64, + ) -> Self { Self { state: State { offset: initial_offset, @@ -267,18 +273,18 @@ impl InnerFilter { delay: 0.0, }, uncertainty: Covariance { - offset: sqr(config.step_threshold.seconds()), + offset: sqr(initial_offset_uncertainty.seconds()), offset_frequency: 0.0, offset_delay: 0.0, - frequency: sqr(config.initial_frequency_uncertainty), + frequency: sqr(initial_frequency_uncertainty), frequency_delay: 0.0, - delay: sqr(config.step_threshold.seconds()), + delay: sqr(initial_offset_uncertainty.seconds()), }, filter_time: time, } } - fn progress_filtertime(&mut self, time: Time, wander: f64, config: &KalmanConfiguration) { + pub(super) fn progress_filtertime(&mut self, time: Time, wander: f64, delay_wander: f64) { debug_assert!(time >= self.filter_time); if time < self.filter_time { return; @@ -287,6 +293,7 @@ impl InnerFilter { let delta_t = (time - self.filter_time).seconds(); let delta_t2 = delta_t * delta_t; + // Expand F P F^T + Q for F=[[1,dt,0],[0,1,0],[0,0,1]]. self.state.offset += delta_t * self.state.frequency; self.uncertainty.offset += 2.0 * delta_t * self.uncertainty.offset_frequency + delta_t2 * self.uncertainty.frequency @@ -295,11 +302,11 @@ impl InnerFilter { delta_t * self.uncertainty.frequency + wander * delta_t2 / 2.0; self.uncertainty.offset_delay += delta_t * self.uncertainty.frequency_delay; self.uncertainty.frequency += wander * delta_t; - self.uncertainty.delay += config.delay_wander * delta_t * sqr(self.state.delay); + self.uncertainty.delay += delay_wander * delta_t * sqr(self.state.delay); self.filter_time = time; } - fn absorb_sync_offset(&mut self, sync_offset: f64, variance: f64) { + pub(super) fn absorb_sync_offset(&mut self, sync_offset: f64, variance: f64) { // h = [1, 0, 1]. let projected = [ self.uncertainty.offset + self.uncertainty.offset_delay, @@ -313,7 +320,7 @@ impl InnerFilter { ); } - fn absorb_delay_offset(&mut self, delay_offset: f64, variance: f64) { + pub(super) fn absorb_delay_offset(&mut self, delay_offset: f64, variance: f64) { // h = [1, 0, -1]. let projected = [ self.uncertainty.offset - self.uncertainty.offset_delay, @@ -327,7 +334,7 @@ impl InnerFilter { ); } - fn absorb_peer_delay(&mut self, peer_delay: f64, variance: f64) { + pub(super) fn absorb_peer_delay(&mut self, peer_delay: f64, variance: f64) { // h = [0, 0, 1]. let projected = [ self.uncertainty.offset_delay, @@ -347,8 +354,7 @@ impl InnerFilter { innovation_variance: f64, projected: [f64; 3], ) { - // projected = P h^T. The scalar Kalman gain is projected divided by - // the innovation variance. + // With u=P h^T and S=h u+R: x+=u*innovation/S and P-=u u^T/S. let scale = 1.0 / innovation_variance; self.state.offset += projected[0] * innovation * scale; self.state.frequency += projected[1] * innovation * scale; @@ -362,18 +368,18 @@ impl InnerFilter { self.uncertainty.delay -= projected[2] * projected[2] * scale; } - fn absorb_frequency_steer( + pub(super) fn absorb_frequency_steer( &mut self, steer: f64, time: Time, wander: f64, - config: &KalmanConfiguration, + delay_wander: f64, ) { - self.progress_filtertime(time, wander, config); + self.progress_filtertime(time, wander, delay_wander); self.state.frequency += steer * 1e-6; } - fn absorb_offset_steer(&mut self, steer: f64) { + pub(super) fn absorb_offset_steer(&mut self, steer: f64) { self.state.offset += steer; self.filter_time += Duration::from_seconds(steer); } @@ -391,6 +397,22 @@ impl InnerFilter { self.uncertainty.offset - 2.0 * self.uncertainty.offset_delay + self.uncertainty.delay, ) } + + pub(super) fn offset(&self) -> f64 { + self.state.offset + } + + pub(super) fn frequency(&self) -> f64 { + self.state.frequency + } + + pub(super) fn delay(&self) -> f64 { + self.state.delay + } + + pub(super) fn time(&self) -> Time { + self.filter_time + } } #[derive(Default, Debug, Clone)] @@ -403,8 +425,15 @@ impl BaseFilter { fn progress_filtertime(&mut self, time: Time, wander: f64, config: &KalmanConfiguration) { match &mut self.0 { - Some(inner) => inner.progress_filtertime(time, wander, config), - None => self.0 = Some(InnerFilter::new(0.0, time, config)), + Some(inner) => inner.progress_filtertime(time, wander, config.delay_wander), + None => { + self.0 = Some(InnerFilter::new( + 0.0, + time, + config.step_threshold, + config.initial_frequency_uncertainty, + )) + } } } @@ -417,7 +446,12 @@ impl BaseFilter { if let Some(inner) = &mut self.0 { if (sync_offset - inner.state.offset).abs() > config.step_threshold.seconds() { log::info!("Measurement too far from state, resetting"); - *inner = InnerFilter::new(sync_offset, inner.filter_time, config); + *inner = InnerFilter::new( + sync_offset, + inner.filter_time, + config.step_threshold, + config.initial_frequency_uncertainty, + ); } else { inner.absorb_sync_offset(sync_offset, variance) } @@ -433,7 +467,12 @@ impl BaseFilter { if let Some(inner) = &mut self.0 { if (delay_offset - inner.state.offset).abs() > config.step_threshold.seconds() { log::info!("Measurement too far from state, resetting"); - *inner = InnerFilter::new(delay_offset, inner.filter_time, config); + *inner = InnerFilter::new( + delay_offset, + inner.filter_time, + config.step_threshold, + config.initial_frequency_uncertainty, + ); } else { inner.absorb_delay_offset(delay_offset, variance) } @@ -454,8 +493,15 @@ impl BaseFilter { config: &KalmanConfiguration, ) { match &mut self.0 { - Some(inner) => inner.absorb_frequency_steer(steer, time, wander, config), - None => self.0 = Some(InnerFilter::new(0.0, time, config)), + Some(inner) => inner.absorb_frequency_steer(steer, time, wander, config.delay_wander), + None => { + self.0 = Some(InnerFilter::new( + 0.0, + time, + config.step_threshold, + config.initial_frequency_uncertainty, + )) + } } } @@ -1060,7 +1106,7 @@ mod dense_reference { let wander = rng.unit() * 1e-6; let time = Time::from_nanos(1 + rng.next() % 10_000_000_000); - actual.progress_filtertime(time, wander, &config); + actual.progress_filtertime(time, wander, config.delay_wander); expected.progress_filtertime(time, wander, &config); assert_equivalent(&actual, &expected); @@ -1095,7 +1141,7 @@ mod dense_reference { let next_time = time + Duration::from_fixed_nanos(rng.unit() * 1e9); let steer = rng.signed() * 100.0; - actual.absorb_frequency_steer(steer, next_time, wander, &config); + actual.absorb_frequency_steer(steer, next_time, wander, config.delay_wander); expected.absorb_frequency_steer(steer, next_time, wander, &config); assert_equivalent(&actual, &expected); @@ -1107,7 +1153,12 @@ mod dense_reference { // Keep the constructor in the reference coverage as well. let config = KalmanConfiguration::default(); - let actual = super::InnerFilter::new(0.25, Time::from_nanos(42), &config); + let actual = super::InnerFilter::new( + 0.25, + Time::from_nanos(42), + config.step_threshold, + config.initial_frequency_uncertainty, + ); let expected = InnerFilter::new(0.25, Time::from_nanos(42), &config); assert_equivalent(&actual, &expected); } diff --git a/statime/src/filters/mod.rs b/statime/src/filters/mod.rs index b15414298..32117758f 100644 --- a/statime/src/filters/mod.rs +++ b/statime/src/filters/mod.rs @@ -1,11 +1,13 @@ //! Definitions and implementations for the abstracted measurement filters mod basic; +mod fixed_wander; mod kalman; #[cfg(test)] mod matrix; pub use basic::BasicFilter; +pub use fixed_wander::{FixedWanderKalmanConfig, FixedWanderKalmanFilter}; pub use kalman::{KalmanConfiguration, KalmanFilter}; use crate::{port::Measurement, time::Duration, Clock}; From c18ccd283a9f3cd209d3de69b33fc5a1e7f65cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 20 Aug 2026 16:33:51 +0200 Subject: [PATCH 3/7] embassy: add statime-embassy-net crate --- Cargo.toml | 1 + statime-embassy-net/.gitignore | 1 + statime-embassy-net/Cargo.lock | 1310 ++++++++++++++++++++++ statime-embassy-net/Cargo.toml | 47 + statime-embassy-net/README.md | 32 + statime-embassy-net/src/embassy_clock.rs | 214 ++++ statime-embassy-net/src/lib.rs | 773 +++++++++++++ statime-embassy-net/src/monitor.rs | 111 ++ statime-embassy-net/src/storage.rs | 69 ++ 9 files changed, 2558 insertions(+) create mode 100644 statime-embassy-net/.gitignore create mode 100644 statime-embassy-net/Cargo.lock create mode 100644 statime-embassy-net/Cargo.toml create mode 100644 statime-embassy-net/README.md create mode 100644 statime-embassy-net/src/embassy_clock.rs create mode 100644 statime-embassy-net/src/lib.rs create mode 100644 statime-embassy-net/src/monitor.rs create mode 100644 statime-embassy-net/src/storage.rs diff --git a/Cargo.toml b/Cargo.toml index a7a531613..dc9085699 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "statime-linux", ] exclude = [ + "statime-embassy-net", "statime-stm32" ] resolver = "2" diff --git a/statime-embassy-net/.gitignore b/statime-embassy-net/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/statime-embassy-net/.gitignore @@ -0,0 +1 @@ +/target diff --git a/statime-embassy-net/Cargo.lock b/statime-embassy-net/Cargo.lock new file mode 100644 index 000000000..4fee39a26 --- /dev/null +++ b/statime-embassy-net/Cargo.lock @@ -0,0 +1,1310 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alterable_logger" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1f292171444480aedbc1b7d1a11192d1b6e48f26154cf4635481c58b9b419f" +dependencies = [ + "arc-swap", + "log", + "once_cell", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cbor-edn" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b43829b1f353168aa8593c2e4ef6e71a81d6749fe09edfc33849f890c69278" +dependencies = [ + "chrono", + "data-encoding", + "data-encoding-macro", + "encoding_rs", + "hex", + "hexfloat2", + "num-bigint", + "num-traits", + "peg", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "data-encoding-macro" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a127ecbb3c4632e1525380e04c0c3fcf8dcb44d32a79ea290d8a36906edcd8" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54e03a951783e8b327515db3f2a2fd0e3bed362a96b066f341ce66ed49b4ead" +dependencies = [ + "data-encoding", + "syn 3.0.3", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags", + "defmt-macros", +] + +[[package]] +name = "defmt-decoder" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0427c033f70d46bffe9fc575aa27cb43b1a73d6a0bbe25710c7308809196d2" +dependencies = [ + "alterable_logger", + "anyhow", + "byteorder", + "cbor-edn", + "colored", + "defmt-json-schema", + "defmt-parser", + "dissimilar", + "gimli", + "log", + "nom", + "object 0.36.7", + "regex", + "ryu", + "serde", + "serde_json", + "time", +] + +[[package]] +name = "defmt-json-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b04d228e57a61cf385d86bc8980bb41b47c6fc0eace90592668df97b2dad6a" +dependencies = [ + "log", + "serde", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt2log" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5be3b401a817cfe24125124eefeb69c5f9c597c5a2def0de6d81e836578a67" +dependencies = [ + "defmt", + "defmt-decoder", + "defmt-parser", + "findshlibs", + "log", + "object 0.39.1", + "serde_json", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "dissimilar" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeda16ab4059c5fd2a83f2b9c9e9c981327b18aa8e3b313f7e6563799d4f093e" + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" + +[[package]] +name = "embassy-net" +version = "0.9.1" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "defmt", + "document-features", + "embassy-futures", + "embassy-net-driver", + "embassy-sync", + "embassy-time", + "embedded-io-async", + "embedded-nal-async", + "heapless", + "managed", + "xarxa", +] + +[[package]] +name = "embassy-net-driver" +version = "0.2.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "defmt", +] + +[[package]] +name = "embassy-sync" +version = "0.8.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "cfg-if", + "critical-section", + "embedded-io-async", + "futures-core", + "futures-sink", + "heapless", +] + +[[package]] +name = "embassy-time" +version = "0.5.1" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "cfg-if", + "critical-section", + "defmt", + "document-features", + "embassy-time-driver", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.2" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "document-features", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-io" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "embedded-io", +] + +[[package]] +name = "embedded-nal" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56a28be191a992f28f178ec338a0bf02f63d7803244add736d026a471e6ed77" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "embedded-nal-async" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5a1bd585135d302f8f6d7de329310938093da6271b37a6c94b8798795c0c6d" +dependencies = [ + "embedded-io-async", + "embedded-nal", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "fixed" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af2cbf772fa6d1c11358f92ef554cb6b386201210bcf0e91fb7fba8a907fb40" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[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" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "defmt", + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexfloat2" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "befe65164a090041cdf6e0d21a0ec3198d856fbfe2b76e324a073e790bb49f8c" + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +dependencies = [ + "serde_core", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "crc32fast", + "hashbrown", + "indexmap", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "peg" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aad070be5b63aa72103f2fcdd70a83adbd5e90112ce5b574171ff1c65501773" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd8ef6825cae95355031ae26a99b616a2a21f22ba2de0197c43dfb05acbe7ee" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7011d97b484a5ebdc4b1fdb3b12d5e4bbbea56e9d22b688f2e79e04b65a7d8a6" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "statime" +version = "0.4.0" +dependencies = [ + "arrayvec", + "az", + "fixed", + "libm", + "log", + "rand", +] + +[[package]] +name = "statime-embassy-net" +version = "0.1.0" +dependencies = [ + "defmt", + "defmt2log", + "embassy-futures", + "embassy-net", + "embassy-time", + "rand_core", + "rand_xorshift", + "statime", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xarxa" +version = "0.13.1" +source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +dependencies = [ + "bitflags", + "byteorder", + "cfg-if", + "defmt", + "heapless", + "managed", + "xarxa-driver", +] + +[[package]] +name = "xarxa-driver" +version = "0.1.0" +source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +dependencies = [ + "defmt", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/statime-embassy-net/Cargo.toml b/statime-embassy-net/Cargo.toml new file mode 100644 index 000000000..5b1ada136 --- /dev/null +++ b/statime-embassy-net/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "statime-embassy-net" +version = "0.1.0" +edition = "2024" +license = "MIT OR Apache-2.0" +description = "PTP ordinary-clock runner for statime on embassy-net" +repository = "https://github.com/pendulum-project/statime" +readme = "README.md" +publish = false + +[features] +default = [] +defmt = ["dep:defmt", "embassy-net/defmt", "embassy-time/defmt"] +monitor = [] + +[dependencies] +rand_core = "0.6" +rand_xorshift = "0.3" +statime = { path = "../statime", default-features = false } +defmt = { version = "1.0.1", optional = true } +embassy-futures = { version = "0.1.2", default-features = false } +embassy-net = { version = "0.9.1", default-features = false, features = [ + "medium-ethernet", + "multicast", + "packetmeta-id", + "packetmeta-timestamp", + "proto-ipv4", + "udp", +] } +embassy-time = { version = "0.5.1", default-features = false } + +[dev-dependencies] +defmt2log = "0.2.1" + +[patch.crates-io] +embassy-futures = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-net = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-net-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-sync = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-time = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-time-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } + +[profile.release] +codegen-units = 1 +opt-level = "s" +debug = true +lto = "fat" diff --git a/statime-embassy-net/README.md b/statime-embassy-net/README.md new file mode 100644 index 000000000..6d43e735c --- /dev/null +++ b/statime-embassy-net/README.md @@ -0,0 +1,32 @@ +# statime-embassy-net + +PTP ordinary-clock runner for `statime` on timestamp-capable `embassy-net` +Ethernet drivers. + +This crate connects: + +- `statime` for the PTP protocol and servo, +- `embassy-net` for UDP multicast transport, +- a `statime::Clock` implementation controlling the same hardware clock used + for packet timestamps. + +The network driver must provide packet timestamps through `embassy-net` packet +metadata and asynchronous transmit timestamp polling. `EmbassyClock` adapts +any `embassy_net::driver::Clock` to Statime's clock interface. The example +uses Embassy STM32; applications using it must select their concrete +`embassy-stm32` chip feature. + +The runner is currently a single-port UDP/IPv4 ordinary clock using E2E delay +measurement. It is slave-only by default. + +The default servo is Statime's `FixedWanderKalmanFilter`, intended for embedded +systems whose oscillator wander is characterized or conservatively bounded. + +The default feature set has no logging backend. Enable `defmt` for diagnostics +and `monitor` to expose lock-free tracking and holdover state. Enabling +`monitor` does not change `Runner::new`; attach a monitor with +`Runner::with_monitor` where needed. + +See the [`examples/stm32h743`](examples/stm32h743) package for a complete +STM32H743 Embassy application. Its target, linker, probe, and board dependencies +are kept outside the reusable library package. diff --git a/statime-embassy-net/src/embassy_clock.rs b/statime-embassy-net/src/embassy_clock.rs new file mode 100644 index 000000000..558688f21 --- /dev/null +++ b/statime-embassy-net/src/embassy_clock.rs @@ -0,0 +1,214 @@ +use embassy_net::driver::{Clock as NetClock, ScaledPpm}; +use statime::{ + Clock as StatimeClock, + config::TimePropertiesDS, + time::{Duration, Time}, +}; + +use crate::time_from; + +/// A Statime clock backed by an Embassy network driver's clock. +#[derive(Debug)] +pub struct EmbassyClock { + inner: T, +} + +impl EmbassyClock { + /// Wrap an initialized Embassy network clock. + pub const fn new(inner: T) -> Self { + Self { inner } + } + + /// Borrow the underlying network clock. + pub const fn inner(&self) -> &T { + &self.inner + } + + /// Mutably borrow the underlying network clock. + pub const fn inner_mut(&mut self) -> &mut T { + &mut self.inner + } + + /// Unwrap the underlying network clock. + pub fn into_inner(self) -> T { + self.inner + } +} + +/// Error returned by [`EmbassyClock`]. +#[derive(Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum EmbassyClockError { + /// The network driver's clock rejected an operation. + Clock(E), + /// Statime requested a frequency adjustment that is not finite. + NonFiniteFrequency, +} + +impl core::fmt::Display for EmbassyClockError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Clock(error) => write!(formatter, "network clock: {error}"), + Self::NonFiniteFrequency => formatter.write_str("non-finite frequency adjustment"), + } + } +} + +impl core::error::Error for EmbassyClockError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Clock(error) => Some(error), + Self::NonFiniteFrequency => None, + } + } +} + +impl StatimeClock for EmbassyClock { + type Error = EmbassyClockError; + + fn now(&self) -> Time { + time_from(self.inner.now()) + } + + fn step_clock(&mut self, offset: Duration) -> Result { + // Embassy clocks step in whole nanoseconds; preserve Statime's lossy + // conversion and clamp values outside the driver's signed range. + let nanos = offset + .nanos_rounded() + .clamp(i64::MIN as i128, i64::MAX as i128) as i64; + self.inner + .step(nanos) + .map(time_from) + .map_err(EmbassyClockError::Clock) + } + + fn set_frequency(&mut self, ppm: f64) -> Result { + if !ppm.is_finite() { + return Err(EmbassyClockError::NonFiniteFrequency); + } + + // The cast saturates. Sub-LSB truncation is integrated away by the + // servo instead of requiring a separate floating-point rounding step. + let adjustment = ScaledPpm::from_raw((ppm * (1i32 << 16) as f64) as i32); + self.inner + .set_frequency(adjustment) + .map(time_from) + .map_err(EmbassyClockError::Clock) + } + + fn set_properties( + &mut self, + _time_properties_ds: &TimePropertiesDS, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use embassy_net::driver::Timestamp; + + use super::*; + + #[derive(Debug, Default)] + struct TestClock { + fail: bool, + step: i64, + frequency: ScaledPpm, + } + + #[derive(Debug)] + #[cfg_attr(feature = "defmt", derive(defmt::Format))] + struct TestError; + + impl core::fmt::Display for TestError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("test clock failure") + } + } + + impl core::error::Error for TestError {} + + impl NetClock for TestClock { + type Error = TestError; + + fn now(&self) -> Timestamp { + Timestamp::from_seconds_and_nanos(1, 2) + } + + fn step(&mut self, offset_nanos: i64) -> Result { + if self.fail { + return Err(TestError); + } + self.step = offset_nanos; + Ok(self.now()) + } + + fn set_frequency(&mut self, adjustment: ScaledPpm) -> Result { + if self.fail { + return Err(TestError); + } + self.frequency = adjustment; + Ok(self.now()) + } + } + + #[test] + fn converts_statime_adjustments_to_driver_units() { + let mut clock = EmbassyClock::new(TestClock::default()); + + clock.step_clock(Duration::from_fixed_nanos(1.6)).unwrap(); + assert_eq!(clock.inner().step, 1); + + clock.set_frequency(-0.25).unwrap(); + assert_eq!(clock.inner().frequency, ScaledPpm::from_raw(-16_384)); + + clock.set_frequency(f64::MAX).unwrap(); + assert_eq!(clock.inner().frequency, ScaledPpm::from_raw(i32::MAX)); + } + + #[test] + fn clamps_steps_to_the_driver_range() { + let mut clock = EmbassyClock::new(TestClock::default()); + + clock + .step_clock(Duration::from_fixed_nanos(i64::MAX as i128 + 1)) + .unwrap(); + assert_eq!(clock.inner().step, i64::MAX); + + clock + .step_clock(Duration::from_fixed_nanos(i64::MIN as i128 - 1)) + .unwrap(); + assert_eq!(clock.inner().step, i64::MIN); + } + + #[test] + fn rejects_non_finite_frequency() { + let mut clock = EmbassyClock::new(TestClock::default()); + + assert!(matches!( + clock.set_frequency(f64::NAN), + Err(EmbassyClockError::NonFiniteFrequency) + )); + assert_eq!(clock.inner().frequency, ScaledPpm::ZERO); + } + + #[test] + fn propagates_driver_errors() { + #[cfg(feature = "defmt")] + fn assert_format() {} + #[cfg(feature = "defmt")] + assert_format::>(); + + let mut clock = EmbassyClock::new(TestClock { + fail: true, + ..Default::default() + }); + + assert!(matches!( + clock.set_frequency(0.0), + Err(EmbassyClockError::Clock(TestError)) + )); + } +} diff --git a/statime-embassy-net/src/lib.rs b/statime-embassy-net/src/lib.rs new file mode 100644 index 000000000..c201932aa --- /dev/null +++ b/statime-embassy-net/src/lib.rs @@ -0,0 +1,773 @@ +#![cfg_attr(not(test), no_std)] +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + +mod embassy_clock; +#[cfg(feature = "monitor")] +mod monitor; +mod storage; + +use core::num::NonZero; +use embassy_futures::select::{Either3, select3}; +use embassy_net::{ + IpAddress, IpEndpoint, Ipv4Address, Stack, TryError, + driver::{Timestamp, TxTimestamp}, + udp, + udp::{UdpMetadata, UdpSocket}, +}; +use embassy_time::{Duration as EmbassyDuration, Instant, with_deadline}; +use rand_core::SeedableRng; +use rand_xorshift::XorShiftRng; +use statime::{ + Clock, PtpInstance, + config::{ + AcceptAnyMaster, ClockIdentity, ClockQuality, DelayMechanism, InstanceConfig, PortConfig, + PtpMinorVersion, TimePropertiesDS, TimeSource, + }, + filters::{Filter, FixedWanderKalmanFilter}, + observability::port::PortState, + port::{NoForwardedTLVs, PortAction, PortActionIterator, TimestampContext}, + time::{Duration, Interval, Time}, +}; + +#[cfg(feature = "defmt")] +macro_rules! info { + ($($arg:tt)*) => { defmt::info!($($arg)*) }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! info { + ($format:literal $(, $arg:expr)* $(,)?) => {{ + let _ = $format; + $(let _ = &$arg;)* + }}; +} + +#[cfg(feature = "defmt")] +macro_rules! warn { + ($($arg:tt)*) => { defmt::warn!($($arg)*) }; +} + +#[cfg(not(feature = "defmt"))] +macro_rules! warn { + ($format:literal $(, $arg:expr)* $(,)?) => {{ + let _ = $format; + $(let _ = &$arg;)* + }}; +} + +pub use embassy_clock::{EmbassyClock, EmbassyClockError}; +#[cfg(feature = "monitor")] +pub use monitor::{ClockState, PtpMonitor}; +pub use storage::PtpStorage; + +const EVENT_PORT: u16 = 319; +const GENERAL_PORT: u16 = 320; +const PRIMARY_MULTICAST: Ipv4Address = Ipv4Address::new(224, 0, 1, 129); +const LINK_LOCAL_MULTICAST: Ipv4Address = Ipv4Address::new(224, 0, 0, 107); +const TX_TIMESTAMP_TIMEOUT: EmbassyDuration = EmbassyDuration::from_millis(100); +const TX_PENDING: usize = 4; +const MSG_DELAY_REQ: u8 = 0x1; +const MSG_PDELAY_REQ: u8 = 0x2; +const MSG_PDELAY_RESP: u8 = 0x3; + +/// Configuration for one PTP ordinary-clock runner. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Config { + /// Ethernet MAC address used to derive the PTP clock identity. + pub mac_address: [u8; 6], + /// Seed for statime's per-port random number generator. + pub rng_seed: u64, + /// PTP domain number accepted and transmitted by this ordinary clock. + pub domain_number: u8, + /// Best master clock algorithm priority1 value. + pub priority_1: u8, + /// Best master clock algorithm priority2 value. + pub priority_2: u8, + /// Keep this clock out of master state. + pub slave_only: bool, + /// Accuracy and stability advertised when this clock becomes master. + pub clock_quality: ClockQuality, + /// Logarithmic E2E delay request interval. + pub delay_request_interval: Interval, + /// Logarithmic announce interval. + pub announce_interval: Interval, + /// Logarithmic sync interval. + pub sync_interval: Interval, + /// Number of missed announces before announce receipt timeout. + pub announce_receipt_timeout: u8, + /// Static path delay asymmetry correction. + pub delay_asymmetry: Duration, + /// PTP v2 minor version used by the statime port. + pub minor_ptp_version: PtpMinorVersion, + /// Time properties advertised by this clock when it becomes master. + pub time_properties: TimePropertiesDS, + /// Maximum time to wait for a hardware transmit timestamp. + pub tx_timestamp_timeout: EmbassyDuration, +} + +impl Config { + /// Create a slave-only ordinary-clock configuration for `mac_address`. + /// + /// The MAC address defines the clock identity and `rng_seed` seeds + /// statime's per-port random scheduling. + pub fn new(mac_address: [u8; 6], rng_seed: u64) -> Self { + Self { + mac_address, + rng_seed, + domain_number: 0, + priority_1: 128, + priority_2: 128, + slave_only: true, + clock_quality: ClockQuality::default(), + delay_request_interval: Interval::from_log_2(0), + announce_interval: Interval::from_log_2(0), + sync_interval: Interval::from_log_2(0), + announce_receipt_timeout: 3, + delay_asymmetry: Duration::ZERO, + minor_ptp_version: PtpMinorVersion::Zero, + time_properties: TimePropertiesDS::new_arbitrary_time( + false, + false, + TimeSource::InternalOscillator, + ), + tx_timestamp_timeout: TX_TIMESTAMP_TIMEOUT, + } + } +} + +/// Single-port PTP ordinary-clock service. +/// +/// Construct one runner per Ethernet port and call [`run`](Self::run) from a +/// background task. Dropping the future is not a supported recovery path; this +/// is intended to run for the lifetime of the network stack. +/// +/// `F` selects the Statime measurement filter. Its [`Filter::Config`] is +/// supplied to [`new`](Self::new), keeping filter policy outside the Embassy +/// transport adapter. +/// +/// The clock must control the same hardware time domain used for packet +/// timestamps by the underlying network driver. +pub struct Runner<'a, C, F: Filter = FixedWanderKalmanFilter> { + stack: Stack<'a>, + clock: C, + storage: &'a mut PtpStorage, + config: Config, + filter_config: F::Config, + #[cfg(feature = "monitor")] + monitor: Option<&'a PtpMonitor>, +} + +struct ClockRef<'a, C> { + clock: &'a mut C, + #[cfg(feature = "monitor")] + monitor: Option<&'a PtpMonitor>, +} + +impl Clock for ClockRef<'_, C> { + type Error = C::Error; + + fn now(&self) -> Time { + self.clock.now() + } + + fn step_clock(&mut self, offset: Duration) -> Result { + let result = self.clock.step_clock(offset); + #[cfg(feature = "monitor")] + if result.is_ok() + && let Some(monitor) = self.monitor + { + monitor.unavailable(); + } + result + } + + fn set_frequency(&mut self, ppm: f64) -> Result { + self.clock.set_frequency(ppm) + } + + fn set_properties(&mut self, time_properties_ds: &TimePropertiesDS) -> Result<(), Self::Error> { + let result = self.clock.set_properties(time_properties_ds); + #[cfg(feature = "monitor")] + if result.is_ok() + && let Some(monitor) = self.monitor + { + monitor.set_ptp_timescale(time_properties_ds.ptp_timescale); + } + result + } +} + +impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { + /// Bind the PTP service to an Embassy network stack, PTP clock, socket + /// storage, protocol configuration, and filter configuration. + pub fn new( + stack: Stack<'a>, + clock: C, + storage: &'a mut PtpStorage, + config: Config, + filter_config: F::Config, + ) -> Self { + Self { + stack, + clock, + storage, + config, + filter_config, + #[cfg(feature = "monitor")] + monitor: None, + } + } + + /// Report clock tracking state through `monitor`. + #[cfg(feature = "monitor")] + pub fn with_monitor(mut self, monitor: &'a PtpMonitor) -> Self { + self.monitor = Some(monitor); + self + } + + /// Run the PTP service forever. + pub async fn run(&mut self) -> ! { + let stack = self.stack; + let clock = ClockRef { + clock: &mut self.clock, + #[cfg(feature = "monitor")] + monitor: self.monitor, + }; + let storage = &mut *self.storage; + let config = self.config; + let filter_config = self.filter_config.clone(); + + info!("ptp: waiting for network configuration"); + stack.wait_config_up().await; + match stack.join_multicast_group(PRIMARY_MULTICAST) { + Ok(()) => info!("ptp: joined primary multicast group"), + Err(error) => { + warn!("ptp: failed to join primary multicast group: {}", error) + } + } + match stack.join_multicast_group(LINK_LOCAL_MULTICAST) { + Ok(()) => info!("ptp: joined link-local multicast group"), + Err(error) => { + warn!("ptp: failed to join link-local multicast group: {}", error) + } + } + + let event_socket = UdpSocket::new( + stack, + &mut storage.event.rx_meta, + &mut storage.event.rx_buffer, + &mut storage.event.tx_meta, + &mut storage.event.tx_buffer, + ); + let general_socket = UdpSocket::new( + stack, + &mut storage.general.rx_meta, + &mut storage.general.rx_buffer, + &mut storage.general.tx_meta, + &mut storage.general.tx_buffer, + ); + let mut io = PortIo::new(event_socket, general_socket, config.tx_timestamp_timeout); + + let clock_identity = clock_identity_from_mac(config.mac_address); + info!( + "ptp: clock identity {=u64:#020x}", + u64::from_be_bytes(clock_identity.0), + ); + + let instance = PtpInstance::::new( + InstanceConfig { + clock_identity, + priority_1: config.priority_1, + priority_2: config.priority_2, + domain_number: config.domain_number, + sdo_id: Default::default(), + slave_only: config.slave_only, + path_trace: false, + clock_quality: config.clock_quality, + }, + config.time_properties, + ); + let port = instance.add_port( + PortConfig { + acceptable_master_list: AcceptAnyMaster, + delay_mechanism: DelayMechanism::E2E { + interval: config.delay_request_interval, + }, + announce_interval: config.announce_interval, + announce_receipt_timeout: config.announce_receipt_timeout, + sync_interval: config.sync_interval, + master_only: false, + delay_asymmetry: config.delay_asymmetry, + minor_ptp_version: config.minor_ptp_version, + }, + filter_config, + clock, + XorShiftRng::seed_from_u64(config.rng_seed), + ); + let (mut port, actions) = port.end_bmca(); + + let mut forwarded_tlvs = NoForwardedTLVs; + let mut bmca = deadline_from_now(instance.bmca_interval()); + let mut tx_timestamp: Option = None; + + io.handle( + actions, + #[cfg(feature = "monitor")] + self.monitor, + ) + .await; + info!("ptp: task started"); + + loop { + if Instant::now() >= bmca { + bmca = deadline_from_now(instance.bmca_interval()); + let old_state = port.port_ds().port_state; + let mut bmca_port = port.start_bmca(); + instance.bmca(&mut [&mut bmca_port]); + let (running_port, actions) = bmca_port.end_bmca(); + port = running_port; + let new_state = port.port_ds().port_state; + #[cfg(feature = "monitor")] + if new_state != PortState::Slave + && let Some(monitor) = self.monitor + { + monitor.holdover(); + } + if new_state != old_state { + info!( + "ptp: state {} -> {}", + state_name(old_state), + state_name(new_state) + ); + } + io.handle( + actions, + #[cfg(feature = "monitor")] + self.monitor, + ) + .await; + continue; + } + + let actions = if let Some(timestamp) = tx_timestamp.take() { + match io.pending_tx.take(timestamp.id) { + Some(context) => { + port.handle_send_timestamp(context, time_from(timestamp.timestamp)) + } + None => PortActionIterator::empty(), + } + } else if let Some(timer) = io.timers.take_due() { + match timer { + StatimeTimer::Announce => port.handle_announce_timer(&mut forwarded_tlvs), + StatimeTimer::Sync => port.handle_sync_timer(), + StatimeTimer::DelayRequest => port.handle_delay_request_timer(), + StatimeTimer::AnnounceReceipt => port.handle_announce_receipt_timer(), + StatimeTimer::FilterUpdate => { + #[cfg(feature = "monitor")] + if let Some(monitor) = self.monitor { + monitor.holdover(); + } + port.handle_filter_update_timer() + } + } + } else { + let receive_delay_requests = port.port_ds().port_state == PortState::Master; + match io.receive(&mut storage.packet, receive_delay_requests) { + Incoming::Event(packet, timestamp) => { + port.handle_event_receive(packet, time_from(timestamp)) + } + Incoming::General(packet) => port.handle_general_receive(packet), + Incoming::None => PortActionIterator::empty(), + } + }; + io.pending_tx.expire(); + + io.handle( + actions, + #[cfg(feature = "monitor")] + self.monitor, + ) + .await; + + let next = io.next_deadline(bmca); + tx_timestamp = io.wait(stack, next).await; + } + } +} + +struct PortIo<'a> { + event: UdpSocket<'a>, + general: UdpSocket<'a>, + timers: Timers, + packet_id: PacketIdGenerator, + pending_tx: PendingTxQueue, +} + +impl<'a> PortIo<'a> { + fn new( + mut event: UdpSocket<'a>, + mut general: UdpSocket<'a>, + tx_timestamp_timeout: EmbassyDuration, + ) -> Self { + event.bind(EVENT_PORT).unwrap(); + general.bind(GENERAL_PORT).unwrap(); + Self { + event, + general, + timers: Timers::default(), + packet_id: PacketIdGenerator::new(), + pending_tx: PendingTxQueue::new(tx_timestamp_timeout), + } + } + + async fn handle( + &mut self, + actions: PortActionIterator<'_>, + #[cfg(feature = "monitor")] monitor: Option<&PtpMonitor>, + ) { + for action in actions { + match action { + PortAction::SendEvent { + context, + data, + link_local, + } => { + let metadata = UdpMetadata { + endpoint: multicast_endpoint(EVENT_PORT, link_local), + meta: self.packet_id.next(), + local_address: None, + }; + match self.event.send_to(data, metadata).await { + Ok(()) => self.pending_tx.push(context, metadata.meta.id), + Err(error) => warn!("ptp: event send failed: {}", &error), + } + } + PortAction::SendGeneral { data, link_local } => { + let metadata = UdpMetadata { + endpoint: multicast_endpoint(GENERAL_PORT, link_local), + meta: udp::PacketMeta::default(), + local_address: None, + }; + if let Err(error) = self.general.send_to(data, metadata).await { + warn!("ptp: general send failed: {}", error); + } + } + PortAction::ResetAnnounceTimer { duration } => { + self.timers.reset(StatimeTimer::Announce, duration) + } + PortAction::ResetSyncTimer { duration } => { + self.timers.reset(StatimeTimer::Sync, duration) + } + PortAction::ResetDelayRequestTimer { duration } => { + self.timers.reset(StatimeTimer::DelayRequest, duration) + } + PortAction::ResetAnnounceReceiptTimer { duration } => { + self.timers.reset(StatimeTimer::AnnounceReceipt, duration) + } + PortAction::ResetFilterUpdateTimer { duration } => { + #[cfg(feature = "monitor")] + if let Some(monitor) = monitor { + monitor.tracking(); + } + self.timers.reset(StatimeTimer::FilterUpdate, duration) + } + PortAction::ForwardTLV { .. } => {} + } + } + } + + fn receive<'b>(&self, packet: &'b mut [u8], receive_delay_requests: bool) -> Incoming<'b> { + match self.event.try_recv_from(packet) { + Ok((n, meta)) => { + let packet = &packet[..n]; + return rx_event_timestamp(packet, meta.meta, receive_delay_requests) + .map_or(Incoming::None, |timestamp| { + Incoming::Event(packet, timestamp) + }); + } + Err(TryError::Other(udp::RecvError::Truncated)) => { + warn!("ptp: truncated event packet"); + return Incoming::None; + } + Err(TryError::WouldBlock) => {} + } + + match self.general.try_recv_from(packet) { + Ok((n, _)) if ptp_message_type(&packet[..n]).is_some() => { + Incoming::General(&packet[..n]) + } + Ok(_) | Err(TryError::WouldBlock) => Incoming::None, + Err(TryError::Other(udp::RecvError::Truncated)) => { + warn!("ptp: truncated general packet"); + Incoming::None + } + } + } + + fn next_deadline(&self, deadline: Instant) -> Instant { + [ + self.timers.next_deadline(), + self.pending_tx.next_timeout_deadline(), + ] + .into_iter() + .flatten() + .fold(deadline, Ord::min) + } + + async fn wait(&self, stack: Stack<'_>, deadline: Instant) -> Option { + match with_deadline( + deadline, + select3( + stack.poll_tx_timestamps(), + self.event.wait_recv_ready(), + self.general.wait_recv_ready(), + ), + ) + .await + { + Ok(Either3::First(timestamp)) => Some(timestamp), + _ => None, + } + } +} + +enum Incoming<'a> { + Event(&'a [u8], Timestamp), + General(&'a [u8]), + None, +} + +fn rx_event_timestamp( + packet: &[u8], + meta: udp::PacketMeta, + receive_delay_requests: bool, +) -> Option { + let message_type = ptp_message_type(packet)?; + if matches!(message_type, MSG_PDELAY_REQ | MSG_PDELAY_RESP) + || message_type == MSG_DELAY_REQ && !receive_delay_requests + { + return None; + } + let timestamp = meta.timestamp; + if timestamp.is_none() { + warn!( + "ptp: missing rx timestamp packet_id={=u32} message_type={=u8}", + meta.id, message_type + ); + } + timestamp +} + +fn ptp_message_type(packet: &[u8]) -> Option { + packet.get(..34)?; + Some(packet[0] & 0x0f) +} + +#[derive(Default)] +struct Timers([Option; StatimeTimer::ALL.len()]); + +impl Timers { + fn reset(&mut self, timer: StatimeTimer, duration: core::time::Duration) { + self.0[timer as usize] = Some(deadline_from_now(duration)); + } + + fn take_due(&mut self) -> Option { + let now = Instant::now(); + StatimeTimer::ALL.into_iter().find(|&timer| { + self.0[timer as usize] + .take_if(|deadline| now >= *deadline) + .is_some() + }) + } + + fn next_deadline(&self) -> Option { + self.0.into_iter().flatten().min() + } +} + +#[repr(usize)] +#[derive(Clone, Copy)] +enum StatimeTimer { + Announce, + Sync, + DelayRequest, + AnnounceReceipt, + FilterUpdate, +} + +impl StatimeTimer { + const ALL: [Self; 5] = [ + Self::Announce, + Self::Sync, + Self::DelayRequest, + Self::AnnounceReceipt, + Self::FilterUpdate, + ]; +} + +struct PendingTx { + context: TimestampContext, + packet_id: u32, + started: Instant, +} + +struct PendingTxQueue { + slots: [Option; TX_PENDING], + timeout: EmbassyDuration, +} + +impl PendingTxQueue { + fn new(timeout: EmbassyDuration) -> Self { + Self { + slots: Default::default(), + timeout, + } + } + + fn push(&mut self, context: TimestampContext, packet_id: u32) { + if let Some(slot) = self.slots.iter_mut().find(|slot| slot.is_none()) { + *slot = Some(PendingTx { + context, + packet_id, + started: Instant::now(), + }); + } else { + warn!("ptp: tx timestamp queue full packet_id={=u32}", packet_id); + } + } + + fn take(&mut self, packet_id: u32) -> Option { + self.slots + .iter_mut() + .find_map(|slot| slot.take_if(|pending| pending.packet_id == packet_id)) + .map(|pending| pending.context) + } + + fn expire(&mut self) { + for slot in self.slots.iter_mut() { + if let Some(pending) = slot.take_if(|pending| pending.started.elapsed() >= self.timeout) + { + warn!( + "ptp: missing tx timestamp packet_id={=u32}", + pending.packet_id + ); + } + } + } + + fn next_timeout_deadline(&self) -> Option { + self.slots + .iter() + .filter_map(|slot| slot.as_ref().map(|pending| pending.started + self.timeout)) + .min() + } +} + +struct PacketIdGenerator(NonZero); + +impl PacketIdGenerator { + const fn new() -> Self { + Self(NonZero::::MIN) + } + + fn next(&mut self) -> udp::PacketMeta { + let id = self.0; + self.0 = self.0.checked_add(1).unwrap_or(NonZero::::MIN); + let mut meta = udp::PacketMeta::default(); + meta.id = id.get(); + meta.request_timestamp = true; + meta + } +} + +fn clock_identity_from_mac(mac: [u8; 6]) -> ClockIdentity { + // Use the IEEE EUI-64 expansion, not statime's zero-padded helper. + ClockIdentity([mac[0], mac[1], mac[2], 0xff, 0xfe, mac[3], mac[4], mac[5]]) +} + +pub(crate) fn time_from(timestamp: Timestamp) -> Time { + let nanos = + u64::from(timestamp.seconds) * 1_000_000_000 + u64::from(timestamp.quarter_nanos >> 2); + Time::from_nanos_subnanos(nanos, (timestamp.quarter_nanos & 3) << 30) +} + +fn multicast_endpoint(port: u16, link_local: bool) -> IpEndpoint { + let address = if link_local { + LINK_LOCAL_MULTICAST + } else { + PRIMARY_MULTICAST + }; + IpEndpoint::new(IpAddress::Ipv4(address), port) +} + +fn deadline_from_now(duration: core::time::Duration) -> Instant { + let nanos = u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX); + Instant::now() + EmbassyDuration::from_nanos(nanos) +} + +fn state_name(state: PortState) -> &'static str { + match state { + PortState::Initializing => "initializing", + PortState::Faulty => "faulty", + PortState::Disabled => "disabled", + PortState::Listening => "listening", + PortState::PreMaster => "pre_master", + PortState::Master => "master", + PortState::Passive => "passive", + PortState::Uncalibrated => "uncalibrated", + PortState::Slave => "slave", + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn init_logging() { + static INIT: std::sync::Once = std::sync::Once::new(); + INIT.call_once(defmt2log::init_from_current_exe); + } + + #[test] + fn preserves_quarter_nanoseconds() { + init_logging(); + for (quarter_nanos, subnanos) in [(40, 0), (41, 1 << 30), (42, 1 << 31), (43, 3 << 30)] { + assert_eq!( + time_from(Timestamp { + seconds: 2, + quarter_nanos, + }), + Time::from_nanos_subnanos(2_000_000_010, subnanos), + ); + } + } + + #[test] + fn accepts_delay_requests_only_for_a_master() { + init_logging(); + let mut packet = [0; 34]; + packet[0] = MSG_DELAY_REQ; + let timestamp = Timestamp::from_seconds_and_nanos(2, 10); + let mut meta = udp::PacketMeta::default(); + meta.timestamp = Some(timestamp); + + assert_eq!(rx_event_timestamp(&packet, meta, false), None); + assert_eq!(rx_event_timestamp(&packet, meta, true), Some(timestamp)); + } + + #[test] + fn ignores_peer_delay_messages() { + init_logging(); + let timestamp = Timestamp::from_seconds_and_nanos(2, 10); + let mut meta = udp::PacketMeta::default(); + meta.timestamp = Some(timestamp); + + for message_type in [MSG_PDELAY_REQ, MSG_PDELAY_RESP] { + let mut packet = [0; 34]; + packet[0] = message_type; + assert_eq!(rx_event_timestamp(&packet, meta, true), None); + } + } +} diff --git a/statime-embassy-net/src/monitor.rs b/statime-embassy-net/src/monitor.rs new file mode 100644 index 000000000..17b9c1259 --- /dev/null +++ b/statime-embassy-net/src/monitor.rs @@ -0,0 +1,111 @@ +use core::sync::atomic::{AtomicU8, Ordering}; + +/// Quality of the PHC's relation to its selected PTP master. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ClockState { + /// No successful servo update has established the clock relation yet. + Unavailable, + /// The servo is receiving measurements and disciplining the PHC. + Tracking, + /// Measurements stopped; the PHC retains its last applied rate. + Holdover, +} + +/// Lock-free observation of state that cannot be reconstructed from PTP time. +pub struct PtpMonitor { + state: AtomicU8, +} + +impl PtpMonitor { + const STATE_MASK: u8 = 0x3; + const PTP_TIMESCALE: u8 = 0x4; + + /// Create an unavailable monitor with no engine history. + pub const fn new() -> Self { + Self { + state: AtomicU8::new(ClockState::Unavailable as u8), + } + } + + /// Read the current TAI clock-relation state. + pub fn state(&self) -> ClockState { + let state = self.state.load(Ordering::Relaxed); + if state & Self::PTP_TIMESCALE == 0 { + ClockState::Unavailable + } else { + match state & Self::STATE_MASK { + 1 => ClockState::Tracking, + 2 => ClockState::Holdover, + _ => ClockState::Unavailable, + } + } + } + + pub(crate) fn tracking(&self) { + let state = self.state.load(Ordering::Relaxed); + if state & Self::PTP_TIMESCALE != 0 { + self.state.store( + Self::PTP_TIMESCALE | ClockState::Tracking as u8, + Ordering::Relaxed, + ); + } + } + + pub(crate) fn holdover(&self) { + let state = self.state.load(Ordering::Relaxed); + if state & Self::STATE_MASK == ClockState::Tracking as u8 { + self.state.store( + state & !Self::STATE_MASK | ClockState::Holdover as u8, + Ordering::Relaxed, + ); + } + } + + pub(crate) fn unavailable(&self) { + let state = self.state.load(Ordering::Relaxed); + self.state + .store(state & !Self::STATE_MASK, Ordering::Relaxed); + } + + pub(crate) fn set_ptp_timescale(&self, enabled: bool) { + let state = self.state.load(Ordering::Relaxed); + self.state.store( + if enabled { + state | Self::PTP_TIMESCALE + } else { + ClockState::Unavailable as u8 + }, + Ordering::Relaxed, + ); + } +} + +impl Default for PtpMonitor { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn state_requires_ptp_timescale_and_continuous_update() { + let monitor = PtpMonitor::new(); + monitor.set_ptp_timescale(true); + assert_eq!(monitor.state(), ClockState::Unavailable); + monitor.tracking(); + assert_eq!(monitor.state(), ClockState::Tracking); + monitor.holdover(); + assert_eq!(monitor.state(), ClockState::Holdover); + monitor.unavailable(); + assert_eq!(monitor.state(), ClockState::Unavailable); + monitor.tracking(); + assert_eq!(monitor.state(), ClockState::Tracking); + monitor.set_ptp_timescale(false); + assert_eq!(monitor.state(), ClockState::Unavailable); + } +} diff --git a/statime-embassy-net/src/storage.rs b/statime-embassy-net/src/storage.rs new file mode 100644 index 000000000..7f62e2303 --- /dev/null +++ b/statime-embassy-net/src/storage.rs @@ -0,0 +1,69 @@ +use embassy_net::udp::PacketMetadata; + +const PACKET_BYTES: usize = 256; +const EVENT_RX_PACKETS: usize = 4; +const EVENT_TX_PACKETS: usize = 2; +const GENERAL_RX_PACKETS: usize = 4; +const GENERAL_TX_PACKETS: usize = 1; + +type EventStorage = SocketStorage< + EVENT_RX_PACKETS, + EVENT_TX_PACKETS, + { PACKET_BYTES * EVENT_RX_PACKETS }, + { PACKET_BYTES * EVENT_TX_PACKETS }, +>; +type GeneralStorage = SocketStorage< + GENERAL_RX_PACKETS, + GENERAL_TX_PACKETS, + { PACKET_BYTES * GENERAL_RX_PACKETS }, + { PACKET_BYTES * GENERAL_TX_PACKETS }, +>; + +/// Static packet and socket storage for one [`crate::Runner`]. +pub struct PtpStorage { + pub(super) event: EventStorage, + pub(super) general: GeneralStorage, + pub(super) packet: [u8; PACKET_BYTES], +} + +impl PtpStorage { + /// Create empty PTP socket storage. + pub const fn new() -> Self { + Self { + event: SocketStorage::new(), + general: SocketStorage::new(), + packet: [0; PACKET_BYTES], + } + } +} + +impl Default for PtpStorage { + fn default() -> Self { + Self::new() + } +} + +pub(super) struct SocketStorage< + const RX_PACKETS: usize, + const TX_PACKETS: usize, + const RX_BYTES: usize, + const TX_BYTES: usize, +> { + pub(super) rx_meta: [PacketMetadata; RX_PACKETS], + pub(super) tx_meta: [PacketMetadata; TX_PACKETS], + pub(super) rx_buffer: [u8; RX_BYTES], + pub(super) tx_buffer: [u8; TX_BYTES], +} + +impl + SocketStorage +{ + const fn new() -> Self { + Self { + rx_meta: [PacketMetadata::EMPTY; RX_PACKETS], + tx_meta: [PacketMetadata::EMPTY; TX_PACKETS], + rx_buffer: [0; RX_BYTES], + tx_buffer: [0; TX_BYTES], + } + } +} From c232b87eb04b1c67b17769896fd689176cec3dab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 20 Aug 2026 16:34:20 +0200 Subject: [PATCH 4/7] embassy: add STM32H743 PTP example --- statime-embassy-net/.gitignore | 1 + .../examples/stm32h743/.cargo/config.toml | 14 + .../examples/stm32h743/Cargo.lock | 1373 +++++++++++++++++ .../examples/stm32h743/Cargo.toml | 72 + .../examples/stm32h743/README.md | 16 + .../examples/stm32h743/build.rs | 8 + .../examples/stm32h743/memory.x | 15 + .../examples/stm32h743/src/main.rs | 162 ++ 8 files changed, 1661 insertions(+) create mode 100644 statime-embassy-net/examples/stm32h743/.cargo/config.toml create mode 100644 statime-embassy-net/examples/stm32h743/Cargo.lock create mode 100644 statime-embassy-net/examples/stm32h743/Cargo.toml create mode 100644 statime-embassy-net/examples/stm32h743/README.md create mode 100644 statime-embassy-net/examples/stm32h743/build.rs create mode 100644 statime-embassy-net/examples/stm32h743/memory.x create mode 100644 statime-embassy-net/examples/stm32h743/src/main.rs diff --git a/statime-embassy-net/.gitignore b/statime-embassy-net/.gitignore index ea8c4bf7f..146697f3d 100644 --- a/statime-embassy-net/.gitignore +++ b/statime-embassy-net/.gitignore @@ -1 +1,2 @@ /target +/examples/stm32h743/target diff --git a/statime-embassy-net/examples/stm32h743/.cargo/config.toml b/statime-embassy-net/examples/stm32h743/.cargo/config.toml new file mode 100644 index 000000000..5686ab26e --- /dev/null +++ b/statime-embassy-net/examples/stm32h743/.cargo/config.toml @@ -0,0 +1,14 @@ +[build] +target = "thumbv7em-none-eabihf" + +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = "probe-rs run --chip STM32H743ZITx" +rustflags = [ + "-C", "link-arg=--nmagic", + "-C", "target-cpu=cortex-m7", + "-C", "link-arg=-Tlink.x", + "-C", "link-arg=-Tdefmt.x", +] + +[env] +DEFMT_LOG = "info" diff --git a/statime-embassy-net/examples/stm32h743/Cargo.lock b/statime-embassy-net/examples/stm32h743/Cargo.lock new file mode 100644 index 000000000..fec34df76 --- /dev/null +++ b/statime-embassy-net/examples/stm32h743/Cargo.lock @@ -0,0 +1,1373 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + +[[package]] +name = "bare-metal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5deb64efa5bd81e31fcd1938615a6d98c82eafcbcd787162b6f63b91d6bac5b3" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitfield" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46afbd2983a5d5a7bd740ccb198caf5b82f45c40c09c0eed36052d91cb92e719" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-device-driver" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c051592f59fe68053524b4c4935249b806f72c1f544cfb7abe4f57c3be258e" +dependencies = [ + "aligned", +] + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cordyceps" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9ab7e0ca1d179628fa0172b2b97203c7fa0cd81be2448bd446fb9559ca9261" +dependencies = [ + "loom", + "tracing", +] + +[[package]] +name = "cortex-m" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "844b9697e922c99847eed515c6eb6d101e7ce62ff556fcaec243798291427ee8" +dependencies = [ + "bare-metal", + "bitfield", + "cortex-m-macros", + "critical-section", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "volatile-register", +] + +[[package]] +name = "cortex-m-macros" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1922be58519ad40368fc4ca595a2cefa51a7abf947be3b0c90586dc7dbd0e2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "cortex-m-rt" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c439b23bf18154d8a634543caf1ee73cceb723f2902f7ea243634159a00fd47" +dependencies = [ + "cortex-m-rt-macros", +] + +[[package]] +name = "cortex-m-rt-macros" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a75f3e7f3f01d649668d68e02d0ef92c7041a9c97b8fe6bc354ddbf40822d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "defmt-parser" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" +dependencies = [ + "thiserror", +] + +[[package]] +name = "defmt-rtt" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05144944c117db54127a8ac17a3c9a28d55e7047bfddb950bcd20b4a94c79b10" +dependencies = [ + "critical-section", + "defmt", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dsp-fixedpoint" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563da37e89f398877d6f709440327a6ff4743cf2f7911e1c96ee14870e11bc74" +dependencies = [ + "num-traits", +] + +[[package]] +name = "embassy-embedded-hal" +version = "0.6.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "defmt", + "embassy-futures", + "embassy-hal-internal", + "embassy-sync", + "embassy-time", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-storage", + "embedded-storage-async", + "nb 1.1.0", +] + +[[package]] +name = "embassy-executor" +version = "0.10.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "cordyceps", + "cortex-m", + "critical-section", + "defmt", + "document-features", + "embassy-executor-macros", + "embassy-executor-timer-queue", + "unitrait", +] + +[[package]] +name = "embassy-executor-macros" +version = "0.8.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "embassy-executor-timer-queue" +version = "0.1.1" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "unitrait", +] + +[[package]] +name = "embassy-futures" +version = "0.1.2" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" + +[[package]] +name = "embassy-hal-internal" +version = "0.5.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "cortex-m", + "critical-section", + "defmt", + "num-traits", +] + +[[package]] +name = "embassy-net" +version = "0.9.1" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "defmt", + "document-features", + "embassy-futures", + "embassy-net-driver", + "embassy-sync", + "embassy-time", + "embedded-io-async 0.7.0", + "embedded-nal-async", + "heapless", + "managed", + "xarxa", +] + +[[package]] +name = "embassy-net-driver" +version = "0.2.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "defmt", +] + +[[package]] +name = "embassy-stm32" +version = "0.6.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "aligned", + "bit_field", + "bitflags 2.13.1", + "block-device-driver", + "cfg-if", + "cortex-m", + "cortex-m-rt", + "critical-section", + "defmt", + "document-features", + "dsp-fixedpoint", + "embassy-embedded-hal", + "embassy-futures", + "embassy-hal-internal", + "embassy-net-driver", + "embassy-sync", + "embassy-time", + "embassy-time-driver", + "embassy-time-queue-utils", + "embassy-usb-driver", + "embassy-usb-synopsys-otg", + "embedded-can", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "embedded-hal-nb", + "embedded-io 0.7.1", + "embedded-io-async 0.7.0", + "embedded-storage", + "embedded-storage-async", + "futures-util", + "heapless", + "nb 1.1.0", + "once_cell", + "proc-macro2", + "quote", + "rand_core 0.10.1", + "rand_core 0.6.4", + "rand_core 0.9.5", + "regex", + "sdio", + "sdio-host", + "static_assertions", + "stm32-fmc", + "stm32-hrtim", + "stm32-metapac", + "trait-set", + "vcell", +] + +[[package]] +name = "embassy-sync" +version = "0.8.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "cfg-if", + "critical-section", + "defmt", + "embedded-io-async 0.7.0", + "futures-core", + "futures-sink", + "heapless", +] + +[[package]] +name = "embassy-time" +version = "0.5.1" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "cfg-if", + "critical-section", + "defmt", + "document-features", + "embassy-time-driver", + "embassy-time-queue-utils", + "embedded-hal 0.2.7", + "embedded-hal 1.0.0", + "embedded-hal-async", + "futures-core", +] + +[[package]] +name = "embassy-time-driver" +version = "0.2.2" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "document-features", +] + +[[package]] +name = "embassy-time-queue-utils" +version = "0.3.2" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "embassy-executor-timer-queue", + "heapless", +] + +[[package]] +name = "embassy-usb-driver" +version = "0.2.2" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "defmt", + "embedded-io-async 0.6.1", + "embedded-io-async 0.7.0", +] + +[[package]] +name = "embassy-usb-synopsys-otg" +version = "0.4.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +dependencies = [ + "defmt", + "document-features", + "embassy-sync", + "embassy-usb-driver", + "portable-atomic", +] + +[[package]] +name = "embedded-can" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d2e857f87ac832df68fa498d18ddc679175cf3d2e4aa893988e5601baf9438" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "embedded-hal" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35949884794ad573cf46071e41c9b60efb0cb311e3ca01f7af807af1debc66ff" +dependencies = [ + "nb 0.1.3", + "void", +] + +[[package]] +name = "embedded-hal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" + +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "embedded-hal-nb" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba4268c14288c828995299e59b12babdbe170f6c6d73731af1b4648142e8605" +dependencies = [ + "embedded-hal 1.0.0", + "nb 1.1.0", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb1aa714776b75c7e67e1da744b81a129b3ff919c8712b5e1b32252c1f07cc7" +dependencies = [ + "defmt", +] + +[[package]] +name = "embedded-io-async" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff09972d4073aa8c299395be75161d582e7629cd663171d62af73c8d50dba3f" +dependencies = [ + "embedded-io 0.6.1", +] + +[[package]] +name = "embedded-io-async" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564b9f813c544241430e147d8bc454815ef9ac998878d30cc3055449f7fd4c0" +dependencies = [ + "defmt", + "embedded-io 0.7.1", +] + +[[package]] +name = "embedded-nal" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56a28be191a992f28f178ec338a0bf02f63d7803244add736d026a471e6ed77" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "embedded-nal-async" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5a1bd585135d302f8f6d7de329310938093da6271b37a6c94b8798795c0c6d" +dependencies = [ + "embedded-io-async 0.7.0", + "embedded-nal", +] + +[[package]] +name = "embedded-storage" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21dea9854beb860f3062d10228ce9b976da520a73474aed3171ec276bc0c032" + +[[package]] +name = "embedded-storage-async" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1763775e2323b7d5f0aa6090657f5e21cfa02ede71f5dc40eead06d64dcd15cc" +dependencies = [ + "embedded-storage", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "fixed" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af2cbf772fa6d1c11358f92ef554cb6b386201210bcf0e91fb7fba8a907fb40" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "fugit" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e639847d312d9a82d2e75b0edcc1e934efcc64e6cb7aa94f0b1fbec0bc231d6" +dependencies = [ + "gcd", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generator" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link", + "windows-result", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "defmt", + "hash32", + "stable_deref_trait", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "managed" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nb" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801d31da0513b6ec5214e9bf433a77966320625a37860f910be265be6e18d06f" +dependencies = [ + "nb 1.1.0", +] + +[[package]] +name = "nb" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5439c4ad607c3c23abf66de8c8bf57ba8adcd1f129e699851a6e43935d339d" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "panic-probe" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd402d00b0fb94c5aee000029204a46884b1262e0c443f166d86d2c0747e1a1a" +dependencies = [ + "cortex-m", + "defmt", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sdio" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f7328e9dbad1337ef2d94a8404428a42d77a9916cda7c2e51287a1998da9afb" +dependencies = [ + "aligned", + "block-device-driver", + "defmt", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + +[[package]] +name = "sdio-host" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b328e2cb950eeccd55b7f55c3a963691455dcd044cfb5354f0c5e68d2c2d6ee2" + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "static_cell" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0530892bb4fa575ee0da4b86f86c667132a94b74bb72160f58ee5a4afec74c23" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "statime" +version = "0.4.0" +dependencies = [ + "arrayvec", + "az", + "fixed", + "libm", + "log", + "rand", +] + +[[package]] +name = "statime-embassy-net" +version = "0.1.0" +dependencies = [ + "defmt", + "embassy-futures", + "embassy-net", + "embassy-time", + "rand_core 0.6.4", + "rand_xorshift", + "statime", +] + +[[package]] +name = "statime-embassy-net-stm32h743" +version = "0.0.0" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", + "defmt-rtt", + "embassy-executor", + "embassy-net", + "embassy-stm32", + "embassy-time", + "panic-probe", + "static_cell", + "statime", + "statime-embassy-net", +] + +[[package]] +name = "stm32-fmc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72692594faa67f052e5e06dd34460951c21e83bc55de4feb8d2666e2f15480a2" +dependencies = [ + "embedded-hal 1.0.0", +] + +[[package]] +name = "stm32-hrtim" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee749ac0511b9977bcd5d508641bae0045964e861a4dc3fb40131f2e8aeb0360" +dependencies = [ + "fugit", + "stm32h7", +] + +[[package]] +name = "stm32-metapac" +version = "21.0.0" +source = "git+https://github.com/embassy-rs/stm32-data-generated?tag=stm32-data-7a8bb7113f8548e09dca4ed214af3acb7925aa31#dd153dd8afdd2029cac5733d3cb5c819df2a8836" +dependencies = [ + "cortex-m", + "cortex-m-rt", + "defmt", +] + +[[package]] +name = "stm32h7" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e8634413ba08452b793b2f0f8574edd801a65b24c7f3cdce8034eef1359db4" +dependencies = [ + "cortex-m", + "vcell", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trait-set" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79e2e9c9ab44c6d7c20d5976961b47e8f49ac199154daa514b77cd1ab536625" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unitrait" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66b59b4e118746027256db08772826e9e0765e93b0bf15d2714a0ac0f940fbe" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcell" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77439c1b53d2303b20d9459b1ade71a83c716e3f9c34f3228c00e6f185d6c002" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "volatile-register" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de437e2a6208b014ab52972a27e59b33fa2920d3e00fe05026167a1c509d19cc" +dependencies = [ + "vcell", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "xarxa" +version = "0.13.1" +source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +dependencies = [ + "bitflags 1.3.2", + "byteorder", + "cfg-if", + "defmt", + "heapless", + "managed", + "xarxa-driver", +] + +[[package]] +name = "xarxa-driver" +version = "0.1.0" +source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +dependencies = [ + "defmt", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/statime-embassy-net/examples/stm32h743/Cargo.toml b/statime-embassy-net/examples/stm32h743/Cargo.toml new file mode 100644 index 000000000..1fa99f704 --- /dev/null +++ b/statime-embassy-net/examples/stm32h743/Cargo.toml @@ -0,0 +1,72 @@ +[package] +name = "statime-embassy-net-stm32h743" +version = "0.0.0" +edition = "2024" +publish = false + +[features] +default = [] +stabilizer = [] + +[dependencies] +statime = { path = "../../../statime", default-features = false } +statime-embassy-net = { path = "../..", features = ["defmt"] } +cortex-m = { version = "0.7.7", features = [ + "inline-asm", + "critical-section-single-core", +] } +cortex-m-rt = "0.7" +defmt = "1.0.1" +defmt-rtt = "1.1.0" +embassy-executor = { version = "0.10.0", features = [ + "platform-cortex-m", + "executor-thread", + "defmt", +] } +embassy-net = { version = "0.9.1", default-features = false, features = [ + "defmt", + "dhcpv4", + "medium-ethernet", + "multicast", + "packetmeta-id", + "packetmeta-timestamp", + "proto-ipv4", + "udp", +] } +embassy-stm32 = { version = "0.6.0", default-features = false, features = [ + "defmt", + "ptp", + "rt", + "stm32h743zi", + "time-driver-tim12", + "unstable-pac", +] } +embassy-time = { version = "0.5.1", default-features = false, features = [ + "defmt", + "generic-queue-8", +] } +panic-probe = { version = "1.0.0", features = ["print-defmt"] } +static_cell = "2.1.1" + +[patch.crates-io] +embassy-embedded-hal = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-executor = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-futures = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-hal-internal = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-net = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-net-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-stm32 = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-sync = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-time = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-time-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-time-queue-utils = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-usb-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-usb-synopsys-otg = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } + +[profile.release] +codegen-units = 1 +opt-level = "s" +debug = true +lto = "fat" + +[workspace] diff --git a/statime-embassy-net/examples/stm32h743/README.md b/statime-embassy-net/examples/stm32h743/README.md new file mode 100644 index 000000000..689245643 --- /dev/null +++ b/statime-embassy-net/examples/stm32h743/README.md @@ -0,0 +1,16 @@ +# STM32H743 example + +Complete `statime-embassy-net` application for an STM32H743ZI with an RMII PHY +at address 0, an 8 MHz HSE oscillator, and DHCPv4. The pin mapping is in +[`src/main.rs`](src/main.rs); Ethernet DMA storage is placed in SRAM3 by +[`memory.x`](memory.x). + +From this directory: + +```console +cargo build --release +cargo run --release +``` + +The optional `stabilizer` feature resets its PHY through PE3 before Ethernet +initialization. diff --git a/statime-embassy-net/examples/stm32h743/build.rs b/statime-embassy-net/examples/stm32h743/build.rs new file mode 100644 index 000000000..486005cdc --- /dev/null +++ b/statime-embassy-net/examples/stm32h743/build.rs @@ -0,0 +1,8 @@ +use std::{env, fs, path::PathBuf}; + +fn main() { + let out = PathBuf::from(env::var_os("OUT_DIR").unwrap()); + fs::write(out.join("memory.x"), include_bytes!("memory.x")).unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/statime-embassy-net/examples/stm32h743/memory.x b/statime-embassy-net/examples/stm32h743/memory.x new file mode 100644 index 000000000..8a2e5cf27 --- /dev/null +++ b/statime-embassy-net/examples/stm32h743/memory.x @@ -0,0 +1,15 @@ +MEMORY +{ + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K + RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K + SRAM3 (rwx) : ORIGIN = 0x30040000, LENGTH = 32K +} + +_stack_start = ORIGIN(RAM) + LENGTH(RAM); + +SECTIONS { + .sram3 (NOLOAD) : ALIGN(4) { + *(.sram3 .sram3.*); + . = ALIGN(4); + } > SRAM3 +} diff --git a/statime-embassy-net/examples/stm32h743/src/main.rs b/statime-embassy-net/examples/stm32h743/src/main.rs new file mode 100644 index 000000000..9a4e0bf1f --- /dev/null +++ b/statime-embassy-net/examples/stm32h743/src/main.rs @@ -0,0 +1,162 @@ +#![no_std] +#![no_main] + +use core::mem::MaybeUninit; + +use defmt::unwrap; +use embassy_executor::Spawner; +use embassy_net::{Config as NetConfig, StackResources}; +use embassy_stm32::{ + bind_interrupts, + eth::{Ethernet, GenericPhy, InterruptHandler, PacketQueue, PtpClockConfig, Sma}, + interrupt::{InterruptExt as _, Priority}, + pac::{self, Interrupt}, + peripherals::{ETH, ETH_SMA}, +}; +use static_cell::StaticCell; +use statime::filters::{FixedWanderKalmanConfig, FixedWanderKalmanFilter}; +use statime_embassy_net::{Config as PtpConfig, EmbassyClock, PtpStorage, Runner as PtpRunner}; + +use {defmt_rtt as _, panic_probe as _}; + +defmt::timestamp!("{=u64:us}", embassy_time::Instant::now().as_micros()); + +#[defmt::panic_handler] +fn defmt_panic() -> ! { + panic_probe::hard_fault() +} + +const ETH_TX_PACKETS: usize = 4; +const ETH_RX_PACKETS: usize = 4; +const STACK_SOCKETS: usize = 4; + +type Device = Ethernet<'static, ETH, GenericPhy>>; + +bind_interrupts!(struct Irqs { + ETH => InterruptHandler; +}); + +#[unsafe(link_section = ".sram3.eth")] +static mut PACKETS: MaybeUninit> = + MaybeUninit::uninit(); +static PTP_STORAGE: StaticCell = StaticCell::new(); +static STACK_RESOURCES: StaticCell> = StaticCell::new(); + +mod board { + use embassy_stm32::{ + Config, + rcc::{ + AHBPrescaler, APBPrescaler, HSIPrescaler, Hse, HseMode, Pll, PllDiv, PllMul, PllPreDiv, + PllSource, Sysclk, VoltageScale, + }, + time::Hertz, + }; + + pub const MAC_ADDRESS: [u8; 6] = [0x02, 0x50, 0x54, 0x50, 0x00, 0x01]; + pub const SEED: u64 = 0x5054_5020_4847_4331; + + pub fn stm32_config() -> Config { + let mut config = Config::default(); + config.rcc.hse = Some(Hse { + freq: Hertz(8_000_000), + mode: HseMode::Oscillator, + }); + config.rcc.hsi = Some(HSIPrescaler::Div1); + config.rcc.csi = false; + config.rcc.pll1 = Some(Pll { + source: PllSource::Hse, + prediv: PllPreDiv::Div4, + mul: PllMul::Mul400, + fracn: None, + divp: Some(PllDiv::Div2), + divq: None, + divr: None, + }); + config.rcc.sys = Sysclk::Pll1P; + config.rcc.d1c_pre = AHBPrescaler::Div1; + config.rcc.ahb_pre = AHBPrescaler::Div2; + config.rcc.apb1_pre = APBPrescaler::Div2; + config.rcc.apb2_pre = APBPrescaler::Div2; + config.rcc.apb3_pre = APBPrescaler::Div2; + config.rcc.apb4_pre = APBPrescaler::Div2; + config.rcc.voltage_scale = VoltageScale::Scale1; + config + } +} + +#[embassy_executor::main] +async fn main(spawner: Spawner) -> ! { + let p = embassy_stm32::init(board::stm32_config()); + + // Ethernet DMA buffers are placed in SRAM3 by this example linker + // script, so enable that RAM before initializing the packet queue. + pac::RCC.ahb2enr().modify(|w| w.set_sram3en(true)); + + // ETH wakes the network runner; TIM12 drives embassy-time deadlines. + Interrupt::ETH.set_priority(Priority::P6); + Interrupt::TIM8_BRK_TIM12.set_priority(Priority::P7); + + #[cfg(feature = "stabilizer")] + { + use embassy_stm32::gpio::{Level, Output, Speed}; + const SYSCLK_HZ: u32 = 400_000_000; + + let mut phy_reset = Output::new(p.PE3, Level::Low, Speed::Low); + phy_reset.set_low(); + cortex_m::asm::delay(SYSCLK_HZ / 4); + phy_reset.set_high(); + cortex_m::asm::delay(SYSCLK_HZ / 4); + core::mem::forget(phy_reset); + } + + let queue = unsafe { + let packets = core::ptr::addr_of_mut!(PACKETS); + PacketQueue::init(&mut *packets); + (*packets).assume_init_mut() + }; + let phy = GenericPhy::new(Sma::new(p.ETH_SMA, p.PA2, p.PC1), 0); + let mut device = Ethernet::new_with_phy( + queue, + p.ETH, + Irqs, + p.PA1, + p.PA7, + p.PC4, + p.PC5, + p.PB12, + p.PG14, + p.PB11, + board::MAC_ADDRESS, + phy, + ); + let ptp_clock = EmbassyClock::new(device.start_ptp(PtpClockConfig::default())); + let (stack, runner) = embassy_net::new( + device, + NetConfig::dhcpv4(Default::default()), + STACK_RESOURCES.init(StackResources::new()), + board::SEED, + ); + let ptp_runner = PtpRunner::<_, FixedWanderKalmanFilter>::new( + stack, + ptp_clock, + PTP_STORAGE.init(PtpStorage::new()), + PtpConfig::new(board::MAC_ADDRESS, board::SEED), + FixedWanderKalmanConfig::default(), + ); + + spawner.spawn(unwrap!(net_task(runner))); + spawner.spawn(unwrap!(ptp_task(ptp_runner))); + match core::future::pending::().await {} +} + +#[embassy_executor::task] +async fn net_task(mut runner: embassy_net::Runner<'static, Device>) -> ! { + runner.run().await +} + +#[embassy_executor::task] +async fn ptp_task( + mut runner: PtpRunner<'static, EmbassyClock>>, +) -> ! { + runner.run().await +} From 367c737700d39f85fdec18d3ea3477d379cfd7d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 20 Aug 2026 09:23:35 +0200 Subject: [PATCH 5/7] ci: cover statime-embassy-net --- .github/dependabot.yml | 2 +- .github/workflows/rust.yml | 25 +++++++++++++++++++++++++ README.md | 5 +++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d98eb3e95..ef46a531d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: patterns: ["*"] - package-ecosystem: cargo - directories: ["/", "/fuzz/", "/statime-stm32/"] + directories: ["/", "/fuzz/", "/statime-embassy-net/", "/statime-embassy-net/examples/stm32h743/", "/statime-stm32/"] allow: - dependency-type: all schedule: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f3649aa6b..44d74e704 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -39,6 +39,10 @@ jobs: working-directory: statime-stm32 run: cargo build + - name: Build Embassy STM32 example + working-directory: statime-embassy-net/examples/stm32h743 + run: cargo build --release + # Build std is handled by test job test: @@ -64,6 +68,10 @@ jobs: env: RUST_BACKTRACE: 1 + - name: Test Embassy integration + working-directory: statime-embassy-net + run: cargo test --target x86_64-unknown-linux-gnu + - name: Upload coverage to Codecov uses: codecov/codecov-action@v7.0.0 with: @@ -82,6 +90,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + target: thumbv7em-none-eabihf components: rustfmt, clippy override: true @@ -99,6 +108,22 @@ jobs: toolchain: stable args: --workspace --all-features -- -D warnings + - name: Check Embassy formatting + working-directory: statime-embassy-net + run: cargo fmt --all -- --check + + - name: Check Embassy STM32 example formatting + working-directory: statime-embassy-net/examples/stm32h743 + run: cargo fmt --all -- --check + + - name: Run Embassy host clippy + working-directory: statime-embassy-net + run: cargo clippy --target x86_64-unknown-linux-gnu --all-targets --features monitor -- -D warnings + + - name: Run Embassy STM32 clippy + working-directory: statime-embassy-net/examples/stm32h743 + run: cargo clippy -- -D warnings + - name: Run clippy (fuzzers) uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 with: diff --git a/README.md b/README.md index f0c351879..1c6afae06 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ It is designed to be able to work with many different underlying platforms, incl On modern Linux kernels, the `statime-linux` crate provides a ready to use PTP daemon. See our [getting started guide](https://docs.statime.pendulum-project.org/guide/getting-started/). -If you want to use Statime on platforms other than Linux, you will need to implement a suitable binary yourself. The `statime-stm32` crate gives an example of how to do this on an embedded target. +For Embassy-based embedded systems, `statime-embassy-net` provides a reusable +single-port ordinary-clock runner and an STM32H743 example. The `statime-stm32` +crate is a separate RTIC-based STM32F7 example.

Statime - PTP in Rust @@ -53,4 +55,3 @@ For all past and present funders and supporters, see the [Statime page](https:// Logo NGI Assure Logo STF - From 679d3dda11f27ded4f519358f57b626ce77ffe87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 20 Aug 2026 15:32:56 +0200 Subject: [PATCH 6/7] embassy: report runner setup failures --- statime-embassy-net/README.md | 8 +- .../examples/stm32h743/src/main.rs | 4 +- statime-embassy-net/src/lib.rs | 99 +++++++++++++++---- statime-embassy-net/src/storage.rs | 4 + 4 files changed, 91 insertions(+), 24 deletions(-) diff --git a/statime-embassy-net/README.md b/statime-embassy-net/README.md index 6d43e735c..0bde75400 100644 --- a/statime-embassy-net/README.md +++ b/statime-embassy-net/README.md @@ -17,7 +17,13 @@ uses Embassy STM32; applications using it must select their concrete `embassy-stm32` chip feature. The runner is currently a single-port UDP/IPv4 ordinary clock using E2E delay -measurement. It is slave-only by default. +measurement. It is slave-only by default. To keep static packet storage small, +it accepts PTP datagrams up to 256 bytes; larger, TLV-heavy messages are +discarded as truncated. + +Runner startup reports multicast-membership and socket-binding errors. Once +started, transient link or IP-configuration loss keeps the clock in holdover; +the existing sockets and protocol state resume when connectivity returns. The default servo is Statime's `FixedWanderKalmanFilter`, intended for embedded systems whose oscillator wander is characterized or conservatively bounded. diff --git a/statime-embassy-net/examples/stm32h743/src/main.rs b/statime-embassy-net/examples/stm32h743/src/main.rs index 9a4e0bf1f..989026867 100644 --- a/statime-embassy-net/examples/stm32h743/src/main.rs +++ b/statime-embassy-net/examples/stm32h743/src/main.rs @@ -157,6 +157,6 @@ async fn net_task(mut runner: embassy_net::Runner<'static, Device>) -> ! { #[embassy_executor::task] async fn ptp_task( mut runner: PtpRunner<'static, EmbassyClock>>, -) -> ! { - runner.run().await +) { + unwrap!(runner.run().await); } diff --git a/statime-embassy-net/src/lib.rs b/statime-embassy-net/src/lib.rs index c201932aa..f4706ac32 100644 --- a/statime-embassy-net/src/lib.rs +++ b/statime-embassy-net/src/lib.rs @@ -10,7 +10,7 @@ mod storage; use core::num::NonZero; use embassy_futures::select::{Either3, select3}; use embassy_net::{ - IpAddress, IpEndpoint, Ipv4Address, Stack, TryError, + IpAddress, IpEndpoint, Ipv4Address, MulticastError, Stack, TryError, driver::{Timestamp, TxTimestamp}, udp, udp::{UdpMetadata, UdpSocket}, @@ -71,6 +71,49 @@ const MSG_DELAY_REQ: u8 = 0x1; const MSG_PDELAY_REQ: u8 = 0x2; const MSG_PDELAY_RESP: u8 = 0x3; +/// Error encountered while starting a [`Runner`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum RunError { + /// The network stack could not join a required PTP multicast group. + Multicast { + /// Multicast address the runner tried to join. + address: Ipv4Address, + /// Error reported by the network stack. + source: MulticastError, + }, + /// A PTP UDP socket could not be bound. + Bind { + /// UDP port the runner tried to bind. + port: u16, + /// Error reported by the socket. + source: udp::BindError, + }, +} + +impl core::fmt::Display for RunError { + fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Multicast { address, source } => { + write!(formatter, "joining PTP multicast group {address}: {source}") + } + Self::Bind { port, source } => { + write!(formatter, "binding PTP UDP port {port}: {source:?}") + } + } + } +} + +impl core::error::Error for RunError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Multicast { source, .. } => Some(source), + Self::Bind { .. } => None, + } + } +} + /// Configuration for one PTP ordinary-clock runner. #[non_exhaustive] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -227,8 +270,12 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { self } - /// Run the PTP service forever. - pub async fn run(&mut self) -> ! { + /// Run the PTP service until cancelled, or return an initialization error. + /// + /// Transient link or IP-configuration loss does not return an error. The + /// runner retains its sockets and protocol state and resumes when the + /// network stack becomes usable again. + pub async fn run(&mut self) -> Result<(), RunError> { let stack = self.stack; let clock = ClockRef { clock: &mut self.clock, @@ -241,18 +288,20 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { info!("ptp: waiting for network configuration"); stack.wait_config_up().await; - match stack.join_multicast_group(PRIMARY_MULTICAST) { - Ok(()) => info!("ptp: joined primary multicast group"), - Err(error) => { - warn!("ptp: failed to join primary multicast group: {}", error) - } - } - match stack.join_multicast_group(LINK_LOCAL_MULTICAST) { - Ok(()) => info!("ptp: joined link-local multicast group"), - Err(error) => { - warn!("ptp: failed to join link-local multicast group: {}", error) - } - } + stack + .join_multicast_group(PRIMARY_MULTICAST) + .map_err(|source| RunError::Multicast { + address: PRIMARY_MULTICAST, + source, + })?; + info!("ptp: joined primary multicast group"); + stack + .join_multicast_group(LINK_LOCAL_MULTICAST) + .map_err(|source| RunError::Multicast { + address: LINK_LOCAL_MULTICAST, + source, + })?; + info!("ptp: joined link-local multicast group"); let event_socket = UdpSocket::new( stack, @@ -268,7 +317,7 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { &mut storage.general.tx_meta, &mut storage.general.tx_buffer, ); - let mut io = PortIo::new(event_socket, general_socket, config.tx_timestamp_timeout); + let mut io = PortIo::new(event_socket, general_socket, config.tx_timestamp_timeout)?; let clock_identity = clock_identity_from_mac(config.mac_address); info!( @@ -410,16 +459,24 @@ impl<'a> PortIo<'a> { mut event: UdpSocket<'a>, mut general: UdpSocket<'a>, tx_timestamp_timeout: EmbassyDuration, - ) -> Self { - event.bind(EVENT_PORT).unwrap(); - general.bind(GENERAL_PORT).unwrap(); - Self { + ) -> Result { + event.bind(EVENT_PORT).map_err(|source| RunError::Bind { + port: EVENT_PORT, + source, + })?; + general + .bind(GENERAL_PORT) + .map_err(|source| RunError::Bind { + port: GENERAL_PORT, + source, + })?; + Ok(Self { event, general, timers: Timers::default(), packet_id: PacketIdGenerator::new(), pending_tx: PendingTxQueue::new(tx_timestamp_timeout), - } + }) } async fn handle( diff --git a/statime-embassy-net/src/storage.rs b/statime-embassy-net/src/storage.rs index 7f62e2303..09b55fa24 100644 --- a/statime-embassy-net/src/storage.rs +++ b/statime-embassy-net/src/storage.rs @@ -20,6 +20,10 @@ type GeneralStorage = SocketStorage< >; /// Static packet and socket storage for one [`crate::Runner`]. +/// +/// Datagram storage is deliberately limited to 256 bytes. This covers the +/// ordinary E2E messages emitted by the runner; larger, TLV-heavy messages are +/// discarded as truncated. pub struct PtpStorage { pub(super) event: EventStorage, pub(super) general: GeneralStorage, From 8860708473481b27dcade5a3505c3a3318cab227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 20 Aug 2026 16:24:05 +0200 Subject: [PATCH 7/7] embassy: use standalone PTP clock driver --- statime-embassy-net/Cargo.lock | 21 +++++++--- statime-embassy-net/Cargo.toml | 4 +- statime-embassy-net/README.md | 2 +- .../examples/stm32h743/Cargo.lock | 40 ++++++++++++------- .../examples/stm32h743/Cargo.toml | 1 + .../examples/stm32h743/src/main.rs | 4 +- statime-embassy-net/src/embassy_clock.rs | 28 +++++++------ statime-embassy-net/src/lib.rs | 11 +++-- 8 files changed, 69 insertions(+), 42 deletions(-) diff --git a/statime-embassy-net/Cargo.lock b/statime-embassy-net/Cargo.lock index 4fee39a26..81fe21f3e 100644 --- a/statime-embassy-net/Cargo.lock +++ b/statime-embassy-net/Cargo.lock @@ -302,12 +302,12 @@ dependencies = [ [[package]] name = "embassy-futures" version = "0.1.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" [[package]] name = "embassy-net" version = "0.9.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "defmt", "document-features", @@ -325,7 +325,15 @@ dependencies = [ [[package]] name = "embassy-net-driver" version = "0.2.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +dependencies = [ + "defmt", +] + +[[package]] +name = "embassy-ptp-driver" +version = "0.1.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "defmt", ] @@ -333,7 +341,7 @@ dependencies = [ [[package]] name = "embassy-sync" version = "0.8.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "cfg-if", "critical-section", @@ -346,7 +354,7 @@ dependencies = [ [[package]] name = "embassy-time" version = "0.5.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "cfg-if", "critical-section", @@ -362,7 +370,7 @@ dependencies = [ [[package]] name = "embassy-time-driver" version = "0.2.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "document-features", ] @@ -966,6 +974,7 @@ dependencies = [ "defmt2log", "embassy-futures", "embassy-net", + "embassy-ptp-driver", "embassy-time", "rand_core", "rand_xorshift", diff --git a/statime-embassy-net/Cargo.toml b/statime-embassy-net/Cargo.toml index 5b1ada136..190e0886f 100644 --- a/statime-embassy-net/Cargo.toml +++ b/statime-embassy-net/Cargo.toml @@ -10,7 +10,7 @@ publish = false [features] default = [] -defmt = ["dep:defmt", "embassy-net/defmt", "embassy-time/defmt"] +defmt = ["dep:defmt", "embassy-net/defmt", "embassy-ptp-driver/defmt", "embassy-time/defmt"] monitor = [] [dependencies] @@ -27,6 +27,7 @@ embassy-net = { version = "0.9.1", default-features = false, features = [ "proto-ipv4", "udp", ] } +embassy-ptp-driver = "0.1.0" embassy-time = { version = "0.5.1", default-features = false } [dev-dependencies] @@ -36,6 +37,7 @@ defmt2log = "0.2.1" embassy-futures = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-net = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-net-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-ptp-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-sync = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-time = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-time-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } diff --git a/statime-embassy-net/README.md b/statime-embassy-net/README.md index 0bde75400..d36439aaa 100644 --- a/statime-embassy-net/README.md +++ b/statime-embassy-net/README.md @@ -12,7 +12,7 @@ This crate connects: The network driver must provide packet timestamps through `embassy-net` packet metadata and asynchronous transmit timestamp polling. `EmbassyClock` adapts -any `embassy_net::driver::Clock` to Statime's clock interface. The example +any `embassy_ptp_driver::Clock` to Statime's clock interface. The example uses Embassy STM32; applications using it must select their concrete `embassy-stm32` chip feature. diff --git a/statime-embassy-net/examples/stm32h743/Cargo.lock b/statime-embassy-net/examples/stm32h743/Cargo.lock index fec34df76..4e3c5be98 100644 --- a/statime-embassy-net/examples/stm32h743/Cargo.lock +++ b/statime-embassy-net/examples/stm32h743/Cargo.lock @@ -282,7 +282,7 @@ dependencies = [ [[package]] name = "embassy-embedded-hal" version = "0.6.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "defmt", "embassy-futures", @@ -300,7 +300,7 @@ dependencies = [ [[package]] name = "embassy-executor" version = "0.10.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "cordyceps", "cortex-m", @@ -315,7 +315,7 @@ dependencies = [ [[package]] name = "embassy-executor-macros" version = "0.8.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "darling", "proc-macro2", @@ -326,7 +326,7 @@ dependencies = [ [[package]] name = "embassy-executor-timer-queue" version = "0.1.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "unitrait", ] @@ -334,12 +334,12 @@ dependencies = [ [[package]] name = "embassy-futures" version = "0.1.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" [[package]] name = "embassy-hal-internal" version = "0.5.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "cortex-m", "critical-section", @@ -350,7 +350,7 @@ dependencies = [ [[package]] name = "embassy-net" version = "0.9.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "defmt", "document-features", @@ -368,7 +368,15 @@ dependencies = [ [[package]] name = "embassy-net-driver" version = "0.2.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +dependencies = [ + "defmt", +] + +[[package]] +name = "embassy-ptp-driver" +version = "0.1.0" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "defmt", ] @@ -376,7 +384,7 @@ dependencies = [ [[package]] name = "embassy-stm32" version = "0.6.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "aligned", "bit_field", @@ -393,6 +401,7 @@ dependencies = [ "embassy-futures", "embassy-hal-internal", "embassy-net-driver", + "embassy-ptp-driver", "embassy-sync", "embassy-time", "embassy-time-driver", @@ -431,7 +440,7 @@ dependencies = [ [[package]] name = "embassy-sync" version = "0.8.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "cfg-if", "critical-section", @@ -445,7 +454,7 @@ dependencies = [ [[package]] name = "embassy-time" version = "0.5.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "cfg-if", "critical-section", @@ -462,7 +471,7 @@ dependencies = [ [[package]] name = "embassy-time-driver" version = "0.2.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "document-features", ] @@ -470,7 +479,7 @@ dependencies = [ [[package]] name = "embassy-time-queue-utils" version = "0.3.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "embassy-executor-timer-queue", "heapless", @@ -479,7 +488,7 @@ dependencies = [ [[package]] name = "embassy-usb-driver" version = "0.2.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "defmt", "embedded-io-async 0.6.1", @@ -489,7 +498,7 @@ dependencies = [ [[package]] name = "embassy-usb-synopsys-otg" version = "0.4.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#5a69e5132d18478b3378252a34b2041df42a1171" +source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" dependencies = [ "defmt", "document-features", @@ -1058,6 +1067,7 @@ dependencies = [ "defmt", "embassy-futures", "embassy-net", + "embassy-ptp-driver", "embassy-time", "rand_core 0.6.4", "rand_xorshift", diff --git a/statime-embassy-net/examples/stm32h743/Cargo.toml b/statime-embassy-net/examples/stm32h743/Cargo.toml index 1fa99f704..17d47ffbf 100644 --- a/statime-embassy-net/examples/stm32h743/Cargo.toml +++ b/statime-embassy-net/examples/stm32h743/Cargo.toml @@ -55,6 +55,7 @@ embassy-futures = { git = "https://github.com/quartiq/embassy.git", branch = "pr embassy-hal-internal = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-net = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-net-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } +embassy-ptp-driver = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-stm32 = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-sync = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } embassy-time = { git = "https://github.com/quartiq/embassy.git", branch = "pr/network-clock" } diff --git a/statime-embassy-net/examples/stm32h743/src/main.rs b/statime-embassy-net/examples/stm32h743/src/main.rs index 989026867..8b4a04fdf 100644 --- a/statime-embassy-net/examples/stm32h743/src/main.rs +++ b/statime-embassy-net/examples/stm32h743/src/main.rs @@ -155,8 +155,6 @@ async fn net_task(mut runner: embassy_net::Runner<'static, Device>) -> ! { } #[embassy_executor::task] -async fn ptp_task( - mut runner: PtpRunner<'static, EmbassyClock>>, -) { +async fn ptp_task(mut runner: PtpRunner<'static, EmbassyClock>>) { unwrap!(runner.run().await); } diff --git a/statime-embassy-net/src/embassy_clock.rs b/statime-embassy-net/src/embassy_clock.rs index 558688f21..9bf878903 100644 --- a/statime-embassy-net/src/embassy_clock.rs +++ b/statime-embassy-net/src/embassy_clock.rs @@ -1,35 +1,39 @@ -use embassy_net::driver::{Clock as NetClock, ScaledPpm}; +use embassy_ptp_driver::{Clock as PtpClock, ScaledPpm, Timestamp}; use statime::{ Clock as StatimeClock, config::TimePropertiesDS, time::{Duration, Time}, }; -use crate::time_from; +use crate::time_from_parts; -/// A Statime clock backed by an Embassy network driver's clock. +fn time_from(timestamp: Timestamp) -> Time { + time_from_parts(timestamp.seconds(), timestamp.quarter_nanos()) +} + +/// A Statime clock backed by an Embassy PTP hardware clock. #[derive(Debug)] pub struct EmbassyClock { inner: T, } impl EmbassyClock { - /// Wrap an initialized Embassy network clock. + /// Wrap an initialized Embassy PTP clock. pub const fn new(inner: T) -> Self { Self { inner } } - /// Borrow the underlying network clock. + /// Borrow the underlying PTP clock. pub const fn inner(&self) -> &T { &self.inner } - /// Mutably borrow the underlying network clock. + /// Mutably borrow the underlying PTP clock. pub const fn inner_mut(&mut self) -> &mut T { &mut self.inner } - /// Unwrap the underlying network clock. + /// Unwrap the underlying PTP clock. pub fn into_inner(self) -> T { self.inner } @@ -40,7 +44,7 @@ impl EmbassyClock { #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[non_exhaustive] pub enum EmbassyClockError { - /// The network driver's clock rejected an operation. + /// The PTP clock rejected an operation. Clock(E), /// Statime requested a frequency adjustment that is not finite. NonFiniteFrequency, @@ -49,7 +53,7 @@ pub enum EmbassyClockError { impl core::fmt::Display for EmbassyClockError { fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Self::Clock(error) => write!(formatter, "network clock: {error}"), + Self::Clock(error) => write!(formatter, "PTP clock: {error}"), Self::NonFiniteFrequency => formatter.write_str("non-finite frequency adjustment"), } } @@ -64,7 +68,7 @@ impl core::error::Error for EmbassyClockError StatimeClock for EmbassyClock { +impl StatimeClock for EmbassyClock { type Error = EmbassyClockError; fn now(&self) -> Time { @@ -107,8 +111,6 @@ impl StatimeClock for EmbassyClock { #[cfg(test)] mod tests { - use embassy_net::driver::Timestamp; - use super::*; #[derive(Debug, Default)] @@ -130,7 +132,7 @@ mod tests { impl core::error::Error for TestError {} - impl NetClock for TestClock { + impl PtpClock for TestClock { type Error = TestError; fn now(&self) -> Timestamp { diff --git a/statime-embassy-net/src/lib.rs b/statime-embassy-net/src/lib.rs index f4706ac32..df0172080 100644 --- a/statime-embassy-net/src/lib.rs +++ b/statime-embassy-net/src/lib.rs @@ -745,9 +745,14 @@ fn clock_identity_from_mac(mac: [u8; 6]) -> ClockIdentity { } pub(crate) fn time_from(timestamp: Timestamp) -> Time { - let nanos = - u64::from(timestamp.seconds) * 1_000_000_000 + u64::from(timestamp.quarter_nanos >> 2); - Time::from_nanos_subnanos(nanos, (timestamp.quarter_nanos & 3) << 30) + time_from_parts(timestamp.seconds, timestamp.quarter_nanos) +} + +// Packet and adjustable-clock timestamps deliberately belong to independent +// Embassy driver crates. Convert their shared representation only here. +pub(crate) fn time_from_parts(seconds: u32, quarter_nanos: u32) -> Time { + let nanos = u64::from(seconds) * 1_000_000_000 + u64::from(quarter_nanos >> 2); + Time::from_nanos_subnanos(nanos, (quarter_nanos & 3) << 30) } fn multicast_endpoint(port: u16, link_local: bool) -> IpEndpoint {