公開日:2025/04/05
34th Season
#34-4 EEPROMをログストレージとして使う
前回購入したEEPROMモジュールをログストレージとして使う環境を整備して行こうと思います。
ログストレージ
他のセンサなどで計測したデータのログを取るために、前回購入したEEPROMモジュールを使おうと考えています(下図)。

本節では、データログを記録するための形式について考案したいと考えます。
ログに残す情報
ログとして残したい情報を考えてみると、日付、時間、測定データ、等、様々であり、かつ、状況によってデータ種は多岐に渡ることが判ります。
- ログに残すデータの性格次第では、
- 日付は不要かも知れない(短時間のログが目的の場合等)。
- 一定時間毎にログを残す用途では、
- データ数と一定時間との掛け算で相対時間が判るので、時間の記録も不要。
- 勿論、データ毎に日付・時間が必要なログも、考え得る。
- 測定データにしても、
- 温度、
- 湿度、
- 気圧、
- 加速度ベクタ、
- 等々、それこそ、色々なデータが考えられる
ログとして残すデータに関しては、柔軟に対応できるようにしておかなくては、ならない様です。
レコード
1回分のデータを記憶する最小単位をレコードと呼ぶ事とします。
前節で考察した様に、ログに残したい情報は様々なため、汎用的な形式だけを決めておき、個別のデータ形式については、用途毎に柔軟に決定できる事にしたいと思います。
まず、レコードサイズを決めてしまいましょう。「柔軟」と言う意味では、レコードサイズも「可変長」にしておいた方が、より柔軟ではあります。しかし、可変長データを選択してしまうと、レコードの始まりを探索したり、次にログデータを書き込む位置を探索したりすることが困難となることが予想できます。
処理の「容易さ」まで考慮に入れると、レコードサイズは固定しておいた方が良いでしょう。
例)日時について考えてみよう。
- 20xx年yy月zz日の数値データxx、yy、zz、をバイナリ値で記憶すると、
- xx(7ビット、00〜99)、yy(4ビット、1〜12)、zz(5ビット、1〜31)の、計16ビット=2バイト程。
- 後で閲覧し易い様に、BCDで記録するなら、6桁BCDであり、3バイト。
- 同様に時間hh:mm:ssをBCDで記録すると、6桁BCDで3バイト。
- 日付+時間で6バイト。
- ここへ、肝心な測定値などのデータを追加する事になる。
今回使うEEPROM(24C256)は?と言うと、ページ書き込みが可能で1ページ=64バイトなので、レコードサイズは64の約数(または倍数)であることが望ましいです。よって、レコードサイズの候補は、1,2,4,8,16,32,64,128,……です。
もし、日付+時間+データを一気に記録する用途では、16バイト未満のレコードサイズでは小さすぎます。
逆に、64バイト以上では冗長に思われます。EEPROMは容量が小さいので、レコードサイズを大きくし過ぎると、記録可能なレコード数が制限されてしまうからです。
残ったのは、レコードサイズ16バイトまたは32バイトなのですが、WireライブラリのBUFFER_LENGTH=32問題がありました。
24C256への書き込みには、レコードデータ以外に、書き込みアドレス2バイトを送信する必要があります。この2バイトもBUFFER_LENGTH=32に含まれるため、レコードサイズ32バイトを選択してしまうと、Wireライブラリでは1回で送信することができません。
Wireライブラリに依存しない、Arduino以外の環境(例えばBlockly for Moscoviumなど)では、32バイトのレコードを一度に書き込む事が可能ですが、Arduinoでも使いたいですからね……。
Wireライブラリの糞仕様に制限されるのは不本意ですが、致し方ありません。レコードサイズは16バイトに決定します。
レコード仕様
前節の考察を踏まえて、レコード仕様を以下の通り決定します。
レコード仕様:
- レコード索引(index): 2バイト(BE※)
- 記録する度に+1増加する索引。
- 0x0000と0xffffは、未使用レコードを示すコードで、索引としては使用不可。
- 索引は、0x0001〜0xfffeまで使ったら、0x0001に戻って繰り返す。
- 記録アドレス順に、差分1の昇順でなくてはならない。
- 記録アドレス昇順に、レコード索引を確認して行き、
- 0x0000または0xffffが見つかるか、あるいは、
- 差分が+1と異なった所が、次にレコードを書き込むアドレスである。
- ※筆者がBE(Big Endian)派である事も確かだけど、メモリダンプした時、数値を直読みできるBEの方がデバッグし易い。
- レコード形式(type): 2バイト
- ASCIIコードの印刷可能文字2文字とする。
- 例)'TX'=テキスト形式
- 例)'DT'=日付および時間
- それ以外の値が見つかった場合、レコード以外の記録データ(ごみデータ含む)と見なし、上書きして良いとする。
- レコードデータ: 12バイト
- レコード形式毎に自由に設定して構わない。
- バイナリ、BCD、テキストなども任意。
- データサイズもエンディアンも任意。
- 以上、2+2+12=16バイトを1レコードとする。
- もし、12バイトを超えるデータを記録したい場合は、
- 例えば、レコード形式='D1'と'D2'等に分割して、複数レコードで記録して良い。
レコード索引とレコード形式で整合性を確認する事で、前回記録した最終データ位置を見つけ出し、その続きから再開可能な仕様と、なっています。また、他の用途に使用していたEEPROMであっても、消去せずにログストレージとして流用可能な仕様でもあります。
※残っていた前のデータが、たまたま、整合性の取れるデータであった場合、その部分だけ、ごみデータが残ってしまうが、それはEEPROMの書き換え回数節約のため許容する。
24C256の場合、256Kビット=32Kバイトなので、レコードサイズ16バイトであれば、2,048個のレコードを記録可能です。
- 毎分のデータ記録で2,048分≒34時間のログが可能。
- 2秒間隔のデータ記録では、2,048×2s≒68分の記録ができる。
- 何らかのイベント毎に記録する場合、イベントの頻度次第。
なお、書き込み時間tWR=10ms(MAX)なので、1秒未満の高速なデータロギングが目的の場合は、書き込み時間にも注意してください。
ソフトウェア設計
レコード仕様に合わせて、EEPROMへレコードを書き込むためのAPI関数仕様を以下の通り制定します。
API関数仕様:
-
int eep_next_record (
uint16 *radr_nx=NULL,
uint16 *ridx_nx=NULL);
- EEPROMの全アドレス範囲をスキャンして、最後に記録されたレコードを見つけ出し、
- 次にレコードを書き込むアドレスradr_nxと、
- 次のレコードに記録すべきレコード索引ridx_nxを返します。
- setup()関数などで最初に一回実行して、書き込みアドレスとレコード索引を決定しておきます。
- 本関数を呼ぶ前に、ArduinoコアライブラリのrandomSeed()関数で、適切な乱数の種を設定してください。
- 例)
-
randomSeed (<seed>);
eep_radr=0;
eep_next_record (&eep_radr, &eep_ridx);
- 引数無しで実行すると、EEPROMの全アドレスをスキャンして、
- 整合性の取れたレコードをシリアルモニタへ送信(表示)します。
- 記録したレコードの簡易閲覧用です。
- グローバル変数eep_show_bin=trueを設定しておくと、12バイトのデータ部分を16進数で表示します。
- radr_nxに変数へのポインタを渡した場合、変数値のアドレス以降のレコードを探索します。
-
int eep_write_record (
int rtyph,
int rtypl,
int rcnt,
uint8 *rdat);
- 引数として、
- レコード形式:rtyph,rtypl
- ログデータ数:rcnt(≦12)
- ログデータ配列:rdat[ ]
- を受け取り、レコードをEEPROMへ書き込む関数です。
- 引数とは別に、以下の変数をグローバル変数から参照します。
- 書き込むアドレス:eep_radr
- 前レコードのアドレス+16で、レコードサイズ16の倍数アドレス。
- 書き込み後、レコードサイズ16バイト分、増加します。
- レコード索引:eep_ridx
- 前レコード索引からの+1増分で、0x0000および0xffffを除きます。
- 書き込み後、+1増加します。
- 例)
-
uint8 rdat[12]={ <record_data> };
eep_rec_write ('T','X', 12,rdat);
簡素に2つの関数だけとしました。
- 最初にsetup()関数などで、eep_next_record()関数を実行する。
- この処理によって、最後のレコードを見つけ、次に書き込むアドレスとレコード索引を決定しておく。
- データを計測するなどして、ログデータを記録したくなったら、
- eep_write_record()関数でEEPROMへレコードを記録する。
- データ取得後のEEPROMからログデータを閲覧するには、
- eep_next_record()関数を引数無しで実行すれば良い。
プログラム
完成したプログラムを以下にリンクします。
完成プログラム:eep_storage.ino
Arduino UNOとDUE、M5StickC Plusで動作確認してあります。
コンパイルしてArduinoへ書き込むと、シリアルモニタから以下のコマンドを受け付けています。
シリアルモニタコマンド仕様:
- scan
- I2Cスレーブアドレスをスキャンして、I2Cバスに接続されているスレーブデバイスのアドレスを表示します。
- slv,<slv_adr_hex>
- 以降、アクセス対象のI2Cスレーブアドレスを指定します。
- リセット解除後のデフォルトは、<slv_adr_hex>=50です。
- rd,<adr_hex>
- EEPROMから、アドレス<adr_hex>のデータを読み出します。
- <adr_hex>は4桁16進数です。
- wr,<adr_hex>,<dat1_hex>[,<dat2_hex>[,...]]
- EEPROMのアドレス<adr_hex>に、1バイト符号無整数データ<dat1_hex>を書き込みます。
- <adr_hex>、<dat*_hex>とも、16進数です。
- <dat2_hex>以降のデータが指定された場合、<adr_hex>を+1増加しながら、次々と書き込みます。
- dump[,<adr_hex>[,<siz>]]
- EEPROMのアドレス<adr_hex>からサイズ<siz>バイトのデータを読み出して表示します。
- <adr_hex>は16進数で、<siz>は10進数です。
- <adr_hex>は、16の倍数に切り下げて表示します。
- <adr_hex>が省略されると、全領域を表示します。
- <siz>が省略されると、<siz>=16と見なします。
- list[b][,<start_adr_hex>]
- EEPROMをスキャンして、現在記録されているレコードを表示します。
- 表示は、不整合なレコードが見つかった時点で停止します。
- 【list】コマンドでは、レコードのデータ部分を文字列として表示します。
- 【listb】コマンドでは、レコードのデータ部分をバイナリ値(16進数)として表示します。
- <start_adr_hex>は4桁の16進数です。
- <start_adr_hex>が指定されると、<start_adr_hex>から後ろのアドレスでレコードを探索します。
- 多くのレコードが記録された後で、既知のアドレスから後ろのレコードだけを表示したい時等に便利です。
- 本コマンドを実行すると、整合性の取れたレコードの最終データを検知して、書き込みアドレスとレコード索引を更新します。
- wrec,<type_asc2>,<rec_text>
- 現在の書き込みアドレスへ、レコード形式<type_asc2>でデータ<rec_text>を書き込みます。
- 書き込み後、書き込みアドレスと、レコード索引が増加して次の書き込みに備えます。
- <type_asc2>は、2桁のASCII印刷可能文字(任意)です。
- <rec_text>には、ASCIIテキストデータが指定可能です。
- 最大12バイトが有効です。
- 12バイトを超えた場合、先頭の12バイトが記録され、残りのデータは破棄されます。
- C言語文字列の終端NILコードは記録されません。
- 以下のエスケープシーケンスを使用可能です。
- \x<hex2>: <hex2>は2桁16進数です。バイナリデータ<hex2>で置換されます。
- \\: 記号バックスラッシュ(¥または\)に置換されます。
- 例)【wrec,TX,hello, world】
- 文字列データ"hello, world"をレコードデータとして書き込みます。
- C言語文字列の終端NILコード('\0')を含まず、ちょうど12バイト(12文字)です。
- レコード形式は'TX'です。
- erase,chip
- EEPROMの全領域に0xffを書き込みます。
- EEPROMは消去せずに上書きで使うのが基本です🙆。
- 本コマンドを実行すると、本当に実行して良いか問い合わせてくるので、適切に処置してください。
- who
- help
前回の予備実験を始めとして、今回制定したAPI関数群の動作検証を兼ねたプログラムです。
追加の実験
実験の準備は前回と同様なので、こちらをご参照ください。以下、実験中の様子も、前回の予備実験と同様です(下図)。

実験を進めましょう。
実験サンプル:【 】内はシリアルモニタからの送信データ
【wrec,TX,hello, world】
OK+ eep_radr=0x0000, eep_ridx=0x7fcd
OK: wrec
【wrec,DT,\x20\x25\x03\x29\x10\x32\x54】
OK: wrec
早速、ログレコード×2個をEEPROMへ書き込んでみました。プログラムとしては、eep_rec_write()関数が実行されました。
2つの目のサンプル(DTレコード)は、「2025年03月29日10時32分54秒」を、BCD(バイナリ値)で記録する例です。
【dump,0000,48】
OK+ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f : 0123456789abcdef
0000: 7f cd 54 58 68 65 6c 6c 6f 2c 20 77 6f 72 6c 64 : ..TXhello, world
0010: 7f ce 44 54 20 25 03 29 10 32 54 ff ff ff ff ff : ..DT %.).2T.....
0020: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff : ................
OK: dump 0x0000 ~ 48
【wrec】コマンドを実行した時点で、EEPROMが購入したままのまっさらな状態なら、上の様な結果が表示されるはずです。なお、先頭レコードのレコード索引(最初の2バイト、上例では0x7fcd)は乱数で決定しているため、皆様の実験では異なる値が読めるはずです。2個目以降のレコード索引は、直前のレコード索引+1で増加して行きます。
【list】
LOG+ 0x0000: 0x7fcd,'TX',hello, world
LOG+ 0x0010: 0x7fce,'DT', %\x03)\x102T\xff\xff\xff\xff\xff
LOG: found 2 records
OK: list: eep_radr=0x0020, eep_ridx=0x7fcf
eep_next_record()関数を引数無しで呼び出すと、上に示した結果の様に、EEPROMに記録されたレコードがシリアルモニタへ送信されてきます。簡易ビュアとしても使用可能な事が確認できました。以上をもって、作成したAPI関数の動作確認完了です。
Arduinoを使ってログを取得する場合は、こちらの関数群を使用できますし、仮に、別な環境を使ってログを取得する場合でも、こちらのレコード仕様に合わせておけば、今回作成したツールでログを閲覧することも可能です。そういう意味で、広く汎用的な仕様を策定して見ました。


