Daten von serieller Schnittstelle sinnvoll "zerlegen" und weiterverarbeiten

Guten Abend zusammen,

ich habe ein paar Fragen zu folgender Situation: Der uC erhält Daten über die serielle Schnittstelle, welche 6 Zeichen lang sind und folgendes Muster haben: xx--yy
xx ist dabei "eine" Zahl (00,01,02...99), die -- sind Trennzeichen (einfach nur der Optik halber), die yy sind wiederum eine Zahl im Format wie xx. Ich sende später also Befehle wie z.B. 01--15 oder 17--02.

Serielle Daten werden soweit ich weiß Zeichen für Zeichen eingelesen. Mein Problem ist, wie ich die Stellen 1-2 und 5-6 (bzw. 0-1 und 4-5) wieder zu einer Zahl zusammenbaue. Wenn ich zu Beginn zwei chars einlese (z.B. '1' und '7'), soll daraus eine 17 werden. Kann ich den char dazu mit atoi in einen int umformen und meine zwei int Werte zusammenfügen, indem ich die 1 mit 10 multipliziere (da Zehnerstelle) und die 7 mit 1 (da Einerstelle)? Soll heißen (110)+(71)=10+7=17 (Datentyp int).

Bis dahin erstmal die Frage, ob das so möglich ist und sinnvoll ist, oder geht das auch einfacher?

Wenn das so ziemlich der einzige Weg ist, würde ich mit der 2. "Zahl" identisch verfahren.

Der erste Wert nimmt später 01-20 an, der zweite 01-99, wie frage ich das sinnvoll ab? Gibt es eine Alternative zu if...else if...else if......? switch sollte auch gehen, aber das sind dann auch enorm viele case.

Wenn ihr Ideen habt, freue ich mich über ein paar Stichworte und gut gemeinte Ratschläge.
Schönen Abend noch :slight_smile:

Wenn ich zu Beginn zwei chars einlese (z.B. '1' und '7'), soll daraus eine 17 werden. Kann ich den char dazu mit atoi in einen int umformen und meine zwei int Werte zusammenfügen, indem ich die 1 mit 10 multipliziere (da Zehnerstelle) und die 7 mit 1 (da Einerstelle)? Soll heißen (110)+(71)=10+7=17 (Datentyp int).

Ja, nur atoi brauchst du nicht.

char Zehner = '1';
char Einer = '7';
byte Wert = ( Zehner - '0' ) * 10 + (Einer - '0');  // Wert ist jetzt die Zahl 17

Statt -'0' siehst du manchmal auch -48, aber das braucht man gar nicht zu wissen.
Wissen muss man nur dass das eingelesene Zeichen '0' was anderes als 0 ist,
und dass die Zeichen '0' , '1' , ... , '9' aufeinander folgen.

Es gibt zwei grobe Möglichkeiten:

1.) Man liest alles erst mal in einen Puffer ein und verwendet dann strtok() um den String zu zerteilen und atoi() um die einzelnen Tokens in Integer zu wandeln. Das ist einfach und flexibel, aber braucht etwas RAM für den Puffer (was aber meistens nicht schlimm ist)

2.) Man liest Zeichen für Zeichen ein und addiert jede Ziffer wie oben gezeigt auf die aktuelle Zahl. Ist mehr Code, aber auch relativ einfach wenn man es mal verstanden hat.

Danke für Eure schnellen Antworten.
Ich dachte gerade wegen char mit ascii Zeichen etc. an das atoi, aber wenn mans nicht braucht, umso besser.

Ist folgender Auszug ein sinnvoller Anfang, wenn ich mich für Sereniflys 2. Methode entscheide? Die erste klingt erstmal abschreckend, ich kenne mich zwar einigermaßen mit C aus, aber Stringbefehle etc. sind mir immer noch ein wenig fremd.

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

void loop() {
        if (Serial.available() = 5) { // Bei 6 Zeichen = 5 oder = 6?
                char1 = Serial.read();
                char2 = Serial.read();
                lim1 = Serial.read(); // Trennzeichen 1
                lim2 = Serial.read(); // Trennzeichen 2
                char3 = Serial.read();
                char4[1] = { Serial.read(), '\0' }; // Brauche ich die '\0' als Abbruchzeichen, macht der Array Sinn?

                
// Hier käme dann die Berechnung/Zusammensetzung der Zahlen....
        }
}

Mit stellt sich dann weiterhin die Frage, wie ich am effektivsten die Werte abfrage und welche zuerst. Im Moment frage ich ja zuerst ab, ob 6 Zeichen eingegeben wurden, danach würde ich in der Theorie die 2 Trennzeichen überprüfen (if (lim1=='-' && lim2=='-')) und in dieser Verschachtelung dann die Zahlen (erst Zahl 1, dann Zahl 2).

Da hast du noch was zu lernen. Vorsicht mit char und Strings. Das wird gerne verwechselt. In C sind Strings ganz einfach Null-terminierte char Arrays. String Funktionen wie atoi() brauchen korrekt terminierte Strings damit sie funktionieren

Hiermit liest du einen String der mit einem Linefeed abgeschlossen ist (ganz wichtig! Irgendwie musst du merken wann du fertig bist):

const int SERIAL_BUFFER_SIZE = 21;
char serialBuffer[SERIAL_BUFFER_SIZE];

bool readSerial()
{
  static byte index;

  while(Serial.available())
  { 
     char c = Serial.read();
 
     if(c >= 32 && index < SERIAL_BUFFER_SIZE - 1)
     {
         serialBuffer[index++] = c;
     }
     else if(c == '\n')
     {
        serialBuffer[index] = '\0';
        index = 0;
        return true;
     }
  }
  return false;
}

Wandeln kann man dann z.B. so machen:

void parseSerial()
{
   int numbers[5];

   int count = 0;
   char* ptr = strtok(serialBuffer, ",");
   while(count < sizeof(numbers) / sizeof(int) && ptr != NULL)
   {
      numbers[count++] = atoi(ptr);
      ptr = strtok(NULL, ",");
   }
}

Das ist aber nicht die einzige Lösung. Wenn man feste Variablen hat geht natürlich auch direkt das:

void parseSerial()
{
    int var1 = atoi(strtok(serialBuffer, ","));
    int var2 = atoi(strtok(NULL, ","));
    int var3 = atoi(strtok(NULL, ","));
}

Die Ziel Variablen können natürlich auch global sein. Oder müssen es wenn man sie noch woanders braucht

Und dann loop() so:

void loop()
{
    if(readSerial())
       parseSerial();
}

Das geht davon aus dass die Zahlen mit Kommas getrennt sind:
“123,456,789”

Ich sende später also Befehle
[...]
Der erste Wert nimmt später 01-20 an, der zweite 01-99,

Nur so als Anmerkung, da du das gesendete Format ja anscheinend selbst bestimmen kannst:
Die Serielle Schnittstelle überträgt 8bit Werte, die vom Monitor als ASCII Zeichen interpretiert werden. Praktischer weise genügen 8bit locker um deinen maximalen Wert von 99 auszudrücken. Anstatt die Werte als dezimalen Klartext zu übertragen könntest du sie auch einfach als char senden. Auf dem Seriellen Monitor siehst du dann für jeden Wert ein Zeichen aus der ASCII Tabelle, nicht schön zu lesen, aber im µC brauchst du nichts anderes tun als das Zeichen auf int zu casten, wenn überhaupt nötig.

char wert1_alsChar = Serial.read();
uint8_t wert1 = (int)wert1_alsChar;

Unter den ersten Zeichen der ASCII Tabelle sind allerdings ein paar Werte mit besonderen Funktionen, wie Zeilenende, die dürften kleinere Probleme machen. Das Alphabet beginnt bei 65, wenn du einfach vor der Übertragung 65 zu jedem Wert addierst und auf dem µC wieder abziehst umgehst du das Problem. Man könnte auch genau schauen welche Zeichen es sind, das weiß ich jetzt nicht auswendig, aber mit 65-255 bleibt der Zahlenbereich noch groß genug für dein Vorhaben.

Bei der Übertragung von Werten darf man auch gerne kreativ sein, dezimal als Klartext ASCII Zeichen macht wenig Sinn, außer wenn man es selbst bequem am Monitor lesen möchte. Jedes Zeichen benötigt 8bit, obwohl man von den 255 zur Verfügung stehenden Werten nur 10 ( Die Zeichen 0-9, oder konkret die Werte 48-57) nutzt.
Passt der Wert nicht in 8bit kann man ihn auch in mehrere 8bit Werte zerlegen und nacheinander schicken. Wie man ihn zerlegt bleibt der Fantasie überlassen. Für den µC wäre es auf Bitebene am einfachsten. Aber 10bit Werte könnte man beispielsweise auch durch 255 teilen, und das Ergebnis und den Rest (modulo) getrennt schicken -> man sendet nur zwei Zeichen für Werte von 0-1023.

Na ja, wenn es auf maximale Datenübertragungsrate ankommt, ist binär schon mindestens doppelt so schnell. Dagegen steht der große Vorteil der seriellen Übertragung, dass sie sehr universell und leicht zu testen ist. (Testdaten per SerialMonitor etc.)

So schlimm finde ich deinen Ansatz nicht:

void loop() {
        if (Serial.available() >= 6) { 
                char c1 = Serial.read();
                char c2 = Serial.read();
                Serial.read(); // Trennzeichen 1 ignorieren
                Serial.read(); // Trennzeichen 2 ignorieren
                char c3 = Serial.read();
                char c4 = Serial.read();
              
                // Hier käme dann die Berechnung/Zusammensetzung der Zahlen....


        }
}

Finde die wesentlichen Unterschiede :wink:

Fehlt natürlich die Klärung, was danach passiert. Wie das ganze auf einen nächsten Datensatz synchronisiert wird ( Zeilenende-Zeichen ?)

Ja, synchronisiert werden muss das noch. Wenn ein Zeichen nicht ankommt ist er aus dem Tritt und liefert nur noch Mist. Sehr bequem, auch was die Lesbarkeit am Monitor betrifft, wäre vor den Werten eine Bezeichnung zu schicken. Bspw "v:" für eine Geschwindigkeit. dann kann man nach dieser Ziechenkette suchen. Man könnte auch noch eine Prüfsumme hinterher schicken, nur für den Fall...
Zeilenende oder CR gehen natürlich auch, dann muss man nur bei jedem ausgelesenen Zeichen prüfen ob es sich um jenes handel, und falls ja beginnt man von vorne, bzw bricht ab und wartet auf die nächste loop.

Wie schon erwähnt entsprechen die ASCII Zeichen 0-9 den Werten 48-57. Zum umrechnen braucht man nur 48 subtrahieren. Dann die Zehner-Ziffer mit 10 multiplizieren und die Einer-Ziffer addieren.

uint8_t wert1, wert2;
wert1 = ((int)c1-48)*10 + (int)c2 - 48;
wert2 = ((int)c3-48)*10 + (int)c4 -48;

Ich glaube hier brauchst es den cast, könnte aber sogar ohne gehen, kanns jetzt nicht testen.

Funktioniert aber nur solange du führende Nullen überträgst.

gsezz:
dezimal als Klartext ASCII Zeichen macht wenig Sinn, außer wenn man es selbst bequem am Monitor lesen möchte.

Wo ASCII Strings sehr sinnvoll sind ist wenn man nicht jedesmal die gleichen Daten überträgt, sondern unregelmäßig verscheidende Datensätze die unterschiedlich behandelt werden.

z.B. sowas:
"a45"
"b56.6"
"c589,569,123"

Dann kann man Anhand des ersten Zeichens eine Fall-Unterscheidung machen und den restlichen Text unterschiedlich behandeln.
Das geht auch mit Binär, wenn man im ersten Byte ein bestimmtes Format angibt das auf beiden Seiten klar definiert ist. Das ist aber letztlich etwas komplizierter.

Klar ist das hin-und-herkonvertieren etwas Aufwand. Aber so langsam ist es nicht. Das geht auch noch mit 500.000 Baud flüssig.

Der Cast ist übrigens unnötig. Mit char kann man beliebig rechnen und es ist mit int kompatibel. Das Arduino "byte" ist ja auch nur ein unsigned char.

Guten Abend und danke für die vielen Rückmeldungen.

Im Moment bin ich mit meinem Programm zufrieden. Ich lese char für char ein und baue mir daraus über die Umrechnung oben eine fertige Zahl - Das ist sicherlich nicht die eleganteste Art es zu programmieren, das soll mir aber für ein Hobbyprojekt egal sein. Ich verwende einen Mega2560, genug Speicherplatz ist also so oder so vorhanden. Ich werde mir aber nach Fertigstellung auf jeden Fall die Zeit nehmen, das Programm umzuschreiben und da bestimmt auch ein paar von euren Ideen gut gebrauchen können :slight_smile:

Habe nun ein anderes Problem, was nicht mehr ganz mit dem Thema hier zu tun hat, ich frage trotzdem mal:
Mit einem der Befehle wird eine Messung durchgeführt, die mit einem delay von 2 Sekunden arbeitet, um genug Messwerte zu ermitteln. Ich öffne den seriellen Monitor, tippe meinen Befehl ein und erhalte nach 2 Sekunden die Rückgabewerte. Nun möchte ich diese Messung allerdings zu jedem Zeitpunkt unter- oder abbrechen können. Mein Stopbefehl wird im Moment natürlich erst bearbeitet, wenn die 2 Sekunden vorbei sind.
Ein Interrupt würde hier vermutlich Sinn machen, weiß aber nicht, wie ich das realisieren soll. Ein Timer Interrupt ist meiner Einsicht nach zwecklos, da ich keinen festen Intervall habe. Das Interrupt muss über die serielle Schnittstelle erfolgen, wenn ein gewisses Zeichen abgeschickt wurde. Während der uC im Delay hängt, blinkt die TX LED ja trotzdem schon, sobald ich das Kommando sende, also sollte das doch möglich sein?

Macht es zur Messerfassung Sinn, statt delay die millis Funktion zu verwenden? Obwohl ich gelesen habe, dass delay Interrupt-kompatibel ist.

Werde mich dazu auf jeden Fall mal informieren, aber vllt. hat ja jemand blitzschnell schon eine Lösung parat, danke im Voraus!

Es ist völlig ok, dass so zu machen. Das hat den Vorteil, dass man weniger Speicher braucht. Das ist auf dem Mega erst mal kein Problem, aber wenn man wirklich sehr, sehr viel Daten einlesen muss kann es auch problematisch sein erst mal alles als String speichern zu müssen.

Man kann das auch um Logik erweitern um Startzeichen, Trennzeichen und Endzeichen zu erkennen, damit das Daten-Protokoll flexibler sein kann. Das ist aber komplizierter als einfach einen String zu bearbeiten.

Macht es zur Messerfassung Sinn, statt delay die millis Funktion zu verwenden?

millis() ist fast immer sinnvoller als delay(). Vor allem für etwas das mehr als nur eine handvoll von ms dauern soll.

Mach deine ganzen zeitlichen Verzögerungen mit millis(). Dann kannst du in den meisten Fällen schnell auf Ereignisse reagieren ohne Interrupts zu verwenden.