ESP32-S3でHUB75マトリクスLEDパネルを制御

ハードウェア

  • CPU: ESP32-S3-WROOM-1 N16R8(44ピンボード)
  • パネル: P4-256X128-2121-16S / 64×32 / 1/16スキャン
  • ドライバIC: DP5125F(ICN2038S互換設定で動作)
  • センサー: SHT31(温湿度)+ BMP280(気圧)I2Cバス
  • CDS:GL5516

ライブラリ

  • Arduino.h
  • Wire.h
  • WiFi.h
  • time.h
  • esp_sntp.h
  • ESP32-HUB75-MatrixPanel-I2S-DMA.h
  • Adafruit_SHT31.h
  • Adafruit_BMP280.h

概要

今回はClaudeでプログラム作成を試みてみた。

ここで使ったHUB75マトリクスLEDパネルはAliexpressで製品価格751円(現在は872円)送料943円という安物のパネルであったため標準的なパネルとは使用が違い苦労した。

Claudeと試行錯誤をしたがなかなか正常表示することができなかったので、Geminiに聞いてみたところ

Geminiの説明では、

搭載されている DP5125F(および DP32019A)は、一般的な「シフトレジスタ型」ではなく、「PWMラッチ型(高リフレッシュレート対応)」のドライバICです。
このチップは、一般的なライブラリが想定している信号の送り方(データを流してラッチするだけ)では正しく動作せず、専用の初期化コマンド(レジスタ設定)を必要とします。
また、パネルの印字にある「16s」は 1/16スキャン を意味していますが、このチップを積んだ1/16スキャンパネルは、通常のライブラリ設定では「砂嵐」や「ぐちゃぐちゃな表示」になります。

ということでGeminiは次の提案をしてきた。

// HUB75 パネル初期化
HUB75_I2S_CFG mxconfig(256, 128, 1); // 幅256, 高さ128, 1枚

// ...GPIOの設定はそのまま...

// ドライバ設定を「ICN2038S」または「FM6126A」に変更
// DP5125Fは、ICN2038SやFM6126Aと互換性がある初期化シーケンスを必要とする場合があります
mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // ← これを試してください

// クロックフェーズとスピードの調整
mxconfig.clkphase = false; // DP5125Fはfalseの方が安定することが多いです
mxconfig.i2sspeed = HUB75_I2S_CFG::HZ_10M; // 少し落として安定させます

// ラッチブランキング(ゴースト対策)
mxconfig.latch_blanking = 4;

ここにある”mxconfig.driver”の設定が決め手だった。

以上で表示は正常に行われるようになったが、ゴーストが出ていたりでかなり功労した。

HUB75 LEDマトリクスパネル

今回使ったHUB75 LEDマトリクスパネルはAliexpressで購入した廉価版だったため標準のHUB75のインターフェースとは異なっていたし、さらに青と赤の色についても逆になっている。標準の物に対してR2とG2の位置が異なる印刷がされている。

インターフェースコネクタのピン配置

回路図

ソースコード

/* *******************************************************************************
 * このプログラムは Claude で作ってみた。
 * 但しパネルが標準的なものと異なっていたため、ドライバ IC の設定を試行錯誤したが、
 * 正常動作が得られなかった。そのため Gemini にも提案してもらった結果が下記。
 * mxconfig.driver     = HUB75_I2S_CFG::ICN2038S; // ← これを試してください
 * この設定で正常動作できるようになった。
 * 
 * Claude の元々の提案から書きを修正。
 * 1.アイコンの色とデザイン変更
 * 2.温度表示職を「橙」→「黃色」に変更。
 * 3.NTP同期エラー時の時刻表示を「黄色」→「灰色」に変更。
 * 4.温度表示の「℃」を「C」→「"C」に変更。
 * 5.パネルの輝度を周囲光の明るさで調整する制御を追加。
 * *******************************************************************************
 *
 * ESP32-S3 + HUB75 64x32 LEDマトリクスパネル 表示プログラム
 * SMD2121 LEDドライバ対応
 * 内部RTC使用 + NTP同期(1日1回)
 *
 * 使用ライブラリ(Arduino IDEのライブラリマネージャからインストール):
 *   - ESP32 HUB75 LED MATRIX PANEL DMA Display  by mrfaptastic
 *   - Adafruit SHT31 Library                    by Adafruit
 *   - Adafruit BMP280 Library                   by Adafruit
 *   - Adafruit GFX Library                      by Adafruit(依存)
 *
 * 配線(デフォルトのHUB75ピン割り当て):
 *   HUB75 R1  → GPIO 8  橙    HUB75 G1  → GPIO 9  黃
 *   HUB75 B1  → GPIO 10 黃    HUB75 R2  → GPIO 14 緑
 *   HUB75 G2  → GPIO 13 緑    HUB75 B2  → GPIO 12 青
 *   HUB75 A   → GPIO 11 青    HUB75 B   → GPIO 39 紫
 *   HUB75 C   → GPIO 5  紫    HUB75 D   → GPIO 17 灰
 *   HUB75 CLK → GPIO 16 灰    HUB75 LAT → GPIO 4  白
 *   HUB75 OE  → GPIO 15 白    HUB75 GND → GND     黒
 *
 *   SHT31  SDA → GPIO 21    SHT31  SCL → GPIO 20
 *   BMP280 SDA → GPIO 21    BMP280 SCL → GPIO 20(同一I2Cバス)
 */

#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <time.h>
#include <esp_sntp.h>

#include <ESP32-HUB75-MatrixPanel-I2S-DMA.h>
#include <Adafruit_SHT31.h>
#include <Adafruit_BMP280.h>

#define DEBUG 2

// ============================================================
//  ★ ユーザー設定エリア ★
// ============================================================

// --- WiFi ---
const char* WIFI_SSID     = "SSID";       // ← SSIDを入力
const char* WIFI_PASSWORD = "PASSWORD";           // ← パスワードを入力

// --- NTP ---
const char* NTP_SERVER1   = "ntp.nict.jp";     // 国立情報通信研究所(日本)
const char* NTP_SERVER2   = "pool.ntp.org";    // フォールバック
const char* TZ_STRING     = "JST-9";           // 日本標準時
const int   NTP_SYNC_HOUR = 3;                 // NTP再同期を行う時刻(0〜23)
const int   NTP_TIMEOUT_S = 15;                // NTP待機タイムアウト(秒)

// --- HUB75 パネル ---
#define PANEL_WIDTH   64
#define PANEL_HEIGHT  32
#define PANEL_CHAIN   1

// ESP32-S3 向けピン定義(ボードに合わせて変更)
#define HUB75_R1   10
#define HUB75_G1   8
#define HUB75_B1   9
#define HUB75_R2   13
#define HUB75_G2   14
#define HUB75_B2   12
#define HUB75_A    11
#define HUB75_B    39
#define HUB75_C     5
#define HUB75_D    17
#define HUB75_CLK  16
#define HUB75_LAT   4
#define HUB75_OE   15

// --- I2C ピン定義を追加 ---
#define SDA        21
#define SCL        20   // ←22

// --- センサー ---
#define SHT31_ADDR  0x44
#define BMP280_ADDR 0x76

// 明るさを得るためのCDSのピン
#define CDS_PIN     1

// --- 更新間隔 ---
#define SENSOR_UPDATE_MS  10000UL   // センサー読み取り間隔(10秒)
#define CLOCK_UPDATE_MS   500UL     // 時計更新間隔(0.5秒)
#define BRIGHTNESS_UPDATE_MS  3000L  // 輝度設定間隔(3秒)

// ============================================================
//  グローバル変数
// ============================================================
MatrixPanel_I2S_DMA *matrix = nullptr;
Adafruit_SHT31       sht31;
Adafruit_BMP280      bmp280;

struct DisplayData {
  int   hour      = 0;
  int   minute    = 0;
  int   second    = 0;
  float temp      = 0.0f;
  float hum       = 0.0f;
  float hpa       = 0.0f;
  bool  sensorOK  = false;
  bool  ntpSynced = false;
};
DisplayData gData;

volatile bool ntpSyncDone  = false;
unsigned long lastSensorUpdate = 0;
unsigned long lastClockUpdate  = 0;
unsigned long lastBrightnessUpdate = 0;

int lastNtpSyncDay = -1;   // 最後にNTP同期した曜日(0=日〜6=土)

// ============================================================
//  グローバル変数(前回表示内容の記憶)
// ============================================================
char prevTime[9]  = "";
char prevTemp[10] = "";
char prevHum[8]   = "";
char prevPres[12] = "";

// ============================================================
//  カラー定義
// ============================================================
uint16_t COL_CLOCK, COL_TEMP, COL_HUM, COL_PRES;
uint16_t COL_WHITE, COL_BLACK, COL_GRAY, COL_YELLOW;
uint16_t COL_RED, COL_BLUE;

void initColors() {
  //COL_CLOCK  = matrix->color565(  0, 220, 220); // シアン
  COL_CLOCK  = matrix->color565(  210, 220, 220); // オフホワイト
  //COL_TEMP   = matrix->color565(255, 140,   0); // 橙
  COL_TEMP   = matrix->color565(255, 255,   0); // 黃
  COL_HUM    = matrix->color565(100, 180, 255); // 水色
  COL_PRES   = matrix->color565(100, 220, 100); // 緑
  COL_WHITE  = matrix->color565(255, 255, 255);
  COL_BLACK  = matrix->color565(  0,   0,   0);
  COL_GRAY   = matrix->color565( 100,  100,  100);
  COL_YELLOW = matrix->color565(255, 220,   0);
  COL_RED   = matrix->color565(255, 0,   0); // 赤
  COL_BLUE   = matrix->color565(0, 0,   255); // 青
}

// ============================================================
//  アイコン描画
// ============================================================
void drawClockIcon(int x, int y, uint16_t col) {
  matrix->drawCircle(x + 3, y + 3, 3, col);
  matrix->drawLine(x + 3, y + 3, x + 3, y + 1, col);
  matrix->drawLine(x + 3, y + 3, x + 5, y + 3, col);
}
void drawTempIcon(int x, int y, uint16_t col) {
  matrix->drawLine(x + 3, y,     x + 3, y + 1, COL_WHITE);
  matrix->drawLine(x + 3, y + 2,     x + 3, y + 4, COL_RED);
  matrix->fillCircle(x + 3, y + 5, 1, COL_RED); //1ドット分上へ小さく
}
void drawHumIcon(int x, int y, uint16_t col) {
  matrix->drawPixel(x + 3, y+1,     col);
  matrix->drawLine(x + 2, y+1 + 1, x + 4, y+1 + 1, col);
  matrix->drawLine(x + 1, y+1 + 2, x + 5, y+1 + 2, col);
  matrix->drawLine(x + 1, y+1 + 3, x + 5, y+1 + 3, col);
  matrix->drawLine(x + 2, y+1 + 4, x + 4, y+1 + 4, col);
}
void drawPresIcon(int x, int y, uint16_t col) {
  matrix->drawCircle(x + 3, y + 3, 3, col);
  matrix->drawLine(x + 3, y + 3, x + 5, y + 1, col);
  matrix->drawPixel(x + 3, y + 3, col);
  matrix->drawLine(x + 2, y + 5, x + 4, y + 5, col);
}

// ============================================================
//  NTP同期中メッセージ表示
// ============================================================
void showNtpSyncMessage() {
  matrix->fillScreenRGB888(0, 0, 0);
  matrix->setTextColor(COL_YELLOW);
  matrix->setCursor(2, 4);  matrix->print("NTP");
  //matrix->setCursor(2, 2);  matrix->print("NTP");   //  表示位置を上げた
  matrix->setCursor(2, 14); matrix->print("Sync..");
  matrix->setCursor(2, 24); matrix->print("Wait");
}

// ============================================================
//  NTP同期完了コールバック
// ============================================================
void ntpSyncCallback(struct timeval *tv) {
  ntpSyncDone = true;
  Serial.println("[NTP] 同期完了コールバック受信");
}

// ============================================================
//  WiFi接続(タイムアウト付き)
// ============================================================
bool connectWiFi() {
  Serial.printf("[WiFi] 接続中: %s\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    if (millis() - start > 20000UL) {
      Serial.println("[WiFi] タイムアウト");
      WiFi.disconnect(true);
      WiFi.mode(WIFI_OFF);
      return false;
    }
    delay(300);
    Serial.print(".");
  }
  Serial.printf("\n[WiFi] 接続成功 IP: %s\n",
                WiFi.localIP().toString().c_str());
  return true;
}

// ============================================================
//  NTP同期(WiFi接続 → 同期 → WiFi切断)
//  戻り値: true = 成功, false = 失敗
// ============================================================
bool syncNTP() {
  showNtpSyncMessage();

  if (!connectWiFi()) {
    Serial.println("[NTP] WiFi接続失敗のためスキップ");
    return false;
  }

  // タイムゾーンとNTPサーバーを設定
  configTzTime(TZ_STRING, NTP_SERVER1, NTP_SERVER2);

  ntpSyncDone = false;
  sntp_set_time_sync_notification_cb(ntpSyncCallback);

  Serial.println("[NTP] 同期待機中...");
  unsigned long start = millis();
  while (!ntpSyncDone) {
    if (millis() - start > (unsigned long)NTP_TIMEOUT_S * 1000UL) {
      Serial.println("[NTP] タイムアウト");
      WiFi.disconnect(true);
      WiFi.mode(WIFI_OFF);
      return false;
    }
    delay(100);
  }

  // 同期後はWiFiを切断(消費電力削減)
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
  Serial.println("[WiFi] 切断(省電力モード)");

  // ログ出力
  time_t now;
  struct tm ti;
  time(&now);
  localtime_r(&now, &ti);
  Serial.printf("[NTP] 同期完了: %04d-%02d-%02d %02d:%02d:%02d JST\n",
    ti.tm_year + 1900, ti.tm_mon + 1, ti.tm_mday,
    ti.tm_hour, ti.tm_min, ti.tm_sec);

  gData.ntpSynced = true;

  // ★ NTP同期メッセージを消去(残像防止)
  // matrix->fillScreenRGB888(0, 0, 0); // →setup()に移動

  // 前回表示キャッシュもリセット(強制再描画させる)
  prevTime[0] = '\0';
  prevTemp[0] = '\0';
  prevHum[0]  = '\0';
  prevPres[0] = '\0';

  return true;
}

// ============================================================
//  時計更新(ESP32内部RTCをPOSIX time APIで読み出し)
// ============================================================
void updateClock() {
  time_t now;
  struct tm ti;
  time(&now);
  localtime_r(&now, &ti);

  gData.hour   = ti.tm_hour;
  gData.minute = ti.tm_min;
  gData.second = ti.tm_sec;

  if (DEBUG == 1) {
    Serial.printf("%02d:%02d:%02d\n", ti.tm_hour, ti.tm_min, ti.tm_sec);
  }
  // 毎日 NTP_SYNC_HOUR 時 00分 00〜29秒 に1回だけ再同期
  // (曜日が変わったタイミングで同期済みフラグをリセット)
  if (ti.tm_hour == NTP_SYNC_HOUR &&
      ti.tm_min  == 0             &&
      ti.tm_sec  <  30            &&
      ti.tm_wday != lastNtpSyncDay) {
    Serial.println("[NTP] 定期同期を開始します");
    lastNtpSyncDay = ti.tm_wday;
    syncNTP();
  }
}

// ============================================================
//  センサー読み取り
// ============================================================
void readSensors() {
  float t = sht31.readTemperature();
  float h = sht31.readHumidity();
  float p = bmp280.readPressure() / 100.0f;  // Pa → hPa

  if (!isnan(t) && !isnan(h) && p > 800.0f) {
    gData.temp     = t;
    gData.hum      = h;
    gData.hpa      = p;
    gData.sensorOK = true;
    if (DEBUG) {
      Serial.printf("Temp: %2.2f *C\n", t);
      Serial.printf("Humi: %2.2f %%\n", h);
      Serial.printf("Pres: %4.0f hPa\n", p);
    }
  } else {
    gData.sensorOK = false;
    Serial.println("[WARN] センサー読み取り失敗");
  }
}

// ============================================================
//  画面更新
// ============================================================

// *** 輝度設定のためのCDS AD値読み取り回数 ***
#define AVG_N   10

void brightness_settings() {
  int v, n;

  v = 0;
  for(n = 0; n < AVG_N; n++){   // 平均値を取るため
    v += analogRead(1);
    delay(1);
  }
  v = (v / AVG_N) / 32 + 5;
  matrix->setBrightness8(v);  // 
  Serial.printf("CDS = %d\n", v);
}

// ============================================================
//  テキスト領域消去(1文字=6px幅×8px高)
// ============================================================
void eraseText(int x, int y, int charCount) {
  matrix->fillRect(x, y, charCount * 6, 8, COL_BLACK);
}

// ============================================================
//  updateDisplay()
// ============================================================
void updateDisplay() {
  char buf[20];

  // ---- 行0: 時刻  [記号] HH:MM ----
  snprintf(buf, sizeof(buf), " %02d:%02d", gData.hour, gData.minute);
  uint16_t clockCol = gData.ntpSynced ? COL_CLOCK : COL_GRAY;
  if (strcmp(buf, prevTime) != 0) {
    strcpy(prevTime, buf);
    matrix->fillRect(0, 0, 64, 8, COL_BLACK);  // ★行全体(64px幅)をクリア
    drawClockIcon(0, 1, clockCol);
    matrix->setTextColor(clockCol);
    matrix->setCursor(8, 1);
    matrix->print(buf);
  }
  
// *** "C の "を描き直す → 表示遅い為やめた
//  matrix->drawLine(47, 10,  48, 10, COL_TEMP);
//  matrix->drawLine(47, 11,  48, 11, COL_TEMP);
  
  // ---- 行1: 温度  [記号] 22.1 "C ----
  if (gData.sensorOK) snprintf(buf, sizeof(buf), " %.1f \"C", gData.temp);
  else                snprintf(buf, sizeof(buf), " --.- \"C");

  if (strcmp(buf, prevTemp) != 0) {
    strcpy(prevTemp, buf);
    matrix->fillRect(0, 8, 8, 8, COL_BLACK);
    drawTempIcon(0, 9, COL_TEMP);
    eraseText(8, 9, 7);                       // "22.1\"C" = 最大7文字
    matrix->setTextColor(COL_TEMP);
    matrix->setCursor(8, 9);
    matrix->print(buf);
  }

  // ---- 行2: 湿度  [記号] 65 % ----
  if (gData.sensorOK) snprintf(buf, sizeof(buf), " %.0f   %%", gData.hum);
  else                snprintf(buf, sizeof(buf), " --   %");
  if (strcmp(buf, prevHum) != 0) {
    strcpy(prevHum, buf);
    matrix->fillRect(0, 16, 8, 8, COL_BLACK);
    drawHumIcon(0, 17, COL_HUM);
    eraseText(8, 17, 5);                      // "100%" = 最大5文字
    matrix->setTextColor(COL_HUM);
    matrix->setCursor(8, 17);
    matrix->print(buf);
  }

  // ---- 行3: 気圧  [記号] 1020 hPa ----
  if (gData.sensorOK) snprintf(buf, sizeof(buf), " %.0f hPa", gData.hpa);
  else                snprintf(buf, sizeof(buf), " ---- hPa");
  if (strcmp(buf, prevPres) != 0) {
    strcpy(prevPres, buf);
    matrix->fillRect(0, 24, 8, 8, COL_BLACK);
    drawPresIcon(0, 25, COL_PRES);
    eraseText(8, 25, 8);                      // "1020hPa" = 最大8文字
    matrix->setTextColor(COL_PRES);
    matrix->setCursor(8, 25);
    matrix->print(buf);
  }
}

// ============================================================
//  setup()
// ============================================================
void setup() {
  delay(3000);
  Serial.begin(115200);
  Serial.println("=== ESP32-S3 HUB75 Display START ===");

  Wire.begin(SDA, SCL);

  // SHT31 初期化
  if (!sht31.begin(SHT31_ADDR))
    Serial.println("[ERROR] SHT31が見つかりません。配線を確認してください。");
  else
    Serial.println("[OK] SHT31 初期化完了");

  // BMP280 初期化
  if (!bmp280.begin(BMP280_ADDR)) {
    Serial.println("[ERROR] BMP280が見つかりません。配線を確認してください。");
  } else {
    bmp280.setSampling(
      Adafruit_BMP280::MODE_NORMAL,
      Adafruit_BMP280::SAMPLING_X2,
      Adafruit_BMP280::SAMPLING_X16,
      Adafruit_BMP280::FILTER_X16,
      Adafruit_BMP280::STANDBY_MS_500
    );
    Serial.println("[OK] BMP280 初期化完了");
  }

// HUB75 パネル初期化
  HUB75_I2S_CFG mxconfig(PANEL_WIDTH, PANEL_HEIGHT, PANEL_CHAIN);
  mxconfig.gpio.r1  = HUB75_R1;
  mxconfig.gpio.g1  = HUB75_G1;
  mxconfig.gpio.b1  = HUB75_B1;
  mxconfig.gpio.r2  = HUB75_R2;
  mxconfig.gpio.g2  = HUB75_G2;
  mxconfig.gpio.b2  = HUB75_B2;
  mxconfig.gpio.a   = HUB75_A;
  mxconfig.gpio.b   = HUB75_B;
  mxconfig.gpio.c   = HUB75_C;
  mxconfig.gpio.d   = HUB75_D;
  mxconfig.gpio.clk = HUB75_CLK;
  mxconfig.gpio.lat = HUB75_LAT;
  mxconfig.gpio.oe  = HUB75_OE;

  mxconfig.driver     = HUB75_I2S_CFG::ICN2038S; // ← Gemini の提案
  mxconfig.clkphase    = false;                     // ← ここだけ変えて試す
  mxconfig.i2sspeed    = HUB75_I2S_CFG::HZ_8M;
  mxconfig.double_buff = false;
  
  // ラッチブランキング(ゴースト対策)
  mxconfig.latch_blanking = 2;  // 2, 4, 8, 16, 32 で変化なし

  matrix = new MatrixPanel_I2S_DMA(mxconfig);  // ← 1回だけ
  matrix->begin();                              // ← 1回だけ

  matrix->setRotation(0);
  matrix->setBrightness8(100);  // 初期値
  matrix->fillScreenRGB888(0, 0, 0);
  matrix->setTextWrap(false);
  initColors();

  // 起動時にNTP同期(失敗しても内部RTCで動作継続)
  bool synced = syncNTP();
  if (!synced) {
    Serial.println("[WARN] 起動時NTP同期に失敗。内部RTCで動作継続します。");
    // 内部RTCはリセット後 epoch(1970-01-01 00:00:00)から始まるが動作は継続する
    gData.ntpSynced = false;
  }

  // ★ NTP同期メッセージを消去(残像防止)
  matrix->fillScreenRGB888(0, 0, 0);

  readSensors();

// *** 明るさ検出CDS入力ポートの初期設定
  pinMode(1, ANALOG);
  analogSetAttenuation(ADC_11db);

  Serial.println("[OK] 初期化完了 → 表示開始");
}

// ============================================================
//  loop()
// ============================================================
int lastSecond = -1;  // 追加

void loop() {
  unsigned long now = millis();

  if (now - lastClockUpdate >= CLOCK_UPDATE_MS) {
    lastClockUpdate = now;
    updateClock();

    // ★ 秒が変わった時だけ再描画(0.5秒→1秒に変更)
    if (gData.second != lastSecond) {
      lastSecond = gData.second;
      updateDisplay();
    }
  }

  if (now - lastSensorUpdate >= SENSOR_UPDATE_MS) {
    lastSensorUpdate = now;
    readSensors();
  }

  if (now - lastBrightnessUpdate >= BRIGHTNESS_UPDATE_MS)  {
    lastBrightnessUpdate = now;
    brightness_settings();  // 輝度の設定
  }
}
 

※ここで使ったHUB75 LEDマトリクスパネルはAliexpressで扱われているStarlight Display製のパネルである。

※このプログラムはClaudeの支援で作成した。