ESP8266の内蔵タイマーの精度


前回、Reset動作を利用した時刻合わせを紹介しました。
こちらでDeepSleep中のESPの内 蔵タイマーの精度を調べてみましたが、かなり時計が狂うことが分かっています。
そこで、通常に通電中のESPの内蔵タイマーの精度を調べてみました。
なお、チップの個体差を考慮して、DeepSleep測定の時と同じチップを使っています。
使用したスケッチは以下のスケッチです。
@初回起動時にはUDPでNTPサーバーに接続
ANTPを使って現在時刻を取得
BRTCのユーザエリアにセット
C強制的にリセット
Dリセットからの復帰時に、RTCのユーザエリアから時刻を読み込んで時刻合わせ
EMQTTサーバーに接続
F1時間ごとにMQTTで現在時刻を送信
MQTTのSubscriberの受信時間と、ESPが送信した時間を比べることで、ESP内蔵タイマーの精度が分かります。
/*
 ESP.reset()/ESP.resart()ではRTC User Memoryがクリアされない。
 この性質を利用した時刻設定のサンプル。
 初回起動時のみNTPに時刻問い合わせ。
 RTC User Memoryに現在日時を設定し強制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";
const char* password = "アクセスポイントのパスワード";
const char* mqtt_server = "192.168.10.40";
//const char* mqtt_server = "broker.hivemq.com";

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

WiFiClient espClient;
PubSubClient client(espClient);

char topic[50];
char msg[50];

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

// UDPローカルポート番号
unsigned int localPort = 2390;
// NTPタイムサーバIPアドレス(ntp.nict.jp NTP server)
IPAddress timeServer(133, 243, 238, 164);
// NTPパケットバッファサイズ
const int NTP_PACKET_SIZE= 48;
// NTP送受信用パケットバッファ
byte packetBuffer[NTP_PACKET_SIZE];
// 最後にパケットを送信した時間(ミリ秒)
unsigned long lastSendPacketTime = 0;

// Udpクラス
WiFiUDP udp;

// dow_char() 曜日文字を戻す [Sun,Mon....]
char * dow_char(byte days) {
  char *you[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
  return you[days];
}

// dow() 曜日を示す数値を戻す[0-Sunday, 1-Monday etc.]
byte dow(int y, int m, int d) {
  static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
//  y -= m < 3;
  if (m < 3) y--;
  return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7;
}

// Calculate CRC
uint32_t calculateCRC32(const uint8_t *data, size_t length)
{
  uint32_t crc = 0xffffffff;
  while (length--) {
    uint8_t c = *data++;
    for (uint32_t i = 0x80; i > 0; i >>= 1) {
      bool bit = crc & 0x80000000;
      if (c & i) {
        bit = !bit;
      }
      crc <<= 1;
      if (bit) {
        crc ^= 0x04c11db7;
      }
    }
  }
  return crc;
}

// Print RTC memory
void printMemory(int sz) {
  char buf[3];
//  for (int i = 0; i < sizeof(rtcData); i++) {
  for (int i = 0; i < sz; i++) {
    sprintf(buf, "%02X", rtcData.data[i]);
    Serial.print(buf);
    if ((i + 1) % 32 == 0) {
      Serial.println();
    }
    else {
      Serial.print(" ");
    }
  }
  Serial.println();
}

// Connect AP
void setup_wifi() {
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.println();
  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());
}

// Receive MQTT topic
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
}

// 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");
      // ... and resubscribe
      client.subscribe("inTopic");
    } 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
unsigned long sendNTPpacket(IPAddress& 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();
}

/*
  * UnixTimeをYY/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));
}

/*
  * 現在日時をYY/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()
{
  Serial.begin(9600);
  delay(500);
  Serial.println();
  Serial.println();
  Serial.println("Wake Up");
 
  // RTC memoryからデータを読み込む
  if (ESP.rtcUserMemoryRead(0, (uint32_t*) &rtcData, sizeof(rtcData))) {
    Serial.println("Read: ");
    printMemory(10);
  // 読み込んだデータでCRC32を計算する
    uint32_t crcOfData = calculateCRC32(((uint8_t*) &rtcData) + 4, sizeof(rtcData) - 4);
    Serial.print("CRC32 of data: ");
    Serial.println(crcOfData, HEX);
    Serial.print("CRC32 read from RTC: ");
    Serial.println(rtcData.crc32, HEX);
  // CRC32が一致しないので初回起動時
    if (crcOfData != rtcData.crc32) {
      Serial.println("CRC32 in RTC memory doesn't match CRC32 of data. Data is probably invalid!");
      for (int i = 0; i < sizeof(rtcData); i++) {
        rtcData.data[i] = 0;
      }
    }
  // CRC32が一致したのでResetからの復帰
    else {
      Serial.println("CRC32 check ok, data is probably valid.");
    }
  }
 
  // NTPクライントモード
  if (rtcData.data[0] == 0) {
    // Wifi接続
    setup_wifi();
    Serial.println("Starting UDP");
    udp.begin(localPort);

  // 最初の時刻リクエストを送信
    sendNTPpacket(timeServer);

  // Resetからの復帰
  } else {
  // 時刻設定
    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);
    client.setCallback(callback);
    server_connect();

  // MQTT送信
    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);
  }
  lastSendPacketTime = millis();
}



void loop()
{
  static int counter=0;
  byte DayOfWeek;

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

  // MQTT送信
  if (now - lastSendPacketTime > 1000) { // 1秒経過
    lastSendPacketTime = now;
    counter++;
    if (counter > INTERVAL) {
      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);
    // バッファに受信データを読み込む
    udp.read(packetBuffer, NTP_PACKET_SIZE); // 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);
    showTime("UTC",epoch);
    showTime("JST",epoch + (9 * 60 * 60));

    // Timeライブラリに時間を設定(UNIXタイム)
    // 日本標準時にあわせるために+9時間しておく
    setTime(epoch + (9 * 60 * 60));

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

    // CRC32を再計算
    rtcData.crc32 = calculateCRC32(((uint8_t*) &rtcData) + 4, sizeof(rtcData) - 4);

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

    // Rset
    WiFi.disconnect();
    ESP.reset();
  }
}

MQTT-SubscriberはOrangePi-PCを使いました。
このサーバもNTPで時刻同期をしています。
一番左のサーバーのメッセージ受信時刻と、一番右のMQTTのメッセージの時刻(ESPの時刻)を比べてみると、
常にメッセージの時刻が2秒ほど遅れていますが、これは最初の時刻合わせの時の誤差と思います。
12時間以上経過しましたが、2秒以上の差にはなっていません。


通電中のESPの内蔵時計の精度は信頼できることが分かりました。