J1900 <-> TwinCAT 1 kHz verification over EL6695 bridge
Re-implementation of rustbootd's ecat_el6695_* examples as a single binary
with fixes found in review and on hardware:
- DC-follow PLL hardened against period-2 hunting: slew-limited anchor
(+/-50us/cycle), bistable-trap snap re-anchor, re-prime on stale deadline
- drift-free absolute-grid ticker mode; probe mode for timestamp forensics
- bounded-memory online stats (histograms), graceful SIGINT/SIGTERM shutdown
with full report, error-streak abort
- timestamp plausibility filter comparing against the previous raw sample
(avoids the deadlock after a startup outlier)
- XFC scope waveform options: --el2202 (with --el2202-dual), --el2262,
--el1252 latch timestamp readback with per-channel edge statistics
- register access unified in regs.rs: named bit constants everywhere,
read-modify-write for enable/activation bytes
- vendored patched ethercrab 0.7.1 (sdo_write_complete, send_raw_coe)
Verified on J1900 (PREEMPT_RT 6.6.135): 600k cycles/600s exact 1 kHz,
tx/rx zero errors, phase_err p50=164us std=5us; EL2202<->EL1252 loopback
edge interval mean 2000.24us std=24.65us.
2026-07-22 08:55:41 +08:00
|
|
|
//! Bounded-memory online statistics: fixed histogram + Welford mean/variance.
|
|
|
|
|
//!
|
|
|
|
|
//! Unlike the original implementation (which pushed every sample into a
|
|
|
|
|
//! `Vec` and sorted at the end — ~30 MB/hour of RAM per metric at 1 kHz),
|
|
|
|
|
//! memory use here is constant, so multi-hour stability runs are safe.
|
|
|
|
|
|
|
|
|
|
pub struct OnlineStats {
|
|
|
|
|
name: &'static str,
|
|
|
|
|
/// Histogram bin width in ns.
|
|
|
|
|
bin_ns: i64,
|
|
|
|
|
/// Values below `low` or above `high` go to under/overflow counters but
|
|
|
|
|
/// still feed min/max/mean/variance.
|
|
|
|
|
low: i64,
|
|
|
|
|
high: i64,
|
|
|
|
|
bins: Vec<u64>,
|
|
|
|
|
under: u64,
|
|
|
|
|
over: u64,
|
|
|
|
|
n: u64,
|
|
|
|
|
min: i64,
|
|
|
|
|
max: i64,
|
|
|
|
|
mean: f64,
|
|
|
|
|
m2: f64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl OnlineStats {
|
|
|
|
|
/// Track values in `[low, high]` with `bin_ns` resolution (all in ns).
|
|
|
|
|
pub fn new(name: &'static str, bin_ns: i64, low: i64, high: i64) -> Self {
|
|
|
|
|
let n_bins = ((high - low) / bin_ns + 1).max(1) as usize;
|
|
|
|
|
Self {
|
|
|
|
|
name,
|
|
|
|
|
bin_ns,
|
|
|
|
|
low,
|
|
|
|
|
high,
|
|
|
|
|
bins: vec![0; n_bins],
|
|
|
|
|
under: 0,
|
|
|
|
|
over: 0,
|
|
|
|
|
n: 0,
|
|
|
|
|
min: i64::MAX,
|
|
|
|
|
max: i64::MIN,
|
|
|
|
|
mean: 0.0,
|
|
|
|
|
m2: 0.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn push(&mut self, v: i64) {
|
|
|
|
|
self.n += 1;
|
|
|
|
|
if v < self.min {
|
|
|
|
|
self.min = v;
|
|
|
|
|
}
|
|
|
|
|
if v > self.max {
|
|
|
|
|
self.max = v;
|
|
|
|
|
}
|
|
|
|
|
// Welford
|
|
|
|
|
let d = v as f64 - self.mean;
|
|
|
|
|
self.mean += d / self.n as f64;
|
|
|
|
|
self.m2 += d * (v as f64 - self.mean);
|
|
|
|
|
|
|
|
|
|
if v < self.low {
|
|
|
|
|
self.under += 1;
|
|
|
|
|
} else if v > self.high {
|
|
|
|
|
self.over += 1;
|
|
|
|
|
} else {
|
|
|
|
|
let idx = ((v - self.low) / self.bin_ns) as usize;
|
|
|
|
|
let last = self.bins.len() - 1;
|
|
|
|
|
self.bins[idx.min(last)] += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn len(&self) -> u64 {
|
|
|
|
|
self.n
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-20 13:03:16 +08:00
|
|
|
pub fn mean(&self) -> f64 {
|
|
|
|
|
self.mean
|
|
|
|
|
}
|
|
|
|
|
|
J1900 <-> TwinCAT 1 kHz verification over EL6695 bridge
Re-implementation of rustbootd's ecat_el6695_* examples as a single binary
with fixes found in review and on hardware:
- DC-follow PLL hardened against period-2 hunting: slew-limited anchor
(+/-50us/cycle), bistable-trap snap re-anchor, re-prime on stale deadline
- drift-free absolute-grid ticker mode; probe mode for timestamp forensics
- bounded-memory online stats (histograms), graceful SIGINT/SIGTERM shutdown
with full report, error-streak abort
- timestamp plausibility filter comparing against the previous raw sample
(avoids the deadlock after a startup outlier)
- XFC scope waveform options: --el2202 (with --el2202-dual), --el2262,
--el1252 latch timestamp readback with per-channel edge statistics
- register access unified in regs.rs: named bit constants everywhere,
read-modify-write for enable/activation bytes
- vendored patched ethercrab 0.7.1 (sdo_write_complete, send_raw_coe)
Verified on J1900 (PREEMPT_RT 6.6.135): 600k cycles/600s exact 1 kHz,
tx/rx zero errors, phase_err p50=164us std=5us; EL2202<->EL1252 loopback
edge interval mean 2000.24us std=24.65us.
2026-07-22 08:55:41 +08:00
|
|
|
fn percentile(&self, p: f64) -> i64 {
|
|
|
|
|
if self.n == 0 {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
let mut target = (self.n as f64 * p).ceil() as u64;
|
|
|
|
|
target = target.max(1);
|
|
|
|
|
// Mass below the histogram window counts first.
|
|
|
|
|
if target <= self.under {
|
|
|
|
|
return self.low;
|
|
|
|
|
}
|
|
|
|
|
target -= self.under;
|
|
|
|
|
let mut acc = 0u64;
|
|
|
|
|
for (i, &c) in self.bins.iter().enumerate() {
|
|
|
|
|
acc += c;
|
|
|
|
|
if acc >= target {
|
|
|
|
|
return self.low + (i as i64 + 1) * self.bin_ns;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.high // fell into overflow region
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn report(&self, unit_div: i64, unit: &str) {
|
|
|
|
|
if self.n == 0 {
|
|
|
|
|
println!("{}: no samples", self.name);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let std = (self.m2 / self.n as f64).sqrt();
|
|
|
|
|
let d = unit_div as f64;
|
|
|
|
|
println!(
|
|
|
|
|
"{}: n={} min={:.1} p50={:.1} p95={:.1} p99={:.1} p99.9={:.1} max={:.1} mean={:.2} std={:.2} ({}) under={} over={}",
|
|
|
|
|
self.name,
|
|
|
|
|
self.n,
|
|
|
|
|
self.min as f64 / d,
|
|
|
|
|
self.percentile(0.50) as f64 / d,
|
|
|
|
|
self.percentile(0.95) as f64 / d,
|
|
|
|
|
self.percentile(0.99) as f64 / d,
|
|
|
|
|
self.percentile(0.999) as f64 / d,
|
|
|
|
|
self.max as f64 / d,
|
|
|
|
|
self.mean / d,
|
|
|
|
|
std / d,
|
|
|
|
|
unit,
|
|
|
|
|
self.under,
|
|
|
|
|
self.over,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|