Arduinoで時刻を管理する

~割り込みを利用して、ソフトウェアで実現する時計管理~

森下功啓製作所 ONLINE

[2013/1/13 追記] 情報を追記しました。

1. はじめに

本ページで紹介するのは、マイコンによるデータロガーに対してタイムスタンプを提供するC++クラスです。 元々は、GPS関連の演算をPCでやるために、マイコン向けだったC版のモジュールを拡張して作ったものです。 従ってArduino用に開発したものではありませんが、C++のクラスは便利ですので使ってみようと思った次第です。

単に観測値にタイムスタンプを押すだけならRTCと接続して通信を行えば大まかな時刻が分かりますが、電池駆動での長期間運用だとCPUを起動させて通信する時間がもったいない場合があります。 それならマイコン内部のタイマーでカウントアップした方が良いと考えられます。 また例えば、指定された時刻までスリープする場合、1 Hzのクロック源の1PPS信号をカウントアップしながら閾値を超えた時点でシーケンスを実行すれば非常に省電力になります。 その場合、「目標となっている日時まで何秒かかるのか?」ということをマイコンが知る必要があります。 これらの機能を提供するライブラリを以下で紹介します。


2. 提供する機能

配布しているクラスは、UTCによる日付と時刻を用いて時刻オブジェクトを生成します。 生成された時刻オブジェクトからは、普通に日時だけでなく、GPS時刻やGPS週秒やGPS週番号を得られます。 GPS時刻は1980/1/6からの通し時間[s]です。 また、観測器では利用しないかもしれませんが時刻の減算も実装しています。 比較演算子には時刻の大小比較や等値判定演算子を用意しました。 例えば、2つの日時におけるGPS時刻の差を計算すれば、その時間差[s]を得ることが可能です。


3. スケッチ(プログラムソースコード)

時刻管理を利用して時刻を出力するサンプルスケッチを図3.1に示します。 ソースコードのダウンロードは下記のリンクより可能です。 解凍してできたフォルダ内にあるcpp/hファイルは、フォルダごとArduino IDEのライブラリフォルダへ移動させてください。 移動後にIDEを起動するとライブラリが参照できるようになっています。 ライブラリ単体の最新版はこちらです。


プログラムのダウンロード
リリース日 プログラムパッケージ 更新内容
2013/1/9 Time_test_20130109.zip
for Arduino IDE 1.0.3(1.0以降)
バグの修正・演算法の修正・機能増強など、大幅にライブラリを更新しました。 変わりすぎたので、変更点を挙げられません。 一部、アクセッサの関数名が変更になっていますので詳しくはヘッダファイルをご覧になってください。 便利になったのに加えて、演算速度が向上しています。

*ビルドサイズはちょっと増えました。

書きながら思ったんですが、時刻情報フォーマット変更用のメソッドはstaticにした方が良かったなぁ。。 それに、オブジェクト同士のプラスやマイナスも定義しておけばよかったw。
2011/6/9 Time_test.zip
for Arduino IDE 0022
初公開です。 このコードのバイナリサイズは5kB程度です。 ATmega328のメモリ容量からすれば小さい方だと思います。
サンプルコードでは、1秒ごとに割り込みがかかって時刻が更新され、その結果が出力されるようになっています。 time.Increase_Clock(long int t)によって、指定されただけ時刻が更新されます。 ここでは日付・曜日・GPS週番号の変化を確認するために時刻増分を3,600秒にしています。 Increase_Clock()の引数は整数であれば±2,000,000,000程度まで指定可能です。 +2,000,000,000秒後って、いつですかね?

time_personal.hに関しては、森下が個人的に一からコツコツ作ってきたライブラリです。
他のコードと同様に著作権は放棄しませんが、改良はどしどしされて下さい。

[スケッチの概要]
AVRマイコンに内蔵されているタイマー2を利用した割り込みを利用して時刻の管理を行います。 タイマー2の割り込みとソフトウェア的に呼び出す機構を利用して、1秒毎に呼び出される処理を実装しています。 呼び出された先の関数で時刻管理クラスの時刻情報を更新させます。 loop()処理中は時刻が変わったことなど意に介さず所望の処理を実行可能です。 (ただし、サンプルプログラムではloop()関数の中で処理させています) 割り込みに関しては「PIC AVR 工作室」の「タイマー割り込みライブラリ」を参考にしました。 感謝。

MsTimer2のライブラリはこちら
/*****************************************************************
 * TimeClass
 * 時刻管理クラスの動作テストプログラム
 * Author: Katsuhiro Morishita, Kumamoto-Univ @ 2012
 * Create: 2013/1/5
 * Abst.:  時刻管理クラスの動作テスト用スケッチです。
 * 時刻の基本的な時刻の演算を行い、GPS時刻等の情報を提供します。
 * 時刻系の切り替えを実施した場合でも、GPS時刻を提供可能です。
 * Build size: 8,620 byte (@ UNO)
 * Platform: Arduino IDE 1.0.3
 ******************************************************************/
#include <time_personal.h>

// 名前空間の使用を宣言
using namespace GPS;

//------------------------------------------------------
GPS::Time timeA(20130106ul, 15200l); // 15200l: 1:52:00 and set UTC, 15200ul: TOW == 15200 and set GPST
const int testpin = A3;
int pinState = LOW;
//------------------------------------------------------
// 時刻オブジェクトの情報を出力
void tprint(GPS::Time* time)                 // テストでは、名前空間を省略できなかった。なぜ?
{
  Serial.print(time->GetTimeSystemName());  // 時刻系名
  Serial.print(", ");
  Serial.print(time->getYear());            // 年月日
  Serial.print("/");
  Serial.print(time->getMonth());
  Serial.print("/");
  Serial.print(time->getDay());
  Serial.print(", hhmmss: ");               // 時刻
  Serial.print(time->getClock());
  Serial.print(", Week no.: ");             // 曜日番号(0:日曜日…6:土曜日)
  Serial.print(time->getWeek());
  Serial.print(", Week name: ");            // 曜日名
  Serial.print(time->GetWeekName());
  Serial.print(", GPSW: ");                 // GPS週番号[week]
  Serial.print(time->getGpsWeek());
  Serial.print(", GPS TOW: ");              // GPS週秒[s]
  Serial.print(time->getGpsTow());
  Serial.print(", DOY: ");                  // 年間の通算日[day]
  Serial.print(time->getDOY());
  Serial.print(", GPST: ");                 // GPS時刻(1980/1/6基準)[s]
  Serial.println(time->getGpsTime());
  return;
}

void setup() {
  // test pin
  pinMode(testpin, OUTPUT);
  // initialize serial communication at 9600 bits per second:
  Serial.begin(115200);        // 処理の遅さを見やすいようにArduinoでは最大の通信速度に設定している。
  Serial.println("This is time class obj. test program.");
  // まずはコピーコンストラクタと代入演算子のテスト
  // 代入や初期化によって、ディープコピー(全メンバの実態(インスタンス)がコピー元とは異なるメモリアドレスに確保されること)がなされることを確認します。
  // もしシャローコピーが欲しい場合はポインタを使ってください。
  Serial.println("\nDeep-Copy test.");
  GPS::Time timeB = timeA;             // コピーコンストラクタも完備しているので、ディープコピーが可能
  GPS::Time timeC;
  timeC = timeA;                       // 代入演算子も完備しているので、ディープコピーになる
  Serial.print("B raw  : ");
  tprint(&timeB);                      // 時刻を表示
  Serial.print("B      : ");
  timeB.Increase_Clock(86400l);        // 時刻を86400秒進める(最大はlong intで表現できる最大値==2147483647l)
  tprint(&timeB);                      // 時刻を表示
  Serial.print("C      : ");
  timeC.Increase_Clock(-43200l);       // 時刻を後退させる
  tprint(&timeC);                      // 時刻を表示
  Serial.print("A pre  : ");
  tprint(&timeA);                      // 元のオブジェクトを表示させて、シャローコピーでなかったことを示す(コピーへの操作がコピー元へ影響していないことを示す)
  // 次に、時刻系の変更をテスト
    timeA.SetTimeSystem(Time::GPST);   // 時刻系をGPSTへ切り替える(時刻系がUTCの場合、うるう秒だけ時間を進める)
  Serial.print("A post : ");          // GPS時刻へ影響していないことに注目して欲しい
  tprint(&timeA);
  timeA.SetTimeSystem(Time::UTC);      // 時刻系をUTCへ切り替える
  Serial.print("A post2: ");
  tprint(&timeA);
  // 次に、比較のテスト
  // 時刻系の差を吸収して比較しています。
  Serial.println("\nCompare test.");
  const char *tf[2] = {
    "False", "True"    };
  Serial.print("A > B : ");
  Serial.println(tf[timeA > timeB]);
  Serial.print("A < B : ");
  Serial.println(tf[timeA < timeB]);
  Serial.print("A == B: ");
  Serial.println(tf[timeA == timeB]);
  Serial.print("A > C : ");
  Serial.println(tf[timeA > timeC]);
  Serial.print("A < C : ");
  Serial.println(tf[timeA < timeC]);
  Serial.print("A == C: ");
  Serial.println(tf[timeA == timeC]);
  delay(3000); 
}

// ループ
//  通常はGPSの出力センテンスかPPS信号、若しくはRTC/GPS受信機のPPS信号で時刻を加算します。
// Arduinoのライブラリは内部でタイマを一瞬だけ停止させることがあるので、外部にクロック源があった方が良いです。
// なお、加算はいつでもlong intの範囲内で動作するため、例えば6 s毎にスリープから目覚める設定で動作させる場合は6をIncrease_Clock()に対して渡せばOKです。
//  処理の方法は、普段は時刻を知る必要はないが時々は要るという場合に合わせて、指定された時間の加算量をカウントしておき、必要になったら時刻を更新する・・・というものです。
// カウントアップのみの処理にかかる時間を計測すると、Arduino UNOで3.467 μs/timeかかっていました。
// UNOは動作周波数が16 MHzですから、時刻の加算のみで56クロック/timeも必要なわけです。
// 表示のために、clock(hhmmss)以外の時刻を更新する(最初の取得で自動的に計算される)には、約16 μs必要でした。
// 2度目の取得には時間はかかりません。
// なお、clockが必要な場合は、加えて約160 μsが必要です。
// clockも2度目以降は取得に時間はかかりません。
// ただし、時刻の更新が行われていないことが条件です。
// 低消費電力動作中のマイコンへ移植する際はこの計算コストを頭に入れておいてください。
void loop() {
  for(long int i = 0l; i < 604800l; i++) // ループを回して、時刻演算の重さを測る。 604800は1 weekの秒数。
  {
    timeA.Increase_Clock(1l);           // 時刻を1 s加算する。マイナスにも対応しています。
    // オシロがあるならピンの出力で計測した方が正確かと思ったが、そうでもなかった。
    //if (pinState == LOW)
    //  pinState = HIGH;
    //else
    //  pinState = LOW;
    //digitalWrite(testpin, pinState);
  }
  // シリアルで確認する場合はこちら
  Serial.print("A: ");
  tprint(&timeA);                       // 時刻を表示
}

図3.1 サンプルスケッチ[2013/1/9リリースのサンプルスケッチ]


4. Arduino内部のTimer2を使った時刻ドリフト量計測とその結果 [2011/6/9リリースのソースコードを使って]

[2011/7/27]訂正を入れました。

気象観測を行うことを念頭に、Arduinoの時刻ドリフト量を調べました。 時刻のドリフト速度の計測には、Tera Termを使いました。 Tera Termはシリアル通信のログを取る機能があり、システム時刻(パソコンの時計)をタイムスタンプとして受信データに付加することができます。 この時刻とシリアルで受信されるデータに含まれた時刻データの差が時間とともにどのように変化するのかを調べます。 ここで、パソコンの時計はネットワークに接続されていませんでしたので10ppm程度のドリフトがあることに留意して下さい。 これはパソコンの時計が一日当たり、10×10-6×86,400(==24h×3,600s)=0.864秒ずれ得ることを意味しています。 本実験では気象観測用に使用できるかを調べたいだけですので10ppmのバイアスは無視することとします。

シリアルの受信データの様子を図4.1に示します。 時刻データはこの様に加工されて出力されます。

シリアル受信データの様子
図4.1 シリアル受信データの様子

受信されたログデータをExcelで処理した結果が図4.2です。 データの最初の方でデータが飛んでいるのは、Tera termが付加するタイムスタンプ値が更新されなかったために生じたものです。 これはPC側の問題でArduinoからの受信データには問題はありませんでした。 なお、約6ppm60.6ppmの時刻ドリフトが確認されました。 思ったより少ないですね? 気温やパソコンとArduinoのドリフトの進む方向(正か負か)によっても変わるので厳密にどちらがどうとは分かりません。

時刻ドリフト
図4.2 時刻ドリフト

時刻のドリフト量は6ppm60.6ppmでしたので実際のドリフト量を余裕を見て20ppm70ppmとすれば、時刻のずれは1日あたり2秒以下6.05秒となります。 これなら1ヶ月間観測を続けたとしても1分3分のずれにしかなりません。 気温の計測等には全く問題ないかと考えられます。

[2011/7/28 2011/6/9リリースのサンプルスケッチに関する追記]
SCP1000LCD表示機のSB1602と同時に使ってみたのですが、どうやら気圧計測時LCD制御時にタイマー2が一時停止されるらしく、時刻の遅れが1日で5分ほどとなりました。 どうやら使用するライブラリとの相性があるようですので、長期間の観測を行う際は一度テストしてからお使いください。


5. 2011/6/9リリースのソースコードを使った動作例

時刻の同期をシリアルからの入力で行う例を以下に示します。


図5.1 時刻同期の例(フルスクリーンにすると見やすいです)


6. 終わりに

本ページでは、特に省電力性が要求され且つ時刻情報も必要となる組み込みに最適なライブラリをご紹介しました。 本ライブラリを用いた応用によって、データロガー系の発展と、マイコンとGPS受信機とのコラボレーションがより容易になることを期待しています。

inserted by FC2 system