Forum: Mikrocontroller und Digitale Elektronik Zeit, Timer, in den Griff bekommen


von Peter M. (sirpete83)


Lesenswert?

Hallo alle zusammen,
habe schon einige kleine Sachen mit dem 89C4051 programmiert und auch 
hinbekommen.
Jedoch waren es immer kleine Aufgaben, welche nicht unbedingt Zeit 
relevant sind / waren.

Jetzt programmiere ich erfolgreich mit dem SDCC C-Compiler, bis jetzt 
kann das Programm nicht viel:

Timer0 mit 16-Bit: preload ist so eingestellt, dass ich alle 0.05 s 
einen Interrupt erhalte, in der Interrupt Routine wird der Timer wieder 
mit den entsprechenden Werten für 0.05 ms nachgeladen.

Jetzt hab ich Probleme damit richtig zurecht zu kommen, die "neue" 
Zeitbasis richtig einzusetzen; ich habe vor zum Beispiel alle 500 ms 
einen Port-Pin abzufragen.
Ebenfalls soll die Zeitbasis auch für einen Signalgeber benutzt werden, 
12 Piepser soll mit 1 oder 2 Hz an und aus gemacht werden für etwa 10 s 
...
usw.

Mir fehlt die entscheidende Idee wie ich dass ganz in den Griff bekomme:

Meine Idee war, dass ich in der Timer Interrupt Routine eine Zähl - 
Variable hochzähle, sagen wir bis 2000 und wenn 2000 erreicht wurde, 
dann wird sie wieder zurück gesetzt.
Desweitern wird in der Routine i mit MODULO 5, 10, 100, usw. überprüft 
und entsprechende Variablen werden gesetzt. time_5ms.
Diese Variablen würde ich dann vor den Aufruf entsprechender Funktionen 
überprüfen. Zum Beispiel für einen Beep, time500ms und time2s ....

Versteht ihr meine Idee? Und geht es eventuell eleganter?

Gruß Peter

von Karl H. (kbuchegg)


Lesenswert?

Peter Max schrieb:

> Meine Idee war, dass ich in der Timer Interrupt Routine eine Zähl -
> Variable hochzähle, sagen wir bis 2000 und wenn 2000 erreicht wurde,
> dann wird sie wieder zurück gesetzt.

Die Idee ist schon mal gut.
Da du weißt, dass die Routine alle 0.05ms aufgerufen wird, vergehen 
logischerweise 2000 * 0.05 = 100ms bis diese Variable einmal von 0 bis 
2000 zählt. Machst du in dem if, der feststellt ob die 2000 erreicht 
sind, wieder irgendeine andere Aktion, dann wird diese Aktion 
folgerichtig alle 100ms ausgeführt.

> Desweitern wird in der Routine i mit MODULO 5, 10, 100, usw. überprüft
> und entsprechende Variablen werden gesetzt.

Kann man machen.
Ich kenne jetzt diesen speziellen Prozessor nicht. Allerdings solltest 
du darauf achten, dass Divisionen (auch Modulo ist im Grunde nichts 
anderes als eine Division) ohne Hardwareunterstützung zeitaufwändig sind 
und für den Prozessor eine ganz schöne Belastung darstellen.

Aber:
Nichts und niemand hindert dich daran, in Analogie zu deiner obigen Idee 
eine weitere Variable einzuführen, die du ebenfalls bei jedem Aufruf um 
1 erhöhst. Nur lässt du sie nicht bis 2000 zählen, sondern bis ...

... mal sehen.
Deine Interrupt Routine wird alle 0.05ms aufgerufen. Du möchtest ein 
Zeitsignal alle 5ms. D.h. wenn diese Variable bis 5/0.05 = 100 gezählt 
hat, sind genau 5ms vergangen.
1
  timer_5_ms ++;
2
  if( timer_5_ms == 100 ) {
3
    timer_5_ms = 0;
4
5
    // tu was immer es alle 5 ms zu tun gibt
6
  }
Und genau das gleiche kannst du mit noch weiteren Variablen machen und 
dir so von den 0.05ms jede beliebige Zeitbasis ableiten, solange sie nur 
ein Vielfaches von 0.05ms ist.

Du kannst diese Zähler auch verschachteln.
Deine erste Variable wird alle 100ms auf 0 zurückgesetzt. Machst du in 
diesem if einen weiteren Zähler, der zb bis 10 zählt, dann wird dieser 
Zähler alle 10*100ms = 1 Sekunde auf 0 zurückgesetzt. Und an dieses 
Rücksetz-if kannst du natürlich wieder andere Aktionen kopppeln
1
  timer_005_ms ++;
2
  if( timer_005_ms == 2000 ) {    // das ist alle 100 ms der Fall
3
    timer_005_ms = 0;
4
5
    timer_100_ms ++;
6
    if( timer_100_ms == 10 ) {    // das ist daher jede Sekunde der Fall
7
      timer_100_ms = 0;
8
9
      // mach die Dinge, die jede Sekunde zu erledigen sind
10
    }
11
  }

Und ich muss dich enttäuschen. So neu ist diese Idee gar nicht :-)
Du benutzt sie jeden Tag im täglichen Leben:
Deine Uhr erzeugt ein Sekundensignal. Nach 60 Sekunden wird die 
Sekundenzahl wieder auf 0 gesetzt und dafür 1 Minute gezählt. Nach 60 
Minuten geht die Minutenzahl wieder auf 0 und dafür gibt es 1 Stunde 
mehr. Nach 24 Stunden geht es wieder bei 0 Stunden weiter und dafür wird 
ein Tag gezählt, etc. etc.
1
   Sekunden++;
2
   if( Sekunden == 60 ) {
3
     Sekunden = 0;
4
5
     Minuten++;
6
     if( Minuten == 60 ) {
7
       Minuten = 0;
8
9
       Stunden++;
10
       if( Stunden == 24 ) {
11
         Stunden = 0;
12
13
         Wochentag++;
14
         if( Wochentag == 7 ) {
15
           Wochentag = 0;
16
17
         }
18
       }
19
     }
20
   }
Du kannst dich an jedem if dranhängen und so ganz leicht festlegen, ob 
Aktionen im Sekundentakt (auch zu bestimmten Sekunden) oder Minuten oder 
nur jede Stunde etc. passieren. Du kannst auch die Wochentage zu Wochen 
weiterführen. Oder anstelle von Wochentagen einfach nur Tage zählen. 
Wenn 30/31/28/29 Tage vergangen sind, wird dann ein Monat weitergezählt. 
Nach 12 Monaten 1 Jahr, etc. Was immer du willst.

Andere Zahlenwerte, aber gleiches Prinzip.

von Peter M. (sirpete83)


Lesenswert?

Ja vom Prinzip, war dass auch meine Idee.

Das mit dem Modulo bzw. Div sehe ich ein. Ich führe also nicht wie 
gedacht, mein ganzes Programm in der Interruptroutine aus sondern 
weiterhin per main.


also mein code sieht dann in etwa so aus:
1
int i = 0, timer_500ms = 0, timer_1s = 0, beep = 0;
2
3
void main(void)
4
{
5
  SetTimer_16bit();  // Timer, Interrupt, etc.
6
  beep_out(5);
7
  while (1);
8
}
9
10
void beep_out(int dauer)
11
{
12
  beep = dauer;
13
}
14
15
void Timerint(void) __interrupt 1
16
{
17
   timer_500_ms ++;
18
  if( timer_500_ms == 2000xxx )   // das ist alle 500 ms der Fall
19
    {    
20
      if beep (!= 0)
21
        P1_1 = !P1_1;
22
          else 
23
        P1_1 = 1;              // Definitiv High (aus).
24
       timer_50_ms = 0;
25
    }
26
    timer_1_s ++;
27
    if( timer_1_s == 10xxx ) {    // das ist daher jede Sekunde der Fall
28
      for (beep ; beep == 0 ; beep --);
29
      
30
      timer_1_s = 0;
31
    }
32
33
34
}

denke ich hab jetzt die entscheidende Idee bekommen, danke für deinen 
Denkanstoß.

von Volker S. (volkerschulz)


Lesenswert?

Die Benutzung des Modulus bringt noch weitere Nachteile mit sich! Wenn 
Du beispielsweise alle 0.05s bis 2000 inkrementierst und dann 
zuruecksetzt koenntest Du mit MOD 1000 zwar noch relativ einfach einen 
50ms-Trigger bauen, aber was machst Du, wenn Du nun einen 60ms-Trigger 
brauchst?

Das Hochzaehlen und Ruecksetzen von weiteren Variablen ist hier 
definitiv der bessere Weg.

Volker

von Karl H. (kbuchegg)


Lesenswert?

Peter Max schrieb:

> void Timerint(void) __interrupt 1
> {
>    timer_500_ms ++;
>   if( timer_500_ms == 2000xxx )   // das ist alle 500 ms der Fall
>     {
>       if beep (!= 0)
>         P1_1 = !P1_1;
>           else
>         P1_1 = 1;              // Definitiv High (aus).
>        timer_50_ms = 0;
>     }
>     timer_1_s ++;
>     if( timer_1_s == 10xxx ) {    // das ist daher jede Sekunde der Fall
>       for (beep ; beep == 0 ; beep --);

Das hier wird dir jeder bessere Compiler rauswerfen.
Das ist eine Schleife, die ausser Rechenzeit verbrauchen nichts macht.

Wenn die Absicht aber war, dass der Beep genau 'beep' Sekunden dauern 
soll, dann kannst du das so machen:

In der Funktion beep_out schaltest du den Beeper ein und merkst dir die 
gewünschte Zeitdauer in beep, wie gehabt.

Hier an dieser Stelle zählst du beep um 1 runter, denn es ist ja 1 
Sekunde vergangen. Ist beep danach 0, dann sind logischerweise seit dem 
Aufruf von beep_out genau die Anzahl an Sekunden vergangen, die beim 
Aufruf angegeben wurden.

        if( beep > 0 ) {
          beep--;
          if( beep == 0 )
            schalte Beeper aus
        }

>
>       timer_1_s = 0;
>     }
>
>
> }
> [/c]
>
> denke ich hab jetzt die entscheidende Idee bekommen, danke für deinen
> Denkanstoß.

Deine for-Schleife zeigt mir, dass du noch in Abläufen denkst. Du musst 
deine Denkweise umstellen.
Du musst denken: Jetzt ist so und soviel Zeit vergangen, was gibt es 
jetzt, genau zu diesem Zeitpunkt, zu tun. Das machst du dann und 
beendest den Interrupt. Insbesondere wartest du keine Zeitdauern. 
Zeitdauern werden festgelegt, indem ausserhalb des Interrupts eine 
Funktion eine Zeitdauer (beepe für x Sekunden) in einen Zeitpunkt 
umwandelt. Im Interrupt wird die aktuelle 'Zeit' hochgezählt (oder wie 
hier, eine Art 'Eieruhr' heruntergezählt) und wenn der vorher berechnete 
Zeitpunkt erreicht ist (oder wie hier die Eieruhr auf 0 heruntergezählt 
wurde), die zugehörige Aktion ausgeführt.
Denke also nicht in Einheiten von 'Der Prozessor muss die nächste Zeit 
dieses und jenes erledigen' sondern: ich lasse eine Aktion etwas 
starten; zu welchem Zeitpunkt muss mit einer anderen Aktion der Vorgang 
wieder gestoppt werden.

Also nicht sequentiell denken, sondern in Ereignissen.

von Peter M. (sirpete83)


Lesenswert?

Ok,
besonders meine for-Schleife ist doch eigentlich Käse. Die ist ja im 
Interrupt und würde nicht so funktionieren wie ich es vor habe.

Gut gut ... denke das ganze nochmal durch und dann geht es heute Abend 
eventuell weiter.

Gruß

von Peter M. (sirpete83)


Angehängte Dateien:

Lesenswert?

Hallo alle zusammen,
mit einiger Verspätung melde ich mich mit guten aber noch nicht 
perfekten Neuigkeiten.

Im Anhang findet ihr meinen Quellcode meines kleinen Alarmanlagen - 
Projektes.

Es klappt alles super, wenn da nicht doch ein paar Kleinigkeiten wären, 
die mich noch stören, oder die wie ich finde von mir noch nicht sehr 
elegant ( so gut es ginge, als Anfänger ) gelöst wurden oder werden 
können.

Damit ihr nicht den gesamten Quellcode studieren müsst hier eine kurze 
Beschreibung:

Der Interrupt zählt die einzelnen Variablen die dann die entsprechenden 
Funktionen ausführt, z.B. alle500ms ... usw.
Da es keine zeitkritische Anwendung ist stört es nicht wenn einige 
Sachen etwas später passieren. Aber jetzt kommt es, bei der Funktion 
alle15min ... da passiert es :-) der "Bug" ... der logischer weise 
keiner ist, weil die Funktion so wie ist völlig richtig arbeitet.

alle15min wird bisher nur für die Funktion verwendet, das Licht bei 
einem Alarm nach 15min wieder auszumachen, nun ... folgendes Szenario, 
nach dem aktivieren der Anlage, wird der Alarm nach 14 Min ausgelöst ... 
so und was passiert, das Licht wird bereits nach einer Minute 
deaktiviert.

Natürlich könnte ich die 15min Variable nur im Alarmfall hoch zählen, 
aber dann wäre der Fehler der unter der Verwendung der alle1min Funktion 
immer noch minus 59 Sekunden, oder ??
Gibt es hierfür noch eine elegantere Lösung?

Gruß Peter Max

von Karl H. (kbuchegg)


Lesenswert?

Peter Max schrieb:

> Der Interrupt zählt die einzelnen Variablen die dann die entsprechenden
> Funktionen ausführt, z.B. alle500ms ... usw.
> Da es keine zeitkritische Anwendung ist stört es nicht wenn einige
> Sachen etwas später passieren. Aber jetzt kommt es, bei der Funktion
> alle15min ... da passiert es :-) der "Bug" ... der logischer weise
> keiner ist, weil die Funktion so wie ist völlig richtig arbeitet.

Ja.
Da hab ich dich in etwas hinein-theatert.

Du kannst deine Zeitzähler ja zu jedem beliebigen Zeitpunkt wieder auf 0 
zurücksetzen.
Wenn dein Alarm auslöst, kannst du ja den 15 Minuten Zähler wieder auf 0 
setzen. Damit beginnen dann die 15 Minuten erneut genau zu diesem 
Zeitpunkt zu laufen, wenn ... ja wenn dieser Zähler nicht vom 5 Minuten 
Zähler abhängen würde.

> einem Alarm nach 15min wieder auszumachen, nun ... folgendes Szenario,
> nach dem aktivieren der Anlage, wird der Alarm nach 14 Min ausgelöst ...
> so und was passiert, das Licht wird bereits nach einer Minute
> deaktiviert.
>
> Natürlich könnte ich die 15min Variable nur im Alarmfall hoch zählen,
> aber dann wäre der Fehler der unter der Verwendung der alle1min Funktion
> immer noch minus 59 Sekunden, oder ??
> Gibt es hierfür noch eine elegantere Lösung?

Was ist das Problem?
Du weißt nie, in welchem 'Zwischenstand' sich die ganze Zählerkette 
befindet. Das ist so, wie wenn du eine Uhr benutzt und sagst, dass 1 
Minute vergangen ist, wenn sich die Minutenanzeige verändert. Ist dein 
erster Zeitpunkt genau bei 01 Sekunden, dann hast du fast keinen Fehler. 
Ist dein erster Zeitpunkt aber bei 59 Sekunden, dann hast du einen 
riesengroßen Fehler. Die Minute (nach der Def. dass eine Minute 
vergangen ist, wenn sich die Anzeige ändert) ist dann völlig falsch.

Aber du musst ja nicht die 15 Minuten abzählen, indem du abwartest bis 3 
mal 5 Minuten vergangen sind. Du weißt ja das 15 Minuten gleich 15*60 = 
900 Sekunden sind. Wenn du deinen 15 Minuten-Zähler also nach jeder 
Sekunde um 1 erhöhst und stattdessen bis 900 laufen lässt, dann ist der 
'Überlauf' auf 900 auch bei 15 Minuten, aber der Fehler ist dann maximal 
1 Sekunde. Und das sollte reichen.

Zu diesem Zwecke würde ich die Variable auch nicht timer_15_min nennen 
(auch wenn ich diesen Begriff so eingeführt habe) sondern den 
licht_timer, der alle 1 Sekunde um 1 erhöht wird und bis zu einer 
Obergrenze zählt und dann das Licht ausmacht. Willst du haben, dass 
diese Zeitdauer wieder von vorne anfängt, dann setzt du den licht_timer 
einfach wieder auf 0.

Die Funktion dieses Timers verändert sich dann weg von 'alle 15 Minuten 
ab einschalten der Anlage' hin zu '15 Minuten nachdem der Timer zuletzt 
auf 0 gesetzt wurde'. Und das ist ja letztenedes das Ziel. Egal aus 
welchem Grund das Licht eingeschaltet wurde (wer diesen Timer auf 0 
gesetzt hat), nach genau 15 Minuten (+- 1 Sekunde) geht es wieder aus.
1
#define LIGHT_DURATION  900  // 900 Sekunden = 15 Minuten
2
3
unsigned int light_time;
4
5
...
6
7
void Timer0_int(void) __interrupt 1  {            // Interrupt Service Routine von Timer0
8
  TL0 = 0x78;                        // Reset Timer Preload
9
  TH0 = 0xEC;
10
11
  time50_ms ++;
12
  
13
  if (time50_ms == 10){
14
      time50_ms = 0;
15
      time250_ms ++;
16
      alle50ms();
17
  };
18
19
  if (time250_ms == 5){
20
      time250_ms = 0;
21
      time500_ms ++;      
22
      alle250ms();  
23
  };
24
25
  if (time500_ms == 2){
26
      time500_ms = 0;
27
      time1_s ++;
28
      alle500ms();
29
30
      if( light_time < LIGHT_DURATION ) {
31
        light_time++;
32
        if( light_time == LIGHT_DURATION )
33
          // Licht ausschalten
34
      }
35
  }
36
37
  ...

Du solltest dir auch überlegen, dasselbe Schema auch auf andere Dinge 
anzuwenden. So gesehen hast du 2 verschiedene 'Arten' von Timern. Die 
einen ticken ständig und regelmässig vor sich hin, sobald die Anlage 
unter Spannung steht. Die anderen fangen mit ihrer Zählerei immer dann 
wieder von vorne an, wenn ihr zugehöriger Zähler auf 0 gesetzt wird.

Bei manchen Dingen (wie zb dem light_timer) ist es auch oft einfacher, 
die Zeit nicht hoch, sondern herunter zu zählen, wie eine Eieruhr. Der 
Wert in der Variablen sagt dir dann wieviel Zeit noch vergehen muss, bis 
das Ereignis eintreten wird.
Das kann zb dann sinnvoll sein, wenn dein Licht im Normalfall nach 15 
Minuten ausgehen soll, im Alarmfall aber 30 Minuten brennen soll. Du 
musst dann einfach nur die gewünschte Zeitdauer (in Sekunden) dem 
light_timer zuweisen und brauchst dich in der ISR nicht darum kümmern, 
welche Sonderfälle gelten. Ist der timer nicht 0, so wird er um 1 
heruntergezählt (das soll verhindern, dass der timer ins 'negative' 
zählt) und wenn der timer auf 0 runterkommt, dann wird ausgeschaltet. In 
der ISR ist es dabei völlig egal, mit welchem Wert der timer anfängt 
bzw. auf welchen Wert er zwischendurch (auch wenn er schon runtergezählt 
hat) gesetzt wird.

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.