工業製造
産業用モノのインターネット | 工業材料 | 機器のメンテナンスと修理 | 産業プログラミング |
home  MfgRobots >> 工業製造 >  >> Manufacturing Technology >> 製造プロセス

限界突破: Arduino NanoでSSD1106 OLEDに30FPS動画表示 – SPIオーバークロック&ディザリング

必要な部品と材料

Arduino Nano R3 (ATmega328P, 16MHz) × 1
0.96インチ OLEDディスプレイ (SSD1106, 128×64, SPI接続) × 1

使用ソフトウェア・ライブラリ

プロジェクト概要

SSD1106 OLED (128×64ピクセル、白黒2値) は通常、テキストや簡易アニメーション表示用途ですが、SPIバスを 2MHz (標準は 4MHz 以下だが、AVRのSPIクロック分周設定で最適化) にオーバークロックし、PC側で OpenCV を用いて動画をフレーム分解→リサイズ→フロイド・スタインバーグ ディザリング (誤差拡散法) で疑似グレースケール化→シリアル (UART 1Mbps) で高速転送することで、**約 30 FPS の動画再生** を実現したプロジェクトです。

技術的チャレンジと解決策

課題解決策
SPI帯域不足 (128×64=8192ピクセル=1024バイト/フレーム、30FPS=30KB/s、SPI 4MHz=500KB/s 理論上余裕だがオーバーヘッド大) SPIクロック最適化、コマンド/データ切替最小化、フレームバッファー一括転送
UART帯域 (PC→Arduino) がボトルネック UART 1Mbps (115200の約8.7倍) 設定、バイナリ転送、フレーム差分圧縮検討
白黒2値表示しかできない フロイド・スタインバーグ ディザリングで疑似階調表現
Arduino RAM不足 (2KBしかない) フレームバッファー持たず、受信即表示 (ストリーミング方式)
電源ノイズで通信エラー デカップリングコンデンサ、短い配線、安定化電源

システム構成

[PC: Python + OpenCV] 
  → 動画ファイル/カメラ読込
  → リサイズ (128x64)
  → グレースケール変換
  → フロイド・スタインバーグ ディザリング (1bit/pixel)
  → バイナリフレームデータ (1024バイト) 生成
  → UART (1Mbps) 送信
       ↓ USB-CDC / FTDI
[Arduino Nano (ATmega328P)]
  → UART受信 (割り込み/ポーリング)
  → SPI (2-4MHz) でSSD1106へ一括転送
  → 表示

配線 (Arduino Nano → SSD1106 SPI)

OLEDピン機能Arduino Nanoピン
VCC3.3V/5V (モジュール仕様確認)3.3V or 5V
GNDGNDGND
D0 (SCLK)SPI ClockD13 (SCK)
D1 (MOSI/SDA)SPI DataD11 (MOSI)
RES (RST)ResetD8 (任意GPIO)
DC (A0)Data/CommandD9 (任意GPIO)
CSChip SelectD10 (SS) または GND (常時選択)

PC側 Python コード概要 (OpenCV + pyserial)


import cv2
import serial
import numpy as np
import time

ser = serial.Serial('COM3', 1000000, timeout=0)  # 1Mbps
cap = cv2.VideoCapture('video.mp4')  # or 0 for webcam

def floyd_steinberg_dither(img):
    # img: uint8 0-255, shape (64, 128)
    h, w = img.shape
    out = np.zeros((h, w), np.uint8)
    # 作業用コピー (floatで誤差蓄積)
    work = img.astype(np.float32)
    for y in range(h):
        for x in range(w):
            old = work[y, x]
            new = 255 if old > 127 else 0
            out[y, x] = new
            err = old - new
            # 誤差拡散
            if x + 1 < w: work[y, x+1]     += err * 7/16
            if y + 1 < h:
                if x > 0:   work[y+1, x-1] += err * 3/16
                work[y+1, x]   += err * 5/16
                if x+1 < w: work[y+1, x+1] += err * 1/16
    return out

def frame_to_bytes(dithered):
    # 1bit/pixel → 8ピクセル=1バイト (MSB first, page addressing)
    # SSD1106はページアドレッシング (8行=1ページ × 8ページ)
    bytes_out = bytearray(1024)
    idx = 0
    for page in range(8):
        for col in range(128):
            b = 0
            for bit in range(8):
                y = page * 8 + bit
                if dithered[y, col] > 127:
                    b |= (1 << (7 - bit))  # MSB first
            bytes_out[idx] = b
            idx += 1
    return bytes_out

while True:
    ret, frame = cap.read()
    if not ret: break
    
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    small = cv2.resize(gray, (128, 64), interpolation=cv2.INTER_AREA)
    dithered = floyd_steinberg_dither(small)
    data = frame_to_bytes(dithered)
    
    ser.write(data)
    # 30FPS = 33.3ms/frame だが、送信時間考慮して調整
    time.sleep(0.033)

ser.close()
cap.release()

Arduino側スケッチ概要


#include <SPI.h>

#define OLED_DC 9
#define OLED_RST 8
#define OLED_CS 10

const int WIDTH = 128;
const int HEIGHT = 64;
const int PAGES = 8;
const int FRAME_SIZE = 1024; // 128 * 64 / 8

uint8_t frameBuf[FRAME_SIZE];
volatile uint16_t bufIdx = 0;
volatile bool frameReady = false;

void setup() {
  pinMode(OLED_DC, OUTPUT);
  pinMode(OLED_RST, OUTPUT);
  pinMode(OLED_CS, OUTPUT);
  digitalWrite(OLED_CS, HIGH);
  
  // OLEDリセット
  digitalWrite(OLED_RST, LOW); delay(10); digitalWrite(OLED_RST, HIGH);
  
  // SPI高速化: クロック分周2 (8MHz/2=4MHz) または4 (2MHz)
  SPI.begin();
  SPI.setClockDivider(SPI_CLOCK_DIV2); // 4MHz
  SPI.setBitOrder(MSBFIRST);
  SPI.setDataMode(SPI_MODE0);
  
  // SSD1106初期化コマンドシーケンス...
  oledInit();
  
  Serial.begin(1000000); // 1Mbps UART
}

void loop() {
  // UART受信 (割り込み駆動推奨)
  while (Serial.available()) {
    frameBuf[bufIdx++] = Serial.read();
    if (bufIdx >= FRAME_SIZE) {
      bufIdx = 0;
      frameReady = true;
    }
  }
  
  if (frameReady) {
    frameReady = false;
    sendFrameToOled(frameBuf);
  }
}

void sendFrameToOled(uint8_t *buf) {
  // ページアドレッシングモードで全ページ書き込み
  for (uint8_t page = 0; page < PAGES; page++) {
    oledCommand(0xB0 | page);      // ページアドレス設定
    oledCommand(0x00);             // 下位カラムアドレス
    oledCommand(0x10);             // 上位カラムアドレス
    
    digitalWrite(OLED_DC, HIGH);   // データモード
    digitalWrite(OLED_CS, LOW);
    // 1ページ = 128バイト 一括SPI転送
    for (uint8_t col = 0; col < WIDTH; col++) {
      SPI.transfer(buf[page * WIDTH + col]);
    }
    digitalWrite(OLED_CS, HIGH);
  }
}

void oledCommand(uint8_t cmd) {
  digitalWrite(OLED_DC, LOW);
  digitalWrite(OLED_CS, LOW);
  SPI.transfer(cmd);
  digitalWrite(OLED_CS, HIGH);
}

void oledInit() {
  // SSD1106標準初期化シーケンス (データシート参照)
  // 省略: ディスプレイOFF, クロック分周, マルチプレックス比, オフセット, 
  // スタートライン, チャージポンプ, メモリモード, セグメントリマップ, 
  // COMスキャン方向, コムピン設定, コントラスト, プリチャージ, VCOMH, 
  // 全点灯OFF, 正常表示, ディスプレイON
}

ディザリング効果の比較

パフォーマンス実測値 (目安)

項目値
フレームレート約 25-30 FPS (動画複雑度・PC性能依存)
UARTボーレート1,000,000 bps (1 Mbps)
SPIクロック4 MHz (SPI_CLOCK_DIV2 @ 16MHz AVR)
フレームデータサイズ1024 バイト (128×64 / 8)
理論転送時間 (UART)約 8.2 ms (1024×8bit / 1Mbps + オーバーヘッド)
理論転送時間 (SPI)約 2.0 ms (1024×8bit / 4MHz)
Arduino CPU使用率約 30-40% (受信+表示のみ)

拡張アイデア

製造プロセス

  1. 液晶ディスプレイ(LCD):技術、製造、将来展望
  2. Primal Display:Raspberry Pi LCD温度モニターの構築
  3. Kuman 3.5インチTFTディスプレイをRetroPieにインストールするステップバイステップガイド
  4. Arduino UNOとOLEDディスプレイでクラシックPongゲームを自作:ステップバイステップチュートリアル
  5. ゲーマー向け自動操作デバイス──Arduinoとサーボでコントローラのボタンを自動連打
  6. ATmega328P自作スロットマシン「エイリアン侵略」 - デュアルMCU、I2C連携、確率設計解説付き
  7. Arduino Nanoで2.8インチTFT SPI液晶にJPEG写真とRGB565動画を表示する
  8. Arduino Nanoと128x64 OLEDで実行するセルラーオートマトン「ライフゲーム」
  9. 新HDMI-APIX3コンバーターが自動車ビデオ/ADASテストを強化
  10. C#メソッドオーバーロード完全ガイド:概念、例、ベストプラクティス
  11. Verilogタスクの理解:関数との違い、構文、使用法