Arduino und x86: Crosskompilation trotz PROGMEM

Bei meinem derzeitigen Projekt lege ich viel Wert darauf, dass die eigentliche Codebasis nicht nur auf dem Arduino, sondern auch auf normalen PCs mit x86- und x64-Architektur läuft. Bisher war das relativ einfach, da ich - soweit möglich - Plattformspezifika vermeide. Dieses Mal war das jedoch nicht möglich: eine Zwischenschicht musste her.

Was genau ist PROGMEM?

Ein großes Problem der Arduino-Plattform ist ihr begrenzter Hauptspeicher (2kb). Dinge wie längere statische Strings, werden normalerweise als Array definiert. Diese werden beim Starten des Programms in den Hauptspeicher geladen und verbrauchen dort Platz. Um diese unnötige Platzverschwendung zu umgehen, gibt es bei AVR-Plattformen PROGMEM. Statische Variablen, die damit markiert werden, werden nicht in den Haupt-, sondern in den Programmspeicher gepackt und von dort aus gelesen. Der Programmspeicher bietet immerhin satte 32kb Platz. Über spezielle Funktionen kann die Variable dann wieder ausgegeben werden.

Und wo ist jetzt das Problem?

Wie schon angesprochen, möchte ich meinen Code sowohl auf dem Arduino als auch auf dem PC verwenden. Dieser unterstützt PROGMEM jedoch nicht. Wenn wir mal Base64 als Beispiel nehmen: Dort benötigen wir ein Array mit den 64 Zeichen, die für die Codierung verwendet werden. Das sind satte 64 Bytes, die uns vom Hauptspeicher beim Arduino verloren gehen. Auf dem PC macht das natürlich nicht sonderlich viel aus.

Und wie sieht die Lösung aus?

Um die PROGMEM-Funktionalität nutzen zu können, benötigt man sowohl die PROGMEM-Direktive als auch die speziellen Funktionen zum Auslesen des Speichers. Diese stehen auf dem PC natürlich nicht zur Verfügung. Aus diesem Grund habe ich mir einen kleinen Wrapper geschrieben.
Dieser definiert pm_*(x,y) Funktionen, die anstatt der normalen Pgmspace-Funktionen verwendet werden sollen. Zudem werden die für PROGMEM verwendeten Datentypen definiert. Wenn man nun folgenden Code in eine Headerdatei packt, diese neue Headerdatei anstelle von "<avr/pgmspace.h>" inkludiert und die pm_*(x,y) Funktionen nutzt, dann funktioniert der Code plattformübergreifend.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#ifndef __PROGMEMUINO_H__
#define __PROGMEMUINO_H__

#ifdef  __cplusplus
extern "C" {
#endif

#include <stdint.h>

#ifdef ARDUINO

#include <avr/pgmspace.h>

#define pm_byte(x,y)  ((uint8_t)pgm_read_byte_near(x+y))
#define pm_word(x,y)  ((uint16_t)pgm_read_word_near(x+y))
#define pm_dword(x,y) ((uint32_t)pgm_read_dword_near(x+y))
#define pm_float(x,y) ((float)pgm_read_float_near(x+y))

#else

#define PROGMEM /**/

#define prog_char     int8_t
#define prog_uchar    uint8_t
#define prog_int16_t  int16_t
#define prog_uint16_t uint16_t
#define prog_int32_t  int32_t
#define prog_uint32_t uint32_t

#define pm_byte(x,y)  ((uint8_t)x[y])
#define pm_word(x,y)  ((uint16_t)x[y])
#define pm_dword(x,y) ((uint32_t)x[y])
#define pm_float(x,y) ((float)x[y])

#endif

#ifdef  __cplusplus
}
#endif

#endif  /* __PROGMEMUINO_H__ */

Wie sieht das in der Praxis aus?

Ich nehme hier mal ein relativ einfaches Beispiel zur Hand. Hier wird ein String per PROGMEM definiert, anschließend zeichenweise ausgelesen und ausgegeben. Ein einfacheres Beispiel ist mir nicht eingefallen. 😀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// contains the PROGMEM wrapper
#include "progmemuino.h"

// needed for "while (true)" loop
#include <stdbool.h>

#ifndef ARDUINO
// needed for printf()
#include <stdio.h>
// needed for sleep()
#include <unistd.h>
#endif

const uint8_t myLength = 17;
PROGMEM prog_uchar myString[] = "Das ist ein Test.";

void setup() {
#ifdef ARDUINO
    Serial.begin(9600);
    while (!Serial) {
    } // for Leonardo
#endif
}

void loop() {
    uint8_t index;

    for (index = 0; index < myLength; index++) {
#ifdef ARDUINO
        Serial.print((char) pm_byte(myString, index));
#else
        printf("%c", pm_byte(myString, index));
#endif
    }

#ifdef ARDUINO
    Serial.println("");
    delay(1000);
#else
    printf("%s", "\n");
    sleep(1);
#endif
}

// emulates the Arduino loop
#ifndef ARDUINO
int main() {
    setup();
    do {
        loop();
    } while (true);

    return 0;
}
#endif

Ich hoffe, dieser kleine Trick hilft dem ein oder anderen bei der Wiederverwendung seiner Codebasis. Für Fragen, Anregungen und Verbesserungen stehe ich natürlich wie immer zur Verfügung. 🙂
Speichersparende Grüße, Kenny

Schreibe einen Kommentar

Um Ihnen beim weiteren Kommentieren auf dieser Webseite die erneute Eingabe Ihrer Daten zu ersparen, wird beim Absenden Ihres Kommentars ein Cookie an Ihren Browser gesendet und von diesem gespeichert. Mit dem Absenden eines Kommentars auf dieser Webseite stimmen Sie der Speicherung und Übertragung dieses Cookies explizit zu.

Pflichtfelder sind mit * markiert.