Skip to content

Instantly share code, notes, and snippets.

@Yuikawa-Akira
Last active June 25, 2026 13:38
Show Gist options
  • Select an option

  • Save Yuikawa-Akira/1e2ff7afe4495e3ad83ea3d1c82279fd to your computer and use it in GitHub Desktop.

Select an option

Save Yuikawa-Akira/1e2ff7afe4495e3ad83ea3d1c82279fd to your computer and use it in GitHub Desktop.
SPI2NTSC
#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);
}
}
}
#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