⏯️

【Wio Terminal】LvGLとWi-Fiの併用・FreeRTOSでのマルチタスク

に公開

LvGLFreeRTOSWi-Fi

これまで、Wio TerminalLvGLを使わんと欲して試行を繰り返してきた。一いは、Wio Terminalでの動かし方。一いは、画面への入力。一いは、操作対象の変更、一いは、LvGLのタスクの使い方。以て、大凡の用途には困らなくなってきた。

次に関心を遣るのは、Wi-Fiによるインターネットの活用である。ありきたりな例だが、時刻を取得して表示すれば時計のようになる。

但し、Wio Terminalに於ける無線通信用のライブラリーSeeed_Arduino_rpcUnifiedの実装には、既にFreeRTOSが含まれている。

実装上の問題

現在私が把握するだけでも、いくつか問題がある。

1. Wio Terminalに於けるWi-FiFreeRTOSとの両立

https://zenn.dev/amenaruya/articles/801a76734428ec

通常であれば、Wio TerminalFreeRTOSを使うと雖も特に変わったことをする必要はない。簡単な用例はWikiにある。

https://wiki.seeedstudio.com/Software-FreeRTOS/

参考:他機の記事

この法は他の機種でも劇変するものではない。完全に同等ではないものの、概ね似たようなプログラムが見られる。

  • 機種不明(Arduino?)

https://qiita.com/ikenohotori/items/8c6285812bc4177c2564

  • ESP32

https://qiita.com/Yukiya_Ishioka/items/ae21b4a1aecf69a885f2

https://tutoduino.fr/en/discover-freertos-on-an-esp32-with-platformio/

  • Raspberry Pi Pico

https://qiita.com/yunkya2/items/7b9b38b7c8c33a6327af

  • Windows

https://note.com/ndenki/n/nccddfac45c6a


  • FreeRTOSドキュメントの日本語訳記事

https://qiita.com/azuki_bar/items/91e12949737f460dbc11

しかしWi-Fiをも使わんとすれば、「eRPC API」というものに従ってプログラムを記述しなければならない。理由は単に動くか否かであるため、選択の余地は無い。

https://embeddedrpc.github.io/eRPC/index.html

補足

Seeed_Arduino_rpcUnifiedの中に、erpc::Threadを使ってタスクを作っている記載がある。

https://github.com/Seeed-Studio/Seeed_Arduino_rpcUnified/blob/master/src/erpc_Unified_init.cpp#L106-L107

https://github.com/Seeed-Studio/Seeed_Arduino_rpcUnified/blob/master/src/erpc_Unified_init.cpp#L133-L134

このように、既に複数のタスクが作成され、起動している。よって、この方法に倣うことでタスクが動くようになる。しかしながら、Wi-Fiに関する記述がFreeRTOSで新たに作成するタスクにあると、「Hard Fault」というエラーを起こすことが分かっている。

https://zenn.dev/link/comments/7668d935e7aa62

Wi-Fiと他の作業を並行するマルチタスクを行うに当たり、Wi-Fiの処理はloop()に書く。erpc::Threadで作られるタスクには、Wi-Fiの処理を記述しない。その理由もまた動くか否かであり、選択の余地は無い。

inoファイル概形の例
#include <rpcWiFi.h>

constexpr uint16_t STACK_SIZE = 256;

void setup() {
    Serial.begin(115200);       // baudrate設定
    vNopDelayMS(1000);          // 一秒待機(故障対策)
    while(!Serial);             // シリアルモニターが開かれるまで待機
    vSetErrorSerial(&Serial);   // エラー出力を有効にする

    erpc::Thread TaskA(&ThreadA, configMAX_PRIORITIES - 10, STACK_SIZE, "Task A");
    TaskA.start();
}

void loop() {
    /* ここにWi-Fiの処理を記述する */
}

static void ThreadA(void* pvParameters) {
    (void) pvParameters;
    while (1) {
        /* ここに別の処理を記述する */
    }
}

2. FreeRTOSLvGL

LvGLRTOSとの併用も想定されている。

ドキュメントの確認

最新バージョンのドキュメントには、幾つもある中にFreeRTOSの名が見られる。(名が有るばかりで中身はない)。

https://docs.lvgl.io/master/details/integration/os/index.html

ここで、Wio TerminalLvGLを使う方法についてもWikiに記事がある。

https://wiki.seeedstudio.com/Wio-Terminal-LVGL/

この記事に示される通り、Wio TerminalではSeeed_Arduino_LvGLを使う。このライブラリーに使われているLvGLのバージョンは7.0.2である。

https://github.com/Seeed-Studio/Seeed_Arduino_LvGL/blob/master/src/library.json#L3

ドキュメントとしてはバージョン7.11.0の記事が残っているため、最新のものではなくこちらを読むこととなる。しかし、RTOSとして記載されているのはNuttXのみであった。

https://docs.lvgl.io/7.11/get-started/nuttx.html

このように、FreeRTOSに関する公式の情報が殆ど無い。

想定されているとは言え、FreeRTOSと用いる情報は少ない。その上、LvGLは「thread safeでない」とある。

LVGL is not thread-safe by default.

thread safeとは並行処理が問題なく行えるということであるため、つまりその逆、並行処理に問題が懸念されるものと思われる。

実際のところ、先のWi-Fiと同様、erpc::Threadで作成するタスクでは実行できなかった。つまり、Wi-Fiの記述をloop()に書かなければならないと同時に、LvGLの記述もloop()に書かなければならないのである。

対策として、LvGLの実行(lv_task_handler())をloop()に、Wi-Fiに関する処理はLvGLのタスクに分ける。

プログラム

適当なサンプルプログラムを改変する。

  • 上下に二つのボタンが配置され、Wio Terminal筐体のスイッチで操作できる
  • ボタンの文字に数を表示する
  • Wi-Fiアクセスポイントをスキャンして数を取得する
  • 上のボタンには経過tickの値を都度反映する
  • 下のボタンにはアクセスポイント数を都度反映する

なお、細かな設定が多くコードの整頓に困ったため、勝手にクラス化している。

https://zenn.dev/amenaruya/articles/273566fb230507

抽象部

コード
LvglArduinoAbstractLayer.hpp
#ifndef __LVGL_ARDUINO_ABSTRACT_LAYER_HPP__
#define __LVGL_ARDUINO_ABSTRACT_LAYER_HPP__

#include <Arduino.h>
#include <TFT_eSPI.h>
#include <lvgl.h>

template<class T>
class LAAL {
private:
    /* delete copy constructor */
    LAAL(const LAAL&) = delete;
    /* delete copy operator */
    LAAL& operator=(const LAAL&) = delete;

protected:
    /* singleton */
    static T*           __smpLaalSingleton;

    /* TFT LCD */
    static TFT_eSPI*    __mpTft;

    /* tick (1~10) */
    uint32_t            __mu32TickPeriod;

    /* ディスプレイ */

    /* display buffer */
    lv_disp_buf_t       __smDisplayBuffer;
    /* main color buffer */
    lv_color_t          __smColorBufferMain[LV_HOR_RES_MAX * 10];
    /* sub color buffer */
    lv_color_t          __smColorBufferSub[LV_HOR_RES_MAX * 10];
    /* display driver */
    lv_disp_drv_t       __mDisplayDriver;
    /* display */
    lv_disp_t*          __mpDisplay;
    /* 画面初期化 */
    static void         __mFDisplayFlush(
        lv_disp_drv_t*      pDisplayDriver,
        const lv_area_t*    pArea,
        lv_color_t*         pColor
    );
    /* 画面設定 */
    virtual void        __mFDisplaySetup();

    /* 入力デバイス(encode) */

    /* input device driver */
    lv_indev_drv_t      __mInputDeviceDriverEncoder;
    /* input device */
    lv_indev_t*         __mpInputDeviceEncoder;
    /* Reading encoder callback */
    static bool         __mFbReadEncoder(
        lv_indev_drv_t*     pInputDeviceDriver,
        lv_indev_data_t*    pInputDevicesData
    );
    /* デバイス設定 */
    virtual void        __mFDeviceSetup();

    /* 画面要素 */

    /* widget */
    /*
    example:
    lv_obj_t*           __mpObject;
    void                __mFObjectEventhandler(
        lv_obj_t*           pObject,
        lv_event_t          event
    ) = 0;
    */

    /* group */
    /*
    example:
    lv_group_t*         __mpGroup;
    void                __mFGroupSetup();
    */

    /* 画面構成 */
    virtual void        __mFContentsSetup() = 0;

    /* constructor */
    void                __mFNew();
    LAAL() = default;
    /* destructor */
    virtual ~LAAL() = default;

public:
    /* 設定 */
    void                mFSetup();
    /* 画面実行 */
    void                mFOperateGraphic();
};

#include "LvglArduinoAbstractLayer.ipp"

#endif // __LVGL_ARDUINO_ABSTRACT_LAYER_HPP__

LvglArduinoAbstractLayer.ipp
template<class T>
T* LAAL<T>::__smpLaalSingleton = nullptr;
template<class T>
TFT_eSPI* LAAL<T>::__mpTft = nullptr;

template<class T>
void LAAL<T>::__mFNew() {
    /* lvglの初期化 */
    lv_init();

    /* TFTの初期設定 */
    (this -> __mpTft) -> begin();
    (this -> __mpTft) -> setRotation(3);
}

template<class T>
void LAAL<T>::mFSetup() {
    /* 画面設定 */
    (this -> __mFDisplaySetup)();
    /* 入力設定 */
    (this -> __mFDeviceSetup)();
    /* 画面作成 */
    (this -> __mFContentsSetup)();
}

template<class T>
void LAAL<T>::__mFDisplayFlush(
    lv_disp_drv_t*      pDisplayDriver,
    const lv_area_t*    pArea,
    lv_color_t*         pColor
) {
    const uint32_t w = pArea -> x2 - pArea -> x1 + 1;
    const uint32_t h = pArea -> y2 - pArea -> y1 + 1;

    LAAL<T>::__mpTft -> startWrite();
    LAAL<T>::__mpTft -> setAddrWindow(pArea -> x1, pArea -> y1, w, h);
    LAAL<T>::__mpTft -> pushColors(&pColor -> full, w * h, true);
    LAAL<T>::__mpTft -> endWrite();

    lv_disp_flush_ready(pDisplayDriver);
}

template<class T>
void LAAL<T>::mFOperateGraphic() {
    while (1) {
        /* operate tasks */
        this -> __mu32TickPeriod = lv_task_handler();
        /* delay */
        delay(this -> __mu32TickPeriod);
        /* tell lvgl system elapsed time */
        lv_tick_inc(this -> __mu32TickPeriod);
    }
}

template<class T>
void LAAL<T>::__mFDisplaySetup() {
    /* 画面表示用bufferそのものの初期化 */
    lv_disp_buf_init(
        &(this -> __smDisplayBuffer),
        this -> __smColorBufferMain,
        this -> __smColorBufferSub,
        LV_HOR_RES_MAX * 10
    );

    /* 画面制御設定 */
    lv_disp_drv_init(&(this -> __mDisplayDriver));
    (this -> __mDisplayDriver).ver_res = LV_VER_RES_MAX;
    (this -> __mDisplayDriver).flush_cb = this -> __mFDisplayFlush;
    (this -> __mDisplayDriver).buffer = &(this -> __smDisplayBuffer);
    this -> __mpDisplay = lv_disp_drv_register(&(this -> __mDisplayDriver));
}

template<class T>
bool LAAL<T>::__mFbReadEncoder(
    lv_indev_drv_t*     pInputDeviceDriver,
    lv_indev_data_t*    pInputDevicesData
) {
    (void) pInputDeviceDriver;
    (void) pInputDevicesData;
    return false;
}

template<class T>
void LAAL<T>::__mFDeviceSetup() {
    /* 入力の設定 */
    lv_indev_drv_init(&(this -> __mInputDeviceDriverEncoder));
    (this -> __mInputDeviceDriverEncoder).type = LV_INDEV_TYPE_ENCODER;
    (this -> __mInputDeviceDriverEncoder).read_cb = this -> __mFbReadEncoder;
    this -> __mpInputDeviceEncoder = lv_indev_drv_register(&(this -> __mInputDeviceDriverEncoder));
}

アプリケーション部

コード
SimpleButtonApp.hpp
#ifndef __SIMPLE_BUTTON_APP_HPP__
#define __SIMPLE_BUTTON_APP_HPP__

#include <rpcWiFi.h>

#include "LvglArduinoAbstractLayer.hpp"

class App final: public LAAL<App> {
private:
    /* 画面設定 */
    void                __mFDisplaySetup();

    /* Reading encoder callback */
    static bool         __mFbReadEncoder(
        lv_indev_drv_t*     pInputDeviceDriver,
        lv_indev_data_t*    pInputDevicesData
    );
    /* デバイス設定 */
    void                __mFDeviceSetup();

    /* button1 */
    static lv_obj_t*    __mpObjectButton1;
    /* label1 */
    static lv_obj_t*    __mpObjectLabel1;
    /* button2 */
    static lv_obj_t*    __mpObjectButton2;
    /* label2 */
    static lv_obj_t*    __mpObjectLabel2;
    /* button配置 */
    void                __mFButtonsSetup();

    /* group */
    static lv_group_t*  __mpGroup;
    /* group設定 */
    void                __mFGroupSetup();

    /* last tick */
    static uint32_t     __mu32LastTick;
    /* task object */
    lv_task_t*          __mpTaskScanWifi;
    /* task function */
    static void         __mFScanWifi(
        lv_task_t*          pTask
    );
    /* set task */
    void                __mFSetTask();

    /* 画面構成 */
    void                __mFContentsSetup();

    /* constructor */
    App();
    /* destructor */
    ~App();

public:
    /* 初期化 */
    static App*         mFpLaalInitialize(
        TFT_eSPI*           pTft
    );
};

#endif // __SIMPLE_BUTTON_APP_HPP__

SimpleButtonApp.cpp
#include "SimpleButtonApp.hpp"

/* static member variable */

uint32_t        App::__mu32LastTick = 0;
lv_obj_t*       App::__mpObjectButton1 = nullptr;
lv_obj_t*       App::__mpObjectLabel1 = nullptr;
lv_obj_t*       App::__mpObjectButton2 = nullptr;
lv_obj_t*       App::__mpObjectLabel2 = nullptr;
lv_group_t*     App::__mpGroup = nullptr;

/* member function */

/* initialize */

/*static*/ App* App::mFpLaalInitialize(
    TFT_eSPI*           pTft
) {
    /* 未定ならば初期化 */
    if (LAAL<App>::__smpLaalSingleton == nullptr) {
        /* static member variableの初期化 */
        LAAL<App>::__mpTft = pTft;
        /* 子クラスの初期化 */
        LAAL<App>::__smpLaalSingleton = new App();
    }
    /* 既初期化の場合でも既存インスタンスへのポインターが返却される */
    return LAAL<App>::__smpLaalSingleton;
}

App::App() {
    LAAL<App>::__mFNew();
}
App::~App() {}

/* display */

void App::__mFDisplaySetup() {
    LAAL<App>::__mFDisplaySetup();
}

/* input */

/*static*/ bool App::__mFbReadEncoder(
    lv_indev_drv_t*  pInputDeviceDriver,
    lv_indev_data_t*        pInputDevicesData
) {
    (void) pInputDeviceDriver;

    if (digitalRead(WIO_5S_PRESS) == LOW) {
        pInputDevicesData -> state = LV_INDEV_STATE_PR;
    } else {
        pInputDevicesData -> state = LV_INDEV_STATE_REL;
    }

    /*
    indev_encoder_proc()に基づく左右の定義
    */
    if (digitalRead(WIO_5S_LEFT) == LOW) {
        pInputDevicesData -> enc_diff = -1;
    } else if (digitalRead(WIO_5S_RIGHT) == LOW) {
        pInputDevicesData -> enc_diff = 1;
    } else {
        pInputDevicesData -> enc_diff = 0;
    }

    /* no data buffered */
    return false;
}

void App::__mFDeviceSetup() {
    /* 入力の設定 */
    lv_indev_drv_init(&(LAAL<App>::__mInputDeviceDriverEncoder));
    (LAAL<App>::__mInputDeviceDriverEncoder).type = LV_INDEV_TYPE_ENCODER;
    (LAAL<App>::__mInputDeviceDriverEncoder).read_cb = App::__mFbReadEncoder;
    LAAL<App>::__mpInputDeviceEncoder = lv_indev_drv_register(&(LAAL<App>::__mInputDeviceDriverEncoder));

    /* enable 5way switch */
    pinMode(WIO_5S_UP, INPUT);
    pinMode(WIO_5S_DOWN, INPUT);
    pinMode(WIO_5S_LEFT, INPUT);
    pinMode(WIO_5S_RIGHT, INPUT);
    pinMode(WIO_5S_PRESS, INPUT);
}

/* contents */

/* callback */

/* widget */

void App::__mFButtonsSetup() {
    this -> __mpObjectButton1 = lv_btn_create(lv_scr_act(), NULL);
    // lv_obj_set_event_cb(this -> __mpObjectButton1, event_handler);
    lv_obj_align(this -> __mpObjectButton1, NULL, LV_ALIGN_CENTER, 0, -40);

    this -> __mpObjectLabel1 = lv_label_create(this -> __mpObjectButton1, NULL);
    lv_label_set_text(this -> __mpObjectLabel1, "Tick:0");

    this -> __mpObjectButton2 = lv_btn_create(lv_scr_act(), NULL);
    // lv_obj_set_event_cb(this -> __mpObjectButton2, event_handler);
    lv_obj_align(this -> __mpObjectButton2, NULL, LV_ALIGN_CENTER, 0, 40);
    lv_btn_set_checkable(this -> __mpObjectButton2, true);
    lv_btn_toggle(this -> __mpObjectButton2);
    lv_btn_set_fit2(this -> __mpObjectButton2, LV_FIT_NONE, LV_FIT_TIGHT);

    this -> __mpObjectLabel2 = lv_label_create(this -> __mpObjectButton2, NULL);
    lv_label_set_text(this -> __mpObjectLabel2, "AP:x");
}

/* group */

void App::__mFGroupSetup() {
    this -> __mpGroup = lv_group_create();
    lv_indev_set_group(LAAL<App>::__mpInputDeviceEncoder, this -> __mpGroup);
    lv_group_add_obj(this -> __mpGroup, this -> __mpObjectButton1);
    lv_group_add_obj(this -> __mpGroup, this -> __mpObjectButton2);
}

/* task */

/*static*/ void App::__mFScanWifi(
    lv_task_t*          pTask
) {
    (void) pTask;

    /* 経過時間 */
    uint32_t u32TickElapsed = lv_tick_get() - App::__mu32LastTick;
    /* tick更新 */
    bool bUpdateTick = false;

    /* label1にtickを更新 */
    lv_label_set_text_fmt(
        App::__mpObjectLabel1,
        "Tick:%d",
        lv_tick_get()
    );

    /* 5000tickより経過した場合 */
    if (u32TickElapsed > 5000) {
        /* Wi-Fiアクセスポイント数 */
        const uint16_t count = WiFi.scanNetworks();
        /* label2にアクセスポイント数を更新 */
        lv_label_set_text_fmt(
            App::__mpObjectLabel2,
            "AP:%d",
            count
        );

        bUpdateTick = true;
    }

    if (bUpdateTick) {
        App::__mu32LastTick = lv_tick_get();
    }
}

void App::__mFSetTask() {
    this -> __mpTaskScanWifi = lv_task_create(
        App::__mFScanWifi,
        500,
        LV_TASK_PRIO_MID,
        NULL
    );
}

void App::__mFContentsSetup() {
    (this -> __mFButtonsSetup)();
    (this -> __mFGroupSetup)();
    (this -> __mFSetTask)();
}

inoファイル

コード
#include <rpcWiFi.h>
#include <TFT_eSPI.h>
#include <lvgl.h>

#include "SimpleButtonApp.hpp"

/* シリアルデバッグ ON/OFF */
#define SERIAL_MONITOR_ON

/* threadの容量 */
constexpr uint16_t STACK_SIZE = 256;

/* TFT LCD */
static TFT_eSPI TFT;

/* App */
App* pApp;

void setup() {
    #ifdef SERIAL_MONITOR_ON
        Serial.begin(115200);
        vNopDelayMS(1000);
        while(!Serial);
        vSetErrorSerial(&Serial);
        Serial.println("Setup\tStart");
    #endif

    #ifdef SERIAL_MONITOR_ON
        Serial.println("Setup\ttask: create");
    #endif

    erpc::Thread TaskA(
        &ThreadA,
        configMAX_PRIORITIES - 10,
        STACK_SIZE,
        "Task A"
    );
    TaskA.start();

    #ifdef SERIAL_MONITOR_ON
        Serial.println("Setup\tWi-Fi: setting");
    #endif

    WiFi.mode(WIFI_STA);
    WiFi.disconnect();
    delay(100);

    #ifdef SERIAL_MONITOR_ON
        Serial.println("Setup\tLvGL: setting");
    #endif
    pApp = App::mFpLaalInitialize(&TFT);
    pApp -> mFSetup();
}

void loop() {
    #ifdef SERIAL_MONITOR_ON
        Serial.println("Loop\tLvGL: operating");
    #endif
    pApp -> mFOperateGraphic();

}

static void ThreadA(void* pvParameters) {
    (void) pvParameters;
    #ifdef SERIAL_MONITOR_ON
        Serial.println("Thread A\tStart");
    #endif
    uint32_t number = 0;
    while (1) {
        #ifdef SERIAL_MONITOR_ON
            Serial.printf("Thread A\tHello: %d\n", number);
        #endif
        number++;
        delay(2000);
    }
}

実行結果

画面の様子は撮影が面倒なため省略する。

なお、Wi-Fiアクセスポイントをスキャンしている数秒間、LvGLの処理は防がれる。これは、LvGLのタスクが単に一定周期で繰り返す処理を示すものであり、「並行処理を実現するものではない」ことによる。このため、スキャン中は画面の更新が止まり、操作を一切受け付けない。

一方、erpc::Threadで作成したFreeRTOSのタスクは「並行処理」である。従って、スキャン中でもThread A Hello: ~~の表示が止まることはない。

serial monitor
Setup   Start
Setup   task: create
Thread A        Start
Thread A        Hello: 0
Setup   Wi-Fi: setting
Setup   LvGL: setting
Loop    LvGL: operating
Thread A        Hello: 1
Thread A        Hello: 2
Thread A        Hello: 3
Thread A        Hello: 4
Thread A        Hello: 5
Thread A        Hello: 6
Thread A        Hello: 7
Thread A        Hello: 8
Thread A        Hello: 9
Thread A        Hello: 10

Discussion