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"));
string_table
Array 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.