色々とアプリケーションを広げていくと、いくつかオプションを持たせて、動作させたいケースが出てきます。そんな時、昔使ったのは「DIPスイッチ(別ウインドウが開きます)」でした。
実際、DIPスイッチは便利なのですが、一々I/Oポートをつぶしてしまうのが難点です。そんな時、ATmegaに搭載された不揮発メモリEEPROMに設定値を記憶できる利点は大きいです。
EEPROMは、Electrically Erasable Programmable Read-Only Memoryの略語です。そのまま、「電気的に消去/書き込み可能な読み出し専用メモリ」ですが、「消去/書き込み可能」なのに、「読み出し専用」とは、矛盾してますよね。たぶん、コンピュータの歴史上、「ROM」と言う言葉に「不揮発メモリ」と言う意味が重畳されているためだと思われます。
ATmega328には、1KバイトのEEPROMが搭載されています。書き換え回数は100,000回で、プログラム格納用のFlashメモリより、一桁多く保証されています。
※Flashメモリは、消去/書き込み単位が「ページ」単位なので、1バイトそこそこのデータを書き換える度に「ページ」全体を消去/書き込みするのは現実的ではありません。その点でも、基本的にはプログラムを書き込むためのメモリなのだと思います。一方EEPROMは、1バイト(1メモリセル)単位で消去/書き込み可能なので、ちょっとしたデータの記憶用として重宝します。
EEPROMも、Flashも、電気的に書き換えする時に、少しづつメモリセルが劣化していくため、一定回数消去/書き込みを繰り返すと、正常に機能しなくなってしまいます。保証回数は、加速試験での試験結果に基づいて、ワーストセルの試験結果から導き出されています。
ATmega328のEEPROMは、その保証回数が100,000回です。仮に、1時間に1回、同じセルにデータを書き込んで、24時間365日フル稼働させると、約11.4年超で寿命を迎えることになります。
例えば、何か不具合が起きた時の解析用ログの記録用などとして定期的に書き込む場合は、セルの寿命に注意が必要です。さもないと、不具合が起きた時にログを確認しようとしたら、書き換え回数オーバで寿命を迎えていて、うまくデータが残っていなかったりします。
逆に考えてみると、1時間に1回&24時間365日の運用ですら、寿命まで約11.4年もかかるのだから、初期設定値の記録用としては十分な書き換え回数だと言えます。
例えば、EEPROMの特定のアドレスに、非同期シリアル通信時の通信速度を書き込んでおくと決めます。以降、自分が作成するすべてのプログラムで、setup()関数の先頭にシリアルの通信速度設定値をEEPROMから読み出し、Serial.begin(<baud>)の引数に渡すようにプログラムしておくと、毎回EEPROMに設定した通信速度でシリアルモニタを開くことができます。
後になって、「もっと高速に通信させたい」とか、「安定しないときがあるので、少し速度を落としたい」と思った時、EEPROMの値と、シリアルモニタの通信速度設定を変更するだけで、全てのプログラムの通信速度が変更できて、便利です。
※SDカードのようなデータ記録用に使うには、1Kバイトというサイズは小さすぎるので、そのような用途も、あまり意識しなくて良いはず。デフォルト設定値の記録用にせよ、何にせよ、使わないと損です。
Arduinoが提供する標準ライブラリにATmegaのEEPROMを制御するライブラリがあります。「#include <EEPROM.h>」をプログラムの先頭に記述しておけば、必要な関数を使えるようになります。
このライブラリは、EEPROMクラスになっています。以下、EEPROMクラスの要約です。
必要十分なメソッドが揃っていますので、適切に組み合わせて使えば良いですね。
実験用に作成したプログラムをリンクしておきます。
実験用プログラム:test_eeprom.ino
コンパイルしてUNOに書き込んだら、シリアルモニタを開いてください。以下のコマンドを受け付けるようになっています。
実験を進めましょう。
実験サンプル:【 】内はシリアルモニタからの送信データ
【dumpa】
OK+ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f
0000: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
0010: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
:
: (中略)
:
03e0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
03f0: ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff
OK: dumpa 1024[byte]
手持ちのUNOでは、今までEEPROMを使ったことは無かったのですが、全て0xffでした。たぶん、工場出荷時状態のはずなので、恐らく、ATmegaのEEPROMは消去状態が0xffなのでしょう。
※理由は知りませんが、消去状態が0xff(all 1)の不揮発メモリって多い気がします。昔、"H"系のマイコンを使っていた時に、nop命令が0x0000だったので、UV-EPROMに書き込んだプログラムの不要な命令を潰すのに重宝してました(別なデータが書き込まれていたとしても、0で上書きすることはできた)。
実験サンプル:【 】内はシリアルモニタからの送信データ
【wr,102,aa】
OK: write [0x0102]=0xaa
【wr,105,55】
OK: write [0x0105]=0x55
【dump,100,8】
OK+ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f
0100: ff ff aa ff ff 55 ff ff
OK: dump 0x0100 ~ 8
不揮発なので、一旦電源を切ってもデータは残っているはず。一度USBケーブルをPCから抜いて、数10秒後にもう一度接続します。シリアルモニタを開き直して、実験再開です。
実験サンプル:【 】内はシリアルモニタからの送信データ
【dump,100,8】
OK+ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f
0100: ff ff aa ff ff 55 ff ff
OK: dump 0x0100 ~ 8
ちゃんと、電源(USBケーブル)を切断する前に書き込んだ値が残っていました。当たり前ですけど、「RAM上に確保されたシャドウをアクセスして、書き込んだつもり」になっていたわけでは無く、「ちゃんとEEPROMに書き込めてましたよ」と言う確認ができました。
ATmega328のデータシートによると、データのリテンション時間は100年(@25℃)だそうです。「100年経ってもデータは消えません」って言うことですけど、そもそも、ATmega328本体が、100年後も起動して動くのかい?っていうくらいなレベルです。その前に、筆者がこの世に居なくなってるか……(1^^;)。
ちなみに、保管温度が85℃になると、一気にリテンション時間は25年に短縮してしまいますが、それでも十分な保持期間ですよね。
EEPROMは、何らかの設定データなど、ちょっとしたデータの記録用として重要なのですが、何らかのセンサで測定したデータの継続的記録用などに使うには、容量が小さすぎます。
やはり、大容量のデータ記録用としては、SDカードですね。「#6-5 microSDカードI/Fを自作する」で手作りしたSDカードI/Fを使って、色々なアプリケーションを考える上で、どうしても知っておかなければならない情報があります。
そう、SDカードにデータを書き込んだり、読み出したりする時、どのくらいの速度でアクセスできるのか?これを知っておかないと、データのバンド幅(バイト/s)を設計できません。と、言うことで、かなりこじつけ気味ですが、先程リンクしたプログラム(test_eeprom.ino)に、SDカードのアクセス速度を測定するコードを埋め込んであります。
以下が、アクセス速度の測定に使うコマンドの仕様です。
SDカードI/Fを接続してください。以下図の様な回路を作成してください。
万一、3.3V電源の電流容量不足で書き込みエラーが多発するなどして、アクセス速度に影響すると面倒なので、ACアダプタ+電源モジュールから3.3V電源を供給します。
これで、準備完了です。
測定に移る前に、ちょっと寄り道。SDカードの挿抜検出ですが、SD Associationのドキュメントによると、SDカードのCS端子(#1ピン、以下図)が50kΩでプルアップされているので、それを使って検出するのだそう。
プルアップの検出って、どうするんだ??50kΩより十分大きい抵抗で接地でもしとくの?よくわからないけど……。
どちらにしても、今回使用したレベルシフタは、プッシュプルタイプのドライバではなくて、プルアップ+オープンドレインっぽくなっているタイプなので、レベルシフタを使った時点で、既にプルアップされてしまっていて、SD Associationが用意した方法では、挿抜検出はできませんでした(1x_x)。
なので、SDカードを抜き/挿しする場合には、【umount】/【mount】コマンドで、手動マウントしてください。
プログラムをコンパイルしてSDカードI/Fを接続したUNOへ書き込んでください。
完成版プログラム:test_eeprom.ino
先ほど、EEPROMの実験で使ったのと同じプログラムですので、既にUNOに書き込んである場合には、そのまま使用できます。SDカードI/Fの接続だけ追加してください。
実験サンプル:【 】内はシリアルモニタからの送信データ
【mount】 …… UNOの電源投入後にSDカードを指した場合
OK: mount SD card
【bench】
OK+ SPI clock=fcpu/4=4.000[MHz]
OK+ SD benchmark 32[KB]: WR.
OK+ check sum: 0x9F(write), 0x9F(read)
OK+ write: 32768[B], 5117.33[ms], 6403.34[B/s]
OK+ read: 32768[B], 1170.10[ms], 28004.44[B/s]
OK: benchmark finished
【umount】
OK: unmount SD card
…… ここで、SDカードを別な種類(2Gバイト→16Gバイト等)に差し替えます
【mount,8】 …… SPIのクロックを8MHzに設定して、マウント
OK: mount SD card 8[MHz]
【bench】
OK+ SPI clock=fcpu/2=8.000[MHz]
OK+ SD benchmark 32[KB]: WR.
OK+ check sum: 0x79(write), 0x79(read)
OK+ write: 32768[B], 5082.62[ms], 6447.07[B/s]
OK+ read: 32768[B], 1137.24[ms], 28813.62[B/s]
OK: benchmark finished
こんな感じで、必要なデータを収集します。以下が測定したアクセス速度データと雑感です。
microSD カード |
size | 2Gバイト | 16Gバイト | 32Gバイト |
FAT | 16 | 32 | 32 | |
Speed C | − | C4 | C10 | |
SPI 1MHz | write | 6,148 | 1,234 | 6,770 |
read | 23,672 | 28,481 | 23,805 | |
SPI 4MHz (デフォルト) |
write | 6,399 | 1,234 | 7,269 |
read | 28,088 | 28,471 | 27,971 | |
SPI 8MHz | write | 6,445 | 1,361 | 7,307 |
read | 28,693 | 29,474 | 28,814 | |
備考 | ほぼ空 | ほぼフル | ほぼ空 | |
※32Kバイトのデータアクセスタイム[バイト/s] |
※後日、書き込み速度が読み出し速度に比べて圧倒的に遅い原因が、測定プログラムにあることが判明しました。本ページ最後の「後日のアップデート#2」をご覧ください。
では、前節で測定した2GバイトのmicroSDカードを使い、サウンドセンサ(コンデンサマイク)のアナログ出力を8ビット/サンプルで記録するアプリケーションを例にとって、バンド幅の評価をしてみます。必要なヘッダ情報さえ付加しておけば、WindowsのWAVファイルになるので、「Arduinoとマイクを使ったボイスレコーダ」になります。
前節の計測結果より、2Gバイト SDカードへの書き込み速度は、およそ6,399B/sでした。プログラム(test_eeprom.ino)をご覧いただくとわかりますが、計測方法は、以下の様に本当にデータを書き込んでいるだけです。
// write test
cnt_wr=0;
sum_wr=0;
usec_wr1=micros ();
for (idx=0; idx<fsiz; idx++)
{
chr=random (256);
sum_wr += chr;
fptr.write (chr);
cnt_wr++;
}
usec_wr2=micros ();
ボイスレコーダとして使用するには、AD変換の実施、一定時間毎のサンプリング処理、等々、オーバヘッドがかかりますので、ひとまず、書き込み速度は1/2程度まで低下すると見積もってみます。これで、書き込み速度は、約3,200B/sです。
8ビット/サンプルのデータを記録するのが目標仕様ですので、3,200B/sで書き込めるデータは、3,200サンプル/sとなります。これがサンプリングレートとなります。
このデータから再現できる音声波形の周波数は、標本化定理より、サンプリングレートの1/2ですから、1,600Hzとなります。
人間の耳の可聴域が、おおよそ20〜20k[Hz]とすると、(1,600Hz=)1.6kHzから20kHzまでの高音域は遮断されてしまいますが、それ以外の可聴域信号は記録可能です。ちなみに、1.6kHzだと、オクターブ6のソ音辺りです。音楽の録音には無理がありますが、会話の内容を記録するくらいの用途には十分だと思います。
2GバイトのSDカードですので、単純計算でも、350時間以上の音声データを記録可能です。
※実際にコーディングしてみて、オーバヘッドを最小限に抑えることができれば、もう少しサンプリングレートを向上できる余地があるかもしれません。逆に、もっとレートを落とさないと、処理が間に合わない場合もありますが……。
どうですか?なんとなく、実現できそうな気がしてきましたか?実現に当たっては、以下の記事が参考になるはずです。
まだ課題はあるので完全ではありませんが、課題は明確なので、それらをクリアすれば、実現できるはずです。
※後日ふと思い立って作って見たボイスレコーダについては「#11-7 そうだ、ボイスレコーダを作ろう」をご覧ください。
今回は、不揮発メモリつながりで、ATmega328搭載のEEPROMと、SDカード(Flashメモリ)をテーマに実験してみました。特に、SDカードの方は、色々と応用のアイディアが浮かんできます。中々興味深いデバイスです。
EEPROMへの書き込み【wr】コマンドに、複数データのシーケンシャルな書き込みをサポートしました。
コマンド仕様については、本ページ内の説明を更新してありますので、ご参照ください。
更新版プログラム:test_eeprom.ino
※オリジナルを差し替えてあるので、どのリンクからダウンロードしても、同じ更新版となります。
プログラム(test_eeprom.ino)には、もう一点、STM32でのコンパイル&実行に対応しする修正を実施しました。ただし、STM32で実行可能な機能は、SDカードのベンチマークに関する機能のみとなります(EEPROMには未対応)。
STM32でSDカードのアクセス速度がどの程度向上するか、実験してみましょう。「#8-6 音声合成、の、マネ事」辺りの記事でも、一度、STM32にSDカードI/Fを接続しましたが、改めて接続表をご覧に入れます。
STM32 | 接続 | SDカードI/F | |
PA4(SS) | → | 1 | CS |
PA7(MOSI) | → | 2 | DI |
GND | ⇔ | 3 | VSS |
3.3V | → | 4 | VDD |
PA5(SCK) | → | 5 | SCLK |
GND | ⇔ | 6 | VSS2 |
PA6(MISO) | ← | 7 | DO |
SDカードが3.3V動作なのに対して、UNOが5Vだったので、手作りのSDカードI/F(下図、「#6-5 microSDカードI/Fを自作する」参照)にL/Sを挟んでインターフェイスする必要があったのですが、STM32は3.3V動作なので、L/S不要で直結できます。
筆者は手作りのSDカードI/Fを使いましたが、SDライブラリが使えるSTM32用のSDカードI/Fモジュールであれば、同じプログラム(test_eeprom.ino)で使用することができます。
以下、実験中の様子です。
プログラム(test_eeprom.ino)をSTM32でコンパイル&書き込んで、シリアルモニタを開いてください。
実験サンプル:【 】内はシリアルモニタからの送信データ
OK+ SD initialize...
OK: SD initialize done.
RDY
【bench】
OK+ SD benchmark 32[KB]: WR.
OK+ check sum: 0xEE(write), 0xEE(read)
OK+ write: 32768[B], 1057.77[ms], 30978.50[B/s]
OK+ read: 32768[B], 319.57[ms], 102536.18[B/s]
OK: benchmark finished
STM32でSDカードのベンチマークを実行してみた結果を以下表でUNOと比べてみましょう。
Arduino UNO | STM32 | ||
microSD カード |
size | 2Gバイト | 2Gバイト |
FAT | 16 | 16 | |
Speed C | − | − | |
SPI (デフォルト) |
write | 6,399 | 30,978 |
read | 28,088 | 102,536 |
※32Kバイトのデータアクセスタイム[バイト/s]
だいぶ速くはなったのですが、STM32 @72MHzに対して、UNO @16MHzと、動作クロック周波数比で×4.5倍の優位があることを考えると、上表の結果は、まあ、周波数比で説明がつく範囲内ですね……。
Thumbとは言え、一応、STM32はARM Cortex-M3 @72MHzを搭載しています。言っても32ビットMCUです。それが、8ビットMCUであるATmega328と比べて、周波数比分の速度しか出せないなんて、ちょっとガッカリです。
それこそが、Thumbの限界なのかもしれないですね。
Arduino IDEのSTM32メニューには、コンパイラの最適化オプションが選べるようになっているのですが、「Fastest (-O3)」を選択しても、速度的にはあまり変わらず、かえって遅くなっているようにも思える始末でした。「Smallest (default)」で使う方が良いみたいです。
大型アップデート第二弾です(1^_^;)。
SDカードI/Fの書き込み速度が、読み出し速度に比べて圧倒的に遅かったのですが、原因がわかりました。筆者のプログラムが良くなかったです。該当箇所は以下の部分。
// write test
cnt_wr=0;
sum_wr=0;
Serial.print (F("W"));
usec_wr1=micros ();
for (idx=0; idx<fsiz; idx++)
{
chr=random (256);
sum_wr += chr;
fptr.write (chr);
cnt_wr++;
}
usec_wr2=micros ();
SDカードにランダムなデータを書き込むところです。micros()関数で書き込み始めusec_wr1と終わりusec_wr2の時間を取得しているので、usec_wr2−usec_wr1が経過時間μsとなります。この間に書き込んだデータ数はfsizバイトなので、fsiz/(usec_wr2−usec_wr1)が、書き込み速度[バイト/s]となります。
問題は、乱数を生成しているrandom()関数です。random()関数は推定で、uint32の掛け算1回と足し算1回、割り算(剰余算)1回が実行(線形合同法)されます。とっても重たい処理だったのです。
うかつでした。Arduino (ATmega328 @16MHz)でしたものね……、Windowsのプログラミングで麻痺してました。
つまり、SDカードへの書き込みwrite()メソッドの実行時間を測定していたつもりが、とっても重たいrandom()関数の実行時間を含めて計測してしまっていました。
正しくSDカードへの書き込み速度を正しく計測するために、乱数の書き込みをやめて、シーケンシャルデータの書き込みに変更しました(↓)。
// write test
cnt_wr=0;
sum_wr=0;
Serial.print (F("W"));
usec_wr1=micros ();
chr=random (256);
for (idx=0; idx<fsiz; idx++)
{
chr=uint8 (chr + 1);
sum_wr += chr;
fptr.write (chr);
cnt_wr++;
}
usec_wr2=micros ();
新しいプログラムで書き込みテストを実行したところ、以下の様に15,000B/s程度の書き込み速度が観測できました。……random()関数、どんだけ遅かったんだか?
実験サンプル:【 】内はシリアルモニタからの送信データ
【bench】
OK+ SPI clock=fcpu/4=4.000[MHz]
OK+ SD benchmark 32[KB]: WR.
OK+ check sum: 0x0(write), 0x0(read)
OK+ write: 32768[B], 2168.08[ms], 15113.86[B/s]
OK+ read: 32768[B], 1338.05[ms], 24489.33[B/s]
OK: benchmark finished
現時点、手元で使用可能だった2Gバイトと16Gバイト SDカードのアクセス速度をまとめておきます。
microSD カード |
size | 2Gバイト | 16Gバイト |
FAT | 16 | 32 | |
Speed C | − | C4 | |
SPI 4MHz (デフォルト) |
write | 15,113 | 20,868 |
read | 24,489 | 24,809 |
※32Kバイトのデータアクセスタイム[バイト/s]
読み出し程ではありませんが、まあまあ見栄えのする書き込み速度になりました。
こんな、基本的な実験結果の計測で間違ってしまっては、その値を使って色々なアプリケーションを設計する時の大きな障害となってしまいます。気を付けねば……。