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:

  1. Display
  2. Steuerung des Rotary-Encoders (=Dreh-Druck-Knopf) & Potentiometer
  3. Menü
  4. Auslösen der Kamera
  5. 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);
}

Coding-Tipps

Einmal pro Woche schicke ich dir einen Coding-Tipp per E-Mail. In kürzer (~2 Minuten Lesezeit) stelle ich dir ein Tool vor oder zeige dir einen Coding-Hack. Spannend für jeden Programmierer!

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.

Timelaps aus Schottland:

youtube
Hinweis: Ein Klick auf den Play-Button lädt ein YouTube-Video. Dabei werden personenbezogene Daten an Youtube und in die USA übermittelt. Es gelten die Datenschutzbestimmungen von YouTube.

Timelaps aus dem Garten:

youtube
Hinweis: Ein Klick auf den Play-Button lädt ein YouTube-Video. Dabei werden personenbezogene Daten an Youtube und in die USA übermittelt. Es gelten die Datenschutzbestimmungen von YouTube.