2010年11月26日金曜日

定点観測データロガー -5 : タイムスタンプ情報の持続テスト


フィールド設置型の定点観測ロガーを作成する。

< 定点観測器の仕様 >
  1. 計測内容は、計測時刻・気温・湿度・温度・照度とする。
  2. Arduino Pro 328 3.3V 8MHz をスタンドアロンで動作させる。
  3. 単三乾電池4個を電源とし、最低一週間は稼働する省電力設計とする。
  4. 設定した間隔で持続的にデータを記録できるものとする。
  5. 定期的に記録データ媒体と乾電池を交換できる設計とする。
  6. 計測データの保存はSDカードを使用する。

< SDカードとEEPROMを使い、タイムスタンプを持続させる >


ArduinoはPCのような持続してカウントする内蔵時計を持たない。
電源を切ると消えてしまう時刻情報を維持させるために、こちらにはGPSを利用した高精度な時計の例がある。
衛星に搭載された原子時計の時刻情報を使うので、通信のために数100msほどの遅れはでるものの、
リアルタイムの時刻情報をその都度取得することができる。
わくわくするような面白いプロジェクトだけど、アウトドアに設置することを考えると電力消費が心配だった。
それに、今後の展開としてこの定点観測ロガーを量産できるようにコストを押さえていきたい。
苦肉の策かもしれないけれど、I2C通信を利用した外部EEPROMを使用し、時刻情報を保存することにした。

以下の回路はこちらこちらを組み合わせたものである。





/*
 * sketch name   : logging_timestamp
 * summary       : 指定のスリープ間隔ごとにタイムスタンプをSDカードに記録する
 *                 SDカードと電池は取り替える事を前提とするため、タイムスタンプ情報は外部EEPROMに保存する
 */

#include <Wire.h>
#include <DateTime.h>
#include <Fat16.h>
#include <Fat16util.h>
#include <Watchdog.h>
#include <Sleep.h>

/* Arduino 基準電圧 */
#define ARDUINO_VCC 3.3

/* 時刻関連 */
// 計測タイムスタンプ初期値(初回セッティング時のみ)
unsigned int year   = 2010;
unsigned int month  = 11;
unsigned int day    = 27;
unsigned int hour   = 0;
unsigned int minute = 0;
unsigned int second = 0;

#define LOGGING_INTERVAL_SEC 10           // 計測間隔(秒)
#define RETOUCH_INTERVAL_SEC 60          // 計測間隔によるタイムスタンプのずれの修正間隔(秒) ※必ず LOGGING_INTERVAL_SEC < RETOUCH_INTERVAL_SEC
#define WATCHDOG_TIMER_SLEEP_INTERVAL 1   // スリープ間隔(1秒固定)
unsigned long log_timestamp;              // 計測時刻タイムスタンプ
unsigned long time1;                      // 計測開始時刻(ms)
unsigned long time2;                      // 計測終了時刻(ms)
unsigned long Integrated_time;            // 計測間隔の集積
unsigned int  cnt_interval;               // スリープカウント
unsigned int  cnt_interval_full;          // 計測までのスリープカウント数
unsigned long retouch_cnt_interval_full;  // タイムスタンプ修正までのカウント(秒)

/* 外部EEPROMアドレス */
#define EEPROM_DEV_ADDRESS 0x50

/* SDカード関連 */
#define SD_ERROR_OK         0
#define SD_ERROR_CARD_INIT  1
#define SD_ERROR_FAT16_INIT 2
#define SD_ERROR_OPEN       3
#define SD_ERROR_WRITE      4
#define SD_ERROR_CLOSE      5
unsigned int sd_error_no;  // SDエラー番号格納
// エラー文字列
char *sd_error[] = {
       "no error.",
       "error about sd card init.",
       "error about sd fat16 init.",
       "error about sd open.",
       "error about sd write.",
       "error about sd close.",
     };
#define LOG_FILE_NAME "log_data.csv"  // 記録ファイル名

/* 計測時の確認用点滅LED */
#define LED_PIN 9

/* ライブラリのインスタンス化 */
WatchdogClass obj_watchdog = WatchdogClass();
SdCard        obj_card;
Fat16         obj_file;

void setup(void) {
  Serial.begin(9600);
  
  pinMode(LED_PIN, OUTPUT);
  
  // 外部EEPROM(I2Cデバイス)に接続
  Wire.begin(EEPROM_DEV_ADDRESS);
  
  // Watchdogとスリープの設定
  obj_watchdog.systemResetEnable(false);                                             // スリープからの自動復帰時にスケッチがリセットされないように設定
  obj_watchdog.enable(WatchdogClass::TimeOut1s);                                     // スリープ間隔を1秒間にセット
  cnt_interval_full         = LOGGING_INTERVAL_SEC / WATCHDOG_TIMER_SLEEP_INTERVAL;  // 1秒間に設定されたスリープの繰り返し回数
  cnt_interval              = 0;                                                     // 計測間隔カウンタ初期化
  retouch_cnt_interval_full = 0;                                                     // タイムスタンプ修正用カウンタ初期化
  
  // タイムスタンプ初期化
  log_timestamp = i2cEeprom4BRead(EEPROM_DEV_ADDRESS, 0);                            // 外部EEPROMにタイムスタンプが記録されていたら読み込む
  if (log_timestamp == 0) {                                                          // そうでないならばDatetime設定値(最初のセッティング時のみ)
    DateTime.sync(DateTime.makeTime(second, minute, hour, day, month, year));        // DateTime初期化
    log_timestamp = DateTime.now();
  }
}


void loop()
{
  // スリープ開始から設定間隔で自動復帰
  
  time1 = micros();
  
  // 計測と記録
  if ((cnt_interval >= cnt_interval_full)) {
    
   /* 処理確認用LED ON */
    digitalWrite(LED_PIN, HIGH);
    
    
   /* 時刻関連処理 */
    // タイムスタンプ追加
    log_timestamp += LOGGING_INTERVAL_SEC;
    
    // タイムスタンプを外部EEPROMに記録
    i2cEeprom4BWrite(EEPROM_DEV_ADDRESS, 0, log_timestamp);
    
    // スリープカウント初期化
    cnt_interval = 0;
    
    // タイムスタンプ修正までのカウントを加える
    retouch_cnt_interval_full += LOGGING_INTERVAL_SEC;
    
    
   /* SDカードに記録 */
    sd_error_no = dataToSdFileWrite (log_timestamp);
    if (sd_error_no != SD_ERROR_OK) {
      Serial.println(sd_error[sd_error_no]);
    }
    
    
   /* 処理確認用LED OFF */
    digitalWrite(LED_PIN, LOW);
    
  // カウントアップ
  } else {
    
    cnt_interval++;
  }
  
  // 計測間隔による時刻カウントのずれの修正
  time2            = micros();
  Integrated_time += (time2 - time1);
  if (retouch_cnt_interval_full >= RETOUCH_INTERVAL_SEC) {
    cnt_interval             += (int)(Integrated_time / 1000000) / WATCHDOG_TIMER_SLEEP_INTERVAL;
    retouch_cnt_interval_full = 0;
    Integrated_time           = 0;
  }
  
  // スリープ開始
  WatchdogClass::timerReset();
  SleepClass::powerDown();
}


/*
 * func name  : dataToSdFileWrite
 * processing : SDカード記録
 * param      : in_sd    / データ格納配列
 *              cnt_data / データ数
 * return     : エラー番号
 */
int dataToSdFileWrite (unsigned long log_timestamp)
{
  if (!obj_card.init()) return SD_ERROR_CARD_INIT;
  if (!Fat16::init(&obj_card)) return SD_ERROR_FAT16_INIT;
  // O_CREAT  : ファイルが存在しなかったら作成する
  // O_APPEND : 書き込み前に、ファイル内のデータの最後尾を探す(追記)
  // O_WRITE  : 書き込みのためにファイルを開く
  if (!obj_file.open(LOG_FILE_NAME, O_CREAT | O_APPEND | O_WRITE)) return SD_ERROR_OPEN;
  obj_file.writeError = false;
  
  // タイムスタンプ記録
  obj_file.print(log_timestamp);
  obj_file.println();
  
  if (obj_file.writeError) return SD_ERROR_WRITE;
  if (!obj_file.close()) return SD_ERROR_CLOSE;
  
  return SD_ERROR_OK;
}

/*
 * func name  : i2cEeprom4BWrite
 * processing : 4byte値をI2C外部EEPROMに書き出す
 * param      : deviceAddress      / I2C外部EEPROMのデバイスアドレス
 *              startMemoryAddress / EEPROM上の書き込み開始アドレス
 *              longParam          / 書き込む4byteのデータ
 * summary    : デバイスの0番目の番地 / 32〜25bit
 *                        1番目の番地 / 24〜17bit
 *                        2番目の番地 / 16〜9bit
 *                        3番目の番地 / 8〜1bit
 * return     : 
*/
void i2cEeprom4BWrite(int deviceAddress, unsigned long startMemoryAddress, long longParam)
{
  Wire.beginTransmission(deviceAddress);
  Wire.send((int)(startMemoryAddress >> 8));
  Wire.send((int)(startMemoryAddress & 0xFF));
  
  byte byteArray[sizeof(long)] = {
         (byte)(longParam >> 24),
         (byte)(longParam >> 16),
         (byte)(longParam >> 8),
         (byte)(longParam >> 0)
       };
  
  for (int i = 0; i < sizeof(long); i++) {
    Wire.send(byteArray[i]);
  }
  
  Wire.endTransmission();
  delay(5);
}

/*
 * func name  : i2cEeprom4BRead
 * processing : 4byte値をI2C外部EEPROMから読み出す
 * param      : deviceAddress      / I2C外部EEPROMのデバイスアドレス
 *              startMemoryAddress / EEPROM上の読み込み開始アドレス
 * return     : return_long        / 読み込んだ4byteデータ 
*/
long i2cEeprom4BRead(int deviceAddress, unsigned long startMemoryAddress)
{
  Wire.beginTransmission(deviceAddress);
  Wire.send((int)(startMemoryAddress >> 8));
  Wire.send((int)(startMemoryAddress & 0xFF));
  Wire.endTransmission();
  Wire.requestFrom(deviceAddress, sizeof(long));
  delay(5);
  
  long received_long;
  long return_long = 0;
  int  cnt = sizeof(long) - 1;
  
  for (int i = 0; i <= cnt; i++) {
    if (Wire.available()) {
      received_long = Wire.receive();
      return_long  |= (received_long << (8 * (cnt - i)));
    }
  }

  return return_long;
}



今回のスケッチはちょっと長め。
まず、WatchdogとSleepを使用して省電力をはかる。
Sleep間隔は固定の1秒とし、計測間隔(今回はセンシングはなし)を10秒とする。
つまり、単純計算で10回スリープを繰り返した後に計測することになる。
しかし、計測とSDカード・EEPROMへのデータ書き込み処理時間が結構かかることを想定し、
スリープターンごとに処理時間を集積しつつ60秒に一回タイムスタンプの修正処理を行っている。
タイムスタンプ修正後の直近の計測間隔は、集積された計測処理分だけ短くなることになる。
強引だけど、このようにしてタイムスタンプの帳尻を合わせている。
計測とデータ保存期間はLEDを点灯させる。
ある程度うまくいったら一度電源をOFFにし、再び起動させて、EEPROMに保存されたタイムスタンプが
続けて使用されるかどうか確かめる。

以下は一度電源をOFFにして、取り出したSDカードに保存されたタイムスタンプ情報。


SDカード情報を削除し、セットして再び起動させる。



電源をOFFにし、再び取り出したタイムスタンプ。
EEPROMから取り出したデータを引き続き使用することにより、前回からの持続したタイムスタンプが続いている。


この方法の欠点は、電源をOFFにして電池とSDカードを交換し、再び電源を入れるまでの間隔分、
実際の時刻情報がずれてしまうことである。
コストを下げた分、こういうところにしわ寄せがくる。
こればかりは交換時間をなるべく手早くやるしかない。
数週間に数秒のずれなのであまり気にする事もないのかもしれない。

次回は、センサも含めたすべてをセッティングして電力がどれくらい持つのかを実験する。

0 コメント:

コメントを投稿