Komfortables UV Belichtungsgerät

Aus Trimension Wiki
Wechseln zu:Navigation, Suche
Exposer v2 title.png

Einleitung

Für die Verarbeitung photobeschichteten Platinen-Materials benötigt man ein UV Belichtungsgerät mit einer Wellenlänge von etwa 400 nm. Kommerzielle Geräte sind entweder sehr teuer, sehr groß und/oder in der Handhabung mangelhaft. Häufig bezahlt man Features die man im privaten Bereich häufig nicht braucht oder es fehlen welche.

Das hier vorgestellte Gerät ist vom Kostenaufwand ebenfalls nicht besonders günstig, dafür aber sehr kompakt, leistungsfähig und komfortabel. Viele Selbstbaulösungen verwenden ausrangierte Scanner, die in der Regel wieder sehr klobig sind und optisch uninteressant wirken. Andere verwenden freihängende Halogenlampen oder spezielle Quecksilberlampen, die wiederum sehr unhandlich sind und keine reproduzierbaren Ergebnisse liefern.
Daher habe ich mich entschlossen einen Belichter zu entwickeln, der den meisten Ansprüchen genügt, einfach zu bedienen und komfortabel zu handhaben ist. Hohe Leistung soll eine kurze und genaue Belichtung ermöglichen.

Das Gerät verwendet LED’s für die Belichtungseinheit, da diese eine optimale Wellenlänge (370-400 nm) haben, leicht zu beschaffen und zu verarbeiten sind. Außerdem erhält das Gerät eine elektronische Steuereinheit (Timer) um die Belichtung optimal zu timen.

Aufbau

Das Gehäuse ist aus Sperrholz geleimt und besteht aus zwei Kammern. Die große (hintere) Kammer nimmt die Belichtungseinheit auf, während die Andere (vorn) für die Steuer-Elektronik vorgesehen ist. Abbildung 1 und Abbildung 2 zeigen den Aufbau des Gehäuses.

Abbildung 1 - Gehäuse Ansicht
Abbildung 2 - Gehäuse Ansicht

Die Höhe der Belichtungskammer ist auf Grund des geringen Abstrahlungswinkels der LED’s vorgegeben, weshalb das Gerät leider nicht flacher gestaltet werden konnte. Siehe hierzu Abschnitt „Belichtungseinheit“. Die Kammer selbst wird durch eine normale Glasscheibe, die in das Gehäuse eingelassen wird, abgeschlossen.

Abbildung 3 - Gehäuse Deckel

Über dieser Kammer befindet sich ein Deckel, der mit Schaumgummi gefüllt und von hinten mit Scharnieren am Gehäuse verschraubt ist. Dieser Deckel schließt die Kammer nach oben ab und presst die Platine später an die Glasscheibe.

In der kleinen Kammer wird die gesamte Elektronik untergebracht. Dazu gehört neben dem Netzteil, die Steuerelektronik, das Display und die Leistungselektronik. Sie wird nach oben durch eine Aluminium-Frontblende abgeschlossen, die die Bedienelemente enthält.

Das Gehäuse ist nur geleimt, aber auf Grund der Konstruktion sehr stabil.

Belichtungseinheit

Die Belichtungseinheit soll eine Fläche von etwas mehr als 100 x 160 mm (Euro-Platine) gleichmäßig mit möglichst hoher Intensität ausleuchten. Dafür kommen 77 UV-LED’s vom Typ LT-0670 zum Einsatz, die auf einer Euro-Platine montiert sind. Diese hohe Zahl sorgt nicht nur für eine hohe Lichtausbeute, sondern ermöglicht auch, wegen der engen Plazierung eine gleichmäßige Ausleuchtung sowie eine reduzierte Bauhöhe des Gehäuses. Da die LED’s nur einen Abstrahlwinkel von 30° haben, ist für eine gleichmäßige Ausleuchtung bei geringer Bauhöhe diese hohe LED-Dichte auf der Platine notwendig. Je größer der Abstand der LED’s zueinander ist, desto größer muß auch der Abstand der LED‘s zur Glasscheibe sein und um so geringer wird die Strahlungsintensität. Die Einsparung von LED‘s wird also an anderer Stelle teuer bezahlt. Abbildung 4 zeigt die Dimensionierung für 77 LED’s auf einer Euro-Platine. Die damit erreichte Ausleuchtung ist in Abbildung 5 dargestellt.

Abbildung 4 - Dimensionierung
Abbildung 5 - Ausleuchtung
Abbildung 6 - Schaltplan der Belichtereinheit

Die Verschaltung der LED’s ist in der Version 2 des Belichters elektrisch leider nicht optimal. Alle LED’s erhalten einen eigenen Vorwiderstand und sind parallel geschaltet. Das hat den Vorteil, daß man mit nur einer Spannung (5V) auskommt. Weniger aufwändig und mit besserer Leistungsbilanz wäre es, mehrere LED’s in Reihe mit einen Vorwiderstand zu schalten und das Ganze dann mit höherer Spannung zu betreiben.

Die folgende Tabelle zeigt die Ergebnisse für verschiedene Konstellationen.

Anzahl LED's LED Spannung Nenn- spannung Abfall am Widerstand Leistung Widerstand Gesamt LED's Anzahl Widerst. Leistung Licht Leistung Widerst. Wirkungs- grad Gesamt Leistung
1 3,2 V 5 V 1,8 V 45 mW 100 100 8,0 W 4,5 W 43,75 % 12,5 W
2 6,4 V 9 V 2,6 V 65 mW 100 50 8,0 W 3,3 W 59,38 % 11,3 W
3 9,6 V 12 V 2,4 V 60 mW 102 34 8,2 W 2,0 W 75,00 % 10,2 W
4 12,8 V 15 V 2,2 V 55 mW 100 25 8,0 W 1,4 W 82,81 % 9,4 W
5 16,0 V 18 V 2,0 V 50 mW 100 20 8,0 W 1,0 W 87,50 % 9,0 W
7 22,4 V 24 V 1,6 V 40 mW 105 15 8,4 W 0,6 W 92,86 % 9,0 W

Jede LED wird durch einen Vorwiderstand von 68 Ω auf etwa 26 mA Durchlaßstrom eingestellt. An den Vorwiderständen fallen dann jeweils 1,8V ab. Damit ergibt sich ein Gesamtstromverbrauch von etwa 2A bei 5V. Mit dieser Verschaltung werden leider ca. 3,5 W an den Widerständen verbraten, weshalb hier eine Menge Wärme entsteht, die durch seitliche Lüftungsbohrungen abgeführt werden muß.

Die Belichtungskammer wird innen zur besseren Lichtreflexion mit Silber-Chrom-Lack beschichtet, ebenso wie die LED-Platine. Nach der Montage der Platine wird die Kammer dann mit einer Glasscheibe abgedeckt, die in das Gehäuse eingelassen wird. Die UV-Durchlässigkeit von Fensterglas ist bei den 400nm der LED‘s noch ausreichend groß, weshalb hier kein spezielles Quarzglas verwendet werden muß. Abbildung 7 und Abbildung 8 zeigen die Belichtungskammer des fertigen Gerätes.

Abbildung 7 - Blick in Belichtungskammer
Abbildung 8 - aktive Belichtungskammer

Steuerelektronik

Die Steuerelektronik weist keine Besonderheiten auf. Die Beschaltung des Controllers beschränkt sich auf das notwendige Minimum. Zur Stromversorgung wird das Schaltnetzteil-Modul KAM15-05 verwendet, welches einen Strom von 3A bei 5V Ausgangsspannung liefert. Es ist keine weitere Stabilisierung notwendig. Mit dem Regler P1 kann die Ausgangsspannung des Netzteils justiert werden. Diese Spannung wird direkt dem Controller und der Belichtungseinheit zugeführt.

Als Controller kommt ein ATtiny2313 zum Einsatz. Für die Programmierung ist ein ISP-Adapter auf der Platine untergebracht, was spätere Updates erleichtert. Am Pin 11 wird das Ausgangssignal entnommen und über einen Leistungs-Treiber der Belichtereinheit zugeführt. Der Logic-Level-HEXFET kann direkt vom Controller angesteuert werden und liegt im Masse-Zweig der LED’s. Abbildung 9 zeigt den vollständigen Stromlaufplan des Belichters.

Abbildung 9 - Schaltplan der Steuerelektronik

Das Benutzerinterface besteht aus einem zweizeiligen LCD-Display und 6 Tastern, für die Steuerung der Belichter-Funktionen:

  • START – Startet den Belichtungsvorgang oder unterbricht einen Laufenden
  • STOP – stoppt die Belichtung und/oder setzt die Uhr auf 00:00 zurück
  • SEC_UP, SEC_DOWN – zum Einstellen der Sekunden (mit Über-/Unterlauf)
  • MIN_UP, MIN_DOWN – zum Einstellen der Minuten (mit Über-/Unterlauf)

Es kann eine Zeit von maximal 30 Minuten eingestellt werden. Die Begrenzung findet in der Software statt und dient dem thermischen Schutz des Belichters. Für mehr Komfort, kann die Zeit nur im Pausen- oder Stop-Modus eingestellt werden. Die START-Taste ist nur bei eingestellter Zeit aktiv.

Die gesamte Elektronik ist auf drei Platinen verteilt:

  • Netzteil – enthält neben dem Netzteil-Modul auch den Leistungstreiber
  • Controller – enthält die Steuerelektronik und das Display
  • Bedienfeld – hier sind die 6 Taster für die Bedienung montiert

Die Abbildungen 10, 11 und 12 zeigen den Aufbau und die Montage der Steuerelektronik im fertigen Belichter-Gehäuse.

Abbildung 10 - Steuerelektronik
Abbildung 11 - Netzteilplatine eingebaut
Abbildung 12 - Steuerelektronik eingebaut

Software

Die Software ist in C geschrieben und besteht aus einer kleinen Bibliothek zur Ansteuerung des LCD-Displays und aus dem Hauptprogramm. Die Listings sind am Ende des Dokumentes eingefügt.

LCD-Display (lcd_tools.c)

Die Funktionen zur Ansteuerung des Displays sind für ein zwei- oder vierstelliges alphanumerisches Display mit jeweils 16 oder 20 Zeichen ausgelegt. Um die Bibliothek wiederverwendbar zu machen, wurden die wichtigsten Parameter, die von der Beschaltung des Controllers abhängen, oder vom Display selbst, als Konstanten im Header definiert.

Die meisten zeichenbasierten Displays werden als HD44780-kompatibel bezeichnet. Das bezieht sich in der Regel aber nur auf die Pin-Belegung. Bei den Befehlen gibt es jedoch häufig Abweichungen und die Speicheradressen unterscheiden sich fast immer. Die Bibliothek kann Original-Controller vom Type HD44780 und Controller vom Type KS0073 ansteuern. Anpassungen an andere Controller-Typen sind leicht möglich. Die entsprechenden Speicherbereiche sind im Headerfile definiert. Weitere Informationen zur LCD-Ansteuerung finden sich in [1] [2] und [3].

Busy-Flag
Die R/W Leitung des Displays ist zwar an den Controller geführt, wird aber nicht genutzt. Sie wird von der Software auf Low gehalten, was bedeutet, daß das Display nur beschrieben und niemals gelesen wird. Das Auslesen des Busy-Flags bringt keinen Vorteil, da dies im Betrieb nur nach der Ausführung eines Befehls notwendig ist. Nur der Clear- und der Cursor-Home-Befehl benötigen ziemlich viel Zeit, weshalb nach diesen Befehlen jeweils 2 ms gewartet wird. Diese beiden Befehle werden aber nur einmal nach der Initialisierung aufgerufen. Somit bleibt im Betrieb nur noch das Setzen des Cursors, was aber nur etwa 40 µs braucht und damit in der Regel sogar ohne Delay benutzt werden kann.
Hier könnte man also noch ein I/O Port Pin anderweitig nutzen.
Initialisierung
Die Initialisierung des Displays erfolgt gemäß dem Datenblatt des Display-Controllers. Nach dem Einschalten wird erstmal 50 ms gewartet, bis sich der Display-Controller initialisiert hat. Im Anschluß wird dann der 8-Bit-Modus eingestellt und danach der 4-Bit Modus. Dieses Vorgehen ist sinnvoll, damit nach einem Reset das Display wieder korrekt angesteuert werden kann.
Im 4-Bit Modus werden nur vier Datenleitungen verwendet, sodaß die beiden Nibbles nacheinander an das Display geschickt werden müssen. Das spart insgesamt 4 Portleitungen. Die Zeichenausgabe-Funktion berücksichtigt das.
Abschließend wird das Display noch entsprechend konfiguriert um die Initialisierung abzuschließen.
Zeichenausgabe
Für die Zeichenausgabe stehen drei Funktionen zur Verfügung. Die wichtigste ist die Basisfunktion lcd_write. Sie gibt ein einzelnes Zeichen an der Cursorposition aus, oder sendet einen Befehl an das Display, was über den zweiten Parameter gesteuert wird. Die anderen beiden Funktionen lcd_writetext und lcd_writetime verwenden die Funktion lcd_write um entsprechend formatierte Daten auszugeben. Die Basisfunktion gibt die Zeichen im 4-Bit Modus aus, also zuerst das High-Nibble und dann das Low-Nibble. Die Cursorposition wird per Konfiguration automatisch nach dem Schreiben inkrementiert, sodaß die Zeichen hintereinander geschrieben werden können.
Stack
Bei der Verwendung der LCD-Bibliothek ist darauf zu achten, daß es hier verschachtelte Funktionsaufrufe gibt, die den Stack belasten. Das betrifft vor Allem die High-Level Zeichenausgabe lcd_writetext und lcd_writetime. (siehe Abschnitt Stack).

Interrupt Service Routine

Der Timer 0 des Controllers ist für den CTC-Mode konfiguriert. Alle 10 ms wird daher (TIMER0 Compare Match A) Interrupt ausgelöst. Die ISR erledigt dann folgende Aufgaben:

  • Entprellen der Tasten
  • Blinkfrequenz steuern
  • Sekundentakt erzeugen
  • Stopuhr herabzählen und die LED’s abschalten, wenn die Uhr abgelaufen ist.

Das Entprellen der Tasten wird in der Software erledigt und geht auf eine Lösung von Peter Dannegger [4] zurück. Dabei wird beim Abfragen der Tasten für jede Taste ein Zähler hochgezählt, wenn die Taste aktiv ist. Die Tasten sind low-aktiv verschaltet, werden also über die internen Pullups auf High gezogen und schalten gegen Masse. Jeder Low-Zustand einer Taste führt zum triggern des Zählers. Ist der Zähler drei mal hintereinander getriggert worden, wird die globale Variable key_state für die jeweilige Taste auf 1 gesetzt. Diese Variable enthält dann den bitkodierten und entprellten Zustand der Tasten. Der Zähler verarbeitet alle Tasten gleichzeitig. Weiterhin wird bei steigender Flanke in key_state, eine weitere (key_press) auf 1 gesetzt. Diese wird später zum Abfragen der Tasten verwendet. Da die ISR alle 10 ms aufgerufen wird, werden die Tasten wirksam entprellt.

Die ISR ermittelt dann über einen weiteren Zähler, ob die Taste länger als 1 s dauerhaft gedrückt wurde und setzt in diesem Fall das entsprechende Bit in der Variablen key_hold. Solange die Taste gedrückt ist, wird key_hold alle 100 ms erneut gesetzt. Das ermöglicht später auch das Verarbeiten lange gedrückter Tasten. Die Zeiten für das erstmalige Aktivieren des Flags sowie das darauf folgende Intervall, sind im Headerfile einstellbar.

Die Blinkfrequenz für die Anzeige bestimmter Informationen auf dem Display wird über die Variable blink_on gesteuert. Dieses Flag wechselt alle 400 ms seinen Zustand, was die ISR über einen weiteren konfigurierbaren Zähler realisiert.

Zum Schluß wird über die interne Zählvariable count_clk im Sekundentakt geprüft, ob die LED’s leuchten, der Belichter also aktiv ist und zählt in diesem Fall die Uhr entsprechend eine Sekunde herunter. Steht die Uhr auf 00:00, werden die LED’s abgeschaltet, indem das Ausgangs-Pin wieder auf Low geschaltet wird.

Die ISR belastet den Stack sehr stark, weshalb hier für ausreichend Platz im RAM gesorgt werden muß (siehe Abschnitt Stack).

Hauptprogramm

Das Hauptprogramm initialisiert zunächst das LCD-Display und zeigt den Standardtext sowie den aktuellen Status an. Im Anschluß werden die Ports und der Timer konfiguriert.

Verwendet wird der 8-Bit Timer 0 des AVR. Um ein 10 ms Intervall für die ISR zu erzeugen, wird der CTC (Clear Timer on Compare Match) Mode des Timers verwendet. Das ermöglicht eine feingranulare Einstellung der Auflösung, da der Timer in diesem Modus nur bis zum Compare-Match zählt und dann wieder von vorn anfängt. Der Prescaler des Timers wird auf 64 eingestellt, was bei einem CPU-Takt von 1 MHz, einen Timer-Takt von 15625 Hz produziert. Für das 10 ms Intervall brauchen wir einen Takt von 100 Hz, weshalb das Compare-Match Register OCR0A auf den Wert 156 gesetzt wird (15625 / 100). Es ergibt sich daraus ein Takt von 100,16 Hz was einem Aufrufintervall von 9,984 ms entspricht. Genauer geht es leider mit dem 1 MHz Quarz nicht. Die Abweichung beträgt aber nur 0,16 % was nur etwa 1 s in 10 Minuten entspricht. Das ist durchaus vertretbar.

Die Hauptschleife des Programms hat die Aufgabe, die Tasten abzufragen und deren Status zu verarbeiten um die Funktionen des Gerätes zu implementieren. Weiterhin wird der Status und die aktuelle Zeit der Stoppuhr am Display angezeigt.

Abbildung 13 - Display Anzeige

Das Display ist in drei Bereiche unterteilt. In der ersten Zeile links wird ein Konstanter Text angezeigt, der das Gerät identifiziert. Ganz rechts in der ersten Zeile befindet sich die Anzeige der aktuell eingestellten Zeit. Diese Anzeige blinkt, wenn die Belichtung angehalten wurde (Pause).

In der zweiten Zeile wird der aktuelle Status des Gerätes angezeigt. Der Status blinkt wenn die Belichtung aktiv ist, die LED’s also eingeschaltet sind.

Tastensteuerung, Betriebsmodi

Das Belichtungsgerät kennt vier Betriebsarten:

  • Initialisierung: wird nur einmal automatisch nach dem Einschalten aktiviert
  • Stop- oder Ruhemodus: Die LED’s sind aus und eine Belichtungszeit kann eingestellt werden. Das Display zeigt „>> set time“ an, wenn die Uhr auf 00:00 steht, sonst „>> start“.
  • Aktiv: wird das Gerät über die Start Taste. Die LED’s sind eingeschaltet und die Uhr wird herabgezählt. Das Display zeigt „running…“ an.
  • Pause: Wenn bei laufender Belichtung die Start-Taste erneut gedrückt wird, werden die LED’s ausgeschaltet und die Uhr wird angehalten. Es blinkt die Anzeige „[pause]“.
Abbildung 14 - Tastenfeld

In der Hauptschleife werden sequentiell alle 6 Tasten abgefragt, wobei die Start Taste nur aktiv ist, wenn die Uhr einen von 00:00 abweichenden Wert hat. Die Tasten zum Setzen der Zeit sind nur aktiv, wenn das Gerät im Stop- oder Pause-Modus ist. Die Zeit kann nicht verändert werden, wenn das Gerät gerade läuft, die Uhr also durch die ISR heruntergezählt wird.

Für die Abfrage der Tasten gibt es zwei Funktionen.

  • get_key_press() gibt den Status des abzufragenden Bits aus der Variablen key_press zurück und löscht das Bit. Da diese Variable nur bei steigender Flanke des entprellten Tastenstatus gesetzt wird (also beim Drücken), geben folgende Aufrufe immer den Status „nicht gedrückt“ zurück, solange bis die Taste erneut gedrückt wird. Da die Hauptschleife sehr schnell abgearbeitet wird, wird jeder Tastendruck damit nur einmal erfasst.
  • get_key_long() gibt nur den Status „gedrückt“ zurück, wenn die Taste länger als 1 s dauerhaft gedrückt wurde. Auch hier wird der Status in der ausgelesenen Variable key_hold zurückgesetzt. Im Gegensatz zur Funktion get_key_press() wird diese Variable jedoch bei anhaltendem Drücken der Taste von der ISR alle 100 ms neu gesetzt, was dazu führt, daß die Hauptschleife alle 100 ms einen weiteren Tastenklick erkennt.

Für die Start- und Stop-Taste wird nur die Funktion get_key_press() aufgerufen, da ein langes Drücken dieser Tasten keinen Sinn macht. Die Start-Taste schaltet den Zustand der LED’s über den Ausgangs-Pin bei Betätigung um und versetzt das Gerät damit entweder in den Aktiv-Modus oder den Pause-Modus.

Die Stop-Taste schaltet die LED’s immer ab und setzt die Uhr auf 00:00 zurück. Diese Taste kann auch bei nicht gestarteter Belichtung gedrückt werden um die Uhr auf 00:00 zurück zu setzen.

Die verbleibenden vier Tasten rufen jeweils beide Abfrage-Funktionen auf, wodurch man die Uhr leichter stellen kann, da die Zähler bei langem Drücken schneller laufen, als man die Taste manuell drücken könnte.

Es gibt Tasten für Minuten und Sekunden, jeweils aufwärts und abwärts. Über das Header File kann ein Limit eingestellt werden, das beim Stellen der Uhr einen Anschlag nach oben realisiert.

Stack

Der Stack ist eine kritische Größe. Der ATtiny2313 besitzt 128 Bytes RAM, was zunächst ausreichend scheint. Die in der Software verwendeten globalen und statischen Variablen, wie auch die vordefinierten Strings für die Anzeige am Display benötigen zusammen 78 Bytes, was man bei der Übersetzung der Software angezeigt bekommt. Für den Stack bleiben dann noch 50 Bytes.

Der Stack wird typischerweise am oberen Ende des RAM gestartet und wächst dann abwärts. Er wird zur Laufzeit durch Funktionsaufrufe und nicht globale Variablen anwachsen, weshalb der Compiler hier keine Fehlermeldung generieren kann, wenn der Stack überläuft. Wenn der Stack also in unserm Fall mehr als 50 Bytes benötigt, werden andere Variable im Speicher einfach überschrieben, was zu sehr interessantem Fehlverhalten führen kann. Die Suche nach solchen sporadischen Fehlern ist sehr zeitintensiv. Daher ist es unbedingt notwendig, im Vorfeld sicher zu stellen, daß der Stack niemals größer als 50 Bytes werden kann um einen Überlauf zur Laufzeit zu vermeiden.

Abbildung 15 - SRAM

Beim Compilieren kann man sich ein Assembler-Listing erstellen lassen (*.lss), das eine Überprüfung möglich macht.

  1. Jeder Funktionsaufruf und die ISR benötigen zwei Bytes auf dem Stack für die Rücksprungadresse. Ruft eine Funktion eine andere auf, addiert sich die Zahl der Bytes auf dem Stack.
  2. Der Compiler sichert beim Aufruf einer ISR oder einer Funktion häufig Register auf dem Stack, die in der Funktion bzw. ISR modifiziert werden. Jeder push-Befehl belegt ein weiteres Byte auf dem Stack.
  3. Die Funktionen können weitere Bytes auf dem Stack reservieren um z.B. kurzzeitig temporäre Variable zu sichern oder für lokale Buffer/Arrays. Da gibt es die abenteuerlichsten Algorithmen. So kann der Compiler einen imaginären Funktionsaufruf auf die nächste Zeile erzeugen, was den Stackpointer dann um zwei verringert. Das wird dann am Ende der Funktion wieder aufaddiert. Es können aber auch einfach nur Register mit push gesichert werden, die gar nicht verändert werden oder der Stackpointer wird einfach entsprechend am Anfang und Ende der Funktion modifiziert.
  4. Es ist somit der Bedarf jeder einzelnen Funktion zu ermitteln. Der Stackbedarf einer Funktion ergibt sich aus den zwei Bytes seines eigenen Aufrufes + Summe aller push-Befehle + aller Bytes durch SP-Manipulation + maximale Stackbedarf aller von der Funktion aufgerufenen Funktionen (Verschachtelung).
  5. Nach der Ermittlung des Stackbedarfs aller Funktionen addiert man den Bedarf der main() Funktion mit dem der ISR (die können immer parallel auftreten!). Die Summe darf nicht größer als 50 sein, besser weniger. Dann gibt’s keine Probleme.

Diese Aufgabe ist sehr mühsam, aber notwendig, da der Code je nach Compiler, Version und Optimierung, sehr unterschiedlich generiert wird. So kann das Programm mit dem einen Compiler übersetzt werden und laufen, während es mit einem anderen Compiler übersetzt nicht zuverlässig läuft. Bei mir ergab sich mit dem AVR-Studio 6 (gcc 4.6.2) und der Optimierung nach Größe (-Os) eine Programmgröße von 1582 Bytes und ein max. Stackbedarf von 36 Bytes, was das RAM gut auslastet.

Man kann bei der Analyse des generierten Assemblercodes eine Menge lernen und das Ergebnis des Compilers nutzen um den Code weiter optimieren.

Erhält man bei der Analyse ein unbefriedigendes Ergebnis, also mehr als 50 Bytes für den Stack, dann muß der Code optimiert werden. Dazu gibt es verschiedene Möglichkeiten:

Funktionen inline deklarieren
Das ist bei kurzen Funktionen die nur wenige Befehle generieren immer ratsam. Häufig optimiert der Compiler das auch selbst. Wenn eine Funktion nicht so oft aufgerufen wird, kann man auch etwas größere Funktionen inline deklarieren. Der Code wird dadurch natürlich größer, weshalb hier ein vernünftiger Kompromiß gefunden werden muß.
Verschachtelungen auflösen
Funktionen die andere Funktionen aufrufen verlangen viel Speicher auf dem Stack. Da gibt es eine Menge Optimierungspotential. Wenn eine Funktion nur andere Funktionen aufruft, kann man sie einfach löschen. Sequentielle Aufrufe kleinerer Funktionen entlasten den Stack.
Speicherverwendung prüfen
Funktionslokale Arrays werden auf dem Stack abgelegt. Diese sollten also so klein wie möglich gehalten werden. Ggf. kann man diese Arrays auch global machen, dann liegen sie fest reserviert im Datenbereich des Speichers (also im unteren Bereich). Das führt zwar dazu, daß der verfügbare Speicher für den Stack sinkt, aber der Speicher wird nicht zur Laufzeit dynamisch auf dem Stack reserviert und der Speicherverbrauch des Stacks wird kalkulierbarer.
Es empfiehlt sich immer, alle Informationen sofort zu verarbeiten bzw. auszugeben. Wenn die später nicht mehr benötigt werden, muß man sie auch nicht aufheben.

Fazit

Das hier vorgestellte Gerät ist seit langer Zeit im Einsatz und hat sich als praxistauglich erwiesen. Der Belichter liefert gute Ergebnisse und benötigt für eine fotobeschichtete Platine etwa 2,5 Minuten.

Leider gibt es noch einige verbesserungswürdige Punkte, die hier noch kurz erwähnt werden sollen:

  • Schlechte Leistungsbilanz: Das Gerät produziert zu viel Wärme und weist dadurch einen schlechten Wirkungsgrad auf. Etwa 3,5 Watt werden sinnlos an den Vorwiderständen der LED’s verbraucht. Dem gegenüber stehen 6,2 Watt, die von den LED’s verbraucht werden. Da auch diese nicht die gesamte Leistung in Licht umsetzen liegt der Wirkungsgrad also nur bei etwa 50%.
  • Der Aufwand ist sehr hoch. Die hohe Anzahl LED’s schlägt sich nicht nur in den Kosten, sondern auch in hohen Anforderungen an das Netzteil und dem Konstruktionsaufwand nieder. Auf Grund des geringen Abstrahlwinkels ist die Bauhöhe des Gerätes immer noch ziemlich groß.
  • Es gibt keine Schutzmechanismen bei zu hoher Wärmeentwicklung, was bei dem schlechten Wirkungsgrad notwendig wäre. Eine automatische Abschaltung oder temperaturabhängige Lüfterregelung zur Wärmeabfuhr wären wünschenswert.
  • Es können nur Materialien bis zum Europaformat belichtet werden.
  • Zweiseitige Belichtungen müssen in zwei Schritten erledigt werden. Gleichzeitiges Belichten beider Seiten ist nicht möglich.

In einer geplanten zukünftigen Version sollen diese Nachteile behoben werden.

Listings

/******************************************************************************/
/* LCD-Display Library - Header file	                         	      */
/*                                                                            */
/* Author:		Juergen Werner                                        */
/* Created:		14.03.2010                                            */
/* Last modified:	22.08.2012					      */
/* Version:			1.0                                           */
/* Copyright (c) by Trimension, Cologne                                       */
/******************************************************************************/
#ifndef LCD_TOOLS_H_
#define LCD_TOOLS_H_
#include <avr/io.h>
#include <stdint.h>
#include <util/delay.h>

#define delay(us)  _delay_loop_2 (((F_CPU/4000)*us)/1000);

inline void lcd_flush (void);
void lcd_write (uint8_t, uint8_t);
void lcd_cls (void);
void lcd_writetext (char *);
void lcd_cursor (uint8_t, uint8_t);
void lcd_writetime(uint8_t, uint8_t);
void lcd_init (void);

// constant definitions

#define HIGH_NIBBLE (uint8_t)0xF0
#define LOW_NIBBLE  (uint8_t)0x0F

// -- Port Definition --
#define LCD_DDR         DDRB
#define LCD_PORT        PORTB

#define LCD_ENABLE	PORTB2
#define LCD_RW		PORTB1
#define LCD_RS		PORTB0
//#define LCD_DATA_MASK	(uint8_t)((1<<PORTB4) | (1<<PORTB5) | (1<<PORTB6) | (1<<PORTB7))
#define LCD_PORT_MASK	(uint8_t)(HIGH_NIBBLE | (1<<LCD_ENABLE) | (1<<LCD_RW) | (1<<LCD_RS))

#define LCD_ADDR_OFFSET	0x80
#define LCD_ADDR_LINE0	0		// CTRL = HD44780 = 0; 	  KS0073 = 0
#define LCD_ADDR_LINE1	0x40	// CTRL = HD44780 = 0x40; KS0073 = 0x20
#define LCD_ADDR_LINE2	0x14	// CTRL = HD44780 = 0x14; KS0073 = 0x40
#define LCD_ADDR_LINE3	0x54	// CTRL = HD44780 = 0x54; KS0073 = 0x60

#endif /* LCD_TOOLS_H_ */
Listing 1 - LCD Bibliothek Header(lcd_tools.h)
/*****************************************************************************/
/* LCD-Display library 			                               	     */
/* Version for Multiline-Text-Displays			                     */
/*                                                                           */
/* Author:	Juergen Werner                                               */
/* Created: 	14.03.2010                                                   */
/* Last update:	22.08.2012						     */
/* Version:	1.0                                                          */
/* Copyright (c) by Trimension, Cologne                                      */
/*****************************************************************************/
#include <stdlib.h>
#include <string.h>
#include "lcd_tools.h"         

/*========================================================================
 * flush data to display (toggle enable line)
 *========================================================================*/
inline void lcd_flush ()
{
	LCD_PORT |= ( 1<<LCD_ENABLE ) ;		// Enable = HIGH
	asm("nop");
	LCD_PORT &= ~( 1<<LCD_ENABLE ) ;	// Enable = LOW
}

/*========================================================================
 * write one character (data/command) to the display 
 * @param 	rs=0 => Command, rs=1 => Character
 *========================================================================*/
void lcd_write (uint8_t data, uint8_t rs)
{
	uint8_t dataBits = LCD_PORT & ~LCD_PORT_MASK;
	if(rs) dataBits |= (1<<LCD_RS);				// write data (RS=1, RW=0)

	LCD_PORT = (dataBits | (data & HIGH_NIBBLE));	// High nibble first, incl. RS-Flag
	lcd_flush();
	LCD_PORT = (dataBits | (data << 4));		// then low nibble incl. RS Flag
	lcd_flush();
	delay(40);
}

/*========================================================================
 * clear display
 *========================================================================*/
void lcd_cls ()
{
	lcd_write(0x01,0);   	// B 0000 0000 => Clear
	lcd_write(0x02,0);
	delay(2000);			// needs another while
}

/*========================================================================
 * sets cursor to the given position 
 * @param line	- display line (0 or 1)
 * @param pos	- position in the line (0 .. 15)
 *========================================================================*/
void lcd_cursor (uint8_t line, uint8_t col) 
{
	switch(line) 
	{
		case 0:	lcd_write( LCD_ADDR_OFFSET + LCD_ADDR_LINE0 + col, 0 );
				break;
		case 1:	lcd_write( LCD_ADDR_OFFSET + LCD_ADDR_LINE1 + col, 0 );
				break;
	}
}

/*========================================================================
 * writes a data-string to the display at cursor position
 *========================================================================*/
void lcd_writetext ( char *text ) 
{
	uint8_t i = 0;
	while( text[i] ) lcd_write ((uint8_t)text[i++], 1);
}

/*========================================================================
 * prints time to the display (Format 00:00)
 * @param 	min, sec
 *========================================================================*/
void lcd_writetime (uint8_t min, uint8_t sec) 
{
	char bf[3];

	uint8_t n = min;
	
	for ( int i = 0; i < 2; ++i)
	{
		itoa(n, bf, 10);
		if(n < 10)
		{
			lcd_write((uint8_t)'0', 1);
			lcd_write((uint8_t)bf[0], 1);
		}
		else
		{
			lcd_write((uint8_t)bf[0], 1);
			lcd_write((uint8_t)bf[1], 1);
		}
	
		if(i == 0) 
		{
			lcd_write((uint8_t)':', 1);
			n = sec;
		}			
	}
}

/*========================================================================
 * display initialization (has to be called first)
 *========================================================================*/
void lcd_init() 
{
	LCD_DDR |= LCD_PORT_MASK;
	delay(20000);			// wait until display is ready

	LCD_PORT = 0x30;		// first set display to 8-Bit Mode
	lcd_flush ();
	delay(5000);			// wait somewhat

	LCD_PORT = 0x30;		// again
	lcd_flush ();
	delay(120);
	
	LCD_PORT = 0x30;		// and again
	lcd_flush ();
	delay(120);

	LCD_PORT = 0x20;		// now, set 4-bit mode
	lcd_flush ();
	delay(40);
	
	lcd_write(0x28,0);	// Function Set: 4-Bit, 2-line Display, 5x7 Dots/Char, RE-Bit off
	lcd_write(0x0C,0);	// Display on, Cursor off, Blink off
	lcd_write(0x06,0);	// Entry Mode Set: DD-RAM automatic inc., Cursor move
}
Listing 2 - LCD Bibliothek (lcd_tools.c)
/******************************************************************************/
/* Auto UV Exposer-Timer II - Header File  				      */
/*                                                                            */
/* Author:	Juergen Werner                                                */
/* Created:	14.03.2010                                                    */
/* Last update:	22.08.2012						      */
/* Version:	1.0                                                           */
/* Copyright (c) by Trimension, Cologne                                       */
/******************************************************************************/

// === Definition of MCU Speed ===
#ifndef F_CPU
#define F_CPU           1000000         // processor clock frequency
#warning kein F_CPU definiert, define 1 MHz
#endif

// === General Programs Constants ===
typedef enum { FALSE=0, TRUE=1} boolean;
#define MAX_TIME_MIN	30		// limit because of reducing heat

// === function prototypes =====
void power_on(void);			// toggle power line
void power_off(void);			// reset power line
void print_status(char*, boolean);
inline uint8_t is_timeset(void);	// check the counter for content
inline uint8_t is_power(void);		// check the power line
uint8_t get_key_press(uint8_t);		// get key press state and clear
uint8_t get_key_long(uint8_t);		// get long press state and clear

// === Timer Definitions ===
// we need to set the compare match register to get an ISR call interval of exactly 10 ms.
// F_CPU / Prescaler  = Clocks per Second, then divide by 100 to get the Value for 100 
// Clocks per Second
#define OCI_INITIAL	(uint8_t)(F_CPU / 64 / 100)	// results for 1 MHz = 156 (,25)
#define CLOCK_CNT_INIT	100		// Time Calibration (times of OCI-Calls for one second!)
#define BLINK_CNT_INIT	40		// Blink-Frequency
 
// === Port Definitions ===
// -- Key (input) Port and Key Constants --
#define KEY_DDR         DDRD
#define KEY_PORT        PORTD
#define KEY_PIN         PIND

#define KEY_START       PORTD5		// Pin 9
#define KEY_STOP        PORTD4		// Pin 8
#define KEY_MIN_UP      PORTD3		// Pin 7
#define KEY_MIN_DOWN    PORTD2		// Pin 6
#define KEY_SEC_UP      PORTD1		// Pin 3
#define KEY_SEC_DOWN    PORTD0		// Pin 2
#define ALL_KEYS        (uint8_t)(1<<KEY_START | 1<<KEY_STOP | 1<<KEY_MIN_UP | 1<<KEY_MIN_DOWN \
				| 1<<KEY_SEC_UP | 1<<KEY_SEC_DOWN )
 
#define LONG_PRESS_KEYS	(uint8_t)(1<<KEY_MIN_UP | 1<<KEY_MIN_DOWN | 1<<KEY_SEC_UP | 1<<KEY_SEC_DOWN )
#define LONG_PRESS_INIT    100          // activate after 1000ms
#define LONG_PRESS_REPEAT  10		// every 100ms
 
// -- Output Port Definition --
#define POWER_DDR	DDRD
#define POWER_PORT	PORTD
#define POWER_PIN       PORTD6		// Pin 11
Listing 3 - Hauptprogramm Header (exposer.h)
/******************************************************************************/
/* Auto UV Exposer-Timer II				       		      */
/*                                                                            */
/* Author:	Juergen Werner                                                */
/* Created:	14.03.2010                                                    */
/* Last update:	22.08.2012						      */
/* Version:	1.0                                                           */
/* Copyright (c) by Trimension, Cologne                                       */
/******************************************************************************/
#include <avr/io.h>
#include <avr/interrupt.h>
#include "exposer.h"
#include "lcd_tools.h"

/* key state bytes hold key state for each key on the apropriate bit-position) */
volatile uint8_t key_state;	// debounced and inverted key state (pressed = 1)
volatile uint8_t key_press;	// key press detect (only once at press down edge)
volatile uint8_t key_hold;		// long key press

volatile boolean blink_on = FALSE;	// is toggled in blinking frequency
uint8_t timeset_min = 0;		// timer counter (count down to zero) minutes part
uint8_t timeset_sec = 0;		// timer counter (count down to zero) second part
boolean pause = FALSE;

//-------------------------------------------------------------------------------------
// Interrupt Service Routine - Timer 0 Compare Match Interrupt
//
// Timer is configured to run in CTC mode. In this mode the TimerCounter will be reset
// to zero if compare match will be reached. The Timer Resolution is reduced. The
// Counter will run from 0 to OCR0A. This should be exactly all 10 ms by Configuration
//
// This ISR does:
// - debounce and save key state
// - get and save long pressed key state
// - set blink marker for blinking display prints
// - step down the main exposer timer and puts off power if counter is down
//-------------------------------------------------------------------------------------
ISR( TIMER0_COMPA_vect )                       // every 10ms
{
	static uint8_t ct0, ct1, rpt;
	static uint8_t cnt_clk = CLOCK_CNT_INIT;	// timer counter frequence adjustment
	static uint8_t cnt_blk = BLINK_CNT_INIT;	// blink signal counter
	uint8_t i;
	
	/***** key debouncer and status processing **********************************/
	i = key_state ^ ~KEY_PIN;			// determine key change information
	ct0 = ~( ct0 & i );				// counter LSB
	ct1 = ct0 ^ (ct1 & i);				// counter MSB
	i &= ct0 & ct1;					// count until roll over ?
	key_state ^= i;					// then toggle debounced state
	key_press |= key_state & i;			// 0->1: key press detect (once)
	
	/***** key holding (long press) **********************************************/
	if( (key_state & LONG_PRESS_KEYS) == 0 ) rpt = LONG_PRESS_INIT; // init counter
	if( --rpt == 0 )				// if hold time expired
	{
		rpt = LONG_PRESS_REPEAT;		// reset repeat counter
		key_hold |= key_state & LONG_PRESS_KEYS;	// and set status
	}

	/***** process blink flag ****************************************************/
	if( --cnt_blk == 0)				// blink flag counter (every 400ms)
	{
		cnt_blk = BLINK_CNT_INIT;		// reset counter
		blink_on ^= TRUE;			// toggle flag (mask)
	}

	/***** exposer timer (main function) *****************************************/
	if( --cnt_clk == 0)				// main counter (seconds)
	{
		cnt_clk = CLOCK_CNT_INIT;		// reset counter to initial value
		if( is_power() ) 			// when power is active
		{
			if( timeset_sec > 0) timeset_sec--;	// and time still not down, decrease
			else if( timeset_min > 0 )	// check seconds underflow
			{
				timeset_sec = 59;	// reset seconds to max
				timeset_min--;		// decrease minutes
			}
			if( !is_timeset()) power_off();	// if time was count down, set power off
		}
	}
}

//-------------------------------------------------------------------------------------
// get and reset key press status
//-------------------------------------------------------------------------------------
uint8_t get_key_press( uint8_t key_mask )
{
	cli();						// read and clear atomic !
	key_mask &= key_press;				// read status
	key_press ^= key_mask;				// clear status
	sei();
	return key_mask;
}

//-------------------------------------------------------------------------------------
// get and reset long key state
//-------------------------------------------------------------------------------------
uint8_t get_key_long( uint8_t key_mask )
{
	cli();						// read and clear atomic !
	key_mask &= key_hold;				// read status
	key_hold ^= key_mask;				// clear status
	sei();
	return key_mask;
}

//-------------------------------------------------------------------------------------
// prints out a text in the status line on the display
// @param  status - the text to be printed out
// @param  blink - flag indicating the text to be blink or not (1 - blink, 0 - normal)
//-------------------------------------------------------------------------------------
void print_status( char *status, boolean blink )
{
	lcd_cursor(1, 0);			// write position on second line
	int8_t len;
	if( blink && !blink_on)			// when blinking, clear field
	{
		len = 16;			// no of blanks to write
	}
	else
	{
		len = 16 - strlen(status);	// calculate no of remainnig blanks
		lcd_writetext(status);		// write message
	}
	for( int8_t i=0; i < len; ++i) lcd_write ((uint8_t)' ', 1); // filling rest of field
}

//-------------------------------------------------------------------------------------
// check the status of the exposer timer
// @return status - 1 counter > 0, otherwise 0
//-------------------------------------------------------------------------------------
inline uint8_t is_timeset()
{
	return (timeset_min | timeset_sec);
}

//-------------------------------------------------------------------------------------
// check the status of the power line
// @return status - 1 if power is on, otherwise 0
//-------------------------------------------------------------------------------------
inline uint8_t is_power()
{
	return ( POWER_PORT & (1<<POWER_PIN) );
}

//-------------------------------------------------------------------------------------
// toggle power line, and set pause flag
//-------------------------------------------------------------------------------------
void power_on()
{
	if( is_power() ) 				// if just running
	{
		POWER_PORT &= ~( 1<<POWER_PIN );	// stop
		pause = TRUE;				// mark as break
	}
	else
	{
		POWER_PORT |= 1<<POWER_PIN;		// otherwise start power
		pause = FALSE;				// reset flag
	}
}

//-------------------------------------------------------------------------------------
// reset power line
//-------------------------------------------------------------------------------------
void power_off()
{
	POWER_PORT &= ~( 1<<POWER_PIN );
	pause = FALSE;
}

//-------------------------------------------------------------------------------------
// main function
//-------------------------------------------------------------------------------------
int main( void )
{
	/***** init display ****************************************************/
	lcd_init();					// init display
	lcd_cls();
	lcd_writetext("UV Exposer");			// print title
	print_status("init...", FALSE);			// print status

	/***** init ports ******************************************************/
	KEY_DDR &= ~ALL_KEYS;				// configure key port for input
	KEY_PORT |= ALL_KEYS;				// and turn on pull up resistors

	POWER_PORT &= ~(1<<POWER_PIN);			// reset power pin
	POWER_DDR |= (1<<POWER_PIN);			// init as output
	
	/***** power savings **************************************************/
	ACSR = (1<<ACD);				// disable anacomp
	PRR = (1<<PRTIM1) | (1<<PRUSI) | (1<<PRUSART);	// disable USI/USART/Timer1
	
	/***** init timer ******************************************************/
	/* CLOCK - 1 MHz (CKSEL3..0)                                           */
	/* CKDIV8 - unprogrammed                                               */
	/* Timer Prescaler /64                                                 */
	/* Compare Match 156 (see Header)                                      */
	/* 1000000 Hz / 64 / 156 = 100 Hz => 10  ms			 	*/
	/***********************************************************************/
	TIMSK |= (1<<OCIE0A);			// enable compare match interrupt
	TCCR0A |= (1<<WGM01);			// set CTC mode
	TCCR0B |= (1<<CS01) | (1<<CS00);	// prescaler /64
	OCR0A = OCI_INITIAL;			// timer/counter resolution

	sei();				// activate global interrupts

	/***** program loop ****************************************************/
	while( TRUE ) {

		/***** handle start key ********************************************/
		if( get_key_press( 1<<KEY_START ) && is_timeset() )
		{
			power_on();		// if start key pressed, power on or pause
		}

		/***** handle stop key *********************************************/
		if( get_key_press( 1<<KEY_STOP ) )
		{
			power_off();		// if stop key pressed reset exposer
			timeset_min = 0;
			timeset_sec = 0;
		}

		/***** handle time setting (available only if not running) *************/
		if( !is_power() )		// time set only in stop or pause mode
		{
			/***** handle minutes up key ***************************************/
			if( get_key_press( 1<<KEY_MIN_UP ) || get_key_long( 1<<KEY_MIN_UP) )
			{
				// increment minutes only within the limit
				if(timeset_min < MAX_TIME_MIN) timeset_min++;
				if(timeset_min == MAX_TIME_MIN) timeset_sec = 0;
			}

			/***** handle minutes down key *************************************/
			if( get_key_press( 1<<KEY_MIN_DOWN ) || get_key_long( 1<<KEY_MIN_DOWN) )
			{
				if(timeset_min > 0)
				{
					timeset_min--;
				}
				else
				{
					timeset_sec = 0;
				}
			}

			/***** handle seconds up key ***************************************/
			if( get_key_press( 1<<KEY_SEC_UP ) || get_key_long( 1<<KEY_SEC_UP) )
			{
				if(timeset_sec == 59) 	// overflow
				{
					timeset_sec = 0;
					timeset_min++;
				}
				else
				{
					if(timeset_min < MAX_TIME_MIN) timeset_sec++;
				}
			}

			/***** handle seconds down key (only active if timer is stopped) ***/
			if( get_key_press( 1<<KEY_SEC_DOWN ) || get_key_long( 1<<KEY_SEC_DOWN) )
			{
				if(timeset_sec > 0)
				{
					timeset_sec--;
				}
				else if(timeset_min > 0)
				{
					timeset_min--;
					timeset_sec = 59;
				}
			}
		}

		/***** print out remaining time to display *************************/
		lcd_cursor(0,11);
		if(pause && !blink_on) lcd_writetext("     ");	// clear for blinking
		else lcd_writetime(timeset_min, timeset_sec);	// print time
		
		/***** print out exposer status to display *************************/
		if( is_power() )
		{
			print_status("running...", TRUE);
		}
		else if( is_timeset() )
		{
			if ( pause ) print_status("[pause]", FALSE);
			else print_status(">> start", FALSE);
		}
		else
		{
			print_status(">> set time", FALSE);
		}
	}

	return 0;	// to make lucky the compiler !
}
Listing 4 - Hauptprogramm (exposer.c)

Literaturverzeichnis

  1. Mikrocontroller.net - LCD-Ansteuerung Online Artikel über LCD-Ansteuerung
  2. Mikrocontroller.net - AVR-Tutorial: LCD Online Tutorial über LCD-Ansteuerung mit AVR
  3. F. Schäffer, AVR, Hardware und C-Programmierung in der Praxis, Elektor Verlag - Grundlagen und Hintergrundinformationen über die Anwendung der AVR-Mikrocontroller
  4. Peter Dannegger - Entprellung - Online artikel über die Entprellung von Tasten