M5Stack は多機能なマイコンモジュールである。スピーカーとか microSD スロットとかがはじめからついているが、リアルタイムクロック (RTC) がついておらず、時計として使うには 毎回 Wi-Fi 経由で NTP を用いて時刻情報を取得する とか、iPhone などと BLE で接続し Current Time Service を用いて時刻情報を取得する とかのテクニックが必要である。これでは不便なので Proto モジュール 上に DS1302 という IC を使用して RTC を構築した。
ポイントは数点ある。
- GPIO のうちどのポートを使うか。
- ボタン電池をどうやって Proto モジュールに収めるか。
1つ目については M5 Stack ハマりどころ に詳しい。要は:
- 使うべきではないポート
- 注意して使うポート
- GPIO 34-39, 入力オンリー
- GPIO 2, [GPIO 0]が0の時、これも0でリセットされるとダウンロードモードになる。
- GPIO 12, 起動時に1だと LDOが1.8V, 0だと3.3Vになる。
- GPIO 15 , GPIO 5 標準でPULLUP 起動時に設定すると SDIOスレーブタイミングの設定。
ひとまず今回は SCLK = 13, IO = 5, CE = 17 とすることに決めた。
2つ目についてはコイン電池ホルダーをいくつか試し、タカチ電機工業 表面実装型 コイン電池ホルダー SMTUシリーズ の CR2032 用 ならば入ったのでそれを利用した。 Proto モジュール内部で許容される高さはだいたい 5.5 mm ぐらいっぽい(上のホルダーは 5.4 mm)。
下に配線図と実際の写真を載せる。めちゃめちゃ汚い配線だが許容した。
いろいろなところからコードをお借りしてガッチャンしてとりあえず動くコードを書いた。
#include <M5Stack.h> #include <DS1302.h> // DS1302 用のモジュールを https://github.com/msparks/arduino-ds1302 からダウンロードして使用可能にしておく。 #define incPin 39 // (+) Inc. button #define decPin 38 // (−) Dec. button #define entPin 37 // Enter button // Timer function: https://lang-ship.com/blog/work/esp32-timer/ hw_timer_t * timer = NULL; void IRAM_ATTR onTimer() { // M5 LCD の左上のカーソル位置は (0, 0) で、右下のカーソル位置は (312, 232) である。 // 1文字の大きさが (6,8) であることと液晶サイズが (320,240) であることとに依拠していると思われる。 // これをはみ出していたら clear する。 if (M5.Lcd.getCursorX() >= 320 || M5.Lcd.getCursorY() >= 240) { M5.Lcd.clear(); M5.Lcd.setCursor(0,0); // https://lang-ship.com/reference/unofficial/M5StickC/Tips/M5Display/ } } // DS1302 // https://github.com/msparks/arduino-ds1302/blob/master/examples/set_clock/set_clock.ino を改変。 namespace { // Set the appropriate digital I/O pin connections. These are the pin // assignments for the Arduino as well for as the DS1302 chip. See the DS1302 // datasheet: // // http://datasheets.maximintegrated.com/en/ds/DS1302.pdf const int kCePin = 17; // Chip Enable const int kIoPin = 5; // Input/Output const int kSclkPin = 13; // Serial Clock // Create a DS1302 object. DS1302 rtc(kCePin, kIoPin, kSclkPin); String dayAsString(const Time::Day day) { switch (day) { case Time::kSunday: return "Sunday"; case Time::kMonday: return "Monday"; case Time::kTuesday: return "Tuesday"; case Time::kWednesday: return "Wednesday"; case Time::kThursday: return "Thursday"; case Time::kFriday: return "Friday"; case Time::kSaturday: return "Saturday"; } return "(unknown day)"; } void printTime() { // Get the current time and date from the chip. Time t = rtc.time(); // Name the day of the week. const String day = dayAsString(t.day); // Format the time and date and insert into the temporary buffer. char buf[50]; snprintf(buf, sizeof(buf), "%s %04d-%02d-%02d %02d:%02d:%02d", day.c_str(), t.yr, t.mon, t.date, t.hr, t.min, t.sec); // Print the formatted string to serial so we can see the time. M5.Lcd.println(buf); } } // namespace //曜日を求める。 https://edu.clipper.co.jp/pg-2-47.html // 0 = 日曜日 int subZeller( int y, int m, int d ) { if( m < 3 ) { y--; m += 12; } return ( y + y/4 - y/100 + y/400 + ( 13*m + 8 )/5 + d )%7; } Time::Day subZellerForDS1302Library( int y, int m, int d) { switch (subZeller(y, m, d)) { case 0: return Time::kSunday; case 1: return Time::kMonday; case 2: return Time::kTuesday; case 3: return Time::kWednesday; case 4: return Time::kThursday; case 5: return Time::kFriday; case 6: return Time::kSaturday; } } // 時計の設定。 http://radiopench.blog96.fc2.com/blog-entry-923.html を改変。 char buff[10]; // 文字列操作バッファ String yymmdd = "yyyy/mm/dd"; // 年月日文字列 String hhmmss = "hh:mm/ss"; // 時分秒文字列 int yy, mo, dd, hh, mi, ss; // 時刻の要素 void getDateTime(){ // RTCに値を読む、日時の文字列を作成する int x; yymmdd = ""; hhmmss = ""; Time t = rtc.time(); x = t.yr%100; // 年 yy = x; sprintf(buff, "20%02d", x); // 20に続いて右詰め2桁、1桁なら先頭にゼロ yymmdd += buff; x = t.mon; // 月 mo = x; sprintf(buff, "/%02d", x); // / に続けて右詰め2桁 yymmdd += buff; x = t.date; // 日 dd = x; sprintf(buff, "/%02d", x); // / に続けて右詰め2桁 yymmdd += buff; // 年月日の文字列完成(ex:2019/02/28) x = t.hr; // 時 hh = x; sprintf(buff, "%02d", x); // 右詰め2桁 hhmmss += buff; x = t.min; // 分 mi = x; sprintf(buff, ":%02d", x); // : に続けて右詰め2桁、 hhmmss += buff; x = t.sec; // 秒 sprintf(buff, ":%02d", x); // :に続けて右詰め2桁 hhmmss += buff; // 時分秒の文字列完成(ex:01:02:03) } void oledDisp2Chr(int x, int y, int val) { // OLEDの指定場所に2桁の値を表示 sprintf(buff, "%02d", val); // データーを10進2桁0フィル文字列に変換 M5.Lcd.setCursor(x, y); // カーソルを指定位置に合わせて M5.Lcd.print(buff); // 数値を書き込み } int oledRW(int x, int y, int d, int stepD, int minD, int maxD) { // OLEから値を入力 // OLEDの指定位置に2桁右詰めで変数の値を表示。ボタン操作で値を増減し、 // Ent入力で値を確定し戻り値として返す。表示位置の左上をx, y 座標で指定 // 操作位置は下線で表示。値は上下限の範囲でサーキュレート。文字サイズは2倍角(12x16画素) // 引数:x座標、y座標、変更したい変数、変更ステップ量、下限値、上限値 oledDisp2Chr(x, y, d); // 画面の指定位置に数値を2桁表示(下線付き) while (digitalRead(entPin) == LOW) { // enterボタンが押されていたら離されるまで待つ } delay(30); while (digitalRead(entPin) == HIGH) { // enterボタンが押されるまで以下を実行 if (digitalRead(incPin) == 0) { // + ボタンが押されていたら d = d + stepD; // x を指定ステップ増加 if (d > maxD) { // 上限超えたら下限へサキュレート d = minD; } oledDisp2Chr(x, y, d); // 画面の指定位置に数値を2桁表示(下線付き) while (digitalRead(incPin) == 0) { // + ボタンが離されるまで待つ } delay(30); } if (digitalRead(decPin) == 0) { // - ボタンが押されていたら d = d - stepD; // x を指定ステップ減らす if (d < minD) { // 下限以下なら上限へサーキュレート d = maxD; } oledDisp2Chr(x, y, d); // 画面の指定位置に数値を2桁表示(下線付き) while (digitalRead(decPin) == 0) { // - ボタンが離されるまで待つ } delay(30); } } delay(30); return d; // 戻り値 } void clockAdjust() { // OLEDとボタンスイッチで時刻を合わせる M5.Lcd.println("Clock adj."); // 時刻合わせ開始表示 while (digitalRead(entPin) == LOW) { // entボタンが離されるまで待つ } getDateTime(); // 現在時刻を取得 M5.Lcd.clear(); // 画面を消して M5.Lcd.setCursor(0, 0); M5.Lcd.println(yymmdd); // 現在の年月日を表示 hhmmss[6] = '-'; // 秒の桁に--を表示 hhmmss[7] = '-'; M5.Lcd.setCursor(13, 16); // (表示位置要調整) M5.Lcd.println(hhmmss); // 時刻表示 // x, y座標, 値, ステップ, 下限, 上限を指定して時計の設定値を入力 yy = oledRW(12, 0, yy, 1, 0, 40); // 年の値を入力 mo = oledRW(30, 0, mo, 1, 1, 12); // 月の入力 dd = oledRW(48, 0, dd, 1, 1, 31); // 日の入力。存在しない日(ex:2/31)も入力可能だが動作は不定 hh = oledRW(13, 16, hh, 1, 0, 23); // 時 (表示位置要調整) mi = oledRW(31, 16, mi, 1, 0, 59); // 分 (表示位置要調整) // Initialize a new chip by turning off write protection and clearing the // clock halt flag. These methods needn't always be called. See the DS1302 // datasheet for details. rtc.writeProtect(false); rtc.halt(false); // Make a new time object to set the date and time. // Sunday, September 22, 2013 at 01:38:50. Time t(2000+yy, int(mo), int(dd), int(hh), int(mi), 0, subZellerForDS1302Library(2000+yy, mo, dd)); // Set the time and date on the chip. rtc.time(t); } void setup() { // put your setup code here, to run once: M5.begin(); // Mute Speaker Noise: https://asukiaaa.blogspot.com/2020/03/m5stack-disable-speaker.html M5.Speaker.begin(); M5.Speaker.mute(); // Set ESP32 Timer: https://lang-ship.com/blog/work/esp32-timer/ timer = timerBegin(0, 80, true); timerAttachInterrupt(timer, &onTimer, true); timerAlarmWrite(timer, 1000000, true); // us timerAlarmEnable(timer); if (digitalRead(entPin) == LOW){ // 起動時にEnt.ボタンが押されていたら clockAdjust(); // OLED画面と押しボタンを使って時刻合わせ。 } } void loop() { // put your main code here, to run repeatedly: printTime(); delay(1000); }
ふつうに起動するとずっと現在時刻を表示しつづける。ボタンC(一番右のボタン)を押しながら電源を入れると時計設定モードに入ることができる。
Misc
Proto モジュール、GND を引き出すことが想定されてないとしか思えない配置でつらい。
はじめは PCF8523 という IC を使用しようと思ったが、この IC の i2c address 0x68 と M5Stack Gray に標準搭載されている MPU9250 の i2c address 0x68 がぶつかってしまい使用不可であった。
ちなみに M5Stick-C ならば RTC がビルトインされているようだ。M5Stack にも載せてくれればよかったのに。
2021-08-16 追記
M5Stack 用 RTC モジュール基盤が Switch Science で販売されている。IC は PCF8563 で、I2C アドレスは 0x51 のようであり MPU9250 とのアドレス衝突もなさそう。