Fortsetzung AVR-Tutorial: 9V-LED-Lampe Teil II

Winfried Mueller, www.reintechnisch.de, Start: 31.12.2005, Stand: 31.12.2005

Es geht weiter mit dem 9V-LED-Lampenprojekt. War der erste Artikel dazu da, die ersten Schritte im Umgang mit AVR-Prozessoren zu lernen, so verfeinern wir hier vor allem die Software. Es soll um eine ordentliche Tastaturentprellung gehen - etwas, was jeder früher oder später braucht, wenn er mit Mikrocontrollern hantiert. Aber auch mit dem eingebauten Comparator werden wir experimentieren.

Hardware-Erweiterung

(Rechte Maustaste > Grafik anzeigen zeigt den Schaltplan größer an)

Die Hardware hat sich ein wenig verändert. Wenn man Akkus für die Lampe einsetzt, so ergibt sich eine ungünstige Situation. Der Akku wird bis hinunter zu etwa 4 Volt leergesaugt, was ihm nicht sonderlich gut bekommt. Gut wäre es also, wenn der Prozessor sich darum kümmert, solche Tiefentladungen zu verhindern.

Die entstandene Lösung ist noch nicht optimal, aber einfach. Sie lässt sich mit dem ATtiny 12 bewerkstelligen. Hierzu verwenden wir den enthaltenen Comparator. Er vergleicht eine interne Referenzspannung von 1.22 Volt mit der Spannung an Portpin PB1.

Wie können wir das nutzen? In der ursprünglichen Schaltung sieht es nicht so rosig aus. Wir können nicht einfach den Portpin irgendwie an Plus der Batterie klemmen, weil dann auch im ausgeschalteten Zustand ein Strom fließen würde. Wir müssen vielmehr versuchen, irgendwie erst am Kollektor von T1 zu messen, weil der im ausgeschalteten Zustand entkoppelt ist. Nun wird aber gerade der Kollektor immer etwa die 3.4-3.6 Volt der weißen Leuchtdiode haben, was uns nichts nützt.

Die Lösung besteht darin, einen Widerstand in Reihe zur LED zu klemmen. Die Spannung, die an ihm abfällt, wählen wir so, dass bei Nennstrom wir eine Spannung von etwa 6-7 Volt am Kollektor haben. Was passiert dann? Fließt ein korrekter Strom, wie er durch die Stromquelle aufgeprägt wird, liegt der Kollektor immer auf z.B. 7 Volt. Sinkt jedoch die Eingangsspannung auf z.B. 6V ab, so kommt die Stromquelle in einen Bereich, wo sie nicht mehr als Stromquelle funktioniert. Sie ist außerhalb ihres Regelbereiches und der Transistor ist voll durchgesteuert, auch wenn die Spannung noch niedriger wird. Folglich sinkt die Spannung am Kollektor genauso ab, der Strom durch die LED auch. Bei 6 Volt Eingangsspannung bleiben so nur noch etwa 5.5 V am Kollektor übrig.

Wenn wir jetzt also die Spannung am Kollektor über einen Spannungsteiler so aufteilen, dass wir unterhalb der 1.22 V Referenzspannung dann rutschen, wenn die Stromquelle außerhalb ihres Regelbereiches ist, dann haben wir genau die Bedingung, die wir brauchen.

Die Schaltung sollte so dimensioniert sein, dass im Regelbereich der Stromquelle, die in den Prozessor eingespeiste Spannung über PB1 etwas über der Referenzspannung von 1.22 V liegt. Hier sollte man nicht zu knapp kalkulieren, um keine Fehlauslöser zu bekommen. Temperaturschwankungen können ja z.B. Parameter verschieben. Wir könnten z.B. die Spannung am Prozessor auf etwa 1.3 Volt halten, das macht dann 80mV Abstand, vorausgesetzt der Tiny12 hat auch wirklich die 1.22V Referenz. Zu hoch dürfen wir sie nicht wählen, weil sich sonst der Auslösepunkt zu einer sehr niedrigen Eingangsspannung verschiebt, was wir ja nicht wollen. Eigentlich wären 6.3-7Volt Eingangsspannung ein idealer Auslösepunkt. (0.9Vx7Zellen=6.3Volt)

Die Dimensionierung ist hier etwas kniffelig. R8, R9 und R10 müssen gut abgestimmt sein. R8 wählt man so groß, dass die Spannung am Kollektor im Regelbereich der Stromquelle bei etwa UBmin - UR2 = 7V - 0.7V = 6.3V liegt. R9 und R10 stimmt man dann so ab, dass bei dieser Spannung etwa 1.3Volt an PB1 ankommen. Man kann hierzu R10 mit 47 K fest auswählen und dann nur noch R9 abstimmen. Der Gesamtwiderstand R9 und R10 sollte nicht zu klein werden, um nicht sinnlos Strom zu verbrauchen. Ansonsten ist man hier recht frei in der Auswahl der Werte. Man kann natürlich auch einen Trimmer zusätzlich einbauen, mir war das aber zu aufwändig.

Normal versieht man den Eingangsping PB1 noch mit einem kleinen Kondensator, um Spikes und kurzzeitige Schwankungen zu filtern. Wir lassen das hier hauptsächlich aus dem Grund, dass so ein Kondensator die Programmierung des Chips unmöglich machen würde. Der Pin PB1 wird ja zusätzlich zum programmieren benötigt. Man müsste so einen Kondensator dann für die Programmierung z.B. mit einem Jumper abklemmen. Andererseits können wir aber auch sehr gut softwaremäßig Spikes oder Schwankungen eleminieren. Zumindest ausreichend für unseren Zweck hier.

Soweit zur veränderten Hardware. Alles andere ist gegenüber der ersten Version gleich geblieben.

Software

 
; 9V-Taschenlampe nach Idee vom 28.10.2005
; Interner RC-Oszi, Tiny 12
; Beginn: 28.10.2005, Stand: 30.12.2005
; Author: Winfried Mueller, www.reintechnisch.de  
; Changelog:
;   30.12.2005 
;     - Unterspannungserkennung
;     - Tastenentprellung im Timerinterrupt
;     - Universal-Wait-Schleife
; PORTB
; PB0: IN    P   NC
; PB1: IN    -   Unterspannungserkennung
; PB2: IN    P   NC
; PB3: IN    -   Switch (Pull-Down extern)
; PB4: IN    P/- On-Transistor, wird durch internen Pullup durchgesteuert 
; PB5: IN       Reset

.include "tn12def.inc"

; IO-PINS
.equ P_SWITCH    = 3
.equ P_ON        = 4


; Register Definition

;   General Purpose
.def a           = r16
.def b           = r17
.def c           = r18

;   Interrupt
.def i_a         = r19
.def sSREG       = r1

;   sonstig
.def key_0_longcount = r4   ;Counter für key_0 LongPressedEvent
.def low_u_count     = r5

.def sec_divider = r20      ;Timer-Divider Sekundentakt
.def cnt_s       = r21      ;Count Sekunden
.def cnt_m       = r22      ;Count Minuten

.def flags       = r23      ;General Flag Register

.def key_0       = r24      ;Bit 0..2 Prellcount
                            ;Bit 3: Keystatus (0=losgel., 1=gedrückt) 
                            ;Bit 4 Event Key OFF (losgelassen)
                            ;Bit 5 Event Key ON  (gedrückt)
                            ;Bit 6 Event Key LONG_ON (lange gedrückt)
                            ;(Bit 4/5/6 wird von Applika rückgesetzt 
                            ;  und von ISR gesetzt)


; Constants

.equ TM0_PRESET      = 209     ;kalibrieren auf Timereinsprung alle 10ms (256-x)
.equ LAMP_OFF_TIME_S = 0        
.equ LAMP_OFF_TIME_M = 10

.equ FLAG_SEC        = 0
.equ FLAG_MASK_SEC   = 0x01

.equ FLAG_LOW_U      = 1
.equ FLAG_MASK_LOW_U = 0x02

.equ KEY_EVENT_OFF     = 4
.equ KEY_EVENT_ON      = 5
.equ KEY_EVENT_LONG_ON = 6

.MACRO INIT_IO_ON
              ldi a, 0b00010101
              out PORTB, a
              ldi a, 0b00000000   
              out DDRB, a
.ENDMACRO

.MACRO INIT_IO_OFF
              ldi a, 0b00000101
              out PORTB, a
              ldi a, 0b00010000   
              out DDRB, a
.ENDMACRO

.MACRO INIT_TIMER
              ldi a, 0b00000100   ;Prescale Timer = 256 -> ca alle 213us
              out TCCR0, a        
              ldi a, TM0_PRESET   ;Timer-Counter Preset
              out TCNT0, a
              ; Set Oscillator Calibration default
              ldi a, 0x60
              out OSCCAL, a
.ENDMACRO

.MACRO RESET_TIME
              ldi sec_divider, 0
              ldi cnt_s, 0
              ldi cnt_m, 0
.ENDMACRO

.MACRO INIT_INT_ON
              cli

              ldi a, 0b00000000   ;kein External/Pin-Change Interrupt
              out GIMSK, a

              ldi a, 0b00000010   ;Timer Overflow Interrupt Enable
              out TIMSK, a

              sei
.ENDMACRO

.MACRO INIT_INT_OFF
              cli
.ENDMACRO

.MACRO INIT_COMPARATOR
              cbi ACSR, ACIE      ;No Interrupt
              cbi ACSR, ACD       ;Comparator enable
              sbi ACSR, AINBG     ;Internal Reference
.ENDMACRO

.MACRO INIT_SLEEP
              ldi a, 0b00100000  ;Idle
              out MCUCR, a
.ENDMACRO


.MACRO RESET_KEY
              ldi key_0, 0
.ENDMACRO

.MACRO RESET_KEY_EVENT
              andi key_0, 0b10001111
.ENDMACRO




;--------------------------------------
; Interruptvektoren
;--------------------------------------

              rjmp RESET
              rjmp EXT_INT0
              rjmp PIN_CHANGE
              rjmp TIMER0
              rjmp EE_RDY
              rjmp COMPARATOR


;--------------------------------------
; Interrupt Service Routine
;--------------------------------------
EXT_INT0:     reti

PIN_CHANGE:   reti

EE_RDY:       reti

COMPARATOR:   reti


TIMER0:       in sSREG, SREG
              ldi i_a, TM0_PRESET    ;Timer-Counter Preset
              out TCNT0, i_a
              ;---

              inc sec_divider
              cpi sec_divider, 100
              brne t0_l1    
              ; Endwert erreicht
              ldi sec_divider, 0
              sbr flags, FLAG_SEC
              inc cnt_s
              cpi cnt_s, 60
              brne t0_l2
              ; Sekundenüberlauf
              inc cnt_m
              ldi cnt_s, 0
t0_l2:
t0_l1:   
t0_kb:        ;-- Tastencheck
              sbrc key_0, 3 ; -> Taste gedrückt gemerkt?
              rjmp t0_l5
              ;-> Taste gemerkt losgelassen, Check, ob gedrückt
              sbis PINB, P_SWITCH
              rjmp t0_l8     ;->gemerkt losgelassen, ist losgelassen
              ;-> gemerkt losgelassen, ist gedrückt
              inc key_0
              mov i_a, key_0
              andi i_a, 0x07
              cpi i_a, 0x02
              brne t0_l4    ; -> noch kein Wechsel einleiten
              ;-> gilt jetzt als gedrückt, Event gedrückt
              andi key_0, 0b11111000
              ori  key_0, 0b00101000
              ldi i_a, 200             ;Init LongKeyPressedCount
              mov key_0_longcount, i_a
              rjmp t0_l4

t0_l5:        ;-> Taste gemerkt gedrückt, Check, ob losgelassen
              sbic PINB, P_SWITCH
              rjmp t0_l6    ;->gemerkt gedrückt, ist gedrückt
              inc key_0
              mov i_a, key_0
              andi i_a, 0x07
              cpi i_a, 0x03
              brne t0_l4   ; -> noch kein Wechsel einleiten
              ;-> gilt jetzt als losgelassen, Event losgelassen
              andi key_0, 0b11110000
              ori  key_0, 0b00010000
              rjmp t0_l4

t0_l6:        tst key_0_longcount
              breq t0_l8
              dec key_0_longcount
              brne t0_l8
              ;-> jetzt Null -> KeyLongPressed-Event
              ori key_0, 0b01000000

t0_l8:        ;->Merker und Tastenstatus gleich -> Prellcount Reset
              andi key_0, 0b11111000  ;Count Reset
              rjmp t0_l4     

t0_l4:        ;---
              out SREG, sSREG
              reti

RESET:
MAIN:         INIT_IO_ON
              RESET_TIME
              INIT_SLEEP
              INIT_TIMER
              INIT_INT_ON
              INIT_COMPARATOR

              clr flags
              clr low_u_count



              ; Check, ob Taster noch gedrückt, wenn nicht, dann war es ein Spike
              ; und das Gerät soll sofort wieder ausschalten
              sbis PINB, P_SWITCH
              rjmp m_l2

              ldi a, 200
              rcall WAIT_M         ;Warten damit Taste nicht sofort geprüft wird
              RESET_KEY_EVENT

m_l1:         sleep
              cpi cnt_m, LAMP_OFF_TIME_M
              breq m_l2
              sbrc key_0, KEY_EVENT_ON
              rjmp m_l3
              rcall Check_U
              sbrc flags, FLAG_LOW_U
              rjmp m_l2            ; Unterspannung -> Abschalten
              rjmp m_l1

m_l3:         ;Taste gedrückt, warte bis losgelassen
              sleep
              sbrc key_0, KEY_EVENT_OFF
              rjmp m_l2
              rjmp m_l3

m_l2:         ;Zeit abgelaufen/Taste gedrückt
              INIT_INT_OFF
              INIT_IO_OFF           ;jetzt sollte Prozessor ausschalten
              ldi a, 255
              rcall WAIT_M
              rjmp RESET            ;Reset, falls Fehler


;Check auf Unterspannung
;  erst, wenn n-mal hintereinander Unterspannung detektiert, wird
;  ein Unterspannungs-Event generiert
Check_U:      sbic ACSR, ACO
              rjmp cu_l2            ; Unterspannung -> Count
              clr low_u_count
              rjmp cu_l1
cu_l2:        inc low_u_count
              ldi a, 200
              cp low_u_count, a
              brcs cu_l1            ; noch kleiner MaxCount
              ori flags, FLAG_MASK_LOW_U  ;Unterspannung Event
cu_l1:        ret


;Zeitschleife
;Übergabe: a - Länge= 10ms * a (a:1..255)
WAIT_M:
WM_0:         ldi b, 0xFF
WM_1:         lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              lpm
              wdr
              wdr
              dec b
              brne WM_1
              dec a
              brne WM_0
              ret



Programmbeschreibung

Fangen wir mit den einfachen Veränderungen an. Die festen Zeitschleifen in der ersten Version wurden durch eine flexible Zeitschleife ersetzt (WAIT_M anstatt WAIT_2S oder WAIT_10MS). Mittels Register a übergibt man ein Wert, der mit 10ms multipliziert die Schleifendauer ergibt. Damit können wir jetzt flexibel Zeitspannen von 10ms bis etwa 2.5 Sekunden überbrücken.

In der Initialisierung wurde nun auch der Oszillator auf einen Standard-Korrekturfaktor gesetzt:

 
.MACRO INIT_TIMER
              ldi a, 0b00000100   ;Prescale Timer = 256 -> ca alle 213us
              out TCCR0, a        
              ldi a, TM0_PRESET   ;Timer-Counter Preset
              out TCNT0, a

hier -->      ; Set Oscillator Calibration default
              ldi a, 0x60
              out OSCCAL, a
.ENDMACRO

Ich wollte damit wegen korrekter Berechnung der Zeitschleifen die Prozessorfrequenz auf 1.2MHz kalibrieren. Defaultmäßig liefen meine Prozessoren eher zu schnell. Hier sollte man also kalibrieren, wenn die Zeitspanne der Einschaltdauer nicht korrekt ist.

Die Tastenentprellung ist sicherlich der aufwändigste Teil, der erneuert wurde. Sie geschieht im Timer0 Interrupt (Label TIMER0). Dort beginnt sie bei Label t0_kb. Alle 10ms wird ja in den Timer gesprungen. Das ist schon ein gut gewählter Wert, was Tastaturentprellung anbelangt. Ein Taste gilt nun dann als gedrückt, wenn sie für n Timereinsprünge als gedrückt erkannt wurde. Dabei wird bei jedem Einsprung, wo sie nicht gedrückt ist, der Entprellcounter wieder auf 0 gesetzt. So muss die Taste also n mal hintereinander als gedrückt erkannt werden.

Wird sie nun als gedrückt erkannt, wird im Register key_0 das Bit 3 gesetzt. Es ist sozusagen ein Merker für den aktuellen Tastenstatus. Bit 0-2 werden für den Entprellcount benutzt. Für n habe ich hier 2 eingestellt, die Taste wird also recht schnell als gedrückt erkannt. Das ist sinnvoll. Es gibt sogar Algorithmen, die bei der ersten Erkennung schon reagieren. Problematisch ist dabei, wenn irgendein Glitch auf der Leitung dafür sorgen kann, dass die Taste als gedrückt erkannt wird. Glitche oder Spikes wollen wir ja zuverlässig rausfiltern. Da Glitche eher kurz sind (im us-Bereich, auf jeden Fall kürzer als 10ms), können wir recht sicher davon ausgehen, das bei einem zweiten positiven Ergebnis die Taste wirklich gedrückt ist. Das zufällig ein zweiter Glitch genau nach 10ms auftaucht, ist extrem unwahrscheinlich.

Wird die Taste nun als gedrückt erkannt, wird zusätzlich noch ein Eventbit "KEY_EVENT_ON" gesetzt. Eine Unterscheidung zwischen Event und Zustand ist hier wichtig. Ein Event wird einmal ausgelöst, genau beim Übergang von losgelassen auf gedrückt. Die Anwendung kann dieses Bit auswerten, reagieren und dann dieses zurücksetzen. Das erleichtert uns die Programmierung, im Gegensatz zur Auswertung des aktuellen Tastenzustandes (Bit 3 von key_0).

Hier werden die Events immer durch die Interrupt-Service-Routine (ISR) gesetzt (ausgelöst) und von der Anwendung ggf. zurückgesetzt (verarbeitet). In aufwändigeren Systemen verwendet man Event-Queues, damit mehrere schnell hintereinander auftauchende Events nicht verloren gehen. Denn wenn hier zwei Tasten-On-Events eintreffen, bevor wir sie in der Anwendung verarbeitet haben, dann sieht die Anwendung nur einen Event. Der andere ist verlorengegangen.

Beim loslassen der Taste sind wir nun vorsichtiger, hier findet vor allem die Entprellung statt. Das Schließen der Taste geschieht ja unpräzisse, der Kontakt federt oder bei Schleifkontakten gibt es Momente, wo der Kontakt kurzzeitig wieder uinterbrochen ist. Hier wollen wir uns nicht unzählige KEY_ON_EVENT's einfangen.

Eine Taste gilt erst dann also losgelassen, wenn über n Timereinsprünge sie als losgelassen erkannt wird. Auch hier verwenden wir wieder Bit 0..2 von key_0 als Entprell-Counter. Wir setzen n auf 3. Auch hier ist es so: Wird auch nur einmal die Taste wieder als gedrückt erkannt, wird der Entprell-Counter auf 0 zurückgesetzt. Es braucht also schon 3 Einsprünge direkt hintereinander, wo die Taste als losgelassen erkannt wurde. Den Wert kann man noch bis zu 7 erhöhen, weil der Prellcount 3 Bit hat. Jedoch sollte man es nicht zu sehr ausdehnen, weil dann die Taste träge wird. Wenn jemand schnelle Eingaben macht, könnte es Probleme geben. Nicht jedoch hier in unserer Anwendung, da werden ja keine schnellen Eingaben gemacht.

Ist die Taste wirklich losgelassen, wird ein KEY_OFF_EVENT generiert und der jetzige Zustand der Taste in Bit 2 von key_0 gespeichert (0=offen). Die Anwendung kann also auch den Loslasszustand auswerten und reagieren. Tatsächlich machen wir das auch an einer Stelle.

Auf Events zu reagieren beinhaltet auch eine Gefahr. Verliert oder vergisst man irgendwo einen Event, dann könnte man in eine nie endende Endlosschleife geraten. Dies wäre z.B. der Fall, wenn wir an folgender Stelle dieses Programm geschrieben hätten:

 
m_l3:         RESET_KEY_EVENT
m_l99:        ;Taste gedrückt, warte bis losgelassen
              sleep
              sbrc key_0, KEY_EVENT_OFF
              rjmp m_l2
              rjmp m_l99

Mit RESET_KEY_EVENT löschen wir zuvor alle Events, was mitunter sinnvoll sein kann, um nicht auf ältere Tastendrücke zu reagieren. Hier warten wir jedoch unbedingt auf den KEY_EVENT_OFF, weil wir auch davon ausgehen können, das die Taste gedrückt sein muss. Wenn die Taste jedoch vor dem "RESET_KEY_EVENT" schon wieder losgelassen wurde (was theoretisch sein kann), löschen wir damit diesen Event. Jetzt wird kein erneuter KEY_EVENT_OFF kommen und die Schleife würde erst dann verlassen, wenn der Anwender erneut die Taste drückt und loslässt. Dies schonmal als Warnung, solche Zustände oder Bugs werden einem als Softwareentwickler irgendwann mal begegnen.

Als Schmankerl gibt es nun noch einen weiteren Event: KEY_EVENT_LONG_ON. Dieser wird ausgelöst, wenn die Taste für etwa 2 Sekunden geschlossen gehalten wird. So etwas kann sinnvoll sein. Auf Computertastaturen wird damit die Dauerfunktion einer Taste aktiviert, bei fast jeder Digital-Armbanduhr wird damit in einen Einstellmodus umgeschaltet (Bedienungsanleitung: "halten sie für 2 Sekunden die Einstelltaste gedrückt...").

Hierfür benötigen wir einen weiteren Counter (key_0_longcount), der 200 Einsprünge in den Timer abwartet, bevor er einen Event auslöst. Gesetzt wird er dort, wo auch der KEY_EVENT_ON gesetzt wird. Ist er bei 0 angekommen, löst er einmal den Event aus und verbleibt dann auf 0. Gesetzt wird er erst wieder, wenn ein erneuter KEY_EVENT_ON kommt.

Wir brauchen diese Funktionalität in unserer Anwendung nicht, aber die Tastenentprellung ist ja eher universeller Art. Beim Schreiben von Code sollte man sich immer überlegen, wie generisch die Lösung sein soll. Es geht dabei um Code-Wiederverwendung. Diese anzustreben ist oft sinnvoll. Und gerade eine Tastenentprellung schreit förmlich nach Wiederverwendung, hat doch fast jede Mikrocontrolleranwendung mindestens einen Taster zu bedienen. Wiederverwendung bedeutet Zeitersparniss und Code, der in der Qualität immer besser wird. Hier lohnt es sich, einmal mehr Zeit für einen generischen Code zu spendieren, als eine schnelle Lösung für einen konkreten Anwendungsfall zu generieren.

In dieser Art könnte man natürlich weitere Tasten einbinden. Für jede Taste bräuchte es 2 Byte, will man den KEY_EVENT_LONG_ON, ansonsten reicht 1 Byte. Allerdings sollte man indirekte Adressierung verwenden, um nicht n mal den gleichen Code schreiben zu müssen. Bei mehr als 3-4 Tasten lohnt sich ein Matrix-Konzept, wo man mit Zeilen- und Spaltenleitungen arbeitet. Diese Matrix wird dann regelmäßig gescannt.

In der Anwendung hat man es nun bequem, man braucht nur die EVENT-Bits auszuwerten, sie zurückzusetzen und entsprechend zu reagieren. Der Code im Timer-Interrupt nimmt uns also schon eine Menge Arbeit ab.

Kommen wir zum Comparator. Die Initialisierung machen wir wegen der Übersichtlichkeit in einem Macro:

 
.MACRO INIT_COMPARATOR
              cbi ACSR, ACIE      ;No Interrupt
              cbi ACSR, ACD       ;Comparator enable
              sbi ACSR, AINBG     ;Internal Reference
.ENDMACRO

Wir brauchen keinen Interrupt. Und die interne Bandgap-Referenz soll benutzt werden.

Folgend brauchen wir nur noch das Bit ACO im ACSR Register auszuwerten. Ist es gesetzt, haben wir Unterspannung und könnten die Lampe ausschalten.

Allerdings wäre das ungünstig. Auch hier könnten Glitches oder Spikes dazu führen, dass im wahrsten Sinne des Wortes "das Licht ausgeht wo wir eigentlich erleuchtet sein wollen".

Realisiert habe ich eine sehr vorsichtige Auswertung: Erst wenn 2 Sekunden lang alle 10 ms hintereinander die Spannung unterhalb des Schwellwertes liegt, gilt die Bedingung Unterspannung. Dies wird durch das Unterprogramm Check_U realisiert:

 
;Check auf Unterspannung
;  erst, wenn n-mal hintereinander Unterspannung detektiert, wird
;  ein Unterspannungs-Event generiert
Check_U:      sbic ACSR, ACO
              rjmp cu_l2            ; Unterspannung -> Count
              clr low_u_count
              rjmp cu_l1
cu_l2:        inc low_u_count
              ldi a, 200
              cp low_u_count, a
              brcs cu_l1            ; noch kleiner MaxCount
              ori flags, FLAG_MASK_LOW_U  ;Unterspannung Event
cu_l1:        ret

Aufgerufen wird es alle 10ms über die Hauptschleife. Nachfolgend wird in dieser Hauptschleife das FLAG_MASK_LOW_U ausgewertet. Bewusst ist hier eine lose Kopplung zwischen Unterprogramm und Hauptprogramm über ein Flag gewählt worden. Damit kann Check_U irgendwo, irgendwie in bestimmten Zeitabständen aufgerufen werden und irgendwo anders die Auswertung geschehen. Diese Art von Design ist damit recht flexibel, auch wenn es hier im konkreten Fall so nicht hätte sein müssen. Man hätte das Unterprogramm mit einem Rückgabewert versehen können, z.B. über ein gesetztes Carry-Flag.

Anregungen

Die Abschaltung bei Unterspannung schützt den Akku. Wenn man nun aber doch noch ein paar Minuten Licht braucht, ist die Lampe dazu nicht zu bewegen. Eigentlich wäre es besser, wenn man frühzeitig gewarnt würde und dann noch Zeit hat.

Solch eine Warnung könnte man mit einer Extra-LED bewerkstelligen. Die weiße Haupt-LED kann man leider nicht blinken lassen, weil die ja zur Versorgung des Prozessors immer bestromt sein muss. Man kann jedoch einen Portpin des Prozessors z.B. mit einem 330 Ohm Widerstand an Plus klemmen. Wird dieser Pin dann nach Masse gezogen, fließen darüber etwa 10 mA. Diese fehlen der LED, welche dann deutlich dunkler wird. So könnte man ein Blinken mit Heller-Dunkler hinbekommen. Ein paar Portpins sind ja dafür noch zu gebrauchen, die, die sonst nur für die Programmierung benutzt werden. Allerdings muss man evtl. mit Jumpern zusätzliche Bauteile entkoppeln, damit man den Chip noch programmieren kann.

Benutzt man einen anderen Chip mit AD-Wandler, so lässt sich viel komfortabler eine leerwerdende Batterie erkennen. Bei einem Comparator hat man ja nur einen Schwellwert, bei einem AD-Wandler kann man genauere Aussagen über die Spannung machen. So kann man schon frühzeitig erkennen, wann die Batterie leer wird, um noch einen längeren Übergangsbereich zu haben, bevor man die Lampe abschaltet. Mögliche Chips für diese Anforderung wären der ATtiny 13, ATtiny 15, ATtiny 26 oder der recht neue ATtiny 45.

Eine Einfachlösung für die jetzige Lampe, könnte darin bestehen: Die erste Minute nach dem Einschalten überprüft man die Unterspannung nicht. Damit hat man im Notfall zumindest die Möglichkeit, jeweils 1 Minute Licht zu haben.