RT-Thread : IEEE1588/PTP 协议的实现

 貌似简单的协议,实现却不易

           IEEE1588/PTP协议对于实时通信非常重要。打算在STM32H750 art PI 上测试一下,可惜RT-Thread OS 没有支持IEEE1588/PTP 协议,网络上介绍IEEE1588 PTP 协议的很多,但是真正在一个MCU 上实现的资料却很少。  于是自己动手移植一个,结果发现并不简单。

       记得几年前曾经在STM32F207 上使用以太网MAC 支持IEEE588 功能做过以太网层的时间同步测试。以为既然后续的STM32F4,H7 都支持MAC IEEEE1588 .大概会比较方便了。结果发现,不仅RT-Thread OS 没有支持IEEE1588/PTP 协议,就连STM32F4_HAL 库以及STM32H7XX_HAL库中的eth.c 都明确指出目前不支持IEEE1588/PTP.网络上的资料依然停留在STM32F107 没有新的内容。

    Github 上有一个STM32F4 的项目(https://github.com/mpthompson/stm32_ptpd)。不过它是在nucleo stm32F429 上实现的。使用的是CMSIS-RTOS,将其修改到RT-Thread H7 ,改动非常大,尝试了几天几乎要放弃了。

     在万般无奈之际,发现了这个项目

https://github.com/hasseb/stm32h7_atsame70_ptpd

  它是基于mpthompson 的项目基础上做的项目,这个项目的优点是

1 将STM32H7 的PTP 底层驱动直接地通过访问底层硬件寄存器完成,没有使用STM32H7xxx_HAL 库。避免了HAL 兼容性的问题。

2 软件以函数库的形式实现,避免了与OS 线程之间的不兼容性。

3 支持lwIP 2.0 ,使用UDP 组播方式(IGMP 协议) 实现PTPD 协议。

4 好像没有实现tick 定时器和TIM2 定时器调整。但是也有好处,避免了与RT-Thread OS 的兼容性问题。

PTP 协议

移植PTP 协议还是需要了解PTP 协议和PTPD 的程序结构。

PTP 报文

PTP协议定义了4种多点传送的报文类型和管理报文

-同步报文(Sync)

     由主设备发送给从设备(一到两秒发一次) , 消息中可以包含 Sync 发送时间标签 , 也可以在后续的Follow UP 消息中包含

-跟随报文(Follow_up)

用于传送Sync 消息的发送时间 

-延迟请求报文(Delay_Req)

-延迟应答报文(Delay_Resp)

一般报文和事件报文
      报文有一般报文和事件报文两种类型。跟随报文和延迟应答报文属于一般报文,一般报文本身不进行时戳处理,它可以携带事件报文的准确发送或接收时刻值信息。同步报文和延迟请求报文属于事件报文,事件报文是时间敏感消息,需要加盖精确的时间戳。

报文收发流程


 

1. 主时钟周期性的发出 sync 报文,并记录下 sync 报文离开主时钟的精确发送时间 t1;

(此处 sync 报文是周期性发出,可以携带或者不携带发送时间信息,因为就算携带也只能是预估发送时间戳 originTimeStamp)

2. 主时钟将精确发送时间 t1 封装到 Follow_up 报文中,发送给从时钟;

(由于 sync 报文不可能携带精确的报文离开时间,所以我们在之后的 Follow_up 报文中,将 sync 报文精确的发送时间戳  t1 封装起来,发给从时钟)

3. 从时钟记录 sync 报文到达从时钟的精确时到达时间 t2;

4. 从时钟发出 delay_req 报文并且记录下精确发送时间 t3;

5. 主时钟记录下 delay_req 报文到达主时钟的精确到达时间 t4;

6. 主时钟发出携带精确时间戳信息 t4 的 delay_resp 报文给从时钟;

 实现IEEE1588 的几种方法

IEEE1588 的基本思想是由一个主时钟设备周期性地发送时间标签,从时钟设备根据主时钟发来的时间标签来调整本地时钟,达到时间同步的目的。

  具体实现时,可以使用下面三种方式

1 软件方式

    在以太网的帧队列中插入时间标签.显然,由于软件执行的不确定性和队列中帧数量,网络负载等因素,会造成时间标签的不精准。

2 在MAC 控制器内实现

  当PTP 帧到达MAC 控制器时,由硬件插入一个时间标签。显然在MAC 控制器内部,有一个时间计数器。并且能够过滤PTP 帧。在STM32 等MCU 中表明支持IEEE1588 就是指MAC 控制器中具备这种功能。

3 在以太网物理层实现

      例如TI 公司的DP83640 芯片,据说这是精度最高的方式。但是需要软件访问Phy 器件的串行管理接口。DP83640 的另一个优点是它能够产生硬件时钟信号。可以直接产生硬件同步信号。

     STM32MCU 从STM32F107 到STM32H7 都支持IEEE1588 。许多年前,笔者曾经在Ethernet 底层测试果IEEE1588 的同步性能。事实上IEEE1588 /PTP 包括了两部分,一部分是底层硬件,另一部分是PTP 协议,它是一个基于UDP 的协议。

    不知道什么原因,ST 公司对IEEE1588 的技术支持并不好,连HAL 库中都没有支持PTP 。网络上也有人表示ST 的MAC IEEE1588 没什么用,有一些人主张使用外部DP83640 之类的方案。但是涉及到需要硬件设计。太麻烦了。我们决定还是试试基于MAC 控制器的方案。

移植的过程和遇到的问题

下载和编译源码

   该项目的文档非常简单。只能根据简单的文档自己摸索实现

  1. 项目在art pi 平台上实现。第一步新建一个art_pi_factory 项目
  2. 删除了wifi ,蓝牙部分
  3. 下载项目源代码,将其中ptp 目录拷贝到RT-Thread OS项目的Application 目录下。将port/stm2H7目录中的constants_dep.h,ptpd_dep.c和ptpd_dep.h三个文件拷贝到ptp/dep 中,其中constants_dep.h 覆盖了原来已有的文件。将port 目录中的congstants.h 拷贝到ptp目录中。
  4. 编译项目,会出现一堆没定义的,我在ptpd.h 中添加了#include “stm32h7.h”,另外删除了一些不用的语句。编译通过了。

编写主程序

令人遗憾的是该项目没有包含主程序。文档中只是提到:

软件初始化通过调用ptpd_init()实现,每1ms 调用PTPD timers ,比如

// ptpd timers
for (uint8_t i=0; i < TIMER_ARRAY_SIZE; i++)
{
    switch (ptpdTimersCounter[i])
    {
        case 0:
        break;
        case 1:
        ptpdTimersExpired[i] = TRUE;
        ptpdTimersCounter[i] = ptpdTimers[i];
        break;
        default :
        ptpdTimersCounter[i]--;
        break;
    }
}

连续地调用  ptpd_task()

每隔100ms 调用igmp_timer() ,我不知道这是什么意思,lwip 源代码中并没有这个函数。暂时放一放再说。

根据他的说明,我做了如下改动

1 在 timer.c 中添加了一个updateTime 函数:

void updateTimer(void){
    for (uint8_t i=0; i < TIMER_ARRAY_SIZE; i++)
      {
          switch (ptpdTimersCounter[i])
          {
              case 0:
              break;
              case 1:
              ptpdTimersExpired[i] = TRUE;
              ptpdTimersCounter[i] = ptpdTimers[i];
              break;
              default :
              ptpdTimersCounter[i]--;
              break;
          }
      }
}

主程序修改为:



#include <rtthread.h>
#include <rtdevice.h>
#include "drv_common.h"
#include <netdev_ipaddr.h>
#include <netdev.h>
#include <dfs_fs.h>
typedef struct
{
        int32_t seconds;
        int32_t nanoseconds;
} TimeInternal;
 
void ptpd_init();
void ptpd_task();
void updateTimer(void);
void getTime(TimeInternal *time);
#define DBG_COLOR
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#define PTPD_DBG
#define LWIP_PTP 1
#include <rtdbg.h>

int main(void)
{
   
    TimeInternal  currentTime;
    printf("IEEE1588/PTP Test\n");
     ptpd_init();

    while(1){
       ptpd_task();
        updateTimer();
    //    getTime(&currentTime);
    //    printf("seconds:%d\n",currentTime.seconds);
        rt_thread_mdelay(1);
    }
    return RT_EOK;
}

#include "stm32h7xx.h"
static int vtor_config(void)
{
    /* Vector Table Relocation in Internal QSPI_FLASH */
    SCB->VTOR = QSPI_BASE;
    return 0;
}
INIT_BOARD_EXPORT(vtor_config);

    运行后,会打印currentTime 。这个时钟应该是MAC 控制器的PTP 时间计数器中读出来的。说明底层的硬件接口是对的。

如何设置为Master Clock?

          slave clock 模式下什么都不发送,wirshark 看不出什么名堂,于是打算改成Master Clock,看看是否能够发出Sync 和 Follow 帧来。结果发现,网络上居然没有说明白如何配置成为Master Clock 模式。只能自己跟踪源代码,看看如何搞。

1   有人说只要将slave-only 修改为FALSE,就可以了。PTP 的参数设置是在ptp.c 的ptp_init()完成的。参数在constans.h 中配置。我做了下面的修改

/* features, only change to refelect changes in implementation */
#define NUMBER_PORTS      1
#define VERSION_PTP       2
#define BOUNDARY_CLOCK    TRUE
#define SLAVE_ONLY        FALSE
#define NO_ADJUST         FALSE

好像没有什么反应,通过分析代码发现要将state 设置为 PTP_MASTER,于是修改了ptpStartup 函数

int16_t ptpdStartup(PtpClock * ptpClock, RunTimeOpts *rtOpts, ForeignMasterRecord* foreign)
{
	ptpClock->rtOpts = rtOpts;
	ptpClock->foreignMasterDS.records = foreign;

	/* 9.2.2 */
	if (rtOpts->slaveOnly) rtOpts->clockQuality.clockClass = DEFAULT_CLOCK_CLASS_SLAVE_ONLY;

	/* No negative or zero attenuation */
	if (rtOpts->servo.ap < 1) rtOpts->servo.ap = 1;
	if (rtOpts->servo.ai < 1) rtOpts->servo.ai = 1;

	printf("event POWER UP\n");

	ETH_PTPStart(ETH_PTP_FineUpdate);

 	toState(ptpClock, PTP_INITIALIZING);
    doState(ptpClock);
 	toState(ptpClock, PTP_MASTER);
 	doState(ptpClock);
	return 0;
}

而且发现toState 后面要跟doState 才行。

又发现在 toState 函数中 delayMechanism 的判断中 E2E 是空的。再追踪到ptp_init 的最后

rtOpts.delayMechanism = DEFAULT_DELAY_MECHANISM;

这里的DEFAULT_DELAY_MECHANISM是E2E,于是改成了

rtOpts.delayMechanism =P2P;// DEFAULT_DELAY_MECHANISM;

当然,你也可以改DEFAULT_DELAY_MECHANISM;

再一次使用wirshark 监控,出现了Sync和Follow 帧,大约是每隔1秒出现一次。

Sync 帧

 follow 帧

修改 Sync和follow 帧的发送周期

SYNC报文由处于MASTER状态的时钟周期性的发送,间隔时间为1(秒) ×(2^portDS.logSyncInterval)。

通过constans.hz中的DEFAULT_SYNC_INTERVAL定义

#define DEFAULT_SYNC_INTERVAL         0 /* -7 in 802.1AS */ 

上面为 1 秒,如果改为 -1 便是1 秒,以此类推。

ubuntu OS 下的测试

下一步进行Ubuntu 与RT-Thread Slave 的测试。

这里要使用两个工具

ethtool和linuxPTP

linuxPTP 编译后 命令行是ptp4l

      首先遇到的问题是ethertools 检测我的PC 不支持硬件时间标签。我的以太网控制芯片是螃蟹,不是Intel 的,看来不行。不过ptp4l 好像支持软件时间标签。

ptp4l -i enp1s -m -S 

-S 表示软件时间标签。

不过好像运行起来与RT-Thread 的Master Clock 无法联通!不知道为啥出现这些

^C(base) yao@minipc:~$ sudo ptp4l -i enp1s0 -S -s -m
ptp4l[95.547]: port 1: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[95.547]: port 0: INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[103.048]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[109.373]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[115.664]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[122.787]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[129.956]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[137.784]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[144.257]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[150.536]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[158.146]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[165.345]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[172.264]: selected local clock 00e44f.fffe.6808ad as best master
ptp4l[179.509]: selected local clock 00e44f.fffe.6808ad as best master

PTP4L 的配置

·为了解决问题,使用wireshark 软件对PTP4L master clock 和RT-Thread 发出来的SYNC和Follw帧做了对比,发现transportSpecific 的号不一样。结果发现 PTP4L 是需要配置的。我的配置文件为

[global]
priority1		248
priority2		248
clockClass		248
transportSpecific	0x08
slaveOnly		1

修改之后,ptp4l能够发出Delay Request 帧了,但是RT-Thread 没有相应 Delay_response

RT-Thread 缺省模式不支持组播 

        又一个问题出现了,发现虽然RT-Thread 能够发送多播UDP ,但是无法接收到多播UDP。尝试了几乎一天时间才找到是这个问题!

发现了问题,解决方法就简单了

我在dvr_eth.c 的rt_stm32_eth_init 的函数中添加了一段(Add By yao 到 end yao之间)

/* EMAC initialization function */
static rt_err_t rt_stm32_eth_init(rt_device_t dev)
{
    ETH_MACConfigTypeDef MACConf;
    uint32_t regvalue = 0;
    uint8_t  status = RT_EOK;

    __HAL_RCC_D2SRAM3_CLK_ENABLE();

    phy_reset();

    /* ETHERNET Configuration */
    EthHandle.Instance = ETH;
    EthHandle.Init.MACAddr = (rt_uint8_t *)&stm32_eth_device.dev_addr[0];
    EthHandle.Init.MediaInterface = HAL_ETH_RMII_MODE;
    EthHandle.Init.TxDesc = DMATxDscrTab;
    EthHandle.Init.RxDesc = DMARxDscrTab;
    EthHandle.Init.RxBuffLen = ETH_MAX_PACKET_SIZE;

    SCB_InvalidateDCache();

    HAL_ETH_DeInit(&EthHandle);

    /* configure ethernet peripheral (GPIOs, clocks, MAC, DMA) */
    if (HAL_ETH_Init(&EthHandle) != HAL_OK)
    {
        LOG_E("eth hardware init failed");
    }
    else
    {
        LOG_D("eth hardware init success");
    }
/*   Add By yao */
    ETH_MACFilterConfigTypeDef pFilterConfig;
    HAL_ETH_GetMACFilterConfig(&EthHandle, &pFilterConfig);
    pFilterConfig.ReceiveAllMode =ENABLE;
    pFilterConfig.PassAllMulticast =ENABLE;
    HAL_ETH_SetMACFilterConfig(&EthHandle, &pFilterConfig);
 /*end By yao*/
    rt_memset(&TxConfig, 0, sizeof(ETH_TxPacketConfig));
    TxConfig.Attributes   = ETH_TX_PACKETS_FEATURES_CSUM | ETH_TX_PACKETS_FEATURES_CRCPAD;
    TxConfig.ChecksumCtrl = ETH_CHECKSUM_IPHDR_PAYLOAD_INSERT_PHDR_CALC;
    TxConfig.CRCPadCtrl   = ETH_CRC_PAD_INSERT;

    for (int idx = 0; idx < ETH_RX_DESC_CNT; idx++)
    {
        HAL_ETH_DescAssignMemory(&EthHandle, idx, &Rx_Buff[idx][0], NULL);
    }

    HAL_ETH_SetMDIOClockRange(&EthHandle);

     for(int i = 0; i <= PHY_ADDR; i ++)
     {
       if(HAL_ETH_ReadPHYRegister(&EthHandle, i, PHY_SPECIAL_MODES_REG, &regvalue) != HAL_OK)
       {
         status = RT_ERROR;
         /* Can't read from this device address continue with next address */
         continue;
       }

       if((regvalue & PHY_BASIC_STATUS_REG) == i)
       {
         PHY_ADDR = i;
         status = RT_EOK;
         LOG_D("Found a phy, address:0x%02X", PHY_ADDR);
         break;
       }
     }

     if(HAL_ETH_WritePHYRegister(&EthHandle, PHY_ADDR, PHY_BASIC_CONTROL_REG, PHY_RESET_MASK) == HAL_OK)
     {
         HAL_ETH_ReadPHYRegister(&EthHandle, PHY_ADDR, PHY_SPECIAL_MODES_REG, &regvalue);

         uint32_t tickstart = rt_tick_get();

         /* wait until software reset is done or timeout occured  */
         while(regvalue & PHY_RESET_MASK)
         {
           if((rt_tick_get() - tickstart) <= 500)
           {
             if(HAL_ETH_ReadPHYRegister(&EthHandle, PHY_ADDR, PHY_BASIC_CONTROL_REG, &regvalue) != HAL_OK)
             {
               status = RT_ERROR;
               break;
             }
           }
           else
           {
             status = RT_ETIMEOUT;
           }
         }
     }

    rt_thread_delay(2000);

     if(HAL_ETH_ReadPHYRegister(&EthHandle, PHY_ADDR, PHY_BASIC_CONTROL_REG, &regvalue) == HAL_OK)
     {
         regvalue |= PHY_AUTO_NEGOTIATION_MASK;
         HAL_ETH_WritePHYRegister(&EthHandle, PHY_ADDR, PHY_BASIC_CONTROL_REG, regvalue);

         eth_device_linkchange(&stm32_eth_device.parent, RT_TRUE);
         HAL_ETH_GetMACConfig(&EthHandle, &MACConf);
         MACConf.DuplexMode = ETH_FULLDUPLEX_MODE;
         MACConf.Speed = ETH_SPEED_100M;
         HAL_ETH_SetMACConfig(&EthHandle, &MACConf);

         HAL_ETH_Start_IT(&EthHandle);
     }
     else
     {
         status = RT_ERROR;
     }

    return status;
}

master offset 太大了

关掉 NTP

timedatectl set-ntp false
sudo systemctl stop systemd-timedated.service
sudo systemctl stop systemd-timesyncd

好像好了许多,us级别 

最近的进展 

     为了测试单片机端到端的同步效果,我又尝试将ptp 移植到了Nucleo F429 的开发板上。这花费了一些时间。首先是STM32F4 和STMH7 的hal 定义是不同的。需要将H7 的EtherType 定义改成STM32H4 的名称,还包括的位定义也需要修改。为了正确地修改,甚至看了STM32F 参考手册,一本5000多页的天书。

    成功以后,将stm32F429 作为Master Clock ,而STM32H750 art pi 作为slave 时钟。结果发现两个CPU 分别都能与ubuntu 的PTP4L 连接,但是他们之间无法进入Slave 状态。做了许多的尝试,几乎怀疑一切。后来断定可能是两个单片机的时钟误差太大了,于是将时间门限放到最大,结果进去了。时间差非常大。

  在这个期间,RT-thread OS 莫名其妙的地挂了几次(好像不能使用信号灯 sem take )。倒也是好事情,发现RT-Thread 的默认时钟是HSI,也就是使用内部时钟,单片机的内部时钟往往是使用半导体工艺实现的硅振荡器。是不准的。于是改成了HSE 模式。再次测试,以前的问题都不出现了。而且时钟差保持在50ppm ,已经是非常好了。要知道,普通的晶体振荡器也就是25ppm。

  今天,又找了一台HP 的原装小PC 。使用Ethertoo检测支持硬件时间戳。于是测试了一下。更满意。大约在40ppm 。非常稳定。

结论

       世间许多事,说说容易,做起来就难了。IEEE1588 协议与时间,网络,硬件有关。实现和移植起来还是比较麻烦的。而RT-Thread OS 的网络资料又比较少。所以,这件事比想象的更难一点。

  测试的结果表明,并不是网络上所说的,PHY 的IEEE1588 比MAC 内部的IEEE1588 好。在条件不高的场合使用STM32 内部的MAC IEEE1588 也是可以的,至少作为Slave Clock 是可行的,而大型系统中,可以使用一台高可靠性的IEEE 1588 Master。可以双冗余。也可以接GPS,北斗。

  下一步要测试在带有IEEE1588 的交换机中,多台设备的同步问题。开发完成了STM32 的IEEE1588 就好办了。要不然也没有条件去买一堆PC 来测试呀。市面上但凡带有IEEE1588 的开发板都很贵。自己做个廉价的板比较现实。

Guess you like

Origin blog.csdn.net/yaojiawan/article/details/121069338