以前、DigisparkでLED256個のイルミネーションを作るで16x16のマトリクスLEDを使った工作をして、
もっと大きいもので何か作ってみたくなり、P2.5,128x64のパネルを買いました。P2.5というのはLEDの間隔が2.5mmということです。
大きいパネルはHUB75というインターフェースが多いようです。
HUB75のLEDパネルはON/OFFしかできないので、階調表示をするためには出力イネーブルのパルス幅を変えて同じ行を複数回点灯させることを繰り返します。
例えば、ピクセルデータのLSBの点灯時間を1μSとし、階調を8ビットとすると、LSBから順にパルス幅を
1μS、2μS、4μS、8μS、16μS、32μS、64μS、128μS
とします。合計255μSです。
アドレスが5ビットのパネルだと、行ごとにこれを32回繰り返すので、1周で8.16mS、リフレッシュレートでいうと122Hzになります。
リフレッシュレートは60Hzもあれば十分なのでは?と思われるかもしれませんが、LEDだとけっこうちらつきが目立つんです。

マイコンはRaspberry Pi Picoを使います。
SDKにはHUB75のサンプル(pico-examples/pio/hub75)があります。
まずpico-examples/pio/hub75/hub75.pioを見てみます。
.wrap_target
public shift0:
    pull             side 0 ; gets patched to `out null, n` if n nonzero (otherwise the PULL is required for fencing)
    in osr, 1        side 0 ; shuffle shuffle shuffle
    out null, 8      side 0
    in osr, 1        side 0
    out null, 8      side 0
    in osr, 1        side 0
    out null, 32     side 0 ; Discard remainder of OSR contents
public shift1:
    pull             side 0 ; gets patched to out null, n if n is nonzero (otherwise PULL required)
    in osr, 1        side 1 ; Note this posedge clocks in the data from the previous iteration
    out null, 8      side 1
    in osr, 1        side 1
    out null, 8      side 1
    in osr, 1        side 1
    out null, 32     side 1
    in null, 26      side 1 ; Note we are just doing this little manoeuvre here to get GPIOs in the order
    mov pins, ::isr  side 1 ; R0, G0, B0, R1, G1, B1. Can go 1 cycle faster if reversed
.wrap
パネルに送るデータを作るためにPIOでRGB888のピクセルデータから1ビットずつ拾っており、16クロック/ループかかります。
横128ピクセルのパネルだと2048クロック/行になるので、システムクロックを128MHzとすると16μSになります。
イネーブルのパルス幅がこれを下回る場合、データ転送待ちで消灯している時間が増えます。
従って、データ転送は速いに越したことはありません。
データを作る作業をCPUが行い、PIOは送出だけにすると
.wrap_target
public shift:
        pull             side 0 ; gets patched to `out null, n` if n nonzero (otherwise the PULL is required for fencing)
        out pins, 6      side 0
        out null, 32 [1] side 1 ; Discard remainder of OSR contents
.wrap
のように、4クロック/ループ(32MHz)まで高速化できます。
[1]で1クロック待っていますが、待たないとテストに使ったパネルでは正常に表示されませんでした。
CPUの計算量が増えますが、もう1つのコアを使えば問題ありません。

次にpico-examples/pio/hub75/hub75.cを見ます。
    for (int rowsel = 0; rowsel < (1 << ROWSEL_N_PINS); ++rowsel) {
        for (int x = 0; x < WIDTH; ++x) {
            gc_row[0][x] = gamma_correct_565_888(img[rowsel * WIDTH + x]);
            gc_row[1][x] = gamma_correct_565_888(img[((1u << ROWSEL_N_PINS) + rowsel) * WIDTH + x]);
        }
        for (int bit = 0; bit < 8; ++bit) {
            hub75_data_rgb888_set_shift(pio, sm_data, data_prog_offs, bit);
            for (int x = 0; x < WIDTH; ++x) {
                pio_sm_put_blocking(pio, sm_data, gc_row[0][x]);
                pio_sm_put_blocking(pio, sm_data, gc_row[1][x]);
            }
            // Dummy pixel per lane
            pio_sm_put_blocking(pio, sm_data, 0);
            pio_sm_put_blocking(pio, sm_data, 0);
            // SM is finished when it stalls on empty TX FIFO
            hub75_wait_tx_stall(pio, sm_data);
            // Also check that previous OEn pulse is finished, else things can get out of sequence
            hub75_wait_tx_stall(pio, sm_row);

            // Latch row data, pulse output enable for new row.
            pio_sm_put_blocking(pio, sm_row, rowsel | (100u * (1u << bit) << 5));
        }
    }
表示ループの中でガンマ補正もしています。
MSBのイネーブル期間が終わってからガンマ補正が終わるまではパネルは消灯状態なので、最大輝度が低くなります。
これを、以下のように変更しました。
    for (int y = 0; y < HEIGHT / 2; y++)
        for (int bit = 0; bit < pwmbits; bit++) {
            uint n = (loop >> pwmbits - 1 - bit) + 1 >> 1;
            if (!n) continue;
            uint b = bit + PWMMAX - pwmbits;
            hub75_rgb_set_shift(pio, sm_rgb, 6 * (b % 5));
            dma_channel_transfer_from_buffer_now(dma_ch, rgb[sw][b / 5][y], WIDTH);
            dma_channel_wait_for_finish_blocking(dma_ch);
            wait_tx_stall(pio, sm_rgb);
            wait_tx_stall(pio, sm_row);
            pio_sm_put_blocking(pio, sm_row, y | n - 1 << 5);
        }
ガンマ補正はピクセルデータから1ビットずつ拾う処理と一体化して別コアに追い出し、
さらにPIOへの転送はDMAを使うことで速度を稼ぎます。
これで消灯時間を極力減らし、最大輝度を向上させることができました。

ムービープレイヤー

今回は、上記で改良した表示ルーチンに、以下のSDカード対応のFatFSを組み合わせてムービープレイヤーを作ります。

https://github.com/elehobica/pico_fatfs_test

音声はI2Sで入力してスピーカーを繋ぐだけのお手軽基板を使いました。

MAX98357A搭載 I2Sオーディオモジュール
https://www.switch-science.com/products/4038

また、Raspberry Pi Picoにディスプレイとキーボードを繋ぐのUSB部分を改造してマウスを使えるようにし、以下の項目をマウスで設定できるようにしました。
ファイル選択
ボリューム     0-15
ガンマ       1.0-3.0  0.1刻み
階調ビット数    1-15
リフレッシュレート 30-120   10刻み

回路図です。



マイコン基板は写真のようにLEDパネルに直接差し込めるように作りました。
右側に出ている赤い線のピンは、上から出ている開発用のUSBを外したときに電源を供給する端子です。



動画はffmpegで変換します。元の動画をsrc.mp4とすると、
ffmpeg -i src.mp4 -vf "scale=128:64:force_original_aspect_ratio=increase,crop=128:64" \
-r 30 -vcodec rawvideo -pix_fmt rgba -ac 1 -ar 32000 -acodec pcm_s16le dst.avi
でdst.aviに変換します。
SDカードはフォーマットしたあと/MOVIEディレクトリを作り、できたAVIファイルをその中にコピーします。
以下のファイルを変換し、再生してみました。

【著作権フリー動画素材】世界中のタイムラプスを繋げてみた【TimeLapse】
https://www.youtube.com/watch?v=aRNqI-xNDcA



iPadで撮影したので、リフレッシュレートが高くてもちらついて見えますが、肉眼では分かりません。
また、もっと明るく鮮やかに見えます。

https://github.com/kwhr0/Pico-movie-player-for-rgb-led-matrix

このページの情報は自由に使用できますが、いっさいの保証もサポートもありません。
何らかの損害を受けたとしても、免責とします。