Forum: Mikrocontroller und Digitale Elektronik Tasterentprellung aus AVR-Tut auf in eigenem Programm anwenden!


von Olli R. (downunderthunder42)


Angehängte Dateien:

Lesenswert?

Hallo ich habe ein Problem mit der Tastenentprellung?

eigentlich wollte ich nur die Tastenentprellung des AVR-Tutorials auf 
mein Programm anwenden.
Leider klappt das nicht so ganz:

Die Entprellung aus dem Tutorial ist in der Datei
"tasterentprellung.asm" zu finden

die andere Datei stellt die Hauptschleife meines Programmes dar.

in dem Programm wird jede Sekunde ein Timer1On COmpare Interrupt 
ausgelöst.

Ich hatte gedacht, dass ich statt der Wartezeit einfach meine 
Ausgabe-Befehle an das LCD einmal aufrufe, dann sollte doch genügend 
Zeit vergangen sein um den "TASTER_PORT" erneut einzulesen?

Leider gibt das Programm den Text, den ich ganz unten angegeben habe 
permanent aus (egal ob eine Taste gedrückt wurde oder nicht?)

von Olli R. (downunderthunder42)


Angehängte Dateien:

Lesenswert?

Ok falls es hilft hier das komplette Programm.

von Peter D. (peda)


Lesenswert?

Olli R. schrieb:
> Ich hatte gedacht, dass ich statt der Wartezeit einfach meine
> Ausgabe-Befehle an das LCD einmal aufrufe, dann sollte doch genügend
> Zeit vergangen sein um den "TASTER_PORT" erneut einzulesen?

Grrr.

Zeiten macht man nicht mit Vermutungen, sondern durch Wissen.
Entweder die Delay-Macros des Compilers benutzen oder mit nem Timer.
Mit Vermuten ist ein Programm schon im Ansatz falsch.


Peter

von Jürgen (Gast)


Angehängte Dateien:

Lesenswert?

Hallo Olli,

ich nutze eine andere Tasterentprellung, ich glaube die von Peter 
Dannegger.
Du solltest die Tasterenprellung als Unterprogramm einbinden 
(.include...), so mache ich es zumindest. Dann behält man leichter den 
Überblick und debuggen wird einfacher. Ich habe dir mal die Datei 
angehängt.

Wenn ich also eine Taste abfragen will, rufe ich das Unterprogramm 
Get_DigiTasten auf. Je nach Tastendruck wird dann eine Zahl von 1-5 in 
die Variable "zeichen" geschrieben. Wenn keine Taste gedrückt wurde 
steht der Wert 0x00 in der Variable. Im Hauptpgramm entscheide ich dann 
was bei welcher Zahl gemacht werden soll, ähnlich einer Case-Anweisung

Das Unterprogramm nutzt den Timer 2, außerdem müssen die Interrupts 
freigegeben werden. Die Definitiopn ist auskommentiert, weil ich mir 
diese immer in Hauptprogramm kopiere.

Ich hoffe das hilft dir weiter,

Gruß, Jürgen

von Olli R. (downunderthunder42)


Lesenswert?

Jürgen schrieb:
> Das Unterprogramm nutzt den Timer 2, außerdem müssen die Interrupts
> freigegeben werden. Die Definitiopn ist auskommentiert, weil ich mir
> diese immer in Hauptprogramm kopiere.
>
> Ich hoffe das hilft dir weiter,

Ja schon aber deine Lösung arbeitet ja auch mit nem Timer!
Da fand ich fürs Erste die Lösung von Peter Dannegger besser.
Dein Sache werde ich mir auch nochmal zu Gemüte führen.

Ich hab nun erstmal die Version "Tastenentprellung und Abfrage" von 
Peter Dannegger in mein Programm eingearbeitet. Das Programm mit diesem 
"get8key" (erstmal vorläufig morgen wird das dann noch übersichtlicher 
gestaltet)

Peter Dannegger schrieb:
> Grrr.
>
> Zeiten macht man nicht mit Vermutungen, sondern durch Wissen.
> Entweder die Delay-Macros des Compilers benutzen oder mit nem Timer.
> Mit Vermuten ist ein Programm schon im Ansatz falsch.

Ok das war wirklich ein Schnellschuß.
Aber ich wollte unbedingt vermeiden, deine Interrupt-Lösung mit dem 
get8key zu verwenden.
Jetzt verwende ich es doch und es scheint sehr gut zu funktionieren.

Eine Frage dazu habe ich allerdings noch:

Die Tastenentprellung verwendet ja den Timer0 und die zugehörige ISR 
wird alle 10ms aufgerufen!

Ich habe in meinem Programm aber noch einen zweiten Interrupt laufen!
Einen Interrupt On-Compare des Timers1. Dieser Interrupt wird einmal pro 
Sekunde ausgelöst!

Inwieweit beeinflusst denn nun mein Overflow-Interrupt an Timer0
meinen On-Compare-Interrupt an Timer1?

der Overflow-Timer0-Interrupt sperrt ja für die Dauer seines Aufrufs 
alle Interrupts?

Würde dann mein Interrupt-On-Compare seltener auftreten?

-->alle 10ms Overflow-Interrupt
--> alle 1000ms On-Compare-Interrupt

von Jürgen (Gast)


Lesenswert?

dort steht drin, dass kein Interrupt verloren geht, die ISRs werden 
nacheinander ausgeführt. Deswegen die ISRs so kurz wie möglich halten.

http://www.mikrocontroller.net/articles/Interrupt

von Hannes L. (hannes)


Lesenswert?

> Die Tastenentprellung verwendet ja den Timer0 und die zugehörige ISR
> wird alle 10ms aufgerufen!

Der Tastenentprellung ist es völlig egal, welcher Interrupt das Timing 
bereitstellt. Wichtig ist nur, dass die (Hintergrund-)Entprellroutine im 
festen Zeitraster von etwa 5 bis 30 ms aufgerufen wird.

> Ich habe in meinem Programm aber noch einen zweiten Interrupt laufen!
> Einen Interrupt On-Compare des Timers1. Dieser Interrupt wird einmal pro
> Sekunde ausgelöst!

Den 1s-Takt kannst Du aus dem Entprell-Takt ableiten. Wenn Dein 
Entprell-Takt (per Compare-Interrupt) alle 10 ms erfolgt, dann kannst Du 
darin neben der Tastenentprellung noch die "Hundertstelsekunden" 
hochzählen und bei Erreichen von 100 auf 0 setzen und die Sekunde 
hochzählen. So hast Du nur einen Timer-Interrupt für mehrere Dinge 
genutzt.

Angenommen, es käme noch ein Drehgeber (manuell betätigt) dazu, dann 
würde man den Interrupt auf 1 ms legen, darin den Drehgeber auswerten, 
über einen (Software-)Teiler (Zählregister) jede 10. Runde die Tasten 
entprellen und über einen weiteren Teiler jede 100. Tastenrunde (1000. 
Interrupt-Runde) die Uhr hochzählen.

Und wenn die Uhr noch wecken soll und das Wecksignal per Count-Down 
deaktiviert werden soll, dann kann dieser Count-Down auch noch durch 
diesen Interrupt synchronisiert werden. Sollte das Wecksignal (Piepsen) 
"zerhackt" (gepulst) werden müssen, so kann man dessen "Blinktakt" auch 
noch aus diesem Interrupt ableiten.

...

von Olli R. (downunderthunder42)


Lesenswert?

Hannes Lux schrieb:
> Den 1s-Takt kannst Du aus dem Entprell-Takt ableiten. Wenn Dein
> Entprell-Takt (per Compare-Interrupt) alle 10 ms erfolgt, dann kannst Du
> darin neben der Tastenentprellung noch die "Hundertstelsekunden"
> hochzählen und bei Erreichen von 100 auf 0 setzen und die Sekunde
> hochzählen. So hast Du nur einen Timer-Interrupt für mehrere Dinge
> genutzt.
>
> Angenommen, es käme noch ein Drehgeber (manuell betätigt) dazu, dann
> würde man den Interrupt auf 1 ms legen, darin den Drehgeber auswerten,
> über einen (Software-)Teiler (Zählregister) jede 10. Runde die Tasten
> entprellen und über einen weiteren Teiler jede 100. Tastenrunde (1000.
> Interrupt-Runde) die Uhr hochzählen.
>
> Und wenn die Uhr noch wecken soll und das Wecksignal per Count-Down
> deaktiviert werden soll, dann kann dieser Count-Down auch noch durch
> diesen Interrupt synchronisiert werden. Sollte das Wecksignal (Piepsen)
> "zerhackt" (gepulst) werden müssen, so kann man dessen "Blinktakt" auch
> noch aus diesem Interrupt ableiten.

Ok Viele Dank Hannes,

also ok so langsam bekomme ich eine Ahnung, wie man möglichst geschickt 
programmiert!

Um nochmal ganz kurz auf den Timer1-On_compare-Interrupt einzugehen:

wenn dieser Interrupt ausgelöst wurde und die ISR gerade abgearbeitet 
wird, wird dann trotzdessen, dass Interrupts gerade gesperrt sind der 
Timer1 hochgezählt (also schon begonnen, sich dem Verlgeichswert, bei 
dem der Interrupt ausgelöst wird, zu nähern?), so dass ich keine 
Verzögerung um die Abarbeitszeit der Befehle innerhalb der ISR habe?

von Hannes L. (hannes)


Lesenswert?

> wenn dieser Interrupt ausgelöst wurde und die ISR gerade abgearbeitet
> wird,

Dann hast Du beim Timing was Grundlegendes falsch gemacht.
Wenn der Compare-Interrupt erneut zuschlägt, ehe die ISR fertig ist, 
dann dauert die ISR zu lange.

> wird dann trotzdessen, dass Interrupts gerade gesperrt sind der
> Timer1 hochgezählt (also schon begonnen, sich dem Verlgeichswert, bei
> dem der Interrupt ausgelöst wird, zu nähern?), so dass ich keine
> Verzögerung um die Abarbeitszeit der Befehle innerhalb der ISR habe?

Diese Betrachtungen sind erst erforderlich, wenn ein Programm mehrere 
Interrupts hat. Deshalb legt man ja möglichst alle zeitabhängigen Dinge 
auf einen Timer-Interrupt zusammen. Das geht zwar nicht immer, aber hier 
(10 ms, 1 s) bietet es sich förmlich an.

Sind mehrere Interrupts nötig, dann muss man darauf achten, dass alle 
ISRs so kurz (so schnell) wie möglich sind. Denn je schneller eine ISR 
fertig ist, desto weniger müssen andere Interrupts auf ihre Abarbeitung 
warten.

Noch kurz zum Thema "gesperrter Interrupt". Ein Interrupt wird durch ein 
Ereignis ausgelöst. Das Ereignis kann z.B. sein: Timer-Überlauf, Timer 
Vergleich (Compare), Pegelwechsel am externen Interrupt-Pin, 
Pegelwechsel am Pin-Cange-Port, Zeichenempfang an U(S)ART, TWI oder SPI, 
..., siehe Interrupt-Vektorliste des jeweiligen AVRs.
Tritt das Ereignis ein, so wird das Interrupt-Flag (Pending-Flag) dieses 
Ereignisses gesetzt. Ist dieser Interrupt durch Setzen des 
Interrupt-Enabled-Flags freigegeben, dann wird zur ISR gesprungen. Dies 
erfolgt aber nur, wenn auch das I-Flag im SREG gesetzt ist. Bei jedem 
Sprung in eine ISR wird das I-Flag gelöscht, denn "es kann nur Einen 
geben". Durch RETI am Ende des ISR wird das I-Flag wieder gesetzt. Das 
Interrupt-Pending-Flag wird bei den meisten Interrupts (aber nicht bei 
allen!) durch den Aufruf der ISR gelöscht (genaue Hinweise gibts im 
Datenblatt). Wenn während einer ISR ein anderer Interrupt ausgelöst 
wird, so wird dieser nach Beenden der aktuell laufenden ISR ausgeführt, 
allerdings arbeitet der AVR zwischendurch noch einen Befehl des 
Hauptprogramms ab. Um diese Verzögerungen klein zu halten, macht man die 
ISRs so kurz (schnell) wie möglich. Dazu macht man in der ISR nur das 
Allernötigste (Daten sichern) und setzt ein Flag (eigene Bitvariable) 
als "Jobauftrag" für die Hauptschleife, die dann die Daten aufarbeitet. 
Wie das genau geschieht, ist stark vom Einzelfall abhängig. Ich habe 
z.B. die Dannegger-Bulletproof-Entprellung mal in der ISR und mal im 
Hauptprogramm laufen, je nachdem, was das Programm sonst noch so machen 
muss.

...

von Olli R. (downunderthunder42)


Lesenswert?

Hannes Lux schrieb:
> Tritt das Ereignis ein, so wird das Interrupt-Flag (Pending-Flag) dieses
> Ereignisses gesetzt. Ist dieser Interrupt durch Setzen des
> Interrupt-Enabled-Flags freigegeben, dann wird zur ISR gesprungen. Dies
> erfolgt aber nur, wenn auch das I-Flag im SREG gesetzt ist. Bei jedem
> Sprung in eine ISR wird das I-Flag gelöscht, denn "es kann nur Einen
> geben". Durch RETI am Ende des ISR wird das I-Flag wieder gesetzt. Das
> Interrupt-Pending-Flag wird bei den meisten Interrupts (aber nicht bei
> allen!) durch den Aufruf der ISR gelöscht (genaue Hinweise gibts im
> Datenblatt). Wenn während einer ISR ein anderer Interrupt ausgelöst
> wird, so wird dieser nach Beenden der aktuell laufenden ISR ausgeführt,
> allerdings arbeitet der AVR zwischendurch noch einen Befehl des
> Hauptprogramms ab.

Ok also ist in etwa so, wie ich es versucht habe zu erklären:

z.B. mein On-Compare-Timer1 löst z.B. bei einem Zählerstand von 10000 
einen Interrupt aus,
dann wird in die ISR gesprungen,
bei Verlassen der ISR hat mein Zähler z.B. den Zählerstand "50" erreicht 
und
wird während der "Main weiter bis 10000 erhöht?

Also beginnt mein Zähler nicht erst wieder bis zum Vergleichswert zu 
zählen, wenn der vorherige ISR-Durchlauf vollzogen wurde!

von Hannes L. (hannes)


Lesenswert?

Der Timer zählt (über den Vorteiler) die CPU-Takte (eigentlich die 
I/O-Takte, siehe Datenblatt, Kapitel zur Takterzeugung). Das macht er, 
solange der Vorteiler im Control-Register eingestellt ist. Dabei ist es 
egal, ob der AVR dabei eine ISR abarbeitet, oder die Mainloop, oder ein 
Unterprogramm oder ob sich der AVR im Sleep (Sleep-Mode Idle) befindet. 
Der Timer zählt die Takte, solange man ihm den Takt nicht wegnimmt 
(siehe Kapitel Timer und Kapitel Sleep im Datenblatt). Ich verweise 
deshalb auf das Datenblatt, da die Timer der AVRs unterschidlich 
ausgestattet sind.

Du benutzt einen Compare-Interrupt. Da gibt es auch wieder 
unterschiedliche Möglichkeiten.

Eine Möglichkeit ist der CTC-Modus des Timers. Bei ihm wird der Timer 
beim Erreichen des Compare-Wertes automatisch gelöscht. Damit erreicht 
man höchste Konstanz, auch wenn die ISR mal wegen eines anderen 
Interrupts etwas warten muss. Die Wartezeiten addieren sich nicht.

Manchmal braucht man aber mehrere Features des Timers und ist nicht 
bereit, auf die zweite Compare-Einheit und/oder die ICP-Einheit zu 
verzichten. Dann darf man den Timer nicht löschen oder anderweitig 
manipulieren, dann muss man ihn frei durchlaufen lassen.

Man könnte nun in der Compare-ISR den aktuellen Timerstand nehmen, das 
gewünschte Intervall (Zeitabstand biszum nächsten Interrupt) aufaddieren 
und als nächsten Interrupt-Termin (Compare-Wert) setzen. Das wird aber 
ungenau, da der Timer während des Aufrufs der ISR weiter läuft.

Genau wird es, wenn man das Compare-Register (das je eine feste Zahl 
enthält, während die Zahl im Timer-Register sich ständig verändert) 
einliest, das Intervall draufaddiert und ins Compare-Register 
zurückschreibt. Dann kann zwar gelegentlich der einzelne ISR-Aufruf 
etwas verzögert werden, die Fehler addieren sich aber nicht, da man ja 
das Intervall auf den Termin des aktuellen Interrupts aufaddiert und als 
Termin des nächsten Interrupts setzt.

Das war zwar etwas abschweifend, ich hoffe aber, es hilft trotzdem beim 
Verständnis der Timer.

...

von Olli R. (downunderthunder42)


Lesenswert?

Jo Vielen Dank Hannes,


ich bin jetzt allerdings auf ein weiteres Problem gestoßen:

und zwar wird der µC, immer wenn ich die Taste an PD0 drücke, resetet?

ich bin schon seit Stunden dabei, den Fehler zu finden.
Bislang ohne Erfolg.

also ich habe ja den PORTD als Eingang und die Internen 
PullUp-Widerstände aktiviert!

von Olli R. (downunderthunder42)


Angehängte Dateien:

Lesenswert?

Ich hatte die Asm-datei vergessen.

von Spess53 (Gast)


Lesenswert?

Hi

>ich bin jetzt allerdings auf ein weiteres Problem gestoßen:
>und zwar wird der µC, immer wenn ich die Taste an PD0 drücke, resetet?


>       brne  stell_wechsel    ; Subroutine stell_wechsel wird aufgerufen

>stell_wechsel:
>       dec   stellen_wechsel
>           brne  stell_wechsel_1
>           ldi   temp1, 7
>       mov   stellen_wechsel, temp1
>stell_wechsel_1:
>           swap  stellen_wechsel
>           ret

Fällt dir etwas auf?

MfG Spess

von Olli R. (downunderthunder42)


Lesenswert?

Spess53 schrieb:
> Fällt dir etwas auf?

Ne leider nicht aber ich beschreib das mal:

also

stellen_wechsel wird mit dem Wert 8 initialisiert!

>stell_wechsel:
>       dec   stellen_wechsel

dann wird stellen_wechsel dekrementiert
und hat den Wert 7

wenn ich beim Dekrementieren auf 0 "gestoßen" bin
wird über das Hilfsregister "temp1"

> brne  stell_wechsel_1
>           ldi   temp1, 7

stellen_wechsel wieder mit dem Wert 7 geladen!

zum Schluß werden die Nibbles miteinander getauscht

>           swap  stellen_wechsel
>           ret

die Zahl 7 oder 6 oder... 1 steht dann im HIGH-Byte

das High-byte wird dann mit der Taste 2 als dem Wert von PD1
verknüpft (addiert).

>       add   temp2, stellen_wechsel
>       cpi   temp2, 114
>       breq  sekunden_stellen

Also der Wert 7 ins High-Byte gepackt (112) und Taste 2 an PD1 gedrückt 
(2)
ergibt zusammen 114

Wenn der Wert 114 erreicht ist sollen z.B. die Sekunden zurückgesetzt 
werden!

So hatte ich mir das gedacht!?

von Spess53 (Gast)


Lesenswert?

Hi

Du springst aus deinem Loop die Routine an und versuchst mit 'ret' 
wieder zurüchzukommen. Das führt zum Absturz.

Ich habe vor kurzem für jemand anderes meine Routinen zum Thema 
Datum/Uhrzeit angehängt. Da sind auch solche Sachen, wie 
Schaltjahrberechnung, Feiertage dabei.

Beitrag "Re: Erste geh versuche mit AVR bitte mal anschauen"

MfG Spess

von Olli R. (downunderthunder42)


Lesenswert?

Ja Danke für den Hinweis.

Also deine Routine ist ja schon ziemlich umfangreich im Vergleich zu 
meinem noch recht unübersichtlichen Gehversuchen.

Aber alles zu seiner Zeit!

Ich hab gerade gemerkt, dass ich das etwas merkwürdig programmiert habe:

zuerst initialisiere ich meine Zählvariable für die Größe, die gerade
verstellt werden darf

> mov   stellen_wechsel, temp1

dann werden die Nibbles vertauscht

> swap  stellen_wechsel

> loop:
>  cli
>  mov   temp1, key_press
>  mov   temp2, key_press
>  clr   key_press
>  sei
>  andi  temp1, 1

dann wird Taste 1 abgefragt!

>  breq  loop_1

Ist diese Taste gedrückt wird das hier ausgeführt:

> stell_wechsel:
hier wird jetzt wieder "umgeswapt", weil ich hier nur die Zähvariable 
inkrementieren will. Alternativ könnte ich auch statt des "swap" und inc 
um 16 erhöhen
>  swap  stellen_wechsel
>  dec   stellen_wechsel

>  brne  stell_wechsel_1
>  ldi   temp1, 7
>  mov   stellen_wechsel, temp1
> stell_wechsel_1:
>  swap  stellen_wechsel

Dann wird die Zählvariable ins High-Byte gepackt!

> loop_1:
>  andi  temp2, 2
>  add   temp2, stellen_wechsel
dann wird die Zählvariable "stellen_wechsel mit dem aktuellen Zustand 
von Taster2 verknüpft (addiert)
>  mov   temp1, temp2
dann Frage ich meine Bedingungen ab, dazu muss der ursprüngliche Inhalt 
erhalten bleiben
>  cpi   temp2, 114
114 entspricht Zählerstellung von stellen_wechsel = 7
>  brne  loop_2
>  rcall sekunden_stellen

> loop_2:
>  mov   temp2, temp1
>  cpi   temp2, 98
98 entspricht Zählerstellung von stellen_wechsel = 6
>  brne  loop_3
>  rcall minuten_stellen


Aber bis ich herausgefunden hatte, dass ich
an der Markierung "stell_wechsel:
gleich ein swap stellen_wechsel einfügen musste
und dann natürlich gleich auch bei der Initialisierung
swappen muss, verging auch schon eine Weile.

Na egal dann werde ich das heute oder besser "demnächst" morgen oder so 
mal etwas übersichtlicher gestalten, auch wenn ich dann immer noch weit 
von deiner Uhren-Routine entfernt bin ;)

Mein Programm ist der VW Jetta und deins ein Porsche!

Spess53 schrieb:
> Ich habe vor kurzem für jemand anderes meine Routinen zum Thema
> Datum/Uhrzeit angehängt. Da sind auch solche Sachen, wie
> Schaltjahrberechnung, Feiertage dabei.

Zu den Feirtagen ich hab das mal überflogen:
Ich gehöre zu den Glücklichen, die wohl keine zusätzlichen Feiertage 
haben.
--> Mein Bundesland war nicht dabei;)

von Spess53 (Gast)


Lesenswert?

Hi

>Na egal dann werde ich das heute oder besser "demnächst" morgen oder so
>mal etwas übersichtlicher gestalten,...

Vielleicht solltest du dir mal überlegen, ob du nicht bestimmte Sachen 
von den Registern in den Ram verlagerst. Abgesehen davon, das ich ohne 
dieses .def-Gedödel glänzend auskomme, ist das nur bei kleineren 
Programmen praktikabel. Irgendwann brauchst du die Register mal für 
etwas anderes.

MfG Spess

von Olli R. (downunderthunder42)


Lesenswert?

Spess53 schrieb:
> Vielleicht solltest du dir mal überlegen, ob du nicht bestimmte Sachen
> von den Registern in den Ram verlagerst.

Ok ja später bestimmt aber soweit bin ich noch nicht!

das heißt ich würde einen bestimmten Wert, den ich momentan noch in 
einem Register speichere, das nur für diesen Zweck zuständig ist, von 
einem Arbeitsregister in den RAM speichere (z.B. Register inkrementieren 
dann den Registerinhalt an einem "bestimmten" Ort (Adresse) im Ram 
speichern, wenn dann dieser Wert z.B. für einen Vergleich herangezogen 
werden soll, kann man den Wert aus dem Ram wieder in ein Arbeitsregister 
laden.

Ist das mit dem Ram prinzipiell so gemeint?


das mit dem .def Gedödel würde sich vermutlich bei Nutzung des RAM 
erübrigen!?

Noch bleibe ich dabei!
Ich habe irgendwie Angst davor in einem Programm einen Fehler zu suchen 
nach dem Motto -> r17 ist 0 aber r 16 sollte den Wert 32 haben, wobei 
das 4te Bit von r18 gesetzt sein sollte.
Da e"rrrrrrrrrrrr"t es bei mir nur noch im Kopf.
Aber mit genügend guten Kommentaren sollte das wohl auch kein Problem 
mehr sein!

von Olli R. (downunderthunder42)


Angehängte Dateien:

Lesenswert?

@ Spess53:


Ich habe mir jetzt mal deine DateTime.asm genauer angeschaut:

bei der Isleapyear-Routine bin ich der Meinung, einen Fehler gefunden zu 
haben?
Ist das möglich?

in der angefügten Datei findest du zuerst meine fast identische Routine
und darunter deine.

Ich habe die Stelle, die mir aufgefallen ist mit ;***** markiert!


Gruß

Olli

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.