ESP32マイコンボードとMAX7219 LEDディスプレイ4連x2でNTPクロックを作ってみた

NTPクロックを作ってみた。

電波時計が自動的に時刻合わせをしてくれないので正確な時刻を表示する時計が欲しくなったというのは前の記事で書いた。
で、Arduino互換のマイコンカードと8x8のLEDマトリクスディスプレイが4つ繋がったものを2つ買ってそれを接続して表示できるうになったというのも書いた。マイコンボードでNTP時刻合わせをするというのも書いた。
今回は、NTPで正しい時刻を取得してその時刻をLDEディスプレイに表示する。つまり元々の目的のものを作る。

普通に日付と時刻を表示しようとしたらディスプレイの表示範囲を超過してしまった
MAX7219 LEDディスプレイモジュールは4連を2個。元々の予定では4連を2つ重ね(2行)にするつもりでいたが、時刻表示(HH:MM:SS)を4連1枚に表示するのは難しいことがわかったので 4連を並べて8連(1行)にした。4連にHH:MM:SSを表示しようと思ったらかなり細いフォントを使うか縮小フォントを使うかになるけど1モジュールが8x8のLEDディスプレイなので細い字にするのも限界がある。読みやすさを犠牲にするのはイヤなので。
ちなみに、工夫なしに普通に日付と時刻を表示しようとしたら表示範囲に収まりきらずに秒の表示が切れてしまった。

何をどのように表示するかを考えた。

  • 時刻は秒まで表示する。HH:MM:SS形式で、これは絶対。
  • 時刻は常時表示にしたい。
  • 日付も表示したい。
  • 曜日も表示したい。
  • 西暦の年も表示したい。できたら4桁で。
  • 他の情報は要らない。

以上から、時刻(HH:MM:SS)を常時表示するエリアと日付のゾーン(エリア)に2分割する。
時刻のゾーンは固定表示。
「年」、「月/日」、「曜日(英字3文字)」を縦スクロールで上下移動して表示。そして、月/日の表示回数が多くてかつ表示時間を長めにする。
年 → 月/日 → 曜日 → 月/日 → 年 のような繰り返し。

ゾーン分割とアニメーションの表示方法についてはMD_Parolaライブラリに付属のスケッチ例からParola_Zone_TimeMsgを参考にした。

モジュール並びとゾーン分け
2021年10月18日: モジュールの並びとゾーン分けについて、↓の2段落分の説明がヘタクソで書いた本人もなんかよく判らんかったのでこの画像を追加

LEDモジュールは8x8の単体タイプを並べようが4連タイプを並べようが、右から左に制御するモジュール番号が付く。つまり7 6 5 4 3 2 1 0のようになる。
ゾーンを分割すると右からゾーン0、その左にゾーン1・・・のように並ぶ。つまり、モジュールもゾーンも右から左に並ぶ。英数字は左から右に書くので感覚としては逆並び。

時刻表示部分は、HH:MM:SSの8文字、日付側は年表示がYYYYで4文字、月日表示はMM/DDの5文字、曜日は英語の短縮形3文字とする。つまり時刻表示の方が文字数が多いので日付表示と時刻表示の表示範囲を1:1ではなく3:5で分割して表示することにした。4連モジュール2個を使うので4連を上下に並べる(4列x2行)表示はこの時点でムリということになった。
3:5と書いたが、モジュールとゾーンは右から左に0から数字が付くのでモジュール 7, 6, 5 がゾーン1でモジュール4 3, 2, 1, 0 がゾーン0になる。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#include <NTPClient.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include <MD_Parola.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include "GF4x8p.h"  // 4x8 font

//Wi-Fi
#define SSID "Wi-Fi-SSID"
#define WIFIKEY "WI-FI-PASSWORD"

//NTP client
#define ntpServer "NTP Server" //Host name or IP Address
#define tzOffset 32400     // JST = 3600 * 9

//MAX7219
#define MAX_DEVICES 8 // eight modules
#define CLK_PIN   27
#define DATA_PIN  12
#define CS_PIN    14

#define SPEED_TIME  25  //Small numbers are faster. Zero is the fastest.
#define PAUSE_TIME_L  1200 //1200ms for month/day long pause
#define PAUSE_TIME_S  300 //300ms for month/day short pause

#define HARDWARE_TYPE MD_MAX72XX::FC16_HW
// Hardware SPI connection
//MD_Parola P = MD_Parola(CS_PIN, MAX_DEVICES);
MD_Parola P = MD_Parola(HARDWARE_TYPE, DATA_PIN, CLK_PIN, CS_PIN, MAX_DEVICES);

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, tzOffset, 3540000); //59 minutes : 59(min) x 60(sec) x 1000(ms) = 3540000


void setup() {
  WiFi.begin(SSID, WIFIKEY);

  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
  }

  timeClient.begin();

  P.begin(2); // 2 zones
  P.setZone(0, 0, 4);  //00011111 <- Zone 0  for Time display
  P.setZone(1, 5, 7);  //11100000 <- Zone 1  for Date display
  P.setFont(0,GF4x8p);
  P.setFont(1,GF4x8p);
  P.setIntensity(0);  //Darkest
}

void loop() {

  time_t epTime = timeClient.getEpochTime();
  static uint8_t  dsw = 0;
  struct tm  ts;
  char bufD[15], bufT[9]; // "yyyy mm/dd aaa" + 1 = 15,  "hh:mm:ss" + 1 = 9   
  char *parr[15];
  ts = *localtime(&epTime);

  timeClient.update();

  P.displayAnimate();

  //Zone 1 Date
  if (P.getZoneStatus(1)) {
    strftime(bufD, sizeof(bufD), "%Y %m/%d %a", &ts);
    parr[0] = strtok(bufD, " "); // year
    parr[1] = strtok(NULL, " "); // month/day
    parr[2] = strtok(NULL, " "); // day of the week

    switch (dsw) {
      case 0: // month/day down
        P.displayZoneText(1, parr[1], PA_CENTER, SPEED_TIME, 0, PA_NO_EFFECT, PA_SCROLL_DOWN);
        dsw++;
        break;
      case 1: // year down
        P.displayZoneText(1, parr[0], PA_CENTER, SPEED_TIME, 0, PA_SCROLL_DOWN, PA_NO_EFFECT);
        P.setPause(1, PAUSE_TIME_S);
        dsw++;
        break;
      case 2: // year up
        P.displayZoneText(1, parr[0], PA_CENTER, SPEED_TIME, 0, PA_NO_EFFECT, PA_SCROLL_UP);
        dsw++;
        break;
      case 3: // month/day up
        P.displayZoneText(1, parr[1], PA_CENTER, SPEED_TIME, 0, PA_SCROLL_UP, PA_NO_EFFECT);
        P.setPause(1, PAUSE_TIME_L);
        dsw++;
        break;
      case 4: // month/day up
        P.displayZoneText(1, parr[1], PA_CENTER, SPEED_TIME, 0, PA_NO_EFFECT, PA_SCROLL_UP);
        dsw++;
        break;
      case 5: // day of the week up
        P.displayZoneText(1, parr[2], PA_CENTER, SPEED_TIME, 0, PA_SCROLL_UP, PA_NO_EFFECT);
        P.setPause(1, PAUSE_TIME_S);
        dsw++;
        break;
      case 6: // day of the week down
        P.displayZoneText(1, parr[2], PA_CENTER, SPEED_TIME, 0, PA_NO_EFFECT, PA_SCROLL_DOWN);
        dsw++;
        break;
      case 7: // month/day  up
        P.displayZoneText(1, parr[1], PA_CENTER, SPEED_TIME, 0, PA_SCROLL_DOWN, PA_NO_EFFECT);
        P.setPause(1, PAUSE_TIME_L);
        dsw = 0;
        break;
      default:
        break;
    }
    P.displayReset(1);
  }

  //Zone 0 Time
  if (P.getZoneStatus(0)) {
    strftime(bufT, sizeof(bufT), "%T", &ts); // %T: hh:mm:ss
    P.displayZoneText(0, bufT, PA_CENTER, SPEED_TIME, 0, PA_PRINT, PA_NO_EFFECT);
    P.displayReset(0);
  }

  delay(50); //Don't remove this delay, and don't make it too small
}

void loop(){} の中はひたすらループ処理が行われるが、P.getZoneStatus(#Zone)が1でなければそのゾーンのアニメーションの制御はループに邪魔されず(実際は多少邪魔される)に進行する。逆に言えばアニメーションの動作が完了してP.getZoneStatus(#Zone)が1になるまでアニメーションの処理に邪魔されず(実際は多少邪魔される)にループが進行するのでアタマがこんがらかるような余計なことを考えなくて済むのでとてもラク。
void loop(){}の最後にディレイを入れているが、値(ミリ秒)が小さすぎるとループ処理が速く行われすぎて動作異常になると思われる。ただし大きすぎても今度は動作が遅くなったり処理開始のタイミングが狂うかもしれないので適度な大きさの数値にする。50msより少し小さい程度?

今回は狭い表示範囲に文字を詰め込むため専用にフォントを作成した。時計の表示は時・分・秒が変わる度に表示位置がガタガタ変わるのはイヤなので数字は4ドットの固定幅(英字も)で数字は4x8の大きめにし、記号は幅を最小限に1〜3ドット(列)の可変幅というハイブリッド型プロポーショナルフォントにした。
ゼロはアルファベットのOと区別がしやすいよう斜線付きタイプにしたが敢えて超少数派の逆斜線にしてみた。(Øと斜線の向きが逆)
一応、フォントのリンクを貼っておくのでダウンロードできます。

GF4x8p.h

まぁ素敵なフォントを作ってやろうという意欲はあるんだけど、残念ながらデザインセンスと実力がからっきし無いので字の見た目が残念かもしれない。もっと良いのが欲しいという人は自分で作っていただければ。 で、ダウンロードしたフォントのファイルをスケッチと同じディレクトリに置く。上の例ではファイル名が GF4x8p.h でフォント名が GF4x8p 。つまり、スケッチの最初の方で#include "GF4x8p.h" と指定してやればそのフォントがインクルードされ、P.setFont(0,GF4x8p);のように指定することで0番ゾーンでそのフォントが使われる(P.setFont(1,GF4x8p);なら1番ゾーン)。なのでゾーン単位でフォントを使い分けることができる。

予定通りに日付と時刻がきれいに表示されるようになった
日付、時刻ともにそれぞれのゾーンの中で中央寄せ。日付表示は8x8 LEDディスプレイ3モジュール分のゾーンなので5文字表示の「月/日」ではカツカツだが、時刻ゾーンは左右の端に余裕がある感じ。

関連記事:

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です