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
|
|
|
//! J1900 <-> TwinCAT 1 kHz communication verification over an EL6695 bridge.
|
|
|
|
|
//!
|
2026-07-23 00:46:48 +08:00
|
|
|
//! SECONDARY-SIDE build: J1900 talks to the EL6695 secondary port. The
|
|
|
|
|
//! secondary side SM layout (probed from EEPROM) uses SM0/TX (0x1000, 1024 B,
|
|
|
|
|
//! ctrl=0x26) for J1900->TwinCAT data and SM1/RX (0x1600, 1024 B, ctrl=0x22)
|
|
|
|
|
//! for TwinCAT->J1900 data. Unlike the primary side there is NO external
|
|
|
|
|
//! timestamp object (0x1A02) — the data is raw 1024 B payload with no prefix.
|
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
|
|
|
//!
|
2026-07-23 00:46:48 +08:00
|
|
|
//! Two modes:
|
|
|
|
|
//! - `ticker` (default): echo TwinCAT's payload back on SM0 with the local
|
|
|
|
|
//! cycle number stamped at bytes 8..16.
|
|
|
|
|
//! - `probe`: read-only; dump the first bytes of SM1 every N cycles and do
|
|
|
|
|
//! NOT write to SM0 (avoids disturbing TwinCAT's view).
|
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
|
|
|
//!
|
2026-07-23 00:46:48 +08:00
|
|
|
//! Graceful SIGINT/SIGTERM shutdown with full statistics.
|
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
|
|
|
|
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
use el6695_rt::{bridge, regs, rt, stats};
|
|
|
|
|
use ethercrab::{
|
|
|
|
|
std::{ethercat_now, tx_rx_task},
|
|
|
|
|
MainDevice, MainDeviceConfig, PduStorage, Timeouts,
|
|
|
|
|
};
|
|
|
|
|
use stats::OnlineStats;
|
|
|
|
|
|
|
|
|
|
const MAX_SUBDEVICES: usize = 16;
|
|
|
|
|
const MAX_PDU_DATA: usize = 1100;
|
|
|
|
|
const MAX_FRAMES: usize = 16;
|
|
|
|
|
const PDI_LEN: usize = 3072;
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
// Secondary side SM layout (ESI-defined process data SMs)
|
|
|
|
|
const SM_TX_ADDR: u16 = 0x1C00; // SM2: outputs (J1900 writes data to TwinCAT)
|
2026-08-06 01:10:50 +08:00
|
|
|
const SM_TX_LEN: usize = 800;
|
2026-07-23 00:46:48 +08:00
|
|
|
const SM_RX_ADDR: u16 = 0x8E00; // SM3: inputs (J1900 reads data from TwinCAT)
|
2026-07-27 19:01:05 +08:00
|
|
|
// Full PDO: 2B diagnostic word + 8B payload (single-entry twin_layout blob
|
|
|
|
|
// expands 0x6000:01 to 8 bytes).
|
2026-08-06 01:10:50 +08:00
|
|
|
const SM_RX_LEN: usize = 800; // SM3 buffer (no diag word, pure 8B payload)
|
|
|
|
|
const SM_RX_PLD_LEN: usize = 800; // actual payload after stripping diagnostic word
|
2026-07-27 19:01:05 +08:00
|
|
|
const RX_PREFIX: usize = 0; // no diag word prefix (0x1C13=[0x1A08] only)
|
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
|
|
|
|
2026-08-06 01:10:50 +08:00
|
|
|
const PAYLOAD_LEN: usize = 800;
|
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
|
|
|
|
|
|
|
|
/// Abort after this many consecutive cycles with a TX or RX error.
|
|
|
|
|
const MAX_ERR_STREAK: u64 = 1000;
|
|
|
|
|
|
|
|
|
|
static STOP: AtomicBool = AtomicBool::new(false);
|
|
|
|
|
static PDU_STORAGE: std::sync::OnceLock<PduStorage<MAX_FRAMES, MAX_PDU_DATA>> =
|
|
|
|
|
std::sync::OnceLock::new();
|
|
|
|
|
|
|
|
|
|
extern "C" fn on_signal(_sig: libc::c_int) {
|
|
|
|
|
STOP.store(true, Ordering::Relaxed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
|
|
|
enum Mode {
|
|
|
|
|
Ticker,
|
|
|
|
|
Probe,
|
2026-07-27 19:01:05 +08:00
|
|
|
Verify, // CoE audit: read 0xF008, layout hash, SM, identity
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
|
|
|
enum Role {
|
|
|
|
|
/// Full PDO config owner: writes 0x1608/0x1A08/0x1C12/0x1C13.
|
|
|
|
|
/// Used when J1900 is the first/only master to configure the bridge.
|
|
|
|
|
ConfigOwner,
|
|
|
|
|
/// Read-only consumer: never writes PDO mapping/assignment.
|
|
|
|
|
/// Adopts whatever layout the primary-side owner (TwinCAT) left.
|
|
|
|
|
Consumer,
|
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
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct Config {
|
|
|
|
|
iface: String,
|
|
|
|
|
mode: Mode,
|
2026-07-27 19:01:05 +08:00
|
|
|
role: Role,
|
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
|
|
|
duration_sec: u64,
|
|
|
|
|
cycle_us: u64,
|
|
|
|
|
core: usize,
|
|
|
|
|
prio: u8,
|
|
|
|
|
quiet: bool,
|
|
|
|
|
/// Toggle an EL2202 digital output every cycle (oscilloscope jitter
|
|
|
|
|
/// waveform). Off by default: the outputs may drive real actuators.
|
|
|
|
|
el2202: bool,
|
|
|
|
|
/// Toggle BOTH EL2202 channels (0x03/0x00): output 1 -> EL1252 readback,
|
|
|
|
|
/// output 2 -> oscilloscope, same edges in two views.
|
|
|
|
|
el2202_dual: bool,
|
|
|
|
|
/// Read EL1252 DC latch timestamps every cycle (edge interval/phase
|
|
|
|
|
/// statistics; wire the signal under test to EL1252 channel 1).
|
|
|
|
|
el1252: bool,
|
|
|
|
|
/// Drive an EL2262 XFC output with DC-sync-locked edges (reference
|
|
|
|
|
/// square wave on the DC 1 ms grid).
|
|
|
|
|
el2262: bool,
|
|
|
|
|
/// Read the EL1252 every N cycles (frame-budget throttle when combined
|
|
|
|
|
/// with other per-cycle frames; edges are latched so nothing is lost
|
|
|
|
|
/// beyond time resolution).
|
|
|
|
|
el1252_every: u32,
|
2026-07-28 00:11:34 +08:00
|
|
|
/// Spin-lock mode: instead of a free-running ticker, poll FPRD until the
|
|
|
|
|
/// payload changes (new TwinCAT frame), then process immediately.
|
|
|
|
|
/// Phase-locks J1900 processing to the primary master's frame arrival.
|
|
|
|
|
spin: bool,
|
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 parse_args() -> Result<Config, String> {
|
|
|
|
|
let mut cfg = Config {
|
|
|
|
|
iface: "eth1".into(),
|
2026-07-23 00:46:48 +08:00
|
|
|
mode: Mode::Ticker,
|
2026-07-27 19:01:05 +08:00
|
|
|
role: Role::ConfigOwner,
|
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
|
|
|
duration_sec: 60,
|
|
|
|
|
cycle_us: 1000,
|
|
|
|
|
core: 2,
|
|
|
|
|
prio: 80,
|
|
|
|
|
quiet: false,
|
|
|
|
|
el2202: false,
|
|
|
|
|
el2202_dual: false,
|
|
|
|
|
el1252: false,
|
|
|
|
|
el2262: false,
|
|
|
|
|
el1252_every: 1,
|
2026-07-28 00:11:34 +08:00
|
|
|
spin: false,
|
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
|
|
|
};
|
|
|
|
|
let mut args = std::env::args().skip(1).peekable();
|
|
|
|
|
if let Some(a) = args.peek() {
|
|
|
|
|
if !a.starts_with("--") {
|
|
|
|
|
cfg.iface = args.next().unwrap();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
while let Some(a) = args.next() {
|
|
|
|
|
let mut val = |name: &str| -> Result<String, String> {
|
|
|
|
|
args.next().ok_or_else(|| format!("missing value for {}", name))
|
|
|
|
|
};
|
|
|
|
|
match a.as_str() {
|
|
|
|
|
"--mode" => {
|
|
|
|
|
cfg.mode = match val("--mode")?.as_str() {
|
|
|
|
|
"ticker" => Mode::Ticker,
|
|
|
|
|
"probe" => Mode::Probe,
|
2026-07-27 19:01:05 +08:00
|
|
|
"verify" => Mode::Verify,
|
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
|
|
|
other => return Err(format!("unknown mode '{}'", other)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"--duration" => {
|
|
|
|
|
cfg.duration_sec = val("--duration")?
|
|
|
|
|
.parse()
|
|
|
|
|
.map_err(|_| "bad --duration")?
|
|
|
|
|
}
|
|
|
|
|
"--cycle-us" => cfg.cycle_us = val("--cycle-us")?.parse().map_err(|_| "bad --cycle-us")?,
|
|
|
|
|
"--core" => cfg.core = val("--core")?.parse().map_err(|_| "bad --core")?,
|
|
|
|
|
"--prio" => cfg.prio = val("--prio")?.parse().map_err(|_| "bad --prio")?,
|
|
|
|
|
"--quiet" => cfg.quiet = true,
|
2026-07-28 00:11:34 +08:00
|
|
|
"--spin" => cfg.spin = true,
|
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
|
|
|
"--el2202" => cfg.el2202 = true,
|
|
|
|
|
"--el2202-dual" => {
|
|
|
|
|
cfg.el2202 = true;
|
|
|
|
|
cfg.el2202_dual = true;
|
|
|
|
|
}
|
|
|
|
|
"--el1252" => cfg.el1252 = true,
|
|
|
|
|
"--el2262" => cfg.el2262 = true,
|
2026-07-27 19:01:05 +08:00
|
|
|
"--consumer" => cfg.role = Role::Consumer,
|
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
|
|
|
"--el1252-every" => {
|
|
|
|
|
cfg.el1252_every = val("--el1252-every")?.parse().map_err(|_| "bad --el1252-every")?
|
|
|
|
|
}
|
|
|
|
|
other => return Err(format!("unknown argument '{}'", other)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if cfg.cycle_us == 0 {
|
|
|
|
|
return Err("--cycle-us must be > 0".into());
|
|
|
|
|
}
|
|
|
|
|
Ok(cfg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn main() {
|
|
|
|
|
let cfg = match parse_args() {
|
|
|
|
|
Ok(c) => c,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("error: {}", e);
|
|
|
|
|
eprintln!(
|
2026-07-23 00:46:48 +08:00
|
|
|
"usage: el6695_rt [iface] [--mode ticker|probe] [--duration SEC] \
|
2026-07-27 19:01:05 +08:00
|
|
|
[--cycle-us N] [--core N] [--prio N] [--quiet] [--el2202] [--consumer]"
|
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
|
|
|
);
|
|
|
|
|
std::process::exit(2);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
unsafe {
|
|
|
|
|
let mut sa: libc::sigaction = core::mem::zeroed();
|
|
|
|
|
sa.sa_sigaction = on_signal as usize;
|
2026-07-23 00:46:48 +08:00
|
|
|
sa.sa_flags = 0;
|
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
|
|
|
libc::sigemptyset(&mut sa.sa_mask);
|
|
|
|
|
libc::sigaction(libc::SIGINT, &sa, core::ptr::null_mut());
|
|
|
|
|
libc::sigaction(libc::SIGTERM, &sa, core::ptr::null_mut());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let code = match futures_lite::future::block_on(run(&cfg)) {
|
|
|
|
|
Ok(()) => 0,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
eprintln!("Failed: {}", e);
|
|
|
|
|
1
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
std::process::exit(code);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn run(cfg: &Config) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
let cycle_ns = cfg.cycle_us * 1000;
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
println!("el6695-rt: J1900 <-> TwinCAT 1 kHz verification (EL6695 bridge, secondary side)");
|
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
|
|
|
println!(
|
2026-07-23 00:46:48 +08:00
|
|
|
" iface={} mode={} duration={}s cycle={}us core={} prio={}",
|
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
|
|
|
cfg.iface,
|
|
|
|
|
match cfg.mode {
|
|
|
|
|
Mode::Ticker => "ticker",
|
|
|
|
|
Mode::Probe => "probe",
|
2026-07-27 19:01:05 +08:00
|
|
|
Mode::Verify => "verify",
|
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
|
|
|
},
|
|
|
|
|
cfg.duration_sec,
|
|
|
|
|
cfg.cycle_us,
|
|
|
|
|
cfg.core,
|
|
|
|
|
cfg.prio
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let pdu_storage = PDU_STORAGE.get_or_init(PduStorage::new);
|
|
|
|
|
let (tx, rx, pdu_loop) = pdu_storage.try_split().map_err(|_| "PDU storage already in use")?;
|
|
|
|
|
|
|
|
|
|
let maindevice = MainDevice::new(
|
|
|
|
|
pdu_loop,
|
|
|
|
|
Timeouts {
|
|
|
|
|
state_transition: Duration::from_millis(10000),
|
|
|
|
|
mailbox_response: Duration::from_millis(2000),
|
|
|
|
|
eeprom: Duration::from_millis(500),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
MainDeviceConfig::default(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let iface = cfg.iface.clone();
|
|
|
|
|
let _txrx = std::thread::spawn(move || {
|
|
|
|
|
let task = tx_rx_task(&iface, tx, rx).expect("create tx_rx task");
|
|
|
|
|
if let Err(e) = futures_lite::future::block_on(task) {
|
|
|
|
|
eprintln!("tx_rx task ended with error: {:?}", e);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
println!("Scanning segment (INIT -> PREOP)...");
|
|
|
|
|
let group = maindevice
|
|
|
|
|
.init_single_group::<MAX_SUBDEVICES, PDI_LEN>(ethercat_now)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let mut el6695_index = None;
|
|
|
|
|
for (idx, subdevice) in group.iter(&maindevice).enumerate() {
|
|
|
|
|
if subdevice.name().contains("EL6695") {
|
|
|
|
|
el6695_index = Some(idx);
|
|
|
|
|
println!("[{}] Found EL6695", idx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let el6695_index = el6695_index.ok_or("EL6695 not found on bus")?;
|
|
|
|
|
let sd = group.subdevice(&maindevice, el6695_index)?;
|
|
|
|
|
let station = sd.configured_address();
|
|
|
|
|
println!("EL6695 station address: 0x{:04X}", station);
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
// ---- Probe SM registers ----
|
|
|
|
|
async fn read_sm(md: &MainDevice<'_>, st: u16, n: u8) -> (u16, u16, u8, u8) {
|
|
|
|
|
let start = ethercrab::Command::fprd(st, regs::sm_base(n as u16) + regs::SM_OFF_START).receive::<u16>(md).await.unwrap_or(0xFFFF);
|
|
|
|
|
let len = ethercrab::Command::fprd(st, regs::sm_base(n as u16) + regs::SM_OFF_LEN).receive::<u16>(md).await.unwrap_or(0xFFFF);
|
|
|
|
|
let ctrl = ethercrab::Command::fprd(st, regs::sm_base(n as u16) + regs::SM_OFF_CTRL).receive::<u8>(md).await.unwrap_or(0xFF);
|
|
|
|
|
let ena = ethercrab::Command::fprd(st, regs::sm_base(n as u16) + regs::SM_OFF_ENABLE).receive::<u8>(md).await.unwrap_or(0xFF);
|
|
|
|
|
(start, len, ctrl, ena)
|
|
|
|
|
}
|
|
|
|
|
for n in [0u8, 1, 2, 3] {
|
|
|
|
|
let (s, l, c, e) = read_sm(&maindevice, station, n).await;
|
|
|
|
|
println!(" SM{}: start=0x{:04X} len={} ctrl=0x{:02X} enable=0x{:02X}", n, s, l, c, e);
|
|
|
|
|
}
|
|
|
|
|
// Probe FMMU registers
|
|
|
|
|
for n in 0u16..4 {
|
|
|
|
|
let reg = 0x0600 + n * 16;
|
|
|
|
|
match ethercrab::Command::fprd(station, reg)
|
|
|
|
|
.receive_slice(&maindevice, 16)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(pdu) => {
|
|
|
|
|
let raw: &[u8] = &pdu;
|
|
|
|
|
let logical_start = u32::from_le_bytes(raw[0..4].try_into().unwrap());
|
|
|
|
|
let length = u16::from_le_bytes(raw[4..6].try_into().unwrap());
|
|
|
|
|
let phy_start = u16::from_le_bytes(raw[8..10].try_into().unwrap());
|
|
|
|
|
println!(" FMMU{}: logical_start=0x{:08X} len={} physical=0x{:04X} enable=0x{:02X}", n, logical_start, length, phy_start, raw[12]);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => println!(" FMMU{} read error: {:?}", n, e),
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
// ---- Configure SM2/SM3 for process data (SM0/SM1 are EEPROM-fixed mailbox) ----
|
|
|
|
|
// 0x1C12/0x1C13 exist (empty), 0x1C10/0x1C11 do not.
|
|
|
|
|
// SM2/SM3 registers are writable because they are not defined in EEPROM.
|
|
|
|
|
println!("\nTesting SM0/SM1 data access...");
|
|
|
|
|
// Read SM1 (buffered input, 0x1600)
|
|
|
|
|
match ethercrab::Command::fprd(station, 0x1600)
|
|
|
|
|
.receive_slice(&maindevice, 32)
|
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
|
|
|
.await
|
2026-07-23 00:46:48 +08:00
|
|
|
{
|
|
|
|
|
Ok(pdu) => {
|
|
|
|
|
let raw: &[u8] = &pdu;
|
|
|
|
|
println!(" SM1 FPRD(0x1600,32) OK: {:02X?}", &raw[..32.min(raw.len())]);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => println!(" SM1 FPRD error: {:?}", e),
|
|
|
|
|
}
|
|
|
|
|
// Write SM0 (mailbox, 0x1000)
|
|
|
|
|
match ethercrab::Command::fpwr(station, 0x1000)
|
|
|
|
|
.send(&maindevice, &[0xBBu8; 16])
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(_) => println!(" SM0 FPWR(0x1000,16) OK"),
|
|
|
|
|
Err(e) => println!(" SM0 FPWR error: {:?}", e),
|
|
|
|
|
}
|
|
|
|
|
// Read back SM0
|
|
|
|
|
match ethercrab::Command::fprd(station, 0x1000)
|
|
|
|
|
.receive_slice(&maindevice, 16)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(pdu) => {
|
|
|
|
|
let raw: &[u8] = &pdu;
|
|
|
|
|
println!(" SM0 FPRD(0x1000,16) back: {:02X?}", &raw[..16.min(raw.len())]);
|
|
|
|
|
}
|
|
|
|
|
Err(e) => println!(" SM0 FPRD back error: {:?}", e),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove SM2/SM3 config (was causing SAFEOP 0x001D)
|
|
|
|
|
// Safety: clear SM2/SM3 in case previous runs left stale values
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x00u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x00u8).await;
|
|
|
|
|
|
2026-07-27 19:01:05 +08:00
|
|
|
// ---- Verify mode: CoE audit (then exit, no SAFEOP) ----
|
|
|
|
|
if cfg.mode == Mode::Verify {
|
|
|
|
|
println!("\n=== CoE Audit ===");
|
|
|
|
|
// 0xF008: NoCoeStorage / vendor reset
|
|
|
|
|
let f008_0 = sd.sdo_read::<u32>(0xF008, 0).await;
|
|
|
|
|
let f008_1 = sd.sdo_read::<u8>(0xF008, 1).await;
|
|
|
|
|
println!(" 0xF008:00 = {:?} sub1={:?}", f008_0, f008_1);
|
|
|
|
|
let no_coe_storage = f008_0.unwrap_or(0) == 0x12345678;
|
|
|
|
|
println!(" NoCoeStorage: {}", if no_coe_storage { "⚠ ENABLED (0x12345678) — config will NOT persist to EEPROM" } else { "✓ disabled" });
|
|
|
|
|
|
|
|
|
|
// 0x1018: Identity
|
|
|
|
|
for sub in 1..=4u8 {
|
|
|
|
|
let v = sd.sdo_read::<u32>(0x1018, sub).await;
|
|
|
|
|
let label = match sub { 1 => "Vendor", 2 => "Product", 3 => "Revision", _ => "Serial" };
|
|
|
|
|
println!(" 0x1018:{:02} ({}) = 0x{:08X}", sub, label, v.unwrap_or(0xFFFFFFFF));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Full PDO layout
|
|
|
|
|
let snap = bridge::snapshot_layout(&sd, &maindevice, station).await;
|
|
|
|
|
let h = bridge::compute_layout_hash(&snap);
|
|
|
|
|
println!(" 0x1C12 assign: {:?}", snap.tx_assign);
|
|
|
|
|
println!(" 0x1C13 assign: {:?}", snap.rx_assign);
|
|
|
|
|
println!(" 0x1608 entries: {} (total {} bits)", snap.tx_entries.len(), snap.tx_total_bits);
|
|
|
|
|
if !snap.tx_entries.is_empty() {
|
|
|
|
|
for (i, (idx, sub, bl)) in snap.tx_entries.iter().enumerate().take(3) {
|
|
|
|
|
println!(" [{:02}] 0x{:04X}:{:02} @ {} bits", i+1, idx, sub, bl);
|
|
|
|
|
}
|
|
|
|
|
if snap.tx_entries.len() > 3 {
|
|
|
|
|
println!(" ... ({} more)", snap.tx_entries.len() - 3);
|
2026-07-23 00:46:48 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-27 19:01:05 +08:00
|
|
|
println!(" 0x1A08 entries: {} (total {} bits)", snap.rx_entries.len(), snap.rx_total_bits);
|
|
|
|
|
if !snap.rx_entries.is_empty() {
|
|
|
|
|
for (i, (idx, sub, bl)) in snap.rx_entries.iter().enumerate().take(3) {
|
|
|
|
|
println!(" [{:02}] 0x{:04X}:{:02} @ {} bits", i+1, idx, sub, bl);
|
|
|
|
|
}
|
|
|
|
|
if snap.rx_entries.len() > 3 {
|
|
|
|
|
println!(" ... ({} more)", snap.rx_entries.len() - 3);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
println!(" SM2 len={} SM3 len={}", snap.sm2_len, snap.sm3_len);
|
|
|
|
|
println!(" Layout CRC32 = 0x{:08X}", h);
|
|
|
|
|
println!(" Expected hash = 0x{:08X}", bridge::EXPECTED_LAYOUT_HASH.load(std::sync::atomic::Ordering::Relaxed));
|
|
|
|
|
println!(" 0x6000[0] = {}", sd.sdo_read::<u8>(0x6000, 0).await.unwrap_or(0xFF));
|
|
|
|
|
println!(" 0x7000[0] = {}", sd.sdo_read::<u8>(0x7000, 0).await.unwrap_or(0xFF));
|
|
|
|
|
|
|
|
|
|
// AL state / error
|
|
|
|
|
let (al, _) = bridge::drive_state(&maindevice, station, 0x0002).await;
|
|
|
|
|
println!(" AL=0x{:04X} err={}", al, al & regs::AL_ERROR != 0);
|
|
|
|
|
println!("=== CoE Audit complete ===");
|
|
|
|
|
return Ok(());
|
2026-07-23 00:46:48 +08:00
|
|
|
}
|
|
|
|
|
|
2026-07-27 19:01:05 +08:00
|
|
|
// ---- ensure_link: adopt-if-valid (Consumer) or full reconfigure (ConfigOwner) ----
|
|
|
|
|
println!("\nensure_link: role={:?} ...", cfg.role);
|
2026-07-23 00:46:48 +08:00
|
|
|
|
2026-07-27 19:01:05 +08:00
|
|
|
// Read current PDO layout from the bridge
|
|
|
|
|
let layout_ok = bridge::layout_matches(&sd, &maindevice, station).await.unwrap_or(false);
|
|
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, station, 0x0002).await;
|
|
|
|
|
let has_err = al & regs::AL_ERROR != 0;
|
|
|
|
|
let al_state = al & 0x0F;
|
|
|
|
|
println!(" AL=0x{:04X} err={} layout_ok={}", al, has_err, layout_ok);
|
2026-07-23 00:46:48 +08:00
|
|
|
|
2026-07-27 19:01:05 +08:00
|
|
|
match cfg.role {
|
|
|
|
|
Role::Consumer => {
|
|
|
|
|
// ---- Consumer: idempotent writes, no INIT→PREOP cycle ----
|
|
|
|
|
// SM/PDO config must ALWAYS be written after scan (scanning wipes
|
|
|
|
|
// SM registers and PDO assign/map). The writes are idempotent:
|
|
|
|
|
// same layout as the expected expanded 1024-byte config.
|
|
|
|
|
// Clear any PREOP+ERR (L1 recovery)
|
|
|
|
|
if has_err {
|
|
|
|
|
println!(" Consumer: clearing PREOP+ERR ...");
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::AL_CONTROL)
|
|
|
|
|
.send(&maindevice, 0x0002u16).await;
|
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
|
|
|
|
}
|
|
|
|
|
// Skip INIT→PREOP — preserves TwinCAT's persisted CoE objects.
|
|
|
|
|
println!(" Consumer: idempotent SM/FMMU/PDO config");
|
|
|
|
|
// SM2/SM3 registers
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x00u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_START)
|
|
|
|
|
.send(&maindevice, 0x1C00u16).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_LEN)
|
|
|
|
|
.send(&maindevice, (PAYLOAD_LEN as u16)).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_CTRL)
|
|
|
|
|
.send(&maindevice, 0x64u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x01u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x00u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_START)
|
|
|
|
|
.send(&maindevice, 0x8E00u16).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_LEN)
|
|
|
|
|
.send(&maindevice, (PAYLOAD_LEN as u16)).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_CTRL)
|
|
|
|
|
.send(&maindevice, 0x20u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x01u8).await;
|
|
|
|
|
bridge::write_fmmus_secondary(&maindevice, station, PAYLOAD_LEN as u16, PAYLOAD_LEN as u16).await;
|
|
|
|
|
// PDO maps and assigns (whitelisted, idempotent)
|
|
|
|
|
println!(" Consumer: write PDO mappings...");
|
|
|
|
|
let blob = bridge::twin_layout_blob_sized(0x7000, PAYLOAD_LEN);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1608, 0, &blob).await;
|
|
|
|
|
let blob = bridge::twin_layout_blob_sized(0x6000, PAYLOAD_LEN);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1A08, 0, &blob).await;
|
|
|
|
|
println!(" Consumer: write PDO assignments...");
|
|
|
|
|
let blob = bridge::assign_blob(&[0x1608]);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1C12, 0, &blob).await;
|
|
|
|
|
let blob = bridge::assign_blob(&[0x1A08]);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1C13, 0, &blob).await;
|
|
|
|
|
let _o7 = sd.sdo_read::<u8>(0x7000, 0).await.unwrap_or(0xFF);
|
|
|
|
|
let _o6 = sd.sdo_read::<u8>(0x6000, 0).await.unwrap_or(0xFF);
|
|
|
|
|
let snap = bridge::snapshot_layout(&sd, &maindevice, station).await;
|
|
|
|
|
let h = bridge::compute_layout_hash(&snap);
|
|
|
|
|
println!(" Consumer: dyn 0x6000[0]={} 0x7000[0]={} hash=0x{:08X}", _o6, _o7, h);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Role::ConfigOwner => {
|
|
|
|
|
// ---- ConfigOwner: full INIT→PREOP + PDO rebuild ----
|
|
|
|
|
// Clear any PREOP+ERR by re-requesting PREOP
|
|
|
|
|
if has_err {
|
|
|
|
|
println!(" ConfigOwner: clearing PREOP+ERR ...");
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::AL_CONTROL)
|
|
|
|
|
.send(&maindevice, 0x0002u16).await;
|
|
|
|
|
std::thread::sleep(Duration::from_millis(300));
|
|
|
|
|
}
|
|
|
|
|
// INIT→PREOP cycle to reset CoE state
|
|
|
|
|
println!(" INIT→PREOP cycle...");
|
|
|
|
|
let (al_i, _) = bridge::drive_state(&maindevice, station, 0x0001).await;
|
|
|
|
|
println!(" INIT: {}", bridge::verdict(al_i, 0));
|
|
|
|
|
if al_i & 0x0F != 0x01 {
|
|
|
|
|
return Err(format!("INIT failed: {}", bridge::verdict(al_i, 0)).into());
|
|
|
|
|
}
|
|
|
|
|
let (al_p, alc_p) = bridge::drive_state(&maindevice, station, 0x0002).await;
|
|
|
|
|
println!(" PREOP: {}", bridge::verdict(al_p, alc_p));
|
|
|
|
|
if al_p & 0x0F != 0x02 {
|
|
|
|
|
return Err(format!("PREOP failed: {}", bridge::verdict(al_p, alc_p)).into());
|
|
|
|
|
}
|
|
|
|
|
// Write PDO mappings (0x1608 TxPDO, 0x1A08 RxPDO)
|
|
|
|
|
// Whitelist-checked: only {0x1608,0x1A08,0x1C12,0x1C13,0xF008,0xF800} allowed.
|
|
|
|
|
println!(" Write PDO mappings...");
|
|
|
|
|
let blob = bridge::twin_layout_blob_sized(0x7000, PAYLOAD_LEN);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1608, 0, &blob).await;
|
|
|
|
|
let blob = bridge::twin_layout_blob_sized(0x6000, PAYLOAD_LEN);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1A08, 0, &blob).await;
|
|
|
|
|
// Write PDO assignments (triggers 0x6000/0x7000 object creation)
|
|
|
|
|
println!(" Write PDO assignments...");
|
|
|
|
|
let blob = bridge::assign_blob(&[0x1608]);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1C12, 0, &blob).await;
|
|
|
|
|
let blob = bridge::assign_blob(&[0x1A08]);
|
|
|
|
|
let _ = bridge::sdo_write_ca_checked(&sd, 0x1C13, 0, &blob).await;
|
|
|
|
|
// Verify the dynamic objects were instantiated
|
|
|
|
|
let _o7 = sd.sdo_read::<u8>(0x7000, 0).await.unwrap_or(0xFF);
|
|
|
|
|
let _o6 = sd.sdo_read::<u8>(0x6000, 0).await.unwrap_or(0xFF);
|
|
|
|
|
// Compute layout hash after successful config
|
|
|
|
|
let snap = bridge::snapshot_layout(&sd, &maindevice, station).await;
|
|
|
|
|
let h = bridge::compute_layout_hash(&snap);
|
|
|
|
|
println!(" dyn: 0x6000[0]={} 0x7000[0]={} layout_hash=0x{:08X}", _o6, _o7, h);
|
|
|
|
|
// Configure SM2/SM3 + FMMU
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x00u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_START)
|
|
|
|
|
.send(&maindevice, 0x1C00u16).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_LEN)
|
|
|
|
|
.send(&maindevice, (PAYLOAD_LEN as u16)).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_CTRL)
|
|
|
|
|
.send(&maindevice, 0x64u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(2) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x01u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x00u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_START)
|
|
|
|
|
.send(&maindevice, 0x8E00u16).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_LEN)
|
|
|
|
|
.send(&maindevice, (PAYLOAD_LEN as u16)).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_CTRL)
|
|
|
|
|
.send(&maindevice, 0x20u8).await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(station, regs::sm_base(3) + regs::SM_OFF_ENABLE)
|
|
|
|
|
.send(&maindevice, 0x01u8).await;
|
|
|
|
|
bridge::write_fmmus_secondary(&maindevice, station, PAYLOAD_LEN as u16, PAYLOAD_LEN as u16).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-23 00:46:48 +08:00
|
|
|
|
|
|
|
|
// SAFEOP
|
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
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, station, 0x0004).await;
|
2026-07-23 00:46:48 +08:00
|
|
|
println!(" SAFEOP: {}", bridge::verdict(al, alc));
|
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
|
|
|
if al & 0x0F != 0x04 || al & 0x10 != 0 {
|
2026-07-23 00:46:48 +08:00
|
|
|
return Err(format!("Cannot reach SAFEOP: {}", bridge::verdict(al, alc)).into());
|
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
|
|
|
}
|
2026-07-23 00:46:48 +08:00
|
|
|
println!(" >>> SAFEOP achieved! <<<");
|
|
|
|
|
let (al2, alc2) = bridge::drive_state(&maindevice, station, 0x0008).await;
|
|
|
|
|
println!(" OP: {}", bridge::verdict(al2, alc2));
|
|
|
|
|
if al2 & 0x0F != 0x08 || al2 & 0x10 != 0 {
|
|
|
|
|
return Err(format!("OP rejected: {}", bridge::verdict(al2, alc2)).into());
|
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
|
|
|
}
|
2026-07-23 00:46:48 +08:00
|
|
|
println!(" >>> OP achieved! <<<");
|
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
|
|
|
|
|
|
|
|
// ---- Optional EL2202 oscilloscope waveform (toggle one DO per cycle) ----
|
|
|
|
|
const EL2202_OUT_ADDR: u16 = 0x0F00;
|
2026-07-23 00:46:48 +08:00
|
|
|
let mut el2202: Option<(u16, u16)> = None;
|
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
|
|
|
if cfg.el2202 {
|
|
|
|
|
let mut found = false;
|
|
|
|
|
for (idx, subdevice) in group.iter(&maindevice).enumerate() {
|
|
|
|
|
if subdevice.name().contains("EL2202") {
|
|
|
|
|
found = true;
|
|
|
|
|
let sd2 = group.subdevice(&maindevice, idx)?;
|
|
|
|
|
let st2 = sd2.configured_address();
|
|
|
|
|
println!("[{}] Found EL2202, station 0x{:04X}", idx, st2);
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st2, regs::sm_base(0) + regs::SM_OFF_START)
|
|
|
|
|
.send(&maindevice, EL2202_OUT_ADDR)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st2, regs::sm_base(0) + regs::SM_OFF_LEN)
|
|
|
|
|
.send(&maindevice, 1u16)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st2, regs::sm_base(0) + regs::SM_OFF_CTRL)
|
|
|
|
|
.send(&maindevice, regs::SM_CTRL_EL2202_OUTPUTS)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = regs::rmw_u8(
|
|
|
|
|
&maindevice,
|
|
|
|
|
st2,
|
|
|
|
|
regs::sm_base(0) + regs::SM_OFF_ENABLE,
|
|
|
|
|
regs::SM_ENABLE | regs::SM_ENABLE_FAMILY,
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, st2, regs::AL_SAFEOP).await;
|
|
|
|
|
println!("EL2202 SAFEOP: {}", bridge::verdict(al, alc));
|
|
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, st2, regs::AL_OP).await;
|
|
|
|
|
println!("EL2202 OP: {}", bridge::verdict(al, alc));
|
|
|
|
|
if al & regs::AL_STATE_MASK != regs::AL_OP || al & regs::AL_ERROR != 0 {
|
|
|
|
|
return Err(format!("EL2202 OP rejected: {}", bridge::verdict(al, alc)).into());
|
|
|
|
|
}
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st2, EL2202_OUT_ADDR)
|
|
|
|
|
.send(&maindevice, regs::EL2202_CH1_DATA)
|
|
|
|
|
.await;
|
|
|
|
|
let rb = ethercrab::Command::fprd(st2, EL2202_OUT_ADDR)
|
|
|
|
|
.receive::<u8>(&maindevice)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap_or(0xFF);
|
|
|
|
|
if rb & regs::EL2202_CH1_DATA == regs::EL2202_CH1_DATA {
|
|
|
|
|
println!(
|
|
|
|
|
"EL2202 scope output: verified, toggle 1 byte @0x{:04X} every cycle",
|
|
|
|
|
EL2202_OUT_ADDR
|
|
|
|
|
);
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st2, EL2202_OUT_ADDR)
|
|
|
|
|
.send(&maindevice, 0x00u8)
|
|
|
|
|
.await;
|
|
|
|
|
el2202 = Some((st2, EL2202_OUT_ADDR));
|
|
|
|
|
} else {
|
2026-07-23 00:46:48 +08:00
|
|
|
println!("EL2202 output readback failed (0x{:02X}); scope output disabled", rb);
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
println!("--el2202 given but no EL2202 on bus; scope output disabled");
|
|
|
|
|
}
|
|
|
|
|
if found && el2202.is_none() {
|
|
|
|
|
println!("EL2202 present but output setup failed; scope output disabled");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Optional EL2262 DC-locked reference output ----
|
|
|
|
|
const EL2262_OUT_ADDR: u16 = 0x1000;
|
2026-07-23 00:46:48 +08:00
|
|
|
let mut el2262: Option<u16> = None;
|
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
|
|
|
if cfg.el2262 {
|
|
|
|
|
let mut found = false;
|
|
|
|
|
for (idx, subdevice) in group.iter(&maindevice).enumerate() {
|
|
|
|
|
if subdevice.name().contains("EL2262") {
|
|
|
|
|
found = true;
|
|
|
|
|
let sd3 = group.subdevice(&maindevice, idx)?;
|
|
|
|
|
let st3 = sd3.configured_address();
|
|
|
|
|
println!("[{}] Found EL2262, station 0x{:04X}", idx, st3);
|
|
|
|
|
let t = ethercrab::Command::fprd(st3, regs::DC_SYSTEM_TIME)
|
|
|
|
|
.receive::<u64>(&maindevice)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap_or(0);
|
|
|
|
|
let start = t.wrapping_add(100_000_000);
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st3, regs::DC_SYNC_START_TIME)
|
|
|
|
|
.send(&maindevice, start)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st3, regs::DC_SYNC0_CYCLE)
|
|
|
|
|
.send(&maindevice, cycle_ns as u32)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st3, regs::DC_SYNC1_CYCLE)
|
|
|
|
|
.send(&maindevice, cycle_ns as u32)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = regs::rmw_u8(
|
|
|
|
|
&maindevice,
|
|
|
|
|
st3,
|
|
|
|
|
regs::DC_SYNC_ACTIVATION,
|
|
|
|
|
regs::DC_SYNC0_ACTIVATE | regs::DC_SYNC1_ACTIVATE,
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
2026-07-23 00:46:48 +08:00
|
|
|
println!("EL2262 DC sync: sync0/1 cycle={}ns start=+100ms (t={})", cycle_ns, t);
|
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
|
|
|
let _ = ethercrab::Command::fpwr(st3, regs::sm_base(0) + regs::SM_OFF_START)
|
|
|
|
|
.send(&maindevice, EL2262_OUT_ADDR)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st3, regs::sm_base(0) + regs::SM_OFF_LEN)
|
|
|
|
|
.send(&maindevice, 1u16)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st3, regs::sm_base(0) + regs::SM_OFF_CTRL)
|
|
|
|
|
.send(&maindevice, regs::SM_CTRL_OUTPUTS_ESI)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = regs::rmw_u8(
|
|
|
|
|
&maindevice,
|
|
|
|
|
st3,
|
|
|
|
|
regs::sm_base(0) + regs::SM_OFF_ENABLE,
|
|
|
|
|
regs::SM_ENABLE | regs::SM_ENABLE_FAMILY,
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, st3, regs::AL_SAFEOP).await;
|
|
|
|
|
println!("EL2262 SAFEOP: {}", bridge::verdict(al, alc));
|
|
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, st3, regs::AL_OP).await;
|
|
|
|
|
println!("EL2262 OP: {}", bridge::verdict(al, alc));
|
|
|
|
|
if al & regs::AL_STATE_MASK != regs::AL_OP || al & regs::AL_ERROR != 0 {
|
|
|
|
|
return Err(format!("EL2262 OP rejected: {}", bridge::verdict(al, alc)).into());
|
|
|
|
|
}
|
|
|
|
|
el2262 = Some(st3);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
println!("--el2262 given but no EL2262 on bus; reference output disabled");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Optional EL1252 DC latch timestamp readback ----
|
2026-07-23 00:46:48 +08:00
|
|
|
let mut el1252: Option<u16> = None;
|
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
|
|
|
if cfg.el1252 {
|
|
|
|
|
let mut found = false;
|
|
|
|
|
for (idx, subdevice) in group.iter(&maindevice).enumerate() {
|
|
|
|
|
if subdevice.name().contains("EL1252") {
|
|
|
|
|
found = true;
|
|
|
|
|
let sd4 = group.subdevice(&maindevice, idx)?;
|
|
|
|
|
let st4 = sd4.configured_address();
|
|
|
|
|
println!("[{}] Found EL1252, station 0x{:04X} (latch readback)", idx, st4);
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st4, regs::sm_base(0) + regs::SM_OFF_START)
|
|
|
|
|
.send(&maindevice, 0x1000u16)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st4, regs::sm_base(0) + regs::SM_OFF_LEN)
|
|
|
|
|
.send(&maindevice, 1u16)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = ethercrab::Command::fpwr(st4, regs::sm_base(0) + regs::SM_OFF_CTRL)
|
|
|
|
|
.send(&maindevice, regs::SM_CTRL_EL1252_INPUTS)
|
|
|
|
|
.await;
|
|
|
|
|
let _ = regs::rmw_u8(
|
|
|
|
|
&maindevice,
|
|
|
|
|
st4,
|
|
|
|
|
regs::sm_base(0) + regs::SM_OFF_ENABLE,
|
|
|
|
|
regs::SM_ENABLE,
|
|
|
|
|
0,
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, st4, regs::AL_SAFEOP).await;
|
|
|
|
|
println!("EL1252 SAFEOP: {}", bridge::verdict(al, alc));
|
|
|
|
|
let (al, alc) = bridge::drive_state(&maindevice, st4, regs::AL_OP).await;
|
|
|
|
|
println!("EL1252 OP: {}", bridge::verdict(al, alc));
|
|
|
|
|
if al & regs::AL_STATE_MASK != regs::AL_OP || al & regs::AL_ERROR != 0 {
|
|
|
|
|
return Err(format!("EL1252 OP rejected: {}", bridge::verdict(al, alc)).into());
|
|
|
|
|
}
|
|
|
|
|
el1252 = Some(st4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !found {
|
|
|
|
|
println!("--el1252 given but no EL1252 on bus; latch readback disabled");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- Pre-allocate everything, then promote to RT ----
|
2026-07-23 00:46:48 +08:00
|
|
|
let mut tx_buf = vec![0u8; SM_TX_LEN];
|
|
|
|
|
let mut rx_buf = vec![0u8; SM_RX_LEN];
|
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
|
|
|
let mut jitter_st = OnlineStats::new("period_jitter", 100, -5_000_000, 5_000_000);
|
|
|
|
|
let mut exec_st = OnlineStats::new("exec_time", 1_000, 0, 20_000_000);
|
|
|
|
|
let mut rx_rtt_st = OnlineStats::new("fprd_rtt", 1_000, 0, 20_000_000);
|
|
|
|
|
let mut tx_rtt_st = OnlineStats::new("fpwr_rtt", 1_000, 0, 20_000_000);
|
|
|
|
|
|
|
|
|
|
println!("Promoting to RT: core={}, fifo={}, mlock=true", cfg.core, cfg.prio);
|
|
|
|
|
if let Err(e) = rt::promote_current_thread(cfg.core, cfg.prio, true) {
|
|
|
|
|
eprintln!("RT promotion failed (needs root, continuing anyway): {}", e);
|
|
|
|
|
}
|
|
|
|
|
rt::prefault_stack(256 * 1024);
|
|
|
|
|
|
|
|
|
|
// ---- Real-time data loop ----
|
|
|
|
|
println!(
|
|
|
|
|
"\n>>> running: {} loop, echo payload, stats (Ctrl-C for graceful stop)",
|
2026-07-23 00:46:48 +08:00
|
|
|
match cfg.mode {
|
|
|
|
|
Mode::Ticker => "ticker",
|
|
|
|
|
Mode::Probe => "probe (read-only, no echo)",
|
2026-07-27 19:01:05 +08:00
|
|
|
Mode::Verify => "verify (CoE audit, no data loop)",
|
2026-07-23 00:46:48 +08:00
|
|
|
}
|
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
|
|
|
);
|
|
|
|
|
let start = Instant::now();
|
|
|
|
|
let mut ticker = rt::Ticker::new(cycle_ns);
|
|
|
|
|
|
|
|
|
|
let mut cyc: u64 = 0;
|
|
|
|
|
let mut tx_err: u64 = 0;
|
|
|
|
|
let mut rx_err: u64 = 0;
|
|
|
|
|
let mut err_streak: u64 = 0;
|
|
|
|
|
let mut no_tc_seq: u64 = 0;
|
|
|
|
|
let mut el2202_err: u64 = 0;
|
|
|
|
|
let mut el2262_err: u64 = 0;
|
|
|
|
|
let mut el1252_err: u64 = 0;
|
|
|
|
|
let mut latch_pos0: u64 = 0;
|
|
|
|
|
let mut latch_neg0: u64 = 0;
|
|
|
|
|
let mut latch_pos1: u64 = 0;
|
|
|
|
|
let mut latch_neg1: u64 = 0;
|
|
|
|
|
let mut pos_edges: u64 = 0;
|
|
|
|
|
let mut neg_edges: u64 = 0;
|
|
|
|
|
let mut pos1_edges: u64 = 0;
|
|
|
|
|
let mut el1252_st: [u8; 2] = [0, 0];
|
|
|
|
|
let mut el1252_in: u8 = 0;
|
|
|
|
|
let mut latch_interval_st = OnlineStats::new("el1252_ch1_edge_interval", 1_000, 0, 20_000_000);
|
|
|
|
|
let mut latch_phase_st = OnlineStats::new("el1252_ch1_edge_phase", 100, 0, 1_000_000);
|
|
|
|
|
let mut latch1_interval_st = OnlineStats::new("el1252_ch2_edge_interval", 1_000, 0, 20_000_000);
|
|
|
|
|
let mut latch1_phase_st = OnlineStats::new("el1252_ch2_edge_phase", 100, 0, 1_000_000);
|
|
|
|
|
let mut last_report = Instant::now();
|
2026-07-27 19:01:05 +08:00
|
|
|
// J1900 data verification counters
|
|
|
|
|
let mut prev_tc_seq: u64 = 0;
|
|
|
|
|
let mut rx_stale: u64 = 0; // tc_seq unchanged (stale read)
|
|
|
|
|
let mut rx_nonmono: u64 = 0; // tc_seq decreased (non-monotonic)
|
|
|
|
|
let mut comp_err: u64 = 0; // internal computation errors
|
|
|
|
|
let mut payload_checked: u64 = 0; // J1900 payload verification cycles
|
|
|
|
|
let mut payload_mismatch: u64 = 0; // J1900 payload mismatch bytes accumulated
|
2026-07-28 00:11:34 +08:00
|
|
|
let mut prev_rx8 = [0u8; 8]; // spin-lock: previous payload (change detect)
|
|
|
|
|
let mut last_fresh: Option<Instant> = None; // spin-lock: last fresh-data instant
|
2026-07-27 19:01:05 +08:00
|
|
|
|
2026-07-28 01:18:05 +08:00
|
|
|
// Async pipelined FPWR: two alternating leaked tx slots so the in-flight
|
|
|
|
|
// write future can hold a 'static buffer while the response is reaped
|
|
|
|
|
// at the start of the next cycle.
|
2026-08-10 01:28:43 +08:00
|
|
|
let tx_slot_a = Box::leak(Box::new([0u8; PAYLOAD_LEN]));
|
|
|
|
|
let tx_slot_b = Box::leak(Box::new([0u8; PAYLOAD_LEN]));
|
2026-07-28 01:18:05 +08:00
|
|
|
let mut tx_slot_toggle = false;
|
|
|
|
|
let mut pending_tx: Option<
|
|
|
|
|
std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), ethercrab::error::Error>> + '_>>,
|
|
|
|
|
> = None;
|
|
|
|
|
let mut tx_late: u64 = 0;
|
|
|
|
|
|
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
|
|
|
let mut aborted: Option<String> = None;
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
if STOP.load(Ordering::Relaxed) {
|
|
|
|
|
println!("\n(stop requested, finishing cycle {})", cyc);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if cfg.duration_sec > 0 && start.elapsed().as_secs() >= cfg.duration_sec {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut rx_ok = true;
|
2026-07-28 00:11:34 +08:00
|
|
|
let spin_mode = cfg.spin && cfg.mode == Mode::Ticker;
|
|
|
|
|
let mut jitter_ns: i64 = 0;
|
|
|
|
|
|
|
|
|
|
if spin_mode {
|
|
|
|
|
// ---- Spin-lock: poll FPRD until the payload changes ----
|
|
|
|
|
// Phase-locks processing to TwinCAT's frame arrival instead of a
|
|
|
|
|
// free-running ticker. Timeout at 1.5 periods keeps the loop
|
|
|
|
|
// alive (counted as stale) if the primary master stops.
|
|
|
|
|
let spin_t0 = Instant::now();
|
|
|
|
|
let timeout_ns = cycle_ns + cycle_ns / 2;
|
|
|
|
|
let mut fresh = false;
|
|
|
|
|
loop {
|
|
|
|
|
match ethercrab::Command::fprd(station, SM_RX_ADDR)
|
|
|
|
|
.receive_slice(&maindevice, SM_RX_LEN as u16)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(pdu) => {
|
|
|
|
|
let raw: &[u8] = &pdu;
|
|
|
|
|
let n = raw.len().min(SM_RX_LEN);
|
|
|
|
|
if n > RX_PREFIX {
|
|
|
|
|
let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN);
|
|
|
|
|
rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]);
|
|
|
|
|
}
|
|
|
|
|
if rx_buf[..8] != prev_rx8 {
|
|
|
|
|
prev_rx8.copy_from_slice(&rx_buf[..8]);
|
|
|
|
|
fresh = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
rx_err += 1;
|
|
|
|
|
}
|
2026-07-27 19:01:05 +08:00
|
|
|
}
|
2026-07-28 00:11:34 +08:00
|
|
|
if spin_t0.elapsed().as_nanos() as u64 > timeout_ns {
|
|
|
|
|
break;
|
2026-07-27 19:01:05 +08:00
|
|
|
}
|
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
|
|
|
}
|
2026-07-28 00:11:34 +08:00
|
|
|
if !fresh {
|
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
|
|
|
rx_ok = false;
|
2026-07-28 00:11:34 +08:00
|
|
|
rx_stale += 1;
|
|
|
|
|
}
|
|
|
|
|
// jitter = deviation of fresh-data arrival interval from nominal
|
|
|
|
|
let now = Instant::now();
|
|
|
|
|
if let Some(prev) = last_fresh {
|
|
|
|
|
let dt = now.duration_since(prev).as_nanos() as i64 - cycle_ns as i64;
|
|
|
|
|
jitter_st.push(dt);
|
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
|
|
|
}
|
2026-07-28 00:11:34 +08:00
|
|
|
last_fresh = Some(now);
|
|
|
|
|
} else {
|
|
|
|
|
jitter_ns = ticker.wait_next_period();
|
|
|
|
|
jitter_st.push(jitter_ns);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let t0 = Instant::now();
|
|
|
|
|
cyc += 1;
|
|
|
|
|
|
2026-07-28 01:18:05 +08:00
|
|
|
// Reap async FPWR kicked last cycle: the spin-detect above gave the
|
|
|
|
|
// response ~one full cycle to arrive, so this should never block.
|
|
|
|
|
if let Some(mut f) = pending_tx.take() {
|
|
|
|
|
match futures_lite::future::poll_once(f.as_mut()).await {
|
|
|
|
|
Some(Ok(_)) => {}
|
|
|
|
|
Some(Err(_)) => {
|
|
|
|
|
tx_err += 1;
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
tx_late += 1;
|
|
|
|
|
let _ = f.await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 00:11:34 +08:00
|
|
|
if !spin_mode {
|
|
|
|
|
// 1. Read SM3 (TwinCAT data via bridge)
|
|
|
|
|
let t_rx0 = Instant::now();
|
|
|
|
|
match ethercrab::Command::fprd(station, SM_RX_ADDR)
|
|
|
|
|
.receive_slice(&maindevice, SM_RX_LEN as u16)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(pdu) => {
|
|
|
|
|
let raw: &[u8] = &pdu;
|
|
|
|
|
// Strip 2-byte diagnostic word prefix (0x1A01) from plain mode
|
|
|
|
|
let n = raw.len().min(SM_RX_LEN);
|
|
|
|
|
if n > RX_PREFIX {
|
|
|
|
|
let copy_len = (n - RX_PREFIX).min(SM_RX_PLD_LEN);
|
|
|
|
|
rx_buf[..copy_len].copy_from_slice(&raw[RX_PREFIX..RX_PREFIX + copy_len]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
|
|
|
|
rx_ok = false;
|
|
|
|
|
rx_err += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
rx_rtt_st.push(t_rx0.elapsed().as_nanos() as i64);
|
|
|
|
|
}
|
|
|
|
|
if cyc <= 3 && rx_ok {
|
|
|
|
|
let live: u32 = rx_buf[..SM_RX_PLD_LEN.min(256)].iter().map(|&b| if b != 0 { 1 } else { 0 }).sum();
|
|
|
|
|
let show = SM_RX_LEN.min(64);
|
|
|
|
|
println!(" rx_buf[0..{}]: {} non-zero (of {}) at cycle {}", show, live, SM_RX_PLD_LEN.min(256), cyc);
|
|
|
|
|
print!(" rx_buf[..{}] hex: ", show);
|
|
|
|
|
for b in rx_buf[..show].iter() { print!("{:02X}", b); }
|
|
|
|
|
println!();
|
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
|
|
|
}
|
|
|
|
|
|
2026-07-27 19:01:05 +08:00
|
|
|
// 2. In ticker mode: prove J1900 processing by transforming TwinCAT's
|
|
|
|
|
// tc_seq instead of blind echo. The function f(x)=x*7+3 is a
|
|
|
|
|
// non-trivial mathematical transform that no hardware bridge could
|
|
|
|
|
// produce — it proves J1900 read, computed AND wrote the data.
|
|
|
|
|
// The original tc_seq is preserved at offset 272 for TwinCAT to
|
|
|
|
|
// verify: aIn1[0..7] == f(aOut1.tc_seq).
|
2026-07-23 00:46:48 +08:00
|
|
|
// In probe mode: skip TX (read-only observation).
|
|
|
|
|
if cfg.mode == Mode::Ticker {
|
|
|
|
|
if rx_ok {
|
2026-07-27 19:01:05 +08:00
|
|
|
if PAYLOAD_LEN == 8 {
|
|
|
|
|
// 8B proof mode: per-byte transform tx[i] = rx[i] XOR 0x5A.
|
|
|
|
|
// Each byte is read, transformed and written back
|
|
|
|
|
// independently, so TwinCAT can verify all 8 bytes.
|
|
|
|
|
for i in 0..PAYLOAD_LEN {
|
|
|
|
|
tx_buf[i] = rx_buf[i] ^ 0x5A;
|
|
|
|
|
}
|
|
|
|
|
payload_checked += 1;
|
|
|
|
|
} else {
|
|
|
|
|
// Echo received data within available bounds
|
|
|
|
|
let echo_len = SM_RX_PLD_LEN.min(tx_buf.len());
|
|
|
|
|
tx_buf[..echo_len].copy_from_slice(&rx_buf[..echo_len]);
|
|
|
|
|
// Read TwinCAT's sequence number
|
|
|
|
|
let tc_seq = u64::from_le_bytes(
|
|
|
|
|
rx_buf[..8].try_into().unwrap_or([0; 8]),
|
|
|
|
|
);
|
|
|
|
|
// J1900 payload verification: pattern at [16..255] = (tc_seq + i) % 256
|
|
|
|
|
// (only present in 1KB layout)
|
|
|
|
|
if SM_RX_PLD_LEN >= 256 {
|
|
|
|
|
let mut mism: u32 = 0;
|
|
|
|
|
for i in 16..256 {
|
|
|
|
|
let expected = ((tc_seq + i as u64) % 256) as u8;
|
|
|
|
|
if rx_buf[i] != expected {
|
|
|
|
|
mism += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
payload_checked += 1;
|
|
|
|
|
payload_mismatch += mism as u64;
|
|
|
|
|
if PAYLOAD_LEN >= 280 {
|
|
|
|
|
// Write J1900 payload mismatch count for TwinCAT (offset 268)
|
|
|
|
|
tx_buf[268..272].copy_from_slice(&mism.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Transform: f(x) = x * 7 + 3 (irreversible by simple subtraction)
|
|
|
|
|
let processed = tc_seq.wrapping_mul(7).wrapping_add(3);
|
|
|
|
|
tx_buf[..8].copy_from_slice(&processed.to_le_bytes());
|
|
|
|
|
if PAYLOAD_LEN >= 280 {
|
|
|
|
|
// Preserve original for TwinCAT verification (offset 272)
|
|
|
|
|
tx_buf[272..280].copy_from_slice(&tc_seq.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if PAYLOAD_LEN >= 16 {
|
|
|
|
|
tx_buf[8..16].copy_from_slice(&cyc.to_le_bytes());
|
|
|
|
|
}
|
|
|
|
|
if PAYLOAD_LEN >= 280 {
|
|
|
|
|
tx_buf[264..268].copy_from_slice(&(jitter_ns as i32).to_le_bytes());
|
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
|
|
|
}
|
|
|
|
|
|
2026-08-10 01:28:43 +08:00
|
|
|
if spin_mode {
|
2026-07-28 01:18:05 +08:00
|
|
|
// Async pipelined FPWR: kick the frame onto the wire now
|
|
|
|
|
// (poll_once drives it to sendable), reap response next
|
|
|
|
|
// cycle. Frees ~240us of blocking write time per cycle.
|
|
|
|
|
let p = if tx_slot_toggle {
|
|
|
|
|
tx_slot_b.as_mut_ptr()
|
|
|
|
|
} else {
|
|
|
|
|
tx_slot_a.as_mut_ptr()
|
|
|
|
|
};
|
|
|
|
|
tx_slot_toggle = !tx_slot_toggle;
|
|
|
|
|
let slot: &'static mut [u8] =
|
|
|
|
|
unsafe { std::slice::from_raw_parts_mut(p, PAYLOAD_LEN) };
|
|
|
|
|
slot.copy_from_slice(&tx_buf[..PAYLOAD_LEN]);
|
|
|
|
|
let mut fut = Box::pin(
|
|
|
|
|
ethercrab::Command::fpwr(station, SM_TX_ADDR).send(&maindevice, &slot[..]),
|
|
|
|
|
);
|
|
|
|
|
// Kick: drive the write future for up to ~20us so the frame
|
|
|
|
|
// is marked sendable and the TX thread puts it on the wire
|
|
|
|
|
// NOW. poll_once alone does not reach sendable state.
|
|
|
|
|
let t_kick = Instant::now();
|
|
|
|
|
let _ = futures_lite::future::or(
|
|
|
|
|
async { fut.as_mut().await.is_ok() },
|
|
|
|
|
async {
|
|
|
|
|
while t_kick.elapsed().as_micros() < 20 {
|
|
|
|
|
futures_lite::future::yield_now().await;
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
.await;
|
|
|
|
|
pending_tx = Some(fut);
|
|
|
|
|
} else {
|
|
|
|
|
let t_tx0 = Instant::now();
|
|
|
|
|
let tx_ok = ethercrab::Command::fpwr(station, SM_TX_ADDR)
|
|
|
|
|
.send(&maindevice, &tx_buf[..PAYLOAD_LEN])
|
|
|
|
|
.await
|
|
|
|
|
.is_ok();
|
|
|
|
|
tx_rtt_st.push(t_tx0.elapsed().as_nanos() as i64);
|
|
|
|
|
if !tx_ok {
|
|
|
|
|
tx_err += 1;
|
|
|
|
|
}
|
2026-07-23 00:46:48 +08:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
// Optional EL2202 scope waveform
|
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
|
|
|
if let Some((st2, addr)) = el2202 {
|
|
|
|
|
let v: u8 = if cfg.el2202_dual {
|
|
|
|
|
(cyc & 1) as u8 * (regs::EL2202_CH1_DATA | regs::EL2202_CH2_DATA)
|
|
|
|
|
} else {
|
|
|
|
|
(cyc & 1) as u8 * regs::EL2202_CH1_DATA
|
|
|
|
|
};
|
|
|
|
|
if ethercrab::Command::fpwr(st2, addr)
|
|
|
|
|
.send(&maindevice, v)
|
|
|
|
|
.await
|
|
|
|
|
.is_err()
|
|
|
|
|
{
|
|
|
|
|
el2202_err += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
// Optional EL2262 DC-locked reference output
|
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
|
|
|
if let Some(st3) = el2262 {
|
|
|
|
|
let v: u8 = (cyc & 1) as u8;
|
|
|
|
|
if ethercrab::Command::fpwr(st3, EL2262_OUT_ADDR)
|
|
|
|
|
.send(&maindevice, v)
|
|
|
|
|
.await
|
|
|
|
|
.is_err()
|
|
|
|
|
{
|
|
|
|
|
el2262_err += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
// Optional EL1252 latch readback
|
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
|
|
|
if let Some(st4) = el1252 {
|
|
|
|
|
if cyc % (cfg.el1252_every.max(1) as u64) == 0 {
|
|
|
|
|
match ethercrab::Command::fprd(st4, 0x09AEu16)
|
|
|
|
|
.receive_slice(&maindevice, 34)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(pdu) => {
|
|
|
|
|
let e: &[u8] = &pdu;
|
|
|
|
|
if e.len() >= 34 {
|
|
|
|
|
el1252_st = [e[0], e[1]];
|
|
|
|
|
let pos0 = u64::from_le_bytes(e[2..10].try_into().unwrap());
|
|
|
|
|
let neg0 = u64::from_le_bytes(e[10..18].try_into().unwrap());
|
|
|
|
|
let pos1 = u64::from_le_bytes(e[18..26].try_into().unwrap());
|
|
|
|
|
let neg1 = u64::from_le_bytes(e[26..34].try_into().unwrap());
|
|
|
|
|
if pos0 != 0 && pos0 != latch_pos0 {
|
|
|
|
|
if latch_pos0 != 0 {
|
|
|
|
|
latch_interval_st.push((pos0 - latch_pos0) as i64);
|
|
|
|
|
}
|
|
|
|
|
latch_phase_st.push((pos0 % cycle_ns) as i64);
|
|
|
|
|
latch_pos0 = pos0;
|
|
|
|
|
pos_edges += 1;
|
|
|
|
|
}
|
|
|
|
|
if neg0 != 0 && neg0 != latch_neg0 {
|
|
|
|
|
latch_neg0 = neg0;
|
|
|
|
|
neg_edges += 1;
|
|
|
|
|
}
|
|
|
|
|
if pos1 != 0 && pos1 != latch_pos1 {
|
|
|
|
|
if latch_pos1 != 0 {
|
|
|
|
|
latch1_interval_st.push((pos1 - latch_pos1) as i64);
|
|
|
|
|
}
|
|
|
|
|
latch1_phase_st.push((pos1 % cycle_ns) as i64);
|
|
|
|
|
latch_pos1 = pos1;
|
|
|
|
|
pos1_edges += 1;
|
|
|
|
|
}
|
|
|
|
|
if neg1 != 0 && neg1 != latch_neg1 {
|
|
|
|
|
latch_neg1 = neg1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(_) => el1252_err += 1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 00:46:48 +08:00
|
|
|
if rx_ok && (cfg.mode == Mode::Probe || !tx_err > 0) {
|
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
|
|
|
err_streak = 0;
|
|
|
|
|
} else {
|
|
|
|
|
err_streak += 1;
|
|
|
|
|
if err_streak > MAX_ERR_STREAK {
|
|
|
|
|
aborted = Some(format!(
|
|
|
|
|
"aborting: {} consecutive cycles with TX/RX errors (link dead?)",
|
|
|
|
|
err_streak
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let tc_seq = u64::from_le_bytes(
|
2026-07-23 00:46:48 +08:00
|
|
|
rx_buf[..8].try_into().unwrap_or([0; 8]),
|
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
|
|
|
);
|
|
|
|
|
if tc_seq == 0 {
|
|
|
|
|
no_tc_seq += 1;
|
|
|
|
|
}
|
2026-07-27 19:01:05 +08:00
|
|
|
// J1900 data verification
|
|
|
|
|
if rx_ok && cfg.mode == Mode::Ticker {
|
|
|
|
|
if tc_seq == prev_tc_seq {
|
|
|
|
|
rx_stale += 1;
|
|
|
|
|
}
|
|
|
|
|
if prev_tc_seq != 0 && tc_seq != 0 {
|
|
|
|
|
let delta = (tc_seq as i64).wrapping_sub(prev_tc_seq as i64);
|
|
|
|
|
if delta < 0 {
|
|
|
|
|
rx_nonmono += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
prev_tc_seq = tc_seq;
|
|
|
|
|
// Self-consistency: verify proc == tc_seq * 7 + 3
|
|
|
|
|
// (always true for 1KB ticker mode; 8B mode uses XOR echo instead)
|
|
|
|
|
if PAYLOAD_LEN != 8 {
|
|
|
|
|
let computed = tc_seq.wrapping_mul(7).wrapping_add(3);
|
|
|
|
|
if cfg.mode == Mode::Ticker && tx_buf[..8] != computed.to_le_bytes() {
|
|
|
|
|
comp_err += 1; // internal computation error (should never fire)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
exec_st.push(t0.elapsed().as_nanos() as i64);
|
|
|
|
|
|
|
|
|
|
if !cfg.quiet && last_report.elapsed() >= Duration::from_millis(1000) {
|
|
|
|
|
if let Some(st4) = el1252 {
|
|
|
|
|
if let Ok(b) = ethercrab::Command::fprd(st4, 0x1000u16)
|
|
|
|
|
.receive::<u8>(&maindevice)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
el1252_in = b;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-27 19:01:05 +08:00
|
|
|
let proc_val = tc_seq.wrapping_mul(7).wrapping_add(3);
|
|
|
|
|
let stale_pct = if cyc > 0 { (rx_stale * 100 / cyc) } else { 0 };
|
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
|
|
|
println!(
|
2026-07-28 01:18:05 +08:00
|
|
|
"S tms={} cyc={} raw={} proc={} txe={} rxe={} late={} stale={}% mono={} cerr={} mism={} e0={} e1={}",
|
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
|
|
|
start.elapsed().as_millis(),
|
|
|
|
|
cyc,
|
|
|
|
|
tc_seq,
|
2026-07-27 19:01:05 +08:00
|
|
|
proc_val,
|
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
|
|
|
tx_err,
|
|
|
|
|
rx_err,
|
2026-07-28 01:18:05 +08:00
|
|
|
tx_late,
|
2026-07-27 19:01:05 +08:00
|
|
|
stale_pct,
|
|
|
|
|
rx_nonmono,
|
|
|
|
|
comp_err,
|
|
|
|
|
payload_mismatch,
|
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
|
|
|
pos_edges,
|
|
|
|
|
pos1_edges,
|
|
|
|
|
);
|
|
|
|
|
last_report = Instant::now();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if aborted.is_some() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-28 01:18:05 +08:00
|
|
|
// Reap any in-flight async FPWR before reporting
|
|
|
|
|
if let Some(f) = pending_tx.take() {
|
|
|
|
|
let _ = f.await;
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// ---- Final report ----
|
|
|
|
|
println!("\n=== timing stats ===");
|
|
|
|
|
jitter_st.report(1000, "us");
|
|
|
|
|
exec_st.report(1000, "us");
|
|
|
|
|
rx_rtt_st.report(1000, "us");
|
|
|
|
|
tx_rtt_st.report(1000, "us");
|
|
|
|
|
if el1252.is_some() {
|
|
|
|
|
println!("=== EL1252 latch: rising-edge intervals and phase in cycle ===");
|
|
|
|
|
latch_interval_st.report(1000, "us");
|
|
|
|
|
latch_phase_st.report(1000, "us");
|
|
|
|
|
latch1_interval_st.report(1000, "us");
|
|
|
|
|
latch1_phase_st.report(1000, "us");
|
|
|
|
|
println!(
|
|
|
|
|
"el1252: ch1 pos_edges={}, ch1 neg_edges={}, ch2 pos_edges={}, read_err={}",
|
|
|
|
|
pos_edges, neg_edges, pos1_edges, el1252_err
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-07-27 19:01:05 +08:00
|
|
|
println!("=== J1900 data verification ===");
|
|
|
|
|
println!(" tc_seq: stale={} ({}%) nonmono={} comp_err={}", rx_stale,
|
|
|
|
|
if cyc > 0 { rx_stale * 100 / cyc } else { 0 },
|
|
|
|
|
rx_nonmono, comp_err);
|
|
|
|
|
println!(" payload: checked={} mismatch={}", payload_checked, payload_mismatch);
|
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
|
|
|
println!(
|
2026-07-23 00:46:48 +08:00
|
|
|
"\n=== done: {} cycles in {:.1}s, tx_err={}, rx_err={}, no_tc_seq={}, el2202_err={}, el2262_err={} ===",
|
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
|
|
|
cyc,
|
|
|
|
|
start.elapsed().as_secs_f64(),
|
|
|
|
|
tx_err,
|
|
|
|
|
rx_err,
|
|
|
|
|
no_tc_seq,
|
|
|
|
|
el2202_err,
|
|
|
|
|
el2262_err
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if let Some(msg) = aborted {
|
|
|
|
|
return Err(msg.into());
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|