RC-Fernsteuerung mit Arduino benutzen – viele Grundlagen und das Auslesen

Irgendwo hatte ich sicherlich schon mal erwähnt, dass ich in meiner Freizeit auch ganz gerne LEGO-Technik-Modelle und früher auch schon Fischertechnik-Modelle baue. Zugegebenerweise mache ich das aber eher streng nach Anleitung und weniger in Eigenkonstruktion.

Bei den motorisierten Modellen wird teilweise von LEGO eine Infrarotfernsteuerung dazu gegeben. Das ist irgendwie nicht zufriedenstellend, da das sogar nur digital (Vollausschlag oder Null) ermöglicht.

Hier liegt vom Drohnenfliegen noch eine 6-Kanal-Funkfernsteuerung mit Empfängern herum. Damit müsste man doch auch die LEGO-Motoren steuern können. Aber wie?

Ich führe im folgenden Beitrag langsam an das Thema heran, wie man die Impulse des RC-Empfängers mit einem Arduino Nano nutzbar macht. In einem weiteren Artikel wird es dann darum gehen Motoren zu steuern.

Also los ….

Das benutzte Fernsteuerungssystem ist ein HK-T6A V2 6 Channels von HobbyKing.  Dazu gesellt sich ein handelsüblicher Arduino Nano. Die Schaltung wird auf einem Breadboard aufgebaut.

Es gibt ein sehr gutes Video auf youtube.com von Sparkfun.  In der Reihe …


Quelle: youtube/Sparkfun

… werden unter dem Titel „Using an RC Hobby Controller with Arduino“ die Grundlagen meines Vorhabens erklärt. Entscheidend ist dabei die Darstellung, wie eigentlich das Signal am Empfänger aussieht.


Quelle: youtube/Sparkfun

Die Signalbreite variiert zwischen 1000µs und 2000µs je nach Steuerknüppelstellung. Im Ruhezustand sind etwa 1500µs zu erwarten. Der Arduino bietet mit seinen PWM-Funktionen (Pulsweitenmodulation) Möglichkeiten so ein Signal zu messen.

Es reicht, den Arduino per USB zu verbinden, den Empfänger mit der gelieferten Spannung vom Arduino zu versorgen und den Signalpin für einen Kanal mit einem Digitalpin zu verbinden.

Der Empfängerbaustein hat sieben Reihen zu je drei Pins. Dabei ist die oberste Reihe mit BAT gekennzeichnet, während die anderen Reihen von unten beginnend mit CH1 bis CH6 gekennzeichnet sind.


Quelle: youtube/Sparkfun

In den Reihen ist die Anordnung der Pins immer gleich: Von links nach rechts ist das Signal, Plus und GND. Im Bild sieht man noch links einen Draht herauslaufen. Das ist die Antenne. Vor den folgenden Experimenten muss natürlich der Empfängerbaustein mit dem Sender gepairt werden. Relativ simpel:

Vom Arduino, der schon am USB-Port hängt, führen von Pin 27 5 Volt und von Pin 29 GND zum mittleren Anschlusspin (+) und zum rechten Anschlusspin (GND) des Empfängers in der CH6-Reihe. Die üblicherweise beiliegende Pairingbrücke verbindet nun in der BAT-Reihe Signal- und GND-Anschluss, – also die beiden äußeren. Die LED im Receiver sollte blinken. Während jetzt auf dem Sender der Pairingbutton gedrückt wird, schaltet man diesen ein. Normalerweise hört das Blinken unmittelbar auf, kann aber im Ausnahmefall auch schon mal 10 Sekunden dauern. Damit ist das Pairing abgeschlossen. Die Brücke kann jetzt weg . Falls im Receiver nichts blinkt, liegt das vermutlich daran, dass er keinen Strom bekommt. Und das kann daran liegen, dass der Arduino noch nicht richtig ins Breadboard gedrückt wurde. Er muss wirklich bis zu den Plastikabstandshaltern eingedrückt werden, sonst geht es nicht. Bei mir war das auch die Lösung.

Die Stromversorgung des Empfängers wird nun umgesteckt auf die BAT-Reihe. Beide Stecker also einfach eine Reihe höher. Es funktioniert sonst zwar auch, aber es ist anders geplant. Von der CH2-Reihe auf dem Empfänger wird eine Verbindung zu DIGITAL08 (PIN11) hergestellt.

Ich fange mit dem von Sparkfun dargestellten Programm an. Die endgültige Lösung sieht aber ganz anders aus und wird erst am Ende des Artikels klar.

// Controller pins
const int CH_2_PIN = 11;

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

}

void loop() {
int ch_2 = pulseIn(CH_2_PIN, HIGH, 25000);
Serial.println(ch_2);
delay(5);

}

Ich benutze die Arduino IDE 1.8.5. Am Anfang des Programms wird der Eingangspin definiert. Die setup-Schleife initialisiert nur die serielle Kommunikation über USB. Die loop liest über pulseIn die Länge des HIGH-Impulses in µ-Sekunden ein. Sollte nach 25ms noch kein Signal gekommen sein, wird die Funktion verlassen und 0 übergeben. Genau das wird auch über den seriellen Monitor der Arduino-IDE ausgegeben. Nach 5ms Wartezeit läuft der nächste Durchgang. Dieses Programm läuft vollkommen problemlos für einen Kanal und liefert auch die erwarteten Ergebnisse.

Sobald es aber mehr als zwei Kanäle werden, versagt die Funktion. Der Grund liegt unter anderem darin, dass während der Messung, – also der Ausführung der Funktion pulseIn, das ganze Programm warten muss. Da unsere Messungen parallele Vorgänge in mehreren Kanälen messen sollen, ist das kaum realisierbar.

Abhilfe schafft ein anderer Ansatz: Über einen Timerinterrupt wird immer wieder getestet, ob der betreffende Pin (Kanal) HIGH wird und wann er wieder LOW wird. Den Ansatz habe ich auch in diversen deutschen und internationalen Foren gefunden. Da eh nicht mehr nachvollziehbar ist, wer hier bei wem „abgeschrieben“ hat, erübrigt sich eine Quellenangabe. (siehe auch Hinweis am Ende)

Dummerweise hatte ich zu Beginn dieser Arbeit überhaupt keine Ahnung von Interruptsteuerungen bei Arduinos und erst recht nicht von Timer-Interrupts. Ich habe also zunächst mal angefangen mir dieses Wissen per Internetrecherche anzueignen. Zum Nachlesen gibt es das in diesem Beitrag:  http://sturm.selfhost.eu/wordpress/arduino-timerinterrupts/
oder in dieser pdf-Datei:  Arduino Timer Interrupt

Der zweite Ansatz mit Timer-Interrupt und Eingang auf Pin D2 sieht so aus:

//Variablen
int cnt_CH_2 = 0; //Zähler Channel 2 für Interruptroutine
int frqraw_CH_2 = 0; //Übergabewert Channel 2 aus Interruptroutine

void setup() {

// Controller pins
pinMode(2, INPUT);

cli(); // Clear interrupts Interrupts ausschalten

// Register zurücksetzen
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;

OCR1A = 20; //Output Compare Register auf Vergleichswert setzen

TCCR1B |= (1 << CS11); //Prescale 8
// 16MHz/8=2MHz mit OCR1A=20 Interrupt alle 10µs

TCCR1B |= (1 << WGM12); //CTC-Mode einschalten
TIMSK1 |= (1 << OCIE1A); //Timer Compare Interrupt setzen

sei(); // Set Interrupts Interrupts einschalten

Serial.begin(9600); //serielle Verbindung etablieren

}

ISR(TIMER1_COMPA_vect) { //die Interruptroutine gibt ein Zehntel der Impulsbreite zurück 

if (digitalRead(2)) {
cnt_CH_2++; //wenn Eingang High dann Zähler inkrementieren
}
else if (cnt_CH_2) { //wenn Eingang Low dann prüfen ob Zähler gestartet
frqraw_CH_2 = cnt_CH_2 – 1; //wenn Zähler gestartet, stoppen und Wert übergeben
cnt_CH_2 = 0; //Zähler zurücksetzen
}

}

void loop() {

Serial.println(frqraw_CH_2); //Wert von frqraw_CH_2 an Console senden

}

Der Timer zählt hier hoch und erreicht seinen Vergleichswert nach 10µs. Dann läuft die Routine ISR(TIMER1_COMPA_vect) los.

Sie testet ob der Eingang mit dem Signal vom Empfänger HIGH ist. Wenn das so ist, wird nur der Zähler cnt_CH_2 inkrementiert.

Ist der Eingang Low, wird geprüft ob der Zähler läuft, – also ungleich 0 ist. Wenn das so ist, hat es gerade eine abfallende Flanke im Signal gegeben. Der Zähler wird beendet und resettet und das Ergebnis über eine eigene Variable frqraw_CH_2 zurückgeliefert. Ist es nicht so, machen wir gar nichts.

Rückgabewert frqraw_CH_2 mal Interrupttakt (10µs) ergibt dann die Impulsbreite in µs geteilt durch 10.

Funktioniert perfekt. Leider aber wieder nur für einen Kanal. Dass die Funktion digitalRead(2) so langsam ist, hätte ich nicht erwartet. Sobald man einen zweiten Kanal ausliest, kommt die nächste Interruptauslösung schneller, als die vorhergehende Schleife abgearbeitet wurde. Es kommen also unsinnige Messwerte oder das Programm stürzt ab. Schade!

Die Lösung lag dann darin, die Pins nicht mehr über eine Funktion zu lesen sondern per Bit aus dem entsprechenden Register. Setzt man das D-Register entsprechend, schaltet man die digitalen Pins D0 bis D7 zwischen Eingang und Ausgang um:

DDRD = 0b01111011; //Setzt D2 und D7 als Eingang 0 und die restlichen als Ausgang 1

ausgelesen wird dann mit:

if (PIND & (1<<PD2)) {…} //prüft ob Pin D2 HIGH ist

Diese Art der Pinabfrage soll etwa 50mal schneller sein als die Abfrage über eine Funktion. Für 4 Kanäle reicht es problemlos.

Für spätere Zwecke „verschönere“ ich noch den übergebenen Wert. Zunächst mal verschiebe ich ihn um den Nullpunkt, damit die Neutralstellung bei 0 liegt und je nach Steuerknüppelstellung positive und negative Werte angezeigt werden. Dann spreize ich den Wertebereich auf und kappe dann an der oberen und unteren Seite einen kleinen Teil ab um „unsaubere Anschläge“ zu vermeiden. Zuletzt wird eine Zone um den Nullpunkt definiert, damit sehr kleine Streuwerte keine Steuerungauswirkung haben. Das Ganze wird einstellbar über Variablen definiert. Unsinnige Werte werden zudem ignoriert. Die Funktion, die das bewirkt, sieht so aus:

// RC pulse in PWM für Motor wandeln
int pulseToPWM(int pulse) {

if (pulse > 100) { // nur Werte > 100 verwerten, andere sind Störung oder Sender off

pulse = map(pulse, llevel, hlevel, -500, 500); // Ausgabewertebereich verschieben

pulse = constrain(pulse, -glevel, glevel); // und jetzt noch begrenzen
} else {

pulse = 0; // keine sinnvollen Impulse
}

// Ruhezone einrichten
if (abs(pulse) <= deadzone) {
pulse = 0;
}

return pulse;
}

Ich habe mich sehr stark von einem zufällig gefundenen Beitrag in einem Forum inspirieren lassen und möchte hier auch die Quelle angeben:  http://www.roboternetz.de/community/threads/44329-RC-Empfänger-auslesen

Der Beitrag von radbruch datiert auf September 2009.

Zuletzt noch der Code für das Auslesen von 4 Kanälen:

//Variablen

//RC-bezogen
int cnt_CH_1 = 0; //Zähler Channel 1 für Interruptroutine
int frqraw_CH_1 = 0; //Übergabewert Channel 1 aus Interruptroutine
int cnt_CH_2 = 0; //Zähler Channel 2 für Interruptroutine
int frqraw_CH_2 = 0; //Übergabewert Channel 2 aus Interruptroutine
int cnt_CH_3 = 0; //Zähler Channel 3 für Interruptroutine
int frqraw_CH_3 = 0; //Übergabewert Channel 3 aus Interruptroutine
int cnt_CH_4 = 0; //Zähler Channel 4 für Interruptroutine
int frqraw_CH_4 = 0; //Übergabewert Channel 4 aus Interruptroutine

//Programmbezogen
const int deadzone = 10;
const int hlevel = 174; //höchste gelieferte Frequenz
const int llevel = 115; //niedrigste gelieferte Frequenz
const int glevel = 310; //Grenzlevel für Ergebnis

void setup() {

// Controller pins
DDRD = 0b11000011; //Setzt D2-D5 als Eingang 0 und die restlichen als Ausgang 1
cli(); // Clear interrupts Interrupts ausschalten

// Register zurücksetzen
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;

OCR1A = 20; //Output Compare Register auf Vergleichswert setzen, war 20

TCCR1B |= (1 << CS11); //Prescale 8
// 16MHz/8=2MHz mit OCR1A=20 Interrupt alle 10µs

TCCR1B |= (1 << WGM12); //CTC-Mode einschalten
TIMSK1 |= (1 << OCIE1A); //Timer Compare Interrupt setzen

sei(); // Set Interrupts Interrupts einschalten

Serial.begin(9600); //serielle Verbindung etablieren

}

ISR(TIMER1_COMPA_vect) { //die Interruptroutine gibt ein Zehntel der Impulsbreite in µs zurück

if (PIND & (1<<PD2)) { //Channel 1 rechts horizontal, das ist PIN D2
cnt_CH_1++; //wenn Eingang High dann Zähler inkrementieren

}
else if (cnt_CH_1) { //wenn Eingang Low dann prüfen ob Zähler gestartet
frqraw_CH_1 = cnt_CH_1 – 1; //wenn Zähler gestartet, stoppen und Wert übergeben

cnt_CH_1 = 0; //Zähler zurücksetzen
}

if (PIND & (1<<PD3)) { //Channel 2 rechts vertikal, das ist PIN D3
cnt_CH_2++; //wenn Eingang High dann Zähler inkrementieren

}
else if (cnt_CH_2) { //wenn Eingang Low dann prüfen ob Zähler gestartet
frqraw_CH_2 = cnt_CH_2 – 1; //wenn Zähler gestartet, stoppen und Wert übergeben

cnt_CH_2 = 0; //Zähler zurücksetzen
}

if (PIND & (1<<PD4)) { //Channel 3 links vertikal, das ist PIN D4
cnt_CH_3++; //wenn Eingang High dann Zähler inkrementieren

}
else if (cnt_CH_3) { //wenn Eingang Low dann prüfen ob Zähler gestartet
frqraw_CH_3 = cnt_CH_3 – 1; //wenn Zähler gestartet, stoppen und Wert übergeben

cnt_CH_3 = 0; //Zähler zurücksetzen
}

if (PIND & (1<<PD5)) { //Channel 4 links horizontal , das ist PIN D5
cnt_CH_4++; //wenn Eingang High dann Zähler inkrementieren

}
else if (cnt_CH_4) { //wenn Eingang Low dann prüfen ob Zähler gestartet
frqraw_CH_4 = cnt_CH_4 – 1; //wenn Zähler gestartet, stoppen und Wert übergeben

cnt_CH_4 = 0; //Zähler zurücksetzen
}

}

void loop() {

Serial.println(pulseToPWM(frqraw_CH_1)); //Wert für Kanal 1 an Console senden
Serial.println(pulseToPWM(frqraw_CH_2)); //Wert für Kanal 2 an Console senden
Serial.println(pulseToPWM(frqraw_CH_3)); //Wert für Kanal 3 an Console senden
Serial.println(pulseToPWM(frqraw_CH_4)); //Wert für Kanal 4 an Console senden
Serial.println(“ „);
Serial.println(“ „);
delayMicroseconds(16000); // Damit man überhaupt etwas sieht

}

// RC pulse in PWM für Motor wandeln
int pulseToPWM(int pulse) {

if (pulse > 100) { // nur Werte > 100 verwerten, andere sind Störung oder Sender off

pulse = map(pulse, llevel, hlevel, -500, 500); // Ausgabewertebereich verschieben

pulse = constrain(pulse, -glevel, glevel); // und jetzt noch begrenzen
} else {

pulse = 0; // keine sinnvollen Impulse
}

// Ruhezone einrichten
if (abs(pulse) <= deadzone) {
pulse = 0;
}

return pulse;
}

… und der ganze Code noch als .txt-File: RC_4Ch_mit_TimerISR_01

5 1 vote
Article Rating
Subscribe
Notify of
guest
12 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Miles

Funktionieren 5 Kanäle nur etwas langsamer, oder was passiert dann?

Miles

Funktioniert der Code exakt so auch auf nem Arduino Mega?

Thomas

Hallo,
ich habe die Timer Interrupt Sketche ausprobiert sowohl für 1 Kanal als auch für 4 Kanäle.
Als Ausgabe habe ich 4 LEDs eingesetzt. Im Monitor werden in beiden Fällen Veränderungen angezeit.
Jedoch bleiben die LEDs dunkel. Kann mir da mal jemand helfen oder habt ihr die gleichen Probleme?

MfG
Thomas

Stefan

Ist es möglich, anstelle des Timers1 auch den Timer2 zu benutzen, damit der Sketch nicht in Konflikt mit der Servo.h – Bibliothek kommt?

Vossie

Danke, danke!!!
Ich habe es dank deiner Hilfe geschafft ein zweimotoriges Boot mit einer einfachen 2-Kanal-Fernsteuerung zu steuern.
Als Antrieb dienen jetzt zwei starke DC-775 Motoren mit günstigen 450 Watt Reglern und ein Baumarkt-Akku mit 18 Volt bzw. 36 Volt.
Das Problem mit der Servo.h hatte ich auch. Im Netz gibt es aber genug Beispiele für eine Lösung ohne Library.
Ein Frage habe ich noch, woher kommen die Werte für Grenzfrequenzen hlevel = 174 und llevel = 115?

Gruß Vossie

Daniel Gronych

Lieber Herr Sturm,

ich wollte. Mich nur bei ganz herzlich bei Ihnen für Ihre tolle profunde. Arbeit bedanken. Und dazu teilen Sie Ihr Wissen noch und schreiben dazu eine so geniale Dokumentation. Ihre Seiten sind eine wahre Schatztruhe.

MEGA!

Herzlichen Dank!

LG von Daniel