www.mikrocontroller.net

Forum: Mikrocontroller und Digitale Elektronik Timer-Interrupt mit Schluckauf (ATmega328P)


Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hallo,

ich frage mich gerade ob, wenn man einen intern (!) getakteten Timer mit 
Interrupt verwendet, irgendwelche Pins dadurch beeinflusst werden bzw. 
irgendwelche Pins den Timer/Interrupt beeinflussen. Was ich erlebe, ist 
genau das. Einige Pins sind ja mit den Timer/Countern verbunden.

Ein konkretes Beispiel: wenn ich den Timer 2 im CTC und eine ISR auf 
TIMER2_COMPA_vect definiere, habe ich einen sauber laufenden Interrupt. 
Ein in der ISR getoggleter Pin (PD3) erzeugt im Oszilloskop ein stabiles 
Rechtecksignal.

PD3 im Oszi:
 _   _   _   _
| |_| |_| |_| |_

Wenn ich nun im main loop das Bit PD5 (welches als Output gesetzt ist) 
regelmäßig ändere, fängt das Rechtecksignal an zu zucken. Genauer 
betrachtet sieht man, dass anscheinend ISR-Aufrufe verschluckt werden.

PD3 im Oszi:
 ___   _     ___   _
|   |_| |___|   |_| |_
oder auch
 ___     ___     ___  
|   |___|   |___|   |___

Das Output-Pin (D5) funktioniert wie erwartet.

Die zusätzliche Funktionen für PD5 sind "T1/OC0B/PCINT21". PD3 hat 
"INT1/OC2B/PCINT19"

Hier die Timer-Initialisierung:
TCCR2A = (1<<WGM21); // CTC
TCCR2B = (1<<CS22) | (1<<CS21) | (1<<CS20); // /1024
OCR2A  = (F_CPU/1024)/100; // ~10 ms
TIMSK2  = (1 << OCIE2A);

Gibt es dafür eine schnelle Erklärung? Falls nicht kann ich gerne mehr 
Infos bzw. Sourcecode liefern.

Das ganze passiert im ATmega328 auf einem Arduino Duemilanove.

Danke und Gruß
Jonas

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Vielleicht sollte ich noch erwähnen, dass das beschriebene Verhalten 
nicht bei allen Pins auftritt. Ändere ich z.B. PD6 oder auch PB0 im main 
loop, tritt das Problem nicht auf. Bei  PD5 oder PD7 schon.

Jonas

Autor: Oliver Ju. (skriptkiddy)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Kompletten Quelltext bitte. Ich hab meine Glaskugel schon im Schubfach.

Autor: Peter Diener (pdiener) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Das ist offenbar ein read-modify-write Problem.
Das passiert, wenn die Pins nicht mit setbit oder clearbit geschaltet 
werden, sondern der Port in ein Pufferrgister eingelesen wird, ein Bit 
im Pufferregister geändert wird (z.B. getoggelt) und der Registerwert 
dann wieder auf den Port geschrieben wird.

Wenn der Interrupt eine read-modify-write Anweisung der Hauptschleife 
unterbricht und den Portwert selbst ändert, weiß die Hauptschleife davon 
nichts und schreibt einen veralteten Portwert aus dem Pufferegister auf 
den Port raus. Die Änderungen, die der Interrupt am Port vorgenommen 
hat, sind damit nach sehr kurzer Zeit überschrieben.

Schau dir mal das Assemblerlistung von deinem Programm an, ich bin mir 
fast zu 100% sicher, dass es genau so ein Problem ist.

Stichwort "atomarer Registerzugriff"!

Damit das funktioniert, muss im Hauptprogramm bei jedem nicht atomaren 
Zugriff auf Register, die der Interrupt verändern darf, der 
entsprechende Interrupt für die Zeit des Zugriffs gesperrt werden.

Grüße,

Peter

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Oh ja, mit cli() vor und sei() nach dem ändern der Bits in der 
Hauptschleife bleibts sauber!

Dann fehlten nicht wie ich vermutete Interrupts, sondern das 
"Rechteck-Bit" lieferte ein falsches Signal ans Oszilloskop.

Vielen Dank! Ich war die ganze Zeit auf der falschen Fährte...

Werd mir das Assemblerlisting noch ansehen.

Gruß
Jonas

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hier der Vollständigkeit halber noch ein reduzierter Quelltext zum 
reproduzieren:
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

ISR( TIMER2_COMPA_vect ) {
  PORTD ^= (1<<PD3);
}

int main(void) {

  DDRD |= (1<<PD5) | (1<<PD3);

  TCCR2A = (1<<WGM21); // CTC
  TCCR2B = (1<<CS22) | (1<<CS21) | (1<<CS20); // /1024
  OCR2A  = (F_CPU/1024)/100; // ~10 ms
  TIMSK2  = (1 << OCIE2A);

  sei();

  for (;;) {
    // ohne cli() und sei() = Problem     
    // cli();
    PORTD |= (0<<PD5);
    // sei();
  }

  return 0;
}

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Ist mir inzwischen wie Schuppen von den Augen gefallen.

Die Zeile:
PORTD |= (0<<PD5);

bedeutet ja:
PORTD = PORTD | (0<<PD5)

Also read-modify-write, wie du erkannt hast, Peter. Und der Interrupt 
funkte regelmäßig irgendwo ins 'modify' rein. Beim 'write' wurden dann 
die Änderungen des Interrups zunichte gemacht.

Das auch nur, weil es auf dem gleichen Port passierte, wenn auch 
(vermeintlich) verschiedene Bits betraf.

Danke nochmal!

Autor: Andreas Ferber (aferber)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Bei neueren AVRs (ATmega328P ist neu genug) gibt es noch eine andere 
Möglichkeit, wenn der Pin 5 nicht explizit auf 0 bzw. 1 gesetzt, sondern 
zwischen den beiden Zuständen umgeschaltet werden soll:
PIND = 1<<PD5;

Das gilt natürlich nur für den in main() gesetzten Pin, im Interrupt 
würde das nicht helfen.

Beachte: es ist volle Absicht, dass da kein '|' drinsteht!

Andreas

Autor: Falk Brunner (falk)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Klassisches Problem von nichtatomarem Zugriff, siehe Interrupt.

MFG
Falk

Autor: Spess53 (Gast)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hi

Nur am Rande bemerkt:

>PORTD |= (0<<PD5);

diese Zeile ist sinnlos.

MfG Spess

Autor: Karl Heinz (kbuchegg) (Moderator)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Jonas P. schrieb:
> Ist mir inzwischen wie Schuppen von den Augen gefallen.

>
> Die Zeile:
>
>
> PORTD |= (0<<PD5);
> 


Die Zeile sollte eigentlich
  PORTD |= (1<<PD5);

heissen. Mit einer 0 ist das recht sinnfrei.

Macht man es aber richtig, so erkennt der Compiler die Absicht und der 
Optimizer ersetzt das komplette Konstrukt durch einen Einzelbit-Setz 
Befehl, wodurch dann auch wieder alles Paletti ist. Das ist dann von 
Haus aus atomar.

Das ist dann wohl auch einer der Grund, warum dieses Problem in der 
Praxis nicht öfter auftritt.

Also:
korrekt schreiben
Optimizer aufdrehen

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Vielen Dank für die Kommentare.

>> PIND = 1<<PD5;

@Andreas: Ja stimmt, das habe ich auch im Datenblatt gelesen: "However, 
writing a logic one to a bit in the PINx Register, will result in a 
toggle in the corresponding bit in the Data Register.". Kann ganz 
nützlich sein.

>> diese Zeile ist sinnlos.

@Spess53: Ihr habt natürlich Recht. Lustigerweise trat das Problem nur 
mit "PORTD |= (0<<PD5);" auf. Bei "PORTD |= (1<<PD5);" nicht. Wie gesagt 
habe ich den Code aufs nötige reduziert um das Phänomoen zu 
reproduzieren. Bei "PORTD |= (1<<PD5);" tritt nehme ich an durch die 
Logik kein 'write' mehr auf. Wenn das Outputbit 5 die ganze Zeit den 
Wert 1 hat resultiert die Operation in "1 | 1" und bricht vor dem Prüfen 
des zweiten Operanden ab (Fall schon erfüllt, keine Modifikation nötig). 
Oder so ähnlich.

Eigentlich hatte ich ein funktionierendes Programm und wollte nur die 
Interruptfrequenz prüfen (durch toggeln eines Pins in der ISR). Dann war 
ich irritiert als das Ergebnis (scheinbar) so instabil war. Jetzt ist 
mir klar, dass ich mir und meinem Programm damit ein Bein gestellt habe.

Gruß
Jonas

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
>> ersetzt das komplette Konstrukt durch einen Einzelbit-Setz Befehl

Ja, oder so...

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Hab mich verzettelt. "@Spess53: Ihr habt natürlich Recht." sollte 
"@Spess53  und Karl Heinz: Ihr habt natürlich Recht." werden. Keine 
Hoheitsanrede... ;-)

Autor: Karl Heinz (kbuchegg) (Moderator)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Jonas P. schrieb:

> reproduzieren. Bei "PORTD |= (1<<PD5);" tritt nehme ich an durch die
> Logik kein 'write' mehr auf. Wenn das Outputbit 5 die ganze Zeit den
> Wert 1 hat resultiert die Operation in "1 | 1" und bricht vor dem Prüfen
> des zweiten Operanden ab (Fall schon erfüllt, keine Modifikation nötig).

Das wiedrrum kann nicht sein.
PORTD ist eine volatile Einheit. Und als solche muss der Compiler jeden 
Zugriff darauf auch durchführen.

> Oder so ähnlich.

Es ist schon so, dass dich hier der Optimizer rettet, indem er diesen 
Zugriff durch die Wahl der Assembler Operation atomar macht.

Was der allerdings macht bei


  PORTD |= (1<<PB0) | (1<<PB1);

weiß ich jetzt auch nicht.

Trotzdem danke für das Problem. Das es hier ein potentielles Problem 
gibt war mir bisher auch nicht bewusst.

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Karl heinz Buchegger schrieb:

> Das wiedrrum kann nicht sein.
> PORTD ist eine volatile Einheit. Und als solche muss der Compiler jeden
> Zugriff darauf auch durchführen.

Verstehe.

> Es ist schon so, dass dich hier der Optimizer rettet, indem er diesen
> Zugriff durch die Wahl der Assembler Operation atomar macht.

Ja, kann ich bestätigen. Hab es verifiziert.

>
> Was der allerdings macht bei
>
>
>   PORTD |= (1<<PB0) | (1<<PB1);
>
> weiß ich jetzt auch nicht.


Aus
PORTD |= (1<<0);
wird
sbi  0x0b, 0


Aus
PORTD |= (1<<0) | (1<<1);
wird
in  r24, 0x0b
ori  r24, 0x03
out  0x0b, r24


Aus
PORTD |= (0<<0);
wird
in  r24, 0x0b
out  0x0b, r24

(-Os und -O3 machen in allen Fällen kein Unterschied.)

>
> Trotzdem danke für das Problem. Das es hier ein potentielles Problem
> gibt war mir bisher auch nicht bewusst.

Hey, bitte gerne. Ich danke auch. :-)

Autor: Jonas P. (jox)
Datum:

Bewertung
0 lesenswert
nicht lesenswert
Von mir auch noch eine Randbemerkung:

Auch wenn Konstrukte wie
PORTD |= (0<<0);
"sinnfrei" erscheinen, sind sie durch Verwendung von Konventionen 
("value<<bit"-Schreibweise), Makros und Präprozessor doch alltäglich.

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.