In der Einführung zu unserer Blog-Reihe zu Robot Cars haben wir ja bereits erwähnt, dass in Sachen Stromversorgung und Anfängertauglichkeit unser Mikrocontroller Board mit ATmega328P, ATmega16U2, kompatibel mit Arduino UNO R3, die erste Wahl ist. Damit wollen wir heute unser erstes Robot Car bauen. Mit Hilfe eines zweiten MC Boards wollen wir dann dafür eine einfache Fernsteuerung mit einem 433MHz-Sender/Empfänger entwickeln. Los geht’s.
Im ersten Teil benötigte Hardware
Anzahl | Bauteil |
---|---|
1 | Mikrocontroller Board mit ATmega328P, ATmega16U2, kompatibel mit Arduino UNO R3 |
1 | 4-Kanal L293D Motortreiber Shield |
1 | Chassis-Kit für 2- oder 4-rädriges Robot Car |
Batterien/Akkus, z.B. 2 x 18650 LiPo-Akku |
Im zweiten Teil benötigte Hardware
Anzahl | Bauteil |
---|---|
1 | Mikrocontroller Board mit ATmega328P, ATmega16U2, kompatibel mit Arduino UNO R3 |
1 | LCD-Keypad-Shield für Uno R3 u.a. |
2 | 433 MHz Sender/Empfängermodul HC-12 (leider nicht mehr im Sortiment) |
Batterien/Akkus, z.B. 2 x 18650 LiPo-Akku oder 9V-Block |
Ergänzend zu den Bauanleitungen, die den Kits beiliegen, ein paar Tipps für den Anbau des Micro Controllers und des Batteriefachs:
Das Motortreiber-Shield wird zum Schutz für die vielen Kontakte mit einer kleinen Schaumstoffplatte geliefert. Die habe ich mit zweiseitigem (Teppich-) Klebeband unter den Micro Controller geklebt und dann auf das Chassis. Damit erspare ich mir das Bohren neuer Löcher und das Verschrauben mit Abstandshaltern.
Für die Befestigung des Batteriefachs habe ich selbstklebendes Klettband genommen, um dieses ggf. schnell entfernen zu können.
Programm-Bibliothek für die Motoren:
Wie so häufig hat Lady Ada (Limor Fried) mit dem Adafruit Team eine sehr gute Bibliothek entwickelt, die wir mit dem Bibliotheksverwalter (library manager) leicht in die Entwicklungsumgebung einbinden können. Durch den IC 74HC595 (Schieberegister) ist die Programmierung des Motortreiber-Shields relativ komplex, aber diese Arbeit hat uns Adafruit abgenommen. Die Anwendung der Methoden (Funktionen) der Bibliothek AFMotor.h macht es uns einfach, Fahrbefehle an das Robot Car zu übermitteln. Bitte beachten: Adafruit hat heute das Motor Shield V2 im Sortiment. Wir benutzen sozusagen die „alte“ Library, nicht V2. Siehe nächstes Bild „Bibliotheksverwalter“, oberster Eintrag.
Das Codefragment zum Instanziieren der vier Motoren lautet:
#include <AFMotor.h>
AF_DCMotor motor1(4);
AF_DCMotor motor2(3);
AF_DCMotor motor3(1);
AF_DCMotor motor4(2);
Sie wundern sich über die Abweichungen bei den Nummern? Ich wollte die Anschlüsse für die Motoren bei meiner Konfiguration nicht in der Programm-Bibliothek ändern, aber auch meine Systematik für die Nummerierung der Motoren (links ungerade, rechts gerade, von vorn nach hinten) einhalten. Bei zwei Motoren können Sie die nicht benötigten Zeilen auskommentieren oder löschen.
Im weiteren Programm-Code können dann die vier Objekte motor1 bis motor4 mit den Methoden run() und setSpeed() verändert werden, also z.B.
motor1.run(FORWARD);
motor1.setSpeed(200);
Die Methode run() kennt die Parameter FORWARD (vorwärts), BACKWARD (rückwärts) und RELEASE (Stillstand).
Die Methode setSpeed() akzeptiert eine Integerzahl zwischen 0 und 255, auch als Variable, die diese Werte annehmen kann. An dieser Stelle möchte ich jedoch eine Warnung aussprechen, deren Auswirkungen wir später noch einmal thematisieren werden: Der Wert 255 bedeutet volle Spannung, die von der externen Spannungsquelle geliefert wird, bei meinen zwei LiPo-Akkus also rund 8 V. Die Motoren sind jedoch für 5V ausgelegt. Mein höchster Wert darf also nur bei ca. 150 liegen!
Als ersten Schritt empfehle ich einen Funktionstest mit dem Beispiel-Sketch MotorTest, das mit der Programm-Bibliothek installiert wurde. Probieren Sie die Zahlen 1 bis 4 in der Zeile
AF_DCMotor motor(4);
um die Anschlussbelegung zu ermitteln und denken Sie an den Maximalwert für setSpeed() in den for-Schleifen, wenn Sie eine höhere Spannung als 5V nutzen.
Sie geben mir sicherlich recht, dass es langweilig ist, ein vorprogrammiertes Fahrprofil zu nutzen. Irgendwie möchte man das Fahrzeug doch steuern. Beim Raspberry Pi konnte ich eine Funktastatur anschließen oder wie bei den ESP-Micro Controllern eine WLAN-Verbindung nutzen. Das geht bei unserem einfachen Mikrocontroller Board mit ATmega328P, ATmega16U2, kompatibel mit Arduino UNO R3 leider nicht. Aber ich hatte ja vor einigen Monaten das 433MHz Sender/Empfänger-Modul HC-12 getestet und einen dreiteiligen Blog-Beitrag (Link zu Teil 1, Teil 2, Teil 3) dazu geschrieben. Vielleicht kann ich dieses Wissen ja erneut anwenden.
Vorüberlegungen:
- Wenn ich für die Fernsteuerung einen Code senden möchte, muss dieser eindeutig auflösbar, aber auch nicht zu lang sein. Bei früheren Versuchen mit dem HC-12 hatte ich einen vierstelligen Code zum Ein- und Ausschalten einer LED erfolgreich verwendet.
- Wenn ich einen analogen Joystick am Micro Controller anschließe, bekomme ich bei einem 10-Bit-Analogeingang Werte zwischen 0 und 1023 mit Werten für die x- und y-Achse von jeweils 511 in der Mittelposition.
Für die y-Richtung (später Vorwärts- und Rückwärtsfahrt) teile ich diesen Wert durch 100 und erhalten so 11 Fahrtstufen von 0 bis 10 mit Stillstand bei 5. Die Idee mit den Fahrtstufen kann auf andere Controller übertragen werden und bietet weitere Vorteile (s.u.).
- Die Drehzahlregelung erfolgt mit Pulsweitenmodulation (PWM), die Fahrtstufen müssen also im Sketch für den Motor Controller des Robot Car in die Duty Cycle-Werte für die PWM umgewandelt werden. Im Sketch für die Fernsteuerung werden nur die Fahrtstufen verwendet.
- Kurvenfahrt beim Robot Car bedeutet, dass ein Motor schneller und der andere langsamer dreht. Hier entscheide ich mich für jeweils vier Stufen, die die jeweilige Erhöhung bzw. Verminderung der Werte des linken bzw. rechten Motors hervorrufen.
- Um mehrere Möglichkeiten der Eingabe realisieren zu können (Joystick, Keypad mit Tasten oder Tastatur-Eingabe) entscheide ich mich für den Code 505 für die Ruhestellung. Die hintere Zahl (zwischen 1 und 9) steht für die Kurvenfahrt, die vordere Zahl (zwischen 0 und 10) für die Fahrtstufe. Also zum Beispiel Code 1005 für schnellste Geradeausfahrt, 505 für Stillstand, 707 für Vorwärtsfahrt nach rechts.
y ↓ 0 x→ |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
← |
↖ |
↑ |
↗ |
→ |
||||
9 |
← |
↖ |
↑ |
↗ |
→ |
||||
8 |
← |
↖ |
↑ |
↗ |
→ |
||||
7 |
← |
↖ |
↑ |
↗ |
→ |
||||
6 |
← |
↖ |
↑ |
↗ |
→ |
||||
5 |
← |
↖ |
0 |
↗ |
→ |
||||
4 |
← |
↙ |
↓ |
↘ |
→ |
||||
3 |
← |
↙ |
↓ |
↘ |
→ |
||||
2 |
← |
↙ |
↓ |
↘ |
→ |
||||
1 |
← |
↙ |
↓ |
↘ |
→ |
||||
0 |
← |
↙ |
↓ |
↘ |
→ |
Für’s erste entscheide ich mich für das LCD-Keypad-Shield, weil es fünf Tasten für meine Fernsteuerung und ein eingebautes LCDisplay zur Anzeige des gesendeten Codes hat.
- Im Code werden die jeweils größtmöglichen Zahlen mit if-Abfragen begrenzt.
- Für weitere Funktionen (z.B.Servos) stehen Codes oberhalb von 1010 zur Verfügung.
Das LCD-Keypad-Shield:
Die fünf Taster des LCD-Keypad-Shield sind mit Spannungsteilern am Eingang A0 angeschlossen. Wie das funktioniert, hat Andreas Wolter in seinem Blog Arduino: Multi-IO und EEPROM - [Teil 1] gut erklärt.
Daneben liegen leicht zugänglich die Pins A1 bis A5, die ich für den 433 MHz-Sender HC-12 nutzen möchte. Dafür löte ich dort eine Federleiste an. Das gleiche mache ich auch am Motortreiber-Shield; auch hier sind die analogen Eingänge leicht zugänglich.
Pinbelegung:
A1=GPIO15 für Spannungsversorgung 5V
A2=GPIO16 für GND
A3=GPIO17 und A4=GPIO18 für RX/TX mittels SoftwareSerial
Noch zwei Anmerkungen zu dem Motor-Shield:
1. Während die Kabel an den Motorklemmen (links im Bild, rechts verdeckt vom HC-12) austauschbar sind, muss die Polarität der externen Spannungsversorgung unbedingt beachtet werden.
2. Der Jumper (Kurzschlussstecker) PWRJMP in der Bildmitte stellt die Verbindung des Micro Controllers zur externen Spannungsversorgung her. Nur bei geöffnetem Pin die MCU über USB an den Computer anschließen. Eine galvanische Trennung erfolgt jedoch durch die gesamte Schaltung auch dann nicht.
/*
LCD-Keypad-Shield als MotorController, Stillstand = 505
je 5 Stufen vor/zurück, je 3 Stufen rechts/links
*/
#include <SoftwareSerial.h>
//Anschlussbelegung für den HC-12 Transceiver
SoftwareSerial mySerial(18, 17); //RX, TX
int HC12PWR = 15;
int HC12GND = 16;
//int HC12SET = 19;
// Das LCD hat keinen I2C-Adapter
// Die Daten werden über die Pins D4 bis D7 übertragen
#include <LiquidCrystal.h>
//LCD pin to Arduino
const int pin_EN = 9;
const int pin_RS = 8;
const int pin_D4 = 4;
const int pin_D5 = 5;
const int pin_D6 = 6;
const int pin_D7 = 7;
LiquidCrystal lcd( pin_RS, pin_EN, pin_D4, pin_D5, pin_D6, pin_D7);
int x = 5;
int y = 5;
int delay4bounce = 250;
void setup() {
pinMode(HC12PWR,OUTPUT);
digitalWrite(HC12PWR,HIGH);
pinMode(HC12GND,OUTPUT);
digitalWrite(HC12GND,LOW);
// pinMode(HC12SET,OUTPUT);
// digitalWrite(HC12SET,LOW);
mySerial.begin(9600);
// Initialisierung des eingebauten LCD
lcd.begin(16, 2); //LCD1602 mit 16 Zeichen und 2 Zeilen
lcd.setCursor(0,0); //Zählung beginnt bei Null, erst Zeichen, dann Zeile
lcd.print("AZ-Delivery.com");
lcd.setCursor(0,1); // 0=Erstes Zeichen, 1=zweite Zeile
lcd.print("Press Key:");
}
void sendcode() {
mySerial.println(100*y + x); //send code for motor
delay(200); // little delay for next button press
}
void loop() {
int A0;
// alle Taster liegen über einen Spannungsteiler an A0
// mit 3,9 kOhm Widerstand liegen die Spannungen zwischen 0 und 3,3 V
// Werte des ADC liegen zwischen 0 und 1023
// ggf. müssen die Werte angepasst werden
A0 = analogRead (0); //
lcd.setCursor(10,1); // Cursor beginnt hinter "Press Key"
if (A0 < 60) {
x = x+1;
if (x>9) {x=9;}
delay(delay4bounce);
lcd.print (" ");
lcd.setCursor(10,1);
lcd.print (100*y + x);
sendcode();
}
else if (A0 < 250) {
y = y+1;
if (y>10) {y=10;}
delay(delay4bounce);
lcd.print (" ");
lcd.setCursor(10,1);
lcd.print (100*y + x);
sendcode();
}
else if (A0 < 400){
y = y-1;
if (y<0) {y=0;}
delay(delay4bounce);
lcd.print (" ");
lcd.setCursor(10,1);
lcd.print (100*y + x);
sendcode();
}
else if (A0 < 700){
x = x-1;
if (x<1) {x=1;}
delay(delay4bounce);
lcd.print (" ");
lcd.setCursor(10,1);
lcd.print (100*y + x);
sendcode();
}
else if (A0 < 900){
x = 5;
y = 5;
delay(delay4bounce);
lcd.print (" ");
lcd.setCursor(10,1);
lcd.print (100*y + x);
sendcode();
}
else {
lcd.setCursor(10,1);
lcd.print (100*y + x);
sendcode();
}
}
Bevor ich Ihnen nun einen möglichen Sketch für das obige Robot Car vorstelle, möchte ich die Idee hinter den Fahrstufen noch einmal aufgreifen:
Die Annahme, dass die Motordrehzahl proportional zur Spannung ist, stimmt nur bedingt. Bauartbedingt drehen die Motoren nicht bei niedrigen Spannungen. Man hört ein Brummen, aber das Drehmoment reicht nicht zum Anlaufen aus. Die erste Fahrtstufe liegt also bei einem Tastgrad von mindestens 30-40%. Und wir hatten gesehen, dass wir in Abhängigkeit von der verwendeten Spannungsquelle die Spannung für die höchste Stufe herabregeln müssen, in meinem Fall ca. 66%. Meine fünf Fahrtstufen je Richtung liegen also bei einem Tastgrad zwischen 30 und 66. Die habe ich in eine Liste eingetragen und greife über den Index auf den jeweiligen Wert zu. Ich habe bei dem Arduino Sketch mit der o.g. Motor-Bibliothek folgende Werte verwendet.
int speedLevel[11]={-65,-57,-49,-41,-33,0,33,41,49,57,65};
Achtung: Es gibt auch Unterschiede bei den Micro Controllern: Bei 8-Bit PWM liegen die Werte zwischen 0 und 255, bei 10-Bit PWM zwischen 0 und 1023, beim Raspberry Pi und dem Modul gpiozero zwischen -1 und +1.
Mit dem Multiplikator faktor passe ich dann die Werte für das jeweilige System an. Aber auch die Werte in der Liste können selbstverständlich je nach Situation angepasst werden.
Und auch bei den Werten für die Kurvenfahrt müssen Sie ggf. ein wenig tüfteln. Bei den von mir verwendeten vier Stufen je Richtung erhöhe ich bzw. vermindere die Werte für den jeweiligen „speedLevel“ auf beiden Seiten.
Hier der Sketch für das Robot Car mit der o.a. Fernsteuerung:
// Adafruit Motor shield library
// copyright Adafruit Industries LLC, 2009
// this code is public domain, enjoy!
// modified for AZ-Delivery
#include <AFMotor.h>
AF_DCMotor motor1(4);
AF_DCMotor motor2(3);
AF_DCMotor motor3(1);
AF_DCMotor motor4(2);
#include <SoftwareSerial.h>
// initialize HC-12
SoftwareSerial mySerial(18, 17); // RX, TX
int HC12PWR = 15;
int HC12GND = 16;
//int HC12SET = 19;
int x = 0;
int y = 0;
int left = 0;
int right = 0;
int code = 505;
int codeRead = 505;
int speedL = 0;
float faktor = 2.2; // Korrektur für Fahrtstufe
void setup() {
Serial.begin(9600); // set up Serial Monitor at 9600 bps
Serial.println("Motor test!");
mySerial.begin(9600); // set up transmission speed for HC-12
// initialize HC-12 power supply
pinMode(HC12PWR,OUTPUT);
digitalWrite(HC12PWR,HIGH);
pinMode(HC12GND,OUTPUT);
digitalWrite(HC12GND,LOW);
// pinMode(HC12SET,OUTPUT);
// digitalWrite(HC12SET,LOW);
// turn on motor
// motor4.setSpeed(200);
// motor4.run(RELEASE);
}
void loop() {
if (mySerial.available() > 1) {
//read serial input and convert to integer (-32,768 to 32,767)
codeRead = mySerial.parseInt();
Serial.print("code received: ");
Serial.println(codeRead);
if (codeRead<=1010) {
if (code != codeRead) {
code = codeRead;
}
}
else {
Serial.print("wrong code received: ");
code = 505;
}
}
else {
code = 505;
}
motor();
mySerial.flush();//clear the serial buffer for unwanted inputs
delay(20); //little delay for better serial communication
}
void motor(){
int speedLevel[11]={-65,-57,-49,-41,-33,0,33,41,49,57,65};
y = int(code /100);
x = code - 100*y;
speedL = speedLevel[y];
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==0){
right = speedL+16;
left = speedL-16;
}
else 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 if (x==10) {
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 < 25 & left > -25) {
motor1.run(RELEASE);
motor3.run(RELEASE);
}
if (right < 25 & right > -25) {
motor2.run(RELEASE);
motor4.run(RELEASE);
}
if (left>=25) {
if (left>65) left=65;
motor1.run(FORWARD);
motor3.run(FORWARD);
motor1.setSpeed(left * faktor);
motor3.setSpeed(left * faktor);
}
if (right>=25) {
if (right>65) right=65;
motor2.run(FORWARD);
motor4.run(FORWARD);
motor2.setSpeed(right * faktor);
motor4.setSpeed(right * faktor);
}
if (left<= -25) {
if (left<-65) left=-65;
motor1.run(BACKWARD);
motor3.run(BACKWARD);
motor1.setSpeed(-left * faktor);
motor3.setSpeed(-left * faktor);
}
if (right<= -25) {
if (right<-65) right=-65;
motor2.run(BACKWARD);
motor4.run(BACKWARD);
motor2.setSpeed(-right * faktor);
motor4.setSpeed(-right * faktor);
}
}
Mit dem Mikrocontroller Board mit ATmega328P, ATmega16U2, kompatibel mit Arduino UNO R3 haben wir eine robuste, mit der Arduino IDE leicht zu programmierende MCU. Bestens geeignet nicht nur für den Einstieg in die Welt der Programmierung (neudeutsch: computational thinking), sondern auch für den Zugang zu elektronischen Schaltungen (neudeutsch: physical computing).
Mit dem 4-Kanal L293D Motortreiber Shield gelingt der leichte Einstieg in die Robotik unter Zuhilfenahme von vorhandenen Programm-Bibliotheken wie der Adafruit Library <AFMotor.h>.
In weiteren Blog-Beiträgen werden wir alternative Lösungen mit anderen Micro Controllern und weiteren Möglichkeiten der Fernsteuerung zeigen.