Servo mit Timer und serieller Kommunikation

Hallo,

inspiriert von diesem Thread, http://forum.arduino.cc/index.php?topic=193284.15
war es ehrlich gesagt anfangs eine Machbarkeitsstudio, die jetzt tatsächlich funktioniert. Die loop wird nicht blockiert, die Servos werden exakt angesteuert, und während der Pulspause kann lustig kommuniziert werden. Ich stell es der Allgemeinheit zur Verfügung. Wer damit was anfangen kann oder möchte, nur zu. Hängt euer Oszi an die 4 Ausgänge. DigitalWrite wird man nicht finden, dass hat seinen Grund. Nur die Empfangsbehandlung habe ich hier rausgenommen bzw. wird übersprungen. Mir gehts mehr darum zu zeigen wie man es machen kann mit Timer und der seriellen. Der Code ist sowieso stark gekürzt und auf das wesentliche reduziert auch wegen den 9000 Zeichen. Noch weiter kürzen macht einen Sinn, deshalb hängt er dran.

Servo Steuerung mit Timer 2

  • Timer 2 für Servopulse taktet nur wenn er zur Pulserzeugung benötigt wird
  • loop wird nicht blockiert
  • während der Servopulszeit wird nicht gesendet/empfangen
  • gesendet wird nur während der Servopulspause, wenn Timer 2 gestoppt ist
  • senden wird 2ms vorm nächsten Servopuls gestoppt, damit noch Zeit wäre für einen Empfang
  • Timereinstellungen werden vorab berechnet
  • man kann die Auflösung ° pro Schritt einstellen, 0,5° wären noch machbar, darunter nicht sinnvoll
  • ausgehend von 1°/Schritt wird die T2 ISR aller 11µs aufgerufen
  • zur Kommunikation wird Serial.1 verwendet, Serial.0 wäre für Debugging frei

Das wesentliche sind diese Funktionen:

int calc_pulscount (const int &grad)
{
  int count = (int) (((((grad - WINKEL_R) * microSek_pro_Grad) + 1000) / ISR_refresh_Time) + 0.5);
  return count;
}


void Timer2_Steuerung ()
{
  static unsigned long last_ms = 0;

  if (global_ms - last_ms > 19) {    // ServoPuls Periodendauer
    last_ms = global_ms;
    P28_EIN;
    P29_EIN;
    P32_EIN;
    run_Timer2();
  }
 
  if (global_ms - last_ms > 17) {
    state_running_Timer2 = true;
  }
}


ISR(TIMER2_COMPA_vect)    // wird aller 11µs aufgerufen (Prescaler 8 und OCR2A 21)
{
  P33_EIN;                // Sichtbarkeit das Timer aktiv ist

  static int pulsCounter = 0;
  
  if (pulsCounter == pulsCountServo1) {    // Impulsdauer Servo 1
    P28_AUS;
  }
  if (pulsCounter == pulsCountServo2) {    // Impulsdauer Servo 2
    P29_AUS;
  }
  if (pulsCounter == pulsCountServo3) {    // Impulsdauer Servo 3
    P32_AUS;
  }
  
  pulsCounter++;
  
  if (pulsCounter > COUNT_maxPULS) {        // nach 2,4ms Timer stoppen
    stop_Timer2();
    pulsCounter = 0;
    state_running_Timer2 = false;
  }
  P33_AUS;
}


void stop_Timer2 ()
{
  TCCR2B &= ~( (1 << CS22) | (1 << CS21) | (1 << CS20) ); // Timer stoppen
}


void run_Timer2 ()
{
  TCCR2B |= (1 << CS21);  // Timer mit Prescaler 8 starten
}


void preSet_Timer2 ()
{
  cli();                    // Interrupts ausschalten
  TCNT2 = 0;                // Start 0
  TCCR2A = 0;
  TCCR2B = 0;
  TIMSK2 = 0;
  TIMSK2 = (1 << OCIE2A);   // enable Compare Match A ISR
  OCR2A = CompareMatch_TOP; // TOP Wert bestimmt mit Prescaler den Takt
  TCCR2A = (1 << WGM21);    // CTC
  sei();                    // Interrupts einschalten
}

verbesserte Version in #14

Servo_Timer2_Forum.ino (12.3 KB)

Die Kürzung des Sketches habe ich in Arbeit. Vorweg eine Frage: Womit hast Du dieses hübsche Timing-Bild gemacht (welches Oszi)?

Gruß

Gregor

gregorss: Vorweg eine Frage: Womit hast Du dieses hübsche Timing-Bild gemacht (welches Oszi)?

Mit einem Logic-Analyzer (kein Oszi). Steht doch in der Titelleiste: Saleae Logic 1.2.14 :)

uxomm: Mit einem Logic-Analyzer (kein Oszi).

Ach, da gibt's Unterschiede?!

uxomm: Steht doch in der Titelleiste: Saleae Logic 1.2.14 :)

Software heißt nicht unbedingt wie das Produkt, mit der sie verkauft wird. Aber Du hast recht: Eine Suchmaschine führt zu https://www.saleae.com/de/

Gruß

Gregor

gregorss: Die Kürzung des Sketches habe ich in Arbeit.

Uh, das ist viel Arbeit. Ich bitte um etwas Geduld :-)

Gruß

Gregor

Hallo,

vermutlich willste die reine Servo Timer Steuerung haben ohne Kommunikationballast? Dem kann ich nachkommen. Die Funktionen zur Servowinkeländerung und Update könnte man noch über Arrays realisieren, dafür bin ich aber im Moment zu faul. Die Winkeländerungen sind eh erstmal nur zu Testzwecken.

/*
  Doc_Arduino - german Arduino Forum
  IDE 1.8.5
  Arduino Mega2560

  07.11.2017

  - Servo Steuerung mit Timer 2
  - Timer 2 für Servopulse taktet nur wenn er zur Pulserzeugung benötigt wird
  - loop wird nicht blockiert
  - Timereinstellungen werden vorab berechnet
  - man kann die Auflösung ° pro Schritt einstellen, 0,5° wären noch machbar, darunter nicht sinnvoll
  - ausgehend von 1°/Schritt wird die T2 ISR aller 11µs aufgerufen
*/

#ifndef sbi
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))   // setzt das angegebene Bit auf 1
#endif
#ifndef cbi
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))  // setzt (löscht) das angegebene Bit auf 0
#endif

#define P28_OUT  sbi (DDRA,6)  // PA6 Ausgang, Pin 28, Servo
#define P29_OUT  sbi (DDRA,7)  // PA7 Ausgang, Pin 29, Servo
#define P32_OUT  sbi (DDRC,5)  // PC5 Ausgang, Pin 32, Servo
#define P33_OUT  sbi (DDRC,4)  // PC4 Ausgang, Pin 33, Sichtbarkeit das Timer aktiv ist
#define P28_EIN  sbi (PORTA,6) // einschalten
#define P29_EIN  sbi (PORTA,7) 
#define P32_EIN  sbi (PORTC,5) 
#define P33_EIN  sbi (PORTC,4) 
#define P28_AUS  cbi (PORTA,6) // ausschalten
#define P29_AUS  cbi (PORTA,7) 
#define P32_AUS  cbi (PORTC,5) 
#define P33_AUS  cbi (PORTC,4) 

#define WINKEL_R  45        // realer Servowinkel  45° = 1000µs <-- für sein Servo festlegen/ermitteln
#define WINKEL_L 135        // realer Servowinkel 135° = 2000µs <-- für sein Servo festlegen/ermitteln
#define WINKEL_MIN  45      // Software Limit  30° <-- veränderbar
#define WINKEL_MAX 135      // Software Limit 150° <-- veränderbar

// Berechnungen für Timer Einstellung
#define microSek_pro_Grad  (1000.0 / (WINKEL_L-WINKEL_R))           // Auflösung pro Grad (Diff Norm Pulszeit - Diff Winkel)
#define Aufl 1                                                      // einstellbare Auflösung "Grad pro Änderung"
// CPU Takt/(1/benötigte ISR Intervallzeit[µs]/Auflösung)/Prescaler)-1 und +0.5 gerundet
#define CompareMatch_TOP  (int) ((16.0/(1/microSek_pro_Grad/Aufl)/8)-0.5)                                          
#define ISR_refresh_Time  ((CompareMatch_TOP+1) * 8.0 / 16)           // echte Timer ISR Intervallzeit [µs]
#define COUNT_PERIODENDAUER  (int) ((20000.0/ISR_refresh_Time)+0.5)   // Counts Periodendauer (20000µs / echte Timer ISR Aufrufzeit)
#define COUNT_maxPULS  (int) (2400/ISR_refresh_Time)                  // nach 2,4ms Pulslänge wird Timer2 abgeschalten

typedef enum {PLUS, MINUS} ZUSTAND;    // Steuerzustände
ZUSTAND state1_direction = PLUS;
ZUSTAND state2_direction = PLUS;
ZUSTAND state3_direction = PLUS;

volatile bool state_running_Timer2 = false;
volatile int pulsCountServo1, pulsCountServo2, pulsCountServo3;       // Vergleichswerte in Timer ISR
int servo1angle = 90;   // u.a. Winkelposition nach Reset
int servo2angle = 90;
int servo3angle = 90;

unsigned long global_ms;


void setup()  {

  preSet_Timer2();
  
  P28_AUS; P29_AUS; P32_AUS; P33_AUS;   // Ausgänge auf LOW
  P28_OUT; P29_OUT; P32_OUT; P33_OUT;   // Ausgänge setzen
  
  Serial.begin(500000);
    
  Serial.print(F("us/Grad")); Serial.print("\t\t");
  Serial.print(F("OCR2A")); Serial.print("\t\t");
  Serial.print(F("ISR_us")); Serial.print("\t\t");
  Serial.print(F("c Pulsende"));
  Serial.println();
  Serial.print(microSek_pro_Grad); Serial.print("\t\t");
  Serial.print(CompareMatch_TOP); Serial.print("\t\t");
  Serial.print(ISR_refresh_Time); Serial.print("\t\t");
  Serial.print(COUNT_maxPULS);
  Serial.println();

  global_ms = millis();
  
  // Position nach Reset
  update_Servos_1(servo1angle);
  update_Servos_2(servo2angle);
  update_Servos_3(servo3angle);

}


void loop() {

  global_ms = millis();
  
  Timer2_Steuerung();

  /*
   *  bool "state_running_Timer2" kann abgefragt werden ob Timer läuft oder nicht
   */
     
  Servos();  
  
} // loop Ende


// ****** Funktionen ******* //

int calc_pulscount (const int &grad)
{
  int count = (int) (((((grad - WINKEL_R) * microSek_pro_Grad) + 1000) / ISR_refresh_Time) + 0.5);
  return count;
}


void Timer2_Steuerung ()
{
  static unsigned long last_ms = 0;

  if (global_ms - last_ms > 19) {    // ServoPuls Periodendauer
    last_ms = global_ms;
    P28_EIN;
    P29_EIN;
    P32_EIN;
    state_running_Timer2 = true;
    run_Timer2();
  }
}


ISR(TIMER2_COMPA_vect)    // wird aller 11µs aufgerufen (Prescaler 8 und OCR2A 21)
{
  P33_EIN;                // Sichtbarkeit das Timer aktiv ist

  static int pulsCounter = 0;
  
  if (pulsCounter == pulsCountServo1) {    // Impulsdauer Servo 1
    P28_AUS;
  }
  if (pulsCounter == pulsCountServo2) {    // Impulsdauer Servo 2
    P29_AUS;
  }
  if (pulsCounter == pulsCountServo3) {    // Impulsdauer Servo 3
    P32_AUS;
  }
  
  pulsCounter++;
  
  if (pulsCounter > COUNT_maxPULS) {        // nach 2,4ms Timer stoppen
    stop_Timer2();
    pulsCounter = 0;
    state_running_Timer2 = false;
  }
  P33_AUS;
}


void stop_Timer2 ()
{
  TCCR2B &= ~( (1 << CS22) | (1 << CS21) | (1 << CS20) ); // Timer stoppen
}


void run_Timer2 ()
{
  TCCR2B |= (1 << CS21);  // Timer mit Prescaler 8 starten
}


void preSet_Timer2 ()
{
  cli();                    // Interrupts ausschalten
  TCNT2 = 0;                // Start 0
  TCCR2A = 0;
  TCCR2B = 0;
  TIMSK2 = 0;
  TIMSK2 = (1 << OCIE2A);   // enable Compare Match A ISR
  OCR2A = CompareMatch_TOP; // TOP Wert bestimmt mit Prescaler den Takt
  TCCR2A = (1 << WGM21);    // CTC
  sei();                    // Interrupts einschalten
}


void Servos ()
{
  static unsigned long last_ms = 0;
  
  if (global_ms - last_ms > 1000) {
    last_ms = global_ms;

    // Servo 1
    if (state1_direction == PLUS) {
      servo1angle += 1;
      if (servo1angle > WINKEL_MAX) {
        servo1angle = WINKEL_MAX;
        state1_direction = MINUS;
      }
    }
    if (state1_direction == MINUS) {
      servo1angle -= 1;
      if (servo1angle < WINKEL_MIN) {
        servo1angle = WINKEL_MIN;
        state1_direction = PLUS;
      }
    }

    // Servo 2
    if (state2_direction == PLUS) {
      servo2angle += 4;
      if (servo2angle > WINKEL_MAX) {
        servo2angle = WINKEL_MAX;
        state2_direction = MINUS;
      }
    }
    if (state2_direction == MINUS) {
      servo2angle -= 4;
      if (servo2angle < WINKEL_MIN) {
        servo2angle = WINKEL_MIN;
        state2_direction = PLUS;
      }
    }

    // Servo 3
    if (state3_direction == PLUS) {
      servo3angle += 7;
      if (servo3angle > WINKEL_MAX) {
        servo3angle = WINKEL_MAX;
        state3_direction = MINUS;
      }
    }
    if (state3_direction == MINUS) {
      servo3angle -= 7;
      if (servo3angle < WINKEL_MIN) {
        servo3angle = WINKEL_MIN;
        state3_direction = PLUS;
      }
    }
    
    update_Servos_1(servo1angle);
    update_Servos_2(servo2angle);
    update_Servos_3(servo3angle);
  }
}  


void update_Servos_1 (const int &angle)
{
  static int old_angle = 0;

  if (angle != old_angle) {
    if (WINKEL_MIN <= angle && angle <= WINKEL_MAX) {
      pulsCountServo1 = calc_pulscount(angle);
    }
    old_angle = angle;
  }  
}


void update_Servos_2 (const int &angle)
{
  static int old_angle = 0;

  if (angle != old_angle) {
    if (WINKEL_MIN <= angle && angle <= WINKEL_MAX) {
      pulsCountServo2 = calc_pulscount(angle);
    }
    old_angle = angle;
  }
}


void update_Servos_3 (const int &angle)
{
  static int old_angle = 0;

  if (angle != old_angle) {
    if (WINKEL_MIN <= angle && angle <= WINKEL_MAX) {
      pulsCountServo3 = calc_pulscount(angle);
    }
    old_angle = angle;
  }
}

wer das nachvollziehen möchte, der lässt sein Servo auf Postion 1ms und 2ms fahren und schaut welcher reale Winkel das ist. Diesen Winkel trägt er in Zeile 36 und 37 ein. Der bleibt dann fix. Wenn dann abweichend davon seinem Servo mehr Bewegungsfreiheit zu traut, der kann dann die Softwarelimits davon ausgehend aufweichen. In Zeile 38 und 39. Die maximale Pulsdauer wird in Zeile 48 festgelegt. Solange arbeitet der Timer. Hier nur den Wert von 2400 ändern. Das sind µs. Der gesamte Rest wird vorab berechnet. Die Winkeländerungen in der Demo sind derzeit fest vorgeben in der Funktion Servo. Wert 1°, 4° und 7°. Dann sind die Grad um die sich die Servoposition ändern soll. Das wird dann als Parameter an die update_Servo_x Funktion übergeben. Die Änderungen erfolgen im 1s Abstand. Steht am Anfang der Servo Funktion.

Mehr ist das erstmal nicht. :grin:

Falls sich einer fragt wie fahre ich mein Servo jetzt auf Position 1ms und 2ms?

Werte in Zeile 36/38 und 37/39 gleich setzen. Meinetwegen 45° und 135°. Sucht sich ein Servo aus, meinetwegen Servo 1 und in der Servo Funktion 2x den Wert servo1angle += 1 auf 90 ändern. Dann bewegt sich das Servo immer von 45° auf 135° und zurück, was automatisch 1ms und 2ms entspricht.

Hallo nochmal,

ich bin mit meiner Überarbeitung des Codes nicht ganz durch. Neben ein paar anderen Dingen habe ich den Code zum Beispiel auf 80 Zeichen Breite formatiert, damit ich das ohne hässliche Zeilenumbrüche in einer Festbreitenschrift drucken kann.

Aktuelle, noch nicht fertig überarbeitete Version im Anhang. Die sollte trotz meiner Änderungen funktionsidentisch mit dem Original sein. Sie ist halt eher länger als kürzer geworden :slight_smile:

Gruß

Gregor

PS: „Kürzen“ würde ich, indem ich die Funktionen in eine separate Datei/Tab auslagern würde.

Servo_Timer2_Forum.ino (12.5 KB)

Hallo,

habe den Code überarbeitet, neue Servos können einfacher hinzugefügt werden. Man kann jetzt verschiedene Servos mischen. Es wird die kleinste benötigte Auflösung µs/Grad berechnet, die dann für alle Servos gilt. Dadurch bedingt kommt es bei den restlichen Servos zu Abweichungen in der Berechnung. Bspw. werden 8,5µs/° als Basis für alle ermittelt und ein Servo benötigt eigentlich 11µs/°. Dieses kann dann auch nur in 8,5µs Schritten verstellt werden statt 11µs/°. Umso größer die Auslenkung umso kleiner wird der Fehler und umgekehrt.

Auf defines für sbi und cbi konnte ich nicht verzichten, weil schneller wie mit den Makros kann man keine Pins umschalten. Die Zeit die man in der Timer ISR vertrödeln darf ist genauso lang wie die kleinste Auflösung minus etwas Overhead. Wenn man einen Drehwinkel von 90° abdeckt wären das zum Bsp. 11µs gleichbedeutend mit 11µs/Grad. Ich hatte schon mit Parameterübergabe probiert in Vorbereitung für eine Lib. Das dauert aber alles länger.

Beim testen kam ich auf folgende Werte, sodass man mit ruhigen Gewissen 10 Servos ansteuern kann wenn die ISR aller 11µs aufgerufen wird.
Vergleiche in der ISR
3 … 3,9µs
6 … 6,0µs
8 … 7,6µs
9 … 8,4µs

Damit man das nicht alles selbst berechnen muss wird das im seriellen Monitor zum Anfang angezeigt.

Das eigentlich geniale an dem Code ist, aus meiner Sicht, dass der Timer nur läuft wenn er auch wirklich zur Pulserzeugung benötigt wird. Das verschont die CPU vor unnötigen Timer Interrupts, weil die so kurz sind. Und bedingt durch den Timer hat man astreine Pulsweiten ohne zittern.

Was muss man alles einstellen?

a)
die Pins seiner Servos,
Bsp. Zeile 27-38

b)
die Winkelstellung seiner Servos für normierte 1ms und 2ms Pulslänge,
Bsp. Zeile 82-87,
dient zur Berechnung der individuellen Pulslängen,

c)
jetzt sucht man sich das Servo raus welches den größten Winkelbereich abdeckt mit den Angaben von b) und trägt diese in Zeile 74 ein, diese Werte werden zu Berechnung der “kleinsten Basis” µs/Grad verwendet

d)
die Limits in Zeile 89-94 dienen der Softwareanschläge die man noch einstellen kann, man kann ja durchaus manche Servos mit Pulsweiten unterhalb 1ms und oberhalb 2ms betreiben

e)
in Zeile 96, 97 werden die Pins auf Ausgang und vorher auf Low gesetzt, andere Schreibweise wie pinMode, weil das mit obigen Makros eh schon alles definiert ist

f)
am Ende ergänzt oder kürzt man noch die Servoeinträge in der
Funktion “Servo_Steuerung”, Zeile 175-177
und
in der Timer 2 Compare ISR ab Zeile 193-201,
Zeile 189 und 211 können entfernt werden

g)
für die Demo oder zum testen können die einzelnen Winkeländerung in der Demoloop in Zeile 245 geändert werden

Damit man sich das alles besser vorstellen kann noch ein kleine Video:
Der obere Kanal zeigt die Timer Interrupts. Die 3 darunter die erzeugten Pulsweiten zwischen 1ms und 2ms.

Servo_Timer2_ohne_Serial_013.ino (9.39 KB)

[quote author=uxomm on Nov 06, 2017, 10:24 pm] Mit einem Logic-Analyzer (kein Oszi). [/quote]

gregorss: Ach, da gibt's Unterschiede?!

Ein Oszi hat meist 1 oder 2 Kanäle und zeichnet eine analoge Kurvenform auf. Ain Logikanalyzer zeichnet Digital auf und hat viel mehr Kanäle.

Grüße Uwe

uwefed: Ein Oszi ... Ain Logikanalyzer...

Danke für Deine Erklärung. Meine Bemerkung war allerdings nicht sonderlich ernst gemeint. Mir ist nur mal wieder aufgefallen, dass ich vor dem Stellen einer mehr oder weniger blöden Frage auch mal nachdenken könnte. Ab und zu klappt das nicht sooo gut ...

Gruß

Gregor

PS @Doc Arduino: Deinen überarbeiteten Sketch werde ich mir bei Gelegenheit nochmal zur Aufhübschung vorknöpfen.

Auf defines für sbi und cbi konnte ich nicht verzichten, weil schneller wie mit den Makros kann man keine Pins umschalten.

Ist das so?
Beide Makcros lesen das Portregister, verknüpfen es mit einer Maske und schreiben es dann wieder. Das ist nicht nötig. Diese Zeit lässt also sich nochmal ca. halbieren, indem man die PIN Toggle Funktionalität der modernen AVR nutzt.

Hallo,

haste dir den Code angeschaut? Ich kann nicht sturr Pins toogeln, ich muss Bits gezielt setzen und löschen. Die Pins dürfen in der ISR(TIMER2_COMPA_vect) nicht getoggelt werden. Wie man das erneute Bit löschen verhindert solange der Timer aktiv bleibt weiß ich nicht. Ich denke das alle Ideen noch mehr Takte fressen als was es bringt. if Bedingung sperren wenn Bit schon gelöscht zum Bsp.

Primär zielt der Vergleich wie geschrieben darauf an, das mich eine mögliche Parameterübergabe noch mehr Zeit kostet, wenn ich es komfortabler gestalten würde. Dann könnte man keine 10 Servos bedienen innerhalb 11µs.

soeben erst bemerkt, es fehlt noch ein Vergleich ... ... der letzte Vergleich if (count > _MaxPulscount) spielt hier keine Rolle, der wird erst am Ende gültig und sollte vorher immer mit wenigen Takten übersprungen werden

Hallo,

was ich noch probiert habe ist ein switch case Vergleich in der Timer ISR. Geht aber nicht mit struct array. Habe dann noch die if Vergleiche auf == geändert, verhindert das wiederholte gültig werden, der Unterschied ist jedoch nicht messbar. Ich denke aktuell mehr ist nicht rauszuholen.

Hallo,

nachdem der erste Versuch nicht optimal war, weil viel zu sehr CPU lastig, hängt permanent im Interrupt, musste ich lernen wie man das richtig macht. Das möchte ich nun weitergeben ans Forum. Der Ablauf ist einer Funk RC Anlage nachempfunden. Die Servopulse werden hintereinander an beliebigen Pins erzeugt und die restliche Zeit füllt die Pulspausenzeit auf. Das bedeutet man kann erstmal locker 10 Servos in das übliche 20ms Fenster packen. Wenn man das streckt ginge noch mehr. Bedingt durch den 16MHz µC Takt und Prescaler 8 ist eine theoretische Auflösung von 0,5µs für die Pulslänge möglich. Das entspricht bei angenommenen 90° Auslenkung zwischen 1ms und 2ms letztlich 0,045°.
Für Servos sicherlich Overkill aber die Anwendung ist nicht auf Servos beschränkt.
Dadurch das die ISR größer 1ms aufgerufen wird, bleibt genügend Rechenzeit für das restliche Programm übrig.

1999 Timercounts entspricht 1ms und 3999 Timercounts entsprechen 2ms Pulsweite.

Zur Veranschaulichung wie das abläuft zwei Videos vom Oszi.
Die zwei mittleren Servos bewegen sich mit gleicher Geschwindigkeit.
Das erste Servo am langsamsten.
Das letzte Servo am schnellsten.
Einmal sieht man die ersten 4 Kanäle und dann die ersten und die letzten zwei Kanäle.

Pulserzeugung Ch. 0-1-2-3

Pulserzeugung Ch. 0-1-6-7

ServoPulserzeugung_002.ino (8.41 KB)