【M5Stack】M5Stack-SD-UpdaterのUIをM5GFXのPanel_CVBSを使って、コンポジットビデオ出力できるように改修してみた


<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103572/4e087775-6376-87c7-c4f5-ac9fd32bf78c.png"><img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103572/affaf001-4902-5219-a937-f54ab676e408.png">

## はじめに

この記事は、ATOM Lite＋TF-CARDモジュール＋Panel_CVBS＋RCAユニットでM5Stack-SD-Updaterを動作させるために改修を行った際のメモです。

なにかの役に立つかもしれないし、役に立たないかもしれませんがコミットさせていただきます。どこかのどなたかのなんらかに貢献できましたら幸いです。

## 概要

### M5Stack-SD-Updaterとは

<img width="500" src="https://github.com/PartsandCircuits/M5Stack-SD-Updater/blob/master/SDUpdaterpic02.png?raw=true">

M5Stack-SD-UpdaterはM5Stack Coreなどの液晶付きデバイス用に作成された、ユーザーがSDカードに保存したbinファイル（実行ファイル）をUIメニューより選択してロードできるbinファイルローダーです。

このアプリケーションのおかげで、PlatfromIOやArduinoIDEなどでビルド＆ダウンロード操作を毎回行う必要がなくなりました。事前にビルドしておいたbinファイルをSDカードに保存しておくことで、M5StackはSDカードより複数のアプリケーションを選択して実行できます。

### M5GFXのPanel_CVBSについて

M5Stack-SD-UpdaterはM5Stack CoreやBasicなどの液晶付きデバイス用に作成されています。riraosanはATOM LiteとTF-CARDモジュールの組み合わせでこのローダーを使ってみたいと思いました。そこで、UI表示先はM5GFXのPanel_CVBSを使用することとしました。

先日[M5GFX](https://github.com/m5stack/M5GFX)のPanel_CVBSがGitHubリポジトリのmasterにマージされました。また[RCAユニット](https://shop.m5stack.com/collections/m5stack-new-arrival/products/rca-audio-video-composite-unit)もリリースされました。

ATOM Lite＋TF-CARDモジュール＋Panel_CVBS＋RCAユニットでM5Stack-SD-Updaterを動作させるために改修を行った際のメモと参考ソースコードを以下に示します


<img width="300" src="https://cdn.shopify.com/s/files/1/0056/7689/2250/products/1_e07ec943-5c9e-482b-abe6-bc65637b0277_1200x1200.jpg">[RCAユニット](https://shop.m5stack.com/collections/m5stack-new-arrival/products/rca-audio-video-composite-unit)

## ソースコードを改修した際におこなったこと

### TF-CARDモジュールに対応してみる

<img width="300" src="https://cdn.shopify.com/s/files/1/0056/7689/2250/products/1_1e557141-6f98-48a1-81b6-e75fcad15d0e_1200x1200.jpg">[TF-CARDモジュール](https://shop.m5stack.com/products/atom-tf-card-kit)

元のソースを参照すると、SPIピンアサインがデフォルトであったので、TF-CARDモジュールに対応させるために、_CLK, _MISO, _MOSIをplatformio.iniより設定することとしました。（ソースコード内に記述してもいいかもしれません。）
https://github.com/riraosan/M5Stack-SD-Updater_with_Panel_CVBS/blob/759323470dad4cb9553663e3e12227ce16f0421f/src/M5StackUpdater.h#L210
```cpp:M5StackUpdater.h
     #if defined (_SD_H_)
        log_d(" Checking for SD Support (pin #%d)",  cfg->TFCardCsPin );
        if( &fs == &SD ) {
          SPI.begin(_CLK, _MISO, _MOSI, cfg->TFCardCsPin);
          SPI.setDataMode(SPI_MODE3);
          if (!SD.begin(cfg->TFCardCsPin, SPI, 80000000)) {  // 80MHz(MAX)
            msg[0] = String("SD MOUNT FAILED (pin #" + String(cfg->TFCardCsPin) + ")").c_str();
            if( report_errors ) _error( msg, 2 );
            return false;
          } else {
            log_d("SD Successfully mounted (pin #%d)", cfg->TFCardCsPin);
          }
          mounted = true;
        }
```
SDカードの読み込みは最高速の設定としています。（たぶん最高速で読めると思う）
SDカードの読み込みが不安定な場合はこちらの設定を改修してください。
```cpp:note
          SPI.begin(_CLK, _MISO, _MOSI, cfg->TFCardCsPin);
          SPI.setDataMode(SPI_MODE3);
          if (!SD.begin(cfg->TFCardCsPin, SPI, 80000000)) {  // 80MHz(MAX)
```
```yaml:platformio.ini
build_flags =
  ${env.build_flags}
  -D ARDUINO_M5STACK_ATOM_AND_TFCARD
  -D _MOSI=19
  -D _MISO=33
  -D _CLK=23
```
### LovyanGFXとESP32-Chimera-Coreライブラリを無効にしてみる
M5GFXのPanel_CVBSライブラリを使う際にLGFX_8BIT_CVBSユーザークラスを使用する目的があるため、lib_ignoreを利用してLovyanGFXとESP32-Chimera-Coreのライブラリの読み込みを無効としました。
```yaml:platformio.ini
lib_ignore =
  LovyanGFX
  ESP32-Chimera-Core
```

### M5GFXライブラリを有効にしてみる
M5GFXとButton2ライブラリを有効にしました。
Button2ライブラリはATOM LiteのG39を使って、UIメニューを選択する際に利用します。目的はG39を通じてUIの操作を行うため、です。
```yaml:platformio.ini
lib_deps =
  ${env.lib_deps}
  M5GFX
  Button2
```
### LGFX_8BIT_CVBSを改修して追加してみる

LovyanGFXのPanel_CVBSを使用するためのサンプルユーザークラスに、M5Stack-SD-Updaterが使用するプログレスバーを追加したものを作成しました。
この改修により、コンポジット信号を出力できるようになり、M5Stack-SD-Menuサンプルプログラムが初回起動直後に表示するプログレスバーを表示できるようになりました。

```cpp:LGFX_8BIT_CVBS.h
#pragma once

#include <M5GFX.h>

class LGFX_8BIT_CVBS : public lgfx::LGFX_Device {
public:
  lgfx::Panel_CVBS _panel_instance;

  LGFX_8BIT_CVBS(void) {
    {                                       // 表示パネル制御の設定を行います。
      auto cfg = _panel_instance.config();  // 表示パネル設定用の構造体を取得します。

      // 出力解像度を設定;
      cfg.memory_width  = 360;  // 出力解像度 幅
      cfg.memory_height = 240;  // 出力解像度 高さ

      // 実際に利用する解像度を設定;
      cfg.panel_width  = 360 - 8;       // 実際に使用する幅   (memory_width と同値か小さい値を設定する)
      cfg.panel_height = 240 - 16;  // 実際に使用する高さ (memory_heightと同値か小さい値を設定する)

      // 表示位置オフセット量を設定;
      cfg.offset_x = 4;  // 表示位置を右にずらす量 (初期値 0)
      cfg.offset_y = 8;  // 表示位置を下にずらす量 (初期値 0)

      _panel_instance.config(cfg);

      // 通常は memory_width と panel_width に同じ値を指定し、 offset_x = 0 で使用します。;
      // 画面端の表示が画面外に隠れるのを防止したい場合は、 panel_width の値をmemory_widthより小さくし、offset_x で左右の位置調整をします。;
      // 例えば memory_width より panel_width を 32 小さい値に設定した場合、offset_x に 16 を設定することで左右位置が中央寄せになります。;
      // 上下方向 (memory_height , panel_height , offset_y ) についても同様に、必要に応じて調整してください。;
    }

    {
      auto cfg = _panel_instance.config_detail();

      // 出力信号の種類を設定;
      // cfg.signal_type = cfg.signal_type_t::NTSC;
      cfg.signal_type = cfg.signal_type_t::NTSC_J;
      // cfg.signal_type = cfg.signal_type_t::PAL;
      // cfg.signal_type = cfg.signal_type_t::PAL_M;
      // cfg.signal_type = cfg.signal_type_t::PAL_N;

      // 出力先のGPIO番号を設定;
      cfg.pin_dac = 26;  // DACを使用するため、 25 または 26 のみが選択できます;

      // PSRAMメモリ割当の設定;
      cfg.use_psram = 0;  // 0=PSRAM不使用 / 1=PSRAMとSRAMを半々使用 / 2=全部PSRAM使用;

      // 出力信号の振幅の強さを設定;
      cfg.output_level = 128;  // 初期値128
      // ※ GPIOに保護抵抗が付いている等の理由で信号が減衰する場合は数値を上げる。;
      // ※ M5StackCore2 はGPIOに保護抵抗が付いているため 200 を推奨。;

      // 彩度信号の振幅の強さを設定;
      cfg.chroma_level = 128;  // 初期値128
      // 数値を下げると彩度が下がり、0で白黒になります。数値を上げると彩度が上がります。;

      _panel_instance.config_detail(cfg);
    }

    setPanel(&_panel_instance);
  }

  template <typename T>
  void clearDisplay(T color = 0) { fillScreen(color); }

  void display() {}

  void progressBar(int x, int y, int w, int h, uint8_t val, uint16_t color = 0x09F1, uint16_t bgcolor = 0x0000)
  {
	drawRect(x, y, w, h, color);
	if (val > 100)
	  val = 100;
	if (val == 0)
	{
	  fillRect(x + 1, y + 1, w - 2, h - 2, bgcolor);
	}
	else
	{
	  int fillw = (w * (((float)val) / 100.0)) - 2;
	  fillRect(x + 1, y + 1, fillw - 2, h - 2, color);
	  fillRect(x + fillw + 1, y + 1, w - fillw - 2, h - 2, bgcolor);
	}
  }

  const uint32_t &textcolor = _text_style.fore_rgb888;
  const uint32_t &textbgcolor = _text_style.back_rgb888;
  const textdatum_t &textdatum = _text_style.datum;
  const float &textsize = _text_style.size_x;
};

```

### menu.hを改修してみる

menu.hを改修しました。ATOMを使用する際は、core.hをコンパイルしないこととしました。
USE_DISPLAY, LGFX_ONLYを定義してM5GFXのPanel_CVBSを使えるようにしました。
また、TF-CARDモジュールではSDカードのCSピンは使用しないので、TFCARD_CS_PINを-1としました。

```cpp:menu.h

#if !defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)
#include "core.h"
#else
#include <SD.h>
#include "LGFX_8BIT_CVBS.h"
#include <Button2.h>
#define USE_DISPLAY
#define LGFX_ONLY
#define TFCARD_CS_PIN -1

static LGFX_8BIT_CVBS tft;
#define LGFX LGFX_8BIT_CVBS
//#define BUTTON_WIDTH 60
#define SDU_APP_NAME "Application Launcher"
#include <M5StackUpdater.h>
static LGFX_Sprite sprite(&tft);
fs::SDFS &M5_FS(SD);

Button2 button;//for G39

#endif
```

```cpp:partition_manager.h
#if !defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)
#include "core.h"
#endif

```


### main.cppを改修してみる

SD Updaterの描画先をPanel_CVBSにする目的で`setSDUGfx()`を記述したあと、Panel_CVBSライブラリを`tft.init()`で初期化しました。

```cpp:main.cpp
void setup() {
#if defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)
	setSDUGfx(&tft);
	tft.init();
#else
	M5.begin(); // bool LCDEnable, bool SDEnable, bool SerialEnable, bool I2CEnable, bool ScreenShotEnable
#endif
....
```
### M5.Lcdをtftに置換してみる

描画系のAPIをLGFX_8BIT_CVBSに更新したので、M5.LcdをLGFX_8BIT_CVBSインスタンスのtftに置換しました。
（例）

https://github.com/riraosan/M5Stack-SD-Updater_with_Panel_CVBS/blob/759323470dad4cb9553663e3e12227ce16f0421f/examples/M5Stack-SD-Menu/SAM.h#L205

### controls.hを改修してみる

メニューを選択するため、G39を押下した際のButton2イベントハンドラーをcontrols.hに追加しました。

```cpp:
HIDSignal HIDFeedback( HIDSignal signal, int ms = 50 )
{
  if( signal != HID_INERT ) {
    HIDFeedback( ms );
  }
  return signal;
}

static HIDSignal _button;

void handler(Button2 &btn)
{
  switch (btn.getType())
  {
  case clickType::single_click:
	Serial.print("single ");
	_button = HIDFeedback(HID_DOWN);
	break;
  case clickType::double_click:
	Serial.print("double ");
	_button = HIDFeedback(HID_PAGE_DOWN);
	break;
  case clickType::triple_click:
	Serial.print("triple ");
	_button = HIDFeedback(HID_PAGE_UP);
	break;
  case clickType::long_click:
	Serial.print("long ");
	_button = HIDFeedback(HID_SELECT);
	break;
  case clickType::empty:
	break;
  default:
	break;
  }

  Serial.print("click");
  Serial.print(" (");
  Serial.print(btn.getNumberOfClicks());
  Serial.println(")");
}

void HIDInit()
{
#if defined CAN_I_HAZ_M5FACES
  Wire.begin(SDA, SCL);
  M5FacesEnabled = Faces.canControlFaces();
  if( M5FacesEnabled ) {
  	// set the interrupt
  	attachInterrupt(5, M5FacesIsr, FALLING); // 5 = KEYBOARD_INT from M5Faces.cpp
  	Serial.println("M5Faces enabled and listening");
  }
#endif

  // G39 button
  button.setClickHandler(handler);
  button.setDoubleClickHandler(handler);
  button.setTripleClickHandler(handler);
  button.setLongClickHandler(handler);
  button.begin(39);
}

....

#if defined ARDUINO_M5Stack_Core_ESP32 || defined ARDUINO_M5STACK_FIRE || defined ARDUINO_M5STACK_Core2

      // legacy buttons support
      bool a = M5.BtnA.wasPressed();
      bool b = M5.BtnB.wasPressed() && !M5.BtnC.isPressed();
      bool c = M5.BtnC.wasPressed() && !M5.BtnB.isPressed();
      bool d = ( M5.BtnB.wasPressed() && M5.BtnC.isPressed() );
      bool e = ( M5.BtnB.isPressed() && M5.BtnC.wasPressed() );

	  if( d || e ) return HIDFeedback( HID_PAGE_UP ); // multiple push, suggested by https://github.com/mongonta0716
	  if( b ) return HIDFeedback( HID_PAGE_DOWN );
	  if( c ) return HIDFeedback( HID_DOWN );
	  if( a ) return HIDFeedback( HID_SELECT );

    #elif defined(ARDUINO_M5STACK_ATOM_AND_TFCARD)
	  button.loop();
	  HIDSignal temp = _button;
	  _button = HID_INERT;
	  return temp;
    #endif

    // if( d || e ) return HIDFeedback( HID_PAGE_UP ); // multiple push, suggested by https://github.com/mongonta0716
    // if( b ) return HIDFeedback( HID_PAGE_DOWN );
    // if( c ) return HIDFeedback( HID_DOWN );
    // if( a ) return HIDFeedback( HID_SELECT );

  #endif

```

## さいごに

これらの改修を行ったソースコードを次のリポジトリに公開しています。

https://github.com/riraosan/M5Stack-SD-Updater_with_Panel_CVBS/tree/develop

MIT Licenseです。

実際の動作風景はこちら。
ご参考になれば幸いです。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ヤッター<br>一方通行ですが、LGFXのPanel_CVBSとATOMのG39ボタンでbinをロード出来ました。<br>参考ですが改修方法をQiitaに書きます。<br>元のメニューに戻る方法を調べてみよう。<a href="https://twitter.com/hashtag/M5Stack?src=hash&amp;ref_src=twsrc%5Etfw">#M5Stack</a> <a href="https://t.co/DR8BhvIBO1">pic.twitter.com/DR8BhvIBO1</a></p>&mdash; riraosan@再起動 (@riraosan_0901) <a href="https://twitter.com/riraosan_0901/status/1609123944338165769?ref_src=twsrc%5Etfw">December 31, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

（了）

---