ArduinoでRTCを使用してファイル管理とAPIの実装する

組み込みエンジニア

こんにちは、ENGかぴです。

Arduino環境においてWireライブラリを使用してRTCモジュールから書き込んだ時刻からの時間を取得しています。RTCモジュール専用のcppファイルとヘッダーファイルを作成しArduinoプロジェクトに組み込んで実装しました。

RTCモジュールはRX8900(秋月電子)を使用しています。モジュールの設定に使用する定義などを区別するためにファイルを分けていますが、Arduinoプロジェクトで関数をコールするだけでデータが取得できるように簡単なAPIを実装しました。

RTCを使った応用例として時刻設定をボタンを使って行えるようにした方法を以下のリンクにまとめています。

ArduinoでRTCモジュールの情報をLCDを使って更新する

ファイルを分離したときの管理方法についても説明しています。この方法はマイコン問わず外部周辺機器ごとにファイルを作成しまとめてコンパイルする際に使えるテクニックです。

Arduino UNOを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

RTCモジュールを使用する

RTCモジュールはRX8900を搭載したAE-RX8900(秋月電子)使用しています。温度補償発振器(DTCXO)を内蔵しており高精度で最大で月差13秒で時刻を管理できます。時刻データはI2C通信(Wireライブラリ)を使用することで取得することができます。

ArduinoとRTCモジュール(AE-RX8900)の配線

ArduinoとAE-RX8900の配線
ArduinoとAE-RX8900の配線

ArduinoとAE-RX8900の配線例を示しています。I2CのSCLとSDAはプルアップする必要がありますがAE-RX8900モジュール内でプルアップ抵抗を有効にすると10kΩでプルアップすることができます。

RX8900はArduinoのSCLのクロックの供給によってデータの返信をします。(クロック同期式という)クロックが供給されなければデータをArduinoに返信することができません。

I2C通信は基本的にマスタ(クロックを出す側)がスレーブ(クロックを受けて動作する側)に対してクロックを供給する関係になります。

センサーモジュールにクロック発振器がついている場合はクロックを発生させられるためマスタにもスレーブにもなることができますが、基本的にはArduinoがマスタでセンサーモジュールがスレーブの関係になります。(RX8900はスレーブ専用)

RTC専用のファイルを作成する

ファイルの追加
ファイルの追加

Arduinoプロジェクトを「RTC.ino」を新規作成するとフォルダが生成されますが、同一のフォルダの中に今回追加する2つのファイルを生成します。

  1. RX8900.cpp→RX8900に関する処理の実体の処理を行うファイル
  2. RX8900.h→RX8900処理に使用する定義をまとめたファイル

これらのファイルをArduinoプロジェクト(RTC.ino)を再起動するか追加したいファイルをArduinoIDE内のタグ部分にドロップすることで追加します。

ファイル名の後にファイルの種別である.cpp(もしくは.c)や.hを入力する際「.」の前にスペースがあるとArduinoプロジェクトをリロードした際に読み込みが無視されることがあります。

ファイルの管理について

追加したcppファイルとhファイルの構成を示します。基本的にRX8900の操作に関する処理はこれら専用のファイル内で完結するように作ります。完結するように作ることで流用する際にファイルをコピーしてインクルードするだけで使用することができるようになります。

//-------RX8900.cpp----------------
#include <Wire.h>
#include <Arduino.h>

#define RTC_C
    #include  "RX8900.h"
#undef RTC_C

cppファイルでは専用のヘッダーファイルである「RX8900.h」をインクルードする前にRTC_Cを定義しています。コンパイルする手順はコンパイラーに依存するため他のファイルで「RX8900.h」をインクルードしていた場合に重複して変数の定義をしないようにするための準備です。

//-------RX8900.h----------------
#ifndef RX8900_H
#define RX8900_H

//この間に専用の定義を入れる
#endif

#ifdef RTC_C
    //RX8900.cppの最初でRTC_Cを定義している
    #define GLOBAL
#else
    #define GLOBAL extern
   //RTC_Cが定義されていなければ外部参照になる
#endif

GLOBAL uint16_t data; //他のファイルでも共通で使用できる変数

#undef GLOBAL

ヘッダーファイルを読み込むときは一度読み込まれているかを判断するために#ifndef RX8900_Hの定義の確認を行っています。初めて読み込まれるときはRX8900_Hが定義されていないためRX8900_Hを定義して以降の専用の定義を参照するようにしています。

2度目に読み込まれた時はRX8900_Hが定義されているため無視されます。これにより重複した読み込みが発生しません。

次にRTC_Cについてですが、RX8900.cppが読み込まれた時にRTC_Cが定義されるため#define GLOBALが有効になります。GLOBALの後には何も記述がないため空欄と同じ扱いになるので変数の宣言となります。

一方RX8900.cpp以外のファイルからRX8900.hが参照された場合はRTC_Hが定義されていないことから#define GLOBAL externが有効になるため外部参照の変数として宣言することになります。

重複して変数を宣言するとコンパイルエラーになることから重複して宣言しないようにファイルの管理を行うことはマイコンに接続する周辺機器が多いほど有効です。また、専用ファイルで管理すると流用する際の煩わしさがなくなります。

RX8900専用のAPIを実装する

APIは「Application Programming Interface」の略ですが、組み込みソフトで解釈すると外部機器が管理するデータをメインのマイコンから呼び出して使用するための命令(関数)になります。今回は外部機器がRX8900になるのでRX8900に関するコマンドの一部をAPIとして実装します。

実装するAPIの説明(宣言と内容)

実装するAPIの定義と宣言はRX8900.hに内部の処理をRX8900.cppに実装していきます。

class RTC8900Class{
    public:
        void begin();
        void begin(struct dateTime *dt);
        void setDateTime(struct dateTime *dt);
        void getDateTime(struct dateTime *dt);
        void getTemp(uint8_t *temp);
        void setRegisters(uint8_t address, int numData, uint8_t *data);
        void getRegisters(uint8_t address, int numData, uint8_t *data);
    private:
        int decimalToBCD(int decimal);
        int BCDToDecimal(int bcd);      
};

RX8900のクラスを宣言し共通して使用する関数(public)と専用ファイルのみで使用する(private)に分けて宣言しています。publicで宣言したものを他のファイルからコールして使用します。

API説明
begin()
begin(引数)
RX8900の初期化を行います。
バックアップ電源が有効であるか判断して時刻の初期化を行います。
引数は初期値として時刻を指定する場合にセットします。
setDataTime(引数)引数に時刻データをセットすると時刻を書き込みます。
getDataTime(引数)時刻データを引数に指定した変数にセットします。
getTemp(引数)温度データを引数に指定した変数にセットします。
温度表示するときは換算が必要です。
setRegisters(引数,引数2,引数3)レジスタの設定を変更するときに使用します。
引数には変更するレジスタのアドレスを指定します。
引数2は書き込みするバイト数です。
引数3はデータのアドレスです。
getRegisters(引数,引数2,引数3)レジスタの設定を読み込むときに使用します。
引数には読み込むレジスタのアドレスを指定します。
引数2は読み込むバイト数です。
引数3はデータのアドレスです。
decimalToBCD(引数)引数の変数を10進数をBCDデータに変換します。(API内部でのみ使用)
BCDToDecimal(引数)引数の変数をBCDデータから10進数に変換します。(API内部でのみ使用)
APIの説明

今回はアラーム機能を使用しないため時刻設定のみのAPIを実装しています。温度情報については換算後のデータを関数の戻り値にしてもよいと思いましたが、データを取得することをメインにしているためAPIで値を取得し換算するようにしています。

privateで宣言しているAPIは内部での処理のみに限定しているため外部から呼び出すことができません。内部で時刻データがBCD方式で保存されるため変換するため関数として宣言しています。10進数からBCD変換するときの関数も同様です。

今回作成したAPIは機能を固定していますが、用途に応じて動作変更ができるよう変数を準備して変数部分を変更することで設定が反映されるようにすると機能が充実します。今回はAPIの作り方とRX8900の動作を確認する目的なので最低限の動作分しか実装していません。

実装するAPIの説明(処理内容)

RX8900の処理に関わるAPIを中心に説明します。

void RX8900Class::begin(){
    struct dateTime dt =  {0, 0, 0, THU, 1, 10, 20}; //(2020/10/01/00:00:00)が初期値
    begin(&dt);
};

void RX8900Class::begin(struct dateTime *dt){
    uint8_t extreg;
    uint8_t flgreg;
    uint8_t flgreg_init;
    uint8_t conreg;

    extreg = 0x08;
    flgreg = 0x00;
    conreg = 0x01;
    
    delay(1000); //RX8900のクロックが安定するまでの待ち時間
    Wire.begin(); //I2C通信を有効にする
    setRegisters(EXTENSION_REG, 1, &extreg); //set WEEK ALARM , 1Hz to FOUT
 
    getRegisters( FLAG_REG, 1, &flgreg_init ); //VLFを確認するためにフラグを読み込む

    if( flgreg_init & 0x02 ){ //VLFの確認(バックアップが有効でない)
       setDateTime(dt); //初期時刻を書き込む
    }
    
    flgreg = 0x00;
    setRegisters(FLAG_REG, 1, &flgreg); //フラグをクリア
    conreg = 0x21;
    setRegisters(CONTROL_REG, 1, &conreg); //時刻更新割り込み許可とカウントリセット
}

電源投入後はRX8900においてクロックが安定するまで待つ必要があります。厳密に管理する場合はデータシートで安定時間を確認する必要がありますが、かなり余裕を見て1000msのウェイトを置いています。

初期電源投入時は初期設定が行われますが、ビット値は不定となるので初期設定を実施することが奨励されているためビットをクリアする処理を入れています。

VLFビットは電源が低下した時にセットされるため初期時にVLFビットを読み込むことでバックアップが有効かの判断をすることができます。バックアップが有効でない場合は初期値の時刻を書き込んでスタートします。

void RX8900Class::setDateTime(struct dateTime *dt){
    uint8_t data[7];

    data[0] = decimalToBCD(dt->second); //10進数からBCDデータに変換
    //・・・・省略・・・・
    data[6] = decimalToBCD(dt->year);
    setRegisters(SEC_REG,7,data); //BCDデータに変換後レジスタに書き込む
}

void RX8900Class::getDateTime(struct dateTime *dt) {
    uint8_t data[7];
   
    getRegisters(SEC_REG, 7, data); //時刻データ(BCDデータ)を読み込む
    dt->second  = BCDToDecimal(data[0] & 0x7f);
    //・・・・省略・・・・
    dt->year    = BCDToDecimal(data[6]);
}

setDateTime()はモジュールに指定した時刻を書き込みますが、レジスタはBCDデータで管理されているため10進数をBCDデータに変換する必要があります。引数の段階でBCDデータに変換している場合は変換の必要はありませんが、時刻データは10進数で管理する方が分かりやすいためBCDデータに変換する方法をお勧めしています。

Weekデータのみ設定したい曜日のビットを立て設定する必要があります。

getRegisters()ではモジュールの時刻を読み込みますが、BCDデータで読み込まれるため10進数で管理する場合変換する必要があります。

inline int RX8900Class::decimalToBCD(int decimal) {
    return (((decimal / 10) << 4) | (decimal % 10));
}
 
inline int RX8900Class::BCDToDecimal(int bcd) {
    return ((bcd >> 4) * 10 + (bcd & 0x0f));

変換関数は引数を換算して戻り値としてする処理ですが、マクロ処理によるものと同じ扱いになります。inline設定することでメモリ移動などの処理を省略できるため処理時間が短くなります。

最近のコンパイラは最適化する際にインライン展開を自動で行うことが多いため特に意識する必要はないと思います。(とは言え自動で行う保証はないためinlineを指示しています。)

APIの動作確認

Arduinoのsetup()とloop()内にAPIをコールして動作確認を行います。

void setup() {

    pinMode(INT_PIN, INPUT_PULLUP); //RX8900の時刻更新割り込みを受けるDI
    Serial.begin(9600);
    //RX8900.begin(); //初期値を指定しない場合(2020/10/01/00:00:00)
    RX8900.begin(&date); //初期値を指定する場合
}

pinMode()で/INTの出力を受けるDIピンの設定を行っています。/INTはLOWアクティブなのでDIをマイコンのプルアップ機能を使用して割り込みを待ちます。

RX8900は時刻更新による割り込みを発生させることができます。今回は時刻更新が1秒ごとに/INTピンがLOWになり時刻更新を通知します。時刻更新割り込みを有効にしていない場合は通知しません。

更新通知設定を変更することで1分ごとに変更することもできます。(Extension RegisterのUSELのビットで選択する。デフォルトは1秒)

RX8900.begin()で初期設定を行います。引数に時間データをセットするとセットした時間が書き込まれその時刻からカウントをスタートします。

void loop() {

    if (digitalRead(INT_PIN)) {
        if(rtcmode == RTC_MODE_TYP::RTC_RX_END ){
            rtcmode = RTC_MODE_TYP::RTC_IDLE;
        }
    }else{ //時刻更新割り込み発生
        if(rtcmode == RTC_MODE_TYP::RTC_IDLE){
          rtcmode = RTC_MODE_TYP::RTC_RX;
        }
    }

    switch(rtcmode ){
    case RTC_MODE_TYP::RTC_IDLE:
      //処理なし
      break;
    case RTC_MODE_TYP::RTC_RX:
        RX8900.getDateTime(&tim); //時刻データを読み込み 
       //時刻データをモニタに表示
        RX8900.getTemp(&temp); //温度データを読み込み
        Serial.print(((float)temp * 2 - 187.19)/ 3.218); //温度データを換算(データシートに換算式がある)
        Serial.println("deg");
        rtcmode = RTC_MODE_TYP::RTC_RX_END;
        break;
    case RTC_MODE_TYP::RTC_RX_END:
        //タイムアップで異常を検出する場合の処理
        break;
    }
}

RTCが動作開始すると1秒に一度割り込みが発生し/INTピンがLOWになります。この時にRTCの時刻を読み込むようにしています。

RX8900.getDataTime()の引数に取得したデータを格納する変数を指定します。時刻データは変数timに取得できているので時刻データをモニタ表示することができます。

RX8900.getTemp()の引数に取得したデータを格納する変数を指定します。温度データは換算する必要があるので換算してモニタに表示するようにしています。

RTCのデータ取得をモード管理することで1秒ごとの割り込みごとに一度しか処理しないようにしています。RTCが異常により割り込みが途中で途絶えた時のエラー検出のための処理を追加することも場合によっては必要です。

ArduinoとRX8900の動作確認
ArduinoとRX8900の動作確認

今回は、初期値を指定(2020/10/02/20:30:00)してスタートしています。電源を入れて3時間ほど放置しましたが、モニタには時刻が1秒ごとに表示されていました。室温が24℃だったので少し誤差が大きな気がしましたが、RTCを指で触れたりすると変化していたことからデータが取得できていると判断できました。

時刻データが初期値のみで決まるため使い勝手が悪いので動作の途中からでも時刻が変更できるように改造してみると面白そうです。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

Arduinoのスケッチ:

#include <Wire.h>
#include "RX8900.h"

const byte INT_PIN = 8;
dateTime tim;
uint8_t temp;
dateTime date =  {0, 30, 20, FRI, 2, 10, 20}; //初期値-秒-分-時-曜日-日-月-年

void setup() {

    pinMode(INT_PIN, INPUT_PULLUP); //RX8900の時刻更新割り込みを受けるDI
    Serial.begin(9600);
    //RX8900.begin(); //初期値を指定しない場合(2020/10/01/00:00:00)
    RX8900.begin(&date); //初期値を指定する場合
}

void loop() {

    if (digitalRead(INT_PIN)) {
        if(rtcmode == RTC_MODE_TYP::RTC_RX_END ){
            rtcmode = RTC_MODE_TYP::RTC_IDLE;
        }
    }else{
        if(rtcmode == RTC_MODE_TYP::RTC_IDLE){
          rtcmode = RTC_MODE_TYP::RTC_RX;
        }
    }

    switch(rtcmode ){
    case RTC_MODE_TYP::RTC_IDLE:
      //処理なし
      break;
    case RTC_MODE_TYP::RTC_RX:
        RX8900.getDateTime(&tim);
        
        Serial.print("20");
        if(tim.year < 10){
          Serial.print('0');
        }
        Serial.print(tim.year);
        Serial.print("/");

        if(tim.month < 10){
          Serial.print('0');
        }
        Serial.print(tim.month);
        Serial.print("/");
        
        if(tim.day < 10){
          Serial.print('0');
        }
        Serial.print(tim.day);
        Serial.print("/");

        if(tim.hour < 10){
          Serial.print('0');
        }
        Serial.print(tim.hour);
        Serial.print(":");

        if(tim.minute < 10){
          Serial.print('0');
        }       
        Serial.print(tim.minute);
        Serial.print(":");

        if(tim.second < 10){
          Serial.print('0');
        }   
        Serial.print(tim.second);
        Serial.print(" Temperature:");
        RX8900.getTemp(&temp);
        Serial.print(((float)temp * 2 - 187.19)/ 3.218);
        Serial.println("deg");
        rtcmode = RTC_MODE_TYP::RTC_RX_END;
        break;
    case RTC_MODE_TYP::RTC_RX_END:
        //タイムアップで異常を検出する場合の処理
        break;
    }
}

RX8900.cpp:

#include <Wire.h>
#include <Arduino.h>

#define RTC_C
    #include  "RX8900.h"
#undef RTC_C

void RX8900Class::begin(){
    struct dateTime dt =  {0, 0, 0, THU, 1, 10, 20};
    begin(&dt);
};

void RX8900Class::begin(struct dateTime *dt){
    uint8_t extreg;
    uint8_t flgreg;
    uint8_t flgreg_init;
    uint8_t conreg;

    extreg = 0x08;
    flgreg = 0x00;
    conreg = 0x01;
    
    delay(1000);
    Wire.begin();
    setRegisters(EXTENSION_REG, 1, &extreg);//set WEEK ALARM , 1Hz to FOUT
    getRegisters( FLAG_REG, 1, &flgreg_init );

    if( flgreg_init & 0x02 ){ //VLFの確認(バックアップが有効でない)
       setDateTime(dt);
    }
    
    flgreg = 0x00;
    setRegisters(FLAG_REG, 1, &flgreg);//reset all flag
    conreg = 0x21;
    setRegisters(CONTROL_REG, 1, &conreg);//reset all flag
}

void RX8900Class::setRegisters(uint8_t address, int sz, uint8_t *data) {
    Wire.beginTransmission(RX8900A_ADRS);
    Wire.write(address);
    Wire.write(data, sz);
    Wire.endTransmission();
}

void RX8900Class::getRegisters(uint8_t address, int sz, uint8_t *data) {
    Wire.beginTransmission(RX8900A_ADRS);
    Wire.write(address);
    Wire.endTransmission();
    Wire.requestFrom(RX8900A_ADRS, sz);
   
    for (int i = 0; i < sz; i++) {
        data[i] = Wire.read();
    }
}

void RX8900Class::setDateTime(struct dateTime *dt){
    uint8_t data[7];

    data[0] = decimalToBCD(dt->second);
    data[1] = decimalToBCD(dt->minute);
    data[2] = decimalToBCD(dt->hour);
    data[3] = dt->week; //weekのみ各ビットを立てる
    data[4] = decimalToBCD(dt->day);
    data[5] = decimalToBCD(dt->month);
    data[6] = decimalToBCD(dt->year);
    
    setRegisters(SEC_REG,7,data);
}

void RX8900Class::getDateTime(struct dateTime *dt) {
    uint8_t data[7];
   
    getRegisters(SEC_REG, 7, data);
    dt->second  = BCDToDecimal(data[0] & 0x7f);
    dt->minute  = BCDToDecimal(data[1] & 0x7f);
    dt->hour    = BCDToDecimal(data[2] & 0x3f);
    dt->week = data[3];
    dt->day     = BCDToDecimal(data[4] & 0x3f);
    dt->month   = BCDToDecimal(data[5] & 0x1f);
    dt->year    = BCDToDecimal(data[6]);
}

void RX8900Class::getTemp(uint8_t *temp) {

    getRegisters( TEMP_REG, 1, temp);
}

inline int RX8900Class::decimalToBCD(int decimal) {
    return (((decimal / 10) << 4) | (decimal % 10));
}
 
inline int RX8900Class::BCDToDecimal(int bcd) {
    return ((bcd >> 4) * 10 + (bcd & 0x0f));
}

RX8900.h:

#ifndef RX8900_H
#define RX8900_H

#define RX8900A_ADRS           0x32

#define SEC_REG                0x00
#define MIN_REG                0x01
#define HOUR_REG               0x02
#define WEEK_REG               0x03
#define DAY_REG                0x04
#define MONTH_REG              0x05
#define YEAR_REG               0x06
#define RAM_REG                0x07
#define MIN_ALARM_REG          0x08
#define HOUR_ALARM_REG         0x09
#define WEEK_DAY_ALARM_REG     0x0A
#define Timer_CNT0_REG         0x0B
#define Timer_CNT1_REG         0x0C
#define EXTENSION_REG          0x0D
#define FLAG_REG               0x0E
#define CONTROL_REG            0x0F
#define TEMP_REG               0x17
#define BACKUP_FUNC_REG        0x18
#define NO_WEEK 0x00
#define SUN 0x01
#define MON 0x02
#define TUE 0x04
#define WED 0x08
#define THU 0x10
#define FRI 0x20
#define SAT 0x40

struct dateTime {
    uint8_t second;             // 0-59
    uint8_t minute;             // 0-59
    uint8_t hour;               // 0-23
    uint8_t week;               // 0x01(Sun)-0x40(Sat)各ビットをセットする
    uint8_t day;                // 1-31
    uint8_t month;              // 1-12
    uint8_t year;               // 00-99
};

enum RTC_MODE_TYP{
    RTC_IDLE = 0,
    RTC_RX,
    RTC_RX_END,
    RTC_WRITE,
    RTC_MODE_MAX
};

class RX8900Class{
    public:
        void begin();
        void begin(struct dateTime *dt);
        void setDateTime(struct dateTime *dt);
        void getDateTime(struct dateTime *dt);
        void getTemp(uint8_t *temp);
        void setRegisters(uint8_t address, int numData, uint8_t *data);
        void getRegisters(uint8_t address, int numData, uint8_t *data);
    private:
        int decimalToBCD(int decimal);
        int BCDToDecimal(int bcd);      
};
#endif

#ifdef RTC_C
    #define GLOBAL
#else
    #define GLOBAL extern
#endif

GLOBAL RX8900Class RX8900;
GLOBAL RTC_MODE_TYP rtcmode;

#undef GLOBAL

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

テックジム-将来のためにプログラミングを学ぶ

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました