时间究竟是什么?这既可以是一个哲学问题,也可以是一个物理问题。古人对太阳进行观测,利用太阳的投影发明了日晷,定义了最初的时间。随着科技的发展,天文观测的精度也越来越准确,人们发现地球的自转并不是完全一致的,这就导致每天经过的时间是不一样的。这点误差对于基本生活基本没有影响,但是对于股票交易、火箭发射等等要求高精度时间的场景就无法忍受了。科学家们开始把观测转移到了微观世界,找到了一种运动高度稳定的原子——铯,最终定义出了准确的时间:铯原子电子跃迁9192631770个周期所持续的时间长度定义为1秒。基于这个定义制造出了高度稳定的原子钟。
时间在计算机中又是如何定义的呢?通常使用Unix时间戳进行表示,记录的是自公元1970年1月1日0时0分0秒以来的秒数。计算机为了维持时钟的走时,硬件层面使用晶体振荡器保障时钟的精确性,操作系统层面使用时钟中断去更新时间的流逝。现代计算机的硬件设计通常有独立的时钟,这源于Intel和微软创立的标准HighPrecisionEventTimer,标准指定了10MHz的时钟速度,因此时钟可以获得100纳秒的分辨率。这也是.NET时间有关的类型中Ticks属性的由来,1秒=10000000Ticks。虽然计算机的时钟已经足够精准,但也会受到环境温度的影响造成过快或者过慢的问题。为了对计算机的时钟进行校准,通常使用NTP协议与网络中的时间服务器进行同步。时间服务器的时间又会使用GPS接收机、无线电或者是原子钟进行校准。
本文将从GPS时间的获取、NTP报文的编写实现一个“玩具”级别的时间同步服务器,使用.NET6编写一个控制台应用程序,通过本文你可以学到:
串口SerialPort类的使用;使用Socket类实现UDP的监听与回复;在程序中使用Process类执行命令行指令;了解GPS数据报文的NMEA-0183协议;了解NTP协议报文。
硬件需求电路GPS数据报文的NMEA-0183协议NTP协议报文编写代码项目结构项目依赖配置串口读取GPS数据实现NTP服务部署应用发布到文件构建Docker镜像后续工作硬件需求名称描述数量计算机可以是运行Linux的开发板,也可以是运行Windows的电脑x1NEO-6MGPS模块x1USB串口可选,使用USB串口将GPS模块与计算机相连x1杜邦线传感器与开发板的连接线若干电路
传感器接口开发板接口NEO-6MTX开发板或USB串口的RXRX开发板或USB串口的TXVCC5VGNDGNDGPS数据报文的NMEA-0183协议NMEA-0183是GPS设备输出信息的标准格式,是由美国国家海洋电子协会定制的标准。NMEA-0183有多种不同的数据报文,每种都是独立的ASCII字符串,使用逗号隔开数据,数据流长度从30-100字符不等,通常以每秒间隔选择输出。NMEA-0183协议定义的语句非常多,但是常用的或者说兼容性最广的语句只有GPGGA、GPGGA、GPGGA、GPGSA、GPGSV、GPGSV、GPGSV、GPRMC、$GPVTG等。下面给出这些常用NMEA-0183语句的解释。
帧名称说明最大帧长$GPGGA全球定位数据72$GPGSA卫星PRN数据65$GPGSV卫星状态信息210$GPRMC推荐最小数据70GPVTG地面速度信息34由于我们只需要从GPS中获取时间信息,选择包含时间信息的“GPVTG地面速度信息34由于我们只需要从GPS中获取时间信息,选择包含时间信息的“GPVTG地面速度信息34由于我们只需要从GPS中获取时间信息,选择包含时间信息的“GPRMC推荐最小数据”帧进行解析:
$GPRMC<1><2><3><4><5><6><7><8><9><10><11><12><13>帧头UTC时间定位状态纬度纬度半球经度经度半球地面速率地面航向UTC日期磁偏角磁偏角方向模式*校验和下面以一个真实的数据帧为例$GPRMC,0137100,A,38157392,N,107073951,E,0.467,050722,A7
$GPRMC0137100A38157392N107073951E0.467050722A*78帧头UTC时间01:37:17A=有效定位,V=无效定位纬度38度157392分北纬经度107度73951分东经地面速率0.467节航向度UTC日期2022/07/05磁偏角度磁偏角方向A=自主定位,N=数据无效通过串口读取$GPRMC数据帧后,需要解析<1>和<9>字段的值,并将其转换为UTC时间。
细心的你也许会发现获取到的时间信息只精确到秒,GPS明明使用的是原子钟,这是为什么?仔细观察手中的GPS模块,还有一个PPS针脚没有使用。PPS是秒脉冲,一般是由GPS接收机或原子钟按秒发出的、宽度小于1秒、有着急升或突降边沿的脉冲信号,通常用于精确计时和测量时间。PPS信号能精确地指示每一秒的开始时间,但不能指示对应现实时间的哪一秒,因此只能作为辅助信号,与卫星导航信息组合使用,提供低延迟、低抖动的授时服务。很遗憾,.NET目前没法直接操作PPS引脚,我们只能实现一个“玩具”级的时间同步服务器了。
NTP协议报文NTP,网络时间协议,是一种使用UDP的计算机之间进行时间同步的网络协议,位于OSI7层网络模型中的应用层,默认使用的端口为12那么使用NTP是如何进行时间同步的呢?简单的说将发送的报文打上本机的时间戳,配合报文来回传输的时延修正本机的时间。如下所示,可以计算出网络传输时延δ,以及客户端与服务端的时间偏移θ:
δ=−
θ=+2
其中,t0是请求报文传输的客户端时间戳,t1是请求报文接收的服务器时间戳,t2是回复报文传输的服务器时间戳,t3是回复报文接收的客户端时间戳。客户端和服务端都有一个时间轴,分别代表着各自系统的时间,当客户端想要同步服务端的时间时,客户端会构造一个NTP报文发送到服务端,客户端会记下此时发送的时间t0,经过一段网络延时传输后,服务器在t1时刻收到报文,经过一段时间处理后在t2时刻向客户端返回报文,再经过一段网络延时传输后客户端在t3时刻收到服务器报文。这样客户端就可以校准自己的本机时间了。
在了解NTP同步时间的过程后,下面解析NTP报文具体包含的字段,一般的NTP报文长度为48字节:
其中要注意的是NTP时间戳的起始时间是1900-01-0100:00:00,而不是Unix时间戳的起始时间1970-01-0100:00:00。
下面是使用Wireshark抓取的Windows时钟同步的NTP报文:
编写代码项目地址:https://github.com/ZhangGaoxing/gps-ntp
项目结构创建一个控制台应用和类库,项目结构如下:
项目依赖添加如下NuGet包引用:
//使用的串口名称conststringSERIAL_NAME=“/dev/ttyUSB0”;usingSerialPortgps=newSerialPort{BaudRate=9600,Encoding=Encoding.UTF8,ReadTimeout=500,WriteTimeout=500,};从串口中获取数据从串口中读取数据时使用的是SerialPort类中的DataReceived事件。事件可以理解为一种广播,当完成某种操作后向外发送通知。即串口接收到数据后,触发数据处理事件。
//////GPS报文处理///voidGpsFrameReceived{//TODO:读取$GPRMC数据帧;提取时间;更新系统时间}由于GPS模块输出的不只有$GPRMC数据帧,因此需要在处理事件中判断帧头以及帧的有效性。
voidGpsFrameReceived{stringframe=gps.ReadLine;
if (frame.StartsWith("$GPRMC"))
{
// $GPRMC,UTC 时间,定位状态,纬度,纬度半球,经度,经度半球,速度,航向,UTC 日期,磁偏角,磁偏角方向,指示模式*校验和
// $GPRMC,013717.00,A,3816.57392,N,10708.73951,E,0.467,,050722,,,A*78
string[] field = frame.Split(",");
// 帧数据有效
if (!field[12].StartsWith("N"))
{
// TODO:提取时间;更新系统时间
}
}
}在验证$GPRMC数据帧有效后,根据帧解析提取对应字段的时间信息。
voidGpsFrameReceived{stringframe=gps.ReadLine;
if (frame.StartsWith("$GPRMC"))
{
string[] field = frame.Split(",");
if (!field[12].StartsWith("N"))
{
// 获取 GPS 时间
string time = field[1][0..6];
string date = field[9];
DateTime utcNow = DateTime.ParseExact($"{date}{time}", "ddMMyyHHmmss", CultureInfo.InvariantCulture);
// TODO:更新系统时间
}
}
}更新系统时间由于.NET并不提供修改系统时间的操作,因此我们要使用间接的方式修改系统时间。一种方式是使用P/Invoke调用C++的函数,这种方式可以精确的修改时间,但涉及引用、数据类型转换,过于复杂,和本入门指南不符。这里使用的是运行命令行指令的方式修改系统的时间,但修改时间的精度只能精确到秒。在Windows中使用PowerShell的Set-Date命令,在Linux中使用date命令。
//////更新系统时间///voidUpdateSystemTime{ProcessStartInfoprocessInfo;if){processInfo=newProcessStartInfo{FileName=“powershell.exe”,Arguments=$“Set-Date“””{time.ToLocalTime.ToString}“”“”,RedirectStandardOutput=true,UseShellExecute=false,CreateNoWindow=true,};}else{processInfo=newProcessStartInfo{FileName=“date”,Arguments=$“-s“{time.ToLocalTime.ToString}””,RedirectStandardOutput=true,UseShellExecute=false,CreateNoWindow=true,};}
var process = Process.Start(processInfo);
process.WaitForExit();
}最终报文处理事件由以下代码构成:
voidGpsFrameReceived{stringframe=gps.ReadLine;
if (frame.StartsWith("$GPRMC"))
{
string[] field = frame.Split(",");
if (!field[12].StartsWith("N"))
{
string time = field[1][0..6];
string date = field[9];
DateTime utcNow = DateTime.ParseExact($"{date}{time}", "ddMMyyHHmmss", CultureInfo.InvariantCulture);
UpdateSystemTime(utcNow);
// 记录时钟最后一次被更新的时间
lastUpdatedTime = utcNow;
}
}
}使用gps.Open;打开串口后就可以获取时间数据了。
实现NTP服务下面使用Socket类实现一个简单的UDP服务器,用于监听和回复NTP报文。
文章为作者独立观点,不代表股票交易接口观点