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 01/10] 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 02/10] 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 62ab4275c7ee1cc7b89e266030c0a79ab0590ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 28 Sep 2026 15:17:25 +0000 Subject: [PATCH 03/10] Avoid float formatting in fixed-wander clamps Keep the f64 clamp comparisons and invalid-bound checks, but use a static panic message so embedded consumers do not retain core float formatting solely for configuration errors. Cover saturation, infinities, NaN propagation, signed zero, and invalid bounds. Paired Stabilizer FLS builds save 15,360 flash bytes. --- statime/src/filters/fixed_wander.rs | 73 +++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/statime/src/filters/fixed_wander.rs b/statime/src/filters/fixed_wander.rs index 5209f2363..b8f0b6bff 100644 --- a/statime/src/filters/fixed_wander.rs +++ b/statime/src/filters/fixed_wander.rs @@ -23,6 +23,19 @@ use crate::{ const ERROR_SAMPLES: usize = 32; +// Match f64::clamp without pulling float formatting into embedded panic paths. +#[allow(clippy::manual_clamp)] +fn clamp(mut value: f64, min: f64, max: f64) -> f64 { + assert!(min <= max, "invalid clamp bounds"); + if value < min { + value = min; + } + if value > max { + value = max; + } + value +} + fn sqr(value: f64) -> f64 { value * value } @@ -296,8 +309,11 @@ impl FixedWanderKalmanFilter { return; }; let requested = target - estimate.frequency() * 1e6; - let next = - (current + requested).clamp(-self.config.max_frequency, self.config.max_frequency); + let next = clamp( + current + requested, + -self.config.max_frequency, + self.config.max_frequency, + ); let applied = next - current; if let Ok(time) = clock.set_frequency(next) { self.frequency = Some(next); @@ -316,8 +332,11 @@ impl FixedWanderKalmanFilter { }; 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); + let target = clamp( + -offset * 1e6 / self.config.steer_time.seconds(), + -self.config.max_steer, + self.config.max_steer, + ); self.change_frequency(target, clock); FilterUpdate { next_update: Some(core::time::Duration::from_secs_f64( @@ -399,6 +418,52 @@ mod tests { assert!((actual - expected).abs() < 1e-12 * expected.abs().max(1.0)); } + #[test] + fn clamp_matches_float_contract() { + for (min, max) in [ + (-200.0, 200.0), + (0.0, 0.0), + (-0.0, 0.0), + (0.0, -0.0), + (f64::NEG_INFINITY, f64::INFINITY), + ] { + for value in [ + f64::NEG_INFINITY, + -400.0, + -200.0, + -0.0, + 0.0, + 200.0, + 400.0, + f64::INFINITY, + f64::from_bits(0x7ff8_0000_0000_0042), + ] { + assert_eq!( + clamp(value, min, max).to_bits(), + value.clamp(min, max).to_bits() + ); + } + } + } + + #[test] + #[should_panic(expected = "invalid clamp bounds")] + fn clamp_rejects_reversed_bounds() { + clamp(0.0, 1.0, -1.0); + } + + #[test] + #[should_panic(expected = "invalid clamp bounds")] + fn clamp_rejects_nan_lower_bound() { + clamp(0.0, f64::NAN, 1.0); + } + + #[test] + #[should_panic(expected = "invalid clamp bounds")] + fn clamp_rejects_nan_upper_bound() { + clamp(0.0, -1.0, f64::NAN); + } + #[test] fn measurement_noise_uses_startup_range_then_ring_variance() { let config = FixedWanderKalmanConfig { From e617206532bcb1f464cab0e34e118efc0f78e4a2 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 04/10] 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 d88495a9096be97f4b6ed69822773bc8e355c8a8 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 05/10] 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 e2e290c4f523800520d5ffb4fd6850ac7bc17c2b 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 06/10] 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 4d8551b93974a43537a841c582282838caeff820 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 07/10] 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 79be43c6162c864e3a1b26db23ed95c638f2fee4 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 08/10] 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 { From 5d45fd458e706ce978dacfe1881210c94cc8a7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 25 Sep 2026 15:07:07 +0000 Subject: [PATCH 09/10] embassy: port runner to Xarxa --- statime-embassy-net/Cargo.lock | 70 +-- statime-embassy-net/Cargo.toml | 16 +- statime-embassy-net/README.md | 49 +- .../examples/stm32h743/.cargo/config.toml | 3 + .../examples/stm32h743/Cargo.lock | 113 ++--- .../examples/stm32h743/Cargo.toml | 29 +- .../examples/stm32h743/README.md | 13 +- .../examples/stm32h743/memory.x | 3 +- .../examples/stm32h743/src/main.rs | 28 +- statime-embassy-net/src/lib.rs | 450 ++---------------- statime-embassy-net/src/monitor.rs | 12 +- statime-embassy-net/src/storage.rs | 73 --- statime-embassy-net/src/transport.rs | 442 +++++++++++++++++ 13 files changed, 640 insertions(+), 661 deletions(-) delete mode 100644 statime-embassy-net/src/storage.rs create mode 100644 statime-embassy-net/src/transport.rs diff --git a/statime-embassy-net/Cargo.lock b/statime-embassy-net/Cargo.lock index 81fe21f3e..db4539273 100644 --- a/statime-embassy-net/Cargo.lock +++ b/statime-embassy-net/Cargo.lock @@ -70,6 +70,12 @@ 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 = "bumpalo" version = "3.20.3" @@ -203,7 +209,7 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "defmt-macros", ] @@ -302,38 +308,28 @@ dependencies = [ [[package]] name = "embassy-futures" version = "0.1.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" [[package]] name = "embassy-net" version = "0.9.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" 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#2773ab550a08e98ec39953c66e00a10bc9268dcd" -dependencies = [ - "defmt", + "xarxa-driver", ] [[package]] name = "embassy-ptp-driver" version = "0.1.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "defmt", ] @@ -341,7 +337,7 @@ dependencies = [ [[package]] name = "embassy-sync" version = "0.8.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "cfg-if", "critical-section", @@ -354,7 +350,7 @@ dependencies = [ [[package]] name = "embassy-time" version = "0.5.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "cfg-if", "critical-section", @@ -370,7 +366,7 @@ dependencies = [ [[package]] name = "embassy-time-driver" version = "0.2.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "document-features", ] @@ -415,25 +411,6 @@ 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" @@ -667,12 +644,6 @@ 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" @@ -1272,24 +1243,25 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "xarxa" -version = "0.13.1" -source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +version = "0.1.0" +source = "git+https://github.com/embassy-rs/xarxa?rev=7dd74474e337a7175c3ec48dabb0504a32e255dc#7dd74474e337a7175c3ec48dabb0504a32e255dc" dependencies = [ - "bitflags", + "bitflags 2.13.1", "byteorder", - "cfg-if", "defmt", + "document-features", "heapless", - "managed", "xarxa-driver", ] [[package]] name = "xarxa-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +source = "git+https://github.com/embassy-rs/xarxa?rev=7dd74474e337a7175c3ec48dabb0504a32e255dc#7dd74474e337a7175c3ec48dabb0504a32e255dc" dependencies = [ + "critical-section", "defmt", + "document-features", ] [[package]] diff --git a/statime-embassy-net/Cargo.toml b/statime-embassy-net/Cargo.toml index 190e0886f..4e458c323 100644 --- a/statime-embassy-net/Cargo.toml +++ b/statime-embassy-net/Cargo.toml @@ -24,7 +24,8 @@ embassy-net = { version = "0.9.1", default-features = false, features = [ "multicast", "packetmeta-id", "packetmeta-timestamp", - "proto-ipv4", + "ipv4", + "iface-bind", "udp", ] } embassy-ptp-driver = "0.1.0" @@ -34,13 +35,12 @@ embassy-time = { version = "0.5.1", default-features = false } 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-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" } +embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-net = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-ptp-driver = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-time-driver = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } [profile.release] codegen-units = 1 diff --git a/statime-embassy-net/README.md b/statime-embassy-net/README.md index d36439aaa..e6a852c8a 100644 --- a/statime-embassy-net/README.md +++ b/statime-embassy-net/README.md @@ -3,36 +3,29 @@ PTP ordinary-clock runner for `statime` on timestamp-capable `embassy-net` Ethernet drivers. -This crate connects: +Construct `Runner::new(iface, clock, config, filter_config)` for an IPv4 +Ethernet interface and run it alongside the Embassy network runner. It must +be the stack's only TX timestamp requester and consumer. PTP sockets remain +bound to the selected interface and its clock. Reserve two UDP sockets for +PTP ports 319 and 320. -- `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 clock must control the hardware time domain used for packet timestamps. +`EmbassyClock` adapts an initialized `embassy_ptp_driver::Clock` to Statime's +clock interface. Clock configuration and network interface setup belong to +the application. -The network driver must provide packet timestamps through `embassy-net` packet -metadata and asynchronous transmit timestamp polling. `EmbassyClock` adapts -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. +The runner supports single-port UDP/IPv4 with E2E delay measurement and is +slave-only by default. See `Config` and `Runner::run` for configuration and +lifecycle behavior. -The runner is currently a single-port UDP/IPv4 ordinary clock using E2E delay -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. - -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. +Enable `defmt` for diagnostics or `monitor` with `Runner::with_monitor` for +protocol/filter activity indicators. These indicators do not establish clock +accuracy or lock. 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. +STM32H743 Embassy application. + +The Cargo patches pin an upstream Embassy main revision that includes the TX +timestamp queue. Embassy pins the matching Xarxa revision. For STM32, enable +`embassy-stm32/ptp`; this crate enables the required Embassy network timestamp +features. diff --git a/statime-embassy-net/examples/stm32h743/.cargo/config.toml b/statime-embassy-net/examples/stm32h743/.cargo/config.toml index 5686ab26e..bf9f76e82 100644 --- a/statime-embassy-net/examples/stm32h743/.cargo/config.toml +++ b/statime-embassy-net/examples/stm32h743/.cargo/config.toml @@ -12,3 +12,6 @@ rustflags = [ [env] DEFMT_LOG = "info" +# Four RX and four TX descriptors, eight queued PTP receives, four spare buffers. +XARXA_PACKET_BUF_COUNT = "20" +XARXA_UDP_SOCKET_COUNT = "2" diff --git a/statime-embassy-net/examples/stm32h743/Cargo.lock b/statime-embassy-net/examples/stm32h743/Cargo.lock index 4e3c5be98..7db7de140 100644 --- a/statime-embassy-net/examples/stm32h743/Cargo.lock +++ b/statime-embassy-net/examples/stm32h743/Cargo.lock @@ -279,14 +279,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "embassy-crypto" +version = "0.1.0" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" +dependencies = [ + "defmt", + "unitrait", +] + [[package]] name = "embassy-embedded-hal" version = "0.6.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "defmt", "embassy-futures", - "embassy-hal-internal", "embassy-sync", "embassy-time", "embedded-hal 0.2.7", @@ -300,7 +308,7 @@ dependencies = [ [[package]] name = "embassy-executor" version = "0.10.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "cordyceps", "cortex-m", @@ -315,7 +323,7 @@ dependencies = [ [[package]] name = "embassy-executor-macros" version = "0.8.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "darling", "proc-macro2", @@ -326,7 +334,7 @@ dependencies = [ [[package]] name = "embassy-executor-timer-queue" version = "0.1.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "unitrait", ] @@ -334,12 +342,12 @@ dependencies = [ [[package]] name = "embassy-futures" version = "0.1.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" [[package]] name = "embassy-hal-internal" version = "0.5.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "cortex-m", "critical-section", @@ -350,33 +358,23 @@ dependencies = [ [[package]] name = "embassy-net" version = "0.9.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" 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#2773ab550a08e98ec39953c66e00a10bc9268dcd" -dependencies = [ - "defmt", + "xarxa-driver", ] [[package]] name = "embassy-ptp-driver" version = "0.1.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "defmt", ] @@ -384,7 +382,7 @@ dependencies = [ [[package]] name = "embassy-stm32" version = "0.6.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "aligned", "bit_field", @@ -397,10 +395,10 @@ dependencies = [ "defmt", "document-features", "dsp-fixedpoint", + "embassy-crypto", "embassy-embedded-hal", "embassy-futures", "embassy-hal-internal", - "embassy-net-driver", "embassy-ptp-driver", "embassy-sync", "embassy-time", @@ -435,12 +433,13 @@ dependencies = [ "stm32-metapac", "trait-set", "vcell", + "xarxa-driver", ] [[package]] name = "embassy-sync" version = "0.8.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "cfg-if", "critical-section", @@ -454,7 +453,7 @@ dependencies = [ [[package]] name = "embassy-time" version = "0.5.1" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "cfg-if", "critical-section", @@ -471,7 +470,7 @@ dependencies = [ [[package]] name = "embassy-time-driver" version = "0.2.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "document-features", ] @@ -479,7 +478,7 @@ dependencies = [ [[package]] name = "embassy-time-queue-utils" version = "0.3.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "embassy-executor-timer-queue", "heapless", @@ -488,7 +487,7 @@ dependencies = [ [[package]] name = "embassy-usb-driver" version = "0.2.2" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "defmt", "embedded-io-async 0.6.1", @@ -498,7 +497,7 @@ dependencies = [ [[package]] name = "embassy-usb-synopsys-otg" version = "0.4.0" -source = "git+https://github.com/quartiq/embassy.git?branch=pr%2Fnetwork-clock#2773ab550a08e98ec39953c66e00a10bc9268dcd" +source = "git+https://github.com/embassy-rs/embassy?rev=8ba7a9c1f110f5b1834630ac2b6cfab436e33022#8ba7a9c1f110f5b1834630ac2b6cfab436e33022" dependencies = [ "defmt", "document-features", @@ -585,25 +584,6 @@ dependencies = [ "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" @@ -782,12 +762,6 @@ dependencies = [ "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" @@ -1114,7 +1088,7 @@ dependencies = [ [[package]] name = "stm32-metapac" version = "21.0.0" -source = "git+https://github.com/embassy-rs/stm32-data-generated?tag=stm32-data-7a8bb7113f8548e09dca4ed214af3acb7925aa31#dd153dd8afdd2029cac5733d3cb5c819df2a8836" +source = "git+https://github.com/embassy-rs/stm32-data-generated?tag=stm32-data-3b1e9bcf7cd6a04e2ccb74182b22ded50d264723#a19c9d360feee606167002e5047455c23ec5a8f8" dependencies = [ "cortex-m", "cortex-m-rt", @@ -1285,9 +1259,21 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unitrait" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66b59b4e118746027256db08772826e9e0765e93b0bf15d2714a0ac0f940fbe" +version = "2.0.0" +source = "git+https://github.com/embassy-rs/unitrait?rev=bb96601b3d293fa27d1ca2891f6ea148e534e433#bb96601b3d293fa27d1ca2891f6ea148e534e433" +dependencies = [ + "unitrait-macros", +] + +[[package]] +name = "unitrait-macros" +version = "2.0.0" +source = "git+https://github.com/embassy-rs/unitrait?rev=bb96601b3d293fa27d1ca2891f6ea148e534e433#bb96601b3d293fa27d1ca2891f6ea148e534e433" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "valuable" @@ -1342,24 +1328,25 @@ dependencies = [ [[package]] name = "xarxa" -version = "0.13.1" -source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +version = "0.1.0" +source = "git+https://github.com/embassy-rs/xarxa?rev=7dd74474e337a7175c3ec48dabb0504a32e255dc#7dd74474e337a7175c3ec48dabb0504a32e255dc" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.1", "byteorder", - "cfg-if", "defmt", + "document-features", "heapless", - "managed", "xarxa-driver", ] [[package]] name = "xarxa-driver" version = "0.1.0" -source = "git+https://github.com/embassy-rs/xarxa?rev=1f332ac32cc33d86aefc8e1c1a9749b93234a6de#1f332ac32cc33d86aefc8e1c1a9749b93234a6de" +source = "git+https://github.com/embassy-rs/xarxa?rev=7dd74474e337a7175c3ec48dabb0504a32e255dc#7dd74474e337a7175c3ec48dabb0504a32e255dc" dependencies = [ + "critical-section", "defmt", + "document-features", ] [[package]] diff --git a/statime-embassy-net/examples/stm32h743/Cargo.toml b/statime-embassy-net/examples/stm32h743/Cargo.toml index 17d47ffbf..23fdc2835 100644 --- a/statime-embassy-net/examples/stm32h743/Cargo.toml +++ b/statime-embassy-net/examples/stm32h743/Cargo.toml @@ -30,7 +30,7 @@ embassy-net = { version = "0.9.1", default-features = false, features = [ "multicast", "packetmeta-id", "packetmeta-timestamp", - "proto-ipv4", + "ipv4", "udp", ] } embassy-stm32 = { version = "0.6.0", default-features = false, features = [ @@ -49,20 +49,19 @@ 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-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" } -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" } +embassy-embedded-hal = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-futures = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-hal-internal = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-net = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-ptp-driver = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-stm32 = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-time-driver = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-time-queue-utils = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-usb-driver = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } +embassy-usb-synopsys-otg = { git = "https://github.com/embassy-rs/embassy", rev = "8ba7a9c1f110f5b1834630ac2b6cfab436e33022" } [profile.release] codegen-units = 1 diff --git a/statime-embassy-net/examples/stm32h743/README.md b/statime-embassy-net/examples/stm32h743/README.md index 689245643..28333c4ac 100644 --- a/statime-embassy-net/examples/stm32h743/README.md +++ b/statime-embassy-net/examples/stm32h743/README.md @@ -2,8 +2,17 @@ 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). +[`src/main.rs`](src/main.rs). [`memory.x`](memory.x) places Ethernet descriptors +in SRAM3 and ordinary data, including Xarxa's shared packet pool, in +DMA-accessible AXI SRAM. DTCM cannot hold Ethernet packet payloads. This +example leaves the data cache disabled. + +Packet-pool and UDP-socket capacities are configured in +[`.cargo/config.toml`](.cargo/config.toml), including the buffer budget for +descriptors, queued receives, and spare capacity. Increase that budget when +adding other network services. + +The sibling Embassy dependency is described in the [crate README](../../README.md). From this directory: diff --git a/statime-embassy-net/examples/stm32h743/memory.x b/statime-embassy-net/examples/stm32h743/memory.x index 8a2e5cf27..a734ffcf8 100644 --- a/statime-embassy-net/examples/stm32h743/memory.x +++ b/statime-embassy-net/examples/stm32h743/memory.x @@ -1,7 +1,8 @@ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K - RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K + /* Includes Xarxa's packet pool: Ethernet DMA cannot access DTCM. */ + RAM (rwx) : ORIGIN = 0x24000000, LENGTH = 512K SRAM3 (rwx) : ORIGIN = 0x30040000, LENGTH = 32K } diff --git a/statime-embassy-net/examples/stm32h743/src/main.rs b/statime-embassy-net/examples/stm32h743/src/main.rs index 8b4a04fdf..3de2b5717 100644 --- a/statime-embassy-net/examples/stm32h743/src/main.rs +++ b/statime-embassy-net/examples/stm32h743/src/main.rs @@ -5,7 +5,7 @@ use core::mem::MaybeUninit; use defmt::unwrap; use embassy_executor::Spawner; -use embassy_net::{Config as NetConfig, StackResources}; +use embassy_net::{Stack, StackStorage}; use embassy_stm32::{ bind_interrupts, eth::{Ethernet, GenericPhy, InterruptHandler, PacketQueue, PtpClockConfig, Sma}, @@ -15,7 +15,7 @@ use embassy_stm32::{ }; use static_cell::StaticCell; use statime::filters::{FixedWanderKalmanConfig, FixedWanderKalmanFilter}; -use statime_embassy_net::{Config as PtpConfig, EmbassyClock, PtpStorage, Runner as PtpRunner}; +use statime_embassy_net::{Config as PtpConfig, EmbassyClock, Runner as PtpRunner}; use {defmt_rtt as _, panic_probe as _}; @@ -28,7 +28,6 @@ fn defmt_panic() -> ! { const ETH_TX_PACKETS: usize = 4; const ETH_RX_PACKETS: usize = 4; -const STACK_SOCKETS: usize = 4; type Device = Ethernet<'static, ETH, GenericPhy>>; @@ -39,8 +38,8 @@ bind_interrupts!(struct Irqs { #[unsafe(link_section = ".sram3.eth")] static mut PACKETS: MaybeUninit> = MaybeUninit::uninit(); -static PTP_STORAGE: StaticCell = StaticCell::new(); -static STACK_RESOURCES: StaticCell> = StaticCell::new(); +static DEVICE: StaticCell = StaticCell::new(); +static STACK_STORAGE: StaticCell> = StaticCell::new(); mod board { use embassy_stm32::{ @@ -88,8 +87,7 @@ mod board { 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. + // Enable SRAM3 before initializing the Ethernet descriptor rings. pac::RCC.ahb2enr().modify(|w| w.set_sram3en(true)); // ETH wakes the network runner; TIM12 drives embassy-time deadlines. @@ -118,7 +116,6 @@ async fn main(spawner: Spawner) -> ! { let mut device = Ethernet::new_with_phy( queue, p.ETH, - Irqs, p.PA1, p.PA7, p.PC4, @@ -126,20 +123,17 @@ async fn main(spawner: Spawner) -> ! { p.PB12, p.PG14, p.PB11, + Irqs, 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 (stack, runner) = Stack::new(STACK_STORAGE.init(StackStorage::new()), board::SEED); + let iface = unwrap!(stack.add_iface_borrowed(DEVICE.init(device))); + unwrap!(iface.set_dhcpv4(Some(Default::default()))); let ptp_runner = PtpRunner::<_, FixedWanderKalmanFilter>::new( - stack, + iface, ptp_clock, - PTP_STORAGE.init(PtpStorage::new()), PtpConfig::new(board::MAC_ADDRESS, board::SEED), FixedWanderKalmanConfig::default(), ); @@ -150,7 +144,7 @@ async fn main(spawner: Spawner) -> ! { } #[embassy_executor::task] -async fn net_task(mut runner: embassy_net::Runner<'static, Device>) -> ! { +async fn net_task(mut runner: embassy_net::Runner<'static>) -> ! { runner.run().await } diff --git a/statime-embassy-net/src/lib.rs b/statime-embassy-net/src/lib.rs index df0172080..3e0c65e40 100644 --- a/statime-embassy-net/src/lib.rs +++ b/statime-embassy-net/src/lib.rs @@ -5,17 +5,14 @@ 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, MulticastError, Stack, TryError, driver::{Timestamp, TxTimestamp}, + iface::{Iface, MulticastError}, udp, - udp::{UdpMetadata, UdpSocket}, + wire::Ipv4Addr, }; -use embassy_time::{Duration as EmbassyDuration, Instant, with_deadline}; +use embassy_time::{Duration as EmbassyDuration, Instant}; use rand_core::SeedableRng; use rand_xorshift::XorShiftRng; use statime::{ @@ -26,7 +23,7 @@ use statime::{ }, filters::{Filter, FixedWanderKalmanFilter}, observability::port::PortState, - port::{NoForwardedTLVs, PortAction, PortActionIterator, TimestampContext}, + port::{NoForwardedTLVs, PortActionIterator}, time::{Duration, Interval, Time}, }; @@ -59,27 +56,26 @@ macro_rules! warn { 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); +mod transport; +use transport::{Incoming, PacketIdGenerator, PortIo, StatimeTimer}; + 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; /// 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 stack has no free UDP socket slot. + Socket { + /// PTP port requiring a socket. + port: u16, + }, /// The network stack could not join a required PTP multicast group. Multicast { /// Multicast address the runner tried to join. - address: Ipv4Address, + address: Ipv4Addr, /// Error reported by the network stack. source: MulticastError, }, @@ -95,6 +91,9 @@ pub enum RunError { impl core::fmt::Display for RunError { fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { + Self::Socket { port } => { + write!(formatter, "no UDP socket available for PTP port {port}") + } Self::Multicast { address, source } => { write!(formatter, "joining PTP multicast group {address}: {source}") } @@ -109,7 +108,7 @@ 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, + _ => None, } } } @@ -147,6 +146,7 @@ pub struct Config { /// Time properties advertised by this clock when it becomes master. pub time_properties: TimePropertiesDS, /// Maximum time to wait for a hardware transmit timestamp. + /// Also bounds waiting for send capacity; protocol deadlines may shorten it. pub tx_timestamp_timeout: EmbassyDuration, } @@ -193,11 +193,11 @@ impl Config { /// 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>, + iface: Iface<'a>, clock: C, - storage: &'a mut PtpStorage, config: Config, filter_config: F::Config, + packet_id: PacketIdGenerator, #[cfg(feature = "monitor")] monitor: Option<&'a PtpMonitor>, } @@ -243,21 +243,15 @@ impl Clock for ClockRef<'_, C> { } 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 { + /// Construct the service for an interface, its PTP clock, and protocol/filter configuration. + /// Socket binding and multicast setup happen in [`run`](Self::run). + pub fn new(iface: Iface<'a>, clock: C, config: Config, filter_config: F::Config) -> Self { Self { - stack, + iface, clock, - storage, config, filter_config, + packet_id: PacketIdGenerator::new(), #[cfg(feature = "monitor")] monitor: None, } @@ -272,52 +266,28 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { /// Run the PTP service until cancelled, or return an initialization error. /// + /// Run the Embassy network runner concurrently. This service must be the + /// stack's only TX timestamp requester and consumer. PTP sockets remain + /// bound to the selected interface and its clock. + /// /// 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. + /// Reuse this runner after cancellation to preserve packet IDs. Replacing it + /// requires that no TX timestamps from the old runner can arrive. pub async fn run(&mut self) -> Result<(), RunError> { - let stack = self.stack; + let iface = self.iface; 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; - 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, - &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)?; + iface.wait_config_v4_up().await; + let mut io = PortIo::new(iface, config.tx_timestamp_timeout)?; let clock_identity = clock_identity_from_mac(config.mac_address); info!( @@ -363,6 +333,8 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { io.handle( actions, + bmca, + &mut self.packet_id, #[cfg(feature = "monitor")] self.monitor, ) @@ -393,6 +365,8 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { } io.handle( actions, + bmca, + &mut self.packet_id, #[cfg(feature = "monitor")] self.monitor, ) @@ -400,6 +374,9 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { continue; } + io.pending_tx.expire(); + // Keep the owned packet alive only through this action batch. + let mut incoming = Incoming::None; let actions = if let Some(timestamp) = tx_timestamp.take() { match io.pending_tx.take(timestamp.id) { Some(context) => { @@ -423,322 +400,31 @@ impl<'a, C: Clock, F: Filter> Runner<'a, C, F> { } } else { let receive_delay_requests = port.port_ds().port_state == PortState::Master; - match io.receive(&mut storage.packet, receive_delay_requests) { + incoming = io.receive(receive_delay_requests); + match &incoming { Incoming::Event(packet, timestamp) => { - port.handle_event_receive(packet, time_from(timestamp)) + port.handle_event_receive(packet.payload(), time_from(*timestamp)) } - Incoming::General(packet) => port.handle_general_receive(packet), + Incoming::General(packet) => port.handle_general_receive(packet.payload()), Incoming::None => PortActionIterator::empty(), } }; - io.pending_tx.expire(); - io.handle( actions, + bmca, + &mut self.packet_id, #[cfg(feature = "monitor")] self.monitor, ) .await; + drop(incoming); 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, - ) -> 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( - &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, + tx_timestamp = io.wait(next).await; } } } -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]]) @@ -755,15 +441,6 @@ pub(crate) fn time_from_parts(seconds: u32, quarter_nanos: u32) -> Time { Time::from_nanos_subnanos(nanos, (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) @@ -787,7 +464,7 @@ fn state_name(state: PortState) -> &'static str { mod tests { use super::*; - fn init_logging() { + pub(crate) fn init_logging() { static INIT: std::sync::Once = std::sync::Once::new(); INIT.call_once(defmt2log::init_from_current_exe); } @@ -805,31 +482,4 @@ mod tests { ); } } - - #[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 index 17b9c1259..3f0e9ba5d 100644 --- a/statime-embassy-net/src/monitor.rs +++ b/statime-embassy-net/src/monitor.rs @@ -1,15 +1,17 @@ use core::sync::atomic::{AtomicU8, Ordering}; -/// Quality of the PHC's relation to its selected PTP master. +/// Protocol/filter activity observed by the runner on the PTP timescale. +/// +/// These indicators do not establish clock accuracy or successful discipline. #[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. + /// No tracking indication, no PTP timescale, or the clock has stepped. Unavailable, - /// The servo is receiving measurements and disciplining the PHC. + /// The filter scheduled an update timer while using the PTP timescale. Tracking, - /// Measurements stopped; the PHC retains its last applied rate. + /// The update timer expired or the port left slave state after tracking. Holdover, } @@ -29,7 +31,7 @@ impl PtpMonitor { } } - /// Read the current TAI clock-relation state. + /// Read the current activity indication, unavailable outside the PTP timescale. pub fn state(&self) -> ClockState { let state = self.state.load(Ordering::Relaxed); if state & Self::PTP_TIMESCALE == 0 { diff --git a/statime-embassy-net/src/storage.rs b/statime-embassy-net/src/storage.rs deleted file mode 100644 index 09b55fa24..000000000 --- a/statime-embassy-net/src/storage.rs +++ /dev/null @@ -1,73 +0,0 @@ -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`]. -/// -/// 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, - 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], - } - } -} diff --git a/statime-embassy-net/src/transport.rs b/statime-embassy-net/src/transport.rs new file mode 100644 index 000000000..147c71a61 --- /dev/null +++ b/statime-embassy-net/src/transport.rs @@ -0,0 +1,442 @@ +use core::num::NonZero; +use embassy_futures::select::{Either3, select3}; +use embassy_net::{ + TryError, + driver::{Timestamp, TxTimestamp}, + iface::Iface, + udp::{self, UdpMetadata, UdpSocket}, + wire::{Ipv4Addr, SocketAddr}, +}; +use embassy_time::{Duration as EmbassyDuration, Instant, with_deadline}; +use statime::port::{PortAction, PortActionIterator, TimestampContext}; + +#[cfg(feature = "monitor")] +use crate::PtpMonitor; +use crate::{RunError, deadline_from_now}; + +const EVENT_PORT: u16 = 319; +const GENERAL_PORT: u16 = 320; +const PRIMARY_MULTICAST: Ipv4Addr = Ipv4Addr::new(224, 0, 1, 129); +const LINK_LOCAL_MULTICAST: Ipv4Addr = Ipv4Addr::new(224, 0, 0, 107); +const TX_PENDING: usize = 4; +const MAX_PACKET_LEN: usize = 256; +const MSG_DELAY_REQ: u8 = 0x1; +const MSG_PDELAY_REQ: u8 = 0x2; +const MSG_PDELAY_RESP: u8 = 0x3; + +pub(super) struct PortIo<'a> { + iface: Iface<'a>, + event: UdpSocket<'a>, + general: UdpSocket<'a>, + joined: [bool; 2], + pub(super) timers: Timers, + pub(super) pending_tx: PendingTxQueue, +} + +impl<'a> PortIo<'a> { + pub(super) fn new( + iface: Iface<'a>, + tx_timestamp_timeout: EmbassyDuration, + ) -> Result { + let mut event = + UdpSocket::new(iface.stack()).map_err(|_| RunError::Socket { port: EVENT_PORT })?; + let mut general = + UdpSocket::new(iface.stack()).map_err(|_| RunError::Socket { port: GENERAL_PORT })?; + event + .bind_to_iface(Some(iface.handle())) + .map_err(|source| RunError::Bind { + port: EVENT_PORT, + source, + })?; + general + .bind_to_iface(Some(iface.handle())) + .map_err(|source| RunError::Bind { + port: GENERAL_PORT, + source, + })?; + event.bind(EVENT_PORT, 0).map_err(|source| RunError::Bind { + port: EVENT_PORT, + source, + })?; + general + .bind(GENERAL_PORT, 0) + .map_err(|source| RunError::Bind { + port: GENERAL_PORT, + source, + })?; + let mut io = Self { + iface, + event, + general, + joined: [false; 2], + timers: Timers::default(), + pending_tx: PendingTxQueue::new(tx_timestamp_timeout), + }; + for (index, address) in [PRIMARY_MULTICAST, LINK_LOCAL_MULTICAST] + .into_iter() + .enumerate() + { + if !iface.has_multicast_group(address) { + iface + .join_multicast_group(address) + .map_err(|source| RunError::Multicast { address, source })?; + io.joined[index] = true; + } + } + Ok(io) + } + + pub(super) async fn handle( + &mut self, + actions: PortActionIterator<'_>, + deadline: Instant, + packet_id: &mut PacketIdGenerator, + #[cfg(feature = "monitor")] monitor: Option<&PtpMonitor>, + ) { + for action in actions { + match action { + PortAction::SendEvent { + context, + data, + link_local, + } => { + let metadata = UdpMetadata { + remote_addr: multicast_endpoint(EVENT_PORT, link_local), + meta: packet_id.next(), + local_addr: None, + }; + if !self.pending_tx.push(context, metadata.meta.id) { + continue; + } + let deadline = self.next_deadline(deadline); + match with_deadline(deadline, self.event.send_to(data, metadata)).await { + Ok(Ok(())) => self.pending_tx.sent(metadata.meta.id), + _ => { + self.pending_tx.take(metadata.meta.id); + warn!("ptp: event send failed or timed out"); + } + } + } + PortAction::SendGeneral { data, link_local } => { + let metadata = UdpMetadata { + remote_addr: multicast_endpoint(GENERAL_PORT, link_local), + meta: udp::PacketMeta::default(), + local_addr: None, + }; + let deadline = self + .next_deadline(deadline) + .min(Instant::now() + self.pending_tx.timeout); + if !matches!( + with_deadline(deadline, self.general.send_to(data, metadata)).await, + Ok(Ok(())) + ) { + warn!("ptp: general send failed or timed out"); + } + } + 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 { .. } => {} + } + } + } + + pub(super) fn receive(&self, receive_delay_requests: bool) -> Incoming { + match self.event.try_recv() { + Ok(packet) => { + if packet.payload().len() > MAX_PACKET_LEN { + warn!("ptp: oversized event packet"); + return Incoming::None; + } + return rx_event_timestamp( + packet.payload(), + packet.meta().meta, + receive_delay_requests, + ) + .map_or(Incoming::None, |timestamp| { + Incoming::Event(packet, timestamp) + }); + } + Err(TryError::Other(error)) => { + warn!("ptp: event receive failed: {}", error); + return Incoming::None; + } + Err(TryError::WouldBlock) => {} + } + + match self.general.try_recv() { + Ok(packet) + if packet.payload().len() <= MAX_PACKET_LEN + && ptp_message_type(packet.payload()).is_some() => + { + Incoming::General(packet) + } + Ok(_) | Err(TryError::WouldBlock) => Incoming::None, + Err(TryError::Other(error)) => { + warn!("ptp: general receive failed: {}", error); + Incoming::None + } + } + } + + pub(super) fn next_deadline(&self, deadline: Instant) -> Instant { + [ + self.timers.next_deadline(), + self.pending_tx.next_timeout_deadline(), + ] + .into_iter() + .flatten() + .fold(deadline, Ord::min) + } + + pub(super) async fn wait(&self, deadline: Instant) -> Option { + let stack = self.iface.stack(); + match with_deadline( + deadline, + select3( + stack.tx_timestamp(), + self.event.wait_recv_ready(), + self.general.wait_recv_ready(), + ), + ) + .await + { + Ok(Either3::First(timestamp)) => Some(timestamp), + _ => None, + } + } +} + +impl Drop for PortIo<'_> { + fn drop(&mut self) { + for (joined, address) in self + .joined + .into_iter() + .zip([PRIMARY_MULTICAST, LINK_LOCAL_MULTICAST]) + { + if joined { + let _ = self.iface.leave_multicast_group(address); + } + } + } +} + +pub(super) enum Incoming { + Event(udp::RecvPacket, Timestamp), + General(udp::RecvPacket), + 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)] +pub(super) 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)); + } + + pub(super) 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)] +pub(super) 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, +} + +pub(super) 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) -> bool { + if let Some(slot) = self.slots.iter_mut().find(|slot| slot.is_none()) { + *slot = Some(PendingTx { + context, + packet_id, + started: Instant::now(), + }); + true + } else { + warn!("ptp: tx timestamp queue full packet_id={=u32}", packet_id); + false + } + } + + fn sent(&mut self, packet_id: u32) { + if let Some(pending) = self + .slots + .iter_mut() + .flatten() + .find(|pending| pending.packet_id == packet_id) + { + pending.started = Instant::now(); + } + } + + pub(super) 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) + } + + pub(super) 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() + } +} + +pub(super) struct PacketIdGenerator(NonZero); + +impl PacketIdGenerator { + pub(super) 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 multicast_endpoint(port: u16, link_local: bool) -> SocketAddr { + let address = if link_local { + LINK_LOCAL_MULTICAST + } else { + PRIMARY_MULTICAST + }; + SocketAddr::new(address.into(), port) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::init_logging; + + #[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); + } + } +} From 559c161eb4eae6744493c291be9b8a0202259f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 8 Oct 2026 10:44:52 +0000 Subject: [PATCH 10/10] Give the H743 example an explicit DMA packet pool --- .../examples/stm32h743/.cargo/config.toml | 2 -- statime-embassy-net/examples/stm32h743/src/main.rs | 11 +++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/statime-embassy-net/examples/stm32h743/.cargo/config.toml b/statime-embassy-net/examples/stm32h743/.cargo/config.toml index bf9f76e82..1821f5c5b 100644 --- a/statime-embassy-net/examples/stm32h743/.cargo/config.toml +++ b/statime-embassy-net/examples/stm32h743/.cargo/config.toml @@ -12,6 +12,4 @@ rustflags = [ [env] DEFMT_LOG = "info" -# Four RX and four TX descriptors, eight queued PTP receives, four spare buffers. -XARXA_PACKET_BUF_COUNT = "20" XARXA_UDP_SOCKET_COUNT = "2" diff --git a/statime-embassy-net/examples/stm32h743/src/main.rs b/statime-embassy-net/examples/stm32h743/src/main.rs index 3de2b5717..8989dfbd4 100644 --- a/statime-embassy-net/examples/stm32h743/src/main.rs +++ b/statime-embassy-net/examples/stm32h743/src/main.rs @@ -5,7 +5,7 @@ use core::mem::MaybeUninit; use defmt::unwrap; use embassy_executor::Spawner; -use embassy_net::{Stack, StackStorage}; +use embassy_net::{Stack, StackStorage, StaticPool}; use embassy_stm32::{ bind_interrupts, eth::{Ethernet, GenericPhy, InterruptHandler, PacketQueue, PtpClockConfig, Sma}, @@ -40,6 +40,9 @@ static mut PACKETS: MaybeUninit> = MaybeUninit::uninit(); static DEVICE: StaticCell = StaticCell::new(); static STACK_STORAGE: StaticCell> = StaticCell::new(); +// Four RX and four TX descriptors, eight queued PTP receives, four spare buffers. +// Normal .bss is in DMA-accessible AXI SRAM; startup clears it. +static PACKET_POOL: StaticPool<1536, 20, 32> = StaticPool::new(); mod board { use embassy_stm32::{ @@ -128,7 +131,11 @@ async fn main(spawner: Spawner) -> ! { phy, ); let ptp_clock = EmbassyClock::new(device.start_ptp(PtpClockConfig::default())); - let (stack, runner) = Stack::new(STACK_STORAGE.init(StackStorage::new()), board::SEED); + let (stack, runner) = Stack::new( + STACK_STORAGE.init(StackStorage::new()), + &PACKET_POOL, + board::SEED, + ); let iface = unwrap!(stack.add_iface_borrowed(DEVICE.init(device))); unwrap!(iface.set_dhcpv4(Some(Default::default()))); let ptp_runner = PtpRunner::<_, FixedWanderKalmanFilter>::new(