【GPS授时系列】通过USB接口获取PPS信号

前言

本文翻译自Don Drawn的博客
https://blog.dan.drown.org/
斜体是译者加的注释,方便读者理解

目标

为了获取精确时间,最划算的做法是从GPS模块同步PPS信号(秒脉冲信号),这种模块一般只需要几十元钱,而精度却能达到100ns,从GPIO引脚获取PPS信号是比较常用的同步PPS信号的方法,但并不是所有系统都支持这种方法,USB接口是一种比GPIO使用更广泛的接口,但是用USB接口同步PPS信号会引入额外的误差,因为USB接口的特性,会给信号同步带来不小的延迟,本项目的目的,既是测量这个延迟,并且试图减小这个延迟。

评价时钟质量的几个指标:
分辨率(resolution):时钟每次自增量,如果时钟每次增加一秒,则分辨率为1秒
精度(precision ):时钟精度,来说是外部程序能获取时钟时间的最小间隔,如时钟的分辨率可能是1ns,但是时钟的精度可能是1ms
抖动(jitter):重复读取时钟值,每两次读取之间的间隔是不同的,两次读取之间的间隔的差值既是jitter
精准度(Accuracy ):时钟值与理想精确时间的差值
频率误差(frequency  error):时钟一般由某种频率发生器件驱动,这些器件频率会发生漂移,频率误差一般用PPM作为单位,PPM既是Part Per Million,如果时钟PPM为12,差不多一天下来积累的误差就有1秒

USB同步PPS信号原理图

从图中可以看出USB的轮询机制必然导致较大的延迟

USB同步PPS信号原理图

测试

测试中同一个PPS信号同时连接了USB和GPIO
GPIO同步PPS信号时,没有做额外的延迟补偿(这个延迟大概在10us左右)

PPS 信号产生IRQ

PPS信号产生IRQ请求的延迟
在这里插入图片描述
大概是 1.7us-1.8us

USB 延迟

图标中的纵坐标标识的是USB设备在收到PPS信号到USB host(既主机)获取到PPS信号之间的时间差。
在这次测试中,USB设备的轮询时间被设成1000us,USB host会在每个USB包之间轮询USB设备的状态,USB延迟就此产生。
在这里插入图片描述
USB延迟介于26us到1033us之间,稍后作者会详细讲解这个三角形的图像背后的原理。

误差修正值

测量值用UART串口传送

数据整合

主机收到测量值与PPS信号后,就可以计算本机时间与GPS时间的差值,这个差值叫偏移(offset),下面的图形是GPIO PPS信号与USB PPS信号的时间差记录,在理想情况下,这两条线都应该是一条平的直线,不过现实是USB PPS有一个125us的固有偏移,可能是由于USB hub 的缓冲机制带来的,固有偏移很容易处理,只要减掉就可以了,所以一下图形中作者已经将125us的固有偏移消除了,这种固有偏移在不同机器间也是相同的,作者测试过树莓派 Pi 2和一台intel的机器。

在上节中作者测量到USB延迟介于26us到1033us之间,但是这里USB PPS的偏移只有100多us,这里作者应该是使用了某种修正方法,利用UART传输测量值来修正延迟,但是即使如此,USB传输延迟中的抖动(jitter)依然存在。

在这里插入图片描述

GPIO PPS 与 USB PPS对比

USB PPS时间源能保持±5us的偏差,因此它是一个长期的,优质的时间源,但是对比GPIO PPS时间,依旧多了絮都尖峰,而且偏移持续时间也更长。
研究偏移时间的分布,作者认为它们都符合高斯分布(Gaussian distribution)。
在这里插入图片描述

GPIO PPS 信号更接近高斯分布,作者认为这是一种很好的测量短期偏移的方法。

扩展

此处未翻译

USB latency pattern

Looking closer at the USB latency, you can see the PPS drifting relative to the host schedule of polling the USB device for its status. The system clock error was 2.215ppm during this time period, and this drift matches that error exactly. This probably means USB on this system shares the same clock as the system clock. This hardware is a Raspberry Pi 2, and I suspect it won’t be true for other platforms. I also suspect the stability of the polling schedule can’t be relied upon, as the host controls it and can reschedule it whenever it wants.
在这里插入图片描述
Subtracting the 2.215ppm drift gives the remaining jitter (root mean squared error=2.278)
在这里插入图片描述

  • 9
    点赞
  • 57
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个简单的C语言例子,用于构建NTP响应包和处理耗时: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <sys/socket.h> #include <arpa/inet.h> #define NTP_TIMESTAMP_DELTA 2208988800ull // NTP时间戳与UNIX时间戳之间的差值 // NTP报文结构体 typedef struct { uint8_t li_vn_mode; // 2 bits li, 3 bits vn, 3 bits mode uint8_t stratum; // 系统时钟的层数 uint8_t poll; // 最大间隔 uint8_t precision; // 精度 uint32_t rootDelay; // 根延迟 uint32_t rootDispersion; // 根频偏 uint32_t refId; // 参考ID uint32_t refTm_s; // 参考时间 uint32_t refTm_f; // 参考时间 uint32_t origTm_s; // 发送时间 uint32_t origTm_f; // 发送时间 uint32_t rxTm_s; // 接收时间 uint32_t rxTm_f; // 接收时间 uint32_t txTm_s; // 发送时间 uint32_t txTm_f; // 发送时间 } ntp_packet; // 构建NTP响应包 void build_ntp_packet(ntp_packet *pkt, uint32_t receive_time, uint32_t transmit_time) { memset(pkt, 0, sizeof(ntp_packet)); pkt->li_vn_mode = (0x3 << 6) | (0x4 << 3) | 0x4; // 版本号为4,模式为服务器 pkt->stratum = 2; // 时钟层数 pkt->poll = 4; // 最大间隔 pkt->precision = -20; // 精度 pkt->rootDelay = 0x00010000; // 根延迟 pkt->rootDispersion = 0x00010000; // 根频偏 pkt->refId = 0x808a8c2c; // 参考ID为本地IP地址 pkt->refTm_s = receive_time; // 参考时间为接收时间 pkt->txTm_s = transmit_time; // 发送时间为当前时间 } int main() { int sockfd; struct sockaddr_in servaddr; char buf[sizeof(ntp_packet)]; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(123); // NTP服务端口号为123 inet_pton(AF_INET, "10.0.0.1", &servaddr.sin_addr); // NTP服务端IP地址 sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd < 0) { perror("socket"); exit(1); } // 构建NTP请求包 memset(buf, 0, sizeof(buf)); ((ntp_packet *)buf)->li_vn_mode = (0x3 << 6) | (0x4 << 3) | 0x3; // 版本号为4,模式为客户端 // 发送NTP请求包到NTP服务器 if (sendto(sockfd, buf, sizeof(ntp_packet), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("sendto"); exit(1); } // 接收NTP响应包 if (recv(sockfd, buf, sizeof(ntp_packet), 0) < 0) { perror("recv"); exit(1); } close(sockfd); // 解析NTP响应包 ntp_packet *pkt = (ntp_packet *)buf; uint32_t receive_time = ntohl(pkt->rxTm_s); uint32_t transmit_time = ntohl(pkt->txTm_s); uint32_t ntp_timestamp = ntohl(pkt->txTm_s); ntp_timestamp += ((uint64_t)ntohl(pkt->txTm_f) << 32) / (1LL << 32); // 转换NTP时间戳为UNIX时间戳 time_t unix_timestamp = ntp_timestamp - NTP_TIMESTAMP_DELTA; // 构建NTP响应包 build_ntp_packet(pkt, receive_time, transmit_time); // 发送NTP响应包到客户端 sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sockfd < 0) { perror("socket"); exit(1); } if (sendto(sockfd, buf, sizeof(ntp_packet), 0, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) { perror("sendto"); exit(1); } close(sockfd); // 计算耗时 uint32_t rtt = transmit_time - receive_time; uint32_t offset = (receive_time - ntp_timestamp + transmit_time - unix_timestamp) / 2; printf("Round-trip time: %d ms\n", rtt); printf("Offset: %d ms\n", offset); return 0; } ``` 这个例子首先通过UDP协议向NTP服务器发送NTP请求包,然后接收NTP响应包。接着,它解析NTP响应包中的时间戳,并将其转换为UNIX时间戳。接下来,它构建一个NTP响应包,并将其发送回客户端。最后,它计算往返时间和时钟偏差。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值