Sekunden-Task mit CTC-Timer (Atmega88)

Gast #6211681
Lesenswert?

Hallo zusammen,

bestimmt habt ihr eine Idee, was ich hier falsch mache:

Ziel:
Auf einem Atmega88 mithilfe des Timer0 im CTC-Modus jede Millisekunde 
einen Compare Match - Interrupt auslösen und somit die Basis für 
zyklisch aufzurufende Funktionen bilden.
Der Interrupt jede Millisekunde klappt soweit.

Problem:
Wenn ich den Inhalt der while(1)-Schleife in die ISR packe (wie man es 
ja eigentlich nicht tun sollte, Stichwort schlanke ISR..) toggelt PD4 
jede Sekunde - das gewünschte Ergebnis.
Wenn der Code jedoch wie hier beschrieben ist (d.h. die task_sekunde()) 
wird aus der while(1) aufgerufen, ergibt sich das Bild wie angehängt.
Jeder vierte Funktionsaufruf passiert zu früh! (Nach ca. 760ms anstatt 
1s).
Weiter unten der Code.

Hat jemand eine Idee? Danke im Voraus, bleibt gesund!

Grüße
Moritz

1
#define F_CPU 16000000UL
2

3
#include <avr/io.h>
4
#include <avr/interrupt.h>
5

6
void task_sekunde();
7

8
volatile uint16_t t = 0;
9

10
int main(void)
11
{  
12
  DDRD |= (1<<PD4);  // PD4 auf Ausgang einstellen                   
13
  PORTD |= (1<<PD4);  // PD4 setzen
14
  
15
  TCCR0A |= (1<<WGM01);  // CTC - Mode
16
  TCCR0B |= (1<<CS01) | (1<<CS00);  // Prescaler 1/64
17
  TIMSK0 |= (1<<OCIE0A);  // Timer Compare Match interrupt enabled
18
  OCR0A = 249;  // Wert fuer Ueberlauf, Frequenz 500Hz: (F_CPU / (2*500*64) - 1)
19
  
20
  sei();
21
  
22
  while (1) 
23
  {
24
    if (t >= 1000)
25
    {
26
      t=0;
27
      task_sekunde();
28
    }
29
  }
30
}
31

32
ISR(TIMER0_COMPA_vect)
33
{
34
  t++;  
35
}
36

37
void task_sekunde()
38
{
39
  PIND |= (1<<PD4);
40
}
Angehängte Dateien:
#6211699
Lesenswert?

Hallo,

wenn innerhalb Deiner while(1) Schleife in Interrupt auftritt, ist nicht 
sichergestellt, daß t gültige Werte aufweist. Such mal nach atomic 
Zugriff oder sperre selbst den Interrupt beim Zugriff, wenn das keine 
anderen IRQs stört.
Also
1
cli();
2
if ...
3
{
4
}
5
sei();

Um die Sperre möglichst kurz zu halten, wäre es sinnvoll, t in eine 
Hilfsvarable zu übernehmen und damit zu arbeiten:
1
cli();
2
uint16_t t1 = t;
3
sei();
4
if ...

Gruß aus Berlin
Michael
Gast #6211715
Lesenswert?

Du solltest task_sekunde() unter Interruptsperre ausführen.
Und Dir dann das Ergebnis noch einmal anschauen. Du wirst überrascht 
sein.
Die if Abfrage ist mit >= unscharf.
Da du bei 1000 triggern möchtest sollte dies > 999 lauten.
Gast #6211750
Lesenswert?

hufnala schrieb:
> Du gibst PIND an
Das ist bei diesem Controller eine einfache Möglichkeit, einen Portpin 
zu toggeln (1 in das PIN-Register schreiben). Deine Schreibweise ist 
aber sicher universeller!


@Michael, Stefan und Oldman:
Danke!! Das war's.
Ich hatte sowas vermutet, den Gedanken dann aber wieder verworfen, weil 
reproduzierbar
- jeder vierte Funktionsaufruf
- nach 760ms
kam.

Grüße
Moritz
Gast #6211758
Lesenswert?

> Die if Abfrage ist mit >= unscharf.
> Da du bei 1000 triggern möchtest sollte dies > 999 lauten.

Das macht keinen Unterschied.


> Das ist bei diesem Controller eine einfache Möglichkeit, einen Portpin
> zu toggeln (1 in das PIN-Register schreiben).

Durch das Odern schreibst du evtl aber mehr als eine 1 und änderst dann 
mehrere Pins.  Ein "=" reicht.
Gast #6211769
Lesenswert?

Vergessen: du hast drei Raceconditions: einmal das Auslesen von t, dann 
das Zurücksetzen (beides keine atomaren Zugriffe) und letztlich die Zeit 
zwischen lesen und rücksetzen (da kann dir ein Count verloren gehen).
Gast #6211777
Lesenswert?

Wenn du gleichmäßige Intervalle brauchst, würde ich t nicht auf 0 zurück 
setzen, sondern 1000 subtrahieren.

Denn stelle Dir mal vor, dein Hauptprogramm verpasst die richtigen 
Zeitpunkte weil es gerade mit anderen Warteschleifen beschäftigt ist 
(z.B. Ausgabe auf ein Display). Dann würde sich dieser Fehler beim 
einfachen Zurücksetzen immer weiter aufaddieren.

Wenn du kannst, dass benutze einen Quarz der "einfache" Zahlen wie 1024 
ermöglicht. Die lassen sich schneller berechnen.
Gast #6211789
Lesenswert?

Stimmt, guter Tipp - danke! :-)

Toll, wie schnell man hier qualifizierte Antworten bekommt, wenn man 
sich an ein paar Regeln hält. Lässt einen doch noch hoffen, dass es 
nicht nur Trolls im Internet unterwegs sind.
Gast #6211809
Lesenswert?

> Wenn du gleichmäßige Intervalle brauchst, würde ich t nicht auf 0 zurück
> setzen, sondern 1000 subtrahieren.

Noch besser: den Timer nie zurücksetzen (ISR erhöht, Anwendung liest 
nur) sondern dein "timeout" (eine extra Variable) um 1000 erhöhen.  So 
kann der einzelne Timer an beliebig vielen Stellen im Programm für 
beliebige Zeiten benutzt werden.  Insb bei Zeiten unterhalb 128 
Timercounts kann man atomar das LSB des Timers lesen und braucht nicht 
mal cli/sei.

Beispiel:
1
// in header:
2
extern volatile u32 systick;  // 100Hz
3
#define systick8 (*(volatile u8*)&systick)
4

5
// main:
6
...
7
void loop(void)
8
{
9
    static u8 t;
10

11
    if ((s8)(systick8 - t) >= 50) // .5s
12
    {
13
        t += 50;
14
        BLINK();
15
        ...
16
     }
17
     ...
18
}
Gast #6212170
Lesenswert?

foobar schrieb:
> den Timer nie zurücksetzen (ISR erhöht, Anwendung liest
> nur) sondern dein "timeout" (eine extra Variable) um 1000 erhöhen.

Das macht er doch längst so!

foobar schrieb:
> Insb bei Zeiten unterhalb 128
> Timercounts kann man atomar das LSB des Timers lesen und braucht nicht
> mal cli/sei.

Damit kann er TO nichts anfangen, weil er 1000 Millisekunden braucht. 
Die passen in das LSB nicht rein.
Gast #6212204
Lesenswert?

Hi, Danke die Möglichkeit kannte kannte ich nicht. Insbesondere dass ein 
|= das bit toggelt, und damit zum exor wird?

Auf das atomic bin ich nicht gekommen, da ich mit einem extra flag 
arbeite und den Zähler nur in der ISR bearbeite. Das Flag wird dann in 
der Hauptschleife abgefragt und zurückgesetztfrage. Kostet halt worst 
case einen weiteren uint oder in einer struct 1 bit und ein paar mehr 
Zugriffe, lässt aber alle ISR weiterlaufen.

Was ist besser/effizienter/sicherer?

//hufnala
Gast #6212227
Lesenswert?

hufnala schrieb:
> Insbesondere dass ein
> |= das bit toggelt, und damit zum exor wird?

Nein, da wird nichts zum xor.

Die Harware vieler ATmega Controller hat ein besonderes Feature, welches 
einen Ausgang toggelt, wenn man eine 1 in das zugehörige PINx Register 
schreibt. Mit C hat das gar nichts zu tun.

Aber wenn du schreibst "PIND |= 1" dann liest du das PIN Register (also 
den aktuellen Zustand des Portes) ein, setzt das Bit 0 auf 1 und 
schreibst das Ergebnis zurück. Das Ergebnis wird sein, nicht nur Bit 0 
getoggelt wird, sondern auch alle anderen Ausgänge, die vorher HIGH 
waren.

> Was ist besser/effizienter/sicherer?

Kommt auf das Programm an. In dem oben gezeigten Programm würde die 
Interrupts sperren um exklusiv Zugriff auf t zu bekommen. Nur wenn das 
aus irgend einem Grund nicht akzeptabel ist, würde ich es komplizierter 
machen.
Gast #6212239
Lesenswert?

Ein Flag zur Signalisierung des Überlaufs wäre auch mein Ansatz.
Das Flag wird in der ISR gesetzt, im Hauptprogramm getestet und wenn 
gesetzt, dann wird es zurückgesetzt und die Ausgaberoutine läuft 
einmalig durch.

Das entkoppelt die ISR vom Hauptprogramm.
Keine weiteren Klimmzüge erforderlich. Sauber und einfach.

Die verbrauchte Rechenzeit wird fast immer weniger als in der obigen 
Lösung sein. Doch da sind auch andere Szenarien denkbar.
Gast #6212275
Lesenswert?

1
 while (1)
2
   {
3
     if (flag_gesetzt)
4
     {
5
       task_sekunde();
6
       flag_gesetzt = 0;
7
     }
8
   }
9
 }
10
 
11
 ISR(TIMER0_COMPA_vect)
12
 {
13
   t++;
14
   If (t >= 1000){
15
     flag_gesetzt = 1;
16
     t = 0;
17
   }
18
 }
19
 
20
 void task_sekunde()
21
 {
22
   PIND |= (1<<PD4);
23
 }

Würde das stabil laufen?
So mache ich das in meinem Uhren Projekt.
Habe aber mehrere Sekunden Abweichung über den Tag. Kannst du den Code 
mit deinem DSO testen?
Gast #6212293
Lesenswert?

Chronisches Leid schrieb:
> Stefan ⛄ F. schrieb:
>> Wenn flag_gesetzt volatile und 8bit ist, dann ja.
>
> Ist es. Danke!
> Dann liegt's doch am Quarz.
> Aber warum müssen es 8 Bit sein?

Weil 16bit Variablen in zwei Schritten geändert werden. Ok, solange da 
nur 0 oder 1 drin steht, nutzt du effektiv doch nur 1 Byte davon.
Gast #6212300
Lesenswert?

Stefan ⛄ F. schrieb:
> Da ist seine Zwischenvariable. Der Timer wäre TCNT0.

Nee, nee. t ist defacto eine Softwareerweiterung des Timers.

Stelle dir einfach vor, dass irgendetwas im Hauptprogramm so lange 
dauert, dass die Abfrage auf (t >= 1000) erst bei t=1001 zum Zuge kommt. 
Wenn dann t auf 0 gesetzt wird, entsteht einen Zeitfehler von 1. Das 
passiert bei jedem längeren Durchlauf, so dass sich die Zeitfehler 
akkumulieren. Und genau dieses Aufsammeln wird vermieden, wenn man t 
nicht jedes Mal auf 0 setzt, sondern mit einem jedes mal um 1000 
steigenden Wert vergleicht, i.e. nur die ISR verändert t, sonst niemand.

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren