1Click飲みRomoCartTempescope色色[:iroiro]Other Projects

2011年12月25日日曜日

Arduinoでミリ秒まで出力出来るRTC8564のライブラリ

秋月電子のRTCモジュール(RTC-8564NB搭載のこいつ)は最小で秒単位の出力しか行えない。
でもセンサ情報の記録(特に1Hz以上で)するんだったら、ミリ秒管理も必要だよね?
秒毎のinterruptとmillis()を組み合わせて、ミリ秒管理も可能なRTCライブラリを作ろう。


まずは・・・

ベースとして以下のライブラリを拝借させてもらう:


このライブラリはbegin()を呼ぶだけでRTCの初期化を全部やってくれて、available()を呼ぶと最新の時刻を取得、そしてyear(), month(), etcで年,月,...をそれぞれ取得出来る。
これを改造して、ms()でミリ秒も取得できるようにしよう。
ついでに、available()という命名が気に入らないので、updateTime()に変更。


秒毎の頭のmillis()を覚える

基本的な考え方は:

  • 1秒に一回、秒が変わったタイミングをinterruptで受け取って、現在の秒の頭時点でのmillis()(=システム起動後の経過時間)を覚える(lastSecUpdateMS)
  • 時刻取得を要求(updateTime())されたら、現在のmillis()からlastSecUpdateMSを引いて、ミリ秒を計算する。


まずはヘッダ:
class RTC8564ms
{
public:
 //time (in millis()) of the start of current second
 unsigned long lastSecUpdateMS; 
 uint8_t data[7];
 int _ms;
 
 RTC8564ms(int); 
 void updateTime();
 void setTime(long years, uint8_t months, uint8_t weekdays, uint8_t days, uint8_t hours, uint8_t minutes, uint8_t seconds);

 int ms();
 uint8_t seconds();
 uint8_t minutes();
 uint8_t hours();
 uint8_t days();
 uint8_t weekdays();
 uint8_t months();
 long years();
 bool century();
 String toString();
};

メンバ変数の説明:

  • lastSecUpdateMSに、現在の秒の頭millis()を格納
  • data[]に、最後にupdateTimeを呼んだ時刻のyears〜secondsを格納
  • _msに、最後にupdateTimeを呼んだ時刻のミリ秒を格納

メンバ関数の説明:

  • ms(): _msを返す
  • seconds()〜century(): そのまんま
  • toString(): フレンドリーな文字列を返す


次に、コンストラクタ:

RTC8564ms::RTC8564ms(int interruptPin)
{
 currentRTC=this;
 lastSecUpdateMS =millis();
 _ms=0;
 
 //make sure we're in input mode
 pinMode(interruptPin,INPUT);
 //attach input for input handler
 attachInterrupt(interruptPin-2, interruptHandler,FALLING);
 
 for(int i=0;i<7;i++)
  data[i]=0;
 
 Wire.begin();
 
 Wire.beginTransmission(RTC8564_SLAVE_ADRS);
 Wire.write((byte)0x01);  //Control2 address
 Wire.write((byte)0x11);  // TI/TP:1(繰り返し割り込みモード),TIE:1(定周期割り込み時、Lレベルの割り込み信号を発生させる)
 Wire.endTransmission();
 
 Wire.beginTransmission(RTC8564_SLAVE_ADRS);
 Wire.write((byte)0x0E);  // Timer Control address
 Wire.write((byte)0x82); //countdown period to 1Hz
 Wire.write((byte)0x01); //interrupt every second
 Wire.endTransmission();
 
}
interruptを受け取るピン番号を引数に取る。Arduino Unoでは2か3のはず。
こちらを参考に、RTCモジュールのピン3をD2かD3に繋げる。

3行目: currentRTCに自分を覚えさせる。interruptのハンドラ(interruptHandler())はグローバル関数しか渡せないので、処理の対象をcurrentRTCで覚える。だったらそもそもsingletonとして設計しろって話だが、そのうち配列で覚えるよ。
7-9行目: 渡されたinterruptピンを念のためINPUTモードにし、interruptHandler(後述)を電圧降下時のinterruptとして登録する。
以降: 1秒に一回interruptが呼ばれるように設定

次は、interruptのハンドラ:
void interruptHandler(){
 //record millis() of current second.000
 currentRTC-> lastSecUpdateMS =millis(); 
}

これが呼ばれたということは秒が変わったということなので、currentRTCで登録されたオブジェクトのlastSecUpdateMSに今のmillis()を格納する。

次に、RTCの最新時刻を取得するupdateTime() (旧available())

void RTC8564ms::updateTime(){
 Wire.beginTransmission(RTC8564_SLAVE_ADRS);
 Wire.write((byte)0x02); // write reg addr 02
 Wire.endTransmission();
 
 Wire.requestFrom(RTC8564_SLAVE_ADRS, 7);
 for(int i=0; i<7; i++){
  if(Wire.available()){
   currentRTC->data[i] = Wire.read();
  }
 }
 long current=millis();
 if(current<lastSecUpdateMS)
  _ms= current;
 else
  _ms=millis()-lastSecUpdateMS;
}
↑prettifyでコード内に「<」があるとバグるのがとてもウザい。なんとかならんのですかね?

RTCから年〜秒情報を読み込む。
更に、今のmillis()から、現在秒の頭で記憶したmillis()を引いて、差分を現在時刻のミリ秒として格納。
millis()は数ヶ月?ぐらいでオーバーフローするので、オーバーフロー時は適当に処理する。誤差の範囲だ誤差の。

ms(), years(), などはそのまんまの実装。
RTCに時刻を設定するための関数setTimeも、大して難しいことはない。詳しくは下記添付参照。


ライブラリ


RTC8564ms [zip]
以上ファイルをダウンロードして、Arduinoのlibrariesフォルダに置く。



実行してみる


projects: Biotope: Arduinoとリアルタイムクロック -1: 定周期タイマー割り込み & スリープで公開されている回路図に基づいて組んで、以下のサンプルコードを動かしてみる:
#include <Wire.h>
#include <RTC8564ms.h>

RTC8564ms *rtc;

void setup(){
  Serial.begin(9600);
  
  rtc=new RTC8564ms(2); //initialize using interrupt pin 2
  
  rtc->setTime(2011,12,3,25,10,30,20);
  
}

void loop(){
  //update rtc
  rtc->updateTime();
  
  //print!
  Serial.println(rtc->toString());
    
  delay(10);
}

実行;
RTC8642ms実行結果

ちゃんと動いてるね。



まとめ
  • millis()を別管理してRTSと同じ簡便さでミリ秒も楽々管理出来るライブラリを作った
  • interrupt中はmillisは止まるので、正直そこまで正確というわけではない。同じ秒のindexを管理するよりはマシってレベルだろうきっと。
  • RTCへのクエリーはどれぐらいのスピードで回せるんだろう?