もよいめも

不定期更新ものづくりブログ

【Arduino】RTC(DS3231)を使ってみた 〜2022年令和最新版〜

5分で作ったクソださアイキャッチ

はるか昔、RTCを使ってみた記事を書きました。↓
moyoi-memo.hatenablog.com

それからもう4年。どうやらライブラリもアップデートされ、そのままでは使えなくなってしまったとの報告をうけましたので、今回は改めてDS3231をArduinoで使っていこうと思います!


↓ライブラリのアップデート内容 結構かわってますね。こりゃ前のじゃ動かんな

変わったところ https://github.com/JChristensen/DS3232RTCより引用



使用するRTC

前記事と同じものを使用します。

どうやら3つセットになったみたいですね。そんなにいらね

ちなみにボタン電池(LIR2032)をぶちこむことで、電源を喪失しても時刻を保持することができます。
しかし注意点が有り、充電可能な二次電池であるLIR2032以外の電池を使用すると、電池に充電電圧がかかってやばいことになります。

Arduinoとの接続方法

RTC側     Arduino

VCC  →  5V
GND  →  GND
SDA  →  A4(SDA)
SCL  →  A5(SCL)

以下の画像も参考にしてください。Arduino UNOバージョンも載せておきます。

配線の仕方


プログラムの方法

さて、いつもどおりにライブラリをインストールしましょう。

「DS3232RTC」と「Arduino Time Library」をインストールする必要があるので、zipでダウンロードするファイルは2つです。
こちらからライブラリをZIP形式でダウンロードします。
github.com
github.com

そして、Arduino ideのメニューのスケッチライブラリをインクルード.zip形式のファイルをインストールからダウンロードしたファイルを選択します。
インストールが完了した旨のメッセージが出れば、導入は完了です。
これをダウンロードした2つのファイルで行います。


では次章でRTCの時刻設定をしていきましょう!

RTCの時刻設定

前記事同様、まずは簡単に時刻設定をしてみましょう。

#include <DS3232RTC.h>//ライブラリの読み込み
DS3232RTC myRTC;//クラスのインスタント化


void setup() {
  myRTC.begin();//i2cなどの初期設定
  setTime(15, 13, 0, 2, 12, 2022);//システムの時間設定 (時、分、秒、日、月、年)の順で入力
  myRTC.set(now());//RTCに書き込み
}

void loop() {
}

RTCの現在時刻を確認

とりあえず時刻取得

#include <DS3232RTC.h>//ライブラリの読み込み
DS3232RTC myRTC;//クラスのインスタント化

void setup() {
  Serial.begin(115200);
  myRTC.begin();//i2cなどの初期設定
}

void loop() {
  tmElements_t tm;//tmElements_tk構造体を宣言
  myRTC.read(tm);//RTCから現在時刻を取得して構造体に代入
  Serial.print(tm.Year + 1970, DEC); //年は1970年からの年数で記録されるので1970を足して表示
  Serial.print("/");
  Serial.print(tm.Month, DEC);//月を表示
  Serial.print("/");
  Serial.print(tm.Day, DEC);//日を表示
  Serial.print(" ");
  Serial.print(tm.Hour, DEC);//時を表示
  Serial.print(":");
  Serial.print(tm.Minute, DEC);//分を表示
  Serial.print(":");
  Serial.println(tm.Second, DEC);//秒を表示して改行
  delay(1000);//1間隔で表示させるため1秒待機
}

シリアルモニタを開いてボーレートを115200に設定し、
2022/12/2 15:26:38
2022/12/2 15:26:39
2022/12/2 15:26:40
2022/12/2 15:26:41
....
こんな感じで表示されれば成功です!

もう少しシンプルに表示

ちなみに、↑の書き方だとあまり賢い方法であるとは言えないので、書き直したものが下になります。

↑に比べてだいぶすっきりすると思います。本来のC言語ではこちらの記法が標準的なのですが、Arduinoのprintf関数にそのような機能はないので、
一旦sprintf関数を挟んで文字列に変換してあります。

#include <DS3232RTC.h>//ライブラリの読み込み
DS3232RTC myRTC;//クラスのインスタント化

void setup() {
  Serial.begin(115200);
  myRTC.begin();//i2cなどの初期設定
}

void loop() {
  tmElements_t tm;  //tmElements_tk構造体を宣言
  myRTC.read(tm);   //RTCから現在時刻を取得して構造体に代入

  char buff[30];                                                                                          //文字列一時保存用の配列buffの宣言
  sprintf(buff, "%d/%02d/%02d %02d:%02d:%02d\n", tm.Year + 1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second);  //フォーマットに従った文字列への変換を行いbuffに代入
  Serial.print(buff);                                                                                     //buffを表示
  delay(1000);    
}

曜日も表示

先程の賢く表示を行ったコードに、曜日を表示&時刻を設定するコードを付け足してみました。

#include <DS3232RTC.h>  //ライブラリの読み込み
DS3232RTC myRTC;        //クラスのインスタント化

const char *week_day[] = { "Non", "San", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };  //曜日が格納された定数を宣言

void setup() {
  Serial.begin(115200);
  myRTC.begin();                    //i2cなどの初期設定
  setTime(15, 13, 0, 5, 12, 2022);  //システムの時間設定 (時、分、秒、日、月、年)の順で入力
  myRTC.set(now());                 //RTCに書き込み
}

void loop() {
  tmElements_t tm;  //tmElements_tk構造体を宣言
  myRTC.read(tm);   //RTCから現在時刻を取得して構造体に代入

  char buff[30];                                                                                                                  //文字列一時保存用の配列buffの宣言
  sprintf(buff, "%d/%02d/%02d %02d:%02d:%02d (%s)\n", tm.Year + 1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, week_day[tm.Wday]);  //フォーマットに従った文字列への変換を行いbuffに代入
  Serial.print(buff);                                                                                                             //buffを表示
  delay(1000);                                                                                                                    //1間隔で表示させるため1秒待機
}

2022/12/05 15:14:31 (Mon)
2022/12/05 15:14:32 (Mon)
2022/12/05 15:14:33 (Mon)
2022/12/05 15:14:34 (Mon)
....
こんな感じで表示されます。

アラーム機能を使ってみる

とりあえずアラームを設定して読み取る

DS3231にはAlarm1とAlarm2の2つのアラームがあり、前者は秒単位で、後者は分単位で設定ができます。

とりあえず、毎分5秒になったときにアラームを設定し、検知するコードを書きました。

#include <DS3232RTC.h>  //ライブラリの読み込み
DS3232RTC myRTC;        //クラスのインスタント化

const char *week_day[] = { "Non", "San", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };  //曜日が格納された定数を宣言

void setup() {
  Serial.begin(115200);
  myRTC.begin();                  //i2cなどの初期設定
  setTime(10, 0, 0, 3, 12, 2022);  //システムの時間設定 (時、分、秒、日、月、年)の順で入力
  myRTC.set(now());               //RTCに書き込み

  myRTC.setAlarm(DS3232RTC::ALM1_MATCH_SECONDS, 5, 0, 0, 0);//アラームの設定  (アラームの種類、秒、分、時間、日付か曜日)
}

void loop() {
  tmElements_t tm;  //tmElements_tk構造体を宣言
  myRTC.read(tm);   //RTCから現在時刻を取得して構造体に代入
  char buff[30];                                                                                                                  //文字列一時保存用の配列buffの宣言
  sprintf(buff, "%d/%02d/%02d %02d:%02d:%02d (%s)\n", tm.Year + 1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, week_day[tm.Wday]);  //フォーマットに従った文字列への変換を行いbuffに代入
  Serial.print(buff);                                                                                                             //buffを表示

  if(myRTC.alarm(DS3232RTC::ALARM_1)){
    Serial.println("Alarm!!");
  }

  delay(1000);  //1間隔で表示させるため1秒待機
}

アラームの設定方法について

アラームはsetAlarm関数の引数である「アラームの種類」と「日付」によって設定します。
アラームの種類の定義は以下の通り(
DS3232RTC/src/DS3232RTC.h at master · JChristensen/DS3232RTC · GitHub
より引用)で、毎秒から指定の日にちまで様々設定できます。

// Alarm masks
        enum ALARM_TYPES_t {
            ALM1_EVERY_SECOND = 0x0F,       // trigger alarm every second
            ALM1_MATCH_SECONDS = 0x0E,      // match seconds
            ALM1_MATCH_MINUTES = 0x0C,      // match minutes *and* seconds
            ALM1_MATCH_HOURS = 0x08,        // match hours *and* minutes, seconds
            ALM1_MATCH_DATE = 0x00,         // match date *and* hours, minutes, seconds
            ALM1_MATCH_DAY = 0x10,          // match day *and* hours, minutes, seconds
            ALM2_EVERY_MINUTE = 0x8E,       // trigger alarm every minute
            ALM2_MATCH_MINUTES = 0x8C,      // match minutes
            ALM2_MATCH_HOURS = 0x88,        // match hours *and* minutes
            ALM2_MATCH_DATE = 0x80,         // match date *and* hours, minutes
            ALM2_MATCH_DAY = 0x90           // match day *and* hours, minutes
        };

最後の引数である「日付か曜日」については、日付はいいとして、曜日を指定したい場合は以下のようにtimeDayOfWeek_tの曜日の定義を使用します。
(他の曜日:dowSunday、dowMonday、dowTuesday、dowWednesday、dowThursday、dowFriday、dowSaturday)
GitHub - JChristensen/DS3232RTC: Arduino Library for Maxim Integrated DS3232 and DS3231 Real-Time Clocksより

// Set Alarm1 for 12:34:56 on Sunday
myRTC.setAlarm(DS3232RTC::ALM1_MATCH_DAY, 56, 34, 12, dowSunday);

Alarm2 は秒の項目が設定できません。

// Set Alarm2 for 12:34 on the 4th day of the month
myRTC.setAlarm(DS3232RTC::ALM2_MATCH_DATE, 34, 12, 4);

アラームの検知について

上でかいたコードでは、1秒おきにアラームがONになったかどうか確認するalarm関数を実行し、trueだった場合に「Alarm!!」を表示するようにしてあります。

なお、alarm関数を実行したタイミングでアラームがONかどうかのフラグが初期化されるため、検知されるのが一度きり(毎分)となっています。
検知しっぱなしにする場合は以下のようにcheckAlarm関数を使用してください。

  if(myRTC.checkAlarm(DS3232RTC::ALARM_1)){
    Serial.println("Alarm!!");
  }

この場合、スイッチが押された際にalarm関数を実行するようにしておく等すれば、よくある時計のアラームのような機能が実現できます。 


しかしながら、上の書き方だと1秒おきにしか検知の処理を行っていないので、アラームの検知に若干の誤差が生じます。
また、マイコンのスリープ復帰などにも、マイコン自体の動作が止まってしまうので使えなくなってしまいます。


そこで活躍するのがアラームの「割り込み機能」です。
以下をsetup書き加えることで、アラームが発生し、アラームがONかどうかのフラグが立っている間、RTC本体のSQWピンがLOWになるように設定できます。(通常時、SQWピンはHIGH)

myRTC.alarmInterrupt(DS3232RTC::ALARM_1, true);

このSQWピンをマイコンに接続し、ピン割り込み処理を使用して監視することで、アラームがONになった瞬間に処理を実行することができます。

ピン割り込みについての説明は省略しますが、マイコンによって対応しているピン番号が決まっているのでこちら(https://garretlab.web.fc2.com/arduino.cc/www/reference/ja/language/functions/external-interrupts/attachinterrupt/)
にサイトから確認して、そのピンを使用してください。


今回、本記事を書くにあたって使用しているマイコンはNano Everyですので、すべてのデジタルピンでピン割り込みが使用できます。

#include <DS3232RTC.h>  //ライブラリの読み込み

#define PIN_INTERRUPT 2  //割り込みピンとして2番を設定

DS3232RTC myRTC;  //クラスのインスタント化

const char *week_day[] = { "Non", "San", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };  //曜日が格納された定数を宣言

bool rtcInterrupt_flag = false; //アラーム1のフラグを初期化用フラグ

void rtcInterrupt()  //ピン割り込み時に実行される関数を定義
{
  Serial.println("Alarm!!");
  rtcInterrupt_flag = true;//アラーム1のフラグ初期化用フラグを立てる
}

void setup() {
  Serial.begin(115200);

  pinMode(PIN_INTERRUPT, INPUT);                                                 //割り込みピンを入力ピンに設定
  attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPT), rtcInterrupt, FALLING);  //ピン割り込みの設定  (ピン番号、割り込み時に実行される関数、検知方法)


  myRTC.begin();                   //i2cなどの初期設定
  setTime(10, 0, 0, 3, 12, 2022);  //システムの時間設定 (時、分、秒、日、月、年)の順で入力
  myRTC.set(now());                //RTCに書き込み

  myRTC.setAlarm(DS3232RTC::ALM1_MATCH_SECONDS, 5, 0, 0, 0);  //アラームの設定  (アラームの種類、秒、分、時間、日付か曜日)
  myRTC.alarm(DS3232RTC::ALARM_1);                            //アラーム1のフラグを初期化
  myRTC.alarmInterrupt(DS3232RTC::ALARM_1, true);             //アラームのピン割り込みを有効に設定
}

void loop() {
  tmElements_t tm;                                                                                                                //tmElements_tk構造体を宣言
  myRTC.read(tm);                                                                                                                 //RTCから現在時刻を取得して構造体に代入
  char buff[30];                                                                                                                  //文字列一時保存用の配列buffの宣言
  sprintf(buff, "%d/%02d/%02d %02d:%02d:%02d (%s)\n", tm.Year + 1970, tm.Month, tm.Day, tm.Hour, tm.Minute, tm.Second, week_day[tm.Wday]);  //フォーマットに従った文字列への変換を行いbuffに代入
  Serial.print(buff);                                                                                                             //buffを表示
  if (rtcInterrupt_flag == true) {//アラーム1のフラグ初期化用フラグの確認
    myRTC.alarm(DS3232RTC::ALARM_1);//アラーム1のフラグを初期化
    rtcInterrupt_flag = false;//アラーム1のフラグ初期化用フラグを初期化
  }
  delay(1000);  //1間隔で表示させるため1秒待機
}

ちなみに、なぜアラームのフラグの初期化にわざわざ追加でフラグを宣言して使用しているかというと、割り込み時に実行される関数ないでアラームの初期化をするとバグるからです。


まあそもそも、割り込み時に実行される関数内では簡単な処理しか実行してはいけない暗黙のルールを無視したので、I2cの通信等でエラーが出たのではないでしょうか。

みなさんもお気をつけください。

その他の機能

RTCの温度取得

DS3231には温度センサーが内蔵されているので、その値を取得することができます。

#include <DS3232RTC.h>  //ライブラリの読み込み
DS3232RTC myRTC;        //クラスのインスタント化

void setup() {
  Serial.begin(115200);
  myRTC.begin();                    //i2cなどの初期設定
  setTime(15, 13, 0, 2, 12, 2022);  //システムの時間設定 (時、分、秒、日、月、年)の順で入力
  myRTC.set(now());                 //RTCに書き込み
}

void loop() {
  int t = myRTC.temperature(); //温度を取得
  float t_celsius = t / 4.0; //取得される値は摂氏×4倍なので4で割る
  Serial.println(t_celsius);  //温度(摂氏)を表示
  delay(1000);        //1間隔で表示させるため1秒待機
}

方形波の出力

RTCのSQWピンから方形波を出力することができます。
ただ、ピン割り込みと競合するので、使わないときはSQWAVE_NONEで無効化しておかないといけません。

#include <DS3232RTC.h>  //ライブラリの読み込み
DS3232RTC myRTC;        //クラスのインスタント化

void setup() {
  Serial.begin(115200);
  myRTC.begin();                    //i2cなどの初期設定
  myRTC.squareWave(DS3232RTC::SQWAVE_1_HZ);   //1Hzの方形波をSQWピンから出力
}

void loop() {
}

なお方形波の設定の定義は以下の通り(
DS3232RTC/src/DS3232RTC.h at master · JChristensen/DS3232RTC · GitHub
より引用)

        // Square-wave output frequency (TS2, RS1 bits)
        enum SQWAVE_FREQS_t {
            SQWAVE_1_HZ,
            SQWAVE_1024_HZ,
            SQWAVE_4096_HZ,
            SQWAVE_8192_HZ,
            SQWAVE_NONE
        };

まとめ

ということで今回はRTC(DS3231)を改めて使用してみました。

以前の記事を書いた頃は、コードを書くのもままならないケツの青いガキでしたが、今ではシステムをイチから構築できるほどに成長しました。
オイシーヤミーカンシャカンシャです。

とは言ってもまだまだ発展途上です。これからも色々勉強していき、わかったことをどんどん共有していければと思います。


今後ともよろしくお願いします!