ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【M5StickC Plus/Arduino】M5StickC Plusで時計を作る

 M5StickC Plusには「ESP32-PICO-D4」が使われているので、ESP32に搭載されているRTC(リアルタイムクロック)を使用することができます。
 WiFi通信もできるのでインターネットに接続して時刻合わせをすることもできます。

 つまり機能的には時計として使うことも可能というわけです。

 なので今回はM5StickC PlusのRTCを使って簡単な時計を作ってみたのでその備忘録です。

 では、始めます。


1:RTCの使い方
 RTCの日時は以下のように取得できます。

#include <M5StickCPlus.h>

RTC_DateTypeDef rtc_date;  // 日付
M5.Rtc.GetData(&rtc_date); // 日付の取り出し

int year  = rtc_date.Year;    // 年
int month = rtc_date.Month;   // 月
int date  = rtc_date.Date;    // 日
int wd    = rtc_date.WeekDay; // 曜日(0~6で日曜日から順番になっている)


RTC_TimeTypeDef rtc_time;  // 時刻
M5.Rtc.GetTime(&rtc_time); // 時刻の取り出し

int hours   = rtc_time.Hours;   // 時
int Minutes = rtc_time.Minutes; // 分
int Seconds = rtc_time.Seconds; // 秒

 またインターネットに接続できている状態で時刻合わせをするには以下のようにします。

#include <M5StickCPlus.h>

// 書き方
configTime([時差], [サマータイム有効/無効], [接続NTPサーバ(複数可能)]);

// 例:時差が9時間(日本)でサマータイム無効で「ntp.nict.jp」と「ntp.jst.mfeed.ad.jp」を使う
configTime(3600L*9, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");

 M5StickC Plusは出荷時点ではRTCの時刻は特に設定されていないので、ちゃんとした時刻を知りたい場合は一度は時刻合わせをしておくとよいです。


2:時計のサンプル
 RTCの使い方がわかったので時計のサンプルスケッチを作りました。

・RTCClock.h

/**
 * RTCクロック関係
 */
#include <M5StickCPlus.h>
#include <WiFi.h>

class RTCClock {
  public:
    const char *wd[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
    const char *defaultFormat = "%04d/%02d/%02d %s\n%02d:%02d:%02d";
    const int O_YEAR   = 0;
    const int O_MONTH  = 1;
    const int O_DAY    = 2;
    const int O_WD     = 3;
    const int O_HOUR   = 4;
    const int O_MINUTE = 5;
    const int O_SECOND = 6;

    RTCClock();
    void setDefault();
    void setSpriteArea(int width, int height);
    void setShowPosition(int x, int y);
    void setFontAndSize(int fontNum, int textSize);
    void setTextAndBgColor(uint32_t textColor, uint32_t bgColor);
    void setFormat(const char *str, ...);
    void setWiFi(const char *ssid, const char *password);
    void connectWifFi();
    void syncTime();
    void disconnectWifFi();
    
    RTC_TimeTypeDef getRTCTime();
    RTC_DateTypeDef getRTCDate();
    void showDateAndTime();
    void deleteDateAndTime();
    void updateDateAndTime();

  private:
    static const int MAX_SHOW_DATE_TIME = 7;
    static const int HIDE_ORDER = -1;

    TFT_eSprite _sprite = TFT_eSprite(&M5.Lcd);

     // wifi関係
    const char *_ssid_p;
    const char *_password_p;

    // 表示フォーマット関連
    const char *_format;
    int _orderArray[MAX_SHOW_DATE_TIME];
    int _orderArraySize;

    // ディスプレイ表示関連
    bool _showFlg;
    String _dateFormat;
    String _timeFormat;
    int _fontNum;
    int _textSize;
    uint32_t _text_color;
    uint32_t _bg_color;
    int _x;
    int _y;
    int _width;
    int _height;

    int getArgumentsSize(const char *str);
    void printDateAntTime(int a[]);
};

// コンストラクタ
RTCClock::RTCClock() {
  // 初期化
  _showFlg = true;

  _ssid_p = "";
  _password_p = "";

  setDefault(); // デフォルト値を設定
}

// WiFi以外の情報のデフォルト値を設定する関数
void RTCClock::setDefault() {
  _format = defaultFormat;
  for (int i=0; i<MAX_SHOW_DATE_TIME; i++) {
    _orderArray[i] = i;
  }
  _orderArraySize = MAX_SHOW_DATE_TIME;

  _fontNum = 1;
  _textSize = 1;
  _text_color = TFT_WHITE;
  _bg_color = TFT_BLACK;

  _x = 0;
  _y = 0;
  _width  = 84 * _textSize;
  _height = 16 * _textSize;
}

// 表示するスプライトの範囲を設定する関数
void RTCClock::setSpriteArea(int width, int height) {
  _width  = width;
  _height = height;
}

// 表示の左上の位置を設定する関数
void RTCClock::setShowPosition(int x, int y) {
  _x = x;
  _y = y;
}

// フォントとサイズを設定する関数
void RTCClock::setFontAndSize(int fontNum, int textSize) {
  _fontNum = fontNum;
  _textSize = textSize;
}

// テキストの色と背景色を設定する関数
void RTCClock::setTextAndBgColor(uint32_t textColor, uint32_t bgColor) {
  _text_color = textColor;
  _bg_color = bgColor;
}

// 表示フォーマットを設定する関数
void RTCClock::setFormat(const char *str, ...) {
  // フォーマットと使用する引数の数を設定
  _format = str;
  _orderArraySize = getArgumentsSize(str);

  // 初期化
  for (int i=0; i<MAX_SHOW_DATE_TIME; i++) {
    _orderArray[i] = HIDE_ORDER;
  }

  // 可変部分の引数の値を設定
  va_list ap;
  va_start(ap, str);
  for (int i=0; i<_orderArraySize; i++) {
    _orderArray[i] = va_arg(ap, int);
  }
  va_end(ap);
}

// WiFiのSSIDとPassを設定する関数
void RTCClock::setWiFi(const char *ssid, const char *password) {
  _ssid_p = ssid;
  _password_p = password;
}

// WiFiに接続する関数
void RTCClock::connectWifFi() {
  _showFlg = false;
  _sprite.fillSprite(_bg_color);
  _sprite.setCursor(0, 0);
  _sprite.setTextFont(1);
  _sprite.setTextSize(1);
  _sprite.print("connecting");

  int counter = 0;
  WiFi.begin(_ssid_p, _password_p);

  while (counter < 10) {
    _sprite.pushSprite(_x, _y);
    if (WiFi.status() == WL_CONNECTED) {
      _sprite.print(" OK");
      _sprite.pushSprite(_x, _y);
      break;
    } else {
      counter++;
      _sprite.print(".");
      delay(500);
    }
  }
  delay(1000);
  _showFlg = true;
}

// 時刻合わせをする関数
void RTCClock::syncTime() {
  _showFlg = false;
  _sprite.fillSprite(_bg_color);
  _sprite.setCursor(0, 0);
  _sprite.setTextFont(1);
  _sprite.setTextSize(1);

  // WiFiに繋がっていたら時刻合わせをする
  if (WiFi.status() == WL_CONNECTED) {
    configTime(3600L*9, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
    _sprite.print("sync OK");
  } else {
    _sprite.print("sync NG");
  }
  _sprite.pushSprite(_x, _y);
  delay(1000);
  _showFlg = true;
}

// WiFiを切断する関数
void RTCClock::disconnectWifFi() {
  _showFlg = false;
  _sprite.fillSprite(_bg_color);
  _sprite.setCursor(0, 0);
  _sprite.setTextFont(1);
  _sprite.setTextSize(1);

  if (WiFi.status() == WL_CONNECTED) {
    WiFi.disconnect(true);
    WiFi.mode(WIFI_OFF);
    _sprite.print("disconnected.");
  } else {
    _sprite.print("disconnect NG");
  }

  _sprite.pushSprite(_x, _y);
  delay(1000);
  _showFlg = true;
}

// 引数の数を計算する関数(%の数で見ている)
int RTCClock::getArgumentsSize(const char *str) {
  int sizeCount = 0;
  while( *str != 0x00 ) {
    char c = str[0];
    if (c == '%') {
      sizeCount += 1;
    }
    str++;
  }
  return sizeCount;
}

// 時刻を取得する関数
RTC_TimeTypeDef RTCClock::getRTCTime() {
  RTC_TimeTypeDef rtc_time;  // 時刻
  M5.Rtc.GetTime(&rtc_time); // 時刻の取り出し
  return rtc_time;
}

// 日付を取得する関数
RTC_DateTypeDef RTCClock::getRTCDate() {
  RTC_DateTypeDef rtc_date;  // 日付
  M5.Rtc.GetData(&rtc_date); // 日付の取り出し
  return rtc_date;
}

// 表示する関数
void RTCClock::showDateAndTime() {
  _showFlg = true;
  _sprite.setColorDepth(16);
  _sprite.createSprite(_width, _height);
}

// 非表示にする関数
void RTCClock::deleteDateAndTime() {
  // スプライト全体を_bg_colorで塗りつぶしてメモリ開放
  _sprite.fillSprite(_bg_color);
  _sprite.pushSprite(_x, _y);
  _sprite.deleteSprite();
  _showFlg = false;
}

// 日時を更新する関数
void RTCClock::updateDateAndTime() {
  if (!_showFlg) { return; }

  _sprite.fillSprite(_bg_color); // 塗りつぶし

  _sprite.setCursor(0, 0);
  _sprite.setTextFont(_fontNum);
  _sprite.setTextColor(_text_color);
  _sprite.setTextSize(_textSize);

  RTC_TimeTypeDef rtc_time = getRTCTime(); // 時刻
  RTC_DateTypeDef rtc_date = getRTCDate(); // 日付

  int dateTimeArray[MAX_SHOW_DATE_TIME] = {
    rtc_date.Year,
    rtc_date.Month,
    rtc_date.Date,
    rtc_date.WeekDay,
    rtc_time.Hours,
    rtc_time.Minutes,
    rtc_time.Seconds
  };

  // 日付と時刻をフォーマットに合わせて表示
  printDateAntTime(dateTimeArray);

  // ディスプレイに表示
  _sprite.pushSprite(_x, _y);
}

// フォーマットに合わせて表示する関数
void RTCClock::printDateAntTime(int a[]) {
  String s = _format;
  int oa[MAX_SHOW_DATE_TIME] = { -1,-1,-1,-1,-1,-1,-1 };

  // 配列を詰め替え
  int count = 0;
  for (int i=0; i<_orderArraySize; i++) {
    int orderNum = _orderArray[i];
    if (orderNum == O_WD) {
      s.replace("%s", wd[a[O_WD]]); // %sを曜日に変更
    } else {
      oa[count] = orderNum;
      count++;
    }
  }

  // それぞれのサイズに合わせてフォーマット
  switch (count) {
    case 1:
       _sprite.printf(s.c_str(), a[oa[0]]);
      break;
    case 2:
       _sprite.printf(s.c_str(), a[oa[0]], a[oa[1]]);
      break;
    case 3:
       _sprite.printf(s.c_str(), a[oa[0]], a[oa[1]], a[oa[2]]);
      break;
    case 4:
       _sprite.printf(s.c_str(), a[oa[0]], a[oa[1]], a[oa[2]], a[oa[3]]);
      break;
    case 5:
       _sprite.printf(s.c_str(), a[oa[0]], a[oa[1]], a[oa[2]], a[oa[3]], a[oa[4]]);
      break;
    case 6:
       _sprite.printf(s.c_str(), a[oa[0]], a[oa[1]], a[oa[2]], a[oa[3]], a[oa[4]], a[oa[5]]);
      break;
    case 7:
      // 7になることはないはずだが一応設定しておく
       _sprite.printf(s.c_str(), a[oa[0]], a[oa[1]], a[oa[2]], a[oa[3]], a[oa[4]], a[oa[5]], a[oa[6]]);
      break;
  }
}


・M5StickCPlus_clock.ino

#include <M5StickCPlus.h>
#include "RTCClock.h"

const char *SSID_CHAR = "*********"; // 任意のものに書き換える
const char *PASS_CHAR = "********"; // 任意のものに書き換える

RTCClock rtcClock = RTCClock();

void setup() {
  Serial.begin(115200);
  
  M5.begin();
  M5.Axp.ScreenBreath(10);  // 画面の明るさ(7-12)
  M5.Lcd.setRotation(3);    // 画面を横向きに(0-3)
  M5.Lcd.fillScreen(GREEN); // 背景を緑

  // 任意の設定を行う
  setCustomClock();

  // WiFiのSSIDとPASSを設定
  rtcClock.setWiFi(SSID_CHAR, PASS_CHAR);

  // 時刻表示
  rtcClock.showDateAndTime();
}

bool defaultFlg = false;

void loop() {
  M5.update();

  // ボタンAが押された時
  if (M5.BtnA.wasPressed()) {
    // WiFiに接続して時刻合わせをして切断
    rtcClock.connectWifFi();
    rtcClock.syncTime();
    rtcClock.disconnectWifFi();

    defaultFlg = !defaultFlg;

    rtcClock.deleteDateAndTime(); // 一旦消す(設定した背景色で塗りつぶし)
    if (defaultFlg) {
      rtcClock.setDefault(); // デフォルトの設定
    } else {
      setCustomClock();      // 自分の設定
    }
    rtcClock.showDateAndTime(); // 再度表示
  }

  rtcClock.updateDateAndTime();
  delay(100);
}

// 自分の設定
void setCustomClock() {
  int textSize = 2;
  int width    = 84 * textSize;
  int height   = 16 * textSize;

  rtcClock.setSpriteArea(width, height);
  rtcClock.setShowPosition(5, 5);
  rtcClock.setFontAndSize(1, textSize);
  rtcClock.setTextAndBgColor(TFT_PINK, TFT_BLUE);

  const char *formatChar = "%s %04d/%02d/%02d\n%02d:%02d:%02d";
  rtcClock.setFormat(formatChar, rtcClock.O_WD, rtcClock.O_YEAR, rtcClock.O_MONTH, rtcClock.O_DAY, rtcClock.O_HOUR, rtcClock.O_MINUTE, rtcClock.O_SECOND);

  // 以下のようなフォーマットもできる
//  const char *formatChar = "%02d:%02d:%02d";
//  rtcClock.setFormat(formatChar, rtcClock.O_HOUR, rtcClock.O_MINUTE, rtcClock.O_SECOND);
}

 少し解説すると、このサンプルを書き込むと時刻が表示されます。「M5」と書かれたボタンを押すとWiFiに接続して時刻合わせをして切断されます。
 ボタンが押されるとデフォルトと自分で設定した時刻表示が切り替わるようになっています。

 以上がM5StickC Plusで時計を作った内容です。

 IoT機器として使い、時刻毎に何かを行うような場合には何かと便利かと思います。

 今回のサンプルを使って時刻合わせを行うことでかなり近い時刻になるのですが、なぜか自分の場合だと14秒ほど遅れて時刻が表示されるという現象が発生しました。何度時刻合わせをしてもその遅延は変わらなかったので何か知っている方はコメントを頂けると助かります…。


・参考資料