ESP32

ESP32でNTPによる時刻同期とRTC設定

ESP32からWiFiを使用してNTPサーバーへの時刻同期を行います。また、取得した時刻をRTCに設定して、電源を落としても時間を保持し続ける時計を作ってみたいと思います。

ESP32とRTC(リアルタイムクロック)の接続

ESP32とRTCモジュールを接続していきます。

RTCの準備

今回は、DS3231を使用したリアルタイムクロックボード「ZS-042」を用意しました。このボードは、「CR2032 3V」の電池が必要になるので別途用意します。

ESP32とRTCを接続

ESP32とRTCモジュールを接続します。

I2C通信による制御になるため、GND、VCC、SDA、SCLの4つをESP32に接続します。

NTPサーバーと時刻同期

NTPサーバーへの同期とRTCへの時刻設定についてプログラムを作成していきます。

RTCのライブラリの追加

RTC(DS3231)を使用するためのライブラリをインストールします。

Arduino IDEを起動して、メニューの「ツール」→「ライブラリを管理」を選択します。

表示した「ライブラリマネージャ」のテキストボックスに「rtc ds3231」を入力します。

複数候補が表示されるので、「DS3232RTC by Jack Christensen」を探してインストールします。

NTPサーバーへの同期とRTCへ時刻設定

ライブラリのインストールが完了したらプログラムを作成していきます。

処理概要は、以下になります。

  • 起動時にWiFiに接続します。
  • NTPサーバーとESP32の内蔵時計を同期します。
  • ESP32の内蔵時計の日時をRTCに設定します。
  • 1秒ごとに、RTCから取得した日時をシリアルモニタに表示します。
#include <WiFi.h>
#include <time.h>
#include <DS3232RTC.h>

char ssid[] = "ssid";
char pass[] = "password";

DS3232RTC myRTC(false);

void setup() {
  struct tm timeInfo;
  
  Serial.begin(115200);
  myRTC.begin();
  
  WiFi.begin(ssid, pass);
  while(WiFi.status() != WL_CONNECTED){
    delay(500);
  }

  configTzTime("JST-9", "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
  getLocalTime(&timeInfo);

  setTime(timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec,
              timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900);
  myRTC.set(now());
}

void loop() {
  DispTime();
  delay(1000);
}

void DispTime() {
  tmElements_t tm;
  myRTC.read(tm);
  
  // 日時表示
  char dateBuf[32];
  sprintf(dateBuf,
          "%04d/%02d/%02d %02d:%02d:%02d", 
          tm.Year + 1970,
          tm.Month,
          tm.Day, 
          tm.Hour,
          tm.Minute,
          tm.Second);
  Serial.println(dateBuf);
}

プログラムについて簡単に説明していきます。

1~3行目で、WiFi、time、DS3232RTC を使用するため、ヘッダの読み込みをしています。

8行目で、DS3232RTC クラスをインスタンス化しています。

14行目の「myRTC.begin()」で、I2Cバスの初期化を行っています。

21行目でローカルのタイムゾーンとNTPサーバーを3カ所設定しています(NTPサーバーは1~3個設定かのうです)。タイムゾーンは、日本標準時と協定世界時の時差を設定します(UTC +0900 (+9時間00分))。

22行目の「getLocalTime()」で内部時刻を取得して、24~26行目でRTCに時刻を設定しています。

「loop()」関数では、1秒ごとにRTCから取得した日時をシリアルモニタに表示しています。

RTCからの時刻の取得は、36行目の「myRTC.read()」で行っています。

ここで使用している「tmElements_t」の定義は以下になるようです。
※Timelib.hを参照

typedef struct  { 
  uint8_t Second; 
  uint8_t Minute; 
  uint8_t Hour; 
  uint8_t Wday;   // day of week, sunday is day 1
  uint8_t Day;
  uint8_t Month; 
  uint8_t Year;   // offset from 1970; 
} 	tmElements_t, TimeElements, *tmElementsPtr_t;

RTCから取得した「tmElements_t」の値をsprintf()で整形してシリアルモニタに表示しています。

動作確認

実際に動作確認をしてみます。

Arduinoのシリアルモニタを表示して、出力される時間を確認してみました。パソコンの日時と一致しています。もともとおかしな時間が表示されていたのですが、ネットワーク接続してNTPサーバーと正常に同期できたようです。

ただ、このプログラムだと起動時にNTPサーバーと同期してしまうため、RTCが正常に動作しているかちょっと不安になります。

ボタン押下時に、NTPサーバーへの同期を行う

上記までのプログラムでは、ESP32起動時にNTPサーバーと同期してしまい、本当にRTCで日時を刻み続けているのか確認することができませんでした。

なので、ボタンを押したときにWiFiに接続して、NTPサーバーとの同期とRTCへの日時設定を行うようにプログラムを改造します。

ボタンを以下のように追加いたしました。

プログラム作成

ボタン押下時にNTPサーバーと同期するように、プログラムを改造していきます。

処理概要は、以下になります。

  • 1秒ごとに、RTCから取得した日時をシリアルモニタに表示します。
  • ループ処理内で、ボタン押下の監視を行います。
  • ボタンが押下時されたら、以下のNTP同期処理を開始します。
    • WiFiに接続します。
    • NTPサーバーとESP32の内蔵時計を同期します。
    • ESP32の内蔵時計の日時をRTCに設定します。
    • WiFiを切断します。
#include <WiFi.h>
#include <time.h>
#include <DS3232RTC.h>

const int BTN1 = 15;  // ボタン入力ピン
unsigned char lastBtnSt = 0; // 前回ボタン状態
unsigned char fixedBtnSt = 0; // 確定ボタン状態
unsigned long smpltmr = 0;  // サンプル時間
unsigned long disptmr = 0;  // 日時表示更新
int ntpStart = 0; // NTP同期
int eventSt = 0;  // 処理イベント状態

char ssid[] = "ssid";
char pass[] = "password";

DS3232RTC myRTC(false);

void setup() {
  pinMode(BTN1, INPUT_PULLUP);  // プルアップ設定
  Serial.begin(115200);
  myRTC.begin();
}

void loop() {
  btnEvent();  // ボタンイベント

  SyncNtpProc();  // NTP同期
  
  if(millis() - disptmr > 1000){
    DispTime();
    disptmr = millis();
  }
}

// 日時表示
void DispTime() {
  tmElements_t tm;
  myRTC.read(tm);
  
  // 日時表示
  char dateBuf[32];
  sprintf(dateBuf,
          "%04d/%02d/%02d %02d:%02d:%02d", 
          tm.Year + 1970,
          tm.Month,
          tm.Day, 
          tm.Hour,
          tm.Minute,
          tm.Second);
  Serial.println(dateBuf);
}

// ボタンイベント
void btnEvent() {
  if(ntpStart > 0) return;
  
  if(millis() - smpltmr < 40) return;
  smpltmr = millis();
   
  int btnSt = digitalRead(BTN1);
  int cmp = (btnSt == lastBtnSt);
  lastBtnSt = btnSt;
 
  if(!cmp) return;
 
  if(!btnSt && (btnSt != fixedBtnSt)){
    ntpStart = 1;
    fixedBtnSt = btnSt;
  }
 
  if(btnSt){
    fixedBtnSt = btnSt;
  }
}

// NTP同期処理
void SyncNtpProc() {
  if(ntpStart <= 0) return;
  
  switch(eventSt){
    case 0: // WiFi接続
      WiFi.begin(ssid, pass);
      eventSt = 1;
    break;
    case 1: // 接続確認
      if(WiFi.status() == WL_CONNECTED){
        eventSt = 2;
      }
    break;
    case 2: // NTP同期
      SyncNtp();
      eventSt = 3;
    break;
    case 3: // WiFi切断
      WiFi.disconnect();
      eventSt = 0;
      ntpStart = 0;
    break;
    default:
    break;
  }
}

// NTP同期
void SyncNtp() {
  struct tm timeInfo;
  configTzTime("JST-9", "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");
  getLocalTime(&timeInfo);
  setTime(timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec,
          timeInfo.tm_mday, timeInfo.tm_mon + 1, timeInfo.tm_year + 1900);
  myRTC.set(now());
}

プログラムについて簡単に説明します。

日時をシリアルモニタに表示する処理だけだった「loop()」関数を変更します。

void loop() {
  btnEvent();  // ボタンイベント

  SyncNtpProc();  // NTP同期
  
  if(millis() - disptmr > 1000){
    DispTime();
    disptmr = millis();
  }
}

25行目に、ボタンイベントを監視する関数を追加します。

27行目には、ボタンが押された後に実行されるNTPサーバーとの同期関数を追加します。

また、delay(1000)で1秒おきに処理を行っていたところを、29行目で1秒経過したら日時をシリアルモニタに表示するように変更しています。

NTP同期処理「SyncNtpProc()」関数は、ループの処理を止めないように1つずつ順番に処理を行うようにしています。処理を実行したら「eventSt」に次の「case文」の番号を指定して、順番に処理を行う方法になります。

// NTP同期処理
void SyncNtpProc() {
  if(ntpStart <= 0) return;
   
  switch(eventSt){
    case 0: // WiFi接続
      WiFi.begin(ssid, pass);
      eventSt = 1;
    break;
    case 1: // 接続確認
      if(WiFi.status() == WL_CONNECTED){
        eventSt = 2;
      }
    break;
    case 2: // NTP同期
      SyncNtp();
      eventSt = 3;
    break;
    case 3: // WiFi切断
      WiFi.disconnect();
      eventSt = 0;
      ntpStart = 0;
    break;
    default:
    break;
  }
}

82行目でWiFi接続処理を行い、86行目で接続確認を行っています。WiFiに接続が成功すると91行目でNTPサーバーとの同期を行い、95行目でWiFiを切断しています。

エラー処理や、タイムアウト処理が入っていないので、何か問題があると動作がおかしくなると思いますので、必要に応じてエラー処理等を追加して下さい。

動作確認

実際に動作確認をしてみます。

RTCの時間を「2000/01/01 00:00:00」に変更してから、動作を確認しています。

起動時は設定した時間になっていましたが、ボタンを押した後すぐに、パソコンの日時と一致しています。正常にNTPサーバーと同期ができたようです。

いったんESP32の電源をOFFして、再度ESPの電源ON後にシリアルモニタの表示を確認したところ、パソコンの時間と一致していました。ちゃんとRTCが動作していることが確認できました。

3日後にRTCの時刻を確認してみたところ、3秒程度進んでいました。許容範囲ぐらいかなと思います。

まとめ

ESP32でNTPサーバーへ時刻同期を行い、RTCに設定するまで試してみました。

WiFiに接続するだけでなく、実際のネットワークのNTPサーバーと接続して時刻同期が簡単に行えたので、ちょっと感動です。

今のところ全然アイデアは浮かんできませんが、ネットワークを介した処理をいろいろと試していければと思います。

また、RTCを使用して時間保持もできるようになったので、ESP32のスリープ処理等も試していきたいと思います。

【参考図書】