Last active
June 25, 2026 13:38
-
-
Save Yuikawa-Akira/1e2ff7afe4495e3ad83ea3d1c82279fd to your computer and use it in GitHub Desktop.
SPI2NTSC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <driver/spi_slave.h> | |
| #include <M5UnitRCA.h> | |
| #include <M5Unified.h> | |
| const uint16_t logical_width_pix = 240, logical_height_pix = 180; // NTSC出力範囲 | |
| const uint16_t canvas_width_pix = 240, canvas_height_pix = 180; // 実際に使用する範囲 | |
| int buffer_size = canvas_width_pix * canvas_height_pix; | |
| // ========================================== | |
| // 描画・通信モードの選択 | |
| // 1: 白黒2値 | |
| // 2: グレースケール (8-bit) | |
| // 3: RGB332 カラー (8-bit) | |
| // ========================================== | |
| int current_mode = 2; // 初期モード: 2 (グレースケール) | |
| M5Canvas* canvasPtr = nullptr; | |
| // Atom Liteの内蔵LEDピン | |
| const int LED_PIN = 27; | |
| bool isOutputEnabled = true; | |
| // SPIピン設定 (Atom Lite) | |
| const int SCK_PIN = 22; | |
| const int MOSI_PIN = 19; | |
| const int CS_PIN = 23; | |
| // DMA転送用の受信バッファポインタ | |
| uint8_t* rx_buffer = nullptr; | |
| // --- 描画モードを切り替える関数 --- | |
| void setMode(int mode) { | |
| current_mode = mode; | |
| // 既存のインスタンスがあれば、メモリから完全に削除してクリーンにする | |
| if (canvasPtr != nullptr) { | |
| canvasPtr->deleteSprite(); | |
| delete canvasPtr; | |
| canvasPtr = nullptr; | |
| } | |
| // まっさらな状態でインスタンスを再生成 | |
| canvasPtr = new M5Canvas(&M5.Display); | |
| if (mode == 1) { | |
| canvasPtr->setColorDepth(1); | |
| buffer_size = (canvas_width_pix * canvas_height_pix) / 8; | |
| } else if (mode == 2) { | |
| canvasPtr->setColorDepth(m5gfx::color_depth_t::grayscale_8bit); | |
| buffer_size = canvas_width_pix * canvas_height_pix; | |
| } else if (mode == 3) { | |
| canvasPtr->setColorDepth(8); | |
| buffer_size = canvas_width_pix * canvas_height_pix; | |
| } | |
| // キャンバスを再生成 | |
| canvasPtr->createSprite(canvas_width_pix, canvas_height_pix); | |
| } | |
| void setup() { | |
| auto cfg = M5.config(); | |
| cfg.internal_imu = false; | |
| cfg.internal_rtc = false; | |
| cfg.internal_spk = false; | |
| cfg.internal_mic = false; | |
| cfg.external_spk = false; | |
| cfg.external_display.unit_rca = true; | |
| cfg.unit_rca.logical_width = logical_width_pix; | |
| cfg.unit_rca.logical_height = logical_height_pix; | |
| cfg.unit_rca.output_width = canvas_width_pix; | |
| cfg.unit_rca.output_height = canvas_height_pix; | |
| cfg.unit_rca.signal_type = M5UnitRCA::signal_type_t::NTSC_J; | |
| cfg.unit_rca.use_psram = M5UnitRCA::use_psram_t::psram_no_use; | |
| cfg.unit_rca.pin_dac = GPIO_NUM_25; // 25 or 26 | |
| cfg.unit_rca.output_level = 128; | |
| M5.begin(cfg); | |
| M5.In_I2C.release(); | |
| setMode(current_mode); | |
| if (canvasPtr == nullptr) return; | |
| M5Canvas& canvas = *canvasPtr; | |
| canvas.clear(); | |
| canvas.setTextSize(1); | |
| canvas.setTextColor(255); | |
| canvas.setCursor(20, 20); | |
| canvas.print("Waiting for Signal..."); | |
| canvas.pushSprite(0, 0); | |
| // --- SPI Slave (ESP-IDFドライバ) の初期化 --- | |
| // DMA転送用のメモリを確保 | |
| rx_buffer = (uint8_t*)heap_caps_malloc(buffer_size, MALLOC_CAP_DMA); | |
| spi_bus_config_t buscfg = {}; | |
| buscfg.mosi_io_num = MOSI_PIN; | |
| buscfg.miso_io_num = -1; // MISO未使用 | |
| buscfg.sclk_io_num = SCK_PIN; | |
| buscfg.quadwp_io_num = -1; | |
| buscfg.quadhd_io_num = -1; | |
| buscfg.max_transfer_sz = buffer_size + 8; // 最大転送サイズをバッファ以上に設定 | |
| spi_slave_interface_config_t slvcfg = {}; | |
| slvcfg.mode = 0; | |
| slvcfg.spics_io_num = CS_PIN; | |
| slvcfg.queue_size = 1; | |
| slvcfg.flags = 0; | |
| // SPI3_HOST (VSPI) をスレーブとして初期化、自動DMAチャネルを割り当て | |
| spi_slave_initialize(SPI3_HOST, &buscfg, &slvcfg, SPI_DMA_CH_AUTO); | |
| } | |
| void loop() { | |
| M5.update(); | |
| // --- BtnAによるモード切替処理 --- | |
| if (M5.BtnA.wasPressed()) { | |
| int next_mode = current_mode + 1; | |
| if (next_mode > 3) next_mode = 1; | |
| setMode(next_mode); | |
| } | |
| if (canvasPtr == nullptr) return; | |
| M5Canvas& canvas = *canvasPtr; | |
| if (current_mode == 1) { | |
| neopixelWrite(LED_PIN, 20, 20, 0); // オレンジ | |
| } else if (current_mode == 2) { | |
| neopixelWrite(LED_PIN, 0, 20, 0); // 緑 | |
| } else if (current_mode == 3) { | |
| neopixelWrite(LED_PIN, 0, 0, 20); // 青 | |
| } | |
| // --- SPIでの映像受信処理 --- | |
| spi_slave_transaction_t t; | |
| memset(&t, 0, sizeof(t)); | |
| t.length = buffer_size * 8; // 送受信サイズは「ビット単位」で指定 | |
| t.rx_buffer = rx_buffer; | |
| // 送信側がCSをLOWにしてデータを送り、HIGHに戻すまで待機 (ブロッキング処理) | |
| esp_err_t ret = spi_slave_transmit(SPI3_HOST, &t, portMAX_DELAY); | |
| if (ret == ESP_OK) { | |
| // DMAバッファからCanvasのバッファへデータをコピー | |
| memcpy(canvas.getBuffer(), rx_buffer, buffer_size); | |
| if (isOutputEnabled) { | |
| canvas.pushSprite(0, 0); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #include <SPI.h> | |
| #include <M5Cardputer.h> | |
| #include <M5Unified.h> | |
| const uint16_t canvas_width_pix = 240, canvas_height_pix = 180; // 実際に使用する範囲 | |
| int buffer_size = canvas_width_pix * canvas_height_pix; | |
| // ========================================== | |
| // 描画・通信モードの選択 | |
| // 1: 白黒2値 | |
| // 2: グレースケール (8-bit) | |
| // 3: RGB332 カラー (8-bit) | |
| // ========================================== | |
| int current_mode = 2; // 初期モード: 2 (グレースケール) | |
| bool prev_opt_state = false; | |
| M5Canvas* canvasPtr = nullptr; | |
| // SPIピン設定 (Cardputer ADV拡張端子) | |
| const int SCK_PIN = 40; | |
| const int MOSI_PIN = 14; | |
| const int CS_PIN = 5; | |
| // 内蔵LCDとの競合を避けるため、空いている HSPI (SPI3) を明示的に指定してインスタンス化 | |
| SPIClass extSPI(HSPI); | |
| // FPS表示用 | |
| uint32_t lastTime = 0; | |
| float fps = 0; | |
| int frameCount = 0; | |
| // 円の状態を管理する構造体 | |
| struct BouncingCircle { | |
| float x; | |
| float y; | |
| float vx; | |
| float vy; | |
| int radius; | |
| }; | |
| BouncingCircle circle1 = { 0.0, 0.0, 3.5, 2.5, 20 }; | |
| BouncingCircle circle2 = { 0.0, 0.0, -2.5, 4.0, 15 }; | |
| // 色の定義 | |
| uint8_t girdColor1; | |
| uint8_t girdColor2; | |
| uint8_t circleColor1; | |
| uint8_t circleColor2; | |
| // --- 描画モードを切り替える関数 --- | |
| void setMode(int mode) { | |
| current_mode = mode; | |
| // 既存のインスタンスがあれば、メモリから完全に削除してクリーンにする | |
| if (canvasPtr != nullptr) { | |
| canvasPtr->deleteSprite(); | |
| delete canvasPtr; | |
| canvasPtr = nullptr; | |
| } | |
| // まっさらな状態でインスタンスを再生成 | |
| canvasPtr = new M5Canvas(&M5.Display); | |
| if (mode == 1) { | |
| canvasPtr->setColorDepth(1); | |
| buffer_size = (canvas_width_pix * canvas_height_pix) / 8; | |
| } else if (mode == 2) { | |
| canvasPtr->setColorDepth(m5gfx::color_depth_t::grayscale_8bit); | |
| buffer_size = canvas_width_pix * canvas_height_pix; | |
| } else if (mode == 3) { | |
| canvasPtr->setColorDepth(8); | |
| buffer_size = canvas_width_pix * canvas_height_pix; | |
| } | |
| // キャンバスを再生成 | |
| canvasPtr->createSprite(canvas_width_pix, canvas_height_pix); | |
| } | |
| void sendFrame(M5Canvas& canvasRef) { | |
| uint8_t* buffer = (uint8_t*)canvasRef.getBuffer(); | |
| digitalWrite(CS_PIN, LOW); | |
| // 独立したSPIバス(extSPI)を使用して通信 | |
| extSPI.beginTransaction(SPISettings(32000000, MSBFIRST, SPI_MODE0)); // 8000000くらいから様子見 | |
| uint8_t* p = buffer; | |
| int remaining = buffer_size; | |
| while (remaining > 0) { | |
| int chunk = (remaining > 4000) ? 4000 : remaining; | |
| extSPI.writeBytes(p, chunk); | |
| p += chunk; | |
| remaining -= chunk; | |
| } | |
| extSPI.endTransaction(); | |
| digitalWrite(CS_PIN, HIGH); | |
| } | |
| void setup() { | |
| auto cfg = M5.config(); | |
| M5Cardputer.begin(cfg, true); | |
| // SPI通信の初期化 | |
| pinMode(CS_PIN, OUTPUT); | |
| digitalWrite(CS_PIN, HIGH); | |
| // 独立したSPIバス(extSPI)のピンを初期化 | |
| extSPI.begin(SCK_PIN, -1, MOSI_PIN, -1); | |
| setMode(current_mode); | |
| if (canvasPtr == nullptr) return; | |
| M5Canvas& canvas = *canvasPtr; | |
| // --- ここから画面デモ用設定 --- | |
| randomSeed(millis()); | |
| circle1.x = random(circle1.radius, canvas.width() - circle1.radius); | |
| circle1.y = random(circle1.radius, canvas.height() - circle1.radius); | |
| circle2.x = random(circle2.radius, canvas.width() - circle2.radius); | |
| circle2.y = random(circle2.radius, canvas.height() - circle2.radius); | |
| } | |
| void loop() { | |
| M5Cardputer.update(); | |
| bool current_opt_state = M5Cardputer.Keyboard.keysState().opt; | |
| if (current_opt_state && !prev_opt_state) { | |
| int next_mode = current_mode + 1; | |
| if (next_mode > 3) next_mode = 1; | |
| setMode(next_mode); | |
| } | |
| prev_opt_state = current_opt_state; | |
| if (canvasPtr == nullptr) return; | |
| M5Canvas& canvas = *canvasPtr; | |
| if (current_mode == 1) { | |
| // --- 白黒2値モードの描画用設定 --- | |
| girdColor1 = 0x00; | |
| girdColor2 = 0xFF; | |
| circleColor1 = 0x00; | |
| circleColor2 = 0xFF; | |
| } else if (current_mode == 2) { | |
| // --- グレースケールモードの描画用設定 --- | |
| girdColor1 = 0x00; | |
| girdColor2 = 0xFF; | |
| circleColor1 = 0x40; | |
| circleColor2 = 0xC0; | |
| } else if (current_mode == 3) { | |
| // --- RGB332モードの描画用設定 --- | |
| girdColor1 = canvas.color332(0, 0, 0); | |
| girdColor2 = canvas.color332(0, 0, 255); | |
| circleColor1 = canvas.color332(255, 0, 0); | |
| circleColor2 = canvas.color332(0, 255, 0); | |
| } | |
| // FPSの計算 | |
| uint32_t now = millis(); | |
| uint32_t dt = now - lastTime; | |
| if (dt > 0) { | |
| fps = fps * 0.9f + (1000.0f / dt) * 0.1f; | |
| } | |
| lastTime = now; | |
| drawCheckerboard(canvas, 12, girdColor1, girdColor2); | |
| updateAndDrawCircle(canvas, circle1, circleColor1); | |
| updateAndDrawCircle(canvas, circle2, circleColor2); | |
| // FPSの表示 | |
| canvas.setTextColor(TFT_WHITE, TFT_BLACK); | |
| canvas.setTextSize(2); | |
| canvas.setCursor(12, 12); | |
| canvas.printf("FPS: %.1f", fps); | |
| canvas.pushSprite(0, 0); | |
| sendFrame(canvas); | |
| yield(); | |
| } | |
| void drawCheckerboard(M5Canvas& canvasRef, int gridSize, uint8_t color1, uint8_t color2) { | |
| int width = canvasRef.width(); | |
| int height = canvasRef.height(); | |
| // 画面全体をマス目ごとにループして描画 | |
| for (int y = 0; y < height; y += gridSize) { | |
| for (int x = 0; x < width; x += gridSize) { | |
| // 列(x)と行(y)のインデックスの和が偶数か奇数かで色を切り替える | |
| bool isColor1 = ((x / gridSize) + (y / gridSize)) % 2 == 0; | |
| uint8_t color = isColor1 ? color1 : color2; | |
| // 矩形を描画(Canvasの範囲外は自動でクリッピングされます) | |
| canvasRef.fillRect(x, y, gridSize, gridSize, color); | |
| } | |
| } | |
| } | |
| float getFluctuatedSpeed() { | |
| return random(20, 46) / 10.0; | |
| } | |
| void updateAndDrawCircle(M5Canvas& canvasRef, BouncingCircle& circle, uint8_t color) { | |
| // 位置の更新 | |
| circle.x += circle.vx; | |
| circle.y += circle.vy; | |
| // X軸(左右の壁)のバウンド判定 | |
| if (circle.x - circle.radius <= 0) { | |
| circle.x = circle.radius; | |
| circle.vx = getFluctuatedSpeed(); // 確実に右向き(プラス)に反射 | |
| // Y方向の速度も、現在の進行方向(プラスかマイナスか)を維持したまま揺らがせる | |
| circle.vy = (circle.vy > 0) ? getFluctuatedSpeed() : -getFluctuatedSpeed(); | |
| } else if (circle.x + circle.radius >= canvasRef.width()) { | |
| circle.x = canvasRef.width() - circle.radius; | |
| circle.vx = -getFluctuatedSpeed(); // 確実に左向き(マイナス)に反射 | |
| circle.vy = (circle.vy > 0) ? getFluctuatedSpeed() : -getFluctuatedSpeed(); | |
| } | |
| // Y軸(上下の壁)のバウンド判定 | |
| if (circle.y - circle.radius <= 0) { | |
| circle.y = circle.radius; | |
| circle.vy = getFluctuatedSpeed(); // 確実に下向き(プラス)に反射 | |
| // X方向の速度も、現在の進行方向を維持したまま揺らがせる | |
| circle.vx = (circle.vx > 0) ? getFluctuatedSpeed() : -getFluctuatedSpeed(); | |
| } else if (circle.y + circle.radius >= canvasRef.height()) { | |
| circle.y = canvasRef.height() - circle.radius; | |
| circle.vy = -getFluctuatedSpeed(); // 確実に上向き(マイナス)に反射 | |
| circle.vx = (circle.vx > 0) ? getFluctuatedSpeed() : -getFluctuatedSpeed(); | |
| } | |
| // キャンバスへ円を描画 | |
| canvasRef.fillCircle((int)circle.x, (int)circle.y, circle.radius, color); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment