Z doświadczenia: Podczas tworzenia projektu na Arduino, najpierw kończą się piny. Możemy jednak zmniejszyć ich użycie przez odpowiednie podłączenie elementów bądź rozszerzanie wejść i wyjść. Później kończy się pamijęć operacyjna, której mamy tylko 2kB.
Dopiero przy bardzo dużych projektach skończy nam się pamięć na program, bo jej jest 32kB.
Pamięć operacyjna kończy się głównie z powodu faktu, że każde dane do funkcji nie będące definicjami są kopiowane z pamięci Flash (programu, nie Flash'a użytkownika) do RAMu w trakcie działania programu. Długie tablice czy łańcuchy tekstu mogą więc sprawiać problemy i zajmować niepotrzebnie pamięć. Jak sobie z tym radzić?

Makro F()
Za pomocą makra F() umieszczamy łańcuch tekstowy tylko w pamięci programu. Dzięki temu możemy kosztem pamięci programu (musimy użyć pamięci programu na procedurę przywołującą łąńcuch) zwolnić pamięć operacyjną gdy np. wyświetlamy stałe łańcuchy tekstu na LCD czy porcie szeregowym (komunikaty). Np.
Serial.println(F("SD2IEC TERMINAL V.1.2 \n INITIALIZATION"));

Uwaga: Porównując techniki, najczęściej własne pozyskiwanie łańcucha z tablicy w za pomocą dyrektywy jest w kodzie programu nieco mniejsze (1-3B) niż użycie makra F, lecz wymaga rozważnego pisania własnego kodu, stąd dla łańcuchów najlepiej użyć po prostu F().

Dyrektywa PROGMEM - co się tym je?
Niemal każda zmienna podczas pracy programu zostaje umieszczona w pamięci RAM, której jest znacznie mniej niż pamięci programu, z którego wartość zmiennej pochodzi. Jeżeli więc mamy dużą zmienną const (przykładowo łańcuch tekstowy, tablica wartości używanych w programie, mapa bitowa np. do wyświetlenia na LCD) może ona po prostu nie zmieścić się nam do RAMu. W takim przypadku, używając dyrektywy PROGMEM pozostawiamy wartości w pamięci programu i z niej korzystamy przy dostępie bajt po bajcie. Konwersja zmiennej na PROGMEM jest realizowana w dwóch etapach: Po pierwsze, wprowadzamy dyrektywę PROGMEM do zmiennej. Następnie zmieniamy wywołania odczytujące zmienną tak, aby używały funkcji wyłuskujących wartość z pamięci programu.
Drobna uwaga: PROGMEM najlepiej działa na zmiennych globalnych. Jeżeli z jakiegoś powodu musimy użyć PROGMEM do zmiennej lokalnej, to powinna to być zmienna statyczna (nie tyle, żeby zachować jej wartość między wywołaniami funkcji, ale żeby kompilator zapisał sobie gdzie się ona w ogóle znajduje).
Kilka przykładów:

const PROGMEM unsigned int charSet[] = {65000, 32796, 16843, 10, 11234};
const char initMessage[] PROGMEM = {"SD2IEC TERMINAL V.2 \n INITIALIZING"};


Jeżeli mamy tablicę 2D (np. łańcuchów tekstowych), wprowadzanie wartości musi odbywać się przez kolejne obiekty, a nie kolumny i wiersze:
const char string_0[] PROGMEM = "String 0";
const char string_1[] PROGMEM = "String 1";
const char string_2[] PROGMEM = "String 2";
const char* const string_table[] PROGMEM = {string_0, string_1, string_2};

(przykład z https://www.arduino.cc/en/Reference/PROGMEM)

Odczytywanie zmiennej polega na użyciu odpowiedniej funkcji, tak więc:
Nie: unsigned int i = charSet[k];
tylko: unsigned int i = pgm_read_word_near(charSet + k);
gdzie k jest iteratorem tablicy.

Jeżeli chodzi o różne typy zmiennych, możemy użyć w bliźniaczy sposób następujących funkcji:
pgm_read_byte - odczytuje 1-bajtową zmienną z pamięci programu (np. byte, char)
pgm_read_word - odczytuje 2-bajtową zmienną z pamięci programu (np. int)
pgm_read_dword - odczytuje 4-bajtową zmienną z pamięci programu (np. long)

Korzystając z tablicy łańcuchów tekstowych musimy skopiować pojedynczy łańcuch do bufora za pomocą specjalnej funkcji strcpy_P:
char buffer[30];
strcpy_P(buffer, (char*)pgm_read_word(&(string_table[i])));

...albo iterować zmienna po zmiennej, co zostanie przedstawione na poniższym przykładzie.

Bardziej zaawansowany przykład:
PROBLEM: Procedura run_script() na Arduino wykonuje skrypt zakodowany w stałych programu. Pojedynczym poleceniem skryptu jest jedna wartość typu long, zawierająca polecenie i argumenty zapisane w jej bitach (mniejsza o to jak jest to zakodowane, po szczegóły odsyłam do kodu testera pamięci, który pewnie puszczę na GitHuba). Dzięki temu program może wykonywać rozmaite czynności zakodowane w postaci tablic zmiennych typu long, a nie w zabierającym dużo pamięci kodzie w C. Potrzebna nam jest biblioteka skryptów, czyli tablica jednowymiarowych tablic wartości long, i to o róznej długości.
Na początku tworzymy pojedyncze skrypty w postaci wektorów long'ów różnej długości:

const long s001[] PROGMEM = {10, 4718736, 8, 4718748, 1179944, 1179948, 2359880, 2359884, 3539816, 3539820};
const long s002[] PROGMEM = {10, 1180224, 8, 1180236, 4718888, 5899116, 2359448, 3539676, 7078328, 7078332};
const long s003[] PROGMEM = {6, 2753184, 8, 2753196, 5505368, 5505372};
const long s004[] PROGMEM = {6, 2753184, 8, 12, 5505368, 8258556};
. . .


A następnie tablicę wskaźników do tych wektorów, aby móc łatwo wybrać np. z menu interesującą nas pozycję:

const long* const script[] PROGMEM = { s001, s002, s003, s004, s005, ...};

Zauważmy, że skrypty mają różną długość, więc umówmy się, że w zerowej wartości long zapisana jest długość skryptu.

Przyjrzyjmy się jak wygląda uruchomienie skryptu numer menuptr1 z biblioteki:
void run_script()
{
  long * ptr = (long*) pgm_read_word(&script[menuptr1]);
  for (long q=1;q<pgm_read_dword(&ptr[0]);q++)
  {
    parse_cmd(pgm_read_dword(&ptr[q]));
  }
} 

Na początku za pomocą pgm_read_word wyłuskaliśmy do ptr wskaźnik do konkretnego wiersza (wskaźnik na Arduino ma 2 bajty). Wiemy, że zerowy jego element to długość skryptu, co wykorzystujemy, by czytać wektor z pamięci programu wartość po wartości i przesyłać je do funkcji odpowiedzialnej za zdekodowanie i uruchomienie. Long'a zawierającego wartość czytamy więc zaczynając od miejsca w pamięci wskazywanego przez ptr tak, jak robimy to w pętli for. Funkcja parse_cmd po prostu wykonuje polecenie skryptu i wraca.

W ten sposób tablica scripts może mieć i kilkanaście kB, co z pewnością nie zmieściłoby się do pamięci operacyjnej, ale my korzystamy z wartości zawartych w pamięci programu.


M. Wilkus, 2017