Arduinoで実験 (RTCモジュール)

2013/04/17

秋月電子で購入したRTC-8564NBを使用したRTCモジュール を制御します。

いろんな方が詳しく解説してくれているので、あくまでも自分用のまとめページとします。


◆ RTCモジュール

CLKOE
CLKOUT
/INT (割り込みピン 割り込み信号は LOW レベルを出力)
Vss (GND)
SDA (I2C)
SCL (I2C)
NC
Vdd (1.8V~5.5V)

I2C での制御なので電源以外は他のI2C機器と同様に、 ⑦SCL → A5(19)、⑧SDA → A4(18) と接続します。

RTCモジュールの電源は、Arduino からとバックアップ用電池からの両方ともショットキーバリアダイオードで逆流を防ぐ。

早速、いつも勝手にお世話になっている『なんでも作っちゃう、かも。』さんのライブラリを利用させていただきます。 リンク先よりダウンロードし Arduino IDE のライブラリに追加します。

上記ライブラリは、古い Arduino IDE 1.0 以前に作られているので、修正が必要です。 テキストエディタで一気に変換した方が楽です。

  修正前 修正後
RTC8564.cpp #include <WConstants.h> #include <Arduino.h> (1個所)
Wire.send Wire.write (26個所)
Wire.receive Wire.read (2個所)

ライブラリのサンプルスケッチを実行し、シリアルモニタで実行の確認を行います。


◆ ライブラリを使わずに制御する

次は、上記のライブラリを参考に自分で制御を行ってみたいと思います。

初めにモジュールの初期化を行い、モジュールより時刻を読み取ります。

RTCモジュールへの命令は、初めに書き込み/読み込みのレジスタ番号を送信します。

書き込みの場合は、書き込みたいデータを転送ます。 転送されたデータはレジスタ番号の順に格納されてゆきます。

読み込みの場合は、レジスタ番号の送信後目的のデータ数をリクエストすれば先頭のレジスタ番号に格納されているデータから順にデータ数分モジュールより転送されます。

以下のサンプルでは初期化で書き込み、loop 内で読み込みを行っています。 

//
// 104-01 RTC実験スケッチ
//

#include <Wire.h>

#define I2C_ADDR (0xA2 >> 1)  // 1ビット目未使用のため1ビット右シフトする。


void setup() {
  Serial.begin(9600);
  Serial.println("RTC Test Start");

  Wire.begin();  // I2C の初期化
  delay(1000);   // 発振子の動作待機

  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x00); // データを転送する先頭のレジスタ番号を指定
  Wire.write(0x20); // 00 Control 1 STOP(bit5)-1 をセットし動作を停止させる。
  Wire.write(0x00); // 01 Control 2
  Wire.write(0x00); // 02 Seconds 初期値を転送(秒)0 ~ 59
  Wire.write(0x00); // 03 Minutes    〃   (分)0 ~ 59
  Wire.write(0x00); // 04 Hours     〃   (時)0 ~ 23
  Wire.write(0x01); // 05 Days      〃   (日)1 ~ 31
  Wire.write(0x01); // 06 Weekdays    〃   (曜日)0 ~ 6
  Wire.write(0x84); // 07 Months     〃   (月)1 ~ 12
  Wire.write(0x13); // 08 Years     〃   (年)0 ~ 99
  Wire.write(0x00); // 09 Minutes Alarm アラームの初期値を転送(分)0 ~ 59
  Wire.write(0x00); // 0A Hours Alarm       〃      (時)0 ~ 23
  Wire.write(0x00); // 0B Days Alarm        〃      (日)1 ~ 31
  Wire.write(0x00); // 0C Weekdays Alarm     〃      (曜日)0 ~ 6
  Wire.write(0x00); // 0D CLKOUT     タイマー用レジスタ
  Wire.write(0x00); // 0E Timer control     〃
  Wire.write(0x00); // 0F Timer         〃
  Wire.write(0x00); // 00 Control 1 STOP(bit5)-0 をリセットし動作を開始する。
                    //    アドレス 0F の次は先頭アドレスの 00 に戻る。
  Wire.endTransmission();
}

void loop() {
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x02);
  Wire.endTransmission();
  delay(1);
  Wire.requestFrom(I2C_ADDR, 7);
  int sec = Wire.read();    // 秒
  int min = Wire.read();    // 分
  int hour = Wire.read();   // 時
  int day = Wire.read();    // 日
  int week = Wire.read();   // 曜日
  int month = Wire.read();  // 月
  int year = Wire.read();   // 年

  Serial.print(year, HEX);
  Serial.print("/");
  Serial.print(month, HEX);
  Serial.print("/");
  Serial.print(day, HEX);
  Serial.print("(");
  Serial.print(week, HEX);
  Serial.print(")");
  Serial.print(hour, HEX);
  Serial.print(":");
  Serial.print(min, HEX);
  Serial.print(":");
  Serial.println(sec, HEX);

  delay(1000);
}

上記のスケッチを実行し、シリアルモニタを起動すると約1秒毎にデータが表示されます。

シリアルモニタでデータを見ていると時々データがおかしくなる時があります。(特に曜日)

曜日で使用される数値は 0~6 のため bit4 ~ bit 6 は使用しません。 ここにゴミが入ると妙な数値となります。

Address Function bit6 bit5 bit4 bit3 bit2 bit1 bit0 曜日
06 Weekdays × × × × 0 0 0
× × × × 0 0 1
× × × × 0 1 0
× × × × 0 1 1
× × × × 1 0 0
× × × × 1 0 1
× × × × 1 1 0

同じような現象は他の数値にも起こります。 論理演算子で未使用ビットに0を入れます。

Address Function DATA & filter
02 Seconds sec  & 0b01111111 (0x7F)
03 Minutes min  & 0b01111111 (0x7F)
04 Hours hour & 0b00111111 (0x3F)
05 Days day  & 0b00111111 (0x3F)
06 Weekdays week & 0b00000111 (0x07)
07 Months / Century mon  & 0b00011111 (0x1F)
08 Years year & 0b11111111 (0xFF)

上記を参考にスケッチを修正します。 長くなるので void loop() 内のみ記載します。

//
// 104-02 受信データのフィルタリング
//

void loop() {
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x02);
  Wire.endTransmission();
  delay(1);
  Wire.requestFrom(I2C_ADDR, 7);
  int sec = Wire.read();
  int min = Wire.read();
  int hour = Wire.read();
  int day = Wire.read();
  int week = Wire.read();
  int month = Wire.read();
  int year = Wire.read();

  Serial.print(year, HEX);
  Serial.print("/");
  Serial.print(month & 0x1f, HEX);
  Serial.print("/");
  Serial.print(day &0x3f, HEX);
  Serial.print("(");
  Serial.print(week & 0x07, HEX);
  Serial.print(")");
  Serial.print(hour & 0x3f, HEX);
  Serial.print(":");
  Serial.print(min & 0x7f, HEX);
  Serial.print(":");
  Serial.println(sec & 0x7f, HEX);

  delay(1000);
}

今度は変な数値が出なくなったはずです。

初期値が13年4月1日になっていますが、 07 Months/Century の bit7 が 0 だと 1900 年代となります。 このまま実行するとうるう年の計算がずれてくるので bit7 を 1 にしておきます。 上記サンプルの月の初期値が 0x84 になっているのはそのためです。

サンプルを実行してデータを確認していくと時々秒が重複したり飛んだりするときがあります。 これは Arduino 内の1秒とRTCモジュールの1秒の長さが異なるためです。 Arduino 側のタイマはRTCモジュールに比べ温度等の環境により動作が安定しないためです。 これを解決するにはもっと短い間隔でデータを取得し、前回データと同じならば表示しない等の工夫や、後述の外部割り込みを利用した制御が必要となります。


◆ 初期化の判定

毎回電源を入れるたびに時刻が初期化されると使い物になりません。

RTCモジュールはボタン電池などをバックアップ電源として利用でき、マイコンの動作が停止しても電力が供給されていればモジュールの動作は継続されます。

逆にモジュールの動作が継続されている場合は初期化しないような工夫が必要となります。

この場合、02 Seconds の bit7 で判定します。 既に電源が入っているのなら bit7 = 1 となります。

次に初期化の際に電源が入っているかの判定を行うサンプルにします。 これも長くなるので上記サンプルの setup() 内のみ修正します。

//
// 104-03 初期化判定スケッチ
//

void setup() {
  Serial.begin(9600);
  Serial.println("RTC Test Start");

  Wire.begin();
  delay(1000);

  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x02);               // RTCモジュールへレジスタの先頭番号を指定
  Wire.endTransmission(); 

  Wire.requestFrom(I2C_ADDR, 1);  // レジスタの先頭より1バイト転送依頼
  byte b = Wire.read();           // 1バイト受信
  if (b & 0x80) {                 // データの bit7 を判定し true なら初期化
    Wire.beginTransmission(I2C_ADDR);
    Wire.write(0x00);
    Wire.write(0x20);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x01);
    Wire.write(0x01);
    Wire.write(0x84);
    Wire.write(0x13);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.write(0x00);
    Wire.endTransmission();
  }
}

シリアルモニタを何度起動し直して、Arduino をリセットしても刻々と時刻を刻みます。 もし、バックアップ用の電池を繋いでいるのなら Arduino と切り離してもモジュールは動作し続けます。


◆ BCDデータの変換

RTCモジュールで使用するデータはBCDフォーマットで行います。

BCDフォーマットは上位4ビットと下位4ビットをそれぞれ独立した数値として扱います。 16進数では10の位と1の位が判りやすい等メリットもあります。 ただし、計算を行う場合などには適さず当たり前の10進数との変換が必要になります。

BCD形式を10進数へ変換する関数

  byte Bcd2Dec(byte b) {
    return ((b >> 4) * 10 + b & 0xf);
  }
10進数をBCD形式へ変換する関数

  byte Dec2Bcd(byte b) {
    return (( b / 10 * 16) + ( b % 10 ));
  }

◆ アラーム割り込みとタイマ割り込み

RTCモジュールでは、アラームに設定した時刻に通知するアラーム割り込みと、周期的に通知するタイマー割り込みの2つの割り込みがあります。初期値ではアラームとタイマーが0となっているので機能していません。

アラーム割り込みは、アドレス 09~0C に設定された時刻とRTCモジュールの時刻との条件が一致した場合に割り込みが発生します。

割り込みを使用するにはアラームやタイマーの時刻や周期を設定するほかに、アドレス 01 Control 2 の設定が必要です。

Address Function bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
01 Control 2 0 × 0 TI/TP AF TF AIE TIE
TI / TP ビット タイマーの動作モードを設定します。
 0 カウントダウンがゼロで通知後に終了
 1 カウントダウン終了後に再度カウントダウンを始める。
AF ビット アラーム割り込みが発生すると 1 になる
TF ビット タイマー割り込みが発生すると 1 になる
AIE ビット アラーム割り込み時に INT 端子へ出力するかどうか
TIE ビット タイマー割り込み時に INT 端子へ出力するかどうか

アラーム割り込み

Address Function bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
09 Minute AE 4 2 1 8 4 2 1
0A Hour AE × 2 1 8 4 2 1
0B Day AE × 2 1 8 4 2 1
0C Weekday AE × × × × 4 2 1
AE ビット アラーム制御ビット
 このビットがセットされているとアラーム割り込みが有効となる。
 AEビットが設定されていない条件は不問とされ、
 毎週月曜日の12時といった設定が可能となる。
 ※ただし、4つ全てのAEビットをセットした場合はアラームは発生しない。

タイマー割り込み

Address Function bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
0E Timer control TE × × × × × TD1 TD0
0F Timer 128 64 32 16 8 4 2 1
Timer control タイマ割り込み機能を制御します。
TE ビット  1 でタイマ割り込みを開始
 0 でタイマ割り込みを終了
TD1,TD0 ビット  タイマ割り込みのクロックを選択する。
 TD1 = 0, TD0 = 0 4096 Hz 244.14us  / 1 count
 TD1 = 0, TD0 = 1   64 Hz  15.625ms / 1 count
 TD1 = 1, TD0 = 0    1 Hz   1 sec   / 1 count
 TD1 = 1, TD0 = 1 1/60 Hz   1 min   / 1 count
Timer タイマ割り込みのカウンタ
 最大 255 から 0 までカウントダウンを行い、
 0 になったらタイマ割り込みイベントを発生させる。
 カウントダウン中、本レジスタを読み込むと残りのカウントが得られる。
 本レジスタのみ 1バイトの16進数

CLKOUT 端子の制御

Address Function bit7 bit6 bit5 bit4 bit3 bit2 bit1 bit0
0D CLKOUT Freq FE × × × × × FD1 FD2
CLKOUT frequency CLKOUT 出力端子のクロック出力を制御します。
FE ビット CLKOE 入力端子の状態
 HIGH の場合 FE ビット = 1
 LOW  の場合 FE ビット = 0

 HIGH の場合のみ CLKOUT が有効となり、FD1,FD0で設定された周波数が CLKOUT 端子より出力される。
FD1,FD0 ビット  FD1 = 0, FD0 = 0 32768 Hz
 FD1 = 0, FD0 = 1  1024 Hz
 FD1 = 1, FD0 = 0    32 Hz
 FD1 = 1, FD0 = 1     1 Hz

◆ 外部割込み

RTCモジュール3番ピンの割り込み信号で Arduino の外部割込みを使用してメッセージを発行する。 Arduino 側は D2 に接続している。

//
// 104-04 外部割込み実験スケッチ
//

#include <Wire.h>

#define I2C_ADDR (0xA2 >> 1)

void message() {
  Serial.println("INT");
}

void setup() {
  Serial.begin(9600);
  Serial.println("RTC Test Start");

  attachInterrupt(0, message, FALLING);  // 外部割り込みを開始する。
                                         // 0 割り込みピン D2
                                         // message 割り込み時に実行される関数
                                         // FALLING ピンの状態が HIGH → LOW になった時に割り込み

  Wire.begin();
  delay(1000);

// 実験用に初期化
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x00);
  Wire.write(0x20); // 00
  Wire.write(0x00); // 01
  Wire.write(0x00); // 02
  Wire.write(0x00); // 03
  Wire.write(0x00); // 04
  Wire.write(0x01); // 05
  Wire.write(0x01); // 06
  Wire.write(0x84); // 07
  Wire.write(0x13); // 08
  Wire.write(0x00); // 09
  Wire.write(0x00); // 0A
  Wire.write(0x00); // 0B
  Wire.write(0x00); // 0C
  Wire.write(0x00); // 0D
  Wire.write(0x00); // 0E
  Wire.write(0x00); // 0F
  Wire.write(0x00);
  Wire.endTransmission();

// タイマーを停止
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x0E);
  Wire.write(0b00000000); // 0E
  Wire.endTransmission();

// Control 2 の設定
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x01);
  Wire.write(0b00010001); // 02 bit4 TI/TP 動作モードを繰り返しに設定
                          //    bit0 TIE   /INT端子へ出力
  Wire.endTransmission();

// タイマーのカウント数を設定
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x0F);
  Wire.write(0b00000010); // 0F カウンタの回数を指定
  Wire.endTransmission();

// タイマーを開始
  Wire.beginTransmission(I2C_ADDR);
  Wire.write(0x0E);
  Wire.write(0b10000010); // 0E bit7 TE   タイマー割り込み開始
                          //    bit1 TD1  1周期の間隔を設定 TD1,0 = 1,0 = 1 秒
                          //    bit0 TD0
  Wire.endTransmission();
}

void loop() {
}

上記サンプルを実行すると 2 秒毎にメッセージが出力される。1秒毎や1分毎に割り込みを行って時計の表示を実行したり出来る。

アラーム割り込みも上記と同じ要領でアラーム条件とコントロールを設定すればよい。

アラーム割り込みとタイマー割り込みを同時に使用する場合(時計表示とアラーム音の出力)の場合は、どちらの場合も /INT 端子への出力を行う事が出来るので control 2 の bit3 AFビット、 bit2 TFビットの状態を読み取りどちらのタイマーが動作しているかを判断し処理を行う。


◆ バックアップ電源

せっかく設定した時刻データも電源が切れるとリセットされるので、データのバックアップを考える。 RTC-8564 は 1.8V ~ 5.5V と幅広い電圧範囲なので 3V のボタン電池で長時間のバックアップが可能です。 電池の接続は別に書き留めておくほどのことではないので、電気二重層コンデンサを使用したバックアップの回路図を残しておきます。 電池ほどのバックアップ時間は望めないが、電池交換のスペースがいらずメンテナンスフリーなのが長所です。


◆ 不明な点

いろいろ実験しているがいくつか解決していない問題で、RTCの制御に関する問題なのか Arduino のプログラムに関する問題なのか判らないがとにかく思ったような制御ができていない点がある。

1秒毎のタイマー割り込み動作中で、外部割り込みイベントで動作させる関数内でRTC内の時刻を読み込もうとしたらうまくいかない。 もしかしたら、/INT の割り込み信号を発行している間は I2C のコマンドを受け付けないのかもしれない。 delay 関数で待機時間を設け読み込む実験を行ってみる。