Servo-Steuerung einer Modellbahn-Weiche oder eines Signals mit Attiny

NEU: Version 1.1

Nachdem ich mit der Steuerung einer Bahnschranke bereits vor einiger Zeit in das Thema "Modellbahn" eingestiegen bin, habe ich mich diesmal mit der Steuerung einer Weiche (bzw. eines Signals) mittels eins Attiny45/85 und einem Servo beschäftigt. Entstanden dabei ist ein Testaufbau, wo durch Betätigung eines Tasters ein Servo zwischen zwei einstellbaren Endstellungen bewegt werden kann. Die Endstellungen können mittels Trimmer eingestellt werden und auch die Möglichkeit, dass die jeweils letzte angefahrene Endstellung gespeichert wird, habe ich vorgesehen.


Testaufbau:

Der Testaufbau besteht aus 2 Teilen: Der Steuerplatine, die im Nahbereich des Servo platziert wird und einer Bedienplatine, die im Bedienpult eingebaut wird und über ein vierpoliges Kabel mit der Steuerplatine verbunden ist. Die LEDs und der Taster auf der Bedienplatine sind mit den entsprechenden Bauelementen auf der Steuerplatine parallelgeschaltet.



Verwendete Bauteile:

Steuerplatine:

  • 1 Attiny45/85 / 8 MHz
  • 1 Analog-Servo
  • 2 Taster (Reset und Prog/Switch)
  • 2 Widerstände 220 Ohm
  • 1 Widerstand 10 kOhm
  • 1 LED rot 3 mm
  • 1 LED grün 3 mm
  • 1 Trimmpoti 10 kOhm
  • 1 Programmierstecker 4-pol
  • 1 Netzteil 5 VDC, 1A


Bedienplatine:

  • 1 Taster
  • 2 Widerstände 220 Ohm
  • 1 LED rot 5 mm
  • 1 LED grün 5 mm


Die parallelgeschalteten LEDs mit ihren Vorwiderständen müssen so gewählt werden, dass der Strom der einzelnen Attiny-Ausgänge maximal 40 mA beträgt! Ansonsten sind wie bei der Schaltung der Bahnschranke die LEDs z.B. über Transistoren anzusteuern.

Wie auch bei der genannten Schaltung habe ich hier den Steuerausgang PB3 zum Servo (Pin 2) mit einem 10 kOhm Pullup-Widerstand versehen. Damit soll die ruckartige Bewegung des Servos um einen Winkel von ca. 5-10 Grad beim Einschalten verhindert werden. Dieser Sprung tritt bei meinen Servos immer auf, wenn ich den Servo an die Versorgungsspannung lege, unabhängig davon, ob ein PWM-Signal anliegt oder nicht.


Programm:

Funktionen:

  • Bedienung einer Weiche oder eines Signals mittels eines Tasters im Arbeitsmodus (Laufrichtungsänderung des Servos auch während des Servolaufs möglich)
  • Anzeigen der Weichen- bzw. Signalstellung über LEDs auf Basis des ausgegebenen PWM-Signals
  • Positionierung der Servo-Endstellungen im Programmiermodus mit einem Trimmpoti und Speicherung im EEPROM des Attiny  1)
  • Möglichkeit der Speicherung der jeweils letzten Servostellung im EEPROM des Attiny
  • NEU in Version 1.1: Auf Zufallszahl basierende Verzögerung beim Anfahren der Startstellung nach Neuanlauf, wenn die Speicherung der letzten Servostellung deaktiviert ist  2)

(Programmier- und Arbeitsmodus siehe unter Programmablauf weiter unten!)


1) Beim Flashen des Attiny wird das EEPROM vollständig gelöscht und daher müssen die Endstellungen nach jedem Flashen neu eingestellt werden.

2) Wenn die Speicherung der letzten Servostellung bei einer größeren Anzahl von Weichen oder Signalen deaktiviert ist, würden nach dem Einschalten der Stromversorgung einer Anlage alle Servos gleichzeitig in die Startstellung fahren. Damit es dabei zu keiner Überlastung der Stromversorgung kommt (pro Servo können je nach Type 100 mA oder mehr auftreten) fährt jeder Servo erst nach einer - mittels Zufallszahl ermittelten - Verzögerung in die Startstellung. Die Verzögerungszeit kann dabei bis zu 20 Sekunden betragen und wird durch abwechselndes Blinken der grünen und roten LEDs angezeigt.


Mit folgenden Vorgaben kann das Verhalten der Steuerung im Programm eingestellt werden:

  • Vorgabe der Verstellgeschwindigkeit des Servos:

//1 ... langsam -> 128 ... schnell
#define pwmDelta 8

  • Vorgabe, ob jeweils die letzte Servostellung gespeichert werden soll:

//0 ... nicht speichern
//1 ... speichern
#define pwmStore 1

  • Vorgabe der Startstellung nach Reset oder bei Neustart, wenn das Speichern der letzten Servostellung deaktiviert ist:

//0 ... Stellung bei gruener LED
//1 ... Stellung bei roter LED
#define startStellung 0

  • Zur Begrenzung der möglichen Endstellungen des Servos können - abhängig vom eingesetzten Servo - die maximale und minimale Impulslänge des PWM Signals vorgegeben werden (Infos siehe dazu unter Servotest).

const int pwmTimeAbsolutMin = 800; //µs
const int pwmTimeAbsolutMax = 2200; //µs

  • Vorgabe der Speicherstelle im EEPROM (1 Byte), wo jeweils die letzte Endstellung gespeichert wird (siehe auch nachfolgende Info):

#define eepromLastPos 4

Info: Eine EEPROM-Speicherstelle kann laut Datenblatt des Attiny45/85 mindestens 100.000-mal neu beschrieben werden. Da immer nur die Endstellung bei einer Änderung neu gespeichert wird, sollte bei einer Modellbahn-Weiche oder bei einem Signal dieser Wert wohl nie erreicht werden. Sollte die EEPROM-Speicherstelle doch einmal defekt werden, kann man die Vorgabe von "eepromLastPos" von derzeit Speicherstelle 4 auf eine Speicherstelle größer 4 ändern. Die Speicherstellen 0 bis 3 enthalten die einstellbaren Servo-Endstellungen. Bei Verwendung eines Attiny45 stehen daher die Speicherstellen 5 bis 255 und beim Attiny85 die Speicherstellen 5 bis 511 zur Verfügung.


Programmablauf:

(In der nachfolgenden Beschreibung werden die jeweils parallelgeschalteten grünen und roten LEDs - der Einfachheit halber - in der Einzahl genannt)

Neuanlauf nach dem Flashen:

Jedes Mal beim Starten des Attiny nach dem Flashen, sind die im EEPROM des Attiny gespeicherten Endstellungen gelöscht und müssen neu programmiert werden. In diesem Fall geht die grüne LED in schnelles Dauerblinken über und es ist keine weitere Aktion möglich.

Programmiermodus:

Im nachfolgenden Ablaufdiagramm ist Programmierung der Servo-Endstellungen ersichtlich:


(1) Um die Endstellungen des Servo programmieren zu können, ist der Attiny im Programmier-Modus zu starten. Dazu ist beim Neustart des Attiny der Prog/Switch-Taster gedrückt zu halten. Die grüne LED blinkt daraufhin 5-mal langsam.

(2) Nach Beendigung des Blinkens kann mit dem Trimmer die Servostellung eingestellt werden, die der grünen LED entspricht (z.B. Weiche in Geradeaus-Stellung). Ist der Servo in der richtigen Stellung, muss die Übernahme der Stellung mit der Prog/Switch-Taste quittiert werden und die grüne LED blinkt zur Bestätigung dabei 2-mal schnell. Danach beginnt die rote LED 5-mal langsam zu blinken.

(3) Nun kann mit dem Trimmer die Servostellung eingestellt werden, die der roten LED entspricht  (z.B. Weiche in Abzweig-Stellung). Wiederrum muss die Übernahme der Stellung mit der Prog/Switch-Taste quittiert werden, die rote LED blinkt dabei 2-mal schnell und die Servo-Endstellungen werden im EEPROM des Attiny gespeichert.

(4) Der Programmiermodus ist damit beendet und der Attiny befindet sich nun im Arbeitsmodus. Um neuerlich in den Programmiermodus zu kommen, ist wiederum ein Neustart des Attiny bei gedrücktem Prog/Switch-Taster erforderlich.

Arbeitsmodus:

In Abhängigkeit der Programmvorgaben "Letzte Stellung speichern" und "Startstellung" (siehe oben unter "Vorgaben") verbleibt der Servo nach Beendigung des Programmiermodus oder beim Neustart des Attiny in der aktuellen Endstellung stehen (beim Neustart leuchten die LEDs zur Funktionskontrolle für 1 Sekunde auf) oder fährt - wie bereits unter "Programm-Funktionen" beschrieben - nach einer auf einer Zufallszahl basierten Verzögerungszeit in die vorgegebene Startstellung. Während der Verzögerungszeit blinken die rote und grüne LED abwechselnd.

Da im zweiten Fall die Steuerung ja nicht weiß, wo der Servo gerade steht, gibt die Steuerung zuerst ein PWM-Signal aus, das genau der Mitte zwischen den Endstellungen entspricht. Der Servo fährt dann sprunghaft in diese Mittelstellung und dann entsprechend der eingestellten Verstellgeschwindigkeit zur vorgegebenen Startstellung. Dieses sprunghafte Fahren in die Mittelstellung ist nicht zu verwechseln mit der bereits unter "Testaufbau" beschriebenen ruckartigen Bewegung, die beim Einschalten des Servos auftritt und für deren Unterdrückung der 10 kOhm Pullup-Widerstand eingebaut ist.

Wurde also bereits einmal eine Endstellung angefahren und ist die Vorgabe "Letzte Stellung speichern" auf ein, so bewegt sich der Servo nach dem Neustart nicht, sondern verbleibt in dieser letzten Stellung.

Bei jedem Drücken des Prog/Switch- oder des Switch-Tasters fährt der Servo jeweils in die andere Endstellung. Wird der Taster während des Fahrens ein weiteres Mal gedrückt, kehrt der Servo sofort wieder in die ursprüngliche Endstellung zurück. Die jeweilige Endstellung wird durch die entsprechende LED angezeigt. Während der Servo sich bewegt, blinken die LEDs wechselweise sehr schnell.


Verwendete Libraries:

sleep und EEPROM: Die verwendeten Libraries sind Bestandteil der Arduino-IDE und müssen deshalb nicht installiert werden


Hier nun das Programm in der Version 1.1:

Leider kann ich hier keine "ino"-Files hochladen, daher zum Verwenden des Programms ".txt" aus den beiden Dateinamen entfernen und in einem neuen Verzeichnis mit dem Namen "Weichen-_und_Signalsteuerung_V1.1" speichern.

Weichen-_und_Signalsteuerung_V1.1.ino.txt


Und hier zum schnellen ansehen:

//Steuerung fuer eine Modellbahn-Weiche oder ein Signal
//Code fuer Attiny45/85 / 8 MHz
//Author Retian
//Version 1.1


#include <EEPROM.h>
#include <avr/sleep.h>


//Prototypen:
void gotoSleep(void);
void progServo(void);
void flashLed(int, int, byte);


//Vorgabe der Startstellung (nach Reset oder Neustart)
//wenn "letzte Stellung speichern" deaktiviert ist
//0 ... Stellung bei gruener LED
//1 ... Stellung bei roter LED

#define startStellung 0


//Verstellgeschwindigkeit
//1 ... langsam -> 128 ... schnell
#define pwmDelta 8


//Letze Stellung speichern?
//0 ... nicht speichern
//1 ... speichern
#define pwmStore 1


//Speicherstelle (1 Byte) im EEPROM fuer Speicherung der letzten Position
#define eepromLastPos 4


//Minimale und maximale PWM-Impulslaenge

const int pwmTimeAbsolutMin = 800; //µs
const int pwmTimeAbsolutMax = 2200; //µs


#define led1Pin 2 //LED1 (gruen) auf PB2 (Pin 7)
#define led2Pin 0 //LED2 (rot) auf PB0   (Pin 5)
#define servoPin 3 //Servo auf PB3       (Pin 2)
//Pin 6 wird im setup() zuerst für randomSeed als Analogeingang verwendet
#define randomSeedPin 1
//und anschließend erst als Taster-Eingang mit internem PULLUP-Widerstand
#define key1Pin 1 //Taster1 auf PB1      (Pin 6)
#define potiPin A2 //Poti auf Pin ADC2   (Pin 3)


int pwmTime;
int pwmTimePos1;
int pwmTimePos2;

byte pwmLastPos;

byte randomMaxTime = 20; //Max. Verzögerung beim Anfahren der Startstellung in Sekunden

volatile bool oldStatusKey1;
volatile bool pwmDirectionFlag;


void setup() {
  bool eepromRead = true;
  bool posRead = true;
  randomSeed(analogRead(1));

  pinMode(key1Pin, INPUT_PULLUP);
  pinMode(servoPin, OUTPUT);
  digitalWrite(servoPin, 0);
  pinMode(led1Pin, OUTPUT);
  digitalWrite(led1Pin, LOW);
  pinMode(led2Pin, OUTPUT);
  digitalWrite(led2Pin, LOW);


  //Lese Grenzwerte aus dem EEPROM
  pwmTimePos1 = (EEPROM.read(0) << 8);
  pwmTimePos1 |= EEPROM.read(1);
  pwmTimePos2 = (EEPROM.read(2) << 8);
  pwmTimePos2 |= EEPROM.read(3);
  //Lese letzte Stellung aus dem EEPROM
  if (pwmStore)
  {
    pwmLastPos = EEPROM.read(eepromLastPos);
  }

  //Servo-Sicherheitsstellung, wenn nach dem Flashen
  //noch keine Werte im EEPROM gespeichert sind
  if ((unsigned int)pwmTimePos1 > 3000 || (unsigned int)pwmTimePos2 > 3000)
  {
    eepromRead = false;
  }

  if (pwmLastPos  > 1) posRead = false;


  //Setzen der Register fuer 20 ms Timerinterrupt
  cli(); // Loesche globales Interruptflag
  TCNT1 = 0; //Loesche Timer Counter 1
  TCCR1 = 0; //Loesche Timer Counter Controll Register
  OCR1C = 155; //Setze Output Compare Register C
  // Setze CS10, CS11 und CS13 - Clock Select Bit 10,11,13 (Prescaler 1024)
  TCCR1 |= (1 << CS10) | (1 << CS11) | (1 << CS13);
  //CTC-Mode ein
  TCCR1 |= (1 << CTC1); // CTC-Mode (Clear Timer and Compare)
  //Timer/Counter Interrupt Mask Register => => Timer Interrupt aktivieren spaeter
  //TIMSK |= (1 << OCIE1A); //Output Compare A Match Interrupt Enable


  //Setzen der Register fuer Pin-Change-Interrupt Pin PB1
  //Loeschen des Global Interrupt Enable Bits (I) im Status Register (SREG)
  SREG &= 0x7F; //entspricht "cli();"
  //Setze des Pin Change Interrupt Enable Bit
  GIMSK |= (1 << PCIE);
  //Setzen des Pin Change Enable Mask Bit 1 (PCINT1)  ==> Pin PB1
  PCMSK |= (1 << PCINT1);

  //Setzen des Global Interrupt Enable Bits (I) im Status Register (SREG)
  SREG |= 0x80; //entspricht "sei();"


  oldStatusKey1 = digitalRead(key1Pin);

  //Wenn Taster1 gedrueckt, programmiere Servostellungen
  if (!digitalRead(key1Pin)) progServo();
  //sonst, starte Servosteuerung, wenn Servostellungen im EEPROM vorhanden oder
  //Dauerblinken, wenn keine Servostellungen im EEPROM vorhanden
  else
  {
    if (eepromRead)
    {

      //Letzte Stellung speichern ist aktiviert und ausgelesen
      if (pwmStore && posRead)
      {
        if (pwmLastPos == 0) pwmTime = pwmTimePos1;
        else if (pwmLastPos == 1) pwmTime = pwmTimePos2;
        pwmDirectionFlag = pwmLastPos;

        digitalWrite(led1Pin, HIGH);
        digitalWrite(led2Pin, HIGH);
        delay(1000);
        digitalWrite(led1Pin, LOW);
        digitalWrite(led2Pin, LOW);
      }

      //Letzte Stellung speichern ist deaktiviert -> Startstellung
      else
      {
        pwmTime = (pwmTimePos1 + pwmTimePos2) / 2;
        pwmDirectionFlag = startStellung;

        int startDelay = random(0, randomMaxTime);
        for (byte i = 0; i < startDelay; i++)
        {
          flashLed(500, 0, led2Pin);
          flashLed(500, 0, led1Pin);
        }
      }


      //Timer Interrupt aktivieren
      //Timer/Counter Interrupt Mask Register
      TIMSK |= (1 << OCIE1A); //Output Compare A Match Interrupt Enable
    }
    else if (!eepromRead)
    {
      //LED1 blinkt, wenn keine Werte im EEPROM gespeichert sind
      //und Programm endet hier
      while (1) flashLed(50, 100, led1Pin);
    }
  }
}


void loop() {
  if (pwmDirectionFlag)
  {
    if (pwmTime < pwmTimePos2) pwmTime += pwmDelta;
    else if (pwmTime > pwmTimePos2) pwmTime -= pwmDelta;
    delay(20);
  }
  else if (!pwmDirectionFlag)
  {
    if (pwmTime > pwmTimePos1) pwmTime -= pwmDelta;
    else if (pwmTime < pwmTimePos1) pwmTime += pwmDelta;
    delay(20);
  }

  if (pwmTimePos1 < pwmTimePos2)
  {
    if (pwmTime <= pwmTimePos1)
    {
      digitalWrite(led1Pin, HIGH);
      digitalWrite(led2Pin, LOW);
    }
    else if (pwmTime >= pwmTimePos2)
    {
      digitalWrite(led1Pin, LOW);
      digitalWrite(led2Pin, HIGH);
    }
    else if (pwmTime > pwmTimePos1 + pwmDelta && pwmTime < pwmTimePos2 - pwmDelta)
    {
      digitalWrite(led1Pin, !digitalRead(led1Pin));
      digitalWrite(led2Pin, !digitalRead(led1Pin));
    }

    if (pwmTime >= pwmTimePos2 || pwmTime <= pwmTimePos1)
    {
      if (digitalRead(led1Pin)) EEPROM.update((int) eepromLastPos, 0);
      else if (digitalRead(led2Pin)) EEPROM.update((int) eepromLastPos, 1);
      delay(100);
      gotoSleep();
    }
  }

  if (pwmTimePos1 > pwmTimePos2)
  {
    if (pwmTime >= pwmTimePos1)
    {
      digitalWrite(led1Pin, HIGH);
      digitalWrite(led2Pin, LOW);
    }
    else if (pwmTime <= pwmTimePos2)
    {
      digitalWrite(led1Pin, LOW);
      digitalWrite(led2Pin, HIGH);
    }
    else if (pwmTime < pwmTimePos1 - pwmDelta && pwmTime > pwmTimePos2 + pwmDelta)
    {
      digitalWrite(led1Pin, !digitalRead(led1Pin));
      digitalWrite(led2Pin, !digitalRead(led1Pin));
    }

    if (pwmTime <= pwmTimePos2 || pwmTime >= pwmTimePos1)
    {
      if (digitalRead(led1Pin)) EEPROM.update((int) eepromLastPos, 0);
      else if (digitalRead(led2Pin)) EEPROM.update((int) eepromLastPos, 1);
      delay(100);
      gotoSleep();
    }
  }
}


//Attiny in den Schlafmodus setzen
void gotoSleep()
{
  byte adcsra;

  adcsra = ADCSRA; //ADC Control and Status Register A sichern
  ADCSRA &= ~_BV(ADEN); //ADC ausschalten

  MCUCR |= (1 << SM1) & ~(1 << SM0); //Sleep Modus = Power Down
  MCUCR |= _BV(SE); //Sleep Enable setzen
  sleep_cpu(); //Schlafe ....
  MCUCR &= ~(1 << SE); //Sleep Disable setzen

  ADCSRA = adcsra; //ADCSRA-Register rueckspeichern
}


//Interrupt Serviceroutine fuer Pin-Change-Interrupt
ISR(PCINT0_vect)
{
  //Schnelles Einlesen von Taster1 durch Registerabfrage
  bool statusKey1 = (PINB & (1 << PINB1)) >> PINB1;

  //Steigende Flanke
  if (!oldStatusKey1 && statusKey1)
  {
    oldStatusKey1 = true;
  }
  //Fallende Flanke
  else if (oldStatusKey1 && !statusKey1)
  {
    oldStatusKey1 = false;
    pwmDirectionFlag = !pwmDirectionFlag;
  }
  delayMicroseconds(5000);
}


//Interrupt-Serviceroutine fuer Timer-Interrupt
ISR(TIMER1_COMPA_vect)
{
  //Setzen des Servopins durch Registermanipulation (siehe: Ein-Ausgangsports)
  PORTB |= (1 << PORTB3);
  delayMicroseconds(pwmTime);
  PORTB &= ~(1 << PORTB3);
}


void progServo()
{
  int potiWert;

  //Pin Change Interrupt sperren
  GIMSK &= ~(1 << PCIE);

  for (byte i = 0; i < 5; i++) flashLed(300, 300, led1Pin);
  //Timer Interrupt aktivieren
  //Timer/Counter Interrupt Mask Register
  TIMSK |= (1 << OCIE1A); //Output Compare A Match Interrupt Enable
  do
  {
    potiWert = analogRead(potiPin);
    pwmTime = map(potiWert, 0, 1023, pwmTimeAbsolutMin, pwmTimeAbsolutMax);
    delay(20);
  }
  while (digitalRead(key1Pin));
  pwmTimePos1 = pwmTime;
  for (byte i = 0; i < 2; i++) flashLed(100, 100, led1Pin);

  for (byte i = 0; i < 5; i++) flashLed(300, 300, led2Pin);
  do
  {
    potiWert = analogRead(potiPin);
    pwmTime = map(potiWert, 0, 1023, pwmTimeAbsolutMin, pwmTimeAbsolutMax);
    delay(20);
  }
  while (digitalRead(key1Pin));
  pwmTimePos2 = pwmTime;
  for (byte i = 0; i < 2; i++) flashLed(100, 100, led2Pin);

  //Speichere Werte im EEPROM
  EEPROM.update((int) 0, highByte(pwmTimePos1));
  EEPROM.update((int) 1, lowByte(pwmTimePos1));
  EEPROM.update((int) 2, highByte(pwmTimePos2));
  EEPROM.update((int) 3, lowByte(pwmTimePos2));

  //Pin Change Interrupt starten
  GIMSK |= (1 << PCIE);
}


void flashLed(int leuchtZeit, int dunkelZeit, byte ledPin)
{
  PORTB |= (1 << ledPin);
  delay(leuchtZeit);
  PORTB &= ~(1 << ledPin);
  delay(dunkelZeit);
}


Zurück zum Servotest Attiny