Mit Roboterautos die Mobilität der Zukunft verstehen, Teil 2
Willkommen zur zweiten Folge unserer neuen Blogreihe über Roboterautos. Im ersten Teil haben wir die Grundlagen für das Basisfahrzeug kennengelernt. Diesmal wollen wir Möglichkeiten der Fernsteuerung thematisieren, dabei ein Codesystem entwickeln, mit dem wir Fahrtstufen des Roboterautos bestimmen und schließlich eine einfache Fernsteuerung mit einer Infrarot-Fernbedienung realisieren.
Wofür benötige ich Fahrtstufen bei einem RC Car für Arduino Mikrocontroller?
1. Für die Steuerung der Motordrehzahlen von einem anderen Gerät aus gibt es grundsätzlich die Möglichkeit, mit einem Potentiometer (Poti) kontinuierlich Werte zu ermitteln und zu übertragen, oder auf Tastendruck den Wert der Fahrtstufe zu erhöhen bzw. zu vermindern. Auf den folgenden Bildern einige ausgewählte Beispiele, im ersten Bild Joystick-Steuerungen:
Auf dem zweiten Bild sehen wir Infrarot-Fernsteuerungen, eine Smartphone App mit Bluetooth-Empfänger sowie das LCD1602-Keypad-Shield, das u.a. mit einem 433 MHz Transceiver betrieben werden kann:
Um ein einheitliches Schema zu erreichen, macht es Sinn, die analogen Werte des Potis mit der map()-Funktion auf Werte zu reduzieren, die per Funk übertragen werden können. Der Analog-Digital-Konverter liefert 10-Bit-Zahlen, also Werte zwischen 0 und 1023. Die Mittelposition der Mini-Joysticks liegt bei ca. 511. Bei Division des Wertes durch 100 kann man mit einem Joystick zwei Werte (x- und y-Richtung) ermitteln, die zwischen 0 und 10 oder bei Anwendung der map()-Funktion zwischen 1 und 9 liegen. Das reicht allemal für die Eingabe der Geschwindigkeit und für Kurvenfahrten. Egal, ob wir 11 oder 9 Werte ermitteln, Stillstand bedeutet Wert 5 in y-Richtung und für Geradeausfahrt ebenfalls Wert 5.
Mit Blick auf den Controller mit den zwei Joysticks (Teil des Bausatzes 4DOF Mini Roboterarm Bausatz mit Joysticks und Servoantrieb) entscheide ich mich bei der Vielfalt von verschiedenen Systemen für einen gut zu übertragenden Code zwischen 1111 und 9999, wobei die erste Stelle für die y-Richtung des linken Joysticks, die zweite Stelle für die x-Richtung des linken Joysticks, die dritte und vierte Stelle für einen optionalen zweiten Joystick oder bestimmte Tasten verwendet werden können. Dabei benötigen wir im ersten Beispiel mit der IR-Fernsteuerung nur die ersten beiden Stellen des vierstelligen Codes.
Die Drehzahlregelung beim RC Car für Arduino
Die Drehzahlregelung der Motoren erfolgt mit Pulsweitenmodulation (PWM). Der Wert für den sogenannten Duty Cycle ist eine 8-Bit-Zahl, also ein Wert zwischen 0 und 255 (=2 hoch 8 -1). Wer mit einem regelbaren Netzteil Spannungen zwischen 0 und 6V an den Motor anlegt, wird feststellen, dass bis ca. 1,5 V gar nichts zu bemerken ist. Dann fängt der Motor an zu brummen, bewegt sich aber nicht. Wer zugleich die Stromstärke misst, wird einen relativ hohen Wert feststellen. Die Energie wird in Wärme und Brummen umgesetzt – nicht wirklich gut. Ab 2,4 – 3 Volt fängt der Motor an zu drehen, die Stromstärke sinkt etwas, wenn der Motor unbelastet ist. Danach steigt die Drehzahl in Abhängigkeit von der angelegten Spannung. Beim Herunterregeln der Spannung dreht der Motor bis unter 2 Volt, aber wenn er z.B. durch erhöhten Reibungswiderstand oder ein Hindernis zum Stehen kommt, läuft er nicht wieder an. Fazit: Werte unterhalb von ca. 2,4 Volt sollten vermieden werden, die Eingabe sollte gleich 0 (null) gesetzt werden, um unnötigen Verschleiß und Stromverbrauch zu verhindern. Außer Stillstand (Code=5) benötigen wir Duty Cycle zwischen ca. 40% und 100% bei einer Versorgungsspannung von 6V für die Motoren.
Die Spannungsversorgung beim RC Car für Arduino
Wir haben im ersten Teil bereits mehrere Möglichkeiten der Spannungsversorgung thematisiert. Vier AA-Batterien je 1,5V ergeben 6V, die maximale Spannung der kleinen gelben Motoren. Vier AA-Akkus je 1,2 V reichen nicht als Spannungsversorgung. Man benötigt dann einen Batteriehalter für 6 Akkus und das ergibt 7,2 Volt. Und zwei Lithium-Ionen-Akkus (Nennspannung 3,7V) liefern im voll aufgeladenen Zustand über 8 Volt. Es macht also Sinn, im Programm einen Faktor festzulegen, der die Spannung der höchsten Fahrtstufe bei 6V limitiert.
Die Steuerung des RC Car für Arduino
Sofern die Fahrtanweisungen mit Tastern (engl. buttons), die sich ggf. sogar in einer Smartphone-App befinden können, erfolgen, wird zum Ausgangswert 55xx (Stillstand) jeweils der passende Wert erhöht oder vermindert, ohne dass das Maximum überschritten werden kann.
Den Code richtig auslesen
Die Zuordnung der ersten Stelle des Codes zu den erforderlichen Werten der Fahrtstufen erfolgt über den Index einer Liste mit den jeweiligen Zahlenwerten.
Fazit: Mit einem vierstelligen Code können wir mit den ersten beiden Stellen Geschwindigkeit und Kurvenfahrten steuern, die hinteren zwei Stellen dienen weiteren Funktionen (zunächst nicht benötigt). Also zum Beispiel Code95xx für schnellste Geradeausfahrt, 55xx für Stillstand, 77xx für Vorwärtsfahrt nach rechts.
y ↓ 0 x→ |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
9 |
← |
|
↖ |
|
↑ |
|
↗ |
|
→ |
8 |
← |
|
↖ |
|
↑ |
|
↗ |
|
→ |
7 |
← |
|
↖ |
|
↑ |
|
↗ |
|
→ |
6 |
← |
|
↖ |
|
↑ |
|
↗ |
|
→ |
5 |
← |
|
↖ |
|
0 |
|
↗ |
|
→ |
4 |
← |
|
↙ |
|
↓ |
|
↘ |
|
→ |
3 |
← |
|
↙ |
|
↓ |
|
↘ |
|
→ |
2 |
← |
|
↙ |
|
↓ |
|
↘ |
|
→ |
1 |
← |
|
↙ |
|
↓ |
|
↘ |
|
→ |
Nun wollen wir mit dem Kit, einem Mikrocontroller vom Typ ATmega328 (Bauform des UNO R3), einem MotorShield V2 und IR-Sender und -Empfänger unser erstes Smart Robot Car bauen.
Das Motor Shield V2 kann bis zu vier Motoren steuern, dabei werden für den Anschluss der Steuerleitungen der sogenannte I2C-Bus mit den Anschlüssen SDA (=Serial Data) am analogen Eingang A4 und SCL (=Serial Clock) an A5 benutzt. Auch hierfür hat Adafruit eine passende Programm-Bibliothek entwickelt und bereitgestellt. Achtung: Die Bibliotheken für die Motor Shields V1 und V2 sind nicht kompatibel.
Bild Motor Shield V2 mit Modifikation:
Eingelötete Buchsenleisten (Federleisten) für Anschluss von Zusatzausstattung
Egal, welches Motor Shield man benutzen möchte - V1 oder V2 -, es macht Sinn, bei beiden Motor Shields zusätzliche Buchsenleisten einzulöten, um später Bluetooth- oder 433 MHz-Sender/Empfänger (engl. Transceiver=Transmitter + Receiver) oder Sensoren anzuschließen. Mehr dazu in den folgenden Blog-Beiträge. Der IR-Empfänger benötigt nur Spannungsversorgung aus Pin 3 und 4 sowie Pin2 für den IR Receiver. Der Lötkolben kann noch kalt bleiben.
Als Steuerung werden wir zunächst die kleine Infrarot-Fernbedienung von Funduino und einen IR-Empfänger benutzen. Obwohl der „nackte“ IR-Sensor genügt, empfehle ich das kleine Breakout-Board, da hier eine LED flackert, wenn IR-Signale empfangen werden; ein wertvolles Hilfsmittel, wenn ein Fehler gesucht wird.
Der Sketch ist zusammengesetzt aus zwei erprobten Teilen: Dem Beispiel-Sketch von Armin Joachimsmeyer, den er seiner tollen Programm-Bibliothek IRremote beigefügt hat und einem Robot Car-Sketch des Autors, das auf der Programm-Bibliothek von Adafruit beruht.
Der Programmcode für das RC Car für Arduino
/* Sample Code for Robot Car with Motor Shield V2 and IR receiver, as of 20220515
* based on Adafruit Motor shield V2 library, copyright Adafruit Industries LLC, 2009
* and SimpleReceiver.cpp, part of Arduino-IRremote https://github.com/Arduino-IRremote/Arduino-IRremote
* MIT License Copyright (c) 2020-2022 Armin Joachimsmeyer
* modified for Funduino
* Motor Shield V2 utilizes I2C with SDA=A4 and SCL=A5
* IRreceiver utilizes Pin 2
************************************************************************************
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is furnished
* to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*************************************************************************************/
#include <Adafruit_MotorShield.h>
// Create the motor shield object with the default I2C address 0x60
Adafruit_MotorShield AFMS = Adafruit_MotorShield();
// Select which 'port' M1, M2, M3 or M4.
Adafruit_DCMotor *motor1 = AFMS.getMotor(2);
Adafruit_DCMotor *motor2 = AFMS.getMotor(3);
// define protocol for remote control, for more see samples code SimpleReceiver.cpp
#define DECODE_NEC // Includes Apple and Onkyo, also for tiny Funduino remote control
//#define INFO // To see valuable informations from universal decoder for pulse width or pulse distance protocols
#define IR_RECEIVE_PIN 2 // instead of #include "PinDefinitionsAndMore.h"
#include <Arduino.h>
#include <IRremote.hpp>
// IR Receiver signal connected to Pin2, VCC to Pin3, GND to Pin4
int IR_GND = 4;
int IR_VCC = 3;
int x = 0;
int y = 0;
int left = 0;
int right = 0;
int code = 5555;
int speedL = 0;
float factor = 1.8; // Correction for speedLevel 255/100 * 6V/VBatt
void setup() {
Serial.begin(9600);
Serial.println("Motor test!");
Serial.println("Motorshield v2 - DC Motor test!");
if (!AFMS.begin()) { // create with the default frequency 1.6KHz
Serial.println("Could not find Motor Shield. Check wiring.");
while (1);
}
Serial.println("Motor Shield found.");
// Just to know which program is running on my Arduino
Serial.println(F("START " __FILE__ " from " __DATE__ "\r\nUsing library version " VERSION_IRREMOTE));
// Start the receiver
IrReceiver.begin(IR_RECEIVE_PIN); //, ENABLE_LED_FEEDBACK);
Serial.print(F("Ready to receive IR signals of protocols: "));
printActiveIRProtocols(&Serial);
Serial.print(F("at pin "));
Serial.println(IR_RECEIVE_PIN);
// initialize digital pins as output for power supply
pinMode(IR_GND,OUTPUT);
pinMode(IR_VCC,OUTPUT);
digitalWrite(IR_GND,LOW);
digitalWrite(IR_VCC,HIGH);
} // end setup
void loop() {
if (IrReceiver.decode()) {
// Print a short summary of received data
IrReceiver.printIRResultShort(&Serial);
if (IrReceiver.decodedIRData.protocol == UNKNOWN) {
// We have an unknown protocol here, print more info
IrReceiver.printIRResultRawFormatted(&Serial, true);
}
Serial.println();
/*
* !!!Important!!! Enable receiving of the next value,
* since receiving has stopped after the end of the current received data packet.
*/
delay(100); // Entprellen, keine schnelle Wiederholung
IrReceiver.resume(); // Enable receiving of the next value
/*
* Finally, check the received data and perform actions according to the received command
*/
if (IrReceiver.decodedIRData.command == 0x46) {
if (code<9000) code = code + 1000;
Serial.print("Code = ");
Serial.println(code);
}
else if (IrReceiver.decodedIRData.command == 0x15) {
if (code>2000) code = code - 1000;
Serial.print("Code = ");
Serial.println(code);
}
else if (IrReceiver.decodedIRData.command == 0x43) {
if ((code-1000*int(code/1000))<900) code = code + 100;
Serial.print("Code = ");
Serial.println(code);
}
else if (IrReceiver.decodedIRData.command == 0x44) {
if (code-1000*int(code/1000) > 200) code = code - 100;
Serial.print("Code = ");
Serial.println(code);
}
else if (IrReceiver.decodedIRData.command == 0x40) {
code = 5555;
Serial.print("Code = ");
Serial.println(code);
}
else {
Serial.print("invalid code");
}
motor();
}
} // end loop
void motor(){
int speedLevel[9]={-100,-80,-60,-40,0,40,60,80,100};
y = int(code / 1000);
x = int((code - 1000*y) / 100);
speedL = speedLevel[y-1];
Serial.print("code = ");
Serial.print(code);
Serial.print(" y = ");
Serial.print(y);
Serial.print(" x = ");
Serial.print(x);
Serial.print(" speedL = ");
Serial.println(speedL);
//Korrektur der Fahrtstufen für Kurvenfahrt
if (x==1){
right = speedL+16;
left = speedL-16;
}
else if (x==2){
right = speedL+13;
left = speedL-13;
}
else if (x==3) {
right = speedL+10;
left = speedL-10;
}
else if (x==4) {
right = speedL+7;
left = speedL-7;
}
else if (x==6) {
right = speedL -7;
left = speedL+7;
}
else if (x==7) {
right = speedL-10;
left = speedL+10;
}
else if (x==8) {
right = speedL-13;
left = speedL+13;
}
else if (x==9) {
right = speedL-16;
left = speedL+16;
}
else {
right = speedL;
left = speedL;
}
//Eingabe der Fahrtstufen für "left" und "right"
Serial.print("left = ");
Serial.print(left);
Serial.print(" right = ");
Serial.println(right);
if (left < 40 & left > -40) {
motor1->run(RELEASE);
}
if (right < 40 & right > -40) {
motor2->run(RELEASE);
}
if (left>=40) {
if (left>100) left=100;
motor1->run(FORWARD);
motor1->setSpeed(left * factor);
}
if (right>=40) {
if (right>100) right=100;
motor2->run(FORWARD);
motor2->setSpeed(right * factor);
}
if (left<= -40) {
if (left<-100) left=-100;
motor1->run(BACKWARD);
motor1->setSpeed(-left * factor);
}
if (right<= -40) {
if (right<-100) right=-100;
motor2->run(BACKWARD);
motor2->setSpeed(-right * factor);
}
} // end motor
Erklärungen des Programmcodes für das RC Car
Nach dem Inkludieren der Bibliotheken für das Motor Shield V2 und IRremote werden zwei Motoren instanziiert mit der Nummer am Terminal Block des Controllers sowie einige globale Variablen deklariert (Datentyp) und initialisiert (Anfangswert).
In der Funktion setup() werden die Serielle Schnittstelle und der IR Receiver initialisiert sowie die Pins für die Spannungsversorgung des IR Receivers als Ausgänge mit dem Zustand HIGH bzw. LOW eingerichtet.
In der Funktion loop() wird zunächst das Signal der IR-Fernbedienung empfangen, dann in Abhängigkeit der gedrückten Taste der Code für die Motorsteuerung verändert. Die Cursortasten up und down verändern die erste Stelle, left und right die zweite Stelle des Codes im Wertebereich 1 bis 9. Die Taste X führt zum Stillstand mit Code 5555. Die dritte und vierte Stelle des Codes sind zurzeit noch nicht von Bedeutung. Am Ende wird die selbst-definierte Funktion motor() ohne Argumente aufgerufen, da wir die Variablen global, d.h. in allen Funktionen gültig, definiert hatten.
In der selbst definierte Funktion motor(), die wir auch in anderen Konfigurationen mit anderen Fernsteuerungen einsetzen werden, werden aus dem Code die Fahrtstufen für den linken und rechten Motor ermittelt. Der Prozentwert der Fahrtstufe wird schlussendlich mit dem eingangs definierten Faktor in den PWM-Wert für setSpeed umgerechnet.
Damit ist das Robot Car einsatzbereit. Funktioniert prima, solange man die optische Verbindung zwischen Fernbedienung und IR Receiver aufrechterhalten kann. Allerdings habe ich beim Fahren auf der Straße die Erfahrung gemacht, dass starke Sonneneinstrahlung den Empfang behindert. Deshalb steige ich auf Funkfernbedienung um. Bis demnächst.