Glohbe - Hansis Blog

Programmierung Kamera Slider mit dem Arduino

veröffentlicht am 10.02.2017 - 09:49 Uhr Uhr - letzte Änderung 10.02.2017 - 13:05 Uhr Uhr Tags: Fotografie Hobby Arduino Programmierung DIY

Im Dritten Teil des Kamera Sliders auf Basis eines Arduino geht es nun endlich um die Programmierung. Mein Quelltext nicht sonderlich lang (nur knapp 500 Zeilen), dafür erfüllt es eigentlich jeden Wunsch :)

Programmierung Kamera Slider

Fertiges Programm kaufen

Da die meisten nicht die Zeit und Motivation haben, sich in die Programmierung einzuarbeiten und es anfangs sehr frustrierend ist bis man ein lauffähiges Programm geschrieben hat, biete ich den Arduino Sketch zum Download an. Den Link maile ich euch zu. Bitte vorab einen Betrag eurer Wahl auf mein Paypal Konto überweisen: https://www.paypal.me/dahansi

In Dieser Anleitung gebe ich nur wieder Denkanstöße und verweise auch Teilweise auf die Programmierung der Brausteuerung, da vieles analog läuft.

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. Das geht genauso wie auch bei der Brausteuerung: Arduino LCD-Display ansteuern

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-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);
  ....
}

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.

Ergebnis aus dem Garten - mit Bewegungsmeldern und so -.-

Kommentare