ESP8266(ESP12F)学习笔记2 -- NTP网络时间获取

Posted GenCoder

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP8266(ESP12F)学习笔记2 -- NTP网络时间获取相关的知识,希望对你有一定的参考价值。

对于已经掌握了ESP8266网络连接的小伙伴来说,第一件事应该就是想着利用网路获取一些数据,或者利用网络去控制一些设备,这里利用NTP服务器来获取网络时间


NTP服务器

NTP服务器【Network Time Protocol(NTP)】是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)做同步化,它可以提供高精准度的时间校正(LAN上与标准间差小于1毫秒,WAN上几十毫秒),且可介由加密确认的方式来防止恶毒的协议攻击。时间按NTP服务器的等级传播。按照离外部UTC源的远近把所有服务器归入不同的Stratum(层)中。

特征介绍

NTP提供准确时间,首先要有准确的时间来源,这一时间应该是国际标准时间UTC。 NTP获得UTC的时间来源可以是原子钟、天文台、卫星,也可以从Internet上获取。这样就有了准确而可靠的时间源。时间按NTP服务器的等级传播。按照离外部UTC 源的远近将所有服务器归入不同的Stratum(层)中。Stratum-1在顶层,有外部UTC接入,而Stratum-2则从Stratum-1获取时间,Stratum-3从Stratum-2获取时间,以此类推,但Stratum层的总数限制在15以内。所有这些服务器在逻辑上形成阶梯式的架构相互连接,而Stratum-1的时间服务器是整个系统的基础。
计算机主机一般同多个时间服务器连接, 利用统计学的算法过滤来自不同服务器的时间,以选择最佳的路径和来源来校正主机时间。即使主机在长时间无法与某一时间服务器相联系的情况下,NTP服务依然有效运转。
为防止对时间服务器的恶意破坏,NTP使用了识别(Authentication)机制,检查来对时的信息是否是真正来自所宣称的服务器并检查资料的返回路径,以提供对抗干扰的保护机制。

网络校时

时间服务器可以利用以下三种方式与其他服务器对时:

  • broadcast/multicast
  • client/server
  • symmetric

(1)broadcast/multicast方式主要适用于局域网的环境,时间服务器周期性的以广播的方式,将时间信息传送给其他网路中的时间服务器,其时间仅会有少许的延迟,而且配置非常的简单。但是此方式的精确度并不高,对时间精确度要求不是很高的情况下可以采用。
(2)symmetric的方式得一台服务器可以从远端时间服务器获取时钟,如果需要也可提供时间信息给远端的时间服务器。此一方式适用于配置冗余的时间服务器,可以提供更高的精确度给主机。
(3)client/server方式与symmetric方式比较相似,只是不提供给其他时间服务器时间信息,此方式适用于一台时间服务器接收上层时间服务器的时间信息,并提供时间信息给下层的用户。

上述三种方式,时间信息的传输都使用UDP协议。时间服务器利用一个过滤演算法,及先前八个校时资料计算出时间参考值,判断后续校时包的精确性,一个相对较高的离散程度,表示一个对时资料的可信度比较低。仅从一个时间服务器获得校时信息,不能校正通讯过程所造成的时间偏差,而同时与许多时间服务器通信校时,就可利用过滤算法找出相对较可靠的时间来源,然后采用它的时间来校时。

历史发展

网络时间协议(NTP)的首次实现记载在Internet Engineering Note之中,其精确度为数百毫秒。稍后出现了首个时间协议的规范,即RFC-778,它被命名为DCNET互联网时间服务,而它提供这种服务还是借助于Internet control MessageProtocol(ICMP),即互联网控制消息协议中的时间戳和时间戳应答消息。作为NTP
名称的首次出现是在RFC-958之中,该版本也被称为NTP v0,其目的是为ARPA网提供时间同步。它己完全脱离ICMP,是作为独立的协议以完成更高要求的时间同步。它对于如本地时钟的误差估算和精密度等基本运算、参考时钟的特性、网络上的分组数据包及其消息格式进行了描述。但是不对任何频率误差进行补偿,也没有规定滤波和同步的算法。
美国特拉华大学(University of Delaware)的David L .Mills主持了由美国国防部高级研究计划局DARPA、美国国家科学基金NSF和美国海军水面武器中心NSWC资助的网络时间同步项目,成功的开发出了NTP协议的第1, 2, 3版。

出现时间

NTP version 1 出现于1988年6月,在RFC-1059中描述了首个完整的NTP的规范和相关算法。这个版本已经采用了client/server模式以及对称操作,但是它不支持授权鉴别和NTP的控制消息。
1989年9月推出了取代RFC-958和RFC-1059的NTP v2版本即RFC-1119。
几乎同时,DEC公司也推出了一个时间同步协议,数字时间同步服务DTSS(Digital Time Synchronization Service).在 1992 年3月,NTP v3版本RFC-1305问世,该版本总结和综合了NTP先前版本和DTSS,正式引入了校正原则,并改进了时钟选择和时钟滤波的算法,而且还引入了时间消息发送的广播模式,这个版本取代了NTP的先前版本。NT P v 3 发布后,一直在不断地进行改进,NTP实现的一个重要功能是对计算机操作系统的时钟调整。在NTPv3研究和推出的同时,有关在操作系统核心中改进时间保持功能的研究也在并行地进行。1994年推出了RFC-1559,名为A KernelModel for Precision Time keening,即精密时01保持的核心模式,这个实现可以把计算机操作系统的时间精确度保持在微秒数量级。几乎同时,改进建议。对本地时钟调整算法,通信模式,新的时钟驱动器,又提出了NTP v4适配规则等方面的改进描述了具体方向。

发展方向

NTP的第4版正在研究和测试中,网络时间同步技术也将向更高精度、更强的兼容性和多平台的适应性方向发展。网络时间协议NTP是用于互联网中时间同步的标准之一,它的用途是把计算机的时钟同步到世界协调时UTC,其精度在局域网内可达0.1ms,在Internet上绝大多数的地方其精度可以达到1- 50ms 。

NTP服务器只要做个大概了解就好,我们需要清楚知道的是怎么去获取到NTP服务器时间的方法

Arduino NTPClient库调用

NTPClient库安装

打开 项目 → 加载库 → 管理库,查看Arduino的相关库。
在这里插入图片描述
输入 ntp 搜索,找到名字为 NTPClient 的库进行安装,我这里安装的版本是3.2.0的。
在这里插入图片描述

NTPClient库示例

文件示例中打开NTPClient库示例 NTPClientBasic ,代码如下,需要修改WiFi名称跟密码,获取NTP时间需要ESP8266首先连上可用网络。

#include <NTPClient.h>
// change next line to use with another board/shield
#include <ESP8266WiFi.h>
//#include <WiFi.h> // for WiFi shield
//#include <WiFi101.h> // for WiFi 101 shield or MKR1000
#include <WiFiUdp.h>

const char *ssid     = "<SSID>";
const char *password = "<PASSWORD>";

WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP);

void setup(){
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }

  timeClient.begin();
}

void loop() {
  timeClient.update();

  Serial.println(timeClient.getFormattedTime());

  delay(1000);
}

NTP网络时间协议基于UDP,需要调用到 WiFiUdp.h ,连接上WiFi之后,timeClient.begin() 开启NTP同步对时,loop函数中 timeClient.update() 更新当前时间,timeClient.getFormattedTime() 在将获取到的时间以时分秒的字符串形式打印出来。

NTPClient库函数粗步解析

NTPClient.h

先看看 NTPClient.h 中的函数,看注释基本都能看明白。

#pragma once

#include "Arduino.h"

#include <Udp.h>

#define SEVENZYYEARS 2208988800UL
#define NTP_PACKET_SIZE 48
#define NTP_DEFAULT_LOCAL_PORT 1337

class NTPClient {
  private:
    UDP*          _udp;
    bool          _udpSetup       = false;

    const char*   _poolServerName = "pool.ntp.org"; // Default time server
    int           _port           = NTP_DEFAULT_LOCAL_PORT;
    long          _timeOffset     = 0;		// In s

    unsigned long _updateInterval = 60000;  // In ms

    unsigned long _currentEpoc    = 0;      // In s
    unsigned long _lastUpdate     = 0;      // In ms

    byte          _packetBuffer[NTP_PACKET_SIZE];

    void          sendNTPPacket();

  public:
    NTPClient(UDP& udp);
    NTPClient(UDP& udp, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval);

    /**
     * Set time server name
     *
     * @param poolServerName
     */
    void setPoolServerName(const char* poolServerName);

    /**
     * Starts the underlying UDP client with the default local port
     */
    void begin();

    /**
     * Starts the underlying UDP client with the specified local port
     */
    void begin(int port);

    /**
     * This should be called in the main loop of your application. By default an update from the NTP Server is only
     * made every 60 seconds. This can be configured in the NTPClient constructor.
     *
     * @return true on success, false on failure
     */
    bool update();

    /**
     * This will force the update from the NTP Server.
     *
     * @return true on success, false on failure
     */
    bool forceUpdate();

    int getDay() const;
    int getHours() const;
    int getMinutes() const;
    int getSeconds() const;

    /**
     * Changes the time offset. Useful for changing timezones dynamically
     */
    void setTimeOffset(int timeOffset);

    /**
     * Set the update interval to another frequency. E.g. useful when the
     * timeOffset should not be set in the constructor
     */
    void setUpdateInterval(unsigned long updateInterval);

    /**
     * @return time formatted like `hh:mm:ss`
     */
    String getFormattedTime() const;

    /**
     * @return time in seconds since Jan. 1, 1970
     */
    unsigned long getEpochTime() const;

    /**
     * Stops the underlying UDP client
     */
    void end();
};

构造函数NTPClient ( )

NTPClient(udp, poolServerName, timeOffset,updateInterval)

首先,看构造函数,以参数最多的函数做个解析

	NTPClient(UDP& udp);
    NTPClient(UDP& udp, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset);
    NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval);

描述:NTPClient类构造函数之一,设置NTP的基本参数。
语法:NTPClient(udp, poolServerName, timeOffset,updateInterval)。
参数:udp:WiFiUDP类对象;poolServerName:NTP服务器路径;timeOffset:NTP时间偏移(时区);updateInterval:时间间隔。
用法:

WiFiUDP    ntp_udp;
timeClient(ntp_udp,"ntp1.aliyun.com",60*60*8,60000);
  • 参数1 - ntp_udp:WiFiUDP对象
  • 参数2 - “ntp1.aliyun.com”:阿里云NTP服务器(任意NTP服务器都可)
  • 参数3 - 60*60*8 :该参数单位为秒,60*60为东一区时间,北京时间为东八区,所以这里参数为 60*60*8
  • 参数4 - 60000:设置NTP更新最小时间间隔,参数单位为毫秒

NTPClient(udp, poolServerName, timeOffset,updateInterval) 函数体

NTPClient::NTPClient(UDP& udp, const char* poolServerName, long timeOffset, unsigned long updateInterval) {
  this->_udp            = &udp;
  this->_timeOffset     = timeOffset;
  this->_poolServerName = poolServerName;
  this->_updateInterval = updateInterval;
}

NTP服务器设置函数

setPoolServerName(poolServerName)

    /**
     * Set time server name
     *
     * @param poolServerName
     */
    void setPoolServerName(const char* poolServerName);

描述:NTP服务器设置函数。
语法:setPoolServerName(const char* poolServerName)。
参数:poolServerName:服务器路径。

setPoolServerName(poolServerName) 函数体

void NTPClient::setPoolServerName(const char* poolServerName) {
    this->_poolServerName = poolServerName;
}

初始化函数

begin()

    /**
     * Starts the underlying UDP client with the default local port
     */
    void begin();

描述:NTP初始化函数。
语法:begin()。
参数:无。

begin()函数体

void NTPClient::begin() {
  this->begin(NTP_DEFAULT_LOCAL_PORT);
}

初始化函数(带端口设置参数)

begin(port)

    /**
     * Starts the underlying UDP client with the specified local port
     */
    void begin(int port);

描述:使用指定的本地端口启动底层UDP客户端。
语法:begin(port)。
参数:port:本地端口。

begin(port)函数体

void NTPClient::begin(int port) {
  this->_port = port;

  this->_udp->begin(this->_port);

  this->_udpSetup = true;
}

NTP更新函数

update()

    /**
     * This should be called in the main loop of your application. By default an update from the NTP Server is only
     * made every 60 seconds. This can be configured in the NTPClient constructor.
     *
     * @return true on success, false on failure
     */
    bool update();

描述:在用户函数中循环调用,用于更新NTP客户端,调用间隔需要大于或等于60秒。
语法:update()。
参数:无。

update()函数体

bool NTPClient::update() {
  if ((millis() - this->_lastUpdate >= this->_updateInterval)     // Update after _updateInterval
    || this->_lastUpdate == 0) {                                // Update if there was no update yet.
    if (!this->_udpSetup) this->begin();                         // setup the UDP client if needed
    return this->forceUpdate();
  }
  return true;
}

强制更新函数

forceUpdate()

可以发现该函数在update中被调用了

    /**
     * This will force the update from the NTP Server.
     *
     * @return true on success, false on failure
     */
    bool forceUpdate();

描述:强制更新NTP服务器。
语法:forceUpdate()。
参数:无。

forceUpdate()函数体

bool NTPClient::forceUpdate() {
  #ifdef DEBUG_NTPClient
    Serial.println("Update from NTP Server");
  #endif

  this->sendNTPPacket();

  // Wait till data is there or timeout...
  byte timeout = 0;
  int cb = 0;
  do {
    delay ( 10 );
    cb = this->_udp->parsePacket();
    if (timeout > 100) return false; // timeout after 1000 ms
    timeout++;
  } while (cb == 0);

  this->_lastUpdate = millis() - (10 * (timeout + 1)); // Account for delay in reading the time

  this->_udp->read(this->_packetBuffer, NTP_PACKET_SIZE);

  unsigned long highWord = word(this->_packetBuffer[40], this->_packetBuffer[41]);
  unsigned long lowWord = word(this->_packetBuffer[42], this->_packetBuffer[43]);
  // combine the four bytes (two words) into a long integer
  // this is NTP time (seconds since Jan 1 1900):
  unsigned long secsSince1900 = highWord << 16 | lowWord;

  this->_currentEpoc = secsSince1900 - SEVENZYYEARS;

  return true;
}

时间获取函数

调用以获取当前时间为周几,小时数,分钟数,秒数等的函数。

    int getDay() const;
    int getHours() const;
    int getMinutes() const;
    int getSeconds() const;

getDay()函数
描述:获取当前时间星期几。
参数:无。
返回值:数字0 - 6,注意,国外将每周星期天定为一周的第一天。所以星期天的返回值为0。星期六的返回值为6。

getHours()函数
描述:获取小时数。
参数:无。
返回值:0 - 23。

getMinutes()函数
描述:获取分钟数。
参数:无。
返回值:0 - 59。

getSeconds()函数
描述:获取秒数。
参数:无。
返回值:0 - 59。

时间获取函数实现方法

int NTPClient::getDay() const {
  return (((this->getEpochTime()  / 86400L) + 4 ) % 7); //0 is Sunday
}
int NTPClient::getHours() const {
  return ((this->getEpochTime()  % 86400L) / 3600);
}
int NTPClient::getMinutes() const {
  return ((this->getEpochTime() % 3600) / 60);
}
int NTPClient::getSeconds() const {
  return (this->getEpochTime() % 60);
}

设置时间间隔(时区)

setTimeOffset(timeOffset)

    /**
     * Changes the time offset. Useful for changing timezones dynamically
     */
    void setTimeOffset(int timeOffset);

描述:设置/修改时间间隔,即时区。
语法:setTimeOffset(timeOffset)。
参数:timeOffset:时间间隔,以秒为单位。
用法:

setTimeOffset(60*60)//获取东一区的NTP时间

setTimeOffset(timeOffset)函数体

void NTPClient::setTimeOffset(int timeOffset) {
  this->_timeOffset     = timeOffset;
}

设置更新间隔

setUpdateInterval(updateInterval)

    /**
     * Set the update interval to another frequency. E.g. useful when the
     * timeOffset should not be set in the constructor
     */
    void setUpdateInterval(unsigned long updateInterval);

描述:设置/修改NTP更新间隔。
语法:setUpdateInterval(updateInterval)。
参数:updateInterval:时间间隔,单位为毫秒。

setUpdateInterval(updateInterval)函数体

void NTPClient::setUpdateInterval(unsigned long updateInterval) {
  this->_updateInterval = updateInterval;
}

获取时间格式 - 字符串

getFormattedTime()

    /**
     * @return time formatted like `hh:mm:ss`
     */
    String getFormattedTime() const;

描述:获取时间格式为 hh:mm:ss 的字符串。
语法:getFormattedTime()。
参数:无。
返回值:字符串 hh:mm:ss

getFormattedTime()函数体

String NTPClient::getFormattedTime() const {
  unsigned long rawTime = this->getEpochTime();
  unsigned long hours = (rawTime % 86400L) / 3600;
  String hoursStr = hours < 10 ? "0" + String(hours) : String(hours);

  unsigned long minutes = (rawTime % 3600) / 60;
  String minuteStr = minutes < 10 ? "0" + String(minutes) : String(minutes);

  unsigned long seconds = rawTime % 60;
  String secondStr = seconds < 10 ? "0" + String(seconds) : String(seconds);

  return hoursStr + ":" + minuteStr + ":" + secondStr;
}

获取绝对时间

getEpochTime()

    /**
     * @return time in seconds since Jan. 1, 1970
     */
    unsigned long getEpochTime() const;

描述:获取自1970年1月1日到现在的时间秒数(很大一个数字)。
语法:getEpochTime()。
参数:无。
返回值:时间秒数。

getEpochTime()函数体

unsigned long NTPClient::getEpochTime() const {
  return this->_timeOffset + // User offset
         this->_currentEpoc + // Epoc returned by the NTP server
         ((millis() - this->_lastUpdate) / 1000); // Time since last update
}

NTP数据请求

sendNTPPacket()

在forceUpdate()函数中被调用

void NTPClient::sendNTPPacket() {
  // set all bytes in the buffer to 0
  memset(this->_packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  this->_packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  this->_packetBuffer[1] = 0;     // Stratum, or type of clock
  this->_packetBuffer[2] = 6ESP8266(ESP12F)中断报错 - ISR not in IRAM解决

ESP8266(ESP12F)中断报错 - ISR not in IRAM解决

ESP8266学习笔记2:实现ESP8266的局域网内通信

ESP8266学习笔记4:ESP8266的SmartConfig

ESP8266学习笔记6:ESP8266规范wifi连接操作

乐鑫esp8266学习rtos3.0笔记:分享在 esp8266 C SDK如何通过外部写入参数,程序里面实现动态获取参数。