FreeRTOS实现微秒级时间同步(基于1588V2)
1. 前言
主要是参考了该代码的实现,不过原代码的RTOS用的是RTX5,这里把它移植到了FreeRTOS上,并额外的做了一层抽象方便后续的移植。硬件:STM32F407ZGT6,软件环境: FreeRTOS,ubuntu18, keil5(ARMCC编译器)
原项目链接:
STM32_PTPD
也非常感谢这个博主的博文:
STM32F407移植1588V2
本项目链接:
FreeRTOS_PTPD
2. 项目的代码框架
在把原来的项目中的ptpd部分整合出来后,主要是以下几个文件
ethernetif.c
ethernetif.h
ethptp.c
ethptp.h
network.c
network.h
ptpd.h
ptpd_arch.c
ptpd_arch.h
ptpd_arith.c
ptpd_bmc.c
ptpd_constants.h
ptpd_datatypes.h
ptpd_main.c
ptpd_msg.c
ptpd_net.c
ptpd_protocol.c
ptpd_servo.c
ptpd_time.c
ptpd_timer.c
- ethernetif:链接lwip和以太网驱动的核心。在这里把以太网驱动也放到了这个文件中,以及和ptpd协议底层状态记录的相关函数
- ethptp: 主要是操作底层ETH外设用来设定时间戳和得到时间戳
- network: 更上层的实现,包括LWIP协议栈初始化的封装和ptpd协议初始化的封装,同时定义了一些需要传给LWIP初始化的参数,包括IP地址,MAC地址等
- lwip初始化
- ptpd协议相关的初始化
- mdns协议 / ping协议等辅助的初始化(mdns相关的文件在lwip/apps中)
- ptpd.h: 定义了一些DBG调试的宏
- ptpd_arch: 对ptpd协议栈用到了进程间通信的IPC的抽象,包括事件标志组,软件定时器等,方便后续适配不同的RTOS平台(个人新增)
- ptpd协议栈的代码:在4中进一步的介绍
3. PTPD的核心—时间戳
整个ptpd协议的核心是如何获取到报文发送的精确时间戳,这个不仅仅是协议层的,更需要硬件层提供支持
3.1 硬件层
这里时间戳依赖于硬件的MAC控制器,好在STM32F4底层的MAC控制器支持获取时间戳相关的参数。所以只需要我们在初始化的时候,做好对相关外设的初始化就可以通过ETH_DMA描述来获取时间戳或者修改时间戳了
typedef struct
{__IO uint32_t Status; /*!< Status */uint32_t ControlBufferSize; /*!< Control and Buffer1, Buffer2 lengths */uint32_t Buffer1Addr; /*!< Buffer1 address pointer */uint32_t Buffer2NextDescAddr; /*!< Buffer2 or next descriptor address pointer *//*!< Enhanced ETHERNET DMA PTP Descriptors */uint32_t ExtendedStatus; /*!< Extended status for PTP receive descriptor */uint32_t Reserved1; /*!< Reserved */uint32_t TimeStampLow; /*!< Time Stamp Low value for transmit and receive */uint32_t TimeStampHigh; /*!< Time Stamp High value for transmit and receive */} ETH_DMADescTypeDef;
void ETH_EnablePTPTimeStampUpdate(void)
{uint32_t tmpreg;/* Enable the PTP system time update with the Time Stamp Update register value */ETH->PTPTSCR |= ETH_PTPTSCR_TSSTU;/* Wait until the write operation will be taken into account :at least four TX_CLK/RX_CLK clock cycles */tmpreg = ETH->PTPTSCR;delay_us(ETH_REG_WRITE_DELAY);ETH->PTPTSCR = tmpreg;
}/*** @brief Initialize the PTP Time Stamp* @param None* @retval None*/
void ETH_InitializePTPTimeStamp(void)
{uint32_t tmpreg;/* Initialize the PTP Time Stamp */ETH->PTPTSCR |= ETH_PTPTSCR_TSSTI;/* Wait until the write operation will be taken into account :at least four TX_CLK/RX_CLK clock cycles */tmpreg = ETH->PTPTSCR;delay_us(ETH_REG_WRITE_DELAY);ETH->PTPTSCR = tmpreg;
}
另外,PTPD协议栈的实现依赖与mdns服务,所以我们需要配置底层的MAC控制器让它支持mdns协议
// Initialize custom MAC parameters.// NOTE: the MulticastFramesFilter is set to none for support of MDNS packets.
ETH_MACInitTypeDef mac_init;memset(&mac_init, 0, sizeof(mac_init));mac_init.Watchdog = ETH_WATCHDOG_ENABLE;mac_init.Jabber = ETH_JABBER_ENABLE;mac_init.InterFrameGap = ETH_INTERFRAMEGAP_96BIT;mac_init.CarrierSense = ETH_CARRIERSENCE_ENABLE;mac_init.ReceiveOwn = ETH_RECEIVEOWN_ENABLE;mac_init.LoopbackMode = ETH_LOOPBACKMODE_DISABLE;mac_init.ChecksumOffload = ETH_CHECKSUMOFFLAOD_ENABLE;mac_init.RetryTransmission = ETH_RETRYTRANSMISSION_DISABLE;mac_init.AutomaticPadCRCStrip = ETH_AUTOMATICPADCRCSTRIP_DISABLE;mac_init.BackOffLimit = ETH_BACKOFFLIMIT_10;mac_init.DeferralCheck = ETH_DEFFERRALCHECK_DISABLE;mac_init.ReceiveAll = ETH_RECEIVEAll_DISABLE;mac_init.SourceAddrFilter = ETH_SOURCEADDRFILTER_DISABLE;mac_init.PassControlFrames = ETH_PASSCONTROLFRAMES_BLOCKALL;mac_init.BroadcastFramesReception = ETH_BROADCASTFRAMESRECEPTION_ENABLE;mac_init.DestinationAddrFilter = ETH_DESTINATIONADDRFILTER_NORMAL;mac_init.PromiscuousMode = ETH_PROMISCUOUS_MODE_DISABLE;mac_init.MulticastFramesFilter = ETH_MULTICASTFRAMESFILTER_NONE;mac_init.UnicastFramesFilter = ETH_UNICASTFRAMESFILTER_PERFECT;mac_init.HashTableHigh = 0x0U;mac_init.HashTableLow = 0x0U;mac_init.PauseTime = 0x0U;mac_init.ZeroQuantaPause = ETH_ZEROQUANTAPAUSE_DISABLE;mac_init.PauseLowThreshold = ETH_PAUSELOWTHRESHOLD_MINUS4;mac_init.UnicastPauseFrameDetect = ETH_UNICASTPAUSEFRAMEDETECT_DISABLE;mac_init.ReceiveFlowControl = ETH_RECEIVEFLOWCONTROL_DISABLE;mac_init.TransmitFlowControl = ETH_TRANSMITFLOWCONTROL_DISABLE;mac_init.VLANTagComparison = ETH_VLANTAGCOMPARISON_16BIT;mac_init.VLANTagIdentifier = 0x0U;HAL_ETH_ConfigMAC(ðernetif_handle, &mac_init);
3.2 协议层
通过函数ethernetif_get_tx_timestamp可以得到具体的时间戳
// Get the TX time associated with the packet buffer.
void ethernetif_get_tx_timestamp(struct pbuf *p)
{// Lock the Ethernet mutex.sys_mutex_lock(ðernetif_mutex_id);// Start without a DMA TX descriptor.__IO ETH_DMADescTypeDef *dma_tx_desc = NULL;// Find the DMA TX descriptor assocated with this packet buffer.// This is a onetime function to prevent issues with accidently// pointing to a recycled DMA TX descriptor.uint32_t index = ethernet_tx_tail;while (index != ethernet_tx_head){// Is this the DMA TX descriptor we are interested in?if (p->time_nsec == ethernet_tx_entries[index].id){// Get the DMA TX descriptor for use below.dma_tx_desc = ethernet_tx_entries[index].tx_desc;// This is a one time operation so clear the entry.ethernet_tx_entries[index].id = 0;ethernet_tx_entries[index].tx_desc = NULL;// We found the DMA TX descriptor.break;}// Increment to the next TX buffer.index = (index + 1) == ETH_TXBUFNB ? 0 : index + 1;}// Release the Ethernet mutex.sys_mutex_unlock(ðernetif_mutex_id);// Fill in the default values.p->time_sec = 0;p->time_nsec = 0;// Did we find the dma tx descriptor?if (dma_tx_desc){// Wait up to 20 millisecond for the DMA transfer to complete.for (uint32_t retry_count = 10; (retry_count > 0) && ((dma_tx_desc->Status & ETH_DMATXDESC_TTSS) != ETH_DMATXDESC_TTSS); --retry_count){// Wait up to two milliseconds for a transfer to complete.EventBits_t xReceivedBits;xReceivedBits = sys_eventgroup_wait_bits(ðernetif_event_id, ETHERNETIF_EVENT_TRANSMIT, 2);if (xReceivedBits & ETHERNETIF_EVENT_TRANSMIT){break;}}// Make sure after waiting we have the timestamp information.if (dma_tx_desc->Status & ETH_DMATXDESC_TTSS ){// Fill in the timestamp information.p->time_sec = dma_tx_desc->TimeStampHigh;p->time_nsec = subsecond_to_nanosecond(dma_tx_desc->TimeStampLow);}else{// Report timeount.printf("ETHERNETIF: tx timestamp timeout\n");}}
}
得到时间戳的过程可以大概简化为:
- 底层调用netif->linkouput发送数据包,如果数据包是PTPD协议相关的,就记录以太网的描述符和pbuf之间的映射关系(这里对pbuf做了一定的修改,新增了时间戳变量成员)
- 当以太网发送完成后,会调用对应的回调函数,此时回调函数会对事件标志组进行置位操作
// Ethernet Tx transfer complete callback from the HAL.
void HAL_ETH_TxCpltCallback(ETH_HandleTypeDef *eth_handle)
{UNUSED(eth_handle);BaseType_t xHigherPriorityTaskWoken = pdFALSE;// Notify the Ethernet thread of the outgoing packet complete.xHigherPriorityTaskWoken = sys_eventgroup_set_bits_isr(ðernetif_event_id, ETHERNETIF_EVENT_TRANSMIT); if( xHigherPriorityTaskWoken != pdFALSE ){portYIELD_FROM_ISR( xHigherPriorityTaskWoken );}
}
- ethernetif_get_tx_timestamp就可以等到这个标志位置位,从而从对应的以太网描述符中取出时间进行记录
4.PTPD其它文件的简介
具体的实现我看的也不深,权当是做了个了解
- 4.1 ptpd_net.c : 在应用层与LWIP协议栈交互
-
ptpd_net_init函数
初始化两个消息队列 -
创建了两个udp控制块
- 事件端口的控制块 eventPcb: 319端口 控制事件消息(Sync同步 Delay_seq(延迟请求))
需要加时间戳的消息 - 通用端口控制块 generalPcb: 320端口 辅助同步的消息
不需要加时间戳的消息
- 事件端口的控制块 eventPcb: 319端口 控制事件消息(Sync同步 Delay_seq(延迟请求))
-
事件端口的接收回调 : ptpd_net_event_callback
把pbuf放入事件队列eventQ并唤醒处理线程 -
通用端口的接收回调 : ptpd_net_general_callback
把pbuf放入通用事件队列generalQ,并唤醒处理线程(ptpd_main当中) -
ptpd发送数据 ptpd_net_send函数:
通过对应的udp控制块发送数据 然后如果需要返回时间戳的会把具体发送时刻通过指针返回if (time != NULL) {// We have special call back into the Ethernet interface to fill the timestamp// of the buffer just transmitted. This call will block for up to a certain amount// of time before it may fail if a timestamp was not obtained.ethernetif_get_tx_timestamp(p); } // Get the timestamp of the sent buffer. We avoid overwriting // the time if it looks to be an invalid zero value. if ((time != NULL) && (p->time_sec != 0)) {time->seconds = p->time_sec;time->nanoseconds = p->time_nsec;DBGV("PTPD: %d sec %d nsec\n", time->seconds, time->nanoseconds); }
-
ptpd真正接收数据
/*从对应队列取出数据并处理*/ static ssize_t ptpd_net_recv(octet_t *buf, TimeInternal *time, BufQueue *queue)
把数据从queue中取出来 数据信息放在buf里 时间信息放在TimeInternal, 时间信息来自以太网的DMA描述符
-
- 4.2 ptpd_protocol.c: 协议实现的核心
- ptpd_protocol_to_state : 状态机状态切换
- ptpd_protocol_do_state: 处理事件 调用 do_state进行状态切换
- handle: 协议事件处理的核心
- step1: 把数据从消息队列取出来
- step2: switch - case 处理 : 根据不同报文的类型去做处理
switch (ptp_clock->msgTmpHeader.messageType) {case ANNOUNCE:handle_announce(ptp_clock, is_from_self);break;case SYNC:handle_sync(ptp_clock, &time, is_from_self);break;case FOLLOW_UP:handle_follow_up(ptp_clock, is_from_self);break;case DELAY_REQ:handle_delay_req(ptp_clock, &time, is_from_self);break;case PDELAY_REQ:handle_peer_delay_req(ptp_clock, &time, is_from_self);... }
- 4.3 ptpd_sero
ptpd的算法部分 包括如何计算同步时间 还有一些滤波操作在这里面- ptpd_aritch.c: 辅助数学函数
- ptpd_bmc.c : 最佳主时钟(BMC)算法核心 作为丛机用不到
- 4.4 ptpd_time 与 ethptp: ptpd算法得到时间后 对于底层的修正
修正以太网的ETH寄存器相关的标志位
ptpd_get_rand无用 正好也没随机数寄存器 删了就行 - 4.5 ptpd_msg
中间层 就是涉及到某些帧具体是怎么封装了(delay_req / Sync / follow_up) 需要适配的话得改这里—根据不同的实现进行修改 - 4.6 ptpd_timer.c:
涉及到一些ptpd协议中用到的软件定时器,是为了辅助前面提到的ptpd_protocol,进行一些超时的处理 - 4.7 ptpd_main.c:
没啥好讲的 就是调用封装的prptocol的API 以及提供一些对外的接口来得到ptpd协议栈的信息 - 为什么底层用udp不用tcp
可能是因为udp的mudp多播吧 把自己报文广播给特定的主机
包括udp的传输效率更高
5 使用与测试
首先要保证你的ubutnu和你的开发板在一个子网下
ubutun上要安装一个ptpd4l,然后启动协议,我这里是ens33网卡,根据具体情况进行修改
sudo ptp4l -E -4 -S -i ens33 -m
最终的结果为
handle: ptpd_net_recv_event returned 44
handle: unpacked message type 0
handle_sync: received in state PTP_LISTENING
handle_sync: disreguard
handle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 44
handle: unpacked message type 8
handle_followup: received in state PTP_LISTENING
handle_followup: disreguard
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 64
handle: unpacked message type 11
handle_announce: received in state PTP_LISTENING
handle_announce: from another foreign master
event STATE_DECISION_EVENT
recommending state PTP_SLAVE
leaving state PTP_LISTENING
ptpd_servo_init_clock
entering state PTP_UNCALIBRATED
PTPD: entering UNCALIBRATED statehandle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 44
handle: unpacked message type 8
handle_followup: received in state PTP_UNCALIBRATED
handle_followup: not waiting a message
event MASTER_CLOCK_CHANGED
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: something
handle: ptpd_net_recv_event returned 44
handle: unpacked message type 0
handle_sync: received in state PTP_UNCALIBRATED
handle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 44
handle: unpacked message type 8
handle_followup: received in state PTP_UNCALIBRATED
ptpd_servo_update_offset
ptpd_servo_update_offset: offset -1759720361 seconds -340254876 nanoseconds
ptpd_servo_update_offset: cannot filter seconds
PTPD: ptpd_servo_update_clock offset -1759720361 sec 340254876 nsec
ptpd_servo_init_clock
PTPD: ptpd_servo_update_clock: one-way delay averaged (E2E): 0 sec 0 nsec
PTPD: ptpd_servo_update_clock: offset from master: -1759720361 sec -340254876 nsec
PTPD: ptpd_servo_update_clock: observed drift: 0
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 64
handle: unpacked message type 11
handle_announce: received in state PTP_UNCALIBRATED
event STATE_DECISION_EVENT
recommending state PTP_SLAVE
handle: something
handle: ptpd_net_recv_event returned 44
handle: unpacked message type 0
handle_sync: received in state PTP_UNCALIBRATED
handle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 44
handle: unpacked message type 8
handle_followup: received in state PTP_UNCALIBRATED
ptpd_servo_update_offset
ptpd_servo_update_offset: offset 0 seconds -76817 nanoseconds
PTPD: filter: -76817 -> -76817 (0)
PTPD: ptpd_servo_update_clock offset 0 sec 76817 nsec
PTPD: ptpd_servo_update_clock: one-way delay averaged (E2E): 0 sec 0 nsec
PTPD: ptpd_servo_update_clock: offset from master: 0 sec -76817 nsec
PTPD: ptpd_servo_update_clock: observed drift: -4801
event MASTER_CLOCK_SELECTED
leaving state PTP_UNCALIBRATED
entering state PTP_SLAVE
PTPD: entering SLAVE statehandle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 0
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: something
handle: ptpd_net_recv_event returned 44
handle: unpacked message type 0
handle_sync: received in state PTP_SLAVE
handle: something
handle: ptpd_net_recv_event returned 0
handle: ptpd_net_recv_general returned 44
handle: unpacked message type 8
handle_followup: received in state PTP_SLAVE
ptpd_servo_update_offset
ptpd_servo_update_offset: offset 0 seconds -44237 nanoseconds
PTPD: filter: -44237 -> -60527 (1)
PTPD: ptpd_servo_update_clock offset 0 sec 60527 nsec
PTPD: ptpd_servo_update_clock: one-way delay averaged (E2E): 0 sec 0 nsec
PTPD: ptpd_servo_update_clock: offset from master: 0 sec -60527 nsec
PTPD: ptpd_servo_update_clock: observed drift: -8583
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: nothing
handle: something
从结果来看,大概是几十微妙,emmm客观来说这个结果算不上太优秀,想要更请准的结果可以修改对应的宏
#define DEFAULT_CALIBRATED_OFFSET_NS 10000 // Offset from master < 10us -> calibrated
#define DEFAULT_UNCALIBRATED_OFFSET_NS 100000 // Offset from master > 100us -> uncalibrated
#define MAX_ADJ_OFFSET_NS 100000000 // Max offset to try to adjust it < 100ms