From 253f4b3cf72aea081f501cfa802d7c0ca6346f94 Mon Sep 17 00:00:00 2001 From: Tony Cao Date: Mon, 10 Aug 2026 01:56:41 +0800 Subject: [PATCH] J1900-side DC-Synchron: EL6695 secondary firmware sync_mode=2 via CoE, DC telemetry, verified 180s zero sync errors --- src/main.rs | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 911b28e..f2edc6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -98,6 +98,9 @@ struct Config { /// payload changes (new TwinCAT frame), then process immediately. /// Phase-locks J1900 processing to the primary master's frame arrival. spin: bool, + /// Enable DC SYNC0 on the EL6695 secondary (cycle = --cycle-us) and print + /// per-second DC phase/drift telemetry in the report line. + dc: bool, } fn parse_args() -> Result { @@ -116,6 +119,7 @@ fn parse_args() -> Result { el2262: false, el1252_every: 1, spin: false, + dc: false, }; let mut args = std::env::args().skip(1).peekable(); if let Some(a) = args.peek() { @@ -146,6 +150,7 @@ fn parse_args() -> Result { "--prio" => cfg.prio = val("--prio")?.parse().map_err(|_| "bad --prio")?, "--quiet" => cfg.quiet = true, "--spin" => cfg.spin = true, + "--dc" => cfg.dc = true, "--el2202" => cfg.el2202 = true, "--el2202-dual" => { cfg.el2202 = true; @@ -525,6 +530,148 @@ async fn run(cfg: &Config) -> Result<(), Box> { } println!(" >>> OP achieved! <<<"); + // ---- Optional DC SYNC0 on the EL6695 secondary ---- + // ethercrab init already distributed the system time (offset/propagation + // delay/static drift). Here we only start the SYNC0 unit on --cycle-us + // grid and verify the clock actually advances. + let mut dc_ref: Option<(u64, u64)> = None; // (dc_time, mono_time) at setup + if cfg.dc { + let t1 = ethercrab::Command::fprd(station, regs::DC_SYSTEM_TIME) + .receive::(&maindevice) + .await + .unwrap_or(0); + std::thread::sleep(Duration::from_millis(100)); + let t2 = ethercrab::Command::fprd(station, regs::DC_SYSTEM_TIME) + .receive::(&maindevice) + .await + .unwrap_or(0); + let adv = t2.wrapping_sub(t1); + println!("DC system time: {} -> {} (adv {} ns in 100ms)", t1, t2, adv); + if adv < 50_000_000 || adv > 500_000_000 { + println!("DC: clock not advancing sanely; SYNC0 setup skipped"); + } else { + // Diagnostics: which DC register groups actually exist? + for (reg, name) in [ + (0x0900u16, "rx_time_port0(u32)"), + (0x0920u16, "sys_time_offset(u64)"), + (0x0928u16, "prop_delay(u32)"), + (0x0930u16, "drift... (u16)"), + (0x0980u16, "sync_activation(u8)"), + (0x0981u16, "sync_active_byte(u8)"), + (0x09A0u16, "sync0_cycle(u32)"), + (0x09AEu16, "sync0_status(u8)"), + ] { + match ethercrab::Command::fprd(station, reg) + .receive_slice(&maindevice, 8) + .await + { + Ok(pdu) => { + let raw: &[u8] = &pdu; + println!(" DC probe 0x{:04X} {}: {:02X?}", reg, name, &raw[..8.min(raw.len())]); + } + Err(e) => println!(" DC probe 0x{:04X} {}: ERR {:?}", reg, name, e), + } + } + // CoE view: 0x1C32 (SM2 outputs) / 0x1C33 (SM3 inputs) sync params + for idx in [0x1C32u16, 0x1C33] { + let n = sd.sdo_read::(idx, 0).await.unwrap_or(0xFF); + println!(" CoE 0x{:04X}: {} subentries", idx, n); + for (sub, name) in [(0x01u8, "sync_mode"), (0x04, "modes_supported"), (0x0B, "sm_event_missed"), (0x0C, "cycle_exceeded")] { + match sd.sdo_read::(idx, sub).await { + Ok(x) => println!(" CoE 0x{:04X}:{:02X} {} = 0x{:04X}", idx, sub, name, x), + Err(e) => println!(" CoE 0x{:04X}:{:02X} {} ERR {:?}", idx, sub, name, e), + } + } + match sd.sdo_read::(idx, 0x02).await { + Ok(x) => println!(" CoE 0x{:04X}:02 cycle_time_ns = {}", idx, x), + Err(e) => println!(" CoE 0x{:04X}:02 cycle_time_ns ERR {:?}", idx, e), + } + match sd.sdo_read::(idx, 0x20).await { + Ok(x) => println!(" CoE 0x{:04X}:20 sync_error = {}", idx, x), + Err(e) => println!(" CoE 0x{:04X}:20 sync_error ERR {:?}", idx, e), + } + } + let start = t2.wrapping_add(100_000_000); + let w1 = ethercrab::Command::fpwr(station, regs::DC_SYNC_START_TIME) + .send(&maindevice, start) + .await; + println!(" DC write start_time(0x0990): {:?}", w1.as_ref().map(|_| "OK").map_err(|e| format!("{:?}", e))); + let w2 = ethercrab::Command::fpwr(station, regs::DC_SYNC0_CYCLE) + .send(&maindevice, cycle_ns as u32) + .await; + println!(" DC write sync0_cycle(0x09A0): {:?}", w2.as_ref().map(|_| "OK").map_err(|e| format!("{:?}", e))); + let w3 = regs::rmw_u8( + &maindevice, + station, + regs::DC_SYNC_ACTIVATION, + regs::DC_SYNC0_ACTIVATE, + 0, + ) + .await; + println!(" DC rmw activation(0x0980): {:?}", w3.as_ref().map(|v| format!("0x{:02X}", v)).map_err(|e| format!("{:?}", e))); + // rmw needs a read (fails here); try a blind full-byte write instead + let w4 = ethercrab::Command::fpwr(station, regs::DC_SYNC_ACTIVATION) + .send(&maindevice, regs::DC_SYNC0_ACTIVATE) + .await; + println!(" DC blind write activation(0x0980)=0x01: {:?}", w4.as_ref().map(|_| "OK").map_err(|e| format!("{:?}", e))); + std::thread::sleep(Duration::from_millis(150)); + // Try selecting DC-Synchron SYNC0 via firmware object (mode 2). + // May be rejected in OP; worth observing the abort code either way. + let mut coe_dc_ok = true; + for idx in [0x1C32u16, 0x1C33] { + match sd.sdo_write::(idx, 0x01, 2).await { + Ok(_) => println!(" CoE write 0x{:04X}:01 sync_mode=2 OK", idx), + Err(e) => { + coe_dc_ok = false; + println!(" CoE write 0x{:04X}:01 sync_mode=2 ERR {:?}", idx, e); + } + } + match sd.sdo_read::(idx, 0x01).await { + Ok(x) => println!(" CoE-after 0x{:04X}:01 sync_mode = {}", idx, x), + Err(e) => { + coe_dc_ok = false; + println!(" CoE-after 0x{:04X}:01 sync_mode ERR {:?}", idx, e); + } + } + } + if coe_dc_ok { + let t_dc = ethercrab::Command::fprd(station, regs::DC_SYSTEM_TIME) + .receive::(&maindevice) + .await + .unwrap_or(0); + dc_ref = Some((t_dc, rt::now_ns())); + println!("DC-Synchron: firmware sync_mode=2 accepted, DC telemetry on"); + } + let act = ethercrab::Command::fprd(station, regs::DC_SYNC_ACTIVATION) + .receive::(&maindevice) + .await; + let cyc0 = ethercrab::Command::fprd(station, regs::DC_SYNC0_CYCLE) + .receive::(&maindevice) + .await; + let t3 = ethercrab::Command::fprd(station, regs::DC_SYSTEM_TIME) + .receive::(&maindevice) + .await + .unwrap_or(0); + let (al, alc) = bridge::drive_state(&maindevice, station, 0x0008).await; + println!(" AL after DC writes: {}", bridge::verdict(al, alc)); + println!( + "DC SYNC0: act={:?} cycle={:?} start=+100ms (t3={} start_delta={}ns)", + act.as_ref().map(|v| format!("0x{:02X}", v)).map_err(|e| format!("{:?}", e)), + cyc0.as_ref().map_err(|e| format!("{:?}", e)), + t3, + t3.wrapping_sub(start) as i64 + ); + if matches!(act, Ok(a) if a & regs::DC_SYNC0_ACTIVATE != 0) + && matches!(cyc0, Ok(c) if c == cycle_ns as u32) + { + dc_ref = Some((t3, rt::now_ns())); + println!("DC SYNC0: active on EL6695 secondary"); + } else { + println!("DC SYNC0: readback mismatch; running without DC telemetry"); + } + } + } + // ---- Optional EL2202 oscilloscope waveform (toggle one DO per cycle) ---- const EL2202_OUT_ADDR: u16 = 0x0F00; let mut el2202: Option<(u16, u16)> = None; @@ -1109,8 +1256,23 @@ async fn run(cfg: &Config) -> Result<(), Box> { } let proc_val = tc_seq.wrapping_mul(7).wrapping_add(3); let stale_pct = if cyc > 0 { (rx_stale * 100 / cyc) } else { 0 }; + let mut dc_info = String::new(); + if let Some((dc0, mono0)) = dc_ref { + if let Ok(dc_now) = ethercrab::Command::fprd(station, regs::DC_SYSTEM_TIME) + .receive::(&maindevice) + .await + { + let mono_now = rt::now_ns(); + let drift = (dc_now.wrapping_sub(dc0)) as i64 - (mono_now - mono0) as i64; + dc_info = format!( + " dcph={} dcdr={}ns", + dc_now % cycle_ns, + drift + ); + } + } println!( - "S tms={} cyc={} raw={} proc={} txe={} rxe={} late={} stale={}% mono={} cerr={} mism={} e0={} e1={}", + "S tms={} cyc={} raw={} proc={} txe={} rxe={} late={} stale={}% mono={} cerr={} mism={} e0={} e1={}{}", start.elapsed().as_millis(), cyc, tc_seq, @@ -1124,6 +1286,7 @@ async fn run(cfg: &Config) -> Result<(), Box> { payload_mismatch, pos_edges, pos1_edges, + dc_info, ); last_report = Instant::now(); } @@ -1138,6 +1301,21 @@ async fn run(cfg: &Config) -> Result<(), Box> { let _ = f.await; } + // ---- DC post-run: re-read firmware sync counters accumulated during the run ---- + if cfg.dc { + println!("\n=== DC post-run sync counters (0x1C32 SM2 / 0x1C33 SM3) ==="); + for idx in [0x1C32u16, 0x1C33] { + let mode = sd.sdo_read::(idx, 0x01).await.unwrap_or(0xFFFF); + let missed = sd.sdo_read::(idx, 0x0B).await.unwrap_or(0xFFFF); + let exceeded = sd.sdo_read::(idx, 0x0C).await.unwrap_or(0xFFFF); + let serr = sd.sdo_read::(idx, 0x20).await.unwrap_or(0xFF); + println!( + " 0x{:04X}: sync_mode={} sm_event_missed={} cycle_exceeded={} sync_error={}", + idx, mode, missed, exceeded, serr + ); + } + } + // ---- Final report ---- println!("\n=== timing stats ==="); jitter_st.report(1000, "us");