pico-jxglib は、ワンボードマイコン Raspberry Pi Pico の Pico SDK プログラミングをサポートするライブラリです。
前の記事では、TFT LCD の初期化と描画方法についての詳細を解説しました。
今回は、pico-jxglib を使って、LVGL による高度な GUI を TFT LCD 上に実装します。タッチスクリーン付きの TFT LCD が、汎用の入力デバイスになりますよー。
LVGL について
LVGL は、限られたリソースしか持たない組み込み機器で高度な GUI を実現できるライブラリです。最小の環境で 16MHz の CPU、64KB の Flash メモリと 16KB の RAM があれば動作します。125MHz の CPU、2MB の Flash メモリと 264KB の SRAM を搭載した Pico ならば、かなりの余裕を持って動かすことができますね!
少ない消費リソースにもかかわらず、表現能力は非常に高いです。以下のスナップショットは LVGL が提供しているサンプルプログラムのひとつです。
スライドバーを操作するとそれぞれの値を制御点としたベジエ曲線をリアルタイムでグラフ中に描画します。さらに右側の再生ボタンをクリックすると、曲線に応じた速度で上部の赤い四角が左から右に移動します。実際の動作イメージをここで確認できます。これとまったく同じ GUI が手元のワンボードマイコンで実現できるのですから驚きです。
さらに感心するのは、単に実用目的の一点張りではなく、視覚表現に遊び心があふれていることです。例えば、ボタンをクリックしたときにはぶわっと広がるような表現効果をつけることができます。コンシューマむけの UI にももってこいですね。
いろいろなプラットフォームへのポーティングも Connecting LVGL to Your Hardware に沿って少ない工数で行うことができます。pico-jxglib もこの恩恵にあずからせてもらいました。
実際のプロジェクト
タッチスクリーンを搭載した ILI9341 を接続し、LVGL を使ったプログラムを実行します。なお、タッチスクリーンを持たないデバイスでも UART 経由でキーボード操作ができる方法を後で紹介します。
開発環境のセットアップ
Visual Studio Code や Git ツール、Pico SDK のセットアップが済んでいない方はこちらをご覧ください。
pico-jxglib は GitHub からレポジトリをクローンすることで入手できます。
git clone https://github.com/ypsitau/pico-jxglib.git
cd pico-jxglib
git submodule update --init
pico-jxglib はほぼ毎日更新されています。すでにクローンしている場合は、pico-jxglib
ディレクトリで以下のコマンドを実行して最新のものにしてください。
git pull
プロジェクトの作成
VSCode のコマンドパレットから >Raspberry Pi Pico: New Pico Project
を実行し、以下の内容でプロジェクトを作成します (Pico SDK プロジェクトの詳細はこちら)。
-
Name ... プロジェクト名を入力します。今回は例として
lvgltest
を入力します - Board type ... ボード種別を選択します
- Location ... プロジェクトディレクトリを作る一つ上のディレクトリを選択します
- Stdio support .. Stdio に接続するポート (GPIO または USB) を選択します
-
Code generation options ...
Generate C++ code
にチェックをつけます
このプロジェクトディレクトリと pico-jxglib
のディレクトリ配置が以下のようになっていると想定します。
+-[pico-jxglib]
+-[termtest]
+-CMakeLists.txt
+-lvgltest.cpp
+- ...
以下、このプロジェクトをもとに CMakeLists.txt
とソースファイルを編集してプログラムを作成していきます。
サンプルプログラムのビルドと実行
ブレッドボードの配線イメージは以下の通りです。
CMakeLists.txt
の最後に以下の行を追加してください。
target_link_libraries(lvgltest jxglib_ILI9341 jxglib_LVGL lvgl_examples)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../pico-jxglib pico-jxglib)
ソースファイルを以下のように編集します。
#include <stdio.h>
#include <examples/lv_examples.h>
#include "pico/stdlib.h"
#include "jxglib/ILI9341.h"
#include "jxglib/LVGL.h"
using namespace jxglib;
int main()
{
::stdio_init_all();
::spi_init(spi0, 2 * 1000 * 1000);
::spi_init(spi1, 125 * 1000 * 1000);
GPIO2.set_function_SPI0_SCK();
GPIO3.set_function_SPI0_TX();
GPIO4.set_function_SPI0_RX();
GPIO14.set_function_SPI1_SCK();
GPIO15.set_function_SPI1_TX();
ILI9341 display(spi1, 240, 320, {RST: GPIO10, DC: GPIO11, CS: GPIO12, BL: GPIO13});
ILI9341::TouchScreen touchScreen(spi0, {CS: GPIO8, IRQ: GPIO9});
display.Initialize(Display::Dir::Rotate90);
touchScreen.Initialize(display);
//touchScreen.Calibrate(display);
LVGL::Initialize();
LVGL::Adapter lvglAdapter;
lvglAdapter.EnableDoubleBuff().AttachOutput(display);
lvglAdapter.AttachInput(touchScreen);
::lv_example_anim_3();
for (;;) {
::sleep_ms(5);
::lv_timer_handler();
}
}
プログラム解説
上記のプログラムの処理内容について説明します。
::spi_init(spi0, 2 * 1000 * 1000);
::spi_init(spi1, 125 * 1000 * 1000);
タッチスクリーン用に SPI0 を 2MHz、TFT LCD 用に SPI1 を 125MHz で初期化します。
GPIO2.set_function_SPI0_SCK();
GPIO3.set_function_SPI0_TX();
GPIO4.set_function_SPI0_RX();
GPIO14.set_function_SPI1_SCK();
GPIO15.set_function_SPI1_TX();
GPIO を SPI0、SPI1 の信号線に割り当てます。
ILI9341 display(spi1, 240, 320, {RST: GPIO10, DC: GPIO11, CS: GPIO12, BL: GPIO13});
ILI9341::TouchScreen touchScreen(spi0, {CS: GPIO6, IRQ: GPIO7});
display.Initialize(Display::Dir::Rotate90);
touchScreen.Initialize(display);
//touchScreen.Calibrate(display);
ILI9341 の TFT LCD 部とタッチスクリーン部に SPI と GPIO を割り当て、初期化します。タッチスクリーンの画面座標へのマッピングは、手元のデバイスでキャリブレーションを行ったプリセット値を実装していますが、デバイスごとのばらつきがどれだけあるのかはまだ分かっていません。あまりずれるようでしたら、Calibrate()
関数の呼び出しを有効にしてキャリブレーションを行ってください。
LVGL::Initialize();
LVGL の初期化を行います。
LVGL::Adapter lvglAdapter;
lvglAdapter.EnableDoubleBuff().AttachOutput(display);
lvglAdapter.AttachInput(touchScreen);
LVGL::Adapter
を使って TFT LCD やタッチスクリーンを LVGL に接続します。EnableDoubleBuff()
を実行すると、描画バッファが二重になり、DMA を使うことで描画速度が向上します。ただし、消費メモリは 2 倍になります。
ここまでの処理で LVGL を使える環境が整いました。以降は正式サイトを参照して LVGL プログラミングをお楽しみください。
::lv_example_anim_3();
LVGL が提供しているサンプルプログラムの関数を呼び出しています。内部では、ウィジェットの生成とコールバック関数の登録が行われます。
for (;;) {
::sleep_ms(5);
::lv_timer_handler();
}
メインループです。::lv_timer_handler()
で LVGL の処理が行われます。
種々のサンプルプログラム
ディレクトリ pico-jxglib/LVGL/lvgl/examples
下には 100 を超える LVGL のサンプルプログラムがあります。これらを Pico ボードで容易に実行できる Pico SDK プロジェクトを用意しました。
上記と同じく、TFT LCD に ILI9341 を使います。ブレッドボードの配線イメージも同じです。GPIO の UART ポート (TX: GPIO0、RX: GPIO1) に USB-シリアル変換器をつなげるか、または USB 端子経由で PC に接続し、シリアルターミナルアプリを起動してください (通信速度は 115200bps)。
ディレクトリ pico-jxglib/LVGL/test-examples
内で VSCode を開いてプロジェクトのビルドおよびボードへのプログラム書き込みを行います。プログラムを実行するとシリアルターミナルに以下のような画面が出るので、実行するサンプルの番号を入力します。
--------
1:anim_1 52:style_5 103:keyboard_2
2:anim_2 53:style_6 104:label_1
3:anim_3 54:style_7 105:label_2
...
50:style_3 101:imagebutton_1 152:tileview_1
51:style_4 102:keyboard_1 153:win_1
Enter Number:
複数 LCD への表示
LVGL::Adapter
インスタンスを複数生成してそれぞれに TFT LCD やタッチスクリーンを接続することで、複数 LCD に LVGL の GUI を表示できます。
今回の例では ILI9341 と ILI9488 を接続します。ブレッドボードの配線イメージは以下の通りです。
CMakeLists.txt
の最後に以下の行を追加してください。
target_link_libraries(lvgltest jxglib_ILI9341 jxglib_ILI9488 jxglib_LVGL lvgl_examples)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../pico-jxglib pico-jxglib)
ソースファイルを以下のように編集します。
#include <stdio.h>
#include <examples/lv_examples.h>
#include "pico/stdlib.h"
#include "jxglib/ILI9341.h"
#include "jxglib/ILI9488.h"
#include "jxglib/LVGL.h"
using namespace jxglib;
int main()
{
::stdio_init_all();
::spi_init(spi0, 2 * 1000 * 1000);
::spi_init(spi1, 125 * 1000 * 1000);
GPIO2.set_function_SPI0_SCK();
GPIO3.set_function_SPI0_TX();
GPIO4.set_function_SPI0_RX();
GPIO14.set_function_SPI1_SCK();
GPIO15.set_function_SPI1_TX();
ILI9341 display1(spi1, 240, 320, {RST: GPIO10, DC: GPIO11, CS: GPIO12, BL: GPIO13});
ILI9488 display2(spi1, 320, 480, {RST: GPIO18, DC: GPIO19, CS: GPIO20, BL: GPIO21});
ILI9341::TouchScreen touchScreen1(spi0, {CS: GPIO8, IRQ: GPIO9});
ILI9488::TouchScreen touchScreen2(spi0, {CS: GPIO16, IRQ: GPIO17});
display1.Initialize(Display::Dir::Rotate90);
display2.Initialize(Display::Dir::Rotate90);
touchScreen1.Initialize(display1);
touchScreen2.Initialize(display2);
LVGL::Initialize();
LVGL::Adapter lvglAdapter1;
lvglAdapter1.AttachOutput(display1);
lvglAdapter1.AttachInput(touchScreen1);
LVGL::Adapter lvglAdapter2;
lvglAdapter2.SetPartialNum(20).AttachOutput(display2);
lvglAdapter2.AttachInput(touchScreen2);
lvglAdapter1.SetDefault();
::lv_example_anim_3();
lvglAdapter2.SetDefault();
::lv_example_keyboard_1();
for (;;) {
::sleep_ms(5);
::lv_timer_handler();
}
}
LVGL::Adapter インスタンスに対して SetDefault()
関数を実行すると、それ以降の LVGL の関数呼び出しはそのアダプタに接続した LCD への操作になります。
SetPartialNum()
関数は、LVGL が画面全体を何分割して描画するかを指定します。数字が大きいほど分割数が多くなるので、描画バッファのサイズは小さくてすみます。通常の設定では 10 分割されますが、今回の例では LCD を二つ接続している上、追加した ILI9488 の画面サイズは大きく、またピクセルあたり 3byte 必要になるので、Pico の RAM 容量を超えてしまうのです。そのため、分割数を多くしてメモリを節約しています。
UART による操作
LVGL の操作は基本的にはタッチスクリーンになるのですが、キーボードによる操作も可能です。ここでは、GPIO の UART ポート (TX: GPIO0、RX: GPIO1) に USB-シリアル変換器をつなげて PC に接続し、シリアルターミナルからの入力でキーボード入力をシミュレートする方法について説明します。
TFT LCD には ST7789 を使います。ブレッドボードの配線イメージは以下の通りです。
CMakeLists.txt
の最後に以下の行を追加してください。
target_link_libraries(lvgltest jxglib_ST7789 jxglib_LVGL lvgl_examples)
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../pico-jxglib pico-jxglib)
ソースファイルを以下のように編集します。
#include <stdio.h>
#include <examples/lv_examples.h>
#include "pico/stdlib.h"
#include "jxglib/ST7789.h"
#include "jxglib/LVGL.h"
using namespace jxglib;
int main()
{
::stdio_init_all();
::spi_init(spi1, 125 * 1000 * 1000);
GPIO14.set_function_SPI1_SCK();
GPIO15.set_function_SPI1_TX();
ST7789 display(spi1, 240, 320, {RST: GPIO10, DC: GPIO11, CS: GPIO12, BL: GPIO13});
display.Initialize(Display::Dir::Rotate90);
LVGL::Initialize();
LVGL::Adapter lvglAdapter;
lvglAdapter.EnableDoubleBuff().AttachOutput(display);
lvglAdapter.AttachInput(UART::Default);
::lv_example_keyboard_1();
for (;;) {
::sleep_ms(5);
::lv_timer_handler();
}
}
PgUp
と PgDn
でフォーカスを移動します。Enter
でフォーカスのついたウィジェットを「クリック」します。
次回の記事
次回は pico-jxglib で TFT LCD にターミナル機能を実装します。