見出し画像

M5 ATOM Lite SSD1306をLovyanGFXで描画する。


M5Stack ATOM Liteで、SSD1306に描画する方法について解説します。今回は、LovyanGFXライブラリを使用してSSD1306を描画します。
LovyanGFXは、センスのいいフォントやグラフィック関数が充実しています。今後もCore2などでも、利用できるので、すこし頑張って学習してみました。


配線図

<I2C arduino Wire0、Wire1>

LovyanGFXを使用してSSD1306にカウンター値を表示させます。
AtomのCPU, ESP32-Picoには、2つのI2Cモジュールが搭載されています。arduinoのWire1(G21:SCL  G25:SDA)をLovyanGFXで使用してSSD1306に接続しています。
また、Wire0をGroveコネクタに割り付けています。G32をSCL、G26をSDAとして初期化しています。電源は5Vではなく、3.3v端子からそれぞれのI2Cデバイスに供給しています。5vでも動きそうですが、念のため3.3vにしました。

<LovyanGFXを使ってSSD1306を駆動するときのI2C の挙動について>

LovyanGFXを使ってSSD1306を描画するとき、Lovyan側でWire1を占有しているらしく、他のI2Cデバイスを接続しても、うまく通信できませんでした。
PCF8574(IOエキスパンダ)をWire1でSSD1306と共有しても、動く、と思ったのですが、動きませんでした。学習の初段階なので、LovyanGFXのI2C通信には、深入りせずに、Wire1はLovyanGFX SSD1306専属にして、他のI2CデバイスをWire0で駆動することにしました。
追伸:24/1/2 Wire1を複数デバイスで共有することができるようになりました。詳細は、一番下に書いてあります。

<LovyanGFXの使い方 SSD1306 OLED液晶>

追加したライブラリLovyanGFXフォルダの中にある、
LovyanGFX\examples\HowToUse\2_user_setting\2_user_setting.inoに、
使い方が書かれたサンプルコードがあります。

PlatformIOのフォルダ サンプルコード2_user_setting.ino

<class LGFX : public lgfx::LGFX_Deviceクラス>

 lgfx::LGFX_Deviceクラスを継承して、使います。クラスの命名は、サンプルコード内に解説があります。(lgfxはnamespace名)。
<以下抜粋>
クラス名は"LGFX"から別の名前に変更しても構いません。 AUTODETECTと併用する場合は"LGFX"は使用されているため、LGFX以外の名前に変更してください。 また、複数枚のパネルを同時使用する場合もそれぞれに異なる名前を付けてください。 ※ クラス名を変更する場合はコンストラクタの名前も併せて同じ名前に変更が必要です。 名前の付け方は自由に決めて構いませんが、設定が増えた場合を想定し、 例えばESP32 DevKit-CでSPI接続のILI9341の設定を行った場合、 LGFX_DevKitC_SPI_ILI9341 のような名前にし、ファイル名とクラス名を一致させておくことで、利用時に迷いにくくなります。 
<抜粋おわり>
以下、サンプルコードから一部抜粋↓

#include <LovyanGFX.hpp>

// ESP32でLovyanGFXを独自設定で利用する場合の設定例

/// 独自の設定を行うクラスを、LGFX_Deviceから派生して作成します。
class LGFX : public lgfx::LGFX_Device
{
/*
 クラス名は"LGFX"から別の名前に変更しても構いません。
 AUTODETECTと併用する場合は"LGFX"は使用されているため、LGFX以外の名前に変更してください。
 また、複数枚のパネルを同時使用する場合もそれぞれに異なる名前を付けてください。
 ※ クラス名を変更する場合はコンストラクタの名前も併せて同じ名前に変更が必要です。

 名前の付け方は自由に決めて構いませんが、設定が増えた場合を想定し、
 例えばESP32 DevKit-CでSPI接続のILI9341の設定を行った場合、
  LGFX_DevKitC_SPI_ILI9341
 のような名前にし、ファイル名とクラス名を一致させておくことで、利用時に迷いにくくなります。
//*/


// 接続するパネルの型にあったインスタンスを用意します。
//lgfx::Panel_GC9A01      _panel_instance;
//lgfx::Panel_GDEW0154M09 _panel_instance;
//lgfx::Panel_HX8357B     _panel_instance;
//lgfx::Panel_HX8357D     _panel_instance;
//lgfx::Panel_ILI9163     _panel_instance;
  lgfx::Panel_ILI9341     _panel_instance;
//lgfx::Panel_ILI9342     _panel_instance;
//lgfx::Panel_ILI9481     _panel_instance;
//lgfx::Panel_ILI9486     _panel_instance;
//lgfx::Panel_ILI9488     _panel_instance;
//lgfx::Panel_IT8951      _panel_instance;
//lgfx::Panel_RA8875      _panel_instance;
//lgfx::Panel_SH110x      _panel_instance; // SH1106, SH1107
//lgfx::Panel_SSD1306     _panel_instance;
//lgfx::Panel_SSD1327     _panel_instance;
//lgfx::Panel_SSD1331     _panel_instance;
//lgfx::Panel_SSD1351     _panel_instance; // SSD1351, SSD1357
//lgfx::Panel_SSD1963     _panel_instance;
//lgfx::Panel_ST7735      _panel_instance;
//lgfx::Panel_ST7735S     _panel_instance;
//lgfx::Panel_ST7789      _panel_instance;
//lgfx::Panel_ST7796      _panel_instance;


// パネルを接続するバスの種類にあったインスタンスを用意します。
  lgfx::Bus_SPI        _bus_instance;   // SPIバスのインスタンス
//lgfx::Bus_I2C        _bus_instance;   // I2Cバスのインスタンス
//lgfx::Bus_Parallel8  _bus_instance;   // 8ビットパラレルバスのインスタンス

// バックライト制御が可能な場合はインスタンスを用意します。(必要なければ削除)
  lgfx::Light_PWM     _light_instance;

// タッチスクリーンの型にあったインスタンスを用意します。(必要なければ削除)
  lgfx::Touch_FT5x06           _touch_instance; // FT5206, FT5306, FT5406, FT6206, FT6236, FT6336, FT6436
//lgfx::Touch_GSL1680E_800x480 _touch_instance; // GSL_1680E, 1688E, 2681B, 2682B
//lgfx::Touch_GSL1680F_800x480 _touch_instance;
//lgfx::Touch_GSL1680F_480x272 _touch_instance;
//lgfx::Touch_GSLx680_320x320  _touch_instance;
//lgfx::Touch_GT911            _touch_instance;
//lgfx::Touch_STMPE610         _touch_instance;
//lgfx::Touch_TT21xxx          _touch_instance; // TT21100
//lgfx::Touch_XPT2046          _touch_instance;

public:

  // コンストラクタを作成し、ここで各種設定を行います。
  // クラス名を変更した場合はコンストラクタも同じ名前を指定してください。
  LGFX(void)
  {
    { // バス制御の設定を行います。
      auto cfg = _bus_instance.config();    // バス設定用の構造体を取得します。

// SPIバスの設定
      cfg.spi_host = VSPI_HOST;     // 使用するSPIを選択  ESP32-S2,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST
      // ※ ESP-IDFバージョンアップに伴い、VSPI_HOST , HSPI_HOSTの記述は非推奨になるため、エラーが出る場合は代わりにSPI2_HOST , SPI3_HOSTを使用してください。
      cfg.spi_mode = 0;             // SPI通信モードを設定 (0 ~ 3)
      cfg.freq_write = 40000000;    // 送信時のSPIクロック (最大80MHz, 80MHzを整数で割った値に丸められます)
      cfg.freq_read  = 16000000;    // 受信時のSPIクロック
      cfg.spi_3wire  = true;        // 受信をMOSIピンで行う場合はtrueを設定
      cfg.use_lock   = true;        // トランザクションロックを使用する場合はtrueを設定
      cfg.dma_channel = SPI_DMA_CH_AUTO; // 使用するDMAチャンネルを設定 (0=DMA不使用 / 1=1ch / 2=ch / SPI_DMA_CH_AUTO=自動設定)
      // ※ ESP-IDFバージョンアップに伴い、DMAチャンネルはSPI_DMA_CH_AUTO(自動設定)が推奨になりました。1ch,2chの指定は非推奨になります。
      cfg.pin_sclk = 18;            // SPIのSCLKピン番号を設定
      cfg.pin_mosi = 23;            // SPIのMOSIピン番号を設定
      cfg.pin_miso = 19;            // SPIのMISOピン番号を設定 (-1 = disable)
      cfg.pin_dc   = 27;            // SPIのD/Cピン番号を設定  (-1 = disable)
     // SDカードと共通のSPIバスを使う場合、MISOは省略せず必ず設定してください。
//*/
/*
// I2Cバスの設定
      cfg.i2c_port    = 0;          // 使用するI2Cポートを選択 (0 or 1)
      cfg.freq_write  = 400000;     // 送信時のクロック
      cfg.freq_read   = 400000;     // 受信時のクロック
      cfg.pin_sda     = 21;         // SDAを接続しているピン番号
      cfg.pin_scl     = 22;         // SCLを接続しているピン番号
      cfg.i2c_addr    = 0x3C;       // I2Cデバイスのアドレス
//*/

<略>

<サンプルコード抜粋 終わり>
ということで、サンプルコードの解説コメントを参考にコーディングしました。

<今回のソースコード>

#include <M5Atom.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include <Wire.h>


/// @brief グローバル変数
int cnt;

// コンストラクタを作成し、ここで各種設定を行います。
// クラス名を変更した場合はコンストラクタも同じ名前を指定してください。
/*
 クラス名は"LGFX"から別の名前に変更しても構いません。
 AUTODETECTと併用する場合は"LGFX"は使用されているため、LGFX以外の名前に変更してください。
 また、複数枚のパネルを同時使用する場合もそれぞれに異なる名前を付けてください。
 ※ クラス名を変更する場合はコンストラクタの名前も併せて同じ名前に変更が必要です。

 名前の付け方は自由に決めて構いませんが、設定が増えた場合を想定し、
 例えばESP32 DevKit-CでSPI接続のILI9341の設定を行った場合、
  LGFX_DevKitC_SPI_ILI9341
 のような名前にし、ファイル名とクラス名を一致させておくことで、利用時に迷いにくくなります。
*/
class LGFX_SSD1306 : public lgfx::LGFX_Device 
{
  lgfx::Panel_SSD1306   _panel_instance;  // SSD1306構造体のインスタンス
  lgfx::Bus_I2C   _bus_instance;          // バス設定用の構造体を取得します。

  // コンストラクタ
  public:
    LGFX_SSD1306()
    {  
      { //I2Cバスクラスの構造体に設定。
        auto cfg = _bus_instance.config();  // I2Cバス構造体インスタンスの取得
        cfg.i2c_port    = 1;          // 使用するI2Cポートを選択 (0 or 1)Wire0, Wire1の選択
        cfg.freq_write  = 400000;     // 送信時のクロック クロック周波数
        cfg.freq_read   = 400000;     // 受信時のクロック クロック周波数
        cfg.pin_sda     = 25;         // SDAを接続しているピン番号 SDAピン
        cfg.pin_scl     = 21;         // SCLを接続しているピン番号 SCLピン
        cfg.i2c_addr    = 0x3C;       // I2Cデバイスのアドレス スレーブアドレス

        _bus_instance.config(cfg);    //設定値をバスに反映します。
        _panel_instance.setBus(&_bus_instance); //バスをパネルにセットします。
      }
      { // 表示パネル制御の設定を行います。
        auto cfg = _panel_instance.config();  
        // 以下はST7735やILI9163のようにピクセル数が可変のドライバで表示がずれる場合にのみ設定してください。
        //cfg.memory_width  = 128;      // ドライバICがサポートしている最大の幅
        //cfg.memory_height =  64;      // ドライバICがサポートしている最大の高さ
        cfg.offset_rotation = 2;      //回転方向のオフセット 0~7 (4~7は上下反転)
        _panel_instance.config(cfg);  //再設定
      }
      setPanel(&_panel_instance);     // 使用するパネルをセットします。
    }
};

//LovyanGFXインスタンス*************************************************
static LGFX_SSD1306 lcd; 
static LGFX_Sprite canvas(&lcd);  //スプライトバッファ

//setup****************************************************************
void setup()
{
  int i;
  //ATOM初期化---------------------------------------------------------
  M5.begin(true, false, true); //初期化(UART, I2C wire0 , LED)
  //ATOM RGBLED点滅----------------------------------------------------
  for(i=0; i<=3; i++)
  {
    M5.dis.drawpix(0, 0x000FFF000);
    delay(300);
    M5.dis.drawpix(0, 0x000000);
    delay(300); 
  }
  //SSD1306初期化 LovyanGFX--------------------------------------------
  lcd.init();
  canvas.createSprite(lcd.width(), lcd.height());
  canvas.setTextWrap(false);       //自動折返し:無し
  canvas.fillScreen(TFT_BLACK);    //背景塗り潰し
  canvas.setTextColor(TFT_WHITE);  //文字色
  //グローバル変数 カウント値初期化-------------------------------------
  cnt=0;
  //Wire0初期化 PCF8574 IOエキスパンダー ------------------------------------
  int err = Wire.begin(26,32,400000UL);
  Serial.printf("Wire0 begin err=%d\r",err);
}

//グローバル変数********************************************************
uint colorNumber=0;//RGBLEDの色配列指定
int color[3]={0xFF0000,0x008000, 0x0000FF};//RGBLED Red(8),Green(8),Blue(8)

//メインルーチン********************************************************
void loop() 
{
  uint8_t err;
  
  //RGBLED-------------------------------------
  M5.dis.drawpix(0,color[colorNumber++]);
  if(colorNumber==3)
  {
    colorNumber=0;
  }

  //PCF8574 LED lightening---------------------
  Wire.beginTransmission(0x25);
  Wire.write(0xAA);
  err=Wire.endTransmission(1);
  //Serial.printf("err0=%d\r", err);

  //SSD1306 表示---------------------------------
  //canvas.startWrite();
  canvas.fillScreen(TFT_BLACK);    // 背景塗り潰し
  canvas.setTextSize(1);
  canvas.setCursor(0,0, &Font7);
  canvas.printf("%03d",cnt);
  canvas.pushSprite(0,0);
  //canvas.endWrite();

  cnt++;
  delay(500);

  //PCF8574 LED lightening---------------------
  Wire.beginTransmission(0x25);
  Wire.write(0x55);
  err=Wire.endTransmission(1);
  //Serial.printf("err1=%d\r", err);

  delay(500);
}

<プロジェクトに追加したライブラリ>

・M5ATOM by M5Stack
・FastLED
by Daniel Garcia
・LovyanGFX by lovyan03
以上3つのライブラリをPlatformIOのプロジェクトに追加します。

<簡単な、コード解説>

1.ATOMLiteのRGBLEDの色を赤、緑、青の順番で点灯させます。
2.PCF8574(IOエキスパンダー)でLED点灯(0xAA)。
3.SSD1306の描画になります。ここで、LovyanGFXを使うことで、とって       も楽に、SSD1306を描画できます。SSD1306は、描画メモリが上から8ページに分割されていて、その都度ページを切り替えて描画します。かなり座標計算が面倒です。そこをLovyanGFXライブラリが、非常に役に立ってくるわけです。図形や、波形を描画するときにLovyanGFXを使うことで、簡単に描画できるはずです。
4.再度、PCF8574のLED点灯(0x55)を送信しています。

<ラベルを張って、ブレッドボードで使いやすくしています。>

IOピンラベルを張ってみました1
IOピンラベルを張ってみました2

最後に、

PICマイコンを使っていた時は、C言語Onlyで開発していたので、C++に慣れていません。なので、C++のテンプレートなどを使ったライブラリーを解読するのに、時間がかかっています。それでも、画像描画や、デバイスドライバを自分で書かなくて済み、また、Wifiなどを使用するときなど、便利なので、オブジェクト指向のコードを解読しながら学習する感じになっています。

<追記(2024年1月2日)Wire1を共有できるようになった。>


Wire1を共有化できた。


LovyanGFXのbus.hppより、以下の関数を使うと、占有を解消できることが
わかりました。
/// 通信トランザクションを開始する。(ペリフェラルを占有する);
virtual void beginTransaction(void) = 0;
startWrite();
/// 通信トランザクションを終了する。(ペリフェラルの占有を終了する);
virtual void endTransaction(void) = 0
endWirte();

改良したソースコード

#include <M5Atom.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include <Wire.h>


/// @brief グローバル変数
int cnt;

// コンストラクタを作成し、ここで各種設定を行います。
// クラス名を変更した場合はコンストラクタも同じ名前を指定してください。
/*
 クラス名は"LGFX"から別の名前に変更しても構いません。
 AUTODETECTと併用する場合は"LGFX"は使用されているため、LGFX以外の名前に変更してください。
 また、複数枚のパネルを同時使用する場合もそれぞれに異なる名前を付けてください。
 ※ クラス名を変更する場合はコンストラクタの名前も併せて同じ名前に変更が必要です。

 名前の付け方は自由に決めて構いませんが、設定が増えた場合を想定し、
 例えばESP32 DevKit-CでSPI接続のILI9341の設定を行った場合、
  LGFX_DevKitC_SPI_ILI9341
 のような名前にし、ファイル名とクラス名を一致させておくことで、利用時に迷いにくくなります。
*/
class LGFX_SSD1306 : public lgfx::LGFX_Device 
{
  lgfx::Panel_SSD1306   _panel_instance;  // SSD1306構造体のインスタンス
  lgfx::Bus_I2C   _bus_instance;          // バス設定用の構造体を取得します。

  // コンストラクタ
  public:
    LGFX_SSD1306()
    {  
      { //I2Cバスクラスの構造体に設定。
        auto cfg = _bus_instance.config();  // I2Cバス構造体インスタンスの取得
        cfg.i2c_port    = 1;          // 使用するI2Cポートを選択 (0 or 1)Wire0, Wire1の選択
        cfg.freq_write  = 400000;     // 送信時のクロック クロック周波数
        cfg.freq_read   = 400000;     // 受信時のクロック クロック周波数
        cfg.pin_sda     = 25;         // SDAを接続しているピン番号 SDAピン
        cfg.pin_scl     = 21;         // SCLを接続しているピン番号 SCLピン
        cfg.i2c_addr    = 0x3C;       // I2Cデバイスのアドレス スレーブアドレス

        _bus_instance.config(cfg);    //設定値をバスに反映します。
        _panel_instance.setBus(&_bus_instance); //バスをパネルにセットします。
      }
      { // 表示パネル制御の設定を行います。
        auto cfg = _panel_instance.config();  
        // 以下はST7735やILI9163のようにピクセル数が可変のドライバで表示がずれる場合にのみ設定してください。
        //cfg.memory_width  = 128;      // ドライバICがサポートしている最大の幅
        //cfg.memory_height =  64;      // ドライバICがサポートしている最大の高さ
        cfg.offset_rotation = 2;      //回転方向のオフセット 0~7 (4~7は上下反転)
        _panel_instance.config(cfg);  //再設定
      }
      setPanel(&_panel_instance);     // 使用するパネルをセットします。
    }
};

//LovyanGFXインスタンス*************************************************
static LGFX_SSD1306 lcd; 
static LGFX_Sprite canvas(&lcd);  //スプライトバッファ

//setup****************************************************************
void setup()
{
  int i;
  //ATOM初期化---------------------------------------------------------
  M5.begin(true, false, true); //初期化(UART, I2C wire0 , LED)
  //ATOM RGBLED点滅----------------------------------------------------
  for(i=0; i<=3; i++)
  {
    M5.dis.drawpix(0, 0x000FFF000);
    delay(300);
    M5.dis.drawpix(0, 0x000000);
    delay(300); 
  }
  //SSD1306初期化 LovyanGFX--------------------------------------------
  lcd.init();
  canvas.createSprite(lcd.width(), lcd.height());
  canvas.setTextWrap(false);       //自動折返し:無し
  canvas.fillScreen(TFT_BLACK);    //背景塗り潰し
  canvas.setTextColor(TFT_WHITE);  //文字色
  //グローバル変数 カウント値初期化-------------------------------------
  cnt=0;
  //Wire0初期化 PCF8574 IOエキスパンダー ------------------------------------
  //int err = Wire.begin(26,32,400000UL);
  //Serial.printf("Wire0 begin err=%d\r",err);
}

//グローバル変数********************************************************
uint colorNumber=0;//RGBLEDの色配列指定
int color[3]={0xFF0000,0x008000, 0x0000FF};//RGBLED Red(8),Green(8),Blue(8)

//メインルーチン********************************************************
void loop() 
{
  uint8_t err;
  
  //RGBLED-------------------------------------
  M5.dis.drawpix(0,color[colorNumber++]);
  if(colorNumber==3)
  {
    colorNumber=0;
  }

  //PCF8574 LED lightening---------------------
  Wire1.beginTransmission(0x25);
  Wire1.write(0xAA);
  err=Wire1.endTransmission(1);
  //Serial.printf("err0=%d\r", err);

  //SSD1306 表示---------------------------------
  //canvas.startWrite();
  canvas.startWrite();//通信を開始する。(ペリフェラルを占有する)
  canvas.fillScreen(TFT_BLACK);    // 背景塗り潰し
  canvas.setTextSize(1);
  canvas.setCursor(0,0, &Font7);
  canvas.printf("%03d",cnt);
  canvas.pushSprite(0,0);
  canvas.endWrite();//通信を終了する。(ペリフェラルの占有を終了する);
  //canvas.endWrite();

  cnt++;
  delay(500);

  //PCF8574 LED lightening---------------------
  Wire1.beginTransmission(0x25);
  Wire1.write(0x55);
  err=Wire1.endTransmission(1);
  //Serial.printf("err1=%d\r", err);

  delay(500);
}


いいなと思ったら応援しよう!