Laufzeitverhalten main-Funktion

OP #6551906
Lesenswert?

Hallo Community,

ich habe ein komisches Laufzeit-Verhalten festgestellt bei der 
main-Funktion bzw. der darin befindlichen while(1) Schleife.

Einfaches Ziel: Timer0 löst alle 1ms einen Interrupt aus -> ein Flag 
wird gesetzt -> das Flag wird in der while-Schleife abgefragt und eine 
Aktion ausgeführt.

Hier mein Test-Programm:
1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3

4
#define PRELOAD_TIMER_FOR_1ms  131u
5

6
bool trigger_1ms;
7

8
static void init_system(void)
9
{
10
  DDRC = 0x18;  // Test-Outputs
11
  
12
  // Timer 0 Interrupt Konfig (1ms)
13
  TCCR0 |= (1<<CS01) | (1<<CS00);  // Presc 64
14
  TCNT0 = PRELOAD_TIMER_FOR_1ms;  // 8MHz / 64 = 125kHz -> Overflow @ 256 (=131+125) -> 1kHz (=1ms)
15
  TIMSK |= (1<<TOIE0);      // Overflow Interrupt Ein
16
  
17
  sei();              // Interrupts global Ein
18
}
19

20
int main(void)
21
{
22
    init_system();
23

24
    while(1)
25
    {
26
      if (trigger_1ms == true)
27
      {
28
        PORTC ^= (1<<PC3);
29
      }
30
      trigger_1ms = false;
31
    }
32
}
33

34
// Timer 0 @1ms
35
ISR (TIMER0_OVF_vect)  // Overflow 8-Bit timer
36
{
37
  PORTC ^= (1<<PC4);
38
  trigger_1ms = true;
39
  TCNT0 = PRELOAD_TIMER_FOR_1ms;
40
}

Wenn ich jetzt ein Oszilloskop an die Ausgänge anschließe, erhalte ich 
folgendes Verhalten:

PC4: (im 1-ms Takt)
1
  _   _   _   _   _
2
_| |_| |_| |_| |_| |  ....

PC3: (das ist das komische)
1
  _   _____   _
2
_| |_|     |_| |____  .... Das Muster wiederholt sich ständig.

CPU-Takt ist 8MHz interner Oszillator.

Ich hätte erwartet, dass PC5 dem PC4 folgt, denn 1ms sind ja Welten, um 
die 3 Zeilen Code auszuführen.

Hat jemand eine Erklärung dafür? An welcher Stelle stehe ich auf dem 
Schlauch?

Danke und viele Grüße
Gast #6551910
Lesenswert?

Hi,
wie sieht denn das Verhalten aus, wenn du das zurücksetzen von 
trigger_1ms mit in die Klammmer der if-Abfrage ziehst?
Vermutlich wird nämlichhäufig dein trigger_1ms zurückgesetzt bevor die 
if-Abfrage überhaupt stattfindet.
OP #6551913
Lesenswert?

Aahh ja klar, wenn die ISR zwischen If-Abfrage und der folgenden 
Anweisung kommt. Das macht Sinn. Allerdings ist es dafür aber seeehr 
regelmäßig.

Wenn ich das Rücksetzen von trigger_1ms in die if-Abfrage rein ziehe, 
geht PC3 gar nicht mehr an.... Warum das denn nun wieder?!?
OP #6551921
Lesenswert?

Perfekt, jetzt läuft es.
1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3

4
#define PRELOAD_TIMER_FOR_1ms  131u
5

6
volatile bool trigger_1ms;
7

8
static void init_system(void)
9
{
10
  DDRC = 0x18;
11
  
12
  // Timer 0 Interrupt Konfig (1ms)
13
  TCCR0 |= (1<<CS01) | (1<<CS00);  // Presc 64
14
  TCNT0 = PRELOAD_TIMER_FOR_1ms;  // 8MHz / 64 = 125kHz -> Overflow @ 256 (=131+125) -> 1kHz (=1ms)
15
  TIMSK |= (1<<TOIE0);      // Overflow Interrupt Ein
16
  
17
  sei();              // Interrupts global Ein
18
}
19

20
int main(void)
21
{
22
    init_system();
23

24
    while(1)
25
    {
26
      if (trigger_1ms == true)
27
      {
28
    PORTC ^= (1<<PC3);
29
    trigger_1ms = false;
30
      }
31
    }
32
}
33

34
// Timer 0 @1ms
35
ISR (TIMER0_OVF_vect)  // Overflow 8-Bit timer
36
{
37
  //cli();
38
  TCNT0 = PRELOAD_TIMER_FOR_1ms;
39
  //sei();
40
  PORTC ^= (1<<PC4);
41
  trigger_1ms = true;  
42
}

Ich habe
- trigger_1ms volatile gemacht
- ins if reingezogen
- und das neu Laden des Timers als erstes in der ISR ausgeführt 
(funktioniert mit und ohne das En-/Disablen der Interrupts)

Danke für die schnelle Hilfe!

Danke auch für die Hinweise! Für CTC müsste ich den Timer1 nehmen (wobei 
ich den bis jetzt mangels besseren Wissens für anderes aufsparen 
wollte). Nun bin ich schlauer :-)
Gast #6551933
Lesenswert?

> Perfekt

Nicht wirklich, da sind immer noch zwei Probleme:

a) ^= ist eine evtl nicht-atomare Read-Modify-Write-Operation, die von 
einer ISR unterbrochen werden kann.  Wenn die die gleichen Daten 
verändert, geht's schief: lese Register, ISR(toggelt Bit 4), schreibe 
Register mit altem Bit 4.  Wird zwar beim AVR oft zu einer atomaren 
Operation optimiert, aber eben nicht immer.

b) Die Abfrage in der main klappt nur, solange die Schleife oft genug 
abgearbeitet wird.  Wenn da mal Verzögerungen auftreten, verlierst du 
schnell einen Interrupt.


Btw, cli/sei in der ISR?  Besser nicht ...
Gast #6551982
Lesenswert?

Ste R. schrieb:
> - und das neu Laden des Timers als erstes in der ISR ausgeführt
> (funktioniert mit und ohne das En-/Disablen der Interrupts)

Damit werden deine Zeiten ungenau, falls aus anderen Gründen die 
Ausführung der ISR um eine Timertaktperiode oder mehr verzögert wird.
(Firma: EleLa - www.elela.de) #6552171
Lesenswert?

Ste R. schrieb:
> Ich habe
> - trigger_1ms volatile gemacht
> - ins if reingezogen
> - und das neu Laden des Timers als erstes in der ISR ausgeführt
> (funktioniert mit und ohne das En-/Disablen der Interrupts)
>
> Danke für die schnelle Hilfe!

Diese Lösung funktioniert zwar zufällig in diesem Fall, ist jedoch immer 
noch nicht wirklich interrupsicher.
Es ist immer noch möglich dass zwischen dem Setzen von PORTC und dem 
löschen von trigger_1ms der 1ms Interrupt erneut kommt. Es funktioniert 
nur zufällig in diesem Beispiel weil fast kein Code drin ist. Bei 
größeren Programmen kommt es sporadisch (1x in paar Tagen) zu 
Fehlfunktionen und man sucht sich einen Wolf.

Daher besser das machen:
1
volatile int trigger_1ms;
2
volatile int trigger_1ms_main;
3
int main(void)
4
{
5
  trigger_1ms = trigger_1ms_main = 0;
6
: : :
7
  while(1)
8
  {
9
    if (trigger_1ms != trigger_1ms_main)
10
    {
11
      PORTC ^= (1<<PC3);
12
      trigger_1ms_main++;
13
    }
14
  }
15
}
16

17
// Timer 0 @1ms
18
ISR (TIMER0_OVF_vect)  // Overflow 8-Bit timer
19
{
20
  //cli();
21
  TCNT0 = PRELOAD_TIMER_FOR_1ms;
22
  //sei();
23
  PORTC ^= (1<<PC4);
24
  trigger_1ms++;
25
}

Zur Erklärung:
Nur im Interrupt wird "trigger_1ms" geschrieben, in der Main nur 
gelesen. Damit umgeht man das Read-Modify-Write Problem und dass da 
dazwischen immer  Interrupts auftreten können. Schließlich kann ein 
Interrupt zu jeder Zeit unterbrechen, z.B. wenn eine C Codezeile in 
mehrere Assembler Befehle unterteilt wird, kann der Interrupt zwischen 
jedem einzelnen Assembler Befehl die main Routine unterbrechen.

PS: Der Portzugriff "C" ist damit immer noch nicht Interruptsicher.
Beitrag #6552199 wurde von einem Moderator gelöscht.
#6552206
Lesenswert?

Markus M. schrieb im Beitrag #6552199:
> Arduino Fanboy D. schrieb:
>> Wenn ich wissen dürfte, welcher AVR da zum Einsatz kommt, würde ich
>> sagen:PINC=(1<<PC3);
>> Toggelt den Pin auf eine wiedereintritsfähige Art und weise.
>
> Nein. Ich weiß jetzt nicht aus wie vielen Assembler Befehlen diese eine
> Codezeile besteht, jedoch garantiert mindestens aus 2 und damit ist es
> nicht Interruptsicher.

Da es um einen AVR geht:
  ldi r,PC3
  out PINC,r
Und das ist interruptfest.

Der Witz daran ist die atomare Eigenheit nicht zu alter AVRs, beim 
Schreiben auf den Input-Port jene Output-Pins zu toggeln, in die eine 1 
geschrieben wird.
OP #6552279
Lesenswert?

Hi :-)

Ich habe gelernt:
- dass meine Trigger-Variable voaltile sein muss, weil sie in der ISR 
verändert wird und in der zyklischen Main-Funktion benutzt wird. 
Offensichtlich hat der Compiler gemeint, hier optimieren zu dürfen, 
deshalb lief es zwischendurch nicht. Mit volatile ist das verhindert und 
läuft wie gewünscht.
- dass Interrupts en- und disablen in einer ISR Blödsinn ist (gut, um 
13:40Uhr merke ich das eher als 1:00Uhr nachts ;-)
- dass bei CTC Funktion ich nix am eigentlichen Zählerwert rumwurschteln 
muss, was die Fehleranfälligkeit verringert/verhindert. Deshalb CTC 
vorziehen vor Zähler vorbelegen
- Dass beim Toggeln von IO-Ports in Betracht gezogen werden muss, dass 
in der Anweisung eine ISR zuschlagen kann --> allerdings weniger 
relevant, da das Port-getoggle ja nur meinem Debugging diente und im 
finalen Programm nicht mehr drin ist. Aber trotzdem gut zu wissen.

Ansonsten weil die Fragen kamen:
- ich benutze den ATmega8
- Das Programm ist in C++

Das "Perfekt" oben galt den Hinweisen, nicht dass ich das Programm für 
perfekt hielt ;-)

VG
#6552288
Lesenswert?

Ste R. schrieb:
> Ich habe gelernt:

Das fehlt noch eine Erkenntnis.
1
while(1)
2
    {
3
      if (trigger_1ms == true)
4
      {
5
        PORTC ^= (1<<PC3);
6
      }
7
      trigger_1ms = false;
8
    }

Das dauerhafte, bedingungslose Löschen von trigger_1ms war falsch, denn 
das löscht die Varible oft, ohne daß die ausgewertet wurde! In diesem 
Beispiel dauert die while(1) Endllosschleife nur wenige Prozessortakte, 
sowas die Variable vielleicht mit 1 MHz immer wieder gelöscht wird! Aber 
auch bei deutlich langsamerer Schleife wäre es falsch, denn das 
Handshake zwischen ISR und Hauptprogramm ist dadurch im Eimer.

Merke. Ein Flag wird nur dann gelöscht, wenn es vorher als aktiv erkannt 
wurde!
(Firma: EleLa - www.elela.de) #6552294
Lesenswert?

Es kommt auf das Design drauf an. Sollte die Main Schleife mal länger 
benötigen als 1ms dann bleibt das ganze mit "while(1);" "Hängen".

Für kleine Programme sollte es kein Problem sein dass mindestens 1x je 
ms der Main-Loop läuft. Hingegen bei großen kann es durchaus mal länger 
dauern. Und dann funktioniert die 1-Variablen Variante nicht mehr.
Hingegen die 2-Variablen Variante zeigt einem wie viele Interrupts 
dazwischen waren und macht die Funktion so oft wie der Zähler != Zähler 
aus dem Interrupt ist.

Es kommt darauf an was man genau machen möchte, daher ist zwar die 
1-Variable-Variante gut, jedoch immer noch nicht 100% Interruptsicher, 
je nach Menge von Code der aus dem Main Loop sonst noch aufgerufen wird.

Beispiel Ablauf:

Main Programmbearbeitung aktiv...
Timer: Timer setzt signal
Main: Main Programm immer noch aktiv wird nach 999,9999µs jedoch fertig.
Main: Im Main wird das "Signal" erkannt -> springe in if()
Timer: Genau jetzt sind 1000,000µs vorbei und der Interrupt kommt erneut
Timer: das "signal" ist immer noch gesetzt
Main: ist jetzt kurz nach dem if() und löscht das "signal"
-> Somit ist das zweite Signal nicht mehr bearbeitet.

Dieses "Multitasking" Beispiel bleibt mit "while(1)" hängen, wenn die 
Bearbeitung der main länger als 1ms dauert, kann ja sein dass es 
beabsichtigt ist um zu sehen dass man "zu viel" programmiert hat.

Der TO wollte dass jeder Timer ISR auch in der main Schleife ausgegeben 
wird, also die gleiche Anzahl der Pulse. In dem Fall ist die 1-Variablen 
Variante nicht optimal.

Ob er dennoch die 1-Variablen Variante einsetzen möchte, da ihm der 
Seiteneffekt mit der Zykluszeit Überwachung nützlich ist, ist ihm 
überlassen.

(PS zu diesem Artikel:
https://www.mikrocontroller.net/articles/Multitasking#Verbesserter_Ansatz_mit_Timer)
#6552297
Lesenswert?

Markus M. schrieb:
> Es kommt auf das Design drauf an. Sollte die Main Schleife mal länger
> benötigen als 1ms dann bleibt das ganze mit "while(1);" "Hängen".

Nö, dann wird ein Timerzyklus übersprungen. Wie man es richtig macht, 
steht in meinem Link.

> Für kleine Programme sollte es kein Problem sein dass mindestens 1x je
> ms der Main-Loop läuft. Hingegen bei großen kann es durchaus mal länger
> dauern. Und dann funktioniert die 1-Variablen Variante nicht mehr.

Unfug!

> Hingegen die 2-Variablen Variante zeigt einem wie viele Interrupts
> dazwischen waren und macht die Funktion so oft wie der Zähler != Zähler
> aus dem Interrupt ist.

Jaja, du und deine Parallelwelt, alles schon 100mal diskutiert.

> Dieses "Multitasking" Beispiel bleibt mit "while(1)" hängen, wenn die
> Bearbeitung der main länger als 1ms dauert, kann ja sein dass es
> beabsichtigt ist um zu sehen dass man "zu viel" programmiert hat.

[ ] Du hast das Beispiel verstanden.

Probier's noch mal.

> Der TO wollte dass jeder Timer ISR auch in der main Schleife ausgegeben
> wird, also die gleiche Anzahl der Pulse. In dem Fall ist die 1-Variablen
> Variante nicht optimal.

Jaja, immer schön weiter plappern, bloß nicht mal zugeben, daß man sich 
geirrt hat. Erbärmlich!
Gast #6552305
Lesenswert?

>> Daher besser das machen:
>> volatile int trigger_1ms;
>> volatile int trigger_1ms_main;
>
> Sicher nicht. Die 2. Variable ist überflüssig und erzeugt nur Stress und
> Chaos.

Die Methode ist schon gut, wenn man sporadisch "verlorene" Interrupts 
nachholen möchte.  Ok, "volatile int" ist blöd (uint8_t wäre ok) und 
"trigger_1ms_main" braucht nicht volatile zu sein, aber ansonsten ok.

> Es reicht EINE Variable. Siehe Multitasking: Verbesserter Ansatz mit Timer

Das ist holprig und eigentlich nur zum Debuggen zu gebrauchen ...
(Firma: EleLa - www.elela.de) #6552315
Lesenswert?

1
    // Endlose Hauptschleife
2
    while (1) {
3
        if (flag_1ms) {
4
            flag_1ms=0;
5
            taste = taste_lesen();
6
            led_blinken(taste);
7
            uart_lesen();
8
            if (flag_1ms) {
9
                // Laufzeit der Tasks >1ms, Fehlersignalisierung
10
                // PB1 auf HIGH, Programm stoppen
11
                PORTB |= (1<<PB1);
12
                while(1);
13
            }
14
        }
15
    }
16

17

18
// Interruptserviceroutine für Timer 0
19
// hier 1ms
20
ISR(TIMER0_COMP_vect) {
21
    if (flag_1ms) {
22
        // Laufzeit der Tasks >2ms, Fehlersignalisierung
23
        // PB1 auf HIGH, Programm stoppen
24
        PORTB |= (1<<PB1);
25
        while(1);
26
    }
27
    flag_1ms = 1;
28
}

Falk B. schrieb:
> [ ] Du hast das Beispiel verstanden.

Ja, ich habe das Beispiel aus dem verlinkten Artikel verstanden. Damit 
wir auch wirklich vom gleichen reden habe ich mal den Codeabschnitt aus 
dem Artikel kopiert.
Durch das "while(1);" bleibt das dann "hängen".

Und nun erkläre mir doch bitte wie das ganze zu dem Wunsch vom TO passt.

Zu Erinnerung: TO wollte nur genau gleich viele Impulse wie im 
Interrupt, egal wie viel Arbeit der Main-Loop zu bewältigen hat.
Hingegen eine Zykluszeitüberwachung war nicht gefordert.

Nach wie vor:
Die Lösung von mir funktioniert immer.
Die Lösung von dir nur so lange der Main-Loop nicht zu viel Arbeit hat.

Klar, beide Lösungen funktionieren im aktuellen Quellcode, in dem man 
ein paar Codezeilen zum Test programmiert, jedoch wird man damit böse 
Erlebnisse erfahren wenn man mal was sehr großes macht.
#6552323
Lesenswert?

So ist es korrekt.
1
#include <stdbool.h>
2
#include <avr/io.h>
3
#include <avr/interrupt.h>
4

5
#define F_CPU 8000000   // CPU Takt, /Hz
6
#define F_TIMER 1000    // Timer-Takt, /Hz
7

8
volatile bool trigger_1ms;
9

10
static void init_system(void) {
11

12
  DDRC = (1<<PC4) | (1<<PC3);  // Test-Outputs
13
  
14
  // Timer 0 init
15
  // mode 2, CTC  Prescaler 64
16
  TCCR0  = (1<<WGM01) | (1<<CS01) | (1<<CS00);
17
  OCR0   = (F_CPU/(64L*F_TIMER)) - 1; 
18
  TIMSK |= (1<<OCIE0);      // Output compare Interrupt  
19
  sei();                    // Interrupts global Ein
20
}
21

22
int main(void) {
23
  init_system();
24

25
  while(1) {
26
    if (trigger_1ms) {
27
      trigger_1ms = false;
28
      // atomares Umschalten eine IO-Pins, funktioniert nur bei halbwegs "neuen" AVRs
29
      // mit PINx Toggle Funktion beim Schreibzugriff, siehe Datenblatt
30
      PINC = (1<<PC3);
31
      /*
32
      // atomares Umschalten für "alte" AVRs ohne PINx Toggle Funktionalität
33
      cli();
34
      PORTC ^= (1<<PC3);
35
      sei();
36
      */                
37
    }
38
  }
39
}
40

41
// Timer ISR, 1kHz
42

43
ISR (TIMER0_COMP_vect)  {
44
  PORTC ^= (1<<PC4);
45
  trigger_1ms = true;
46
}
Gast #6552333
Lesenswert?

> Die Lösung von mir funktioniert immer.

Naja, mit "immer" wäre ich vorsichtig ;-)  Wenn main konstant zu langsam 
ist fällt sie auf die Schnauze - es muß schon genügend Zeit vorhanden 
sein, die Interrupts nachzuholen.

Allerdings versucht sie, das Problem zu behandeln anstatt das Problem 
nur zu erkennen und dann den Dienst einzustellen.
#6552389
Lesenswert?

Arduino Fanboy D. schrieb:
> c99 kennt _Bool
> Und hier <stdbool.h> wird dann auch bool kreiert.

Würde es, wenn’s im Code stehen würde. Tut es aber nicht.

Ste R. schrieb:
> Ich habe gelernt:
> - dass meine Trigger-Variable voaltile sein muss, weil sie in der ISR
> verändert wird und in der zyklischen Main-Funktion benutzt wird.
> Offensichtlich hat der Compiler gemeint, hier optimieren zu dürfen,
> deshalb lief es zwischendurch nicht. Mit volatile ist das verhindert und
> läuft wie gewünscht.

Sowohl gcc 7.2.0 als auch 10.2.0 optimieren die Variable nicht weg. Das 
war also in dem Fall nicht das Problem.

Ste R. schrieb:
> - dass Interrupts en- und disablen in einer ISR Blödsinn ist (gut, um
> 13:40Uhr merke ich das eher als 1:00Uhr nachts ;-)

Es verbrät zwei Prozessorzyklen für nichts, Mahr geht dadurch aber nicht 
kaputt. Das war also in dem Fall nicht das Problem.


> - dass bei CTC Funktion ich nix am eigentlichen Zählerwert rumwurschteln
> muss, was die Fehleranfälligkeit verringert/verhindert. Deshalb CTC
> vorziehen vor Zähler vorbelegen

Das „rumwurschteln“ am Zähler wird dann ein Problem, wenn es direkt vor 
oder gleichzeitig mit Compare-Matches stattfindet. Da du die aber nicht 
nutzt, und den Zähler in einer OVF-ISR setzt, macht das bei dir nichts. 
Das war also in dem Fall nicht das Problem. Richtig ist natürlich, daß 
man für die Aufgabenstellung besser Den CTC-Mode benutzt, wenn 
vorhanden.

> - Dass beim Toggeln von IO-Ports in Betracht gezogen werden muss, dass
> in der Anweisung eine ISR zuschlagen kann --> allerdings weniger
> relevant, da das Port-getoggle ja nur meinem Debugging diente und im
> finalen Programm nicht mehr drin ist.

Das sollte man beachten, wobei bei allen neueren AVRs nach dem 
steinalten Mega8 die Operation atomar toggelt.

Das eigentliche Problem war die Behandlung des Flags.

Oliver

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