「オープンソースハードウェアセミナーVol1」レポート Arduino PROGMEMの効き具合を確かめる [ATmarquino Arduino]
※chip 1 stopからいらした方!不明点等はコメント欄にお気軽にどうぞ。判る範囲で対応します。
普通、マイコンの開発では、現在のメモリの使用状況を調べ、もし使用したいマイコンの搭載メモリ量を超えてしまった場合、何らかの対策を行わなければならないのです。
何故かと言えば、PC上のプログラミングならば、最近のPCを使用している限りメインメモリなんてGbyte単位で増設可能で、滅多にこれを使い切る事は無いでしょう。いざとなったら遅いですが仮想メモリなんて手もある訳だし。
ですがマイコンのメモリ搭載量は、特にArduinoで使用しているAVRの様なOne chipマイコンではとても小さなメモリしか搭載されておらず、また増設も不可能か、もしくは容易な事ではありません。
なのでマイコンの開発環境にはコンパイル後(実際にはリンク後)のメモリの使用状況を報告してくれる機能が基本的に備わっています。勿論Arduinoにも有る筈なのですけれど。
※GCCはあまり使わないので良く知らなかったのですね。
ArduinoのスケッチのコンパイルにはGCCツールチェインが動いています。何故かこれをJavaが動いていると紹介しているところも有ったりしますが、JavaはArduino IDEその物を動かしている方でしょう。スケッチの言語も「C言語っぽい簡単ななんとか言語」とか言う怪しい言語ではなく、C++をベースに若干の構文整形ツールがあると言ったところでしょう。 ※なんでいつも「簡単な」って言葉で誤魔化すのがよく判りませんが、言語面では結局のところ使いこなす為にはCやC++を勉強するのが一番の近道だと思います。
「PS3とLinux、電子工作も」さんの最近の話題「Arduinoメールチェッカー(その3)」
http://todotani.cocolog-nifty.com/blog/2009/06/arduino-36b3.html
で、スタックの話題に触れており、その中でメモリの使用状況を調べるツールに付いて書かれていましたので、今回あらためてPROGMEMの効用を確認してみました。
※PROGMEMの効用自体は知っていましたが、具体的な数字を出す事が今まで出来なかった。
さてPROGMEMの効用を調べる為には適当なお題が必要ですので、例のWEBサーバーを引き合いに出してみます。以下のスケッチです。
PROGMEMを使用すると、使い方によってはAVRに搭載されているメモリの内、RAMの使用量を減らす事が可能です。詳細は下の方で述べます。
13行目にコンパイル制御を行う為の定義(#define __PROGMEM_USE__)を行っています。この一文が有るとPROGMEMを使ってコンパイルするように、またこの一文を削除またはコメントアウトするとPROGMEMを使わずにコンパイルするようになっています。
このスケッチはWEBサーバーですので、クライアント(ブラウザ)からの要求を受け付けて応答を返します。
htmlファイルを出力しますが、htmlのフォーマットには固定メッセージで済むところが沢山あります。例えばhtml文の開始と終了は<html></html>で括られますが、これなんかは普通固定メッセージで済みますよね。
PROGMEMを使って修飾した定数はROMに配置され、プログラムの実行中もROMから参照されます。
PROGMEMを使わずにconstを付けただけだと、ROMにデータとして保存されますがC言語のmainが実行される前にその内容をRAMにコピーされ、プログラムの実行中もRAMから参照されます。
※ただしこれはAVR特有の事です。他のマイコンの場合はconstを使って修飾すればROMに配置され、参照もROMから行われる物がほとんどです。
この為、PROGMEMを使わずに定数を使うと、その分だけどんどんRAMを消費してしまいます。参照するだけ(内容を読み出すだけで変化させて使ったりしない)なのにです。
勿論1byte、2byte分の定数ならば面倒なのでそのまま使ってもあまり害は無い場合がほとんどですが、それが今回の様に固定メッセージを沢山使ったりした場合は、積極的にPROGMEMを使用しないと、あっと言う間にRAMを使い切ってしまいます。なんせATmega328Pですら搭載RAM容量は2Kbyte(2048byte)しか無いのですから。
実際にPROGMEMを使った場合と、使わなかった場合でどれ位違いが出るのか、それを調べたのが冒頭の図です。ツールの出力を以下に記載しておきます。
PROGMEMを使った場合です。
PROGMEMを使わなかった場合です。
共に注目すべきは0番のdataセクションです。dataセクションはRAM上に展開される初期化済み(変数)領域です。
PROGMEMを使用した場合は00000050、つまり80byteであるのに、使用しなかった場合は0000059c(1436byte)となっています。使用しなかった場合は2KbyteのRAM領域の内、実に7割もの領域が使われています。おそらくこの値はこのスケッチが動作可能なギリギリの値だと思われます。これに更に何かメッセージを追加したら、謎のプログラム停止とかに成りかねません。
※textセクションはプログラムコード、bssセクションは未初期化変数領域です。
結果、PROGMEMを使用するようにした場合は、1356byteも使用済みRAM領域を減らせました。
※今回の様にプログラム実行中に値の変化しないデータまでRAMに配置されてしまい、無駄にRAMを消費する問題は、別に今回の様な文字データの配列を使ったからに限りません。
試しに以下の様なスケッチを書いてみます。
この中で定数は0~9までの数字とアルファベット、それに見落としてしまい勝ちな38400です。これを先のメモリの状況を調べるツールに掛けて見ます。
データセクションで00000030(48byte)消費している事が判ります。0~9の数字で10+1byte、アルファベットで26+1byte、38400はint型なら2byteの合計40byteが定数です。あれ8byteは何処に???。
※毎回、毎回同じ事をタイプするのは面倒なので(忘れちゃうし)、batファイルを作ってやっています。
avr-objdump -h %1 | more
avr-nm -n -C %1 | more
本当はビルド時に作成するように出来れば良いのだけれど、、、検討中です。情報求む!。
※追記 write_Pの実体
普通、マイコンの開発では、現在のメモリの使用状況を調べ、もし使用したいマイコンの搭載メモリ量を超えてしまった場合、何らかの対策を行わなければならないのです。
何故かと言えば、PC上のプログラミングならば、最近のPCを使用している限りメインメモリなんてGbyte単位で増設可能で、滅多にこれを使い切る事は無いでしょう。いざとなったら遅いですが仮想メモリなんて手もある訳だし。
ですがマイコンのメモリ搭載量は、特にArduinoで使用しているAVRの様なOne chipマイコンではとても小さなメモリしか搭載されておらず、また増設も不可能か、もしくは容易な事ではありません。
なのでマイコンの開発環境にはコンパイル後(実際にはリンク後)のメモリの使用状況を報告してくれる機能が基本的に備わっています。勿論Arduinoにも有る筈なのですけれど。
※GCCはあまり使わないので良く知らなかったのですね。
ArduinoのスケッチのコンパイルにはGCCツールチェインが動いています。何故かこれをJavaが動いていると紹介しているところも有ったりしますが、JavaはArduino IDEその物を動かしている方でしょう。スケッチの言語も「C言語っぽい簡単ななんとか言語」とか言う怪しい言語ではなく、C++をベースに若干の構文整形ツールがあると言ったところでしょう。 ※なんでいつも「簡単な」って言葉で誤魔化すのがよく判りませんが、言語面では結局のところ使いこなす為にはCやC++を勉強するのが一番の近道だと思います。
「PS3とLinux、電子工作も」さんの最近の話題「Arduinoメールチェッカー(その3)」
http://todotani.cocolog-nifty.com/blog/2009/06/arduino-36b3.html
で、スタックの話題に触れており、その中でメモリの使用状況を調べるツールに付いて書かれていましたので、今回あらためてPROGMEMの効用を確認してみました。
※PROGMEMの効用自体は知っていましたが、具体的な数字を出す事が今まで出来なかった。
さてPROGMEMの効用を調べる為には適当なお題が必要ですので、例のWEBサーバーを引き合いに出してみます。以下のスケッチです。
PROGMEMを使用すると、使い方によってはAVRに搭載されているメモリの内、RAMの使用量を減らす事が可能です。詳細は下の方で述べます。
13行目にコンパイル制御を行う為の定義(#define __PROGMEM_USE__)を行っています。この一文が有るとPROGMEMを使ってコンパイルするように、またこの一文を削除またはコメントアウトするとPROGMEMを使わずにコンパイルするようになっています。
/* * PROGMEMの効き具合を確認してみる */ #include <Ethernet.h> #include <ctype.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <avr/pgmspace.h> #define LINE_STRING_SIZE 128 #define __PROGMEM_USE__ char *line; char meth[10],url[30],ver[20]; long access_count; const byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; const byte sip[] = { 192, 168, 1, 177 }; const byte gateway[] = { 192, 168, 1, 1 }; /*contents*/ #ifdef __PROGMEM_USE__ const char PROGMEM http_head[] = #else const char http_head[] = #endif "HTTP/1.0 200 OK\r\n" \ "Server: Arduino with Ether Shield/ ver.0.1\r\n" \ "Content-Type: text/html; charset=UTF-8\r\n\r\n" \ "<html lang=\"ja\"><head>\r\n" \ "<meta HTTP-EQUIV=\"Content-type\" CONTENT=\"text/html; charset=UTF-8\">\r\n" \ "<title>Arduino WEB Server 0.1</title></head>\r\n" \ "<body bgcolor=\"#ccffcc\">\r\n" \ "<h3>designed by hamayan</h3>\r\n"; #ifdef __PROGMEM_USE__ const char PROGMEM http_foot[] = #else const char http_foot[] = #endif "</body></html>\r\n\r\n"; #ifdef __PROGMEM_USE__ const char PROGMEM thanks[] = #else const char thanks[] = #endif "アクセス有難うございます。このページはArduino+Ether Shieldで表示しています。<br>\r\n" \ "現在はリクエストのパーサーを作成しています。<br>\r\n"; #ifdef __PROGMEM_USE__ const char PROGMEM links[] = #else const char links[] = #endif "hamayan blog <a href=\"http://hamayan.blog.so-net.ne.jp/\">http://hamayan.blog.so-net.ne.jp/</a><br>\r\n" \ "chip 1 stop <a href=\"http://www.chip1stop.com/\">http://www.chip1stop.com/</a><br>\r\n" \ "オープンソースハードウェアセミナーのページ <a href=\"http://www.chip1stop.com/knowledge/Arduino/\">http://www.chip1stop.com/knowledge/Arduino/</a><br>\r\n" \ "Make:Japan <a href=\"http://jp.makezine.com/blog/\">http://jp.makezine.com/blog/</a><br><br>\r\n"; #ifdef __PROGMEM_USE__ const char PROGMEM banner_01[] = #else const char banner_01[] = #endif "<p><a href=\"http://www.chip1stop.com/knowledge/Arduino/\">" \ "<img src=\"http://www.chip1stop.com/img/link_Arduino.gif\" width=\"468\" height=\"60\" alt=\"Arduinoモニタープログラム参加中\" /></a><br>" \ "<a href=\"http://www.chip1stop.com/\" title=\"電子部品・半導体の通販サイト - チップワンストップ\">電子部品・半導体の通販サイト - チップワンストップ</a></p><br><br>\r\n"; #ifdef __PROGMEM_USE__ const char PROGMEM img_src[] = #else const char img_src[] = #endif "<p><img src=\"./image2.jpg\" width=\"200\" height=\"140\" align=\"center\"></p><br><br>\r\n"; #ifdef __PROGMEM_USE__ const char PROGMEM jpeg_head[] = #else const char jpeg_head[] = #endif "HTTP/1.0 200 OK\r\n" \ "Server: Arduino with Ether Shield/ ver.0.1\r\n" \ "Content-Type: image/JPEG\r\n\r\n"; Server server( 8888 ); void setup() { Ethernet.begin( (uint8_t *)mac, (uint8_t *)sip, (uint8_t *)gateway ); server.begin(); line = (char *)malloc( LINE_STRING_SIZE ); } void loop() { Client client = server.available(); if( client ) { while( client.connected() ) { char *dst = HTTPGets( client, line, LINE_STRING_SIZE ); char *argv[ 10 ]; int div_num = split( dst, argv, sizeof(argv) / sizeof(argv[0]) ); /*文字列分割*/ strncpy( meth, argv[ 0 ], sizeof(meth) ); strncpy( url, argv[ 1 ], sizeof(url) ); strncpy( ver, argv[ 2 ], sizeof(ver) ); break; } while( client.connected() ) { char *dst = HTTPGets( client, line, LINE_STRING_SIZE ); if( dst != NULL && *dst == '\0' ) /*改行のみの行を検出*/ { index_html( client ); break; } } delay( 1 ); client.stop(); } } void index_html( Client client ) { //httpヘッダーの返信 #ifdef __PROGMEM_USE__ client.write_P( http_head, sizeof( http_head ) - 1 ); #else client.write( (const uint8_t *)http_head, sizeof( http_head ) - 1 ); #endif //サンキューメッセージ #ifdef __PROGMEM_USE__ client.write_P( thanks, sizeof( thanks ) - 1 ); #else client.write( (const uint8_t *)thanks, sizeof( thanks ) - 1 ); #endif //リンク #ifdef __PROGMEM_USE__ client.write_P( (const uint8_t *)links, sizeof( links ) - 1 ); #else client.write( (const uint8_t *)links, sizeof( links ) - 1 ); #endif //Arduinoバージョン sprintf( line, "Arduino ver=%d <br>\r\n", ARDUINO ); client.write( line ); //アクセスカウント sprintf( line, "COUNT=%d <br>\r\n", ++access_count ); client.write( line ); //バナー #ifdef __PROGMEM_USE__ client.write_P( (const uint8_t *)banner_01, sizeof( banner_01 ) - 1 ); #else client.write( (const uint8_t *)banner_01, sizeof( banner_01 ) - 1 ); #endif //Footer #ifdef __PROGMEM_USE__ client.write_P( (const uint8_t *)http_foot, sizeof( http_foot ) - 1 ); #else client.write( (const uint8_t *)http_foot, sizeof( http_foot ) - 1 ); #endif }
このスケッチはWEBサーバーですので、クライアント(ブラウザ)からの要求を受け付けて応答を返します。
htmlファイルを出力しますが、htmlのフォーマットには固定メッセージで済むところが沢山あります。例えばhtml文の開始と終了は<html></html>で括られますが、これなんかは普通固定メッセージで済みますよね。
PROGMEMを使って修飾した定数はROMに配置され、プログラムの実行中もROMから参照されます。
PROGMEMを使わずにconstを付けただけだと、ROMにデータとして保存されますがC言語のmainが実行される前にその内容をRAMにコピーされ、プログラムの実行中もRAMから参照されます。
※ただしこれはAVR特有の事です。他のマイコンの場合はconstを使って修飾すればROMに配置され、参照もROMから行われる物がほとんどです。
この為、PROGMEMを使わずに定数を使うと、その分だけどんどんRAMを消費してしまいます。参照するだけ(内容を読み出すだけで変化させて使ったりしない)なのにです。
勿論1byte、2byte分の定数ならば面倒なのでそのまま使ってもあまり害は無い場合がほとんどですが、それが今回の様に固定メッセージを沢山使ったりした場合は、積極的にPROGMEMを使用しないと、あっと言う間にRAMを使い切ってしまいます。なんせATmega328Pですら搭載RAM容量は2Kbyte(2048byte)しか無いのですから。
実際にPROGMEMを使った場合と、使わなかった場合でどれ位違いが出るのか、それを調べたのが冒頭の図です。ツールの出力を以下に記載しておきます。
PROGMEMを使った場合です。
Sections: Idx Name Size VMA LMA File off Algn 0 .data 00000050 00800100 000023c2 00002456 2**0 CONTENTS, ALLOC, LOAD, DATA 1 .text 000023c2 00000000 00000000 00000094 2**1 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .bss 00000091 00800150 00800150 000024a6 2**0 ALLOC
PROGMEMを使わなかった場合です。
Sections: Idx Name Size VMA LMA File off Algn 0 .data 0000059c 00800100 00001e76 00001f0a 2**0 CONTENTS, ALLOC, LOAD, DATA 1 .text 00001e76 00000000 00000000 00000094 2**1 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .bss 00000091 0080069c 0080069c 000024a6 2**0 ALLOC
共に注目すべきは0番のdataセクションです。dataセクションはRAM上に展開される初期化済み(変数)領域です。
PROGMEMを使用した場合は00000050、つまり80byteであるのに、使用しなかった場合は0000059c(1436byte)となっています。使用しなかった場合は2KbyteのRAM領域の内、実に7割もの領域が使われています。おそらくこの値はこのスケッチが動作可能なギリギリの値だと思われます。これに更に何かメッセージを追加したら、謎のプログラム停止とかに成りかねません。
※textセクションはプログラムコード、bssセクションは未初期化変数領域です。
結果、PROGMEMを使用するようにした場合は、1356byteも使用済みRAM領域を減らせました。
※今回の様にプログラム実行中に値の変化しないデータまでRAMに配置されてしまい、無駄にRAMを消費する問題は、別に今回の様な文字データの配列を使ったからに限りません。
試しに以下の様なスケッチを書いてみます。
void setup() { Serial.begin( 38400 ); Serial.println("0123456789"); Serial.println("abcdefghijklmnopqrstuvwxyz"); } void loop() { }
この中で定数は0~9までの数字とアルファベット、それに見落としてしまい勝ちな38400です。これを先のメモリの状況を調べるツールに掛けて見ます。
Sections: Idx Name Size VMA LMA File off Algn 0 .data 00000030 00800100 00000576 0000060a 2**0 CONTENTS, ALLOC, LOAD, DATA 1 .text 00000576 00000000 00000000 00000094 2**1 CONTENTS, ALLOC, LOAD, READONLY, CODE 2 .bss 0000009f 00800130 00800130 0000063a 2**0 ALLOC
データセクションで00000030(48byte)消費している事が判ります。0~9の数字で10+1byte、アルファベットで26+1byte、38400はint型なら2byteの合計40byteが定数です。あれ8byteは何処に???。
※毎回、毎回同じ事をタイプするのは面倒なので(忘れちゃうし)、batファイルを作ってやっています。
avr-objdump -h %1 | more
avr-nm -n -C %1 | more
本当はビルド時に作成するように出来れば良いのだけれど、、、検討中です。情報求む!。
※追記 write_Pの実体
void Client::write_P( PGM_VOID_P buf, int size ) { uint8_t *temp = (uint8_t *)malloc( TEMP_HEAP_SIZE ); int sz; PGM_P ptr = (PGM_P)buf; for( ; size > 0; ) { sz = ( size < TEMP_HEAP_SIZE ) ? size : TEMP_HEAP_SIZE; memcpy_P( temp, ptr, sz ); send( _sock, (const uint8_t *)temp, sz ); size -= sz; ptr += sz; } free( temp ); }
Making Things Talk -Arduinoで作る「会話」するモノたち
- 作者: Tom Igoe
- 出版社/メーカー: オライリージャパン
- 発売日: 2008/11/17
- メディア: 大型本
2009-06-12 00:54
nice!(0)
コメント(5)
トラックバック(0)
どうも、おじゃまします。
初期化済みの静的データーを定義すると、起動時にFlashからSRAMに転送するのですか。知りませんでした。同じデーターでメモリを2回占有するのはもったいない。
AVRはハーバード・アーキテクチャーのため、FlashとSRAMでデーターバスが分かれており、Flashのデーターはプログラム・カウンタ経由で読み出すのが基本だと思ったので、pgm_read_byte()でどうやってflashからデーターを取り込んでいるかavr/pgmspace.hの定義を覗いてみました。
LPM rd, Zを使用してZレジスタの間接指定でアクセスしています。SRAMからのデーターLDが2クロック、LPMは3クロックですので1クロックオーバーヘッドがありますが、通常のアプリなら気にならないレベルですね。ですので、コーディングが面倒ですが、PROGMEM使用のメリットありですね。
ちなみに、PROGMEMを使った際に呼んでいる、 client.write_P()の実体はどこかに定義されているのでしょうか? client.write_P()の引数に http_headを指定した場合、pgm_read_xxx()をどこかで呼ばないとFlashのデーターにアクセスできないのではと思ったのです。
by todotani (2009-06-13 01:06)
どうもです。
write_Pを本文の一番下に書いておきました。
結局ROMから読み出した後はRAMに保存して、それをsendに渡していますので、ペナルティはもう少し大きいかな。
これを避けるには、もう直接ROMからw5100のバッファへのコピーを行う処理を自分で書かなくてはいけないかもしれません。面倒だったのでこの辺で妥協しています。
ご指摘の様にPROGMEMの乱用はメリットよりデメリットが大きく感じられる事も有ると思います。その辺は使う人次第かな。1byteでもRAMを削りたいとか。
by hamayan (2009-06-13 01:46)
write_P()のコード公開ありがとうございました。
おっしゃる通り、Ethernet (w5100)などの既存ライブラリに、ROMに格納したデーターを渡す場合は、一旦SRAMに引き上げてからが現実的ですね。
by todotani (2009-06-13 10:32)
RAMから読み出すのか、ROMから読み出すのか、考えたくないという時には、
1) 「メモリ読み出しオブジェクト」というメモリ読み出し操作を供えた抽象クラスを作ります。
2) その具象クラスとして「ROM読み出しクラス」と「RAM読み出しクラス」を作ります。
3) writeには、 「メモリ読み出しオブジェクト」を渡す約束にして、それがROMであるかRAMであるかは、一切気にしないコーディングをします。
ついでに、「シリアルEEPROM読み出しオブジェクト」「SDカード読み出しオブジェクト」も作れば、無敵です。
by noritan (2009-06-13 11:17)
ついでにEthernet読み出し、書き込みオブジェクトとか。
全部xxx.printでやるような感じ?。
C言語しかやった事無い私には「そんな抽象化必要なの?、重くなるだけジャン」と思っていましたとも。
ですが、極僅かですがC++を目の当たりにして以外と良いかもと思えるところも出てきました。
最大の問題は思いは有っても技術・知識が付いていない点ですね。
今度HCS08やColdFireでC++講座やってください。
by hamayan (2009-06-13 11:35)