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描述来获取时间戳或者修改时间戳了
typedefstruct{__IOuint32_tStatus;/*!< Status */uint32_tControlBufferSize;/*!< Control and Buffer1, Buffer2 lengths */uint32_tBuffer1Addr;/*!< Buffer1 address pointer */uint32_tBuffer2NextDescAddr;/*!< Buffer2 or next descriptor address pointer *//*!< Enhanced ETHERNET DMA PTP Descriptors */uint32_tExtendedStatus;/*!< Extended status for PTP receive descriptor */uint32_tReserved1;/*!< Reserved */uint32_tTimeStampLow;/*!< Time Stamp Low value for transmit and receive */uint32_tTimeStampHigh;/*!< Time Stamp High value for transmit and receive */}ETH_DMADescTypeDef;voidETH_EnablePTPTimeStampUpdate(void){uint32_ttmpreg;/* 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 */voidETH_InitializePTPTimeStamp(void){uint32_ttmpreg;/* 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.voidethernetif_get_tx_timestamp(structpbuf*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_tindex=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_tretry_count=10;(retry_count>0)&&((dma_tx_desc->StatusÐ_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ÐERNETIF_EVENT_TRANSMIT){break;}}// Make sure after waiting we have the timestamp information.if(dma_tx_desc->StatusÐ_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.voidHAL_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真正接收数据
/*从对应队列取出数据并处理*/staticssize_tptpd_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){caseANNOUNCE:handle_announce(ptp_clock,is_from_self);break;caseSYNC:handle_sync(ptp_clock,&time,is_from_self);break;caseFOLLOW_UP:handle_follow_up(ptp_clock,is_from_self);break;caseDELAY_REQ:handle_delay_req(ptp_clock,&time,is_from_self);break;casePDELAY_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 returned44handle:unpacked message type0handle_sync:received in state PTP_LISTENING handle_sync:disreguard handle:something handle:ptpd_net_recv_event returned0handle:ptpd_net_recv_general returned44handle:unpacked message type8handle_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 returned0handle:ptpd_net_recv_general returned64handle:unpacked message type11handle_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 returned0handle:ptpd_net_recv_general returned44handle:unpacked message type8handle_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 returned44handle:unpacked message type0handle_sync:received in state PTP_UNCALIBRATED handle:something handle:ptpd_net_recv_event returned0handle:ptpd_net_recv_general returned44handle:unpacked message type8handle_followup:received in state PTP_UNCALIBRATED ptpd_servo_update_offset ptpd_servo_update_offset:offset-1759720361seconds-340254876nanoseconds ptpd_servo_update_offset:cannot filter seconds PTPD:ptpd_servo_update_clock offset-1759720361sec340254876nsec ptpd_servo_init_clock PTPD:ptpd_servo_update_clock:one-way delayaveraged(E2E):0sec0nsec PTPD:ptpd_servo_update_clock:offset from master:-1759720361sec-340254876nsec PTPD:ptpd_servo_update_clock:observed drift:0handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:something handle:ptpd_net_recv_event returned0handle:ptpd_net_recv_general returned64handle:unpacked message type11handle_announce:received in state PTP_UNCALIBRATED event STATE_DECISION_EVENT recommending state PTP_SLAVE handle:something handle:ptpd_net_recv_event returned44handle:unpacked message type0handle_sync:received in state PTP_UNCALIBRATED handle:something handle:ptpd_net_recv_event returned0handle:ptpd_net_recv_general returned44handle:unpacked message type8handle_followup:received in state PTP_UNCALIBRATED ptpd_servo_update_offset ptpd_servo_update_offset:offset0seconds-76817nanoseconds PTPD:filter:-76817->-76817(0)PTPD:ptpd_servo_update_clock offset0sec76817nsec PTPD:ptpd_servo_update_clock:one-way delayaveraged(E2E):0sec0nsec PTPD:ptpd_servo_update_clock:offset from master:0sec-76817nsec PTPD:ptpd_servo_update_clock:observed drift:-4801event MASTER_CLOCK_SELECTED leaving state PTP_UNCALIBRATED entering state PTP_SLAVE PTPD:entering SLAVE statehandle:something handle:ptpd_net_recv_event returned0handle:ptpd_net_recv_general returned0handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:something handle:ptpd_net_recv_event returned44handle:unpacked message type0handle_sync:received in state PTP_SLAVE handle:something handle:ptpd_net_recv_event returned0handle:ptpd_net_recv_general returned44handle:unpacked message type8handle_followup:received in state PTP_SLAVE ptpd_servo_update_offset ptpd_servo_update_offset:offset0seconds-44237nanoseconds PTPD:filter:-44237->-60527(1)PTPD:ptpd_servo_update_clock offset0sec60527nsec PTPD:ptpd_servo_update_clock:one-way delayaveraged(E2E):0sec0nsec PTPD:ptpd_servo_update_clock:offset from master:0sec-60527nsec PTPD:ptpd_servo_update_clock:observed drift:-8583handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:nothing handle:something从结果来看,大概是几十微妙,emmm客观来说这个结果算不上太优秀,想要更请准的结果可以修改对应的宏
#defineDEFAULT_CALIBRATED_OFFSET_NS10000// Offset from master < 10us -> calibrated#defineDEFAULT_UNCALIBRATED_OFFSET_NS100000// Offset from master > 100us -> uncalibrated#defineMAX_ADJ_OFFSET_NS100000000// Max offset to try to adjust it < 100ms