PROGMEM: Muss ich Daten zum Lesen von Flash in den RAM kopieren?

8

Ich habe einige Schwierigkeiten, die Speicherverwaltung zu verstehen.

In der Arduino-Dokumentation heißt es, dass es möglich ist, Konstanten wie Strings oder was auch immer ich zur Laufzeit nicht ändern möchte, im Programmspeicher zu behalten. Ich halte es für irgendwo im Codesegment eingebettet, was in einer von-Neumann-Architektur durchaus möglich sein muss. Ich möchte das nutzen, um mein UI-Menü auf einem LCD zu ermöglichen.

Aber ich bin verwirrt über diese Anweisungen, nur Daten aus dem Programmspeicher zu lesen und zu drucken:

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

Warum um alles in der Welt muss ich den verdammten Inhalt in den Arbeitsspeicher kopieren, bevor ich darauf zugreifen kann? Und wenn dies wahr ist, was passiert dann mit dem gesamten Code? Wird es vor der Ausführung auch in den RAM geladen? Wie wird der Code (32kiB) dann mit nur 2kiB RAM behandelt? Wo tragen diese kleinen Kobolde Disketten?

Und noch interessanter: Was passiert mit wörtlichen Konstanten wie in diesem Ausdruck:

a = 5*(10+7)

Werden 5, 10 und 7 wirklich in den Arbeitsspeicher kopiert, bevor sie in Register geladen werden? Das kann ich einfach nicht glauben.

Ariser - Monica wieder einsetzen
quelle
Eine globale Variable wird in den Speicher geladen und nie daraus freigegeben. Der obige Code kopiert die Daten nur bei Bedarf in den Speicher und gibt sie dann frei, wenn sie fertig sind. Beachten Sie auch, dass der obige Code nur ein Byte aus dem string_tableArray liest . Dieses Array könnte 20 KB groß sein und würde niemals in den Speicher passen (auch nicht vorübergehend). Mit der obigen Methode können Sie jedoch nur einen Index laden.
Gerben
@Gerben: Dies ist ein echter Nachteil bei globalen Variablen, ich habe dies noch nicht berücksichtigt. Ich bekomme jetzt Kopfschmerzen. Und das Code-Snippet war nur ein Beispiel aus der Dokumentation. Ich habe es unterlassen, etw zu programmieren. Ich selbst, bevor ich mich über die Konzepte geklärt habe. Aber ich habe jetzt einen Einblick. Vielen Dank!
Ariser - wieder Monica
Ich fand die Dokumentation etwas verwirrend, als ich sie zum ersten Mal las. Schauen Sie sich auch einige Beispiele aus dem wirklichen Leben an (z. B. eine Bibliothek).
Gerben

Antworten:

10

AVR ist eine modifizierte Harvard-Architekturfamilie , sodass Code nur in Flash gespeichert wird, während Daten bei der Bearbeitung hauptsächlich im RAM vorhanden sind.

Lassen Sie uns in diesem Sinne Ihre Fragen beantworten.

Warum um alles in der Welt muss ich den verdammten Inhalt in den Arbeitsspeicher kopieren, bevor ich darauf zugreifen kann?

Sie müssen dies nicht per se tun, aber standardmäßig setzt der Code voraus, dass sich die Daten im RAM befinden, es sei denn, der Code wird so geändert, dass er speziell in Flash danach sucht (z. B. mit strcpy_P()).

Und wenn dies wahr ist, was passiert dann mit dem gesamten Code? Wird es vor der Ausführung auch in den RAM geladen?

Nee. Harvard-Architektur. Weitere Informationen finden Sie auf der Wikipedia-Seite.

Wie wird der Code (32kiB) dann mit nur 2kiB RAM behandelt?

Die vom Compiler generierte Präambel kopiert die Daten, die geändert werden sollen, in SRAM, bevor das eigentliche Programm ausgeführt wird.

Wo tragen diese kleinen Kobolde Disketten?

Keine Ahnung. Aber wenn Sie sie zufällig sehen, kann ich nichts tun, um zu helfen.

... werden 5, 10 und 7 wirklich in den RAM kopiert, bevor sie in Register geladen werden?

Nein, nein. Der Compiler wertet den Ausdruck zur Kompilierungszeit aus. Was auch immer sonst passiert, hängt von den Codezeilen ab.

Ignacio Vazquez-Abrams
quelle
Ok, ich wusste nicht, dass AVR Harvard ist. Aber ich bin mit diesem Konzept vertraut. Abgesehen von den Goblins weiß ich, wann ich diese Kopierfunktionen jetzt verwenden muss. Ich muss die Verwendung von PROGMEM auf Daten beschränken, die selten zum Speichern von CPU-Zyklen verwendet werden.
Ariser - wieder Monica
Oder ändern Sie Ihren Code, um ihn direkt von Flash aus zu verwenden.
Ignacio Vazquez-Abrams
Aber wie würde dieser Code aussehen? Angenommen, ich habe mehrere Arrays von uint8_t, die Zeichenfolgen darstellen, die ich über SPI auf ein LCD-Display übertragen möchte. 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};Wie bringe ich diese Daten zum Flashen und später in die Funktion SPI.transfer (), die pro Aufruf einen uint8_t benötigt?
Ariser - wieder Monica
8

So wird Print::printaus dem Programmspeicher in der Arduino-Bibliothek gedruckt:

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*ist eine leere Klasse, die überladene Funktionen wie print ermöglicht, um einen Zeiger auf den Programmspeicher von einem auf den normalen Speicher zu unterscheiden, da beide const char*vom Compiler gesehen werden (siehe /programming/16597437/arduino-f- was-macht-es-tatsächlich-tun )

Sie können also die printFunktion für Ihr LCD-Display überladen , sodass es ein __FlashStringHelper*Argument aufnimmt , es aufruft LCD::printund dann lcd.print(F("this is a string in progmem"));' to call it.F () verwendet. `Ist ein Makro, das sicherstellt, dass sich die Zeichenfolge im Programmspeicher befindet.

Um die Zeichenfolge vorab zu definieren (um mit dem integrierten Arduino-Druck kompatibel zu sein), habe ich Folgendes verwendet:

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

Ich denke, eine Alternative wäre so etwas wie

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;
}

das würde die __FlashStringHelperBesetzung vermeiden .

geometrisch
quelle
2

In der Arduino-Dokumentation heißt es, dass es möglich ist, Konstanten wie Strings oder was auch immer ich zur Laufzeit nicht ändern möchte, im Programmspeicher zu behalten.

Alle Konstanten befinden sich zunächst im Programmspeicher. Wo sonst wären sie, wenn der Strom ausgeschaltet ist?

Ich halte es für irgendwo im Codesegment eingebettet, was in einer von-Neumann-Architektur durchaus möglich sein muss.

Es ist eigentlich Harvard-Architektur .

Warum um alles in der Welt muss ich den verdammten Inhalt in den Arbeitsspeicher kopieren, bevor ich darauf zugreifen kann?

Das tust du nicht. Tatsächlich gibt es einen Hardware-Befehl (LPM - Load Program Memory), der Daten direkt aus dem Programmspeicher in ein Register verschiebt.

Ich habe ein Beispiel für diese Technik in Arduino Uno Ausgabe auf VGA-Monitor . In diesem Code ist eine Bitmap-Schriftart im Programmspeicher gespeichert. Es wird im laufenden Betrieb gelesen und wie folgt in die Ausgabe kopiert:

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

Eine Demontage dieser Leitungen zeigt (teilweise):

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

Sie können sehen, dass ein Byte Programmspeicher in R30 kopiert und dann sofort im USART-Register UDR0 gespeichert wurde. Kein RAM beteiligt.


Es gibt jedoch eine Komplexität. Für normale Zeichenfolgen erwartet der Compiler, dass Daten im RAM und nicht in PROGMEM gefunden werden. Sie sind unterschiedliche Adressräume, und daher unterscheidet sich 0x200 im RAM von 0x200 im PROGMEM. Daher macht sich der Compiler die Mühe, Konstanten (wie Zeichenfolgen) beim Programmstart in den Arbeitsspeicher zu kopieren, sodass er sich später keine Gedanken mehr über den Unterschied machen muss.

Wie wird der Code (32kiB) dann mit nur 2kiB RAM behandelt?

Gute Frage. Sie werden nicht mit mehr als 2 KB konstanten Zeichenfolgen davonkommen, da nicht genügend Platz zum Kopieren vorhanden ist.

Aus diesem Grund unternehmen Leute, die Dinge wie Menüs und andere wortreiche Dinge schreiben, zusätzliche Schritte, um den Zeichenfolgen das PROGMEM-Attribut zu geben, das das Kopieren in den RAM verhindert.

Aber ich bin verwirrt über diese Anweisungen, nur Daten aus dem Programmspeicher zu lesen und zu drucken:

Wenn Sie das PROGMEM-Attribut hinzufügen, müssen Sie Schritte ausführen, um den Compiler darüber zu informieren, dass sich diese Zeichenfolgen in einem anderen Adressraum befinden. Das Erstellen einer vollständigen (temporären) Kopie ist eine Möglichkeit. Oder drucken Sie einfach byteweise direkt aus PROGMEM. Ein Beispiel dafür ist:

// 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

Wenn Sie dieser Funktion einen Zeiger auf eine Zeichenfolge in PROGMEM übergeben, führt sie das "spezielle Lesen" (pgm_read_byte) durch, um die Daten aus PROGMEM und nicht aus dem RAM abzurufen, und druckt sie aus. Beachten Sie, dass dies einen zusätzlichen Taktzyklus pro Byte erfordert.

Und noch interessanter: Was passiert mit Literalkonstanten wie in diesem Ausdruck a = 5*(10+7), wenn 5, 10 und 7 wirklich in den RAM kopiert werden, bevor sie in Register geladen werden? Das kann ich einfach nicht glauben.

Nein, weil sie nicht sein müssen. Das würde sich zu einer Anweisung "Literal in Register laden" kompilieren. Diese Anweisung befindet sich bereits in PROGMEM, daher wird das Literal jetzt behandelt. Sie müssen es nicht in den Arbeitsspeicher kopieren und dann zurücklesen.


Ich habe eine ausführliche Beschreibung dieser Dinge auf der Seite Konstante Daten in den Programmspeicher (PROGMEM) einfügen . Das hat Beispielcode zum relativ einfachen Einrichten von Strings und Arrays von Strings.

Außerdem wird das Makro F () erwähnt, mit dem Sie einfach aus PROGMEM drucken können:

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

Ein bisschen Präprozessor-Komplexität ermöglicht das Kompilieren in eine Hilfsfunktion, die die Bytes in der Zeichenfolge byteweise aus PROGMEM zieht. Es ist keine Zwischenverwendung von RAM erforderlich.

Es ist einfach genug, diese Technik für andere Dinge als seriell (z. B. Ihr LCD) zu verwenden, indem Sie den LCD-Druck aus der Druckklasse ableiten.

Als Beispiel habe ich in einer der LCD-Bibliotheken, die ich geschrieben habe, genau das getan:

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

Der entscheidende Punkt hierbei ist, vom Drucken abzuleiten und die "Schreib" -Funktion zu überschreiben. Jetzt macht Ihre überschriebene Funktion alles, was sie zur Ausgabe eines Zeichens benötigt. Da es von Print abgeleitet ist, können Sie jetzt das Makro F () verwenden. z.B.

lcd.println (F("Hello, world"));
Nick Gammon
quelle