Arduino Uno Rev3とRTC、SSD1309搭載の2.42インチとSSD1306搭載の0.96インチOLED(有機ELディスプレイ)で作った時計の時分秒や年月日、曜日といった文字をu8g.drawXBMP()で描画表示するスケッチ(プログラム)作成時のメモです。
準備したパーツ、I2Cバス用双方向電圧レベル変換モジュールを追加
「ESP32-DevkitCとDS3231SN RTCモジュールを使ったOLED時計」の「ESP32-DevkitC」を「Arduino Uno Rev3」に置き換えました。ArduinoのI2C信号レベルに合わせて5Vから3.3Vへの双方向電圧レベル変換とU8glib.h対応のスケッチ修正を行います。
OLEDの仕様、集めたパーツ
ネット通販(Amazon など)で集めたOLEDの仕様です。サイズ、発光色の異なる2個で試しました。
# | 発光色 | サイズ解像度 | 接続方式(I2Cアドレス) | ドライバーIC | 電源電圧(V) |
1 | 2.42インチ 黄色 | 128×64 | I2Cに設定変更(0x3C) ※デフォルトはSPI | SSD1309 | 3.3 |
2 | 0.96インチ 青色 | 128×64 | I2C(0x3C) | SSD1306 | 3.3〜5.0 |
OLED以外のパーツです(No.1、2は秋月電子通商、他はAmazon など)。モジュールの電源電圧とI2Cの電圧レベルは3.3Vで統一です。ArduinoのI2C電圧レベルは5Vなので双方向電圧レベル変換モジュールを入れて3.3Vにしています。
# | パーツ | 個数 |
1 | Arduino Uno Rev3 | 1 |
2 | I2Cバス用双方向電圧レベル変換モジュール(PCA9306) プルアップ抵抗付き(1kΩx4個) | 1 |
3 | DS3231SN I2C RTCモジュール | 1 |
4 | コイン形リチウムイオン2次電池「LIR2032」(4個入り) | 1 |
5 | サンハヤト ニューブレッドボード SAD-101 | 1 |
6 | ジャンパーワイヤ(オス-オス、メス-オス) | 適量 |
7 | USBケーブル(USB A オス to B オス、PC接続用) | 1 (手持ち) |
大型のOLEDは消費電流も増えます。Arduino Unoのバージョンによっては3.3V出力が不足するかもしれません。Uno Rev3は3.3VのレギュレータとしてLP2985-33DBVRを搭載しているのでマージンがあったものと思います。
2.42インチOLED(有機ELディスプレイ)をSPI接続からI2C接続に変更
購入した2.42インチOLEDはSPI接続がデフォルトでした。転送速度はSPIが高速ですが、今回は時計用途なので配線数が少ないI2C接続に変更しました。
ブレッドボード上で結線
モジュール間の結線は、I2Cの2線(SCL、SDA)と電源の2線(3.3V、GND)の4線なので容易です。Arduino Uno Rev3とブレッドボード上に配置した双方向電圧レベル変換モジュール、DS3231SN RTCモジュール、OLEDを4色に色分けしたジャンパーワイヤでつないでいきます。
それぞれのOLED単体でも動作しますが、今回両方のOLEDを同じI2CアドレスのままI2Cバスにつないで、一つのスケッチで同時表示しています。
OLED表示のスケッチ、漢字は16×16ピクセルのHEXデータをu8g.drawXBMP()で描画
以前、U8glib.hを使ってOLEDでFM局名の漢字表示を行ったのですがメモリ消費も少なく使い勝手も良かったで、今回も利用させていただきました。
ESP32-DevkitCにはWiFi機能があったのでNTPサーバとの時刻合わせを行いましたが、今回のArduino版のスケッチでは時刻合わせに「Arduino DS3232RTC Library sample sketch」のcompileTime()を利用させていただきました。「add fudge factor to allow for compile time」とあるように正確な時刻設定には微調整が必要です。
RTCはバッテリーバックアップが行われているので電源が復旧した際にスケッチ実行時刻:compileTimeに初期化されないように、RTC.getよりもcompileTimeが新しい時のみcompiletimeをRTC.setするように if 文を挿入しました。
OLEDモジュール表示に必要なライブラリ「U8glib.h」は、Arduino IDEメニューの ツール –> ライブラリの管理 からライブラリマネージャで「U8glib」を検索してインストールします。
u8g.setFont()で使うフォント見本は fontsize | olikraus/u8glib を参考にさせていただきました。今回は英数字の表示用フォントとして「u8g_font_fub17」を使っています。
OLED時計で使った漢字は「時、分、秒、年、月、日、火、水、木、金、土」と少ないので、メモリ消費を抑えるために、利用する特定の文字のみを漢字で表示します。これらの漢字は画像(BMPファイル)から変換した16×16ピクセルのHEXデータをu8g.drawXBMP()で描画します。
最大32256バイトのフラッシュメモリのうち、スケッチが15800バイト(48%)を使っています。
最大2048バイトのRAMのうち、グローバル変数が532バイト(25%)を使っていて、ローカル変数で1516バイト使うことができます。
記号や漢字のBMPファイルは、Windows標準アプリのペイント3Dで100×100ピクセル程度で作った後、キャンパスを16×16ピクセルに変更して圧縮します。このBMPファイルをHEXデータに変換します。HEXデータへの変換にはProgramResource.netサイトの「nfBmptoHex.exe」を利用させていただきました。
時分秒、月日などが数字1桁の場合、2桁分の描画スペースの片側に寄ってバランスが悪いので、中央に配置されるように void disp_centre(int digit) で数字の先頭にスペースを加えてセンターリングしています。
arduino_ds3231sn_oled_clock.ino
※ここをクリックするとコード表示を開閉できます。
#include <Wire.h>
#include <U8glib.h> // U8gライブラリ
#include <time.h> // Timeライブラリ
#include <DS3232RTC.h> // DS3232、DS3231用ライブラリ
U8GLIB_SSD1306_128X64 u8g(U8G_I2C_OPT_DEV_0 | U8G_I2C_OPT_NO_ACK | U8G_I2C_OPT_FAST);
DS3232RTC myRTC(false);
// 漢字画像(BMP)から変換したHEXデータ
const unsigned char yar_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0xF8, 0x3F, 0x0C, 0x02, 0x06, 0x02, 0xF8, 0x3F, 0x08, 0x02, 0x08, 0x02, 0x08, 0x02, 0x08, 0x03, 0xFE, 0x7F, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, };
const unsigned char mon_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xF8, 0x3F, 0x18, 0x30, 0x18, 0x30, 0x18, 0x30, 0xF8, 0x3F, 0x18, 0x30, 0x18, 0x30, 0xF8, 0x3F, 0x18, 0x30, 0x08, 0x30, 0x0C, 0x30, 0x04, 0x30, 0x06, 0x1F, 0x00, 0x00, };
const unsigned char sun_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xFC, 0x3F, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0xFC, 0x3F, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0xF8, 0x1F, 0x00, 0x00, };
const unsigned char tue_bmp[] PROGMEM = {0x00, 0x00, 0x80, 0x00, 0x80, 0x01, 0x84, 0x21, 0x84, 0x31, 0x8C, 0x31, 0x88, 0x31, 0x88, 0x11, 0x88, 0x19, 0x80, 0x01, 0xC0, 0x02, 0x60, 0x06, 0x30, 0x1C, 0x0C, 0x70, 0x02, 0x00, 0x00, 0x00, };
const unsigned char wed_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0xBE, 0x61, 0xA0, 0x1B, 0xA0, 0x0F, 0xB0, 0x07, 0xB0, 0x05, 0x90, 0x0D, 0x98, 0x19, 0x8C, 0x31, 0x86, 0x61, 0x90, 0x01, 0xE0, 0x01, 0x00, 0x00, };
const unsigned char thu_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0xFC, 0x7F, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x0D, 0xB0, 0x09, 0x98, 0x19, 0x8C, 0x71, 0x82, 0x61, 0x80, 0x01, 0x80, 0x01, 0x00, 0x00, };
const unsigned char fri_bmp[] PROGMEM = {0x00, 0x00, 0xC0, 0x03, 0x60, 0x06, 0x30, 0x1C, 0x1C, 0x30, 0xF7, 0xEF, 0x80, 0x01, 0x80, 0x01, 0xFE, 0x7F, 0x80, 0x01, 0x8C, 0x31, 0x98, 0x11, 0x98, 0x19, 0x90, 0x09, 0xFE, 0x7F, 0x00, 0x00, };
const unsigned char sat_bmp[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xFC, 0x3F, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xFE, 0x7F, 0x00, 0x00, };
const unsigned char jik_bmp[] PROGMEM = {0x00, 0x04, 0x1E, 0x04, 0xD3, 0x7F, 0x13, 0x04, 0x13, 0x04, 0x13, 0x04, 0xD3, 0x7F, 0x1F, 0x20, 0x13, 0x30, 0xD3, 0x7F, 0x93, 0x21, 0x13, 0x23, 0x13, 0x22, 0x1E, 0x20, 0x00, 0x3F, 0x00, 0x00, };
const unsigned char fun_bmp[] PROGMEM = {0x00, 0x00, 0x30, 0x0C, 0x18, 0x08, 0x08, 0x18, 0x0C, 0x30, 0x06, 0x60, 0xFA, 0x5F, 0x60, 0x30, 0x40, 0x30, 0x60, 0x30, 0x20, 0x30, 0x30, 0x30, 0x18, 0x30, 0x0C, 0x30, 0xC6, 0x1F, 0x00, 0x00, };
const unsigned char byo_bmp[] PROGMEM = {0x00, 0x00, 0x3E, 0x04, 0x08, 0x04, 0x88, 0x25, 0xBE, 0x65, 0x8C, 0x65, 0x8C, 0x44, 0x8C, 0x44, 0x9C, 0xE4, 0x6E, 0xE4, 0x2A, 0x24, 0x08, 0x37, 0x08, 0x18, 0x08, 0x0C, 0x88, 0x03, 0x00, 0x00, };
const char* weekStr[7] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
void setup(void) {
u8g.setColorIndex(1);
u8g.setContrast(0); // 明るさも多少変わる(0~255)@2022-05-19追加
struct tm timeInfo;
myRTC.begin();
// myRTC.get()よりcompileTime()が新しい時はcompiletimeをmyRTC.set
time_t time_now, compiletime;;
time_now = myRTC.get();
compiletime = compileTime();
if (time_now < compiletime) {
myRTC.set(compiletime); // set compiled time to RTC
}
// DS3231に手動設定(書込み時間を加算した値で調整)
// setTime(9, 45, 0, 4, 5, 2022); // 時、分、秒、日、月、年
// myRTC.set(now());
}
void loop() {
tmElements_t tm;
myRTC.read(tm);
u8g.firstPage();
do {
u8g.setFont(u8g_font_fub17);
// 時分秒の描画
u8g.setPrintPos(0,17);
disp_centre(tm.Hour);
u8g.drawXBMP(28, 3, 16, 16, jik_bmp);
u8g.setPrintPos(45,17);
disp_centre(tm.Minute);
u8g.drawXBMP(71, 3, 16, 16, fun_bmp);
u8g.setPrintPos(87,17);
disp_centre(tm.Second);
u8g.drawXBMP(113, 3, 16, 16, byo_bmp);
// 年月日の描画
u8g.setPrintPos(0,40);
u8g.print(tm.Year + 1970);
u8g.drawXBMP(54, 25, 16, 16, yar_bmp);
u8g.setPrintPos(0,64);
disp_centre(tm.Month);
u8g.drawXBMP(28, 48, 16, 16, mon_bmp);
u8g.setPrintPos(48,64);
disp_centre(tm.Day);
u8g.drawXBMP(76, 48, 16, 16, sun_bmp);
// 曜日の描画
u8g.setPrintPos(94,64);
u8g.print("(");
u8g.setPrintPos(121,64);
u8g.print(")");
if ( (weekStr[tm.Wday - 1]) == "Sun" ) {
u8g.drawXBMP(104, 48, 16, 16, sun_bmp);
}else if ( (weekStr[tm.Wday - 1]) == "Mon" ) {
u8g.drawXBMP(104, 48, 16, 16, mon_bmp);
}else if ( (weekStr[tm.Wday - 1]) == "Tue" ) {
u8g.drawXBMP(104, 48, 16, 16, tue_bmp);
}else if ( (weekStr[tm.Wday - 1]) == "Wed" ) {
u8g.drawXBMP(104, 48, 16, 16, wed_bmp);
}else if ( (weekStr[tm.Wday - 1]) == "Thu" ) {
u8g.drawXBMP(104, 48, 16, 16, thu_bmp);
}else if ( (weekStr[tm.Wday - 1]) == "Fri" ) {
u8g.drawXBMP(104, 48, 16, 16, fri_bmp);
}else if ( (weekStr[tm.Wday - 1]) == "Sat" ) {
u8g.drawXBMP(104, 48, 16, 16, sat_bmp);
}
} while ( u8g.nextPage() );
}
// 1桁数字は先頭にスペースを加えてセンターリング
void disp_centre(int digit) {
if(digit < 10)
u8g.print(" ");
u8g.print(digit);
}
// function to return the compile date and time as a time_t value
// from alarm_ex1.ino in Arduino DS3232RTC Library sample sketch by Jack Christensen.
time_t compileTime() {
const time_t FUDGE(10); //fudge factor to allow for upload time, etc. (seconds, YMMV)
const char *compDate = __DATE__, *compTime = __TIME__, *months = "JanFebMarAprMayJunJulAugSepOctNovDec";
char compMon[4], *m;
strncpy(compMon, compDate, 3);
compMon[3] = '\0';
m = strstr(months, compMon);
tmElements_t tm;
tm.Month = ((m - months) / 3 + 1);
tm.Day = atoi(compDate + 4);
tm.Year = atoi(compDate + 7) - 1970;
tm.Hour = atoi(compTime);
tm.Minute = atoi(compTime + 3);
tm.Second = atoi(compTime + 6);
time_t t = makeTime(tm);
return t + FUDGE; //add fudge factor to allow for compile time
}