M5Stack Core2/CoreS3 + ISOLATED RS485 Unit DMXモニター 開発詳細ログ
📝 はじめに - 本記事について
この記事は「M5Stack Core2 + ISOLATED RS485 UnitでつくるDMX512モニター」の 限定公開版詳細記事 です。
公開版では触れなかった以下の内容を詳しく解説します:
- 🔧 パフォーマンスチューニングの全過程
- 🖥️ Core2 vs CoreS3 の比較テスト
- ⚡ デュアルコア方式によるスピードアップ手法
- 📊 LCD更新速度の限界への挑戦(25ms間隔 = 40fps目標)
- 🤖 Claude Opus 4.5との協働開発の詳細
- 🐛 発生したバグと解決方法
📚 完成映像公開リンク

その後この構成で最高性能のDMX-WIFIテスターが出来そうです。

M5DmxモニターWifiブラウザ接続
https://www.youtube.com/watch?v=CUYLYL7zsFs
M5Stack-S3 DMX mon wifi pc-test(現在wifi接続で20fps本体27fps/40Hz実現しています)
https://www.youtube.com/watch?v=yggU2PEBT-0
https://www.switch-science.com/products/10610?_pos=6&_sid=0f5b96067&_ss=r
https://www.switch-science.com/products/9926?_pos=2&_sid=69b816f5c&_ss=r
Core2 + ISOLATED RS485 Unitで作るDMX512モニター
https://youtu.be/LcLmSH-R_LU

🎯 開発の背景と目標
当初の問題
初期実装では、シングルコア方式で以下のような問題が発生していました:
DMX入力: 40Hz(標準的なDMX512信号)
LCD表示: 14Hz(期待値の1/3以下!)
DMXコンソールから40Hzで信号を送っているのに、モニター表示が14Hzしか出ない。これでは「今の値」がリアルタイムに確認できません。
目標設定
- DMX Hz表示の正確化 - 入力40Hzに対して40Hz±1を表示
- LCD更新速度の最大化 - 可能な限り高いフレームレートを実現
- Core2とCoreS3の比較 - どちらが高性能か検証
🔬 問題分析:なぜ14Hzしか出なかったのか
シングルコア方式の限界
初期実装のコード構造:
void loop() {
// DMX受信(ブロッキング、最大30ms待機)
processDmxReceive();
// 画面更新(約50ms)
updateDmxMonitor();
// ボタン処理
handleButtons();
}
この構造の問題点:
-
DMX受信のブロッキング待機 -
dmx_receive()は最大30msブロック - 画面更新の重さ - 60セル + 512chバーグラフの描画は約50ms
- 処理の直列化 - 1ループ = 約80ms → 12.5Hz が理論限界
ESP32のデュアルコア
ESP32/ESP32-S3はデュアルコアCPU:
| コア | 用途(Arduino) | 備考 |
|---|---|---|
| Core 0 | WiFi/Bluetooth、ユーザータスク | 優先度低めのタスク向け |
| Core 1 |
loop() 実行 |
メインループ |
解決策:DMX受信とLCD更新を別コアで並列実行!
⚡ デュアルコア方式の実装
アーキテクチャ設計
┌─────────────────────────────────────────────────┐
│ ESP32/ESP32-S3 │
├─────────────────────┬───────────────────────────┤
│ Core 0 │ Core 1 │
├─────────────────────┼───────────────────────────┤
│ displayTask() │ loop() │
│ ├─ 画面更新 │ └─ DMX受信専用 │
│ │ (25msごと) │ (30msタイムアウト) │
│ └─ ボタン処理 │ │
│ (100msごと) │ │
├─────────────────────┴───────────────────────────┤
│ dmxDataMutex (データ保護) │
│ dmx_data[513] (共有バッファ) │
└─────────────────────────────────────────────────┘
コア実装コード
ミューテックスとタスクハンドル
// デュアルコア:画面更新タスク用
static TaskHandle_t displayTaskHandle = NULL;
static SemaphoreHandle_t dmxDataMutex = NULL; // DMXデータ保護用ミューテックス
static uint8_t dmx_data_copy[DMX_PACKET_SIZE]; // 画面更新用コピー
画面更新タスク(Core 0)
void displayTask(void* parameter) {
unsigned long last_display_update = 0;
unsigned long last_button_check = 0;
Serial.println("[DisplayTask] Started on Core 0");
while (true) {
unsigned long now = millis();
// ボタン処理(100msごと)
if (now - last_button_check >= 100) {
last_button_check = now;
handleButtons();
}
// 画面更新(25msごと = 40fps目標)
if (now - last_display_update >= 25) {
last_display_update = now;
// DMXデータのコピーを取得(ミューテックスで保護)
if (xSemaphoreTake(dmxDataMutex, pdMS_TO_TICKS(5)) == pdTRUE) {
memcpy(dmx_data_copy, dmx_data, sizeof(dmx_data));
xSemaphoreGive(dmxDataMutex);
}
// 画面更新
updateDmxMonitor();
}
// 少し待機してCPUを解放
vTaskDelay(pdMS_TO_TICKS(1));
}
}
セットアップでのタスク起動
void setup() {
// M5Stack初期化
auto cfg = M5.config();
cfg.serial_baudrate = 115200;
M5.begin(cfg);
// ミューテックス作成
dmxDataMutex = xSemaphoreCreateMutex();
// ディスプレイ設定
M5.Display.setRotation(1);
M5.Display.setBrightness(128);
M5.Display.fillScreen(TFT_BLACK);
// DMX初期化
initDmx();
// 初期画面
drawInitScreen();
// 画面更新タスクをCore 0で起動
xTaskCreatePinnedToCore(
displayTask, // タスク関数
"DisplayTask", // タスク名
8192, // スタックサイズ
NULL, // パラメータ
1, // 優先度(低め)
&displayTaskHandle, // タスクハンドル
0 // Core 0で実行
);
Serial.println("Setup complete!");
Serial.printf("Main loop running on Core %d\n", xPortGetCoreID());
}
メインループ(Core 1 - DMX専用)
void loop() {
// DMX受信のみ(画面更新は別タスク)
processDmxReceive();
}
デュアルコア化の効果
| 項目 | シングルコア | デュアルコア |
|---|---|---|
| DMX受信 | loop()で直列 | Core 1専用 |
| 画面更新 | loop()で直列 | Core 0専用 |
| DMX Hz表示 | 14Hz | 40Hz ✅ |
| LCD更新 | 14Hz | 20Hz以上 |
🖥️ Core2 vs CoreS3 比較テスト
ハードウェア比較
| 項目 | M5Stack Core2 | M5Stack CoreS3 |
|---|---|---|
| CPU | ESP32 | ESP32-S3 |
| クロック | 240MHz | 240MHz |
| コア | 2コア | 2コア |
| RAM | 520KB SRAM + 8MB PSRAM | 512KB SRAM + 8MB PSRAM |
| ディスプレイ | 320×240 ILI9342C | 320×240 ILI9342C |
| タッチ | FT6336U | FT6336U |
| Port A (GPIO) | GPIO32/33 | GPIO1/2 |
GPIO設定の違い
Core2版
// Core2 Port A: GPIO32(TX), GPIO33(RX)
#define DMX_RX_PIN 33 // GPIO33 (Port A) ← ユニットのTXDに接続
#define DMX_TX_PIN 32 // GPIO32 (Port A) → ユニットのRXDに接続
#define DMX_EN_PIN -1 // ENピンなし(IS3082Wは自動方向切り替え)
CoreS3版
// CoreS3 Port A: GPIO1(SDA/白線), GPIO2(SCL/黄線)
#define DMX_RX_PIN 1 // GPIO1 (Port A G1) ← ユニットのTXDに接続
#define DMX_TX_PIN 2 // GPIO2 (Port A G2) → ユニットのRXDに接続
#define DMX_EN_PIN -1 // ENピンなし(IS3082Wは自動方向切り替え)
CoreS3版作成時のトラブル
最初、公式ドキュメントを誤読してGPIO8/9で設定しましたが、DMX受信できませんでした:
// ❌ 最初の間違った設定
#define DMX_RX_PIN 8 // 間違い!
#define DMX_TX_PIN 9 // 間違い!
正しくは、Port A = GPIO1/2 でした。
💡 教訓: CoreS3のPort Aは公式ドキュメントで「G1, G2」と表記されており、GPIO番号そのままでした。
📊 LCD更新速度の限界への挑戦
画面更新間隔のテスト
デュアルコア化後、画面更新間隔をどこまで短くできるかテストしました。
テスト条件
- DMX入力: 40Hz(安定)
- 表示内容: 60chセル + 512chバーグラフ + ステータス表示
50ms間隔(初期設定)
// 画面更新(50msごと = 20fps目標)
if (now - last_display_update >= 50) {
結果:
| デバイス | LCD Hz | DMX Hz |
|---|---|---|
| Core2 | 21 | 40 |
| CoreS3 | 20 | 40 |
両者ともDMX 40Hzを正しく表示。LCD更新は約20Hz。
25ms間隔(高速化)
// 画面更新(25msごと = 40fps目標)
if (now - last_display_update >= 25) {
結果:
| デバイス | LCD Hz | DMX Hz |
|---|---|---|
| Core2 | 23 | 40 |
| CoreS3 | 31 | 40 |
CoreS3が約35%高速!
結果まとめ
| 設定 | Core2 LCD Hz | CoreS3 LCD Hz | DMX Hz |
|---|---|---|---|
| 50ms間隔 | 21 | 20 | 40 |
| 25ms間隔 | 23 | 31 | 40 |
なぜCoreS3が速いのか
ESP32-S3はESP32と比べて以下の点で優れています:
- グラフィック処理能力: ESP32-S3はベクトル拡張命令(SIMD)をサポート
- メモリアクセス: キャッシュ効率が向上
- SPIバス: ディスプレイへのデータ転送が高速
実測では、同じコードでもCoreS3は約35%高速に画面更新できました。
🎨 描画最適化テクニック
1. スプライト(オフスクリーンバッファ)の活用
static M5Canvas cellSprite(&M5.Display);
void setup() {
// スプライト初期化(28px高さで横線を上書きしない)
cellSprite.setColorDepth(16);
cellSprite.createSprite(32, 28); // セルサイズ
}
void updateDmxMonitor() {
// スプライトにセルを描画
cellSprite.fillSprite(TFT_BLACK);
// ... セル内容描画 ...
// 一度に転送(高速)
cellSprite.pushSprite(x, y);
}
スプライトを使うメリット:
- 個別のdrawPixel/fillRectを減らせる
- まとめて転送することでSPIオーバーヘッド削減
2. 差分更新
static uint8_t prev_dmx_values[DMX_PACKET_SIZE];
for (int i = 0; i < DISPLAY_CH_COUNT; i++) {
int ch = display_start_ch + i;
uint8_t value = dmx_data[ch];
// 変更がなければスキップ(高速化のキモ)
if (value == prev_dmx_values[ch]) continue;
prev_dmx_values[ch] = value;
// 変化したセルのみ再描画
// ...
}
DMX信号は毎フレーム全チャンネル送られてきますが、値が変化したチャンネルのみ再描画することで大幅に高速化。
3. 小型カスタムフォント
// 4x6ピクセルの小型数字フォント
static const uint8_t smallDigits[16][6] = {
{0b0110, 0b1001, 0b1001, 0b1001, 0b1001, 0b0110}, // 0
{0b0010, 0b0110, 0b0010, 0b0010, 0b0010, 0b0111}, // 1
{0b0110, 0b1001, 0b0010, 0b0100, 0b1000, 0b1111}, // 2
{0b0110, 0b1001, 0b0010, 0b0001, 0b1001, 0b0110}, // 3
{0b0001, 0b0011, 0b0101, 0b1001, 0b1111, 0b0001}, // 4
{0b1111, 0b1000, 0b1110, 0b0001, 0b1001, 0b0110}, // 5
{0b0110, 0b1000, 0b1110, 0b1001, 0b1001, 0b0110}, // 6
{0b1111, 0b0001, 0b0010, 0b0010, 0b0100, 0b0100}, // 7
{0b0110, 0b1001, 0b0110, 0b1001, 0b1001, 0b0110}, // 8
{0b0110, 0b1001, 0b1001, 0b0111, 0b0001, 0b0110}, // 9
{0b0110, 0b1001, 0b1111, 0b1001, 0b1001, 0b1001}, // A
{0b1110, 0b1001, 0b1110, 0b1001, 0b1001, 0b1110}, // B
{0b0110, 0b1001, 0b1000, 0b1000, 0b1001, 0b0110}, // C
{0b1110, 0b1001, 0b1001, 0b1001, 0b1001, 0b1110}, // D
{0b1111, 0b1000, 0b1110, 0b1000, 0b1000, 0b1111}, // E
{0b1111, 0b1000, 0b1110, 0b1000, 0b1000, 0b1000}, // F
};
void drawSmallDigitSprite(M5Canvas* spr, int x, int y, int digit, uint16_t color, int scale) {
if (digit < 0 || digit > 15) return;
for (int row = 0; row < 6; row++) {
for (int col = 0; col < 4; col++) {
if (smallDigits[digit][row] & (0b1000 >> col)) {
spr->drawPixel(x + col, y + row, color);
}
}
}
}
標準フォントよりビットマップフォントの方が高速に描画できます。
4. 512chバーグラフの差分更新
static uint8_t prev_512_values[513];
for (int ch = 1; ch <= 260; ch++) {
uint8_t val = dmx_data[ch];
// 変化したチャンネルのみ更新
if (val != prev_512_values[ch]) {
prev_512_values[ch] = val;
int bar_h = (val * bar512_h) / 255;
// 1ピクセル幅の縦線で描画(高速)
M5.Display.drawFastVLine(x, bar512_y1, bar512_h, TFT_BLACK);
if (bar_h > 0) {
M5.Display.drawFastVLine(x, bar512_y1 + bar512_h - bar_h, bar_h, bar_color);
}
}
}
512ch全てを毎フレーム描画すると重いので、差分のみ更新。
🐛 発生したバグと解決方法
バグ1: ページ切り替え後に512chバーグラフが消える
症状
ページを切り替えると、512chバーグラフとデータ長表示が消えてしまう。
原因
drawInitScreen() 関数で画面を fillScreen(TFT_BLACK) でクリアした際に、以下のフラグ・前回値がリセットされていなかった:
-
bar512_initialized- 512chバーグラフの初期化フラグ -
prev_512_values[]- 512chバーグラフの前回値 -
各種
prev_*変数 - フッター表示(Hz、データ長等)の前回値
解決策
drawInitScreen() の最後に以下を追加:
// 512chバーグラフも再初期化が必要
bar512_initialized = false;
memset(prev_512_values, 0, sizeof(prev_512_values));
// フッター表示も強制更新
prev_packet_per_sec = -1;
prev_data_length = -1;
prev_len_min = -1;
prev_len_max = -1;
prev_len_change_count = -1;
prev_lcd_update_per_sec = -1;
バグ2: CoreS3でDMX受信できない
症状
CoreS3版を作成したが、DMX信号を受信できない(NO SIGNALのまま)。
原因
GPIO設定を間違えていた。最初はGPIO8/9で設定したが、CoreS3のPort AはGPIO1/2だった。
解決策
正しいGPIO設定:
// CoreS3 Port A: GPIO1(G1), GPIO2(G2)
#define DMX_RX_PIN 1 // GPIO1 (Port A G1)
#define DMX_TX_PIN 2 // GPIO2 (Port A G2)
バグ3: ビルド時に「multiple definition」エラー
症状
Core2版をビルドすると、CoreS3版のソースも含まれてリンクエラーになる。
multiple definition of `setup'
multiple definition of `loop'
原因
platformio.ini の build_src_filter にCoreS3版のソースを除外する設定がなかった。
解決策
build_src_filter に除外設定を追加:
# Core2版
build_src_filter = +<*> -<src.cpp> -<src_dmx_monitor_s3_iso.cpp> +<src_dmx_monitor_core2_iso.cpp>
# CoreS3版
build_src_filter = +<*> -<src.cpp> -<src_dmx_monitor_core2_iso.cpp> +<src_dmx_monitor_s3_iso.cpp>
🔧 PlatformIO設定
Core2版
[env:m5stack-core2-iso485]
platform = espressif32
board = m5stack-core2
framework = arduino
board_build.f_flash = 80000000L
board_build.f_cpu = 240000000L
monitor_speed = 115200
upload_speed = 921600
monitor_filters = time, esp32_exception_decoder
build_flags =
-DCORE_DEBUG_LEVEL=3
-DDMX_MONITOR_ONLY
-DISO_RS485
build_type = release
build_src_filter = +<*> -<src.cpp> -<src_dmx_monitor_s3.cpp> -<src_dmx_monitor_s3_base.cpp> -<src_dmx_monitor_core2_base.cpp> -<src_dmx_monitor_s3_iso.cpp> +<src_dmx_monitor_core2_iso.cpp>
lib_deps =
M5Stack/M5Unified
M5Stack/M5GFX
https://github.com/someweisguy/esp_dmx.git
; Core2 DMXモニター(ISOLATED RS485 Unit - Port A)
; Port A: TXD=GPIO33, RXD=GPIO32
upload_port = COM22
CoreS3版
[env:m5stack-cores3-iso485]
platform = espressif32
board = m5stack-cores3
framework = arduino
board_build.f_flash = 80000000L
board_build.f_cpu = 240000000L
monitor_speed = 115200
upload_speed = 921600
monitor_filters = time, esp32_exception_decoder
build_flags =
-DCORE_DEBUG_LEVEL=3
-DDMX_MONITOR_ONLY
-DISO_RS485
-DUSE_CORE_S3
build_type = release
build_src_filter = +<*> -<src.cpp> -<src_dmx_monitor_s3.cpp> -<src_dmx_monitor_s3_base.cpp> -<src_dmx_monitor_core2_base.cpp> -<src_dmx_monitor_core2_iso.cpp> +<src_dmx_monitor_s3_iso.cpp>
lib_deps =
M5Stack/M5Unified
M5Stack/M5GFX
https://github.com/someweisguy/esp_dmx.git
; CoreS3 DMXモニター(ISOLATED RS485 Unit - Port A)
; Port A: TXD=GPIO2, RXD=GPIO1
upload_port = COM30
📝 完全なソースコード
Core2版(src_dmx_monitor_core2_iso.cpp)
クリックで展開(約760行)
/**
* DMX Monitor for M5Stack Core2 + ISOLATED RS485 Unit
*
* ISOLATED RS485ユニット(CA IS3082W絶縁トランシーバ)をPort Aに接続して
* DMX512信号を受信し、Core2のディスプレイにチャンネル値を表示
*
* 接続:
* ISOLATED RS485 Unit (Port A):
* - TXD: GPIO33
* - RXD: GPIO32
* - 5V: 5V
* - GND: GND
* RS485側:
* - A → DMX+ (Data+)
* - B → DMX- (Data-)
* - GND → DMX GND
*/
#include <M5Unified.hpp>
#include <M5GFX.h>
#include <esp_dmx.h>
#include <esp_intr_alloc.h>
#include "build_time.h"
// ========================================
// DMX設定(Core2 + ISOLATED RS485 Unit - Port A)
// ========================================
#define DMX_RX_PIN 33 // GPIO33 (Port A) ← ユニットのTXDに接続
#define DMX_TX_PIN 32 // GPIO32 (Port A) → ユニットのRXDに接続
#define DMX_EN_PIN -1 // ENピンなし(IS3082Wは自動方向切り替え)
const dmx_port_t DMX_PORT = DMX_NUM_1;
#define DMX_CH_COUNT 512
#define DMX_TIMEOUT_MS 3000
#define DMX_RECV_TIMEOUT_TICK pdMS_TO_TICKS(30)
// DMXデータ
static uint8_t dmx_data[DMX_PACKET_SIZE];
static bool dmx_signal_detected = false;
static unsigned long last_dmx_packet = 0;
static int dmx_packet_count = 0;
static int dmx_packet_per_sec = 0;
static unsigned long last_count_reset = 0;
static int dmx_data_length = -1;
static int dmx_data_length_min = 512;
static int dmx_data_length_max = 0;
static int dmx_data_length_change_count = 0;
// 表示設定
static int display_start_ch = 1;
static const int DISPLAY_COLS = 10;
static const int DISPLAY_ROWS = 6;
static const int DISPLAY_CH_COUNT = DISPLAY_COLS * DISPLAY_ROWS;
// 前回値(差分更新用)
static uint8_t prev_dmx_values[DMX_PACKET_SIZE];
static bool screen_initialized = false;
static int prev_packet_per_sec = -1;
static int prev_data_length = -1;
static int prev_len_min = -1;
static int prev_len_max = -1;
static int prev_len_change_count = -1;
static int prev_start_ch = -1;
static int dmx_display_mode = 0;
// LCD更新周波数計測
static int lcd_update_count = 0;
static int lcd_update_per_sec = 0;
static unsigned long last_lcd_count_reset = 0;
static int prev_lcd_update_per_sec = -1;
// 最後に変化したチャンネル情報
static int last_changed_ch = 0;
static uint8_t last_changed_value = 0;
static int prev_last_changed_ch = -1;
static uint8_t prev_last_changed_value = 255;
static int frame_min_changed_ch = 9999;
static uint8_t frame_min_changed_value = 0;
// デュアルコア
static TaskHandle_t displayTaskHandle = NULL;
static SemaphoreHandle_t dmxDataMutex = NULL;
static uint8_t dmx_data_copy[DMX_PACKET_SIZE];
// 512chバーグラフ用
static uint8_t prev_512_values[513];
static bool bar512_initialized = false;
// スプライト
static M5Canvas cellSprite(&M5.Display);
static bool sprite_initialized = false;
// 小型数字フォント(4x6ピクセル)
static const uint8_t smallDigits[16][6] = {
{0b0110, 0b1001, 0b1001, 0b1001, 0b1001, 0b0110}, // 0
{0b0010, 0b0110, 0b0010, 0b0010, 0b0010, 0b0111}, // 1
{0b0110, 0b1001, 0b0010, 0b0100, 0b1000, 0b1111}, // 2
{0b0110, 0b1001, 0b0010, 0b0001, 0b1001, 0b0110}, // 3
{0b0001, 0b0011, 0b0101, 0b1001, 0b1111, 0b0001}, // 4
{0b1111, 0b1000, 0b1110, 0b0001, 0b1001, 0b0110}, // 5
{0b0110, 0b1000, 0b1110, 0b1001, 0b1001, 0b0110}, // 6
{0b1111, 0b0001, 0b0010, 0b0010, 0b0100, 0b0100}, // 7
{0b0110, 0b1001, 0b0110, 0b1001, 0b1001, 0b0110}, // 8
{0b0110, 0b1001, 0b1001, 0b0111, 0b0001, 0b0110}, // 9
{0b0110, 0b1001, 0b1111, 0b1001, 0b1001, 0b1001}, // A
{0b1110, 0b1001, 0b1110, 0b1001, 0b1001, 0b1110}, // B
{0b0110, 0b1001, 0b1000, 0b1000, 0b1001, 0b0110}, // C
{0b1110, 0b1001, 0b1001, 0b1001, 0b1001, 0b1110}, // D
{0b1111, 0b1000, 0b1110, 0b1000, 0b1000, 0b1111}, // E
{0b1111, 0b1000, 0b1110, 0b1000, 0b1000, 0b1000}, // F
};
// スプライトに小さい数字を描画
void drawSmallDigitSprite(M5Canvas* spr, int x, int y, int digit, uint16_t color, int scale) {
if (digit < 0 || digit > 15) return;
for (int row = 0; row < 6; row++) {
for (int col = 0; col < 4; col++) {
if (smallDigits[digit][row] & (0b1000 >> col)) {
if (scale == 1) {
spr->drawPixel(x + col, y + row, color);
} else {
spr->fillRect(x + col * scale, y + row * scale, scale, scale, color);
}
}
}
}
}
// LCDに小さい数字を描画
void drawSmallDigit(int x, int y, int digit, uint16_t color, int scale) {
if (digit < 0 || digit > 15) return;
for (int row = 0; row < 6; row++) {
for (int col = 0; col < 4; col++) {
if (smallDigits[digit][row] & (0b1000 >> col)) {
if (scale == 1) {
M5.Display.drawPixel(x + col, y + row, color);
} else {
M5.Display.fillRect(x + col * scale, y + row * scale, scale, scale, color);
}
}
}
}
}
// 2桁数字を描画(LCD)
void drawSmallNumber2(int x, int y, int num, uint16_t color, int scale) {
char buf[3];
snprintf(buf, sizeof(buf), "%2d", num);
int char_w = 5 * scale;
for (int i = 0; i < 2; i++) {
char c = buf[i];
if (c == ' ') continue;
int digit = c - '0';
drawSmallDigit(x + i * char_w, y, digit, color, scale);
}
}
// 3桁数字を描画(LCD)- ゼロ埋め
void drawSmallNumber3(int x, int y, int num, uint16_t color, int scale) {
char buf[4];
snprintf(buf, sizeof(buf), "%03d", num);
int char_w = 5 * scale;
for (int i = 0; i < 3; i++) {
char c = buf[i];
int digit = c - '0';
drawSmallDigit(x + i * char_w, y, digit, color, scale);
}
}
// ========================================
// DMX初期化
// ========================================
void initDmx() {
Serial.println("Initializing DMX...");
dmx_config_t config = {
.interrupt_flags = ESP_INTR_FLAG_IRAM,
.root_device_parameter_count = 0,
.sub_device_parameter_count = 0,
.model_id = 0,
.product_category = 0,
.software_version_id = 0x00010000,
.software_version_label = "1.0.0",
.queue_size_max = 0
};
if (!dmx_driver_install(DMX_PORT, &config, nullptr, 0)) {
Serial.println("DMX driver install failed!");
return;
}
if (!dmx_set_pin(DMX_PORT, DMX_TX_PIN, DMX_RX_PIN, DMX_EN_PIN)) {
Serial.println("DMX set pin failed!");
return;
}
if (DMX_EN_PIN >= 0) {
pinMode(DMX_EN_PIN, OUTPUT);
digitalWrite(DMX_EN_PIN, LOW);
}
Serial.printf("DMX initialized: RX=GPIO%d, TX=GPIO%d\n", DMX_RX_PIN, DMX_TX_PIN);
}
// ========================================
// 初期画面描画
// ========================================
void drawInitScreen() {
M5.Display.fillScreen(TFT_BLACK);
M5.Display.setTextDatum(textdatum_t::top_left);
M5.Display.setFont(&fonts::Font0);
M5.Display.setTextColor(M5.Display.color565(128, 128, 128));
char title[32];
snprintf(title, sizeof(title), "DMX %03d-%03d", display_start_ch, display_start_ch + DISPLAY_CH_COUNT - 1);
M5.Display.drawString(title, 2, 2);
prev_last_changed_ch = -1;
const int grid_x = 0;
const int grid_y = 15;
const int cell_w = 32;
const int cell_h = 30;
const int bar_w = 2;
const int bar_max_h = 26;
const uint16_t row_line_color = M5.Display.color565(0, 0, 60);
for (int row = 0; row < DISPLAY_ROWS; row++) {
int top_line_y = grid_y + row * cell_h - 1;
M5.Display.drawFastHLine(0, top_line_y, 320, row_line_color);
for (int col = 0; col < DISPLAY_COLS; col++) {
int ch = display_start_ch + row * DISPLAY_COLS + col;
int x = grid_x + col * cell_w;
int y = grid_y + row * cell_h;
drawSmallNumber2(x + cell_w - bar_w - 11, y + 1, ch, TFT_DARKGREY, 1);
int bar_x = x + cell_w - bar_w;
int bar_y = y - 1;
M5.Display.fillRect(bar_x, bar_y, bar_w, bar_max_h, M5.Display.color565(30, 30, 30));
}
int line_y = grid_y + (row + 1) * cell_h - 1;
M5.Display.drawFastHLine(0, line_y, 320, row_line_color);
}
M5.Display.fillRect(0, 232, 320, 8, TFT_BLACK);
M5.Display.setTextDatum(textdatum_t::bottom_right);
M5.Display.setTextColor(TFT_DARKGREY);
M5.Display.setFont(&fonts::Font0);
M5.Display.drawString(BUILD_TIMESTAMP, 318, 240);
if (!sprite_initialized) {
cellSprite.setColorDepth(16);
cellSprite.createSprite(32, 28);
sprite_initialized = true;
}
screen_initialized = true;
prev_start_ch = display_start_ch;
memset(prev_dmx_values, 0xFF, sizeof(prev_dmx_values));
// 512chバーグラフも再初期化が必要
bar512_initialized = false;
memset(prev_512_values, 0, sizeof(prev_512_values));
// フッター表示も強制更新
prev_packet_per_sec = -1;
prev_data_length = -1;
prev_len_min = -1;
prev_len_max = -1;
prev_len_change_count = -1;
prev_lcd_update_per_sec = -1;
}
// ========================================
// DMX受信処理
// ========================================
void processDmxReceive() {
dmx_packet_t packet;
unsigned long now = millis();
if (dmx_receive(DMX_PORT, &packet, DMX_RECV_TIMEOUT_TICK)) {
if (packet.err == DMX_OK) {
if (dmxDataMutex != NULL && xSemaphoreTake(dmxDataMutex, pdMS_TO_TICKS(1)) == pdTRUE) {
dmx_read(DMX_PORT, dmx_data, packet.size);
xSemaphoreGive(dmxDataMutex);
} else {
dmx_read(DMX_PORT, dmx_data, packet.size);
}
int len = packet.size - 1;
if (len != dmx_data_length) {
if (dmx_data_length_change_count < 999) {
dmx_data_length_change_count++;
}
}
if (len < dmx_data_length_min) {
dmx_data_length_min = len;
}
if (len > dmx_data_length_max) {
dmx_data_length_max = len;
}
dmx_data_length = len;
last_dmx_packet = now;
dmx_signal_detected = true;
dmx_packet_count++;
if (last_count_reset == 0) {
last_count_reset = now;
}
if (now - last_count_reset >= 1000) {
dmx_packet_per_sec = dmx_packet_count;
dmx_packet_count = 0;
last_count_reset = now;
}
}
}
if (dmx_signal_detected && (now - last_dmx_packet > DMX_TIMEOUT_MS)) {
dmx_signal_detected = false;
dmx_packet_per_sec = 0;
dmx_data_length = -1;
dmx_data_length_min = 512;
dmx_data_length_max = 0;
dmx_data_length_change_count = 0;
prev_data_length = -1;
prev_len_min = -1;
prev_len_max = -1;
prev_len_change_count = -1;
last_count_reset = 0;
}
}
// ========================================
// ボタン処理
// ========================================
void handleButtons() {
M5.update();
auto touch = M5.Touch.getDetail();
if (touch.wasPressed()) {
int tx = touch.x;
int ty = touch.y;
if (ty > 160) {
if (tx < 107) {
display_start_ch -= DISPLAY_CH_COUNT;
if (display_start_ch < 1) display_start_ch = 1;
} else if (tx > 213) {
display_start_ch += DISPLAY_CH_COUNT;
if (display_start_ch > 512 - DISPLAY_CH_COUNT + 1) {
display_start_ch = 512 - DISPLAY_CH_COUNT + 1;
}
} else {
dmx_display_mode = (dmx_display_mode + 1) % 3;
memset(prev_dmx_values, 0xFF, sizeof(prev_dmx_values));
}
}
}
}
// ========================================
// 画面更新タスク(Core 0で実行)
// ========================================
void displayTask(void* parameter) {
unsigned long last_display_update = 0;
unsigned long last_button_check = 0;
while (true) {
unsigned long now = millis();
if (now - last_button_check >= 100) {
last_button_check = now;
handleButtons();
}
if (now - last_display_update >= 25) {
last_display_update = now;
if (xSemaphoreTake(dmxDataMutex, pdMS_TO_TICKS(5)) == pdTRUE) {
memcpy(dmx_data_copy, dmx_data, sizeof(dmx_data));
xSemaphoreGive(dmxDataMutex);
}
updateDmxMonitor();
}
vTaskDelay(pdMS_TO_TICKS(1));
}
}
// ========================================
// セットアップ
// ========================================
void setup() {
auto cfg = M5.config();
cfg.serial_baudrate = 115200;
M5.begin(cfg);
Serial.println("\n=== DMX Monitor for Core2 + ISOLATED RS485 ===");
dmxDataMutex = xSemaphoreCreateMutex();
M5.Display.setRotation(1);
M5.Display.setBrightness(128);
M5.Display.fillScreen(TFT_BLACK);
initDmx();
drawInitScreen();
xTaskCreatePinnedToCore(
displayTask, "DisplayTask", 8192, NULL, 1,
&displayTaskHandle, 0
);
Serial.println("Setup complete!");
}
// ========================================
// メインループ(Core 1でDMX受信専用)
// ========================================
void loop() {
processDmxReceive();
}
CoreS3版との違い
CoreS3版(src_dmx_monitor_s3_iso.cpp)はCore2版とほぼ同じですが、GPIOピン設定のみ異なります:
// CoreS3 Port A
#define DMX_RX_PIN 1 // GPIO1 (Port A G1)
#define DMX_TX_PIN 2 // GPIO2 (Port A G2)
🤖 Claude Opus 4.5との協働開発
開発ログ
| 時間 | 作業内容 | AIの貢献 |
|---|---|---|
| 0:00 | 「DMX Hz表示が14Hzしか出ない」と相談 | 問題分析 |
| 0:10 | シングルコア方式の限界を説明 | デュアルコア方式を提案 |
| 0:30 | デュアルコア実装コード生成 | FreeRTOSタスク作成 |
| 0:45 | 動作確認 → 21/40Hz達成 | ミューテックス保護追加 |
| 1:00 | CoreS3版のリクエスト | GPIO設定を調査・提案 |
| 1:15 | CoreS3で受信できない問題 | GPIO8/9→GPIO1/2の修正案 |
| 1:30 | CoreS3で20/40Hz達成 | 動作確認 |
| 1:45 | 「LCD更新を速くしたい」 | 25ms間隔を提案 |
| 2:00 | CoreS3で31/40Hz達成! | 結果分析 |
| 2:15 | Core2でも23/40Hz達成 | 比較表作成 |
| 2:30 | ページ切替でバーグラフ消える | 原因特定・修正 |
AIとの対話例
私: LCD更新最速はいくつですか?
Claude:
現在50ms間隔(20fps目標)で設定されています。25ms間隔(40fps目標)に変更してみましょうか?ただし、描画処理が間に合わなければ実効フレームレートは上がりません。
私: やってみて
Claude:
両デバイスの画面更新間隔を25msに変更しました。アップロードして確認してください。
→ CoreS3で31Hz、Core2で23Hzを達成!
私: core2で、データ長カウンタ、ラベルなど無くなりました。なぜ?
Claude:
原因がわかりました。
drawInitScreen()関数でbar512_initialized = falseへのリセットがありません。ページを切り替えると画面がクリアされますが、フラグはリセットされないため、バーグラフの背景・ラベルが再描画されません。
→ 修正コードを生成、アップロードで解決!
📈 パフォーマンス最終結果
最終結果まとめ
| 設定 | Core2 LCD Hz | CoreS3 LCD Hz | DMX Hz |
|---|---|---|---|
| シングルコア 50ms | 14 | - | 14 ❌ |
| デュアルコア 50ms | 21 | 20 | 40 ✅ |
| デュアルコア 25ms | 23 | 31 | 40 ✅ |
改善率
- DMX Hz精度: 14Hz → 40Hz(約3倍向上、入力と一致)
- LCD更新: 14Hz → 31Hz(CoreS3で約2.2倍向上)
🎓 まとめ
学んだこと
- ESP32/S3のデュアルコアを活用 - タイムクリティカルな処理(DMX受信)とUI処理(画面更新)を分離
- ミューテックスによるデータ保護 - 共有データへのアクセスを安全に
- 差分更新による描画最適化 - 変化したピクセルのみ再描画
- CoreS3はCore2より高速 - 同じコードで約35%高いLCD更新レート
- 状態フラグのリセット忘れに注意 - 画面クリア時は関連フラグもリセットする
今後の改善案
- DMA転送の活用(さらなる高速化)
- WiFi経由のリモートモニタリング
- SDカードへのロギング機能
- 複数ユニバース対応(Art-Net)
📚 参考リンク
開発環境: VS Code + GitHub Copilot Chat (Claude Opus 4.5 Preview)
開発期間: 約2.5時間
開発日: 2025年12月10日
ライセンス: MIT License