ESP8266のReset動作 その2


前回、ESP.reset() や ESP.restart() で強制的にリセットしても、RTCユーザエリアはクリアされないことが分かりました。
RTCメモリーのサイズは768バイトですが、先頭の256バイトはシステムが使うので、ユーザが使えるのは512バイトです。
RTCメモリーサイズに関しては、以下に解説があります。
https://github.com/esp8266/Arduino/issues/2875

そこで、この性質を利用した時刻合わせを紹介します。
ESPの内蔵タイマーが正確なら、RTCモジュールは使わなくても正確に時間を刻むことができます。
使用したスケッチは以下のスケッチです。
@初回起動時にはUDPでNTPサーバーに接続
ANTPを使って現在日時を取得
BRTCユーザエリアに現在日時をセット
C強制的にリセット
Dリセットからの復帰時に、RTCユーザエリアから現在日時を読み込んで内部の時計を設定

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

/*
ESP.reset()/ESP.resart()ではRTC User Memoryがクリアされない。
この性質を利用した時刻設定のサンプル。
初回起動時のみWiFiに接続してNTPに時刻問い合わせ。
RTC User Memoryに現在日時を設定し強制Reset。
Resetからの復帰時にRTC User Memoryから日時を取り出して設定。
ESP8266 core for Arduino V2.4.2以上が必要
*/

#include <ESP8266WiFi.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";

#define INTERVAL 60

//RTC memory(512Byte)の定義
struct {
  uint8_t  data[512]; // NTPから取り出した年月日時分秒
} 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;


// 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クライントモード
    Serial.println("Starting UDP");
    // 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("WiFi connected");
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    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");
  }
  lastMillis = millis();
}



void loop()
{
  static int counter=0;

  unsigned long now = millis();
  if (now < lastMillis) lastMillis = now;
  if (now - lastMillis > 1000) { // 1秒経過
    lastMillis = now;
    counter++;
    if (counter > INTERVAL) { // 60秒経過
      showNow("Loop");
      counter=0;
    }
  }

  // NTPサーバからのパケット受信(初回起動時のみ実行される)
  int cb = udp.parsePacket();
  if (cb) {
    Serial.print("parsePacket 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();
  }
}

シリアルモニターの表示は以下の通りです。
ESP.getResetReason()=External System
reset reason=6
rtcUserMemoryRead Success
Starting UDP
Wait for WiFi..........WiFi connected
IP address: 192.168.10.144
sending NTP packet...
parsePacket length=48
reading NTP packet...
Seconds since Jan 1 1900 = 3837975121
Unix time = 1628986321
UTC:2021/8/15 0:12:1
JST:2021/8/15 9:12:1
rtcUserMemoryWrite Success
Do ESP.restart

ESP.getResetReason()=Software/System restart
reset reason=4
rtcUserMemoryRead Success
rtcData=229 8 15 9 38 57
Before:1970/1/1 0:0:1
After:2021/8/15 9:38:57
Loop:2021/8/15 9:39:58
Loop:2021/8/15 9:40:59
Loop:2021/8/15 9:42:0
Loop:2021/8/15 9:43:1
Loop:2021/8/15 9:44:2
Loop:2021/8/15 9:45:3
Loop:2021/8/15 9:46:4
Loop:2021/8/15 9:47:5
Loop:2021/8/15 9:48:6
Loop:2021/8/15 9:49:7

ビルド直後はExternal Systemで起動します。
NTPで時刻問い合わせを行い、その結果をRTCユーザエリアに設定し強制的に再起動します。

Reset復帰直後(Before)は時計は初期値を示しますが、RTCユーザエリアの時間を設定することで、
Loop内で正しい時刻を刻みます。

ESP8266の内蔵タイマーの精度が気になったので調べてみました。

続く....