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 协议和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 的基本思想是由一个主时钟设备周期性地发送时间标签,从时钟设备根据主时钟发来的时间标签来调整本地时钟,达到时间同步的目的。
具体实现时,可以使用下面三种方式
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 控制器的方案。
该项目的文档非常简单。只能根据简单的文档自己摸索实现
令人遗憾的是该项目没有包含主程序。文档中只是提到:
软件初始化通过调用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
#include
#include "drv_common.h"
#include
#include
#include
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
int main(void)
{
TimeInternal currentTime;
printf("IEEE1588/PTP Test\n");
ptpd_init();
while(1){
ptpd_task();
updateTimer();
// getTime(¤tTime);
// 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 时间计数器中读出来的。说明底层的硬件接口是对的。
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 与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
·为了解决问题,使用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 能够发送多播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, ®value) != 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, ®value);
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, ®value) != 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, ®value) == 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;
}
关掉 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 的开发板都很贵。自己做个廉价的板比较现实。