ATOM Lite で I2C OLEDディスプレイを使う ~ LovyanGFXライブラリが最強すぎる
少し前に、Raspberry PiでI2C OLEDディスプレイを使用しました。
Raspberry PiでC言語にこだわると、作りこまなければならないことが多いです。そこで、LovyanGFXライブラリが使えるArduino IDEでATOM Liteを使った時計プログラムを作成してみました。
1.LovyanGFXについて
ESP32とSPI, I2C, 8ビットパラレル接続のディスプレイ / ESP8266とSPI接続のディスプレイ / ATSAMD51とSPI接続のディスプレイの組み合わせで動作するグラフィックライブラリです。
LovyanGFX
とにかく、機能が豊富です。そして、多くの液晶ディスプレイやOLEDに対応していることが特徴です。おかげで、電子部品を扱う店で売っている液晶やOLEDが結構使えるようになりました。
また、ディスプレイデバイスだけではなく、M5Stack / M5StickC などのグラフィックライブラリとしても使えます。そのため、標準を使わずに、LovyanGFXを使った方が多彩な表示ができるでしょう。
LovyanGFXライブラリを使うには、Arduino IDEのライブラリマネージャーで「LovyanGFX by lovyan03」をインストールしてください。
2.ATOM Liteと0.96インチOLEDを使ってアナログ時計を作る
SSD1306ドライバを使う0.96インチOLEDでアナログ時計を作ってみます。制御にはATOM Liteを使います。
アナログ盤のひな型は、ChatGPTに作ってもらいました。さらに目盛を追加してみたのですが、OLEDの解像度(128×64)では表示が厳しいのでやめました。また、時間の針は60分ごと、分の針は1分ごとに動かす仕様です。この理由は、目盛がないので、物理的なアナログ時計のように動かすと時刻を読み取りづらいからです。
時刻はインターネットからの取得にします。また、LGFX_Deviceクラスの設定値については、以下のサイト様を参考にさせていただきました。
以下、プログラムコードです。
#include <M5Atom.h>
#define LGFX_USE_V1
#include <LovyanGFX.hpp>
#include <WiFi.h>
#include <time.h>
// wifiの設定
const char* ssid = "XXXXXXXXXXXXXXX";
const char* password = "XXXXXXXXXXXXXXX";
// 時計の設定
const char* ntpServer = "ntp.jst.mfeed.ad.jp"; // NTPサーバー
const long gmtOffset_sec = 9 * 3600; // 時差9時間
const int daylightOffset_sec = 0; // サマータイム設定なし
int mode = 0 ; // 0 : analog 1 : digital
static String weekname[7] = { "日", "月", "火", "水", "木", "金", "土"} ;
class LGFX_SSD1306 : public lgfx::LGFX_Device {
lgfx::Panel_SSD1306 _panel_instance;
lgfx::Bus_I2C _bus_instance;
public:
LGFX_SSD1306() {
{
auto cfg = _bus_instance.config();
cfg.i2c_port = 1;
cfg.freq_write = 400000;
cfg.freq_read = 400000;
// cfg.pin_sda = 26; // GROVE
// cfg.pin_scl = 32;
cfg.pin_sda = 21; // ATOM LITE GPIOソケット
cfg.pin_scl = 25;
cfg.i2c_addr = 0x3C;
_bus_instance.config(cfg);
_panel_instance.setBus(&_bus_instance);
}
{
auto cfg = _panel_instance.config();
cfg.memory_width = 128;
cfg.memory_height = 64;
_panel_instance.config(cfg);
}
setPanel(&_panel_instance);
}
};
static LGFX_SSD1306 lcd;
static LGFX_Sprite canvas(&lcd);
void print_wifi_state(){
delay(100);
Serial.println("Wi-Fi接続完了");
Serial.println(WiFi.localIP());
delay(100);
}
void setup_wifi(){
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.println("\nWi-Fi 接続開始");
while (WiFi.status() != WL_CONNECTED) {
delay(100);
}
print_wifi_state();
}
void setup() {
// put your setup code here, to run once:
M5.begin(true, true, true); // 本体初期化(UART, I2C, LED)
lcd.init();
canvas.setTextWrap(false);
canvas.createSprite(lcd.width(), lcd.height());
setup_wifi();
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
delay(1000);
}
void loop() {
// put your main code here, to run repeatedly:
M5.update();
if (M5.Btn.wasPressed()) {
switch (mode) {
case 0 :
mode = 1 ;
break ;
case 1 :
mode = 0 ;
break ;
default :
mode = 0 ;
break ;
}
}
canvas.fillScreen(TFT_BLACK); // 背景塗り潰し
time_t t;
struct tm *tm;
t = time(NULL);
if (t != 0) {
tm = localtime(&t);
}
delay(100);
// 現在の時刻を取得
int hours = tm->tm_hour ;
int minutes = tm->tm_min ;
int seconds = tm->tm_sec ;
if (mode == 0) {
// アナログ表示
// 時計盤の描画
int radius = min(lcd.width(), lcd.height()) / 2 - 1;
int centerX = lcd.width() / 2;
int centerY = lcd.height() / 2;
// 時計盤の外枠を描画
canvas.drawCircle(centerX, centerY, radius, TFT_WHITE);
int hourAngle = map(hours % 12, 0, 12, 0, 360);
int minuteAngle = map(minutes, 0, 60, 0, 360);
int secondAngle = map(seconds, 0, 60, 0, 360);
// 時針の描画
int hourHandLength = radius * 0.5;
int hourHandX = centerX + hourHandLength * sin((hourAngle - 0) * DEG_TO_RAD);
int hourHandY = centerY - hourHandLength * cos((hourAngle - 0) * DEG_TO_RAD);
canvas.drawLine(centerX, centerY, hourHandX, hourHandY, TFT_WHITE);
// 分針の描画
int minuteHandLength = radius * 0.7;
int minuteHandX = centerX + minuteHandLength * sin((minuteAngle - 0) * DEG_TO_RAD);
int minuteHandY = centerY - minuteHandLength * cos((minuteAngle - 0) * DEG_TO_RAD);
canvas.drawLine(centerX, centerY, minuteHandX, minuteHandY, TFT_WHITE);
// 秒針の描画
int secondHandLength = radius * 0.85;
int secondHandX = centerX + secondHandLength * sin((secondAngle - 0) * DEG_TO_RAD);
int secondHandY = centerY - secondHandLength * cos((secondAngle - 0) * DEG_TO_RAD);
canvas.drawLine(centerX, centerY, secondHandX, secondHandY, TFT_WHITE);
} else if (mode == 1) {
// デジタル表示
canvas.setFont(&fonts::Font7);
canvas.setTextSize(0.65) ;
canvas.setCursor(0, 20);
canvas.printf("%02d:%02d", hours, minutes);
}
// 日本語表示
canvas.setTextSize(1.0) ;
canvas.setFont(&fonts::lgfxJapanGothic_16);
canvas.setCursor(0, 0);
canvas.printf(weekname[tm->tm_wday].c_str());
canvas.setCursor(96, 0);
canvas.printf("%2d月", tm->tm_mon + 1);
canvas.setCursor(96, 48);
canvas.printf("%2d日", tm->tm_mday);
canvas.pushSprite(0, 0);
delay(250); // 0.25秒ごとに更新
}
画面を仮想バッファ(LGFX_Sprite)に書き込んでから表示するため、ちらつきが起こりません。ほんと、良くできたグラフィックライブラリです。
3.動作させてみて
動作させてみたのが、一番最初の画像です。実用として作るのであれば、デジタル時計にした方が絶対にいいです。今回は、あくまでもLovyanGFXのテストが目的なので、アナログ時計にしました。⇒両方表示できるソースに入れ替えました。
今回のテストを通して、LovyanGFXを使えば、電子機器の液晶/OLED表示に十分使えそうだと思いました。昔、ある電気製品の液晶部分の表示に携わったことがあるのですけれど、今回のESP32とLovyanGFXを使えば、開発工数を激減できたと思います。もちろん、昔にはなかったものですが、あの頃は大変な思いをしてプログラムをしていましたね。
LovyanGFXのおかげで、いろいろな表示器を使うことができそうです。透過型OLEDを使ってみたいですね。皆さんもチャレンジしてみてください。
OLEDのピン配列とATOM Liteのピン配列が同じ並びであれば、直結もできます。こっちの製品が良いでしょうか。
※この直結方法は、ATOMS3 LITEではピン配列が変わったため、できなくなっています。ATOM LITEを探してください。
動画も作ってみました。