PROGMEM:読み取りのためにフラッシュからRAMにデータをコピーする必要がありますか?


8

メモリ管理を理解するのが難しいです。

Arduinoのドキュメントによれば、文字列などの定数や、実行時に変更したくないものはプログラムメモリに保持することが可能です。私はそれをコードセグメントのどこかに埋め込んでいると考えています。これは、フォンノイマンアーキテクチャ内でかなり可能でなければなりません。そこからLCDのUIメニューを可能にしたい。

しかし、プログラムメモリからデータを読み取って出力するだけの命令には戸惑っています。

strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i]))); // Necessary casts and dereferencing, just copy. 
    Serial.println( buffer );

アクセスする前に、どうしてコンテンツをRAMにコピーする必要があるのですか?そして、これが本当なら、すべてのコードはどうなりますか?実行前にRAMに読み込まれますか?コード(32kiB)は2kiB RAMだけでどのように処理されますか?フロッピーディスクを運ぶ小さなゴブリンはどこにいますか?

そしてさらに興味深い:この式のようにリテラル定数に何が起こるか:

a = 5*(10+7)

5、10、7をレジスタにロードする前に実際にRAMにコピーするのですか?信じられません。


グローバル変数はメモリにロードされ、メモリから解放されることはありません。上記のコードは、必要なときにのみデータをメモリにコピーし、完了したらそれを解放します。また、上記のコードはstring_table配列から1バイトのみを読み取ることに注意してください。その配列は20KBで、メモリに収まらない場合があります(一時的にでも)。ただし、上記の方法を使用して1つのインデックスのみをロードできます。
Gerben 2014年

@Gerben:これはグローバル変数の本当の欠点です、私はまだこれを考慮していませんでした。今、頭痛がします。そして、コードスニペットはドキュメントからの単なる例でした。私はプログラムを控えました。コンセプトについて明確にする前に自分自身。しかし、私は今いくつかの洞察力を持っています。ありがとう!
Ariser-モニカを2014年

初めて読んだときに、ドキュメントがやや混乱していることに気づきました。実際の例もいくつか見てみましょう(例:ライブラリ)。
Gerben 2014年

回答:


10

AVRは修正されたハーバードアーキテクチャファミリであるため、コードはフラッシュにのみ格納されますが、データは操作時に主にRAMに存在します。

それを念頭に置いて、あなたの質問に取り組みましょう。

アクセスする前に、どうしてコンテンツをRAMにコピーする必要があるのですか?

それ自体は必要ありませんが、デフォルトでは、フラッシュなどで特に調べるようにコードを変更しない限り、コードはデータがRAMにあると想定しますstrcpy_P()

そして、これが本当なら、すべてのコードはどうなりますか?実行前にRAMに読み込まれますか?

いいえ。ハーバード建築。詳細については、ウィキペディアのページをご覧ください。

コード(32kiB)は2kiB RAMだけでどのように処理されますか?

コンパイラーによって生成されたプリアンブルは、実際のプログラムを実行する前に、変更可能/変更すべきデータをSRAMにコピーします。

フロッピーディスクを運ぶ小さなゴブリンはどこにいますか?

ダンノ。しかし、あなたがそれらを偶然目にした場合、私が助けることができることは何もありません。

... 5、10、7をレジスタにロードする前に、実際にはRAMにコピーされていますか?

いや。コンパイラーはコンパイル時に式を評価します。他に何が起こるかは、その周りのコード行によって異なります。


わかりました、AVRがハーバードだったことを知りませんでした。しかし、私はその概念に精通しています。ゴブリンはさておき、私はこれらのコピー機能をいつ使うべきかを知っていると思います。CPUサイクルを節約するためにめったに使用されないデータにPROGMEMの使用を制限する必要があります。
Ariser-モニカを2014年

または、フラッシュから直接使用するようにコードを変更します。
Ignacio Vazquez-Abrams

しかし、このコードはどのように見えるでしょうか?SPIを介してLCDディスプレイに配置する文字列を表すuint8_tの配列がいくつかあるとします。const uint8_t test1[5]= { 0x54, 0x65, 0x73, 0x74, 0x31 }; const uint8_t bla[9]= { 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x61, 0x62, 0x6c, 0x62 }; const uint8_t Menu[4]= { 0x3d, 0x65, 0x6e, 0x75};このデータをフラッシュし、後で呼び出しごとに1つのuint8_tが必要なSPI.transfer()関数に取り込む方法を教えてください。
Ariser-モニカを2014年


8

これはPrint::print、Arduinoライブラリのプログラムメモリから出力する方法です。

size_t Print::print(const __FlashStringHelper *ifsh)
{
  const char PROGMEM *p = (const char PROGMEM *)ifsh;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

__FlashStringHelper*const char*コンパイラのように見られるため、プログラムメモリへのポインタを1から通常のメモリに区別するために、printなどのオーバーロードされた関数を許可する空のクラスです(/programming/16597437/arduino-f-を参照) - -それは、実際に何をするのか

したがってprint、LCDディスプレイの関数をオーバーロードして、__FlashStringHelper*引数を取り、それを呼び出しLCD::printlcd.print(F("this is a string in progmem"));' to call it.F() `を使用して、文字列がプログラムメモリに存在することを確認できます。

文字列を事前定義するために(組み込みのArduino印刷と互換性を持たせるため)、次のように使用しました。

const char firmware_version_s[] PROGMEM = {"1.0.2"};
__FlashStringHelper* firmware_version = (__FlashStringHelper*) firmware_version_s;
...
Serial.println(firmware_version);

代替案は次のようなものになると思います

size_t LCD::print_from_flash(const char *pgms)
{
  const char PROGMEM *p = (const char PROGMEM *) pgms;
  size_t n = 0;
  while (1) {
    unsigned char c = pgm_read_byte(p++);
    if (c == 0) break;
    n += write(c);
  }
  return n;
}

__FlashStringHelperキャストを避けるでしょう。


2

Arduinoのドキュメントによれば、文字列などの定数や、実行時に変更したくないものはプログラムメモリに保持することが可能です。

すべての定数は、最初はプログラムメモリにあります。電源がオフのとき、彼らは他にどこにいますか?

私はそれをコードセグメントのどこかに埋め込んでいると考えています。これは、フォンノイマンアーキテクチャ内でかなり可能でなければなりません。

これは実際にはハーバードアーキテクチャです。

アクセスする前に、どうしてコンテンツをRAMにコピーする必要があるのですか?

あなたはしません。実際、ハードウェア命令(LPM-プログラムメモリのロード)があり、プログラムメモリからレジスタにデータを直接移動します。

Arduino UnoのVGAモニターへの出力でこのテクニックの例を持っています。そのコードでは、プログラムメモリにビットマップフォントが格納されています。その場で読み込まれ、次のように出力にコピーされます。

  // blit pixel data to screen    
  while (i--)
    UDR0 = pgm_read_byte (linePtr + (* messagePtr++));

これらの行の分解は、(部分的に)示しています。

  f1a:  e4 91           lpm r30, Z+
  f1c:  e0 93 c6 00     sts 0x00C6, r30

プログラムメモリの1バイトがR30にコピーされ、すぐにUSARTレジスタUDR0に格納されていることがわかります。RAMは必要ありません。


ただし、複雑さがあります。通常の文字列の場合、コンパイラはPROGMEMではなくRAMでデータを検索することを期待しています。それらは異なるアドレス空間であるため、RAMの0x200はPROGMEMの0x200とは異なります。したがって、コンパイラはプログラムの起動時に定数(文字列など)をRAMにコピーするという問題を抱えているため、後でその違いを知る必要はありません。

コード(32kiB)は2kiB RAMだけでどのように処理されますか?

良い質問。それらをすべてコピーする余地がないため、2 KBを超える定数文字列があっても問題はありません。

そのため、メニューやその他の冗長なものを書いている人は、文字列にPROGMEM属性を追加するために追加の手順を実行します。これにより、RAMへのコピーが無効になります。

しかし、プログラムメモリからデータを読み取って出力するだけの命令には戸惑っています。

PROGMEM属性を追加する場合は、これらの文字列が別のアドレス空間にあることをコンパイラーに知らせる手順を実行する必要があります。完全な(一時的な)コピーを作成するのは1つの方法です。または、一度に1バイトずつ、PROGMEMから直接印刷します。その例は次のとおりです。

// Print a string from Program Memory directly to save RAM 
void printProgStr (const char * str)
{
  char c;
  if (!str) 
    return;
  while ((c = pgm_read_byte(str++)))
    Serial.print (c);
} // end of printProgStr

この関数にPROGMEMの文字列へのポインターを渡すと、RAMからではなくPROGMEMからデータをプルするために「特別な読み取り」(pgm_read_byte)が行われ、印刷されます。これには、バイトごとに1クロックサイクルかかることに注意してください。

そしてさらに興味深い:この式のようなリテラル定数は、a = 5*(10+7)レジスタにロードする前に実際にRAMにコピーされる5、10、7はどうなりますか?信じられません。

いいえ、そうである必要はありません。これは、「リテラルをレジスターにロードする」命令にコンパイルされます。その命令はすでにPROGMEMにあるため、リテラルが処理されます。それをRAMにコピーしてから読み戻す必要はありません。


定数データをプログラムメモリに格納する(PROGMEM)のページに、これらの詳細な説明があります。これには、文字列と文字列の配列を簡単に設定するためのサンプルコードがあります。

また、PROGMEMから単に印刷する簡単な方法であるF()マクロについても説明します。

Serial.println (F("Hello, world"));

プリプロセッサの複雑さが少しあるため、PROGMEMから文字列のバイトを一度に1バイトずつプルするヘルパー関数にコンパイルできます。RAMを途中で使用する必要はありません。

PrintクラスからLCD印刷を派生させることで、Serial以外(LCDなど)にその手法を使用するのは簡単です。

例として、私が書いたLCDライブラリの1つで、まさにそれを行いました。

class I2C_graphical_LCD_display : public Print
{
...
    size_t write(uint8_t c);
};

ここでの重要なポイントは、Printから派生し、「書き込み」機能をオーバーライドすることです。これで、オーバーライドされた関数は、文字を出力するために必要なことをすべて実行します。Printから派生しているため、F()マクロを使用できます。例えば。

lcd.println (F("Hello, world"));
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.