SSブログ

「オープンソースハードウェアセミナーVol1」レポート Arduino PROGMEMの効き具合を確かめる [ATmarquino Arduino]

※chip 1 stopからいらした方!不明点等はコメント欄にお気軽にどうぞ。判る範囲で対応します。
ether_shield_016.png普通、マイコンの開発では、現在のメモリの使用状況を調べ、もし使用したいマイコンの搭載メモリ量を超えてしまった場合、何らかの対策を行わなければならないのです。

何故かと言えば、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>で括られますが、これなんかは普通固定メッセージで済みますよね。


ether_shield_017.pngPROGMEMを使って修飾した定数は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 );
}


Arduinoモニタープログラム参加中
電子部品・半導体の通販サイト - チップワンストップ




Arduinoをはじめよう

Arduinoをはじめよう

  • 作者: Massimo Banzi
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2009/03/27
  • メディア: 単行本(ソフトカバー)



Making Things Talk -Arduinoで作る「会話」するモノたち

Making Things Talk -Arduinoで作る「会話」するモノたち

  • 作者: Tom Igoe
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2008/11/17
  • メディア: 大型本



nice!(0)  コメント(5)  トラックバック(0) 

nice! 0

コメント 5

todotani

どうも、おじゃまします。

初期化済みの静的データーを定義すると、起動時に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) 

hamayan

どうもです。
write_Pを本文の一番下に書いておきました。

結局ROMから読み出した後はRAMに保存して、それをsendに渡していますので、ペナルティはもう少し大きいかな。
これを避けるには、もう直接ROMからw5100のバッファへのコピーを行う処理を自分で書かなくてはいけないかもしれません。面倒だったのでこの辺で妥協しています。

ご指摘の様にPROGMEMの乱用はメリットよりデメリットが大きく感じられる事も有ると思います。その辺は使う人次第かな。1byteでもRAMを削りたいとか。

by hamayan (2009-06-13 01:46) 

todotani

write_P()のコード公開ありがとうございました。
おっしゃる通り、Ethernet (w5100)などの既存ライブラリに、ROMに格納したデーターを渡す場合は、一旦SRAMに引き上げてからが現実的ですね。
by todotani (2009-06-13 10:32) 

noritan

RAMから読み出すのか、ROMから読み出すのか、考えたくないという時には、

1) 「メモリ読み出しオブジェクト」というメモリ読み出し操作を供えた抽象クラスを作ります。
2) その具象クラスとして「ROM読み出しクラス」と「RAM読み出しクラス」を作ります。
3) writeには、 「メモリ読み出しオブジェクト」を渡す約束にして、それがROMであるかRAMであるかは、一切気にしないコーディングをします。

ついでに、「シリアルEEPROM読み出しオブジェクト」「SDカード読み出しオブジェクト」も作れば、無敵です。

by noritan (2009-06-13 11:17) 

hamayan

ついでにEthernet読み出し、書き込みオブジェクトとか。
全部xxx.printでやるような感じ?。

C言語しかやった事無い私には「そんな抽象化必要なの?、重くなるだけジャン」と思っていましたとも。
ですが、極僅かですがC++を目の当たりにして以外と良いかもと思えるところも出てきました。

最大の問題は思いは有っても技術・知識が付いていない点ですね。

今度HCS08やColdFireでC++講座やってください。

by hamayan (2009-06-13 11:35) 

コメントを書く

お名前:[必須]
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

※ブログオーナーが承認したコメントのみ表示されます。

トラックバック 0

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。