【格安】Raspberry Pi用3.5インチ液晶の実力を徹底検証

【PR】この記事には広告が含まれています。

【格安】Raspberry Pi用3.5インチ液晶の実力を徹底検証

Raspberry Piで使える、手頃な液晶ディスプレイを探していませんか?

今回、3.5インチのタッチ対応液晶ディスプレイモジュールを2,850円(税込・送料無料)で入手しました。

Raspberry Pi用ディスプレイモジュール(3.5 インチ)

販売サイトには、Raspberry Pi 5への対応について「動作確認はしていない」との注意書きがありますが、Raspberry Pi 5でも動作し、画面表示やタッチ操作ができました。

この記事では、これまで数え切れないほどのRaspberry Pi用液晶ディスプレイを使ってきた経験をもとに、使い勝手や表示性能を検証し、安価な液晶がどこまで実用的なのかを見ていきます。

今回購入した3.5インチ液晶ディスプレイ

購入したのは、Raspberry PiのGPIO端子に直接取り付けて使用する、3.5インチのタッチ対応液晶ディスプレイです。液晶のサイズはRaspberry Pi 5の基板とほぼ同じで、取り付けた際の収まりがよく、コンパクトにまとまるのも魅力です。解像度は480×320ピクセルで、液晶の制御にはILI9486というICが使われています。

液晶モジュールの裏面

主な仕様は以下のとおりです。

画面サイズ3.5インチ
解像度480×320ピクセル
液晶方式TFT
液晶ドライバーICILI9486
タッチ機能あり
入力電圧3.3V / 5V

ディスプレイ本体に加えて、タッチ操作用のペンが付属しています。

液晶はRaspberry PiのGPIO端子に直接取り付けるタイプです。HDMIケーブルや別途USBケーブルを接続する必要がなく、すっきりとした構成にできます。一方、GPIO端子を使用するため、ほかの電子部品を接続する場合は端子の競合に注意が必要です。

Raspberry Pi 5への取り付けと初期設定

この液晶ディスプレイは、Raspberry PiのGPIO端子に直接差し込むだけで取り付けられます。GPIO端子の一部に被せるタイプなので、着脱もスムーズです。 今回はRaspberry Pi 5で検証をしました。

画面表示によるCPUの発熱が大きいため、Raspberry Pi公式アクティブクーラーの取り付けをおすすめします。

アクティブクーラー(ヒートシンク + 冷却ファン)
そぞら
そぞら

CPU温度はクーラー取り付け前に83℃まで上昇しましたが、取り付け後は57℃まで低下しました。

液晶モジュールとアクティブクーラーは干渉せず、両方を同時に取り付けられます。

Raspberry Piに液晶を取り付けたら、デスクトップ画面を表示するための設定を行います。今回は、Raspberry Pi OS(64-bit / Trixie)デスクトップ版を使用します。

初期設定がまだの方は、「Raspberry PiのOSインストールから初期設定までの手順」を参考に、OSのインストールと初期設定、Wi-Fi接続を済ませておきましょう。

液晶にデスクトップ画面を映してみる

このような小型液晶には、Raspberry Piのデスクトップ画面そのものを映す使い方と、デスクトップ画面とは別に、時計やセンサーの測定値などを表示する使い方があります。

今回は前者の方法を採用します。Pythonでデスクトップ画面を取り込み、小型液晶に表示することで、通常のモニターと同じようにアプリの起動やタッチ操作ができるようにします。

そぞら
そぞら

ただし、液晶を取り付けるだけではデスクトップ画面を表示できません。

ChatGPTと試行錯誤を重ね、表示が遅い、タッチ位置がずれるといった問題を解決し、ようやく安定した動作を実現できました。作成したプログラムはGitHubで公開しており、用意したインストーラーのコマンドを実行するだけで、必要な設定をまとめて行えます。

設定には、別のパソコンからRaspberry Piを遠隔操作できるSSHやVNCを利用すると便利です。

  • SSHの場合: パソコンからRaspberry PiにSSH接続すると、そのままコマンドを入力できます。
  • VNCの場合: パソコンに表示されたRaspberry Piのデスクトップ画面から、ターミナルを開きます。

ターミナルとは、コマンドと呼ばれる文字の命令を入力して、Raspberry Piを操作するためのアプリです。

準備ができたら、以下のコマンドを入力し、Enterキーを押して実行してください。

① プログラムをダウンロードする

git clone https://github.com/sozoraemon/raspberry-pi-spi-lcd-mirror.git

GitHubで公開しているプログラムをRaspberry Piにダウンロードします。

② ダウンロードしたフォルダーに移動する

cd raspberry-pi-spi-lcd-mirror

ダウンロードした「raspberry-pi-spi-lcd-mirror」フォルダーに移動し、その中にあるインストール用のプログラムを実行できる状態にします。

③ インストールを実行する

sudo bash install.sh

液晶の表示やタッチ操作に必要なプログラムと設定を自動でインストールします。sudoは管理者モードで実行する命令であり、安全確認としてパスワード入力が求められます。Raspberry Piにログインするときのパスワードを入力後にEnterキーを押してください。

④ Raspberry Piを再起動する

sudo reboot

再起動後、デスクトップ画面が液晶に表示され、タッチ操作も利用できるようになります。しかし初期設定の解像度では文字やアイコンが小さく表示されるため、操作しにくい状態です。そこで、次に画面の解像度を調整して、見やすく使いやすい表示にしていきます。

画面の解像度を変更する

この液晶ディスプレイの解像度は480×320ピクセルですが、デスクトップ画面も同じ解像度にすると、表示できる範囲が狭くなり、かえって操作しにくくなります。

そこで、表示範囲の広さとアイコンのタッチしやすさのバランスがよい、960×640ピクセルへの変更をおすすめします。SSHまたはVNCから、以下のコマンドを実行してください。

設定用のフォルダーを作成する

mkdir -p ~/.config/labwc

Raspberry Piのデスクトップ環境「labwc」の設定を保存するフォルダーを作成します。

② 起動時の解像度を設定する

echo '( sleep 3; wlr-randr --output NOOP-1 --custom-mode 960x640 ) &' >> ~/.config/labwc/autostart

Raspberry Piの起動時に、デスクトップ画面の解像度が自動的に960×640ピクセルに変更されるよう設定します。

③ Raspberry Piを再起動する

sudo reboot

再起動後、アイコンがタッチしやすい大きさになります。

文字入力も可能

ターミナルを開くと仮想キーボードが自動で表示され、タッチ操作で文字やコマンドを入力できます。

ブラウザの操作もできますが、ある程度慣れが必要です。

日本語入力ができない場合は、「fcitx5」と「mozc」をインストールして、再起動します。

sudo apt install -y fcitx5-mozc

画面の向きを変更する

縦向き表示(0°)に変更

初期設定は270°(横向き)です。画面の向きを変更したい場合は、以下のコマンドを使用します。

回転角度コマンド画面の向き
0°sudo spi-rotate 0縦向き
90°sudo spi-rotate 90横向き
180°sudo spi-rotate 180縦向き
270°sudo spi-rotate 270横向き

コマンドを実行すると、再起動するか確認するメッセージが表示されます。

Reboot now? [Y/n]:

Enterキーを押すとRaspberry Piが再起動し、画面の向きが変更されます。

画面の向きに合わせてタッチ位置も自動で補正されますが、それでもずれる場合は、キャリブレーション機能で補正できます。詳しい手順はGitHubのREADMEで紹介しています。

株価表示風の時計を作ってみた

せっかくなので、この液晶ディスプレイを使って、日経平均株価の表示画面を再現したデジタル時計を作ってみました。

モチーフにしたのは、経済ニュースなどで見かける日経平均株価の表示画面です。黒と青を基調とした背景に、大きな数字や赤い前日比を配置し、株価情報を表示する独特のデザインを再現しています。

こだわったのは時刻が1分進むたびに数字が点滅する演出です。現在時刻と前日比の表示部分が5秒間点滅し、500ミリ秒ごとに明るい表示と暗い表示を切り替えます。

そぞら
そぞら

下部の数値には固定のサンプルデータを使用しており、リアルタイムに更新されるのは上部の時計部分だけです。

プログラムにはPythonのPygameライブラリを使用。文字や数字、背景のグラデーション、表示枠などをすべてプログラムで描画しています。ここからは、Raspberry Piでプログラムを動かす手順を紹介します。

画面解像度と表示方向を設定する

今回のプログラムは、960×640ピクセルの画面解像度に合わせて作成しています。前述の画面解像度を変更するを参考に、デスクトップ画面の解像度を960×640ピクセルに設定してください。

画面の向きは、初期設定の270°(横向き)のままで問題ありません。ほかの角度に変更している場合のみ、以下のコマンドで270°に戻してください。

sudo spi-rotate 270

再起動の確認メッセージが表示されたら、Enterキーを押してRaspberry Piを再起動してください。

日本語フォントをインストールする

VNCでRaspberry Piのデスクトップに接続し、ターミナルを開いて以下のコマンドを実行してください。インストールできるソフトウェアの情報を最新の状態に更新します。

sudo apt update

日本語フォントをインストールします。

sudo apt install -y fonts-noto-cjk

Thonnyでプログラムを作成する

次に、Raspberry PiにインストールされているPython開発環境「Thonny」を使用します。

デスクトップ左上のメニューから「プログラミング」→「Thonny」を選択して起動してください。

Thonnyが開いたら、以下のPythonコードをすべてコピーし、エディター部分に貼り付けます。

チェックポイント

VNC経由でパソコンからコードを貼り付けると、日本語が文字化けする場合があります。 そのため、Raspberry PiのデスクトップでWebブラウザを起動し、この記事を開いてコードを直接コピーすることをおすすめします。

import pygame
import sys
from pathlib import Path
from datetime import datetime, timedelta

# =========================================================
# 表示データ(サンプルデータ)
# =========================================================

CHANGE_VALUE = 1853.13
OPEN_VALUE = 66339.85
HIGH_VALUE = 66441.77
LOW_VALUE = 63721.55

# 試験用(時間)
TIME_OFFSET_HOURS = 0

# =========================================================
# 画面設定
# =========================================================

pygame.init()

WIDTH = 960
HEIGHT = 640

FULLSCREEN = True

if FULLSCREEN:
    screen = pygame.display.set_mode(
        (WIDTH, HEIGHT),
        pygame.FULLSCREEN
    )
    pygame.mouse.set_visible(False)

else:
    screen = pygame.display.set_mode(
        (WIDTH, HEIGHT)
    )
    pygame.mouse.set_visible(True)

pygame.display.set_caption("日経平均株価")
pygame.mouse.set_visible(False)

clock = pygame.time.Clock()


# =========================================================
# 色設定
# =========================================================

BLACK = (4, 6, 9)
WHITE = (248, 248, 248)

HEADER_BLUE = (0, 77, 178)
MAIN_BLUE_TOP = (16, 110, 219)
MAIN_BLUE_BOTTOM = (0, 71, 172)

VALUE_BLUE_TOP = (7, 80, 183)
VALUE_BLUE_BOTTOM = (5, 52, 137)

POSITIVE_RED_TOP = (255, 78, 71)
POSITIVE_RED_BOTTOM = (224, 43, 40)

NEGATIVE_BLUE_TOP = (54, 126, 230)
NEGATIVE_BLUE_BOTTOM = (20, 74, 167)
NEGATIVE_TEXT = (120, 255, 120)

CURRENT_BOX_TOP = (255, 255, 255)
CURRENT_BOX_BOTTOM = (226, 230, 237)

DARK_BLUE = (8, 55, 145)
SHADOW = (0, 40, 110)

BLINK_BLUE_TOP = (8, 55, 145)
BLINK_BLUE_BOTTOM = (3, 28, 90)

# =========================================================
# フォント設定
# =========================================================

FONT_PATHS = [
    Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc"),
    Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"),
    Path("/usr/share/fonts/truetype/noto/NotoSansCJK-Bold.ttc"),
    Path("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"),
]


def find_font_path():
    for path in FONT_PATHS:
        if path.exists():
            return str(path)

    return None


FONT_PATH = find_font_path()


def load_font(size):
    if FONT_PATH:
        return pygame.font.Font(FONT_PATH, size)

    return pygame.font.SysFont(None, size)


font_title_jp = load_font(59)
font_title_en = load_font(43)

font_current_jp = load_font(61)
font_current_en = load_font(32)

font_label_jp = load_font(42)
font_label_en = load_font(29)

font_current_value = load_font(90)
font_change_value = load_font(59)
font_detail_value = load_font(52)


# =========================================================
# 描画用関数
# =========================================================

def draw_vertical_gradient(surface, rect, top_color, bottom_color):
    """縦方向のグラデーションを描画します。"""

    x, y, width, height = rect

    for line_y in range(height):
        ratio = line_y / max(height - 1, 1)

        red = int(
            top_color[0]
            + (bottom_color[0] - top_color[0]) * ratio
        )
        green = int(
            top_color[1]
            + (bottom_color[1] - top_color[1]) * ratio
        )
        blue = int(
            top_color[2]
            + (bottom_color[2] - top_color[2]) * ratio
        )

        pygame.draw.line(
            surface,
            (red, green, blue),
            (x, y + line_y),
            (x + width - 1, y + line_y)
        )


def draw_gradient_box(
    surface,
    rect,
    top_color,
    bottom_color,
    border_color=None,
    border_width=0,
    radius=0
):
    """枠付きのグラデーションボックスを描画します。"""

    x, y, width, height = rect

    box_surface = pygame.Surface(
        (width, height),
        pygame.SRCALPHA
    )

    draw_vertical_gradient(
        box_surface,
        (0, 0, width, height),
        top_color,
        bottom_color
    )

    if radius > 0:
        mask = pygame.Surface(
            (width, height),
            pygame.SRCALPHA
        )

        pygame.draw.rect(
            mask,
            (255, 255, 255, 255),
            (0, 0, width, height),
            border_radius=radius
        )

        box_surface.blit(
            mask,
            (0, 0),
            special_flags=pygame.BLEND_RGBA_MULT
        )

    surface.blit(box_surface, (x, y))

    if border_color and border_width > 0:
        pygame.draw.rect(
            surface,
            border_color,
            rect,
            width=border_width,
            border_radius=radius
        )


def draw_text(
    text,
    font,
    color,
    position,
    anchor="topleft"
):
    """指定位置に文字を描画します。"""

    text_surface = font.render(
        text,
        True,
        color
    )

    text_rect = text_surface.get_rect()

    setattr(text_rect, anchor, position)

    screen.blit(
        text_surface,
        text_rect
    )

    return text_rect


def draw_shadow_text(
    text,
    font,
    color,
    position,
    anchor="topleft",
    shadow_offset=2
):
    """影付き文字を描画します。"""

    shadow_position = (
        position[0] + shadow_offset,
        position[1] + shadow_offset
    )

    draw_text(
        text,
        font,
        (0, 35, 95),
        shadow_position,
        anchor
    )

    return draw_text(
        text,
        font,
        color,
        position,
        anchor
    )


def format_value(value):
    return f"{value:.2f}"


def format_change(value):
    if value > 0:
        return f"+{value:.2f}"

    return f"{value:.2f}"


# =========================================================
# メイン画面
# =========================================================

def draw_screen():
    screen.fill(BLACK)
    
    global last_minute
    global blink_start

    now = datetime.now() + timedelta(hours=TIME_OFFSET_HOURS)

    # 分が変わった瞬間
    if now.minute != last_minute:
        last_minute = now.minute
        blink_start = pygame.time.get_ticks()

    # 点滅するか判定(5秒間だけ)
    blinking = (
        blink_start is not None
        and pygame.time.get_ticks() - blink_start < 5000
    )

    # 点滅状態(500msごと)
    blink_on = (
        not blinking
        or (pygame.time.get_ticks() // 500) % 2 == 0
    )
    
    # -----------------------------------------------------
    # 上部タイトル
    # -----------------------------------------------------

    draw_text(
        "日経平均株価",
        font_title_jp,
        WHITE,
        (42, -11)
    )

    draw_text(
        "Nikkei 225",
        font_title_en,
        WHITE,
        (450, 11)
    )

    # -----------------------------------------------------
    # 青い背景
    # -----------------------------------------------------

    main_rect = (0, 85, 960, 555)

    draw_gradient_box(
        screen,
        main_rect,
        MAIN_BLUE_TOP,
        MAIN_BLUE_BOTTOM,
        radius=5
    )

    # -----------------------------------------------------
    # 現在値ラベル
    # -----------------------------------------------------

    draw_shadow_text(
        "現在値",
        font_current_jp,
        WHITE,
        (40, 93)
    )

    draw_text(
        "Current",
        font_current_en,
        WHITE,
        (42, 170)
    )

    # -----------------------------------------------------
    # 現在値ボックス
    # -----------------------------------------------------

    current_rect = (414, 121, 504, 144)

    pygame.draw.rect(
        screen,
        SHADOW,
        (
            current_rect[0] + 4,
            current_rect[1] + 5,
            current_rect[2],
            current_rect[3]
        )
    )

    if blink_on:
        current_top = CURRENT_BOX_TOP
        current_bottom = CURRENT_BOX_BOTTOM
        current_text_color = DARK_BLUE
    else:
        current_top = BLINK_BLUE_TOP
        current_bottom = BLINK_BLUE_BOTTOM
        current_text_color = WHITE

    draw_gradient_box(
        screen,
        current_rect,
        current_top,
        current_bottom,
        border_color=DARK_BLUE,
        border_width=3
    )

    current_time = (
        datetime.now()
        + timedelta(hours=TIME_OFFSET_HOURS)
    ).strftime("%H:%M.%S")

    draw_text(
        current_time,
        font_current_value,
        current_text_color,
        (898, 193),
        anchor="midright"
    )

    # -----------------------------------------------------
    # 左側ラベル
    # -----------------------------------------------------

    rows = [
        ("前日比", "Change", 329),
        ("始値", "Open", 421),
        ("高値", "High", 511),
        ("安値", "Low", 599),
    ]

    for japanese, english, center_y in rows:
        draw_shadow_text(
            japanese,
            font_label_jp,
            WHITE,
            (54, center_y),
            anchor="midleft"
        )

        draw_text(
            english,
            font_label_en,
            WHITE,
            (217, center_y + 4),
            anchor="midleft"
        )

    # -----------------------------------------------------
    # 前日比ボックス
    # -----------------------------------------------------

    change_rect = (530, 281, 388, 96)

    if blink_on:

        if CHANGE_VALUE >= 0:
            change_top = POSITIVE_RED_TOP
            change_bottom = POSITIVE_RED_BOTTOM
        else:
            change_top = NEGATIVE_BLUE_TOP
            change_bottom = NEGATIVE_BLUE_BOTTOM

    else:
        change_top = BLINK_BLUE_TOP
        change_bottom = BLINK_BLUE_BOTTOM

    pygame.draw.rect(
        screen,
        SHADOW,
        (
            change_rect[0] + 4,
            change_rect[1] + 5,
            change_rect[2],
            change_rect[3]
        )
    )

    draw_gradient_box(
        screen,
        change_rect,
        change_top,
        change_bottom,
        border_color=DARK_BLUE,
        border_width=3
    )

    if CHANGE_VALUE >= 0:
        change_text_color = WHITE
    else:
        change_text_color = NEGATIVE_TEXT

    draw_text(
        format_change(CHANGE_VALUE),
        font_change_value,
        change_text_color,
        (899, 329),
        anchor="midright"
    )

    # -----------------------------------------------------
    # 始値・高値・安値ボックス
    # -----------------------------------------------------

    detail_rect = (530, 388, 388, 241)

    draw_gradient_box(
        screen,
        detail_rect,
        VALUE_BLUE_TOP,
        VALUE_BLUE_BOTTOM
    )

    detail_values = [
        (OPEN_VALUE, 421),
        (HIGH_VALUE, 511),
        (LOW_VALUE, 599),
    ]

    for value, center_y in detail_values:
        draw_text(
            format_value(value),
            font_detail_value,
            WHITE,
            (900, center_y),
            anchor="midright"
        )


# =========================================================
# メインループ
# =========================================================
last_minute = datetime.now().minute
blink_start = None

while True:

    for event in pygame.event.get():

        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

        if event.type == pygame.KEYDOWN:

            if event.key == pygame.K_ESCAPE:
                pygame.quit()
                sys.exit()

    draw_screen()

    pygame.display.flip()
    clock.tick(20)

貼り付けたら、Thonnyの画面上部にある緑色の実行ボタンをクリックします。プログラムが起動すると、液晶ディスプレイに日経平均株価風の時計が全画面表示されます。プログラムを終了する場合は、キーボードのEscキーを押してください。

ちなみに、Raspberry Piと液晶を横向きに立てて置く場合は、写真のようなL字型のコネクタを使うと、ケーブルの収まりが良いです。

弱点を理解すればコスパが光る格安液晶

タッチ操作でアプリを起動したり、文字を入力したりといった基本的な操作は可能です。ただし、3.5インチの小さな画面でPythonのコードを作成するのは現実的ではなく、プログラミングにはVNCを利用する方が便利だと感じました。

また、今回自作したデスクトップ画面の表示システムでは約20FPSで動作しており、動きの滑らかさには物足りなさがあります。

そぞら
そぞら

表示方法を工夫すれば改善できるかもしれませんが、現状は十分な滑らかさではありません。

一方、私がよく使っているHyperPixel 4.0は、Raspberry Piに直接取り付けられる4インチ・800×480ピクセルの高解像度ディスプレイです。60FPSの滑らかな表示が可能で、昔のカーオーディオや駅のパタパタ式案内表示など、動きのリアルさを追求する作品では、表示速度の違いを大きく感じます。

正面からでないと鮮明に見えない

解像度についても、HyperPixel 4.0と比較すると低く、文字や画像の精細さには差があります。また、視野角が狭いため、少し斜めから見るだけで色合いが変わってしまう点も気になりました。

そぞら
そぞら

とはいえ、HyperPixel 4.0は価格が約3倍するため、単純に比較できるものではありません。

今回の液晶ディスプレイは、Raspberry Piに低予算で手軽に小型画面を追加したい人や、時計・センサーの測定値・システムの稼働状況などを表示したい人におすすめです。滑らかなアニメーションや画質にこだわる用途には物足りないものの、文字や数値を中心とした情報表示であれば、十分に活用できると思います。

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です