ESP8266の内蔵タイマーの精度


前回、Reset動作を利用した時刻合わせを紹介しました。
こちらでDeepSleep中の ESP8266の内 蔵タイマーの精度を調べてみましたが、かなり時計が狂うことが分かっています。
そこで、通常に通電中のESP8266の内蔵タイマーの精度を調べてみました。
使用したスケッチは以下のスケッチです。
@初回起動時にはUDPでNTPサーバーに接続
ANTPを使って現在日時を取得
BRTCのユーザエリアに現在日時をセット
C強制的にリセット
Dリセットからの復帰時に、RTCのユーザエリアから現在日時を読み込んで内部の時計を設定
EMQTTサーバーに接続
F10分ごとにMQTTで現在時刻を送信

WeMosやNodeMCUの様に、スケッチの書き込み(ブートモードの変更)を自動的に行う機能が付いているボードでは、
リセットが掛かると、自動的にUART Download mode(書き込みモード)になってしまうボードが有ります
リセット動作を確認するときは、裸のモジュールを使った方が余計なトラブルが有りません

MQTTのSubscriberの受信時間と、ESP8266が送信した時間を比べることで、ESP8266の内蔵タイマーの精度が分かります。
MQTTサーバーはローカルなサーバーを使いました。
/*
 ESP.reset()/ESP.resart()ではRTC User Memoryがクリアされない。
 この性質を利用した時刻設定のサンプル。
 初回起動時のみWiFiに接続してNTPに時刻問い合わせ。
 RTC User Memoryに現在日時を設定し強制Reset。
 Resetからの復帰時にRTC User Memoryから日時を取り出して設定。
 ESPの内蔵タイマーの精度を調べる。
*/

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiUdp.h>
#include <TimeLib.h> // https://github.com/PaulStoffregen/Time

// Update these with values suitable for your network.
const char* ssid = "SSID of your AP";
const char* password = "PASSWORD of your AP";
const char* mqtt_server = "broker.hivemq.com";

#define INTERVAL 600 // 10分
//#define INTERVAL 3600 // 60分

WiFiClient espClient;
PubSubClient client(espClient);

//RTC memory(512Byte)の定義
struct {
  uint8_t data[512]; // User Data
} rtcData;

// UDPローカルポート番号
unsigned int localPort = 2390;
// NTPサーバー
char timeServer[] = "ntp.nict.jp";
// NTPパケットバッファサイズ
const int NTP_PACKET_SIZE= 48;
// NTP送受信用パケットバッファ
byte packetBuffer[NTP_PACKET_SIZE];

unsigned long lastMillis = 0;

// Udpクラス
WiFiUDP udp;

// Connect AP
void setup_wifi() {
  // We start by connecting to a WiFi network
  Serial.print("Wait for WiFi...");
  WiFi.begin(ssid, password);

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

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

// Connect MQTT server
void server_connect() {
  char clientid[20];

  sprintf(clientid,"ESP8266-%06x",ESP.getChipId());
  Serial.print("clientid=");
  Serial.println(clientid);
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Attempt to connect
    if (client.connect(clientid)) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

// send an NTP request to the time server at the given address
void sendNTPpacket(char * address)
{
  Serial.println("sending NTP packet...");
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;

  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:
  udp.beginPacket(address, 123); //NTP requests are to port 123
  udp.write(packetBuffer, NTP_PACKET_SIZE);
  udp.endPacket();
}

// read an NTP request from the time server
unsigned long readNTPpacket(byte * packetBuffer, int bufferLength) {
  Serial.println("reading NTP packet...");
  // バッファに受信データを読み込む
  udp.read(packetBuffer, bufferLength); // read the packet into the buffer

  // 時刻情報はパケットの40バイト目からはじまる4バイトのデータ
  unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
  unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);

  // NTPタイムスタンプは64ビットの符号無し固定小数点数(整数部32ビット、小数部32ビット)
  // 1900年1月1日0時との相対的な差を秒単位で表している
  // 小数部は切り捨てて、秒を求めている
  unsigned long secsSince1900 = highWord << 16 | lowWord;
  Serial.print("Seconds since Jan 1 1900 = " );
  Serial.println(secsSince1900);

  // NTPタイムスタンプをUNIXタイムに変換する
  // UNITタイムは1970年1月1日0時からはじまる
  // 1900年から1970年の70年を秒で表すと2208988800秒になる
  const unsigned long seventyYears = 2208988800UL;
  // NTPタイムスタンプから70年分の秒を引くとUNIXタイムが得られる
  unsigned long epoch = secsSince1900 - seventyYears;
  Serial.print("Unix time = ");
  Serial.println(epoch);
  return epoch;
}

// time_tをYYYY/MM/DD hh:mm:ssで表示する
void showTime(char * title, time_t timet) {
   Serial.print(title);
   Serial.print(":");
   Serial.print(year(timet));
   Serial.print("/");
   Serial.print(month(timet));
   Serial.print("/");
   Serial.print(day(timet));
   Serial.print(" ");
   Serial.print(hour(timet));
   Serial.print(":");
   Serial.print(minute(timet));
   Serial.print(":");
   Serial.println(second(timet));
}

// 現在日時をYYYY/MM/DD hh:mm:ssで表示する
void showNow(char * title) {
   Serial.print(title);
   Serial.print(":");
   Serial.print(year());
   Serial.print("/");
   Serial.print(month());
   Serial.print("/");
   Serial.print(day());
   Serial.print(" ");
   Serial.print(hour());
   Serial.print(":");
   Serial.print(minute());
   Serial.print(":");
   Serial.println(second());
}

void setup()
{
  delay(1000);
  Serial.begin(115200);
  Serial.println();
  Serial.print("ESP.getResetReason()=");
  Serial.println(ESP.getResetReason());
  String resetReason = ESP.getResetReason();

  /*
  enum rst_reason {
  REANSON_DEFAULT_RST = 0, // ノーマルスタート。電源オンなど。
  REANSON_WDT_RST = 1, // ハードウェアウォッチドッグによるリセット
  REANSON_EXCEPTION_RST = 2, // 例外によるリセット。GPIO状態は変化しない
  REANSON_SOFT_WDT_RST = 3, // ソフトウェアウォッチドッグによるリセット。GPIO状態は変化しない
  REANSON_SOFT_RESTART = 4, // ソフトウェアによるリセット。GPIO状態は変化しない
  REANSON_DEEP_SLEEP_AWAKE= 5, // ディープスリープ復帰
  REANSON_EXT_SYS_RST = 6, // 外部要因(RSTピン)によるリセット。
  };
  */

  rst_info *prst = ESP.getResetInfoPtr();
  /*
  struct rst_info{
      uint32 reason;
      uint32 exccause;
      uint32 epc1;
      uint32 epc2;
      uint32 epc3;
      uint32 excvaddr;
      uint32 depc;
  };
  */
  Serial.print("reset reason=");
  Serial.println(prst->reason);
 
  // RTC memoryからデータを読み込む
  if (ESP.rtcUserMemoryRead(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
    Serial.println("rtcUserMemoryRead Success");
    if (prst->reason != 4) { // Not Software/System restart
      for (int i = 0; i < sizeof(rtcData.data); i++) {
        rtcData.data[i] = 0;
      }
    }
  } else {
    Serial.println("rtcUserMemoryRead Fail");
  }
 
  if (rtcData.data[0] == 0) { // NTPクライントモード
    // Wifi接続
    setup_wifi();
    Serial.println("Starting UDP");
    udp.begin(localPort);

    // NTPサーバーに時刻リクエストを送信
    sendNTPpacket(timeServer);

  } else { // Resetからの復帰
    Serial.print("rtcData=");
    Serial.print(rtcData.data[0]);
    Serial.print(" ");
    Serial.print(rtcData.data[1]);
    Serial.print(" ");
    Serial.print(rtcData.data[2]);
    Serial.print(" ");
    Serial.print(rtcData.data[3]);
    Serial.print(" ");
    Serial.print(rtcData.data[4]);
    Serial.print(" ");
    Serial.print(rtcData.data[5]);
    Serial.println();

    showNow("Before");
    setTime(rtcData.data[3],rtcData.data[4],rtcData.data[5],
    rtcData.data[2],rtcData.data[1],rtcData.data[0]);
    showNow("After");

    // Wifi接続
    setup_wifi();
    Serial.println("Starting MQTT");
    client.setServer(mqtt_server, 1883);
    server_connect();

    // MQTT送信
    char topic[50];
    char msg[50];
    sprintf(topic,"Reset/ESP8266-%06x",ESP.getChipId());
    sprintf(msg, "%02d/%02d/%02d %02d:%02d:%02d",
    year(),month(),day(),hour(),minute(),second());
    Serial.print("Publish message: ");
    Serial.println(msg);
    client.publish(topic, msg);
  }
  lastMillis = millis();
}



void loop()
{
  static int counter=0;

  client.loop();
  long now = millis();

  // MQTT送信
  if (now - lastMillis > 1000) { // 1秒経過
    lastMillis = now;
    counter++;
    if (counter > INTERVAL) {
      char topic[50];
      char msg[50];
      sprintf(topic,"Reset/ESP8266-%06x",ESP.getChipId());
      sprintf(msg, "%02d/%02d/%02d %02d:%02d:%02d",
      year(),month(),day(),hour(),minute(),second());
      Serial.print("Publish message: ");
      Serial.println(msg);
      client.publish(topic, msg);
      showNow("Loop");
      counter=0;
    }
  }

  // NTPサーバからのパケット受信(初回起動時のみ)
  int cb = udp.parsePacket();
  if (cb) {
    Serial.print("packet received, length=");
    Serial.println(cb);

    // NTPサーバーからUTC時刻を受信
    unsigned long utc_epoch;
    utc_epoch = readNTPpacket(packetBuffer, NTP_PACKET_SIZE);
    showTime("UTC",utc_epoch);

    // 日本時間を求める
    // 日本標準時にあわせるために+9時間する
    unsigned long jst_epoch = utc_epoch + (9 * 60 * 60);
    showTime("JST",jst_epoch);

    // 現在の時間を設定
    rtcData.data[0] = year(jst_epoch);
    rtcData.data[1] = month(jst_epoch);
    rtcData.data[2] = day(jst_epoch);
    rtcData.data[3] = hour(jst_epoch);
    rtcData.data[4] = minute(jst_epoch);
    rtcData.data[5] = second(jst_epoch);

    // RTC memoryにデータを書き込む
    if (ESP.rtcUserMemoryWrite(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
      Serial.println("rtcUserMemoryWrite Success");
    } else {
      Serial.println("rtcUserMemoryWrite Fail");
    }

    // Reset
    WiFi.disconnect();
    Serial.println("Do ESP.restart");
    ESP.reset();
  }
}

mosquittoクライアントを使って受信した日時とMQTTのメッセージを確認してみました。
mosquittoクライアントのマシンもNTPで時刻同期をしています。
一番左のMQTTメッセージ受信時刻と、一番右のESP8266の時刻を比べてみると、
常にESP8266の時刻が2秒程度遅れていますが、これは最初の時刻合わせの時の誤差と思います。
2時間以上経過しましたが、2秒以上の差にはなりません。


通電中のESP8266の内蔵時計の精度は信頼できることが分かりました。
次回はSPIFFS機能を紹介します。

続く...