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 ,改动非常大,尝试了几天几乎要放弃了。
在万般无奈之际,发现了这个项目
它是基于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 控制器的方案。
移植的过程和遇到的问题
下载和编译源码
该项目的文档非常简单。只能根据简单的文档自己摸索实现
- 项目在art pi 平台上实现。第一步新建一个art_pi_factory 项目
- 删除了wifi ,蓝牙部分
- 下载项目源代码,将其中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目录中。
- 编译项目,会出现一堆没定义的,我在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(¤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 时间计数器中读出来的。说明底层的硬件接口是对的。
如何设置为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;