String (objekt) vs. char array

Hi Leute,

nach langer Pause wollte ich mal wieder etwas mit meinem Arduino machen :wink:

Da ich mit meinem Arduino ASCII-Text √ľber die serielle Schnittstelle empfange und bearbeite, stellt sich mir immer wieder die Frage, ob ein char Array oder ein String in Frage kommt. Beides hat seine Vor- und Nachteile.

Laut Arduino-Referenz benötigt ein String mehr Speicher. …aber was bedeutet das genau? Belegt ein String pauschal xx Byte mehr im RAM oder wächst dies mit der Stringlänge?

Prinzipiell habe ich gehofft, dass ich mit Strings RAM spare, da ich im gegensatz zu einem char array keine max-länge angeben muss. …oder täusche ich mich da?

hier ist mein aktuell bescheidener Versuch:

void setup ()
{
  Serial.begin(115200);
}


#define  PUFFER 100

byte     Segmente_aufnehmen   = 0;
char     Segment;
String   Befehl;
String   Temp;

int     CRC_berechnet;
int     CRC_empfangen;

int      Counter;
int      Index;

char     CRC[4];

int     Pos_a;
int     Pos_b;



void loop ()
{

  while (Serial.available() > 0)
  {
    
    // ein Zeichen aus dem Eingangs-Puffer lesen
    Segment = Serial.read();
    
    // den Befehls-String zusammensetzen
    if (Segment == '

ich habe vor, solche Ausdr√ľcke zu Parsen: $ACCAL,1120,895,0,D2,4220*14
Es hat den gleichen Aufbau, wie der Datenstrom des NMEA 0183-Protokolls (GPS-Daten).

… ist das sehr ineffizient programmiert? zugegeben… ich bastel noch an diesem code.

viele Gr√ľ√üe,
SBond
&& Segmente_aufnehmen == 0) {
      Segmente_aufnehmen = PUFFER-1;
    }
    else if (Segment == ’


ich habe vor, solche Ausdr√ľcke zu Parsen: `¬ß_DISCOURSE_HOISTED_CODE_1_¬ß`
Es hat den gleichen Aufbau, wie der Datenstrom des NMEA 0183-Protokolls (GPS-Daten).

... ist das sehr ineffizient programmiert? zugegeben.... ich bastel noch an diesem code.

viele Gr√ľ√üe,
SBond
 && Segmente_aufnehmen > 0) {
      Segmente_aufnehmen = PUFFER-1;
      Befehl="";
    } 
    else if (Segment == '*' && Segmente_aufnehmen > 2) {
      Segmente_aufnehmen = 3;
    } 

    if (Segmente_aufnehmen > 0) {
      
      Befehl += (char) Segment;
      Segmente_aufnehmen--;
    }
    
    
    // der Befehl ist komplett....
    if (Segmente_aufnehmen == 0 && Befehl != ""){

      // CRC berechnen
      Index = Befehl.indexOf('*') + 1;
      Temp = Befehl.substring(Index);
      Temp.toCharArray(CRC, 4);
      
      CRC_empfangen = (int)strtol(CRC, NULL, 16);
      CRC_berechnet = 0;
      
      for (Counter = 1; Counter < Befehl.length()-3; Counter++){
        CRC_berechnet ^= (int)Befehl.charAt(Counter);
      }
      
      if (CRC_empfangen == CRC_berechnet) {
        Serial.println("CRC OK");
      }
      else {
        Serial.println("CRC FEHELR");
      }
      
      Serial.println(Befehl);
      
      Pos_a = 0;
      
      while(Pos_a >= 0 && Pos_b >= 0) {
        
        Pos_a = Pos_b;
        Pos_b = Befehl.indexOf(',', Pos_a + 1);
        if (Pos_b == -1) {
          Pos_b = Befehl.indexOf('*', Pos_a + 1);
        }
        Serial.println(Befehl.substring(Pos_a + 1,Pos_b));

      }

      Befehl="";
    } 

  } 
      
}

ich habe vor, solche Ausdr√ľcke zu Parsen: ¬ß_DISCOURSE_HOISTED_CODE_1_¬ß
Es hat den gleichen Aufbau, wie der Datenstrom des NMEA 0183-Protokolls (GPS-Daten).

… ist das sehr ineffizient programmiert? zugegeben… ich bastel noch an diesem code.

viele Gr√ľ√üe,
SBond

String bedeutet dynamischer Speicher (malloc, free). Was auf ¬ĶC mit sehr begrenztem RAM generell problematisch ist. Jedenfalls wenn es wie bei Strings so oft gemacht wird. Es sei denn man macht vorher reserve() um Speicher zu reservieren, bedeutet eine Konkatenation mit +, dass im Hintergrund ein neues Objekt erzeugt wird und der Speicher der zwei anderen Objekte freigegeben wird! Leute die da zig Strings aneinanderf√ľgen wissen nicht was sie da speicher- und performance-technisch machen.

Das gleiche ist wenn der String vergrößert werden muss. Ich habe mir die Arduino Implementation nicht angeschaut, aber theoretisch kann man realloc() machen was idealerweise keine Kopie erfordert. Es ist jedoch auch möglich dass malloc() gemacht wird und der alte String einfach kopiert wird. Selbst bei realloc() kann im worst case Kopieren erforderlich sein, wenn der Heap fragmentiert ist und im aktuellen Block nicht mehr genug Platz ist.
Dann wächst ein String (oder Vektor um allgemein zu sein) nicht immer um 1. Das wäre sehr ineffizient. Statt dessen lässt man ihn um z.B. 10 oder 20 Bytes wachsen. Oder was man auch manchmal sieht ist dass der Zuwachs der Kapazität schrittweise größer wird, z.B. 2, 4, 8, 10, 10, 10. Kommt darauf wie das hier gemacht wird, aber einen gewissen Overhead hat man so oder so.

Wie gesagt, dass kann man mit reserve() etwas umgehen:

Damit wird gleich so viel Speicher alloziert wie man angibt. Dann kann man Strings aneinanderf√ľgen ohne dass das interne Array unbedingt vergr√∂√üert werden muss.

Ein anderes großes Problem der String Klasse ist aber, dass die Arduino Implementation sehr minimal ist. Man kann kaum etwas damit machen. Die Standard C String Funktionen in string.h und stdlib.h erlauben wesentlich mehr. Das hat auch die C++ String Klasse etwas an sich, aber da kann man wenigstens mit c_str() einen Zeiger auf das interne Array erhalten. Nicht so mit der Arduino Klasse. Um da einen C String draus zu machen, muss man ein neues Array anlegen. Dann kann man auch gleich C Strings verwenden.

Beim Einlesen hast du zwei Optionen:
1.) immer Teil-Strings bis ein Komma kommt und gleich verarbeiten
2.) den gesamten String einlesen und danach splitten, z.B. mit strtok()

vielen Dank f√ľr die ausf√ľhrliche Antwort.

So wie ich das sehe, werde ich wohl doch wieder auch char array umsatteln. Ich habe selber gemerkt, dass ich mit einem normalen Array mehr machen kann, als mit den Strings. Meine einzige Hoffnung war, dass ich RAM einspare.

danke dir erstmal :wink:

SBond:
Ich habe selber gemerkt, dass ich mit einem normalen Array mehr machen kann, als mit den Strings.

Aha. Deshalb wohl der merkw√ľrdige Zwitter in Deinem Programm, wo vor dem Umwandeln des HEX-Wertes in der CRC-Summe ein Substring in ein char-Array umkopiert wird, um dann doch mit einem char-Array zu arbeiten.

String-Objekte sind eben extrem stark eingeschränkt, was man damit machen kann, das sind nicht besonders viele Funktionen.

SBond:
Meine einzige Hoffnung war, dass ich RAM einspare.

Teste mal diesen Sketch, in dem ich zweimal drei Zeilen auskommentiert habe:

void setup() {
  Serial.begin(9600);
  unsigned long time=micros();
  /*
  char text[31]= "123456890";
  char text1[]="12345568901234556890";
  strncat(text,text1,sizeof(text));
  */
  /*
  String text= "123456890";
  String text1="12345568901234556890";
  text= text+text1;
  */
  time=micros()-time;
  Serial.println(text);
  Serial.print("Dauer: ");Serial.println(time);
  Serial.print("Ram frei: ");Serial.println(freeRam());
}

 int freeRam () {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}

void loop() {}

Entferne mal die Kommentierung f√ľr die einen oder f√ľr die anderen drei Zeilen. Es werden jeweils ein 10 Zeichen langer String und ein 20 Zeichen langer String zu einem 30 Zeichen langen String zusammenkopiert und dabei wird gemessen

  • der Zeitbedarf
  • der freie RAM-Speicher am Ende nach dem Zusammenkopieren

Das Arbeiten mit String-Objekten f√ľhrt im Programm nicht nur zu einem h√∂heren RAM-Speicherverbrauch, sondern im √ľbrigen auch zu einem deutlich h√∂heren Rechenzeitbedarf f√ľr die Stringoperationen.

Ergebnis: char array:
12345689012345568901234556890
Dauer: 40
Ram frei: 1705
Binäre Sketchgröße: 2.874 Bytes

Ergebnis: string:
12345689012345568901234556890
Dauer: 188
Ram frei: 1653
Binäre Sketchgröße: 4.568 Bytes

boa.. :astonished:

ok, das ist echt √ľbel. Aber an die Performance habe ich auch gar nicht gedacht.
vielen Dank f√ľr die Tipps.

An meinem Code sieht man ja schnell, dass ich noch kein gro√üer Programmierer bin. Die Umwandelung der Typen f√ľr den CRC hat mich gestern auch sehr aufgeregt. Bin damit auch nicht zufrieden gewesen.

...aber es kann ja nur besser werden :wink:

SBond:
…aber es kann ja nur besser werden :wink:

Habe Dir mal was aufgeschrieben, nur mit char-Array und char-Pointern.
Dabei bin ich mal davon ausgegangen, dass am Zeilenende ein ASCII-13 (‚ÄúCR‚ÄĚ) gesendet wird:

void setup() {
  Serial.begin(115200);
}

char* SerialStringRead()
{
  static char text[81];
  static byte charcount=0;
  if (!Serial.available()) return NULL;
  if (charcount==0) memset(text,0,sizeof(text));
  char c=Serial.read();
  if (c>=32 && charcount<sizeof(text)-1) 
  {
    text[charcount]=c;
    charcount++;
  }
  else if (c==13) 
  {
    charcount=0;
    return text;
  }
  return NULL;
}

void Auswertung(char* text)
{
//  char text[]="$ACCAL,1120,895,0,D2,4220*14";
  int CRC_empfangen, CRC_berechnet=0;
  char* searchPtr=strchr(text,'*');
  if (searchPtr!=NULL) 
  {
    CRC_empfangen=strtol(searchPtr+1, NULL, 16);
    for (int i=1;i<searchPtr-text;i++) CRC_berechnet^= (int)text[i];
  }
  else CRC_empfangen=-1; // no crc in string
  if (CRC_empfangen==CRC_berechnet) // fehlerfrei empfangen
  {
    Serial.println(F("CRC OK"));
    searchPtr=strtok(text,",*");
    while (searchPtr!=NULL)
    {
      Serial.println(searchPtr);
      searchPtr=strtok(NULL,",*");
    }
  }
  else 
  {
    Serial.print(F("CRC falsch: "));
    Serial.println(text);
  }
}

int freeRam () {
  extern int __heap_start, *__brkval; 
  int v; 
  return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); 
}


void loop() 
{
  char* strPtr=SerialStringRead();
  if (strPtr!=NULL) 
  {
    Auswertung(strPtr);
    Serial.print(F("RAM frei: "));
    Serial.println(freeRam());
  }
}

Die freeRam-Funktion gibt am Ende noch den freien Speicher aus.
Du kannst ja mal spaßeshalber in Deinen Code auch noch die freeRam-Funktion einbauen und vergleichen.

cool :wink:

vielen Dank. Muss ich gleich mal testen :slight_smile:

Ich hole den Thread mal wieder hoch.
Habe meinen Webserver auf Char Arrays umgebaut.
Meine Erfahrungen als Einsteiger dazu:
Man schon aufpassen wenn man den Pointern hantiert, sonst kommt es zu unerwarteten Ergebnissen :grin:
Und es reicht der kleinste Fehler aus um die Anwendung in die W√ľste zu schicken.

Kleine Frage am Rande.
Ich will dies nach der Get Anfrage parsen( also Tag und monat lesen) :

GET /page4.htm?Tag=22&monat=4 HTTP/1.1
Host: 192.168.0.177
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gec
ko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0......

Das mache ich mit diesem Code:

void Parse_Get(char* request )
{

if (strstr(request,"GET /page4.htm?Tag=")!=NULL)
                                             {
                                       
                               tagmode=true;
                              int a,b;
                              char* erg;
                              char* zeich;
                              
                               zeich=strstr(request,"?Tag=");

                                 a=(int)zeich-(int)(request)+5;
                                                 zeich=strstr(request,"&");
                                                  b=(int)zeich-(int)(request);
                                                  erg=(char*)malloc(b-a+1);
                                                  strncpy(erg,request+a,b-a);
                                                 *(erg+b-a)='\0';
                                                 anzeigetag=atoi(erg);
                                                    free(erg);

                                 a=b+7;
                                 zeich=strstr(request,"HTTP/1.1");

                                  b=(int)zeich-(int)(request)-1;
                                               erg=(char*)malloc(b-a+1);
                                                strncpy(erg,request+a,b-a);
                                                *(erg+b-a)='\0';
                                               anzeigemonat=atoi(erg);
                                                  free(erg);
                                                }
}

Der Code funktioniert, nur kommt es mir zu aufwendig vor. Evtl. gibt es eine elegantere lösung ?

Wieso legst du da Speicher mit malloc() an? Das ist völlig unnötig. Du gibt ihn ja selbst gleich wieder frei. atoi() bricht selbst ab wenn es auf einen char trifft der keine Zahl ist. Also beim & und beim Leerzeichen. Du musst den Teil-String also nicht erst in ein extra Array kopieren.

http://www.cplusplus.com/reference/cstdlib/atoi/

The string can contain additional characters after those that form the integral number, which are ignored and have no effect on the behavior of this function.

Selbst wenn, w√ľrde hier ein lokales Array auf dem Stack ausreichen. Das macht man halt so gro√ü wie der l√§ngste String sein kann und fertig.

Deswegen auch meine Frage…es geht mir ums Prinzip. Will das ja lernen :slight_smile:

Wieso legst du da Speicher mit malloc() an? Das ist völlig unnötig.

Wenn ich mit strncpy einen Teilstring kopieren will, muss das Ziel doch definiert sein ? oder nicht ?
Deswegen reserviere ich Speicher, wenn ich das nicht mache, kackt das ganze ab.

Deine Hinweiß zu atoi habe ich verstanden, die Aktion mit strncpy ist unnötig.
Ich muss den Startpunkt im char nur an die richtige Position stellen und atoi √ľbergeben.

rudirabbit:
Der Code funktioniert, nur kommt es mir zu aufwendig vor. Evtl. gibt es eine elegantere lösung ?

void Parse_int(char* request, char* searchstr, int &value)
{
  char* str=strstr(request,searchstr);
  if (str==NULL) value=-1; // Fehlerwert
  else value=atoi(str+strlen(searchstr));
}

void setup() {
  Serial.begin(9600);
  int monat,tag;
  char request[]="GET /page4.htm?Tag=22&monat=4 HTTP/1.1";
  Parse_int(request, "monat=", monat);
  Serial.print("Monat: ");Serial.println(monat);
  Parse_int(request, "Tag=", tag);
  Serial.print("Tag: ");Serial.println(tag);
}

void loop() {}

rudirabbit:
Wenn ich mit strncpy einen Teilstring kopieren will, muss das Ziel doch definiert sein ? oder nicht ?
Deswegen reserviere ich Speicher, wenn ich das nicht mache, kackt das ganze ab.

Ja, schon, aber in diesem speziellen Fall wurde auch ein normales Array z.B. der Größe 5 reichen, da du in etwa weißt wie lang der String maximal ist.

Wenn du nach einem einzelnen Zeichen suchst, ist √ľbrigens strchr() vielleicht die bessere Wahl, auch wenn da strstr() genauso geht:
http://www.cplusplus.com/reference/cstring/strchr/
Belegt halt auch zusätzlich wieder Flash, während strstr() schon drin ist...

Danke euch,
Mein Kernproblem war nat√ľrlich, das ich die funktion von atoi nicht genau verstanden habe :blush:

Und jurs mit seiner dreizeiler Funktion hat mir wieder mal gezeigt wie viel ich noch zu lernen habe.