NanoPi NEO3 Plus で SPI なモニタを使う

やりたいこと

NanoPi NEO では映像を扱うつもりはなかったけど、やってみたらギリギリアウトw
けど、4ドルしか価格が違わないのに性能がおそらく3~4倍はあろう NEO3 Plus なら?

こんなことが割と余裕。(ただし、SPI の帯域がいっぱいいっぱい過ぎてフレームレートは出せないw)

用意したもの

NanoPi NEO3 Plus
3.2 インチ LCD:ILI9341 SPI接続 320×240

Amazon.co.jp: DIANN 3.2" ILI9341 SPI TFT LCD Display Touch Panel 320×240 TFT LCD Touch Screen Shield 5V/3.3V STM32 Display Module SPI Serial with Touch Pen : 産業・研究開発用品
Amazon.co.jp: DIANN 3.2" ILI9341 SPI TFT LCD Display Touch Panel 320×240 TFT LCD Touch Screen Shield 5V/3.3V STM32 Displ…

注意事項:2026年7月12日時点

Friendly Elec の NanoPi NEO3 Plus の Wiki に間違いがある。
ケアレスミスのようなものなのですぐ直るとは思うけど。

NanoPi NEO3 Plus – FriendlyELEC WiKi

次の画像は Wiki からの抜粋で、Pin#22 と Pin#24 に注目。

Pin#24、GPIO_4_B3_SPI4_CSN0 と書いてあるけど、GPIO1_C1_SPI1_CSN0 の間違え。

最初、「無印NEO から CS だけ変わったのか。SPI4?…ま、その上にSPI1_CSN1があるから、これを使えばいいのか」と接続してみたけれど、全く動かないw

踏襲されたピンアサインが早々変わることはないはずなのに、「SPI1」という文字に釣られるとハマるかも。

下記に回路図があるので念のため見てみた。

https://wiki.friendlyelec.com/wiki/images/a/ae/NanoPi_NEO3_Plus_2601_SCH.pdf

GPIO の項目。

うん、やっぱり GPIO4 でもなければ B3 でもなく SPI4 でもない。

正しくは GPIO1_C1_SPI1_CSN0

なので、CS としてつなげるべきは Pin#22 ではなく Pin#24 で、無印 NEO と全く同じで良い。

そして、結果的に Pin#24 で動いた。

Friendly Elec 公式 Ubuntu ならやることほとんどない

すでに最初から SPI が有効になってる。

$ sudo ls -al /dev/spi*
crw------- 1 root root 153, 0 Jul 12 18:42 /dev/spidev1.0

フレームバッファも存在してはいるけれど、これ、たぶんHDMI用。

$ ls -al /dev/fb*
crw-rw---- 1 root video 29, 0 Jul 12 18:43 /dev/fb0

$ sudo fbset -i -fb /dev/fb0
 
mode "1920x1080"
    geometry 1920 1080 1920 2560 32
    timings 0 0 0 0 0 0 0
    accel true
    rgba 8/16,8/8,8/0,0/0
endmode
 
Frame buffer device information:
    Name        : rockchipdrmfb
    Address     : 0
    Size        : 19660800
    Type        : PACKED PIXELS
    Visual      : TRUECOLOR
    XPanStep    : 1
    YPanStep    : 1
    YWrapStep   : 0
    LineLength  : 7680
    Accelerator : No

fbset はでおフォルトでは入ってないので、フレームバッファの情報を見たい場合は下記のようにインストール。
…でもそんなに使わないからスルーでもいいかな。

$ sudo apt install fbset

もし、ILI9341のドライバ読み込んでフレームバッファ使えれば楽かもしれない。
けど、上記 Wiki の誤植にハメられてちょっと時間を無駄に使ってしまったので、やったのは SPI 直叩き。

OPi.GPIO を使う

以前、下記で書いてるので基本はそっちで。

NanoPi NEO の GPIO が使えるようになるまで
NanoPi NEO って?ちょっと今更感はある。(2025年11月)性能的にはAIの推論もほぼほぼできないし、WiFi も Bluetooth もない。だが、そこがいい。小さいのにGPIO豊富だし、UARTも複数持ってるので。そして、Ar…

ただ、設定ファイルに書き足す内容は全く違ってしまうので、その部分だけ以下に差し替え。

# User defined, initialized as empty
CUSTOM: {
    7:    147,   # GPIO4_C3
    8:    134,   # GPIO4_A6_UART1_TX_M0
    10:   135,   # GPIO4_A7_UART1_RX_M0
    11:   133,   # GPIO4_A5
    12:   132,   # GPIO4_A4
    13:   140,   # GPIO4_B4
    15:   128,   # GPIO4_A0
    16:   44,    # GPIO1_B4
    18:   45,    # GPIO1_B5
    19:   47,    # GPIO1_B7_SPI1_MOSI
    21:   48,    # GPIO1_C0_SPI1_MISO
    22:   50,    # GPIO1_C2_SPI1_CSN1
    23:   46,    # GPIO1_B6_SPI1_CLK
    24:   139,   # GPIO4_B3_SPI4_CSN0
    26:   150    # GPIO4_C6
}

ということで、動かすコード

その前に、一応配線。

19:MOSI  21:MISO  23:CLK  24:CS  1:VCC  は完全に固定。

GND は適当。
DC と RESET は任意の GPIO で OK。ここではそれぞれ 16 と 18 を使用した。

あと、LEDに5V供給してあげるとバックライトが点く。(バックライトないと真っ暗)

合計9本とちょっと多いかな…

こんな感じでつながっている状態で下記コードを実行すると絵が出る。

import OPi.GPIO as GPIO
import spidev, time, subprocess
 
DC = 16
RST = 18
 
GPIO.setmode(GPIO.CUSTOM)
GPIO.setup(DC, GPIO.OUT)
GPIO.setup(RST, GPIO.OUT)
 
spi = spidev.SpiDev()
spi.open(1, 0)
spi.mode = 0
spi.max_speed_hz = int(32 * 1000 * 1000)
 
def cmd(c):
    GPIO.output(DC, 0)
    spi.writebytes([c])
 
def cmd_with_data(c, data_list):
    GPIO.output(DC, 0)
    spi.writebytes([c])
    GPIO.output(DC, 1)
    spi.writebytes(data_list)
 
# LCD初期化
def init_lcd():
    GPIO.output(RST, 0)
    time.sleep(0.05)
    GPIO.output(RST, 1)
    time.sleep(0.05)
    cmd(0x11)
    time.sleep(0.12)
 
    # 画面転送方向を横向き(320x240)に変更
    cmd_with_data(0x36, [0x28])
    time.sleep(0.01)
    cmd_with_data(0x2A, [0x00, 0x00, 0x01, 0x3F])
    cmd_with_data(0x2B, [0x00, 0x00, 0x00, 0xEF])
    time.sleep(0.01)
    cmd(0x29)
    time.sleep(0.02)
 
 
init_lcd()
 
# --- 配信・画面設定 ---
RTSP_URL = "rtsp://[ID]:[PW]@[Camera IP]/"
WIDTH = 320
HEIGHT = 240
FRAME_RATE = 10
FRAME_SIZE = WIDTH * HEIGHT * 3
 
# ffmpegのコマンド作成
ffmpeg_cmd = [
    'ffmpeg',
    '-fflags', 'nobuffer',
    '-rtsp_transport', 'tcp',
    '-r', f'{FRAME_RATE}',
    '-i', RTSP_URL,
    '-vf', f'scale={WIDTH}:{HEIGHT}:flags=neighbor',
    '-f', 'rawvideo',
    '-pix_fmt', 'rgb24',
    '-an', '-sn',
    '-loglevel', 'quiet',
    '-'
]

proc = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, bufsize=FRAME_SIZE)
 
 
try:
    while True:
        t_start = time.time()
 
        frame_data = proc.stdout.read(FRAME_SIZE)
        if len(frame_data) < FRAME_SIZE:
            break
 
        # LCDへ書き込み
        cmd(0x2C)
        GPIO.output(DC, 1)
        spi.writebytes2(frame_data)
 
        # ウェイト
        t_elasped = time.time() - t_start
        t_sleep = 1.0 / FRAME_RATE - t_elasped
 
        if t_sleep > 0:
            time.sleep(t_sleep)
 
 
except KeyboardInterrupt:
    print("\n終了")
finally:
    if proc.stdout:
        proc.stdout.close()
    proc.terminate()
    proc.wait()
    spi.close()
    GPIO.cleanup()

ついでに、ローカルの動画ファイルを再生するなら下のような感じ。

import OPi.GPIO as GPIO
import spidev, time, subprocess
 
DC = 16
RST = 18
 
GPIO.setmode(GPIO.CUSTOM)
GPIO.setup(DC, GPIO.OUT)
GPIO.setup(RST, GPIO.OUT)
 
spi = spidev.SpiDev()
spi.open(1, 0)
spi.mode = 0
spi.max_speed_hz = int(32 * 1000 * 1000)
 
def cmd(c):
    GPIO.output(DC, 0)
    spi.writebytes([c])
 
def cmd_with_data(c, data_list):
    GPIO.output(DC, 0)
    spi.writebytes([c])
    GPIO.output(DC, 1)
    spi.writebytes(data_list)
 
# LCD初期化
def init_lcd():
    GPIO.output(RST, 0)
    time.sleep(0.05)
    GPIO.output(RST, 1)
    time.sleep(0.05)
    cmd(0x11)
    time.sleep(0.12)
 
    # 画面転送方向を横向き(320x240)に変更
    cmd_with_data(0x36, [0x28])
    time.sleep(0.01)
    cmd_with_data(0x2A, [0x00, 0x00, 0x01, 0x3F])
    cmd_with_data(0x2B, [0x00, 0x00, 0x00, 0xEF])
    time.sleep(0.01)
    cmd(0x29)
    time.sleep(0.02)
 
 
init_lcd()
 
# --- 配信・画面設定 ---
FILE_PATH = "violet.mp4"
WIDTH = 320
HEIGHT = 240
FRAME_RATE = 10
FRAME_SIZE = WIDTH * HEIGHT * 3
 
# ffmpegのコマンド作成 
ffmpeg_cmd = [
    'ffmpeg',
    '-re',
    '-stream_loop', '-1',
    '-r',f'{FRAME_RATE}',
    '-i', FILE_PATH,
    '-vf', 'scale=320:180:flags=neighbor, pad=320:240:-1:-1:color=black',
    '-f', 'rawvideo',
    '-pix_fmt', 'rgb24',
    '-loglevel', 'quiet',
    '-'
]

proc = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, bufsize=FRAME_SIZE)
 
 
try:
    while True:
        frame_data = proc.stdout.read(FRAME_SIZE)
        if len(frame_data) < FRAME_SIZE:
            break
 
        # LCDへ書き込み
        cmd(0x2C)
        GPIO.output(DC, 1)
        spi.writebytes2(frame_data)
 
 
except KeyboardInterrupt:
    print("\n終了")
finally:
    if proc.stdout:
        proc.stdout.close()
    proc.terminate()
    proc.wait()
    spi.close()
    GPIO.cleanup()

SPI とはいえ 30fps はなかなか厳しい。

そして、個人的にはカメラの RTSP ストリームを受信したいだけなので、ぶっちゃけ 5fps でも良いくらい。
動画を再生しているのは、まぁ、デモ用?w

ということで、NEO3 Plus、いいね。

この記事にコメントしてみる