experiment(frame-pll): phase-gated write harmful; event-driven async is optimal
- frame-arrival phase estimator (wrap-aware min-tracking) + optional boundary+500us write gating (--dc-discipline): j2cx 413->2912us, nMatchLag 5->11, loop rate 990->315/s - gating reintroduces the very quantization the event-driven loop avoids; disabled (if false), kept as measurement tooling (fresh_phase stats) - verdict: E2E 4-5ms at 1kHz is structural (bridge 2-3 cycles + task quantization); only lever = task frequency (4kHz -> 1.25ms verified)
This commit is contained in:
parent
49de477028
commit
788499cf55
69
src/main.rs
69
src/main.rs
|
|
@ -127,6 +127,7 @@ struct Config {
|
||||||
/// 完全采纳从站 EEPROM 默认配置, 不写任何 SM/FMMU/PDO
|
/// 完全采纳从站 EEPROM 默认配置, 不写任何 SM/FMMU/PDO
|
||||||
adopt_pure: bool,
|
adopt_pure: bool,
|
||||||
sync_ts: bool,
|
sync_ts: bool,
|
||||||
|
dc_discipline: bool,
|
||||||
/// 强制 SM3 长度 (0=默认 PAYLOAD_LEN+sm3_prefix)
|
/// 强制 SM3 长度 (0=默认 PAYLOAD_LEN+sm3_prefix)
|
||||||
sm3_len_force: u16,
|
sm3_len_force: u16,
|
||||||
/// Enable DC SYNC0 on the EL6695 secondary (cycle = --cycle-us) and print
|
/// Enable DC SYNC0 on the EL6695 secondary (cycle = --cycle-us) and print
|
||||||
|
|
@ -159,6 +160,7 @@ fn parse_args() -> Result<Config, String> {
|
||||||
sii_fix: false,
|
sii_fix: false,
|
||||||
adopt_pure: false,
|
adopt_pure: false,
|
||||||
sync_ts: false,
|
sync_ts: false,
|
||||||
|
dc_discipline: false,
|
||||||
sm3_len_force: 0,
|
sm3_len_force: 0,
|
||||||
dc: false,
|
dc: false,
|
||||||
iouring: false,
|
iouring: false,
|
||||||
|
|
@ -205,6 +207,7 @@ fn parse_args() -> Result<Config, String> {
|
||||||
"--sii-fix" => cfg.sii_fix = true,
|
"--sii-fix" => cfg.sii_fix = true,
|
||||||
"--adopt-pure" => cfg.adopt_pure = true,
|
"--adopt-pure" => cfg.adopt_pure = true,
|
||||||
"--sync-ts" => cfg.sync_ts = true,
|
"--sync-ts" => cfg.sync_ts = true,
|
||||||
|
"--dc-discipline" => cfg.dc_discipline = true,
|
||||||
"--sm3-len" => { cfg.sm3_len_force = val("--sm3-len")?.parse().map_err(|_| "bad --sm3-len")?; },
|
"--sm3-len" => { cfg.sm3_len_force = val("--sm3-len")?.parse().map_err(|_| "bad --sm3-len")?; },
|
||||||
"--dc" => cfg.dc = true,
|
"--dc" => cfg.dc = true,
|
||||||
"--iouring" => cfg.iouring = true,
|
"--iouring" => cfg.iouring = true,
|
||||||
|
|
@ -1554,7 +1557,7 @@ async fn run(cfg: &Config) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// ---- rfm-async: event-driven forwarder, no tick ----
|
// ---- rfm-async: event-driven forwarder, no tick ----
|
||||||
if cfg.mode == Mode::RfmAsync {
|
if cfg.mode == Mode::RfmAsync {
|
||||||
let rfm = rfm_ctx.take().expect("rfm not opened");
|
let rfm = rfm_ctx.take().expect("rfm not opened");
|
||||||
return rfm_async_loop(&maindevice, station, rfm, cfg.duration_sec).await;
|
return rfm_async_loop(&maindevice, station, rfm, cfg.duration_sec, cfg.dc_discipline).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -2316,6 +2319,7 @@ async fn rfm_async_loop(
|
||||||
station: u16,
|
station: u16,
|
||||||
mut rfm: el6695_rt::rfm::Rfm,
|
mut rfm: el6695_rt::rfm::Rfm,
|
||||||
duration_sec: u64,
|
duration_sec: u64,
|
||||||
|
dc_discipline: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use el6695_rt::rfm::*;
|
use el6695_rt::rfm::*;
|
||||||
println!("rfm-async: event-driven forwarding, no tick (Ctrl-C to stop)");
|
println!("rfm-async: event-driven forwarding, no tick (Ctrl-C to stop)");
|
||||||
|
|
@ -2373,10 +2377,49 @@ async fn rfm_async_loop(
|
||||||
let mut seg_fpwr_us: u64 = 0;
|
let mut seg_fpwr_us: u64 = 0;
|
||||||
let mut seg_rpt_us: u64 = 0;
|
let mut seg_rpt_us: u64 = 0;
|
||||||
|
|
||||||
|
// IgH-style continuous DC discipline (manual 6.7 direct coupling)
|
||||||
|
let mut dc_prev: Option<(u64, u64)> = None; // (esc_time_ns, local_ns)
|
||||||
|
let mut dc_first: Option<(u64, u64)> = None;
|
||||||
|
let mut dc_off_applied: u64 = 0;
|
||||||
|
let mut dc_last_tick: Option<std::time::Instant> = None;
|
||||||
|
let mut dc_err_ns: i64 = 0;
|
||||||
|
let mut dc_drift_ppm: f64 = 0.0;
|
||||||
|
let mut phase_st = OnlineStats::new("fresh_phase_us", 200, -1_500_000, 1_500_000);
|
||||||
|
let mut phase_est: i64 = 0; // estimated TwinCAT frame boundary phase (ns within 1ms)
|
||||||
|
let mut wphase_st = OnlineStats::new("write_phase_us", 200, -1_500_000, 1_500_000);
|
||||||
|
|
||||||
while !STOP.load(Ordering::Relaxed) {
|
while !STOP.load(Ordering::Relaxed) {
|
||||||
if duration_sec > 0 && start.elapsed().as_secs() >= duration_sec {
|
if duration_sec > 0 && start.elapsed().as_secs() >= duration_sec {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// DC discipline tick (every ~10ms): align ESC time to local clock
|
||||||
|
if dc_discipline {
|
||||||
|
let tick = dc_last_tick.map(|t| t.elapsed().as_millis() as u64).unwrap_or(999);
|
||||||
|
if tick >= 10 {
|
||||||
|
dc_last_tick = Some(std::time::Instant::now());
|
||||||
|
if let Ok(esc) = ethercrab::Command::fprd(station, 0x0910u16)
|
||||||
|
.receive::<u64>(&maindevice).await
|
||||||
|
{
|
||||||
|
let loc = rt::now_ns();
|
||||||
|
if let Some((e0, l0)) = dc_prev {
|
||||||
|
let dt_loc = loc.wrapping_sub(l0) as f64;
|
||||||
|
let dt_esc = esc.wrapping_sub(e0) as f64;
|
||||||
|
if dt_loc > 1_000_000.0 {
|
||||||
|
dc_drift_ppm = ((dt_loc - dt_esc) / dt_loc) * 1e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let err = loc.wrapping_sub(esc) as i64;
|
||||||
|
dc_err_ns = err;
|
||||||
|
// measurement only: do NOT write offsets (naive actuation wrapped clock)
|
||||||
|
if dc_prev.is_none() {
|
||||||
|
let _ = ethercrab::Command::fpwr(station, 0x0920u16)
|
||||||
|
.send(&maindevice, 0u64).await; // reset offset once to raw
|
||||||
|
}
|
||||||
|
if dc_first.is_none() { dc_first = Some((esc, loc)); }
|
||||||
|
dc_prev = Some((esc, loc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let iter_t0 = Instant::now();
|
let iter_t0 = Instant::now();
|
||||||
if let Some(p) = last_iter {
|
if let Some(p) = last_iter {
|
||||||
let gap = iter_t0.duration_since(p).as_micros() as u64;
|
let gap = iter_t0.duration_since(p).as_micros() as u64;
|
||||||
|
|
@ -2499,6 +2542,13 @@ async fn rfm_async_loop(
|
||||||
unsafe { std::slice::from_raw_parts_mut(p, PAYLOAD_LEN) };
|
unsafe { std::slice::from_raw_parts_mut(p, PAYLOAD_LEN) };
|
||||||
slot.copy_from_slice(&tx_latest[..PAYLOAD_LEN]);
|
slot.copy_from_slice(&tx_latest[..PAYLOAD_LEN]);
|
||||||
tx_inflight_seq = tx_latest_seq;
|
tx_inflight_seq = tx_latest_seq;
|
||||||
|
if false && dc_discipline && phase_est != 0 {
|
||||||
|
let now_w = rt::now_ns() as i64;
|
||||||
|
let mut target = now_w - phase_est + 1_000_000 + 500_000;
|
||||||
|
while target < now_w + 50_000 { target += 1_000_000; }
|
||||||
|
while (rt::now_ns() as i64) < target { core::hint::spin_loop(); }
|
||||||
|
wphase_st.push(((rt::now_ns() % 1_000_000) / 1000) as i64);
|
||||||
|
}
|
||||||
let tx_ok = ethercrab::Command::fpwr(station, SM_TX_ADDR)
|
let tx_ok = ethercrab::Command::fpwr(station, SM_TX_ADDR)
|
||||||
.send(maindevice, &slot[..PAYLOAD_LEN])
|
.send(maindevice, &slot[..PAYLOAD_LEN])
|
||||||
.await
|
.await
|
||||||
|
|
@ -2542,6 +2592,17 @@ async fn rfm_async_loop(
|
||||||
if rx_buf[..8] != prev_rx8 {
|
if rx_buf[..8] != prev_rx8 {
|
||||||
prev_rx8.copy_from_slice(&rx_buf[..8]);
|
prev_rx8.copy_from_slice(&rx_buf[..8]);
|
||||||
n_fresh += 1;
|
n_fresh += 1;
|
||||||
|
{
|
||||||
|
let p = (rt::now_ns() % 1_000_000) as i64;
|
||||||
|
let mut adj = p;
|
||||||
|
if phase_est != 0 {
|
||||||
|
let d = p - phase_est;
|
||||||
|
if d > 500_000 { adj = p - 1_000_000; } else if d < -500_000 { adj = p + 1_000_000; }
|
||||||
|
phase_est = phase_est.min(adj);
|
||||||
|
} else { phase_est = p; }
|
||||||
|
phase_est += 2; // 2ns creep per fresh event (~2us/s drift compensation)
|
||||||
|
phase_st.push((p / 1000) as i64);
|
||||||
|
}
|
||||||
// 5. forward fresh CX data to RFM REQ immediately
|
// 5. forward fresh CX data to RFM REQ immediately
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
let n = SM_RX_PLD_LEN.min(MB_MAX_PAYLOAD);
|
let n = SM_RX_PLD_LEN.min(MB_MAX_PAYLOAD);
|
||||||
|
|
@ -2645,6 +2706,12 @@ async fn rfm_async_loop(
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("\n=== rfm-async done: {:.1}s ===", start.elapsed().as_secs_f64());
|
println!("\n=== rfm-async done: {:.1}s ===", start.elapsed().as_secs_f64());
|
||||||
|
print_stats(&phase_st);
|
||||||
|
print_stats(&wphase_st);
|
||||||
|
if let (Some((e0, l0)), Some((e1, l1))) = (dc_first, dc_prev) {
|
||||||
|
let ratio_ppm = ((e1.wrapping_sub(e0) as f64) / (l1.wrapping_sub(l0) as f64 - 1.0) - 1.0) * 1e6;
|
||||||
|
println!(" ESC-vs-local rate ratio: {:+.2} ppm (esc_free_running if nonzero)", ratio_ppm);
|
||||||
|
}
|
||||||
println!(
|
println!(
|
||||||
"fprd={} (err={}) fresh={} req={} rep={} fpwr={} (err={}) overflow={}",
|
"fprd={} (err={}) fresh={} req={} rep={} fpwr={} (err={}) overflow={}",
|
||||||
n_fprd, n_fprd_err, n_fresh, n_req, n_rep, n_fpwr, n_fpwr_err, n_ovf
|
n_fprd, n_fprd_err, n_fresh, n_req, n_rep, n_fpwr, n_fpwr_err, n_ovf
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user