2010年11月23日火曜日

定点観測データロガー -2 : EEPROMにデータを保存する


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

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


< 持続した時刻情報の保存 >

前回のWatchdogとスリープ制御によって、定点観測ロガーの電力補給問題はなんとかなりそうである。
まだ実験途中だけど、スリープと乾電池4個を使えば最低でも1週間は持つだろう。

もう一つの問題は持続した正確な時刻情報の記録だ。
電池とデータ保存用のSDカードを交換する時には当然電源を切らなくてはならないけれど、
Arduinoには電源を切ってもカウントを続けるPCのような内蔵時計の機能はない。
しかしデータの計測にはある程度正確な時刻情報がともなっていないと意味がないのである。
例えばDatetimeライブラリを使用するとして、setup()関数で日付をセットしても、
電源を切った途端に日付情報は消えてしまう。
電池とSDカードを交換した後に、現在の時刻情報をもう一度スケッチにセットしてArduinoにアップロードしなくてはならない。
フィールドにPCを持ち歩くのは面倒なのでやりたくない。

解決方法として、ArduinoのEEPROMメモリ領域に時刻情報を記録することにした。
EEPROMはArduinoの電源を切っても消えないメモリ領域である(Arduino 日本語リファレンス EEPROMの項を参照)。
Arduino Pro 328 3.3V 8MHzの場合はATmega328チップなので、1024byteの情報を読み書きできる。
使い方は以下のようになる。
  1. SDカードに環境情報を記録するタイミングでEEPROMにも時刻情報のみを記録する。
  2. 電源を切る(電池とSDカード交換)。
  3. 起動。
  4. EEPROM領域に時刻情報が記録されていればそれを使い、記録されていなければDatetimeライブラリで初期化した時刻情報を使う(初めて畑にロガーをセットする時のみ)。setup()関数内に処理を書いておけばいい。

時刻情報はタイムスタンプとして記録する。
タイムスタンプとは1970年1月1日00:00:00からの経過秒数のことで、たいていの言語で簡単に日付時刻に変換できるので、
コンピュータの世界では時刻情報として利用されている。
例えば2010年11月23日14時8分40秒(日本標準時)のタイムスタンプは「1290456520」といった感じ。

UNIXタイムスタンプ変換ツール

タイムスタンプはlong型のデータ情報で、EEPROM領域を4byte使用する。
データ書き換え時に古いタイムスタンプ情報を上書きしてしまえばいいので、1024byteのメモリ容量でも十分である。


< タイムスタンプ情報(4 Byte値)をEEPROMに書き込み、読み込む >



/*
 * sketch name   : eeprom_r_w_4B
 * summary       : タイムスタンプ情報(4 Byte値)をEEPROMに書き込み、読み込む
 */

#include <EEPROM.h>
#include <DateTime.h>

// log start date
int year   = 2010;
int month  = 11;
int day    = 20;
int hour   = 18;
int minute = 53;
int second = 0;

long log_timestamp;

void setup(void) {
  Serial.begin(9600);
  DateTime.sync(DateTime.makeTime(second, minute, hour, day, month, year));
}

void loop() {
  log_timestamp = DateTime.now();
  Serial.print(" write timestamp / ");
  Serial.println(log_timestamp, DEC);
  
  eep4Bwrite(0, log_timestamp);  // 書き込み
  log_timestamp = eep4Bread(0);  // 読み込み
  
  Serial.print(" read timestamp / ");
  Serial.println(log_timestamp, DEC);
  Serial.println();
  
  delay(3000);
}

/*
 * func name  : eep4Bwrite
 * processing : 4byte値をEEPROMに書き出す
 * param      : int  address / 書き出し開始EEPROMアドレス
 *              long param   / 書き出す値
 * return     : 
 */
void eep4Bwrite(int address, long param)
{
  int  l_shift;     // 左シフトbit数
  long inleft;
  byte in_bytes;    // EEPROM格納用byte値
  int  eep_address; // EEPROM上のアドレス
  int  i;
  for (i = 0; i <= 3; i++) {
    l_shift     = 8 * (3 - i);
    inleft      = param  << l_shift;     // 左にシフト
    in_bytes    = inleft >> 24;          // 右にシフト
    eep_address = address + i;
    EEPROM.write(eep_address, in_bytes);  // 書き込み
    
    //デバック用
//    Serial.print(" eeprom address : ");
//    Serial.print(eep_address,DEC);
//    Serial.print(" / byte : ");
//    Serial.println(in_bytes,BIN);
//    Serial.println(" ");
  }
}

/*
 * func name  : eep4Bread
 * processing : 4byte値をEEPROMから読み出す
 * param      : int  address   / 読み出し開始EEPROMアドレス
 * return     : long ret_param / 4byte値
 */
long eep4Bread(int address)
{
  int  eep_address;   // EEPROM上のアドレス
  long inleft;        // EEPROM格納用byte値
  int  l_shift;       // 左シフトbit数
  long in_bytes;      // byte値格納用
  long ret_param = 0; // リターン値初期化
  int  i;
  for (i = 0; i <= 3; i++) {
    eep_address = address + i;
    inleft      = EEPROM.read(eep_address);  // 読み込み
    l_shift     = 8 * i;
    in_bytes    = inleft << l_shift;
    ret_param  |= in_bytes;
    
    //デバック用
//    Serial.print(" read test eeprom address : ");
//    Serial.print(eep_address,DEC);
//    Serial.print(" / byte : ");
//    Serial.println(ret_param,BIN);
//    Serial.println(" ");
  }
  return ret_param;
}


Arduino Pro 328 3.3V 8MHzにスケッチをアップロードするには3.3V動作Arduino製品向け小型USB-シリアルアダプタとUSBケーブル(Aオス-miniBタイプ)が必要である。
スケッチの実行は、Arduino Pro 328 3.3V 8MHzとPCをただ繋ぐだけである。

シリアルモニタで確認



スケッチは、以下のリンク先を参考にさせて頂きました。ありがとうございます。
Reflection of my mind arduinoのEEPROMテスト

リンク先のスケッチを理解してEEPROM書き込み関数を作成するにあたってビット演算を少し勉強する必要があった。
タイムスタンプを格納しているunsigned long型というのは符号なし整数型の4byteのデータ型だけど、
まずは"4byte"とはなんだ?というレベルからである。
およばずながら、理解できた範囲で確認してみる。

正の整数を格納するデータ型は

byte型(1byte)
unsigned int型(2byte)
unsigned long型(4byte)

がある。
これは2進数であらわすと、

■ byte型(10進数では255 〜 0、つまり2の8乗〜2の0乗。1byte。8bit。)
11111111 〜 00000000

■ unsigned int型(10進数では65,535 〜 0、つまり2の16乗〜2の0乗。2byte。16bit。)
11111111 11111111 〜 00000000 00000000

■ unsigned long型(10進数では4,294,967,2952 〜 0、つまり2の32乗〜2の0乗。4byte。32bit。)
11111111 11111111 11111111 11111111 〜 00000000 00000000 00000000 00000000

となる。

EEPROM.write()メソッドは1byteづつしか書き込めない。
ということは、タイムスタンプ型のunsigned long型は4回に分けて書き込む必要があるということになる。

eep4Bwrite()関数の

  for (i = 0; i <= 3; i++) {
    l_shift     = 8 * (3 - i);
    inleft      = param  << l_shift;     // 左にシフト
    in_bytes    = inleft >> 24;          // 右にシフト
    eep_address = address + i;
    EEPROM.write(eep_address, in_bytes);  // 書き込み
  }
の箇所でやっていることがそれである。

引数 "param"(4byteのタイムスタンプ情報)の値を仮に「1290456520」とすると、
2進数では「01001100 11101010 11001101 11001000」となる。

2進数、8進数、10進数、16進数相互変換

これを、「01001100」と「11101010」と「11001101」と「11001000」の4つの値(1byte)に分解して
EEPROM.write()メソッドで書き込む。

for文ループの0回目で、
「1290456520」を左に24bitシフトし、long型変数 "inleft" に格納する。
右に24bitシフトして、byte型変数 "in_bytes" に格納する。
ということをやっている。

←に24bitシフト
01001100 11101010 11001101 11001000
                                 ↓
11001000 00000000 00000000 00000000

→に24bitシフト
11001000 00000000 00000000 00000000
                                 ↓
00000000 00000000 00000000 11001000

できあがった値「00000000 00000000 00000000 11001000」は、
byte型変数 "in_bytes" に2進数で「11001000」という値として格納される。
それをすかさずEEPROMメモリアドレスの0番地に保存する。

同様に、

for文の1回目 : in_bytes = 11101010、EEPROMメモリアドレスの1番地に保存
for文の2回目 : in_bytes = 11001101、EEPROMメモリアドレスの2番地に保存
for文の3回目 : in_bytes = 11001000、EEPROMメモリアドレスの3番地に保存

と繰り返せば、long型のタイムスタンプ情報がEEPROMメモリアドレスの0〜3番地に保存できる。

読み込みの eep4Bread()関数ではこの逆の処理をやっている。
for文ループの0回目で、
EEPROMアドレス0番地の値「01001100」を読み込み、左に0bitシフトし、long型(4byte格納)変数 "in_bytes" に格納する。

←に0bitシフト
00000000 00000000 00000000 11001000
                                 ↓
00000000 00000000 00000000 11001000

そして、値0が格納されたlong型変数 "ret_param"(値は2進数で「00000000 00000000 00000000 00000000」)と
"|"(OR演算子)でbit演算した値を"ret_param"に格納する。

    ret_param  |= in_bytes;

は、わかりやすく書くと

    ret_param  = ret_param | in_bytes;

という処理をしている。
"|"(OR演算子)をかけると、

00000000 00000000 00000000 00000000 : ret_param
00000000 00000000 00000000 11001000 : in_bytes
----------------------------------------------------------------------
00000000 00000000 00000000 11001000 : ret_paramに格納

つまり、"ret_param" には「00000000 00000000 00000000 11001000」が格納される。
同様に、

for文の1回目 :
00000000 00000000 00000000 11001000 : ret_param
00000000 00000000 11001101 00000000 : in_bytes
----------------------------------------------------------------------
00000000 00000000 11001101 11001000 : ret_paramに格納

for文の2回目 :
00000000 00000000 11001101 11001000 : ret_param
00000000 11101010 00000000 00000000 : in_bytes
----------------------------------------------------------------------
00000000 11101010 11001101 11001000 : ret_paramに格納

for文の3回目 :
00000000 11101010 11001101 11001000 : ret_param
01001100 00000000 00000000 00000000 : in_bytes
----------------------------------------------------------------------
01001100 11101010 11001101 11001000 : ret_paramに格納

となり、long型変数 "ret_param" には最終的に「01001100 11101010 11001101 11001000」、つまりもとのタイムスタンプ「1290456520」が格納される。


< I2C通信を使い、タイムスタンプ情報(4 Byte値)を外部EEPROMに書き込み、読み込む >




ArduinoのEEPROMに値を格納できたけれど、ひとつ問題がある。
ArduinoのEEPROMの読み書き回数は、公称100000回だそうだ。
タイムスタンプ情報を一回書き込むためには4回の書き込みが必要である。
畑に設置する定点観測器で計測間隔を5分にするとして、1時間で48回、
1日で1152回もの書き込み処理が発生することになる。
つまり、たった86日間で書き込み限度回数に達してしまうのである。
三ヶ月弱でArduinoを買い替えるのは経済的に無駄である。
そこで、内部EEPROMの代わりに、Arduinoに外部接続できる非常に安価なEEPROM、24LC64を使用する事にする。
24LC64は公称書き込み限度回数1000000回、秋月だと60円で購入できる。
上記の計算だと28ヶ月持つ。
コストが安くて設置も簡単だけど、あえて欠点を挙げるならばアナログピンを二つ使用しなければならないところ。

回路は、以下のリンク先を参考にさせて頂きました。ありがとうございます。
なんでも作っちゃう、かも。 Arduino で遊ぼう - 大容量EEPROMに毎日の温度変化を保存する





/*
 * sketch name   : eeprom_I2C_r_w_4B
 * summary       : タイムスタンプ情報(4 Byte値)を外部EEPROMに書き込み、読み込む
 */

#include <Wire.h>
#include <DateTime.h>

// log start date
int year   = 2010;
int month  = 11;
int day    = 20;
int hour   = 20;
int minute = 45;
int second = 0;

#define EEPROM_DEV_ADDRESS 0x50  // I2CEEPROM 24LC64

long log_timestamp;

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

void setup(void) {
  Serial.begin(9600);
  pinMode(LED_PIN, OUTPUT);
  Wire.begin(EEPROM_DEV_ADDRESS);
  DateTime.sync(DateTime.makeTime(second, minute, hour, day, month, year));
}

void loop() {
  digitalWrite(LED_PIN, HIGH);
  
  log_timestamp = DateTime.now();
  
  Serial.println("Timestamp is written to the EEPROM...");
  Serial.print("  DEC : ");
  Serial.println(log_timestamp, DEC);
  Serial.print("  BIN : ");
  Serial.println(log_timestamp, BIN);
  Serial.println(" ");
    
  i2cEeprom4BWrite(EEPROM_DEV_ADDRESS, 0, log_timestamp);  // 外部EEPROMにタイムスタンプ情報(4byte値)を書き込む
  log_timestamp = i2cEeprom4BRead(EEPROM_DEV_ADDRESS, 0);  // 外部EEPROMからタイムスタンプ情報(4byte値)を読み込む
  
  Serial.println("Time stamp read from the EEPROM...");
  Serial.print("  DEC : ");
  Serial.println(log_timestamp, DEC);
  Serial.print("  BIN : ");
  Serial.println(log_timestamp, BIN);
  Serial.println(" ");
  
  digitalWrite(LED_PIN, LOW);
  
  delay(5000);
}

/*
 * 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;
}


次回は入力するセンサについて実験してみます。


< 参考リンク >


0 コメント:

コメントを投稿