Programmierung Kamera Slider mit dem Arduino
Im Dritten Teil des Kamera Sliders auf Basis eines Arduino geht es nun endlich um die Programmierung. Mein Quelltext ist nicht sonderlich lang (nur knapp 500 Zeilen), dafür erfüllt es eigentlich jeden Wunsch.
Die einzelnen Bestandteile
Die Slider-Steuerung besteht aus nur 5 Teile:
- Display
- Steuerung des Rotary-Encoders (=Dreh-Druck-Knopf) & Potentiometer
- Menü
- Auslösen der Kamera
- Ansteuern des Motors
Display
Um überhaupt etwas sehen zu können, müssen wir das Display ansteuern.
New LiquidCrystal-Library runterladen
Da wir das über ein I2C-Modul das Display steuern, benötigen wir zu erst die passende Bibliothek “New LiquidCrystal”. Also von Github runterladen: New LiquidCrystal-Bibliothek und nach dortiger Anleitung installieren.
LCD-Display PINs belegen
Da wir das ganze über ein I2C-Modul realisieren brauchen wir nur 4 PINS: Ground, 5V, A4 und A5. Bitte auch genau A4 und A5 belegen. Dann die Arduino Entwicklungsumgebung öffnen und diesen Code ganz oben einfügen:
#include
#include
#define I2C_ADDR 0x27 // Define I2C Address where the PCF8574A is
#define BACKLIGHT_PIN 3
#define En_pin 2
#define Rw_pin 1
#define Rs_pin 0
#define D4_pin 4
#define D5_pin 5
#define D6_pin 6
#define D7_pin 7
LiquidCrystal_I2C lcd(I2C_ADDR,En_pin,Rw_pin,Rs_pin,D4_pin,D5_pin,D6_pin,D7_pin);
Dann kommen wir auch schon zur setup()-Funktion. Die schaut so aus:
void setup() {
lcd.begin(20,4); // Was für ein Display: 20x4 oder 12x2
lcd.setBacklightPin(BACKLIGHT_PIN,POSITIVE); // Beleuchtung PIN festlegen
lcd.setBacklight(HIGH); // Beleuchtung AN
lcd.home(); // Cursor Links oben setzen
lcd.print("Kameraslider v1.0"); // Brauhelfer v1.0 in der ersten Zeile (=0te Zeile im Code) ausgeben
lcd.setCursor(0,1); // Cursor auf die 2te Zeile (=1te Zeile im Code) setzen
lcd.print("Initialisiere...");
lcd.setCursor(0,3);
lcd.print("[15.05.16] by jb-dev.io");
}
Damit können wir schon den ersten Text ausgeben. Glückwunsch :)
lcd.begin(20,4);
Brauchen wir nur einmal aufrufen, um der Bibliothek zusagen, wie groß das Display ist und wie viele Zeichen zur Verfügung stehen
lcd.setBacklight(HIGH);
Auch nur einmalig, damit die Hintergrundbeleuchtung angeht
lcd.home();
Werden wir noch öfter brauchen. Damit setzen wir den Cursor auf “0,0”. Sprich 0te Spalte in der 0ten Zeile. In Programmiersprachen ist die 0 immer der erste Wert. Also 0te Zeile im Code ist die erste Zeile im Display.
lcd.print(”…”);
Text ausgeben, beginnend von der Position, wo der Cursor gerade steht
lcd.setCursor(0,1);
Cursor Position verändern. Dabei ist die erste Zahl die Spalte und die zweite Zahl die Zeile. Also im Display wäre das das erste Zeichen in der 2ten Zeile.
lcd.clear();
Komplettes Display löschen und Cursor auf 0,0 setzen.
Fehlerquellen LiquidCrystal_I2C
- Fehler 1: Falsche Verkabelung. Noch genau prüfen, ob alle Kabel richtig sind.
- Fehler 2: Die Adresse I2C_ADDR 0x27 ist nicht korrekt. Die findet ihr über den Sketch heraus: I2C-Adresse herausfinden. Die 0x27 dann mit der gefunden Adresse ersetzen.
Steuerung des Rotary-Encoders und Potentiometers
Mit dem Encoder möchten wir rauf / runter und bestätigen. Mit dem Potentiometer wollen wir die Größe der Schritte bestimmen (oder wollt ihr 10.000 Schritte mit dem Encoder drehen ;) ).
Rotary Encoder
Zu erst fangen wir mit der PIN-Belegung an und setzen ein paar Variablen:
// Drehknopf
#define DIR_CCW 0x10
#define DIR_CW 0x20
int Rotary1 = 10; // Rotary Drehknopf PIN
int Rotary2 = 11; // Rotary Drehknopf PIN
int RotaryButton = 12; // Rotary Button PIN
int rAlt = 0; // Rotary PressStatus Alt
int rWert = 0; // Rotary DrehWert
//in die void setup() Funktion:
void setup(){
// Rotary Encoder:
pinMode(Rotary1, INPUT); // Drehknopf initialisieren
pinMode(Rotary2, INPUT); // Drehknopf initialisieren
digitalWrite(Rotary1, HIGH);
digitalWrite(Rotary2, HIGH);
pinMode(RotaryButton, INPUT); // Button initialisieren
digitalWrite(RotaryButton, HIGH); // Button aktivieren
}
Falls der Knopf später “falschrum” dreht, tauscht einfach die PIN-Belegung 10 und 11. Falsch gar nichts geht, habt ihr vermutlich die ganze PIN-Belegung verhauen, prüft daher nochmal genau, wo Button und die Dreh-Impuls Kabel hingehen.
Dann prüfen wir mit dem folgender Funktion, ob er gedrückt wurde. Wir machen später immer eine While-Schleife um diese Funktion im Menü. Solange diese false
ist wird sie ausgeführt, wird der Button gedrückt, gibt die Funktion “true” zurück und die Schleife bricht ab:
// Knopfdruck abfragen
boolean pushed(){
int rStatus = digitalRead(RotaryButton);
if (rStatus != rAlt) {
rAlt = rStatus;
if(rStatus == HIGH)
return true;
}
return false;
}
Der Code für die Drehrichtung ist etwas komplizierter, sollte aber 1:1 übernommen werden können. Gibt die Funktion 1 zurück, wurde nach rechts, bei -1 nach links gedreht. Wird false
zurückgegeben, wurde gar nicht gedreht:
// Drehimpuls abfragen
const unsigned char ttable[7][4] = {
{0x0, 0x2, 0x4, 0x0}, {0x3, 0x0, 0x1, 0x10},
{0x3, 0x2, 0x0, 0x0}, {0x3, 0x2, 0x1, 0x0},
{0x6, 0x0, 0x4, 0x0}, {0x6, 0x5, 0x0, 0x20},
{0x6, 0x5, 0x4, 0x0},
};
volatile unsigned char stateR = 0;
int getRotary() {
unsigned char pinstate = (digitalRead(Rotary2) << 1) | digitalRead(Rotary1);
stateR = ttable[stateR & 0xf][pinstate];
unsigned char result = (stateR & 0x30);
if(result)
return (result == DIR_CCW ? -1 : 1);
return false;
}
Potentiometer
Das Potentiometer ist recht simpel. Hier wird einfach ein Wert zwischen 0 und 1023 zurückgeben - je nach dem wie weit er gedreht wurde. Anhand dessen kann man deutlich schneller zu hohen Zahlen kommen.
// Poti
int potPin = 2;
int stepSize = 0;
// zum Abfragen des Wertes einfach diesen Befehl aufrufen:
stepSize = analogRead(potPin);
stepSize
wäre z.B. der Wert 23 und dann zählt er immer 23 auf die aufzunehmenden Bilder dazu: 23, 64, 87, usw.
Die Menüsteuerung eines Arduinos
Es gibt viele Methoden ein Menü zu erstellen. Ich habe mir das ganze so überlegt:
Zu erst lade ich das Hauptmenü, dort gibt es die Auswahl zwischen “Setup Starten” und “Manueller Modus”. Im Manuellen Modus kann man einzelne Bilder aufnehmen und den Slider bewegen. Im Setup kann man die Zeitrafferaufnahme starten und alles Schritt für Schritt einstellen (Anzahl Bilder, Zeit Interval, Richtung, usw).
Die Grundfunktion
Fangen wir mit der Funktion an, die die einzelnen Menüs lädt. Das kommt in die void loop()
Funktion:
/*
* Die 2 Variablen werden zu Beginn festgelegt:
* 0 = Hauptmenü
* 1 = Setup
* 2 = Manuell
*/
int state = 0; // Aktueller Programmteil
int stateAlt = -1; // Alter Status
// unter die setup-Funktion:
int menuAkt = 0; // Aktueller MenüPunkt
int menu[] = {0,1,2}; // Mögliche Menüs
int menuSize = sizeof(menu) / sizeof(int);
void loop() {
if(state != stateAlt){ // Falls sich etwas ändert
stateAlt = state;
switch(state){
case 1: state = loadSetup(); break; // Setup Starten
case 2: state = loadManuell(); break; // Manueller Modus
default: state = loadMenu(); break; // Hauptmenü
}
}
delay(1000);
}
Das Hauptmenü
Im Prinzip sind alles einzelne Endlosschleifen, die mit dem Klick eines Buttons ausgeführt werden. Die Funktion für das Hauptmenü ist etwas komplizierter, da wir hier einen Pfeil rauf und runter bewegen der zeigt welchen Menüpunkt wir ausgewählt haben Die Funktion loadMenu()
wird im Schritt zuvor aufgerufen:
boolean menuTrigger = false;
int loadMenu(){
// 0: Setup starten
// 1: Manueller Modus
int menuAlt = 0;
changeMenu(0); // Updated das Menü und zeigt die neue Pfeil Position an
while(1){
int rotary = getRotary(); // Ruft ab ob der Encoder bewegt wurde
if(rotary){ // Falls ja, Menü neuladen -> Pfeilposition ändern
changeMenu(rotary);
}
if(pushed() && menuTrigger){ // Falls Button gedrückt wurde
menuAkt++;
return menuAkt; //Gibt die ID des neuen Menüs zurück
}
menuTrigger = true;
}
return 0;
}
void changeMenu(int s){
if(!s)
showMenu(0); // Einfach nur Menü anzeigen -> beim ersten Aufruf
else{
if(s < 0)
menuAkt--; // Linksrum gedreht
else
menuAkt++; // Rechtsrum gedreht
if(menuAkt > menuSize) // Falls Pfeil mehr als Anzahl der Menüpunkte, fängt er wieder von vorne an
menuAkt = 0;
else if(menuAkt < 0) // Falls kleiner 0, fängt er beim letzten Menüpunkt an
menuAkt = menuSize;
showMenu(menuAkt); //Neues Menü laden
}
}
/* Pfeil */
byte arrow[8] = {
B00000,
B11000,
B01100,
B00110,
B01100,
B11000,
B00000,
};
void showMenu(int s){
lcd.createChar(0, arrow); // Pfeil anzeigen
lcd.clear(); // LCD löschen
lcd.setCursor(2,0); lcd.print("Setup starten");
lcd.setCursor(2,1); lcd.print("Manueller Modus");
switch(s){
case 1: lcd.setCursor(0,1); lcd.write(byte(0)); break; //Pfeil anzeigen
default: lcd.setCursor(0,0); lcd.write(byte(0)); break; //Pfeil anzeigen
}
menuAkt = s;
}
Das Manuelle Menü
Wählt man jetzt das Manuelle Menü, wird die Funktion loadManuell
über die Grundfunktion aufgerufen. In loadManuell() soll man ein Foto auslösen und den Motor bewegen können. Mir fällt gerade auf, dass ich die Motorsteuerung und den Auslöser vielleicht hätte vorher zeigen sollen, aber dann kommt das halt danach.
boolean loadManuell(){
setRes(16); // Geschwindigkeit des Motors
digitalWrite(motorEnable,LOW); // Motor aktivieren
lcd.clear(); // LCD löschen
myTxt = String("Manueller Modus");
lcd.print(myTxt);
int stepSize = 10; // Schrittgröße des Motors
while(1){
rotary = getRotary();
stepSize = analogRead(potPin); // Motorgeschwindigkeit über das Potentiometer auslesen
if(stepSize < 5) // Kleiner 5 macht mein Motor nicht mit, daher mindestens 5 möglich
stepSize = 5;
if(rotary){ // Wenn Rotary Encoder gedreht wird, bewegt sich der Motor vor bzw. zurück
if(rotary > 0){
digitalWrite(motorDir,HIGH);
steps += stepSize;
}else{
digitalWrite(motorDir,LOW);
steps -= stepSize;
}
Move(stepSize);
float prozent = ((float) steps / float(maxSteps)) * 100;
myTxt = String("Distanz: ") + prozent + String("% "); // Zurückgelegt Entfernung
lcd.setCursor(0,1); lcd.print(myTxt);
}
if(pushed()){ // Wenn gedrückt wird, macht er ein Foto
Serial.println("Bild!");
delay(500);
shutter();
delay(500);
myTxt = String("# Fotos: ") + pictureCounter; // Anzahl der Fotos zählen
lcd.setCursor(0,2); lcd.print(myTxt);
}
}
digitalWrite(motorEnable,HIGH); // Motor deaktivieren
return 0;
}
Das war soweit der Code für das Menü. Die Funktion fürs Timelapse ist genauso aufgebaut. Ihr müsst lediglich in einzelnen Schritten die Anzahl der Bilder, das Zeitinterval und die Laufrichtung des Sliders abfragen. Ich habe noch eine “Kann es losgehen?”-Seite gemacht, damit nicht sofort gestartet wird.
Klickt man dann, lädt es die doTimelpase Funktion. Dort wird in einer while-Schleife geprüft ob die maxBilder Anzahl bereits erreicht ist. Solange das nicht ist, wird der Slider bewegt und ein Foto geschossen. Die Distanz die der Motor zurücklegt, müsst ihr natürlich immer berechnen. Also ob er 20 Bilder auf die ganze Distanz macht oder 200 Bilder, ist ja ein erheblicher Unterschied.
Das bekommt aber jeder selber hin, der bis jetzt noch durchblickt. Vorher schauen, wie viele Schritte der Motor braucht, um den ganzen Slider entlang zu fahren. Bei mir waren das 52.600 Stück. Sprich so viele Bilder könnte ich machen, und der Slider bewegt sich jedes mal dazwischen.
Auslösen einer Kamera über Arduino
Jetzt kommen wir zum coolsten Teil, das Auslösen der Kamera! Das ist super simpel und braucht nur ein paar Zeilen Code:
// Counter-Variable für die Bilder
int pictureCounter = 0;
short int shutterPin = 2; // PIN der Kamera
//In die Setup-Funktion:
void setup(){
pinMode (shutterPin, OUTPUT);
}
// Funktion zum fotografieren:
void shutter(){
digitalWrite(shutterPin, HIGH);
delay(50);
digitalWrite(shutterPin, LOW);
pictureCounter++; //Hochzählen des Counters
}
Das wars es auch schon. Sobald ihr jetzt die Funktion shutter()
aufruft, wird die Kamera ausgelöst.
Stepper-Motor steuern Arduino
Der letzte Teil ist die Steuerung des Motors. Das ist gar nicht so simpel und ich bin um ehrlich zu sein auch noch nicht zu 100% zufrieden. Aber der Zweck wird erfüllt und es funktioniert alles sehr gut.
Vorab ein bisschen Mathe: Schaut mal wie viel Grad pro Schritt euer Motor macht. Bei mir sind das 1,8. Jetzt rechnet ihr 360/1.8 = 200. Falls bei euch etwas anderes rauskommt, müsst ihr die 200 in meinem Code mit eurer Zahl ersetzen.
Wie gehabt, ein paar Variablen definieren und die Setup-Funktion:
const int resolution = 16; // bzw. 1/16tel
const long int maxSteps = 52600; // von Links nach Rechts 17 ganze Schritt á 200 Einzel Steps
int speed = 800;
long int steps = 0;
long int maxPics = 0;
long int maxDelay = 0;
short int motorEnable = 6; // HIGH = Motor deaktiviert | LOW = Motor aktiviert
short int motorStep = 5; // Step
short int motorDir = 4; // Dir | LOW hin zum Motor | High = Weg vom Motor
boolean motorDirection = HIGH; //LOW hin zum Motor | High = Weg vom Motor
short int motorM1 = 7; // \
short int motorM2 = 8; // -> Bestimmen die Revolution | M1,M2,M3 High = 1/16, M1,M2,M3 auf LOW = 1
short int motorM3 = 9; // /
void setup{
// Motor
pinMode(motorEnable,OUTPUT); // Enable
digitalWrite(motorEnable,HIGH); // Motor deaktivieren
pinMode(motorStep,OUTPUT);
pinMode(motorDir,OUTPUT);
pinMode(motorM1,OUTPUT);
pinMode(motorM2,OUTPUT);
pinMode(motorM3,OUTPUT);
setRes(16);
}
Resolution bestimmen
Hier wird bereits die erste Funktion, nämlich die setRes (für setResolution) aufgerufen. Die Resolution bestimmt das A4988-Board (also diese kleine Platine für den StepperMotor). Es gilt:
- MS1, MS2 und MS3 = LOW -> Full Step (also eine ganze Umdrehung)
- MS1 = HIGH, MS2 und MS3 = LOW -> Half Step (also eine halbeUmdrehung)
- MS1 = LOW, MS2 = HIGH, MS3 = LOW -> 1/4 Step (also eine viertel Umdrehung)
- MS1 & MS2 = HIGH und MS3 = LOW -> 1/8 Step (also eine achtel Umdrehung)
- MS1, MS2 und MS3 = HIGH -> Full Step (also eine 16tel Umdrehung) MS1-MS3 sind die Pins auf dem A4988 Board. Wenn also alle 3 auf HIGH stehen, sind die schritte nochmal viel genauer, als wenn sie auf LOW stehen. Ich habe nur alle auf LOW oder alle auf HIGH eingebaut. LOW nutze ich nur, wenn der Motor schnell zur Ausgangsposition fahren soll. HIGH sobald der Timelapse gestartet wurde. Natürlich macht es hier Sinn auch die anderen Resolutionen einzubauen, allerdings ist das dann deutlich aufwendiger, da man eine Logik einbauen müsste, wann welche Resolution genommen werden soll ;)
Der Code schaut also aus:
void setRes(int i){
if(i == 16){
digitalWrite(motorM1,HIGH);
digitalWrite(motorM2,HIGH);
digitalWrite(motorM3,HIGH);
}else{
digitalWrite(motorM1,LOW);
digitalWrite(motorM2,LOW);
digitalWrite(motorM3,LOW);
}
}
Die Move-Funktion
Die Bewegung des Motors ist wieder sehr simpel. Man übergibt die Anzahl der Schritt und der Motor bewegt sich mit Hilfe einer for-Schleife bis alle Schritte fertig sind:
void Move(int steps){
for(int i = 0;i < steps;i++){
digitalWrite(motorStep,HIGH);
delayMicroseconds(speed); // 1/2ms warten
digitalWrite(motorStep,LOW);
delayMicroseconds(speed); // 1/2ms warten
}
}
Aufruf der Move-Funktion
Standardmäßig ist der Motor deaktiviert. In jeder Funktion, in der der Motor sich bewegen soll (loadManuell()
und loadSetup()
bzw. timelapse()
), muss er erst aktiviert werden und die Resolution gesetzt werden:
loadManuell(){
....
setRes(16);
digitalWrite(motorEnable,LOW);
....
Move(stepSize);
....
}
Quelltext Arduino Kamera Slider
Wer sich nicht die Mühe machen möchte, um den restlichen Code selber zu schreiben, findet das vollständige Programm unter Github: https://github.com/jb-dev0/arduino-camera-slider
Wie gehts weiter
Ich denke, das gibt einen guten Überblick über die einzelnen Bestandteile des Programms und man sollte es gut schaffen die Steuerung selbst zu bauen. Gerne kann der Code auch bei mir erworben werden (s. oben).
Wer 2- oder 3- Achsen nutzen möchte, kann das ebenfalls in die Steuerung einbauen, das sollte genauso gut funktionieren. Cool wäre auch einen Start- und Endpunkt (+ ggf. Zwischenpunkte) festzulegen und die Steuerung errechnet sich darauf den ideal Pfad. Bisher ist es nämlich so, dass sie immer auf die ganze Länge des Sliders ausgelegt ist.