ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた

3年前に作成したESP32マイコンボードとMAX7219 LEDディスプレイ4連x2を使ったNTP時計は、昨年の夏にLEDパネルの一部が不調となり、時刻が正しく表示できなくなってしまいました。このNTP時計で使用していたのは、8x8ドットのLEDパネルを4枚連結したものを2セット組み合わせた、64x8ドットの表示が可能なLEDディスプレイでした。しかし、今回は以前のパネルではなく、LEDドット数が4倍の64x32ドットのLEDマトリクスパネルを新たに購入しました。このタイプのパネルは「HUB75」というキーワードで検索すると見つけることができます。

新しい64x32ドットパネルは、以前のものに比べて消費電力が大きく、ESP32などのシングルボードコンピュータの5V電源出力ピンだけでは必要な電力を供給することができません。そのため、LEDパネルを動作させるには別途5V/8A程度の電流を供給できるACアダプタやスイッチング電源が必須となります。LEDの明るさや表示のスムーズさを保つためには十分な電力供給が欠かせないため、電源選びには特に注意が必要です。

実は、新しいパネルとACアダプタは昨年(2023年)の夏、前回のNTP時計が壊れた直後にすぐ購入していました。しかし、ちょうどその頃、体調を崩して入院することになり、その後も長期間静養していたため、修理に手をつけることができませんでした。そのまま年が明け、2024年になってもなかなかやる気が湧かず、さらに他の用事に追われてしまった結果、このパネルを使ったNTPクロックのことはすっかり忘れ去られていました。そのため、新しいパネルや電源を用意してから、実に1年半近く放置されることになってしまいました。

それが、ようやくやる気スイッチが入り、長らく手を付けられずにいた新NTPクロック作成をスタートすることができました。今回は新しい64x32ドットパネルいっぱいにNTP時計を表示する部分についてを紹介します。パネルの額縁や背面隠しについても綺麗に作成する予定を立てていますが、もしかするとブログでは取り上げないかもしれません。今回の記事の写真では、パネルを立てて固定するために発泡ボードを切り出して簡易的な衝立を作成し、それを仮付けした状態で撮影しました。この方法はあくまで作業の便宜上行ったものであり、完成版ではよりしっかりとした支持構造を採用する予定です。

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 1
LEDパネルの電源には、一般的には小型のスイッチング電源が利用されることが多いかもしれません。しかし、電源への埃の侵入を防ぎたかったため、密閉ケースに収められたACアダプタを採用しました。写真のACアダプタの出力は5V8Aで、40W程度の容量があります。パネルを最大輝度で全ドット点灯させるのでない限り、20〜30Wでも運用可能ですが、供給できる最大電力には余裕を持たせることが重要です。電源に余裕がないと、動作が不安定になったり表示できなくなる可能性があります。

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 2
ACアダプタのプラグに合うソケットを用意し、パネル付属の電源ケーブルをはんだ付けしました。パネル付属の電源ケーブルはプラス・マイナスが2本ずつで、さらに増設パネルに電力を供給できるようにそれがダブル構成になっていましたが、今回は増設パネルを使用しないため増長分は取り外しています。その後、接続部を保護するために熱収縮チューブで覆っています。これによりうっかりによるショートを発生させないようになっています。上の写真は電源ソケットにACアダプタを接続した様子を示しています。

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 3
パネルの背面には、中央に電源ソケットが配置され、上の写真では右側のHUB75ソケットは隠れていますが、左右にHUB75ソケットがあり、左のソケットが入力用、右のソケットが出力用です。左側のソケットを使用してESP32ボードなどからパネルへの信号を入力します。

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 4
今回使用するHUB75 64×32ドットのLEDパネルをESP32で制御するには、tidbyt/ESP32-HUB75-MatrixPanel-I2S-DMAライブラリを利用すると非常に簡単です。
ESP32とHUB75のドットマトリクスパネルの接続のデフォルトはこの画像のとおりです。この接続であればスケッチ(ソース)内で接続ピンを宣言する必要はありません。理由があってデフォルトと異なるGPIOピンを使用して接続するならそのスケッチ内で変更する接続ピンを指定する必要があります。
64x32ドットの1/16スキャンのパネルを使用する場合は、パネルとESP32を接続するために14本のジャンパケーブルが必要です。64x64ドットの1/32スキャンのパネルを使用する場合はHUB75のEピンとESP32の空きピンを接続するするので15本のジャンパケーブルが必要です。今回は64x32ドットパネルを使用するのでEピンは接続していません。

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 5
今回採用した64×32ドットのパネルは2.5mmピッチ(P2.5)で、サイズは160×80mmです。以前使用していた8×8ドットの4連パネルは3.5mmピッチ(P3.5)だったため、4連(130mm)×2パネルで260mmです。8ドット×4連×2パネルで今回のパネルと同じ横64ドットなので今回のパネルはドット密度が約4割高まっています。なお、HUB75のパネルはピッチの異なるバリアントが存在するので好みのピッチのものを購入してください。
NTPクロック用のスケッチは、jenizar/esp32-p5-rgb-32x64-ntp-clockをベースにしました。1から作成する必要がなくなるため大幅に作成時間を短縮できました。ただし、デフォルトの文字のデザインに満足できなかったため、フォントを変更しました。日付や曜日、秒数の表示にはビルトインの6×8ドットフォントを使用し、「時:分」部分には自作の縦長(Tall)フォントを採用しました。ただし、秒数の表示が小さすぎるため、ビルトインフォントより一回り大きなフォントを作成することにしました。(次)

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 6
新たに作成した8×12ドットのフォントを秒表示に適用しました。この変更により、秒表示がより大きく、パネル全体を効果的に活用した時計デザインとなりました。

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 7
「秒」表示用に数字だけ(空白とコロンを含む)の8x12ドットのSimpleNumber8x12フォントと、「時:分」表示用の11x20ドットのTallNumber11x20フォントを自作しました。以下フォントファイルは、ユーザーの「Adruino」ディレクトリ→「スケッチ名」ディレクトリ下に「Fonts」ディレクトリを作成し、そこにSimpleNumber8x12.hとTallNumber11x20.hを置きます。
なお、文字を並べた際に隣接する部分が繋がって表示されることを防ぐため、各文字の一番右は1列空けています。つまり、8x12フォントの文字部分の領域は7x12ドット、11x20フォントの文字部分の領域は10x20ドットになっています。

SimpleNumber8x12.h
 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
#pragma once
#include <Adafruit_GFX.h>

const uint8_t SimpleNumber8x12Bitmaps[] PROGMEM = {
  0xFF, 0xEA, 0x03, 0x7C, 0xFE, 0xC6, 0xCE, 0xCE, 0xD6, 0xD6, 0xE6, 0xE6,
  0xC6, 0xFE, 0x7C, 0x30, 0x30, 0x70, 0x70, 0x30, 0x30, 0x30, 0x30, 0x30,
  0x30, 0x78, 0x78, 0x7C, 0xFE, 0xC6, 0x06, 0x06, 0x7E, 0xFC, 0xC0, 0xC0,
  0xC0, 0xFE, 0xFE, 0x7C, 0xFE, 0xC6, 0x06, 0x06, 0x1C, 0x1C, 0x06, 0xC6,
  0xC6, 0xFE, 0x7C, 0x04, 0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xCC, 0xFE, 0xFE,
  0x0C, 0x0C, 0x0C, 0xFE, 0xFE, 0xC0, 0xC0, 0xC0, 0xFC, 0xFE, 0x06, 0x06,
  0xC6, 0xFE, 0x7C, 0x7C, 0xFE, 0xC6, 0xC0, 0xC0, 0xFC, 0xFE, 0xC6, 0xC6,
  0xC6, 0xFE, 0x7C, 0xFE, 0xFE, 0xC6, 0x06, 0x0C, 0x0C, 0x0C, 0x18, 0x18,
  0x18, 0x18, 0x18, 0x7C, 0xFE, 0xC6, 0xC6, 0xC6, 0x7C, 0x7C, 0xC6, 0xC6,
  0xC6, 0xFE, 0x7C, 0x7C, 0xFE, 0xC6, 0xC6, 0xC6, 0xFE, 0x7E, 0x06, 0x06,
  0xC6, 0xFE, 0x7C, 0x3C, 0x3C
};

const GFXglyph SimpleNumber8x12Glyphs[] PROGMEM = {
  {     0,   0,   0,   5,    0,    1 },   // 0x20 ' '
  {     0,   0,   0,   0,    0,    0 },   // 0x21 '!'
  {     0,   0,   0,   0,    0,    0 },   // 0x22 '"'
  {     0,   0,   0,   0,    0,    0 },   // 0x23 '#'
  {     0,   0,   0,   0,    0,    0 },   // 0x24 '$'
  {     0,   0,   0,   0,    0,    0 },   // 0x25 '%'
  {     0,   0,   0,   0,    0,    0 },   // 0x26 '&'
  {     0,   0,   0,   0,    0,    0 },   // 0x27 '''
  {     0,   0,   0,   0,    0,    0 },   // 0x28 '('
  {     0,   0,   0,   0,    0,    0 },   // 0x29 ')'
  {     0,   0,   0,   0,    0,    0 },   // 0x2A '*'
  {     0,   0,   0,   0,    0,    0 },   // 0x2B '+'
  {     0,   0,   0,   0,    0,    0 },   // 0x2C ','
  {     0,   0,   0,   0,    0,    0 },   // 0x2D '-'
  {     0,   0,   0,   0,    0,    0 },   // 0x2E '.'
  {     0,   0,   0,   0,    0,    0 },   // 0x2F '/'
  {     3,   8,  12,   8,    0,  -11 },   // 0x30 '0'
  {    15,   8,  12,   8,    0,  -11 },   // 0x31 '1'
  {    27,   8,  12,   8,    0,  -11 },   // 0x32 '2'
  {    39,   8,  12,   8,    0,  -11 },   // 0x33 '3'
  {    51,   8,  12,   8,    0,  -11 },   // 0x34 '4'
  {    63,   8,  12,   8,    0,  -11 },   // 0x35 '5'
  {    75,   8,  12,   8,    0,  -11 },   // 0x36 '6'
  {    87,   8,  12,   8,    0,  -11 },   // 0x37 '7'
  {    99,   8,  12,   8,    0,  -11 },   // 0x38 '8'
  {   111,   8,  12,   8,    0,  -11 },   // 0x39 '9'
  {   123,   2,   8,   5,    1,  -10 }    // 0x3A ':'
};

const GFXfont SimpleNumber8x12 PROGMEM = {(uint8_t *)SimpleNumber8x12Bitmaps,
  (GFXglyph *)SimpleNumber8x12Glyphs, 0x20, 0x3A,      12};
TallNumber11x20.h
 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
#pragma once
#include <Adafruit_GFX.h>

const uint8_t TallNumber11x20Bitmaps[] PROGMEM = {
  0xFF, 0xEA, 0x03, 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x1D, 0x83, 0xB0,
  0xB6, 0x16, 0xC4, 0xD8, 0x9B, 0x23, 0x64, 0x6D, 0x0D, 0xA1, 0xB8, 0x37,
  0x06, 0xC0, 0xDC, 0x39, 0xFE, 0x1F, 0x80, 0x0C, 0x01, 0x80, 0xF0, 0x1E,
  0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00,
  0xC0, 0x18, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0xFC, 0x1F, 0x80, 0x3F,
  0x0F, 0xF3, 0x87, 0x60, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x01, 0xC7,
  0xF1, 0xFC, 0x70, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03,
  0xFF, 0x7F, 0xE0, 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x60, 0x0C, 0x01, 0x80,
  0x30, 0x06, 0x01, 0x80, 0xE0, 0x1C, 0x00, 0xC0, 0x0C, 0x01, 0x80, 0x36,
  0x06, 0xC0, 0xDC, 0x39, 0xFE, 0x1F, 0x80, 0x03, 0x00, 0x60, 0x1C, 0x03,
  0x80, 0xF0, 0x1E, 0x06, 0xC0, 0xD8, 0x33, 0x06, 0x61, 0x8C, 0x31, 0x8C,
  0x31, 0x86, 0x3F, 0xF7, 0xFE, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0xFF,
  0xDF, 0xFB, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xFF, 0x1F,
  0xF0, 0x07, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0xC0, 0xDC, 0x39,
  0xFE, 0x1F, 0x80, 0x3F, 0x0F, 0xF3, 0x07, 0x60, 0x6C, 0x0D, 0x80, 0x30,
  0x06, 0x00, 0xDF, 0x1F, 0xF3, 0x87, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x36,
  0x06, 0xC0, 0xDC, 0x39, 0xFE, 0x1F, 0x80, 0xFF, 0xDF, 0xFB, 0x03, 0x60,
  0x60, 0x18, 0x03, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x18, 0x03, 0x00,
  0x60, 0x0C, 0x03, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0x3F,
  0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0D, 0x81, 0xB0, 0x36, 0x06, 0x61, 0x87,
  0xE0, 0xFC, 0x30, 0xCC, 0x0D, 0x81, 0xB0, 0x36, 0x06, 0xC0, 0xDC, 0x39,
  0xFE, 0x1F, 0x80, 0x3F, 0x0F, 0xF3, 0x87, 0x60, 0x6C, 0x0D, 0x81, 0xB0,
  0x36, 0x06, 0xE1, 0xCF, 0xF8, 0xFF, 0x00, 0x60, 0x0C, 0x01, 0x80, 0x30,
  0x06, 0xC0, 0xDC, 0x39, 0xFE, 0x1F, 0x80, 0x00, 0x01, 0xB0, 0x00, 0x01,
  0xB0, 0x00, 0x00
};

const GFXglyph TallNumber11x20Glyphs[] PROGMEM = {
  {     0,   0,   0,   5,    0,    1 },   // 0x20 ' '
  {     0,   0,   0,   0,    0,    0 },   // 0x21 '!'
  {     0,   0,   0,   0,    0,    0 },   // 0x22 '"'
  {     0,   0,   0,   0,    0,    0 },   // 0x23 '#'
  {     0,   0,   0,   0,    0,    0 },   // 0x24 '$'
  {     0,   0,   0,   0,    0,    0 },   // 0x25 '%'
  {     0,   0,   0,   0,    0,    0 },   // 0x26 '&'
  {     0,   0,   0,   0,    0,    0 },   // 0x27 '''
  {     0,   0,   0,   0,    0,    0 },   // 0x28 '('
  {     0,   0,   0,   0,    0,    0 },   // 0x29 ')'
  {     0,   0,   0,   0,    0,    0 },   // 0x2A '*'
  {     0,   0,   0,   0,    0,    0 },   // 0x2B '+'
  {     0,   0,   0,   0,    0,    0 },   // 0x2C ','
  {     0,   0,   0,   0,    0,    0 },   // 0x2D '-'
  {     0,   0,   0,   0,    0,    0 },   // 0x2E '.'
  {     0,   0,   0,   0,    0,    0 },   // 0x2F '/'
  {     3,  11,  20,  11,    0,  -19 },   // 0x30 '0'
  {    31,  11,  20,  11,    0,  -19 },   // 0x31 '1'
  {    59,  11,  20,  11,    0,  -19 },   // 0x32 '2'
  {    87,  11,  20,  11,    0,  -19 },   // 0x33 '3'
  {   115,  11,  20,  11,    0,  -19 },   // 0x34 '4'
  {   143,  11,  20,  11,    0,  -19 },   // 0x35 '5'
  {   171,  11,  20,  11,    0,  -19 },   // 0x36 '6'
  {   199,  11,  20,  11,    0,  -19 },   // 0x37 '7'
  {   227,  11,  20,  11,    0,  -19 },   // 0x38 '8'
  {   255,  11,  20,  11,    0,  -19 },   // 0x39 '9'
  {   283,   3,  20,   3,    0,  -19 }    // 0x3A ':'
};

const GFXfont TallNumber11x20 PROGMEM = {(uint8_t *)TallNumber11x20Bitmaps,
  (GFXglyph *)TallNumber11x20Glyphs, 0x20, 0x3A,          12};

ESP32とHUB75のドットマトリクスパネルでNTP時計を作ってみた 8
今回使用したライブラリでは、文字を表示する前に描画領域をリセットしません。そのため、すでに表示されている内容の上に新しい文字が重なって表示されます。たとえば、「0」が表示されている場所にそのまま「1」を表示すると、重なった表示になります。この問題を解決するため、文字を表示する前に領域を黒などで塗りつぶすなどの処理が必要です。
先に挙げたjenizar/esp32-p5-rgb-32x64-ntp-clockの例では以下のように文字を表示する前に領域を塗りつぶす指定になっています。

uint16_t myBLACK = dma_display->color333(0,0,0); //先に宣言しておきます
dma_display->fillRect(x, y, W, H, myBLACK);  //x,y,W,Hは整数で指定します
xとyで塗りつぶす領域の左上の隅の座標を指定し、そこから幅(W)と高さ(H)の範囲を塗りつぶします。

文字は以下のように表示します。
#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h> //スケッチの最初の方でインクルードしておきます
#include <Fonts/TallNumber11x20.h>           //好きなフォントをインクルードします

uint16_t myWHITE = dma_display->color333(7,7,7);   //先に宣言しておきます

dma_display->setCursor(x, y);             //文字位置の指定でx,yは整数で指定します
dma_display->setFont(&TallNumber11x20);     //フォントを指定します
dma_display->setTextSize(n);                //nで整数を指定します n倍の大きさで表示します
dma_display->setTextColor(myWHITE);
dma_display->fillRect(x, y, W, H, myBLACK); //x,y,W,Hは整数で指定します
dma_display->printf("012345");               //012345を表示
dma_display->setFont();                     //フォントの初期化

setCursor(x, y)で文字の左上の位置を指定すれば良さそうに思われる方が多いと思われますが、setCursor(x,y)のyはフォントの上辺ではなく画面上辺から文字(フォント)のベースラインまでの高さのようです。ベースラインの高さを知っていないと想定より上下どちらかにズレて表示され、微調整が必要になります。今回の自作フォントは文字単位で上下にずれて表示されるのを防ぐためベースラインを文字の下辺に統一しています。ベースラインは日本語の文字ではあまり意識しませんがアルファベットでは重要な概念です。

関連記事:

記事へのコメント

いただいたコメントは管理人が確認した後に記事の 下部(ここ)に公開されます。
コメントスパム対策: 2022年4月以降、コメント内にリンクURLを含めると自動破棄されます。(記録されません)