Schlechter Geschmack. Es ist schon mühselig, auf seinen Teebeutel zu achten. Fast immer, wenn ich einen Tee koche, kommt etwas dazwischen und der Tee gerät in Vergessenheit. Glücklicherweise besitze ich eine Tasse mit meinem Namen darauf. Irgendwann kommt dann ein Kollege und erklärt stolz, meine Tasse gefunden zu haben. Der Tee ist hat mittlerweile sehr lange gezogen, ist kalt geworden und man kann ihn nicht mehr trinken. Da muss eine Automatik her!

Im Blog von Brian McEvoy habe ich dann dieses Gerät gefunden:

Tea Maker

Hier mein Nachbau:

In­halts­ver­zeich­nis

1. Der Plan
2. Dipper-Arm
3. Steuerteil
3.A Schaltung
3.B Gehäuse
4. Software
4.A Ressourcenverwendung
4.B Tastersteuerung
4.C Zeitgeber
4.D Sieben-Segment-Display-Steuerung
4.E Tonausgabe
4.F Servo-Steuerung
5. Download

1. Der Plan

Es muss ein automatisierter Tee-Dipper her! Ein Gestell aus dem 3D-Drucker, ein Servo zum Bewegen eines Hebels, an dessen Ende eine Klammer zum Anheften des Teebeutels. Dazu kommen ein paar Tasten zum Einschalten und Festlegen der Zieh-Dauer, eine Anzeige der Restzeit und ein Tongeber, der nach Ablauf der vorgegebenen Zeit Alarm schlägt. Das Ganze im Batterie-Betrieb. Und ein Mikrocontroller, der das alles steuert.

Software-Features:

Optimierung

Batterie-Spannung durch Restlaufzeit oder Prozent-Angabe ersetzen.


2. Dipper-Arm

Die 3D-Druck-Vorlagen für die Dipper-Mechanik habe ich der Vorbild ohne Modifikationen entnommen. Hier sind die Duplikate der Dateien:

Teedipper 3D Mechanik OpenScad-Dateien Arn in Betrieb

Zum Bewegen des Arms habe ich einen Servo vom Typ SG90 9 g Micro Servo benutzt. Zu den Spezifikationen gibt es bei den verschiedenen Quellen leicht unterschiedliche Angaben.

Spezifiaktion Wert Abbildung
Betriebsspannung 4,8 - 6 V TowerPro SG90 - Micro Servo
Modulation Analog
Drehmonent 4,8V: 1,80 kg cm
Winkelgeschwindigkeit 4,8V: 60° / 0.10 sec
Gewicht 9.0 g
L/B/H 23,1 mm / 12,2 mm / 29,0 mm
Drehbereich 180 °
Steuerpuls-Zyklus 20 ms
Steuerpulsdauer 500 - 2400 µs
Datenblatt SG90 9 g Micro Servo

Optimierung

Pads Der Aufsatz für die Tasse ist nicht variabel. Hat die Tasse einen zu kleinen Umfang, wackelt das Ganze.
Abhilfe schaffen z.B. kleine Filz-Pads, die man auf die Halter klebt. Besser wäre natürlich irgendeine Federmechanik, die den Halter automatisch an den Umfang bzw. Durchmesser der Tasse anpasst.

3. Steuerteil

3.A Schaltung

Zweistöckiger Aufbau Chips mit vielen Pins anzuschließen ist auf einseitig gedruckten Schaltungen nicht einfach. Bei Zweiseitigen Platinen ist das Durchkontaktieren aufwändig. Aus diesem Grund habe ich einen zweistöckigen Aufbau gewählt. Auf der Oberseite sind die Bedienelemente angebracht, deren Anschlüsse zu Pfostenstecker am Platinenrand herausgeführt sind. Hier verlaufen die Leiterbahnen vorwiegend horizontal. Auf der unteren Platine ist der ist der µC, weitere Bauteile und vor allen Dingen Buchsen zur Aufnahme des Oberteils untergebracht.

Die Verdrahtung wurde so gewählt, dass die Führung der Leiterbahnen möglichst einfach ist. Es wurde z.B. darauf verzichtet, die einzelnen Segmente des Sieben-Segment-Displays an einen gemeinsamen Port zu legen, damit z.B. alle Segmente gleichzeitig mit einer Anweisung angesprochen werden können. Die Konsequenzen müssen durch die zugehörige Software ausglichen werden. Beim angeführten Beispiel heißt dies, dass jedes Segment einzeln angesteuert werden muss (s.u. Abschnitt Software).

Steuereinheit

Steuereinheit

Auf der Hauptplatine ist hauptsächlich ein ATmega48 als Steuereinheit untergebracht.

Ein Piezosignalgeber dient zum Abspielen der Schluss-Fanfaren. Das Teil war einmal der Pieps-Ton-Generator eines PCs. Ich habe ihn aus einem alten Computer ausgelötet.

Ein kleiner FET sorgt dafür, dass der Servo im Ruhezustand von der Stromversorgung getrennt werden kann, damit er keinen Strom verbraucht.

Die Batteriespannung (nominal 6V) wird durch eine Diode auf ungefährliche 5,3 V zur Versorgung des µC herabgesetzt. Der Servo erhält die volle Batteriespannung (s.u.).

Die weiteren Bauteile sind Stecker zum Anschluss des Bedienteils, des Servo und eines Programmier-Adapters.

Sieben-Segment-Anzeige

Display-Einheit Die Sieben-Segment-Anzeige stammt von einem Billig-Volt-Meter aus China. Ich hatte aktuell nichts Besseres zur Hand. Es gibt keine Vorwiderstände zur Strombegrenzung für die LEDs. Die Software sogt dafür, dass zu einer Zeit immer nur eine LED brennt und das nur für kurze Zeit. Da reicht die interne Strombegrenzung des ATmega um die LEDs nicht zu zerstören.

Treiber-Transistoren für die gemeinsame Anode sind ebenfalls nicht notwendig, da der Strombedarf für eine einzelne LED vom Ausgangstreiber des ATmega gedeckt werden kann.
    Digitales Volt-Meter aus China

Eingabe-Tasten

Eingabe-Tasten Für die Eingabe-Tasten habe ich farbige quadratische Tasten von Adafruit (Artikel 1010) gewählt. 15 Stück kosten nur etwa 6,- € und sie sehen gut aus. Die Tasten sind an die µC-Pins mit einem hohen Pull-Up-Widerstand gegen VCC angeschlossen. Beim Drücken ziehen sie den Pin auf Masse.
       Farbige quadratische Taster

Stromversorgung

Das Ganze soll im Batterie-Betrieb laufen. Vier AA-Zellen liefern nominal 6 Volt. Das ist für einen ATmega zu viel! Ein ATmega verträgt bis zu 5,5 Volt, wobei anzunehmen ist, dass er bei einer kleinen Überspannung auch nicht sofort den Geist aufgibt. Eine in Durchlassrichtung geschaltete Diode reduziert die Versorgungsspannung um ca. 0,7 Volt auf etwa 5,3 Volt.

Der Chip wird mit 8 MHz betrieben und funktioniert bei dieser Frequenz bis herunter zu etwa 3 Volt, je nach Typ auch noch weniger. Zieht man die 0,7 Volt Diodenspannung ab, kann die Batterie-Spannung bis auf etwa 3,7 Volt sinken. Bei dieser Spannung sind die Batterien im auch Wesentlichen leer und müssen getauscht werden.

Von Seiten der Stromversorgung für den µC scheint also alles in Ordnung zu sein. Mal schauen, wie weit das Sieben-Segment-Display und der Signalgeber mitmachen. Z.Zt. beträgt die Batteriespannung etwa 5,6 Volt und es klappt noch alles einwandfrei.

Um den Servo, insbesondere bei leeren Batterien, mit möglichst hoher Spannung zu versorgen, ist er direkt an die Batterien angeschlossen (nur. über einen FET mit geringem, Durchlasswiderstand). Wenn die Spannung soweit wie oben beschrieben herunter gefallen ist, wird der Servo-Motor wahrscheinlich nicht mehr ausreichend Kraft haben, um den Teebeutel zu heben. Ich denke, dass der Servo-Motor der begrenzende Faktor ist.

Damit die Batterien möglichst lange halten, ist es wichtig, auf den Stromverbrauch zu achten. Während des Betriebs, wird das Sieben-Segment-Display im Pulsmodus betrieben. Hier ist es ggf. möglich die Tastgrad  zu reduzieren. Damit verringert sich allerdings die Leuchtstärke.

Noch wichtiger ist es, den Stromverbrauch im Ruhezustand zu reduzieren. Der Teedipper besitzt keinen Ein/Aus-Schalter, ist also immer an der Batterie angeschlossen. Den ATmega kann man in den Sleep-Zustand versetzen, dann verbraucht er nur noch etwa 0,6 µA. Das ist ausreichend wenig. Der Servo verbraucht jedoch knapp 4 mA, wenn er nicht angesteuert wird. Geht man von einer mittleren Batterie-Kapazität bei AA-Zellen von etwa 1.600 mAh aus (im Netz schwirren Werte zwischen 800 und 2300 mAh herum), sind die Batterien nach 400 Stunden nur(!) Leerlauf, d.h. rd. 2  Wochen, leer.

Ein FET in der Versorgungsleitung des Servo ermöglicht es, den Servo bei Nichtbenutzung von der Stromversorgung zu trennen. Mein Dipper liegt jetzt etwas mehr als eine Wochen unbenutzt herum. Lediglich die Spannung habe ich hin und wieder ausgelesen. Sie beträgt die ganze Zeit konstant 5,6 Volt.

Download

Die Eagle-Dateien zum Download:   Eagle-Dateien zum Download

Optimierung

Um die Frontplatte zu drucken waren eine Reihe von Versuchen notwendig. Bei der Positionierung der Bauteile, die in Verbindung zu Gehäuse-Elementen stehen, z.B. Frontplatten-Ausschnitte benötigen oder Bohrlöcher, sollte man die Positionierung sehr auf der Platine sorgfältig vornehmen, z.B. durch explizite Angabe der Positionen. Dann könnte man bei bekannten Bauteil-Abmessungen die Frontplatten-Ausschnitte berechnen und müsste sie nicht ausmessen.

 

3.B Gehäuse

Das Gehäuse ist auf einem 3D-Drucker entstanden. Es besteht aus vier Komponenten. Die Schnitte wurden so gewählt, dass keine Überhänge entstanden:

Frontplatte

Die Frontplatte ist im 3D-Drucker entstanden. Die Beschriftung wurde mit PowerPoint entworfen, gedruckt und aufgeklebt.

Frontplatte OpenScad-Vorlage Frontplatte OpenScad-Vorlage Rückseite Frontplatte Rückseite Frontplatte beschriftet Beschriftungsvorlage
Frontplatte OpenScad-Vorlage Frontplatte OpenScad-Vorlage Rückseite Frontplatte Rückseite Frontplatte beschriftet Beschriftungsvorlage

Leider hatte ich bei Herstellung der Platine nicht an die Herstellung der Frontplatte gedacht. Die Position und Abmessung der einzelnen Elemente habe ich ausmessen müssen. Dazu habe ich eine dünne Scheibe (0,8 mm) gedruckt, geschaut wo es klemmt und die Daten angepasst, bis alles am richtigen Platz war.

Die dünnen Fahnen am Rand der Platte verhindern das Verziehen des Objekts beim Druck auf Grund von unterschiedlichen Schrumpfungsraten des Objekts (Warping). Die Fahnen besitzen eine Sollbruchstelle und können einfach abgeschnitten werden.

Zunächst habe ich versucht, die Beschriftung in die Oberfläche einzulassen. Das sah aber ziemlich unordentlich aus. Die notwendige Auflösung war für meinen 3D-Drucker einfach zu hoch. Also habe ich ein anderes Verfahren gewählt.

Die Beschriftung wurde mit PowerPoint gezeichnet. Dazu habe ich vier farblich unterschiedliche Varianten entworfen (s. Abb. ganz rechts). Diese habe ich mit einem Farb-Laser-Drucker im Copy-Shop drucken lassen. Der Druck mit einem Laser-Drucker hat den Vorteil, dass die Farbe (der Toner) nicht feuchtigkeitsempfindlich ist. Außerdem sind die Druckergebnisse besser als bei einem Tintenstrahldrucker. Das Papier war Standardpapier (80 g/m²). Allerdings war die Oberfläche sehr glatt. Letztendlich habe ich mich für die hellblaue variante entschieden.

Die Vorlage habe ich grob ausgeschnitten und mit einem Laminier-Gerät versiegelt. Die Folie habe ich dann auf die Frontplatte gelegt und die Ränder markiert. Diese Markierungen sind notwendig, weil man beim späteren Aufkleben nur einen Versuch hat. Ein nachträgliches Verschieben ist nicht möglich.

Die Frontplatte habe ich mit Sprühkleber eingesprüht. Leider hatte ich nur Sprühkleber vom Type "non-permanent" zur Verfügung. Hier kommt man weiter, wenn man beide Teile einsprüht und den Kleber vor dem Zusammenfügen der Teile ablüften lässt. Dann muss die Frontplatte passgerecht auf die Folie aufgesetzt (vorher Markierungen abringen, s.o.) und mit einem weichen Tuch kräftig angedrückt werden.

Im nächsten Schritt wurden die Ränder der Folie mit einem Skalpell passend ab- und die Löcher für die Bedienelemente ausgeschnitten. Durch das Beschneiden liegen die Papierkanten offen. Diese Kanten sind zwar sehr schmal, aber Flüssigkeiten treten auf Grund des Kapillar-Effekts dennoch ein. Das gibt hässliche Flecken! Um die Kanten zu versiegeln habe ich die Oberfläche an den Rändern (Außenränder und Ausschnitte) mit Tesafilm so abgeklebt, dass ein Teil des Streifens auf der Folie klebt und der andere in der Luft hängt. Die Ecken müssen fest angedrückt werden, damit der Tesafilm dicht auf der Oberfläche der Folie liegt. In die so entstandene Kehle wird Sekundenkleber gegeben (s. folgende Grafik). Das Tesafilm schützt die Folienoberfläche, der Sekundenkleber kann nicht auf die Oberseite gelangen. Der Sekundenkleber versiegelt die offen liegende Papierkante uns verbessert die Verklebung der Folie mit der Frontplatte. Insbesondere an den Rändern ist eine gute Verklebung wichtig. Der Klebefilmkann wegen der geringen Viskosität dünn aufgetragen werden.

Schichtung von oben Schichtung von oben

Nach dem Abtrocken des Klebers wird der Tesafilm wieder abgezogen. Die entstanden Grate des Sekundenklebers lassen sich mit einer harten Kante (Rückseite eines Messers) abkratzen. Schneiden verletzt die Kleberschicht evtl. so stark, dass die Versiegelung unterbrochen wird.

Ich habe versehentlich beide Seiten des Blattes laminiert. Eigentlich wollte ich dies nur bei der Vorderseite machen. Dann wäre auf der Rückseite das nackte Papier geblieben. Zum späteren Aufkleben wäre dies wahrscheinlich besser geworden.

Hinweis: Die Frontplatte erst dann mit dem Rahmen verkleben, wenn die Bodenplatte montiert ist (s.u.)!

Rahmen

Das Gehäuse besteht aus zwei Teilen, die aufeinander geklebt wurden. Die Klebeflächen sind auf der jeweiligen Unterseite der Grafiken.

Rahmen Oberteil Rahmen Batterie-Box Zusammengeklebter Rahmen
Rahmen Oberteil
Platinen
Rahmen Unterteil
Batterie
Gedruckter und
verklebter Rahmen

Das Oberteil besitzt einen Einlassrand, in den den die Frontplatte eingesetzt und festgeklebt wird. Der Rahmenhöhe ist durch die Höhe der Platinen bestimmt.

Das Unterteil besitzt ebenfalls einen Einlassrand für die Bodenplatte. Es bietet Raum für den Batterie-Halter. An den vier Ecken sind viereckige Bolzen mit Löchern zur Aufnahme von Schrauben mit selbstschneidendem oder selbstprägendem Gewinde (Blechschrauben, o.ä.) ausgebildet (Nachtrag: hat nicht gut funktioniert, s. Abschnitt Optimierung). Ein Schlitz an der Seite dient als Durchlass für das Servo-Kabel.

Hinweis: Die Frontplatte erst dann mit dem Rahmen verkleben, wenn die Bodenplatte montiert ist (s.u.)!

Bodenplatte

Bodenplatte montiert Batterie-Halter Bodenplatte 3D-Teile
Bodenplatte
montiert
Batterie-Halter Bodenplatte 3D-Teile
Sicht von unten

Die Bodenplatte besteht aus einer Platte mit versenkten Schraublöchern. Diese Plate ist zu dünn um Schrauben aufnehmen zu können. Deshalb gibt es zwei Distanzstücke für den Batterie-Halter. Am besten schraubt zunächst man die Distanzstücke soweit an den Batterie-Halter an, dass die Schrauben eben noch nicht hindurch treten. Diese Konstruktion klebt man auf die Bodenplatte. Das Kleben sollte mit aufgestecktem Batterie-Clip erfolgen. Ebenfalls sollte die Bodenplatte im Rahmen eingelassen sein. So kann man den Batterie-Halter passend positionieren.

Montiert

Frontpanel montiert Komplett montiert
Frontpanel
montiert
Komplett
montiert

Download

Die 3D-Druck-Dateien zum Download:   OpenScad-Dateien

Optimierung

Ich habe beide Seiten der Papierauflage für die Frontplatte laminiert. Eigentlich wollte ich dies nur bei der Vorderseite machen. Dann wäre auf der Rückseite das nackte Papier geblieben. Zur Haftung nach dem Aufkleben wäre dies wahrscheinlich besser gewesen. Folien lassen sich je nach Material ggf. schlecht kleben.

Die Frontplatte schließt bündig mit der Oberseite des Gehäuses ab. Sie steht sogar auf Grund der Ungenauigkeiten beim Druck ein wenig hervor und das aufgelegte laminierte Papier verstärkt dies. Es sähe wahrscheinlich besser aus, wenn die Platte ein wenig eingelassen wäre. Außerdem wäre die Haltbarkeit besser, weil die Außenkante des Papiers und durch das hervorstehende Gehäuse geschützt wäre.

Die Bodenplatte musste auf Grund der Ungenauigkeiten beim Druck an den Rändern stark abgeschliffen werden, damit es in die Einlassung des Rahmen-Unterteils passte. Es sollte etwas kleiner gemacht werden.

Probleme gab es mit den Befestigungsschrauben für die Bodenplatte. Der Rahmen wurde mit 2 Shells und geringem Infill-Grad gedruckt. Beim Eindrehen der Schraube hat sich der Zylinder zur Aufnahme der Schraube aus dem Infill heraus gedreht. Entweder man benutzt reguläre Gewindeschrauben (z.B. M3) und sieht Aufnahmen für die Muttern vor oder man druckt die Aufnahme-Blöcke separat und wesentlich stabiler und klebt sie anschließend in den Rahmen ein. Den gesamten Rahmen verstärkt zu drucken ist nicht sinnvoll.


4. Software

Bei der Erstellung der Software habe ich mir Mühe gegeben, möglichst strukturiert vorzugehen. Für jede Funktionseinheit gibt es ein eigenes Modul:

Modul Funktion Anmerkung
Main.c Hauptprogramm, Initialisierung und Gesamtsteuerung
Key (.c, .h) Interruptgetriebene Kontrolle der Tasten  
Melody (.c, .h) Interruptgetriebene Erzeugung der Ende-Fanfare siehe RTTTL: Melodien mit einem AVR ♫♬♯♪
Servo (.c, .h) Interruptgetriebene Servo-Steuerung  
SevenSeg (.c, .h) Interruptgetriebene Steuerung des Sieben-Segment-Displays  
Time (.c, .h) Interruptgetriebener Zeitgeber für die Dipp-Zeit  
ADC (.c, .h, .cfg) Ansteuerung des ADC siehe ADC: Mehr als 0 und 1
AppVersion.h Verwaltung der Versionsnummer siehe Build-Nummer: Immer Eins drauf!
GeneralDefinitions.h Allgemeine Hilfs-Makros siehe Bibliothek: Kein zweites Mal!
Ressources.h Festlegung der benutzten Prozessor-Ressourcen an zentraler Stelle  

4.A Ressourcenverwendung

Verwendung der Timer

  Timer0: LED-Steuerung
  Timer1: Servo-Steuerung
  Timer2: Zeitmessung für die Dipp-Zeit & Ton-Erzeugung

Zeitmessung und Ton-Erzeugung können mit dem gleichen Timer erfolgen, da die Ende-Fanfare erst nach Abschluss des Dippens ausgegeben wird.

Zuteilung der IO-Ports

Die Verbindung der Peripherie-Bauteil mit dem µC erfolgte so, dass ein möglichst unkomplizierter Leiterbahnverlauf erreicht wurde. Demzufolge sind verwandte Funktionen (wie z.B. die Segment-Ansteuerung des Displays) nicht auf einem Port angebracht. Das hat zur Folge, dass jeder Pin einzeln angesteuert werden muss. Dies führt zu etwas größeren Programmen und zu längeren Laufzeiten. Beides ist kein Problem und z.B. beim Arduino Standard.

Ansonsten muss bedacht werden, dass

Exemplarisch seien hier die Definitionen für die Taster angeführt. Die weiteren Definitionen befinden sich in der Datei Ressources.h.

// Taster
#define Button1Port PORTB
#define Button2Port PORTB
#define Button3Port PORTD
#define Button1Pin  PB4 // PCINT4
#define Button2Pin  PB5 // PCINT5
#define Button3Pin  PD5 // PCINT21

4.B Tastersteuerung

/*
 * Key.h
 *
 * Projekt: Teedipper
 * Created: 15.08.2015
 *  Author: Ulli
 */

#ifndef KEY_H_
#define KEY_H_

void KeyInit(void);

extern volatile uint8_t ButtonState;
// Masken für die einzelnen Taster
#define Button1       1 // 3 Minuten
#define Button2       2 // 5 Minuten
#define Button3       4 // 7 Minuten
#define ButtonStp (1+2) // Stopp
#define ButtonTst (2+4) // Batterie-Test

#endif /* KEY_H_ */

Das Taster-API stellt die Funktion KeyInit() zur Verfügung, mit der die Taster-Steuerung initialisiert wird, und die globale Variable ButtonState, über die der aktuelle Zustand der Taster abgefragt werden kann. Für die einzelnen Zustände sind die Konstanten Button1 ... ButtonTst hinterlegt.

Die Funktion KeyInit() gibt die Pin-Change-Interrupts (PCINT) frei.

void KeyInit(void)
{ // Pin Change Interrupts freigeben
  PCMSK0 = _BV(PCINT5)|_BV(PCINT4); // Pin change mask register
  PCMSK2 = _BV(PCINT21);
  PCICR  = _BV(PCIE2)|_BV(PCIE0); //Pin change interrupt control register
}

Die zugehörigen Interrupt-Service-Routinen rufen lediglich die Funktion ChkButtons() auf.

ISR(PCINT0_vect)
{ ChkButtons();
}


ISR(PCINT2_vect)
{ ChkButtons();
}

Diese fragt die betroffenen PIN-Register ab, ermittelt den Tasten-Zustand und legt ihn in der globalen Variablen ButtonState ab.

inline void ChkButtons(void) // Gedrückte Taste zieht auf Masse
{ if (PIN(Button1Port) &  _BV(Button1Pin))
    ButtonState |= Button1;
  else
    ButtonState &= ~Button1;

  if (PIN(Button2Port) &  _BV(Button2Pin))
	ButtonState |= Button2;
  else
	ButtonState &= ~Button2;
	
  if (PIN(Button3Port) &  _BV(Button3Pin))
	ButtonState |= Button3;
  else
	ButtonState &= ~Button3;
}

4.C Zeitgeber

/*
 * Time.h
 *
 * Projekt: Teedipper
 * Created: 15.08.2015
 *  Author: Ulli
 */

#ifndef TIME_H_
#define TIME_H_

void StartDippingClock(uint16_t Seconds); // Zählt RemainingSeconds im Sekundentakt bis 0 herunter
extern volatile uint16_t RemainingSeconds;

void delay100();
void delay50();
void delay25();

#endif /* TIME_H_ */

Das Zeitgeber-API stellt die Funktion StartDippingClock(uint16_t Seconds) bereit, mit der ein Sekundenzähler gestartet wird. Als Parameter wird übergeben, wie lange der Zähler laufen soll. Über die globale Variable RemainingSeconds kann die Restzeit abgefragt werden.

Hinzu kommen die Funktionen delay25() ... delay100(), die eine Warteschleife der entsprechenden Länge in Millisekunden implementieren. Das direkte Einbindungen des Makros _delay_ms() würde wegen der Mehrfachnutzung dazu führen, dass der Programmspeicher überläuft.

StartDippingClock() initialisiert TIMER2 mit einem Prescaler von 256 und gibt den Overflow-Interrupt frei.

void StartDippingClock(uint16_t Seconds)
{ RemainingSeconds = Seconds;
  LoopCount = 0;
  
  // Timer starten
  TCCR2A  = 0;
  TCCR2B  = _BV(CS22)|_BV(CS21);    // Normal Mode, Prescaler: 256
  TIMSK2 |= _BV(TOIE2);             // Overflow Interrupt freigeben
}

Die Timer-OVF-ISR ist recht einfach aufgebaut. Bei 8 MHz Taktfrequenz, einem Prescaler von 256 und einem 8-Bit-Zähler ergibt sich eine Frequenz von 8.000.000 / 256 / 256 ≈ 122 Hz mit der der Overflow eintritt. Die lokale Variable LoopCount zählt mit, wie häufig der Interrupt ausgelöst wurde und erniedrigt zu gegebener Zeit die globale Variable RemainingSeconds. Ist diese bei 0 angekommen, wird der Timer ausgeschaltet (von der Taktquelle getrennt).

// 8.000.000 / 256 (8-Bit-Zähler) / 256 (Prescaler) = 122,0703125 Interrupt-Auslösungen machen eine Sekunde aus
#define OvfLoops 124 // 122 war etwas zu schnell
ISR(TIMER2_OVF_vect)
{ if(++LoopCount > OvfLoops)
  { if (--RemainingSeconds == 0)
	  TCCR2B = 0; // Timer wieder aus
	else
	  LoopCount = 0;
  }
}

Ich habe später noch einmal das laufende Gerät mit einer Uhr überprüft und daraufhin die Anzahl der ISR-Auslösungen auf eine Sekunde nachgebessert.

Bleiben noch die Zeitschleifen. Die Konstruktion soll, wie bereits oben beschrieben, dafür sorgen, dass der Kompiler nicht mehrfach das Makro _delay_ms in den Code einbindet. Dies würde zum Überlauf des Programmspeichers führen.

void __attribute__ ((noinline)) delay25(void)
{ _delay_ms(25);
}

void __attribute__ ((noinline)) delay50(void)
{ delay25();
  delay25();
}

void delay100()
{ delay50();
  delay50();
}

4.D Sieben-Segment-Display-Steuerung

/*
 * SevenSeg.h
 *
 * Projekt: Teedipper
 * Created: 15.08.2015
 *  Author: Ulli
 */

#ifndef SEVENSEG_H_
#define SEVENSEG_H_

#include "GeneralDefinitions.h"

void SevenSegInit(void); // Ports und Timer zur Ansteuerung der 7-Segment-Anzeige vorbereiten
void SevenSegStop(void); // Ports und Timer zurück in den Originalzustand

void SetSevenSeg(uint8_t Digit, uint8_t Value); // 0..9: Ziffern, 10: -, 11..17: A..G, 21: aus
void SetSevenSegOff(uint8_t Digit); // 0..9: Ziffern, 10: -, 11..17: A..G, 21: aus
void SetSevenSegAllOff(void);

void SetSevenSegDP(uint8_t Digit); // 0..2 = Digit, sonst aus
void SetSevenSegBlink(bool Blink);


#endif /* SEVENSEG_H_ */

Das API stellt eine Reihe von Funktionen zur Verfügung.

Funktion Wirkung
SevenSegInit() Initialisierung des Timers und der Ports
SevenSegStop() Versetzt die Ressourcen zurück in den Originalzustand
SetSevenSeg() Ausgabe des Symbols mit der übergebenen Symbolnummer im angegebenen Digit
SetSevenSegOff() Das angegebene Digit ausschalten
SetSevenSegAllOff() Alle Digits ausschalten
SetSevenSegDP() Den Dezimalpunkt am angegeben Dezimalpunkt einschalten
SetSevenSegBlink() Blinkende Anzeige

Um die Ansteuerung des Display einigermaßen übersichtlich gestalten zu können, habe ich zunächst einmal eine Möglichkeit geschaffen, auf einzelne Elemente (Segmente(Kathoden), Digits(gemeinsame Anode)) des Displays einfach zugreifen zu können.

Die Struktur PinAccess_t dient dazu, sowohl den Port als auch den Pin des Elements abzulegen. Wichtig ist, den korrekten Typ für die Port-Angabe zu wählen. Dieser muss volatile uint8_t * sein (s. mikrocontroller.net: AVR-GCC-Tutorial).

// Structure, um ein einzelnen Pin zu adressieren
typedef struct
{ volatile uint8_t * Port; // Adresse des Ports, zu dem der Pin gehört
  uint8_t PinMask;         // Maske des Pins im Port
}  PinAccess_t;

Die Elemente des Displays werden dann einfach als Arrays dieses Typs angelegt.

// Pin-Adressen der gemeinsamen Anoden
static PinAccess_t Digits[] = { {&PortDigit1, PinDigit1},
                                {&PortDigit2, PinDigit2},
                                {&PortDigit3, PinDigit3} };

// Pin-Adressen der Segmente									
static PinAccess_t Segments[] = {{&PortSegA, PinSegA},
                                 {&PortSegB, PinSegB},
                                 {&PortSegC, PinSegC},
                                 {&PortSegD, PinSegD},
                                 {&PortSegE, PinSegE},
                                 {&PortSegF, PinSegF},
                                 {&PortSegG, PinSegG},
                                 {&PortSegDP, PinSegDP} };

Der Einfachheit halber habe ich noch Funktionen zum Ein- und Ausschalten der einzelnen Elemente definiert.

// Die folgenden vier Methoden dienen zum Ein- und Ausschalten der Ziffern und der Segmente
// Sie sind ausgelegt für eine gemeinsame Anode
// Bei gemeinsamer Anode müssen die Polaritäten getauscht werden (d.h. Off <-> On)
inline static void DigitOn(uint8_t DigitNo)  // Digit einschalten
{ *Digits[DigitNo].Port |= Digits[DigitNo].PinMask;
}
inline static void DigitOff(uint8_t DigitNo)  // Digit einschalten
{ *Digits[DigitNo].Port &= ~Digits[DigitNo].PinMask;
}

inline static void SegmentOn(uint8_t SegmentNo)  // Segment einschalten
{ *Segments[SegmentNo].Port &= ~Segments[SegmentNo].PinMask;
}
inline static void SegmentOff(uint8_t SegmentNo)  // Segment ausschalten
{ *Segments[SegmentNo].Port |= Segments[SegmentNo].PinMask;
}

Im nächsten Schritt geht es darum, die Symbole zu definieren, die angezeigt werden sollen. Dies sind zunächst die Ziffern 0 .. 9, Symbole für ein Laufsymbol (#10..20), das am Ende der Dipp-Zeit, während des Abspielens der Fanfare animiert werden soll, und ein Symbol (#21), bei dem kein Segment leuchtet. Das Makro Symbol hilft beim Anlegen der Daten.

#define Symbol(A, B, C, D, E, F, G) ((A<<7) + (B<<6) + (C<<5) + (D<<4) + (E<<3) + (F<<2) + (G<<1))

//                           A  B  C  D  E  F  G
uint8_t Symbols[] = { Symbol(1, 1, 1, 1, 1, 1, 0), // 0
                      Symbol(0, 1, 1, 0, 0, 0, 0), // 1
					  Symbol(1, 1, 0, 1, 1, 0, 1), // 2
					  Symbol(1, 1, 1, 1, 0, 0, 1), // 3
					  Symbol(0, 1, 1, 0, 0, 1, 1), // 4
					  Symbol(1, 0, 1, 1, 0, 1, 1), // 5
					  Symbol(1, 0, 1, 1, 1, 1, 1), // 6
					  Symbol(1, 1, 1, 0, 0, 0, 0), // 7
					  Symbol(1, 1, 1, 1, 1, 1, 1), // 8
					  Symbol(1, 1, 1, 1, 0, 1, 1), // 9
					  Symbol(0, 0, 0, 0, 0, 0, 1), // 10= -

                      // Lauf-Symbol
                      //     A  B  C  D  E  F  G
					  Symbol(1, 0, 0, 0, 0, 0, 0), // 11
					  Symbol(1, 0, 0, 0, 0, 1, 0), // 12
					  Symbol(0, 0, 0, 0, 0, 1, 1), // 13
					  Symbol(0, 0, 1, 0, 0, 0, 1), // 14
					  Symbol(0, 0, 1, 1, 0, 0, 0), // 15
					  Symbol(0, 0, 0, 1, 0, 0, 0), // 16
					  Symbol(0, 0, 0, 1, 1, 0, 0), // 17
					  Symbol(0, 0, 0, 0, 1, 0, 1), // 18
					  Symbol(0, 1, 0, 0, 0, 0, 1), // 19
					  Symbol(1, 1, 0, 0, 0, 0, 0), // 20
					  
					  Symbol(0, 0, 0, 0, 0, 0, 0)};// 21 = Aus
Symbol #0 Symbol #1 Symbol #2 Symbol #3 Symbol #4 Symbol #5 Symbol #6 Symbol #7 Symbol #8 Symbol #9 Symbol #10 Symbol #11 Symbol #12 Symbol #13 Symbol #14 Symbol #15 Symbol #16 Symbol #17 Symbol #18 Symbol #19 Symbol #20 Symbol #21
#0 #1 #2 #3 #4 #5 #6 #7 #8 #9 #10 #11 #12 #13 #14 #15 #16 #17 #18 #19 #20 #21

Drei lokale Variablen dienen zur Speicherung des aktuellen Anzeige-Zustands:

// Aktuelle Belegung des Displays
static uint8_t Display[3];  // 0 Links, 1 Mitte, 2 Rechts

static uint8_t DpPos = 3; // Position DezimalPunkt. >2: Aus
static bool DoBlink = false; // Gibt an, ob die Anzeige blinken soll

Das Array Display nimmt das aktuelle Segment-Muster für die drei Ziffern auf. DpPos gibt an, bei welchem Digit der Dezimalpunkt angezeigt werden soll. Ein Wert > 2 schaltet den Punkt aus. DoBlink gibt an, ob die Anzeige blinken soll. Dazu kommen die öffentlichen Funktion zur Befüllung dieser Variablen:

// Belegt die Display mit dem anzuzeigenden Wert
void SetSevenSeg(uint8_t Digit, uint8_t Value)
{ if (Digit < 3)
    Display[Digit] = Symbols[Value];
  if (DpPos < 3)
	Display[DpPos] |= 1; // Dezimalpunkt auf dem letztem Bit
}

void SetSevenSegOff(uint8_t Digit)
{ if (Digit < 3)
	Display[Digit] = 0;
}

// Legt fest, wo der Dezimalpunkt angezeigt wird ( >2: keine Anzeige
void SetSevenSegDP(uint8_t Digit)
{ Display[DpPos] &= ~1; // Alten Punkt ausschalten
  DpPos = Digit;
  if (DpPos < 3)
	Display[DpPos] |= 1;
}

// legt fest, ob die Anzeige blinken soll.
void SetSevenSegBlink(bool Blink)
{ DoBlink = Blink;
}

void SetSevenSegAllOff()
{ SetSevenSegBlink(false);
	SetSevenSegOff(0);
	SetSevenSegOff(1);
	SetSevenSegOff(2);
	SetSevenSegDP(3);
}

Die Funktionen sind nicht schwierig zu verstehen.

Zwei Funktionen schalten die Display-Steuerung ein und aus:

void SevenSegInit(void)
{ // Pins für die Segmente auf Output
  DDR(PortSegA)  |= PinSegA;  // Segment auf Output
  DDR(PortSegB)  |= PinSegB;  // Segment auf Output
  DDR(PortSegC)  |= PinSegC;  // Segment auf Output
  DDR(PortSegD)  |= PinSegD;  // Segment auf Output
  DDR(PortSegE)  |= PinSegE;  // Segment auf Output
  DDR(PortSegF)  |= PinSegF;  // Segment auf Output
  DDR(PortSegG)  |= PinSegG;  // Segment auf Output
  DDR(PortSegDP) |= PinSegDP; // Segment auf Output
  
  // Pins für die Anoden auf Output
  DDR(PortDigit1) |= PinDigit1; // Gemeinsame Anode/Kathode Digit1: Output
  DDR(PortDigit2) |= PinDigit2; // Gemeinsame Anode/Kathode Digit2: Output
  DDR(PortDigit3) |= PinDigit3; // Gemeinsame Anode/Kathode Digit3: Output
  
  // Timer0 starten
  TCCR0B = _BV(CS01);  // Normal Mode, Prescaler 8, f ca. 4 kHz, Taktdauer ca. 0,25 ms
  TIFR0  = _BV(TOV0);  // Gff. gesetztes Interrupt-Flag löschen
  TIMSK0 = _BV(TOIE0); // Timer/Counter0 overflow interrupt enable
}

SevenSegInit() schaltet das Display ein. Zuerst werden alle betroffenen Pins in den Output-Modus geschaltet, danach Timer0 gestartet und der Overflow-Interrupt frei gegeben. Der Timer wird so eingestellt, das der Overflow-Interrupt etwa alle 0,25 ms ausgelöst wird. Das ist auch die Brenndauer für ein einzelnes Segment.

void SevenSegStop(void)
{ // Timer 0 ist exklusiv für die 7-Segment-Steuerung reserviert
  TCCR0B = 0;  // Alle Register zurück auf 0
  TIMSK0 = 0;

  SetSevenSegAllOff();

  // Alle Ports auf Input
  DDR(PortSegA)  &= ~PinSegA;  // Pins zurück auf Input
  DDR(PortSegB)  &= ~PinSegB;
  DDR(PortSegC)  &= ~PinSegC;
  DDR(PortSegD)  &= ~PinSegD;
  DDR(PortSegE)  &= ~PinSegE;
  DDR(PortSegF)  &= ~PinSegF;
  DDR(PortSegG)  &= ~PinSegG;
  DDR(PortSegDP) &= ~PinSegDP;
  
  DDR(PortDigit1) &= ~PinDigit1;
  DDR(PortDigit2) &= ~PinDigit2;
  DDR(PortDigit3) &= ~PinDigit3;
}

Die genau gegenteilige Wirkung hat SevenSegStop().

Als Letztes kommt die ISR, die das Display ansteuert. ActDigit (0..2) und ActSegment (0..7) dienen zur Speicherung des aktuell leuchtenden Segment und des Digits.

Das Blinken wird durch Abzählen des 0,25-ms-Takts gesteuert. Die Konstante BlinkPhase bestimmt, wie viele Takte eine Blink-Phase (jeweils An und Aus) dauern soll. Aktuell ist 1000 eingestellt. Eine einzelne Phase dauert somit 250 ms. Die Blinkfrequenz ist also etwa 2 Hz. ActBlinkCount zählt die in der aktuellen Phase verbrachten Takte. ActBlink hält fest, ob die aktuelle Phase AN oder AUS ist.

#define BlinkPhase 1000        // bei 0,25 ms pro Takt sind dies ca. 250ms
static uint8_t  ActDigit = 0;  // Aktuell geschaltete Ziffer
static uint8_t  ActSegment;    // Aktuell geschaltetes Segment
static uint16_t ActBlinkCount; // Anzahl verbleibender Durchläufe für diese Blink-Phase 
static uint8_t  ActBlink;      // Aktueller Blink-Zustand, An oder Aus

Etwa alle 0,25 ms wird sie ISR aufgerufen.

// Es wird zu einer Zeit immer ein Segment angezeigt
ISR(TIMER0_OVF_vect)
{ // Alte Anzeige ausschalten
  DigitOff(ActDigit);
  SegmentOff(ActSegment);
   
  // Nächstes anzuzeigende Segment ermitteln
  if (++ActSegment > 7)
  { ActSegment = 0;
    if (++ActDigit > 2)
	  ActDigit = 0;
  }
 
  if (++ActBlinkCount > BlinkPhase)
  { ActBlinkCount = 0;
	ActBlink = ~ ActBlink;
  }
 
  if (!DoBlink || ActBlink)
  { // Neue Anzeige einschalten
    DigitOn(ActDigit);
    if(Display[ActDigit] & (1<<(7-ActSegment)))
     SegmentOn(ActSegment);
  }
}

Zunächst wird die alte Anzeige ausgeschaltet. Dann wird der Segment-Zähler hochgesetzt. Findet ein Überlauf statt, wird er zurück gesetzt und der Digit-Zähler erhöht.

Ist die aktuelle Blinkphase abgelaufen, wird der Taktzähler zurück gesetzt und in die andere Phase geschaltet. Dies geschieht auch dann, wenn aktuell kein Blinken eingestellt ist.

Als letztes wird das neu ermittelte Segment eingeschaltet. Falls Blinken eingestellt ist, geschieht dies nur währen der AN-Phase. Die Anode wird auf jeden fall eingeschaltet. Das Segment nur dann, wenn es für die Darstellung des aktuellen Symbols notwendig ist: if(Display[ActDigit] & (1<<(7-ActSegment))).

Animation

Die Animation während des Abspielens der Ende-Fanfare wird im Hauptprogramm durchgeführt. Die Melodie-Ausgabe ist interruptgetrieben, so dass im Hauptprogramm die Animation mit Hilfe von einfachen Zeitschleifen realisiert werden kann. Die Symbolfolge für die Animation ist für jeweils ein Digit in den Arrays AnimationX hinterlegt. Es handelt sich um eine "8" über die gesamte Display-Breite.

#define S0 21
const uint8_t Animation0[] = {10, S0, S0, S0, S0, 16, 17, 18, 10, S0, S0, S0, S0, 11, 12, 13}; // Linkes Digit
const uint8_t Animation1[] = {10, 10, S0, S0, 16, 16, S0, S0, 10, 10, S0, S0, 11, 11, S0, S0}; // Mittleres
const uint8_t Animation2[] = {S0, 10, 14, 15, 16, S0, S0, S0, S0, 10, 19, 20, 11, S0, S0, S0}; // Rechtes Digit

Die Ausgabe erfolgt durch einspielen der Symbole in einer Schleife, die durch das Melodie-Ende oder Druck der Tastenkombination "Stopp" unterbrochen wird.

while(IsPlayingMelody())
  { for (uint8_t i=0; i < sizeof(Animation0); i++)
	{ if (!IsPlayingMelody())
	    break;
	  SetSevenSeg(0,Animation0[i]);
	  SetSevenSeg(1,Animation1[i]);
	  SetSevenSeg(2,Animation2[i]);
	  delay50();
		  
	  if (ButtonState == ButtonStp)
	  { delay100(); // Entprellung
	    goto Exit;
	  }
	} // for
  } // while(isPlaying())

4.E Tonausgabe

Die Ausgabe der Melodie für die Ende-Fanfare habe ich bereits an anderer Stelle beschrieben: RTTTL: Melodien mit einem AVR ♫♬♯♪.

Hinzugekommen ist die Funktion PlayRandom(), die das Abspielen einer zufällig ausgewählten Melodie erlaubt.

static const note_t * Melodies[] ={GoodBad, BurgerTime, Star, Smoke, PinkPanther, Muppets};
static uint8_t Initialized = 0;

void PlayRandom()
{ if(!Initialized)
  { srand(GetSeed());
	Initialized = 1;
  }
	PlayMelody(Melodies[rand()%(sizeof(Melodies)/sizeof(Melodies[0]))]);
}

Das Array Melodies enthält Pointer auf die zur Verfügung stehenden Melodien. Je nach Größe des Programmspeichers können hier weitere Melodien aufgeführt werden. Hier nicht aufgeführte Melodien werden vom Kompiler nicht eingebunden und verauchen demzufolge auch keinen Speicher. Aus diesem Array wird dann eine zufällige Melodie ausgewählt.

Beim ersten Aufruf der Funktion wird der Zufallszahlengenerator auf einen zufälligen Wert eingestellt (s. Zufallswert zur Initialisierung des Zufallszahlengenerators).

4.F Servo-Steuerung

Prinzip

Die Servo-Steuerung erfolgt vollkommen interruptgesteuert. Hierbei sind zwei Aspekte zu berücksichtigen. Das Servo benötigt alle etwa 20-50 ms einen Puls, dessen Länge (etwa 1-2 ms) die Soll-Position des Servos angibt. Dies lässt sich sehr elegant mit einen Timer im PWM-Modus realisieren. Die Gesamtdauer eines Timer-Zyklus muss dann der Periodendauer von etwa 20 ms entsprechen. Der Tastgrad (duty cycle) wird so eingestellt, dass ein Puls von 1-2 ms Dauer entsteht. Durch die Variation des Tastgrads kann die Position des Servos beeinflusst werden.

Der Timer1 (16-Bit) wird im Fast-PWM-Modus (Mode 14 im ATmega48-Datenblatt). Der Prescaler ist auf 64 eingestellt, so dass das Timer-Register jeweils in Schritten etwa 8µs erhöht wird (1s / 8.000.000 x 64).

Die Obergrenze der Zählers wird in diesem Modus durch das Register "ICR1" festgelegt. Bei einem ICR1-Wert von 2500 ergibt sich eine Pulsdauer liegt bei etwa 20 ms. Die realisierte Periodendauer liegt innerhalb des erwarteten Zeitfensters.

Das Steuersignal selbst wird über das Tastverhältnis des PWM-Signals generiert. Pin OC1A (Sonderfunktion von Pin PB1) kann so geschaltet werden, dass er beim Überlauf des Timer-Zählers auf High geht und bei Erreichen des in Register OCR1A vorgegebenen Zählerstandes wieder auf Low geht. Durch passendes Einstellen des Registers OCR1A kann die gewünschte Länge des Steuerpulses mit einer Genauigkeit von 8 µs (s.o.) erzeugt werden.

Für die Puls-Erzeugung ist keine weitere Funktionalität notwendig. Die Pulse können komplett über die Hardware des ATmega generiert werden. Für den Tee-Dipper ist es jedoch notwendig, dass der Arm langsam auf und ab bewegt wird. Das kontinuierliche Verändern der Servo-Stellung soll auch über einen Interrupt gesteuert werden, das Hauptprogramm soll sich hierum natürlich auch nicht kümmern müssen.

Praktischerweise nutzt man hierzu ebenfalls den Timer1. Es bietet sich der Overflow-Interrupt an. Die Zeitdauer für eine Periode beträgt ca. 20 ms (s.o.) . Zur Bewegung des Servo-Arms muss OCR1A zwischen den Werten 100 (Stellung oben) und 180 (Stellung unten) variieren*. Wenn man den OCR1A-Wert bei jeder 20-ms-Periode um 1 erhöht bzw. erniedrigt, würde sich der Arm in etwa 2 x (180-100) x 20 ms = 3,2 s aus und ab bewegen. Das ist eine angenehme Geschwindigkeit für einen Teebeutel . Einen 20-ms-Takt bietet bei der angegeben Konfiguration der Overflow-Interrupt.

* Die Werte wurden experimentell ermittelt und hängen davon ab, in welcher Stellung der Arm auf das Servo montiert wird. Wenn das Servo andersherum einbaut wird, wird die obere Positionen einen größeren OCR1A-Wert benötigen als die umgekehrte. Die Software berücksichtigt dies.

ISR

Die vereinfachte ISR sähe wir folgt aus (die implementierte Funktion ist etwas aufwändiger um der möglichen umgekehrten Richtung bei spiegelbildlichem Einbau des Servos Rechnung zu tragen):

#define OcrAtTop      100
#define OcrAtBottom   180

#define DirectionToBottom  1
#define DirectionToTop    -1

static uint16_t Direction;

// OCR1A langsam erhöhen bzw. erniedrigen
ISR(TIMER1_OVF_vect)
{ OCR1A += Direction;

  if (OCR1A > OcrAtBottom)
    Direction = DirectionToTop;
  if (OCR1A < OcrAtTop)
    Direction = DirectionToBottom;
}

API

Das Modul "Servo" bietet zwei Funktionen: ServoStart() startet den o.g. Mechanismus zu beachten ist, dass der FET zur Versorgung des Servos mit Strom eingeschaltet wird. ServoStop() unterbricht den Interrupt, fährt das Servo in die Ruhestellung, schaltet die Stromversorgung des Servos aus und versetzt betroffenen Pins wieder in den Input-Modus.

#define ICR_VAL  2500

void ServoStart()
{ TCCR1A = _BV(COM1A1) | _BV(WGM11); // Fast PWM, TOP = ICR1
  TCCR1B = _BV(WGM12)  | _BV(WGM13)  | _BV(CS11) | _BV(CS10); // Prescaler 64
  ICR1 = ICR_VAL;
  DDR(ServoPort)  |= _BV(ServoControlPin) | _BV(ServoPowerPin); // Pins für ServoControlPin, ServoPowerPin auf Output
  ServoPort       |= _BV(ServoPowerPin);                        // Servo-Power ein
  TIMSK1 = _BV(TOIE1); // Overflow Interrupt freigeben
  OCR1A  = OcrAtTop;
  Direction = DirectionToBottom;
}
void ServoStop()
{ Direction = DirectionToTop;

  while (OCR1A > OcrAtTop); // Warten auf Endstellung

  OCR1A = OcrAtTop;
  TIMSK1 = 0; // Overflow Interrupt sperren
  _delay_ms(100);
  TCCR1A = 0;
  TCCR1B = 0;
  ServoPort      &= ~_BV(ServoPowerPin);
  DDR(ServoPort) &= ~(_BV(ServoControlPin) | _BV(ServoPowerPin)); // ServoControlPin, ServoPowerPin auf Input
}
Das Atmel-Studio-6-Projekt zum Download:   Eagle-Dateien zum Download

5. Download

Alle Komponenten des Projekts zusammengefasst zum Download:

OpenScad-Dateien Eagle-Dateien zum Download OpenScad-Dateien Eagle-Dateien zum Download
3D-Dateien
für den Arm
Eagle-Dateien
für die Schaltung
3D-Dateien
für das Gehäuse
Firmware