LCDディスプレ(ST7735)でのFPSチェック
Raspberry Pi Zero 2 Wを使用してST7735ディスプレイをPythonで制御する際、フレームレート(FPS)はパフォーマンス評価の重要な指標です。 本記事でPythonのライブラリであるst7735を使用し、ディスプレイにイメージを出力しFPSを計測します。
テスト環境
- ボード: Raspberry Pi Zero 2 W Rev 1.0
- OS: Debian(Bookworm)ベース
- カーネル: 6.6.28-v8+
- ファームウェア: 1.20240424
- ディスプレイ: TFT LCDディスプレイ
- WINGONEER 128×160 ST7735S
- パッケージ
背景
st7735のバージョン0.0.5
と1.0.0
st7735はST7735のTFT LCD ディスプレイを操作するためのライブラリです。 メンテナーはPimoroni Ltd.であり、主に、Pimoroniのディスプレイ1をターゲットにしているようです。
バージョン0.0.5
では、BGR/RGBを入れ替えられるようになり、基本的なディスプレイ操作が可能でした。
しかし、バージョン1.0.0
で大きな変更があり、従来のExampleなどがそのままでは動作しなくなりました。
原因はRPi.GPIOから、lgpiodとgpiodeviceの組み合わせへの変更です。
今回は、どちらのケースでも動作を確認し、FPSへの影響を検証します。
rpi-gpio
からrpi-lgpio
への変更
今回のst7735のパッケージとは直接関係ありませんが、RPi.GPIOはカーネルの更新により現在では一部の機能が利用できないとの報告があります2。
バージョン0.0.5
ではRPi.GPIO(rpi-gpio
)を利用しています。
代替品としてrpi-lgpio
を利用できます。本記事ではrpi-lgpio
を利用して実験しています。
2つのライブラリの違いについては、以下のページで解説されています。
FPSを図る方法
ディスプレイサイズに合わせた画像を作成し、その画像を連続表示することで、FPSを計測します。 表示にかかった総時間と画像の枚数から、FPSを算出します。 今回のケースでは148枚の画像を使用しました。 以下に、今回使用した画像イメージ(GIF)を示します。 生成用のスクリプトは末尾に掲載しています(fpsチェック用イメージ生成参照)。
このアニメーションの1フレームあたりのデータ量を計算します。
- 解像度: 128×160ピクセル
- カラーフォーマット: RGB565(16ビット/ピクセル)
- データ量: 128×160×16 = 327,680ビット = 40,960バイト
実際には以下のようにイメージを送信します。
port=0,
cs=0,
dc=24,
backlight=5,
rst=25,
width=128,
height=160,
rotation=0,
invert=False,
spi_speed_hz=10**6,
bgr=False
)
for
)
つまり、約6Mバイトのデータを送信する必要があります。
FPS
FPSはSPIの転送速度によって変化します。 以下に、SPI速度とFPSの結果をグラフにまとめます。 各プロットは10回平均の値を使用しています(なお、ディスプレイの物理的な制約により、120FPS以上は表示されません)。
このグラフから、FPSがステップ的に推移していることがわかります。 このステップ状の挙動については、既に素晴らしい記事がありました3。
実際の周波数は、指定した値に応じて、ハードウェア上の分周比で設定可能な近い周波数が選択されます。
つまり、SPIの周波数は任意の値を指定できるわけではなく、ハードウェアの制約により特定の周波数が選択されます。
これらを踏まえて、SPI速度とFPSの結果を表にまとめます。
SPI速度 (MHz) | 0.0.5 (FPS) | 1.0.0 (FPS) |
---|---|---|
6.00 | 8.725 | 8.763 |
6.10 | 8.833 | 8.872 |
6.50 | 9.267 | 9.309 |
7.00 | 9.809 | 9.855 |
8.00 | 10.703 | 10.755 |
9.00 | 11.597 | 11.655 |
10.00 | 12.402 | 12.476 |
11.00 | 13.206 | 13.297 |
12.00 | 13.974 | 14.066 |
13.00 | 14.742 | 14.835 |
14.00 | 15.349 | 15.466 |
15.00 | 15.955 | 16.097 |
16.00 | 16.696 | 16.852 |
17.00 | 17.438 | 17.606 |
19.00 | 18.254 | 18.436 |
20.00 | 18.697 | 18.908 |
23.00 | 20.146 | 20.413 |
25.00 | 21.269 | 21.509 |
30.00 | 22.524 | 22.869 |
34.00 | 23.254 | 23.538 |
40.00 | 24.826 | 25.097 |
50.00 | 26.508 | 26.870 |
67.00 | 29.469 | 29.995 |
100.00 | 30.869 | 31.305 |
注記: 実測値は、取得したデータから線形補間を用いて計算しています。
理論値と実測値の比較
理論上の最大FPSの計算
SPI通信速度を考慮します。 例えば、50MHzの場合:
- データ転送速度:50,000,000ビット/秒 = 6,250,000バイト/秒
理論上の最大FPSは以下のように計算できます。
理論FPS = 6,250,000 / 40,960 ≈ 152.59 (FPS)
実測値との比較
実際の測定では、SPI速度を上げても理論FPSに近づかないことがわかりました。 その理由を理解するために、SPI速度と実測フレーム時間、オーバーヘッド時間を比較します。
オーバーヘッド時間の定義
オーバーヘッド時間とは、データ転送以外の処理にかかる時間を指します。 具体的には、以下の要素が含まれます。
- Pythonの処理時間:データ準備、関数の呼び出しなどのスクリプト実行時間。
- GPIO操作の遅延:データ/コマンドの切り替え、チップセレクト(CS)の制御などのGPIOピン操作。
- システムコールの遅延:SPI通信の開始・終了に伴うシステムコール(writeやioctlなど)。
オーバーヘッド時間は、実測フレーム時間から理論フレーム時間(データ転送時間)を差し引くことで求められます。
オーバーヘッド時間 = 実測フレーム時間 − 理論フレーム時間
実測値と理論値の比較
以下の表に、SPI速度ごとの理論フレーム時間、実測フレーム時間、およびオーバーヘッド時間を示します。
SPI速度 (MHz) | 理論フレーム時間 (秒) | 実測フレーム時間 (秒) | オーバーヘッド時間 (秒) |
---|---|---|---|
6 | 0.05461 | 0.06003 | 0.00542 |
100 | 0.00328 | 0.02796 | 0.02468 |
オーバーヘッド時間がSPI速度の増加に伴い増大していることがわかります。 これは、CPU処理やPythonのスクリプト実行がデータ転送の高速化に追いついていないためと考えられます。 6MHzの場合、データ転送時間が長いため、CPUやPythonの処理遅延がデータ転送中に吸収されやすく、100MHzの場合、データ転送が高速なため、CPUやPythonの処理がボトルネックとなり、待ち時間が増加していると考えられます。
おまけ
SpiDev_xfer3
関数による影響
SPI通信はspidev
モジュールのxfer3
関数を介して行われています。
この関数は、C言語の実装になっています。
コードをコピーする
static PyObject *
このような低レベルの関数の処理時間を確認することで、オーバーヘッドの原因を特定できる可能性があります。
ボタンが使えない問題
RPi.GPIOを使用して、add_event_detect
でスイッチに動作を割り当てる簡単なテストプログラムが突然動かなくなりました。
# RuntimeError: Failed to add edge detection
rpi.gpioを使い続ける意味はありませんので、とりあえずrpi-lgpio
を使います。
ただ、gpiodを使う、という選択肢もあります。現にPimoroniは移行しています。
どちらが良いかは今後考えていこうかと思います。
この問題は、カーネルのバージョンアップに伴い、RPi.GPIOが一部の機能を利用できなくなったためです。 そのため、rpi-gpioを使い続ける意味は薄れ、代替としてrpi-lgpioやgpiodを利用する選択肢があります。 実際に、Pimoroniはgpiodへの移行を進めています4。
どちらを選択するかは、今後の検討課題とします。
fpsチェック用イメージ生成
以下は、FPS測定用の画像を生成するためのスクリプトです。
:
:
=
=
=
= \
=
=
= /2 - /2
=
=
=
=
= -
= * 2 * * /
=
= +
=
=
return
=
coresize
# Prints:
# 16384