Hallo zusammen
Ich möchte eine LED blinken lassen. Die Idee ist mit einem vorgegebenen
Wert j den Timer0 fallend zählen zu lassen bis er 0 erreicht und einen
Interrupt auslöst. Diese Lösung ist mir sympathischer als aufwärts
zählen zu lassen wobei man noch den Vorlauf bestimmen muss. Eine Routine
mit _delay_ms() will ich vermeiden.
1
{
2
while(0<j--);
3
LEDS_OFF;
4
j=112; // ~500ms
5
}
Leider bekomme ich die Routine nicht zum Laufen (siehe Anhang).
Hat jemand Rat? ... Bestimmt ;-)
Gruss
Firebird
Firebird schrieb:> Hallo zusammen>> Ich möchte eine LED blinken lassen. Die Idee ist mit einem vorgegebenen> Wert j den Timer0 fallend zählen zu lassen bis er 0 erreicht und einen> Interrupt auslöst.
Der Timer zählt immer aufwärts. Das ist Hardwaremässig vorgegeben und es
gibt nichts was du daran ändern könntest.
> Diese Lösung ist mir sympathischer als aufwärts> zählen zu lassen wobei man noch den Vorlauf bestimmen muss.
Muss man doch gar nicht.
Man muss nur den richtigen Timer nehmen und den auf CTC stellen.
> Eine Routine> mit _delay_ms() will ich vermeiden.
Löblich. Allerdings bei dir reichlich sinnlos, wenn du den Timer wie
einen _delay_ms Ersatz benutzt.
>>
Ich muss ein bischen ausholen.
Im Anhang ist der funktionierende Code wo ich _delay_ms() verwendet
habe. Jetzt möchte ich _delay_ms() durch eine Timer Routine ersetzen.
Erstens weil ich LEDs habe die dauern an sind und weil ich später noch
eine PWM Funktion implementieren möchte um die LED Helligkeiten
anzupassen.
Gruss
Firebird
Eine Art das zu machen ist eigentlich im Artikel [[AVR-GCC-Tutorial/Die
Timer und Zähler des AVR]] erklärt. Dort wird mit Interrupts und einem
signalisierendem Interruptflag zwischen ISR und Hauptprogramm
gearbeitet.
Alternativ kannst du auch einen Timer starten und interruptfrrei
arbeitet, indem du "händisch" in einem Anweisungsblock (while...) oder
einer Wartefunktion abfragst, ob der Timerzähler einen bestimmten Wert
erreicht hat.
In deinem Fall würde ich mit der ersten Variante arbeiten. Dabei würde
ich zwei Zeitschienen anlegen und den Timer in der ISR bei bestimmten
Zeitpunkten Aktionen ausführen lassen. Die Zeitschienen kann man z.B.
durch Felder (Arrays) automatisch finden und abarbeiten lassen.
Bsp. Zeitschiene für 0xc1
Zeit Aktion
====================
0 POSLEDS_OFF
500 BEACONLEDS_ON
580 LEDS_OFF
1000 TIMER_RESET
Bei der zweiten Zeitschiene (0xc3)
Zeit Aktion
=====================
0 POSLEDS_ON
STROBETAILLEDS_ON
80 LEDS_OFF
200 STROBELEDS_ON
280 LEDS_OFF
500 BEACONLEDS_ON
580 LEDS_OFF
960 TIMER_RESET
Damit diese Zeitpunkte "getroffen" werden, könnte der Timer mit einem
zeitlichen Abstand von 20ms laufen
> Gemäss deiner 'Alternative' interruptfrei könnte es in etwa so aussehen,> wobei j gemäss Timertakt angepasst werden muss:
Nein, so meinte ich das nicht.
Bei modernen Toolchains so wie AVR-GCC für AVRs kann vom Optimierer eine
einfache Zählschleife mit j u.U. gnadenlos weggeworfen werden (wenn j
nicht volatile ist) und/oder der Code kann so umstrukturiert werden,
dass das Hinfummeln eines exakten Zeitablaufs Glückssache wird und bei
der nächsten Sourcecodeänderung alles den Bach runter geht.
Die interruptfreie Alternative würde statt j genau den Timerzähler, also
z.B. bei Timer0 TCNT0 abfragen. Dabei muss der Timer vorher ins Laufen
gebracht werden (Taktquelle, Prescaler...) und der Start-/Endwert müssen
bekannt und berechnet werden.
Eine Ersatzwartefunktion mit Timerabfrage statt vorberechneten
Warteschleifen (delay..) könnte in etwa so aussehen:
1
// Atmega8
2
voidtimer0_warten(uint8_tticks)
3
{
4
TCCR0=(1<<CS00);// Prescaler: CPU-Takt/1 und Timer0 starten
5
TCNT0=0;// Timer0 Zähler resetten
6
while(TCNT0!=TICKS){}// warten
7
}
Den Prescaler dann so anpassen, dass längere Zeiten als 1 CPU-Tick
möglich sind.
Du gewinnst damit aber nichts gegeüber einer _delay_ms() Funktion. Das
Programm trödelt während ticks Takten immer noch in der Warteschleife
und fragt z.B. nicht "parallel" die Eingaben an PINC ab.
Deine Programmlogik verlangt dringend nach einer nebenläufigen
Zeitkontrolle per Interrupt. Der Timerteil des Programms kümmert sich um
die LED-Ansteuerung und der Hauptteil deines Programms kümmert sich um
die Eingaben und macht die LED-Vorgaben.
Helfer schrieb:> Deine Programmlogik verlangt dringend nach einer nebenläufigen> Zeitkontrolle per Interrupt. Der Timerteil des Programms kümmert sich um> die LED-Ansteuerung und der Hauptteil deines Programms kümmert sich um> die Eingaben und macht die LED-Vorgaben.
Danke für die Hinweise.
Hier ist mein erneuter Versuch die Programmlogik umzusetzen.
Die 'Timer0' Einstellungen muss ich überprüfen und die 'counter' Werte
nachrechnen.
1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3
4
#define LEDS_ON PORTD=0x44
5
#define LEDS_OFF PORTD=0x00
6
7
volatile unsigned int counter = 0;
8
9
ISR (TIMER0_OVF_vect)
10
{
11
counter++;
12
}
13
14
int main(void)
15
{
16
unsigned char input;
17
18
DDRC = 0x00; // Port C is Input
19
PORTC = 0xff; // Port C is "high" <-- pull-ups active
20
21
DDRD = 0xff; // Port D is Output
22
PORTD = 0x00; // Port D is "low"
23
24
// initiate Timer0
25
TCCR0 = (1<<CS00) | (1<<CS01); // Prescaler 64
26
TIMSK = (1<<TOIE0); // Overflow Interrupt Enable
27
28
sei(); //Enable Global Interrupts
29
30
while(1);
31
{
32
input = ~PINC; // LEDs on Anode (+5V) and Cathode on GRD; inverted
// 16-Bit Zuweisung ist nicht atomar deshalb ATOMIC_BLOCK
41
tcounter=counter;
42
}
43
if(tcounter>=0&&tcounter<500){
44
LEDS_OFF;
45
}elseif(tcounter>=500&&tcounter<600){
46
LEDS_ON;
47
}else{
48
counter=0
49
ATOMIC_BLOCK(ATOMIC_FORCEON)
50
{
51
// 16-Bit Zuweisung ist nicht atomar deshalb ATOMIC_BLOCK
52
counter=0;
53
}
54
}
55
}
56
}
57
}
Wenn das zufriedenstellend läuft kannst du den while-Anweisungsblock in
die ISR reinziehen. Dann sparst du das Kapseln der 16-Bit-Zugriffe mit
ATOMIC_BLOCK und dein Hauptprogramm (while-schleife) wird schlanker.
Achtung: Wenn in der ISR input abgefragt wird, muss diese Variable
auch volatile sein.
genau die willst du NICHT haben.
Du möchtest keine Warteschleifen haben.
Im Moment machst du folgendes:
Du stehst in der Küche und kochst Gulasch. Wie wir alle wissen, dauert
das 2 Stunden. In diesen 2 Stunden stehst du die ganze Zeit am Herd um
wartest jeweils 5 Minuten ab um einmal umzurühren.
Bist du mit dem Gulasch fertig, startest du die Waschmaschine. Die
braucht 1 Stunde 15 Minuten. In dieser Zeit stehst du ebenfalls neben
der Waschmaschine und wartest darauf, dass die fertig wird.
Genau das willst du eben nicht tun!
Was du tun willst:
Du bringst das Gulasch auf den Weg (schaltest den Herd ein).
Dann wechselst du zur Waschmaschine und startest die ebenfalls.
Die ganze restliche Zeit pendelst du ständig zwischen Herd und
Waschmaschine hin und her. Bist du beim Herd, siehst du dort (auf einer
Uhr) nach, ob seit dem letzten Umrühren 5 Minuten vergangen sind und
wenn ja rührst du kurz um und startest den 5 Minuten Wecker neu. Du
schaust auch nach, ob die 2 Stunden um sind und wenn ja, schaltest du
den Herd ab.
Auf jeden Fall aber belibst du nicht beim Herd stehen sondern du
erfüllst deine KURZE Kontrollaufgabe (mit möglicherweise einer kurzen
Aktion - Umrühren), und danach wechselst du zur Waschmaschine, um
nachzusehen ob die fertig ist. Dann beginnt das Spielchen wieder von
vorne: Herd - Waschmaschine - Herd - Waschmaschine.
Deine Vorgehensweise ist also
du schaust dir reihum alle Dinge an, die du überwachen sollst und siehst
nach ob es etwas zu tun gibt. Gibt es etwas zu tun, dann machst du das.
Aber auf keinen Fall wartest du darauf, dass irgendetwas fertig wird
sondern du wechselst sofort zur nächsten potentiellen Aufgabe und siehst
dort nach, ob es etwas zu tun gibt.
Und siehe da: plötzlich braucht kein Mensch mehr Warteschleifen! Auf
nichts und niemanden wird mehr gewartet, sondern wenn es etwas zu tun
gibt wird das erledigt. D.h. die beiden Dinge, Gulasch und
Waschmaschine, laufen nebeneinander her, ohne das eines der beiden
vernachlässigt wird. Wenn du umrührst, kann es sein, dass das Ende der
Waschmaschine mal kurz etwas warten muss, das ist aber nicht weiter
schlimm, denn auf die 5 Sekunden kommt es nicht an (und bei einem µC ist
das noch viel weniger schlimm, denn im Regelfall dauert eine Aktion ein
paar µs; andere Aufgaben werden also maximal mit ein paar µs Verzögerung
angegangen).
Dein Werkzeug, ist der Timer. Aber anders als du denkst. Mit dem Timer
baust du dir eine Zeitbasis, die du in der Hauptschleife als Taktgeber
benutzt. Aktionen, die du hast sind: LED einschalten, LED ausschalten.
UNd die definierst du jetzt in Zeitpunkten, die dein Taktgeber vorgibt.
Ist der jeweilige Zeitpunkt da, dann machst du die Aktion.
Beispiel: du möchtest 2 LED blinken lassen, aber die eine LED soll 1
Sekunden an und 2 Sekunden aus sein, während die andere 2 Sekunden an
und 2 Sekunden aus sein soll.
Als Zeitbasis wählst du einen Zähler, der von 0 bis 11 zählt. das
eergibt dir 12 Zeitstempel, die du wie folgt verteilst
0 LED 1 ein, LED 2 ein
1 LED 1 aus
2 LED 2 aus
3 LED 1 ein
4 LED 1 aus LED 2 ein
5
6 LED 1 ein LED 2 aus
7 LED 1 aus
8 LED 2 ein
9 LED 1 ein
10 LED 1 aus LED 2 aus
11
0 LED 1 ein, LED 2 ein
1 LE.....
nach diesen 12 Takten ist ein kompletter Zyklus durch. Wenn nach dem 12
wieder mit dem Takt 0 weitergemacht wird, läuft alles korrekt weiter.
Beide LED blinken im gewünschten Takt.
im Programm sieht das dann so aus
while( 1 ) {
if( time == 0 || time == 3 || time == 6 || time == 9 )
LED1_ON;
if( time == 1 || time == 4 || time == 7 || time == 10 )
LED1_OFF;
if( time == 0 || time == 4 || time == 8 )
LED2_ON;
if( time == 2 || time == 6 || time == 10 )
LED2_OFF;
}
(time ist eine Variable, die zb in einer ISR jede Sekunde um 1 erhöht
wird und bei 12 wieder auf 0 zurückgesetzt wird)
Das kann man so machen, muss man aber nicht. Es spricht auch nichts
dagegen, sich mit dem Timer in der ISR 2 Zeitgeber zu bauen, die genau
den gewünschten LED Zeiten entsprechen. Der eine zählt ständig von 0 bis
2 (3 Zeittakte) und der andere von 0 bis 3 (4 Zeittakte).
in der Hauptschleife wird dann jeder Zeitgeber geprüft, ob sein Ereignis
vorliegt
auch das ergibt das gewünschte asymetrische Blinken der LED. Und wieder:
nirgends wird gewartet. Diese Warteschleifen (egal ob du sie mit Timer
realsierst oder ob du _delay_ms nimmst), die sind dein Grundübel. Die
musst du loswerden! Und dazu musst du deine Denkweise umstellen. Nicht
mehr: Ich schalte ein, warte eine Zeitlang, dann schalte ich aus.
Statt dessen muss es lauten: In jedem Schleifendurchlauf ist ein bischen
Zeit vergangen; was gibt es jetzt, an diesem speziellen Zeitpunkt, alles
zu tun? Welche Ereignisse sind genau jetzt zu diesem Zeitpunkt
eingetreten und müssen bearbeitet werden?
Und wenn dieses Ereigniss behandelt wurde, dann wird der nächste
Ereignisgeber untersucht, ob es etwas zu tun gibt. Und dann geht das
ganze Spiel wieder von vorne los: ein Ereignisgeber nach dem anderen
wird abgeklappert ob es etwas zu tun gibt.
#define LED1_ON PORTB=0x15
#define LED1_OFF PORTB=0x00
#define LED2_ON PORTB=0x2a
#define LED2_OFF PORTB=0x00
Das wird so nichts.
Du kannst nicht mehr den ganzen Port einfach so umstellen. Du musst
schon gezielt ein einzelnes Bit (das mit der jeweiligen LED
korrespondiert) entweder setzen oder löschen.
Wenn du zb LED1_OFF ausführen lässt
POTB = 0x00;
dann löscht du damit ja auch gleichzeitig die LED2. Und das darf
natürlich nicht sein. LED2 ist ja unabhängig von LED1
Das hier
if( time == 4000 ) // 8sec
counter = 0; // Reset counter = 0
sprich das Rücksetzen des counter, das verlagerst du besser in die ISR
selber. Das ist dort besser aufgehoben.
ISR (TIMER0_OVF_vect)
{
counter++;
if( counter == 4000 )
counter = 0;
}
Und bei diesen Abfragen
if( time == 40 || time == 120 || time == 200 || time == 280 ) //
80ms,240ms,400ms,560ms
solltest du auch mal darüber nachdenken, ob man die nicht zuverlässiger
machen kann. (Ich weiß, ich hab dir das so vorgegeben).
Überleg mal: angenommen time ist bei einem Schleifendurchgang 39, d.h.
da passiert jetzt nichts. Aus irgendeinem Grund geht sich das jetzt aber
so aus, dass dein Hauptprogramm etwas zu viel Zeit braucht, so dass beim
nächsten Durchlauf durch die Schleife time schon 41 ist. Sprich: dein
Programm hat den exakten Zählerstand 40 gerade ein klein wenig verpasst.
Das bedeutet aber auch, dass die zugehörige Aktion dann nicht ausgeführt
wird. Das kann man auch so formulieren, dass es da kein Problem gibt.
Die LED1 muss ausgeschaltet sein, wenn der Zählerstand zwischen 40 und
80 liegt (40 inklusive, 80 exclusive) und natürlich bei den anderen
Zeiten entsprechend gleich
if ( ( time >= 40 && time < 80 ) ||
( time >= 120 && time < 160 ) ||
( time >= 200 && time < 240 ) ||
( time >= 280 && time < 320 ) )
LED1_OFF;
(und natürlich bei den anderen Abfragen genau gleich.)
Programme immer so formulieren, dass sich Problemstellen nicht
auswirken. Abfragen auf genaue Werte, überhaupt dann wenn diese Werte
durch einen Timer verändert werden, sind aber immer etwas problematisch.
Eine kleine Verzögerung im Programm genügt und man verpasst einen
bestimmten Zählerstand. Also immer so formulieren, dass sich dieses
Problem nicht auswirkt. Machst du im realen Leben ja auch: Wenn du umm
13:20:00 die Gartenbewässerung einschalten sollst und du verpasst den
Zeitpunkt um 2 Sekunden, dann drehst du trotzdem das Wasser auf, auch
wenn es eigentlich nicht mehr exakt 13:20:00 sondern schon 13:20:02 ist.
Die anderen Anpassungen habe ich auch gemacht.
Jetzt müsste theoretisch etwas geschehen, aber die LEDs bleiben dunkel.
Kann es sein das ich beim Timer0 etwas übersehen habe?
Danke und Gruss
Firebird
#define LED1_OFF PORTB &= ~(0<<PB0)
so kann man aber kein Bit löschen!
Als Merkregel kannst du dir merken:
eine 1 kann man entsprechend oft nach links schieben, dann wandert die 1
aber eine 0 kannst du nach links schieben so oft du willst, das Ergebnis
bleibt trotdem immer 0.
Oder etwas mahr mathematisch:
einmal nach links schieben entspricht einer Multiplikation mit 2, 2 mal
schieben wäre eine Multiplikation mit 4, 3 mal schieben wäre mal 8, etc.
Bei einem 1 Bit: 1 * 2 ergibt 2
1 * 4 ergibt 4
1 * 8 ergibt 8
....
Aber: bei einem 0 Bit 0 * 2 ergibt immer noch 0
0 * 4 ist ebenfalls 0
0 * 8 ist erst recht wieder 0
.... ist immer 0
Karl Heinz Buchegger schrieb:> #define LED1_OFF PORTB &= ~(0<<PB0)>>> so kann man aber kein Bit löschen!
Meine Konzentration lässt zu später Stunde nach ... Ich habe die Fehler
korrigiert. Irgenwo ist trotzdem noch der Wurm drin.
Ich hätte noch eine theoretische Frage.
Der Takt ist so berechnet, dass alle 2ms ein Overflow Interrupt erfolgt.
1
/* Interrupt Aktion alle
2
(8000000/64)/250 = 500Hz
3
bzw. 1/500s = 2,0ms
4
*/
5
6
TCCR0 |= (1<<CS01) | (1<<CS00); // Prescaler 64
7
TIMSK |= (1<<TOIE0); // Overflow Interrupt Enable
8
TCNT0 = 6; // Preload 256-250 = 6
Werden die LEDs genau nach den definierten Zeitvorgaben geschaltet oder
benötigt das Programm noch Takte die man bei zeitkritischen Abläufe
theoretisch berücksichtigen müsste? Abgesehen von kleine
Verzögerung/Abweichungen die weiter oben schon erklärt sind.
> Der Takt ist so berechnet, dass alle 2ms ein Overflow Interrupt erfolgt.
Der erste Overflowinterrupt kommpt nach 250*64/8000000 = 2 ms.
Die weiteren Overflowinterrupts kommen nach 256*64/8000000 = 2,048 ms,
weil TCNT0 in der ISR nicht auf 6 gesetzt wird.
Helfer schrieb:>> Der Takt ist so berechnet, dass alle 2ms ein Overflow Interrupt erfolgt.>> Der erste Overflowinterrupt kommpt nach 250*64/8000000 = 2 ms.>> Die weiteren Overflowinterrupts kommen nach 256*64/8000000 = 2,048 ms,> weil TCNT0 in der ISR nicht auf 6 gesetzt wird.
Aha, das leuchtet ein.
Was passiert wenn ich jetzt TCNT0 von 'int main(void)' in die ISR
verschiebe?
Gemäss meinen Überlegungen ist dann der erste Overflowinterrupt
immernoch die gewollten 2 ms.
TCNT0 ist nach dem Reset 0. Wenn du die Zeile TCNT0 = 6; aus main in die
ISR verschiebst, kommt der erste Overflowinterrupt nach 256
Zählschritten, d.h. 2,048 ms.
Ok, für mein Verständnis:
Beim Starten des Programms wird 'TCNT0 = 6' im 'int main()' gesetzt.
Danach geht es in die 'while(1)' Schleife.
Nach dem ersten Overflowinterrupt wird der 'counter' auf 0 gesetzt und
TCNT0 beginnt mit 0.
Weil wir uns in der 'while(1)' Schleife befinden muss 'TCNTO = 6' auch
im ISR definiert werden.
> Nach dem ersten Overflowinterrupt wird der 'counter' auf 0 gesetzt und> TCNT0 beginnt mit 0.
Ja. Bei jedem Overflowinterrupt wird TCNT0 von der AVR Hardware auf 0
gesetzt und zwar bevor die Anweisungen in der ISR ausgeführt werden.
Wenn du haben möchtest, dass der TCNT0 immer bei 6 startet (damit der
Overflow alle 250 Zählschritte = 2 ms kommt), muss der Code in der ISR
dem TCNT0 diesen Wert zuweisen.