Serielle Kommunikation synchronisieren

Hallo,

ich bin noch relativ neu bei der Sache und stehe im Moment vor einem Problem, welches ich einfach nicht lösen kann.

Es geht darum, dass ich am Ende meine RC Helikopter mit einem Xbox Gamepad steuern möchte. Meine Idee dazu war folgende:
Ich schließe das Gamepad an den PC und sende die Position der Gamepad Sticks an einen Arduino Uno, der mir dann ein PPM Signal erzeugt, was ich dann an ein HF-Modul weitergebe. Soweit funktioniert auch alles. :slight_smile:

Jetzt zu Meinem Problem. Ein einzelner PPM Frame ist etwa 22,5 ms lang. davon werden ca. 8-16ms für die Daten benötigt und ca. 6,5-14ms zur Synchronisation. Da sich in der Zeit der Synchronisation der Wert des Signals nicht ändert hab ich hier Zeit für Berechnungen, Kommunikation usw... Auf der Seite kann man sich so ein Signal noch einmal genau anschauen:
http://www.mftech.de/ppm.htm
Wenn mein PC nun ein Signal an den Arduino schickt muss ich damit im Idealfall den Anfang der Synchronisationzeit treffen, damit ich die Daten lesen kann usw..., tue ich das nicht, dann braucht der Arduino zu lange zum lesen und die Synchronisationphase wird zu lang, was dazu führt, dass meine Servos zucken...
Ich bin als erstes etwas Blauäugig rangegangen und habe einfach alle 50ms ein Signal an den Arduino geschickt und mir gedacht, dass ich damit schon irgendwie treffen werde. Leider ist das nun nicht der Fall und auch andere Sendeintervalle helfen nicht.
Als zweites habe ich versucht ein Signal an den PC zu senden, wenn die Synchronisationphase beginnt und ihn dann Daten schicken zu lassen, was dann leider noch vielweniger funktioniert.
Der dritte Versuch war dann über Timer und Interrupts die Kommunikationsphase zu erzwingen. Aber wie man sich schon denken kann wird der Controller dann beim Lesen der Daten gestört und das Signal spinnt durch die unvollständigen Daten wie verrückt...

Für mein Problem gibt es sicherlich eine Lösung, die ich aber auf Grund meiner Unerfahrenheit nicht kenne. Es wäre schön, wenn mir jemand helfen könnte.

Ach ja hier noch der relevante Quelltext: (Ich weiß er ist nicht unbedingt hübsch)

Arduino:

int pin = 12;
int a = 1000;  
int b = 1000;  
int c = 1000;  
int d = 1000;  
int e = 1000;  
int f = 1000;  
int g = 1000;  
int h = 1000;
int incomingByte;
char chan;
unsigned long prevMicros = 0;


void setup() {                
  pinMode(pin, OUTPUT);
  Serial.begin(115200);
  Serial.setTimeout(0);
}


void loop() { 
  unsigned long currentMicros = micros();
  int i = 0;

  if((currentMicros - prevMicros) < (22500-a-b-c-d-e-f-g-h-2.7)){
    digitalWrite(pin, 1);   

    if (Serial.available() > 0) {
      delay(1);
      while(Serial.available() > 0)
      {
        chan = Serial.read();
        switch (chan){
        case 'a':
          a = Serial.parseInt();
          break;
        case 'b':
          b = Serial.parseInt();
          break;
        case 'c':
          c = Serial.parseInt();
          break;
        case 'd':
          d = Serial.parseInt();
          break;
        case 'e':
          e = Serial.parseInt();
          break;
        case 'f':
          f = Serial.parseInt();
          break;
        case 'g':
          g = Serial.parseInt();
          break;
        case 'h':
          h = Serial.parseInt();
          break;
        }
      }
    }
  }
  else{ 

    digitalWrite(pin, 0);   
    delayMicroseconds(300);  
    digitalWrite(pin, 1);
    delayMicroseconds(a);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(b);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(c);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(d);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(e);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(f);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(g);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(h);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    prevMicros = micros();
  }
}

PC(C#):

private void timer1_Tick(object sender, EventArgs e)
        {

                State state = _Controller.GetState();
                RightStick = new ThumbstickState(
                    Normalize(state.Gamepad.RightThumbX, state.Gamepad.RightThumbY, Gamepad.GamepadRightThumbDeadZone),
                    (state.Gamepad.Buttons & GamepadButtonFlags.RightThumb) != 0);

                LeftStick = new ThumbstickState(
                    Normalize(state.Gamepad.LeftThumbX, state.Gamepad.LeftThumbY, Gamepad.GamepadLeftThumbDeadZone),
                    (state.Gamepad.Buttons & GamepadButtonFlags.LeftThumb) != 0);


                string str = "c" + (1000 + (int)(RightStick.Position.Y * 200)) + "b" + (1000 + (int)(RightStick.Position.X * 300)) + "a" + (1000 + (int)(state.Gamepad.RightTrigger / (float)byte.MaxValue * 500))
                + "d" + (1000 + (int)(LeftStick.Position.X * 300));
                label1.Text = str;
                port.Write(str);
        }
      delay(1);

Ein delay() mit einer ganzen Millisekunde hat in diesem Sketch gar nichts verloren und ist auch völlig unnütz.

Zudem: wieso machst Du die Behandlung der seriellen Schnittstelle nicht während Du auf den nächsten Event wartest? Das Zeichen vom Computer kann auch während der Ausgabe empfangen werden.
Dann würde ich auf binäre Übertragung umstellen, dann sparst Du die Zeit zum Parsen.

Vielen Dank für deine Hilfe! Damit gings.

Allerdings habe ich bemerkt, dass mein zitterndes Signal damit garnichts zu tun hat.

Nehme ich nur dieses Minimalbeispiel:

int pin = 8;
int a = 1000;  
int b = 1000;  
int c = 1000;  
int d = 1000;  
int e = 1000;  
int f = 1000;  
int g = 1000;  
int h = 1000;
int incomingByte;
char chan;
unsigned long prevMicros = 0;


void setup() {                
  pinMode(pin, OUTPUT);
  Serial.begin(115200);
  Serial.setTimeout(0);
}


void loop() { 
  unsigned long currentMicros = micros();

  if((currentMicros - prevMicros) < (22500-a-b-c-d-e-f-g-h-2.7)){
    digitalWrite(pin, 1);   
  }
  else{ 
    digitalWrite(pin, 0);   
    delayMicroseconds(300);  
    digitalWrite(pin, 1);
    delayMicroseconds(a);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(b);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(c);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(d);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(e);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(f);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(g);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    digitalWrite(pin, 1);
    delayMicroseconds(h);
    digitalWrite(pin, 0);   
    delayMicroseconds(300);
    prevMicros = micros();
  }
}

Und lasse den PC alle 50ms irgendetwas senden, dann bekomme ich das selbe Zucken des Signals =(.
Ich habe mal ein Video hochgeladen. Ab ca. 11sek. fange ich an vom PC zu senden. Ich hoffe man kann es erkennen.

Schau dir mal die main.cpp an, da steht folgendes:

#include <Arduino.h>

int main(void)
{
	init();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();
    
	for (;;) {
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}

Die for-Anweisung läuft ständig und ruft die loop() auf, nach jedem loop-Durchlauf wird aber auch nach Aktivität an der seriellen Schnittstelle gefragt, wenn ein Event anliegt macht sich das auch in zusätzlichen Durchlaufzeiten bemerkbar - folglich hast du dann auch einen minimalen Versatz im Oszillogramm.

Wer lesen kann, ist klar im Vorteil. Da habe ich mich ja total verhauen.

Die for-Anweisung läuft ständig und ruft die loop() auf, nach jedem loop-Durchlauf wird aber auch nach Aktivität an der seriellen Schnittstelle gefragt, wenn ein Event anliegt macht sich das auch in zusätzlichen Durchlaufzeiten bemerkbar - folglich hast du dann auch einen minimalen Versatz im Oszillogramm.

Das ist nicht richtig. Hier wird nur getestet, ob eine Funktion serialEventRun() definiert ist und falls ja, wird diese aufgerufen.

Die Verzögerung geschieht durch den Interrupt den die Hardware beim vollständigen Erhalt eines Bytes auslöst, damit dieses in den Puffer kopiert werden kann.

Raphael90:
Ach ja hier noch der relevante Quelltext: (Ich weiß er ist nicht unbedingt hübsch)

Deine serielle Einleseroutine auf dem Arduino macht garantiert nicht das, was Du möchtest, ich schreibe mal ein paar Kommentare an den Code:

    if (Serial.available() > 0) {  // ist erfüllt mit nur einem Zeichen im Puffer
      delay(1);  // bei 115200 baud kommen ca. 11520 Zeichen pro Sekunde, in 1 ms also ca. 11 Zeichen
      while(Serial.available() > 0) // hier sind im ersten Durchlauf 12 Zeichen im seriellen Eingangspuffer
      {
        chan = Serial.read();  // ein Zeichen wird ausgelesen, bleiben 11 im Eingangspuffer
        switch (chan){
        case 'a':
          a = Serial.parseInt(); // bei einem vierstelligen Integer werden nochmal vier Zeichen ausgelesen, bleiben 7
          break;                    
        case 'b':                    // mit dem nächsten Schleifendurchlauf "chan = Serial.read();" noch ein Zeichen auslesen, bleiben 6
          b = Serial.parseInt();// und wieder vier auslesen bleiben 2 im Eingangspuffer
          break;
        case 'c':                    // mit dem nächsten Schleifendurchlauf "chan = Serial.read();" noch ein Zeichen auslesen, bleibt 1
          c = Serial.parseInt(); // das Parsen des dritten Integer-Parameters wird abgebrochen, sobald der Eingangspuffer leer ist
          break;
... danach ist Sense mit irgendwas sinnvollem

Das Delay ist ein Murks. Wenn Du mit Delay murksen möchtest, dann muß das Delay natürlich so lange dauern, daß ALLE Zeichen eines Datentelegramms während des Delays eintreffen. Wenn Du 8 Buchstaben und 8 vierstellige Zahlen im Datentelegramm hast, sind das 8+8*4= 40 Zeichen, und bei 11,5 Zeichen pro Sekunde dauert der Empfang 4 Millisekunden und nicht eine.

Die Zeile mit Delay-Murks muß also lauten:
delay(4);
Und dann die Verarbeitungsschleife, weil eben erst 4 Millisekunden nach Empfang des ersten Zeichens das Datentelegramm komplett ist, so dass es in einem Rutsch verarbeitet werden kann.

Ohne zu murksen darf natürlich kein(!) Delay beim Empfang von der seriellen Schnittstelle drin sein und Du darfst auch nicht versuchen, das komplette Datentelegramm in einem Durchlauf der loop zu bekommen. Stattdessen mußt Du in ganz vielen loop-Durchläufen nacheinander das Datentelegramm aus einzelnen empfangenen Zeichen zusammenstückeln, bis es komplett empfangen wurde. Wenn es komplett empfangen wurde, wird es sofort weiter verarbeitet. Und alles komplett ohne Delay, wenn es nicht gemurkst sein soll.

Aber wenn es gemurkst sein soll und mit Deiner Einleselogik funktionieren soll, dann richtig, und dann muß das Delay lang genug zum Empfang eines kompletten Datentelegramms innerhalb der Delay-Zeit sein!

Und die Logik zur Simulation eines PPM Signals muss natürlich auch erstmal stimmen. D.h. bevor Du anfängst, Werte von ausserhalb Deines Arduino zu empfangen, versuchst Du am besten erstmal, Werte aus dem Programm heraus korrekt auf die Servos zu geben und diese mit Werten zu steuern, die bereits im Programm vorhanden sind und nicht empfangen werden müssen. Und wenn das dann mit den Werten und dem Servosteuern klappt, versuchst Du im zweiten Schritt Werte von ausserhalb zu empfangen.

Raphael90:
Und lasse den PC alle 50ms irgendetwas senden, dann bekomme ich das selbe Zucken des Signals .

Ja, der Empfang von Zeichen über die serielle Schnittstelle beim Arduino passiert nicht in Nullzeit, sondern jedes empfangene Zeichen benötigt Zeit für den Empfang. Sobald die serielle Schnittstelle offen ist und Zeichen empfangen werden, fangen im Hintergrund Interruptroutinen die eintreffenden Zeichen ab und stecken sie in den seriellen Eingangspuffer. Die Zeit, die die Interruptroutinen laufen, läuft dann die loop-Funktion verzögert ab. D.h. wenn Zeichen empfangen werden, laufen die Funktionen in der loop ein ganz klein wenig langsamer ab als wenn keine Zeichen empfangen werden. Das Zittern sollte daher verschwinden, wenn Du den Empfang serieller Zeichen in dem Minimalprogramm gar nicht aktivierst, also die Zeile "Serial.begin(115200);" mal auskommentierst, so dass sie nicht ausgeführt wird.

:slight_smile: Nu gehts hier aber los. Vielen Dank schonmal für die Antworten!

jurs:

Raphael90:
Ach ja hier noch der relevante Quelltext: (Ich weiß er ist nicht unbedingt hübsch)

Deine serielle Einleseroutine auf dem Arduino macht garantiert nicht das, was Du möchtest, ich schreibe mal ein paar Kommentare an den Code:

Das Delay ist ein Murks. Wenn Du mit Delay murksen möchtest, dann muß das Delay natürlich so lange dauern, daß ALLE Zeichen eines Datentelegramms während des Delays eintreffen. Wenn Du 8 Buchstaben und 8 vierstellige Zahlen im Datentelegramm hast, sind das 8+8*4= 40 Zeichen, und bei 11,5 Zeichen pro Sekunde dauert der Empfang 4 Millisekunden und nicht eine.

Du wirst wahrscheinlich lachen aber genau die Rechnung habe ich am Anfang auch gemacht. Da ich bisher aber nur mit 4 Kanälen rumspiele habe ich die Zeit durch rumprobieren so weit wie möglich runtergesetzt, um dort die Fehlerquelle zu minimieren.

Mittlerweile empfange ich die Daten während des "pulsens" des PPM Signals und kann auf das Delay verzichten. Wobei es natürlich unschön ist, wenn mir die angespochenen Interruptroutinen dazwischen funken.

jurs:
Und die Logik zur Simulation eines PPM Signals muss natürlich auch erstmal stimmen. D.h. bevor Du anfängst, Werte von ausserhalb Deines Arduino zu empfangen, versuchst Du am besten erstmal, Werte aus dem Programm heraus korrekt auf die Servos zu geben und diese mit Werten zu steuern, die bereits im Programm vorhanden sind und nicht empfangen werden müssen. Und wenn das dann mit den Werten und dem Servosteuern klappt, versuchst Du im zweiten Schritt Werte von ausserhalb zu empfangen.

An dem Punkt bin ich ja gerade. Lasse ich die Servos über eine interne Methode bewegen funktioniert der Weg Arduino->Sender->Empfänger->Servo ohne Probleme. Prinzipiell eben auch mit einem Gamepad nur bekomme ich störende Unregelmäßigkeiten in das Signal, welche die Servos dann als Bewegung interpretieren -.- .

Hält es denn Jemand überhaupt für möglich, dass mein Vorhaben so möglich ist, oder brauche ich einen ganz anderen Ansatz? Ich möchte hier jetzt keine Komplettlösung (wäre ja langweilig) aber ein "nein so wird das nichts.. ich würde es eher so oder so machen" wäre vlt. hilfreich. Ich weiß halt leider nicht wie ich es schaffe die Interrupts so "einzuplanen", dass sie mein Signal nicht stören.

Raphael90:
Hält es denn Jemand überhaupt für möglich, dass mein Vorhaben so möglich ist, oder brauche ich einen ganz anderen Ansatz? Ich möchte hier jetzt keine Komplettlösung (wäre ja langweilig) aber ein "nein so wird das nichts.. ich würde es eher so oder so machen" wäre vlt. hilfreich. Ich weiß halt leider nicht wie ich es schaffe die Interrupts so "einzuplanen", dass sie mein Signal nicht stören.

Möglich ist vieles, aber wenn niemand außer Dir die genaue Hardware, die Software und das exakte Übertragungsprotokoll kennt, ist es immer schwierig zu helfen.

  1. Als erstes würde ich mal prüfen, ob es wirklich die Serial-Interrupts beim Empfang der Zeichen sind, die das Timing der Delays bei Dir zerstören. Was passiert denn, wenn Du im Arduino-Testsketch (der keine Daten empfängt), die Zeile:
    // Serial.begin(115200);
    mal auskommentierst, so daß kein serieller Empfang und daher auch keine seriellen Empfangsinterrupts mehr stattfinden. Ist das Jittern des Signals dann weg?

  2. Außerdem würde ich diese Zeile mal prüfen, was darin der Gleitkommawert 2.7 zu suchen hat:
    if((currentMicros - prevMicros) < (22500-a-b-c-d-e-f-g-h-2.7))

Die kleinste Auflösung des Mikrosekunden-Timers ist 4, d.h. er tickt nicht nur um 1, sondern jeweils um 4 Mikrosekunden weiter. Und Gleitkommaberechnungen kosten viel Zeit. Also was macht da die Gleitkommazahl "2.7" in der Berechnung?

Also was passiert mit dem Jittern des Signals in einem Programmsketch ohne seriellen Datenempfang und was ist mit dem 2.7 Gleitkommawert in der Mikrosekunden-Berechnung?

jurs:

  1. Als erstes würde ich mal prüfen, ob es wirklich die Serial-Interrupts beim Empfang der Zeichen sind, die das Timing der Delays bei Dir zerstören. Was passiert denn, wenn Du im Arduino-Testsketch (der keine Daten empfängt), die Zeile:
    // Serial.begin(115200);
    mal auskommentierst, so daß kein serieller Empfang und daher auch keine seriellen Empfangsinterrupts mehr stattfinden. Ist das Jittern des Signals dann weg?

Ja ab dann ist es weg.

jurs:
2. Außerdem würde ich diese Zeile mal prüfen, was darin der Gleitkommawert 2.7 zu suchen hat:
if((currentMicros - prevMicros) < (22500-a-b-c-d-e-f-g-h-2.7))

Die kleinste Auflösung des Mikrosekunden-Timers ist 4, d.h. er tickt nicht nur um 1, sondern jeweils um 4 Mikrosekunden weiter. Und Gleitkommaberechnungen kosten viel Zeit. Also was macht da die Gleitkommazahl "2.7" in der Berechnung?

:astonished: Gut, dass du das sagst! es müssen 2700 sein.... Das ist die Zeit der Pausen zwischen den Pulsen... Da habe ich irgendwie in ms gerechnet...

Scheint aber auch so zu funktionieren. Habe es jetzt eben hinbekommen. Ich sende dem PC am Ende der Signalphase, dass er nun Daten schicken soll und somit landen die Interrupts in der großen Pause und stören nicht weiter.

Vielen Dank an alle! :slight_smile: