【M5】M5StackとRTCで本格置き時計を作る:DualCoreとM5FontRenderによる滑らかで正確な表示実装

M5Stack・Arduino

構成

プロジェクト名:M5-Rtc

デバイス:M5STACK

技術選定の理由(ここを書くと読者が喜びます)

  • BM8563 (RTC): 「M5Stack本体のタイマーは再起動でリセットされるが、RTCがあれば電源オフでも時刻を維持できる」点。
  • M5FontRender: 「標準フォントやバイナリフォントに比べ、アンチエイリアスが効いた美しい文字表示が可能」な点。
  • DualCore (Task): 「Core 1でセンサー通信やロジック、Core 0で重い描画処理(FontRender)を分担させることで、UIの引っ掛かりをなくす」点。

DualCore(マルチタスク)の実装解説

詰まりポイント

Core 0の役割: Core 0はWi-Fi/Bluetooth通信も担当しているため、delay(1) または vTaskDelay を入れないとウォッチドッグタイマーでリセットがかかる点。

プログラム

変数

// RTC(BM8563)用の構造体
struct RTC_Time {
    uint8_t hour, min, sec;
};
struct RTC_Date {
    uint16_t year;
    uint8_t month, day;
};
RTC_Date now_date;
RTC_Time now_time;
RTC_Date old_date;
RTC_Time old_time;

// DualCore   
TaskHandle_t task_handl; 	//マルチタスクハンドル定義


boolean isDisp_ok = false;     //ディスプレイ表示フラグ
#define MODE_BOOT 0
#define MODE_HOME 1
#define MODE_RTC 2
int8_t task_no=MODE_BOOT;
int8_t last_task_no=0;

初期化処理

void setup() {
  M5.begin();
  Wire.begin(); // I2Cの開始(RTC通信用)
  // --- デバッグ用:RTC時刻初期化 (2026/01/12 00:00:00) ---
  setRTC(2026, 1, 12, 0, 0, 0);
  //
  setup_wifi();
  myIP = WiFi.softAPIP();

  M5.Lcd.fillScreen(BLACK);
  M5.Lcd.setTextColor(WHITE);
  M5.Lcd.setCursor(0, 8);
  M5.Lcd.setTextFont(2);
  M5.Lcd.setTextSize(2);
  //--------------------------------------------------------
  //SDファイルチェック
  // 本システムではSDカードが挿入されていることを必須とする。
  //--------------------------------------------------------
  SD_Exitst=SD.begin(); // SDカードと通信できるか確認

  if(!SD_Exitst){
    #ifdef DEBUG_MODE
    Serial.println("SD Card is not installed.\n");
    #endif   
     M5.Lcd.println("SD Card is not installed.\n"); //通信できなければエラーを表示
    M5.Lcd.println("please confirm."); //通信できなければエラーを表示
    // SDカードがないと困るので、ここで無限ループさせて先に進ませない
    while (1) {
      delay(1000);
    }
  }
  //--------------------------------------------------------
  //FontFile notexist
  //--------------------------------------------------------
  if (!render.loadFont("/ombre.ttf")) {
    #ifdef DEBUG_MODE
    Serial.println("FontFile not Exist");
    #endif
    M5.Lcd.println("FontFile Not Exist.");
    while (1) {
      delay(1000);
    }
  }
  //
  xTaskCreatePinnedToCore(&taskDisplay, "taskDisplay", 8192, NULL, 10, &task_handl, 0);
  delay(500); 
  task_no=MODE_HOME;
  isDisp_ok=true;
}

メイン

void loop() {
  M5.update();      // ボタン状態の更新
  button_action();  // ボタンが押されたら task_no を切り替え、isDisp_ok = true にする

  // --- 時刻のリアルタイム更新処理 ---
  static uint8_t last_sec = 99; // 前回の秒を記憶する変数
  getRTC(&now_date, &now_time); // RTCから最新時刻を取得

  // 現在のモードが RTC の時だけ、秒が変わった瞬間に更新フラグを立てる
  if (task_no == MODE_RTC) {
    if (now_time.sec != last_sec) {
      isDisp_ok = true;   // 描画タスクを動かす
      last_sec = now_time.sec; // 秒を更新
    }
  }
  delay(10); // 10ms待機(ボタン反応と更新精度のバランス)
}

RTC関連

// RTCに時刻を書き込む (2028/01/18 00:00:00 など)
void setRTC(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t min, uint8_t sec) {
  Wire.beginTransmission(0x51);
  Wire.write(0x02); // 秒レジスタから開始
  Wire.write(decToBcd(sec));
  Wire.write(decToBcd(min));
  Wire.write(decToBcd(hour));
  Wire.write(decToBcd(day));
  Wire.write(0x00); // 曜日(無視)
  Wire.write(decToBcd(month)); // 月(ここに含まれるセンチュリービットは通常0で2000年〜2099年)
  Wire.write(decToBcd(year % 100)); // 年の下2桁
  Wire.endTransmission();
}
// RTCから読み出す
void getRTC(RTC_Date* d, RTC_Time* t) {
  Wire.beginTransmission(0x51);
  Wire.write(0x02);
  Wire.endTransmission();
  Wire.requestFrom(0x51, 7);

  t->sec   = bcdToDec(Wire.read() & 0x7F);
  t->min   = bcdToDec(Wire.read() & 0x7F);
  t->hour  = bcdToDec(Wire.read() & 0x3F);
  d->day   = bcdToDec(Wire.read() & 0x3F);
  Wire.read(); // 曜日スキップ
  d->month = bcdToDec(Wire.read() & 0x1F);
  d->year  = bcdToDec(Wire.read()) + 2000;
}

表示タスク

void taskDisplay(void *pvParameters) {
  while (true) {
    if (isDisp_ok) {
      // モードが切り替わった瞬間だけ画面全体を黒塗りする
      if (task_no != last_task_no) {
        M5.Lcd.fillScreen(BLACK);
        last_task_no = task_no;
      }
      switch (task_no) {
        case MODE_BOOT:
          break;
        case MODE_HOME: { // ★波括弧を追加
          // ★ 描画先を本体ディスプレイのポインタに戻す
          render.setTextColor(WHITE);
          render.setTextSize(24);
          render.setTextColor(GREEN);
          render.setCursor(65, 20);
          render.printf("Wifi Router\n");
          render.setTextSize(20);
          render.setTextColor(WHITE);
          render.setCursor(30, 80);
          // IPアドレス取得
          String str = String(myIP[0]) + '.' + String(myIP[1]) + '.' + String(myIP[2]) + '.' + String(myIP[3]);
          render.printf("IP:%s\n", str.c_str());
          render.setCursor(30, 110);
          render.printf("ID=ombre_wifi");
          render.setCursor(30, 140);
          render.printf("Pass=pass1234");
          break;
        }
        case MODE_RTC:
          // --- 日付エリアの更新 ---
          // 日付が表示される範囲(x=30, y=80)だけをピンポイントで消去
          // 2. 日付に変化があるかチェック
          if (!isDateSame(now_date, old_date)) {
            #ifdef DEBUG_MODE
            Serial.println("renew date\n");
            #endif 
            M5.Lcd.fillRect(30, 80, 260, 26, BLACK); 
            render.setCursor(30, 80);
            render.setTextColor(WHITE);
            render.printf("Date: %04d/%02d/%02d", now_date.year, now_date.month, now_date.day);
            // 3. 次回比較のために保存
            old_date = now_date;
          }
          // --- 時刻エリアの更新 ---
          // 時刻が表示される範囲(x=30, y=110)だけをピンポイントで消去
          // 4. 時刻に変化があるかチェック
          if (!isTimeSame(now_time, old_time)) {
            #ifdef DEBUG_MODE
            Serial.println("renew time\n");
            #endif 
            M5.Lcd.fillRect(30, 110, 260, 26, BLACK); 
            render.setCursor(30, 110);
            render.printf("Time: %02d:%02d:%02d", now_time.hour, now_time.min, now_time.sec);
            // 3. 次回比較のために保存
            old_time = now_time;
          }
          break;
      }
      if (isDisp_ok) {
        isDisp_ok = false;
      }
    }
    vTaskDelay(10 / portTICK_PERIOD_MS);
  }
}

※一部作成した関数は記載省略

今後

日次の設定方法については今回WifiRooter機能も搭載したので現在、UDP、Socket通信にするか検討中

日時設定における通信方式の比較

特徴UDP (User Datagram Protocol)Socket (TCP)
信頼性送りっぱなし(届かない可能性あり)確実に届く(ハンドシェイクあり)
速度速い、オーバーヘッドが少ないUDPよりは遅い(接続処理が必要)
実装難易度非常に簡単少し複雑
向いている用途時刻同期(NTPなど)、一斉配信確実な設定変更、双方向通信

補足

なぜ今回DualCoreにしたのか?

今後WiFi通信(Socket)を追加した際に、通信処理による描画の遅延を防ぐため。

コメント