etherage/
clock.rs

1/*!
2    Implementation of clock synchonization between master and slaves.
3
4    Clock synchronization in the ethercat protocol serves two different purposes
5
6    - slave task synchronization
7
8        The slaves whose clocks are synchronized will be able to run their realtime tasks with the same time reference (like applying new commands at the same time, or using the same durations), and at the same rate as the master sends order.
9
10    - timestamps synchronization
11
12        The slaves whose clocks are synchronized will progress at the same rate (slight differences can be found due to synchronization jitter, but it will remain small), so data retreived from slaves will have been measured at the same time and to retreive one only timestamp per frame will be sufficient for these slaves.
13
14    All this is described in ETG.1000.4 + ETG.1000.6 and ETG.1020.21
15
16    ## synchronization modes
17
18    There is 3 modes of slave task synchronization (ETG.1020.21.1.1):
19
20    - **free run**
21        slaves tasks are not synchronized to ethercat. This mode is when no clock is initialized
22    - **SM-synchronous**
23        slaves tasks are triggered by an ethercat sending
24    - **DC-synchronous**
25        slaves tasks are triggered by their clock synchronized with other slaves and the master. This mode is implemented in [DistributedClock]
26        according to ETG.1020, this mode is required only for the application that require high precision (<ms) in operation.
27        
28        Multiple mode of DC synchronous for DC unit in slave are available. The default one used only the sync_0 impulse to trigger based time. (see [this](https://raw.githubusercontent.com/jimy-byerley/etherage/master/schemes/synchronization-DC-submodes.svg) schematic to get more information)
29
30
31    ![synchronization modes](https://raw.githubusercontent.com/jimy-byerley/etherage/master/schemes/synchronization-modes.svg)
32
33    Depending on the synchronization mode, you can expect different execution behavior on your slaves, whose importance higher with the number of slaves. The following chronogram shows typical scheduling of task executions.
34
35    ![synchronization of slaves](https://raw.githubusercontent.com/jimy-byerley/etherage/master/schemes/synchronization-slaves.svg)
36
37    ## roles and responsibilities in the ethercat network
38
39    Since the master can be connected to the ethercat segment using less reliable hardware, its clock cannot be used to synchronize slaves. Instead, the first slave supporting DC (distributed clock) is used as reference clock (the first slave is the called *referent*).
40    the reference clock time is used to monitor the jitter between master and referent, and the jitter between all slaves.
41
42    In case of hotplug, or any change in the transmission delays in the segment, the clock must be reinitialized.
43*/
44
45use crate::{
46    data::PduData,
47    registers,
48    rawmaster::{RawMaster, PduCommand, SlaveAddress},
49    error::{EthercatError, EthercatResult}, 
50    };
51use std::{
52    collections::HashMap,
53    time::{SystemTime, Instant, Duration},
54    sync::Arc,
55    };
56use core::sync::atomic::{AtomicI64, Ordering::*};
57
58use futures_concurrency::future::Join;
59
60
61
62
63
64/**
65    implementation of the Distributed Clock (DC) at the master level.
66
67    The time offsets and delays measured by this clock synchronization mode are shows in the following chronogram for one packet sending.
68
69    ![clock offsets references](https://raw.githubusercontent.com/jimy-byerley/etherage/master/schemes/clock-references.svg)
70*/
71pub struct DistributedClock {
72    // Raw master reference
73    master: Arc<RawMaster>,
74    
75    /// start instant used as master's clock, it serves as monotonic clock for all offset computations to guarantee the synchronization success
76    start: Instant,
77    /// system clock when the clock has been initialized, it serves as reference to return an offset between slaves clock and system clock. If the system clock has not been monotonic all the time since the initialization of this instance, any offset from slave to master will not mean anything to the user
78    epoch: SystemTime,
79    /// offset from master clock to system clock
80    offset: AtomicI64,
81    /// transmission delay from master to reference slave
82    delay: u32,
83    
84    /// topological index of clock reference slave
85    referent: usize,
86    /// per-slave variables, slaves are indexed by topological order
87    slaves: Vec<ClockSlave>,
88    /// topological position of slaves indexed by fixed address (for user needs)
89    index: HashMap<SlaveAddress, usize>,
90}
91#[derive(Debug)]
92struct ClockSlave {
93	/// fixed address of slave
94	address: SlaveAddress,
95	/// whether the slave supports DC
96	enabled: bool,
97	/// topological index of slaves connected to each port
98	topology: [Option<usize>; 4],
99	/// offset from local time to system clock
100	offset: i64,
101	/// transmission delay from clock reference slave to present slave
102	delay: u32,
103}
104
105
106type DLSlave = Vec<(u16, registers::DLInformation, registers::DLStatus)>;
107
108
109impl DistributedClock {
110	/**
111		initialize the distributed clock on the ethercat segment
112		
113		Since the slaves responsibilities in the distributed clock only depend from the topology of the network, this initializer will automatically detect the topology and act accordingly
114		
115		## parameters
116		
117		- `delays_samples`: number of clock sampling used in estimating the propagation delays between slaves (defaults to `8`)
118		- `offsets_samples`: number of clock sampling used in estimating the offsets between slaves clocks (defaults to `15_000`)
119	*/
120	pub async fn new(
121			master: Arc<RawMaster>,
122			delays_samples: Option<usize>,
123			offsets_samples: Option<usize>,
124			) -> EthercatResult<Self> {
125		// create struct
126		let mut clock = Self {
127			master,
128            
129            start: Instant::now(),
130            epoch: SystemTime::now(),
131            offset: AtomicI64::new(0),
132            delay: 0,
133            
134            referent: 0,
135            slaves: Vec::new(),
136            index: HashMap::new(),
137			};
138		
139        // according to the absent details from the docs, this is enabling dynamic drift using the reference slave as master clock
140        clock.master.bwr(registers::dc::param_2, dc_control_loop::PARAM_2_OMRON).await;
141        // according to the absent details from the docs, this is reseting the drift compensation
142        clock.master.bwr(registers::dc::param_0, dc_control_loop::PARAM_0_RESET).await;
143			
144		let infos = clock.init_slaves().await?;
145		clock.init_topology(&infos).await?;
146		clock.init_delays(&infos, delays_samples.unwrap_or(8)).await?;
147		clock.init_offsets(offsets_samples.unwrap_or(15_000)).await?;
148		
149		Ok(clock)
150	}
151	
152	async fn init_slaves(&mut self) -> EthercatResult<DLSlave> {
153		// check number of slaves
154        let support = self.master.brd(registers::dl::information).await;
155        if support.answers == 0 || ! support.value()?.dc_supported()
156            {return Err(EthercatError::Master("no slave supporting clock"))}
157		
158		// retreive informations about all slaves in the network
159		let master = self.master.as_ref();
160		let infos = (0 .. support.answers).map(|slave| async move {
161				let (address, support, status) = (
162					master.aprd(slave, registers::address::fixed),
163					master.aprd(slave, registers::dl::information),
164					master.aprd(slave, registers::dl::status),
165					).join().await;
166				Ok((address.one()?, support.one()?, status.one()?))
167			})
168			.collect::<Vec<_>>()
169			.join().await
170			.drain(..).collect::<EthercatResult<Vec<_>>>()?;
171		
172		// check addresses and dc-enabled slaves
173		self.slaves = infos.iter().enumerate()
174			.map(|(index, (fixed, information, _))| ClockSlave {
175				address: 
176					if *fixed == 0  {SlaveAddress::AutoIncremented(index as _)}
177					else           {SlaveAddress::Fixed(*fixed)},
178				enabled: 
179					information.dc_supported(),
180				topology: [None; 4],
181				offset: 0,
182				delay: 0,
183			})
184			.collect::<Vec<_>>();
185		
186		// the reference slave must be the first supporting clock in the network
187		self.referent = (0 .. self.slaves.len())
188			.find(|index|  self.slaves[*index].enabled)
189			.ok_or(EthercatError::Protocol("cannot find first slave supporting clock"))?;
190		
191		self.index = HashMap::from_iter(self.slaves
192				.iter().enumerate()
193				.map(|(index, slave)|  (slave.address, index))
194				);
195		
196		Ok(infos)
197	}
198	
199	/// build topology
200	async fn init_topology(&mut self, infos: &DLSlave) -> EthercatResult {
201		let mut stack = Vec::<usize>::new();
202		for index in 0 .. infos.len() {
203			if index == 0 {
204				self.slaves[index].topology[0] = Some(0);
205			}
206			else {
207				let (parent, port) = loop {
208					let Some(&parent) = stack.last()
209						else {return Err(EthercatError::Protocol("topology identification failed due to wrong slave port activation"))};
210					if let Some(port) = (0 .. self.slaves[parent].topology.len())
211							.find(|&port|  infos[parent].2.port_link_status_at(port) && self.slaves[parent].topology[port].is_none()) 
212						{break (parent, port)}
213					stack.pop();
214				};
215				self.slaves[parent].topology[port] = Some(index);
216				self.slaves[index].topology[0] = Some(parent);
217			}
218			stack.push(index);
219		}
220		// TODO: check that topological position of non-dc-enabled slaves do not compromise the clock work
221		Ok(())
222	}
223	/// compute delays
224	async fn init_delays(&mut self, infos: &DLSlave, samples: usize) -> EthercatResult {
225		// get samples
226		let mut stamps = vec![[0; 4]; infos.len()*samples];
227		let mut master = vec![[0; 2]; samples];
228		for i in 0 .. samples {
229			// sample the master time so its delay to the reference can be computed
230			master[i][0] = self.reduced();
231			self.master.bwr(registers::dc::measure_time, 0).await;
232			master[i][1] = self.reduced();
233			
234			let master = self.master.as_ref();
235			for (index, times) in self.slaves.iter()
236				.enumerate()
237				.filter(|(_, slave)|  slave.enabled)
238				.map(|(index, slave)| async move {
239					(index, master.read(slave.address, registers::dc::received_time).await)
240					})
241				.collect::<Vec<_>>()
242				.join().await
243			{
244				stamps[i + index*samples] = times.one()?;
245			}
246		}
247		
248		// mean samples
249		// compute master delay to reference
250		let mut transitions: u64 = 0;
251		for i in 0 .. samples {
252			let child = &stamps[i + self.referent*samples];
253			let child_before = 0;
254			let child_after = self.slaves[self.referent].topology.iter().enumerate().rev()
255				.find(|(_, &next)|  next.is_some()).unwrap().0;
256			
257			let transition = master[i][1].wrapping_sub(master[i][0]) 
258						- u64::from(child[child_after].wrapping_sub(child[child_before]));
259			transitions += transition;
260		}
261		self.delay = u32::try_from( transitions / (2*(samples as u64)) ).unwrap();
262		
263		// compute slaves delay to master
264		for index in 1 .. self.slaves.len() {
265			// TODO: check whether dc is supported and account for a null delay otherwise
266			
267			let parent = self.slaves[index].topology[0].unwrap();
268			
269			// find enclosing timestamps (activated ports) in parent and child
270			let parent_after = self.slaves[parent].topology.iter().enumerate()
271				.find(|(_, &next)|  next == Some(index)).unwrap().0;
272			let parent_before = self.slaves[parent].topology[0 .. parent_after].iter().enumerate().rev()
273				.find(|(_, &next)|  next.is_some()).unwrap().0;
274				
275			let child_before = 0;
276			let child_after = self.slaves[index].topology.iter().enumerate().rev()
277				.find(|(_, &next)|  next.is_some()).unwrap().0;
278			
279			// sum of transition delays times from parent to child
280			let mut transitions: u64 = 0;
281			// sum of branchs delays from parent port 0 to parent slave port
282			let mut ports: u64 = 0;
283			
284			for i in 0 .. samples {
285				let child = &stamps[i + index*samples];
286				let parent = &stamps[i + parent*samples];
287				let transition = parent[parent_after].wrapping_sub(parent[parent_before])
288								 - child[child_after].wrapping_sub(child[child_before]);
289				let port = parent[parent_before].wrapping_sub(parent[0]);
290				// TODO: use intermediate sums for increase the tolerated delay from 4s in total to 4s per branch
291				// TODO: take into account that the slaves clocks might be 32bits using [DLInformaton::dc_range]
292				// summation is exact since we are using integers
293				transitions += u64::from(transition);
294				ports += u64::from(port);
295			}
296			
297			self.slaves[index].delay = self.slaves[parent].delay + u32::try_from(
298											transitions / (2*(samples as u64)) + ports / (samples as u64)
299											).unwrap();
300		}
301		// send delays
302		self.slaves.iter().map(|slave| async {
303				self.master.write(slave.address, registers::dc::system_delay, slave.delay).await.one()
304			})
305			.collect::<Vec<_>>()
306			.join().await
307			.drain(..).collect::<EthercatResult>()?;
308			
309		Ok(())
310	}
311	
312	/// compute offsets (static drift compensation)
313	async fn init_offsets(&mut self, samples: usize) -> EthercatResult {
314		// we will need an immutable reference to self while modifying the indivudual slave structs. This is safe because we will not access these structs concurrently and will not use methods of self that need them
315		let clock = self as *mut Self;
316		
317		// approximate offset first to get the most significant digits because divergence measurement is only 32 bits
318		self.slaves.iter_mut()
319			.filter(|slave|  slave.enabled)
320			.map(|slave| async move {
321				let clock = unsafe {&*clock};
322				let remote = clock.master.read(slave.address, registers::dc::local_time).await.one()?;
323				let local = clock.reduced();
324				let offset = local.wrapping_sub(remote);
325				clock.master.write(
326					slave.address, 
327					registers::dc::system_offset, 
328					offset,
329					).await.one()?;
330				slave.offset = i64::from_ne_bytes(offset.to_ne_bytes());
331				Ok(())
332			})
333			.collect::<Vec<_>>()
334			.join().await
335			.drain(..).collect::<EthercatResult>()?;
336		
337		// send many samples of system time (master time), the slave will mean it
338        for _ in 0 .. samples {
339            self.sync().await;
340        }
341        // retreive divergence and correct offsets
342        self.slaves.iter_mut()
343			.filter(|slave|  slave.enabled)
344			.map(|slave| async move {
345				let clock = unsafe {&*clock};
346				slave.offset += i64::from(i32::from(clock.master.read(slave.address, registers::dc::system_difference).await.one()?));
347				clock.master.write(
348					slave.address, 
349					registers::dc::system_offset, 
350					u64::from_ne_bytes(slave.offset.to_ne_bytes()),
351					).await.one()?;
352				EthercatResult::<(), ()>::Ok(())
353			})
354			.collect::<Vec<_>>()
355			.join().await 
356			.drain(..).collect::<EthercatResult>()?;
357		Ok(())
358	}
359	
360	/// getters
361	
362    /// return the slave address of the slave used as reference clock. This slave is called referent, or reference slave.
363    pub fn referent(&self) -> SlaveAddress  {
364        self.slaves[self.referent].address
365    }
366    /// return the (estimated) current time on the reference clock
367    pub fn system(&self) -> i128  {
368        i128::try_from(self.start.elapsed().as_nanos()).unwrap()
369			+ i128::from(self.offset.load(SeqCst))
370        // TODO: this clock is 64bits on the slaves, so should the master clock be. The epoch shall be changed when the clock overflows
371    }
372	
373	/// like [Self::system] operating system clock wrapped to 64 bit according to ETG
374	fn reduced(&self) -> u64 {
375        u64::try_from( self.start.elapsed().as_nanos() % u128::from(u64::MAX) ).unwrap()
376	}
377	
378    
379    /** 
380		offset between the ethercat system clock (arbitrarily zeroed) and unix epoch.
381		In this implementation, the ethercat system clock zero is when this struct is initialized
382	*/
383    pub fn epoch(&self) -> i128 {
384        self.epoch.duration_since(SystemTime::UNIX_EPOCH).unwrap()
385			.as_nanos()
386			.try_into().unwrap()
387    }
388
389    /// time offset from given slave clock to the reference clock
390    pub fn offset(&self, slave: SlaveAddress) -> i128   {
391        self.slaves[self.index[&slave]].offset.into()
392    }
393    /// return the transmission delay from the reference slave to the given slave
394    pub fn delay(&self, slave: SlaveAddress) -> i128  {
395        self.slaves[self.index[&slave]].delay.into()
396    }
397    
398    /// time offset from the reference clock to the master clock
399	pub fn offset_master(&self) -> i128 {
400		self.offset.load(SeqCst).into()
401	}
402	/// return the transmission delay from the master to the reference slave
403	pub fn delay_master(&self) -> i128 {
404		self.delay.into()
405	}
406    
407
408    /**
409        distributed clock synchronisation step. It must be called periodically to save the distributed clock from divergence
410    */
411    pub async fn sync(&self) {
412		// send RMW to update system time on slaves
413		let referent = self.referent();
414		let command = match referent {
415			SlaveAddress::AutoIncremented(_) => PduCommand::ARMW,
416			SlaveAddress::Fixed(_) => PduCommand::FRMW,
417			_ => unreachable!(),
418		};
419		let mut buffer = (0u64).packed().unwrap();
420		let sent = self.reduced();
421		let received = {
422			let mut command = self.master.topic(
423				command,
424				referent,
425				registers::dc::system_time.byte as u32,
426				&mut buffer,
427				).await;
428			command.send(None).await;
429			self.master.flush();
430			command.wait().await;
431			command.receive(None).answers
432			};
433		// update master offset to update system time on master
434		if received != 0 {
435			let div = 512;
436			let offset = (u64::unpack(&buffer).unwrap())
437							.wrapping_sub(sent + u64::from(self.slaves[self.referent].delay));
438			self.offset.store(i64::try_from((
439					(div-1) * i128::from(self.offset.load(Relaxed))
440					+ 1 * i128::from(i64::from_ne_bytes(offset.to_ne_bytes()))
441					)/div ).unwrap(), SeqCst);
442		}
443		// we don't care if packet is lost, so no error checking here, it will not bother slaves
444	}
445	
446    /**
447        distributed clock synchronisation task. Using cycle time as execution period
448
449        Once the automatic control is start, time cannot period cannot be changed.
450        Start cyclic time correction only with conitnuous drift flag set.
451
452        One task only should run this function at the same time. It will be calling [Self::sync]
453    */
454	pub async fn sync_loop(&self, period: Duration) {
455        use futures::stream::StreamExt;
456        let mut interval = tokio_timerfd::Interval::new_interval(period).unwrap();
457        
458        loop {
459			interval.next().await.unwrap().unwrap();
460			self.sync().await;
461		}
462	}
463}
464
465#[allow(unused)]
466mod dc_control_loop {
467	/// Value required to enable the DC clock - see ETG.1020.22.2.4
468	pub const PARAM_0_RESET: u16 = 0x1000;
469	/// this value disables the dynamic drift
470	pub const PARAM_2_DISABLED: u16 = u16::from_le_bytes([0, 0]);
471	/// omron values for enabling dynamic drift
472	pub const PARAM_2_OMRON: u16 = u16::from_le_bytes([0, 12]);
473	/// this value enables the dynamic drift using the reference slave clock as master clock
474	pub const PARAM_2_REFERENCE_MASTER: u16 = u16::from_le_bytes([4, 12]);
475	/// this value enables the dynamic drift by adjusting the reference slave clock to a grand master clock
476	pub const PARAM_2_GRAND_MASTER:  u16 = u16::from_le_bytes([4, 0]);
477}