Forum: Mikrocontroller und Digitale Elektronik Richtiger Umgang mit Timer im Interruptbetrieb


von Mike (Gast)


Lesenswert?

Hallo zusammen,

ich habe ein "Detail"-Verständnisproblem im Umgang mit Timern.
Die Funktion, die ich programiert habe, läuft nicht so, wie ich es gerne 
hätte.

Ich schreibe mal was ich vorhabe, daher zeige ich keinen Code, es geht 
hier um das Verständnis.

Ich möchte zwei Ausgänge zeitabhängig schalten. Bisher habe ich immer 
delays benutzt, möchte aber zukünftig mit Timerinterups arbeiten.
Ablauf: Ausgang 1 50ms einschalten, 150ms warten, dann Ausgang 2 80ms 
Einschalten, wieder 150ms warten und dann evtl wiederholen.

Mein Gedankengang war: Timer arbeitet im CTC-Mode.
Die Abfolge Ausgänge schalten erledige ich mit der switch Anweisung, 
schalte dort den Timer ein und schreibe die Zeiteinheit in das OCRA 
Register. Jetzt warte ich in der Interruptroutine auf den Overflow, 
schalte dort den Timer aus und zähle die VAriable für die 
switch-Anweisung eins höher.

Ist dieser Ablauf richtig, oder wie löst man solche Probleme? Hier noch 
mal kurz zusammengefasst:
1
switch (variable)
2
case 1:
3
Ausgang 1 setzen
4
OCRA "laden" mit 50ms
5
Timer einschalten (CS-Register setzen)
6
switch-variable um eins erhöhen
7
break;
8
9
case2:
10
hier passiert nichts
11
ich warte lediglich auf den Interrupt, der die switchVariable um eins erhöht und den Timer stoppt.
12
daher steht hier lediglich "break"
13
14
case3:
15
Ausgang 1 rücksetzen
16
OCRA "laden" mit 150ms (Wartezeit)
17
Timer einschalten (CS-Register setzen)
18
switch-variable um eins erhöhen
19
break;
20
21
case4:
22
hier passiert nichts
23
ich warte lediglich auf den Interrupt, der die switchVariable um eins erhöht und den Timer stoppt.
24
daher steht hier lediglich "break"
25
26
usw...
27
//ende switch
28
29
In der Interruptroutine steht
30
ISR(TIMER1_COMPA_vect) //CTC Mode, Vergleich mit OCRA
31
{
32
  switchVariable um eins erhöhen (wurde im Code mit volatile definiert)
33
  Timer ausschalten (CS-Register auf 0 setzen)
34
}

von Daniel V. (danvet)


Lesenswert?

Wieso lässt du den Timer nicht mit 1ms laufen und verwendest Zähler?
Also TimerInterupt alle 1ms in dem ein Zähler hochgezählt wird..


Zähler == 0 --> Ausgang1 = HIGH
Zähler == 50 --> Ausgang1 = LOW
Zähler == 200 --> Ausgang2 = HIGH
Zähler == 280 --> Ausgang2 = LOW
Zähler == 330 --> Zähler = 0

: Bearbeitet durch User
von Karl H. (kbuchegg)


Lesenswert?

Mike schrieb:

> Die Abfolge Ausgänge schalten erledige ich mit der switch Anweisung,
> schalte dort den Timer ein

Diese Denkweise ist meistens schlecht.
Es gibt zwar Fälle in denen man Timer ein/aus schaltet, aber einem 
Anfänger würde ich davon abraten.

Der Timer wird am Programmanfang gestartet und läuft dann durch!
Genauso wie deine Armbanduhr auch die ganze Zeit tickt. Wenn du damit 
was stoppen willst, dann stellst du die ja auch nicht auf 0 und 
'startest sie', sondern du lässt sie die ganze Zeit weiterlaufen. Willst 
du 10 Sekunden stoppen, dann machst du das (in diesem Fall) anders. Du 
schaust auf die Uhr, siehst den Sekundenzeiger bei 28, rechnest im Kopf 
ganz schnell 28+10 macht 38 und dann schaust die immer wieder auf den 
Sekundenzeiger, ob der bei 38 ist.
Oder du schaust nicht auf den Sekundenzeiger, hörst aber die Uhr 
ticken,. Und dann zählst du mit jedem Tick den du hörst: 10, 9, 8, 7, 6, 
... 2, 1, 0.

Aber die Uhr an sich ... die läuft die ganze Zeit weiter.

Und genau das gleiche machst du auch mit dem Timer im µC.
vergiss dieses 'ich starte den Timer, ich stoppe den Timer' etc. Der 
Timer läuft durch! Die ganze Zeit!
Das was bei deiner Armbanduhr das 'Ticken' des Uhrwerks ist, dass ist 
bei deinem Timer der Aufruf einer ISR. Das ist dein Ticken. Richtest du 
den Timer so ein, dass eine ISR (hausnummer) alle 1 ms aufgerufen wird, 
dann ist deine Aufgabenstellung lediglich das Runterzählen von ein paar 
Zählvariablen bis auf 0 innerhalb dieser ISR und zugehöriges Schalten 
von Ausgängen.
(Ist eine Möglichkeit. Es gibt auch andere, wie du ja auch deine 
Armbanduhr auf verschiedene Art und Weise zum Stoppen von Zeiträumen 
benutzen kannst)


Edit:
Ich möchte noch hinzufügen, dass man deine konkrete Problemstellung 
natürlich auch so lösen kann, wie du das vorschlägst. Kann man. Aber die 
hat den Nachteil, dass du damit den Timer spezifisch auf nur diese eine 
Aufgabe festnagelst. Die andere Version ist die wichtigere. Denn mit der 
kannst du auch 100 Ausgänge nach irgendwelchen zeitlichen Mustern 
schalten lassen. Diese Technik verallgemeinert daher viel besser, wenn 
deine Aufgabenstellung mal anders aussieht.

: Bearbeitet durch User
von Mike (Gast)


Lesenswert?

Vielen Dank schon mal für die Antworten.

Aber wie stelle ich das konkret an?

Bei dem Schalten von Ausgängen warte ich auf ein Zeichen von "aussen": 
Ich frage mit meinem Programm einen Taster ab.
- dann wird der Ausgang geschaltet (ich merke mir die zeit)
- programm läuft weiter
->1 komme ich an dem punkt, an dem im programm der zähler abfragt wird, 
bin ich erst bei 49ms oder aber ich habe unter umständen schon 78ms 
erreicht
->2 verlagere ich die "intelligenz" in die ISR:
   50ms sind um, dann tue das, abhängig vom vorherigen zustand und 
welcher Ausgang kommt als nächstes?...
Folge: ich blähe die ISR auf, was ja nicht gewünscht ist

ich kriege momentan die kurve noch nicht, die ISR klein zu halten, die 
intelligenz im Programm zu verarbeiten aber die Vorgänge zeitgenau zu 
steuern

Mein Programm ist etwas größer und braucht für einen durchlauf ein paar 
ms

von Karl H. (kbuchegg)


Lesenswert?

Mike schrieb:
> Vielen Dank schon mal für die Antworten.
>
> Aber wie stelle ich das konkret an?

Indem du dir zb für jeden 'Zeitraum' eine Variable einrichtest.
Die könnte zb die Wertbedeutungen haben
1
-1  mach nichts, Zeit läuft nicht
2
 0  die Zeit ist abgelaufen
3
>0  die noch verbleibende Restzeit

In der ISR machst du dann
1
volatile int8_t  Zeit1;
2
3
ISR( ... )     // die richtest du zb für 1ms ein
4
{
5
  if( Zeit1 != -1 )      // läuft die Zeit überhaupt?
6
  {
7
    Zeit1--;
8
    if( Zeit1 == 0 )     // Zeit abgelaufen
9
    {
10
      ... mache was immer es zu machen gibt
11
      ... zb Pins abschalten, zb andere Zeiten starten
12
    }
13
  }
14
}

gestartet wird das ganze dann ganz einfach dadurch, dass du der 
Variablen Zeit1 einen Wert zuweist, nämlich die Anzahl der MIllisekunden 
nach der irgendwas wieder abgeschaltet werden soll.
Das einschalten machst du in der Hauptschleife, du setzt Zeit1 auf die 
gewünschte Zeit und innerhalb der ISR, wenn die Zeit abgelaufen ist, 
schaltest du den Ausgang wieder ab.
Ganz einfach.

: Bearbeitet durch User
von Daniel V. (danvet)


Lesenswert?

Mike schrieb:
> Vielen Dank schon mal für die Antworten.
>
> Aber wie stelle ich das konkret an?
> Mein Programm ist etwas größer und braucht für einen durchlauf ein paar
> ms

Dann würde ich das Schalten der Ausgänge in die ISR verlegen.

von Karl H. (kbuchegg)


Lesenswert?

> die ISR klein zu halten

Man muss nicht auf Biegen und Brechen ALLES aus einer ISR raushalten. In 
einer ISR darf ruhig auch ein bischen was gemacht werden. Nicht ekzessiv 
aber doch. Eine Variable abfragen, bei Bedarf dekrementieren und einen 
Ausgang schalten ist noch lange nicht exzessiv. Eine Ausgabe auf LCD 
oder UART wäre exzessiv. Aber den Fall hast du ja nicht.

von Ah. (Gast)


Lesenswert?

Man hat einen Zaehler(Variable), die wird im Interrupt, sofern ungleich 
null decrementiert. Im interrupt setzt man ein Flag(Boolean) wenn der 
Interrupt kam. Also muss man im main nur irgendwo :

 if (flag) {
  if (zehler=0) {...}
  flag=0
 }

von Daniel V. (danvet)


Lesenswert?

Karl Heinz Buchegger schrieb:
> gestartet wird das ganze dann ganz einfach dadurch, dass du der
> Variablen Zeit1 einen Wert zuweist, nämlich die Anzahl der MIllisekunden
> nach der irgendwas wieder abgeschaltet werden soll.
> Das einschalten machst du in der Hauptschleife, du setzt Zeit1 auf die
> gewünschte Zeit und innerhalb der ISR, wenn die Zeit abgelaufen ist,
> schaltest du den Ausgang wieder ab.
> Ganz einfach.

Er möchte aber eine definierte Sequenz fahren...

von Karl H. (kbuchegg)


Lesenswert?

Daniel V. schrieb:
> Karl Heinz Buchegger schrieb:
>> gestartet wird das ganze dann ganz einfach dadurch, dass du der
>> Variablen Zeit1 einen Wert zuweist, nämlich die Anzahl der MIllisekunden
>> nach der irgendwas wieder abgeschaltet werden soll.
>> Das einschalten machst du in der Hauptschleife, du setzt Zeit1 auf die
>> gewünschte Zeit und innerhalb der ISR, wenn die Zeit abgelaufen ist,
>> schaltest du den Ausgang wieder ab.
>> Ganz einfach.
>
> Er möchte aber eine definierte Sequenz fahren...

Niemand hindert ihn daran, innerhalb der ISR durch Zuweisung an eine 
2.te Zähle-Time-variable diesen 2.ten Zeitraum zu starten. Genau 
deswegen hab ich auch postuliert, dass ein Zählwert von -1 in der 
Time-variable die Bedeutung von (Zeit läuft momentan nicht) hat.

Das ist eine Möglichkeit.
Eine andere wäre es, alle Zeiten der Squenz zu addieren, einen 
entsprechenden Zähler dafür aufzusetzen und dann in der ISR anhand des 
aktuellen Zählwertes die entsprechenden Ausgänge zu schalten.

Viele Wege führen nach Rom.

: Bearbeitet durch User
von Karl H. (kbuchegg)


Lesenswert?

> Mein Programm ist etwas größer und braucht für einen durchlauf ein paar ms

Dann sieh zu, dass du als erstes mal alle _deley_ms los wirst. Jetzt 
weißt du auch warum delays (ausser in ganz kurzen Fällen, wie sie zur 
Protokollerzeugung machmal gebraucht werden), kein taugliches Mittel für 
echte Programme sind.
_delay_ms ist gut, weil Anfänger auch mal irgendwas brauchen um ihre 
Programm ins Laufen zu kriegen. Aber irgendwann ist der Punkt erreicht, 
an dem man einsehen muss, dass _delay_ms eine Sackgasse ist. Für die 
ersten Programme ok, aber irgendwann müssen die Stützräder am Fahrrad 
weg und man muss richtig Rad fahren.

von Thomas E. (thomase)


Lesenswert?

Karl Heinz Buchegger schrieb:

> -1  mach nichts, Zeit läuft nicht
>  0  die Zeit ist abgelaufen
> >0  die noch verbleibende Restzeit

> In der ISR machst du dann
1
volatile int8_t  Zeit1;
2
3
ISR( ... )     // die richtest du zb für 1ms ein
4
{
5
  if( Zeit1 != -1 )      // läuft die Zeit überhaupt?
6
  {
7
    Zeit1--;
8
    if( Zeit1 == 0 )     // Zeit abgelaufen
9
    {
10
      ... mache was immer es zu machen gibt
11
      ... zb Pins abschalten, zb andere Zeiten starten
12
    }
13
  }
14
}

Warum baust du da noch -1 ein?

1
if(Zeit1)
2
{
3
  Zeit1--;
4
  if(!Zeit1)
5
  {
6
    //mach was
7
  }
8
}

Das tut es doch genauso. Mit dem Unterschied, daß im nächsten Interrupt 
die Variable nicht auf -1 gesetzt wird, sondern auf 0 stehen bleibt.
Rein akademisch betrachtet, lässt sich Zeit dann sogar auf 255 setzen.

mfg.

von Daniel V. (danvet)


Lesenswert?

Thomas Eckmann schrieb:
> Rein akademisch betrachtet, lässt sich Zeit dann sogar auf 255 setzen.
>
> mfg.

255 ist doch -1

von Thomas E. (thomase)


Lesenswert?

Daniel V. schrieb:
> Thomas Eckmann schrieb:
>> Rein akademisch betrachtet, lässt sich Zeit dann sogar auf 255 setzen.
>>
>> mfg.
>
> 255 ist doch -1

Ach ja? Du hast überhaupt nichts verstanden.

mfg.

von Karl H. (kbuchegg)


Lesenswert?

Thomas Eckmann schrieb:

> Warum baust du da noch -1 ein?

gute Frage.
Frage ich mich im Moment auch.

Irgendwie hatte ich das Gefühl, dass eine Unterschiedung zwischen
* die Zeit läuft grundsätzlich nicht, und
* die Zeit ist abgelaufen
sinnvoll wäre.

Wenn die Auswertung innerhalb der Hauptschleife passiert, ist das 
manchmal sinnvoll. Aber hier sehe ich momentan auch nicht, was das 
bringen sollte.

von Mike (Gast)


Lesenswert?

Oh ha, vielen Dank!

Sehr viele neue Denkansätze.
Das hilft mir sehr, ich danke euch.
Jetzt muss ich nur noch mein Programm dahingehen umbauen.

Falls Probleme auftauchen melde ich mich noch einmal.

Die Delays habe ich mittlerweile alle raus.
Aber ich habe sehr viele ADC-Messungen mit Mittelwertbildung im 
Programm, die etwas Zeit benötigen.
Ich schaue mal, wie ich zurechtkomme.

von Karl H. (kbuchegg)


Lesenswert?

Daniel V. schrieb:
> Thomas Eckmann schrieb:
>> Rein akademisch betrachtet, lässt sich Zeit dann sogar auf 255 setzen.
>>
>> mfg.
>
> 255 ist doch -1

darum gehts nicht.
Es geht darum, dass ich einen speziellen Wert postuliert habe, für .... 
eigentlich nichts.
Es geht auch darum, dass ich dafür akzeptiert habe, den möglichen 
Wertebereich für die Zeiten von 255ms auf maximal 127ms einzuschränken.
Speziell letzteres ist oft eine schwerwiegende Limitierung.

von Daniel V. (danvet)


Lesenswert?

Thomas Eckmann schrieb:
> Daniel V. schrieb:
>> Thomas Eckmann schrieb:
>>> Rein akademisch betrachtet, lässt sich Zeit dann sogar auf 255 setzen.
>>>
>>> mfg.
>>
>> 255 ist doch -1

rein akademisch betrachtet.

>
> Ach ja? Du hast überhaupt nichts verstanden.
>
> mfg.

Doch, doch. Du hast 255 zur Verfügung, KH nur die Hälfte. Wobei man dann 
auch ein uint8_t Zeit1 verwenden könnte.

von Karl H. (kbuchegg)


Lesenswert?

Mike schrieb:

> Die Delays habe ich mittlerweile alle raus.
> Aber ich habe sehr viele ADC-Messungen mit Mittelwertbildung im
> Programm, die etwas Zeit benötigen.

OK.
Allerdings muessen die ja meistens nicht 'in einem Rutsch' gemacht 
werden.
Die Messungen kann man oft ja auch auf mehrere Durchgänge der 
Hauptschleife aufteilen.
Im Prinzip ist das eigentlich sowas wie ein allgemeines Prinzip in der 
µC-Programmierung. Mach in einem Durchgang durch die Hauptschleife eine 
Sache. Mache diese Sache gut und sieh zu, dass sie nicht zu lange 
dauert. Wenn du dazu einen Zähler mit dazunimmst, dann kannst du pro 
Durchgang 1 Sache machen, entsprechend mitzählen und nach n derartigen 
'Sachen' wertest du die gesammelten Daten aus.

Das ist so einer der Schlüssel zu Programmen, die scheinbar mehrere 
Dinge gleichzeitig machen können: keine exzessiv lang andauernden 
Teilprozesse. Oder salopp ausgedrückt: gewartet wird auf nichts und 
niemanden.

: Bearbeitet durch User
von Ah. (Gast)


Lesenswert?

>Aber ich habe sehr viele ADC-Messungen mit Mittelwertbildung im
Programm, die etwas Zeit benötigen.

Ein Mittelwert ist eine eher einfache Sache : Siehe auch
http://www.ibrtses.com/embedded/exponential.html

Ein paar additionen und shifts pro messwert.

von Thomas E. (thomase)


Lesenswert?

Mike schrieb:

> Die Delays habe ich mittlerweile alle raus.
> Aber ich habe sehr viele ADC-Messungen mit Mittelwertbildung im
> Programm, die etwas Zeit benötigen.

Müssen sie nicht.
Man muß nicht n Werte einlesen, aufaddieren, dividieren. Dann die 
nächsten n Werte und so weiter und so fort...

Sondern man liest einen Wert ein, addiert auf und schreibt den 
eingelesenen Wert in ein Array der Grösse n. Aus dem Array holt man sich 
den ältesten Wert und subtrahiert ihn vom noch nicht geteilten 
Durchschnittswert.

Dann besteht Mittelwertbidung aus einmal Addieren, einmal Subtrahieren 
und einmal Dividieren und nicht n Mal Addieren und einmal Dividieren. 
Dividieren sollte man auch nicht durch 10 oder irgendeinen anderen 
krummen Wert. Sondern immer durch Zweierpotenzen 8, 16, 32... Damit wird 
nur geschoben statt dividiert.

Auf den ADC muß man auch nicht ständig warten, sondern erledigt das in 
der ADC-ISR. Kleinigkeiten kann man nämlich sofort erledigen.

Kostet natürlich ein bisschen RAM. Für einen Attiny13 ist das nichts. 
Aber beim Atmega gibt es für nicht verwendeten RAM kein Geld zurück. Und 
wenn man nicht zum Atmega8 gezwungen wird, kann man auch einen 328 mit 
doppelt soviel RAM verwenden. Falls es doch knapp wird.

Man kann sich das Array aber auch sparen, indem man, an Stelle des 
ältesten Wertes, vom Durchschnittswert Durchschnittswert/n abzieht. Dann 
wird es allerdings ein bisschen ungenauer und kostet durch die weitere 
Division(Shift) etwas mehr Rechenzeit.

mfg.

von Karl H. (kbuchegg)


Lesenswert?

Thomas Eckmann schrieb:

> Auf den ADC muß man auch nicht ständig warten, sondern erledigt das in
> der ADC-ISR.


Ich mach das gerne auch schon mal so, dass ich die Reihenfolge 
vertausche
1
...
2
3
    Wert holen
4
    ADC für den nächsten Messwert starten
5
    Wert verarbeiten
6
....


d.h. der ADC arbeitet für sich eigenständig dahin, während das Programm 
was anderes macht. Kommt das Pgm beim nächsten Durchlauf durch die 
Hauptschleife wieder zur Stelle der ADC Abfrage, dann ist der ADC längst 
wieder fertig, ich hol mir den Wert und starte sofort die nächste 
Messung. Die läuft dann, während der zuletzt geholte Wert bearbeitet 
wird und das Pgm wieder was anderes macht.
Im Grunde besteht der 'Trick' an dieser Stelle einfach nur in der 
Erkenntnis, dass der ADC ja auch ganz alleine arbeiten kann und dabei 
das Programm nicht benötigt.

von Thomas E. (thomase)


Lesenswert?

Karl Heinz Buchegger schrieb:
> Im Grunde besteht der 'Trick' an dieser Stelle einfach nur in der
> Erkenntnis, dass der ADC ja auch ganz alleine arbeiten kann und dabei
> das Programm nicht benötigt.

Bei ADC-Kleinigkeiten, wie Potis einlesen oder Batteriespanung messen, 
benutze ich den ADC-Interrupt meist gar nicht, sondern mache das in der 
Timer0-ISR, indem ich den ADC mit Timer0 triggere. Bei einem üblichen 
1-ms-Systick ist der ADC beim Eintritt in die Timer-ISR (schon 
lange)fertig und der ADC-Wert kann dort verarbeitet oder zumindest zur 
weiteren Verwendung eingelesen werden. Spart eine ISR und den damit 
verbundenen üblichen Overhead.

Zum Videos digitalisieren ist das natürlich zu langsam. Aber meistens 
reicht das. Handbediente Potis muss man nicht 100000 Mal in der Sekunde 
einlesen.

mfg.

: Bearbeitet durch User
von Mike (Gast)


Lesenswert?

Dieser Thread entpuppt sich als wahrer Wissenspool,bzw. Wissensquelle 
für effiziente Programmierung!
Ich bin begeistert und lese gerne mit - jeden Beitrag. Interessant ist 
auch die unterschiedliche Sichtweise.

Ich habe am WE mein Programm umgebaut. Ich hatte eigentlich noch fragen, 
aber durch weitere diskussionen und Anregeungen hier im Thread, konnte 
ich mir selbst helfen.

Danke und Gruß
Mike

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.