//! Demonstrate oversampling with EK1100, EL3702. use env_logger::Env; use ethercrab::{ DcSync, EtherCrabWireRead, EtherCrabWireSized, MainDevice, MainDeviceConfig, PduStorage, RegisterAddress, Timeouts, error::Error, std::ethercat_now, subdevice_group::{CycleInfo, DcConfiguration, TxRxResponse}, }; use futures_lite::StreamExt; use std::{ sync::{ Arc, atomic::{AtomicBool, Ordering}, }, thread, time::{Duration, Instant}, }; /// Maximum number of SubDevices that can be stored. This must be a power of 2 greater than 1. const MAX_SUBDEVICES: usize = 16; const MAX_PDU_DATA: usize = PduStorage::element_size(1100); const MAX_FRAMES: usize = 32; const PDI_LEN: usize = 128; static PDU_STORAGE: PduStorage = PduStorage::new(); const TICK_INTERVAL: Duration = Duration::from_millis(5); /// PDI layout of EL3702, based on looking at the ESI file and the Beckhoff InfoSys pages #[derive(Debug, ethercrab_wire::EtherCrabWireRead)] #[allow(unused)] #[wire(bytes = 108)] struct EL3702 { #[wire(bytes = 2)] ch1_cycle_count: u16, #[wire(bytes = 50)] ch1_samples: [i16; 25], #[wire(bytes = 2)] ch2_cycle_count: u16, #[wire(bytes = 50)] ch2_samples: [i16; 25], #[wire(bytes = 4)] start_time_next_latch: u32, } impl EL3702 { // Make sure this is the same value as the array lengths in the struct definition const OVERSAMPLE_MUL: u8 = 25; } fn main() -> Result<(), Error> { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let interface = std::env::args() .nth(1) .expect("Provide network interface as first argument."); log::info!("Starting Distributed Clocks demo..."); log::info!("Run with RUST_LOG=ethercrab=debug or =trace for debug information"); let (tx, rx, pdu_loop) = PDU_STORAGE.try_split().expect("can only split once"); let maindevice = Arc::new(MainDevice::new( pdu_loop, Timeouts { wait_loop_delay: Duration::from_millis(5), state_transition: Duration::from_secs(10), pdu: Duration::from_millis(2000), ..Timeouts::default() }, MainDeviceConfig { dc_static_sync_iterations: 10_000, ..MainDeviceConfig::default() }, )); let mut tick_interval = smol::Timer::interval(TICK_INTERVAL); #[cfg(target_os = "windows")] std::thread::spawn(move || { ethercrab::std::tx_rx_task_blocking( &interface, tx, rx, ethercrab::std::TxRxTaskConfig { spinloop: false }, ) .expect("TX/RX task") }); #[cfg(not(target_os = "windows"))] smol::spawn(ethercrab::std::tx_rx_task(&interface, tx, rx).expect("spawn TX/RX task")).detach(); // Wait for TX/RX loop to start thread::sleep(Duration::from_millis(200)); #[cfg(target_os = "linux")] thread_priority::set_current_thread_priority(thread_priority::ThreadPriority::Crossplatform( thread_priority::ThreadPriorityValue::try_from(48u8).unwrap(), )) .expect("Main thread prio"); smol::block_on(async { let mut group = maindevice .init_single_group::(ethercat_now) .await .expect("Init"); // The group will be in PRE-OP at this point for mut subdevice in group.iter_mut(&maindevice) { if subdevice.name() == "EL3702" { log::info!("Found EL3702"); // Configure oversampling for both input channels subdevice.set_oversampling(&[ (0x1a00, EL3702::OVERSAMPLE_MUL), (0x1a80, EL3702::OVERSAMPLE_MUL), ]); subdevice.set_dc_sync(DcSync::Sync01 { sync1_period: Duration::from_micros( TICK_INTERVAL.as_micros() as u64 * EL3702::OVERSAMPLE_MUL as u64, ), }); } } log::info!("Group has {} SubDevices", group.len()); log::info!("Moving into PRE-OP with PDI"); let group = group.into_pre_op_pdi(&maindevice).await?; log::info!("Done. PDI available. Waiting for SubDevices to align"); let mut now = Instant::now(); let start = Instant::now(); // Repeatedly send group PDI and sync frame to align all SubDevice clocks. We use an // exponential moving average of each SubDevice's deviation from the EtherCAT System Time // (the time in the DC reference SubDevice) and take the maximum deviation. When that is // below 100ns (arbitraily chosen value for this demo), we call the sync good enough and // exit the loop. loop { group .tx_rx_sync_system_time(&maindevice) .await .expect("TX/RX"); let mut max_deviation = 0; for s1 in group.iter(&maindevice) { let diff = match s1 .register_read::(RegisterAddress::DcSystemTimeDifference) .await { Ok(value) => // The returned value is NOT in two's compliment, rather the upper bit specifies // whether the number in the remaining bits is odd or even, so we convert the // value to `i32` using that logic here. { let flag = 0b1u32 << 31; if value >= flag { // Strip off negative flag bit and negate value as normal -((value & !flag) as i32) } else { value as i32 } } Err(Error::WorkingCounter { .. }) => 0, Err(e) => return Err(e), }; max_deviation = max_deviation.max(diff as u32); } if now.elapsed() >= Duration::from_millis(1000) { now = Instant::now(); log::info!("--> Max deviation {} ns", max_deviation); // Less than 500ns max deviation as an example threshold. // // mentions less than 100us as a good enough value as well. if max_deviation < 500 { log::info!("Clocks settled after {} ms", start.elapsed().as_millis()); break; } } tick_interval.next().await; } log::info!("Alignment done"); // SubDevice clocks are aligned. We can turn DC on now. let group = group .configure_dc_sync( &maindevice, DcConfiguration { // Start SYNC0 100ms in the future start_delay: Duration::from_millis(100), // SYNC0 period should be the same as the process data loop in most cases sync0_period: TICK_INTERVAL, // Send process data half way through cycle sync0_shift: TICK_INTERVAL / 2, }, ) .await?; let group = group .into_safe_op(&maindevice) .await .expect("PRE-OP -> SAFE-OP"); log::info!("SAFE-OP"); // Request OP state without waiting for all SubDevices to reach it. Allows the immediate // start of the process data cycle, which is required when DC sync is used, otherwise // SubDevices never reach OP, most often timing out with a SyncManagerWatchdog error. let group = group .request_into_op(&maindevice) .await .expect("SAFE-OP -> OP"); log::info!("OP requested"); let op_request = Instant::now(); // Send PDI and check group state until all SubDevices enter OP state. At this point, we can // exit this loop and enter the main process data loop that does not have the state check // overhead present here. loop { let now = Instant::now(); let response @ TxRxResponse { working_counter: _wkc, extra: CycleInfo { next_cycle_wait, .. }, .. } = group.tx_rx_dc(&maindevice).await.expect("TX/RX"); if response.all_op() { break; } smol::Timer::at(now + next_cycle_wait).await; } log::info!( "All SubDevices entered OP in {} us", op_request.elapsed().as_micros() ); for subdevice in group.iter(&maindevice) { let io = subdevice.io_raw(); log::info!( "-> SubDevice {:#06x} {} inputs: {} bytes, outputs: {} bytes", subdevice.configured_address(), subdevice.name(), io.inputs().len(), io.outputs().len() ); } let term = Arc::new(AtomicBool::new(false)); signal_hook::flag::register(signal_hook::consts::SIGINT, Arc::clone(&term)) .expect("Register hook"); println!(); // Main application process data cycle loop { let now = Instant::now(); let TxRxResponse { working_counter: _wkc, extra: CycleInfo { next_cycle_wait, .. }, .. } = group.tx_rx_dc(&maindevice).await.expect("TX/RX"); smol::Timer::at(now + next_cycle_wait).await; if let Some(first_el3702) = group.iter(&maindevice).find(|sd| sd.name() == "EL3702") { let i = first_el3702.inputs_raw(); match EL3702::unpack_from_slice(&i) { Ok(inputs) => { print!("\r{:?}", inputs); } Err(e) => { log::error!("{} want {}, got {}", e, EL3702::PACKED_LEN, i.len()); } } } else { println!("ASs"); } // Hook signal so we can write CSV data before exiting if term.load(Ordering::Relaxed) { println!(); log::info!("Exiting..."); break; } } let group = group .into_safe_op(&maindevice) .await .expect("OP -> SAFE-OP"); log::info!("OP -> SAFE-OP"); let group = group .into_pre_op(&maindevice) .await .expect("SAFE-OP -> PRE-OP"); log::info!("SAFE-OP -> PRE-OP"); let _group = group.into_init(&maindevice).await.expect("PRE-OP -> INIT"); log::info!("PRE-OP -> INIT, shutdown complete"); Ok(()) }) }