分布时钟(DC,Dsitributed Clock)可以使所有EtherCAT设备使用相同的系统时间,从而控制各设备任务的同步执行。
支持分布式时钟的从站称为DC从站,同步原理如下所示:
其中,
Tref: 参考时间,网络中第一个DC从站的时间
Tsys_local(n): 第n个从站的本地系统时间副本(同步后等于Tref),这个时间用来产生同步信号和锁存信号时间标记,供从站微处理器使用。
Tdleay(n): 从第一个DC从站到从站n的传输延时。
Toffset(n):从站n的时间和参考时间的初始偏移量。
其中,Toffset(n)和Tdelay(n)在初始化时由主站测量,并写入从站对应寄存器中。Tref在运行过程中由主站使用FRMW命令定期更新。
SOEM中配置同步时钟的步骤如下:
在/soem/EthercatConfig.c 的ecx_config_init(ecx_contextt *context, uint8 usetable)函数中,获取从站信息,判断从站是否为DC从站。
if ((etohs(ecx_FPRDw(context->port, configadr, ECT_REG_ESCSUP, EC_TIMEOUTRET3)) & 0x04) > 0) /* Support DC? */
{
context->slavelist[slave].hasdc = TRUE;
}
else
{
context->slavelist[slave].hasdc = FALSE;
}
在计算时间偏差Toffset(n)和传输延时之前,主站需要发送一个广播命令BWR,写所有从站端口0的接收时间寄存器0x0900,将所有从站捕捉数据帧第一个前导位到达每个端口的本地时间保存到寄存器0x0900~0x090F, 每个端口使用4个字节。
具体代码在/soem/Ethercatdc.c的函数ecx_configdc(ecx_contextt *context)中:
ecx_BWR(context->port, 0, ECT_REG_DCTIME0, sizeof(ht), &ht, EC_TIMEOUTRET); /* latch DCrecvTimeA of all slaves */
读取数据帧处理单元接收时间,与主站的系统时间比较,差值作为Toffset(n)写入从站寄存器0x0920. 计算代码为:
(void)ecx_FPRD(context->port, slaveh, ECT_REG_DCSOF, sizeof(hrt), &hrt, EC_TIMEOUTRET);//前导位到达的本地时间
/* use it as offset in order to set local time around 0 + mastertime */
hrt = htoell(-etohll(hrt) + mastertime64);//初始偏移量
/* save it in the offset register */
(void)ecx_FPWR(context->port, slaveh, ECT_REG_DCSYSOFFSET, sizeof(hrt), &hrt, EC_TIMEOUTRET);
SOEM只在初始化时使从站时间同步主站时间,若要运行后使主从时间同步,需要定期修正主站时间或从站时间 。
传输延时的计算比较复杂,与总线拓扑密切相关,计算原理图如下:
假设系统有5个从站,拓扑如上图所示,以计算从站4的传输延时Tdelay(4)为例。
Tdelay(4) = deltaT + Tdelay(2) + dt2 //deltaT为图中黄色粗箭头所示时间
而deltaT = (dt3-dt1)/2。 假设数据帧经过图中两段黄色粗箭头的时间相等。
dt3为数据帧从Slave2端口2离开到返回的时间,计算代码为:
dt3 = ecx_porttime(context, parent, context->slavelist[i].parentport) -
ecx_porttime(context, parent,
ecx_prevport(context, parent, context->slavelist[i].parentport));
dt1为从站4后续从站的传输时间,计算代码为:
/* current slave has children */
/* those childrens delays need to be substacted */
if (context->slavelist[i].topology > 1)
{
dt1 = ecx_porttime(context, i,
ecx_prevport(context, i, context->slavelist[i].entryport)) -
ecx_porttime(context, i, context->slavelist[i].entryport);
}
dt2为从站2其它接口所接从站的传输时间,计算代码为:
/* current slave is not the first child of parent */
/* previous childs delays need to be added */
if ((child - parent) > 1)
{
dt2 = ecx_porttime(context, parent,
ecx_prevport(context, parent, context->slavelist[i].parentport)) -
ecx_porttime(context, parent, context->slavelist[parent].entryport);
}
计算传输延时并写入从站寄存器0x0928,代码为:
/* calculate current slave delay from delta times */
/* assumption : forward delay equals return delay */
context->slavelist[i].pdelay = ((dt3 - dt1) / 2) + dt2 +
context->slavelist[parent].pdelay;
ht = htoel(context->slavelist[i].pdelay);
/* write propagation delay*/
(void)ecx_FPWR(context->port, slaveh, ECT_REG_DCSYSDELAY, sizeof(ht), &ht, EC_TIMEOUTRET);
在运行过程中,主站使用命令FRMW读取第一个DC从站的时间并写入到后续DC从站,达到时间同步的目的。
if(first)//第一个DC从站
{
context->DCl = sublength;
/* FPRMW in second datagram */
context->DCtO = ecx_adddatagram(context->port, &(context->port->txbuf[idx]), EC_CMD_FRMW, idx, FALSE,
context->slavelist[context->grouplist[group].DCnext].configadr,
ECT_REG_DCSYSTIME, sizeof(int64), context->DCtime);
first = FALSE;
}